diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index 9c9ed952df..9911ff6d81 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -25,6 +25,8 @@ ADMINS adml admx advancedpaste +advancedpasteui +advancedpasteuishortcut advfirewall AFeature affordances @@ -40,6 +42,7 @@ ALLINPUT Allman Allmodule ALLOWUNDO +allpc ALLVIEW ALPHATYPE AModifier @@ -115,6 +118,7 @@ bigbar bigobj binlog binres +binskim BITMAPFILEHEADER bitmapimage BITMAPINFO @@ -255,6 +259,7 @@ Corpor cotaskmem COULDNOT countof +covrun cpcontrols cph cplusplus @@ -627,6 +632,7 @@ HKCU hkey HKLM HKM +hkmng HKPD HKU HMD @@ -635,6 +641,7 @@ hmodule hmonitor homies homljgmgpmcbpjbnjpfijnhipfkiclkd +HOOKPROC HORZRES HORZSIZE Hostbackdropbrush @@ -643,7 +650,11 @@ Hostx hotfixes hotkeycontrol HOTKEYF +hotkeylockmachine +hotkeyreconnect hotkeys +hotkeyswitch +hotkeytoggleeasymouse hotlight hotspot HPAINTBUFFER @@ -701,9 +712,12 @@ IMAGERESIZERCONTEXTMENU IMAGERESIZEREXT imageresizerinput imageresizersettings +imagetotext +imagetotextshortcut imagingdevices ime imgflip +inapp inbox INCONTACT Indo @@ -757,6 +771,7 @@ istep ith ITHUMBNAIL IUI +IUWP IWIC jfif jgeosdfsdsgmkedfgdfgdfgbkmhcgcflmi @@ -786,6 +801,7 @@ keyvault KILLFOCUS killrunner kmph +kvp Kybd lastcodeanalysissucceeded LASTEXITCODE @@ -824,6 +840,7 @@ localappdata localpackage LOCALSYSTEM LOCATIONCHANGE +LOCKMACHINE LOCKTYPE LOGFONT LOGFONTW @@ -909,6 +926,7 @@ MDL mdtext mdtxt mdwn +measuretool meme memicmp MENUITEMINFO @@ -958,6 +976,7 @@ MOUSEHWHEEL MOUSEINPUT mousejump mousepointer +mousepointercrosshairs mouseutils MOVESIZEEND MOVESIZESTART @@ -969,6 +988,7 @@ msc mscorlib msctls msdata +msdia MSDL MSGFLT MSHCTX @@ -1157,6 +1177,18 @@ PARENTRELATIVEFORADDRESSBAR PARENTRELATIVEPARSING parray PARTIALCONFIRMATIONDIALOGTITLE +pasteashtmlfile +pasteashtmlfileshortcut +pasteasjson +pasteasjsonshortcut +pasteasmarkdown +pasteasmarkdownshortcut +pasteasplaintext +pasteasplaintextshortcut +pasteaspngfile +pasteaspngfileshortcut +pasteastxtfile +pasteastxtfileshortcut PATCOPY PATHMUSTEXIST PATINVERT @@ -1224,6 +1256,7 @@ Pomodoro Popups POPUPWINDOW POSITIONITEM +powerocr POWERRENAMECONTEXTMENU powerrenameinput POWERRENAMETEST @@ -1364,6 +1397,7 @@ Removelnk renamable RENAMEONCOLLISION reparented +reparenthotkey reparenting reportfileaccesses requery @@ -1613,6 +1647,7 @@ STYLECHANGED STYLECHANGING subkeys sublang +Subdomain SUBMODULEUPDATE subresource Superbar @@ -1683,6 +1718,7 @@ THH THICKFRAME THISCOMPONENT throughs +thumbnailhotkey TILEDWINDOW TILLSON timedate @@ -1697,6 +1733,7 @@ tlb tlbimp tlc TNP +TOGGLEEASYMOUSE Toolhelp toolkitconverters toolwindow @@ -1710,6 +1747,7 @@ tracelogging tracerpt trackbar trafficmanager +transcodetomp transicc TRAYMOUSEMESSAGE triaging diff --git a/.github/workflows/msstore-submissions.yml b/.github/workflows/msstore-submissions.yml index 8878780987..a44dafb199 100644 --- a/.github/workflows/msstore-submissions.yml +++ b/.github/workflows/msstore-submissions.yml @@ -17,7 +17,7 @@ jobs: steps: - name: BODGY - Set up Gnome Keyring for future Cert Auth run: |- - sudo apt-get install -y gnome-keyring + sudo apt-get update && sudo apt-get install -y gnome-keyring export $(dbus-launch --sh-syntax) export $(echo 'anypass_just_to_unlock' | gnome-keyring-daemon --unlock) export $(echo 'anypass_just_to_unlock' | gnome-keyring-daemon --start --components=gpg,pkcs11,secrets,ssh) diff --git a/.pipelines/v2/release.yml b/.pipelines/v2/release.yml index 18163e899a..d6c2177720 100644 --- a/.pipelines/v2/release.yml +++ b/.pipelines/v2/release.yml @@ -64,6 +64,10 @@ extends: tsa: enabled: true configFile: '$(Build.SourcesDirectory)\.pipelines\tsa.json' + binskim: + enabled: true + # Exclude every dll/exe in tests/*, as well as all msdia*, covrun* and vcruntime* + analyzeTargetGlob: +:file|$(Build.ArtifactStagingDirectory)/**/*.dll;+:file|$(Build.ArtifactStagingDirectory)/**/*.exe;-:file:regex|tests.*\.(dll|exe)$;-:file:regex|(covrun.*)\.dll$;-:file:regex|(msdia.*)\.dll$;-:file:regex|(vcruntime.*)\.dll$ stages: - stage: Build diff --git a/PowerToys.sln b/PowerToys.sln index cad0fbcae4..4fdef8f659 100644 --- a/PowerToys.sln +++ b/PowerToys.sln @@ -792,6 +792,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{E11826E1 EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ScreenRuler.UITests", "src\modules\MeasureTool\Tests\ScreenRuler.UITests\ScreenRuler.UITests.csproj", "{66C069F8-C548-4CA6-8CDE-239104D68E88}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.Apps.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.Apps.UnitTests\Microsoft.CmdPal.Ext.Apps.UnitTests.csproj", "{E816D7B1-4688-4ECB-97CC-3D8E798F3830}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.Bookmarks.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.Bookmarks.UnitTests\Microsoft.CmdPal.Ext.Bookmarks.UnitTests.csproj", "{E816D7B3-4688-4ECB-97CC-3D8E798F3832}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|ARM64 = Debug|ARM64 @@ -2854,18 +2858,22 @@ Global {00D8659C-2068-40B6-8B86-759CD6284BBB}.Release|ARM64.Build.0 = Release|ARM64 {00D8659C-2068-40B6-8B86-759CD6284BBB}.Release|x64.ActiveCfg = Release|x64 {00D8659C-2068-40B6-8B86-759CD6284BBB}.Release|x64.Build.0 = Release|x64 - {66C069F8-C548-4CA6-8CDE-239104D68E88}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {66C069F8-C548-4CA6-8CDE-239104D68E88}.Debug|ARM64.Build.0 = Debug|ARM64 - {66C069F8-C548-4CA6-8CDE-239104D68E88}.Debug|ARM64.Deploy.0 = Debug|ARM64 - {66C069F8-C548-4CA6-8CDE-239104D68E88}.Debug|x64.ActiveCfg = Debug|x64 - {66C069F8-C548-4CA6-8CDE-239104D68E88}.Debug|x64.Build.0 = Debug|x64 - {66C069F8-C548-4CA6-8CDE-239104D68E88}.Debug|x64.Deploy.0 = Debug|x64 - {66C069F8-C548-4CA6-8CDE-239104D68E88}.Release|ARM64.ActiveCfg = Release|ARM64 - {66C069F8-C548-4CA6-8CDE-239104D68E88}.Release|ARM64.Build.0 = Release|ARM64 - {66C069F8-C548-4CA6-8CDE-239104D68E88}.Release|ARM64.Deploy.0 = Release|ARM64 - {66C069F8-C548-4CA6-8CDE-239104D68E88}.Release|x64.ActiveCfg = Release|x64 - {66C069F8-C548-4CA6-8CDE-239104D68E88}.Release|x64.Build.0 = Release|x64 - {66C069F8-C548-4CA6-8CDE-239104D68E88}.Release|x64.Deploy.0 = Release|x64 + {E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Debug|ARM64.Build.0 = Debug|ARM64 + {E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Debug|x64.ActiveCfg = Debug|x64 + {E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Debug|x64.Build.0 = Debug|x64 + {E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Release|ARM64.ActiveCfg = Release|ARM64 + {E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Release|ARM64.Build.0 = Release|ARM64 + {E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Release|x64.ActiveCfg = Release|x64 + {E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Release|x64.Build.0 = Release|x64 + {E816D7B3-4688-4ECB-97CC-3D8E798F3832}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {E816D7B3-4688-4ECB-97CC-3D8E798F3832}.Debug|ARM64.Build.0 = Debug|ARM64 + {E816D7B3-4688-4ECB-97CC-3D8E798F3832}.Debug|x64.ActiveCfg = Debug|x64 + {E816D7B3-4688-4ECB-97CC-3D8E798F3832}.Debug|x64.Build.0 = Debug|x64 + {E816D7B3-4688-4ECB-97CC-3D8E798F3832}.Release|ARM64.ActiveCfg = Release|ARM64 + {E816D7B3-4688-4ECB-97CC-3D8E798F3832}.Release|ARM64.Build.0 = Release|ARM64 + {E816D7B3-4688-4ECB-97CC-3D8E798F3832}.Release|x64.ActiveCfg = Release|x64 + {E816D7B3-4688-4ECB-97CC-3D8E798F3832}.Release|x64.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -3177,8 +3185,8 @@ Global {E816D7AF-4688-4ECB-97CC-3D8E798F3828} = {8EF25507-2575-4ADE-BF7E-D23376903AB8} {E816D7B0-4688-4ECB-97CC-3D8E798F3829} = {8EF25507-2575-4ADE-BF7E-D23376903AB8} {00D8659C-2068-40B6-8B86-759CD6284BBB} = {8EF25507-2575-4ADE-BF7E-D23376903AB8} - {E11826E1-76DF-42AC-985C-164CC2EE57A1} = {7AC943C9-52E8-44CF-9083-744D8049667B} - {66C069F8-C548-4CA6-8CDE-239104D68E88} = {E11826E1-76DF-42AC-985C-164CC2EE57A1} + {E816D7B1-4688-4ECB-97CC-3D8E798F3830} = {8EF25507-2575-4ADE-BF7E-D23376903AB8} + {E816D7B3-4688-4ECB-97CC-3D8E798F3832} = {8EF25507-2575-4ADE-BF7E-D23376903AB8} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0} diff --git a/README.md b/README.md index 493878bbde..27c98d07ff 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Microsoft PowerToys is a set of utilities for power users to tune and streamline | [Color Picker](https://aka.ms/PowerToysOverview_ColorPicker) | [Command Not Found](https://aka.ms/PowerToysOverview_CmdNotFound) | [Command Palette](https://aka.ms/PowerToysOverview_CmdPal) | | [Crop And Lock](https://aka.ms/PowerToysOverview_CropAndLock) | [Environment Variables](https://aka.ms/PowerToysOverview_EnvironmentVariables) | [FancyZones](https://aka.ms/PowerToysOverview_FancyZones) | | [File Explorer Add-ons](https://aka.ms/PowerToysOverview_FileExplorerAddOns) | [File Locksmith](https://aka.ms/PowerToysOverview_FileLocksmith) | [Hosts File Editor](https://aka.ms/PowerToysOverview_HostsFileEditor) | -| [Image Resizer](https://aka.ms/PowerToysOverview_ImageResizer) | [Keyboard Manager](https://aka.ms/PowerToysOverview_KeyboardManager) | [Mouse utilities](https://aka.ms/PowerToysOverview_MouseUtilities) | +| [Image Resizer](https://aka.ms/PowerToysOverview_ImageResizer) | [Keyboard Manager](https://aka.ms/PowerToysOverview_KeyboardManager) | [Mouse Utilities](https://aka.ms/PowerToysOverview_MouseUtilities) | | [Mouse Without Borders](https://aka.ms/PowerToysOverview_MouseWithoutBorders) | [New+](https://aka.ms/PowerToysOverview_NewPlus) | [Paste as Plain Text](https://aka.ms/PowerToysOverview_PastePlain) | | [Peek](https://aka.ms/PowerToysOverview_Peek) | [PowerRename](https://aka.ms/PowerToysOverview_PowerRename) | [PowerToys Run](https://aka.ms/PowerToysOverview_PowerToysRun) | | [Quick Accent](https://aka.ms/PowerToysOverview_QuickAccent) | [Registry Preview](https://aka.ms/PowerToysOverview_RegistryPreview) | [Screen Ruler](https://aka.ms/PowerToysOverview_ScreenRuler) | @@ -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. -[github-next-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.93%22 -[github-current-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.92%22 -[ptUserX64]: https://github.com/microsoft/PowerToys/releases/download/v0.92.1/PowerToysUserSetup-0.92.1-x64.exe -[ptUserArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.92.1/PowerToysUserSetup-0.92.1-arm64.exe -[ptMachineX64]: https://github.com/microsoft/PowerToys/releases/download/v0.92.1/PowerToysSetup-0.92.1-x64.exe -[ptMachineArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.92.1/PowerToysSetup-0.92.1-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.92.1-x64.exe][ptUserX64] | -| Per user - ARM64 | [PowerToysUserSetup-0.92.1-arm64.exe][ptUserArm64] | -| Machine wide - x64 | [PowerToysSetup-0.92.1-x64.exe][ptMachineX64] | -| Machine wide - ARM64 | [PowerToysSetup-0.92.1-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,139 +93,119 @@ 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.92 - June 2025 Update +### 0.93 - Aug 2025 Update In this release, we focused on new features, stability, optimization improvements, and automation. **✨Highlights** - - PowerToys settings now has a toggle for the system tray icon, giving users control over its visibility based on personal preference. Thanks [@BLM16](https://github.com/BLM16)! - - Command Palette now has Ahead-of-Time ([AOT](https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot)) compatibility for all first-party extensions, improved extensibility, and core UX fixes, resulting in better performance and stability across commands. - - Color Picker now has customizable mouse button actions, enabling more personalized workflows by assigning functions to left, right, and middle clicks. Thanks [@PesBandi](https://github.com/PesBandi)! - - Bug Report Tool now has a faster and clearer reporting process, with progress indicators, improved compression, auto-cleanup of old trace logs, and inclusion of MSIX installer logs for more efficient diagnostics. - - File Explorer add-ons now have improved rendering stability, resolving issues with PDF previews, blank thumbnails, and text file crashes during file browsing. - -### Color Picker - - - Added mouse button actions so you can choose what left, right, or middle click does. Thanks [@PesBandi](https://github.com/PesBandi)! - -### Crop & Lock - - - Aligned window styling with current Windows theme for a cleaner look. Thanks [@sadirano](https://github.com/sadirano)! + - 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 - - Enhanced performance by resolving a regression in page loading. - - Applied consistent hotkey handling across all Command Palette commands for a smoother user experience. - - Improved graceful closing of Command Palette. Thanks [@davidegiacometti](https://github.com/davidegiacometti)! - - Fixed consistency issue for extensions' alias with "Direct" setting and enabled localization for "Direct" and "Indirect" for better user understanding. Thanks [@davidegiacometti](https://github.com/davidegiacometti)! - - Improved visual clarity by styling critical context items correctly. - - Automatically focused the field when only one is present on the content page. - - Improved stability and efficiency when loading file icons in SDK ThumbnailHelper.cs by removing unnecessary operations. Thanks [@OldUser101](https://github.com/OldUser101)! - - Enhanced details view with commands implementation. (See [Extension sample](./src/modules/cmdpal/ext/SamplePagesExtension/Pages/SampleListPageWithDetails.cs)) + - 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 - - Added "Copy Path" command to *App* search results for convenience. Thanks [@PesBandi](https://github.com/PesBandi)! - - Improved *Calculator* input experience by ignoring leading equal signs. Thanks [@PesBandi](https://github.com/PesBandi)! - - Corrected input handling in the *Calculator* extension to avoid showing errors for input with only leading whitespace. - - Improved *New Extension* wizard by validating names to prevent namespace errors. - - Ensured consistent context items display for the *Run* extension between fallback and top-level results. - - Fixed missing *Time & Date* commands in fallback results. Thanks [@htcfreek](https://github.com/htcfreek)! - - Fixed outdated results in the *Time & Date* extension. Thanks [@htcfreek](https://github.com/htcfreek)! - - Fixed an issue where *Web Search* always opened Microsoft Edge instead of the user's default browser on Windows 11 24H2 and later. Thanks [@RuggMatt](https://github.com/RuggMatt)! - - Improved ordering of *Windows Settings* extension search results from alphabetical to relevance-based for quicker access. - - Added "Restart Windows Explorer" command to the *Windows System Commands* provider for gracefully terminate and relaunch explorer.exe. Thanks [@jiripolasek](https://github.com/jiripolasek)! + - 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)! -### Command Palette Ahead-of-Time (AOT) readiness +### Mouse Utilities - - We’ve made foundational changes to prepare the Command Palette for future Ahead-of-Time (AOT) publishing. This includes replacing the calculator library with ExprTk, improving COM object handling, refining Win32 interop, and correcting trimming behavior—all to ensure compatibility, performance, and reliability under AOT constraints. All first-party extensions are now AOT-compatible. These improvements lay the groundwork for publishing Command Palette as an AOT application in the next release. - - Special thanks to [@Sergio0694](https://github.com/Sergio0694) for guidance on making COM APIs AOT-compatible, [@jtschuster](https://github.com/jtschuster) for fixing COM object handling, [@ArashPartow](https://github.com/ArashPartow) from ExprTk for integration suggestions, and [@tian-lt](https://github.com/tian-lt) from the Windows Calculator team for valuable suggestion throughout the migration journey and review. - - As part of the upcoming release, we’re also enabling AOT compatibility for key dependencies, including markdown rendering, Adaptive Cards, internal logging and telemetry library, and the core Command Palette UX. - -### FancyZones - - - Fixed DPI-scaling issues to ensure FancyZones Editor displays crisply on high-resolution monitors. Thanks [@HO-COOH](https://github.com/HO-COOH)! This inspired us a broader review across other PowerToys modules, leading to DPI display optimizations in Awake, Color Picker, PowerAccent, and more. - -### File Explorer add-ons - - - Fixed potential failures in PDF previewer and thumbnail generation, improving reliability when browsing PDF files. Thanks [@mohiuddin-khan-shiam](https://github.com/mohiuddin-khan-shiam)! - - Prevented Monaco Preview Handler crash when opening UTF-8-BOM text files. - -### Hosts File Editor - - - Added an in-app *“Learn more”* link to warning dialogs for quick guidance. Thanks [@PesBandi](https://github.com/PesBandi)! - -### Mouse Without Borders - - - Fixed firewall rule so MWB now accepts connections from IPs outside your local subnet. - - Cleaned legacy logs to reduce disk usage and noise. + - 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 - - Updated QOI reader so 3-channel QOI images preview correctly in Peek and File Explorer. Thanks [@mbartlett21](https://github.com/mbartlett21)! - - Added codec detection with a clear warning when a video can’t be previewed, along with a link to the Microsoft Store to download the required codec. + - 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)! -### PowerRename +### Quick Accent - - Added support for $YY-$MM-$DD in ModificationTime and AccessTime to enable flexible date-based renaming. - -### PowerToys Run - - - Suppressed error UI for known WPF-related crashes to reduce user confusion, while retaining diagnostic logging for analysis. This targets COMException 0xD0000701 and 0x80263001 caused by temporary DWM unavailability. - -### Registry Preview - - - Added "Extended data preview" via magnifier icon and context menu in the Data Grid, enabled easier inspection of complex registry types like REG_BINARY, REG_EXPAND_SZ, and REG_MULTI_SZ, etc. Thanks [@htcfreek](https://github.com/htcfreek)! - - Improved file-saving experience in Registry Preview by aligning with Notepad-like behavior, enhancing user prompts, error handling, and preventing crashes during unsaved or interrupted actions. Thanks [@htcfreek](https://github.com/htcfreek)! + - 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 an option to hide or show the PowerToys system tray icon. Thanks [@BLM16](https://github.com/BLM16)! - - Improved settings to show progress while a bug report package is being generated. - -### Workspaces - - - Stored Workspaces icons in user AppData to ensure profile portability and prevent loss during temporary folder cleanup. - - Enabled capture and launch of PWAs on non-default Edge or Chrome profiles, ensuring consistent behavior during creation and execution. + - 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 - - Added SpeedTest and Dictionary Definition to the third-party plugins documentation for PowerToys Run. Thanks [@ruslanlap](https://github.com/ruslanlap)! - - Corrected sample links and typo in Command Palette documentation. Thanks [@daverayment](https://github.com/daverayment) and [@roycewilliams](https://github.com/roycewilliams)! + - 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 - - Updated .NET libraries to 9.0.6 for performance and security. Thanks [@snickler](https://github.com/snickler)! - - Updated WinAppSDK to 1.7.2 for better stability and Windows support. - - Introduced a one-step local build script that generates a signed installer, enhancing developer productivity. - - Generated portable PDBs so cross-platform debuggers can read symbol files, improving debugging experience in VSCode and other tools. - - Simplified WinGet configuration files by using the [Microsoft.Windows.Settings](https://www.powershellgallery.com/packages/Microsoft.Windows.Settings) module to enable Developer Mode. Thanks [@mdanish-kh](https://github.com/mdanish-kh)! - - Adjusted build scripts for the latest Az.Accounts module to keep CI green. - - Streamlined release pipeline by removing hard-coded telemetry version numbers, and unified Command Palette versioning with Windows Terminal's versioning method for consistent updates. - - Enhanced the build validation step to show detailed differences between NOTICE.md and actual package dependencies and versions. - - Improved spell-checking accuracy across the repo. Thanks [@rovercoder](https://github.com/rovercoder)! - - Upgraded CI to TouchdownBuild v5 for faster pipelines. - - Added context comments to *Resources.resw* to help translators. - - Expanded fuzz testing coverage to include FancyZones. - - Integrated all unit tests into the CI pipeline, increasing from ~3,000 to ~5,000 tests. - - Enabled daily UI test automation on the main branch, now covering over 370 UI tests for end-to-end validation. - - Newly added unit tests for WorkspacesLib to improve reliability and maintainability. + - 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. -### General +### What is being planned over the next few releases -- Updated bug report compression library (cziplib 0.3.3) for faster and more reliable package creation. Thanks [@Chubercik](https://github.com/Chubercik)! -- Included App Installer (“AppX Deployment Server”) event logs in bug reports for more thorough diagnostics. - -### What is being planned for version 0.93 - -For [v0.93][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 - - New UI automation tests - - Working on installer upgrades + - 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 + - New UI automation tests - Stability, bug fixes ## PowerToys Community diff --git a/doc/devdocs/core/settings/settings-implementation.md b/doc/devdocs/core/settings/settings-implementation.md index d97aff2dac..defe59a3fa 100644 --- a/doc/devdocs/core/settings/settings-implementation.md +++ b/doc/devdocs/core/settings/settings-implementation.md @@ -71,6 +71,41 @@ When the user changes settings in the UI: 3. The runner calls the `set_config` function on the appropriate module 4. The module parses the JSON and applies the new settings +# Shortcut Conflict Detection + +Steps to enable conflict detection for a hotkey: + +### 1. Implement module interface for hotkeys +Ensure the module interface provides either `size_t get_hotkeys(Hotkey* hotkeys, size_t buffer_size)` or `std::optional GetHotkeyEx()`. + +- If not yet implemented, you need to add it so that it returns all hotkeys used by the module. +- **Important**: The order of the returned hotkeys matters. This order is used as an index to uniquely identify each hotkey for conflict detection and lookup. +- For reference, see: `src/modules/AdvancedPaste/AdvancedPasteModuleInterface/dllmain.cpp` + +### 2. Implement IHotkeyConfig in the module settings (UI side) +Make sure the module’s settings file inherits from `IHotkeyConfig` and implements `HotkeyAccessor[] GetAllHotkeyAccessors()`. + +- This method should return all hotkeys used in the module. +- **Important**: The order of the returned hotkeys must be consistent with step 1 (`get_hotkeys()` or `GetHotkeyEx()`). +- For reference, see: `src/settings-ui/Settings.UI.Library/AdvancedPasteSettings.cs` +- **_Note:_** `HotkeyAccessor` is a wrapper around HotkeySettings. +It provides both `getter` and `setter` methods to read and update the corresponding hotkey. +Additionally, each `HotkeyAccessor` requires a resource string that describes the purpose of the hotkey. +This string is typically defined in: `src/settings-ui/Settings.UI/Strings/en-us/Resources.resw` + +### 3. Update the module’s ViewModel +The corresponding ViewModel should inherit from `PageViewModelBase` and implement `Dictionary GetAllHotkeySettings()`. + +- This method should return all hotkeys, maintaining the same order as in steps 1 and 2. +- For reference, see: `src/settings-ui/Settings.UI/ViewModels/AdvancedPasteViewModel.cs` + +### 4. Ensure the module’s Views call `OnPageLoaded()` +Once the module’s view is loaded, make sure to invoke the ViewModel’s `OnPageLoaded()` method: +```cs +Loaded += (s, e) => ViewModel.OnPageLoaded(); +``` +- For reference, see: `src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPaste.xaml.cs` + ## Debugging Settings To debug settings issues: diff --git a/doc/devdocs/readme.md b/doc/devdocs/readme.md index c1be49a0cd..1709d12abe 100644 --- a/doc/devdocs/readme.md +++ b/doc/devdocs/readme.md @@ -52,7 +52,7 @@ Once you've discussed your proposed feature/fix/etc. with a team member, and an ## Rules - **Follow the pattern of what you already see in the code.** -- [Coding style](development/style.md). +- [Coding style](style.md). - Try to package new functionality/components into libraries that have nicely defined interfaces. - Package new functionality into classes or refactor existing functionality into a class as you extend the code. - When adding new classes/methods/changing existing code, add new unit tests or update the existing tests. diff --git a/installer/PowerToysSetup/Resources.wxs b/installer/PowerToysSetup/Resources.wxs index 5da4db1390..b238799dd1 100644 --- a/installer/PowerToysSetup/Resources.wxs +++ b/installer/PowerToysSetup/Resources.wxs @@ -11,7 +11,7 @@ - + @@ -181,7 +181,7 @@ @@ -553,6 +553,7 @@ + diff --git a/src/modules/AdvancedPaste/AdvancedPasteModuleInterface/dllmain.cpp b/src/modules/AdvancedPaste/AdvancedPasteModuleInterface/dllmain.cpp index 896b362735..6af0d636ac 100644 --- a/src/modules/AdvancedPaste/AdvancedPasteModuleInterface/dllmain.cpp +++ b/src/modules/AdvancedPaste/AdvancedPasteModuleInterface/dllmain.cpp @@ -112,7 +112,7 @@ private: return {}; } - static Hotkey parse_single_hotkey(const winrt::Windows::Data::Json::JsonObject& jsonHotkeyObject) + static Hotkey parse_single_hotkey(const winrt::Windows::Data::Json::JsonObject& jsonHotkeyObject, bool isShown = true) { try { @@ -122,6 +122,7 @@ private: hotkey.shift = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_SHIFT); hotkey.ctrl = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_CTRL); hotkey.key = static_cast(jsonHotkeyObject.GetNamedNumber(JSON_KEY_CODE)); + hotkey.isShown = isShown; return hotkey; } catch (...) @@ -231,8 +232,10 @@ private: return false; } - void process_additional_action(const winrt::hstring& actionName, const winrt::Windows::Data::Json::IJsonValue& actionValue) + void process_additional_action(const winrt::hstring& actionName, const winrt::Windows::Data::Json::IJsonValue& actionValue, bool actionsGroupIsShown = true) { + bool actionIsShown = true; + if (actionValue.ValueType() != winrt::Windows::Data::Json::JsonValueType::Object) { return; @@ -240,9 +243,9 @@ private: const auto action = actionValue.GetObjectW(); - if (!action.GetNamedBoolean(JSON_KEY_IS_SHOWN, false)) + if (!action.GetNamedBoolean(JSON_KEY_IS_SHOWN, false) || !actionsGroupIsShown) { - return; + actionIsShown = false; } if (action.HasKey(JSON_KEY_SHORTCUT)) @@ -250,7 +253,7 @@ private: const AdditionalAction additionalAction { actionName.c_str(), - parse_single_hotkey(action.GetNamedObject(JSON_KEY_SHORTCUT)) + parse_single_hotkey(action.GetNamedObject(JSON_KEY_SHORTCUT), actionIsShown) }; m_additional_actions.push_back(additionalAction); @@ -259,12 +262,12 @@ private: { for (const auto& [subActionName, subAction] : action) { - process_additional_action(subActionName, subAction); + process_additional_action(subActionName, subAction, actionIsShown); } } } - void read_settings(PowerToysSettings::PowerToyValues& settings) + void read_settings(PowerToysSettings::PowerToyValues& settings) { const auto settingsObject = settings.get_raw_json(); @@ -317,9 +320,21 @@ private: { const auto additionalActions = propertiesObject.GetNamedObject(JSON_KEY_ADDITIONAL_ACTIONS); - for (const auto& [actionName, additionalAction] : additionalActions) + // Define the expected order to ensure consistent hotkey ID assignment + const std::vector expectedOrder = { + L"image-to-text", + L"paste-as-file", + L"transcode" + }; + + // Process actions in the predefined order + for (auto& actionKey : expectedOrder) { - process_additional_action(actionName, additionalAction); + if (additionalActions.HasKey(actionKey)) + { + const auto actionValue = additionalActions.GetNamedValue(actionKey); + process_additional_action(actionKey, actionValue); + } } } @@ -331,17 +346,14 @@ private: for (const auto& customAction : customActions) { const auto object = customAction.GetObjectW(); + bool actionIsShown = object.GetNamedBoolean(JSON_KEY_IS_SHOWN, false); - if (object.GetNamedBoolean(JSON_KEY_IS_SHOWN, false)) - { - const CustomAction customActionData - { - static_cast(object.GetNamedNumber(JSON_KEY_ID)), - parse_single_hotkey(object.GetNamedObject(JSON_KEY_SHORTCUT)) - }; + const CustomAction customActionData{ + static_cast(object.GetNamedNumber(JSON_KEY_ID)), + parse_single_hotkey(object.GetNamedObject(JSON_KEY_SHORTCUT), actionIsShown) + }; - m_custom_actions.push_back(customActionData); - } + m_custom_actions.push_back(customActionData); } } } diff --git a/src/modules/Hosts/Hosts.Tests/HostsServiceTest.cs b/src/modules/Hosts/Hosts.Tests/HostsServiceTest.cs index 8eaa37a348..81052fd101 100644 --- a/src/modules/Hosts/Hosts.Tests/HostsServiceTest.cs +++ b/src/modules/Hosts/Hosts.Tests/HostsServiceTest.cs @@ -298,5 +298,34 @@ namespace Hosts.Tests var hidden = fileSystem.FileInfo.New(service.HostsFilePath).Attributes.HasFlag(FileAttributes.Hidden); Assert.IsTrue(hidden); } + + [TestMethod] + public async Task NoLeadingSpaces_Disabled_RemovesIndent() + { + var content = + @"10.1.1.1 host host.local # comment +10.1.1.2 host2 host2.local # another comment +"; + + var expected = + @"10.1.1.1 host host.local # comment +10.1.1.2 host2 host2.local # another comment +# 10.1.1.30 host30 host30.local # new entry +"; + + var fs = new CustomMockFileSystem(); + var settings = new Mock(); + settings.Setup(s => s.NoLeadingSpaces).Returns(true); + var svc = new HostsService(fs, settings.Object, _elevationHelper.Object); + fs.AddFile(svc.HostsFilePath, new MockFileData(content)); + + var data = await svc.ReadAsync(); + var entries = data.Entries.ToList(); + entries.Add(new Entry(0, "10.1.1.30", "host30 host30.local", "new entry", false)); + await svc.WriteAsync(data.AdditionalLines, entries); + + var result = fs.GetFile(svc.HostsFilePath); + Assert.AreEqual(expected, result.TextContents); + } } } diff --git a/src/modules/Hosts/Hosts/Settings/UserSettings.cs b/src/modules/Hosts/Hosts/Settings/UserSettings.cs index 3530a3f74b..75da5d214d 100644 --- a/src/modules/Hosts/Hosts/Settings/UserSettings.cs +++ b/src/modules/Hosts/Hosts/Settings/UserSettings.cs @@ -26,6 +26,8 @@ namespace Hosts.Settings private bool _loopbackDuplicates; + public bool NoLeadingSpaces { get; private set; } + public bool LoopbackDuplicates { get => _loopbackDuplicates; @@ -88,6 +90,7 @@ namespace Hosts.Settings AdditionalLinesPosition = (HostsAdditionalLinesPosition)settings.Properties.AdditionalLinesPosition; Encoding = (HostsEncoding)settings.Properties.Encoding; LoopbackDuplicates = settings.Properties.LoopbackDuplicates; + NoLeadingSpaces = settings.Properties.NoLeadingSpaces; } retry = false; diff --git a/src/modules/Hosts/HostsUILib/Helpers/HostsService.cs b/src/modules/Hosts/HostsUILib/Helpers/HostsService.cs index b07eb8f93c..83aa3544b1 100644 --- a/src/modules/Hosts/HostsUILib/Helpers/HostsService.cs +++ b/src/modules/Hosts/HostsUILib/Helpers/HostsService.cs @@ -157,7 +157,7 @@ namespace HostsUILib.Helpers { lineBuilder.Append('#').Append(' '); } - else if (anyDisabled) + else if (anyDisabled && !_userSettings.NoLeadingSpaces) { lineBuilder.Append(' ').Append(' '); } diff --git a/src/modules/Hosts/HostsUILib/Settings/IUserSettings.cs b/src/modules/Hosts/HostsUILib/Settings/IUserSettings.cs index 21a8e6fa36..46c7a7dab5 100644 --- a/src/modules/Hosts/HostsUILib/Settings/IUserSettings.cs +++ b/src/modules/Hosts/HostsUILib/Settings/IUserSettings.cs @@ -19,5 +19,7 @@ namespace HostsUILib.Settings event EventHandler LoopbackDuplicatesChanged; public delegate void OpenSettingsFunction(); + + public bool NoLeadingSpaces { get; } } } diff --git a/src/modules/MouseUtils/MouseHighlighter/MouseHighlighter.cpp b/src/modules/MouseUtils/MouseHighlighter/MouseHighlighter.cpp index 25a95f4d39..05670742ec 100644 --- a/src/modules/MouseUtils/MouseHighlighter/MouseHighlighter.cpp +++ b/src/modules/MouseUtils/MouseHighlighter/MouseHighlighter.cpp @@ -5,6 +5,7 @@ #include "MouseHighlighter.h" #include "trace.h" #include +#include #ifdef COMPOSITION namespace winrt @@ -49,6 +50,9 @@ private: void BringToFront(); HHOOK m_mouseHook = NULL; static LRESULT CALLBACK MouseHookProc(int nCode, WPARAM wParam, LPARAM lParam) noexcept; + // Helpers for spotlight overlay + float GetDpiScale() const; + void UpdateSpotlightMask(float cx, float cy, float radius, bool show); static constexpr auto m_className = L"MouseHighlighter"; static constexpr auto m_windowTitle = L"PowerToys Mouse Highlighter"; @@ -67,7 +71,14 @@ private: winrt::CompositionSpriteShape m_leftPointer{ nullptr }; winrt::CompositionSpriteShape m_rightPointer{ nullptr }; winrt::CompositionSpriteShape m_alwaysPointer{ nullptr }; - winrt::CompositionSpriteShape m_spotlightPointer{ nullptr }; + // Spotlight overlay (mask with soft feathered edge) + winrt::SpriteVisual m_overlay{ nullptr }; + winrt::CompositionMaskBrush m_spotlightMask{ nullptr }; + winrt::CompositionRadialGradientBrush m_spotlightMaskGradient{ nullptr }; + winrt::CompositionColorBrush m_spotlightSource{ nullptr }; + winrt::CompositionColorGradientStop m_maskStopCenter{ nullptr }; + winrt::CompositionColorGradientStop m_maskStopInner{ nullptr }; + winrt::CompositionColorGradientStop m_maskStopOuter{ nullptr }; bool m_leftPointerEnabled = true; bool m_rightPointerEnabled = true; @@ -123,6 +134,35 @@ bool Highlighter::CreateHighlighter() m_shape.RelativeSizeAdjustment({ 1.0f, 1.0f }); m_root.Children().InsertAtTop(m_shape); + // Create spotlight overlay (soft feather, DPI-aware) + m_overlay = m_compositor.CreateSpriteVisual(); + m_overlay.RelativeSizeAdjustment({ 1.0f, 1.0f }); + m_spotlightSource = m_compositor.CreateColorBrush(m_alwaysColor); + m_spotlightMaskGradient = m_compositor.CreateRadialGradientBrush(); + m_spotlightMaskGradient.MappingMode(winrt::CompositionMappingMode::Absolute); + // Center region fully transparent + m_maskStopCenter = m_compositor.CreateColorGradientStop(); + m_maskStopCenter.Offset(0.0f); + m_maskStopCenter.Color(winrt::Windows::UI::ColorHelper::FromArgb(0, 0, 0, 0)); + // Inner edge of feather (still transparent) + m_maskStopInner = m_compositor.CreateColorGradientStop(); + m_maskStopInner.Offset(0.995f); // will be updated per-radius + m_maskStopInner.Color(winrt::Windows::UI::ColorHelper::FromArgb(0, 0, 0, 0)); + // Outer edge (opaque mask -> overlay visible) + m_maskStopOuter = m_compositor.CreateColorGradientStop(); + m_maskStopOuter.Offset(1.0f); + m_maskStopOuter.Color(winrt::Windows::UI::ColorHelper::FromArgb(255, 255, 255, 255)); + m_spotlightMaskGradient.ColorStops().Append(m_maskStopCenter); + m_spotlightMaskGradient.ColorStops().Append(m_maskStopInner); + m_spotlightMaskGradient.ColorStops().Append(m_maskStopOuter); + + m_spotlightMask = m_compositor.CreateMaskBrush(); + m_spotlightMask.Source(m_spotlightSource); + m_spotlightMask.Mask(m_spotlightMaskGradient); + m_overlay.Brush(m_spotlightMask); + m_overlay.IsVisible(false); + m_root.Children().InsertAtTop(m_overlay); + return true; } catch (...) @@ -165,12 +205,8 @@ void Highlighter::AddDrawingPoint(MouseButton button) // always if (m_spotlightMode) { - float borderThickness = static_cast(std::hypot(GetSystemMetrics(SM_CXVIRTUALSCREEN), GetSystemMetrics(SM_CYVIRTUALSCREEN))); - circleGeometry.Radius({ static_cast(borderThickness / 2.0 + m_radius), static_cast(borderThickness / 2.0 + m_radius) }); - circleShape.FillBrush(nullptr); - circleShape.StrokeBrush(m_compositor.CreateColorBrush(m_alwaysColor)); - circleShape.StrokeThickness(borderThickness); - m_spotlightPointer = circleShape; + UpdateSpotlightMask(static_cast(pt.x), static_cast(pt.y), m_radius, true); + return; } else { @@ -209,20 +245,14 @@ void Highlighter::UpdateDrawingPointPosition(MouseButton button) } else { - // always + // always / spotlight idle if (m_spotlightMode) { - if (m_spotlightPointer) - { - m_spotlightPointer.Offset({ static_cast(pt.x), static_cast(pt.y) }); - } + UpdateSpotlightMask(static_cast(pt.x), static_cast(pt.y), m_radius, true); } - else + else if (m_alwaysPointer) { - if (m_alwaysPointer) - { - m_alwaysPointer.Offset({ static_cast(pt.x), static_cast(pt.y) }); - } + m_alwaysPointer.Offset({ static_cast(pt.x), static_cast(pt.y) }); } } } @@ -266,9 +296,9 @@ void Highlighter::ClearDrawingPoint() { if (m_spotlightMode) { - if (m_spotlightPointer) + if (m_overlay) { - m_spotlightPointer.StrokeBrush().as().Color(winrt::Windows::UI::ColorHelper::FromArgb(0, 0, 0, 0)); + m_overlay.IsVisible(false); } } else @@ -421,7 +451,10 @@ void Highlighter::StopDrawing() m_leftPointer = nullptr; m_rightPointer = nullptr; m_alwaysPointer = nullptr; - m_spotlightPointer = nullptr; + if (m_overlay) + { + m_overlay.IsVisible(false); + } ShowWindow(m_hwnd, SW_HIDE); UnhookWindowsHookEx(m_mouseHook); ClearDrawing(); @@ -452,6 +485,16 @@ void Highlighter::ApplySettings(MouseHighlighterSettings settings) m_rightPointerEnabled = false; } + // Keep spotlight overlay color updated + if (m_spotlightSource) + { + m_spotlightSource.Color(m_alwaysColor); + } + if (!m_spotlightMode && m_overlay) + { + m_overlay.IsVisible(false); + } + if (instance->m_visible) { instance->StopDrawing(); @@ -563,6 +606,43 @@ void Highlighter::Terminate() } } +float Highlighter::GetDpiScale() const +{ + return static_cast(GetDpiForWindow(m_hwnd)) / 96.0f; +} + +// Update spotlight radial mask center/radius with DPI-aware feather +void Highlighter::UpdateSpotlightMask(float cx, float cy, float radius, bool show) +{ + if (!m_spotlightMaskGradient) + { + return; + } + + m_spotlightMaskGradient.EllipseCenter({ cx, cy }); + m_spotlightMaskGradient.EllipseRadius({ radius, radius }); + + const float dpiScale = GetDpiScale(); + // Target a very fine edge: ~1 physical pixel, convert to DIPs: 1 / dpiScale + const float featherDip = 1.0f / (dpiScale > 0.0f ? dpiScale : 1.0f); + const float safeRadius = (std::max)(radius, 1.0f); + const float featherRel = (std::min)(0.25f, featherDip / safeRadius); + + if (m_maskStopInner) + { + m_maskStopInner.Offset((std::max)(0.0f, 1.0f - featherRel)); + } + + if (m_spotlightSource) + { + m_spotlightSource.Color(m_alwaysColor); + } + if (m_overlay) + { + m_overlay.IsVisible(show); + } +} + #pragma region MouseHighlighter_API void MouseHighlighterApplySettings(MouseHighlighterSettings settings) diff --git a/src/modules/MouseWithoutBorders/ModuleInterface/dllmain.cpp b/src/modules/MouseWithoutBorders/ModuleInterface/dllmain.cpp index 33030fbdfb..29d7a781ae 100644 --- a/src/modules/MouseWithoutBorders/ModuleInterface/dllmain.cpp +++ b/src/modules/MouseWithoutBorders/ModuleInterface/dllmain.cpp @@ -556,6 +556,61 @@ public: return m_enabled; } + virtual size_t get_hotkeys(Hotkey* hotkeys, size_t buffer_size) override + { + constexpr size_t num_hotkeys = 4; // We have 4 hotkeys + + if (hotkeys && buffer_size >= num_hotkeys) + { + PowerToysSettings::PowerToyValues values = PowerToysSettings::PowerToyValues::load_from_settings_file(MODULE_NAME); + + // Cache the raw JSON object to avoid multiple parsing + json::JsonObject root_json = values.get_raw_json(); + json::JsonObject properties_json = root_json.GetNamedObject(L"properties", json::JsonObject{}); + + size_t hotkey_index = 0; + + // Helper lambda to extract hotkey from JSON properties + auto extract_hotkey = [&](const wchar_t* property_name) -> Hotkey { + if (properties_json.HasKey(property_name)) + { + try + { + json::JsonObject hotkey_json = properties_json.GetNamedObject(property_name); + + // Extract hotkey properties directly from JSON + bool win = hotkey_json.GetNamedBoolean(L"win", false); + bool ctrl = hotkey_json.GetNamedBoolean(L"ctrl", false); + bool alt = hotkey_json.GetNamedBoolean(L"alt", false); + bool shift = hotkey_json.GetNamedBoolean(L"shift", false); + unsigned char key = static_cast( + hotkey_json.GetNamedNumber(L"code", 0)); + + return { win, ctrl, shift, alt, key }; + } + catch (...) + { + // If parsing individual hotkey fails, use defaults + return { false, false, false, false, 0 }; + } + } + else + { + // Property doesn't exist, use defaults + return { false, false, false, false, 0 }; + } + }; + + // Extract all hotkeys using the optimized helper + hotkeys[hotkey_index++] = extract_hotkey(L"ToggleEasyMouseShortcut"); // [0] Toggle Easy Mouse + hotkeys[hotkey_index++] = extract_hotkey(L"LockMachineShortcut"); // [1] Lock Machine + hotkeys[hotkey_index++] = extract_hotkey(L"Switch2AllPCShortcut"); // [2] Switch to All PCs + hotkeys[hotkey_index++] = extract_hotkey(L"ReconnectShortcut"); // [3] Reconnect + } + + return num_hotkeys; + } + void launch_add_firewall_process() { Logger::trace(L"Starting Process to add firewall rule"); diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Common/Helpers/DiagnosticsHelper.cs b/src/modules/cmdpal/Microsoft.CmdPal.Common/Helpers/DiagnosticsHelper.cs new file mode 100644 index 0000000000..d2e9ddbcb3 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.Common/Helpers/DiagnosticsHelper.cs @@ -0,0 +1,66 @@ +// 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.Runtime.InteropServices; + +namespace Microsoft.CmdPal.Common.Helpers; + +/// +/// Provides utility methods for building diagnostic and error messages. +/// +public static class DiagnosticsHelper +{ + /// + /// Builds a comprehensive exception message with timestamp and detailed diagnostic information. + /// + /// The exception that occurred. + /// A hint about which extension caused the exception to help with debugging. + /// A string containing the exception details, timestamp, and source information for diagnostic purposes. + public static string BuildExceptionMessage(Exception exception, string? extensionHint) + { + var locationHint = string.IsNullOrWhiteSpace(extensionHint) ? "application" : $"'{extensionHint}' extension"; + + // let's try to get a message from the exception or inferred it from the HRESULT + // to show at least something + var message = exception.Message; + if (string.IsNullOrWhiteSpace(message)) + { + var temp = Marshal.GetExceptionForHR(exception.HResult)?.Message; + if (!string.IsNullOrWhiteSpace(temp)) + { + message = temp + $" (inferred from HRESULT 0x{exception.HResult:X8})"; + } + } + + if (string.IsNullOrWhiteSpace(message)) + { + message = "[No message available]"; + } + + // note: keep date time kind and format consistent with the log + return $""" + ============================================================ + 😢 An unexpected error occurred in the {locationHint}. + + Summary: + Message: {message} + Type: {exception.GetType().FullName} + Source: {exception.Source ?? "N/A"} + Time: {DateTimeOffset.Now:yyyy-MM-dd HH:mm:ss.fffffff} + HRESULT: 0x{exception.HResult:X8} ({exception.HResult}) + + Stack Trace: + {exception.StackTrace ?? "[No stack trace available]"} + + ------------------ Full Exception Details ------------------ + {exception} + + ℹ️ If you need further assistance, please include this information in your support request. + ℹ️ Before sending, take a quick look to make sure it doesn't contain any personal or sensitive information. + ============================================================ + + """; + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Common/Helpers/ExtensionHostInstance.cs b/src/modules/cmdpal/Microsoft.CmdPal.Common/Helpers/ExtensionHostInstance.cs index 25ff815a69..76de2729d0 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.Common/Helpers/ExtensionHostInstance.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.Common/Helpers/ExtensionHostInstance.cs @@ -24,7 +24,7 @@ public partial class ExtensionHostInstance /// The log message to send public void LogMessage(ILogMessage message) { - if (Host != null) + if (Host is not null) { _ = Task.Run(async () => { @@ -47,7 +47,7 @@ public partial class ExtensionHostInstance public void ShowStatus(IStatusMessage message, StatusContext context) { - if (Host != null) + if (Host is not null) { _ = Task.Run(async () => { @@ -64,7 +64,7 @@ public partial class ExtensionHostInstance public void HideStatus(IStatusMessage message) { - if (Host != null) + if (Host is not null) { _ = Task.Run(async () => { diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Common/Helpers/SupersedingAsyncGate.cs b/src/modules/cmdpal/Microsoft.CmdPal.Common/Helpers/SupersedingAsyncGate.cs index d4618b5c3b..9313ba6755 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.Common/Helpers/SupersedingAsyncGate.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.Common/Helpers/SupersedingAsyncGate.cs @@ -89,7 +89,7 @@ public class SupersedingAsyncGate : IDisposable } catch (OperationCanceledException) { - CompleteIfCurrent(currentTcs, currentCallId, tcs => tcs.SetCanceled(currentCts.Token)); + CompleteIfCurrent(currentTcs, currentCallId, tcs => tcs.TrySetCanceled(currentCts.Token)); } catch (Exception ex) { diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/AppExtensionHost.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/AppExtensionHost.cs index 3a828a3e5d..8a93aee51d 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/AppExtensionHost.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/AppExtensionHost.cs @@ -36,7 +36,7 @@ public abstract partial class AppExtensionHost : IExtensionHost public IAsyncAction HideStatus(IStatusMessage? message) { - if (message == null) + if (message is null) { return Task.CompletedTask.AsAsyncAction(); } @@ -55,7 +55,7 @@ public abstract partial class AppExtensionHost : IExtensionHost public IAsyncAction LogMessage(ILogMessage? message) { - if (message == null) + if (message is null) { return Task.CompletedTask.AsAsyncAction(); } @@ -80,7 +80,7 @@ public abstract partial class AppExtensionHost : IExtensionHost try { var vm = StatusMessages.Where(messageVM => messageVM.Model.Unsafe == message).FirstOrDefault(); - if (vm != null) + if (vm is not null) { StatusMessages.Remove(vm); } @@ -113,7 +113,7 @@ public abstract partial class AppExtensionHost : IExtensionHost { // If this message is already in the list of messages, just bring it to the top var oldVm = StatusMessages.Where(messageVM => messageVM.Model.Unsafe == message).FirstOrDefault(); - if (oldVm != null) + if (oldVm is not null) { Task.Factory.StartNew( () => @@ -142,7 +142,7 @@ public abstract partial class AppExtensionHost : IExtensionHost public IAsyncAction ShowStatus(IStatusMessage? message, StatusContext context) { - if (message == null) + if (message is null) { return Task.CompletedTask.AsAsyncAction(); } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/CommandBarViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/CommandBarViewModel.cs index f506c127f2..c01cb13730 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/CommandBarViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/CommandBarViewModel.cs @@ -2,7 +2,6 @@ // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System.Collections.ObjectModel; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Messaging; using Microsoft.CmdPal.Core.ViewModels.Messages; @@ -35,13 +34,13 @@ public partial class CommandBarViewModel : ObservableObject, [NotifyPropertyChangedFor(nameof(HasPrimaryCommand))] public partial CommandItemViewModel? PrimaryCommand { get; set; } - public bool HasPrimaryCommand => PrimaryCommand != null && PrimaryCommand.ShouldBeVisible; + public bool HasPrimaryCommand => PrimaryCommand is not null && PrimaryCommand.ShouldBeVisible; [ObservableProperty] [NotifyPropertyChangedFor(nameof(HasSecondaryCommand))] public partial CommandItemViewModel? SecondaryCommand { get; set; } - public bool HasSecondaryCommand => SecondaryCommand != null; + public bool HasSecondaryCommand => SecondaryCommand is not null; [ObservableProperty] public partial bool ShouldShowContextMenu { get; set; } = false; @@ -58,14 +57,14 @@ public partial class CommandBarViewModel : ObservableObject, private void SetSelectedItem(ICommandBarContext? value) { - if (value != null) + if (value is not null) { PrimaryCommand = value.PrimaryCommand; value.PropertyChanged += SelectedItemPropertyChanged; } else { - if (SelectedItem != null) + if (SelectedItem is not null) { SelectedItem.PropertyChanged -= SelectedItemPropertyChanged; } @@ -88,7 +87,7 @@ public partial class CommandBarViewModel : ObservableObject, private void UpdateContextItems() { - if (SelectedItem == null) + if (SelectedItem is null) { SecondaryCommand = null; ShouldShowContextMenu = false; @@ -127,13 +126,13 @@ public partial class CommandBarViewModel : ObservableObject, public ContextKeybindingResult CheckKeybinding(bool ctrl, bool alt, bool shift, bool win, VirtualKey key) { var keybindings = SelectedItem?.Keybindings(); - if (keybindings != null) + if (keybindings is not null) { // Does the pressed key match any of the keybindings? var pressedKeyChord = KeyChordHelpers.FromModifiers(ctrl, alt, shift, win, key, 0); if (keybindings.TryGetValue(pressedKeyChord, out var matchedItem)) { - return matchedItem != null ? PerformCommand(matchedItem) : ContextKeybindingResult.Unhandled; + return matchedItem is not null ? PerformCommand(matchedItem) : ContextKeybindingResult.Unhandled; } } @@ -142,7 +141,7 @@ public partial class CommandBarViewModel : ObservableObject, private ContextKeybindingResult PerformCommand(CommandItemViewModel? command) { - if (command == null) + if (command is null) { return ContextKeybindingResult.Unhandled; } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/CommandContextItemViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/CommandContextItemViewModel.cs index 60fc815a52..4b25f68e0a 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/CommandContextItemViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/CommandContextItemViewModel.cs @@ -2,11 +2,14 @@ // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System.Diagnostics.CodeAnalysis; using Microsoft.CmdPal.Core.ViewModels.Models; using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; namespace Microsoft.CmdPal.Core.ViewModels; +[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] public partial class CommandContextItemViewModel(ICommandContextItem contextItem, WeakReference context) : CommandItemViewModel(new(contextItem), context), IContextItemViewModel { private readonly KeyChord nullKeyChord = new(0, 0, 0); @@ -17,7 +20,7 @@ public partial class CommandContextItemViewModel(ICommandContextItem contextItem public KeyChord? RequestedShortcut { get; private set; } - public bool HasRequestedShortcut => RequestedShortcut != null && (RequestedShortcut.Value != nullKeyChord); + public bool HasRequestedShortcut => RequestedShortcut is not null && (RequestedShortcut.Value != nullKeyChord); public override void InitializeProperties() { @@ -29,7 +32,7 @@ public partial class CommandContextItemViewModel(ICommandContextItem contextItem base.InitializeProperties(); var contextItem = Model.Unsafe; - if (contextItem == null) + if (contextItem is null) { return; // throw? } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/CommandItemViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/CommandItemViewModel.cs index 1861b53ef7..4f589a4e2f 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/CommandItemViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/CommandItemViewModel.cs @@ -2,6 +2,7 @@ // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System.Diagnostics.CodeAnalysis; using Microsoft.CmdPal.Core.ViewModels.Messages; using Microsoft.CmdPal.Core.ViewModels.Models; using Microsoft.CommandPalette.Extensions; @@ -9,6 +10,7 @@ using Microsoft.CommandPalette.Extensions.Toolkit; namespace Microsoft.CmdPal.Core.ViewModels; +[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBarContext { public ExtensionObject Model => _commandItemModel; @@ -66,7 +68,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa { get { - List l = _defaultCommandContextItem == null ? + List l = _defaultCommandContextItem is null ? new() : [_defaultCommandContextItem]; @@ -98,7 +100,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa } var model = _commandItemModel.Unsafe; - if (model == null) + if (model is null) { return; } @@ -126,7 +128,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa } var model = _commandItemModel.Unsafe; - if (model == null) + if (model is null) { return; } @@ -134,7 +136,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa Command.InitializeProperties(); var listIcon = model.Icon; - if (listIcon != null) + if (listIcon is not null) { _listItemIcon = new(listIcon); _listItemIcon.InitializeProperties(); @@ -170,13 +172,13 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa } var model = _commandItemModel.Unsafe; - if (model == null) + if (model is null) { return; } var more = model.MoreCommands; - if (more != null) + if (more is not null) { MoreCommands = more .Select(item => @@ -298,7 +300,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa protected virtual void FetchProperty(string propertyName) { var model = this._commandItemModel.Unsafe; - if (model == null) + if (model is null) { return; // throw? } @@ -306,7 +308,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa switch (propertyName) { case nameof(Command): - if (Command != null) + if (Command is not null) { Command.PropertyChanged -= Command_PropertyChanged; } @@ -337,7 +339,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa case nameof(model.MoreCommands): var more = model.MoreCommands; - if (more != null) + if (more is not null) { var newContextMenu = more .Select(item => @@ -392,7 +394,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa // Extensions based on Command Palette SDK < 0.3 CommandItem class won't notify when Title changes because Command // or Command.Name change. This is a workaround to ensure that the Title is always up-to-date for extensions with old SDK. var model = _commandItemModel.Unsafe; - if (model != null) + if (model is not null) { _itemTitle = model.Title; } @@ -428,7 +430,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa Command.SafeCleanup(); var model = _commandItemModel.Unsafe; - if (model != null) + if (model is not null) { model.PropChanged -= Model_PropChanged; } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/CommandViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/CommandViewModel.cs index 6e48cef382..30a85045d3 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/CommandViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/CommandViewModel.cs @@ -44,7 +44,7 @@ public partial class CommandViewModel : ExtensionObjectViewModel } var model = Model.Unsafe; - if (model == null) + if (model is null) { return; } @@ -67,13 +67,13 @@ public partial class CommandViewModel : ExtensionObjectViewModel } var model = Model.Unsafe; - if (model == null) + if (model is null) { return; } var ico = model.Icon; - if (ico != null) + if (ico is not null) { Icon = new(ico); Icon.InitializeProperties(); @@ -98,7 +98,7 @@ public partial class CommandViewModel : ExtensionObjectViewModel protected void FetchProperty(string propertyName) { var model = Model.Unsafe; - if (model == null) + if (model is null) { return; // throw? } @@ -125,7 +125,7 @@ public partial class CommandViewModel : ExtensionObjectViewModel Icon = new(null); // necessary? var model = Model.Unsafe; - if (model != null) + if (model is not null) { model.PropChanged -= Model_PropChanged; } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ConfirmResultViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ConfirmResultViewModel.cs index 45cd18f4dd..c653357ccd 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ConfirmResultViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ConfirmResultViewModel.cs @@ -25,7 +25,7 @@ public partial class ConfirmResultViewModel(IConfirmationArgs _args, WeakReferen public override void InitializeProperties() { var model = Model.Unsafe; - if (model == null) + if (model is null) { return; } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ContentPageViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ContentPageViewModel.cs index 7787916de5..0c0f7c7c12 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ContentPageViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ContentPageViewModel.cs @@ -28,7 +28,7 @@ public abstract partial class ContentPageViewModel : PageViewModel, ICommandBarC public DetailsViewModel? Details { get; private set; } [MemberNotNullWhen(true, nameof(Details))] - public bool HasDetails => Details != null; + public bool HasDetails => Details is not null; /////// ICommandBarContext /////// public IEnumerable MoreCommands => Commands.Skip(1); @@ -67,7 +67,7 @@ public abstract partial class ContentPageViewModel : PageViewModel, ICommandBarC foreach (var item in newItems) { var viewModel = ViewModelFromContent(item, PageContext); - if (viewModel != null) + if (viewModel is not null) { viewModel.InitializeProperties(); newContent.Add(viewModel); @@ -104,7 +104,7 @@ public abstract partial class ContentPageViewModel : PageViewModel, ICommandBarC base.InitializeProperties(); var model = _model.Unsafe; - if (model == null) + if (model is null) { return; // throw? } @@ -133,7 +133,7 @@ public abstract partial class ContentPageViewModel : PageViewModel, ICommandBarC }); var extensionDetails = model.Details; - if (extensionDetails != null) + if (extensionDetails is not null) { Details = new(extensionDetails, PageContext); Details.InitializeProperties(); @@ -156,7 +156,7 @@ public abstract partial class ContentPageViewModel : PageViewModel, ICommandBarC base.FetchProperty(propertyName); var model = this._model.Unsafe; - if (model == null) + if (model is null) { return; // throw? } @@ -166,7 +166,7 @@ public abstract partial class ContentPageViewModel : PageViewModel, ICommandBarC case nameof(Commands): var more = model.Commands; - if (more != null) + if (more is not null) { var newContextMenu = more .ToList() @@ -216,7 +216,7 @@ public abstract partial class ContentPageViewModel : PageViewModel, ICommandBarC break; case nameof(Details): var extensionDetails = model.Details; - Details = extensionDetails != null ? new(extensionDetails, PageContext) : null; + Details = extensionDetails is not null ? new(extensionDetails, PageContext) : null; UpdateDetails(); break; } @@ -248,7 +248,7 @@ public abstract partial class ContentPageViewModel : PageViewModel, ICommandBarC [RelayCommand] private void InvokePrimaryCommand(ContentPageViewModel page) { - if (PrimaryCommand != null) + if (PrimaryCommand is not null) { WeakReferenceMessenger.Default.Send(new(PrimaryCommand.Command.Model, PrimaryCommand.Model)); } @@ -258,7 +258,7 @@ public abstract partial class ContentPageViewModel : PageViewModel, ICommandBarC [RelayCommand] private void InvokeSecondaryCommand(ContentPageViewModel page) { - if (SecondaryCommand != null) + if (SecondaryCommand is not null) { WeakReferenceMessenger.Default.Send(new(SecondaryCommand.Command.Model, SecondaryCommand.Model)); } @@ -285,7 +285,7 @@ public abstract partial class ContentPageViewModel : PageViewModel, ICommandBarC Content.Clear(); var model = _model.Unsafe; - if (model != null) + if (model is not null) { model.ItemsChanged -= Model_ItemsChanged; } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ContextMenuViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ContextMenuViewModel.cs index c13d4dbb96..02af0aa67e 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ContextMenuViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ContextMenuViewModel.cs @@ -8,7 +8,6 @@ using CommunityToolkit.Mvvm.Messaging; using Microsoft.CmdPal.Core.ViewModels.Messages; using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions.Toolkit; -using Microsoft.Diagnostics.Utilities; using Windows.System; namespace Microsoft.CmdPal.Core.ViewModels; @@ -51,7 +50,7 @@ public partial class ContextMenuViewModel : ObservableObject, public void UpdateContextItems() { - if (SelectedItem != null) + if (SelectedItem is not null) { if (SelectedItem.MoreCommands.Count() > 1) { @@ -68,14 +67,14 @@ public partial class ContextMenuViewModel : ObservableObject, return; } - if (SelectedItem == null) + if (SelectedItem is null) { return; } _lastSearchText = searchText; - if (CurrentContextMenu == null) + if (CurrentContextMenu is null) { ListHelpers.InPlaceUpdateList(FilteredItems, []); return; @@ -124,7 +123,7 @@ public partial class ContextMenuViewModel : ObservableObject, /// that have a shortcut key set. public Dictionary Keybindings() { - if (CurrentContextMenu == null) + if (CurrentContextMenu is null) { return []; } @@ -140,7 +139,7 @@ public partial class ContextMenuViewModel : ObservableObject, public ContextKeybindingResult? CheckKeybinding(bool ctrl, bool alt, bool shift, bool win, VirtualKey key) { var keybindings = Keybindings(); - if (keybindings != null) + if (keybindings is not null) { // Does the pressed key match any of the keybindings? var pressedKeyChord = KeyChordHelpers.FromModifiers(ctrl, alt, shift, win, key, 0); @@ -190,7 +189,7 @@ public partial class ContextMenuViewModel : ObservableObject, OnPropertyChanging(nameof(CurrentContextMenu)); OnPropertyChanged(nameof(CurrentContextMenu)); - if (CurrentContextMenu != null) + if (CurrentContextMenu is not null) { ListHelpers.InPlaceUpdateList(FilteredItems, [.. CurrentContextMenu!]); } @@ -198,7 +197,7 @@ public partial class ContextMenuViewModel : ObservableObject, public ContextKeybindingResult InvokeCommand(CommandItemViewModel? command) { - if (command == null) + if (command is null) { return ContextKeybindingResult.Unhandled; } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/DetailsCommandsViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/DetailsCommandsViewModel.cs index b85aeaba81..11a67603e9 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/DetailsCommandsViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/DetailsCommandsViewModel.cs @@ -22,7 +22,7 @@ public partial class DetailsCommandsViewModel( { base.InitializeProperties(); var model = _dataModel.Unsafe; - if (model == null) + if (model is null) { return; } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/DetailsElementViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/DetailsElementViewModel.cs index 390459f26c..9739220b65 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/DetailsElementViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/DetailsElementViewModel.cs @@ -16,7 +16,7 @@ public abstract partial class DetailsElementViewModel(IDetailsElement _detailsEl public override void InitializeProperties() { var model = _model.Unsafe; - if (model == null) + if (model is null) { return; } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/DetailsLinkViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/DetailsLinkViewModel.cs index e7aa9b67af..427fcd170e 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/DetailsLinkViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/DetailsLinkViewModel.cs @@ -18,7 +18,7 @@ public partial class DetailsLinkViewModel( public Uri? Link { get; private set; } - public bool IsLink => Link != null; + public bool IsLink => Link is not null; public bool IsText => !IsLink; @@ -26,14 +26,14 @@ public partial class DetailsLinkViewModel( { base.InitializeProperties(); var model = _dataModel.Unsafe; - if (model == null) + if (model is null) { return; } Text = model.Text ?? string.Empty; Link = model.Link; - if (string.IsNullOrEmpty(Text) && Link != null) + if (string.IsNullOrEmpty(Text) && Link is not null) { Text = Link.ToString(); } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/DetailsTagsViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/DetailsTagsViewModel.cs index 803585c1ce..747a0a74c9 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/DetailsTagsViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/DetailsTagsViewModel.cs @@ -22,7 +22,7 @@ public partial class DetailsTagsViewModel( { base.InitializeProperties(); var model = _dataModel.Unsafe; - if (model == null) + if (model is null) { return; } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/DetailsViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/DetailsViewModel.cs index 034e247519..a381cfda6b 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/DetailsViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/DetailsViewModel.cs @@ -26,7 +26,7 @@ public partial class DetailsViewModel(IDetails _details, WeakReference new DetailsTagsViewModel(element, this.PageContext), _ => null, }; - if (vm != null) + if (vm is not null) { vm.InitializeProperties(); Metadata.Add(vm); diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/IContextItemViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/IContextItemViewModel.cs index 704947c3a8..cad1af9d4d 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/IContextItemViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/IContextItemViewModel.cs @@ -4,12 +4,14 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Microsoft.CmdPal.Core.ViewModels; +[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] public interface IContextItemViewModel { } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/IconDataViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/IconDataViewModel.cs index 70b143864c..5f4b4436f2 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/IconDataViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/IconDataViewModel.cs @@ -16,7 +16,7 @@ public partial class IconDataViewModel : ObservableObject, IIconData // If the extension previously gave us a Data, then died, the data will // throw if we actually try to read it, but the pointer itself won't be // null, so this is relatively safe. - public bool HasIcon => !string.IsNullOrEmpty(Icon) || Data.Unsafe != null; + public bool HasIcon => !string.IsNullOrEmpty(Icon) || Data.Unsafe is not null; // Locally cached properties from IIconData. public string Icon { get; private set; } = string.Empty; @@ -36,7 +36,7 @@ public partial class IconDataViewModel : ObservableObject, IIconData public void InitializeProperties() { var model = _model.Unsafe; - if (model == null) + if (model is null) { return; } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/IconInfoViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/IconInfoViewModel.cs index 21ddbe99d9..aebe9b03aa 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/IconInfoViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/IconInfoViewModel.cs @@ -26,7 +26,7 @@ public partial class IconInfoViewModel : ObservableObject, IIconInfo public bool HasIcon(bool light) => IconForTheme(light).HasIcon; - public bool IsSet => _model.Unsafe != null; + public bool IsSet => _model.Unsafe is not null; IIconData? IIconInfo.Dark => Dark; @@ -43,7 +43,7 @@ public partial class IconInfoViewModel : ObservableObject, IIconInfo public void InitializeProperties() { var model = _model.Unsafe; - if (model == null) + if (model is null) { return; } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ListItemViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ListItemViewModel.cs index 682bf4daea..ad1aebe2d1 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ListItemViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ListItemViewModel.cs @@ -27,7 +27,7 @@ public partial class ListItemViewModel(IListItem model, WeakReference Details != null; + public bool HasDetails => Details is not null; public override void InitializeProperties() { @@ -40,7 +40,7 @@ public partial class ListItemViewModel(IListItem model, WeakReference newViewModels = []; + try { + // Check for cancellation before starting expensive operations + cancellationToken.ThrowIfCancellationRequested(); + var newItems = _model.Unsafe!.GetItems(); - // Collect all the items into new viewmodels - Collection newViewModels = []; + // Check for cancellation after getting items from extension + cancellationToken.ThrowIfCancellationRequested(); // TODO we can probably further optimize this by also keeping a // HashSet of every ExtensionObject we currently have, and only // building new viewmodels for the ones we haven't already built. foreach (var item in newItems) { + // Check for cancellation during item processing + cancellationToken.ThrowIfCancellationRequested(); + ListItemViewModel viewModel = new(item, new(this)); // If an item fails to load, silently ignore it. @@ -154,25 +171,57 @@ public partial class ListViewModel : PageViewModel, IDisposable } } + // Check for cancellation before initializing first twenty items + cancellationToken.ThrowIfCancellationRequested(); + var firstTwenty = newViewModels.Take(20); foreach (var item in firstTwenty) { + cancellationToken.ThrowIfCancellationRequested(); item?.SafeInitializeProperties(); } // Cancel any ongoing search _cancellationTokenSource?.Cancel(); + // Check for cancellation before updating the list + cancellationToken.ThrowIfCancellationRequested(); + + List removedItems = []; lock (_listLock) { // Now that we have new ViewModels for everything from the // extension, smartly update our list of VMs - ListHelpers.InPlaceUpdateList(Items, newViewModels); + ListHelpers.InPlaceUpdateList(Items, newViewModels, out removedItems); + + // DO NOT ThrowIfCancellationRequested AFTER THIS! If you do, + // you'll clean up list items that we've now transferred into + // .Items + } + + // If we removed items, we need to clean them up, to remove our event handlers + foreach (var removedItem in removedItems) + { + removedItem.SafeCleanup(); } // TODO: Iterate over everything in Items, and prune items from the // cache if we don't need them anymore } + catch (OperationCanceledException) + { + // Cancellation is expected, don't treat as error + + // However, if we were cancelled, we didn't actually add these items to + // our Items list. Before we release them to the GC, make sure we clean + // them up + foreach (var vm in newViewModels) + { + vm.SafeCleanup(); + } + + return; + } catch (Exception ex) { // TODO: Move this within the for loop, so we can catch issues with individual items @@ -298,11 +347,11 @@ public partial class ListViewModel : PageViewModel, IDisposable [RelayCommand] private void InvokeItem(ListItemViewModel? item) { - if (item != null) + if (item is not null) { WeakReferenceMessenger.Default.Send(new(item.Command.Model, item.Model)); } - else if (ShowEmptyContent && EmptyContent.PrimaryCommand?.Model.Unsafe != null) + else if (ShowEmptyContent && EmptyContent.PrimaryCommand?.Model.Unsafe is not null) { WeakReferenceMessenger.Default.Send(new( EmptyContent.PrimaryCommand.Command.Model, @@ -314,14 +363,14 @@ public partial class ListViewModel : PageViewModel, IDisposable [RelayCommand] private void InvokeSecondaryCommand(ListItemViewModel? item) { - if (item != null) + if (item is not null) { - if (item.SecondaryCommand != null) + if (item.SecondaryCommand is not null) { WeakReferenceMessenger.Default.Send(new(item.SecondaryCommand.Command.Model, item.Model)); } } - else if (ShowEmptyContent && EmptyContent.SecondaryCommand?.Model.Unsafe != null) + else if (ShowEmptyContent && EmptyContent.SecondaryCommand?.Model.Unsafe is not null) { WeakReferenceMessenger.Default.Send(new( EmptyContent.SecondaryCommand.Command.Model, @@ -332,12 +381,12 @@ public partial class ListViewModel : PageViewModel, IDisposable [RelayCommand] private void UpdateSelectedItem(ListItemViewModel? item) { - if (_lastSelectedItem != null) + if (_lastSelectedItem is not null) { _lastSelectedItem.PropertyChanged -= SelectedItemPropertyChanged; } - if (item != null) + if (item is not null) { SetSelectedItem(item); } @@ -383,7 +432,7 @@ public partial class ListViewModel : PageViewModel, IDisposable private void SelectedItemPropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) { var item = _lastSelectedItem; - if (item == null) + if (item is null) { return; } @@ -438,7 +487,7 @@ public partial class ListViewModel : PageViewModel, IDisposable base.InitializeProperties(); var model = _model.Unsafe; - if (model == null) + if (model is null) { return; // throw? } @@ -465,7 +514,7 @@ public partial class ListViewModel : PageViewModel, IDisposable public void LoadMoreIfNeeded() { var model = this._model.Unsafe; - if (model == null) + if (model is null) { return; } @@ -509,7 +558,7 @@ public partial class ListViewModel : PageViewModel, IDisposable base.FetchProperty(propertyName); var model = this._model.Unsafe; - if (model == null) + if (model is null) { return; // throw? } @@ -540,7 +589,7 @@ public partial class ListViewModel : PageViewModel, IDisposable private void UpdateEmptyContent() { UpdateProperty(nameof(ShowEmptyContent)); - if (!ShowEmptyContent || EmptyContent.Model.Unsafe == null) + if (!ShowEmptyContent || EmptyContent.Model.Unsafe is null) { return; } @@ -560,6 +609,10 @@ public partial class ListViewModel : PageViewModel, IDisposable _cancellationTokenSource?.Cancel(); _cancellationTokenSource?.Dispose(); _cancellationTokenSource = null; + + _fetchItemsCancellationTokenSource?.Cancel(); + _fetchItemsCancellationTokenSource?.Dispose(); + _fetchItemsCancellationTokenSource = null; } protected override void UnsafeCleanup() @@ -570,6 +623,7 @@ public partial class ListViewModel : PageViewModel, IDisposable EmptyContent = new(new(null), PageContext); // necessary? _cancellationTokenSource?.Cancel(); + _fetchItemsCancellationTokenSource?.Cancel(); lock (_listLock) { @@ -588,7 +642,7 @@ public partial class ListViewModel : PageViewModel, IDisposable } var model = _model.Unsafe; - if (model != null) + if (model is not null) { model.ItemsChanged -= Model_ItemsChanged; } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/LogMessageViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/LogMessageViewModel.cs index 9ebff20304..969bf60aea 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/LogMessageViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/LogMessageViewModel.cs @@ -22,7 +22,7 @@ public partial class LogMessageViewModel : ExtensionObjectViewModel public override void InitializeProperties() { var model = _model.Unsafe; - if (model == null) + if (model is null) { return; // throw? } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/PageViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/PageViewModel.cs index 552971b96c..046c9fae93 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/PageViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/PageViewModel.cs @@ -5,6 +5,7 @@ using System.Collections.ObjectModel; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; +using Microsoft.CmdPal.Common.Helpers; using Microsoft.CmdPal.Core.ViewModels.Models; using Microsoft.CommandPalette.Extensions; @@ -45,7 +46,7 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext [ObservableProperty] public partial AppExtensionHost ExtensionHost { get; private set; } - public bool HasStatusMessage => MostRecentStatusMessage != null; + public bool HasStatusMessage => MostRecentStatusMessage is not null; [ObservableProperty] [NotifyPropertyChangedFor(nameof(HasStatusMessage))] @@ -132,7 +133,7 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext public override void InitializeProperties() { var page = _pageModel.Unsafe; - if (page == null) + if (page is null) { return; // throw? } @@ -177,7 +178,7 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext protected virtual void FetchProperty(string propertyName) { var model = this._pageModel.Unsafe; - if (model == null) + if (model is null) { return; // throw? } @@ -223,9 +224,10 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext extensionHint ??= ExtensionHost.GetExtensionDisplayName() ?? Title; Task.Factory.StartNew( () => - { - ErrorMessage += $"A bug occurred in {$"the \"{extensionHint}\"" ?? "an unknown's"} extension's code:\n{ex.Message}\n{ex.Source}\n{ex.StackTrace}\n\n"; - }, + { + var message = DiagnosticsHelper.BuildExceptionMessage(ex, extensionHint); + ErrorMessage += message; + }, CancellationToken.None, TaskCreationOptions.None, Scheduler); @@ -240,7 +242,7 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext ExtensionHost.StatusMessages.CollectionChanged -= StatusMessages_CollectionChanged; var model = _pageModel.Unsafe; - if (model != null) + if (model is not null) { model.PropChanged -= Model_PropChanged; } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ProgressViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ProgressViewModel.cs index 40ea290dd4..4ddcfb22e7 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ProgressViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ProgressViewModel.cs @@ -24,7 +24,7 @@ public partial class ProgressViewModel : ExtensionObjectViewModel public override void InitializeProperties() { var model = Model.Unsafe; - if (model == null) + if (model is null) { return; // throw? } @@ -50,7 +50,7 @@ public partial class ProgressViewModel : ExtensionObjectViewModel protected virtual void FetchProperty(string propertyName) { var model = this.Model.Unsafe; - if (model == null) + if (model is null) { return; // throw? } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/SeparatorContextItemViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/SeparatorContextItemViewModel.cs index c6858f490d..8d896bd341 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/SeparatorContextItemViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/SeparatorContextItemViewModel.cs @@ -2,11 +2,13 @@ // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System.Diagnostics.CodeAnalysis; using Microsoft.CmdPal.Core.ViewModels; using Microsoft.CommandPalette.Extensions; namespace Microsoft.CmdPal.Core.ViewModels; +[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] public partial class SeparatorContextItemViewModel() : IContextItemViewModel, ISeparatorContextItem { } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ShellViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ShellViewModel.cs index 3663190f11..6c660d52f2 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ShellViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ShellViewModel.cs @@ -120,7 +120,7 @@ public partial class ShellViewModel : ObservableObject, ////LoadedState = ViewModelLoadedState.Loading; if (!viewModel.IsInitialized - && viewModel.InitializeCommand != null) + && viewModel.InitializeCommand is not null) { _ = Task.Run(async () => { @@ -185,7 +185,7 @@ public partial class ShellViewModel : ObservableObject, private void PerformCommand(PerformCommandMessage message) { var command = message.Command.Unsafe; - if (command == null) + if (command is null) { return; } @@ -205,7 +205,7 @@ public partial class ShellViewModel : ObservableObject, // Construct our ViewModel of the appropriate type and pass it the UI Thread context. var pageViewModel = _pageViewModelFactory.TryCreatePageViewModel(page, _isNested, host); - if (pageViewModel == null) + if (pageViewModel is null) { Logger.LogError($"Failed to create ViewModel for page {page.GetType().Name}"); throw new NotSupportedException(); @@ -240,7 +240,7 @@ public partial class ShellViewModel : ObservableObject, // TODO GH #525 This needs more better locking. lock (_invokeLock) { - if (_handleInvokeTask != null) + if (_handleInvokeTask is not null) { // do nothing - a command is already doing a thing } @@ -280,7 +280,7 @@ public partial class ShellViewModel : ObservableObject, private void UnsafeHandleCommandResult(ICommandResult? result) { - if (result == null) + if (result is null) { // No result, nothing to do. return; diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/StatusMessageViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/StatusMessageViewModel.cs index fb8b333637..2c78ff407e 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/StatusMessageViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/StatusMessageViewModel.cs @@ -17,7 +17,7 @@ public partial class StatusMessageViewModel : ExtensionObjectViewModel public ProgressViewModel? Progress { get; private set; } - public bool HasProgress => Progress != null; + public bool HasProgress => Progress is not null; public StatusMessageViewModel(IStatusMessage message, WeakReference context) : base(context) @@ -28,7 +28,7 @@ public partial class StatusMessageViewModel : ExtensionObjectViewModel public override void InitializeProperties() { var model = Model.Unsafe; - if (model == null) + if (model is null) { return; // throw? } @@ -36,7 +36,7 @@ public partial class StatusMessageViewModel : ExtensionObjectViewModel Message = model.Message; State = model.State; var modelProgress = model.Progress; - if (modelProgress != null) + if (modelProgress is not null) { Progress = new(modelProgress, this.PageContext); Progress.InitializeProperties(); @@ -61,7 +61,7 @@ public partial class StatusMessageViewModel : ExtensionObjectViewModel protected virtual void FetchProperty(string propertyName) { var model = this.Model.Unsafe; - if (model == null) + if (model is null) { return; // throw? } @@ -76,7 +76,7 @@ public partial class StatusMessageViewModel : ExtensionObjectViewModel break; case nameof(Progress): var modelProgress = model.Progress; - if (modelProgress != null) + if (modelProgress is not null) { Progress = new(modelProgress, this.PageContext); Progress.InitializeProperties(); diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/TagViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/TagViewModel.cs index 98ea66f4e8..5287cf441c 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/TagViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/TagViewModel.cs @@ -28,7 +28,7 @@ public partial class TagViewModel(ITag _tag, WeakReference context public override void InitializeProperties() { var model = _tagModel.Unsafe; - if (model == null) + if (model is null) { return; } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/AliasManager.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/AliasManager.cs index 131d633940..642e5ad4a9 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/AliasManager.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/AliasManager.cs @@ -35,7 +35,7 @@ public partial class AliasManager : ObservableObject try { var topLevelCommand = _topLevelCommandManager.LookupCommand(alias.CommandId); - if (topLevelCommand != null) + if (topLevelCommand is not null) { WeakReferenceMessenger.Default.Send(); @@ -88,7 +88,7 @@ public partial class AliasManager : ObservableObject } // If we already have _this exact alias_, do nothing - if (newAlias != null && + if (newAlias is not null && _aliases.TryGetValue(newAlias.SearchPrefix, out var existingAlias)) { if (existingAlias.CommandId == commandId) @@ -113,7 +113,7 @@ public partial class AliasManager : ObservableObject _aliases.Remove(alias.SearchPrefix); } - if (newAlias != null) + if (newAlias is not null) { AddAlias(newAlias); } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/AppStateModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/AppStateModel.cs index dd0cfa817d..3a11c50a59 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/AppStateModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/AppStateModel.cs @@ -55,7 +55,7 @@ public partial class AppStateModel : ObservableObject var loaded = JsonSerializer.Deserialize(jsonContent, JsonSerializationContext.Default.AppStateModel); - Debug.WriteLine(loaded != null ? "Loaded settings file" : "Failed to parse"); + Debug.WriteLine(loaded is not null ? "Loaded settings file" : "Failed to parse"); return loaded ?? new(); } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandProviderWrapper.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandProviderWrapper.cs index 852babe4b7..59903d7ed8 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandProviderWrapper.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandProviderWrapper.cs @@ -15,7 +15,7 @@ namespace Microsoft.CmdPal.UI.ViewModels; public sealed class CommandProviderWrapper { - public bool IsExtension => Extension != null; + public bool IsExtension => Extension is not null; private readonly bool isValid; @@ -188,14 +188,14 @@ public sealed class CommandProviderWrapper return topLevelViewModel; }; - if (commands != null) + if (commands is not null) { TopLevelItems = commands .Select(c => makeAndAdd(c, false)) .ToArray(); } - if (fallbacks != null) + if (fallbacks is not null) { FallbackItems = fallbacks .Select(c => makeAndAdd(c, true)) diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandSettingsViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandSettingsViewModel.cs index 5709a643cb..2c2eafc44c 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandSettingsViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandSettingsViewModel.cs @@ -18,18 +18,18 @@ public partial class CommandSettingsViewModel(ICommandSettings? _unsafeSettings, public bool Initialized { get; private set; } public bool HasSettings => - _model.Unsafe != null && // We have a settings model AND - (!Initialized || SettingsPage != null); // we weren't initialized, OR we were, and we do have a settings page + _model.Unsafe is not null && // We have a settings model AND + (!Initialized || SettingsPage is not null); // we weren't initialized, OR we were, and we do have a settings page private void UnsafeInitializeProperties() { var model = _model.Unsafe; - if (model == null) + if (model is null) { return; } - if (model.SettingsPage != null) + if (model.SettingsPage is not null) { SettingsPage = new CommandPaletteContentPageViewModel(model.SettingsPage, mainThread, provider.ExtensionHost); SettingsPage.InitializeProperties(); diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/CreatedExtensionForm.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/CreatedExtensionForm.cs index 44bcb49cb3..4ab993d84a 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/CreatedExtensionForm.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/CreatedExtensionForm.cs @@ -30,7 +30,7 @@ internal sealed partial class CreatedExtensionForm : NewExtensionFormBase public override ICommandResult SubmitForm(string inputs, string data) { var dataInput = JsonNode.Parse(data)?.AsObject(); - if (dataInput == null) + if (dataInput is null) { return CommandResult.KeepOpen(); } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/LogMessagesPage.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/LogMessagesPage.cs index acb04889fb..90dea58e5c 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/LogMessagesPage.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/LogMessagesPage.cs @@ -23,7 +23,7 @@ public partial class LogMessagesPage : ListPage private void LogMessages_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) { - if (e.Action == NotifyCollectionChangedAction.Add && e.NewItems != null) + if (e.Action == NotifyCollectionChangedAction.Add && e.NewItems is not null) { foreach (var item in e.NewItems) { diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/MainListPage.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/MainListPage.cs index 71c0a4e810..781371a866 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/MainListPage.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/MainListPage.cs @@ -9,6 +9,7 @@ using ManagedCommon; using Microsoft.CmdPal.Common.Helpers; using Microsoft.CmdPal.Core.ViewModels.Messages; using Microsoft.CmdPal.Ext.Apps; +using Microsoft.CmdPal.UI.ViewModels.Messages; using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions.Toolkit; using Microsoft.Extensions.DependencyInjection; @@ -60,6 +61,7 @@ public partial class MainListPage : DynamicListPage, var settings = _serviceProvider.GetService()!; settings.SettingsChanged += SettingsChangedHandler; HotReloadSettings(settings); + _includeApps = _tlcManager.IsProviderActive(AllAppsCommandProvider.WellKnownId); IsLoading = true; } @@ -201,7 +203,7 @@ public partial class MainListPage : DynamicListPage, // If we don't have any previous filter results to work with, start // with a list of all our commands & apps. - if (_filteredItems == null) + if (_filteredItems is null) { _filteredItems = commands; _filteredItemsIncludesApps = _includeApps; diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/NewExtensionForm.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/NewExtensionForm.cs index c1f3f64612..62301714e9 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/NewExtensionForm.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/NewExtensionForm.cs @@ -98,7 +98,7 @@ internal sealed partial class NewExtensionForm : NewExtensionFormBase public override CommandResult SubmitForm(string payload) { var formInput = JsonNode.Parse(payload)?.AsObject(); - if (formInput == null) + if (formInput is null) { return CommandResult.KeepOpen(); } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/NewExtensionPage.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/NewExtensionPage.cs index 8cfa9658d4..6bdb8a7330 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/NewExtensionPage.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/NewExtensionPage.cs @@ -14,7 +14,7 @@ public partial class NewExtensionPage : ContentPage public override IContent[] GetContent() { - return _resultForm != null ? [_resultForm] : [_inputForm]; + return _resultForm is not null ? [_resultForm] : [_inputForm]; } public NewExtensionPage() @@ -28,13 +28,13 @@ public partial class NewExtensionPage : ContentPage private void FormSubmitted(NewExtensionFormBase sender, NewExtensionFormBase? args) { - if (_resultForm != null) + if (_resultForm is not null) { _resultForm.FormSubmitted -= FormSubmitted; } _resultForm = args; - if (_resultForm != null) + if (_resultForm is not null) { _resultForm.FormSubmitted += FormSubmitted; } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/OpenSettingsCommand.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/OpenSettingsCommand.cs index a5af351fd4..ac7fe624e5 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/OpenSettingsCommand.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/OpenSettingsCommand.cs @@ -3,7 +3,7 @@ // See the LICENSE file in the project root for more information. using CommunityToolkit.Mvvm.Messaging; -using Microsoft.CmdPal.Core.ViewModels.Messages; +using Microsoft.CmdPal.UI.Messages; using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions.Toolkit; diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/QuitAction.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/QuitAction.cs index 313685f6f2..bd3cee3159 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/QuitAction.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/QuitAction.cs @@ -3,7 +3,7 @@ // See the LICENSE file in the project root for more information. using CommunityToolkit.Mvvm.Messaging; -using Microsoft.CmdPal.Core.ViewModels.Messages; +using Microsoft.CmdPal.UI.ViewModels.Messages; using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions.Toolkit; diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/ReloadExtensionsCommand.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/ReloadExtensionsCommand.cs index 77efb05a73..88024efe2f 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/ReloadExtensionsCommand.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/ReloadExtensionsCommand.cs @@ -4,6 +4,7 @@ using CommunityToolkit.Mvvm.Messaging; using Microsoft.CmdPal.Core.ViewModels.Messages; +using Microsoft.CmdPal.UI.ViewModels.Messages; using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions.Toolkit; diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ContentFormViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ContentFormViewModel.cs index 9728e8339e..9b2234fb16 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ContentFormViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ContentFormViewModel.cs @@ -2,6 +2,7 @@ // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System.Diagnostics.CodeAnalysis; using System.Text.Json; using AdaptiveCards.ObjectModel.WinUI3; using AdaptiveCards.Templating; @@ -96,109 +97,40 @@ public partial class ContentFormViewModel(IFormContent _form, WeakReference()` - // or similar will throw a System.InvalidCastException. - // - // Instead we have this horror show. - // - // The `action.ToJson()` blob ACTUALLY CONTAINS THE `type` field, which - // we can use to determine what kind of action it is. Then we can parse - // the JSON manually based on the type. - var actionJson = action.ToJson(); - - if (actionJson.TryGetValue("type", out var actionTypeValue)) + if (action is AdaptiveOpenUrlAction openUrlAction) { - var actionTypeString = actionTypeValue.GetString(); - Logger.LogTrace($"atString={actionTypeString}"); - - var actionType = actionTypeString switch - { - "Action.Submit" => ActionType.Submit, - "Action.Execute" => ActionType.Execute, - "Action.OpenUrl" => ActionType.OpenUrl, - _ => ActionType.Unsupported, - }; - - Logger.LogDebug($"{actionTypeString}->{actionType}"); - - switch (actionType) - { - case ActionType.OpenUrl: - { - HandleOpenUrlAction(action, actionJson); - } - - break; - case ActionType.Submit: - case ActionType.Execute: - { - HandleSubmitAction(action, actionJson, inputs); - } - - break; - default: - Logger.LogError($"{actionType} was an unexpected action `type`"); - break; - } - } - else - { - Logger.LogError($"actionJson.TryGetValue(type) failed"); - } - } - - private void HandleOpenUrlAction(IAdaptiveActionElement action, JsonObject actionJson) - { - if (actionJson.TryGetValue("url", out var actionUrlValue)) - { - var actionUrl = actionUrlValue.GetString() ?? string.Empty; - if (Uri.TryCreate(actionUrl, default(UriCreationOptions), out var uri)) - { - WeakReferenceMessenger.Default.Send(new(uri)); - } - else - { - Logger.LogError($"Failed to produce URI for {actionUrlValue}"); - } - } - } - - private void HandleSubmitAction( - IAdaptiveActionElement action, - JsonObject actionJson, - JsonObject inputs) - { - var dataString = string.Empty; - if (actionJson.TryGetValue("data", out var actionDataValue)) - { - dataString = actionDataValue.Stringify() ?? string.Empty; + WeakReferenceMessenger.Default.Send(new(openUrlAction.Url)); + return; } - var inputString = inputs.Stringify(); - _ = Task.Run(() => + if (action is AdaptiveSubmitAction or AdaptiveExecuteAction) { - try + // Get the data and inputs + var dataString = (action as AdaptiveSubmitAction)?.DataJson.Stringify() ?? string.Empty; + var inputString = inputs.Stringify(); + + _ = Task.Run(() => { - var model = _formModel.Unsafe!; - if (model != null) + try { - var result = model.SubmitForm(inputString, dataString); - Logger.LogDebug($"SubmitForm() returned {result}"); - WeakReferenceMessenger.Default.Send(new(new(result))); + var model = _formModel.Unsafe!; + if (model != null) + { + var result = model.SubmitForm(inputString, dataString); + WeakReferenceMessenger.Default.Send(new(new(result))); + } } - } - catch (Exception ex) - { - ShowException(ex); - } - }); + catch (Exception ex) + { + ShowException(ex); + } + }); + } } private static readonly string ErrorCardJson = """ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ContentMarkdownViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ContentMarkdownViewModel.cs index 8ae0935f12..c747bfc231 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ContentMarkdownViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ContentMarkdownViewModel.cs @@ -20,7 +20,7 @@ public partial class ContentMarkdownViewModel(IMarkdownContent _markdown, WeakRe public override void InitializeProperties() { var model = Model.Unsafe; - if (model == null) + if (model is null) { return; } @@ -47,7 +47,7 @@ public partial class ContentMarkdownViewModel(IMarkdownContent _markdown, WeakRe protected void FetchProperty(string propertyName) { var model = Model.Unsafe; - if (model == null) + if (model is null) { return; // throw? } @@ -66,7 +66,7 @@ public partial class ContentMarkdownViewModel(IMarkdownContent _markdown, WeakRe { base.UnsafeCleanup(); var model = Model.Unsafe; - if (model != null) + if (model is not null) { model.PropChanged -= Model_PropChanged; } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ContentTreeViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ContentTreeViewModel.cs index 6368f86ac6..6b6a579207 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ContentTreeViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ContentTreeViewModel.cs @@ -30,13 +30,13 @@ public partial class ContentTreeViewModel(ITreeContent _tree, WeakReference item.Hotkey == null); + _commandHotkeys.RemoveAll(item => item.Hotkey is null); foreach (var item in _commandHotkeys) { diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/Messages/OpenSettingsMessage.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/OpenSettingsMessage.cs similarity index 80% rename from src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/Messages/OpenSettingsMessage.cs rename to src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/OpenSettingsMessage.cs index e4b02b5c0c..c699ab427a 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/Messages/OpenSettingsMessage.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/OpenSettingsMessage.cs @@ -2,7 +2,7 @@ // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace Microsoft.CmdPal.Core.ViewModels.Messages; +namespace Microsoft.CmdPal.UI.Messages; public record OpenSettingsMessage() { diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/Messages/QuitMessage.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/QuitMessage.cs similarity index 87% rename from src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/Messages/QuitMessage.cs rename to src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/QuitMessage.cs index 12b9cec827..ae65782336 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/Messages/QuitMessage.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/QuitMessage.cs @@ -2,7 +2,7 @@ // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace Microsoft.CmdPal.Core.ViewModels.Messages; +namespace Microsoft.CmdPal.UI.ViewModels.Messages; /// /// Message which closes the application. Used by via . diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/Messages/ReloadCommandsMessage.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/ReloadCommandsMessage.cs similarity index 81% rename from src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/Messages/ReloadCommandsMessage.cs rename to src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/ReloadCommandsMessage.cs index a553568f50..cba0fa3f56 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/Messages/ReloadCommandsMessage.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/ReloadCommandsMessage.cs @@ -2,7 +2,7 @@ // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace Microsoft.CmdPal.Core.ViewModels.Messages; +namespace Microsoft.CmdPal.UI.ViewModels.Messages; public record ReloadCommandsMessage() { diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/Messages/UpdateFallbackItemsMessage.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/UpdateFallbackItemsMessage.cs similarity index 81% rename from src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/Messages/UpdateFallbackItemsMessage.cs rename to src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/UpdateFallbackItemsMessage.cs index 8a913f7a3f..08e65c2213 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/Messages/UpdateFallbackItemsMessage.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/UpdateFallbackItemsMessage.cs @@ -2,7 +2,7 @@ // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace Microsoft.CmdPal.Core.ViewModels.Messages; +namespace Microsoft.CmdPal.UI.ViewModels.Messages; public record UpdateFallbackItemsMessage() { diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Models/ExtensionService.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Models/ExtensionService.cs index 364d234f5e..f0a14ab7db 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Models/ExtensionService.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Models/ExtensionService.cs @@ -90,7 +90,7 @@ public partial class ExtensionService : IExtensionService, IDisposable }).Result; var isExtension = isCmdPalExtensionResult.IsExtension; var extension = isCmdPalExtensionResult.Extension; - if (isExtension && extension != null) + if (isExtension && extension is not null) { CommandPaletteHost.Instance.DebugLog($"Installed new extension app {extension.DisplayName}"); @@ -152,7 +152,7 @@ public partial class ExtensionService : IExtensionService, IDisposable { var (cmdPalProvider, classId) = await GetCmdPalExtensionPropertiesAsync(extension); - return new(cmdPalProvider != null && classId.Count != 0, extension); + return new(cmdPalProvider is not null && classId.Count != 0, extension); } } @@ -237,7 +237,7 @@ public partial class ExtensionService : IExtensionService, IDisposable { var (cmdPalProvider, classIds) = await GetCmdPalExtensionPropertiesAsync(extension); - if (cmdPalProvider == null || classIds.Count == 0) + if (cmdPalProvider is null || classIds.Count == 0) { return []; } @@ -352,12 +352,12 @@ public partial class ExtensionService : IExtensionService, IDisposable { var propSetList = new List(); var singlePropertySet = GetSubPropertySet(activationPropSet, CreateInstanceProperty); - if (singlePropertySet != null) + if (singlePropertySet is not null) { var classId = GetProperty(singlePropertySet, ClassIdProperty); // If the instance has a classId as a single string, then it's only supporting a single instance. - if (classId != null) + if (classId is not null) { propSetList.Add(classId); } @@ -365,7 +365,7 @@ public partial class ExtensionService : IExtensionService, IDisposable else { var propertySetArray = GetSubPropertySetArray(activationPropSet, CreateInstanceProperty); - if (propertySetArray != null) + if (propertySetArray is not null) { foreach (var prop in propertySetArray) { @@ -375,7 +375,7 @@ public partial class ExtensionService : IExtensionService, IDisposable } var classId = GetProperty(propertySet, ClassIdProperty); - if (classId != null) + if (classId is not null) { propSetList.Add(classId); } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ProviderSettings.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ProviderSettings.cs index ec33bf4216..1e20040d57 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ProviderSettings.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ProviderSettings.cs @@ -35,7 +35,7 @@ public class ProviderSettings public void Connect(CommandProviderWrapper wrapper) { ProviderId = wrapper.ProviderId; - IsBuiltin = wrapper.Extension == null; + IsBuiltin = wrapper.Extension is null; ProviderDisplayName = wrapper.DisplayName; diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ProviderSettingsViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ProviderSettingsViewModel.cs index 3c8e402364..714b3ca805 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ProviderSettingsViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ProviderSettingsViewModel.cs @@ -7,7 +7,7 @@ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Messaging; using Microsoft.CmdPal.Common.Services; using Microsoft.CmdPal.Core.ViewModels; -using Microsoft.CmdPal.Core.ViewModels.Messages; +using Microsoft.CmdPal.UI.ViewModels.Messages; using Microsoft.CmdPal.UI.ViewModels.Properties; using Microsoft.Extensions.DependencyInjection; @@ -33,7 +33,7 @@ public partial class ProviderSettingsViewModel( Resources.builtin_disabled_extension; [MemberNotNullWhen(true, nameof(Extension))] - public bool IsFromExtension => _provider.Extension != null; + public bool IsFromExtension => _provider.Extension is not null; public IExtensionWrapper? Extension => _provider.Extension; @@ -76,7 +76,7 @@ public partial class ProviderSettingsViewModel( { get { - if (_provider.Settings == null) + if (_provider.Settings is null) { return false; } @@ -100,7 +100,7 @@ public partial class ProviderSettingsViewModel( { get { - if (_provider.Settings == null) + if (_provider.Settings is null) { return null; } @@ -126,7 +126,7 @@ public partial class ProviderSettingsViewModel( { get { - if (field == null) + if (field is null) { field = BuildTopLevelViewModels(); } @@ -149,7 +149,7 @@ public partial class ProviderSettingsViewModel( { get { - if (field == null) + if (field is null) { field = BuildFallbackViewModels(); } @@ -173,7 +173,7 @@ public partial class ProviderSettingsViewModel( private void InitializeSettingsPage() { - if (_provider.Settings == null) + if (_provider.Settings is null) { return; } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/RecentCommandsManager.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/RecentCommandsManager.cs index 8551b5f964..9135c9588a 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/RecentCommandsManager.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/RecentCommandsManager.cs @@ -30,7 +30,7 @@ public partial class RecentCommandsManager : ObservableObject // These numbers are vaguely scaled so that "VS" will make "Visual Studio" the // match after one use. // Usually it has a weight of 84, compared to 109 for the VS cmd prompt - if (entry.Item != null) + if (entry.Item is not null) { var index = entry.Index; @@ -61,7 +61,7 @@ public partial class RecentCommandsManager : ObservableObject var entry = History .Where(item => item.CommandId == commandId) .FirstOrDefault(); - if (entry == null) + if (entry is null) { var newitem = new HistoryItem() { CommandId = commandId, Uses = 1 }; History.Insert(0, newitem); diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsModel.cs index 587a8e4f62..b0d10a5285 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsModel.cs @@ -95,7 +95,7 @@ public partial class SettingsModel : ObservableObject var loaded = JsonSerializer.Deserialize(jsonContent, JsonSerializationContext.Default.SettingsModel); - Debug.WriteLine(loaded != null ? "Loaded settings file" : "Failed to parse"); + Debug.WriteLine(loaded is not null ? "Loaded settings file" : "Failed to parse"); return loaded ?? new(); } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelCommandManager.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelCommandManager.cs index a783a2458a..f55a322792 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelCommandManager.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelCommandManager.cs @@ -12,7 +12,7 @@ using ManagedCommon; using Microsoft.CmdPal.Common.Helpers; using Microsoft.CmdPal.Common.Services; using Microsoft.CmdPal.Core.ViewModels; -using Microsoft.CmdPal.Core.ViewModels.Messages; +using Microsoft.CmdPal.UI.ViewModels.Messages; using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions.Toolkit; using Microsoft.Extensions.DependencyInjection; @@ -151,7 +151,7 @@ public partial class TopLevelCommandManager : ObservableObject, WeakReference weakSelf = new(this); await sender.LoadTopLevelCommands(_serviceProvider, weakSelf); - List newItems = [..sender.TopLevelItems]; + List newItems = [.. sender.TopLevelItems]; foreach (var i in sender.FallbackItems) { if (i.IsEnabled) @@ -170,20 +170,29 @@ public partial class TopLevelCommandManager : ObservableObject, // TODO: just added a lock around all of this anyway, but keeping the clone // while looking on some other ways to improve this; can be removed later. List clone = [.. TopLevelCommands]; - var startIndex = -1; + var startIndex = FindIndexForFirstProviderItem(clone, sender.ProviderId); + clone.RemoveAll(item => item.CommandProviderId == sender.ProviderId); + clone.InsertRange(startIndex, newItems); + + ListHelpers.InPlaceUpdateList(TopLevelCommands, clone); + } + + return; + + static int FindIndexForFirstProviderItem(List topLevelItems, string providerId) + { // Tricky: all Commands from a single provider get added to the // top-level list all together, in a row. So if we find just the first // one, we can slice it out and insert the new ones there. - for (var i = 0; i < clone.Count; i++) + for (var i = 0; i < topLevelItems.Count; i++) { - var wrapper = clone[i]; + var wrapper = topLevelItems[i]; try { - if (sender.ProviderId == wrapper.CommandProviderId) + if (providerId == wrapper.CommandProviderId) { - startIndex = i; - break; + return i; } } catch @@ -191,9 +200,8 @@ public partial class TopLevelCommandManager : ObservableObject, } } - clone.RemoveAll(item => item.CommandProviderId == sender.ProviderId); - clone.InsertRange(startIndex, newItems); - ListHelpers.InPlaceUpdateList(TopLevelCommands, clone); + // If we didn't find any, then we just append the new commands to the end of the list. + return topLevelItems.Count; } } @@ -241,7 +249,7 @@ public partial class TopLevelCommandManager : ObservableObject, _extensionCommandProviders.Clear(); } - if (extensions != null) + if (extensions is not null) { await StartExtensionsAndGetCommands(extensions); } @@ -275,7 +283,7 @@ public partial class TopLevelCommandManager : ObservableObject, var startTasks = extensions.Select(StartExtensionWithTimeoutAsync); // Wait for all extensions to start - var wrappers = (await Task.WhenAll(startTasks)).Where(wrapper => wrapper != null).Select(w => w!).ToList(); + var wrappers = (await Task.WhenAll(startTasks)).Where(wrapper => wrapper is not null).Select(w => w!).ToList(); lock (_commandProvidersLock) { @@ -285,7 +293,7 @@ public partial class TopLevelCommandManager : ObservableObject, // Load the commands from the providers in parallel var loadTasks = wrappers.Select(LoadCommandsWithTimeoutAsync); - var commandSets = (await Task.WhenAll(loadTasks)).Where(results => results != null).Select(r => r!).ToList(); + var commandSets = (await Task.WhenAll(loadTasks)).Where(results => results is not null).Select(r => r!).ToList(); lock (TopLevelCommands) { @@ -402,8 +410,8 @@ public partial class TopLevelCommandManager : ObservableObject, void IPageContext.ShowException(Exception ex, string? extensionHint) { - var errorMessage = $"A bug occurred in {$"the \"{extensionHint}\"" ?? "an unknown's"} extension's code:\n{ex.Message}\n{ex.Source}\n{ex.StackTrace}\n\n"; - CommandPaletteHost.Instance.Log(errorMessage); + var message = DiagnosticsHelper.BuildExceptionMessage(ex, extensionHint ?? "TopLevelCommandManager"); + CommandPaletteHost.Instance.Log(message); } internal bool IsProviderActive(string id) diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelViewModel.cs index f439f0fb84..e73f5b09ba 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelViewModel.cs @@ -8,6 +8,7 @@ using CommunityToolkit.Mvvm.Messaging; using ManagedCommon; using Microsoft.CmdPal.Core.ViewModels; using Microsoft.CmdPal.Core.ViewModels.Messages; +using Microsoft.CmdPal.UI.ViewModels.Messages; using Microsoft.CmdPal.UI.ViewModels.Settings; using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions.Toolkit; @@ -239,7 +240,7 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem private void FetchAliasFromAliasManager() { var am = _serviceProvider.GetService(); - if (am != null) + if (am is not null) { var commandAlias = am.AliasFromId(Id); if (commandAlias is not null) @@ -253,7 +254,7 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem private void UpdateHotkey() { var hotkey = _settings.CommandHotkeys.Where(hk => hk.CommandId == Id).FirstOrDefault(); - if (hotkey != null) + if (hotkey is not null) { _hotkey = hotkey.Hotkey; } @@ -263,12 +264,12 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem { List tags = []; - if (Hotkey != null) + if (Hotkey is not null) { tags.Add(new Tag() { Text = Hotkey.ToString() }); } - if (Alias != null) + if (Alias is not null) { tags.Add(new Tag() { Text = Alias.SearchPrefix }); } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs index 03b40bf8d8..ec3604621d 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs @@ -64,6 +64,9 @@ public partial class App : Application this.InitializeComponent(); + // Ensure types used in XAML are preserved for AOT compilation + TypePreservation.PreserveTypes(); + NativeEventWaiter.WaitForEventLoop( "Local\\PowerToysCmdPal-ExitEvent-eb73f6be-3f22-4b36-aee3-62924ba40bfd", () => { diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CommandBar.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CommandBar.xaml index 9fb047641f..49fef61ecb 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CommandBar.xaml +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CommandBar.xaml @@ -47,7 +47,8 @@ + Opened="ContextMenuFlyout_Opened" + ShouldConstrainToRootBounds="False"> @@ -59,8 +60,24 @@ + + + + + @@ -155,12 +172,7 @@ Style="{StaticResource CaptionTextBlockStyle}" Text="{x:Bind ViewModel.PrimaryCommand.Name, Mode=OneWay}" /> - + @@ -179,19 +191,10 @@ Text="{x:Bind ViewModel.SecondaryCommand.Name, Mode=OneWay}" /> - + - + @@ -199,7 +202,7 @@ -internal sealed partial class PackageRepository : ListRepository, IProgramRepository +internal sealed partial class PackageRepository : ListRepository, IProgramRepository { private readonly IPackageCatalog _packageCatalog; diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Storage/Win32ProgramRepository.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Storage/Win32ProgramRepository.cs index 6fdb5e49f6..98da723743 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Storage/Win32ProgramRepository.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Storage/Win32ProgramRepository.cs @@ -10,7 +10,6 @@ using System.Diagnostics.CodeAnalysis; using System.IO; using System.Threading.Tasks; using ManagedCommon; -using Microsoft.CmdPal.Ext.Apps.Programs; using Win32Program = Microsoft.CmdPal.Ext.Apps.Programs.Win32Program; namespace Microsoft.CmdPal.Ext.Apps.Storage; @@ -53,7 +52,7 @@ internal sealed partial class Win32ProgramRepository : ListRepository(T* comPtr) where T : unmanaged { - if (comPtr != null) + if (comPtr is not null) { ((IUnknown*)comPtr)->Release(); } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Utils/ShellLinkHelper.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Utils/ShellLinkHelper.cs index 543abf5dcf..11652c7524 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Utils/ShellLinkHelper.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Utils/ShellLinkHelper.cs @@ -3,8 +3,6 @@ // See the LICENSE file in the project root for more information. using System; -using System.Text; -using ManagedCommon; using Windows.Win32; using Windows.Win32.Foundation; using Windows.Win32.System.Com; @@ -37,7 +35,7 @@ public class ShellLinkHelper : IShellLinkHelper IPersistFile* persistFile = null; Guid iid = typeof(IPersistFile).GUID; ((IUnknown*)link)->QueryInterface(&iid, (void**)&persistFile); - if (persistFile != null) + if (persistFile is not null) { using var persistFileHandle = new SafeComHandle((IntPtr)persistFile); try diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Utils/ShellLocalization.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Utils/ShellLocalization.cs index 87e152b7e0..c157847861 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Utils/ShellLocalization.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Utils/ShellLocalization.cs @@ -45,7 +45,7 @@ public class ShellLocalization var filename = ComFreeHelper.GetStringAndFree(hr, filenamePtr); - if (filename == null) + if (filename is null) { return string.Empty; } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Utils/ThemeHelper.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Utils/ThemeHelper.cs index 8175667d0a..005f1962f6 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Utils/ThemeHelper.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Utils/ThemeHelper.cs @@ -32,7 +32,7 @@ public static class ThemeHelper // Retrieve the registry value, which is a DWORD (0 or 1) var registryValueObj = Registry.GetValue(registryKey, registryValue, null); - if (registryValueObj != null) + if (registryValueObj is not null) { // 0 = Dark mode, 1 = Light mode var isLightMode = Convert.ToBoolean((int)registryValueObj, CultureInfo.InvariantCulture); diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/AddBookmarkForm.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/AddBookmarkForm.cs index c5f0b4ea3e..93fc6d8d01 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/AddBookmarkForm.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/AddBookmarkForm.cs @@ -63,7 +63,7 @@ internal sealed partial class AddBookmarkForm : FormContent public override CommandResult SubmitForm(string payload) { var formInput = JsonNode.Parse(payload); - if (formInput == null) + if (formInput is null) { return CommandResult.GoHome(); } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/BookmarkJsonParser.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/BookmarkJsonParser.cs new file mode 100644 index 0000000000..7cc82c9c02 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/BookmarkJsonParser.cs @@ -0,0 +1,45 @@ +// 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.Collections.Generic; +using System.Text.Json; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.Bookmarks; + +public class BookmarkJsonParser +{ + public BookmarkJsonParser() + { + } + + public Bookmarks ParseBookmarks(string json) + { + if (string.IsNullOrWhiteSpace(json)) + { + return new Bookmarks(); + } + + try + { + var bookmarks = JsonSerializer.Deserialize(json, BookmarkSerializationContext.Default.Bookmarks); + return bookmarks ?? new Bookmarks(); + } + catch (JsonException ex) + { + ExtensionHost.LogMessage($"parse bookmark data failed. ex: {ex.Message}"); + return new Bookmarks(); + } + } + + public string SerializeBookmarks(Bookmarks? bookmarks) + { + if (bookmarks == null) + { + return string.Empty; + } + + return JsonSerializer.Serialize(bookmarks, BookmarkSerializationContext.Default.Bookmarks); + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/BookmarkPlaceholderForm.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/BookmarkPlaceholderForm.cs index 4aac3e600e..965f42d1b0 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/BookmarkPlaceholderForm.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/BookmarkPlaceholderForm.cs @@ -73,7 +73,7 @@ internal sealed partial class BookmarkPlaceholderForm : FormContent // parse the submitted JSON and then open the link var formInput = JsonNode.Parse(payload); var formObject = formInput?.AsObject(); - if (formObject == null) + if (formObject is null) { return CommandResult.GoHome(); } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/Bookmarks.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/Bookmarks.cs index 8f2e257782..b02eb54e0f 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/Bookmarks.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/Bookmarks.cs @@ -11,34 +11,4 @@ namespace Microsoft.CmdPal.Ext.Bookmarks; public sealed class Bookmarks { public List Data { get; set; } = []; - - private static readonly JsonSerializerOptions _jsonOptions = new() - { - IncludeFields = true, - }; - - public static Bookmarks ReadFromFile(string path) - { - var data = new Bookmarks(); - - // if the file exists, load it and append the new item - if (File.Exists(path)) - { - var jsonStringReading = File.ReadAllText(path); - - if (!string.IsNullOrEmpty(jsonStringReading)) - { - data = JsonSerializer.Deserialize(jsonStringReading, BookmarkSerializationContext.Default.Bookmarks) ?? new Bookmarks(); - } - } - - return data; - } - - public static void WriteToFile(string path, Bookmarks data) - { - var jsonString = JsonSerializer.Serialize(data, BookmarkSerializationContext.Default.Bookmarks); - - File.WriteAllText(BookmarksCommandProvider.StateJsonPath(), jsonString); - } } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/BookmarksCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/BookmarksCommandProvider.cs index 91d9f902cb..1174685729 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/BookmarksCommandProvider.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/BookmarksCommandProvider.cs @@ -20,10 +20,20 @@ public partial class BookmarksCommandProvider : CommandProvider private readonly AddBookmarkPage _addNewCommand = new(null); + private readonly IBookmarkDataSource _dataSource; + private readonly BookmarkJsonParser _parser; private Bookmarks? _bookmarks; public BookmarksCommandProvider() + : this(new FileBookmarkDataSource(StateJsonPath())) { + } + + internal BookmarksCommandProvider(IBookmarkDataSource dataSource) + { + _dataSource = dataSource; + _parser = new BookmarkJsonParser(); + Id = "Bookmarks"; DisplayName = Resources.bookmarks_display_name; Icon = Icons.PinIcon; @@ -49,10 +59,14 @@ public partial class BookmarksCommandProvider : CommandProvider private void SaveAndUpdateCommands() { - if (_bookmarks != null) + try { - var jsonPath = BookmarksCommandProvider.StateJsonPath(); - Bookmarks.WriteToFile(jsonPath, _bookmarks); + var jsonData = _parser.SerializeBookmarks(_bookmarks); + _dataSource.SaveBookmarkData(jsonData); + } + catch (Exception ex) + { + Logger.LogError($"Failed to save bookmarks: {ex.Message}"); } LoadCommands(); @@ -64,12 +78,12 @@ public partial class BookmarksCommandProvider : CommandProvider List collected = []; collected.Add(new CommandItem(_addNewCommand)); - if (_bookmarks == null) + if (_bookmarks is null) { LoadBookmarksFromFile(); } - if (_bookmarks != null) + if (_bookmarks is not null) { collected.AddRange(_bookmarks.Data.Select(BookmarkToCommandItem)); } @@ -82,18 +96,15 @@ public partial class BookmarksCommandProvider : CommandProvider { try { - var jsonFile = StateJsonPath(); - if (File.Exists(jsonFile)) - { - _bookmarks = Bookmarks.ReadFromFile(jsonFile); - } + var jsonData = _dataSource.GetBookmarkData(); + _bookmarks = _parser.ParseBookmarks(jsonData); } catch (Exception ex) { Logger.LogError(ex.Message); } - if (_bookmarks == null) + if (_bookmarks is null) { _bookmarks = new(); } @@ -134,7 +145,7 @@ public partial class BookmarksCommandProvider : CommandProvider name: Resources.bookmarks_delete_name, action: () => { - if (_bookmarks != null) + if (_bookmarks is not null) { ExtensionHost.LogMessage($"Deleting bookmark ({bookmark.Name},{bookmark.Bookmark})"); diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/FileBookmarkDataSource.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/FileBookmarkDataSource.cs new file mode 100644 index 0000000000..a87859c3ce --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/FileBookmarkDataSource.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.IO; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.Bookmarks; + +public class FileBookmarkDataSource : IBookmarkDataSource +{ + private readonly string _filePath; + + public FileBookmarkDataSource(string filePath) + { + _filePath = filePath; + } + + public string GetBookmarkData() + { + if (!File.Exists(_filePath)) + { + return string.Empty; + } + + try + { + return File.ReadAllText(_filePath); + } + catch (Exception ex) + { + ExtensionHost.LogMessage($"Read bookmark data failed. ex: {ex.Message}"); + return string.Empty; + } + } + + public void SaveBookmarkData(string jsonData) + { + try + { + File.WriteAllText(_filePath, jsonData); + } + catch (Exception ex) + { + ExtensionHost.LogMessage($"Failed to save bookmark data: {ex}"); + } + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/IBookmarkDataSource.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/IBookmarkDataSource.cs new file mode 100644 index 0000000000..7ed936a1c7 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/IBookmarkDataSource.cs @@ -0,0 +1,11 @@ +// 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. +namespace Microsoft.CmdPal.Ext.Bookmarks; + +public interface IBookmarkDataSource +{ + string GetBookmarkData(); + + void SaveBookmarkData(string jsonData); +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/Properties/AssemblyInfo.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..a74d97eeca --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/Properties/AssemblyInfo.cs @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Microsoft.CmdPal.Ext.Bookmarks.UnitTests")] diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/UrlCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/UrlCommand.cs index d94f4619f1..db60a31940 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/UrlCommand.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/UrlCommand.cs @@ -73,7 +73,7 @@ public partial class UrlCommand : InvokableCommand if (string.IsNullOrEmpty(args)) { var uri = GetUri(exe); - if (uri != null) + if (uri is not null) { _ = Launcher.LaunchUriAsync(uri); } @@ -109,7 +109,7 @@ public partial class UrlCommand : InvokableCommand // First, try to get the icon from the thumbnail helper // This works for local files and folders icon = await MaybeGetIconForPath(target); - if (icon != null) + if (icon is not null) { return icon; } @@ -142,7 +142,7 @@ public partial class UrlCommand : InvokableCommand { // If the executable exists, try to get the icon from the file icon = await MaybeGetIconForPath(fullExePath); - if (icon != null) + if (icon is not null) { return icon; } @@ -154,7 +154,7 @@ public partial class UrlCommand : InvokableCommand try { var uri = GetUri(baseString); - if (uri != null) + if (uri is not null) { var hostname = uri.Host; var faviconUrl = $"{uri.Scheme}://{hostname}/favicon.ico"; @@ -176,7 +176,7 @@ public partial class UrlCommand : InvokableCommand try { var stream = await ThumbnailHelper.GetThumbnail(target); - if (stream != null) + if (stream is not null) { var data = new IconData(RandomAccessStreamReference.CreateFromStream(stream)); return new IconInfo(data, data); diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/CalculatorCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/CalculatorCommandProvider.cs index 0f4a8b0d9b..cdf0ccfa47 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/CalculatorCommandProvider.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/CalculatorCommandProvider.cs @@ -12,21 +12,21 @@ namespace Microsoft.CmdPal.Ext.Calc; public partial class CalculatorCommandProvider : CommandProvider { + private static ISettingsInterface settings = new SettingsManager(); private readonly ListItem _listItem = new(new CalculatorListPage(settings)) { Subtitle = Resources.calculator_top_level_subtitle, - MoreCommands = [new CommandContextItem(settings.Settings.SettingsPage)], + MoreCommands = [new CommandContextItem(((SettingsManager)settings).Settings.SettingsPage)], }; private readonly FallbackCalculatorItem _fallback = new(settings); - private static SettingsManager settings = new(); public CalculatorCommandProvider() { Id = "Calculator"; DisplayName = Resources.calculator_display_name; Icon = Icons.CalculatorIcon; - Settings = settings.Settings; + Settings = ((SettingsManager)settings).Settings; } public override ICommandItem[] TopLevelCommands() => [_listItem]; diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/CalculateEngine.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/CalculateEngine.cs index ca2e1d1ea8..a927e07499 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/CalculateEngine.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/CalculateEngine.cs @@ -34,7 +34,7 @@ public static class CalculateEngine /// Interpret /// /// Use CultureInfo.CurrentCulture if something is user facing - public static CalculateResult Interpret(SettingsManager settings, string input, CultureInfo cultureInfo, out string error) + public static CalculateResult Interpret(ISettingsInterface settings, string input, CultureInfo cultureInfo, out string error) { error = default; diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/ISettingsInterface.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/ISettingsInterface.cs new file mode 100644 index 0000000000..f4b7a50644 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/ISettingsInterface.cs @@ -0,0 +1,18 @@ +// 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.CmdPal.Ext.Calc.Helper; + +namespace Microsoft.CmdPal.Ext.Calc.Helper; + +public interface ISettingsInterface +{ + public CalculateEngine.TrigMode TrigUnit { get; } + + public bool InputUseEnglishFormat { get; } + + public bool OutputUseEnglishFormat { get; } + + public bool CloseOnEnter { get; } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/NumberTranslator.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/NumberTranslator.cs index 7331c44b40..34da2872cf 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/NumberTranslator.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/NumberTranslator.cs @@ -107,7 +107,7 @@ public class NumberTranslator // Currently, we only convert base literals (hexadecimal, binary, octal) to decimal. var converted = ConvertBaseLiteral(token, cultureTo); - if (converted != null) + if (converted is not null) { outputBuilder.Append(converted); continue; diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/QueryHelper.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/QueryHelper.cs index b6c41f3831..99f782d714 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/QueryHelper.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/QueryHelper.cs @@ -12,7 +12,7 @@ namespace Microsoft.CmdPal.Ext.Calc.Helper; public static partial class QueryHelper { - public static ListItem Query(string query, SettingsManager settings, bool isFallbackSearch, TypedEventHandler handleSave = null) + public static ListItem Query(string query, ISettingsInterface settings, bool isFallbackSearch, TypedEventHandler handleSave = null) { ArgumentNullException.ThrowIfNull(query); if (!isFallbackSearch) diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/ResultHelper.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/ResultHelper.cs index c729086543..cd2b811567 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/ResultHelper.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/ResultHelper.cs @@ -13,10 +13,10 @@ namespace Microsoft.CmdPal.Ext.Calc.Helper; public static class ResultHelper { - public static ListItem CreateResult(decimal? roundedResult, CultureInfo inputCulture, CultureInfo outputCulture, string query, SettingsManager settings, TypedEventHandler handleSave) + public static ListItem CreateResult(decimal? roundedResult, CultureInfo inputCulture, CultureInfo outputCulture, string query, ISettingsInterface settings, TypedEventHandler handleSave) { // Return null when the expression is not a valid calculator query. - if (roundedResult == null) + if (roundedResult is null) { return null; } @@ -48,7 +48,7 @@ public static class ResultHelper public static ListItem CreateResult(decimal? roundedResult, CultureInfo inputCulture, CultureInfo outputCulture, string query) { // Return null when the expression is not a valid calculator query. - if (roundedResult == null) + if (roundedResult is null) { return null; } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/SettingsManager.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/SettingsManager.cs index cb5104011e..cea59e170f 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/SettingsManager.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/SettingsManager.cs @@ -8,7 +8,7 @@ using Microsoft.CommandPalette.Extensions.Toolkit; namespace Microsoft.CmdPal.Ext.Calc.Helper; -public class SettingsManager : JsonSettingsManager +public class SettingsManager : JsonSettingsManager, ISettingsInterface { private static readonly string _namespace = "calculator"; diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Pages/CalculatorListPage.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Pages/CalculatorListPage.cs index 4b0cf29d64..72e5f3db30 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Pages/CalculatorListPage.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Pages/CalculatorListPage.cs @@ -23,7 +23,7 @@ namespace Microsoft.CmdPal.Ext.Calc.Pages; public sealed partial class CalculatorListPage : DynamicListPage { private readonly Lock _resultsLock = new(); - private readonly SettingsManager _settingsManager; + private readonly ISettingsInterface _settingsManager; private readonly List _items = []; private readonly List history = []; private readonly ListItem _emptyItem; @@ -32,7 +32,7 @@ public sealed partial class CalculatorListPage : DynamicListPage // We need to avoid the double calculation. This may cause some wierd behaviors. private string skipQuerySearchText = string.Empty; - public CalculatorListPage(SettingsManager settings) + public CalculatorListPage(ISettingsInterface settings) { _settingsManager = settings; Icon = Icons.CalculatorIcon; @@ -82,7 +82,7 @@ public sealed partial class CalculatorListPage : DynamicListPage { this._items.Clear(); - if (result != null) + if (result is not null) { this._items.Add(result); } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Pages/FallbackCalculatorItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Pages/FallbackCalculatorItem.cs index 10d305bb7c..4367c67810 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Pages/FallbackCalculatorItem.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Pages/FallbackCalculatorItem.cs @@ -11,9 +11,9 @@ namespace Microsoft.CmdPal.Ext.Calc.Pages; public sealed partial class FallbackCalculatorItem : FallbackCommandItem { private readonly CopyTextCommand _copyCommand = new(string.Empty); - private readonly SettingsManager _settings; + private readonly ISettingsInterface _settings; - public FallbackCalculatorItem(SettingsManager settings) + public FallbackCalculatorItem(ISettingsInterface settings) : base(new NoOpCommand(), Resources.calculator_title) { Command = _copyCommand; @@ -28,7 +28,7 @@ public sealed partial class FallbackCalculatorItem : FallbackCommandItem { var result = QueryHelper.Query(query, _settings, true, null); - if (result == null) + if (result is null) { _copyCommand.Text = string.Empty; _copyCommand.Name = string.Empty; diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Assets/ClipboardHistory.png b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Assets/ClipboardHistory.png new file mode 100644 index 0000000000..2dbdeb30ac Binary files /dev/null and b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Assets/ClipboardHistory.png differ diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Assets/ClipboardHistory.svg b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Assets/ClipboardHistory.svg new file mode 100644 index 0000000000..3abe18e1a4 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Assets/ClipboardHistory.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/ClipboardHelper.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/ClipboardHelper.cs index 066d8822f3..87937c8100 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/ClipboardHelper.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/ClipboardHelper.cs @@ -139,7 +139,7 @@ internal static class ClipboardHelper switch (clipboardFormat) { case ClipboardFormat.Text: - if (clipboardItem.Content == null) + if (clipboardItem.Content is null) { ExtensionHost.LogMessage(new LogMessage() { Message = "No valid clipboard content" }); return; @@ -152,7 +152,7 @@ internal static class ClipboardHelper break; case ClipboardFormat.Image: - if (clipboardItem.ImageData == null) + if (clipboardItem.ImageData is null) { ExtensionHost.LogMessage(new LogMessage() { Message = "No valid clipboard content" }); return; @@ -240,7 +240,7 @@ internal static class ClipboardHelper internal static async Task GetClipboardImageContentAsync(DataPackageView clipboardData) { using var stream = await GetClipboardImageStreamAsync(clipboardData); - if (stream != null) + if (stream is not null) { var decoder = await BitmapDecoder.CreateAsync(stream); return await decoder.GetSoftwareBitmapAsync(); @@ -255,7 +255,7 @@ internal static class ClipboardHelper { var storageItems = await clipboardData.GetStorageItemsAsync(); var file = storageItems.Count == 1 ? storageItems[0] as StorageFile : null; - if (file != null) + if (file is not null) { return await file.OpenReadAsync(); } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Icons.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Icons.cs index 38fbc06d07..be7533d569 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Icons.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Icons.cs @@ -14,5 +14,5 @@ internal sealed class Icons internal static IconInfo PasteIcon { get; } = new("\uE77F"); - internal static IconInfo ClipboardListIcon { get; } = new("\uF0E3"); + internal static IconInfo ClipboardListIcon { get; } = IconHelpers.FromRelativePath("Assets\\ClipboardHistory.svg"); } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Microsoft.CmdPal.Ext.ClipboardHistory.csproj b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Microsoft.CmdPal.Ext.ClipboardHistory.csproj index 7bc3bd65af..dc2ee202df 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Microsoft.CmdPal.Ext.ClipboardHistory.csproj +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Microsoft.CmdPal.Ext.ClipboardHistory.csproj @@ -30,4 +30,13 @@ PublicResXFileCodeGenerator + + + + PreserveNewest + + + PreserveNewest + + diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Models/ClipboardItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Models/ClipboardItem.cs index 94c5a86dc3..ec01883b87 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Models/ClipboardItem.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Models/ClipboardItem.cs @@ -37,7 +37,7 @@ public class ClipboardItem } [MemberNotNullWhen(true, nameof(ImageData))] - private bool IsImage => ImageData != null; + private bool IsImage => ImageData is not null; [MemberNotNullWhen(true, nameof(Content))] private bool IsText => !string.IsNullOrEmpty(Content); diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Pages/ClipboardHistoryListPage.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Pages/ClipboardHistoryListPage.cs index b8cccb987b..b6b72afba3 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Pages/ClipboardHistoryListPage.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Pages/ClipboardHistoryListPage.cs @@ -54,7 +54,7 @@ internal sealed partial class ClipboardHistoryListPage : ListPage try { var allowClipboardHistory = Registry.GetValue(registryKey, "AllowClipboardHistory", null); - return allowClipboardHistory != null ? (int)allowClipboardHistory == 0 : false; + return allowClipboardHistory is not null ? (int)allowClipboardHistory == 0 : false; } catch (Exception) { @@ -100,7 +100,7 @@ internal sealed partial class ClipboardHistoryListPage : ListPage { var imageReceived = await item.Item.Content.GetBitmapAsync(); - if (imageReceived != null) + if (imageReceived is not null) { item.ImageData = imageReceived; } @@ -141,7 +141,7 @@ internal sealed partial class ClipboardHistoryListPage : ListPage for (var i = 0; i < clipboardHistory.Count; i++) { var item = clipboardHistory[i]; - if (item != null) + if (item is not null) { listItems.Add(item.ToListItem()); } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/FallbackOpenFileItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/FallbackOpenFileItem.cs index 7da8702f1e..2b408f24dc 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/FallbackOpenFileItem.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/FallbackOpenFileItem.cs @@ -46,7 +46,7 @@ internal sealed partial class FallbackOpenFileItem : FallbackCommandItem, System return; } - if (_suppressCallback != null && _suppressCallback(query)) + if (_suppressCallback is not null && _suppressCallback(query)) { Command = new NoOpCommand(); Title = string.Empty; @@ -71,7 +71,7 @@ internal sealed partial class FallbackOpenFileItem : FallbackCommandItem, System try { var stream = ThumbnailHelper.GetThumbnail(item.FullPath).Result; - if (stream != null) + if (stream is not null) { var data = new IconData(RandomAccessStreamReference.CreateFromStream(stream)); Icon = new IconInfo(data, data); @@ -92,7 +92,7 @@ internal sealed partial class FallbackOpenFileItem : FallbackCommandItem, System _searchEngine.Query(query, _queryCookie); var results = _searchEngine.FetchItems(0, 20, _queryCookie, out var _); - if (results.Count == 0 || ((results[0] as IndexerListItem) == null)) + if (results.Count == 0 || ((results[0] as IndexerListItem) is null)) { // Exit 2: We searched for the file, and found nothing. Oh well. // Hide ourselves. diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Indexer/DataSourceManager.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Indexer/DataSourceManager.cs index 7b9f9bd45b..1e2119f0ab 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Indexer/DataSourceManager.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Indexer/DataSourceManager.cs @@ -16,7 +16,7 @@ internal static class DataSourceManager public static IDBInitialize GetDataSource() { - if (_dataSource == null) + if (_dataSource is null) { InitializeDataSource(); } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Indexer/SearchQuery.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Indexer/SearchQuery.cs index 8fa972f302..6b85834bb8 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Indexer/SearchQuery.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Indexer/SearchQuery.cs @@ -12,7 +12,6 @@ using ManagedCsWin32; using Microsoft.CmdPal.Ext.Indexer.Indexer.OleDB; using Microsoft.CmdPal.Ext.Indexer.Indexer.SystemSearch; using Microsoft.CmdPal.Ext.Indexer.Indexer.Utils; -using static Microsoft.CmdPal.Ext.Indexer.Indexer.Utils.NativeHelpers; namespace Microsoft.CmdPal.Ext.Indexer.Indexer; @@ -54,14 +53,14 @@ internal sealed partial class SearchQuery : IDisposable try { queryTpTimer = new Timer(QueryTimerCallback, this, Timeout.Infinite, Timeout.Infinite); - if (queryTpTimer == null) + if (queryTpTimer is null) { Logger.LogError("Failed to create query timer"); return; } queryCompletedEvent = new EventWaitHandle(false, EventResetMode.ManualReset); - if (queryCompletedEvent == null) + if (queryCompletedEvent is null) { Logger.LogError("Failed to create query completed event"); return; @@ -85,7 +84,7 @@ internal sealed partial class SearchQuery : IDisposable // Are we currently doing work? If so, let's cancel lock (_lockObject) { - if (queryTpTimer != null) + if (queryTpTimer is not null) { queryTpTimer.Change(Timeout.Infinite, Timeout.Infinite); queryTpTimer.Dispose(); @@ -117,7 +116,7 @@ internal sealed partial class SearchQuery : IDisposable try { // We need to generate a search query string with the search text the user entered above - if (currentRowset != null) + if (currentRowset is not null) { // We have a previous rowset, this means the user is typing and we should store this // recapture the where ID from this so the next ExecuteSync call will be faster @@ -146,14 +145,14 @@ internal sealed partial class SearchQuery : IDisposable { getRow.GetRowFromHROW(null, rowHandle, ref Unsafe.AsRef(in IID.IPropertyStore), out var propertyStore); - if (propertyStore == null) + if (propertyStore is null) { Logger.LogError("Failed to get IPropertyStore interface"); return false; } var searchResult = SearchResult.Create(propertyStore); - if (searchResult == null) + if (searchResult is null) { Logger.LogError("Failed to create search result"); return false; @@ -171,7 +170,7 @@ internal sealed partial class SearchQuery : IDisposable public bool FetchRows(int offset, int limit) { - if (currentRowset == null) + if (currentRowset is null) { Logger.LogError("No rowset to fetch rows from"); return false; @@ -241,7 +240,7 @@ internal sealed partial class SearchQuery : IDisposable { var queryStr = QueryStringBuilder.GeneratePrimingQuery(); var rowset = ExecuteCommand(queryStr); - if (rowset != null) + if (rowset is not null) { reuseRowset = rowset; reuseWhereID = GetReuseWhereId(reuseRowset); @@ -261,7 +260,7 @@ internal sealed partial class SearchQuery : IDisposable var guid = typeof(IDBCreateCommand).GUID; session.CreateSession(IntPtr.Zero, ref guid, out var ppDBSession); - if (ppDBSession == null) + if (ppDBSession is null) { Logger.LogError("CreateSession failed"); return null; @@ -271,7 +270,7 @@ internal sealed partial class SearchQuery : IDisposable guid = typeof(ICommandText).GUID; createCommand.CreateCommand(IntPtr.Zero, ref guid, out ICommandText commandText); - if (commandText == null) + if (commandText is null) { Logger.LogError("Failed to get ICommandText interface"); return null; @@ -342,13 +341,13 @@ internal sealed partial class SearchQuery : IDisposable { var rowsetInfo = (IRowsetInfo)rowset; - if (rowsetInfo == null) + if (rowsetInfo is null) { return 0; } var prop = GetPropset(rowsetInfo); - if (prop == null) + if (prop is null) { return 0; } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Indexer/SearchResult.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Indexer/SearchResult.cs index b44e9ab11b..fa2cf31704 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Indexer/SearchResult.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Indexer/SearchResult.cs @@ -26,7 +26,7 @@ internal sealed class SearchResult ItemUrl = url; IsFolder = isFolder; - if (LaunchUri == null || LaunchUri.Length == 0) + if (LaunchUri is null || LaunchUri.Length == 0) { // Launch the file with the default app, so use the file path LaunchUri = filePath; diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Indexer/Utils/QueryStringBuilder.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Indexer/Utils/QueryStringBuilder.cs index 997d364b4d..068ea08750 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Indexer/Utils/QueryStringBuilder.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Indexer/Utils/QueryStringBuilder.cs @@ -5,8 +5,6 @@ using System; using System.Globalization; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Runtime.InteropServices.Marshalling; using ManagedCommon; using ManagedCsWin32; using Microsoft.CmdPal.Ext.Indexer.Indexer.SystemSearch; @@ -28,7 +26,7 @@ internal sealed partial class QueryStringBuilder public static string GenerateQuery(string searchText, uint whereId) { - if (queryHelper == null) + if (queryHelper is null) { ISearchManager searchManager; @@ -43,13 +41,13 @@ internal sealed partial class QueryStringBuilder } ISearchCatalogManager catalogManager = searchManager.GetCatalog(SystemIndex); - if (catalogManager == null) + if (catalogManager is null) { throw new ArgumentException($"Failed to get catalog manager for {SystemIndex}"); } queryHelper = catalogManager.GetQueryHelper(); - if (queryHelper == null) + if (queryHelper is null) { throw new ArgumentException("Failed to get query helper from catalog manager"); } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Pages/ActionsListContextItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Pages/ActionsListContextItem.cs index 0c2823fed5..9f37f94c1b 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Pages/ActionsListContextItem.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Pages/ActionsListContextItem.cs @@ -44,12 +44,12 @@ internal sealed partial class ActionsListContextItem : CommandContextItem, IDisp { lock (UpdateMoreCommandsLock) { - if (actionRuntime == null) + if (actionRuntime is null) { actionRuntime = ActionRuntimeManager.InstanceAsync.GetAwaiter().GetResult(); } - if (actionRuntime == null) + if (actionRuntime is null) { return; } @@ -62,7 +62,7 @@ internal sealed partial class ActionsListContextItem : CommandContextItem, IDisp { var extension = System.IO.Path.GetExtension(fullPath).ToLower(CultureInfo.InvariantCulture); ActionEntity entity = null; - if (extension != null) + if (extension is not null) { if (extension == ".jpg" || extension == ".jpeg" || extension == ".png") { @@ -74,7 +74,7 @@ internal sealed partial class ActionsListContextItem : CommandContextItem, IDisp } } - if (entity == null) + if (entity is null) { entity = actionRuntime.EntityFactory.CreateFileEntity(fullPath); } @@ -100,7 +100,7 @@ internal sealed partial class ActionsListContextItem : CommandContextItem, IDisp { lock (UpdateMoreCommandsLock) { - if (actionRuntime != null) + if (actionRuntime is not null) { actionRuntime.ActionCatalog.Changed -= ActionCatalog_Changed; } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Pages/DirectoryExplorePage.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Pages/DirectoryExplorePage.cs index 9bb7820a07..3bdbe1b0ce 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Pages/DirectoryExplorePage.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Pages/DirectoryExplorePage.cs @@ -35,14 +35,14 @@ public sealed partial class DirectoryExplorePage : DynamicListPage public override void UpdateSearchText(string oldSearch, string newSearch) { - if (_directoryContents == null) + if (_directoryContents is null) { return; } if (string.IsNullOrEmpty(newSearch)) { - if (_filteredContents != null) + if (_filteredContents is not null) { _filteredContents = null; RaiseItemsChanged(-1); @@ -58,7 +58,7 @@ public sealed partial class DirectoryExplorePage : DynamicListPage newSearch, (s, i) => ListHelpers.ScoreListItem(s, i)); - if (_filteredContents != null) + if (_filteredContents is not null) { lock (_filteredContents) { @@ -75,12 +75,12 @@ public sealed partial class DirectoryExplorePage : DynamicListPage public override IListItem[] GetItems() { - if (_filteredContents != null) + if (_filteredContents is not null) { return _filteredContents.ToArray(); } - if (_directoryContents != null) + if (_directoryContents is not null) { return _directoryContents.ToArray(); } @@ -120,7 +120,7 @@ public sealed partial class DirectoryExplorePage : DynamicListPage try { var stream = ThumbnailHelper.GetThumbnail(item.FilePath).Result; - if (stream != null) + if (stream is not null) { var data = new IconData(RandomAccessStreamReference.CreateFromStream(stream)); icon = new IconInfo(data, data); diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Pages/DirectoryPage.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Pages/DirectoryPage.cs index a6f989c41e..91e78d87fd 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Pages/DirectoryPage.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Pages/DirectoryPage.cs @@ -31,7 +31,7 @@ public sealed partial class DirectoryPage : ListPage public override IListItem[] GetItems() { - if (_directoryContents != null) + if (_directoryContents is not null) { return _directoryContents.ToArray(); } @@ -86,7 +86,7 @@ public sealed partial class DirectoryPage : ListPage try { var stream = ThumbnailHelper.GetThumbnail(item.FilePath).Result; - if (stream != null) + if (stream is not null) { var data = new IconData(RandomAccessStreamReference.CreateFromStream(stream)); icon = new IconInfo(data, data); diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Pages/IndexerPage.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Pages/IndexerPage.cs index a62f03295a..f03452effb 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Pages/IndexerPage.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Pages/IndexerPage.cs @@ -21,6 +21,8 @@ internal sealed partial class IndexerPage : DynamicListPage, IDisposable private string initialQuery = string.Empty; + private bool _isEmptyQuery = true; + public IndexerPage() { Id = "com.microsoft.indexer.fileSearch"; @@ -43,15 +45,19 @@ internal sealed partial class IndexerPage : DynamicListPage, IDisposable disposeSearchEngine = false; } + public override ICommandItem EmptyContent => GetEmptyContent(); + public override void UpdateSearchText(string oldSearch, string newSearch) { if (oldSearch != newSearch && newSearch != initialQuery) { _ = Task.Run(() => { + _isEmptyQuery = string.IsNullOrWhiteSpace(newSearch); Query(newSearch); LoadMore(); - initialQuery = string.Empty; + OnPropertyChanged(nameof(EmptyContent)); + initialQuery = null; }); } } @@ -68,6 +74,16 @@ internal sealed partial class IndexerPage : DynamicListPage, IDisposable RaiseItemsChanged(_indexerListItems.Count); } + private CommandItem GetEmptyContent() + { + return new CommandItem(new NoOpCommand()) + { + Icon = Icon, + Title = _isEmptyQuery ? Resources.Indexer_Subtitle : Resources.Indexer_NoResultsMessage, + Subtitle = Resources.Indexer_NoResultsMessageTip, + }; + } + private void Query(string query) { ++_queryCookie; diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Properties/Resources.Designer.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Properties/Resources.Designer.cs index f5d1ba2d61..a78488a7f1 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Properties/Resources.Designer.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Properties/Resources.Designer.cs @@ -61,7 +61,7 @@ namespace Microsoft.CmdPal.Ext.Indexer.Properties { } /// - /// Looks up a localized string similar to Actions. + /// Looks up a localized string similar to Actions.... /// internal static string Indexer_Command_Actions { get { @@ -177,6 +177,24 @@ namespace Microsoft.CmdPal.Ext.Indexer.Properties { } } + /// + /// Looks up a localized string similar to No items found. + /// + internal static string Indexer_NoResultsMessage { + get { + return ResourceManager.GetString("Indexer_NoResultsMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Tip: Improve your search result using filters like in Windows Explorer. (For example: type:directory). + /// + internal static string Indexer_NoResultsMessageTip { + get { + return ResourceManager.GetString("Indexer_NoResultsMessageTip", resourceCulture); + } + } + /// /// Looks up a localized string similar to Search for files and folders.... /// diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Properties/Resources.resx b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Properties/Resources.resx index 61d51998b2..bbe8f0bd31 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Properties/Resources.resx +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Properties/Resources.resx @@ -180,4 +180,10 @@ Search for "{0}" in files + + No items found + + + Tip: Refine your search using filters, just like in File Explorer (e.g., type:directory). + \ No newline at end of file diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/SearchEngine.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/SearchEngine.cs index 4d77de679a..eb1ca563b4 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/SearchEngine.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/SearchEngine.cs @@ -42,7 +42,7 @@ public sealed partial class SearchEngine : IDisposable { hasMore = false; var results = new List(); - if (_searchQuery != null) + if (_searchQuery is not null) { var cookie = _searchQuery.Cookie; if (cookie == queryCookie) @@ -59,7 +59,7 @@ public sealed partial class SearchEngine : IDisposable try { var stream = ThumbnailHelper.GetThumbnail(result.LaunchUri).Result; - if (stream != null) + if (stream is not null) { var data = new IconData(RandomAccessStreamReference.CreateFromStream(stream)); icon = new IconInfo(data, data); diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Registry/Helpers/ISettingsInterface.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Registry/Helpers/ISettingsInterface.cs new file mode 100644 index 0000000000..bec1fb3271 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Registry/Helpers/ISettingsInterface.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.CmdPal.Ext.Registry.Helpers; + +public interface ISettingsInterface +{ + // Add registry-specific settings methods here if needed + // For now, this can be empty if there are no settings for Registry +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Registry/Helpers/RegistryHelper.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Registry/Helpers/RegistryHelper.cs index 9cd30b2bea..b64894baaf 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Registry/Helpers/RegistryHelper.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Registry/Helpers/RegistryHelper.cs @@ -7,7 +7,6 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using System.Linq; -using ManagedCommon; using Microsoft.CmdPal.Ext.Registry.Classes; using Microsoft.CmdPal.Ext.Registry.Constants; using Microsoft.CmdPal.Ext.Registry.Properties; @@ -118,7 +117,7 @@ internal static class RegistryHelper subKey = result.First().Key; } - if (result.Count > 1 || subKey == null) + if (result.Count > 1 || subKey is null) { break; } @@ -183,7 +182,7 @@ internal static class RegistryHelper if (string.Equals(subKey, searchSubKey, StringComparison.OrdinalIgnoreCase)) { var key = parentKey.OpenSubKey(subKey, RegistryKeyPermissionCheck.ReadSubTree); - if (key != null) + if (key is not null) { list.Add(new RegistryEntry(key)); } @@ -194,7 +193,7 @@ internal static class RegistryHelper try { var key = parentKey.OpenSubKey(subKey, RegistryKeyPermissionCheck.ReadSubTree); - if (key != null) + if (key is not null) { list.Add(new RegistryEntry(key)); } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Registry/Helpers/ResultHelper.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Registry/Helpers/ResultHelper.cs index c05af5f594..0ac3159531 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Registry/Helpers/ResultHelper.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Registry/Helpers/ResultHelper.cs @@ -88,7 +88,7 @@ internal static class ResultHelper foreach (var valueName in valueNames) { var value = key.GetValue(valueName); - if (value != null) + if (value is not null) { valueList.Add(KeyValuePair.Create(valueName, value)); } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Registry/Helpers/SettingsManager.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Registry/Helpers/SettingsManager.cs new file mode 100644 index 0000000000..aaf5d2cce0 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Registry/Helpers/SettingsManager.cs @@ -0,0 +1,37 @@ +// 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.IO; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.Registry.Helpers; + +public class SettingsManager : JsonSettingsManager, ISettingsInterface +{ + private static readonly string _namespace = "registry"; + + private static string Namespaced(string propertyName) => $"{_namespace}.{propertyName}"; + + internal static string SettingsJsonPath() + { + var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal"); + Directory.CreateDirectory(directory); + + // now, the state is just next to the exe + return Path.Combine(directory, "settings.json"); + } + + public SettingsManager() + { + FilePath = SettingsJsonPath(); + + // Add settings here when needed + // Settings.Add(setting); + + // Load settings from file upon initialization + LoadSettings(); + + Settings.SettingsChanged += (s, a) => this.SaveSettings(); + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Registry/Helpers/ValueHelper.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Registry/Helpers/ValueHelper.cs index e0e6eaf951..f25bc56064 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Registry/Helpers/ValueHelper.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Registry/Helpers/ValueHelper.cs @@ -26,7 +26,7 @@ internal static class ValueHelper { var unformattedValue = key.GetValue(valueName); - if (unformattedValue == null) + if (unformattedValue is null) { throw new InvalidOperationException($"Cannot proceed when {nameof(unformattedValue)} is null."); } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Registry/Pages/RegistryListPage.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Registry/Pages/RegistryListPage.cs index fbc80d5d1e..b37f0bc313 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Registry/Pages/RegistryListPage.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Registry/Pages/RegistryListPage.cs @@ -18,12 +18,14 @@ internal sealed partial class RegistryListPage : DynamicListPage public static IconInfo RegistryIcon { get; } = new("\uE74C"); // OEM private readonly CommandItem _emptyMessage; + private readonly ISettingsInterface _settingsManager; - public RegistryListPage() + public RegistryListPage(ISettingsInterface settingsManager) { Icon = Icons.RegistryIcon; Name = Title = Resources.Registry_Page_Title; Id = "com.microsoft.cmdpal.registry"; + _settingsManager = settingsManager; _emptyMessage = new CommandItem() { Icon = Icons.RegistryIcon, diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Registry/RegistryCommandsProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Registry/RegistryCommandsProvider.cs index 3f4218c81b..22eca4cc3f 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Registry/RegistryCommandsProvider.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Registry/RegistryCommandsProvider.cs @@ -2,6 +2,7 @@ // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using Microsoft.CmdPal.Ext.Registry.Helpers; using Microsoft.CmdPal.Ext.Registry.Properties; using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions.Toolkit; @@ -10,6 +11,8 @@ namespace Microsoft.CmdPal.Ext.Registry; public partial class RegistryCommandsProvider : CommandProvider { + private static readonly ISettingsInterface _settingsManager = new SettingsManager(); + public RegistryCommandsProvider() { Id = "Windows.Registry"; @@ -20,7 +23,7 @@ public partial class RegistryCommandsProvider : CommandProvider public override ICommandItem[] TopLevelCommands() { return [ - new CommandItem(new RegistryListPage()) + new CommandItem(new RegistryListPage(_settingsManager)) { Title = "Registry", Subtitle = "Navigate the Windows registry", diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Commands/ExecuteItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Commands/ExecuteItem.cs index 5058b386b3..4ca772dc1e 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Commands/ExecuteItem.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Commands/ExecuteItem.cs @@ -46,7 +46,7 @@ internal sealed partial class ExecuteItem : InvokableCommand private void Execute(Func startProcess, ProcessStartInfo info) { - if (startProcess == null) + if (startProcess is null) { return; } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/FallbackExecuteItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/FallbackExecuteItem.cs index 167956c166..79be63cd65 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/FallbackExecuteItem.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/FallbackExecuteItem.cs @@ -4,6 +4,7 @@ using System; using System.IO; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.CmdPal.Ext.Shell.Helpers; @@ -15,6 +16,8 @@ namespace Microsoft.CmdPal.Ext.Shell; internal sealed partial class FallbackExecuteItem : FallbackCommandItem, IDisposable { + private static readonly char[] _systemDirectoryRoots = ['\\', '/']; + private readonly Action? _addToHistory; private CancellationTokenSource? _cancellationTokenSource; private Task? _currentUpdateTask; @@ -80,8 +83,8 @@ internal sealed partial class FallbackExecuteItem : FallbackCommandItem, IDispos cancellationToken.ThrowIfCancellationRequested(); var searchText = query.Trim(); - var expanded = Environment.ExpandEnvironmentVariables(searchText); - searchText = expanded; + Expand(ref searchText); + if (string.IsNullOrEmpty(searchText) || string.IsNullOrWhiteSpace(searchText)) { Command = null; @@ -184,8 +187,8 @@ internal sealed partial class FallbackExecuteItem : FallbackCommandItem, IDispos internal static bool SuppressFileFallbackIf(string query) { var searchText = query.Trim(); - var expanded = Environment.ExpandEnvironmentVariables(searchText); - searchText = expanded; + Expand(ref searchText); + if (string.IsNullOrEmpty(searchText) || string.IsNullOrWhiteSpace(searchText)) { return false; @@ -197,4 +200,57 @@ internal sealed partial class FallbackExecuteItem : FallbackCommandItem, IDispos return exeExists || pathIsDir; } + + private static void Expand(ref string searchText) + { + if (searchText.Length == 0) + { + return; + } + + var singleCharQuery = searchText.Length == 1; + + searchText = Environment.ExpandEnvironmentVariables(searchText); + + if (!TryExpandHome(ref searchText)) + { + TryExpandRoot(ref searchText); + } + } + + private static bool TryExpandHome(ref string searchText) + { + if (searchText[0] == '~') + { + var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + + if (searchText.Length == 1) + { + searchText = home; + } + else if (_systemDirectoryRoots.Contains(searchText[1])) + { + searchText = Path.Combine(home, searchText[2..]); + } + + return true; + } + + return false; + } + + private static bool TryExpandRoot(ref string searchText) + { + if (_systemDirectoryRoots.Contains(searchText[0]) && (searchText.Length == 1 || !_systemDirectoryRoots.Contains(searchText[1]))) + { + var root = Path.GetPathRoot(Environment.SystemDirectory); + if (root != null) + { + searchText = searchText.Length == 1 ? root : Path.Combine(root, searchText[1..]); + return true; + } + } + + return false; + } } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Helpers/ShellListPageHelpers.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Helpers/ShellListPageHelpers.cs index acf739cdbf..6a545c7225 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Helpers/ShellListPageHelpers.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Helpers/ShellListPageHelpers.cs @@ -126,7 +126,7 @@ public class ShellListPageHelpers return null; } - if (li != null) + if (li is not null) { li.TextToSuggest = searchText; } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Pages/RunExeItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Pages/RunExeItem.cs index ea98f2fe47..a8d578939e 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Pages/RunExeItem.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Pages/RunExeItem.cs @@ -78,7 +78,7 @@ internal sealed partial class RunExeItem : ListItem try { var stream = await ThumbnailHelper.GetThumbnail(FullExePath); - if (stream != null) + if (stream is not null) { var data = new IconData(RandomAccessStreamReference.CreateFromStream(stream)); icon = new IconInfo(data, data); diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Pages/ShellListPage.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Pages/ShellListPage.cs index 8ecfe091c1..4b99477d2a 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Pages/ShellListPage.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Pages/ShellListPage.cs @@ -245,14 +245,14 @@ internal sealed partial class ShellListPage : DynamicListPage, IDisposable var histItemsNotInSearch = _historyItems .Where(kv => !kv.Key.Equals(newSearch, StringComparison.OrdinalIgnoreCase)); - if (_exeItem != null) + if (_exeItem is not null) { // If we have an exe item, we want to remove it from the history items histItemsNotInSearch = histItemsNotInSearch .Where(kv => !kv.Value.Title.Equals(_exeItem.Title, StringComparison.OrdinalIgnoreCase)); } - if (_uriItem != null) + if (_uriItem is not null) { // If we have an uri item, we want to remove it from the history items histItemsNotInSearch = histItemsNotInSearch @@ -307,8 +307,8 @@ internal sealed partial class ShellListPage : DynamicListPage, IDisposable } var filteredTopLevel = ListHelpers.FilterList(_topLevelItems, SearchText); - List uriItems = _uriItem != null ? [_uriItem] : []; - List exeItems = _exeItem != null ? [_exeItem] : []; + List uriItems = _uriItem is not null ? [_uriItem] : []; + List exeItems = _exeItem is not null ? [_exeItem] : []; return exeItems @@ -459,7 +459,7 @@ internal sealed partial class ShellListPage : DynamicListPage, IDisposable var hist = _historyService.GetRunHistory(); var histItems = hist .Select(h => (h, ShellListPageHelpers.ListItemForCommandString(h, AddToHistory))) - .Where(tuple => tuple.Item2 != null) + .Where(tuple => tuple.Item2 is not null) .Select(tuple => (tuple.h, tuple.Item2!)) .ToList(); _historyItems.Clear(); diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/PathListItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/PathListItem.cs index 2e1dd38349..18f37818e6 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/PathListItem.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/PathListItem.cs @@ -72,7 +72,7 @@ internal sealed partial class PathListItem : ListItem _icon = new Lazy(() => { var iconStream = ThumbnailHelper.GetThumbnail(path).Result; - var icon = iconStream != null ? IconInfo.FromStream(iconStream) : + var icon = iconStream is not null ? IconInfo.FromStream(iconStream) : _isDirectory ? Icons.FolderIcon : Icons.RunV2Icon; return icon; }); diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/FallbackSystemCommandItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/FallbackSystemCommandItem.cs index 6cb6e7a3ec..6914c00626 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/FallbackSystemCommandItem.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/FallbackSystemCommandItem.cs @@ -57,7 +57,7 @@ internal sealed partial class FallbackSystemCommandItem : FallbackCommandItem } } - if (result == null) + if (result is null) { Command = null; Title = string.Empty; diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Helpers/NetworkConnectionProperties.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Helpers/NetworkConnectionProperties.cs index 486eeaa8b5..69e5c2ac78 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Helpers/NetworkConnectionProperties.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Helpers/NetworkConnectionProperties.cs @@ -155,7 +155,7 @@ internal sealed class NetworkConnectionProperties internal static List GetList() { var interfaces = NetworkInterface.GetAllNetworkInterfaces() - .Where(x => x.NetworkInterfaceType != NetworkInterfaceType.Loopback && x.GetPhysicalAddress() != null) + .Where(x => x.NetworkInterfaceType != NetworkInterfaceType.Loopback && x.GetPhysicalAddress() is not null) .Select(i => new NetworkConnectionProperties(i)) .OrderByDescending(i => i.IPv4) // list IPv4 first .ThenBy(i => i.IPv6Primary) // then IPv6 @@ -195,9 +195,9 @@ internal sealed class NetworkConnectionProperties CreateIpInfoForDetailsText($"**{Resources.Microsoft_plugin_sys_Ip6Site}:**\n\n* ", IPv6SiteLocal) + CreateIpInfoForDetailsText($"**{Resources.Microsoft_plugin_sys_Ip6Unique}:**\n\n* ", IPv6UniqueLocal) + CreateIpInfoForDetailsText($"**{Resources.Microsoft_plugin_sys_Gateways}:**\n\n* ", Gateways) + - CreateIpInfoForDetailsText($"**{Resources.Microsoft_plugin_sys_Dhcp}:**\n\n* ", DhcpServers == null ? string.Empty : DhcpServers) + - CreateIpInfoForDetailsText($"**{Resources.Microsoft_plugin_sys_Dns}:**\n\n* ", DnsServers == null ? string.Empty : DnsServers) + - CreateIpInfoForDetailsText($"**{Resources.Microsoft_plugin_sys_Wins}:**\n\n* ", WinsServers == null ? string.Empty : WinsServers) + + CreateIpInfoForDetailsText($"**{Resources.Microsoft_plugin_sys_Dhcp}:**\n\n* ", DhcpServers is null ? string.Empty : DhcpServers) + + CreateIpInfoForDetailsText($"**{Resources.Microsoft_plugin_sys_Dns}:**\n\n* ", DnsServers is null ? string.Empty : DnsServers) + + CreateIpInfoForDetailsText($"**{Resources.Microsoft_plugin_sys_Wins}:**\n\n* ", WinsServers is null ? string.Empty : WinsServers) + $"\n\n**{Resources.Microsoft_plugin_sys_AdapterName}:** {Adapter}" + $"\n\n**{Resources.Microsoft_plugin_sys_PhysicalAddress}:** {PhysicalAddress}" + $"\n\n**{Resources.Microsoft_plugin_sys_Speed}:** {GetFormattedSpeedValue(Speed)}"; diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/FallbackTimeDateItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/FallbackTimeDateItem.cs index 3b797e4cfb..57749d3b8d 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/FallbackTimeDateItem.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/FallbackTimeDateItem.cs @@ -4,11 +4,9 @@ using System; using System.Collections.Generic; -using System.Drawing; using System.Globalization; using System.Linq; using Microsoft.CmdPal.Ext.TimeDate.Helpers; -using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions.Toolkit; namespace Microsoft.CmdPal.Ext.TimeDate; @@ -16,10 +14,10 @@ namespace Microsoft.CmdPal.Ext.TimeDate; internal sealed partial class FallbackTimeDateItem : FallbackCommandItem { private readonly HashSet _validOptions; - private SettingsManager _settingsManager; + private ISettingsInterface _settingsManager; private DateTime? _timestamp; - public FallbackTimeDateItem(SettingsManager settings, DateTime? timestamp = null) + public FallbackTimeDateItem(ISettingsInterface settings, DateTime? timestamp = null) : base(new NoOpCommand(), Resources.Microsoft_plugin_timedate_fallback_display_title) { Title = string.Empty; @@ -66,7 +64,7 @@ internal sealed partial class FallbackTimeDateItem : FallbackCommandItem } } - if (result != null) + if (result is not null) { Title = result.Title; Subtitle = result.Subtitle; @@ -90,7 +88,7 @@ internal sealed partial class FallbackTimeDateItem : FallbackCommandItem foreach (var option in _validOptions) { - if (option == null) + if (option is null) { continue; } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Helpers/AvailableResultsList.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Helpers/AvailableResultsList.cs index 38366345c4..5666ff6fa3 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Helpers/AvailableResultsList.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Helpers/AvailableResultsList.cs @@ -22,14 +22,14 @@ internal static class AvailableResultsList /// Required for UnitTest: Use custom first week of the year instead of the plugin setting. /// Required for UnitTest: Use custom first day of the week instead the plugin setting. /// List of results - internal static List GetList(bool isKeywordSearch, SettingsManager settings, bool? timeLongFormat = null, bool? dateLongFormat = null, DateTime? timestamp = null, CalendarWeekRule? firstWeekOfYear = null, DayOfWeek? firstDayOfWeek = null) + internal static List GetList(bool isKeywordSearch, ISettingsInterface settings, bool? timeLongFormat = null, bool? dateLongFormat = null, DateTime? timestamp = null, CalendarWeekRule? firstWeekOfYear = null, DayOfWeek? firstDayOfWeek = null) { var results = new List(); var calendar = CultureInfo.CurrentCulture.Calendar; var timeExtended = timeLongFormat ?? settings.TimeWithSecond; var dateExtended = dateLongFormat ?? settings.DateWithWeekday; - var isSystemDateTime = timestamp == null; + var isSystemDateTime = timestamp is null; var dateTimeNow = timestamp ?? DateTime.Now; var dateTimeNowUtc = dateTimeNow.ToUniversalTime(); var firstWeekRule = firstWeekOfYear ?? TimeAndDateHelper.GetCalendarWeekRule(settings.FirstWeekOfYear); diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Helpers/ISettingsInterface.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Helpers/ISettingsInterface.cs new file mode 100644 index 0000000000..12e53ccf11 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Helpers/ISettingsInterface.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.CmdPal.Ext.TimeDate.Helpers; + +public interface ISettingsInterface +{ + public int FirstWeekOfYear { get; } + + public int FirstDayOfWeek { get; } + + public bool EnableFallbackItems { get; } + + public bool TimeWithSecond { get; } + + public bool DateWithWeekday { get; } + + public List CustomFormats { get; } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Helpers/SettingsManager.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Helpers/SettingsManager.cs index 7b351fe3b8..4bd8bf4d7c 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Helpers/SettingsManager.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Helpers/SettingsManager.cs @@ -11,7 +11,7 @@ using Microsoft.CommandPalette.Extensions.Toolkit; namespace Microsoft.CmdPal.Ext.TimeDate.Helpers; -public class SettingsManager : JsonSettingsManager +public class SettingsManager : JsonSettingsManager, ISettingsInterface { // Line break character used in WinUI3 TextBox and TextBlock. private const char TEXTBOXNEWLINE = '\r'; @@ -103,7 +103,7 @@ public class SettingsManager : JsonSettingsManager { get { - if (_firstWeekOfYear.Value == null || string.IsNullOrEmpty(_firstWeekOfYear.Value)) + if (_firstWeekOfYear.Value is null || string.IsNullOrEmpty(_firstWeekOfYear.Value)) { return -1; } @@ -123,7 +123,7 @@ public class SettingsManager : JsonSettingsManager { get { - if (_firstDayOfWeek.Value == null || string.IsNullOrEmpty(_firstDayOfWeek.Value)) + if (_firstDayOfWeek.Value is null || string.IsNullOrEmpty(_firstDayOfWeek.Value)) { return -1; } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Helpers/TimeDateCalculator.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Helpers/TimeDateCalculator.cs index 38f417ad5b..6128ef56ad 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Helpers/TimeDateCalculator.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Helpers/TimeDateCalculator.cs @@ -27,7 +27,7 @@ public sealed partial class TimeDateCalculator /// /// Search query object /// List of Wox s. - public static List ExecuteSearch(SettingsManager settings, string query) + public static List ExecuteSearch(ISettingsInterface settings, string query) { var isEmptySearchInput = string.IsNullOrWhiteSpace(query); List availableFormats = new List(); diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Pages/TimeDateExtensionPage.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Pages/TimeDateExtensionPage.cs index 4eb95034b7..36eb39461f 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Pages/TimeDateExtensionPage.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Pages/TimeDateExtensionPage.cs @@ -19,9 +19,9 @@ internal sealed partial class TimeDateExtensionPage : DynamicListPage private IList _results = new List(); private bool _dataLoaded; - private SettingsManager _settingsManager; + private ISettingsInterface _settingsManager; - public TimeDateExtensionPage(SettingsManager settingsManager) + public TimeDateExtensionPage(ISettingsInterface settingsManager) { Icon = Icons.TimeDateExtIcon; Title = Resources.Microsoft_plugin_timedate_main_page_title; diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/TimeDateCommandsProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/TimeDateCommandsProvider.cs index d29356fa77..26bd4d8453 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/TimeDateCommandsProvider.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/TimeDateCommandsProvider.cs @@ -15,7 +15,7 @@ namespace Microsoft.CmdPal.Ext.TimeDate; public partial class TimeDateCommandsProvider : CommandProvider { private readonly CommandItem _command; - private static readonly SettingsManager _settingsManager = new(); + private static readonly SettingsManager _settingsManager = new SettingsManager(); private static readonly CompositeFormat MicrosoftPluginTimedatePluginDescription = System.Text.CompositeFormat.Parse(Resources.Microsoft_plugin_timedate_plugin_description); private static readonly TimeDateExtensionPage _timeDateExtensionPage = new(_settingsManager); private readonly FallbackTimeDateItem _fallbackTimeDateItem = new(_settingsManager); diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/DefaultBrowserInfo.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/DefaultBrowserInfo.cs index 87b87c7ff5..f6b82ecfbb 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/DefaultBrowserInfo.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/DefaultBrowserInfo.cs @@ -3,7 +3,6 @@ // See the LICENSE file in the project root for more information. using System; -using System.Runtime.InteropServices; using System.Text; using System.Threading; using ManagedCommon; @@ -87,7 +86,7 @@ public static class DefaultBrowserInfo var appName = GetRegistryValue($@"HKEY_CLASSES_ROOT\{progId}\Application", "ApplicationName") ?? GetRegistryValue($@"HKEY_CLASSES_ROOT\{progId}", "FriendlyTypeName"); - if (appName != null) + if (appName is not null) { // Handle indirect strings: if (appName.StartsWith('@')) diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/SettingsManager.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/SettingsManager.cs index 8a39bca35b..300cb105fb 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/SettingsManager.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/SettingsManager.cs @@ -67,7 +67,7 @@ public class SettingsManager : JsonSettingsManager public void SaveHistory(HistoryItem historyItem) { - if (historyItem == null) + if (historyItem is null) { return; } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Pages/WebSearchListPage.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Pages/WebSearchListPage.cs index c96efe24c7..faf65cd973 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Pages/WebSearchListPage.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Pages/WebSearchListPage.cs @@ -34,7 +34,7 @@ internal sealed partial class WebSearchListPage : DynamicListPage Id = "com.microsoft.cmdpal.websearch"; _settingsManager = settingsManager; _historyItems = _settingsManager.ShowHistory != Resources.history_none ? _settingsManager.LoadHistory() : null; - if (_historyItems != null) + if (_historyItems is not null) { _allItems.AddRange(_historyItems); } @@ -55,7 +55,7 @@ internal sealed partial class WebSearchListPage : DynamicListPage ArgumentNullException.ThrowIfNull(query); IEnumerable? filteredHistoryItems = null; - if (_historyItems != null) + if (_historyItems is not null) { filteredHistoryItems = _settingsManager.ShowHistory != Resources.history_none ? ListHelpers.FilterList(_historyItems, query).OfType() : null; } @@ -74,7 +74,7 @@ internal sealed partial class WebSearchListPage : DynamicListPage results.Add(result); } - if (filteredHistoryItems != null) + if (filteredHistoryItems is not null) { results.AddRange(filteredHistoryItems); } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/Pages/InstallPackageCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/Pages/InstallPackageCommand.cs index c4fd7b7a4c..d2c1ea7283 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/Pages/InstallPackageCommand.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/Pages/InstallPackageCommand.cs @@ -5,7 +5,6 @@ using System; using System.Globalization; using System.Text; -using System.Threading; using System.Threading.Tasks; using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions.Toolkit; @@ -79,7 +78,7 @@ public partial class InstallPackageCommand : InvokableCommand { // TODO: LOCK in here, so this can only be invoked once until the // install / uninstall is done. Just use like, an atomic - if (_installTask != null) + if (_installTask is not null) { return CommandResult.KeepOpen(); } @@ -143,7 +142,7 @@ public partial class InstallPackageCommand : InvokableCommand { await Task.Delay(2500).ConfigureAwait(false); - if (_installTask == null) + if (_installTask is null) { WinGetExtensionHost.Instance.HideStatus(_installBanner); } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/Pages/InstallPackageListItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/Pages/InstallPackageListItem.cs index dd51e297a9..e2a3d2e4b4 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/Pages/InstallPackageListItem.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/Pages/InstallPackageListItem.cs @@ -34,7 +34,7 @@ public partial class InstallPackageListItem : ListItem var version = _package.DefaultInstallVersion ?? _package.InstalledVersion; var versionTagText = "Unknown"; - if (version != null) + if (version is not null) { versionTagText = version.Version == "Unknown" && version.PackageCatalog.Info.Id == "StoreEdgeFD" ? "msstore" : version.Version; } @@ -60,11 +60,11 @@ public partial class InstallPackageListItem : ListItem Logger.LogWarning($"{ex.ErrorCode}"); } - if (metadata != null) + if (metadata is not null) { if (metadata.Tags.Where(t => t.Equals(WinGetExtensionPage.ExtensionsTag, StringComparison.OrdinalIgnoreCase)).Any()) { - if (_installCommand != null) + if (_installCommand is not null) { _installCommand.SkipDependencies = true; } @@ -172,7 +172,7 @@ public partial class InstallPackageListItem : ListItem return; } - var isInstalled = _package.InstalledVersion != null; + var isInstalled = _package.InstalledVersion is not null; var installedState = isInstalled ? (_package.IsUpdateAvailable ? @@ -193,11 +193,11 @@ public partial class InstallPackageListItem : ListItem Icon = Icons.DeleteIcon, }; - if (WinGetStatics.AppSearchCallback != null) + if (WinGetStatics.AppSearchCallback is not null) { var callback = WinGetStatics.AppSearchCallback; - var installedApp = callback(_package.DefaultInstallVersion == null ? _package.Name : _package.DefaultInstallVersion.DisplayName); - if (installedApp != null) + var installedApp = callback(_package.DefaultInstallVersion is null ? _package.Name : _package.DefaultInstallVersion.DisplayName); + if (installedApp is not null) { this.Command = installedApp.Command; contextMenu = [.. installedApp.MoreCommands]; diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/Pages/WinGetExtensionPage.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/Pages/WinGetExtensionPage.cs index 1ca113d55c..c348d209d4 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/Pages/WinGetExtensionPage.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/Pages/WinGetExtensionPage.cs @@ -53,7 +53,7 @@ internal sealed partial class WinGetExtensionPage : DynamicListPage, IDisposable { // emptySearchForTag === // we don't have results yet, we haven't typed anything, and we're searching for a tag - var emptySearchForTag = _results == null && + var emptySearchForTag = _results is null && string.IsNullOrEmpty(SearchText) && HasTag; @@ -64,7 +64,7 @@ internal sealed partial class WinGetExtensionPage : DynamicListPage, IDisposable return items; } - if (_results != null && _results.Any()) + if (_results is not null && _results.Any()) { ListItem[] results = _results.Select(PackageToListItem).ToArray(); IsLoading = false; @@ -100,7 +100,7 @@ internal sealed partial class WinGetExtensionPage : DynamicListPage, IDisposable private void DoUpdateSearchText(string newSearch) { // Cancel any ongoing search - if (_cancellationTokenSource != null) + if (_cancellationTokenSource is not null) { Logger.LogDebug("Cancelling old search", memberName: nameof(DoUpdateSearchText)); _cancellationTokenSource.Cancel(); @@ -221,7 +221,7 @@ internal sealed partial class WinGetExtensionPage : DynamicListPage, IDisposable // WinGetStatics static ctor when we were created. PackageCatalog catalog = await catalogTask.Value; - if (catalog == null) + if (catalog is null) { // This error should have already been displayed by WinGetStatics return []; diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Commands/SwitchToWindowCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Commands/SwitchToWindowCommand.cs index 0bfc1e0396..695eaa2c83 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Commands/SwitchToWindowCommand.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Commands/SwitchToWindowCommand.cs @@ -18,10 +18,10 @@ internal sealed partial class SwitchToWindowCommand : InvokableCommand { Name = Resources.switch_to_command_title; _window = window; - if (_window != null) + if (_window is not null) { var p = Process.GetProcessById((int)_window.Process.ProcessID); - if (p != null) + if (p is not null) { try { diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Components/ResultHelper.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Components/ResultHelper.cs index fd7cf9149c..8739d88a2f 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Components/ResultHelper.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Components/ResultHelper.cs @@ -23,7 +23,7 @@ internal static class ResultHelper /// List of results internal static List GetResultList(List searchControllerResults, bool isKeywordSearch) { - if (searchControllerResults == null || searchControllerResults.Count == 0) + if (searchControllerResults is null || searchControllerResults.Count == 0) { return []; } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Helpers/ISettingsInterface.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Helpers/ISettingsInterface.cs new file mode 100644 index 0000000000..e77acb56cf --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Helpers/ISettingsInterface.cs @@ -0,0 +1,26 @@ +// 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. + +namespace Microsoft.CmdPal.Ext.WindowWalker.Helpers; + +public interface ISettingsInterface +{ + public bool ResultsFromVisibleDesktopOnly { get; } + + public bool SubtitleShowPid { get; } + + public bool SubtitleShowDesktopName { get; } + + public bool ConfirmKillProcess { get; } + + public bool KillProcessTree { get; } + + public bool OpenAfterKillAndClose { get; } + + public bool HideKillProcessOnElevatedProcesses { get; } + + public bool HideExplorerSettingInfo { get; } + + public bool InMruOrder { get; } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Helpers/SettingsManager.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Helpers/SettingsManager.cs index 6f541d28df..b2a248beca 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Helpers/SettingsManager.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Helpers/SettingsManager.cs @@ -8,7 +8,7 @@ using Microsoft.CommandPalette.Extensions.Toolkit; namespace Microsoft.CmdPal.Ext.WindowWalker.Helpers; -public class SettingsManager : JsonSettingsManager +public class SettingsManager : JsonSettingsManager, ISettingsInterface { private static readonly string _namespace = "windowWalker"; diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Helpers/VirtualDesktopHelper.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Helpers/VirtualDesktopHelper.cs index 41e6f1fc7f..131ec7ae82 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Helpers/VirtualDesktopHelper.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Helpers/VirtualDesktopHelper.cs @@ -83,7 +83,7 @@ public class VirtualDesktopHelper /// /// Gets a value indicating whether the Virtual Desktop Manager is initialized successfully /// - public bool VirtualDesktopManagerInitialized => _virtualDesktopManager != null; + public bool VirtualDesktopManagerInitialized => _virtualDesktopManager is not null; /// /// Method to update the list of Virtual Desktops from Registry @@ -98,10 +98,10 @@ public class VirtualDesktopHelper // List of all desktops using RegistryKey? virtualDesktopKey = Registry.CurrentUser.OpenSubKey(registryExplorerVirtualDesktops, false); - if (virtualDesktopKey != null) + if (virtualDesktopKey is not null) { var allDeskValue = (byte[]?)virtualDesktopKey.GetValue("VirtualDesktopIDs", null) ?? Array.Empty(); - if (allDeskValue != null) + if (allDeskValue is not null) { // We clear only, if we can read from registry. Otherwise, we keep the existing values. _availableDesktops.Clear(); @@ -124,10 +124,10 @@ public class VirtualDesktopHelper // Guid for current desktop var virtualDesktopsKeyName = _isWindowsEleven ? registryExplorerVirtualDesktops : registrySessionVirtualDesktops; using RegistryKey? virtualDesktopsKey = Registry.CurrentUser.OpenSubKey(virtualDesktopsKeyName, false); - if (virtualDesktopsKey != null) + if (virtualDesktopsKey is not null) { var currentVirtualDesktopValue = virtualDesktopsKey.GetValue("CurrentVirtualDesktop", null); - if (currentVirtualDesktopValue != null) + if (currentVirtualDesktopValue is not null) { _currentDesktop = new Guid((byte[])currentVirtualDesktopValue); } @@ -268,7 +268,7 @@ public class VirtualDesktopHelper using RegistryKey? deskSubKey = Registry.CurrentUser.OpenSubKey(registryPath, false); var desktopName = deskSubKey?.GetValue("Name"); - return (desktopName != null) ? (string)desktopName : defaultName; + return (desktopName is not null) ? (string)desktopName : defaultName; } /// @@ -313,7 +313,7 @@ public class VirtualDesktopHelper /// HResult of the called method as integer. public int GetWindowDesktopId(IntPtr hWindow, out Guid desktopId) { - if (_virtualDesktopManager == null) + if (_virtualDesktopManager is null) { ExtensionHost.LogMessage(new LogMessage() { Message = "VirtualDesktopHelper.GetWindowDesktopId() failed: The instance of isn't available." }); desktopId = Guid.Empty; @@ -330,7 +330,7 @@ public class VirtualDesktopHelper /// An instance of for the desktop where the window is assigned to, or an empty instance of on failure. public VDesktop GetWindowDesktop(IntPtr hWindow) { - if (_virtualDesktopManager == null) + if (_virtualDesktopManager is null) { ExtensionHost.LogMessage(new LogMessage() { Message = "VirtualDesktopHelper.GetWindowDesktop() failed: The instance of isn't available." }); return CreateVDesktopInstance(Guid.Empty); @@ -348,7 +348,7 @@ public class VirtualDesktopHelper /// Type of . public VirtualDesktopAssignmentType GetWindowDesktopAssignmentType(IntPtr hWindow, Guid? desktop = null) { - if (_virtualDesktopManager == null) + if (_virtualDesktopManager is null) { ExtensionHost.LogMessage(new LogMessage() { Message = "VirtualDesktopHelper.GetWindowDesktopAssignmentType() failed: The instance of isn't available." }); return VirtualDesktopAssignmentType.Unknown; @@ -415,7 +415,7 @@ public class VirtualDesktopHelper /// on success and on failure. public bool MoveWindowToDesktop(IntPtr hWindow, ref Guid desktopId) { - if (_virtualDesktopManager == null) + if (_virtualDesktopManager is null) { ExtensionHost.LogMessage(new LogMessage() { Message = "VirtualDesktopHelper.MoveWindowToDesktop() failed: The instance of isn't available." }); return false; diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Pages/WindowWalkerListPage.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Pages/WindowWalkerListPage.cs index ff7217498a..f0cbc01995 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Pages/WindowWalkerListPage.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Pages/WindowWalkerListPage.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; +using System.Globalization; using Microsoft.CmdPal.Ext.WindowWalker.Components; using Microsoft.CmdPal.Ext.WindowWalker.Properties; using Microsoft.CommandPalette.Extensions; @@ -23,6 +24,13 @@ internal sealed partial class WindowWalkerListPage : DynamicListPage, IDisposabl Name = Resources.windowwalker_name; Id = "com.microsoft.cmdpal.windowwalker"; PlaceholderText = Resources.windowwalker_PlaceholderText; + + EmptyContent = new CommandItem(new NoOpCommand()) + { + Icon = Icon, + Title = Resources.window_walker_top_level_command_title, + Subtitle = Resources.windowwalker_NoResultsMessage, + }; } public override void UpdateSearchText(string oldSearch, string newSearch) => diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Properties/Resources.Designer.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Properties/Resources.Designer.cs index 1884f3b3e5..ecb09c8c38 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Properties/Resources.Designer.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Properties/Resources.Designer.cs @@ -142,7 +142,7 @@ namespace Microsoft.CmdPal.Ext.WindowWalker.Properties { } /// - /// Looks up a localized string similar to You are going to end the following process:. + /// Looks up a localized string similar to The following process will be ended:. /// public static string windowwalker_KillMessage { get { @@ -186,6 +186,15 @@ namespace Microsoft.CmdPal.Ext.WindowWalker.Properties { } } + /// + /// Looks up a localized string similar to No open windows found. + /// + public static string windowwalker_NoResultsMessage { + get { + return ResourceManager.GetString("windowwalker_NoResultsMessage", resourceCulture); + } + } + /// /// Looks up a localized string similar to Not Responding. /// diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Properties/Resources.resx b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Properties/Resources.resx index 1c4191bfee..c610b7b09c 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Properties/Resources.resx +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Properties/Resources.resx @@ -232,4 +232,7 @@ Search open windows... + + No open windows found + \ No newline at end of file diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsServices/Helpers/ServiceHelper.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsServices/Helpers/ServiceHelper.cs index ed67163ca5..6e874b1581 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsServices/Helpers/ServiceHelper.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsServices/Helpers/ServiceHelper.cs @@ -47,7 +47,7 @@ public static class ServiceHelper var result = serviceList.Select(s => { var serviceResult = ServiceResult.CreateServiceController(s); - if (serviceResult == null) + if (serviceResult is null) { return null; } @@ -98,7 +98,7 @@ public static class ServiceHelper // ToolTipData = new ToolTipData(serviceResult.DisplayName, serviceResult.ServiceName), // IcoPath = icoPath, }; - }).Where(s => s != null); + }).Where(s => s is not null); return result; } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsSettings/Classes/WindowsSetting.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsSettings/Classes/WindowsSetting.cs index fa6485d138..b276d3a876 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsSettings/Classes/WindowsSetting.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsSettings/Classes/WindowsSetting.cs @@ -19,7 +19,7 @@ internal sealed class WindowsSetting Name = string.Empty; Command = string.Empty; Type = string.Empty; - ShowAsFirstResult = false; + AppHomepageScore = 0; } /// @@ -65,9 +65,9 @@ internal sealed class WindowsSetting public uint? DeprecatedInBuild { get; set; } /// - /// Gets or sets a value indicating whether to use a higher score as normal for this setting to show it as one of the first results. + /// Gets or sets the score for entries if they are a settings app (homepage). If the score is higher 0 they are shown on empty query. /// - public bool ShowAsFirstResult { get; set; } + public int AppHomepageScore { get; set; } /// /// Gets or sets the value with the generated area path as string. diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsSettings/Helpers/UnsupportedSettingsHelper.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsSettings/Helpers/UnsupportedSettingsHelper.cs index 9ad75ad561..c53844a005 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsSettings/Helpers/UnsupportedSettingsHelper.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsSettings/Helpers/UnsupportedSettingsHelper.cs @@ -49,8 +49,8 @@ internal static class UnsupportedSettingsHelper : currentBuildNumber; var filteredSettingsList = windowsSettings.Settings.Where(found - => (found.DeprecatedInBuild == null || currentWindowsBuild < found.DeprecatedInBuild) - && (found.IntroducedInBuild == null || currentWindowsBuild >= found.IntroducedInBuild)); + => (found.DeprecatedInBuild is null || currentWindowsBuild < found.DeprecatedInBuild) + && (found.IntroducedInBuild is null || currentWindowsBuild >= found.IntroducedInBuild)); filteredSettingsList = filteredSettingsList.OrderBy(found => found.Name); diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsSettings/Pages/WindowsSettingsListPage.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsSettings/Pages/WindowsSettingsListPage.cs index 3c27d28537..1196de0b31 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsSettings/Pages/WindowsSettingsListPage.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsSettings/Pages/WindowsSettingsListPage.cs @@ -23,6 +23,13 @@ internal sealed partial class WindowsSettingsListPage : DynamicListPage Name = Resources.settings_title; Id = "com.microsoft.cmdpal.windowsSettings"; _windowsSettings = windowsSettings; + + EmptyContent = new CommandItem(new NoOpCommand()) + { + Icon = Icon, + Title = Resources.settings_subtitle, + Subtitle = Resources.PluginNoResultsMessage + "\n\n" + Resources.PluginNoResultsMessageHelp, + }; } public WindowsSettingsListPage(Classes.WindowsSettings windowsSettings, string query) @@ -38,11 +45,21 @@ internal sealed partial class WindowsSettingsListPage : DynamicListPage return new List(0); } - var filteredList = _windowsSettings.Settings - .Select(setting => ScoringHelper.SearchScoringPredicate(query, setting)) - .Where(scoredSetting => scoredSetting.Score > 0) - .OrderByDescending(scoredSetting => scoredSetting.Score) - .Select(scoredSetting => scoredSetting.Setting); + var filteredList = _windowsSettings.Settings; + if (!string.IsNullOrEmpty(query)) + { + filteredList = filteredList + .Select(setting => ScoringHelper.SearchScoringPredicate(query, setting)) + .Where(scoredSetting => scoredSetting.Score > 0) + .OrderByDescending(scoredSetting => scoredSetting.Score) + .Select(scoredSetting => scoredSetting.Setting); + } + else + { + filteredList = filteredList + .Where(s => s.AppHomepageScore > 0) + .OrderByDescending(s => s.AppHomepageScore); + } var newList = ResultHelper.GetResultList(filteredList); return newList; diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsSettings/Properties/Resources.Designer.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsSettings/Properties/Resources.Designer.cs index 0d5ce2cede..114ff4912a 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsSettings/Properties/Resources.Designer.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsSettings/Properties/Resources.Designer.cs @@ -322,7 +322,7 @@ namespace Microsoft.CmdPal.Ext.WindowsSettings.Properties { } /// - /// Looks up a localized string similar to System settings. + /// Looks up a localized string similar to Settings app. /// internal static string AppSettingsApp { get { @@ -3049,7 +3049,7 @@ namespace Microsoft.CmdPal.Ext.WindowsSettings.Properties { } /// - /// Looks up a localized string similar to Control Panel (Application homepage). + /// Looks up a localized string similar to Open Control Panel. /// internal static string OpenControlPanel { get { @@ -3058,7 +3058,16 @@ namespace Microsoft.CmdPal.Ext.WindowsSettings.Properties { } /// - /// Looks up a localized string similar to Open Settings. + /// Looks up a localized string similar to Open Microsoft Management Console. + /// + internal static string OpenMMC { + get { + return ResourceManager.GetString("OpenMMC", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Open. /// internal static string OpenSettings { get { @@ -3067,7 +3076,7 @@ namespace Microsoft.CmdPal.Ext.WindowsSettings.Properties { } /// - /// Looks up a localized string similar to Settings (Application homepage). + /// Looks up a localized string similar to Open Settings app. /// internal static string OpenSettingsApp { get { @@ -3345,6 +3354,24 @@ namespace Microsoft.CmdPal.Ext.WindowsSettings.Properties { } } + /// + /// Looks up a localized string similar to No settings found. + /// + internal static string PluginNoResultsMessage { + get { + return ResourceManager.GetString("PluginNoResultsMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Tip: Use ':' to search for setting categories (e.g., Update:), and > to search by setting path (e.g., Settings app>Apps).. + /// + internal static string PluginNoResultsMessageHelp { + get { + return ResourceManager.GetString("PluginNoResultsMessageHelp", resourceCulture); + } + } + /// /// Looks up a localized string similar to Windows settings. /// diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsSettings/Properties/Resources.resx b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsSettings/Properties/Resources.resx index 467defe1e8..95b4b5a174 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsSettings/Properties/Resources.resx +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsSettings/Properties/Resources.resx @@ -228,7 +228,7 @@ Area Apps - System settings + Settings app Type of the setting is a "Modern Windows settings". We use the same term as used in start menu search at the moment. @@ -1319,11 +1319,11 @@ On-Screen - Control Panel (Application homepage) + Open Control Panel 'Control Panel' is here the name of the legacy settings app. - Settings (Application homepage) + Open Settings app 'Settings' is here the name of the modern settings app. @@ -2080,7 +2080,8 @@ Mean zooming of things via a magnifier - Open Settings + Open + Open 'the setting' in Settings app, Control Panel or MMC. Windows Settings @@ -2097,4 +2098,13 @@ Search Windows settings for this device + + Tip: Use ':' to search for setting categories (e.g., Update:), and > to search by setting path (e.g., Settings app>Apps). + + + No settings found + + + Open Microsoft Management Console + \ No newline at end of file diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsSettings/WindowsSettings.json b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsSettings/WindowsSettings.json index 97c4d3f65c..794dbcc280 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsSettings/WindowsSettings.json +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsSettings/WindowsSettings.json @@ -6,13 +6,13 @@ "Type": "AppSettingsApp", "AltNames": [ "SettingsApp", "AppSettingsApp" ], "Command": "ms-settings:", - "ShowAsFirstResult": true + "AppHomepageScore": 30 }, { "Name": "OpenControlPanel", "Type": "AppControlPanel", "Command": "control.exe", - "ShowAsFirstResult": true + "AppHomepageScore": 20 }, { "Name": "AccessWorkOrSchool", @@ -1834,11 +1834,11 @@ "Command": "ms-settings-connectabledevices:devicediscovery" }, { - "Name": "AppMMC", + "Name": "OpenMMC", "Type": "AppMMC", "AltNames": [ "MMC_mmcexe" ], "Command": "mmc.exe", - "ShowAsFirstResult" : true + "AppHomepageScore" : 10 }, { "Name": "AuthorizationManager", diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsSettings/WindowsSettings.schema.json b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsSettings/WindowsSettings.schema.json index a60e5c5ffd..ad7f84b4bd 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsSettings/WindowsSettings.schema.json +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsSettings/WindowsSettings.schema.json @@ -62,9 +62,10 @@ "minimum": 0, "maximum": 4294967295 }, - "ShowAsFirstResult": { - "description": "Use a higher score as normal for this setting to show it as one of the first results.", - "type": "boolean" + "AppHomepageScore": { + "description": "Order score for the result if it is a settings app (homepage). Use a score > 0.", + "type": "integer", + "minimum": 1 } } } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/Pages/ProfilesListPage.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/Pages/ProfilesListPage.cs index b426e96914..7e9d2cec31 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/Pages/ProfilesListPage.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/Pages/ProfilesListPage.cs @@ -8,7 +8,6 @@ using Microsoft.CmdPal.Ext.WindowsTerminal.Helpers; using Microsoft.CmdPal.Ext.WindowsTerminal.Properties; using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions.Toolkit; -using Microsoft.UI.Xaml.Media.Imaging; namespace Microsoft.CmdPal.Ext.WindowsTerminal.Pages; @@ -16,7 +15,6 @@ internal sealed partial class ProfilesListPage : ListPage { private readonly TerminalQuery _terminalQuery = new(); private readonly SettingsManager _terminalSettings; - private readonly Dictionary _logoCache = []; private bool showHiddenProfiles; private bool openNewTab; @@ -54,14 +52,6 @@ internal sealed partial class ProfilesListPage : ListPage MoreCommands = [ new CommandContextItem(new LaunchProfileAsAdminCommand(profile.Terminal.AppUserModelId, profile.Name, openNewTab, openQuake)), ], - - // Icon = () => GetLogo(profile.Terminal), - // Action = _ => - // { - // Launch(profile.Terminal.AppUserModelId, profile.Name); - // return true; - // }, - // ContextData = profile, #pragma warning restore SA1108 }); } @@ -70,17 +60,4 @@ internal sealed partial class ProfilesListPage : ListPage } public override IListItem[] GetItems() => Query().ToArray(); - - private BitmapImage GetLogo(TerminalPackage terminal) - { - var aumid = terminal.AppUserModelId; - - if (!_logoCache.TryGetValue(aumid, out var value)) - { - value = terminal.GetLogo(); - _logoCache.Add(aumid, value); - } - - return value; - } } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/TerminalPackage.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/TerminalPackage.cs index 1b2cec7e3d..3301028da1 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/TerminalPackage.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/TerminalPackage.cs @@ -5,7 +5,6 @@ using System; using System.IO; using ManagedCommon; -using Microsoft.UI.Xaml.Media.Imaging; // using Wox.Infrastructure.Image; namespace Microsoft.CmdPal.Ext.WindowsTerminal; @@ -30,23 +29,4 @@ public class TerminalPackage SettingsPath = settingsPath; LogoPath = logoPath; } - - public BitmapImage GetLogo() - { - var image = new BitmapImage(); - - if (File.Exists(LogoPath)) - { - using var fileStream = File.OpenRead(LogoPath); - image.SetSource(fileStream.AsRandomAccessStream()); - } - else - { - // Not using wox anymore, TODO: find the right new way to handle this - // image.UriSource = new Uri(ImageLoader.ErrorIconPath); - Logger.LogError($"Logo file not found: {LogoPath}"); - } - - return image; - } } diff --git a/src/modules/cmdpal/ext/ProcessMonitorExtension/ProcessMonitorExtension.csproj b/src/modules/cmdpal/ext/ProcessMonitorExtension/ProcessMonitorExtension.csproj index d36c277705..8ec263d9bb 100644 --- a/src/modules/cmdpal/ext/ProcessMonitorExtension/ProcessMonitorExtension.csproj +++ b/src/modules/cmdpal/ext/ProcessMonitorExtension/ProcessMonitorExtension.csproj @@ -31,6 +31,10 @@ + + + + diff --git a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/Settings.cs b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/Settings.cs index 0d163ec3fb..fbd74ce694 100644 --- a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/Settings.cs +++ b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/Settings.cs @@ -41,7 +41,7 @@ public sealed partial class Settings : ICommandSettings .Values .Where(s => s is ISettingsForm) .Select(s => s as ISettingsForm) - .Where(s => s != null) + .Where(s => s is not null) .Select(s => s!); var bodies = string.Join(",", settings @@ -77,7 +77,7 @@ public sealed partial class Settings : ICommandSettings .Values .Where(s => s is ISettingsForm) .Select(s => s as ISettingsForm) - .Where(s => s != null) + .Where(s => s is not null) .Select(s => s!); var content = string.Join(",\n", settings.Select(s => s.ToState())); return $"{{\n{content}\n}}"; @@ -86,7 +86,7 @@ public sealed partial class Settings : ICommandSettings public void Update(string data) { var formInput = JsonNode.Parse(data)?.AsObject(); - if (formInput == null) + if (formInput is null) { return; } diff --git a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/SettingsForm.cs b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/SettingsForm.cs index 79f548bf56..2bab5e78dc 100644 --- a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/SettingsForm.cs +++ b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/SettingsForm.cs @@ -19,7 +19,7 @@ public partial class SettingsForm : FormContent public override ICommandResult SubmitForm(string inputs, string data) { var formInput = JsonNode.Parse(inputs)?.AsObject(); - if (formInput == null) + if (formInput is null) { return CommandResult.KeepOpen(); } diff --git a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/ShellHelpers.cs b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/ShellHelpers.cs index 4ab7cfb02f..6c761edcf2 100644 --- a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/ShellHelpers.cs +++ b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/ShellHelpers.cs @@ -125,7 +125,7 @@ public static class ShellHelpers else { var values = Environment.GetEnvironmentVariable("PATH"); - if (values != null) + if (values is not null) { foreach (var path in values.Split(';')) { diff --git a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/StringMatcher.cs b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/StringMatcher.cs index 798bce3b9f..6d9009661a 100644 --- a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/StringMatcher.cs +++ b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/StringMatcher.cs @@ -93,7 +93,7 @@ public partial class StringMatcher query = query.Trim(); - // if (_alphabet != null) + // if (_alphabet is not null) // { // query = _alphabet.Translate(query); // stringToCompare = _alphabet.Translate(stringToCompare); diff --git a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/TextSetting.cs b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/TextSetting.cs index aaa8c2fbee..7cf9147159 100644 --- a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/TextSetting.cs +++ b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/TextSetting.cs @@ -50,7 +50,7 @@ public partial class TextSetting : Setting public override void Update(JsonObject payload) { // If the key doesn't exist in the payload, don't do anything - if (payload[Key] != null) + if (payload[Key] is not null) { Value = payload[Key]?.GetValue(); } diff --git a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/ToggleSetting.cs b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/ToggleSetting.cs index c5e1838608..cdb7b72b25 100644 --- a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/ToggleSetting.cs +++ b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/ToggleSetting.cs @@ -43,7 +43,7 @@ public sealed class ToggleSetting : Setting public override void Update(JsonObject payload) { // If the key doesn't exist in the payload, don't do anything - if (payload[Key] != null) + if (payload[Key] is not null) { // Adaptive cards returns boolean values as a string "true"/"false", cause of course. var strFromJson = payload[Key]?.GetValue() ?? string.Empty; diff --git a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions/Microsoft.CommandPalette.Extensions.vcxproj b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions/Microsoft.CommandPalette.Extensions.vcxproj index e620524c77..983c1594a4 100644 --- a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions/Microsoft.CommandPalette.Extensions.vcxproj +++ b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions/Microsoft.CommandPalette.Extensions.vcxproj @@ -180,4 +180,4 @@ - + \ No newline at end of file diff --git a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions/packages.config b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions/packages.config index fc2bc5a5df..6a99b79e23 100644 --- a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions/packages.config +++ b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions/packages.config @@ -1,7 +1,5 @@  - - \ No newline at end of file diff --git a/src/modules/interface/powertoy_module_interface.h b/src/modules/interface/powertoy_module_interface.h index b569552659..b88763d1a3 100644 --- a/src/modules/interface/powertoy_module_interface.h +++ b/src/modules/interface/powertoy_module_interface.h @@ -45,14 +45,44 @@ public: bool shift = false; bool alt = false; unsigned char key = 0; + // The id is used to identify the hotkey in the module. The order in module interface should be the same as in the settings. + int id = 0; + // Currently, this is only used by AdvancedPaste to determine if the hotkey is shown in the settings. + bool isShown = true; - std::strong_ordering operator<=>(const Hotkey&) const = default; + std::strong_ordering operator<=>(const Hotkey& other) const + { + // Compare bool fields first + if (auto cmp = (win <=> other.win); cmp != 0) + return cmp; + if (auto cmp = (ctrl <=> other.ctrl); cmp != 0) + return cmp; + if (auto cmp = (shift <=> other.shift); cmp != 0) + return cmp; + if (auto cmp = (alt <=> other.alt); cmp != 0) + return cmp; + + // Compare key value only + return key <=> other.key; + + // Note: Deliberately NOT comparing 'name' field + } + + bool operator==(const Hotkey& other) const + { + return win == other.win && + ctrl == other.ctrl && + shift == other.shift && + alt == other.alt && + key == other.key; + } }; struct HotkeyEx { WORD modifiersMask = 0; WORD vkCode = 0; + int id = 0; }; /* Returns the localized name of the PowerToy*/ diff --git a/src/runner/centralized_hotkeys.h b/src/runner/centralized_hotkeys.h index 29ef079f9e..bb503d332d 100644 --- a/src/runner/centralized_hotkeys.h +++ b/src/runner/centralized_hotkeys.h @@ -20,11 +20,13 @@ namespace CentralizedHotkeys { WORD modifiersMask; WORD vkCode; + int hotkeyID; - Shortcut(WORD modifiersMask = 0, WORD vkCode = 0) + Shortcut(WORD modifiersMask = 0, WORD vkCode = 0, const int hotkeyID = 0) { this->modifiersMask = modifiersMask; this->vkCode = vkCode; + this->hotkeyID = hotkeyID; } bool operator<(const Shortcut& key) const diff --git a/src/runner/general_settings.cpp b/src/runner/general_settings.cpp index 4fdf6b74d2..bb45f7f5ae 100644 --- a/src/runner/general_settings.cpp +++ b/src/runner/general_settings.cpp @@ -3,6 +3,7 @@ #include "auto_start_helper.h" #include "tray_icon.h" #include "Generated files/resource.h" +#include "hotkey_conflict_detector.h" #include #include "powertoy_module.h" @@ -204,11 +205,15 @@ void apply_general_settings(const json::JsonObject& general_configs, bool save) { Logger::info(L"apply_general_settings: Enabling powertoy {}", name); powertoy->enable(); + auto& hkmng = HotkeyConflictDetector::HotkeyConflictManager::GetInstance(); + hkmng.EnableHotkeyByModule(name); } else { Logger::info(L"apply_general_settings: Disabling powertoy {}", name); powertoy->disable(); + auto& hkmng = HotkeyConflictDetector::HotkeyConflictManager::GetInstance(); + hkmng.DisableHotkeyByModule(name); } // Sync the hotkey state with the module state, so it can be removed for disabled modules. powertoy.UpdateHotkeyEx(); @@ -315,6 +320,8 @@ void start_enabled_powertoys() { Logger::info(L"start_enabled_powertoys: Enabling powertoy {}", name); powertoy->enable(); + auto& hkmng = HotkeyConflictDetector::HotkeyConflictManager::GetInstance(); + hkmng.EnableHotkeyByModule(name); powertoy.UpdateHotkeyEx(); } } diff --git a/src/runner/hotkey_conflict_detector.cpp b/src/runner/hotkey_conflict_detector.cpp new file mode 100644 index 0000000000..14c8a1ecd9 --- /dev/null +++ b/src/runner/hotkey_conflict_detector.cpp @@ -0,0 +1,471 @@ +#include "pch.h" +#include "hotkey_conflict_detector.h" +#include +#include +#include +#include + +namespace HotkeyConflictDetector +{ + Hotkey ShortcutToHotkey(const CentralizedHotkeys::Shortcut& shortcut) + { + Hotkey hotkey; + + hotkey.win = (shortcut.modifiersMask & MOD_WIN) != 0; + hotkey.ctrl = (shortcut.modifiersMask & MOD_CONTROL) != 0; + hotkey.shift = (shortcut.modifiersMask & MOD_SHIFT) != 0; + hotkey.alt = (shortcut.modifiersMask & MOD_ALT) != 0; + + hotkey.key = shortcut.vkCode > 255 ? 0 : static_cast(shortcut.vkCode); + + return hotkey; + } + + HotkeyConflictManager* HotkeyConflictManager::instance = nullptr; + std::mutex HotkeyConflictManager::instanceMutex; + + HotkeyConflictManager& HotkeyConflictManager::GetInstance() + { + std::lock_guard lock(instanceMutex); + if (instance == nullptr) + { + instance = new HotkeyConflictManager(); + } + return *instance; + } + + HotkeyConflictType HotkeyConflictManager::HasConflict(Hotkey const& _hotkey, const wchar_t* _moduleName, const int _hotkeyID) + { + if (disabledHotkeys.find(_moduleName) != disabledHotkeys.end()) + { + return HotkeyConflictType::NoConflict; + } + + uint16_t handle = GetHotkeyHandle(_hotkey); + + if (handle == 0) + { + return HotkeyConflictType::NoConflict; + } + + // The order is important, first to check sys conflict and then inapp conflict + if (sysConflictHotkeyMap.find(handle) != sysConflictHotkeyMap.end()) + { + return HotkeyConflictType::SystemConflict; + } + + if (inAppConflictHotkeyMap.find(handle) != inAppConflictHotkeyMap.end()) + { + return HotkeyConflictType::InAppConflict; + } + + auto it = hotkeyMap.find(handle); + + if (it == hotkeyMap.end()) + { + return HasConflictWithSystemHotkey(_hotkey) ? + HotkeyConflictType::SystemConflict : + HotkeyConflictType::NoConflict; + } + + if (wcscmp(it->second.moduleName.c_str(), _moduleName) == 0 && it->second.hotkeyID == _hotkeyID) + { + // A shortcut matching its own assignment is not considered a conflict. + return HotkeyConflictType::NoConflict; + } + + return HotkeyConflictType::InAppConflict; + } + + HotkeyConflictType HotkeyConflictManager::HasConflict(Hotkey const& _hotkey) + { + uint16_t handle = GetHotkeyHandle(_hotkey); + + if (handle == 0) + { + return HotkeyConflictType::NoConflict; + } + + // The order is important, first to check sys conflict and then inapp conflict + if (sysConflictHotkeyMap.find(handle) != sysConflictHotkeyMap.end()) + { + return HotkeyConflictType::SystemConflict; + } + + if (inAppConflictHotkeyMap.find(handle) != inAppConflictHotkeyMap.end()) + { + return HotkeyConflictType::InAppConflict; + } + + auto it = hotkeyMap.find(handle); + + if (it == hotkeyMap.end()) + { + return HasConflictWithSystemHotkey(_hotkey) ? + HotkeyConflictType::SystemConflict : + HotkeyConflictType::NoConflict; + } + + return HotkeyConflictType::InAppConflict; + } + + // This function should only be called when a conflict has already been identified. + // It returns a list of all conflicting shortcuts. + std::vector HotkeyConflictManager::GetAllConflicts(Hotkey const& _hotkey) + { + std::vector conflicts; + uint16_t handle = GetHotkeyHandle(_hotkey); + + // Check in-app conflicts first + auto inAppIt = inAppConflictHotkeyMap.find(handle); + if (inAppIt != inAppConflictHotkeyMap.end()) + { + // Add all in-app conflicts + for (const auto& conflict : inAppIt->second) + { + conflicts.push_back(conflict); + } + + return conflicts; + } + + // Check system conflicts + auto sysIt = sysConflictHotkeyMap.find(handle); + if (sysIt != sysConflictHotkeyMap.end()) + { + HotkeyConflictInfo systemConflict; + systemConflict.hotkey = _hotkey; + systemConflict.moduleName = L"System"; + systemConflict.hotkeyID = 0; + + conflicts.push_back(systemConflict); + + return conflicts; + } + + // Check if there's a successfully registered hotkey that would conflict + auto registeredIt = hotkeyMap.find(handle); + if (registeredIt != hotkeyMap.end()) + { + conflicts.push_back(registeredIt->second); + + return conflicts; + } + + // If all the above conditions are ruled out, a system-level conflict is the only remaining explanation. + HotkeyConflictInfo systemConflict; + systemConflict.hotkey = _hotkey; + systemConflict.moduleName = L"System"; + systemConflict.hotkeyID = 0; + conflicts.push_back(systemConflict); + + return conflicts; + } + + bool HotkeyConflictManager::AddHotkey(Hotkey const& _hotkey, const wchar_t* _moduleName, const int _hotkeyID, bool isEnabled) + { + if (!isEnabled) + { + disabledHotkeys[_moduleName].push_back({ _hotkey, _moduleName, _hotkeyID }); + return true; + } + + uint16_t handle = GetHotkeyHandle(_hotkey); + + if (handle == 0) + { + return false; + } + + HotkeyConflictType conflictType = HasConflict(_hotkey, _moduleName, _hotkeyID); + if (conflictType != HotkeyConflictType::NoConflict) + { + if (conflictType == HotkeyConflictType::InAppConflict) + { + auto hotkeyFound = hotkeyMap.find(handle); + inAppConflictHotkeyMap[handle].insert({ _hotkey, _moduleName, _hotkeyID }); + + if (hotkeyFound != hotkeyMap.end()) + { + inAppConflictHotkeyMap[handle].insert(hotkeyFound->second); + hotkeyMap.erase(hotkeyFound); + } + } + else + { + sysConflictHotkeyMap[handle].insert({ _hotkey, _moduleName, _hotkeyID }); + } + return false; + } + + HotkeyConflictInfo hotkeyInfo; + hotkeyInfo.moduleName = _moduleName; + hotkeyInfo.hotkeyID = _hotkeyID; + hotkeyInfo.hotkey = _hotkey; + hotkeyMap[handle] = hotkeyInfo; + + return true; + } + + std::vector HotkeyConflictManager::RemoveHotkeyByModule(const std::wstring& moduleName) + { + std::vector removedHotkeys; + + if (disabledHotkeys.find(moduleName) != disabledHotkeys.end()) + { + disabledHotkeys.erase(moduleName); + } + + std::lock_guard lock(hotkeyMutex); + bool foundRecord = false; + + for (auto it = sysConflictHotkeyMap.begin(); it != sysConflictHotkeyMap.end();) + { + auto& conflictSet = it->second; + for (auto setIt = conflictSet.begin(); setIt != conflictSet.end();) + { + if (setIt->moduleName == moduleName) + { + removedHotkeys.push_back(*setIt); + setIt = conflictSet.erase(setIt); + foundRecord = true; + } + else + { + ++setIt; + } + } + if (conflictSet.empty()) + { + it = sysConflictHotkeyMap.erase(it); + } + else + { + ++it; + } + } + + for (auto it = inAppConflictHotkeyMap.begin(); it != inAppConflictHotkeyMap.end();) + { + auto& conflictSet = it->second; + uint16_t handle = it->first; + + for (auto setIt = conflictSet.begin(); setIt != conflictSet.end();) + { + if (setIt->moduleName == moduleName) + { + removedHotkeys.push_back(*setIt); + setIt = conflictSet.erase(setIt); + foundRecord = true; + } + else + { + ++setIt; + } + } + + if (conflictSet.empty()) + { + it = inAppConflictHotkeyMap.erase(it); + } + else if (conflictSet.size() == 1) + { + // Move the only remaining conflict to main map + const auto& onlyConflict = *conflictSet.begin(); + hotkeyMap[handle] = onlyConflict; + it = inAppConflictHotkeyMap.erase(it); + } + else + { + ++it; + } + } + + for (auto it = hotkeyMap.begin(); it != hotkeyMap.end();) + { + if (it->second.moduleName == moduleName) + { + uint16_t handle = it->first; + removedHotkeys.push_back(it->second); + it = hotkeyMap.erase(it); + foundRecord = true; + + auto inAppIt = inAppConflictHotkeyMap.find(handle); + if (inAppIt != inAppConflictHotkeyMap.end() && inAppIt->second.size() == 1) + { + // Move the only in-app conflict to main map + const auto& onlyConflict = *inAppIt->second.begin(); + hotkeyMap[handle] = onlyConflict; + inAppConflictHotkeyMap.erase(inAppIt); + } + } + else + { + ++it; + } + } + + return removedHotkeys; + } + + void HotkeyConflictManager::EnableHotkeyByModule(const std::wstring& moduleName) + { + if (disabledHotkeys.find(moduleName) == disabledHotkeys.end()) + { + return; // No disabled hotkeys for this module + } + + auto hotkeys = disabledHotkeys[moduleName]; + disabledHotkeys.erase(moduleName); + + for (const auto& hotkeyInfo : hotkeys) + { + // Re-add the hotkey as enabled + AddHotkey(hotkeyInfo.hotkey, moduleName.c_str(), hotkeyInfo.hotkeyID, true); + } + } + + void HotkeyConflictManager::DisableHotkeyByModule(const std::wstring& moduleName) + { + auto hotkeys = RemoveHotkeyByModule(moduleName); + disabledHotkeys[moduleName] = hotkeys; + } + + bool HotkeyConflictManager::HasConflictWithSystemHotkey(const Hotkey& hotkey) + { + // Convert PowerToys Hotkey format to Win32 RegisterHotKey format + UINT modifiers = 0; + if (hotkey.win) + { + modifiers |= MOD_WIN; + } + if (hotkey.ctrl) + { + modifiers |= MOD_CONTROL; + } + if (hotkey.alt) + { + modifiers |= MOD_ALT; + } + if (hotkey.shift) + { + modifiers |= MOD_SHIFT; + } + + // No modifiers or no key is not a valid hotkey + if (modifiers == 0 || hotkey.key == 0) + { + return false; + } + + // Use a unique ID for this test registration + const int hotkeyId = 0x0FFF; // Arbitrary ID for temporary registration + + // Try to register the hotkey with Windows, using nullptr instead of a window handle + if (!RegisterHotKey(nullptr, hotkeyId, modifiers, hotkey.key)) + { + // If registration fails with ERROR_HOTKEY_ALREADY_REGISTERED, it means the hotkey + // is already in use by the system or another application + if (GetLastError() == ERROR_HOTKEY_ALREADY_REGISTERED) + { + return true; + } + } + else + { + // If registration succeeds, unregister it immediately + UnregisterHotKey(nullptr, hotkeyId); + } + + return false; + } + + json::JsonObject HotkeyConflictManager::GetHotkeyConflictsAsJson() + { + std::lock_guard lock(hotkeyMutex); + + using namespace json; + JsonObject root; + + // Serialize hotkey to a unique string format for grouping + auto serializeHotkey = [](const Hotkey& hotkey) -> JsonObject { + JsonObject obj; + obj.Insert(L"win", value(hotkey.win)); + obj.Insert(L"ctrl", value(hotkey.ctrl)); + obj.Insert(L"shift", value(hotkey.shift)); + obj.Insert(L"alt", value(hotkey.alt)); + obj.Insert(L"key", value(static_cast(hotkey.key))); + return obj; + }; + + // New format: Group conflicts by hotkey + JsonArray inAppConflictsArray; + JsonArray sysConflictsArray; + + // Process in-app conflicts - only include hotkeys that are actually in conflict + for (const auto& [handle, conflicts] : inAppConflictHotkeyMap) + { + if (!conflicts.empty()) + { + JsonObject conflictGroup; + + // All entries have the same hotkey, so use the first one for the key + conflictGroup.Insert(L"hotkey", serializeHotkey(conflicts.begin()->hotkey)); + + // Create an array of module info without repeating the hotkey + JsonArray modules; + for (const auto& info : conflicts) + { + JsonObject moduleInfo; + moduleInfo.Insert(L"moduleName", value(info.moduleName)); + moduleInfo.Insert(L"hotkeyID", value(info.hotkeyID)); + modules.Append(moduleInfo); + } + + conflictGroup.Insert(L"modules", modules); + inAppConflictsArray.Append(conflictGroup); + } + } + + // Process system conflicts - only include hotkeys that are actually in conflict + for (const auto& [handle, conflicts] : sysConflictHotkeyMap) + { + if (!conflicts.empty()) + { + JsonObject conflictGroup; + + // All entries have the same hotkey, so use the first one for the key + conflictGroup.Insert(L"hotkey", serializeHotkey(conflicts.begin()->hotkey)); + + // Create an array of module info without repeating the hotkey + JsonArray modules; + for (const auto& info : conflicts) + { + JsonObject moduleInfo; + moduleInfo.Insert(L"moduleName", value(info.moduleName)); + moduleInfo.Insert(L"hotkeyID", value(info.hotkeyID)); + modules.Append(moduleInfo); + } + + conflictGroup.Insert(L"modules", modules); + sysConflictsArray.Append(conflictGroup); + } + } + + // Add the grouped conflicts to the root object + root.Insert(L"inAppConflicts", inAppConflictsArray); + root.Insert(L"sysConflicts", sysConflictsArray); + + return root; + } + + uint16_t HotkeyConflictManager::GetHotkeyHandle(const Hotkey& hotkey) + { + uint16_t handle = hotkey.key; + handle |= hotkey.win << 8; + handle |= hotkey.ctrl << 9; + handle |= hotkey.shift << 10; + handle |= hotkey.alt << 11; + return handle; + } +} \ No newline at end of file diff --git a/src/runner/hotkey_conflict_detector.h b/src/runner/hotkey_conflict_detector.h new file mode 100644 index 0000000000..c32954e3e4 --- /dev/null +++ b/src/runner/hotkey_conflict_detector.h @@ -0,0 +1,100 @@ +#pragma once +#include "pch.h" +#include +#include +#include + +#include "../modules/interface/powertoy_module_interface.h" +#include "centralized_hotkeys.h" +#include "common/utils/json.h" + +namespace HotkeyConflictDetector +{ + using Hotkey = PowertoyModuleIface::Hotkey; + using HotkeyEx = PowertoyModuleIface::HotkeyEx; + using Shortcut = CentralizedHotkeys::Shortcut; + + struct HotkeyConflictInfo + { + Hotkey hotkey; + std::wstring moduleName; + int hotkeyID = 0; + + inline bool operator==(const HotkeyConflictInfo& other) const + { + return hotkey == other.hotkey && + moduleName == other.moduleName && + hotkeyID == other.hotkeyID; + } + }; + + Hotkey ShortcutToHotkey(const CentralizedHotkeys::Shortcut& shortcut); + + enum HotkeyConflictType + { + NoConflict = 0, + SystemConflict = 1, + InAppConflict = 2, + }; + + class HotkeyConflictManager + { + public: + static HotkeyConflictManager& GetInstance(); + + HotkeyConflictType HasConflict(const Hotkey& hotkey, const wchar_t* moduleName, const int hotkeyID); + HotkeyConflictType HotkeyConflictManager::HasConflict(Hotkey const& _hotkey); + std::vector HotkeyConflictManager::GetAllConflicts(Hotkey const& hotkey); + bool AddHotkey(const Hotkey& hotkey, const wchar_t* moduleName, const int hotkeyID, bool isEnabled); + std::vector RemoveHotkeyByModule(const std::wstring& moduleName); + + void EnableHotkeyByModule(const std::wstring& moduleName); + void DisableHotkeyByModule(const std::wstring& moduleName); + + json::JsonObject GetHotkeyConflictsAsJson(); + + private: + static std::mutex instanceMutex; + static HotkeyConflictManager* instance; + + std::mutex hotkeyMutex; + // Hotkey in hotkeyMap means the hotkey has been registered successfully + std::unordered_map hotkeyMap; + // Hotkey in sysConflictHotkeyMap means the hotkey has conflict with system defined hotkeys + std::unordered_map> sysConflictHotkeyMap; + // Hotkey in inAppConflictHotkeyMap means the hotkey has conflict with other modules + std::unordered_map> inAppConflictHotkeyMap; + + std::unordered_map> disabledHotkeys; + + uint16_t GetHotkeyHandle(const Hotkey&); + bool HasConflictWithSystemHotkey(const Hotkey&); + + HotkeyConflictManager() = default; + }; +}; + +namespace std +{ + template<> + struct hash + { + size_t operator()(const HotkeyConflictDetector::HotkeyConflictInfo& info) const + { + + size_t hotkeyHash = + (info.hotkey.win ? 1ULL : 0ULL) | + ((info.hotkey.ctrl ? 1ULL : 0ULL) << 1) | + ((info.hotkey.shift ? 1ULL : 0ULL) << 2) | + ((info.hotkey.alt ? 1ULL : 0ULL) << 3) | + (static_cast(info.hotkey.key) << 4); + + size_t moduleHash = std::hash{}(info.moduleName); + size_t idHash = std::hash{}(info.hotkeyID); + + return hotkeyHash ^ + ((moduleHash << 1) | (moduleHash >> (sizeof(size_t) * 8 - 1))) ^ // rotate left 1 bit + ((idHash << 2) | (idHash >> (sizeof(size_t) * 8 - 2))); // rotate left 2 bits + } + }; +} diff --git a/src/runner/powertoy_module.cpp b/src/runner/powertoy_module.cpp index 32f856f465..eb1f7c4fd7 100644 --- a/src/runner/powertoy_module.cpp +++ b/src/runner/powertoy_module.cpp @@ -40,13 +40,14 @@ json::JsonObject PowertoyModule::json_config() const } PowertoyModule::PowertoyModule(PowertoyModuleIface* pt_module, HMODULE handle) : - handle(handle), pt_module(pt_module) + handle(handle), pt_module(pt_module), hkmng(HotkeyConflictDetector::HotkeyConflictManager::GetInstance()) { if (!pt_module) { throw std::runtime_error("Module not initialized"); } + remove_hotkey_records(); update_hotkeys(); UpdateHotkeyEx(); } @@ -63,19 +64,27 @@ void PowertoyModule::update_hotkeys() for (size_t i = 0; i < hotkeyCount; i++) { - CentralizedKeyboardHook::SetHotkeyAction(pt_module->get_key(), hotkeys[i], [modulePtr, i] { - Logger::trace(L"{} hotkey is invoked from Centralized keyboard hook", modulePtr->get_key()); - return modulePtr->on_hotkey(i); - }); + if (hotkeys[i].isShown) + { + hkmng.AddHotkey(hotkeys[i], pt_module->get_key(), static_cast(i), pt_module->is_enabled()); + + CentralizedKeyboardHook::SetHotkeyAction(pt_module->get_key(), hotkeys[i], [modulePtr, i] { + Logger::trace(L"{} hotkey is invoked from Centralized keyboard hook", modulePtr->get_key()); + return modulePtr->on_hotkey(i); + }); + } } } void PowertoyModule::UpdateHotkeyEx() { CentralizedHotkeys::UnregisterHotkeysForModule(pt_module->get_key()); + auto container = pt_module->GetHotkeyEx(); if (container.has_value() && pt_module->is_enabled()) { + hkmng.RemoveHotkeyByModule(pt_module->get_key()); + auto hotkey = container.value(); auto modulePtr = pt_module.get(); auto action = [modulePtr](WORD /*modifiersMask*/, WORD /*vkCode*/) { @@ -83,6 +92,9 @@ void PowertoyModule::UpdateHotkeyEx() modulePtr->OnHotkeyEx(); }; + HotkeyConflictDetector::Hotkey _hotkey = HotkeyConflictDetector::ShortcutToHotkey({ hotkey.modifiersMask, hotkey.vkCode }); + hkmng.AddHotkey(_hotkey, pt_module->get_key(), 0, pt_module->is_enabled()); // This is the only one activation hotkey, so we use "0" as the name. + CentralizedHotkeys::AddHotkeyAction({ hotkey.modifiersMask, hotkey.vkCode }, { pt_module->get_key(), action }); } diff --git a/src/runner/powertoy_module.h b/src/runner/powertoy_module.h index 9332e5f025..9b7a9a59bd 100644 --- a/src/runner/powertoy_module.h +++ b/src/runner/powertoy_module.h @@ -5,6 +5,7 @@ #include #include #include +#include "hotkey_conflict_detector.h" #include @@ -44,9 +45,17 @@ public: void UpdateHotkeyEx(); + inline void remove_hotkey_records() + { + hkmng.RemoveHotkeyByModule(pt_module->get_key()); + } + private: + HotkeyConflictDetector::HotkeyConflictManager& hkmng; std::unique_ptr handle; std::unique_ptr pt_module; + + }; PowertoyModule load_powertoy(const std::wstring_view filename); diff --git a/src/runner/runner.vcxproj b/src/runner/runner.vcxproj index a55396a71a..90dafb5e45 100644 --- a/src/runner/runner.vcxproj +++ b/src/runner/runner.vcxproj @@ -51,6 +51,7 @@ + Create @@ -71,6 +72,7 @@ + diff --git a/src/runner/runner.vcxproj.filters b/src/runner/runner.vcxproj.filters index a91782fd24..812d7857a2 100644 --- a/src/runner/runner.vcxproj.filters +++ b/src/runner/runner.vcxproj.filters @@ -45,6 +45,9 @@ Utils + + Utils + @@ -93,6 +96,9 @@ Utils + + Utils + diff --git a/src/runner/settings_window.cpp b/src/runner/settings_window.cpp index e811ff5d65..b3ced3b858 100644 --- a/src/runner/settings_window.cpp +++ b/src/runner/settings_window.cpp @@ -13,6 +13,7 @@ #include "UpdateUtils.h" #include "centralized_kb_hook.h" #include "Generated files/resource.h" +#include "hotkey_conflict_detector.h" #include #include @@ -153,6 +154,8 @@ void send_json_config_to_module(const std::wstring& module_key, const std::wstri if (moduleIt != modules().end()) { moduleIt->second->set_config(settings.c_str()); + + moduleIt->second.remove_hotkey_records(); moduleIt->second.update_hotkeys(); moduleIt->second.UpdateHotkeyEx(); } @@ -249,6 +252,77 @@ void dispatch_received_json(const std::wstring& json_to_parse) const std::wstring save_file_location = PTSettingsHelper::get_root_save_folder_location() + language_filename; json::to_file(save_file_location, j); } + else if (name == L"check_hotkey_conflict") + { + try + { + PowertoyModuleIface::Hotkey hotkey; + hotkey.win = value.GetObjectW().GetNamedBoolean(L"win", false); + hotkey.ctrl = value.GetObjectW().GetNamedBoolean(L"ctrl", false); + hotkey.shift = value.GetObjectW().GetNamedBoolean(L"shift", false); + hotkey.alt = value.GetObjectW().GetNamedBoolean(L"alt", false); + hotkey.key = static_cast(value.GetObjectW().GetNamedNumber(L"key", 0)); + + std::wstring requestId = value.GetObjectW().GetNamedString(L"request_id", L"").c_str(); + + auto& hkmng = HotkeyConflictDetector::HotkeyConflictManager::GetInstance(); + bool hasConflict = hkmng.HasConflict(hotkey); + + json::JsonObject response; + response.SetNamedValue(L"response_type", json::JsonValue::CreateStringValue(L"hotkey_conflict_result")); + response.SetNamedValue(L"request_id", json::JsonValue::CreateStringValue(requestId)); + response.SetNamedValue(L"has_conflict", json::JsonValue::CreateBooleanValue(hasConflict)); + + if (hasConflict) + { + auto conflicts = hkmng.GetAllConflicts(hotkey); + if (!conflicts.empty()) + { + // Include all conflicts in the response + json::JsonArray allConflicts; + for (const auto& conflict : conflicts) + { + json::JsonObject conflictObj; + conflictObj.SetNamedValue(L"module", json::JsonValue::CreateStringValue(conflict.moduleName)); + conflictObj.SetNamedValue(L"hotkeyID", json::JsonValue::CreateNumberValue(conflict.hotkeyID)); + allConflicts.Append(conflictObj); + } + response.SetNamedValue(L"all_conflicts", allConflicts); + } + } + + std::unique_lock lock{ ipc_mutex }; + if (current_settings_ipc) + { + current_settings_ipc->send(response.Stringify().c_str()); + } + } + catch (...) + { + Logger::error(L"Failed to process hotkey conflict check request"); + } + } + else if (name == L"get_all_hotkey_conflicts") + { + try + { + auto& hkmng = HotkeyConflictDetector::HotkeyConflictManager::GetInstance(); + auto conflictsJson = hkmng.GetHotkeyConflictsAsJson(); + + // Add response type identifier + conflictsJson.SetNamedValue(L"response_type", json::JsonValue::CreateStringValue(L"all_hotkey_conflicts")); + + std::unique_lock lock{ ipc_mutex }; + if (current_settings_ipc) + { + current_settings_ipc->send(conflictsJson.Stringify().c_str()); + } + } + catch (...) + { + Logger::error(L"Failed to process get all hotkey conflicts request"); + } + } } return; } diff --git a/src/settings-ui/Settings.UI.Library/AdvancedPasteAdditionalAction.cs b/src/settings-ui/Settings.UI.Library/AdvancedPasteAdditionalAction.cs index 28bed92012..1642ecf9c4 100644 --- a/src/settings-ui/Settings.UI.Library/AdvancedPasteAdditionalAction.cs +++ b/src/settings-ui/Settings.UI.Library/AdvancedPasteAdditionalAction.cs @@ -12,7 +12,9 @@ namespace Microsoft.PowerToys.Settings.UI.Library; public sealed partial class AdvancedPasteAdditionalAction : Observable, IAdvancedPasteAction { private HotkeySettings _shortcut = new(); - private bool _isShown = true; + private bool _isShown; + private bool _hasConflict; + private string _tooltip; [JsonPropertyName("shortcut")] public HotkeySettings Shortcut @@ -38,6 +40,20 @@ public sealed partial class AdvancedPasteAdditionalAction : Observable, IAdvance set => Set(ref _isShown, value); } + [JsonIgnore] + public bool HasConflict + { + get => _hasConflict; + set => Set(ref _hasConflict, value); + } + + [JsonIgnore] + public string Tooltip + { + get => _tooltip; + set => Set(ref _tooltip, value); + } + [JsonIgnore] public IEnumerable SubActions => []; } diff --git a/src/settings-ui/Settings.UI.Library/AdvancedPasteAdditionalActions.cs b/src/settings-ui/Settings.UI.Library/AdvancedPasteAdditionalActions.cs index 3b1a859364..6d908c617a 100644 --- a/src/settings-ui/Settings.UI.Library/AdvancedPasteAdditionalActions.cs +++ b/src/settings-ui/Settings.UI.Library/AdvancedPasteAdditionalActions.cs @@ -28,16 +28,23 @@ public sealed class AdvancedPasteAdditionalActions public IEnumerable GetAllActions() { - Queue queue = new([ImageToText, PasteAsFile, Transcode]); + return GetAllActionsRecursive([ImageToText, PasteAsFile, Transcode]); + } - while (queue.Count != 0) + /// + /// Changed to depth-first traversal to ensure ordered output + /// + /// The collection of actions to traverse + /// All actions returned in depth-first order + private static IEnumerable GetAllActionsRecursive(IEnumerable actions) + { + foreach (var action in actions) { - var action = queue.Dequeue(); yield return action; - foreach (var subAction in action.SubActions) + foreach (var subAction in GetAllActionsRecursive(action.SubActions)) { - queue.Enqueue(subAction); + yield return subAction; } } } diff --git a/src/settings-ui/Settings.UI.Library/AdvancedPasteCustomAction.cs b/src/settings-ui/Settings.UI.Library/AdvancedPasteCustomAction.cs index 971d24c93b..43baf89351 100644 --- a/src/settings-ui/Settings.UI.Library/AdvancedPasteCustomAction.cs +++ b/src/settings-ui/Settings.UI.Library/AdvancedPasteCustomAction.cs @@ -5,8 +5,8 @@ using System; using System.Collections.Generic; using System.Text.Json.Serialization; - using Microsoft.PowerToys.Settings.UI.Library.Helpers; +using Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts; namespace Microsoft.PowerToys.Settings.UI.Library; @@ -20,6 +20,8 @@ public sealed class AdvancedPasteCustomAction : Observable, IAdvancedPasteAction private bool _canMoveUp; private bool _canMoveDown; private bool _isValid; + private bool _hasConflict; + private string _tooltip; [JsonPropertyName("id")] public int Id @@ -65,7 +67,6 @@ public sealed class AdvancedPasteCustomAction : Observable, IAdvancedPasteAction // We null-coalesce here rather than outside this branch as we want to raise PropertyChanged when the setter is called // with null; the ShortcutControl depends on this. _shortcut = value ?? new(); - OnPropertyChanged(); } } @@ -99,6 +100,20 @@ public sealed class AdvancedPasteCustomAction : Observable, IAdvancedPasteAction private set => Set(ref _isValid, value); } + [JsonIgnore] + public bool HasConflict + { + get => _hasConflict; + set => Set(ref _hasConflict, value); + } + + [JsonIgnore] + public string Tooltip + { + get => _tooltip; + set => Set(ref _tooltip, value); + } + [JsonIgnore] public IEnumerable SubActions => []; @@ -118,6 +133,8 @@ public sealed class AdvancedPasteCustomAction : Observable, IAdvancedPasteAction IsShown = other.IsShown; CanMoveUp = other.CanMoveUp; CanMoveDown = other.CanMoveDown; + HasConflict = other.HasConflict; + Tooltip = other.Tooltip; } private HotkeySettings GetShortcutClone() diff --git a/src/settings-ui/Settings.UI.Library/AdvancedPasteSettings.cs b/src/settings-ui/Settings.UI.Library/AdvancedPasteSettings.cs index e3ba7d4122..ca9cdacff6 100644 --- a/src/settings-ui/Settings.UI.Library/AdvancedPasteSettings.cs +++ b/src/settings-ui/Settings.UI.Library/AdvancedPasteSettings.cs @@ -3,14 +3,16 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Generic; using System.Text.Json; using System.Text.Json.Serialization; - +using ManagedCommon; +using Microsoft.PowerToys.Settings.UI.Library.Helpers; using Microsoft.PowerToys.Settings.UI.Library.Interfaces; namespace Microsoft.PowerToys.Settings.UI.Library { - public class AdvancedPasteSettings : BasePTModuleSettings, ISettingsConfig + public class AdvancedPasteSettings : BasePTModuleSettings, ISettingsConfig, IHotkeyConfig { public const string ModuleName = "AdvancedPaste"; @@ -39,6 +41,64 @@ namespace Microsoft.PowerToys.Settings.UI.Library settingsUtils.SaveSettings(JsonSerializer.Serialize(this, options), ModuleName); } + public ModuleType GetModuleType() => ModuleType.AdvancedPaste; + + public HotkeyAccessor[] GetAllHotkeyAccessors() + { + var hotkeyAccessors = new List + { + new HotkeyAccessor( + () => Properties.PasteAsPlainTextShortcut, + value => Properties.PasteAsPlainTextShortcut = value ?? AdvancedPasteProperties.DefaultPasteAsPlainTextShortcut, + "PasteAsPlainText_Shortcut"), + new HotkeyAccessor( + () => Properties.AdvancedPasteUIShortcut, + value => Properties.AdvancedPasteUIShortcut = value ?? AdvancedPasteProperties.DefaultAdvancedPasteUIShortcut, + "AdvancedPasteUI_Shortcut"), + new HotkeyAccessor( + () => Properties.PasteAsMarkdownShortcut, + value => Properties.PasteAsMarkdownShortcut = value ?? new HotkeySettings(), + "PasteAsMarkdown_Shortcut"), + new HotkeyAccessor( + () => Properties.PasteAsJsonShortcut, + value => Properties.PasteAsJsonShortcut = value ?? new HotkeySettings(), + "PasteAsJson_Shortcut"), + }; + + string[] additionalActionHeaderKeys = + [ + "ImageToText", + "PasteAsTxtFile", + "PasteAsPngFile", + "PasteAsHtmlFile", + "TranscodeToMp3", + "TranscodeToMp4", + ]; + int index = 0; + foreach (var action in Properties.AdditionalActions.GetAllActions()) + { + if (action is AdvancedPasteAdditionalAction additionalAction) + { + hotkeyAccessors.Add(new HotkeyAccessor( + () => additionalAction.Shortcut, + value => additionalAction.Shortcut = value ?? new HotkeySettings(), + additionalActionHeaderKeys[index])); + index++; + } + } + + // Custom actions do not have localization header, just use the action name. + foreach (var customAction in Properties.CustomActions.Value) + { + hotkeyAccessors.Add(new HotkeyAccessor( + () => customAction.Shortcut, + value => customAction.Shortcut = value ?? new HotkeySettings(), + customAction.Name)); + } + + return hotkeyAccessors.ToArray(); + } + public string GetModuleName() => Name; diff --git a/src/settings-ui/Settings.UI.Library/AlwaysOnTopSettings.cs b/src/settings-ui/Settings.UI.Library/AlwaysOnTopSettings.cs index 449c1c0a76..cb7e138596 100644 --- a/src/settings-ui/Settings.UI.Library/AlwaysOnTopSettings.cs +++ b/src/settings-ui/Settings.UI.Library/AlwaysOnTopSettings.cs @@ -2,13 +2,15 @@ // 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.Collections.Generic; using System.Text.Json.Serialization; - +using ManagedCommon; +using Microsoft.PowerToys.Settings.UI.Library.Helpers; using Microsoft.PowerToys.Settings.UI.Library.Interfaces; namespace Microsoft.PowerToys.Settings.UI.Library { - public class AlwaysOnTopSettings : BasePTModuleSettings, ISettingsConfig + public class AlwaysOnTopSettings : BasePTModuleSettings, ISettingsConfig, IHotkeyConfig { public const string ModuleName = "AlwaysOnTop"; public const string ModuleVersion = "0.0.1"; @@ -28,6 +30,21 @@ namespace Microsoft.PowerToys.Settings.UI.Library return Name; } + public ModuleType GetModuleType() => ModuleType.AlwaysOnTop; + + public HotkeyAccessor[] GetAllHotkeyAccessors() + { + var hotkeyAccessors = new List + { + new HotkeyAccessor( + () => Properties.Hotkey.Value, + value => Properties.Hotkey.Value = value ?? AlwaysOnTopProperties.DefaultHotkeyValue, + "AlwaysOnTop_ActivationShortcut"), + }; + + return hotkeyAccessors.ToArray(); + } + public bool UpgradeSettingsConfiguration() { return false; diff --git a/src/settings-ui/Settings.UI.Library/ColorPickerSettings.cs b/src/settings-ui/Settings.UI.Library/ColorPickerSettings.cs index 641625e180..b601b75baa 100644 --- a/src/settings-ui/Settings.UI.Library/ColorPickerSettings.cs +++ b/src/settings-ui/Settings.UI.Library/ColorPickerSettings.cs @@ -7,14 +7,14 @@ using System.Collections.Generic; using System.Linq; using System.Text.Json; using System.Text.Json.Serialization; - using ManagedCommon; using Microsoft.PowerToys.Settings.UI.Library.Enumerations; +using Microsoft.PowerToys.Settings.UI.Library.Helpers; using Microsoft.PowerToys.Settings.UI.Library.Interfaces; namespace Microsoft.PowerToys.Settings.UI.Library { - public class ColorPickerSettings : BasePTModuleSettings, ISettingsConfig + public class ColorPickerSettings : BasePTModuleSettings, ISettingsConfig, IHotkeyConfig { public const string ModuleName = "ColorPicker"; @@ -64,6 +64,21 @@ namespace Microsoft.PowerToys.Settings.UI.Library return false; } + public ModuleType GetModuleType() => ModuleType.ColorPicker; + + public HotkeyAccessor[] GetAllHotkeyAccessors() + { + var hotkeyAccessors = new List + { + new HotkeyAccessor( + () => Properties.ActivationShortcut, + value => Properties.ActivationShortcut = value ?? Properties.DefaultActivationShortcut, + "Activation_Shortcut"), + }; + + return hotkeyAccessors.ToArray(); + } + public static object UpgradeSettings(object oldSettingsObject) { ColorPickerSettingsVersion1 oldSettings = (ColorPickerSettingsVersion1)oldSettingsObject; diff --git a/src/settings-ui/Settings.UI.Library/ColorPickerSettingsVersion1.cs b/src/settings-ui/Settings.UI.Library/ColorPickerSettingsVersion1.cs index 840788992d..1ddff1946f 100644 --- a/src/settings-ui/Settings.UI.Library/ColorPickerSettingsVersion1.cs +++ b/src/settings-ui/Settings.UI.Library/ColorPickerSettingsVersion1.cs @@ -5,7 +5,6 @@ using System; using System.Text.Json; using System.Text.Json.Serialization; - using Microsoft.PowerToys.Settings.UI.Library.Interfaces; namespace Microsoft.PowerToys.Settings.UI.Library diff --git a/src/settings-ui/Settings.UI.Library/CropAndLockSettings.cs b/src/settings-ui/Settings.UI.Library/CropAndLockSettings.cs index ed6600f287..517c4e8754 100644 --- a/src/settings-ui/Settings.UI.Library/CropAndLockSettings.cs +++ b/src/settings-ui/Settings.UI.Library/CropAndLockSettings.cs @@ -2,13 +2,15 @@ // 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.Collections.Generic; using System.Text.Json.Serialization; - +using ManagedCommon; +using Microsoft.PowerToys.Settings.UI.Library.Helpers; using Microsoft.PowerToys.Settings.UI.Library.Interfaces; namespace Microsoft.PowerToys.Settings.UI.Library { - public class CropAndLockSettings : BasePTModuleSettings, ISettingsConfig + public class CropAndLockSettings : BasePTModuleSettings, ISettingsConfig, IHotkeyConfig { public const string ModuleName = "CropAndLock"; public const string ModuleVersion = "0.0.1"; @@ -28,6 +30,25 @@ namespace Microsoft.PowerToys.Settings.UI.Library return Name; } + public ModuleType GetModuleType() => ModuleType.CropAndLock; + + public HotkeyAccessor[] GetAllHotkeyAccessors() + { + var hotkeyAccessors = new List + { + new HotkeyAccessor( + () => Properties.ReparentHotkey.Value, + value => Properties.ReparentHotkey.Value = value ?? CropAndLockProperties.DefaultReparentHotkeyValue, + "CropAndLock_ReparentActivation_Shortcut"), + new HotkeyAccessor( + () => Properties.ThumbnailHotkey.Value, + value => Properties.ThumbnailHotkey.Value = value ?? CropAndLockProperties.DefaultThumbnailHotkeyValue, + "CropAndLock_ThumbnailActivation_Shortcut"), + }; + + return hotkeyAccessors.ToArray(); + } + public bool UpgradeSettingsConfiguration() { return false; diff --git a/src/settings-ui/Settings.UI.Library/FindMyMouseSettings.cs b/src/settings-ui/Settings.UI.Library/FindMyMouseSettings.cs index aca45d0b01..fb00351ee2 100644 --- a/src/settings-ui/Settings.UI.Library/FindMyMouseSettings.cs +++ b/src/settings-ui/Settings.UI.Library/FindMyMouseSettings.cs @@ -2,13 +2,15 @@ // 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.Collections.Generic; using System.Text.Json.Serialization; - +using ManagedCommon; +using Microsoft.PowerToys.Settings.UI.Library.Helpers; using Microsoft.PowerToys.Settings.UI.Library.Interfaces; namespace Microsoft.PowerToys.Settings.UI.Library { - public class FindMyMouseSettings : BasePTModuleSettings, ISettingsConfig + public class FindMyMouseSettings : BasePTModuleSettings, ISettingsConfig, IHotkeyConfig { public const string ModuleName = "FindMyMouse"; @@ -27,6 +29,21 @@ namespace Microsoft.PowerToys.Settings.UI.Library return Name; } + public ModuleType GetModuleType() => ModuleType.FindMyMouse; + + public HotkeyAccessor[] GetAllHotkeyAccessors() + { + var hotkeyAccessors = new List + { + new HotkeyAccessor( + () => Properties.ActivationShortcut, + value => Properties.ActivationShortcut = value ?? Properties.DefaultActivationShortcut, + "MouseUtils_FindMyMouse_ActivationShortcut"), + }; + + return hotkeyAccessors.ToArray(); + } + // This can be utilized in the future if the settings.json file is to be modified/deleted. public bool UpgradeSettingsConfiguration() { diff --git a/src/settings-ui/Settings.UI.Library/Helpers/HotkeyAccessor.cs b/src/settings-ui/Settings.UI.Library/Helpers/HotkeyAccessor.cs new file mode 100644 index 0000000000..41c4d4af61 --- /dev/null +++ b/src/settings-ui/Settings.UI.Library/Helpers/HotkeyAccessor.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.PowerToys.Settings.UI.Library.Helpers +{ + public class HotkeyAccessor + { + public Func Getter { get; } + + public Action Setter { get; } + + public HotkeyAccessor(Func getter, Action setter, string localizationHeaderKey = "") + { + Getter = getter ?? throw new ArgumentNullException(nameof(getter)); + Setter = setter ?? throw new ArgumentNullException(nameof(setter)); + LocalizationHeaderKey = localizationHeaderKey; + } + + public HotkeySettings Value + { + get => Getter(); + set => Setter(value); + } + + public string LocalizationHeaderKey { get; set; } + } +} diff --git a/src/settings-ui/Settings.UI.Library/HostsProperties.cs b/src/settings-ui/Settings.UI.Library/HostsProperties.cs index 90a576601d..6ec9924049 100644 --- a/src/settings-ui/Settings.UI.Library/HostsProperties.cs +++ b/src/settings-ui/Settings.UI.Library/HostsProperties.cs @@ -24,6 +24,9 @@ namespace Microsoft.PowerToys.Settings.UI.Library public HostsEncoding Encoding { get; set; } + [JsonConverter(typeof(BoolPropertyJsonConverter))] + public bool NoLeadingSpaces { get; set; } + public HostsProperties() { ShowStartupWarning = true; @@ -31,6 +34,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library LoopbackDuplicates = false; AdditionalLinesPosition = HostsAdditionalLinesPosition.Top; Encoding = HostsEncoding.Utf8; + NoLeadingSpaces = false; } } } diff --git a/src/settings-ui/Settings.UI.Library/HotkeyConflicts/AllHotkeyConflictsData.cs b/src/settings-ui/Settings.UI.Library/HotkeyConflicts/AllHotkeyConflictsData.cs new file mode 100644 index 0000000000..00d2145f29 --- /dev/null +++ b/src/settings-ui/Settings.UI.Library/HotkeyConflicts/AllHotkeyConflictsData.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts +{ + public class AllHotkeyConflictsData + { + public List InAppConflicts { get; set; } = new List(); + + public List SystemConflicts { get; set; } = new List(); + } +} diff --git a/src/settings-ui/Settings.UI.Library/HotkeyConflicts/AllHotkeyConflictsEventArgs.cs b/src/settings-ui/Settings.UI.Library/HotkeyConflicts/AllHotkeyConflictsEventArgs.cs new file mode 100644 index 0000000000..28f034d81b --- /dev/null +++ b/src/settings-ui/Settings.UI.Library/HotkeyConflicts/AllHotkeyConflictsEventArgs.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts +{ + public class AllHotkeyConflictsEventArgs : EventArgs + { + public AllHotkeyConflictsData Conflicts { get; } + + public AllHotkeyConflictsEventArgs(AllHotkeyConflictsData conflicts) + { + Conflicts = conflicts; + } + } +} diff --git a/src/settings-ui/Settings.UI.Library/HotkeyConflicts/HotkeyConflictGroupData.cs b/src/settings-ui/Settings.UI.Library/HotkeyConflicts/HotkeyConflictGroupData.cs new file mode 100644 index 0000000000..a420ec7a2b --- /dev/null +++ b/src/settings-ui/Settings.UI.Library/HotkeyConflicts/HotkeyConflictGroupData.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts +{ + public class HotkeyConflictGroupData + { + public HotkeyData Hotkey { get; set; } + + public bool IsSystemConflict { get; set; } + + public List Modules { get; set; } + } +} diff --git a/src/settings-ui/Settings.UI.Library/HotkeyConflicts/HotkeyConflictInfo.cs b/src/settings-ui/Settings.UI.Library/HotkeyConflicts/HotkeyConflictInfo.cs new file mode 100644 index 0000000000..193eb39d89 --- /dev/null +++ b/src/settings-ui/Settings.UI.Library/HotkeyConflicts/HotkeyConflictInfo.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts +{ + public class HotkeyConflictInfo + { + public bool IsSystemConflict { get; set; } + + public string ConflictingModuleName { get; set; } + + public int ConflictingHotkeyID { get; set; } + + public List AllConflictingModules { get; set; } = new List(); + } +} diff --git a/src/settings-ui/Settings.UI.Library/HotkeyConflicts/HotkeyData.cs b/src/settings-ui/Settings.UI.Library/HotkeyConflicts/HotkeyData.cs new file mode 100644 index 0000000000..9e416db7d9 --- /dev/null +++ b/src/settings-ui/Settings.UI.Library/HotkeyConflicts/HotkeyData.cs @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.PowerToys.Settings.UI.Library.Utilities; + +namespace Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts +{ + public class HotkeyData + { + public bool Win { get; set; } + + public bool Ctrl { get; set; } + + public bool Shift { get; set; } + + public bool Alt { get; set; } + + public int Key { get; set; } + + public List GetKeysList() + { + List shortcutList = new List(); + + if (Win) + { + shortcutList.Add(92); // The Windows key or button. + } + + if (Ctrl) + { + shortcutList.Add("Ctrl"); + } + + if (Alt) + { + shortcutList.Add("Alt"); + } + + if (Shift) + { + shortcutList.Add(16); // The Shift key or button. + } + + if (Key > 0) + { + switch (Key) + { + // https://learn.microsoft.com/uwp/api/windows.system.virtualkey?view=winrt-20348 + case 38: // The Up Arrow key or button. + case 40: // The Down Arrow key or button. + case 37: // The Left Arrow key or button. + case 39: // The Right Arrow key or button. + shortcutList.Add(Key); + break; + default: + var localKey = Helper.GetKeyName((uint)Key); + shortcutList.Add(localKey); + break; + } + } + + return shortcutList; + } + } +} diff --git a/src/settings-ui/Settings.UI.Library/HotkeyConflicts/ModuleConflictsData.cs b/src/settings-ui/Settings.UI.Library/HotkeyConflicts/ModuleConflictsData.cs new file mode 100644 index 0000000000..2b343693bd --- /dev/null +++ b/src/settings-ui/Settings.UI.Library/HotkeyConflicts/ModuleConflictsData.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts +{ + public class ModuleConflictsData + { + public List InAppConflicts { get; set; } = new List(); + + public List SystemConflicts { get; set; } = new List(); + + public bool HasConflicts => InAppConflicts.Count > 0 || SystemConflicts.Count > 0; + } +} diff --git a/src/settings-ui/Settings.UI.Library/HotkeyConflicts/ModuleHotkeyData.cs b/src/settings-ui/Settings.UI.Library/HotkeyConflicts/ModuleHotkeyData.cs new file mode 100644 index 0000000000..f24e02e650 --- /dev/null +++ b/src/settings-ui/Settings.UI.Library/HotkeyConflicts/ModuleHotkeyData.cs @@ -0,0 +1,84 @@ +// 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.ComponentModel; +using System.Runtime.CompilerServices; +using ManagedCommon; +using Microsoft.PowerToys.Settings.UI.Library.Helpers; +using Windows.Web.AtomPub; + +namespace Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts +{ + public class ModuleHotkeyData : INotifyPropertyChanged + { + private string _moduleName; + private int _hotkeyID; + private HotkeySettings _hotkeySettings; + private bool _isSystemConflict; + + public event PropertyChangedEventHandler PropertyChanged; + + public string IconPath { get; set; } + + public string DisplayName { get; set; } + + public string Header { get; set; } + + protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + public string ModuleName + { + get => _moduleName; + set + { + if (_moduleName != value) + { + _moduleName = value; + } + } + } + + public int HotkeyID + { + get => _hotkeyID; + set + { + if (_hotkeyID != value) + { + _hotkeyID = value; + } + } + } + + public HotkeySettings HotkeySettings + { + get => _hotkeySettings; + set + { + if (_hotkeySettings != value) + { + _hotkeySettings = value; + OnPropertyChanged(); + } + } + } + + public bool IsSystemConflict + { + get => _isSystemConflict; + set + { + if (_isSystemConflict != value) + { + _isSystemConflict = value; + } + } + } + + public ModuleType ModuleType { get; set; } + } +} diff --git a/src/settings-ui/Settings.UI.Library/HotkeySettings.cs b/src/settings-ui/Settings.UI.Library/HotkeySettings.cs index 89c1a1995d..724e1b5159 100644 --- a/src/settings-ui/Settings.UI.Library/HotkeySettings.cs +++ b/src/settings-ui/Settings.UI.Library/HotkeySettings.cs @@ -4,17 +4,29 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Globalization; +using System.Runtime.CompilerServices; using System.Text; using System.Text.Json.Serialization; - +using ManagedCommon; using Microsoft.PowerToys.Settings.UI.Library.Utilities; namespace Microsoft.PowerToys.Settings.UI.Library { - public record HotkeySettings : ICmdLineRepresentable + public record HotkeySettings : ICmdLineRepresentable, INotifyPropertyChanged { private const int VKTAB = 0x09; + private bool _hasConflict; + private string _conflictDescription; + private bool _isSystemConflict; + + public event PropertyChangedEventHandler PropertyChanged; + + protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } public HotkeySettings() { @@ -23,6 +35,8 @@ namespace Microsoft.PowerToys.Settings.UI.Library Alt = false; Shift = false; Code = 0; + + HasConflict = false; } /// @@ -40,6 +54,51 @@ namespace Microsoft.PowerToys.Settings.UI.Library Alt = alt; Shift = shift; Code = code; + HasConflict = false; + } + + public bool HasConflict + { + get => _hasConflict; + set + { + if (_hasConflict != value) + { + _hasConflict = value; + OnPropertyChanged(); + } + } + } + + public string ConflictDescription + { + get => _conflictDescription ?? string.Empty; + set + { + if (_conflictDescription != value) + { + _conflictDescription = value; + OnPropertyChanged(); + } + } + } + + public bool IsSystemConflict + { + get => _isSystemConflict; + set + { + if (_isSystemConflict != value) + { + _isSystemConflict = value; + OnPropertyChanged(); + } + } + } + + public virtual void UpdateConflictStatus() + { + Logger.LogInfo($"{this.ToString()}"); } [JsonPropertyName("win")] diff --git a/src/settings-ui/Settings.UI.Library/Interfaces/IHotkeyConfig.cs b/src/settings-ui/Settings.UI.Library/Interfaces/IHotkeyConfig.cs new file mode 100644 index 0000000000..ee38f51cad --- /dev/null +++ b/src/settings-ui/Settings.UI.Library/Interfaces/IHotkeyConfig.cs @@ -0,0 +1,17 @@ +// 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.Collections.Generic; +using ManagedCommon; +using Microsoft.PowerToys.Settings.UI.Library.Helpers; + +namespace Microsoft.PowerToys.Settings.UI.Library.Interfaces +{ + public interface IHotkeyConfig + { + HotkeyAccessor[] GetAllHotkeyAccessors(); + + ModuleType GetModuleType(); + } +} diff --git a/src/settings-ui/Settings.UI.Library/MeasureToolSettings.cs b/src/settings-ui/Settings.UI.Library/MeasureToolSettings.cs index 5720c70ca5..e2d034eb21 100644 --- a/src/settings-ui/Settings.UI.Library/MeasureToolSettings.cs +++ b/src/settings-ui/Settings.UI.Library/MeasureToolSettings.cs @@ -2,13 +2,15 @@ // 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.Collections.Generic; using System.Text.Json.Serialization; - +using ManagedCommon; +using Microsoft.PowerToys.Settings.UI.Library.Helpers; using Microsoft.PowerToys.Settings.UI.Library.Interfaces; namespace Microsoft.PowerToys.Settings.UI.Library { - public class MeasureToolSettings : BasePTModuleSettings, ISettingsConfig + public class MeasureToolSettings : BasePTModuleSettings, ISettingsConfig, IHotkeyConfig { public const string ModuleName = "Measure Tool"; @@ -25,6 +27,21 @@ namespace Microsoft.PowerToys.Settings.UI.Library public string GetModuleName() => Name; + public ModuleType GetModuleType() => ModuleType.MeasureTool; + + public HotkeyAccessor[] GetAllHotkeyAccessors() + { + var hotkeyAccessors = new List + { + new HotkeyAccessor( + () => Properties.ActivationShortcut, + value => Properties.ActivationShortcut = value ?? Properties.DefaultActivationShortcut, + "MeasureTool_ActivationShortcut"), + }; + + return hotkeyAccessors.ToArray(); + } + // This can be utilized in the future if the settings.json file is to be modified/deleted. public bool UpgradeSettingsConfiguration() => false; diff --git a/src/settings-ui/Settings.UI.Library/MouseHighlighterSettings.cs b/src/settings-ui/Settings.UI.Library/MouseHighlighterSettings.cs index e23a7fe288..54f28c026b 100644 --- a/src/settings-ui/Settings.UI.Library/MouseHighlighterSettings.cs +++ b/src/settings-ui/Settings.UI.Library/MouseHighlighterSettings.cs @@ -2,15 +2,17 @@ // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System.Collections.Generic; using System.Globalization; using System.Runtime.InteropServices; using System.Text.Json.Serialization; - +using ManagedCommon; +using Microsoft.PowerToys.Settings.UI.Library.Helpers; using Microsoft.PowerToys.Settings.UI.Library.Interfaces; namespace Microsoft.PowerToys.Settings.UI.Library { - public class MouseHighlighterSettings : BasePTModuleSettings, ISettingsConfig + public class MouseHighlighterSettings : BasePTModuleSettings, ISettingsConfig, IHotkeyConfig { public const string ModuleName = "MouseHighlighter"; @@ -29,6 +31,21 @@ namespace Microsoft.PowerToys.Settings.UI.Library return Name; } + public ModuleType GetModuleType() => ModuleType.MouseHighlighter; + + public HotkeyAccessor[] GetAllHotkeyAccessors() + { + var hotkeyAccessors = new List + { + new HotkeyAccessor( + () => Properties.ActivationShortcut, + value => Properties.ActivationShortcut = value ?? Properties.DefaultActivationShortcut, + "MouseUtils_MouseHighlighter_ActivationShortcut"), + }; + + return hotkeyAccessors.ToArray(); + } + // This can be utilized in the future if the settings.json file is to be modified/deleted. public bool UpgradeSettingsConfiguration() { diff --git a/src/settings-ui/Settings.UI.Library/MouseJumpSettings.cs b/src/settings-ui/Settings.UI.Library/MouseJumpSettings.cs index 450e6aec93..a4c5a04555 100644 --- a/src/settings-ui/Settings.UI.Library/MouseJumpSettings.cs +++ b/src/settings-ui/Settings.UI.Library/MouseJumpSettings.cs @@ -3,16 +3,18 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Generic; using System.Text.Json; using System.Text.Json.Serialization; - +using ManagedCommon; +using Microsoft.PowerToys.Settings.UI.Library.Helpers; using Microsoft.PowerToys.Settings.UI.Library.Interfaces; using MouseJump.Common.Helpers; using MouseJump.Common.Models.Settings; namespace Microsoft.PowerToys.Settings.UI.Library { - public class MouseJumpSettings : BasePTModuleSettings, ISettingsConfig + public class MouseJumpSettings : BasePTModuleSettings, ISettingsConfig, IHotkeyConfig { public const string ModuleName = "MouseJump"; @@ -46,6 +48,21 @@ namespace Microsoft.PowerToys.Settings.UI.Library return Name; } + public ModuleType GetModuleType() => ModuleType.MouseJump; + + public HotkeyAccessor[] GetAllHotkeyAccessors() + { + var hotkeyAccessors = new List + { + new HotkeyAccessor( + () => Properties.ActivationShortcut, + value => Properties.ActivationShortcut = value ?? Properties.DefaultActivationShortcut, + "MouseUtils_MouseJump_ActivationShortcut"), + }; + + return hotkeyAccessors.ToArray(); + } + // This can be utilized in the future if the settings.json file is to be modified/deleted. public bool UpgradeSettingsConfiguration() { diff --git a/src/settings-ui/Settings.UI.Library/MousePointerCrosshairsSettings.cs b/src/settings-ui/Settings.UI.Library/MousePointerCrosshairsSettings.cs index 2658a2adec..81b3eadca4 100644 --- a/src/settings-ui/Settings.UI.Library/MousePointerCrosshairsSettings.cs +++ b/src/settings-ui/Settings.UI.Library/MousePointerCrosshairsSettings.cs @@ -2,13 +2,15 @@ // 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.Collections.Generic; using System.Text.Json.Serialization; - +using ManagedCommon; +using Microsoft.PowerToys.Settings.UI.Library.Helpers; using Microsoft.PowerToys.Settings.UI.Library.Interfaces; namespace Microsoft.PowerToys.Settings.UI.Library { - public class MousePointerCrosshairsSettings : BasePTModuleSettings, ISettingsConfig + public class MousePointerCrosshairsSettings : BasePTModuleSettings, ISettingsConfig, IHotkeyConfig { public const string ModuleName = "MousePointerCrosshairs"; @@ -27,6 +29,21 @@ namespace Microsoft.PowerToys.Settings.UI.Library return Name; } + public ModuleType GetModuleType() => ModuleType.MousePointerCrosshairs; + + public HotkeyAccessor[] GetAllHotkeyAccessors() + { + var hotkeyAccessors = new List + { + new HotkeyAccessor( + () => Properties.ActivationShortcut, + value => Properties.ActivationShortcut = value ?? Properties.DefaultActivationShortcut, + "MouseUtils_MousePointerCrosshairs_ActivationShortcut"), + }; + + return hotkeyAccessors.ToArray(); + } + // This can be utilized in the future if the settings.json file is to be modified/deleted. public bool UpgradeSettingsConfiguration() { diff --git a/src/settings-ui/Settings.UI.Library/MouseWithoutBordersSettings.cs b/src/settings-ui/Settings.UI.Library/MouseWithoutBordersSettings.cs index 6a51a150e5..3cab182fec 100644 --- a/src/settings-ui/Settings.UI.Library/MouseWithoutBordersSettings.cs +++ b/src/settings-ui/Settings.UI.Library/MouseWithoutBordersSettings.cs @@ -3,15 +3,17 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Generic; using System.Diagnostics; using System.Text.Json; using System.Text.Json.Serialization; - +using ManagedCommon; +using Microsoft.PowerToys.Settings.UI.Library.Helpers; using Microsoft.PowerToys.Settings.UI.Library.Interfaces; namespace Microsoft.PowerToys.Settings.UI.Library { - public class MouseWithoutBordersSettings : BasePTModuleSettings, ISettingsConfig + public class MouseWithoutBordersSettings : BasePTModuleSettings, ISettingsConfig, IHotkeyConfig { public const string ModuleName = "MouseWithoutBorders"; @@ -37,6 +39,33 @@ namespace Microsoft.PowerToys.Settings.UI.Library return Name; } + public ModuleType GetModuleType() => ModuleType.MouseWithoutBorders; + + public HotkeyAccessor[] GetAllHotkeyAccessors() + { + var hotkeyAccessors = new List + { + new HotkeyAccessor( + () => Properties.ToggleEasyMouseShortcut, + value => Properties.ToggleEasyMouseShortcut = value ?? MouseWithoutBordersProperties.DefaultHotKeyToggleEasyMouse, + "MouseWithoutBorders_ToggleEasyMouseShortcut"), + new HotkeyAccessor( + () => Properties.LockMachineShortcut, + value => Properties.LockMachineShortcut = value ?? MouseWithoutBordersProperties.DefaultHotKeyLockMachine, + "MouseWithoutBorders_LockMachinesShortcut"), + new HotkeyAccessor( + () => Properties.Switch2AllPCShortcut, + value => Properties.Switch2AllPCShortcut = value ?? MouseWithoutBordersProperties.DefaultHotKeySwitch2AllPC, + "MouseWithoutBorders_Switch2AllPcShortcut"), + new HotkeyAccessor( + () => Properties.ReconnectShortcut, + value => Properties.ReconnectShortcut = value ?? MouseWithoutBordersProperties.DefaultHotKeyReconnect, + "MouseWithoutBorders_ReconnectShortcut"), + }; + + return hotkeyAccessors.ToArray(); + } + public HotkeySettings ConvertMouseWithoutBordersHotKeyToPowerToys(int value) { // VK_A <= value <= VK_Z diff --git a/src/settings-ui/Settings.UI.Library/PeekSettings.cs b/src/settings-ui/Settings.UI.Library/PeekSettings.cs index f5ad2a0e26..73993c72fa 100644 --- a/src/settings-ui/Settings.UI.Library/PeekSettings.cs +++ b/src/settings-ui/Settings.UI.Library/PeekSettings.cs @@ -3,14 +3,16 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Generic; using System.Text.Json; using System.Text.Json.Serialization; - +using ManagedCommon; +using Microsoft.PowerToys.Settings.UI.Library.Helpers; using Microsoft.PowerToys.Settings.UI.Library.Interfaces; namespace Microsoft.PowerToys.Settings.UI.Library { - public class PeekSettings : BasePTModuleSettings, ISettingsConfig + public class PeekSettings : BasePTModuleSettings, ISettingsConfig, IHotkeyConfig { public const string ModuleName = "Peek"; public const string ModuleVersion = "0.0.1"; @@ -35,6 +37,21 @@ namespace Microsoft.PowerToys.Settings.UI.Library return Name; } + public ModuleType GetModuleType() => ModuleType.Peek; + + public HotkeyAccessor[] GetAllHotkeyAccessors() + { + var hotkeyAccessors = new List + { + new HotkeyAccessor( + () => Properties.ActivationShortcut, + value => Properties.ActivationShortcut = value ?? Properties.DefaultActivationShortcut, + "Activation_Shortcut"), + }; + + return hotkeyAccessors.ToArray(); + } + public bool UpgradeSettingsConfiguration() { return false; diff --git a/src/settings-ui/Settings.UI.Library/PowerLauncherSettings.cs b/src/settings-ui/Settings.UI.Library/PowerLauncherSettings.cs index c21ce67df5..18d2c2da1c 100644 --- a/src/settings-ui/Settings.UI.Library/PowerLauncherSettings.cs +++ b/src/settings-ui/Settings.UI.Library/PowerLauncherSettings.cs @@ -6,12 +6,13 @@ using System; using System.Collections.Generic; using System.Text.Json; using System.Text.Json.Serialization; - +using ManagedCommon; +using Microsoft.PowerToys.Settings.UI.Library.Helpers; using Microsoft.PowerToys.Settings.UI.Library.Interfaces; namespace Microsoft.PowerToys.Settings.UI.Library { - public class PowerLauncherSettings : BasePTModuleSettings, ISettingsConfig + public class PowerLauncherSettings : BasePTModuleSettings, ISettingsConfig, IHotkeyConfig { public const string ModuleName = "PowerToys Run"; @@ -49,6 +50,21 @@ namespace Microsoft.PowerToys.Settings.UI.Library return Name; } + public ModuleType GetModuleType() => ModuleType.PowerLauncher; + + public HotkeyAccessor[] GetAllHotkeyAccessors() + { + var hotkeyAccessors = new List + { + new HotkeyAccessor( + () => Properties.OpenPowerLauncher, + value => Properties.OpenPowerLauncher = value ?? Properties.DefaultOpenPowerLauncher, + "PowerLauncher_OpenPowerLauncher"), + }; + + return hotkeyAccessors.ToArray(); + } + // This can be utilized in the future if the settings.json file is to be modified/deleted. public bool UpgradeSettingsConfiguration() { diff --git a/src/settings-ui/Settings.UI.Library/PowerOcrSettings.cs b/src/settings-ui/Settings.UI.Library/PowerOcrSettings.cs index 46d176d2b0..4a296832ae 100644 --- a/src/settings-ui/Settings.UI.Library/PowerOcrSettings.cs +++ b/src/settings-ui/Settings.UI.Library/PowerOcrSettings.cs @@ -3,14 +3,16 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Generic; using System.Text.Json; using System.Text.Json.Serialization; - +using ManagedCommon; +using Microsoft.PowerToys.Settings.UI.Library.Helpers; using Microsoft.PowerToys.Settings.UI.Library.Interfaces; namespace Microsoft.PowerToys.Settings.UI.Library { - public class PowerOcrSettings : BasePTModuleSettings, ISettingsConfig + public class PowerOcrSettings : BasePTModuleSettings, ISettingsConfig, IHotkeyConfig { public const string ModuleName = "TextExtractor"; @@ -42,6 +44,21 @@ namespace Microsoft.PowerToys.Settings.UI.Library public string GetModuleName() => Name; + public ModuleType GetModuleType() => ModuleType.PowerOCR; + + public HotkeyAccessor[] GetAllHotkeyAccessors() + { + var hotkeyAccessors = new List + { + new HotkeyAccessor( + () => Properties.ActivationShortcut, + value => Properties.ActivationShortcut = value ?? Properties.DefaultActivationShortcut, + "Activation_Shortcut"), + }; + + return hotkeyAccessors.ToArray(); + } + // This can be utilized in the future if the settings.json file is to be modified/deleted. public bool UpgradeSettingsConfiguration() => false; diff --git a/src/settings-ui/Settings.UI.Library/SettingsFactory.cs b/src/settings-ui/Settings.UI.Library/SettingsFactory.cs new file mode 100644 index 0000000000..2bb9e79121 --- /dev/null +++ b/src/settings-ui/Settings.UI.Library/SettingsFactory.cs @@ -0,0 +1,197 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Microsoft.PowerToys.Settings.UI.Library; +using Microsoft.PowerToys.Settings.UI.Library.Helpers; +using Microsoft.PowerToys.Settings.UI.Library.Interfaces; + +namespace Microsoft.PowerToys.Settings.UI.Services +{ + /// + /// Factory service for getting PowerToys module Settings that implement IHotkeyConfig + /// + public class SettingsFactory + { + private readonly ISettingsUtils _settingsUtils; + private readonly Dictionary _settingsTypes; + + public SettingsFactory(ISettingsUtils settingsUtils) + { + _settingsUtils = settingsUtils ?? throw new ArgumentNullException(nameof(settingsUtils)); + _settingsTypes = DiscoverSettingsTypes(); + } + + /// + /// Dynamically discovers all Settings types that implement IHotkeyConfig + /// + private Dictionary DiscoverSettingsTypes() + { + var settingsTypes = new Dictionary(); + + // Get the Settings.UI.Library assembly + var assembly = Assembly.GetAssembly(typeof(IHotkeyConfig)); + if (assembly == null) + { + return settingsTypes; + } + + try + { + // Find all types that implement IHotkeyConfig and ISettingsConfig + var hotkeyConfigTypes = assembly.GetTypes() + .Where(type => + type.IsClass && + !type.IsAbstract && + typeof(IHotkeyConfig).IsAssignableFrom(type) && + typeof(ISettingsConfig).IsAssignableFrom(type)) + .ToList(); + + foreach (var type in hotkeyConfigTypes) + { + // Try to get the ModuleName using SettingsRepository + try + { + var repositoryType = typeof(SettingsRepository<>).MakeGenericType(type); + var getInstanceMethod = repositoryType.GetMethod("GetInstance", BindingFlags.Public | BindingFlags.Static); + var repository = getInstanceMethod?.Invoke(null, new object[] { _settingsUtils }); + + if (repository != null) + { + var settingsConfigProperty = repository.GetType().GetProperty("SettingsConfig"); + var settingsInstance = settingsConfigProperty?.GetValue(repository) as ISettingsConfig; + + if (settingsInstance != null) + { + var moduleName = settingsInstance.GetModuleName(); + if (!string.IsNullOrEmpty(moduleName)) + { + settingsTypes[moduleName] = type; + System.Diagnostics.Debug.WriteLine($"Discovered settings type: {type.Name} for module: {moduleName}"); + } + } + } + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Error getting module name for {type.Name}: {ex.Message}"); + } + } + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Error scanning assembly {assembly.FullName}: {ex.Message}"); + } + + return settingsTypes; + } + + public IHotkeyConfig GetFreshSettings(string moduleKey) + { + if (!_settingsTypes.TryGetValue(moduleKey, out var settingsType)) + { + return null; + } + + try + { + // Create a generic method call to _settingsUtils.GetSettingsOrDefault(moduleKey) + var getSettingsMethod = typeof(ISettingsUtils).GetMethod("GetSettingsOrDefault", new[] { typeof(string), typeof(string) }); + var genericMethod = getSettingsMethod?.MakeGenericMethod(settingsType); + + // Call GetSettingsOrDefault(moduleKey) to get fresh settings from file + var freshSettings = genericMethod?.Invoke(_settingsUtils, new object[] { moduleKey, "settings.json" }); + + return freshSettings as IHotkeyConfig; + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Error getting fresh settings for {moduleKey}: {ex.Message}"); + return null; + } + } + + /// + /// Gets a settings instance for the specified module using SettingsRepository + /// + /// The module key/name + /// The settings instance implementing IHotkeyConfig, or null if not found + public IHotkeyConfig GetSettings(string moduleKey) + { + if (!_settingsTypes.TryGetValue(moduleKey, out var settingsType)) + { + return null; + } + + try + { + var repositoryType = typeof(SettingsRepository<>).MakeGenericType(settingsType); + var getInstanceMethod = repositoryType.GetMethod("GetInstance", BindingFlags.Public | BindingFlags.Static); + var repository = getInstanceMethod?.Invoke(null, new object[] { _settingsUtils }); + + if (repository != null) + { + var settingsConfigProperty = repository.GetType().GetProperty("SettingsConfig"); + return settingsConfigProperty?.GetValue(repository) as IHotkeyConfig; + } + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Error getting Settings for {moduleKey}: {ex.Message}"); + } + + return null; + } + + /// + /// Gets all available module names that have settings implementing IHotkeyConfig + /// + /// List of module names + public List GetAvailableModuleNames() + { + return _settingsTypes.Keys.ToList(); + } + + /// + /// Gets all available settings that implement IHotkeyConfig + /// + /// Dictionary of module name to settings instance + public Dictionary GetAllHotkeySettings() + { + var result = new Dictionary(); + + foreach (var moduleKey in _settingsTypes.Keys) + { + try + { + var settings = GetSettings(moduleKey); + if (settings != null) + { + result[moduleKey] = settings; + } + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Error getting settings for {moduleKey}: {ex.Message}"); + } + } + + return result; + } + + /// + /// Gets a specific settings repository instance + /// + /// The settings type + /// The settings repository instance + public ISettingsRepository GetRepository() + where T : class, ISettingsConfig, new() + { + return SettingsRepository.GetInstance(_settingsUtils); + } + } +} diff --git a/src/settings-ui/Settings.UI.Library/ShortcutGuideSettings.cs b/src/settings-ui/Settings.UI.Library/ShortcutGuideSettings.cs index c39e757fe3..40174aeb81 100644 --- a/src/settings-ui/Settings.UI.Library/ShortcutGuideSettings.cs +++ b/src/settings-ui/Settings.UI.Library/ShortcutGuideSettings.cs @@ -2,13 +2,15 @@ // 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.Collections.Generic; using System.Text.Json.Serialization; - +using ManagedCommon; +using Microsoft.PowerToys.Settings.UI.Library.Helpers; using Microsoft.PowerToys.Settings.UI.Library.Interfaces; namespace Microsoft.PowerToys.Settings.UI.Library { - public class ShortcutGuideSettings : BasePTModuleSettings, ISettingsConfig + public class ShortcutGuideSettings : BasePTModuleSettings, ISettingsConfig, IHotkeyConfig { public const string ModuleName = "Shortcut Guide"; @@ -27,6 +29,21 @@ namespace Microsoft.PowerToys.Settings.UI.Library return Name; } + public ModuleType GetModuleType() => ModuleType.ShortcutGuide; + + public HotkeyAccessor[] GetAllHotkeyAccessors() + { + var hotkeyAccessors = new List + { + new HotkeyAccessor( + () => Properties.OpenShortcutGuide, + value => Properties.OpenShortcutGuide = value ?? Properties.DefaultOpenShortcutGuide, + "Activation_Shortcut"), + }; + + return hotkeyAccessors.ToArray(); + } + // This can be utilized in the future if the settings.json file is to be modified/deleted. public bool UpgradeSettingsConfiguration() { diff --git a/src/settings-ui/Settings.UI.Library/WorkspacesSettings.cs b/src/settings-ui/Settings.UI.Library/WorkspacesSettings.cs index 1e3ce2261e..fafb034935 100644 --- a/src/settings-ui/Settings.UI.Library/WorkspacesSettings.cs +++ b/src/settings-ui/Settings.UI.Library/WorkspacesSettings.cs @@ -3,14 +3,16 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Generic; using System.Text.Json; using System.Text.Json.Serialization; - +using ManagedCommon; +using Microsoft.PowerToys.Settings.UI.Library.Helpers; using Microsoft.PowerToys.Settings.UI.Library.Interfaces; namespace Microsoft.PowerToys.Settings.UI.Library { - public class WorkspacesSettings : BasePTModuleSettings, ISettingsConfig + public class WorkspacesSettings : BasePTModuleSettings, ISettingsConfig, IHotkeyConfig { public const string ModuleName = "Workspaces"; public const string ModuleVersion = "0.0.1"; @@ -39,6 +41,21 @@ namespace Microsoft.PowerToys.Settings.UI.Library return false; } + public ModuleType GetModuleType() => ModuleType.Workspaces; + + public HotkeyAccessor[] GetAllHotkeyAccessors() + { + var hotkeyAccessors = new List + { + new HotkeyAccessor( + () => Properties.Hotkey.Value, + value => Properties.Hotkey.Value = value ?? WorkspacesProperties.DefaultHotkeyValue, + "Workspaces_ActivationShortcut"), + }; + + return hotkeyAccessors.ToArray(); + } + public virtual void Save(ISettingsUtils settingsUtils) { // Save settings to file diff --git a/src/settings-ui/Settings.UI.UnitTests/ViewModelTests/PowerLauncherViewModelTest.cs b/src/settings-ui/Settings.UI.UnitTests/ViewModelTests/PowerLauncherViewModelTest.cs index f1084d498a..59b61559f4 100644 --- a/src/settings-ui/Settings.UI.UnitTests/ViewModelTests/PowerLauncherViewModelTest.cs +++ b/src/settings-ui/Settings.UI.UnitTests/ViewModelTests/PowerLauncherViewModelTest.cs @@ -3,8 +3,8 @@ // See the LICENSE file in the project root for more information. using System; - using Microsoft.PowerToys.Settings.UI.Library; +using Microsoft.PowerToys.Settings.UI.Library.Interfaces; using Microsoft.PowerToys.Settings.UI.UnitTests.BackwardsCompatibility; using Microsoft.PowerToys.Settings.UI.ViewModels; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -13,7 +13,7 @@ using Moq; namespace ViewModelTests { [TestClass] - public class PowerLauncherViewModelTest + public class PowerLauncherViewModelTest : IDisposable { private sealed class SendCallbackMock { @@ -26,20 +26,48 @@ namespace ViewModelTests { TimesSent++; } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1313:Parameter names should begin with lower-case letter", Justification = "We actually don't validate setting, just calculate it was sent")] + public int OnSendIPC(string _) + { + TimesSent++; + return 0; + } } private PowerLauncherViewModel viewModel; private PowerLauncherSettings mockSettings; private SendCallbackMock sendCallbackMock; + private BackCompatTestProperties.MockSettingsRepository mockGeneralSettingsRepository; [TestInitialize] public void Initialize() { mockSettings = new PowerLauncherSettings(); sendCallbackMock = new SendCallbackMock(); + + var settingPathMock = new Mock(); + var mockGeneralIOProvider = BackCompatTestProperties.GetGeneralSettingsIOProvider("v0.22.0"); + var mockGeneralSettingsUtils = new SettingsUtils(mockGeneralIOProvider.Object, settingPathMock.Object); + mockGeneralSettingsRepository = new BackCompatTestProperties.MockSettingsRepository(mockGeneralSettingsUtils); + viewModel = new PowerLauncherViewModel( mockSettings, - new PowerLauncherViewModel.SendCallback(sendCallbackMock.OnSend)); + mockGeneralSettingsRepository, + sendCallbackMock.OnSendIPC, + () => false); + } + + [TestCleanup] + public void Cleanup() + { + viewModel?.Dispose(); + } + + public void Dispose() + { + viewModel?.Dispose(); + GC.SuppressFinalize(this); } /// @@ -67,7 +95,7 @@ namespace ViewModelTests // Initialise View Model with test Config files Func sendMockIPCConfigMSG = msg => { return 0; }; - PowerLauncherViewModel viewModel = new PowerLauncherViewModel(originalSettings, generalSettingsRepository, sendMockIPCConfigMSG, () => true); + using PowerLauncherViewModel viewModel = new PowerLauncherViewModel(originalSettings, generalSettingsRepository, sendMockIPCConfigMSG, () => true); // Verify that the old settings persisted Assert.AreEqual(originalGeneralSettings.Enabled.PowerLauncher, viewModel.EnablePowerLauncher); diff --git a/src/settings-ui/Settings.UI/Converters/BoolToConflictTypeConverter.cs b/src/settings-ui/Settings.UI/Converters/BoolToConflictTypeConverter.cs new file mode 100644 index 0000000000..826bfa19dd --- /dev/null +++ b/src/settings-ui/Settings.UI/Converters/BoolToConflictTypeConverter.cs @@ -0,0 +1,27 @@ +// 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.Data; + +namespace Microsoft.PowerToys.Settings.UI.Converters +{ + public partial class BoolToConflictTypeConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, string language) + { + if (value is bool isSystemConflict) + { + return isSystemConflict ? "System Conflict" : "In-App Conflict"; + } + + return "Unknown Conflict"; + } + + public object ConvertBack(object value, Type targetType, object parameter, string language) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/settings-ui/Settings.UI/Helpers/HotkeyConflictHelper.cs b/src/settings-ui/Settings.UI/Helpers/HotkeyConflictHelper.cs new file mode 100644 index 0000000000..d7f56fceea --- /dev/null +++ b/src/settings-ui/Settings.UI/Helpers/HotkeyConflictHelper.cs @@ -0,0 +1,73 @@ +// 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.Text.Json.Nodes; +using Microsoft.PowerToys.Settings.UI.Library; + +namespace Microsoft.PowerToys.Settings.UI.Helpers +{ + public class HotkeyConflictHelper + { + public delegate void HotkeyConflictCheckCallback(bool hasConflict, HotkeyConflictResponse conflicts); + + private static readonly Dictionary PendingHotkeyConflictChecks = new Dictionary(); + private static readonly object LockObject = new object(); + + public static void CheckHotkeyConflict(HotkeySettings hotkeySettings, Func ipcMSGCallBackFunc, HotkeyConflictCheckCallback callback) + { + if (hotkeySettings == null || ipcMSGCallBackFunc == null) + { + return; + } + + string requestId = GenerateRequestId(); + + lock (LockObject) + { + PendingHotkeyConflictChecks[requestId] = callback; + } + + var hotkeyObj = new JsonObject + { + ["request_id"] = requestId, + ["win"] = hotkeySettings.Win, + ["ctrl"] = hotkeySettings.Ctrl, + ["shift"] = hotkeySettings.Shift, + ["alt"] = hotkeySettings.Alt, + ["key"] = hotkeySettings.Code, + }; + + var requestObject = new JsonObject + { + ["check_hotkey_conflict"] = hotkeyObj, + }; + + ipcMSGCallBackFunc(requestObject.ToString()); + } + + public static void HandleHotkeyConflictResponse(HotkeyConflictResponse response) + { + if (response.AllConflicts.Count == 0) + { + return; + } + + HotkeyConflictCheckCallback callback = null; + + lock (LockObject) + { + if (PendingHotkeyConflictChecks.TryGetValue(response.RequestId, out callback)) + { + PendingHotkeyConflictChecks.Remove(response.RequestId); + } + } + + callback?.Invoke(response.HasConflict, response); + } + + private static string GenerateRequestId() => Guid.NewGuid().ToString(); + } +} diff --git a/src/settings-ui/Settings.UI/Helpers/HotkeyConflictResponse.cs b/src/settings-ui/Settings.UI/Helpers/HotkeyConflictResponse.cs new file mode 100644 index 0000000000..90803df64c --- /dev/null +++ b/src/settings-ui/Settings.UI/Helpers/HotkeyConflictResponse.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts; + +namespace Microsoft.PowerToys.Settings.UI.Helpers +{ + public class HotkeyConflictResponse + { + public string RequestId { get; set; } + + public bool HasConflict { get; set; } + + public List AllConflicts { get; set; } = new List(); + } +} diff --git a/src/settings-ui/Settings.UI/SerializationContext/SourceGenerationContextContext.cs b/src/settings-ui/Settings.UI/SerializationContext/SourceGenerationContextContext.cs index 8fd948fd86..bd72be5f8c 100644 --- a/src/settings-ui/Settings.UI/SerializationContext/SourceGenerationContextContext.cs +++ b/src/settings-ui/Settings.UI/SerializationContext/SourceGenerationContextContext.cs @@ -13,23 +13,29 @@ using Microsoft.PowerToys.Settings.UI.Library; namespace Microsoft.PowerToys.Settings.UI.SerializationContext; -[JsonSerializable(typeof(WINDOWPLACEMENT))] +[JsonSerializable(typeof(ActionMessage))] [JsonSerializable(typeof(AdvancedPasteSettings))] -[JsonSerializable(typeof(Dictionary>))] [JsonSerializable(typeof(AlwaysOnTopSettings))] [JsonSerializable(typeof(ColorPickerSettings))] [JsonSerializable(typeof(CropAndLockSettings))] +[JsonSerializable(typeof(Dictionary>))] [JsonSerializable(typeof(FileLocksmithSettings))] +[JsonSerializable(typeof(FindMyMouseSettings))] +[JsonSerializable(typeof(IList))] [JsonSerializable(typeof(MeasureToolSettings))] +[JsonSerializable(typeof(MouseHighlighterSettings))] +[JsonSerializable(typeof(MouseJumpSettings))] +[JsonSerializable(typeof(MousePointerCrosshairsSettings))] [JsonSerializable(typeof(MouseWithoutBordersSettings))] [JsonSerializable(typeof(NewPlusSettings))] [JsonSerializable(typeof(PeekSettings))] [JsonSerializable(typeof(PowerLauncherSettings))] [JsonSerializable(typeof(PowerOcrSettings))] +[JsonSerializable(typeof(PowerOcrSettings))] [JsonSerializable(typeof(RegistryPreviewSettings))] +[JsonSerializable(typeof(ShortcutGuideSettings))] +[JsonSerializable(typeof(WINDOWPLACEMENT))] [JsonSerializable(typeof(WorkspacesSettings))] -[JsonSerializable(typeof(IList))] -[JsonSerializable(typeof(ActionMessage))] public sealed partial class SourceGenerationContextContext : JsonSerializerContext { } diff --git a/src/settings-ui/Settings.UI/Services/GlobalHotkeyConflictManager.cs b/src/settings-ui/Settings.UI/Services/GlobalHotkeyConflictManager.cs new file mode 100644 index 0000000000..3971c0589e --- /dev/null +++ b/src/settings-ui/Settings.UI/Services/GlobalHotkeyConflictManager.cs @@ -0,0 +1,121 @@ +// 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.PowerToys.Settings.UI.Library; +using Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts; + +namespace Microsoft.PowerToys.Settings.UI.Services +{ + public class GlobalHotkeyConflictManager + { + private readonly Func _sendIPCMessage; + + private static GlobalHotkeyConflictManager _instance; + private AllHotkeyConflictsData _currentConflicts = new AllHotkeyConflictsData(); + + public static GlobalHotkeyConflictManager Instance => _instance; + + public static void Initialize(Func sendIPCMessage) + { + _instance = new GlobalHotkeyConflictManager(sendIPCMessage); + } + + private GlobalHotkeyConflictManager(Func sendIPCMessage) + { + _sendIPCMessage = sendIPCMessage; + + IPCResponseService.AllHotkeyConflictsReceived += OnAllHotkeyConflictsReceived; + } + + public event EventHandler ConflictsUpdated; + + public void RequestAllConflicts() + { + var requestMessage = "{\"get_all_hotkey_conflicts\":{}}"; + _sendIPCMessage?.Invoke(requestMessage); + } + + private void OnAllHotkeyConflictsReceived(object sender, AllHotkeyConflictsEventArgs e) + { + _currentConflicts = e.Conflicts; + ConflictsUpdated?.Invoke(this, e); + } + + public bool HasConflictForHotkey(HotkeySettings hotkey, string moduleName, int hotkeyID) + { + if (hotkey == null) + { + return false; + } + + var allConflictGroups = _currentConflicts.InAppConflicts.Concat(_currentConflicts.SystemConflicts); + + foreach (var group in allConflictGroups) + { + if (IsHotkeyMatch(hotkey, group.Hotkey)) + { + if (!string.IsNullOrEmpty(moduleName) && hotkeyID >= 0) + { + var selfModule = group.Modules.FirstOrDefault(m => + m.ModuleName.Equals(moduleName, StringComparison.OrdinalIgnoreCase) && + m.HotkeyID == hotkeyID); + + if (selfModule != null && group.Modules.Count == 1) + { + return false; + } + } + + return true; + } + } + + return false; + } + + public HotkeyConflictInfo GetConflictInfo(HotkeySettings hotkey) + { + if (hotkey == null) + { + return null; + } + + var allConflictGroups = _currentConflicts.InAppConflicts.Concat(_currentConflicts.SystemConflicts); + + foreach (var group in allConflictGroups) + { + if (IsHotkeyMatch(hotkey, group.Hotkey)) + { + var conflictModules = group.Modules.Where(m => m != null).ToList(); + if (conflictModules.Count != 0) + { + var firstModule = conflictModules.First(); + return new HotkeyConflictInfo + { + IsSystemConflict = group.IsSystemConflict, + ConflictingModuleName = firstModule.ModuleName, + ConflictingHotkeyID = firstModule.HotkeyID, + AllConflictingModules = conflictModules.Select(m => $"{m.ModuleName}:{m.HotkeyID}").ToList(), + }; + } + } + } + + return null; + } + + private bool IsHotkeyMatch(HotkeySettings settings, HotkeyData data) + { + return settings.Win == data.Win && + settings.Ctrl == data.Ctrl && + settings.Shift == data.Shift && + settings.Alt == data.Alt && + settings.Code == data.Key; + } + } +} diff --git a/src/settings-ui/Settings.UI/Services/IPCResponseService.cs b/src/settings-ui/Settings.UI/Services/IPCResponseService.cs new file mode 100644 index 0000000000..ed16b43603 --- /dev/null +++ b/src/settings-ui/Settings.UI/Services/IPCResponseService.cs @@ -0,0 +1,199 @@ +// 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 Microsoft.PowerToys.Settings.UI.Helpers; +using Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts; +using Microsoft.PowerToys.Settings.UI.Views; +using Windows.Data.Json; + +namespace Microsoft.PowerToys.Settings.UI.Services +{ + public class IPCResponseService + { + private static IPCResponseService _instance; + + public static IPCResponseService Instance => _instance ??= new IPCResponseService(); + + public static event EventHandler AllHotkeyConflictsReceived; + + public void RegisterForIPC() + { + ShellPage.ShellHandler?.IPCResponseHandleList.Add(ProcessIPCMessage); + } + + public void UnregisterFromIPC() + { + ShellPage.ShellHandler?.IPCResponseHandleList.Remove(ProcessIPCMessage); + } + + private void ProcessIPCMessage(JsonObject json) + { + try + { + if (json.TryGetValue("response_type", out IJsonValue responseTypeValue) && + responseTypeValue.ValueType == JsonValueType.String) + { + string responseType = responseTypeValue.GetString(); + + if (responseType.Equals("hotkey_conflict_result", StringComparison.Ordinal)) + { + ProcessHotkeyConflictResult(json); + } + else if (responseType.Equals("all_hotkey_conflicts", StringComparison.Ordinal)) + { + ProcessAllHotkeyConflicts(json); + } + } + } + catch (Exception) + { + } + } + + private void ProcessHotkeyConflictResult(JsonObject json) + { + string requestId = string.Empty; + if (json.TryGetValue("request_id", out IJsonValue requestIdValue) && + requestIdValue.ValueType == JsonValueType.String) + { + requestId = requestIdValue.GetString(); + } + + bool hasConflict = false; + if (json.TryGetValue("has_conflict", out IJsonValue hasConflictValue) && + hasConflictValue.ValueType == JsonValueType.Boolean) + { + hasConflict = hasConflictValue.GetBoolean(); + } + + var allConflicts = new List(); + + if (hasConflict) + { + // Parse the all_conflicts array + if (json.TryGetValue("all_conflicts", out IJsonValue allConflictsValue) && + allConflictsValue.ValueType == JsonValueType.Array) + { + var conflictsArray = allConflictsValue.GetArray(); + foreach (var conflictItem in conflictsArray) + { + if (conflictItem.ValueType == JsonValueType.Object) + { + var conflictObj = conflictItem.GetObject(); + + string moduleName = string.Empty; + int hotkeyID = -1; + + if (conflictObj.TryGetValue("module", out IJsonValue moduleValue) && + moduleValue.ValueType == JsonValueType.String) + { + moduleName = moduleValue.GetString(); + } + + if (conflictObj.TryGetValue("hotkeyID", out IJsonValue hotkeyValue) && + hotkeyValue.ValueType == JsonValueType.Number) + { + hotkeyID = (int)hotkeyValue.GetNumber(); + } + + allConflicts.Add(new ModuleHotkeyData + { + ModuleName = moduleName, + HotkeyID = hotkeyID, + }); + } + } + } + } + + var response = new HotkeyConflictResponse + { + RequestId = requestId, + HasConflict = hasConflict, + AllConflicts = allConflicts, + }; + + HotkeyConflictHelper.HandleHotkeyConflictResponse(response); + } + + private void ProcessAllHotkeyConflicts(JsonObject json) + { + var allConflicts = new AllHotkeyConflictsData(); + + if (json.TryGetValue("inAppConflicts", out IJsonValue inAppValue) && + inAppValue.ValueType == JsonValueType.Array) + { + var inAppArray = inAppValue.GetArray(); + foreach (var conflictGroup in inAppArray) + { + var conflictObj = conflictGroup.GetObject(); + var conflictData = ParseConflictGroup(conflictObj, false); + if (conflictData != null) + { + allConflicts.InAppConflicts.Add(conflictData); + } + } + } + + if (json.TryGetValue("sysConflicts", out IJsonValue sysValue) && + sysValue.ValueType == JsonValueType.Array) + { + var sysArray = sysValue.GetArray(); + foreach (var conflictGroup in sysArray) + { + var conflictObj = conflictGroup.GetObject(); + var conflictData = ParseConflictGroup(conflictObj, true); + if (conflictData != null) + { + allConflicts.SystemConflicts.Add(conflictData); + } + } + } + + AllHotkeyConflictsReceived?.Invoke(this, new AllHotkeyConflictsEventArgs(allConflicts)); + } + + private HotkeyConflictGroupData ParseConflictGroup(JsonObject conflictObj, bool isSystemConflict) + { + if (!conflictObj.TryGetValue("hotkey", out var hotkeyValue) || + !conflictObj.TryGetValue("modules", out var modulesValue)) + { + return null; + } + + var hotkeyObj = hotkeyValue.GetObject(); + bool win = hotkeyObj.TryGetValue("win", out var winVal) && winVal.GetBoolean(); + bool ctrl = hotkeyObj.TryGetValue("ctrl", out var ctrlVal) && ctrlVal.GetBoolean(); + bool shift = hotkeyObj.TryGetValue("shift", out var shiftVal) && shiftVal.GetBoolean(); + bool alt = hotkeyObj.TryGetValue("alt", out var altVal) && altVal.GetBoolean(); + int key = hotkeyObj.TryGetValue("key", out var keyVal) ? (int)keyVal.GetNumber() : 0; + + var conflictGroup = new HotkeyConflictGroupData + { + Hotkey = new HotkeyData { Win = win, Ctrl = ctrl, Shift = shift, Alt = alt, Key = key }, + IsSystemConflict = isSystemConflict, + Modules = new List(), + }; + + var modulesArray = modulesValue.GetArray(); + foreach (var module in modulesArray) + { + var moduleObj = module.GetObject(); + string moduleName = moduleObj.TryGetValue("moduleName", out var modNameVal) ? modNameVal.GetString() : string.Empty; + int hotkeyID = moduleObj.TryGetValue("hotkeyID", out var hotkeyIDVal) ? (int)hotkeyIDVal.GetNumber() : -1; + + conflictGroup.Modules.Add(new ModuleHotkeyData + { + ModuleName = moduleName, + HotkeyID = hotkeyID, + }); + } + + return conflictGroup; + } + } +} diff --git a/src/settings-ui/Settings.UI/SettingsXAML/App.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/App.xaml.cs index f897f4bc8c..d5bd0977e1 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/App.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/App.xaml.cs @@ -232,6 +232,12 @@ namespace Microsoft.PowerToys.Settings.UI }); ipcmanager.Start(); + GlobalHotkeyConflictManager.Initialize(message => + { + ipcmanager.Send(message); + return 0; + }); + if (!ShowOobe && !ShowScoobe && !ShowFlyout) { settingsWindow = new MainWindow(); @@ -320,40 +326,22 @@ namespace Microsoft.PowerToys.Settings.UI WindowHelpers.ForceTopBorder1PixelInsetOnWindows10(WindowNative.GetWindowHandle(settingsWindow)); settingsWindow.Activate(); settingsWindow.NavigateToSection(StartupPage); - ShowMessageDialog("The application is running in Debug mode.", "DEBUG"); + + // In DEBUG mode, we might not have IPC set up, so provide a dummy implementation + GlobalHotkeyConflictManager.Initialize(message => + { + // In debug mode, just log or do nothing + System.Diagnostics.Debug.WriteLine($"IPC Message: {message}"); + return 0; + }); #else - /* If we try to run Settings as a standalone app, it will start PowerToys.exe if not running and open Settings again through it in the Dashboard page. */ - Common.UI.SettingsDeepLink.OpenSettings(Common.UI.SettingsDeepLink.SettingsWindow.Dashboard, true); - Exit(); + /* If we try to run Settings as a standalone app, it will start PowerToys.exe if not running and open Settings again through it in the Dashboard page. */ + Common.UI.SettingsDeepLink.OpenSettings(Common.UI.SettingsDeepLink.SettingsWindow.Dashboard, true); + Exit(); #endif } } -#if !DEBUG - private async void ShowMessageDialogAndExit(string content, string title = null) -#else - private async void ShowMessageDialog(string content, string title = null) -#endif - { - await ShowDialogAsync(content, title); -#if !DEBUG - this.Exit(); -#endif - } - - public static Task ShowDialogAsync(string content, string title = null) - { - var dialog = new MessageDialog(content, title ?? string.Empty); - var handle = NativeMethods.GetActiveWindow(); - if (handle == IntPtr.Zero) - { - throw new InvalidOperationException(); - } - - InitializeWithWindow.Initialize(dialog, handle); - return dialog.ShowAsync().AsTask(); - } - public static TwoWayPipeMessageIPCManaged GetTwoWayIPCManager() { return ipcmanager; diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictControl.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictControl.xaml index ed3e153682..69a7a1084d 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictControl.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictControl.xaml @@ -8,7 +8,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"> - + - + \ No newline at end of file diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictControl.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictControl.xaml.cs index 9b0c0f4574..25643e0c64 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictControl.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictControl.xaml.cs @@ -4,37 +4,122 @@ using System; using System.Collections.Generic; -using System.IO; +using System.ComponentModel; using System.Linq; -using System.Runtime.InteropServices.WindowsRuntime; +using Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts; +using Microsoft.PowerToys.Settings.UI.SettingsXAML.Controls.Dashboard; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; -using Microsoft.UI.Xaml.Controls.Primitives; -using Microsoft.UI.Xaml.Data; -using Microsoft.UI.Xaml.Input; -using Microsoft.UI.Xaml.Media; -using Microsoft.UI.Xaml.Navigation; -using Windows.Foundation; -using Windows.Foundation.Collections; +using Microsoft.Windows.ApplicationModel.Resources; namespace Microsoft.PowerToys.Settings.UI.Controls { - public sealed partial class ShortcutConflictControl : UserControl + public sealed partial class ShortcutConflictControl : UserControl, INotifyPropertyChanged { + private static readonly ResourceLoader ResourceLoader = Helpers.ResourceLoaderInstance.ResourceLoader; + + public static readonly DependencyProperty AllHotkeyConflictsDataProperty = + DependencyProperty.Register( + nameof(AllHotkeyConflictsData), + typeof(AllHotkeyConflictsData), + typeof(ShortcutConflictControl), + new PropertyMetadata(null, OnAllHotkeyConflictsDataChanged)); + + public AllHotkeyConflictsData AllHotkeyConflictsData + { + get => (AllHotkeyConflictsData)GetValue(AllHotkeyConflictsDataProperty); + set => SetValue(AllHotkeyConflictsDataProperty, value); + } + + public int ConflictCount + { + get + { + if (AllHotkeyConflictsData == null) + { + return 0; + } + + int count = 0; + if (AllHotkeyConflictsData.InAppConflicts != null) + { + count += AllHotkeyConflictsData.InAppConflicts.Count; + } + + if (AllHotkeyConflictsData.SystemConflicts != null) + { + count += AllHotkeyConflictsData.SystemConflicts.Count; + } + + return count; + } + } + + public string ConflictText + { + get + { + var count = ConflictCount; + return count switch + { + 0 => ResourceLoader.GetString("ShortcutConflictControl_NoConflictsFound"), + 1 => ResourceLoader.GetString("ShortcutConflictControl_SingleConflictFound"), + _ => string.Format( + System.Globalization.CultureInfo.CurrentCulture, + ResourceLoader.GetString("ShortcutConflictControl_MultipleConflictsFound"), + count), + }; + } + } + + public bool HasConflicts => ConflictCount > 0; + + private static void OnAllHotkeyConflictsDataChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is ShortcutConflictControl control) + { + control.UpdateProperties(); + } + } + + public event PropertyChangedEventHandler PropertyChanged; + + private void UpdateProperties() + { + OnPropertyChanged(nameof(ConflictCount)); + OnPropertyChanged(nameof(ConflictText)); + OnPropertyChanged(nameof(HasConflicts)); + + // Update visibility based on conflict count + Visibility = HasConflicts ? Visibility.Visible : Visibility.Collapsed; + } + + private void OnPropertyChanged(string propertyName) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + public ShortcutConflictControl() { InitializeComponent(); - GetShortcutConflicts(); - } + DataContext = this; - private void GetShortcutConflicts() - { - // TO DO: Implement the logic to retrieve and display shortcut conflicts. Make sure to Collapse this control if not conflicts are found. + // Initially hide the control if no conflicts + Visibility = HasConflicts ? Visibility.Visible : Visibility.Collapsed; } private void ShortcutConflictBtn_Click(object sender, RoutedEventArgs e) { - // TO DO: Handle the button click event to show the shortcut conflicts window. + if (AllHotkeyConflictsData == null || !HasConflicts) + { + return; + } + + // Create and show the new window instead of dialog + var conflictWindow = new ShortcutConflictWindow(); + + // Show the window + conflictWindow.Activate(); } } } diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictWindow.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictWindow.xaml new file mode 100644 index 0000000000..46f8d4f962 --- /dev/null +++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictWindow.xaml @@ -0,0 +1,176 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictWindow.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictWindow.xaml.cs new file mode 100644 index 0000000000..5bcc282261 --- /dev/null +++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictWindow.xaml.cs @@ -0,0 +1,91 @@ +// 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 CommunityToolkit.WinUI.Controls; +using Microsoft.PowerToys.Settings.UI.Helpers; +using Microsoft.PowerToys.Settings.UI.Library; +using Microsoft.PowerToys.Settings.UI.Library.Helpers; +using Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts; +using Microsoft.PowerToys.Settings.UI.Services; +using Microsoft.PowerToys.Settings.UI.ViewModels; +using Microsoft.PowerToys.Settings.UI.Views; +using Microsoft.UI; +using Microsoft.UI.Windowing; +using Microsoft.UI.Xaml; +using Windows.Graphics; +using WinUIEx; + +namespace Microsoft.PowerToys.Settings.UI.SettingsXAML.Controls.Dashboard +{ + public sealed partial class ShortcutConflictWindow : WindowEx + { + public ShortcutConflictViewModel DataContext { get; } + + public ShortcutConflictViewModel ViewModel { get; private set; } + + public ShortcutConflictWindow() + { + var settingsUtils = new SettingsUtils(); + ViewModel = new ShortcutConflictViewModel( + settingsUtils, + SettingsRepository.GetInstance(settingsUtils), + ShellPage.SendDefaultIPCMessage); + + DataContext = ViewModel; + InitializeComponent(); + + this.Activated += Window_Activated_SetIcon; + + // Set localized window title + var resourceLoader = ResourceLoaderInstance.ResourceLoader; + this.ExtendsContentIntoTitleBar = true; + + this.Title = resourceLoader.GetString("ShortcutConflictWindow_Title"); + this.CenterOnScreen(); + + ViewModel.OnPageLoaded(); + } + + private void CenterOnScreen() + { + var displayArea = DisplayArea.GetFromWindowId(this.AppWindow.Id, DisplayAreaFallback.Nearest); + if (displayArea != null) + { + var windowSize = this.AppWindow.Size; + var centeredPosition = new PointInt32 + { + X = (displayArea.WorkArea.Width - windowSize.Width) / 2, + Y = (displayArea.WorkArea.Height - windowSize.Height) / 2, + }; + this.AppWindow.Move(centeredPosition); + } + } + + private void SettingsCard_Click(object sender, RoutedEventArgs e) + { + if (sender is SettingsCard settingsCard && + settingsCard.DataContext is ModuleHotkeyData moduleData) + { + var moduleType = moduleData.ModuleType; + NavigationService.Navigate(ModuleHelper.GetModulePageType(moduleType)); + this.Close(); + } + } + + private void WindowEx_Closed(object sender, WindowEventArgs args) + { + ViewModel?.Dispose(); + } + + private void Window_Activated_SetIcon(object sender, WindowActivatedEventArgs args) + { + // Set window icon + var hWnd = WinRT.Interop.WindowNative.GetWindowHandle(this); + WindowId windowId = Win32Interop.GetWindowIdFromWindow(hWnd); + AppWindow appWindow = AppWindow.GetFromWindowId(windowId); + appWindow.SetIcon("Assets\\Settings\\icon.ico"); + } + } +} diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/KeyVisual/KeyCharPresenter.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Controls/KeyVisual/KeyCharPresenter.xaml index c45f1ba0d2..a28874b4ee 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/KeyVisual/KeyCharPresenter.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/KeyVisual/KeyCharPresenter.xaml @@ -49,6 +49,40 @@ + + - + \ No newline at end of file diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/KeyVisual/KeyVisual.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Controls/KeyVisual/KeyVisual.xaml.cs index b1a967fb16..b638c32f2b 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/KeyVisual/KeyVisual.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/KeyVisual/KeyVisual.xaml.cs @@ -95,9 +95,23 @@ namespace Microsoft.PowerToys.Settings.UI.Controls return; } - if (Content is string) + if (Content is string key) { - _keyPresenter.Style = (Style)Application.Current.Resources["DefaultKeyCharPresenterStyle"]; + switch (key) + { + case "Copilot": + _keyPresenter.Style = (Style)Application.Current.Resources["CopilotKeyCharPresenterStyle"]; + break; + + case "Office": + _keyPresenter.Style = (Style)Application.Current.Resources["OfficeKeyCharPresenterStyle"]; + break; + + default: + _keyPresenter.Style = (Style)Application.Current.Resources["DefaultKeyCharPresenterStyle"]; + break; + } + return; } diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutControl.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutControl.xaml index 72cb4a3c55..d81be4aa6c 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutControl.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutControl.xaml @@ -3,6 +3,7 @@ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:controls="using:Microsoft.PowerToys.Settings.UI.Controls" + xmlns:converters="using:Microsoft.PowerToys.Settings.UI.Converters" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" x:Name="LayoutRoot" @@ -39,6 +40,7 @@ Content="{Binding}" CornerRadius="{StaticResource ControlCornerRadius}" FontWeight="SemiBold" + IsInvalid="{Binding ElementName=LayoutRoot, Path=HasConflict}" IsTabStop="False" Style="{StaticResource AccentKeyVisualStyle}" /> diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutControl.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutControl.xaml.cs index c75017300c..3e3df56690 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutControl.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutControl.xaml.cs @@ -3,9 +3,15 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; using CommunityToolkit.WinUI; using Microsoft.PowerToys.Settings.UI.Helpers; using Microsoft.PowerToys.Settings.UI.Library; +using Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts; +using Microsoft.PowerToys.Settings.UI.Services; +using Microsoft.PowerToys.Settings.UI.Views; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Automation; using Microsoft.UI.Xaml.Controls; @@ -33,8 +39,9 @@ namespace Microsoft.PowerToys.Settings.UI.Controls public static readonly DependencyProperty IsActiveProperty = DependencyProperty.Register("Enabled", typeof(bool), typeof(ShortcutControl), null); public static readonly DependencyProperty HotkeySettingsProperty = DependencyProperty.Register("HotkeySettings", typeof(HotkeySettings), typeof(ShortcutControl), null); - public static readonly DependencyProperty AllowDisableProperty = DependencyProperty.Register("AllowDisable", typeof(bool), typeof(ShortcutControl), new PropertyMetadata(false, OnAllowDisableChanged)); + public static readonly DependencyProperty HasConflictProperty = DependencyProperty.Register("HasConflict", typeof(bool), typeof(ShortcutControl), new PropertyMetadata(false, OnHasConflictChanged)); + public static readonly DependencyProperty TooltipProperty = DependencyProperty.Register("Tooltip", typeof(string), typeof(ShortcutControl), new PropertyMetadata(null, OnTooltipChanged)); private static ResourceLoader resourceLoader = Helpers.ResourceLoaderInstance.ResourceLoader; @@ -58,6 +65,28 @@ namespace Microsoft.PowerToys.Settings.UI.Controls description.Text = text; } + private static void OnHasConflictChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var control = d as ShortcutControl; + if (control == null) + { + return; + } + + control.UpdateKeyVisualStyles(); + } + + private static void OnTooltipChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var control = d as ShortcutControl; + if (control == null) + { + return; + } + + control.UpdateTooltip(); + } + private ShortcutDialogContentControl c = new ShortcutDialogContentControl(); private ContentDialog shortcutDialog; @@ -67,6 +96,18 @@ namespace Microsoft.PowerToys.Settings.UI.Controls set => SetValue(AllowDisableProperty, value); } + public bool HasConflict + { + get => (bool)GetValue(HasConflictProperty); + set => SetValue(HasConflictProperty, value); + } + + public string Tooltip + { + get => (string)GetValue(TooltipProperty); + set => SetValue(TooltipProperty, value); + } + public bool Enabled { get @@ -101,14 +142,54 @@ namespace Microsoft.PowerToys.Settings.UI.Controls { if (hotkeySettings != value) { + // Unsubscribe from old settings + if (hotkeySettings != null) + { + hotkeySettings.PropertyChanged -= OnHotkeySettingsPropertyChanged; + } + hotkeySettings = value; SetValue(HotkeySettingsProperty, value); + + // Subscribe to new settings + if (hotkeySettings != null) + { + hotkeySettings.PropertyChanged += OnHotkeySettingsPropertyChanged; + + // Update UI based on conflict properties + UpdateConflictStatusFromHotkeySettings(); + } + SetKeys(); - c.Keys = HotkeySettings.GetKeysList(); + c.Keys = HotkeySettings?.GetKeysList(); } } } + private void OnHotkeySettingsPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(HotkeySettings.HasConflict) || + e.PropertyName == nameof(HotkeySettings.ConflictDescription)) + { + UpdateConflictStatusFromHotkeySettings(); + } + } + + private void UpdateConflictStatusFromHotkeySettings() + { + if (hotkeySettings != null) + { + // Update the ShortcutControl's conflict properties from HotkeySettings + HasConflict = hotkeySettings.HasConflict; + Tooltip = hotkeySettings.HasConflict ? hotkeySettings.ConflictDescription : null; + } + else + { + HasConflict = false; + Tooltip = null; + } + } + public ShortcutControl() { InitializeComponent(); @@ -136,6 +217,29 @@ namespace Microsoft.PowerToys.Settings.UI.Controls OnAllowDisableChanged(this, null); } + private void UpdateKeyVisualStyles() + { + if (PreviewKeysControl?.ItemsSource != null) + { + // Force refresh of the ItemsControl to update KeyVisual styles + var items = PreviewKeysControl.ItemsSource; + PreviewKeysControl.ItemsSource = null; + PreviewKeysControl.ItemsSource = items; + } + } + + private void UpdateTooltip() + { + if (!string.IsNullOrEmpty(Tooltip)) + { + ToolTipService.SetToolTip(EditButton, Tooltip); + } + else + { + ToolTipService.SetToolTip(EditButton, null); + } + } + private void ShortcutControl_Unloaded(object sender, RoutedEventArgs e) { shortcutDialog.PrimaryButtonClick -= ShortcutDialog_PrimaryButtonClick; @@ -147,6 +251,12 @@ namespace Microsoft.PowerToys.Settings.UI.Controls App.GetSettingsWindow().Activated -= ShortcutDialog_SettingsWindow_Activated; } + // Unsubscribe from HotkeySettings property changes + if (hotkeySettings != null) + { + hotkeySettings.PropertyChanged -= OnHotkeySettingsPropertyChanged; + } + // Dispose the HotkeySettingsControlHook object to terminate the hook threads when the textbox is unloaded hook?.Dispose(); @@ -168,6 +278,9 @@ namespace Microsoft.PowerToys.Settings.UI.Controls { App.GetSettingsWindow().Activated += ShortcutDialog_SettingsWindow_Activated; } + + // Initialize tooltip when loaded + UpdateTooltip(); } private void KeyEventHandler(int key, bool matchValue, int matchValueCode) @@ -302,6 +415,8 @@ namespace Microsoft.PowerToys.Settings.UI.Controls KeyEventHandler(key, true, key); c.Keys = internalSettings.GetKeysList(); + c.ConflictMessage = string.Empty; + c.HasConflict = false; if (internalSettings.GetKeysList().Count == 0) { @@ -336,12 +451,74 @@ namespace Microsoft.PowerToys.Settings.UI.Controls else { EnableKeys(); + if (lastValidSettings.IsValid()) + { + if (string.Equals(lastValidSettings.ToString(), hotkeySettings.ToString(), StringComparison.OrdinalIgnoreCase)) + { + c.HasConflict = hotkeySettings.HasConflict; + c.ConflictMessage = hotkeySettings.ConflictDescription; + } + else + { + // Check for conflicts with the new hotkey settings + CheckForConflicts(lastValidSettings); + } + } } } c.IsWarningAltGr = internalSettings.Ctrl && internalSettings.Alt && !internalSettings.Win && (internalSettings.Code > 0); } + private void CheckForConflicts(HotkeySettings settings) + { + void UpdateUIForConflict(bool hasConflict, HotkeyConflictResponse hotkeyConflictResponse) + { + DispatcherQueue.TryEnqueue(() => + { + if (hasConflict) + { + // Build conflict message from all conflicts - only show module names + var conflictingModules = new HashSet(); + + foreach (var conflict in hotkeyConflictResponse.AllConflicts) + { + if (!string.IsNullOrEmpty(conflict.ModuleName)) + { + conflictingModules.Add(conflict.ModuleName); + } + } + + if (conflictingModules.Count > 0) + { + var moduleNames = conflictingModules.ToArray(); + var conflictMessage = moduleNames.Length == 1 + ? $"Conflict detected with {moduleNames[0]}" + : $"Conflicts detected with: {string.Join(", ", moduleNames)}"; + + c.ConflictMessage = conflictMessage; + } + else + { + c.ConflictMessage = "Conflict detected with unknown module"; + } + + c.HasConflict = true; + } + else + { + c.ConflictMessage = string.Empty; + c.HasConflict = false; + } + }); + } + + HotkeyConflictHelper.CheckHotkeyConflict( + settings, + ShellPage.SendDefaultIPCMessage, + UpdateUIForConflict); + } + private void EnableKeys() { shortcutDialog.IsPrimaryButtonEnabled = true; @@ -416,6 +593,9 @@ namespace Microsoft.PowerToys.Settings.UI.Controls c.Keys = null; c.Keys = HotkeySettings.GetKeysList(); + c.HasConflict = hotkeySettings.HasConflict; + c.ConflictMessage = hotkeySettings.ConflictDescription; + // 92 means the Win key. The logic is: warning should be visible if the shortcut contains Alt AND contains Ctrl AND NOT contains Win. // Additional key must be present, as this is a valid, previously used shortcut shown at dialog open. Check for presence of non-modifier-key is not necessary therefore c.IsWarningAltGr = c.Keys.Contains("Ctrl") && c.Keys.Contains("Alt") && !c.Keys.Contains(92); @@ -434,16 +614,32 @@ namespace Microsoft.PowerToys.Settings.UI.Controls lastValidSettings = hotkeySettings; shortcutDialog.Hide(); + + // Send RequestAllConflicts IPC to update the UI after changed hotkey settings. + GlobalHotkeyConflictManager.Instance?.RequestAllConflicts(); } private void ShortcutDialog_PrimaryButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs args) { if (ComboIsValid(lastValidSettings)) { - HotkeySettings = lastValidSettings with { }; + if (c.HasConflict) + { + lastValidSettings = lastValidSettings with { HasConflict = true }; + } + else + { + lastValidSettings = lastValidSettings with { HasConflict = false }; + } + + HotkeySettings = lastValidSettings; } SetKeys(); + + // Send RequestAllConflicts IPC to update the UI after changed hotkey settings. + GlobalHotkeyConflictManager.Instance?.RequestAllConflicts(); + shortcutDialog.Hide(); } @@ -520,7 +716,7 @@ namespace Microsoft.PowerToys.Settings.UI.Controls private void SetKeys() { - var keys = HotkeySettings.GetKeysList(); + var keys = HotkeySettings?.GetKeysList(); if (keys != null && keys.Count > 0) { diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutDialogContentControl.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutDialogContentControl.xaml index da982289e7..13033344ab 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutDialogContentControl.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutDialogContentControl.xaml @@ -63,6 +63,13 @@ IsOpen="{Binding ElementName=ShortcutContentControl, Path=IsWarningAltGr, Mode=OneWay}" IsTabStop="{Binding ElementName=ShortcutContentControl, Path=IsWarningAltGr, Mode=OneWay}" Severity="Warning" /> + - + \ No newline at end of file diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutDialogContentControl.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutDialogContentControl.xaml.cs index 5d44f7c451..8907f12415 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutDialogContentControl.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutDialogContentControl.xaml.cs @@ -11,6 +11,24 @@ namespace Microsoft.PowerToys.Settings.UI.Controls { public sealed partial class ShortcutDialogContentControl : UserControl { + public static readonly DependencyProperty KeysProperty = DependencyProperty.Register("Keys", typeof(List), typeof(ShortcutDialogContentControl), new PropertyMetadata(default(string))); + public static readonly DependencyProperty IsErrorProperty = DependencyProperty.Register("IsError", typeof(bool), typeof(ShortcutDialogContentControl), new PropertyMetadata(false)); + public static readonly DependencyProperty IsWarningAltGrProperty = DependencyProperty.Register("IsWarningAltGr", typeof(bool), typeof(ShortcutDialogContentControl), new PropertyMetadata(false)); + public static readonly DependencyProperty HasConflictProperty = DependencyProperty.Register("HasConflict", typeof(bool), typeof(ShortcutDialogContentControl), new PropertyMetadata(false)); + public static readonly DependencyProperty ConflictMessageProperty = DependencyProperty.Register("ConflictMessage", typeof(string), typeof(ShortcutDialogContentControl), new PropertyMetadata(string.Empty)); + + public bool HasConflict + { + get => (bool)GetValue(HasConflictProperty); + set => SetValue(HasConflictProperty, value); + } + + public string ConflictMessage + { + get => (string)GetValue(ConflictMessageProperty); + set => SetValue(ConflictMessageProperty, value); + } + public ShortcutDialogContentControl() { this.InitializeComponent(); @@ -22,22 +40,16 @@ namespace Microsoft.PowerToys.Settings.UI.Controls set { SetValue(KeysProperty, value); } } - public static readonly DependencyProperty KeysProperty = DependencyProperty.Register("Keys", typeof(List), typeof(SettingsPageControl), new PropertyMetadata(default(string))); - public bool IsError { get => (bool)GetValue(IsErrorProperty); set => SetValue(IsErrorProperty, value); } - public static readonly DependencyProperty IsErrorProperty = DependencyProperty.Register("IsError", typeof(bool), typeof(ShortcutDialogContentControl), new PropertyMetadata(false)); - public bool IsWarningAltGr { get => (bool)GetValue(IsWarningAltGrProperty); set => SetValue(IsWarningAltGrProperty, value); } - - public static readonly DependencyProperty IsWarningAltGrProperty = DependencyProperty.Register("IsWarningAltGr", typeof(bool), typeof(ShortcutDialogContentControl), new PropertyMetadata(false)); } } diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutWithTextLabelControl.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutWithTextLabelControl.xaml index 78d95a4c3b..ea3be0bff8 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutWithTextLabelControl.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutWithTextLabelControl.xaml @@ -16,6 +16,7 @@ + IsTabStop="False" + Style="{StaticResource DefaultKeyVisualStyle}" /> + + + + + + + + + + + diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutWithTextLabelControl.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutWithTextLabelControl.xaml.cs index ed18669eba..c3829e3984 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutWithTextLabelControl.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutWithTextLabelControl.xaml.cs @@ -17,7 +17,7 @@ namespace Microsoft.PowerToys.Settings.UI.Controls set { SetValue(TextProperty, value); } } - public static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string), typeof(ShortcutWithTextLabelControl), new PropertyMetadata(default(string))); + public static readonly DependencyProperty TextProperty = DependencyProperty.Register(nameof(Text), typeof(string), typeof(ShortcutWithTextLabelControl), new PropertyMetadata(default(string))); public List Keys { @@ -25,11 +25,40 @@ namespace Microsoft.PowerToys.Settings.UI.Controls set { SetValue(KeysProperty, value); } } - public static readonly DependencyProperty KeysProperty = DependencyProperty.Register("Keys", typeof(List), typeof(ShortcutWithTextLabelControl), new PropertyMetadata(default(string))); + public static readonly DependencyProperty KeysProperty = DependencyProperty.Register(nameof(Keys), typeof(List), typeof(ShortcutWithTextLabelControl), new PropertyMetadata(default(string))); + + public LabelPlacement LabelPlacement + { + get { return (LabelPlacement)GetValue(LabelPlacementProperty); } + set { SetValue(LabelPlacementProperty, value); } + } + + public static readonly DependencyProperty LabelPlacementProperty = DependencyProperty.Register(nameof(LabelPlacement), typeof(LabelPlacement), typeof(ShortcutWithTextLabelControl), new PropertyMetadata(defaultValue: LabelPlacement.After, OnIsLabelPlacementChanged)); public ShortcutWithTextLabelControl() { this.InitializeComponent(); } + + private static void OnIsLabelPlacementChanged(DependencyObject d, DependencyPropertyChangedEventArgs newValue) + { + if (d is ShortcutWithTextLabelControl labelControl) + { + if (labelControl.LabelPlacement == LabelPlacement.Before) + { + VisualStateManager.GoToState(labelControl, "LabelBefore", true); + } + else + { + VisualStateManager.GoToState(labelControl, "LabelAfter", true); + } + } + } + } + + public enum LabelPlacement + { + Before, + After, } } diff --git a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeOverview.xaml b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeOverview.xaml index b04c800bca..20815cd81c 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeOverview.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeOverview.xaml @@ -20,33 +20,56 @@ - + + Margin="0,24,0,0" + Style="{StaticResource BodyStrongTextBlockStyle}" /> + + + + + + + - - - + + + + + + + + + - - - - - - - + + + + + + + - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + \ No newline at end of file diff --git a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeWhatsNew.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeWhatsNew.xaml.cs index 1b2524eee8..15fcea6452 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeWhatsNew.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeWhatsNew.xaml.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Diagnostics; using System.Linq; using System.Net; @@ -17,9 +18,11 @@ using CommunityToolkit.WinUI.UI.Controls; using global::PowerToys.GPOWrapper; using ManagedCommon; using Microsoft.PowerToys.Settings.UI.Helpers; +using Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts; using Microsoft.PowerToys.Settings.UI.OOBE.Enums; using Microsoft.PowerToys.Settings.UI.OOBE.ViewModel; using Microsoft.PowerToys.Settings.UI.SerializationContext; +using Microsoft.PowerToys.Settings.UI.Services; using Microsoft.PowerToys.Settings.UI.Views; using Microsoft.PowerToys.Telemetry; using Microsoft.UI.Xaml.Controls; @@ -27,12 +30,54 @@ using Microsoft.UI.Xaml.Navigation; namespace Microsoft.PowerToys.Settings.UI.OOBE.Views { - public sealed partial class OobeWhatsNew : Page + public sealed partial class OobeWhatsNew : Page, INotifyPropertyChanged { public OobePowerToysModule ViewModel { get; set; } + private AllHotkeyConflictsData _allHotkeyConflictsData = new AllHotkeyConflictsData(); + public bool ShowDataDiagnosticsInfoBar => GetShowDataDiagnosticsInfoBar(); + public AllHotkeyConflictsData AllHotkeyConflictsData + { + get => _allHotkeyConflictsData; + set + { + if (_allHotkeyConflictsData != value) + { + _allHotkeyConflictsData = value; + OnPropertyChanged(nameof(AllHotkeyConflictsData)); + OnPropertyChanged(nameof(HasConflicts)); + } + } + } + + public bool HasConflicts + { + get + { + if (AllHotkeyConflictsData == null) + { + return false; + } + + int count = 0; + if (AllHotkeyConflictsData.InAppConflicts != null) + { + count += AllHotkeyConflictsData.InAppConflicts.Count; + } + + if (AllHotkeyConflictsData.SystemConflicts != null) + { + count += AllHotkeyConflictsData.SystemConflicts.Count; + } + + return count > 0; + } + } + + public event PropertyChangedEventHandler PropertyChanged; + /// /// Initializes a new instance of the class. /// @@ -40,7 +85,27 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views { this.InitializeComponent(); ViewModel = new OobePowerToysModule(OobeShellPage.OobeShellHandler.Modules[(int)PowerToysModules.WhatsNew]); - DataContext = ViewModel; + DataContext = this; + + // Subscribe to hotkey conflict updates + if (GlobalHotkeyConflictManager.Instance != null) + { + GlobalHotkeyConflictManager.Instance.ConflictsUpdated += OnConflictsUpdated; + GlobalHotkeyConflictManager.Instance.RequestAllConflicts(); + } + } + + private void OnConflictsUpdated(object sender, AllHotkeyConflictsEventArgs e) + { + this.DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Normal, () => + { + AllHotkeyConflictsData = e.Conflicts ?? new AllHotkeyConflictsData(); + }); + } + + private void OnPropertyChanged(string propertyName) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } private bool GetShowDataDiagnosticsInfoBar() @@ -184,6 +249,12 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views protected override void OnNavigatedFrom(NavigationEventArgs e) { ViewModel.LogClosingModuleEvent(); + + // Unsubscribe from conflict updates when leaving the page + if (GlobalHotkeyConflictManager.Instance != null) + { + GlobalHotkeyConflictManager.Instance.ConflictsUpdated -= OnConflictsUpdated; + } } private void ReleaseNotesMarkdown_LinkClicked(object sender, LinkClickedEventArgs e) diff --git a/src/settings-ui/Settings.UI/SettingsXAML/OobeWindow.xaml b/src/settings-ui/Settings.UI/SettingsXAML/OobeWindow.xaml index 4a5f4233de..f277350fbc 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/OobeWindow.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/OobeWindow.xaml @@ -14,4 +14,4 @@ - + \ No newline at end of file diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPaste.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPaste.xaml.cs index a395ac767b..8442262688 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPaste.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPaste.xaml.cs @@ -31,6 +31,8 @@ namespace Microsoft.PowerToys.Settings.UI.Views ShellPage.SendDefaultIPCMessage); DataContext = ViewModel; InitializeComponent(); + + Loaded += (s, e) => ViewModel.OnPageLoaded(); } public void RefreshEnabledState() diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/AlwaysOnTopPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/AlwaysOnTopPage.xaml.cs index b38fffc59e..2e22da3120 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/AlwaysOnTopPage.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/AlwaysOnTopPage.xaml.cs @@ -19,6 +19,8 @@ namespace Microsoft.PowerToys.Settings.UI.Views ViewModel = new AlwaysOnTopViewModel(settingsUtils, SettingsRepository.GetInstance(settingsUtils), SettingsRepository.GetInstance(settingsUtils), ShellPage.SendDefaultIPCMessage); DataContext = ViewModel; InitializeComponent(); + + Loaded += (s, e) => ViewModel.OnPageLoaded(); } public void RefreshEnabledState() diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/CmdPalPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/CmdPalPage.xaml.cs index a9a016b80e..fb3a97e309 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/CmdPalPage.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/CmdPalPage.xaml.cs @@ -26,6 +26,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views ShellPage.SendDefaultIPCMessage, DispatcherQueue); DataContext = ViewModel; + Loaded += (s, e) => ViewModel.OnPageLoaded(); InitializeComponent(); } diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/ColorPickerPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/ColorPickerPage.xaml.cs index 37e6ffd47c..ce0f723633 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/ColorPickerPage.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/ColorPickerPage.xaml.cs @@ -35,6 +35,8 @@ namespace Microsoft.PowerToys.Settings.UI.Views ShellPage.SendDefaultIPCMessage); DataContext = ViewModel; InitializeComponent(); + + Loaded += (s, e) => ViewModel.OnPageLoaded(); } /// diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/CropAndLockPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/CropAndLockPage.xaml.cs index 66e3652da8..d769650dd1 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/CropAndLockPage.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/CropAndLockPage.xaml.cs @@ -19,6 +19,8 @@ namespace Microsoft.PowerToys.Settings.UI.Views ViewModel = new CropAndLockViewModel(settingsUtils, SettingsRepository.GetInstance(settingsUtils), SettingsRepository.GetInstance(settingsUtils), ShellPage.SendDefaultIPCMessage); DataContext = ViewModel; InitializeComponent(); + + Loaded += (s, e) => ViewModel.OnPageLoaded(); } public void RefreshEnabledState() diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/DashboardPage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/DashboardPage.xaml index 80adc56c0b..e5b800cda1 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/DashboardPage.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/DashboardPage.xaml @@ -133,7 +133,7 @@ Grid.Column="1" Orientation="Horizontal" Spacing="16"> - + diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/DashboardPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/DashboardPage.xaml.cs index 2d6cf95bae..bf792e2b75 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/DashboardPage.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/DashboardPage.xaml.cs @@ -39,6 +39,8 @@ namespace Microsoft.PowerToys.Settings.UI.Views ViewModel = new DashboardViewModel( SettingsRepository.GetInstance(settingsUtils), ShellPage.SendDefaultIPCMessage); DataContext = ViewModel; + + Loaded += (s, e) => ViewModel.OnPageLoaded(); } public void RefreshEnabledState() diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/FancyZonesPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/FancyZonesPage.xaml.cs index c224c42683..61865c89fa 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/FancyZonesPage.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/FancyZonesPage.xaml.cs @@ -19,6 +19,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views var settingsUtils = new SettingsUtils(); ViewModel = new FancyZonesViewModel(settingsUtils, SettingsRepository.GetInstance(settingsUtils), SettingsRepository.GetInstance(settingsUtils), ShellPage.SendDefaultIPCMessage); DataContext = ViewModel; + Loaded += (s, e) => ViewModel.OnPageLoaded(); } private void OpenColorsSettings_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e) diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/HostsPage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/HostsPage.xaml index 2d0a6d0c2d..6ceffa96d4 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/HostsPage.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/HostsPage.xaml @@ -56,6 +56,9 @@ + + + diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/MeasureToolPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/MeasureToolPage.xaml.cs index f48bc7cd5a..795e8a87cb 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/MeasureToolPage.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/MeasureToolPage.xaml.cs @@ -26,6 +26,8 @@ namespace Microsoft.PowerToys.Settings.UI.Views DataContext = ViewModel; InitializeComponent(); + + Loaded += (s, e) => ViewModel.OnPageLoaded(); } public void RefreshEnabledState() diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/MouseUtilsPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/MouseUtilsPage.xaml.cs index 2a0cfa536f..ab3e8192ac 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/MouseUtilsPage.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/MouseUtilsPage.xaml.cs @@ -48,6 +48,8 @@ namespace Microsoft.PowerToys.Settings.UI.Views InitializeComponent(); this.MouseUtils_MouseJump_Panel.ViewModel = ViewModel; + + Loaded += (s, e) => ViewModel.OnPageLoaded(); } public void RefreshEnabledState() diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/MouseWithoutBordersPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/MouseWithoutBordersPage.xaml.cs index f29056245f..a2e16ea987 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/MouseWithoutBordersPage.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/MouseWithoutBordersPage.xaml.cs @@ -47,6 +47,8 @@ namespace Microsoft.PowerToys.Settings.UI.Views DataContext = ViewModel; InitializeComponent(); + + Loaded += (s, e) => ViewModel.OnPageLoaded(); } private void OnConfigFileUpdate() diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/PeekPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/PeekPage.xaml.cs index 24ca93208a..91adfa9a2e 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/PeekPage.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/PeekPage.xaml.cs @@ -23,6 +23,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views DispatcherQueue); DataContext = ViewModel; InitializeComponent(); + Loaded += (s, e) => ViewModel.OnPageLoaded(); } public void RefreshEnabledState() diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/PowerLauncherPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/PowerLauncherPage.xaml.cs index f02327caa8..d8adcdc5a4 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/PowerLauncherPage.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/PowerLauncherPage.xaml.cs @@ -40,6 +40,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views PowerLauncherSettings settings = SettingsRepository.GetInstance(settingsUtils)?.SettingsConfig; ViewModel = new PowerLauncherViewModel(settings, SettingsRepository.GetInstance(settingsUtils), SendDefaultIPCMessageTimed, App.IsDarkTheme); DataContext = ViewModel; + _ = Helper.GetFileWatcher(PowerLauncherSettings.ModuleName, "settings.json", () => { if (Environment.TickCount < _lastIPCMessageSentTick + 500) @@ -79,6 +80,8 @@ namespace Microsoft.PowerToys.Settings.UI.Views searchTypePreferencesOptions.Add(Tuple.Create(loader.GetString("PowerLauncher_SearchTypePreference_ApplicationName"), "application_name")); searchTypePreferencesOptions.Add(Tuple.Create(loader.GetString("PowerLauncher_SearchTypePreference_StringInApplication"), "string_in_application")); searchTypePreferencesOptions.Add(Tuple.Create(loader.GetString("PowerLauncher_SearchTypePreference_ExecutableName"), "executable_name")); + + Loaded += (s, e) => ViewModel.OnPageLoaded(); } private void OpenColorsSettings_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e) diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/PowerOcrPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/PowerOcrPage.xaml.cs index 07b999fce0..7acd547abe 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/PowerOcrPage.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/PowerOcrPage.xaml.cs @@ -23,6 +23,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views ShellPage.SendDefaultIPCMessage); DataContext = ViewModel; InitializeComponent(); + Loaded += (s, e) => ViewModel.OnPageLoaded(); } private void TextExtractor_ComboBox_Loaded(object sender, Microsoft.UI.Xaml.RoutedEventArgs e) diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/ShellPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/ShellPage.xaml.cs index 4ed3faff9a..11835ceeb2 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/ShellPage.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/ShellPage.xaml.cs @@ -141,6 +141,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views // NL moved navigation to general page to the moment when the window is first activated (to not make flyout window disappear) // shellFrame.Navigate(typeof(GeneralPage)); IPCResponseHandleList.Add(ReceiveMessage); + Services.IPCResponseService.Instance.RegisterForIPC(); SetTitleBar(); if (_navViewParentLookup.Count > 0) diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/ShortcutGuidePage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/ShortcutGuidePage.xaml.cs index 750007595a..21b72f10ff 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/ShortcutGuidePage.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/ShortcutGuidePage.xaml.cs @@ -20,6 +20,8 @@ namespace Microsoft.PowerToys.Settings.UI.Views var settingsUtils = new SettingsUtils(); ViewModel = new ShortcutGuideViewModel(settingsUtils, SettingsRepository.GetInstance(settingsUtils), SettingsRepository.GetInstance(settingsUtils), ShellPage.SendDefaultIPCMessage); DataContext = ViewModel; + + Loaded += (s, e) => ViewModel.OnPageLoaded(); } private void OpenColorsSettings_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e) diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/WorkspacesPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/WorkspacesPage.xaml.cs index 52814104c7..1c3905a406 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/WorkspacesPage.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/WorkspacesPage.xaml.cs @@ -19,6 +19,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views ViewModel = new WorkspacesViewModel(settingsUtils, SettingsRepository.GetInstance(settingsUtils), SettingsRepository.GetInstance(settingsUtils), ShellPage.SendDefaultIPCMessage); DataContext = ViewModel; InitializeComponent(); + Loaded += (s, e) => ViewModel.OnPageLoaded(); } public void RefreshEnabledState() diff --git a/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw b/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw index 76f15a390c..17bb9267b6 100644 --- a/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw +++ b/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw @@ -2049,18 +2049,27 @@ Take a moment to preview the various utilities listed or view our comprehensive Diagnostics & feedback helps us to improve PowerToys and keep it secure, up to date, and working as expected. + + Shortcut conflict detection + + + Shortcuts configured by PowerToys are conflicting. + + + Shortcuts configured by PowerToys are conflicting + + + No conflicts found + + + All shortcuts function correctly + View more diagnostic data settings Learn more about the information PowerToys logs & how it gets used - - Diagnostic data - - - Helps us make PowerToys faster, more stable, and better over time - Turn on diagnostic data to help us improve PowerToys? @@ -5127,4 +5136,62 @@ To record a specific window, enter the hotkey with the Alt key in the opposite m Back key + + This shortcut is already in use by another utility. + + + This shortcut is already in use by a default system shortcut. + + + PowerToys shortcut conflicts + + + PowerToys shortcut conflicts + + + Conflicting shortcuts may cause unexpected behavior. Edit them here or go to the module settings to update them. + + + Conflicts found for + + + System + + + Windows system shortcut + + + This shortcut can't be changed. + + + This shortcut is used by Windows and can't be changed. + + + No conflicts detected + + + All shortcuts function correctly + + + Resolve conflicts + + + Shortcut conflicts + + + No conflicts found + + + 1 conflict found + + + {0} conflicts found + {0} is replaced with the number of conflicts + + + No leading spaces + + + Do not prepend spaces to active lines when saving the hosts file + \ No newline at end of file diff --git a/src/settings-ui/Settings.UI/ViewModels/AdvancedPasteViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/AdvancedPasteViewModel.cs index 289eec97b8..0fdf2ca940 100644 --- a/src/settings-ui/Settings.UI/ViewModels/AdvancedPasteViewModel.cs +++ b/src/settings-ui/Settings.UI/ViewModels/AdvancedPasteViewModel.cs @@ -13,8 +13,8 @@ using System.Reflection; using System.Text.Json; using System.Text.Json.Serialization; using System.Timers; - using global::PowerToys.GPOWrapper; +using Microsoft.PowerToys.Settings.UI.Helpers; using Microsoft.PowerToys.Settings.UI.Library; using Microsoft.PowerToys.Settings.UI.Library.Helpers; using Microsoft.PowerToys.Settings.UI.Library.Interfaces; @@ -24,15 +24,16 @@ using Windows.Security.Credentials; namespace Microsoft.PowerToys.Settings.UI.ViewModels { - public partial class AdvancedPasteViewModel : Observable, IDisposable + public partial class AdvancedPasteViewModel : PageViewModelBase { private static readonly HashSet WarnHotkeys = ["Ctrl + V", "Ctrl + Shift + V"]; - - private bool disposedValue; + private bool _disposed; // Delay saving of settings in order to avoid calling save multiple times and hitting file in use exception. If there is no other request to save settings in given interval, we proceed to save it; otherwise, we schedule saving it after this interval private const int SaveSettingsDelayInMs = 500; + protected override string ModuleName => AdvancedPasteSettings.ModuleName; + private GeneralSettings GeneralSettingsConfig { get; set; } private readonly ISettingsUtils _settingsUtils; @@ -98,6 +99,36 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels UpdateCustomActionsCanMoveUpDown(); } + public override Dictionary GetAllHotkeySettings() + { + var hotkeySettings = new List + { + PasteAsPlainTextShortcut, + AdvancedPasteUIShortcut, + PasteAsMarkdownShortcut, + PasteAsJsonShortcut, + }; + + foreach (var action in _additionalActions.GetAllActions()) + { + if (action is AdvancedPasteAdditionalAction additionalAction) + { + hotkeySettings.Add(additionalAction.Shortcut); + } + } + + // Custom actions do not have localization header, just use the action name. + foreach (var customAction in _customActions) + { + hotkeySettings.Add(customAction.Shortcut); + } + + return new Dictionary + { + [ModuleName] = hotkeySettings.ToArray(), + }; + } + private void InitializeEnabledValue() { _enabledGpoRuleConfiguration = GPOWrapper.GetConfiguredAdvancedPasteEnabledValue(); @@ -264,9 +295,8 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels if (_advancedPasteSettings.Properties.AdvancedPasteUIShortcut != value) { _advancedPasteSettings.Properties.AdvancedPasteUIShortcut = value ?? AdvancedPasteProperties.DefaultAdvancedPasteUIShortcut; - OnPropertyChanged(nameof(AdvancedPasteUIShortcut)); OnPropertyChanged(nameof(IsConflictingCopyShortcut)); - + OnPropertyChanged(nameof(AdvancedPasteUIShortcut)); SaveAndNotifySettings(); } } @@ -280,9 +310,8 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels if (_advancedPasteSettings.Properties.PasteAsPlainTextShortcut != value) { _advancedPasteSettings.Properties.PasteAsPlainTextShortcut = value ?? AdvancedPasteProperties.DefaultPasteAsPlainTextShortcut; - OnPropertyChanged(nameof(PasteAsPlainTextShortcut)); OnPropertyChanged(nameof(IsConflictingCopyShortcut)); - + OnPropertyChanged(nameof(PasteAsPlainTextShortcut)); SaveAndNotifySettings(); } } @@ -296,9 +325,8 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels if (_advancedPasteSettings.Properties.PasteAsMarkdownShortcut != value) { _advancedPasteSettings.Properties.PasteAsMarkdownShortcut = value ?? new HotkeySettings(); - OnPropertyChanged(nameof(PasteAsMarkdownShortcut)); OnPropertyChanged(nameof(IsConflictingCopyShortcut)); - + OnPropertyChanged(nameof(PasteAsMarkdownShortcut)); SaveAndNotifySettings(); } } @@ -312,9 +340,8 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels if (_advancedPasteSettings.Properties.PasteAsJsonShortcut != value) { _advancedPasteSettings.Properties.PasteAsJsonShortcut = value ?? new HotkeySettings(); - OnPropertyChanged(nameof(PasteAsJsonShortcut)); OnPropertyChanged(nameof(IsConflictingCopyShortcut)); - + OnPropertyChanged(nameof(PasteAsJsonShortcut)); SaveAndNotifySettings(); } } @@ -399,23 +426,31 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels OnPropertyChanged(nameof(ShowClipboardHistoryIsGpoConfiguredInfoBar)); } - protected virtual void Dispose(bool disposing) + protected override void Dispose(bool disposing) { - if (!disposedValue) + if (!_disposed) { if (disposing) { - _delayedTimer.Dispose(); + _delayedTimer?.Dispose(); + + foreach (var action in _additionalActions.GetAllActions()) + { + action.PropertyChanged -= OnAdditionalActionPropertyChanged; + } + + foreach (var customAction in _customActions) + { + customAction.PropertyChanged -= OnCustomActionPropertyChanged; + } + + _customActions.CollectionChanged -= OnCustomActionsCollectionChanged; } - disposedValue = true; + _disposed = true; } - } - public void Dispose() - { - Dispose(disposing: true); - GC.SuppressFinalize(this); + base.Dispose(disposing); } internal void DisableAI() diff --git a/src/settings-ui/Settings.UI/ViewModels/AlwaysOnTopViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/AlwaysOnTopViewModel.cs index 789ef92dfc..d9be787e70 100644 --- a/src/settings-ui/Settings.UI/ViewModels/AlwaysOnTopViewModel.cs +++ b/src/settings-ui/Settings.UI/ViewModels/AlwaysOnTopViewModel.cs @@ -3,11 +3,13 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Generic; using System.Globalization; using System.Runtime.CompilerServices; using System.Text.Json; using global::PowerToys.GPOWrapper; using ManagedCommon; +using Microsoft.PowerToys.Settings.UI.Helpers; using Microsoft.PowerToys.Settings.UI.Library; using Microsoft.PowerToys.Settings.UI.Library.Helpers; using Microsoft.PowerToys.Settings.UI.Library.Interfaces; @@ -16,8 +18,10 @@ using Microsoft.PowerToys.Settings.UI.SerializationContext; namespace Microsoft.PowerToys.Settings.UI.ViewModels { - public partial class AlwaysOnTopViewModel : Observable + public partial class AlwaysOnTopViewModel : PageViewModelBase { + protected override string ModuleName => AlwaysOnTopSettings.ModuleName; + private ISettingsUtils SettingsUtils { get; set; } private GeneralSettings GeneralSettingsConfig { get; set; } @@ -75,6 +79,16 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels } } + public override Dictionary GetAllHotkeySettings() + { + var hotkeysDict = new Dictionary + { + [ModuleName] = [Hotkey], + }; + + return hotkeysDict; + } + public bool IsEnabled { get => _isEnabled; diff --git a/src/settings-ui/Settings.UI/ViewModels/CmdPalViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/CmdPalViewModel.cs index 07806bf31a..6d7b2a0bae 100644 --- a/src/settings-ui/Settings.UI/ViewModels/CmdPalViewModel.cs +++ b/src/settings-ui/Settings.UI/ViewModels/CmdPalViewModel.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Generic; using System.IO; using System.IO.Abstractions; using System.Linq; @@ -11,6 +12,7 @@ using System.Text.Json; using System.Text.RegularExpressions; using global::PowerToys.GPOWrapper; using ManagedCommon; +using Microsoft.PowerToys.Settings.UI.Helpers; using Microsoft.PowerToys.Settings.UI.Library; using Microsoft.PowerToys.Settings.UI.Library.Helpers; using Microsoft.PowerToys.Settings.UI.Library.Interfaces; @@ -21,8 +23,10 @@ using Windows.Management.Deployment; namespace Microsoft.PowerToys.Settings.UI.ViewModels { - public class CmdPalViewModel : Observable + public class CmdPalViewModel : PageViewModelBase { + protected override string ModuleName => "CmdPal"; + private GpoRuleConfigured _enabledGpoRuleConfiguration; private bool _isEnabled; private HotkeySettings _hotkey; @@ -88,6 +92,16 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels } } + public override Dictionary GetAllHotkeySettings() + { + var hotkeysDict = new Dictionary + { + [ModuleName] = [Hotkey], + }; + + return hotkeysDict; + } + public bool IsEnabled { get => _isEnabled; diff --git a/src/settings-ui/Settings.UI/ViewModels/ColorPickerViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/ColorPickerViewModel.cs index f3084c05e8..5ea84d2caf 100644 --- a/src/settings-ui/Settings.UI/ViewModels/ColorPickerViewModel.cs +++ b/src/settings-ui/Settings.UI/ViewModels/ColorPickerViewModel.cs @@ -9,9 +9,9 @@ using System.Globalization; using System.Linq; using System.Text.Json; using System.Timers; - using global::PowerToys.GPOWrapper; using ManagedCommon; +using Microsoft.PowerToys.Settings.UI.Helpers; using Microsoft.PowerToys.Settings.UI.Library; using Microsoft.PowerToys.Settings.UI.Library.Enumerations; using Microsoft.PowerToys.Settings.UI.Library.Helpers; @@ -20,9 +20,11 @@ using Microsoft.PowerToys.Settings.UI.SerializationContext; namespace Microsoft.PowerToys.Settings.UI.ViewModels { - public partial class ColorPickerViewModel : Observable, IDisposable + public partial class ColorPickerViewModel : PageViewModelBase { - private bool disposedValue; + protected override string ModuleName => ColorPickerSettings.ModuleName; + + private bool _disposed; // Delay saving of settings in order to avoid calling save multiple times and hitting file in use exception. If there is no other request to save settings in given interval, we proceed to save it; otherwise, we schedule saving it after this interval private const int SaveSettingsDelayInMs = 500; @@ -87,6 +89,16 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels } } + public override Dictionary GetAllHotkeySettings() + { + var hotkeysDict = new Dictionary + { + [ModuleName] = [ActivationShortcut], + }; + + return hotkeysDict; + } + public bool IsEnabled { get => _isEnabled; @@ -409,23 +421,25 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels OnPropertyChanged(nameof(IsEnabled)); } - protected virtual void Dispose(bool disposing) + protected override void Dispose(bool disposing) { - if (!disposedValue) + if (!_disposed) { if (disposing) { - _delayedTimer.Dispose(); + _delayedTimer?.Dispose(); + foreach (var colorFormat in ColorFormats) + { + colorFormat.PropertyChanged -= ColorFormat_PropertyChanged; + } + + ColorFormats.CollectionChanged -= ColorFormats_CollectionChanged; } - disposedValue = true; + _disposed = true; } - } - public void Dispose() - { - Dispose(disposing: true); - GC.SuppressFinalize(this); + base.Dispose(disposing); } internal ColorFormatModel GetNewColorFormatModel() diff --git a/src/settings-ui/Settings.UI/ViewModels/CropAndLockViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/CropAndLockViewModel.cs index dc5f6846ef..e5e8a6383a 100644 --- a/src/settings-ui/Settings.UI/ViewModels/CropAndLockViewModel.cs +++ b/src/settings-ui/Settings.UI/ViewModels/CropAndLockViewModel.cs @@ -3,11 +3,12 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Generic; using System.Globalization; using System.Runtime.CompilerServices; using System.Text.Json; - using global::PowerToys.GPOWrapper; +using Microsoft.PowerToys.Settings.UI.Helpers; using Microsoft.PowerToys.Settings.UI.Library; using Microsoft.PowerToys.Settings.UI.Library.Helpers; using Microsoft.PowerToys.Settings.UI.Library.Interfaces; @@ -16,8 +17,10 @@ using Microsoft.PowerToys.Settings.UI.SerializationContext; namespace Microsoft.PowerToys.Settings.UI.ViewModels { - public partial class CropAndLockViewModel : Observable + public partial class CropAndLockViewModel : PageViewModelBase { + protected override string ModuleName => CropAndLockSettings.ModuleName; + private ISettingsUtils SettingsUtils { get; set; } private GeneralSettings GeneralSettingsConfig { get; set; } @@ -66,6 +69,16 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels } } + public override Dictionary GetAllHotkeySettings() + { + var hotkeysDict = new Dictionary + { + [ModuleName] = [ReparentActivationShortcut, ThumbnailActivationShortcut], + }; + + return hotkeysDict; + } + public bool IsEnabled { get => _isEnabled; diff --git a/src/settings-ui/Settings.UI/ViewModels/DashboardViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/DashboardViewModel.cs index 8dd97c85fa..7b62732e87 100644 --- a/src/settings-ui/Settings.UI/ViewModels/DashboardViewModel.cs +++ b/src/settings-ui/Settings.UI/ViewModels/DashboardViewModel.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.IO.Abstractions; using System.Linq; +using System.Threading.Tasks; using System.Windows.Threading; using CommunityToolkit.WinUI.Controls; using global::PowerToys.GPOWrapper; @@ -14,6 +15,7 @@ using ManagedCommon; using Microsoft.PowerToys.Settings.UI.Helpers; using Microsoft.PowerToys.Settings.UI.Library; using Microsoft.PowerToys.Settings.UI.Library.Helpers; +using Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts; using Microsoft.PowerToys.Settings.UI.Library.Interfaces; using Microsoft.PowerToys.Settings.UI.Library.Utilities; using Microsoft.PowerToys.Settings.UI.Services; @@ -23,8 +25,10 @@ using Microsoft.UI.Xaml.Controls; namespace Microsoft.PowerToys.Settings.UI.ViewModels { - public partial class DashboardViewModel : Observable + public partial class DashboardViewModel : PageViewModelBase { + protected override string ModuleName => "Dashboard"; + private const string JsonFileType = ".json"; private Dispatcher dispatcher; @@ -36,6 +40,20 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels public ObservableCollection ActionModules { get; set; } = new ObservableCollection(); + private AllHotkeyConflictsData _allHotkeyConflictsData = new AllHotkeyConflictsData(); + + public AllHotkeyConflictsData AllHotkeyConflictsData + { + get => _allHotkeyConflictsData; + set + { + if (Set(ref _allHotkeyConflictsData, value)) + { + OnPropertyChanged(); + } + } + } + public string PowerToysVersion { get @@ -66,6 +84,20 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels GetShortcutModules(); } + protected override void OnConflictsUpdated(object sender, AllHotkeyConflictsEventArgs e) + { + dispatcher.BeginInvoke(() => + { + AllHotkeyConflictsData = e.Conflicts ?? new AllHotkeyConflictsData(); + }); + } + + private void RequestConflictData() + { + // Request current conflicts data + GlobalHotkeyConflictManager.Instance?.RequestAllConflicts(); + } + private void AddDashboardListItem(ModuleType moduleType) { GpoRuleConfigured gpo = ModuleHelper.GetModuleGpoConfiguration(moduleType); @@ -93,6 +125,9 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels var settings = NewPlusViewModel.LoadSettings(settingsUtils); NewPlusViewModel.CopyTemplateExamples(settings.Properties.TemplateLocation.Value); } + + // Request updated conflicts after module state change + RequestConflictData(); } public void ModuleEnabledChangedOnSettingsPage() @@ -102,6 +137,9 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels GetShortcutModules(); OnPropertyChanged(nameof(ShortcutModules)); + + // Request updated conflicts after module state change + RequestConflictData(); } catch (Exception ex) { diff --git a/src/settings-ui/Settings.UI/ViewModels/FancyZonesViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/FancyZonesViewModel.cs index cd8ace4703..0f0ba98d11 100644 --- a/src/settings-ui/Settings.UI/ViewModels/FancyZonesViewModel.cs +++ b/src/settings-ui/Settings.UI/ViewModels/FancyZonesViewModel.cs @@ -3,9 +3,11 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Generic; using System.Runtime.CompilerServices; using global::PowerToys.GPOWrapper; using ManagedCommon; +using Microsoft.PowerToys.Settings.UI.Helpers; using Microsoft.PowerToys.Settings.UI.Library; using Microsoft.PowerToys.Settings.UI.Library.Helpers; using Microsoft.PowerToys.Settings.UI.Library.Interfaces; @@ -13,14 +15,14 @@ using Microsoft.PowerToys.Settings.UI.Library.ViewModels.Commands; namespace Microsoft.PowerToys.Settings.UI.ViewModels { - public partial class FancyZonesViewModel : Observable + public partial class FancyZonesViewModel : PageViewModelBase { - private SettingsUtils SettingsUtils { get; set; } + protected override string ModuleName => FancyZonesSettings.ModuleName; + + private ISettingsUtils SettingsUtils { get; set; } private GeneralSettings GeneralSettingsConfig { get; set; } - private const string ModuleName = FancyZonesSettings.ModuleName; - public ButtonClickCommand LaunchEditorEventHandler { get; set; } private FancyZonesSettings Settings { get; set; } @@ -44,7 +46,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels Positional = 2, } - public FancyZonesViewModel(SettingsUtils settingsUtils, ISettingsRepository settingsRepository, ISettingsRepository moduleSettingsRepository, Func ipcMSGCallBackFunc, string configFileSubfolder = "") + public FancyZonesViewModel(ISettingsUtils settingsUtils, ISettingsRepository settingsRepository, ISettingsRepository moduleSettingsRepository, Func ipcMSGCallBackFunc, string configFileSubfolder = "") { ArgumentNullException.ThrowIfNull(settingsUtils); @@ -88,8 +90,9 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels _excludedApps = Settings.Properties.FancyzonesExcludedApps.Value; _systemTheme = Settings.Properties.FancyzonesSystemTheme.Value; _showZoneNumber = Settings.Properties.FancyzonesShowZoneNumber.Value; - EditorHotkey = Settings.Properties.FancyzonesEditorHotkey.Value; _windowSwitching = Settings.Properties.FancyzonesWindowSwitching.Value; + + EditorHotkey = Settings.Properties.FancyzonesEditorHotkey.Value; NextTabHotkey = Settings.Properties.FancyzonesNextTabHotkey.Value; PrevTabHotkey = Settings.Properties.FancyzonesPrevTabHotkey.Value; @@ -134,6 +137,16 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels } } + public override Dictionary GetAllHotkeySettings() + { + var hotkeysDict = new Dictionary + { + [ModuleName] = [EditorHotkey, NextTabHotkey, PrevTabHotkey], + }; + + return hotkeysDict; + } + private GpoRuleConfigured _enabledGpoRuleConfiguration; private bool _enabledStateIsGPOConfigured; private bool _isEnabled; diff --git a/src/settings-ui/Settings.UI/ViewModels/HostsViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/HostsViewModel.cs index d2bfb989e7..34b6157d63 100644 --- a/src/settings-ui/Settings.UI/ViewModels/HostsViewModel.cs +++ b/src/settings-ui/Settings.UI/ViewModels/HostsViewModel.cs @@ -105,6 +105,19 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels } } + public bool NoLeadingSpaces + { + get => Settings.Properties.NoLeadingSpaces; + set + { + if (value != Settings.Properties.NoLeadingSpaces) + { + Settings.Properties.NoLeadingSpaces = value; + NotifyPropertyChanged(); + } + } + } + public int AdditionalLinesPosition { get => (int)Settings.Properties.AdditionalLinesPosition; diff --git a/src/settings-ui/Settings.UI/ViewModels/MeasureToolViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/MeasureToolViewModel.cs index ea66fd58dd..023cc06032 100644 --- a/src/settings-ui/Settings.UI/ViewModels/MeasureToolViewModel.cs +++ b/src/settings-ui/Settings.UI/ViewModels/MeasureToolViewModel.cs @@ -3,11 +3,12 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Generic; using System.Globalization; using System.Runtime.CompilerServices; using System.Text.Json; - using global::PowerToys.GPOWrapper; +using Microsoft.PowerToys.Settings.UI.Helpers; using Microsoft.PowerToys.Settings.UI.Library; using Microsoft.PowerToys.Settings.UI.Library.Helpers; using Microsoft.PowerToys.Settings.UI.Library.Interfaces; @@ -15,8 +16,10 @@ using Microsoft.PowerToys.Settings.UI.SerializationContext; namespace Microsoft.PowerToys.Settings.UI.ViewModels { - public partial class MeasureToolViewModel : Observable + public partial class MeasureToolViewModel : PageViewModelBase { + protected override string ModuleName => MeasureToolSettings.ModuleName; + private ISettingsUtils SettingsUtils { get; set; } private GeneralSettings GeneralSettingsConfig { get; set; } @@ -59,6 +62,16 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels } } + public override Dictionary GetAllHotkeySettings() + { + var hotkeysDict = new Dictionary + { + [ModuleName] = [ActivationShortcut], + }; + + return hotkeysDict; + } + public bool IsEnabled { get => _isEnabled; diff --git a/src/settings-ui/Settings.UI/ViewModels/MouseUtilsViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/MouseUtilsViewModel.cs index 110f682164..a3adc16e62 100644 --- a/src/settings-ui/Settings.UI/ViewModels/MouseUtilsViewModel.cs +++ b/src/settings-ui/Settings.UI/ViewModels/MouseUtilsViewModel.cs @@ -3,10 +3,11 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Generic; using System.ComponentModel; using System.Runtime.CompilerServices; - using global::PowerToys.GPOWrapper; +using Microsoft.PowerToys.Settings.UI.Helpers; using Microsoft.PowerToys.Settings.UI.Library; using Microsoft.PowerToys.Settings.UI.Library.Helpers; using Microsoft.PowerToys.Settings.UI.Library.Interfaces; @@ -14,8 +15,10 @@ using Microsoft.PowerToys.Settings.Utilities; namespace Microsoft.PowerToys.Settings.UI.ViewModels { - public partial class MouseUtilsViewModel : Observable + public partial class MouseUtilsViewModel : PageViewModelBase { + protected override string ModuleName => "MouseUtils"; + private ISettingsUtils SettingsUtils { get; set; } private GeneralSettings GeneralSettingsConfig { get; set; } @@ -101,7 +104,8 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels _mousePointerCrosshairsAutoActivate = MousePointerCrosshairsSettingsConfig.Properties.AutoActivate.Value; int isEnabled = 0; - NativeMethods.SystemParametersInfo(NativeMethods.SPI_GETCLIENTAREAANIMATION, 0, ref isEnabled, 0); + + Utilities.NativeMethods.SystemParametersInfo(Utilities.NativeMethods.SPI_GETCLIENTAREAANIMATION, 0, ref isEnabled, 0); _isAnimationEnabledBySystem = isEnabled != 0; // set the callback functions value to handle outgoing IPC message. @@ -149,6 +153,19 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels } } + public override Dictionary GetAllHotkeySettings() + { + var hotkeysDict = new Dictionary + { + [FindMyMouseSettings.ModuleName] = [FindMyMouseActivationShortcut], + [MouseHighlighterSettings.ModuleName] = [MouseHighlighterActivationShortcut], + [MousePointerCrosshairsSettings.ModuleName] = [MousePointerCrosshairsActivationShortcut], + [MouseJumpSettings.ModuleName] = [MouseJumpActivationShortcut], + }; + + return hotkeysDict; + } + public bool IsFindMyMouseEnabled { get => _isFindMyMouseEnabled; diff --git a/src/settings-ui/Settings.UI/ViewModels/MouseUtilsViewModel_MouseJump.cs b/src/settings-ui/Settings.UI/ViewModels/MouseUtilsViewModel_MouseJump.cs index e3a6f8f136..2ccd510bc9 100644 --- a/src/settings-ui/Settings.UI/ViewModels/MouseUtilsViewModel_MouseJump.cs +++ b/src/settings-ui/Settings.UI/ViewModels/MouseUtilsViewModel_MouseJump.cs @@ -25,7 +25,7 @@ using MouseJump.Common.Models.Styles; namespace Microsoft.PowerToys.Settings.UI.ViewModels { - public partial class MouseUtilsViewModel : Observable + public partial class MouseUtilsViewModel : PageViewModelBase { private GpoRuleConfigured _jumpEnabledGpoRuleConfiguration; private bool _jumpEnabledStateIsGPOConfigured; @@ -37,6 +37,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels { ArgumentNullException.ThrowIfNull(mouseJumpSettingsRepository); this.MouseJumpSettingsConfig = mouseJumpSettingsRepository.SettingsConfig; + this.MouseJumpSettingsConfig.Properties.ThumbnailSize.PropertyChanged += this.MouseJumpThumbnailSizePropertyChanged; } diff --git a/src/settings-ui/Settings.UI/ViewModels/MouseWithoutBordersViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/MouseWithoutBordersViewModel.cs index 2420ffccfd..496a8712a1 100644 --- a/src/settings-ui/Settings.UI/ViewModels/MouseWithoutBordersViewModel.cs +++ b/src/settings-ui/Settings.UI/ViewModels/MouseWithoutBordersViewModel.cs @@ -13,7 +13,6 @@ using System.Runtime.CompilerServices; using System.Text.Json; using System.Threading; using System.Threading.Tasks; - using global::PowerToys.GPOWrapper; using ManagedCommon; using Microsoft.PowerToys.Settings.UI.Helpers; @@ -30,8 +29,10 @@ using Windows.ApplicationModel.DataTransfer; namespace Microsoft.PowerToys.Settings.UI.ViewModels { - public partial class MouseWithoutBordersViewModel : Observable, IDisposable + public partial class MouseWithoutBordersViewModel : PageViewModelBase, IDisposable { + protected override string ModuleName => MouseWithoutBordersSettings.ModuleName; + // These should be in the same order as the ComboBoxItems in MouseWithoutBordersPage.xaml switch machine shortcut options private readonly int[] _switchBetweenMachineShortcutOptions = { @@ -43,18 +44,18 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels private readonly Lock _machineMatrixStringLock = new(); private static readonly Dictionary StatusColors = new Dictionary() -{ - { SocketStatus.NA, new SolidColorBrush(ColorHelper.FromArgb(0, 0x71, 0x71, 0x71)) }, - { SocketStatus.Resolving, new SolidColorBrush(Colors.Yellow) }, - { SocketStatus.Connecting, new SolidColorBrush(Colors.Orange) }, - { SocketStatus.Handshaking, new SolidColorBrush(Colors.Blue) }, - { SocketStatus.Error, new SolidColorBrush(Colors.Red) }, - { SocketStatus.ForceClosed, new SolidColorBrush(Colors.Purple) }, - { SocketStatus.InvalidKey, new SolidColorBrush(Colors.Brown) }, - { SocketStatus.Timeout, new SolidColorBrush(Colors.Pink) }, - { SocketStatus.SendError, new SolidColorBrush(Colors.Maroon) }, - { SocketStatus.Connected, new SolidColorBrush(Colors.Green) }, -}; + { + { SocketStatus.NA, new SolidColorBrush(ColorHelper.FromArgb(0, 0x71, 0x71, 0x71)) }, + { SocketStatus.Resolving, new SolidColorBrush(Colors.Yellow) }, + { SocketStatus.Connecting, new SolidColorBrush(Colors.Orange) }, + { SocketStatus.Handshaking, new SolidColorBrush(Colors.Blue) }, + { SocketStatus.Error, new SolidColorBrush(Colors.Red) }, + { SocketStatus.ForceClosed, new SolidColorBrush(Colors.Purple) }, + { SocketStatus.InvalidKey, new SolidColorBrush(Colors.Brown) }, + { SocketStatus.Timeout, new SolidColorBrush(Colors.Pink) }, + { SocketStatus.SendError, new SolidColorBrush(Colors.Maroon) }, + { SocketStatus.Connected, new SolidColorBrush(Colors.Green) }, + }; private bool _connectFieldsVisible; @@ -545,6 +546,20 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels _policyDefinedIpMappingRulesIsGPOConfigured = !string.IsNullOrWhiteSpace(_policyDefinedIpMappingRulesGPOData); } + public override Dictionary GetAllHotkeySettings() + { + var hotkeysDict = new Dictionary + { + [ModuleName] = [ + ToggleEasyMouseShortcut, + LockMachinesShortcut, + HotKeySwitch2AllPC, + ReconnectShortcut], + }; + + return hotkeysDict; + } + private void LoadViewModelFromSettings(MouseWithoutBordersSettings moduleSettings) { ArgumentNullException.ThrowIfNull(moduleSettings); @@ -998,6 +1013,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels { Settings.Properties.ToggleEasyMouseShortcut = value ?? MouseWithoutBordersProperties.DefaultHotKeyToggleEasyMouse; NotifyPropertyChanged(); + NotifyModuleUpdatedSettings(); } } } @@ -1013,6 +1029,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels Settings.Properties.LockMachineShortcut = value; Settings.Properties.LockMachineShortcut = value ?? MouseWithoutBordersProperties.DefaultHotKeyLockMachine; NotifyPropertyChanged(); + NotifyModuleUpdatedSettings(); } } } @@ -1028,6 +1045,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels Settings.Properties.ReconnectShortcut = value; Settings.Properties.ReconnectShortcut = value ?? MouseWithoutBordersProperties.DefaultHotKeyReconnect; NotifyPropertyChanged(); + NotifyModuleUpdatedSettings(); } } } @@ -1043,6 +1061,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels Settings.Properties.Switch2AllPCShortcut = value; Settings.Properties.Switch2AllPCShortcut = value ?? MouseWithoutBordersProperties.DefaultHotKeySwitch2AllPC; NotifyPropertyChanged(); + NotifyModuleUpdatedSettings(); } } } @@ -1201,11 +1220,11 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels private void NotifyModuleUpdatedSettings() { SendConfigMSG( - string.Format( - CultureInfo.InvariantCulture, - "{{ \"powertoys\": {{ \"{0}\": {1} }} }}", - MouseWithoutBordersSettings.ModuleName, - JsonSerializer.Serialize(Settings, SourceGenerationContextContext.Default.MouseWithoutBordersSettings))); + string.Format( + CultureInfo.InvariantCulture, + "{{ \"powertoys\": {{ \"{0}\": {1} }} }}", + MouseWithoutBordersSettings.ModuleName, + JsonSerializer.Serialize(Settings, SourceGenerationContextContext.Default.MouseWithoutBordersSettings))); } public void NotifyUpdatedSettings() @@ -1241,9 +1260,43 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels Clipboard.SetContent(data); } - public void Dispose() + protected override void Dispose(bool disposing) { - GC.SuppressFinalize(this); + if (disposing) + { + // Cancel the cancellation token source + _cancellationTokenSource?.Cancel(); + _cancellationTokenSource?.Dispose(); + + // Wait for the machine polling task to complete + try + { + _machinePollingThreadTask?.Wait(TimeSpan.FromSeconds(1)); + } + catch (AggregateException) + { + // Task was cancelled, which is expected + } + + // Dispose the named pipe stream + try + { + syncHelperStream?.Dispose(); + } + catch (Exception ex) + { + Logger.LogError($"Error disposing sync helper stream: {ex}"); + } + finally + { + syncHelperStream = null; + } + + // Dispose the semaphore + _ipcSemaphore?.Dispose(); + } + + base.Dispose(disposing); } internal void UninstallService() diff --git a/src/settings-ui/Settings.UI/ViewModels/PageViewModelBase.cs b/src/settings-ui/Settings.UI/ViewModels/PageViewModelBase.cs new file mode 100644 index 0000000000..78b66d6470 --- /dev/null +++ b/src/settings-ui/Settings.UI/ViewModels/PageViewModelBase.cs @@ -0,0 +1,251 @@ +// 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.Diagnostics; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.PowerToys.Settings.UI.Helpers; +using Microsoft.PowerToys.Settings.UI.Library; +using Microsoft.PowerToys.Settings.UI.Library.Helpers; +using Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts; +using Microsoft.PowerToys.Settings.UI.Services; + +namespace Microsoft.PowerToys.Settings.UI.ViewModels +{ + public abstract class PageViewModelBase : Observable, IDisposable + { + private readonly Dictionary _hotkeyConflictStatus = new Dictionary(); + private readonly Dictionary _hotkeyConflictTooltips = new Dictionary(); + private bool _disposed; + + protected abstract string ModuleName { get; } + + protected PageViewModelBase() + { + if (GlobalHotkeyConflictManager.Instance != null) + { + GlobalHotkeyConflictManager.Instance.ConflictsUpdated += OnConflictsUpdated; + } + } + + public virtual void OnPageLoaded() + { + Debug.WriteLine($"=== PAGE LOADED: {ModuleName} ==="); + GlobalHotkeyConflictManager.Instance?.RequestAllConflicts(); + } + + /// + /// Handles updates to hotkey conflicts for the module. This method is called when the + /// raises the ConflictsUpdated event. + /// + /// The source of the event, typically the instance. + /// An object containing details about the hotkey conflicts. + /// + /// Derived classes can override this method to provide custom handling for hotkey conflicts. + /// Ensure that the overridden method maintains the expected behavior of processing and logging conflict data. + /// + protected virtual void OnConflictsUpdated(object sender, AllHotkeyConflictsEventArgs e) + { + UpdateHotkeyConflictStatus(e.Conflicts); + var allHotkeySettings = GetAllHotkeySettings(); + + void UpdateConflictProperties() + { + if (allHotkeySettings != null) + { + foreach (KeyValuePair kvp in allHotkeySettings) + { + var module = kvp.Key; + var hotkeySettingsList = kvp.Value; + + for (int i = 0; i < hotkeySettingsList.Length; i++) + { + var key = $"{module.ToLowerInvariant()}_{i}"; + hotkeySettingsList[i].HasConflict = GetHotkeyConflictStatus(key); + hotkeySettingsList[i].ConflictDescription = GetHotkeyConflictTooltip(key); + } + } + } + } + + _ = Task.Run(() => + { + try + { + var settingsWindow = App.GetSettingsWindow(); + settingsWindow.DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Normal, UpdateConflictProperties); + } + catch + { + UpdateConflictProperties(); + } + }); + } + + public virtual Dictionary GetAllHotkeySettings() + { + return null; + } + + protected ModuleConflictsData GetModuleRelatedConflicts(AllHotkeyConflictsData allConflicts) + { + var moduleConflicts = new ModuleConflictsData(); + + if (allConflicts.InAppConflicts != null) + { + foreach (var conflict in allConflicts.InAppConflicts) + { + if (IsModuleInvolved(conflict)) + { + moduleConflicts.InAppConflicts.Add(conflict); + } + } + } + + if (allConflicts.SystemConflicts != null) + { + foreach (var conflict in allConflicts.SystemConflicts) + { + if (IsModuleInvolved(conflict)) + { + moduleConflicts.SystemConflicts.Add(conflict); + } + } + } + + return moduleConflicts; + } + + private void ProcessMouseUtilsConflictGroup(HotkeyConflictGroupData conflict, HashSet mouseUtilsModules, bool isSysConflict) + { + // Check if any of the modules in this conflict are MouseUtils submodules + var involvedMouseUtilsModules = conflict.Modules + .Where(module => mouseUtilsModules.Contains(module.ModuleName)) + .ToList(); + + if (involvedMouseUtilsModules.Count != 0) + { + // For each involved MouseUtils module, mark the hotkey as having a conflict + foreach (var module in involvedMouseUtilsModules) + { + string hotkeyKey = $"{module.ModuleName.ToLowerInvariant()}_{module.HotkeyID}"; + _hotkeyConflictStatus[hotkeyKey] = true; + _hotkeyConflictTooltips[hotkeyKey] = isSysConflict + ? ResourceLoaderInstance.ResourceLoader.GetString("SysHotkeyConflictTooltipText") + : ResourceLoaderInstance.ResourceLoader.GetString("InAppHotkeyConflictTooltipText"); + } + } + } + + protected virtual void UpdateHotkeyConflictStatus(AllHotkeyConflictsData allConflicts) + { + _hotkeyConflictStatus.Clear(); + _hotkeyConflictTooltips.Clear(); + + // Since MouseUtils in Settings consolidates four modules: Find My Mouse, Mouse Highlighter, Mouse Pointer Crosshairs, and Mouse Jump + // We need to handle this case separately here. + if (string.Equals(ModuleName, "MouseUtils", StringComparison.OrdinalIgnoreCase)) + { + var mouseUtilsModules = new HashSet + { + FindMyMouseSettings.ModuleName, + MouseHighlighterSettings.ModuleName, + MousePointerCrosshairsSettings.ModuleName, + MouseJumpSettings.ModuleName, + }; + + // Process in-app conflicts + foreach (var conflict in allConflicts.InAppConflicts) + { + ProcessMouseUtilsConflictGroup(conflict, mouseUtilsModules, false); + } + + // Process system conflicts + foreach (var conflict in allConflicts.SystemConflicts) + { + ProcessMouseUtilsConflictGroup(conflict, mouseUtilsModules, true); + } + } + else + { + if (allConflicts.InAppConflicts.Count > 0) + { + foreach (var conflictGroup in allConflicts.InAppConflicts) + { + foreach (var conflict in conflictGroup.Modules) + { + if (string.Equals(conflict.ModuleName, ModuleName, StringComparison.OrdinalIgnoreCase)) + { + var keyName = $"{conflict.ModuleName.ToLowerInvariant()}_{conflict.HotkeyID}"; + _hotkeyConflictStatus[keyName] = true; + _hotkeyConflictTooltips[keyName] = ResourceLoaderInstance.ResourceLoader.GetString("InAppHotkeyConflictTooltipText"); + } + } + } + } + + if (allConflicts.SystemConflicts.Count > 0) + { + foreach (var conflictGroup in allConflicts.SystemConflicts) + { + foreach (var conflict in conflictGroup.Modules) + { + if (string.Equals(conflict.ModuleName, ModuleName, StringComparison.OrdinalIgnoreCase)) + { + var keyName = $"{conflict.ModuleName.ToLowerInvariant()}_{conflict.HotkeyID}"; + _hotkeyConflictStatus[keyName] = true; + _hotkeyConflictTooltips[keyName] = ResourceLoaderInstance.ResourceLoader.GetString("SysHotkeyConflictTooltipText"); + } + } + } + } + } + } + + protected virtual bool GetHotkeyConflictStatus(string key) + { + return _hotkeyConflictStatus.ContainsKey(key) && _hotkeyConflictStatus[key]; + } + + protected virtual string GetHotkeyConflictTooltip(string key) + { + return _hotkeyConflictTooltips.TryGetValue(key, out string value) ? value : null; + } + + private bool IsModuleInvolved(HotkeyConflictGroupData conflict) + { + if (conflict.Modules == null) + { + return false; + } + + return conflict.Modules.Any(module => + string.Equals(module.ModuleName, ModuleName, StringComparison.OrdinalIgnoreCase)); + } + + public virtual void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + if (GlobalHotkeyConflictManager.Instance != null) + { + GlobalHotkeyConflictManager.Instance.ConflictsUpdated -= OnConflictsUpdated; + } + } + + _disposed = true; + } + } + } +} diff --git a/src/settings-ui/Settings.UI/ViewModels/PeekViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/PeekViewModel.cs index a96a1aeec5..3688e2e14d 100644 --- a/src/settings-ui/Settings.UI/ViewModels/PeekViewModel.cs +++ b/src/settings-ui/Settings.UI/ViewModels/PeekViewModel.cs @@ -3,13 +3,14 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Generic; using System.Globalization; using System.IO; using System.IO.Abstractions; using System.Text.Json; - using global::PowerToys.GPOWrapper; using ManagedCommon; +using Microsoft.PowerToys.Settings.UI.Helpers; using Microsoft.PowerToys.Settings.UI.Library; using Microsoft.PowerToys.Settings.UI.Library.Helpers; using Microsoft.PowerToys.Settings.UI.Library.Interfaces; @@ -20,10 +21,14 @@ using Settings.UI.Library; namespace Microsoft.PowerToys.Settings.UI.ViewModels { - public class PeekViewModel : Observable, IDisposable + public class PeekViewModel : PageViewModelBase { + protected override string ModuleName => PeekSettings.ModuleName; + private bool _isEnabled; + private bool _disposed; + private bool _settingsUpdating; private GeneralSettings GeneralSettingsConfig { get; set; } @@ -59,6 +64,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels // Load the application-specific settings, including preview items. _peekSettings = _settingsUtils.GetSettingsOrDefault(PeekSettings.ModuleName); _peekPreviewSettings = _settingsUtils.GetSettingsOrDefault(PeekSettings.ModuleName, PeekPreviewSettings.FileName); + SetupSettingsFileWatcher(); InitializeEnabledValue(); @@ -118,6 +124,16 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels } } + public override Dictionary GetAllHotkeySettings() + { + var hotkeysDict = new Dictionary + { + [ModuleName] = [ActivationShortcut], + }; + + return hotkeysDict; + } + public bool IsEnabled { get => _isEnabled; @@ -302,11 +318,20 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels OnPropertyChanged(nameof(IsEnabled)); } - public void Dispose() + protected override void Dispose(bool disposing) { - _watcher?.Dispose(); + if (!_disposed) + { + if (disposing) + { + _watcher?.Dispose(); + _watcher = null; + } - GC.SuppressFinalize(this); + _disposed = true; + } + + base.Dispose(disposing); } } } diff --git a/src/settings-ui/Settings.UI/ViewModels/PowerLauncherViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/PowerLauncherViewModel.cs index 8c02d58319..31efe28260 100644 --- a/src/settings-ui/Settings.UI/ViewModels/PowerLauncherViewModel.cs +++ b/src/settings-ui/Settings.UI/ViewModels/PowerLauncherViewModel.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; using System.Globalization; @@ -10,9 +11,9 @@ using System.Linq; using System.Runtime.CompilerServices; using System.Text.Json; using System.Windows.Input; - using global::PowerToys.GPOWrapper; using ManagedCommon; +using Microsoft.PowerToys.Settings.UI.Helpers; using Microsoft.PowerToys.Settings.UI.Library; using Microsoft.PowerToys.Settings.UI.Library.Helpers; using Microsoft.PowerToys.Settings.UI.Library.Interfaces; @@ -21,7 +22,7 @@ using Microsoft.PowerToys.Settings.UI.SerializationContext; namespace Microsoft.PowerToys.Settings.UI.ViewModels { - public partial class PowerLauncherViewModel : Observable + public partial class PowerLauncherViewModel : PageViewModelBase, IDisposable { private int _themeIndex; private int _monitorPositionIndex; @@ -37,6 +38,8 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels public delegate void SendCallback(PowerLauncherSettings settings); + protected override string ModuleName => PowerLauncherSettings.ModuleName; + private readonly SendCallback callback; private readonly Func isDark; @@ -122,6 +125,16 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels } } + public override Dictionary GetAllHotkeySettings() + { + var hotkeysDict = new Dictionary + { + [ModuleName] = [OpenPowerLauncher], + }; + + return hotkeysDict; + } + private void OnPluginInfoChange(object sender, PropertyChangedEventArgs e) { if ( diff --git a/src/settings-ui/Settings.UI/ViewModels/PowerOcrViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/PowerOcrViewModel.cs index fced94ad06..cb67dfc237 100644 --- a/src/settings-ui/Settings.UI/ViewModels/PowerOcrViewModel.cs +++ b/src/settings-ui/Settings.UI/ViewModels/PowerOcrViewModel.cs @@ -11,6 +11,7 @@ using System.Text.Json; using System.Timers; using global::PowerToys.GPOWrapper; using ManagedCommon; +using Microsoft.PowerToys.Settings.UI.Helpers; using Microsoft.PowerToys.Settings.UI.Library; using Microsoft.PowerToys.Settings.UI.Library.Helpers; using Microsoft.PowerToys.Settings.UI.Library.Interfaces; @@ -20,9 +21,11 @@ using Windows.Media.Ocr; namespace Microsoft.PowerToys.Settings.UI.ViewModels { - public partial class PowerOcrViewModel : Observable, IDisposable + public partial class PowerOcrViewModel : PageViewModelBase { - private bool disposedValue; + protected override string ModuleName => PowerOcrSettings.ModuleName; + + private bool _disposed; // Delay saving of settings in order to avoid calling save multiple times and hitting file in use exception. If there is no other request to save settings in given interval, we proceed to save it; otherwise, we schedule saving it after this interval private const int SaveSettingsDelayInMs = 500; @@ -114,6 +117,16 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels } } + public override Dictionary GetAllHotkeySettings() + { + var hotkeysDict = new Dictionary + { + [ModuleName] = [ActivationShortcut], + }; + + return hotkeysDict; + } + public bool IsEnabled { get => _isEnabled; @@ -246,23 +259,20 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels OnPropertyChanged(nameof(IsEnabled)); } - protected virtual void Dispose(bool disposing) + protected override void Dispose(bool disposing) { - if (!disposedValue) + if (!_disposed) { if (disposing) { - _delayedTimer.Dispose(); + _delayedTimer?.Dispose(); + _delayedTimer = null; } - disposedValue = true; + _disposed = true; } - } - public void Dispose() - { - Dispose(disposing: true); - GC.SuppressFinalize(this); + base.Dispose(disposing); } public string SnippingToolInfoBarMargin diff --git a/src/settings-ui/Settings.UI/ViewModels/ShortcutConflictViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/ShortcutConflictViewModel.cs new file mode 100644 index 0000000000..b489d29fca --- /dev/null +++ b/src/settings-ui/Settings.UI/ViewModels/ShortcutConflictViewModel.cs @@ -0,0 +1,384 @@ +// 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.Collections.ObjectModel; +using System.ComponentModel; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; +using System.Windows; +using System.Windows.Threading; +using ManagedCommon; +using Microsoft.PowerToys.Settings.UI.Helpers; +using Microsoft.PowerToys.Settings.UI.Library; +using Microsoft.PowerToys.Settings.UI.Library.Helpers; +using Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts; +using Microsoft.PowerToys.Settings.UI.Library.Interfaces; +using Microsoft.PowerToys.Settings.UI.SerializationContext; +using Microsoft.PowerToys.Settings.UI.Services; +using Microsoft.Windows.ApplicationModel.Resources; + +namespace Microsoft.PowerToys.Settings.UI.ViewModels +{ + public class ShortcutConflictViewModel : PageViewModelBase + { + private readonly SettingsFactory _settingsFactory; + private readonly Func _ipcMSGCallBackFunc; + private readonly Dispatcher _dispatcher; + + private bool _disposed; + private AllHotkeyConflictsData _conflictsData = new(); + private ObservableCollection _conflictItems = new(); + private ResourceLoader resourceLoader; + + public ShortcutConflictViewModel( + ISettingsUtils settingsUtils, + ISettingsRepository settingsRepository, + Func ipcMSGCallBackFunc) + { + _dispatcher = Dispatcher.CurrentDispatcher; + _ipcMSGCallBackFunc = ipcMSGCallBackFunc ?? throw new ArgumentNullException(nameof(ipcMSGCallBackFunc)); + resourceLoader = ResourceLoaderInstance.ResourceLoader; + + // Create SettingsFactory + _settingsFactory = new SettingsFactory(settingsUtils ?? throw new ArgumentNullException(nameof(settingsUtils))); + } + + public AllHotkeyConflictsData ConflictsData + { + get => _conflictsData; + set + { + if (Set(ref _conflictsData, value)) + { + UpdateConflictItems(); + } + } + } + + public ObservableCollection ConflictItems + { + get => _conflictItems; + private set => Set(ref _conflictItems, value); + } + + protected override string ModuleName => "ShortcutConflictsWindow"; + + private IHotkeyConfig GetModuleSettings(string moduleKey) + { + try + { + // MouseWithoutBorders and Peek settings may be changed by the logic in the utility as machines connect. + // We need to get a fresh version every time instead of using a repository. + if (string.Equals(moduleKey, MouseWithoutBordersSettings.ModuleName, StringComparison.OrdinalIgnoreCase) || + string.Equals(moduleKey, PeekSettings.ModuleName, StringComparison.OrdinalIgnoreCase)) + { + return _settingsFactory.GetFreshSettings(moduleKey); + } + + // For other modules, get the settings from SettingsRepository + return _settingsFactory.GetSettings(moduleKey); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Error loading settings for {moduleKey}: {ex.Message}"); + return null; + } + } + + protected override void OnConflictsUpdated(object sender, AllHotkeyConflictsEventArgs e) + { + _dispatcher.BeginInvoke(() => + { + ConflictsData = e.Conflicts ?? new AllHotkeyConflictsData(); + }); + } + + private void UpdateConflictItems() + { + var items = new ObservableCollection(); + + ProcessConflicts(ConflictsData?.InAppConflicts, false, items); + ProcessConflicts(ConflictsData?.SystemConflicts, true, items); + + ConflictItems = items; + OnPropertyChanged(nameof(ConflictItems)); + } + + private void ProcessConflicts(IEnumerable conflicts, bool isSystemConflict, ObservableCollection items) + { + if (conflicts == null) + { + return; + } + + foreach (var conflict in conflicts) + { + ProcessConflictGroup(conflict, isSystemConflict); + items.Add(conflict); + } + } + + private void ProcessConflictGroup(HotkeyConflictGroupData conflict, bool isSystemConflict) + { + foreach (var module in conflict.Modules) + { + SetupModuleData(module, isSystemConflict); + } + } + + private void SetupModuleData(ModuleHotkeyData module, bool isSystemConflict) + { + try + { + var settings = GetModuleSettings(module.ModuleName); + var allHotkeyAccessors = settings.GetAllHotkeyAccessors(); + var hotkeyAccessor = allHotkeyAccessors[module.HotkeyID]; + + if (hotkeyAccessor != null) + { + // Get current hotkey settings (fresh from file) using the accessor's getter + module.HotkeySettings = hotkeyAccessor.Value; + + // Set header using localization key + module.Header = GetHotkeyLocalizationHeader(module.ModuleName, module.HotkeyID, hotkeyAccessor.LocalizationHeaderKey); + module.IsSystemConflict = isSystemConflict; + + // Set module display info + var moduleType = settings.GetModuleType(); + module.ModuleType = moduleType; + var displayName = resourceLoader.GetString(ModuleHelper.GetModuleLabelResourceName(moduleType)); + module.DisplayName = displayName; + module.IconPath = ModuleHelper.GetModuleTypeFluentIconName(moduleType); + + if (module.HotkeySettings != null) + { + SetConflictProperties(module.HotkeySettings, isSystemConflict); + } + + module.PropertyChanged -= OnModuleHotkeyDataPropertyChanged; + module.PropertyChanged += OnModuleHotkeyDataPropertyChanged; + } + else + { + System.Diagnostics.Debug.WriteLine($"Could not find hotkey accessor for {module.ModuleName}.{module.HotkeyID}"); + } + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Error setting up module data for {module.ModuleName}: {ex.Message}"); + } + } + + private void SetConflictProperties(HotkeySettings settings, bool isSystemConflict) + { + settings.HasConflict = true; + settings.IsSystemConflict = isSystemConflict; + } + + private void OnModuleHotkeyDataPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (sender is ModuleHotkeyData moduleData && e.PropertyName == nameof(ModuleHotkeyData.HotkeySettings)) + { + UpdateModuleHotkeySettings(moduleData.ModuleName, moduleData.HotkeyID, moduleData.HotkeySettings); + } + } + + private void UpdateModuleHotkeySettings(string moduleName, int hotkeyID, HotkeySettings newHotkeySettings) + { + try + { + var settings = GetModuleSettings(moduleName); + var accessors = settings.GetAllHotkeyAccessors(); + + var hotkeyAccessor = accessors[hotkeyID]; + + // Use the accessor's setter to update the hotkey settings + hotkeyAccessor.Value = newHotkeySettings; + + if (settings is ISettingsConfig settingsConfig) + { + // No need to save settings here, the runner will call module interface to save it + // SaveSettingsToFile(settings); + + // Send IPC notification using the same format as other ViewModels + SendConfigMSG(settingsConfig, moduleName); + } + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Error updating hotkey settings for {moduleName}.{hotkeyID}: {ex.Message}"); + } + } + + private void SaveModuleSettingsAndNotify(string moduleName) + { + try + { + var settings = GetModuleSettings(moduleName); + + if (settings is ISettingsConfig settingsConfig) + { + // No need to save settings here, the runner will call module interface to save it + // SaveSettingsToFile(settings); + + // Send IPC notification using the same format as other ViewModels + SendConfigMSG(settingsConfig, moduleName); + + System.Diagnostics.Debug.WriteLine($"Saved settings and sent IPC notification for module: {moduleName}"); + } + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Error saving settings and notifying for {moduleName}: {ex.Message}"); + } + } + + private void SaveSettingsToFile(IHotkeyConfig settings) + { + try + { + // Get the repository for this settings type using reflection + var settingsType = settings.GetType(); + var repositoryMethod = typeof(SettingsFactory).GetMethod("GetRepository"); + if (repositoryMethod != null) + { + var genericMethod = repositoryMethod.MakeGenericMethod(settingsType); + var repository = genericMethod.Invoke(_settingsFactory, null); + + if (repository != null) + { + var saveMethod = repository.GetType().GetMethod("SaveSettingsToFile"); + saveMethod?.Invoke(repository, null); + System.Diagnostics.Debug.WriteLine($"Saved settings to file for type: {settingsType.Name}"); + } + } + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Error saving settings to file: {ex.Message}"); + } + } + + /// + /// Sends IPC notification using the same format as other ViewModels + /// + private void SendConfigMSG(ISettingsConfig settingsConfig, string moduleName) + { + try + { + var jsonTypeInfo = GetJsonTypeInfo(settingsConfig.GetType()); + var serializedSettings = jsonTypeInfo != null + ? JsonSerializer.Serialize(settingsConfig, jsonTypeInfo) + : JsonSerializer.Serialize(settingsConfig); + + var ipcMessage = string.Format( + CultureInfo.InvariantCulture, + "{{ \"powertoys\": {{ \"{0}\": {1} }} }}", + moduleName, + serializedSettings); + + var result = _ipcMSGCallBackFunc(ipcMessage); + System.Diagnostics.Debug.WriteLine($"Sent IPC notification for {moduleName}, result: {result}"); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Error sending IPC notification for {moduleName}: {ex.Message}"); + } + } + + private JsonTypeInfo GetJsonTypeInfo(Type settingsType) + { + try + { + var contextType = typeof(SourceGenerationContextContext); + var defaultProperty = contextType.GetProperty("Default", BindingFlags.Public | BindingFlags.Static); + var defaultContext = defaultProperty?.GetValue(null) as JsonSerializerContext; + + if (defaultContext != null) + { + var typeInfoProperty = contextType.GetProperties(BindingFlags.Public | BindingFlags.Instance) + .FirstOrDefault(p => p.PropertyType.IsGenericType && + p.PropertyType.GetGenericTypeDefinition() == typeof(JsonTypeInfo<>) && + p.PropertyType.GetGenericArguments()[0] == settingsType); + + return typeInfoProperty?.GetValue(defaultContext) as JsonTypeInfo; + } + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Error getting JsonTypeInfo for {settingsType.Name}: {ex.Message}"); + } + + return null; + } + + private string GetHotkeyLocalizationHeader(string moduleName, int hotkeyID, string headerKey) + { + // Handle AdvancedPaste custom actions + if (string.Equals(moduleName, AdvancedPasteSettings.ModuleName, StringComparison.OrdinalIgnoreCase) + && hotkeyID > 9) + { + return headerKey; + } + + try + { + return resourceLoader.GetString($"{headerKey}/Header"); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Error getting hotkey header for {moduleName}.{hotkeyID}: {ex.Message}"); + return headerKey; // Return the key itself as fallback + } + } + + protected override void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + UnsubscribeFromEvents(); + } + + _disposed = true; + } + + base.Dispose(disposing); + } + + private void UnsubscribeFromEvents() + { + try + { + if (ConflictItems != null) + { + foreach (var conflictGroup in ConflictItems) + { + if (conflictGroup?.Modules != null) + { + foreach (var module in conflictGroup.Modules) + { + if (module != null) + { + module.PropertyChanged -= OnModuleHotkeyDataPropertyChanged; + } + } + } + } + } + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Error unsubscribing from events: {ex.Message}"); + } + } + } +} diff --git a/src/settings-ui/Settings.UI/ViewModels/ShortcutGuideViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/ShortcutGuideViewModel.cs index 6ae2dd0746..1f25f02dfb 100644 --- a/src/settings-ui/Settings.UI/ViewModels/ShortcutGuideViewModel.cs +++ b/src/settings-ui/Settings.UI/ViewModels/ShortcutGuideViewModel.cs @@ -3,25 +3,29 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Generic; +using System.Globalization; using System.Runtime.CompilerServices; - +using System.Text.Json; using global::PowerToys.GPOWrapper; +using Microsoft.PowerToys.Settings.UI.Helpers; using Microsoft.PowerToys.Settings.UI.Library; using Microsoft.PowerToys.Settings.UI.Library.Helpers; using Microsoft.PowerToys.Settings.UI.Library.Interfaces; +using Microsoft.PowerToys.Settings.UI.SerializationContext; namespace Microsoft.PowerToys.Settings.UI.ViewModels { - public partial class ShortcutGuideViewModel : Observable + public partial class ShortcutGuideViewModel : PageViewModelBase { + protected override string ModuleName => ShortcutGuideSettings.ModuleName; + private ISettingsUtils SettingsUtils { get; set; } private GeneralSettings GeneralSettingsConfig { get; set; } private ShortcutGuideSettings Settings { get; set; } - private const string ModuleName = ShortcutGuideSettings.ModuleName; - private Func SendConfigMSG { get; } private string _settingsConfigFileFolder = string.Empty; @@ -79,6 +83,16 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels } } + public override Dictionary GetAllHotkeySettings() + { + var hotkeysDict = new Dictionary + { + [ModuleName] = [OpenShortcutGuide], + }; + + return hotkeysDict; + } + private GpoRuleConfigured _enabledGpoRuleConfiguration; private bool _enabledStateIsGPOConfigured; private bool _isEnabled; diff --git a/src/settings-ui/Settings.UI/ViewModels/WorkspacesViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/WorkspacesViewModel.cs index e24b2ce597..2c05c79358 100644 --- a/src/settings-ui/Settings.UI/ViewModels/WorkspacesViewModel.cs +++ b/src/settings-ui/Settings.UI/ViewModels/WorkspacesViewModel.cs @@ -3,11 +3,12 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Generic; using System.Globalization; using System.Runtime.CompilerServices; using System.Text.Json; - using global::PowerToys.GPOWrapper; +using Microsoft.PowerToys.Settings.UI.Helpers; using Microsoft.PowerToys.Settings.UI.Library; using Microsoft.PowerToys.Settings.UI.Library.Helpers; using Microsoft.PowerToys.Settings.UI.Library.Interfaces; @@ -16,8 +17,10 @@ using Microsoft.PowerToys.Settings.UI.SerializationContext; namespace Microsoft.PowerToys.Settings.UI.ViewModels { - public partial class WorkspacesViewModel : Observable + public partial class WorkspacesViewModel : PageViewModelBase { + protected override string ModuleName => WorkspacesSettings.ModuleName; + private ISettingsUtils SettingsUtils { get; set; } private GeneralSettings GeneralSettingsConfig { get; set; } @@ -75,6 +78,16 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels } } + public override Dictionary GetAllHotkeySettings() + { + var hotkeysDict = new Dictionary + { + [ModuleName] = [Hotkey], + }; + + return hotkeysDict; + } + public bool IsEnabled { get => _isEnabled;