diff --git a/.github/actions/spell-check/excludes.txt b/.github/actions/spell-check/excludes.txt
index 3ffc4199f3..b7f4d46897 100644
--- a/.github/actions/spell-check/excludes.txt
+++ b/.github/actions/spell-check/excludes.txt
@@ -104,6 +104,8 @@
^src/common/ManagedCommon/ColorFormatHelper\.cs$
^src/common/notifications/BackgroundActivatorDLL/cpp\.hint$
^src/common/sysinternals/Eula/
+^src/modules/cmdpal/Tests/Microsoft\.CommandPalette\.Extensions\.Toolkit\.UnitTests/FuzzyMatcherComparisonTests.cs$
+^src/modules/cmdpal/Tests/Microsoft\.CommandPalette\.Extensions\.Toolkit\.UnitTests/FuzzyMatcherDiacriticsTests.cs$
^src/modules/cmdpal/doc/initial-sdk-spec/list-elements-mock-002\.pdn$
^src/modules/cmdpal/ext/SamplePagesExtension/Pages/SampleMarkdownImagesPage\.cs$
^src/modules/cmdpal/Microsoft\.CmdPal\.UI/Settings/InternalPage\.SampleData\.cs$
diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt
index 25c714a03f..d94db89953 100644
--- a/.github/actions/spell-check/expect.txt
+++ b/.github/actions/spell-check/expect.txt
@@ -635,6 +635,7 @@ GMEM
GNumber
googleai
googlegemini
+Gotchas
gpedit
gpo
GPOCA
@@ -892,9 +893,9 @@ Lclean
Ldone
Ldr
LEFTALIGN
+leftclick
LEFTSCROLLBAR
LEFTTEXT
-leftclick
LError
LEVELID
LExit
@@ -1020,9 +1021,12 @@ MENUITEMINFO
MENUITEMINFOW
MERGECOPY
MERGEPAINT
+Metacharacter
metadatamatters
Metadatas
+Metacharacter
metafile
+Metacharacter
mfc
Mgmt
Microwaved
@@ -1069,7 +1073,7 @@ mouseutils
MOVESIZEEND
MOVESIZESTART
MRM
-MRT
+Mrt
mru
MSAL
msc
@@ -1487,7 +1491,9 @@ regfile
REGISTERCLASSFAILED
REGISTRYHEADER
REGISTRYPREVIEWEXT
+registryroot
regkey
+regroot
regsvr
REINSTALLMODE
releaseblog
@@ -1826,6 +1832,7 @@ TEXTBOXNEWLINE
textextractor
TEXTINCLUDE
tfopen
+tgamma
tgz
THEMECHANGED
themeresources
@@ -2167,4 +2174,4 @@ Zoneszonabletester
Zoomin
zoomit
ZOOMITX
-Zorder
+Zorder
\ No newline at end of file
diff --git a/.pipelines/v2/release.yml b/.pipelines/v2/release.yml
index a9799dc031..d3adc45f04 100644
--- a/.pipelines/v2/release.yml
+++ b/.pipelines/v2/release.yml
@@ -91,6 +91,7 @@ extends:
official: true
codeSign: true
runTests: false
+ buildTests: false
signingIdentity:
serviceName: $(SigningServiceName)
appId: $(SigningAppId)
diff --git a/.pipelines/v2/templates/job-build-project.yml b/.pipelines/v2/templates/job-build-project.yml
index fb321f6f2f..e41bfbc0ad 100644
--- a/.pipelines/v2/templates/job-build-project.yml
+++ b/.pipelines/v2/templates/job-build-project.yml
@@ -258,6 +258,7 @@ jobs:
-restore -graph
/p:RestorePackagesConfig=true
/p:CIBuild=true
+ /p:BuildTests=${{ parameters.buildTests }}
/bl:$(LogOutputDirectory)\build-0-main.binlog
${{ parameters.additionalBuildOptions }}
$(MSBuildCacheParameters)
diff --git a/.pipelines/v2/templates/pipeline-ci-build.yml b/.pipelines/v2/templates/pipeline-ci-build.yml
index 0ef570d0c8..a56c575399 100644
--- a/.pipelines/v2/templates/pipeline-ci-build.yml
+++ b/.pipelines/v2/templates/pipeline-ci-build.yml
@@ -59,6 +59,7 @@ stages:
enableMsBuildCaching: ${{ parameters.enableMsBuildCaching }}
msBuildCacheIsReadOnly: ${{ parameters.msBuildCacheIsReadOnly }}
runTests: ${{ parameters.runTests }}
+ buildTests: true
useVSPreview: ${{ parameters.useVSPreview }}
useLatestWinAppSDK: ${{ parameters.useLatestWinAppSDK }}
${{ if eq(parameters.useLatestWinAppSDK, true) }}:
@@ -78,7 +79,9 @@ stages:
${{ else }}:
name: SHINE-OSS-L
${{ if eq(parameters.useVSPreview, true) }}:
- demands: ImageOverride -equals SHINE-VS17-Preview
+ demands: ImageOverride -equals SHINE-VS18-Preview
+ ${{ else }}:
+ demands: ImageOverride -equals SHINE-VS18-Latest
buildConfigurations: [Release]
official: false
codeSign: false
diff --git a/.pipelines/verifyNoticeMdAgainstNugetPackages.ps1 b/.pipelines/verifyNoticeMdAgainstNugetPackages.ps1
index af9ab8ff6f..a5cf73e6e9 100644
--- a/.pipelines/verifyNoticeMdAgainstNugetPackages.ps1
+++ b/.pipelines/verifyNoticeMdAgainstNugetPackages.ps1
@@ -90,9 +90,15 @@ if ($noticeMatch.Success) {
$currentNoticePackageList = ""
}
+# Test-only packages that are allowed to be in NOTICE.md but not in the build
+# (e.g., when BuildTests=false, these packages won't appear in the NuGet list)
+$allowedExtraPackages = @(
+ "- Moq"
+)
+
if (!$noticeFile.Trim().EndsWith($returnList.Trim()))
{
- Write-Host -ForegroundColor Red "Notice.md does not match NuGet list."
+ Write-Host -ForegroundColor Yellow "Notice.md does not exactly match NuGet list. Analyzing differences..."
# Show detailed differences
$generatedPackages = $returnList -split "`r`n|`n" | Where-Object { $_.Trim() -ne "" } | Sort-Object
@@ -105,7 +111,7 @@ if (!$noticeFile.Trim().EndsWith($returnList.Trim()))
# Find packages in proj file list but not in NOTICE.md
$missingFromNotice = $generatedPackages | Where-Object { $noticePackages -notcontains $_ }
if ($missingFromNotice.Count -gt 0) {
- Write-Host -ForegroundColor Red "MissingFromNotice:"
+ Write-Host -ForegroundColor Red "MissingFromNotice (ERROR - these must be added to NOTICE.md):"
foreach ($pkg in $missingFromNotice) {
Write-Host -ForegroundColor Red " $pkg"
}
@@ -114,10 +120,23 @@ if (!$noticeFile.Trim().EndsWith($returnList.Trim()))
# Find packages in NOTICE.md but not in proj file list
$extraInNotice = $noticePackages | Where-Object { $generatedPackages -notcontains $_ }
- if ($extraInNotice.Count -gt 0) {
- Write-Host -ForegroundColor Yellow "ExtraInNotice:"
- foreach ($pkg in $extraInNotice) {
- Write-Host -ForegroundColor Yellow " $pkg"
+
+ # Filter out allowed extra packages (test-only dependencies)
+ $unexpectedExtra = $extraInNotice | Where-Object { $allowedExtraPackages -notcontains $_ }
+ $allowedExtra = $extraInNotice | Where-Object { $allowedExtraPackages -contains $_ }
+
+ if ($allowedExtra.Count -gt 0) {
+ Write-Host -ForegroundColor Green "ExtraInNotice (OK - allowed test-only packages):"
+ foreach ($pkg in $allowedExtra) {
+ Write-Host -ForegroundColor Green " $pkg"
+ }
+ Write-Host ""
+ }
+
+ if ($unexpectedExtra.Count -gt 0) {
+ Write-Host -ForegroundColor Red "ExtraInNotice (ERROR - unexpected packages in NOTICE.md):"
+ foreach ($pkg in $unexpectedExtra) {
+ Write-Host -ForegroundColor Red " $pkg"
}
Write-Host ""
}
@@ -127,10 +146,17 @@ if (!$noticeFile.Trim().EndsWith($returnList.Trim()))
Write-Host " Proj file list has $($generatedPackages.Count) packages"
Write-Host " NOTICE.md has $($noticePackages.Count) packages"
Write-Host " MissingFromNotice: $($missingFromNotice.Count) packages"
- Write-Host " ExtraInNotice: $($extraInNotice.Count) packages"
+ Write-Host " ExtraInNotice (allowed): $($allowedExtra.Count) packages"
+ Write-Host " ExtraInNotice (unexpected): $($unexpectedExtra.Count) packages"
Write-Host ""
- exit 1
+ # Fail if there are missing packages OR unexpected extra packages
+ if ($missingFromNotice.Count -gt 0 -or $unexpectedExtra.Count -gt 0) {
+ Write-Host -ForegroundColor Red "FAILED: NOTICE.md mismatch detected."
+ exit 1
+ } else {
+ Write-Host -ForegroundColor Green "PASSED: NOTICE.md matches (with allowed test-only packages)."
+ }
}
exit 0
diff --git a/Cpp.Build.props b/Cpp.Build.props
index 4b8a206306..5acfbdee1a 100644
--- a/Cpp.Build.props
+++ b/Cpp.Build.props
@@ -2,6 +2,12 @@
+
+
+ false
+ false
+
+
diff --git a/Directory.Build.props b/Directory.Build.props
index e7b415cbca..99379ecefc 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -19,6 +19,39 @@
$(Platform)
+
+
+ <_ProjectName>$(MSBuildProjectName)
+
+ <_IsSkippedTestProject Condition="$(_ProjectName.EndsWith('Test'))">true
+ <_IsSkippedTestProject Condition="$(_ProjectName.EndsWith('Tests'))">true
+
+ <_IsSkippedTestProject Condition="$(_ProjectName.StartsWith('UnitTests-'))">true
+ <_IsSkippedTestProject Condition="$(_ProjectName.StartsWith('UITest-'))">true
+
+ <_IsSkippedTestProject Condition="$(MSBuildProjectDirectory.Contains('\Tests\'))">true
+
+
+
+ false
+ false
+ false
+ false
+ disable
+
+ false
+ false
+ false
+ false
+
+
$(Version).0https://github.com/microsoft/PowerToys
@@ -30,7 +63,9 @@
<_PropertySheetDisplayName>PowerToys.Root.Props
$(MsbuildThisFileDirectory)\Cpp.Build.props
-
+
+
+ allruntime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/Directory.Build.targets b/Directory.Build.targets
index ab9bad297e..9efab5a9a5 100644
--- a/Directory.Build.targets
+++ b/Directory.Build.targets
@@ -28,4 +28,41 @@
$(NoWarn);CS8305;SA1500;CA1852
-
\ No newline at end of file
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/PowerToys.slnx b/PowerToys.slnx
index 8a166bb32e..1dc26be394 100644
--- a/PowerToys.slnx
+++ b/PowerToys.slnx
@@ -360,6 +360,10 @@
+
+
+
+
diff --git a/doc/devdocs/development/new-powertoy.md b/doc/devdocs/development/new-powertoy.md
new file mode 100644
index 0000000000..1e0f7bccfa
--- /dev/null
+++ b/doc/devdocs/development/new-powertoy.md
@@ -0,0 +1,311 @@
+# 🧭 Creating a new PowerToy: end-to-end developer guide
+
+First of all, thank you for wanting to contribute to PowerToys. The work we do would not be possible without the support of community supporters like you.
+
+This guide documents the process of building a new PowerToys utility from scratch, including architecture decisions, integration steps, and common pitfalls.
+
+---
+
+## 1. Overview and prerequisites
+
+A PowerToy module is a self-contained utility integrated into the PowerToys ecosystem. It can be UI-based, service-based, or both.
+
+### Requirements
+
+- [Visual Studio 2026](https://visualstudio.microsoft.com/downloads/) and the following workloads/individual components:
+ - Desktop Development with C++
+ - WinUI application development
+ - .NET desktop development
+ - Windows 10 SDK (10.0.22621.0)
+ - Windows 11 SDK (10.0.26100.3916)
+- .NET 8 SDK
+- Fork the [PowerToys repository](https://github.com/microsoft/PowerToys/tree/main) locally
+- [Validate that you are able to build and run](https://github.com/microsoft/PowerToys/blob/main/doc/devdocs/development/debugging.md) `PowerToys.slnx`.
+
+Optional:
+- [WiX v5 toolset](https://github.com/microsoft/PowerToys/tree/main) for the installer
+
+> [!NOTE]
+> To ensure all the correct VS Workloads are installed, use [the WinGet configuration files](https://github.com/microsoft/PowerToys/tree/e13d6a78aafbcf32a4bb5f8581d041e1d057c3f1/.config) in the project repository. (Use the one that matches your VS distribution. ie: VS Community would use `configuration.winget`)
+
+### Folder structure
+
+```
+src/
+ modules/
+ your_module/
+ YourModule.sln
+ YourModuleInterface/
+ YourModuleUI/ (if needed)
+ YourModuleService/ (if needed)
+```
+
+---
+## 2. Design and planning
+
+### Decide the type of module
+
+Think about how your module works and which existing modules behave similarly. You are going to want to think about the UI needed for the application, the lifecycle, whether it is a service that is always running or event based. Below are some basic scenarios with some modules to explore. You can write your application in C++ or C#.
+- **UI-only:** e.g., ColorPicker
+- **Background service:** e.g., LightSwitch, Awake
+- **Hybrid (UI + background logic):** e.g., ShortcutGuide
+- **C++/C# interop:** e.g., PowerRename
+
+### Write your module interface
+
+Begin by setting up the [PowerToy module template project](https://github.com/microsoft/PowerToys/tree/main/tools/project_template). This will generate boilerplate for you to begin your new module. Below are the key headers in the Module Interface (`dllmain.cpp`) and an explanation of their purpose:
+1. This is where module settings are defined. These can be anything from strings, bools, ints, and even custom Enums.
+```c++
+struct ModuleSettings {};
+```
+
+2. This is the header for the full class. It inherits the PowerToyModuleIface
+```c++
+class ModuleInterface : public PowertoyModuleIface
+{
+ private:
+ // the private members of the class
+ // Can include the enabled variable, logic for event handlers, or hotkeys.
+ public:
+ // the public members of the class
+ // Will include the constructor and initialization logic.
+}
+```
+
+> [!NOTE]
+> Many of the class functions are boilerplate and need simple string replacements with your module name. The rest of the functions below will require bigger changes.
+
+3. GPO stands for "Group Policy Object" and allows for administrators to configure settings across a network of machines. It is required that your module is on this list of settings. You can right click the `powertoys_gpo` object to go to the definition and set up the `getConfiguredModuleEnabledValue` for your module.
+```c++
+virtual powertoys_gpo::gpo_rule_configured_t gpo_policy_enabled_configuration() override
+{
+ return powertoys_gpo::getConfiguredModuleEnabledValue();
+}
+```
+
+4. `init_settings()` initializes the settings for the interface. Will either pull from existing settings.json or use defaults.
+```c++
+void ModuleInterface::init_settings()
+```
+
+5. `get_config` retrieves the settings from the settings.json file.
+```c++
+virtual bool get_config(wchar_t* buffer, int* buffer_size) override
+```
+
+6. `set_config` sets the new settings to the settings.json file.
+```c++
+virtual void set_config(const wchar_t* config) override
+```
+
+7. `call_custom_action` allows custom actions to be called based on signals from the settings app.
+```c++
+void call_custom_action(const wchar_t* action) override
+```
+
+8. Lifecycle events control whether the module is enabled or not, as well as the default status of the module.
+```c++
+virtual void enable() // starts the module
+virtual void disable() // terminates the module and performs any cleanup
+virtual bool is_enabled() // returns if the module is currently enabled
+virtual bool is_enabled_by_default() const override // allows the module to dictate whether it should be enabled by default in the PowerToys app.
+```
+
+9. Hotkey functions control the status of the hotkey.
+```c++
+// takes the hotkey from settings into a format that the interface can understand
+void parse_hotkey(PowerToysSettings::PowerToyValues& settings)
+
+// returns the hotkeys from settings
+virtual size_t get_hotkeys(Hotkey* hotkeys, size_t buffer_size) override
+
+// performs logic when the hotkey event is fired
+virtual bool on_hotkey(size_t hotkeyId) override
+```
+
+### Notes
+
+- Keep module logic isolated under `/modules/`
+- Use shared utilities from [`common`](https://github.com/microsoft/PowerToys/tree/main/src/common) instead of cross-module dependencies
+- init/set/get config use preset functions to access the settings. Check out the [`settings_objects.h`](https://github.com/microsoft/PowerToys/blob/main/src/common/SettingsAPI/settings_helpers.h) in `src\common\SettingsAPI`
+
+---
+## 3. Bootstrapping your module
+
+1. Use the [template](https://github.com/microsoft/PowerToys/tree/main/tools/project_template) to generate the module interface starter code.
+2. Update all projects and namespaces with your module name.
+3. Update GUIDs in `.vcxproj` and solution files.
+4. Update the functions mentioned in the above section with your custom logic.
+5. In order for your module to be detected by the runner you are required to add references to various lists. In order to register your module, add the corresponding module reference to the lists that can be found in the following files. (Hint: search other modules names to find the lists quicker)
+ - `src/runner/modules.h`
+ - `src/runner/modules.cpp`
+ - `src/runner/resource.h`
+ - `src/runner/settings_window.h`
+ - `src/runner/settings_window.cpp`
+ - `src/runner/main.cpp`
+ - `src/common/logger.h` (for logging)
+6. ModuleInterface should build your `ModuleInterface.dll`. This will allow the runner to interact with your service.
+
+> [!TIP]
+> Mismatched module IDs are one of the most common causes of load failures. Keep your ID consistent across manifest, registry, and service.
+
+---
+## 4. Write your service
+
+This is going to look different for every PowerToy. It may be easier to develop the application independently, and then link in the PowerToys settings logic later. But you have to write the service first, before connecting it to the runner.
+
+### Notes
+
+- This is a separate project from the Module Interface.
+- You can develop this project using C# or C++.
+- Set the service icon using the `.rc` file.
+- Set the service name in the `.vcxproj` by setting the ``
+```
+
+ ..\..\..\..\$(Platform)\$(Configuration)\$(MSBuildProjectName)\
+ PowerToys.LightSwitchService
+
+```
+- To view the code of the `.vcxproj`, right click the item and select **Unload project**
+- Use the following functions to interact with settings from your service
+```
+ModuleSettings::instance().InitFileWatcher();
+ModuleSettings::instance().LoadSettings();
+auto& settings = ModuleSettings::instance().settings();
+```
+These come from the `ModuleSettings.h` file that lives with the Service. You can copy this from another module (e.g., Light Switch) and adjust to fit your needs.
+
+If your module has a user interface:
+- Use the **WinUI Blank App** template when setting up your project
+- Use [Windows design best practices](https://learn.microsoft.com/windows/apps/design/basics/)
+- Use the [WinUI 3 Gallery](https://apps.microsoft.com/detail/9p3jfpwwdzrc) for help with your UI code, and additional guidance.
+
+## 5. Settings integration
+
+PowerToys settings are stored per-module as JSON under:
+
+```
+%LOCALAPPDATA%\Microsoft\PowerToys\\settings.json
+```
+
+### Implementation steps
+
+- In `src\settings-ui\Settings.UI.Library\` create `Properties.cs` and `Settings.cs`
+- `Properties.cs` is where you will define your defaults. Every setting needs to be represented here. This should match what was set in the Module Interface.
+- `Settings.cs`is where your settings.json will be built from. The structure should match the following
+```cs
+public ModuleSettings()
+{
+ Name = ModuleName;
+ Version = Assembly.GetExecutingAssembly().GetName().Version.ToString();
+ Properties = new ModuleProperties(); // settings properties you set above.
+}
+```
+
+- In `src\settings-ui\Settings.UI\ViewModels` create `ViewModel.cs` this is where the interaction happens between your settings page in the PowerToys app and the settings file that is stored on the device. Changes here will trigger the settings watcher via a `NotifyPropertyChanged` event.
+- Create a `SettingsPage.xaml` at `src\settings-ui\Settings.UI\SettingsXAML\Views`. This will be the page where the user interacts with the settings of your module.
+- Be sure to use resource strings for user facing strings so they can be localized. (`x:Uid` connects to Resources.resw)
+```xaml
+// LightSwitch.xaml
+
+
+// Resources.resw
+
+ Off
+
+```
+> [!IMPORTANT]
+> In the above example we use `.Content` to target the content of the Combobox. This can change per UI element (e.g., `.Text`, `.Header`, etc.)
+
+> **Reminder:** Manual changes via external editors (VS Code, Notepad) do **not** trigger the settings watcher. Only changes written through PowerToys trigger reloads.
+
+---
+
+### Gotchas:
+
+- Only use the WinUI 3 framework, _not_ UWP.
+- Use [`DispatcherQueue`](https://learn.microsoft.com/windows/apps/develop/dispatcherqueue) when updating UI from non-UI threads.
+
+---
+## 6. Building and debugging
+
+### Debugging steps
+
+1. If this is your first time debugging PowerToys, be sure to follow [these steps first](https://github.com/microsoft/PowerToys/blob/main/doc/devdocs/development/debugging.md#pre-debugging-setup).
+2. Set "runner" as the start up project and ensure your build configuration is set to match your system (ARM64/x64)
+3. Select F5 or the **Local Windows Debugger** button to begin debugging. This should start the PowerToys runner.
+4. To set breakpoints in your service, select Ctrl+Alt+P and search for your service to attach to the runner.
+5. Use logs to document changes. The logs live at `%LOCALAPPDATA%\Microsoft\PowerToys\RunnerLogs` and `%LOCALAPPDATA%\Microsoft\PowerToys\Module\Service\` for the specific module.
+
+> [!TIP]
+> PowerToys caches `.nuget` artifacts aggressively. Use `git clean -xfd` when builds behave unexpectedly.
+
+---
+## 7. Installer and packaging (WiX)
+
+### Add your module to installer
+
+1. Install [`WixToolset.Heat`](https://www.nuget.org/packages/WixToolset.Heat/) for Wix5 via nuget
+2. Inside `installer\PowerToysInstallerVNext` add a new file for your module: `Module.wxs`
+3. Inside of this file you will need copy the format from another module (ie: Light Switch) and replace the strings and GUID values.
+4. The key part will be `` which is a placeholder for code that will be generated by `generateFileComponents.ps1`.
+5. Inside `Product.wxs` add a line item in the `` section. It will look like a list of ` ` items.
+6. Inside `generateFileComponents.ps1` you will need to add an entry to the bottom for your new module. It will follow the following format. `-fileListName Files` will match the string you set in `Module.wxs`, `` will match the name of your exe.
+```bash
+# Module Name
+Generate-FileList -fileDepsJson "" -fileListName Files -wxsFilePath $PSScriptRoot\.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\"
+Generate-FileComponents -fileListName "Files" -wxsFilePath $PSScriptRoot\.wxs -regroot $registryroot
+```
+---
+## 8. Testing and validation
+
+### UI tests
+
+- Place under `/modules//Tests`
+- Create a new [WinUI Unit Test App](https://learn.microsoft.com/windows/apps/winui/winui3/testing/create-winui-unit-test-project)
+- Write unit tests following the format from previous modules (ie: Light Switch). This can be to test your standalone UI (if you're a module like Color Picker) or to verify that the Settings UI in the PowerToys app is controlling your service.
+
+### Manual validation
+
+- Enable/disable in PowerToys Settings
+- Check initialization in logs
+- Confirm icons, tooltips, and OOBE page appear correctly
+
+### Pro tips
+
+1. Validate wake/sleep and elevation states. Background modules often fail silently after resume if event handles aren’t recreated.
+2. Use Windows Sandbox to simulate clean install environments
+3. To simulate a "new user" you can delete the PowerToys folder from `%LOCALAPPDATA%\Microsoft`
+
+### Shortcut conflict detection
+
+If your module has a shortcut, ensure that it is properly registered following [the steps listed in the documentation](https://github.com/microsoft/PowerToys/blob/main/doc/devdocs/core/settings/settings-implementation.md#shortcut-conflict-detection) for conflict detection.
+
+---
+## 9. The final touches
+
+### Out-of-Box experience (OOBE) page
+
+The OOBE page is a custom settings page that gives the user at a glance information about each module. This window opens before the Settings application for new users and after updates. Create `OOBE.xaml` at `src\settings-ui\Settings.UI\SettingsXAML\OOBE\Views`. You will also need to add your module name to the enum at `src\settings-ui\Settings.UI\OOBE\Enums\PowerToysModules.cs`.
+
+### Module assets
+
+Now that your PowerToy is _done_ you can start to think about the assets that will represent your module.
+- Module Icon: This will be displayed in a number of places: OOBE page, in the README, on the home screen of PowerToys, on your individual module settings page, etc.
+- Module Image: This is the image you see at the top of each individual settings page.
+- OOBE Image: This is the header you see on the OOBE page for each module
+
+> [!NOTE]
+> This step is something that the Design team will handle internally to ensure consistency throughout the application. If you have ideas or recommendations on what the icon or screenshots should be for your module feel free to leave it in the "Additional Comments" section of the PR and the team will take it into consideration.
+
+### Documentation
+
+There are two types of documentation that will be required when submitting a new PowerToy:
+1. Developer documentation: This will live in the [PowerToys repo](https://github.com/microsoft/PowerToys/blob/main/doc/devdocs/modules) at `/doc/devdocs/modules/` and should tell a developer how to work on your app. It should outline the module architecture, key files, testing, and tips on debugging if necessary.
+2. Microsoft Learn documentation: When your new Module is ready to be merged into the PowerToys repository, an internal team member will create Microsoft Learn documentation so that users will understand how to use your module. There is not much work on your end as the developer for this step, but keep an eye on your PR in case we need more information about your PowerToy for this step.
+
+---
+Thank you again for contributing! If you need help, feel free to [open an issue](https://github.com/microsoft/PowerToys/issues/new/choose) and use the `Needs-Team-Response` label so we know you need attention.
diff --git a/doc/devdocs/modules/advancedpaste.md b/doc/devdocs/modules/advancedpaste.md
index e23fde3a8a..b2ab244432 100644
--- a/doc/devdocs/modules/advancedpaste.md
+++ b/doc/devdocs/modules/advancedpaste.md
@@ -18,13 +18,28 @@ Advanced Paste is a PowerToys module that provides enhanced clipboard pasting wi
TODO: Add implementation details
+### Paste with AI Preview
+
+The "Show preview" setting (`ShowCustomPreview`) controls whether AI-generated results are displayed in a preview window before pasting. **The preview feature does not consume additional AI credits**—the preview displays the same AI response that was already generated, cached locally from a single API call.
+
+The implementation flow:
+1. User initiates "Paste with AI" action
+2. A single AI API call is made via `ExecutePasteFormatAsync`
+3. The result is cached in `GeneratedResponses`
+4. If preview is enabled, the cached result is displayed in the preview UI
+5. User can paste the cached result without any additional API calls
+
+See the `ExecutePasteFormatAsync(PasteFormat, PasteActionSource)` method in `OptionsViewModel.cs` for the implementation.
+
## Debugging
TODO: Add debugging information
## Settings
-TODO: Add settings documentation
+| Setting | Description |
+|---------|-------------|
+| `ShowCustomPreview` | When enabled, shows AI-generated results in a preview window before pasting. Does not affect AI credit consumption. |
## Future Improvements
diff --git a/installer/PowerToysSetupVNext/Resources.wxs b/installer/PowerToysSetupVNext/Resources.wxs
index 7e62a34be9..a392004320 100644
--- a/installer/PowerToysSetupVNext/Resources.wxs
+++ b/installer/PowerToysSetupVNext/Resources.wxs
@@ -367,6 +367,12 @@
+
+
+
+
+
+
diff --git a/src/common/CalculatorEngineCommon/ExprtkEvaluator.cpp b/src/common/CalculatorEngineCommon/ExprtkEvaluator.cpp
index dbc2120b24..eb04c18783 100644
--- a/src/common/CalculatorEngineCommon/ExprtkEvaluator.cpp
+++ b/src/common/CalculatorEngineCommon/ExprtkEvaluator.cpp
@@ -3,9 +3,27 @@
#include
#include
#include
+#include
+#include
namespace ExprtkCalculator::internal
{
+ static double factorial(const double n)
+ {
+ // Only allow non-negative integers
+ if (n < 0.0 || std::floor(n) != n)
+ {
+ return std::numeric_limits::quiet_NaN();
+ }
+ return std::tgamma(n + 1.0);
+ }
+
+ static double sign(const double n)
+ {
+ if (n > 0.0) return 1.0;
+ if (n < 0.0) return -1.0;
+ return 0.0;
+ }
std::wstring ToWStringFullPrecision(double value)
{
@@ -25,6 +43,9 @@ namespace ExprtkCalculator::internal
symbol_table.add_constant(name, value);
}
+ symbol_table.add_function("factorial", factorial);
+ symbol_table.add_function("sign", sign);
+
exprtk::expression expression;
expression.register_symbol_table(symbol_table);
diff --git a/src/common/interop/shared_constants.h b/src/common/interop/shared_constants.h
index 118683f24c..3dad776cf6 100644
--- a/src/common/interop/shared_constants.h
+++ b/src/common/interop/shared_constants.h
@@ -72,6 +72,10 @@ namespace CommonSharedConstants
const wchar_t ALWAYS_ON_TOP_TERMINATE_EVENT[] = L"Local\\AlwaysOnTopTerminateEvent-cfdf1eae-791f-4953-8021-2f18f3837eae";
+ const wchar_t ALWAYS_ON_TOP_INCREASE_OPACITY_EVENT[] = L"Local\\AlwaysOnTopIncreaseOpacityEvent-a1b2c3d4-e5f6-7890-abcd-ef1234567890";
+
+ const wchar_t ALWAYS_ON_TOP_DECREASE_OPACITY_EVENT[] = L"Local\\AlwaysOnTopDecreaseOpacityEvent-b2c3d4e5-f6a7-8901-bcde-f12345678901";
+
// Path to the event used by PowerAccent
const wchar_t POWERACCENT_EXIT_EVENT[] = L"Local\\PowerToysPowerAccentExitEvent-53e93389-d19a-4fbb-9b36-1981c8965e17";
diff --git a/src/modules/Workspaces/WorkspacesEditor/OverlayWindow.xaml.cs b/src/modules/Workspaces/WorkspacesEditor/OverlayWindow.xaml.cs
index 54e892d9bd..d1646e7282 100644
--- a/src/modules/Workspaces/WorkspacesEditor/OverlayWindow.xaml.cs
+++ b/src/modules/Workspaces/WorkspacesEditor/OverlayWindow.xaml.cs
@@ -2,8 +2,11 @@
// 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.Windows;
+using WorkspacesEditor.Utils;
+
namespace WorkspacesEditor
{
///
@@ -11,9 +14,40 @@ namespace WorkspacesEditor
///
public partial class OverlayWindow : Window
{
+ private int _targetX;
+ private int _targetY;
+ private int _targetWidth;
+ private int _targetHeight;
+
public OverlayWindow()
{
InitializeComponent();
+ SourceInitialized += OnWindowSourceInitialized;
+ }
+
+ ///
+ /// Sets the target bounds for the overlay window.
+ /// The window will be positioned using DPI-unaware context after initialization.
+ ///
+ public void SetTargetBounds(int x, int y, int width, int height)
+ {
+ _targetX = x;
+ _targetY = y;
+ _targetWidth = width;
+ _targetHeight = height;
+
+ // Set initial WPF properties (will be corrected after HWND creation)
+ Left = x;
+ Top = y;
+ Width = width;
+ Height = height;
+ }
+
+ private void OnWindowSourceInitialized(object sender, EventArgs e)
+ {
+ // Reposition window using DPI-unaware context to match the virtual coordinates.
+ // This fixes overlay positioning on mixed-DPI multi-monitor setups.
+ NativeMethods.SetWindowPositionDpiUnaware(this, _targetX, _targetY, _targetWidth, _targetHeight);
}
}
}
diff --git a/src/modules/Workspaces/WorkspacesEditor/Utils/NativeMethods.cs b/src/modules/Workspaces/WorkspacesEditor/Utils/NativeMethods.cs
index 4105cbe959..9687aeac63 100644
--- a/src/modules/Workspaces/WorkspacesEditor/Utils/NativeMethods.cs
+++ b/src/modules/Workspaces/WorkspacesEditor/Utils/NativeMethods.cs
@@ -4,6 +4,8 @@
using System;
using System.Runtime.InteropServices;
+using System.Windows;
+using System.Windows.Interop;
namespace WorkspacesEditor.Utils
{
@@ -17,6 +19,39 @@ namespace WorkspacesEditor.Utils
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool MoveWindow(IntPtr hWnd, int X, int Y, int nWidth, int nHeight, bool bRepaint);
+ [DllImport("user32.dll", SetLastError = true)]
+ private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);
+
+ [DllImport("user32.dll")]
+ private static extern IntPtr SetThreadDpiAwarenessContext(IntPtr dpiContext);
+
+ private const uint SWP_NOZORDER = 0x0004;
+ private const uint SWP_NOACTIVATE = 0x0010;
+
+ private static readonly IntPtr DPI_AWARENESS_CONTEXT_UNAWARE = new IntPtr(-1);
+
+ ///
+ /// Positions a WPF window using DPI-unaware context to match the virtual coordinates.
+ /// This fixes overlay positioning on mixed-DPI multi-monitor setups.
+ ///
+ public static void SetWindowPositionDpiUnaware(Window window, int x, int y, int width, int height)
+ {
+ var helper = new WindowInteropHelper(window).Handle;
+ if (helper != IntPtr.Zero)
+ {
+ // Temporarily switch to DPI-unaware context to position window.
+ IntPtr oldContext = SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_UNAWARE);
+ try
+ {
+ SetWindowPos(helper, IntPtr.Zero, x, y, width, height, SWP_NOZORDER | SWP_NOACTIVATE);
+ }
+ finally
+ {
+ SetThreadDpiAwarenessContext(oldContext);
+ }
+ }
+ }
+
[DllImport("USER32.DLL")]
public static extern bool SetForegroundWindow(IntPtr hWnd);
diff --git a/src/modules/Workspaces/WorkspacesEditor/ViewModels/MainViewModel.cs b/src/modules/Workspaces/WorkspacesEditor/ViewModels/MainViewModel.cs
index 9c76c26fa0..5741fd65ab 100644
--- a/src/modules/Workspaces/WorkspacesEditor/ViewModels/MainViewModel.cs
+++ b/src/modules/Workspaces/WorkspacesEditor/ViewModels/MainViewModel.cs
@@ -495,10 +495,10 @@ namespace WorkspacesEditor.ViewModels
{
var bounds = screen.Bounds;
OverlayWindow overlayWindow = new OverlayWindow();
- overlayWindow.Top = bounds.Top;
- overlayWindow.Left = bounds.Left;
- overlayWindow.Width = bounds.Width;
- overlayWindow.Height = bounds.Height;
+
+ // Use DPI-unaware positioning to fix overlay on mixed-DPI multi-monitor setups
+ overlayWindow.SetTargetBounds(bounds.Left, bounds.Top, bounds.Width, bounds.Height);
+
overlayWindow.ShowActivated = true;
overlayWindow.Topmost = true;
overlayWindow.Show();
diff --git a/src/modules/alwaysontop/AlwaysOnTop/AlwaysOnTop.cpp b/src/modules/alwaysontop/AlwaysOnTop/AlwaysOnTop.cpp
index 8287ee1cce..f09cc2997f 100644
--- a/src/modules/alwaysontop/AlwaysOnTop/AlwaysOnTop.cpp
+++ b/src/modules/alwaysontop/AlwaysOnTop/AlwaysOnTop.cpp
@@ -153,9 +153,21 @@ LRESULT AlwaysOnTop::WndProc(HWND window, UINT message, WPARAM wparam, LPARAM lp
{
if (message == WM_HOTKEY)
{
+ int hotkeyId = static_cast(wparam);
if (HWND fw{ GetForegroundWindow() })
{
- ProcessCommand(fw);
+ if (hotkeyId == static_cast(HotkeyId::Pin))
+ {
+ ProcessCommand(fw);
+ }
+ else if (hotkeyId == static_cast(HotkeyId::IncreaseOpacity))
+ {
+ StepWindowTransparency(fw, Settings::transparencyStep);
+ }
+ else if (hotkeyId == static_cast(HotkeyId::DecreaseOpacity))
+ {
+ StepWindowTransparency(fw, -Settings::transparencyStep);
+ }
}
}
else if (message == WM_PRIV_SETTINGS_CHANGED)
@@ -191,6 +203,10 @@ void AlwaysOnTop::ProcessCommand(HWND window)
m_topmostWindows.erase(iter);
}
+ // Restore transparency when unpinning
+ RestoreWindowAlpha(window);
+ m_windowOriginalLayeredState.erase(window);
+
Trace::AlwaysOnTop::UnpinWindow();
}
}
@@ -200,6 +216,7 @@ void AlwaysOnTop::ProcessCommand(HWND window)
{
soundType = Sound::Type::On;
AssignBorder(window);
+
Trace::AlwaysOnTop::PinWindow();
}
}
@@ -269,11 +286,22 @@ void AlwaysOnTop::RegisterHotkey() const
{
if (m_useCentralizedLLKH)
{
+ // All hotkeys are handled by centralized LLKH
return;
}
+ // Register hotkeys only when not using centralized LLKH
UnregisterHotKey(m_window, static_cast(HotkeyId::Pin));
+ UnregisterHotKey(m_window, static_cast(HotkeyId::IncreaseOpacity));
+ UnregisterHotKey(m_window, static_cast(HotkeyId::DecreaseOpacity));
+
+ // Register pin hotkey
RegisterHotKey(m_window, static_cast(HotkeyId::Pin), AlwaysOnTopSettings::settings().hotkey.get_modifiers(), AlwaysOnTopSettings::settings().hotkey.get_code());
+
+ // Register transparency hotkeys using the same modifiers as the pin hotkey
+ UINT modifiers = AlwaysOnTopSettings::settings().hotkey.get_modifiers();
+ RegisterHotKey(m_window, static_cast(HotkeyId::IncreaseOpacity), modifiers, VK_OEM_PLUS);
+ RegisterHotKey(m_window, static_cast(HotkeyId::DecreaseOpacity), modifiers, VK_OEM_MINUS);
}
void AlwaysOnTop::RegisterLLKH()
@@ -285,6 +313,8 @@ void AlwaysOnTop::RegisterLLKH()
m_hPinEvent = CreateEventW(nullptr, false, false, CommonSharedConstants::ALWAYS_ON_TOP_PIN_EVENT);
m_hTerminateEvent = CreateEventW(nullptr, false, false, CommonSharedConstants::ALWAYS_ON_TOP_TERMINATE_EVENT);
+ m_hIncreaseOpacityEvent = CreateEventW(nullptr, false, false, CommonSharedConstants::ALWAYS_ON_TOP_INCREASE_OPACITY_EVENT);
+ m_hDecreaseOpacityEvent = CreateEventW(nullptr, false, false, CommonSharedConstants::ALWAYS_ON_TOP_DECREASE_OPACITY_EVENT);
if (!m_hPinEvent)
{
@@ -298,30 +328,54 @@ void AlwaysOnTop::RegisterLLKH()
return;
}
- HANDLE handles[2] = { m_hPinEvent,
- m_hTerminateEvent };
+ if (!m_hIncreaseOpacityEvent)
+ {
+ Logger::warn(L"Failed to create increaseOpacityEvent. {}", get_last_error_or_default(GetLastError()));
+ }
+
+ if (!m_hDecreaseOpacityEvent)
+ {
+ Logger::warn(L"Failed to create decreaseOpacityEvent. {}", get_last_error_or_default(GetLastError()));
+ }
+
+ HANDLE handles[4] = { m_hPinEvent,
+ m_hTerminateEvent,
+ m_hIncreaseOpacityEvent,
+ m_hDecreaseOpacityEvent };
m_thread = std::thread([this, handles]() {
MSG msg;
while (m_running)
{
- DWORD dwEvt = MsgWaitForMultipleObjects(2, handles, false, INFINITE, QS_ALLINPUT);
+ DWORD dwEvt = MsgWaitForMultipleObjects(4, handles, false, INFINITE, QS_ALLINPUT);
if (!m_running)
{
break;
}
switch (dwEvt)
{
- case WAIT_OBJECT_0:
+ case WAIT_OBJECT_0: // Pin event
if (HWND fw{ GetForegroundWindow() })
{
ProcessCommand(fw);
}
break;
- case WAIT_OBJECT_0 + 1:
+ case WAIT_OBJECT_0 + 1: // Terminate event
PostThreadMessage(m_mainThreadId, WM_QUIT, 0, 0);
break;
- case WAIT_OBJECT_0 + 2:
+ case WAIT_OBJECT_0 + 2: // Increase opacity event
+ if (HWND fw{ GetForegroundWindow() })
+ {
+ StepWindowTransparency(fw, Settings::transparencyStep);
+ }
+ break;
+ case WAIT_OBJECT_0 + 3: // Decrease opacity event
+ if (HWND fw{ GetForegroundWindow() })
+ {
+ StepWindowTransparency(fw, -Settings::transparencyStep);
+ }
+ break;
+ case WAIT_OBJECT_0 + 4: // Message queue
if (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE))
{
TranslateMessage(&msg);
@@ -370,9 +424,12 @@ void AlwaysOnTop::UnpinAll()
{
Logger::error(L"Unpinning topmost window failed");
}
+ // Restore transparency when unpinning all
+ RestoreWindowAlpha(topWindow);
}
m_topmostWindows.clear();
+ m_windowOriginalLayeredState.clear();
}
void AlwaysOnTop::CleanUp()
@@ -456,6 +513,7 @@ void AlwaysOnTop::HandleWinHookEvent(WinHookEvent* data) noexcept
for (const auto window : toErase)
{
m_topmostWindows.erase(window);
+ m_windowOriginalLayeredState.erase(window);
}
switch (data->event)
@@ -556,4 +614,166 @@ void AlwaysOnTop::RefreshBorders()
}
}
}
+}
+
+HWND AlwaysOnTop::ResolveTransparencyTargetWindow(HWND window)
+{
+ if (!window || !IsWindow(window))
+ {
+ return nullptr;
+ }
+
+ // Only allow transparency changes on pinned windows
+ if (!IsPinned(window))
+ {
+ return nullptr;
+ }
+
+ return window;
+}
+
+
+void AlwaysOnTop::StepWindowTransparency(HWND window, int delta)
+{
+ HWND targetWindow = ResolveTransparencyTargetWindow(window);
+ if (!targetWindow)
+ {
+ return;
+ }
+
+ int currentTransparency = Settings::maxTransparencyPercentage;
+ LONG exStyle = GetWindowLong(targetWindow, GWL_EXSTYLE);
+ if (exStyle & WS_EX_LAYERED)
+ {
+ BYTE alpha = 255;
+ if (GetLayeredWindowAttributes(targetWindow, nullptr, &alpha, nullptr))
+ {
+ currentTransparency = (alpha * 100) / 255;
+ }
+ }
+
+ int newTransparency = (std::max)(Settings::minTransparencyPercentage,
+ (std::min)(Settings::maxTransparencyPercentage, currentTransparency + delta));
+
+ if (newTransparency != currentTransparency)
+ {
+ ApplyWindowAlpha(targetWindow, newTransparency);
+
+ if (AlwaysOnTopSettings::settings().enableSound)
+ {
+ m_sound.Play(delta > 0 ? Sound::Type::IncreaseOpacity : Sound::Type::DecreaseOpacity);
+ }
+
+ Logger::debug(L"Transparency adjusted to {}%", newTransparency);
+ }
+}
+
+void AlwaysOnTop::ApplyWindowAlpha(HWND window, int percentage)
+{
+ if (!window || !IsWindow(window))
+ {
+ return;
+ }
+
+ percentage = (std::max)(Settings::minTransparencyPercentage,
+ (std::min)(Settings::maxTransparencyPercentage, percentage));
+
+ LONG exStyle = GetWindowLong(window, GWL_EXSTYLE);
+ bool isCurrentlyLayered = (exStyle & WS_EX_LAYERED) != 0;
+
+ // Cache original state on first transparency application
+ if (m_windowOriginalLayeredState.find(window) == m_windowOriginalLayeredState.end())
+ {
+ WindowLayeredState state;
+ state.hadLayeredStyle = isCurrentlyLayered;
+
+ if (isCurrentlyLayered)
+ {
+ BYTE alpha = 255;
+ COLORREF colorKey = 0;
+ DWORD flags = 0;
+ if (GetLayeredWindowAttributes(window, &colorKey, &alpha, &flags))
+ {
+ state.originalAlpha = alpha;
+ state.usedColorKey = (flags & LWA_COLORKEY) != 0;
+ state.colorKey = colorKey;
+ }
+ else
+ {
+ Logger::warn(L"GetLayeredWindowAttributes failed for layered window, skipping");
+ return;
+ }
+ }
+ m_windowOriginalLayeredState[window] = state;
+ }
+
+ // Clear WS_EX_LAYERED first to ensure SetLayeredWindowAttributes works
+ if (isCurrentlyLayered)
+ {
+ SetWindowLong(window, GWL_EXSTYLE, exStyle & ~WS_EX_LAYERED);
+ SetWindowPos(window, nullptr, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED);
+ exStyle = GetWindowLong(window, GWL_EXSTYLE);
+ }
+
+ BYTE alphaValue = static_cast((255 * percentage) / 100);
+ SetWindowLong(window, GWL_EXSTYLE, exStyle | WS_EX_LAYERED);
+ SetLayeredWindowAttributes(window, 0, alphaValue, LWA_ALPHA);
+}
+
+void AlwaysOnTop::RestoreWindowAlpha(HWND window)
+{
+ if (!window || !IsWindow(window))
+ {
+ return;
+ }
+
+ LONG exStyle = GetWindowLong(window, GWL_EXSTYLE);
+ auto it = m_windowOriginalLayeredState.find(window);
+
+ if (it != m_windowOriginalLayeredState.end())
+ {
+ const auto& originalState = it->second;
+
+ if (originalState.hadLayeredStyle)
+ {
+ // Window originally had WS_EX_LAYERED - restore original attributes
+ // Clear and re-add to ensure clean state
+ if (exStyle & WS_EX_LAYERED)
+ {
+ SetWindowLong(window, GWL_EXSTYLE, exStyle & ~WS_EX_LAYERED);
+ exStyle = GetWindowLong(window, GWL_EXSTYLE);
+ }
+ SetWindowLong(window, GWL_EXSTYLE, exStyle | WS_EX_LAYERED);
+
+ // Restore original alpha and/or color key
+ DWORD flags = LWA_ALPHA;
+ if (originalState.usedColorKey)
+ {
+ flags |= LWA_COLORKEY;
+ }
+ SetLayeredWindowAttributes(window, originalState.colorKey, originalState.originalAlpha, flags);
+ SetWindowPos(window, nullptr, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED);
+ }
+ else
+ {
+ // Window originally didn't have WS_EX_LAYERED - remove it completely
+ if (exStyle & WS_EX_LAYERED)
+ {
+ SetLayeredWindowAttributes(window, 0, 255, LWA_ALPHA);
+ SetWindowLong(window, GWL_EXSTYLE, exStyle & ~WS_EX_LAYERED);
+ SetWindowPos(window, nullptr, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED);
+ }
+ }
+
+ m_windowOriginalLayeredState.erase(it);
+ }
+ else
+ {
+ // Fallback: no cached state, just remove layered style
+ if (exStyle & WS_EX_LAYERED)
+ {
+ SetLayeredWindowAttributes(window, 0, 255, LWA_ALPHA);
+ SetWindowLong(window, GWL_EXSTYLE, exStyle & ~WS_EX_LAYERED);
+ }
+ }
}
\ No newline at end of file
diff --git a/src/modules/alwaysontop/AlwaysOnTop/AlwaysOnTop.h b/src/modules/alwaysontop/AlwaysOnTop/AlwaysOnTop.h
index 0505c837a2..438eaa64c4 100644
--- a/src/modules/alwaysontop/AlwaysOnTop/AlwaysOnTop.h
+++ b/src/modules/alwaysontop/AlwaysOnTop/AlwaysOnTop.h
@@ -10,6 +10,7 @@
#include
#include
+#include
class AlwaysOnTop : public SettingsObserver
{
@@ -38,6 +39,8 @@ private:
enum class HotkeyId : int
{
Pin = 1,
+ IncreaseOpacity = 2,
+ DecreaseOpacity = 3,
};
static inline AlwaysOnTop* s_instance = nullptr;
@@ -48,8 +51,20 @@ private:
HWND m_window{ nullptr };
HINSTANCE m_hinstance;
std::map> m_topmostWindows{};
+
+ // Store original window layered state for proper restoration
+ struct WindowLayeredState {
+ bool hadLayeredStyle = false;
+ BYTE originalAlpha = 255;
+ bool usedColorKey = false;
+ COLORREF colorKey = 0;
+ };
+ std::map m_windowOriginalLayeredState{};
+
HANDLE m_hPinEvent;
HANDLE m_hTerminateEvent;
+ HANDLE m_hIncreaseOpacityEvent;
+ HANDLE m_hDecreaseOpacityEvent;
DWORD m_mainThreadId;
std::thread m_thread;
const bool m_useCentralizedLLKH;
@@ -78,6 +93,12 @@ private:
bool AssignBorder(HWND window);
void RefreshBorders();
+ // Transparency methods
+ HWND ResolveTransparencyTargetWindow(HWND window);
+ void StepWindowTransparency(HWND window, int delta);
+ void ApplyWindowAlpha(HWND window, int percentage);
+ void RestoreWindowAlpha(HWND window);
+
virtual void SettingsUpdate(SettingId type) override;
static void CALLBACK WinHookProc(HWINEVENTHOOK winEventHook,
diff --git a/src/modules/alwaysontop/AlwaysOnTop/Settings.h b/src/modules/alwaysontop/AlwaysOnTop/Settings.h
index 7b674c5cf0..9c0624298e 100644
--- a/src/modules/alwaysontop/AlwaysOnTop/Settings.h
+++ b/src/modules/alwaysontop/AlwaysOnTop/Settings.h
@@ -15,6 +15,9 @@ class SettingsObserver;
struct Settings
{
PowerToysSettings::HotkeyObject hotkey = PowerToysSettings::HotkeyObject::from_settings(true, true, false, false, 84); // win + ctrl + T
+ static constexpr int minTransparencyPercentage = 20; // minimum transparency (can't go below 20%)
+ static constexpr int maxTransparencyPercentage = 100; // maximum (fully opaque)
+ static constexpr int transparencyStep = 10; // step size for +/- adjustment
bool enableFrame = true;
bool enableSound = true;
bool roundCornersEnabled = true;
diff --git a/src/modules/alwaysontop/AlwaysOnTop/Sound.h b/src/modules/alwaysontop/AlwaysOnTop/Sound.h
index e8f8dd5de4..3bb868b179 100644
--- a/src/modules/alwaysontop/AlwaysOnTop/Sound.h
+++ b/src/modules/alwaysontop/AlwaysOnTop/Sound.h
@@ -2,7 +2,6 @@
#include "pch.h"
-#include
#include // sound
class Sound
@@ -12,12 +11,10 @@ public:
{
On,
Off,
+ IncreaseOpacity,
+ DecreaseOpacity,
};
- Sound()
- : isPlaying(false)
- {}
-
void Play(Type type)
{
BOOL success = false;
@@ -29,6 +26,12 @@ public:
case Type::Off:
success = PlaySound(TEXT("Media\\Speech Sleep.wav"), NULL, SND_FILENAME | SND_ASYNC);
break;
+ case Type::IncreaseOpacity:
+ success = PlaySound(TEXT("Media\\Windows Hardware Insert.wav"), NULL, SND_FILENAME | SND_ASYNC);
+ break;
+ case Type::DecreaseOpacity:
+ success = PlaySound(TEXT("Media\\Windows Hardware Remove.wav"), NULL, SND_FILENAME | SND_ASYNC);
+ break;
default:
break;
}
@@ -38,7 +41,4 @@ public:
Logger::error(L"Sound playing error");
}
}
-
-private:
- std::atomic isPlaying;
};
\ No newline at end of file
diff --git a/src/modules/alwaysontop/AlwaysOnTopModuleInterface/dllmain.cpp b/src/modules/alwaysontop/AlwaysOnTopModuleInterface/dllmain.cpp
index 1ed96e79bd..bc52137ed2 100644
--- a/src/modules/alwaysontop/AlwaysOnTopModuleInterface/dllmain.cpp
+++ b/src/modules/alwaysontop/AlwaysOnTopModuleInterface/dllmain.cpp
@@ -105,17 +105,28 @@ public:
}
}
- virtual bool on_hotkey(size_t /*hotkeyId*/) override
+ virtual bool on_hotkey(size_t hotkeyId) override
{
if (m_enabled)
{
- Logger::trace(L"AlwaysOnTop hotkey pressed");
+ Logger::trace(L"AlwaysOnTop hotkey pressed, id={}", hotkeyId);
if (!is_process_running())
{
Enable();
}
- SetEvent(m_hPinEvent);
+ if (hotkeyId == 0)
+ {
+ SetEvent(m_hPinEvent);
+ }
+ else if (hotkeyId == 1)
+ {
+ SetEvent(m_hIncreaseOpacityEvent);
+ }
+ else if (hotkeyId == 2)
+ {
+ SetEvent(m_hDecreaseOpacityEvent);
+ }
return true;
}
@@ -125,19 +136,48 @@ public:
virtual size_t get_hotkeys(Hotkey* hotkeys, size_t buffer_size) override
{
+ size_t count = 0;
+
+ // Hotkey 0: Pin/Unpin (e.g., Win+Ctrl+T)
if (m_hotkey.key)
{
- if (hotkeys && buffer_size >= 1)
+ if (hotkeys && buffer_size > count)
{
- hotkeys[0] = m_hotkey;
+ hotkeys[count] = m_hotkey;
+ Logger::trace(L"AlwaysOnTop hotkey[0]: win={}, ctrl={}, shift={}, alt={}, key={}",
+ m_hotkey.win, m_hotkey.ctrl, m_hotkey.shift, m_hotkey.alt, m_hotkey.key);
}
+ count++;
+ }
- return 1;
- }
- else
+ // Hotkey 1: Increase opacity (same modifiers + VK_OEM_PLUS '=')
+ if (m_hotkey.key)
{
- return 0;
+ if (hotkeys && buffer_size > count)
+ {
+ hotkeys[count] = m_hotkey;
+ hotkeys[count].key = VK_OEM_PLUS; // '=' key
+ Logger::trace(L"AlwaysOnTop hotkey[1] (increase opacity): win={}, ctrl={}, shift={}, alt={}, key={}",
+ hotkeys[count].win, hotkeys[count].ctrl, hotkeys[count].shift, hotkeys[count].alt, hotkeys[count].key);
+ }
+ count++;
}
+
+ // Hotkey 2: Decrease opacity (same modifiers + VK_OEM_MINUS '-')
+ if (m_hotkey.key)
+ {
+ if (hotkeys && buffer_size > count)
+ {
+ hotkeys[count] = m_hotkey;
+ hotkeys[count].key = VK_OEM_MINUS; // '-' key
+ Logger::trace(L"AlwaysOnTop hotkey[2] (decrease opacity): win={}, ctrl={}, shift={}, alt={}, key={}",
+ hotkeys[count].win, hotkeys[count].ctrl, hotkeys[count].shift, hotkeys[count].alt, hotkeys[count].key);
+ }
+ count++;
+ }
+
+ Logger::trace(L"AlwaysOnTop get_hotkeys returning count={}", count);
+ return count;
}
// Enable the powertoy
@@ -175,6 +215,8 @@ public:
app_key = NonLocalizable::ModuleKey;
m_hPinEvent = CreateDefaultEvent(CommonSharedConstants::ALWAYS_ON_TOP_PIN_EVENT);
m_hTerminateEvent = CreateDefaultEvent(CommonSharedConstants::ALWAYS_ON_TOP_TERMINATE_EVENT);
+ m_hIncreaseOpacityEvent = CreateDefaultEvent(CommonSharedConstants::ALWAYS_ON_TOP_INCREASE_OPACITY_EVENT);
+ m_hDecreaseOpacityEvent = CreateDefaultEvent(CommonSharedConstants::ALWAYS_ON_TOP_DECREASE_OPACITY_EVENT);
init_settings();
}
@@ -292,6 +334,8 @@ private:
// Handle to event used to pin/unpin windows
HANDLE m_hPinEvent;
HANDLE m_hTerminateEvent;
+ HANDLE m_hIncreaseOpacityEvent;
+ HANDLE m_hDecreaseOpacityEvent;
};
extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create()
diff --git a/src/modules/cmdpal/CommandPalette.slnf b/src/modules/cmdpal/CommandPalette.slnf
index 6575a60790..c6ccbb7338 100644
--- a/src/modules/cmdpal/CommandPalette.slnf
+++ b/src/modules/cmdpal/CommandPalette.slnf
@@ -30,6 +30,7 @@
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.Ext.WindowWalker.UnitTests\\Microsoft.CmdPal.Ext.WindowWalker.UnitTests.csproj",
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.UI.ViewModels.UnitTests\\Microsoft.CmdPal.UI.ViewModels.UnitTests.csproj",
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.UITests\\Microsoft.CmdPal.UITests.csproj",
+ "src\\modules\\cmdpal\\Tests\\Microsoft.CommandPalette.Extensions.Toolkit.UnitTests\\Microsoft.CommandPalette.Extensions.Toolkit.UnitTests.csproj",
"src\\modules\\cmdpal\\ext\\Microsoft.CmdPal.Ext.Apps\\Microsoft.CmdPal.Ext.Apps.csproj",
"src\\modules\\cmdpal\\ext\\Microsoft.CmdPal.Ext.Bookmark\\Microsoft.CmdPal.Ext.Bookmarks.csproj",
"src\\modules\\cmdpal\\ext\\Microsoft.CmdPal.Ext.Calc\\Microsoft.CmdPal.Ext.Calc.csproj",
diff --git a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/BatchUpdateManager.cs b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/BatchUpdateManager.cs
new file mode 100644
index 0000000000..52d6091bd8
--- /dev/null
+++ b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/BatchUpdateManager.cs
@@ -0,0 +1,136 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections.Concurrent;
+using Microsoft.CmdPal.Core.Common;
+using Microsoft.CmdPal.Core.Common.Helpers;
+
+namespace Microsoft.CmdPal.Core.ViewModels;
+
+internal static class BatchUpdateManager
+{
+ private const int ExpectedBatchSize = 32;
+
+ // 30 ms chosen empirically to balance responsiveness and batching:
+ // - Keeps perceived latency low (< ~50 ms) for user-visible updates.
+ // - Still allows multiple COM/background events to be coalesced into a single batch.
+ private static readonly TimeSpan BatchDelay = TimeSpan.FromMilliseconds(30);
+ private static readonly ConcurrentQueue DirtyQueue = [];
+ private static readonly Timer Timer = new(static _ => Flush(), null, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
+
+ private static InterlockedBoolean _isFlushScheduled;
+
+ ///
+ /// Enqueue a target for batched processing. Safe to call from any thread (including COM callbacks).
+ ///
+ public static void Queue(IBatchUpdateTarget target)
+ {
+ if (!target.TryMarkBatchQueued())
+ {
+ return; // already queued in current batch window
+ }
+
+ DirtyQueue.Enqueue(target);
+ TryScheduleFlush();
+ }
+
+ private static void TryScheduleFlush()
+ {
+ if (!_isFlushScheduled.Set())
+ {
+ return;
+ }
+
+ if (DirtyQueue.IsEmpty)
+ {
+ _isFlushScheduled.Clear();
+
+ if (DirtyQueue.IsEmpty)
+ {
+ return;
+ }
+
+ if (!_isFlushScheduled.Set())
+ {
+ return;
+ }
+ }
+
+ try
+ {
+ Timer.Change(BatchDelay, Timeout.InfiniteTimeSpan);
+ }
+ catch (Exception ex)
+ {
+ _isFlushScheduled.Clear();
+ CoreLogger.LogError("Failed to arm batch timer.", ex);
+ }
+ }
+
+ private static void Flush()
+ {
+ try
+ {
+ var drained = new List(ExpectedBatchSize);
+ while (DirtyQueue.TryDequeue(out var item))
+ {
+ drained.Add(item);
+ }
+
+ if (drained.Count == 0)
+ {
+ return;
+ }
+
+ // LOAD BEARING:
+ // ApplyPendingUpdates must run on a background thread.
+ // The VM itself is responsible for marshaling UI notifications to its _uiScheduler.
+ ApplyBatch(drained);
+ }
+ catch (Exception ex)
+ {
+ // Don't kill the timer thread.
+ CoreLogger.LogError("Batch flush failed.", ex);
+ }
+ finally
+ {
+ _isFlushScheduled.Clear();
+ TryScheduleFlush();
+ }
+ }
+
+ private static void ApplyBatch(List items)
+ {
+ // Runs on the Timer callback thread (ThreadPool). That's fine: background work only.
+ foreach (var item in items)
+ {
+ // Allow re-queueing immediately if more COM events arrive during apply.
+ item.ClearBatchQueued();
+
+ try
+ {
+ item.ApplyPendingUpdates();
+ }
+ catch (Exception ex)
+ {
+ CoreLogger.LogError("Failed to apply pending updates for a batched target.", ex);
+ }
+ }
+ }
+}
+
+internal interface IBatchUpdateTarget
+{
+ /// UI scheduler (used by targets internally for UI marshaling). Kept here for diagnostics / consistency.
+ TaskScheduler UIScheduler { get; }
+
+ /// Apply any coalesced updates. Must be safe to call on a background thread.
+ void ApplyPendingUpdates();
+
+ /// De-dupe gate: returns true only for the first enqueue until cleared.
+ bool TryMarkBatchQueued();
+
+ /// Clear the de-dupe gate so the item can be queued again.
+ void ClearBatchQueued();
+}
diff --git a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/ExtensionObjectViewModel.cs b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/ExtensionObjectViewModel.cs
index e7bed46db6..53af586431 100644
--- a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/ExtensionObjectViewModel.cs
+++ b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/ExtensionObjectViewModel.cs
@@ -2,36 +2,99 @@
// 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.Buffers;
+using System.Collections.Concurrent;
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.CmdPal.Core.Common;
+using Microsoft.CmdPal.Core.Common.Helpers;
namespace Microsoft.CmdPal.Core.ViewModels;
-public abstract partial class ExtensionObjectViewModel : ObservableObject
+public abstract partial class ExtensionObjectViewModel : ObservableObject, IBatchUpdateTarget, IBackgroundPropertyChangedNotification
{
- public WeakReference PageContext { get; set; }
+ private const int InitialPropertyBatchingBufferSize = 16;
- internal ExtensionObjectViewModel(IPageContext? context)
- {
- var realContext = context ?? (this is IPageContext c ? c : throw new ArgumentException("You need to pass in an IErrorContext"));
- PageContext = new(realContext);
- }
+ // Raised on the background thread before UI notifications. It's raised on the background thread to prevent
+ // blocking the COM proxy.
+ public event PropertyChangedEventHandler? PropertyChangedBackground;
- internal ExtensionObjectViewModel(WeakReference context)
- {
- PageContext = context;
- }
+ private readonly ConcurrentQueue _pendingProps = [];
- public async virtual Task InitializePropertiesAsync()
+ private readonly TaskScheduler _uiScheduler;
+
+ private InterlockedBoolean _batchQueued;
+
+ public WeakReference PageContext { get; private set; } = null!;
+
+ TaskScheduler IBatchUpdateTarget.UIScheduler => _uiScheduler;
+
+ void IBatchUpdateTarget.ApplyPendingUpdates() => ApplyPendingUpdates();
+
+ bool IBatchUpdateTarget.TryMarkBatchQueued() => _batchQueued.Set();
+
+ void IBatchUpdateTarget.ClearBatchQueued() => _batchQueued.Clear();
+
+ private protected ExtensionObjectViewModel(TaskScheduler scheduler)
{
- var t = new Task(() =>
+ if (this is not IPageContext)
{
- SafeInitializePropertiesSynchronous();
- });
- t.Start();
- await t;
+ throw new InvalidOperationException($"Constructor overload without IPageContext can only be used when the derived class implements IPageContext. Type: {GetType().FullName}");
+ }
+
+ _uiScheduler = scheduler ?? throw new ArgumentNullException(nameof(scheduler));
+
+ // Defer PageContext assignment - derived constructor MUST call InitializePageContext()
+ // or we set it lazily on first access
}
+ private protected ExtensionObjectViewModel(IPageContext context)
+ {
+ ArgumentNullException.ThrowIfNull(context);
+
+ PageContext = new WeakReference(context);
+ _uiScheduler = context.Scheduler;
+
+ LogIfDefaultScheduler();
+ }
+
+ private protected ExtensionObjectViewModel(WeakReference contextRef)
+ {
+ ArgumentNullException.ThrowIfNull(contextRef);
+
+ if (!contextRef.TryGetTarget(out var context))
+ {
+ throw new ArgumentException("IPageContext must be alive when creating view models.", nameof(contextRef));
+ }
+
+ PageContext = contextRef;
+ _uiScheduler = context.Scheduler;
+
+ LogIfDefaultScheduler();
+ }
+
+ protected void InitializeSelfAsPageContext()
+ {
+ if (this is not IPageContext self)
+ {
+ throw new InvalidOperationException("This method can only be called when the class implements IPageContext.");
+ }
+
+ PageContext = new WeakReference(self);
+ }
+
+ private void LogIfDefaultScheduler()
+ {
+ if (_uiScheduler == TaskScheduler.Default)
+ {
+ CoreLogger.LogDebug($"ExtensionObjectViewModel created with TaskScheduler.Default. Type: {GetType().FullName}");
+ }
+ }
+
+ public virtual Task InitializePropertiesAsync()
+ => Task.Run(SafeInitializePropertiesSynchronous);
+
public void SafeInitializePropertiesSynchronous()
{
try
@@ -46,49 +109,151 @@ public abstract partial class ExtensionObjectViewModel : ObservableObject
public abstract void InitializeProperties();
- protected void UpdateProperty(string propertyName)
- {
- DoOnUiThread(() => OnPropertyChanged(propertyName));
- }
+ protected void UpdateProperty(string propertyName) => MarkPropertyDirty(propertyName);
protected void UpdateProperty(string propertyName1, string propertyName2)
{
- DoOnUiThread(() =>
- {
- OnPropertyChanged(propertyName1);
- OnPropertyChanged(propertyName2);
- });
- }
-
- protected void UpdateProperty(string propertyName1, string propertyName2, string propertyName3)
- {
- DoOnUiThread(() =>
- {
- OnPropertyChanged(propertyName1);
- OnPropertyChanged(propertyName2);
- OnPropertyChanged(propertyName3);
- });
+ MarkPropertyDirty(propertyName1);
+ MarkPropertyDirty(propertyName2);
}
protected void UpdateProperty(params string[] propertyNames)
{
- DoOnUiThread(() =>
+ foreach (var p in propertyNames)
{
- foreach (var propertyName in propertyNames)
- {
- OnPropertyChanged(propertyName);
- }
- });
+ MarkPropertyDirty(p);
+ }
}
+ internal void MarkPropertyDirty(string? propertyName)
+ {
+ if (string.IsNullOrEmpty(propertyName))
+ {
+ return;
+ }
+
+ // We should re-consider if this worth deduping
+ _pendingProps.Enqueue(propertyName);
+ BatchUpdateManager.Queue(this);
+ }
+
+ public void ApplyPendingUpdates()
+ {
+ ((IBatchUpdateTarget)this).ClearBatchQueued();
+
+ var buffer = ArrayPool.Shared.Rent(InitialPropertyBatchingBufferSize);
+ var count = 0;
+ var transferred = false;
+
+ try
+ {
+ while (_pendingProps.TryDequeue(out var name))
+ {
+ if (count == buffer.Length)
+ {
+ var bigger = ArrayPool.Shared.Rent(buffer.Length * 2);
+ Array.Copy(buffer, bigger, buffer.Length);
+ ArrayPool.Shared.Return(buffer, clearArray: true);
+ buffer = bigger;
+ }
+
+ buffer[count++] = name;
+ }
+
+ if (count == 0)
+ {
+ return;
+ }
+
+ // 1) Background subscribers (must be raised before UI notifications).
+ var propertyChangedEventHandler = PropertyChangedBackground;
+ if (propertyChangedEventHandler is not null)
+ {
+ RaiseBackground(propertyChangedEventHandler, this, buffer, count);
+ }
+
+ // 2) UI-facing PropertyChanged: ALWAYS marshal to UI scheduler.
+ // Hand-off pooled buffer to UI task (UI task returns it).
+ //
+ // It would be lovely to do nothing if no one is actually listening on PropertyChanged,
+ // but ObservableObject doesn't expose that information.
+ _ = Task.Factory.StartNew(
+ static state =>
+ {
+ var p = (UiBatch)state!;
+ try
+ {
+ p.Owner.RaiseUi(p.Names, p.Count);
+ }
+ catch (Exception ex)
+ {
+ CoreLogger.LogError("Failed to raise property change notifications on UI thread.", ex);
+ }
+ finally
+ {
+ ArrayPool.Shared.Return(p.Names, clearArray: true);
+ }
+ },
+ new UiBatch(this, buffer, count),
+ CancellationToken.None,
+ TaskCreationOptions.DenyChildAttach,
+ _uiScheduler);
+
+ transferred = true;
+ }
+ catch (Exception ex)
+ {
+ CoreLogger.LogError("Failed to apply pending property updates.", ex);
+ }
+ finally
+ {
+ if (!transferred)
+ {
+ ArrayPool.Shared.Return(buffer, clearArray: true);
+ }
+ }
+ }
+
+ private void RaiseUi(string[] names, int count)
+ {
+ for (var i = 0; i < count; i++)
+ {
+ OnPropertyChanged(Args(names[i]));
+ }
+ }
+
+ private static void RaiseBackground(PropertyChangedEventHandler handlers, object sender, string[] names, int count)
+ {
+ try
+ {
+ for (var i = 0; i < count; i++)
+ {
+ handlers(sender, Args(names[i]));
+ }
+ }
+ catch (Exception ex)
+ {
+ CoreLogger.LogError("Failed to raise PropertyChangedBackground notifications.", ex);
+ }
+ }
+
+ private sealed record UiBatch(ExtensionObjectViewModel Owner, string[] Names, int Count);
+
protected void ShowException(Exception ex, string? extensionHint = null)
{
if (PageContext.TryGetTarget(out var pageContext))
{
pageContext.ShowException(ex, extensionHint);
}
+ else
+ {
+ CoreLogger.LogError("Failed to show exception because PageContext is no longer available.", ex);
+ }
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static PropertyChangedEventArgs Args(string name) => new(name);
+
protected void DoOnUiThread(Action action)
{
if (PageContext.TryGetTarget(out var pageContext))
diff --git a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/IBackgroundPropertyChangedNotification.cs b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/IBackgroundPropertyChangedNotification.cs
new file mode 100644
index 0000000000..4db157f46d
--- /dev/null
+++ b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/IBackgroundPropertyChangedNotification.cs
@@ -0,0 +1,19 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.ComponentModel;
+
+namespace Microsoft.CmdPal.Core.ViewModels;
+
+///
+/// Provides a notification mechanism for property changes that fires
+/// synchronously on the calling thread.
+///
+public interface IBackgroundPropertyChangedNotification
+{
+ ///
+ /// Occurs when the value of a property changes.
+ ///
+ event PropertyChangedEventHandler? PropertyChangedBackground;
+}
diff --git a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/Microsoft.CmdPal.Core.ViewModels.csproj b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/Microsoft.CmdPal.Core.ViewModels.csproj
index 4ace6c5783..6e1b224ecd 100644
--- a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/Microsoft.CmdPal.Core.ViewModels.csproj
+++ b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/Microsoft.CmdPal.Core.ViewModels.csproj
@@ -1,5 +1,9 @@
+
+
+ false
+
diff --git a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/PageViewModel.cs b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/PageViewModel.cs
index 2a82f80a02..3b08b9266b 100644
--- a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/PageViewModel.cs
+++ b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/PageViewModel.cs
@@ -77,11 +77,11 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext
public IconInfoViewModel Icon { get; protected set; }
public PageViewModel(IPage? model, TaskScheduler scheduler, AppExtensionHost extensionHost)
- : base((IPageContext?)null)
+ : base(scheduler)
{
+ InitializeSelfAsPageContext();
_pageModel = new(model);
Scheduler = scheduler;
- PageContext = new(this);
ExtensionHost = extensionHost;
Icon = new(null);
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelViewModel.cs
index 6d7f830658..cc863fe362 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelViewModel.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelViewModel.cs
@@ -199,7 +199,7 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
_fallbackId = fallback.Id;
}
- item.PropertyChanged += Item_PropertyChanged;
+ item.PropertyChangedBackground += Item_PropertyChanged;
// UpdateAlias();
// UpdateHotkey();
diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Calc.UnitTests/CloseOnEnterTests.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Calc.UnitTests/CloseOnEnterTests.cs
index 5c4cf39783..04771fc621 100644
--- a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Calc.UnitTests/CloseOnEnterTests.cs
+++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Calc.UnitTests/CloseOnEnterTests.cs
@@ -19,6 +19,7 @@ public class CloseOnEnterTests
{
var settings = new Settings(closeOnEnter: true);
TypedEventHandler
-
+
@@ -83,4 +83,14 @@
IL2081;$(WarningsNotAsErrors)
+
+
+
+
+
+
+
+
+
+
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeColorPicker.xaml b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeColorPicker.xaml
index 115bdd417a..1700a3b05f 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeColorPicker.xaml
+++ b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeColorPicker.xaml
@@ -21,6 +21,7 @@
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeColorPicker.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeColorPicker.xaml.cs
index 8fd2bfb28d..ce69157269 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeColorPicker.xaml.cs
+++ b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeColorPicker.xaml.cs
@@ -5,6 +5,7 @@
using System.Threading;
using Microsoft.PowerToys.Settings.UI.Library;
+using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.OOBE.Enums;
using Microsoft.PowerToys.Settings.UI.OOBE.ViewModel;
using Microsoft.PowerToys.Settings.UI.Views;
@@ -53,6 +54,10 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
ColorPickerSettings settings = SettingsUtils.Default.GetSettingsOrDefault(ColorPickerSettings.ModuleName, settingsUpgrader: ColorPickerSettings.UpgradeSettings);
HotkeyControl.Keys = settings.Properties.ActivationShortcut.GetKeysList();
+
+ // Disable the Launch button if the module is disabled
+ var generalSettings = SettingsRepository.GetInstance(SettingsUtils.Default).SettingsConfig;
+ LaunchButton.IsEnabled = ModuleHelper.GetIsModuleEnabled(generalSettings, ManagedCommon.ModuleType.ColorPicker);
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeEnvironmentVariables.xaml b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeEnvironmentVariables.xaml
index f41c62b4f5..69d8a6aa17 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeEnvironmentVariables.xaml
+++ b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeEnvironmentVariables.xaml
@@ -12,6 +12,7 @@
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeEnvironmentVariables.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeEnvironmentVariables.xaml.cs
index 3e2b66b9d6..ed13489f78 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeEnvironmentVariables.xaml.cs
+++ b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeEnvironmentVariables.xaml.cs
@@ -5,6 +5,7 @@
using System.Threading;
using Microsoft.PowerToys.Settings.UI.Library;
+using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.OOBE.Enums;
using Microsoft.PowerToys.Settings.UI.OOBE.ViewModel;
using Microsoft.PowerToys.Settings.UI.Views;
@@ -28,6 +29,10 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
protected override void OnNavigatedTo(NavigationEventArgs e)
{
ViewModel.LogOpeningModuleEvent();
+
+ // Disable the Launch button if the module is disabled
+ var generalSettings = SettingsRepository.GetInstance(SettingsUtils.Default).SettingsConfig;
+ LaunchButton.IsEnabled = ModuleHelper.GetIsModuleEnabled(generalSettings, ManagedCommon.ModuleType.EnvironmentVariables);
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeHosts.xaml b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeHosts.xaml
index 66c9e2f2fd..918090fb13 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeHosts.xaml
+++ b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeHosts.xaml
@@ -12,6 +12,7 @@
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeHosts.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeHosts.xaml.cs
index 0a98df472e..2c0ec62fd8 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeHosts.xaml.cs
+++ b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeHosts.xaml.cs
@@ -5,6 +5,7 @@
using System.Threading;
using Microsoft.PowerToys.Settings.UI.Library;
+using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.OOBE.Enums;
using Microsoft.PowerToys.Settings.UI.OOBE.ViewModel;
using Microsoft.PowerToys.Settings.UI.Views;
@@ -28,6 +29,10 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
protected override void OnNavigatedTo(NavigationEventArgs e)
{
ViewModel.LogOpeningModuleEvent();
+
+ // Disable the Launch button if the module is disabled
+ var generalSettings = SettingsRepository.GetInstance(SettingsUtils.Default).SettingsConfig;
+ LaunchButton.IsEnabled = ModuleHelper.GetIsModuleEnabled(generalSettings, ManagedCommon.ModuleType.Hosts);
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeRegistryPreview.xaml b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeRegistryPreview.xaml
index df2be88ee8..17b8c478ba 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeRegistryPreview.xaml
+++ b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeRegistryPreview.xaml
@@ -21,6 +21,7 @@
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeRegistryPreview.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeRegistryPreview.xaml.cs
index 5e78f18988..a0505bf2e9 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeRegistryPreview.xaml.cs
+++ b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeRegistryPreview.xaml.cs
@@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information.
using Microsoft.PowerToys.Settings.UI.Library;
+using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.OOBE.Enums;
using Microsoft.PowerToys.Settings.UI.OOBE.ViewModel;
using Microsoft.PowerToys.Settings.UI.Views;
@@ -43,6 +44,10 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
protected override void OnNavigatedTo(NavigationEventArgs e)
{
ViewModel.LogOpeningModuleEvent();
+
+ // Disable the Launch button if the module is disabled
+ var generalSettings = SettingsRepository.GetInstance(SettingsUtils.Default).SettingsConfig;
+ LaunchButton.IsEnabled = ModuleHelper.GetIsModuleEnabled(generalSettings, ManagedCommon.ModuleType.RegistryPreview);
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeRun.xaml b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeRun.xaml
index 9071403785..33bc401751 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeRun.xaml
+++ b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeRun.xaml
@@ -21,6 +21,7 @@
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeRun.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeRun.xaml.cs
index 796e05bdea..5d9f13f7e6 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeRun.xaml.cs
+++ b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeRun.xaml.cs
@@ -5,6 +5,7 @@
using System.Threading;
using Microsoft.PowerToys.Settings.UI.Library;
+using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.OOBE.Enums;
using Microsoft.PowerToys.Settings.UI.OOBE.ViewModel;
using Microsoft.PowerToys.Settings.UI.Views;
@@ -55,6 +56,10 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
ViewModel.LogOpeningModuleEvent();
HotkeyControl.Keys = SettingsRepository.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.OpenPowerLauncher.GetKeysList();
+
+ // Disable the Launch button if the module is disabled
+ var generalSettings = SettingsRepository.GetInstance(SettingsUtils.Default).SettingsConfig;
+ LaunchButton.IsEnabled = ModuleHelper.GetIsModuleEnabled(generalSettings, ManagedCommon.ModuleType.PowerLauncher);
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeShortcutGuide.xaml b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeShortcutGuide.xaml
index 7b73486617..36465cac9b 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeShortcutGuide.xaml
+++ b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeShortcutGuide.xaml
@@ -16,6 +16,7 @@
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeShortcutGuide.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeShortcutGuide.xaml.cs
index e721deefd6..91f0402af1 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeShortcutGuide.xaml.cs
+++ b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeShortcutGuide.xaml.cs
@@ -8,6 +8,7 @@ using System.Globalization;
using System.IO;
using Microsoft.PowerToys.Settings.UI.Library;
+using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.OOBE.Enums;
using Microsoft.PowerToys.Settings.UI.OOBE.ViewModel;
using Microsoft.PowerToys.Settings.UI.Views;
@@ -66,6 +67,10 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
{
HotkeyControl.Keys = settingsProperties.OpenShortcutGuide.GetKeysList();
}
+
+ // Disable the Launch button if the module is disabled
+ var generalSettings = SettingsRepository.GetInstance(SettingsUtils.Default).SettingsConfig;
+ LaunchButton.IsEnabled = ModuleHelper.GetIsModuleEnabled(generalSettings, ManagedCommon.ModuleType.ShortcutGuide);
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/AlwaysOnTopPage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/AlwaysOnTopPage.xaml
index d6194dc5b2..f36483ffb1 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Views/AlwaysOnTopPage.xaml
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/AlwaysOnTopPage.xaml
@@ -38,6 +38,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw b/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw
index 83a524b938..9caee53c6f 100644
--- a/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw
+++ b/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw
@@ -3240,7 +3240,19 @@ Right-click to remove the key combination, thereby deactivating the shortcut.Activation shortcut
- Customize the shortcut to pin or unpin an app window
+ Customize the shortcut to pin or unpin an app window. Use the same modifier keys with + or - to adjust window transparency.
+
+
+ Transparency adjustment
+
+
+ Increase opacity
+
+
+ Decrease opacity
+
+
+ Range: 20%-100%Always On Top
@@ -4109,7 +4121,7 @@ Activate by holding the key for the character you want to add an accent to, then
Advanced AI
- Advanced Paste is a tool to put your clipboard content into any format you need, focused towards developer workflows. It can paste as plain text, markdown, or json directly with the UX or with a direct keystroke invoke. These are fully locally executed. In addition, it has an AI powered option that is 100% opt-in and requires an Open AI key. Note: this will replace the formatted text in your clipboard with the selected format.
+ Advanced Paste is a tool to put your clipboard content into any format you need, focused towards developer workflows. It can paste as plain text, Markdown, or JSON directly with the UX or with a direct keystroke invoke. These are fully locally executed. In addition, it has an opt-in AI feature that can use an online or local language model endpoint. Note: this will replace the formatted text in your clipboard with the selected format.Advanced Paste
diff --git a/src/settings-ui/Settings.UI/ViewModels/AlwaysOnTopViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/AlwaysOnTopViewModel.cs
index 0a4860b016..65842cf942 100644
--- a/src/settings-ui/Settings.UI/ViewModels/AlwaysOnTopViewModel.cs
+++ b/src/settings-ui/Settings.UI/ViewModels/AlwaysOnTopViewModel.cs
@@ -5,6 +5,7 @@
using System;
using System.Collections.Generic;
using System.Globalization;
+using System.Linq;
using System.Runtime.CompilerServices;
using System.Text.Json;
using global::PowerToys.GPOWrapper;
@@ -133,6 +134,10 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
Settings.Properties.Hotkey.Value = _hotkey;
NotifyPropertyChanged();
+ // Also notify that transparency keys have changed
+ OnPropertyChanged(nameof(IncreaseOpacityKeysList));
+ OnPropertyChanged(nameof(DecreaseOpacityKeysList));
+
// Using InvariantCulture as this is an IPC message
SendConfigMSG(
string.Format(
@@ -289,6 +294,62 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
}
}
+ ///
+ /// Gets the keys list for increasing window opacity (modifier keys + "+").
+ ///
+ public List IncreaseOpacityKeysList
+ {
+ get
+ {
+ var keys = GetModifierKeysList();
+ keys.Add("+");
+ return keys;
+ }
+ }
+
+ ///
+ /// Gets the keys list for decreasing window opacity (modifier keys + "-").
+ ///
+ public List DecreaseOpacityKeysList
+ {
+ get
+ {
+ var keys = GetModifierKeysList();
+ keys.Add("-");
+ return keys;
+ }
+ }
+
+ ///
+ /// Gets only the modifier keys from the current hotkey setting.
+ ///
+ private List GetModifierKeysList()
+ {
+ var modifierKeys = new List();
+
+ if (_hotkey.Win)
+ {
+ modifierKeys.Add(92); // The Windows key
+ }
+
+ if (_hotkey.Ctrl)
+ {
+ modifierKeys.Add("Ctrl");
+ }
+
+ if (_hotkey.Alt)
+ {
+ modifierKeys.Add("Alt");
+ }
+
+ if (_hotkey.Shift)
+ {
+ modifierKeys.Add(16); // The Shift key
+ }
+
+ return modifierKeys;
+ }
+
public void NotifyPropertyChanged([CallerMemberName] string propertyName = null)
{
OnPropertyChanged(propertyName);