Compare commits

..

2 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
062dce8002 Fix context menu registry cleanup when modules disabled via GPO
Co-authored-by: yeelam-gordon <73506701+yeelam-gordon@users.noreply.github.com>
2025-08-27 04:21:42 +00:00
copilot-swe-agent[bot]
bef6f6c49a Initial plan 2025-08-27 04:14:37 +00:00
25 changed files with 350 additions and 810 deletions

View File

@@ -1315,7 +1315,6 @@ PRODUCTVERSION
Progman
programdata
projectname
projitems
PROPERTYKEY
Propset
PROPVARIANT
@@ -1395,7 +1394,6 @@ regkey
regroot
regsvr
REINSTALLMODE
releaseblog
reloadable
Relogger
remappings

199
README.md
View File

@@ -35,19 +35,19 @@ Microsoft PowerToys is a set of utilities for power users to tune and streamline
Go to the [Microsoft PowerToys GitHub releases page][github-release-link] and click on `Assets` at the bottom to show the files available in the release. Please use the appropriate PowerToys installer that matches your machine's architecture and install scope. For most, it is `x64` and per-user.
<!-- items that need to be updated release to release -->
[github-next-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.95%22
[github-current-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.94%22
[ptUserX64]: https://github.com/microsoft/PowerToys/releases/download/v0.94.0/PowerToysUserSetup-0.94.0-x64.exe
[ptUserArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.94.0/PowerToysUserSetup-0.94.0-arm64.exe
[ptMachineX64]: https://github.com/microsoft/PowerToys/releases/download/v0.94.0/PowerToysSetup-0.94.0-x64.exe
[ptMachineArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.94.0/PowerToysSetup-0.94.0-arm64.exe
[github-next-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.94%22
[github-current-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.93%22
[ptUserX64]: https://github.com/microsoft/PowerToys/releases/download/v0.93.0/PowerToysUserSetup-0.93.0-x64.exe
[ptUserArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.93.0/PowerToysUserSetup-0.93.0-arm64.exe
[ptMachineX64]: https://github.com/microsoft/PowerToys/releases/download/v0.93.0/PowerToysSetup-0.93.0-x64.exe
[ptMachineArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.93.0/PowerToysSetup-0.93.0-arm64.exe
| Description | Filename |
|----------------|----------|
| Per user - x64 | [PowerToysUserSetup-0.94.0-x64.exe][ptUserX64] |
| Per user - ARM64 | [PowerToysUserSetup-0.94.0-arm64.exe][ptUserArm64] |
| Machine wide - x64 | [PowerToysSetup-0.94.0-x64.exe][ptMachineX64] |
| Machine wide - ARM64 | [PowerToysSetup-0.94.0-arm64.exe][ptMachineArm64] |
| Per user - x64 | [PowerToysUserSetup-0.93.0-x64.exe][ptUserX64] |
| Per user - ARM64 | [PowerToysUserSetup-0.93.0-arm64.exe][ptUserArm64] |
| Machine wide - x64 | [PowerToysSetup-0.93.0-x64.exe][ptMachineX64] |
| Machine wide - ARM64 | [PowerToysSetup-0.93.0-arm64.exe][ptMachineArm64] |
This is our preferred method.
@@ -93,145 +93,118 @@ For guidance on developing for PowerToys, please read the [developer docs](./doc
Our [prioritized roadmap][roadmap] of features and utilities that the core team is focusing on.
### 0.94 - Sep 2025 Update
### 0.93 - Aug 2025 Update
In this release, we focused on new features, stability, optimization improvements, and automation.
For an in-depth look at the latest changes, visit the [release blog](https://aka.ms/powertoys-releaseblog).
**✨Highlights**
- PowerToys Settings added a Settings search with fuzzy matching, suggestions, a results page, and UX polish to make finding options faster.
- A comprehensive hotkey conflict detection system was introduced in Settings to surface and help resolve conflicting shortcuts. Note that the default hotkey settings (Win+Ctrl+Shift+T, Win+Ctrl+V, Win+Ctrl+T, Win+Shift+T) may overlap with existing Windows system shortcuts. This is expected. You can resolve the conflict by assigning different hotkeys.
- Mouse Utilities added a “Gliding cursor” accessibility feature to Mouse Pointer Crosshairs for singlebutton cursor movement and clicking. Thanks [@mikehall-ms](https://github.com/mikehall-ms)!
- The installer was upgraded to WiX 5 after WiX 3 reached end-of-life; this move improved installer security, reliability, and community support.
- Tons of bug fixes and improvements for Command Palette, including visual updates and new support for filters on ListPages (handy for extension developers).
- Hosts Editor now has a “No leading spaces” option so active host entries can start at column 0 even if others are disabled. Thanks [@mohammed-saalim](https://github.com/mohammed-saalim)!
- Context menu registration was moved from the installer to runtime to avoid loading disabled modules (runtime registrations).
- Quick Accent now supports Maltese, and frequently used accents appear first (and are remembered across sessions). Thanks [@rovercoder](https://github.com/rovercoder)! [@davidegiacometti](https://github.com/davidegiacometti)!
### Always On Top
- Fixed the border hover cursor so it shows the arrow instead of the wait cursor. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
- PowerToys settings debuts a modern, card-based dashboard with clearer descriptions and faster navigation for a streamlined user experience.
- Command Palette had over 99 issues resolved, including bringing back Clipboard History, adding context menu shortcuts, pinning favorite apps, and supporting history in Run.
- Command Palette reduced its startup memory usage by ~15%, load time by ~40%, built-in extensions loading time by ~70%, and installation size by ~55%—all due to using the full Ahead-of-Time (AOT) compilation mode in Windows App SDK.
- Peek now supports instant previews and embedded thumbnails for Binary G-code (.bgcode) 3D printing files, making it easy to inspect models at a glance. Thanks [@pedrolamas](https://github.com/pedrolamas)!
- Mouse Utilities introduces a new spotlight highlighting mode that dims the screen and draws attention to your cursor, perfect for presentations.
- Test coverage improvements for multiple PowerToys modules including Command Palette, Advanced Paste, Peek, Text Extractor, and PowerRename — ensuring better reliability and quality, with over 600 new unit tests (mostly for Command Palette) and doubled UI automation coverage.
### Command Palette
- Applied single-click activation only to pointer input; keyboard always activates immediately. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Let context menus open at the cursor by removing window-bound constraints. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Made error messages clearer with timestamps, HRESULTs, and full details for easier diagnosis. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Prevented crashes and improved robustness when updating providers without commands. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Ensured the Settings window reliably comes to the front when opened. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Replaced the Clipboard History icon with a colorful Fluent icon. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Hardened ContentIcon to avoid duplicate parenting and improve diagnostics. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Standardized null checks using C# pattern matching for safer behavior.
- Improved accessibility by focusing the activation shortcut dialog and making text reachable. Thanks [@chatasweetie](https://github.com/chatasweetie)!
- Moved the extension SDK to a stable Windows SDK and cleaned up message namespaces.
- Added path shortcuts: ~ to home, and / or \\ to system root, plus UNC support. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
- Fixed a race in cancellation handling to avoid InvalidOperationException. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Aligned separator styling with WinUI 3 for consistent visuals. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Added ARM64 PDBs to the Extensions SDK NuGet for better debugging.
- Added single-select filters to DynamicListPage and updated Windows Services sample.
- Updated main page placeholder text to better describe what can be searched. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Removed explicit WinAppSDK/WebView2 dependencies from toolkit and API. Thanks [@rluengen](https://github.com/rluengen)!
- Added a local keyboard hook to handle the GoBack key reliably. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Propagated alias changes safely and resolved conflicts across view models.
- Allowed providers to override Dispose with a virtual method.
- Fixed memory leaks by cleaning up removed or cancelled list items.
- Sorted DateTime extension results by relevance for better usability.
- Reduced search text “jiggling” by avoiding redundant change notifications.
- Centralized automation notifications in a UIHelper for better accessibility. Thanks [@chatasweetie](https://github.com/chatasweetie)!
- Preserved Adaptive Card action types during trimming via DynamicDependency.
- Added an acrylic backdrop and refined styling to the context menu. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Prevented disposed pages and Settings windows from handling stale messages. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Made the extension API easier to evolve without breaking clients.
- Added “evil” sample pages to help reproduce tricky bugs.
- Fixed WinGet trim-safety issues by replacing LINQ with manual iteration.
- Cancelled stale list fetches to avoid older results overwriting newer ones in CmdPal.
- Ensured screen readers are notified when the selected item in the list changes for better accessibility.
- Fixed command title changes not being properly notified to screen readers. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Made icon controls excluded from keyboard navigation by default for better accessibility. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Improved UI design with better text sizing and alignment.
- Fixed keyboard shortcuts to work better in text boxes and context menus.
- Added right-click context menus with critical command styling and separators.
- Improved various context menu issues, improving item selection, handling of long titles, search bar text scaling, initial item behavior, and primary button functionality.
- Fixed context menu crashes with better type handling.
- Fixed "Reload" command to work with both uppercase and lowercase letters.
- Added mouse back button support for easier navigation. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Fixed Alt+Left Arrow navigation not working when search box contains text. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Updated back button tooltip to show keyboard shortcut information. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Fixed Command Palette window not appearing properly when activated. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Fixed Command Palette window staying hidden from taskbar after File Explorer restarts. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Fixed window focus not returning to previous app properly.
- Fixed Command Palette window to always appear on top when shown and move to bottom when hidden. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Fixed window hiding to properly work on UI thread. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Fixed crashes and improved stability with better synchronization of Command list updates. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Improved extension disposal with better error handling to prevent crashes. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Improved stability by fixing a UI threading issue when loading more results, preventing possible crashes and ensuring the loading state resets if loading fails. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Enhanced icon loading stability with better exception handling. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Added thread safety to recent commands to prevent crashes. Thanks [@MaoShengelia](https://github.com/MaoShengelia)!
- Fixed acrylic (frosted glass) system backdrop display issues by ensuring proper UI thread handling. Thanks [@jiripolasek](https://github.com/jiripolasek)!
### Command Palette extensions
- Improved empty states and ranking logic for multiple extensions. Thanks [@htcfreek](https://github.com/htcfreek)!
- Added app icons to the All Apps "Run" context command when available.
- Restored missing builtin icons by standardizing extension dependencies.
- Unblocked local deployment by adding WinAppSDK to two sample extensions.
### Hosts File Editor
- Added a "No leading spaces" option so active hosts entries can start at column 0 even when others are disabled. Thanks [@mohammed-saalim](https://github.com/mohammed-saalim)!
### Image Resizer
- Fixed Image Resizer localization by installing satellite resources under the WinUI 3 apps culture path.
- Added settings to each provider to control which fallback commands are enabled. Thanks [@jiripolasek](https://github.com/jiripolasek)! for fixing a regression in this feature.
- Added sample code showing how Command Palette extensions can track when their pages are loaded or unloaded. [Check it out here](./src/modules/cmdpal/ext/SamplePagesExtension/OnLoadPage.cs).
- Fixed *Calculator* to accept regular spaces in numbers that use space separators. Thanks [@PesBandi](https://github.com/PesBandi)!
- Added a new setting to *Calculator* to make "Copy" the primary button (replacing “Save”) and enable "Close on Enter", streamlining the workflow. Thanks [@PesBandi](https://github.com/PesBandi)!
- Improved *Apps* indexing error handling and removed obsolete code. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
- Prevented apps from showing in search when the *Apps* extension is disabled. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Added ability to pin/unpin *Apps* using Ctrl+P shortcut.
- Added keyboard shortcuts to the *Apps* context menu items for faster access.
- Added all file context menu options to the *Apps* items context menu, making all file actions available there for better functionality.
- Streamlined All *Apps* extension settings by removing redundant descriptions, making the UI clearer.
- Added command history to the *Run* page for easier access to previous commands.
- Fixed directory path handling in *Run* fallback for better file navigation.
- Fixed URL fallback item hiding properly in *Web Search* extension when search query becomes invalid. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Added proper empty state message for *Web Search* extension when no results found. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Added fallback command to *Windows Settings* extension for better search results.
- Re-enabled *Clipboard History* feature with proper window handling.
- Improved *Add Bookmark* extension to automatically detect file, folder, or URL types without manual input.
- Updated terminology from "Kill process" to "End task" in *Window Walker* for consistency with Windows.
- Fixed minor grammar error in SamplePagesExtension code comments. Thanks [@purofle](https://github.com/purofle)!
### Mouse Utilities
- Introduced "Gliding cursor" to control the pointer and click with a single hotkey for better accessibility. Thanks [@mikehall-ms](https://github.com/mikehall-ms)!
### Mouse Without Borders
- Blocked Easy Mouse from switching machines during fullscreen apps, with an allow-list for exceptions. Thanks [@dot-tb](https://github.com/dot-tb)!
- Added a new spotlight highlighting mode that creates a large transparent circle around your cursor with a backdrop effect, providing an alternative to the traditional circle highlight. Perfect for presentations where you want to focus attention on a specific area while dimming the rest of the screen.
### Peek
- Added Visual Studio shared project file types to XML preview and fixed bgcode handler registration. Thanks [@rezanid](https://github.com/rezanid)!
- Fixes bgcode preview handler registration and events for reliable previews. Thanks [@pedrolamas](https://github.com/pedrolamas)!
### PowerRename
- Changed the Explorer accelerator key to PowErRename to avoid clashing with the New menu. Thanks [@aaron-ni](https://github.com/aaron-ni)!
- Added preview and thumbnail support for Binary G-code (.bgcode) files used in 3D printing. You can now see embedded thumbnails and preview these compressed 3D printing files directly in Peek and File Explorer. Thanks [@pedrolamas](https://github.com/pedrolamas)!
### Quick Accent
- Remembered character usage across sessions so frequently used accents appear first. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
- Added Maltese language support with specific characters and the Euro symbol. Thanks [@rovercoder](https://github.com/rovercoder)!
- Reduced GPU usage issues by making the window Topmost only when the picker is visible. Thanks [@daverayment](https://github.com/daverayment)!
- Added Vietnamese language support to Quick Accent, mappings for Vietnamese vowels (a, e, i, o, u, y) and the letter d. Thanks [@octastylos-pseudodipteros](https://github.com/octastylos-pseudodipteros)!
### Settings
- Added telemetry to track usage of the new shortcut conflict detection workflow.
- Moved the shutdown action from the title bar to a footer menu item with confirmation. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
- Implemented comprehensive hotkey conflict detection with a dedicated resolution dialog.
- Added branded visuals for Office and Copilot keys in the KeyVisual control.
- Introduced Settings search with fuzzy matching and navigation to specific controls.
- Corrected Spanish localization so product names like Awake remain in English across Settings and OOBE.
- Simplified the Advanced Paste description in Settings for quicker reading and consistent capitalization. Thanks [@OldUser101](https://github.com/OldUser101)!
- Localized conflict messages in the conflict window and dialog.
### Installer
- Upgraded the installer to WiX 5 with silent "Files in Use" handling for smoother winget installs.
- Switched Win10 context menu modules to runtime registration and added cleanup on uninstall to avoid stale entries.
- Completely redesigned the Settings dashboard with a modern card-based layout featuring organized sections for quick actions and shortcuts overview, replacing the old module list.
- Rewrote setting descriptions to be more concise and follow Windows writing style guidelines, making them easier to understand.
- Improved formatting and readability of release notes in the "What's New" section with better typography and spacing.
- Added missing deep link support for various settings pages (Peek, Quick Accent, PowerToys Run, etc.) so you can jump directly to specific settings.
- Resolved an issue where the settings page header would drift away from its position when resizing the settings window.
- Resolved a settings crash related to incompatible property names in ZoomIt configuration.
### Documentation
- Adds docs for building the installer locally and testing winget installs.
- Fixed a broken style guide link in developer documentation. Thanks [@denizmaral](https://github.com/denizmaral)!
- Added detailed step-by-step instructions for first-time developers building the Command Palette module, including prerequisites and Visual Studio setup guidance. Thanks [@chatasweetie](https://github.com/chatasweetie)!
- **Fixed Broken SDK Link**: Corrected a broken markdown link in the Command Palette SDK README that was pointing to an incorrect directory path. Thanks [@ChrisGuzak](https://github.com/ChrisGuzak)!
- Added documentation for the "Open With Cursor" plugin that enables opening Visual Studio and VS Code recent files using Cursor AI. Thanks [@VictorNoxx](https://github.com/VictorNoxx)!
- Added documentation for two new community plugins - Hotkeys plugin for creating custom keyboard shortcuts, and RandomGen plugin for generating random data like passwords, colors, and placeholder text. Thanks [@ruslanlap](https://github.com/ruslanlap)!
### Development
- Excluded test and coverage DLLs from BinSkim scans to cut false positives and speed up security analysis.
- Simplified NOTICE maintenance by removing version numbers and filtering out Microsoft/System packages.
- Improved NuGet dependency validation to prevent package downgrades and catch issues during restore.
- Updated UTF.Unknown to a modern version to improve compatibility without breaking changes. Thanks [@304NotModified](https://github.com/304NotModified)!
- Refreshed package catalog in CI before installing dependencies to prevent Linux workflow failures.
- Refactored CmdPal tests with dependency injection and added coverage for queries and settings.
- Added unit tests to verify Close on Enter swaps Copy/Save as expected. Thanks [@mohammed-saalim](https://github.com/mohammed-saalim)!
- Added accessibility IDs to CmdPal UI for stable UI tests.
- Rewrote system command tests with a new test base and cleaner patterns.
- Added unit tests for WebSearch and Shell extensions with mockable settings.
- Added unit tests and abstractions for Apps and Bookmarks extensions.
- Cleans up AIgenerated tests; adds meaningful query tests across extensions.
- Removed the obsolete debug dialog from Settings for a smoother developer loop.
- Updated .NET libraries to 9.0.8 for performance and security. Thanks [@snickler](https://github.com/snickler)!
- Updated the spell check system to version 0.0.25 with better GitHub integration and SARIF reporting, plus fixed numerous spelling errors throughout the codebase including property names and documentation. Thanks [@jsoref](https://github.com/jsoref)!
- Cleaned up spelling check configuration to eliminate false positives and excessive noise that was appearing in every pull request, making the development process smoother.
- Replaced NuGet feed with Azure Artifacts for better package management.
- Implemented configurable UI test pipeline that can use pre-built official releases instead of building everything from scratch, reducing test execution time from 2+ hours.
- Replaced brittle pixel-by-pixel image comparison with perceptual hash (pHash) technology that's more robust to minor rendering differences - no more test failures due to anti-aliasing or compression artifacts.
- Reduced CI/fuzzing/UI test timeouts from 4 hours to 90 minutes, dramatically improving developer feedback loops and preventing long waits when builds get stuck.
- Standardized test project naming across the entire codebase and improved pipeline result identification by adding platform/install mode context to test run titles. Thanks [@khmyznikov](https://github.com/khmyznikov)!
- Added comprehensive UI test suites for multiple PowerToys modules including Command Palette, Advanced Paste, Peek, Text Extractor, and PowerRename - ensuring better reliability and quality.
- Enhanced UI test automation with command-line argument support, better session management, and improved element location methods using pattern matching to avoid failures from minor differences in exact matches.
### What is being planned over the next few releases
For [v0.95][github-next-release-work], we'll work on the items below:
For [v0.94][github-next-release-work], we'll work on the items below:
- Continued Command Palette polish
- Working on Shortcut Guide v2 (Thanks [@noraa-junker](https://github.com/noraa-junker)!)
- Working on upgrading the installer to WiX 5
- Working on shortcut conflict detection
- Working on setting search
- Upgrading Keyboard Manager's editor UI
- UI tweaking utility with day/night theme switcher
- DSC v3 support for top utilities
- New UI automation tests
- Stability, bug fixes

View File

@@ -19,26 +19,6 @@
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()
{
@@ -108,16 +88,21 @@ 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
@@ -150,7 +135,6 @@ private:
{
m_enabled = FileLocksmithSettingsInstance().GetEnabled();
m_extended_only = FileLocksmithSettingsInstance().GetShowInExtendedContextMenu();
UpdateRegistration(m_enabled);
Trace::EnableFileLocksmith(m_enabled);
}

View File

@@ -21,26 +21,6 @@
// 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()
{
@@ -118,9 +98,14 @@ 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
@@ -165,14 +150,19 @@ 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

@@ -27,9 +27,7 @@ public partial class MainListPage : DynamicListPage,
private readonly IServiceProvider _serviceProvider;
private readonly TopLevelCommandManager _tlcManager;
private IEnumerable<Scored<IListItem>>? _filteredItems;
private IEnumerable<Scored<IListItem>>? _filteredApps;
private IEnumerable<IListItem>? _allApps;
private IEnumerable<IListItem>? _filteredItems;
private bool _includeApps;
private bool _filteredItemsIncludesApps;
@@ -85,7 +83,7 @@ public partial class MainListPage : DynamicListPage,
}
else
{
RaiseItemsChanged();
RaiseItemsChanged(_tlcManager.TopLevelCommands.Count);
}
}
@@ -150,13 +148,7 @@ public partial class MainListPage : DynamicListPage,
{
lock (_tlcManager.TopLevelCommands)
{
var items = Enumerable.Empty<Scored<IListItem>>()
.Concat(_filteredItems is not null ? _filteredItems : [])
.Concat(_filteredApps is not null ? _filteredApps : [])
.OrderByDescending(o => o.Score)
.Select(s => s.Item)
.ToArray();
return items;
return _filteredItems?.ToArray() ?? [];
}
}
}
@@ -175,8 +167,6 @@ public partial class MainListPage : DynamicListPage,
{
_filteredItemsIncludesApps = _includeApps;
_filteredItems = null;
_filteredApps = null;
_allApps = null;
}
}
@@ -194,8 +184,6 @@ public partial class MainListPage : DynamicListPage,
{
_filteredItemsIncludesApps = _includeApps;
_filteredItems = null;
_filteredApps = null;
_allApps = null;
RaiseItemsChanged(commands.Count);
return;
}
@@ -205,49 +193,35 @@ public partial class MainListPage : DynamicListPage,
if (!newSearch.StartsWith(oldSearch, StringComparison.CurrentCultureIgnoreCase))
{
_filteredItems = null;
_filteredApps = null;
_allApps = null;
}
// If the internal state has changed, reset _filteredItems to reset the list.
if (_filteredItemsIncludesApps != _includeApps)
{
_filteredItems = null;
_filteredApps = null;
_allApps = null;
}
var newFilteredItems = _filteredItems?.Select(s => s.Item);
// If we don't have any previous filter results to work with, start
// with a list of all our commands & apps.
if (newFilteredItems is null && _filteredApps is null)
if (_filteredItems is null)
{
newFilteredItems = commands;
_filteredItems = commands;
_filteredItemsIncludesApps = _includeApps;
if (_includeApps)
{
_allApps = AllAppsCommandProvider.Page.GetItems();
IEnumerable<IListItem> apps = AllAppsCommandProvider.Page.GetItems();
var appIds = apps.Select(app => app.Command.Id).ToArray();
// Remove any top level pinned apps and use the apps from AllAppsCommandProvider.Page.GetItems()
// since they contain details.
_filteredItems = _filteredItems.Where(item => item.Command is not AppCommand);
_filteredItems = _filteredItems.Concat(apps);
}
}
// Produce a list of everything that matches the current filter.
_filteredItems = ListHelpers.FilterListWithScores<IListItem>(newFilteredItems ?? [], SearchText, ScoreTopLevelItem);
// Produce a list of filtered apps with the appropriate limit
if (_allApps is not null)
{
_filteredApps = ListHelpers.FilterListWithScores<IListItem>(_allApps, SearchText, ScoreTopLevelItem);
var appResultLimit = AllAppsCommandProvider.TopLevelResultLimit;
if (appResultLimit >= 0)
{
_filteredApps = _filteredApps.Take(appResultLimit);
}
}
RaiseItemsChanged();
_filteredItems = ListHelpers.FilterList<IListItem>(_filteredItems, SearchText, ScoreTopLevelItem);
RaiseItemsChanged(_filteredItems.Count());
}
}

View File

@@ -3,7 +3,9 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.CmdPal.Ext.Apps.Programs;
using Microsoft.CmdPal.Ext.Apps.Properties;
using Microsoft.CmdPal.Ext.Apps.State;
using Microsoft.CommandPalette.Extensions;
@@ -43,28 +45,6 @@ public partial class AllAppsCommandProvider : CommandProvider
PinnedAppsManager.Instance.PinStateChanged += OnPinStateChanged;
}
public static int TopLevelResultLimit
{
get
{
var limitSetting = AllAppsSettings.Instance.SearchResultLimit;
if (limitSetting is null)
{
return -1;
}
var quantity = -1;
if (int.TryParse(limitSetting, out var result))
{
quantity = result;
}
return quantity;
}
}
public override ICommandItem[] TopLevelCommands() => [_listItem, .._page.GetPinnedApps()];
public ICommandItem? LookupApp(string displayName)

View File

@@ -20,16 +20,6 @@ public class AllAppsSettings : JsonSettingsManager, ISettingsInterface
private static string Experimental(string propertyName) => $"{_namespace}.experimental.{propertyName}";
private static readonly List<ChoiceSetSetting.Choice> _searchResultLimitChoices =
[
new ChoiceSetSetting.Choice(Resources.limit_none, "-1"),
new ChoiceSetSetting.Choice(Resources.limit_0, "0"),
new ChoiceSetSetting.Choice(Resources.limit_1, "1"),
new ChoiceSetSetting.Choice(Resources.limit_5, "5"),
new ChoiceSetSetting.Choice(Resources.limit_10, "10"),
new ChoiceSetSetting.Choice(Resources.limit_20, "20"),
];
#pragma warning disable SA1401 // Fields should be private
internal static AllAppsSettings Instance = new();
#pragma warning restore SA1401 // Fields should be private
@@ -52,14 +42,6 @@ public class AllAppsSettings : JsonSettingsManager, ISettingsInterface
public bool EnablePathEnvironmentVariableSource => _enablePathEnvironmentVariableSource.Value;
private readonly ChoiceSetSetting _searchResultLimitSource = new(
Namespaced(nameof(SearchResultLimit)),
Resources.limit_fallback_results_source,
Resources.limit_fallback_results_source_description,
_searchResultLimitChoices);
public string SearchResultLimit => _searchResultLimitSource.Value ?? string.Empty;
private readonly ToggleSetting _enableStartMenuSource = new(
Namespaced(nameof(EnableStartMenuSource)),
Resources.enable_start_menu_source,
@@ -105,7 +87,6 @@ public class AllAppsSettings : JsonSettingsManager, ISettingsInterface
Settings.Add(_enableDesktopSource);
Settings.Add(_enableRegistrySource);
Settings.Add(_enablePathEnvironmentVariableSource);
Settings.Add(_searchResultLimitSource);
// Load settings from file upon initialization
LoadSettings();

View File

@@ -159,78 +159,6 @@ namespace Microsoft.CmdPal.Ext.Apps.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to 0.
/// </summary>
internal static string limit_0 {
get {
return ResourceManager.GetString("limit_0", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to 1.
/// </summary>
internal static string limit_1 {
get {
return ResourceManager.GetString("limit_1", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to 10.
/// </summary>
internal static string limit_10 {
get {
return ResourceManager.GetString("limit_10", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to 20.
/// </summary>
internal static string limit_20 {
get {
return ResourceManager.GetString("limit_20", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to 5.
/// </summary>
internal static string limit_5 {
get {
return ResourceManager.GetString("limit_5", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Limit the number of applications returned from the top level.
/// </summary>
internal static string limit_fallback_results_source {
get {
return ResourceManager.GetString("limit_fallback_results_source", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Limit fallback results to n apps.
/// </summary>
internal static string limit_fallback_results_source_description {
get {
return ResourceManager.GetString("limit_fallback_results_source_description", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to None.
/// </summary>
internal static string limit_none {
get {
return ResourceManager.GetString("limit_none", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Open containing folder.
/// </summary>

View File

@@ -198,28 +198,4 @@
<data name="unpin_app" xml:space="preserve">
<value>Unpin</value>
</data>
<data name="limit_1" xml:space="preserve">
<value>1</value>
</data>
<data name="limit_5" xml:space="preserve">
<value>5</value>
</data>
<data name="limit_10" xml:space="preserve">
<value>10</value>
</data>
<data name="limit_20" xml:space="preserve">
<value>20</value>
</data>
<data name="limit_fallback_results_source" xml:space="preserve">
<value>Limit the number of applications returned from the top level</value>
</data>
<data name="limit_fallback_results_source_description" xml:space="preserve">
<value>Limit fallback results to n apps</value>
</data>
<data name="limit_0" xml:space="preserve">
<value>0</value>
</data>
<data name="limit_none" xml:space="preserve">
<value>Unlimited</value>
</data>
</root>
</root>

View File

@@ -43,18 +43,13 @@ public partial class ListHelpers
}
public static IEnumerable<T> FilterList<T>(IEnumerable<T> items, string query, Func<string, T, int> scoreFunction)
{
return FilterListWithScores<T>(items, query, scoreFunction)
.Select(score => score.Item);
}
public static IEnumerable<Scored<T>> FilterListWithScores<T>(IEnumerable<T> items, string query, Func<string, T, int> scoreFunction)
{
var scores = items
.Select(li => new Scored<T>() { Item = li, Score = scoreFunction(query, li) })
.Where(score => score.Score > 0)
.OrderByDescending(score => score.Score);
return scores;
return scores
.Select(score => score.Item);
}
/// <summary>

View File

@@ -43,32 +43,11 @@ 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);
@@ -133,7 +112,10 @@ public:
package::RegisterSparsePackage(path, packageUri);
}
}
UpdateRegistration(m_enabled);
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
ImageResizerRuntimeRegistration::EnsureRegistered();
#endif
Trace::EnableImageResizer(m_enabled);
}
@@ -141,8 +123,11 @@ 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,25 +168,6 @@ 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
@@ -221,7 +202,9 @@ public:
package::RegisterSparsePackage(path, packageUri);
}
}
UpdateRegistration(m_enabled);
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
PowerRenameRuntimeRegistration::EnsureRegistered();
#endif
}
// Disable the powertoy
@@ -229,7 +212,10 @@ public:
{
m_enabled = false;
Logger::info(L"PowerRename disabled");
UpdateRegistration(m_enabled);
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
PowerRenameRuntimeRegistration::Unregister();
Logger::info(L"PowerRename context menu unregistered (Win10)");
#endif
}
// Returns if the powertoy is enabled
@@ -329,7 +315,6 @@ public:
void init_settings()
{
m_enabled = CSettingsInstance().GetEnabled();
UpdateRegistration(m_enabled);
Trace::EnablePowerRename(m_enabled);
}

View File

@@ -316,6 +316,7 @@ void start_enabled_powertoys()
should_powertoy_be_enabled = false;
}
bool module_currently_enabled = powertoy->is_enabled();
if (should_powertoy_be_enabled)
{
Logger::info(L"start_enabled_powertoys: Enabling powertoy {}", name);
@@ -324,5 +325,13 @@ void start_enabled_powertoys()
hkmng.EnableHotkeyByModule(name);
powertoy.UpdateHotkeyEx();
}
else if (module_currently_enabled)
{
Logger::info(L"start_enabled_powertoys: Disabling powertoy {}", name);
powertoy->disable();
auto& hkmng = HotkeyConflictDetector::HotkeyConflictManager::GetInstance();
hkmng.DisableHotkeyByModule(name);
powertoy.UpdateHotkeyEx();
}
}
}

View File

@@ -19,13 +19,6 @@ 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,
@@ -40,117 +33,32 @@ namespace Microsoft.PowerToys.Tools.XamlIndexBuilder
Environment.Exit(1);
}
string xamlRootDirectory = args[0];
string xamlDirectory = args[0];
string outputFile = args[1];
if (!Directory.Exists(xamlRootDirectory))
if (!Directory.Exists(xamlDirectory))
{
Debug.WriteLine($"Error: Directory '{xamlRootDirectory}' does not exist.");
Debug.WriteLine($"Error: Directory '{xamlDirectory}' does not exist.");
Environment.Exit(1);
}
try
{
var searchableElements = new List<SettingEntry>();
var processedFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var xamlFiles = Directory.GetFiles(xamlDirectory, "*.xaml", SearchOption.AllDirectories);
void ScanDirectory(string root)
foreach (var xamlFile in xamlFiles)
{
if (!Directory.Exists(root))
var fileName = Path.GetFileName(xamlFile);
if (ExcludedXamlFiles.Contains(fileName))
{
return;
// Skip ShellPage.xaml as it contains many elements not relevant for search
continue;
}
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}");
}
Debug.WriteLine($"Processing: {fileName}");
var elements = ExtractSearchableElements(xamlFile);
searchableElements.AddRange(elements);
}
searchableElements = searchableElements.OrderBy(e => e.PageTypeName).ThenBy(e => e.ElementName).ToList();
@@ -189,15 +97,15 @@ namespace Microsoft.PowerToys.Tools.XamlIndexBuilder
.Where(e => e.Name.LocalName == "SettingsPageControl")
.Where(e => e.Attribute(x + "Uid") != null);
// Extract SettingsCard elements (support both Name and x:Name)
// Extract SettingsCard elements
var settingsElements = doc.Descendants()
.Where(e => e.Name.LocalName == "SettingsCard")
.Where(e => e.Attribute("Name") != null || e.Attribute(x + "Name") != null || e.Attribute(x + "Uid") != null);
.Where(e => e.Attribute("Name") != null || e.Attribute(x + "Uid") != null);
// Extract SettingsExpander elements (support both Name and x:Name)
// Extract SettingsExpander elements
var settingsExpanderElements = doc.Descendants()
.Where(e => e.Name.LocalName == "SettingsExpander")
.Where(e => e.Attribute("Name") != null || e.Attribute(x + "Name") != null || e.Attribute(x + "Uid") != null);
.Where(e => e.Attribute("Name") != null || e.Attribute(x + "Uid") != null);
// Process SettingsPageControl elements
foreach (var element in settingsPageElements)
@@ -277,36 +185,16 @@ 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 on the element itself
// Try x:Uid
var uid = element.Attribute(x + "Uid")?.Value;
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;
return uid;
}
public static string GetParentElementName(XElement element, XNamespace x)
@@ -323,11 +211,6 @@ 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;
@@ -338,11 +221,6 @@ 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,15 +29,16 @@
<!-- 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>
<XamlRootDir Condition="'$(XamlRootDir)' == ''">$(MSBuildProjectDirectory)\..\Settings.UI\SettingsXAML</XamlRootDir>
<XamlRootDir Condition="'$(XamlViewsDir)' != ''">$([System.IO.Path]::GetDirectoryName('$(XamlViewsDir)'))</XamlRootDir>
<XamlViewsDir Condition="'$(XamlViewsDir)' == ''">$(MSBuildProjectDirectory)\..\Settings.UI\SettingsXAML\Views</XamlViewsDir>
<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. Root='$(XamlRootDir)'; Out='$(GeneratedJsonFile)'; Tool='$(TargetPath)'; DotNet='$(DotNetExe)'." />
<Exec Command="&quot;$(DotNetExe)&quot; &quot;$(TargetPath)&quot; &quot;$(XamlRootDir)&quot; &quot;$(GeneratedJsonFile)&quot;" />
<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;" />
</Target>
</Project>

View File

@@ -17,7 +17,7 @@ namespace Microsoft.PowerToys.Settings.UI.Helpers;
public abstract partial class NavigablePage : Page
{
private const int ExpandWaitDuration = 500;
private const int AnimationDuration = 1850;
private const int AnimationDuration = 2000;
private NavigationParams _pendingNavigationParams;
@@ -80,9 +80,6 @@ public abstract partial class NavigablePage : 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;
@@ -95,9 +92,9 @@ public abstract partial class NavigablePage : Page
dropShadow.Offset = new Vector3(0, 0, 0);
var spriteVisual = compositor.CreateSpriteVisual();
spriteVisual.Size = new Vector2((float)target.ActualWidth + 8, (float)target.ActualHeight + 8);
spriteVisual.Size = new Vector2((float)target.ActualWidth, (float)target.ActualHeight);
spriteVisual.Shadow = dropShadow;
spriteVisual.Offset = new Vector3(-4, -4, 0);
spriteVisual.Offset = new Vector3(0, 0, 0);
// Insert the shadow visual behind the target element
ElementCompositionPreview.SetElementChildVisual(target, spriteVisual);
@@ -116,129 +113,9 @@ public abstract partial class NavigablePage : Page
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;
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;
return element;
}
}

View File

@@ -181,6 +181,9 @@
</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)" />

View File

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

View File

@@ -24,7 +24,6 @@
<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}}">
@@ -43,7 +42,6 @@
</InfoBar>
<tkcontrols:SettingsCard
x:Name="MouseUtilsMouseJumpActivationShortcut"
x:Uid="MouseUtils_MouseJump_ActivationShortcut"
HeaderIcon="{ui:FontIcon Glyph=&#xEDA7;}"
IsEnabled="{x:Bind ViewModel.IsMouseJumpEnabled, Mode=OneWay}">
@@ -128,7 +126,6 @@
</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 Name="FancyZonesShiftDragCheckBoxControlHeader" ContentAlignment="Left">
<tkcontrols:SettingsCard ContentAlignment="Left">
<CheckBox x:Uid="FancyZones_ShiftDragCheckBoxControl_Header" IsChecked="{x:Bind ViewModel.ShiftDrag, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard Name="FancyZonesMouseDragCheckBoxControlHeader" ContentAlignment="Left">
<tkcontrols:SettingsCard ContentAlignment="Left">
<CheckBox x:Uid="FancyZones_MouseDragCheckBoxControl_Header" IsChecked="{x:Bind ViewModel.MouseSwitch, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard Name="FancyZonesMouseMiddleClickSpanningMultipleZonesCheckBoxControlHeader" ContentAlignment="Left">
<tkcontrols:SettingsCard ContentAlignment="Left">
<CheckBox x:Uid="FancyZones_MouseMiddleClickSpanningMultipleZonesCheckBoxControl_Header" IsChecked="{x:Bind ViewModel.MouseMiddleClickSpanningMultipleZones, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard Name="FancyZonesShowZonesOnAllMonitorsCheckBoxControl" ContentAlignment="Left">
<tkcontrols:SettingsCard ContentAlignment="Left">
<CheckBox x:Uid="FancyZones_ShowZonesOnAllMonitorsCheckBoxControl" IsChecked="{x:Bind ViewModel.ShowOnAllMonitors, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard Name="FancyZonesSpanZonesAcrossMonitors" ContentAlignment="Left">
<tkcontrols:SettingsCard 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 Name="FancyZonesPreviewCard" ContentAlignment="Left">
<tkcontrols:SettingsCard 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 Name="FancyZonesShowZoneNumberCheckBoxControl" ContentAlignment="Left">
<tkcontrols:SettingsCard 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,31 +164,28 @@
x:Uid="FancyZones_WindowBehavior_GroupSettings"
IsExpanded="True">
<tkcontrols:SettingsExpander.Items>
<tkcontrols:SettingsCard Name="FancyZonesDisplayOrWorkAreaChangeMoveWindowsCheckBoxControl" ContentAlignment="Left">
<tkcontrols:SettingsCard ContentAlignment="Left">
<CheckBox x:Uid="FancyZones_DisplayOrWorkAreaChangeMoveWindowsCheckBoxControl" IsChecked="{x:Bind ViewModel.DisplayOrWorkAreaChangeMoveWindows, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard Name="FancyZonesZoneSetChangeMoveWindows" ContentAlignment="Left">
<tkcontrols:SettingsCard ContentAlignment="Left">
<CheckBox x:Uid="FancyZones_ZoneSetChangeMoveWindows" IsChecked="{x:Bind ViewModel.ZoneSetChangeMoveWindows, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard Name="FancyZonesAppLastZoneMoveWindows" ContentAlignment="Left">
<tkcontrols:SettingsCard ContentAlignment="Left">
<CheckBox x:Uid="FancyZones_AppLastZoneMoveWindows" IsChecked="{x:Bind ViewModel.AppLastZoneMoveWindows, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard Name="FancyZonesOpenWindowOnActiveMonitor" ContentAlignment="Left">
<tkcontrols:SettingsCard ContentAlignment="Left">
<CheckBox x:Uid="FancyZones_OpenWindowOnActiveMonitor" IsChecked="{x:Bind ViewModel.OpenWindowOnActiveMonitor, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard Name="FancyZonesRestoreSize" ContentAlignment="Left">
<tkcontrols:SettingsCard ContentAlignment="Left">
<CheckBox x:Uid="FancyZones_RestoreSize" IsChecked="{x:Bind ViewModel.RestoreSize, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard Name="FancyZonesMakeDraggedWindowTransparentCheckBoxControl" ContentAlignment="Left">
<tkcontrols:SettingsCard ContentAlignment="Left">
<CheckBox x:Uid="FancyZones_MakeDraggedWindowTransparentCheckBoxControl" IsChecked="{x:Bind ViewModel.MakeDraggedWindowsTransparent, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard Name="FancyZonesAllowChildWindowSnap" ContentAlignment="Left">
<tkcontrols:SettingsCard ContentAlignment="Left">
<CheckBox x:Uid="FancyZones_AllowChildWindowSnap" IsChecked="{x:Bind ViewModel.AllowChildWindowSnap, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard
Name="FancyZonesDisableRoundCornersOnWindowSnap"
ContentAlignment="Left"
Visibility="{x:Bind ViewModel.Windows11, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
<tkcontrols:SettingsCard 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>
@@ -202,7 +199,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 Name="FancyZonesWindowSwitchingPlaceholder" Visibility="Collapsed" />
<tkcontrols:SettingsCard Visibility="Collapsed" />
<tkcontrols:SettingsCard
Name="FancyZonesHotkeyNextTabControl"
x:Uid="FancyZones_HotkeyNextTabControl"
@@ -251,10 +248,7 @@
</ComboBoxItem>
</ComboBox>
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard
Name="FancyZonesMoveWindowsAcrossAllMonitorsCheckBoxControl"
ContentAlignment="Left"
IsEnabled="{x:Bind ViewModel.SnapHotkeysCategoryEnabled, Mode=OneWay}">
<tkcontrols:SettingsCard 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>
@@ -268,10 +262,7 @@
HeaderIcon="{ui:FontIcon Glyph=&#xEDA7;}">
<ToggleSwitch x:Uid="ToggleSwitch" IsOn="{x:Bind ViewModel.QuickLayoutSwitch, Mode=TwoWay}" />
<tkcontrols:SettingsExpander.Items>
<tkcontrols:SettingsCard
Name="FancyZonesFlashZonesOnQuickSwitch"
ContentAlignment="Left"
IsEnabled="{x:Bind ViewModel.QuickSwitchEnabled, Mode=OneWay}">
<tkcontrols:SettingsCard 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>
@@ -286,10 +277,7 @@
HeaderIcon="{ui:FontIcon Glyph=&#xECE4;}"
IsExpanded="True">
<tkcontrols:SettingsExpander.Items>
<tkcontrols:SettingsCard
Name="FancyZonesExcludeAppsTextBoxControl"
HorizontalContentAlignment="Stretch"
ContentAlignment="Vertical">
<tkcontrols:SettingsCard HorizontalContentAlignment="Stretch" ContentAlignment="Vertical">
<TextBox
x:Uid="FancyZones_ExcludeApps_TextBoxControl"
MinWidth="240"

View File

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

View File

@@ -57,6 +57,12 @@
</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"
@@ -65,10 +71,10 @@
</Grid>
</DataTemplate>
<DataTemplate x:Key="ShowAllSearchResultTemplate" x:DataType="models:SuggestionItem">
<Grid>
<Grid Padding="16,8">
<Rectangle
Height="1"
Margin="0,-12,0,0"
Margin="0,-4,0,0"
HorizontalAlignment="Stretch"
VerticalAlignment="Top"
Fill="{ThemeResource DividerStrokeColorDefaultBrush}" />
@@ -86,6 +92,7 @@
<ic:InvokeCommandAction Command="{x:Bind ViewModel.LoadedCommand}" />
</ic:EventTriggerBehavior>
</i:Interaction.Behaviors>
<Grid x:Name="RootGrid">
<Grid.RowDefinitions>
<RowDefinition Height="48" />

View File

@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -141,7 +142,8 @@ namespace Microsoft.PowerToys.Settings.UI.Views
private const int SearchDebounceMs = 500;
private bool _disposed;
// Removed trace id counter per cleanup
// Tracing id for correlating logs of a single search interaction
private static long _searchTraceIdCounter;
/// <summary>
/// Initializes a new instance of the <see cref="ShellPage"/> class.
@@ -441,11 +443,25 @@ 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(_ => { });
.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.");
}
});
}
private void NavigationView_DisplayModeChanged(NavigationView sender, NavigationViewDisplayModeChangedEventArgs args)
@@ -496,6 +512,10 @@ 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();
@@ -508,6 +528,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
sender.IsSuggestionListOpen = false;
_lastSearchResults.Clear();
_lastQueryText = string.Empty;
Logger.LogDebug($"[Search][TextChanged][{traceId}] empty query. end");
return;
}
@@ -517,11 +538,14 @@ namespace Microsoft.PowerToys.Settings.UI.Views
}
catch (TaskCanceledException)
{
return; // debounce canceled
// A newer keystroke arrived; abandon this run
Logger.LogDebug($"[Search][TextChanged][{traceId}] debounce canceled at +{swOverall.ElapsedMilliseconds} ms");
return;
}
if (token.IsCancellationRequested)
{
Logger.LogDebug($"[Search][TextChanged][{traceId}] token canceled post-debounce at +{swOverall.ElapsedMilliseconds} ms");
return;
}
@@ -530,25 +554,106 @@ 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;
var top = BuildSuggestionItems(query, results);
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 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)
@@ -605,98 +710,23 @@ 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)
{
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;
// do not prompt unless search for text.
return;
}
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;
}
@@ -704,6 +734,7 @@ 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;
}
@@ -711,10 +742,21 @@ 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(() => SearchIndexService.Search(queryText));
: 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;
});
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

@@ -2902,19 +2902,20 @@ 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">
@@ -5271,7 +5272,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>
@@ -5292,8 +5293,4 @@ 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,21 +126,14 @@ 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();
var actualResourceName = resourceNames.FirstOrDefault(n => string.Equals(n, resourceName, StringComparison.OrdinalIgnoreCase));
if (actualResourceName is null)
if (!resourceNames.Contains(resourceName))
{
throw new InvalidOperationException($"Embedded resource '{resourceName}' (case-insensitive) does not exist.");
throw new InvalidOperationException($"Embedded resource '{resourceName}' does not exist.");
}
var stream = assembly.GetManifestResourceStream(actualResourceName)
var stream = assembly.GetManifestResourceStream(resourceName)
?? throw new InvalidOperationException();
var image = (Bitmap)Image.FromStream(stream);
return image;