diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index c5b83b656db..f0795e524be 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -112,7 +112,7 @@ AUTHN AUTHZ AUTOHIDE AUTOMATIONPROPERTIES -autorun +Autorun AUTOUPDATE AValid awakeness @@ -136,6 +136,7 @@ BITMAPFILEHEADER bitmapimage BITMAPINFO BITMAPINFOHEADER +Bitmaps bitmask BITSPIXEL bla @@ -144,7 +145,7 @@ Blockquotes blogs Blt BLUEGRAY -bluetooth +Bluetooth BLURBEHIND BLURREGION bmi @@ -1369,7 +1370,6 @@ pef PElems Pels PERCEIVEDFLAG -Percision perfmon pesi petabyte diff --git a/Cpp.Build.props b/Cpp.Build.props index 2da75a85876..5eb275a5076 100644 --- a/Cpp.Build.props +++ b/Cpp.Build.props @@ -42,6 +42,9 @@ arm64 false $(MSBuildThisFileFullPath)\..\deps\;$(MSBuildThisFileFullPath)\..\packages\;$(ExternalIncludePath) + + + Guard @@ -53,9 +56,12 @@ false true stdcpplatest - false + false /await %(AdditionalOptions) _UNICODE;UNICODE;%(PreprocessorDefinitions) + + Guard + ProgramDatabase Windows diff --git a/Directory.Packages.props b/Directory.Packages.props index ff663341762..d26fb2dbfb2 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -40,7 +40,7 @@ - + diff --git a/NOTICE.md b/NOTICE.md index cbe70501233..07378214cc7 100644 --- a/NOTICE.md +++ b/NOTICE.md @@ -341,7 +341,7 @@ SOFTWARE. - Microsoft.Windows.CsWinRT 2.0.3 - Microsoft.Windows.SDK.BuildTools 10.0.22621.756 - Microsoft.Windows.SDK.Contracts 10.0.19041.1 -- Microsoft.WindowsAppSDK 1.4.230822000 +- Microsoft.WindowsAppSDK 1.4.230913002 - Microsoft.Xaml.Behaviors.WinUI.Managed 2.0.9 - Microsoft.Xaml.Behaviors.Wpf 1.1.39 - ModernWpfUI 0.9.4 diff --git a/README.md b/README.md index 3923a7c8278..d61ddeab830 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,22 @@ Microsoft PowerToys is a set of utilities for power users to tune and streamline ### Via GitHub with EXE [Recommended] -Go to [Microsoft PowerToys GitHub releases page][github-release-link], 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. +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=project%3Amicrosoft%2FPowerToys%2F47 -[github-current-release-work]: https://github.com/microsoft/PowerToys/issues?q=project%3Amicrosoft%2FPowerToys%2F46 -[ptUserX64]: https://github.com/microsoft/PowerToys/releases/download/v0.73.0/PowerToysUserSetup-0.73.0-x64.exe -[ptUserArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.73.0/PowerToysUserSetup-0.73.0-arm64.exe -[ptMachineX64]: https://github.com/microsoft/PowerToys/releases/download/v0.73.0/PowerToysSetup-0.73.0-x64.exe -[ptMachineArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.73.0/PowerToysSetup-0.73.0-arm64.exe +[github-next-release-work]: https://github.com/microsoft/PowerToys/issues?q=project%3Amicrosoft%2FPowerToys%2F48 +[github-current-release-work]: https://github.com/microsoft/PowerToys/issues?q=project%3Amicrosoft%2FPowerToys%2F47 +[ptUserX64]: https://github.com/microsoft/PowerToys/releases/download/v0.74.1/PowerToysUserSetup-0.74.1-x64.exe +[ptUserArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.74.1/PowerToysUserSetup-0.74.1-arm64.exe +[ptMachineX64]: https://github.com/microsoft/PowerToys/releases/download/v0.74.1/PowerToysSetup-0.74.1-x64.exe +[ptMachineArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.74.1/PowerToysSetup-0.74.1-arm64.exe | Description | Filename | sha256 hash | |----------------|----------|-------------| -| Per user - x64 | [PowerToysUserSetup-0.73.0-x64.exe][ptUserX64] | BA55D245BDD734FD6F19803DD706A3AB8E0ABC491591195534997CF2122D3B7E | -| Per user - ARM64 | [PowerToysUserSetup-0.73.0-arm64.exe][ptUserArm64] | FBFA40EA5FFA05236A7CCDD05E5142EE0C93D7485B965784196ED9B086BFEBF4 | -| Machine wide - x64 | [PowerToysSetup-0.73.0-x64.exe][ptMachineX64] | 7FDA06292C7C2E6DA5AEF88D8E9D3DE89D331E9E356A232289F9B37CE4503894 | -| Machine wide - ARM64 | [PowerToysSetup-0.73.0-arm64.exe][ptMachineArm64] | 4260AA30A1F52F194EE07E9E7ECD9E9F4CF35289267F213BC933F7A5191AC17C | +| Per user - x64 | [PowerToysUserSetup-0.74.1-x64.exe][ptUserX64] | 748BF7BA33913237D36D6F48E3839D0C8035967305137A17DEFF39D775735C81 | +| Per user - ARM64 | [PowerToysUserSetup-0.74.1-arm64.exe][ptUserArm64] | F5DAA89A9CF3A2805E121085AFD056A890F241A170FAB5007AA58E2755C88C54 | +| Machine wide - x64 | [PowerToysSetup-0.74.1-x64.exe][ptMachineX64] | 298C6F4E4391BDC06E128BED86A303C3300A68EAF754B4630AF7542C78C0944A | +| Machine wide - ARM64 | [PowerToysSetup-0.74.1-arm64.exe][ptMachineArm64] | A65F3C300A48F9F81312B7FC7B306382CB87F591612D0CEC7E5C0E47E868904B | This is our preferred method. @@ -79,15 +79,15 @@ There are [community driven install methods](./doc/unofficialInstallMethods.md) ## Third-Party Run Plugins -Collection of [third-party plugins](./doc/thirdPartyRunPlugins.md) created by the community that aren't distributed with PowerToys. +There is a collection of [third-party plugins](./doc/thirdPartyRunPlugins.md) created by the community that aren't distributed with PowerToys. ## Contributing -This project welcomes contributions of all types. Help spec'ing, design, documentation, finding bugs are ways everyone can help on top of coding features / bug fixes. We are excited to work with the power user community to build a set of tools for helping you get the most out of Windows. +This project welcomes contributions of all types. Besides coding features / bug fixes, other ways to assist include spec writing, design, documentation, and finding bugs. We are excited to work with the power user community to build a set of tools for helping you get the most out of Windows. -We ask that **before you start work on a feature that you would like to contribute**, please read our [Contributor's Guide](CONTRIBUTING.md). We will be happy to work with you to figure out the best approach, provide guidance and mentorship throughout feature development, and help avoid any wasted or duplicate effort. +We ask that **before you start work on a feature that you would like to contribute**, please read our [Contributor's Guide](CONTRIBUTING.md). We would be happy to work with you to figure out the best approach, provide guidance and mentorship throughout feature development, and help avoid any wasted or duplicate effort. -Most contributions require you to agree to a [Contributor License Agreement (CLA)][oss-CLA] declaring that you have the right to, and actually do, grant us the rights to use your contribution. +Most contributions require you to agree to a [Contributor License Agreement (CLA)][oss-CLA] declaring that you grant us the rights to use your contribution and that you have permission to do so. For guidance on developing for PowerToys, please read the [developer docs](/doc/devdocs) for a detailed breakdown. This includes how to setup your computer to compile. @@ -97,142 +97,158 @@ 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.73 - August 2023 Update +### 0.74 - September 2023 Update -In this release, we focused on releasing new features, stability and improvements. +In this release, we focused on stability and improvements. **Highlights** - - Keyboard manager now supports Numpad. Note, with previously bound hotkeys stored in settings.json would only react to non-Numpad keys now. If a user wishes to restore the previous behavior, it could be done by manually adding another binding for the Numpad variant. - - New utility: Crop And Lock allows you to crop a current application into a smaller window or just create a thumbnail. Focus the target window and press the shortcut to start cropping. - - FancyZones code improvements and refactor. - - Modernized ImageResizer UX. - - PowerRename advanced counter functionality. + - Upgraded to Windows App SDK 1.4.1, increasing stability of WinUI3 utilities. Thanks [@dongle-the-gadget](https://github.com/dongle-the-gadget) for starting the upgrade! + - Text Extractor was upgraded to its version 2.0, with a new overlay, table mode and more Quality of Life improvements. Thanks [@TheJoeFin](https://github.com/TheJoeFin)! + - Improved FancyZones stability, fixing some layout resets and improving handling of newly created windows on Windows 11. + - Fixed many silent crashes that were reported to Watson and the user's event viewer. ### General - - Added missing CoUninitialize call in elevation logic. Thanks [@sredna](https://github.com/sredna)! - - New utility: Crop And Lock. Thanks [@robmikh](https://github.com/robmikh)! and [@kevinguo305](https://github.com/kevinguo305)! - - Added new /helped fabric bot command to GitHub repo. Thanks [@Aaron-Junker](https://github.com/Aaron-Junker)! - - Fixed crashes caused by invalid settings. Thanks [@davidegiacometti](https://github.com/davidegiacometti)! + - Turning animations off in Windows Settings will now also turn them off in PowerToys. + - Upgraded the Windows App SDK dependency to 1.4.1. Thanks [@dongle-the-gadget](https://github.com/dongle-the-gadget) for the original 1.4.0 upgrade! + - Show in the thumbnail label and application titles when running as administrator. Thanks [@davidegiacometti](https://github.com/davidegiacometti)! + - Upgraded the Win UI Community Toolkit dependency to 8.0. Thanks [@niels9001](https://github.com/niels9001)! -### Always On Top +### Awake - - Added border transparency. + - Added down-sampled variants to the application's icon. Thanks [@morriscurtis](https://github.com/morriscurtis)! -### FancyZones +### Color Picker - - Fixed issue causing canvas zones being drawn only when dragging in zone area. - - Fixed user-defined default layout highlighting issue. - - Refactored and improved code quality. - - Fixed issue causing wrong layout to be applied when duplicating non-selected layout. + - After adding a new color in the editor, the history will scroll the new color into view. Thanks [@peerpalo](https://github.com/peerpalo)! -### File Locksmith +### Crop and Lock + - Fixed a Crop and Lock crash that would occur when trying to reparent a window crashes the target application. An error message is shown instead. - - Icon update. Thanks [@jmaraujouy](https://github.com/jmaraujouy)! - -### File Explorer add-ons +### FancyZones + + - Set the process and main thread priority to normal. + - Fixed handling newly created windows on Windows 11. + - Fixed scenarios where opening the FancyZones Editor would reset the layouts. - - Fixed issue causing thumbnail previewers to lock files. - - Open URIs from developer files in default browser. Thanks [@davidegiacometti](https://github.com/davidegiacometti)! +### File Explorer add-ons -### Installer + - Optimized CPU usage for generating SVG thumbnails. + - Improved handling of Gcode Thumbnails, including JPG and QOI formats. Thanks [@pedrolamas](https://github.com/pedrolamas)! + - Better handled errors when sending telemetry, which were causing reported crashes. + - Fixed some thumbnails not being shown centered like before the optimization. - - Fixed PowerToys autorun after installing as SYSTEM user. - - Removed CreateScheduledTask custom action to handle task creation only from runner code. +### File Locksmith -### Image Resizer + - Shows files opened by processes with PID greater than 65535. Thanks [@poke30744](https://github.com/poke30744)! + - Fixed a GDI object leak in the context menu which would crash Explorer. + +### Find My Mouse - - Moved from ModernWPF to WpfUI to refresh and modernize UI/UX. Thanks [@niels9001](https://github.com/niels9001)! + - Added new activation methods, including by hotkey. Thanks [@davidegiacometti](https://github.com/davidegiacometti)! -### Keyboard Manager +### Hosts File Editor - - Rephrased labels to enhance clarity. Thanks [@Jay-o-Way](https://github.com/Jay-o-Way)! - - Keyboard manager now supports Numpad. Note, with previously bound hotkeys stored in settings.json would only react to non-Numpad keys now. If a user wishes to restore the previous behavior, it could be done by manually adding another binding for the Numpad variant. + - Ignore the default ACME sample entries in the hosts file. Thanks [@davidegiacometti](https://github.com/davidegiacometti)! + - Improved save error handling and added better error messages. Thanks [@davidegiacometti](https://github.com/davidegiacometti)! + - Corrected a check for an error when signaling the application to start as administrator. + - Refactored the context menu. Thanks [@davidegiacometti](https://github.com/davidegiacometti)! + - Fixed dialogs overlapping the title bar after the upgrade to Windows App SDK 1.4. Thanks [@davidegiacometti](https://github.com/davidegiacometti)! -### Mouse Highlighter +### Keyboard Manager - - Fixed highlighter being invisible issue for Always on Top windows. - - Added settings for automatic activation on startup. Thanks [@davidegiacometti](https://github.com/davidegiacometti)! + - Distinguish between the regular minus key and the numpad minus key. -### Mouse Pointer Crosshairs +### Mouse Without Borders - - Added settings for automatic activation on startup. Thanks [@davidegiacometti](https://github.com/davidegiacometti)! + - Fixed a crash when trying to restart the application. ### Peek - - Show correct file type for shortcuts. Thanks [@davidegiacometti](https://github.com/davidegiacometti)! - - Fixed issue causing wrong file size to be displayed. Thanks [@davidegiacometti](https://github.com/davidegiacometti)! - - Show 1 byte instead of 1 bytes file size. Thanks [@Deepak-Sangle](https://github.com/Deepak-Sangle)! - - Open URIs from developer files in default browser. Thanks [@davidegiacometti](https://github.com/davidegiacometti)! - - Show thumbnail and fallback to icon for unsupported files. Thanks [@pedrolamas](https://github.com/pedrolamas)! + - Using Peek on HTML files will show a white background by default, similar to a browser's default behavior. + - Fix a white flash on Dark theme when switching file and improved the development file preview detection and adjustments. ### PowerRename - - Updated OOBE gif. Thanks [@ChaseKnowlden](https://github.com/ChaseKnowlden)! - - Localized renamed parts combo box. - - Introduced advanced counter functionality. - - Added remember last window size logic and optimized items sorting. - - Enable "Enumerate items" option by default. + - Fixed a crash caused by big counter values on the new enumeration method. ### PowerToys Run - - Fixed issue causing original search to be abandoned when cycling through results. - - Updated device and bluetooth results for Settings plugin. Thanks [@htcfreek](https://github.com/htcfreek)! - - Fixed InvalidOperationException exception thrown. Thanks [@davidegiacometti](https://github.com/davidegiacometti)! - - Add Base64 Decoding function to the Value Generator plugin. Thanks [@LeagueOfPoro](https://github.com/LeagueOfPoro)! - - Added Keep shell open option for Shell plugin. - - Added Crop And Lock to PowerToys plugin. Thanks [@davidegiacometti](https://github.com/davidegiacometti)! + - It's now possible to select which shell is used by the Shell plugin. + - A combobox option type was added to the plugin options. + - Fixed a bug in the Calculator plugin that was causing decimal numbers to be misinterpreted on locales where the dot (`.`) character isn't used as a decimal or digit separator. + - Improved the Program plugin stability when it fails to load a program's thumbnail at startup. + - The use of Pinyin for querying some plugins can now be turned on in Settings. Thanks [@ChaseKnowlden](https://github.com/ChaseKnowlden)! + - Refactored option types for plugin and added number, string and composite types to be used in the future. Thanks [@htcfreek](https://github.com/htcfreek)! + - Fixed the entry for searching for Windows updates in the Settings plugin. Thanks [@htcfreek](https://github.com/htcfreek)! + +### Quick Accent + + - The "All languages" character set is now calculated by programmatically querying the characters for every available language. Thanks [@dannysummerlin](https://github.com/dannysummerlin)! + - Added é to the Norwegian and Swedish languages. Thanks [@Aaron-Junker](https://github.com/Aaron-Junker)! + - Added a runtime cache to the "All languages" character set, to only calculate accents once per key. ### Registry Preview - - Updated AppBarButtons to use an explicit AppBarButton.Icon. Thanks [@randyrants](https://github.com/randyrants)! - - Fixed crash on clicking Save As button. + - Fixed focusing issues at startup. + - Improved the data visualization to show data in a similar way to the Windows Registry Editor. Thanks [@dillydylann](https://github.com/dillydylann)! ### Runner - - Removed unneeded RegisterWindowMessage from tray icon logic. Thanks [@sredna](https://github.com/sredna)! - - Fixed startup looping issue. - - Improved old logs and installers cleanup logic. Thanks [@davidegiacometti](https://github.com/davidegiacometti)! + - Fixed hanging when a bug report was generated from the flyout. Thanks [@davidegiacometti](https://github.com/davidegiacometti)! -### Screen Ruler +### Settings - - Use proper resources file. + - Improved the way the OOBE window reacts to Windows theme change. + - Fixed an issue that made it impossible to change the "Switch between windows in the current zone" "Next window" shortcut for FancyZones. + - Fixed a crash when entering a duplicate name for a color in the Color Picker page and improved clean up when cancelling a color edit. Thanks [@davidegiacometti](https://github.com/davidegiacometti)! -### Settings +### Text Extractor - - Fixed issue causing problems with modifier keys and ShortcutControl. Thanks [@sh0ckj0ckey](https://github.com/sh0ckj0ckey)! - - Fixed crash when clicking "Windows color settings" link. - - Added support for launching Settings app directly. - - Fixed issue causing DisplayDescription not showing for PowerToys Run PluginAdditionalOption. - - Fixed issue causing FileLocksmith 'Show File Locksmith in' setting not showing correct value. - - Fixed issue causing Awake on/off toggle in Settings flyout not to work when Settings Awake page is opened. + - Text Extractor 2.0, with a new overlay, table mode and more Quality of Life improvements. Thanks [@TheJoeFin](https://github.com/TheJoeFin)! ### Documentation - - Added documentation for PowerToys Run third-party plugins. Thanks [@davidegiacometti](https://github.com/davidegiacometti)! - - Fixed broken links in keyboardmanagerui.md. Thanks [@shubhsardana29](https://github.com/shubhsardana29)! - - Updated core team in COMMUNITY.md. - - Fixed broken links in ui-architecture.md. Thanks [@SamB](https://github.com/SamB)! - - Updated community.valuegenerator.md with Base64DecodeRequest description. + - SECURITY.md was updated from 0.0.2 to 0.0.9. Thanks [@Aaron-Junker](https://github.com/Aaron-Junker)! + - Improved the README and main development document for clarity and completeness. Thanks [@codeofdusk](https://github.com/codeofdusk) and [@aprilbbrockhoeft](https://github.com/aprilbbrockhoeft)! ### Development - - Updated test packages and StyleCop. Thanks [@davidegiacometti](https://github.com/davidegiacometti)! - - Condense NuGet Restore into MSBuild Tasks. Thanks [@snickler](https://github.com/snickler)! + - Fixed PowerToys Run DateTime plugin tests that were failing depending on locale, so that they can be run correctly on all dev machines. + - Fixed PowerToys Run System plugin tests that were failing for certain network interfaces, so that they can be run correctly on all dev machines. Thanks [@snickler](https://github.com/snickler)! + - Fixed a markdown bug on the GitHub /helped command. + - Switched build pipelines to a new agent pool. Thanks [@DHowett](https://github.com/DHowett)! + - New .cs files created in Visual Studio get the header added automatically. Thanks [@davidegiacometti](https://github.com/davidegiacometti)! -#### What is being planned for version 0.74 +#### What is being planned for version 0.75 -For [v0.74][github-next-release-work], we'll work on below: +For [v0.75][github-next-release-work], we'll work on the items below: - Language selection - - Modernize and refresh UX of PowerToys based on WPF + - .NET 8 upgrade + - Policy support for managing PowerToys Run plugins. +*Attention*: A breaking change is planned (for 0.75), in which each plugin has to declare its identifier programmatically so that it can be controlled through GPO. For third-party plugin developers, please check https://github.com/microsoft/PowerToys/pull/27468 for more details. + + - New utility: Environment Variables Editor. Here's a Work in Progress preview: + +![Environment Variables Editor WIP](https://github.com/microsoft/PowerToys/assets/26118718/f99532a8-5aae-481b-a662-19a95f4aa03d) + + - New Settings homepage. Here's a Work in Progress preview: + +![PowerToys Settings Dashboard WIP](https://github.com/microsoft/PowerToys/assets/26118718/938a5715-0a9b-4fe9-9e15-adfec92da694) + + - Modernize and refresh the UX of PowerToys based on WPF. Here's Work in Progress previews for the modules "PowerToys Run" and "Color Picker": + +![PowerToys Run UI refresh WIP](https://github.com/microsoft/PowerToys/assets/9866362/16903bcb-c18e-49fb-93ca-738b81957055) + +![ColorPicker UI refresh WIP](https://github.com/microsoft/PowerToys/assets/9866362/ceebe54b-de63-4ce7-afcb-2cd4280bf4d1) + - Stability / bug fixes - - Peek: UI improvements ## PowerToys Community -The PowerToys team is extremely grateful to have the [support of an amazing active community][community-link]. The work you do is incredibly important. PowerToys wouldn’t be nearly what it is today without your help filing bugs, updating documentation, guiding the design, or writing features. We want to say thank you and take time to recognize your work. Month over month, you directly help make PowerToys a better piece of software. +The PowerToys team is extremely grateful to have the [support of an amazing active community][community-link]. The work you do is incredibly important. PowerToys wouldn’t be nearly what it is today without your help filing bugs, updating documentation, guiding the design, or writing features. We want to say thank you and take time to recognize your work. Month by month, you directly help make PowerToys a better piece of software. ## Code of Conduct diff --git a/doc/devdocs/modules/launcher/plugins/system.md b/doc/devdocs/modules/launcher/plugins/system.md index dcf6c3d9f8d..72f2cf38fc3 100644 --- a/doc/devdocs/modules/launcher/plugins/system.md +++ b/doc/devdocs/modules/launcher/plugins/system.md @@ -15,7 +15,7 @@ Available commands: * Hibernate * Open / Empty Recycle Bin * UEFI Firmware Settings (Only available on systems, that boot in UEFI mode.) -* IP / MAC / Address => Show informations about network connections. +* IP / MAC / Address => Show information about network connections. ## Optional plugin settings @@ -46,7 +46,7 @@ Available commands: ### [`NetworkConnectionProperties.cs`](/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.System/Components/NetworkConnectionProperties.cs) - The [`NetworkConnectionProperties`](/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.System/Components/NetworkConnectionProperties.cs) class contains methods to get the properties of a network interface/connection. -- An instance of this class collects/provides all required informations about one connection/adapter. +- An instance of this class collects/provides all required information about one connection/adapter. ### [`SystemPluginContext.cs`](/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.System/Components/SystemPluginContext.cs) - An instance of the class [`SystemPluginContext`](/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.System/Components/SystemPluginContext.cs) contains/defines the context data of a system plugin result. We select the context menu based on the defined properties. diff --git a/doc/devdocs/readme.md b/doc/devdocs/readme.md index 7c2a12061a4..69993d4596d 100644 --- a/doc/devdocs/readme.md +++ b/doc/devdocs/readme.md @@ -2,35 +2,36 @@ ## Fork, Clone, Branch and Create your PR -Once you've discussed your proposed feature/fix/etc. with a team member, and you've agreed an approach or a spec has been written and approved, it's time to start development: +Once you've discussed your proposed feature/fix/etc. with a team member, and an approach or a spec has been written and approved, it's time to start development: -1. Fork the repo if you haven't already +1. Fork the repo on GitHub if you haven't already 1. Clone your fork locally -1. Create & push a feature branch -1. Create a [Draft Pull Request (PR)](https://github.blog/2019-02-14-introducing-draft-pull-requests/) +1. Create a feature branch 1. Work on your changes +1. Create a [Draft Pull Request (PR)](https://github.blog/2019-02-14-introducing-draft-pull-requests/) +1. When ready, mark your PR as "ready for review". ## Rules - **Follow the pattern of what you already see in the code.** - [Coding style](style.md). -- Try to package new ideas/components into libraries that have nicely defined interfaces. -- Package new ideas into classes or refactor existing ideas into a class as you extend. -- When adding new classes/methods/changing existing code: add new unit tests or update the existing tests. +- 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. ## GitHub Workflow - Before starting to work on a fix/feature, make sure there is an open issue to track the work. -- Add the `In progress` label to the issue, if not already present also add a `Cost-Small/Medium/Large` estimate and make sure all appropriate labels are set. -- If you are a community contributor, you will not be able to add labels to the issue, in that case just add a comment saying that you started to work on the issue and try to give an estimate for the delivery date. +- Add the `In progress` label to the issue, if not already present. Also add a `Cost-Small/Medium/Large` estimate and make sure all appropriate labels are set. +- If you are a community contributor, you will not be able to add labels to the issue; in that case just add a comment saying that you have started work on the issue and try to give an estimate for the delivery date. - If the work item has a medium/large cost, using the markdown task list, list each sub item and update the list with a check mark after completing each sub item. - When opening a PR, follow the PR template. -- When you'd like the team to take a look, (even if the work is not yet fully-complete), mark the PR as 'Ready For Review' so that the team can review your work and provide comments, suggestions, and request changes. It may take several cycles, but the end result will be solid, testable, conformant code that is safe for us to merge. -- When the PR is approved, let the owner of the PR merge it. For community contributions the reviewer that approved the PR can also merge it. -- Use the `Squash and merge` option to merge a PR, if you don't want to squash it because there are logically different commits, use `Rebase and merge`. +- When you'd like the team to take a look (even if the work is not yet fully complete) mark the PR as 'Ready For Review' so that the team can review your work and provide comments, suggestions, and request changes. It may take several cycles, but the end result will be solid, testable, conformant code that is safe for us to merge. +- When the PR is approved, let the owner of the PR merge it. For community contributions, the reviewer who approved the PR can also merge it. +- Use the `Squash and merge` option to merge a PR. If you don't want to squash it because there are logically different commits, use `Rebase and merge`. - We don't close issues automatically when referenced in a PR, so after the PR is merged: - - mark the issue(s), that the PR solved, with the `Resolution-Fix-Committed` label, remove the `In progress` label and if the issue is assigned to a project, move the item to the `Done` status. - - don't close the issue if it's a bug in the current released version since users tend to not search for closed issues, we will close the resolved issues when a new version is released. + - mark the issue(s) that the PR solved with the `Resolution-Fix-Committed` label, remove the `In progress` label and if the issue is assigned to a project, move the item to the `Done` status. + - don't close the issue if it's a bug in the current released version; since users tend to not search for closed issues, we will close the resolved issues when a new version is released. - if it's not a code fix that effects the end user, the issue can be closed (for example a fix in the build or a code refactoring and so on). ## Compiling PowerToys @@ -39,12 +40,16 @@ Once you've discussed your proposed feature/fix/etc. with a team member, and you 1. Windows 10 April 2018 Update (version 1803) or newer 1. Visual Studio Community/Professional/Enterprise 2022 17.4 or newer -1. Git clone PowerToys repository +1. A local clone of the PowerToys repository + +### Install Visual Studio dependencies + 1. Open the `PowerToys.sln` file. 1. If you see a dialog that says `install extra components` in the solution explorer pane, click `install` ### Get Submodules to compile -We have submodules that need to be initialized before you can compile most parts of PowerToys. This should be a one time step. + +We have submodules that need to be initialized before you can compile most parts of PowerToys. This should be a one-time step. 1. Open a terminal 1. Navigate to the folder you cloned PowerToys to. @@ -52,9 +57,11 @@ We have submodules that need to be initialized before you can compile most parts ### Compiling Source Code -- Open `PowerToys.sln` in Visual Studio, in the `Solutions Configuration` drop-down menu select `Release` or `Debug`, from the `Build` menu choose `Build Solution`. -- The PowerToys binaries will be in your repo under `x64\Release\`. -- You can run `x64\Release\PowerToys.exe` directly without installing PowerToys, but some modules (i.e. PowerRename, ImageResizer, File Explorer extension etc.) will not be available unless you also build the installer and install PowerToys. +- Open `PowerToys.sln` in Visual Studio. +- In the `Solutions Configuration` drop-down menu select `Release` or `Debug`. +- From the `Build` menu choose `Build Solution`, or press Control+Shift+b on your keyboard. +- The build process may take several minutes depending on your computer's performance. Once it completes, the PowerToys binaries will be in your repo under `x64\Release\`. + - You can run `x64\Release\PowerToys.exe` directly without installing PowerToys, but some modules (i.e. PowerRename, ImageResizer, File Explorer extension etc.) will not be available unless you also build the installer and install PowerToys. ## Compile the installer @@ -62,7 +69,7 @@ Our installer is two parts, an EXE and an MSI. The EXE (Bootstrapper) contains - The EXE installs all prerequisites and installs PowerToys via the MSI. It has additional features such as the installation flags (see below). - The MSI installs the PowerToys binaries. -The installer can only be compiled in `Release` mode, step 1 and 2 must be done before the MSI will be able to be compiled. +The installer can only be compiled in `Release` mode; steps 1 and 2 must be performed before the MSI can be compiled. 1. Compile `PowerToys.sln`. Instructions are listed above. 1. Compile `BugReportTool.sln` tool. Path from root: `tools\BugReportTool\BugReportTool.sln` (details listed below) @@ -76,9 +83,11 @@ The installer can only be compiled in `Release` mode, step 1 and 2 must be done 1. Install the [WiX Toolset build tools](https://wixtoolset.org/docs/v3/releases/v3-14-0-6526/). (installer [direct link](https://wixtoolset.org/downloads/v3.14.0.6526/wix314.exe)) 1. Download [WiX binaries](https://wixtoolset.org/downloads/v3.14.0.6526/wix314-binaries.zip) and extract `wix.targets` to `C:\Program Files (x86)\WiX Toolset v3.14`. -### Locally building the installer prerequisite projects all at once from the command-line +### Building prerequisite projects + +#### From the command line -1. Open a `Developer Command Prompt for VS 2022` +1. From the start menu, open a `Developer Command Prompt for VS 2022` 1. Ensure `nuget.exe` is in your `%path%` 1. In the repo root, run these commands: @@ -93,20 +102,16 @@ nuget restore .\tools\StylesReportTool\StylesReportTool.sln msbuild -p:Platform=x64 -p:Configuration=Release .\tools\StylesReportTool\StylesReportTool.sln ``` -### Locally compiling the Bug reporting tool +#### From Visual Studio + +If you prefer, you can alternatively build prerequisite projects for the installer using the Visual Studio UI. 1. Open `tools\BugReportTool\BugReportTool.sln` 1. In Visual Studio, in the `Solutions Configuration` drop-down menu select `Release` 1. From the `Build` menu, choose `Build Solution`. - -### Locally compiling the Webcam reporting tool - 1. Open `tools\WebcamReportTool\WebcamReportTool.sln` 1. In Visual Studio, in the `Solutions Configuration` drop-down menu select `Release` 1. From the `Build` menu, choose `Build Solution`. - -### Locally compiling the Window styles reporting tool - 1. Open `tools\StylesReportTool\StylesReportTool.sln` 1. In Visual Studio, in the `Solutions Configuration` drop-down menu select `Release` 1. From the `Build` menu, choose `Build Solution`. @@ -125,14 +130,14 @@ Head over to the wiki to see the [full list of supported installer arguments][in ## Debugging -The following configuration issue only applies if the user is a member of the Administrators group. +To debug the PowerToys application in Visual Studio, set the `runner` project as your start-up project, then start the debugger. -Some PowerToys modules require being run with the highest permission level if the current user is a member of the Administrators group. The highest permission level is required to be able to perform some actions when an elevated application (e.g. Task Manager) is in the foreground or is the target of an action. Without elevated privileges some PowerToys modules will still work but with some limitations: +Some PowerToys modules must be run with the highest permission level if the current user is a member of the Administrators group. The highest permission level is required to be able to perform some actions when an elevated application (e.g. Task Manager) is in the foreground or is the target of an action. Without elevated privileges some PowerToys modules will still work but with some limitations: -- The `FancyZones` module will be not be able to move an elevated window to a zone. +- The `FancyZones` module will not be able to move an elevated window to a zone. - The `Shortcut Guide` module will not appear if the foreground window belongs to an elevated application. -To run and debug PowerToys from Visual Studio when the user is a member of the Administrators group, Visual Studio has to be started with elevated privileges. If you want to avoid running Visual Studio with elevated privileges and don't mind the limitations described above, you can do the following: open the `runner` project properties and navigate to the `Linker -> Manifest File` settings, edit the `UAC Execution Level` property and change it from `highestAvailable (level='highestAvailable')` to `asInvoker (/level='asInvoker')`, save the changes. +Therefore, it is recommended to run Visual Studio with elevated privileges when debugging these scenarios. If you want to avoid running Visual Studio with elevated privileges and don't mind the limitations described above, you can do the following: open the `runner` project properties and navigate to the `Linker -> Manifest File` settings, edit the `UAC Execution Level` property and change it from `highestAvailable (level='highestAvailable')` to `asInvoker (/level='asInvoker'). ## How to create new PowerToys @@ -155,7 +160,7 @@ It's responsible for: ### [`Interface`](modules/interface.md) -Definition of the interface used by the [`runner`](/src/runner) to manage the PowerToys. All PowerToys must implement this interface. +The definition of the interface used by the [`runner`](/src/runner) to manage the PowerToys. All PowerToys must implement this interface. ### [`Common`](common.md) @@ -163,4 +168,4 @@ The common lib, as the name suggests, contains code shared by multiple PowerToys ### [`Settings`](settingsv2/) -Settings v2 is our current settings implementation. Please head over to the dev docs that goes into the current settings system. +Settings v2 is our current settings implementation. Please head over to the dev docs that describe the current settings system. diff --git a/src/common/GPOWrapper/GPOWrapper.vcxproj b/src/common/GPOWrapper/GPOWrapper.vcxproj index 86c34f7ec56..7cb115d6aae 100644 --- a/src/common/GPOWrapper/GPOWrapper.vcxproj +++ b/src/common/GPOWrapper/GPOWrapper.vcxproj @@ -111,6 +111,11 @@ + + + {cc6e41ac-8174-4e8a-8d22-85dd7f4851df} + + diff --git a/src/common/SettingsAPI/SettingsAPI.vcxproj b/src/common/SettingsAPI/SettingsAPI.vcxproj index 834de6db209..306b4a8c3be 100644 --- a/src/common/SettingsAPI/SettingsAPI.vcxproj +++ b/src/common/SettingsAPI/SettingsAPI.vcxproj @@ -43,6 +43,11 @@ + + + {cc6e41ac-8174-4e8a-8d22-85dd7f4851df} + + diff --git a/src/common/UnitTests-CommonLib/UnitTests-CommonLib.vcxproj b/src/common/UnitTests-CommonLib/UnitTests-CommonLib.vcxproj index a2e84a9cb03..f3dd518759b 100644 --- a/src/common/UnitTests-CommonLib/UnitTests-CommonLib.vcxproj +++ b/src/common/UnitTests-CommonLib/UnitTests-CommonLib.vcxproj @@ -48,9 +48,6 @@ {6955446d-23f7-4023-9bb3-8657f904af99} - - {cc6e41ac-8174-4e8a-8d22-85dd7f4851df} - diff --git a/src/common/interop/keyboard_layout.cpp b/src/common/interop/keyboard_layout.cpp index afac8fe1410..7e0a13e848e 100644 --- a/src/common/interop/keyboard_layout.cpp +++ b/src/common/interop/keyboard_layout.cpp @@ -238,6 +238,7 @@ void LayoutMap::LayoutMapImpl::UpdateLayout() keyboardLayoutMap[VK_NONCONVERT] = L"IME Non-Convert"; keyboardLayoutMap[VK_ACCEPT] = L"IME Kana"; keyboardLayoutMap[VK_MODECHANGE] = L"IME Mode Change"; + keyboardLayoutMap[VK_DECIMAL] = L". (Numpad)"; keyboardLayoutMap[CommonSharedConstants::VK_DISABLED] = L"Disable"; } diff --git a/src/common/logger/logger.vcxproj b/src/common/logger/logger.vcxproj index a371b25109e..f4915e78e35 100644 --- a/src/common/logger/logger.vcxproj +++ b/src/common/logger/logger.vcxproj @@ -74,6 +74,9 @@ {7e1e3f13-2bd6-3f75-a6a7-873a2b55c60f} + + {cc6e41ac-8174-4e8a-8d22-85dd7f4851df} + diff --git a/src/common/notifications/BackgroundActivatorDLL/BackgroundActivatorDLL.vcxproj b/src/common/notifications/BackgroundActivatorDLL/BackgroundActivatorDLL.vcxproj index 68fca43ea57..394fd21d9b5 100644 --- a/src/common/notifications/BackgroundActivatorDLL/BackgroundActivatorDLL.vcxproj +++ b/src/common/notifications/BackgroundActivatorDLL/BackgroundActivatorDLL.vcxproj @@ -67,6 +67,9 @@ + + {cc6e41ac-8174-4e8a-8d22-85dd7f4851df} + {031AC72E-FA28-4AB7-B690-6F7B9C28AA73} diff --git a/src/common/updating/updating.vcxproj b/src/common/updating/updating.vcxproj index 5dec7c902f2..97089b03a1f 100644 --- a/src/common/updating/updating.vcxproj +++ b/src/common/updating/updating.vcxproj @@ -51,9 +51,6 @@ {6955446d-23f7-4023-9bb3-8657f904af99} - - {cc6e41ac-8174-4e8a-8d22-85dd7f4851df} - diff --git a/src/common/version/version.vcxproj b/src/common/version/version.vcxproj index f91d2e4b1e7..7aaa57c5bfa 100644 --- a/src/common/version/version.vcxproj +++ b/src/common/version/version.vcxproj @@ -10,6 +10,25 @@ + + + + Debug + Win32 + + + Debug + x64 + + + Release + Win32 + + + Release + x64 + + 16.0 {CC6E41AC-8174-4E8A-8D22-85DD7F4851DF} diff --git a/src/modules/CropAndLock/CropAndLock/ReparentCropAndLockWindow.cpp b/src/modules/CropAndLock/CropAndLock/ReparentCropAndLockWindow.cpp index cdf9fb3eb06..83087502c88 100644 --- a/src/modules/CropAndLock/CropAndLock/ReparentCropAndLockWindow.cpp +++ b/src/modules/CropAndLock/CropAndLock/ReparentCropAndLockWindow.cpp @@ -131,7 +131,10 @@ void ReparentCropAndLockWindow::CropAndLock(HWND windowToCrop, RECT cropRect) SetWindowLongPtrW(m_currentTarget, GWL_STYLE, targetStyle); auto x = -cropRect.left; auto y = -cropRect.top; - winrt::check_bool(SetWindowPos(m_currentTarget, nullptr, x, y, 0, 0, SWP_NOSIZE | SWP_FRAMECHANGED | SWP_NOZORDER)); + if (0 == SetWindowPos(m_currentTarget, nullptr, x, y, 0, 0, SWP_NOSIZE | SWP_FRAMECHANGED | SWP_NOZORDER)) + { + MessageBoxW(nullptr, L"CropAndLock couldn't properly reparent the target window. It might not handle reparenting well.", L"CropAndLock", MB_ICONERROR); + } } void ReparentCropAndLockWindow::Hide() diff --git a/src/modules/FileLocksmith/FileLocksmithExt/FileLocksmithExt.vcxproj b/src/modules/FileLocksmith/FileLocksmithExt/FileLocksmithExt.vcxproj index cf3a4723194..e9186c86a53 100644 --- a/src/modules/FileLocksmith/FileLocksmithExt/FileLocksmithExt.vcxproj +++ b/src/modules/FileLocksmith/FileLocksmithExt/FileLocksmithExt.vcxproj @@ -260,9 +260,6 @@ {98537082-0fdb-40de-abd8-0dc5a4269bab} - - {cc6e41ac-8174-4e8a-8d22-85dd7f4851df} - diff --git a/src/modules/Hosts/Hosts/HostsXAML/Views/MainPage.xaml b/src/modules/Hosts/Hosts/HostsXAML/Views/MainPage.xaml index e889c0381ee..f62eef0a98a 100644 --- a/src/modules/Hosts/Hosts/HostsXAML/Views/MainPage.xaml +++ b/src/modules/Hosts/Hosts/HostsXAML/Views/MainPage.xaml @@ -199,7 +199,7 @@ + IsEnabled="{x:Bind ViewModel.Filtered, Mode=OneWay, Converter={StaticResource BoolNegationConverter}}"> @@ -207,7 +207,7 @@ + IsEnabled="{x:Bind ViewModel.Filtered, Mode=OneWay, Converter={StaticResource BoolNegationConverter}}"> diff --git a/src/modules/Hosts/Hosts/Strings/en-us/Resources.resw b/src/modules/Hosts/Hosts/Strings/en-us/Resources.resw index c40e36aaafa..75303506e68 100644 --- a/src/modules/Hosts/Hosts/Strings/en-us/Resources.resw +++ b/src/modules/Hosts/Hosts/Strings/en-us/Resources.resw @@ -239,7 +239,7 @@ "Hosts" refers to the system hosts file, do not loc - Seperate multiple hosts by space (e.g. server server.local). Maximum 9 hosts per entry. + Separate multiple hosts by space (e.g. server server.local). Maximum 9 hosts per entry. Do not localize "server" and "server.local" diff --git a/src/modules/MeasureTool/MeasureToolCore/PowerToys.MeasureToolCore.vcxproj b/src/modules/MeasureTool/MeasureToolCore/PowerToys.MeasureToolCore.vcxproj index 4271e03a55e..ad51f8998b6 100644 --- a/src/modules/MeasureTool/MeasureToolCore/PowerToys.MeasureToolCore.vcxproj +++ b/src/modules/MeasureTool/MeasureToolCore/PowerToys.MeasureToolCore.vcxproj @@ -1,6 +1,6 @@  - + @@ -147,7 +147,7 @@ - + @@ -158,7 +158,7 @@ - - + + \ No newline at end of file diff --git a/src/modules/MeasureTool/MeasureToolCore/packages.config b/src/modules/MeasureTool/MeasureToolCore/packages.config index 817fac08122..ca3592e5b04 100644 --- a/src/modules/MeasureTool/MeasureToolCore/packages.config +++ b/src/modules/MeasureTool/MeasureToolCore/packages.config @@ -3,5 +3,5 @@ - + \ No newline at end of file diff --git a/src/modules/ShortcutGuide/ShortcutGuide/shortcut_guide.cpp b/src/modules/ShortcutGuide/ShortcutGuide/shortcut_guide.cpp index f1c843d6155..719f713e793 100644 --- a/src/modules/ShortcutGuide/ShortcutGuide/shortcut_guide.cpp +++ b/src/modules/ShortcutGuide/ShortcutGuide/shortcut_guide.cpp @@ -73,7 +73,7 @@ namespace result.hwnd = active_window; // In reality, Windows Snap works if even one of those styles is set // for a window, it is just limited. If there is no WS_MAXIMIZEBOX using - // WinKey + Up just won't maximize the window. Similary, without + // WinKey + Up just won't maximize the window. Similarly, without // WS_MINIMIZEBOX the window will not get minimized. A "Save As..." dialog // is a example of such window - it can be snapped to both sides and to // all screen corners, but will not get maximized nor minimized. diff --git a/src/modules/fancyzones/FancyZonesLib/EditorParameters.cpp b/src/modules/fancyzones/FancyZonesLib/EditorParameters.cpp index 26397eb1ba0..b7a7ea4523c 100644 --- a/src/modules/fancyzones/FancyZonesLib/EditorParameters.cpp +++ b/src/modules/fancyzones/FancyZonesLib/EditorParameters.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -36,6 +37,31 @@ namespace JsonUtils int monitorHeight{}; bool isSelected = false; + bool FillFromWorkArea(const WorkArea* const workArea) + { + const auto virtualDesktopIdStr = FancyZonesUtils::GuidToString(workArea->UniqueId().virtualDesktopId); + if (!virtualDesktopIdStr) + { + return false; + } + + const auto& monitorId = workArea->UniqueId().monitorId; + monitorName = monitorId.deviceId.id; + monitorInstanceId = monitorId.deviceId.instanceId; + monitorNumber = monitorId.deviceId.number; + monitorSerialNumber = monitorId.serialNumber; + + const auto& rect = workArea->GetWorkAreaRect(); + top = rect.top(); + left = rect.left(); + workAreaWidth = rect.width(); + workAreaHeight = rect.height(); + + virtualDesktop = virtualDesktopIdStr.value(); + + return true; + } + static json::JsonObject ToJson(const MonitorInfo& monitor) { json::JsonObject result{}; @@ -84,21 +110,8 @@ namespace JsonUtils }; } -bool EditorParameters::Save() noexcept +bool EditorParameters::Save(const WorkAreaConfiguration& configuration, OnThreadExecutor& dpiUnawareThread) noexcept { - const auto virtualDesktopIdStr = FancyZonesUtils::GuidToString(VirtualDesktop::instance().GetCurrentVirtualDesktopId()); - if (!virtualDesktopIdStr) - { - Logger::error(L"Save editor params: invalid virtual desktop id"); - return false; - } - - OnThreadExecutor dpiUnawareThread; - dpiUnawareThread.submit(OnThreadExecutor::task_t{ [] { - SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_UNAWARE); - SetThreadDpiHostingBehavior(DPI_HOSTING_BEHAVIOR_MIXED); - } }).wait(); - const bool spanZonesAcrossMonitors = FancyZonesSettings::settings().spanZonesAcrossMonitors; JsonUtils::EditorArgs argsJson; @@ -107,23 +120,31 @@ bool EditorParameters::Save() noexcept if (spanZonesAcrossMonitors) { + const auto& workArea = configuration.GetWorkArea(nullptr); + if (!workArea) + { + return false; + } + + JsonUtils::MonitorInfo monitorJson; + if (!monitorJson.FillFromWorkArea(workArea)) + { + return false; + } + RECT combinedWorkArea; - dpiUnawareThread.submit(OnThreadExecutor::task_t{ [&]() { - combinedWorkArea = FancyZonesUtils::GetAllMonitorsCombinedRect<&MONITORINFOEX::rcWork>(); - + dpiUnawareThread.submit(OnThreadExecutor::task_t{ + [&]() { + combinedWorkArea = FancyZonesUtils::GetAllMonitorsCombinedRect<&MONITORINFOEX::rcWork>(); } }).wait(); - RECT combinedMonitorArea = FancyZonesUtils::GetAllMonitorsCombinedRect<&MONITORINFOEX::rcMonitor>(); - JsonUtils::MonitorInfo monitorJson; - monitorJson.monitorName = ZonedWindowProperties::MultiMonitorName; - monitorJson.monitorInstanceId = ZonedWindowProperties::MultiMonitorInstance; - monitorJson.monitorNumber = 0; - monitorJson.virtualDesktop = virtualDesktopIdStr.value(); + // use dpi-unaware values monitorJson.top = combinedWorkArea.top; monitorJson.left = combinedWorkArea.left; monitorJson.workAreaWidth = combinedWorkArea.right - combinedWorkArea.left; monitorJson.workAreaHeight = combinedWorkArea.bottom - combinedWorkArea.top; + monitorJson.monitorWidth = combinedMonitorArea.right - combinedMonitorArea.left; monitorJson.monitorHeight = combinedMonitorArea.bottom - combinedMonitorArea.top; monitorJson.isSelected = true; @@ -133,8 +154,6 @@ bool EditorParameters::Save() noexcept } else { - auto monitors = MonitorUtils::IdentifyMonitors(); - HMONITOR targetMonitor{}; if (FancyZonesSettings::settings().use_cursorpos_editor_startupscreen) { @@ -153,31 +172,16 @@ bool EditorParameters::Save() noexcept return false; } - for (auto& monitorData : monitors) + const auto& config = configuration.GetAllWorkAreas(); + for (auto& [monitor, workArea] : config) { - HMONITOR monitor = monitorData.monitor; - - MONITORINFOEX monitorInfo{}; - dpiUnawareThread.submit(OnThreadExecutor::task_t{ [&] { - monitorInfo.cbSize = sizeof(monitorInfo); - if (!GetMonitorInfo(monitor, &monitorInfo)) - { - return; - } - } }).wait(); - JsonUtils::MonitorInfo monitorJson; - - if (monitor == targetMonitor) + if (!monitorJson.FillFromWorkArea(workArea.get())) { - monitorJson.isSelected = true; /* Is monitor selected for the main editor window opening */ + continue; } - monitorJson.monitorName = monitorData.deviceId.id; - monitorJson.monitorInstanceId = monitorData.deviceId.instanceId; - monitorJson.monitorNumber = monitorData.deviceId.number; - monitorJson.monitorSerialNumber = monitorData.serialNumber; - monitorJson.virtualDesktop = virtualDesktopIdStr.value(); + monitorJson.isSelected = monitor == targetMonitor; /* Is monitor selected for the main editor window opening */ UINT dpi = 0; if (DPIAware::GetScreenDPIForMonitor(monitor, dpi) != S_OK) @@ -186,10 +190,15 @@ bool EditorParameters::Save() noexcept } monitorJson.dpi = dpi; - monitorJson.top = monitorInfo.rcWork.top; - monitorJson.left = monitorInfo.rcWork.left; - monitorJson.workAreaWidth = monitorInfo.rcWork.right - monitorInfo.rcWork.left; - monitorJson.workAreaHeight = monitorInfo.rcWork.bottom - monitorInfo.rcWork.top; + + MONITORINFOEX monitorInfo{}; + dpiUnawareThread.submit(OnThreadExecutor::task_t{ [&] { + monitorInfo.cbSize = sizeof(monitorInfo); + if (!GetMonitorInfo(monitor, &monitorInfo)) + { + return; + } + } }).wait(); float width = static_cast(monitorInfo.rcMonitor.right - monitorInfo.rcMonitor.left); float height = static_cast(monitorInfo.rcMonitor.bottom - monitorInfo.rcMonitor.top); @@ -198,6 +207,12 @@ bool EditorParameters::Save() noexcept monitorJson.monitorWidth = static_cast(std::roundf(width)); monitorJson.monitorHeight = static_cast(std::roundf(height)); + // use dpi-unaware values + monitorJson.top = monitorInfo.rcWork.top; + monitorJson.left = monitorInfo.rcWork.left; + monitorJson.workAreaWidth = monitorInfo.rcWork.right - monitorInfo.rcWork.left; + monitorJson.workAreaHeight = monitorInfo.rcWork.bottom - monitorInfo.rcWork.top; + argsJson.monitors.emplace_back(std::move(monitorJson)); } } diff --git a/src/modules/fancyzones/FancyZonesLib/EditorParameters.h b/src/modules/fancyzones/FancyZonesLib/EditorParameters.h index 70124613d63..74415e9c289 100644 --- a/src/modules/fancyzones/FancyZonesLib/EditorParameters.h +++ b/src/modules/fancyzones/FancyZonesLib/EditorParameters.h @@ -1,5 +1,8 @@ #pragma once +#include +#include + namespace NonLocalizable { namespace EditorParametersIds @@ -26,5 +29,5 @@ namespace NonLocalizable class EditorParameters { public: - static bool Save() noexcept; + static bool Save(const WorkAreaConfiguration& configuration, OnThreadExecutor& dpiUnawareThread) noexcept; }; diff --git a/src/modules/fancyzones/FancyZonesLib/FancyZones.cpp b/src/modules/fancyzones/FancyZonesLib/FancyZones.cpp index 76f99175d5d..b2b470c1a4b 100644 --- a/src/modules/fancyzones/FancyZonesLib/FancyZones.cpp +++ b/src/modules/fancyzones/FancyZonesLib/FancyZones.cpp @@ -15,20 +15,23 @@ #include #include #include +#include #include #include #include #include #include #include -#include #include #include #include #include +#include #include #include +#include #include +#include enum class DisplayChangeType { @@ -87,6 +90,7 @@ struct FancyZones : public winrt::implements m_windowMouseSnapper{}; WindowKeyboardSnap m_windowKeyboardSnapper{}; - MonitorWorkAreaMap m_workAreaHandler; + WorkAreaConfiguration m_workAreaConfiguration; DraggingState m_draggingState; wil::unique_handle m_terminateEditorEvent; // Handle of FancyZonesEditor.exe we launch and wait on @@ -253,6 +257,7 @@ FancyZones::Run() noexcept } }); + VirtualDesktop::instance().UpdateVirtualDesktopId(); SyncVirtualDesktops(); // id format of applied-layouts and app-zone-history was changed in 0.60 @@ -267,7 +272,7 @@ FancyZones::Run() noexcept IFACEMETHODIMP_(void) FancyZones::Destroy() noexcept { - m_workAreaHandler.Clear(); + m_workAreaConfiguration.Clear(); BufferedPaintUnInit(); if (m_window) { @@ -289,7 +294,7 @@ FancyZones::VirtualDesktopChanged() noexcept void FancyZones::MoveSizeStart(HWND window, HMONITOR monitor) { - m_windowMouseSnapper = WindowMouseSnap::Create(window, m_workAreaHandler.GetAllWorkAreas()); + m_windowMouseSnapper = WindowMouseSnap::Create(window, m_workAreaConfiguration.GetAllWorkAreas()); if (m_windowMouseSnapper) { if (FancyZonesSettings::settings().spanZonesAcrossMonitors) @@ -329,7 +334,7 @@ void FancyZones::MoveSizeEnd() bool FancyZones::MoveToAppLastZone(HWND window, HMONITOR monitor) noexcept { - const auto& workAreas = m_workAreaHandler.GetAllWorkAreas(); + const auto& workAreas = m_workAreaConfiguration.GetAllWorkAreas(); WorkArea* workArea{ nullptr }; ZoneIndexSet indexes{}; @@ -390,15 +395,18 @@ void FancyZones::WindowCreated(HWND window) noexcept return; } - // Avoid already stamped (zoned) windows - const bool isZoned = !FancyZonesWindowProperties::RetrieveZoneIndexProperty(window).empty(); - if (isZoned) + // Hotfix + // Avoid automatically moving popup windows, as they can be just popup menus. + bool isPopup = FancyZonesWindowUtils::IsPopupWindow(window); + bool hasThickFrame = FancyZonesWindowUtils::HasThickFrame(window); + if (isPopup && !hasThickFrame) { return; } - const bool isCandidateForLastKnownZone = FancyZonesWindowUtils::IsCandidateForZoning(window); - if (!isCandidateForLastKnownZone) + // Avoid already stamped (zoned) windows + const bool isZoned = !FancyZonesWindowProperties::RetrieveZoneIndexProperty(window).empty(); + if (isZoned) { return; } @@ -523,7 +531,7 @@ void FancyZones::ToggleEditor() noexcept m_terminateEditorEvent.reset(CreateEvent(nullptr, true, false, nullptr)); - if (!EditorParameters::Save()) + if (!EditorParameters::Save(m_workAreaConfiguration, m_dpiUnawareThread)) { Logger::error(L"Failed to save editor startup parameters"); return; @@ -623,11 +631,11 @@ LRESULT FancyZones::WndProc(HWND window, UINT message, WPARAM wparam, LPARAM lpa // Check whether Alt is used in the shortcut key combination if (GetAsyncKeyState(VK_MENU) & 0x8000) { - m_windowKeyboardSnapper.Extend(foregroundWindow, windowRect, monitor, static_cast(lparam), m_workAreaHandler.GetAllWorkAreas()); + m_windowKeyboardSnapper.Extend(foregroundWindow, windowRect, monitor, static_cast(lparam), m_workAreaConfiguration.GetAllWorkAreas()); } else { - m_windowKeyboardSnapper.Snap(foregroundWindow, windowRect, monitor, static_cast(lparam), m_workAreaHandler.GetAllWorkAreas(), monitors); + m_windowKeyboardSnapper.Snap(foregroundWindow, windowRect, monitor, static_cast(lparam), m_workAreaConfiguration.GetAllWorkAreas(), monitors); } } else @@ -637,7 +645,7 @@ LRESULT FancyZones::WndProc(HWND window, UINT message, WPARAM wparam, LPARAM lpa } else { - m_windowKeyboardSnapper.Snap(foregroundWindow, monitor, static_cast(lparam), m_workAreaHandler.GetAllWorkAreas(), FancyZonesUtils::GetMonitorsOrdered()); + m_windowKeyboardSnapper.Snap(foregroundWindow, monitor, static_cast(lparam), m_workAreaConfiguration.GetAllWorkAreas(), FancyZonesUtils::GetMonitorsOrdered()); } } else if (message == WM_PRIV_INIT) @@ -732,6 +740,7 @@ void FancyZones::OnDisplayChange(DisplayChangeType changeType) noexcept updateWindowsPositions = FancyZonesSettings::settings().displayChange_moveWindows; break; case DisplayChangeType::VirtualDesktop: // Switched virtual desktop + SyncVirtualDesktops(); break; case DisplayChangeType::Initialization: // Initialization updateWindowsPositions = FancyZonesSettings::settings().zoneSetChange_moveWindows; @@ -760,15 +769,18 @@ bool FancyZones::AddWorkArea(HMONITOR monitor, const FancyZonesDataTypes::WorkAr { rect = FancyZonesUtils::GetAllMonitorsCombinedRect<&MONITORINFO::rcWork>(); } - - auto workArea = WorkArea::Create(m_hinstance, id, m_workAreaHandler.GetParent(monitor), rect); + + auto parentWorkAreaId = id; + parentWorkAreaId.virtualDesktopId = LastUsedVirtualDesktop::instance().GetId(); + + auto workArea = WorkArea::Create(m_hinstance, id, parentWorkAreaId, rect); if (!workArea) { Logger::error(L"Failed to create work area {}", id.toString()); return false; } - m_workAreaHandler.AddWorkArea(monitor, std::move(workArea)); + m_workAreaConfiguration.AddWorkArea(monitor, std::move(workArea)); return true; } @@ -790,8 +802,7 @@ void FancyZones::UpdateWorkAreas(bool updateWindowPositions) noexcept { Logger::debug(L"Update work areas, update windows positions: {}", updateWindowPositions); - m_workAreaHandler.SaveParentIds(); - m_workAreaHandler.Clear(); + m_workAreaConfiguration.Clear(); if (FancyZonesSettings::settings().spanZonesAcrossMonitors) { @@ -829,7 +840,7 @@ void FancyZones::UpdateWorkAreas(bool updateWindowPositions) noexcept if (FancyZonesSettings::settings().spanZonesAcrossMonitors) // one work area across monitors { - const auto workArea = m_workAreaHandler.GetWorkArea(nullptr); + const auto workArea = m_workAreaConfiguration.GetWorkArea(nullptr); if (workArea) { for (const auto& [window, zones] : windowsToSnap) @@ -846,7 +857,7 @@ void FancyZones::UpdateWorkAreas(bool updateWindowPositions) noexcept const auto window = iter->first; const auto zones = iter->second; const auto monitor = MonitorFromWindow(window, MONITOR_DEFAULTTONULL); - const auto workAreaForMonitor = m_workAreaHandler.GetWorkArea(monitor); + const auto workAreaForMonitor = m_workAreaConfiguration.GetWorkArea(monitor); if (workAreaForMonitor && AppZoneHistory::instance().GetAppLastZoneIndexSet(window, workAreaForMonitor->UniqueId(), workAreaForMonitor->GetLayoutId()) == zones) { workAreaForMonitor->Snap(window, zones, false); @@ -861,7 +872,7 @@ void FancyZones::UpdateWorkAreas(bool updateWindowPositions) noexcept // snap rest of the windows to other work areas (in case they were moved after the monitor unplug) for (const auto& [window, zones] : windowsToSnap) { - for (const auto& [_, workArea] : m_workAreaHandler.GetAllWorkAreas()) + for (const auto& [_, workArea] : m_workAreaConfiguration.GetAllWorkAreas()) { const auto savedIndexes = AppZoneHistory::instance().GetAppLastZoneIndexSet(window, workArea->UniqueId(), workArea->GetLayoutId()); if (savedIndexes == zones) @@ -874,7 +885,7 @@ void FancyZones::UpdateWorkAreas(bool updateWindowPositions) noexcept if (updateWindowPositions) { - for (const auto& [_, workArea] : m_workAreaHandler.GetAllWorkAreas()) + for (const auto& [_, workArea] : m_workAreaConfiguration.GetAllWorkAreas()) { if (workArea) { @@ -889,7 +900,7 @@ void FancyZones::CycleWindows(bool reverse) noexcept auto window = GetForegroundWindow(); HMONITOR current = WorkAreaKeyFromWindow(window); - auto workArea = m_workAreaHandler.GetWorkArea(current); + auto workArea = m_workAreaConfiguration.GetWorkArea(current); if (workArea) { workArea->CycleWindows(window, reverse); @@ -898,15 +909,23 @@ void FancyZones::CycleWindows(bool reverse) noexcept void FancyZones::SyncVirtualDesktops() noexcept { + // Explorer persists current virtual desktop identifier to registry on a per session basis, + // but only after first virtual desktop switch happens. If the user hasn't switched virtual + // desktops in this session value in registry will be empty and we will use default GUID in + // that case (00000000-0000-0000-0000-000000000000). + + auto lastUsed = LastUsedVirtualDesktop::instance().GetId(); + auto current = VirtualDesktop::instance().GetCurrentVirtualDesktopId(); auto guids = VirtualDesktop::instance().GetVirtualDesktopIdsFromRegistry(); - if (guids.has_value()) + + if (current != lastUsed) { - AppZoneHistory::instance().RemoveDeletedVirtualDesktops(*guids); - AppliedLayouts::instance().RemoveDeletedVirtualDesktops(*guids); + LastUsedVirtualDesktop::instance().SetId(current); + LastUsedVirtualDesktop::instance().SaveData(); } - AppZoneHistory::instance().SyncVirtualDesktops(); - AppliedLayouts::instance().SyncVirtualDesktops(); + AppliedLayouts::instance().SyncVirtualDesktops(current, lastUsed, guids); + AppZoneHistory::instance().SyncVirtualDesktops(current, lastUsed, guids); } void FancyZones::UpdateHotkey(int hotkeyId, const PowerToysSettings::HotkeyObject& hotkeyObject, bool enable) noexcept @@ -960,7 +979,7 @@ void FancyZones::SettingsUpdate(SettingId id) break; case SettingId::SpanZonesAcrossMonitors: { - m_workAreaHandler.Clear(); + m_workAreaConfiguration.Clear(); PostMessageW(m_window, WM_PRIV_INIT, NULL, NULL); } break; @@ -971,7 +990,7 @@ void FancyZones::SettingsUpdate(SettingId id) void FancyZones::RefreshLayouts() noexcept { - for (const auto& [_, workArea] : m_workAreaHandler.GetAllWorkAreas()) + for (const auto& [_, workArea] : m_workAreaConfiguration.GetAllWorkAreas()) { if (workArea) { @@ -994,11 +1013,11 @@ bool FancyZones::ShouldProcessSnapHotkey(DWORD vkCode) noexcept return false; } - if (FancyZonesSettings::settings().overrideSnapHotkeys && FancyZonesWindowUtils::IsCandidateForZoning(window)) + if (FancyZonesSettings::settings().overrideSnapHotkeys) { HMONITOR monitor = WorkAreaKeyFromWindow(window); - auto workArea = m_workAreaHandler.GetWorkArea(monitor); + auto workArea = m_workAreaConfiguration.GetWorkArea(monitor); if (!workArea) { Logger::error(L"No work area for processing snap hotkey"); @@ -1043,13 +1062,15 @@ void FancyZones::ApplyQuickLayout(int key) noexcept return; } - auto workArea = m_workAreaHandler.GetWorkAreaFromCursor(); + auto workArea = m_workAreaConfiguration.GetWorkAreaFromCursor(); if (workArea) { - AppliedLayouts::instance().ApplyLayout(workArea->UniqueId(), layout.value()); - AppliedLayouts::instance().SaveData(); - RefreshLayouts(); - FlashZones(); + if (AppliedLayouts::instance().ApplyLayout(workArea->UniqueId(), layout.value())) + { + RefreshLayouts(); + FlashZones(); + AppliedLayouts::instance().SaveData(); + } } } @@ -1057,7 +1078,7 @@ void FancyZones::FlashZones() noexcept { if (FancyZonesSettings::settings().flashZonesOnQuickSwitch && !m_draggingState.IsDragging()) { - for (const auto& [_, workArea] : m_workAreaHandler.GetAllWorkAreas()) + for (const auto& [_, workArea] : m_workAreaConfiguration.GetAllWorkAreas()) { if (workArea) { diff --git a/src/modules/fancyzones/FancyZonesLib/FancyZonesData.h b/src/modules/fancyzones/FancyZonesLib/FancyZonesData.h index 565abb31944..db7fe004a96 100644 --- a/src/modules/fancyzones/FancyZonesLib/FancyZonesData.h +++ b/src/modules/fancyzones/FancyZonesLib/FancyZonesData.h @@ -26,13 +26,7 @@ class FancyZonesData return settingsFileName; } -private: #if defined(UNIT_TESTS) - friend class FancyZonesUnitTests::LayoutHotkeysUnitTests; - friend class FancyZonesUnitTests::LayoutTemplatesUnitTests; - friend class FancyZonesUnitTests::CustomLayoutsUnitTests; - friend class FancyZonesUnitTests::AppliedLayoutsUnitTests; - inline void SetSettingsModulePath(std::wstring_view moduleName) { std::wstring result = PTSettingsHelper::get_module_save_folder_location(moduleName); @@ -46,6 +40,8 @@ class FancyZonesData return result + L"\\" + std::wstring(L"zones-settings.json"); } #endif + +private: std::wstring settingsFileName; std::wstring zonesSettingsFileName; std::wstring appZoneHistoryFileName; diff --git a/src/modules/fancyzones/FancyZonesLib/FancyZonesData/AppZoneHistory.cpp b/src/modules/fancyzones/FancyZonesLib/FancyZonesData/AppZoneHistory.cpp index 886dec5a848..c633f0c0f7d 100644 --- a/src/modules/fancyzones/FancyZonesLib/FancyZonesData/AppZoneHistory.cpp +++ b/src/modules/fancyzones/FancyZonesLib/FancyZonesData/AppZoneHistory.cpp @@ -591,67 +591,43 @@ ZoneIndexSet AppZoneHistory::GetAppLastZoneIndexSet(HWND window, const FancyZone return {}; } -void AppZoneHistory::SyncVirtualDesktops() +void AppZoneHistory::SyncVirtualDesktops(const GUID& currentVirtualDesktop, const GUID& lastUsedVirtualDesktop, std::optional> desktops) { - // Explorer persists current virtual desktop identifier to registry on a per session basis, - // but only after first virtual desktop switch happens. If the user hasn't switched virtual - // desktops in this session value in registry will be empty and we will use default GUID in - // that case (00000000-0000-0000-0000-000000000000). - - auto savedInRegistryVirtualDesktopID = VirtualDesktop::instance().GetCurrentVirtualDesktopIdFromRegistry(); - if (!savedInRegistryVirtualDesktopID.has_value() || savedInRegistryVirtualDesktopID.value() == GUID_NULL) - { - return; - } + TAppZoneHistoryMap history; - auto currentVirtualDesktopStr = FancyZonesUtils::GuidToString(savedInRegistryVirtualDesktopID.value()); - if (!currentVirtualDesktopStr.has_value()) + std::unordered_set activeDesktops{}; + if (desktops.has_value()) { - Logger::error(L"Failed to convert virtual desktop GUID to string"); - return; + activeDesktops = std::unordered_set(std::begin(desktops.value()), std::end(desktops.value())); } - Logger::info(L"AppZoneHistory Sync virtual desktops: current {}", currentVirtualDesktopStr.value()); - - bool dirtyFlag = false; - - for (auto& [path, perDesktopData] : m_history) - { - for (auto& data : perDesktopData) + auto findCurrentVirtualDesktopInSavedHistory = [&](const std::pair>& val) -> bool + { + for (auto& data : val.second) { - if (data.workAreaId.virtualDesktopId == GUID_NULL) + if (data.workAreaId.virtualDesktopId == currentVirtualDesktop) { - data.workAreaId.virtualDesktopId = savedInRegistryVirtualDesktopID.value(); - dirtyFlag = true; + return true; } } - } - - if (dirtyFlag) - { - Logger::info(L"Update Virtual Desktop id to {}", currentVirtualDesktopStr.value()); - SaveData(); - } -} + return false; + }; + bool replaceLastUsedWithCurrent = !desktops.has_value() || currentVirtualDesktop == GUID_NULL || lastUsedVirtualDesktop == GUID_NULL || std::find_if(m_history.begin(), m_history.end(), findCurrentVirtualDesktopInSavedHistory) == m_history.end(); -void AppZoneHistory::RemoveDeletedVirtualDesktops(const std::vector& activeDesktops) -{ - std::unordered_set active(std::begin(activeDesktops), std::end(activeDesktops)); bool dirtyFlag = false; - for (auto it = std::begin(m_history); it != std::end(m_history);) { auto& perDesktopData = it->second; for (auto desktopIt = std::begin(perDesktopData); desktopIt != std::end(perDesktopData);) { - if (desktopIt->workAreaId.virtualDesktopId != GUID_NULL && !active.contains(desktopIt->workAreaId.virtualDesktopId)) + if (replaceLastUsedWithCurrent && desktopIt->workAreaId.virtualDesktopId == lastUsedVirtualDesktop) { - auto virtualDesktopIdStr = FancyZonesUtils::GuidToString(desktopIt->workAreaId.virtualDesktopId); - if (virtualDesktopIdStr) - { - Logger::info(L"Remove Virtual Desktop id {} from app-zone-history", virtualDesktopIdStr.value()); - } + desktopIt->workAreaId.virtualDesktopId = currentVirtualDesktop; + dirtyFlag = true; + } + if (desktopIt->workAreaId.virtualDesktopId != currentVirtualDesktop && !activeDesktops.contains(desktopIt->workAreaId.virtualDesktopId)) + { desktopIt = perDesktopData.erase(desktopIt); dirtyFlag = true; } @@ -664,6 +640,7 @@ void AppZoneHistory::RemoveDeletedVirtualDesktops(const std::vector& activ if (perDesktopData.empty()) { it = m_history.erase(it); + dirtyFlag = true; } else { diff --git a/src/modules/fancyzones/FancyZonesLib/FancyZonesData/AppZoneHistory.h b/src/modules/fancyzones/FancyZonesLib/FancyZonesData/AppZoneHistory.h index 18ec84ac93b..6356ab50fad 100644 --- a/src/modules/fancyzones/FancyZonesLib/FancyZonesData/AppZoneHistory.h +++ b/src/modules/fancyzones/FancyZonesLib/FancyZonesData/AppZoneHistory.h @@ -41,6 +41,13 @@ class AppZoneHistory #endif } +#if defined(UNIT_TESTS) + inline void SetAppZoneHistory(const TAppZoneHistoryMap& history) + { + m_history = history; + } +#endif + void LoadData(); void SaveData(); void AdjustWorkAreaIds(const std::vector& ids); @@ -56,9 +63,8 @@ class AppZoneHistory bool IsAnotherWindowOfApplicationInstanceZoned(HWND window, const FancyZonesDataTypes::WorkAreaId& workAreaId) const noexcept; ZoneIndexSet GetAppLastZoneIndexSet(HWND window, const FancyZonesDataTypes::WorkAreaId& workAreaId, const GUID& layoutId) const; - void SyncVirtualDesktops(); - void RemoveDeletedVirtualDesktops(const std::vector& activeDesktops); - + void SyncVirtualDesktops(const GUID& currentVirtualDesktop, const GUID& lastUsedVirtualDesktop, std::optional> desktops); + private: AppZoneHistory(); ~AppZoneHistory() = default; diff --git a/src/modules/fancyzones/FancyZonesLib/FancyZonesData/AppliedLayouts.cpp b/src/modules/fancyzones/FancyZonesLib/FancyZonesData/AppliedLayouts.cpp index a6c86952d68..b05cd3da0b2 100644 --- a/src/modules/fancyzones/FancyZonesLib/FancyZonesData/AppliedLayouts.cpp +++ b/src/modules/fancyzones/FancyZonesLib/FancyZonesData/AppliedLayouts.cpp @@ -279,29 +279,7 @@ void AppliedLayouts::LoadData() void AppliedLayouts::SaveData() { - bool dirtyFlag = false; - TAppliedLayoutsMap updatedMap; - - for (const auto& [id, data] : m_layouts) - { - auto updatedId = id; - if (!VirtualDesktop::instance().IsVirtualDesktopIdSavedInRegistry(id.virtualDesktopId)) - { - updatedId.virtualDesktopId = GUID_NULL; - dirtyFlag = true; - } - - updatedMap.insert({ updatedId, data }); - } - - if (dirtyFlag) - { - json::to_file(AppliedLayoutsFileName(), JsonUtils::SerializeJson(updatedMap)); - } - else - { - json::to_file(AppliedLayoutsFileName(), JsonUtils::SerializeJson(m_layouts)); - } + json::to_file(AppliedLayoutsFileName(), JsonUtils::SerializeJson(m_layouts)); } void AppliedLayouts::AdjustWorkAreaIds(const std::vector& ids) @@ -344,86 +322,75 @@ void AppliedLayouts::AdjustWorkAreaIds(const std::vector> desktops) { - // Explorer persists current virtual desktop identifier to registry on a per session basis, - // but only after first virtual desktop switch happens. If the user hasn't switched virtual - // desktops in this session value in registry will be empty and we will use default GUID in - // that case (00000000-0000-0000-0000-000000000000). + TAppliedLayoutsMap layouts; - auto savedInRegistryVirtualDesktopID = VirtualDesktop::instance().GetCurrentVirtualDesktopIdFromRegistry(); - if (!savedInRegistryVirtualDesktopID.has_value() || savedInRegistryVirtualDesktopID.value() == GUID_NULL) - { - return; - } + auto findCurrentVirtualDesktopInSavedLayouts = [&](const std::pair& val) -> bool { return val.first.virtualDesktopId == currentVirtualDesktop; }; + bool replaceLastUsedWithCurrent = !desktops.has_value() || currentVirtualDesktop == GUID_NULL || + std::find_if(m_layouts.begin(), m_layouts.end(), findCurrentVirtualDesktopInSavedLayouts) == m_layouts.end(); + bool copyToOtherVirtualDesktops = lastUsedVirtualDesktop == GUID_NULL && currentVirtualDesktop != GUID_NULL && desktops.has_value(); - auto currentVirtualDesktopStr = FancyZonesUtils::GuidToString(savedInRegistryVirtualDesktopID.value()); - if (!currentVirtualDesktopStr.has_value()) + for (const auto& [workAreaId, layout] : m_layouts) { - Logger::error(L"Failed to convert virtual desktop GUID to string"); - return; - } - - Logger::info(L"AppliedLayouts Sync virtual desktops: current {}", currentVirtualDesktopStr.value()); - - bool dirtyFlag = false; + if (replaceLastUsedWithCurrent && workAreaId.virtualDesktopId == lastUsedVirtualDesktop) + { + // replace "lastUsedVirtualDesktop" with "currentVirtualDesktop" + auto updatedWorkAreaId = workAreaId; + updatedWorkAreaId.virtualDesktopId = currentVirtualDesktop; + layouts.insert({ updatedWorkAreaId, layout }); - std::vector replaceWithCurrentId{}; + if (copyToOtherVirtualDesktops) + { + // Copy to other virtual desktops on the 1st VD creation. + // If we just replace the id, we'll lose the layout on the other desktops. + // Usage scenario: + // apply the layout to the single virtual desktop with id = GUID_NULL, + // create the 2nd virtual desktop and switch to it, + // so virtual desktop id changes from GUID_NULL to a valid value of the 2nd VD. + // Then change the layout on the 2nd VD and switch back to the 1st VD. + // Layout on the initial VD will be changed too without initializing it beforehand. + for (const auto& id : desktops.value()) + { + if (id != currentVirtualDesktop) + { + auto copyWorkAreaId = workAreaId; + copyWorkAreaId.virtualDesktopId = id; + layouts.insert({ copyWorkAreaId, layout }); + } + } + } + } - for (const auto& [id, data] : m_layouts) - { - if (id.virtualDesktopId == GUID_NULL) + if (workAreaId.virtualDesktopId == currentVirtualDesktop || (desktops.has_value() && + std::find(desktops.value().begin(), desktops.value().end(), workAreaId.virtualDesktopId) != desktops.value().end())) { - replaceWithCurrentId.push_back(id); - dirtyFlag = true; + // keep only actual virtual desktop values + layouts.insert({ workAreaId, layout }); } } - for (const auto& id : replaceWithCurrentId) + if (layouts != m_layouts) { - auto mapEntry = m_layouts.extract(id); - mapEntry.key().virtualDesktopId = savedInRegistryVirtualDesktopID.value(); - m_layouts.insert(std::move(mapEntry)); - } - - if (dirtyFlag) - { - Logger::info(L"Update Virtual Desktop id to {}", currentVirtualDesktopStr.value()); + m_layouts = layouts; SaveData(); - } -} - -void AppliedLayouts::RemoveDeletedVirtualDesktops(const std::vector& activeDesktops) -{ - std::unordered_set active(std::begin(activeDesktops), std::end(activeDesktops)); - bool dirtyFlag = false; - - for (auto it = std::begin(m_layouts); it != std::end(m_layouts);) - { - GUID desktopId = it->first.virtualDesktopId; - if (desktopId != GUID_NULL) + std::wstring currentStr = FancyZonesUtils::GuidToString(currentVirtualDesktop).value_or(L"incorrect guid"); + std::wstring lastUsedStr = FancyZonesUtils::GuidToString(lastUsedVirtualDesktop).value_or(L"incorrect guid"); + std::wstring registryStr{}; + if (desktops.has_value()) { - auto foundId = active.find(desktopId); - if (foundId == std::end(active)) + for (const auto& id : desktops.value()) { - wil::unique_cotaskmem_string virtualDesktopIdStr; - if (SUCCEEDED(StringFromCLSID(desktopId, &virtualDesktopIdStr))) - { - Logger::info(L"Remove Virtual Desktop id {}", virtualDesktopIdStr.get()); - } - - it = m_layouts.erase(it); - dirtyFlag = true; - continue; + registryStr += FancyZonesUtils::GuidToString(id).value_or(L"incorrect guid") + L" "; } } - ++it; - } + else + { + registryStr = L"empty"; + } - if (dirtyFlag) - { - SaveData(); + Logger::info(L"Synced virtual desktops. Current: {}, last used: {}, registry: {}", currentStr, lastUsedStr, registryStr); } } @@ -449,9 +416,9 @@ bool AppliedLayouts::IsLayoutApplied(const FancyZonesDataTypes::WorkAreaId& id) return iter != m_layouts.end(); } -bool AppliedLayouts::ApplyLayout(const FancyZonesDataTypes::WorkAreaId& deviceId, LayoutData layout) +bool AppliedLayouts::ApplyLayout(const FancyZonesDataTypes::WorkAreaId& workAreaId, LayoutData layout) { - m_layouts[deviceId] = std::move(layout); + m_layouts[workAreaId] = layout; return true; } @@ -496,9 +463,5 @@ bool AppliedLayouts::CloneLayout(const FancyZonesDataTypes::WorkAreaId& srcId, c } Logger::info(L"Clone layout from {} to {}", dstId.toString(), srcId.toString()); - m_layouts[dstId] = m_layouts[srcId]; - - SaveData(); - - return true; + return ApplyLayout(dstId, m_layouts[srcId]); } diff --git a/src/modules/fancyzones/FancyZonesLib/FancyZonesData/AppliedLayouts.h b/src/modules/fancyzones/FancyZonesLib/FancyZonesData/AppliedLayouts.h index bea37c6d92b..e4f796913b8 100644 --- a/src/modules/fancyzones/FancyZonesLib/FancyZonesData/AppliedLayouts.h +++ b/src/modules/fancyzones/FancyZonesLib/FancyZonesData/AppliedLayouts.h @@ -49,12 +49,18 @@ class AppliedLayouts #endif } +#if defined(UNIT_TESTS) + inline void SetAppliedLayouts(TAppliedLayoutsMap layouts) + { + m_layouts = layouts; + } +#endif + void LoadData(); void SaveData(); void AdjustWorkAreaIds(const std::vector& ids); - void SyncVirtualDesktops(); - void RemoveDeletedVirtualDesktops(const std::vector& activeDesktops); + void SyncVirtualDesktops(const GUID& currentVirtualDesktop, const GUID& lastUsedVirtualDesktop, std::optional> desktops); std::optional GetDeviceLayout(const FancyZonesDataTypes::WorkAreaId& id) const noexcept; const TAppliedLayoutsMap& GetAppliedLayoutMap() const noexcept; diff --git a/src/modules/fancyzones/FancyZonesLib/FancyZonesData/LastUsedVirtualDesktop.cpp b/src/modules/fancyzones/FancyZonesLib/FancyZonesData/LastUsedVirtualDesktop.cpp new file mode 100644 index 00000000000..3c62df9e88c --- /dev/null +++ b/src/modules/fancyzones/FancyZonesLib/FancyZonesData/LastUsedVirtualDesktop.cpp @@ -0,0 +1,78 @@ +#include "../pch.h" +#include "LastUsedVirtualDesktop.h" + +#include + +#include + +namespace JsonUtils +{ + GUID ParseJson(const json::JsonObject& json) + { + auto idStr = json.GetNamedString(NonLocalizable::LastUsedVirtualDesktop::LastUsedVirtualDesktopID); + auto idOpt = FancyZonesUtils::GuidFromString(idStr.c_str()); + + if (!idOpt.has_value()) + { + return {}; + } + + return idOpt.value(); + } + + json::JsonObject SerializeJson(const GUID& id) + { + json::JsonObject result{}; + + auto virtualDesktopStr = FancyZonesUtils::GuidToString(id); + if (virtualDesktopStr) + { + result.SetNamedValue(NonLocalizable::LastUsedVirtualDesktop::LastUsedVirtualDesktopID, json::value(virtualDesktopStr.value())); + } + + return result; + } +} + + +LastUsedVirtualDesktop& LastUsedVirtualDesktop::instance() +{ + static LastUsedVirtualDesktop self; + return self; +} + +void LastUsedVirtualDesktop::LoadData() +{ + auto data = json::from_file(LastUsedVirtualDesktopFileName()); + + try + { + if (data) + { + m_id = JsonUtils::ParseJson(data.value()); + } + else + { + m_id = GUID_NULL; + } + } + catch (const winrt::hresult_error& e) + { + Logger::error(L"Parsing last-used-virtual-desktop error: {}", e.message()); + } +} + +void LastUsedVirtualDesktop::SaveData() const +{ + json::to_file(LastUsedVirtualDesktopFileName(), JsonUtils::SerializeJson(m_id)); +} + +GUID LastUsedVirtualDesktop::GetId() const +{ + return m_id; +} + +void LastUsedVirtualDesktop::SetId(GUID id) +{ + m_id = id; +} \ No newline at end of file diff --git a/src/modules/fancyzones/FancyZonesLib/FancyZonesData/LastUsedVirtualDesktop.h b/src/modules/fancyzones/FancyZonesLib/FancyZonesData/LastUsedVirtualDesktop.h new file mode 100644 index 00000000000..da9e07eca4b --- /dev/null +++ b/src/modules/fancyzones/FancyZonesLib/FancyZonesData/LastUsedVirtualDesktop.h @@ -0,0 +1,41 @@ +#pragma once + +#include + +#include + +namespace NonLocalizable +{ + namespace LastUsedVirtualDesktop + { + const static wchar_t* LastUsedVirtualDesktopID = L"last-used-virtual-desktop"; + } +} + +class LastUsedVirtualDesktop +{ +public: + static LastUsedVirtualDesktop& instance(); + + inline static std::wstring LastUsedVirtualDesktopFileName() + { + std::wstring saveFolderPath = PTSettingsHelper::get_module_save_folder_location(NonLocalizable::ModuleKey); +#if defined(UNIT_TESTS) + return saveFolderPath + L"\\test-last-used-virtual-desktop.json"; +#else + return saveFolderPath + L"\\last-used-virtual-desktop.json"; +#endif + } + + void LoadData(); + void SaveData() const; + + GUID GetId() const; + void SetId(GUID id); + +private: + LastUsedVirtualDesktop() = default; + ~LastUsedVirtualDesktop() = default; + + GUID m_id{}; +}; diff --git a/src/modules/fancyzones/FancyZonesLib/FancyZonesDataTypes.h b/src/modules/fancyzones/FancyZonesLib/FancyZonesDataTypes.h index 6f61b3e125b..58673c70129 100644 --- a/src/modules/fancyzones/FancyZonesLib/FancyZonesDataTypes.h +++ b/src/modules/fancyzones/FancyZonesLib/FancyZonesDataTypes.h @@ -209,14 +209,12 @@ namespace FancyZonesDataTypes inline bool operator==(const WorkAreaId& lhs, const WorkAreaId& rhs) { - bool vdEqual = (lhs.virtualDesktopId == rhs.virtualDesktopId || lhs.virtualDesktopId == GUID_NULL || rhs.virtualDesktopId == GUID_NULL); - return vdEqual && lhs.monitorId == rhs.monitorId; + return lhs.virtualDesktopId == rhs.virtualDesktopId && lhs.monitorId == rhs.monitorId; } inline bool operator!=(const WorkAreaId& lhs, const WorkAreaId& rhs) { - bool vdEqual = (lhs.virtualDesktopId == rhs.virtualDesktopId || lhs.virtualDesktopId == GUID_NULL || rhs.virtualDesktopId == GUID_NULL); - return !vdEqual || lhs.monitorId != rhs.monitorId; + return lhs.virtualDesktopId != rhs.virtualDesktopId || lhs.monitorId != rhs.monitorId; } inline bool operator<(const WorkAreaId& lhs, const WorkAreaId& rhs) @@ -240,6 +238,11 @@ namespace FancyZonesDataTypes return lhs.monitorId.deviceId < rhs.monitorId.deviceId; } + + inline bool operator==(const AppZoneHistoryData& lhs, const AppZoneHistoryData& rhs) + { + return lhs.layoutId == rhs.layoutId && lhs.workAreaId == rhs.workAreaId && lhs.zoneIndexSet == rhs.zoneIndexSet && lhs.processIdToHandleMap == rhs.processIdToHandleMap; + } } namespace std diff --git a/src/modules/fancyzones/FancyZonesLib/FancyZonesLib.vcxproj b/src/modules/fancyzones/FancyZonesLib/FancyZonesLib.vcxproj index 9bb26ccd958..faf124b3efd 100644 --- a/src/modules/fancyzones/FancyZonesLib/FancyZonesLib.vcxproj +++ b/src/modules/fancyzones/FancyZonesLib/FancyZonesLib.vcxproj @@ -43,6 +43,7 @@ + @@ -59,7 +60,7 @@ - + @@ -100,9 +101,13 @@ ../pch.h + + ../pch.h + ../pch.h + @@ -114,7 +119,7 @@ - + Create diff --git a/src/modules/fancyzones/FancyZonesLib/FancyZonesLib.vcxproj.filters b/src/modules/fancyzones/FancyZonesLib/FancyZonesLib.vcxproj.filters index e8012868f2e..220b0cc22f6 100644 --- a/src/modules/fancyzones/FancyZonesLib/FancyZonesLib.vcxproj.filters +++ b/src/modules/fancyzones/FancyZonesLib/FancyZonesLib.vcxproj.filters @@ -54,7 +54,7 @@ Header Files - + Header Files @@ -168,6 +168,9 @@ Header Files + + Header Files\FancyZonesData + @@ -200,7 +203,7 @@ Source Files - + Source Files @@ -272,6 +275,12 @@ Source Files + + Source Files\FancyZonesData + + + Source Files + diff --git a/src/modules/fancyzones/FancyZonesLib/FancyZonesWindowProcessing.cpp b/src/modules/fancyzones/FancyZonesLib/FancyZonesWindowProcessing.cpp new file mode 100644 index 00000000000..102392ba91b --- /dev/null +++ b/src/modules/fancyzones/FancyZonesLib/FancyZonesWindowProcessing.cpp @@ -0,0 +1,56 @@ +#include "pch.h" +#include "FancyZonesWindowProcessing.h" + +#include +#include +#include + +bool FancyZonesWindowProcessing::IsProcessable(HWND window) noexcept +{ + const bool isSplashScreen = FancyZonesWindowUtils::IsSplashScreen(window); + if (isSplashScreen) + { + return false; + } + + const bool windowMinimized = IsIconic(window); + if (windowMinimized) + { + return false; + } + + const bool standard = FancyZonesWindowUtils::IsStandardWindow(window); + if (!standard) + { + return false; + } + + // popup could be the window we don't want to snap: start menu, notification popup, tray window, etc. + // also, popup could be the windows we want to snap disregarding the "allowSnapPopupWindows" setting, e.g. Telegram + bool isPopup = FancyZonesWindowUtils::IsPopupWindow(window) && !FancyZonesWindowUtils::HasThickFrameAndMinimizeMaximizeButtons(window); + if (isPopup && !FancyZonesSettings::settings().allowSnapPopupWindows) + { + return false; + } + + // allow child windows + auto hasOwner = FancyZonesWindowUtils::HasVisibleOwner(window); + if (hasOwner && !FancyZonesSettings::settings().allowSnapChildWindows) + { + return false; + } + + if (FancyZonesWindowUtils::IsExcluded(window)) + { + return false; + } + + // Switch between virtual desktops results with posting same windows messages that also indicate + // creation of new window. We need to check if window being processed is on currently active desktop. + if (!VirtualDesktop::instance().IsWindowOnCurrentDesktop(window)) + { + return false; + } + + return true; +} diff --git a/src/modules/fancyzones/FancyZonesLib/FancyZonesWindowProcessing.h b/src/modules/fancyzones/FancyZonesLib/FancyZonesWindowProcessing.h index fc2108ffbb3..21fbcfbad2a 100644 --- a/src/modules/fancyzones/FancyZonesLib/FancyZonesWindowProcessing.h +++ b/src/modules/fancyzones/FancyZonesLib/FancyZonesWindowProcessing.h @@ -1,39 +1,6 @@ #pragma once -#include -#include - namespace FancyZonesWindowProcessing { - inline bool IsProcessable(HWND window) noexcept - { - const bool isSplashScreen = FancyZonesWindowUtils::IsSplashScreen(window); - if (isSplashScreen) - { - return false; - } - - const bool windowMinimized = IsIconic(window); - if (windowMinimized) - { - return false; - } - - // For windows that FancyZones shouldn't process (start menu, tray, popup menus) - // VirtualDesktopManager is unable to retrieve virtual desktop id and returns an error. - auto desktopId = VirtualDesktop::instance().GetDesktopId(window); - if (!desktopId.has_value()) - { - return false; - } - - // Switch between virtual desktops results with posting same windows messages that also indicate - // creation of new window. We need to check if window being processed is on currently active desktop. - if (!VirtualDesktop::instance().IsWindowOnCurrentDesktop(window)) - { - return false; - } - - return true; - } + bool IsProcessable(HWND window) noexcept; } \ No newline at end of file diff --git a/src/modules/fancyzones/FancyZonesLib/VirtualDesktop.cpp b/src/modules/fancyzones/FancyZonesLib/VirtualDesktop.cpp index 67bf726d2bc..dbd1511d5ad 100644 --- a/src/modules/fancyzones/FancyZonesLib/VirtualDesktop.cpp +++ b/src/modules/fancyzones/FancyZonesLib/VirtualDesktop.cpp @@ -270,30 +270,16 @@ GUID VirtualDesktop::GetCurrentVirtualDesktopId() const noexcept return m_currentVirtualDesktopId; } -GUID VirtualDesktop::GetPreviousVirtualDesktopId() const noexcept -{ - return m_previousDesktopId; -} - void VirtualDesktop::UpdateVirtualDesktopId() noexcept { - m_previousDesktopId = m_currentVirtualDesktopId; - auto currentVirtualDesktopId = GetCurrentVirtualDesktopIdFromRegistry(); - if (!currentVirtualDesktopId.has_value()) + if (currentVirtualDesktopId.has_value()) { - Logger::info("No Virtual Desktop Id found in registry"); - currentVirtualDesktopId = VirtualDesktop::instance().GetDesktopIdByTopLevelWindows(); + m_currentVirtualDesktopId = currentVirtualDesktopId.value(); } - - if (currentVirtualDesktopId.has_value()) + else { - m_currentVirtualDesktopId = *currentVirtualDesktopId; - - if (m_currentVirtualDesktopId == GUID_NULL) - { - Logger::warn("Couldn't retrieve virtual desktop id"); - } + m_currentVirtualDesktopId = GUID_NULL; } Trace::VirtualDesktopChanged(); diff --git a/src/modules/fancyzones/FancyZonesLib/VirtualDesktop.h b/src/modules/fancyzones/FancyZonesLib/VirtualDesktop.h index 069e1883ea3..4c25518ce8a 100644 --- a/src/modules/fancyzones/FancyZonesLib/VirtualDesktop.h +++ b/src/modules/fancyzones/FancyZonesLib/VirtualDesktop.h @@ -7,7 +7,6 @@ class VirtualDesktop // saved values GUID GetCurrentVirtualDesktopId() const noexcept; - GUID GetPreviousVirtualDesktopId() const noexcept; void UpdateVirtualDesktopId() noexcept; // IVirtualDesktopManager @@ -29,7 +28,6 @@ class VirtualDesktop IVirtualDesktopManager* m_vdManager{nullptr}; GUID m_currentVirtualDesktopId{}; - GUID m_previousDesktopId{}; std::optional> GetVirtualDesktopIdsFromRegistry(HKEY hKey) const; }; diff --git a/src/modules/fancyzones/FancyZonesLib/WindowMouseSnap.cpp b/src/modules/fancyzones/FancyZonesLib/WindowMouseSnap.cpp index c6b8f8ceddb..a69aaa7ef72 100644 --- a/src/modules/fancyzones/FancyZonesLib/WindowMouseSnap.cpp +++ b/src/modules/fancyzones/FancyZonesLib/WindowMouseSnap.cpp @@ -32,7 +32,6 @@ WindowMouseSnap::~WindowMouseSnap() std::unique_ptr WindowMouseSnap::Create(HWND window, const std::unordered_map>& activeWorkAreas) { if (!FancyZonesWindowProcessing::IsProcessable(window) || - !FancyZonesWindowUtils::IsCandidateForZoning(window) || FancyZonesWindowUtils::IsCursorTypeIndicatingSizeEvent()) { return nullptr; diff --git a/src/modules/fancyzones/FancyZonesLib/WindowUtils.cpp b/src/modules/fancyzones/FancyZonesLib/WindowUtils.cpp index dbad77732c3..ec246180fda 100644 --- a/src/modules/fancyzones/FancyZonesLib/WindowUtils.cpp +++ b/src/modules/fancyzones/FancyZonesLib/WindowUtils.cpp @@ -166,6 +166,8 @@ bool FancyZonesWindowUtils::HasVisibleOwner(HWND window) noexcept bool FancyZonesWindowUtils::IsStandardWindow(HWND window) { + // True if from the styles the window looks like a standard window + if (GetAncestor(window, GA_ROOT) != window) { return false; @@ -181,13 +183,6 @@ bool FancyZonesWindowUtils::IsStandardWindow(HWND window) return false; } - std::array class_name; - GetClassNameA(window, class_name.data(), static_cast(class_name.size())); - if (is_system_window(window, class_name.data())) - { - return false; - } - return true; } @@ -197,48 +192,16 @@ bool FancyZonesWindowUtils::IsPopupWindow(HWND window) noexcept return ((style & WS_POPUP) == WS_POPUP); } -bool FancyZonesWindowUtils::HasThickFrameAndMinimizeMaximizeButtons(HWND window) noexcept +bool FancyZonesWindowUtils::HasThickFrame(HWND window) noexcept { auto style = GetWindowLong(window, GWL_STYLE); - return ((style & WS_THICKFRAME) == WS_THICKFRAME && (style & WS_MINIMIZEBOX) == WS_MINIMIZEBOX && (style & WS_MAXIMIZEBOX) == WS_MAXIMIZEBOX); + return ((style & WS_THICKFRAME) == WS_THICKFRAME); } -bool FancyZonesWindowUtils::IsCandidateForZoning(HWND window) +bool FancyZonesWindowUtils::HasThickFrameAndMinimizeMaximizeButtons(HWND window) noexcept { - bool isStandard = IsStandardWindow(window); - if (!isStandard) - { - return false; - } - - // popup could be the window we don't want to snap: start menu, notification popup, tray window, etc. - // also, popup could be the windows we want to snap disregarding the "allowSnapPopupWindows" setting, e.g. Telegram - bool isPopup = IsPopupWindow(window); - if (isPopup && !HasThickFrameAndMinimizeMaximizeButtons(window) && !FancyZonesSettings::settings().allowSnapPopupWindows) - { - return false; - } - - // allow child windows - auto hasOwner = HasVisibleOwner(window); - if (hasOwner && !FancyZonesSettings::settings().allowSnapChildWindows) - { - return false; - } - - std::wstring processPath = get_process_path_waiting_uwp(window); - CharUpperBuffW(const_cast(processPath).data(), static_cast(processPath.length())); - if (IsExcludedByUser(window, processPath)) - { - return false; - } - - if (IsExcludedByDefault(window, processPath)) - { - return false; - } - - return true; + auto style = GetWindowLong(window, GWL_STYLE); + return ((style & WS_THICKFRAME) == WS_THICKFRAME && (style & WS_MINIMIZEBOX) == WS_MINIMIZEBOX && (style & WS_MAXIMIZEBOX) == WS_MAXIMIZEBOX); } bool FancyZonesWindowUtils::IsProcessOfWindowElevated(HWND window) @@ -268,6 +231,23 @@ bool FancyZonesWindowUtils::IsProcessOfWindowElevated(HWND window) return false; } +bool FancyZonesWindowUtils::IsExcluded(HWND window) +{ + std::wstring processPath = get_process_path_waiting_uwp(window); + CharUpperBuffW(const_cast(processPath).data(), static_cast(processPath.length())); + if (IsExcludedByUser(window, processPath)) + { + return true; + } + + if (IsExcludedByDefault(window, processPath)) + { + return true; + } + + return false; +} + bool FancyZonesWindowUtils::IsExcludedByUser(const HWND& hwnd, std::wstring& processPath) noexcept { return (check_excluded_app(hwnd, processPath, FancyZonesSettings::settings().excludedAppsArray)); @@ -281,6 +261,13 @@ bool FancyZonesWindowUtils::IsExcludedByDefault(const HWND& hwnd, std::wstring& return true; } + std::array class_name; + GetClassNameA(hwnd, class_name.data(), static_cast(class_name.size())); + if (is_system_window(hwnd, class_name.data())) + { + return true; + } + static std::vector defaultExcludedApps = { NonLocalizable::PowerToysAppFZEditor, NonLocalizable::CoreWindow, NonLocalizable::SearchUI }; return (check_excluded_app(hwnd, processPath, defaultExcludedApps)); } diff --git a/src/modules/fancyzones/FancyZonesLib/WindowUtils.h b/src/modules/fancyzones/FancyZonesLib/WindowUtils.h index 9659d78b9cb..80a265e0ab8 100644 --- a/src/modules/fancyzones/FancyZonesLib/WindowUtils.h +++ b/src/modules/fancyzones/FancyZonesLib/WindowUtils.h @@ -20,9 +20,11 @@ namespace FancyZonesWindowUtils bool HasVisibleOwner(HWND window) noexcept; bool IsStandardWindow(HWND window); bool IsPopupWindow(HWND window) noexcept; + bool HasThickFrame(HWND window) noexcept; bool HasThickFrameAndMinimizeMaximizeButtons(HWND window) noexcept; - bool IsCandidateForZoning(HWND window); bool IsProcessOfWindowElevated(HWND window); // If HWND is already dead, we assume it wasn't elevated + + bool IsExcluded(HWND window); bool IsExcludedByUser(const HWND& hwnd, std::wstring& processPath) noexcept; bool IsExcludedByDefault(const HWND& hwnd, std::wstring& processPath) noexcept; diff --git a/src/modules/fancyzones/FancyZonesLib/WorkArea.cpp b/src/modules/fancyzones/FancyZonesLib/WorkArea.cpp index 2d4b6adf902..638776c25ae 100644 --- a/src/modules/fancyzones/FancyZonesLib/WorkArea.cpp +++ b/src/modules/fancyzones/FancyZonesLib/WorkArea.cpp @@ -244,14 +244,12 @@ void WorkArea::InitLayout(const FancyZonesDataTypes::WorkAreaId& parentUniqueId) const bool isLayoutAlreadyApplied = AppliedLayouts::instance().IsLayoutApplied(m_uniqueId); if (!isLayoutAlreadyApplied) { - if (parentUniqueId.virtualDesktopId != GUID_NULL) - { - AppliedLayouts::instance().CloneLayout(parentUniqueId, m_uniqueId); - } - else + if (!AppliedLayouts::instance().CloneLayout(parentUniqueId, m_uniqueId)) { AppliedLayouts::instance().ApplyDefaultLayout(m_uniqueId); } + + AppliedLayouts::instance().SaveData(); } CalculateZoneSet(); diff --git a/src/modules/fancyzones/FancyZonesLib/MonitorWorkAreaMap.cpp b/src/modules/fancyzones/FancyZonesLib/WorkAreaConfiguration.cpp similarity index 57% rename from src/modules/fancyzones/FancyZonesLib/MonitorWorkAreaMap.cpp rename to src/modules/fancyzones/FancyZonesLib/WorkAreaConfiguration.cpp index 486e2739ea5..a3cfa7cf45a 100644 --- a/src/modules/fancyzones/FancyZonesLib/MonitorWorkAreaMap.cpp +++ b/src/modules/fancyzones/FancyZonesLib/WorkAreaConfiguration.cpp @@ -1,9 +1,9 @@ #include "pch.h" -#include "MonitorWorkAreaMap.h" +#include "WorkAreaConfiguration.h" #include -WorkArea* const MonitorWorkAreaMap::GetWorkArea(HMONITOR monitor) const +WorkArea* const WorkAreaConfiguration::GetWorkArea(HMONITOR monitor) const { auto iter = m_workAreaMap.find(monitor); if (iter != m_workAreaMap.end()) @@ -14,7 +14,7 @@ WorkArea* const MonitorWorkAreaMap::GetWorkArea(HMONITOR monitor) const return nullptr; } -WorkArea* const MonitorWorkAreaMap::GetWorkAreaFromCursor() const +WorkArea* const WorkAreaConfiguration::GetWorkAreaFromCursor() const { const auto allMonitorsWorkArea = GetWorkArea(nullptr); if (allMonitorsWorkArea) @@ -35,7 +35,7 @@ WorkArea* const MonitorWorkAreaMap::GetWorkAreaFromCursor() const } } -WorkArea* const MonitorWorkAreaMap::GetWorkAreaFromWindow(HWND window) const +WorkArea* const WorkAreaConfiguration::GetWorkAreaFromWindow(HWND window) const { const auto allMonitorsWorkArea = GetWorkArea(nullptr); if (allMonitorsWorkArea) @@ -51,39 +51,17 @@ WorkArea* const MonitorWorkAreaMap::GetWorkAreaFromWindow(HWND window) const } } -const std::unordered_map>& MonitorWorkAreaMap::GetAllWorkAreas() const noexcept +const std::unordered_map>& WorkAreaConfiguration::GetAllWorkAreas() const noexcept { return m_workAreaMap; } -void MonitorWorkAreaMap::AddWorkArea(HMONITOR monitor, std::unique_ptr workArea) +void WorkAreaConfiguration::AddWorkArea(HMONITOR monitor, std::unique_ptr workArea) { m_workAreaMap.insert({ monitor, std::move(workArea) }); } -FancyZonesDataTypes::WorkAreaId MonitorWorkAreaMap::GetParent(HMONITOR monitor) const -{ - if (m_workAreaParents.contains(monitor)) - { - return m_workAreaParents.at(monitor); - } - - return FancyZonesDataTypes::WorkAreaId{}; -} - -void MonitorWorkAreaMap::SaveParentIds() -{ - m_workAreaParents.clear(); - for (const auto& [monitor, workArea] : m_workAreaMap) - { - if (workArea) - { - m_workAreaParents.insert({ monitor, workArea->UniqueId() }); - } - } -} - -void MonitorWorkAreaMap::Clear() noexcept +void WorkAreaConfiguration::Clear() noexcept { m_workAreaMap.clear(); } diff --git a/src/modules/fancyzones/FancyZonesLib/MonitorWorkAreaMap.h b/src/modules/fancyzones/FancyZonesLib/WorkAreaConfiguration.h similarity index 78% rename from src/modules/fancyzones/FancyZonesLib/MonitorWorkAreaMap.h rename to src/modules/fancyzones/FancyZonesLib/WorkAreaConfiguration.h index 45fabda7091..9530dbdae3a 100644 --- a/src/modules/fancyzones/FancyZonesLib/MonitorWorkAreaMap.h +++ b/src/modules/fancyzones/FancyZonesLib/WorkAreaConfiguration.h @@ -1,15 +1,14 @@ #pragma once -#include "GuidUtils.h" #include class WorkArea; -class MonitorWorkAreaMap +class WorkAreaConfiguration { public: /** - * Get work area based on virtual desktop id and monitor handle. + * Get work area based on monitor handle. * * @param[in] monitor Monitor handle. * @@ -19,7 +18,7 @@ class MonitorWorkAreaMap WorkArea* const GetWorkArea(HMONITOR monitor) const; /** - * Get work area based on virtual desktop id and the current cursor position. + * Get work area based on the current cursor position. * * @returns Object representing single work area, interface to all actions available on work area * (e.g. moving windows through zone layout specified for that work area). @@ -49,13 +48,6 @@ class MonitorWorkAreaMap */ void AddWorkArea(HMONITOR monitor, std::unique_ptr workArea); - FancyZonesDataTypes::WorkAreaId GetParent(HMONITOR monitor) const; - - /** - * Saving current work area IDs as parents for later use. - */ - void SaveParentIds(); - /** * Clear all persisted work area related data. */ @@ -63,5 +55,4 @@ class MonitorWorkAreaMap private: std::unordered_map> m_workAreaMap; - std::unordered_map m_workAreaParents{}; }; diff --git a/src/modules/fancyzones/FancyZonesTests/UnitTests/AppZoneHistoryTests.Spec.cpp b/src/modules/fancyzones/FancyZonesTests/UnitTests/AppZoneHistoryTests.Spec.cpp index c3e954fb00f..c3eea232365 100644 --- a/src/modules/fancyzones/FancyZonesTests/UnitTests/AppZoneHistoryTests.Spec.cpp +++ b/src/modules/fancyzones/FancyZonesTests/UnitTests/AppZoneHistoryTests.Spec.cpp @@ -339,4 +339,159 @@ namespace FancyZonesUnitTests Assert::IsFalse(AppZoneHistory::instance().RemoveAppLastZone(nullptr, workAreaId, layoutId)); } }; + + TEST_CLASS (AppZoneHistorySyncVirtualDesktops) + { + const GUID virtualDesktop1 = FancyZonesUtils::GuidFromString(L"{30387C86-BB15-476D-8683-AF93F6D73E99}").value(); + const GUID virtualDesktop2 = FancyZonesUtils::GuidFromString(L"{65F6343A-868F-47EE-838E-55A178A7FB7A}").value(); + const GUID deletedVirtualDesktop = FancyZonesUtils::GuidFromString(L"{2D9F3E2D-F61D-4618-B35D-85C9B8DFDFD8}").value(); + + FancyZonesDataTypes::WorkAreaId GetWorkAreaID(GUID virtualDesktop) + { + return FancyZonesDataTypes::WorkAreaId{ + .monitorId = { + .deviceId = { .id = L"id", .instanceId = L"id", .number = 1 }, + .serialNumber = L"serial-number" + }, + .virtualDesktopId = virtualDesktop + }; + } + + FancyZonesDataTypes::AppZoneHistoryData GetAppZoneHistoryData(GUID virtualDesktop, const std::wstring& layoutId, const ZoneIndexSet& zones) + { + return FancyZonesDataTypes::AppZoneHistoryData{ + .layoutId = FancyZonesUtils::GuidFromString(layoutId).value(), + .workAreaId = GetWorkAreaID(virtualDesktop), + .zoneIndexSet = zones + }; + }; + + TEST_METHOD_INITIALIZE(Init) + { + AppZoneHistory::instance().LoadData(); + } + + TEST_METHOD_CLEANUP(CleanUp) + { + std::filesystem::remove(AppZoneHistory::AppZoneHistoryFileName()); + } + + TEST_METHOD (SyncVirtualDesktops_SwitchVirtualDesktop) + { + AppZoneHistory::TAppZoneHistoryMap history{}; + const std::wstring app = L"app"; + history.insert({ app, std::vector{ + GetAppZoneHistoryData(virtualDesktop1, L"{147243D0-1111-4225-BCD3-31029FE384FC}", { 0 }), + GetAppZoneHistoryData(virtualDesktop2, L"{EAC1BB3B-13D6-4839-BBF7-58C3E8AB7229}", { 1 }), + } }); + AppZoneHistory::instance().SetAppZoneHistory(history); + + GUID currentVirtualDesktop = virtualDesktop1; + GUID lastUsedVirtualDesktop = virtualDesktop2; + std::optional> virtualDesktopsInRegistry = { { virtualDesktop1, virtualDesktop2 } }; + AppZoneHistory::instance().SyncVirtualDesktops(currentVirtualDesktop, lastUsedVirtualDesktop, virtualDesktopsInRegistry); + + Assert::IsTrue(history.at(app)[0] == AppZoneHistory::instance().GetZoneHistory(app, GetWorkAreaID(virtualDesktop1)).value()); + Assert::IsTrue(history.at(app)[1] == AppZoneHistory::instance().GetZoneHistory(app, GetWorkAreaID(virtualDesktop2)).value()); + } + + TEST_METHOD (SyncVirtualDesktops_CurrentVirtualDesktopDeleted) + { + AppZoneHistory::TAppZoneHistoryMap history{}; + const std::wstring app = L"app"; + history.insert({ app, std::vector{ + GetAppZoneHistoryData(virtualDesktop1, L"{147243D0-1111-4225-BCD3-31029FE384FC}", { 0 }), + GetAppZoneHistoryData(deletedVirtualDesktop, L"{EAC1BB3B-13D6-4839-BBF7-58C3E8AB7229}", { 1 }), + } }); + AppZoneHistory::instance().SetAppZoneHistory(history); + + GUID currentVirtualDesktop = virtualDesktop1; + GUID lastUsedVirtualDesktop = deletedVirtualDesktop; + std::optional> virtualDesktopsInRegistry = { { virtualDesktop1 } }; + AppZoneHistory::instance().SyncVirtualDesktops(currentVirtualDesktop, lastUsedVirtualDesktop, virtualDesktopsInRegistry); + + Assert::IsTrue(history.at(app)[0] == AppZoneHistory::instance().GetZoneHistory(app, GetWorkAreaID(virtualDesktop1)).value()); + Assert::IsFalse(AppZoneHistory::instance().GetZoneHistory(app, GetWorkAreaID(deletedVirtualDesktop)).has_value()); + } + + TEST_METHOD (SyncVirtualDesktops_NotCurrentVirtualDesktopDeleted) + { + AppZoneHistory::TAppZoneHistoryMap history{}; + const std::wstring app = L"app"; + history.insert({ app, std::vector{ + GetAppZoneHistoryData(virtualDesktop1, L"{147243D0-1111-4225-BCD3-31029FE384FC}", { 0 }), + GetAppZoneHistoryData(deletedVirtualDesktop, L"{EAC1BB3B-13D6-4839-BBF7-58C3E8AB7229}", { 1 }), + } }); + AppZoneHistory::instance().SetAppZoneHistory(history); + + GUID currentVirtualDesktop = virtualDesktop1; + GUID lastUsedVirtualDesktop = virtualDesktop1; + std::optional> virtualDesktopsInRegistry = { { virtualDesktop1 } }; + AppZoneHistory::instance().SyncVirtualDesktops(currentVirtualDesktop, lastUsedVirtualDesktop, virtualDesktopsInRegistry); + + Assert::IsTrue(history.at(app)[0] == AppZoneHistory::instance().GetZoneHistory(app, GetWorkAreaID(virtualDesktop1)).value()); + Assert::IsFalse(AppZoneHistory::instance().GetZoneHistory(app, GetWorkAreaID(deletedVirtualDesktop)).has_value()); + } + + TEST_METHOD (SyncVirtualDesktops_AllIdsFromRegistryAreNew) + { + AppZoneHistory::TAppZoneHistoryMap history{}; + const std::wstring app = L"app"; + history.insert({ app, std::vector{ + GetAppZoneHistoryData(deletedVirtualDesktop, L"{147243D0-1111-4225-BCD3-31029FE384FC}", { 0 }), + } }); + AppZoneHistory::instance().SetAppZoneHistory(history); + + GUID currentVirtualDesktop = virtualDesktop1; + GUID lastUsedVirtualDesktop = deletedVirtualDesktop; + std::optional> virtualDesktopsInRegistry = { { virtualDesktop1, virtualDesktop2 } }; + AppZoneHistory::instance().SyncVirtualDesktops(currentVirtualDesktop, lastUsedVirtualDesktop, virtualDesktopsInRegistry); + + auto expected = history.at(app)[0]; + expected.workAreaId.virtualDesktopId = currentVirtualDesktop; + Assert::IsTrue(expected == AppZoneHistory::instance().GetZoneHistory(app, GetWorkAreaID(virtualDesktop1)).value()); + Assert::IsFalse(AppZoneHistory::instance().GetZoneHistory(app, GetWorkAreaID(virtualDesktop2)).has_value()); + Assert::IsFalse(AppZoneHistory::instance().GetZoneHistory(app, GetWorkAreaID(deletedVirtualDesktop)).has_value()); + } + + TEST_METHOD (SyncVirtualDesktop_NoDesktopsInRegistry) + { + AppZoneHistory::TAppZoneHistoryMap history{}; + const std::wstring app = L"app"; + history.insert({ app, std::vector{ + GetAppZoneHistoryData(deletedVirtualDesktop, L"{147243D0-1111-4225-BCD3-31029FE384FC}", { 0 }), + } }); + AppZoneHistory::instance().SetAppZoneHistory(history); + + GUID currentVirtualDesktop = GUID_NULL; + GUID lastUsedVirtualDesktop = deletedVirtualDesktop; + std::optional> virtualDesktopsInRegistry = std::nullopt; + AppZoneHistory::instance().SyncVirtualDesktops(currentVirtualDesktop, lastUsedVirtualDesktop, virtualDesktopsInRegistry); + + auto expected = history.at(app)[0]; + expected.workAreaId.virtualDesktopId = currentVirtualDesktop; + Assert::IsTrue(expected == AppZoneHistory::instance().GetZoneHistory(app, GetWorkAreaID(currentVirtualDesktop)).value()); + Assert::IsFalse(AppZoneHistory::instance().GetZoneHistory(app, GetWorkAreaID(deletedVirtualDesktop)).has_value()); + } + + TEST_METHOD (SyncVirtualDesktop_SwithVirtualDesktopFirstTime) + { + AppZoneHistory::TAppZoneHistoryMap history{}; + const std::wstring app = L"app"; + history.insert({ app, std::vector{ + GetAppZoneHistoryData(GUID_NULL, L"{147243D0-1111-4225-BCD3-31029FE384FC}", { 0 }), + } }); + AppZoneHistory::instance().SetAppZoneHistory(history); + + GUID currentVirtualDesktop = virtualDesktop1; + GUID lastUsedVirtualDesktop = GUID_NULL; + std::optional> virtualDesktopsInRegistry = { { virtualDesktop1, virtualDesktop2 } }; + AppZoneHistory::instance().SyncVirtualDesktops(currentVirtualDesktop, lastUsedVirtualDesktop, virtualDesktopsInRegistry); + + auto expected = history.at(app)[0]; + expected.workAreaId.virtualDesktopId = currentVirtualDesktop; + Assert::IsTrue(expected == AppZoneHistory::instance().GetZoneHistory(app, GetWorkAreaID(currentVirtualDesktop)).value()); + } + }; + } diff --git a/src/modules/fancyzones/FancyZonesTests/UnitTests/AppliedLayoutsTests.Spec.cpp b/src/modules/fancyzones/FancyZonesTests/UnitTests/AppliedLayoutsTests.Spec.cpp index 87f5da5a3c6..ec9256664b2 100644 --- a/src/modules/fancyzones/FancyZonesTests/UnitTests/AppliedLayoutsTests.Spec.cpp +++ b/src/modules/fancyzones/FancyZonesTests/UnitTests/AppliedLayoutsTests.Spec.cpp @@ -14,24 +14,14 @@ namespace FancyZonesUnitTests { TEST_CLASS (AppliedLayoutsUnitTests) { - FancyZonesData& m_fzData = FancyZonesDataInstance(); - std::wstring m_testFolder = L"FancyZonesUnitTests"; - std::wstring m_testFolderPath = PTSettingsHelper::get_module_save_folder_location(m_testFolder); - TEST_METHOD_INITIALIZE(Init) { - m_fzData.SetSettingsModulePath(L"FancyZonesUnitTests"); + AppliedLayouts::instance().LoadData(); } TEST_METHOD_CLEANUP(CleanUp) { - // Move...FromZonesSettings creates all of these files, clean up - std::filesystem::remove(AppliedLayouts::AppliedLayoutsFileName()); - std::filesystem::remove(CustomLayouts::CustomLayoutsFileName()); - std::filesystem::remove(LayoutHotkeys::LayoutHotkeysFileName()); - std::filesystem::remove(LayoutTemplates::LayoutTemplatesFileName()); - std::filesystem::remove_all(m_testFolderPath); - AppliedLayouts::instance().LoadData(); // clean data + std::filesystem::remove(AppliedLayouts::AppliedLayoutsFileName()); } TEST_METHOD (AppliedLayoutsParse) @@ -75,7 +65,7 @@ namespace FancyZonesUnitTests Assert::IsTrue(AppliedLayouts::instance().IsLayoutApplied(id)); } - TEST_METHOD(AppliedLayoutsParseDataWithResolution) + TEST_METHOD (AppliedLayoutsParseDataWithResolution) { // prepare json::JsonObject root{}; @@ -242,143 +232,94 @@ namespace FancyZonesUnitTests Assert::IsTrue(AppliedLayouts::instance().GetAppliedLayoutMap().empty()); } - TEST_METHOD (MoveAppliedLayoutsFromZonesSettings) + TEST_METHOD (Save) { - // prepare - json::JsonObject root{}; - json::JsonArray devicesArray{}, customLayoutsArray{}, templateLayoutsArray{}, quickLayoutKeysArray{}; - - { - json::JsonObject activeZoneset{}; - activeZoneset.SetNamedValue(L"uuid", json::value(L"{ACE817FD-2C51-4E13-903A-84CAB86FD17C}")); - activeZoneset.SetNamedValue(L"type", json::value(FancyZonesDataTypes::TypeToString(FancyZonesDataTypes::ZoneSetLayoutType::Rows))); - - json::JsonObject obj{}; - obj.SetNamedValue(L"device-id", json::value(L"VSC9636#5&37ac4db&0&UID160005_3840_2160_{00000000-0000-0000-0000-000000000000}")); - obj.SetNamedValue(L"active-zoneset", activeZoneset);; - obj.SetNamedValue(L"editor-show-spacing", json::value(true)); - obj.SetNamedValue(L"editor-spacing", json::value(3)); - obj.SetNamedValue(L"editor-zone-count", json::value(4)); - obj.SetNamedValue(L"editor-sensitivity-radius", json::value(22)); - - devicesArray.Append(obj); - } - - root.SetNamedValue(L"devices", devicesArray); - root.SetNamedValue(L"custom-zone-sets", customLayoutsArray); - root.SetNamedValue(L"templates", templateLayoutsArray); - root.SetNamedValue(L"quick-layout-keys", quickLayoutKeysArray); - json::to_file(m_fzData.GetZoneSettingsPath(m_testFolder), root); - - // test - m_fzData.ReplaceZoneSettingsFileFromOlderVersions(); - AppliedLayouts::instance().LoadData(); - Assert::AreEqual((size_t)1, AppliedLayouts::instance().GetAppliedLayoutMap().size()); - - FancyZonesDataTypes::WorkAreaId id{ - .monitorId = { .deviceId = { .id = L"VSC9636", .instanceId = L"5&37ac4db&0&UID160005" }, .serialNumber = L"" }, - .virtualDesktopId = FancyZonesUtils::GuidFromString(L"{00000000-0000-0000-0000-000000000000}").value() + FancyZonesDataTypes::WorkAreaId workAreaId1{ + .monitorId = { + .deviceId = { .id = L"id-1", .instanceId = L"id-1", .number = 1 }, + .serialNumber = L"serial-number-1" + }, + .virtualDesktopId = FancyZonesUtils::GuidFromString(L"{30387C86-BB15-476D-8683-AF93F6D73E99}").value() }; - Assert::IsTrue(AppliedLayouts::instance().GetDeviceLayout(id).has_value()); - } - - TEST_METHOD (MoveAppliedLayoutsFromZonesSettingsNoAppliedLayoutsData) - { - // prepare - json::JsonObject root{}; - json::JsonArray customLayoutsArray{}, templateLayoutsArray{}, quickLayoutKeysArray{}; - root.SetNamedValue(L"custom-zone-sets", customLayoutsArray); - root.SetNamedValue(L"templates", templateLayoutsArray); - root.SetNamedValue(L"quick-layout-keys", quickLayoutKeysArray); - json::to_file(m_fzData.GetZoneSettingsPath(m_testFolder), root); - - // test - m_fzData.ReplaceZoneSettingsFileFromOlderVersions(); - AppliedLayouts::instance().LoadData(); - Assert::IsTrue(AppliedLayouts::instance().GetAppliedLayoutMap().empty()); - } - - TEST_METHOD (MoveAppliedLayoutsFromZonesSettingsNoFile) - { - // test - m_fzData.ReplaceZoneSettingsFileFromOlderVersions(); - AppliedLayouts::instance().LoadData(); - Assert::IsTrue(AppliedLayouts::instance().GetAppliedLayoutMap().empty()); - } - - TEST_METHOD (CloneDeviceInfo) - { - FancyZonesDataTypes::WorkAreaId deviceSrc{ - .monitorId = { .deviceId = { .id = L"Device1", .instanceId = L"" }, .serialNumber = L"" }, - .virtualDesktopId = FancyZonesUtils::GuidFromString(L"{00000000-0000-0000-0000-000000000000}").value() + FancyZonesDataTypes::WorkAreaId workAreaId2{ + .monitorId = { + .deviceId = { .id = L"id-2", .instanceId = L"id-2", .number = 2 }, + .serialNumber = L"serial-number-2" }, + .virtualDesktopId = FancyZonesUtils::GuidFromString(L"{30387C86-BB15-476D-8683-AF93F6D73E99}").value() }; - FancyZonesDataTypes::WorkAreaId deviceDst{ - .monitorId = { .deviceId = { .id = L"Device2", .instanceId = L"" }, .serialNumber = L"" }, - .virtualDesktopId = FancyZonesUtils::GuidFromString(L"{00000000-0000-0000-0000-000000000000}").value() + FancyZonesDataTypes::WorkAreaId workAreaId3{ + .monitorId = { + .deviceId = { .id = L"id-1", .instanceId = L"id-1", .number = 1 }, + .serialNumber = L"serial-number-1" }, + .virtualDesktopId = GUID_NULL + }; + FancyZonesDataTypes::WorkAreaId workAreaId4{ + .monitorId = { + .deviceId = { .id = L"id-2", .instanceId = L"id-2", .number = 2 }, + .serialNumber = L"serial-number-2" }, + .virtualDesktopId = GUID_NULL }; - Assert::IsTrue(AppliedLayouts::instance().ApplyDefaultLayout(deviceSrc)); - Assert::IsTrue(AppliedLayouts::instance().ApplyDefaultLayout(deviceDst)); - - AppliedLayouts::instance().CloneLayout(deviceSrc, deviceDst); + LayoutData layout1{ .uuid = FancyZonesUtils::GuidFromString(L"{D7DBECFA-23FC-4F45-9B56-51CFA9F6ABA2}").value() }; + LayoutData layout2{ .uuid = FancyZonesUtils::GuidFromString(L"{B9EDB48C-EC48-4E82-993F-A15DC1FF09D3}").value() }; + LayoutData layout3{ .uuid = FancyZonesUtils::GuidFromString(L"{94CF0000-7814-4D72-9624-794060FA269C}").value() }; + LayoutData layout4{ .uuid = FancyZonesUtils::GuidFromString(L"{13FA7ADF-1B6C-4FB6-8142-254B77C128E2}").value() }; - auto actualMap = AppliedLayouts::instance().GetAppliedLayoutMap(); - Assert::IsFalse(actualMap.find(deviceSrc) == actualMap.end()); - Assert::IsFalse(actualMap.find(deviceDst) == actualMap.end()); + AppliedLayouts::TAppliedLayoutsMap expected{}; + expected.insert({ workAreaId1, layout1 }); + expected.insert({ workAreaId2, layout2 }); + expected.insert({ workAreaId3, layout3 }); + expected.insert({ workAreaId4, layout4 }); - auto expected = AppliedLayouts::instance().GetDeviceLayout(deviceSrc); - auto actual = AppliedLayouts::instance().GetDeviceLayout(deviceDst); + AppliedLayouts::instance().SetAppliedLayouts(expected); + AppliedLayouts::instance().SaveData(); - Assert::IsTrue(expected.has_value()); - Assert::IsTrue(actual.has_value()); - Assert::IsTrue(expected.value().uuid == actual.value().uuid); + AppliedLayouts::instance().LoadData(); + auto actual = AppliedLayouts::instance().GetAppliedLayoutMap(); + Assert::AreEqual(expected.size(), actual.size()); + Assert::IsTrue(expected.at(workAreaId1) == actual.at(workAreaId1)); + Assert::IsTrue(expected.at(workAreaId2) == actual.at(workAreaId2)); + Assert::IsTrue(expected.at(workAreaId3) == actual.at(workAreaId3)); + Assert::IsTrue(expected.at(workAreaId4) == actual.at(workAreaId4)); } - TEST_METHOD (CloneDeviceInfoIntoUnknownDevice) + TEST_METHOD (CloneDeviceInfo) { FancyZonesDataTypes::WorkAreaId deviceSrc{ .monitorId = { .deviceId = { .id = L"Device1", .instanceId = L"" }, .serialNumber = L"" }, - .virtualDesktopId = FancyZonesUtils::GuidFromString(L"{00000000-0000-0000-0000-000000000000}").value() + .virtualDesktopId = FancyZonesUtils::GuidFromString(L"{EA6B6934-D55F-49F5-A9A5-CFADE21FFFB8}").value() }; FancyZonesDataTypes::WorkAreaId deviceDst{ .monitorId = { .deviceId = { .id = L"Device2", .instanceId = L"" }, .serialNumber = L"" }, - .virtualDesktopId = FancyZonesUtils::GuidFromString(L"{00000000-0000-0000-0000-000000000000}").value() + .virtualDesktopId = FancyZonesUtils::GuidFromString(L"{EF1A8099-7D1E-4738-805A-571B31B02674}").value() }; - Assert::IsTrue(AppliedLayouts::instance().ApplyDefaultLayout(deviceSrc)); - + LayoutData layout { .uuid = FancyZonesUtils::GuidFromString(L"{361F96DD-FD10-4D01-ABAC-CC1C857294DD}").value() }; + Assert::IsTrue(AppliedLayouts::instance().ApplyLayout(deviceSrc, layout)); + AppliedLayouts::instance().CloneLayout(deviceSrc, deviceDst); - auto actualMap = AppliedLayouts::instance().GetAppliedLayoutMap(); - Assert::IsFalse(actualMap.find(deviceSrc) == actualMap.end()); - Assert::IsFalse(actualMap.find(deviceDst) == actualMap.end()); - - auto expected = AppliedLayouts::instance().GetDeviceLayout(deviceSrc); - auto actual = AppliedLayouts::instance().GetDeviceLayout(deviceDst); - - Assert::IsTrue(expected.has_value()); - Assert::IsTrue(actual.has_value()); - Assert::IsTrue(expected.value().uuid == actual.value().uuid); + Assert::IsTrue(layout == AppliedLayouts::instance().GetDeviceLayout(deviceSrc)); + Assert::IsTrue(layout == AppliedLayouts::instance().GetDeviceLayout(deviceDst)); } TEST_METHOD (CloneDeviceInfoFromUnknownDevice) { FancyZonesDataTypes::WorkAreaId deviceSrc{ .monitorId = { .deviceId = { .id = L"Device1", .instanceId = L"" }, .serialNumber = L"" }, - .virtualDesktopId = FancyZonesUtils::GuidFromString(L"{00000000-0000-0000-0000-000000000000}").value() + .virtualDesktopId = FancyZonesUtils::GuidFromString(L"{EA6B6934-D55F-49F5-A9A5-CFADE21FFFB8}").value() }; FancyZonesDataTypes::WorkAreaId deviceDst{ .monitorId = { .deviceId = { .id = L"Device2", .instanceId = L"" }, .serialNumber = L"" }, - .virtualDesktopId = FancyZonesUtils::GuidFromString(L"{00000000-0000-0000-0000-000000000000}").value() + .virtualDesktopId = FancyZonesUtils::GuidFromString(L"{EF1A8099-7D1E-4738-805A-571B31B02674}").value() }; AppliedLayouts::instance().LoadData(); - Assert::IsTrue(AppliedLayouts::instance().ApplyDefaultLayout(deviceDst)); Assert::IsFalse(AppliedLayouts::instance().CloneLayout(deviceSrc, deviceDst)); Assert::IsFalse(AppliedLayouts::instance().GetDeviceLayout(deviceSrc).has_value()); - Assert::IsTrue(AppliedLayouts::instance().GetDeviceLayout(deviceDst).has_value()); + Assert::IsFalse(AppliedLayouts::instance().GetDeviceLayout(deviceDst).has_value()); } TEST_METHOD (CloneDeviceInfoNullVirtualDesktopId) @@ -389,35 +330,25 @@ namespace FancyZonesUnitTests }; FancyZonesDataTypes::WorkAreaId deviceDst{ .monitorId = { .deviceId = { .id = L"Device2", .instanceId = L"" }, .serialNumber = L"" }, - .virtualDesktopId = FancyZonesUtils::GuidFromString(L"{00000000-0000-0000-0000-000000000000}").value() + .virtualDesktopId = FancyZonesUtils::GuidFromString(L"{EF1A8099-7D1E-4738-805A-571B31B02674}").value() }; - Assert::IsTrue(AppliedLayouts::instance().ApplyDefaultLayout(deviceSrc)); - Assert::IsTrue(AppliedLayouts::instance().ApplyDefaultLayout(deviceDst)); - + LayoutData layout{ .uuid = FancyZonesUtils::GuidFromString(L"{361F96DD-FD10-4D01-ABAC-CC1C857294DD}").value() }; + Assert::IsTrue(AppliedLayouts::instance().ApplyLayout(deviceSrc, layout)); + AppliedLayouts::instance().CloneLayout(deviceSrc, deviceDst); - auto actualMap = AppliedLayouts::instance().GetAppliedLayoutMap(); - Assert::IsFalse(actualMap.find(deviceSrc) == actualMap.end()); - Assert::IsFalse(actualMap.find(deviceDst) == actualMap.end()); - - auto expected = AppliedLayouts::instance().GetDeviceLayout(deviceSrc); - auto actual = AppliedLayouts::instance().GetDeviceLayout(deviceDst); - - Assert::IsTrue(expected.has_value()); - Assert::IsTrue(actual.has_value()); - Assert::IsTrue(expected.value().uuid == actual.value().uuid); + Assert::IsTrue(layout == AppliedLayouts::instance().GetDeviceLayout(deviceSrc)); + Assert::IsTrue(layout == AppliedLayouts::instance().GetDeviceLayout(deviceDst)); } TEST_METHOD (ApplyLayout) { - // prepare - FancyZonesDataTypes::WorkAreaId deviceId { + FancyZonesDataTypes::WorkAreaId workAreaId { .monitorId = { .deviceId = { .id = L"DELA026", .instanceId = L"5&10a58c63&0&UID16777488" }, .serialNumber = L"" }, .virtualDesktopId = FancyZonesUtils::GuidFromString(L"{61FA9FC0-26A6-4B37-A834-491C148DFC57}").value() }; - // test LayoutData expectedLayout { .uuid = FancyZonesUtils::GuidFromString(L"{33A2B101-06E0-437B-A61E-CDBECF502906}").value(), .type = FancyZonesDataTypes::ZoneSetLayoutType::Focus, @@ -427,47 +358,30 @@ namespace FancyZonesUnitTests .sensitivityRadius = 30 }; - AppliedLayouts::instance().ApplyLayout(deviceId, expectedLayout); - - Assert::IsFalse(AppliedLayouts::instance().GetAppliedLayoutMap().empty()); - Assert::IsTrue(AppliedLayouts::instance().GetDeviceLayout(deviceId).has_value()); + AppliedLayouts::instance().ApplyLayout(workAreaId, expectedLayout); - auto actual = AppliedLayouts::instance().GetAppliedLayoutMap().find(deviceId)->second; - Assert::IsTrue(expectedLayout.type == actual.type); - Assert::AreEqual(expectedLayout.showSpacing, actual.showSpacing); - Assert::AreEqual(expectedLayout.spacing, actual.spacing); - Assert::AreEqual(expectedLayout.zoneCount, actual.zoneCount); - Assert::AreEqual(expectedLayout.sensitivityRadius, actual.sensitivityRadius); + Assert::IsTrue(AppliedLayouts::instance().GetDeviceLayout(workAreaId).has_value()); + Assert::IsTrue(expectedLayout == AppliedLayouts::instance().GetAppliedLayoutMap().find(workAreaId)->second); } TEST_METHOD (ApplyLayoutReplace) { // prepare - FancyZonesDataTypes::WorkAreaId deviceId{ + FancyZonesDataTypes::WorkAreaId workAreaId{ .monitorId = { .deviceId = { .id = L"DELA026", .instanceId = L"5&10a58c63&0&UID16777488" }, .serialNumber = L"" }, .virtualDesktopId = FancyZonesUtils::GuidFromString(L"{61FA9FC0-26A6-4B37-A834-491C148DFC57}").value() }; + + LayoutData layout{ + .uuid = FancyZonesUtils::GuidFromString(L"{ACE817FD-2C51-4E13-903A-84CAB86FD17C}").value(), + .type = FancyZonesDataTypes::ZoneSetLayoutType::Rows, + .showSpacing = true, + .spacing = 3, + .zoneCount = 4, + .sensitivityRadius = 22 + }; - json::JsonObject root{}; - json::JsonArray layoutsArray{}; - { - json::JsonObject layout{}; - layout.SetNamedValue(NonLocalizable::AppliedLayoutsIds::UuidID, json::value(L"{ACE817FD-2C51-4E13-903A-84CAB86FD17C}")); - layout.SetNamedValue(NonLocalizable::AppliedLayoutsIds::TypeID, json::value(FancyZonesDataTypes::TypeToString(FancyZonesDataTypes::ZoneSetLayoutType::Rows))); - layout.SetNamedValue(NonLocalizable::AppliedLayoutsIds::ShowSpacingID, json::value(true)); - layout.SetNamedValue(NonLocalizable::AppliedLayoutsIds::SpacingID, json::value(3)); - layout.SetNamedValue(NonLocalizable::AppliedLayoutsIds::ZoneCountID, json::value(4)); - layout.SetNamedValue(NonLocalizable::AppliedLayoutsIds::SensitivityRadiusID, json::value(22)); - - json::JsonObject obj{}; - obj.SetNamedValue(NonLocalizable::AppliedLayoutsIds::DeviceIdID, json::value(L"DELA026#5&10a58c63&0&UID16777488_2194_1234_{61FA9FC0-26A6-4B37-A834-491C148DFC57}")); - obj.SetNamedValue(NonLocalizable::AppliedLayoutsIds::AppliedLayoutID, layout); - - layoutsArray.Append(obj); - } - root.SetNamedValue(NonLocalizable::AppliedLayoutsIds::AppliedLayoutsArrayID, layoutsArray); - json::to_file(AppliedLayouts::AppliedLayoutsFileName(), root); - AppliedLayouts::instance().LoadData(); + AppliedLayouts::instance().SetAppliedLayouts({ {workAreaId, layout} }); // test LayoutData expectedLayout{ @@ -479,18 +393,8 @@ namespace FancyZonesUnitTests .sensitivityRadius = 30 }; - AppliedLayouts::instance().ApplyLayout(deviceId, expectedLayout); - - Assert::AreEqual((size_t)1, AppliedLayouts::instance().GetAppliedLayoutMap().size()); - Assert::IsTrue(AppliedLayouts::instance().GetDeviceLayout(deviceId).has_value()); - - auto actual = AppliedLayouts::instance().GetAppliedLayoutMap().find(deviceId)->second; - Assert::AreEqual(FancyZonesUtils::GuidToString(expectedLayout.uuid).value().c_str(), FancyZonesUtils::GuidToString(actual.uuid).value().c_str()); - Assert::IsTrue(expectedLayout.type == actual.type); - Assert::AreEqual(expectedLayout.showSpacing, actual.showSpacing); - Assert::AreEqual(expectedLayout.spacing, actual.spacing); - Assert::AreEqual(expectedLayout.zoneCount, actual.zoneCount); - Assert::AreEqual(expectedLayout.sensitivityRadius, actual.sensitivityRadius); + AppliedLayouts::instance().ApplyLayout(workAreaId, expectedLayout); + Assert::IsTrue(expectedLayout == AppliedLayouts::instance().GetDeviceLayout(workAreaId)); } TEST_METHOD (ApplyDefaultLayout) @@ -553,4 +457,245 @@ namespace FancyZonesUnitTests Assert::IsFalse(AppliedLayouts::instance().IsLayoutApplied(id2)); } }; + + TEST_CLASS (AppliedLayoutsSyncVirtualDesktops) + { + const GUID virtualDesktop1 = FancyZonesUtils::GuidFromString(L"{30387C86-BB15-476D-8683-AF93F6D73E99}").value(); + const GUID virtualDesktop2 = FancyZonesUtils::GuidFromString(L"{65F6343A-868F-47EE-838E-55A178A7FB7A}").value(); + const GUID deletedVirtualDesktop = FancyZonesUtils::GuidFromString(L"{2D9F3E2D-F61D-4618-B35D-85C9B8DFDFD8}").value(); + + LayoutData layout1{ .uuid = FancyZonesUtils::GuidFromString(L"{D7DBECFA-23FC-4F45-9B56-51CFA9F6ABA2}").value() }; + LayoutData layout2{ .uuid = FancyZonesUtils::GuidFromString(L"{B9EDB48C-EC48-4E82-993F-A15DC1FF09D3}").value() }; + LayoutData layout3{ .uuid = FancyZonesUtils::GuidFromString(L"{94CF0000-7814-4D72-9624-794060FA269C}").value() }; + LayoutData layout4{ .uuid = FancyZonesUtils::GuidFromString(L"{13FA7ADF-1B6C-4FB6-8142-254B77C128E2}").value() }; + + FancyZonesDataTypes::WorkAreaId GetWorkAreaID(int number, GUID virtualDesktop) + { + return FancyZonesDataTypes::WorkAreaId{ + .monitorId = { + .deviceId = { + .id = std::wstring(L"id-") + std::to_wstring(number), + .instanceId = std::wstring(L"id-") + std::to_wstring(number), + .number = number + }, + .serialNumber = std::wstring(L"serial-number-") + std::to_wstring(number) + }, + .virtualDesktopId = virtualDesktop + }; + } + + TEST_METHOD_INITIALIZE(Init) + { + AppliedLayouts::instance().LoadData(); + } + + TEST_METHOD_CLEANUP(CleanUp) + { + std::filesystem::remove(AppliedLayouts::AppliedLayoutsFileName()); + } + + TEST_METHOD(SyncVirtualDesktops_SwitchVirtualDesktop) + { + AppliedLayouts::TAppliedLayoutsMap layouts{}; + layouts.insert({ GetWorkAreaID(1, virtualDesktop1), layout1 }); + layouts.insert({ GetWorkAreaID(2, virtualDesktop1), layout2 }); + layouts.insert({ GetWorkAreaID(1, virtualDesktop2), layout3 }); + layouts.insert({ GetWorkAreaID(2, virtualDesktop2), layout4 }); + AppliedLayouts::instance().SetAppliedLayouts(layouts); + + GUID currentVirtualDesktop = virtualDesktop1; + GUID lastUsedVirtualDesktop = virtualDesktop2; + std::optional> virtualDesktopsInRegistry = { { virtualDesktop1, virtualDesktop2 } }; + AppliedLayouts::instance().SyncVirtualDesktops(currentVirtualDesktop, lastUsedVirtualDesktop, virtualDesktopsInRegistry); + + Assert::IsTrue(layout1 == AppliedLayouts::instance().GetDeviceLayout(GetWorkAreaID(1, virtualDesktop1))); + Assert::IsTrue(layout2 == AppliedLayouts::instance().GetDeviceLayout(GetWorkAreaID(2, virtualDesktop1))); + Assert::IsTrue(layout3 == AppliedLayouts::instance().GetDeviceLayout(GetWorkAreaID(1, virtualDesktop2))); + Assert::IsTrue(layout4 == AppliedLayouts::instance().GetDeviceLayout(GetWorkAreaID(2, virtualDesktop2))); + } + + TEST_METHOD (SyncVirtualDesktops_CurrentVirtualDesktopDeleted) + { + AppliedLayouts::TAppliedLayoutsMap layouts{}; + layouts.insert({ GetWorkAreaID(1, virtualDesktop1), layout1 }); + layouts.insert({ GetWorkAreaID(2, virtualDesktop1), layout2 }); + layouts.insert({ GetWorkAreaID(1, deletedVirtualDesktop), layout3 }); + layouts.insert({ GetWorkAreaID(2, deletedVirtualDesktop), layout4 }); + AppliedLayouts::instance().SetAppliedLayouts(layouts); + + GUID currentVirtualDesktop = virtualDesktop1; + GUID lastUsedVirtualDesktop = deletedVirtualDesktop; + std::optional> virtualDesktopsInRegistry = { { virtualDesktop1 } }; + AppliedLayouts::instance().SyncVirtualDesktops(currentVirtualDesktop, lastUsedVirtualDesktop, virtualDesktopsInRegistry); + + Assert::IsTrue(layout1 == AppliedLayouts::instance().GetDeviceLayout(GetWorkAreaID(1, virtualDesktop1))); + Assert::IsTrue(layout2 == AppliedLayouts::instance().GetDeviceLayout(GetWorkAreaID(2, virtualDesktop1))); + Assert::IsFalse(AppliedLayouts::instance().GetDeviceLayout(GetWorkAreaID(1, deletedVirtualDesktop)).has_value()); + Assert::IsFalse(AppliedLayouts::instance().GetDeviceLayout(GetWorkAreaID(2, deletedVirtualDesktop)).has_value()); + } + + TEST_METHOD (SyncVirtualDesktops_NotCurrentVirtualDesktopDeleted) + { + AppliedLayouts::TAppliedLayoutsMap layouts{}; + layouts.insert({ GetWorkAreaID(1, virtualDesktop1), layout1 }); + layouts.insert({ GetWorkAreaID(2, virtualDesktop1), layout2 }); + layouts.insert({ GetWorkAreaID(1, deletedVirtualDesktop), layout3 }); + layouts.insert({ GetWorkAreaID(2, deletedVirtualDesktop), layout4 }); + AppliedLayouts::instance().SetAppliedLayouts(layouts); + + GUID currentVirtualDesktop = virtualDesktop1; + GUID lastUsedVirtualDesktop = virtualDesktop1; + std::optional> virtualDesktopsInRegistry = { { virtualDesktop1 } }; + AppliedLayouts::instance().SyncVirtualDesktops(currentVirtualDesktop, lastUsedVirtualDesktop, virtualDesktopsInRegistry); + + Assert::IsTrue(layout1 == AppliedLayouts::instance().GetDeviceLayout(GetWorkAreaID(1, virtualDesktop1))); + Assert::IsTrue(layout2 == AppliedLayouts::instance().GetDeviceLayout(GetWorkAreaID(2, virtualDesktop1))); + Assert::IsFalse(AppliedLayouts::instance().GetDeviceLayout(GetWorkAreaID(1, deletedVirtualDesktop)).has_value()); + Assert::IsFalse(AppliedLayouts::instance().GetDeviceLayout(GetWorkAreaID(2, deletedVirtualDesktop)).has_value()); + } + + TEST_METHOD (SyncVirtualDesktops_AllIdsFromRegistryAreNew) + { + AppliedLayouts::TAppliedLayoutsMap layouts{}; + layouts.insert({ GetWorkAreaID(1, deletedVirtualDesktop), layout1 }); + layouts.insert({ GetWorkAreaID(2, deletedVirtualDesktop), layout2 }); + AppliedLayouts::instance().SetAppliedLayouts(layouts); + + GUID currentVirtualDesktop = virtualDesktop1; + GUID lastUsedVirtualDesktop = deletedVirtualDesktop; + std::optional> virtualDesktopsInRegistry = { { virtualDesktop1, virtualDesktop2 } }; + AppliedLayouts::instance().SyncVirtualDesktops(currentVirtualDesktop, lastUsedVirtualDesktop, virtualDesktopsInRegistry); + + Assert::IsTrue(layout1 == AppliedLayouts::instance().GetDeviceLayout(GetWorkAreaID(1, virtualDesktop1))); + Assert::IsTrue(layout2 == AppliedLayouts::instance().GetDeviceLayout(GetWorkAreaID(2, virtualDesktop1))); + Assert::IsFalse(AppliedLayouts::instance().GetDeviceLayout(GetWorkAreaID(1, virtualDesktop2)).has_value()); + Assert::IsFalse(AppliedLayouts::instance().GetDeviceLayout(GetWorkAreaID(2, virtualDesktop2)).has_value()); + Assert::IsFalse(AppliedLayouts::instance().GetDeviceLayout(GetWorkAreaID(1, deletedVirtualDesktop)).has_value()); + Assert::IsFalse(AppliedLayouts::instance().GetDeviceLayout(GetWorkAreaID(2, deletedVirtualDesktop)).has_value()); + } + + TEST_METHOD (SyncVirtualDesktop_NoDesktopsInRegistry) + { + AppliedLayouts::TAppliedLayoutsMap layouts{}; + layouts.insert({ GetWorkAreaID(1, deletedVirtualDesktop), layout1 }); + layouts.insert({ GetWorkAreaID(2, deletedVirtualDesktop), layout2 }); + AppliedLayouts::instance().SetAppliedLayouts(layouts); + + GUID currentVirtualDesktop = GUID_NULL; + GUID lastUsedVirtualDesktop = deletedVirtualDesktop; + std::optional> virtualDesktopsInRegistry = std::nullopt; + AppliedLayouts::instance().SyncVirtualDesktops(currentVirtualDesktop, lastUsedVirtualDesktop, virtualDesktopsInRegistry); + + Assert::IsTrue(layout1 == AppliedLayouts::instance().GetDeviceLayout(GetWorkAreaID(1, GUID_NULL))); + Assert::IsTrue(layout2 == AppliedLayouts::instance().GetDeviceLayout(GetWorkAreaID(2, GUID_NULL))); + Assert::IsFalse(AppliedLayouts::instance().GetDeviceLayout(GetWorkAreaID(1, deletedVirtualDesktop)).has_value()); + Assert::IsFalse(AppliedLayouts::instance().GetDeviceLayout(GetWorkAreaID(2, deletedVirtualDesktop)).has_value()); + } + + TEST_METHOD(SyncVirtualDesktops_SwithVirtualDesktopFirstTime) + { + AppliedLayouts::TAppliedLayoutsMap layouts{}; + layouts.insert({ GetWorkAreaID(1, GUID_NULL), layout1 }); + layouts.insert({ GetWorkAreaID(2, GUID_NULL), layout2 }); + AppliedLayouts::instance().SetAppliedLayouts(layouts); + + GUID currentVirtualDesktop = virtualDesktop2; + GUID lastUsedVirtualDesktop = GUID_NULL; + std::optional> virtualDesktopsInRegistry = { { virtualDesktop1, virtualDesktop2 } }; + AppliedLayouts::instance().SyncVirtualDesktops(currentVirtualDesktop, lastUsedVirtualDesktop, virtualDesktopsInRegistry); + + Assert::IsTrue(layout1 == AppliedLayouts::instance().GetDeviceLayout(GetWorkAreaID(1, virtualDesktop1))); + Assert::IsTrue(layout2 == AppliedLayouts::instance().GetDeviceLayout(GetWorkAreaID(2, virtualDesktop1))); + Assert::IsTrue(layout1 == AppliedLayouts::instance().GetDeviceLayout(GetWorkAreaID(1, virtualDesktop2))); + Assert::IsTrue(layout2 == AppliedLayouts::instance().GetDeviceLayout(GetWorkAreaID(2, virtualDesktop2))); + } + }; + + TEST_CLASS (AppliedLayoutsFromOutdatedFileMappingUnitTests) + { + FancyZonesData& m_fzData = FancyZonesDataInstance(); + std::wstring m_testFolder = L"FancyZonesUnitTests"; + std::wstring m_testFolderPath = PTSettingsHelper::get_module_save_folder_location(m_testFolder); + + TEST_METHOD_INITIALIZE(Init) + { + m_fzData.SetSettingsModulePath(m_testFolder); + } + + TEST_METHOD_CLEANUP(CleanUp) + { + // MoveAppliedLayoutsFromZonesSettings creates all of these files, clean up + std::filesystem::remove(AppliedLayouts::AppliedLayoutsFileName()); + std::filesystem::remove(CustomLayouts::CustomLayoutsFileName()); + std::filesystem::remove(LayoutHotkeys::LayoutHotkeysFileName()); + std::filesystem::remove(LayoutTemplates::LayoutTemplatesFileName()); + std::filesystem::remove_all(m_testFolderPath); + AppliedLayouts::instance().LoadData(); // clean data + } + + TEST_METHOD (MoveAppliedLayoutsFromZonesSettings) + { + // prepare + json::JsonObject root{}; + json::JsonArray devicesArray{}, customLayoutsArray{}, templateLayoutsArray{}, quickLayoutKeysArray{}; + + { + json::JsonObject activeZoneset{}; + activeZoneset.SetNamedValue(L"uuid", json::value(L"{ACE817FD-2C51-4E13-903A-84CAB86FD17C}")); + activeZoneset.SetNamedValue(L"type", json::value(FancyZonesDataTypes::TypeToString(FancyZonesDataTypes::ZoneSetLayoutType::Rows))); + + json::JsonObject obj{}; + obj.SetNamedValue(L"device-id", json::value(L"VSC9636#5&37ac4db&0&UID160005_3840_2160_{00000000-0000-0000-0000-000000000000}")); + obj.SetNamedValue(L"active-zoneset", activeZoneset); + + obj.SetNamedValue(L"editor-show-spacing", json::value(true)); + obj.SetNamedValue(L"editor-spacing", json::value(3)); + obj.SetNamedValue(L"editor-zone-count", json::value(4)); + obj.SetNamedValue(L"editor-sensitivity-radius", json::value(22)); + + devicesArray.Append(obj); + } + + root.SetNamedValue(L"devices", devicesArray); + root.SetNamedValue(L"custom-zone-sets", customLayoutsArray); + root.SetNamedValue(L"templates", templateLayoutsArray); + root.SetNamedValue(L"quick-layout-keys", quickLayoutKeysArray); + json::to_file(m_fzData.GetZoneSettingsPath(m_testFolder), root); + + // test + m_fzData.ReplaceZoneSettingsFileFromOlderVersions(); + AppliedLayouts::instance().LoadData(); + Assert::AreEqual((size_t)1, AppliedLayouts::instance().GetAppliedLayoutMap().size()); + + FancyZonesDataTypes::WorkAreaId id{ + .monitorId = { .deviceId = { .id = L"VSC9636", .instanceId = L"5&37ac4db&0&UID160005" }, .serialNumber = L"" }, + .virtualDesktopId = FancyZonesUtils::GuidFromString(L"{00000000-0000-0000-0000-000000000000}").value() + }; + Assert::IsTrue(AppliedLayouts::instance().GetDeviceLayout(id).has_value()); + } + + TEST_METHOD (MoveAppliedLayoutsFromZonesSettingsNoAppliedLayoutsData) + { + // prepare + json::JsonObject root{}; + json::JsonArray customLayoutsArray{}, templateLayoutsArray{}, quickLayoutKeysArray{}; + root.SetNamedValue(L"custom-zone-sets", customLayoutsArray); + root.SetNamedValue(L"templates", templateLayoutsArray); + root.SetNamedValue(L"quick-layout-keys", quickLayoutKeysArray); + json::to_file(m_fzData.GetZoneSettingsPath(m_testFolder), root); + + // test + m_fzData.ReplaceZoneSettingsFileFromOlderVersions(); + AppliedLayouts::instance().LoadData(); + Assert::IsTrue(AppliedLayouts::instance().GetAppliedLayoutMap().empty()); + } + + TEST_METHOD (MoveAppliedLayoutsFromZonesSettingsNoFile) + { + // test + m_fzData.ReplaceZoneSettingsFileFromOlderVersions(); + AppliedLayouts::instance().LoadData(); + Assert::IsTrue(AppliedLayouts::instance().GetAppliedLayoutMap().empty()); + } + }; } \ No newline at end of file diff --git a/src/modules/fancyzones/FancyZonesTests/UnitTests/WorkAreaIdTests.Spec.cpp b/src/modules/fancyzones/FancyZonesTests/UnitTests/WorkAreaIdTests.Spec.cpp index db5c660db0b..5b024b718d2 100644 --- a/src/modules/fancyzones/FancyZonesTests/UnitTests/WorkAreaIdTests.Spec.cpp +++ b/src/modules/fancyzones/FancyZonesTests/UnitTests/WorkAreaIdTests.Spec.cpp @@ -43,11 +43,11 @@ namespace FancyZonesUnitTests Assert::IsFalse(id1 == id2); } - TEST_METHOD (VirtualDesktopNull) + TEST_METHOD (VirtualDesktopDifferent) { FancyZonesDataTypes::WorkAreaId id1{ .monitorId = { .deviceId = { .id = L"device", .instanceId = L"instance-id" }, .serialNumber = L"serial-number" }, - .virtualDesktopId = GUID_NULL + .virtualDesktopId = FancyZonesUtils::GuidFromString(L"{F21F6F29-76FD-4FC1-8970-17AB8AD64847}").value() }; FancyZonesDataTypes::WorkAreaId id2{ @@ -55,14 +55,14 @@ namespace FancyZonesUnitTests .virtualDesktopId = FancyZonesUtils::GuidFromString(L"{E21F6F29-76FD-4FC1-8970-17AB8AD64847}").value() }; - Assert::IsTrue(id1 == id2); + Assert::IsFalse(id1 == id2); } - TEST_METHOD (VirtualDesktopDifferent) + TEST_METHOD (VirtualDesktopNull) { FancyZonesDataTypes::WorkAreaId id1{ .monitorId = { .deviceId = { .id = L"device", .instanceId = L"instance-id" }, .serialNumber = L"serial-number" }, - .virtualDesktopId = FancyZonesUtils::GuidFromString(L"{F21F6F29-76FD-4FC1-8970-17AB8AD64847}").value() + .virtualDesktopId = GUID_NULL }; FancyZonesDataTypes::WorkAreaId id2{ diff --git a/src/modules/imageresizer/codeAnalysis/GlobalSuppressions.cs b/src/modules/imageresizer/codeAnalysis/GlobalSuppressions.cs index 421fd16329f..0c41e798397 100644 --- a/src/modules/imageresizer/codeAnalysis/GlobalSuppressions.cs +++ b/src/modules/imageresizer/codeAnalysis/GlobalSuppressions.cs @@ -13,10 +13,10 @@ [assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1101:PrefixLocalCallsWithThis", Justification = "We follow the C# Core Coding Style which avoids using `this` unless absolutely necessary.")] [assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1200:UsingDirectivesMustBePlacedWithinNamespace", Justification = "We follow the C# Core Coding Style which puts using statements outside the namespace.")] -[assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1201:ElementsMustAppearInTheCorrectOrder", Justification = "It is not a priority and have hight impact in code changes.")] -[assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1202:ElementsMustBeOrderedByAccess", Justification = "It is not a priority and have hight impact in code changes.")] -[assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1203:ConstantsMustAppearBeforeFields", Justification = "It is not a priority and have hight impact in code changes.")] -[assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1204:StaticElementsMustAppearBeforeInstanceElements", Justification = "It is not a priority and have hight impact in code changes.")] +[assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1201:ElementsMustAppearInTheCorrectOrder", Justification = "It is not a priority and has a high impact in code changes.")] +[assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1202:ElementsMustBeOrderedByAccess", Justification = "It is not a priority and has a high impact in code changes.")] +[assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1203:ConstantsMustAppearBeforeFields", Justification = "It is not a priority and has a high impact in code changes.")] +[assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1204:StaticElementsMustAppearBeforeInstanceElements", Justification = "It is not a priority and has a high impact in code changes.")] [assembly: SuppressMessage("StyleCop.CSharp.NamingRules", "SA1309:FieldNamesMustNotBeginWithUnderscore", Justification = "We follow the C# Core Coding Style which uses underscores as prefixes rather than using `this.`.")] diff --git a/src/modules/imageresizer/ui/Extensions/BitmapMetadataExtension.cs b/src/modules/imageresizer/ui/Extensions/BitmapMetadataExtension.cs index 6ed051d4f36..f5f74498344 100644 --- a/src/modules/imageresizer/ui/Extensions/BitmapMetadataExtension.cs +++ b/src/modules/imageresizer/ui/Extensions/BitmapMetadataExtension.cs @@ -183,7 +183,7 @@ object GetQueryWithPreCheck(BitmapMetadata metadata, string query) /// Prints all metadata to debug console /// /// - /// Intented for debug only!!! + /// Intended for debug only!!! /// public static void PrintsAllMetadataToDebugOutput(this BitmapMetadata metadata) { @@ -205,7 +205,7 @@ public static void PrintsAllMetadataToDebugOutput(this BitmapMetadata metadata) /// Iterates recursively through all metadata /// /// - /// Intented for debug only!!! + /// Intended for debug only!!! /// public static List<(string MetadataPath, object Value)> GetListOfMetadataForDebug(this BitmapMetadata metadata) { diff --git a/src/modules/launcher/Plugins/Microsoft.Plugin.Shell/Main.cs b/src/modules/launcher/Plugins/Microsoft.Plugin.Shell/Main.cs index d458b7996a4..dfbdbc8450b 100644 --- a/src/modules/launcher/Plugins/Microsoft.Plugin.Shell/Main.cs +++ b/src/modules/launcher/Plugins/Microsoft.Plugin.Shell/Main.cs @@ -52,7 +52,7 @@ public class Main : IPlugin, IPluginI18n, ISettingProvider, IContextMenu, ISavab { Key = "ShellCommandExecution", DisplayLabel = Resources.wox_shell_command_execution, - SelectionTypeValue = (int)PluginAdditionalOption.SelectionType.Combobox, + PluginOptionType = PluginAdditionalOption.AdditionalOptionType.Combobox, ComboBoxOptions = new List { Resources.run_command_in_command_prompt, @@ -60,7 +60,7 @@ public class Main : IPlugin, IPluginI18n, ISettingProvider, IContextMenu, ISavab Resources.find_executable_file_and_run_it, Resources.run_command_in_windows_terminal, }, - Option = (int)_settings.Shell, + ComboBoxValue = (int)_settings.Shell, }, }; @@ -442,7 +442,7 @@ public void UpdateSettings(PowerLauncherPluginSettings settings) _settings.LeaveShellOpen = leaveShellOpen; var optionShell = settings.AdditionalOptions.FirstOrDefault(x => x.Key == "ShellCommandExecution"); - shellOption = optionShell?.Option ?? shellOption; + shellOption = optionShell?.ComboBoxValue ?? shellOption; _settings.Shell = (ExecutionShell)shellOption; } diff --git a/src/modules/launcher/Plugins/Microsoft.Plugin.WindowWalker/Components/ResultHelper.cs b/src/modules/launcher/Plugins/Microsoft.Plugin.WindowWalker/Components/ResultHelper.cs index d9ee193e8aa..b7b4484dfc9 100644 --- a/src/modules/launcher/Plugins/Microsoft.Plugin.WindowWalker/Components/ResultHelper.cs +++ b/src/modules/launcher/Plugins/Microsoft.Plugin.WindowWalker/Components/ResultHelper.cs @@ -43,7 +43,7 @@ internal static List GetResultList(List searchControllerRe return true; }, - // For debugging you can set the second parameter to true to see more informations. + // For debugging you can set the second parameter to true to see more information. ToolTipData = GetToolTip(x.Result, false), }); } diff --git a/src/modules/launcher/Plugins/Microsoft.Plugin.WindowWalker/Properties/Resources.Designer.cs b/src/modules/launcher/Plugins/Microsoft.Plugin.WindowWalker/Properties/Resources.Designer.cs index d7ff3a4c36b..024368b1584 100644 --- a/src/modules/launcher/Plugins/Microsoft.Plugin.WindowWalker/Properties/Resources.Designer.cs +++ b/src/modules/launcher/Plugins/Microsoft.Plugin.WindowWalker/Properties/Resources.Designer.cs @@ -79,7 +79,7 @@ public static string wox_plugin_windowwalker_Desktop { } /// - /// Looks up a localized string similar to Folder windows doesn't run in separate processes. (Klick to open Explorer properties.). + /// Looks up a localized string similar to Folder windows doesn't run in separate processes. (Click to open Explorer properties.). /// public static string wox_plugin_windowwalker_ExplorerInfoSubTitle { get { diff --git a/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.Registry/Helper/ContextMenuHelper.cs b/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.Registry/Helper/ContextMenuHelper.cs index ad959441e10..39858ecea1b 100644 --- a/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.Registry/Helper/ContextMenuHelper.cs +++ b/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.Registry/Helper/ContextMenuHelper.cs @@ -22,7 +22,7 @@ internal static class ContextMenuHelper /// Return a list with all context menu entries for the given /// Symbols taken from /// - /// The result for the context menu entires + /// The result for the context menu entries /// The name of the this assembly /// A list with context menu entries internal static List GetContextMenu(Result result, string assemblyName) diff --git a/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.System/Components/NetworkConnectionProperties.cs b/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.System/Components/NetworkConnectionProperties.cs index d806efdb7a0..aaba05682ef 100644 --- a/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.System/Components/NetworkConnectionProperties.cs +++ b/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.System/Components/NetworkConnectionProperties.cs @@ -15,7 +15,7 @@ namespace Microsoft.PowerToys.Run.Plugin.System.Components { /// - /// This class represents the informations for a network connection/interface + /// This class represents the information for a network connection/interface /// internal sealed class NetworkConnectionProperties { diff --git a/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.System/Main.cs b/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.System/Main.cs index 93c67587666..0591f4d1a13 100644 --- a/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.System/Main.cs +++ b/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.System/Main.cs @@ -124,7 +124,7 @@ public List Query(Query query) // { // results.Add(new Result() // { - // Title = "Getting network informations. Please wait ...", + // Title = "Getting network information. Please wait ...", // IcoPath = $"Images\\networkAdapter.{IconTheme}.png", // Score = StringMatcher.FuzzySearch("address", "ip address").Score, // }); diff --git a/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.TimeDate/Components/AvailableResultsList.cs b/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.TimeDate/Components/AvailableResultsList.cs index e051d895687..2fc553f8220 100644 --- a/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.TimeDate/Components/AvailableResultsList.cs +++ b/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.TimeDate/Components/AvailableResultsList.cs @@ -59,7 +59,7 @@ internal static List GetList(bool isKeywordSearch, bool? timeLo if (isKeywordSearch || !TimeDateSettings.Instance.OnlyDateTimeNowGlobal) { - // We use long instead of int for unix time stamp because int ist to small after 03:14:07 UTC 2038-01-19 + // We use long instead of int for unix time stamp because int is too small after 03:14:07 UTC 2038-01-19 long unixTimestamp = (long)dateTimeNowUtc.Subtract(new DateTime(1970, 1, 1)).TotalSeconds; int weekOfYear = calendar.GetWeekOfYear(dateTimeNow, DateTimeFormatInfo.CurrentInfo.CalendarWeekRule, DateTimeFormatInfo.CurrentInfo.FirstDayOfWeek); string era = DateTimeFormatInfo.CurrentInfo.GetEraName(calendar.GetEra(dateTimeNow)); diff --git a/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.TimeDate/Components/TimeAndDateHelper.cs b/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.TimeDate/Components/TimeAndDateHelper.cs index 86e8ca2274a..86294474c6c 100644 --- a/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.TimeDate/Components/TimeAndDateHelper.cs +++ b/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.TimeDate/Components/TimeAndDateHelper.cs @@ -99,7 +99,7 @@ internal static bool ParseStringAsDateTime(in string input, out DateTime timesta else if (Regex.IsMatch(input, @"^u\d+") && input.Length <= 12 && long.TryParse(input.TrimStart('u'), out long secondsInt)) { // unix time stamp - // we use long instead of int because int ist to small after 03:14:07 UTC 2038-01-19 + // we use long instead of int because int is too small after 03:14:07 UTC 2038-01-19 timestamp = new DateTime(1970, 1, 1).AddSeconds(secondsInt).ToLocalTime(); return true; } diff --git a/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.WindowsSettings/Helper/ContextMenuHelper.cs b/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.WindowsSettings/Helper/ContextMenuHelper.cs index 735199c0def..e25d3314247 100644 --- a/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.WindowsSettings/Helper/ContextMenuHelper.cs +++ b/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.WindowsSettings/Helper/ContextMenuHelper.cs @@ -21,7 +21,7 @@ internal static class ContextMenuHelper /// Return a list with all context menu entries for the given /// Symbols taken from /// - /// The result for the context menu entires + /// The result for the context menu entries /// The name of the this assembly /// A list with context menu entries internal static List GetContextMenu(in Result result, in string assemblyName) diff --git a/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.WindowsSettings/Helper/ResultHelper.cs b/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.WindowsSettings/Helper/ResultHelper.cs index 7240ee0fdc6..53cf403f60d 100644 --- a/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.WindowsSettings/Helper/ResultHelper.cs +++ b/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.WindowsSettings/Helper/ResultHelper.cs @@ -67,7 +67,7 @@ internal static List GetResultList( /// /// Add a tool-tip to the given , based o the given . /// - /// The that contain informations for the tool-tip. + /// The that contains information for the tool-tip. /// The that need a tool-tip. private static void AddOptionalToolTip(WindowsSetting entry, Result result) { diff --git a/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.WindowsSettings/WindowsSettings.json b/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.WindowsSettings/WindowsSettings.json index c7a1a8e950b..2695b7c1d25 100644 --- a/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.WindowsSettings/WindowsSettings.json +++ b/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.WindowsSettings/WindowsSettings.json @@ -1240,7 +1240,15 @@ "Name": "WindowsUpdateCheckForUpdates", "Areas": [ "AreaUpdateAndSecurity" ], "Type": "AppSettingsApp", - "Command": "ms-settings:windowsupdate-action" + "Command": "ms-settings:windowsupdate-action", + "DeprecatedInBuild": 22000 + }, + { + "Name": "WindowsUpdateCheckForUpdates", + "Areas": [ "AreaUpdateAndSecurity" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:windowsupdate", + "IntroducedInBuild": 22000 }, { "Name": "WindowsUpdateAdvancedOptions", diff --git a/src/modules/launcher/PowerLauncher/Helper/EnvironmentHelper.cs b/src/modules/launcher/PowerLauncher/Helper/EnvironmentHelper.cs index cbb98683e82..fd9a8e2ef67 100644 --- a/src/modules/launcher/PowerLauncher/Helper/EnvironmentHelper.cs +++ b/src/modules/launcher/PowerLauncher/Helper/EnvironmentHelper.cs @@ -214,7 +214,7 @@ private static void GetMergedMachineAndUserVariables(Dictionary } /// - /// Returns the variables for the specified target. Errors that occurs will be catched and logged. + /// Returns the variables for the specified target. Errors that occurs will be caught and logged. /// /// The target variable source of the type /// A dictionary with the variable or an empty dictionary on errors. diff --git a/src/modules/launcher/PowerLauncher/Plugin/PluginConfig.cs b/src/modules/launcher/PowerLauncher/Plugin/PluginConfig.cs index 5c160db40d5..fa445f135b1 100644 --- a/src/modules/launcher/PowerLauncher/Plugin/PluginConfig.cs +++ b/src/modules/launcher/PowerLauncher/Plugin/PluginConfig.cs @@ -39,7 +39,7 @@ public static List Parse(string[] pluginDirectories) private static void ParsePluginConfigs(IEnumerable directories) { - // todo use linq when diable plugin is implemented since parallel.foreach + list is not thread saft + // todo use linq when disable plugin is implemented since parallel.foreach + list is not thread saft foreach (var directory in directories) { if (File.Exists(Path.Combine(directory, "NeedDelete.txt"))) diff --git a/src/modules/launcher/PowerLauncher/ViewModel/MainViewModel.cs b/src/modules/launcher/PowerLauncher/ViewModel/MainViewModel.cs index b03269698a8..098e16a9ba8 100644 --- a/src/modules/launcher/PowerLauncher/ViewModel/MainViewModel.cs +++ b/src/modules/launcher/PowerLauncher/ViewModel/MainViewModel.cs @@ -1065,7 +1065,6 @@ public void HandleContextMenu(Key acceleratorKey, ModifierKeys acceleratorModifi { if (contextMenuItems.AcceleratorKey == acceleratorKey && contextMenuItems.AcceleratorModifiers == acceleratorModifiers) { - MainWindowVisibility = Visibility.Collapsed; contextMenuItems.Command.Execute(null); } } diff --git a/src/modules/launcher/Wox.Plugin/Common/VirtualDesktop/VirtualDesktopHelper.cs b/src/modules/launcher/Wox.Plugin/Common/VirtualDesktop/VirtualDesktopHelper.cs index 602bf05efee..e7039b1c964 100644 --- a/src/modules/launcher/Wox.Plugin/Common/VirtualDesktop/VirtualDesktopHelper.cs +++ b/src/modules/launcher/Wox.Plugin/Common/VirtualDesktop/VirtualDesktopHelper.cs @@ -16,7 +16,7 @@ namespace Wox.Plugin.Common.VirtualDesktop.Helper { /// /// Helper class to work with Virtual Desktops. - /// This helper uses only public available and documented COM-Interfaces or informations from registry. + /// This helper uses only public available and documented COM-Interfaces or information from registry. /// /// /// To use this helper you have to create an instance of it and access the method via the helper instance. diff --git a/src/modules/launcher/Wox.Plugin/Common/Win32/NativeMethods.cs b/src/modules/launcher/Wox.Plugin/Common/Win32/NativeMethods.cs index 8f02de16c5c..140a6c37b8b 100644 --- a/src/modules/launcher/Wox.Plugin/Common/Win32/NativeMethods.cs +++ b/src/modules/launcher/Wox.Plugin/Common/Win32/NativeMethods.cs @@ -1077,7 +1077,7 @@ public enum ExtendedWindowStyles : uint /// /// The window itself contains child windows that should take part in dialog box, navigation. If this /// style is specified, the dialog manager recurses into children of this window when performing - /// navigation operations such as handling tha TAB key, an arrow key, or a keyboard mnemonic. + /// navigation operations such as handling the TAB key, an arrow key, or a keyboard mnemonic. /// WS_EX_CONTROLPARENT = 0x10000, diff --git a/src/modules/peek/Peek.Common/Helpers/MathHelper.cs b/src/modules/peek/Peek.Common/Helpers/MathHelper.cs index e731d14030e..d9b8322c7d3 100644 --- a/src/modules/peek/Peek.Common/Helpers/MathHelper.cs +++ b/src/modules/peek/Peek.Common/Helpers/MathHelper.cs @@ -2,6 +2,9 @@ // 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.Globalization; + namespace Peek.Common.Helpers { public static class MathHelper @@ -10,5 +13,10 @@ public static int Modulo(int a, int b) { return a < 0 ? ((a % b) + b) % b : a % b; } + + public static int NumberOfDigits(int num) + { + return Math.Abs(num).ToString(CultureInfo.InvariantCulture).Length; + } } } diff --git a/src/modules/peek/Peek.Common/Helpers/PropertyStoreHelper.cs b/src/modules/peek/Peek.Common/Helpers/PropertyStoreHelper.cs index 6c5e7524169..a1c5b6678e0 100644 --- a/src/modules/peek/Peek.Common/Helpers/PropertyStoreHelper.cs +++ b/src/modules/peek/Peek.Common/Helpers/PropertyStoreHelper.cs @@ -65,7 +65,7 @@ public static partial class PropertyStoreHelper /// /// The file/folder path /// The property store flags - /// an IPropertyStroe interface + /// an IPropertyStore interface private static DisposablePropertyStore GetPropertyStoreFromPath(string path, GETPROPERTYSTOREFLAGS flags = GETPROPERTYSTOREFLAGS.GPS_EXTRINSICPROPERTIES | GETPROPERTYSTOREFLAGS.GPS_BESTEFFORT) { IShellItem2? shellItem2 = null; diff --git a/src/modules/peek/Peek.Common/Helpers/ReadableStringHelper.cs b/src/modules/peek/Peek.Common/Helpers/ReadableStringHelper.cs index bc2cba4547c..0ea75816e92 100644 --- a/src/modules/peek/Peek.Common/Helpers/ReadableStringHelper.cs +++ b/src/modules/peek/Peek.Common/Helpers/ReadableStringHelper.cs @@ -10,34 +10,40 @@ namespace Peek.Common.Helpers { public static class ReadableStringHelper { - private const int DecimalPercision = 10; + private const int MaxDigitsToDisplay = 3; + private const int PowerFactor = 1024; public static string BytesToReadableString(ulong bytes) { - var resourceLoader = ResourceLoaderInstance.ResourceLoader; - List format = new List - { - (bytes == 1) ? - resourceLoader.GetString("ReadableString_ByteAbbreviationFormat") : // "byte" - resourceLoader.GetString("ReadableString_BytesAbbreviationFormat"), // "bytes" - resourceLoader.GetString("ReadableString_KiloByteAbbreviationFormat"), // "KB" - resourceLoader.GetString("ReadableString_MegaByteAbbreviationFormat"), // "MB" - resourceLoader.GetString("ReadableString_GigaByteAbbreviationFormat"), // "GB" - resourceLoader.GetString("ReadableString_TeraByteAbbreviationFormat"), // "TB" - resourceLoader.GetString("ReadableString_PetaByteAbbreviationFormat"), // "PB" - resourceLoader.GetString("ReadableString_ExaByteAbbreviationFormat"), // "EB" - }; + string totalBytesDisplays = (bytes == 1) ? + ResourceLoaderInstance.ResourceLoader.GetString("ReadableString_ByteString") : + ResourceLoaderInstance.ResourceLoader.GetString("ReadableString_BytesString"); int index = 0; double number = 0.0; if (bytes > 0) { - index = (int)Math.Floor(Math.Log(bytes) / Math.Log(1024)); - number = Math.Round((bytes / Math.Pow(1024, index)) * DecimalPercision) / DecimalPercision; + index = (int)Math.Floor(Math.Log(bytes) / Math.Log(PowerFactor)); + number = bytes / Math.Pow(PowerFactor, index); + } + + if (index > 0 && number >= Math.Pow(10, MaxDigitsToDisplay)) + { + index++; + number = bytes / Math.Pow(PowerFactor, index); } - return string.Format(CultureInfo.InvariantCulture, format[index], number); + int precision = GetPrecision(index, number); + int decimalPrecision = (int)Math.Pow(10, precision); + + number = Math.Truncate(number * decimalPrecision) / decimalPrecision; + + string formatSpecifier = GetFormatSpecifierString(index, number, bytes, precision); + + return bytes == 0 + ? string.Format(CultureInfo.CurrentCulture, formatSpecifier, number) + : string.Format(CultureInfo.CurrentCulture, formatSpecifier + totalBytesDisplays, number, bytes); } public static string FormatResourceString(string resourceId, object? args) @@ -55,5 +61,32 @@ public static string FormatResourceString(string resourceId, object? args0, obje return formattedString; } + + public static int GetPrecision(int index, double number) + { + int numberOfDigits = MathHelper.NumberOfDigits((int)number); + return index == 0 ? + 0 : + MaxDigitsToDisplay - numberOfDigits; + } + + public static string GetFormatSpecifierString(int index, double number, ulong bytes, int precision) + { + var resourceLoader = ResourceLoaderInstance.ResourceLoader; + List format = new List + { + (bytes == 1) ? + resourceLoader.GetString("ReadableString_ByteAbbreviationFormat") : // "byte" + resourceLoader.GetString("ReadableString_BytesAbbreviationFormat"), // "bytes" + resourceLoader.GetString("ReadableString_KiloByteAbbreviationFormat"), // "KB" + resourceLoader.GetString("ReadableString_MegaByteAbbreviationFormat"), // "MB" + resourceLoader.GetString("ReadableString_GigaByteAbbreviationFormat"), // "GB" + resourceLoader.GetString("ReadableString_TeraByteAbbreviationFormat"), // "TB" + resourceLoader.GetString("ReadableString_PetaByteAbbreviationFormat"), // "PB" + resourceLoader.GetString("ReadableString_ExaByteAbbreviationFormat"), // "EB" + }; + + return "{0:F" + precision + "} " + format[index]; + } } } diff --git a/src/modules/peek/Peek.FilePreviewer/Controls/BrowserControl.xaml.cs b/src/modules/peek/Peek.FilePreviewer/Controls/BrowserControl.xaml.cs index 39f94af55ea..61efadd7fc7 100644 --- a/src/modules/peek/Peek.FilePreviewer/Controls/BrowserControl.xaml.cs +++ b/src/modules/peek/Peek.FilePreviewer/Controls/BrowserControl.xaml.cs @@ -24,6 +24,8 @@ public sealed partial class BrowserControl : UserControl, IDisposable /// private Uri? _navigatedUri; + private Color? _originalBackgroundColor; + public delegate void NavigationCompletedHandler(WebView2? sender, CoreWebView2NavigationCompletedEventArgs? args); public delegate void DOMContentLoadedHandler(CoreWebView2? sender, CoreWebView2DOMContentLoadedEventArgs? args); @@ -50,6 +52,7 @@ public Uri? Source typeof(BrowserControl), new PropertyMetadata(null, new PropertyChangedCallback((d, e) => ((BrowserControl)d).OnIsDevFilePreviewChanged()))); + // Will actually be true for Markdown files as well. public bool IsDevFilePreview { get @@ -99,6 +102,11 @@ public void Navigate() private void SourcePropertyChanged() { OpenUriDialog.Hide(); + + // Setting the background color to transparent. + // This ensures that non-HTML files are displayed with a transparent background. + PreviewBrowser.DefaultBackgroundColor = Color.FromArgb(0, 0, 0, 0); + Navigate(); } @@ -111,6 +119,10 @@ private void OnIsDevFilePreviewChanged() { PreviewBrowser.CoreWebView2.SetVirtualHostNameToFolderMapping(Microsoft.PowerToys.FilePreviewCommon.MonacoHelper.VirtualHostName, Microsoft.PowerToys.FilePreviewCommon.MonacoHelper.MonacoDirectory, CoreWebView2HostResourceAccessKind.Allow); } + else + { + PreviewBrowser.CoreWebView2.ClearVirtualHostNameToFolderMapping(Microsoft.PowerToys.FilePreviewCommon.MonacoHelper.VirtualHostName); + } } } @@ -120,7 +132,14 @@ private async void PreviewWV2_Loaded(object sender, RoutedEventArgs e) { await PreviewBrowser.EnsureCoreWebView2Async(); - // transparent background when loading the page + // Storing the original background color so it can be reset later for specific file types like HTML. + if (!_originalBackgroundColor.HasValue) + { + _originalBackgroundColor = PreviewBrowser.DefaultBackgroundColor; + } + + // Setting the background color to transparent when initially loading the WebView2 component. + // This ensures that non-HTML files are displayed with a transparent background. PreviewBrowser.DefaultBackgroundColor = Color.FromArgb(0, 0, 0, 0); PreviewBrowser.CoreWebView2.Settings.AreDefaultScriptDialogsEnabled = false; @@ -136,6 +155,10 @@ private async void PreviewWV2_Loaded(object sender, RoutedEventArgs e) { PreviewBrowser.CoreWebView2.SetVirtualHostNameToFolderMapping(Microsoft.PowerToys.FilePreviewCommon.MonacoHelper.VirtualHostName, Microsoft.PowerToys.FilePreviewCommon.MonacoHelper.MonacoDirectory, CoreWebView2HostResourceAccessKind.Allow); } + else + { + PreviewBrowser.CoreWebView2.ClearVirtualHostNameToFolderMapping(Microsoft.PowerToys.FilePreviewCommon.MonacoHelper.VirtualHostName); + } PreviewBrowser.CoreWebView2.DOMContentLoaded += CoreWebView2_DOMContentLoaded; PreviewBrowser.CoreWebView2.NewWindowRequested += CoreWebView2_NewWindowRequested; @@ -150,6 +173,20 @@ private async void PreviewWV2_Loaded(object sender, RoutedEventArgs e) private void CoreWebView2_DOMContentLoaded(CoreWebView2 sender, CoreWebView2DOMContentLoadedEventArgs args) { + // If the file being previewed is HTML or HTM, reset the background color to its original state. + // This is done to ensure that HTML and HTM files are displayed as intended, with their own background settings. + // This shouldn't be done for dev file previewer. + if (!IsDevFilePreview && + (Source?.ToString().EndsWith(".html", StringComparison.OrdinalIgnoreCase) == true || + Source?.ToString().EndsWith(".htm", StringComparison.OrdinalIgnoreCase) == true)) + { + // Reset to default behavior for HTML files + if (_originalBackgroundColor.HasValue) + { + PreviewBrowser.DefaultBackgroundColor = _originalBackgroundColor.Value; + } + } + DOMContentLoaded?.Invoke(sender, args); } diff --git a/src/modules/peek/Peek.FilePreviewer/Previewers/WebBrowserPreviewer/WebBrowserPreviewer.cs b/src/modules/peek/Peek.FilePreviewer/Previewers/WebBrowserPreviewer/WebBrowserPreviewer.cs index d3403b4c228..f13bad1d395 100644 --- a/src/modules/peek/Peek.FilePreviewer/Previewers/WebBrowserPreviewer/WebBrowserPreviewer.cs +++ b/src/modules/peek/Peek.FilePreviewer/Previewers/WebBrowserPreviewer/WebBrowserPreviewer.cs @@ -118,6 +118,8 @@ await Dispatcher.RunOnUiThread(async () => } else { + // Simple html file to preview. Shouldn't do things like enabling scripts or using a virtual mapped directory. + IsDevFilePreview = false; Preview = new Uri(File.Path); } }); diff --git a/src/modules/peek/Peek.UI/Strings/en-us/Resources.resw b/src/modules/peek/Peek.UI/Strings/en-us/Resources.resw index d8df22b84a2..b711a9e26f2 100644 --- a/src/modules/peek/Peek.UI/Strings/en-us/Resources.resw +++ b/src/modules/peek/Peek.UI/Strings/en-us/Resources.resw @@ -154,31 +154,31 @@ Date Modified label for the unsupported files view. {0} is the date. - {0} byte + byte Abbreviation for the size unit byte. - {0} KB + KB Abbreviation for the size unit kilobyte. - {0} MB + MB Abbreviation for the size unit megabyte. - {0} GB + GB Abbreviation for the size unit gigabyte. - {0} TB + TB Abbreviation for the size unit terabyte. - {0} PB + PB Abbreviation for the size unit petabyte. - {0} EB + EB Abbreviation for the size unit exabyte. @@ -234,7 +234,7 @@ {0} is the size of the archive, {1} is the extracted size - {0} bytes + bytes Abbreviation for the size bytes @@ -253,4 +253,12 @@ Do you want Peek to open the external application? Title of the dialog showed when an URI is clicked,"Peek" is the name of the utility. + + ({1:N0} bytes) + Displays total number of bytes. Don't localize the "{1:N0}" part. + + + ({1:N0} byte) + Displays unit byte. Don't localize the "{1:N0}" part. + \ No newline at end of file diff --git a/src/modules/poweraccent/PowerAccent.Core/Languages.cs b/src/modules/poweraccent/PowerAccent.Core/Languages.cs index 4a611f2082e..0695b27f06d 100644 --- a/src/modules/poweraccent/PowerAccent.Core/Languages.cs +++ b/src/modules/poweraccent/PowerAccent.Core/Languages.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.Collections.Concurrent; using PowerToys.PowerAccentKeyboardService; namespace PowerAccent.Core @@ -80,11 +81,15 @@ public static string[] GetDefaultLetterKey(LetterKey letter, Language lang) }; } + // Store the computed letters for each key, so that subsequent calls don't take as long. + private static ConcurrentDictionary _allLanguagesCache = new ConcurrentDictionary(); + // All private static string[] GetDefaultLetterKeyALL(LetterKey letter) { - // would be even better to loop through Languages and call these functions dynamically, but I don't know how to do that! - return GetDefaultLetterKeyCA(letter) + if (!_allLanguagesCache.ContainsKey(letter)) + { + _allLanguagesCache[letter] = GetDefaultLetterKeyCA(letter) .Union(GetDefaultLetterKeyCUR(letter)) .Union(GetDefaultLetterKeyCY(letter)) .Union(GetDefaultLetterKeyCZ(letter)) @@ -113,7 +118,53 @@ private static string[] GetDefaultLetterKeyALL(LetterKey letter) .Union(GetDefaultLetterKeySR(letter)) .Union(GetDefaultLetterKeySV(letter)) .Union(GetDefaultLetterKeyTK(letter)) - .ToArray(); + .Union(GetDefaultLetterKeyAllLanguagesOnly(letter)) + .ToArray(); + } + + return _allLanguagesCache[letter]; + } + + // Contains all characters that should be shown in all languages but currently don't belong to any of the single languages available for that letter. + // These characters can be removed from this list after they've been added to one of the other languages for that specific letter. + private static string[] GetDefaultLetterKeyAllLanguagesOnly(LetterKey letter) + { + return letter switch + { + LetterKey.VK_A => new[] { "α", "ȧ" }, + LetterKey.VK_B => new[] { "ḃ", "β" }, + LetterKey.VK_C => new[] { "ċ", "χ", "°C", "©", "ℂ" }, + LetterKey.VK_D => new[] { "ḍ", "ḋ", "δ" }, + LetterKey.VK_E => new[] { "ε", "η", "∈" }, + LetterKey.VK_F => new[] { "ḟ", "°F" }, + LetterKey.VK_G => new[] { "ģ", "ǧ", "ġ", "ĝ", "ǥ", "γ" }, + LetterKey.VK_H => new[] { "ḣ", "ĥ", "ħ" }, + LetterKey.VK_I => new[] { "ι" }, + LetterKey.VK_J => new[] { "ĵ" }, + LetterKey.VK_K => new[] { "ķ", "ǩ", "κ" }, + LetterKey.VK_L => new[] { "ļ", "₺", "λ" }, // ₺ is in VK_T for other languages, but not VK_L, so we add it here. + LetterKey.VK_M => new[] { "ṁ", "μ" }, + LetterKey.VK_N => new[] { "ņ", "ṅ", "ⁿ", "ν", "ℕ" }, + LetterKey.VK_O => new[] { "ȯ", "ω", "ο" }, + LetterKey.VK_P => new[] { "ṗ", "φ", "ψ", "℗" }, + LetterKey.VK_Q => new[] { "ℚ" }, + LetterKey.VK_R => new[] { "ṙ", "ρ", "®", "ℝ" }, + LetterKey.VK_S => new[] { "ṡ", "σ", "\u00A7" }, + LetterKey.VK_T => new[] { "ţ", "ṫ", "ŧ", "θ", "τ", "™" }, + LetterKey.VK_U => new[] { "ŭ", "υ" }, + LetterKey.VK_V => new[] { "V̇" }, + LetterKey.VK_W => new[] { "ẇ" }, + LetterKey.VK_X => new[] { "ẋ", "ξ", "×" }, + LetterKey.VK_Y => new[] { "ẏ" }, + LetterKey.VK_Z => new[] { "ʒ", "ǯ", "ζ", "ℤ" }, + LetterKey.VK_COMMA => new[] { "∙", "₋", "⁻", "–" }, // – is in VK_MINUS for other languages, but not VK_COMMA, so we add it here. + LetterKey.VK_PERIOD => new[] { "\u0300", "\u0301", "\u0302", "\u0303", "\u0304", "\u0308", "\u030C" }, + LetterKey.VK_MINUS => new[] { "~", "‐", "‑", "‒", "—", "―", "⁓", "−", "⸺", "⸻" }, + LetterKey.VK_SLASH_ => new[] { "÷" }, + LetterKey.VK_DIVIDE_ => new[] { "÷" }, + LetterKey.VK_MULTIPLY_ => new[] { "×", "⋅" }, + _ => Array.Empty(), + }; } // Currencies (source: https://www.eurochange.co.uk/travel-money/world-currency-abbreviations-symbols-and-codes-travel-money) @@ -209,7 +260,9 @@ private static string[] GetDefaultLetterKeySP(LetterKey letter) { LetterKey.VK_A => new[] { "á" }, LetterKey.VK_E => new[] { "é", "€" }, + LetterKey.VK_H => new[] { "ḥ" }, LetterKey.VK_I => new[] { "í" }, + LetterKey.VK_L => new[] { "ḷ" }, LetterKey.VK_N => new[] { "ñ" }, LetterKey.VK_O => new[] { "ó" }, LetterKey.VK_U => new[] { "ú", "ü" }, diff --git a/src/modules/poweraccent/PowerAccentKeyboardService/PowerAccentKeyboardService.vcxproj b/src/modules/poweraccent/PowerAccentKeyboardService/PowerAccentKeyboardService.vcxproj index 6b216ac3c47..c8ecde85156 100644 --- a/src/modules/poweraccent/PowerAccentKeyboardService/PowerAccentKeyboardService.vcxproj +++ b/src/modules/poweraccent/PowerAccentKeyboardService/PowerAccentKeyboardService.vcxproj @@ -113,9 +113,6 @@ {6955446d-23f7-4023-9bb3-8657f904af99} - - {cc6e41ac-8174-4e8a-8d22-85dd7f4851df} - diff --git a/src/modules/powerrename/PowerRenameUILib/PowerRenameUI.vcxproj b/src/modules/powerrename/PowerRenameUILib/PowerRenameUI.vcxproj index 8287fd95976..08e216564ea 100644 --- a/src/modules/powerrename/PowerRenameUILib/PowerRenameUI.vcxproj +++ b/src/modules/powerrename/PowerRenameUILib/PowerRenameUI.vcxproj @@ -1,7 +1,7 @@  - + true @@ -193,9 +193,6 @@ {98537082-0fdb-40de-abd8-0dc5a4269bab} - - {cc6e41ac-8174-4e8a-8d22-85dd7f4851df} - {51920f1f-c28c-4adf-8660-4238766796c2} @@ -208,7 +205,7 @@ - + @@ -220,8 +217,8 @@ - - + + diff --git a/src/modules/powerrename/PowerRenameUILib/packages.config b/src/modules/powerrename/PowerRenameUILib/packages.config index eb4be241a80..53ee497bdd9 100644 --- a/src/modules/powerrename/PowerRenameUILib/packages.config +++ b/src/modules/powerrename/PowerRenameUILib/packages.config @@ -5,5 +5,5 @@ - + \ No newline at end of file diff --git a/src/modules/powerrename/dll/PowerRenameExt.vcxproj b/src/modules/powerrename/dll/PowerRenameExt.vcxproj index 747f34944e4..cf58198d656 100644 --- a/src/modules/powerrename/dll/PowerRenameExt.vcxproj +++ b/src/modules/powerrename/dll/PowerRenameExt.vcxproj @@ -63,9 +63,6 @@ {98537082-0fdb-40de-abd8-0dc5a4269bab} - - {cc6e41ac-8174-4e8a-8d22-85dd7f4851df} - {51920f1f-c28c-4adf-8660-4238766796c2} diff --git a/src/modules/previewpane/MonacoPreviewHandler/MonacoPreviewHandlerControl.cs b/src/modules/previewpane/MonacoPreviewHandler/MonacoPreviewHandlerControl.cs index ca16bcc8353..7480acd1dca 100644 --- a/src/modules/previewpane/MonacoPreviewHandler/MonacoPreviewHandlerControl.cs +++ b/src/modules/previewpane/MonacoPreviewHandler/MonacoPreviewHandlerControl.cs @@ -165,7 +165,7 @@ public override void DoPreview(T dataSource) } catch (NullReferenceException e) { - Logger.LogError("NullReferenceException catched. Skipping exception.", e); + Logger.LogError("NullReferenceException caught. Skipping exception.", e); } } catch (WebView2RuntimeNotFoundException e) diff --git a/src/modules/previewpane/UnitTests-SvgThumbnailProvider/HelperFiles/WithComments.svg b/src/modules/previewpane/UnitTests-SvgThumbnailProvider/HelperFiles/WithComments.svg new file mode 100644 index 00000000000..3acbefe3d12 --- /dev/null +++ b/src/modules/previewpane/UnitTests-SvgThumbnailProvider/HelperFiles/WithComments.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/modules/previewpane/UnitTests-SvgThumbnailProvider/SvgThumbnailProviderTests.cs b/src/modules/previewpane/UnitTests-SvgThumbnailProvider/SvgThumbnailProviderTests.cs index 4e50e765c3c..4fd38cf164f 100644 --- a/src/modules/previewpane/UnitTests-SvgThumbnailProvider/SvgThumbnailProviderTests.cs +++ b/src/modules/previewpane/UnitTests-SvgThumbnailProvider/SvgThumbnailProviderTests.cs @@ -4,14 +4,12 @@ using System; using System.Drawing; +using System.Drawing.Imaging; using System.Runtime.InteropServices; -using System.Runtime.InteropServices.ComTypes; using System.Text; -using Common.ComInterlop; using Microsoft.PowerToys.STATestExtension; using Microsoft.PowerToys.ThumbnailHandler.Svg; using Microsoft.VisualStudio.TestTools.UnitTesting; -using Moq; namespace SvgThumbnailProviderUnitTests { @@ -211,5 +209,17 @@ public void GetThumbnailValidStreamHTML() Assert.IsTrue(bitmap != null); } + + [TestMethod] + public void SvgCommentsAreHandledCorrectly() + { + var filePath = "HelperFiles/WithComments.svg"; + + SvgThumbnailProvider svgThumbnailProvider = new SvgThumbnailProvider(filePath); + + Bitmap bitmap = svgThumbnailProvider.GetThumbnail(8); + + Assert.IsTrue(bitmap != null); + } } } diff --git a/src/modules/previewpane/UnitTests-SvgThumbnailProvider/UnitTests-SvgThumbnailProvider.csproj b/src/modules/previewpane/UnitTests-SvgThumbnailProvider/UnitTests-SvgThumbnailProvider.csproj index 100bc8ecdb5..6aec341be5e 100644 --- a/src/modules/previewpane/UnitTests-SvgThumbnailProvider/UnitTests-SvgThumbnailProvider.csproj +++ b/src/modules/previewpane/UnitTests-SvgThumbnailProvider/UnitTests-SvgThumbnailProvider.csproj @@ -38,15 +38,19 @@ - - + + + - + Always - + Always + \ No newline at end of file diff --git a/src/modules/previewpane/common/Utilities/SvgPreviewHandlerHelper.cs b/src/modules/previewpane/common/Utilities/SvgPreviewHandlerHelper.cs index 5cb346d70e0..180158152cd 100644 --- a/src/modules/previewpane/common/Utilities/SvgPreviewHandlerHelper.cs +++ b/src/modules/previewpane/common/Utilities/SvgPreviewHandlerHelper.cs @@ -119,7 +119,7 @@ private static int FindFirstXmlOpenTagIndex(string s) while ((index = s.IndexOf('<', index)) != -1) { - if (index < s.Length - 1 && s[index + 1] != '?') + if (index < s.Length - 1 && s[index + 1] != '?' && s[index + 1] != '!') { return index; } @@ -130,11 +130,11 @@ private static int FindFirstXmlOpenTagIndex(string s) return -1; } - private static int FindFirstXmlCloseTagIndex(string s) + private static int FindFirstXmlCloseTagIndex(string s, int openTagIndex) { int index = 1; - while ((index = s.IndexOf('>', index)) != -1) + while ((index = s.IndexOf('>', openTagIndex)) != -1) { if (index > 0 && s[index - 1] != '?') { @@ -160,7 +160,7 @@ public static string AddStyleSVG(string stringSvgData) return stringSvgData; } - int firstXmlCloseTagIndex = FindFirstXmlCloseTagIndex(stringSvgData); + int firstXmlCloseTagIndex = FindFirstXmlCloseTagIndex(stringSvgData, firstXmlOpenTagIndex); if (firstXmlCloseTagIndex == -1) { return stringSvgData; @@ -192,13 +192,18 @@ public static string AddStyleSVG(string stringSvgData) styleIndex -= numRemoved; } + firstXmlCloseTagIndex -= numRemoved; + stringSvgData = RemoveAttribute(stringSvgData, heightIndex, HeightAttribute, out numRemoved); if (styleIndex != -1 && styleIndex > heightIndex) { styleIndex -= numRemoved; } + firstXmlCloseTagIndex -= numRemoved; + stringSvgData = RemoveAttribute(stringSvgData, styleIndex, StyleAttribute, out numRemoved); + firstXmlCloseTagIndex -= numRemoved; width = CheckUnit(width); height = CheckUnit(height); @@ -210,10 +215,10 @@ public static string AddStyleSVG(string stringSvgData) // max-width and max-height not supported. Extra CSS is needed for it to work. string scaling = $"max-width: {width} ; max-height: {height} ;"; - scaling += $" _height:expression(this.scrollHeight > {heightR} ? \" {height}\" : \"auto\"); _width:expression(this.scrollWidth > {widthR} ? \"{width}\" : \"auto\");"; + scaling += $" _height:expression(this.scrollHeight > {heightR} ? " {height}" : "auto"); _width:expression(this.scrollWidth > {widthR} ? "{width}" : "auto");"; string newStyle = $"style=\"{scaling}{centering}{oldStyle}\""; - int insertAt = stringSvgData.IndexOf(">", StringComparison.InvariantCultureIgnoreCase); + int insertAt = firstXmlCloseTagIndex; stringSvgData = stringSvgData.Insert(insertAt, " " + newStyle); diff --git a/src/modules/registrypreview/RegistryPreviewUI/MainWindow.Utilities.cs b/src/modules/registrypreview/RegistryPreviewUI/MainWindow.Utilities.cs index 07f0837fdd0..f3da13a340a 100644 --- a/src/modules/registrypreview/RegistryPreviewUI/MainWindow.Utilities.cs +++ b/src/modules/registrypreview/RegistryPreviewUI/MainWindow.Utilities.cs @@ -8,7 +8,9 @@ using System.Diagnostics; using System.Globalization; using System.IO; +using System.Linq; using System.Reflection; +using System.Text; using System.Threading.Tasks; using Microsoft.UI.Input; using Microsoft.UI.Xaml; @@ -413,21 +415,75 @@ private bool ParseRegistryFile(string filenameText) value += registryLine; } - // Clean out any escaped characters in the value, only for the preview - value = StripEscapedCharacters(value); - // update the ListViewItem with the loaded value, based off REG value type switch (registryValue.Type) { case "ERROR": // do nothing break; + case "REG_SZ": + if (value == "\"") + { + // Value is most likely missing an end quote + registryValue.Type = "ERROR"; + value = resourceLoader.GetString("InvalidString"); + } + else + { + for (int i = 1; i < value.Length; i++) + { + if (value[i - 1] == '\\') + { + // Only allow these escape characters + if (value[i] != '"' && value[i] != '\\') + { + registryValue.Type = "ERROR"; + value = resourceLoader.GetString("InvalidString"); + break; + } + + i++; + } + + if (i < value.Length && value[i - 1] != '\\' && value[i] == '"') + { + // Don't allow non-escaped quotes + registryValue.Type = "ERROR"; + value = resourceLoader.GetString("InvalidString"); + break; + } + } + + if (registryValue.Type != "ERROR") + { + // Clean out any escaped characters in the value, only for the preview + value = StripEscapedCharacters(value); + } + } + + registryValue.Value = value; + break; case "REG_BINARY": case "REG_NONE": if (value.Length <= 0) { value = resourceLoader.GetString("ZeroLength"); } + else + { + try + { + // Hexes are usually two characters (00), it's invalid if less or more than 2 + var bytes = value.Split(',').Select( + c => c.Length == 2 ? byte.Parse(c, NumberStyles.HexNumber, CultureInfo.InvariantCulture) : throw null); + value = string.Join(' ', bytes.Select(b => b.ToString("x2", CultureInfo.CurrentCulture))); + } + catch + { + registryValue.Type = "ERROR"; + value = resourceLoader.GetString("InvalidBinary"); + } + } registryValue.Value = value; @@ -436,6 +492,19 @@ private bool ParseRegistryFile(string filenameText) if (value.Length <= 0) { registryValue.Type = "ERROR"; + value = resourceLoader.GetString("InvalidDword"); + } + else + { + if (uint.TryParse(value, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out uint dword)) + { + value = $"0x{dword:x8} ({dword})"; + } + else + { + registryValue.Type = "ERROR"; + value = resourceLoader.GetString("InvalidDword"); + } } registryValue.Value = value; @@ -444,8 +513,55 @@ private bool ParseRegistryFile(string filenameText) case "REG_QWORD": if (value.Length <= 0) { + registryValue.Type = "ERROR"; value = resourceLoader.GetString("InvalidQword"); } + else + { + try + { + // Hexes are usually two characters (00), it's invalid if less or more than 2 + var bytes = value.Split(',').Select( + c => c.Length == 2 ? byte.Parse(c, NumberStyles.HexNumber, CultureInfo.InvariantCulture) : throw null).ToArray(); + ulong qword = BitConverter.ToUInt64(bytes); + value = $"0x{qword:x8} ({qword})"; + } + catch + { + registryValue.Type = "ERROR"; + value = resourceLoader.GetString("InvalidQword"); + } + } + + registryValue.Value = value; + break; + case "REG_EXPAND_SZ": + case "REG_MULTI_SZ": + try + { + // Hexes are usually two characters (00), it's invalid if less or more than 2 + var bytes = value.Split(',').Select( + c => c.Length == 2 ? byte.Parse(c, NumberStyles.HexNumber, CultureInfo.InvariantCulture) : throw null).ToArray(); + + if (registryValue.Type == "REG_MULTI_SZ") + { + // Replace zeros (00,00) with spaces + for (int i = 0; i < bytes.Length; i += 2) + { + if (bytes[i] == 0 && bytes[i + 1] == 0) + { + bytes[i] = 0x20; + } + } + } + + value = Encoding.Unicode.GetString(bytes); + } + catch + { + registryValue.Type = "ERROR"; + value = resourceLoader.GetString("InvalidString"); + } registryValue.Value = value; break; @@ -1036,7 +1152,7 @@ private string ScanAndRemoveComments(string value) value = value.Remove(indexOf, value.Length - indexOf); } - return value; + return value.TrimEnd(); } /// diff --git a/src/modules/registrypreview/RegistryPreviewUI/RegistryValue.xaml.cs b/src/modules/registrypreview/RegistryPreviewUI/RegistryValue.xaml.cs index a7df53f1f82..161e9cbeddb 100644 --- a/src/modules/registrypreview/RegistryPreviewUI/RegistryValue.xaml.cs +++ b/src/modules/registrypreview/RegistryPreviewUI/RegistryValue.xaml.cs @@ -33,7 +33,7 @@ public Uri ImageUri switch (Type) { case "REG_SZ": - case "REG_EXAND_SZ": + case "REG_EXPAND_SZ": case "REG_MULTI_SZ": return uriStringValue; case "ERROR": diff --git a/src/modules/registrypreview/RegistryPreviewUI/Strings/en-US/Resources.resw b/src/modules/registrypreview/RegistryPreviewUI/Strings/en-US/Resources.resw index 6b766a65702..d637b2e1871 100644 --- a/src/modules/registrypreview/RegistryPreviewUI/Strings/en-US/Resources.resw +++ b/src/modules/registrypreview/RegistryPreviewUI/Strings/en-US/Resources.resw @@ -138,6 +138,12 @@ Registry files (*.reg) + + (Invalid binary value) + + + (Invalid DWORD (32-bit) value) + (Invalid QWORD (64-bit) value) @@ -147,6 +153,9 @@ File was not a Registry file + + (Invalid string value) + is larger than 10MB which is too large for this application. @@ -228,7 +237,7 @@ User Account Control - Value + Data Write to Registry diff --git a/src/modules/videoconference/VideoConferenceProxyFilter/VideoConferenceProxyFilter.vcxproj b/src/modules/videoconference/VideoConferenceProxyFilter/VideoConferenceProxyFilter.vcxproj index 43ff97ace1e..a2779581488 100644 --- a/src/modules/videoconference/VideoConferenceProxyFilter/VideoConferenceProxyFilter.vcxproj +++ b/src/modules/videoconference/VideoConferenceProxyFilter/VideoConferenceProxyFilter.vcxproj @@ -140,6 +140,9 @@ {459e0768-7ebd-4c41-bba1-6db3b3815e0a} + + {cc6e41ac-8174-4e8a-8d22-85dd7f4851df} + diff --git a/src/settings-ui/Settings.UI.Library/ColorPickerProperties.cs b/src/settings-ui/Settings.UI.Library/ColorPickerProperties.cs index 09dde9318ca..6f974faba71 100644 --- a/src/settings-ui/Settings.UI.Library/ColorPickerProperties.cs +++ b/src/settings-ui/Settings.UI.Library/ColorPickerProperties.cs @@ -51,7 +51,7 @@ public ColorPickerProperties() [JsonPropertyName("activationaction")] public ColorPickerActivationAction ActivationAction { get; set; } - // Property ColorHistory is not used, the color history is saved separatedly in the colorHistory.json file + // Property ColorHistory is not used, the color history is saved separately in the colorHistory.json file [JsonPropertyName("colorhistory")] public List ColorHistory { get; set; } diff --git a/src/settings-ui/Settings.UI.Library/Enumerations/ColorRepresentationType.cs b/src/settings-ui/Settings.UI.Library/Enumerations/ColorRepresentationType.cs index 5bebe799a87..09ef003e883 100644 --- a/src/settings-ui/Settings.UI.Library/Enumerations/ColorRepresentationType.cs +++ b/src/settings-ui/Settings.UI.Library/Enumerations/ColorRepresentationType.cs @@ -4,7 +4,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library.Enumerations { - // NOTE: don't change the order (numbers) of the enumeration entires + // NOTE: don't change the order (numbers) of the enumeration entries /// /// The type of the color representation diff --git a/src/settings-ui/Settings.UI.Library/PluginAdditionalOption.cs b/src/settings-ui/Settings.UI.Library/PluginAdditionalOption.cs index 362384c5346..54e0c7868e7 100644 --- a/src/settings-ui/Settings.UI.Library/PluginAdditionalOption.cs +++ b/src/settings-ui/Settings.UI.Library/PluginAdditionalOption.cs @@ -3,17 +3,28 @@ // See the LICENSE file in the project root for more information. using System.Collections.Generic; +using System.Text.Json.Serialization; namespace Microsoft.PowerToys.Settings.UI.Library { public class PluginAdditionalOption { - public enum SelectionType + public enum AdditionalOptionType { Checkbox = 0, Combobox = 1, + Textbox = 2, + Numberbox = 3, + CheckboxAndCombobox = 11, + CheckboxAndTextbox = 12, + CheckboxAndNumberbox = 13, } + /// + /// Gets or sets the layout type of the option in settings ui (Optional; Default is checkbox) + /// + public AdditionalOptionType PluginOptionType { get; set; } + public string Key { get; set; } public string DisplayLabel { get; set; } @@ -21,14 +32,64 @@ public enum SelectionType /// /// Gets or sets a value to show a description of this setting in the settings ui. (Optional) /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string DisplayDescription { get; set; } + /// + /// Gets or sets a value to show a label for the second setting if two combined settings are shown + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string SecondDisplayLabel { get; set; } + + /// + /// Gets or sets a value to show a description for the second setting in the settings ui if two combined settings are shown. (Optional) + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string SecondDisplayDescription { get; set; } + + /// + /// Gets or sets a value indicating whether the checkbox is set or not set + /// public bool Value { get; set; } + public int ComboBoxValue { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public List ComboBoxOptions { get; set; } - public int Option { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string TextValue { get; set; } - public int SelectionTypeValue { get; set; } + /// + /// Gets or sets the value that specifies the maximum number of characters allowed for user input in the text box. (Optional; Default is 0 which means no limit.) + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public int? TextBoxMaxLength { get; set; } + + public double NumberValue { get; set; } + + /// + /// Gets or sets a minimal value for the number box. (Optional; Default is Double.MinValue) + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public double? NumberBoxMin { get; set; } + + /// + /// Gets or sets a maximal value for the number box. (Optional; Default is Double.MaxValue) + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public double? NumberBoxMax { get; set; } + + /// + /// Gets or sets the value for small changes of the number box. (Optional; Default is 1) + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public double? NumberBoxSmallChange { get; set; } + + /// + /// Gets or sets the value for large changes of the number box. (Optional; Default is 10) + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public double? NumberBoxLargeChange { get; set; } } } diff --git a/src/settings-ui/Settings.UI.Library/PowerLauncherSettings.cs b/src/settings-ui/Settings.UI.Library/PowerLauncherSettings.cs index b1e4cec04d9..b85906a0010 100644 --- a/src/settings-ui/Settings.UI.Library/PowerLauncherSettings.cs +++ b/src/settings-ui/Settings.UI.Library/PowerLauncherSettings.cs @@ -33,6 +33,7 @@ public virtual void Save(ISettingsUtils settingsUtils) var options = new JsonSerializerOptions { WriteIndented = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, }; if (settingsUtils == null) diff --git a/src/settings-ui/Settings.UI.Library/SettingsBackupAndRestoreUtils.cs b/src/settings-ui/Settings.UI.Library/SettingsBackupAndRestoreUtils.cs index 759e86d7515..8315c52bdb6 100644 --- a/src/settings-ui/Settings.UI.Library/SettingsBackupAndRestoreUtils.cs +++ b/src/settings-ui/Settings.UI.Library/SettingsBackupAndRestoreUtils.cs @@ -292,9 +292,9 @@ public static string GetRegSettingsBackupAndRestoreRegItem(string itemName) } // get data needed for process - var backupRetoreSettings = JsonNode.Parse(GetBackupRestoreSettingsJson()); - var currentSettingsFiles = GetSettingsFiles(backupRetoreSettings, appBasePath).ToList().ToDictionary(x => x.Substring(appBasePath.Length)); - var backupSettingsFiles = GetSettingsFiles(backupRetoreSettings, latestSettingsFolder).ToList().ToDictionary(x => x.Substring(latestSettingsFolder.Length)); + var backupRestoreSettings = JsonNode.Parse(GetBackupRestoreSettingsJson()); + var currentSettingsFiles = GetSettingsFiles(backupRestoreSettings, appBasePath).ToList().ToDictionary(x => x.Substring(appBasePath.Length)); + var backupSettingsFiles = GetSettingsFiles(backupRestoreSettings, latestSettingsFolder).ToList().ToDictionary(x => x.Substring(latestSettingsFolder.Length)); if (backupSettingsFiles.Count == 0) { @@ -306,13 +306,13 @@ public static string GetRegSettingsBackupAndRestoreRegItem(string itemName) foreach (var currentFile in backupSettingsFiles) { var relativePath = currentFile.Value.Substring(latestSettingsFolder.Length + 1); - var retoreFullPath = Path.Combine(appBasePath, relativePath); - var settingsToRestoreJson = GetExportVersion(backupRetoreSettings, currentFile.Key, currentFile.Value); + var restoreFullPath = Path.Combine(appBasePath, relativePath); + var settingsToRestoreJson = GetExportVersion(backupRestoreSettings, currentFile.Key, currentFile.Value); if (currentSettingsFiles.TryGetValue(currentFile.Key, out string value)) { // we have a setting file to restore to - var currentSettingsFileJson = GetExportVersion(backupRetoreSettings, currentFile.Key, value); + var currentSettingsFileJson = GetExportVersion(backupRestoreSettings, currentFile.Key, value); if (JsonNormalizer.Normalize(settingsToRestoreJson) != JsonNormalizer.Normalize(currentSettingsFileJson)) { @@ -339,7 +339,7 @@ public static string GetRegSettingsBackupAndRestoreRegItem(string itemName) if (anyFilesUpdated) { // something was changed do we need to return true to indicate a restart is needed. - var restartAfterRestore = (bool?)backupRetoreSettings!["RestartAfterRestore"]; + var restartAfterRestore = (bool?)backupRestoreSettings!["RestartAfterRestore"]; if (!restartAfterRestore.HasValue || restartAfterRestore.Value) { return (true, $"RESTART APP", "Success"); @@ -639,11 +639,11 @@ private static string[] GetSettingsFiles(JsonNode settings, string path) } // get data needed for process - var backupRetoreSettings = JsonNode.Parse(GetBackupRestoreSettingsJson()); - var currentSettingsFiles = GetSettingsFiles(backupRetoreSettings, appBasePath).ToList().ToDictionary(x => x.Substring(appBasePath.Length)); + var backupRestoreSettings = JsonNode.Parse(GetBackupRestoreSettingsJson()); + var currentSettingsFiles = GetSettingsFiles(backupRestoreSettings, appBasePath).ToList().ToDictionary(x => x.Substring(appBasePath.Length)); var fullBackupDir = Path.Combine(Path.GetTempPath(), $"settings_{DateTime.UtcNow.ToFileTimeUtc().ToString(CultureInfo.InvariantCulture)}"); var latestSettingsFolder = GetLatestSettingsFolder(); - var lastBackupSettingsFiles = GetSettingsFiles(backupRetoreSettings, latestSettingsFolder).ToList().ToDictionary(x => x.Substring(latestSettingsFolder.Length)); + var lastBackupSettingsFiles = GetSettingsFiles(backupRestoreSettings, latestSettingsFolder).ToList().ToDictionary(x => x.Substring(latestSettingsFolder.Length)); lastBackupExists = lastBackupSettingsFiles.Count > 0; @@ -661,13 +661,13 @@ private static string[] GetSettingsFiles(JsonNode settings, string path) tempFile = currentFile; // need to check and back this up; - var currentSettingsFileToBackup = GetExportVersion(backupRetoreSettings, currentFile.Key, currentFile.Value); + var currentSettingsFileToBackup = GetExportVersion(backupRestoreSettings, currentFile.Key, currentFile.Value); var doBackup = false; if (lastBackupSettingsFiles.TryGetValue(currentFile.Key, out string value)) { // there is a previous backup for this, get an export version of it. - var lastSettingsFileDoc = GetExportVersion(backupRetoreSettings, currentFile.Key, value); + var lastSettingsFileDoc = GetExportVersion(backupRestoreSettings, currentFile.Key, value); // check to see if the new export version would be same as last export version. if (JsonNormalizer.Normalize(currentSettingsFileToBackup) != JsonNormalizer.Normalize(lastSettingsFileDoc)) @@ -804,9 +804,9 @@ private static string WildCardToRegular(string value) /// Method GetExportVersion gets the version of the settings file that we want to backup. /// It will be formatted and all problematic settings removed from it. /// - public static string GetExportVersion(JsonNode backupRetoreSettings, string settingFileKey, string settingsFileName) + public static string GetExportVersion(JsonNode backupRestoreSettings, string settingFileKey, string settingsFileName) { - var ignoredSettings = GetIgnoredSettings(backupRetoreSettings, settingFileKey); + var ignoredSettings = GetIgnoredSettings(backupRestoreSettings, settingFileKey); var settingsFile = JsonDocument.Parse(File.ReadAllText(settingsFileName)); var outputBuffer = new ArrayBufferWriter(); @@ -828,7 +828,7 @@ public static string GetExportVersion(JsonNode backupRetoreSettings, string sett if (settingFileKey.Equals("\\PowerToys Run\\settings.json", StringComparison.OrdinalIgnoreCase)) { // PowerToys Run hack fix-up - var ptRunIgnoredSettings = GetPTRunIgnoredSettings(backupRetoreSettings); + var ptRunIgnoredSettings = GetPTRunIgnoredSettings(backupRestoreSettings); var ptrSettings = JsonNode.Parse(Encoding.UTF8.GetString(outputBuffer.WrittenSpan)); foreach (JsonObject pluginToChange in ptRunIgnoredSettings) @@ -856,16 +856,16 @@ public static string GetExportVersion(JsonNode backupRetoreSettings, string sett /// /// Method GetPTRunIgnoredSettings gets the 'Run-Plugin-level' settings we should ignore because they are problematic to backup/restore. /// - private static JsonArray GetPTRunIgnoredSettings(JsonNode backupRetoreSettings) + private static JsonArray GetPTRunIgnoredSettings(JsonNode backupRestoreSettings) { - if (backupRetoreSettings == null) + if (backupRestoreSettings == null) { - throw new ArgumentNullException(nameof(backupRetoreSettings)); + throw new ArgumentNullException(nameof(backupRestoreSettings)); } - if (backupRetoreSettings["IgnoredPTRunSettings"] != null) + if (backupRestoreSettings["IgnoredPTRunSettings"] != null) { - return (JsonArray)backupRetoreSettings["IgnoredPTRunSettings"]; + return (JsonArray)backupRestoreSettings["IgnoredPTRunSettings"]; } return new JsonArray(); @@ -874,11 +874,11 @@ private static JsonArray GetPTRunIgnoredSettings(JsonNode backupRetoreSettings) /// /// Method GetIgnoredSettings gets the 'top-level' settings we should ignore because they are problematic to backup/restore. /// - private static string[] GetIgnoredSettings(JsonNode backupRetoreSettings, string settingFileKey) + private static string[] GetIgnoredSettings(JsonNode backupRestoreSettings, string settingFileKey) { - if (backupRetoreSettings == null) + if (backupRestoreSettings == null) { - throw new ArgumentNullException(nameof(backupRetoreSettings)); + throw new ArgumentNullException(nameof(backupRestoreSettings)); } if (settingFileKey.StartsWith("\\", StringComparison.OrdinalIgnoreCase)) @@ -886,11 +886,11 @@ private static string[] GetIgnoredSettings(JsonNode backupRetoreSettings, string settingFileKey = settingFileKey.Substring(1); } - if (backupRetoreSettings["IgnoredSettings"] != null) + if (backupRestoreSettings["IgnoredSettings"] != null) { - if (backupRetoreSettings["IgnoredSettings"][settingFileKey] != null) + if (backupRestoreSettings["IgnoredSettings"][settingFileKey] != null) { - var settingsArray = (JsonArray)backupRetoreSettings["IgnoredSettings"][settingFileKey]; + var settingsArray = (JsonArray)backupRestoreSettings["IgnoredSettings"][settingFileKey]; Console.WriteLine("settingsArray " + settingsArray.GetType().FullName); diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/ColorPickerPage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/ColorPickerPage.xaml index e847ce27cc0..8b583d31866 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/ColorPickerPage.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/ColorPickerPage.xaml @@ -162,7 +162,7 @@ x:Uid="ColorFormatDialog" IsPrimaryButtonEnabled="{Binding IsValid, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" PrimaryButtonStyle="{ThemeResource AccentButtonStyle}" - SecondaryButtonClick="ColorFormatDialog_CancelButtonClick"> + Closed="ColorFormatDialog_Closed"> 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 1a408c88cd2..3d725a55fa8 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/ColorPickerPage.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/ColorPickerPage.xaml.cs @@ -112,7 +112,6 @@ private void Update() ColorFormatModel colorFormat = ColorFormatDialog.DataContext as ColorFormatModel; string oldName = ((KeyValuePair)ColorFormatDialog.Tag).Key; ViewModel.UpdateColorFormat(oldName, colorFormat); - ColorFormatDialog.Hide(); } private async void NewFormatClick(object sender, Microsoft.UI.Xaml.RoutedEventArgs e) @@ -127,19 +126,6 @@ private async void NewFormatClick(object sender, Microsoft.UI.Xaml.RoutedEventAr await ColorFormatDialog.ShowAsync(); } - private void ColorFormatDialog_CancelButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs args) - { - if (ColorFormatDialog.Tag is KeyValuePair) - { - ColorFormatModel modifiedColorFormat = ColorFormatDialog.DataContext as ColorFormatModel; - KeyValuePair oldProperties = (KeyValuePair)ColorFormatDialog.Tag; - modifiedColorFormat.Name = oldProperties.Key; - modifiedColorFormat.Format = oldProperties.Value; - } - - ColorFormatDialog.Hide(); - } - private async void EditButton_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e) { SettingsCard btn = sender as SettingsCard; @@ -162,5 +148,16 @@ public void RefreshEnabledState() { ViewModel.RefreshEnabledState(); } + + private void ColorFormatDialog_Closed(ContentDialog sender, ContentDialogClosedEventArgs args) + { + if (args.Result != ContentDialogResult.Primary && ColorFormatDialog.Tag is KeyValuePair) + { + ColorFormatModel modifiedColorFormat = ColorFormatDialog.DataContext as ColorFormatModel; + KeyValuePair oldProperties = (KeyValuePair)ColorFormatDialog.Tag; + modifiedColorFormat.Name = oldProperties.Key; + modifiedColorFormat.Format = oldProperties.Value; + } + } } } diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/FancyZonesPage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/FancyZonesPage.xaml index de957df76cf..db1d3bb29ea 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/FancyZonesPage.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/FancyZonesPage.xaml @@ -171,6 +171,8 @@ IsExpanded="True"> + + diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/PowerLauncherPage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/PowerLauncherPage.xaml index 94fd68d5bc7..9e62c422065 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/PowerLauncherPage.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/PowerLauncherPage.xaml @@ -360,6 +360,7 @@ + - + CornerRadius="0" + Visibility="{x:Bind Path=ShowCheckBox, Converter={StaticResource BoolToVisibilityConverter}}"> + + + + + + + + + + + + + + + + + + IsChecked="{x:Bind Path=Value, Mode=TwoWay}" /> + + + + + + + + - - + IsChecked="{x:Bind Path=Value, Mode=TwoWay}" /> + + + + + + + + + + + + + + + Security key - Chose the encoding of the hosts file + Choose the encoding of the hosts file "Hosts" refers to the system hosts file, do not loc diff --git a/src/settings-ui/Settings.UI/ViewModels/ColorPickerViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/ColorPickerViewModel.cs index c3d7be6e709..fc4b4a84b1e 100644 --- a/src/settings-ui/Settings.UI/ViewModels/ColorPickerViewModel.cs +++ b/src/settings-ui/Settings.UI/ViewModels/ColorPickerViewModel.cs @@ -300,13 +300,16 @@ private void ColorFormats_CollectionChanged(object sender, System.Collections.Sp UpdateColorFormats(); UpdateColorFormatPreview(); - ScheduleSavingOfSettings(); } private void ColorFormat_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) { - UpdateColorFormats(); - ScheduleSavingOfSettings(); + // Remaining properties are handled by the collection and by the dialog + if (e.PropertyName == nameof(ColorFormatModel.IsShown)) + { + UpdateColorFormats(); + ScheduleSavingOfSettings(); + } } private void ScheduleSavingOfSettings() @@ -437,6 +440,7 @@ internal void UpdateColorFormat(string oldName, ColorFormatModel colorFormat) SelectedColorRepresentationValue = colorFormat.Name; // name might be changed by the user } + UpdateColorFormats(); UpdateColorFormatPreview(); } diff --git a/src/settings-ui/Settings.UI/ViewModels/GeneralViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/GeneralViewModel.cs index f16584c6c89..afee54379d6 100644 --- a/src/settings-ui/Settings.UI/ViewModels/GeneralViewModel.cs +++ b/src/settings-ui/Settings.UI/ViewModels/GeneralViewModel.cs @@ -703,7 +703,7 @@ public void NotifyPropertyChanged([CallerMemberName] string propertyName = null, } /// - /// Method SelectSettingBackupDir opens folder browser to select a backup and retore location. + /// Method SelectSettingBackupDir opens folder browser to select a backup and restore location. /// private async void SelectSettingBackupDir() { diff --git a/src/settings-ui/Settings.UI/ViewModels/PluginAdditionalOptionViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/PluginAdditionalOptionViewModel.cs index 4e625d56d95..4463b1287b8 100644 --- a/src/settings-ui/Settings.UI/ViewModels/PluginAdditionalOptionViewModel.cs +++ b/src/settings-ui/Settings.UI/ViewModels/PluginAdditionalOptionViewModel.cs @@ -18,10 +18,19 @@ internal PluginAdditionalOptionViewModel(PluginAdditionalOption additionalOption _additionalOption = additionalOption; } + // Labels of single and first setting of combined types public string DisplayLabel => _additionalOption.DisplayLabel; public string DisplayDescription => _additionalOption.DisplayDescription; + // Labels of second setting of combined types + public string SecondDisplayLabel => _additionalOption.SecondDisplayLabel; + + public string SecondDisplayDescription => _additionalOption.SecondDisplayDescription; + + // Bool checkbox setting + public bool ShowCheckBox => _additionalOption.PluginOptionType == PluginAdditionalOption.AdditionalOptionType.Checkbox; + public bool Value { get => _additionalOption.Value; @@ -31,29 +40,83 @@ public bool Value { _additionalOption.Value = value; NotifyPropertyChanged(); + NotifyPropertyChanged(nameof(SecondSettingIsEnabled)); } } } + // ComboBox setting + public bool ShowComboBox => _additionalOption.PluginOptionType == PluginAdditionalOption.AdditionalOptionType.Combobox && + _additionalOption.ComboBoxOptions != null && _additionalOption.ComboBoxOptions.Count > 0; + public List ComboBoxOptions => _additionalOption.ComboBoxOptions; - public int Option + public int ComboBoxValue + { + get => _additionalOption.ComboBoxValue; + set + { + if (value != _additionalOption.ComboBoxValue) + { + _additionalOption.ComboBoxValue = value; + NotifyPropertyChanged(); + } + } + } + + // TextBox setting + public bool ShowTextBox => _additionalOption.PluginOptionType == PluginAdditionalOption.AdditionalOptionType.Textbox; + + public int TextBoxMaxLength => (_additionalOption.TextBoxMaxLength == null) ? 0 : _additionalOption.TextBoxMaxLength.Value; // 0 is the default and means no limit. + + public string TextValue { - get => _additionalOption.Option; + get => _additionalOption.TextValue; set { - if (value != _additionalOption.Option) + if (value != _additionalOption.TextValue) { - _additionalOption.Option = value; + _additionalOption.TextValue = value; NotifyPropertyChanged(); } } } - public bool ShowComboBox => _additionalOption.SelectionTypeValue == (int)PluginAdditionalOption.SelectionType.Combobox && _additionalOption.ComboBoxOptions != null && _additionalOption.ComboBoxOptions.Count > 0; + // NumberBox setting + public bool ShowNumberBox => _additionalOption.PluginOptionType == PluginAdditionalOption.AdditionalOptionType.Numberbox; + + public double NumberBoxMin => (_additionalOption.NumberBoxMin == null) ? double.MinValue : _additionalOption.NumberBoxMin.Value; + + public double NumberBoxMax => (_additionalOption.NumberBoxMax == null) ? double.MaxValue : _additionalOption.NumberBoxMax.Value; + + public double NumberBoxSmallChange => (_additionalOption.NumberBoxSmallChange == null) ? 1 : _additionalOption.NumberBoxSmallChange.Value; + + public double NumberBoxLargeChange => (_additionalOption.NumberBoxLargeChange == null) ? 10 : _additionalOption.NumberBoxLargeChange.Value; + + public double NumberValue + { + get => _additionalOption.NumberValue; + set + { + if (value != _additionalOption.NumberValue) + { + _additionalOption.NumberValue = value; + NotifyPropertyChanged(); + } + } + } + + // Show combined settings cards + public bool ShowCheckboxAndCombobox => _additionalOption.PluginOptionType == PluginAdditionalOption.AdditionalOptionType.CheckboxAndCombobox; + + public bool ShowCheckboxAndTextbox => _additionalOption.PluginOptionType == PluginAdditionalOption.AdditionalOptionType.CheckboxAndTextbox; + + public bool ShowCheckboxAndNumberbox => _additionalOption.PluginOptionType == PluginAdditionalOption.AdditionalOptionType.CheckboxAndNumberbox; - public bool ShowCheckBox => _additionalOption.SelectionTypeValue == (int)PluginAdditionalOption.SelectionType.Checkbox; + // Enabled state of ComboBox, TextBox, NumberBox (If combined with checkbox then checkbox value decides it.) + public bool SecondSettingIsEnabled => (int)_additionalOption.PluginOptionType > 10 ? _additionalOption.Value : true; + // Handle property changes public event PropertyChangedEventHandler PropertyChanged; private void NotifyPropertyChanged([CallerMemberName] string propertyName = "")