mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-01-15 16:56:26 +01:00
Compare commits
1 Commits
main
...
leilzh/sig
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
50ea57ab67 |
1
.github/actions/spell-check/expect.txt
vendored
1
.github/actions/spell-check/expect.txt
vendored
@@ -209,7 +209,6 @@ changecursor
|
||||
CHILDACTIVATE
|
||||
CHILDWINDOW
|
||||
CHOOSEFONT
|
||||
CIBUILD
|
||||
cidl
|
||||
CIELCh
|
||||
cim
|
||||
|
||||
@@ -13,8 +13,6 @@
|
||||
<PackageVersion Include="Microsoft.Bot.AdaptiveExpressions.Core" Version="4.23.0" />
|
||||
<PackageVersion Include="Appium.WebDriver" Version="4.4.5" />
|
||||
<PackageVersion Include="CoenM.ImageSharp.ImageHash" Version="1.3.6" />
|
||||
<!-- Pin the SixLabors.ImageSharp version (a transitive dependency of CoenM.ImageSharp.ImageHash) to restore functionality and apply patches. -->
|
||||
<PackageVersion Include="SixLabors.ImageSharp" Version="2.1.12" />
|
||||
<PackageVersion Include="CommunityToolkit.Common" Version="8.4.0" />
|
||||
<PackageVersion Include="CommunityToolkit.Mvvm" Version="8.4.0" />
|
||||
<PackageVersion Include="CommunityToolkit.WinUI.Animations" Version="8.2.250402" />
|
||||
@@ -26,7 +24,7 @@
|
||||
<PackageVersion Include="CommunityToolkit.WinUI.Converters" Version="8.2.250402" />
|
||||
<PackageVersion Include="CommunityToolkit.WinUI.Extensions" Version="8.2.250402" />
|
||||
<PackageVersion Include="CommunityToolkit.WinUI.UI.Controls.DataGrid" Version="7.1.2" />
|
||||
<PackageVersion Include="CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock" Version="0.1.260107-build.2454" />
|
||||
<PackageVersion Include="CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock" Version="0.1.251002-build.2316" />
|
||||
<PackageVersion Include="ControlzEx" Version="6.0.0" />
|
||||
<PackageVersion Include="HelixToolkit" Version="2.24.0" />
|
||||
<PackageVersion Include="HelixToolkit.Core.Wpf" Version="2.24.0" />
|
||||
|
||||
@@ -48,7 +48,7 @@ But to get started quickly, choose one of the installation methods below:
|
||||
<details open>
|
||||
<summary><strong>Download .exe from GitHub</strong></summary>
|
||||
<br/>
|
||||
Go to the <a href="https://aka.ms/installPowerToys">PowerToys GitHub releases</a>, click Assets to reveal the downloads, and choose the installer that matches your architecture and install scope. For most devices, that's the x64 per-user installer.
|
||||
Go to the [PowerToys GitHub releases][github-release-link], click Assets to reveal the downloads, and choose the installer that matches your architecture and install scope. For most devices, that's the x64 per-user installer.
|
||||
|
||||
<!-- items that need to be updated release to release -->
|
||||
[github-next-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.97%22
|
||||
@@ -83,7 +83,7 @@ You can easily install PowerToys from the Microsoft Store:
|
||||
<details>
|
||||
<summary><strong>WinGet</strong></summary>
|
||||
<br/>
|
||||
Download PowerToys from <a href="https://github.com/microsoft/winget-cli#installing-the-client">WinGet</a>. Updating PowerToys via winget will respect the current PowerToys installation scope. To install PowerToys, run the following command from the command line / PowerShell:
|
||||
Download PowerToys from [WinGet][winget-link]. Updating PowerToys via winget will respect the current PowerToys installation scope. To install PowerToys, run the following command from the command line / PowerShell:
|
||||
|
||||
*User scope installer [default]*
|
||||
```powershell
|
||||
@@ -99,7 +99,7 @@ winget install --scope machine Microsoft.PowerToys -s winget
|
||||
<details>
|
||||
<summary><strong>Other methods</strong></summary>
|
||||
<br/>
|
||||
There are <a href="https://learn.microsoft.com/windows/powertoys/install#community-driven-install-tools">community driven install methods</a> such as Chocolatey and Scoop. If these are your preferred install solutions, you can find the install instructions there.
|
||||
There are [community driven install methods](./doc/unofficialInstallMethods.md) such as Chocolatey and Scoop. If these are your preferred install solutions, you can find the install instructions there.
|
||||
</details>
|
||||
|
||||
## ✨ What's new
|
||||
|
||||
@@ -1549,7 +1549,7 @@ UINT __stdcall TerminateProcessesCA(MSIHANDLE hInstall)
|
||||
}
|
||||
processes.resize(bytes / sizeof(processes[0]));
|
||||
|
||||
std::array<std::wstring_view, 44> processesToTerminate = {
|
||||
std::array<std::wstring_view, 42> processesToTerminate = {
|
||||
L"PowerToys.PowerLauncher.exe",
|
||||
L"PowerToys.Settings.exe",
|
||||
L"PowerToys.AdvancedPaste.exe",
|
||||
@@ -1584,14 +1584,12 @@ UINT __stdcall TerminateProcessesCA(MSIHANDLE hInstall)
|
||||
L"PowerToys.MouseWithoutBordersService.exe",
|
||||
L"PowerToys.CropAndLock.exe",
|
||||
L"PowerToys.EnvironmentVariables.exe",
|
||||
L"PowerToys.QuickAccess.exe",
|
||||
L"PowerToys.WorkspacesSnapshotTool.exe",
|
||||
L"PowerToys.WorkspacesLauncher.exe",
|
||||
L"PowerToys.WorkspacesLauncherUI.exe",
|
||||
L"PowerToys.WorkspacesEditor.exe",
|
||||
L"PowerToys.WorkspacesWindowArranger.exe",
|
||||
L"Microsoft.CmdPal.UI.exe",
|
||||
L"Microsoft.CmdPal.Ext.PowerToys.exe",
|
||||
L"PowerToys.ZoomIt.exe",
|
||||
L"PowerToys.exe",
|
||||
};
|
||||
|
||||
@@ -61,16 +61,6 @@
|
||||
</RegistryKey>
|
||||
<File Source="$(var.RepoDir)\Notice.md" Id="Notice.md" />
|
||||
</Component>
|
||||
<Directory Id="SvgsFolder" Name="svgs">
|
||||
<Component Id="svgs_icons" Guid="A9B7C5D3-E1F2-4A6B-8C9D-0E1F2A3B4C5D" Bitness="always64">
|
||||
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
|
||||
<RegistryValue Type="string" Name="svgs_icons" Value="" KeyPath="yes" />
|
||||
</RegistryKey>
|
||||
<File Id="icon.ico" Source="$(var.BinDir)svgs\icon.ico" />
|
||||
<File Id="PowerToysWhite.ico" Source="$(var.BinDir)svgs\PowerToysWhite.ico" />
|
||||
<File Id="PowerToysDark.ico" Source="$(var.BinDir)svgs\PowerToysDark.ico" />
|
||||
</Component>
|
||||
</Directory>
|
||||
</DirectoryRef>
|
||||
|
||||
<?if $(var.PerUser) = "true" ?>
|
||||
@@ -122,7 +112,6 @@
|
||||
<RemoveFolder Id="RemoveBaseApplicationsAssetsFolder" Directory="BaseApplicationsAssetsFolder" On="uninstall" />
|
||||
<RemoveFolder Id="RemoveWinUI3AppsInstallFolder" Directory="WinUI3AppsInstallFolder" On="uninstall" />
|
||||
<RemoveFolder Id="RemoveWinUI3AppsAssetsFolder" Directory="WinUI3AppsAssetsFolder" On="uninstall" />
|
||||
<RemoveFolder Id="RemoveSvgsFolder" Directory="SvgsFolder" On="uninstall" />
|
||||
<RemoveFolder Id="RemoveINSTALLFOLDER" Directory="INSTALLFOLDER" On="uninstall" />
|
||||
</Component>
|
||||
<ComponentRef Id="powertoys_exe" />
|
||||
@@ -131,7 +120,6 @@
|
||||
<ComponentRef Id="powertoys_toast_clsid" />
|
||||
<ComponentRef Id="License_rtf" />
|
||||
<ComponentRef Id="Notice_md" />
|
||||
<ComponentRef Id="svgs_icons" />
|
||||
<ComponentRef Id="DesktopShortcut" />
|
||||
<?if $(var.PerUser) = "true" ?>
|
||||
<ComponentRef Id="powertoys_env_path_user" />
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace ManagedCommon
|
||||
{
|
||||
public static bool IsWindows10()
|
||||
{
|
||||
return Environment.OSVersion.Version.Major >= 10 && Environment.OSVersion.Version.Build < 22000;
|
||||
return Environment.OSVersion.Version.Major >= 10 && Environment.OSVersion.Version.Minor < 22000;
|
||||
}
|
||||
|
||||
public static bool IsWindows11()
|
||||
|
||||
@@ -466,27 +466,39 @@
|
||||
TextChanged="EditVariableDialogValueTxtBox_TextChanged"
|
||||
TextWrapping="Wrap" />
|
||||
<MenuFlyoutSeparator Visibility="{Binding ShowAsList, Converter={StaticResource BoolToVisibilityConverter}}" />
|
||||
<ItemsControl
|
||||
<ListView
|
||||
x:Name="EditVariableValuesList"
|
||||
Margin="0,-8,0,12"
|
||||
HorizontalAlignment="Stretch"
|
||||
AllowDrop="True"
|
||||
CanDragItems="True"
|
||||
CanReorderItems="True"
|
||||
DragItemsCompleted="EditVariableValuesList_DragItemsCompleted"
|
||||
ItemsSource="{Binding ValuesList, Mode=TwoWay}"
|
||||
Visibility="{Binding ShowAsList, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<ListView.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="32" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="40" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<FontIcon
|
||||
Grid.Column="0"
|
||||
Margin="0,0,8,0"
|
||||
FontSize="16"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Glyph="" />
|
||||
<TextBox
|
||||
Grid.Column="1"
|
||||
Background="Transparent"
|
||||
BorderBrush="Transparent"
|
||||
LostFocus="EditVariableValuesListTextBox_LostFocus"
|
||||
Text="{Binding Text}" />
|
||||
<Button
|
||||
x:Uid="More_Options_Button"
|
||||
Grid.Column="1"
|
||||
Grid.Column="2"
|
||||
VerticalAlignment="Center"
|
||||
Content=""
|
||||
FontFamily="{ThemeResource SymbolThemeFontFamily}"
|
||||
@@ -523,8 +535,8 @@
|
||||
</Button>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ListView.ItemTemplate>
|
||||
</ListView>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</ContentDialog>
|
||||
|
||||
@@ -16,6 +16,8 @@ namespace EnvironmentVariablesUILib
|
||||
{
|
||||
public sealed partial class EnvironmentVariablesMainPage : Page
|
||||
{
|
||||
private const string ValueListSeparator = ";";
|
||||
|
||||
private sealed class RelayCommandParameter
|
||||
{
|
||||
public RelayCommandParameter(Variable variable, VariablesSet set)
|
||||
@@ -440,7 +442,7 @@ namespace EnvironmentVariablesUILib
|
||||
variable.ValuesList.Move(index, index - 1);
|
||||
}
|
||||
|
||||
var newValues = string.Join(";", variable.ValuesList?.Select(x => x.Text).ToArray());
|
||||
var newValues = string.Join(ValueListSeparator, variable.ValuesList?.Select(x => x.Text).ToArray());
|
||||
EditVariableDialogValueTxtBox.Text = newValues;
|
||||
}
|
||||
|
||||
@@ -461,7 +463,7 @@ namespace EnvironmentVariablesUILib
|
||||
variable.ValuesList.Move(index, index + 1);
|
||||
}
|
||||
|
||||
var newValues = string.Join(";", variable.ValuesList?.Select(x => x.Text).ToArray());
|
||||
var newValues = string.Join(ValueListSeparator, variable.ValuesList?.Select(x => x.Text).ToArray());
|
||||
EditVariableDialogValueTxtBox.Text = newValues;
|
||||
}
|
||||
|
||||
@@ -476,7 +478,7 @@ namespace EnvironmentVariablesUILib
|
||||
var variable = EditVariableDialog.DataContext as Variable;
|
||||
variable.ValuesList.Remove(listItem);
|
||||
|
||||
var newValues = string.Join(";", variable.ValuesList?.Select(x => x.Text).ToArray());
|
||||
var newValues = string.Join(ValueListSeparator, variable.ValuesList?.Select(x => x.Text).ToArray());
|
||||
EditVariableDialogValueTxtBox.Text = newValues;
|
||||
}
|
||||
|
||||
@@ -492,7 +494,7 @@ namespace EnvironmentVariablesUILib
|
||||
var index = variable.ValuesList.IndexOf(listItem);
|
||||
variable.ValuesList.Insert(index, new Variable.ValuesListItem { Text = string.Empty });
|
||||
|
||||
var newValues = string.Join(";", variable.ValuesList?.Select(x => x.Text).ToArray());
|
||||
var newValues = string.Join(ValueListSeparator, variable.ValuesList?.Select(x => x.Text).ToArray());
|
||||
EditVariableDialogValueTxtBox.TextChanged -= EditVariableDialogValueTxtBox_TextChanged;
|
||||
EditVariableDialogValueTxtBox.Text = newValues;
|
||||
EditVariableDialogValueTxtBox.TextChanged += EditVariableDialogValueTxtBox_TextChanged;
|
||||
@@ -510,7 +512,7 @@ namespace EnvironmentVariablesUILib
|
||||
var index = variable.ValuesList.IndexOf(listItem);
|
||||
variable.ValuesList.Insert(index + 1, new Variable.ValuesListItem { Text = string.Empty });
|
||||
|
||||
var newValues = string.Join(";", variable.ValuesList?.Select(x => x.Text).ToArray());
|
||||
var newValues = string.Join(ValueListSeparator, variable.ValuesList?.Select(x => x.Text).ToArray());
|
||||
EditVariableDialogValueTxtBox.TextChanged -= EditVariableDialogValueTxtBox_TextChanged;
|
||||
EditVariableDialogValueTxtBox.Text = newValues;
|
||||
EditVariableDialogValueTxtBox.TextChanged += EditVariableDialogValueTxtBox_TextChanged;
|
||||
@@ -532,7 +534,7 @@ namespace EnvironmentVariablesUILib
|
||||
listItem.Text = (sender as TextBox)?.Text;
|
||||
var variable = EditVariableDialog.DataContext as Variable;
|
||||
|
||||
var newValues = string.Join(";", variable.ValuesList?.Select(x => x.Text).ToArray());
|
||||
var newValues = string.Join(ValueListSeparator, variable.ValuesList?.Select(x => x.Text).ToArray());
|
||||
EditVariableDialogValueTxtBox.TextChanged -= EditVariableDialogValueTxtBox_TextChanged;
|
||||
EditVariableDialogValueTxtBox.Text = newValues;
|
||||
EditVariableDialogValueTxtBox.TextChanged += EditVariableDialogValueTxtBox_TextChanged;
|
||||
@@ -548,5 +550,16 @@ namespace EnvironmentVariablesUILib
|
||||
CancelAddVariable();
|
||||
ConfirmAddVariableBtn.IsEnabled = false;
|
||||
}
|
||||
|
||||
private void EditVariableValuesList_DragItemsCompleted(ListViewBase sender, DragItemsCompletedEventArgs args)
|
||||
{
|
||||
if (EditVariableDialog.DataContext is Variable variable && variable.ValuesList != null)
|
||||
{
|
||||
var newValues = string.Join(ValueListSeparator, variable.ValuesList.Select(x => x.Text));
|
||||
EditVariableDialogValueTxtBox.TextChanged -= EditVariableDialogValueTxtBox_TextChanged;
|
||||
EditVariableDialogValueTxtBox.Text = newValues;
|
||||
EditVariableDialogValueTxtBox.TextChanged += EditVariableDialogValueTxtBox_TextChanged;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,10 +10,10 @@ public struct ApplicationWrapper
|
||||
{
|
||||
public struct WindowPositionWrapper
|
||||
{
|
||||
[JsonPropertyName("X")]
|
||||
[JsonPropertyName("x")]
|
||||
public int X { get; set; }
|
||||
|
||||
[JsonPropertyName("Y")]
|
||||
[JsonPropertyName("y")]
|
||||
public int Y { get; set; }
|
||||
|
||||
[JsonPropertyName("width")]
|
||||
|
||||
@@ -547,15 +547,6 @@ public partial class MainListPage : DynamicListPage,
|
||||
// above "git" from "whatever"
|
||||
max = max + extensionTitleMatch;
|
||||
|
||||
// Apply a penalty to fallback items so they rank below direct matches.
|
||||
// Fallbacks that dynamically match queries (like RDP connections) should
|
||||
// appear after apps and direct command matches.
|
||||
if (isFallback && max > 1)
|
||||
{
|
||||
// Reduce fallback scores by 50% to prioritize direct matches
|
||||
max = max * 0.5;
|
||||
}
|
||||
|
||||
var matchSomething = max
|
||||
+ (isAliasMatch ? 9001 : (isAliasSubstringMatch ? 1 : 0));
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// 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.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Helpers;
|
||||
@@ -17,41 +18,19 @@ internal static class BuildInfo
|
||||
// Runtime AOT detection
|
||||
public static bool IsNativeAot => !RuntimeFeature.IsDynamicCodeSupported;
|
||||
|
||||
// build-time values
|
||||
public static bool PublishTrimmed
|
||||
{
|
||||
get
|
||||
{
|
||||
#if BUILD_INFO_PUBLISH_TRIMMED
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
// From assembly metadata (build-time values)
|
||||
public static bool PublishTrimmed => GetBoolMetadata("PublishTrimmed", false);
|
||||
|
||||
// build-time values
|
||||
public static bool PublishAot
|
||||
{
|
||||
get
|
||||
{
|
||||
#if BUILD_INFO_PUBLISH_AOT
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
// From assembly metadata (build-time values)
|
||||
public static bool PublishAot => GetBoolMetadata("PublishAot", false);
|
||||
|
||||
public static bool IsCiBuild
|
||||
{
|
||||
get
|
||||
{
|
||||
#if BUILD_INFO_CIBUILD
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
public static bool IsCiBuild => GetBoolMetadata("CIBuild", false);
|
||||
|
||||
private static string? GetMetadata(string key) =>
|
||||
Assembly.GetExecutingAssembly()
|
||||
.GetCustomAttributes<AssemblyMetadataAttribute>()
|
||||
.FirstOrDefault(a => a.Key == key)?.Value;
|
||||
|
||||
private static bool GetBoolMetadata(string key, bool defaultValue) =>
|
||||
bool.TryParse(GetMetadata(key), out var result) ? result : defaultValue;
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- This disables the auto-generated main, so we can be single-instanced -->
|
||||
<DefineConstants>$(DefineConstants);DISABLE_XAML_GENERATED_MAIN</DefineConstants>
|
||||
<DefineConstants>DISABLE_XAML_GENERATED_MAIN</DefineConstants>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- BODGY: XES Versioning and WinAppSDK get into a fight about the app manifest, which breaks WinAppSDK. -->
|
||||
@@ -291,15 +291,24 @@
|
||||
</ItemGroup>
|
||||
<!-- </AdaptiveCardsWorkaround> -->
|
||||
|
||||
<!-- Build information -->
|
||||
<PropertyGroup Condition=" '$(PublishAot)' == 'true' ">
|
||||
<DefineConstants>$(DefineConstants);BUILD_INFO_PUBLISH_AOT</DefineConstants>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(PublishTrimmed)' == 'true' ">
|
||||
<DefineConstants>$(DefineConstants);BUILD_INFO_PUBLISH_TRIMMED</DefineConstants>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(CIBuild)' == 'true' ">
|
||||
<DefineConstants>$(DefineConstants);BUILD_INFO_CIBUILD</DefineConstants>
|
||||
</PropertyGroup>
|
||||
<!-- Metadata for build information -->
|
||||
<ItemGroup>
|
||||
<AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
|
||||
<_Parameter1>PublishTrimmed</_Parameter1>
|
||||
<_Parameter2>$(PublishTrimmed)</_Parameter2>
|
||||
</AssemblyAttribute>
|
||||
<AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
|
||||
<_Parameter1>PublishAot</_Parameter1>
|
||||
<_Parameter2>$(PublishAot)</_Parameter2>
|
||||
</AssemblyAttribute>
|
||||
<AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
|
||||
<_Parameter1>CIBuild</_Parameter1>
|
||||
<_Parameter2>$(CIBuild)</_Parameter2>
|
||||
</AssemblyAttribute>
|
||||
<AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
|
||||
<_Parameter1>CommandPaletteBranding</_Parameter1>
|
||||
<_Parameter2>$(CommandPaletteBranding)</_Parameter2>
|
||||
</AssemblyAttribute>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -372,7 +372,7 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
|
||||
<value>Windows Command Palette</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_About_SettingsExpander.Description" xml:space="preserve">
|
||||
<value>© 2026. All rights reserved.</value>
|
||||
<value>© 2025. All rights reserved.</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_About_GithubLink_Hyperlink.Content" xml:space="preserve">
|
||||
<value>View GitHub repository</value>
|
||||
|
||||
@@ -55,7 +55,7 @@ public class BasicTests : CommandPaletteTestBase
|
||||
|
||||
SetTimeAndDaterExtensionSearchBox("year");
|
||||
|
||||
Assert.IsNotNull(this.Find<NavigationViewItem>("2026"));
|
||||
Assert.IsNotNull(this.Find<NavigationViewItem>("2025"));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
<OutputType>WinExe</OutputType>
|
||||
<RootNamespace>PowerToysExtension</RootNamespace>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<ApplicationIcon>..\..\..\..\runner\svgs\icon.ico</ApplicationIcon>
|
||||
<PublishProfile>win-$(Platform).pubxml</PublishProfile>
|
||||
<EnableMsixTooling>false</EnableMsixTooling>
|
||||
<WindowsPackageType>None</WindowsPackageType>
|
||||
|
||||
@@ -31,7 +31,7 @@ internal static class Program
|
||||
Console.InputEncoding = Encoding.Unicode;
|
||||
|
||||
// Initialize logger to file (same as other modules)
|
||||
CliLogger.Initialize("\\Image Resizer\\CLI");
|
||||
CliLogger.Initialize("\\ImageResizer\\Logs");
|
||||
CliLogger.Info($"ImageResizerCLI started with {args.Length} argument(s)");
|
||||
|
||||
try
|
||||
|
||||
@@ -126,10 +126,13 @@ namespace ImageResizer.Properties
|
||||
h => ncc.CollectionChanged -= h,
|
||||
() => settings.CustomSize = new CustomSize());
|
||||
|
||||
// Reset is used instead of Replace to avoid ArgumentOutOfRangeException
|
||||
// when notifying changes for virtual items (CustomSize/AiSize) that exist
|
||||
// outside the bounds of the underlying _sizes collection.
|
||||
Assert.AreEqual(NotifyCollectionChangedAction.Reset, result.Arguments.Action);
|
||||
Assert.AreEqual(NotifyCollectionChangedAction.Replace, result.Arguments.Action);
|
||||
Assert.AreEqual(1, result.Arguments.NewItems.Count);
|
||||
Assert.AreEqual(settings.CustomSize, result.Arguments.NewItems[0]);
|
||||
Assert.AreEqual(0, result.Arguments.NewStartingIndex);
|
||||
Assert.AreEqual(1, result.Arguments.OldItems.Count);
|
||||
Assert.AreEqual(originalCustomSize, result.Arguments.OldItems[0]);
|
||||
Assert.AreEqual(0, result.Arguments.OldStartingIndex);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
|
||||
@@ -216,15 +216,27 @@ namespace ImageResizer.Properties
|
||||
{
|
||||
if (e.PropertyName == nameof(Models.CustomSize))
|
||||
{
|
||||
var oldCustomSize = _customSize;
|
||||
_customSize = settings.CustomSize;
|
||||
|
||||
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
|
||||
OnCollectionChanged(
|
||||
new NotifyCollectionChangedEventArgs(
|
||||
NotifyCollectionChangedAction.Replace,
|
||||
_customSize,
|
||||
oldCustomSize,
|
||||
_sizes.Count));
|
||||
}
|
||||
else if (e.PropertyName == nameof(Models.AiSize))
|
||||
{
|
||||
var oldAiSize = _aiSize;
|
||||
_aiSize = settings.AiSize;
|
||||
|
||||
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
|
||||
OnCollectionChanged(
|
||||
new NotifyCollectionChangedEventArgs(
|
||||
NotifyCollectionChangedAction.Replace,
|
||||
_aiSize,
|
||||
oldAiSize,
|
||||
_sizes.Count + 1));
|
||||
}
|
||||
else if (e.PropertyName == nameof(Sizes))
|
||||
{
|
||||
|
||||
@@ -388,13 +388,6 @@ namespace Peek.UI
|
||||
IsErrorVisible = true;
|
||||
}
|
||||
|
||||
public void ShowError(string message)
|
||||
{
|
||||
IsErrorVisible = false;
|
||||
ErrorMessage = message;
|
||||
IsErrorVisible = true;
|
||||
}
|
||||
|
||||
private void NavigationThrottleTimer_Tick(object? sender, object e)
|
||||
{
|
||||
if (sender == null)
|
||||
|
||||
@@ -50,8 +50,7 @@
|
||||
Item="{x:Bind ViewModel.CurrentItem, Mode=OneWay}"
|
||||
NumberOfFiles="{x:Bind ViewModel.DisplayItemCount, Mode=OneWay}"
|
||||
PreviewSizeChanged="FilePreviewer_PreviewSizeChanged"
|
||||
ScalingFactor="{x:Bind ViewModel.ScalingFactor, Mode=OneWay}"
|
||||
Visibility="{x:Bind ContentVisibility(ViewModel.IsErrorVisible), Mode=OneWay}" />
|
||||
ScalingFactor="{x:Bind ViewModel.ScalingFactor, Mode=OneWay}" />
|
||||
|
||||
<InfoBar
|
||||
x:Name="ErrorInfoBar"
|
||||
@@ -60,7 +59,6 @@
|
||||
Grid.RowSpan="2"
|
||||
Margin="4,0,4,6"
|
||||
VerticalAlignment="Bottom"
|
||||
Closed="ErrorInfoBar_Closed"
|
||||
IsOpen="{x:Bind ViewModel.IsErrorVisible, Mode=TwoWay}"
|
||||
Message="{x:Bind ViewModel.ErrorMessage, Mode=OneWay}"
|
||||
Severity="Error" />
|
||||
|
||||
@@ -14,7 +14,6 @@ using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
using Peek.Common.Constants;
|
||||
using Peek.Common.Extensions;
|
||||
using Peek.Common.Helpers;
|
||||
using Peek.FilePreviewer.Models;
|
||||
using Peek.FilePreviewer.Previewers;
|
||||
using Peek.UI.Extensions;
|
||||
@@ -196,20 +195,6 @@ namespace Peek.UI
|
||||
bootTime.Start();
|
||||
|
||||
ViewModel.Initialize(selectedItem);
|
||||
|
||||
// If no files were found (e.g., in virtual folders like Home/Recent), show an error
|
||||
if (ViewModel.CurrentItem == null)
|
||||
{
|
||||
Logger.LogInfo("Peek: No files found to preview, showing error.");
|
||||
var errorMessage = ResourceLoaderInstance.ResourceLoader.GetString("NoFilesSelected");
|
||||
ViewModel.ShowError(errorMessage);
|
||||
|
||||
// Still show the window so user can see the warning
|
||||
this.Show();
|
||||
WindowHelpers.BringToForeground(this.GetWindowHandle());
|
||||
return;
|
||||
}
|
||||
|
||||
ViewModel.ScalingFactor = this.GetMonitorScale();
|
||||
this.Content.KeyUp += Content_KeyUp;
|
||||
|
||||
@@ -317,24 +302,5 @@ namespace Peek.UI
|
||||
{
|
||||
themeListener?.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns Visibility.Collapsed when error is showing, Visibility.Visible when not.
|
||||
/// </summary>
|
||||
public Visibility ContentVisibility(bool isErrorVisible)
|
||||
{
|
||||
return isErrorVisible ? Visibility.Collapsed : Visibility.Visible;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle InfoBar closed - if there's no current item, close the window.
|
||||
/// </summary>
|
||||
private void ErrorInfoBar_Closed(InfoBar sender, InfoBarClosedEventArgs args)
|
||||
{
|
||||
if (ViewModel.CurrentItem == null)
|
||||
{
|
||||
Uninitialize();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -341,10 +341,6 @@
|
||||
<value>No more files to preview.</value>
|
||||
<comment>The message to show when there are no files remaining to preview.</comment>
|
||||
</data>
|
||||
<data name="NoFilesSelected" xml:space="preserve">
|
||||
<value>No files selected or this folder is not supported for preview.</value>
|
||||
<comment>Displayed when Peek is activated in a virtual folder (like Home or Recent) where file selection cannot be retrieved.</comment>
|
||||
</data>
|
||||
<data name="DeleteFileError_NotFound" xml:space="preserve">
|
||||
<value>The file cannot be found. Please check if the file has been moved, renamed, or deleted.</value>
|
||||
<comment>Displayed if the file or path was not found</comment>
|
||||
|
||||
@@ -193,102 +193,6 @@ GeneralSettings get_general_settings()
|
||||
return settings;
|
||||
}
|
||||
|
||||
void apply_module_status_update(const json::JsonObject& module_config, bool save)
|
||||
{
|
||||
Logger::info(L"apply_module_status_update: {}", std::wstring{ module_config.ToString() });
|
||||
|
||||
// Expected format: {"ModuleName": true/false} - only one module per update
|
||||
auto iter = module_config.First();
|
||||
if (!iter.HasCurrent())
|
||||
{
|
||||
Logger::warn(L"apply_module_status_update: Empty module config");
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& element = iter.Current();
|
||||
const auto value = element.Value();
|
||||
if (value.ValueType() != json::JsonValueType::Boolean)
|
||||
{
|
||||
Logger::warn(L"apply_module_status_update: Invalid value type for module status");
|
||||
return;
|
||||
}
|
||||
|
||||
const std::wstring name{ element.Key().c_str() };
|
||||
if (modules().find(name) == modules().end())
|
||||
{
|
||||
Logger::warn(L"apply_module_status_update: Module {} not found", name);
|
||||
return;
|
||||
}
|
||||
|
||||
PowertoyModule& powertoy = modules().at(name);
|
||||
const bool module_inst_enabled = powertoy->is_enabled();
|
||||
bool target_enabled = value.GetBoolean();
|
||||
|
||||
auto gpo_rule = powertoy->gpo_policy_enabled_configuration();
|
||||
if (gpo_rule == powertoys_gpo::gpo_rule_configured_enabled || gpo_rule == powertoys_gpo::gpo_rule_configured_disabled)
|
||||
{
|
||||
// Apply the GPO Rule.
|
||||
target_enabled = gpo_rule == powertoys_gpo::gpo_rule_configured_enabled;
|
||||
}
|
||||
|
||||
if (module_inst_enabled == target_enabled)
|
||||
{
|
||||
Logger::info(L"apply_module_status_update: Module {} already in target state {}", name, target_enabled);
|
||||
return;
|
||||
}
|
||||
|
||||
if (target_enabled)
|
||||
{
|
||||
Logger::info(L"apply_module_status_update: Enabling powertoy {}", name);
|
||||
powertoy->enable();
|
||||
auto& hkmng = HotkeyConflictDetector::HotkeyConflictManager::GetInstance();
|
||||
hkmng.EnableHotkeyByModule(name);
|
||||
|
||||
// Trigger AI capability detection when ImageResizer is enabled
|
||||
if (name == L"Image Resizer")
|
||||
{
|
||||
Logger::info(L"ImageResizer enabled, triggering AI capability detection");
|
||||
DetectAiCapabilitiesAsync(true); // Skip settings check since we know it's being enabled
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::info(L"apply_module_status_update: Disabling powertoy {}", name);
|
||||
powertoy->disable();
|
||||
auto& hkmng = HotkeyConflictDetector::HotkeyConflictManager::GetInstance();
|
||||
hkmng.DisableHotkeyByModule(name);
|
||||
}
|
||||
// Sync the hotkey state with the module state, so it can be removed for disabled modules.
|
||||
powertoy.UpdateHotkeyEx();
|
||||
|
||||
if (save)
|
||||
{
|
||||
// Load existing settings and only update the specific module's enabled state
|
||||
json::JsonObject current_settings = PTSettingsHelper::load_general_settings();
|
||||
|
||||
json::JsonObject enabled;
|
||||
if (current_settings.HasKey(L"enabled"))
|
||||
{
|
||||
enabled = current_settings.GetNamedObject(L"enabled");
|
||||
}
|
||||
|
||||
// Check if the saved state is different from the requested state
|
||||
bool current_saved = enabled.HasKey(name) ? enabled.GetNamedBoolean(name, true) : true;
|
||||
|
||||
if (current_saved != target_enabled)
|
||||
{
|
||||
// Update only this module's enabled state
|
||||
enabled.SetNamedValue(name, json::value(target_enabled));
|
||||
current_settings.SetNamedValue(L"enabled", enabled);
|
||||
|
||||
PTSettingsHelper::save_general_settings(current_settings);
|
||||
|
||||
GeneralSettings settings_for_trace = get_general_settings();
|
||||
Trace::SettingsChanged(settings_for_trace);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void apply_general_settings(const json::JsonObject& general_configs, bool save)
|
||||
{
|
||||
std::wstring old_settings_json_string;
|
||||
@@ -463,21 +367,11 @@ void apply_general_settings(const json::JsonObject& general_configs, bool save)
|
||||
if (json::has(general_configs, L"show_theme_adaptive_tray_icon", json::JsonValueType::Boolean))
|
||||
{
|
||||
bool new_theme_adaptive = general_configs.GetNamedBoolean(L"show_theme_adaptive_tray_icon");
|
||||
Logger::info(L"apply_general_settings: show_theme_adaptive_tray_icon current={}, new={}",
|
||||
show_theme_adaptive_tray_icon, new_theme_adaptive);
|
||||
if (show_theme_adaptive_tray_icon != new_theme_adaptive)
|
||||
{
|
||||
show_theme_adaptive_tray_icon = new_theme_adaptive;
|
||||
set_tray_icon_theme_adaptive(show_theme_adaptive_tray_icon);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::info(L"apply_general_settings: show_theme_adaptive_tray_icon unchanged, skipping update");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::warn(L"apply_general_settings: show_theme_adaptive_tray_icon not found in config");
|
||||
}
|
||||
|
||||
if (json::has(general_configs, L"ignored_conflict_properties", json::JsonValueType::Object))
|
||||
|
||||
@@ -38,5 +38,4 @@ struct GeneralSettings
|
||||
json::JsonObject load_general_settings();
|
||||
GeneralSettings get_general_settings();
|
||||
void apply_general_settings(const json::JsonObject& general_configs, bool save = true);
|
||||
void apply_module_status_update(const json::JsonObject& module_config, bool save = true);
|
||||
void start_enabled_powertoys();
|
||||
@@ -215,12 +215,6 @@ void dispatch_received_json(const std::wstring& json_to_parse)
|
||||
// current_settings_ipc->send(settings_string);
|
||||
// }
|
||||
}
|
||||
else if (name == L"module_status")
|
||||
{
|
||||
// Handle single module enable/disable update
|
||||
// Expected format: {"module_status": {"ModuleName": true/false}}
|
||||
apply_module_status_update(value.GetObjectW());
|
||||
}
|
||||
else if (name == L"powertoys")
|
||||
{
|
||||
dispatch_json_config_to_modules(value.GetObjectW());
|
||||
|
||||
@@ -273,19 +273,12 @@ static HICON get_icon(Theme theme)
|
||||
{
|
||||
std::wstring icon_path = get_module_folderpath();
|
||||
icon_path += theme == Theme::Dark ? L"\\svgs\\PowerToysWhite.ico" : L"\\svgs\\PowerToysDark.ico";
|
||||
Logger::trace(L"get_icon: Loading icon from path: {}", icon_path);
|
||||
|
||||
HICON icon = static_cast<HICON>(LoadImage(NULL,
|
||||
return static_cast<HICON>(LoadImage(NULL,
|
||||
icon_path.c_str(),
|
||||
IMAGE_ICON,
|
||||
0,
|
||||
0,
|
||||
LR_LOADFROMFILE | LR_DEFAULTSIZE | LR_SHARED));
|
||||
if (!icon)
|
||||
{
|
||||
Logger::warn(L"get_icon: Failed to load icon from {}, error: {}", icon_path, GetLastError());
|
||||
}
|
||||
return icon;
|
||||
}
|
||||
|
||||
|
||||
@@ -381,45 +374,13 @@ void set_tray_icon_visible(bool shouldIconBeVisible)
|
||||
|
||||
void set_tray_icon_theme_adaptive(bool theme_adaptive)
|
||||
{
|
||||
Logger::info(L"set_tray_icon_theme_adaptive: Called with theme_adaptive={}, current theme_adaptive_enabled={}",
|
||||
theme_adaptive, theme_adaptive_enabled);
|
||||
|
||||
auto h_instance = reinterpret_cast<HINSTANCE>(&__ImageBase);
|
||||
HICON icon = nullptr;
|
||||
|
||||
if (theme_adaptive)
|
||||
{
|
||||
icon = get_icon(theme_listener.AppTheme);
|
||||
if (!icon)
|
||||
{
|
||||
Logger::warn(L"set_tray_icon_theme_adaptive: Failed to load theme adaptive icon, falling back to default");
|
||||
}
|
||||
}
|
||||
|
||||
// If not requesting adaptive icon, or if adaptive icon failed to load, use default icon
|
||||
if (!icon)
|
||||
{
|
||||
icon = LoadIcon(h_instance, MAKEINTRESOURCE(APPICON));
|
||||
if (theme_adaptive && icon)
|
||||
{
|
||||
// We requested adaptive but had to fall back, so update the flag
|
||||
theme_adaptive = false;
|
||||
Logger::info(L"set_tray_icon_theme_adaptive: Using default icon as fallback");
|
||||
}
|
||||
}
|
||||
|
||||
theme_adaptive_enabled = theme_adaptive;
|
||||
|
||||
auto h_instance = reinterpret_cast<HINSTANCE>(&__ImageBase);
|
||||
HICON const icon = theme_adaptive ? get_icon(theme_listener.AppTheme) : LoadIcon(h_instance, MAKEINTRESOURCE(APPICON));
|
||||
if (icon)
|
||||
{
|
||||
tray_icon_data.hIcon = icon;
|
||||
BOOL result = Shell_NotifyIcon(NIM_MODIFY, &tray_icon_data);
|
||||
Logger::info(L"set_tray_icon_theme_adaptive: Icon updated, theme_adaptive_enabled={}, Shell_NotifyIcon result={}",
|
||||
theme_adaptive_enabled, result);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::error(L"set_tray_icon_theme_adaptive: Failed to load any icon");
|
||||
Shell_NotifyIcon(NIM_MODIFY, &tray_icon_data);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
<TargetFramework>net9.0-windows10.0.26100.0</TargetFramework>
|
||||
<RootNamespace>Microsoft.PowerToys.QuickAccess</RootNamespace>
|
||||
<AssemblyName>PowerToys.QuickAccess</AssemblyName>
|
||||
<ApplicationIcon>..\..\runner\svgs\icon.ico</ApplicationIcon>
|
||||
<UseWinUI>true</UseWinUI>
|
||||
<WindowsPackageType>None</WindowsPackageType>
|
||||
<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
|
||||
|
||||
@@ -51,7 +51,6 @@ public sealed partial class AppsListPage : Page
|
||||
if (ViewModel != null)
|
||||
{
|
||||
ViewModel.DashboardSortOrder = DashboardSortOrder.Alphabetical;
|
||||
((ToggleMenuFlyoutItem)sender).IsChecked = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +59,6 @@ public sealed partial class AppsListPage : Page
|
||||
if (ViewModel != null)
|
||||
{
|
||||
ViewModel.DashboardSortOrder = DashboardSortOrder.ByStatus;
|
||||
((ToggleMenuFlyoutItem)sender).IsChecked = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,7 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
|
||||
namespace Microsoft.PowerToys.QuickAccess.Services;
|
||||
|
||||
@@ -21,9 +19,9 @@ public interface IQuickAccessCoordinator
|
||||
|
||||
Task<bool> ShowDocumentationAsync();
|
||||
|
||||
bool UpdateModuleEnabled(ModuleType moduleType, bool isEnabled);
|
||||
void NotifyUserSettingsInteraction();
|
||||
|
||||
void SendSortOrderUpdate(GeneralSettings generalSettings);
|
||||
bool UpdateModuleEnabled(ModuleType moduleType, bool isEnabled);
|
||||
|
||||
void ReportBug();
|
||||
|
||||
|
||||
@@ -55,8 +55,37 @@ internal sealed class QuickAccessCoordinator : IQuickAccessCoordinator, IDisposa
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
|
||||
public void NotifyUserSettingsInteraction()
|
||||
{
|
||||
Logger.LogDebug("QuickAccessCoordinator.NotifyUserSettingsInteraction invoked.");
|
||||
}
|
||||
|
||||
public bool UpdateModuleEnabled(ModuleType moduleType, bool isEnabled)
|
||||
=> TrySendIpcMessage($"{{\"module_status\": {{\"{ModuleHelper.GetModuleKey(moduleType)}\": {isEnabled.ToString().ToLowerInvariant()}}}}}", "module status update");
|
||||
{
|
||||
GeneralSettings? updatedSettings = null;
|
||||
lock (_generalSettingsLock)
|
||||
{
|
||||
var repository = SettingsRepository<GeneralSettings>.GetInstance(_settingsUtils);
|
||||
var generalSettings = repository.SettingsConfig;
|
||||
var current = ModuleHelper.GetIsModuleEnabled(generalSettings, moduleType);
|
||||
if (current == isEnabled)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ModuleHelper.SetIsModuleEnabled(generalSettings, moduleType, isEnabled);
|
||||
_settingsUtils.SaveSettings(generalSettings.ToJsonString());
|
||||
Logger.LogInfo($"QuickAccess updated module '{moduleType}' enabled state to {isEnabled}.");
|
||||
updatedSettings = generalSettings;
|
||||
}
|
||||
|
||||
if (updatedSettings != null)
|
||||
{
|
||||
SendGeneralSettingsUpdate(updatedSettings);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void ReportBug()
|
||||
{
|
||||
@@ -102,10 +131,20 @@ internal sealed class QuickAccessCoordinator : IQuickAccessCoordinator, IDisposa
|
||||
Logger.LogDebug($"QuickAccessCoordinator received IPC payload: {message}");
|
||||
}
|
||||
|
||||
public void SendSortOrderUpdate(GeneralSettings generalSettings)
|
||||
private void SendGeneralSettingsUpdate(GeneralSettings updatedSettings)
|
||||
{
|
||||
var outgoing = new OutGoingGeneralSettings(generalSettings);
|
||||
TrySendIpcMessage(outgoing.ToString(), "sort order update");
|
||||
string payload;
|
||||
try
|
||||
{
|
||||
payload = new OutGoingGeneralSettings(updatedSettings).ToString();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("QuickAccessCoordinator: failed to serialize general settings payload.", ex);
|
||||
return;
|
||||
}
|
||||
|
||||
TrySendIpcMessage(payload, "general settings update");
|
||||
}
|
||||
|
||||
private bool TrySendIpcMessage(string payload, string operationDescription)
|
||||
|
||||
@@ -22,7 +22,6 @@ namespace Microsoft.PowerToys.QuickAccess.ViewModels;
|
||||
|
||||
public sealed class AllAppsViewModel : Observable
|
||||
{
|
||||
private readonly object _sortLock = new object();
|
||||
private readonly IQuickAccessCoordinator _coordinator;
|
||||
private readonly ISettingsRepository<GeneralSettings> _settingsRepository;
|
||||
private readonly SettingsUtils _settingsUtils;
|
||||
@@ -31,9 +30,6 @@ public sealed class AllAppsViewModel : Observable
|
||||
private readonly List<FlyoutMenuItem> _allFlyoutMenuItems = new();
|
||||
private GeneralSettings _generalSettings;
|
||||
|
||||
// Flag to prevent toggle operations during sorting to avoid race conditions.
|
||||
private bool _isSorting;
|
||||
|
||||
public ObservableCollection<FlyoutMenuItem> FlyoutMenuItems { get; }
|
||||
|
||||
public DashboardSortOrder DashboardSortOrder
|
||||
@@ -44,9 +40,9 @@ public sealed class AllAppsViewModel : Observable
|
||||
if (_generalSettings.DashboardSortOrder != value)
|
||||
{
|
||||
_generalSettings.DashboardSortOrder = value;
|
||||
_coordinator.SendSortOrderUpdate(_generalSettings);
|
||||
_settingsUtils.SaveSettings(_generalSettings.ToJsonString(), _generalSettings.GetModuleName());
|
||||
OnPropertyChanged();
|
||||
SortFlyoutMenuItems();
|
||||
RefreshFlyoutMenuItems();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -58,6 +54,7 @@ public sealed class AllAppsViewModel : Observable
|
||||
_settingsUtils = SettingsUtils.Default;
|
||||
_settingsRepository = SettingsRepository<GeneralSettings>.GetInstance(_settingsUtils);
|
||||
_generalSettings = _settingsRepository.SettingsConfig;
|
||||
_generalSettings.AddEnabledModuleChangeNotification(ModuleEnabledChangedOnSettingsPage);
|
||||
_settingsRepository.SettingsChanged += OnSettingsChanged;
|
||||
|
||||
_resourceLoader = Helpers.ResourceLoaderInstance.ResourceLoader;
|
||||
@@ -90,6 +87,7 @@ public sealed class AllAppsViewModel : Observable
|
||||
_dispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
_generalSettings = newSettings;
|
||||
_generalSettings.AddEnabledModuleChangeNotification(ModuleEnabledChangedOnSettingsPage);
|
||||
OnPropertyChanged(nameof(DashboardSortOrder));
|
||||
RefreshFlyoutMenuItems();
|
||||
});
|
||||
@@ -122,55 +120,30 @@ public sealed class AllAppsViewModel : Observable
|
||||
}
|
||||
}
|
||||
|
||||
SortFlyoutMenuItems();
|
||||
}
|
||||
|
||||
private void SortFlyoutMenuItems()
|
||||
{
|
||||
if (_isSorting)
|
||||
var sortedItems = DashboardSortOrder switch
|
||||
{
|
||||
DashboardSortOrder.ByStatus => _allFlyoutMenuItems.OrderByDescending(x => x.IsEnabled).ThenBy(x => x.Label).ToList(),
|
||||
_ => _allFlyoutMenuItems.OrderBy(x => x.Label).ToList(),
|
||||
};
|
||||
|
||||
if (FlyoutMenuItems.Count == 0)
|
||||
{
|
||||
foreach (var item in sortedItems)
|
||||
{
|
||||
FlyoutMenuItems.Add(item);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_sortLock)
|
||||
for (int i = 0; i < sortedItems.Count; i++)
|
||||
{
|
||||
_isSorting = true;
|
||||
try
|
||||
var item = sortedItems[i];
|
||||
var oldIndex = FlyoutMenuItems.IndexOf(item);
|
||||
|
||||
if (oldIndex != -1 && oldIndex != i)
|
||||
{
|
||||
var sortedItems = DashboardSortOrder switch
|
||||
{
|
||||
DashboardSortOrder.ByStatus => _allFlyoutMenuItems.OrderByDescending(x => x.IsEnabled).ThenBy(x => x.Label).ToList(),
|
||||
_ => _allFlyoutMenuItems.OrderBy(x => x.Label).ToList(),
|
||||
};
|
||||
|
||||
if (FlyoutMenuItems.Count == 0)
|
||||
{
|
||||
foreach (var item in sortedItems)
|
||||
{
|
||||
FlyoutMenuItems.Add(item);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < sortedItems.Count; i++)
|
||||
{
|
||||
var item = sortedItems[i];
|
||||
var oldIndex = FlyoutMenuItems.IndexOf(item);
|
||||
|
||||
if (oldIndex != -1 && oldIndex != i)
|
||||
{
|
||||
FlyoutMenuItems.Move(oldIndex, i);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Use dispatcher to reset flag after UI updates complete
|
||||
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
|
||||
{
|
||||
_isSorting = false;
|
||||
});
|
||||
FlyoutMenuItems.Move(oldIndex, i);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -178,17 +151,14 @@ public sealed class AllAppsViewModel : Observable
|
||||
private void EnabledChangedOnUI(ModuleListItem item)
|
||||
{
|
||||
var flyoutItem = (FlyoutMenuItem)item;
|
||||
var isEnabled = flyoutItem.IsEnabled;
|
||||
|
||||
// Ignore toggle operations during sorting to prevent race conditions.
|
||||
// Revert the toggle state since UI already changed due to TwoWay binding.
|
||||
if (_isSorting)
|
||||
if (_coordinator.UpdateModuleEnabled(flyoutItem.Tag, flyoutItem.IsEnabled))
|
||||
{
|
||||
flyoutItem.UpdateStatus(!isEnabled);
|
||||
return;
|
||||
_coordinator.NotifyUserSettingsInteraction();
|
||||
}
|
||||
}
|
||||
|
||||
_coordinator.UpdateModuleEnabled(flyoutItem.Tag, flyoutItem.IsEnabled);
|
||||
SortFlyoutMenuItems();
|
||||
private void ModuleEnabledChangedOnSettingsPage()
|
||||
{
|
||||
RefreshFlyoutMenuItems();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,47 +117,5 @@ namespace Microsoft.PowerToys.Settings.UI.Library.Helpers
|
||||
case ModuleType.GeneralSettings: generalSettingsConfig.EnableQuickAccess = isEnabled; break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the module key name used in IPC messages and settings JSON.
|
||||
/// These names match the JsonPropertyName attributes in EnabledModules class.
|
||||
/// </summary>
|
||||
public static string GetModuleKey(ModuleType moduleType)
|
||||
{
|
||||
return moduleType switch
|
||||
{
|
||||
ModuleType.AdvancedPaste => AdvancedPasteSettings.ModuleName,
|
||||
ModuleType.AlwaysOnTop => AlwaysOnTopSettings.ModuleName,
|
||||
ModuleType.Awake => AwakeSettings.ModuleName,
|
||||
ModuleType.CmdPal => "CmdPal", // No dedicated settings class
|
||||
ModuleType.ColorPicker => ColorPickerSettings.ModuleName,
|
||||
ModuleType.CropAndLock => CropAndLockSettings.ModuleName,
|
||||
ModuleType.CursorWrap => CursorWrapSettings.ModuleName,
|
||||
ModuleType.EnvironmentVariables => EnvironmentVariablesSettings.ModuleName,
|
||||
ModuleType.FancyZones => FancyZonesSettings.ModuleName,
|
||||
ModuleType.FileLocksmith => FileLocksmithSettings.ModuleName,
|
||||
ModuleType.FindMyMouse => FindMyMouseSettings.ModuleName,
|
||||
ModuleType.Hosts => HostsSettings.ModuleName,
|
||||
ModuleType.ImageResizer => ImageResizerSettings.ModuleName,
|
||||
ModuleType.KeyboardManager => KeyboardManagerSettings.ModuleName,
|
||||
ModuleType.LightSwitch => LightSwitchSettings.ModuleName,
|
||||
ModuleType.MouseHighlighter => MouseHighlighterSettings.ModuleName,
|
||||
ModuleType.MouseJump => MouseJumpSettings.ModuleName,
|
||||
ModuleType.MousePointerCrosshairs => MousePointerCrosshairsSettings.ModuleName,
|
||||
ModuleType.MouseWithoutBorders => MouseWithoutBordersSettings.ModuleName,
|
||||
ModuleType.NewPlus => NewPlusSettings.ModuleName,
|
||||
ModuleType.Peek => PeekSettings.ModuleName,
|
||||
ModuleType.PowerRename => PowerRenameSettings.ModuleName,
|
||||
ModuleType.PowerLauncher => PowerLauncherSettings.ModuleName,
|
||||
ModuleType.PowerAccent => PowerAccentSettings.ModuleName,
|
||||
ModuleType.RegistryPreview => RegistryPreviewSettings.ModuleName,
|
||||
ModuleType.MeasureTool => MeasureToolSettings.ModuleName,
|
||||
ModuleType.ShortcutGuide => ShortcutGuideSettings.ModuleName,
|
||||
ModuleType.PowerOCR => PowerOcrSettings.ModuleName,
|
||||
ModuleType.Workspaces => WorkspacesSettings.ModuleName,
|
||||
ModuleType.ZoomIt => ZoomItSettings.ModuleName,
|
||||
_ => moduleType.ToString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Enums
|
||||
MeasureTool,
|
||||
Hosts,
|
||||
Workspaces,
|
||||
WhatsNew,
|
||||
RegistryPreview,
|
||||
NewPlus,
|
||||
ZoomIt,
|
||||
|
||||
@@ -9,6 +9,7 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Settings.UI.Helpers;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
@@ -226,6 +227,7 @@ namespace Microsoft.PowerToys.Settings.UI
|
||||
{
|
||||
settingsWindow = new MainWindow();
|
||||
settingsWindow.Activate();
|
||||
settingsWindow.ExtendsContentIntoTitleBar = true;
|
||||
settingsWindow.NavigateToSection(StartupPage);
|
||||
|
||||
// https://github.com/microsoft/microsoft-ui-xaml/issues/7595 - Activate doesn't bring window to the foreground
|
||||
@@ -255,10 +257,11 @@ namespace Microsoft.PowerToys.Settings.UI
|
||||
else if (ShowScoobe)
|
||||
{
|
||||
PowerToysTelemetry.Log.WriteEvent(new ScoobeStartedEvent());
|
||||
ScoobeWindow newScoobeWindow = new ScoobeWindow();
|
||||
newScoobeWindow.Activate();
|
||||
OobeWindow scoobeWindow = new OobeWindow(OOBE.Enums.PowerToysModules.WhatsNew);
|
||||
scoobeWindow.Activate();
|
||||
scoobeWindow.ExtendsContentIntoTitleBar = true;
|
||||
WindowHelpers.ForceTopBorder1PixelInsetOnWindows10(WindowNative.GetWindowHandle(settingsWindow));
|
||||
SetScoobeWindow(newScoobeWindow);
|
||||
SetOobeWindow(scoobeWindow);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -336,7 +339,6 @@ namespace Microsoft.PowerToys.Settings.UI
|
||||
|
||||
private static MainWindow settingsWindow;
|
||||
private static OobeWindow oobeWindow;
|
||||
private static ScoobeWindow scoobeWindow;
|
||||
|
||||
public static void ClearSettingsWindow()
|
||||
{
|
||||
@@ -363,21 +365,6 @@ namespace Microsoft.PowerToys.Settings.UI
|
||||
oobeWindow = null;
|
||||
}
|
||||
|
||||
public static ScoobeWindow GetScoobeWindow()
|
||||
{
|
||||
return scoobeWindow;
|
||||
}
|
||||
|
||||
public static void SetScoobeWindow(ScoobeWindow window)
|
||||
{
|
||||
scoobeWindow = window;
|
||||
}
|
||||
|
||||
public static void ClearScoobeWindow()
|
||||
{
|
||||
scoobeWindow = null;
|
||||
}
|
||||
|
||||
public static Type GetPage(string settingWindow)
|
||||
{
|
||||
switch (settingWindow)
|
||||
|
||||
@@ -20,6 +20,9 @@ using WinUIEx;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// An empty window that can be used on its own or navigated to within a Frame.
|
||||
/// </summary>
|
||||
public sealed partial class MainWindow : WindowEx
|
||||
{
|
||||
public MainWindow(bool createHidden = false)
|
||||
@@ -32,12 +35,10 @@ namespace Microsoft.PowerToys.Settings.UI
|
||||
App.ThemeService.ThemeChanged += OnThemeChanged;
|
||||
App.ThemeService.ApplyTheme();
|
||||
|
||||
this.ExtendsContentIntoTitleBar = true;
|
||||
|
||||
ShellPage.SetElevationStatus(App.IsElevated);
|
||||
ShellPage.SetIsUserAnAdmin(App.IsUserAnAdmin);
|
||||
|
||||
var hWnd = WindowNative.GetWindowHandle(this);
|
||||
var hWnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
|
||||
var placement = WindowHelper.DeserializePlacementOrDefault(hWnd);
|
||||
if (createHidden)
|
||||
{
|
||||
@@ -120,12 +121,16 @@ namespace Microsoft.PowerToys.Settings.UI
|
||||
// open whats new window
|
||||
ShellPage.SetOpenWhatIsNewCallback(() =>
|
||||
{
|
||||
if (App.GetScoobeWindow() == null)
|
||||
if (App.GetOobeWindow() == null)
|
||||
{
|
||||
App.SetScoobeWindow(new ScoobeWindow());
|
||||
App.SetOobeWindow(new OobeWindow(OOBE.Enums.PowerToysModules.WhatsNew));
|
||||
}
|
||||
else
|
||||
{
|
||||
App.GetOobeWindow().SetAppWindow(OOBE.Enums.PowerToysModules.WhatsNew);
|
||||
}
|
||||
|
||||
App.GetScoobeWindow().Activate();
|
||||
App.GetOobeWindow().Activate();
|
||||
});
|
||||
|
||||
this.InitializeComponent();
|
||||
@@ -182,7 +187,7 @@ namespace Microsoft.PowerToys.Settings.UI
|
||||
var hWnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
|
||||
WindowHelper.SerializePlacement(hWnd);
|
||||
|
||||
if (App.GetOobeWindow() == null && App.GetScoobeWindow() == null)
|
||||
if (App.GetOobeWindow() == null)
|
||||
{
|
||||
App.ClearSettingsWindow();
|
||||
}
|
||||
|
||||
@@ -1,44 +1,60 @@
|
||||
<Page
|
||||
<UserControl
|
||||
x:Class="Microsoft.PowerToys.Settings.UI.OOBE.Views.OobeShellPage"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:animations="using:CommunityToolkit.WinUI.Animations"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:ui="using:CommunityToolkit.WinUI"
|
||||
HighContrastAdjustment="None"
|
||||
Loaded="ShellPage_Loaded"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="48" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<TitleBar
|
||||
<Button
|
||||
x:Name="PaneToggleBtn"
|
||||
Width="48"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
Click="PaneToggleBtn_Click"
|
||||
Style="{StaticResource PaneToggleButtonStyle}" />
|
||||
<Grid
|
||||
x:Name="AppTitleBar"
|
||||
x:Uid="OobeWindow_TitleTxt"
|
||||
IsBackButtonVisible="False"
|
||||
IsPaneToggleButtonVisible="False"
|
||||
PaneToggleRequested="TitleBar_PaneButtonClick">
|
||||
<!-- 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="TitleBarIcon"
|
||||
Height="48"
|
||||
Margin="48,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
IsHitTestVisible="True">
|
||||
<animations:Implicit.Animations>
|
||||
<animations:OffsetAnimation Duration="0:0:0.3" />
|
||||
</animations:Implicit.Animations>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Image
|
||||
Width="16"
|
||||
Height="16"
|
||||
Margin="16,0,0,0"
|
||||
Source="/Assets/Settings/icon.ico" />
|
||||
</TitleBar.LeftHeader>
|
||||
</TitleBar>
|
||||
<TextBlock
|
||||
x:Name="AppTitleBarText"
|
||||
x:Uid="OobeWindow_TitleTxt"
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
TextWrapping="NoWrap" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<NavigationView
|
||||
x:Name="navigationView"
|
||||
Grid.Row="1"
|
||||
CompactModeThresholdWidth="1007"
|
||||
DisplayModeChanged="NavigationView_DisplayModeChanged"
|
||||
ExpandedModeThresholdWidth="1007"
|
||||
IsBackButtonVisible="Collapsed"
|
||||
IsPaneOpen="True"
|
||||
IsPaneToggleButtonVisible="False"
|
||||
IsSettingsVisible="False"
|
||||
OpenPaneLength="296"
|
||||
PaneDisplayMode="Left"
|
||||
SelectionChanged="NavigationView_SelectionChanged">
|
||||
<NavigationView.MenuItems>
|
||||
<NavigationViewItem
|
||||
@@ -158,16 +174,34 @@
|
||||
Icon="{ui:BitmapIcon Source=/Assets/Settings/Icons/ZoomIt.png}"
|
||||
Tag="ZoomIt" />
|
||||
</NavigationView.MenuItems>
|
||||
<NavigationView.PaneFooter>
|
||||
<NavigationView.FooterMenuItems>
|
||||
<NavigationViewItem
|
||||
x:Uid="Shell_WhatsNew"
|
||||
AutomationProperties.AutomationId="WhatIsNewNavItem"
|
||||
Icon="{ui:FontIcon Glyph=}"
|
||||
Tapped="WhatIsNewItem_Tapped" />
|
||||
</NavigationView.PaneFooter>
|
||||
Tag="WhatsNew" />
|
||||
</NavigationView.FooterMenuItems>
|
||||
<NavigationView.Content>
|
||||
<Frame x:Name="NavigationFrame" />
|
||||
</NavigationView.Content>
|
||||
</NavigationView>
|
||||
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup x:Name="LayoutVisualStates">
|
||||
<VisualState x:Name="WideLayout">
|
||||
<VisualState.StateTriggers>
|
||||
<AdaptiveTrigger MinWindowWidth="720" />
|
||||
</VisualState.StateTriggers>
|
||||
</VisualState>
|
||||
<VisualState x:Name="SmallLayout">
|
||||
<VisualState.StateTriggers>
|
||||
<AdaptiveTrigger MinWindowWidth="600" />
|
||||
<AdaptiveTrigger MinWindowWidth="0" />
|
||||
</VisualState.StateTriggers>
|
||||
<VisualState.Setters>
|
||||
<Setter Target="navigationView.PaneDisplayMode" Value="LeftMinimal" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
</Grid>
|
||||
</Page>
|
||||
</UserControl>
|
||||
|
||||
@@ -5,17 +5,18 @@
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Globalization;
|
||||
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.PowerToys.Settings.UI.OOBE.Enums;
|
||||
using Microsoft.PowerToys.Settings.UI.OOBE.ViewModel;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
using WinRT.Interop;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
|
||||
{
|
||||
public sealed partial class OobeShellPage : Page
|
||||
public sealed partial class OobeShellPage : UserControl
|
||||
{
|
||||
public static Func<string> RunSharedEventCallback { get; set; }
|
||||
|
||||
@@ -62,6 +63,7 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
|
||||
|
||||
// NOTE: Experimentation for OOBE is currently turned off on server side. Keeping this code in a comment to allow future experiments.
|
||||
// ExperimentationToggleSwitchEnabled = SettingsRepository<GeneralSettings>.GetInstance(settingsUtils).SettingsConfig.EnableExperimentation;
|
||||
SetTitleBar();
|
||||
DataContext = ViewModel;
|
||||
OobeShellHandler = this;
|
||||
Modules = new ObservableCollection<OobePowerToysModule>();
|
||||
@@ -200,6 +202,12 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
|
||||
IsNew = true,
|
||||
});
|
||||
|
||||
Modules.Insert((int)PowerToysModules.WhatsNew, new OobePowerToysModule()
|
||||
{
|
||||
ModuleName = "WhatsNew",
|
||||
IsNew = false,
|
||||
});
|
||||
|
||||
Modules.Insert((int)PowerToysModules.RegistryPreview, new OobePowerToysModule()
|
||||
{
|
||||
ModuleName = "RegistryPreview",
|
||||
@@ -221,7 +229,7 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
|
||||
|
||||
public void OnClosing()
|
||||
{
|
||||
NavigationViewItem selectedItem = this.navigationView.SelectedItem as NavigationViewItem;
|
||||
Microsoft.UI.Xaml.Controls.NavigationViewItem selectedItem = this.navigationView.SelectedItem as Microsoft.UI.Xaml.Controls.NavigationViewItem;
|
||||
if (selectedItem != null)
|
||||
{
|
||||
Modules[(int)(PowerToysModules)Enum.Parse(typeof(PowerToysModules), (string)selectedItem.Tag, true)].LogClosingModuleEvent();
|
||||
@@ -230,22 +238,19 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
|
||||
|
||||
public void NavigateToModule(PowerToysModules selectedModule)
|
||||
{
|
||||
navigationView.SelectedItem = navigationView.MenuItems[(int)selectedModule];
|
||||
}
|
||||
|
||||
private static void OpenScoobeWindow()
|
||||
{
|
||||
if (App.GetScoobeWindow() == null)
|
||||
if (selectedModule == PowerToysModules.WhatsNew)
|
||||
{
|
||||
App.SetScoobeWindow(new ScoobeWindow());
|
||||
navigationView.SelectedItem = navigationView.FooterMenuItems[0];
|
||||
}
|
||||
else
|
||||
{
|
||||
navigationView.SelectedItem = navigationView.MenuItems[(int)selectedModule];
|
||||
}
|
||||
|
||||
App.GetScoobeWindow().Activate();
|
||||
}
|
||||
|
||||
private void NavigationView_SelectionChanged(NavigationView sender, NavigationViewSelectionChangedEventArgs args)
|
||||
private void NavigationView_SelectionChanged(Microsoft.UI.Xaml.Controls.NavigationView sender, Microsoft.UI.Xaml.Controls.NavigationViewSelectionChangedEventArgs args)
|
||||
{
|
||||
NavigationViewItem selectedItem = args.SelectedItem as NavigationViewItem;
|
||||
Microsoft.UI.Xaml.Controls.NavigationViewItem selectedItem = args.SelectedItem as Microsoft.UI.Xaml.Controls.NavigationViewItem;
|
||||
|
||||
if (selectedItem != null)
|
||||
{
|
||||
@@ -273,7 +278,7 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
|
||||
break;
|
||||
}
|
||||
*/
|
||||
|
||||
case "WhatsNew": NavigationFrame.Navigate(typeof(OobeWhatsNew)); break;
|
||||
case "AdvancedPaste": NavigationFrame.Navigate(typeof(OobeAdvancedPaste)); break;
|
||||
case "AlwaysOnTop": NavigationFrame.Navigate(typeof(OobeAlwaysOnTop)); break;
|
||||
case "Awake": NavigationFrame.Navigate(typeof(OobeAwake)); break;
|
||||
@@ -306,37 +311,43 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
|
||||
}
|
||||
}
|
||||
|
||||
private void SetTitleBar()
|
||||
{
|
||||
var u = App.GetOobeWindow();
|
||||
if (u != null)
|
||||
{
|
||||
// A custom title bar is required for full window theme and Mica support.
|
||||
// https://docs.microsoft.com/windows/apps/develop/title-bar?tabs=winui3#full-customization
|
||||
u.ExtendsContentIntoTitleBar = true;
|
||||
WindowHelpers.ForceTopBorder1PixelInsetOnWindows10(WindowNative.GetWindowHandle(u));
|
||||
u.SetTitleBar(AppTitleBar);
|
||||
}
|
||||
}
|
||||
|
||||
private void ShellPage_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// Select the first module by default
|
||||
if (navigationView.MenuItems.Count > 0)
|
||||
{
|
||||
navigationView.SelectedItem = navigationView.MenuItems[0];
|
||||
}
|
||||
SetTitleBar();
|
||||
}
|
||||
|
||||
private void NavigationView_DisplayModeChanged(NavigationView sender, NavigationViewDisplayModeChangedEventArgs args)
|
||||
{
|
||||
if (args.DisplayMode == NavigationViewDisplayMode.Compact || args.DisplayMode == NavigationViewDisplayMode.Minimal)
|
||||
{
|
||||
TitleBarIcon.Margin = new Thickness(0, 0, 8, 0); // Workaround, see XAML comment
|
||||
AppTitleBar.IsPaneToggleButtonVisible = true;
|
||||
PaneToggleBtn.Visibility = Visibility.Visible;
|
||||
AppTitleBar.Margin = new Thickness(48, 0, 0, 0);
|
||||
AppTitleBarText.Margin = new Thickness(12, 0, 0, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
TitleBarIcon.Margin = new Thickness(16, 0, 0, 0); // Workaround, see XAML comment
|
||||
AppTitleBar.IsPaneToggleButtonVisible = false;
|
||||
PaneToggleBtn.Visibility = Visibility.Collapsed;
|
||||
AppTitleBar.Margin = new Thickness(16, 0, 0, 0);
|
||||
AppTitleBarText.Margin = new Thickness(16, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
private void TitleBar_PaneButtonClick(TitleBar sender, object args)
|
||||
private void PaneToggleBtn_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
navigationView.IsPaneOpen = !navigationView.IsPaneOpen;
|
||||
}
|
||||
|
||||
private void WhatIsNewItem_Tapped(object sender, TappedRoutedEventArgs e)
|
||||
{
|
||||
OpenScoobeWindow();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
<Page
|
||||
x:Class="Microsoft.PowerToys.Settings.UI.OOBE.Views.OobeWhatsNew"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:controls="using:Microsoft.PowerToys.Settings.UI.Controls"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls"
|
||||
xmlns:ui="using:CommunityToolkit.WinUI"
|
||||
Loaded="Page_Loaded"
|
||||
mc:Ignorable="d">
|
||||
<Page.Resources>
|
||||
<tkcontrols:MarkdownThemes
|
||||
x:Key="ReleaseNotesMarkdownThemeConfig"
|
||||
H1FontSize="22"
|
||||
H1FontWeight="SemiBold"
|
||||
H1Margin="0, 36, 0, 8"
|
||||
H2FontSize="16"
|
||||
H2FontWeight="SemiBold"
|
||||
H2Margin="0, 16, 0, 4"
|
||||
H3FontSize="16"
|
||||
H3FontWeight="SemiBold"
|
||||
H3Margin="0, 16, 0, 4" />
|
||||
<tkcontrols:MarkdownConfig x:Key="ReleaseNotesMarkdownConfig" Themes="{StaticResource ReleaseNotesMarkdownThemeConfig}" />
|
||||
</Page.Resources>
|
||||
|
||||
<!-- Main layout container -->
|
||||
<Grid>
|
||||
<!-- Main content grid -->
|
||||
<Grid Margin="0,24,0,0">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Compact Header overlay that covers both InfoBar and Title sections -->
|
||||
<Border
|
||||
x:Name="HeaderOverlay"
|
||||
Grid.Row="0"
|
||||
Grid.RowSpan="2"
|
||||
Margin="0,-24,0,0"
|
||||
VerticalAlignment="Top"
|
||||
BorderThickness="0"
|
||||
Canvas.ZIndex="1">
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<tkcontrols:SettingsCard
|
||||
x:Name="WhatsNewDataDiagnosticsInfoBar"
|
||||
x:Uid="Oobe_WhatsNew_DataDiagnostics_InfoBar"
|
||||
Grid.Row="0"
|
||||
Padding="12,8,12,8"
|
||||
Background="{ThemeResource InfoBarInformationalSeverityBackgroundBrush}"
|
||||
IsTabStop="{x:Bind ShowDataDiagnosticsInfoBar, Mode=OneWay}"
|
||||
Visibility="{x:Bind ShowDataDiagnosticsInfoBar, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
<tkcontrols:SettingsCard.HeaderIcon>
|
||||
<FontIcon Foreground="{ThemeResource InfoBarInformationalSeverityIconBackground}" Glyph="" />
|
||||
</tkcontrols:SettingsCard.HeaderIcon>
|
||||
<tkcontrols:SettingsCard.Description>
|
||||
<StackPanel>
|
||||
<TextBlock x:Name="WhatsNewDataDiagnosticsInfoBarDescText">
|
||||
<Hyperlink NavigateUri="https://aka.ms/powertoys-data-and-privacy-documentation">
|
||||
<Run x:Uid="Oobe_WhatsNew_DataDiagnostics_InfoBar_Desc" />
|
||||
</Hyperlink>
|
||||
</TextBlock>
|
||||
<TextBlock x:Name="WhatsNewDataDiagnosticsInfoBarDescTextYesClicked" Visibility="Collapsed">
|
||||
<Run x:Uid="Oobe_WhatsNew_DataDiagnostics_Yes_Click_InfoBar_Desc" />
|
||||
<Hyperlink Click="DataDiagnostics_OpenSettings_Click">
|
||||
<Run x:Uid="Oobe_WhatsNew_DataDiagnostics_Yes_Click_OpenSettings_Text" />
|
||||
</Hyperlink>
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
</tkcontrols:SettingsCard.Description>
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Button
|
||||
x:Name="DataDiagnosticsButtonYes"
|
||||
x:Uid="Oobe_WhatsNew_DataDiagnostics_Button_Yes"
|
||||
Click="DataDiagnostics_InfoBar_YesNo_Click"
|
||||
CommandParameter="Yes" />
|
||||
<HyperlinkButton
|
||||
x:Name="DataDiagnosticsButtonNo"
|
||||
x:Uid="Oobe_WhatsNew_DataDiagnostics_Button_No"
|
||||
Click="DataDiagnostics_InfoBar_YesNo_Click"
|
||||
CommandParameter="No" />
|
||||
<Button
|
||||
Margin="16,0,0,0"
|
||||
Click="DataDiagnostics_InfoBar_Close_Click"
|
||||
Content="{ui:FontIcon Glyph=,
|
||||
FontSize=16}"
|
||||
Style="{StaticResource SubtleButtonStyle}" />
|
||||
</StackPanel>
|
||||
</tkcontrols:SettingsCard>
|
||||
|
||||
<Grid Grid.Row="1" Margin="16,12,0,12">
|
||||
<StackPanel
|
||||
VerticalAlignment="Top"
|
||||
Orientation="Vertical"
|
||||
Spacing="4">
|
||||
<TextBlock
|
||||
x:Uid="Oobe_WhatsNew"
|
||||
AutomationProperties.HeadingLevel="Level1"
|
||||
Style="{StaticResource TitleTextBlockStyle}" />
|
||||
<HyperlinkButton NavigateUri="https://github.com/microsoft/PowerToys/releases" Style="{StaticResource TextButtonStyle}">
|
||||
<TextBlock x:Uid="Oobe_WhatsNew_DetailedReleaseNotesLink" TextWrapping="Wrap" />
|
||||
</HyperlinkButton>
|
||||
</StackPanel>
|
||||
|
||||
<!-- ShortcutConflictControl positioned at the right side -->
|
||||
<controls:ShortcutConflictControl
|
||||
Grid.RowSpan="2"
|
||||
Margin="0,0,16,0"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
AllHotkeyConflictsData="{x:Bind AllHotkeyConflictsData, Mode=OneWay}"
|
||||
Visibility="{x:Bind HasConflicts, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Reduced spacer for the compact header overlay -->
|
||||
<Grid Grid.Row="0" Height="0" />
|
||||
<Grid Grid.Row="1" Height="80" />
|
||||
|
||||
<InfoBar
|
||||
x:Name="ErrorInfoBar"
|
||||
x:Uid="Oobe_WhatsNew_LoadingError"
|
||||
Grid.Row="2"
|
||||
VerticalAlignment="Top"
|
||||
IsClosable="False"
|
||||
IsTabStop="False"
|
||||
Severity="Error">
|
||||
<InfoBar.ActionButton>
|
||||
<Button
|
||||
x:Uid="RetryBtn"
|
||||
HorizontalAlignment="Right"
|
||||
Click="LoadReleaseNotes_Click">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<FontIcon FontSize="16" Glyph="" />
|
||||
<TextBlock x:Uid="RetryLabel" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</InfoBar.ActionButton>
|
||||
</InfoBar>
|
||||
<InfoBar
|
||||
x:Name="ProxyWarningInfoBar"
|
||||
x:Uid="Oobe_WhatsNew_ProxyAuthenticationWarning"
|
||||
Grid.Row="2"
|
||||
VerticalAlignment="Top"
|
||||
IsClosable="False"
|
||||
IsTabStop="False"
|
||||
Severity="Warning">
|
||||
<InfoBar.ActionButton>
|
||||
<Button
|
||||
x:Uid="RetryBtn"
|
||||
HorizontalAlignment="Right"
|
||||
Click="LoadReleaseNotes_Click">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<FontIcon FontSize="16" Glyph="" />
|
||||
<TextBlock x:Uid="RetryLabel" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</InfoBar.ActionButton>
|
||||
</InfoBar>
|
||||
|
||||
<ScrollViewer Grid.Row="3" VerticalScrollBarVisibility="Auto">
|
||||
<Grid Margin="32,16,32,24">
|
||||
<ProgressRing
|
||||
x:Name="LoadingProgressRing"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
IsIndeterminate="True"
|
||||
Visibility="Visible" />
|
||||
<tkcontrols:MarkdownTextBlock
|
||||
x:Name="ReleaseNotesMarkdown"
|
||||
Config="{StaticResource ReleaseNotesMarkdownConfig}"
|
||||
UseAutoLinks="True"
|
||||
UseEmphasisExtras="True"
|
||||
UseListExtras="True"
|
||||
UsePipeTables="True"
|
||||
UseTaskLists="True" />
|
||||
</Grid>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Page>
|
||||
@@ -0,0 +1,359 @@
|
||||
// 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.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.WinUI.Controls;
|
||||
using global::PowerToys.GPOWrapper;
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Settings.UI.Helpers;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts;
|
||||
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
|
||||
using Microsoft.PowerToys.Settings.UI.OOBE.Enums;
|
||||
using Microsoft.PowerToys.Settings.UI.OOBE.ViewModel;
|
||||
using Microsoft.PowerToys.Settings.UI.SerializationContext;
|
||||
using Microsoft.PowerToys.Settings.UI.Services;
|
||||
using Microsoft.PowerToys.Settings.UI.Views;
|
||||
using Microsoft.PowerToys.Telemetry;
|
||||
using Microsoft.UI.Text;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Microsoft.UI.Xaml.Navigation;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
|
||||
{
|
||||
public sealed partial class OobeWhatsNew : Page, INotifyPropertyChanged
|
||||
{
|
||||
public OobePowerToysModule ViewModel { get; set; }
|
||||
|
||||
private AllHotkeyConflictsData _allHotkeyConflictsData = new AllHotkeyConflictsData();
|
||||
|
||||
public bool ShowDataDiagnosticsInfoBar => GetShowDataDiagnosticsInfoBar();
|
||||
|
||||
private int _conflictCount;
|
||||
|
||||
public AllHotkeyConflictsData AllHotkeyConflictsData
|
||||
{
|
||||
get => _allHotkeyConflictsData;
|
||||
set
|
||||
{
|
||||
if (_allHotkeyConflictsData != value)
|
||||
{
|
||||
_allHotkeyConflictsData = value;
|
||||
|
||||
UpdateConflictCount();
|
||||
|
||||
OnPropertyChanged(nameof(AllHotkeyConflictsData));
|
||||
OnPropertyChanged(nameof(HasConflicts));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool HasConflicts => _conflictCount > 0;
|
||||
|
||||
private void UpdateConflictCount()
|
||||
{
|
||||
int count = 0;
|
||||
if (AllHotkeyConflictsData == null)
|
||||
{
|
||||
_conflictCount = count;
|
||||
}
|
||||
|
||||
if (AllHotkeyConflictsData.InAppConflicts != null)
|
||||
{
|
||||
foreach (var inAppConflict in AllHotkeyConflictsData.InAppConflicts)
|
||||
{
|
||||
if (!inAppConflict.ConflictIgnored)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (AllHotkeyConflictsData.SystemConflicts != null)
|
||||
{
|
||||
foreach (var systemConflict in AllHotkeyConflictsData.SystemConflicts)
|
||||
{
|
||||
if (!systemConflict.ConflictIgnored)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_conflictCount = count;
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="OobeWhatsNew"/> class.
|
||||
/// </summary>
|
||||
public OobeWhatsNew()
|
||||
{
|
||||
this.InitializeComponent();
|
||||
ViewModel = new OobePowerToysModule(OobeShellPage.OobeShellHandler.Modules[(int)PowerToysModules.WhatsNew]);
|
||||
DataContext = this;
|
||||
|
||||
// Subscribe to hotkey conflict updates
|
||||
if (GlobalHotkeyConflictManager.Instance != null)
|
||||
{
|
||||
GlobalHotkeyConflictManager.Instance.ConflictsUpdated += OnConflictsUpdated;
|
||||
GlobalHotkeyConflictManager.Instance.RequestAllConflicts();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnConflictsUpdated(object sender, AllHotkeyConflictsEventArgs e)
|
||||
{
|
||||
this.DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Normal, () =>
|
||||
{
|
||||
var allConflictData = e.Conflicts;
|
||||
foreach (var inAppConflict in allConflictData.InAppConflicts)
|
||||
{
|
||||
var hotkey = inAppConflict.Hotkey;
|
||||
var hotkeySetting = new HotkeySettings(hotkey.Win, hotkey.Ctrl, hotkey.Alt, hotkey.Shift, hotkey.Key);
|
||||
inAppConflict.ConflictIgnored = HotkeyConflictIgnoreHelper.IsIgnoringConflicts(hotkeySetting);
|
||||
}
|
||||
|
||||
foreach (var systemConflict in allConflictData.SystemConflicts)
|
||||
{
|
||||
var hotkey = systemConflict.Hotkey;
|
||||
var hotkeySetting = new HotkeySettings(hotkey.Win, hotkey.Ctrl, hotkey.Alt, hotkey.Shift, hotkey.Key);
|
||||
systemConflict.ConflictIgnored = HotkeyConflictIgnoreHelper.IsIgnoringConflicts(hotkeySetting);
|
||||
}
|
||||
|
||||
AllHotkeyConflictsData = e.Conflicts ?? new AllHotkeyConflictsData();
|
||||
});
|
||||
}
|
||||
|
||||
private void OnPropertyChanged(string propertyName)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
|
||||
private bool GetShowDataDiagnosticsInfoBar()
|
||||
{
|
||||
var isDataDiagnosticsGpoDisallowed = GPOWrapper.GetAllowDataDiagnosticsValue() == GpoRuleConfigured.Disabled;
|
||||
|
||||
if (isDataDiagnosticsGpoDisallowed)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool userActed = DataDiagnosticsSettings.GetUserActionValue();
|
||||
|
||||
if (userActed)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool registryValue = DataDiagnosticsSettings.GetEnabledValue();
|
||||
|
||||
bool isFirstRunAfterUpdate = (App.Current as Microsoft.PowerToys.Settings.UI.App).ShowScoobe;
|
||||
if (isFirstRunAfterUpdate && registryValue == false)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Regex to remove installer hash sections from the release notes.
|
||||
/// </summary>
|
||||
private const string RemoveInstallerHashesRegex = @"(\r\n)+## Installer Hashes(\r\n.*)+## Highlights";
|
||||
private const string RemoveHotFixInstallerHashesRegex = @"(\r\n)+## Installer Hashes(\r\n.*)+$";
|
||||
private const RegexOptions RemoveInstallerHashesRegexOptions = RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant;
|
||||
private bool _loadingReleaseNotes;
|
||||
|
||||
private static async Task<string> GetReleaseNotesMarkdown()
|
||||
{
|
||||
string releaseNotesJSON = string.Empty;
|
||||
|
||||
// Let's use system proxy
|
||||
using var proxyClientHandler = new HttpClientHandler
|
||||
{
|
||||
DefaultProxyCredentials = CredentialCache.DefaultCredentials,
|
||||
Proxy = WebRequest.GetSystemWebProxy(),
|
||||
PreAuthenticate = true,
|
||||
};
|
||||
|
||||
using var getReleaseInfoClient = new HttpClient(proxyClientHandler);
|
||||
|
||||
// GitHub APIs require sending an user agent
|
||||
// https://docs.github.com/rest/overview/resources-in-the-rest-api#user-agent-required
|
||||
getReleaseInfoClient.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", "PowerToys");
|
||||
releaseNotesJSON = await getReleaseInfoClient.GetStringAsync("https://api.github.com/repos/microsoft/PowerToys/releases");
|
||||
IList<PowerToysReleaseInfo> releases = JsonSerializer.Deserialize<IList<PowerToysReleaseInfo>>(releaseNotesJSON, SourceGenerationContextContext.Default.IListPowerToysReleaseInfo);
|
||||
|
||||
// Get the latest releases
|
||||
var latestReleases = releases.OrderByDescending(release => release.PublishedDate).Take(5);
|
||||
|
||||
StringBuilder releaseNotesHtmlBuilder = new StringBuilder(string.Empty);
|
||||
|
||||
// Regex to remove installer hash sections from the release notes.
|
||||
Regex removeHashRegex = new Regex(RemoveInstallerHashesRegex, RemoveInstallerHashesRegexOptions);
|
||||
|
||||
// Regex to remove installer hash sections from the release notes, since there'll be no Highlights section for hotfix releases.
|
||||
Regex removeHotfixHashRegex = new Regex(RemoveHotFixInstallerHashesRegex, RemoveInstallerHashesRegexOptions);
|
||||
int counter = 0;
|
||||
foreach (var release in latestReleases)
|
||||
{
|
||||
releaseNotesHtmlBuilder.AppendLine("# " + release.Name);
|
||||
var notes = removeHashRegex.Replace(release.ReleaseNotes, "\r\n### Highlights");
|
||||
|
||||
// Add a unique counter to [github-current-release-work] to distinguish each release,
|
||||
// since this variable is used for all latest releases when they are merged.
|
||||
notes = notes.Replace("[github-current-release-work]", $"[github-current-release-work{++counter}]");
|
||||
notes = removeHotfixHashRegex.Replace(notes, string.Empty);
|
||||
releaseNotesHtmlBuilder.AppendLine(notes);
|
||||
releaseNotesHtmlBuilder.AppendLine(" ");
|
||||
}
|
||||
|
||||
return releaseNotesHtmlBuilder.ToString();
|
||||
}
|
||||
|
||||
private async Task Reload()
|
||||
{
|
||||
if (_loadingReleaseNotes)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_loadingReleaseNotes = true;
|
||||
ReleaseNotesMarkdown.Visibility = Microsoft.UI.Xaml.Visibility.Collapsed;
|
||||
LoadingProgressRing.Visibility = Microsoft.UI.Xaml.Visibility.Visible;
|
||||
string releaseNotesMarkdown = await GetReleaseNotesMarkdown();
|
||||
ProxyWarningInfoBar.IsOpen = false;
|
||||
ErrorInfoBar.IsOpen = false;
|
||||
|
||||
ReleaseNotesMarkdown.Text = releaseNotesMarkdown;
|
||||
ReleaseNotesMarkdown.Visibility = Microsoft.UI.Xaml.Visibility.Visible;
|
||||
LoadingProgressRing.Visibility = Microsoft.UI.Xaml.Visibility.Collapsed;
|
||||
}
|
||||
catch (HttpRequestException httpEx)
|
||||
{
|
||||
Logger.LogError("Exception when loading the release notes", httpEx);
|
||||
if (httpEx.Message.Contains("407", StringComparison.CurrentCulture))
|
||||
{
|
||||
ProxyWarningInfoBar.IsOpen = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
ErrorInfoBar.IsOpen = true;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Exception when loading the release notes", ex);
|
||||
ErrorInfoBar.IsOpen = true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
LoadingProgressRing.Visibility = Microsoft.UI.Xaml.Visibility.Collapsed;
|
||||
_loadingReleaseNotes = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async void Page_Loaded(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
|
||||
{
|
||||
await Reload();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnNavigatedTo(NavigationEventArgs e)
|
||||
{
|
||||
ViewModel.LogOpeningModuleEvent();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnNavigatedFrom(NavigationEventArgs e)
|
||||
{
|
||||
ViewModel.LogClosingModuleEvent();
|
||||
|
||||
// Unsubscribe from conflict updates when leaving the page
|
||||
if (GlobalHotkeyConflictManager.Instance != null)
|
||||
{
|
||||
GlobalHotkeyConflictManager.Instance.ConflictsUpdated -= OnConflictsUpdated;
|
||||
}
|
||||
}
|
||||
|
||||
private void DataDiagnostics_InfoBar_YesNo_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
|
||||
{
|
||||
string commandArg = string.Empty;
|
||||
if (sender is Button senderBtn)
|
||||
{
|
||||
commandArg = senderBtn.CommandParameter.ToString();
|
||||
}
|
||||
else if (sender is HyperlinkButton senderLink)
|
||||
{
|
||||
commandArg = senderLink.CommandParameter.ToString();
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(commandArg))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Update UI
|
||||
if (commandArg == "Yes")
|
||||
{
|
||||
WhatsNewDataDiagnosticsInfoBar.Header = ResourceLoaderInstance.ResourceLoader.GetString("Oobe_WhatsNew_DataDiagnostics_Yes_Click_InfoBar_Title");
|
||||
}
|
||||
else
|
||||
{
|
||||
WhatsNewDataDiagnosticsInfoBar.Header = ResourceLoaderInstance.ResourceLoader.GetString("Oobe_WhatsNew_DataDiagnostics_No_Click_InfoBar_Title");
|
||||
}
|
||||
|
||||
WhatsNewDataDiagnosticsInfoBarDescText.Visibility = Microsoft.UI.Xaml.Visibility.Collapsed;
|
||||
WhatsNewDataDiagnosticsInfoBarDescTextYesClicked.Visibility = Microsoft.UI.Xaml.Visibility.Visible;
|
||||
DataDiagnosticsButtonYes.Visibility = Microsoft.UI.Xaml.Visibility.Collapsed;
|
||||
DataDiagnosticsButtonNo.Visibility = Microsoft.UI.Xaml.Visibility.Collapsed;
|
||||
|
||||
// Set Data Diagnostics registry values
|
||||
if (commandArg == "Yes")
|
||||
{
|
||||
DataDiagnosticsSettings.SetEnabledValue(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
DataDiagnosticsSettings.SetEnabledValue(false);
|
||||
}
|
||||
|
||||
DataDiagnosticsSettings.SetUserActionValue(true);
|
||||
|
||||
this.DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Normal, () =>
|
||||
{
|
||||
ShellPage.ShellHandler?.SignalGeneralDataUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
private void DataDiagnostics_InfoBar_Close_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
|
||||
{
|
||||
WhatsNewDataDiagnosticsInfoBar.Visibility = Microsoft.UI.Xaml.Visibility.Collapsed;
|
||||
}
|
||||
|
||||
private void DataDiagnostics_OpenSettings_Click(Microsoft.UI.Xaml.Documents.Hyperlink sender, Microsoft.UI.Xaml.Documents.HyperlinkClickEventArgs args)
|
||||
{
|
||||
Common.UI.SettingsDeepLink.OpenSettings(Common.UI.SettingsDeepLink.SettingsWindow.Overview);
|
||||
}
|
||||
|
||||
private async void LoadReleaseNotes_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
|
||||
{
|
||||
await Reload();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,62 +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.Globalization;
|
||||
|
||||
using Microsoft.PowerToys.Settings.UI.Helpers;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
|
||||
{
|
||||
/// <summary>
|
||||
/// View model for a group of releases (grouped by major.minor version).
|
||||
/// </summary>
|
||||
public class ScoobeReleaseGroupViewModel
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the list of releases in this group.
|
||||
/// </summary>
|
||||
public IList<PowerToysReleaseInfo> Releases { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the version text to display (e.g., "0.96.0").
|
||||
/// </summary>
|
||||
public string VersionText { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the date text to display (e.g., "December 2025").
|
||||
/// </summary>
|
||||
public string DateText { get; }
|
||||
|
||||
public ScoobeReleaseGroupViewModel(IList<PowerToysReleaseInfo> releases)
|
||||
{
|
||||
Releases = releases ?? throw new ArgumentNullException(nameof(releases));
|
||||
|
||||
if (releases.Count > 0)
|
||||
{
|
||||
var latestRelease = releases[0];
|
||||
VersionText = GetVersionFromRelease(latestRelease);
|
||||
DateText = latestRelease.PublishedDate.ToString("MMMM yyyy", CultureInfo.CurrentCulture);
|
||||
}
|
||||
else
|
||||
{
|
||||
VersionText = "Unknown";
|
||||
DateText = string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetVersionFromRelease(PowerToysReleaseInfo release)
|
||||
{
|
||||
// TagName is typically like "v0.96.0", Name might be "Release v0.96.0"
|
||||
string version = release.TagName ?? release.Name ?? "Unknown";
|
||||
if (version.StartsWith("v", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
version = version.Substring(1);
|
||||
}
|
||||
|
||||
return version;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
<Page
|
||||
x:Class="Microsoft.PowerToys.Settings.UI.OOBE.Views.ScoobeReleaseNotesPage"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls"
|
||||
Loaded="Page_Loaded"
|
||||
mc:Ignorable="d">
|
||||
<Page.Resources>
|
||||
<tkcontrols:MarkdownThemes
|
||||
x:Key="ReleaseNotesMarkdownThemeConfig"
|
||||
BoldFontWeight="SemiBold"
|
||||
H1FontSize="28"
|
||||
H1FontWeight="SemiBold"
|
||||
H1Margin="0, 36, 0, 8"
|
||||
H2FontSize="20"
|
||||
H2FontWeight="SemiBold"
|
||||
H2Margin="0, 16, 0, 4"
|
||||
H3FontSize="16"
|
||||
H3FontWeight="SemiBold"
|
||||
H3Margin="0, 16, 0, 4"
|
||||
HorizontalRuleBrush="{StaticResource DividerStrokeColorDefaultBrush}"
|
||||
HorizontalRuleThickness="1"
|
||||
ImageStretch="Uniform"
|
||||
ListBulletSpacing="1"
|
||||
ListGutterWidth="10" />
|
||||
<tkcontrols:MarkdownConfig x:Key="ReleaseNotesMarkdownConfig" Themes="{StaticResource ReleaseNotesMarkdownThemeConfig}" />
|
||||
</Page.Resources>
|
||||
|
||||
<!-- Main layout container -->
|
||||
<Grid MaxWidth="1000">
|
||||
<ScrollViewer Padding="0,0,0,0" VerticalScrollBarVisibility="Auto">
|
||||
<Grid Margin="0,0,0,24">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<Image
|
||||
x:Name="HeroImageHolder"
|
||||
Height="186"
|
||||
HorizontalAlignment="Left"
|
||||
Stretch="UniformToFill" />
|
||||
<Grid Grid.Row="1" Margin="24,16,24,24">
|
||||
<ProgressRing
|
||||
x:Name="LoadingProgressRing"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
IsIndeterminate="True"
|
||||
Visibility="Visible" />
|
||||
<tkcontrols:MarkdownTextBlock
|
||||
x:Name="ReleaseNotesMarkdown"
|
||||
Config="{StaticResource ReleaseNotesMarkdownConfig}"
|
||||
UseAutoLinks="True"
|
||||
UseEmphasisExtras="True"
|
||||
UseListExtras="True"
|
||||
UsePipeTables="True"
|
||||
UseTaskLists="True" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</Page>
|
||||
@@ -1,165 +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.Globalization;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Settings.UI.Helpers;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Media.Imaging;
|
||||
using Microsoft.UI.Xaml.Navigation;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
|
||||
{
|
||||
public sealed partial class ScoobeReleaseNotesPage : Page
|
||||
{
|
||||
private IList<PowerToysReleaseInfo> _currentReleases;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ScoobeReleaseNotesPage"/> class.
|
||||
/// </summary>
|
||||
public ScoobeReleaseNotesPage()
|
||||
{
|
||||
this.InitializeComponent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Regex to remove installer hash sections from the release notes.
|
||||
/// </summary>
|
||||
private const string RemoveInstallerHashesRegex = @"(\r\n)+## Installer Hashes(\r\n.*)+## Highlights";
|
||||
private const string RemoveHotFixInstallerHashesRegex = @"(\r\n)+## Installer Hashes(\r\n.*)+$";
|
||||
private const RegexOptions RemoveInstallerHashesRegexOptions = RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant;
|
||||
|
||||
/// <summary>
|
||||
/// Regex to match markdown images with 'Hero' in the alt text.
|
||||
/// Matches: 
|
||||
/// </summary>
|
||||
private static readonly Regex HeroImageRegex = new Regex(
|
||||
@"!\[([^\]]*Hero[^\]]*)\]\(([^)]+)\)",
|
||||
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
/// <summary>
|
||||
/// Regex to match GitHub PR/Issue references (e.g., #41029).
|
||||
/// Only matches # followed by digits that are not already part of a markdown link.
|
||||
/// </summary>
|
||||
private static readonly Regex GitHubPrReferenceRegex = new Regex(
|
||||
@"(?<!\[)#(\d+)(?!\])",
|
||||
RegexOptions.Compiled);
|
||||
|
||||
private static readonly CompositeFormat GitHubPrLinkTemplate = CompositeFormat.Parse("[#{0}](https://github.com/microsoft/PowerToys/pull/{0})");
|
||||
private static readonly CompositeFormat GitHubReleaseLinkTemplate = CompositeFormat.Parse("https://github.com/microsoft/PowerToys/releases/tag/{0}");
|
||||
|
||||
private static (string Markdown, string HeroImageUrl) ProcessReleaseNotesMarkdown(IList<PowerToysReleaseInfo> releases)
|
||||
{
|
||||
if (releases == null || releases.Count == 0)
|
||||
{
|
||||
return (string.Empty, null);
|
||||
}
|
||||
|
||||
StringBuilder releaseNotesHtmlBuilder = new StringBuilder(string.Empty);
|
||||
|
||||
// Regex to remove installer hash sections from the release notes.
|
||||
Regex removeHashRegex = new Regex(RemoveInstallerHashesRegex, RemoveInstallerHashesRegexOptions);
|
||||
|
||||
// Regex to remove installer hash sections from the release notes, since there'll be no Highlights section for hotfix releases.
|
||||
Regex removeHotfixHashRegex = new Regex(RemoveHotFixInstallerHashesRegex, RemoveInstallerHashesRegexOptions);
|
||||
|
||||
string lastHeroImageUrl = null;
|
||||
|
||||
int counter = 0;
|
||||
bool isFirst = true;
|
||||
foreach (var release in releases)
|
||||
{
|
||||
// Add separator between releases
|
||||
if (!isFirst)
|
||||
{
|
||||
releaseNotesHtmlBuilder.AppendLine("---");
|
||||
releaseNotesHtmlBuilder.AppendLine();
|
||||
}
|
||||
|
||||
isFirst = false;
|
||||
|
||||
var releaseUrl = string.Format(CultureInfo.InvariantCulture, GitHubReleaseLinkTemplate, release.TagName);
|
||||
releaseNotesHtmlBuilder.AppendLine(CultureInfo.InvariantCulture, $"# {release.Name}");
|
||||
releaseNotesHtmlBuilder.AppendLine(CultureInfo.InvariantCulture, $"{release.PublishedDate.ToString("MMMM d, yyyy", CultureInfo.CurrentCulture)} <20> [View on GitHub]({releaseUrl})");
|
||||
releaseNotesHtmlBuilder.AppendLine();
|
||||
releaseNotesHtmlBuilder.AppendLine(" ");
|
||||
releaseNotesHtmlBuilder.AppendLine();
|
||||
var notes = removeHashRegex.Replace(release.ReleaseNotes, "\r\n## Highlights");
|
||||
notes = notes.Replace("[github-current-release-work]", $"[github-current-release-work{++counter}]");
|
||||
notes = removeHotfixHashRegex.Replace(notes, string.Empty);
|
||||
|
||||
// Find all Hero images and keep track of the last one
|
||||
var heroMatches = HeroImageRegex.Matches(notes);
|
||||
foreach (Match match in heroMatches)
|
||||
{
|
||||
lastHeroImageUrl = match.Groups[2].Value;
|
||||
}
|
||||
|
||||
// Remove Hero images from the markdown
|
||||
notes = HeroImageRegex.Replace(notes, string.Empty);
|
||||
|
||||
// Convert GitHub PR/Issue references to hyperlinks
|
||||
notes = GitHubPrReferenceRegex.Replace(notes, match =>
|
||||
string.Format(CultureInfo.InvariantCulture, GitHubPrLinkTemplate, match.Groups[1].Value));
|
||||
|
||||
releaseNotesHtmlBuilder.AppendLine(notes);
|
||||
releaseNotesHtmlBuilder.AppendLine(" ");
|
||||
}
|
||||
|
||||
return (releaseNotesHtmlBuilder.ToString(), lastHeroImageUrl);
|
||||
}
|
||||
|
||||
private void DisplayReleaseNotes()
|
||||
{
|
||||
if (_currentReleases == null || _currentReleases.Count == 0)
|
||||
{
|
||||
ReleaseNotesMarkdown.Visibility = Visibility.Collapsed;
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
LoadingProgressRing.Visibility = Visibility.Collapsed;
|
||||
|
||||
var (releaseNotesMarkdown, heroImageUrl) = ProcessReleaseNotesMarkdown(_currentReleases);
|
||||
|
||||
// Set the Hero image if found
|
||||
if (!string.IsNullOrEmpty(heroImageUrl))
|
||||
{
|
||||
HeroImageHolder.Source = new BitmapImage(new Uri(heroImageUrl));
|
||||
HeroImageHolder.Visibility = Visibility.Visible;
|
||||
}
|
||||
else
|
||||
{
|
||||
HeroImageHolder.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
|
||||
ReleaseNotesMarkdown.Text = releaseNotesMarkdown;
|
||||
ReleaseNotesMarkdown.Visibility = Visibility.Visible;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Exception when displaying the release notes", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void Page_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
DisplayReleaseNotes();
|
||||
}
|
||||
|
||||
protected override void OnNavigatedTo(NavigationEventArgs e)
|
||||
{
|
||||
if (e.Parameter is IList<PowerToysReleaseInfo> releases)
|
||||
{
|
||||
_currentReleases = releases;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
<Page
|
||||
x:Class="Microsoft.PowerToys.Settings.UI.OOBE.Views.ScoobeShellPage"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="using:Microsoft.PowerToys.Settings.UI.OOBE.Views"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
HighContrastAdjustment="None"
|
||||
Loaded="ShellPage_Loaded"
|
||||
mc:Ignorable="d">
|
||||
<Page.Resources>
|
||||
<!-- Template for NavigationViewItem content with version and date -->
|
||||
<DataTemplate x:Key="ReleaseNavItemTemplate" x:DataType="local:ScoobeReleaseGroupViewModel">
|
||||
<StackPanel
|
||||
Margin="0,8,0,8"
|
||||
Orientation="Vertical"
|
||||
Spacing="4">
|
||||
<TextBlock Style="{StaticResource BodyTextBlockStyle}" Text="{x:Bind DateText}" />
|
||||
<TextBlock
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{x:Bind VersionText}" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</Page.Resources>
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="48" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<TitleBar
|
||||
x:Name="AppTitleBar"
|
||||
x:Uid="ScoobeWindow_TitleTxt"
|
||||
IsBackButtonVisible="False"
|
||||
IsPaneToggleButtonVisible="False"
|
||||
PaneToggleRequested="TitleBar_PaneButtonClick">
|
||||
<!-- 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="TitleBarIcon"
|
||||
Height="16"
|
||||
Margin="16,0,0,0"
|
||||
Source="/Assets/Settings/icon.ico" />
|
||||
</TitleBar.LeftHeader>
|
||||
</TitleBar>
|
||||
<NavigationView
|
||||
x:Name="navigationView"
|
||||
Grid.Row="1"
|
||||
CompactModeThresholdWidth="1007"
|
||||
DisplayModeChanged="NavigationView_DisplayModeChanged"
|
||||
ExpandedModeThresholdWidth="1007"
|
||||
IsBackButtonVisible="Collapsed"
|
||||
IsPaneOpen="True"
|
||||
IsPaneToggleButtonVisible="False"
|
||||
IsSettingsVisible="False"
|
||||
MenuItemTemplate="{StaticResource ReleaseNavItemTemplate}"
|
||||
OpenPaneLength="186"
|
||||
SelectionChanged="NavigationView_SelectionChanged">
|
||||
<NavigationView.MenuItems>
|
||||
<!-- Items are added dynamically -->
|
||||
</NavigationView.MenuItems>
|
||||
<NavigationView.Content>
|
||||
<Grid>
|
||||
<ProgressRing
|
||||
x:Name="LoadingProgressRing"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
IsIndeterminate="True"
|
||||
Visibility="Collapsed" />
|
||||
<InfoBar
|
||||
x:Name="ErrorInfoBar"
|
||||
x:Uid="Oobe_WhatsNew_LoadingError"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
IsClosable="False"
|
||||
IsOpen="False"
|
||||
Severity="Error">
|
||||
<InfoBar.ActionButton>
|
||||
<Button
|
||||
x:Uid="RetryBtn"
|
||||
HorizontalAlignment="Right"
|
||||
Click="RetryButton_Click">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<FontIcon FontSize="16" Glyph="" />
|
||||
<TextBlock x:Uid="RetryLabel" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</InfoBar.ActionButton>
|
||||
</InfoBar>
|
||||
<Frame x:Name="NavigationFrame" />
|
||||
</Grid>
|
||||
</NavigationView.Content>
|
||||
</NavigationView>
|
||||
</Grid>
|
||||
</Page>
|
||||
@@ -1,194 +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.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Settings.UI.Helpers;
|
||||
using Microsoft.PowerToys.Settings.UI.SerializationContext;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
|
||||
{
|
||||
public sealed partial class ScoobeShellPage : Page
|
||||
{
|
||||
public static Action<Type> OpenMainWindowCallback { get; set; }
|
||||
|
||||
public static void SetOpenMainWindowCallback(Action<Type> implementation)
|
||||
{
|
||||
OpenMainWindowCallback = implementation;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a shell handler to be used to update contents of the shell dynamically from page within the frame.
|
||||
/// </summary>
|
||||
public static ScoobeShellPage ScoobeShellHandler { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of release groups loaded from GitHub (grouped by major.minor version).
|
||||
/// </summary>
|
||||
public IList<IList<PowerToysReleaseInfo>> ReleaseGroups { get; private set; }
|
||||
|
||||
private bool _isLoading;
|
||||
|
||||
public ScoobeShellPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
ScoobeShellHandler = this;
|
||||
}
|
||||
|
||||
private async void ShellPage_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
SetTitleBar();
|
||||
await LoadReleasesAsync();
|
||||
}
|
||||
|
||||
private async Task LoadReleasesAsync()
|
||||
{
|
||||
if (_isLoading)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_isLoading = true;
|
||||
LoadingProgressRing.Visibility = Visibility.Visible;
|
||||
ErrorInfoBar.IsOpen = false;
|
||||
navigationView.MenuItems.Clear();
|
||||
|
||||
try
|
||||
{
|
||||
var releases = await FetchReleasesFromGitHubAsync();
|
||||
ReleaseGroups = GroupReleasesByMajorMinor(releases);
|
||||
PopulateNavigationItems();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Failed to load releases", ex);
|
||||
ErrorInfoBar.IsOpen = true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
LoadingProgressRing.Visibility = Visibility.Collapsed;
|
||||
_isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<IList<PowerToysReleaseInfo>> FetchReleasesFromGitHubAsync()
|
||||
{
|
||||
using var proxyClientHandler = new HttpClientHandler
|
||||
{
|
||||
DefaultProxyCredentials = CredentialCache.DefaultCredentials,
|
||||
Proxy = WebRequest.GetSystemWebProxy(),
|
||||
PreAuthenticate = true,
|
||||
};
|
||||
|
||||
using var httpClient = new HttpClient(proxyClientHandler);
|
||||
httpClient.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", "PowerToys");
|
||||
|
||||
string json = await httpClient.GetStringAsync("https://api.github.com/repos/microsoft/PowerToys/releases?per_page=20");
|
||||
var allReleases = JsonSerializer.Deserialize<IList<PowerToysReleaseInfo>>(json, SourceGenerationContextContext.Default.IListPowerToysReleaseInfo);
|
||||
|
||||
return allReleases
|
||||
.OrderByDescending(r => r.PublishedDate)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static IList<IList<PowerToysReleaseInfo>> GroupReleasesByMajorMinor(IList<PowerToysReleaseInfo> releases)
|
||||
{
|
||||
return releases
|
||||
.GroupBy(r => GetMajorMinorVersion(r))
|
||||
.Select(g => g.OrderByDescending(r => r.PublishedDate).ToList() as IList<PowerToysReleaseInfo>)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static string GetMajorMinorVersion(PowerToysReleaseInfo release)
|
||||
{
|
||||
string version = GetVersionFromRelease(release);
|
||||
var parts = version.Split('.');
|
||||
if (parts.Length >= 2)
|
||||
{
|
||||
return $"{parts[0]}.{parts[1]}";
|
||||
}
|
||||
|
||||
return version;
|
||||
}
|
||||
|
||||
private static string GetVersionFromRelease(PowerToysReleaseInfo release)
|
||||
{
|
||||
// TagName is typically like "v0.96.0", Name might be "Release v0.96.0"
|
||||
string version = release.TagName ?? release.Name ?? "Unknown";
|
||||
if (version.StartsWith("v", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
version = version.Substring(1);
|
||||
}
|
||||
|
||||
return version;
|
||||
}
|
||||
|
||||
private void PopulateNavigationItems()
|
||||
{
|
||||
if (ReleaseGroups == null || ReleaseGroups.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var releaseGroup in ReleaseGroups)
|
||||
{
|
||||
var viewModel = new ScoobeReleaseGroupViewModel(releaseGroup);
|
||||
navigationView.MenuItems.Add(viewModel);
|
||||
}
|
||||
|
||||
// Select the first item to trigger navigation
|
||||
navigationView.SelectedItem = navigationView.MenuItems[0];
|
||||
}
|
||||
|
||||
private void NavigationView_SelectionChanged(NavigationView sender, NavigationViewSelectionChangedEventArgs args)
|
||||
{
|
||||
if (args.SelectedItem is ScoobeReleaseGroupViewModel viewModel)
|
||||
{
|
||||
NavigationFrame.Navigate(typeof(ScoobeReleaseNotesPage), viewModel.Releases);
|
||||
}
|
||||
}
|
||||
|
||||
private async void RetryButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
await LoadReleasesAsync();
|
||||
}
|
||||
|
||||
private void SetTitleBar()
|
||||
{
|
||||
var window = App.GetScoobeWindow();
|
||||
if (window != null)
|
||||
{
|
||||
window.ExtendsContentIntoTitleBar = true;
|
||||
window.SetTitleBar(AppTitleBar);
|
||||
}
|
||||
}
|
||||
|
||||
private void NavigationView_DisplayModeChanged(NavigationView sender, NavigationViewDisplayModeChangedEventArgs args)
|
||||
{
|
||||
if (args.DisplayMode == NavigationViewDisplayMode.Compact || args.DisplayMode == NavigationViewDisplayMode.Minimal)
|
||||
{
|
||||
TitleBarIcon.Margin = new Thickness(0, 0, 8, 0); // Workaround, see XAML comment
|
||||
AppTitleBar.IsPaneToggleButtonVisible = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
TitleBarIcon.Margin = new Thickness(16, 0, 0, 0); // Workaround, see XAML comment
|
||||
AppTitleBar.IsPaneToggleButtonVisible = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void TitleBar_PaneButtonClick(TitleBar sender, object args)
|
||||
{
|
||||
navigationView.IsPaneOpen = !navigationView.IsPaneOpen;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -44,7 +44,6 @@ namespace Microsoft.PowerToys.Settings.UI
|
||||
_windowId = Win32Interop.GetWindowIdFromWindow(_hWnd);
|
||||
_appWindow = AppWindow.GetFromWindowId(_windowId);
|
||||
this.Activated += Window_Activated_SetIcon;
|
||||
this.ExtendsContentIntoTitleBar = true;
|
||||
|
||||
var dpi = NativeMethods.GetDpiForWindow(_hWnd);
|
||||
_currentDPI = dpi;
|
||||
@@ -61,7 +60,7 @@ namespace Microsoft.PowerToys.Settings.UI
|
||||
|
||||
this.SizeChanged += OobeWindow_SizeChanged;
|
||||
|
||||
var loader = ResourceLoaderInstance.ResourceLoader;
|
||||
var loader = Helpers.ResourceLoaderInstance.ResourceLoader;
|
||||
Title = loader.GetString("OobeWindow_Title");
|
||||
|
||||
if (shellPage != null)
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
<winuiex:WindowEx
|
||||
x:Class="Microsoft.PowerToys.Settings.UI.ScoobeWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="using:Microsoft.PowerToys.Settings.UI.OOBE.Views"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:winuiex="using:WinUIEx"
|
||||
MinWidth="480"
|
||||
MinHeight="480"
|
||||
Closed="Window_Closed"
|
||||
mc:Ignorable="d">
|
||||
<Window.SystemBackdrop>
|
||||
<MicaBackdrop />
|
||||
</Window.SystemBackdrop>
|
||||
<local:ScoobeShellPage x:Name="shellPage" />
|
||||
</winuiex:WindowEx>
|
||||
@@ -1,121 +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 Microsoft.PowerToys.Settings.UI.Helpers;
|
||||
using Microsoft.PowerToys.Settings.UI.OOBE.Views;
|
||||
using Microsoft.UI;
|
||||
using Microsoft.UI.Windowing;
|
||||
using Microsoft.UI.Xaml;
|
||||
using PowerToys.Interop;
|
||||
using Windows.Graphics;
|
||||
using WinUIEx;
|
||||
using WinUIEx.Messaging;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI
|
||||
{
|
||||
public sealed partial class ScoobeWindow : WindowEx, IDisposable
|
||||
{
|
||||
private const int ExpectedWidth = 1100;
|
||||
private const int ExpectedHeight = 700;
|
||||
private const int DefaultDPI = 96;
|
||||
private int _currentDPI;
|
||||
private WindowId _windowId;
|
||||
private IntPtr _hWnd;
|
||||
private AppWindow _appWindow;
|
||||
private bool disposedValue;
|
||||
|
||||
public ScoobeWindow()
|
||||
{
|
||||
App.ThemeService.ThemeChanged += OnThemeChanged;
|
||||
App.ThemeService.ApplyTheme();
|
||||
|
||||
this.InitializeComponent();
|
||||
|
||||
_hWnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
|
||||
_windowId = Win32Interop.GetWindowIdFromWindow(_hWnd);
|
||||
_appWindow = AppWindow.GetFromWindowId(_windowId);
|
||||
this.Activated += Window_Activated_SetIcon;
|
||||
this.ExtendsContentIntoTitleBar = true;
|
||||
|
||||
var dpi = NativeMethods.GetDpiForWindow(_hWnd);
|
||||
_currentDPI = dpi;
|
||||
float scalingFactor = (float)dpi / DefaultDPI;
|
||||
int width = (int)(ExpectedWidth * scalingFactor);
|
||||
int height = (int)(ExpectedHeight * scalingFactor);
|
||||
|
||||
SizeInt32 size;
|
||||
size.Width = width;
|
||||
size.Height = height;
|
||||
_appWindow.Resize(size);
|
||||
|
||||
this.SizeChanged += ScoobeWindow_SizeChanged;
|
||||
|
||||
var loader = Helpers.ResourceLoaderInstance.ResourceLoader;
|
||||
Title = loader.GetString("ScoobeWindow_Title");
|
||||
|
||||
ScoobeShellPage.SetOpenMainWindowCallback((Type type) =>
|
||||
{
|
||||
App.OpenSettingsWindow(type);
|
||||
});
|
||||
}
|
||||
|
||||
private void Window_Activated_SetIcon(object sender, WindowActivatedEventArgs args)
|
||||
{
|
||||
// Set window icon
|
||||
_appWindow.SetIcon("Assets\\Settings\\icon.ico");
|
||||
}
|
||||
|
||||
private void ScoobeWindow_SizeChanged(object sender, WindowSizeChangedEventArgs args)
|
||||
{
|
||||
var dpi = NativeMethods.GetDpiForWindow(_hWnd);
|
||||
if (_currentDPI != dpi)
|
||||
{
|
||||
// Reacting to a DPI change. Should not cause a resize -> sizeChanged loop.
|
||||
_currentDPI = dpi;
|
||||
float scalingFactor = (float)dpi / DefaultDPI;
|
||||
int width = (int)(ExpectedWidth * scalingFactor);
|
||||
int height = (int)(ExpectedHeight * scalingFactor);
|
||||
SizeInt32 size;
|
||||
size.Width = width;
|
||||
size.Height = height;
|
||||
_appWindow.Resize(size);
|
||||
}
|
||||
}
|
||||
|
||||
private void Window_Closed(object sender, WindowEventArgs args)
|
||||
{
|
||||
App.ClearScoobeWindow();
|
||||
|
||||
var mainWindow = App.GetSettingsWindow();
|
||||
if (mainWindow != null)
|
||||
{
|
||||
mainWindow.CloseHiddenWindow();
|
||||
}
|
||||
|
||||
App.ThemeService.ThemeChanged -= OnThemeChanged;
|
||||
}
|
||||
|
||||
private void OnThemeChanged(object sender, ElementTheme theme)
|
||||
{
|
||||
WindowHelper.SetTheme(this, theme);
|
||||
}
|
||||
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (!disposedValue)
|
||||
{
|
||||
disposedValue = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
||||
Dispose(disposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,8 @@ using System.Threading.Tasks;
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Settings.UI.Helpers;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.PowerToys.Settings.UI.OOBE.Enums;
|
||||
using Microsoft.PowerToys.Settings.UI.OOBE.Views;
|
||||
using Microsoft.PowerToys.Settings.UI.ViewModels;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
@@ -48,30 +50,26 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
||||
|
||||
private void WhatsNewButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (App.GetScoobeWindow() == null)
|
||||
if (App.GetOobeWindow() == null)
|
||||
{
|
||||
App.SetScoobeWindow(new ScoobeWindow());
|
||||
App.SetOobeWindow(new OobeWindow(PowerToysModules.WhatsNew));
|
||||
}
|
||||
else
|
||||
{
|
||||
App.GetOobeWindow().SetAppWindow(PowerToysModules.WhatsNew);
|
||||
}
|
||||
|
||||
App.GetScoobeWindow().Activate();
|
||||
App.GetOobeWindow().Activate();
|
||||
}
|
||||
|
||||
private void SortAlphabetical_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
ViewModel.DashboardSortOrder = DashboardSortOrder.Alphabetical;
|
||||
if (sender is ToggleMenuFlyoutItem item)
|
||||
{
|
||||
item.IsChecked = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void SortByStatus_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
ViewModel.DashboardSortOrder = DashboardSortOrder.ByStatus;
|
||||
if (sender is ToggleMenuFlyoutItem item)
|
||||
{
|
||||
item.IsChecked = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2419,15 +2419,9 @@ From there, simply click on one of the supported files in the File Explorer and
|
||||
<data name="OobeWindow_Title" xml:space="preserve">
|
||||
<value>Welcome to PowerToys</value>
|
||||
</data>
|
||||
<data name="OobeWindow_TitleTxt.Title" xml:space="preserve">
|
||||
<data name="OobeWindow_TitleTxt.Text" xml:space="preserve">
|
||||
<value>Welcome to PowerToys</value>
|
||||
</data>
|
||||
<data name="ScoobeWindow_Title" xml:space="preserve">
|
||||
<value>What's new in PowerToys</value>
|
||||
</data>
|
||||
<data name="ScoobeWindow_TitleTxt.Title" xml:space="preserve">
|
||||
<value>What's new in PowerToys</value>
|
||||
</data>
|
||||
<data name="SettingsWindow_Title" xml:space="preserve">
|
||||
<value>PowerToys Settings</value>
|
||||
<comment>Title of the settings window when running as user</comment>
|
||||
|
||||
@@ -29,8 +29,6 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
{
|
||||
public partial class DashboardViewModel : PageViewModelBase
|
||||
{
|
||||
private readonly object _sortLock = new object();
|
||||
|
||||
protected override string ModuleName => "Dashboard";
|
||||
|
||||
private Dispatcher dispatcher;
|
||||
@@ -53,9 +51,6 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
// Flag to prevent circular updates when a UI toggle triggers settings changes.
|
||||
private bool _isUpdatingFromUI;
|
||||
|
||||
// Flag to prevent toggle operations during sorting to avoid race conditions.
|
||||
private bool _isSorting;
|
||||
|
||||
private AllHotkeyConflictsData _allHotkeyConflictsData = new AllHotkeyConflictsData();
|
||||
|
||||
public AllHotkeyConflictsData AllHotkeyConflictsData
|
||||
@@ -85,17 +80,15 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
get => generalSettingsConfig.DashboardSortOrder;
|
||||
set
|
||||
{
|
||||
if (_dashboardSortOrder != value)
|
||||
if (Set(ref _dashboardSortOrder, value))
|
||||
{
|
||||
_dashboardSortOrder = value;
|
||||
generalSettingsConfig.DashboardSortOrder = value;
|
||||
OutGoingGeneralSettings outgoing = new OutGoingGeneralSettings(generalSettingsConfig);
|
||||
|
||||
// Save settings to file
|
||||
SettingsUtils.Default.SaveSettings(generalSettingsConfig.ToJsonString());
|
||||
|
||||
SendConfigMSG(outgoing.ToString());
|
||||
|
||||
// Notify UI before sorting so menu updates its checked state
|
||||
OnPropertyChanged(nameof(DashboardSortOrder));
|
||||
|
||||
SortModuleList();
|
||||
}
|
||||
}
|
||||
@@ -110,7 +103,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
dispatcher = Dispatcher.CurrentDispatcher;
|
||||
_settingsRepository = settingsRepository;
|
||||
generalSettingsConfig = settingsRepository.SettingsConfig;
|
||||
|
||||
generalSettingsConfig.AddEnabledModuleChangeNotification(ModuleEnabledChangedOnSettingsPage);
|
||||
_settingsRepository.SettingsChanged += OnSettingsChanged;
|
||||
|
||||
// Initialize dashboard sort order from settings
|
||||
@@ -135,14 +128,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
dispatcher.BeginInvoke(() =>
|
||||
{
|
||||
generalSettingsConfig = newSettings;
|
||||
|
||||
// Update local field and notify UI if sort order changed
|
||||
if (_dashboardSortOrder != generalSettingsConfig.DashboardSortOrder)
|
||||
{
|
||||
_dashboardSortOrder = generalSettingsConfig.DashboardSortOrder;
|
||||
OnPropertyChanged(nameof(DashboardSortOrder));
|
||||
}
|
||||
|
||||
generalSettingsConfig.AddEnabledModuleChangeNotification(ModuleEnabledChangedOnSettingsPage);
|
||||
ModuleEnabledChangedOnSettingsPage();
|
||||
});
|
||||
}
|
||||
@@ -212,58 +198,40 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
/// Sorts the module list according to the current sort order and updates the AllModules collection.
|
||||
/// On first call, populates AllModules. On subsequent calls, uses Move() to reorder items in-place
|
||||
/// to avoid destroying and recreating UI elements.
|
||||
/// Temporarily disables interaction on all items during sorting to prevent race conditions.
|
||||
/// </summary>
|
||||
private void SortModuleList()
|
||||
{
|
||||
if (_isSorting)
|
||||
var sortedItems = (DashboardSortOrder switch
|
||||
{
|
||||
DashboardSortOrder.ByStatus => _moduleItems.OrderByDescending(x => x.IsEnabled).ThenBy(x => x.Label),
|
||||
_ => _moduleItems.OrderBy(x => x.Label), // Default alphabetical
|
||||
}).ToList();
|
||||
|
||||
// If AllModules is empty (first load), just populate it.
|
||||
if (AllModules.Count == 0)
|
||||
{
|
||||
foreach (var item in sortedItems)
|
||||
{
|
||||
AllModules.Add(item);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_sortLock)
|
||||
// Otherwise, update the collection in place using Move to avoid UI glitches.
|
||||
for (int i = 0; i < sortedItems.Count; i++)
|
||||
{
|
||||
_isSorting = true;
|
||||
try
|
||||
var currentItem = sortedItems[i];
|
||||
var currentIndex = AllModules.IndexOf(currentItem);
|
||||
|
||||
if (currentIndex != -1 && currentIndex != i)
|
||||
{
|
||||
var sortedItems = (DashboardSortOrder switch
|
||||
{
|
||||
DashboardSortOrder.ByStatus => _moduleItems.OrderByDescending(x => x.IsEnabled).ThenBy(x => x.Label),
|
||||
_ => _moduleItems.OrderBy(x => x.Label), // Default alphabetical
|
||||
}).ToList();
|
||||
|
||||
// If AllModules is empty (first load), just populate it.
|
||||
if (AllModules.Count == 0)
|
||||
{
|
||||
foreach (var item in sortedItems)
|
||||
{
|
||||
AllModules.Add(item);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, update the collection in place using Move to avoid UI glitches.
|
||||
for (int i = 0; i < sortedItems.Count; i++)
|
||||
{
|
||||
var currentItem = sortedItems[i];
|
||||
var currentIndex = AllModules.IndexOf(currentItem);
|
||||
|
||||
if (currentIndex != -1 && currentIndex != i)
|
||||
{
|
||||
AllModules.Move(currentIndex, i);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Use dispatcher to reset flag after UI updates complete
|
||||
dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Background, () =>
|
||||
{
|
||||
_isSorting = false;
|
||||
});
|
||||
AllModules.Move(currentIndex, i);
|
||||
}
|
||||
}
|
||||
|
||||
// Notify that DashboardSortOrder changed so the menu updates its checked state.
|
||||
OnPropertyChanged(nameof(DashboardSortOrder));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -311,25 +279,10 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
var dashboardListItem = (DashboardListItem)item;
|
||||
var isEnabled = dashboardListItem.IsEnabled;
|
||||
|
||||
// Ignore toggle operations during sorting to prevent race conditions.
|
||||
// Revert the toggle state since UI already changed due to TwoWay binding.
|
||||
if (_isSorting)
|
||||
{
|
||||
dashboardListItem.UpdateStatus(!isEnabled);
|
||||
return;
|
||||
}
|
||||
|
||||
_isUpdatingFromUI = true;
|
||||
try
|
||||
{
|
||||
// Send optimized IPC message with only the module status update
|
||||
// Format: {"module_status": {"ModuleName": true/false}}
|
||||
string moduleKey = ModuleHelper.GetModuleKey(dashboardListItem.Tag);
|
||||
string moduleStatusJson = $"{{\"module_status\": {{\"{moduleKey}\": {isEnabled.ToString().ToLowerInvariant()}}}}}";
|
||||
SendConfigMSG(moduleStatusJson);
|
||||
|
||||
// Update local settings config to keep UI in sync
|
||||
ModuleHelper.SetIsModuleEnabled(generalSettingsConfig, dashboardListItem.Tag, isEnabled);
|
||||
Views.ShellPage.UpdateGeneralSettingsCallback(dashboardListItem.Tag, isEnabled);
|
||||
|
||||
if (dashboardListItem.Tag == ModuleType.NewPlus && isEnabled == true)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user