Compare commits
64 Commits
dev/vanzue
...
Jaylyn-Bar
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f40bd3e49c | ||
|
|
d052e33607 | ||
|
|
bdfa7c0f5f | ||
|
|
9fbd3de3a2 | ||
|
|
4a0d9912ae | ||
|
|
15c79a0176 | ||
|
|
97d46efec2 | ||
|
|
46242b384e | ||
|
|
84be261581 | ||
|
|
5a8095b704 | ||
|
|
417c1a6b98 | ||
|
|
2593149d22 | ||
|
|
0b50c38fe1 | ||
|
|
840808b465 | ||
|
|
b50df36b70 | ||
|
|
b94593ef73 | ||
|
|
7a01d56179 | ||
|
|
130e9a0a68 | ||
|
|
34c37f2d38 | ||
|
|
47aed03c03 | ||
|
|
6423c7693d | ||
|
|
3e14d50f65 | ||
|
|
db7c9e180e | ||
|
|
bcc3ded280 | ||
|
|
24a3cdd486 | ||
|
|
1884e6abc1 | ||
|
|
ad4b553bb1 | ||
|
|
193d9aacbe | ||
|
|
483e773299 | ||
|
|
dd4c7ba57e | ||
|
|
29534601be | ||
|
|
74225b01ce | ||
|
|
2f001e8150 | ||
|
|
a983a773f3 | ||
|
|
02eb55e3b9 | ||
|
|
afaf904016 | ||
|
|
4425e3ad28 | ||
|
|
9f95d9b477 | ||
|
|
8fc43e1a22 | ||
|
|
42ebf8d992 | ||
|
|
76b6a25ac4 | ||
|
|
1c646ecb2a | ||
|
|
a51b2647d9 | ||
|
|
b41ed2feb1 | ||
|
|
1b742ef817 | ||
|
|
2a40e1ce4d | ||
|
|
b015d6a778 | ||
|
|
3bfa0a0cf8 | ||
|
|
5884375e9d | ||
|
|
a0a2f493c5 | ||
|
|
5e3e0660e7 | ||
|
|
29688cea0e | ||
|
|
a0f33c8af1 | ||
|
|
e1edcc13b7 | ||
|
|
756feb9f8c | ||
|
|
2fff688c6f | ||
|
|
24d7ae54ce | ||
|
|
a271a2f8af | ||
|
|
a33c484c93 | ||
|
|
f20c3b832b | ||
|
|
9c2884ab41 | ||
|
|
076828f592 | ||
|
|
301d504db1 | ||
|
|
e4a8488a2a |
13
.github/actions/spell-check/expect.txt
vendored
@@ -47,7 +47,6 @@ Allmodule
|
||||
ALLOWUNDO
|
||||
ALLVIEW
|
||||
ALPHATYPE
|
||||
amazonbedrock
|
||||
AModifier
|
||||
amr
|
||||
ANDSCANS
|
||||
@@ -66,6 +65,7 @@ APIIs
|
||||
Apm
|
||||
APPBARDATA
|
||||
APPEXECLINK
|
||||
appext
|
||||
APPLICATIONFRAMEHOST
|
||||
appmanifest
|
||||
APPMODEL
|
||||
@@ -141,7 +141,7 @@ bla
|
||||
BLACKFRAME
|
||||
BLENDFUNCTION
|
||||
Blockquotes
|
||||
Blt
|
||||
blt
|
||||
BLURBEHIND
|
||||
BLURREGION
|
||||
bmi
|
||||
@@ -187,6 +187,7 @@ CAPTUREBLT
|
||||
CAPTURECHANGED
|
||||
CARETBLINKING
|
||||
CAtl
|
||||
CBN
|
||||
cch
|
||||
CCHDEVICENAME
|
||||
CCHFORMNAME
|
||||
@@ -414,6 +415,9 @@ DNLEN
|
||||
DONOTROUND
|
||||
DONTVALIDATEPATH
|
||||
dotnet
|
||||
downsampled
|
||||
downsampling
|
||||
Downsampled
|
||||
downscale
|
||||
DPICHANGED
|
||||
DPIs
|
||||
@@ -598,6 +602,7 @@ getfilesiginforedist
|
||||
geolocator
|
||||
GETHOTKEY
|
||||
GETICON
|
||||
GETLBTEXT
|
||||
GETMINMAXINFO
|
||||
GETNONCLIENTMETRICS
|
||||
GETPROPERTYSTOREFLAGS
|
||||
@@ -605,6 +610,7 @@ GETSCREENSAVERRUNNING
|
||||
GETSECKEY
|
||||
GETSTICKYKEYS
|
||||
GETTEXTLENGTH
|
||||
GIFs
|
||||
gitmodules
|
||||
GHND
|
||||
GMEM
|
||||
@@ -615,6 +621,7 @@ GPOCA
|
||||
gpp
|
||||
gpu
|
||||
gradians
|
||||
grctlext
|
||||
Gridcustomlayout
|
||||
GSM
|
||||
gtm
|
||||
@@ -688,7 +695,6 @@ hmonitor
|
||||
homies
|
||||
homljgmgpmcbpjbnjpfijnhipfkiclkd
|
||||
HOOKPROC
|
||||
huggingface
|
||||
HORZRES
|
||||
HORZSIZE
|
||||
Hostbackdropbrush
|
||||
@@ -1867,6 +1873,7 @@ UPDATENOW
|
||||
UPDATEREGISTRY
|
||||
updown
|
||||
UPGRADINGPRODUCTCODE
|
||||
upscaling
|
||||
Uptool
|
||||
urld
|
||||
Usb
|
||||
|
||||
1
.github/pull_request_template.md
vendored
@@ -5,6 +5,7 @@
|
||||
## PR Checklist
|
||||
|
||||
- [ ] Closes: #xxx
|
||||
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved issues) -->
|
||||
- [ ] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected
|
||||
- [ ] **Tests:** Added/updated and all pass
|
||||
- [ ] **Localization:** All end-user-facing strings can be localized
|
||||
|
||||
2
.github/workflows/msstore-submissions.yml
vendored
@@ -40,7 +40,7 @@ jobs:
|
||||
echo powerToysInstallerArm64Url=$(jq -n "$powerToysSetup" | jq -r '[.[]|select(.name | contains("arm64"))][0].browser_download_url') >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Setup .NET 9.0
|
||||
uses: actions/setup-dotnet@v4
|
||||
uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
dotnet-version: '9.0.x'
|
||||
|
||||
|
||||
3
.gitmodules
vendored
@@ -4,6 +4,3 @@
|
||||
[submodule "deps/expected-lite"]
|
||||
path = deps/expected-lite
|
||||
url = https://github.com/martinmoene/expected-lite.git
|
||||
[submodule "deps/cziplib"]
|
||||
path = deps/cziplib
|
||||
url = https://github.com/kuba--/zip.git
|
||||
|
||||
@@ -291,6 +291,7 @@
|
||||
"Mono.Cecil.Rocks.dll",
|
||||
"Newtonsoft.Json.dll",
|
||||
"CommunityToolkit.WinUI.Controls.TitleBar.dll",
|
||||
"CommunityToolkit.WinUI.Controls.OpacityMaskView.dll",
|
||||
|
||||
"NLog.dll",
|
||||
"HtmlAgilityPack.dll",
|
||||
|
||||
@@ -1,56 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="4.0"
|
||||
xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<!-- Hybrid CRT configuration -->
|
||||
<PropertyGroup Condition="'$(HybridCrtConfiguration)'==''">
|
||||
<HybridCrtConfiguration>$(Configuration)</HybridCrtConfiguration>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Skip Hybrid CRT for AppContainer/UWP projects as they require MultiThreadedDLL -->
|
||||
<PropertyGroup Condition="'$(AppContainerApplication)'=='true' and '$(_VC_Target_Library_Platform)'!='Desktop'">
|
||||
<HybridCrtConfiguration></HybridCrtConfiguration>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemDefinitionGroup Condition="'$(HybridCrtConfiguration)'=='Debug'">
|
||||
<ClCompile>
|
||||
<!-- We use MultiThreadedDebug, rather than MultiThreadedDebugDLL, to avoid DLL dependencies on VCRUNTIME140d.dll and MSVCP140d.dll. -->
|
||||
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<!-- Link statically against the runtime and STL, but link dynamically against the CRT by ignoring the static CRT
|
||||
lib and instead linking against the Universal CRT DLL import library. This "hybrid" linking mechanism is
|
||||
supported according to the CRT maintainer. Dynamic linking against the CRT makes the binaries a bit smaller
|
||||
than they would otherwise be if the CRT, runtime, and STL were all statically linked in. -->
|
||||
<IgnoreSpecificDefaultLibraries>%(IgnoreSpecificDefaultLibraries);libucrtd.lib</IgnoreSpecificDefaultLibraries>
|
||||
<AdditionalOptions>%(AdditionalOptions) /defaultlib:ucrtd.lib</AdditionalOptions>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(HybridCrtConfiguration)'=='Release'">
|
||||
<ClCompile>
|
||||
<!-- We use MultiThreaded, rather than MultiThreadedDLL, to avoid DLL dependencies on VCRUNTIME140.dll and MSVCP140.dll. -->
|
||||
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<!-- Link statically against the runtime and STL, but link dynamically against the CRT by ignoring the static CRT
|
||||
lib and instead linking against the Universal CRT DLL import library. This "hybrid" linking mechanism is
|
||||
supported according to the CRT maintainer. Dynamic linking against the CRT makes the binaries a bit smaller
|
||||
than they would otherwise be if the CRT, runtime, and STL were all statically linked in. -->
|
||||
<IgnoreSpecificDefaultLibraries>%(IgnoreSpecificDefaultLibraries);libucrt.lib</IgnoreSpecificDefaultLibraries>
|
||||
<AdditionalOptions>%(AdditionalOptions) /defaultlib:ucrt.lib</AdditionalOptions>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
|
||||
<!-- AppContainer/UWP projects must use MultiThreadedDLL -->
|
||||
<ItemDefinitionGroup Condition="'$(AppContainerApplication)'=='true' and '$(_VC_Target_Library_Platform)'!='Desktop' And '$(Configuration)'=='Debug'">
|
||||
<ClCompile>
|
||||
<RuntimeLibrary>MultiThreadedDebugDLL</RuntimeLibrary>
|
||||
</ClCompile>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(AppContainerApplication)'=='true' and '$(_VC_Target_Library_Platform)'!='Desktop' And '$(Configuration)'=='Release'">
|
||||
<ClCompile>
|
||||
<RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>
|
||||
</ClCompile>
|
||||
</ItemDefinitionGroup>
|
||||
|
||||
<!-- Project configurations -->
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
@@ -76,6 +26,7 @@
|
||||
<PropertyGroup Condition="'$(SkipCppCodeAnalysis)' == ''">
|
||||
<RunCodeAnalysis>true</RunCodeAnalysis>
|
||||
<CodeAnalysisRuleSet>$(MsbuildThisFileDirectory)\CppRuleSet.ruleset</CodeAnalysisRuleSet>
|
||||
<CAExcludePath>$(MSBuildThisFileDirectory)deps;$(MSBuildThisFileDirectory)packages;$(CAExcludePath)</CAExcludePath>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- C++ source compile-specific things for all configurations -->
|
||||
@@ -84,7 +35,7 @@
|
||||
<PreferredToolArchitecture Condition="'$(PROCESSOR_ARCHITECTURE)' == 'ARM64' or '$(PROCESSOR_ARCHITEW6432)' == 'ARM64'">arm64</PreferredToolArchitecture>
|
||||
<VcpkgEnabled>false</VcpkgEnabled>
|
||||
<ReplaceWildcardsInProjectItems>true</ReplaceWildcardsInProjectItems>
|
||||
<ExternalIncludePath>$(MSBuildThisFileFullPath)\..\deps\;$(MSBuildThisFileFullPath)\..\packages\;$(ExternalIncludePath)</ExternalIncludePath>
|
||||
<ExternalIncludePath>$(MSBuildThisFileDirectory)deps;$(MSBuildThisFileDirectory)packages;$(ExternalIncludePath)</ExternalIncludePath>
|
||||
<!-- Enable control flow guard for C++ projects that don't consume any C++ files -->
|
||||
<!-- This covers the case where a .dll exports a .lib, but doesn't have any ClCompile entries. -->
|
||||
<LinkControlFlowGuard>Guard</LinkControlFlowGuard>
|
||||
@@ -123,6 +74,7 @@
|
||||
<ClCompile>
|
||||
<PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<Optimization>Disabled</Optimization>
|
||||
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
@@ -132,6 +84,7 @@
|
||||
<ClCompile>
|
||||
<PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<Optimization>MaxSpeed</Optimization>
|
||||
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
</ClCompile>
|
||||
|
||||
@@ -147,6 +147,18 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
<td>Microsoft.PowerToys.AdvancedPasteSemanticKernelFormatEvent</td>
|
||||
<td>Triggered when Advanced Paste leverages the Semantic Kernel.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.AdvancedPasteSemanticKernelErrorEvent</td>
|
||||
<td>Occurs when the Semantic Kernel workflow encounters an error.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.AdvancedPasteEndpointUsageEvent</td>
|
||||
<td>Logs the AI provider, model, and processing duration for each endpoint call.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.AdvancedPasteCustomActionErrorEvent</td>
|
||||
<td>Records provider, model, and status details when a custom action fails.</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
### Always on Top
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<PackageVersion Include="AdaptiveCards.ObjectModel.WinUI3" Version="2.0.0-beta" />
|
||||
<PackageVersion Include="AdaptiveCards.Rendering.WinUI3" Version="2.1.0-beta" />
|
||||
<PackageVersion Include="AdaptiveCards.Templating" Version="2.0.5" />
|
||||
<PackageVersion Include="CommunityToolkit.Labs.WinUI.Controls.OpacityMaskView" Version="0.1.251101-build.2372" />
|
||||
<PackageVersion Include="Microsoft.Bot.AdaptiveExpressions.Core" Version="4.23.0" />
|
||||
<PackageVersion Include="Appium.WebDriver" Version="4.4.5" />
|
||||
<PackageVersion Include="CoenM.ImageSharp.ImageHash" Version="1.3.6" />
|
||||
@@ -51,10 +52,8 @@
|
||||
<PackageVersion Include="Microsoft.AI.Foundry.Local" Version="0.3.0" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel" Version="1.66.0" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.OpenAI" Version="1.66.0" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.Amazon" Version="1.66.0-alpha" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.AzureAIInference" Version="1.66.0-beta" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.Google" Version="1.66.0-alpha" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.HuggingFace" Version="1.66.0-preview" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.MistralAI" Version="1.66.0-alpha" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.Ollama" Version="1.66.0-alpha" />
|
||||
<PackageVersion Include="Microsoft.Toolkit.Uwp.Notifications" Version="7.1.2" />
|
||||
|
||||
@@ -1498,6 +1498,7 @@ SOFTWARE.
|
||||
- CoenM.ImageSharp.ImageHash
|
||||
- CommunityToolkit.Common
|
||||
- CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock
|
||||
- CommunityToolkit.Labs.WinUI.Controls.OpacityMaskView
|
||||
- CommunityToolkit.Mvvm
|
||||
- CommunityToolkit.WinUI.Animations
|
||||
- CommunityToolkit.WinUI.Collections
|
||||
|
||||
284
README.md
@@ -7,19 +7,23 @@
|
||||
<h1 align="center">
|
||||
<span>Microsoft PowerToys</span>
|
||||
</h1>
|
||||
|
||||
<p align="center">
|
||||
<span align="center">Microsoft PowerToys is a collection of utilities that help you customize Windows and streamline everyday tasks.</span>
|
||||
</p>
|
||||
<h3 align="center">
|
||||
<a href="#-installation">Installation</a>
|
||||
<span> . </span>
|
||||
<span> · </span>
|
||||
<a href="https://aka.ms/powertoys-docs">Documentation</a>
|
||||
<span> . </span>
|
||||
<span> · </span>
|
||||
<a href="https://aka.ms/powertoys-releaseblog">Blog</a>
|
||||
<span> . </span>
|
||||
<span> · </span>
|
||||
<a href="#-whats-new">Release notes</a>
|
||||
</h3>
|
||||
<br/><br/>
|
||||
Microsoft PowerToys is a collection of utilities that help you customize Windows and streamline everyday tasks.
|
||||
<br/><br/>
|
||||
|
||||
## 🔨 Utilities
|
||||
|
||||
PowerToys includes over 25 utilities to help you customize and optimize your Windows experience:
|
||||
|
||||
| | | |
|
||||
|---|---|---|
|
||||
@@ -37,41 +41,35 @@ Microsoft PowerToys is a collection of utilities that help you customize Windows
|
||||
|
||||
## 📋 Installation
|
||||
|
||||
For detailed installation instructions, visit the [installation docs](https://learn.microsoft.com/windows/powertoys/install).
|
||||
|
||||
Before you begin, make sure your device meets the system requirements:
|
||||
|
||||
> [!NOTE]
|
||||
> - Windows 11 or Windows 10 version 2004 (20H1 / build 19041) or newer
|
||||
> - 64-bit processor: x64 or ARM64
|
||||
> - Latest stable version of [Microsoft Edge WebView2 Runtime](https://go.microsoft.com/fwlink/p/?LinkId=2124703) is installed via the bootstrapper during setup
|
||||
|
||||
Choose one of the installation methods below:
|
||||
For detailed installation instructions and system requirements, visit the [installation docs](https://learn.microsoft.com/windows/powertoys/install).
|
||||
|
||||
But to get started quickly, choose one of the installation methods below:
|
||||
<br/><br/>
|
||||
<details open>
|
||||
<summary>Download .exe from GitHub</summary>
|
||||
|
||||
<summary><strong>Download .exe from GitHub</strong></summary>
|
||||
<br/>
|
||||
Go to the [PowerToys GitHub releases][github-release-link], click Assets to reveal the downloads, and choose the installer that matches your architecture and install scope. For most devices, that's the x64 per-user installer.
|
||||
|
||||
<!-- items that need to be updated release to release -->
|
||||
[github-next-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.96%22
|
||||
[github-current-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.95%22
|
||||
[ptUserX64]: https://github.com/microsoft/PowerToys/releases/download/v0.95.1/PowerToysUserSetup-0.95.1-x64.exe
|
||||
[ptUserArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.95.1/PowerToysUserSetup-0.95.1-arm64.exe
|
||||
[ptMachineX64]: https://github.com/microsoft/PowerToys/releases/download/v0.95.1/PowerToysSetup-0.95.1-x64.exe
|
||||
[ptMachineArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.95.1/PowerToysSetup-0.95.1-arm64.exe
|
||||
[github-next-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.97%22
|
||||
[github-current-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.96%22
|
||||
[ptUserX64]: https://github.com/microsoft/PowerToys/releases/download/v0.96.0/PowerToysUserSetup-0.96.0-x64.exe
|
||||
[ptUserArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.96.0/PowerToysUserSetup-0.96.0-arm64.exe
|
||||
[ptMachineX64]: https://github.com/microsoft/PowerToys/releases/download/v0.96.0/PowerToysSetup-0.96.0-x64.exe
|
||||
[ptMachineArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.96.0/PowerToysSetup-0.96.0-arm64.exe
|
||||
|
||||
| Description | Filename |
|
||||
|----------------|----------|
|
||||
| Per user - x64 | [PowerToysUserSetup-0.95.1-x64.exe][ptUserX64] |
|
||||
| Per user - ARM64 | [PowerToysUserSetup-0.95.1-arm64.exe][ptUserArm64] |
|
||||
| Machine wide - x64 | [PowerToysSetup-0.95.1-x64.exe][ptMachineX64] |
|
||||
| Machine wide - ARM64 | [PowerToysSetup-0.95.1-arm64.exe][ptMachineArm64] |
|
||||
| Per user - x64 | [PowerToysUserSetup-0.96.0-x64.exe][ptUserX64] |
|
||||
| Per user - ARM64 | [PowerToysUserSetup-0.96.0-arm64.exe][ptUserArm64] |
|
||||
| Machine wide - x64 | [PowerToysSetup-0.96.0-x64.exe][ptMachineX64] |
|
||||
| Machine wide - ARM64 | [PowerToysSetup-0.96.0-arm64.exe][ptMachineArm64] |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Microsoft Store</summary>
|
||||
<summary><strong>Microsoft Store</strong></summary>
|
||||
<br/>
|
||||
You can easily install PowerToys from the Microsoft Store:
|
||||
<p>
|
||||
<a style="text-decoration:none" href="https://aka.ms/getPowertoys">
|
||||
@@ -82,10 +80,9 @@ You can easily install PowerToys from the Microsoft Store:
|
||||
</p>
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>WinGet</summary>
|
||||
|
||||
<summary><strong>WinGet</strong></summary>
|
||||
<br/>
|
||||
Download PowerToys from [WinGet][winget-link]. Updating PowerToys via winget will respect the current PowerToys installation scope. To install PowerToys, run the following command from the command line / PowerShell:
|
||||
|
||||
*User scope installer [default]*
|
||||
@@ -100,162 +97,137 @@ winget install --scope machine Microsoft.PowerToys -s winget
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Other methods</summary>
|
||||
|
||||
<summary><strong>Other methods</strong></summary>
|
||||
<br/>
|
||||
There are [community driven install methods](./doc/unofficialInstallMethods.md) such as Chocolatey and Scoop. If these are your preferred install solutions, you can find the install instructions there.
|
||||
</details>
|
||||
|
||||
## ✨ What's new
|
||||
**Version 0.95 (October 2025)**
|
||||
**Version 0.96 (November 2025)**
|
||||
|
||||
For an in-depth look at the latest changes, visit the [Windows Command Line blog](https://aka.ms/powertoys-releaseblog).
|
||||
|
||||
**✨ Highlights**
|
||||
- **NEW:** The **Light Switch** utility in PowerToys allows you to automatically switch between light and dark themes in Windows based on the time of day.
|
||||
- Command Palette delivered major search performance gains (new fuzzy matcher and smarter fallbacks) improving relevance and speed.
|
||||
- Peek can now be activated using just the Spacebar!
|
||||
- Find My Mouse added transparent spotlight with independent backdrop opacity, boosting focus and accessibility.
|
||||
- Settings now lets you delete shortcuts entirely and ignore conflicts.
|
||||
- Mouse Pointer Crosshairs gained orientation options (vertical / horizontal / both) for customizable accessibility. Thanks [@mikehall-ms](https://github.com/mikehall-ms)!
|
||||
- PowerRename fixed enumeration counter skipping ensuring reliable batch renames. Thanks [@daverayment](https://github.com/daverayment)!
|
||||
- ZoomIt restored legacy draw and snipping behaviors, and fixed recording issues, improving reliability. Thanks [@chakrik73](https://github.com/chakrik73)!
|
||||
- Advanced Paste now supports multiple online and on-device AI model providers: Azure OpenAI, OpenAI, Google Gemini, Mistral, Foundry Local and Ollama.
|
||||
- Command Palette received extensive improvements including file search filters, better clipboard history metadata, context-menu styling, and dozens of bug fixes and enhancements.
|
||||
- PowerRename can now extract and use photo metadata (EXIF, XMP) in renaming patterns like `%Camera`, `%Lens`, and `%ExposureTime`.
|
||||
|
||||
### Advanced Paste
|
||||
- Advanced Paste now lets you connect to multiple AI providers instead of being limited to a single OpenAI provider. See [Advanced Paste documentation](https://learn.microsoft.com/windows/powertoys/advanced-paste) for usage.
|
||||
|
||||
### Awake
|
||||
- The Awake countdown timer now stays accurate over long periods. Thanks [@daverayment](https://github.com/daverayment)!
|
||||
- Fixed Awake context menu positioning. The fix removed the conversion of the mouse cursor from screen to client-window coordinates, instead using the raw screen coordinates returned by GetCursorPos; the context menu now appears at the correct screen position. Thanks [@lzandman](https://github.com/lzandman)!
|
||||
|
||||
### Command Palette
|
||||
- Applied conditional margin for icon-only tags to tighten layout. Thanks [@samrueby](https://github.com/samrueby)
|
||||
- Improved the reliability of accessing Command Palette settings through PowerToys Settings and executing other x-cmdpal:// protocol commands. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Enabled AOT by default for improved performance while simplifying publish configs.
|
||||
- Replaced service state color dots with play/pause/stop icons for enhanced accessibility. Thanks [@samrueby](https://github.com/samrueby)
|
||||
- Fixed filter dropdown sync and crash by binding SelectedValue and raising UI-thread notifications. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Ensured long links wrap correctly in details view.
|
||||
- Removed animation and enforced minimum width on filter dropdown for clarity. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Restored focus to More button after ESC closes context menu, improving keyboard flow. Thanks [@chatasweetie](https://github.com/chatasweetie)
|
||||
- Marked main and toast windows as tool windows to keep them out of Alt+Tab while preserving style. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Fixed AOT template and theming issues for filter separators. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Introduced grid layouts (small, medium, gallery) for richer page presentation.
|
||||
- Materialized result lists to avoid rescoring overhead.
|
||||
- Disabled problematic selection TextToSuggest behind environment flag.
|
||||
- Major search performance improvements (new fuzzy matcher, smarter fallbacks, fewer exceptions).
|
||||
- Added context menu "Show Details" command when details pane is hidden.
|
||||
- Reduced window flicker by avoiding unnecessary cloaking. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Restored EmptyContent rendering for blank states. Thanks [@DevLGuilherme](https://github.com/DevLGuilherme)
|
||||
- Saved new state even if prior app state file was corrupt (better resilience). Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Migrated settings window to WinUI TitleBar control. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Prevented crash on duplicate keybindings and simplified matching. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Hotkeys now always respect the “Ignore shortcut in fullscreen” setting. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Hid search box on content pages, improving focus and accessibility, and added Home title. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Blocked Ctrl+I from inserting stray tabs in search box.
|
||||
- Logged HRESULT codes in error logs for deeper diagnostics. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Advanced font and emoji icon classification and alignment improvements. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Ensured that fallback command icons are visible on the extension settings page. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Fixed breadcrumb margin misalignment (visual polish). Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Truncated overly long command labels with ellipsis to prevent overflow.
|
||||
- Added a setting to configure the page transition animation.
|
||||
- Collection of small improvements and nits for Run Commands.
|
||||
- Improved bookmarks performance and experience. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Added Ctrl+O shortcut in Clipboard History to open links directly.
|
||||
- Resolved conflict with external software that blocked Command Palette from hiding.
|
||||
- Updated context menu items to reflect name and icon changes, and ensured application icons are displayed correctly. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Added Alt+Home shortcut to return immediately to the Command Palette home page. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Fixed a crash when displaying code blocks in markdown on detail or content pages. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Fixed an issue where the search bar icon and title were not updated when rapidly switching pages. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Improved the appearance of the search box in the context menu.
|
||||
|
||||
- The search field in context menus now matches the look of the Command Palette, with a smoke backdrop and improved padding.
|
||||
- Fallback items such as math calculations or the Run command now appear in results more quickly. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Ensured the command bar updates correctly after navigating to another page and commands are displayed correctly. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- The Command Palette settings page has been reorganized. Activation-key options are grouped under an expander and extension settings are framed for improved readability.
|
||||
- When you modify a command, its alias, hotkey, and tags now update in the top-level list, keeping the displayed information in sync. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Press `Ctrl + ,` to open Command Palette settings from anywhere. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- You can use `Page Up` and `Page Down` to navigate the list while focus is in the search box. Thanks [@samrueby](https://github.com/samrueby)!
|
||||
- Fixed an issue where the search box could disappear when navigating pages. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Ensured search text is selected when *Go home when activated* and *Highlight search on activate* are both enabled. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Fixed an issue where Command Palette window occasionally appeared on the taskbar under certain Windows settings. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Ensured that labels and icons of list items and menu items update when they change. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Fixed visibility of list filters when navigating to a content page. Thanks [@DevLGuilherme](https://github.com/DevLGuilherme)!
|
||||
- Added search to the extension list and a link to extensions on the Microsoft Store. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added options to open the Command Palette window at its last position or re-center it.
|
||||
- The Command Palette now remembers its window size after restarting.
|
||||
- Added a global error handler that logs fatal errors and provides feedback when unexpected failures force Command Palette to close. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Fixed forms and extension settings not showing on some machines due to a missing VC++ runtime.
|
||||
- Restored ranking of fallback commands for built-in extensions (Sleep, Shutdown, Windows settings, Web search, etc.). Thanks [@jiripolasek](https://github.com/jiripolasek).
|
||||
- Improved and unified labels and texts across the application!
|
||||
- Maintainance: Resolved numerous build warnings in Command Palette projects; no user-visible impact. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Maintainance: Fixed a logging issue so exception messages are properly recorded instead of placeholder text, improving troubleshooting. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
|
||||
### Command Palette Extensions
|
||||
- Replaced localized WebSearch setting keys with stable literals and numeric history count. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Enabled advanced markdown tables and emphasis extensions. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added setting to choose Clipboard History primary action (Paste vs Copy). Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Added actionable empty-state hints for File Search (search PC / open indexing settings). Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Ensured all WinGet extension assets copy reliably to output. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Improved Run command line parsing for paths with spaces; sped up related tests.
|
||||
- Updated WebSearch extension icon set for enhanced clarity and contrast. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added Terminal profile sort order setting including MRU tracking. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added Uninstall Application command (UWP direct, Win32 via Settings). Thanks [@mKpwnz](https://github.com/mKpwnz)!
|
||||
- Deferred WinGet details loading and added timing logs.
|
||||
- Removed LINQ from All Apps extension for performance.
|
||||
- Added standardized key chord system + shortcuts to File Search commands. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added Terminal channel filter & remembered selection option. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Enabled loading local/data/app images in markdown with sizing hints. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added external extension reload via x-cmdpal://reload (configurable). Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Instant WebSearch history updates with in-memory store & events. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added keep-after-paste option and safe delete with confirmation for Clipboard History. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
|
||||
### Environment Variables
|
||||
- Replaced custom window chrome with WinUI TitleBar for cleaner, maintainable Environment Variables UI.
|
||||
|
||||
### File Locksmith
|
||||
- Adopted WinUI TitleBar to simplify window chrome while preserving appearance.
|
||||
- Bookmarks: Added hints about bookmark placeholders to the Add/Edit Bookmark form. — Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Bookmarks: Improved migration of bookmarks from older versions and fixed an issue where aliases or keyboard shortcuts could be lost after restart. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Clipboard history: Items shown in Command Palette’s clipboard history now include helpful metadata. For example, image items show dimensions, text files show names and sizes, web links include page titles, and text entries display word counts. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- File search: Added filter buttons to show *all items*, *files only*, or *folders only*. Selecting a filter adds `kind:folders` or `kind:not folders` to narrow results.
|
||||
- System commands: Replaced the `:red_circle:` placeholder with an actual red-circle emoji so the correct icon appears in the UI. Thanks [@samrueby](https://github.com/samrueby)!
|
||||
- WinGet: Search performance feels more responsive because typed input is now processed via a task queue rather than complex cancellation tokens!
|
||||
- Window Walker: UWP apps no longer show a "not responding" label when suspended. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Window Walker: Now displays the actual icon of each window rather than using the process icon, improving recognition of PWAs and Python GUIs. Thanks [@Lee-WonJun](https://github.com/Lee-WonJun)!
|
||||
- Windows Terminal profiles: Fixed a rare crash in the Windows Terminal extension when the `LOCALAPPDATA` environment variable was missing. The path is now retrieved via a reliable API. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
|
||||
### Find My Mouse
|
||||
- Added transparent spotlight support with separate backdrop opacity; migrated to Windows App SDK composition APIs.
|
||||
- Activating Find My Mouse no longer makes the cursor change to the busy (hourglass) icon or steals focus from your active application.
|
||||
|
||||
### Hosts File Editor
|
||||
- Migrated to native WinUI TitleBar for cleaner, maintainable window chrome.
|
||||
- Added customizable backup settings allowing users to configure backup frequency, location, and auto-deletion policies. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
|
||||
|
||||
### Image Resizer
|
||||
- Fixed settings consistency during batch resize operations by capturing settings once before processing. Thanks [@daverayment](https://github.com/daverayment)!
|
||||
|
||||
### Light Switch
|
||||
- Introduced as a brand-new PowerToy module.
|
||||
- Automatically switches between light and dark themes.
|
||||
- Supports time-based scheduling or location-based sunrise/sunset switching.
|
||||
- Supports using a keyboard shortcut to force a change.
|
||||
- Supports filtering changes for Apps and/or System Theme.
|
||||
- Introduced new UI to allow users to manually enter their latitude and longitude in Sunrise to Sunset mode.
|
||||
- Refactored service with cleaner state management for stability.
|
||||
- Removed logs from every tick, only logging key events to largely reduce log size.
|
||||
|
||||
### Mouse Pointer Crosshairs
|
||||
- Added Esc key to cancel active gliding cursor sequence. Thanks [@mikehall-ms](https://github.com/mikehall-ms)!
|
||||
- Added orientation option (vertical / horizontal / both) for crosshairs customization. Thanks [@mikehall-ms](https://github.com/mikehall-ms)!
|
||||
- Enabled switching between Mouse Pointer Crosshairs and Gliding Cursor modes. Thanks [@mikehall-ms](https://github.com/mikehall-ms)!
|
||||
|
||||
### Mouse Without Borders
|
||||
- Continued Common class refactor (part 5/7) by extracting clipboard and init/cleanup logic into focused classes. Thanks [@mikeclayton](https://github.com/mikeclayton)!
|
||||
|
||||
- Fix connection failures caused by conflicting MachineId across machines. Thanks [@noraa-junker](https://github.com/noraa-junker) for troubleshooting!
|
||||
- Added horizontal scrolling support. Thanks [@MasonBergstrom](https://github.com/MasonBergstrom)!
|
||||
|
||||
### Peek
|
||||
- Added the option to activate Peek with just the Spacebar.
|
||||
- Fixed media files remaining locked after preview window closes. Thanks [@daverayment](https://github.com/daverayment)!
|
||||
- Added a command-line interface for file previewing. See the [Peek documentation](https://learn.microsoft.com/windows/powertoys/peek) for usage. Thanks [@prochan2](https://github.com/prochan2)!
|
||||
|
||||
### PowerRename
|
||||
- Fixed enumeration counter skipping when regex replacement equals original filename (counters now advance reliably). Thanks [@daverayment](https://github.com/daverayment)!
|
||||
- PowerRename no longer crashes due to a missing resources file.
|
||||
- Added photo metadata extraction support using EXIF and XMP for pattern-based renaming with camera info, GPS coordinates, and date taken. See [PowerRename Documentation](https://learn.microsoft.com/en-us/windows/powertoys/powerrename).
|
||||
|
||||
### Quick Accent
|
||||
- Expanded Welsh layout with acute, grave, and dieresis variants for vowels (consistent ordering). Thanks [@PesBandi](https://github.com/PesBandi)!
|
||||
### PowerToys Run
|
||||
- Added retry logic with exponential backoff to handle DWM composition errors during theme changes. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Updated OneNote icons to reflect new Microsoft 365 design. Thanks [@trevorNgo](https://github.com/trevorNgo)!
|
||||
|
||||
### Registry Preview
|
||||
- Migrated to native TitleBar and AppWindow APIs for cleaner window chrome.
|
||||
### Quick Accent
|
||||
- Added diameter symbol (⌀) for Shift+O in Special Characters mode, thanks to [@anselumjuju](https://github.com/anselumjuju)!
|
||||
|
||||
### Screen Ruler
|
||||
- Fixed ARM64 crash by aligning cursor position structure to 8-byte boundary.
|
||||
### Zoomit
|
||||
- Smoothed out zoom-animation in ZoomIt by coalescing mouse-move and timer events, thanks to [@foxmsft](https://github.com/foxmsft)!
|
||||
- Enabled GIF support for ZoomIt, thanks to [@MarioHewardt](https://github.com/MarioHewardt)!
|
||||
- Fixed spelling mistakes, and refactored some literal strings to string constants, thanks to [@lzandman](https://github.com/lzandman)!
|
||||
- Fixed inaccurate "actual size" screenshots in ZoomIt and resolves a GDI handle leak, improving capture fidelity and long-session stability. thanks to [@daverayment](https://github.com/daverayment)!
|
||||
|
||||
### Settings
|
||||
- Added ability to ignore specific hotkey conflicts to reduce noise.
|
||||
- Stopped creating backup directory during dry-run status checks (cleaner first-run).
|
||||
- Standardized casing and localization for ZoomIt and modules header.
|
||||
- Improved search results page accessibility and conditional module grouping.
|
||||
- Fixed title bar overlapping issue at smaller window sizes.
|
||||
- Refined shortcut control visual design with improved consistency and spacing.
|
||||
- Added dashboard utilities sorting by name or status.
|
||||
- Made update notification InfoBar in flyout clickable for direct navigation to update page.
|
||||
- Expanded installation instructions by default in README.
|
||||
- Improved accessibility for shortcut conflict button with static resource-based automation properties.
|
||||
- Added ScrollViewer to Command Palette page in PowerToys Settings. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Fixed module list glitches and Sort Status checkmark issue. Thanks [@daverayment](https://github.com/daverayment)!
|
||||
|
||||
### ZoomIt
|
||||
- Updated resource file to reflect standalone v9.01 and current copyright year. Thanks [@foxmsft](https://github.com/foxmsft)!
|
||||
- Restored legacy draw/snipping behaviors and fixed recording race conditions. Thanks [@chakrik73](https://github.com/chakrik73)!
|
||||
- Added smooth image option for improved zoom quality using GDI+ for static zoom and Magnifier API for live zoom. Thanks [@markrussinovich](https://github.com/markrussinovich)!
|
||||
|
||||
### Documentation
|
||||
- New Microsoft Learn documentation for the Light Switch module.
|
||||
- New dev docs for the Light Switch module.
|
||||
|
||||
### Development (Area-Build & Area-Tests)
|
||||
- Allowed debug launches to continue when modules fail to load, speeding developer iteration.
|
||||
- Fixed spell checker dictionary entry (advapi) to eliminate false error.
|
||||
- Added VS Code development guide and launch configs to streamline cross-editor workflows.
|
||||
- Upgraded Windows App SDK and related dependencies to 1.8 for newer platform features.
|
||||
- Rewrote YAML comment to resolve new spell checker forbidden pattern. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Corrected solution structure by returning misplaced Common project, reducing build confusion.
|
||||
- Modernized build scripts with shared helpers and VS environment autodetection for simpler CLI builds.
|
||||
- Standardized build scripts and platform detection to improve reliability and reuse.
|
||||
- Added missing Command Palette version bump to align module release cadence.
|
||||
- Added EXECUTEDEFAULT term to dictionary to prevent regression build failures. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Introduced nightly pre-warm pipeline and configurable MSBuild cache mode to improve CI performance.
|
||||
- Resolved CI forbidden pattern spelling complaint to keep pipelines green.
|
||||
- Added AI contributor instruction set to clarify code area expectations.
|
||||
- Added accessibility IDs to settings and FancyZones toggles, stabilizing UI tests.
|
||||
- Added automatic log collection on UI test failures to speed root cause analysis.
|
||||
- Stabilized Mouse Utils tests by switching to AccessibilityId selectors.
|
||||
- Added Screen Ruler UI test coverage to validate core measurement workflows.
|
||||
### Development
|
||||
- Fixed accessibility by associating controls with labels for screen readers.
|
||||
- Added accessible name to Shortcut Conflicts button for screen readers.
|
||||
- Excluded TitleBars from tab navigation across multiple utilities. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Migrated build infrastructure from Windows Server 2019 to Server 2022 with improved failure logging and predictable NuGet package paths.
|
||||
- Configured build agents to use larger P: drive for release builds to address disk space constraints.
|
||||
- Enhanced DSC v3 support by organizing resource manifests in a dedicated subfolder with PATH configuration.
|
||||
- Reduced installer bundle size by 6-7MB through centralized Hybrid CRT configuration across all C++ projects.
|
||||
- Updated .NET packages to version 9.0.10 for security fixes. Thanks [@snickler](https://github.com/snickler)!
|
||||
- Fixed spell check dictionary entries for consistency.
|
||||
- Restored accidentally deleted NuGet configuration file for Command Palette extensions.
|
||||
- Fixed package identity build by updating AppxManifest entry points to use PowerShell Core.
|
||||
- Optimized CI pipeline by replacing file copy operations with hard links and moves, reducing build time and disk usage by 10-15GB.
|
||||
- Updated Copilot guidance and PR prompt workflow.
|
||||
- Included high-volume bugs in issue template header. Thanks [@daverayment](https://github.com/daverayment)!
|
||||
- Fixed incorrect HRESULT logging for inner exceptions. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Introduced shared sparse package identity for PowerToys Win32 components to enable access to Windows platform APIs.
|
||||
- Consolidated installer builds to produce both machine and user installers simultaneously, reducing build time and complexity.
|
||||
- Migrated exclusively to WiX v5 installer infrastructure, removing legacy WiX v3 support.
|
||||
- Temporarily removed PowerToys installer path from PATH environment variable to prevent application crashes.
|
||||
- Added complete OCR UI test coverage with automated tests for activation, settings, language selection, and text extraction.
|
||||
- Fixed test input for drive path normalization in bookmark resolver unit tests.
|
||||
- Fixed Peek UI tests by restoring Ctrl+Space activation shortcut for test scenarios.
|
||||
- Hided apps in PowerToys.SpareApps package from Start Menu. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
|
||||
## 🛣️ Roadmap
|
||||
We are planning some nice new features and improvements for the next releases – a revamped Keyboard Manager UI, custom endpoint and local model support for Advanced Paste, Command Palette improvements and a brand-new Shortcut Guide experience! Stay tuned for [v0.96][github-next-release-work]!
|
||||
|
||||
1
deps/cziplib
vendored
@@ -33,9 +33,12 @@ The **Light Switch** module lets users automatically transition between light an
|
||||
|
||||
> **Note:** Using the shortcut overrides the current schedule until the next transition event.
|
||||
|
||||
* **LightSwitchService**
|
||||
Reads settings and applies theming. Runs a check every minute to ensure the state is correct.
|
||||
|
||||
* **LightSwitchService.cpp**
|
||||
is the heart beat of the module. Controls ticking every minute and depending on user actions (manual override, settings changing, etc) triggers the state manager to perform the corresponding operation.
|
||||
|
||||
* **LightSwitchStateManager.cpp**
|
||||
handles updating the state based on the signals sent by LightSwitchService.
|
||||
|
||||
* **SettingsXAML/LightSwitch**
|
||||
Provides the settings UI for configuring schedules, syncing location, and customizing shortcuts.
|
||||
|
||||
|
||||
BIN
doc/images/icons/CursorWrap.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 21 KiB |
BIN
doc/images/icons/MouseJump.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 22 KiB |
@@ -4,6 +4,7 @@
|
||||
#include <ProjectTelemetry.h>
|
||||
#include <spdlog/sinks/base_sink.h>
|
||||
#include <filesystem>
|
||||
#include <string_view>
|
||||
|
||||
#include "../../src/common/logger/logger.h"
|
||||
#include "../../src/common/utils/gpo.h"
|
||||
@@ -856,14 +857,69 @@ UINT __stdcall UnsetAdvancedPasteAPIKeyCA(MSIHANDLE hInstall)
|
||||
|
||||
try
|
||||
{
|
||||
winrt::Windows::Security::Credentials::PasswordVault vault;
|
||||
winrt::Windows::Security::Credentials::PasswordCredential cred;
|
||||
|
||||
hr = WcaInitialize(hInstall, "UnsetAdvancedPasteAPIKey");
|
||||
ExitOnFailure(hr, "Failed to initialize");
|
||||
|
||||
cred = vault.Retrieve(L"https://platform.openai.com/api-keys", L"PowerToys_AdvancedPaste_OpenAIKey");
|
||||
vault.Remove(cred);
|
||||
winrt::Windows::Security::Credentials::PasswordVault vault;
|
||||
|
||||
auto hasPrefix = [](std::wstring_view value, wchar_t const* prefix) {
|
||||
std::wstring_view prefixView{ prefix };
|
||||
return value.compare(0, prefixView.size(), prefixView) == 0;
|
||||
};
|
||||
|
||||
const wchar_t* resourcePrefixes[] = {
|
||||
L"https://platform.openai.com/api-keys",
|
||||
L"https://azure.microsoft.com/products/ai-services/openai-service",
|
||||
L"https://azure.microsoft.com/products/ai-services/ai-inference",
|
||||
L"https://console.mistral.ai/account/api-keys",
|
||||
L"https://ai.google.dev/",
|
||||
};
|
||||
|
||||
const wchar_t* usernamePrefixes[] = {
|
||||
L"PowerToys_AdvancedPaste_",
|
||||
};
|
||||
|
||||
auto credentials = vault.RetrieveAll();
|
||||
for (auto const& credential : credentials)
|
||||
{
|
||||
bool shouldRemove = false;
|
||||
|
||||
std::wstring resource{ credential.Resource() };
|
||||
for (auto const prefix : resourcePrefixes)
|
||||
{
|
||||
if (hasPrefix(resource, prefix))
|
||||
{
|
||||
shouldRemove = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!shouldRemove)
|
||||
{
|
||||
std::wstring username{ credential.UserName() };
|
||||
for (auto const prefix : usernamePrefixes)
|
||||
{
|
||||
if (hasPrefix(username, prefix))
|
||||
{
|
||||
shouldRemove = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!shouldRemove)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
vault.Remove(credential);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
|
||||
@@ -110,6 +110,7 @@
|
||||
<Optimization>Disabled</Optimization>
|
||||
<PreprocessorDefinitions>_DEBUG;_WINDOWS;_USRDLL;CUSTOMACTIONTEST_EXPORTS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<BasicRuntimeChecks>EnableFastChecks</BasicRuntimeChecks>
|
||||
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
@@ -122,6 +123,7 @@
|
||||
<Optimization>MaxSpeed</Optimization>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
<PreprocessorDefinitions>NDEBUG;_WINDOWS;_USRDLL;CUSTOMACTIONTEST_EXPORTS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<Project>
|
||||
<Import Project="..\..\Directory.Build.props" Condition="Exists('..\..\Directory.Build.props')" />
|
||||
<Import Project="..\..\src\Version.props" Condition="Exists('..\..\src\Version.props')" />
|
||||
<PropertyGroup>
|
||||
<!-- Set BaseIntermediateOutputPath for each project to avoid conflicts -->
|
||||
@@ -9,4 +8,4 @@
|
||||
<!-- Set MSBuildProjectExtensionsPath to use the BaseIntermediateOutputPath -->
|
||||
<MSBuildProjectExtensionsPath Condition="'$(BaseIntermediateOutputPath)' != ''">$(BaseIntermediateOutputPath)</MSBuildProjectExtensionsPath>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
@@ -120,8 +120,8 @@
|
||||
|
||||
<Custom Action="SetUnApplyModulesRegistryChangeSetsParam" Before="UnApplyModulesRegistryChangeSets" />
|
||||
<Custom Action="CheckGPO" After="InstallInitialize" Condition="NOT Installed" />
|
||||
<Custom Action="SetBundleInstallLocationData" Before="SetBundleInstallLocation" Condition="NOT Installed" />
|
||||
<Custom Action="SetBundleInstallLocation" After="InstallFiles" Condition="NOT Installed" />
|
||||
<Custom Action="SetBundleInstallLocationData" Before="SetBundleInstallLocation" Condition="NOT Installed OR WIX_UPGRADE_DETECTED" />
|
||||
<Custom Action="SetBundleInstallLocation" After="InstallFiles" Condition="NOT Installed OR WIX_UPGRADE_DETECTED" />
|
||||
<Custom Action="ApplyModulesRegistryChangeSets" After="InstallFiles" Condition="NOT Installed" />
|
||||
<Custom Action="InstallCmdPalPackage" After="InstallFiles" Condition="NOT Installed" />
|
||||
<Custom Action="InstallPackageIdentityMSIX" After="InstallFiles" Condition="NOT Installed" />
|
||||
|
||||
@@ -1,7 +1,30 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->
|
||||
|
||||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
<ProjectConfiguration Include="Debug|ARM64">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>ARM64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|ARM64">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>ARM64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Debug|x64">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|x64">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup Label="Globals">
|
||||
<ProjectGuid>{F8B9F842-F5C3-4A2D-8C85-7F8B9E2B4F1D}</ProjectGuid>
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
<TargetName>SilentFilesInUseBAFunction</TargetName>
|
||||
<ProjectName>PowerToysSetupCustomActionsVNext</ProjectName>
|
||||
<ProjectModuleDefinitionFile>bafunctions.def</ProjectModuleDefinitionFile>
|
||||
@@ -10,6 +33,7 @@
|
||||
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
|
||||
<!-- Configuration-specific property groups -->
|
||||
<PropertyGroup Condition="'$(Configuration)'=='Debug'" Label="Configuration">
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
@@ -41,10 +65,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ClCompile Include="SilentFilesInUseBAFunctions.cpp">
|
||||
<PrecompiledHeader>Use</PrecompiledHeader>
|
||||
<PrecompiledHeaderFile>precomp.h</PrecompiledHeaderFile>
|
||||
</ClCompile>
|
||||
<ClCompile Include="SilentFilesInUseBAFunctions.cpp" />
|
||||
<ClCompile Include="bafunctions.cpp">
|
||||
<PrecompiledHeader>Create</PrecompiledHeader>
|
||||
<PrecompiledHeaderFile>precomp.h</PrecompiledHeaderFile>
|
||||
@@ -71,5 +92,31 @@
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
|
||||
<!-- C++ source compile-specific things for Debug/Release configurations -->
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)'=='Debug'">
|
||||
<ClCompile>
|
||||
<PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<Optimization>Disabled</Optimization>
|
||||
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)'=='Release'">
|
||||
<ClCompile>
|
||||
<PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<Optimization>MaxSpeed</Optimization>
|
||||
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
||||
<OptimizeReferences>true</OptimizeReferences>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
</Project>
|
||||
|
||||
@@ -18,6 +18,7 @@ public: // IBootstrapperApplication
|
||||
|
||||
BalLog(BOOTSTRAPPER_LOG_LEVEL_STANDARD, "*** CUSTOM BA FUNCTION SYSTEM ACTIVE *** Running detect begin BA function. fCached=%d, registrationType=%d, cPackages=%u, fCancel=%d", fCached, registrationType, cPackages, *pfCancel);
|
||||
|
||||
LExit:
|
||||
return hr;
|
||||
}
|
||||
|
||||
@@ -31,6 +32,12 @@ public: // IBAFunctions
|
||||
|
||||
BalLog(BOOTSTRAPPER_LOG_LEVEL_STANDARD, "*** CUSTOM BA FUNCTION SYSTEM ACTIVE *** Running plan begin BA function. cPackages=%u, fCancel=%d", cPackages, *pfCancel);
|
||||
|
||||
//-------------------------------------------------------------------------------------------------
|
||||
// YOUR CODE GOES HERE
|
||||
// BalExitOnFailure(hr, "Change this message to represent real error handling.");
|
||||
//-------------------------------------------------------------------------------------------------
|
||||
|
||||
LExit:
|
||||
return hr;
|
||||
}
|
||||
|
||||
@@ -56,7 +63,6 @@ public: // IBAFunctions
|
||||
)
|
||||
{
|
||||
HRESULT hr = S_OK;
|
||||
UNREFERENCED_PARAMETER(source);
|
||||
|
||||
BalLog(BOOTSTRAPPER_LOG_LEVEL_STANDARD, "*** CUSTOM BA FUNCTION CALLED *** Running OnExecuteFilesInUse BA function. packageId=%ls, cFiles=%u, recommendation=%d", wzPackageId, cFiles, nRecommendation);
|
||||
|
||||
|
||||
@@ -42,7 +42,8 @@
|
||||
Description="PowerToys OCR Module"
|
||||
BackgroundColor="transparent"
|
||||
Square150x150Logo="Images\Square150x150Logo.png"
|
||||
Square44x44Logo="Images\Square44x44Logo.png">
|
||||
Square44x44Logo="Images\Square44x44Logo.png"
|
||||
AppListEntry="none">
|
||||
</uap:VisualElements>
|
||||
</Application>
|
||||
<Application Id="PowerToys.SettingsUI" Executable="WinUI3Apps\PowerToys.Settings.exe" EntryPoint="Windows.FullTrustApplication">
|
||||
@@ -51,7 +52,8 @@
|
||||
Description="PowerToys Settings UI"
|
||||
BackgroundColor="transparent"
|
||||
Square150x150Logo="Images\Square150x150Logo.png"
|
||||
Square44x44Logo="Images\Square44x44Logo.png">
|
||||
Square44x44Logo="Images\Square44x44Logo.png"
|
||||
AppListEntry="none">
|
||||
</uap:VisualElements>
|
||||
</Application>
|
||||
<Application Id="PowerToys.ImageResizerUI" Executable="WinUI3Apps\PowerToys.ImageResizer.exe" EntryPoint="Windows.FullTrustApplication">
|
||||
@@ -60,7 +62,8 @@
|
||||
Description="PowerToys Image Resizer UI"
|
||||
BackgroundColor="transparent"
|
||||
Square150x150Logo="Images\Square150x150Logo.png"
|
||||
Square44x44Logo="Images\Square44x44Logo.png">
|
||||
Square44x44Logo="Images\Square44x44Logo.png"
|
||||
AppListEntry="none">
|
||||
</uap:VisualElements>
|
||||
</Application>
|
||||
</Applications>
|
||||
|
||||
@@ -9,6 +9,12 @@
|
||||
<RootNamespace>CalculatorEngineCommon</RootNamespace>
|
||||
<AppxPackage>false</AppxPackage>
|
||||
</PropertyGroup>
|
||||
<!-- BEGIN common.build.pre.props -->
|
||||
<PropertyGroup Label="Configuration">
|
||||
<EnableHybridCRT>true</EnableHybridCRT>
|
||||
<UseCrtSDKReferenceStaticWarning Condition="'$(EnableHybridCRT)'=='true'">false</UseCrtSDKReferenceStaticWarning>
|
||||
</PropertyGroup>
|
||||
<!-- END common.build.pre.props -->
|
||||
<!-- BEGIN cppwinrt.build.pre.props -->
|
||||
<PropertyGroup Label="Globals">
|
||||
<CppWinRTEnabled>true</CppWinRTEnabled>
|
||||
@@ -19,9 +25,11 @@
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<MinimalCoreWin>true</MinimalCoreWin>
|
||||
<AppContainerApplication>false</AppContainerApplication>
|
||||
<AppContainerApplication>true</AppContainerApplication>
|
||||
<WindowsStoreApp>true</WindowsStoreApp>
|
||||
<ApplicationType>Windows Store</ApplicationType>
|
||||
<UseCrtSDKReference Condition="'$(EnableHybridCRT)'=='true'">false</UseCrtSDKReference>
|
||||
<!-- The SDK reference breaks the Hybrid CRT -->
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<!-- We have to use the Desktop platform for Hybrid CRT to work. -->
|
||||
@@ -140,5 +148,43 @@
|
||||
</PropertyGroup>
|
||||
<Error Condition="!Exists('..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props'))" />
|
||||
<Error Condition="!Exists('..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets'))" />
|
||||
</Target> <!-- END common.build.post.props -->
|
||||
</Target>
|
||||
<!-- BEGIN common.build.post.props -->
|
||||
<!--
|
||||
The Hybrid CRT model statically links the runtime and STL and dynamically
|
||||
links the UCRT instead of the VC++ CRT. The UCRT ships with Windows.
|
||||
WinAppSDK asserts that this is "supported according to the CRT maintainer."
|
||||
|
||||
This must come before Microsoft.Cpp.targets because it manipulates ClCompile.RuntimeLibrary.
|
||||
-->
|
||||
<ItemDefinitionGroup Condition="'$(EnableHybridCRT)'=='true' and '$(Configuration)'=='Debug'">
|
||||
<ClCompile>
|
||||
<!-- We use MultiThreadedDebug, rather than MultiThreadedDebugDLL, to avoid DLL dependencies on VCRUNTIME140d.dll and MSVCP140d.dll. -->
|
||||
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
|
||||
<LanguageStandard Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">stdcpp17</LanguageStandard>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<!-- Link statically against the runtime and STL, but link dynamically against the CRT by ignoring the static CRT
|
||||
lib and instead linking against the Universal CRT DLL import library. This "hybrid" linking mechanism is
|
||||
supported according to the CRT maintainer. Dynamic linking against the CRT makes the binaries a bit smaller
|
||||
than they would otherwise be if the CRT, runtime, and STL were all statically linked in. -->
|
||||
<IgnoreSpecificDefaultLibraries>%(IgnoreSpecificDefaultLibraries);libucrtd.lib</IgnoreSpecificDefaultLibraries>
|
||||
<AdditionalOptions>%(AdditionalOptions) /defaultlib:ucrtd.lib</AdditionalOptions>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(EnableHybridCRT)'=='true' and ('$(Configuration)'=='Release' or '$(Configuration)'=='AuditMode')">
|
||||
<ClCompile>
|
||||
<!-- We use MultiThreaded, rather than MultiThreadedDLL, to avoid DLL dependencies on VCRUNTIME140.dll and MSVCP140.dll. -->
|
||||
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<!-- Link statically against the runtime and STL, but link dynamically against the CRT by ignoring the static CRT
|
||||
lib and instead linking against the Universal CRT DLL import library. This "hybrid" linking mechanism is
|
||||
supported according to the CRT maintainer. Dynamic linking against the CRT makes the binaries a bit smaller
|
||||
than they would otherwise be if the CRT, runtime, and STL were all statically linked in. -->
|
||||
<IgnoreSpecificDefaultLibraries>%(IgnoreSpecificDefaultLibraries);libucrt.lib</IgnoreSpecificDefaultLibraries>
|
||||
<AdditionalOptions>%(AdditionalOptions) /defaultlib:ucrt.lib</AdditionalOptions>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<!-- END common.build.post.props -->
|
||||
</Project>
|
||||
@@ -216,10 +216,6 @@ namespace winrt::PowerToys::GPOWrapper::implementation
|
||||
{
|
||||
return static_cast<GpoRuleConfigured>(powertoys_gpo::getAllowedAdvancedPasteGoogleValue());
|
||||
}
|
||||
GpoRuleConfigured GPOWrapper::GetAllowedAdvancedPasteAnthropicValue()
|
||||
{
|
||||
return static_cast<GpoRuleConfigured>(powertoys_gpo::getAllowedAdvancedPasteAnthropicValue());
|
||||
}
|
||||
GpoRuleConfigured GPOWrapper::GetAllowedAdvancedPasteOllamaValue()
|
||||
{
|
||||
return static_cast<GpoRuleConfigured>(powertoys_gpo::getAllowedAdvancedPasteOllamaValue());
|
||||
|
||||
@@ -60,7 +60,6 @@ namespace winrt::PowerToys::GPOWrapper::implementation
|
||||
static GpoRuleConfigured GetAllowedAdvancedPasteAzureAIInferenceValue();
|
||||
static GpoRuleConfigured GetAllowedAdvancedPasteMistralValue();
|
||||
static GpoRuleConfigured GetAllowedAdvancedPasteGoogleValue();
|
||||
static GpoRuleConfigured GetAllowedAdvancedPasteAnthropicValue();
|
||||
static GpoRuleConfigured GetAllowedAdvancedPasteOllamaValue();
|
||||
static GpoRuleConfigured GetAllowedAdvancedPasteFoundryLocalValue();
|
||||
static GpoRuleConfigured GetConfiguredNewPlusEnabledValue();
|
||||
|
||||
@@ -64,7 +64,6 @@ namespace PowerToys
|
||||
static GpoRuleConfigured GetAllowedAdvancedPasteAzureAIInferenceValue();
|
||||
static GpoRuleConfigured GetAllowedAdvancedPasteMistralValue();
|
||||
static GpoRuleConfigured GetAllowedAdvancedPasteGoogleValue();
|
||||
static GpoRuleConfigured GetAllowedAdvancedPasteAnthropicValue();
|
||||
static GpoRuleConfigured GetAllowedAdvancedPasteOllamaValue();
|
||||
static GpoRuleConfigured GetAllowedAdvancedPasteFoundryLocalValue();
|
||||
static GpoRuleConfigured GetConfiguredNewPlusEnabledValue();
|
||||
|
||||
@@ -10,6 +10,23 @@ namespace LanguageModelProvider.FoundryLocal;
|
||||
internal sealed class FoundryClient
|
||||
{
|
||||
public static async Task<FoundryClient?> CreateAsync()
|
||||
{
|
||||
// First attempt with current environment
|
||||
var client = await TryCreateClientAsync().ConfigureAwait(false);
|
||||
if (client != null)
|
||||
{
|
||||
return client;
|
||||
}
|
||||
|
||||
// If failed, refresh PATH from registry and retry once
|
||||
// This handles cases where PowerToys was launched by MSI installer.
|
||||
Logger.LogInfo("[FoundryClient] First attempt failed, refreshing PATH and retrying");
|
||||
RefreshEnvironmentPath();
|
||||
|
||||
return await TryCreateClientAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static async Task<FoundryClient?> TryCreateClientAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -168,41 +185,95 @@ internal sealed class FoundryClient
|
||||
}
|
||||
|
||||
public async Task<bool> EnsureModelLoaded(string modelId)
|
||||
{
|
||||
Logger.LogInfo($"[FoundryClient] EnsureModelLoaded called with: {modelId}");
|
||||
|
||||
// Check if already loaded
|
||||
if (await IsModelLoaded(modelId).ConfigureAwait(false))
|
||||
{
|
||||
Logger.LogInfo($"[FoundryClient] Model already loaded: {modelId}");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Load the model
|
||||
Logger.LogInfo($"[FoundryClient] Loading model: {modelId}");
|
||||
await _foundryManager.LoadModelAsync(modelId).ConfigureAwait(false);
|
||||
|
||||
// Verify it's loaded
|
||||
var loaded = await IsModelLoaded(modelId).ConfigureAwait(false);
|
||||
Logger.LogInfo($"[FoundryClient] Model load result: {loaded}");
|
||||
return loaded;
|
||||
}
|
||||
|
||||
public async Task EnsureRunning()
|
||||
{
|
||||
if (!_foundryManager.IsServiceRunning)
|
||||
{
|
||||
await _foundryManager.StartServiceAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes the PATH environment variable from the system registry.
|
||||
/// This is necessary when tools are installed while PowerToys is running,
|
||||
/// as the installer updates the system PATH but running processes don't see the change.
|
||||
/// </summary>
|
||||
private static void RefreshEnvironmentPath()
|
||||
{
|
||||
try
|
||||
{
|
||||
Logger.LogInfo($"[FoundryClient] EnsureModelLoaded called with: {modelId}");
|
||||
Logger.LogInfo("[FoundryClient] Refreshing PATH environment variable from system");
|
||||
|
||||
// Check if already loaded
|
||||
if (await IsModelLoaded(modelId).ConfigureAwait(false))
|
||||
var currentPath = Environment.GetEnvironmentVariable("PATH", EnvironmentVariableTarget.Process) ?? string.Empty;
|
||||
var machinePath = Environment.GetEnvironmentVariable("PATH", EnvironmentVariableTarget.Machine) ?? string.Empty;
|
||||
var userPath = Environment.GetEnvironmentVariable("PATH", EnvironmentVariableTarget.User) ?? string.Empty;
|
||||
|
||||
var pathsToAdd = new List<string>();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(currentPath))
|
||||
{
|
||||
Logger.LogInfo($"[FoundryClient] Model already loaded: {modelId}");
|
||||
return true;
|
||||
pathsToAdd.AddRange(currentPath.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries));
|
||||
}
|
||||
|
||||
// Check if model exists in cache
|
||||
var cachedModels = await ListCachedModels().ConfigureAwait(false);
|
||||
Logger.LogInfo($"[FoundryClient] Cached models: {string.Join(", ", cachedModels.Select(m => m.Name))}");
|
||||
|
||||
if (!cachedModels.Any(m => m.Name == modelId))
|
||||
if (!string.IsNullOrWhiteSpace(userPath))
|
||||
{
|
||||
Logger.LogWarning($"[FoundryClient] Model not found in cache: {modelId}");
|
||||
return false;
|
||||
var userPaths = userPath.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries);
|
||||
foreach (var path in userPaths)
|
||||
{
|
||||
if (!pathsToAdd.Contains(path, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
pathsToAdd.Add(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load the model
|
||||
Logger.LogInfo($"[FoundryClient] Loading model: {modelId}");
|
||||
await _foundryManager.LoadModelAsync(modelId).ConfigureAwait(false);
|
||||
if (!string.IsNullOrWhiteSpace(machinePath))
|
||||
{
|
||||
var machinePaths = machinePath.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries);
|
||||
foreach (var path in machinePaths)
|
||||
{
|
||||
if (!pathsToAdd.Contains(path, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
pathsToAdd.Add(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Verify it's loaded
|
||||
var loaded = await IsModelLoaded(modelId).ConfigureAwait(false);
|
||||
Logger.LogInfo($"[FoundryClient] Model load result: {loaded}");
|
||||
return loaded;
|
||||
var newPath = string.Join(Path.PathSeparator.ToString(), pathsToAdd);
|
||||
|
||||
if (currentPath != newPath)
|
||||
{
|
||||
Logger.LogInfo("[FoundryClient] Updating process PATH with latest system values");
|
||||
Environment.SetEnvironmentVariable("PATH", newPath, EnvironmentVariableTarget.Process);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogInfo("[FoundryClient] PATH is already up to date");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"[FoundryClient] EnsureModelLoaded exception: {ex.Message}");
|
||||
return false;
|
||||
Logger.LogError($"[FoundryClient] Failed to refresh PATH: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,8 @@ namespace LanguageModelProvider;
|
||||
|
||||
public sealed class FoundryLocalModelProvider : ILanguageModelProvider
|
||||
{
|
||||
private IEnumerable<ModelDetails>? _downloadedModels;
|
||||
private FoundryClient? _foundryManager;
|
||||
private FoundryClient? _foundryClient;
|
||||
private IEnumerable<FoundryCatalogModel>? _catalogModels;
|
||||
private string? _serviceUrl;
|
||||
|
||||
public static FoundryLocalModelProvider Instance { get; } = new();
|
||||
@@ -22,66 +22,45 @@ public sealed class FoundryLocalModelProvider : ILanguageModelProvider
|
||||
|
||||
public string ProviderDescription => "The model will run locally via Foundry Local";
|
||||
|
||||
public string UrlPrefix => "fl://";
|
||||
|
||||
public IChatClient? GetIChatClient(string url)
|
||||
public IChatClient? GetIChatClient(string modelId)
|
||||
{
|
||||
try
|
||||
{
|
||||
Logger.LogInfo($"[FoundryLocal] GetIChatClient called with url: {url}");
|
||||
InitializeAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"[FoundryLocal] Failed to initialize: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
Logger.LogInfo($"[FoundryLocal] GetIChatClient called with url: {modelId}");
|
||||
InitializeAsync().GetAwaiter().GetResult();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(_serviceUrl) || _foundryManager == null)
|
||||
{
|
||||
Logger.LogError("[FoundryLocal] Service URL or manager is null");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Extract model ID from URL (format: fl://modelname)
|
||||
var modelId = url.Replace(UrlPrefix, string.Empty).Trim('/');
|
||||
if (string.IsNullOrWhiteSpace(modelId))
|
||||
{
|
||||
Logger.LogError("[FoundryLocal] Model ID is empty after extraction");
|
||||
return null;
|
||||
}
|
||||
|
||||
Logger.LogInfo($"[FoundryLocal] Extracted model ID: {modelId}");
|
||||
// Check if model is in catalog
|
||||
var isInCatalog = _catalogModels?.Any(m => m.Name == modelId) ?? false;
|
||||
if (!isInCatalog)
|
||||
{
|
||||
var errorMessage = $"{modelId} is not supported in Foundry Local. Please configure supported models in Settings.";
|
||||
Logger.LogError($"[FoundryLocal] {errorMessage}");
|
||||
throw new InvalidOperationException(errorMessage);
|
||||
}
|
||||
|
||||
// Ensure the model is loaded before returning chat client
|
||||
try
|
||||
var isLoaded = _foundryClient!.EnsureModelLoaded(modelId).GetAwaiter().GetResult();
|
||||
if (!isLoaded)
|
||||
{
|
||||
var isLoaded = _foundryManager.EnsureModelLoaded(modelId).GetAwaiter().GetResult();
|
||||
if (!isLoaded)
|
||||
{
|
||||
Logger.LogError($"[FoundryLocal] Failed to load model: {modelId}");
|
||||
return null;
|
||||
}
|
||||
|
||||
Logger.LogInfo($"[FoundryLocal] Model is loaded: {modelId}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"[FoundryLocal] Exception ensuring model loaded: {ex.Message}");
|
||||
return null;
|
||||
Logger.LogError($"[FoundryLocal] Failed to load model: {modelId}");
|
||||
throw new InvalidOperationException($"Failed to load the model '{modelId}'.");
|
||||
}
|
||||
|
||||
// Use ServiceUri instead of Endpoint since Endpoint already includes /v1
|
||||
var baseUri = _foundryManager.GetServiceUri();
|
||||
var baseUri = _foundryClient.GetServiceUri();
|
||||
if (baseUri == null)
|
||||
{
|
||||
Logger.LogError("[FoundryLocal] Service URI is null");
|
||||
return null;
|
||||
const string message = "Foundry Local service URL is not available. Please make sure Foundry Local is installed and running.";
|
||||
Logger.LogError($"[FoundryLocal] {message}");
|
||||
throw new InvalidOperationException(message);
|
||||
}
|
||||
|
||||
var endpointUri = new Uri($"{baseUri.ToString().TrimEnd('/')}/v1");
|
||||
Logger.LogInfo($"[FoundryLocal] Creating OpenAI client with endpoint: {endpointUri}");
|
||||
Logger.LogInfo($"[FoundryLocal] Model ID for chat client: {modelId}");
|
||||
|
||||
return new OpenAIClient(
|
||||
new ApiKeyCredential("none"),
|
||||
@@ -111,48 +90,16 @@ public sealed class FoundryLocalModelProvider : ILanguageModelProvider
|
||||
return $"new OpenAIClient(new ApiKeyCredential(\"none\"), new OpenAIClientOptions{{ Endpoint = new Uri(\"{_serviceUrl}/v1\") }}).GetChatClient(\"{modelId}\").AsIChatClient()";
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<ModelDetails>> GetModelsAsync(bool ignoreCached = false, CancellationToken cancelationToken = default)
|
||||
public async Task<IEnumerable<ModelDetails>> GetModelsAsync(CancellationToken cancelationToken = default)
|
||||
{
|
||||
if (ignoreCached)
|
||||
{
|
||||
Logger.LogInfo("[FoundryLocal] Ignoring cached models, resetting");
|
||||
Reset();
|
||||
}
|
||||
|
||||
await InitializeAsync(cancelationToken);
|
||||
|
||||
Logger.LogInfo($"[FoundryLocal] Returning {_downloadedModels?.Count() ?? 0} downloaded models");
|
||||
return _downloadedModels ?? [];
|
||||
}
|
||||
|
||||
private void Reset()
|
||||
{
|
||||
_downloadedModels = null;
|
||||
_ = InitializeAsync();
|
||||
}
|
||||
|
||||
private async Task InitializeAsync(CancellationToken cancelationToken = default)
|
||||
{
|
||||
if (_foundryManager != null && _downloadedModels != null && _downloadedModels.Any())
|
||||
if (_foundryClient == null)
|
||||
{
|
||||
return;
|
||||
return Array.Empty<ModelDetails>();
|
||||
}
|
||||
|
||||
Logger.LogInfo("[FoundryLocal] Initializing provider");
|
||||
_foundryManager ??= await FoundryClient.CreateAsync();
|
||||
|
||||
if (_foundryManager == null)
|
||||
{
|
||||
Logger.LogError("[FoundryLocal] Failed to create Foundry client");
|
||||
return;
|
||||
}
|
||||
|
||||
_serviceUrl ??= await _foundryManager.GetServiceUrl();
|
||||
Logger.LogInfo($"[FoundryLocal] Service URL: {_serviceUrl}");
|
||||
|
||||
var cachedModels = await _foundryManager.ListCachedModels();
|
||||
Logger.LogInfo($"[FoundryLocal] Found {cachedModels.Count} cached models");
|
||||
|
||||
var cachedModels = await _foundryClient.ListCachedModels();
|
||||
List<ModelDetails> downloadedModels = [];
|
||||
|
||||
foreach (var model in cachedModels)
|
||||
@@ -162,23 +109,47 @@ public sealed class FoundryLocalModelProvider : ILanguageModelProvider
|
||||
{
|
||||
Id = $"fl-{model.Name}",
|
||||
Name = model.Name,
|
||||
Url = $"{UrlPrefix}{model.Name}",
|
||||
Url = $"fl://{model.Name}",
|
||||
Description = $"{model.Name} running locally with Foundry Local",
|
||||
HardwareAccelerators = [HardwareAccelerator.FOUNDRYLOCAL],
|
||||
SupportedOnQualcomm = true,
|
||||
ProviderModelDetails = model,
|
||||
});
|
||||
}
|
||||
|
||||
_downloadedModels = downloadedModels;
|
||||
Logger.LogInfo($"[FoundryLocal] Initialization complete. Total downloaded models: {downloadedModels.Count}");
|
||||
return downloadedModels;
|
||||
}
|
||||
|
||||
private async Task InitializeAsync(CancellationToken cancelationToken = default)
|
||||
{
|
||||
if (_foundryClient != null && _catalogModels != null && _catalogModels.Any())
|
||||
{
|
||||
await _foundryClient.EnsureRunning().ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.LogInfo("[FoundryLocal] Initializing provider");
|
||||
_foundryClient ??= await FoundryClient.CreateAsync();
|
||||
|
||||
if (_foundryClient == null)
|
||||
{
|
||||
const string message = "Foundry Local client could not be created. Please make sure Foundry Local is installed and running.";
|
||||
Logger.LogError($"[FoundryLocal] {message}");
|
||||
throw new InvalidOperationException(message);
|
||||
}
|
||||
|
||||
_serviceUrl ??= await _foundryClient.GetServiceUrl();
|
||||
Logger.LogInfo($"[FoundryLocal] Service URL: {_serviceUrl}");
|
||||
|
||||
var catalogModels = await _foundryClient.ListCatalogModels();
|
||||
Logger.LogInfo($"[FoundryLocal] Found {catalogModels.Count} catalog models");
|
||||
_catalogModels = catalogModels;
|
||||
}
|
||||
|
||||
public async Task<bool> IsAvailable()
|
||||
{
|
||||
Logger.LogInfo("[FoundryLocal] Checking availability");
|
||||
await InitializeAsync();
|
||||
var available = _foundryManager != null;
|
||||
var available = _foundryClient != null;
|
||||
Logger.LogInfo($"[FoundryLocal] Available: {available}");
|
||||
return available;
|
||||
}
|
||||
|
||||
@@ -10,13 +10,11 @@ public interface ILanguageModelProvider
|
||||
{
|
||||
string Name { get; }
|
||||
|
||||
string UrlPrefix { get; }
|
||||
|
||||
string ProviderDescription { get; }
|
||||
|
||||
Task<IEnumerable<ModelDetails>> GetModelsAsync(bool ignoreCached = false, CancellationToken cancelationToken = default);
|
||||
Task<IEnumerable<ModelDetails>> GetModelsAsync(CancellationToken cancelationToken = default);
|
||||
|
||||
IChatClient? GetIChatClient(string url);
|
||||
IChatClient? GetIChatClient(string modelId);
|
||||
|
||||
string GetIChatClientString(string url);
|
||||
}
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
// 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.Extensions.AI;
|
||||
|
||||
namespace LanguageModelProvider;
|
||||
|
||||
public sealed class LanguageModelService
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, ILanguageModelProvider> _providersByPrefix;
|
||||
|
||||
public LanguageModelService(IEnumerable<ILanguageModelProvider> providers)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(providers);
|
||||
|
||||
_providersByPrefix = new ConcurrentDictionary<string, ILanguageModelProvider>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var provider in providers)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(provider.UrlPrefix))
|
||||
{
|
||||
_providersByPrefix[provider.UrlPrefix] = provider;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static LanguageModelService CreateDefault()
|
||||
{
|
||||
return new LanguageModelService(new[]
|
||||
{
|
||||
FoundryLocalModelProvider.Instance,
|
||||
});
|
||||
}
|
||||
|
||||
public IReadOnlyCollection<ILanguageModelProvider> Providers => _providersByPrefix.Values.ToArray();
|
||||
|
||||
public bool RegisterProvider(ILanguageModelProvider provider)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(provider);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(provider.UrlPrefix))
|
||||
{
|
||||
throw new ArgumentException("Provider must supply a URL prefix.", nameof(provider));
|
||||
}
|
||||
|
||||
_providersByPrefix[provider.UrlPrefix] = provider;
|
||||
return true;
|
||||
}
|
||||
|
||||
public ILanguageModelProvider? GetProviderFor(string? modelReference)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(modelReference))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (var provider in _providersByPrefix.Values)
|
||||
{
|
||||
if (modelReference.StartsWith(provider.UrlPrefix, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return provider;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<ModelDetails>> GetModelsAsync(bool refresh = false, CancellationToken cancellationToken = default)
|
||||
{
|
||||
List<ModelDetails> models = [];
|
||||
|
||||
foreach (var provider in _providersByPrefix.Values)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var providerModels = await provider.GetModelsAsync(refresh, cancellationToken).ConfigureAwait(false);
|
||||
models.AddRange(providerModels);
|
||||
}
|
||||
|
||||
return models;
|
||||
}
|
||||
|
||||
public IChatClient? GetClient(ModelDetails model)
|
||||
{
|
||||
if (model is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var reference = !string.IsNullOrWhiteSpace(model.Url) ? model.Url : model.Id;
|
||||
return GetClient(reference);
|
||||
}
|
||||
|
||||
public IChatClient? GetClient(string? modelReference)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(modelReference))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var provider = GetProviderFor(modelReference);
|
||||
|
||||
return provider?.GetIChatClient(modelReference);
|
||||
}
|
||||
}
|
||||
@@ -24,8 +24,6 @@ public class ModelDetails
|
||||
|
||||
public List<HardwareAccelerator> HardwareAccelerators { get; set; } = [];
|
||||
|
||||
public bool SupportedOnQualcomm { get; set; }
|
||||
|
||||
public string License { get; set; } = string.Empty;
|
||||
|
||||
public object? ProviderModelDetails { get; set; }
|
||||
|
||||
@@ -63,12 +63,14 @@
|
||||
</PropertyGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)'=='Debug'">
|
||||
<ClCompile>
|
||||
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
<LinkIncremental>true</LinkIncremental>
|
||||
</ClCompile>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)'=='Release'">
|
||||
<ClCompile>
|
||||
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
<LinkIncremental>false</LinkIncremental>
|
||||
|
||||
@@ -46,6 +46,16 @@
|
||||
<PropertyGroup>
|
||||
<TargetName>notifications</TargetName>
|
||||
</PropertyGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)'=='Debug'">
|
||||
<ClCompile>
|
||||
<RuntimeLibrary>MultiThreadedDebugDLL</RuntimeLibrary>
|
||||
</ClCompile>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)'=='Release'">
|
||||
<ClCompile>
|
||||
<RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>
|
||||
</ClCompile>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup>
|
||||
<ClCompile>
|
||||
<PrecompiledHeaderOutputFile>$(IntDir)pch.pch</PrecompiledHeaderOutputFile>
|
||||
|
||||
@@ -89,7 +89,6 @@ namespace powertoys_gpo
|
||||
const std::wstring POLICY_ALLOW_ADVANCED_PASTE_AZURE_AI_INFERENCE = L"AllowAdvancedPasteAzureAIInference";
|
||||
const std::wstring POLICY_ALLOW_ADVANCED_PASTE_MISTRAL = L"AllowAdvancedPasteMistral";
|
||||
const std::wstring POLICY_ALLOW_ADVANCED_PASTE_GOOGLE = L"AllowAdvancedPasteGoogle";
|
||||
const std::wstring POLICY_ALLOW_ADVANCED_PASTE_ANTHROPIC = L"AllowAdvancedPasteAnthropic";
|
||||
const std::wstring POLICY_ALLOW_ADVANCED_PASTE_OLLAMA = L"AllowAdvancedPasteOllama";
|
||||
const std::wstring POLICY_ALLOW_ADVANCED_PASTE_FOUNDRY_LOCAL = L"AllowAdvancedPasteFoundryLocal";
|
||||
const std::wstring POLICY_MWB_CLIPBOARD_SHARING_ENABLED = L"MwbClipboardSharingEnabled";
|
||||
@@ -615,11 +614,6 @@ namespace powertoys_gpo
|
||||
return getConfiguredValue(POLICY_ALLOW_ADVANCED_PASTE_GOOGLE);
|
||||
}
|
||||
|
||||
inline gpo_rule_configured_t getAllowedAdvancedPasteAnthropicValue()
|
||||
{
|
||||
return getConfiguredValue(POLICY_ALLOW_ADVANCED_PASTE_ANTHROPIC);
|
||||
}
|
||||
|
||||
inline gpo_rule_configured_t getAllowedAdvancedPasteOllamaValue()
|
||||
{
|
||||
return getConfiguredValue(POLICY_ALLOW_ADVANCED_PASTE_OLLAMA);
|
||||
|
||||
@@ -49,6 +49,8 @@ internal sealed class IntegrationTestUserSettings : IUserSettings
|
||||
|
||||
public bool CloseAfterLosingFocus => false;
|
||||
|
||||
public bool EnableClipboardPreview => true;
|
||||
|
||||
public IReadOnlyList<AdvancedPasteCustomAction> CustomActions => _customActions;
|
||||
|
||||
public IReadOnlyList<PasteFormats> AdditionalActions => _additionalActions;
|
||||
|
||||
@@ -59,10 +59,8 @@
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Controls.Primitives" />
|
||||
<!-- Including MessagePack to force version, since it's used by StreamJsonRpc but contains vulnerabilities. After StreamJsonRpc updates the version of MessagePack, we can upgrade StreamJsonRpc instead. -->
|
||||
<PackageReference Include="MessagePack" />
|
||||
<PackageReference Include="Microsoft.SemanticKernel.Connectors.Amazon" />
|
||||
<PackageReference Include="Microsoft.SemanticKernel.Connectors.AzureAIInference" />
|
||||
<PackageReference Include="Microsoft.SemanticKernel.Connectors.Google" />
|
||||
<PackageReference Include="Microsoft.SemanticKernel.Connectors.HuggingFace" />
|
||||
<PackageReference Include="Microsoft.SemanticKernel.Connectors.MistralAI" />
|
||||
<PackageReference Include="Microsoft.SemanticKernel.Connectors.Ollama" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" />
|
||||
|
||||
@@ -112,11 +112,7 @@ namespace AdvancedPaste
|
||||
/// Invoked when the application is launched.
|
||||
/// </summary>
|
||||
/// <param name="args">Details about the launch request and process.</param>
|
||||
#if DEBUG
|
||||
protected async override void OnLaunched(LaunchActivatedEventArgs args)
|
||||
#else
|
||||
protected override void OnLaunched(LaunchActivatedEventArgs args)
|
||||
#endif
|
||||
{
|
||||
var cmdArgs = Environment.GetCommandLineArgs();
|
||||
if (cmdArgs?.Length > 1)
|
||||
@@ -138,10 +134,6 @@ namespace AdvancedPaste
|
||||
{
|
||||
ProcessNamedPipe(cmdArgs[2]);
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
await ShowWindow(); // This allows for direct access without using PowerToys Runner, not all functionality might work
|
||||
#endif
|
||||
}
|
||||
|
||||
private void ProcessNamedPipe(string pipeName)
|
||||
|
||||
@@ -558,7 +558,7 @@
|
||||
<TextBlock
|
||||
x:Uid="AIProvidersFlyoutHeader"
|
||||
Grid.Row="0"
|
||||
Style="{StaticResource BodyStrongTextBlockStyle}" />
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
|
||||
<ListView
|
||||
x:Name="AIProviderListView"
|
||||
Grid.Row="1"
|
||||
@@ -611,10 +611,10 @@
|
||||
CornerRadius="{StaticResource ControlCornerRadius}"
|
||||
Visibility="{x:Bind IsLocalModel, Mode=OneWay}">
|
||||
<TextBlock
|
||||
x:Uid="LocalModelBadge"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
FontSize="10"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Text="Local" />
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
|
||||
</Border>
|
||||
<!--<Border
|
||||
Grid.Column="2"
|
||||
|
||||
@@ -156,7 +156,7 @@
|
||||
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="20"
|
||||
Visibility="{x:Bind ViewModel.ClipboardHasData, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}">
|
||||
Visibility="{x:Bind ViewModel.ShowClipboardPreview, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
@@ -168,7 +168,8 @@
|
||||
Margin="0,0,4,0"
|
||||
VerticalAlignment="Center"
|
||||
IsEnabled="{x:Bind ViewModel.ClipboardHistoryEnabled, Mode=TwoWay}"
|
||||
Style="{StaticResource SubtleButtonStyle}">
|
||||
Style="{StaticResource SubtleButtonStyle}"
|
||||
Visibility="{x:Bind ViewModel.ShowClipboardHistoryButton, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}">
|
||||
<ToolTipService.ToolTip>
|
||||
<TextBlock x:Uid="ClipboardHistoryButtonToolTip" />
|
||||
</ToolTipService.ToolTip>
|
||||
@@ -263,16 +264,17 @@
|
||||
<Button
|
||||
Padding="0"
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource SubtleButtonStyle}">
|
||||
Style="{StaticResource SubtleButtonStyle}"
|
||||
Visibility="{x:Bind ViewModel.HasLegalLinks, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
<FontIcon FontSize="12" Glyph="" />
|
||||
<Button.Flyout>
|
||||
<Flyout>
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock TextWrapping="Wrap">
|
||||
<Run x:Uid="AIMistakeNote" /><LineBreak /><Run
|
||||
x:Uid="CustomEndpointWarning"
|
||||
FontSize="12"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Text="You are using a custom endpoint. Verify all answers." />
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
|
||||
</TextBlock>
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<HyperlinkButton
|
||||
@@ -297,47 +299,49 @@
|
||||
</StackPanel>
|
||||
</controls:PromptBox.Footer>
|
||||
</controls:PromptBox>
|
||||
<Grid Grid.Row="2" RowSpacing="4">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="{x:Bind ViewModel.StandardPasteFormats.Count, Mode=OneWay, Converter={StaticResource standardPasteFormatsToHeightConverter}}" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" MinHeight="{x:Bind ViewModel.CustomActionPasteFormats.Count, Mode=OneWay, Converter={StaticResource customActionsToMinHeightConverter}}" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<ListView
|
||||
x:Name="PasteOptionsListView"
|
||||
Grid.Row="0"
|
||||
VerticalAlignment="Bottom"
|
||||
IsItemClickEnabled="True"
|
||||
ItemClick="PasteFormat_ItemClick"
|
||||
ItemContainerTransitions="{x:Null}"
|
||||
ItemTemplateSelector="{StaticResource PasteFormatTemplateSelector}"
|
||||
ItemsSource="{x:Bind ViewModel.StandardPasteFormats, Mode=OneWay}"
|
||||
ScrollViewer.VerticalScrollBarVisibility="Visible"
|
||||
ScrollViewer.VerticalScrollMode="Auto"
|
||||
SelectionMode="None"
|
||||
TabIndex="1" />
|
||||
<Rectangle
|
||||
Grid.Row="1"
|
||||
Height="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
Fill="{ThemeResource DividerStrokeColorDefaultBrush}"
|
||||
Visibility="{x:Bind ViewModel.CustomActionPasteFormats.Count, Mode=OneWay, Converter={StaticResource countToVisibilityConverter}}" />
|
||||
<ScrollViewer Grid.Row="2">
|
||||
<Grid RowSpacing="4">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="{x:Bind ViewModel.StandardPasteFormats.Count, Mode=OneWay, Converter={StaticResource standardPasteFormatsToHeightConverter}}" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" MinHeight="{x:Bind ViewModel.CustomActionPasteFormats.Count, Mode=OneWay, Converter={StaticResource customActionsToMinHeightConverter}}" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<ListView
|
||||
x:Name="PasteOptionsListView"
|
||||
Grid.Row="0"
|
||||
VerticalAlignment="Bottom"
|
||||
IsItemClickEnabled="True"
|
||||
ItemClick="PasteFormat_ItemClick"
|
||||
ItemContainerTransitions="{x:Null}"
|
||||
ItemTemplateSelector="{StaticResource PasteFormatTemplateSelector}"
|
||||
ItemsSource="{x:Bind ViewModel.StandardPasteFormats, Mode=OneWay}"
|
||||
ScrollViewer.VerticalScrollBarVisibility="Disabled"
|
||||
ScrollViewer.VerticalScrollMode="Disabled"
|
||||
SelectionMode="None"
|
||||
TabIndex="1" />
|
||||
<Rectangle
|
||||
Grid.Row="1"
|
||||
Height="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
Fill="{ThemeResource DividerStrokeColorDefaultBrush}"
|
||||
Visibility="{x:Bind ViewModel.CustomActionPasteFormats.Count, Mode=OneWay, Converter={StaticResource countToVisibilityConverter}}" />
|
||||
|
||||
<ListView
|
||||
x:Name="CustomActionsListView"
|
||||
Grid.Row="2"
|
||||
VerticalAlignment="Top"
|
||||
IsItemClickEnabled="True"
|
||||
ItemClick="PasteFormat_ItemClick"
|
||||
ItemContainerTransitions="{x:Null}"
|
||||
ItemTemplateSelector="{StaticResource PasteFormatTemplateSelector}"
|
||||
ItemsSource="{x:Bind ViewModel.CustomActionPasteFormats, Mode=OneWay}"
|
||||
ScrollViewer.VerticalScrollBarVisibility="Visible"
|
||||
ScrollViewer.VerticalScrollMode="Auto"
|
||||
SelectionMode="None"
|
||||
TabIndex="2" />
|
||||
</Grid>
|
||||
<ListView
|
||||
x:Name="CustomActionsListView"
|
||||
Grid.Row="2"
|
||||
VerticalAlignment="Top"
|
||||
IsItemClickEnabled="True"
|
||||
ItemClick="PasteFormat_ItemClick"
|
||||
ItemContainerTransitions="{x:Null}"
|
||||
ItemTemplateSelector="{StaticResource PasteFormatTemplateSelector}"
|
||||
ItemsSource="{x:Bind ViewModel.CustomActionPasteFormats, Mode=OneWay}"
|
||||
ScrollViewer.VerticalScrollBarVisibility="Disabled"
|
||||
ScrollViewer.VerticalScrollMode="Disabled"
|
||||
SelectionMode="None"
|
||||
TabIndex="2" />
|
||||
</Grid>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</Page>
|
||||
|
||||
@@ -18,6 +18,7 @@ namespace AdvancedPaste.Helpers
|
||||
PromptTokens = semanticKernelFormatEvent.PromptTokens;
|
||||
CompletionTokens = semanticKernelFormatEvent.CompletionTokens;
|
||||
ModelName = semanticKernelFormatEvent.ModelName;
|
||||
ProviderType = semanticKernelFormatEvent.ProviderType;
|
||||
ActionChain = semanticKernelFormatEvent.ActionChain;
|
||||
}
|
||||
|
||||
@@ -38,6 +39,8 @@ namespace AdvancedPaste.Helpers
|
||||
|
||||
public string ModelName { get; set; }
|
||||
|
||||
public string ProviderType { get; set; }
|
||||
|
||||
public string ActionChain { get; set; }
|
||||
|
||||
public string ToJsonString() => JsonSerializer.Serialize(this, SourceGenerationContext.Default.AIServiceFormatEvent);
|
||||
|
||||
@@ -19,6 +19,8 @@ namespace AdvancedPaste.Settings
|
||||
|
||||
public bool CloseAfterLosingFocus { get; }
|
||||
|
||||
public bool EnableClipboardPreview { get; }
|
||||
|
||||
public IReadOnlyList<AdvancedPasteCustomAction> CustomActions { get; }
|
||||
|
||||
public IReadOnlyList<PasteFormats> AdditionalActions { get; }
|
||||
|
||||
@@ -40,6 +40,8 @@ namespace AdvancedPaste.Settings
|
||||
|
||||
public bool CloseAfterLosingFocus { get; private set; }
|
||||
|
||||
public bool EnableClipboardPreview { get; private set; }
|
||||
|
||||
public IReadOnlyList<PasteFormats> AdditionalActions => _additionalActions;
|
||||
|
||||
public IReadOnlyList<AdvancedPasteCustomAction> CustomActions => _customActions;
|
||||
@@ -53,6 +55,7 @@ namespace AdvancedPaste.Settings
|
||||
IsAIEnabled = false;
|
||||
ShowCustomPreview = true;
|
||||
CloseAfterLosingFocus = false;
|
||||
EnableClipboardPreview = true;
|
||||
PasteAIConfiguration = new PasteAIConfiguration();
|
||||
_additionalActions = [];
|
||||
_customActions = [];
|
||||
@@ -107,6 +110,7 @@ namespace AdvancedPaste.Settings
|
||||
IsAIEnabled = properties.IsAIEnabled;
|
||||
ShowCustomPreview = properties.ShowCustomPreview;
|
||||
CloseAfterLosingFocus = properties.CloseAfterLosingFocus;
|
||||
EnableClipboardPreview = properties.EnableClipboardPreview;
|
||||
PasteAIConfiguration = properties.PasteAIConfiguration ?? new PasteAIConfiguration();
|
||||
|
||||
var sourceAdditionalActions = properties.AdditionalActions;
|
||||
@@ -163,28 +167,134 @@ namespace AdvancedPaste.Settings
|
||||
return false;
|
||||
}
|
||||
|
||||
if (settings.Properties.IsAIEnabled || !LegacyOpenAIKeyExists())
|
||||
var properties = settings.Properties;
|
||||
bool legacyAdvancedAIConsumed = properties.TryConsumeLegacyAdvancedAIEnabled(out var advancedFlag);
|
||||
bool legacyAdvancedAIEnabled = legacyAdvancedAIConsumed && advancedFlag;
|
||||
PasswordCredential legacyCredential = TryGetLegacyOpenAICredential();
|
||||
|
||||
if (legacyCredential is null)
|
||||
{
|
||||
return false;
|
||||
return legacyAdvancedAIConsumed;
|
||||
}
|
||||
|
||||
settings.Properties.IsAIEnabled = true;
|
||||
return true;
|
||||
var configuration = properties.PasteAIConfiguration;
|
||||
|
||||
if (configuration is null)
|
||||
{
|
||||
configuration = new PasteAIConfiguration();
|
||||
properties.PasteAIConfiguration = configuration;
|
||||
}
|
||||
|
||||
bool configurationUpdated = false;
|
||||
|
||||
var ensureResult = AdvancedPasteMigrationHelper.EnsureOpenAIProvider(configuration);
|
||||
PasteAIProviderDefinition openAIProvider = ensureResult.Provider;
|
||||
configurationUpdated |= ensureResult.Updated;
|
||||
|
||||
if (legacyAdvancedAIConsumed && openAIProvider is not null && openAIProvider.EnableAdvancedAI != legacyAdvancedAIEnabled)
|
||||
{
|
||||
openAIProvider.EnableAdvancedAI = legacyAdvancedAIEnabled;
|
||||
configurationUpdated = true;
|
||||
}
|
||||
|
||||
if (openAIProvider is not null)
|
||||
{
|
||||
StoreMigratedOpenAICredential(openAIProvider.Id, openAIProvider.ServiceType, legacyCredential.Password);
|
||||
RemoveLegacyOpenAICredential();
|
||||
}
|
||||
|
||||
const bool shouldEnableAI = true;
|
||||
bool enabledUpdated = false;
|
||||
if (properties.IsAIEnabled != shouldEnableAI)
|
||||
{
|
||||
properties.IsAIEnabled = shouldEnableAI;
|
||||
enabledUpdated = true;
|
||||
}
|
||||
|
||||
return configurationUpdated || enabledUpdated || legacyAdvancedAIConsumed;
|
||||
}
|
||||
|
||||
private static bool LegacyOpenAIKeyExists()
|
||||
private static PasswordCredential TryGetLegacyOpenAICredential()
|
||||
{
|
||||
try
|
||||
{
|
||||
PasswordVault vault = new();
|
||||
return vault.Retrieve("https://platform.openai.com/api-keys", "PowerToys_AdvancedPaste_OpenAIKey") is not null;
|
||||
var credential = vault.Retrieve("https://platform.openai.com/api-keys", "PowerToys_AdvancedPaste_OpenAIKey");
|
||||
credential?.RetrievePassword();
|
||||
return credential;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static void RemoveLegacyOpenAICredential()
|
||||
{
|
||||
try
|
||||
{
|
||||
PasswordVault vault = new();
|
||||
TryRemoveCredential(vault, "https://platform.openai.com/api-keys", "PowerToys_AdvancedPaste_OpenAIKey");
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private static void StoreMigratedOpenAICredential(string providerId, string serviceType, string password)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(password))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var serviceKind = serviceType.ToAIServiceType();
|
||||
if (serviceKind != AIServiceType.OpenAI)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string resource = "https://platform.openai.com/api-keys";
|
||||
string username = $"PowerToys_AdvancedPaste_PasteAI_openai_{NormalizeProviderIdentifier(providerId)}";
|
||||
|
||||
PasswordVault vault = new();
|
||||
TryRemoveCredential(vault, resource, username);
|
||||
|
||||
PasswordCredential credential = new(resource, username, password);
|
||||
vault.Add(credential);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Failed to migrate legacy OpenAI credential", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static void TryRemoveCredential(PasswordVault vault, string credentialResource, string credentialUserName)
|
||||
{
|
||||
try
|
||||
{
|
||||
PasswordCredential existingCred = vault.Retrieve(credentialResource, credentialUserName);
|
||||
vault.Remove(existingCred);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Credential doesn't exist, which is fine
|
||||
}
|
||||
}
|
||||
|
||||
private static string NormalizeProviderIdentifier(string providerId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(providerId))
|
||||
{
|
||||
return "default";
|
||||
}
|
||||
|
||||
var filtered = new string(providerId.Where(char.IsLetterOrDigit).ToArray());
|
||||
return string.IsNullOrWhiteSpace(filtered) ? "default" : filtered.ToLowerInvariant();
|
||||
}
|
||||
|
||||
public async Task SetActiveAIProviderAsync(string providerId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(providerId))
|
||||
|
||||
@@ -11,12 +11,6 @@ using AdvancedPaste.Settings;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.SemanticKernel;
|
||||
using Microsoft.SemanticKernel.ChatCompletion;
|
||||
using Microsoft.SemanticKernel.Connectors.Amazon;
|
||||
using Microsoft.SemanticKernel.Connectors.AzureAIInference;
|
||||
using Microsoft.SemanticKernel.Connectors.Google;
|
||||
using Microsoft.SemanticKernel.Connectors.HuggingFace;
|
||||
using Microsoft.SemanticKernel.Connectors.MistralAI;
|
||||
using Microsoft.SemanticKernel.Connectors.Ollama;
|
||||
using Microsoft.SemanticKernel.Connectors.OpenAI;
|
||||
|
||||
namespace AdvancedPaste.Services;
|
||||
@@ -220,7 +214,7 @@ public sealed class AdvancedAIKernelService : KernelServiceBase
|
||||
var serviceType = GetRuntimeConfiguration().ServiceType;
|
||||
return new OpenAIPromptExecutionSettings
|
||||
{
|
||||
FunctionChoiceBehavior = FunctionChoiceBehavior.Required(),
|
||||
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(),
|
||||
Temperature = 0.01,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -83,32 +83,39 @@ namespace AdvancedPaste.Services.CustomActions
|
||||
SystemPrompt = systemPrompt,
|
||||
};
|
||||
|
||||
var operationStart = DateTime.UtcNow;
|
||||
|
||||
var providerContent = await provider.ProcessPasteAsync(
|
||||
request,
|
||||
cancellationToken,
|
||||
progress);
|
||||
|
||||
var durationMs = (int)Math.Round((DateTime.UtcNow - operationStart).TotalMilliseconds);
|
||||
|
||||
var usage = request.Usage;
|
||||
var content = providerContent ?? string.Empty;
|
||||
|
||||
// Log endpoint usage
|
||||
var endpointEvent = new AdvancedPasteEndpointUsageEvent(providerConfig.ProviderType);
|
||||
// Log endpoint usage (custom action pipeline is not the advanced SK flow)
|
||||
var endpointEvent = new AdvancedPasteEndpointUsageEvent(providerConfig.ProviderType, providerConfig.Model ?? string.Empty, isAdvanced: false, durationMs: durationMs);
|
||||
PowerToysTelemetry.Log.WriteEvent(endpointEvent);
|
||||
|
||||
Logger.LogDebug($"{nameof(CustomActionTransformService)}.{nameof(TransformAsync)} complete; ModelName={providerConfig.Model ?? string.Empty}, PromptTokens={usage.PromptTokens}, CompletionTokens={usage.CompletionTokens}");
|
||||
Logger.LogDebug($"{nameof(CustomActionTransformService)}.{nameof(TransformAsync)} complete; ModelName={providerConfig.Model ?? string.Empty}, PromptTokens={usage.PromptTokens}, CompletionTokens={usage.CompletionTokens}, DurationMs={durationMs}");
|
||||
|
||||
return new CustomActionTransformResult(content, usage);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"{nameof(CustomActionTransformService)}.{nameof(TransformAsync)} failed", ex);
|
||||
var statusCode = ExtractStatusCode(ex);
|
||||
var modelName = providerConfig.Model ?? string.Empty;
|
||||
AdvancedPasteCustomActionErrorEvent errorEvent = new(providerConfig.ProviderType, modelName, statusCode, ex is PasteActionModeratedException ? PasteActionModeratedException.ErrorDescription : ex.Message);
|
||||
PowerToysTelemetry.Log.WriteEvent(errorEvent);
|
||||
|
||||
if (ex is PasteActionException or OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
|
||||
var statusCode = ExtractStatusCode(ex);
|
||||
var failureMessage = providerConfig.ProviderType switch
|
||||
{
|
||||
AIServiceType.OpenAI or AIServiceType.AzureOpenAI => ErrorHelpers.TranslateErrorText(statusCode),
|
||||
@@ -181,8 +188,6 @@ namespace AdvancedPaste.Services.CustomActions
|
||||
{
|
||||
AIServiceType.Onnx => false,
|
||||
AIServiceType.Ollama => false,
|
||||
AIServiceType.Anthropic => false,
|
||||
AIServiceType.AmazonBedrock => false,
|
||||
_ => true,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AdvancedPaste.Helpers;
|
||||
using AdvancedPaste.Models;
|
||||
using LanguageModelProvider;
|
||||
using Microsoft.Extensions.AI;
|
||||
@@ -23,7 +24,7 @@ public sealed class FoundryLocalPasteProvider : IPasteAIProvider
|
||||
|
||||
public static PasteAIProviderRegistration Registration { get; } = new(SupportedTypes, config => new FoundryLocalPasteProvider(config));
|
||||
|
||||
private static readonly LanguageModelService LanguageModels = LanguageModelService.CreateDefault();
|
||||
private static readonly FoundryLocalModelProvider _modelProvider = FoundryLocalModelProvider.Instance;
|
||||
|
||||
private readonly PasteAIConfig _config;
|
||||
|
||||
@@ -33,10 +34,6 @@ public sealed class FoundryLocalPasteProvider : IPasteAIProvider
|
||||
_config = config;
|
||||
}
|
||||
|
||||
public string ProviderName => AIServiceType.FoundryLocal.ToNormalizedKey();
|
||||
|
||||
public string DisplayName => string.IsNullOrWhiteSpace(_config?.Model) ? "Foundry Local" : _config.Model;
|
||||
|
||||
public async Task<bool> IsAvailableAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
@@ -72,21 +69,25 @@ public sealed class FoundryLocalPasteProvider : IPasteAIProvider
|
||||
throw new PasteActionException(
|
||||
"No Foundry Local model selected",
|
||||
new InvalidOperationException("Model identifier is required"),
|
||||
aiServiceMessage: "Please select a model in the AI provider settings. Model identifier should be in the format 'fl://model-name'.");
|
||||
aiServiceMessage: "Please select a model in the AI provider settings.");
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var chatClient = LanguageModels.GetClient(modelReference);
|
||||
if (chatClient is null)
|
||||
{
|
||||
throw new PasteActionException(
|
||||
$"Unable to load Foundry Local model: {modelReference}",
|
||||
new InvalidOperationException("Chat client resolution failed"),
|
||||
aiServiceMessage: "The model may not be downloaded or the Foundry Local service may not be running. Please check the model status in settings.");
|
||||
}
|
||||
|
||||
// Extract actual model ID from the URL (format: fl://modelId)
|
||||
var actualModelId = modelReference.Replace("fl://", string.Empty).Trim('/');
|
||||
IChatClient chatClient;
|
||||
try
|
||||
{
|
||||
chatClient = _modelProvider.GetIChatClient(modelReference);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
// GetIChatClient throws InvalidOperationException for user-facing errors
|
||||
var errorMessage = string.Format(System.Globalization.CultureInfo.CurrentCulture, ResourceLoaderInstance.ResourceLoader.GetString("FoundryLocal_UnableToLoadModel"), modelReference);
|
||||
throw new PasteActionException(
|
||||
errorMessage,
|
||||
ex,
|
||||
aiServiceMessage: ex.Message);
|
||||
}
|
||||
|
||||
var userMessageContent = $"""
|
||||
User instructions:
|
||||
@@ -104,7 +105,7 @@ public sealed class FoundryLocalPasteProvider : IPasteAIProvider
|
||||
new(ChatRole.User, userMessageContent),
|
||||
};
|
||||
|
||||
var chatOptions = CreateChatOptions(_config?.SystemPrompt, actualModelId);
|
||||
var chatOptions = CreateChatOptions(_config?.SystemPrompt, modelReference);
|
||||
|
||||
progress?.Report(0.1);
|
||||
|
||||
|
||||
@@ -11,10 +11,8 @@ using AdvancedPaste.Models;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.SemanticKernel;
|
||||
using Microsoft.SemanticKernel.ChatCompletion;
|
||||
using Microsoft.SemanticKernel.Connectors.Amazon;
|
||||
using Microsoft.SemanticKernel.Connectors.AzureAIInference;
|
||||
using Microsoft.SemanticKernel.Connectors.Google;
|
||||
using Microsoft.SemanticKernel.Connectors.HuggingFace;
|
||||
using Microsoft.SemanticKernel.Connectors.MistralAI;
|
||||
using Microsoft.SemanticKernel.Connectors.Ollama;
|
||||
using Microsoft.SemanticKernel.Connectors.OpenAI;
|
||||
@@ -29,11 +27,8 @@ namespace AdvancedPaste.Services.CustomActions
|
||||
AIServiceType.AzureOpenAI,
|
||||
AIServiceType.Mistral,
|
||||
AIServiceType.Google,
|
||||
AIServiceType.HuggingFace,
|
||||
AIServiceType.AzureAIInference,
|
||||
AIServiceType.Ollama,
|
||||
AIServiceType.Anthropic,
|
||||
AIServiceType.AmazonBedrock,
|
||||
};
|
||||
|
||||
public static PasteAIProviderRegistration Registration { get; } = new(SupportedTypes, config => new SemanticKernelPasteProvider(config));
|
||||
@@ -142,21 +137,12 @@ namespace AdvancedPaste.Services.CustomActions
|
||||
case AIServiceType.Google:
|
||||
kernelBuilder.AddGoogleAIGeminiChatCompletion(_config.Model, apiKey: apiKey);
|
||||
break;
|
||||
case AIServiceType.HuggingFace:
|
||||
kernelBuilder.AddHuggingFaceChatCompletion(_config.Model, apiKey: apiKey);
|
||||
break;
|
||||
case AIServiceType.AzureAIInference:
|
||||
kernelBuilder.AddAzureAIInferenceChatCompletion(_config.Model, apiKey: apiKey, endpoint: new Uri(endpoint));
|
||||
break;
|
||||
case AIServiceType.Ollama:
|
||||
kernelBuilder.AddOllamaChatCompletion(_config.Model, endpoint: new Uri(endpoint));
|
||||
break;
|
||||
case AIServiceType.Anthropic:
|
||||
kernelBuilder.AddBedrockChatCompletionService(_config.Model);
|
||||
break;
|
||||
case AIServiceType.AmazonBedrock:
|
||||
kernelBuilder.AddBedrockChatCompletionService(_config.Model);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new NotSupportedException($"Provider '{_config.ProviderType}' is not supported by {nameof(SemanticKernelPasteProvider)}");
|
||||
@@ -184,8 +170,6 @@ namespace AdvancedPaste.Services.CustomActions
|
||||
return serviceType switch
|
||||
{
|
||||
AIServiceType.Ollama => false,
|
||||
AIServiceType.Anthropic => false,
|
||||
AIServiceType.AmazonBedrock => false,
|
||||
_ => true,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -156,16 +156,10 @@ public sealed class EnhancedVaultCredentialsProvider : IAICredentialsProvider
|
||||
resource = "https://ai.google.dev/";
|
||||
serviceKey = "google";
|
||||
break;
|
||||
case AIServiceType.HuggingFace:
|
||||
resource = "https://huggingface.co/settings/tokens";
|
||||
serviceKey = "huggingface";
|
||||
break;
|
||||
case AIServiceType.FoundryLocal:
|
||||
case AIServiceType.ML:
|
||||
case AIServiceType.Onnx:
|
||||
case AIServiceType.Ollama:
|
||||
case AIServiceType.Anthropic:
|
||||
case AIServiceType.AmazonBedrock:
|
||||
return null;
|
||||
default:
|
||||
return null;
|
||||
|
||||
@@ -29,6 +29,7 @@ public abstract class KernelServiceBase(
|
||||
ICustomActionTransformService customActionTransformService) : IKernelService
|
||||
{
|
||||
private const string PromptParameterName = "prompt";
|
||||
private const string DefaultSystemPrompt = "You are an agent who is tasked with helping users paste their clipboard data. You have functions available to help you with this task. Call function when necessary to help user finish the transformation task. You never need to ask permission, always try to do as the user asks. The user will only input one message and will not be available for further questions, so try your best. The user will put in a request to format their clipboard data and you will fulfill it. Do not output anything else besides the reformatted clipboard content.";
|
||||
|
||||
private readonly IKernelQueryCacheService _queryCacheService = queryCacheService;
|
||||
private readonly IPromptModerationService _promptModerationService = promptModerationService;
|
||||
@@ -144,7 +145,8 @@ public abstract class KernelServiceBase(
|
||||
|
||||
ChatHistory chatHistory = [];
|
||||
|
||||
chatHistory.AddSystemMessage(runtimeConfig.SystemPrompt);
|
||||
var systemPrompt = string.IsNullOrWhiteSpace(runtimeConfig.SystemPrompt) ? DefaultSystemPrompt : runtimeConfig.SystemPrompt;
|
||||
chatHistory.AddSystemMessage(systemPrompt);
|
||||
chatHistory.AddSystemMessage($"Available clipboard formats: {await kernel.GetDataFormatsAsync()}");
|
||||
chatHistory.AddUserMessage(prompt);
|
||||
|
||||
@@ -186,12 +188,20 @@ public abstract class KernelServiceBase(
|
||||
|
||||
private void LogResult(bool cacheUsed, bool isSavedQuery, IEnumerable<ActionChainItem> actionChain, AIServiceUsage usage)
|
||||
{
|
||||
AdvancedPasteSemanticKernelFormatEvent telemetryEvent = new(cacheUsed, isSavedQuery, usage.PromptTokens, usage.CompletionTokens, AdvancedAIModelName, AdvancedPasteSemanticKernelFormatEvent.FormatActionChain(actionChain));
|
||||
var runtimeConfig = GetRuntimeConfiguration();
|
||||
|
||||
AdvancedPasteSemanticKernelFormatEvent telemetryEvent = new(
|
||||
cacheUsed,
|
||||
isSavedQuery,
|
||||
usage.PromptTokens,
|
||||
usage.CompletionTokens,
|
||||
AdvancedAIModelName,
|
||||
runtimeConfig.ServiceType.ToString(),
|
||||
AdvancedPasteSemanticKernelFormatEvent.FormatActionChain(actionChain));
|
||||
PowerToysTelemetry.Log.WriteEvent(telemetryEvent);
|
||||
|
||||
// Log endpoint usage
|
||||
var runtimeConfig = GetRuntimeConfiguration();
|
||||
var endpointEvent = new AdvancedPasteEndpointUsageEvent(runtimeConfig.ServiceType);
|
||||
var endpointEvent = new AdvancedPasteEndpointUsageEvent(runtimeConfig.ServiceType, AdvancedAIModelName, isAdvanced: true);
|
||||
PowerToysTelemetry.Log.WriteEvent(endpointEvent);
|
||||
|
||||
var logEvent = new AIServiceFormatEvent(telemetryEvent);
|
||||
|
||||
@@ -160,10 +160,10 @@
|
||||
<value>Active provider: {0}</value>
|
||||
</data>
|
||||
<data name="AIProvidersFlyoutHeader.Text" xml:space="preserve">
|
||||
<value>AI providers</value>
|
||||
<value>Configured models</value>
|
||||
</data>
|
||||
<data name="AIProvidersEmptyText.Text" xml:space="preserve">
|
||||
<value>No AI providers configured</value>
|
||||
<value>No models configured</value>
|
||||
</data>
|
||||
<data name="AIProvidersManageButtonContent.Content" xml:space="preserve">
|
||||
<value>Configure models in Settings</value>
|
||||
@@ -359,6 +359,17 @@
|
||||
</data>
|
||||
<data name="Relative_Date_TimeFormat" xml:space="preserve">
|
||||
<value>{0}, {1}</value>
|
||||
<comment>(e.g., “10/20/2025, 17:05” in the user’s locale)</comment>
|
||||
<comment>(e.g., "10/20/2025, 17:05" in the user's locale)</comment>
|
||||
</data>
|
||||
<data name="CustomEndpointWarning" xml:space="preserve">
|
||||
<value>You are using a custom endpoint. Verify all answers.</value>
|
||||
</data>
|
||||
<data name="LocalModelBadge.Text" xml:space="preserve">
|
||||
<value>Local</value>
|
||||
<comment>Badge label displayed next to local AI model providers (e.g., Ollama, Foundry Local) to indicate the model runs locally</comment>
|
||||
</data>
|
||||
<data name="FoundryLocal_UnableToLoadModel" xml:space="preserve">
|
||||
<value>Unable to load Foundry Local model: {0}</value>
|
||||
<comment>{0} is the model identifier. Do not translate {0}.</comment>
|
||||
</data>
|
||||
</root>
|
||||
@@ -0,0 +1,34 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Diagnostics.Tracing;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.PowerToys.Telemetry;
|
||||
using Microsoft.PowerToys.Telemetry.Events;
|
||||
|
||||
namespace AdvancedPaste.Telemetry;
|
||||
|
||||
[EventData]
|
||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)]
|
||||
public sealed class AdvancedPasteCustomActionErrorEvent : EventBase, IEvent
|
||||
{
|
||||
public AdvancedPasteCustomActionErrorEvent(AIServiceType providerType, string modelName, int statusCode, string error)
|
||||
{
|
||||
ProviderType = providerType.ToString();
|
||||
ModelName = modelName;
|
||||
StatusCode = statusCode;
|
||||
Error = error;
|
||||
}
|
||||
|
||||
public string ProviderType { get; set; }
|
||||
|
||||
public string ModelName { get; set; }
|
||||
|
||||
public int StatusCode { get; set; }
|
||||
|
||||
public string Error { get; set; }
|
||||
|
||||
public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
|
||||
}
|
||||
@@ -15,13 +15,31 @@ namespace AdvancedPaste.Telemetry;
|
||||
public class AdvancedPasteEndpointUsageEvent : EventBase, IEvent
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the AI provider type (e.g., OpenAI, AzureOpenAI, Anthropic).
|
||||
/// Gets or sets the AI provider type (e.g., OpenAI, AzureOpenAI, Google).
|
||||
/// </summary>
|
||||
public string ProviderType { get; set; }
|
||||
|
||||
public AdvancedPasteEndpointUsageEvent(AIServiceType providerType)
|
||||
/// <summary>
|
||||
/// Gets or sets the configured model name.
|
||||
/// </summary>
|
||||
public string ModelName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the advanced AI pipeline was used.
|
||||
/// </summary>
|
||||
public bool IsAdvanced { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the total duration in milliseconds, or -1 if unavailable.
|
||||
/// </summary>
|
||||
public int DurationMs { get; set; }
|
||||
|
||||
public AdvancedPasteEndpointUsageEvent(AIServiceType providerType, string modelName, bool isAdvanced, int durationMs = -1)
|
||||
{
|
||||
ProviderType = providerType.ToString();
|
||||
ModelName = modelName;
|
||||
IsAdvanced = isAdvanced;
|
||||
DurationMs = durationMs;
|
||||
}
|
||||
|
||||
public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace AdvancedPaste.Telemetry;
|
||||
|
||||
[EventData]
|
||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)]
|
||||
public class AdvancedPasteSemanticKernelFormatEvent(bool cacheUsed, bool isSavedQuery, int promptTokens, int completionTokens, string modelName, string actionChain) : EventBase, IEvent
|
||||
public class AdvancedPasteSemanticKernelFormatEvent(bool cacheUsed, bool isSavedQuery, int promptTokens, int completionTokens, string modelName, string providerType, string actionChain) : EventBase, IEvent
|
||||
{
|
||||
public static string FormatActionChain(IEnumerable<ActionChainItem> actionChain) => FormatActionChain(actionChain.Select(item => item.Format));
|
||||
|
||||
@@ -30,6 +30,8 @@ public class AdvancedPasteSemanticKernelFormatEvent(bool cacheUsed, bool isSaved
|
||||
|
||||
public string ModelName { get; set; } = modelName;
|
||||
|
||||
public string ProviderType { get; set; } = providerType;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a comma-separated list of paste formats used - in the same order they were executed.
|
||||
/// Conceptually an array but formatted this way to work around https://github.com/dotnet/runtime/issues/10428
|
||||
|
||||
@@ -63,6 +63,7 @@ namespace AdvancedPaste.ViewModels
|
||||
private ClipboardFormat _availableClipboardFormats;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(ShowClipboardHistoryButton))]
|
||||
private bool _clipboardHistoryEnabled;
|
||||
|
||||
[ObservableProperty]
|
||||
@@ -76,6 +77,7 @@ namespace AdvancedPaste.ViewModels
|
||||
[NotifyPropertyChangedFor(nameof(PrivacyLinkUri))]
|
||||
[NotifyPropertyChangedFor(nameof(HasTermsLink))]
|
||||
[NotifyPropertyChangedFor(nameof(HasPrivacyLink))]
|
||||
[NotifyPropertyChangedFor(nameof(HasLegalLinks))]
|
||||
private bool _isAllowedByGPO;
|
||||
|
||||
[ObservableProperty]
|
||||
@@ -221,10 +223,16 @@ namespace AdvancedPaste.ViewModels
|
||||
|
||||
public bool HasPrivacyLink => GetActiveProviderMetadata().HasPrivacyLink;
|
||||
|
||||
public bool HasLegalLinks => HasTermsLink || HasPrivacyLink;
|
||||
|
||||
public bool ClipboardHasData => AvailableClipboardFormats != ClipboardFormat.None;
|
||||
|
||||
public bool ClipboardHasDataForCustomAI => PasteFormat.SupportsClipboardFormats(CustomAIFormat, AvailableClipboardFormats);
|
||||
|
||||
public bool ShowClipboardPreview => _userSettings.EnableClipboardPreview;
|
||||
|
||||
public bool ShowClipboardHistoryButton => ClipboardHistoryEnabled;
|
||||
|
||||
public bool HasIndeterminateTransformProgress => double.IsNaN(TransformProgress);
|
||||
|
||||
private PasteFormats CustomAIFormat =>
|
||||
@@ -310,6 +318,7 @@ namespace AdvancedPaste.ViewModels
|
||||
OnPropertyChanged(nameof(IsAdvancedAIEnabled));
|
||||
OnPropertyChanged(nameof(AIProviders));
|
||||
OnPropertyChanged(nameof(AllowedAIProviders));
|
||||
OnPropertyChanged(nameof(ShowClipboardPreview));
|
||||
|
||||
NotifyActiveProviderChanged();
|
||||
|
||||
@@ -361,6 +370,7 @@ namespace AdvancedPaste.ViewModels
|
||||
OnPropertyChanged(nameof(PrivacyLinkUri));
|
||||
OnPropertyChanged(nameof(HasTermsLink));
|
||||
OnPropertyChanged(nameof(HasPrivacyLink));
|
||||
OnPropertyChanged(nameof(HasLegalLinks));
|
||||
}
|
||||
|
||||
private void RefreshPasteFormats()
|
||||
@@ -798,7 +808,6 @@ namespace AdvancedPaste.ViewModels
|
||||
AIServiceType.AzureAIInference => PowerToys.GPOWrapper.GPOWrapper.GetAllowedAdvancedPasteAzureAIInferenceValue() != PowerToys.GPOWrapper.GpoRuleConfigured.Disabled,
|
||||
AIServiceType.Mistral => PowerToys.GPOWrapper.GPOWrapper.GetAllowedAdvancedPasteMistralValue() != PowerToys.GPOWrapper.GpoRuleConfigured.Disabled,
|
||||
AIServiceType.Google => PowerToys.GPOWrapper.GPOWrapper.GetAllowedAdvancedPasteGoogleValue() != PowerToys.GPOWrapper.GpoRuleConfigured.Disabled,
|
||||
AIServiceType.Anthropic => PowerToys.GPOWrapper.GPOWrapper.GetAllowedAdvancedPasteAnthropicValue() != PowerToys.GPOWrapper.GpoRuleConfigured.Disabled,
|
||||
AIServiceType.Ollama => PowerToys.GPOWrapper.GPOWrapper.GetAllowedAdvancedPasteOllamaValue() != PowerToys.GPOWrapper.GpoRuleConfigured.Disabled,
|
||||
AIServiceType.FoundryLocal => PowerToys.GPOWrapper.GPOWrapper.GetAllowedAdvancedPasteFoundryLocalValue() != PowerToys.GPOWrapper.GpoRuleConfigured.Disabled,
|
||||
_ => true, // Allow unknown types by default
|
||||
|
||||
@@ -82,6 +82,8 @@
|
||||
<ClCompile>
|
||||
<Optimization>Disabled</Optimization>
|
||||
<PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<RuntimeLibrary Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">MultiThreadedDebug</RuntimeLibrary>
|
||||
<RuntimeLibrary Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">MultiThreadedDebug</RuntimeLibrary>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<GenerateWindowsMetadata>false</GenerateWindowsMetadata>
|
||||
@@ -93,6 +95,8 @@
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
<PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<RuntimeLibrary Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">MultiThreaded</RuntimeLibrary>
|
||||
<RuntimeLibrary Condition="'$(Configuration)|$(Platform)'=='Release|x64'">MultiThreaded</RuntimeLibrary>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)'=='Debug'">
|
||||
<ClCompile>
|
||||
<PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<RuntimeLibrary>MultiThreadedDebugDLL</RuntimeLibrary>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
<LinkIncremental>true</LinkIncremental>
|
||||
</ClCompile>
|
||||
@@ -48,6 +49,7 @@
|
||||
<PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
<RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
<LinkIncremental>false</LinkIncremental>
|
||||
|
||||
@@ -11,19 +11,17 @@
|
||||
#include <logger/logger_settings.h>
|
||||
#include <logger/logger.h>
|
||||
#include <utils/logger_helper.h>
|
||||
#include "LightSwitchStateManager.h"
|
||||
#include <LightSwitchUtils.h>
|
||||
|
||||
SERVICE_STATUS g_ServiceStatus = {};
|
||||
SERVICE_STATUS_HANDLE g_StatusHandle = nullptr;
|
||||
HANDLE g_ServiceStopEvent = nullptr;
|
||||
extern int g_lastUpdatedDay = -1;
|
||||
static ScheduleMode prevMode = ScheduleMode::Off;
|
||||
static std::wstring prevLat, prevLon;
|
||||
static int prevMinutes = -1;
|
||||
static bool lastOverrideStatus = false;
|
||||
|
||||
VOID WINAPI ServiceMain(DWORD argc, LPTSTR* argv);
|
||||
VOID WINAPI ServiceCtrlHandler(DWORD dwCtrl);
|
||||
DWORD WINAPI ServiceWorkerThread(LPVOID lpParam);
|
||||
void ApplyTheme(bool shouldBeLight);
|
||||
|
||||
// Entry point for the executable
|
||||
int _tmain(int argc, TCHAR* argv[])
|
||||
@@ -124,31 +122,66 @@ VOID WINAPI ServiceCtrlHandler(DWORD dwCtrl)
|
||||
}
|
||||
}
|
||||
|
||||
static void update_sun_times(auto& settings)
|
||||
void ApplyTheme(bool shouldBeLight)
|
||||
{
|
||||
double latitude = std::stod(settings.latitude);
|
||||
double longitude = std::stod(settings.longitude);
|
||||
const auto& s = LightSwitchSettings::settings();
|
||||
|
||||
if (s.changeSystem)
|
||||
{
|
||||
bool isSystemCurrentlyLight = GetCurrentSystemTheme();
|
||||
if (shouldBeLight != isSystemCurrentlyLight)
|
||||
{
|
||||
SetSystemTheme(shouldBeLight);
|
||||
Logger::info(L"[LightSwitchService] Changed system theme to {}.", shouldBeLight ? L"light" : L"dark");
|
||||
}
|
||||
}
|
||||
|
||||
if (s.changeApps)
|
||||
{
|
||||
bool isAppsCurrentlyLight = GetCurrentAppsTheme();
|
||||
if (shouldBeLight != isAppsCurrentlyLight)
|
||||
{
|
||||
SetAppsTheme(shouldBeLight);
|
||||
Logger::info(L"[LightSwitchService] Changed apps theme to {}.", shouldBeLight ? L"light" : L"dark");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void DetectAndHandleExternalThemeChange(LightSwitchStateManager& stateManager)
|
||||
{
|
||||
const auto& s = LightSwitchSettings::settings();
|
||||
if (s.scheduleMode == ScheduleMode::Off)
|
||||
return;
|
||||
|
||||
SYSTEMTIME st;
|
||||
GetLocalTime(&st);
|
||||
int nowMinutes = st.wHour * 60 + st.wMinute;
|
||||
|
||||
SunTimes newTimes = CalculateSunriseSunset(latitude, longitude, st.wYear, st.wMonth, st.wDay);
|
||||
// Compute effective boundaries (with offsets if needed)
|
||||
int effectiveLight = s.lightTime;
|
||||
int effectiveDark = s.darkTime;
|
||||
|
||||
int newLightTime = newTimes.sunriseHour * 60 + newTimes.sunriseMinute;
|
||||
int newDarkTime = newTimes.sunsetHour * 60 + newTimes.sunsetMinute;
|
||||
try
|
||||
if (s.scheduleMode == ScheduleMode::SunsetToSunrise)
|
||||
{
|
||||
auto values = PowerToysSettings::PowerToyValues::load_from_settings_file(L"LightSwitch");
|
||||
values.add_property(L"lightTime", newLightTime);
|
||||
values.add_property(L"darkTime", newDarkTime);
|
||||
values.save_to_settings_file();
|
||||
|
||||
Logger::info(L"[LightSwitchService] Updated sun times and saved to config.");
|
||||
effectiveLight = (s.lightTime + s.sunrise_offset) % 1440;
|
||||
effectiveDark = (s.darkTime + s.sunset_offset) % 1440;
|
||||
}
|
||||
catch (const std::exception& e)
|
||||
|
||||
// Use shared helper (handles wraparound logic)
|
||||
bool shouldBeLight = ShouldBeLight(nowMinutes, effectiveLight, effectiveDark);
|
||||
|
||||
// Compare current system/apps theme
|
||||
bool currentSystemLight = GetCurrentSystemTheme();
|
||||
bool currentAppsLight = GetCurrentAppsTheme();
|
||||
|
||||
bool systemMismatch = s.changeSystem && (currentSystemLight != shouldBeLight);
|
||||
bool appsMismatch = s.changeApps && (currentAppsLight != shouldBeLight);
|
||||
|
||||
// Trigger manual override only if mismatch and not already active
|
||||
if ((systemMismatch || appsMismatch) && !stateManager.GetState().isManualOverride)
|
||||
{
|
||||
std::wstring wmsg(e.what(), e.what() + strlen(e.what()));
|
||||
Logger::error(L"[LightSwitchService] Exception during sun time update: {}", wmsg);
|
||||
Logger::info(L"[LightSwitchService] External theme change detected (Windows Settings). Entering manual override mode.");
|
||||
stateManager.OnManualOverride();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,307 +195,98 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
|
||||
Logger::info(L"[LightSwitchService] Worker thread starting...");
|
||||
Logger::info(L"[LightSwitchService] Parent PID: {}", parentPid);
|
||||
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
// Initialization
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
static LightSwitchStateManager stateManager;
|
||||
|
||||
LightSwitchSettings::instance().InitFileWatcher();
|
||||
|
||||
HANDLE hManualOverride = OpenEventW(SYNCHRONIZE | EVENT_MODIFY_STATE, FALSE, L"POWERTOYS_LIGHTSWITCH_MANUAL_OVERRIDE");
|
||||
HANDLE hSettingsChanged = LightSwitchSettings::instance().GetSettingsChangedEvent();
|
||||
|
||||
LightSwitchSettings::instance().LoadSettings();
|
||||
auto& settings = LightSwitchSettings::instance().settings();
|
||||
const auto& settings = LightSwitchSettings::instance().settings();
|
||||
|
||||
SYSTEMTIME st;
|
||||
GetLocalTime(&st);
|
||||
int nowMinutes = st.wHour * 60 + st.wMinute;
|
||||
|
||||
// Handle initial theme application if necessary
|
||||
if (settings.scheduleMode != ScheduleMode::Off)
|
||||
{
|
||||
Logger::info(L"[LightSwitchService] Schedule mode is set to {}. Applying theme if necessary.", settings.scheduleMode);
|
||||
LightSwitchSettings::instance().ApplyThemeIfNecessary();
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::info(L"[LightSwitchService] Schedule mode is set to Off.");
|
||||
}
|
||||
Logger::info(L"[LightSwitchService] Initialized at {:02d}:{:02d}.", st.wHour, st.wMinute);
|
||||
stateManager.SyncInitialThemeState();
|
||||
stateManager.OnTick(nowMinutes);
|
||||
|
||||
g_lastUpdatedDay = st.wDay;
|
||||
Logger::info(L"[LightSwitchService] Initializing g_lastUpdatedDay to {}.", g_lastUpdatedDay);
|
||||
ULONGLONG lastSettingsReload = 0;
|
||||
|
||||
// ticker loop
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
// Worker Loop
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
for (;;)
|
||||
{
|
||||
HANDLE waits[2] = { g_ServiceStopEvent, hParent };
|
||||
DWORD count = hParent ? 2 : 1;
|
||||
bool skipRest = false;
|
||||
|
||||
const auto& settings = LightSwitchSettings::instance().settings();
|
||||
|
||||
// If the mode is set to Off, suspend the scheduler and avoid extra work
|
||||
if (settings.scheduleMode == ScheduleMode::Off)
|
||||
{
|
||||
Logger::info(L"[LightSwitchService] Schedule mode is OFF - suspending scheduler but keeping service alive.");
|
||||
|
||||
if (!hManualOverride)
|
||||
hManualOverride = OpenEventW(SYNCHRONIZE | EVENT_MODIFY_STATE, FALSE, L"POWERTOYS_LIGHTSWITCH_MANUAL_OVERRIDE");
|
||||
|
||||
HANDLE waitsOff[4];
|
||||
DWORD countOff = 0;
|
||||
waitsOff[countOff++] = g_ServiceStopEvent;
|
||||
if (hParent)
|
||||
waitsOff[countOff++] = hParent;
|
||||
if (hManualOverride)
|
||||
waitsOff[countOff++] = hManualOverride;
|
||||
waitsOff[countOff++] = LightSwitchSettings::instance().GetSettingsChangedEvent();
|
||||
|
||||
for (;;)
|
||||
{
|
||||
DWORD wait = WaitForMultipleObjects(countOff, waitsOff, FALSE, INFINITE);
|
||||
|
||||
if (wait == WAIT_OBJECT_0)
|
||||
{
|
||||
Logger::info(L"[LightSwitchService] Stop event triggered - exiting worker loop.");
|
||||
goto cleanup;
|
||||
}
|
||||
if (hParent && wait == WAIT_OBJECT_0 + 1)
|
||||
{
|
||||
Logger::info(L"[LightSwitchService] Parent exited - stopping service.");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
if (wait == WAIT_OBJECT_0 + (hParent ? 2 : 1))
|
||||
{
|
||||
Logger::info(L"[LightSwitchService] Manual override received while schedule OFF.");
|
||||
ResetEvent(hManualOverride);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (wait == WAIT_OBJECT_0 + (hParent ? 3 : 2))
|
||||
{
|
||||
Logger::trace(L"[LightSwitchService] Settings change event triggered, reloading settings...");
|
||||
ResetEvent(LightSwitchSettings::instance().GetSettingsChangedEvent());
|
||||
const auto& newSettings = LightSwitchSettings::instance().settings();
|
||||
lastSettingsReload = GetTickCount64();
|
||||
|
||||
if (newSettings.scheduleMode != ScheduleMode::Off)
|
||||
{
|
||||
Logger::info(L"[LightSwitchService] Schedule re-enabled, resuming normal loop.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
bool scheduleJustEnabled = (prevMode == ScheduleMode::Off && settings.scheduleMode != ScheduleMode::Off);
|
||||
prevMode = settings.scheduleMode;
|
||||
|
||||
ULONGLONG nowTick = GetTickCount64();
|
||||
bool recentSettingsReload = (nowTick - lastSettingsReload < 2000);
|
||||
|
||||
Logger::debug(L"[LightSwitchService] Current g_lastUpdatedDay value = {}.", g_lastUpdatedDay);
|
||||
|
||||
// Manual Override Detection Logic
|
||||
bool manualOverrideActive = (hManualOverride && WaitForSingleObject(hManualOverride, 0) == WAIT_OBJECT_0);
|
||||
|
||||
if (manualOverrideActive != lastOverrideStatus)
|
||||
{
|
||||
Logger::debug(L"[LightSwitchService] Manual override active = {}", manualOverrideActive);
|
||||
lastOverrideStatus = manualOverrideActive;
|
||||
}
|
||||
|
||||
if (settings.scheduleMode != ScheduleMode::Off && !recentSettingsReload && !scheduleJustEnabled && !manualOverrideActive)
|
||||
{
|
||||
bool currentSystemTheme = GetCurrentSystemTheme();
|
||||
bool currentAppsTheme = GetCurrentAppsTheme();
|
||||
|
||||
SYSTEMTIME st;
|
||||
GetLocalTime(&st);
|
||||
int nowMinutes = st.wHour * 60 + st.wMinute;
|
||||
|
||||
int lightBoundary = 0;
|
||||
int darkBoundary = 0;
|
||||
|
||||
if (settings.scheduleMode == ScheduleMode::SunsetToSunrise)
|
||||
{
|
||||
lightBoundary = (settings.lightTime + settings.sunrise_offset) % 1440;
|
||||
darkBoundary = (settings.darkTime + settings.sunset_offset) % 1440;
|
||||
}
|
||||
else
|
||||
{
|
||||
lightBoundary = settings.lightTime;
|
||||
darkBoundary = settings.darkTime;
|
||||
}
|
||||
|
||||
bool shouldBeLight = (lightBoundary < darkBoundary) ? (nowMinutes >= lightBoundary && nowMinutes < darkBoundary) : (nowMinutes >= lightBoundary || nowMinutes < darkBoundary);
|
||||
|
||||
Logger::debug(L"[LightSwitchService] shouldBeLight = {}", shouldBeLight);
|
||||
|
||||
bool systemMismatch = settings.changeSystem && (currentSystemTheme != shouldBeLight);
|
||||
bool appsMismatch = settings.changeApps && (currentAppsTheme != shouldBeLight);
|
||||
|
||||
if (systemMismatch || appsMismatch)
|
||||
{
|
||||
// Make sure this is not because we crossed a boundary
|
||||
bool crossedBoundary = false;
|
||||
if (prevMinutes != -1)
|
||||
{
|
||||
if (nowMinutes < prevMinutes)
|
||||
{
|
||||
// wrapped around midnight
|
||||
crossedBoundary = (prevMinutes <= lightBoundary || nowMinutes >= lightBoundary) ||
|
||||
(prevMinutes <= darkBoundary || nowMinutes >= darkBoundary);
|
||||
}
|
||||
else
|
||||
{
|
||||
crossedBoundary = (prevMinutes < lightBoundary && nowMinutes >= lightBoundary) ||
|
||||
(prevMinutes < darkBoundary && nowMinutes >= darkBoundary);
|
||||
}
|
||||
}
|
||||
|
||||
if (crossedBoundary)
|
||||
{
|
||||
Logger::info(L"[LightSwitchService] Missed boundary detected. Applying theme instead of triggering manual override.");
|
||||
LightSwitchSettings::instance().ApplyThemeIfNecessary();
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::info(L"[LightSwitchService] External {} theme change detected, enabling manual override.",
|
||||
systemMismatch && appsMismatch ? L"system/app" :
|
||||
systemMismatch ? L"system" :
|
||||
L"app");
|
||||
SetEvent(hManualOverride);
|
||||
skipRest = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::debug(L"[LightSwitchService] Skipping external-change detection (schedule off, recent reload, or just enabled).");
|
||||
}
|
||||
|
||||
HANDLE waits[4];
|
||||
DWORD count = 0;
|
||||
waits[count++] = g_ServiceStopEvent;
|
||||
if (hParent)
|
||||
waits[count++] = hParent;
|
||||
if (hManualOverride)
|
||||
manualOverrideActive = (WaitForSingleObject(hManualOverride, 0) == WAIT_OBJECT_0);
|
||||
waits[count++] = hManualOverride;
|
||||
waits[count++] = hSettingsChanged;
|
||||
|
||||
if (manualOverrideActive)
|
||||
{
|
||||
int lightBoundary = (settings.lightTime + settings.sunrise_offset) % 1440;
|
||||
int darkBoundary = (settings.darkTime + settings.sunset_offset) % 1440;
|
||||
|
||||
SYSTEMTIME st;
|
||||
GetLocalTime(&st);
|
||||
nowMinutes = st.wHour * 60 + st.wMinute;
|
||||
|
||||
bool crossedLight = false;
|
||||
bool crossedDark = false;
|
||||
|
||||
if (prevMinutes != -1)
|
||||
{
|
||||
// this means we are in a new day cycle
|
||||
if (nowMinutes < prevMinutes)
|
||||
{
|
||||
crossedLight = (prevMinutes <= lightBoundary || nowMinutes >= lightBoundary);
|
||||
crossedDark = (prevMinutes <= darkBoundary || nowMinutes >= darkBoundary);
|
||||
}
|
||||
else
|
||||
{
|
||||
crossedLight = (prevMinutes < lightBoundary && nowMinutes >= lightBoundary);
|
||||
crossedDark = (prevMinutes < darkBoundary && nowMinutes >= darkBoundary);
|
||||
}
|
||||
}
|
||||
|
||||
if (crossedLight || crossedDark)
|
||||
{
|
||||
ResetEvent(hManualOverride);
|
||||
Logger::info(L"[LightSwitchService] Manual override cleared after crossing schedule boundary.");
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::debug(L"[LightSwitchService] Skipping schedule due to manual override");
|
||||
skipRest = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply theme if nothing has made us skip
|
||||
if (!skipRest)
|
||||
{
|
||||
// Next two conditionals check for any updates necessary to the sun times.
|
||||
bool modeChangedToSunset = (prevMode != settings.scheduleMode &&
|
||||
settings.scheduleMode == ScheduleMode::SunsetToSunrise);
|
||||
bool coordsChanged = (prevLat != settings.latitude || prevLon != settings.longitude);
|
||||
|
||||
if ((modeChangedToSunset || coordsChanged) && settings.scheduleMode == ScheduleMode::SunsetToSunrise)
|
||||
{
|
||||
SYSTEMTIME st;
|
||||
GetLocalTime(&st);
|
||||
|
||||
Logger::info(L"[LightSwitchService] Mode or coordinates changed, recalculating sun times.");
|
||||
update_sun_times(settings);
|
||||
g_lastUpdatedDay = st.wDay;
|
||||
prevMode = settings.scheduleMode;
|
||||
prevLat = settings.latitude;
|
||||
prevLon = settings.longitude;
|
||||
}
|
||||
|
||||
SYSTEMTIME st;
|
||||
GetLocalTime(&st);
|
||||
int nowMinutes = st.wHour * 60 + st.wMinute;
|
||||
|
||||
if ((g_lastUpdatedDay != st.wDay) && (settings.scheduleMode == ScheduleMode::SunsetToSunrise))
|
||||
{
|
||||
update_sun_times(settings);
|
||||
g_lastUpdatedDay = st.wDay;
|
||||
prevMinutes = -1;
|
||||
Logger::info(L"[LightSwitchService] Recalculated sun times at new day boundary.");
|
||||
}
|
||||
|
||||
// settings after any necessary updates.
|
||||
LightSwitchSettings::instance().LoadSettings();
|
||||
const auto& currentSettings = LightSwitchSettings::instance().settings();
|
||||
|
||||
wchar_t msg[160];
|
||||
swprintf_s(msg,
|
||||
L"[LightSwitchService] now=%02d:%02d | light=%02d:%02d | dark=%02d:%02d | mode=%s",
|
||||
st.wHour,
|
||||
st.wMinute,
|
||||
currentSettings.lightTime / 60,
|
||||
currentSettings.lightTime % 60,
|
||||
currentSettings.darkTime / 60,
|
||||
currentSettings.darkTime % 60,
|
||||
ToString(currentSettings.scheduleMode).c_str());
|
||||
Logger::info(msg);
|
||||
|
||||
LightSwitchSettings::instance().ApplyThemeIfNecessary();
|
||||
}
|
||||
|
||||
// ─── Wait For Next Minute Tick Or Stop Event ────────────────────────────────
|
||||
// Wait for one of these to trigger or for a new minute tick
|
||||
SYSTEMTIME st;
|
||||
GetLocalTime(&st);
|
||||
int msToNextMinute = (60 - st.wSecond) * 1000 - st.wMilliseconds;
|
||||
if (msToNextMinute < 50)
|
||||
msToNextMinute = 50;
|
||||
|
||||
prevMinutes = nowMinutes;
|
||||
|
||||
DWORD wait = WaitForMultipleObjects(count, waits, FALSE, msToNextMinute);
|
||||
|
||||
if (wait == WAIT_TIMEOUT)
|
||||
{
|
||||
// regular minute tick
|
||||
GetLocalTime(&st);
|
||||
nowMinutes = st.wHour * 60 + st.wMinute;
|
||||
DetectAndHandleExternalThemeChange(stateManager);
|
||||
stateManager.OnTick(nowMinutes);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (wait == WAIT_OBJECT_0)
|
||||
{
|
||||
Logger::info(L"[LightSwitchService] Stop event triggered - exiting worker loop.");
|
||||
Logger::info(L"[LightSwitchService] Stop event triggered — exiting.");
|
||||
break;
|
||||
}
|
||||
|
||||
if (hParent && wait == WAIT_OBJECT_0 + 1)
|
||||
{
|
||||
Logger::info(L"[LightSwitchService] Parent process exited - stopping service.");
|
||||
Logger::info(L"[LightSwitchService] Parent process exited — stopping service.");
|
||||
break;
|
||||
}
|
||||
|
||||
if (hManualOverride && wait == WAIT_OBJECT_0 + (hParent ? 2 : 1))
|
||||
{
|
||||
Logger::info(L"[LightSwitchService] Manual override event detected.");
|
||||
stateManager.OnManualOverride();
|
||||
ResetEvent(hManualOverride);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (wait == WAIT_OBJECT_0 + (hParent ? (hManualOverride ? 3 : 2) : 2))
|
||||
{
|
||||
ResetEvent(hSettingsChanged);
|
||||
LightSwitchSettings::instance().LoadSettings();
|
||||
stateManager.OnSettingsChanged();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
cleanup:
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
// Cleanup
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
if (hManualOverride)
|
||||
CloseHandle(hManualOverride);
|
||||
if (hParent)
|
||||
CloseHandle(hParent);
|
||||
|
||||
Logger::info(L"[LightSwitchService] Worker thread exiting cleanly.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -75,6 +75,7 @@
|
||||
<ItemGroup>
|
||||
<ClCompile Include="LightSwitchService.cpp" />
|
||||
<ClCompile Include="LightSwitchSettings.cpp" />
|
||||
<ClCompile Include="LightSwitchStateManager.cpp" />
|
||||
<ClCompile Include="SettingsConstants.cpp" />
|
||||
<ClCompile Include="ThemeHelper.cpp" />
|
||||
<ClCompile Include="ThemeScheduler.cpp" />
|
||||
@@ -85,6 +86,8 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="LightSwitchSettings.h" />
|
||||
<ClInclude Include="LightSwitchStateManager.h" />
|
||||
<ClInclude Include="LightSwitchUtils.h" />
|
||||
<ClInclude Include="SettingsConstants.h" />
|
||||
<ClInclude Include="SettingsObserver.h" />
|
||||
<ClInclude Include="ThemeHelper.h" />
|
||||
|
||||
@@ -33,6 +33,9 @@
|
||||
<ClCompile Include="WinHookEventIDs.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="LightSwitchStateManager.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="ThemeScheduler.h">
|
||||
@@ -53,6 +56,12 @@
|
||||
<ClInclude Include="WinHookEventIDs.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="LightSwitchStateManager.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="LightSwitchUtils.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Natvis Include="$(MSBuildThisFileDirectory)..\..\natvis\wil.natvis" />
|
||||
|
||||
@@ -2,10 +2,8 @@
|
||||
#include <common/utils/json.h>
|
||||
#include <common/SettingsAPI/settings_helpers.h>
|
||||
#include "SettingsObserver.h"
|
||||
#include "ThemeHelper.h"
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <WinHookEventIDs.h>
|
||||
#include <logger.h>
|
||||
|
||||
using namespace std;
|
||||
@@ -69,7 +67,6 @@ void LightSwitchSettings::InitFileWatcher()
|
||||
try
|
||||
{
|
||||
LoadSettings();
|
||||
ApplyThemeIfNecessary();
|
||||
SetEvent(m_settingsChangedEvent);
|
||||
}
|
||||
catch (const std::exception& e)
|
||||
@@ -250,48 +247,3 @@ void LightSwitchSettings::LoadSettings()
|
||||
// Keeps defaults if load fails
|
||||
}
|
||||
}
|
||||
|
||||
void LightSwitchSettings::ApplyThemeIfNecessary()
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(m_settingsMutex);
|
||||
|
||||
SYSTEMTIME st;
|
||||
GetLocalTime(&st);
|
||||
int nowMinutes = st.wHour * 60 + st.wMinute;
|
||||
|
||||
bool shouldBeLight = false;
|
||||
if (m_settings.lightTime < m_settings.darkTime)
|
||||
shouldBeLight = (nowMinutes >= m_settings.lightTime && nowMinutes < m_settings.darkTime);
|
||||
else
|
||||
shouldBeLight = (nowMinutes >= m_settings.lightTime || nowMinutes < m_settings.darkTime);
|
||||
|
||||
bool isSystemCurrentlyLight = GetCurrentSystemTheme();
|
||||
bool isAppsCurrentlyLight = GetCurrentAppsTheme();
|
||||
|
||||
if (shouldBeLight)
|
||||
{
|
||||
if (m_settings.changeSystem && !isSystemCurrentlyLight)
|
||||
{
|
||||
SetSystemTheme(true);
|
||||
Logger::info(L"[LightSwitchService] Changing system theme to light mode.");
|
||||
}
|
||||
if (m_settings.changeApps && !isAppsCurrentlyLight)
|
||||
{
|
||||
SetAppsTheme(true);
|
||||
Logger::info(L"[LightSwitchService] Changing apps theme to light mode.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (m_settings.changeSystem && isSystemCurrentlyLight)
|
||||
{
|
||||
SetSystemTheme(false);
|
||||
Logger::info(L"[LightSwitchService] Changing system theme to dark mode.");
|
||||
}
|
||||
if (m_settings.changeApps && isAppsCurrentlyLight)
|
||||
{
|
||||
SetAppsTheme(false);
|
||||
Logger::info(L"[LightSwitchService] Changing apps theme to dark mode.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -81,7 +81,6 @@ public:
|
||||
void RemoveObserver(SettingsObserver& observer);
|
||||
|
||||
void LoadSettings();
|
||||
void ApplyThemeIfNecessary();
|
||||
|
||||
HANDLE GetSettingsChangedEvent() const;
|
||||
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
#include "pch.h"
|
||||
#include "LightSwitchStateManager.h"
|
||||
#include <logger.h>
|
||||
#include <LightSwitchUtils.h>
|
||||
#include "ThemeScheduler.h"
|
||||
#include <ThemeHelper.h>
|
||||
|
||||
void ApplyTheme(bool shouldBeLight);
|
||||
|
||||
// Constructor
|
||||
LightSwitchStateManager::LightSwitchStateManager()
|
||||
{
|
||||
Logger::info(L"[LightSwitchStateManager] Initialized");
|
||||
}
|
||||
|
||||
// Called when settings.json changes
|
||||
void LightSwitchStateManager::OnSettingsChanged()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_stateMutex);
|
||||
|
||||
// If manual override was active, clear it so new settings take effect
|
||||
if (_state.isManualOverride)
|
||||
{
|
||||
_state.isManualOverride = false;
|
||||
}
|
||||
|
||||
EvaluateAndApplyIfNeeded();
|
||||
}
|
||||
|
||||
// Called once per minute
|
||||
void LightSwitchStateManager::OnTick(int currentMinutes)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_stateMutex);
|
||||
EvaluateAndApplyIfNeeded();
|
||||
}
|
||||
|
||||
// Called when manual override is triggered
|
||||
void LightSwitchStateManager::OnManualOverride()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_stateMutex);
|
||||
Logger::info(L"[LightSwitchStateManager] Manual override triggered");
|
||||
_state.isManualOverride = !_state.isManualOverride;
|
||||
|
||||
// When entering manual override, sync internal theme state to match the current system
|
||||
if (_state.isManualOverride)
|
||||
{
|
||||
_state.isSystemLightActive = GetCurrentSystemTheme();
|
||||
|
||||
_state.isAppsLightActive = GetCurrentAppsTheme();
|
||||
|
||||
Logger::debug(L"[LightSwitchStateManager] Synced internal theme state to current system theme ({}) and apps theme ({}).",
|
||||
(_state.isSystemLightActive ? L"light" : L"dark"),
|
||||
(_state.isAppsLightActive ? L"light" : L"dark"));
|
||||
}
|
||||
|
||||
EvaluateAndApplyIfNeeded();
|
||||
}
|
||||
|
||||
// Helpers
|
||||
bool LightSwitchStateManager::CoordinatesAreValid(const std::wstring& lat, const std::wstring& lon)
|
||||
{
|
||||
try
|
||||
{
|
||||
double latVal = std::stod(lat);
|
||||
double lonVal = std::stod(lon);
|
||||
return !(latVal == 0 && lonVal == 0) && (latVal >= -90.0 && latVal <= 90.0) && (lonVal >= -180.0 && lonVal <= 180.0);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void LightSwitchStateManager::SyncInitialThemeState()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_stateMutex);
|
||||
_state.isSystemLightActive = GetCurrentSystemTheme();
|
||||
_state.isAppsLightActive = GetCurrentAppsTheme();
|
||||
Logger::debug(L"[LightSwitchStateManager] Synced initial state to current system theme ({})",
|
||||
_state.isSystemLightActive ? L"light" : L"dark");
|
||||
Logger::debug(L"[LightSwitchStateManager] Synced initial state to current apps theme ({})",
|
||||
_state.isAppsLightActive ? L"light" : L"dark");
|
||||
}
|
||||
|
||||
static std::pair<int, int> update_sun_times(auto& settings)
|
||||
{
|
||||
double latitude = std::stod(settings.latitude);
|
||||
double longitude = std::stod(settings.longitude);
|
||||
|
||||
SYSTEMTIME st;
|
||||
GetLocalTime(&st);
|
||||
|
||||
SunTimes newTimes = CalculateSunriseSunset(latitude, longitude, st.wYear, st.wMonth, st.wDay);
|
||||
|
||||
int newLightTime = newTimes.sunriseHour * 60 + newTimes.sunriseMinute;
|
||||
int newDarkTime = newTimes.sunsetHour * 60 + newTimes.sunsetMinute;
|
||||
|
||||
try
|
||||
{
|
||||
auto values = PowerToysSettings::PowerToyValues::load_from_settings_file(L"LightSwitch");
|
||||
values.add_property(L"lightTime", newLightTime);
|
||||
values.add_property(L"darkTime", newDarkTime);
|
||||
values.save_to_settings_file();
|
||||
|
||||
Logger::info(L"[LightSwitchService] Updated sun times and saved to config.");
|
||||
}
|
||||
catch (const std::exception& e)
|
||||
{
|
||||
std::string msg = e.what();
|
||||
std::wstring wmsg(msg.begin(), msg.end());
|
||||
Logger::error(L"[LightSwitchService] Exception during sun time update: {}", wmsg);
|
||||
}
|
||||
|
||||
return { newLightTime, newDarkTime };
|
||||
}
|
||||
|
||||
// Internal: decide what should happen now
|
||||
void LightSwitchStateManager::EvaluateAndApplyIfNeeded()
|
||||
{
|
||||
LightSwitchSettings::instance().LoadSettings();
|
||||
const auto& _currentSettings = LightSwitchSettings::settings();
|
||||
auto now = GetNowMinutes();
|
||||
|
||||
// Early exit: OFF mode just pauses activity
|
||||
if (_currentSettings.scheduleMode == ScheduleMode::Off)
|
||||
{
|
||||
_state.lastTickMinutes = now;
|
||||
return;
|
||||
}
|
||||
|
||||
bool coordsValid = CoordinatesAreValid(_currentSettings.latitude, _currentSettings.longitude);
|
||||
|
||||
// Handle Sun Mode recalculation
|
||||
if (_currentSettings.scheduleMode == ScheduleMode::SunsetToSunrise && coordsValid)
|
||||
{
|
||||
SYSTEMTIME st;
|
||||
GetLocalTime(&st);
|
||||
bool newDay = (_state.lastEvaluatedDay != st.wDay);
|
||||
bool modeChangedToSun = (_state.lastAppliedMode != ScheduleMode::SunsetToSunrise &&
|
||||
_currentSettings.scheduleMode == ScheduleMode::SunsetToSunrise);
|
||||
|
||||
if (newDay || modeChangedToSun)
|
||||
{
|
||||
auto [newLightTime, newDarkTime] = update_sun_times(_currentSettings);
|
||||
_state.lastEvaluatedDay = st.wDay;
|
||||
_state.effectiveLightMinutes = newLightTime + _currentSettings.sunrise_offset;
|
||||
_state.effectiveDarkMinutes = newDarkTime + _currentSettings.sunset_offset;
|
||||
}
|
||||
else
|
||||
{
|
||||
_state.effectiveLightMinutes = _currentSettings.lightTime + _currentSettings.sunrise_offset;
|
||||
_state.effectiveDarkMinutes = _currentSettings.darkTime + _currentSettings.sunset_offset;
|
||||
}
|
||||
}
|
||||
else if (_currentSettings.scheduleMode == ScheduleMode::FixedHours)
|
||||
{
|
||||
_state.effectiveLightMinutes = _currentSettings.lightTime;
|
||||
_state.effectiveDarkMinutes = _currentSettings.darkTime;
|
||||
}
|
||||
|
||||
// Handle manual override logic
|
||||
if (_state.isManualOverride)
|
||||
{
|
||||
bool crossedBoundary = false;
|
||||
if (_state.lastTickMinutes != -1)
|
||||
{
|
||||
int prev = _state.lastTickMinutes;
|
||||
|
||||
// Handle midnight wraparound safely
|
||||
if (now < prev)
|
||||
{
|
||||
crossedBoundary =
|
||||
(prev <= _state.effectiveLightMinutes || now >= _state.effectiveLightMinutes) ||
|
||||
(prev <= _state.effectiveDarkMinutes || now >= _state.effectiveDarkMinutes);
|
||||
}
|
||||
else
|
||||
{
|
||||
crossedBoundary =
|
||||
(prev < _state.effectiveLightMinutes && now >= _state.effectiveLightMinutes) ||
|
||||
(prev < _state.effectiveDarkMinutes && now >= _state.effectiveDarkMinutes);
|
||||
}
|
||||
}
|
||||
|
||||
if (crossedBoundary)
|
||||
{
|
||||
_state.isManualOverride = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
_state.lastTickMinutes = now;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_state.lastAppliedMode = _currentSettings.scheduleMode;
|
||||
|
||||
bool shouldBeLight = ShouldBeLight(now, _state.effectiveLightMinutes, _state.effectiveDarkMinutes);
|
||||
|
||||
bool appsNeedsToChange = _currentSettings.changeApps && (_state.isAppsLightActive != shouldBeLight);
|
||||
bool systemNeedsToChange = _currentSettings.changeSystem && (_state.isSystemLightActive != shouldBeLight);
|
||||
|
||||
/* Logger::debug(
|
||||
L"[LightSwitchStateManager] now = {:02d}:{:02d}, light boundary = {:02d}:{:02d} ({}), dark boundary = {:02d}:{:02d} ({})",
|
||||
now / 60,
|
||||
now % 60,
|
||||
_state.effectiveLightMinutes / 60,
|
||||
_state.effectiveLightMinutes % 60,
|
||||
_state.effectiveLightMinutes,
|
||||
_state.effectiveDarkMinutes / 60,
|
||||
_state.effectiveDarkMinutes % 60,
|
||||
_state.effectiveDarkMinutes); */
|
||||
|
||||
/* Logger::debug("should be light = {}, apps needs change = {}, system needs change = {}",
|
||||
shouldBeLight ? "true" : "false",
|
||||
appsNeedsToChange ? "true" : "false",
|
||||
systemNeedsToChange ? "true" : "false"); */
|
||||
|
||||
// Only apply theme if there's a change or no override active
|
||||
if (!_state.isManualOverride && (appsNeedsToChange || systemNeedsToChange))
|
||||
{
|
||||
Logger::info(L"[LightSwitchStateManager] Applying {} theme", shouldBeLight ? L"light" : L"dark");
|
||||
ApplyTheme(shouldBeLight);
|
||||
|
||||
_state.isSystemLightActive = GetCurrentSystemTheme();
|
||||
_state.isAppsLightActive = GetCurrentAppsTheme();
|
||||
}
|
||||
|
||||
_state.lastTickMinutes = now;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
#pragma once
|
||||
#include "LightSwitchSettings.h"
|
||||
#include <optional>
|
||||
|
||||
// Represents runtime-only information (not saved in settings.json)
|
||||
struct LightSwitchState
|
||||
{
|
||||
ScheduleMode lastAppliedMode = ScheduleMode::Off;
|
||||
bool isManualOverride = false;
|
||||
bool isSystemLightActive = false;
|
||||
bool isAppsLightActive = false;
|
||||
int lastEvaluatedDay = -1;
|
||||
int lastTickMinutes = -1;
|
||||
|
||||
// Derived, runtime-resolved times
|
||||
int effectiveLightMinutes = 0; // the boundary we actually act on
|
||||
int effectiveDarkMinutes = 0; // includes offsets if needed
|
||||
};
|
||||
|
||||
// The controller that reacts to settings changes, time ticks, and manual overrides.
|
||||
class LightSwitchStateManager
|
||||
{
|
||||
public:
|
||||
LightSwitchStateManager();
|
||||
|
||||
// Called when settings.json changes or stabilizes.
|
||||
void OnSettingsChanged();
|
||||
|
||||
// Called every minute (from service worker tick).
|
||||
void OnTick(int currentMinutes);
|
||||
|
||||
// Called when manual override is toggled (via shortcut or system change).
|
||||
void OnManualOverride();
|
||||
|
||||
// Initial sync at startup to align internal state with system theme
|
||||
void SyncInitialThemeState();
|
||||
|
||||
// Accessor for current state (optional, for debugging or telemetry)
|
||||
const LightSwitchState& GetState() const { return _state; }
|
||||
|
||||
private:
|
||||
LightSwitchState _state;
|
||||
std::mutex _stateMutex;
|
||||
|
||||
void EvaluateAndApplyIfNeeded();
|
||||
bool CoordinatesAreValid(const std::wstring& lat, const std::wstring& lon);
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
#include <windows.h>
|
||||
|
||||
constexpr bool ShouldBeLight(int nowMinutes, int lightTime, int darkTime)
|
||||
{
|
||||
// Normalize values into [0, 1439]
|
||||
int normalizedLightTime = (lightTime % 1440 + 1440) % 1440;
|
||||
int normalizedDarkTime = (darkTime % 1440 + 1440) % 1440;
|
||||
int normalizedNowMinutes = (nowMinutes % 1440 + 1440) % 1440;
|
||||
|
||||
// Case 1: Normal range, e.g. light mode comes before dark mode in the same day
|
||||
if (normalizedLightTime < normalizedDarkTime)
|
||||
return normalizedNowMinutes >= normalizedLightTime && normalizedNowMinutes < normalizedDarkTime;
|
||||
|
||||
// Case 2: Wrap-around range, e.g. light mode starts in the evening and dark mode starts in the morning
|
||||
return normalizedNowMinutes >= normalizedLightTime || normalizedNowMinutes < normalizedDarkTime;
|
||||
}
|
||||
|
||||
inline int GetNowMinutes()
|
||||
{
|
||||
SYSTEMTIME st;
|
||||
GetLocalTime(&st);
|
||||
return st.wHour * 60 + st.wMinute;
|
||||
}
|
||||
@@ -152,16 +152,16 @@ namespace LightSwitch.UITests
|
||||
|
||||
var neededTabs = 6;
|
||||
|
||||
if (modeCombobox.Text != "Manual")
|
||||
if (modeCombobox.Text != "Fixed hours")
|
||||
{
|
||||
modeCombobox.Click();
|
||||
var manualListItem = testBase.Session.Find<Element>(By.AccessibilityId("ManualCBItem_LightSwitch"), 5000);
|
||||
Assert.IsNotNull(manualListItem, "Manual combobox item not found.");
|
||||
Assert.IsNotNull(manualListItem, "Fixed Hours combobox item not found.");
|
||||
manualListItem.Click();
|
||||
neededTabs = 1;
|
||||
}
|
||||
|
||||
Assert.AreEqual("Manual", modeCombobox.Text, "Mode combobox should be set to Manual.");
|
||||
Assert.AreEqual("Fixed hours", modeCombobox.Text, "Mode combobox should be set to Fixed hours.");
|
||||
|
||||
var timeline = testBase.Session.Find<Element>(By.AccessibilityId("Timeline_LightSwitch"), 5000);
|
||||
Assert.IsNotNull(timeline, "Timeline not found.");
|
||||
@@ -198,7 +198,7 @@ namespace LightSwitch.UITests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Perform a update geolocation test operation
|
||||
/// Perform a update manual location test operation
|
||||
/// </summary>
|
||||
public static void PerformUserSelectedLocationTest(UITestBase testBase)
|
||||
{
|
||||
@@ -216,19 +216,22 @@ namespace LightSwitch.UITests
|
||||
|
||||
Assert.AreEqual("Sunset to sunrise", modeCombobox.Text, "Mode combobox should be set to Sunset to sunrise.");
|
||||
|
||||
// Click the select location button
|
||||
var setLocationButton = testBase.Session.Find<Element>(By.AccessibilityId("SetLocationButton_LightSwitch"), 5000);
|
||||
Assert.IsNotNull(setLocationButton, "Set location button not found.");
|
||||
setLocationButton.Click();
|
||||
setLocationButton.Click(msPostAction: 1000);
|
||||
|
||||
var autoSuggestTextbox = testBase.Session.Find<Element>(By.AccessibilityId("CitySearchBox_LightSwitch"), 5000);
|
||||
Assert.IsNotNull(autoSuggestTextbox, "City search box not found.");
|
||||
autoSuggestTextbox.Click();
|
||||
autoSuggestTextbox.SendKeys("Seattle");
|
||||
autoSuggestTextbox.SendKeys(OpenQA.Selenium.Keys.Down);
|
||||
autoSuggestTextbox.SendKeys(OpenQA.Selenium.Keys.Enter);
|
||||
var latitudeBox = testBase.Session.Find<Element>(By.AccessibilityId("LatitudeBox_LightSwitch"), 5000);
|
||||
Assert.IsNotNull(latitudeBox, "Latitude text box not found.");
|
||||
latitudeBox.Click();
|
||||
|
||||
var latLong = testBase.Session.Find<Element>(By.AccessibilityId("LocationResultText_LightSwitch"), 5000);
|
||||
Assert.IsFalse(string.IsNullOrWhiteSpace(latLong.Text));
|
||||
testBase.Session.SendKeys(Key.Up);
|
||||
|
||||
var longitudeBox = testBase.Session.Find<Element>(By.AccessibilityId("LongitudeBox_LightSwitch"), 5000);
|
||||
Assert.IsNotNull(longitudeBox, "Longitude text box not found.");
|
||||
longitudeBox.Click();
|
||||
|
||||
testBase.Session.SendKeys(Key.Down);
|
||||
|
||||
var sunrise = testBase.Session.Find<Element>(By.AccessibilityId("SunriseText_LightSwitch"), 5000);
|
||||
Assert.IsFalse(string.IsNullOrWhiteSpace(sunrise.Text));
|
||||
@@ -256,13 +259,14 @@ namespace LightSwitch.UITests
|
||||
|
||||
Assert.AreEqual("Sunset to sunrise", modeCombobox.Text, "Mode combobox should be set to Sunset to sunrise.");
|
||||
|
||||
// Click the select city button
|
||||
// Click the select location button
|
||||
var setLocationButton = testBase.Session.Find<Element>(By.AccessibilityId("SetLocationButton_LightSwitch"), 5000);
|
||||
Assert.IsNotNull(setLocationButton, "Set location button not found.");
|
||||
setLocationButton.Click(msPostAction: 8000);
|
||||
setLocationButton.Click(msPostAction: 1000);
|
||||
|
||||
var latLong = testBase.Session.Find<Element>(By.AccessibilityId("LocationResultText_LightSwitch"), 5000);
|
||||
Assert.IsFalse(string.IsNullOrWhiteSpace(latLong.Text));
|
||||
var syncLocationButton = testBase.Session.Find<Element>(By.AccessibilityId("SyncLocationButton_LightSwitch"), 5000);
|
||||
Assert.IsNotNull(syncLocationButton, "Sync location button not found.");
|
||||
syncLocationButton.Click(msPostAction: 8000);
|
||||
|
||||
var sunrise = testBase.Session.Find<Element>(By.AccessibilityId("SunriseText_LightSwitch"), 5000);
|
||||
Assert.IsFalse(string.IsNullOrWhiteSpace(sunrise.Text));
|
||||
@@ -363,6 +367,7 @@ namespace LightSwitch.UITests
|
||||
var systemBeforeValue = GetSystemTheme();
|
||||
var appsBeforeValue = GetAppsTheme();
|
||||
|
||||
Task.Delay(1000).Wait();
|
||||
testBase.Session.SendKeys(activationKeys);
|
||||
Task.Delay(5000).Wait();
|
||||
|
||||
@@ -389,6 +394,7 @@ namespace LightSwitch.UITests
|
||||
var noneSystemBeforeValue = GetSystemTheme();
|
||||
var noneAppsBeforeValue = GetAppsTheme();
|
||||
|
||||
Task.Delay(1000).Wait();
|
||||
testBase.Session.SendKeys(activationKeys);
|
||||
Task.Delay(5000).Wait();
|
||||
|
||||
|
||||
@@ -196,10 +196,10 @@ public:
|
||||
m_enabled = true;
|
||||
Trace::EnableCursorWrap(true);
|
||||
|
||||
if (m_autoActivate)
|
||||
{
|
||||
StartMouseHook();
|
||||
}
|
||||
// Always start the mouse hook when the module is enabled
|
||||
// This ensures cursor wrapping is active immediately after enabling
|
||||
StartMouseHook();
|
||||
Logger::info("CursorWrap enabled - mouse hook started");
|
||||
}
|
||||
|
||||
// Disable the powertoy
|
||||
@@ -208,6 +208,7 @@ public:
|
||||
m_enabled = false;
|
||||
Trace::EnableCursorWrap(false);
|
||||
StopMouseHook();
|
||||
Logger::info("CursorWrap disabled - mouse hook stopped");
|
||||
}
|
||||
|
||||
// Returns if the powertoys is enabled
|
||||
|
||||
@@ -64,6 +64,7 @@
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<PreprocessorDefinitions>_DEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
|
||||
<LanguageStandard>stdcpplatest</LanguageStandard>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
@@ -81,6 +82,7 @@
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<PreprocessorDefinitions>NDEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
|
||||
<LanguageStandard>stdcpplatest</LanguageStandard>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<PreprocessorDefinitions>_DEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
|
||||
<LanguageStandard>stdcpplatest</LanguageStandard>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
@@ -65,6 +66,7 @@
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<PreprocessorDefinitions>NDEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
|
||||
<LanguageStandard>stdcpplatest</LanguageStandard>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<PreprocessorDefinitions>_DEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
|
||||
<LanguageStandard>stdcpplatest</LanguageStandard>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
@@ -65,6 +66,7 @@
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<PreprocessorDefinitions>NDEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
|
||||
<LanguageStandard>stdcpplatest</LanguageStandard>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<PreprocessorDefinitions>_DEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
|
||||
<LanguageStandard>stdcpplatest</LanguageStandard>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
@@ -66,6 +67,7 @@
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<PreprocessorDefinitions>NDEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
|
||||
<LanguageStandard>stdcpplatest</LanguageStandard>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
<PreprocessorDefinitions>_DEBUG;NEWPLUSSHELLEXTENSIONWIN10_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<PrecompiledHeader>Use</PrecompiledHeader>
|
||||
<AdditionalIncludeDirectories>..\..\..\common\Telemetry;..\..\;..\..\..\;%(AdditionalIncludeDirectories);..\NewShellExtensionContextMenu;$(MSBuildThisFileDirectory)Generated Files</AdditionalIncludeDirectories>
|
||||
<AdditionalIncludeDirectories>..\..\..\common\Telemetry;..\..\;..\..\..\;%(AdditionalIncludeDirectories);..\NewShellExtensionContextMenu;$(MSBuildThisFileDirectory)</AdditionalIncludeDirectories>
|
||||
<CompileAsWinRT>false</CompileAsWinRT>
|
||||
<LanguageStandard>stdcpplatest</LanguageStandard>
|
||||
</ClCompile>
|
||||
@@ -67,7 +67,7 @@
|
||||
<PreprocessorDefinitions>NDEBUG;NEWPLUSSHELLEXTENSIONWIN10_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<PrecompiledHeader>Use</PrecompiledHeader>
|
||||
<AdditionalIncludeDirectories>..\..\..\common\Telemetry;..\..\;..\..\..\;%(AdditionalIncludeDirectories);..\NewShellExtensionContextMenu;$(MSBuildThisFileDirectory)Generated Files</AdditionalIncludeDirectories>
|
||||
<AdditionalIncludeDirectories>..\..\..\common\Telemetry;..\..\;..\..\..\;%(AdditionalIncludeDirectories);..\NewShellExtensionContextMenu;$(MSBuildThisFileDirectory)</AdditionalIncludeDirectories>
|
||||
<CompileAsWinRT>false</CompileAsWinRT>
|
||||
<LanguageStandard>stdcpplatest</LanguageStandard>
|
||||
</ClCompile>
|
||||
|
||||
@@ -67,6 +67,8 @@
|
||||
<EnableUAC>false</EnableUAC>
|
||||
<ModuleDefinitionFile>dll.def</ModuleDefinitionFile>
|
||||
<AdditionalDependencies>runtimeobject.lib;$(CoreLibraryDependencies)</AdditionalDependencies>
|
||||
<IgnoreSpecificDefaultLibraries>
|
||||
</IgnoreSpecificDefaultLibraries>
|
||||
</Link>
|
||||
<PreBuildEvent>
|
||||
<Command>del $(OutDir)\NewPlusPackage.msix /q
|
||||
@@ -98,6 +100,8 @@ MakeAppx.exe pack /d . /p $(OutDir)NewPlusPackage.msix /nv</Command>
|
||||
<EnableUAC>false</EnableUAC>
|
||||
<ModuleDefinitionFile>dll.def</ModuleDefinitionFile>
|
||||
<AdditionalDependencies>runtimeobject.lib;$(CoreLibraryDependencies)</AdditionalDependencies>
|
||||
<IgnoreSpecificDefaultLibraries>
|
||||
</IgnoreSpecificDefaultLibraries>
|
||||
</Link>
|
||||
<PreBuildEvent>
|
||||
<Command>del $(OutDir)\NewPlusPackage.msix /q
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
<PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<Optimization>Disabled</Optimization>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
@@ -39,6 +40,7 @@
|
||||
<PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<Optimization>MaxSpeed</Optimization>
|
||||
<SDLCheck>false</SDLCheck>
|
||||
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
</ClCompile>
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
<PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<Optimization>Disabled</Optimization>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
@@ -39,6 +40,7 @@
|
||||
<PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<Optimization>MaxSpeed</Optimization>
|
||||
<SDLCheck>false</SDLCheck>
|
||||
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
</ClCompile>
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
<PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<Optimization>Disabled</Optimization>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
@@ -39,6 +40,7 @@
|
||||
<PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<Optimization>MaxSpeed</Optimization>
|
||||
<SDLCheck>false</SDLCheck>
|
||||
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
</ClCompile>
|
||||
|
||||
548
src/modules/ZoomIt/ZoomIt/GifRecordingSession.cpp
Normal file
@@ -0,0 +1,548 @@
|
||||
//==============================================================================
|
||||
//
|
||||
// Zoomit
|
||||
// Sysinternals - www.sysinternals.com
|
||||
//
|
||||
// GIF recording support using Windows Imaging Component (WIC)
|
||||
//
|
||||
//==============================================================================
|
||||
#include "pch.h"
|
||||
#include "GifRecordingSession.h"
|
||||
#include "CaptureFrameWait.h"
|
||||
#include <shcore.h>
|
||||
|
||||
extern DWORD g_RecordScaling;
|
||||
|
||||
namespace winrt
|
||||
{
|
||||
using namespace Windows::Foundation;
|
||||
using namespace Windows::Graphics;
|
||||
using namespace Windows::Graphics::Capture;
|
||||
using namespace Windows::Graphics::DirectX;
|
||||
using namespace Windows::Graphics::DirectX::Direct3D11;
|
||||
using namespace Windows::Storage;
|
||||
using namespace Windows::UI::Composition;
|
||||
}
|
||||
|
||||
namespace util
|
||||
{
|
||||
using namespace robmikh::common::uwp;
|
||||
}
|
||||
|
||||
const float CLEAR_COLOR[] = { 0.0f, 0.0f, 0.0f, 1.0f };
|
||||
|
||||
int32_t EnsureEvenGif(int32_t value)
|
||||
{
|
||||
if (value % 2 == 0)
|
||||
{
|
||||
return value;
|
||||
}
|
||||
else
|
||||
{
|
||||
return value + 1;
|
||||
}
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
//
|
||||
// GifRecordingSession::GifRecordingSession
|
||||
//
|
||||
//----------------------------------------------------------------------------
|
||||
GifRecordingSession::GifRecordingSession(
|
||||
winrt::IDirect3DDevice const& device,
|
||||
winrt::GraphicsCaptureItem const& item,
|
||||
RECT const cropRect,
|
||||
uint32_t frameRate,
|
||||
winrt::Streams::IRandomAccessStream const& stream)
|
||||
{
|
||||
m_device = device;
|
||||
m_d3dDevice = GetDXGIInterfaceFromObject<ID3D11Device>(m_device);
|
||||
m_d3dDevice->GetImmediateContext(m_d3dContext.put());
|
||||
m_item = item;
|
||||
m_frameRate = frameRate;
|
||||
m_stream = stream;
|
||||
|
||||
auto itemSize = item.Size();
|
||||
auto inputWidth = EnsureEvenGif(itemSize.Width);
|
||||
auto inputHeight = EnsureEvenGif(itemSize.Height);
|
||||
m_frameWait = std::make_shared<CaptureFrameWait>(m_device, m_item, winrt::SizeInt32{ inputWidth, inputHeight });
|
||||
auto weakPointer{ std::weak_ptr{ m_frameWait } };
|
||||
m_itemClosed = item.Closed(winrt::auto_revoke, [weakPointer](auto&, auto&)
|
||||
{
|
||||
auto sharedPointer{ weakPointer.lock() };
|
||||
if (sharedPointer)
|
||||
{
|
||||
sharedPointer->StopCapture();
|
||||
}
|
||||
});
|
||||
|
||||
// Get crop dimension
|
||||
if ((cropRect.right - cropRect.left) != 0)
|
||||
{
|
||||
m_rcCrop = cropRect;
|
||||
m_frameWait->ShowCaptureBorder(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_rcCrop.left = 0;
|
||||
m_rcCrop.top = 0;
|
||||
m_rcCrop.right = inputWidth;
|
||||
m_rcCrop.bottom = inputHeight;
|
||||
}
|
||||
|
||||
// Apply scaling
|
||||
constexpr int c_minimumSize = 34;
|
||||
auto scaledWidth = MulDiv(m_rcCrop.right - m_rcCrop.left, g_RecordScaling, 100);
|
||||
auto scaledHeight = MulDiv(m_rcCrop.bottom - m_rcCrop.top, g_RecordScaling, 100);
|
||||
m_width = scaledWidth;
|
||||
m_height = scaledHeight;
|
||||
if (m_width < c_minimumSize)
|
||||
{
|
||||
m_width = c_minimumSize;
|
||||
m_height = MulDiv(m_height, m_width, scaledWidth);
|
||||
}
|
||||
if (m_height < c_minimumSize)
|
||||
{
|
||||
m_height = c_minimumSize;
|
||||
m_width = MulDiv(m_width, m_height, scaledHeight);
|
||||
}
|
||||
if (m_width > inputWidth)
|
||||
{
|
||||
m_width = inputWidth;
|
||||
m_height = c_minimumSize, MulDiv(m_height, scaledWidth, m_width);
|
||||
}
|
||||
if (m_height > inputHeight)
|
||||
{
|
||||
m_height = inputHeight;
|
||||
m_width = c_minimumSize, MulDiv(m_width, scaledHeight, m_height);
|
||||
}
|
||||
m_width = EnsureEvenGif(m_width);
|
||||
m_height = EnsureEvenGif(m_height);
|
||||
|
||||
m_frameDelay = (frameRate > 0) ? (100 / frameRate) : 15;
|
||||
|
||||
// Initialize WIC
|
||||
winrt::check_hresult(CoCreateInstance(
|
||||
CLSID_WICImagingFactory,
|
||||
nullptr,
|
||||
CLSCTX_INPROC_SERVER,
|
||||
IID_PPV_ARGS(m_wicFactory.put())));
|
||||
|
||||
// Create WIC stream from IRandomAccessStream
|
||||
winrt::check_hresult(m_wicFactory->CreateStream(m_wicStream.put()));
|
||||
|
||||
// Get the IStream from the IRandomAccessStream
|
||||
winrt::com_ptr<IStream> streamInterop;
|
||||
winrt::check_hresult(CreateStreamOverRandomAccessStream(
|
||||
winrt::get_unknown(stream),
|
||||
IID_PPV_ARGS(streamInterop.put())));
|
||||
winrt::check_hresult(m_wicStream->InitializeFromIStream(streamInterop.get()));
|
||||
|
||||
// Create GIF encoder
|
||||
winrt::check_hresult(m_wicFactory->CreateEncoder(
|
||||
GUID_ContainerFormatGif,
|
||||
nullptr,
|
||||
m_gifEncoder.put()));
|
||||
|
||||
winrt::check_hresult(m_gifEncoder->Initialize(m_wicStream.get(), WICBitmapEncoderNoCache));
|
||||
|
||||
// Set global GIF metadata for looping (NETSCAPE2.0 application extension)
|
||||
try
|
||||
{
|
||||
winrt::com_ptr<IWICMetadataQueryWriter> encoderMetadataWriter;
|
||||
if (SUCCEEDED(m_gifEncoder->GetMetadataQueryWriter(encoderMetadataWriter.put())) && encoderMetadataWriter)
|
||||
{
|
||||
OutputDebugStringW(L"Setting NETSCAPE2.0 looping extension on encoder...\n");
|
||||
|
||||
// Set application extension
|
||||
PROPVARIANT propValue;
|
||||
PropVariantInit(&propValue);
|
||||
propValue.vt = VT_UI1 | VT_VECTOR;
|
||||
propValue.caub.cElems = 11;
|
||||
propValue.caub.pElems = static_cast<UCHAR*>(CoTaskMemAlloc(11));
|
||||
if (propValue.caub.pElems != nullptr)
|
||||
{
|
||||
memcpy(propValue.caub.pElems, "NETSCAPE2.0", 11);
|
||||
HRESULT hr = encoderMetadataWriter->SetMetadataByName(L"/appext/application", &propValue);
|
||||
if (SUCCEEDED(hr))
|
||||
{
|
||||
OutputDebugStringW(L"Encoder application extension set successfully\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
OutputDebugStringW(L"Failed to set encoder application extension\n");
|
||||
}
|
||||
PropVariantClear(&propValue);
|
||||
|
||||
// Set loop count (0 = infinite)
|
||||
PropVariantInit(&propValue);
|
||||
propValue.vt = VT_UI1 | VT_VECTOR;
|
||||
propValue.caub.cElems = 5;
|
||||
propValue.caub.pElems = static_cast<UCHAR*>(CoTaskMemAlloc(5));
|
||||
if (propValue.caub.pElems != nullptr)
|
||||
{
|
||||
propValue.caub.pElems[0] = 3;
|
||||
propValue.caub.pElems[1] = 1;
|
||||
propValue.caub.pElems[2] = 0;
|
||||
propValue.caub.pElems[3] = 0;
|
||||
propValue.caub.pElems[4] = 0;
|
||||
hr = encoderMetadataWriter->SetMetadataByName(L"/appext/data", &propValue);
|
||||
if (SUCCEEDED(hr))
|
||||
{
|
||||
OutputDebugStringW(L"Encoder loop count set successfully\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
OutputDebugStringW(L"Failed to set encoder loop count\n");
|
||||
}
|
||||
PropVariantClear(&propValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
OutputDebugStringW(L"Failed to get encoder metadata writer\n");
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
OutputDebugStringW(L"Warning: Failed to set GIF encoder looping metadata\n");
|
||||
}
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
//
|
||||
// GifRecordingSession::~GifRecordingSession
|
||||
//
|
||||
//----------------------------------------------------------------------------
|
||||
GifRecordingSession::~GifRecordingSession()
|
||||
{
|
||||
Close();
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
//
|
||||
// GifRecordingSession::Create
|
||||
//
|
||||
//----------------------------------------------------------------------------
|
||||
std::shared_ptr<GifRecordingSession> GifRecordingSession::Create(
|
||||
winrt::IDirect3DDevice const& device,
|
||||
winrt::GraphicsCaptureItem const& item,
|
||||
RECT const& crop,
|
||||
uint32_t frameRate,
|
||||
winrt::Streams::IRandomAccessStream const& stream)
|
||||
{
|
||||
return std::shared_ptr<GifRecordingSession>(new GifRecordingSession(device, item, crop, frameRate, stream));
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
//
|
||||
// GifRecordingSession::EncodeFrame
|
||||
//
|
||||
//----------------------------------------------------------------------------
|
||||
HRESULT GifRecordingSession::EncodeFrame(ID3D11Texture2D* frameTexture)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Create a staging texture for CPU access
|
||||
D3D11_TEXTURE2D_DESC frameDesc;
|
||||
frameTexture->GetDesc(&frameDesc);
|
||||
|
||||
// GIF encoding with palette generation is VERY slow at high resolutions (4K takes 1 second per frame!)
|
||||
|
||||
UINT targetWidth = frameDesc.Width;
|
||||
UINT targetHeight = frameDesc.Height;
|
||||
|
||||
if (frameDesc.Width > static_cast<uint32_t>(m_width) || frameDesc.Height > static_cast<uint32_t>(m_height))
|
||||
{
|
||||
float scaleX = static_cast<float>(m_width) / frameDesc.Width;
|
||||
float scaleY = static_cast<float>(m_height) / frameDesc.Height;
|
||||
float scale = min(scaleX, scaleY);
|
||||
|
||||
targetWidth = static_cast<UINT>(frameDesc.Width * scale);
|
||||
targetHeight = static_cast<UINT>(frameDesc.Height * scale);
|
||||
|
||||
// Ensure even dimensions for GIF
|
||||
targetWidth = (targetWidth / 2) * 2;
|
||||
targetHeight = (targetHeight / 2) * 2;
|
||||
}
|
||||
|
||||
D3D11_TEXTURE2D_DESC stagingDesc = frameDesc;
|
||||
stagingDesc.Usage = D3D11_USAGE_STAGING;
|
||||
stagingDesc.BindFlags = 0;
|
||||
stagingDesc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
|
||||
stagingDesc.MiscFlags = 0;
|
||||
|
||||
winrt::com_ptr<ID3D11Texture2D> stagingTexture;
|
||||
winrt::check_hresult(m_d3dDevice->CreateTexture2D(&stagingDesc, nullptr, stagingTexture.put()));
|
||||
|
||||
// Copy the frame to staging texture
|
||||
m_d3dContext->CopyResource(stagingTexture.get(), frameTexture);
|
||||
|
||||
// Map the staging texture
|
||||
D3D11_MAPPED_SUBRESOURCE mappedResource;
|
||||
winrt::check_hresult(m_d3dContext->Map(stagingTexture.get(), 0, D3D11_MAP_READ, 0, &mappedResource));
|
||||
|
||||
// Create a new frame in the GIF
|
||||
winrt::com_ptr<IWICBitmapFrameEncode> frameEncode;
|
||||
winrt::com_ptr<IPropertyBag2> propertyBag;
|
||||
winrt::check_hresult(m_gifEncoder->CreateNewFrame(frameEncode.put(), propertyBag.put()));
|
||||
|
||||
// Initialize the frame encoder with property bag
|
||||
winrt::check_hresult(frameEncode->Initialize(propertyBag.get()));
|
||||
|
||||
// CRITICAL: For GIF, we MUST set size and pixel format BEFORE WriteSource
|
||||
// Use target dimensions (may be downsampled)
|
||||
winrt::check_hresult(frameEncode->SetSize(targetWidth, targetHeight));
|
||||
|
||||
// Set the pixel format to 8-bit indexed (required for GIF)
|
||||
WICPixelFormatGUID pixelFormat = GUID_WICPixelFormat8bppIndexed;
|
||||
winrt::check_hresult(frameEncode->SetPixelFormat(&pixelFormat));
|
||||
|
||||
// Create a WIC bitmap from the BGRA texture data
|
||||
winrt::com_ptr<IWICBitmap> sourceBitmap;
|
||||
winrt::check_hresult(m_wicFactory->CreateBitmapFromMemory(
|
||||
frameDesc.Width,
|
||||
frameDesc.Height,
|
||||
GUID_WICPixelFormat32bppBGRA,
|
||||
mappedResource.RowPitch,
|
||||
frameDesc.Height * mappedResource.RowPitch,
|
||||
static_cast<BYTE*>(mappedResource.pData),
|
||||
sourceBitmap.put()));
|
||||
|
||||
// If we need downsampling, use WIC scaler
|
||||
winrt::com_ptr<IWICBitmapSource> finalSource = sourceBitmap;
|
||||
if (targetWidth != frameDesc.Width || targetHeight != frameDesc.Height)
|
||||
{
|
||||
winrt::com_ptr<IWICBitmapScaler> scaler;
|
||||
winrt::check_hresult(m_wicFactory->CreateBitmapScaler(scaler.put()));
|
||||
winrt::check_hresult(scaler->Initialize(
|
||||
sourceBitmap.get(),
|
||||
targetWidth,
|
||||
targetHeight,
|
||||
WICBitmapInterpolationModeHighQualityCubic));
|
||||
finalSource = scaler;
|
||||
|
||||
OutputDebugStringW((L"Downsampled from " + std::to_wstring(frameDesc.Width) + L"x" + std::to_wstring(frameDesc.Height) +
|
||||
L" to " + std::to_wstring(targetWidth) + L"x" + std::to_wstring(targetHeight) + L"\n").c_str());
|
||||
}
|
||||
|
||||
// Use WriteSource - WIC will handle the BGRA to 8bpp indexed conversion
|
||||
winrt::check_hresult(frameEncode->WriteSource(finalSource.get(), nullptr));
|
||||
|
||||
try
|
||||
{
|
||||
winrt::com_ptr<IWICMetadataQueryWriter> frameMetadataWriter;
|
||||
if (SUCCEEDED(frameEncode->GetMetadataQueryWriter(frameMetadataWriter.put())) && frameMetadataWriter)
|
||||
{
|
||||
// Set the frame delay in the metadata (in hundredths of a second)
|
||||
PROPVARIANT propValue;
|
||||
PropVariantInit(&propValue);
|
||||
propValue.vt = VT_UI2;
|
||||
propValue.uiVal = static_cast<USHORT>(m_frameDelay);
|
||||
frameMetadataWriter->SetMetadataByName(L"/grctlext/Delay", &propValue);
|
||||
PropVariantClear(&propValue);
|
||||
|
||||
// Set disposal method (2 = restore to background, needed for animation)
|
||||
PropVariantInit(&propValue);
|
||||
propValue.vt = VT_UI1;
|
||||
propValue.bVal = 2; // Disposal method: restore to background color
|
||||
frameMetadataWriter->SetMetadataByName(L"/grctlext/Disposal", &propValue);
|
||||
PropVariantClear(&propValue);
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
// Metadata setting failed, continue anyway
|
||||
OutputDebugStringW(L"Warning: Failed to set GIF frame metadata\n");
|
||||
}
|
||||
|
||||
// Commit the frame
|
||||
OutputDebugStringW(L"About to commit frame to encoder...\n");
|
||||
winrt::check_hresult(frameEncode->Commit());
|
||||
OutputDebugStringW(L"Frame committed successfully\n");
|
||||
|
||||
// Unmap the staging texture
|
||||
m_d3dContext->Unmap(stagingTexture.get(), 0);
|
||||
|
||||
// Increment and log frame count
|
||||
m_frameCount++;
|
||||
OutputDebugStringW((L"GIF Frame #" + std::to_wstring(m_frameCount) + L" fully encoded and committed\n").c_str());
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
catch (const winrt::hresult_error& error)
|
||||
{
|
||||
OutputDebugStringW(error.message().c_str());
|
||||
return error.code();
|
||||
}
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
//
|
||||
// GifRecordingSession::StartAsync
|
||||
//
|
||||
//----------------------------------------------------------------------------
|
||||
winrt::IAsyncAction GifRecordingSession::StartAsync()
|
||||
{
|
||||
auto expected = false;
|
||||
if (m_isRecording.compare_exchange_strong(expected, true))
|
||||
{
|
||||
auto self = shared_from_this();
|
||||
|
||||
try
|
||||
{
|
||||
// Start capturing frames
|
||||
auto frameStartTime = std::chrono::high_resolution_clock::now();
|
||||
int captureAttempts = 0;
|
||||
int successfulCaptures = 0;
|
||||
int duplicatedFrames = 0;
|
||||
|
||||
// Keep track of the last frame to duplicate when needed
|
||||
winrt::com_ptr<ID3D11Texture2D> lastCroppedTexture;
|
||||
|
||||
while (m_isRecording && !m_closed)
|
||||
{
|
||||
captureAttempts++;
|
||||
auto frame = m_frameWait->TryGetNextFrame();
|
||||
|
||||
winrt::com_ptr<ID3D11Texture2D> croppedTexture;
|
||||
|
||||
if (frame)
|
||||
{
|
||||
successfulCaptures++;
|
||||
auto contentSize = frame->ContentSize;
|
||||
auto frameTexture = GetDXGIInterfaceFromObject<ID3D11Texture2D>(frame->FrameTexture);
|
||||
D3D11_TEXTURE2D_DESC desc = {};
|
||||
frameTexture->GetDesc(&desc);
|
||||
|
||||
// Use the smaller of the crop size or content size
|
||||
auto width = min(m_rcCrop.right - m_rcCrop.left, contentSize.Width);
|
||||
auto height = min(m_rcCrop.bottom - m_rcCrop.top, contentSize.Height);
|
||||
|
||||
D3D11_TEXTURE2D_DESC croppedDesc = {};
|
||||
croppedDesc.Width = width;
|
||||
croppedDesc.Height = height;
|
||||
croppedDesc.MipLevels = 1;
|
||||
croppedDesc.ArraySize = 1;
|
||||
croppedDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
|
||||
croppedDesc.SampleDesc.Count = 1;
|
||||
croppedDesc.Usage = D3D11_USAGE_DEFAULT;
|
||||
croppedDesc.BindFlags = D3D11_BIND_RENDER_TARGET;
|
||||
|
||||
winrt::check_hresult(m_d3dDevice->CreateTexture2D(&croppedDesc, nullptr, croppedTexture.put()));
|
||||
|
||||
// Set the content region to copy and clamp the coordinates
|
||||
D3D11_BOX region = {};
|
||||
region.left = std::clamp(m_rcCrop.left, static_cast<LONG>(0), static_cast<LONG>(desc.Width));
|
||||
region.right = std::clamp(m_rcCrop.left + width, static_cast<LONG>(0), static_cast<LONG>(desc.Width));
|
||||
region.top = std::clamp(m_rcCrop.top, static_cast<LONG>(0), static_cast<LONG>(desc.Height));
|
||||
region.bottom = std::clamp(m_rcCrop.top + height, static_cast<LONG>(0), static_cast<LONG>(desc.Height));
|
||||
region.back = 1;
|
||||
|
||||
// Copy the cropped region
|
||||
m_d3dContext->CopySubresourceRegion(
|
||||
croppedTexture.get(),
|
||||
0,
|
||||
0, 0, 0,
|
||||
frameTexture.get(),
|
||||
0,
|
||||
®ion);
|
||||
|
||||
// Save this as the last frame for duplication
|
||||
lastCroppedTexture = croppedTexture;
|
||||
}
|
||||
else if (lastCroppedTexture)
|
||||
{
|
||||
// No new frame, duplicate the last one
|
||||
duplicatedFrames++;
|
||||
croppedTexture = lastCroppedTexture;
|
||||
}
|
||||
|
||||
// Encode the frame (either new or duplicated)
|
||||
if (croppedTexture)
|
||||
{
|
||||
HRESULT hr = EncodeFrame(croppedTexture.get());
|
||||
if (FAILED(hr))
|
||||
{
|
||||
CloseInternal();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for the next frame interval
|
||||
co_await winrt::resume_after(std::chrono::milliseconds(1000 / m_frameRate));
|
||||
}
|
||||
|
||||
// Commit the GIF encoder
|
||||
if (m_gifEncoder)
|
||||
{
|
||||
auto frameEndTime = std::chrono::high_resolution_clock::now();
|
||||
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(frameEndTime - frameStartTime).count();
|
||||
|
||||
OutputDebugStringW(L"Recording stopped. Committing GIF encoder...\n");
|
||||
OutputDebugStringW((L"Total frames captured: " + std::to_wstring(m_frameCount) + L"\n").c_str());
|
||||
OutputDebugStringW((L"Capture attempts: " + std::to_wstring(captureAttempts) + L"\n").c_str());
|
||||
OutputDebugStringW((L"Successful captures: " + std::to_wstring(successfulCaptures) + L"\n").c_str());
|
||||
OutputDebugStringW((L"Duplicated frames: " + std::to_wstring(duplicatedFrames) + L"\n").c_str());
|
||||
OutputDebugStringW((L"Recording duration: " + std::to_wstring(duration) + L"ms\n").c_str());
|
||||
OutputDebugStringW((L"Actual FPS: " + std::to_wstring(m_frameCount * 1000.0 / duration) + L"\n").c_str());
|
||||
|
||||
winrt::check_hresult(m_gifEncoder->Commit());
|
||||
OutputDebugStringW(L"GIF encoder committed successfully\n");
|
||||
}
|
||||
}
|
||||
catch (const winrt::hresult_error& error)
|
||||
{
|
||||
OutputDebugStringW(L"Error in GIF recording: ");
|
||||
OutputDebugStringW(error.message().c_str());
|
||||
OutputDebugStringW(L"\n");
|
||||
|
||||
// Try to commit the encoder even on error
|
||||
if (m_gifEncoder)
|
||||
{
|
||||
try
|
||||
{
|
||||
m_gifEncoder->Commit();
|
||||
}
|
||||
catch (...) {}
|
||||
}
|
||||
|
||||
CloseInternal();
|
||||
}
|
||||
}
|
||||
co_return;
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
//
|
||||
// GifRecordingSession::Close
|
||||
//
|
||||
//----------------------------------------------------------------------------
|
||||
void GifRecordingSession::Close()
|
||||
{
|
||||
auto expected = false;
|
||||
if (m_closed.compare_exchange_strong(expected, true))
|
||||
{
|
||||
expected = true;
|
||||
if (!m_isRecording.compare_exchange_strong(expected, false))
|
||||
{
|
||||
CloseInternal();
|
||||
}
|
||||
else
|
||||
{
|
||||
m_frameWait->StopCapture();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
//
|
||||
// GifRecordingSession::CloseInternal
|
||||
//
|
||||
//----------------------------------------------------------------------------
|
||||
void GifRecordingSession::CloseInternal()
|
||||
{
|
||||
m_frameWait->StopCapture();
|
||||
m_itemClosed.revoke();
|
||||
}
|
||||
69
src/modules/ZoomIt/ZoomIt/GifRecordingSession.h
Normal file
@@ -0,0 +1,69 @@
|
||||
//==============================================================================
|
||||
//
|
||||
// Zoomit
|
||||
// Sysinternals - www.sysinternals.com
|
||||
//
|
||||
// GIF recording support using Windows Imaging Component (WIC)
|
||||
//
|
||||
//==============================================================================
|
||||
#pragma once
|
||||
|
||||
#include "CaptureFrameWait.h"
|
||||
#include <d3d11_4.h>
|
||||
#include <vector>
|
||||
|
||||
class GifRecordingSession : public std::enable_shared_from_this<GifRecordingSession>
|
||||
{
|
||||
public:
|
||||
[[nodiscard]] static std::shared_ptr<GifRecordingSession> Create(
|
||||
winrt::Direct3D11::IDirect3DDevice const& device,
|
||||
winrt::GraphicsCaptureItem const& item,
|
||||
RECT const& cropRect,
|
||||
uint32_t frameRate,
|
||||
winrt::Streams::IRandomAccessStream const& stream);
|
||||
~GifRecordingSession();
|
||||
|
||||
winrt::IAsyncAction StartAsync();
|
||||
void EnableCursorCapture(bool enable = true) { m_frameWait->EnableCursorCapture(enable); }
|
||||
void Close();
|
||||
|
||||
private:
|
||||
GifRecordingSession(
|
||||
winrt::Direct3D11::IDirect3DDevice const& device,
|
||||
winrt::Capture::GraphicsCaptureItem const& item,
|
||||
RECT const cropRect,
|
||||
uint32_t frameRate,
|
||||
winrt::Streams::IRandomAccessStream const& stream);
|
||||
void CloseInternal();
|
||||
HRESULT EncodeFrame(ID3D11Texture2D* texture);
|
||||
|
||||
private:
|
||||
winrt::Direct3D11::IDirect3DDevice m_device{ nullptr };
|
||||
winrt::com_ptr<ID3D11Device> m_d3dDevice;
|
||||
winrt::com_ptr<ID3D11DeviceContext> m_d3dContext;
|
||||
RECT m_rcCrop;
|
||||
uint32_t m_frameRate;
|
||||
|
||||
winrt::GraphicsCaptureItem m_item{ nullptr };
|
||||
winrt::GraphicsCaptureItem::Closed_revoker m_itemClosed;
|
||||
std::shared_ptr<CaptureFrameWait> m_frameWait;
|
||||
|
||||
winrt::Streams::IRandomAccessStream m_stream{ nullptr };
|
||||
|
||||
// WIC components for GIF encoding
|
||||
winrt::com_ptr<IWICImagingFactory> m_wicFactory;
|
||||
winrt::com_ptr<IWICStream> m_wicStream;
|
||||
winrt::com_ptr<IWICBitmapEncoder> m_gifEncoder;
|
||||
winrt::com_ptr<IWICMetadataQueryWriter> m_encoderMetadataWriter;
|
||||
|
||||
std::atomic<bool> m_isRecording = false;
|
||||
std::atomic<bool> m_closed = false;
|
||||
|
||||
uint32_t m_frameWidth=0;
|
||||
uint32_t m_frameHeight=0;
|
||||
uint32_t m_frameDelay=0;
|
||||
uint32_t m_frameCount = 0;
|
||||
|
||||
int32_t m_width=0;
|
||||
int32_t m_height=0;
|
||||
};
|
||||
@@ -39,6 +39,10 @@ type_pEnableThemeDialogTexture pEnableThemeDialogTexture;
|
||||
#define WIN7_VERSION 0x106
|
||||
#define WIN10_VERSION 0x206
|
||||
|
||||
// Default recording format frame rates
|
||||
#define RECORDING_FORMAT_GIF_DEFAULT_FRAMERATE 15
|
||||
#define RECORDING_FORMAT_MP4_DEFAULT_FRAMERATE 30
|
||||
|
||||
// Time that we'll cache live zoom window to avoid flicker
|
||||
// of live zooming on Vista/ws2k8
|
||||
#define LIVEZOOM_WINDOW_TIMEOUT 2*3600*1000
|
||||
|
||||
@@ -32,18 +32,18 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
|
||||
// TEXTINCLUDE
|
||||
//
|
||||
|
||||
1 TEXTINCLUDE
|
||||
1 TEXTINCLUDE
|
||||
BEGIN
|
||||
"resource.h\0"
|
||||
END
|
||||
|
||||
2 TEXTINCLUDE
|
||||
2 TEXTINCLUDE
|
||||
BEGIN
|
||||
"#include ""winres.h""\r\n"
|
||||
"\0"
|
||||
END
|
||||
|
||||
3 TEXTINCLUDE
|
||||
3 TEXTINCLUDE
|
||||
BEGIN
|
||||
"#include ""binres.rc""\0"
|
||||
END
|
||||
@@ -121,7 +121,7 @@ FONT 8, "MS Shell Dlg", 0, 0, 0x0
|
||||
BEGIN
|
||||
DEFPUSHBUTTON "OK",IDOK,166,306,50,14
|
||||
PUSHBUTTON "Cancel",IDCANCEL,223,306,50,14
|
||||
LTEXT "ZoomIt v9.10",IDC_VERSION,42,7,73,10
|
||||
LTEXT "ZoomIt v9.21",IDC_VERSION,42,7,73,10
|
||||
LTEXT "Copyright © 2006-2025 Mark Russinovich",IDC_COPYRIGHT,42,17,231,8
|
||||
CONTROL "<a HREF=""https://www.sysinternals.com"">Sysinternals - www.sysinternals.com</a>",IDC_LINK,
|
||||
"SysLink",WS_TABSTOP,42,26,150,9
|
||||
@@ -272,13 +272,15 @@ BEGIN
|
||||
LTEXT "Note: Recording is only available on Windows 10 (version 1903) and higher.",IDC_STATIC,7,77,246,19
|
||||
LTEXT "Scaling:",IDC_STATIC,30,115,26,8
|
||||
COMBOBOX IDC_RECORD_SCALING,61,114,26,30,CBS_DROPDOWNLIST | CBS_AUTOHSCROLL | CBS_OEMCONVERT | CBS_SORT | WS_VSCROLL | WS_TABSTOP
|
||||
LTEXT "Format:",IDC_STATIC,30,132,26,8
|
||||
COMBOBOX IDC_RECORD_FORMAT,61,131,60,30,CBS_DROPDOWNLIST | CBS_AUTOHSCROLL | CBS_OEMCONVERT | WS_VSCROLL | WS_TABSTOP
|
||||
LTEXT "Frame Rate:",IDC_STATIC,119,115,44,8,NOT WS_VISIBLE
|
||||
COMBOBOX IDC_RECORD_FRAME_RATE,166,114,42,30,CBS_DROPDOWNLIST | CBS_AUTOHSCROLL | CBS_OEMCONVERT | CBS_SORT | NOT WS_VISIBLE | WS_VSCROLL | WS_TABSTOP
|
||||
LTEXT "To crop the portion of the screen that will be recorded, enter the hotkey with the Shift key in the opposite mode. ",IDC_STATIC,7,32,246,19
|
||||
LTEXT "To record a specific window, enter the hotkey with the Alt key in the opposite mode.",IDC_STATIC,7,55,246,19
|
||||
CONTROL "&Capture audio input:",IDC_CAPTURE_AUDIO,"Button",BS_AUTOCHECKBOX | BS_LEFTTEXT | WS_TABSTOP,7,137,83,10
|
||||
COMBOBOX IDC_MICROPHONE,81,152,172,30,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP
|
||||
LTEXT "Microphone:",IDC_STATIC,32,154,47,8
|
||||
CONTROL "&Capture audio input:",IDC_CAPTURE_AUDIO,"Button",BS_AUTOCHECKBOX | BS_LEFTTEXT | WS_TABSTOP,7,149,83,10
|
||||
COMBOBOX IDC_MICROPHONE,81,164,172,30,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP
|
||||
LTEXT "Microphone:",IDC_STATIC,32,166,47,8
|
||||
END
|
||||
|
||||
SNIP DIALOGEX 0, 0, 260, 68
|
||||
|
||||
@@ -81,6 +81,7 @@
|
||||
<Optimization>MaxSpeed</Optimization>
|
||||
<PreprocessorDefinitions>__ZOOMIT_POWERTOYS__;_UNICODE;UNICODE;WINVER=0x602;NDEBUG;_WIN32_WINNT=0x602;_WIN32_WINDOWS=0x501;WIN32;_WINDOWS;MSVC6=1;_CRT_SECURE_NO_DEPRECATE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<StringPooling>true</StringPooling>
|
||||
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
</ClCompile>
|
||||
<ResourceCompile>
|
||||
@@ -102,6 +103,7 @@
|
||||
<Optimization>MaxSpeed</Optimization>
|
||||
<PreprocessorDefinitions>__ZOOMIT_POWERTOYS__;_UNICODE;UNICODE;WINVER=0x602;NDEBUG;_WIN32_WINNT=0x602;_WIN32_WINDOWS=0x501;WIN32;_WINDOWS;MSVC6=1;_CRT_SECURE_NO_DEPRECATE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<StringPooling>true</StringPooling>
|
||||
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
</ClCompile>
|
||||
<ResourceCompile>
|
||||
@@ -124,6 +126,7 @@
|
||||
<Optimization>MaxSpeed</Optimization>
|
||||
<PreprocessorDefinitions>__ZOOMIT_POWERTOYS__;_UNICODE;UNICODE;WINVER=0x602;NDEBUG;_WIN32_WINNT=0x602;_WIN32_WINDOWS=0x501;WIN32;_WINDOWS;MSVC6=1;_CRT_SECURE_NO_DEPRECATE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<StringPooling>true</StringPooling>
|
||||
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
</ClCompile>
|
||||
<ResourceCompile>
|
||||
@@ -145,6 +148,7 @@
|
||||
<Optimization>Disabled</Optimization>
|
||||
<PreprocessorDefinitions>__ZOOMIT_POWERTOYS__;_UNICODE;UNICODE;WINVER=0x0602;_DEBUG;_WIN32_WINNT=0x602.MSVC6;_WIN32_WINDOWS=0x600;WIN32;_WINDOWS;_WIN32_WINNT=0x602;MSVC6=1;_CRT_SECURE_NO_DEPRECATE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<BasicRuntimeChecks>EnableFastChecks</BasicRuntimeChecks>
|
||||
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
|
||||
</ClCompile>
|
||||
<ResourceCompile>
|
||||
<PreprocessorDefinitions>_DEBUG;_M_IX86;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
@@ -165,6 +169,7 @@
|
||||
<Optimization>Disabled</Optimization>
|
||||
<PreprocessorDefinitions>__ZOOMIT_POWERTOYS__;_UNICODE;UNICODE;WINVER=0x0602;_DEBUG;_WIN32_WINNT=0x602.MSVC6;_WIN32_WINDOWS=0x600;WIN32;_WINDOWS;_WIN32_WINNT=0x602;MSVC6=1;_CRT_SECURE_NO_DEPRECATE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<BasicRuntimeChecks>EnableFastChecks</BasicRuntimeChecks>
|
||||
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
|
||||
</ClCompile>
|
||||
<ResourceCompile>
|
||||
<PreprocessorDefinitions>_DEBUG;_M_X64;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
@@ -186,6 +191,7 @@
|
||||
<Optimization>Disabled</Optimization>
|
||||
<PreprocessorDefinitions>__ZOOMIT_POWERTOYS__;_UNICODE;UNICODE;WINVER=0x0602;_DEBUG;_WIN32_WINNT=0x602.MSVC6;_WIN32_WINDOWS=0x600;WIN32;_WINDOWS;_WIN32_WINNT=0x602;MSVC6=1;_CRT_SECURE_NO_DEPRECATE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<BasicRuntimeChecks>EnableFastChecks</BasicRuntimeChecks>
|
||||
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
|
||||
</ClCompile>
|
||||
<ResourceCompile>
|
||||
<PreprocessorDefinitions>_DEBUG;_M_ARM64;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
@@ -234,6 +240,7 @@
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="GifRecordingSession.cpp" />
|
||||
<ClCompile Include="pch.cpp" />
|
||||
<ClCompile Include="SelectRectangle.cpp">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">Use</PrecompiledHeader>
|
||||
@@ -288,6 +295,7 @@
|
||||
<ClInclude Include="AudioSampleGenerator.h" />
|
||||
<ClInclude Include="$(MSBuildThisFileDirectory)..\..\..\common\sysinternals\Eula\Eula.h" />
|
||||
<ClInclude Include="$(MSBuildThisFileDirectory)..\ZoomItModuleInterface\Trace.h" />
|
||||
<ClInclude Include="GifRecordingSession.h" />
|
||||
<ClInclude Include="pch.h" />
|
||||
<ClInclude Include="Registry.h" />
|
||||
<ClInclude Include="resource.h" />
|
||||
|
||||
@@ -54,6 +54,9 @@
|
||||
<ClCompile Include="$(MSBuildThisFileDirectory)..\..\..\common\sysinternals\WindowsVersions.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="GifRecordingSession.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="Registry.h">
|
||||
@@ -95,6 +98,9 @@
|
||||
<ClInclude Include="ZoomItSettings.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="GifRecordingSession.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Image Include="appicon.ico">
|
||||
|
||||
@@ -3,6 +3,13 @@
|
||||
#include "Registry.h"
|
||||
#include "DemoType.h"
|
||||
|
||||
// Recording format enum
|
||||
enum class RecordingFormat
|
||||
{
|
||||
GIF = 0,
|
||||
MP4 = 1
|
||||
};
|
||||
|
||||
DWORD g_ToggleKey = (HOTKEYF_CONTROL << 8)| '1';
|
||||
DWORD g_LiveZoomToggleKey = ((HOTKEYF_CONTROL) << 8)| '4';
|
||||
DWORD g_DrawToggleKey = ((HOTKEYF_CONTROL) << 8)| '2';
|
||||
@@ -37,9 +44,11 @@ LOGFONT g_LogFont;
|
||||
BOOLEAN g_DemoTypeUserDriven = false;
|
||||
TCHAR g_DemoTypeFile[MAX_PATH] = {0};
|
||||
DWORD g_DemoTypeSpeedSlider = static_cast<int>(((MIN_TYPING_SPEED - MAX_TYPING_SPEED) / 2) + MAX_TYPING_SPEED);
|
||||
DWORD g_RecordFrameRate = 30;
|
||||
// Divide by 100 to get actual scaling
|
||||
DWORD g_RecordScaling = 100;
|
||||
DWORD g_RecordFrameRate = 30; // We default to 30 here, but g_RecordFrameRate can be different depending on recording format and gets set accordingly
|
||||
DWORD g_RecordScaling = 100;
|
||||
DWORD g_RecordScalingGIF = 50;
|
||||
DWORD g_RecordScalingMP4 = 100;
|
||||
RecordingFormat g_RecordingFormat = RecordingFormat::MP4;
|
||||
BOOLEAN g_CaptureAudio = FALSE;
|
||||
TCHAR g_MicrophoneDeviceId[MAX_PATH] = {0};
|
||||
|
||||
@@ -78,8 +87,9 @@ REG_SETTING RegSettings[] = {
|
||||
{ L"SnapToGrid", SETTING_TYPE_BOOLEAN, 0, &g_SnapToGrid, static_cast<DOUBLE>(g_SnapToGrid) },
|
||||
{ L"ZoominSliderLevel", SETTING_TYPE_DWORD, 0, &g_SliderZoomLevel, static_cast<DOUBLE>(g_SliderZoomLevel) },
|
||||
{ L"Font", SETTING_TYPE_BINARY, sizeof g_LogFont, &g_LogFont, static_cast<DOUBLE>(0) },
|
||||
{ L"RecordFrameRate", SETTING_TYPE_DWORD, 0, &g_RecordFrameRate, static_cast<DOUBLE>(g_RecordFrameRate) },
|
||||
{ L"RecordScaling", SETTING_TYPE_DWORD, 0, &g_RecordScaling, static_cast<DOUBLE>(g_RecordScaling) },
|
||||
{ L"RecordingFormat", SETTING_TYPE_DWORD, 0, &g_RecordingFormat, static_cast<DOUBLE>(g_RecordingFormat) },
|
||||
{ L"RecordScalingGIF", SETTING_TYPE_DWORD, 0, &g_RecordScalingGIF, static_cast<DOUBLE>(g_RecordScalingGIF) },
|
||||
{ L"RecordScalingMP4", SETTING_TYPE_DWORD, 0, &g_RecordScalingMP4, static_cast<DOUBLE>(g_RecordScalingMP4) },
|
||||
{ L"CaptureAudio", SETTING_TYPE_BOOLEAN, 0, &g_CaptureAudio, static_cast<DOUBLE>(g_CaptureAudio) },
|
||||
{ L"MicrophoneDeviceId", SETTING_TYPE_STRING, sizeof(g_MicrophoneDeviceId), g_MicrophoneDeviceId, static_cast<DOUBLE>(0) },
|
||||
{ NULL, SETTING_TYPE_DWORD, 0, NULL, static_cast<DOUBLE>(0) }
|
||||
|
||||
@@ -93,6 +93,7 @@
|
||||
#define IDC_DEMOTYPE_SLIDER2 1074
|
||||
#define IDC_DEMOTYPE_STATIC2 1074
|
||||
#define IDC_COPYRIGHT 1075
|
||||
#define IDC_RECORD_FORMAT 1076
|
||||
#define IDC_PEN_WIDTH 1105
|
||||
#define IDC_TIMER 1106
|
||||
#define IDC_SMOOTH_IMAGE 1107
|
||||
|
||||
@@ -14,6 +14,9 @@ namespace winrt::PowerToys::ZoomItSettingsInterop::implementation
|
||||
const unsigned int SPECIAL_SEMANTICS_SHORTCUT = 1;
|
||||
const unsigned int SPECIAL_SEMANTICS_COLOR = 2;
|
||||
const unsigned int SPECIAL_SEMANTICS_LOG_FONT = 3;
|
||||
const unsigned int SPECIAL_SEMANTICS_RECORDING_FORMAT = 4;
|
||||
const unsigned int SPECIAL_SEMANTICS_RECORD_SCALING_GIF = 5;
|
||||
const unsigned int SPECIAL_SEMANTICS_RECORD_SCALING_MP4 = 6;
|
||||
|
||||
std::vector<unsigned char> base64_decode(const std::wstring& base64_string)
|
||||
{
|
||||
@@ -72,6 +75,9 @@ namespace winrt::PowerToys::ZoomItSettingsInterop::implementation
|
||||
{ L"PenColor", SPECIAL_SEMANTICS_COLOR },
|
||||
{ L"BreakPenColor", SPECIAL_SEMANTICS_COLOR },
|
||||
{ L"Font", SPECIAL_SEMANTICS_LOG_FONT },
|
||||
{ L"RecordingFormat", SPECIAL_SEMANTICS_RECORDING_FORMAT },
|
||||
{ L"RecordScalingGIF", SPECIAL_SEMANTICS_RECORD_SCALING_GIF },
|
||||
{ L"RecordScalingMP4", SPECIAL_SEMANTICS_RECORD_SCALING_MP4 },
|
||||
};
|
||||
|
||||
hstring ZoomItSettings::LoadSettingsJson()
|
||||
@@ -103,6 +109,11 @@ namespace winrt::PowerToys::ZoomItSettingsInterop::implementation
|
||||
value & 0xFF);
|
||||
_settings.add_property(curSetting->ValueName, hotkey.get_json());
|
||||
}
|
||||
else if (special_semantics->second == SPECIAL_SEMANTICS_RECORDING_FORMAT)
|
||||
{
|
||||
std::wstring formatString = (value == 0) ? L"GIF" : L"MP4";
|
||||
_settings.add_property(L"RecordFormat", formatString);
|
||||
}
|
||||
else if (special_semantics->second == SPECIAL_SEMANTICS_COLOR)
|
||||
{
|
||||
/* PowerToys settings likes colors as #FFFFFF strings.
|
||||
@@ -156,6 +167,9 @@ namespace winrt::PowerToys::ZoomItSettingsInterop::implementation
|
||||
curSetting++;
|
||||
}
|
||||
|
||||
DWORD recordScaling = (g_RecordingFormat == static_cast<RecordingFormat>(0)) ? g_RecordScalingGIF : g_RecordScalingMP4;
|
||||
_settings.add_property<DWORD>(L"RecordScaling", recordScaling);
|
||||
|
||||
return _settings.get_raw_json().Stringify();
|
||||
}
|
||||
|
||||
@@ -167,6 +181,8 @@ namespace winrt::PowerToys::ZoomItSettingsInterop::implementation
|
||||
PowerToysSettings::PowerToyValues valuesFromSettings =
|
||||
PowerToysSettings::PowerToyValues::from_json_string(json, L"ZoomIt");
|
||||
|
||||
bool formatChanged = false;
|
||||
|
||||
PREG_SETTING curSetting = RegSettings;
|
||||
while (curSetting->ValueName)
|
||||
{
|
||||
@@ -212,6 +228,42 @@ namespace winrt::PowerToys::ZoomItSettingsInterop::implementation
|
||||
*static_cast<PDWORD>(curSetting->Setting) = value;
|
||||
}
|
||||
}
|
||||
else if (special_semantics->second == SPECIAL_SEMANTICS_RECORDING_FORMAT)
|
||||
{
|
||||
// Convert string ("GIF" or "MP4") to DWORD enum value (0=GIF, 1=MP4)
|
||||
auto possibleValue = valuesFromSettings.get_string_value(L"RecordFormat");
|
||||
if (possibleValue.has_value())
|
||||
{
|
||||
RecordingFormat oldFormat = g_RecordingFormat;
|
||||
DWORD formatValue = (possibleValue.value() == L"GIF") ? 0 : 1;
|
||||
RecordingFormat newFormat = static_cast<RecordingFormat>(formatValue);
|
||||
|
||||
*static_cast<PDWORD>(curSetting->Setting) = formatValue;
|
||||
|
||||
if (oldFormat != newFormat)
|
||||
{
|
||||
formatChanged = true;
|
||||
|
||||
if (oldFormat == static_cast<RecordingFormat>(0))
|
||||
{
|
||||
g_RecordScalingGIF = g_RecordScaling;
|
||||
}
|
||||
else
|
||||
{
|
||||
g_RecordScalingMP4 = g_RecordScaling;
|
||||
}
|
||||
|
||||
if (newFormat == static_cast<RecordingFormat>(0))
|
||||
{
|
||||
g_RecordScaling = g_RecordScalingGIF;
|
||||
}
|
||||
else
|
||||
{
|
||||
g_RecordScaling = g_RecordScalingMP4;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (special_semantics->second == SPECIAL_SEMANTICS_COLOR)
|
||||
{
|
||||
/* PowerToys settings likes colors as #FFFFFF strings.
|
||||
@@ -275,6 +327,22 @@ namespace winrt::PowerToys::ZoomItSettingsInterop::implementation
|
||||
}
|
||||
curSetting++;
|
||||
}
|
||||
|
||||
auto recordScalingValue = valuesFromSettings.get_uint_value(L"RecordScaling");
|
||||
if (recordScalingValue.has_value() && !formatChanged)
|
||||
{
|
||||
g_RecordScaling = recordScalingValue.value();
|
||||
|
||||
if (g_RecordingFormat == static_cast<RecordingFormat>(0))
|
||||
{
|
||||
g_RecordScalingGIF = recordScalingValue.value();
|
||||
}
|
||||
else
|
||||
{
|
||||
g_RecordScalingMP4 = recordScalingValue.value();
|
||||
}
|
||||
}
|
||||
|
||||
reg.WriteRegSettings(RegSettings);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
<PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<Optimization>Disabled</Optimization>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
@@ -39,6 +40,7 @@
|
||||
<PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<Optimization>MaxSpeed</Optimization>
|
||||
<SDLCheck>false</SDLCheck>
|
||||
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
</ClCompile>
|
||||
|
||||
@@ -141,4 +141,43 @@
|
||||
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props'))" />
|
||||
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets'))" />
|
||||
</Target>
|
||||
|
||||
<!-- BEGIN common.build.post.props -->
|
||||
<!--
|
||||
The Hybrid CRT model statically links the runtime and STL and dynamically
|
||||
links the UCRT instead of the VC++ CRT. The UCRT ships with Windows.
|
||||
WinAppSDK asserts that this is "supported according to the CRT maintainer."
|
||||
|
||||
This must come before Microsoft.Cpp.targets because it manipulates ClCompile.RuntimeLibrary.
|
||||
-->
|
||||
<ItemDefinitionGroup Condition="'$(EnableHybridCRT)'=='true' and '$(Configuration)'=='Debug'">
|
||||
<ClCompile>
|
||||
<!-- We use MultiThreadedDebug, rather than MultiThreadedDebugDLL, to avoid DLL dependencies on VCRUNTIME140d.dll and MSVCP140d.dll. -->
|
||||
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<!-- Link statically against the runtime and STL, but link dynamically against the CRT by ignoring the static CRT
|
||||
lib and instead linking against the Universal CRT DLL import library. This "hybrid" linking mechanism is
|
||||
supported according to the CRT maintainer. Dynamic linking against the CRT makes the binaries a bit smaller
|
||||
than they would otherwise be if the CRT, runtime, and STL were all statically linked in. -->
|
||||
<IgnoreSpecificDefaultLibraries>%(IgnoreSpecificDefaultLibraries);libucrtd.lib</IgnoreSpecificDefaultLibraries>
|
||||
<AdditionalOptions>%(AdditionalOptions) /defaultlib:ucrtd.lib</AdditionalOptions>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(EnableHybridCRT)'=='true' and ('$(Configuration)'=='Release' or '$(Configuration)'=='AuditMode')">
|
||||
<ClCompile>
|
||||
<!-- We use MultiThreaded, rather than MultiThreadedDLL, to avoid DLL dependencies on VCRUNTIME140.dll and MSVCP140.dll. -->
|
||||
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<!-- Link statically against the runtime and STL, but link dynamically against the CRT by ignoring the static CRT
|
||||
lib and instead linking against the Universal CRT DLL import library. This "hybrid" linking mechanism is
|
||||
supported according to the CRT maintainer. Dynamic linking against the CRT makes the binaries a bit smaller
|
||||
than they would otherwise be if the CRT, runtime, and STL were all statically linked in. -->
|
||||
<IgnoreSpecificDefaultLibraries>%(IgnoreSpecificDefaultLibraries);libucrt.lib</IgnoreSpecificDefaultLibraries>
|
||||
<AdditionalOptions>%(AdditionalOptions) /defaultlib:ucrt.lib</AdditionalOptions>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<!-- END common.build.post.props -->
|
||||
|
||||
</Project>
|
||||
@@ -64,6 +64,8 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext
|
||||
|
||||
public string Title { get => string.IsNullOrEmpty(field) ? Name : field; protected set; } = string.Empty;
|
||||
|
||||
public string Id { get; protected set; } = string.Empty;
|
||||
|
||||
// This property maps to `IPage.IsLoading`, but we want to expose our own
|
||||
// `IsLoading` property as a combo of this value and `IsInitialized`
|
||||
public bool ModelIsLoading { get; protected set; } = true;
|
||||
@@ -142,6 +144,7 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext
|
||||
return; // throw?
|
||||
}
|
||||
|
||||
Id = page.Id;
|
||||
Name = page.Name;
|
||||
ModelIsLoading = page.IsLoading;
|
||||
Title = page.Title;
|
||||
|
||||
@@ -15,9 +15,12 @@ public class OpenPage : EventBase, IEvent
|
||||
{
|
||||
public int PageDepth { get; set; }
|
||||
|
||||
public OpenPage(int pageDepth)
|
||||
public string Id { get; set; }
|
||||
|
||||
public OpenPage(int pageDepth, string id)
|
||||
{
|
||||
PageDepth = pageDepth;
|
||||
Id = id;
|
||||
|
||||
EventName = "CmdPal_OpenPage";
|
||||
}
|
||||
|
||||
@@ -68,7 +68,6 @@ public sealed partial class MainWindow : WindowEx,
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
HideWindow();
|
||||
|
||||
_hwnd = new HWND(WinRT.Interop.WindowNative.GetWindowHandle(this).ToInt32());
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@ using Microsoft.PowerToys.Telemetry;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Input;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Automation.Peers;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
using Microsoft.UI.Xaml.Media.Animation;
|
||||
@@ -160,7 +159,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
new AsyncNavigationRequest(message.Page, message.CancellationToken),
|
||||
message.WithAnimation ? DefaultPageAnimation : _noAnimation);
|
||||
|
||||
PowerToysTelemetry.Log.WriteEvent(new OpenPage(RootFrame.BackStackDepth));
|
||||
PowerToysTelemetry.Log.WriteEvent(new OpenPage(RootFrame.BackStackDepth, message.Page.Id));
|
||||
|
||||
if (!ViewModel.IsNested)
|
||||
{
|
||||
@@ -655,15 +654,15 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
e.Handled = true;
|
||||
break;
|
||||
default:
|
||||
{
|
||||
// The CommandBar is responsible for handling all the item keybindings,
|
||||
// since the bound context item may need to then show another
|
||||
// context menu
|
||||
TryCommandKeybindingMessage msg = new(ctrlPressed, altPressed, shiftPressed, winPressed, e.Key);
|
||||
WeakReferenceMessenger.Default.Send(msg);
|
||||
e.Handled = msg.Handled;
|
||||
break;
|
||||
}
|
||||
{
|
||||
// The CommandBar is responsible for handling all the item keybindings,
|
||||
// since the bound context item may need to then show another
|
||||
// context menu
|
||||
TryCommandKeybindingMessage msg = new(ctrlPressed, altPressed, shiftPressed, winPressed, e.Key);
|
||||
WeakReferenceMessenger.Default.Send(msg);
|
||||
e.Handled = msg.Handled;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,11 @@
|
||||
"nativeDebugging": false,
|
||||
"doNotLaunchApp": false
|
||||
},
|
||||
"Microsoft.CmdPal.UI (Package) + Native debugging": {
|
||||
"commandName": "MsixPackage",
|
||||
"nativeDebugging": true,
|
||||
"doNotLaunchApp": false
|
||||
},
|
||||
"Microsoft.CmdPal.UI (Unpackaged)": {
|
||||
"commandName": "Project"
|
||||
}
|
||||
|
||||
@@ -429,7 +429,7 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
|
||||
<value>More</value>
|
||||
</data>
|
||||
<data name="Run_Radio_Position_LastPosition.Content" xml:space="preserve">
|
||||
<value>Last Position</value>
|
||||
<value>Last position</value>
|
||||
<comment>Reopen the window where it was last closed</comment>
|
||||
</data>
|
||||
<data name="TrayMenu_Settings" xml:space="preserve">
|
||||
|
||||
@@ -74,11 +74,35 @@
|
||||
<ClCompile>
|
||||
<LanguageStandard>stdcpp20</LanguageStandard>
|
||||
</ClCompile>
|
||||
<ClCompile>
|
||||
<!-- We use MultiThreadedDebug, rather than MultiThreadedDebugDLL, to avoid DLL dependencies on VCRUNTIME140d.dll and MSVCP140d.dll. -->
|
||||
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<!-- Link statically against the runtime and STL, but link dynamically against the CRT by ignoring the static CRT
|
||||
lib and instead linking against the Universal CRT DLL import library. This "hybrid" linking mechanism is
|
||||
supported according to the CRT maintainer. Dynamic linking against the CRT makes the binaries a bit smaller
|
||||
than they would otherwise be if the CRT, runtime, and STL were all statically linked in. -->
|
||||
<IgnoreSpecificDefaultLibraries>%(IgnoreSpecificDefaultLibraries);libucrtd.lib</IgnoreSpecificDefaultLibraries>
|
||||
<AdditionalOptions>%(AdditionalOptions) /defaultlib:ucrtd.lib /profile /opt:ref /opt:icf</AdditionalOptions>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)'=='Release'">
|
||||
<ClCompile>
|
||||
<LanguageStandard>stdcpp20</LanguageStandard>
|
||||
</ClCompile>
|
||||
<ClCompile>
|
||||
<!-- We use MultiThreaded, rather than MultiThreadedDLL, to avoid DLL dependencies on VCRUNTIME140.dll and MSVCP140.dll. -->
|
||||
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<!-- Link statically against the runtime and STL, but link dynamically against the CRT by ignoring the static CRT
|
||||
lib and instead linking against the Universal CRT DLL import library. This "hybrid" linking mechanism is
|
||||
supported according to the CRT maintainer. Dynamic linking against the CRT makes the binaries a bit smaller
|
||||
than they would otherwise be if the CRT, runtime, and STL were all statically linked in. -->
|
||||
<IgnoreSpecificDefaultLibraries>%(IgnoreSpecificDefaultLibraries);libucrt.lib</IgnoreSpecificDefaultLibraries>
|
||||
<AdditionalOptions>%(AdditionalOptions) /defaultlib:ucrt.lib /profile /opt:ref /opt:icf</AdditionalOptions>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
<ImportGroup Label="ExtensionSettings">
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<XesUseOneStoreVersioning>true</XesUseOneStoreVersioning>
|
||||
<XesBaseYearForStoreVersion>2025</XesBaseYearForStoreVersion>
|
||||
<VersionMajor>0</VersionMajor>
|
||||
<VersionMinor>6</VersionMinor>
|
||||
<VersionMinor>7</VersionMinor>
|
||||
<VersionInfoProductName>Microsoft Command Palette</VersionInfoProductName>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
||||
@@ -15,8 +15,6 @@ namespace Microsoft.CmdPal.Ext.Apps.Programs;
|
||||
|
||||
public sealed partial class AppListItem : ListItem
|
||||
{
|
||||
private static readonly Tag _appTag = new("App");
|
||||
|
||||
private readonly AppCommand _appCommand;
|
||||
private readonly AppItem _app;
|
||||
private readonly Lazy<Details> _details;
|
||||
@@ -48,7 +46,6 @@ public sealed partial class AppListItem : ListItem
|
||||
_app = app;
|
||||
Title = app.Name;
|
||||
Subtitle = app.Subtitle;
|
||||
Tags = [_appTag];
|
||||
Icon = Icons.GenericAppIcon;
|
||||
|
||||
MoreCommands = AddPinCommands(_app.Commands!, isPinned);
|
||||
|
||||
@@ -15,6 +15,7 @@ namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
[JsonSerializable(typeof(List<Choice>))]
|
||||
[JsonSerializable(typeof(List<ChoiceSetSetting>))]
|
||||
[JsonSerializable(typeof(Dictionary<string, object>), TypeInfoPropertyName = "Dictionary")]
|
||||
[JsonSerializable(typeof(List<Dictionary<string, object>>))]
|
||||
[JsonSourceGenerationOptions(UseStringEnumConverter = true, WriteIndented = true)]
|
||||
internal sealed partial class JsonSerializationContext : JsonSerializerContext
|
||||
{
|
||||
|
||||
@@ -26,15 +26,69 @@ public sealed class ToggleSetting : Setting<bool>
|
||||
|
||||
public override Dictionary<string, object> ToDictionary()
|
||||
{
|
||||
return new Dictionary<string, object>
|
||||
var items = new List<Dictionary<string, object>>();
|
||||
|
||||
if (!string.IsNullOrEmpty(Label))
|
||||
{
|
||||
{ "type", "Input.Toggle" },
|
||||
{ "title", Label },
|
||||
{ "id", Key },
|
||||
{ "label", Description },
|
||||
{ "value", JsonSerializer.Serialize(Value, JsonSerializationContext.Default.Boolean) },
|
||||
{ "isRequired", IsRequired },
|
||||
{ "errorMessage", ErrorMessage },
|
||||
items.Add(
|
||||
new()
|
||||
{
|
||||
{ "type", "TextBlock" },
|
||||
{ "text", Label },
|
||||
{ "wrap", true },
|
||||
});
|
||||
}
|
||||
|
||||
if (!(string.IsNullOrEmpty(Description) || string.Equals(Description, Label, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
items.Add(
|
||||
new()
|
||||
{
|
||||
{ "type", "TextBlock" },
|
||||
{ "text", Description },
|
||||
{ "isSubtle", true },
|
||||
{ "size", "Small" },
|
||||
{ "spacing", "Small" },
|
||||
{ "wrap", true },
|
||||
});
|
||||
}
|
||||
|
||||
return new()
|
||||
{
|
||||
{ "type", "ColumnSet" },
|
||||
{
|
||||
"columns", new List<Dictionary<string, object>>
|
||||
{
|
||||
new()
|
||||
{
|
||||
{ "type", "Column" },
|
||||
{ "width", "20px" },
|
||||
{
|
||||
"items", new List<Dictionary<string, object>>
|
||||
{
|
||||
new()
|
||||
{
|
||||
{ "type", "Input.Toggle" },
|
||||
{ "title", " " },
|
||||
{ "id", Key },
|
||||
{ "value", JsonSerializer.Serialize(Value, JsonSerializationContext.Default.Boolean) },
|
||||
{ "isRequired", IsRequired },
|
||||
{ "errorMessage", ErrorMessage },
|
||||
},
|
||||
}
|
||||
},
|
||||
{ "verticalContentAlignment", "Center" },
|
||||
},
|
||||
new()
|
||||
{
|
||||
{ "type", "Column" },
|
||||
{ "width", "stretch" },
|
||||
{ "items", items },
|
||||
{ "verticalContentAlignment", "Center" },
|
||||
},
|
||||
}
|
||||
},
|
||||
{ "spacing", "Medium" },
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -68,6 +68,34 @@
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
<LinkIncremental>false</LinkIncremental>
|
||||
</PropertyGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)'=='Debug'">
|
||||
<ClCompile>
|
||||
<!-- We use MultiThreadedDebug, rather than MultiThreadedDebugDLL, to avoid DLL dependencies on VCRUNTIME140d.dll and MSVCP140d.dll. -->
|
||||
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<!-- Link statically against the runtime and STL, but link dynamically against the CRT by ignoring the static CRT
|
||||
lib and instead linking against the Universal CRT DLL import library. This "hybrid" linking mechanism is
|
||||
supported according to the CRT maintainer. Dynamic linking against the CRT makes the binaries a bit smaller
|
||||
than they would otherwise be if the CRT, runtime, and STL were all statically linked in. -->
|
||||
<IgnoreSpecificDefaultLibraries>%(IgnoreSpecificDefaultLibraries);libucrtd.lib</IgnoreSpecificDefaultLibraries>
|
||||
<AdditionalOptions>%(AdditionalOptions) /defaultlib:ucrtd.lib /profile /opt:ref /opt:icf</AdditionalOptions>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)'=='Release'">
|
||||
<ClCompile>
|
||||
<!-- We use MultiThreaded, rather than MultiThreadedDLL, to avoid DLL dependencies on VCRUNTIME140.dll and MSVCP140.dll. -->
|
||||
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<!-- Link statically against the runtime and STL, but link dynamically against the CRT by ignoring the static CRT
|
||||
lib and instead linking against the Universal CRT DLL import library. This "hybrid" linking mechanism is
|
||||
supported according to the CRT maintainer. Dynamic linking against the CRT makes the binaries a bit smaller
|
||||
than they would otherwise be if the CRT, runtime, and STL were all statically linked in. -->
|
||||
<IgnoreSpecificDefaultLibraries>%(IgnoreSpecificDefaultLibraries);libucrt.lib</IgnoreSpecificDefaultLibraries>
|
||||
<AdditionalOptions>%(AdditionalOptions) /defaultlib:ucrt.lib /profile /opt:ref /opt:icf</AdditionalOptions>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
<ImportGroup Label="ExtensionSettings">
|
||||
</ImportGroup>
|
||||
|
||||