Compare commits
37 Commits
dev/migrie
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
af45c3ec7c | ||
|
|
de4859454c | ||
|
|
93669df118 | ||
|
|
56fabda79c | ||
|
|
70555459ab | ||
|
|
d6319516d0 | ||
|
|
53737cbe31 | ||
|
|
bf6ff579d3 | ||
|
|
0afe525f31 | ||
|
|
a43fb12d6f | ||
|
|
bc56443443 | ||
|
|
3298625b67 | ||
|
|
ae9f241ef1 | ||
|
|
67a9fa2d13 | ||
|
|
1cfc923bdb | ||
|
|
2dd802f367 | ||
|
|
a0d17406ba | ||
|
|
4a27c5d5f9 | ||
|
|
8bd5c1be6f | ||
|
|
7b19b4c219 | ||
|
|
b73fd670be | ||
|
|
a46a4437e5 | ||
|
|
3bf682048e | ||
|
|
28a9bbe8f0 | ||
|
|
536e768cac | ||
|
|
70ff4013b9 | ||
|
|
7a04d4c270 | ||
|
|
8c434cd6f4 | ||
|
|
d983dbc285 | ||
|
|
fb6843b0f1 | ||
|
|
6dd1ce5dd1 | ||
|
|
9ea30ec523 | ||
|
|
c777fcc1e4 | ||
|
|
28e078897a | ||
|
|
64f1243bdf | ||
|
|
e1074bc835 | ||
|
|
2390aacbfc |
6
.github/actions/spell-check/expect.txt
vendored
@@ -135,6 +135,7 @@ BITMAPINFO
|
||||
BITMAPINFOHEADER
|
||||
BITSPERPEL
|
||||
BITSPIXEL
|
||||
Blackmagic
|
||||
bla
|
||||
BLENDFUNCTION
|
||||
blittable
|
||||
@@ -539,6 +540,7 @@ EXTRINSICPROPERTIES
|
||||
eyetracker
|
||||
FANCYZONESDRAWLAYOUTTEST
|
||||
FANCYZONESEDITOR
|
||||
Fairlight
|
||||
FARPROC
|
||||
fdw
|
||||
fdx
|
||||
@@ -836,6 +838,7 @@ INTRESOURCE
|
||||
INVALIDARG
|
||||
invalidoperatioexception
|
||||
invokecommand
|
||||
iOS
|
||||
ipcmanager
|
||||
ipreviewhandlervisualssetfont
|
||||
IPTC
|
||||
@@ -1232,6 +1235,8 @@ NOTSRCCOPY
|
||||
NOTSRCERASE
|
||||
Notupdated
|
||||
notwindows
|
||||
NOTXORPEN
|
||||
Nouveaut
|
||||
nowarn
|
||||
NOZORDER
|
||||
NPH
|
||||
@@ -2178,6 +2183,7 @@ xclip
|
||||
xcopy
|
||||
xdf
|
||||
xfd
|
||||
xhair
|
||||
xmp
|
||||
Xoshiro
|
||||
xsi
|
||||
|
||||
6
.github/copilot-instructions.md
vendored
@@ -30,6 +30,12 @@ These are auto-applied based on file location:
|
||||
- [Runner & Settings UI](.github/instructions/runner-settings-ui.instructions.md)
|
||||
- [Common Libraries](.github/instructions/common-libraries.instructions.md)
|
||||
|
||||
## Shortcut Guide V2 Manifests
|
||||
|
||||
When creating or editing Shortcut Guide keyboard shortcut manifest files, follow the schema and naming conventions in the spec:
|
||||
|
||||
- [WinGet Manifest Keyboard Shortcuts schema](<../doc/specs/WinGet Manifest Keyboard Shortcuts schema.md>) – manifest file format, field definitions, file naming, and the `+` prefix convention for apps without a WinGet package
|
||||
|
||||
## Detailed Documentation
|
||||
|
||||
- [Architecture](../doc/devdocs/core/architecture.md)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: wpf-to-winui3-migration
|
||||
description: Guide for migrating PowerToys modules from WPF to WinUI 3 (Windows App SDK). Use when asked to migrate WPF code, convert WPF XAML to WinUI, replace System.Windows namespaces with Microsoft.UI.Xaml, update Dispatcher to DispatcherQueue, replace DynamicResource with ThemeResource, migrate imaging APIs from System.Windows.Media.Imaging to Windows.Graphics.Imaging, convert WPF Window to WinUI Window, migrate .resx to .resw resources, migrate custom Observable/RelayCommand to CommunityToolkit.Mvvm source generators, handle WPF-UI (Lepo) to WinUI native control migration, or fix installer/build pipeline issues after migration. Keywords: WPF, WinUI, WinUI3, migration, porting, convert, namespace, XAML, Dispatcher, DispatcherQueue, imaging, BitmapImage, Window, ContentDialog, ThemeResource, DynamicResource, ResourceLoader, resw, resx, CommunityToolkit, ObservableProperty, WPF-UI, SizeToContent, AppWindow, SoftwareBitmap.
|
||||
description: 'Guide for migrating PowerToys modules from WPF to WinUI 3 (Windows App SDK). Use when asked to migrate WPF code, convert WPF XAML to WinUI, replace System.Windows namespaces with Microsoft.UI.Xaml, update Dispatcher to DispatcherQueue, replace DynamicResource with ThemeResource, migrate imaging APIs from System.Windows.Media.Imaging to Windows.Graphics.Imaging, convert WPF Window to WinUI Window, migrate .resx to .resw resources, migrate custom Observable/RelayCommand to CommunityToolkit.Mvvm source generators, handle WPF-UI (Lepo) to WinUI native control migration, or fix installer/build pipeline issues after migration. Keywords: WPF, WinUI, WinUI3, migration, porting, convert, namespace, XAML, Dispatcher, DispatcherQueue, imaging, BitmapImage, Window, ContentDialog, ThemeResource, DynamicResource, ResourceLoader, resw, resx, CommunityToolkit, ObservableProperty, WPF-UI, SizeToContent, AppWindow, SoftwareBitmap.'
|
||||
license: Complete terms in LICENSE.txt
|
||||
---
|
||||
|
||||
|
||||
7
.github/workflows/auto-labeler.yml
vendored
@@ -73,6 +73,13 @@ jobs:
|
||||
|
||||
const itemType = issue.pull_request ? 'Pull request' : 'Issue';
|
||||
|
||||
// Skip pull requests that already have labels applied.
|
||||
if (issue.pull_request && issue.labels && issue.labels.length > 0) {
|
||||
const existingLabels = issue.labels.map(l => l.name).join(', ');
|
||||
console.log(`${itemType} #${issueNumber} already has labels (${existingLabels}); skipping.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const title = issue.title ?? '';
|
||||
const body = issue.body ?? '';
|
||||
|
||||
|
||||
@@ -4,6 +4,29 @@
|
||||
<Import Project="$(MSBuildCachePackageRoot)\build\$(MSBuildCachePackageName).targets" Condition="'$(MSBuildCacheEnabled)' == 'true'" />
|
||||
<Import Project="$(MSBuildCacheSharedCompilationPackageRoot)\build\Microsoft.MSBuildCache.SharedCompilation.targets" Condition="'$(MSBuildCacheEnabled)' == 'true'" />
|
||||
|
||||
<!--
|
||||
Onboarding guard: PowerToys has deeply nested source paths that exceed the legacy
|
||||
260-character MAX_PATH limit. Without Windows long path support enabled, the build
|
||||
fails with cryptic "path too long" / "could not find file" errors that are hard for
|
||||
new contributors to diagnose. Detect the missing registry setting up front and emit a
|
||||
clear, actionable error before the confusing failures occur.
|
||||
|
||||
- Covers both Visual Studio (Ctrl+Shift+B) and the command-line build scripts.
|
||||
- Runs only during real builds (skips design-time/IntelliSense passes).
|
||||
- Bypass with /p:SkipLongPathsCheck=true if you know what you're doing.
|
||||
See tools\build\setup-dev-environment.ps1 to enable everything automatically.
|
||||
-->
|
||||
<Target Name="EnsureLongPathsEnabled"
|
||||
BeforeTargets="PrepareForBuild"
|
||||
Condition="'$(DesignTimeBuild)' != 'true' and '$(SkipLongPathsCheck)' != 'true' and '$(OS)' == 'Windows_NT'">
|
||||
<PropertyGroup>
|
||||
<_LongPathsEnabled>$([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\FileSystem', 'LongPathsEnabled', null, RegistryView.Registry64))</_LongPathsEnabled>
|
||||
</PropertyGroup>
|
||||
<Error Condition="'$(_LongPathsEnabled)' != '1'"
|
||||
Code="PTLONGPATH"
|
||||
Text="Windows long path support is not enabled. PowerToys source paths exceed the 260-character MAX_PATH limit, so the build will fail with cryptic 'path too long' errors. Fix it by running (from an elevated PowerShell): .\tools\build\setup-dev-environment.ps1 -- or set HKLM\SYSTEM\CurrentControlSet\Control\FileSystem\LongPathsEnabled = 1 (DWORD) and restart Windows. To bypass this check, build with /p:SkipLongPathsCheck=true." />
|
||||
</Target>
|
||||
|
||||
<!-- Override ManifestTool to the x64 host tool under WindowsSdkDir for all projects once the SDK path is known. -->
|
||||
<PropertyGroup Label="ManifestToolOverride">
|
||||
<ManifestTool Condition="Exists('$(WindowsSdkDir)bin\x64\mt.exe')">$(WindowsSdkDir)bin\x64\mt.exe</ManifestTool>
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
<PackageVersion Include="CommunityToolkit.WinUI.Controls.Sizers" Version="8.2.251219" />
|
||||
<PackageVersion Include="CommunityToolkit.WinUI.Converters" Version="8.2.251219" />
|
||||
<PackageVersion Include="CommunityToolkit.WinUI.Extensions" Version="8.2.251219" />
|
||||
<PackageVersion Include="CommunityToolkit.WinUI.UI.Controls.DataGrid" Version="7.1.2" />
|
||||
<PackageVersion Include="CommunityToolkit.WinUI.UI.Controls.DataGrid" Version="7.1.2" />
|
||||
<PackageVersion Include="CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock" Version="0.1.260116-build.2514" />
|
||||
<PackageVersion Include="ControlzEx" Version="6.0.0" />
|
||||
<PackageVersion Include="HelixToolkit" Version="2.24.0" />
|
||||
@@ -38,7 +38,7 @@
|
||||
<PackageVersion Include="Mages" Version="3.0.0" />
|
||||
<PackageVersion Include="Markdig.Signed" Version="0.34.0" />
|
||||
<!-- 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. -->
|
||||
<PackageVersion Include="MessagePack" Version="3.1.3" />
|
||||
<PackageVersion Include="MessagePack" Version="3.1.7" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="10.0.102" />
|
||||
<PackageVersion Include="Microsoft.CommandPalette.Extensions" Version="0.9.260303001" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.8" />
|
||||
@@ -64,7 +64,7 @@
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.MistralAI" Version="1.71.0-alpha" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.Ollama" Version="1.71.0-alpha" />
|
||||
<PackageVersion Include="Microsoft.Toolkit.Uwp.Notifications" Version="7.1.2" />
|
||||
<PackageVersion Include="Microsoft.Web.WebView2" Version="1.0.3719.77" />
|
||||
<PackageVersion Include="Microsoft.Web.WebView2" Version="1.0.4022.49" />
|
||||
<!-- Package Microsoft.Win32.SystemEvents added as a hack for being able to exclude the runtime assets so they don't conflict with 8.0.1. This is a dependency of System.Drawing.Common but the 8.0.1 version wasn't published to nuget. -->
|
||||
<PackageVersion Include="Microsoft.Win32.SystemEvents" Version="10.0.8" />
|
||||
<PackageVersion Include="Microsoft.WindowsPackageManager.ComInterop" Version="1.10.340" />
|
||||
@@ -76,7 +76,7 @@
|
||||
This is present due to a bug in CsWinRT where WPF projects cause the analyzer to fail.
|
||||
-->
|
||||
<PackageVersion Include="Microsoft.Windows.CsWinRT" Version="2.2.0" />
|
||||
<PackageVersion Include="Microsoft.Windows.ImplementationLibrary" Version="1.0.250325.1"/>
|
||||
<PackageVersion Include="Microsoft.Windows.ImplementationLibrary" Version="1.0.250325.1" />
|
||||
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.6901" />
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="2.2.0" />
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK.Foundation" Version="2.1.0" />
|
||||
@@ -151,4 +151,4 @@
|
||||
<PackageVersion Include="Microsoft.VariantAssignment.Client" Version="2.4.17140001" />
|
||||
<PackageVersion Include="Microsoft.VariantAssignment.Contract" Version="3.0.16990001" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
64
NOTICE.md
@@ -12,6 +12,7 @@ This software incorporates material from third parties.
|
||||
- Peek
|
||||
- PowerDisplay
|
||||
- Registry Preview
|
||||
- ZoomIt
|
||||
|
||||
## Utility: Color Picker
|
||||
|
||||
@@ -1549,6 +1550,69 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
## Utility: ZoomIt
|
||||
|
||||
### libwebp
|
||||
|
||||
ZoomIt uses libwebp to encode screenshots in the WebP image format.
|
||||
|
||||
**Source**: <https://github.com/webmproject/libwebp>
|
||||
|
||||
BSD-3-Clause License
|
||||
|
||||
Copyright (c) 2010, Google Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in
|
||||
the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
|
||||
* Neither the name of Google nor the names of its contributors may
|
||||
be used to endorse or promote products derived from this software
|
||||
without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
Additional IP Rights Grant (Patents)
|
||||
|
||||
"These implementations" means the copyrightable works that implement the WebM
|
||||
codecs distributed by Google as part of the WebM Project.
|
||||
|
||||
Google hereby grants to you a perpetual, worldwide, non-exclusive, no-charge,
|
||||
royalty-free, irrevocable (except as stated in this section) patent license to
|
||||
make, have made, use, offer to sell, sell, import, transfer, and otherwise
|
||||
run, modify and propagate the contents of these implementations of WebM, where
|
||||
such license applies only to those patent claims, both currently owned by
|
||||
Google and acquired in the future, licensable by Google that are necessarily
|
||||
infringed by these implementations of WebM. This grant does not include claims
|
||||
that would be infringed only as a consequence of further modification of these
|
||||
implementations. If you or your agent or exclusive licensee institute or order
|
||||
or agree to the institution of patent litigation or any other patent
|
||||
enforcement activity against any entity (including a cross-claim or
|
||||
counterclaim in a lawsuit) alleging that any of these implementations of WebM
|
||||
or any code incorporated within any of these implementations of WebM
|
||||
constitute direct or contributory patent infringement, or inducement of
|
||||
patent infringement, then any patent rights granted to you under this License
|
||||
for these implementations of WebM shall terminate as of the date such
|
||||
litigation is filed.
|
||||
|
||||
## NuGet Packages used by PowerToys
|
||||
|
||||
- AdaptiveCards.ObjectModel.WinUI3
|
||||
|
||||
@@ -59,13 +59,26 @@ This component:
|
||||
|
||||
### Activation Mechanism
|
||||
|
||||
The Quick Accent is activated when:
|
||||
Quick Accent supports two activation styles, selected by the **Activation key** setting.
|
||||
|
||||
**Trigger-key modes** (`Left/Right arrow`, `Space`, or `Both` — the default):
|
||||
1. A user presses and holds a character key (e.g., 'a')
|
||||
2. User presses the trigger key
|
||||
3. After a brief delay (around 300ms per setting), the accent toolbar appears
|
||||
4. The user can select an accented variant using the trigger key
|
||||
5. Upon releasing the keys, the selected accented character is inserted
|
||||
|
||||
**Press-and-hold mode** (`Press and hold the letter`, iOS/macOS style, opt-in):
|
||||
1. A user presses and holds an accent-capable character key (e.g., 'a'); the base
|
||||
letter is typed immediately
|
||||
2. After the configured **Hold duration** (around 500ms per setting), the accent
|
||||
toolbar appears automatically — no separate trigger key is required
|
||||
3. The user navigates the options with the arrow keys or Space
|
||||
4. Upon releasing the letter, the selected accent replaces the base letter; if no
|
||||
option was selected, the base letter that was already typed simply remains
|
||||
5. A quick tap (shorter than the Hold duration) types the base letter only, and
|
||||
modifier combinations (Ctrl/Alt/AltGr/Win + letter) are left untouched
|
||||
|
||||
### Character Sets
|
||||
|
||||
The module includes multiple language-specific character sets and special character sets:
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
# [CleanUp_tool](/tools/CleanUp_tool/) and [CleanUp_tool_powershell_script](/tools/CleanUp_tool_powershell_script/CleanUp_tool.ps1)
|
||||
|
||||
This tool, respective this powershell script, is used to clean up the PowerToys installation. It cleans the `AppData` folder and the registry.
|
||||
|
||||
This tool is currently very outdated and just cleans up the registry keys of some few modules.
|
||||
@@ -10,7 +10,6 @@ Following tools are currently available:
|
||||
|
||||
* [BugReportTool](bug-report-tool.md) - A tool to collect logs and system information for bug reports.
|
||||
* [Build tools](build-tools.md) - A set of scripts that help building PowerToys.
|
||||
* [Clean up tool](clean-up-tool.md) - A tool to clean up the PowerToys installation.
|
||||
* [Monitor info report](monitor-info-report.md) - A small diagnostic tool which helps identifying WinAPI bugs related to the physical monitor detection.
|
||||
* [project template](/tools/project_template/README.md) - A Visual Studio project template for a new PowerToys project.
|
||||
* [StylesReportTool](styles-report-tool.md) - A tool to collect information about an open window.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="Microsoft.Windows.ImplementationLibrary" version="1.0.250325.1" targetFramework="native" />
|
||||
</packages>
|
||||
<package id="Microsoft.Windows.ImplementationLibrary" version="1.0.260126.7" targetFramework="native" />
|
||||
</packages>
|
||||
@@ -29,8 +29,30 @@ namespace Microsoft.PowerToys.Common.UI.Controls.Backdrops;
|
||||
/// </remarks>
|
||||
public sealed partial class AlwaysActiveDesktopAcrylicBackdrop : SystemBackdrop
|
||||
{
|
||||
/// <summary>
|
||||
/// Identifies the <see cref="Kind"/> dependency property.
|
||||
/// </summary>
|
||||
public static readonly DependencyProperty KindProperty = DependencyProperty.Register(
|
||||
nameof(Kind),
|
||||
typeof(DesktopAcrylicKind),
|
||||
typeof(AlwaysActiveDesktopAcrylicBackdrop),
|
||||
new PropertyMetadata(DesktopAcrylicKind.Default, OnKindChanged));
|
||||
|
||||
private readonly Dictionary<ICompositionSupportsSystemBackdrop, BackdropTarget> _targets = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the desktop acrylic material variant to render. Defaults to
|
||||
/// <see cref="DesktopAcrylicKind.Default"/> (the standard, more opaque
|
||||
/// acrylic); <see cref="DesktopAcrylicKind.Thin"/> renders a lighter, more
|
||||
/// translucent material and <see cref="DesktopAcrylicKind.Base"/> the base
|
||||
/// material. Changing this updates any live backdrop targets immediately.
|
||||
/// </summary>
|
||||
public DesktopAcrylicKind Kind
|
||||
{
|
||||
get => (DesktopAcrylicKind)GetValue(KindProperty);
|
||||
set => SetValue(KindProperty, value);
|
||||
}
|
||||
|
||||
protected override void OnTargetConnected(ICompositionSupportsSystemBackdrop connectedTarget, XamlRoot xamlRoot)
|
||||
{
|
||||
base.OnTargetConnected(connectedTarget, xamlRoot);
|
||||
@@ -41,7 +63,10 @@ public sealed partial class AlwaysActiveDesktopAcrylicBackdrop : SystemBackdrop
|
||||
Theme = ResolveTheme(xamlRoot),
|
||||
};
|
||||
|
||||
var controller = new DesktopAcrylicController();
|
||||
var controller = new DesktopAcrylicController
|
||||
{
|
||||
Kind = Kind,
|
||||
};
|
||||
controller.SetSystemBackdropConfiguration(configuration);
|
||||
controller.AddSystemBackdropTarget(connectedTarget);
|
||||
|
||||
@@ -70,6 +95,17 @@ public sealed partial class AlwaysActiveDesktopAcrylicBackdrop : SystemBackdrop
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnKindChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
var self = (AlwaysActiveDesktopAcrylicBackdrop)d;
|
||||
var kind = (DesktopAcrylicKind)e.NewValue;
|
||||
|
||||
foreach (var target in self._targets.Values)
|
||||
{
|
||||
target.Controller.Kind = kind;
|
||||
}
|
||||
}
|
||||
|
||||
private static SystemBackdropTheme ResolveTheme(XamlRoot xamlRoot) =>
|
||||
xamlRoot.Content is FrameworkElement rootElement
|
||||
? rootElement.ActualTheme switch
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
xmlns:backdrops="using:Microsoft.PowerToys.Common.UI.Controls.Backdrops"
|
||||
xmlns:local="using:Microsoft.PowerToys.Common.UI.Controls">
|
||||
|
||||
<Style BasedOn="{StaticResource DefaultTransparentCardStyle}" TargetType="local:TransparentCard" />
|
||||
<Style BasedOn="{StaticResource DefaultTransientSurfaceStyle}" TargetType="local:TransientSurface" />
|
||||
|
||||
<Style x:Key="DefaultTransparentCardStyle" TargetType="local:TransparentCard">
|
||||
<Style x:Key="DefaultTransientSurfaceStyle" TargetType="local:TransientSurface">
|
||||
<Setter Property="BorderBrush" Value="{ThemeResource SurfaceStrokeColorDefaultBrush}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="CornerRadius" Value="8" />
|
||||
@@ -16,7 +16,7 @@
|
||||
<Setter Property="VerticalContentAlignment" Value="Stretch" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="local:TransparentCard">
|
||||
<ControlTemplate TargetType="local:TransientSurface">
|
||||
<Grid
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
@@ -27,7 +27,7 @@
|
||||
</Grid.Shadow>
|
||||
<SystemBackdropElement CornerRadius="{TemplateBinding CornerRadius}">
|
||||
<SystemBackdropElement.SystemBackdrop>
|
||||
<backdrops:AlwaysActiveDesktopAcrylicBackdrop />
|
||||
<backdrops:AlwaysActiveDesktopAcrylicBackdrop Kind="{TemplateBinding AcrylicKind}" />
|
||||
</SystemBackdropElement.SystemBackdrop>
|
||||
</SystemBackdropElement>
|
||||
<ContentPresenter
|
||||
@@ -41,5 +41,4 @@
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
</ResourceDictionary>
|
||||
@@ -0,0 +1,467 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using CommunityToolkit.WinUI;
|
||||
using CommunityToolkit.WinUI.Animations;
|
||||
using Microsoft.PowerToys.Common.UI.Controls.Window;
|
||||
using Microsoft.UI.Composition.SystemBackdrops;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Hosting;
|
||||
|
||||
namespace Microsoft.PowerToys.Common.UI.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// A floating, self-animating "pseudo window" surface for transient PowerToys
|
||||
/// overlays (toasts, banners, indicators). It looks like a control but behaves
|
||||
/// like a lightweight window: it provides the PowerToys-standard chrome — 1 px
|
||||
/// border in <c>SurfaceStrokeColorDefaultBrush</c>, 8 px corner radius, a
|
||||
/// <c>ThemeShadow</c>, and an always-active desktop acrylic backdrop — and owns
|
||||
/// its own show/hide animations.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>Designed to be declared as the root content of a
|
||||
/// <see cref="TransparentWindow"/>, which stays animation-agnostic. Call
|
||||
/// <see cref="SubscribeTo"/> once (e.g. from the hosting window's constructor)
|
||||
/// to wire this surface to the window's <see cref="TransparentWindow.Showing"/> /
|
||||
/// <see cref="TransparentWindow.Hiding"/> events. From then on the surface
|
||||
/// animates itself in/out whenever the window is shown or hidden, and uses the
|
||||
/// <see cref="TransparentWindow.Hiding"/> deferral to keep the window visible
|
||||
/// until its out-animation finishes.</para>
|
||||
/// <para>The show transition comes from the window's
|
||||
/// <see cref="TransparentWindow.Show(Transition)"/> call (or from
|
||||
/// <see cref="ShowTransition"/> when shown without one); the hide transition
|
||||
/// always comes from <see cref="HideTransition"/>. Animations target the
|
||||
/// surface itself, so the entire surface (border, acrylic, shadow, inner
|
||||
/// content) animates as one. Apps that want a different look supply their own
|
||||
/// <c>Style TargetType="TransientSurface"</c> in resources — the standard WinUI
|
||||
/// restyle path.</para>
|
||||
/// </remarks>
|
||||
public sealed partial class TransientSurface : ContentControl
|
||||
{
|
||||
private const float ShadowDepth = 32f;
|
||||
private const double SlideInOffset = 24;
|
||||
private const double SlideOutOffset = 12;
|
||||
|
||||
// "Pop" transition: scale between 96% and 100% (a subtle 4% grow). Following
|
||||
// Fluent motion guidance the scale uses a decelerate (EaseOut) curve; the
|
||||
// fade is kept fast so the surface reads as an instant, light pop.
|
||||
//
|
||||
// The fade must run at least as long as the scale: if the scale outlasted the
|
||||
// fade, the surface would reach full opacity while still visibly growing,
|
||||
// which reads as a "resize" rather than a pop. Keeping the fade >= the scale
|
||||
// hides the growth under the opacity ramp, so by the time it is fully opaque
|
||||
// it is already at 100% size.
|
||||
private const float PopScaleFrom = 0.96f;
|
||||
private const double PopFadeShowMs = 180;
|
||||
private const double PopScaleShowMs = 150;
|
||||
private const double PopFadeHideMs = 120;
|
||||
private const double PopScaleHideMs = 120;
|
||||
|
||||
public static readonly DependencyProperty ShowTransitionProperty = DependencyProperty.Register(
|
||||
nameof(ShowTransition),
|
||||
typeof(Transition),
|
||||
typeof(TransientSurface),
|
||||
new PropertyMetadata(Transition.None, OnTransitionChanged));
|
||||
|
||||
public static readonly DependencyProperty HideTransitionProperty = DependencyProperty.Register(
|
||||
nameof(HideTransition),
|
||||
typeof(Transition),
|
||||
typeof(TransientSurface),
|
||||
new PropertyMetadata(Transition.None, OnTransitionChanged));
|
||||
|
||||
public static readonly DependencyProperty AcrylicKindProperty = DependencyProperty.Register(
|
||||
nameof(AcrylicKind),
|
||||
typeof(DesktopAcrylicKind),
|
||||
typeof(TransientSurface),
|
||||
new PropertyMetadata(DesktopAcrylicKind.Thin));
|
||||
|
||||
private readonly DispatcherQueueTimer _hideCompletedTimer = DispatcherQueue.GetForCurrentThread().CreateTimer();
|
||||
|
||||
private readonly ImplicitAnimationSet _noAnimations = new();
|
||||
|
||||
private ImplicitAnimationSet _showAnimations = new();
|
||||
private ImplicitAnimationSet _hideAnimations = new();
|
||||
private bool _hasCustomShowAnimations;
|
||||
private bool _hasCustomHideAnimations;
|
||||
private Action? _abandonPendingHide;
|
||||
|
||||
public TransientSurface()
|
||||
{
|
||||
DefaultStyleKey = typeof(TransientSurface);
|
||||
|
||||
RebuildDefaultAnimations();
|
||||
|
||||
// Pin the scale center to the surface's center so the "Pop" transition
|
||||
// grows/shrinks from the middle, not the top-left corner. An expression
|
||||
// animation bound to the visual's own size keeps the center correct from
|
||||
// the very first frame (a SizeChanged handler would race the show
|
||||
// animation and let the first pop scale from 0,0).
|
||||
PinScaleCenter();
|
||||
|
||||
// Start hidden so the first Show() animates in from the configured pose.
|
||||
Visibility = Visibility.Collapsed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raised after <see cref="Hide"/> once the longest animation in
|
||||
/// <see cref="HideAnimations"/> (delay + duration) has completed.
|
||||
/// </summary>
|
||||
public event EventHandler? HideCompleted;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the transition played when the surface is shown without an
|
||||
/// explicit one (see <see cref="Show()"/>). Defaults to
|
||||
/// <see cref="Transition.None"/>, which plays no animation at all (the
|
||||
/// surface appears instantly); a directional value adds a fade plus a slide
|
||||
/// in from that edge, and <see cref="Transition.Pop"/> a fade plus a subtle
|
||||
/// scale-up. Changing this regenerates the default <see cref="ShowAnimations"/>
|
||||
/// unless it has been set explicitly.
|
||||
/// </summary>
|
||||
public Transition ShowTransition
|
||||
{
|
||||
get => (Transition)GetValue(ShowTransitionProperty);
|
||||
set => SetValue(ShowTransitionProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the transition played when the surface is hidden (see
|
||||
/// <see cref="Hide"/>). Defaults to <see cref="Transition.None"/>, which
|
||||
/// plays no animation at all (the surface disappears instantly); a
|
||||
/// directional value adds a fade plus a slide out toward that edge, and
|
||||
/// <see cref="Transition.Pop"/> a fade plus a subtle scale-down. Changing
|
||||
/// this regenerates the default <see cref="HideAnimations"/> unless it has
|
||||
/// been set explicitly.
|
||||
/// </summary>
|
||||
public Transition HideTransition
|
||||
{
|
||||
get => (Transition)GetValue(HideTransitionProperty);
|
||||
set => SetValue(HideTransitionProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the desktop acrylic material variant painted behind the
|
||||
/// surface. Defaults to <see cref="DesktopAcrylicKind.Thin"/> (a lighter,
|
||||
/// more translucent material); set <see cref="DesktopAcrylicKind.Default"/>
|
||||
/// for the standard, more opaque acrylic or <see cref="DesktopAcrylicKind.Base"/>
|
||||
/// for the base material. Has no effect when a custom template without the
|
||||
/// default acrylic backdrop is applied.
|
||||
/// </summary>
|
||||
public DesktopAcrylicKind AcrylicKind
|
||||
{
|
||||
get => (DesktopAcrylicKind)GetValue(AcrylicKindProperty);
|
||||
set => SetValue(AcrylicKindProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the animations played when <see cref="Show()"/> flips the
|
||||
/// surface to <see cref="Visibility.Visible"/>. Defaults to the animation
|
||||
/// derived from <see cref="ShowTransition"/>. Assigning a value marks the set
|
||||
/// as custom so <see cref="ShowTransition"/> no longer overwrites it.
|
||||
/// </summary>
|
||||
public ImplicitAnimationSet ShowAnimations
|
||||
{
|
||||
get => _showAnimations;
|
||||
set
|
||||
{
|
||||
_showAnimations = value ?? new ImplicitAnimationSet();
|
||||
_hasCustomShowAnimations = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the animations played when <see cref="Hide"/> flips the
|
||||
/// surface to <see cref="Visibility.Collapsed"/>. Defaults to the animation
|
||||
/// derived from <see cref="HideTransition"/>. Assigning a value marks the set
|
||||
/// as custom so <see cref="HideTransition"/> no longer overwrites it.
|
||||
/// </summary>
|
||||
public ImplicitAnimationSet HideAnimations
|
||||
{
|
||||
get => _hideAnimations;
|
||||
set
|
||||
{
|
||||
_hideAnimations = value ?? new ImplicitAnimationSet();
|
||||
_hasCustomHideAnimations = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wires this surface to a hosting <see cref="TransparentWindow"/> so it
|
||||
/// animates itself in and out in response to the window's
|
||||
/// <see cref="TransparentWindow.Showing"/> / <see cref="TransparentWindow.Hiding"/>
|
||||
/// events. Call this once after the surface has been set as (or placed within)
|
||||
/// the window's content.
|
||||
/// </summary>
|
||||
/// <param name="host">The window whose show/hide transitions drive this surface.</param>
|
||||
public void SubscribeTo(TransparentWindow host)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(host);
|
||||
|
||||
host.Showing += OnHostShowing;
|
||||
host.Hiding += OnHostHiding;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the surface to its hidden pose and flips it to
|
||||
/// <see cref="Visibility.Visible"/> so <see cref="ShowAnimations"/> plays,
|
||||
/// using <paramref name="transition"/> as the show transition.
|
||||
/// </summary>
|
||||
/// <param name="transition">The transition to play when showing.</param>
|
||||
public void Show(Transition transition)
|
||||
{
|
||||
ShowTransition = transition;
|
||||
Show();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the surface to its hidden pose and flips it to
|
||||
/// <see cref="Visibility.Visible"/> so <see cref="ShowAnimations"/> plays.
|
||||
/// Repeated calls re-trigger the show animation cleanly and cancel any
|
||||
/// pending <see cref="HideCompleted"/> notification.
|
||||
/// </summary>
|
||||
public void Show()
|
||||
{
|
||||
_hideCompletedTimer.Stop();
|
||||
|
||||
// If a hide from a previous cycle is still in flight, abandon it: drop its
|
||||
// pending HideCompleted handler so the outstanding deferral is never
|
||||
// completed. We are showing again, so the host must keep the window
|
||||
// visible instead of later hiding it for this interrupted cycle.
|
||||
_abandonPendingHide?.Invoke();
|
||||
_abandonPendingHide = null;
|
||||
|
||||
// Attach the show animation and detach any hide animation: when Show() is
|
||||
// called while the surface is still visible, the Collapsed -> Visible
|
||||
// restart below would otherwise play the hide animation (a fade/scale out)
|
||||
// immediately before the show, producing a visible flash. The real hide
|
||||
// animation is re-attached just-in-time in Hide().
|
||||
Implicit.SetShowAnimations(this, _showAnimations);
|
||||
Implicit.SetHideAnimations(this, _noAnimations);
|
||||
|
||||
// Reset to the hidden pose so the show animation always animates from the
|
||||
// configured starting frame.
|
||||
Visibility = Visibility.Collapsed;
|
||||
Visibility = Visibility.Visible;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Flips the surface to <see cref="Visibility.Collapsed"/> so
|
||||
/// <see cref="HideAnimations"/> plays, then raises <see cref="HideCompleted"/>
|
||||
/// once the longest animation in <see cref="HideAnimations"/> (delay +
|
||||
/// duration) has completed.
|
||||
/// </summary>
|
||||
public void Hide()
|
||||
{
|
||||
// Attach the hide animation just before collapsing (Show() detaches it to
|
||||
// avoid a flash when re-showing an already-visible surface).
|
||||
Implicit.SetHideAnimations(this, _hideAnimations);
|
||||
|
||||
Visibility = Visibility.Collapsed;
|
||||
|
||||
_hideCompletedTimer.Debounce(
|
||||
() => HideCompleted?.Invoke(this, EventArgs.Empty),
|
||||
interval: GetAnimationSetTotalDuration(_hideAnimations),
|
||||
immediate: false);
|
||||
}
|
||||
|
||||
private static void OnTransitionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
((TransientSurface)d).RebuildDefaultAnimations();
|
||||
}
|
||||
|
||||
private static TimeSpan GetAnimationSetTotalDuration(ImplicitAnimationSet set)
|
||||
{
|
||||
TimeSpan longest = TimeSpan.Zero;
|
||||
foreach (var animation in set)
|
||||
{
|
||||
if (animation is Animation anim)
|
||||
{
|
||||
var total = (anim.Delay ?? TimeSpan.Zero) + (anim.Duration ?? TimeSpan.Zero);
|
||||
if (total > longest)
|
||||
{
|
||||
longest = total;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return longest;
|
||||
}
|
||||
|
||||
private static (string? ShowFrom, string? HideTo) GetSlideOffsets(Transition transition) => transition switch
|
||||
{
|
||||
Transition.Bottom => ($"0,{SlideInOffset},{ShadowDepth}", $"0,{SlideOutOffset},{ShadowDepth}"),
|
||||
Transition.Top => ($"0,{-SlideInOffset},{ShadowDepth}", $"0,{-SlideOutOffset},{ShadowDepth}"),
|
||||
Transition.Left => ($"{-SlideInOffset},0,{ShadowDepth}", $"{-SlideOutOffset},0,{ShadowDepth}"),
|
||||
Transition.Right => ($"{SlideInOffset},0,{ShadowDepth}", $"{SlideOutOffset},0,{ShadowDepth}"),
|
||||
_ => (null, null),
|
||||
};
|
||||
|
||||
private void OnHostShowing(TransparentWindow sender, ShowingEventArgs e)
|
||||
{
|
||||
if (e.Transition is Transition transition)
|
||||
{
|
||||
Show(transition);
|
||||
}
|
||||
else
|
||||
{
|
||||
Show();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnHostHiding(TransparentWindow sender, HidingEventArgs e)
|
||||
{
|
||||
// Take a deferral so the host keeps its window visible until our
|
||||
// out-animation has finished, then complete it from HideCompleted.
|
||||
var deferral = e.GetDeferral();
|
||||
|
||||
void OnHideCompleted(object? s, EventArgs args)
|
||||
{
|
||||
HideCompleted -= OnHideCompleted;
|
||||
_abandonPendingHide = null;
|
||||
deferral.Complete();
|
||||
}
|
||||
|
||||
// Let a subsequent Show() cancel this hide cleanly: unsubscribe the
|
||||
// handler so the deferral is never completed (the window stays visible)
|
||||
// rather than firing AppWindow.Hide for an interrupted cycle.
|
||||
_abandonPendingHide = () => HideCompleted -= OnHideCompleted;
|
||||
|
||||
HideCompleted += OnHideCompleted;
|
||||
Hide();
|
||||
}
|
||||
|
||||
private void RebuildDefaultAnimations()
|
||||
{
|
||||
if (!_hasCustomShowAnimations)
|
||||
{
|
||||
_showAnimations = BuildShowAnimations(ShowTransition);
|
||||
}
|
||||
|
||||
if (!_hasCustomHideAnimations)
|
||||
{
|
||||
_hideAnimations = BuildHideAnimations(HideTransition);
|
||||
}
|
||||
}
|
||||
|
||||
private void PinScaleCenter()
|
||||
{
|
||||
var visual = ElementCompositionPreview.GetElementVisual(this);
|
||||
var center = visual.Compositor.CreateExpressionAnimation("Vector3(this.Target.Size.X * 0.5, this.Target.Size.Y * 0.5, 0)");
|
||||
visual.StartAnimation("CenterPoint", center);
|
||||
}
|
||||
|
||||
private static ImplicitAnimationSet BuildShowAnimations(Transition transition)
|
||||
{
|
||||
var animations = new ImplicitAnimationSet();
|
||||
|
||||
if (transition == Transition.None)
|
||||
{
|
||||
// No animation at all.
|
||||
return animations;
|
||||
}
|
||||
|
||||
if (transition == Transition.Pop)
|
||||
{
|
||||
animations.Add(new OpacityAnimation
|
||||
{
|
||||
From = 0,
|
||||
To = 1.0,
|
||||
Duration = TimeSpan.FromMilliseconds(PopFadeShowMs),
|
||||
EasingMode = Microsoft.UI.Xaml.Media.Animation.EasingMode.EaseOut,
|
||||
EasingType = EasingType.Cubic,
|
||||
});
|
||||
|
||||
animations.Add(new ScaleAnimation
|
||||
{
|
||||
From = $"{PopScaleFrom},{PopScaleFrom},1",
|
||||
To = "1,1,1",
|
||||
Duration = TimeSpan.FromMilliseconds(PopScaleShowMs),
|
||||
EasingMode = Microsoft.UI.Xaml.Media.Animation.EasingMode.EaseOut,
|
||||
EasingType = EasingType.Cubic,
|
||||
});
|
||||
|
||||
return animations;
|
||||
}
|
||||
|
||||
var (slideFrom, _) = GetSlideOffsets(transition);
|
||||
|
||||
animations.Add(new OpacityAnimation
|
||||
{
|
||||
From = 0,
|
||||
To = 1.0,
|
||||
Duration = TimeSpan.FromMilliseconds(200),
|
||||
EasingMode = Microsoft.UI.Xaml.Media.Animation.EasingMode.EaseOut,
|
||||
EasingType = EasingType.Cubic,
|
||||
});
|
||||
|
||||
animations.Add(new TranslationAnimation
|
||||
{
|
||||
From = slideFrom,
|
||||
To = $"0,0,{ShadowDepth}",
|
||||
Duration = TimeSpan.FromMilliseconds(250),
|
||||
EasingMode = Microsoft.UI.Xaml.Media.Animation.EasingMode.EaseOut,
|
||||
EasingType = EasingType.Cubic,
|
||||
});
|
||||
|
||||
return animations;
|
||||
}
|
||||
|
||||
private static ImplicitAnimationSet BuildHideAnimations(Transition transition)
|
||||
{
|
||||
var animations = new ImplicitAnimationSet();
|
||||
|
||||
if (transition == Transition.None)
|
||||
{
|
||||
// No animation at all.
|
||||
return animations;
|
||||
}
|
||||
|
||||
if (transition == Transition.Pop)
|
||||
{
|
||||
animations.Add(new OpacityAnimation
|
||||
{
|
||||
From = 1.0,
|
||||
To = 0,
|
||||
Duration = TimeSpan.FromMilliseconds(PopFadeHideMs),
|
||||
EasingMode = Microsoft.UI.Xaml.Media.Animation.EasingMode.EaseIn,
|
||||
EasingType = EasingType.Cubic,
|
||||
});
|
||||
|
||||
animations.Add(new ScaleAnimation
|
||||
{
|
||||
From = "1,1,1",
|
||||
To = $"{PopScaleFrom},{PopScaleFrom},1",
|
||||
Duration = TimeSpan.FromMilliseconds(PopScaleHideMs),
|
||||
EasingMode = Microsoft.UI.Xaml.Media.Animation.EasingMode.EaseIn,
|
||||
EasingType = EasingType.Cubic,
|
||||
});
|
||||
|
||||
return animations;
|
||||
}
|
||||
|
||||
var (_, slideTo) = GetSlideOffsets(transition);
|
||||
|
||||
animations.Add(new OpacityAnimation
|
||||
{
|
||||
From = 1.0,
|
||||
To = 0,
|
||||
Duration = TimeSpan.FromMilliseconds(180),
|
||||
EasingMode = Microsoft.UI.Xaml.Media.Animation.EasingMode.EaseIn,
|
||||
EasingType = EasingType.Cubic,
|
||||
});
|
||||
|
||||
animations.Add(new TranslationAnimation
|
||||
{
|
||||
From = $"0,0,{ShadowDepth}",
|
||||
To = slideTo,
|
||||
Duration = TimeSpan.FromMilliseconds(180),
|
||||
EasingMode = Microsoft.UI.Xaml.Media.Animation.EasingMode.EaseIn,
|
||||
EasingType = EasingType.Cubic,
|
||||
});
|
||||
|
||||
return animations;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.PowerToys.Common.UI.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// A show or hide transition a surface (e.g. <see cref="TransientSurface"/>)
|
||||
/// plays when it is shown or hidden. The directional values describe an edge —
|
||||
/// interpreted as <em>in from</em> that edge on show and <em>out toward</em> it
|
||||
/// on hide — while <see cref="None"/> and <see cref="Pop"/> are non-directional.
|
||||
/// </summary>
|
||||
public enum Transition
|
||||
{
|
||||
/// <summary>No animation; the surface appears and disappears instantly.</summary>
|
||||
None,
|
||||
|
||||
/// <summary>Slide from the left edge (in from on show, out toward on hide).</summary>
|
||||
Left,
|
||||
|
||||
/// <summary>Slide from the top edge (in from on show, out toward on hide).</summary>
|
||||
Top,
|
||||
|
||||
/// <summary>Slide from the right edge (in from on show, out toward on hide).</summary>
|
||||
Right,
|
||||
|
||||
/// <summary>Slide from the bottom edge (in from on show, out toward on hide).</summary>
|
||||
Bottom,
|
||||
|
||||
/// <summary>
|
||||
/// A subtle "pop": a quick fade combined with a small scale between 96% and
|
||||
/// 100% from the surface's center. Stays in place — no slide.
|
||||
/// </summary>
|
||||
Pop,
|
||||
}
|
||||
@@ -1,27 +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 Microsoft.UI.Xaml.Controls;
|
||||
|
||||
namespace Microsoft.PowerToys.Common.UI.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// A floating "card" surface for transient PowerToys overlays (toasts,
|
||||
/// banners, indicators). Provides the PowerToys-standard chrome — 1 px
|
||||
/// border in <c>SurfaceStrokeColorDefaultBrush</c>, 8 px corner radius,
|
||||
/// a <c>ThemeShadow</c>, and an always-active desktop acrylic backdrop —
|
||||
/// via a default <see cref="Microsoft.UI.Xaml.Controls.ControlTemplate"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Lives inside a <see cref="Window.TransparentWindow"/>. Apps that want a
|
||||
/// different look supply their own <c>Style TargetType="TransparentCard"</c>
|
||||
/// in resources — the standard WinUI restyle path.
|
||||
/// </remarks>
|
||||
public sealed partial class TransparentCard : ContentControl
|
||||
{
|
||||
public TransparentCard()
|
||||
{
|
||||
DefaultStyleKey = typeof(TransparentCard);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,6 @@
|
||||
<ResourceDictionary Source="ms-appx:///PowerToys.Common.UI.Controls/Controls/KeyVisual/KeyCharPresenter.xaml" />
|
||||
<ResourceDictionary Source="ms-appx:///PowerToys.Common.UI.Controls/Controls/IsEnabledTextBlock/IsEnabledTextBlock.xaml" />
|
||||
<ResourceDictionary Source="ms-appx:///PowerToys.Common.UI.Controls/Controls/ShortcutWithTextLabelControl/ShortcutWithTextLabelControl.xaml" />
|
||||
<ResourceDictionary Source="ms-appx:///PowerToys.Common.UI.Controls/Controls/TransparentCard/TransparentCard.xaml" />
|
||||
<ResourceDictionary Source="ms-appx:///PowerToys.Common.UI.Controls/Controls/TransientSurface/TransientSurface.xaml" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
|
||||
@@ -9,7 +9,7 @@ using Microsoft.UI.Windowing;
|
||||
using Windows.Graphics;
|
||||
using WinUIEx;
|
||||
|
||||
namespace Microsoft.PowerToys.Common.UI.Controls.Flyout;
|
||||
namespace Microsoft.PowerToys.Common.UI.Controls.Window;
|
||||
|
||||
/// <summary>
|
||||
/// Shared helper for positioning and sizing flyout-style WinUI 3 windows
|
||||
@@ -187,16 +187,13 @@ public static partial class FlyoutWindowHelper
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Two-step move that avoids WM_DPICHANGED double-scaling. First teleports a 1×1
|
||||
/// window into the target display (which may trigger an auto-rescale, but on a 1×1
|
||||
/// rect the effect is invisible). Then sets the real position+size while the window
|
||||
/// is already on the target monitor — no DPI boundary crossing, so WinUI's auto
|
||||
/// handler doesn't fire and overwrite our computed rect.
|
||||
///
|
||||
/// Skips the teleport when the window is already on the target display, since there
|
||||
/// is no boundary to cross.
|
||||
/// Move and resize <paramref name="window"/> to <paramref name="finalRect"/> (absolute
|
||||
/// screen physical-pixel coordinates) on <paramref name="targetDisplay"/>. Performs a
|
||||
/// two-step move that avoids WM_DPICHANGED double-scaling: first a 1×1 teleport into the
|
||||
/// target display (invisible at that size), then the real position+size while the window
|
||||
/// is already on that monitor. Skips the teleport when already on the target display.
|
||||
/// </summary>
|
||||
private static void MoveAndResizeOnDisplay(WindowEx window, DisplayArea targetDisplay, RectInt32 finalRect)
|
||||
public static void MoveAndResizeOnDisplay(WindowEx window, DisplayArea targetDisplay, RectInt32 finalRect)
|
||||
{
|
||||
var currentDisplay = DisplayArea.GetFromWindowId(window.AppWindow.Id, DisplayAreaFallback.Nearest);
|
||||
bool needsTeleport = currentDisplay is null || currentDisplay.DisplayId.Value != targetDisplay.DisplayId.Value;
|
||||
@@ -1,290 +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;
|
||||
using System.Runtime.InteropServices;
|
||||
using CommunityToolkit.WinUI;
|
||||
using CommunityToolkit.WinUI.Animations;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Windowing;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Markup;
|
||||
using WinUIEx;
|
||||
|
||||
namespace Microsoft.PowerToys.Common.UI.Controls.Window;
|
||||
|
||||
/// <summary>
|
||||
/// Reusable transparent host window for transient overlays
|
||||
/// (toasts, banners, indicators) that should not steal foreground.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>The constructor applies all of the boilerplate that PowerToys overlays
|
||||
/// currently hand-roll:</para>
|
||||
/// <list type="bullet">
|
||||
/// <item>Strip the native frame and caption (<c>WS_THICKFRAME</c> etc.).</item>
|
||||
/// <item>Disable the Win11 1-pixel DWM border and corner rounding.</item>
|
||||
/// <item>Mark the window as a tool window so it stays out of the taskbar and Alt-Tab.</item>
|
||||
/// <item>Extend content into the title bar and collapse the title bar.</item>
|
||||
/// </list>
|
||||
/// <para>The visible chrome (acrylic + border + corner radius + shadow) lives
|
||||
/// in a <see cref="TransparentCard"/> that the constructor assigns to
|
||||
/// <see cref="Microsoft.UI.Xaml.Window.Content"/>. Consumers supply their own
|
||||
/// UI via <see cref="InnerContent"/> — which is the XAML default-content slot
|
||||
/// thanks to <see cref="ContentPropertyAttribute"/> — so a derived window can
|
||||
/// be written as <c><common:TransparentWindow><TextBlock/></common:TransparentWindow></c>.</para>
|
||||
/// <para>Transparency is achieved with a <see cref="TransparentTintBackdrop"/>
|
||||
/// system backdrop so the area outside the <see cref="TransparentCard"/> is
|
||||
/// fully see-through. That buffer area is NOT click-through, so consumers
|
||||
/// should keep it as small as possible (just enough to give the card's
|
||||
/// shadow + slide animation room to breathe — roughly 24 px on each side).</para>
|
||||
/// <para><see cref="Show"/> and <see cref="Hide"/> coordinate <c>SW_SHOWNA</c>
|
||||
/// (no-activate), the <see cref="Microsoft.UI.Xaml.UIElement.Visibility"/>
|
||||
/// toggle on the card, and a debounced
|
||||
/// <see cref="Microsoft.UI.Windowing.AppWindow.Hide"/> sized from the longest
|
||||
/// animation in <see cref="HideAnimations"/>. Animations target the card so
|
||||
/// the entire surface (border, acrylic, shadow, inner content) slides as one.</para>
|
||||
/// </remarks>
|
||||
[ContentProperty(Name = nameof(InnerContent))]
|
||||
public partial class TransparentWindow : WinUIEx.WindowEx
|
||||
{
|
||||
private const uint DwmwaColorNone = 0xFFFFFFFE;
|
||||
private const int DwmwaWindowCornerPreference = 33;
|
||||
private const int DwmwaBorderColor = 34;
|
||||
private const int DwmwcpDoNotRound = 1;
|
||||
|
||||
private const int GwlExStyle = -20;
|
||||
private const int WsExToolWindow = 0x00000080;
|
||||
|
||||
private const int SwShowNa = 8;
|
||||
|
||||
private readonly DispatcherQueueTimer _hideCloseTimer = DispatcherQueue.GetForCurrentThread().CreateTimer();
|
||||
private readonly nint _hwnd;
|
||||
private readonly TransparentCard _card;
|
||||
|
||||
private ImplicitAnimationSet _showAnimations;
|
||||
private ImplicitAnimationSet _hideAnimations;
|
||||
|
||||
public TransparentWindow()
|
||||
{
|
||||
AppWindow.Hide();
|
||||
ExtendsContentIntoTitleBar = true;
|
||||
AppWindow.TitleBar.PreferredHeightOption = TitleBarHeightOption.Collapsed;
|
||||
|
||||
_hwnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
|
||||
|
||||
HwndExtensions.ToggleWindowStyle(_hwnd, false, WindowStyle.TiledWindow);
|
||||
|
||||
unsafe
|
||||
{
|
||||
uint borderColor = DwmwaColorNone;
|
||||
_ = DwmSetWindowAttribute(_hwnd, DwmwaBorderColor, &borderColor, sizeof(uint));
|
||||
|
||||
int cornerPref = DwmwcpDoNotRound;
|
||||
_ = DwmSetWindowAttribute(_hwnd, DwmwaWindowCornerPreference, &cornerPref, sizeof(int));
|
||||
}
|
||||
|
||||
ApplyExStyleBit(WsExToolWindow, true);
|
||||
|
||||
_showAnimations = BuildDefaultShowAnimations();
|
||||
_hideAnimations = BuildDefaultHideAnimations();
|
||||
|
||||
_card = new TransparentCard();
|
||||
Content = _card;
|
||||
|
||||
SystemBackdrop = new TransparentTintBackdrop();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="TransparentCard"/> that provides the window's
|
||||
/// visible chrome (acrylic + border + shadow). Consumers can configure
|
||||
/// its layout (e.g. <c>HorizontalAlignment</c>, <c>VerticalAlignment</c>,
|
||||
/// <c>MaxWidth</c>, <c>Margin</c>) to position the card inside the
|
||||
/// window, or apply a custom <c>Style</c> to change its look.
|
||||
/// </summary>
|
||||
public TransparentCard Card => _card;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the visual hosted inside the window's
|
||||
/// <see cref="TransparentCard"/>. This is the XAML default-content slot:
|
||||
/// child elements declared between the opening and closing
|
||||
/// <c>TransparentWindow</c> tags in a derived .xaml are routed here.
|
||||
/// </summary>
|
||||
public object? InnerContent
|
||||
{
|
||||
get => _card.Content;
|
||||
set => _card.Content = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the animations played against
|
||||
/// <see cref="Microsoft.UI.Xaml.Window.Content"/> when <see cref="Show"/>
|
||||
/// flips it to <see cref="Visibility.Visible"/>. Defaults to a 200 ms
|
||||
/// fade-in plus a 250 ms slide-up of 24 px.
|
||||
/// </summary>
|
||||
public ImplicitAnimationSet ShowAnimations
|
||||
{
|
||||
get => _showAnimations;
|
||||
set => _showAnimations = value ?? new ImplicitAnimationSet();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the animations played against
|
||||
/// <see cref="Microsoft.UI.Xaml.Window.Content"/> when <see cref="Hide"/>
|
||||
/// flips it to <see cref="Visibility.Collapsed"/>. Defaults to a 180 ms
|
||||
/// fade-out plus a 180 ms slide-down of 12 px.
|
||||
/// </summary>
|
||||
public ImplicitAnimationSet HideAnimations
|
||||
{
|
||||
get => _hideAnimations;
|
||||
set => _hideAnimations = value ?? new ImplicitAnimationSet();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shows the window without activation (<c>SW_SHOWNA</c>) and flips
|
||||
/// <see cref="Microsoft.UI.Xaml.Window.Content"/> to
|
||||
/// <see cref="Visibility.Visible"/> so <see cref="ShowAnimations"/> plays.
|
||||
/// Repeated calls reset the content to its hidden pose first so the show
|
||||
/// animation re-triggers cleanly. Any pending hide is cancelled.
|
||||
/// </summary>
|
||||
public void Show()
|
||||
{
|
||||
DispatcherQueue.TryEnqueue(
|
||||
DispatcherQueuePriority.Low,
|
||||
() =>
|
||||
{
|
||||
_hideCloseTimer.Stop();
|
||||
|
||||
if (Content is UIElement content)
|
||||
{
|
||||
// Re-apply each call so swapping animation collections at
|
||||
// runtime takes effect on the next show/hide cycle.
|
||||
Implicit.SetShowAnimations(content, _showAnimations);
|
||||
Implicit.SetHideAnimations(content, _hideAnimations);
|
||||
|
||||
// Reset to the hidden pose so the show animation always
|
||||
// animates from the configured starting frame.
|
||||
content.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
|
||||
_ = ShowWindow(_hwnd, SwShowNa);
|
||||
|
||||
if (Content is UIElement c2)
|
||||
{
|
||||
c2.Visibility = Visibility.Visible;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Flips <see cref="Microsoft.UI.Xaml.Window.Content"/> to
|
||||
/// <see cref="Visibility.Collapsed"/> so <see cref="HideAnimations"/>
|
||||
/// plays, then hides the underlying
|
||||
/// <see cref="Microsoft.UI.Windowing.AppWindow"/> once the longest
|
||||
/// animation in <see cref="HideAnimations"/> (delay + duration) has
|
||||
/// completed.
|
||||
/// </summary>
|
||||
public void Hide()
|
||||
{
|
||||
DispatcherQueue.TryEnqueue(
|
||||
DispatcherQueuePriority.Low,
|
||||
() =>
|
||||
{
|
||||
if (Content is UIElement content)
|
||||
{
|
||||
content.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
|
||||
_hideCloseTimer.Debounce(
|
||||
AppWindow.Hide,
|
||||
interval: GetAnimationSetTotalDuration(_hideAnimations),
|
||||
immediate: false);
|
||||
});
|
||||
}
|
||||
|
||||
private static TimeSpan GetAnimationSetTotalDuration(ImplicitAnimationSet set)
|
||||
{
|
||||
TimeSpan longest = TimeSpan.Zero;
|
||||
foreach (var animation in set)
|
||||
{
|
||||
if (animation is Animation anim)
|
||||
{
|
||||
var total = (anim.Delay ?? TimeSpan.Zero) + (anim.Duration ?? TimeSpan.Zero);
|
||||
if (total > longest)
|
||||
{
|
||||
longest = total;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return longest;
|
||||
}
|
||||
|
||||
private void ApplyExStyleBit(int bit, bool set)
|
||||
{
|
||||
if (_hwnd == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
nint exStyle = GetWindowLongPtr(_hwnd, GwlExStyle);
|
||||
nint updated = set ? exStyle | bit : exStyle & ~(nint)bit;
|
||||
if (updated != exStyle)
|
||||
{
|
||||
_ = SetWindowLongPtr(_hwnd, GwlExStyle, updated);
|
||||
}
|
||||
}
|
||||
|
||||
private static ImplicitAnimationSet BuildDefaultShowAnimations() => new()
|
||||
{
|
||||
new OpacityAnimation
|
||||
{
|
||||
From = 0,
|
||||
To = 1.0,
|
||||
Duration = TimeSpan.FromMilliseconds(200),
|
||||
EasingMode = Microsoft.UI.Xaml.Media.Animation.EasingMode.EaseOut,
|
||||
EasingType = EasingType.Cubic,
|
||||
},
|
||||
new TranslationAnimation
|
||||
{
|
||||
From = "0,24,32",
|
||||
To = "0,0,32",
|
||||
Duration = TimeSpan.FromMilliseconds(250),
|
||||
EasingMode = Microsoft.UI.Xaml.Media.Animation.EasingMode.EaseOut,
|
||||
EasingType = EasingType.Cubic,
|
||||
},
|
||||
};
|
||||
|
||||
private static ImplicitAnimationSet BuildDefaultHideAnimations() => new()
|
||||
{
|
||||
new OpacityAnimation
|
||||
{
|
||||
From = 1.0,
|
||||
To = 0,
|
||||
Duration = TimeSpan.FromMilliseconds(180),
|
||||
EasingMode = Microsoft.UI.Xaml.Media.Animation.EasingMode.EaseIn,
|
||||
EasingType = EasingType.Cubic,
|
||||
},
|
||||
new TranslationAnimation
|
||||
{
|
||||
From = "0,0,32",
|
||||
To = "0,12,32",
|
||||
Duration = TimeSpan.FromMilliseconds(180),
|
||||
EasingMode = Microsoft.UI.Xaml.Media.Animation.EasingMode.EaseIn,
|
||||
EasingType = EasingType.Cubic,
|
||||
},
|
||||
};
|
||||
|
||||
[LibraryImport("user32.dll", EntryPoint = "GetWindowLongPtrW")]
|
||||
private static partial nint GetWindowLongPtr(nint hWnd, int nIndex);
|
||||
|
||||
[LibraryImport("user32.dll", EntryPoint = "SetWindowLongPtrW")]
|
||||
private static partial nint SetWindowLongPtr(nint hWnd, int nIndex, nint dwNewLong);
|
||||
|
||||
[LibraryImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static partial bool ShowWindow(nint hWnd, int nCmdShow);
|
||||
|
||||
[LibraryImport("dwmapi.dll")]
|
||||
private static unsafe partial int DwmSetWindowAttribute(nint hwnd, int dwAttribute, void* pvAttribute, int cbAttribute);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using Deferral = global::Windows.Foundation.Deferral;
|
||||
|
||||
namespace Microsoft.PowerToys.Common.UI.Controls.Window;
|
||||
|
||||
/// <summary>
|
||||
/// Data for <see cref="TransparentWindow.Hiding"/>. Supports deferrals so an
|
||||
/// animated surface can keep the window visible until its out-animation has
|
||||
/// finished. If no handler takes a deferral, the window hides immediately.
|
||||
/// </summary>
|
||||
public sealed class HidingEventArgs : EventArgs
|
||||
{
|
||||
private int _outstanding;
|
||||
private bool _raised;
|
||||
private Action? _continuation;
|
||||
|
||||
/// <summary>
|
||||
/// Requests that the window stay visible until the returned deferral is
|
||||
/// completed. Call <see cref="Deferral.Complete"/> once the out-animation
|
||||
/// has finished.
|
||||
/// </summary>
|
||||
/// <returns>A deferral that must be completed to allow the window to hide.</returns>
|
||||
public Deferral GetDeferral()
|
||||
{
|
||||
Interlocked.Increment(ref _outstanding);
|
||||
return new Deferral(OnDeferralCompleted);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called by the window after raising the event to register what should run
|
||||
/// once every outstanding deferral has completed (or immediately if none
|
||||
/// were taken).
|
||||
/// </summary>
|
||||
internal void RunWhenComplete(Action continuation)
|
||||
{
|
||||
_continuation = continuation;
|
||||
_raised = true;
|
||||
TryComplete();
|
||||
}
|
||||
|
||||
private void OnDeferralCompleted()
|
||||
{
|
||||
Interlocked.Decrement(ref _outstanding);
|
||||
TryComplete();
|
||||
}
|
||||
|
||||
private void TryComplete()
|
||||
{
|
||||
if (_raised && Volatile.Read(ref _outstanding) == 0)
|
||||
{
|
||||
var continuation = _continuation;
|
||||
_continuation = null;
|
||||
continuation?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.PowerToys.Common.UI.Controls.Window;
|
||||
|
||||
/// <summary>
|
||||
/// Data for <see cref="TransparentWindow.Showing"/>. Carries the transition the
|
||||
/// content should play, or <see langword="null"/> to let the content use its own
|
||||
/// configured show transition.
|
||||
/// </summary>
|
||||
public sealed class ShowingEventArgs : EventArgs
|
||||
{
|
||||
public ShowingEventArgs(Transition? transition)
|
||||
{
|
||||
Transition = transition;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the transition the content should play, or <see langword="null"/> to
|
||||
/// use the content's own configured show transition.
|
||||
/// </summary>
|
||||
public Transition? Transition { get; }
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Windowing;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
using Windows.Foundation;
|
||||
using WinUIEx;
|
||||
|
||||
namespace Microsoft.PowerToys.Common.UI.Controls.Window;
|
||||
|
||||
/// <summary>
|
||||
/// Reusable transparent host window for transient overlays
|
||||
/// (toasts, banners, indicators) that should not steal foreground.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>The constructor applies all of the boilerplate that PowerToys overlays
|
||||
/// currently hand-roll:</para>
|
||||
/// <list type="bullet">
|
||||
/// <item>Strip the native frame and caption (<c>WS_THICKFRAME</c> etc.).</item>
|
||||
/// <item>Disable the Win11 1-pixel DWM border and corner rounding.</item>
|
||||
/// <item>Mark the window as a tool window so it stays out of the taskbar and Alt-Tab.</item>
|
||||
/// <item>Extend content into the title bar and collapse the title bar.</item>
|
||||
/// <item>Apply a <see cref="TransparentTintBackdrop"/> so the HWND is fully
|
||||
/// see-through and the visible chrome can be drawn by the content.</item>
|
||||
/// </list>
|
||||
/// <para>This window is intentionally animation-agnostic: it does not own any
|
||||
/// chrome or motion. Consumers supply their own content (typically a
|
||||
/// <see cref="TransientSurface"/>) which draws the acrylic, border, corners and
|
||||
/// shadow, and animates itself. <see cref="Show()"/> and <see cref="Hide"/>
|
||||
/// coordinate <c>SW_SHOWNA</c> (no-activate) with the
|
||||
/// <see cref="Showing"/> / <see cref="Hiding"/> events: a content surface
|
||||
/// subscribes to those (e.g. via <see cref="TransientSurface.SubscribeTo"/>)
|
||||
/// and plays its in/out animation. The <see cref="Hiding"/> event supports
|
||||
/// deferrals, so the underlying
|
||||
/// <see cref="Microsoft.UI.Windowing.AppWindow.Hide"/> is delayed until the
|
||||
/// content has finished animating out. With no listener the window simply shows
|
||||
/// or hides immediately.</para>
|
||||
/// <para><b>Multiple surfaces.</b> More than one <see cref="TransientSurface"/>
|
||||
/// may host on the same window by each calling
|
||||
/// <see cref="TransientSurface.SubscribeTo"/>. The <see cref="Showing"/> and
|
||||
/// <see cref="Hiding"/> events are simply raised for every subscriber, and
|
||||
/// because <see cref="HidingEventArgs"/> aggregates deferrals the underlying
|
||||
/// window is hidden only after <em>all</em> surfaces have finished animating
|
||||
/// out. To let each surface play its own distinct transition, call the
|
||||
/// parameterless <see cref="Show()"/> (so every surface uses its configured
|
||||
/// <c>ShowTransition</c>/<c>HideTransition</c>); the <see cref="Show(Transition)"/>
|
||||
/// overload instead broadcasts a single transition to all surfaces. Sizing the
|
||||
/// window and positioning each surface within it remain the consumer's
|
||||
/// responsibility (this window owns no layout).</para>
|
||||
/// </remarks>
|
||||
public partial class TransparentWindow : WinUIEx.WindowEx
|
||||
{
|
||||
private const uint DwmwaColorNone = 0xFFFFFFFE;
|
||||
private const int DwmwaWindowCornerPreference = 33;
|
||||
private const int DwmwaBorderColor = 34;
|
||||
private const int DwmwcpDoNotRound = 1;
|
||||
|
||||
private const int GwlExStyle = -20;
|
||||
private const int WsExToolWindow = 0x00000080;
|
||||
|
||||
private const int SwShowNa = 8;
|
||||
|
||||
private readonly nint _hwnd;
|
||||
|
||||
private bool _inputHooked;
|
||||
private bool _seenActivated;
|
||||
|
||||
public TransparentWindow()
|
||||
{
|
||||
AppWindow.Hide();
|
||||
ExtendsContentIntoTitleBar = true;
|
||||
AppWindow.TitleBar.PreferredHeightOption = TitleBarHeightOption.Collapsed;
|
||||
|
||||
_hwnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
|
||||
|
||||
HwndExtensions.ToggleWindowStyle(_hwnd, false, WindowStyle.TiledWindow);
|
||||
|
||||
unsafe
|
||||
{
|
||||
uint borderColor = DwmwaColorNone;
|
||||
_ = DwmSetWindowAttribute(_hwnd, DwmwaBorderColor, &borderColor, sizeof(uint));
|
||||
|
||||
int cornerPref = DwmwcpDoNotRound;
|
||||
_ = DwmSetWindowAttribute(_hwnd, DwmwaWindowCornerPreference, &cornerPref, sizeof(int));
|
||||
}
|
||||
|
||||
ApplyExStyleBit(WsExToolWindow, true);
|
||||
|
||||
SystemBackdrop = new TransparentTintBackdrop();
|
||||
|
||||
Activated += OnActivatedForDismiss;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether pressing <c>Esc</c> while the
|
||||
/// window content has keyboard focus dismisses the window (<see cref="Hide"/>).
|
||||
/// Defaults to <see langword="false"/>. The window is shown without
|
||||
/// activation, so the consumer must activate it for its content to receive
|
||||
/// keyboard input.
|
||||
/// </summary>
|
||||
public bool DismissOnEscape { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the window dismisses itself
|
||||
/// (<see cref="Hide"/>) when it loses focus (is deactivated), i.e. light
|
||||
/// dismiss. Defaults to <see langword="false"/>. Only takes effect after the
|
||||
/// window has been activated at least once since the last <see cref="Show()"/>,
|
||||
/// so the transient deactivation that can occur during the show sequence does
|
||||
/// not dismiss it prematurely. The window is shown without activation, so the
|
||||
/// consumer must activate it for this to apply.
|
||||
/// </summary>
|
||||
public bool DismissOnFocusLost { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Raised (without activation) when <see cref="Show()"/> makes the window
|
||||
/// visible. A content surface subscribes to this to play its in-animation,
|
||||
/// using <see cref="ShowingEventArgs.Transition"/>.
|
||||
/// </summary>
|
||||
public event TypedEventHandler<TransparentWindow, ShowingEventArgs>? Showing;
|
||||
|
||||
/// <summary>
|
||||
/// Raised when <see cref="Hide"/> begins dismissing the window. A content
|
||||
/// surface subscribes to this to play its out-animation, taking a deferral
|
||||
/// (<see cref="HidingEventArgs.GetDeferral"/>) so the underlying window stays
|
||||
/// visible until the animation completes.
|
||||
/// </summary>
|
||||
public event TypedEventHandler<TransparentWindow, HidingEventArgs>? Hiding;
|
||||
|
||||
/// <summary>
|
||||
/// Shows the window without activation (<c>SW_SHOWNA</c>) and raises
|
||||
/// <see cref="Showing"/> without a transition, so subscribed content animates
|
||||
/// in using its own configured show transition.
|
||||
/// </summary>
|
||||
public void Show() => RaiseShow(null);
|
||||
|
||||
/// <summary>
|
||||
/// Shows the window without activation (<c>SW_SHOWNA</c>) and raises
|
||||
/// <see cref="Showing"/> so subscribed content animates in using
|
||||
/// <paramref name="transition"/>, overriding its configured show transition.
|
||||
/// </summary>
|
||||
/// <param name="transition">The transition the content should play.</param>
|
||||
public void Show(Transition transition) => RaiseShow(transition);
|
||||
|
||||
private void RaiseShow(Transition? transition)
|
||||
{
|
||||
DispatcherQueue.TryEnqueue(
|
||||
DispatcherQueuePriority.Low,
|
||||
() =>
|
||||
{
|
||||
_seenActivated = false;
|
||||
EnsureInputHooks();
|
||||
_ = ShowWindow(_hwnd, SwShowNa);
|
||||
Showing?.Invoke(this, new ShowingEventArgs(transition));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raises <see cref="Hiding"/> so subscribed content animates out, then hides
|
||||
/// the underlying <see cref="Microsoft.UI.Windowing.AppWindow"/> once every
|
||||
/// deferral taken by a handler has completed (immediately if none were taken).
|
||||
/// </summary>
|
||||
public void Hide()
|
||||
{
|
||||
DispatcherQueue.TryEnqueue(
|
||||
DispatcherQueuePriority.Low,
|
||||
() =>
|
||||
{
|
||||
var args = new HidingEventArgs();
|
||||
Hiding?.Invoke(this, args);
|
||||
args.RunWhenComplete(AppWindow.Hide);
|
||||
});
|
||||
}
|
||||
|
||||
private void OnActivatedForDismiss(object sender, WindowActivatedEventArgs args)
|
||||
{
|
||||
if (args.WindowActivationState == WindowActivationState.Deactivated)
|
||||
{
|
||||
if (DismissOnFocusLost && _seenActivated)
|
||||
{
|
||||
Hide();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
_seenActivated = true;
|
||||
}
|
||||
|
||||
private void EnsureInputHooks()
|
||||
{
|
||||
if (_inputHooked || Content is not UIElement element)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
element.KeyDown += OnContentKeyDown;
|
||||
_inputHooked = true;
|
||||
}
|
||||
|
||||
private void OnContentKeyDown(object sender, KeyRoutedEventArgs e)
|
||||
{
|
||||
if (DismissOnEscape && e.Key == global::Windows.System.VirtualKey.Escape)
|
||||
{
|
||||
e.Handled = true;
|
||||
Hide();
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyExStyleBit(int bit, bool set)
|
||||
{
|
||||
if (_hwnd == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
nint exStyle = GetWindowLongPtr(_hwnd, GwlExStyle);
|
||||
nint updated = set ? exStyle | bit : exStyle & ~(nint)bit;
|
||||
if (updated != exStyle)
|
||||
{
|
||||
_ = SetWindowLongPtr(_hwnd, GwlExStyle, updated);
|
||||
}
|
||||
}
|
||||
|
||||
[LibraryImport("user32.dll", EntryPoint = "GetWindowLongPtrW")]
|
||||
private static partial nint GetWindowLongPtr(nint hWnd, int nIndex);
|
||||
|
||||
[LibraryImport("user32.dll", EntryPoint = "SetWindowLongPtrW")]
|
||||
private static partial nint SetWindowLongPtr(nint hWnd, int nIndex, nint dwNewLong);
|
||||
|
||||
[LibraryImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static partial bool ShowWindow(nint hWnd, int nCmdShow);
|
||||
|
||||
[LibraryImport("dwmapi.dll")]
|
||||
private static unsafe partial int DwmSetWindowAttribute(nint hwnd, int dwAttribute, void* pvAttribute, int cbAttribute);
|
||||
}
|
||||
@@ -32,6 +32,17 @@ namespace EnvironmentVariables
|
||||
var loader = ResourceLoaderInstance.ResourceLoader;
|
||||
var title = App.GetService<IElevationHelper>().IsElevated ? loader.GetString("WindowAdminTitle") : loader.GetString("WindowTitle");
|
||||
|
||||
// The WinUI TitleBar control reads the owning window's title (AppWindow.Title) during a
|
||||
// deferred layout pass. If the native window title is empty at that instant, the windowing
|
||||
// layer can fault while resolving it and terminate the process. ResourceLoader.GetString
|
||||
// returns an empty string when the resource map can't be resolved at runtime, which would
|
||||
// leave the title empty here, so fall back to a non-empty product name to keep the native
|
||||
// window title populated.
|
||||
if (string.IsNullOrEmpty(title))
|
||||
{
|
||||
title = "Environment Variables";
|
||||
}
|
||||
|
||||
Title = title;
|
||||
titleBar.Title = title;
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 433 B After Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 433 B After Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 328 B After Width: | Height: | Size: 2.3 KiB |
@@ -25,6 +25,15 @@ namespace FileLocksmithUI
|
||||
|
||||
var loader = ResourceLoaderInstance.ResourceLoader;
|
||||
var title = isElevated ? loader.GetString("AppAdminTitle") : loader.GetString("AppTitle");
|
||||
|
||||
// Guard against an empty title: ResourceLoader.GetString returns "" when the resource
|
||||
// map can't be resolved, and an empty native window title can fault the WinUI TitleBar
|
||||
// control while it reads AppWindow.Title during a deferred layout pass.
|
||||
if (string.IsNullOrEmpty(title))
|
||||
{
|
||||
title = "File Locksmith";
|
||||
}
|
||||
|
||||
Title = title;
|
||||
titleBar.Title = title;
|
||||
}
|
||||
|
||||
@@ -373,6 +373,13 @@ static int g_overlayRenderedH = 0;
|
||||
// Always On Top (WindowCornerUtils::CornersRadius).
|
||||
static int CornerRadiusForWindow(HWND hwnd)
|
||||
{
|
||||
// Remote sessions draw square windows even on Win11, yet still report DWMWCP_DEFAULT. Match the
|
||||
// window: a remote session gets square (radius 0) so the overlay border doesn't round off the corner.
|
||||
if (GetSystemMetrics(SM_REMOTESESSION))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
int pref = 0; // DWMWCP_DEFAULT
|
||||
if (DwmGetWindowAttribute(hwnd, DWMWA_WINDOW_CORNER_PREFERENCE, &pref, sizeof(pref)) != S_OK)
|
||||
{
|
||||
|
||||
@@ -33,6 +33,15 @@ namespace Hosts
|
||||
var loader = new ResourceLoader("PowerToys.HostsUILib.pri", "PowerToys.HostsUILib/Resources");
|
||||
|
||||
var title = Host.GetService<IElevationHelper>().IsElevated ? loader.GetString("WindowAdminTitle") : loader.GetString("WindowTitle");
|
||||
|
||||
// Guard against an empty title: ResourceLoader.GetString returns "" when the resource
|
||||
// map can't be resolved, and an empty native window title can fault the WinUI TitleBar
|
||||
// control while it reads AppWindow.Title during a deferred layout pass.
|
||||
if (string.IsNullOrEmpty(title))
|
||||
{
|
||||
title = "Hosts File Editor";
|
||||
}
|
||||
|
||||
Title = title;
|
||||
titleBar.Title = title;
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
#include "trace.h"
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#ifdef COMPOSITION
|
||||
namespace winrt
|
||||
@@ -48,6 +50,18 @@ private:
|
||||
void ClearDrawingPoint();
|
||||
void ClearDrawing();
|
||||
void BringToFront();
|
||||
// Ripple mode: spawn the press/hold ring + glow at the click point and
|
||||
// continue the animation into a fade-out on release. The held ring may
|
||||
// optionally follow the cursor while held (gated by m_rippleShowDragTrail).
|
||||
void SpawnRippleHoldDot(MouseButton button);
|
||||
void FadeRippleHoldDot(MouseButton button);
|
||||
// Ripple mode: emit a single self-contained ripple (grow + fade) for a quick
|
||||
// click, independent of any held indicator.
|
||||
void EmitSingleRipple(MouseButton button);
|
||||
// Spotlight mode: pressed-state animation that shrinks the mask while
|
||||
// a mouse button is held and restores it on release.
|
||||
void SpotlightAnimatePress();
|
||||
void SpotlightAnimateRelease();
|
||||
HHOOK m_mouseHook = NULL;
|
||||
static LRESULT CALLBACK MouseHookProc(int nCode, WPARAM wParam, LPARAM lParam) noexcept;
|
||||
// Helpers for spotlight overlay
|
||||
@@ -71,6 +85,16 @@ private:
|
||||
winrt::CompositionSpriteShape m_leftPointer{ nullptr };
|
||||
winrt::CompositionSpriteShape m_rightPointer{ nullptr };
|
||||
winrt::CompositionSpriteShape m_alwaysPointer{ nullptr };
|
||||
// Ellipse geometries kept alongside the pointer shapes so press-down /
|
||||
// release animations can target the radius directly.
|
||||
winrt::CompositionEllipseGeometry m_leftGeometry{ nullptr };
|
||||
winrt::CompositionEllipseGeometry m_rightGeometry{ nullptr };
|
||||
// Ripple-mode held glow (the soft halo behind the ring) — paired with
|
||||
// m_left/rightPointer (which holds the ring shape) while a button is held.
|
||||
winrt::CompositionSpriteShape m_leftRippleGlow{ nullptr };
|
||||
winrt::CompositionSpriteShape m_rightRippleGlow{ nullptr };
|
||||
winrt::CompositionEllipseGeometry m_leftGlowGeometry{ nullptr };
|
||||
winrt::CompositionEllipseGeometry m_rightGlowGeometry{ nullptr };
|
||||
// Spotlight overlay (mask with soft feathered edge)
|
||||
winrt::SpriteVisual m_overlay{ nullptr };
|
||||
winrt::CompositionMaskBrush m_spotlightMask{ nullptr };
|
||||
@@ -84,9 +108,22 @@ private:
|
||||
bool m_rightPointerEnabled = true;
|
||||
bool m_alwaysPointerEnabled = true;
|
||||
bool m_spotlightMode = false;
|
||||
bool m_spotlightPressed = false;
|
||||
bool m_rippleMode = true;
|
||||
bool m_rippleShowDragTrail = MOUSE_HIGHLIGHTER_DEFAULT_RIPPLE_SHOW_DRAG_TRAIL;
|
||||
bool m_rippleShowReleasePulse = MOUSE_HIGHLIGHTER_DEFAULT_RIPPLE_SHOW_RELEASE_PULSE;
|
||||
float m_rippleSize = static_cast<float>(MOUSE_HIGHLIGHTER_DEFAULT_RIPPLE_SIZE);
|
||||
float m_rippleIntensity = static_cast<float>(MOUSE_HIGHLIGHTER_DEFAULT_RIPPLE_INTENSITY);
|
||||
int m_rippleDurationMs = MOUSE_HIGHLIGHTER_DEFAULT_RIPPLE_DURATION_MS;
|
||||
|
||||
bool m_leftButtonPressed = false;
|
||||
bool m_rightButtonPressed = false;
|
||||
// Pending hold-detection timers. A ripple "held indicator" is only spawned
|
||||
// once the button has been held past a short threshold; a quick click that
|
||||
// releases before then emits a single self-contained ripple instead. This
|
||||
// prevents a single click from rendering two ripples (press + release).
|
||||
UINT_PTR m_leftHoldTimer = 0;
|
||||
UINT_PTR m_rightHoldTimer = 0;
|
||||
UINT_PTR m_timer_id = 0;
|
||||
|
||||
bool m_visible = false;
|
||||
@@ -102,6 +139,11 @@ private:
|
||||
winrt::Windows::UI::Color m_alwaysColor = MOUSE_HIGHLIGHTER_DEFAULT_ALWAYS_COLOR;
|
||||
};
|
||||
static const uint32_t BRING_TO_FRONT_TIMER_ID = 123;
|
||||
static const uint32_t HOLD_RIPPLE_TIMER_LEFT = 124;
|
||||
static const uint32_t HOLD_RIPPLE_TIMER_RIGHT = 125;
|
||||
// How long a ripple button must be held before the persistent "held indicator"
|
||||
// is shown. Releasing before this is treated as a quick click (single ripple).
|
||||
static const uint32_t HOLD_RIPPLE_THRESHOLD_MS = 180;
|
||||
Highlighter* Highlighter::instance = nullptr;
|
||||
|
||||
bool Highlighter::CreateHighlighter()
|
||||
@@ -194,11 +236,34 @@ void Highlighter::AddDrawingPoint(MouseButton button)
|
||||
{
|
||||
circleShape.FillBrush(m_compositor.CreateColorBrush(m_leftClickColor));
|
||||
m_leftPointer = circleShape;
|
||||
m_leftGeometry = circleGeometry;
|
||||
|
||||
// Niels-style press-down shrink: holding the button squeezes the
|
||||
// circle to 70% over 180ms after a 150ms delay so quick clicks skip
|
||||
// it. StartDrawingPointFading stops this animation on release.
|
||||
const float pressedRadius = m_radius * 0.70f;
|
||||
auto ease = m_compositor.CreateCubicBezierEasingFunction({ 0.2f, 0.0f }, { 0.4f, 1.0f });
|
||||
auto anim = m_compositor.CreateVector2KeyFrameAnimation();
|
||||
anim.InsertKeyFrame(0.0f, { m_radius, m_radius });
|
||||
anim.InsertKeyFrame(1.0f, { pressedRadius, pressedRadius }, ease);
|
||||
anim.Duration(std::chrono::milliseconds(180));
|
||||
anim.DelayTime(std::chrono::milliseconds(150));
|
||||
circleGeometry.StartAnimation(L"Radius", anim);
|
||||
}
|
||||
else if (button == MouseButton::Right)
|
||||
{
|
||||
circleShape.FillBrush(m_compositor.CreateColorBrush(m_rightClickColor));
|
||||
m_rightPointer = circleShape;
|
||||
m_rightGeometry = circleGeometry;
|
||||
|
||||
const float pressedRadius = m_radius * 0.70f;
|
||||
auto ease = m_compositor.CreateCubicBezierEasingFunction({ 0.2f, 0.0f }, { 0.4f, 1.0f });
|
||||
auto anim = m_compositor.CreateVector2KeyFrameAnimation();
|
||||
anim.InsertKeyFrame(0.0f, { m_radius, m_radius });
|
||||
anim.InsertKeyFrame(1.0f, { pressedRadius, pressedRadius }, ease);
|
||||
anim.Duration(std::chrono::milliseconds(180));
|
||||
anim.DelayTime(std::chrono::milliseconds(150));
|
||||
circleGeometry.StartAnimation(L"Radius", anim);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -238,17 +303,36 @@ void Highlighter::UpdateDrawingPointPosition(MouseButton button)
|
||||
if (button == MouseButton::Left)
|
||||
{
|
||||
m_leftPointer.Offset({ static_cast<float>(pt.x), static_cast<float>(pt.y) });
|
||||
if (m_leftRippleGlow)
|
||||
{
|
||||
m_leftRippleGlow.Offset({ static_cast<float>(pt.x), static_cast<float>(pt.y) });
|
||||
}
|
||||
}
|
||||
else if (button == MouseButton::Right)
|
||||
{
|
||||
m_rightPointer.Offset({ static_cast<float>(pt.x), static_cast<float>(pt.y) });
|
||||
if (m_rightRippleGlow)
|
||||
{
|
||||
m_rightRippleGlow.Offset({ static_cast<float>(pt.x), static_cast<float>(pt.y) });
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// always / spotlight idle
|
||||
if (m_spotlightMode)
|
||||
{
|
||||
UpdateSpotlightMask(static_cast<float>(pt.x), static_cast<float>(pt.y), m_radius, true);
|
||||
if (m_spotlightPressed)
|
||||
{
|
||||
// Only update position while pressed — radius is being animated
|
||||
if (m_spotlightMaskGradient)
|
||||
{
|
||||
m_spotlightMaskGradient.EllipseCenter({ static_cast<float>(pt.x), static_cast<float>(pt.y) });
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
UpdateSpotlightMask(static_cast<float>(pt.x), static_cast<float>(pt.y), m_radius, true);
|
||||
}
|
||||
}
|
||||
else if (m_alwaysPointer)
|
||||
{
|
||||
@@ -259,14 +343,24 @@ void Highlighter::UpdateDrawingPointPosition(MouseButton button)
|
||||
void Highlighter::StartDrawingPointFading(MouseButton button)
|
||||
{
|
||||
winrt::Windows::UI::Composition::CompositionSpriteShape circleShape{ nullptr };
|
||||
winrt::Windows::UI::Composition::CompositionEllipseGeometry geom{ nullptr };
|
||||
if (button == MouseButton::Left)
|
||||
{
|
||||
circleShape = m_leftPointer;
|
||||
geom = m_leftGeometry;
|
||||
}
|
||||
else
|
||||
{
|
||||
// right
|
||||
circleShape = m_rightPointer;
|
||||
geom = m_rightGeometry;
|
||||
}
|
||||
|
||||
// Stop any in-flight press-down shrink so the geometry doesn't keep
|
||||
// animating while the fill is being faded out.
|
||||
if (geom && m_compositor)
|
||||
{
|
||||
geom.StopAnimation(L"Radius");
|
||||
}
|
||||
|
||||
auto brushColor = circleShape.FillBrush().as<winrt::Windows::UI::Composition::CompositionColorBrush>().Color();
|
||||
@@ -329,6 +423,30 @@ LRESULT CALLBACK Highlighter::MouseHookProc(int nCode, WPARAM wParam, LPARAM lPa
|
||||
switch (wParam)
|
||||
{
|
||||
case WM_LBUTTONDOWN:
|
||||
if (instance->m_spotlightMode)
|
||||
{
|
||||
instance->SpotlightAnimatePress();
|
||||
break;
|
||||
}
|
||||
if (instance->m_rippleMode)
|
||||
{
|
||||
if (instance->m_leftPointerEnabled)
|
||||
{
|
||||
// Defer the held indicator: only spawn it if the button is
|
||||
// still down after the hold threshold. A quick click handled
|
||||
// on button-up emits a single ripple instead.
|
||||
instance->m_leftButtonPressed = true;
|
||||
if (instance->m_leftHoldTimer == 0)
|
||||
{
|
||||
instance->m_leftHoldTimer = SetTimer(instance->m_hwnd, HOLD_RIPPLE_TIMER_LEFT, HOLD_RIPPLE_THRESHOLD_MS, nullptr);
|
||||
}
|
||||
if (instance->m_timer_id == 0)
|
||||
{
|
||||
instance->m_timer_id = SetTimer(instance->m_hwnd, BRING_TO_FRONT_TIMER_ID, 10, nullptr);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (instance->m_leftPointerEnabled)
|
||||
{
|
||||
if (instance->m_alwaysPointerEnabled && !instance->m_rightButtonPressed)
|
||||
@@ -354,6 +472,28 @@ LRESULT CALLBACK Highlighter::MouseHookProc(int nCode, WPARAM wParam, LPARAM lPa
|
||||
}
|
||||
break;
|
||||
case WM_RBUTTONDOWN:
|
||||
if (instance->m_spotlightMode)
|
||||
{
|
||||
instance->SpotlightAnimatePress();
|
||||
break;
|
||||
}
|
||||
if (instance->m_rippleMode)
|
||||
{
|
||||
if (instance->m_rightPointerEnabled)
|
||||
{
|
||||
// Defer the held indicator (see WM_LBUTTONDOWN).
|
||||
instance->m_rightButtonPressed = true;
|
||||
if (instance->m_rightHoldTimer == 0)
|
||||
{
|
||||
instance->m_rightHoldTimer = SetTimer(instance->m_hwnd, HOLD_RIPPLE_TIMER_RIGHT, HOLD_RIPPLE_THRESHOLD_MS, nullptr);
|
||||
}
|
||||
if (instance->m_timer_id == 0)
|
||||
{
|
||||
instance->m_timer_id = SetTimer(instance->m_hwnd, BRING_TO_FRONT_TIMER_ID, 10, nullptr);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (instance->m_rightPointerEnabled)
|
||||
{
|
||||
if (instance->m_alwaysPointerEnabled && !instance->m_leftButtonPressed)
|
||||
@@ -376,6 +516,24 @@ LRESULT CALLBACK Highlighter::MouseHookProc(int nCode, WPARAM wParam, LPARAM lPa
|
||||
}
|
||||
break;
|
||||
case WM_MOUSEMOVE:
|
||||
if (instance->m_rippleMode)
|
||||
{
|
||||
// Held ripple ring follows the cursor while a button is down,
|
||||
// gated by the "follow cursor while held" setting. When the
|
||||
// setting is off, the ring stays anchored at the click point.
|
||||
if (instance->m_rippleShowDragTrail)
|
||||
{
|
||||
if (instance->m_leftButtonPressed && instance->m_leftPointer)
|
||||
{
|
||||
instance->UpdateDrawingPointPosition(MouseButton::Left);
|
||||
}
|
||||
if (instance->m_rightButtonPressed && instance->m_rightPointer)
|
||||
{
|
||||
instance->UpdateDrawingPointPosition(MouseButton::Right);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (instance->m_leftButtonPressed)
|
||||
{
|
||||
instance->UpdateDrawingPointPosition(MouseButton::Left);
|
||||
@@ -390,11 +548,33 @@ LRESULT CALLBACK Highlighter::MouseHookProc(int nCode, WPARAM wParam, LPARAM lPa
|
||||
}
|
||||
break;
|
||||
case WM_LBUTTONUP:
|
||||
if (instance->m_spotlightPressed)
|
||||
{
|
||||
instance->SpotlightAnimateRelease();
|
||||
}
|
||||
if (instance->m_leftButtonPressed)
|
||||
{
|
||||
instance->StartDrawingPointFading(MouseButton::Left);
|
||||
if (instance->m_rippleMode)
|
||||
{
|
||||
if (instance->m_leftHoldTimer != 0)
|
||||
{
|
||||
// Released before the hold threshold => quick click.
|
||||
KillTimer(instance->m_hwnd, instance->m_leftHoldTimer);
|
||||
instance->m_leftHoldTimer = 0;
|
||||
instance->EmitSingleRipple(MouseButton::Left);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Held indicator was already shown; expand + fade it.
|
||||
instance->FadeRippleHoldDot(MouseButton::Left);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
instance->StartDrawingPointFading(MouseButton::Left);
|
||||
}
|
||||
instance->m_leftButtonPressed = false;
|
||||
if (instance->m_alwaysPointerEnabled && !instance->m_rightButtonPressed)
|
||||
if (!instance->m_rippleMode && instance->m_alwaysPointerEnabled && !instance->m_rightButtonPressed)
|
||||
{
|
||||
// Add AlwaysPointer only when it's enabled and RightPointer is not active
|
||||
instance->AddDrawingPoint(MouseButton::None);
|
||||
@@ -402,11 +582,32 @@ LRESULT CALLBACK Highlighter::MouseHookProc(int nCode, WPARAM wParam, LPARAM lPa
|
||||
}
|
||||
break;
|
||||
case WM_RBUTTONUP:
|
||||
if (instance->m_spotlightPressed)
|
||||
{
|
||||
instance->SpotlightAnimateRelease();
|
||||
}
|
||||
if (instance->m_rightButtonPressed)
|
||||
{
|
||||
instance->StartDrawingPointFading(MouseButton::Right);
|
||||
if (instance->m_rippleMode)
|
||||
{
|
||||
if (instance->m_rightHoldTimer != 0)
|
||||
{
|
||||
// Released before the hold threshold => quick click.
|
||||
KillTimer(instance->m_hwnd, instance->m_rightHoldTimer);
|
||||
instance->m_rightHoldTimer = 0;
|
||||
instance->EmitSingleRipple(MouseButton::Right);
|
||||
}
|
||||
else
|
||||
{
|
||||
instance->FadeRippleHoldDot(MouseButton::Right);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
instance->StartDrawingPointFading(MouseButton::Right);
|
||||
}
|
||||
instance->m_rightButtonPressed = false;
|
||||
if (instance->m_alwaysPointerEnabled && !instance->m_leftButtonPressed)
|
||||
if (!instance->m_rippleMode && instance->m_alwaysPointerEnabled && !instance->m_leftButtonPressed)
|
||||
{
|
||||
// Add AlwaysPointer only when it's enabled and LeftPointer is not active
|
||||
instance->AddDrawingPoint(MouseButton::None);
|
||||
@@ -448,9 +649,16 @@ void Highlighter::StopDrawing()
|
||||
m_visible = false;
|
||||
m_leftButtonPressed = false;
|
||||
m_rightButtonPressed = false;
|
||||
m_spotlightPressed = false;
|
||||
m_leftPointer = nullptr;
|
||||
m_rightPointer = nullptr;
|
||||
m_alwaysPointer = nullptr;
|
||||
m_leftGeometry = nullptr;
|
||||
m_rightGeometry = nullptr;
|
||||
m_leftRippleGlow = nullptr;
|
||||
m_rightRippleGlow = nullptr;
|
||||
m_leftGlowGeometry = nullptr;
|
||||
m_rightGlowGeometry = nullptr;
|
||||
if (m_overlay)
|
||||
{
|
||||
m_overlay.IsVisible(false);
|
||||
@@ -478,6 +686,16 @@ void Highlighter::ApplySettings(MouseHighlighterSettings settings)
|
||||
m_rightPointerEnabled = settings.rightButtonColor.A != 0;
|
||||
m_alwaysPointerEnabled = settings.alwaysColor.A != 0;
|
||||
m_spotlightMode = settings.spotlightMode && settings.alwaysColor.A != 0;
|
||||
m_rippleMode = settings.rippleMode && !m_spotlightMode;
|
||||
m_rippleSize = (settings.rippleSize > 0) ? static_cast<float>(settings.rippleSize) : static_cast<float>(MOUSE_HIGHLIGHTER_DEFAULT_RIPPLE_SIZE);
|
||||
m_rippleIntensity = (settings.rippleIntensity > 0.0) ? static_cast<float>(settings.rippleIntensity) : static_cast<float>(MOUSE_HIGHLIGHTER_DEFAULT_RIPPLE_INTENSITY);
|
||||
m_rippleDurationMs = (settings.rippleDurationMs > 0) ? settings.rippleDurationMs : MOUSE_HIGHLIGHTER_DEFAULT_RIPPLE_DURATION_MS;
|
||||
m_rippleShowDragTrail = settings.rippleShowDragTrail;
|
||||
m_rippleShowReleasePulse = settings.rippleShowReleasePulse;
|
||||
|
||||
// Reset transient pressed-state flag so a settings change while a button
|
||||
// happens to be down doesn't leave the spotlight stuck at a shrunken size.
|
||||
m_spotlightPressed = false;
|
||||
|
||||
if (m_spotlightMode)
|
||||
{
|
||||
@@ -548,6 +766,7 @@ LRESULT CALLBACK Highlighter::WndProc(HWND hWnd, UINT message, WPARAM wParam, LP
|
||||
// If we would use a timer with a 50 ms period, there would be a flickering on the UI, as in most of the cases
|
||||
// the pinned window hides our window in a few milliseconds.
|
||||
case BRING_TO_FRONT_TIMER_ID:
|
||||
{
|
||||
static int fireCount = 0;
|
||||
if (fireCount++ >= 4)
|
||||
{
|
||||
@@ -558,6 +777,24 @@ LRESULT CALLBACK Highlighter::WndProc(HWND hWnd, UINT message, WPARAM wParam, LP
|
||||
instance->BringToFront();
|
||||
break;
|
||||
}
|
||||
case HOLD_RIPPLE_TIMER_LEFT:
|
||||
// Button held past the threshold: show the persistent held indicator.
|
||||
KillTimer(instance->m_hwnd, instance->m_leftHoldTimer);
|
||||
instance->m_leftHoldTimer = 0;
|
||||
if (instance->m_leftButtonPressed)
|
||||
{
|
||||
instance->SpawnRippleHoldDot(MouseButton::Left);
|
||||
}
|
||||
break;
|
||||
case HOLD_RIPPLE_TIMER_RIGHT:
|
||||
KillTimer(instance->m_hwnd, instance->m_rightHoldTimer);
|
||||
instance->m_rightHoldTimer = 0;
|
||||
if (instance->m_rightButtonPressed)
|
||||
{
|
||||
instance->SpawnRippleHoldDot(MouseButton::Right);
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
@@ -643,6 +880,548 @@ void Highlighter::UpdateSpotlightMask(float cx, float cy, float radius, bool sho
|
||||
}
|
||||
}
|
||||
|
||||
// Spotlight press-down: shrink the mask radius briefly while a button is held.
|
||||
void Highlighter::SpotlightAnimatePress()
|
||||
{
|
||||
if (!m_spotlightMode || !m_spotlightMaskGradient)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
m_spotlightPressed = true;
|
||||
const float pressedRadius = m_radius * 0.85f;
|
||||
|
||||
auto ease = m_compositor.CreateCubicBezierEasingFunction({ 0.2f, 0.0f }, { 0.4f, 1.0f });
|
||||
auto anim = m_compositor.CreateVector2KeyFrameAnimation();
|
||||
anim.InsertKeyFrame(0.0f, { m_radius, m_radius });
|
||||
anim.InsertKeyFrame(1.0f, { pressedRadius, pressedRadius }, ease);
|
||||
anim.Duration(std::chrono::milliseconds(120));
|
||||
m_spotlightMaskGradient.StartAnimation(L"EllipseRadius", anim);
|
||||
}
|
||||
|
||||
// Spotlight release: animate the mask back to the configured radius.
|
||||
void Highlighter::SpotlightAnimateRelease()
|
||||
{
|
||||
m_spotlightPressed = false;
|
||||
|
||||
if (!m_spotlightMode || !m_spotlightMaskGradient)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
auto ease = m_compositor.CreateCubicBezierEasingFunction({ 0.215f, 0.61f }, { 0.355f, 1.0f });
|
||||
auto current = m_spotlightMaskGradient.EllipseRadius();
|
||||
auto anim = m_compositor.CreateVector2KeyFrameAnimation();
|
||||
anim.InsertKeyFrame(0.0f, current);
|
||||
anim.InsertKeyFrame(1.0f, { m_radius, m_radius }, ease);
|
||||
anim.Duration(std::chrono::milliseconds(200));
|
||||
m_spotlightMaskGradient.StartAnimation(L"EllipseRadius", anim);
|
||||
}
|
||||
|
||||
// Spawn the press/hold ring + glow at the click point. The shapes persist
|
||||
// until FadeRippleHoldDot is called (button-up). While held they can be
|
||||
// re-positioned to follow the cursor (UpdateDrawingPointPosition).
|
||||
void Highlighter::SpawnRippleHoldDot(MouseButton button)
|
||||
{
|
||||
if (!m_compositor || !m_shape)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
winrt::Windows::UI::Color color = (button == MouseButton::Left) ? m_leftClickColor : m_rightClickColor;
|
||||
if (color.A == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
POINT pt{};
|
||||
if (!GetCursorPos(&pt))
|
||||
{
|
||||
return;
|
||||
}
|
||||
ScreenToClient(m_hwnd, &pt);
|
||||
const float fx = static_cast<float>(pt.x);
|
||||
const float fy = static_cast<float>(pt.y);
|
||||
|
||||
// Resolve sizing/intensity from the ripple-specific settings so they're
|
||||
// independent of the legacy "always-on dot" controls.
|
||||
const float baseSize = (m_rippleSize > 1.0f) ? m_rippleSize : 1.0f;
|
||||
float intensity = m_rippleIntensity;
|
||||
if (intensity < 0.15f) intensity = 0.15f;
|
||||
if (intensity > 1.35f) intensity = 1.35f;
|
||||
|
||||
const float ringHeld = baseSize * 0.55f;
|
||||
const float glowHeld = baseSize * 0.65f;
|
||||
const float lineWidth = (std::max)(2.25f, baseSize * (0.035f + intensity * 0.045f));
|
||||
|
||||
auto ease = m_compositor.CreateCubicBezierEasingFunction({ 0.215f, 0.61f }, { 0.355f, 1.0f });
|
||||
// Held indicator: appears once the button has been held past the hold
|
||||
// threshold and sits at the held radius until release. It must NOT expand
|
||||
// outward on appearance — it only FADES IN at the held size. The single
|
||||
// outward "ripple" expansion happens exclusively on release
|
||||
// (FadeRippleHoldDot). If this grew outward, a slow single click (release
|
||||
// shortly after the threshold) would show grow-to-held + release as two
|
||||
// expansions — the double-ripple bug.
|
||||
auto dur = std::chrono::milliseconds(120);
|
||||
|
||||
auto clampByte = [](float v) -> uint8_t {
|
||||
if (v < 0.0f) v = 0.0f;
|
||||
if (v > 255.0f) v = 255.0f;
|
||||
return static_cast<uint8_t>(v);
|
||||
};
|
||||
|
||||
// Glow color is the click color, lower alpha (×0.30), scaled by intensity.
|
||||
const float glowAlpha = static_cast<float>(color.A) * 0.30f * intensity;
|
||||
auto glowColor = winrt::Windows::UI::ColorHelper::FromArgb(clampByte(glowAlpha), color.R, color.G, color.B);
|
||||
auto glowTransparent = winrt::Windows::UI::ColorHelper::FromArgb(0, color.R, color.G, color.B);
|
||||
|
||||
// Ring color uses full base alpha (alphaMul like the press recipe).
|
||||
const float alphaMul = 0.18f + intensity * 0.78f;
|
||||
auto ringColor = winrt::Windows::UI::ColorHelper::FromArgb(clampByte(static_cast<float>(color.A) * alphaMul), color.R, color.G, color.B);
|
||||
auto ringTransparent = winrt::Windows::UI::ColorHelper::FromArgb(0, color.R, color.G, color.B);
|
||||
|
||||
// Clean up any stray "still held" shapes for this button — guards against
|
||||
// stray button-down without matching button-up (e.g. focus loss).
|
||||
winrt::CompositionSpriteShape& heldRing = (button == MouseButton::Left) ? m_leftPointer : m_rightPointer;
|
||||
winrt::CompositionSpriteShape& heldGlow = (button == MouseButton::Left) ? m_leftRippleGlow : m_rightRippleGlow;
|
||||
winrt::CompositionEllipseGeometry& heldGeom = (button == MouseButton::Left) ? m_leftGeometry : m_rightGeometry;
|
||||
winrt::CompositionEllipseGeometry& heldGlowGeom = (button == MouseButton::Left) ? m_leftGlowGeometry : m_rightGlowGeometry;
|
||||
|
||||
if (m_shape && m_shape.Shapes())
|
||||
{
|
||||
auto shapes = m_shape.Shapes();
|
||||
uint32_t idx = 0;
|
||||
if (heldRing && shapes.IndexOf(heldRing, idx))
|
||||
{
|
||||
shapes.RemoveAt(idx);
|
||||
}
|
||||
if (heldGlow && shapes.IndexOf(heldGlow, idx))
|
||||
{
|
||||
shapes.RemoveAt(idx);
|
||||
}
|
||||
}
|
||||
|
||||
// Glow (filled) — added first so the ring renders on top. Sits at the held
|
||||
// radius and fades its alpha in (no outward size growth).
|
||||
auto glowGeom = m_compositor.CreateEllipseGeometry();
|
||||
glowGeom.Radius({ glowHeld, glowHeld });
|
||||
auto glowBrush = m_compositor.CreateColorBrush(glowTransparent);
|
||||
auto glowShape = m_compositor.CreateSpriteShape(glowGeom);
|
||||
glowShape.Offset({ fx, fy });
|
||||
glowShape.FillBrush(glowBrush);
|
||||
m_shape.Shapes().Append(glowShape);
|
||||
|
||||
auto glowFadeIn = m_compositor.CreateColorKeyFrameAnimation();
|
||||
glowFadeIn.InsertKeyFrame(0.0f, glowTransparent);
|
||||
glowFadeIn.InsertKeyFrame(1.0f, glowColor, ease);
|
||||
glowFadeIn.Duration(dur);
|
||||
glowBrush.StartAnimation(L"Color", glowFadeIn);
|
||||
|
||||
// Ring (stroked) — same: fixed at held radius, alpha fade-in only.
|
||||
auto ringGeom = m_compositor.CreateEllipseGeometry();
|
||||
ringGeom.Radius({ ringHeld, ringHeld });
|
||||
auto ringBrush = m_compositor.CreateColorBrush(ringTransparent);
|
||||
auto ringShape = m_compositor.CreateSpriteShape(ringGeom);
|
||||
ringShape.Offset({ fx, fy });
|
||||
ringShape.StrokeBrush(ringBrush);
|
||||
ringShape.StrokeThickness(lineWidth);
|
||||
ringShape.IsStrokeNonScaling(true);
|
||||
m_shape.Shapes().Append(ringShape);
|
||||
|
||||
auto ringFadeIn = m_compositor.CreateColorKeyFrameAnimation();
|
||||
ringFadeIn.InsertKeyFrame(0.0f, ringTransparent);
|
||||
ringFadeIn.InsertKeyFrame(1.0f, ringColor, ease);
|
||||
ringFadeIn.Duration(dur);
|
||||
ringBrush.StartAnimation(L"Color", ringFadeIn);
|
||||
|
||||
heldRing = ringShape;
|
||||
heldGlow = glowShape;
|
||||
heldGeom = ringGeom;
|
||||
heldGlowGeom = glowGeom;
|
||||
}
|
||||
|
||||
// Continue the held-ring/glow animation outward and fade both to transparent.
|
||||
// For right-click, optionally spawn the expanding crosshair lines.
|
||||
void Highlighter::FadeRippleHoldDot(MouseButton button)
|
||||
{
|
||||
if (!m_compositor || !m_shape)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
winrt::CompositionSpriteShape& heldRing = (button == MouseButton::Left) ? m_leftPointer : m_rightPointer;
|
||||
winrt::CompositionSpriteShape& heldGlow = (button == MouseButton::Left) ? m_leftRippleGlow : m_rightRippleGlow;
|
||||
winrt::CompositionEllipseGeometry& heldGeom = (button == MouseButton::Left) ? m_leftGeometry : m_rightGeometry;
|
||||
winrt::CompositionEllipseGeometry& heldGlowGeom = (button == MouseButton::Left) ? m_leftGlowGeometry : m_rightGlowGeometry;
|
||||
|
||||
if (!heldRing && !heldGlow)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
winrt::Windows::UI::Color color = (button == MouseButton::Left) ? m_leftClickColor : m_rightClickColor;
|
||||
|
||||
const float baseSize = (m_rippleSize > 1.0f) ? m_rippleSize : 1.0f;
|
||||
float intensity = m_rippleIntensity;
|
||||
if (intensity < 0.15f) intensity = 0.15f;
|
||||
if (intensity > 1.35f) intensity = 1.35f;
|
||||
|
||||
int durationMs = m_rippleDurationMs;
|
||||
if (durationMs < 60) durationMs = 60;
|
||||
if (durationMs > 2000) durationMs = 2000;
|
||||
auto dur = std::chrono::milliseconds(durationMs);
|
||||
|
||||
const float ringHeld = baseSize * 0.55f;
|
||||
const float ringEnd = baseSize * 1.05f;
|
||||
const float glowHeld = baseSize * 0.65f;
|
||||
const float glowEnd = baseSize * 1.40f;
|
||||
|
||||
auto clampByte = [](float v) -> uint8_t {
|
||||
if (v < 0.0f) v = 0.0f;
|
||||
if (v > 255.0f) v = 255.0f;
|
||||
return static_cast<uint8_t>(v);
|
||||
};
|
||||
|
||||
auto ease = m_compositor.CreateCubicBezierEasingFunction({ 0.215f, 0.61f }, { 0.355f, 1.0f });
|
||||
auto transparent = winrt::Windows::UI::ColorHelper::FromArgb(0, color.R, color.G, color.B);
|
||||
|
||||
// Track everything spawned by this fade (and the held shapes themselves)
|
||||
// so the completion callback can remove them in one pass.
|
||||
auto spawned = std::make_shared<std::vector<winrt::CompositionSpriteShape>>();
|
||||
|
||||
auto batch = m_compositor.CreateScopedBatch(winrt::CompositionBatchTypes::Animation);
|
||||
|
||||
if (heldGlow && heldGlowGeom)
|
||||
{
|
||||
// The held indicator has settled at the held radius; expand it outward
|
||||
// from there and fade it to transparent.
|
||||
heldGlowGeom.StopAnimation(L"Radius");
|
||||
auto glowAnim = m_compositor.CreateVector2KeyFrameAnimation();
|
||||
glowAnim.InsertKeyFrame(0.0f, { glowHeld, glowHeld });
|
||||
glowAnim.InsertKeyFrame(1.0f, { glowEnd, glowEnd }, ease);
|
||||
glowAnim.Duration(dur);
|
||||
heldGlowGeom.StartAnimation(L"Radius", glowAnim);
|
||||
|
||||
auto brush = heldGlow.FillBrush().as<winrt::Windows::UI::Composition::CompositionColorBrush>();
|
||||
auto startColor = brush.Color();
|
||||
auto colorAnim = m_compositor.CreateColorKeyFrameAnimation();
|
||||
colorAnim.InsertKeyFrame(0.0f, startColor);
|
||||
colorAnim.InsertKeyFrame(1.0f, transparent, ease);
|
||||
colorAnim.Duration(dur);
|
||||
brush.StartAnimation(L"Color", colorAnim);
|
||||
|
||||
spawned->push_back(heldGlow);
|
||||
}
|
||||
|
||||
if (heldRing && heldGeom)
|
||||
{
|
||||
heldGeom.StopAnimation(L"Radius");
|
||||
auto ringAnim = m_compositor.CreateVector2KeyFrameAnimation();
|
||||
ringAnim.InsertKeyFrame(0.0f, { ringHeld, ringHeld });
|
||||
ringAnim.InsertKeyFrame(1.0f, { ringEnd, ringEnd }, ease);
|
||||
ringAnim.Duration(dur);
|
||||
heldGeom.StartAnimation(L"Radius", ringAnim);
|
||||
|
||||
auto brush = heldRing.StrokeBrush().as<winrt::Windows::UI::Composition::CompositionColorBrush>();
|
||||
auto startColor = brush.Color();
|
||||
auto colorAnim = m_compositor.CreateColorKeyFrameAnimation();
|
||||
colorAnim.InsertKeyFrame(0.0f, startColor);
|
||||
colorAnim.InsertKeyFrame(1.0f, transparent, ease);
|
||||
colorAnim.Duration(dur);
|
||||
brush.StartAnimation(L"Color", colorAnim);
|
||||
|
||||
spawned->push_back(heldRing);
|
||||
}
|
||||
|
||||
// Right-click only: spawn expanding crosshair lines centered on the ring.
|
||||
// Gated by the "show crosshairs on right-click release" toggle.
|
||||
if (button == MouseButton::Right && m_rippleShowReleasePulse && heldRing)
|
||||
{
|
||||
const float xhairAlphaMul = 0.18f + intensity * 0.78f;
|
||||
auto xhairColor = winrt::Windows::UI::ColorHelper::FromArgb(clampByte(static_cast<float>(color.A) * xhairAlphaMul), color.R, color.G, color.B);
|
||||
const float xhairThickness = (std::max)(1.25f, baseSize * (0.025f + intensity * 0.03f));
|
||||
|
||||
auto center = heldRing.Offset();
|
||||
const float startSpan = ringHeld * 0.85f;
|
||||
const float endSpan = ringEnd * 0.85f;
|
||||
|
||||
auto makeLine = [&](float ax1, float ay1, float ax2, float ay2,
|
||||
float bx1, float by1, float bx2, float by2) {
|
||||
auto lineGeom = m_compositor.CreateLineGeometry();
|
||||
lineGeom.Start({ ax1, ay1 });
|
||||
lineGeom.End({ ax2, ay2 });
|
||||
|
||||
auto lineBrush = m_compositor.CreateColorBrush(xhairColor);
|
||||
auto lineShape = m_compositor.CreateSpriteShape(lineGeom);
|
||||
lineShape.StrokeBrush(lineBrush);
|
||||
lineShape.StrokeThickness(xhairThickness);
|
||||
lineShape.IsStrokeNonScaling(true);
|
||||
m_shape.Shapes().Append(lineShape);
|
||||
spawned->push_back(lineShape);
|
||||
|
||||
auto startAnim = m_compositor.CreateVector2KeyFrameAnimation();
|
||||
startAnim.InsertKeyFrame(0.0f, { ax1, ay1 });
|
||||
startAnim.InsertKeyFrame(1.0f, { bx1, by1 }, ease);
|
||||
startAnim.Duration(dur);
|
||||
lineGeom.StartAnimation(L"Start", startAnim);
|
||||
|
||||
auto endAnim = m_compositor.CreateVector2KeyFrameAnimation();
|
||||
endAnim.InsertKeyFrame(0.0f, { ax2, ay2 });
|
||||
endAnim.InsertKeyFrame(1.0f, { bx2, by2 }, ease);
|
||||
endAnim.Duration(dur);
|
||||
lineGeom.StartAnimation(L"End", endAnim);
|
||||
|
||||
auto colorAnim = m_compositor.CreateColorKeyFrameAnimation();
|
||||
colorAnim.InsertKeyFrame(0.0f, xhairColor);
|
||||
colorAnim.InsertKeyFrame(1.0f, transparent, ease);
|
||||
colorAnim.Duration(dur);
|
||||
lineBrush.StartAnimation(L"Color", colorAnim);
|
||||
};
|
||||
|
||||
// Horizontal line (left half, right half).
|
||||
makeLine(center.x - startSpan, center.y, center.x - startSpan * 0.30f, center.y,
|
||||
center.x - endSpan, center.y, center.x - endSpan * 0.30f, center.y);
|
||||
makeLine(center.x + startSpan * 0.30f, center.y, center.x + startSpan, center.y,
|
||||
center.x + endSpan * 0.30f, center.y, center.x + endSpan, center.y);
|
||||
// Vertical line (top half, bottom half).
|
||||
makeLine(center.x, center.y - startSpan, center.x, center.y - startSpan * 0.30f,
|
||||
center.x, center.y - endSpan, center.x, center.y - endSpan * 0.30f);
|
||||
makeLine(center.x, center.y + startSpan * 0.30f, center.x, center.y + startSpan,
|
||||
center.x, center.y + endSpan * 0.30f, center.x, center.y + endSpan);
|
||||
}
|
||||
|
||||
// Detach our member handles BEFORE the batch completes so subsequent
|
||||
// press events on this button create fresh shapes rather than racing.
|
||||
heldRing = nullptr;
|
||||
heldGlow = nullptr;
|
||||
heldGeom = nullptr;
|
||||
heldGlowGeom = nullptr;
|
||||
|
||||
batch.End();
|
||||
|
||||
if (spawned->empty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
auto dispatcher = m_dispatcherQueueController.DispatcherQueue();
|
||||
batch.Completed([dispatcher, spawned](auto&&, auto&&) {
|
||||
dispatcher.TryEnqueue([spawned]() {
|
||||
try
|
||||
{
|
||||
if (Highlighter::instance == nullptr || Highlighter::instance->m_shape == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
auto shapes = Highlighter::instance->m_shape.Shapes();
|
||||
for (auto const& s : *spawned)
|
||||
{
|
||||
uint32_t index = 0;
|
||||
if (shapes.IndexOf(s, index))
|
||||
{
|
||||
shapes.RemoveAt(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
// Highlighter may have torn down between batch completion and dispatch — ignore.
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Self-contained single ripple for a quick click (press + release before the
|
||||
// hold threshold). Spawns a fresh ring + glow that grow from the click point
|
||||
// outward and fade to transparent in one continuous animation — no held
|
||||
// indicator, so a single click produces exactly one ripple. For right-click,
|
||||
// optionally spawns the expanding crosshair lines too.
|
||||
void Highlighter::EmitSingleRipple(MouseButton button)
|
||||
{
|
||||
if (!m_compositor || !m_shape)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
winrt::Windows::UI::Color color = (button == MouseButton::Left) ? m_leftClickColor : m_rightClickColor;
|
||||
if (color.A == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
POINT pt{};
|
||||
if (!GetCursorPos(&pt))
|
||||
{
|
||||
return;
|
||||
}
|
||||
ScreenToClient(m_hwnd, &pt);
|
||||
const float fx = static_cast<float>(pt.x);
|
||||
const float fy = static_cast<float>(pt.y);
|
||||
|
||||
const float baseSize = (m_rippleSize > 1.0f) ? m_rippleSize : 1.0f;
|
||||
float intensity = m_rippleIntensity;
|
||||
if (intensity < 0.15f) intensity = 0.15f;
|
||||
if (intensity > 1.35f) intensity = 1.35f;
|
||||
|
||||
int durationMs = m_rippleDurationMs;
|
||||
if (durationMs < 60) durationMs = 60;
|
||||
if (durationMs > 2000) durationMs = 2000;
|
||||
auto dur = std::chrono::milliseconds(durationMs);
|
||||
|
||||
const float ringStart = baseSize * 0.20f;
|
||||
const float ringEnd = baseSize * 1.05f;
|
||||
const float glowStart = baseSize * 0.30f;
|
||||
const float glowEnd = baseSize * 1.40f;
|
||||
const float lineWidth = (std::max)(2.25f, baseSize * (0.035f + intensity * 0.045f));
|
||||
|
||||
auto clampByte = [](float v) -> uint8_t {
|
||||
if (v < 0.0f) v = 0.0f;
|
||||
if (v > 255.0f) v = 255.0f;
|
||||
return static_cast<uint8_t>(v);
|
||||
};
|
||||
|
||||
const float glowAlpha = static_cast<float>(color.A) * 0.30f * intensity;
|
||||
auto glowColor = winrt::Windows::UI::ColorHelper::FromArgb(clampByte(glowAlpha), color.R, color.G, color.B);
|
||||
const float alphaMul = 0.18f + intensity * 0.78f;
|
||||
auto ringColor = winrt::Windows::UI::ColorHelper::FromArgb(clampByte(static_cast<float>(color.A) * alphaMul), color.R, color.G, color.B);
|
||||
|
||||
auto ease = m_compositor.CreateCubicBezierEasingFunction({ 0.215f, 0.61f }, { 0.355f, 1.0f });
|
||||
auto transparent = winrt::Windows::UI::ColorHelper::FromArgb(0, color.R, color.G, color.B);
|
||||
|
||||
auto spawned = std::make_shared<std::vector<winrt::CompositionSpriteShape>>();
|
||||
|
||||
auto batch = m_compositor.CreateScopedBatch(winrt::CompositionBatchTypes::Animation);
|
||||
|
||||
// Glow (filled) — added first so the ring renders on top.
|
||||
auto glowGeom = m_compositor.CreateEllipseGeometry();
|
||||
glowGeom.Radius({ glowStart, glowStart });
|
||||
auto glowBrush = m_compositor.CreateColorBrush(glowColor);
|
||||
auto glowShape = m_compositor.CreateSpriteShape(glowGeom);
|
||||
glowShape.Offset({ fx, fy });
|
||||
glowShape.FillBrush(glowBrush);
|
||||
m_shape.Shapes().Append(glowShape);
|
||||
spawned->push_back(glowShape);
|
||||
|
||||
auto glowAnim = m_compositor.CreateVector2KeyFrameAnimation();
|
||||
glowAnim.InsertKeyFrame(0.0f, { glowStart, glowStart });
|
||||
glowAnim.InsertKeyFrame(1.0f, { glowEnd, glowEnd }, ease);
|
||||
glowAnim.Duration(dur);
|
||||
glowGeom.StartAnimation(L"Radius", glowAnim);
|
||||
|
||||
auto glowColorAnim = m_compositor.CreateColorKeyFrameAnimation();
|
||||
glowColorAnim.InsertKeyFrame(0.0f, glowColor);
|
||||
glowColorAnim.InsertKeyFrame(1.0f, transparent, ease);
|
||||
glowColorAnim.Duration(dur);
|
||||
glowBrush.StartAnimation(L"Color", glowColorAnim);
|
||||
|
||||
// Ring (stroked).
|
||||
auto ringGeom = m_compositor.CreateEllipseGeometry();
|
||||
ringGeom.Radius({ ringStart, ringStart });
|
||||
auto ringBrush = m_compositor.CreateColorBrush(ringColor);
|
||||
auto ringShape = m_compositor.CreateSpriteShape(ringGeom);
|
||||
ringShape.Offset({ fx, fy });
|
||||
ringShape.StrokeBrush(ringBrush);
|
||||
ringShape.StrokeThickness(lineWidth);
|
||||
ringShape.IsStrokeNonScaling(true);
|
||||
m_shape.Shapes().Append(ringShape);
|
||||
spawned->push_back(ringShape);
|
||||
|
||||
auto ringAnim = m_compositor.CreateVector2KeyFrameAnimation();
|
||||
ringAnim.InsertKeyFrame(0.0f, { ringStart, ringStart });
|
||||
ringAnim.InsertKeyFrame(1.0f, { ringEnd, ringEnd }, ease);
|
||||
ringAnim.Duration(dur);
|
||||
ringGeom.StartAnimation(L"Radius", ringAnim);
|
||||
|
||||
auto ringColorAnim = m_compositor.CreateColorKeyFrameAnimation();
|
||||
ringColorAnim.InsertKeyFrame(0.0f, ringColor);
|
||||
ringColorAnim.InsertKeyFrame(1.0f, transparent, ease);
|
||||
ringColorAnim.Duration(dur);
|
||||
ringBrush.StartAnimation(L"Color", ringColorAnim);
|
||||
|
||||
// Right-click only: spawn expanding crosshair lines centered on the click
|
||||
// point. Gated by the "show crosshairs on right-click release" toggle.
|
||||
if (button == MouseButton::Right && m_rippleShowReleasePulse)
|
||||
{
|
||||
auto xhairColor = ringColor;
|
||||
const float xhairThickness = (std::max)(1.25f, baseSize * (0.025f + intensity * 0.03f));
|
||||
|
||||
const float startSpan = (baseSize * 0.55f) * 0.85f;
|
||||
const float endSpan = ringEnd * 0.85f;
|
||||
|
||||
auto makeLine = [&](float ax1, float ay1, float ax2, float ay2,
|
||||
float bx1, float by1, float bx2, float by2) {
|
||||
auto lineGeom = m_compositor.CreateLineGeometry();
|
||||
lineGeom.Start({ ax1, ay1 });
|
||||
lineGeom.End({ ax2, ay2 });
|
||||
|
||||
auto lineBrush = m_compositor.CreateColorBrush(xhairColor);
|
||||
auto lineShape = m_compositor.CreateSpriteShape(lineGeom);
|
||||
lineShape.StrokeBrush(lineBrush);
|
||||
lineShape.StrokeThickness(xhairThickness);
|
||||
lineShape.IsStrokeNonScaling(true);
|
||||
m_shape.Shapes().Append(lineShape);
|
||||
spawned->push_back(lineShape);
|
||||
|
||||
auto startAnim = m_compositor.CreateVector2KeyFrameAnimation();
|
||||
startAnim.InsertKeyFrame(0.0f, { ax1, ay1 });
|
||||
startAnim.InsertKeyFrame(1.0f, { bx1, by1 }, ease);
|
||||
startAnim.Duration(dur);
|
||||
lineGeom.StartAnimation(L"Start", startAnim);
|
||||
|
||||
auto endAnim = m_compositor.CreateVector2KeyFrameAnimation();
|
||||
endAnim.InsertKeyFrame(0.0f, { ax2, ay2 });
|
||||
endAnim.InsertKeyFrame(1.0f, { bx2, by2 }, ease);
|
||||
endAnim.Duration(dur);
|
||||
lineGeom.StartAnimation(L"End", endAnim);
|
||||
|
||||
auto colorAnim = m_compositor.CreateColorKeyFrameAnimation();
|
||||
colorAnim.InsertKeyFrame(0.0f, xhairColor);
|
||||
colorAnim.InsertKeyFrame(1.0f, transparent, ease);
|
||||
colorAnim.Duration(dur);
|
||||
lineBrush.StartAnimation(L"Color", colorAnim);
|
||||
};
|
||||
|
||||
// Horizontal line (left half, right half).
|
||||
makeLine(fx - startSpan, fy, fx - startSpan * 0.30f, fy,
|
||||
fx - endSpan, fy, fx - endSpan * 0.30f, fy);
|
||||
makeLine(fx + startSpan * 0.30f, fy, fx + startSpan, fy,
|
||||
fx + endSpan * 0.30f, fy, fx + endSpan, fy);
|
||||
// Vertical line (top half, bottom half).
|
||||
makeLine(fx, fy - startSpan, fx, fy - startSpan * 0.30f,
|
||||
fx, fy - endSpan, fx, fy - endSpan * 0.30f);
|
||||
makeLine(fx, fy + startSpan * 0.30f, fx, fy + startSpan,
|
||||
fx, fy + endSpan * 0.30f, fx, fy + endSpan);
|
||||
}
|
||||
|
||||
batch.End();
|
||||
|
||||
auto dispatcher = m_dispatcherQueueController.DispatcherQueue();
|
||||
batch.Completed([dispatcher, spawned](auto&&, auto&&) {
|
||||
dispatcher.TryEnqueue([spawned]() {
|
||||
try
|
||||
{
|
||||
if (Highlighter::instance == nullptr || Highlighter::instance->m_shape == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
auto shapes = Highlighter::instance->m_shape.Shapes();
|
||||
for (auto const& s : *spawned)
|
||||
{
|
||||
uint32_t index = 0;
|
||||
if (shapes.IndexOf(s, index))
|
||||
{
|
||||
shapes.RemoveAt(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
// Highlighter may have torn down between batch completion and dispatch — ignore.
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#pragma region MouseHighlighter_API
|
||||
|
||||
void MouseHighlighterApplySettings(MouseHighlighterSettings settings)
|
||||
|
||||
@@ -4,10 +4,16 @@
|
||||
const winrt::Windows::UI::Color MOUSE_HIGHLIGHTER_DEFAULT_LEFT_BUTTON_COLOR = winrt::Windows::UI::ColorHelper::FromArgb(166, 255, 255, 0);
|
||||
const winrt::Windows::UI::Color MOUSE_HIGHLIGHTER_DEFAULT_RIGHT_BUTTON_COLOR = winrt::Windows::UI::ColorHelper::FromArgb(166, 0, 0, 255);
|
||||
const winrt::Windows::UI::Color MOUSE_HIGHLIGHTER_DEFAULT_ALWAYS_COLOR = winrt::Windows::UI::ColorHelper::FromArgb(0, 255, 0, 0);
|
||||
constexpr int MOUSE_HIGHLIGHTER_DEFAULT_RADIUS = 20;
|
||||
constexpr int MOUSE_HIGHLIGHTER_DEFAULT_DELAY_MS = 500;
|
||||
constexpr int MOUSE_HIGHLIGHTER_DEFAULT_DURATION_MS = 250;
|
||||
constexpr int MOUSE_HIGHLIGHTER_DEFAULT_RADIUS = 30;
|
||||
constexpr int MOUSE_HIGHLIGHTER_DEFAULT_DELAY_MS = 400;
|
||||
constexpr int MOUSE_HIGHLIGHTER_DEFAULT_DURATION_MS = 400;
|
||||
constexpr bool MOUSE_HIGHLIGHTER_DEFAULT_AUTO_ACTIVATE = false;
|
||||
// Ripple-specific defaults (independent of the always-on circle settings above).
|
||||
constexpr int MOUSE_HIGHLIGHTER_DEFAULT_RIPPLE_SIZE = 60;
|
||||
constexpr double MOUSE_HIGHLIGHTER_DEFAULT_RIPPLE_INTENSITY = 0.7;
|
||||
constexpr int MOUSE_HIGHLIGHTER_DEFAULT_RIPPLE_DURATION_MS = 480;
|
||||
constexpr bool MOUSE_HIGHLIGHTER_DEFAULT_RIPPLE_SHOW_DRAG_TRAIL = true;
|
||||
constexpr bool MOUSE_HIGHLIGHTER_DEFAULT_RIPPLE_SHOW_RELEASE_PULSE = true;
|
||||
|
||||
struct MouseHighlighterSettings
|
||||
{
|
||||
@@ -19,6 +25,12 @@ struct MouseHighlighterSettings
|
||||
int fadeDurationMs = MOUSE_HIGHLIGHTER_DEFAULT_DURATION_MS;
|
||||
bool autoActivate = MOUSE_HIGHLIGHTER_DEFAULT_AUTO_ACTIVATE;
|
||||
bool spotlightMode = false;
|
||||
bool rippleMode = true;
|
||||
int rippleSize = MOUSE_HIGHLIGHTER_DEFAULT_RIPPLE_SIZE;
|
||||
double rippleIntensity = MOUSE_HIGHLIGHTER_DEFAULT_RIPPLE_INTENSITY;
|
||||
int rippleDurationMs = MOUSE_HIGHLIGHTER_DEFAULT_RIPPLE_DURATION_MS;
|
||||
bool rippleShowDragTrail = MOUSE_HIGHLIGHTER_DEFAULT_RIPPLE_SHOW_DRAG_TRAIL;
|
||||
bool rippleShowReleasePulse = MOUSE_HIGHLIGHTER_DEFAULT_RIPPLE_SHOW_RELEASE_PULSE;
|
||||
};
|
||||
|
||||
int MouseHighlighterMain(HINSTANCE hinst, MouseHighlighterSettings settings);
|
||||
|
||||
@@ -21,6 +21,12 @@ namespace
|
||||
const wchar_t JSON_KEY_HIGHLIGHT_FADE_DURATION_MS[] = L"highlight_fade_duration_ms";
|
||||
const wchar_t JSON_KEY_AUTO_ACTIVATE[] = L"auto_activate";
|
||||
const wchar_t JSON_KEY_SPOTLIGHT_MODE[] = L"spotlight_mode";
|
||||
const wchar_t JSON_KEY_RIPPLE_MODE[] = L"ripple_mode";
|
||||
const wchar_t JSON_KEY_RIPPLE_SIZE[] = L"ripple_size";
|
||||
const wchar_t JSON_KEY_RIPPLE_INTENSITY[] = L"ripple_intensity";
|
||||
const wchar_t JSON_KEY_RIPPLE_DURATION_MS[] = L"ripple_duration_ms";
|
||||
const wchar_t JSON_KEY_RIPPLE_SHOW_DRAG_TRAIL[] = L"ripple_show_drag_trail";
|
||||
const wchar_t JSON_KEY_RIPPLE_SHOW_RELEASE_PULSE[] = L"ripple_show_release_pulse";
|
||||
}
|
||||
|
||||
extern "C" IMAGE_DOS_HEADER __ImageBase;
|
||||
@@ -392,6 +398,90 @@ public:
|
||||
{
|
||||
Logger::warn("Failed to initialize spotlight mode settings. Will use default value");
|
||||
}
|
||||
try
|
||||
{
|
||||
// Parse ripple mode
|
||||
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_RIPPLE_MODE);
|
||||
highlightSettings.rippleMode = jsonPropertiesObject.GetNamedBoolean(JSON_KEY_VALUE);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Logger::warn("Failed to initialize ripple mode settings. Will use default value");
|
||||
}
|
||||
try
|
||||
{
|
||||
// Parse ripple size
|
||||
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_RIPPLE_SIZE);
|
||||
int value = static_cast<int>(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE));
|
||||
if (value > 0)
|
||||
{
|
||||
highlightSettings.rippleSize = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw std::runtime_error("Invalid ripple size value");
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Logger::warn("Failed to initialize ripple size from settings. Will use default value");
|
||||
}
|
||||
try
|
||||
{
|
||||
// Parse ripple intensity
|
||||
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_RIPPLE_INTENSITY);
|
||||
double value = jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE);
|
||||
if (value > 0.0)
|
||||
{
|
||||
highlightSettings.rippleIntensity = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw std::runtime_error("Invalid ripple intensity value");
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Logger::warn("Failed to initialize ripple intensity from settings. Will use default value");
|
||||
}
|
||||
try
|
||||
{
|
||||
// Parse ripple duration
|
||||
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_RIPPLE_DURATION_MS);
|
||||
int value = static_cast<int>(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE));
|
||||
if (value > 0)
|
||||
{
|
||||
highlightSettings.rippleDurationMs = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw std::runtime_error("Invalid ripple duration value");
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Logger::warn("Failed to initialize ripple duration from settings. Will use default value");
|
||||
}
|
||||
try
|
||||
{
|
||||
// Parse ripple show drag trail
|
||||
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_RIPPLE_SHOW_DRAG_TRAIL);
|
||||
highlightSettings.rippleShowDragTrail = jsonPropertiesObject.GetNamedBoolean(JSON_KEY_VALUE);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Logger::warn("Failed to initialize ripple show drag trail from settings. Will use default value");
|
||||
}
|
||||
try
|
||||
{
|
||||
// Parse ripple show release pulse
|
||||
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_RIPPLE_SHOW_RELEASE_PULSE);
|
||||
highlightSettings.rippleShowReleasePulse = jsonPropertiesObject.GetNamedBoolean(JSON_KEY_VALUE);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Logger::warn("Failed to initialize ripple show release pulse from settings. Will use default value");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Natvis Include="$(MSBuildThisFileDirectory)..\..\natvis\wil.natvis" />
|
||||
<Natvis Include="$(MSBuildThisFileDirectory)..\..\natvis\wil.natstepfilter" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="Microsoft.Windows.ImplementationLibrary" version="1.0.250325.1" targetFramework="native" />
|
||||
</packages>
|
||||
<package id="Microsoft.Windows.ImplementationLibrary" version="1.0.260126.7" targetFramework="native" />
|
||||
</packages>
|
||||
@@ -119,7 +119,7 @@
|
||||
</resheader>
|
||||
<data name="context_menu_item_new" xml:space="preserve">
|
||||
<value>New+</value>
|
||||
<comment>The main context menu item that users click on. This should be localized to match New in Windows. e.g. Danish it would become Ny+</comment>
|
||||
<comment>The main context menu item that users click on. This should be localized to match New in Windows. e.g. Danish it would become Ny+, French it would become Nouveau+ (not Nouveauté+)</comment>
|
||||
</data>
|
||||
<data name="context_menu_item_open_templates" xml:space="preserve">
|
||||
<value>Open templates</value>
|
||||
|
||||
@@ -119,7 +119,7 @@
|
||||
</resheader>
|
||||
<data name="context_menu_item_new" xml:space="preserve">
|
||||
<value>New+</value>
|
||||
<comment>The main context menu item that users click on. This should be localized to match New in Windows. e.g. Danish it would become Ny+</comment>
|
||||
<comment>The main context menu item that users click on. This should be localized to match New in Windows. e.g. Danish it would become Ny+, French it would become Nouveau+ (not Nouveauté+)</comment>
|
||||
</data>
|
||||
<data name="context_menu_item_open_templates" xml:space="preserve">
|
||||
<value>Open templates</value>
|
||||
|
||||
@@ -0,0 +1,821 @@
|
||||
PackageName: BlackmagicDesign.DaVinciResolve
|
||||
Name: DaVinci Resolve
|
||||
WindowFilter: "Resolve.exe"
|
||||
BackgroundProcess: false
|
||||
Shortcuts:
|
||||
- SectionName: Popular shortcuts
|
||||
Properties:
|
||||
- Name: Edit
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- F5
|
||||
- Name: Color
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- F6
|
||||
- Name: Fairlight
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- F7
|
||||
- Name: Deliver
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- F8
|
||||
- Name: Play / Pause
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- Space
|
||||
- Name: Play Reverse
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- J
|
||||
- Name: Stop
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- K
|
||||
- Name: Play Forward
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- L
|
||||
- Name: Import Media
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- I
|
||||
- Name: Export / Deliver
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: true
|
||||
Alt: false
|
||||
Keys:
|
||||
- E
|
||||
- Name: Save Project
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- S
|
||||
- Name: Cut Clip
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- B
|
||||
- Name: Blade Edit
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- Backslash
|
||||
- Name: Ripple Delete
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: true
|
||||
Alt: false
|
||||
Keys:
|
||||
- Delete
|
||||
- Name: Undo
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- Z
|
||||
- Name: Redo
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: true
|
||||
Alt: false
|
||||
Keys:
|
||||
- Z
|
||||
- Name: Mark In
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- I
|
||||
- Name: Mark Out
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- O
|
||||
- Name: Marker
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- M
|
||||
- Name: Select All
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- A
|
||||
- Name: Go to Beginning
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- Home
|
||||
- Name: Go to End
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- End
|
||||
- Name: Snapping
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- N
|
||||
- Name: Selection Mode
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- A
|
||||
- Name: Trim Mode
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- T
|
||||
- Name: Change Clip Speed
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- R
|
||||
- SectionName: Timeline navigation
|
||||
Properties:
|
||||
- Name: Go to Next Frame
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- Right
|
||||
- Name: Go to Previous Frame
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- Left
|
||||
- Name: Jump Forward 5 Frames
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: true
|
||||
Alt: false
|
||||
Keys:
|
||||
- Right
|
||||
- Name: Jump Back 5 Frames
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: true
|
||||
Alt: false
|
||||
Keys:
|
||||
- Left
|
||||
- Name: Go to Next Clip
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: true
|
||||
Alt: false
|
||||
Keys:
|
||||
- Up
|
||||
- Name: Go to Previous Clip
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: true
|
||||
Alt: false
|
||||
Keys:
|
||||
- Down
|
||||
- Name: Go to Next Track
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- Down
|
||||
- Name: Go to Previous Track
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- Up
|
||||
- Name: Zoom In Timeline
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- Equals
|
||||
- Name: Zoom Out Timeline
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- Minus
|
||||
- Name: Full Screen Playback
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: true
|
||||
Alt: false
|
||||
Keys:
|
||||
- Space
|
||||
- Name: Go to Previous Edit Point
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- PageUp
|
||||
- Name: Go to Next Edit Point
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- PageDown
|
||||
- SectionName: Edit
|
||||
Properties:
|
||||
- Name: Delete
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- Delete
|
||||
- Name: Copy
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- C
|
||||
- Name: Paste
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- V
|
||||
- Name: Cut
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- X
|
||||
- Name: Duplicate Clip
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- D
|
||||
- Name: Render in Place
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: true
|
||||
Alt: false
|
||||
Keys:
|
||||
- X
|
||||
- Name: Add Edit
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- Backslash
|
||||
- Name: Append to End of Timeline
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- End
|
||||
- Name: Replace Clip
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- R
|
||||
- Name: Move Clip Up One Track
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: true
|
||||
Alt: false
|
||||
Keys:
|
||||
- Up
|
||||
- Name: Move Clip Down One Track
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: true
|
||||
Alt: false
|
||||
Keys:
|
||||
- Down
|
||||
- Name: Split Clip
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- B
|
||||
- Name: Link Clips
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- L
|
||||
- Name: Create Compound Clip
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- G
|
||||
- SectionName: Color
|
||||
Properties:
|
||||
- Name: Add Serial Node
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: true
|
||||
Keys:
|
||||
- S
|
||||
- Name: Add Parallel Node
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: true
|
||||
Keys:
|
||||
- P
|
||||
- Name: Add Layer Node
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: true
|
||||
Keys:
|
||||
- L
|
||||
- Name: Select Node 1
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- "1"
|
||||
- Name: Select Node 2
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- "2"
|
||||
- Name: Select Node 3
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- "3"
|
||||
- Name: Select Node 4
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- "4"
|
||||
- Name: Select Node 5
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- "5"
|
||||
- Name: Enable/Disable Current Grade
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- D
|
||||
- Name: Preview Mode
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: true
|
||||
Alt: false
|
||||
Keys:
|
||||
- W
|
||||
- Name: Grade All Frames in Clip
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- D
|
||||
- Name: Keyframe Mode
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- K
|
||||
- Name: Select Color Wheels
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- "1"
|
||||
- Name: Select Curves
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- "2"
|
||||
- Name: Select Qualifier
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- "3"
|
||||
- Name: Select Power Window
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- "4"
|
||||
- Name: Select Tracking
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- "5"
|
||||
- Name: Reset Color Grade
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- U
|
||||
- SectionName: Fairlight
|
||||
Properties:
|
||||
- Name: Mute Track
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- M
|
||||
- Name: Solo Track
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- S
|
||||
- Name: Automation Mode
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- F
|
||||
- Name: Record Arm Selected Track
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- R
|
||||
- Name: Headphones Solo
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- H
|
||||
- Name: Add Marker
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- Insert
|
||||
- Name: Add Audio Track
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: true
|
||||
Alt: false
|
||||
Keys:
|
||||
- A
|
||||
- Name: Bounce Mix
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: true
|
||||
Alt: false
|
||||
Keys:
|
||||
- X
|
||||
- SectionName: Fusion
|
||||
Properties:
|
||||
- Name: Switch Between Spline and Keyframes
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- K
|
||||
- Name: Add Keyframe
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- Shift
|
||||
- Name: View Current Tool
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- "1"
|
||||
- Name: View Node Flow
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- "2"
|
||||
- Name: View Keyframes
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- "3"
|
||||
- Name: View Spline
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- "4"
|
||||
- Name: Merge Selected Tools
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- N
|
||||
- Name: Bypass Selected Tool
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- "1"
|
||||
- SectionName: Media
|
||||
Properties:
|
||||
- Name: Reveal in Explorer
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- E
|
||||
- Name: Smart Bin
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: true
|
||||
Alt: false
|
||||
Keys:
|
||||
- S
|
||||
- Name: Rename Clip
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- F2
|
||||
- Name: Import XML / AAF
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: true
|
||||
Alt: false
|
||||
Keys:
|
||||
- I
|
||||
- Name: Create New Bin
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- N
|
||||
- Name: Add Clip to Timeline
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- Enter
|
||||
- Name: Viewer Zoom In
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- Equals
|
||||
- Name: Viewer Zoom Out
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- Minus
|
||||
- SectionName: Deliver
|
||||
Properties:
|
||||
- Name: Add to Render Queue
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- Enter
|
||||
- Name: Start Render
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: true
|
||||
Alt: false
|
||||
Keys:
|
||||
- Enter
|
||||
- Name: Select Preset
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- F2
|
||||
- Name: Render Settings
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- E
|
||||
- Name: Browse Output Location
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- B
|
||||
@@ -57,7 +57,17 @@ namespace ShortcutGuide
|
||||
return _currentApplicationIds;
|
||||
});
|
||||
|
||||
Title = ResourceLoaderInstance.ResourceLoader.GetString("Title")!;
|
||||
var title = ResourceLoaderInstance.ResourceLoader.GetString("Title")!;
|
||||
|
||||
// Guard against an empty title: ResourceLoader.GetString returns "" when the resource
|
||||
// map can't be resolved, and an empty native window title can fault the WinUI TitleBar
|
||||
// control while it reads AppWindow.Title during a deferred layout pass.
|
||||
if (string.IsNullOrEmpty(title))
|
||||
{
|
||||
title = "Shortcut Guide";
|
||||
}
|
||||
|
||||
Title = title;
|
||||
ExtendsContentIntoTitleBar = true;
|
||||
|
||||
#if !DEBUG
|
||||
|
||||
@@ -171,6 +171,7 @@ FONT 8, "MS Shell Dlg", 400, 0, 0x1
|
||||
BEGIN
|
||||
CONTROL "",IDC_HOTKEY,"msctls_hotkey32",WS_BORDER | WS_TABSTOP,59,57,80,12
|
||||
LTEXT "After toggling ZoomIt you can zoom in with the mouse wheel or up and down arrow keys. Exit zoom mode with Escape or by pressing the right mouse button.",IDC_STATIC,7,6,230,26
|
||||
LTEXT "Copy a zoomed screen with Ctrl+C or save it by typing Ctrl+S. Crop the copy or save region by entering Ctrl+Shift instead of Ctrl.",IDC_STATIC,7,34,230,18
|
||||
LTEXT "Zoom Toggle:",IDC_STATIC,7,59,51,8
|
||||
CONTROL "",IDC_ZOOM_SLIDER,"msctls_trackbar32",TBS_AUTOTICKS | TBS_BOTH | TBS_NOTICKS | WS_TABSTOP,53,118,150,15,WS_EX_TRANSPARENT
|
||||
LTEXT "Specify the initial level of magnification when zooming in:",IDC_STATIC,7,105,230,10
|
||||
@@ -182,8 +183,6 @@ BEGIN
|
||||
LTEXT "4.0",IDC_STATIC,190,136,12,8
|
||||
CONTROL "Animate zoom in and zoom out:",IDC_ANIMATE_ZOOM,"Button",BS_AUTOCHECKBOX | BS_LEFTTEXT | WS_TABSTOP,7,74,116,10
|
||||
CONTROL "Smooth zoomed image:",IDC_SMOOTH_IMAGE,"Button",BS_AUTOCHECKBOX | BS_LEFTTEXT | WS_TABSTOP,7,88,116,10
|
||||
LTEXT "Copy a zoomed screen with Ctrl+C or save it by typing Ctrl+S. Crop the copy or save region by entering Ctrl+Shift instead of Ctrl.",IDC_STATIC,7,148,230,17
|
||||
LTEXT "Copy a zoomed screen with Ctrl+C or save it by typing Ctrl+S. Crop the copy or save region by entering Ctrl+Shift instead of Ctrl.",IDC_STATIC,6,34,230,18
|
||||
END
|
||||
|
||||
DRAW DIALOGEX 0, 0, 260, 228
|
||||
@@ -315,26 +314,31 @@ BEGIN
|
||||
PUSHBUTTON "Cancel",IDCANCEL,162,142,50,14
|
||||
END
|
||||
|
||||
SNIP DIALOGEX 0, 0, 260, 80
|
||||
SNIP DIALOGEX 0, 0, 272, 105
|
||||
STYLE DS_SETFONT | DS_FIXEDSYS | DS_CONTROL | WS_CHILD | WS_CLIPSIBLINGS | WS_SYSMENU
|
||||
FONT 8, "MS Shell Dlg", 400, 0, 0x1
|
||||
BEGIN
|
||||
LTEXT "Copy a region of the screen to the clipboard or enter the hotkey with the Shift key in the opposite mode to save it to a file.",IDC_STATIC,7,7,230,19
|
||||
LTEXT "Snip Toggle:",IDC_STATIC,7,33,45,8
|
||||
CONTROL "",IDC_SNIP_HOTKEY,"msctls_hotkey32",WS_BORDER | WS_TABSTOP,67,32,80,12
|
||||
LTEXT "Copy text from the selected region to the clipboard:",IDC_STATIC,7,50,230,10
|
||||
LTEXT "Text Toggle:",IDC_STATIC,7,65,55,8
|
||||
CONTROL "",IDC_SNIP_OCR_HOTKEY,"msctls_hotkey32",WS_BORDER | WS_TABSTOP,67,63,80,12
|
||||
LTEXT "Copy a region of the screen to the clipboard, or save it to a file using the save shortcut.",IDC_STATIC,7,7,230,18
|
||||
RTEXT "Snip Toggle:",IDC_STATIC,22,33,45,8
|
||||
CONTROL "",IDC_SNIP_HOTKEY,"msctls_hotkey32",WS_BORDER | WS_TABSTOP,71,32,80,12
|
||||
RTEXT "Snip Save Toggle:",IDC_STATIC,7,49,60,8
|
||||
CONTROL "",IDC_SNIP_SAVE_HOTKEY,"msctls_hotkey32",WS_BORDER | WS_TABSTOP,71,48,80,12
|
||||
LTEXT "Copy text from the selected region to the clipboard:",IDC_STATIC,7,66,230,10
|
||||
RTEXT "Text Toggle:",IDC_STATIC,12,82,55,8
|
||||
CONTROL "",IDC_SNIP_OCR_HOTKEY,"msctls_hotkey32",WS_BORDER | WS_TABSTOP,71,81,80,12
|
||||
END
|
||||
|
||||
PANORAMA DIALOGEX 0, 0, 260, 105
|
||||
PANORAMA DIALOGEX 0, 0, 260, 140
|
||||
STYLE DS_SETFONT | DS_FIXEDSYS | DS_CONTROL | WS_CHILD | WS_CLIPSIBLINGS | WS_SYSMENU
|
||||
FONT 8, "MS Shell Dlg", 400, 0, 0x1
|
||||
BEGIN
|
||||
LTEXT "Capture a scrolling panorama of a selected screen region. Select the area, then scroll the content. Move slowly and consistently, and do not rewind to previously covered areas. Press the hotkey again or with Shift to save to a file.",IDC_STATIC,7,7,245,33
|
||||
LTEXT "Panorama Toggle:",IDC_STATIC,7,74,63,8
|
||||
CONTROL "",IDC_SNIP_PANORAMA_HOTKEY,"msctls_hotkey32",WS_BORDER | WS_TABSTOP,73,72,80,12
|
||||
LTEXT "For the best results, scroll slowly and at a constant rate, do not include stationary content (like scrollbars) in the capture area, and avoid content that is changing (e.g., animations or videos). ",IDC_STATIC,7,41,245,30
|
||||
LTEXT "Capture a scrolling panorama of a selected screen region. Select the area, then scroll the content. Move slowly and consistently, and do not rewind to previously covered areas.",IDC_STATIC,7,7,245,30
|
||||
LTEXT "Press the panorama toggle again to copy to the clipboard, or use the save shortcut to save to a file.",IDC_STATIC,7,39,245,18
|
||||
LTEXT "For the best results, scroll slowly and at a constant rate, do not include stationary content (like scrollbars) in the capture area, and avoid content that is changing (e.g., animations or videos). ",IDC_STATIC,7,62,245,30
|
||||
LTEXT "Panorama Toggle:",IDC_STATIC,7,95,80,8
|
||||
CONTROL "",IDC_SNIP_PANORAMA_HOTKEY,"msctls_hotkey32",WS_BORDER | WS_TABSTOP,90,93,80,12
|
||||
LTEXT "Panorama Save Toggle:",IDC_STATIC,7,111,80,8
|
||||
CONTROL "",IDC_SNIP_PANORAMA_SAVE_HOTKEY,"msctls_hotkey32",WS_BORDER | WS_TABSTOP,90,109,80,12
|
||||
END
|
||||
|
||||
DEMOTYPE DIALOGEX 0, 0, 260, 249
|
||||
@@ -456,7 +460,9 @@ BEGIN
|
||||
"SNIP", DIALOG
|
||||
BEGIN
|
||||
LEFTMARGIN, 7
|
||||
RIGHTMARGIN, 265
|
||||
TOPMARGIN, 7
|
||||
BOTTOMMARGIN, 98
|
||||
END
|
||||
|
||||
"PANORAMA", DIALOG
|
||||
|
||||
@@ -17,7 +17,9 @@ DWORD g_BreakToggleKey = ((HOTKEYF_CONTROL) << 8)| '3';
|
||||
DWORD g_DemoTypeToggleKey = ((HOTKEYF_CONTROL) << 8) | '7';
|
||||
DWORD g_RecordToggleKey = ((HOTKEYF_CONTROL) << 8) | '5';
|
||||
DWORD g_SnipToggleKey = ((HOTKEYF_CONTROL) << 8) | '6';
|
||||
DWORD g_SnipSaveToggleKey = ((HOTKEYF_CONTROL | HOTKEYF_SHIFT) << 8) | '6';
|
||||
DWORD g_SnipPanoramaToggleKey = ((HOTKEYF_CONTROL) << 8) | '8';
|
||||
DWORD g_SnipPanoramaSaveToggleKey = ((HOTKEYF_CONTROL | HOTKEYF_SHIFT) << 8) | '8';
|
||||
DWORD g_SnipOcrToggleKey = ((HOTKEYF_CONTROL | HOTKEYF_ALT) << 8) | '6';
|
||||
|
||||
DWORD g_ShowExpiredTime = 1;
|
||||
@@ -80,7 +82,9 @@ REG_SETTING RegSettings[] = {
|
||||
{ L"DrawToggleKey", SETTING_TYPE_DWORD, 0, &g_DrawToggleKey, static_cast<DOUBLE>(g_DrawToggleKey) },
|
||||
{ L"RecordToggleKey", SETTING_TYPE_DWORD, 0, &g_RecordToggleKey, static_cast<DOUBLE>(g_RecordToggleKey) },
|
||||
{ L"SnipToggleKey", SETTING_TYPE_DWORD, 0, &g_SnipToggleKey, static_cast<DOUBLE>(g_SnipToggleKey) },
|
||||
{ L"SnipSaveToggleKey", SETTING_TYPE_DWORD, 0, &g_SnipSaveToggleKey, static_cast<DOUBLE>(g_SnipSaveToggleKey) },
|
||||
{ L"SnipPanoramaToggleKey", SETTING_TYPE_DWORD, 0, &g_SnipPanoramaToggleKey, static_cast<DOUBLE>(g_SnipPanoramaToggleKey) },
|
||||
{ L"SnipPanoramaSaveToggleKey", SETTING_TYPE_DWORD, 0, &g_SnipPanoramaSaveToggleKey, static_cast<DOUBLE>(g_SnipPanoramaSaveToggleKey) },
|
||||
{ L"SnipOcrToggleKey", SETTING_TYPE_DWORD, 0, &g_SnipOcrToggleKey, static_cast<DOUBLE>(g_SnipOcrToggleKey) },
|
||||
{ L"PenColor", SETTING_TYPE_DWORD, 0, &g_PenColor, static_cast<DOUBLE>(g_PenColor) },
|
||||
{ L"PenWidth", SETTING_TYPE_DWORD, 0, &g_RootPenWidth, static_cast<DOUBLE>(g_RootPenWidth) },
|
||||
|
||||
@@ -174,6 +174,8 @@ DWORD g_RecordToggleMod;
|
||||
DWORD g_SnipToggleMod;
|
||||
DWORD g_SnipPanoramaToggleMod;
|
||||
DWORD g_SnipOcrToggleMod;
|
||||
DWORD g_SnipSaveToggleMod;
|
||||
DWORD g_SnipPanoramaSaveToggleMod;
|
||||
|
||||
BOOLEAN g_ZoomOnLiveZoom = FALSE;
|
||||
DWORD g_PenWidth = PEN_WIDTH;
|
||||
@@ -212,7 +214,10 @@ BOOL g_RecordToggle = FALSE;
|
||||
BOOL g_RecordCropping = FALSE;
|
||||
SelectRectangle g_SelectRectangle;
|
||||
WebcamPreviewWindow g_WebcamPreview;
|
||||
// The full path of the last saved recording file.
|
||||
std::wstring g_RecordingSaveLocation;
|
||||
// The last user-chosen recording filename. Used to construct unique recording filenames.
|
||||
std::wstring g_RecordingSaveBaseFilename;
|
||||
std::wstring g_ScreenshotSaveLocation;
|
||||
winrt::IDirect3DDevice g_RecordDevice{ nullptr };
|
||||
std::shared_ptr<VideoRecordingSession> g_RecordingSession = nullptr;
|
||||
@@ -3582,12 +3587,16 @@ void RegisterAllHotkeys(HWND hWnd)
|
||||
}
|
||||
if (g_SnipToggleKey) {
|
||||
registerHotkey( SNIP_HOTKEY, g_SnipToggleMod, g_SnipToggleKey & 0xFF );
|
||||
registerHotkey( SNIP_SAVE_HOTKEY, ( g_SnipToggleMod ^ MOD_SHIFT ), g_SnipToggleKey & 0xFF );
|
||||
}
|
||||
if( g_SnipPanoramaToggleKey &&
|
||||
if (g_SnipSaveToggleKey) {
|
||||
registerHotkey( SNIP_SAVE_HOTKEY, g_SnipSaveToggleMod, g_SnipSaveToggleKey & 0xFF);
|
||||
}
|
||||
if (g_SnipPanoramaToggleKey &&
|
||||
(g_SnipPanoramaToggleKey != g_SnipToggleKey || g_SnipPanoramaToggleMod != g_SnipToggleMod) ) {
|
||||
registerHotkey( SNIP_PANORAMA_HOTKEY, g_SnipPanoramaToggleMod | MOD_NOREPEAT, g_SnipPanoramaToggleKey & 0xFF );
|
||||
registerHotkey( SNIP_PANORAMA_SAVE_HOTKEY, ( g_SnipPanoramaToggleMod ^ MOD_SHIFT ) | MOD_NOREPEAT, g_SnipPanoramaToggleKey & 0xFF );
|
||||
}
|
||||
if (g_SnipPanoramaSaveToggleKey) {
|
||||
registerHotkey( SNIP_PANORAMA_SAVE_HOTKEY, g_SnipPanoramaSaveToggleMod | MOD_NOREPEAT, g_SnipPanoramaSaveToggleKey & 0xFF );
|
||||
}
|
||||
if (g_SnipOcrToggleKey) {
|
||||
registerHotkey( SNIP_OCR_HOTKEY, g_SnipOcrToggleMod, g_SnipOcrToggleKey & 0xFF );
|
||||
@@ -4816,6 +4825,8 @@ INT_PTR CALLBACK OptionsProc( HWND hDlg, UINT message,
|
||||
TCHAR text[32];
|
||||
DWORD newToggleKey, newTimeout, newToggleMod, newBreakToggleKey, newDemoTypeToggleKey, newRecordToggleKey, newSnipToggleKey, newSnipPanoramaToggleKey, newSnipOcrToggleKey;
|
||||
DWORD newDrawToggleKey, newDrawToggleMod, newBreakToggleMod, newDemoTypeToggleMod, newRecordToggleMod, newSnipToggleMod, newSnipPanoramaToggleMod, newSnipOcrToggleMod;
|
||||
DWORD newSnipSaveToggleKey, newSnipSaveToggleMod;
|
||||
DWORD newSnipPanoramaSaveToggleKey, newSnipPanoramaSaveToggleMod;
|
||||
DWORD newLiveZoomToggleKey, newLiveZoomToggleMod;
|
||||
static std::vector<std::pair<std::wstring, std::wstring>> microphones;
|
||||
|
||||
@@ -5050,7 +5061,9 @@ INT_PTR CALLBACK OptionsProc( HWND hDlg, UINT message,
|
||||
if( g_DemoTypeToggleKey ) SendMessage( GetDlgItem( g_OptionsTabs[DEMOTYPE_PAGE].hPage, IDC_DEMOTYPE_HOTKEY ), HKM_SETHOTKEY, g_DemoTypeToggleKey, 0 );
|
||||
if( g_RecordToggleKey ) SendMessage( GetDlgItem( g_OptionsTabs[RECORD_PAGE].hPage, IDC_RECORD_HOTKEY), HKM_SETHOTKEY, g_RecordToggleKey, 0 );
|
||||
if( g_SnipToggleKey) SendMessage( GetDlgItem( g_OptionsTabs[SNIP_PAGE].hPage, IDC_SNIP_HOTKEY), HKM_SETHOTKEY, g_SnipToggleKey, 0 );
|
||||
if( g_SnipSaveToggleKey) SendMessage( GetDlgItem( g_OptionsTabs[SNIP_PAGE].hPage, IDC_SNIP_SAVE_HOTKEY), HKM_SETHOTKEY, g_SnipSaveToggleKey, 0 );
|
||||
if( g_SnipPanoramaToggleKey) SendMessage( GetDlgItem( g_OptionsTabs[PANORAMA_PAGE].hPage, IDC_SNIP_PANORAMA_HOTKEY), HKM_SETHOTKEY, g_SnipPanoramaToggleKey, 0 );
|
||||
if( g_SnipPanoramaSaveToggleKey) SendMessage( GetDlgItem( g_OptionsTabs[PANORAMA_PAGE].hPage, IDC_SNIP_PANORAMA_SAVE_HOTKEY), HKM_SETHOTKEY, g_SnipPanoramaSaveToggleKey, 0 );
|
||||
if( g_SnipOcrToggleKey) SendMessage( GetDlgItem( g_OptionsTabs[SNIP_PAGE].hPage, IDC_SNIP_OCR_HOTKEY), HKM_SETHOTKEY, g_SnipOcrToggleKey, 0 );
|
||||
CheckDlgButton( hDlg, IDC_SHOW_TRAY_ICON,
|
||||
g_ShowTrayIcon ? BST_CHECKED: BST_UNCHECKED );
|
||||
@@ -5512,7 +5525,9 @@ INT_PTR CALLBACK OptionsProc( HWND hDlg, UINT message,
|
||||
newDemoTypeToggleKey = static_cast<DWORD>(SendMessage( GetDlgItem( g_OptionsTabs[DEMOTYPE_PAGE].hPage, IDC_DEMOTYPE_HOTKEY ), HKM_GETHOTKEY, 0, 0 ));
|
||||
newRecordToggleKey = static_cast<DWORD>(SendMessage(GetDlgItem(g_OptionsTabs[RECORD_PAGE].hPage, IDC_RECORD_HOTKEY), HKM_GETHOTKEY, 0, 0));
|
||||
newSnipToggleKey = static_cast<DWORD>(SendMessage( GetDlgItem( g_OptionsTabs[SNIP_PAGE].hPage, IDC_SNIP_HOTKEY), HKM_GETHOTKEY, 0, 0 ));
|
||||
newSnipSaveToggleKey = static_cast<DWORD>(SendMessage( GetDlgItem( g_OptionsTabs[SNIP_PAGE].hPage, IDC_SNIP_SAVE_HOTKEY), HKM_GETHOTKEY, 0, 0 ));
|
||||
newSnipPanoramaToggleKey = static_cast<DWORD>(SendMessage( GetDlgItem( g_OptionsTabs[PANORAMA_PAGE].hPage, IDC_SNIP_PANORAMA_HOTKEY), HKM_GETHOTKEY, 0, 0 ));
|
||||
newSnipPanoramaSaveToggleKey = static_cast<DWORD>(SendMessage( GetDlgItem( g_OptionsTabs[PANORAMA_PAGE].hPage, IDC_SNIP_PANORAMA_SAVE_HOTKEY), HKM_GETHOTKEY, 0, 0 ));
|
||||
newSnipOcrToggleKey = static_cast<DWORD>(SendMessage( GetDlgItem( g_OptionsTabs[SNIP_PAGE].hPage, IDC_SNIP_OCR_HOTKEY), HKM_GETHOTKEY, 0, 0 ));
|
||||
|
||||
newToggleMod = GetKeyMod( newToggleKey );
|
||||
@@ -5522,7 +5537,9 @@ INT_PTR CALLBACK OptionsProc( HWND hDlg, UINT message,
|
||||
newDemoTypeToggleMod = GetKeyMod( newDemoTypeToggleKey );
|
||||
newRecordToggleMod = GetKeyMod(newRecordToggleKey);
|
||||
newSnipToggleMod = GetKeyMod( newSnipToggleKey );
|
||||
newSnipSaveToggleMod = GetKeyMod( newSnipSaveToggleKey );
|
||||
newSnipPanoramaToggleMod = GetKeyMod( newSnipPanoramaToggleKey );
|
||||
newSnipPanoramaSaveToggleMod = GetKeyMod( newSnipPanoramaSaveToggleKey );
|
||||
newSnipOcrToggleMod = GetKeyMod( newSnipOcrToggleKey );
|
||||
|
||||
g_SliderZoomLevel = static_cast<int>(SendMessage( GetDlgItem(g_OptionsTabs[ZOOM_PAGE].hPage, IDC_ZOOM_SLIDER), TBM_GETPOS, 0, 0 ));
|
||||
@@ -5591,25 +5608,41 @@ INT_PTR CALLBACK OptionsProc( HWND hDlg, UINT message,
|
||||
|
||||
}
|
||||
else if (newSnipToggleKey &&
|
||||
(!RegisterHotKey(GetParent(hDlg), SNIP_HOTKEY, newSnipToggleMod, newSnipToggleKey & 0xFF) ||
|
||||
!RegisterHotKey(GetParent(hDlg), SNIP_SAVE_HOTKEY, (newSnipToggleMod ^ MOD_SHIFT), newSnipToggleKey & 0xFF))) {
|
||||
!RegisterHotKey(GetParent(hDlg), SNIP_HOTKEY, newSnipToggleMod, newSnipToggleKey & 0xFF)) {
|
||||
|
||||
MessageBox(hDlg, L"The specified snip hotkey is already in use.\nSelect a different snip hotkey.",
|
||||
APPNAME, MB_ICONERROR);
|
||||
UnregisterAllHotkeys(GetParent(hDlg));
|
||||
break;
|
||||
|
||||
}
|
||||
else if (newSnipSaveToggleKey &&
|
||||
!RegisterHotKey(GetParent(hDlg), SNIP_SAVE_HOTKEY, newSnipSaveToggleMod, newSnipSaveToggleKey & 0xFF)) {
|
||||
|
||||
MessageBox(hDlg, L"The specified snip save hotkey is already in use.\nSelect a different snip save hotkey.",
|
||||
APPNAME, MB_ICONERROR);
|
||||
UnregisterAllHotkeys(GetParent(hDlg));
|
||||
break;
|
||||
|
||||
}
|
||||
else if (newSnipPanoramaToggleKey &&
|
||||
(newSnipPanoramaToggleKey != newSnipToggleKey || newSnipPanoramaToggleMod != newSnipToggleMod) &&
|
||||
(!RegisterHotKey(GetParent(hDlg), SNIP_PANORAMA_HOTKEY, newSnipPanoramaToggleMod | MOD_NOREPEAT, newSnipPanoramaToggleKey & 0xFF) ||
|
||||
!RegisterHotKey(GetParent(hDlg), SNIP_PANORAMA_SAVE_HOTKEY, ( newSnipPanoramaToggleMod ^ MOD_SHIFT ) | MOD_NOREPEAT, newSnipPanoramaToggleKey & 0xFF))) {
|
||||
!RegisterHotKey(GetParent(hDlg), SNIP_PANORAMA_HOTKEY, newSnipPanoramaToggleMod | MOD_NOREPEAT, newSnipPanoramaToggleKey & 0xFF)) {
|
||||
|
||||
MessageBox(hDlg, L"The specified panorama snip hotkey is already in use.\nSelect a different panorama snip hotkey.",
|
||||
APPNAME, MB_ICONERROR);
|
||||
UnregisterAllHotkeys(GetParent(hDlg));
|
||||
break;
|
||||
|
||||
}
|
||||
else if (newSnipPanoramaSaveToggleKey &&
|
||||
!RegisterHotKey(GetParent(hDlg), SNIP_PANORAMA_SAVE_HOTKEY, newSnipPanoramaSaveToggleMod | MOD_NOREPEAT, newSnipPanoramaSaveToggleKey & 0xFF)) {
|
||||
|
||||
MessageBox(hDlg, L"The specified panorama snip save hotkey is already in use.\nSelect a different panorama snip save hotkey.",
|
||||
APPNAME, MB_ICONERROR);
|
||||
UnregisterAllHotkeys(GetParent(hDlg));
|
||||
break;
|
||||
|
||||
}
|
||||
else if (newSnipOcrToggleKey &&
|
||||
!RegisterHotKey(GetParent(hDlg), SNIP_OCR_HOTKEY, newSnipOcrToggleMod, newSnipOcrToggleKey & 0xFF)) {
|
||||
@@ -5645,8 +5678,12 @@ INT_PTR CALLBACK OptionsProc( HWND hDlg, UINT message,
|
||||
g_RecordToggleMod = newRecordToggleMod;
|
||||
g_SnipToggleKey = newSnipToggleKey;
|
||||
g_SnipToggleMod = newSnipToggleMod;
|
||||
g_SnipSaveToggleKey = newSnipSaveToggleKey;
|
||||
g_SnipSaveToggleMod = newSnipSaveToggleMod;
|
||||
g_SnipPanoramaToggleKey = newSnipPanoramaToggleKey;
|
||||
g_SnipPanoramaToggleMod = newSnipPanoramaToggleMod;
|
||||
g_SnipPanoramaSaveToggleKey = newSnipPanoramaSaveToggleKey;
|
||||
g_SnipPanoramaSaveToggleMod = newSnipPanoramaSaveToggleMod;
|
||||
g_SnipOcrToggleKey = newSnipOcrToggleKey;
|
||||
g_SnipOcrToggleMod = newSnipOcrToggleMod;
|
||||
reg.WriteRegSettings( RegSettings );
|
||||
@@ -6737,6 +6774,45 @@ void StopRecording()
|
||||
}
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
// GetTimestampSuffix
|
||||
//
|
||||
// Returns a timestamp string for disambiguating filenames.
|
||||
// Format: " YYYY-MM-DD HHMMSS", e.g." 2025-11-02 143000".
|
||||
//
|
||||
// Used as a suffix for the default recording filename. Ensures
|
||||
// chronological name sorting in Explorer.
|
||||
//
|
||||
//----------------------------------------------------------------------------
|
||||
static std::wstring GetTimestampSuffix()
|
||||
{
|
||||
auto const now = std::chrono::system_clock::now();
|
||||
auto const in_time_t = std::chrono::system_clock::to_time_t( now );
|
||||
|
||||
std::tm buf{};
|
||||
localtime_s( &buf, &in_time_t );
|
||||
|
||||
std::wstringstream ss;
|
||||
ss << L" " << std::put_time( &buf, L"%Y-%m-%d %H%M%S" );
|
||||
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
// IsDefaultRecordingFilename
|
||||
//
|
||||
// Determines if the provided filename matches the default recording name.
|
||||
// Case-insensitive comparison.
|
||||
//
|
||||
// Returns:
|
||||
// true if filename is the default; otherwise false.
|
||||
//
|
||||
//----------------------------------------------------------------------------
|
||||
static bool IsDefaultRecordingFilename(const std::wstring& filename)
|
||||
{
|
||||
return CompareStringOrdinal( DEFAULT_RECORDING_FILE, -1, filename.c_str(), -1, TRUE ) == CSTR_EQUAL
|
||||
|| CompareStringOrdinal( DEFAULT_GIF_RECORDING_FILE, -1, filename.c_str(), -1, TRUE ) == CSTR_EQUAL;
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
//
|
||||
@@ -6791,19 +6867,70 @@ std::wstring GetUniqueFilename(const std::wstring& lastSavePath, const wchar_t*
|
||||
//
|
||||
// GetUniqueRecordingFilename
|
||||
//
|
||||
// Gets a unique file name for recording saves, using the " (N)" suffix
|
||||
// approach so that the user can hit OK without worrying about overwriting
|
||||
// if they are making multiple recordings in one session or don't want to
|
||||
// always see an overwrite dialog or stop to clean up files.
|
||||
// Generates a unique filename to be suggested in the "Save As" recording
|
||||
// dialog, based on the user's last chosen filename and save location.
|
||||
// This allows the user to quickly save a recording without worrying about
|
||||
// manual renaming to prevent overwriting earlier recordings.
|
||||
//
|
||||
// There are two distinct behaviors based on the last used filename:
|
||||
//
|
||||
// 1. For the default filename ("Recording.mp4"):
|
||||
// Generates a more descriptive name by appending a timestamp, e.g.
|
||||
// "Recording 2025-11-03 143015.mp4". This ensures chronological sorting
|
||||
// in Explorer when ordered by name and is consistent with other tools.
|
||||
//
|
||||
// 2. For custom filenames (e.g. "Presentation.mp4"):
|
||||
// Appends a numeric suffix if the file already exists, e.g.
|
||||
// "Presentation (1).mp4", "Presentation (2).mp4", etc.
|
||||
//
|
||||
// Returns:
|
||||
// A unique filename (without folder path).
|
||||
//
|
||||
// Relies upon the global state of `g_RecordingSaveLocation` and
|
||||
// `g_RecordingSaveBaseFilename`.
|
||||
//
|
||||
//----------------------------------------------------------------------------
|
||||
auto GetUniqueRecordingFilename()
|
||||
static auto GetUniqueRecordingFilename()
|
||||
{
|
||||
const wchar_t* defaultFile = (g_RecordingFormat == RecordingFormat::GIF)
|
||||
? DEFAULT_GIF_RECORDING_FILE
|
||||
: DEFAULT_RECORDING_FILE;
|
||||
|
||||
return GetUniqueFilename(g_RecordingSaveLocation, defaultFile, FOLDERID_Videos);
|
||||
// Without a remembered filename, suggest the default name for the current format.
|
||||
std::wstring baseFilename = g_RecordingSaveBaseFilename.empty()
|
||||
? std::wstring( defaultFile )
|
||||
: g_RecordingSaveBaseFilename;
|
||||
|
||||
std::filesystem::path basePath{ baseFilename };
|
||||
|
||||
// For the default filename, append a timestamp so successive default saves stay
|
||||
// unique and sort chronologically in Explorer.
|
||||
if ( IsDefaultRecordingFilename( basePath.filename().wstring() ) )
|
||||
{
|
||||
return basePath.stem().wstring() + GetTimestampSuffix() + basePath.extension().wstring();
|
||||
}
|
||||
|
||||
// For custom filenames, append a numeric suffix to avoid collisions.
|
||||
std::filesystem::path directory;
|
||||
if ( !g_RecordingSaveLocation.empty() )
|
||||
directory = std::filesystem::path( g_RecordingSaveLocation ).parent_path();
|
||||
if ( directory.empty() )
|
||||
{
|
||||
wil::unique_cotaskmem_string folderPath;
|
||||
if ( SUCCEEDED( SHGetKnownFolderPath( FOLDERID_Videos, KF_FLAG_DEFAULT, nullptr, folderPath.put() ) ) )
|
||||
directory = folderPath.get();
|
||||
}
|
||||
|
||||
std::wstring baseStem = basePath.stem().wstring();
|
||||
std::wstring baseExtension = basePath.extension().wstring();
|
||||
|
||||
std::filesystem::path testPath = directory / ( baseStem + baseExtension );
|
||||
for ( int index = 1; std::filesystem::exists( testPath ); index++ )
|
||||
{
|
||||
testPath = directory / ( baseStem + L" (" + std::to_wstring( index ) + L')' + baseExtension );
|
||||
}
|
||||
|
||||
return testPath.filename().wstring();
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
@@ -6835,7 +6962,7 @@ auto GetUniqueScreenshotFilename()
|
||||
//
|
||||
// StartRecordingAsync
|
||||
//
|
||||
// Starts the screen recording.
|
||||
// Initiates screen recording and handles the save dialog workflow.
|
||||
//
|
||||
//----------------------------------------------------------------------------
|
||||
winrt::fire_and_forget StartRecordingAsync( HWND hWnd, LPRECT rcCrop, HWND hWndRecord ) try
|
||||
@@ -7080,8 +7207,30 @@ winrt::fire_and_forget StartRecordingAsync( HWND hWnd, LPRECT rcCrop, HWND hWndR
|
||||
if (!finalPath.empty())
|
||||
{
|
||||
auto path = std::filesystem::path(finalPath);
|
||||
|
||||
// Remember the user's chosen filename and apply a timestamp to default
|
||||
// names so successive saves stay unique and sort chronologically.
|
||||
std::wstring filename = path.filename().wstring();
|
||||
std::wstring finalFilename = filename;
|
||||
if ( IsDefaultRecordingFilename( filename ) )
|
||||
{
|
||||
// The user accepted or re-typed the default filename. Remember it so the
|
||||
// next suggestion also uses a timestamp, and append one to this save.
|
||||
g_RecordingSaveBaseFilename = filename;
|
||||
finalFilename = path.stem().wstring() + GetTimestampSuffix() + path.extension().wstring();
|
||||
}
|
||||
else if ( CompareStringOrdinal( suggestedName.c_str(), -1, filename.c_str(), -1, TRUE ) != CSTR_EQUAL )
|
||||
{
|
||||
// The user chose their own filename instead of the suggested one. Remember
|
||||
// it so future suggestions use numeric suffixes based on this name.
|
||||
g_RecordingSaveBaseFilename = filename;
|
||||
}
|
||||
|
||||
// The path actually written to disk (with any timestamp applied).
|
||||
std::wstring savedPath = ( path.parent_path() / finalFilename ).wstring();
|
||||
|
||||
winrt::StorageFolder folder{ co_await winrt::StorageFolder::GetFolderFromPathAsync(path.parent_path().c_str()) };
|
||||
destFile = co_await folder.CreateFileAsync(path.filename().c_str(), winrt::CreationCollisionOption::ReplaceExisting);
|
||||
destFile = co_await folder.CreateFileAsync(finalFilename.c_str(), winrt::CreationCollisionOption::ReplaceExisting);
|
||||
|
||||
// If user trimmed, use the trimmed file
|
||||
winrt::StorageFile sourceFile = file;
|
||||
@@ -7099,8 +7248,8 @@ winrt::fire_and_forget StartRecordingAsync( HWND hWnd, LPRECT rcCrop, HWND hWndR
|
||||
try { co_await file.DeleteAsync(); } catch (...) {}
|
||||
}
|
||||
|
||||
// Use finalPath directly - destFile.Path() may be stale after MoveAndReplaceAsync
|
||||
g_RecordingSaveLocation = finalPath;
|
||||
// Use savedPath directly - destFile.Path() may be stale after MoveAndReplaceAsync
|
||||
g_RecordingSaveLocation = savedPath;
|
||||
// Update the registry buffer and save to persist across app restarts
|
||||
wcsncpy_s(g_RecordingSaveLocationBuffer, g_RecordingSaveLocation.c_str(), _TRUNCATE);
|
||||
reg.WriteRegSettings(RegSettings);
|
||||
@@ -7600,7 +7749,9 @@ LRESULT APIENTRY MainWndProc(
|
||||
g_BreakToggleMod = GetKeyMod( g_BreakToggleKey );
|
||||
g_DemoTypeToggleMod = GetKeyMod( g_DemoTypeToggleKey );
|
||||
g_SnipToggleMod = GetKeyMod( g_SnipToggleKey );
|
||||
g_SnipSaveToggleMod = GetKeyMod( g_SnipSaveToggleKey );
|
||||
g_SnipPanoramaToggleMod = GetKeyMod( g_SnipPanoramaToggleKey );
|
||||
g_SnipPanoramaSaveToggleMod = GetKeyMod( g_SnipPanoramaSaveToggleKey );
|
||||
g_SnipOcrToggleMod = GetKeyMod( g_SnipOcrToggleKey );
|
||||
g_RecordToggleMod = GetKeyMod( g_RecordToggleKey );
|
||||
|
||||
@@ -7651,23 +7802,37 @@ LRESULT APIENTRY MainWndProc(
|
||||
|
||||
}
|
||||
else if (g_SnipToggleKey &&
|
||||
(!RegisterHotKey(hWnd, SNIP_HOTKEY, g_SnipToggleMod, g_SnipToggleKey & 0xFF) ||
|
||||
!RegisterHotKey(hWnd, SNIP_SAVE_HOTKEY, (g_SnipToggleMod ^ MOD_SHIFT), g_SnipToggleKey & 0xFF))) {
|
||||
!RegisterHotKey(hWnd, SNIP_HOTKEY, g_SnipToggleMod, g_SnipToggleKey & 0xFF)) {
|
||||
|
||||
MessageBox(hWnd, L"The specified snip hotkey is already in use.\nSelect a different snip hotkey.",
|
||||
APPNAME, MB_ICONERROR);
|
||||
showOptions = TRUE;
|
||||
|
||||
}
|
||||
else if (g_SnipSaveToggleKey &&
|
||||
!RegisterHotKey(hWnd, SNIP_SAVE_HOTKEY, g_SnipSaveToggleMod, g_SnipSaveToggleKey & 0xFF)) {
|
||||
|
||||
MessageBox(hWnd, L"The specified snip save hotkey is already in use.\nSelect a different snip save hotkey.",
|
||||
APPNAME, MB_ICONERROR);
|
||||
showOptions = TRUE;
|
||||
|
||||
}
|
||||
else if (g_SnipPanoramaToggleKey &&
|
||||
(g_SnipPanoramaToggleKey != g_SnipToggleKey || g_SnipPanoramaToggleMod != g_SnipToggleMod) &&
|
||||
(!RegisterHotKey(hWnd, SNIP_PANORAMA_HOTKEY, g_SnipPanoramaToggleMod | MOD_NOREPEAT, g_SnipPanoramaToggleKey & 0xFF) ||
|
||||
!RegisterHotKey(hWnd, SNIP_PANORAMA_SAVE_HOTKEY, ( g_SnipPanoramaToggleMod ^ MOD_SHIFT ) | MOD_NOREPEAT, g_SnipPanoramaToggleKey & 0xFF))) {
|
||||
!RegisterHotKey(hWnd, SNIP_PANORAMA_HOTKEY, g_SnipPanoramaToggleMod | MOD_NOREPEAT, g_SnipPanoramaToggleKey & 0xFF)) {
|
||||
|
||||
MessageBox(hWnd, L"The specified panorama snip hotkey is already in use.\nSelect a different panorama snip hotkey.",
|
||||
APPNAME, MB_ICONERROR);
|
||||
showOptions = TRUE;
|
||||
|
||||
}
|
||||
else if (g_SnipPanoramaSaveToggleKey &&
|
||||
!RegisterHotKey(hWnd, SNIP_PANORAMA_SAVE_HOTKEY, g_SnipPanoramaSaveToggleMod | MOD_NOREPEAT, g_SnipPanoramaSaveToggleKey & 0xFF)) {
|
||||
|
||||
MessageBox(hWnd, L"The specified panorama snip save hotkey is already in use.\nSelect a different panorama snip save hotkey.",
|
||||
APPNAME, MB_ICONERROR);
|
||||
showOptions = TRUE;
|
||||
|
||||
}
|
||||
else if (g_SnipOcrToggleKey &&
|
||||
!RegisterHotKey(hWnd, SNIP_OCR_HOTKEY, g_SnipOcrToggleMod, g_SnipOcrToggleKey & 0xFF)) {
|
||||
@@ -10254,7 +10419,9 @@ LRESULT APIENTRY MainWndProc(
|
||||
g_BreakToggleMod = GetKeyMod(g_BreakToggleKey);
|
||||
g_DemoTypeToggleMod = GetKeyMod(g_DemoTypeToggleKey);
|
||||
g_SnipToggleMod = GetKeyMod(g_SnipToggleKey);
|
||||
g_SnipSaveToggleMod = GetKeyMod(g_SnipSaveToggleKey);
|
||||
g_SnipPanoramaToggleMod = GetKeyMod(g_SnipPanoramaToggleKey);
|
||||
g_SnipPanoramaSaveToggleMod = GetKeyMod(g_SnipPanoramaSaveToggleKey);
|
||||
g_SnipOcrToggleMod = GetKeyMod(g_SnipOcrToggleKey);
|
||||
g_RecordToggleMod = GetKeyMod(g_RecordToggleKey);
|
||||
BOOL showOptions = FALSE;
|
||||
@@ -10317,8 +10484,7 @@ LRESULT APIENTRY MainWndProc(
|
||||
}
|
||||
if (g_SnipToggleKey)
|
||||
{
|
||||
if (!RegisterHotKey(hWnd, SNIP_HOTKEY, g_SnipToggleMod, g_SnipToggleKey & 0xFF) ||
|
||||
!RegisterHotKey(hWnd, SNIP_SAVE_HOTKEY, (g_SnipToggleMod ^ MOD_SHIFT), g_SnipToggleKey & 0xFF))
|
||||
if (!RegisterHotKey(hWnd, SNIP_HOTKEY, g_SnipToggleMod, g_SnipToggleKey & 0xFF))
|
||||
{
|
||||
if(!g_StartedByPowerToys)
|
||||
{
|
||||
@@ -10327,11 +10493,21 @@ LRESULT APIENTRY MainWndProc(
|
||||
showOptions = TRUE;
|
||||
}
|
||||
}
|
||||
if (g_SnipSaveToggleKey)
|
||||
{
|
||||
if (!RegisterHotKey(hWnd, SNIP_SAVE_HOTKEY, g_SnipSaveToggleMod, g_SnipSaveToggleKey & 0xFF))
|
||||
{
|
||||
if(!g_StartedByPowerToys)
|
||||
{
|
||||
MessageBox(hWnd, L"The specified snip save hotkey is already in use.\nSelect a different snip save hotkey.", APPNAME, MB_ICONERROR);
|
||||
}
|
||||
showOptions = TRUE;
|
||||
}
|
||||
}
|
||||
if (g_SnipPanoramaToggleKey &&
|
||||
(g_SnipPanoramaToggleKey != g_SnipToggleKey || g_SnipPanoramaToggleMod != g_SnipToggleMod))
|
||||
{
|
||||
if (!RegisterHotKey(hWnd, SNIP_PANORAMA_HOTKEY, g_SnipPanoramaToggleMod | MOD_NOREPEAT, g_SnipPanoramaToggleKey & 0xFF) ||
|
||||
!RegisterHotKey(hWnd, SNIP_PANORAMA_SAVE_HOTKEY, ( g_SnipPanoramaToggleMod ^ MOD_SHIFT ) | MOD_NOREPEAT, g_SnipPanoramaToggleKey & 0xFF))
|
||||
if (!RegisterHotKey(hWnd, SNIP_PANORAMA_HOTKEY, g_SnipPanoramaToggleMod | MOD_NOREPEAT, g_SnipPanoramaToggleKey & 0xFF))
|
||||
{
|
||||
if(!g_StartedByPowerToys)
|
||||
{
|
||||
@@ -10340,6 +10516,17 @@ LRESULT APIENTRY MainWndProc(
|
||||
showOptions = TRUE;
|
||||
}
|
||||
}
|
||||
if (g_SnipPanoramaSaveToggleKey)
|
||||
{
|
||||
if (!RegisterHotKey(hWnd, SNIP_PANORAMA_SAVE_HOTKEY, g_SnipPanoramaSaveToggleMod | MOD_NOREPEAT, g_SnipPanoramaSaveToggleKey & 0xFF))
|
||||
{
|
||||
if(!g_StartedByPowerToys)
|
||||
{
|
||||
MessageBox(hWnd, L"The specified panorama snip save hotkey is already in use.\nSelect a different panorama snip save hotkey.", APPNAME, MB_ICONERROR);
|
||||
}
|
||||
showOptions = TRUE;
|
||||
}
|
||||
}
|
||||
if (g_SnipOcrToggleKey)
|
||||
{
|
||||
if (!RegisterHotKey(hWnd, SNIP_OCR_HOTKEY, g_SnipOcrToggleMod, g_SnipOcrToggleKey & 0xFF))
|
||||
|
||||
@@ -93,7 +93,6 @@
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <future>
|
||||
#include <regex>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
|
||||
|
||||
@@ -137,6 +137,8 @@
|
||||
#define IDC_WEBCAM_BRIGHTNESS_LABEL 1131
|
||||
#define IDC_WEBCAM_BRIGHTNESS_SLIDER 1132
|
||||
#define IDC_NOISE_CANCELLATION 1133
|
||||
#define IDC_SNIP_SAVE_HOTKEY 1134
|
||||
#define IDC_SNIP_PANORAMA_SAVE_HOTKEY 1135
|
||||
#define IDC_SAVE 40002
|
||||
#define IDC_COPY 40004
|
||||
#define IDC_RECORD 40006
|
||||
@@ -151,8 +153,8 @@
|
||||
#ifdef APSTUDIO_INVOKED
|
||||
#ifndef APSTUDIO_READONLY_SYMBOLS
|
||||
#define _APS_NEXT_RESOURCE_VALUE 120
|
||||
#define _APS_NEXT_COMMAND_VALUE 40015
|
||||
#define _APS_NEXT_CONTROL_VALUE 1134
|
||||
#define _APS_NEXT_COMMAND_VALUE 40012
|
||||
#define _APS_NEXT_CONTROL_VALUE 1136
|
||||
#define _APS_NEXT_SYMED_VALUE 101
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -70,8 +70,10 @@ namespace winrt::PowerToys::ZoomItSettingsInterop::implementation
|
||||
{ L"DrawToggleKey", SPECIAL_SEMANTICS_SHORTCUT },
|
||||
{ L"RecordToggleKey", SPECIAL_SEMANTICS_SHORTCUT },
|
||||
{ L"SnipToggleKey", SPECIAL_SEMANTICS_SHORTCUT },
|
||||
{ L"SnipSaveToggleKey", SPECIAL_SEMANTICS_SHORTCUT },
|
||||
{ L"SnipOcrToggleKey", SPECIAL_SEMANTICS_SHORTCUT },
|
||||
{ L"SnipPanoramaToggleKey", SPECIAL_SEMANTICS_SHORTCUT },
|
||||
{ L"SnipPanoramaSaveToggleKey", SPECIAL_SEMANTICS_SHORTCUT },
|
||||
{ L"BreakTimerKey", SPECIAL_SEMANTICS_SHORTCUT },
|
||||
{ L"DemoTypeToggleKey", SPECIAL_SEMANTICS_SHORTCUT },
|
||||
{ L"PenColor", SPECIAL_SEMANTICS_COLOR },
|
||||
|
||||
@@ -160,7 +160,7 @@ bool WindowBorder::Init(HINSTANCE hinstance)
|
||||
|
||||
void WindowBorder::UpdateBorderPosition() const
|
||||
{
|
||||
if (!m_trackingWindow)
|
||||
if (!m_trackingWindow || !m_frameDrawer || !m_window)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
// 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.Text;
|
||||
|
||||
namespace Microsoft.CmdPal.Common.Helpers;
|
||||
|
||||
public static class ShellArgumentBuilder
|
||||
{
|
||||
public static string BuildArguments(params string[] arguments)
|
||||
{
|
||||
if (arguments.Length <= 0)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var stringBuilder = new StringBuilder();
|
||||
foreach (var argument in arguments)
|
||||
{
|
||||
AppendArgument(stringBuilder, argument);
|
||||
}
|
||||
|
||||
return stringBuilder.ToString();
|
||||
}
|
||||
|
||||
private static void AppendArgument(StringBuilder stringBuilder, string argument)
|
||||
{
|
||||
if (stringBuilder.Length > 0)
|
||||
{
|
||||
stringBuilder.Append(' ');
|
||||
}
|
||||
|
||||
if (argument.Length == 0 || ShouldBeQuoted(argument))
|
||||
{
|
||||
stringBuilder.Append('"');
|
||||
var index = 0;
|
||||
while (index < argument.Length)
|
||||
{
|
||||
var c = argument[index++];
|
||||
if (c == '\\')
|
||||
{
|
||||
var numBackSlash = 1;
|
||||
while (index < argument.Length && argument[index] == '\\')
|
||||
{
|
||||
index++;
|
||||
numBackSlash++;
|
||||
}
|
||||
|
||||
if (index == argument.Length)
|
||||
{
|
||||
stringBuilder.Append('\\', numBackSlash * 2);
|
||||
}
|
||||
else if (argument[index] == '"')
|
||||
{
|
||||
stringBuilder.Append('\\', (numBackSlash * 2) + 1);
|
||||
stringBuilder.Append('"');
|
||||
index++;
|
||||
}
|
||||
else
|
||||
{
|
||||
stringBuilder.Append('\\', numBackSlash);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c == '"')
|
||||
{
|
||||
stringBuilder.Append('\\');
|
||||
stringBuilder.Append('"');
|
||||
continue;
|
||||
}
|
||||
|
||||
stringBuilder.Append(c);
|
||||
}
|
||||
|
||||
stringBuilder.Append('"');
|
||||
}
|
||||
else
|
||||
{
|
||||
stringBuilder.Append(argument);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool ShouldBeQuoted(string argument)
|
||||
{
|
||||
foreach (var c in argument)
|
||||
{
|
||||
if (char.IsWhiteSpace(c) || c == '"')
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -94,6 +94,7 @@ internal sealed partial class HttpCachingClient : IDisposable
|
||||
public void Dispose()
|
||||
{
|
||||
_httpClient.Dispose();
|
||||
_cacheHandler.Dispose();
|
||||
}
|
||||
|
||||
private static bool IsSupportedHttpUri(Uri resourceUri)
|
||||
|
||||
@@ -146,13 +146,7 @@ public sealed partial class MainListPage : DynamicListPage,
|
||||
// The all apps page will kick off a BG thread to start loading apps.
|
||||
// We just want to know when it is done.
|
||||
var allApps = AllAppsCommandProvider.Page;
|
||||
allApps.PropChanged += (s, p) =>
|
||||
{
|
||||
if (p.PropertyName == nameof(allApps.IsLoading))
|
||||
{
|
||||
IsLoading = ActuallyLoading();
|
||||
}
|
||||
};
|
||||
allApps.PropChanged += AllApps_PropChanged;
|
||||
|
||||
WeakReferenceMessenger.Default.Register<ClearSearchMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<UpdateFallbackItemsMessage>(this);
|
||||
@@ -172,6 +166,14 @@ public sealed partial class MainListPage : DynamicListPage,
|
||||
}
|
||||
}
|
||||
|
||||
private void AllApps_PropChanged(object? sender, IPropChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName == nameof(AllAppsCommandProvider.Page.IsLoading))
|
||||
{
|
||||
IsLoading = ActuallyLoading();
|
||||
}
|
||||
}
|
||||
|
||||
private void PinnedCommands_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
_defaultViewDirty = true;
|
||||
@@ -443,7 +445,7 @@ public sealed partial class MainListPage : DynamicListPage,
|
||||
{
|
||||
specialFallbacks.Add(s);
|
||||
}
|
||||
else
|
||||
else if (s.IsEnabled)
|
||||
{
|
||||
commonFallbacks.Add(s);
|
||||
}
|
||||
@@ -782,6 +784,8 @@ public sealed partial class MainListPage : DynamicListPage,
|
||||
_tlcManager.TopLevelCommands.CollectionChanged -= Commands_CollectionChanged;
|
||||
_tlcManager.PinnedCommands.CollectionChanged -= PinnedCommands_CollectionChanged;
|
||||
|
||||
AllAppsCommandProvider.Page.PropChanged -= AllApps_PropChanged;
|
||||
|
||||
if (_settingsService is not null)
|
||||
{
|
||||
_settingsService.SettingsChanged -= SettingsChangedHandler;
|
||||
|
||||
@@ -105,6 +105,13 @@ public sealed partial class ExtensionGalleryItemViewModel : ObservableObject
|
||||
|
||||
public string? Homepage => _entry.Homepage;
|
||||
|
||||
// Validated, browser-openable homepage uri. Null when the entry has no
|
||||
// homepage or it is not a web uri. NavigateUri bindings must use this
|
||||
// (a Uri) rather than the raw Homepage string: x:Bind evaluates bindings
|
||||
// regardless of element visibility, and converting a null/invalid string
|
||||
// to Uri throws and crashes the page.
|
||||
public Uri? HomepageUri => _homepageHttpUri;
|
||||
|
||||
public Uri IconUri { get; }
|
||||
|
||||
public ImageSource IconSource
|
||||
|
||||
@@ -541,6 +541,9 @@ public partial class WinRTExtensionService : IExtensionService, IDisposable
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_catalog.PackageInstalling -= Catalog_PackageInstalling;
|
||||
_catalog.PackageUninstalling -= Catalog_PackageUninstalling;
|
||||
_catalog.PackageUpdating -= Catalog_PackageUpdating;
|
||||
_getInstalledExtensionsLock.Dispose();
|
||||
}
|
||||
|
||||
|
||||
@@ -94,6 +94,7 @@ internal sealed partial class BlurImageControl : Control
|
||||
private SpriteVisual? _effectVisual;
|
||||
private CompositionEffectBrush? _effectBrush;
|
||||
private CompositionSurfaceBrush? _imageBrush;
|
||||
private LoadedImageSurface? _lastLoadedSurface;
|
||||
|
||||
public BlurImageControl()
|
||||
{
|
||||
@@ -379,10 +380,20 @@ internal sealed partial class BlurImageControl : Control
|
||||
}
|
||||
|
||||
Logger.LogDebug($"Starting load of BlurImageControl from '{bitmapImage.UriSource}'");
|
||||
|
||||
// Each call to LoadImageAsync creates a new LoadedImageSurface backed by native
|
||||
// composition resources. The old surface becomes unrooted once the brush points at
|
||||
// the new one, so it isn't leaked, but dispose it explicitly so the unmanaged
|
||||
// resources are released deterministically instead of waiting for finalization.
|
||||
var previousSurface = _lastLoadedSurface;
|
||||
|
||||
var loadedSurface = LoadedImageSurface.StartLoadFromUri(bitmapImage.UriSource);
|
||||
_lastLoadedSurface = loadedSurface;
|
||||
loadedSurface.LoadCompleted += OnLoadedSurfaceOnLoadCompleted;
|
||||
SetLoadedSurfaceToBrush(loadedSurface);
|
||||
_effectBrush?.SetSourceParameter(ImageSourceParameterName, _imageBrush);
|
||||
|
||||
previousSurface?.Dispose();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -156,8 +156,9 @@ public sealed partial class CommandBar : UserControl,
|
||||
|
||||
private void ContextMenuFlyout_Opened(object sender, object e)
|
||||
{
|
||||
// We need to wait until our flyout is opened to try and toss focus
|
||||
// at its search box. The control isn't in the UI tree before that
|
||||
// Focus the filter box so the flyout captures keyboard input,
|
||||
// then fire a single consolidated Narrator announcement.
|
||||
ContextControl.FocusSearchBox();
|
||||
ContextControl.AnnounceOpened();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,9 @@ using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Automation;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Windows.System;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Controls;
|
||||
|
||||
@@ -22,6 +24,7 @@ public sealed partial class ContentFormControl : UserControl
|
||||
// tree. If this gets GC'ed, then it'll revoke our Action handler, and the
|
||||
// form will do seemingly nothing.
|
||||
private RenderedAdaptiveCard? _renderedCard;
|
||||
private AdaptiveCard? _adaptiveCard;
|
||||
|
||||
public ContentFormViewModel? ViewModel { get => _viewModel; set => AttachViewModel(value); }
|
||||
|
||||
@@ -95,9 +98,11 @@ public sealed partial class ContentFormControl : UserControl
|
||||
private void DisplayCard(AdaptiveCardParseResult result)
|
||||
{
|
||||
_renderedCard = _renderer.RenderAdaptiveCard(result.AdaptiveCard);
|
||||
_adaptiveCard = result.AdaptiveCard;
|
||||
ContentGrid.Children.Clear();
|
||||
if (_renderedCard.FrameworkElement is not null)
|
||||
{
|
||||
_renderedCard.FrameworkElement.KeyDown += OnFormKeyDown;
|
||||
ContentGrid.Children.Add(_renderedCard.FrameworkElement);
|
||||
|
||||
// Use the Loaded event to ensure we focus after the card is in the visual tree
|
||||
@@ -114,8 +119,9 @@ public sealed partial class ContentFormControl : UserControl
|
||||
|
||||
private void OnFrameworkElementLayoutUpdated(object? sender, object e)
|
||||
{
|
||||
// Only fix once — unhook after first layout pass
|
||||
if (_renderedCard?.FrameworkElement is FrameworkElement element)
|
||||
// Only fix once — unhook from sender (not _renderedCard, which may have been
|
||||
// reassigned by the time this fires).
|
||||
if (sender is FrameworkElement element)
|
||||
{
|
||||
element.LayoutUpdated -= OnFrameworkElementLayoutUpdated;
|
||||
FixToggleAccessibilityNames(element);
|
||||
@@ -276,6 +282,50 @@ public sealed partial class ContentFormControl : UserControl
|
||||
return null;
|
||||
}
|
||||
|
||||
private void OnFormKeyDown(object sender, KeyRoutedEventArgs e)
|
||||
{
|
||||
// Snapshot the fields so a subsequent DisplayCard call can't swap the
|
||||
// rendered/parsed card out from under us mid-method. This keeps the
|
||||
// resolved submit action and the gathered inputs from the same card.
|
||||
var renderedCard = _renderedCard;
|
||||
var adaptiveCard = _adaptiveCard;
|
||||
|
||||
if (e.Key != VirtualKey.Enter || renderedCard == null || adaptiveCard == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Only submit when Enter is pressed inside a single-line TextBox
|
||||
if (e.OriginalSource is TextBox textBox && !textBox.AcceptsReturn)
|
||||
{
|
||||
// Find the first Submit or Execute action on the card
|
||||
IAdaptiveActionElement? submitAction = null;
|
||||
foreach (var action in adaptiveCard.Actions)
|
||||
{
|
||||
if (action is AdaptiveSubmitAction or AdaptiveExecuteAction)
|
||||
{
|
||||
submitAction = action;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (submitAction != null)
|
||||
{
|
||||
e.Handled = true;
|
||||
|
||||
// Validate (and gather) the inputs before submitting. AsJson() only
|
||||
// returns the values cached by a successful ValidateInputs() call, so
|
||||
// skipping this would submit an empty payload. This mirrors what the
|
||||
// renderer does internally when a submit button is clicked.
|
||||
var inputs = renderedCard.UserInputs;
|
||||
if (inputs.ValidateInputs(submitAction))
|
||||
{
|
||||
ViewModel?.HandleSubmit(submitAction, inputs.AsJson());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Rendered_Action(RenderedAdaptiveCard sender, AdaptiveActionEventArgs args) =>
|
||||
ViewModel?.HandleSubmit(args.Action, args.Inputs.AsJson());
|
||||
}
|
||||
|
||||
@@ -139,10 +139,23 @@
|
||||
<RowDefinition />
|
||||
<RowDefinition />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!--
|
||||
Hidden element used solely for raising Narrator notifications.
|
||||
It must be Content-visible in UIA but has no visual presence.
|
||||
-->
|
||||
<TextBlock
|
||||
x:Name="NarratorAnnouncer"
|
||||
Width="0"
|
||||
Height="0"
|
||||
AutomationProperties.AccessibilityView="Content"
|
||||
AutomationProperties.LiveSetting="Assertive" />
|
||||
|
||||
<ListView
|
||||
x:Name="CommandsDropdown"
|
||||
MinWidth="248"
|
||||
Margin="0,4,0,2"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
IsItemClickEnabled="True"
|
||||
ItemClick="CommandsDropdown_ItemClick"
|
||||
ItemTemplateSelector="{StaticResource ContextItemTemplateSelector}"
|
||||
@@ -168,6 +181,7 @@
|
||||
x:Uid="ContextFilterBox"
|
||||
Margin="0"
|
||||
Padding="10,7,6,8"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
Background="{ThemeResource AcrylicBackgroundFillColorBaseBrush}"
|
||||
BorderThickness="0,0,0,2"
|
||||
CornerRadius="8, 8, 0, 0"
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
// 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.Globalization;
|
||||
using System.Text;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using CommunityToolkit.WinUI;
|
||||
using Microsoft.CmdPal.Common.Text;
|
||||
@@ -11,6 +13,7 @@ using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Automation.Peers;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
using Windows.System;
|
||||
@@ -27,6 +30,15 @@ public sealed partial class ContextMenu : UserControl,
|
||||
public static readonly DependencyProperty SubscribeToCommandBarProperty =
|
||||
DependencyProperty.Register(nameof(SubscribeToCommandBar), typeof(bool), typeof(ContextMenu), new PropertyMetadata(true, OnSubscribeToCommandBarChanged));
|
||||
|
||||
private static readonly CompositeFormat _contextMenuOpenedFormat =
|
||||
CompositeFormat.Parse(ResourceLoaderInstance.GetString("ScreenReader_Announcement_ContextMenuOpened"));
|
||||
|
||||
/// <summary>
|
||||
/// True while the context menu is transitioning from PrepareForOpen to AnnounceOpened.
|
||||
/// Prevents ViewModel_PropertyChanged from triggering UIA-visible selection changes.
|
||||
/// </summary>
|
||||
private bool _isOpening;
|
||||
|
||||
public bool ShowFilterBox
|
||||
{
|
||||
get => (bool)GetValue(ShowFilterBoxProperty);
|
||||
@@ -103,12 +115,47 @@ public sealed partial class ContextMenu : UserControl,
|
||||
|
||||
internal void PrepareForOpen(ContextMenuFilterLocation filterLocation)
|
||||
{
|
||||
_isOpening = true;
|
||||
|
||||
ViewModel.FilterOnTop = filterLocation == ContextMenuFilterLocation.Top;
|
||||
ViewModel.ResetContextMenu();
|
||||
|
||||
UpdateUiForStackChange();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fires a single consolidated Narrator announcement.
|
||||
/// Call this after the flyout is opened and focus has been set.
|
||||
/// </summary>
|
||||
internal void AnnounceOpened()
|
||||
{
|
||||
// Defer the announcement to the next dispatcher cycle. This ensures
|
||||
// any pending FilteredItems updates have completed and the flyout
|
||||
// content is fully materialized in the UIA tree.
|
||||
DispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
_isOpening = false;
|
||||
|
||||
var commandItems = ViewModel.FilteredItems.OfType<CommandContextItemViewModel>().ToList();
|
||||
var itemCount = commandItems.Count;
|
||||
var selectedItem = CommandsDropdown.SelectedItem as CommandContextItemViewModel;
|
||||
var selectedName = selectedItem?.Title ?? string.Empty;
|
||||
var selectedIndex = selectedItem is not null ? commandItems.IndexOf(selectedItem) + 1 : 0;
|
||||
|
||||
var announcement = string.Format(
|
||||
CultureInfo.CurrentCulture,
|
||||
_contextMenuOpenedFormat,
|
||||
itemCount,
|
||||
selectedName,
|
||||
selectedIndex);
|
||||
|
||||
RaiseNarratorNotification(
|
||||
AutomationNotificationKind.ActionCompleted,
|
||||
announcement,
|
||||
"ContextMenuOpened");
|
||||
});
|
||||
}
|
||||
|
||||
public void Receive(UpdateCommandBarMessage message)
|
||||
{
|
||||
UpdateUiForStackChange();
|
||||
@@ -197,7 +244,7 @@ public sealed partial class ContextMenu : UserControl,
|
||||
{
|
||||
var prop = e.PropertyName;
|
||||
|
||||
if (prop == nameof(ContextMenuViewModel.FilteredItems))
|
||||
if (prop == nameof(ContextMenuViewModel.FilteredItems) && !_isOpening)
|
||||
{
|
||||
UpdateUiForStackChange();
|
||||
}
|
||||
@@ -255,12 +302,14 @@ public sealed partial class ContextMenu : UserControl,
|
||||
if (e.Key == VirtualKey.Up)
|
||||
{
|
||||
NavigateUp();
|
||||
AnnounceSelectedItem();
|
||||
|
||||
e.Handled = true;
|
||||
}
|
||||
else if (e.Key == VirtualKey.Down)
|
||||
{
|
||||
NavigateDown();
|
||||
AnnounceSelectedItem();
|
||||
|
||||
e.Handled = true;
|
||||
}
|
||||
@@ -347,6 +396,46 @@ public sealed partial class ContextMenu : UserControl,
|
||||
return item is SeparatorViewModel;
|
||||
}
|
||||
|
||||
private void AnnounceSelectedItem()
|
||||
{
|
||||
if (CommandsDropdown.SelectedItem is not CommandContextItemViewModel selected)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var commandItems = ViewModel.FilteredItems.OfType<CommandContextItemViewModel>().ToList();
|
||||
var position = commandItems.IndexOf(selected) + 1;
|
||||
var total = commandItems.Count;
|
||||
var announcement = $"{selected.Title}, {position} of {total}";
|
||||
|
||||
RaiseNarratorNotification(
|
||||
AutomationNotificationKind.ItemAdded,
|
||||
announcement,
|
||||
"ContextMenuSelectionChanged");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raises a UIA notification via the dedicated NarratorAnnouncer element.
|
||||
/// Ensures the element has a peer (forcing layout if needed on first use).
|
||||
/// </summary>
|
||||
private void RaiseNarratorNotification(AutomationNotificationKind kind, string announcement, string activityId)
|
||||
{
|
||||
// On first flyout open the announcer may not have a peer yet.
|
||||
// UpdateLayout ensures the element is materialized in the UIA tree.
|
||||
var peer = FrameworkElementAutomationPeer.FromElement(NarratorAnnouncer);
|
||||
if (peer is null)
|
||||
{
|
||||
NarratorAnnouncer.UpdateLayout();
|
||||
peer = FrameworkElementAutomationPeer.CreatePeerForElement(NarratorAnnouncer);
|
||||
}
|
||||
|
||||
peer?.RaiseNotificationEvent(
|
||||
kind,
|
||||
AutomationNotificationProcessing.ImportantMostRecent,
|
||||
announcement,
|
||||
activityId);
|
||||
}
|
||||
|
||||
private void UpdateUiForStackChange()
|
||||
{
|
||||
ContextFilterBox.Text = string.Empty;
|
||||
|
||||
@@ -500,9 +500,10 @@ public sealed partial class DockControl : UserControl, IRecipient<CloseContextMe
|
||||
|
||||
private void ContextMenuFlyout_Opened(object sender, object e)
|
||||
{
|
||||
// We need to wait until our flyout is opened to try and toss focus
|
||||
// at its search box. The control isn't in the UI tree before that
|
||||
// Focus the filter box so the flyout captures keyboard input,
|
||||
// then fire a single consolidated Narrator announcement.
|
||||
ContextControl.FocusSearchBox();
|
||||
ContextControl.AnnounceOpened();
|
||||
}
|
||||
|
||||
public void Receive(CloseContextMenuMessage message)
|
||||
|
||||
@@ -9,6 +9,7 @@ using CommunityToolkit.Mvvm.Messaging;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.Common.Helpers;
|
||||
using Microsoft.CmdPal.Common.Messages;
|
||||
using Microsoft.CmdPal.Common.Services;
|
||||
using Microsoft.CmdPal.UI.Controls;
|
||||
using Microsoft.CmdPal.UI.Dock;
|
||||
using Microsoft.CmdPal.UI.Events;
|
||||
@@ -191,7 +192,7 @@ public sealed partial class MainWindow : WindowEx,
|
||||
App.Current.Services.GetRequiredService<ISettingsService>().SettingsChanged += SettingsChangedHandler;
|
||||
|
||||
// Make sure that we update the acrylic theme when the OS theme changes
|
||||
RootElement.ActualThemeChanged += (s, e) => DispatcherQueue.TryEnqueue(UpdateBackdrop);
|
||||
RootElement.ActualThemeChanged += RootElement_ActualThemeChanged;
|
||||
|
||||
// Hardcoding event name to avoid bringing in the PowerToys.interop dependency. Event name must match CMDPAL_SHOW_EVENT from shared_constants.h
|
||||
NativeEventWaiter.WaitForEventLoop("Local\\PowerToysCmdPal-ShowEvent-62336fcd-8611-4023-9b30-091a6af4cc5a", () =>
|
||||
@@ -221,6 +222,11 @@ public sealed partial class MainWindow : WindowEx,
|
||||
UpdateBackdrop();
|
||||
}
|
||||
|
||||
private void RootElement_ActualThemeChanged(FrameworkElement sender, object args)
|
||||
{
|
||||
DispatcherQueue.TryEnqueue(UpdateBackdrop);
|
||||
}
|
||||
|
||||
private static void LocalKeyboardListener_OnKeyPressed(object? sender, LocalKeyboardListenerKeyPressedEventArgs e)
|
||||
{
|
||||
if (e.Key == VirtualKey.GoBack)
|
||||
@@ -556,8 +562,6 @@ public sealed partial class MainWindow : WindowEx,
|
||||
var borderColor = showFrame ? DWMWA_COLOR_DEFAULT : DWMWA_COLOR_NONE;
|
||||
PInvoke.DwmSetWindowAttribute(_hwnd, DWMWINDOWATTRIBUTE.DWMWA_BORDER_COLOR, &borderColor, sizeof(uint));
|
||||
}
|
||||
|
||||
RedrawWindow(_hwnd);
|
||||
}
|
||||
|
||||
private void InitializeBackdropSupport()
|
||||
@@ -622,7 +626,7 @@ public sealed partial class MainWindow : WindowEx,
|
||||
var positionWindowForAnchor = (HWND hwnd) =>
|
||||
{
|
||||
PInvoke.GetWindowRect(hwnd, out var bounds);
|
||||
var swpFlags = SET_WINDOW_POS_FLAGS.SWP_NOSIZE | SET_WINDOW_POS_FLAGS.SWP_NOACTIVATE | SET_WINDOW_POS_FLAGS.SWP_NOZORDER | SET_WINDOW_POS_FLAGS.SWP_FRAMECHANGED;
|
||||
var swpFlags = SET_WINDOW_POS_FLAGS.SWP_NOSIZE | SET_WINDOW_POS_FLAGS.SWP_NOACTIVATE | SET_WINDOW_POS_FLAGS.SWP_NOZORDER;
|
||||
switch (anchorCorner)
|
||||
{
|
||||
case AnchorPoint.TopLeft:
|
||||
@@ -722,27 +726,11 @@ public sealed partial class MainWindow : WindowEx,
|
||||
// topmost status when we hide the window (because we cloak it instead
|
||||
// of hiding it).
|
||||
//
|
||||
// SWP_FRAMECHANGED re-sends WM_NCCALCSIZE so the OS recomputes the
|
||||
// (zero-width) frame. But on this cloak->show path it doesn't reliably
|
||||
// *repaint* the non-client area, so the WS_THICKFRAME border that was
|
||||
// painted earlier survives on the top/left/right edges (the bottom is
|
||||
// clipped away by the card region). A real resize fixes it because it
|
||||
// forces that NC repaint - so we do the same explicitly below.
|
||||
// SWP_FRAMECHANGED is load-bearing for the borderless look on a cold
|
||||
// start. Asking for SWP_FRAMECHANGED here re-sends WM_NCCALCSIZE and
|
||||
// forces the NC repaint every time we show, so the frame is gone from
|
||||
// the very first summon.
|
||||
PInvoke.SetWindowPos(hwnd, HWND.HWND_TOPMOST, 0, 0, 0, 0, SET_WINDOW_POS_FLAGS.SWP_NOMOVE | SET_WINDOW_POS_FLAGS.SWP_NOSIZE | SET_WINDOW_POS_FLAGS.SWP_FRAMECHANGED);
|
||||
|
||||
// Force the non-client frame to actually redraw (RDW_FRAME) and the
|
||||
// client to repaint over wherever it used to be. Without this the stale
|
||||
// border lingers until the user resizes the window.
|
||||
RedrawWindow(hwnd);
|
||||
}
|
||||
|
||||
private static void RedrawWindow(HWND hwnd)
|
||||
{
|
||||
const uint RDW_INVALIDATE = 0x0001;
|
||||
const uint RDW_UPDATENOW = 0x0100;
|
||||
const uint RDW_ALLCHILDREN = 0x0080;
|
||||
const uint RDW_FRAME = 0x0400;
|
||||
_ = RedrawWindow(hwnd, IntPtr.Zero, IntPtr.Zero, RDW_INVALIDATE | RDW_UPDATENOW | RDW_ALLCHILDREN | RDW_FRAME);
|
||||
}
|
||||
|
||||
private static DisplayArea GetScreen(HWND currentHwnd, MonitorBehavior target)
|
||||
@@ -1199,10 +1187,6 @@ public sealed partial class MainWindow : WindowEx,
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool DeleteObject(IntPtr hObject);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool RedrawWindow(IntPtr hWnd, IntPtr lprcUpdate, IntPtr hrgnUpdate, uint flags);
|
||||
|
||||
internal void MainWindow_Activated(object sender, WindowActivatedEventArgs args)
|
||||
{
|
||||
if (!_themeServiceInitialized && args.WindowActivationState != WindowActivationState.Deactivated)
|
||||
@@ -1568,17 +1552,6 @@ public sealed partial class MainWindow : WindowEx,
|
||||
case PInvoke.WM_NCCALCSIZE when wParam.Value != 0 && _hwndFrameVisible != true:
|
||||
return (LRESULT)0;
|
||||
|
||||
// On every activation change the OS repaints the non-client frame to
|
||||
// flip between the active / inactive caption. With WS_THICKFRAME still
|
||||
// present that paints the OS border back over our borderless window each
|
||||
// time we lose (or gain) focus - which is the frame that reappears when
|
||||
// another window steals focus, and the one that shows up shortly after
|
||||
// startup (the first activation flip). Passing -1 as the update-region
|
||||
// (lParam) to DefWindowProc keeps the correct activation result while
|
||||
// telling it to skip the non-client repaint entirely.
|
||||
case PInvoke.WM_NCACTIVATE when _hwndFrameVisible != true:
|
||||
return PInvoke.CallWindowProc(_originalWndProc, hwnd, uMsg, wParam, new LPARAM(-1));
|
||||
|
||||
case PInvoke.WM_HOTKEY:
|
||||
{
|
||||
var hotkeyIndex = (int)wParam.Value;
|
||||
@@ -1715,6 +1688,9 @@ public sealed partial class MainWindow : WindowEx,
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_themeService.ThemeChanged -= ThemeServiceOnThemeChanged;
|
||||
App.Current.Services.GetRequiredService<ISettingsService>().SettingsChanged -= SettingsChangedHandler;
|
||||
|
||||
_localKeyboardListener.Dispose();
|
||||
_windowThemeSynchronizer.Dispose();
|
||||
DisposeAcrylic();
|
||||
|
||||
@@ -232,7 +232,7 @@
|
||||
Grid.Row="3"
|
||||
Padding="0"
|
||||
AutomationProperties.AutomationId="CmdPal_GalleryItemPage_ViewRepository"
|
||||
NavigateUri="{x:Bind ViewModel.Homepage, Mode=OneWay}"
|
||||
NavigateUri="{x:Bind ViewModel.HomepageUri, Mode=OneWay}"
|
||||
ToolTipService.ToolTip="{x:Bind ViewModel.Homepage, Mode=OneWay}"
|
||||
Visibility="{x:Bind help:BindTransformers.BoolToVisibility(ViewModel.HasHomepage), Mode=OneWay, FallbackValue=Collapsed}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="4">
|
||||
@@ -332,7 +332,7 @@
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="232" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<TextBlock
|
||||
@@ -365,6 +365,7 @@
|
||||
<SolidColorBrush x:Key="ItemContainerBackgroundPressed" Color="Transparent" />
|
||||
</ItemContainer.Resources>
|
||||
<Border
|
||||
Width="356"
|
||||
Height="200"
|
||||
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
|
||||
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
|
||||
|
||||
@@ -22,6 +22,7 @@ public sealed partial class ExtensionsPage : Page
|
||||
|
||||
private readonly SettingsViewModel? viewModel;
|
||||
private readonly Dictionary<string, WeakReference<SettingsCard>> _vmToCardMap = new();
|
||||
private readonly Dictionary<SettingsCard, ProviderSettingsViewModel> _cardToVmMap = new();
|
||||
|
||||
public ExtensionsPage()
|
||||
{
|
||||
@@ -31,6 +32,23 @@ public sealed partial class ExtensionsPage : Page
|
||||
var themeService = App.Current.Services.GetService<IThemeService>()!;
|
||||
var settingsService = App.Current.Services.GetRequiredService<ISettingsService>();
|
||||
viewModel = new SettingsViewModel(topLevelCommandManager, _mainTaskScheduler, themeService, settingsService);
|
||||
|
||||
Unloaded += ExtensionsPage_Unloaded;
|
||||
}
|
||||
|
||||
private void ExtensionsPage_Unloaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// ProviderSettingsViewModel subscribes to its CommandProviderWrapper (owned by the
|
||||
// singleton TopLevelCommandManager), so a live VM roots this page through the
|
||||
// PropertyChanged handler below. Drain any VMs still hooked when the page is torn
|
||||
// down; SettingsCard_DataContextChanged only unhooks the ones that get recycled.
|
||||
foreach (var vm in _cardToVmMap.Values)
|
||||
{
|
||||
vm.PropertyChanged -= ProviderViewModel_PropertyChanged;
|
||||
}
|
||||
|
||||
_cardToVmMap.Clear();
|
||||
_vmToCardMap.Clear();
|
||||
}
|
||||
|
||||
private void SettingsCard_Click(object sender, RoutedEventArgs e)
|
||||
@@ -46,16 +64,28 @@ public sealed partial class ExtensionsPage : Page
|
||||
|
||||
private void SettingsCard_DataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args)
|
||||
{
|
||||
// Store the card reference keyed by Id (not the VM itself) to avoid leaking VM references
|
||||
if (sender is SettingsCard card && card.DataContext is ProviderSettingsViewModel newVm)
|
||||
if (sender is SettingsCard card)
|
||||
{
|
||||
_vmToCardMap[newVm.Id] = new WeakReference<SettingsCard>(card);
|
||||
newVm.PropertyChanged += ProviderViewModel_PropertyChanged;
|
||||
|
||||
// Immediately update automation name in case DisplayName is already available
|
||||
if (card.Content is ToggleSwitch toggle && !string.IsNullOrEmpty(newVm.DisplayName))
|
||||
// Unsubscribe from the previous ViewModel to prevent handler accumulation
|
||||
// when virtualization recycles items with a new DataContext.
|
||||
if (_cardToVmMap.TryGetValue(card, out var oldVm))
|
||||
{
|
||||
AutomationProperties.SetName(toggle, newVm.DisplayName);
|
||||
oldVm.PropertyChanged -= ProviderViewModel_PropertyChanged;
|
||||
_cardToVmMap.Remove(card);
|
||||
}
|
||||
|
||||
// Store the card reference keyed by Id (not the VM itself) to avoid leaking VM references
|
||||
if (card.DataContext is ProviderSettingsViewModel newVm)
|
||||
{
|
||||
_vmToCardMap[newVm.Id] = new WeakReference<SettingsCard>(card);
|
||||
_cardToVmMap[card] = newVm;
|
||||
newVm.PropertyChanged += ProviderViewModel_PropertyChanged;
|
||||
|
||||
// Immediately update automation name in case DisplayName is already available
|
||||
if (card.Content is ToggleSwitch toggle && !string.IsNullOrEmpty(newVm.DisplayName))
|
||||
{
|
||||
AutomationProperties.SetName(toggle, newVm.DisplayName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,20 +160,24 @@ public sealed partial class SettingsWindow : WindowEx,
|
||||
break;
|
||||
}
|
||||
|
||||
if (pageType is not null)
|
||||
if (pageType is null)
|
||||
{
|
||||
NavFrame.Navigate(pageType);
|
||||
return;
|
||||
}
|
||||
|
||||
// Now, make sure to actually select the correct menu item too
|
||||
foreach (var obj in NavView.MenuItems)
|
||||
if (NavFrame.Content?.GetType() == pageType)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
NavFrame.Navigate(pageType);
|
||||
|
||||
// Now, make sure to actually select the correct menu item too
|
||||
foreach (var obj in NavView.MenuItems)
|
||||
{
|
||||
if (obj is NavigationViewItem item && item.Tag is string s && s == page)
|
||||
{
|
||||
if (obj is NavigationViewItem item)
|
||||
{
|
||||
if (item.Tag is string s && s == page)
|
||||
{
|
||||
NavView.SelectedItem = item;
|
||||
}
|
||||
}
|
||||
NavView.SelectedItem = item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -559,6 +559,9 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
|
||||
<data name="ScreenReader_Announcement_NavigatedToPage0" xml:space="preserve">
|
||||
<value>Navigated to {0} page</value>
|
||||
</data>
|
||||
<data name="ScreenReader_Announcement_ContextMenuOpened" xml:space="preserve">
|
||||
<value>Menu, {0} commands. {1}, {2} of {0}.</value>
|
||||
</data>
|
||||
<data name="SettingsButton.[using:Microsoft.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
|
||||
<value>Settings (Ctrl+,)</value>
|
||||
</data>
|
||||
|
||||
@@ -4,13 +4,23 @@
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:common="using:Microsoft.PowerToys.Common.UI.Controls.Window"
|
||||
xmlns:controls="using:Microsoft.PowerToys.Common.UI.Controls"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="using:Microsoft.CmdPal.UI"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d">
|
||||
<TextBlock
|
||||
Margin="16,10,20,12"
|
||||
Text="{x:Bind ViewModel.ToastMessage, Mode=OneWay}"
|
||||
TextAlignment="Center"
|
||||
TextWrapping="Wrap" />
|
||||
<controls:TransientSurface
|
||||
x:Name="Surface"
|
||||
MaxWidth="560"
|
||||
Margin="24,24,24,16"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Bottom"
|
||||
HideTransition="Bottom"
|
||||
ShowTransition="Bottom">
|
||||
<TextBlock
|
||||
Margin="16,10,20,12"
|
||||
Text="{x:Bind ViewModel.ToastMessage, Mode=OneWay}"
|
||||
TextAlignment="Center"
|
||||
TextWrapping="Wrap" />
|
||||
</controls:TransientSurface>
|
||||
</common:TransparentWindow>
|
||||
|
||||
@@ -17,11 +17,14 @@ using RS_ = Microsoft.CmdPal.UI.Helpers.ResourceLoaderInstance;
|
||||
namespace Microsoft.CmdPal.UI;
|
||||
|
||||
/// <summary>
|
||||
/// CmdPal's transient toast banner. Inherits all of its chrome, click-through,
|
||||
/// acrylic, and fade/slide animations from
|
||||
/// <see cref="TransparentWindow"/>; adds only the bits that are bespoke to
|
||||
/// CmdPal toasts: a bound message <c>TextBlock</c>, a 2.5 s auto-dismiss timer,
|
||||
/// bottom-center positioning, and <see cref="QuitMessage"/> handling.
|
||||
/// CmdPal's transient toast notification. It is a bare
|
||||
/// <see cref="TransparentWindow"/> host whose content is a
|
||||
/// <see cref="Microsoft.PowerToys.Common.UI.Controls.TransientSurface"/> — the
|
||||
/// surface supplies the acrylic, border, corners, shadow, and the fade/slide
|
||||
/// animation, driven automatically off the window's show/hide events. This class
|
||||
/// adds only the bits bespoke to CmdPal toasts: a bound message <c>TextBlock</c>,
|
||||
/// a 2.5 s auto-dismiss timer, bottom-center positioning, and
|
||||
/// <see cref="QuitMessage"/> handling.
|
||||
/// </summary>
|
||||
public sealed partial class ToastWindow : TransparentWindow,
|
||||
IRecipient<QuitMessage>
|
||||
@@ -39,13 +42,10 @@ public sealed partial class ToastWindow : TransparentWindow,
|
||||
AppWindow.Title = RS_.GetString("ToastWindowTitle");
|
||||
this.SetWindowSize(600, 180);
|
||||
|
||||
// Pin the chrome card to bottom-center with the toast's classic 560-wide
|
||||
// pill shape. The window itself stays 600x180 so the slide animations
|
||||
// have headroom and we don't have to chase SizeToContent.
|
||||
Card.HorizontalAlignment = Microsoft.UI.Xaml.HorizontalAlignment.Center;
|
||||
Card.VerticalAlignment = Microsoft.UI.Xaml.VerticalAlignment.Bottom;
|
||||
Card.MaxWidth = 560;
|
||||
Card.Margin = new Microsoft.UI.Xaml.Thickness(24, 24, 24, 16);
|
||||
// Let the surface animate itself in/out in response to this window's
|
||||
// Show()/Hide(). The 600x180 window leaves the bottom-center 560-wide
|
||||
// pill (positioned in XAML) room for its slide + shadow.
|
||||
Surface.SubscribeTo(this);
|
||||
|
||||
WeakReferenceMessenger.Default.Register<QuitMessage>(this);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CmdPal.Common.Helpers;
|
||||
|
||||
namespace Microsoft.CmdPal.Common.UnitTests.Helpers;
|
||||
|
||||
[TestClass]
|
||||
public class ShellArgumentBuilderTests
|
||||
{
|
||||
[DataTestMethod]
|
||||
[DataRow("plain", "plain")]
|
||||
[DataRow("C:\\Program Files\\PowerToys", "\"C:\\Program Files\\PowerToys\"")]
|
||||
[DataRow("say \"hello\"", "\"say \\\"hello\\\"\"")]
|
||||
[DataRow("", "\"\"")]
|
||||
[DataRow("C:\\Program Files\\", "\"C:\\Program Files\\\\\"")]
|
||||
public void BuildArguments_FormatsSingleArgument(string argument, string expected)
|
||||
{
|
||||
var actual = ShellArgumentBuilder.BuildArguments(argument);
|
||||
|
||||
Assert.AreEqual(expected, actual);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void BuildArguments_FormatsMultipleArguments()
|
||||
{
|
||||
var actual = ShellArgumentBuilder.BuildArguments("plain", "C:\\Program Files\\PowerToys", "two words");
|
||||
|
||||
Assert.AreEqual("plain \"C:\\Program Files\\PowerToys\" \"two words\"", actual);
|
||||
}
|
||||
}
|
||||
@@ -119,6 +119,7 @@ public class ExtensionGalleryItemViewModelTests
|
||||
var viewModel = CreateViewModel(entry);
|
||||
|
||||
Assert.IsFalse(viewModel.HasHomepage);
|
||||
Assert.IsNull(viewModel.HomepageUri);
|
||||
Assert.IsFalse(viewModel.HasAuthorUrl);
|
||||
Assert.IsFalse(viewModel.HasUrlSource);
|
||||
Assert.IsFalse(viewModel.HasActionableSourceDetails);
|
||||
@@ -131,6 +132,32 @@ public class ExtensionGalleryItemViewModelTests
|
||||
Assert.IsFalse(viewModel.OpenInstallUrlCommand.CanExecute(null));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Constructor_SetsHomepageUri_WhenHomepageIsWebUri()
|
||||
{
|
||||
var entry = CreateEntry(iconUrl: null);
|
||||
entry.Homepage = "https://example.com/extension";
|
||||
|
||||
var viewModel = CreateViewModel(entry);
|
||||
|
||||
Assert.IsTrue(viewModel.HasHomepage);
|
||||
Assert.AreEqual(new Uri("https://example.com/extension"), viewModel.HomepageUri);
|
||||
Assert.IsTrue(viewModel.OpenHomepageCommand.CanExecute(null));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Constructor_LeavesHomepageUriNull_WhenHomepageIsMissing()
|
||||
{
|
||||
var entry = CreateEntry(iconUrl: null);
|
||||
entry.Homepage = null;
|
||||
|
||||
var viewModel = CreateViewModel(entry);
|
||||
|
||||
Assert.IsFalse(viewModel.HasHomepage);
|
||||
Assert.IsNull(viewModel.HomepageUri);
|
||||
Assert.IsFalse(viewModel.OpenHomepageCommand.CanExecute(null));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Constructor_EnablesCopyCommand_WhenWinGetIdIsAvailable()
|
||||
{
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.InteropServices;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.Common.Helpers;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Bookmarks.Helpers;
|
||||
|
||||
@@ -24,7 +25,7 @@ internal static class CommandLauncher
|
||||
// You can notice the difference with Recycle Bin for example:
|
||||
// - "explorer ::{645FF040-5081-101B-9F08-00AA002F954E}"
|
||||
// - "::{645FF040-5081-101B-9F08-00AA002F954E}"
|
||||
return ShellHelpers.OpenInShell("explorer.exe", classification.Target);
|
||||
return ShellHelpers.OpenInShell("explorer.exe", ShellArgumentBuilder.BuildArguments(classification.Target));
|
||||
|
||||
case LaunchMethod.ActivateAppId:
|
||||
return ActivateAppId(classification.Target, classification.Arguments);
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
<ProjectPriFileName>Microsoft.CmdPal.Ext.Bookmarks.pri</ProjectPriFileName>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Microsoft.CmdPal.Common\Microsoft.CmdPal.Common.csproj" />
|
||||
<ProjectReference Include="..\..\extensionsdk\Microsoft.CommandPalette.Extensions.Toolkit\Microsoft.CommandPalette.Extensions.Toolkit.csproj" />
|
||||
<ProjectReference Include="..\..\ext\Microsoft.CmdPal.Ext.Indexer\Microsoft.CmdPal.Ext.Indexer.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -134,6 +134,25 @@ internal sealed partial class PerformanceWidgetsPage : OnLoadStaticListPage, IDi
|
||||
MoreCommands = _networkPage.Commands,
|
||||
};
|
||||
|
||||
if (isBandPage)
|
||||
{
|
||||
_networkUpItem = new ListItem(_networkPage)
|
||||
{
|
||||
Title = $"{_networkUpSpeed}",
|
||||
Subtitle = Resources.GetResource("Network_Send_Subtitle"),
|
||||
Icon = Icons.NetworkUpIcon,
|
||||
MoreCommands = _networkPage.Commands,
|
||||
};
|
||||
|
||||
_networkDownItem = new ListItem(_networkPage)
|
||||
{
|
||||
Title = $"{_networkDownSpeed}",
|
||||
Subtitle = Resources.GetResource("Network_Receive_Subtitle"),
|
||||
Icon = Icons.NetworkDownIcon,
|
||||
MoreCommands = _networkPage.Commands,
|
||||
};
|
||||
}
|
||||
|
||||
_networkPage.Updated += (s, e) =>
|
||||
{
|
||||
_networkItem.Title = _networkPage.GetItemTitle(isBandPage);
|
||||
@@ -253,22 +272,6 @@ internal sealed partial class PerformanceWidgetsPage : OnLoadStaticListPage, IDi
|
||||
}
|
||||
else
|
||||
{
|
||||
_networkUpItem = new ListItem(_networkPage!)
|
||||
{
|
||||
Title = $"{_networkUpSpeed}",
|
||||
Subtitle = Resources.GetResource("Network_Send_Subtitle"),
|
||||
Icon = Icons.NetworkUpIcon,
|
||||
MoreCommands = _networkPage!.Commands,
|
||||
};
|
||||
|
||||
_networkDownItem = new ListItem(_networkPage!)
|
||||
{
|
||||
Title = $"{_networkDownSpeed}",
|
||||
Subtitle = Resources.GetResource("Network_Receive_Subtitle"),
|
||||
Icon = Icons.NetworkDownIcon,
|
||||
MoreCommands = _networkPage!.Commands,
|
||||
};
|
||||
|
||||
return _batteryItem is not null
|
||||
? new[] { _cpuItem!, _memoryItem!, _networkUpItem!, _networkDownItem!, _gpuItem!, _batteryItem! }
|
||||
: new[] { _cpuItem!, _memoryItem!, _networkUpItem!, _networkDownItem!, _gpuItem! };
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Text;
|
||||
using Microsoft.CmdPal.Common.Helpers;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Shell.Helpers;
|
||||
@@ -42,98 +42,7 @@ public class ShellListPageHelpers
|
||||
executable = segments[0];
|
||||
if (segments.Length > 1)
|
||||
{
|
||||
arguments = ArgumentBuilder.BuildArguments(segments[1..]);
|
||||
}
|
||||
}
|
||||
|
||||
private static class ArgumentBuilder
|
||||
{
|
||||
internal static string BuildArguments(string[] arguments)
|
||||
{
|
||||
if (arguments.Length <= 0)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var stringBuilder = new StringBuilder();
|
||||
foreach (var argument in arguments)
|
||||
{
|
||||
AppendArgument(stringBuilder, argument);
|
||||
}
|
||||
|
||||
return stringBuilder.ToString();
|
||||
}
|
||||
|
||||
private static void AppendArgument(StringBuilder stringBuilder, string argument)
|
||||
{
|
||||
if (stringBuilder.Length > 0)
|
||||
{
|
||||
stringBuilder.Append(' ');
|
||||
}
|
||||
|
||||
if (argument.Length == 0 || ShouldBeQuoted(argument))
|
||||
{
|
||||
stringBuilder.Append('\"');
|
||||
var index = 0;
|
||||
while (index < argument.Length)
|
||||
{
|
||||
var c = argument[index++];
|
||||
if (c == '\\')
|
||||
{
|
||||
var numBackSlash = 1;
|
||||
while (index < argument.Length && argument[index] == '\\')
|
||||
{
|
||||
index++;
|
||||
numBackSlash++;
|
||||
}
|
||||
|
||||
if (index == argument.Length)
|
||||
{
|
||||
stringBuilder.Append('\\', numBackSlash * 2);
|
||||
}
|
||||
else if (argument[index] == '\"')
|
||||
{
|
||||
stringBuilder.Append('\\', (numBackSlash * 2) + 1);
|
||||
stringBuilder.Append('\"');
|
||||
index++;
|
||||
}
|
||||
else
|
||||
{
|
||||
stringBuilder.Append('\\', numBackSlash);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c == '\"')
|
||||
{
|
||||
stringBuilder.Append('\\');
|
||||
stringBuilder.Append('\"');
|
||||
continue;
|
||||
}
|
||||
|
||||
stringBuilder.Append(c);
|
||||
}
|
||||
|
||||
stringBuilder.Append('\"');
|
||||
}
|
||||
else
|
||||
{
|
||||
stringBuilder.Append(argument);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool ShouldBeQuoted(string s)
|
||||
{
|
||||
foreach (var c in s)
|
||||
{
|
||||
if (char.IsWhiteSpace(c) || c == '\"')
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
arguments = ShellArgumentBuilder.BuildArguments(segments[1..]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ public partial class ShowFileInFolderCommand : InvokableCommand
|
||||
try
|
||||
{
|
||||
var argument = "/select, \"" + _path + "\"";
|
||||
Process.Start("explorer.exe", argument);
|
||||
using var process = Process.Start("explorer.exe", argument);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
|
||||
@@ -866,6 +866,14 @@ void FancyZones::UpdateWorkAreas(bool updateWindowPositions) noexcept
|
||||
std::vector<FancyZonesDataTypes::MonitorId> monitors = { FancyZonesDataTypes::MonitorId{ .monitor = nullptr, .deviceId = { .id = ZonedWindowProperties::MultiMonitorName, .instanceId = ZonedWindowProperties::MultiMonitorInstance } } };
|
||||
if (ShouldWorkAreasBeRecreated(monitors, currentVirtualDesktop, m_workAreaConfiguration.GetAllWorkAreas()))
|
||||
{
|
||||
// WindowMouseSnap caches a raw WorkArea* in m_currentWorkArea and the
|
||||
// WorkArea map by reference. WorkAreaConfiguration::Clear() destroys
|
||||
// every unique_ptr<WorkArea> (and hence the inner ZonesOverlay and
|
||||
// its std::mutex). If a drag is in flight, the next MoveSizeUpdate
|
||||
// would dereference that dangling WorkArea* and lock the freed
|
||||
// mutex. Drain the active drag first so subsequent drag messages
|
||||
// hit the snapper's `if (m_windowMouseSnapper)` guard and no-op.
|
||||
MoveSizeEnd();
|
||||
m_workAreaConfiguration.Clear();
|
||||
|
||||
FancyZonesDataTypes::WorkAreaId workAreaId;
|
||||
@@ -882,6 +890,8 @@ void FancyZones::UpdateWorkAreas(bool updateWindowPositions) noexcept
|
||||
|
||||
if (ShouldWorkAreasBeRecreated(monitors, currentVirtualDesktop, workAreas))
|
||||
{
|
||||
// See comment above the matching Clear() in the span-zones branch.
|
||||
MoveSizeEnd();
|
||||
m_workAreaConfiguration.Clear();
|
||||
for (const auto& monitor : monitors)
|
||||
{
|
||||
@@ -1094,6 +1104,9 @@ void FancyZones::SettingsUpdate(SettingId id)
|
||||
break;
|
||||
case SettingId::SpanZonesAcrossMonitors:
|
||||
{
|
||||
// See UpdateWorkAreas() — same WindowMouseSnap dangling-WorkArea*
|
||||
// hazard if the user toggles this setting mid-drag.
|
||||
MoveSizeEnd();
|
||||
m_workAreaConfiguration.Clear();
|
||||
PostMessageW(m_window, WM_PRIV_INIT, NULL, NULL);
|
||||
}
|
||||
|
||||
@@ -48,7 +48,14 @@ void OnThreadExecutor::worker_thread()
|
||||
|
||||
OnThreadExecutor::~OnThreadExecutor()
|
||||
{
|
||||
_shutdown_request = true;
|
||||
{
|
||||
// Modify the shared shutdown flag while holding the mutex so the
|
||||
// worker reliably observes it on its next wake. Without this, a notify
|
||||
// racing the worker entering _task_cv.wait can be missed and the join
|
||||
// below hangs forever.
|
||||
std::lock_guard lock{ _task_mutex };
|
||||
_shutdown_request = true;
|
||||
}
|
||||
_task_cv.notify_one();
|
||||
_worker_thread.join();
|
||||
}
|
||||
|
||||
@@ -115,6 +115,11 @@ WorkArea::WorkArea(HINSTANCE hinstance, const FancyZonesDataTypes::WorkAreaId& u
|
||||
|
||||
WorkArea::~WorkArea()
|
||||
{
|
||||
// Tear down the renderer (joining its background thread) before returning
|
||||
// the HWND to the pool. Otherwise, the render thread can still be drawing
|
||||
// through m_renderTarget into an HWND that has already been recycled by a
|
||||
// subsequent NewZonesOverlayWindow call.
|
||||
m_zonesOverlay.reset();
|
||||
windowPool.FreeZonesOverlayWindow(m_window);
|
||||
}
|
||||
|
||||
|
||||
@@ -340,13 +340,19 @@ void ZonesOverlay::DrawActiveZoneSet(const ZonesMap& zones,
|
||||
|
||||
ZonesOverlay::~ZonesOverlay()
|
||||
{
|
||||
// Constructor early-returns (e.g. CreateHwndRenderTarget failing during a
|
||||
// display-driver TDR) leave m_renderThread default-constructed; calling
|
||||
// join() on a non-joinable thread terminates the process.
|
||||
if (m_renderThread.joinable())
|
||||
{
|
||||
std::unique_lock lock(m_mutex);
|
||||
m_abortThread = true;
|
||||
m_shouldRender = true;
|
||||
{
|
||||
std::unique_lock lock(m_mutex);
|
||||
m_abortThread = true;
|
||||
m_shouldRender = true;
|
||||
}
|
||||
m_cv.notify_all();
|
||||
m_renderThread.join();
|
||||
}
|
||||
m_cv.notify_all();
|
||||
m_renderThread.join();
|
||||
|
||||
if (m_renderTarget)
|
||||
{
|
||||
|
||||
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 433 B After Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 433 B After Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 328 B After Width: | Height: | Size: 2.7 KiB |
@@ -59,7 +59,6 @@
|
||||
</PropertyGroup>
|
||||
<!-- Props that are constant for both Debug and Release configurations -->
|
||||
<PropertyGroup Label="Configuration">
|
||||
|
||||
<OutDir>..\..\..\..\$(Platform)\$(Configuration)\$(MSBuildProjectName)\</OutDir>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
<SpectreMitigation>Spectre</SpectreMitigation>
|
||||
@@ -164,7 +163,7 @@
|
||||
<Import Project="..\..\..\..\packages\Microsoft.Toolkit.Win32.UI.XamlApplication.6.1.3\build\native\Microsoft.Toolkit.Win32.UI.XamlApplication.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Toolkit.Win32.UI.XamlApplication.6.1.3\build\native\Microsoft.Toolkit.Win32.UI.XamlApplication.targets')" />
|
||||
<Import Project="..\..\..\..\packages\Microsoft.VCRTForwarders.140.1.0.7\build\native\Microsoft.VCRTForwarders.140.targets" Condition="Exists('..\..\..\..\packages\Microsoft.VCRTForwarders.140.1.0.7\build\native\Microsoft.VCRTForwarders.140.targets')" />
|
||||
<Import Project="..\..\..\..\packages\Microsoft.UI.Xaml.2.8.2-prerelease.220830001\build\native\Microsoft.UI.Xaml.targets" Condition="Exists('..\..\..\..\packages\Microsoft.UI.Xaml.2.8.2-prerelease.220830001\build\native\Microsoft.UI.Xaml.targets')" />
|
||||
<Import Project="..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets')" />
|
||||
<Import Project="..\..\..\..\packages\Microsoft.Web.WebView2.1.0.4022.49\build\native\Microsoft.Web.WebView2.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Web.WebView2.1.0.4022.49\build\native\Microsoft.Web.WebView2.targets')" />
|
||||
</ImportGroup>
|
||||
<Import Project="..\..\..\..\deps\spdlog.props" />
|
||||
<Target Name="GenerateResourceFiles" BeforeTargets="PrepareForBuild">
|
||||
@@ -181,7 +180,7 @@
|
||||
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.VCRTForwarders.140.1.0.7\build\native\Microsoft.VCRTForwarders.140.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.VCRTForwarders.140.1.0.7\build\native\Microsoft.VCRTForwarders.140.targets'))" />
|
||||
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.UI.Xaml.2.8.2-prerelease.220830001\build\native\Microsoft.UI.Xaml.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.UI.Xaml.2.8.2-prerelease.220830001\build\native\Microsoft.UI.Xaml.props'))" />
|
||||
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.UI.Xaml.2.8.2-prerelease.220830001\build\native\Microsoft.UI.Xaml.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.UI.Xaml.2.8.2-prerelease.220830001\build\native\Microsoft.UI.Xaml.targets'))" />
|
||||
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets'))" />
|
||||
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Web.WebView2.1.0.4022.49\build\native\Microsoft.Web.WebView2.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Web.WebView2.1.0.4022.49\build\native\Microsoft.Web.WebView2.targets'))" />
|
||||
</Target>
|
||||
<Target Name="FakeResourcesPriMerge" BeforeTargets="FinalizeBuildStatus" DependsOnTargets="CopyFilesToOutputDirectory">
|
||||
<Message Text="Renaming Microsoft.UI.Xaml.pri to resources.pri" />
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
<package id="Microsoft.Toolkit.Win32.UI.XamlApplication" version="6.1.3" targetFramework="native" />
|
||||
<package id="Microsoft.UI.Xaml" version="2.8.2-prerelease.220830001" targetFramework="native" />
|
||||
<package id="Microsoft.VCRTForwarders.140" version="1.0.7" targetFramework="native" />
|
||||
<package id="Microsoft.Web.WebView2" version="1.0.2903.40" targetFramework="native" />
|
||||
<package id="Microsoft.Web.WebView2" version="1.0.4022.49" targetFramework="native" />
|
||||
<package id="Microsoft.Windows.CppWinRT" version="2.0.250303.1" targetFramework="native" />
|
||||
</packages>
|
||||
@@ -15,7 +15,6 @@
|
||||
<OutDir>..\..\..\..\$(Platform)\$(Configuration)\</OutDir>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Label="Configuration">
|
||||
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
<ImportGroup Label="ExtensionSettings">
|
||||
@@ -101,7 +100,7 @@
|
||||
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.targets')" />
|
||||
<Import Project="..\..\..\..\packages\Microsoft.Toolkit.Win32.UI.XamlApplication.6.1.3\build\native\Microsoft.Toolkit.Win32.UI.XamlApplication.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Toolkit.Win32.UI.XamlApplication.6.1.3\build\native\Microsoft.Toolkit.Win32.UI.XamlApplication.targets')" />
|
||||
<Import Project="..\..\..\..\packages\Microsoft.UI.Xaml.2.8.2-prerelease.220830001\build\native\Microsoft.UI.Xaml.targets" Condition="Exists('..\..\..\..\packages\Microsoft.UI.Xaml.2.8.2-prerelease.220830001\build\native\Microsoft.UI.Xaml.targets')" />
|
||||
<Import Project="..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets')" />
|
||||
<Import Project="..\..\..\..\packages\Microsoft.Web.WebView2.1.0.4022.49\build\native\Microsoft.Web.WebView2.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Web.WebView2.1.0.4022.49\build\native\Microsoft.Web.WebView2.targets')" />
|
||||
</ImportGroup>
|
||||
<Import Project="..\..\..\..\deps\spdlog.props" />
|
||||
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
|
||||
@@ -114,7 +113,7 @@
|
||||
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Toolkit.Win32.UI.XamlApplication.6.1.3\build\native\Microsoft.Toolkit.Win32.UI.XamlApplication.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Toolkit.Win32.UI.XamlApplication.6.1.3\build\native\Microsoft.Toolkit.Win32.UI.XamlApplication.targets'))" />
|
||||
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.UI.Xaml.2.8.2-prerelease.220830001\build\native\Microsoft.UI.Xaml.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.UI.Xaml.2.8.2-prerelease.220830001\build\native\Microsoft.UI.Xaml.props'))" />
|
||||
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.UI.Xaml.2.8.2-prerelease.220830001\build\native\Microsoft.UI.Xaml.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.UI.Xaml.2.8.2-prerelease.220830001\build\native\Microsoft.UI.Xaml.targets'))" />
|
||||
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets'))" />
|
||||
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Web.WebView2.1.0.4022.49\build\native\Microsoft.Web.WebView2.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Web.WebView2.1.0.4022.49\build\native\Microsoft.Web.WebView2.targets'))" />
|
||||
</Target>
|
||||
<Target Name="GenerateResourceFiles" BeforeTargets="PrepareForBuild">
|
||||
<Exec Command="powershell -NonInteractive -executionpolicy Unrestricted $(SolutionDir)tools\build\convert-resx-to-rc.ps1 $(MSBuildThisFileDirectory)\..\KeyboardManagerEditor\ resource.base.h resource.h KeyboardManagerEditor.base.rc KeyboardManagerEditor.rc" />
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
<packages>
|
||||
<package id="Microsoft.Toolkit.Win32.UI.XamlApplication" version="6.1.3" targetFramework="native" />
|
||||
<package id="Microsoft.UI.Xaml" version="2.8.2-prerelease.220830001" targetFramework="native" />
|
||||
<package id="Microsoft.Web.WebView2" version="1.0.2903.40" targetFramework="native" />
|
||||
<package id="Microsoft.Web.WebView2" version="1.0.4022.49" targetFramework="native" />
|
||||
<package id="Microsoft.Windows.CppWinRT" version="2.0.250303.1" targetFramework="native" />
|
||||
</packages>
|
||||
@@ -33,16 +33,40 @@ namespace Community.PowerToys.Run.Plugin.VSCodeWorkspaces.WorkspacesHelper
|
||||
return null;
|
||||
}
|
||||
|
||||
var (workspaceEnv, machineName) = ParseVSCodeAuthority.GetWorkspaceEnvironment(authority ?? rfc3986Uri.Authority);
|
||||
var isFileUri = string.Equals(
|
||||
rfc3986Uri.Scheme,
|
||||
Uri.UriSchemeFile,
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
var isUncFileUri =
|
||||
isFileUri &&
|
||||
string.IsNullOrEmpty(authority) &&
|
||||
!string.IsNullOrEmpty(rfc3986Uri.Authority) &&
|
||||
!string.Equals(
|
||||
rfc3986Uri.Authority,
|
||||
"localhost",
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
// file://server/share is a local Windows UNC path, not a VS Code remote URI.
|
||||
var effectiveAuthority =
|
||||
isFileUri && string.IsNullOrEmpty(authority)
|
||||
? string.Empty
|
||||
: authority ?? rfc3986Uri.Authority;
|
||||
|
||||
var (workspaceEnv, machineName) =
|
||||
ParseVSCodeAuthority.GetWorkspaceEnvironment(effectiveAuthority);
|
||||
|
||||
if (workspaceEnv is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var path = rfc3986Uri.Path;
|
||||
var path = isUncFileUri
|
||||
? $@"\\{rfc3986Uri.Authority}{rfc3986Uri.Path.Replace('/', '\\')}"
|
||||
: rfc3986Uri.Path;
|
||||
|
||||
// Remove preceding '/' from local (Windows) path
|
||||
if (workspaceEnv == WorkspaceEnvironment.Local)
|
||||
// file:///C:/... becomes C:/...
|
||||
if (workspaceEnv == WorkspaceEnvironment.Local && !isUncFileUri)
|
||||
{
|
||||
path = path[1..];
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ public static class CharacterMappings
|
||||
[LetterKey.VK_M] = ["ṁ", "ᵐ", "ₘ"],
|
||||
[LetterKey.VK_N] = ["ņ", "ṅ", "ⁿ", "ℕ", "№", "ₙ"],
|
||||
[LetterKey.VK_O] = ["ȯ", "∅", "⌀", "ᵒ", "ₒ"],
|
||||
[LetterKey.VK_P] = ["ṗ", "℗", "∏", "¶", "ᵖ", "ₚ"],
|
||||
[LetterKey.VK_P] = ["ṗ", "℗", "∏", "¶", "ᵖ", "ₚ", "‰", "‱"],
|
||||
[LetterKey.VK_Q] = ["ℚ", "𐞥"],
|
||||
[LetterKey.VK_R] = ["ṙ", "®", "ℝ", "ʳ", "ᵣ"],
|
||||
[LetterKey.VK_S] = ["ṡ", "§", "∑", "∫", "ˢ", "ₛ"],
|
||||
@@ -73,10 +73,10 @@ public static class CharacterMappings
|
||||
[LetterKey.VK_X] = ["ẋ", "×", "ˣ", "ₓ"],
|
||||
[LetterKey.VK_Y] = ["ẏ", "ꝡ", "ʸ"],
|
||||
[LetterKey.VK_Z] = ["ʒ", "ǯ", "ℤ", "ᶻ"],
|
||||
[LetterKey.VK_COMMA] = ["∙", "₋", "⁻", "–", "√", "‟", "《", "》", "‛", "〈", "〉", "″", "‴", "⁗"], // – is in VK_MINUS for other languages, but not VK_COMMA, so we add it here.
|
||||
[LetterKey.VK_COMMA] = ["∙", "₋", "⁻", "–", "√", "‟", "⟪", "⟫", "‛", "⟨", "⟩", "″", "‴", "⁗"],
|
||||
[LetterKey.VK_PERIOD] = ["…", "⁝", "\u0300", "\u0301", "\u0302", "\u0303", "\u0304", "\u0308", "\u030B", "\u030C"],
|
||||
[LetterKey.VK_MINUS] = ["~", "‐", "‑", "‒", "–", "—", "―", "⁓", "−", "⸺", "⸻", "∓", "₋", "⁻"],
|
||||
[LetterKey.VK_SLASH_] = ["÷", "√"],
|
||||
[LetterKey.VK_SLASH_] = ["÷", "√", "‽", "⸘"],
|
||||
[LetterKey.VK_DIVIDE_] = ["÷", "√"],
|
||||
[LetterKey.VK_MULTIPLY_] = ["×", "⋅", "ˣ", "ₓ"],
|
||||
[LetterKey.VK_PLUS] = ["≤", "≥", "≠", "≈", "≙", "⊕", "⊗", "±", "≅", "≡", "₊", "⁺", "₌", "⁼"],
|
||||
@@ -368,25 +368,32 @@ public static class CharacterMappings
|
||||
// a spoken language, but rather a set of symbols used across languages.
|
||||
new(Language.IPA, "IPA", LanguageGroup.Special, new Dictionary<LetterKey, string[]>
|
||||
{
|
||||
[LetterKey.VK_A] = ["ɐ", "ɑ", "ɒ", "ǎ"],
|
||||
[LetterKey.VK_B] = ["ʙ"],
|
||||
[LetterKey.VK_E] = ["ɘ", "ɵ", "ə", "ɛ", "ɜ", "ɞ"],
|
||||
[LetterKey.VK_F] = ["ɟ", "ɸ"],
|
||||
[LetterKey.VK_G] = ["ɢ", "ɣ"],
|
||||
[LetterKey.VK_H] = ["ɦ", "ʜ"],
|
||||
[LetterKey.VK_I] = ["ɨ", "ɪ"],
|
||||
[LetterKey.VK_J] = ["ʝ"],
|
||||
[LetterKey.VK_L] = ["ɬ", "ɮ", "ꞎ", "ɭ", "ʎ", "ʟ", "ɺ"],
|
||||
[LetterKey.VK_N] = ["ɳ", "ɲ", "ŋ", "ɴ"],
|
||||
[LetterKey.VK_O] = ["ɤ", "ɔ", "ɶ", "ǒ"],
|
||||
[LetterKey.VK_R] = ["ʁ", "ɹ", "ɻ", "ɾ", "ɽ", "ʀ"],
|
||||
[LetterKey.VK_S] = ["ʃ", "ʂ", "ɕ"],
|
||||
[LetterKey.VK_U] = ["ʉ", "ʊ", "ǔ"],
|
||||
[LetterKey.VK_V] = ["ʋ", "ⱱ", "ʌ"],
|
||||
[LetterKey.VK_W] = ["ɰ", "ɯ"],
|
||||
[LetterKey.VK_Y] = ["ʏ"],
|
||||
[LetterKey.VK_A] = ["ɑ", "æ", "ɒ", "ɐ"],
|
||||
[LetterKey.VK_B] = ["β", "ʙ", "ɓ", "ʘ"],
|
||||
[LetterKey.VK_C] = ["ç", "χ", "ǂ"],
|
||||
[LetterKey.VK_D] = ["ð", "ɗ", "ɖ", "ǀ"],
|
||||
[LetterKey.VK_E] = ["ə", "ɛ", "ɚ", "ɘ", "ɜ", "ɵ", "ɞ", "æ", "œ"],
|
||||
[LetterKey.VK_F] = ["ɸ"],
|
||||
[LetterKey.VK_G] = ["ɡ", "ɣ", "ɢ", "ɠ", "ʛ"],
|
||||
[LetterKey.VK_H] = ["ɦ", "ħ", "ɥ", "ʜ", "ɧ", "ʱ"],
|
||||
[LetterKey.VK_I] = ["ɪ", "ɨ"],
|
||||
[LetterKey.VK_J] = ["ɟ", "ʝ", "ʄ"],
|
||||
[LetterKey.VK_L] = ["ɫ", "ʎ", "ɬ", "ɮ", "ɭ", "ʟ", "ɺ", "ꞎ", "ǁ"],
|
||||
[LetterKey.VK_M] = ["ɱ"],
|
||||
[LetterKey.VK_N] = ["ŋ", "ɲ", "ɳ", "ɴ"],
|
||||
[LetterKey.VK_O] = ["ɔ", "ø", "œ", "ɤ", "ɶ", "ʘ"],
|
||||
[LetterKey.VK_R] = ["ɹ", "ɾ", "ʁ", "ʀ", "ɻ", "ɽ"],
|
||||
[LetterKey.VK_S] = ["ʃ", "ɕ", "ʂ"],
|
||||
[LetterKey.VK_T] = ["θ", "ʈ", "ǃ"],
|
||||
[LetterKey.VK_U] = ["ʊ", "ʉ"],
|
||||
[LetterKey.VK_V] = ["ʌ", "ʋ", "ⱱ"],
|
||||
[LetterKey.VK_W] = ["ʍ", "ɯ", "ɰ"],
|
||||
[LetterKey.VK_X] = ["χ"],
|
||||
[LetterKey.VK_Y] = ["ʎ", "ʏ"],
|
||||
[LetterKey.VK_Z] = ["ʒ", "ʐ", "ʑ"],
|
||||
[LetterKey.VK_COMMA] = ["ʡ", "ʔ", "ʕ", "ʢ"],
|
||||
[LetterKey.VK_COMMA] = ["ʔ", "ʕ", "ʡ", "ʢ"],
|
||||
[LetterKey.VK_PERIOD] = ["ˈ", "ˌ", "ː", "ʼ", "\u031D", "\u0325", "\u031A", "\u0361", "\u035C"],
|
||||
[LetterKey.VK_SLASH_] = ["ʔ"],
|
||||
}),
|
||||
|
||||
new(Language.IT, "Italian", LanguageGroup.Language, new Dictionary<LetterKey, string[]>
|
||||
|
||||
@@ -11,6 +11,8 @@ using PowerAccent.Core.Services;
|
||||
using PowerAccent.Core.Tools;
|
||||
using PowerToys.PowerAccentKeyboardService;
|
||||
|
||||
using PowerAccentActivationKey = Microsoft.PowerToys.Settings.UI.Library.Enumerations.PowerAccentActivationKey;
|
||||
|
||||
namespace PowerAccent.Core;
|
||||
|
||||
public partial class PowerAccent : IDisposable
|
||||
@@ -22,6 +24,7 @@ public partial class PowerAccent : IDisposable
|
||||
private const double ScreenMinPadding = 150;
|
||||
|
||||
private bool _visible;
|
||||
private int _showGeneration;
|
||||
private string[] _characters = Array.Empty<string>();
|
||||
private string[] _characterDescriptions = Array.Empty<string>();
|
||||
private int _selectedIndex = -1;
|
||||
@@ -95,24 +98,49 @@ public partial class PowerAccent : IDisposable
|
||||
|
||||
private void ShowToolbar(LetterKey letterKey)
|
||||
{
|
||||
_initialShiftState = WindowsFunctions.IsShiftState();
|
||||
_visible = true;
|
||||
|
||||
_characters = GetCharacters(letterKey);
|
||||
_characterDescriptions = GetCharacterDescriptions(_characters);
|
||||
_showUnicodeDescription = _settingService.ShowUnicodeDescription;
|
||||
bool isPressAndHold = _settingService.ActivationKey == PowerAccentActivationKey.PressAndHold;
|
||||
|
||||
Task.Delay(_settingService.InputTime).ContinueWith(
|
||||
// Each summon gets a generation id so a delayed render queued by an earlier
|
||||
// press can't fire for a newer one (or after the toolbar was hidden).
|
||||
int generation = ++_showGeneration;
|
||||
|
||||
// Trigger modes navigate the instant the toolbar is summoned, so the character data must
|
||||
// be ready synchronously. Press-and-hold can't navigate until the popup is actually shown,
|
||||
// so defer the (relatively expensive) character/description build to the delayed render and
|
||||
// keep quick taps off the keystroke hot path.
|
||||
if (!isPressAndHold)
|
||||
{
|
||||
PrepareCharacters(letterKey);
|
||||
}
|
||||
|
||||
int displayDelay = isPressAndHold ? _settingService.HoldDuration : _settingService.InputTime;
|
||||
|
||||
Task.Delay(displayDelay).ContinueWith(
|
||||
t =>
|
||||
{
|
||||
if (_visible)
|
||||
if (_visible && generation == _showGeneration)
|
||||
{
|
||||
if (isPressAndHold)
|
||||
{
|
||||
PrepareCharacters(letterKey);
|
||||
}
|
||||
|
||||
OnChangeDisplay?.Invoke(true, _characters);
|
||||
}
|
||||
},
|
||||
TaskScheduler.FromCurrentSynchronizationContext());
|
||||
}
|
||||
|
||||
private void PrepareCharacters(LetterKey letterKey)
|
||||
{
|
||||
_initialShiftState = WindowsFunctions.IsShiftState();
|
||||
_characters = GetCharacters(letterKey);
|
||||
_characterDescriptions = GetCharacterDescriptions(_characters);
|
||||
_showUnicodeDescription = _settingService.ShowUnicodeDescription;
|
||||
}
|
||||
|
||||
private string[] GetCharacters(LetterKey letterKey)
|
||||
{
|
||||
var characters = CharacterMappings.GetCharacters(letterKey, _settingService.SelectedLang);
|
||||
@@ -237,10 +265,18 @@ public partial class PowerAccent : IDisposable
|
||||
OnChangeDisplay?.Invoke(false, null);
|
||||
_selectedIndex = -1;
|
||||
_visible = false;
|
||||
_showGeneration++;
|
||||
}
|
||||
|
||||
private void ProcessNextChar(TriggerKey triggerKey, bool shiftPressed)
|
||||
{
|
||||
// Press-and-hold builds its character set lazily when the popup renders; ignore any
|
||||
// navigation that races ahead of it (there is nothing to select yet).
|
||||
if (_characters.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Use an async hardware check as a fallback in case the keyboard hook misses a
|
||||
// quick Shift press. If the popup was opened while holding Shift (e.g., typing a
|
||||
// capital letter), ignore the hardware check so we don't accidentally trigger a
|
||||
|
||||
@@ -59,6 +59,9 @@ public class SettingsService
|
||||
InputTime = settings.Properties.InputTime.Value;
|
||||
_keyboardListener.UpdateInputTime(InputTime);
|
||||
|
||||
HoldDuration = settings.Properties.HoldDuration.Value;
|
||||
_keyboardListener.UpdateHoldDuration(HoldDuration);
|
||||
|
||||
ExcludedApps = settings.Properties.ExcludedApps.Value;
|
||||
_keyboardListener.UpdateExcludedApps(ExcludedApps);
|
||||
|
||||
@@ -196,6 +199,8 @@ public class SettingsService
|
||||
}
|
||||
}
|
||||
|
||||
public int HoldDuration { get; set; } = PowerAccentSettings.DefaultHoldDurationMs;
|
||||
|
||||
private string _excludedApps;
|
||||
|
||||
public string ExcludedApps
|
||||
|
||||
@@ -96,6 +96,11 @@ namespace winrt::PowerToys::PowerAccentKeyboardService::implementation
|
||||
m_settings.inputTime = std::chrono::milliseconds(inputTime);
|
||||
}
|
||||
|
||||
void KeyboardListener::UpdateHoldDuration(int32_t holdDuration)
|
||||
{
|
||||
m_settings.holdDuration = std::chrono::milliseconds(holdDuration);
|
||||
}
|
||||
|
||||
void KeyboardListener::UpdateExcludedApps(std::wstring_view excludedAppsView)
|
||||
{
|
||||
std::vector<std::wstring> excludedApps;
|
||||
@@ -123,6 +128,17 @@ namespace winrt::PowerToys::PowerAccentKeyboardService::implementation
|
||||
return m_settings.doNotActivateOnGameMode && detect_game_mode();
|
||||
}
|
||||
|
||||
bool KeyboardListener::IsBlockingModifierDown()
|
||||
{
|
||||
// Ctrl / Alt (including AltGr = Ctrl+Alt) / Win turn a held letter into a shortcut,
|
||||
// so they must not trigger press-and-hold. Shift is intentionally allowed so that
|
||||
// uppercase accents still work.
|
||||
return (GetAsyncKeyState(VK_CONTROL) & 0x8000) ||
|
||||
(GetAsyncKeyState(VK_MENU) & 0x8000) ||
|
||||
(GetAsyncKeyState(VK_LWIN) & 0x8000) ||
|
||||
(GetAsyncKeyState(VK_RWIN) & 0x8000);
|
||||
}
|
||||
|
||||
bool KeyboardListener::IsForegroundAppExcluded()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex_excluded_apps);
|
||||
@@ -181,6 +197,25 @@ namespace winrt::PowerToys::PowerAccentKeyboardService::implementation
|
||||
letterPressed = letterKey;
|
||||
}
|
||||
|
||||
// Press-and-hold activation: the held letter itself opens the toolbar after the hold
|
||||
// duration. The base letter still types on first press; auto-repeats are swallowed above.
|
||||
if (m_settings.activationKey == PowerAccentActivationKey::PressAndHold &&
|
||||
!m_toolbarVisible &&
|
||||
letterPressed != LetterKey::None &&
|
||||
letterKey == letterPressed &&
|
||||
!IsBlockingModifierDown() &&
|
||||
!IsSuppressedByGameMode() &&
|
||||
!IsForegroundAppExcluded())
|
||||
{
|
||||
Logger::debug(L"Show toolbar (press-and-hold). Letter: {}", letterPressed);
|
||||
m_triggeredWithSpace = false;
|
||||
m_triggeredWithLeftArrow = false;
|
||||
m_triggeredWithRightArrow = false;
|
||||
m_toolbarVisible = true;
|
||||
m_showToolbarCb(letterPressed);
|
||||
return false;
|
||||
}
|
||||
|
||||
UINT triggerPressed = 0;
|
||||
if (letterPressed != LetterKey::None)
|
||||
{
|
||||
@@ -199,7 +234,9 @@ namespace winrt::PowerToys::PowerAccentKeyboardService::implementation
|
||||
}
|
||||
}
|
||||
|
||||
if (!m_toolbarVisible && letterPressed != LetterKey::None && triggerPressed && !IsSuppressedByGameMode() && !IsForegroundAppExcluded())
|
||||
// Trigger-key activation (letter + Space/arrow) is exclusive to the non-hold modes.
|
||||
if (m_settings.activationKey != PowerAccentActivationKey::PressAndHold &&
|
||||
!m_toolbarVisible && letterPressed != LetterKey::None && triggerPressed && !IsSuppressedByGameMode() && !IsForegroundAppExcluded())
|
||||
{
|
||||
Logger::debug(L"Show toolbar. Letter: {}, Trigger: {}", letterPressed, triggerPressed);
|
||||
|
||||
@@ -211,7 +248,14 @@ namespace winrt::PowerToys::PowerAccentKeyboardService::implementation
|
||||
m_showToolbarCb(letterPressed);
|
||||
}
|
||||
|
||||
if (m_toolbarVisible && triggerPressed)
|
||||
// In press-and-hold the popup only appears once the hold duration elapses, so Space/arrow
|
||||
// must pass through until then; treat the picker as interactive only once it is shown.
|
||||
const bool pickerInteractive =
|
||||
m_toolbarVisible &&
|
||||
(m_settings.activationKey != PowerAccentActivationKey::PressAndHold ||
|
||||
m_stopwatch.elapsed() >= m_settings.holdDuration);
|
||||
|
||||
if (pickerInteractive && triggerPressed)
|
||||
{
|
||||
if (triggerPressed == VK_LEFT)
|
||||
{
|
||||
@@ -247,13 +291,27 @@ namespace winrt::PowerToys::PowerAccentKeyboardService::implementation
|
||||
m_rightShiftPressed = false;
|
||||
}
|
||||
|
||||
if (std::find(std::begin(letters), end(letters), static_cast<LetterKey>(info.vkCode)) != end(letters) && m_isLanguageLetterCb(static_cast<LetterKey>(info.vkCode)))
|
||||
const auto releasedLetter = static_cast<LetterKey>(info.vkCode);
|
||||
if (std::find(std::begin(letters), end(letters), releasedLetter) != end(letters) && m_isLanguageLetterCb(releasedLetter))
|
||||
{
|
||||
// Only react to the key-up of the letter that owns the toolbar, so releasing a
|
||||
// different held letter can't cancel or commit the active picker.
|
||||
if (letterPressed != releasedLetter)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
letterPressed = LetterKey::None;
|
||||
|
||||
if (m_toolbarVisible)
|
||||
{
|
||||
if (m_stopwatch.elapsed() < m_settings.inputTime)
|
||||
// Press-and-hold uses its own (typically longer) hold duration as the
|
||||
// minimum-hold threshold; the trigger-key modes use inputTime.
|
||||
const auto activationThreshold =
|
||||
m_settings.activationKey == PowerAccentActivationKey::PressAndHold
|
||||
? m_settings.holdDuration
|
||||
: m_settings.inputTime;
|
||||
if (m_stopwatch.elapsed() < activationThreshold)
|
||||
{
|
||||
Logger::debug(L"Activation too fast. Do nothing.");
|
||||
|
||||
@@ -275,7 +333,11 @@ namespace winrt::PowerToys::PowerAccentKeyboardService::implementation
|
||||
m_hideToolbarCb(InputType::None);
|
||||
}
|
||||
m_toolbarVisible = false;
|
||||
return true;
|
||||
|
||||
// In press-and-hold the base letter already typed on key-down and no trigger
|
||||
// key was consumed, so let this key-up pass through to avoid a stuck-key
|
||||
// perception. Trigger modes keep swallowing it as before.
|
||||
return m_settings.activationKey != PowerAccentActivationKey::PressAndHold;
|
||||
}
|
||||
Logger::debug(L"Hide toolbar event and input char");
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ namespace winrt::PowerToys::PowerAccentKeyboardService::implementation
|
||||
LeftRightArrow,
|
||||
Space,
|
||||
Both,
|
||||
PressAndHold,
|
||||
};
|
||||
|
||||
struct PowerAccentSettings
|
||||
@@ -18,6 +19,7 @@ namespace winrt::PowerToys::PowerAccentKeyboardService::implementation
|
||||
PowerAccentActivationKey activationKey{ PowerAccentActivationKey::Both };
|
||||
bool doNotActivateOnGameMode{ true };
|
||||
std::chrono::milliseconds inputTime{ 300 }; // Should match with UI.Library.PowerAccentSettings.DefaultInputTimeMs
|
||||
std::chrono::milliseconds holdDuration{ 500 }; // Should match with UI.Library.PowerAccentSettings.DefaultHoldDurationMs
|
||||
std::vector<std::wstring> excludedApps;
|
||||
};
|
||||
|
||||
@@ -39,6 +41,7 @@ namespace winrt::PowerToys::PowerAccentKeyboardService::implementation
|
||||
void UpdateActivationKey(int32_t activationKey);
|
||||
void UpdateDoNotActivateOnGameMode(bool doNotActivateOnGameMode);
|
||||
void UpdateInputTime(int32_t inputTime);
|
||||
void UpdateHoldDuration(int32_t holdDuration);
|
||||
void UpdateExcludedApps(std::wstring_view excludedApps);
|
||||
|
||||
static LRESULT CALLBACK LowLevelKeyboardProc(int nCode, WPARAM wParam, LPARAM lParam);
|
||||
@@ -48,6 +51,7 @@ namespace winrt::PowerToys::PowerAccentKeyboardService::implementation
|
||||
bool OnKeyUp(KBDLLHOOKSTRUCT info) noexcept;
|
||||
bool IsSuppressedByGameMode();
|
||||
bool IsForegroundAppExcluded();
|
||||
bool IsBlockingModifierDown();
|
||||
|
||||
static inline KeyboardListener* s_instance;
|
||||
HHOOK s_llKeyboardHook = nullptr;
|
||||
|
||||
@@ -83,6 +83,7 @@ namespace PowerToys
|
||||
void UpdateActivationKey(Int32 activationKey);
|
||||
void UpdateDoNotActivateOnGameMode(Boolean doNotActivateOnGameMode);
|
||||
void UpdateInputTime(Int32 inputTime);
|
||||
void UpdateHoldDuration(Int32 holdDuration);
|
||||
void UpdateExcludedApps(String excludedApps);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
// 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.Text.Json;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace PowerDisplay.UnitTests;
|
||||
|
||||
/// <summary>
|
||||
/// Covers the persisted shape of the mouse-wheel-increment setting on
|
||||
/// <see cref="PowerDisplayProperties"/>: its default of 5 (the historical hardcoded step),
|
||||
/// its snake_case JSON key, round-trip fidelity, and the forward-compatibility promise that
|
||||
/// settings.json written before the feature existed deserializes to the default of 5 with no
|
||||
/// migration.
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class MouseWheelIncrementSettingsTests
|
||||
{
|
||||
[TestMethod]
|
||||
public void Default_IsFive()
|
||||
{
|
||||
var properties = new PowerDisplayProperties();
|
||||
|
||||
Assert.AreEqual(5, properties.MouseWheelIncrement, "Default must preserve the historical hardcoded step of 5.");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Deserialize_LegacyJsonMissingField_DefaultsToFive()
|
||||
{
|
||||
// A settings.json captured before this feature shipped has no mouse_wheel_increment key.
|
||||
// Deserializing must fall back to the constructor default of 5, not 0. System.Text.Json
|
||||
// calls the parameterless constructor (which sets MouseWheelIncrement = 5) and then fills
|
||||
// only the fields present in JSON. If PowerDisplayProperties ever gains a
|
||||
// [JsonConstructor]-annotated constructor, re-verify this "defaults to 5" behavior.
|
||||
const string legacyJson = """
|
||||
{
|
||||
"monitor_refresh_delay": 5,
|
||||
"restore_settings_on_startup": false,
|
||||
"show_system_tray_icon": true
|
||||
}
|
||||
""";
|
||||
|
||||
var properties = JsonSerializer.Deserialize<PowerDisplayProperties>(legacyJson);
|
||||
|
||||
Assert.IsNotNull(properties);
|
||||
Assert.AreEqual(5, properties.MouseWheelIncrement);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void RoundTrip_PreservesValue()
|
||||
{
|
||||
var original = new PowerDisplayProperties { MouseWheelIncrement = 15 };
|
||||
|
||||
var json = JsonSerializer.Serialize(original);
|
||||
var restored = JsonSerializer.Deserialize<PowerDisplayProperties>(json);
|
||||
|
||||
Assert.IsNotNull(restored);
|
||||
Assert.AreEqual(15, restored.MouseWheelIncrement);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Serialize_UsesSnakeCaseJsonKey()
|
||||
{
|
||||
var properties = new PowerDisplayProperties { MouseWheelIncrement = 10 };
|
||||
|
||||
var json = JsonSerializer.Serialize(properties);
|
||||
|
||||
StringAssert.Contains(json, "\"mouse_wheel_increment\":10");
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ namespace PowerDisplay.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// PowerDisplay-local window helpers. Flyout positioning/sizing now lives in
|
||||
/// <c>Microsoft.PowerToys.Common.UI.Flyout.FlyoutWindowHelper</c> (Common.UI.Controls).
|
||||
/// <c>Microsoft.PowerToys.Common.UI.Controls.Window.FlyoutWindowHelper</c> (Common.UI.Controls).
|
||||
/// </summary>
|
||||
internal static partial class WindowHelper
|
||||
{
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using Microsoft.PowerToys.Common.UI.Controls.Flyout;
|
||||
using Microsoft.PowerToys.Common.UI.Controls.Window;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Windowing;
|
||||
using PowerDisplay.Configuration;
|
||||
|
||||
@@ -235,7 +235,7 @@
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
helpers:SliderExtensions.IsMouseWheelEnabled="True"
|
||||
helpers:SliderExtensions.MouseWheelChange="5"
|
||||
helpers:SliderExtensions.MouseWheelChange="{x:Bind ViewModel.MouseWheelIncrement, Mode=OneWay}"
|
||||
IsEnabled="{x:Bind ViewModel.IsLinkedBrightnessSliderEnabled, Mode=OneWay}"
|
||||
IsTabStop="True"
|
||||
Maximum="100"
|
||||
@@ -525,7 +525,7 @@
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
helpers:SliderExtensions.IsMouseWheelEnabled="True"
|
||||
helpers:SliderExtensions.MouseWheelChange="5"
|
||||
helpers:SliderExtensions.MouseWheelChange="{x:Bind MouseWheelIncrement, Mode=OneWay}"
|
||||
IsEnabled="{x:Bind IsBrightnessSliderEnabled, Mode=OneWay}"
|
||||
IsTabStop="True"
|
||||
Maximum="100"
|
||||
@@ -556,7 +556,7 @@
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
helpers:SliderExtensions.IsMouseWheelEnabled="True"
|
||||
helpers:SliderExtensions.MouseWheelChange="5"
|
||||
helpers:SliderExtensions.MouseWheelChange="{x:Bind MouseWheelIncrement, Mode=OneWay}"
|
||||
IsEnabled="{x:Bind IsInteractionEnabled, Mode=OneWay}"
|
||||
IsTabStop="True"
|
||||
Maximum="100"
|
||||
@@ -586,7 +586,7 @@
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
helpers:SliderExtensions.IsMouseWheelEnabled="True"
|
||||
helpers:SliderExtensions.MouseWheelChange="5"
|
||||
helpers:SliderExtensions.MouseWheelChange="{x:Bind MouseWheelIncrement, Mode=OneWay}"
|
||||
IsEnabled="{x:Bind IsInteractionEnabled, Mode=OneWay}"
|
||||
IsTabStop="True"
|
||||
Maximum="100"
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Common.UI.Controls.Flyout;
|
||||
using Microsoft.PowerToys.Common.UI.Controls.Window;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.UI.Windowing;
|
||||
|
||||
@@ -122,6 +122,7 @@ public partial class MainViewModel
|
||||
foreach (var monitor in Monitors)
|
||||
{
|
||||
monitor.RefreshCustomVcpNames();
|
||||
monitor.RefreshMouseWheelIncrement();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -93,6 +93,7 @@ public partial class MainViewModel : ObservableObject, IDisposable
|
||||
IsScanning = true;
|
||||
ShowProfileSwitcher = true;
|
||||
ShowIdentifyMonitorsButton = true;
|
||||
MouseWheelIncrement = 5;
|
||||
|
||||
// Initialize settings utils
|
||||
_settingsUtils = SettingsUtils.Default;
|
||||
@@ -129,6 +130,13 @@ public partial class MainViewModel : ObservableObject, IDisposable
|
||||
[ObservableProperty]
|
||||
public partial bool ShowIdentifyMonitorsButton { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the per-mouse-wheel-notch step applied to every flyout slider. Loaded from
|
||||
/// PowerDisplaySettings; defaults to 5 (the historical hardcoded step).
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
public partial int MouseWheelIncrement { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether brightness slider changes are broadcast to all
|
||||
/// non-excluded monitors as one linked level. Persisted in <c>PowerDisplaySettings</c> so
|
||||
@@ -479,6 +487,7 @@ public partial class MainViewModel : ObservableObject, IDisposable
|
||||
var settings = _settingsUtils.GetSettingsOrDefault<PowerDisplaySettings>(PowerDisplaySettings.ModuleName);
|
||||
ShowProfileSwitcher = settings.Properties.ShowProfileSwitcher;
|
||||
ShowIdentifyMonitorsButton = settings.Properties.ShowIdentifyMonitorsButton;
|
||||
MouseWheelIncrement = settings.Properties.MouseWheelIncrement;
|
||||
|
||||
// Load the linked-brightness exclusion set before applying LinkedLevelsActive. If this
|
||||
// method runs after monitors are already discovered, the toggle hook can seed the master
|
||||
|
||||
@@ -210,6 +210,12 @@ public partial class MonitorViewModel : ObservableObject, IDisposable
|
||||
// Property to access IsInteractionEnabled from parent ViewModel
|
||||
public bool IsInteractionEnabled => _mainViewModel?.IsInteractionEnabled ?? true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the shared per-mouse-wheel-notch step for this monitor's sliders, proxied from the
|
||||
/// owning <see cref="MainViewModel"/>. Falls back to 5 if the owner is unavailable.
|
||||
/// </summary>
|
||||
public int MouseWheelIncrement => _mainViewModel?.MouseWheelIncrement ?? 5;
|
||||
|
||||
public MonitorViewModel(Monitor monitor, MonitorManager monitorManager, MainViewModel mainViewModel)
|
||||
{
|
||||
_monitor = monitor;
|
||||
@@ -669,6 +675,16 @@ public partial class MonitorViewModel : ObservableObject, IDisposable
|
||||
OnPropertyChanged(nameof(AvailableInputSources));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raise <see cref="PropertyChanged"/> for <see cref="MouseWheelIncrement"/> so per-monitor
|
||||
/// sliders pick up a new value after the user changes it in Settings. Called from
|
||||
/// <c>MainViewModel.ApplySettingsFromUI</c>.
|
||||
/// </summary>
|
||||
public void RefreshMouseWheelIncrement()
|
||||
{
|
||||
OnPropertyChanged(nameof(MouseWheelIncrement));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set input source for this monitor
|
||||
/// </summary>
|
||||
|
||||