Compare commits
35 Commits
powerscrip
...
copilot/mi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
79f45e85ab | ||
|
|
caff537592 | ||
|
|
ecfc9ce18d | ||
|
|
d4addaa661 | ||
|
|
d6f20c8ef7 | ||
|
|
8a2870d61d | ||
|
|
74a8c309fb | ||
|
|
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 |
5
.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
|
||||
@@ -1232,6 +1234,8 @@ NOTSRCCOPY
|
||||
NOTSRCERASE
|
||||
Notupdated
|
||||
notwindows
|
||||
NOTXORPEN
|
||||
Nouveaut
|
||||
nowarn
|
||||
NOZORDER
|
||||
NPH
|
||||
@@ -2178,6 +2182,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)
|
||||
|
||||
11
.github/policies/resourceManagement.yml
vendored
@@ -163,7 +163,7 @@ configuration:
|
||||
association: Collaborator
|
||||
then:
|
||||
- addReply:
|
||||
reply: We've identified this issue as a duplicate of an existing one and are closing this thread so discussion stays in one place.<br/><br/>Please see the comment above for the link to the original tracking issue, and feel free to subscribe there for updates.
|
||||
reply: We've identified this issue as a duplicate of an existing one and are closing this thread so discussion stays in one place.<br/><br/>Please see the comment above for the link to the original tracking issue, and feel free to subscribe there for updates.
|
||||
- closeIssue
|
||||
- removeLabel:
|
||||
label: Needs-Triage
|
||||
@@ -257,14 +257,5 @@ configuration:
|
||||
- addReply:
|
||||
reply: "To help debug your layout, please run [this script](https://github.com/microsoft/PowerToys/blob/main/src/modules/MouseUtils/CursorWrap/CursorWrapTests/Capture-MonitorLayout.ps1) and attach the generated JSON output to this thread.\n\nThis allows us to better understand the issue and investigate potential fixes."
|
||||
description:
|
||||
- if:
|
||||
- payloadType: Issue_Comment
|
||||
- commentContains:
|
||||
pattern: "I(( would|'d) (like|love|be happy)| want) (to help|helping|to contribute|contributing|to implement|implementing|to fix|fixing)"
|
||||
isRegex: True
|
||||
then:
|
||||
- addReply:
|
||||
reply: Hi! Your last comment indicates to our system, that you might want to contribute to this feature/fix this bug. Thank you! Please make us aware on our ["Would you like to contribute to PowerToys?" thread](https://github.com/microsoft/PowerToys/issues/28769), as we don't see all the comments. <br /><br />_I'm a bot (beep!) so please excuse any mistakes I may make_
|
||||
description:
|
||||
onFailure:
|
||||
onSuccess:
|
||||
|
||||
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 ?? '';
|
||||
|
||||
|
||||
60
.github/workflows/contribution-intent-reply.md
vendored
Normal file
@@ -0,0 +1,60 @@
|
||||
---
|
||||
description: Reply when a new issue comment indicates intent to contribute
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
roles: all
|
||||
permissions:
|
||||
contents: read
|
||||
issues: read
|
||||
models: read
|
||||
tools:
|
||||
github:
|
||||
toolsets: [issues]
|
||||
safe-outputs:
|
||||
add-comment:
|
||||
max: 1
|
||||
noop:
|
||||
---
|
||||
|
||||
# Contribution Intent Reply
|
||||
|
||||
You handle newly created issue comments in microsoft/PowerToys.
|
||||
|
||||
## Goal
|
||||
|
||||
Detect whether the new comment indicates that the author wants to contribute to fixing or implementing work for the issue.
|
||||
|
||||
## Scope
|
||||
|
||||
- Process **issue comments only**.
|
||||
- If `${{ github.event.issue.pull_request }}` is present, this is a PR comment context and you must do nothing.
|
||||
- Ignore comments from bot accounts.
|
||||
|
||||
## Decision rule
|
||||
|
||||
Reply only when the new comment clearly expresses first-person contribution intent for this issue, such as:
|
||||
|
||||
- "I want to contribute"
|
||||
- "I'd like to help"
|
||||
- "I can implement this"
|
||||
- "I want to fix this"
|
||||
|
||||
Do **not** reply when the comment is only:
|
||||
|
||||
- agreement or feedback without volunteering
|
||||
- third-person suggestions ("someone should fix this")
|
||||
- requests for others to contribute
|
||||
|
||||
When uncertain, do not reply.
|
||||
|
||||
## Required reply text
|
||||
|
||||
When contribution intent is detected, post exactly this comment on the same issue:
|
||||
|
||||
Hi! Your last comment indicates to our system, that you might want to contribute to this feature/fix this bug. Thank you! Please make us aware on our ["Would you like to contribute to PowerToys?" thread](https://github.com/microsoft/PowerToys/issues/28769), as we don't see all the comments. <br /><br />_I'm a bot (beep!) so please excuse any mistakes I may make_
|
||||
|
||||
## Safe output behavior
|
||||
|
||||
- If contribution intent is detected: use `add-comment` once on issue `${{ github.event.issue.number }}` with the required reply text above.
|
||||
- If no reply is needed: use `noop` with a short reason.
|
||||
@@ -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
|
||||
|
||||
@@ -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,164 @@
|
||||
// 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 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>
|
||||
/// </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;
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
/// <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,
|
||||
() =>
|
||||
{
|
||||
_ = 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 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);
|
||||
}
|
||||
|
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 |
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -443,7 +443,7 @@ public sealed partial class MainListPage : DynamicListPage,
|
||||
{
|
||||
specialFallbacks.Add(s);
|
||||
}
|
||||
else
|
||||
else if (s.IsEnabled)
|
||||
{
|
||||
commonFallbacks.Add(s);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -276,6 +281,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)
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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..]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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[]>
|
||||
|
||||
@@ -22,6 +22,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;
|
||||
@@ -98,6 +99,10 @@ public partial class PowerAccent : IDisposable
|
||||
_initialShiftState = WindowsFunctions.IsShiftState();
|
||||
_visible = true;
|
||||
|
||||
// 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;
|
||||
|
||||
_characters = GetCharacters(letterKey);
|
||||
_characterDescriptions = GetCharacterDescriptions(_characters);
|
||||
_showUnicodeDescription = _settingService.ShowUnicodeDescription;
|
||||
@@ -105,7 +110,7 @@ public partial class PowerAccent : IDisposable
|
||||
Task.Delay(_settingService.InputTime).ContinueWith(
|
||||
t =>
|
||||
{
|
||||
if (_visible)
|
||||
if (_visible && generation == _showGeneration)
|
||||
{
|
||||
OnChangeDisplay?.Invoke(true, _characters);
|
||||
}
|
||||
@@ -237,6 +242,7 @@ public partial class PowerAccent : IDisposable
|
||||
OnChangeDisplay?.Invoke(false, null);
|
||||
_selectedIndex = -1;
|
||||
_visible = false;
|
||||
_showGeneration++;
|
||||
}
|
||||
|
||||
private void ProcessNextChar(TriggerKey triggerKey, bool shiftPressed)
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 433 B After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 433 B After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 328 B After Width: | Height: | Size: 2.0 KiB |
@@ -7,7 +7,7 @@ using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Common.UI.Controls.Flyout;
|
||||
using Microsoft.PowerToys.Common.UI.Controls.Window;
|
||||
using Microsoft.PowerToys.QuickAccess.Services;
|
||||
using Microsoft.PowerToys.QuickAccess.ViewModels;
|
||||
using Microsoft.UI.Dispatching;
|
||||
|
||||
@@ -44,6 +44,24 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
[JsonPropertyName("spotlight_mode")]
|
||||
public BoolProperty SpotlightMode { get; set; }
|
||||
|
||||
[JsonPropertyName("ripple_mode")]
|
||||
public BoolProperty RippleMode { get; set; }
|
||||
|
||||
[JsonPropertyName("ripple_size")]
|
||||
public IntProperty RippleSize { get; set; }
|
||||
|
||||
[JsonPropertyName("ripple_intensity")]
|
||||
public DoubleProperty RippleIntensity { get; set; }
|
||||
|
||||
[JsonPropertyName("ripple_duration_ms")]
|
||||
public IntProperty RippleDurationMs { get; set; }
|
||||
|
||||
[JsonPropertyName("ripple_show_drag_trail")]
|
||||
public BoolProperty RippleShowDragTrail { get; set; }
|
||||
|
||||
[JsonPropertyName("ripple_show_release_pulse")]
|
||||
public BoolProperty RippleShowReleasePulse { get; set; }
|
||||
|
||||
public MouseHighlighterProperties()
|
||||
{
|
||||
ActivationShortcut = DefaultActivationShortcut;
|
||||
@@ -51,11 +69,17 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
RightButtonClickColor = new StringProperty("#a60000FF");
|
||||
AlwaysColor = new StringProperty("#00FF0000");
|
||||
HighlightOpacity = new IntProperty(166); // for migration from <=1.1 to 1.2
|
||||
HighlightRadius = new IntProperty(20);
|
||||
HighlightFadeDelayMs = new IntProperty(500);
|
||||
HighlightFadeDurationMs = new IntProperty(250);
|
||||
HighlightRadius = new IntProperty(30);
|
||||
HighlightFadeDelayMs = new IntProperty(400);
|
||||
HighlightFadeDurationMs = new IntProperty(400);
|
||||
AutoActivate = new BoolProperty(false);
|
||||
SpotlightMode = new BoolProperty(false);
|
||||
RippleMode = new BoolProperty(true);
|
||||
RippleSize = new IntProperty(60);
|
||||
RippleIntensity = new DoubleProperty(0.7);
|
||||
RippleDurationMs = new IntProperty(480);
|
||||
RippleShowDragTrail = new BoolProperty(true);
|
||||
RippleShowReleasePulse = new BoolProperty(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
{
|
||||
ActivationShortcut = DefaultActivationShortcut;
|
||||
MonitorRefreshDelay = 5;
|
||||
MouseWheelIncrement = 5;
|
||||
Monitors = new List<MonitorInfo>();
|
||||
RestoreSettingsOnStartup = false;
|
||||
ShowSystemTrayIcon = true;
|
||||
@@ -48,6 +49,13 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
[JsonPropertyName("monitor_refresh_delay")]
|
||||
public int MonitorRefreshDelay { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the amount each PowerDisplay flyout slider (brightness, contrast, volume)
|
||||
/// changes per mouse-wheel notch. Defaults to 5, the historical hardcoded step.
|
||||
/// </summary>
|
||||
[JsonPropertyName("mouse_wheel_increment")]
|
||||
public int MouseWheelIncrement { get; set; }
|
||||
|
||||
[JsonPropertyName("monitors")]
|
||||
public List<MonitorInfo> Monitors { get; set; }
|
||||
|
||||
|
||||
44
src/settings-ui/Settings.UI/Helpers/TitleBarHelper.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
// 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;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Windows.UI;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Helpers for theming the system caption buttons (minimize/maximize/close) of a window.
|
||||
/// </summary>
|
||||
public static class TitleBarHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Applies the given element theme to a window's system caption buttons.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Workaround for the AppWindow TitleBar not updating caption button colors to match the
|
||||
/// app theme when the OS theme differs from the app theme or the theme changes at runtime.
|
||||
/// Mirrors the helper used by the WinUI Gallery (https://github.com/microsoft/WinUI-Gallery).
|
||||
/// </remarks>
|
||||
public static void ApplySystemThemeToCaptionButtons(Window window, ElementTheme theme)
|
||||
{
|
||||
if (window?.AppWindow is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var titleBar = window.AppWindow.TitleBar;
|
||||
var foregroundColor = theme == ElementTheme.Dark ? Colors.White : Colors.Black;
|
||||
|
||||
titleBar.ButtonBackgroundColor = Colors.Transparent;
|
||||
titleBar.ButtonInactiveBackgroundColor = Colors.Transparent;
|
||||
titleBar.ButtonForegroundColor = foregroundColor;
|
||||
titleBar.ButtonHoverForegroundColor = foregroundColor;
|
||||
titleBar.ButtonInactiveForegroundColor = Colors.DarkGray;
|
||||
titleBar.ButtonHoverBackgroundColor = theme == ElementTheme.Dark
|
||||
? Color.FromArgb(24, 255, 255, 255)
|
||||
: Color.FromArgb(24, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,11 +52,18 @@
|
||||
<tkcontrols:MarkdownTextBlock
|
||||
x:Name="ReleaseNotesMarkdown"
|
||||
Config="{StaticResource ReleaseNotesMarkdownConfig}"
|
||||
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
|
||||
UseAutoLinks="True"
|
||||
UseEmphasisExtras="True"
|
||||
UseListExtras="True"
|
||||
UsePipeTables="True"
|
||||
UseTaskLists="True" />
|
||||
<!-- Hidden helper used to resolve the accent brush for the active element theme (see ApplyMarkdownThemeWorkaround). -->
|
||||
<TextBlock
|
||||
x:Name="LinkBrushProvider"
|
||||
Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}"
|
||||
IsHitTestVisible="False"
|
||||
Visibility="Collapsed" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
@@ -7,10 +7,12 @@ using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using CommunityToolkit.WinUI.Controls;
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Settings.UI.Helpers;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Microsoft.UI.Xaml.Media.Imaging;
|
||||
using Microsoft.UI.Xaml.Navigation;
|
||||
|
||||
@@ -19,6 +21,7 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
|
||||
public sealed partial class ScoobeReleaseNotesPage : Page
|
||||
{
|
||||
private IList<PowerToysReleaseInfo> _currentReleases;
|
||||
private string _releaseNotesMarkdownText;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ScoobeReleaseNotesPage"/> class.
|
||||
@@ -26,6 +29,37 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
|
||||
public ScoobeReleaseNotesPage()
|
||||
{
|
||||
this.InitializeComponent();
|
||||
|
||||
// Re-apply the markdown theme workaround when the theme changes at runtime so the
|
||||
// headings/links stay readable after the user switches between light and dark.
|
||||
this.ActualThemeChanged += OnActualThemeChanged;
|
||||
this.Unloaded += OnUnloaded;
|
||||
}
|
||||
|
||||
private void OnUnloaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
this.ActualThemeChanged -= OnActualThemeChanged;
|
||||
this.Unloaded -= OnUnloaded;
|
||||
}
|
||||
|
||||
private void OnActualThemeChanged(FrameworkElement sender, object args)
|
||||
{
|
||||
RefreshMarkdownTheme();
|
||||
}
|
||||
|
||||
private void RefreshMarkdownTheme()
|
||||
{
|
||||
if (string.IsNullOrEmpty(_releaseNotesMarkdownText))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ApplyMarkdownThemeWorkaround();
|
||||
|
||||
// The MarkdownTextBlock captures heading/link brushes when it renders, so re-set the
|
||||
// text to force it to rebuild with the brushes for the now-active theme.
|
||||
ReleaseNotesMarkdown.Text = string.Empty;
|
||||
ReleaseNotesMarkdown.Text = _releaseNotesMarkdownText;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -128,7 +162,18 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
|
||||
{
|
||||
LoadingProgressRing.Visibility = Visibility.Collapsed;
|
||||
|
||||
// Workaround: the MarkdownTextBlock control captures its heading foreground
|
||||
// brushes from Application.Current.Resources when its theme config is created,
|
||||
// which resolves against the OS (application) theme rather than the app's
|
||||
// selected theme. When the OS is Light but PowerToys is Dark (or vice versa),
|
||||
// headings render with an unreadable color. Force the control's theme and
|
||||
// reapply correctly-themed heading brushes before the markdown is rendered.
|
||||
// TODO: Remove once the upstream control resolves brushes against the element theme.
|
||||
// Upstream fix: https://github.com/CommunityToolkit/Labs-Windows/pull/785
|
||||
ApplyMarkdownThemeWorkaround();
|
||||
|
||||
var (releaseNotesMarkdown, heroImageUrl) = ProcessReleaseNotesMarkdown(_currentReleases);
|
||||
_releaseNotesMarkdownText = releaseNotesMarkdown;
|
||||
|
||||
// Set the Hero image if found
|
||||
if (!string.IsNullOrEmpty(heroImageUrl))
|
||||
@@ -150,6 +195,46 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Works around the <see cref="MarkdownTextBlock"/> control pinning its heading and link
|
||||
/// brushes to the OS (application) theme instead of the element's selected theme, which makes
|
||||
/// titles/links unreadable when the OS and PowerToys themes differ. Pins the control's theme and
|
||||
/// reassigns the heading/link brushes resolved for the selected theme before the markdown renders.
|
||||
/// TODO: Remove once the upstream control resolves brushes against the element theme.
|
||||
/// Upstream fix: https://github.com/CommunityToolkit/Labs-Windows/pull/785
|
||||
/// </summary>
|
||||
private void ApplyMarkdownThemeWorkaround()
|
||||
{
|
||||
var elementTheme = App.IsDarkTheme() ? ElementTheme.Dark : ElementTheme.Light;
|
||||
ReleaseNotesMarkdown.RequestedTheme = elementTheme;
|
||||
LinkBrushProvider.RequestedTheme = elementTheme;
|
||||
|
||||
if (Resources["ReleaseNotesMarkdownConfig"] is MarkdownConfig config
|
||||
&& config.Themes is MarkdownThemes themes)
|
||||
{
|
||||
// The control's Foreground is bound to TextFillColorPrimaryBrush via ThemeResource,
|
||||
// so after setting RequestedTheme it resolves to the brush for the selected theme.
|
||||
// Reuse it for the heading brushes, which the control would otherwise pin to the OS theme.
|
||||
if (ReleaseNotesMarkdown.Foreground is Brush headingForeground)
|
||||
{
|
||||
themes.H1Foreground = headingForeground;
|
||||
themes.H2Foreground = headingForeground;
|
||||
themes.H3Foreground = headingForeground;
|
||||
themes.H4Foreground = headingForeground;
|
||||
themes.H5Foreground = headingForeground;
|
||||
themes.H6Foreground = headingForeground;
|
||||
}
|
||||
|
||||
// The link brush is likewise pinned to the OS theme's accent color, which can be
|
||||
// unreadable when the app theme differs from the OS theme. Reapply the accent brush
|
||||
// resolved for the selected theme using the hidden helper element.
|
||||
if (LinkBrushProvider.Foreground is Brush linkForeground)
|
||||
{
|
||||
themes.LinkForeground = linkForeground;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Page_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
DisplayReleaseNotes();
|
||||
|
||||
@@ -57,6 +57,16 @@ namespace Microsoft.PowerToys.Settings.UI
|
||||
this.ExtendsContentIntoTitleBar = true;
|
||||
this.SetTitleBar(AppTitleBar);
|
||||
Title = ResourceLoaderInstance.ResourceLoader.GetString("ScoobeWindow_Title");
|
||||
|
||||
// The built-in WinUI TitleBar does not tint the system caption buttons (min/max/close)
|
||||
// to match the app's selected theme, so they can be unreadable when the OS theme differs
|
||||
// from the PowerToys theme. Drive their colors from the window content's actual theme.
|
||||
if (this.Content is FrameworkElement rootElement)
|
||||
{
|
||||
TitleBarHelper.ApplySystemThemeToCaptionButtons(this, rootElement.ActualTheme);
|
||||
rootElement.ActualThemeChanged += (s, e) =>
|
||||
TitleBarHelper.ApplySystemThemeToCaptionButtons(this, s.ActualTheme);
|
||||
}
|
||||
}
|
||||
|
||||
private void Window_Activated(object sender, WindowActivatedEventArgs args)
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
xmlns:local="using:Microsoft.PowerToys.Settings.UI.Helpers"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:panels="using:Microsoft.PowerToys.Settings.UI.Panels"
|
||||
xmlns:ptcontrols="using:Microsoft.PowerToys.Common.UI.Controls"
|
||||
xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls"
|
||||
xmlns:tkconverters="using:CommunityToolkit.WinUI.Converters"
|
||||
xmlns:ui="using:CommunityToolkit.WinUI"
|
||||
@@ -265,30 +266,32 @@
|
||||
</tkcontrols:SettingsExpander>
|
||||
<tkcontrols:SettingsExpander
|
||||
Name="MouseHighlighterAppearanceBehavior"
|
||||
x:Uid="Appearance_Behavior"
|
||||
x:Uid="MouseUtils_MouseHighlighter_Appearance"
|
||||
AutomationProperties.AutomationId="MouseUtils_MouseHighlighterAppearanceBehaviorId"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}"
|
||||
IsEnabled="{x:Bind ViewModel.IsMouseHighlighterEnabled, Mode=OneWay}">
|
||||
<ComboBox
|
||||
x:Uid="MouseUtils_MouseHighlighter_SpotlightModeType"
|
||||
MinWidth="{StaticResource SettingActionControlMinWidth}"
|
||||
SelectedIndex="{x:Bind ViewModel.HighlightModeIndex, Mode=TwoWay}">
|
||||
<ComboBoxItem x:Uid="HighlightMode_Spotlight_Mode" />
|
||||
<ComboBoxItem x:Uid="HighlightMode_Circle_Highlight_Mode" />
|
||||
<ComboBoxItem x:Uid="HighlightMode_Ripple_Mode" />
|
||||
</ComboBox>
|
||||
<tkcontrols:SettingsExpander.Items>
|
||||
<tkcontrols:SettingsCard x:Uid="MouseUtils_MouseHighlighter_PrimaryButtonClickColor" IsEnabled="{x:Bind ViewModel.IsSpotlightModeEnabled, Mode=OneWay, Converter={StaticResource BoolNegationConverter}}">
|
||||
<tkcontrols:SettingsCard x:Uid="MouseUtils_MouseHighlighter_PrimaryButtonClickColor" Visibility="{x:Bind ViewModel.HighlightModeIndex, Mode=OneWay, Converter={StaticResource IndexBitFieldToVisibilityConverter}, ConverterParameter=0x6}">
|
||||
<controls:ColorPickerButton IsAlphaEnabled="True" SelectedColor="{x:Bind Path=ViewModel.MouseHighlighterLeftButtonClickColor, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard x:Uid="MouseUtils_MouseHighlighter_SecondaryButtonClickColor" IsEnabled="{x:Bind ViewModel.IsSpotlightModeEnabled, Mode=OneWay, Converter={StaticResource BoolNegationConverter}}">
|
||||
<tkcontrols:SettingsCard x:Uid="MouseUtils_MouseHighlighter_SecondaryButtonClickColor" Visibility="{x:Bind ViewModel.HighlightModeIndex, Mode=OneWay, Converter={StaticResource IndexBitFieldToVisibilityConverter}, ConverterParameter=0x6}">
|
||||
<controls:ColorPickerButton IsAlphaEnabled="True" SelectedColor="{x:Bind Path=ViewModel.MouseHighlighterRightButtonClickColor, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard Name="MouseUtilsMouseHighlighterAlwaysColor" x:Uid="MouseUtils_MouseHighlighter_AlwaysColor">
|
||||
<tkcontrols:SettingsCard
|
||||
Name="MouseUtilsMouseHighlighterAlwaysColor"
|
||||
x:Uid="MouseUtils_MouseHighlighter_AlwaysColor"
|
||||
Visibility="{x:Bind ViewModel.HighlightModeIndex, Mode=OneWay, Converter={StaticResource IndexBitFieldToVisibilityConverter}, ConverterParameter=0x3}">
|
||||
<controls:ColorPickerButton IsAlphaEnabled="True" SelectedColor="{x:Bind Path=ViewModel.MouseHighlighterAlwaysColor, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard x:Uid="HighlightMode">
|
||||
<ComboBox
|
||||
x:Uid="MouseUtils_MouseHighlighter_SpotlightModeType"
|
||||
MinWidth="{StaticResource SettingActionControlMinWidth}"
|
||||
SelectedIndex="{x:Bind ViewModel.IsSpotlightModeEnabled, Converter={StaticResource ReverseBoolToComboBoxIndexConverter}, Mode=TwoWay}">
|
||||
<ComboBoxItem x:Uid="HighlightMode_Spotlight_Mode" />
|
||||
<ComboBoxItem x:Uid="HighlightMode_Circle_Highlight_Mode" />
|
||||
</ComboBox>
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard x:Uid="MouseUtils_MouseHighlighter_HighlightRadius">
|
||||
<tkcontrols:SettingsCard x:Uid="MouseUtils_MouseHighlighter_HighlightRadius" Visibility="{x:Bind ViewModel.HighlightModeIndex, Mode=OneWay, Converter={StaticResource IndexBitFieldToVisibilityConverter}, ConverterParameter=0x3}">
|
||||
<NumberBox
|
||||
MinWidth="{StaticResource SettingActionControlMinWidth}"
|
||||
LargeChange="10"
|
||||
@@ -297,7 +300,10 @@
|
||||
SpinButtonPlacementMode="Compact"
|
||||
Value="{x:Bind ViewModel.MouseHighlighterRadius, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard Name="MouseUtilsMouseHighlighterFadeDelayMs" x:Uid="MouseUtils_MouseHighlighter_FadeDelayMs">
|
||||
<tkcontrols:SettingsCard
|
||||
Name="MouseUtilsMouseHighlighterFadeDelayMs"
|
||||
x:Uid="MouseUtils_MouseHighlighter_FadeDelayMs"
|
||||
Visibility="{x:Bind ViewModel.HighlightModeIndex, Mode=OneWay, Converter={StaticResource IndexBitFieldToVisibilityConverter}, ConverterParameter=0x3}">
|
||||
<NumberBox
|
||||
MinWidth="{StaticResource SettingActionControlMinWidth}"
|
||||
LargeChange="100"
|
||||
@@ -306,7 +312,10 @@
|
||||
SpinButtonPlacementMode="Compact"
|
||||
Value="{x:Bind ViewModel.MouseHighlighterFadeDelayMs, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard Name="MouseUtilsMouseHighlighterFadeDurationMs" x:Uid="MouseUtils_MouseHighlighter_FadeDurationMs">
|
||||
<tkcontrols:SettingsCard
|
||||
Name="MouseUtilsMouseHighlighterFadeDurationMs"
|
||||
x:Uid="MouseUtils_MouseHighlighter_FadeDurationMs"
|
||||
Visibility="{x:Bind ViewModel.HighlightModeIndex, Mode=OneWay, Converter={StaticResource IndexBitFieldToVisibilityConverter}, ConverterParameter=0x3}">
|
||||
<NumberBox
|
||||
MinWidth="{StaticResource SettingActionControlMinWidth}"
|
||||
LargeChange="100"
|
||||
@@ -315,6 +324,42 @@
|
||||
SpinButtonPlacementMode="Compact"
|
||||
Value="{x:Bind ViewModel.MouseHighlighterFadeDurationMs, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard x:Uid="MouseUtils_MouseHighlighter_RippleSize" Visibility="{x:Bind ViewModel.HighlightModeIndex, Mode=OneWay, Converter={StaticResource IndexBitFieldToVisibilityConverter}, ConverterParameter=0x4}">
|
||||
<NumberBox
|
||||
MinWidth="{StaticResource SettingActionControlMinWidth}"
|
||||
LargeChange="10"
|
||||
Maximum="300"
|
||||
Minimum="10"
|
||||
SmallChange="1"
|
||||
SpinButtonPlacementMode="Compact"
|
||||
Value="{x:Bind ViewModel.RippleSize, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard x:Uid="MouseUtils_MouseHighlighter_RippleIntensity" Visibility="{x:Bind ViewModel.HighlightModeIndex, Mode=OneWay, Converter={StaticResource IndexBitFieldToVisibilityConverter}, ConverterParameter=0x4}">
|
||||
<Slider
|
||||
MinWidth="{StaticResource SettingActionControlMinWidth}"
|
||||
LargeChange="0.1"
|
||||
Maximum="1.35"
|
||||
Minimum="0.15"
|
||||
SmallChange="0.05"
|
||||
StepFrequency="0.05"
|
||||
Value="{x:Bind ViewModel.RippleIntensity, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard x:Uid="MouseUtils_MouseHighlighter_RippleDurationMs" Visibility="{x:Bind ViewModel.HighlightModeIndex, Mode=OneWay, Converter={StaticResource IndexBitFieldToVisibilityConverter}, ConverterParameter=0x4}">
|
||||
<NumberBox
|
||||
MinWidth="{StaticResource SettingActionControlMinWidth}"
|
||||
LargeChange="100"
|
||||
Maximum="2000"
|
||||
Minimum="60"
|
||||
SmallChange="10"
|
||||
SpinButtonPlacementMode="Compact"
|
||||
Value="{x:Bind ViewModel.RippleDurationMs, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard ContentAlignment="Left" Visibility="{x:Bind ViewModel.HighlightModeIndex, Mode=OneWay, Converter={StaticResource IndexBitFieldToVisibilityConverter}, ConverterParameter=0x4}">
|
||||
<ptcontrols:CheckBoxWithDescriptionControl x:Uid="MouseUtils_MouseHighlighter_RippleShowDragTrail" IsChecked="{x:Bind ViewModel.RippleShowDragTrail, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard ContentAlignment="Left" Visibility="{x:Bind ViewModel.HighlightModeIndex, Mode=OneWay, Converter={StaticResource IndexBitFieldToVisibilityConverter}, ConverterParameter=0x4}">
|
||||
<ptcontrols:CheckBoxWithDescriptionControl x:Uid="MouseUtils_MouseHighlighter_RippleShowReleasePulse" IsChecked="{x:Bind ViewModel.RippleShowReleasePulse, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
</tkcontrols:SettingsExpander.Items>
|
||||
</tkcontrols:SettingsExpander>
|
||||
</controls:SettingsGroup>
|
||||
|
||||
@@ -207,7 +207,7 @@
|
||||
<controls:PageLink x:Uid="NewPlus_Learn_More" Link="https://aka.ms/PowerToysOverview_NewPlus" />
|
||||
</controls:SettingsPageControl.PrimaryLinks>
|
||||
<controls:SettingsPageControl.SecondaryLinks>
|
||||
<controls:PageLink Link="https://www.linkedin.com/in/christian-gaardmark/" Text="Christian Gaardmark" />
|
||||
<controls:PageLink Link="https://www.onegreatworld.com/products/productivity-plus-pack/?ref=settings_pt" Text="Based on Christian Gaardmark's New++ from the Productivity Plus Pack" />
|
||||
</controls:SettingsPageControl.SecondaryLinks>
|
||||
</controls:SettingsPageControl>
|
||||
|
||||
|
||||
@@ -84,6 +84,12 @@
|
||||
ItemsSource="{x:Bind ViewModel.MonitorRefreshDelayOptions}"
|
||||
SelectedItem="{x:Bind ViewModel.MonitorRefreshDelay, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard x:Uid="PowerDisplay_MouseWheelIncrement">
|
||||
<ComboBox
|
||||
MinWidth="{StaticResource PowerDisplayCompactActionControlMinWidth}"
|
||||
ItemsSource="{x:Bind ViewModel.MouseWheelIncrementOptions}"
|
||||
SelectedItem="{x:Bind ViewModel.MouseWheelIncrement, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard ContentAlignment="Left">
|
||||
<CheckBox x:Uid="PowerDisplay_RestoreSettingsOnStartup" IsChecked="{x:Bind ViewModel.RestoreSettingsOnStartup, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
|
||||
@@ -1149,6 +1149,12 @@ opera.exe</value>
|
||||
<data name="Appearance_Behavior.Header" xml:space="preserve">
|
||||
<value>Appearance & behavior</value>
|
||||
</data>
|
||||
<data name="MouseUtils_MouseHighlighter_Appearance.Header" xml:space="preserve">
|
||||
<value>Highlight mode</value>
|
||||
</data>
|
||||
<data name="MouseUtils_MouseHighlighter_Appearance.Description" xml:space="preserve">
|
||||
<value>Choose the highlight style and customize its appearance</value>
|
||||
</data>
|
||||
<data name="StartupAndPermissions.Header" xml:space="preserve">
|
||||
<value>Startup & permissions</value>
|
||||
</data>
|
||||
@@ -2811,17 +2817,29 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
|
||||
<value>Fade delay (ms)</value>
|
||||
<comment>ms = milliseconds</comment>
|
||||
</data>
|
||||
<data name="MouseUtils_MouseHighlighter_FadeDelayMs.Description" xml:space="preserve">
|
||||
<value>Time before the highlight begins to fade (ms)</value>
|
||||
<comment>ms = milliseconds</comment>
|
||||
</data>
|
||||
<data name="MouseUtils_MouseHighlighter_FadeDurationMs.Header" xml:space="preserve">
|
||||
<value>Fade duration (ms)</value>
|
||||
<comment>ms = milliseconds</comment>
|
||||
</data>
|
||||
<data name="MouseUtils_MouseHighlighter_FadeDurationMs.Description" xml:space="preserve">
|
||||
<value>Duration of the disappear animation (ms)</value>
|
||||
<comment>ms = milliseconds</comment>
|
||||
<data name="MouseUtils_MouseHighlighter_RippleSize.Header" xml:space="preserve">
|
||||
<value>Size (px)</value>
|
||||
<comment>Ripple mode only. px = pixels. Base radius of the click pulse.</comment>
|
||||
</data>
|
||||
<data name="MouseUtils_MouseHighlighter_RippleIntensity.Header" xml:space="preserve">
|
||||
<value>Intensity</value>
|
||||
<comment>Ripple mode only. Brightness/punch of the pulse.</comment>
|
||||
</data>
|
||||
<data name="MouseUtils_MouseHighlighter_RippleDurationMs.Header" xml:space="preserve">
|
||||
<value>Duration (ms)</value>
|
||||
<comment>Ripple mode only. ms = milliseconds.</comment>
|
||||
</data>
|
||||
<data name="MouseUtils_MouseHighlighter_RippleShowDragTrail.Header" xml:space="preserve">
|
||||
<value>Follow cursor while held</value>
|
||||
<comment>Ripple mode only. Toggle for whether the held ring tracks the cursor when dragged.</comment>
|
||||
</data>
|
||||
<data name="MouseUtils_MouseHighlighter_RippleShowReleasePulse.Header" xml:space="preserve">
|
||||
<value>Show crosshairs on right-click release</value>
|
||||
<comment>Ripple mode only. Toggle for the expanding crosshair lines drawn when a right-click is released.</comment>
|
||||
</data>
|
||||
<data name="MouseUtils_MousePointerCrosshairs.Header" xml:space="preserve">
|
||||
<value>Mouse Pointer Crosshairs</value>
|
||||
@@ -4414,22 +4432,22 @@ Activate by holding the key for the character you want to add an accent to, then
|
||||
</data>
|
||||
<data name="NewPlus.ModuleTitle" xml:space="preserve">
|
||||
<value>New+</value>
|
||||
<comment>New+ is the name of the utility. Localize product name in accordance with Windows New</comment>
|
||||
<comment>New+ is the name of the utility. Localize product name in accordance with Windows New. e.g. French would be Nouveau+ (not Nouveauté+)</comment>
|
||||
</data>
|
||||
<data name="NewPlus.ModuleDescription" xml:space="preserve">
|
||||
<value>Create files and folders from a personalized set of templates</value>
|
||||
</data>
|
||||
<data name="NewPlus_Product_Name.Content" xml:space="preserve">
|
||||
<value>New+</value>
|
||||
<comment>New+ is the name of the utility. Localize product name in accordance with Windows New</comment>
|
||||
<comment>New+ is the name of the utility. Localize product name in accordance with Windows New. e.g. French would be Nouveau+ (not Nouveauté+)</comment>
|
||||
</data>
|
||||
<data name="NewPlus_Learn_More.Text" xml:space="preserve">
|
||||
<value>Learn more about New+</value>
|
||||
<comment>New+ learn more link. Localize product name in accordance with Windows New</comment>
|
||||
<comment>New+ learn more link. Localize product name in accordance with Windows New. e.g. French would be Nouveau+ (not Nouveauté+)</comment>
|
||||
</data>
|
||||
<data name="NewPlus_Enable_Toggle.Header" xml:space="preserve">
|
||||
<value>New+</value>
|
||||
<comment>Localize product name in accordance with Windows New</comment>
|
||||
<comment>Localize product name in accordance with Windows New. e.g. French would be Nouveau+ (not Nouveauté+)</comment>
|
||||
</data>
|
||||
<data name="NewPlus_TemplatesNotBackupAndRestoreWarning.Title" xml:space="preserve">
|
||||
<value>PowerToys "Back up and Restore" feature doesn't take templates into account at this moment. If you use that feature, templates will have to be copied manually.</value>
|
||||
@@ -4516,7 +4534,7 @@ Activate by holding the key for the character you want to add an accent to, then
|
||||
</data>
|
||||
<data name="Oobe_NewPlus.Title" xml:space="preserve">
|
||||
<value>New+</value>
|
||||
<comment>New+ is the name of the utility. Localize product name in accordance with Windows New</comment>
|
||||
<comment>New+ is the name of the utility. Localize product name in accordance with Windows New. e.g. French would be Nouveau+ (not Nouveauté+)</comment>
|
||||
</data>
|
||||
<data name="Oobe_NewPlus.Description" xml:space="preserve">
|
||||
<value>Create files and folders from a personalized set of templates.</value>
|
||||
@@ -5188,7 +5206,7 @@ The break timer font matches the text font.</value>
|
||||
<value>No shortcuts to show.</value>
|
||||
</data>
|
||||
<data name="HighlightMode.Description" xml:space="preserve">
|
||||
<value>Highlight the cursor or dim the screen to spotlight it</value>
|
||||
<value>Highlight the cursor, dim the screen to spotlight it, or pulse a ripple on each click</value>
|
||||
</data>
|
||||
<data name="HighlightMode.Header" xml:space="preserve">
|
||||
<value>Highlight mode</value>
|
||||
@@ -5199,6 +5217,10 @@ The break timer font matches the text font.</value>
|
||||
<data name="HighlightMode_Spotlight_Mode.Content" xml:space="preserve">
|
||||
<value>Spotlight</value>
|
||||
</data>
|
||||
<data name="HighlightMode_Ripple_Mode.Content" xml:space="preserve">
|
||||
<value>Ripple</value>
|
||||
<comment>Name of the highlight mode that draws an expanding ring pulse on each click.</comment>
|
||||
</data>
|
||||
<data name="GeneralPage_EnableDataDiagnosticsText.Text" xml:space="preserve">
|
||||
<value>Helps us make PowerToys faster, more stable, and better over time</value>
|
||||
</data>
|
||||
@@ -5539,6 +5561,12 @@ The break timer font matches the text font.</value>
|
||||
<data name="PowerDisplay_MonitorRefreshDelay.Description" xml:space="preserve">
|
||||
<value>Number of seconds to wait after display changes before refreshing monitors. Increase if monitors are not detected after hot-plug.</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_MouseWheelIncrement.Header" xml:space="preserve">
|
||||
<value>Mouse wheel increment</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_MouseWheelIncrement.Description" xml:space="preserve">
|
||||
<value>How much brightness, contrast, and volume sliders change per mouse wheel notch.</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_AdvancedSettings.Header" xml:space="preserve">
|
||||
<value>Advanced</value>
|
||||
</data>
|
||||
|
||||
@@ -77,6 +77,12 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
string alwaysColor = MouseHighlighterSettingsConfig.Properties.AlwaysColor.Value;
|
||||
_highlighterAlwaysColor = !string.IsNullOrEmpty(alwaysColor) ? alwaysColor : "#00FF0000";
|
||||
_isSpotlightModeEnabled = MouseHighlighterSettingsConfig.Properties.SpotlightMode.Value;
|
||||
_isRippleModeEnabled = MouseHighlighterSettingsConfig.Properties.RippleMode.Value;
|
||||
_rippleSize = MouseHighlighterSettingsConfig.Properties.RippleSize.Value;
|
||||
_rippleIntensity = MouseHighlighterSettingsConfig.Properties.RippleIntensity.Value;
|
||||
_rippleDurationMs = MouseHighlighterSettingsConfig.Properties.RippleDurationMs.Value;
|
||||
_rippleShowDragTrail = MouseHighlighterSettingsConfig.Properties.RippleShowDragTrail.Value;
|
||||
_rippleShowReleasePulse = MouseHighlighterSettingsConfig.Properties.RippleShowReleasePulse.Value;
|
||||
|
||||
_highlighterRadius = MouseHighlighterSettingsConfig.Properties.HighlightRadius.Value;
|
||||
_highlightFadeDelayMs = MouseHighlighterSettingsConfig.Properties.HighlightFadeDelayMs.Value;
|
||||
@@ -608,6 +614,64 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsRippleModeEnabled
|
||||
{
|
||||
get => _isRippleModeEnabled;
|
||||
set
|
||||
{
|
||||
if (_isRippleModeEnabled != value)
|
||||
{
|
||||
_isRippleModeEnabled = value;
|
||||
MouseHighlighterSettingsConfig.Properties.RippleMode.Value = value;
|
||||
NotifyMouseHighlighterPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ComboBox index for the highlight mode selector.
|
||||
// 0 = Spotlight, 1 = Circle, 2 = Ripple
|
||||
public int HighlightModeIndex
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_isSpotlightModeEnabled)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return _isRippleModeEnabled ? 2 : 1;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
bool spotlight = value == 0;
|
||||
bool ripple = value == 2;
|
||||
bool changed = false;
|
||||
|
||||
if (_isSpotlightModeEnabled != spotlight)
|
||||
{
|
||||
_isSpotlightModeEnabled = spotlight;
|
||||
MouseHighlighterSettingsConfig.Properties.SpotlightMode.Value = spotlight;
|
||||
OnPropertyChanged(nameof(IsSpotlightModeEnabled));
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (_isRippleModeEnabled != ripple)
|
||||
{
|
||||
_isRippleModeEnabled = ripple;
|
||||
MouseHighlighterSettingsConfig.Properties.RippleMode.Value = ripple;
|
||||
OnPropertyChanged(nameof(IsRippleModeEnabled));
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (changed)
|
||||
{
|
||||
OnPropertyChanged(nameof(HighlightModeIndex));
|
||||
NotifyMouseHighlighterPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int MouseHighlighterRadius
|
||||
{
|
||||
get
|
||||
@@ -680,6 +744,76 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
public int RippleSize
|
||||
{
|
||||
get => _rippleSize;
|
||||
set
|
||||
{
|
||||
if (value != _rippleSize)
|
||||
{
|
||||
_rippleSize = value;
|
||||
MouseHighlighterSettingsConfig.Properties.RippleSize.Value = value;
|
||||
NotifyMouseHighlighterPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public double RippleIntensity
|
||||
{
|
||||
get => _rippleIntensity;
|
||||
set
|
||||
{
|
||||
if (value != _rippleIntensity)
|
||||
{
|
||||
_rippleIntensity = value;
|
||||
MouseHighlighterSettingsConfig.Properties.RippleIntensity.Value = value;
|
||||
NotifyMouseHighlighterPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int RippleDurationMs
|
||||
{
|
||||
get => _rippleDurationMs;
|
||||
set
|
||||
{
|
||||
if (value != _rippleDurationMs)
|
||||
{
|
||||
_rippleDurationMs = value;
|
||||
MouseHighlighterSettingsConfig.Properties.RippleDurationMs.Value = value;
|
||||
NotifyMouseHighlighterPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool RippleShowDragTrail
|
||||
{
|
||||
get => _rippleShowDragTrail;
|
||||
set
|
||||
{
|
||||
if (value != _rippleShowDragTrail)
|
||||
{
|
||||
_rippleShowDragTrail = value;
|
||||
MouseHighlighterSettingsConfig.Properties.RippleShowDragTrail.Value = value;
|
||||
NotifyMouseHighlighterPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool RippleShowReleasePulse
|
||||
{
|
||||
get => _rippleShowReleasePulse;
|
||||
set
|
||||
{
|
||||
if (value != _rippleShowReleasePulse)
|
||||
{
|
||||
_rippleShowReleasePulse = value;
|
||||
MouseHighlighterSettingsConfig.Properties.RippleShowReleasePulse.Value = value;
|
||||
NotifyMouseHighlighterPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void NotifyMouseHighlighterPropertyChanged([CallerMemberName] string propertyName = null)
|
||||
{
|
||||
OnPropertyChanged(propertyName);
|
||||
@@ -1214,6 +1348,12 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
private string _highlighterRightButtonClickColor;
|
||||
private string _highlighterAlwaysColor;
|
||||
private bool _isSpotlightModeEnabled;
|
||||
private bool _isRippleModeEnabled;
|
||||
private int _rippleSize;
|
||||
private double _rippleIntensity;
|
||||
private int _rippleDurationMs;
|
||||
private bool _rippleShowDragTrail;
|
||||
private bool _rippleShowReleasePulse;
|
||||
private int _highlighterRadius;
|
||||
private int _highlightFadeDelayMs;
|
||||
private int _highlightFadeDurationMs;
|
||||
|
||||
@@ -380,6 +380,26 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
|
||||
public List<int> MonitorRefreshDelayOptions => _monitorRefreshDelayOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the per-mouse-wheel-notch step shared by all PowerDisplay flyout sliders.
|
||||
/// </summary>
|
||||
public int MouseWheelIncrement
|
||||
{
|
||||
get => _settings.Properties.MouseWheelIncrement;
|
||||
set
|
||||
{
|
||||
if (SetSettingsProperty(_settings.Properties.MouseWheelIncrement, value, v => _settings.Properties.MouseWheelIncrement = v))
|
||||
{
|
||||
// Push to the (possibly open) flyout so the new step takes effect immediately.
|
||||
SignalSettingsUpdated();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private readonly List<int> _mouseWheelIncrementOptions = new List<int> { 1, 2, 5, 10, 15, 20, 25 };
|
||||
|
||||
public List<int> MouseWheelIncrementOptions => _mouseWheelIncrementOptions;
|
||||
|
||||
public ObservableCollection<MonitorInfo> Monitors
|
||||
{
|
||||
get => _monitors;
|
||||
|
||||