Compare commits

...

8 Commits

Author SHA1 Message Date
Mike Griese
51aeaafb30 CmdPal: Bump version to 0.5 (#41442)
title
2025-08-29 14:07:23 -05:00
Kai Tao
baaa4d8f88 Settings: Search fancy zone settings and swallow the ctrl+f event (#41437)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
This pull request primarily improves the maintainability and robustness
of the FancyZones settings UI by assigning unique `Name` attributes to
`SettingsCard` controls in the `FancyZonesPage.xaml` file. Additionally,
it enhances the logic for retrieving element UIDs and improves fallback
behavior for missing localizations. There is also a minor usability fix
in the shell page to prevent unintended navigation when focusing the
search box.

**FancyZones settings UI improvements:**

* Added unique `Name` attributes to all `tkcontrols:SettingsCard`
elements in `FancyZonesPage.xaml` to facilitate easier referencing and
future maintainability. This affects all relevant settings groups and
controls in the file.
[[1]](diffhunk://#diff-93623d4db1d295dde0ef793053c9db0d9f673d753b787ad12fd71b8e9e40a79fL73-R85)
[[2]](diffhunk://#diff-93623d4db1d295dde0ef793053c9db0d9f673d753b787ad12fd71b8e9e40a79fL109-R109)
[[3]](diffhunk://#diff-93623d4db1d295dde0ef793053c9db0d9f673d753b787ad12fd71b8e9e40a79fL121-R121)
[[4]](diffhunk://#diff-93623d4db1d295dde0ef793053c9db0d9f673d753b787ad12fd71b8e9e40a79fL167-R188)
[[5]](diffhunk://#diff-93623d4db1d295dde0ef793053c9db0d9f673d753b787ad12fd71b8e9e40a79fL202-R202)
[[6]](diffhunk://#diff-93623d4db1d295dde0ef793053c9db0d9f673d753b787ad12fd71b8e9e40a79fL251-R251)
[[7]](diffhunk://#diff-93623d4db1d295dde0ef793053c9db0d9f673d753b787ad12fd71b8e9e40a79fL265-R265)
[[8]](diffhunk://#diff-93623d4db1d295dde0ef793053c9db0d9f673d753b787ad12fd71b8e9e40a79fL280-R280)

**Element UID retrieval and localization:**

* Improved `GetElementUid` in `Program.cs` to fall back to the first
child's `x:Uid` if the element itself lacks one, increasing robustness
when parsing XAML.
* Updated `GetLocalizedSettingHeaderAndD` in `SearchIndexService.cs` to
provide a fallback for missing localizations by trying the
`"{elementUid}/Content"` resource key.

**Shell page usability:**

* Modified `CtrlF_Invoked` in `ShellPage.xaml.cs` to mark the event as
handled, preventing unintended navigation when the search box is focused
with Ctrl+F.
<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [ ] Closes: #xxx
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [x] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed


https://github.com/user-attachments/assets/9cf15605-1114-4c6d-923c-d05c2733a274
2025-08-28 11:35:15 +08:00
leileizhang
931204abad fix: Context menu registry entries are not cleaned up when disabled via GPO (#41411)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
This PR fixes an issue where context menu runtime registration wasn't
properly cleaned up when GPO (Group Policy Object) policies disabled the
module. The problem occurred because the module constructor didn't
consider GPO policies when determining its initial enabled state.

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [x] Closes: #41387 
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed

This pull request refactors how context menu registration and
unregistration are handled across several PowerToys modules. The main
improvement is the introduction of a new `UpdateRegistration` helper
method in each module, which centralizes and simplifies the logic for
registering or unregistering context menus when the module is enabled or
disabled. This reduces code duplication and ensures consistent behavior.

**Context menu registration logic refactor:**

* Added a private `UpdateRegistration` method to each of the following
modules to handle context menu registration and unregistration based on
the enabled state:
  - `FileLocksmithModule` in `PowerToysModule.cpp`
  - `NewModule` in `powertoys_module.cpp`
  - `ImageResizerModule` in `dllmain.cpp`
  - `PowerRenameModule` in `dllmain.cpp`

* Replaced direct calls to registration/unregistration functions in
`enable`, `disable`, and `init_settings` methods with calls to the new
`UpdateRegistration` method in all affected modules, ensuring consistent
and centralized handling
[[1]](diffhunk://#diff-256ed936dafec1bf6ff17849b4797dd276f5b07bebe2e483bc1580c8f06e92d9L91-R122)
[[2]](diffhunk://#diff-256ed936dafec1bf6ff17849b4797dd276f5b07bebe2e483bc1580c8f06e92d9R155)
[[3]](diffhunk://#diff-4a3942d548f3daec02a833983ed9b2b69f75e2cd1b74a8ce1b874f3fd33fde55L101-R125)
[[4]](diffhunk://#diff-4a3942d548f3daec02a833983ed9b2b69f75e2cd1b74a8ce1b874f3fd33fde55L153-R177)
[[5]](diffhunk://#diff-0c0a89e812ff4625d165417da14f1c3f203e5ac7907555ae4fde122f3dddcf7aL115-L130)
[[6]](diffhunk://#diff-34581ec47c37b0d2e1d9b59696225c47342930694e732db06cbdf653ceb2c2d7L205-R234)
[[7]](diffhunk://#diff-34581ec47c37b0d2e1d9b59696225c47342930694e732db06cbdf653ceb2c2d7R334).

These changes improve maintainability and reduce the risk of
inconsistent registration behavior across modules.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-28 11:35:06 +08:00
Kai Tao
aaded11741 Settings: Settings search fixes (#41381)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
Fix 3 issues

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [x] Closes: #41369, #41374, #41380
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed


https://github.com/user-attachments/assets/0e0df9fb-5aca-4b26-9d53-e6ddc49cab04

---------

Co-authored-by: Niels Laute <niels.laute@live.nl>
Co-authored-by: Jiří Polášek <me@jiripolasek.com>
2025-08-28 11:34:54 +08:00
Niels Laute
e1117ca181 Search UX improvements (#41386)
### Fixing visual glitch in the 'Show all results' template:

Before
<img width="588" height="103" alt="image"
src="https://github.com/user-attachments/assets/00da60ea-d0ab-4ffe-8f69-75be7e537d63"
/>

After
<img width="598" height="70" alt="image"
src="https://github.com/user-attachments/assets/dc859731-8783-494a-b561-ea6396ca69b3"
/>


### Displaying "Show results for * search term *" on search results page
Before
<img width="716" height="239" alt="image"
src="https://github.com/user-attachments/assets/a80e6e58-df88-47b2-85ab-c39cbdc88690"
/>

After
<img width="349" height="264" alt="image"
src="https://github.com/user-attachments/assets/99029ee6-94f7-4454-a443-640c22f9e6f3"
/>


### Using Accent Color for the search highlight for better visibility
and fixing a bug
Before

![highlight-bug](https://github.com/user-attachments/assets/2d4c0f5b-4030-4c10-a4ec-c7c5023034f0)


After


![fix-accent](https://github.com/user-attachments/assets/62a223a9-9360-4297-9127-5aaef82c162f)

---------

Co-authored-by: vanzue <vanzue@outlook.com>
2025-08-28 11:34:42 +08:00
Shawn Yuan
04963aceae Fix localization issue for shortcut conflict window (#41378)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
Added localization for conflict message shown in shortcut conflict
window and shortcut dialog.

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [x] Closes: #41373 
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [x] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed

Signed-off-by: Shawn Yuan <shuaiyuan@microsoft.com>
2025-08-28 11:34:27 +08:00
Niels Laute
d6fc550f3e [Fix] Adding Toolkit's TitleBar manually (#41383)
Temp fix for a VS bug when building the `PowerToys.Settings` project. 

There seems to be something wrong with the Labs TitleBar package, so
following up with the team to get it resolved. Meanwhile, I've moved the
Toolkit's source code for TitleBar to Settings as a custom control.

Once the package is fixed we can revert this change.
2025-08-28 11:34:14 +08:00
Michael Jolley
4d294f5d83 Adding sort to DateTime extension results (#41389)
Closes #41385

Adds a sort to the DateTime extension results.

<img width="1001" height="602" alt="image"
src="https://github.com/user-attachments/assets/ccccae6a-c4a4-460f-a6d3-3325ecfc53da"
/>

<img width="992" height="606" alt="image"
src="https://github.com/user-attachments/assets/b8af51bd-cbd0-4341-ac46-0fb3e97ec2ac"
/>
2025-08-28 11:33:45 +08:00
29 changed files with 1569 additions and 293 deletions

View File

@@ -49,6 +49,7 @@ ALPHATYPE
AModifier
amr
ANDSCANS
animatedvisuals
Animnate
ANull
AOC

View File

@@ -23,7 +23,6 @@
<PackageVersion Include="CommunityToolkit.WinUI.UI.Controls.DataGrid" Version="7.1.2" />
<PackageVersion Include="CommunityToolkit.WinUI.UI.Controls.Markdown" Version="7.1.2" />
<PackageVersion Include="CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock" Version="0.1.250703-build.2173" />
<PackageVersion Include="CommunityToolkit.Labs.WinUI.TitleBar" Version="0.0.1-build.2206" />
<PackageVersion Include="ControlzEx" Version="6.0.0" />
<PackageVersion Include="HelixToolkit" Version="2.24.0" />
<PackageVersion Include="HelixToolkit.Core.Wpf" Version="2.24.0" />

View File

@@ -1499,7 +1499,6 @@ SOFTWARE.
- CoenM.ImageSharp.ImageHash
- CommunityToolkit.Common
- CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock
- CommunityToolkit.Labs.WinUI.TitleBar
- CommunityToolkit.Mvvm
- CommunityToolkit.WinUI.Animations
- CommunityToolkit.WinUI.Collections

View File

@@ -19,6 +19,26 @@
class FileLocksmithModule : public PowertoyModuleIface
{
private:
// Update registration based on enabled state
void UpdateRegistration(bool enabled)
{
if (enabled)
{
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
FileLocksmithRuntimeRegistration::EnsureRegistered();
Logger::info(L"File Locksmith context menu registered");
#endif
}
else
{
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
FileLocksmithRuntimeRegistration::Unregister();
Logger::info(L"File Locksmith context menu unregistered");
#endif
}
}
public:
FileLocksmithModule()
{
@@ -88,21 +108,16 @@ public:
package::RegisterSparsePackage(path, packageUri);
}
}
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
FileLocksmithRuntimeRegistration::EnsureRegistered();
#endif
m_enabled = true;
UpdateRegistration(m_enabled);
}
virtual void disable() override
{
Logger::info(L"File Locksmith disabled");
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
FileLocksmithRuntimeRegistration::Unregister();
Logger::info(L"File Locksmith context menu unregistered (Win10)");
#endif
m_enabled = false;
UpdateRegistration(m_enabled);
}
virtual bool is_enabled() override
@@ -135,6 +150,7 @@ private:
{
m_enabled = FileLocksmithSettingsInstance().GetEnabled();
m_extended_only = FileLocksmithSettingsInstance().GetShowInExtendedContextMenu();
UpdateRegistration(m_enabled);
Trace::EnableFileLocksmith(m_enabled);
}

View File

@@ -21,6 +21,26 @@
// Note: Settings are managed via Settings and UI Settings
class NewModule : public PowertoyModuleIface
{
private:
// Update registration based on enabled state
void UpdateRegistration(bool enabled)
{
if (enabled)
{
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
NewPlusRuntimeRegistration::EnsureRegisteredWin10();
Logger::info(L"New+ context menu registered");
#endif
}
else
{
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
NewPlusRuntimeRegistration::Unregister();
Logger::info(L"New+ context menu unregistered");
#endif
}
}
public:
NewModule()
{
@@ -98,14 +118,9 @@ public:
{
newplus::utilities::register_msix_package();
}
else
{
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
NewPlusRuntimeRegistration::EnsureRegisteredWin10();
#endif
}
powertoy_new_enabled = true;
UpdateRegistration(powertoy_new_enabled);
}
virtual void disable() override
@@ -150,19 +165,14 @@ private:
{
Trace::EventToggleOnOff(false);
}
if (!package::IsWin11OrGreater())
{
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
NewPlusRuntimeRegistration::Unregister();
Logger::info(L"New+ context menu unregistered (Win10)");
#endif
}
powertoy_new_enabled = false;
UpdateRegistration(powertoy_new_enabled);
}
void init_settings()
{
powertoy_new_enabled = NewSettingsInstance().GetEnabled();
UpdateRegistration(powertoy_new_enabled);
}
};

View File

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

View File

@@ -79,16 +79,22 @@ public sealed partial class TimeDateCalculator
}
else
{
List<(int Score, AvailableResult Item)> itemScores = [];
// Generate filtered list of results
foreach (var f in availableFormats)
{
var score = f.Score(query, f.Label, f.AlternativeSearchTag);
if (score > 0)
{
results.Add(f.ToListItem());
itemScores.Add((score, f));
}
}
results = itemScores
.OrderByDescending(s => s.Score)
.Select(s => s.Item.ToListItem())
.ToList();
}
if (results.Count == 0)

View File

@@ -43,11 +43,32 @@ private:
//contains the non localized key of the powertoy
std::wstring app_key;
// Update registration based on enabled state
void UpdateRegistration(bool enabled)
{
if (enabled)
{
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
ImageResizerRuntimeRegistration::EnsureRegistered();
Logger::info(L"ImageResizer context menu registered");
#endif
}
else
{
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
ImageResizerRuntimeRegistration::Unregister();
Logger::info(L"ImageResizer context menu unregistered");
#endif
}
}
public:
// Constructor
ImageResizerModule()
{
m_enabled = CSettingsInstance().GetEnabled();
UpdateRegistration(m_enabled);
app_name = GET_RESOURCE_STRING(IDS_IMAGERESIZER);
app_key = ImageResizerConstants::ModuleKey;
LoggerHelpers::init_logger(app_key, L"ModuleInterface", LogSettings::imageResizerLoggerName);
@@ -112,10 +133,7 @@ public:
package::RegisterSparsePackage(path, packageUri);
}
}
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
ImageResizerRuntimeRegistration::EnsureRegistered();
#endif
UpdateRegistration(m_enabled);
Trace::EnableImageResizer(m_enabled);
}
@@ -123,11 +141,8 @@ public:
virtual void disable()
{
m_enabled = false;
UpdateRegistration(m_enabled);
Trace::EnableImageResizer(m_enabled);
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
ImageResizerRuntimeRegistration::Unregister();
Logger::info(L"ImageResizer context menu unregistered (Win10)");
#endif
}
// Returns if the powertoys is enabled

View File

@@ -168,6 +168,25 @@ private:
//contains the non localized key of the powertoy
std::wstring app_key;
// Update registration based on enabled state
void UpdateRegistration(bool enabled)
{
if (enabled)
{
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
PowerRenameRuntimeRegistration::EnsureRegistered();
Logger::info(L"PowerRename context menu registered");
#endif
}
else
{
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
PowerRenameRuntimeRegistration::Unregister();
Logger::info(L"PowerRename context menu unregistered");
#endif
}
}
public:
// Return the localized display name of the powertoy
virtual PCWSTR get_name() override
@@ -202,9 +221,7 @@ public:
package::RegisterSparsePackage(path, packageUri);
}
}
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
PowerRenameRuntimeRegistration::EnsureRegistered();
#endif
UpdateRegistration(m_enabled);
}
// Disable the powertoy
@@ -212,10 +229,7 @@ public:
{
m_enabled = false;
Logger::info(L"PowerRename disabled");
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
PowerRenameRuntimeRegistration::Unregister();
Logger::info(L"PowerRename context menu unregistered (Win10)");
#endif
UpdateRegistration(m_enabled);
}
// Returns if the powertoy is enabled
@@ -315,6 +329,7 @@ public:
void init_settings()
{
m_enabled = CSettingsInstance().GetEnabled();
UpdateRegistration(m_enabled);
Trace::EnablePowerRename(m_enabled);
}

View File

@@ -19,6 +19,13 @@ namespace Microsoft.PowerToys.Tools.XamlIndexBuilder
"ShellPage.xaml",
};
// Hardcoded panel-to-page mapping (temporary until generic panel host mapping is needed)
// Key: panel file base name (without .xaml), Value: owning page base name
private static readonly Dictionary<string, string> PanelPageMapping = new(StringComparer.OrdinalIgnoreCase)
{
{ "MouseJumpPanel", "MouseUtilsPage" },
};
private static JsonSerializerOptions serializeOption = new()
{
WriteIndented = true,
@@ -33,32 +40,117 @@ namespace Microsoft.PowerToys.Tools.XamlIndexBuilder
Environment.Exit(1);
}
string xamlDirectory = args[0];
string xamlRootDirectory = args[0];
string outputFile = args[1];
if (!Directory.Exists(xamlDirectory))
if (!Directory.Exists(xamlRootDirectory))
{
Debug.WriteLine($"Error: Directory '{xamlDirectory}' does not exist.");
Debug.WriteLine($"Error: Directory '{xamlRootDirectory}' does not exist.");
Environment.Exit(1);
}
try
{
var searchableElements = new List<SettingEntry>();
var xamlFiles = Directory.GetFiles(xamlDirectory, "*.xaml", SearchOption.AllDirectories);
var processedFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var xamlFile in xamlFiles)
void ScanDirectory(string root)
{
var fileName = Path.GetFileName(xamlFile);
if (ExcludedXamlFiles.Contains(fileName))
if (!Directory.Exists(root))
{
// Skip ShellPage.xaml as it contains many elements not relevant for search
continue;
return;
}
Debug.WriteLine($"Processing: {fileName}");
var elements = ExtractSearchableElements(xamlFile);
searchableElements.AddRange(elements);
Debug.WriteLine($"[XamlIndexBuilder] Scanning root: {root}");
var xamlFilesLocal = Directory.GetFiles(root, "*.xaml", SearchOption.AllDirectories);
foreach (var xamlFile in xamlFilesLocal)
{
var fullPath = Path.GetFullPath(xamlFile);
if (processedFiles.Contains(fullPath))
{
continue; // already handled (can happen if overlapping directories)
}
var fileName = Path.GetFileName(xamlFile);
if (ExcludedXamlFiles.Contains(fileName))
{
continue; // explicitly excluded
}
Debug.WriteLine($"Processing: {fileName}");
var elements = ExtractSearchableElements(xamlFile);
// Apply hardcoded panel mapping override
var baseName = Path.GetFileNameWithoutExtension(xamlFile);
if (PanelPageMapping.TryGetValue(baseName, out var hostPage))
{
for (int i = 0; i < elements.Count; i++)
{
var entry = elements[i];
entry.PageTypeName = hostPage;
elements[i] = entry;
}
}
searchableElements.AddRange(elements);
processedFiles.Add(fullPath);
}
}
// Scan well-known subdirectories under the provided root
var subDirs = new[] { "Views", "Panels" };
foreach (var sub in subDirs)
{
ScanDirectory(Path.Combine(xamlRootDirectory, sub));
}
// Fallback: also scan root directly (in case some XAML lives at root level)
ScanDirectory(xamlRootDirectory);
// -----------------------------------------------------------------------------
// Explicit include section: add specific XAML files that we always want indexed
// even if future logic excludes them or they live outside typical scan patterns.
// Add future files to the ExplicitExtraXamlFiles array below.
// -----------------------------------------------------------------------------
string[] explicitExtraXamlFiles = new[]
{
"MouseJumpPanel.xaml", // Mouse Jump settings panel
};
foreach (var extraFileName in explicitExtraXamlFiles)
{
try
{
var matches = Directory.GetFiles(xamlRootDirectory, extraFileName, SearchOption.AllDirectories);
foreach (var match in matches)
{
var full = Path.GetFullPath(match);
if (processedFiles.Contains(full))
{
continue; // already processed in general scan
}
Debug.WriteLine($"Processing (explicit include): {extraFileName}");
var elements = ExtractSearchableElements(full);
var baseName = Path.GetFileNameWithoutExtension(full);
if (PanelPageMapping.TryGetValue(baseName, out var hostPage))
{
for (int i = 0; i < elements.Count; i++)
{
var entry = elements[i];
entry.PageTypeName = hostPage;
elements[i] = entry;
}
}
searchableElements.AddRange(elements);
processedFiles.Add(full);
}
}
catch (Exception ex)
{
Debug.WriteLine($"Explicit include failed for {extraFileName}: {ex.Message}");
}
}
searchableElements = searchableElements.OrderBy(e => e.PageTypeName).ThenBy(e => e.ElementName).ToList();
@@ -97,15 +189,15 @@ namespace Microsoft.PowerToys.Tools.XamlIndexBuilder
.Where(e => e.Name.LocalName == "SettingsPageControl")
.Where(e => e.Attribute(x + "Uid") != null);
// Extract SettingsCard elements
// Extract SettingsCard elements (support both Name and x:Name)
var settingsElements = doc.Descendants()
.Where(e => e.Name.LocalName == "SettingsCard")
.Where(e => e.Attribute("Name") != null || e.Attribute(x + "Uid") != null);
.Where(e => e.Attribute("Name") != null || e.Attribute(x + "Name") != null || e.Attribute(x + "Uid") != null);
// Extract SettingsExpander elements
// Extract SettingsExpander elements (support both Name and x:Name)
var settingsExpanderElements = doc.Descendants()
.Where(e => e.Name.LocalName == "SettingsExpander")
.Where(e => e.Attribute("Name") != null || e.Attribute(x + "Uid") != null);
.Where(e => e.Attribute("Name") != null || e.Attribute(x + "Name") != null || e.Attribute(x + "Uid") != null);
// Process SettingsPageControl elements
foreach (var element in settingsPageElements)
@@ -185,16 +277,36 @@ namespace Microsoft.PowerToys.Tools.XamlIndexBuilder
public static string GetElementName(XElement element, XNamespace x)
{
// Get Name attribute (we call it ElementName in our indexing system)
var name = element.Attribute("Name")?.Value;
if (string.IsNullOrEmpty(name))
{
name = element.Attribute(x + "Name")?.Value;
}
return name;
}
public static string GetElementUid(XElement element, XNamespace x)
{
// Try x:Uid
// Try x:Uid on the element itself
var uid = element.Attribute(x + "Uid")?.Value;
return uid;
if (!string.IsNullOrWhiteSpace(uid))
{
return uid;
}
// Fallback: check the first direct child element's x:Uid
var firstChild = element.Elements().FirstOrDefault();
if (firstChild != null)
{
var childUid = firstChild.Attribute(x + "Uid")?.Value;
if (!string.IsNullOrWhiteSpace(childUid))
{
return childUid;
}
}
return null;
}
public static string GetParentElementName(XElement element, XNamespace x)
@@ -211,6 +323,11 @@ namespace Microsoft.PowerToys.Tools.XamlIndexBuilder
if (expanderParent?.Name.LocalName == "SettingsExpander")
{
var expanderName = expanderParent.Attribute("Name")?.Value;
if (string.IsNullOrEmpty(expanderName))
{
expanderName = expanderParent.Attribute(x + "Name")?.Value;
}
if (!string.IsNullOrEmpty(expanderName))
{
return expanderName;
@@ -221,6 +338,11 @@ namespace Microsoft.PowerToys.Tools.XamlIndexBuilder
{
// Direct child of SettingsExpander
var expanderName = current.Attribute("Name")?.Value;
if (string.IsNullOrEmpty(expanderName))
{
expanderName = current.Attribute(x + "Name")?.Value;
}
if (!string.IsNullOrEmpty(expanderName))
{
return expanderName;

View File

@@ -29,16 +29,15 @@
<!-- Remove UI library reference to avoid pulling WindowsDesktop runtime (WindowsBase) -->
<PropertyGroup>
<!-- Fallback to dotnet if not provided by the environment -->
<DotNetExe Condition="'$(DotNetExe)' == ''">dotnet</DotNetExe>
<XamlViewsDir Condition="'$(XamlViewsDir)' == ''">$(MSBuildProjectDirectory)\..\Settings.UI\SettingsXAML\Views</XamlViewsDir>
<XamlRootDir Condition="'$(XamlRootDir)' == ''">$(MSBuildProjectDirectory)\..\Settings.UI\SettingsXAML</XamlRootDir>
<XamlRootDir Condition="'$(XamlViewsDir)' != ''">$([System.IO.Path]::GetDirectoryName('$(XamlViewsDir)'))</XamlRootDir>
<GeneratedJsonFile Condition="'$(GeneratedJsonFile)' == ''">$(MSBuildProjectDirectory)\..\Settings.UI\Assets\Settings\search.index.json</GeneratedJsonFile>
</PropertyGroup>
<Target Name="GenerateSearchIndexSelf" AfterTargets="Build">
<RemoveDir Directories="$(MSBuildProjectDirectory)\obj\ARM64;$(MSBuildProjectDirectory)\obj\x64;$(MSBuildProjectDirectory)\bin" />
<MakeDir Directories="$([System.IO.Path]::GetDirectoryName('$(GeneratedJsonFile)'))" />
<Message Importance="high" Text="[XamlIndexBuilder] Generating search index. Views='$(XamlViewsDir)'; Out='$(GeneratedJsonFile)'; Tool='$(TargetPath)'; DotNet='$(DotNetExe)'." />
<!-- Execute via dotnet so host architecture doesn't need to match -->
<Exec Command="&quot;$(DotNetExe)&quot; &quot;$(TargetPath)&quot; &quot;$(XamlViewsDir)&quot; &quot;$(GeneratedJsonFile)&quot;" />
<Message Importance="high" Text="[XamlIndexBuilder] Generating search index. Root='$(XamlRootDir)'; Out='$(GeneratedJsonFile)'; Tool='$(TargetPath)'; DotNet='$(DotNetExe)'." />
<Exec Command="&quot;$(DotNetExe)&quot; &quot;$(TargetPath)&quot; &quot;$(XamlRootDir)&quot; &quot;$(GeneratedJsonFile)&quot;" />
</Target>
</Project>

View File

@@ -10,13 +10,14 @@ using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Hosting;
using Microsoft.UI.Xaml.Media;
using Windows.UI;
namespace Microsoft.PowerToys.Settings.UI.Helpers;
public abstract partial class NavigatablePage : Page
{
private const int ExpandWaitDuration = 500;
private const int AnimationDuration = 1000;
private const int AnimationDuration = 1850;
private NavigationParams _pendingNavigationParams;
@@ -79,21 +80,24 @@ public abstract partial class NavigatablePage : Page
return;
}
// Attempt to set keyboard focus so that screen readers announce the element and keyboard users land directly on it.
TrySetFocus(target);
// Get the visual and compositor
var visual = ElementCompositionPreview.GetElementVisual(target);
var compositor = visual.Compositor;
// Create a subtle glow effect using drop shadow
var dropShadow = compositor.CreateDropShadow();
dropShadow.Color = Microsoft.UI.Colors.Gray;
dropShadow.BlurRadius = 8f;
dropShadow.Color = (Color)Application.Current.Resources["SystemAccentColorLight2"];
dropShadow.BlurRadius = 16f;
dropShadow.Opacity = 0f;
dropShadow.Offset = new Vector3(0, 0, 0);
var spriteVisual = compositor.CreateSpriteVisual();
spriteVisual.Size = new Vector2((float)target.ActualWidth + 16, (float)target.ActualHeight + 16);
spriteVisual.Size = new Vector2((float)target.ActualWidth + 8, (float)target.ActualHeight + 8);
spriteVisual.Shadow = dropShadow;
spriteVisual.Offset = new Vector3(-8, -8, 0);
spriteVisual.Offset = new Vector3(-4, -4, 0);
// Insert the shadow visual behind the target element
ElementCompositionPreview.SetElementChildVisual(target, spriteVisual);
@@ -106,39 +110,135 @@ public abstract partial class NavigatablePage : Page
fadeAnimation.Duration = TimeSpan.FromMilliseconds(AnimationDuration);
dropShadow.StartAnimation("Opacity", fadeAnimation);
if (target is Control ctrl)
{
// TODO: ability to adjust brush color and animation from settings.
var originalBackground = ctrl.Background;
var highlightBrush = new SolidColorBrush();
var grayColor = Microsoft.UI.Colors.Gray;
grayColor.A = 50; // Very subtle transparency
highlightBrush.Color = grayColor;
// Apply the highlight
ctrl.Background = highlightBrush;
// Wait for animation to complete
await Task.Delay(AnimationDuration);
// Restore original background
ctrl.Background = originalBackground;
}
else
{
// For non-control elements, just wait for the glow animation
await Task.Delay(AnimationDuration);
}
await Task.Delay(AnimationDuration);
// Clean up the shadow visual
ElementCompositionPreview.SetElementChildVisual(target, null);
}
private static void TrySetFocus(FrameworkElement target)
{
try
{
// Prefer Control.Focus when available.
if (target is Control ctrl)
{
// Ensure it can receive focus.
if (!ctrl.IsTabStop)
{
ctrl.IsTabStop = true;
}
ctrl.Focus(FocusState.Programmatic);
}
// Target is not a Control. Find first focusable descendant Control.
var focusCandidate = FindFirstFocusableDescendant(target);
if (focusCandidate != null)
{
if (!focusCandidate.IsTabStop)
{
focusCandidate.IsTabStop = true;
}
focusCandidate.Focus(FocusState.Programmatic);
return;
}
// Fallback: attempt to focus parent control if no descendant found.
if (target.Parent is Control parent)
{
if (!parent.IsTabStop)
{
parent.IsTabStop = true;
}
parent.Focus(FocusState.Programmatic);
}
}
catch
{
// Swallow focus exceptions; not critical. Could log if logging enabled.
// Leave the default focus as it is.
}
}
private static Control FindFirstFocusableDescendant(FrameworkElement root)
{
if (root == null)
{
return null;
}
var queue = new System.Collections.Generic.Queue<DependencyObject>();
queue.Enqueue(root);
while (queue.Count > 0)
{
var current = queue.Dequeue();
if (current is Control c && c.IsEnabled && c.Visibility == Visibility.Visible)
{
return c;
}
int count = VisualTreeHelper.GetChildrenCount(current);
for (int i = 0; i < count; i++)
{
queue.Enqueue(VisualTreeHelper.GetChild(current, i));
}
}
return null;
}
protected FrameworkElement FindElementByName(string name)
{
var element = this.FindName(name) as FrameworkElement;
return element;
if (element != null)
{
return element;
}
if (this.Content is DependencyObject root)
{
var found = FindInDescendants(root, name);
if (found != null)
{
return found;
}
}
return null;
}
private static FrameworkElement FindInDescendants(DependencyObject root, string name)
{
if (root == null || string.IsNullOrEmpty(name))
{
return null;
}
var queue = new System.Collections.Generic.Queue<DependencyObject>();
queue.Enqueue(root);
while (queue.Count > 0)
{
var current = queue.Dequeue();
if (current is FrameworkElement fe)
{
var local = fe.FindName(name) as FrameworkElement;
if (local != null)
{
return local;
}
}
int count = VisualTreeHelper.GetChildrenCount(current);
for (int i = 0; i < count; i++)
{
var child = VisualTreeHelper.GetChild(current, i);
queue.Enqueue(child);
}
}
return null;
}
}

View File

@@ -25,6 +25,7 @@
<None Remove="SettingsXAML\Controls\Dashboard\CheckUpdateControl.xaml" />
<None Remove="SettingsXAML\Controls\Dashboard\ShortcutConflictControl.xaml" />
<None Remove="SettingsXAML\Controls\KeyVisual\KeyCharPresenter.xaml" />
<None Remove="SettingsXAML\Controls\TitleBar\TitleBar.xaml" />
</ItemGroup>
<ItemGroup>
<Page Remove="SettingsXAML\App.xaml" />
@@ -63,7 +64,6 @@
<PackageReference Include="CommunityToolkit.WinUI.Extensions" />
<PackageReference Include="CommunityToolkit.WinUI.Converters" />
<PackageReference Include="CommunityToolkit.WinUI.UI.Controls.Markdown" />
<PackageReference Include="CommunityToolkit.Labs.WinUI.TitleBar" />
<PackageReference Include="System.Net.Http" />
<PackageReference Include="System.Private.Uri" />
<PackageReference Include="System.Text.RegularExpressions" />
@@ -156,7 +156,8 @@
</None>
<None Update="Assets\Settings\Scripts\DisableModule.ps1">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</None> <Page Update="SettingsXAML\Controls\TitleBar\TitleBar.xaml">
</Page>
<Page Update="SettingsXAML\Controls\KeyVisual\KeyCharPresenter.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
@@ -180,14 +181,8 @@
</Page>
</ItemGroup>
<!-- Removed nested publish/exec and copy targets. -->
<!-- Build XamlIndexBuilder before compiling Settings to ensure the search index exists without taking a project reference. -->
<Target Name="BuildXamlIndexBeforeSettings" BeforeTargets="CoreCompile">
<Message Importance="high" Text="[Settings] Building XamlIndexBuilder prior to compile. Views='$(MSBuildProjectDirectory)\SettingsXAML\Views' Out='$(GeneratedJsonFile)'" />
<MSBuild
Projects="..\Settings.UI.XamlIndexBuilder\Settings.UI.XamlIndexBuilder.csproj"
Targets="Build"
Properties="Configuration=$(Configuration);Platform=Any CPU;TargetFramework=net9.0;XamlViewsDir=$(MSBuildProjectDirectory)\SettingsXAML\Views;GeneratedJsonFile=$(GeneratedJsonFile)" />
<MSBuild Projects="..\Settings.UI.XamlIndexBuilder\Settings.UI.XamlIndexBuilder.csproj" Targets="Build" Properties="Configuration=$(Configuration);Platform=Any CPU;TargetFramework=net9.0;XamlViewsDir=$(MSBuildProjectDirectory)\SettingsXAML\Views;GeneratedJsonFile=$(GeneratedJsonFile)" />
</Target>
</Project>

View File

@@ -170,7 +170,7 @@ namespace Microsoft.PowerToys.Settings.UI.Services
if (string.IsNullOrEmpty(header))
{
Debug.WriteLine($"[SearchIndexService] WARNING: No header localization found for ElementUid: '{elementUid}'");
header = GetString(resourceLoader, $"{elementUid}/Content");
}
return (header, description);

View File

@@ -11,6 +11,7 @@
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
<ResourceDictionary Source="/SettingsXAML/Controls/KeyVisual/KeyVisual.xaml" />
<ResourceDictionary Source="/SettingsXAML/Controls/KeyVisual/KeyCharPresenter.xaml" />
<ResourceDictionary Source="/SettingsXAML/Controls/TitleBar/TitleBar.xaml" />
<ResourceDictionary Source="/SettingsXAML/Styles/TextBlock.xaml" />
<ResourceDictionary Source="/SettingsXAML/Styles/Button.xaml" />
<ResourceDictionary Source="/SettingsXAML/Styles/InfoBadge.xaml" />

View File

@@ -548,18 +548,14 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
}
}
if (conflictingModules.Count > 0)
var moduleNames = conflictingModules.ToArray();
if (string.Equals(moduleNames[0], "System", StringComparison.OrdinalIgnoreCase))
{
var moduleNames = conflictingModules.ToArray();
var conflictMessage = moduleNames.Length == 1
? $"Conflict detected with {moduleNames[0]}"
: $"Conflicts detected with: {string.Join(", ", moduleNames)}";
c.ConflictMessage = conflictMessage;
c.ConflictMessage = ResourceLoaderInstance.ResourceLoader.GetString("SysHotkeyConflictTooltipText");
}
else
{
c.ConflictMessage = "Conflict detected with unknown module";
c.ConflictMessage = ResourceLoaderInstance.ResourceLoader.GetString("InAppHotkeyConflictTooltipText");
}
c.HasConflict = true;

View File

@@ -64,7 +64,7 @@
IsTabStop="{Binding ElementName=ShortcutContentControl, Path=IsWarningAltGr, Mode=OneWay}"
Severity="Warning" />
<InfoBar
Title="Hotkey Conflict"
x:Uid="WarningShortcutConflict"
IsClosable="False"
IsOpen="{Binding ElementName=ShortcutContentControl, Path=HasConflict, Mode=OneWay}"
IsTabStop="{Binding ElementName=ShortcutContentControl, Path=HasConflict, Mode=OneWay}"

View File

@@ -0,0 +1,229 @@
// 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.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace Microsoft.PowerToys.Settings.UI.Controls;
public partial class TitleBar : Control
{
/// <summary>
/// The backing <see cref="DependencyProperty"/> for the <see cref="Icon"/> property.
/// </summary>
public static readonly DependencyProperty IconProperty = DependencyProperty.Register(nameof(Icon), typeof(IconElement), typeof(TitleBar), new PropertyMetadata(null, IconChanged));
/// <summary>
/// The backing <see cref="DependencyProperty"/> for the <see cref="Title"/> property.
/// </summary>
public static readonly DependencyProperty TitleProperty = DependencyProperty.Register(nameof(Title), typeof(string), typeof(TitleBar), new PropertyMetadata(default(string)));
/// <summary>
/// The backing <see cref="DependencyProperty"/> for the <see cref="Subtitle"/> property.
/// </summary>
public static readonly DependencyProperty SubtitleProperty = DependencyProperty.Register(nameof(Subtitle), typeof(string), typeof(TitleBar), new PropertyMetadata(default(string)));
/// <summary>
/// The backing <see cref="DependencyProperty"/> for the <see cref="Content"/> property.
/// </summary>
public static readonly DependencyProperty ContentProperty = DependencyProperty.Register(nameof(Content), typeof(object), typeof(TitleBar), new PropertyMetadata(null, ContentChanged));
/// <summary>
/// The backing <see cref="DependencyProperty"/> for the <see cref="Footer"/> property.
/// </summary>
public static readonly DependencyProperty FooterProperty = DependencyProperty.Register(nameof(Footer), typeof(object), typeof(TitleBar), new PropertyMetadata(null, FooterChanged));
/// <summary>
/// The backing <see cref="DependencyProperty"/> for the <see cref="IsBackButtonVisible"/> property.
/// </summary>
public static readonly DependencyProperty IsBackButtonVisibleProperty = DependencyProperty.Register(nameof(IsBackButtonVisible), typeof(bool), typeof(TitleBar), new PropertyMetadata(false, IsBackButtonVisibleChanged));
/// <summary>
/// The backing <see cref="DependencyProperty"/> for the <see cref="IsPaneButtonVisible"/> property.
/// </summary>
public static readonly DependencyProperty IsPaneButtonVisibleProperty = DependencyProperty.Register(nameof(IsPaneButtonVisible), typeof(bool), typeof(TitleBar), new PropertyMetadata(false, IsPaneButtonVisibleChanged));
/// <summary>
/// The backing <see cref="DependencyProperty"/> for the <see cref="DisplayMode"/> property.
/// </summary>
public static readonly DependencyProperty DisplayModeProperty = DependencyProperty.Register(nameof(DisplayMode), typeof(DisplayMode), typeof(TitleBar), new PropertyMetadata(DisplayMode.Standard, DisplayModeChanged));
/// <summary>
/// The backing <see cref="DependencyProperty"/> for the <see cref="CompactStateBreakpoint
/// "/> property.
/// </summary>
public static readonly DependencyProperty CompactStateBreakpointProperty = DependencyProperty.Register(nameof(CompactStateBreakpoint), typeof(int), typeof(TitleBar), new PropertyMetadata(850));
/// <summary>
/// The backing <see cref="DependencyProperty"/> for the <see cref="AutoConfigureCustomTitleBar"/> property.
/// </summary>
public static readonly DependencyProperty AutoConfigureCustomTitleBarProperty = DependencyProperty.Register(nameof(AutoConfigureCustomTitleBar), typeof(bool), typeof(TitleBar), new PropertyMetadata(true, AutoConfigureCustomTitleBarChanged));
/// <summary>
/// The backing <see cref="DependencyProperty"/> for the <see cref="Window"/> property.
/// </summary>
public static readonly DependencyProperty WindowProperty = DependencyProperty.Register(nameof(Window), typeof(Window), typeof(TitleBar), new PropertyMetadata(null));
/// <summary>
/// The event that gets fired when the back button is clicked
/// </summary>
#pragma warning disable CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
public event EventHandler<RoutedEventArgs>? BackButtonClick;
#pragma warning restore CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
/// <summary>
/// The event that gets fired when the pane toggle button is clicked
/// </summary>
#pragma warning disable CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
public event EventHandler<RoutedEventArgs>? PaneButtonClick;
#pragma warning restore CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
/// <summary>
/// Gets or sets the Icon
/// </summary>
public IconElement Icon
{
get => (IconElement)GetValue(IconProperty);
set => SetValue(IconProperty, value);
}
/// <summary>
/// Gets or sets the Title
/// </summary>
public string Title
{
get => (string)GetValue(TitleProperty);
set => SetValue(TitleProperty, value);
}
/// <summary>
/// Gets or sets the Subtitle
/// </summary>
public string Subtitle
{
get => (string)GetValue(SubtitleProperty);
set => SetValue(SubtitleProperty, value);
}
/// <summary>
/// Gets or sets the content shown at the center of the TitleBar. When setting this, using DisplayMode=Tall is recommended.
/// </summary>
public object Content
{
get => (object)GetValue(ContentProperty);
set => SetValue(ContentProperty, value);
}
/// <summary>
/// Gets or sets the content shown at the right of the TitleBar, next to the caption buttons. When setting this, using DisplayMode=Tall is recommended.
/// </summary>
public object Footer
{
get => (object)GetValue(FooterProperty);
set => SetValue(FooterProperty, value);
}
/// <summary>
/// Gets or sets DisplayMode. Compact is default (32px), Tall is recommended when setting the Content or Footer.
/// </summary>
public DisplayMode DisplayMode
{
get => (DisplayMode)GetValue(DisplayModeProperty);
set => SetValue(DisplayModeProperty, value);
}
/// <summary>
/// Gets or sets a value indicating whether gets or sets the visibility of the back button.
/// </summary>
public bool IsBackButtonVisible
{
get => (bool)GetValue(IsBackButtonVisibleProperty);
set => SetValue(IsBackButtonVisibleProperty, value);
}
/// <summary>
/// Gets or sets a value indicating whether gets or sets the visibility of the pane toggle button.
/// </summary>
public bool IsPaneButtonVisible
{
get => (bool)GetValue(IsPaneButtonVisibleProperty);
set => SetValue(IsPaneButtonVisibleProperty, value);
}
/// <summary>
/// Gets or sets the breakpoint of when the compact state is triggered.
/// </summary>
public int CompactStateBreakpoint
{
get => (int)GetValue(CompactStateBreakpointProperty);
set => SetValue(CompactStateBreakpointProperty, value);
}
/// <summary>
/// Gets or sets a value indicating whether gets or sets if the TitleBar should auto configure ExtendContentIntoTitleBar and CaptionButton background colors.
/// </summary>
public bool AutoConfigureCustomTitleBar
{
get => (bool)GetValue(AutoConfigureCustomTitleBarProperty);
set => SetValue(AutoConfigureCustomTitleBarProperty, value);
}
/// <summary>
/// Gets or sets the window the TitleBar should configure.
/// </summary>
public Window Window
{
get => (Window)GetValue(WindowProperty);
set => SetValue(WindowProperty, value);
}
private static void IsBackButtonVisibleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((TitleBar)d).Update();
}
private static void IsPaneButtonVisibleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((TitleBar)d).Update();
}
private static void DisplayModeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((TitleBar)d).Update();
}
private static void ContentChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((TitleBar)d).Update();
}
private static void FooterChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((TitleBar)d).Update();
}
private static void IconChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((TitleBar)d).Update();
}
private static void AutoConfigureCustomTitleBarChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (((TitleBar)d).AutoConfigureCustomTitleBar)
{
((TitleBar)d).Configure();
}
else
{
((TitleBar)d).Reset();
}
}
}
public enum DisplayMode
{
Standard,
Tall,
}

View File

@@ -0,0 +1,195 @@
// 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 Microsoft.UI;
using Microsoft.UI.Input;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
using Windows.Foundation;
using Windows.Graphics;
namespace Microsoft.PowerToys.Settings.UI.Controls;
[TemplatePart(Name = nameof(PART_FooterPresenter), Type = typeof(ContentPresenter))]
[TemplatePart(Name = nameof(PART_ContentPresenter), Type = typeof(ContentPresenter))]
public partial class TitleBar : Control
{
#pragma warning disable SA1306 // Field names should begin with lower-case letter
#pragma warning disable SA1310 // Field names should not contain underscore
#pragma warning disable SA1400 // Access modifier should be declared
#pragma warning disable CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
ContentPresenter? PART_ContentPresenter;
ContentPresenter? PART_FooterPresenter;
#pragma warning restore CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
#pragma warning restore SA1400 // Access modifier should be declared
#pragma warning restore SA1306 // Field names should begin with lower-case letter
#pragma warning restore SA1310 // Field names should not contain underscore
private void SetWASDKTitleBar()
{
if (this.Window == null)
{
return;
}
if (AutoConfigureCustomTitleBar)
{
Window.AppWindow.TitleBar.ExtendsContentIntoTitleBar = true;
this.Window.SizeChanged -= Window_SizeChanged;
this.Window.SizeChanged += Window_SizeChanged;
this.Window.Activated -= Window_Activated;
this.Window.Activated += Window_Activated;
if (Window.Content is FrameworkElement rootElement)
{
UpdateCaptionButtons(rootElement);
rootElement.ActualThemeChanged += (s, e) =>
{
UpdateCaptionButtons(rootElement);
};
}
PART_ContentPresenter = GetTemplateChild(nameof(PART_ContentPresenter)) as ContentPresenter;
PART_FooterPresenter = GetTemplateChild(nameof(PART_FooterPresenter)) as ContentPresenter;
// Get caption button occlusion information.
int captionButtonOcclusionWidthRight = Window.AppWindow.TitleBar.RightInset;
int captionButtonOcclusionWidthLeft = Window.AppWindow.TitleBar.LeftInset;
PART_LeftPaddingColumn!.Width = new GridLength(captionButtonOcclusionWidthLeft);
PART_RightPaddingColumn!.Width = new GridLength(captionButtonOcclusionWidthRight);
if (DisplayMode == DisplayMode.Tall)
{
// Choose a tall title bar to provide more room for interactive elements
// like search box or person picture controls.
Window.AppWindow.TitleBar.PreferredHeightOption = TitleBarHeightOption.Tall;
}
else
{
Window.AppWindow.TitleBar.PreferredHeightOption = TitleBarHeightOption.Standard;
}
// Recalculate the drag region for the custom title bar
// if you explicitly defined new draggable areas.
SetDragRegionForCustomTitleBar();
_isAutoConfigCompleted = true;
}
}
private void Window_SizeChanged(object sender, WindowSizeChangedEventArgs args)
{
UpdateVisualStateAndDragRegion(args.Size);
}
private void UpdateCaptionButtons(FrameworkElement rootElement)
{
Window.AppWindow.TitleBar.ButtonBackgroundColor = Colors.Transparent;
Window.AppWindow.TitleBar.ButtonInactiveBackgroundColor = Colors.Transparent;
if (rootElement.ActualTheme == ElementTheme.Dark)
{
Window.AppWindow.TitleBar.ButtonForegroundColor = Colors.White;
Window.AppWindow.TitleBar.ButtonInactiveForegroundColor = Colors.DarkGray;
}
else
{
Window.AppWindow.TitleBar.ButtonForegroundColor = Colors.Black;
Window.AppWindow.TitleBar.ButtonInactiveForegroundColor = Colors.DarkGray;
}
}
private void ResetWASDKTitleBar()
{
if (this.Window == null)
{
return;
}
// Only reset if we were the ones who configured
if (_isAutoConfigCompleted)
{
Window.AppWindow.TitleBar.ExtendsContentIntoTitleBar = false;
this.Window.SizeChanged -= Window_SizeChanged;
this.Window.Activated -= Window_Activated;
SizeChanged -= this.TitleBar_SizeChanged;
Window.AppWindow.TitleBar.ResetToDefault();
}
}
private void Window_Activated(object sender, WindowActivatedEventArgs args)
{
if (args.WindowActivationState == WindowActivationState.Deactivated)
{
VisualStateManager.GoToState(this, WindowDeactivatedState, true);
}
else
{
VisualStateManager.GoToState(this, WindowActivatedState, true);
}
}
public void SetDragRegionForCustomTitleBar()
{
if (AutoConfigureCustomTitleBar && Window is not null)
{
ClearDragRegions(NonClientRegionKind.Passthrough);
#pragma warning disable CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
var items = new FrameworkElement?[] { PART_ContentPresenter, PART_FooterPresenter, PART_ButtonHolder };
#pragma warning restore CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
var validItems = items.Where(x => x is not null).Select(x => x!).ToArray(); // Prune null items
SetDragRegion(NonClientRegionKind.Passthrough, validItems);
}
}
public double GetRasterizationScaleForElement(UIElement element)
{
if (element.XamlRoot != null)
{
return element.XamlRoot.RasterizationScale;
}
return 0.0;
}
public void SetDragRegion(NonClientRegionKind nonClientRegionKind, params FrameworkElement[] frameworkElements)
{
List<RectInt32> rects = new List<RectInt32>();
var scale = GetRasterizationScaleForElement(this);
foreach (var frameworkElement in frameworkElements)
{
if (frameworkElement == null)
{
continue;
}
GeneralTransform transformElement = frameworkElement.TransformToVisual(null);
Rect bounds = transformElement.TransformBounds(new Rect(0, 0, frameworkElement.ActualWidth, frameworkElement.ActualHeight));
var transparentRect = new RectInt32(
_X: (int)Math.Round(bounds.X * scale),
_Y: (int)Math.Round(bounds.Y * scale),
_Width: (int)Math.Round(bounds.Width * scale),
_Height: (int)Math.Round(bounds.Height * scale));
rects.Add(transparentRect);
}
if (rects.Count > 0)
{
InputNonClientPointerSource.GetForWindowId(Window.AppWindow.Id).SetRegionRects(nonClientRegionKind, rects.ToArray());
}
}
public void ClearDragRegions(NonClientRegionKind nonClientRegionKind)
{
InputNonClientPointerSource.GetForWindowId(Window.AppWindow.Id).ClearRegionRects(nonClientRegionKind);
}
}

View File

@@ -0,0 +1,220 @@
// 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 Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Windows.Foundation;
namespace Microsoft.PowerToys.Settings.UI.Controls;
[TemplateVisualState(Name = BackButtonVisibleState, GroupName = BackButtonStates)]
[TemplateVisualState(Name = BackButtonCollapsedState, GroupName = BackButtonStates)]
[TemplateVisualState(Name = PaneButtonVisibleState, GroupName = PaneButtonStates)]
[TemplateVisualState(Name = PaneButtonCollapsedState, GroupName = PaneButtonStates)]
[TemplateVisualState(Name = WindowActivatedState, GroupName = ActivationStates)]
[TemplateVisualState(Name = WindowDeactivatedState, GroupName = ActivationStates)]
[TemplateVisualState(Name = StandardState, GroupName = DisplayModeStates)]
[TemplateVisualState(Name = TallState, GroupName = DisplayModeStates)]
[TemplateVisualState(Name = IconVisibleState, GroupName = IconStates)]
[TemplateVisualState(Name = IconCollapsedState, GroupName = IconStates)]
[TemplateVisualState(Name = ContentVisibleState, GroupName = ContentStates)]
[TemplateVisualState(Name = ContentCollapsedState, GroupName = ContentStates)]
[TemplateVisualState(Name = FooterVisibleState, GroupName = FooterStates)]
[TemplateVisualState(Name = FooterCollapsedState, GroupName = FooterStates)]
[TemplateVisualState(Name = WideState, GroupName = ReflowStates)]
[TemplateVisualState(Name = NarrowState, GroupName = ReflowStates)]
[TemplatePart(Name = PartBackButton, Type = typeof(Button))]
[TemplatePart(Name = PartPaneButton, Type = typeof(Button))]
[TemplatePart(Name = nameof(PART_LeftPaddingColumn), Type = typeof(ColumnDefinition))]
[TemplatePart(Name = nameof(PART_RightPaddingColumn), Type = typeof(ColumnDefinition))]
[TemplatePart(Name = nameof(PART_ButtonHolder), Type = typeof(StackPanel))]
public partial class TitleBar : Control
{
private const string PartBackButton = "PART_BackButton";
private const string PartPaneButton = "PART_PaneButton";
private const string BackButtonVisibleState = "BackButtonVisible";
private const string BackButtonCollapsedState = "BackButtonCollapsed";
private const string BackButtonStates = "BackButtonStates";
private const string PaneButtonVisibleState = "PaneButtonVisible";
private const string PaneButtonCollapsedState = "PaneButtonCollapsed";
private const string PaneButtonStates = "PaneButtonStates";
private const string WindowActivatedState = "Activated";
private const string WindowDeactivatedState = "Deactivated";
private const string ActivationStates = "WindowActivationStates";
private const string IconVisibleState = "IconVisible";
private const string IconCollapsedState = "IconCollapsed";
private const string IconStates = "IconStates";
private const string StandardState = "Standard";
private const string TallState = "Tall";
private const string DisplayModeStates = "DisplayModeStates";
private const string ContentVisibleState = "ContentVisible";
private const string ContentCollapsedState = "ContentCollapsed";
private const string ContentStates = "ContentStates";
private const string FooterVisibleState = "FooterVisible";
private const string FooterCollapsedState = "FooterCollapsed";
private const string FooterStates = "FooterStates";
private const string WideState = "Wide";
private const string NarrowState = "Narrow";
private const string ReflowStates = "ReflowStates";
#pragma warning disable SA1306 // Field names should begin with lower-case letter
#pragma warning disable SA1310 // Field names should not contain underscore
#pragma warning disable SA1400 // Access modifier should be declared
#pragma warning disable CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
ColumnDefinition? PART_RightPaddingColumn;
ColumnDefinition? PART_LeftPaddingColumn;
StackPanel? PART_ButtonHolder;
#pragma warning restore CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
#pragma warning restore SA1400 // Access modifier should be declared
#pragma warning restore SA1306 // Field names should begin with lower-case letter
#pragma warning restore SA1310 // Field names should not contain underscore
// We only want to reset TitleBar configuration in app, if we're the TitleBar instance that's managing that state.
private bool _isAutoConfigCompleted;
public TitleBar()
{
this.DefaultStyleKey = typeof(TitleBar);
}
protected override void OnApplyTemplate()
{
PART_LeftPaddingColumn = GetTemplateChild(nameof(PART_LeftPaddingColumn)) as ColumnDefinition;
PART_RightPaddingColumn = GetTemplateChild(nameof(PART_RightPaddingColumn)) as ColumnDefinition;
ConfigureButtonHolder();
Configure();
if (GetTemplateChild(PartBackButton) is Button backButton)
{
backButton.Click -= BackButton_Click;
backButton.Click += BackButton_Click;
}
if (GetTemplateChild(PartPaneButton) is Button paneButton)
{
paneButton.Click -= PaneButton_Click;
paneButton.Click += PaneButton_Click;
}
SizeChanged -= this.TitleBar_SizeChanged;
SizeChanged += this.TitleBar_SizeChanged;
Update();
base.OnApplyTemplate();
}
private void TitleBar_SizeChanged(object sender, SizeChangedEventArgs e)
{
UpdateVisualStateAndDragRegion(e.NewSize);
}
private void UpdateVisualStateAndDragRegion(Size size)
{
if (size.Width <= CompactStateBreakpoint)
{
if (Content != null || Footer != null)
{
VisualStateManager.GoToState(this, NarrowState, true);
}
}
else
{
VisualStateManager.GoToState(this, WideState, true);
}
SetDragRegionForCustomTitleBar();
}
private void BackButton_Click(object sender, RoutedEventArgs e)
{
BackButtonClick?.Invoke(this, new RoutedEventArgs());
}
private void PaneButton_Click(object sender, RoutedEventArgs e)
{
PaneButtonClick?.Invoke(this, new RoutedEventArgs());
}
private void ConfigureButtonHolder()
{
if (PART_ButtonHolder != null)
{
PART_ButtonHolder.SizeChanged -= PART_ButtonHolder_SizeChanged;
}
PART_ButtonHolder = GetTemplateChild(nameof(PART_ButtonHolder)) as StackPanel;
if (PART_ButtonHolder != null)
{
PART_ButtonHolder.SizeChanged += PART_ButtonHolder_SizeChanged;
}
}
private void PART_ButtonHolder_SizeChanged(object sender, SizeChangedEventArgs e)
{
SetDragRegionForCustomTitleBar();
}
private void Configure()
{
SetWASDKTitleBar();
}
public void Reset()
{
ResetWASDKTitleBar();
}
private void Update()
{
if (Icon != null)
{
VisualStateManager.GoToState(this, IconVisibleState, true);
}
else
{
VisualStateManager.GoToState(this, IconCollapsedState, true);
}
VisualStateManager.GoToState(this, IsBackButtonVisible ? BackButtonVisibleState : BackButtonCollapsedState, true);
VisualStateManager.GoToState(this, IsPaneButtonVisible ? PaneButtonVisibleState : PaneButtonCollapsedState, true);
if (DisplayMode == DisplayMode.Tall)
{
VisualStateManager.GoToState(this, TallState, true);
}
else
{
VisualStateManager.GoToState(this, StandardState, true);
}
if (Content != null)
{
VisualStateManager.GoToState(this, ContentVisibleState, true);
}
else
{
VisualStateManager.GoToState(this, ContentCollapsedState, true);
}
if (Footer != null)
{
VisualStateManager.GoToState(this, FooterVisibleState, true);
}
else
{
VisualStateManager.GoToState(this, FooterCollapsedState, true);
}
SetDragRegionForCustomTitleBar();
}
}

View File

@@ -0,0 +1,371 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:animatedvisuals="using:Microsoft.UI.Xaml.Controls.AnimatedVisuals"
xmlns:local="using:Microsoft.PowerToys.Settings.UI.Controls">
<x:Double x:Key="TitleBarCompactHeight">32</x:Double>
<x:Double x:Key="TitleBarTallHeight">48</x:Double>
<x:Double x:Key="TitleBarContentMinWidth">360</x:Double>
<Style BasedOn="{StaticResource DefaultTitleBarStyle}" TargetType="local:TitleBar" />
<Style x:Key="DefaultTitleBarStyle" TargetType="local:TitleBar">
<Setter Property="MinHeight" Value="{ThemeResource TitleBarCompactHeight}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:TitleBar">
<Grid
x:Name="PART_RootGrid"
Height="{TemplateBinding MinHeight}"
Padding="4,0,0,0"
VerticalAlignment="Stretch"
Background="{TemplateBinding Background}">
<Grid.ColumnDefinitions>
<ColumnDefinition x:Name="PART_LeftPaddingColumn" Width="0" />
<ColumnDefinition x:Name="PART_ButtonsHolderColumn" Width="Auto" />
<ColumnDefinition x:Name="PART_IconColumn" Width="Auto" />
<ColumnDefinition x:Name="PART_TitleColumn" Width="Auto" />
<ColumnDefinition
x:Name="PART_LeftDragColumn"
Width="*"
MinWidth="4" />
<ColumnDefinition x:Name="PART_ContentColumn" Width="Auto" />
<ColumnDefinition
x:Name="PART_RightDragColumn"
Width="*"
MinWidth="4" />
<ColumnDefinition x:Name="PART_FooterColumn" Width="Auto" />
<ColumnDefinition x:Name="PART_RightPaddingColumn" Width="0" />
</Grid.ColumnDefinitions>
<Border
x:Name="PART_IconHolder"
Grid.Column="2"
Margin="12,0,0,0"
VerticalAlignment="Center">
<Viewbox
x:Name="PART_Icon"
MaxWidth="16"
MaxHeight="16">
<ContentPresenter
x:Name="PART_IconPresenter"
AutomationProperties.AccessibilityView="Raw"
Content="{TemplateBinding Icon}"
HighContrastAdjustment="None" />
</Viewbox>
</Border>
<StackPanel
x:Name="PART_TitleHolder"
Grid.Column="3"
Margin="16,0,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Orientation="Horizontal"
Spacing="4">
<TextBlock
x:Name="PART_TitleText"
MinWidth="48"
Margin="0,0,0,1"
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{TemplateBinding Title}"
TextTrimming="CharacterEllipsis"
TextWrapping="NoWrap" />
<TextBlock
x:Name="PART_SubtitleText"
MinWidth="48"
Margin="0,0,0,1"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{TemplateBinding Subtitle}"
TextTrimming="CharacterEllipsis"
TextWrapping="NoWrap" />
</StackPanel>
<Grid
x:Name="PART_DragRegion"
Grid.Column="2"
Grid.ColumnSpan="6"
Background="Transparent" />
<StackPanel
x:Name="PART_ButtonHolder"
Grid.Column="1"
Orientation="Horizontal">
<Button
x:Name="PART_BackButton"
Style="{ThemeResource TitleBarBackButtonStyle}"
ToolTipService.ToolTip="Back" />
<Button
x:Name="PART_PaneButton"
Style="{StaticResource TitleBarPaneToggleButtonStyle}"
ToolTipService.ToolTip="Toggle menu" />
</StackPanel>
<ContentPresenter
x:Name="PART_ContentPresenter"
Grid.Column="5"
MinWidth="{ThemeResource TitleBarContentMinWidth}"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
HorizontalContentAlignment="Stretch"
Content="{TemplateBinding Content}" />
<ContentPresenter
x:Name="PART_FooterPresenter"
Grid.Column="7"
Margin="4,0,8,0"
HorizontalContentAlignment="Right"
Content="{TemplateBinding Footer}" />
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="BackButtonStates">
<VisualState x:Name="BackButtonVisible" />
<VisualState x:Name="BackButtonCollapsed">
<VisualState.Setters>
<Setter Target="PART_BackButton.Visibility" Value="Collapsed" />
<Setter Target="PART_BackButton.Visibility" Value="Collapsed" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="PaneButtonStates">
<VisualState x:Name="PaneButtonVisible" />
<VisualState x:Name="PaneButtonCollapsed">
<VisualState.Setters>
<Setter Target="PART_PaneButton.Visibility" Value="Collapsed" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="IconStates">
<VisualState x:Name="IconVisible" />
<VisualState x:Name="IconCollapsed">
<VisualState.Setters>
<Setter Target="PART_IconHolder.Visibility" Value="Collapsed" />
<Setter Target="PART_TitleHolder.Margin" Value="4,0,0,0" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="ContentStates">
<VisualState x:Name="ContentVisible" />
<VisualState x:Name="ContentCollapsed">
<VisualState.Setters>
<Setter Target="PART_ContentPresenter.Visibility" Value="Collapsed" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="FooterStates">
<VisualState x:Name="FooterVisible" />
<VisualState x:Name="FooterCollapsed">
<VisualState.Setters>
<Setter Target="PART_FooterPresenter.Visibility" Value="Collapsed" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="ReflowStates">
<VisualState x:Name="Wide" />
<VisualState x:Name="Narrow">
<VisualState.Setters>
<Setter Target="PART_TitleHolder.Visibility" Value="Collapsed" />
<Setter Target="PART_LeftDragColumn.Width" Value="Auto" />
<Setter Target="PART_RightDragColumn.Width" Value="Auto" />
<Setter Target="PART_RightDragColumn.MinWidth" Value="16" />
<Setter Target="PART_LeftDragColumn.MinWidth" Value="16" />
<Setter Target="PART_ContentColumn.Width" Value="*" />
<!-- Content can stretch now -->
<Setter Target="PART_ContentPresenter.MinWidth" Value="0" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="WindowActivationStates">
<VisualState x:Name="Activated" />
<VisualState x:Name="Deactivated">
<VisualState.Setters>
<Setter Target="PART_TitleText.Foreground" Value="{ThemeResource TextFillColorDisabledBrush}" />
<Setter Target="PART_SubtitleText.Foreground" Value="{ThemeResource TextFillColorDisabledBrush}" />
<Setter Target="PART_BackButton.IsEnabled" Value="False" />
<Setter Target="PART_PaneButton.IsEnabled" Value="False" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="DisplayModeStates">
<VisualState x:Name="Standard" />
<VisualState x:Name="Tall">
<VisualState.Setters>
<Setter Target="PART_RootGrid.MinHeight" Value="{ThemeResource TitleBarTallHeight}" />
<Setter Target="PART_RootGrid.Padding" Value="4" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- Copy of WinUI NavigationBackButtonNormalStyle - cannot use it as it picks up the generic.xaml version, not the WinUI version -->
<Style x:Key="TitleBarBackButtonStyle" TargetType="Button">
<Setter Property="Background" Value="{ThemeResource NavigationViewBackButtonBackground}" />
<Setter Property="Foreground" Value="{ThemeResource NavigationViewItemForegroundChecked}" />
<Setter Property="FontFamily" Value="{ThemeResource SymbolThemeFontFamily}" />
<Setter Property="FontSize" Value="16" />
<Setter Property="MaxHeight" Value="40" />
<Setter Property="VerticalAlignment" Value="Stretch" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="UseSystemFocusVisuals" Value="{StaticResource UseSystemFocusVisuals}" />
<Setter Property="Content" Value="&#xE72B;" />
<Setter Property="Padding" Value="12,4,12,4" />
<Setter Property="CornerRadius" Value="{ThemeResource ControlCornerRadius}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Grid
x:Name="RootGrid"
Padding="{TemplateBinding Padding}"
Background="{TemplateBinding Background}"
CornerRadius="{TemplateBinding CornerRadius}">
<AnimatedIcon
x:Name="Content"
Width="16"
Height="16"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
AnimatedIcon.State="Normal"
AutomationProperties.AccessibilityView="Raw">
<animatedvisuals:AnimatedBackVisualSource />
<AnimatedIcon.FallbackIconSource>
<FontIconSource
FontFamily="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=FontFamily}"
FontSize="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=FontSize}"
Glyph="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Content}"
MirroredWhenRightToLeft="True" />
</AnimatedIcon.FallbackIconSource>
</AnimatedIcon>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="PointerOver">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="RootGrid" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource NavigationViewButtonBackgroundPointerOver}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Content" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource NavigationViewButtonForegroundPointerOver}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
<VisualState.Setters>
<Setter Target="Content.(AnimatedIcon.State)" Value="PointerOver" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Pressed">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="RootGrid" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource NavigationViewButtonBackgroundPressed}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Content" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource NavigationViewButtonForegroundPressed}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
<VisualState.Setters>
<Setter Target="Content.(AnimatedIcon.State)" Value="Pressed" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Disabled">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Content" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource NavigationViewButtonForegroundDisabled}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- Copy of WinUI PaneToggleButtonStyle - cannot use it as it picks up the generic.xaml version, not the WinUI version -->
<Style x:Key="TitleBarPaneToggleButtonStyle" TargetType="Button">
<Setter Property="Background" Value="{ThemeResource NavigationViewBackButtonBackground}" />
<Setter Property="Foreground" Value="{ThemeResource NavigationViewItemForegroundChecked}" />
<Setter Property="FontFamily" Value="{ThemeResource SymbolThemeFontFamily}" />
<Setter Property="FontSize" Value="16" />
<Setter Property="MaxHeight" Value="40" />
<Setter Property="VerticalAlignment" Value="Stretch" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="UseSystemFocusVisuals" Value="{StaticResource UseSystemFocusVisuals}" />
<Setter Property="Content" Value="&#xE700;" />
<Setter Property="Padding" Value="12,4,12,4" />
<Setter Property="CornerRadius" Value="{ThemeResource ControlCornerRadius}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Grid
x:Name="RootGrid"
Padding="{TemplateBinding Padding}"
Background="{TemplateBinding Background}"
CornerRadius="{TemplateBinding CornerRadius}">
<AnimatedIcon
x:Name="Content"
Width="16"
Height="16"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
AnimatedIcon.State="Normal"
AutomationProperties.AccessibilityView="Raw">
<animatedvisuals:AnimatedGlobalNavigationButtonVisualSource />
<AnimatedIcon.FallbackIconSource>
<FontIconSource
FontFamily="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=FontFamily}"
FontSize="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=FontSize}"
Glyph="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Content}"
MirroredWhenRightToLeft="True" />
</AnimatedIcon.FallbackIconSource>
</AnimatedIcon>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="PointerOver">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="RootGrid" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource NavigationViewButtonBackgroundPointerOver}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Content" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource NavigationViewButtonForegroundPointerOver}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
<VisualState.Setters>
<Setter Target="Content.(AnimatedIcon.State)" Value="PointerOver" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Pressed">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="RootGrid" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource NavigationViewButtonBackgroundPressed}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Content" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource NavigationViewButtonForegroundPressed}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
<VisualState.Setters>
<Setter Target="Content.(AnimatedIcon.State)" Value="Pressed" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Disabled">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Content" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource NavigationViewButtonForegroundDisabled}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View File

@@ -24,6 +24,7 @@
<controls:SettingsGroup x:Uid="MouseUtils_MouseJump">
<tkcontrols:SettingsCard
x:Name="MouseUtilsEnableMouseJump"
x:Uid="MouseUtils_Enable_MouseJump"
HeaderIcon="{ui:BitmapIcon Source=/Assets/Settings/Icons/MouseJump.png}"
IsEnabled="{x:Bind ViewModel.IsJumpEnabledGpoConfigured, Mode=OneWay, Converter={StaticResource BoolNegationConverter}}">
@@ -42,6 +43,7 @@
</InfoBar>
<tkcontrols:SettingsCard
x:Name="MouseUtilsMouseJumpActivationShortcut"
x:Uid="MouseUtils_MouseJump_ActivationShortcut"
HeaderIcon="{ui:FontIcon Glyph=&#xEDA7;}"
IsEnabled="{x:Bind ViewModel.IsMouseJumpEnabled, Mode=OneWay}">
@@ -126,6 +128,7 @@
</tkcontrols:SettingsCard>
<tkcontrols:SettingsExpander
x:Name="MouseUtilsMouseJumpAppearance"
x:Uid="MouseUtils_MouseJump_Appearance"
HeaderIcon="{ui:FontIcon Glyph=&#xEB3C;}"
IsEnabled="{x:Bind ViewModel.IsMouseJumpEnabled, Mode=OneWay}"

View File

@@ -70,19 +70,19 @@
x:Uid="FancyZones_ZoneBehavior_GroupSettings"
IsExpanded="True">
<tkcontrols:SettingsExpander.Items>
<tkcontrols:SettingsCard ContentAlignment="Left">
<tkcontrols:SettingsCard Name="FancyZonesShiftDragCheckBoxControlHeader" ContentAlignment="Left">
<CheckBox x:Uid="FancyZones_ShiftDragCheckBoxControl_Header" IsChecked="{x:Bind ViewModel.ShiftDrag, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard ContentAlignment="Left">
<tkcontrols:SettingsCard Name="FancyZonesMouseDragCheckBoxControlHeader" ContentAlignment="Left">
<CheckBox x:Uid="FancyZones_MouseDragCheckBoxControl_Header" IsChecked="{x:Bind ViewModel.MouseSwitch, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard ContentAlignment="Left">
<tkcontrols:SettingsCard Name="FancyZonesMouseMiddleClickSpanningMultipleZonesCheckBoxControlHeader" ContentAlignment="Left">
<CheckBox x:Uid="FancyZones_MouseMiddleClickSpanningMultipleZonesCheckBoxControl_Header" IsChecked="{x:Bind ViewModel.MouseMiddleClickSpanningMultipleZones, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard ContentAlignment="Left">
<tkcontrols:SettingsCard Name="FancyZonesShowZonesOnAllMonitorsCheckBoxControl" ContentAlignment="Left">
<CheckBox x:Uid="FancyZones_ShowZonesOnAllMonitorsCheckBoxControl" IsChecked="{x:Bind ViewModel.ShowOnAllMonitors, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard ContentAlignment="Left">
<tkcontrols:SettingsCard Name="FancyZonesSpanZonesAcrossMonitors" ContentAlignment="Left">
<controls:CheckBoxWithDescriptionControl x:Uid="FancyZones_SpanZonesAcrossMonitors" IsChecked="{x:Bind ViewModel.SpanZonesAcrossMonitors, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard Name="FancyZonesOverlappingZones" x:Uid="FancyZones_OverlappingZones">
@@ -106,7 +106,7 @@
<ComboBoxItem x:Uid="FancyZones_Radio_Default_Theme" />
</ComboBox>
<tkcontrols:SettingsExpander.Items>
<tkcontrols:SettingsCard ContentAlignment="Left">
<tkcontrols:SettingsCard Name="FancyZonesPreviewCard" ContentAlignment="Left">
<controls:FancyZonesPreviewControl
Width="192"
Height="108"
@@ -118,7 +118,7 @@
IsSystemTheme="{x:Bind ViewModel.SystemTheme, Mode=OneWay}"
ShowZoneNumber="{x:Bind Path=ViewModel.ShowZoneNumber, Mode=OneWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard ContentAlignment="Left">
<tkcontrols:SettingsCard Name="FancyZonesShowZoneNumberCheckBoxControl" ContentAlignment="Left">
<CheckBox x:Uid="FancyZones_ShowZoneNumberCheckBoxControl" IsChecked="{x:Bind ViewModel.ShowZoneNumber, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard Name="FancyZonesHighlightOpacity" x:Uid="FancyZones_HighlightOpacity">
@@ -164,28 +164,31 @@
x:Uid="FancyZones_WindowBehavior_GroupSettings"
IsExpanded="True">
<tkcontrols:SettingsExpander.Items>
<tkcontrols:SettingsCard ContentAlignment="Left">
<tkcontrols:SettingsCard Name="FancyZonesDisplayOrWorkAreaChangeMoveWindowsCheckBoxControl" ContentAlignment="Left">
<CheckBox x:Uid="FancyZones_DisplayOrWorkAreaChangeMoveWindowsCheckBoxControl" IsChecked="{x:Bind ViewModel.DisplayOrWorkAreaChangeMoveWindows, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard ContentAlignment="Left">
<tkcontrols:SettingsCard Name="FancyZonesZoneSetChangeMoveWindows" ContentAlignment="Left">
<CheckBox x:Uid="FancyZones_ZoneSetChangeMoveWindows" IsChecked="{x:Bind ViewModel.ZoneSetChangeMoveWindows, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard ContentAlignment="Left">
<tkcontrols:SettingsCard Name="FancyZonesAppLastZoneMoveWindows" ContentAlignment="Left">
<CheckBox x:Uid="FancyZones_AppLastZoneMoveWindows" IsChecked="{x:Bind ViewModel.AppLastZoneMoveWindows, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard ContentAlignment="Left">
<tkcontrols:SettingsCard Name="FancyZonesOpenWindowOnActiveMonitor" ContentAlignment="Left">
<CheckBox x:Uid="FancyZones_OpenWindowOnActiveMonitor" IsChecked="{x:Bind ViewModel.OpenWindowOnActiveMonitor, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard ContentAlignment="Left">
<tkcontrols:SettingsCard Name="FancyZonesRestoreSize" ContentAlignment="Left">
<CheckBox x:Uid="FancyZones_RestoreSize" IsChecked="{x:Bind ViewModel.RestoreSize, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard ContentAlignment="Left">
<tkcontrols:SettingsCard Name="FancyZonesMakeDraggedWindowTransparentCheckBoxControl" ContentAlignment="Left">
<CheckBox x:Uid="FancyZones_MakeDraggedWindowTransparentCheckBoxControl" IsChecked="{x:Bind ViewModel.MakeDraggedWindowsTransparent, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard ContentAlignment="Left">
<tkcontrols:SettingsCard Name="FancyZonesAllowChildWindowSnap" ContentAlignment="Left">
<CheckBox x:Uid="FancyZones_AllowChildWindowSnap" IsChecked="{x:Bind ViewModel.AllowChildWindowSnap, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard ContentAlignment="Left" Visibility="{x:Bind ViewModel.Windows11, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
<tkcontrols:SettingsCard
Name="FancyZonesDisableRoundCornersOnWindowSnap"
ContentAlignment="Left"
Visibility="{x:Bind ViewModel.Windows11, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
<CheckBox x:Uid="FancyZones_DisableRoundCornersOnWindowSnap" IsChecked="{x:Bind ViewModel.DisableRoundCornersOnWindowSnap, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
</tkcontrols:SettingsExpander.Items>
@@ -199,7 +202,7 @@
<ToggleSwitch x:Uid="ToggleSwitch" IsOn="{x:Bind ViewModel.WindowSwitching, Mode=TwoWay}" />
<tkcontrols:SettingsExpander.Items>
<!-- HACK: For some weird reason, a Shortcut Control is not working correctly if it's the first item in the expander, so we add an invisible card as the first one. -->
<tkcontrols:SettingsCard Visibility="Collapsed" />
<tkcontrols:SettingsCard Name="FancyZonesWindowSwitchingPlaceholder" Visibility="Collapsed" />
<tkcontrols:SettingsCard
Name="FancyZonesHotkeyNextTabControl"
x:Uid="FancyZones_HotkeyNextTabControl"
@@ -248,7 +251,10 @@
</ComboBoxItem>
</ComboBox>
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard ContentAlignment="Left" IsEnabled="{x:Bind ViewModel.SnapHotkeysCategoryEnabled, Mode=OneWay}">
<tkcontrols:SettingsCard
Name="FancyZonesMoveWindowsAcrossAllMonitorsCheckBoxControl"
ContentAlignment="Left"
IsEnabled="{x:Bind ViewModel.SnapHotkeysCategoryEnabled, Mode=OneWay}">
<CheckBox x:Uid="FancyZones_MoveWindowsAcrossAllMonitorsCheckBoxControl" IsChecked="{x:Bind ViewModel.MoveWindowsAcrossMonitors, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
</tkcontrols:SettingsExpander.Items>
@@ -262,7 +268,10 @@
HeaderIcon="{ui:FontIcon Glyph=&#xEDA7;}">
<ToggleSwitch x:Uid="ToggleSwitch" IsOn="{x:Bind ViewModel.QuickLayoutSwitch, Mode=TwoWay}" />
<tkcontrols:SettingsExpander.Items>
<tkcontrols:SettingsCard ContentAlignment="Left" IsEnabled="{x:Bind ViewModel.QuickSwitchEnabled, Mode=OneWay}">
<tkcontrols:SettingsCard
Name="FancyZonesFlashZonesOnQuickSwitch"
ContentAlignment="Left"
IsEnabled="{x:Bind ViewModel.QuickSwitchEnabled, Mode=OneWay}">
<CheckBox x:Uid="FancyZones_FlashZonesOnQuickSwitch" IsChecked="{x:Bind ViewModel.FlashZonesOnQuickSwitch, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
</tkcontrols:SettingsExpander.Items>
@@ -277,7 +286,10 @@
HeaderIcon="{ui:FontIcon Glyph=&#xECE4;}"
IsExpanded="True">
<tkcontrols:SettingsExpander.Items>
<tkcontrols:SettingsCard HorizontalContentAlignment="Stretch" ContentAlignment="Vertical">
<tkcontrols:SettingsCard
Name="FancyZonesExcludeAppsTextBoxControl"
HorizontalContentAlignment="Stretch"
ContentAlignment="Vertical">
<TextBox
x:Uid="FancyZones_ExcludeApps_TextBoxControl"
MinWidth="240"

View File

@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using CommunityToolkit.WinUI.Controls;
using Microsoft.PowerToys.Settings.UI.Helpers;
using Microsoft.PowerToys.Settings.UI.Services;
using Microsoft.PowerToys.Settings.UI.ViewModels;
@@ -32,7 +33,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
if (e.Parameter is SearchResultsNavigationParams searchParams)
{
ViewModel.SetSearchResults(searchParams.Query, searchParams.Results);
PageControl.ModuleDescription = string.Empty;
PageControl.ModuleDescription = $"{ResourceLoaderInstance.ResourceLoader.GetString("Search_ResultsFor")} '{searchParams.Query}'";
}
}
@@ -43,7 +44,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
private void ModuleButton_Click(object sender, RoutedEventArgs e)
{
if (sender is CommunityToolkit.WinUI.Controls.SettingsCard card && card.DataContext is SettingEntry tagEntry)
if (sender is SettingsCard card && card.DataContext is SettingEntry tagEntry)
{
NavigateToModule(tagEntry);
}
@@ -51,7 +52,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
private void SettingButton_Click(object sender, RoutedEventArgs e)
{
if (sender is CommunityToolkit.WinUI.Controls.SettingsCard card && card.DataContext is SettingEntry tagEntry)
if (sender is SettingsCard card && card.DataContext is SettingEntry tagEntry)
{
NavigateToSetting(tagEntry);
}

View File

@@ -4,6 +4,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:animatedVisuals="using:Microsoft.UI.Xaml.Controls.AnimatedVisuals"
xmlns:animations="using:CommunityToolkit.WinUI.Animations"
xmlns:controls="using:Microsoft.PowerToys.Settings.UI.Controls"
xmlns:converters="using:Microsoft.PowerToys.Settings.UI.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:helpers="using:Microsoft.PowerToys.Settings.UI.Helpers"
@@ -11,7 +12,6 @@
xmlns:ic="using:Microsoft.Xaml.Interactions.Core"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:models="using:Microsoft.PowerToys.Settings.UI.ViewModels"
xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls"
xmlns:tkconverters="using:CommunityToolkit.WinUI.Converters"
xmlns:ui="using:CommunityToolkit.WinUI"
xmlns:views="using:Microsoft.PowerToys.Settings.UI.Views"
@@ -57,12 +57,6 @@
</DataTemplate>
<DataTemplate x:Key="NoResultSearchResultTemplate" x:DataType="models:SuggestionItem">
<Grid>
<Rectangle
Height="1"
Margin="0,-4,0,0"
HorizontalAlignment="Stretch"
VerticalAlignment="Top"
Fill="{ThemeResource DividerStrokeColorDefaultBrush}" />
<TextBlock
Margin="8"
HorizontalAlignment="Center"
@@ -71,10 +65,10 @@
</Grid>
</DataTemplate>
<DataTemplate x:Key="ShowAllSearchResultTemplate" x:DataType="models:SuggestionItem">
<Grid Padding="16,8">
<Grid>
<Rectangle
Height="1"
Margin="0,-4,0,0"
Margin="0,-12,0,0"
HorizontalAlignment="Stretch"
VerticalAlignment="Top"
Fill="{ThemeResource DividerStrokeColorDefaultBrush}" />
@@ -92,23 +86,22 @@
<ic:InvokeCommandAction Command="{x:Bind ViewModel.LoadedCommand}" />
</ic:EventTriggerBehavior>
</i:Interaction.Behaviors>
<Grid x:Name="RootGrid">
<Grid.RowDefinitions>
<RowDefinition Height="48" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<tkcontrols:TitleBar
<controls:TitleBar
x:Name="AppTitleBar"
AutoConfigureCustomTitleBar="True"
PaneButtonClick="PaneToggleBtn_Click">
<tkcontrols:TitleBar.Resources>
<controls:TitleBar.Resources>
<x:Double x:Key="TitleBarContentMinWidth">516</x:Double>
</tkcontrols:TitleBar.Resources>
<tkcontrols:TitleBar.Icon>
</controls:TitleBar.Resources>
<controls:TitleBar.Icon>
<BitmapIcon ShowAsMonochrome="False" UriSource="/Assets/Settings/icon.ico" />
</tkcontrols:TitleBar.Icon>
<tkcontrols:TitleBar.Content>
</controls:TitleBar.Icon>
<controls:TitleBar.Content>
<AutoSuggestBox
x:Name="SearchBox"
x:Uid="Shell_SearchBox"
@@ -129,8 +122,8 @@
Modifiers="Control" />
</AutoSuggestBox.KeyboardAccelerators>
</AutoSuggestBox>
</tkcontrols:TitleBar.Content>
</tkcontrols:TitleBar>
</controls:TitleBar.Content>
</controls:TitleBar>
<NavigationView
x:Name="navigationView"
Grid.Row="1"

View File

@@ -4,13 +4,13 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Common.Search;
using Common.Search.FuzzSearch;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Controls;
using Microsoft.PowerToys.Settings.UI.Helpers;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.Services;
@@ -132,7 +132,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
public static bool IsUserAnAdmin { get; set; }
public CommunityToolkit.WinUI.Controls.TitleBar TitleBar => AppTitleBar;
public Controls.TitleBar TitleBar => AppTitleBar;
private Dictionary<Type, NavigationViewItem> _navViewParentLookup = new Dictionary<Type, NavigationViewItem>();
private List<string> _searchSuggestions = [];
@@ -141,8 +141,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
private const int SearchDebounceMs = 500;
private bool _disposed;
// Tracing id for correlating logs of a single search interaction
private static long _searchTraceIdCounter;
// Removed trace id counter per cleanup
/// <summary>
/// Initializes a new instance of the <see cref="ShellPage"/> class.
@@ -442,25 +441,11 @@ namespace Microsoft.PowerToys.Settings.UI.Views
private void ShellPage_Loaded(object sender, RoutedEventArgs e)
{
Logger.LogDebug("[Search][Index] Scheduling BuildIndex...");
var swIndex = Stopwatch.StartNew();
Task.Run(() =>
{
Logger.LogDebug("[Search][Index] BuildIndex started");
SearchIndexService.BuildIndex();
})
.ContinueWith(t =>
{
swIndex.Stop();
if (t.IsFaulted)
{
Logger.LogDebug($"[Search][Index] BuildIndex FAILED after {swIndex.ElapsedMilliseconds} ms: {t.Exception?.Flatten().InnerException?.Message}");
}
else
{
Logger.LogDebug($"[Search][Index] BuildIndex completed in {swIndex.ElapsedMilliseconds} ms.");
}
});
.ContinueWith(_ => { });
}
private void NavigationView_DisplayModeChanged(NavigationView sender, NavigationViewDisplayModeChangedEventArgs args)
@@ -511,10 +496,6 @@ namespace Microsoft.PowerToys.Settings.UI.Views
var query = sender.Text?.Trim() ?? string.Empty;
var traceId = Interlocked.Increment(ref _searchTraceIdCounter);
var swOverall = Stopwatch.StartNew();
Logger.LogDebug($"[Search][TextChanged][{traceId}] start. query='{query}'");
// Debounce: cancel previous pending search
_searchDebounceCts?.Cancel();
_searchDebounceCts?.Dispose();
@@ -527,7 +508,6 @@ namespace Microsoft.PowerToys.Settings.UI.Views
sender.IsSuggestionListOpen = false;
_lastSearchResults.Clear();
_lastQueryText = string.Empty;
Logger.LogDebug($"[Search][TextChanged][{traceId}] empty query. end");
return;
}
@@ -537,14 +517,11 @@ namespace Microsoft.PowerToys.Settings.UI.Views
}
catch (TaskCanceledException)
{
// A newer keystroke arrived; abandon this run
Logger.LogDebug($"[Search][TextChanged][{traceId}] debounce canceled at +{swOverall.ElapsedMilliseconds} ms");
return;
return; // debounce canceled
}
if (token.IsCancellationRequested)
{
Logger.LogDebug($"[Search][TextChanged][{traceId}] token canceled post-debounce at +{swOverall.ElapsedMilliseconds} ms");
return;
}
@@ -553,106 +530,25 @@ namespace Microsoft.PowerToys.Settings.UI.Views
try
{
// If the token is already canceled before scheduling, the task won't start.
var swSearch = Stopwatch.StartNew();
Logger.LogDebug($"[Search][TextChanged][{traceId}] dispatch search...");
results = await Task.Run(() => SearchIndexService.Search(query, token), token);
swSearch.Stop();
Logger.LogDebug($"[Search][TextChanged][{traceId}] search done in {swSearch.ElapsedMilliseconds} ms. results={results?.Count ?? 0}");
}
catch (OperationCanceledException)
{
Logger.LogDebug($"[Search][TextChanged][{traceId}] search canceled at +{swOverall.ElapsedMilliseconds} ms");
return;
}
if (token.IsCancellationRequested)
{
Logger.LogDebug($"[Search][TextChanged][{traceId}] token canceled after search at +{swOverall.ElapsedMilliseconds} ms");
return;
}
_lastSearchResults = results;
_lastQueryText = query;
List<SuggestionItem> top;
if (results.Count == 0)
{
// Explicit no-results row
var rl = ResourceLoaderInstance.ResourceLoader;
var noResultsPrefix = rl.GetString("Shell_Search_NoResults");
if (string.IsNullOrEmpty(noResultsPrefix))
{
noResultsPrefix = "No results for";
}
var top = BuildSuggestionItems(query, results);
var headerText = $"{noResultsPrefix} '{query}'";
top =
[
new()
{
Header = headerText,
IsNoResults = true,
},
];
Logger.LogDebug($"[Search][TextChanged][{traceId}] no results -> added placeholder item (count={top.Count})");
}
else
{
// Project top 5 suggestions
var swProject = Stopwatch.StartNew();
top = [.. results.Take(5)
.Select(e =>
{
string subtitle = string.Empty;
if (e.Type != EntryType.SettingsPage)
{
var swSubtitle = Stopwatch.StartNew();
subtitle = SearchIndexService.GetLocalizedPageName(e.PageTypeName);
if (string.IsNullOrEmpty(subtitle))
{
// Fallback: look up the module title from the in-memory index
var swFallback = Stopwatch.StartNew();
subtitle = SearchIndexService.Index
.Where(x => x.Type == EntryType.SettingsPage && x.PageTypeName == e.PageTypeName)
.Select(x => x.Header)
.FirstOrDefault() ?? string.Empty;
swFallback.Stop();
Logger.LogDebug($"[Search][TextChanged][{traceId}] fallback subtitle for '{e.PageTypeName}' took {swFallback.ElapsedMilliseconds} ms");
}
swSubtitle.Stop();
Logger.LogDebug($"[Search][TextChanged][{traceId}] subtitle for '{e.PageTypeName}' took {swSubtitle.ElapsedMilliseconds} ms");
}
return new SuggestionItem
{
Header = e.Header,
Icon = e.Icon,
PageTypeName = e.PageTypeName,
ElementName = e.ElementName,
ParentElementName = e.ParentElementName,
Subtitle = subtitle,
IsShowAll = false,
};
})];
swProject.Stop();
Logger.LogDebug($"[Search][TextChanged][{traceId}] project suggestions took {swProject.ElapsedMilliseconds} ms. topCount={top.Count}");
if (results.Count > 5)
{
// Add a tail item to show all results if there are more than 5
top.Add(new SuggestionItem { IsShowAll = true });
Logger.LogDebug($"[Search][TextChanged][{traceId}] added 'Show all results' item");
}
}
var swUi = Stopwatch.StartNew();
sender.ItemsSource = top;
sender.IsSuggestionListOpen = top.Count > 0;
swUi.Stop();
swOverall.Stop();
Logger.LogDebug($"[Search][TextChanged][{traceId}] UI update took {swUi.ElapsedMilliseconds} ms. total={swOverall.ElapsedMilliseconds} ms");
}
private void SearchBox_SuggestionChosen(AutoSuggestBox sender, AutoSuggestBoxSuggestionChosenEventArgs args)
@@ -709,23 +605,98 @@ namespace Microsoft.PowerToys.Settings.UI.Views
private void CtrlF_Invoked(KeyboardAccelerator sender, KeyboardAcceleratorInvokedEventArgs args)
{
SearchBox.Focus(FocusState.Programmatic);
args.Handled = true; // prevent further processing (e.g., unintended navigation)
}
private void SearchBox_GotFocus(object sender, RoutedEventArgs e)
{
// do not prompt unless search for text.
return;
var box = sender as AutoSuggestBox;
var current = box?.Text?.Trim() ?? string.Empty;
if (string.IsNullOrEmpty(current))
{
return; // nothing to restore
}
// If current text matches last query and we have results, reconstruct the suggestion list.
if (string.Equals(current, _lastQueryText, StringComparison.Ordinal) && _lastSearchResults?.Count > 0)
{
try
{
var top = BuildSuggestionItems(current, _lastSearchResults);
box.ItemsSource = top;
box.IsSuggestionListOpen = top.Count > 0;
}
catch (Exception ex)
{
Logger.LogError($"Error restoring suggestion list {ex.Message}");
}
}
}
// Centralized suggestion projection logic used by TextChanged & GotFocus restore.
private List<SuggestionItem> BuildSuggestionItems(string query, List<SettingEntry> results)
{
results ??= new();
if (results.Count == 0)
{
var rl = ResourceLoaderInstance.ResourceLoader;
var noResultsPrefix = rl.GetString("Shell_Search_NoResults");
if (string.IsNullOrEmpty(noResultsPrefix))
{
noResultsPrefix = "No results for";
}
var headerText = $"{noResultsPrefix} '{query}'";
return new List<SuggestionItem>
{
new()
{
Header = headerText,
IsNoResults = true,
},
};
}
var list = results.Take(5).Select(e =>
{
string subtitle = string.Empty;
if (e.Type != EntryType.SettingsPage)
{
subtitle = SearchIndexService.GetLocalizedPageName(e.PageTypeName);
if (string.IsNullOrEmpty(subtitle))
{
subtitle = SearchIndexService.Index
.Where(x => x.Type == EntryType.SettingsPage && x.PageTypeName == e.PageTypeName)
.Select(x => x.Header)
.FirstOrDefault() ?? string.Empty;
}
}
return new SuggestionItem
{
Header = e.Header,
Icon = e.Icon,
PageTypeName = e.PageTypeName,
ElementName = e.ElementName,
ParentElementName = e.ParentElementName,
Subtitle = subtitle,
IsShowAll = false,
};
}).ToList();
if (results.Count > 5)
{
list.Add(new SuggestionItem { IsShowAll = true });
}
return list;
}
private async void SearchBox_QuerySubmitted(AutoSuggestBox sender, AutoSuggestBoxQuerySubmittedEventArgs args)
{
var swSubmit = Stopwatch.StartNew();
Logger.LogDebug("[Search][Submit] start");
// If a suggestion is selected, navigate directly
if (args.ChosenSuggestion is SuggestionItem chosen)
{
Logger.LogDebug($"[Search][Submit] chosen suggestion -> navigate to {chosen.PageTypeName} element={chosen.ElementName ?? "<page>"}");
NavigateFromSuggestion(chosen);
return;
}
@@ -733,7 +704,6 @@ namespace Microsoft.PowerToys.Settings.UI.Views
var queryText = (args.QueryText ?? _lastQueryText)?.Trim();
if (string.IsNullOrWhiteSpace(queryText))
{
Logger.LogDebug("[Search][Submit] empty query -> navigate Dashboard");
NavigationService.Navigate<DashboardPage>();
return;
}
@@ -741,21 +711,10 @@ namespace Microsoft.PowerToys.Settings.UI.Views
// Prefer cached results (from live search); if empty, perform a fresh search
var matched = _lastSearchResults?.Count > 0 && string.Equals(_lastQueryText, queryText, StringComparison.Ordinal)
? _lastSearchResults
: await Task.Run(() =>
{
var sw = Stopwatch.StartNew();
Logger.LogDebug($"[Search][Submit] background search for '{queryText}'...");
var r = SearchIndexService.Search(queryText);
sw.Stop();
Logger.LogDebug($"[Search][Submit] background search done in {sw.ElapsedMilliseconds} ms. results={r?.Count ?? 0}");
return r;
});
: await Task.Run(() => SearchIndexService.Search(queryText));
var searchParams = new SearchResultsNavigationParams(queryText, matched);
Logger.LogDebug($"[Search][Submit] navigate to SearchResultsPage (results={matched?.Count ?? 0})");
NavigationService.Navigate<SearchResultsPage>(searchParams);
swSubmit.Stop();
Logger.LogDebug($"[Search][Submit] total {swSubmit.ElapsedMilliseconds} ms");
}
public void Dispose()

View File

@@ -2691,6 +2691,12 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
<value>Shortcuts with **Ctrl** and **Alt** may remove functionality from some international keyboards, because **Ctrl** + **Alt** = **Alt Gr** in those keyboards.</value>
<comment>The ** sequences are used for text formatting of the key names. Don't remove them on translation.</comment>
</data>
<data name="WarningShortcutConflict.Title" xml:space="preserve">
<value>Shortcut conflict</value>
</data>
<data name="WarningShortcutConflict.ToolTipService.ToolTip" xml:space="preserve">
<value>A conflict has been detected for this shortcut.</value>
</data>
<data name="FancyZones_SpanZonesAcrossMonitors.Description" xml:space="preserve">
<value>All monitors must have the same DPI scaling and will be treated as one large combined rectangle which contains all monitors</value>
</data>
@@ -2896,20 +2902,19 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
<data name="MouseUtils_GlidingCursor.Description" xml:space="preserve">
<value>An accessibility feature that lets you control the mouse with a single button using guided horizontal and vertical lines</value>
</data>
<data name="MouseUtils_GlidingCursor_InitialSpeed.Header" xml:space="preserve">
<data name="MouseUtils_GlidingCursor_InitialSpeed.Header" xml:space="preserve">
<value>Initial line speed</value>
</data>
<data name="MouseUtils_GlidingCursor_InitialSpeed.Description" xml:space="preserve">
<data name="MouseUtils_GlidingCursor_InitialSpeed.Description" xml:space="preserve">
<value>Speed of the horizontal or vertical line when it begins moving</value>
</data>
<data name="MouseUtils_GlidingCursor_DelaySpeed.Header" xml:space="preserve">
<data name="MouseUtils_GlidingCursor_DelaySpeed.Header" xml:space="preserve">
<value>Reduced line speed</value>
</data>
<data name="MouseUtils_GlidingCursor_DelaySpeed.Description" xml:space="preserve">
<data name="MouseUtils_GlidingCursor_DelaySpeed.Description" xml:space="preserve">
<value>Speed after slowing down the line with a second shortcut press</value>
</data>
<data name="FancyZones_Radio_Custom_Colors.Content" xml:space="preserve">
<data name="FancyZones_Radio_Custom_Colors.Content" xml:space="preserve">
<value>Custom colors</value>
</data>
<data name="FancyZones_Radio_Default_Theme.Content" xml:space="preserve">
@@ -5266,7 +5271,7 @@ To record a specific window, enter the hotkey with the Alt key in the opposite m
<value>All shortcuts function correctly</value>
</data>
<data name="ResolveConflicts_Button.Content" xml:space="preserve">
<value>Resolve conflicts</value>
<value>Resolve conflicts</value>
</data>
<data name="ShortcutConflictControl_Title.Text" xml:space="preserve">
<value>Shortcut conflicts</value>
@@ -5287,4 +5292,8 @@ To record a specific window, enter the hotkey with the Alt key in the opposite m
<data name="Hosts_NoLeadingSpaces.Description" xml:space="preserve">
<value>Do not prepend spaces to active lines when saving the hosts file</value>
</data>
<data name="Search_ResultsFor" xml:space="preserve">
<value>Results for</value>
<comment>Prefix for search string. E.g. "Results for 'shortcut'"</comment>
</data>
</root>

View File

@@ -126,14 +126,21 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
{
var assembly = Assembly.GetExecutingAssembly();
var assemblyName = new AssemblyName(assembly.FullName ?? throw new InvalidOperationException());
// Build the fully-qualified manifest resource name. Historically, subtle casing differences
// (e.g. folder names or the assembly name) caused exact (case-sensitive) lookup failures on
// some developer machines when the embedded resource's actual name differed only by case.
// Manifest resource name comparison here does not need to be case-sensitive, so we resolve
// the actual name using an OrdinalIgnoreCase match, then use the real casing for the stream.
var resourceName = $"Microsoft.{assemblyName.Name}.{filename.Replace("/", ".")}";
var resourceNames = assembly.GetManifestResourceNames();
if (!resourceNames.Contains(resourceName))
var actualResourceName = resourceNames.FirstOrDefault(n => string.Equals(n, resourceName, StringComparison.OrdinalIgnoreCase));
if (actualResourceName is null)
{
throw new InvalidOperationException($"Embedded resource '{resourceName}' does not exist.");
throw new InvalidOperationException($"Embedded resource '{resourceName}' (case-insensitive) does not exist.");
}
var stream = assembly.GetManifestResourceStream(resourceName)
var stream = assembly.GetManifestResourceStream(actualResourceName)
?? throw new InvalidOperationException();
var image = (Bitmap)Image.FromStream(stream);
return image;

View File

@@ -145,6 +145,9 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
{
// Get current hotkey settings (fresh from file) using the accessor's getter
module.HotkeySettings = hotkeyAccessor.Value;
module.HotkeySettings.ConflictDescription = isSystemConflict
? ResourceLoaderInstance.ResourceLoader.GetString("SysHotkeyConflictTooltipText")
: ResourceLoaderInstance.ResourceLoader.GetString("InAppHotkeyConflictTooltipText");
// Set header using localization key
module.Header = GetHotkeyLocalizationHeader(module.ModuleName, module.HotkeyID, hotkeyAccessor.LocalizationHeaderKey);