Merge branch 'jay/lsv2' of https://github.com/microsoft/PowerToys into jay/lsv2

This commit is contained in:
Jaylyn Barbee
2025-10-01 14:10:38 -04:00
31 changed files with 756 additions and 291 deletions

View File

@@ -210,6 +210,7 @@ capturevideosample
cmdow
Controlz
cortana
devhints
dlnilsson
fancymouse
firefox
@@ -229,6 +230,7 @@ regedit
roslyn
Skia
Spotify
tldr
Vanara
wangyi
WEX

View File

@@ -29,8 +29,8 @@ steps:
displayName: 'Touchdown Build - 37400, PRODEXT'
inputs:
teamId: 37400
TDBuildServiceConnection: $(TouchdownServiceConnection)
authType: SubjectNameIssuer
FederatedIdentityTDBuildServiceConnection: $(TouchdownServiceConnection)
authType: FederatedIdentityTDBuild
resourceFilePath: |
src\**\Resources.resx
src\**\Resource.resx

View File

@@ -32,7 +32,7 @@ parameters:
- name: enableMsBuildCaching
type: boolean
displayName: "Enable MSBuild Caching"
default: false
default: true
- name: runTests
type: boolean
displayName: "Run Tests"

View File

@@ -8,8 +8,8 @@ steps:
displayName: 'Download Localization Files -- PowerToys 37400'
inputs:
teamId: 37400
TDBuildServiceConnection: $(TouchdownServiceConnection)
authType: SubjectNameIssuer
FederatedIdentityTDBuildServiceConnection: $(TouchdownServiceConnection)
authType: FederatedIdentityTDBuild
resourceFilePath: |
**\Resources.resx
**\Resource.resx

View File

@@ -108,7 +108,7 @@
<PackageVersion Include="UnicodeInformation" Version="2.6.0" />
<PackageVersion Include="UnitsNet" Version="5.56.0" />
<PackageVersion Include="UTF.Unknown" Version="2.6.0" />
<PackageVersion Include="WinUIEx" Version="2.2.0" />
<PackageVersion Include="WinUIEx" Version="2.8.0" />
<PackageVersion Include="WPF-UI" Version="3.0.5" />
<PackageVersion Include="WyHash" Version="1.0.5" />
<PackageVersion Include="WixToolset.Heat" Version="5.0.2" />

View File

@@ -823,6 +823,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{3DCCD936
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LightSwitch.UITests", "src\modules\LightSwitch\Tests\LightSwitch.UITests\LightSwitch.UITests.csproj", "{F5333ED7-06D8-4AB3-953A-36D63F08CB6F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests\Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests.csproj", "{4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|ARM64 = Debug|ARM64
@@ -2985,6 +2987,14 @@ Global
{F5333ED7-06D8-4AB3-953A-36D63F08CB6F}.Release|x64.ActiveCfg = Release|x64
{F5333ED7-06D8-4AB3-953A-36D63F08CB6F}.Release|x64.Build.0 = Release|x64
{F5333ED7-06D8-4AB3-953A-36D63F08CB6F}.Release|x64.Deploy.0 = Release|x64
{4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}.Debug|ARM64.ActiveCfg = Debug|ARM64
{4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}.Debug|ARM64.Build.0 = Debug|ARM64
{4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}.Debug|x64.ActiveCfg = Debug|x64
{4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}.Debug|x64.Build.0 = Debug|x64
{4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}.Release|ARM64.ActiveCfg = Release|ARM64
{4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}.Release|ARM64.Build.0 = Release|ARM64
{4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}.Release|x64.ActiveCfg = Release|x64
{4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}.Release|x64.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -3312,6 +3322,7 @@ Global
{E816D7B4-4688-4ECB-97CC-3D8E798F3833} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
{3DCCD936-D085-4869-A1DE-CA6A64152C94} = {5B201255-53C8-490B-A34F-01F05D48A477}
{F5333ED7-06D8-4AB3-953A-36D63F08CB6F} = {3DCCD936-D085-4869-A1DE-CA6A64152C94}
{4E0FCF69-B06B-D272-76BF-ED3A559B4EDA} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0}

View File

@@ -50,6 +50,7 @@ Contact the developers of a plugin directly for assistance with a specific plugi
| [Hotkeys](https://github.com/ruslanlap/PowerToysRun-Hotkeys) | [ruslanlap](https://github.com/ruslanlap) | Create, manage, and trigger custom keyboard shortcuts directly from PowerToys Run. |
| [RandomGen](https://github.com/ruslanlap/PowerToysRun-RandomGen) | [ruslanlap](https://github.com/ruslanlap) | 🎲 Generate random data instantly with a single keystroke. Perfect for developers, testers, designers, and anyone who needs quick access to random data. Features include secure passwords, PINs, names, business data, dates, numbers, GUIDs, color codes, and more. Especially useful for designers who need random color codes and placeholder content. |
| [Open With Cursor](https://github.com/VictorNoxx/PowerToys-Run-Cursor/) | [VictorNoxx](https://github.com/VictorNoxx) | Open Visual Studio, VS Code recents with Cursor AI |
| [CheatSheets](https://github.com/ruslanlap/PowerToysRun-CheatSheets) | [ruslanlap](https://github.com/ruslanlap) | 📚 Find cheat sheets and command examples instantly from tldr pages, cheat.sh, and devhints.io. Features include favorites system, categories, offline mode, and smart caching. |
## Extending software plugins

View File

@@ -2,7 +2,10 @@
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<CmdPalVersion Condition="'$(CmdPalVersion)'=='' and '$(XES_APPXMANIFESTVERSION)'!=''">$(XES_APPXMANIFESTVERSION)</CmdPalVersion>
<!-- MIKE: The file you're looking for is src/modules/cmdpal/custom.props -->
<CmdPalVersion Condition="'$(CmdPalVersion)'==''">0.0.1.0</CmdPalVersion>
<DevEnvironment>Local</DevEnvironment>
<!-- Forcing for every DLL on by default -->

View File

@@ -1,5 +1,64 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">

View File

@@ -3,12 +3,13 @@
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Microsoft.CommandPalette.Extensions" Version="0.2.0" />
<PackageVersion Include="Microsoft.CommandPalette.Extensions" Version="0.5.250829002" />
<PackageVersion Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="9.0.0-preview.24508.2" />
<PackageVersion Include="Microsoft.Web.WebView2" Version="1.0.2903.40" />
<PackageVersion Include="Microsoft.Windows.CsWin32" Version="0.3.183" />
<PackageVersion Include="Microsoft.Windows.CsWinRT" Version="2.2.0" />
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.4188" />
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools.MSIX" Version="1.7.20250829.1" />
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.8.250907003" />
<PackageVersion Include="Shmuelie.WinRTServer" Version="2.1.1" />
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />

View File

@@ -40,10 +40,13 @@
<ItemGroup>
<PackageReference Include="Microsoft.CommandPalette.Extensions" />
<PackageReference Include="Microsoft.Windows.CsWinRT" />
<PackageReference Include="Microsoft.WindowsAppSDK" />
<PackageReference Include="Microsoft.Web.WebView2" />
<PackageReference Include="System.Text.Json" />
<PackageReference Include="Shmuelie.WinRTServer" />
<!-- Needed to enable building an MSIX package -->
<PackageReference Include="Microsoft.Windows.SDK.BuildTools.MSIX">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>
<!--
@@ -84,6 +87,9 @@
<!-- In Release builds, trimming is enabled by default.
feel free to disable this if needed -->
<PublishTrimmed>true</PublishTrimmed>
<!-- In release, also ignore the aforementioned ILLink warning -->
<ILLinkTreatWarningsAsErrors>false</ILLinkTreatWarningsAsErrors>
</PropertyGroup>

View File

@@ -2,9 +2,17 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.UI.Xaml;
namespace Microsoft.CmdPal.UI.Helpers;
internal static class BindTransformers
{
public static bool Negate(bool value) => !value;
public static Visibility EmptyToCollapsed(string? input)
=> string.IsNullOrEmpty(input) ? Visibility.Collapsed : Visibility.Visible;
public static Visibility EmptyOrWhitespaceToCollapsed(string? input)
=> string.IsNullOrWhiteSpace(input) ? Visibility.Collapsed : Visibility.Visible;
}

View File

@@ -41,6 +41,31 @@
FalseValue="Visible"
TrueValue="Collapsed" />
<Style
x:Key="DetailKeyTextBlockStyle"
BasedOn="{StaticResource CaptionTextBlockStyle}"
TargetType="TextBlock">
<Setter Property="IsTextSelectionEnabled" Value="True" />
<Setter Property="TextWrapping" Value="WrapWholeWords" />
<Setter Property="Foreground" Value="{ThemeResource TextFillColorSecondaryBrush}" />
</Style>
<Style
x:Key="SeparatorKeyTextBlockStyle"
BasedOn="{StaticResource BodyStrongTextBlockStyle}"
TargetType="TextBlock">
<Setter Property="IsTextSelectionEnabled" Value="True" />
<Setter Property="TextWrapping" Value="WrapWholeWords" />
</Style>
<Style
x:Key="DetailValueTextBlockStyle"
BasedOn="{StaticResource BodyTextBlockStyle}"
TargetType="TextBlock">
<Setter Property="IsTextSelectionEnabled" Value="True" />
<Setter Property="TextWrapping" Value="WrapWholeWords" />
</Style>
<DataTemplate x:Key="TagTemplate" x:DataType="coreViewModels:TagViewModel">
<cpcontrols:Tag
HorizontalAlignment="Left"
@@ -68,7 +93,7 @@
Margin="0,3,8,0"
SourceKey="{x:Bind Icon, Mode=OneWay}"
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}" />
<TextBlock Text="{x:Bind Name}" />
<TextBlock Style="{StaticResource BodyTextBlockStyle}" Text="{x:Bind Name}" />
</StackPanel>
</Button>
</StackPanel>
@@ -76,20 +101,13 @@
<DataTemplate x:Key="DetailsLinkTemplate" x:DataType="coreViewModels:DetailsLinkViewModel">
<StackPanel Orientation="Vertical">
<TextBlock Style="{StaticResource DetailKeyTextBlockStyle}" Text="{x:Bind Key, Mode=OneWay}" />
<TextBlock
IsTextSelectionEnabled="True"
Text="{x:Bind Key, Mode=OneWay}"
TextWrapping="WrapWholeWords" />
<TextBlock
FontSize="12"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
IsTextSelectionEnabled="True"
Style="{StaticResource DetailValueTextBlockStyle}"
Text="{x:Bind Text, Mode=OneWay}"
TextWrapping="WrapWholeWords"
Visibility="{x:Bind IsText, Mode=OneWay}" />
<HyperlinkButton
Padding="0"
FontSize="12"
NavigateUri="{x:Bind Link, Mode=OneWay}"
Visibility="{x:Bind IsLink, Mode=OneWay}">
<TextBlock Text="{x:Bind Text, Mode=OneWay}" TextWrapping="Wrap" />
@@ -98,10 +116,7 @@
</DataTemplate>
<DataTemplate x:Key="DetailsCommandsTemplate" x:DataType="coreViewModels:DetailsCommandsViewModel">
<StackPanel Orientation="Vertical" Spacing="4">
<TextBlock
IsTextSelectionEnabled="True"
Text="{x:Bind Key, Mode=OneWay}"
TextWrapping="WrapWholeWords" />
<TextBlock Style="{StaticResource DetailKeyTextBlockStyle}" Text="{x:Bind Key, Mode=OneWay}" />
<ItemsControl
ItemTemplate="{StaticResource CommandTemplate}"
ItemsSource="{x:Bind Commands, Mode=OneWay}"
@@ -111,24 +126,20 @@
<DataTemplate x:Key="DetailsSeparatorTemplate" x:DataType="coreViewModels:DetailsSeparatorViewModel">
<StackPanel Margin="0,8,8,0" Orientation="Vertical">
<Border
Margin="8,0,0,0"
BorderBrush="{ThemeResource TextFillColorSecondaryBrush}"
Margin="0,0,0,0"
BorderBrush="{ThemeResource DividerStrokeColorDefaultBrush}"
BorderThickness="0,0,0,2">
<TextBlock
Margin="-8,0,0,8"
FontWeight="SemiBold"
IsTextSelectionEnabled="True"
Margin="0,0,0,0"
Style="{StaticResource SeparatorKeyTextBlockStyle}"
Text="{x:Bind Key, Mode=OneWay}"
TextWrapping="WrapWholeWords" />
Visibility="{x:Bind help:BindTransformers.EmptyOrWhitespaceToCollapsed(Key), FallbackValue=Collapsed}" />
</Border>
</StackPanel>
</DataTemplate>
<DataTemplate x:Key="DetailsTagsTemplate" x:DataType="coreViewModels:DetailsTagsViewModel">
<StackPanel Orientation="Vertical" Spacing="4">
<TextBlock
IsTextSelectionEnabled="True"
Text="{x:Bind Key, Mode=OneWay}"
TextWrapping="WrapWholeWords" />
<TextBlock Style="{StaticResource DetailKeyTextBlockStyle}" Text="{x:Bind Key, Mode=OneWay}" />
<ItemsControl
ItemTemplate="{StaticResource TagTemplate}"
ItemsSource="{x:Bind Tags, Mode=OneWay}"

View File

@@ -24,10 +24,11 @@
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TitleBar x:Name="TitleBar">
<TitleBar x:Name="AppTitleBar" PaneToggleRequested="AppTitleBar_PaneToggleRequested">
<!-- This is a workaround for https://github.com/microsoft/microsoft-ui-xaml/issues/10374, once fixed we should just be using IconSource -->
<TitleBar.LeftHeader>
<ImageIcon
x:Name="WorkAroundIcon"
Height="16"
Margin="16,0,0,0"
Source="ms-appx:///Assets/icon.svg" />
@@ -36,18 +37,19 @@
<NavigationView
x:Name="NavView"
Grid.Row="1"
CompactModeThresholdWidth="1007"
DisplayModeChanged="NavView_DisplayModeChanged"
ExpandedModeThresholdWidth="1007"
IsBackButtonVisible="Collapsed"
IsPaneToggleButtonVisible="False"
IsSettingsVisible="False"
ItemInvoked="NavView_ItemInvoked"
Loaded="NavView_Loaded"
OpenPaneLength="200">
Loaded="NavView_Loaded">
<NavigationView.Resources>
<SolidColorBrush x:Key="NavigationViewContentBackground" Color="Transparent" />
<SolidColorBrush x:Key="NavigationViewContentGridBorderBrush" Color="Transparent" />
<Thickness x:Key="NavigationViewHeaderMargin">15,0,0,0</Thickness>
</NavigationView.Resources>
<NavigationView.MenuItems>
<NavigationViewItem
x:Uid="Settings_GeneralPage_NavigationViewItem_General"
@@ -58,34 +60,32 @@
Icon="{ui:FontIcon Glyph=&#xEA86;}"
Tag="Extensions" />
</NavigationView.MenuItems>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<BreadcrumbBar
x:Name="NavigationBreadcrumbBar"
Grid.Row="0"
MaxWidth="1000"
ItemClicked="NavigationBreadcrumbBar_ItemClicked"
ItemsSource="{x:Bind BreadCrumbs, Mode=OneWay}">
<BreadcrumbBar.ItemTemplate>
<DataTemplate x:DataType="local:Crumb">
<TextBlock Text="{x:Bind Label, Mode=OneWay}" />
</DataTemplate>
</BreadcrumbBar.ItemTemplate>
<BreadcrumbBar.Resources>
<ResourceDictionary>
<x:Double x:Key="BreadcrumbBarItemThemeFontSize">28</x:Double>
<Thickness x:Key="BreadcrumbBarChevronPadding">7,4,8,0</Thickness>
<FontWeight x:Key="BreadcrumbBarItemFontWeight">SemiBold</FontWeight>
<x:Double x:Key="BreadcrumbBarChevronFontSize">16</x:Double>
</ResourceDictionary>
</BreadcrumbBar.Resources>
</BreadcrumbBar>
<Grid Padding="16,0">
<BreadcrumbBar
x:Name="NavigationBreadcrumbBar"
MaxWidth="1000"
ItemClicked="NavigationBreadcrumbBar_ItemClicked"
ItemsSource="{x:Bind BreadCrumbs, Mode=OneWay}">
<BreadcrumbBar.ItemTemplate>
<DataTemplate x:DataType="local:Crumb">
<TextBlock Text="{x:Bind Label, Mode=OneWay}" />
</DataTemplate>
</BreadcrumbBar.ItemTemplate>
<BreadcrumbBar.Resources>
<ResourceDictionary>
<x:Double x:Key="BreadcrumbBarItemThemeFontSize">28</x:Double>
<Thickness x:Key="BreadcrumbBarChevronPadding">7,4,8,0</Thickness>
<FontWeight x:Key="BreadcrumbBarItemFontWeight">SemiBold</FontWeight>
<x:Double x:Key="BreadcrumbBarChevronFontSize">16</x:Double>
</ResourceDictionary>
</BreadcrumbBar.Resources>
</BreadcrumbBar>
</Grid>
<Frame x:Name="NavFrame" Grid.Row="1" />
</Grid>
</NavigationView>

View File

@@ -14,6 +14,7 @@ using Microsoft.UI.Xaml.Automation.Peers;
using Microsoft.UI.Xaml.Controls;
using WinUIEx;
using RS_ = Microsoft.CmdPal.UI.Helpers.ResourceLoaderInstance;
using TitleBar = Microsoft.UI.Xaml.Controls.TitleBar;
namespace Microsoft.CmdPal.UI.Settings;
@@ -34,7 +35,7 @@ public sealed partial class SettingsWindow : WindowEx,
var title = RS_.GetString("SettingsWindowTitle");
this.AppWindow.Title = title;
this.AppWindow.TitleBar.PreferredHeightOption = TitleBarHeightOption.Tall;
this.TitleBar.Title = title;
this.AppTitleBar.Title = title;
PositionCentered();
WeakReferenceMessenger.Default.Register<NavigateToExtensionSettingsMessage>(this);
@@ -142,11 +143,13 @@ public sealed partial class SettingsWindow : WindowEx,
{
if (args.DisplayMode == NavigationViewDisplayMode.Compact || args.DisplayMode == NavigationViewDisplayMode.Minimal)
{
NavView.IsPaneToggleButtonVisible = false;
AppTitleBar.IsPaneToggleButtonVisible = true;
WorkAroundIcon.Margin = new Thickness(8, 0, 16, 0); // Required for workaround, see XAML comment
}
else
{
NavView.IsPaneToggleButtonVisible = true;
AppTitleBar.IsPaneToggleButtonVisible = false;
WorkAroundIcon.Margin = new Thickness(16, 0, 0, 0); // Required for workaround, see XAML comment
}
}
@@ -155,6 +158,11 @@ public sealed partial class SettingsWindow : WindowEx,
// This might come in on a background thread
DispatcherQueue.TryEnqueue(() => Close());
}
private void AppTitleBar_PaneToggleRequested(TitleBar sender, object args)
{
NavView.IsPaneOpen = !NavView.IsPaneOpen;
}
}
public readonly struct Crumb

View File

@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- Look at Directory.Build.props in root for common stuff as well -->
<Import Project="..\..\..\..\Common.Dotnet.CsWinRT.props" />
<PropertyGroup>
<IsPackable>false</IsPackable>
<RootNamespace>Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests</RootNamespace>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Moq" />
<PackageReference Include="MSTest" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\ext\Microsoft.CmdPal.Ext.ClipboardHistory\Microsoft.CmdPal.Ext.ClipboardHistory.csproj" />
<ProjectReference Include="..\Microsoft.CmdPal.Ext.UnitTestsBase\Microsoft.CmdPal.Ext.UnitTestBase.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,274 @@
// 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.Ext.ClipboardHistory.Helpers;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests;
[TestClass]
public class UrlHelperTests
{
[TestMethod]
[DataRow(null)]
[DataRow("")]
[DataRow(" ")]
[DataRow("\t")]
[DataRow("\r\n")]
public void IsValidUrl_ReturnsFalse_WhenUrlIsNullOrWhitespace(string url)
{
// Act
var result = UrlHelper.IsValidUrl(url);
// Assert
Assert.IsFalse(result);
}
[TestMethod]
[DataRow("test\nurl")]
[DataRow("test\rurl")]
[DataRow("http://example.com\nmalicious")]
[DataRow("https://test.com\r\nheader")]
public void IsValidUrl_ReturnsFalse_WhenUrlContainsNewlines(string url)
{
// Act
var result = UrlHelper.IsValidUrl(url);
// Assert
Assert.IsFalse(result);
}
[TestMethod]
[DataRow("com")]
[DataRow("org")]
[DataRow("localhost")]
[DataRow("test")]
[DataRow("http")]
[DataRow("https")]
public void IsValidUrl_ReturnsFalse_WhenUrlDoesNotContainDot(string url)
{
// Act
var result = UrlHelper.IsValidUrl(url);
// Assert
Assert.IsFalse(result);
}
[TestMethod]
[DataRow("https://www.example.com")]
[DataRow("http://test.org")]
[DataRow("ftp://files.example.net")]
[DataRow("file://localhost/path/to/file.txt")]
[DataRow("https://subdomain.example.co.uk")]
[DataRow("http://192.168.1.1")]
[DataRow("https://example.com:8080/path")]
public void IsValidUrl_ReturnsTrue_WhenUrlIsWellFormedAbsolute(string url)
{
// Act
var result = UrlHelper.IsValidUrl(url);
// Assert
Assert.IsTrue(result);
}
[TestMethod]
[DataRow("www.example.com")]
[DataRow("example.org")]
[DataRow("subdomain.test.net")]
[DataRow("github.com/user/repo")]
[DataRow("stackoverflow.com/questions/123")]
[DataRow("192.168.1.1")]
public void IsValidUrl_ReturnsTrue_WhenUrlIsValidWithoutProtocol(string url)
{
// Act
var result = UrlHelper.IsValidUrl(url);
// Assert
Assert.IsTrue(result);
}
[TestMethod]
[DataRow("not a url")]
[DataRow("invalid..url")]
[DataRow("http://")]
[DataRow("https://")]
[DataRow("://example.com")]
[DataRow("ht tp://example.com")]
public void IsValidUrl_ReturnsFalse_WhenUrlIsInvalid(string url)
{
// Act
var result = UrlHelper.IsValidUrl(url);
// Assert
Assert.IsFalse(result);
}
[TestMethod]
[DataRow(" https://www.example.com ")]
[DataRow("\t\tgithub.com\t\t")]
[DataRow(" \r\n stackoverflow.com \r\n ")]
public void IsValidUrl_TrimsWhitespace_BeforeValidation(string url)
{
// Act
var result = UrlHelper.IsValidUrl(url);
// Assert
Assert.IsTrue(result);
}
[TestMethod]
[DataRow("tel:+1234567890")]
[DataRow("javascript:alert('test')")]
public void IsValidUrl_ReturnsFalse_ForNonWebProtocols(string url)
{
// Act
var result = UrlHelper.IsValidUrl(url);
// Assert
Assert.IsFalse(result);
}
[TestMethod]
[DataRow(null)]
[DataRow("")]
[DataRow(" ")]
public void NormalizeUrl_ReturnsInput_WhenUrlIsNullOrWhitespace(string url)
{
// Act
var result = UrlHelper.NormalizeUrl(url);
// Assert
Assert.AreEqual(url, result);
}
[TestMethod]
[DataRow("https://www.example.com")]
[DataRow("http://test.org")]
[DataRow("ftp://files.example.net")]
[DataRow("file://localhost/path/to/file.txt")]
public void NormalizeUrl_ReturnsUnchanged_WhenUrlIsAlreadyWellFormed(string url)
{
// Act
var result = UrlHelper.NormalizeUrl(url);
// Assert
Assert.AreEqual(url, result);
}
[TestMethod]
[DataRow("www.example.com", "https://www.example.com")]
[DataRow("example.org", "https://example.org")]
[DataRow("github.com/user/repo", "https://github.com/user/repo")]
[DataRow("stackoverflow.com/questions/123", "https://stackoverflow.com/questions/123")]
public void NormalizeUrl_AddsHttpsPrefix_WhenNoProtocolPresent(string input, string expected)
{
// Act
var result = UrlHelper.NormalizeUrl(input);
// Assert
Assert.AreEqual(expected, result);
}
[TestMethod]
[DataRow(" www.example.com ", "https://www.example.com")]
[DataRow("\t\tgithub.com\t\t", "https://github.com")]
[DataRow(" \r\n stackoverflow.com \r\n ", "https://stackoverflow.com")]
public void NormalizeUrl_TrimsWhitespace_BeforeNormalizing(string input, string expected)
{
// Act
var result = UrlHelper.NormalizeUrl(input);
// Assert
Assert.AreEqual(expected, result);
}
[TestMethod]
[DataRow(@"C:\Users\Test\Documents\file.txt")]
[DataRow(@"D:\Projects\MyProject\readme.md")]
[DataRow(@"E:\")]
[DataRow(@"F:")]
[DataRow(@"G:\folder\subfolder")]
public void IsValidUrl_ReturnsTrue_ForValidLocalPaths(string path)
{
// Act
var result = UrlHelper.IsValidUrl(path);
// Assert
Assert.IsTrue(result);
}
[TestMethod]
[DataRow(@"\\server\share")]
[DataRow(@"\\server\share\folder")]
[DataRow(@"\\192.168.1.100\public")]
[DataRow(@"\\myserver\documents\file.docx")]
[DataRow(@"\\domain.com\share\folder\file.pdf")]
public void IsValidUrl_ReturnsTrue_ForValidNetworkPaths(string path)
{
// Act
var result = UrlHelper.IsValidUrl(path);
// Assert
Assert.IsTrue(result);
}
[TestMethod]
[DataRow(@"\\")]
[DataRow(@":")]
[DataRow(@"Z")]
[DataRow(@"folder")]
[DataRow(@"folder\file.txt")]
[DataRow(@"documents\project\readme.md")]
[DataRow(@"./config/settings.json")]
[DataRow(@"../data/input.csv")]
public void IsValidUrl_ReturnsFalse_ForInvalidPathsAndRelativePaths(string path)
{
// Act
var result = UrlHelper.IsValidUrl(path);
// Assert
Assert.IsFalse(result);
}
[TestMethod]
[DataRow(@"C:\Users\Test\Documents\file.txt")]
[DataRow(@"D:\Projects\MyProject")]
[DataRow(@"E:\")]
public void NormalizeUrl_ConvertsLocalPathToFileUri_WhenValidLocalPath(string path)
{
// Act
var result = UrlHelper.NormalizeUrl(path);
// Assert
Assert.IsTrue(result.StartsWith("file:///", StringComparison.OrdinalIgnoreCase));
Assert.IsTrue(result.Contains(path.Replace('\\', '/')));
}
[TestMethod]
[DataRow(@"\\server\share")]
[DataRow(@"\\192.168.1.100\public")]
[DataRow(@"\\myserver\documents")]
public void NormalizeUrl_ConvertsNetworkPathToFileUri_WhenValidNetworkPath(string path)
{
// Act
var result = UrlHelper.NormalizeUrl(path);
// Assert
Assert.IsTrue(result.StartsWith("file://", StringComparison.OrdinalIgnoreCase));
Assert.IsTrue(result.Contains(path.Replace('\\', '/')));
}
[TestMethod]
[DataRow("file:///C:/Users/Test/file.txt")]
[DataRow("file://server/share/folder")]
public void NormalizeUrl_ReturnsUnchanged_WhenAlreadyFileUri(string fileUri)
{
// Act
var result = UrlHelper.NormalizeUrl(fileUri);
// Assert
Assert.AreEqual(fileUri, result);
}
}

View File

@@ -23,6 +23,7 @@ public class NormalizeCommandLineTests : CommandPaletteUnitTestBase
[DataRow("ping bing.com", "c:\\Windows\\system32\\ping.exe", "bing.com")]
[DataRow("curl bing.com", "c:\\Windows\\system32\\curl.exe", "bing.com")]
[DataRow("ipconfig /all", "c:\\Windows\\system32\\ipconfig.exe", "/all")]
[DataRow("ipconfig a b \"c d\"", "c:\\Windows\\system32\\ipconfig.exe", "a b \"c d\"")]
public void NormalizeCommandLineSimple(string input, string expectedExe, string expectedArgs = "")
{
NormalizeTestCore(input, expectedExe, expectedArgs);
@@ -46,7 +47,7 @@ public class NormalizeCommandLineTests : CommandPaletteUnitTestBase
[TestMethod]
[DataRow("cmd --run --test", "C:\\Windows\\System32\\cmd.exe", "--run --test")]
[DataRow("cmd --run --test ", "C:\\Windows\\System32\\cmd.exe", "--run --test")]
[DataRow("cmd \"--run --test\" --pass", "C:\\Windows\\System32\\cmd.exe", "--run --test --pass")]
[DataRow("cmd \"--run --test\" --pass", "C:\\Windows\\System32\\cmd.exe", "\"--run --test\" --pass")]
public void NormalizeArgsWithSpaces(string input, string expectedExe, string expectedArgs = "")
{
NormalizeTestCore(input, expectedExe, expectedArgs);

View File

@@ -5,7 +5,7 @@
<XesUseOneStoreVersioning>true</XesUseOneStoreVersioning>
<XesBaseYearForStoreVersion>2025</XesBaseYearForStoreVersion>
<VersionMajor>0</VersionMajor>
<VersionMinor>5</VersionMinor>
<VersionMinor>6</VersionMinor>
<VersionInfoProductName>Microsoft Command Palette</VersionInfoProductName>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,144 @@
// 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.IO;
namespace Microsoft.CmdPal.Ext.ClipboardHistory.Helpers;
internal static class UrlHelper
{
/// <summary>
/// Validates if a string is a valid URL or file path
/// </summary>
/// <param name="url">The string to validate</param>
/// <returns>True if the string is a valid URL or file path, false otherwise</returns>
internal static bool IsValidUrl(string url)
{
if (string.IsNullOrWhiteSpace(url))
{
return false;
}
// Trim whitespace for validation
url = url.Trim();
// URLs should not contain newlines
if (url.Contains('\n', StringComparison.Ordinal) || url.Contains('\r', StringComparison.Ordinal))
{
return false;
}
// Check if it's a valid file path (local or network)
if (IsValidFilePath(url))
{
return true;
}
if (!url.Contains('.', StringComparison.OrdinalIgnoreCase))
{
// eg: 'com', 'org'. We don't think it's a valid url.
// This can simplify the logic of checking if the url is valid.
return false;
}
if (Uri.IsWellFormedUriString(url, UriKind.Absolute))
{
return true;
}
if (!url.StartsWith("http://", StringComparison.OrdinalIgnoreCase) &&
!url.StartsWith("https://", StringComparison.OrdinalIgnoreCase) &&
!url.StartsWith("ftp://", StringComparison.OrdinalIgnoreCase) &&
!url.StartsWith("file://", StringComparison.OrdinalIgnoreCase))
{
if (Uri.IsWellFormedUriString("https://" + url, UriKind.Absolute))
{
return true;
}
}
return false;
}
/// <summary>
/// Normalizes a URL or file path by adding appropriate schema if none is present
/// </summary>
/// <param name="url">The URL or file path to normalize</param>
/// <returns>Normalized URL or file path with schema</returns>
internal static string NormalizeUrl(string url)
{
if (string.IsNullOrWhiteSpace(url))
{
return url;
}
// Trim whitespace
url = url.Trim();
// If it's a valid file path, convert to file:// URI
if (IsValidFilePath(url) && !url.StartsWith("file://", StringComparison.OrdinalIgnoreCase))
{
try
{
// Convert to file URI (path is already absolute since we only accept absolute paths)
return new Uri(url).ToString();
}
catch
{
// If conversion fails, return original
return url;
}
}
if (!Uri.IsWellFormedUriString(url, UriKind.Absolute))
{
if (!url.StartsWith("http://", StringComparison.OrdinalIgnoreCase) &&
!url.StartsWith("https://", StringComparison.OrdinalIgnoreCase) &&
!url.StartsWith("ftp://", StringComparison.OrdinalIgnoreCase) &&
!url.StartsWith("file://", StringComparison.OrdinalIgnoreCase))
{
url = "https://" + url;
}
}
return url;
}
/// <summary>
/// Checks if a string represents a valid file path (local or network)
/// </summary>
/// <param name="path">The string to check</param>
/// <returns>True if the string is a valid file path, false otherwise</returns>
private static bool IsValidFilePath(string path)
{
if (string.IsNullOrWhiteSpace(path))
{
return false;
}
try
{
// Check for UNC paths (network paths starting with \\)
if (path.StartsWith(@"\\", StringComparison.Ordinal))
{
// Basic UNC path validation: \\server\share or \\server\share\path
var parts = path.Substring(2).Split('\\', StringSplitOptions.RemoveEmptyEntries);
return parts.Length >= 2; // At minimum: server and share
}
// Check for drive letters (C:\ or C:)
if (path.Length >= 2 && char.IsLetter(path[0]) && path[1] == ':')
{
return true;
}
return false;
}
catch
{
return false;
}
}
}

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.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.System;
@@ -16,4 +11,6 @@ namespace Microsoft.CmdPal.Ext.ClipboardHistory;
internal static class KeyChords
{
internal static KeyChord DeleteEntry { get; } = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: VirtualKey.Delete);
internal static KeyChord OpenUrl { get; } = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.O);
}

View File

@@ -22,6 +22,7 @@ internal sealed partial class ClipboardListItem : ListItem
private readonly CommandContextItem _deleteContextMenuItem;
private readonly CommandContextItem? _pasteCommand;
private readonly CommandContextItem? _copyCommand;
private readonly CommandContextItem? _openUrlCommand;
private readonly Lazy<Details> _lazyDetails;
public override IDetails? Details
@@ -72,11 +73,26 @@ internal sealed partial class ClipboardListItem : ListItem
_pasteCommand = new CommandContextItem(new PasteCommand(_item, ClipboardFormat.Text, _settingsManager));
_copyCommand = new CommandContextItem(new CopyCommand(_item, ClipboardFormat.Text));
// Check if the text content is a valid URL and add OpenUrl command
if (UrlHelper.IsValidUrl(_item.Content ?? string.Empty))
{
var normalizedUrl = UrlHelper.NormalizeUrl(_item.Content ?? string.Empty);
_openUrlCommand = new CommandContextItem(new OpenUrlCommand(normalizedUrl))
{
RequestedShortcut = KeyChords.OpenUrl,
};
}
else
{
_openUrlCommand = null;
}
}
else
{
_pasteCommand = null;
_copyCommand = null;
_openUrlCommand = null;
}
RefreshCommands();
@@ -99,12 +115,7 @@ internal sealed partial class ClipboardListItem : ListItem
{
case PrimaryAction.Paste:
Command = _pasteCommand?.Command;
MoreCommands =
[
_copyCommand!,
new Separator(),
_deleteContextMenuItem,
];
MoreCommands = BuildMoreCommands(_copyCommand);
if (_item.IsText)
{
@@ -124,12 +135,7 @@ internal sealed partial class ClipboardListItem : ListItem
case PrimaryAction.Copy:
default:
Command = _copyCommand?.Command;
MoreCommands =
[
_pasteCommand!,
new Separator(),
_deleteContextMenuItem,
];
MoreCommands = BuildMoreCommands(_pasteCommand);
if (_item.IsText)
{
@@ -148,6 +154,26 @@ internal sealed partial class ClipboardListItem : ListItem
}
}
private IContextItem[] BuildMoreCommands(CommandContextItem? firstCommand)
{
var commands = new List<IContextItem>();
if (firstCommand != null)
{
commands.Add(firstCommand);
}
if (_openUrlCommand != null)
{
commands.Add(_openUrlCommand);
}
commands.Add(new Separator());
commands.Add(_deleteContextMenuItem);
return commands.ToArray();
}
private Details CreateDetails()
{
IDetailsElement[] metadata =

View File

@@ -0,0 +1,7 @@
// 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.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests")]

View File

@@ -183,4 +183,7 @@
<data name="settings_primary_action_copy" xml:space="preserve">
<value>Copy to Clipboard</value>
</data>
<data name="open_url_command_name" xml:space="preserve">
<value>Open URL</value>
</data>
</root>

View File

@@ -157,7 +157,98 @@ public class ShellListPageHelpers
executable = segments[0];
if (segments.Length > 1)
{
arguments = string.Join(' ', segments[1..]);
arguments = ArgumentBuilder.BuildArguments(segments[1..]);
}
}
private static class ArgumentBuilder
{
internal static string BuildArguments(string[] arguments)
{
if (arguments.Length <= 0)
{
return string.Empty;
}
var stringBuilder = new StringBuilder();
foreach (var argument in arguments)
{
AppendArgument(stringBuilder, argument);
}
return stringBuilder.ToString();
}
private static void AppendArgument(StringBuilder stringBuilder, string argument)
{
if (stringBuilder.Length > 0)
{
stringBuilder.Append(' ');
}
if (argument.Length == 0 || ShouldBeQuoted(argument))
{
stringBuilder.Append('\"');
var index = 0;
while (index < argument.Length)
{
var c = argument[index++];
if (c == '\\')
{
var numBackSlash = 1;
while (index < argument.Length && argument[index] == '\\')
{
index++;
numBackSlash++;
}
if (index == argument.Length)
{
stringBuilder.Append('\\', numBackSlash * 2);
}
else if (argument[index] == '\"')
{
stringBuilder.Append('\\', (numBackSlash * 2) + 1);
stringBuilder.Append('\"');
index++;
}
else
{
stringBuilder.Append('\\', numBackSlash);
}
continue;
}
if (c == '\"')
{
stringBuilder.Append('\\');
stringBuilder.Append('\"');
continue;
}
stringBuilder.Append(c);
}
stringBuilder.Append('\"');
}
else
{
stringBuilder.Append(argument);
}
}
private static bool ShouldBeQuoted(string s)
{
foreach (var c in s)
{
if (char.IsWhiteSpace(c) || c == '\"')
{
return true;
}
}
return false;
}
}
}

View File

@@ -159,15 +159,6 @@ namespace Microsoft.CommandPalette.Extensions.Toolkit.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Open.
/// </summary>
internal static string Page_Name {
get {
return ResourceManager.GetString("Page_Name", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Settings.
/// </summary>

View File

@@ -1,44 +0,0 @@
// 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.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
namespace Microsoft.PowerToys.Settings.UI.Activation
{
// For more information on understanding and extending activation flow see
// https://github.com/Microsoft/WindowsTemplateStudio/blob/master/docs/activation.md
internal abstract class ActivationHandler
{
public abstract bool CanHandle(object args);
public abstract Task HandleAsync(object args);
}
[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "abstract T and abstract")]
internal abstract class ActivationHandler<T> : ActivationHandler
where T : class
{
public override async Task HandleAsync(object args)
{
await HandleInternalAsync(args as T).ConfigureAwait(false);
}
public override bool CanHandle(object args)
{
// CanHandle checks the args is of type you have configured
return args is T && CanHandleInternal(args as T);
}
// Override this method to add the activation logic in your activation handler
protected abstract Task HandleInternalAsync(T args);
// You can override this method to add extra validation on activation args
// to determine if your ActivationHandler should handle this activation args
protected virtual bool CanHandleInternal(T args)
{
return true;
}
}
}

View File

@@ -1,42 +0,0 @@
// 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.Tasks;
using Microsoft.PowerToys.Settings.UI.Services;
using Windows.ApplicationModel.Activation;
namespace Microsoft.PowerToys.Settings.UI.Activation
{
internal sealed class DefaultActivationHandler : ActivationHandler<IActivatedEventArgs>
{
private readonly Type navElement;
public DefaultActivationHandler(Type navElement)
{
this.navElement = navElement;
}
protected override async Task HandleInternalAsync(IActivatedEventArgs args)
{
// When the navigation stack isn't restored, navigate to the first page and configure
// the new page by passing required information in the navigation parameter
object arguments = null;
if (args is LaunchActivatedEventArgs launchArgs)
{
arguments = launchArgs.Arguments;
}
NavigationService.Navigate(navElement, arguments);
await Task.CompletedTask.ConfigureAwait(false);
}
protected override bool CanHandleInternal(IActivatedEventArgs args)
{
// None of the ActivationHandlers has handled the app activation
return NavigationService.Frame.Content == null && navElement != null;
}
}
}

View File

@@ -1,106 +0,0 @@
// 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.Linq;
using System.Threading.Tasks;
using Microsoft.PowerToys.Settings.UI.Activation;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Windows.ApplicationModel.Activation;
namespace Microsoft.PowerToys.Settings.UI.Services
{
// For more information on understanding and extending activation flow see
// https://github.com/Microsoft/WindowsTemplateStudio/blob/master/docs/activation.md
internal sealed class ActivationService
{
private readonly App app;
private readonly Type defaultNavItem;
private Lazy<UIElement> shell;
private object lastActivationArgs;
public ActivationService(App app, Type defaultNavItem, Lazy<UIElement> shell = null)
{
this.app = app;
this.shell = shell;
this.defaultNavItem = defaultNavItem;
}
public async Task ActivateAsync(object activationArgs)
{
if (IsInteractive(activationArgs))
{
// Initialize services that you need before app activation
// take into account that the splash screen is shown while this code runs.
await InitializeAsync().ConfigureAwait(false);
// Do not repeat app initialization when the Window already has content,
// just ensure that the window is active
if (Window.Current.Content == null)
{
// Create a Shell or Frame to act as the navigation context
Window.Current.Content = shell?.Value ?? new Frame();
}
}
// Depending on activationArgs one of ActivationHandlers or DefaultActivationHandler
// will navigate to the first page
await HandleActivationAsync(activationArgs).ConfigureAwait(false);
lastActivationArgs = activationArgs;
if (IsInteractive(activationArgs))
{
// Ensure the current window is active
Window.Current.Activate();
// Tasks after activation
await StartupAsync().ConfigureAwait(false);
}
}
private static async Task InitializeAsync()
{
await Task.CompletedTask.ConfigureAwait(false);
}
private async Task HandleActivationAsync(object activationArgs)
{
var activationHandler = GetActivationHandlers()
.FirstOrDefault(h => h.CanHandle(activationArgs));
if (activationHandler != null)
{
await activationHandler.HandleAsync(activationArgs).ConfigureAwait(false);
}
if (IsInteractive(activationArgs))
{
var defaultHandler = new DefaultActivationHandler(defaultNavItem);
if (defaultHandler.CanHandle(activationArgs))
{
await defaultHandler.HandleAsync(activationArgs).ConfigureAwait(false);
}
}
}
private static async Task StartupAsync()
{
await Task.CompletedTask.ConfigureAwait(false);
}
private static IEnumerable<ActivationHandler> GetActivationHandlers()
{
yield break;
}
private static bool IsInteractive(object args)
{
return args is IActivatedEventArgs;
}
}
}

View File

@@ -24,12 +24,6 @@ namespace Microsoft.PowerToys.Settings.UI.Services
{
get
{
if (frame == null)
{
frame = Window.Current.Content as Frame;
RegisterFrameEvents();
}
return frame;
}