Compare commits

..

1 Commits

Author SHA1 Message Date
Shawn Yuan (from Dev Box)
73dda5a08d Fix quickaccess localization issue 2026-01-09 09:38:30 +08:00
67 changed files with 329 additions and 953 deletions

View File

@@ -209,7 +209,6 @@ changecursor
CHILDACTIVATE
CHILDWINDOW
CHOOSEFONT
CIBUILD
cidl
CIELCh
cim

View File

@@ -6,45 +6,13 @@ description: 'Generate an 80-character git commit title for the local diff'
# Generate Commit Title
## Purpose
Provide a single-line, ready-to-paste git commit title (<= 80 characters) that reflects the most important local changes since `HEAD`.
**Goal:** Provide a ready-to-paste git commit title (<= 80 characters) that captures the most important local changes since `HEAD`.
## Input to collect
- Run exactly one command to view the local diff:
```@terminal
git diff HEAD
```
## How to decide the title
1. From the diff, find the dominant area (e.g., `src/modules/*`, `doc/devdocs/**`) and the change type (bug fix, docs update, config tweak).
2. Draft an imperative, plain-ASCII title that:
- Mentions the primary component when obvious (e.g., `FancyZones:` or `Docs:`)
- Stays within 80 characters and has no trailing punctuation
## Final output
- Reply with only the commit title on a single line—no extra text.
## PR title convention (when asked)
Use Conventional Commits style:
`<type>(<scope>): <summary>`
**Allowed types**
- feat, fix, docs, refactor, perf, test, build, ci, chore
**Scope rules**
- Use a short, PowerToys-focused scope (one word preferred). Common scopes:
- Core: `runner`, `settings-ui`, `common`, `docs`, `build`, `ci`, `installer`, `gpo`, `dsc`
- Modules: `fancyzones`, `powerrename`, `awake`, `colorpicker`, `imageresizer`, `keyboardmanager`, `mouseutils`, `peek`, `hosts`, `file-locksmith`, `screen-ruler`, `text-extractor`, `cropandlock`, `paste`, `powerlauncher`
- If unclear, pick the closest module or subsystem; omit only if unavoidable
**Summary rules**
- Imperative, present tense (“add”, “update”, “remove”, “fix”)
- Keep it <= 72 characters when possible; be specific, avoid “misc changes”
**Examples**
- `feat(fancyzones): add canvas template duplication`
- `fix(mouseutils): guard crosshair toggle when dpi info missing`
- `docs(runner): document tray icon states`
- `build(installer): align wix v5 suffix flag`
- `ci(ci): cache pipeline artifacts for x64`
**Workflow:**
1. Run a single command to view the local diff since the last commit:
```@terminal
git diff HEAD
```
2. From that diff, identify the dominant area (reference key paths like `src/modules/*`, `doc/devdocs/**`, etc.), the type of change (bug fix, docs update, config tweak), and any notable impact.
3. Draft a concise, imperative commit title summarizing the dominant change. Keep it plain ASCII, <= 80 characters, and avoid trailing punctuation. Mention the primary component when obvious (for example `FancyZones:` or `Docs:`).
4. Respond with only the final commit title on a single line so it can be pasted directly into `git commit`.

View File

@@ -22,4 +22,3 @@ description: 'Generate a PowerToys-ready pull request description from the local
5. Confirm validation: list tests executed with results or state why tests were skipped in line with repo guidance.
6. Load `.github/pull_request_template.md`, mirror its section order, and populate it with the gathered facts. Include only relevant checklist entries, marking them `[x]/[ ]` and noting any intentional omissions as "N/A".
7. Present the filled template inside a fenced ```markdown code block with no extra commentary so it is ready to paste into a PR, clearly flagging any placeholders that still need user input.
8. Prepend the PR title above the filled template, applying the Conventional Commit type/scope rules from `.github/prompts/create-commit-title.prompt.md`; pick the dominant component from the diff and keep the title concise and imperative.

View File

@@ -10,8 +10,8 @@ description: 'Resolve Code scanning / check-spelling comments on the active PR'
**Guardrails:**
- Update only discussion threads authored by `github-actions` or `github-actions[bot]` that mention `Code scanning results / check-spelling`.
- Prefer improving the wording in the originally flagged file when it clarifies intent without changing meaning; if the wording is already clear/standard for the context, handle it via `.github/actions/spell-check/expect.txt` and reuse existing entries.
- Limit edits to the flagged text and `.github/actions/spell-check/expect.txt`; leave all other files and topics untouched.
- Resolve findings solely by editing `.github/actions/spell-check/expect.txt`; reuse existing entries.
- Leave all other files and topics untouched.
**Prerequisites:**
- Install GitHub CLI if it is not present: `winget install GitHub.cli`.
@@ -20,6 +20,5 @@ description: 'Resolve Code scanning / check-spelling comments on the active PR'
**Workflow:**
1. Determine the active pull request with a single `gh pr view --json number` call (default to the current branch).
2. Fetch all PR discussion data once via `gh pr view --json comments,reviews` and filter to check-spelling comments authored by `github-actions` or `github-actions[bot]` that are not minimized; when several remain, process only the most recent comment body.
3. For each flagged token, first consider tightening or rephrasing the original text to avoid the false positive while keeping the meaning intact; if the existing wording is already normal and professional for the context, proceed to allowlisting instead of changing it.
4. When allowlisting, review `.github/actions/spell-check/expect.txt` for an equivalent term (for example an existing lowercase variant); when found, reuse that normalized term rather than adding a new entry, even if the flagged token differs only by casing. Only add a new entry after confirming no equivalent already exists.
5. Add any remaining missing token to `.github/actions/spell-check/expect.txt`, keeping surrounding formatting intact.
3. For each flagged token, review `.github/actions/spell-check/expect.txt` for an equivalent term (for example an existing lowercase variant); when found, reuse that normalized term rather than adding a new entry, even if the flagged token differs only by casing. Only add a new entry after confirming no equivalent already exists.
4. Add any remaining missing token to `.github/actions/spell-check/expect.txt`, keeping surrounding formatting intact.

View File

@@ -10,7 +10,7 @@ parameters:
default: {}
steps:
- task: EsrpCodeSigning@6
- task: EsrpCodeSigning@5
displayName: 🔏 ${{ parameters.displayName }}
inputs:
ConnectedServiceName: ${{ parameters.signingIdentity.serviceName }}

17
.vscode/settings.json vendored
View File

@@ -1,17 +0,0 @@
{
"github.copilot.chat.reviewSelection.instructions": [
{
"file": ".github/prompts/review-pr.prompt.md"
}
],
"github.copilot.chat.commitMessageGeneration.instructions": [
{
"file": ".github/prompts/create-commit-title.prompt.md"
}
],
"github.copilot.chat.pullRequestDescriptionGeneration.instructions": [
{
"file": ".github/prompts/create-pr-summary.prompt.md"
}
]
}

View File

@@ -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" />

View File

@@ -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",
};

View File

@@ -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" />

View File

@@ -66,10 +66,5 @@ namespace PowerToys.GPOWrapperProjection
{
return (GpoRuleConfigured)PowerToys.GPOWrapper.GPOWrapper.GetConfiguredWorkspacesEnabledValue();
}
public static GpoRuleConfigured GetConfiguredLightSwitchEnabledValue()
{
return (GpoRuleConfigured)PowerToys.GPOWrapper.GPOWrapper.GetConfiguredLightSwitchEnabledValue();
}
}
}

View File

@@ -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()

View File

@@ -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="&#xE759;" />
<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="&#xE712;"
FontFamily="{ThemeResource SymbolThemeFontFamily}"
@@ -523,8 +535,8 @@
</Button>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ListView.ItemTemplate>
</ListView>
</StackPanel>
</ScrollViewer>
</ContentDialog>

View File

@@ -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;
}
}
}
}

View File

@@ -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")]

View File

@@ -21,7 +21,7 @@ public sealed partial class BuiltInsCommandProvider : CommandProvider
public override ICommandItem[] TopLevelCommands() =>
[
new CommandItem(openSettings) { },
new CommandItem(_newExtension) { Title = _newExtension.Title },
new CommandItem(_newExtension) { Title = _newExtension.Title, Subtitle = Properties.Resources.builtin_new_extension_subtitle },
];
public override IFallbackCommandItem[] FallbackCommands() =>

View File

@@ -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));

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -90,5 +90,20 @@ namespace Microsoft.CmdPal.Ext.TimeDate.UnitTests
// Assert
Assert.IsFalse(string.IsNullOrEmpty(displayName));
}
[TestMethod]
public void GetTranslatedPluginDescriptionTest()
{
// Setup
var provider = new TimeDateCommandsProvider();
// Act
var commands = provider.TopLevelCommands();
var subtitle = commands[0].Subtitle;
// Assert
Assert.IsFalse(string.IsNullOrEmpty(subtitle));
Assert.IsTrue(subtitle.Contains("Show time and date values in different formats"));
}
}
}

View File

@@ -55,7 +55,7 @@ public class BasicTests : CommandPaletteTestBase
SetTimeAndDaterExtensionSearchBox("year");
Assert.IsNotNull(this.Find<NavigationViewItem>("2026"));
Assert.IsNotNull(this.Find<NavigationViewItem>("2025"));
}
[TestMethod]

View File

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

View File

@@ -15,6 +15,7 @@ public partial class CalculatorCommandProvider : CommandProvider
private static ISettingsInterface settings = new SettingsManager();
private readonly ListItem _listItem = new(new CalculatorListPage(settings))
{
Subtitle = Resources.calculator_top_level_subtitle,
MoreCommands = [new CommandContextItem(((SettingsManager)settings).Settings.SettingsPage)],
};

View File

@@ -19,6 +19,7 @@ public partial class ClipboardHistoryCommandsProvider : CommandProvider
_clipboardHistoryListItem = new ListItem(new ClipboardHistoryListPage(_settingsManager))
{
Title = Properties.Resources.list_item_title,
Subtitle = Properties.Resources.list_item_subtitle,
Icon = Icons.ClipboardListIcon,
MoreCommands = [
new CommandContextItem(_settingsManager.Settings.SettingsPage),

View File

@@ -2,7 +2,6 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Globalization;
using Microsoft.CommandPalette.Extensions;
@@ -42,19 +41,15 @@ internal sealed partial class FancyZonesMonitorListItem : ListItem
public static Details BuildMonitorDetails(FancyZonesMonitorDescriptor monitor)
{
var currentVirtualDesktop = FancyZonesVirtualDesktop.GetCurrentVirtualDesktopIdString();
// Calculate physical resolution from logical pixels and DPI
var scaleFactor = monitor.Data.Dpi > 0 ? monitor.Data.Dpi / 96.0 : 1.0;
var physicalWidth = (int)Math.Round(monitor.Data.MonitorWidth * scaleFactor);
var physicalHeight = (int)Math.Round(monitor.Data.MonitorHeight * scaleFactor);
var resolution = $"{physicalWidth}\u00D7{physicalHeight}";
var tags = new List<IDetailsElement>
{
DetailTag(Resources.FancyZones_Monitor, monitor.Data.Monitor),
DetailTag(Resources.FancyZones_Instance, monitor.Data.MonitorInstanceId),
DetailTag(Resources.FancyZones_Serial, monitor.Data.MonitorSerialNumber),
DetailTag(Resources.FancyZones_Number, monitor.Data.MonitorNumber.ToString(CultureInfo.InvariantCulture)),
DetailTag(Resources.FancyZones_VirtualDesktop, currentVirtualDesktop),
DetailTag(Resources.FancyZones_Resolution, resolution),
DetailTag(Resources.FancyZones_WorkArea, $"{monitor.Data.LeftCoordinate},{monitor.Data.TopCoordinate} {monitor.Data.WorkAreaWidth}\u00D7{monitor.Data.WorkAreaHeight}"),
DetailTag(Resources.FancyZones_Resolution, $"{monitor.Data.MonitorWidth}\u00D7{monitor.Data.MonitorHeight}"),
DetailTag(Resources.FancyZones_DPI, monitor.Data.Dpi.ToString(CultureInfo.InvariantCulture)),
};

View File

@@ -19,12 +19,8 @@ internal readonly record struct FancyZonesMonitorDescriptor(
{
get
{
// MonitorWidth/Height are logical (DPI-scaled) pixels, calculate physical resolution
var scaleFactor = Data.Dpi > 0 ? Data.Dpi / 96.0 : 1.0;
var physicalWidth = (int)Math.Round(Data.MonitorWidth * scaleFactor);
var physicalHeight = (int)Math.Round(Data.MonitorHeight * scaleFactor);
var size = $"{physicalWidth}×{physicalHeight}";
var scaling = Data.Dpi > 0 ? string.Format(CultureInfo.InvariantCulture, "{0}%", (int)Math.Round(scaleFactor * 100)) : "n/a";
var size = $"{Data.MonitorWidth}×{Data.MonitorHeight}";
var scaling = Data.Dpi > 0 ? string.Format(CultureInfo.InvariantCulture, "{0}%", (int)Math.Round(Data.Dpi * 100 / 96.0)) : "n/a";
return $"{size} \u2022 {scaling}";
}
}

View File

@@ -485,21 +485,12 @@ internal static class FancyZonesThumbnailRenderer
private static List<NormalizedRect> GetFocusRects(int zoneCount)
{
// Focus layout parameters from FancyZonesEditor CanvasLayoutModel:
// - DefaultOffset = 100px from top-left (normalized: ~0.05 for typical screen)
// - OffsetShift = 50px per zone (normalized: ~0.025)
// - ZoneSizeMultiplier = 0.4 (zones are 40% of screen)
zoneCount = Math.Clamp(zoneCount, 1, 8);
var rects = new List<NormalizedRect>(zoneCount);
const float defaultOffset = 0.05f; // ~100px on 1920px screen
const float offsetShift = 0.025f; // ~50px on 1920px screen
const float zoneSize = 0.4f; // 40% of screen
for (var i = 0; i < zoneCount; i++)
{
var offset = i * offsetShift;
rects.Add(new NormalizedRect(defaultOffset + offset, defaultOffset + offset, zoneSize, zoneSize));
var offset = i * 0.06f;
rects.Add(new NormalizedRect(0.1f + offset, 0.1f + offset, 0.8f, 0.8f));
}
return rects;

View File

@@ -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>

View File

@@ -31,6 +31,7 @@ public partial class RemoteDesktopCommandProvider : CommandProvider
listPageCommand = new CommandItem(listPage)
{
Subtitle = Resources.remotedesktop_subtitle,
Icon = Icons.RDPIcon,
MoreCommands = [
new CommandContextItem(settingsManager.Settings.SettingsPage),

View File

@@ -39,6 +39,7 @@ public partial class ShellCommandsProvider : CommandProvider
{
Icon = Icons.RunV2Icon,
Title = Resources.shell_command_name,
Subtitle = Resources.cmd_plugin_description,
MoreCommands = [
new CommandContextItem(Settings.SettingsPage),
],

View File

@@ -28,6 +28,7 @@ public sealed partial class TimeDateCommandsProvider : CommandProvider
{
Icon = _timeDateExtensionPage.Icon,
Title = Resources.Microsoft_plugin_timedate_plugin_name,
Subtitle = GetTranslatedPluginDescription(),
MoreCommands = [new CommandContextItem(_settingsManager.Settings.SettingsPage)],
};

View File

@@ -26,6 +26,7 @@ public partial class WindowWalkerCommandsProvider : CommandProvider
_windowWalkerPageItem = new CommandItem(new WindowWalkerListPage())
{
Title = Resources.window_walker_top_level_command_title,
Subtitle = Resources.windowwalker_name,
MoreCommands = [
new CommandContextItem(Settings.SettingsPage),
],

View File

@@ -30,6 +30,7 @@ public sealed partial class WindowsSettingsCommandsProvider : CommandProvider
_searchSettingsListItem = new CommandItem(new WindowsSettingsListPage(_windowsSettings))
{
Title = Resources.settings_title,
Subtitle = Resources.settings_subtitle,
};
_fallback = new(_windowsSettings);

View File

@@ -9,7 +9,6 @@ using System.CommandLine.Invocation;
using System.Globalization;
using System.Linq;
using FancyZonesCLI.Utils;
using FancyZonesEditorCommon.Data;
using FancyZonesEditorCommon.Utils;
@@ -36,19 +35,13 @@ internal sealed partial class SetHotkeyCommand : FancyZonesBaseCommand
{
// FancyZones running guard is handled by FancyZonesBaseCommand.
int key = context.ParseResult.GetValueForArgument(_key);
string layoutInput = context.ParseResult.GetValueForArgument(_layout);
string layout = context.ParseResult.GetValueForArgument(_layout);
if (key < 0 || key > 9)
{
throw new InvalidOperationException(Properties.Resources.set_hotkey_error_invalid_key);
}
// Normalize GUID to Windows format with braces (supports input with or without braces)
if (!GuidHelper.TryNormalizeGuid(layoutInput, out string layout))
{
throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, Properties.Resources.set_hotkey_error_not_custom, layoutInput));
}
// Editor only allows assigning hotkeys to existing custom layouts.
var customLayouts = FancyZonesDataIO.ReadCustomLayouts();
@@ -67,7 +60,7 @@ internal sealed partial class SetHotkeyCommand : FancyZonesBaseCommand
if (!matchedLayout.HasValue)
{
throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, Properties.Resources.set_hotkey_error_not_custom, layoutInput));
throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, Properties.Resources.set_hotkey_error_not_custom, layout));
}
string layoutName = matchedLayout.Value.Name;

View File

@@ -140,12 +140,9 @@ internal sealed partial class SetLayoutCommand : FancyZonesBaseCommand
return null;
}
// Normalize GUID to Windows format with braces (supports input with or without braces)
string normalizedLayout = GuidHelper.NormalizeGuid(layout) ?? layout;
foreach (var customLayout in customLayouts.CustomLayouts)
{
if (customLayout.Uuid.Equals(normalizedLayout, StringComparison.OrdinalIgnoreCase))
if (customLayout.Uuid.Equals(layout, StringComparison.OrdinalIgnoreCase))
{
return customLayout;
}

View File

@@ -4,7 +4,6 @@
using System;
using System.CommandLine;
using System.Globalization;
using System.Linq;
namespace FancyZonesCLI.CommandLine;
@@ -14,15 +13,15 @@ internal static class FancyZonesCliUsage
public static void PrintUsage()
{
Console.OutputEncoding = System.Text.Encoding.UTF8;
Console.WriteLine(Properties.Resources.usage_title);
Console.WriteLine("FancyZones CLI - Command line interface for FancyZones");
Console.WriteLine();
var cmd = FancyZonesCliCommandFactory.CreateRootCommand();
Console.WriteLine(Properties.Resources.usage_syntax);
Console.WriteLine("Usage: FancyZonesCLI [command] [options]");
Console.WriteLine();
Console.WriteLine(Properties.Resources.usage_options);
Console.WriteLine("Options:");
foreach (var option in cmd.Options)
{
var aliases = string.Join(", ", option.Aliases);
@@ -31,7 +30,7 @@ internal static class FancyZonesCliUsage
}
Console.WriteLine();
Console.WriteLine(Properties.Resources.usage_commands);
Console.WriteLine("Commands:");
foreach (var command in cmd.Subcommands)
{
if (command.IsHidden)
@@ -52,7 +51,7 @@ internal static class FancyZonesCliUsage
}
Console.WriteLine();
Console.WriteLine(Properties.Resources.usage_examples);
Console.WriteLine("Examples:");
Console.WriteLine(" FancyZonesCLI --help");
Console.WriteLine(" FancyZonesCLI --version");
Console.WriteLine(" FancyZonesCLI get-monitors");
@@ -60,135 +59,4 @@ internal static class FancyZonesCliUsage
Console.WriteLine(" FancyZonesCLI set-layout <uuid> --monitor 1");
Console.WriteLine(" FancyZonesCLI get-hotkeys");
}
public static void PrintCommandUsage(string commandName)
{
Console.OutputEncoding = System.Text.Encoding.UTF8;
var rootCmd = FancyZonesCliCommandFactory.CreateRootCommand();
// Find matching subcommand by name or alias
var subcommand = rootCmd.Subcommands.FirstOrDefault(c =>
c.Aliases.Any(a => string.Equals(a, commandName, StringComparison.OrdinalIgnoreCase)));
if (subcommand == null)
{
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.usage_unknown_command, commandName));
Console.WriteLine();
Console.WriteLine(Properties.Resources.usage_run_help);
return;
}
// Command name and description
Console.WriteLine($"{Properties.Resources.usage_command} {subcommand.Name}");
if (!string.IsNullOrEmpty(subcommand.Description))
{
Console.WriteLine($" {subcommand.Description}");
}
Console.WriteLine();
// Usage line
string argsLabel = string.Join(" ", subcommand.Arguments.Select(a => $"<{a.Name}>"));
string optionsLabel = subcommand.Options.Any() ? " [options]" : string.Empty;
Console.WriteLine($"Usage: FancyZonesCLI {subcommand.Name} {argsLabel}{optionsLabel}".TrimEnd());
Console.WriteLine();
// Aliases
var aliases = subcommand.Aliases.Where(a => !string.Equals(a, subcommand.Name, StringComparison.OrdinalIgnoreCase)).ToList();
if (aliases.Count > 0)
{
Console.WriteLine($"{Properties.Resources.usage_aliases} {string.Join(", ", aliases)}");
Console.WriteLine();
}
// Arguments
if (subcommand.Arguments.Any())
{
Console.WriteLine(Properties.Resources.usage_arguments);
foreach (var arg in subcommand.Arguments)
{
var argDescription = arg.Description ?? string.Empty;
Console.WriteLine($" <{arg.Name}>{(arg.Arity.MinimumNumberOfValues == 0 ? $" {Properties.Resources.usage_optional}" : string.Empty),-20} {argDescription}");
}
Console.WriteLine();
}
// Options
if (subcommand.Options.Any())
{
Console.WriteLine(Properties.Resources.usage_options);
foreach (var option in subcommand.Options)
{
var optAliases = string.Join(", ", option.Aliases);
var optDescription = option.Description ?? string.Empty;
Console.WriteLine($" {optAliases,-25} {optDescription}");
}
Console.WriteLine();
}
// Command-specific examples
PrintCommandExamples(subcommand.Name);
}
private static void PrintCommandExamples(string commandName)
{
Console.WriteLine(Properties.Resources.usage_examples);
switch (commandName.ToLowerInvariant())
{
case "get-monitors":
Console.WriteLine(" FancyZonesCLI get-monitors");
Console.WriteLine(" FancyZonesCLI m");
break;
case "get-layouts":
Console.WriteLine(" FancyZonesCLI get-layouts");
Console.WriteLine(" FancyZonesCLI ls");
break;
case "get-active-layout":
Console.WriteLine(" FancyZonesCLI get-active-layout");
Console.WriteLine(" FancyZonesCLI active");
break;
case "set-layout":
Console.WriteLine(" FancyZonesCLI set-layout focus");
Console.WriteLine(" FancyZonesCLI set-layout columns --monitor 1");
Console.WriteLine(" FancyZonesCLI set-layout {uuid} --all");
Console.WriteLine(" FancyZonesCLI s rows -m 2");
break;
case "open-editor":
Console.WriteLine(" FancyZonesCLI open-editor");
Console.WriteLine(" FancyZonesCLI e");
break;
case "open-settings":
Console.WriteLine(" FancyZonesCLI open-settings");
Console.WriteLine(" FancyZonesCLI settings");
break;
case "get-hotkeys":
Console.WriteLine(" FancyZonesCLI get-hotkeys");
Console.WriteLine(" FancyZonesCLI hk");
break;
case "set-hotkey":
Console.WriteLine(" FancyZonesCLI set-hotkey 1 {layout-uuid}");
Console.WriteLine(" FancyZonesCLI shk 2 0CEBCBA9-9C32-4395-B93E-DC77485AD6D0");
break;
case "remove-hotkey":
Console.WriteLine(" FancyZonesCLI remove-hotkey 1");
Console.WriteLine(" FancyZonesCLI rhk 2");
break;
default:
Console.WriteLine($" FancyZonesCLI {commandName}");
break;
}
}
}

View File

@@ -12,8 +12,6 @@ namespace FancyZonesCLI;
internal sealed class Program
{
private static readonly string[] HelpFlags = ["--help", "-h", "-?"];
private static async Task<int> Main(string[] args)
{
Logger.InitializeLogger();
@@ -23,17 +21,14 @@ internal sealed class Program
NativeMethods.InitializeWindowMessages();
// Intercept help requests early and print custom usage.
if (TryHandleHelpRequest(args))
if (args.Any(a => string.Equals(a, "--help", StringComparison.OrdinalIgnoreCase) ||
string.Equals(a, "-h", StringComparison.OrdinalIgnoreCase) ||
string.Equals(a, "-?", StringComparison.OrdinalIgnoreCase)))
{
FancyZonesCliUsage.PrintUsage();
return 0;
}
// Detect PowerShell script block expansion (when {} is interpreted as script block)
if (DetectPowerShellScriptBlockArgs(args))
{
return 1;
}
RootCommand rootCommand = FancyZonesCliCommandFactory.CreateRootCommand();
int exitCode = await rootCommand.InvokeAsync(args);
@@ -48,69 +43,4 @@ internal sealed class Program
return exitCode;
}
/// <summary>
/// Handles help requests for root command and subcommands.
/// </summary>
/// <returns>True if help was printed, false otherwise.</returns>
private static bool TryHandleHelpRequest(string[] args)
{
bool hasHelpFlag = args.Any(a => HelpFlags.Any(h => string.Equals(a, h, StringComparison.OrdinalIgnoreCase)));
if (!hasHelpFlag)
{
return false;
}
// Get non-help arguments to identify subcommand
var nonHelpArgs = args.Where(a => !HelpFlags.Any(h => string.Equals(a, h, StringComparison.OrdinalIgnoreCase))).ToArray();
if (nonHelpArgs.Length == 0)
{
// Root help: fancyzones cli --help
FancyZonesCliUsage.PrintUsage();
}
else
{
// Subcommand help: fancyzones cli <command> --help
string subcommandName = nonHelpArgs[0];
FancyZonesCliUsage.PrintCommandUsage(subcommandName);
}
return true;
}
/// <summary>
/// Detects when PowerShell interprets {GUID} as a script block and converts it to encoded command args.
/// This happens when users forget to quote GUIDs with braces in PowerShell.
/// </summary>
/// <returns>True if PowerShell script block args were detected, false otherwise.</returns>
private static bool DetectPowerShellScriptBlockArgs(string[] args)
{
// PowerShell converts {scriptblock} to: -encodedCommand <base64> -inputFormat xml -outputFormat text
bool hasEncodedCommand = args.Any(a => string.Equals(a, "-encodedCommand", StringComparison.OrdinalIgnoreCase));
bool hasInputFormat = args.Any(a => string.Equals(a, "-inputFormat", StringComparison.OrdinalIgnoreCase));
bool hasOutputFormat = args.Any(a => string.Equals(a, "-outputFormat", StringComparison.OrdinalIgnoreCase));
if (hasEncodedCommand || (hasInputFormat && hasOutputFormat))
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine(Properties.Resources.error_powershell_scriptblock_title);
Console.ResetColor();
Console.WriteLine();
Console.WriteLine(Properties.Resources.error_powershell_scriptblock_explanation);
Console.WriteLine(Properties.Resources.error_powershell_scriptblock_hint);
Console.WriteLine();
Console.WriteLine($" {Properties.Resources.error_powershell_scriptblock_option1}");
Console.WriteLine($" {Properties.Resources.error_powershell_scriptblock_option1_example}");
Console.WriteLine();
Console.WriteLine($" {Properties.Resources.error_powershell_scriptblock_option2}");
Console.WriteLine($" {Properties.Resources.error_powershell_scriptblock_option2_example}");
Console.WriteLine();
Logger.LogWarning("PowerShell script block expansion detected - user needs to quote GUID or omit braces");
return true;
}
return false;
}
}

View File

@@ -349,113 +349,5 @@ namespace FancyZonesCLI.Properties {
return ResourceManager.GetString("editor_params_timeout", resourceCulture);
}
}
internal static string error_powershell_scriptblock_title {
get {
return ResourceManager.GetString("error_powershell_scriptblock_title", resourceCulture);
}
}
internal static string error_powershell_scriptblock_explanation {
get {
return ResourceManager.GetString("error_powershell_scriptblock_explanation", resourceCulture);
}
}
internal static string error_powershell_scriptblock_hint {
get {
return ResourceManager.GetString("error_powershell_scriptblock_hint", resourceCulture);
}
}
internal static string error_powershell_scriptblock_option1 {
get {
return ResourceManager.GetString("error_powershell_scriptblock_option1", resourceCulture);
}
}
internal static string error_powershell_scriptblock_option1_example {
get {
return ResourceManager.GetString("error_powershell_scriptblock_option1_example", resourceCulture);
}
}
internal static string error_powershell_scriptblock_option2 {
get {
return ResourceManager.GetString("error_powershell_scriptblock_option2", resourceCulture);
}
}
internal static string error_powershell_scriptblock_option2_example {
get {
return ResourceManager.GetString("error_powershell_scriptblock_option2_example", resourceCulture);
}
}
internal static string usage_title {
get {
return ResourceManager.GetString("usage_title", resourceCulture);
}
}
internal static string usage_syntax {
get {
return ResourceManager.GetString("usage_syntax", resourceCulture);
}
}
internal static string usage_options {
get {
return ResourceManager.GetString("usage_options", resourceCulture);
}
}
internal static string usage_commands {
get {
return ResourceManager.GetString("usage_commands", resourceCulture);
}
}
internal static string usage_examples {
get {
return ResourceManager.GetString("usage_examples", resourceCulture);
}
}
internal static string usage_arguments {
get {
return ResourceManager.GetString("usage_arguments", resourceCulture);
}
}
internal static string usage_aliases {
get {
return ResourceManager.GetString("usage_aliases", resourceCulture);
}
}
internal static string usage_command {
get {
return ResourceManager.GetString("usage_command", resourceCulture);
}
}
internal static string usage_optional {
get {
return ResourceManager.GetString("usage_optional", resourceCulture);
}
}
internal static string usage_unknown_command {
get {
return ResourceManager.GetString("usage_unknown_command", resourceCulture);
}
}
internal static string usage_run_help {
get {
return ResourceManager.GetString("usage_run_help", resourceCulture);
}
}
}
}

View File

@@ -230,62 +230,4 @@ Tip: For templates, use the type name (e.g., 'focus', 'columns', 'rows', 'grid',
<data name="editor_params_timeout" xml:space="preserve">
<value>Could not get current monitor information (timed out after {0}ms waiting for '{1}').</value>
</data>
<!-- PowerShell Script Block Detection -->
<data name="error_powershell_scriptblock_title" xml:space="preserve">
<value>Error: Invalid GUID format detected.</value>
</data>
<data name="error_powershell_scriptblock_explanation" xml:space="preserve">
<value>PowerShell interprets curly braces {} as script blocks.</value>
</data>
<data name="error_powershell_scriptblock_hint" xml:space="preserve">
<value>Please quote your GUID or use it without braces:</value>
</data>
<data name="error_powershell_scriptblock_option1" xml:space="preserve">
<value>Option 1 - Quote the GUID:</value>
</data>
<data name="error_powershell_scriptblock_option1_example" xml:space="preserve">
<value>FancyZonesCLI shk 1 '{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}'</value>
</data>
<data name="error_powershell_scriptblock_option2" xml:space="preserve">
<value>Option 2 - Omit the braces (recommended):</value>
</data>
<data name="error_powershell_scriptblock_option2_example" xml:space="preserve">
<value>FancyZonesCLI shk 1 xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx</value>
</data>
<!-- CLI Usage -->
<data name="usage_title" xml:space="preserve">
<value>FancyZones CLI - Command line interface for FancyZones</value>
</data>
<data name="usage_syntax" xml:space="preserve">
<value>Usage: FancyZonesCLI [command] [options]</value>
</data>
<data name="usage_options" xml:space="preserve">
<value>Options:</value>
</data>
<data name="usage_commands" xml:space="preserve">
<value>Commands:</value>
</data>
<data name="usage_examples" xml:space="preserve">
<value>Examples:</value>
</data>
<data name="usage_arguments" xml:space="preserve">
<value>Arguments:</value>
</data>
<data name="usage_aliases" xml:space="preserve">
<value>Aliases:</value>
</data>
<data name="usage_command" xml:space="preserve">
<value>Command:</value>
</data>
<data name="usage_optional" xml:space="preserve">
<value>(optional)</value>
</data>
<data name="usage_unknown_command" xml:space="preserve">
<value>Unknown command: {0}</value>
</data>
<data name="usage_run_help" xml:space="preserve">
<value>Run 'FancyZonesCLI --help' to see available commands.</value>
</data>
</root>

View File

@@ -1,52 +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.Diagnostics.CodeAnalysis;
#nullable enable
namespace FancyZonesCLI.Utils;
/// <summary>
/// Helper class for normalizing GUID strings to Windows format with braces.
/// Supports input with or without braces: both "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
/// and "{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}" are accepted.
/// </summary>
internal static class GuidHelper
{
/// <summary>
/// Normalizes a GUID string to Windows format with braces.
/// Returns null if the input is not a valid GUID.
/// </summary>
/// <param name="input">GUID string with or without braces.</param>
/// <returns>GUID in "{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}" format, or null if invalid.</returns>
public static string? NormalizeGuid(string? input)
{
if (string.IsNullOrWhiteSpace(input))
{
return null;
}
if (Guid.TryParse(input, out Guid guid))
{
// "B" format includes braces: {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}
return guid.ToString("B").ToUpperInvariant();
}
return null;
}
/// <summary>
/// Tries to normalize a GUID string to Windows format with braces.
/// </summary>
/// <param name="input">GUID string with or without braces.</param>
/// <param name="normalizedGuid">The normalized GUID string, or the original input if normalization fails.</param>
/// <returns>True if the input was successfully normalized; otherwise, false.</returns>
public static bool TryNormalizeGuid(string? input, [NotNullWhen(true)] out string? normalizedGuid)
{
normalizedGuid = NormalizeGuid(input);
return normalizedGuid != null;
}
}

View File

@@ -4,7 +4,6 @@
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
using static FancyZonesEditorCommon.Data.CustomLayouts;
@@ -24,10 +23,8 @@ namespace FancyZonesEditorCommon.Data
{
public struct CanvasZoneWrapper
{
[JsonPropertyName("X")]
public int X { get; set; }
[JsonPropertyName("Y")]
public int Y { get; set; }
public int Width { get; set; }

View File

@@ -191,22 +191,27 @@ bool EditorParameters::Save(const WorkAreaConfiguration& configuration, OnThread
monitorJson.dpi = dpi;
// Get DPI-unaware values for dimensions (virtual coordinates for WPF sizing)
MONITORINFOEX monitorInfoUnaware{};
MONITORINFOEX monitorInfo{};
dpiUnawareThread.submit(OnThreadExecutor::task_t{ [&] {
monitorInfoUnaware.cbSize = sizeof(monitorInfoUnaware);
GetMonitorInfo(monitor, &monitorInfoUnaware);
monitorInfo.cbSize = sizeof(monitorInfo);
if (!GetMonitorInfo(monitor, &monitorInfo))
{
return;
}
} }).wait();
// Dimensions in virtual coordinates (from DPI-unaware thread)
monitorJson.monitorWidth = monitorInfoUnaware.rcMonitor.right - monitorInfoUnaware.rcMonitor.left;
monitorJson.monitorHeight = monitorInfoUnaware.rcMonitor.bottom - monitorInfoUnaware.rcMonitor.top;
monitorJson.workAreaWidth = monitorInfoUnaware.rcWork.right - monitorInfoUnaware.rcWork.left;
monitorJson.workAreaHeight = monitorInfoUnaware.rcWork.bottom - monitorInfoUnaware.rcWork.top;
float width = static_cast<float>(monitorInfo.rcMonitor.right - monitorInfo.rcMonitor.left);
float height = static_cast<float>(monitorInfo.rcMonitor.bottom - monitorInfo.rcMonitor.top);
DPIAware::Convert(monitor, width, height);
// Position in virtual coordinates (matched by DPI-unaware context in WPF editor)
monitorJson.left = monitorInfoUnaware.rcWork.left;
monitorJson.top = monitorInfoUnaware.rcWork.top;
monitorJson.monitorWidth = static_cast<int>(std::roundf(width));
monitorJson.monitorHeight = static_cast<int>(std::roundf(height));
// use dpi-unaware values
monitorJson.top = monitorInfo.rcWork.top;
monitorJson.left = monitorInfo.rcWork.left;
monitorJson.workAreaWidth = monitorInfo.rcWork.right - monitorInfo.rcWork.left;
monitorJson.workAreaHeight = monitorInfo.rcWork.bottom - monitorInfo.rcWork.top;
argsJson.monitors.emplace_back(std::move(monitorJson));
}

View File

@@ -67,18 +67,10 @@ namespace FancyZonesEditor.Models
Window.KeyUp += ((App)Application.Current).App_KeyUp;
Window.KeyDown += ((App)Application.Current).App_KeyDown;
// Store for DPI-unaware positioning
_virtualWorkArea = workArea;
// Set initial WPF properties
Window.Left = workArea.X;
Window.Top = workArea.Y;
Window.Width = workArea.Width;
Window.Height = workArea.Height;
// After HWND is created, reposition using DPI-unaware context
// This matches the C++ backend which uses a DPI-unaware thread
Window.SourceInitialized += OnWindowSourceInitialized;
}
public Monitor(string monitorName, string monitorInstanceId, string monitorSerialNumber, string virtualDesktop, int dpi, Rect workArea, Size monitorSize)
@@ -88,33 +80,16 @@ namespace FancyZonesEditor.Models
}
private LayoutSettings _settings;
private Rect _virtualWorkArea;
private void OnWindowSourceInitialized(object sender, EventArgs e)
{
// Reposition window using DPI-unaware context to match the virtual coordinates
// from the FancyZones C++ backend (which uses a DPI-unaware thread)
Utils.NativeMethods.SetWindowPositionDpiUnaware(
Window,
(int)_virtualWorkArea.X,
(int)_virtualWorkArea.Y,
(int)_virtualWorkArea.Width,
(int)_virtualWorkArea.Height);
}
public void Scale(double scaleFactor)
{
Device.Scale(scaleFactor);
_virtualWorkArea = Device.WorkAreaRect;
// Use DPI-unaware positioning
Utils.NativeMethods.SetWindowPositionDpiUnaware(
Window,
(int)_virtualWorkArea.X,
(int)_virtualWorkArea.Y,
(int)_virtualWorkArea.Width,
(int)_virtualWorkArea.Height);
var workArea = Device.WorkAreaRect;
Window.Left = workArea.X;
Window.Top = workArea.Y;
Window.Width = workArea.Width;
Window.Height = workArea.Height;
}
public void SetLayoutSettings(LayoutModel model)

View File

@@ -69,11 +69,7 @@ namespace FancyZonesEditor.Utils
}
else
{
// Convert virtual coordinates to physical resolution by applying DPI scale
double scale = DPI / 96.0;
int physicalWidth = (int)Math.Round(ScreenBoundsWidth * scale);
int physicalHeight = (int)Math.Round(ScreenBoundsHeight * scale);
return physicalWidth + " × " + physicalHeight;
return ScreenBoundsWidth + " × " + ScreenBoundsHeight;
}
}
}

View File

@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation
// 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.
@@ -17,48 +17,14 @@ namespace FancyZonesEditor.Utils
[DllImport("user32.dll")]
private static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);
[DllImport("user32.dll", SetLastError = true)]
private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);
[DllImport("user32.dll")]
private static extern IntPtr SetThreadDpiAwarenessContext(IntPtr dpiContext);
private const int GWL_EX_STYLE = -20;
private const int WS_EX_APPWINDOW = 0x00040000;
private const int WS_EX_TOOLWINDOW = 0x00000080;
private const uint SWP_NOZORDER = 0x0004;
private const uint SWP_NOACTIVATE = 0x0010;
private static readonly IntPtr DPI_AWARENESS_CONTEXT_UNAWARE = new IntPtr(-1);
public static void SetWindowStyleToolWindow(Window hwnd)
{
var helper = new WindowInteropHelper(hwnd).Handle;
_ = SetWindowLong(helper, GWL_EX_STYLE, (GetWindowLong(helper, GWL_EX_STYLE) | WS_EX_TOOLWINDOW) & ~WS_EX_APPWINDOW);
}
/// <summary>
/// Positions a WPF window using DPI-unaware context to match the virtual coordinates
/// from the FancyZones C++ backend (which uses a DPI-unaware thread).
/// This fixes overlay positioning on mixed-DPI multi-monitor setups.
/// </summary>
public static void SetWindowPositionDpiUnaware(Window window, int x, int y, int width, int height)
{
var helper = new WindowInteropHelper(window).Handle;
if (helper != IntPtr.Zero)
{
// Temporarily switch to DPI-unaware context to position window.
// This matches how the C++ backend gets coordinates via dpiUnawareThread.
IntPtr oldContext = SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_UNAWARE);
try
{
SetWindowPos(helper, IntPtr.Zero, x, y, width, height, SWP_NOZORDER | SWP_NOACTIVATE);
}
finally
{
SetThreadDpiAwarenessContext(oldContext);
}
}
}
}
}

View File

@@ -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

View File

@@ -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]

View File

@@ -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))
{

View File

@@ -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)

View File

@@ -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" />

View File

@@ -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();
}
}
}
}

View File

@@ -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>

View File

@@ -367,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))

View File

@@ -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);
}
}

View File

@@ -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>

View File

@@ -10,7 +10,6 @@ using global::PowerToys.GPOWrapper;
using ManagedCommon;
using Microsoft.PowerToys.QuickAccess.Helpers;
using Microsoft.PowerToys.QuickAccess.Services;
using Microsoft.PowerToys.Settings.UI.Controls;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
@@ -27,7 +26,6 @@ public sealed class AllAppsViewModel : Observable
private readonly SettingsUtils _settingsUtils;
private readonly ResourceLoader _resourceLoader;
private readonly DispatcherQueue _dispatcherQueue;
private readonly List<FlyoutMenuItem> _allFlyoutMenuItems = new();
private GeneralSettings _generalSettings;
public ObservableCollection<FlyoutMenuItem> FlyoutMenuItems { get; }
@@ -60,28 +58,9 @@ public sealed class AllAppsViewModel : Observable
_resourceLoader = Helpers.ResourceLoaderInstance.ResourceLoader;
FlyoutMenuItems = new ObservableCollection<FlyoutMenuItem>();
BuildFlyoutMenuItems();
RefreshFlyoutMenuItems();
}
private void BuildFlyoutMenuItems()
{
_allFlyoutMenuItems.Clear();
foreach (ModuleType moduleType in Enum.GetValues<ModuleType>())
{
if (moduleType == ModuleType.GeneralSettings)
{
continue;
}
_allFlyoutMenuItems.Add(new FlyoutMenuItem
{
Tag = moduleType,
EnabledChangedCallback = EnabledChangedOnUI,
});
}
}
private void OnSettingsChanged(GeneralSettings newSettings)
{
_dispatcherQueue.TryEnqueue(() =>
@@ -103,37 +82,63 @@ public sealed class AllAppsViewModel : Observable
private void RefreshFlyoutMenuItems()
{
foreach (var item in _allFlyoutMenuItems)
var desiredItems = new List<FlyoutMenuItem>();
foreach (ModuleType moduleType in Enum.GetValues<ModuleType>())
{
var moduleType = item.Tag;
if (moduleType == ModuleType.GeneralSettings)
{
continue;
}
var gpo = Helpers.ModuleGpoHelper.GetModuleGpoConfiguration(moduleType);
var isLocked = gpo is GpoRuleConfigured.Enabled or GpoRuleConfigured.Disabled;
var isEnabled = gpo == GpoRuleConfigured.Enabled || (!isLocked && Microsoft.PowerToys.Settings.UI.Library.Helpers.ModuleHelper.GetIsModuleEnabled(_generalSettings, moduleType));
item.Label = _resourceLoader.GetString(Microsoft.PowerToys.Settings.UI.Library.Helpers.ModuleHelper.GetModuleLabelResourceName(moduleType));
item.IsLocked = isLocked;
item.Icon = Microsoft.PowerToys.Settings.UI.Library.Helpers.ModuleHelper.GetModuleTypeFluentIconName(moduleType);
var existingItem = FlyoutMenuItems.FirstOrDefault(x => x.Tag == moduleType);
if (item.IsEnabled != isEnabled)
if (existingItem != null)
{
item.UpdateStatus(isEnabled);
existingItem.Label = _resourceLoader.GetString(Microsoft.PowerToys.Settings.UI.Library.Helpers.ModuleHelper.GetModuleLabelResourceName(moduleType));
existingItem.IsLocked = isLocked;
existingItem.Icon = Microsoft.PowerToys.Settings.UI.Library.Helpers.ModuleHelper.GetModuleTypeFluentIconName(moduleType);
if (existingItem.IsEnabled != isEnabled)
{
var callback = existingItem.EnabledChangedCallback;
existingItem.EnabledChangedCallback = null;
existingItem.IsEnabled = isEnabled;
existingItem.EnabledChangedCallback = callback;
}
desiredItems.Add(existingItem);
}
else
{
desiredItems.Add(new FlyoutMenuItem
{
Label = _resourceLoader.GetString(Microsoft.PowerToys.Settings.UI.Library.Helpers.ModuleHelper.GetModuleLabelResourceName(moduleType)),
IsEnabled = isEnabled,
IsLocked = isLocked,
Tag = moduleType,
Icon = Microsoft.PowerToys.Settings.UI.Library.Helpers.ModuleHelper.GetModuleTypeFluentIconName(moduleType),
EnabledChangedCallback = EnabledChangedOnUI,
});
}
}
var sortedItems = DashboardSortOrder switch
{
DashboardSortOrder.ByStatus => _allFlyoutMenuItems.OrderByDescending(x => x.IsEnabled).ThenBy(x => x.Label).ToList(),
_ => _allFlyoutMenuItems.OrderBy(x => x.Label).ToList(),
DashboardSortOrder.ByStatus => desiredItems.OrderByDescending(x => x.IsEnabled).ThenBy(x => x.Label).ToList(),
_ => desiredItems.OrderBy(x => x.Label).ToList(),
};
if (FlyoutMenuItems.Count == 0)
for (int i = FlyoutMenuItems.Count - 1; i >= 0; i--)
{
foreach (var item in sortedItems)
if (!sortedItems.Contains(FlyoutMenuItems[i]))
{
FlyoutMenuItems.Add(item);
FlyoutMenuItems.RemoveAt(i);
}
return;
}
for (int i = 0; i < sortedItems.Count; i++)
@@ -141,22 +146,22 @@ public sealed class AllAppsViewModel : Observable
var item = sortedItems[i];
var oldIndex = FlyoutMenuItems.IndexOf(item);
if (oldIndex != -1 && oldIndex != i)
if (oldIndex < 0)
{
FlyoutMenuItems.Insert(i, item);
}
else if (oldIndex != i)
{
FlyoutMenuItems.Move(oldIndex, i);
}
}
}
private void EnabledChangedOnUI(ModuleListItem item)
private void EnabledChangedOnUI(FlyoutMenuItem item)
{
var flyoutItem = (FlyoutMenuItem)item;
if (_coordinator.UpdateModuleEnabled(flyoutItem.Tag, flyoutItem.IsEnabled))
if (_coordinator.UpdateModuleEnabled(item.Tag, item.IsEnabled))
{
_coordinator.NotifyUserSettingsInteraction();
// Trigger re-sort immediately when status changes on UI
RefreshFlyoutMenuItems();
}
}

View File

@@ -22,6 +22,21 @@ public sealed class FlyoutMenuItem : ModuleListItem
set => base.Tag = value;
}
public override bool IsEnabled
{
get => base.IsEnabled;
set
{
if (base.IsEnabled != value)
{
base.IsEnabled = value;
EnabledChangedCallback?.Invoke(this);
}
}
}
public Action<FlyoutMenuItem>? EnabledChangedCallback { get; set; }
public bool Visible
{
get => _visible;

View File

@@ -2,7 +2,6 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System; // For Action
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Input;
@@ -18,22 +17,6 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
private bool _isLocked;
private object? _tag;
private ICommand? _clickCommand;
private bool _isUpdating;
public Action<ModuleListItem>? EnabledChangedCallback { get; set; }
public void UpdateStatus(bool isEnabled)
{
_isUpdating = true;
try
{
IsEnabled = isEnabled;
}
finally
{
_isUpdating = false;
}
}
public virtual string Label
{
@@ -96,11 +79,6 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
{
_isEnabled = value;
OnPropertyChanged();
if (!_isUpdating)
{
EnabledChangedCallback?.Invoke(this);
}
}
}
}

View File

@@ -112,13 +112,6 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
eventHandle.Set();
}
return true;
case ModuleType.LightSwitch:
using (var eventHandle = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.LightSwitchToggleEvent()))
{
eventHandle.Set();
}
return true;
default:
return false;

View File

@@ -64,7 +64,6 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
AddFlyoutMenuItem(ModuleType.EnvironmentVariables);
AddFlyoutMenuItem(ModuleType.FancyZones);
AddFlyoutMenuItem(ModuleType.Hosts);
AddFlyoutMenuItem(ModuleType.LightSwitch);
AddFlyoutMenuItem(ModuleType.PowerLauncher);
AddFlyoutMenuItem(ModuleType.PowerOCR);
AddFlyoutMenuItem(ModuleType.RegistryPreview);
@@ -121,7 +120,6 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
{
ModuleType.ColorPicker => SettingsRepository<ColorPickerSettings>.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.ActivationShortcut.ToString(),
ModuleType.FancyZones => SettingsRepository<FancyZonesSettings>.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.FancyzonesEditorHotkey.Value.ToString(),
ModuleType.LightSwitch => SettingsRepository<LightSwitchSettings>.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.ToggleThemeHotkey.Value.ToString(),
ModuleType.PowerLauncher => SettingsRepository<PowerLauncherSettings>.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.OpenPowerLauncher.ToString(),
ModuleType.PowerOCR => SettingsRepository<PowerOcrSettings>.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.ActivationShortcut.ToString(),
ModuleType.Workspaces => SettingsRepository<WorkspacesSettings>.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.Hotkey.Value.ToString(),

View File

@@ -11,7 +11,7 @@ using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
namespace Microsoft.PowerToys.Settings.UI.Library
namespace Settings.UI.Library
{
public class LightSwitchSettings : BasePTModuleSettings, ISettingsConfig, ICloneable, IHotkeyConfig
{

View File

@@ -56,7 +56,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
[JsonSerializable(typeof(HostsSettings))]
[JsonSerializable(typeof(ImageResizerSettings))]
[JsonSerializable(typeof(KeyboardManagerSettings))]
[JsonSerializable(typeof(LightSwitchSettings))]
[JsonSerializable(typeof(SettingsUILibrary.LightSwitchSettings))]
[JsonSerializable(typeof(MeasureToolSettings))]
[JsonSerializable(typeof(MouseHighlighterSettings))]
[JsonSerializable(typeof(MouseJumpSettings))]

View File

@@ -23,7 +23,7 @@ namespace Microsoft.PowerToys.Settings.UI.SerializationContext;
[JsonSerializable(typeof(FileLocksmithSettings))]
[JsonSerializable(typeof(FindMyMouseSettings))]
[JsonSerializable(typeof(IList<PowerToysReleaseInfo>))]
[JsonSerializable(typeof(LightSwitchSettings))]
[JsonSerializable(typeof(SettingsUILibrary.LightSwitchSettings))]
[JsonSerializable(typeof(MeasureToolSettings))]
[JsonSerializable(typeof(MouseHighlighterSettings))]
[JsonSerializable(typeof(MouseJumpSettings))]

View File

@@ -50,7 +50,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
var darkSettings = this.moduleSettingsRepository.SettingsConfig;
// Pass them into the ViewModel
this.ViewModel = new LightSwitchViewModel(this.generalSettingsRepository, darkSettings, ShellPage.SendDefaultIPCMessage);
this.ViewModel = new LightSwitchViewModel(darkSettings, this.sendConfigMsg);
this.ViewModel.PropertyChanged += ViewModel_PropertyChanged;
this.LoadSettings(this.generalSettingsRepository, this.moduleSettingsRepository);
@@ -185,7 +185,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
// need to save the values
this.ViewModel.Latitude = latitude.ToString(CultureInfo.InvariantCulture);
this.ViewModel.Longitude = longitude.ToString(CultureInfo.InvariantCulture);
this.ViewModel.SyncButtonInformation = $"{this.ViewModel.Latitude}°, {this.ViewModel.Longitude}°";
this.ViewModel.SyncButtonInformation = $"{this.ViewModel.Latitude}<EFBFBD>, {this.ViewModel.Longitude}<EFBFBD>";
var result = SunCalc.CalculateSunriseSunset(latitude, longitude, DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day);
@@ -293,7 +293,18 @@ namespace Microsoft.PowerToys.Settings.UI.Views
private void UpdateEnabledState(bool recommendedState)
{
ViewModel.RefreshEnabledState();
var enabledGpoRuleConfiguration = GPOWrapper.GetConfiguredLightSwitchEnabledValue();
if (enabledGpoRuleConfiguration == GpoRuleConfigured.Disabled || enabledGpoRuleConfiguration == GpoRuleConfigured.Enabled)
{
// Get the enabled state from GPO.
this.ViewModel.IsEnabledGpoConfigured = true;
this.ViewModel.EnabledGPOConfiguration = enabledGpoRuleConfiguration == GpoRuleConfigured.Enabled;
}
else
{
this.ViewModel.IsEnabled = recommendedState;
}
}
private async void SyncLocationButton_Click(object sender, RoutedEventArgs e)

View File

@@ -26,6 +26,21 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
set => base.Tag = value;
}
public Action<DashboardListItem> EnabledChangedCallback { get; set; }
public override bool IsEnabled
{
get => base.IsEnabled;
set
{
if (base.IsEnabled != value)
{
base.IsEnabled = value;
EnabledChangedCallback?.Invoke(this);
}
}
}
public bool Visible
{
get => _visible;

View File

@@ -50,6 +50,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
// Flag to prevent circular updates when a UI toggle triggers settings changes.
private bool _isUpdatingFromUI;
private bool _isUpdatingFromSettings;
private AllHotkeyConflictsData _allHotkeyConflictsData = new AllHotkeyConflictsData();
@@ -257,7 +258,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
// Only update if there's an actual change to minimize UI notifications.
if (item.IsEnabled != newEnabledState)
{
item.UpdateStatus(newEnabledState);
item.IsEnabled = newEnabledState;
}
if (item.IsLocked != newLockedState)
@@ -274,17 +275,19 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
/// Sets the _isUpdatingFromUI flag to prevent circular updates, then updates
/// settings, re-sorts if needed, and refreshes dependent collections.
/// </summary>
private void EnabledChangedOnUI(ModuleListItem item)
private void EnabledChangedOnUI(DashboardListItem dashboardListItem)
{
var dashboardListItem = (DashboardListItem)item;
var isEnabled = dashboardListItem.IsEnabled;
if (_isUpdatingFromSettings)
{
return;
}
_isUpdatingFromUI = true;
try
{
Views.ShellPage.UpdateGeneralSettingsCallback(dashboardListItem.Tag, isEnabled);
Views.ShellPage.UpdateGeneralSettingsCallback(dashboardListItem.Tag, dashboardListItem.IsEnabled);
if (dashboardListItem.Tag == ModuleType.NewPlus && isEnabled == true)
if (dashboardListItem.Tag == ModuleType.NewPlus && dashboardListItem.IsEnabled == true)
{
var settingsUtils = SettingsUtils.Default;
var settings = NewPlusViewModel.LoadSettings(settingsUtils);
@@ -322,6 +325,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
return;
}
_isUpdatingFromSettings = true;
try
{
RefreshModuleList();
@@ -336,6 +340,10 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
{
Logger.LogError($"Updating active/disabled modules list failed: {ex.Message}");
}
finally
{
_isUpdatingFromSettings = false;
}
}
/// <summary>

View File

@@ -14,10 +14,8 @@ using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Helpers;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
using Microsoft.PowerToys.Settings.UI.SerializationContext;
using Newtonsoft.Json.Linq;
using PowerToys.GPOWrapper;
using Settings.UI.Library;
using Settings.UI.Library.Helpers;
@@ -29,16 +27,10 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
private Func<string, int> SendConfigMSG { get; }
private GeneralSettings GeneralSettingsConfig { get; set; }
public ObservableCollection<SearchLocation> SearchLocations { get; } = new();
public LightSwitchViewModel(ISettingsRepository<GeneralSettings> settingsRepository, LightSwitchSettings initialSettings = null, Func<string, int> ipcMSGCallBackFunc = null)
public LightSwitchViewModel(LightSwitchSettings initialSettings = null, Func<string, int> ipcMSGCallBackFunc = null)
{
ArgumentNullException.ThrowIfNull(settingsRepository);
GeneralSettingsConfig = settingsRepository.SettingsConfig;
InitializeEnabledValue();
_moduleSettings = initialSettings ?? new LightSwitchSettings();
SendConfigMSG = ipcMSGCallBackFunc;
@@ -66,21 +58,6 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
return hotkeysDict;
}
private void InitializeEnabledValue()
{
_enabledGpoRuleConfiguration = GPOWrapper.GetConfiguredLightSwitchEnabledValue();
if (_enabledGpoRuleConfiguration == GpoRuleConfigured.Disabled || _enabledGpoRuleConfiguration == GpoRuleConfigured.Enabled)
{
// Get the enabled state from GPO.
_enabledStateIsGPOConfigured = true;
_isEnabled = _enabledGpoRuleConfiguration == GpoRuleConfigured.Enabled;
}
else
{
_isEnabled = GeneralSettingsConfig.Enabled.LightSwitch;
}
}
private void ForceLightNow()
{
Logger.LogInfo("Sending custom action: forceLight");
@@ -116,26 +93,33 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
public bool IsEnabled
{
get => _isEnabled;
set
get
{
if (_enabledStateIsGPOConfigured)
{
// If it's GPO configured, shouldn't be able to change this state.
return;
return _enabledGPOConfiguration;
}
if (value != _isEnabled)
else
{
return _isEnabled;
}
}
set
{
if (_isEnabled != value)
{
if (_enabledStateIsGPOConfigured)
{
// If it's GPO configured, shouldn't be able to change this state.
return;
}
_isEnabled = value;
// Set the status in the general settings configuration
GeneralSettingsConfig.Enabled.LightSwitch = value;
OutGoingGeneralSettings snd = new OutGoingGeneralSettings(GeneralSettingsConfig);
RefreshEnabledState();
SendConfigMSG(snd.ToString());
OnPropertyChanged(nameof(IsEnabled));
NotifyPropertyChanged();
}
}
}
@@ -143,16 +127,24 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
public bool IsEnabledGpoConfigured
{
get => _enabledStateIsGPOConfigured;
}
public GpoRuleConfigured EnabledGPOConfiguration
{
get => _enabledGpoRuleConfiguration;
set
{
if (_enabledGpoRuleConfiguration != value)
if (_enabledStateIsGPOConfigured != value)
{
_enabledGpoRuleConfiguration = value;
_enabledStateIsGPOConfigured = value;
NotifyPropertyChanged();
}
}
}
public bool EnabledGPOConfiguration
{
get => _enabledGPOConfiguration;
set
{
if (_enabledGPOConfiguration != value)
{
_enabledGPOConfiguration = value;
NotifyPropertyChanged();
}
}
@@ -583,7 +575,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
}
private bool _enabledStateIsGPOConfigured;
private GpoRuleConfigured _enabledGpoRuleConfiguration;
private bool _enabledGPOConfiguration;
private LightSwitchSettings _moduleSettings;
private bool _isEnabled;
private HotkeySettings _toggleThemeHotkey;