Compare commits
19 Commits
powerscrip
...
niels9001/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
49f43fdff8 | ||
|
|
5b7c461d6e | ||
|
|
536e768cac | ||
|
|
70ff4013b9 | ||
|
|
7a04d4c270 | ||
|
|
8c434cd6f4 | ||
|
|
d983dbc285 | ||
|
|
fb6843b0f1 | ||
|
|
6dd1ce5dd1 | ||
|
|
9ea30ec523 | ||
|
|
c777fcc1e4 | ||
|
|
28e078897a | ||
|
|
64f1243bdf | ||
|
|
e1074bc835 | ||
|
|
2390aacbfc | ||
|
|
4677ef50e9 | ||
|
|
e87527d6a4 | ||
|
|
a3c251b064 | ||
|
|
e3e9d132fc |
3
.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
|
||||
@@ -2178,6 +2180,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)
|
||||
|
||||
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 ?? '';
|
||||
|
||||
|
||||
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
|
||||
|
||||
@@ -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 |
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
@@ -5,19 +5,34 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<OutputPath>$(RepoRoot)$(Platform)\$(Configuration)</OutputPath>
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>Awake</RootNamespace>
|
||||
<AssemblyName>PowerToys.Awake</AssemblyName>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<ApplicationIcon>Assets\Awake\Awake.ico</ApplicationIcon>
|
||||
<Platforms>x64;ARM64</Platforms>
|
||||
<UseWinUI>true</UseWinUI>
|
||||
<EnableMsixTooling>true</EnableMsixTooling>
|
||||
<EnablePreviewMsixTooling>true</EnablePreviewMsixTooling>
|
||||
<WindowsPackageType>None</WindowsPackageType>
|
||||
<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
<OutputPath>$(RepoRoot)$(Platform)\$(Configuration)\WinUI3Apps</OutputPath>
|
||||
<!-- MRT from windows app sdk will search for a pri file with the same name of the module before defaulting to resources.pri -->
|
||||
<ProjectPriFileName>PowerToys.Awake.pri</ProjectPriFileName>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<Nullable>enable</Nullable>
|
||||
<UseWindowsForms>False</UseWindowsForms>
|
||||
<!--Per documentation: https://learn.microsoft.com/dotnet/core/compatibility/windows-forms/5.0/automatically-infer-winexe-output-type#outputtype-set-to-winexe-for-wpf-and-winforms-apps -->
|
||||
<DisableWinExeOutputInference>true</DisableWinExeOutputInference>
|
||||
<AssemblyName>PowerToys.Awake</AssemblyName>
|
||||
|
||||
<ApplicationIcon>Assets\Awake\Awake.ico</ApplicationIcon>
|
||||
<!-- Background tray app: workstation, non-concurrent GC keeps a smaller heap reserve. -->
|
||||
<ServerGarbageCollection>false</ServerGarbageCollection>
|
||||
<ConcurrentGarbageCollection>false</ConcurrentGarbageCollection>
|
||||
<PackageProjectUrl>https://awake.den.dev</PackageProjectUrl>
|
||||
<RepositoryUrl>https://github.com/microsoft/powertoys</RepositoryUrl>
|
||||
<GenerateAssemblyInfo>true</GenerateAssemblyInfo>
|
||||
<!-- Awake provides its own Program.Main; suppress the XAML-generated entry point. -->
|
||||
<DefineConstants>$(DefineConstants);DISABLE_XAML_GENERATED_MAIN</DefineConstants>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- See https://learn.microsoft.com/windows/apps/develop/platform/csharp-winrt/net-projection-from-cppwinrt-component for more info -->
|
||||
@@ -25,9 +40,27 @@
|
||||
<CsWinRTIncludes>PowerToys.GPOWrapper</CsWinRTIncludes>
|
||||
<CsWinRTGeneratedFilesDir>$(OutDir)</CsWinRTGeneratedFilesDir>
|
||||
<ErrorOnDuplicatePublishOutputFiles>false</ErrorOnDuplicatePublishOutputFiles>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Add WindowsDesktop.App framework reference to align Microsoft.VisualBasic.dll version
|
||||
with other projects that use UseWPF/UseWindowsForms. This does NOT enable WPF/WinForms,
|
||||
it only ensures consistent runtime DLL versions across all WinUI3Apps. -->
|
||||
<FrameworkReference Include="Microsoft.WindowsDesktop.App" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Page Remove="AwakeXAML\App.xaml" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ApplicationDefinition Include="AwakeXAML\App.xaml" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="AwakeXAML\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="Assets\Awake\Awake.ico" />
|
||||
<None Remove="Assets\Awake\disabled.ico" />
|
||||
@@ -39,15 +72,39 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK" />
|
||||
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" />
|
||||
<PackageReference Include="WinUIEx" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Controls.SettingsControls" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Controls.Segmented" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Controls.Primitives" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Extensions" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Converters" />
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" />
|
||||
<PackageReference Include="Microsoft.Windows.CsWin32">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="System.CommandLine" />
|
||||
<PackageReference Include="System.Reactive" />
|
||||
<PackageReference Include="System.Runtime.Caching" />
|
||||
<Manifest Include="$(ApplicationManifest)" />
|
||||
</ItemGroup>
|
||||
|
||||
<!--
|
||||
Defining the "Msix" ProjectCapability here allows the Single-project MSIX Packaging
|
||||
Tools extension to be activated for this project even if the Windows App SDK Nuget
|
||||
package has not yet been restored.
|
||||
-->
|
||||
<ItemGroup Condition="'$(DisableMsixProjectCapabilityAddedByProject)'!='true' and '$(EnableMsixTooling)'=='true'">
|
||||
<ProjectCapability Include="Msix" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\common\GPOWrapper\GPOWrapper.vcxproj" />
|
||||
<ProjectReference Include="..\..\..\common\interop\PowerToys.Interop.vcxproj" />
|
||||
<ProjectReference Include="..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
|
||||
<ProjectReference Include="..\..\..\common\Common.UI\Common.UI.csproj" />
|
||||
<ProjectReference Include="..\..\..\common\Common.UI.Controls\Common.UI.Controls.csproj" />
|
||||
<ProjectReference Include="..\..\..\settings-ui\Settings.UI.Library\Settings.UI.Library.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -93,6 +150,15 @@
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
|
||||
<!--
|
||||
Defining the "HasPackageAndPublishMenuAddedByProject" property here allows the Solution
|
||||
Explorer "Package and Publish" context menu entry to be enabled for this project even if
|
||||
the Windows App SDK Nuget package has not yet been restored.
|
||||
-->
|
||||
<PropertyGroup Condition="'$(DisableHasPackageAndPublishMenuAddedByProject)'!='true' and '$(EnableMsixTooling)'=='true'">
|
||||
<HasPackageAndPublishMenu>true</HasPackageAndPublishMenu>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Update="Microsoft.CodeAnalysis.NetAnalyzers">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
|
||||
13
src/modules/awake/Awake/AwakeXAML/App.xaml
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<Application
|
||||
x:Class="Awake.AwakeApp"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
90
src/modules/awake/Awake/AwakeXAML/App.xaml.cs
Normal file
@@ -0,0 +1,90 @@
|
||||
// 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 Awake.Core;
|
||||
using ManagedCommon;
|
||||
using Microsoft.UI.Xaml;
|
||||
|
||||
namespace Awake
|
||||
{
|
||||
/// <summary>
|
||||
/// WinUI application host for Awake. Owns the (initially hidden) flyout window
|
||||
/// and the tray icon service that toggles its visibility.
|
||||
/// </summary>
|
||||
public partial class AwakeApp : Application, IDisposable
|
||||
{
|
||||
private readonly bool _startedFromPowerToys;
|
||||
|
||||
private MainWindow? _mainWindow;
|
||||
private TrayIconService? _trayIconService;
|
||||
private bool _disposed;
|
||||
|
||||
public static new AwakeApp? Current { get; private set; }
|
||||
|
||||
public MainWindow? MainWindow => _mainWindow;
|
||||
|
||||
public AwakeApp(bool startedFromPowerToys)
|
||||
{
|
||||
_startedFromPowerToys = startedFromPowerToys;
|
||||
Current = this;
|
||||
|
||||
this.InitializeComponent();
|
||||
this.UnhandledException += OnUnhandledException;
|
||||
}
|
||||
|
||||
protected override void OnLaunched(LaunchActivatedEventArgs args)
|
||||
{
|
||||
try
|
||||
{
|
||||
Logger.LogInfo("AwakeApp.OnLaunched: creating MainWindow");
|
||||
_mainWindow = new MainWindow(_startedFromPowerToys);
|
||||
|
||||
Logger.LogInfo("AwakeApp.OnLaunched: creating TrayIconService");
|
||||
_trayIconService = new TrayIconService(toggleWindow: () => _mainWindow?.ToggleWindow());
|
||||
|
||||
_trayIconService.SetupTrayIcon(Constants.FullAppName, TrayIconService.DefaultIcon);
|
||||
|
||||
// Apply the current Awake mode (this also updates the tray icon to match).
|
||||
Manager.SetModeShellIcon(forceAdd: true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"AwakeApp.OnLaunched failed: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateTrayIcon(System.Drawing.Icon icon, string tooltip)
|
||||
{
|
||||
_trayIconService?.UpdateIcon(icon, tooltip);
|
||||
}
|
||||
|
||||
public void Shutdown()
|
||||
{
|
||||
Logger.LogInfo("AwakeApp.Shutdown");
|
||||
_trayIconService?.Destroy();
|
||||
_trayIconService = null;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
_mainWindow?.Dispose();
|
||||
_mainWindow = null;
|
||||
_trayIconService?.Destroy();
|
||||
_trayIconService = null;
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private static void OnUnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)
|
||||
{
|
||||
Logger.LogError($"AwakeApp unhandled exception: {e.Exception}");
|
||||
}
|
||||
}
|
||||
}
|
||||
99
src/modules/awake/Awake/AwakeXAML/AwakeAppPickerPage.xaml
Normal file
@@ -0,0 +1,99 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<Page
|
||||
x:Class="Awake.AwakeAppPickerPage"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:models="using:Awake.Core.Models"
|
||||
KeyDown="OnKeyDown">
|
||||
|
||||
<Grid Background="{ThemeResource LayerOnAcrylicFillColorDefaultBrush}">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Header with back button -->
|
||||
<Grid Padding="4,4,16,8" ColumnSpacing="4">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Button
|
||||
x:Name="BackButton"
|
||||
x:Uid="AwakeBackButton"
|
||||
Width="40"
|
||||
Height="40"
|
||||
VerticalAlignment="Center"
|
||||
Click="OnBackClick"
|
||||
Style="{StaticResource SubtleButtonStyle}">
|
||||
<FontIcon FontSize="14" Glyph="" />
|
||||
</Button>
|
||||
<TextBlock
|
||||
x:Uid="AppPickerTitle"
|
||||
Grid.Column="1"
|
||||
Margin="0,-1,0,0"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
|
||||
<AutoSuggestBox
|
||||
x:Name="AppSearchBox"
|
||||
x:Uid="AppSearchBox"
|
||||
Grid.Row="1"
|
||||
Margin="16,0,16,8"
|
||||
QueryIcon="Find"
|
||||
TextChanged="OnAppSearchTextChanged" />
|
||||
|
||||
<Grid Grid.Row="2">
|
||||
<ListView
|
||||
x:Name="AppListView"
|
||||
Padding="8,0,8,12"
|
||||
IsItemClickEnabled="True"
|
||||
ItemClick="OnAppSelected"
|
||||
SelectionMode="None">
|
||||
<ListView.ItemTemplate>
|
||||
<DataTemplate x:DataType="models:RunningAppInfo">
|
||||
<Grid Padding="0,4" ColumnSpacing="12">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Image
|
||||
Grid.Column="0"
|
||||
Width="16"
|
||||
Height="16"
|
||||
VerticalAlignment="Center"
|
||||
Source="{x:Bind Icon}" />
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Center">
|
||||
<TextBlock Text="{x:Bind DisplayName}" TextTrimming="CharacterEllipsis" />
|
||||
<!--<TextBlock
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Style="{ThemeResource CaptionTextBlockStyle}"
|
||||
Text="{x:Bind WindowTitle}"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
TextWrapping="NoWrap" />-->
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListView.ItemTemplate>
|
||||
</ListView>
|
||||
|
||||
<ProgressRing
|
||||
x:Name="AppLoadingRing"
|
||||
Width="32"
|
||||
Height="32"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
IsActive="False"
|
||||
Visibility="Collapsed" />
|
||||
|
||||
<TextBlock
|
||||
x:Name="AppEmptyText"
|
||||
x:Uid="AppEmptyText"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Visibility="Collapsed" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Page>
|
||||
157
src/modules/awake/Awake/AwakeXAML/AwakeAppPickerPage.xaml.cs
Normal file
@@ -0,0 +1,157 @@
|
||||
// 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.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Awake.Core;
|
||||
using Awake.Core.Models;
|
||||
using Awake.ViewModels;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
using Microsoft.UI.Xaml.Navigation;
|
||||
using Windows.System;
|
||||
|
||||
namespace Awake
|
||||
{
|
||||
/// <summary>
|
||||
/// Lets the user pick a running app to keep the system awake while it runs. Selecting an app
|
||||
/// records it as the pending selection and returns to the launch page; it starts when the user
|
||||
/// presses Start (or live-rebinds if a session is already running).
|
||||
/// </summary>
|
||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicMethods)]
|
||||
public sealed partial class AwakeAppPickerPage : Page
|
||||
{
|
||||
private AwakeFlyoutNavigationContext? _context;
|
||||
private List<RunningAppInfo> _windowedApps = new();
|
||||
private List<RunningAppInfo> _allProcesses = new();
|
||||
private bool _allProcessesLoaded;
|
||||
private const int MaxResults = 100;
|
||||
|
||||
public AwakeAppPickerPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public AwakeFlyoutViewModel ViewModel { get; private set; } = default!;
|
||||
|
||||
protected override void OnNavigatedTo(NavigationEventArgs e)
|
||||
{
|
||||
base.OnNavigatedTo(e);
|
||||
|
||||
if (e.Parameter is AwakeFlyoutNavigationContext context)
|
||||
{
|
||||
_context = context;
|
||||
ViewModel = context.ViewModel;
|
||||
}
|
||||
|
||||
_ = LoadRunningAppsAsync();
|
||||
}
|
||||
|
||||
private async Task LoadRunningAppsAsync()
|
||||
{
|
||||
AppSearchBox.Text = string.Empty;
|
||||
AppListView.ItemsSource = null;
|
||||
AppEmptyText.Visibility = Visibility.Collapsed;
|
||||
AppLoadingRing.IsActive = true;
|
||||
AppLoadingRing.Visibility = Visibility.Visible;
|
||||
|
||||
_windowedApps = await RunningAppsProvider.GetRunningAppsAsync();
|
||||
|
||||
AppLoadingRing.IsActive = false;
|
||||
AppLoadingRing.Visibility = Visibility.Collapsed;
|
||||
|
||||
await ApplyAppFilterAsync(string.Empty);
|
||||
|
||||
// Load the full process list in the background so the first search is responsive.
|
||||
_ = LoadAllProcessesAsync();
|
||||
}
|
||||
|
||||
private async Task LoadAllProcessesAsync()
|
||||
{
|
||||
if (_allProcessesLoaded)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_allProcesses = await RunningAppsProvider.GetAllProcessesAsync();
|
||||
_allProcessesLoaded = true;
|
||||
}
|
||||
|
||||
private async Task ApplyAppFilterAsync(string query)
|
||||
{
|
||||
// Empty query shows the curated windowed-app list; typing searches every process.
|
||||
bool searching = !string.IsNullOrWhiteSpace(query);
|
||||
List<RunningAppInfo> source = searching && _allProcessesLoaded ? _allProcesses : _windowedApps;
|
||||
|
||||
IEnumerable<RunningAppInfo> filtered = source;
|
||||
if (searching)
|
||||
{
|
||||
filtered = source.Where(a =>
|
||||
a.DisplayName.Contains(query, StringComparison.CurrentCultureIgnoreCase)
|
||||
|| a.WindowTitle.Contains(query, StringComparison.CurrentCultureIgnoreCase));
|
||||
}
|
||||
|
||||
List<RunningAppInfo> list = filtered.Take(MaxResults).ToList();
|
||||
|
||||
// Build icons lazily for only the items about to be shown (the full process list can be
|
||||
// large, so we avoid materializing hundreds of bitmaps up front).
|
||||
foreach (RunningAppInfo app in list)
|
||||
{
|
||||
if (app.Icon is null && app.IconBytes is not null)
|
||||
{
|
||||
app.Icon = await RunningAppsProvider.BuildIconAsync(app.IconBytes);
|
||||
}
|
||||
}
|
||||
|
||||
AppListView.ItemsSource = list;
|
||||
AppEmptyText.Visibility = list.Count == 0 ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
|
||||
private async void OnAppSearchTextChanged(AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs args)
|
||||
{
|
||||
if (args.Reason == AutoSuggestionBoxTextChangeReason.UserInput)
|
||||
{
|
||||
// If the user starts typing before the full list finished loading, fetch it now.
|
||||
if (!string.IsNullOrWhiteSpace(sender.Text) && !_allProcessesLoaded)
|
||||
{
|
||||
await LoadAllProcessesAsync();
|
||||
}
|
||||
|
||||
await ApplyAppFilterAsync(sender.Text);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAppSelected(object sender, ItemClickEventArgs e)
|
||||
{
|
||||
if (e.ClickedItem is RunningAppInfo app)
|
||||
{
|
||||
ViewModel.SetPendingApp(app.ProcessId, app.DisplayName, app.Icon);
|
||||
GoBack();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnBackClick(object sender, RoutedEventArgs e) => GoBack();
|
||||
|
||||
private void OnKeyDown(object sender, KeyRoutedEventArgs e)
|
||||
{
|
||||
if (e.Key == VirtualKey.Escape)
|
||||
{
|
||||
GoBack();
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void GoBack()
|
||||
{
|
||||
if (Frame != null && Frame.CanGoBack)
|
||||
{
|
||||
Frame.GoBack();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
114
src/modules/awake/Awake/AwakeXAML/AwakeCustomTimePage.xaml
Normal file
@@ -0,0 +1,114 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<Page
|
||||
x:Class="Awake.AwakeCustomTimePage"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls"
|
||||
KeyDown="OnKeyDown">
|
||||
|
||||
<Page.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary Source="ms-appx:///CommunityToolkit.WinUI.Controls.Segmented/Segmented/Segmented.xaml" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
</Page.Resources>
|
||||
|
||||
<Grid Background="{ThemeResource LayerOnAcrylicFillColorDefaultBrush}">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Header with back button -->
|
||||
<Grid Padding="4,4,16,8" ColumnSpacing="4">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Button
|
||||
x:Name="BackButton"
|
||||
x:Uid="AwakeBackButton"
|
||||
Width="40"
|
||||
Height="40"
|
||||
VerticalAlignment="Center"
|
||||
Click="OnBackClick"
|
||||
Style="{StaticResource SubtleButtonStyle}">
|
||||
<FontIcon FontSize="14" Glyph="" />
|
||||
</Button>
|
||||
<TextBlock
|
||||
x:Uid="CustomTimeTitle"
|
||||
Grid.Column="1"
|
||||
Margin="0,-1,0,0"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
|
||||
<StackPanel
|
||||
Grid.Row="1"
|
||||
Padding="16,8,16,16"
|
||||
Spacing="12">
|
||||
<tkcontrols:Segmented
|
||||
x:Name="CustomTypeSelector"
|
||||
HorizontalAlignment="Stretch"
|
||||
SelectedIndex="0">
|
||||
<tkcontrols:SegmentedItem x:Name="CustomDurationSegment" x:Uid="CustomDurationRadio" />
|
||||
<tkcontrols:SegmentedItem x:Name="CustomUntilSegment" x:Uid="CustomUntilRadio" />
|
||||
</tkcontrols:Segmented>
|
||||
|
||||
<tkcontrols:SwitchPresenter TargetType="x:Int32" Value="{x:Bind CustomTypeSelector.SelectedIndex, Mode=OneWay}">
|
||||
<!-- Case 0 = duration (hours/minutes) -->
|
||||
<tkcontrols:Case Value="0">
|
||||
<Grid ColumnSpacing="8">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<NumberBox
|
||||
x:Name="IntervalHoursInput"
|
||||
x:Uid="IntervalHoursInput"
|
||||
Grid.Column="0"
|
||||
LargeChange="5"
|
||||
Maximum="23"
|
||||
Minimum="0"
|
||||
SmallChange="1"
|
||||
SpinButtonPlacementMode="Compact"
|
||||
Value="{x:Bind ViewModel.IntervalHours, Mode=TwoWay}" />
|
||||
<NumberBox
|
||||
x:Name="IntervalMinutesInput"
|
||||
x:Uid="IntervalMinutesInput"
|
||||
Grid.Column="1"
|
||||
LargeChange="5"
|
||||
Maximum="59"
|
||||
Minimum="0"
|
||||
SmallChange="1"
|
||||
SpinButtonPlacementMode="Compact"
|
||||
Value="{x:Bind ViewModel.IntervalMinutes, Mode=TwoWay}" />
|
||||
</Grid>
|
||||
</tkcontrols:Case>
|
||||
<!-- Case 1 = until a specific date/time -->
|
||||
<tkcontrols:Case Value="1">
|
||||
<StackPanel Spacing="8">
|
||||
<DatePicker
|
||||
x:Name="ExpirationDatePicker"
|
||||
x:Uid="ExpirationDatePicker"
|
||||
HorizontalAlignment="Stretch"
|
||||
Date="{x:Bind ViewModel.ExpirationDate, Mode=TwoWay}" />
|
||||
<TimePicker
|
||||
x:Name="ExpirationTimePicker"
|
||||
x:Uid="ExpirationTimePicker"
|
||||
HorizontalAlignment="Stretch"
|
||||
ClockIdentifier="24HourClock"
|
||||
MinuteIncrement="5"
|
||||
Time="{x:Bind ViewModel.ExpirationTime, Mode=TwoWay}" />
|
||||
</StackPanel>
|
||||
</tkcontrols:Case>
|
||||
</tkcontrols:SwitchPresenter>
|
||||
|
||||
<Button
|
||||
x:Name="CustomApplyButton"
|
||||
x:Uid="CustomApplyButton"
|
||||
HorizontalAlignment="Stretch"
|
||||
Click="OnApplyClick" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Page>
|
||||
@@ -0,0 +1,73 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Awake.ViewModels;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
using Microsoft.UI.Xaml.Navigation;
|
||||
using Windows.System;
|
||||
|
||||
namespace Awake
|
||||
{
|
||||
/// <summary>
|
||||
/// Lets the user configure a custom keep-awake duration (hours/minutes) or an "until a
|
||||
/// specific date and time" expiration, then applies it and returns to the launch page.
|
||||
/// </summary>
|
||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicMethods)]
|
||||
public sealed partial class AwakeCustomTimePage : Page
|
||||
{
|
||||
private AwakeFlyoutNavigationContext? _context;
|
||||
|
||||
public AwakeCustomTimePage()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public AwakeFlyoutViewModel ViewModel { get; private set; } = default!;
|
||||
|
||||
protected override void OnNavigatedTo(NavigationEventArgs e)
|
||||
{
|
||||
base.OnNavigatedTo(e);
|
||||
|
||||
if (e.Parameter is AwakeFlyoutNavigationContext context)
|
||||
{
|
||||
_context = context;
|
||||
ViewModel = context.ViewModel;
|
||||
this.Bindings.Update();
|
||||
|
||||
// Reflect the current pending selection so reopening keeps the chosen sub-mode.
|
||||
// The SwitchPresenter swaps the duration/until panels off this selection in XAML.
|
||||
CustomTypeSelector.SelectedIndex = ViewModel.PendingCustomIsUntil ? 1 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnApplyClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
ViewModel.SetPendingCustom(CustomTypeSelector.SelectedIndex == 1);
|
||||
|
||||
GoBack();
|
||||
}
|
||||
|
||||
private void OnBackClick(object sender, RoutedEventArgs e) => GoBack();
|
||||
|
||||
private void OnKeyDown(object sender, KeyRoutedEventArgs e)
|
||||
{
|
||||
if (e.Key == VirtualKey.Escape)
|
||||
{
|
||||
GoBack();
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void GoBack()
|
||||
{
|
||||
if (Frame != null && Frame.CanGoBack)
|
||||
{
|
||||
Frame.GoBack();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
using Awake.ViewModels;
|
||||
|
||||
namespace Awake
|
||||
{
|
||||
/// <summary>
|
||||
/// Carries the shared <see cref="AwakeFlyoutViewModel"/> (and a request-to-close callback)
|
||||
/// between the flyout pages hosted in <see cref="AwakeShellPage"/>'s navigation frame.
|
||||
/// </summary>
|
||||
internal sealed class AwakeFlyoutNavigationContext
|
||||
{
|
||||
public AwakeFlyoutNavigationContext(AwakeFlyoutViewModel viewModel, Action requestClose)
|
||||
{
|
||||
ViewModel = viewModel;
|
||||
RequestClose = requestClose;
|
||||
}
|
||||
|
||||
public AwakeFlyoutViewModel ViewModel { get; }
|
||||
|
||||
public Action RequestClose { get; }
|
||||
}
|
||||
}
|
||||
537
src/modules/awake/Awake/AwakeXAML/AwakeLaunchPage.xaml
Normal file
@@ -0,0 +1,537 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<Page
|
||||
x:Class="Awake.AwakeLaunchPage"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:animatedVisuals="using:Microsoft.UI.Xaml.Controls.AnimatedVisuals"
|
||||
xmlns:animations="using:CommunityToolkit.WinUI.Animations"
|
||||
KeyDown="OnKeyDown">
|
||||
|
||||
<Page.Resources>
|
||||
<Style
|
||||
x:Key="FlyoutIconButtonStyle"
|
||||
BasedOn="{StaticResource SubtleButtonStyle}"
|
||||
TargetType="Button">
|
||||
<Setter Property="Padding" Value="6" />
|
||||
<Setter Property="Width" Value="32" />
|
||||
<Setter Property="Height" Value="32" />
|
||||
<Setter Property="FontFamily" Value="{ThemeResource SymbolThemeFontFamily}" />
|
||||
</Style>
|
||||
|
||||
<Style x:Key="DurationCardStyle" TargetType="ToggleButton">
|
||||
<Setter Property="MinWidth" Value="0" />
|
||||
<Setter Property="MinHeight" Value="48" />
|
||||
<Setter Property="Padding" Value="4,8" />
|
||||
<Setter Property="FontSize" Value="12" />
|
||||
<Setter Property="HorizontalAlignment" Value="Stretch" />
|
||||
<Setter Property="VerticalAlignment" Value="Stretch" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Center" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
<Setter Property="Background" Value="{ThemeResource CardBackgroundFillColorDefaultBrush}" />
|
||||
<Setter Property="Foreground" Value="{ThemeResource TextFillColorPrimaryBrush}" />
|
||||
<Setter Property="BorderBrush" Value="{ThemeResource ControlElevationBorderBrush}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="FontSize" Value="12" />
|
||||
<Setter Property="CornerRadius" Value="{StaticResource ControlCornerRadius}" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ToggleButton">
|
||||
<Grid
|
||||
x:Name="RootGrid"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="{TemplateBinding CornerRadius}">
|
||||
<ContentPresenter
|
||||
x:Name="ContentPresenter"
|
||||
Padding="{TemplateBinding Padding}"
|
||||
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
Content="{TemplateBinding Content}"
|
||||
ContentTransitions="{TemplateBinding ContentTransitions}"
|
||||
Foreground="{TemplateBinding Foreground}" />
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Normal" />
|
||||
<VisualState x:Name="PointerOver">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="RootGrid.Background" Value="{ThemeResource CardBackgroundFillColorSecondaryBrush}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="Pressed">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="RootGrid.Background" Value="{ThemeResource CardBackgroundFillColorSecondaryBrush}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="Disabled">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="RootGrid.Background" Value="{ThemeResource ControlFillColorDisabledBrush}" />
|
||||
<Setter Target="ContentPresenter.Foreground" Value="{ThemeResource TextFillColorDisabledBrush}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="Checked">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="RootGrid.BorderBrush" Value="{ThemeResource AccentFillColorDefaultBrush}" />
|
||||
<Setter Target="RootGrid.BorderThickness" Value="2" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="CheckedPointerOver">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="RootGrid.Background" Value="{ThemeResource CardBackgroundFillColorSecondaryBrush}" />
|
||||
<Setter Target="RootGrid.BorderBrush" Value="{ThemeResource AccentFillColorDefaultBrush}" />
|
||||
<Setter Target="RootGrid.BorderThickness" Value="2" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="CheckedPressed">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="RootGrid.Background" Value="{ThemeResource CardBackgroundFillColorSecondaryBrush}" />
|
||||
<Setter Target="RootGrid.BorderBrush" Value="{ThemeResource AccentFillColorDefaultBrush}" />
|
||||
<Setter Target="RootGrid.BorderThickness" Value="2" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
</Grid>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style
|
||||
x:Key="CustomCardStyle"
|
||||
BasedOn="{StaticResource DefaultButtonStyle}"
|
||||
TargetType="Button">
|
||||
<Setter Property="MinWidth" Value="0" />
|
||||
<Setter Property="MinHeight" Value="48" />
|
||||
<Setter Property="Padding" Value="4,8" />
|
||||
<Setter Property="HorizontalAlignment" Value="Stretch" />
|
||||
<Setter Property="VerticalAlignment" Value="Stretch" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Center" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
<Setter Property="CornerRadius" Value="6" />
|
||||
</Style>
|
||||
</Page.Resources>
|
||||
|
||||
<Grid
|
||||
x:Name="RootGrid"
|
||||
IsTabStop="True"
|
||||
TabFocusNavigation="Local">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="120" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="48" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Idle header: neutral status ring + title/subtitle -->
|
||||
<Grid
|
||||
x:Name="IdleHeader"
|
||||
Padding="16"
|
||||
ColumnSpacing="16"
|
||||
RowSpacing="8">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" MinHeight="16" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<animations:Implicit.ShowAnimations>
|
||||
<animations:OpacityAnimation
|
||||
From="0.0"
|
||||
To="1.0"
|
||||
Duration="0:0:0.4" />
|
||||
</animations:Implicit.ShowAnimations>
|
||||
<animations:Implicit.HideAnimations>
|
||||
<animations:OpacityAnimation
|
||||
From="1.0"
|
||||
To="0.0"
|
||||
Duration="0:0:0.25" />
|
||||
</animations:Implicit.HideAnimations>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<TextBlock
|
||||
x:Uid="HeaderTitle"
|
||||
Grid.Row="1"
|
||||
FontSize="16"
|
||||
FontWeight="SemiBold"
|
||||
TextWrapping="Wrap" />
|
||||
|
||||
<TextBlock
|
||||
x:Uid="HeaderSubtitle"
|
||||
Grid.Row="2"
|
||||
FontSize="12"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
TextWrapping="Wrap" />
|
||||
|
||||
<Button
|
||||
x:Name="StartButton"
|
||||
Grid.RowSpan="3"
|
||||
Grid.Column="1"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
Click="OnStartButtonClick">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<FontIcon FontSize="14" Glyph="" />
|
||||
<TextBlock x:Uid="ActionStartText" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<!-- Active header: translucent accent base + animated accent glow + countdown -->
|
||||
<Border
|
||||
x:Name="ActiveHeader"
|
||||
Grid.Row="0"
|
||||
Visibility="Collapsed">
|
||||
<Border.Background>
|
||||
<LinearGradientBrush Opacity="0.6" StartPoint="0.5,0" EndPoint="0.5,1">
|
||||
<GradientStop Offset="0.0" Color="{ThemeResource SystemAccentColorLight3}" />
|
||||
<GradientStop Offset="1.0" Color="Transparent" />
|
||||
</LinearGradientBrush>
|
||||
</Border.Background>
|
||||
<animations:Implicit.ShowAnimations>
|
||||
<animations:OpacityAnimation
|
||||
From="0.0"
|
||||
To="1.0"
|
||||
Duration="0:0:0.4" />
|
||||
</animations:Implicit.ShowAnimations>
|
||||
<animations:Implicit.HideAnimations>
|
||||
<animations:OpacityAnimation
|
||||
From="1.0"
|
||||
To="0.0"
|
||||
Duration="0:0:0.25" />
|
||||
</animations:Implicit.HideAnimations>
|
||||
<Grid>
|
||||
<!-- Subtle, organic accent glow (Composition-animated) — fills the whole header -->
|
||||
<Grid
|
||||
x:Name="HeaderGlowHost"
|
||||
IsHitTestVisible="False" />
|
||||
|
||||
<Grid
|
||||
Padding="16"
|
||||
ColumnSpacing="16"
|
||||
RowSpacing="8">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" MinHeight="16" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Border
|
||||
Padding="8,2"
|
||||
HorizontalAlignment="Left"
|
||||
CornerRadius="4">
|
||||
<Border.Background>
|
||||
<SolidColorBrush Opacity="0.22" Color="{ThemeResource SystemAccentColor}" />
|
||||
</Border.Background>
|
||||
<TextBlock
|
||||
x:Uid="ActiveBadgeText"
|
||||
FontSize="10"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}" />
|
||||
</Border>
|
||||
<StackPanel
|
||||
Grid.Row="1"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Bottom"
|
||||
Orientation="Horizontal"
|
||||
Spacing="8">
|
||||
<Image
|
||||
Width="24"
|
||||
Height="24"
|
||||
VerticalAlignment="Center"
|
||||
Source="{x:Bind ViewModel.WhileAppCardIcon, Mode=OneWay}"
|
||||
Visibility="{x:Bind ViewModel.ActiveAppIconVisibility, Mode=OneWay}" />
|
||||
<TextBlock
|
||||
VerticalAlignment="Bottom"
|
||||
FontSize="20"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}"
|
||||
Text="{x:Bind ViewModel.CountdownTime, Mode=OneWay}"
|
||||
Visibility="{x:Bind ViewModel.ActiveCountdownVisibility, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
<TextBlock
|
||||
Grid.Row="2"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Bottom"
|
||||
FontSize="12"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Text="{x:Bind ViewModel.OffAtText, Mode=OneWay}" />
|
||||
<Button
|
||||
x:Name="ActionButton"
|
||||
Grid.RowSpan="3"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
Click="OnStopButtonClick"
|
||||
Style="{StaticResource AccentButtonStyle}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<FontIcon FontSize="14" Glyph="" />
|
||||
<TextBlock x:Uid="ActionStopText" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<Grid
|
||||
Grid.Row="1"
|
||||
Padding="16"
|
||||
Background="{ThemeResource LayerOnAcrylicFillColorDefaultBrush}"
|
||||
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
|
||||
BorderThickness="0,1,0,1"
|
||||
RowSpacing="12">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Grid ColumnSpacing="8">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<ToggleButton
|
||||
x:Name="Card30"
|
||||
Click="OnDurationCardClick"
|
||||
Style="{StaticResource DurationCardStyle}"
|
||||
Tag="30">
|
||||
<StackPanel HorizontalAlignment="Center" Spacing="0">
|
||||
<TextBlock
|
||||
HorizontalAlignment="Center"
|
||||
FontSize="14"
|
||||
FontWeight="SemiBold"
|
||||
Text="30" />
|
||||
<TextBlock
|
||||
x:Uid="Card30Unit"
|
||||
HorizontalAlignment="Center"
|
||||
FontSize="11"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
|
||||
</StackPanel>
|
||||
</ToggleButton>
|
||||
<ToggleButton
|
||||
x:Name="Card60"
|
||||
Grid.Column="1"
|
||||
Click="OnDurationCardClick"
|
||||
Style="{StaticResource DurationCardStyle}"
|
||||
Tag="60">
|
||||
<StackPanel HorizontalAlignment="Center" Spacing="0">
|
||||
<TextBlock
|
||||
HorizontalAlignment="Center"
|
||||
FontSize="14"
|
||||
FontWeight="SemiBold"
|
||||
Text="1" />
|
||||
<TextBlock
|
||||
x:Uid="Card60Unit"
|
||||
HorizontalAlignment="Center"
|
||||
FontSize="11"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
|
||||
</StackPanel>
|
||||
</ToggleButton>
|
||||
<ToggleButton
|
||||
x:Name="Card120"
|
||||
Grid.Column="2"
|
||||
Click="OnDurationCardClick"
|
||||
Style="{StaticResource DurationCardStyle}"
|
||||
Tag="120">
|
||||
<StackPanel HorizontalAlignment="Center" Spacing="0">
|
||||
<TextBlock
|
||||
HorizontalAlignment="Center"
|
||||
FontSize="14"
|
||||
FontWeight="SemiBold"
|
||||
Text="2" />
|
||||
<TextBlock
|
||||
x:Uid="Card120Unit"
|
||||
HorizontalAlignment="Center"
|
||||
FontSize="11"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
|
||||
</StackPanel>
|
||||
</ToggleButton>
|
||||
<ToggleButton
|
||||
x:Name="CardForever"
|
||||
Grid.Column="3"
|
||||
Click="OnForeverCardClick"
|
||||
FontSize="16"
|
||||
Style="{StaticResource DurationCardStyle}">
|
||||
<StackPanel HorizontalAlignment="Center" Spacing="0">
|
||||
<TextBlock
|
||||
HorizontalAlignment="Center"
|
||||
FontSize="18"
|
||||
FontWeight="SemiBold"
|
||||
Text="∞" />
|
||||
<TextBlock
|
||||
x:Uid="CardForeverText"
|
||||
HorizontalAlignment="Center"
|
||||
FontSize="11"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
|
||||
</StackPanel>
|
||||
</ToggleButton>
|
||||
</Grid>
|
||||
<!-- Custom: left toggle selects, right chevron navigates to the picker page -->
|
||||
<Grid Grid.Row="1">
|
||||
<ToggleButton
|
||||
x:Name="CardCustom"
|
||||
MinHeight="48"
|
||||
Padding="8,4,40,4"
|
||||
HorizontalAlignment="Stretch"
|
||||
Click="OnCustomToggleClick"
|
||||
Style="{StaticResource DurationCardStyle}">
|
||||
<Grid HorizontalAlignment="Stretch" ColumnSpacing="8">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<FontIcon
|
||||
Grid.Column="0"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="16"
|
||||
Glyph="" />
|
||||
<TextBlock
|
||||
x:Name="CardCustomText"
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
Text="{x:Bind ViewModel.CustomCardText, Mode=OneWay}"
|
||||
TextWrapping="Wrap" />
|
||||
</Grid>
|
||||
</ToggleButton>
|
||||
<Rectangle
|
||||
Width="1"
|
||||
Margin="0,8,36,8"
|
||||
HorizontalAlignment="Right"
|
||||
Fill="{ThemeResource DividerStrokeColorDefaultBrush}"
|
||||
IsHitTestVisible="False" />
|
||||
<Button
|
||||
x:Name="CardCustomNav"
|
||||
x:Uid="CardCustomNav"
|
||||
Width="36"
|
||||
Margin="2"
|
||||
Padding="0"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Stretch"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Click="OnCustomNavClick"
|
||||
CornerRadius="0,4,4,0"
|
||||
Style="{StaticResource SubtleButtonStyle}">
|
||||
<FontIcon FontSize="12" Glyph="" />
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<!-- While app runs: left toggle selects, right chevron navigates to the app picker -->
|
||||
<Grid Grid.Row="2">
|
||||
<ToggleButton
|
||||
x:Name="CardWhileApp"
|
||||
MinHeight="48"
|
||||
Padding="8,4,40,4"
|
||||
HorizontalAlignment="Stretch"
|
||||
Click="OnWhileAppToggleClick"
|
||||
Style="{StaticResource DurationCardStyle}">
|
||||
<Grid HorizontalAlignment="Stretch" ColumnSpacing="8">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid
|
||||
Grid.Column="0"
|
||||
Width="20"
|
||||
Height="20"
|
||||
VerticalAlignment="Center">
|
||||
<Image
|
||||
x:Name="CardWhileAppIcon"
|
||||
Width="20"
|
||||
Height="20"
|
||||
Source="{x:Bind ViewModel.WhileAppCardIcon, Mode=OneWay}"
|
||||
Visibility="Collapsed" />
|
||||
<FontIcon
|
||||
x:Name="CardWhileAppGlyph"
|
||||
FontSize="16"
|
||||
Glyph="" />
|
||||
</Grid>
|
||||
<TextBlock
|
||||
x:Name="CardWhileAppText"
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
Text="{x:Bind ViewModel.WhileAppCardText, Mode=OneWay}"
|
||||
TextWrapping="Wrap" />
|
||||
</Grid>
|
||||
</ToggleButton>
|
||||
<Rectangle
|
||||
Width="1"
|
||||
Margin="0,8,36,8"
|
||||
HorizontalAlignment="Right"
|
||||
Fill="{ThemeResource DividerStrokeColorDefaultBrush}"
|
||||
IsHitTestVisible="False" />
|
||||
<Button
|
||||
x:Name="CardWhileAppNav"
|
||||
x:Uid="CardWhileAppNav"
|
||||
Width="36"
|
||||
Margin="2"
|
||||
Padding="0"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Stretch"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Click="OnWhileAppNavClick"
|
||||
CornerRadius="0,4,4,0"
|
||||
Style="{StaticResource SubtleButtonStyle}">
|
||||
<FontIcon FontSize="12" Glyph="" />
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<!-- Keep screen on -->
|
||||
<CheckBox
|
||||
x:Name="KeepDisplayOnToggle"
|
||||
x:Uid="KeepDisplayOnToggle"
|
||||
Grid.Row="3"
|
||||
Margin="0,4,0,0"
|
||||
IsChecked="{x:Bind ViewModel.KeepDisplayOn, Mode=TwoWay}"
|
||||
IsEnabled="{x:Bind ViewModel.KeepDisplayOnEnabled, Mode=OneWay}" />
|
||||
|
||||
<!-- Keep awake with lid closed -->
|
||||
<CheckBox
|
||||
Grid.Row="4"
|
||||
VerticalAlignment="Top"
|
||||
Content="Keep awake when lid is closed" />
|
||||
</Grid>
|
||||
|
||||
<Grid Grid.Row="2">
|
||||
<Button
|
||||
x:Name="OpenSettingsButton"
|
||||
x:Uid="OpenSettingsButton"
|
||||
Margin="0,0,12,0"
|
||||
HorizontalAlignment="Right"
|
||||
Click="OnOpenSettingsClick"
|
||||
Style="{StaticResource FlyoutIconButtonStyle}">
|
||||
<AnimatedIcon x:Name="SettingsAnimatedIcon">
|
||||
<AnimatedIcon.Source>
|
||||
<animatedVisuals:AnimatedSettingsVisualSource />
|
||||
</AnimatedIcon.Source>
|
||||
<AnimatedIcon.FallbackIconSource>
|
||||
<SymbolIconSource Symbol="Setting" />
|
||||
</AnimatedIcon.FallbackIconSource>
|
||||
</AnimatedIcon>
|
||||
</Button>
|
||||
</Grid>
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup x:Name="ActivationStates">
|
||||
<VisualState x:Name="IdleState" />
|
||||
<VisualState x:Name="ActiveState">
|
||||
<VisualState.StateTriggers>
|
||||
<StateTrigger IsActive="{x:Bind ViewModel.IsActive, Mode=OneWay}" />
|
||||
</VisualState.StateTriggers>
|
||||
<VisualState.Setters>
|
||||
<Setter Target="IdleHeader.Visibility" Value="Collapsed" />
|
||||
<Setter Target="ActiveHeader.Visibility" Value="Visible" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
</Grid>
|
||||
</Page>
|
||||
557
src/modules/awake/Awake/AwakeXAML/AwakeLaunchPage.xaml.cs
Normal file
@@ -0,0 +1,557 @@
|
||||
// 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.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.Numerics;
|
||||
using Awake.ViewModels;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.UI;
|
||||
using Microsoft.UI.Composition;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Controls.Primitives;
|
||||
using Microsoft.UI.Xaml.Hosting;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
using Microsoft.UI.Xaml.Media.Animation;
|
||||
using Microsoft.UI.Xaml.Navigation;
|
||||
using Windows.System;
|
||||
using Windows.UI;
|
||||
|
||||
namespace Awake
|
||||
{
|
||||
/// <summary>
|
||||
/// The flyout's main page: the idle/active header (with the composition glow) and the
|
||||
/// duration selection. The "Custom" and "While app runs" cards navigate the shell frame to
|
||||
/// their own pages instead of opening flyouts.
|
||||
/// </summary>
|
||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicMethods)]
|
||||
public sealed partial class AwakeLaunchPage : Page
|
||||
{
|
||||
private ContainerVisual? _glowRoot;
|
||||
private AwakeFlyoutNavigationContext? _context;
|
||||
private bool _subscribed;
|
||||
|
||||
public AwakeLaunchPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
HeaderGlowHost.Loaded += OnHeaderGlowLoaded;
|
||||
HeaderGlowHost.SizeChanged += OnHeaderGlowSizeChanged;
|
||||
}
|
||||
|
||||
public AwakeFlyoutViewModel ViewModel { get; private set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Moves keyboard focus into the page so Escape and tab navigation work.
|
||||
/// </summary>
|
||||
public void FocusContent() => RootGrid.Focus(FocusState.Programmatic);
|
||||
|
||||
protected override void OnNavigatedTo(NavigationEventArgs e)
|
||||
{
|
||||
base.OnNavigatedTo(e);
|
||||
|
||||
if (e.Parameter is AwakeFlyoutNavigationContext context)
|
||||
{
|
||||
_context = context;
|
||||
ViewModel = context.ViewModel;
|
||||
this.Bindings.Update();
|
||||
|
||||
if (!_subscribed)
|
||||
{
|
||||
ViewModel.PropertyChanged += OnViewModelPropertyChanged;
|
||||
_subscribed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Only realign the pending selection with the running mode on a fresh open / forward
|
||||
// navigation. On Back navigation the user may have just chosen a custom duration or an
|
||||
// app on a sub-page; re-syncing here would clobber that pending selection.
|
||||
if (e.NavigationMode != NavigationMode.Back)
|
||||
{
|
||||
ViewModel?.SyncPendingFromMode();
|
||||
}
|
||||
|
||||
HighlightSelectedCard();
|
||||
RefreshWhileAppVisuals();
|
||||
}
|
||||
|
||||
protected override void OnNavigatedFrom(NavigationEventArgs e)
|
||||
{
|
||||
base.OnNavigatedFrom(e);
|
||||
|
||||
if (_subscribed && ViewModel is not null)
|
||||
{
|
||||
ViewModel.PropertyChanged -= OnViewModelPropertyChanged;
|
||||
_subscribed = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnViewModelPropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName == nameof(AwakeFlyoutViewModel.Mode))
|
||||
{
|
||||
ViewModel?.SyncPendingFromMode();
|
||||
HighlightSelectedCard();
|
||||
RefreshWhileAppVisuals();
|
||||
}
|
||||
else if (e.PropertyName == nameof(AwakeFlyoutViewModel.WhileAppCardIcon))
|
||||
{
|
||||
RefreshWhileAppVisuals();
|
||||
}
|
||||
}
|
||||
|
||||
// Highlights the card matching the current pending selection so the buttons and the header
|
||||
// state stay in lockstep.
|
||||
private void HighlightSelectedCard()
|
||||
{
|
||||
if (ViewModel is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ToggleButton? selected = ViewModel.PendingSelection switch
|
||||
{
|
||||
FlyoutSelectionKind.Forever => CardForever,
|
||||
FlyoutSelectionKind.Custom => CardCustom,
|
||||
FlyoutSelectionKind.WhileApp => CardWhileApp,
|
||||
_ => CardForMinutes(ViewModel.PendingMinutes),
|
||||
};
|
||||
|
||||
SetSelectedCard(selected);
|
||||
}
|
||||
|
||||
// Shows the captured app icon on the While-app card when one is available, otherwise falls
|
||||
// back to the generic glyph.
|
||||
private void RefreshWhileAppVisuals()
|
||||
{
|
||||
bool hasIcon = ViewModel?.WhileAppCardIcon is not null;
|
||||
CardWhileAppIcon.Visibility = hasIcon ? Visibility.Visible : Visibility.Collapsed;
|
||||
CardWhileAppGlyph.Visibility = hasIcon ? Visibility.Collapsed : Visibility.Visible;
|
||||
}
|
||||
|
||||
private ToggleButton? CardForMinutes(uint minutes) => minutes switch
|
||||
{
|
||||
30 => Card30,
|
||||
60 => Card60,
|
||||
120 => Card120,
|
||||
_ => null,
|
||||
};
|
||||
|
||||
private void SetSelectedCard(ToggleButton? selected)
|
||||
{
|
||||
foreach (ToggleButton card in new[] { Card30, Card60, Card120, CardForever, CardCustom, CardWhileApp })
|
||||
{
|
||||
card.IsChecked = ReferenceEquals(card, selected);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDurationCardClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is ToggleButton button
|
||||
&& button.Tag is string tag
|
||||
&& uint.TryParse(tag, NumberStyles.Integer, CultureInfo.InvariantCulture, out uint minutes))
|
||||
{
|
||||
ViewModel.PendingSelection = FlyoutSelectionKind.Timed;
|
||||
ViewModel.PendingMinutes = minutes;
|
||||
SetSelectedCard(button);
|
||||
ViewModel.ApplyPendingIfActive();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnForeverCardClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
ViewModel.PendingSelection = FlyoutSelectionKind.Forever;
|
||||
SetSelectedCard(CardForever);
|
||||
ViewModel.ApplyPendingIfActive();
|
||||
}
|
||||
|
||||
// Left segment of the Custom split tile: select the custom duration (using whatever value
|
||||
// was last configured) without leaving the launch page.
|
||||
private void OnCustomToggleClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
ViewModel.PendingSelection = FlyoutSelectionKind.Custom;
|
||||
HighlightSelectedCard();
|
||||
ViewModel.ApplyPendingIfActive();
|
||||
}
|
||||
|
||||
// Right chevron of the Custom split tile: navigate to the custom-time picker.
|
||||
private void OnCustomNavClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// Keep the custom page's tab in sync with the running session (duration vs. until-date).
|
||||
ViewModel.RefreshPendingCustomSubMode();
|
||||
|
||||
if (_context != null && Frame != null)
|
||||
{
|
||||
Frame.Navigate(typeof(AwakeCustomTimePage), _context, new SlideNavigationTransitionInfo { Effect = SlideNavigationTransitionEffect.FromRight });
|
||||
}
|
||||
}
|
||||
|
||||
// Left segment of the While-app split tile: select the existing app binding if one was
|
||||
// already chosen; otherwise jump straight to the picker since there is nothing to select.
|
||||
private void OnWhileAppToggleClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (ViewModel.PendingProcessId != 0)
|
||||
{
|
||||
ViewModel.PendingSelection = FlyoutSelectionKind.WhileApp;
|
||||
HighlightSelectedCard();
|
||||
ViewModel.ApplyPendingIfActive();
|
||||
}
|
||||
else
|
||||
{
|
||||
HighlightSelectedCard();
|
||||
NavigateToAppPicker();
|
||||
}
|
||||
}
|
||||
|
||||
// Right chevron of the While-app split tile: navigate to the app picker.
|
||||
private void OnWhileAppNavClick(object sender, RoutedEventArgs e) => NavigateToAppPicker();
|
||||
|
||||
private void NavigateToAppPicker()
|
||||
{
|
||||
if (_context != null && Frame != null)
|
||||
{
|
||||
Frame.Navigate(typeof(AwakeAppPickerPage), _context, new SlideNavigationTransitionInfo { Effect = SlideNavigationTransitionEffect.FromRight });
|
||||
}
|
||||
}
|
||||
|
||||
private void OnStartButtonClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
ViewModel.ApplyPendingSelection();
|
||||
}
|
||||
|
||||
private void OnStopButtonClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
ViewModel.Mode = AwakeMode.PASSIVE;
|
||||
}
|
||||
|
||||
private void OnOpenSettingsClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
ViewModel.OpenSettingsCommand.Execute(null);
|
||||
_context?.RequestClose();
|
||||
}
|
||||
|
||||
private void OnKeyDown(object sender, KeyRoutedEventArgs e)
|
||||
{
|
||||
if (e.Key == VirtualKey.Escape)
|
||||
{
|
||||
_context?.RequestClose();
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnHeaderGlowLoaded(object sender, RoutedEventArgs e) => BuildGlow();
|
||||
|
||||
private void OnHeaderGlowSizeChanged(object sender, SizeChangedEventArgs e) => LayoutGlow();
|
||||
|
||||
// Builds a couple of soft accent-tinted radial "blobs" that slowly drift and pulse behind
|
||||
// the active header, giving a subtle organic glow without a flat colored fill.
|
||||
private void BuildGlow()
|
||||
{
|
||||
if (_glowRoot != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Compositor compositor = ElementCompositionPreview.GetElementVisual(HeaderGlowHost).Compositor;
|
||||
_glowRoot = compositor.CreateContainerVisual();
|
||||
ElementCompositionPreview.SetElementChildVisual(HeaderGlowHost, _glowRoot);
|
||||
|
||||
PopulateGlowBlobs(compositor);
|
||||
LayoutGlow();
|
||||
}
|
||||
|
||||
// Clears and re-creates the glow blobs with the current accent color. Called when the
|
||||
// flyout opens so the glow picks up any accent/theme change that happened while hidden.
|
||||
public void RefreshGlow()
|
||||
{
|
||||
if (_glowRoot == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_glowRoot.Children.RemoveAll();
|
||||
PopulateGlowBlobs(_glowRoot.Compositor);
|
||||
LayoutGlow();
|
||||
}
|
||||
|
||||
// Builds a layered, animated Fluent "aurora" behind the active header: drifting/breathing
|
||||
// accent light clouds, brighter drifting light streaks, twinkling sparkles, and periodic
|
||||
// bright shine sweeps that glint diagonally across for a lively, premium feel.
|
||||
private void PopulateGlowBlobs(Compositor compositor)
|
||||
{
|
||||
Color accent = (Application.Current.Resources["SystemAccentColor"] is Color a) ? a : Colors.White;
|
||||
Color accentLight1 = (Application.Current.Resources["SystemAccentColorLight1"] is Color l1) ? l1 : accent;
|
||||
Color accentLight2 = (Application.Current.Resources["SystemAccentColorLight2"] is Color l2) ? l2 : accent;
|
||||
Color white = Colors.White;
|
||||
|
||||
// Aurora clouds (bottom layer): (color, sizeRel, startRel, endRel, maxOpacity, seconds, delay)
|
||||
AddAurora(compositor, accentLight1, new Vector2(0.95f, 1.30f), new Vector2(0.15f, 0.30f), new Vector2(0.52f, 0.20f), 0.60f, 9, 0f);
|
||||
AddAurora(compositor, accentLight2, new Vector2(0.85f, 1.15f), new Vector2(0.82f, 0.42f), new Vector2(0.44f, 0.30f), 0.52f, 11, 2f);
|
||||
AddAurora(compositor, accent, new Vector2(0.70f, 1.00f), new Vector2(0.52f, 0.16f), new Vector2(0.74f, 0.34f), 0.34f, 13, 4f);
|
||||
|
||||
// Soft, blurred drifting light streaks: (color, sizeRel, startRel, endRel, maxOpacity, minOpacity, seconds, delay, angle)
|
||||
AddStreak(compositor, white, new Vector2(1.60f, 1.05f), new Vector2(0.10f, 0.20f), new Vector2(0.55f, 0.12f), 0.42f, 0.14f, 16, 0f, -18f);
|
||||
AddStreak(compositor, accentLight2, new Vector2(1.75f, 1.20f), new Vector2(0.60f, 0.46f), new Vector2(0.98f, 0.32f), 0.40f, 0.12f, 20, 2f, -24f);
|
||||
|
||||
// Shine sweeps (the "pop"): bright bands that glint across, then pause. (color, maxOpacity, seconds, delay, angle)
|
||||
AddSweep(compositor, white, 0.85f, 4.2f, 0.4f, -20f);
|
||||
AddSweep(compositor, accentLight1, 0.60f, 5.3f, 2.1f, -14f);
|
||||
AddSweep(compositor, white, 0.70f, 6.1f, 3.6f, -26f);
|
||||
|
||||
// Sparkles (top layer): (color, posRel, maxOpacity, diameter, delay)
|
||||
AddSparkle(compositor, white, new Vector2(0.70f, 0.20f), 1.0f, 3.6f, 0f);
|
||||
AddSparkle(compositor, white, new Vector2(0.86f, 0.38f), 0.85f, 3.0f, 1.1f);
|
||||
AddSparkle(compositor, white, new Vector2(0.58f, 0.30f), 0.75f, 2.6f, 0.6f);
|
||||
AddSparkle(compositor, accentLight1, new Vector2(0.78f, 0.52f), 0.70f, 2.4f, 1.7f);
|
||||
}
|
||||
|
||||
// A soft vertical fade (transparent top/bottom, opaque middle) used to give layers soft
|
||||
// edges and keep the glow concentrated away from the header's bottom text.
|
||||
private CompositionLinearGradientBrush VerticalFade(Compositor compositor)
|
||||
{
|
||||
var fade = compositor.CreateLinearGradientBrush();
|
||||
fade.StartPoint = new Vector2(0.5f, 0f);
|
||||
fade.EndPoint = new Vector2(0.5f, 1f);
|
||||
fade.ColorStops.Add(compositor.CreateColorGradientStop(0f, Color.FromArgb(0, 255, 255, 255)));
|
||||
fade.ColorStops.Add(compositor.CreateColorGradientStop(0.18f, Colors.White));
|
||||
fade.ColorStops.Add(compositor.CreateColorGradientStop(0.6f, Colors.White));
|
||||
fade.ColorStops.Add(compositor.CreateColorGradientStop(1f, Color.FromArgb(0, 255, 255, 255)));
|
||||
return fade;
|
||||
}
|
||||
|
||||
// A large, soft accent cloud that slowly drifts (LayoutGlow) and gently breathes (scale).
|
||||
private void AddAurora(Compositor compositor, Color color, Vector2 sizeRel, Vector2 startRel, Vector2 endRel, float maxOpacity, int seconds, float delay)
|
||||
{
|
||||
var radial = compositor.CreateRadialGradientBrush();
|
||||
radial.ColorStops.Add(compositor.CreateColorGradientStop(0f, Color.FromArgb((byte)(maxOpacity * 255), color.R, color.G, color.B)));
|
||||
radial.ColorStops.Add(compositor.CreateColorGradientStop(0.55f, Color.FromArgb((byte)(maxOpacity * 0.45f * 255), color.R, color.G, color.B)));
|
||||
radial.ColorStops.Add(compositor.CreateColorGradientStop(1f, Color.FromArgb(0, color.R, color.G, color.B)));
|
||||
|
||||
var mask = compositor.CreateMaskBrush();
|
||||
mask.Source = radial;
|
||||
mask.Mask = VerticalFade(compositor);
|
||||
|
||||
var blob = compositor.CreateSpriteVisual();
|
||||
blob.Brush = mask;
|
||||
blob.AnchorPoint = new Vector2(0.5f, 0.5f);
|
||||
blob.Properties.InsertVector2("Start", startRel);
|
||||
blob.Properties.InsertVector2("End", endRel);
|
||||
blob.Properties.InsertVector2("SizeRel", sizeRel);
|
||||
blob.Properties.InsertScalar("Seconds", seconds);
|
||||
blob.Properties.InsertScalar("Delay", delay);
|
||||
_glowRoot!.Children.InsertAtTop(blob);
|
||||
|
||||
var breathe = compositor.CreateVector3KeyFrameAnimation();
|
||||
breathe.InsertKeyFrame(0f, new Vector3(0.9f, 0.9f, 1f));
|
||||
breathe.InsertKeyFrame(0.5f, new Vector3(1.15f, 1.15f, 1f));
|
||||
breathe.InsertKeyFrame(1f, new Vector3(0.9f, 0.9f, 1f));
|
||||
breathe.Duration = TimeSpan.FromSeconds(seconds);
|
||||
breathe.IterationBehavior = AnimationIterationBehavior.Forever;
|
||||
breathe.DelayTime = TimeSpan.FromSeconds(delay);
|
||||
blob.StartAnimation("Scale", breathe);
|
||||
}
|
||||
|
||||
// A bright, narrow diagonal band that periodically sweeps across the header and then pauses,
|
||||
// producing a Fluent "reveal" shimmer. Offset is driven in LayoutGlow (needs host size).
|
||||
private void AddSweep(Compositor compositor, Color color, float maxOpacity, float seconds, float delay, float angle)
|
||||
{
|
||||
byte core = (byte)(maxOpacity * 255);
|
||||
byte soft = (byte)(maxOpacity * 0.5f * 255);
|
||||
|
||||
var band = compositor.CreateLinearGradientBrush();
|
||||
band.StartPoint = new Vector2(0f, 0.5f);
|
||||
band.EndPoint = new Vector2(1f, 0.5f);
|
||||
band.ColorStops.Add(compositor.CreateColorGradientStop(0f, Color.FromArgb(0, color.R, color.G, color.B)));
|
||||
band.ColorStops.Add(compositor.CreateColorGradientStop(0.42f, Color.FromArgb(soft, color.R, color.G, color.B)));
|
||||
band.ColorStops.Add(compositor.CreateColorGradientStop(0.5f, Color.FromArgb(core, 255, 255, 255)));
|
||||
band.ColorStops.Add(compositor.CreateColorGradientStop(0.58f, Color.FromArgb(soft, color.R, color.G, color.B)));
|
||||
band.ColorStops.Add(compositor.CreateColorGradientStop(1f, Color.FromArgb(0, color.R, color.G, color.B)));
|
||||
|
||||
var mask = compositor.CreateMaskBrush();
|
||||
mask.Source = band;
|
||||
mask.Mask = VerticalFade(compositor);
|
||||
|
||||
var sweep = compositor.CreateSpriteVisual();
|
||||
sweep.Brush = mask;
|
||||
sweep.AnchorPoint = new Vector2(0.5f, 0.5f);
|
||||
sweep.RotationAngleInDegrees = angle;
|
||||
sweep.Opacity = 0f;
|
||||
sweep.Properties.InsertScalar("Sweep", 1f);
|
||||
sweep.Properties.InsertScalar("Seconds", seconds);
|
||||
sweep.Properties.InsertScalar("Delay", delay);
|
||||
_glowRoot!.Children.InsertAtTop(sweep);
|
||||
|
||||
// Flash the band on only while it crosses, then stay dark, with soft eased fade in/out.
|
||||
var flashEase = compositor.CreateCubicBezierEasingFunction(new Vector2(0.33f, 0f), new Vector2(0.67f, 1f));
|
||||
var flash = compositor.CreateScalarKeyFrameAnimation();
|
||||
flash.InsertKeyFrame(0f, 0f);
|
||||
flash.InsertKeyFrame(0.05f, 0f);
|
||||
flash.InsertKeyFrame(0.18f, maxOpacity, flashEase);
|
||||
flash.InsertKeyFrame(0.30f, maxOpacity);
|
||||
flash.InsertKeyFrame(0.42f, 0f, flashEase);
|
||||
flash.InsertKeyFrame(1f, 0f);
|
||||
flash.Duration = TimeSpan.FromSeconds(seconds);
|
||||
flash.IterationBehavior = AnimationIterationBehavior.Forever;
|
||||
flash.DelayTime = TimeSpan.FromSeconds(delay);
|
||||
sweep.StartAnimation("Opacity", flash);
|
||||
}
|
||||
|
||||
// A soft, rotated band of light (transparent -> color -> transparent) that slowly drifts
|
||||
// and twinkles. Size/position are resolved against the host in LayoutGlow.
|
||||
private void AddStreak(Compositor compositor, Color color, Vector2 sizeRel, Vector2 startRel, Vector2 endRel, float maxOpacity, float minOpacity, int seconds, float delay, float angle)
|
||||
{
|
||||
var band = compositor.CreateLinearGradientBrush();
|
||||
band.StartPoint = new Vector2(0f, 0.5f);
|
||||
band.EndPoint = new Vector2(1f, 0.5f);
|
||||
band.ColorStops.Add(compositor.CreateColorGradientStop(0f, Color.FromArgb(0, color.R, color.G, color.B)));
|
||||
band.ColorStops.Add(compositor.CreateColorGradientStop(0.25f, Color.FromArgb((byte)(maxOpacity * 0.35f * 255), color.R, color.G, color.B)));
|
||||
band.ColorStops.Add(compositor.CreateColorGradientStop(0.5f, Color.FromArgb((byte)(maxOpacity * 255), color.R, color.G, color.B)));
|
||||
band.ColorStops.Add(compositor.CreateColorGradientStop(0.75f, Color.FromArgb((byte)(maxOpacity * 0.35f * 255), color.R, color.G, color.B)));
|
||||
band.ColorStops.Add(compositor.CreateColorGradientStop(1f, Color.FromArgb(0, color.R, color.G, color.B)));
|
||||
|
||||
var mask = compositor.CreateMaskBrush();
|
||||
mask.Source = band;
|
||||
mask.Mask = VerticalFade(compositor);
|
||||
|
||||
var streak = compositor.CreateSpriteVisual();
|
||||
streak.Brush = mask;
|
||||
streak.AnchorPoint = new Vector2(0.5f, 0.5f);
|
||||
streak.RotationAngleInDegrees = angle;
|
||||
streak.Opacity = minOpacity;
|
||||
streak.Properties.InsertVector2("Start", startRel);
|
||||
streak.Properties.InsertVector2("End", endRel);
|
||||
streak.Properties.InsertVector2("SizeRel", sizeRel);
|
||||
streak.Properties.InsertScalar("Seconds", seconds);
|
||||
streak.Properties.InsertScalar("Delay", delay);
|
||||
_glowRoot!.Children.InsertAtTop(streak);
|
||||
|
||||
var twinkleEase = compositor.CreateCubicBezierEasingFunction(new Vector2(0.42f, 0f), new Vector2(0.58f, 1f));
|
||||
var twinkle = compositor.CreateScalarKeyFrameAnimation();
|
||||
twinkle.InsertKeyFrame(0f, minOpacity);
|
||||
twinkle.InsertKeyFrame(0.5f, maxOpacity, twinkleEase);
|
||||
twinkle.InsertKeyFrame(1f, minOpacity, twinkleEase);
|
||||
twinkle.Duration = TimeSpan.FromSeconds(seconds * 0.6);
|
||||
twinkle.IterationBehavior = AnimationIterationBehavior.Forever;
|
||||
twinkle.DelayTime = TimeSpan.FromSeconds(delay);
|
||||
streak.StartAnimation("Opacity", twinkle);
|
||||
}
|
||||
|
||||
// A tiny radial highlight that fades and pulses in place to read as a shimmer/sparkle.
|
||||
private void AddSparkle(Compositor compositor, Color color, Vector2 posRel, float maxOpacity, float diameter, float delay)
|
||||
{
|
||||
var radial = compositor.CreateRadialGradientBrush();
|
||||
radial.ColorStops.Add(compositor.CreateColorGradientStop(0f, Color.FromArgb(255, color.R, color.G, color.B)));
|
||||
radial.ColorStops.Add(compositor.CreateColorGradientStop(1f, Color.FromArgb(0, color.R, color.G, color.B)));
|
||||
|
||||
var sparkle = compositor.CreateSpriteVisual();
|
||||
sparkle.Brush = radial;
|
||||
sparkle.Size = new Vector2(diameter, diameter);
|
||||
sparkle.AnchorPoint = new Vector2(0.5f, 0.5f);
|
||||
sparkle.CenterPoint = new Vector3(diameter / 2f, diameter / 2f, 0);
|
||||
sparkle.Opacity = 0f;
|
||||
sparkle.Properties.InsertVector2("Pos", posRel);
|
||||
_glowRoot!.Children.InsertAtTop(sparkle);
|
||||
|
||||
var twinkle = compositor.CreateScalarKeyFrameAnimation();
|
||||
twinkle.InsertKeyFrame(0f, 0f);
|
||||
twinkle.InsertKeyFrame(0.5f, maxOpacity);
|
||||
twinkle.InsertKeyFrame(1f, 0f);
|
||||
twinkle.Duration = TimeSpan.FromSeconds(2.6);
|
||||
twinkle.IterationBehavior = AnimationIterationBehavior.Forever;
|
||||
twinkle.DelayTime = TimeSpan.FromSeconds(delay);
|
||||
sparkle.StartAnimation("Opacity", twinkle);
|
||||
|
||||
var pulse = compositor.CreateVector3KeyFrameAnimation();
|
||||
pulse.InsertKeyFrame(0f, new Vector3(0.4f, 0.4f, 1f));
|
||||
pulse.InsertKeyFrame(0.5f, new Vector3(1f, 1f, 1f));
|
||||
pulse.InsertKeyFrame(1f, new Vector3(0.4f, 0.4f, 1f));
|
||||
pulse.Duration = TimeSpan.FromSeconds(2.6);
|
||||
pulse.IterationBehavior = AnimationIterationBehavior.Forever;
|
||||
pulse.DelayTime = TimeSpan.FromSeconds(delay);
|
||||
sparkle.StartAnimation("Scale", pulse);
|
||||
}
|
||||
|
||||
private void LayoutGlow()
|
||||
{
|
||||
if (_glowRoot == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var size = new Vector2((float)HeaderGlowHost.ActualWidth, (float)HeaderGlowHost.ActualHeight);
|
||||
if (size.X <= 0 || size.Y <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Compositor compositor = _glowRoot.Compositor;
|
||||
_glowRoot.Size = size;
|
||||
_glowRoot.Clip = compositor.CreateInsetClip();
|
||||
|
||||
foreach (var child in _glowRoot.Children)
|
||||
{
|
||||
if (child is not SpriteVisual visual)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Sparkles: fixed-size dots anchored at a relative point.
|
||||
if (visual.Properties.TryGetVector2("Pos", out Vector2 pos) == CompositionGetValueStatus.Succeeded)
|
||||
{
|
||||
visual.Offset = new Vector3(pos.X * size.X, pos.Y * size.Y, 0);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Shine sweep: narrow band that races across in the first third of the cycle, then holds.
|
||||
if (visual.Properties.TryGetScalar("Sweep", out float sweepFlag) == CompositionGetValueStatus.Succeeded && sweepFlag > 0f)
|
||||
{
|
||||
visual.Size = new Vector2(size.X * 0.5f, size.Y * 1.9f);
|
||||
visual.CenterPoint = new Vector3(visual.Size.X / 2f, visual.Size.Y / 2f, 0);
|
||||
|
||||
visual.Properties.TryGetScalar("Seconds", out float sweepSecs);
|
||||
visual.Properties.TryGetScalar("Delay", out float sweepDelay);
|
||||
|
||||
float midY = size.Y * 0.5f;
|
||||
var ease = compositor.CreateCubicBezierEasingFunction(new Vector2(0.45f, 0f), new Vector2(0.35f, 1f));
|
||||
var run = compositor.CreateVector3KeyFrameAnimation();
|
||||
run.InsertKeyFrame(0f, new Vector3(-0.45f * size.X, midY, 0));
|
||||
run.InsertKeyFrame(0.42f, new Vector3(1.45f * size.X, midY, 0), ease);
|
||||
run.InsertKeyFrame(1f, new Vector3(1.45f * size.X, midY, 0));
|
||||
run.Duration = TimeSpan.FromSeconds(sweepSecs > 0 ? sweepSecs : 5);
|
||||
run.DelayTime = TimeSpan.FromSeconds(sweepDelay);
|
||||
run.IterationBehavior = AnimationIterationBehavior.Forever;
|
||||
visual.Offset = new Vector3(-0.45f * size.X, midY, 0);
|
||||
visual.StartAnimation("Offset", run);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Aurora clouds & streaks: size relative to the host, rotate about their center,
|
||||
// and gently drift back and forth between two points.
|
||||
if (visual.Properties.TryGetVector2("SizeRel", out Vector2 sizeRel) == CompositionGetValueStatus.Succeeded
|
||||
&& visual.Properties.TryGetVector2("Start", out Vector2 start) == CompositionGetValueStatus.Succeeded
|
||||
&& visual.Properties.TryGetVector2("End", out Vector2 end) == CompositionGetValueStatus.Succeeded)
|
||||
{
|
||||
visual.Size = new Vector2(sizeRel.X * size.X, sizeRel.Y * size.Y);
|
||||
visual.CenterPoint = new Vector3(visual.Size.X / 2f, visual.Size.Y / 2f, 0);
|
||||
|
||||
visual.Properties.TryGetScalar("Seconds", out float secs);
|
||||
visual.Properties.TryGetScalar("Delay", out float delay);
|
||||
|
||||
var driftEase = compositor.CreateCubicBezierEasingFunction(new Vector2(0.42f, 0f), new Vector2(0.58f, 1f));
|
||||
var drift = compositor.CreateVector3KeyFrameAnimation();
|
||||
drift.InsertKeyFrame(0f, new Vector3(start.X * size.X, start.Y * size.Y, 0));
|
||||
drift.InsertKeyFrame(1f, new Vector3(end.X * size.X, end.Y * size.Y, 0), driftEase);
|
||||
drift.Duration = TimeSpan.FromSeconds(secs > 0 ? secs : 20);
|
||||
drift.DelayTime = TimeSpan.FromSeconds(delay);
|
||||
drift.IterationBehavior = AnimationIterationBehavior.Forever;
|
||||
drift.Direction = Microsoft.UI.Composition.AnimationDirection.Alternate;
|
||||
visual.Offset = new Vector3(start.X * size.X, start.Y * size.Y, 0);
|
||||
visual.StartAnimation("Offset", drift);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
src/modules/awake/Awake/AwakeXAML/AwakeShellPage.xaml
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<Page
|
||||
x:Class="Awake.AwakeShellPage"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Loaded="OnPageLoaded">
|
||||
|
||||
<Grid>
|
||||
<Frame x:Name="ContentFrame" />
|
||||
</Grid>
|
||||
</Page>
|
||||
102
src/modules/awake/Awake/AwakeXAML/AwakeShellPage.xaml.cs
Normal file
@@ -0,0 +1,102 @@
|
||||
// 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.Diagnostics.CodeAnalysis;
|
||||
using Awake.ViewModels;
|
||||
using ManagedCommon;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Media.Animation;
|
||||
using Microsoft.UI.Xaml.Navigation;
|
||||
|
||||
namespace Awake
|
||||
{
|
||||
/// <summary>
|
||||
/// Hosts the flyout's navigation frame. The launch page is the root; the custom-time and
|
||||
/// app-picker pages slide in over it with a back button (same pattern as the QuickAccess
|
||||
/// flyout shell).
|
||||
/// </summary>
|
||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicMethods)]
|
||||
public sealed partial class AwakeShellPage : Page
|
||||
{
|
||||
private AwakeFlyoutNavigationContext? _context;
|
||||
|
||||
public AwakeShellPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
ContentFrame.NavigationFailed += OnNavigationFailed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raised when a hosted page asks the window to dismiss the flyout (Escape on the launch
|
||||
/// page, or opening Settings).
|
||||
/// </summary>
|
||||
public event EventHandler? CloseRequested;
|
||||
|
||||
public void Initialize(AwakeFlyoutViewModel viewModel)
|
||||
{
|
||||
_context = new AwakeFlyoutNavigationContext(
|
||||
viewModel,
|
||||
() => CloseRequested?.Invoke(this, EventArgs.Empty));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the frame to the launch page (clearing any sub-page and the back stack) so each
|
||||
/// time the flyout is summoned it opens on the main view. A fresh navigation also rebuilds
|
||||
/// the header glow with the current accent color.
|
||||
/// </summary>
|
||||
public void NavigateToLaunch()
|
||||
{
|
||||
if (_context == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ContentFrame.Navigate(typeof(AwakeLaunchPage), _context, new SuppressNavigationTransitionInfo());
|
||||
ContentFrame.BackStack.Clear();
|
||||
}
|
||||
|
||||
public void FocusContent()
|
||||
{
|
||||
if (ContentFrame.Content is AwakeLaunchPage launchPage)
|
||||
{
|
||||
launchPage.FocusContent();
|
||||
}
|
||||
else
|
||||
{
|
||||
(ContentFrame.Content as Control)?.Focus(FocusState.Programmatic);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Forwards a glow rebuild to the launch page when it is the active content.
|
||||
/// </summary>
|
||||
public void RefreshGlow()
|
||||
{
|
||||
if (ContentFrame.Content is AwakeLaunchPage launchPage)
|
||||
{
|
||||
launchPage.RefreshGlow();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPageLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_context == null || ContentFrame.Content is AwakeLaunchPage)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ContentFrame.Navigate(typeof(AwakeLaunchPage), _context, new SuppressNavigationTransitionInfo());
|
||||
}
|
||||
|
||||
private static void OnNavigationFailed(object sender, NavigationFailedEventArgs e)
|
||||
{
|
||||
// A page constructor or XAML load failure here would otherwise crash the flyout.
|
||||
// Log and mark handled so the flyout stays available; the next summon retries.
|
||||
Logger.LogError($"Awake: navigation to '{e.SourcePageType?.FullName}' failed.", e.Exception);
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
24
src/modules/awake/Awake/AwakeXAML/MainWindow.xaml
Normal file
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<winuiex:WindowEx
|
||||
x:Class="Awake.MainWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:backdrops="using:Microsoft.PowerToys.Common.UI.Controls.Backdrops"
|
||||
xmlns:local="using:Awake"
|
||||
xmlns:winuiex="using:WinUIEx"
|
||||
Width="364"
|
||||
Height="486"
|
||||
MinWidth="364"
|
||||
MinHeight="486"
|
||||
IsMaximizable="False"
|
||||
IsMinimizable="False"
|
||||
IsResizable="False"
|
||||
IsTitleBarVisible="False">
|
||||
<winuiex:WindowEx.SystemBackdrop>
|
||||
<backdrops:AlwaysActiveDesktopAcrylicBackdrop Kind="Default" />
|
||||
</winuiex:WindowEx.SystemBackdrop>
|
||||
|
||||
<Grid x:Name="RootHost">
|
||||
<local:AwakeShellPage x:Name="ShellHost" />
|
||||
</Grid>
|
||||
</winuiex:WindowEx>
|
||||
269
src/modules/awake/Awake/AwakeXAML/MainWindow.xaml.cs
Normal file
@@ -0,0 +1,269 @@
|
||||
// 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.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Awake.Properties;
|
||||
using Awake.ViewModels;
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Common.UI.Controls.Window;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.UI.Windowing;
|
||||
using Microsoft.UI.Xaml;
|
||||
using WinUIEx;
|
||||
|
||||
namespace Awake
|
||||
{
|
||||
/// <summary>
|
||||
/// The Awake tray flyout window. Hidden at startup; shown when the user clicks the tray icon.
|
||||
/// Auto-hides when it loses activation (same behavior as the PowerDisplay flyout).
|
||||
/// The flyout body lives in <see cref="AwakeShellPage"/> (a navigation frame whose root
|
||||
/// is <see cref="AwakeLaunchPage"/>); this window owns the
|
||||
/// window lifecycle, positioning, and the countdown timer.
|
||||
/// </summary>
|
||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicMethods)]
|
||||
public sealed partial class MainWindow : WindowEx, IDisposable
|
||||
{
|
||||
// Flyout size is declared in XAML (Width/Height). We capture those values at
|
||||
// construction time so later DPI transitions don't perturb WindowEx.Width/Height
|
||||
// and our positioning math stays stable across multiple Show/Hide cycles
|
||||
// (same pattern as QuickAccess.UI MainWindow).
|
||||
private const int FlyoutRightMarginDip = 12;
|
||||
private const int FlyoutBottomMarginDip = 12;
|
||||
|
||||
// Delay the working-set trim after hide so quick toggles don't trigger aggressive
|
||||
// GC; cancel it on re-show. The trim only releases idle UI/heap pages back to the OS —
|
||||
// it has no effect on the keep-awake state (driven by SetThreadExecutionState in Manager).
|
||||
private const int MemoryTrimDelayMs = 2000;
|
||||
|
||||
private readonly AwakeFlyoutViewModel _viewModel;
|
||||
private readonly int _designWidthDip;
|
||||
private readonly int _designHeightDip;
|
||||
private readonly DispatcherTimer _countdownTimer;
|
||||
private CancellationTokenSource? _trimCts;
|
||||
private bool _isShowingWindow;
|
||||
private bool _disposed;
|
||||
|
||||
public AwakeFlyoutViewModel ViewModel => _viewModel;
|
||||
|
||||
public MainWindow(bool startedFromPowerToys)
|
||||
{
|
||||
try
|
||||
{
|
||||
_viewModel = new AwakeFlyoutViewModel(SettingsUtils.Default, startedFromPowerToys);
|
||||
|
||||
this.InitializeComponent();
|
||||
|
||||
// Snapshot the XAML-declared design size BEFORE anything else touches
|
||||
// the window — see comment above on _designWidthDip.
|
||||
_designWidthDip = (int)Math.Ceiling(this.Width);
|
||||
_designHeightDip = (int)Math.Ceiling(this.Height);
|
||||
|
||||
ShellHost.Initialize(_viewModel);
|
||||
ShellHost.CloseRequested += OnFlyoutCloseRequested;
|
||||
|
||||
// The window title isn't a XAML element, so it can't use x:Uid; set it here.
|
||||
// All other UI strings are localized via x:Uid against Strings\<lang>\Resources.resw.
|
||||
this.AppWindow.Title = Resources.AWAKE_FLYOUT_TITLE;
|
||||
|
||||
ConfigureWindow();
|
||||
RegisterEventHandlers();
|
||||
|
||||
_countdownTimer = new DispatcherTimer
|
||||
{
|
||||
Interval = TimeSpan.FromSeconds(1),
|
||||
};
|
||||
_countdownTimer.Tick += OnCountdownTick;
|
||||
|
||||
this.SetIsShownInSwitchers(false);
|
||||
|
||||
// Window starts hidden at launch; trim the initial working set so the idle
|
||||
// background footprint drops without waiting for a first show/hide cycle.
|
||||
ScheduleMemoryTrim();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"MainWindow constructor failed: {ex}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnCountdownTick(object? sender, object e)
|
||||
{
|
||||
_viewModel.UpdateCountdown();
|
||||
}
|
||||
|
||||
private void ConfigureWindow()
|
||||
{
|
||||
try
|
||||
{
|
||||
PositionFlyout();
|
||||
|
||||
var titleBar = this.AppWindow.TitleBar;
|
||||
if (titleBar != null)
|
||||
{
|
||||
titleBar.ExtendsContentIntoTitleBar = true;
|
||||
titleBar.PreferredHeightOption = TitleBarHeightOption.Collapsed;
|
||||
titleBar.SetDragRectangles(Array.Empty<Windows.Graphics.RectInt32>());
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning($"ConfigureWindow: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void RegisterEventHandlers()
|
||||
{
|
||||
this.Closed += OnWindowClosed;
|
||||
this.Activated += OnWindowActivated;
|
||||
}
|
||||
|
||||
private void OnFlyoutCloseRequested(object? sender, EventArgs e)
|
||||
{
|
||||
HideWindow();
|
||||
}
|
||||
|
||||
private void OnWindowActivated(object sender, WindowActivatedEventArgs args)
|
||||
{
|
||||
if (args.WindowActivationState == WindowActivationState.Deactivated && !_isShowingWindow)
|
||||
{
|
||||
HideWindow();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnWindowClosed(object sender, WindowEventArgs args)
|
||||
{
|
||||
args.Handled = true;
|
||||
HideWindow();
|
||||
}
|
||||
|
||||
public void ShowWindow()
|
||||
{
|
||||
_isShowingWindow = true;
|
||||
try
|
||||
{
|
||||
CancelMemoryTrim();
|
||||
_viewModel.Refresh();
|
||||
ShellHost.NavigateToLaunch();
|
||||
ShellHost.RefreshGlow();
|
||||
PositionFlyout();
|
||||
this.Activate();
|
||||
this.Show();
|
||||
this.IsAlwaysOnTop = true;
|
||||
this.BringToFront();
|
||||
ShellHost.FocusContent();
|
||||
_countdownTimer.Start();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"ShowWindow failed: {ex}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isShowingWindow = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void HideWindow()
|
||||
{
|
||||
try
|
||||
{
|
||||
_countdownTimer.Stop();
|
||||
this.Hide();
|
||||
ScheduleMemoryTrim();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"HideWindow failed: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
// Releases idle pages back to the OS ~2s after the flyout is hidden so the background
|
||||
// working set drops. Cancelled on re-show to avoid GC churn during quick toggles.
|
||||
private void ScheduleMemoryTrim()
|
||||
{
|
||||
CancelMemoryTrim();
|
||||
_trimCts = new CancellationTokenSource();
|
||||
var token = _trimCts.Token;
|
||||
|
||||
Task.Delay(MemoryTrimDelayMs, token).ContinueWith(
|
||||
_ =>
|
||||
{
|
||||
if (token.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
GC.Collect();
|
||||
GC.WaitForPendingFinalizers();
|
||||
GC.Collect();
|
||||
SetProcessWorkingSetSize(System.Diagnostics.Process.GetCurrentProcess().Handle, -1, -1);
|
||||
},
|
||||
token,
|
||||
TaskContinuationOptions.OnlyOnRanToCompletion,
|
||||
TaskScheduler.Default);
|
||||
}
|
||||
|
||||
private void CancelMemoryTrim()
|
||||
{
|
||||
_trimCts?.Cancel();
|
||||
_trimCts?.Dispose();
|
||||
_trimCts = null;
|
||||
}
|
||||
|
||||
public void ToggleWindow()
|
||||
{
|
||||
if (this.Visible)
|
||||
{
|
||||
HideWindow();
|
||||
}
|
||||
else
|
||||
{
|
||||
ShowWindow();
|
||||
}
|
||||
}
|
||||
|
||||
private void PositionFlyout()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Use the cached XAML design size — this.Width/Height are runtime values
|
||||
// that can drift across DPI transitions; reusing them in PositionWindowBottomRight
|
||||
// would slowly walk the flyout off-screen over multiple Show/Hide cycles.
|
||||
FlyoutWindowHelper.PositionWindowBottomRight(
|
||||
this,
|
||||
_designWidthDip,
|
||||
_designHeightDip,
|
||||
FlyoutRightMarginDip,
|
||||
FlyoutBottomMarginDip);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Non-critical: window positioning failures fall back to OS-default placement.
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
CancelMemoryTrim();
|
||||
_countdownTimer.Stop();
|
||||
_countdownTimer.Tick -= OnCountdownTick;
|
||||
ShellHost.CloseRequested -= OnFlyoutCloseRequested;
|
||||
_viewModel.Dispose();
|
||||
}
|
||||
|
||||
[DllImport("kernel32.dll")]
|
||||
private static extern bool SetProcessWorkingSetSize(IntPtr hProcess, int dwMinimumWorkingSetSize, int dwMaximumWorkingSetSize);
|
||||
}
|
||||
}
|
||||
@@ -39,15 +39,42 @@ namespace Awake.Core
|
||||
|
||||
internal static AwakeMode CurrentOperatingMode { get; private set; }
|
||||
|
||||
private static bool IsDisplayOn { get; set; }
|
||||
internal static bool IsDisplayOn { get; private set; }
|
||||
|
||||
private static uint TimeRemaining { get; set; }
|
||||
internal static uint TimeRemaining { get; private set; }
|
||||
|
||||
private static string ScreenStateString => IsDisplayOn ? Resources.AWAKE_SCREEN_ON : Resources.AWAKE_SCREEN_OFF;
|
||||
|
||||
private static int ProcessId { get; set; }
|
||||
internal static int ProcessId { get; private set; }
|
||||
|
||||
private static DateTimeOffset ExpireAt { get; set; }
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the current keep-awake session is bound to a process
|
||||
/// the user picked from the flyout (the "While app runs" mode). When true the session ends
|
||||
/// automatically once <see cref="ProcessId"/> exits, reverting Awake to passive.
|
||||
/// </summary>
|
||||
internal static bool IsProcessBound { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the friendly name of the process the current session is bound to (for display in
|
||||
/// the flyout/tray). Empty when not process-bound.
|
||||
/// </summary>
|
||||
internal static string BoundProcessName { get; private set; } = string.Empty;
|
||||
|
||||
internal static DateTimeOffset ExpireAt { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the timestamp at which the current timed/expirable keep-awake session began.
|
||||
/// Together with <see cref="ExpireAt"/> this lets the flyout render a determinate
|
||||
/// countdown progress bar (elapsed = now - start, total = ExpireAt - start).
|
||||
/// </summary>
|
||||
internal static DateTimeOffset ModeStartedAt { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Raised whenever the operating mode, screen state, or expiration target changes
|
||||
/// so the WinUI flyout can refresh its state. Always raised on the thread that
|
||||
/// initiated the change; subscribers must marshal to the UI dispatcher themselves.
|
||||
/// </summary>
|
||||
internal static event EventHandler? ModeChanged;
|
||||
|
||||
private static readonly CompositeFormat AwakeMinute = CompositeFormat.Parse(Resources.AWAKE_MINUTE);
|
||||
private static readonly CompositeFormat AwakeMinutes = CompositeFormat.Parse(Resources.AWAKE_MINUTES);
|
||||
@@ -170,6 +197,13 @@ namespace Awake.Core
|
||||
_timerSubscription?.Dispose();
|
||||
_timerSubscription = null;
|
||||
|
||||
// Clear any process binding. Callers that establish a new binding (the CLI --pid path
|
||||
// and SetProcessBoundKeepAwake) re-set ProcessId afterwards, so this only clears stale
|
||||
// bindings when switching to a non-process mode.
|
||||
ProcessId = 0;
|
||||
IsProcessBound = false;
|
||||
BoundProcessName = string.Empty;
|
||||
|
||||
Logger.LogInfo("Timer subscription disposed.");
|
||||
}
|
||||
|
||||
@@ -183,32 +217,42 @@ namespace Awake.Core
|
||||
case AwakeMode.INDEFINITE:
|
||||
string pidLine = ProcessId == 0
|
||||
? string.Empty
|
||||
: $"\nPID: {ProcessId}";
|
||||
: (IsProcessBound && BoundProcessName.Length > 0
|
||||
? $"\n{BoundProcessName} (PID: {ProcessId})"
|
||||
: $"\nPID: {ProcessId}");
|
||||
iconText = $"{Constants.FullAppName}\n{Resources.AWAKE_TRAY_TEXT_INDEFINITE}\n{Resources.AWAKE_TRAY_DISPLAY}: {ScreenStateString}{pidLine}";
|
||||
icon = TrayHelper.IndefiniteIcon;
|
||||
icon = TrayIconService.IndefiniteIcon;
|
||||
break;
|
||||
|
||||
case AwakeMode.PASSIVE:
|
||||
iconText = $"{Constants.FullAppName}\n{Resources.AWAKE_SCREEN_OFF}";
|
||||
icon = TrayHelper.DisabledIcon;
|
||||
icon = TrayIconService.DisabledIcon;
|
||||
break;
|
||||
|
||||
case AwakeMode.EXPIRABLE:
|
||||
iconText = $"{Constants.FullAppName}\n{Resources.AWAKE_TRAY_UNTIL} {ExpireAt:MMM d, h:mm tt}\n{Resources.AWAKE_TRAY_DISPLAY}: {ScreenStateString}";
|
||||
icon = TrayHelper.ExpirableIcon;
|
||||
icon = TrayIconService.ExpirableIcon;
|
||||
break;
|
||||
|
||||
case AwakeMode.TIMED:
|
||||
iconText = $"{Constants.FullAppName}\n{Resources.AWAKE_TRAY_TEXT_TIMED}\n{Resources.AWAKE_TRAY_DISPLAY}: {ScreenStateString}";
|
||||
icon = TrayHelper.TimedIcon;
|
||||
icon = TrayIconService.TimedIcon;
|
||||
break;
|
||||
}
|
||||
|
||||
TrayHelper.SetShellIcon(
|
||||
TrayHelper.WindowHandle,
|
||||
iconText,
|
||||
icon,
|
||||
forceAdd ? TrayIconAction.Add : TrayIconAction.Update);
|
||||
if (icon is not null)
|
||||
{
|
||||
AwakeApp.Current?.UpdateTrayIcon(icon, iconText);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
ModeChanged?.Invoke(null, EventArgs.Empty);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Awake ModeChanged subscriber threw: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
internal static void SetIndefiniteKeepAwake(bool keepDisplayOn = false, int processId = 0, [CallerMemberName] string callerName = "")
|
||||
@@ -254,6 +298,46 @@ namespace Awake.Core
|
||||
SetModeShellIcon();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Keeps the system awake indefinitely while the process identified by <paramref name="processId"/>
|
||||
/// is running, automatically reverting to passive once it exits. This is the in-flyout
|
||||
/// counterpart of the CLI <c>--pid</c> path: it reuses the same indefinite keep-awake plus
|
||||
/// <see cref="RunnerHelper.WaitForPowerToysRunner"/> process-watch primitives, but (1) the exit
|
||||
/// callback reverts to passive instead of terminating Awake (the process must stay alive to host
|
||||
/// the tray icon and flyout), and (2) it does not persist the mode to settings because a PID is
|
||||
/// not stable across restarts.
|
||||
/// </summary>
|
||||
internal static void SetProcessBoundKeepAwake(int processId, string processName, bool keepDisplayOn = false, [CallerMemberName] string callerName = "")
|
||||
{
|
||||
PowerToysTelemetry.Log.WriteEvent(new Telemetry.AwakeIndefinitelyKeepAwakeEvent());
|
||||
|
||||
Logger.LogInfo($"Process-bound keep-awake starting for PID {processId} ({processName}), invoked by {callerName}...");
|
||||
|
||||
CancelExistingThread();
|
||||
|
||||
_stateQueue.Add(ComputeAwakeState(keepDisplayOn));
|
||||
|
||||
IsDisplayOn = keepDisplayOn;
|
||||
CurrentOperatingMode = AwakeMode.INDEFINITE;
|
||||
ProcessId = processId;
|
||||
IsProcessBound = true;
|
||||
BoundProcessName = processName ?? string.Empty;
|
||||
|
||||
SetModeShellIcon();
|
||||
|
||||
// Watch the bound process; when it exits, revert to passive on this (long-lived) process.
|
||||
// The callback runs on a background thread (same as the timed/expirable completion paths),
|
||||
// and guards against stale watchers by checking we are still bound to this exact PID.
|
||||
RunnerHelper.WaitForPowerToysRunner(processId, () =>
|
||||
{
|
||||
if (CurrentOperatingMode == AwakeMode.INDEFINITE && IsProcessBound && ProcessId == processId)
|
||||
{
|
||||
Logger.LogInfo($"Bound process {processId} exited; reverting Awake to passive.");
|
||||
SetPassiveKeepAwake(updateSettings: false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
internal static void SetExpirableKeepAwake(DateTimeOffset expireAt, bool keepDisplayOn = true, [CallerMemberName] string callerName = "")
|
||||
{
|
||||
Logger.LogInfo($"Expirable keep-awake invoked by {callerName}. Expected expiration date/time: {expireAt} with display on setting set to {keepDisplayOn}.");
|
||||
@@ -303,13 +387,38 @@ namespace Awake.Core
|
||||
IsDisplayOn = keepDisplayOn;
|
||||
CurrentOperatingMode = AwakeMode.EXPIRABLE;
|
||||
ExpireAt = expireAt;
|
||||
ModeStartedAt = DateTimeOffset.Now;
|
||||
|
||||
SetModeShellIcon();
|
||||
|
||||
TimeSpan remainingTime = expireAt - DateTimeOffset.Now;
|
||||
// Use a 1s interval that completes once the expiry time passes, rather than a single
|
||||
// Observable.Timer(remainingTime): a one-shot timer overflows for spans beyond ~49.7
|
||||
// days (System.Threading.Timer dueTime is capped at uint.MaxValue ms). This also keeps
|
||||
// the tray tooltip and flyout countdown ticking down.
|
||||
var targetExpiryTime = expireAt;
|
||||
|
||||
_timerSubscription = Observable.Timer(remainingTime).Subscribe(
|
||||
_ => HandleTimerCompletion("expirable"));
|
||||
_timerSubscription = Observable.Interval(TimeSpan.FromSeconds(1))
|
||||
.Select(_ => targetExpiryTime - DateTimeOffset.Now)
|
||||
.TakeWhile(remaining => remaining.TotalSeconds > 0)
|
||||
.Subscribe(
|
||||
remainingTimeSpan =>
|
||||
{
|
||||
TimeRemaining = (uint)remainingTimeSpan.TotalSeconds;
|
||||
|
||||
AwakeApp.Current?.UpdateTrayIcon(
|
||||
TrayIconService.TimedIcon,
|
||||
$"{Constants.FullAppName}\n{remainingTimeSpan.ToHumanReadableString()} {Resources.AWAKE_TRAY_REMAINING}\n{Resources.AWAKE_TRAY_DISPLAY}: {ScreenStateString}");
|
||||
|
||||
try
|
||||
{
|
||||
ModeChanged?.Invoke(null, EventArgs.Empty);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Awake ModeChanged subscriber threw: {ex.Message}");
|
||||
}
|
||||
},
|
||||
() => HandleTimerCompletion("expirable"));
|
||||
}
|
||||
|
||||
internal static void SetTimedKeepAwake(uint seconds, bool keepDisplayOn = true, [CallerMemberName] string callerName = "")
|
||||
@@ -365,6 +474,10 @@ namespace Awake.Core
|
||||
|
||||
var targetExpiryTime = DateTimeOffset.Now.AddSeconds(seconds);
|
||||
|
||||
// Expose the session bounds so the flyout can render a determinate countdown.
|
||||
ModeStartedAt = DateTimeOffset.Now;
|
||||
ExpireAt = targetExpiryTime;
|
||||
|
||||
_timerSubscription = Observable.Interval(TimeSpan.FromSeconds(1))
|
||||
.Select(_ => targetExpiryTime - DateTimeOffset.Now)
|
||||
.TakeWhile(remaining => remaining.TotalSeconds > 0)
|
||||
@@ -373,11 +486,18 @@ namespace Awake.Core
|
||||
{
|
||||
TimeRemaining = (uint)remainingTimeSpan.TotalSeconds;
|
||||
|
||||
TrayHelper.SetShellIcon(
|
||||
TrayHelper.WindowHandle,
|
||||
$"{Constants.FullAppName}\n{remainingTimeSpan.ToHumanReadableString()} {Resources.AWAKE_TRAY_REMAINING}\n{Resources.AWAKE_TRAY_DISPLAY}: {ScreenStateString}",
|
||||
TrayHelper.TimedIcon,
|
||||
TrayIconAction.Update);
|
||||
AwakeApp.Current?.UpdateTrayIcon(
|
||||
TrayIconService.TimedIcon,
|
||||
$"{Constants.FullAppName}\n{remainingTimeSpan.ToHumanReadableString()} {Resources.AWAKE_TRAY_REMAINING}\n{Resources.AWAKE_TRAY_DISPLAY}: {ScreenStateString}");
|
||||
|
||||
try
|
||||
{
|
||||
ModeChanged?.Invoke(null, EventArgs.Empty);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Awake ModeChanged subscriber threw: {ex.Message}");
|
||||
}
|
||||
},
|
||||
() => HandleTimerCompletion("timed"));
|
||||
}
|
||||
@@ -419,21 +539,17 @@ namespace Awake.Core
|
||||
_timerSubscription?.Dispose();
|
||||
_timerSubscription = null;
|
||||
|
||||
// Dispose tray icons
|
||||
TrayHelper.DisposeIcons();
|
||||
|
||||
if (TrayHelper.WindowHandle != IntPtr.Zero)
|
||||
// Shut down the WinUI app: this removes the tray icon and closes the hidden flyout
|
||||
// window so the message pump can exit cleanly.
|
||||
try
|
||||
{
|
||||
// Delete the icon.
|
||||
TrayHelper.SetShellIcon(TrayHelper.WindowHandle, string.Empty, null, TrayIconAction.Delete);
|
||||
|
||||
// Close the message window that we used for the tray.
|
||||
Bridge.SendMessage(TrayHelper.WindowHandle, Native.Constants.WM_CLOSE, 0, 0);
|
||||
|
||||
Bridge.DestroyWindow(TrayHelper.WindowHandle);
|
||||
AwakeApp.Current?.Shutdown();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to shut down AwakeApp cleanly: {ex.Message}");
|
||||
}
|
||||
|
||||
Bridge.PostQuitMessage(exitCode);
|
||||
Environment.Exit(exitCode);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +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;
|
||||
|
||||
namespace Awake.Core.Models
|
||||
{
|
||||
internal struct Msg
|
||||
{
|
||||
public IntPtr HWnd;
|
||||
public uint Message;
|
||||
public IntPtr WParam;
|
||||
public IntPtr LParam;
|
||||
public uint Time;
|
||||
public Point Pt;
|
||||
}
|
||||
}
|
||||
@@ -1,21 +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;
|
||||
|
||||
namespace Awake.Core.Models
|
||||
{
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct MenuInfo
|
||||
{
|
||||
public uint CbSize; // Size of the structure, in bytes
|
||||
public uint FMask; // Specifies which members of the structure are valid
|
||||
public uint DwStyle; // Style of the menu
|
||||
public uint CyMax; // Maximum height of the menu, in pixels
|
||||
public IntPtr HbrBack; // Handle to the brush used for the menu's background
|
||||
public uint DwContextHelpID; // Context help ID
|
||||
public IntPtr DwMenuData; // Pointer to the menu's user data
|
||||
}
|
||||
}
|
||||
@@ -1,22 +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;
|
||||
|
||||
namespace Awake.Core.Models
|
||||
{
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct NotifyIconData
|
||||
{
|
||||
public int CbSize;
|
||||
public IntPtr HWnd;
|
||||
public int UId;
|
||||
public int UFlags;
|
||||
public int UCallbackMessage;
|
||||
public IntPtr HIcon;
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
|
||||
public string SzTip;
|
||||
}
|
||||
}
|
||||
@@ -1,15 +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.Runtime.InteropServices;
|
||||
|
||||
namespace Awake.Core.Models
|
||||
{
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct Point
|
||||
{
|
||||
public int X;
|
||||
public int Y;
|
||||
}
|
||||
}
|
||||
26
src/modules/awake/Awake/Core/Models/RunningAppInfo.cs
Normal file
@@ -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 Microsoft.UI.Xaml.Media;
|
||||
|
||||
namespace Awake.Core.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// A running application with a visible window, surfaced in the flyout's "While app runs"
|
||||
/// picker. <see cref="IconBytes"/> is captured off the UI thread during enumeration; the
|
||||
/// XAML <see cref="Icon"/> is built from it on the UI thread before binding.
|
||||
/// </summary>
|
||||
public sealed class RunningAppInfo
|
||||
{
|
||||
public int ProcessId { get; init; }
|
||||
|
||||
public string DisplayName { get; init; } = string.Empty;
|
||||
|
||||
public string WindowTitle { get; init; } = string.Empty;
|
||||
|
||||
public byte[]? IconBytes { get; init; }
|
||||
|
||||
public ImageSource? Icon { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,16 +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.
|
||||
|
||||
namespace Awake.Core.Models
|
||||
{
|
||||
internal enum TrayCommands : uint
|
||||
{
|
||||
TC_DISPLAY_SETTING = Native.Constants.WM_USER + 0x2,
|
||||
TC_MODE_PASSIVE = Native.Constants.WM_USER + 0x3,
|
||||
TC_MODE_INDEFINITE = Native.Constants.WM_USER + 0x4,
|
||||
TC_MODE_EXPIRABLE = Native.Constants.WM_USER + 0x5,
|
||||
TC_EXIT = Native.Constants.WM_USER + 0x64,
|
||||
TC_TIME = Native.Constants.WM_USER + 0x65,
|
||||
}
|
||||
}
|
||||
@@ -1,13 +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.
|
||||
|
||||
namespace Awake.Core.Models
|
||||
{
|
||||
internal enum TrayIconAction
|
||||
{
|
||||
Add,
|
||||
Update,
|
||||
Delete,
|
||||
}
|
||||
}
|
||||
@@ -1,26 +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;
|
||||
|
||||
namespace Awake.Core.Models
|
||||
{
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
||||
internal struct WndClassEx
|
||||
{
|
||||
public uint CbSize;
|
||||
public uint Style;
|
||||
public IntPtr LpfnWndProc;
|
||||
public int CbClsExtra;
|
||||
public int CbWndExtra;
|
||||
public IntPtr HInstance;
|
||||
public IntPtr HIcon;
|
||||
public IntPtr HCursor;
|
||||
public IntPtr HbrBackground;
|
||||
public string LpszMenuName;
|
||||
public string LpszClassName;
|
||||
public IntPtr HIconSm;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// 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.
|
||||
|
||||
@@ -10,11 +10,14 @@ using Awake.Core.Models;
|
||||
|
||||
namespace Awake.Core.Native
|
||||
{
|
||||
/// <summary>
|
||||
/// P/Invokes used by the headless Awake core (console attach, power capability query,
|
||||
/// thread execution state, parent-PID lookup). Tray-icon and HMENU P/Invokes were moved
|
||||
/// to <see cref="TrayIconService"/> (CsWin32-generated) when the WinUI flyout replaced
|
||||
/// the legacy popup menu.
|
||||
/// </summary>
|
||||
internal sealed class Bridge
|
||||
{
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi, SetLastError = true)]
|
||||
internal delegate int WndProcDelegate(IntPtr hWnd, uint message, IntPtr wParam, IntPtr lParam);
|
||||
|
||||
[DllImport("Powrprof.dll", SetLastError = true)]
|
||||
internal static extern bool GetPwrCapabilities(out SystemPowerCapabilities lpSystemPowerCapabilities);
|
||||
|
||||
@@ -48,70 +51,7 @@ namespace Awake.Core.Native
|
||||
[MarshalAs(UnmanagedType.U4)] FileAttributes flagsAndAttributes,
|
||||
IntPtr templateFile);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
internal static extern IntPtr CreatePopupMenu();
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
internal static extern bool InsertMenu(IntPtr hMenu, uint uPosition, uint uFlags, uint uIDNewItem, string lpNewItem);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
public static extern bool TrackPopupMenuEx(IntPtr hMenu, uint uFlags, int x, int y, IntPtr hWnd, IntPtr lptpm);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
internal static extern IntPtr SendMessage(IntPtr hWnd, uint msg, nuint wParam, nint lParam);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static extern bool DestroyMenu(IntPtr hMenu);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
internal static extern bool DestroyWindow(IntPtr hWnd);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
internal static extern void PostQuitMessage(int nExitCode);
|
||||
|
||||
[DllImport("shell32.dll", SetLastError = true)]
|
||||
internal static extern bool Shell_NotifyIcon(int dwMessage, ref NotifyIconData pnid);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
internal static extern bool TranslateMessage(ref Msg lpMsg);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
internal static extern IntPtr DispatchMessage(ref Msg lpMsg);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
internal static extern IntPtr RegisterClassEx(ref WndClassEx lpwcx);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
internal static extern IntPtr CreateWindowEx(uint dwExStyle, string lpClassName, string lpWindowName, uint dwStyle, int x, int y, int nWidth, int nHeight, IntPtr hWndParent, IntPtr hMenu, IntPtr hInstance, IntPtr lpParam);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
internal static extern int DefWindowProc(IntPtr hWnd, uint message, IntPtr wParam, IntPtr lParam);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
internal static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static extern bool GetCursorPos(out Point lpPoint);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
internal static extern bool GetMessage(out Msg lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
internal static extern bool UpdateWindow(IntPtr hWnd);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static extern bool SetMenuInfo(IntPtr hMenu, ref MenuInfo lpcmi);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
internal static extern bool SetForegroundWindow(IntPtr hWnd);
|
||||
|
||||
[DllImport("ntdll.dll")]
|
||||
internal static extern int NtQueryInformationProcess(IntPtr processHandle, int processInformationClass, ref ProcessBasicInformation processInformation, int processInformationLength, out int returnLength);
|
||||
|
||||
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
|
||||
internal static extern int RegisterWindowMessage(string lpString);
|
||||
}
|
||||
}
|
||||
|
||||
187
src/modules/awake/Awake/Core/RunningAppsProvider.cs
Normal file
@@ -0,0 +1,187 @@
|
||||
// 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.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Imaging;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Awake.Core.Models;
|
||||
using ManagedCommon;
|
||||
using Microsoft.UI.Xaml.Media.Imaging;
|
||||
|
||||
namespace Awake.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Enumerates running applications for the flyout's "While app runs" picker. The default list
|
||||
/// (<see cref="GetRunningAppsAsync"/>) is limited to apps with a visible main window; searching
|
||||
/// widens the net to all processes (<see cref="GetAllProcessesAsync"/>). Enumeration and icon
|
||||
/// extraction are intended to run off the UI thread; the per-item XAML icon is built from the
|
||||
/// captured PNG bytes on the UI thread via <see cref="BuildIconAsync"/>.
|
||||
/// </summary>
|
||||
internal static class RunningAppsProvider
|
||||
{
|
||||
internal static Task<List<RunningAppInfo>> GetRunningAppsAsync() => Task.Run(() => GetRunningApps(windowedOnly: true));
|
||||
|
||||
internal static Task<List<RunningAppInfo>> GetAllProcessesAsync() => Task.Run(() => GetRunningApps(windowedOnly: false));
|
||||
|
||||
private static List<RunningAppInfo> GetRunningApps(bool windowedOnly)
|
||||
{
|
||||
var apps = new List<RunningAppInfo>();
|
||||
var seen = new HashSet<int>();
|
||||
var iconCache = new Dictionary<string, byte[]?>(StringComparer.OrdinalIgnoreCase);
|
||||
int ownPid = Environment.ProcessId;
|
||||
|
||||
foreach (Process process in Process.GetProcesses())
|
||||
{
|
||||
try
|
||||
{
|
||||
string title = process.MainWindowTitle;
|
||||
bool hasWindow = process.MainWindowHandle != IntPtr.Zero && !string.IsNullOrWhiteSpace(title);
|
||||
|
||||
// The default list only surfaces user-facing apps (a real main window with a
|
||||
// non-empty title); search widens to every accessible process.
|
||||
if (windowedOnly && !hasWindow)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (process.Id == ownPid || !seen.Add(process.Id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string executablePath = TryGetExecutablePath(process);
|
||||
|
||||
apps.Add(new RunningAppInfo
|
||||
{
|
||||
ProcessId = process.Id,
|
||||
DisplayName = GetFriendlyName(process, executablePath),
|
||||
WindowTitle = hasWindow ? title : string.Empty,
|
||||
IconBytes = GetIconPngCached(executablePath, iconCache),
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Inaccessible/elevated/system processes throw on property access; skip them.
|
||||
Logger.LogInfo($"Skipping process during enumeration: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
process.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
return apps
|
||||
.OrderBy(a => a.DisplayName, StringComparer.CurrentCultureIgnoreCase)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static string TryGetExecutablePath(Process process)
|
||||
{
|
||||
try
|
||||
{
|
||||
return process.MainModule?.FileName ?? string.Empty;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// MainModule throws for processes we cannot fully open; no path available.
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetFriendlyName(Process process, string executablePath)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(executablePath))
|
||||
{
|
||||
try
|
||||
{
|
||||
string description = FileVersionInfo.GetVersionInfo(executablePath).FileDescription ?? string.Empty;
|
||||
if (!string.IsNullOrWhiteSpace(description))
|
||||
{
|
||||
return description;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Fall through to the process name.
|
||||
}
|
||||
}
|
||||
|
||||
return process.ProcessName;
|
||||
}
|
||||
|
||||
private static byte[]? GetIconPngCached(string executablePath, Dictionary<string, byte[]?> cache)
|
||||
{
|
||||
if (string.IsNullOrEmpty(executablePath))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (cache.TryGetValue(executablePath, out byte[]? cached))
|
||||
{
|
||||
return cached;
|
||||
}
|
||||
|
||||
byte[]? png = TryGetIconPng(executablePath);
|
||||
cache[executablePath] = png;
|
||||
return png;
|
||||
}
|
||||
|
||||
private static byte[]? TryGetIconPng(string executablePath)
|
||||
{
|
||||
if (string.IsNullOrEmpty(executablePath))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using Icon? icon = Icon.ExtractAssociatedIcon(executablePath);
|
||||
if (icon == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
using Bitmap bitmap = icon.ToBitmap();
|
||||
using var stream = new MemoryStream();
|
||||
bitmap.Save(stream, ImageFormat.Png);
|
||||
return stream.ToArray();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a XAML <see cref="BitmapImage"/> from captured PNG bytes. Must be called on the
|
||||
/// UI thread (creates a XAML object).
|
||||
/// </summary>
|
||||
internal static async Task<BitmapImage?> BuildIconAsync(byte[]? iconBytes)
|
||||
{
|
||||
if (iconBytes == null || iconBytes.Length == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var image = new BitmapImage();
|
||||
using var stream = new MemoryStream(iconBytes);
|
||||
await image.SetSourceAsync(stream.AsRandomAccessStream());
|
||||
return image;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogInfo($"Failed to build app icon: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,522 +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.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Awake.Core.Models;
|
||||
using Awake.Core.Native;
|
||||
using Awake.Core.Threading;
|
||||
using Awake.Properties;
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
|
||||
namespace Awake.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Helper class used to manage the system tray.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Because Awake is a console application, there is no built-in
|
||||
/// way to embed UI components so we have to heavily rely on the native Windows API.
|
||||
/// </remarks>
|
||||
internal static class TrayHelper
|
||||
{
|
||||
private static NotifyIconData _notifyIconData;
|
||||
private static SingleThreadSynchronizationContext? _syncContext;
|
||||
private static Thread? _mainThread;
|
||||
private static uint _taskbarCreatedMessage;
|
||||
|
||||
private static IntPtr TrayMenu { get; set; }
|
||||
|
||||
internal static IntPtr WindowHandle { get; private set; }
|
||||
|
||||
internal static readonly Icon DefaultAwakeIcon = new(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Assets/Awake/awake.ico"));
|
||||
internal static readonly Icon TimedIcon = new(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Assets/Awake/timed.ico"));
|
||||
internal static readonly Icon ExpirableIcon = new(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Assets/Awake/expirable.ico"));
|
||||
internal static readonly Icon IndefiniteIcon = new(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Assets/Awake/indefinite.ico"));
|
||||
internal static readonly Icon DisabledIcon = new(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Assets/Awake/disabled.ico"));
|
||||
|
||||
private const int TrayIconId = 1000;
|
||||
|
||||
static TrayHelper()
|
||||
{
|
||||
TrayMenu = IntPtr.Zero;
|
||||
WindowHandle = IntPtr.Zero;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes of all icon resources to prevent GDI handle leaks.
|
||||
/// </summary>
|
||||
internal static void DisposeIcons()
|
||||
{
|
||||
DefaultAwakeIcon?.Dispose();
|
||||
TimedIcon?.Dispose();
|
||||
ExpirableIcon?.Dispose();
|
||||
IndefiniteIcon?.Dispose();
|
||||
DisabledIcon?.Dispose();
|
||||
}
|
||||
|
||||
private static void ShowContextMenu(IntPtr hWnd)
|
||||
{
|
||||
if (TrayMenu == IntPtr.Zero)
|
||||
{
|
||||
Logger.LogError("Tried to create a context menu while the TrayMenu object is a null pointer. Normal when used in standalone mode.");
|
||||
return;
|
||||
}
|
||||
|
||||
Bridge.SetForegroundWindow(hWnd);
|
||||
|
||||
// Get cursor position in screen coordinates
|
||||
Bridge.GetCursorPos(out Models.Point cursorPos);
|
||||
|
||||
// Set menu information
|
||||
MenuInfo menuInfo = new()
|
||||
{
|
||||
CbSize = (uint)Marshal.SizeOf<MenuInfo>(),
|
||||
FMask = Native.Constants.MIM_STYLE,
|
||||
DwStyle = Native.Constants.MNS_AUTO_DISMISS,
|
||||
};
|
||||
Bridge.SetMenuInfo(TrayMenu, ref menuInfo);
|
||||
|
||||
// Display the context menu at the cursor position
|
||||
Bridge.TrackPopupMenuEx(
|
||||
TrayMenu,
|
||||
Native.Constants.TPM_LEFT_ALIGN | Native.Constants.TPM_BOTTOMALIGN | Native.Constants.TPM_LEFT_BUTTON,
|
||||
cursorPos.X,
|
||||
cursorPos.Y,
|
||||
hWnd,
|
||||
IntPtr.Zero);
|
||||
}
|
||||
|
||||
public static Task InitializeTray(Icon icon, string text)
|
||||
{
|
||||
TaskCompletionSource<bool> trayInitialized = new();
|
||||
|
||||
IntPtr hWnd = IntPtr.Zero;
|
||||
|
||||
// Start the message loop asynchronously
|
||||
_mainThread = new Thread(() =>
|
||||
{
|
||||
_syncContext = new SingleThreadSynchronizationContext();
|
||||
SynchronizationContext.SetSynchronizationContext(_syncContext);
|
||||
|
||||
RunOnMainThread(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
WndClassEx wcex = new()
|
||||
{
|
||||
CbSize = (uint)Marshal.SizeOf<WndClassEx>(),
|
||||
Style = 0,
|
||||
LpfnWndProc = Marshal.GetFunctionPointerForDelegate<Bridge.WndProcDelegate>(WndProc),
|
||||
CbClsExtra = 0,
|
||||
CbWndExtra = 0,
|
||||
HInstance = Marshal.GetHINSTANCE(typeof(Program).Module),
|
||||
HIcon = IntPtr.Zero,
|
||||
HCursor = IntPtr.Zero,
|
||||
HbrBackground = IntPtr.Zero,
|
||||
LpszMenuName = string.Empty,
|
||||
LpszClassName = Constants.TrayWindowId,
|
||||
HIconSm = IntPtr.Zero,
|
||||
};
|
||||
|
||||
Bridge.RegisterClassEx(ref wcex);
|
||||
|
||||
hWnd = Bridge.CreateWindowEx(
|
||||
0,
|
||||
Constants.TrayWindowId,
|
||||
text,
|
||||
0x00CF0000 | 0x00000001 | 0x00000008, // WS_OVERLAPPEDWINDOW | WS_VISIBLE | WS_MINIMIZEBOX
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
IntPtr.Zero,
|
||||
IntPtr.Zero,
|
||||
Marshal.GetHINSTANCE(typeof(Program).Module),
|
||||
IntPtr.Zero);
|
||||
|
||||
if (hWnd == IntPtr.Zero)
|
||||
{
|
||||
int errorCode = Marshal.GetLastWin32Error();
|
||||
throw new Win32Exception(errorCode, "Failed to add tray icon. Error code: " + errorCode);
|
||||
}
|
||||
|
||||
// Keep this as a reference because we will need it when we update
|
||||
// the tray icon in the future.
|
||||
WindowHandle = hWnd;
|
||||
|
||||
Bridge.ShowWindow(hWnd, 0); // SW_HIDE
|
||||
Bridge.UpdateWindow(hWnd);
|
||||
Logger.LogInfo($"Created HWND for the window: {hWnd}");
|
||||
|
||||
SetShellIcon(hWnd, text, icon);
|
||||
|
||||
trayInitialized.SetResult(true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to properly initialize the tray. {ex.Message}");
|
||||
trayInitialized.SetException(ex);
|
||||
}
|
||||
});
|
||||
|
||||
RunOnMainThread(() =>
|
||||
{
|
||||
RunMessageLoop();
|
||||
});
|
||||
|
||||
_syncContext!.BeginMessageLoop();
|
||||
});
|
||||
|
||||
_mainThread.IsBackground = true;
|
||||
_mainThread.Start();
|
||||
|
||||
return trayInitialized.Task;
|
||||
}
|
||||
|
||||
internal static void SetShellIcon(IntPtr hWnd, string text, Icon? icon, TrayIconAction action = TrayIconAction.Add, [CallerMemberName] string callerName = "")
|
||||
{
|
||||
// For Delete operations, we don't need an icon - only hWnd is required
|
||||
// For Add/Update operations, we need both hWnd and icon
|
||||
bool canProceed = hWnd != IntPtr.Zero && (action == TrayIconAction.Delete || icon != null);
|
||||
|
||||
if (canProceed)
|
||||
{
|
||||
int message = Native.Constants.NIM_ADD;
|
||||
|
||||
switch (action)
|
||||
{
|
||||
case TrayIconAction.Update:
|
||||
message = Native.Constants.NIM_MODIFY;
|
||||
break;
|
||||
case TrayIconAction.Delete:
|
||||
message = Native.Constants.NIM_DELETE;
|
||||
break;
|
||||
case TrayIconAction.Add:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (action is TrayIconAction.Add or TrayIconAction.Update)
|
||||
{
|
||||
_notifyIconData = new NotifyIconData
|
||||
{
|
||||
CbSize = Marshal.SizeOf<NotifyIconData>(),
|
||||
HWnd = hWnd,
|
||||
UId = TrayIconId,
|
||||
UFlags = Native.Constants.NIF_ICON | Native.Constants.NIF_TIP | Native.Constants.NIF_MESSAGE,
|
||||
UCallbackMessage = (int)Native.Constants.WM_USER,
|
||||
HIcon = icon?.Handle ?? IntPtr.Zero,
|
||||
SzTip = text,
|
||||
};
|
||||
}
|
||||
else if (action == TrayIconAction.Delete)
|
||||
{
|
||||
_notifyIconData = new NotifyIconData
|
||||
{
|
||||
CbSize = Marshal.SizeOf<NotifyIconData>(),
|
||||
HWnd = hWnd,
|
||||
UId = TrayIconId,
|
||||
UFlags = 0,
|
||||
};
|
||||
}
|
||||
|
||||
// Retry configuration based on action type
|
||||
// Add operations need longer delays as Explorer may still be initializing after Windows updates
|
||||
int maxRetryAttempts;
|
||||
int baseDelayMs;
|
||||
|
||||
if (action == TrayIconAction.Add)
|
||||
{
|
||||
maxRetryAttempts = 10;
|
||||
baseDelayMs = 500; // 500, 1000, 2000, 2000, 2000... (capped)
|
||||
}
|
||||
else
|
||||
{
|
||||
maxRetryAttempts = 3;
|
||||
baseDelayMs = 100; // 100, 200, 400 (existing behavior)
|
||||
}
|
||||
|
||||
const int maxDelayMs = 2000; // Cap delay at 2 seconds
|
||||
|
||||
for (int attempt = 1; attempt <= maxRetryAttempts; attempt++)
|
||||
{
|
||||
if (Bridge.Shell_NotifyIcon(message, ref _notifyIconData))
|
||||
{
|
||||
if (attempt > 1)
|
||||
{
|
||||
Logger.LogInfo($"Successfully set shell icon on attempt {attempt}. Action: {action}");
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
int errorCode = Marshal.GetLastWin32Error();
|
||||
Logger.LogInfo($"Could not set the shell icon. Action: {action}, error code: {errorCode}, attempt: {attempt}/{maxRetryAttempts}. HIcon handle is {icon?.Handle} and HWnd is {hWnd}. Invoked by {callerName}.");
|
||||
|
||||
if (attempt == maxRetryAttempts)
|
||||
{
|
||||
Logger.LogError($"Failed to change tray icon after {maxRetryAttempts} attempts. Action: {action} and error code: {errorCode}. Invoked by {callerName}.");
|
||||
break;
|
||||
}
|
||||
|
||||
// Exponential backoff with cap
|
||||
int delayMs = Math.Min(baseDelayMs * (1 << (attempt - 1)), maxDelayMs);
|
||||
Thread.Sleep(delayMs);
|
||||
}
|
||||
}
|
||||
|
||||
if (action == TrayIconAction.Delete)
|
||||
{
|
||||
_notifyIconData = default;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogInfo($"Cannot set the shell icon - parent window handle is zero{(action != TrayIconAction.Delete && icon == null ? " or icon is not available" : string.Empty)}. Text: {text} Action: {action}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void RunMessageLoop()
|
||||
{
|
||||
while (Bridge.GetMessage(out Msg msg, IntPtr.Zero, 0, 0))
|
||||
{
|
||||
Bridge.TranslateMessage(ref msg);
|
||||
Bridge.DispatchMessage(ref msg);
|
||||
}
|
||||
|
||||
Logger.LogInfo("Message loop terminated.");
|
||||
}
|
||||
|
||||
private static int WndProc(IntPtr hWnd, uint message, IntPtr wParam, IntPtr lParam)
|
||||
{
|
||||
switch (message)
|
||||
{
|
||||
case Native.Constants.WM_USER:
|
||||
if (lParam is Native.Constants.WM_LBUTTONDOWN or Native.Constants.WM_RBUTTONDOWN)
|
||||
{
|
||||
// Show the context menu associated with the tray icon
|
||||
ShowContextMenu(hWnd);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case Native.Constants.WM_CREATE:
|
||||
{
|
||||
_taskbarCreatedMessage = (uint)Bridge.RegisterWindowMessage("TaskbarCreated");
|
||||
}
|
||||
|
||||
break;
|
||||
case Native.Constants.WM_DESTROY:
|
||||
// Clean up resources when the window is destroyed
|
||||
Bridge.PostQuitMessage(0);
|
||||
break;
|
||||
case Native.Constants.WM_COMMAND:
|
||||
long targetCommandValue = wParam.ToInt64() & 0xFFFF;
|
||||
|
||||
switch (targetCommandValue)
|
||||
{
|
||||
case (uint)TrayCommands.TC_EXIT:
|
||||
{
|
||||
Manager.CompleteExit(Environment.ExitCode);
|
||||
break;
|
||||
}
|
||||
|
||||
case (uint)TrayCommands.TC_DISPLAY_SETTING:
|
||||
{
|
||||
Manager.SetDisplay();
|
||||
break;
|
||||
}
|
||||
|
||||
case (uint)TrayCommands.TC_MODE_INDEFINITE:
|
||||
{
|
||||
AwakeSettings settings = Manager.ModuleSettings!.GetSettings<AwakeSettings>(Constants.AppName) ?? new AwakeSettings();
|
||||
Manager.SetIndefiniteKeepAwake(keepDisplayOn: settings.Properties.KeepDisplayOn);
|
||||
break;
|
||||
}
|
||||
|
||||
case (uint)TrayCommands.TC_MODE_PASSIVE:
|
||||
{
|
||||
Manager.SetPassiveKeepAwake();
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
// Custom tray time commands start at TC_TIME and increment by 1 for each entry.
|
||||
// Check if this command falls within the custom time range.
|
||||
if (targetCommandValue >= (uint)TrayCommands.TC_TIME)
|
||||
{
|
||||
AwakeSettings settings = Manager.ModuleSettings!.GetSettings<AwakeSettings>(Constants.AppName) ?? new AwakeSettings();
|
||||
if (settings.Properties.CustomTrayTimes.Count == 0)
|
||||
{
|
||||
settings.Properties.CustomTrayTimes.AddRange(Manager.GetDefaultTrayOptions());
|
||||
}
|
||||
|
||||
int index = (int)targetCommandValue - (int)TrayCommands.TC_TIME;
|
||||
|
||||
if (index >= 0 && index < settings.Properties.CustomTrayTimes.Count)
|
||||
{
|
||||
uint targetTime = settings.Properties.CustomTrayTimes.Values.Skip(index).First();
|
||||
Manager.SetTimedKeepAwake(targetTime, keepDisplayOn: settings.Properties.KeepDisplayOn);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogError($"Custom tray time index {index} is out of range. Available entries: {settings.Properties.CustomTrayTimes.Count}");
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
case Native.Constants.WM_POWERBROADCAST:
|
||||
int eventType = wParam.ToInt32();
|
||||
if (eventType == Native.Constants.PBT_APMRESUMEAUTOMATIC ||
|
||||
eventType == Native.Constants.PBT_APMRESUMESUSPEND ||
|
||||
eventType == Native.Constants.PBT_APMPOWERSTATUSCHANGE)
|
||||
{
|
||||
Manager.ReapplyAwakeState();
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
if (message == _taskbarCreatedMessage)
|
||||
{
|
||||
Logger.LogInfo("Taskbar re-created");
|
||||
Manager.SetModeShellIcon(forceAdd: true);
|
||||
}
|
||||
|
||||
// Let the default window procedure handle other messages
|
||||
return Bridge.DefWindowProc(hWnd, message, wParam, lParam);
|
||||
}
|
||||
|
||||
return Bridge.DefWindowProc(hWnd, message, wParam, lParam);
|
||||
}
|
||||
|
||||
internal static void RunOnMainThread(Action action)
|
||||
{
|
||||
_syncContext!.Post(
|
||||
_ =>
|
||||
{
|
||||
try
|
||||
{
|
||||
Logger.LogInfo($"Thread execution is on: {Environment.CurrentManagedThreadId}");
|
||||
action();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.LogError($"Error in tray thread execution: {e.Message}");
|
||||
}
|
||||
},
|
||||
null);
|
||||
}
|
||||
|
||||
internal static void SetTray(AwakeSettings settings, bool startedFromPowerToys)
|
||||
{
|
||||
SetTray(
|
||||
settings.Properties.KeepDisplayOn,
|
||||
settings.Properties.Mode,
|
||||
settings.Properties.CustomTrayTimes,
|
||||
startedFromPowerToys);
|
||||
}
|
||||
|
||||
public static void SetTray(bool keepDisplayOn, AwakeMode mode, Dictionary<string, uint> trayTimeShortcuts, bool startedFromPowerToys)
|
||||
{
|
||||
ClearExistingTrayMenu();
|
||||
CreateNewTrayMenu(startedFromPowerToys, keepDisplayOn, mode);
|
||||
|
||||
InsertAwakeModeMenuItems(mode);
|
||||
|
||||
EnsureDefaultTrayTimeShortcuts(trayTimeShortcuts);
|
||||
CreateAwakeTimeSubMenu(trayTimeShortcuts, mode == AwakeMode.TIMED);
|
||||
}
|
||||
|
||||
private static void ClearExistingTrayMenu()
|
||||
{
|
||||
if (TrayMenu != IntPtr.Zero && !Bridge.DestroyMenu(TrayMenu))
|
||||
{
|
||||
int errorCode = Marshal.GetLastWin32Error();
|
||||
Logger.LogError($"Failed to destroy menu: {errorCode}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void CreateNewTrayMenu(bool startedFromPowerToys, bool keepDisplayOn, AwakeMode mode)
|
||||
{
|
||||
TrayMenu = Bridge.CreatePopupMenu();
|
||||
|
||||
if (TrayMenu == IntPtr.Zero)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!startedFromPowerToys)
|
||||
{
|
||||
InsertMenuItem(0, TrayCommands.TC_EXIT, Resources.AWAKE_EXIT);
|
||||
}
|
||||
|
||||
InsertMenuItem(0, TrayCommands.TC_DISPLAY_SETTING, Resources.AWAKE_KEEP_SCREEN_ON, keepDisplayOn, mode == AwakeMode.PASSIVE);
|
||||
|
||||
if (!startedFromPowerToys)
|
||||
{
|
||||
InsertSeparator(1);
|
||||
}
|
||||
}
|
||||
|
||||
private static void InsertMenuItem(int position, TrayCommands command, string text, bool checkedState = false, bool disabled = false)
|
||||
{
|
||||
uint state = Native.Constants.MF_BYPOSITION | Native.Constants.MF_STRING;
|
||||
state |= checkedState ? Native.Constants.MF_CHECKED : Native.Constants.MF_UNCHECKED;
|
||||
state |= disabled ? Native.Constants.MF_DISABLED : Native.Constants.MF_ENABLED;
|
||||
|
||||
Bridge.InsertMenu(TrayMenu, (uint)position, state, (uint)command, text);
|
||||
}
|
||||
|
||||
private static void InsertSeparator(int position)
|
||||
{
|
||||
Bridge.InsertMenu(TrayMenu, (uint)position, Native.Constants.MF_BYPOSITION | Native.Constants.MF_SEPARATOR, 0, string.Empty);
|
||||
}
|
||||
|
||||
private static void EnsureDefaultTrayTimeShortcuts(Dictionary<string, uint> trayTimeShortcuts)
|
||||
{
|
||||
if (trayTimeShortcuts.Count == 0)
|
||||
{
|
||||
trayTimeShortcuts.AddRange(Manager.GetDefaultTrayOptions());
|
||||
}
|
||||
}
|
||||
|
||||
private static void CreateAwakeTimeSubMenu(Dictionary<string, uint> trayTimeShortcuts, bool isChecked = false)
|
||||
{
|
||||
nint awakeTimeMenu = Bridge.CreatePopupMenu();
|
||||
int i = 0;
|
||||
foreach (var shortcut in trayTimeShortcuts)
|
||||
{
|
||||
Bridge.InsertMenu(awakeTimeMenu, (uint)i, Native.Constants.MF_BYPOSITION | Native.Constants.MF_STRING, (uint)TrayCommands.TC_TIME + (uint)i, shortcut.Key);
|
||||
i++;
|
||||
}
|
||||
|
||||
Bridge.InsertMenu(TrayMenu, 0, Native.Constants.MF_BYPOSITION | Native.Constants.MF_POPUP | (isChecked ? Native.Constants.MF_CHECKED : Native.Constants.MF_UNCHECKED), (uint)awakeTimeMenu, Resources.AWAKE_KEEP_ON_INTERVAL);
|
||||
}
|
||||
|
||||
private static void InsertAwakeModeMenuItems(AwakeMode mode)
|
||||
{
|
||||
InsertSeparator(0);
|
||||
|
||||
InsertMenuItem(0, TrayCommands.TC_MODE_PASSIVE, Resources.AWAKE_OFF, mode == AwakeMode.PASSIVE);
|
||||
InsertMenuItem(0, TrayCommands.TC_MODE_INDEFINITE, Resources.AWAKE_KEEP_INDEFINITELY, mode == AwakeMode.INDEFINITE);
|
||||
InsertMenuItem(0, TrayCommands.TC_MODE_EXPIRABLE, Resources.AWAKE_KEEP_UNTIL_EXPIRATION, mode == AwakeMode.EXPIRABLE, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
212
src/modules/awake/Awake/Core/TrayIconService.cs
Normal file
@@ -0,0 +1,212 @@
|
||||
// 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.Diagnostics.CodeAnalysis;
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using ManagedCommon;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Windows.Win32;
|
||||
using Windows.Win32.Foundation;
|
||||
using Windows.Win32.UI.Shell;
|
||||
using Windows.Win32.UI.WindowsAndMessaging;
|
||||
using WinRT.Interop;
|
||||
|
||||
namespace Awake.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Window-procedure delegate exposed via primitive types so it can interop with
|
||||
/// CsWin32's SetWindowLongPtr without accessibility issues.
|
||||
/// </summary>
|
||||
/// <param name="hwnd">Handle to the window.</param>
|
||||
/// <param name="msg">The message identifier.</param>
|
||||
/// <param name="wParam">Additional message information.</param>
|
||||
/// <param name="lParam">Additional message information.</param>
|
||||
/// <returns>The result of the message processing.</returns>
|
||||
internal delegate nint AwakeTrayWndProcDelegate(nint hwnd, uint msg, nuint wParam, nint lParam);
|
||||
|
||||
/// <summary>
|
||||
/// Owns the Awake notification-area icon. Mirrors PowerDisplay's TrayIconService:
|
||||
/// a hidden helper Window receives Shell_NotifyIcon callbacks; both left- and
|
||||
/// right-clicks toggle the WinUI flyout. The legacy HMENU is gone.
|
||||
/// </summary>
|
||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.PublicProperties)]
|
||||
internal sealed partial class TrayIconService
|
||||
{
|
||||
private const uint MyNotifyId = 1000;
|
||||
private const uint WmTrayIcon = PInvoke.WM_USER + 1;
|
||||
|
||||
private readonly Action _toggleWindow;
|
||||
private readonly uint _wmTaskbarRestart;
|
||||
|
||||
private Window? _window;
|
||||
private nint _hwnd;
|
||||
private nint _originalWndProc;
|
||||
private AwakeTrayWndProcDelegate? _trayWndProc;
|
||||
private NOTIFYICONDATAW? _trayIconData;
|
||||
private string _currentTooltip = string.Empty;
|
||||
private nint _currentIconHandle;
|
||||
|
||||
public static readonly Icon DefaultIcon = new(Path.Combine(AppContext.BaseDirectory, "Assets", "Awake", "Awake.ico"));
|
||||
public static readonly Icon TimedIcon = new(Path.Combine(AppContext.BaseDirectory, "Assets", "Awake", "timed.ico"));
|
||||
public static readonly Icon ExpirableIcon = new(Path.Combine(AppContext.BaseDirectory, "Assets", "Awake", "expirable.ico"));
|
||||
public static readonly Icon IndefiniteIcon = new(Path.Combine(AppContext.BaseDirectory, "Assets", "Awake", "indefinite.ico"));
|
||||
public static readonly Icon DisabledIcon = new(Path.Combine(AppContext.BaseDirectory, "Assets", "Awake", "disabled.ico"));
|
||||
|
||||
public TrayIconService(Action toggleWindow)
|
||||
{
|
||||
_toggleWindow = toggleWindow ?? throw new ArgumentNullException(nameof(toggleWindow));
|
||||
_wmTaskbarRestart = RegisterWindowMessageNative("TaskbarCreated");
|
||||
}
|
||||
|
||||
public void SetupTrayIcon(string tooltip, Icon icon)
|
||||
{
|
||||
if (_window is null)
|
||||
{
|
||||
_window = new Window();
|
||||
_hwnd = WindowNative.GetWindowHandle(_window);
|
||||
|
||||
// LOAD BEARING: store the delegate in a field so the marshaled pointer
|
||||
// we hand to SetWindowLongPtr survives past this stack frame.
|
||||
_trayWndProc = WindowProc;
|
||||
var trayWndProcPointer = Marshal.GetFunctionPointerForDelegate(_trayWndProc);
|
||||
_originalWndProc = SetWindowLongPtrNative(_hwnd, GwlWndproc, trayWndProcPointer);
|
||||
}
|
||||
|
||||
_currentTooltip = tooltip;
|
||||
_currentIconHandle = icon.Handle;
|
||||
|
||||
if (!CreateOrUpdateTrayIcon(isAdd: true))
|
||||
{
|
||||
// Shell can refuse NIM_ADD during explorer startup; we'll retry from WM_WINDOWPOSCHANGING / TaskbarCreated.
|
||||
Logger.LogWarning("[Awake] Shell_NotifyIcon(NIM_ADD) failed; will retry when shell is ready");
|
||||
_trayIconData = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateIcon(Icon icon, string tooltip)
|
||||
{
|
||||
_currentIconHandle = icon.Handle;
|
||||
_currentTooltip = tooltip;
|
||||
|
||||
if (_trayIconData is null)
|
||||
{
|
||||
// No icon registered yet; try to add it now.
|
||||
CreateOrUpdateTrayIcon(isAdd: true);
|
||||
return;
|
||||
}
|
||||
|
||||
CreateOrUpdateTrayIcon(isAdd: false);
|
||||
}
|
||||
|
||||
public void Destroy()
|
||||
{
|
||||
if (_trayIconData is not null)
|
||||
{
|
||||
var d = (NOTIFYICONDATAW)_trayIconData;
|
||||
unsafe
|
||||
{
|
||||
if (Shell_NotifyIconNative((uint)NOTIFY_ICON_MESSAGE.NIM_DELETE, &d))
|
||||
{
|
||||
_trayIconData = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_window is not null)
|
||||
{
|
||||
_window.Close();
|
||||
_window = null;
|
||||
_hwnd = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private bool CreateOrUpdateTrayIcon(bool isAdd)
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
var data = new NOTIFYICONDATAW
|
||||
{
|
||||
cbSize = (uint)sizeof(NOTIFYICONDATAW),
|
||||
hWnd = new HWND(_hwnd),
|
||||
uID = MyNotifyId,
|
||||
uFlags = NOTIFY_ICON_DATA_FLAGS.NIF_MESSAGE | NOTIFY_ICON_DATA_FLAGS.NIF_ICON | NOTIFY_ICON_DATA_FLAGS.NIF_TIP,
|
||||
uCallbackMessage = WmTrayIcon,
|
||||
hIcon = new HICON(_currentIconHandle),
|
||||
szTip = _currentTooltip ?? string.Empty,
|
||||
};
|
||||
|
||||
bool success = Shell_NotifyIconNative(
|
||||
isAdd ? (uint)NOTIFY_ICON_MESSAGE.NIM_ADD : (uint)NOTIFY_ICON_MESSAGE.NIM_MODIFY,
|
||||
&data);
|
||||
|
||||
if (success)
|
||||
{
|
||||
_trayIconData = data;
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
}
|
||||
|
||||
private nint WindowProc(nint hwnd, uint uMsg, nuint wParam, nint lParam)
|
||||
{
|
||||
switch (uMsg)
|
||||
{
|
||||
// Shell can refuse NIM_ADD during explorer startup; WM_WINDOWPOSCHANGING is the first
|
||||
// reliable signal that the shell is ready, so re-attempt the add there.
|
||||
case PInvoke.WM_WINDOWPOSCHANGING:
|
||||
if (_trayIconData is null && _currentIconHandle != 0)
|
||||
{
|
||||
CreateOrUpdateTrayIcon(isAdd: true);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
if (uMsg == _wmTaskbarRestart)
|
||||
{
|
||||
Logger.LogInfo("[Awake] TaskbarCreated received; re-adding tray icon");
|
||||
_trayIconData = null;
|
||||
if (_currentIconHandle != 0)
|
||||
{
|
||||
CreateOrUpdateTrayIcon(isAdd: true);
|
||||
}
|
||||
}
|
||||
else if (uMsg == WmTrayIcon)
|
||||
{
|
||||
// Per Awake spec (#28530): both buttons open the flyout, no Win32 menu.
|
||||
switch ((uint)lParam)
|
||||
{
|
||||
case PInvoke.WM_LBUTTONUP:
|
||||
case PInvoke.WM_RBUTTONUP:
|
||||
_toggleWindow?.Invoke();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return CallWindowProcIntPtr(_originalWndProc, hwnd, uMsg, wParam, lParam);
|
||||
}
|
||||
|
||||
[LibraryImport("user32.dll", EntryPoint = "CallWindowProcW")]
|
||||
private static partial nint CallWindowProcIntPtr(IntPtr lpPrevWndFunc, nint hWnd, uint msg, nuint wParam, nint lParam);
|
||||
|
||||
[LibraryImport("user32.dll", EntryPoint = "RegisterWindowMessageW", StringMarshalling = StringMarshalling.Utf16)]
|
||||
private static partial uint RegisterWindowMessageNative(string lpString);
|
||||
|
||||
[LibraryImport("user32.dll", EntryPoint = "SetWindowLongPtrW")]
|
||||
private static partial nint SetWindowLongPtrNative(nint hWnd, int nIndex, nint dwNewLong);
|
||||
|
||||
[LibraryImport("shell32.dll", EntryPoint = "Shell_NotifyIconW")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static unsafe partial bool Shell_NotifyIconNative(uint dwMessage, NOTIFYICONDATAW* lpData);
|
||||
|
||||
private const int GwlWndproc = -4;
|
||||
}
|
||||
}
|
||||
12
src/modules/awake/Awake/NativeMethods.txt
Normal file
@@ -0,0 +1,12 @@
|
||||
// Structs and types only - functions use LibraryImport
|
||||
NOTIFYICONDATAW
|
||||
NOTIFY_ICON_MESSAGE
|
||||
NOTIFY_ICON_DATA_FLAGS
|
||||
|
||||
// Window message constants (used by TrayIconService)
|
||||
WM_USER
|
||||
WM_COMMAND
|
||||
WM_RBUTTONUP
|
||||
WM_LBUTTONUP
|
||||
WM_LBUTTONDBLCLK
|
||||
WM_WINDOWPOSCHANGING
|
||||
@@ -23,6 +23,7 @@ using Awake.Telemetry;
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.PowerToys.Telemetry;
|
||||
using Microsoft.UI.Dispatching;
|
||||
|
||||
namespace Awake
|
||||
{
|
||||
@@ -50,7 +51,8 @@ namespace Awake
|
||||
private static ConsoleEventHandler? _handler;
|
||||
private static SystemPowerCapabilities _powerCapabilities;
|
||||
|
||||
private static async Task<int> Main(string[] args)
|
||||
[STAThread]
|
||||
private static int Main(string[] args)
|
||||
{
|
||||
Logger.InitializeLogger(Path.Combine("\\", Core.Constants.AppName, "Logs"));
|
||||
|
||||
@@ -90,8 +92,20 @@ namespace Awake
|
||||
Logger.LogError("CultureNotFoundException: " + ex.Message);
|
||||
}
|
||||
|
||||
await TrayHelper.InitializeTray(TrayHelper.DefaultAwakeIcon, Core.Constants.FullAppName);
|
||||
AppDomain.CurrentDomain.ProcessExit += (_, _) => TrayHelper.RunOnMainThread(() => LockMutex?.ReleaseMutex());
|
||||
// Note: dispose (rather than ReleaseMutex) here because ProcessExit runs on a
|
||||
// different thread than the one that acquired the mutex, and ReleaseMutex requires
|
||||
// the owning thread. Disposing closes the handle and the OS releases ownership on exit.
|
||||
AppDomain.CurrentDomain.ProcessExit += (_, _) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
LockMutex?.Dispose();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Failed to dispose the single-instance mutex on exit: " + ex.Message);
|
||||
}
|
||||
};
|
||||
AppDomain.CurrentDomain.UnhandledException += AwakeUnhandledExceptionCatcher;
|
||||
|
||||
if (!instantiated)
|
||||
@@ -102,40 +116,68 @@ namespace Awake
|
||||
Exit(Core.Constants.AppName + " is already running! Exiting the application.", 1);
|
||||
return 1;
|
||||
}
|
||||
else
|
||||
|
||||
if (PowerToys.GPOWrapper.GPOWrapper.GetConfiguredAwakeEnabledValue() == PowerToys.GPOWrapper.GpoRuleConfigured.Disabled)
|
||||
{
|
||||
if (PowerToys.GPOWrapper.GPOWrapper.GetConfiguredAwakeEnabledValue() == PowerToys.GPOWrapper.GpoRuleConfigured.Disabled)
|
||||
{
|
||||
LogCLITelemetry(successful: false);
|
||||
Exit("PowerToys.Awake tried to start with a group policy setting that disables the tool. Please contact your system administrator.", 1);
|
||||
return 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogInfo($"Launching {Core.Constants.AppName}...");
|
||||
Logger.LogInfo(FileVersionInfo.GetVersionInfo(Assembly.GetExecutingAssembly().Location).FileVersion);
|
||||
Logger.LogInfo($"Build: {Core.Constants.BuildId}");
|
||||
Logger.LogInfo($"OS: {Environment.OSVersion}");
|
||||
Logger.LogInfo($"OS Build: {Manager.GetOperatingSystemBuild()}");
|
||||
|
||||
TaskScheduler.UnobservedTaskException += (sender, args) =>
|
||||
{
|
||||
Trace.WriteLine($"Task scheduler error: {args.Exception.Message}"); // somebody forgot to check!
|
||||
args.SetObserved();
|
||||
};
|
||||
|
||||
// To make it easier to diagnose future issues, let's get the
|
||||
// system power capabilities and aggregate them in the log.
|
||||
Bridge.GetPwrCapabilities(out _powerCapabilities);
|
||||
Logger.LogInfo(JsonSerializer.Serialize(_powerCapabilities, _serializerOptions));
|
||||
|
||||
var result = await rootCommand.InvokeAsync(args);
|
||||
LogCLITelemetry(successful: result == 0);
|
||||
return result;
|
||||
}
|
||||
LogCLITelemetry(successful: false);
|
||||
Exit("PowerToys.Awake tried to start with a group policy setting that disables the tool. Please contact your system administrator.", 1);
|
||||
return 1;
|
||||
}
|
||||
|
||||
Logger.LogInfo($"Launching {Core.Constants.AppName}...");
|
||||
Logger.LogInfo(FileVersionInfo.GetVersionInfo(Assembly.GetExecutingAssembly().Location).FileVersion);
|
||||
Logger.LogInfo($"Build: {Core.Constants.BuildId}");
|
||||
Logger.LogInfo($"OS: {Environment.OSVersion}");
|
||||
Logger.LogInfo($"OS Build: {Manager.GetOperatingSystemBuild()}");
|
||||
|
||||
TaskScheduler.UnobservedTaskException += (sender, args) =>
|
||||
{
|
||||
Trace.WriteLine($"Task scheduler error: {args.Exception.Message}"); // somebody forgot to check!
|
||||
args.SetObserved();
|
||||
};
|
||||
|
||||
// To make it easier to diagnose future issues, let's get the
|
||||
// system power capabilities and aggregate them in the log.
|
||||
Bridge.GetPwrCapabilities(out _powerCapabilities);
|
||||
Logger.LogInfo(JsonSerializer.Serialize(_powerCapabilities, _serializerOptions));
|
||||
|
||||
// Stash the parsed arguments so the WinUI dispatcher can pick them up after the
|
||||
// tray icon and main window are ready.
|
||||
_pendingArgs = args;
|
||||
|
||||
// Hand control to the WinUI runtime. The callback runs on the dispatcher thread;
|
||||
// we create the AwakeApp there (which creates the tray icon and the hidden flyout)
|
||||
// and then invoke the existing System.CommandLine handler from the UI thread.
|
||||
WinRT.ComWrappersSupport.InitializeComWrappers();
|
||||
|
||||
Microsoft.UI.Xaml.Application.Start((_) =>
|
||||
{
|
||||
var context = new DispatcherQueueSynchronizationContext(DispatcherQueue.GetForCurrentThread());
|
||||
SynchronizationContext.SetSynchronizationContext(context);
|
||||
|
||||
var app = new AwakeApp(_startedFromPowerToys);
|
||||
|
||||
// Defer the handler invocation until after AwakeApp.OnLaunched has run.
|
||||
DispatcherQueue.GetForCurrentThread().TryEnqueue(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
int handlerResult = rootCommand.Invoke(_pendingArgs ?? Array.Empty<string>());
|
||||
LogCLITelemetry(successful: handlerResult == 0);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Unhandled exception while invoking command handler: {ex}");
|
||||
LogCLITelemetry(successful: false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return Environment.ExitCode;
|
||||
}
|
||||
|
||||
private static string[]? _pendingArgs;
|
||||
|
||||
private static RootCommand BuildRootCommand()
|
||||
{
|
||||
Logger.LogInfo("Parsing parameters...");
|
||||
@@ -523,8 +565,9 @@ namespace Awake
|
||||
|
||||
private static void InitializeSettings()
|
||||
{
|
||||
AwakeSettings settings = Manager.ModuleSettings?.GetSettings<AwakeSettings>(Core.Constants.AppName) ?? new AwakeSettings();
|
||||
TrayHelper.SetTray(settings, _startedFromPowerToys);
|
||||
// The flyout reads CustomTrayTimes directly from settings on open, so there's
|
||||
// nothing for us to push at startup beyond logging that the settings exist.
|
||||
_ = Manager.ModuleSettings?.GetSettings<AwakeSettings>(Core.Constants.AppName) ?? new AwakeSettings();
|
||||
}
|
||||
|
||||
private static void HandleAwakeConfigChange(FileSystemEventArgs fileEvent)
|
||||
@@ -585,8 +628,6 @@ namespace Awake
|
||||
Logger.LogError("Unknown mode of operation. Check config file.");
|
||||
break;
|
||||
}
|
||||
|
||||
TrayHelper.SetTray(settings, _startedFromPowerToys);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
252
src/modules/awake/Awake/Properties/Resources.Designer.cs
generated
@@ -338,5 +338,257 @@ namespace Awake.Properties {
|
||||
return ResourceManager.GetString("AWAKE_TRAY_REMAINING", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string AWAKE_FLYOUT_TITLE {
|
||||
get {
|
||||
return ResourceManager.GetString("AWAKE_FLYOUT_TITLE", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string AWAKE_FLYOUT_MODE_HEADER {
|
||||
get {
|
||||
return ResourceManager.GetString("AWAKE_FLYOUT_MODE_HEADER", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string AWAKE_FLYOUT_MODE_OFF {
|
||||
get {
|
||||
return ResourceManager.GetString("AWAKE_FLYOUT_MODE_OFF", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string AWAKE_FLYOUT_MODE_INDEFINITE {
|
||||
get {
|
||||
return ResourceManager.GetString("AWAKE_FLYOUT_MODE_INDEFINITE", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string AWAKE_FLYOUT_MODE_TIMED {
|
||||
get {
|
||||
return ResourceManager.GetString("AWAKE_FLYOUT_MODE_TIMED", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string AWAKE_FLYOUT_MODE_EXPIRABLE {
|
||||
get {
|
||||
return ResourceManager.GetString("AWAKE_FLYOUT_MODE_EXPIRABLE", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string AWAKE_FLYOUT_DURATION_DAY {
|
||||
get {
|
||||
return ResourceManager.GetString("AWAKE_FLYOUT_DURATION_DAY", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string AWAKE_FLYOUT_DURATION_DAYS {
|
||||
get {
|
||||
return ResourceManager.GetString("AWAKE_FLYOUT_DURATION_DAYS", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string AWAKE_FLYOUT_DURATION_WEEK {
|
||||
get {
|
||||
return ResourceManager.GetString("AWAKE_FLYOUT_DURATION_WEEK", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string AWAKE_FLYOUT_DURATION_WEEKS {
|
||||
get {
|
||||
return ResourceManager.GetString("AWAKE_FLYOUT_DURATION_WEEKS", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string AWAKE_FLYOUT_DURATION_MONTH {
|
||||
get {
|
||||
return ResourceManager.GetString("AWAKE_FLYOUT_DURATION_MONTH", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string AWAKE_FLYOUT_DURATION_MONTHS {
|
||||
get {
|
||||
return ResourceManager.GetString("AWAKE_FLYOUT_DURATION_MONTHS", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string AWAKE_FLYOUT_AWAKE_UNTIL {
|
||||
get {
|
||||
return ResourceManager.GetString("AWAKE_FLYOUT_AWAKE_UNTIL", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string AWAKE_FLYOUT_AWAKE_INDEFINITELY {
|
||||
get {
|
||||
return ResourceManager.GetString("AWAKE_FLYOUT_AWAKE_INDEFINITELY", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string AWAKE_FLYOUT_WHILE_APP_RUNS {
|
||||
get {
|
||||
return ResourceManager.GetString("AWAKE_FLYOUT_WHILE_APP_RUNS", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string AWAKE_FLYOUT_FOREVER {
|
||||
get {
|
||||
return ResourceManager.GetString("AWAKE_FLYOUT_FOREVER", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string AWAKE_FLYOUT_CUSTOM {
|
||||
get {
|
||||
return ResourceManager.GetString("AWAKE_FLYOUT_CUSTOM", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string AWAKE_FLYOUT_CARD_WHILE_APP {
|
||||
get {
|
||||
return ResourceManager.GetString("AWAKE_FLYOUT_CARD_WHILE_APP", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string AWAKE_FLYOUT_CARD_CUSTOM_UNTIL {
|
||||
get {
|
||||
return ResourceManager.GetString("AWAKE_FLYOUT_CARD_CUSTOM_UNTIL", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string AWAKE_FLYOUT_CUSTOM_DURATION {
|
||||
get {
|
||||
return ResourceManager.GetString("AWAKE_FLYOUT_CUSTOM_DURATION", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string AWAKE_FLYOUT_STOP {
|
||||
get {
|
||||
return ResourceManager.GetString("AWAKE_FLYOUT_STOP", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string AWAKE_FLYOUT_START {
|
||||
get {
|
||||
return ResourceManager.GetString("AWAKE_FLYOUT_START", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string AWAKE_FLYOUT_ACTIVE_UNTIL {
|
||||
get {
|
||||
return ResourceManager.GetString("AWAKE_FLYOUT_ACTIVE_UNTIL", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string AWAKE_FLYOUT_UNIT_MIN {
|
||||
get {
|
||||
return ResourceManager.GetString("AWAKE_FLYOUT_UNIT_MIN", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string AWAKE_FLYOUT_UNIT_HOUR {
|
||||
get {
|
||||
return ResourceManager.GetString("AWAKE_FLYOUT_UNIT_HOUR", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string AWAKE_FLYOUT_UNIT_HOURS {
|
||||
get {
|
||||
return ResourceManager.GetString("AWAKE_FLYOUT_UNIT_HOURS", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string AWAKE_FLYOUT_SUBTITLE {
|
||||
get {
|
||||
return ResourceManager.GetString("AWAKE_FLYOUT_SUBTITLE", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string AWAKE_FLYOUT_ACTIVE {
|
||||
get {
|
||||
return ResourceManager.GetString("AWAKE_FLYOUT_ACTIVE", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string AWAKE_FLYOUT_KEEP_SCREEN_ON_DESC {
|
||||
get {
|
||||
return ResourceManager.GetString("AWAKE_FLYOUT_KEEP_SCREEN_ON_DESC", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string AWAKE_FLYOUT_TIMED_HEADER {
|
||||
get {
|
||||
return ResourceManager.GetString("AWAKE_FLYOUT_TIMED_HEADER", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string AWAKE_FLYOUT_EXPIRABLE_HEADER {
|
||||
get {
|
||||
return ResourceManager.GetString("AWAKE_FLYOUT_EXPIRABLE_HEADER", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string AWAKE_FLYOUT_EXPIRABLE_DATE {
|
||||
get {
|
||||
return ResourceManager.GetString("AWAKE_FLYOUT_EXPIRABLE_DATE", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string AWAKE_FLYOUT_EXPIRABLE_TIME {
|
||||
get {
|
||||
return ResourceManager.GetString("AWAKE_FLYOUT_EXPIRABLE_TIME", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string AWAKE_FLYOUT_EXPIRABLE_APPLY {
|
||||
get {
|
||||
return ResourceManager.GetString("AWAKE_FLYOUT_EXPIRABLE_APPLY", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string AWAKE_FLYOUT_EDIT_PRESETS {
|
||||
get {
|
||||
return ResourceManager.GetString("AWAKE_FLYOUT_EDIT_PRESETS", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string AWAKE_FLYOUT_OPEN_SETTINGS {
|
||||
get {
|
||||
return ResourceManager.GetString("AWAKE_FLYOUT_OPEN_SETTINGS", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string AWAKE_FLYOUT_STATUS_OFF {
|
||||
get {
|
||||
return ResourceManager.GetString("AWAKE_FLYOUT_STATUS_OFF", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string AWAKE_FLYOUT_STATUS_INDEFINITE {
|
||||
get {
|
||||
return ResourceManager.GetString("AWAKE_FLYOUT_STATUS_INDEFINITE", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string AWAKE_FLYOUT_STATUS_TIMED {
|
||||
get {
|
||||
return ResourceManager.GetString("AWAKE_FLYOUT_STATUS_TIMED", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string AWAKE_FLYOUT_STATUS_EXPIRABLE {
|
||||
get {
|
||||
return ResourceManager.GetString("AWAKE_FLYOUT_STATUS_EXPIRABLE", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string AWAKE_FLYOUT_INTERVAL_HOURS {
|
||||
get {
|
||||
return ResourceManager.GetString("AWAKE_FLYOUT_INTERVAL_HOURS", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string AWAKE_FLYOUT_INTERVAL_MINUTES {
|
||||
get {
|
||||
return ResourceManager.GetString("AWAKE_FLYOUT_INTERVAL_MINUTES", resourceCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,4 +221,172 @@
|
||||
<value>remaining</value>
|
||||
<comment>Suffix for timed mode showing time remaining, e.g. "1:30:00 remaining".</comment>
|
||||
</data>
|
||||
<data name="AWAKE_FLYOUT_TITLE" xml:space="preserve">
|
||||
<value>PowerToys Awake</value>
|
||||
<comment>Title shown at the top of the Awake tray flyout.</comment>
|
||||
</data>
|
||||
<data name="AWAKE_FLYOUT_MODE_HEADER" xml:space="preserve">
|
||||
<value>Mode</value>
|
||||
<comment>Header label above the mode selector (Off / Indefinite / Timed / Until expiration).</comment>
|
||||
</data>
|
||||
<data name="AWAKE_FLYOUT_MODE_OFF" xml:space="preserve">
|
||||
<value>Off</value>
|
||||
<comment>Short label for the Passive (no keep-awake) mode in the flyout's mode selector.</comment>
|
||||
</data>
|
||||
<data name="AWAKE_FLYOUT_MODE_INDEFINITE" xml:space="preserve">
|
||||
<value>Indefinite</value>
|
||||
<comment>Short label for the Indefinite keep-awake mode in the flyout's mode selector.</comment>
|
||||
</data>
|
||||
<data name="AWAKE_FLYOUT_MODE_TIMED" xml:space="preserve">
|
||||
<value>Timed</value>
|
||||
<comment>Short label for the Timed keep-awake mode in the flyout's mode selector.</comment>
|
||||
</data>
|
||||
<data name="AWAKE_FLYOUT_MODE_EXPIRABLE" xml:space="preserve">
|
||||
<value>Until date</value>
|
||||
<comment>Short label for the Expirable keep-awake mode (active until a specified date and time) in the flyout's mode selector.</comment>
|
||||
</data>
|
||||
<data name="AWAKE_FLYOUT_DURATION_DAY" xml:space="preserve">
|
||||
<value>{0} day</value>
|
||||
<comment>Subtext under the countdown ring for a single remaining day. {0} is the number 1.</comment>
|
||||
</data>
|
||||
<data name="AWAKE_FLYOUT_DURATION_DAYS" xml:space="preserve">
|
||||
<value>{0} days</value>
|
||||
<comment>Subtext under the countdown ring for the number of remaining days. {0} is the day count.</comment>
|
||||
</data>
|
||||
<data name="AWAKE_FLYOUT_DURATION_WEEK" xml:space="preserve">
|
||||
<value>{0} week</value>
|
||||
<comment>Subtext under the countdown ring for a single remaining week. {0} is the number 1.</comment>
|
||||
</data>
|
||||
<data name="AWAKE_FLYOUT_DURATION_WEEKS" xml:space="preserve">
|
||||
<value>{0} weeks</value>
|
||||
<comment>Subtext under the countdown ring for the number of remaining weeks. {0} is the week count.</comment>
|
||||
</data>
|
||||
<data name="AWAKE_FLYOUT_DURATION_MONTH" xml:space="preserve">
|
||||
<value>{0} month</value>
|
||||
<comment>Subtext under the countdown ring for a single remaining month. {0} is the number 1.</comment>
|
||||
</data>
|
||||
<data name="AWAKE_FLYOUT_DURATION_MONTHS" xml:space="preserve">
|
||||
<value>{0} months</value>
|
||||
<comment>Subtext under the countdown ring for the number of remaining months. {0} is the month count.</comment>
|
||||
</data>
|
||||
<data name="AWAKE_FLYOUT_AWAKE_UNTIL" xml:space="preserve">
|
||||
<value>Awake until {0}</value>
|
||||
<comment>Line under the countdown gauge stating when keep-awake ends. {0} is a time or date/time.</comment>
|
||||
</data>
|
||||
<data name="AWAKE_FLYOUT_AWAKE_INDEFINITELY" xml:space="preserve">
|
||||
<value>Stays awake until stopped</value>
|
||||
<comment>Line under the countdown gauge when the machine is kept awake indefinitely.</comment>
|
||||
</data>
|
||||
<data name="AWAKE_FLYOUT_WHILE_APP_RUNS" xml:space="preserve">
|
||||
<value>Awake while {0} runs</value>
|
||||
<comment>Status line shown while keep-awake is bound to a running app. {0} is the app name.</comment>
|
||||
</data>
|
||||
<data name="AWAKE_FLYOUT_FOREVER" xml:space="preserve">
|
||||
<value>Forever</value>
|
||||
<comment>Label for the flyout chip that keeps the machine awake indefinitely.</comment>
|
||||
</data>
|
||||
<data name="AWAKE_FLYOUT_CUSTOM" xml:space="preserve">
|
||||
<value>Custom</value>
|
||||
<comment>Label for the flyout chip that opens a popup to set a custom duration or end date/time.</comment>
|
||||
</data>
|
||||
<data name="AWAKE_FLYOUT_CARD_WHILE_APP" xml:space="preserve">
|
||||
<value>While app runs</value>
|
||||
<comment>Default label on the flyout card that keeps the PC awake while a chosen app runs.</comment>
|
||||
</data>
|
||||
<data name="AWAKE_FLYOUT_CARD_CUSTOM_UNTIL" xml:space="preserve">
|
||||
<value>Until {0}</value>
|
||||
<comment>Custom card label when keeping awake until a specific time. {0} is a time or date/time.</comment>
|
||||
</data>
|
||||
<data name="AWAKE_FLYOUT_CUSTOM_DURATION" xml:space="preserve">
|
||||
<value>Duration</value>
|
||||
<comment>Label for the option in the Custom popup that keeps the machine awake for a specified hours/minutes duration.</comment>
|
||||
</data>
|
||||
<data name="AWAKE_FLYOUT_STOP" xml:space="preserve">
|
||||
<value>Stop</value>
|
||||
<comment>Label for the button that stops keeping the machine awake and returns to following the power plan.</comment>
|
||||
</data>
|
||||
<data name="AWAKE_FLYOUT_START" xml:space="preserve">
|
||||
<value>Start</value>
|
||||
<comment>Label for the button that starts keeping the machine awake using the selected duration.</comment>
|
||||
</data>
|
||||
<data name="AWAKE_FLYOUT_ACTIVE_UNTIL" xml:space="preserve">
|
||||
<value>Active until {0}</value>
|
||||
<comment>Preview line stating when keep-awake will end. {0} is a time or date/time.</comment>
|
||||
</data>
|
||||
<data name="AWAKE_FLYOUT_UNIT_MIN" xml:space="preserve">
|
||||
<value>min</value>
|
||||
<comment>Short unit label for minutes shown under a duration preset (e.g. "15 min").</comment>
|
||||
</data>
|
||||
<data name="AWAKE_FLYOUT_UNIT_HOUR" xml:space="preserve">
|
||||
<value>hour</value>
|
||||
<comment>Singular unit label for an hour shown under a duration preset (e.g. "1 hour").</comment>
|
||||
</data>
|
||||
<data name="AWAKE_FLYOUT_UNIT_HOURS" xml:space="preserve">
|
||||
<value>hours</value>
|
||||
<comment>Plural unit label for hours shown under a duration preset (e.g. "2 hours").</comment>
|
||||
</data>
|
||||
<data name="AWAKE_FLYOUT_SUBTITLE" xml:space="preserve">
|
||||
<value>Choose a duration to prevent sleep.</value>
|
||||
<comment>Subtitle shown in the flyout header while keep-awake is not active.</comment>
|
||||
</data>
|
||||
<data name="AWAKE_FLYOUT_ACTIVE" xml:space="preserve">
|
||||
<value>ACTIVE</value>
|
||||
<comment>Badge text shown in the flyout header while keep-awake is running.</comment>
|
||||
</data>
|
||||
<data name="AWAKE_FLYOUT_KEEP_SCREEN_ON_DESC" xml:space="preserve">
|
||||
<value>Prevent your display from going to sleep.</value>
|
||||
<comment>Description shown beneath the "Keep screen on" checkbox.</comment>
|
||||
</data>
|
||||
<data name="AWAKE_FLYOUT_TIMED_HEADER" xml:space="preserve">
|
||||
<value>Keep awake for</value>
|
||||
<comment>Header above the list of preset timed durations.</comment>
|
||||
</data>
|
||||
<data name="AWAKE_FLYOUT_EXPIRABLE_HEADER" xml:space="preserve">
|
||||
<value>Keep awake until</value>
|
||||
<comment>Header above the date and time pickers for expirable mode.</comment>
|
||||
</data>
|
||||
<data name="AWAKE_FLYOUT_EXPIRABLE_DATE" xml:space="preserve">
|
||||
<value>Date</value>
|
||||
<comment>Label next to the date picker.</comment>
|
||||
</data>
|
||||
<data name="AWAKE_FLYOUT_EXPIRABLE_TIME" xml:space="preserve">
|
||||
<value>Time</value>
|
||||
<comment>Label next to the time picker.</comment>
|
||||
</data>
|
||||
<data name="AWAKE_FLYOUT_EXPIRABLE_APPLY" xml:space="preserve">
|
||||
<value>Apply</value>
|
||||
<comment>Button that confirms the chosen expiration date/time.</comment>
|
||||
</data>
|
||||
<data name="AWAKE_FLYOUT_EDIT_PRESETS" xml:space="preserve">
|
||||
<value>Edit presets in Settings</value>
|
||||
<comment>Link that opens the Awake page in PowerToys Settings to edit the list of preset durations.</comment>
|
||||
</data>
|
||||
<data name="AWAKE_FLYOUT_OPEN_SETTINGS" xml:space="preserve">
|
||||
<value>Open settings</value>
|
||||
<comment>Footer button that opens the Awake page in PowerToys Settings.</comment>
|
||||
</data>
|
||||
<data name="AWAKE_FLYOUT_STATUS_OFF" xml:space="preserve">
|
||||
<value>Using the system power plan</value>
|
||||
<comment>Status line shown below the title when Awake is in Passive (Off) mode.</comment>
|
||||
</data>
|
||||
<data name="AWAKE_FLYOUT_STATUS_INDEFINITE" xml:space="preserve">
|
||||
<value>Keeping your PC awake indefinitely</value>
|
||||
<comment>Status line shown below the title when Awake is in Indefinite mode.</comment>
|
||||
</data>
|
||||
<data name="AWAKE_FLYOUT_STATUS_TIMED" xml:space="preserve">
|
||||
<value>Keeping your PC awake for {0}</value>
|
||||
<comment>Status line shown below the title when Awake is in Timed mode. {0} is the duration, e.g. "30 minutes".</comment>
|
||||
</data>
|
||||
<data name="AWAKE_FLYOUT_STATUS_EXPIRABLE" xml:space="preserve">
|
||||
<value>Keeping your PC awake until {0}</value>
|
||||
<comment>Status line shown below the title when Awake is in Expirable mode. {0} is the formatted date/time.</comment>
|
||||
</data>
|
||||
<data name="AWAKE_FLYOUT_INTERVAL_HOURS" xml:space="preserve">
|
||||
<value>Hours</value>
|
||||
<comment>Header above the hours number input in the flyout's Timed mode section.</comment>
|
||||
</data>
|
||||
<data name="AWAKE_FLYOUT_INTERVAL_MINUTES" xml:space="preserve">
|
||||
<value>Minutes</value>
|
||||
<comment>Header above the minutes number input in the flyout's Timed mode section.</comment>
|
||||
</data>
|
||||
</root>
|
||||
260
src/modules/awake/Awake/Strings/en-us/Resources.resw
Normal file
@@ -0,0 +1,260 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="HeaderTitle.Text" xml:space="preserve">
|
||||
<value>Awake is not running</value>
|
||||
<comment>Title shown in the flyout header while keep-awake is not active.</comment>
|
||||
</data>
|
||||
<data name="HeaderSubtitle.Text" xml:space="preserve">
|
||||
<value>PC sleep follows your power plan.</value>
|
||||
<comment>Subtitle shown in the flyout header while keep-awake is not active.</comment>
|
||||
</data>
|
||||
<data name="ActiveBadgeText.Text" xml:space="preserve">
|
||||
<value>ACTIVE</value>
|
||||
<comment>Badge text shown in the flyout header while keep-awake is running.</comment>
|
||||
</data>
|
||||
<data name="Card30Unit.Text" xml:space="preserve">
|
||||
<value>min</value>
|
||||
<comment>Unit label shown under the "30" on the 30-minute preset card.</comment>
|
||||
</data>
|
||||
<data name="Card60Unit.Text" xml:space="preserve">
|
||||
<value>hour</value>
|
||||
<comment>Unit label shown under the "1" on the 1-hour preset card.</comment>
|
||||
</data>
|
||||
<data name="Card120Unit.Text" xml:space="preserve">
|
||||
<value>hours</value>
|
||||
<comment>Unit label shown under the "2" on the 2-hour preset card.</comment>
|
||||
</data>
|
||||
<data name="Card30.Content" xml:space="preserve">
|
||||
<value>30 min</value>
|
||||
<comment>Duration preset button that keeps the PC awake for 30 minutes.</comment>
|
||||
</data>
|
||||
<data name="Card60.Content" xml:space="preserve">
|
||||
<value>1 hour</value>
|
||||
<comment>Duration preset button that keeps the PC awake for 1 hour.</comment>
|
||||
</data>
|
||||
<data name="Card120.Content" xml:space="preserve">
|
||||
<value>2 hours</value>
|
||||
<comment>Duration preset button that keeps the PC awake for 2 hours.</comment>
|
||||
</data>
|
||||
<data name="CardForever.Content" xml:space="preserve">
|
||||
<value>Forever</value>
|
||||
<comment>Duration preset button that keeps the PC awake indefinitely.</comment>
|
||||
</data>
|
||||
<data name="CardForeverText.Text" xml:space="preserve">
|
||||
<value>Forever</value>
|
||||
<comment>Label on the card that keeps the PC awake indefinitely.</comment>
|
||||
</data>
|
||||
<data name="CardCustomText.Text" xml:space="preserve">
|
||||
<value>Custom</value>
|
||||
<comment>Label on the button that opens the custom duration picker.</comment>
|
||||
</data>
|
||||
<data name="CardWhileAppText.Text" xml:space="preserve">
|
||||
<value>While app runs</value>
|
||||
<comment>Label on the button that opens the running-app picker; keeps the PC awake while the chosen app runs.</comment>
|
||||
</data>
|
||||
<data name="CardCustomNav.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||
<value>Configure custom duration</value>
|
||||
<comment>Accessible name for the chevron that opens the custom duration picker page.</comment>
|
||||
</data>
|
||||
<data name="CardCustomNav.[using:Microsoft.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
|
||||
<value>Configure custom duration</value>
|
||||
<comment>Tooltip for the chevron that opens the custom duration picker page.</comment>
|
||||
</data>
|
||||
<data name="CardWhileAppNav.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||
<value>Choose an app</value>
|
||||
<comment>Accessible name for the chevron that opens the running-app picker page.</comment>
|
||||
</data>
|
||||
<data name="CardWhileAppNav.[using:Microsoft.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
|
||||
<value>Choose an app</value>
|
||||
<comment>Tooltip for the chevron that opens the running-app picker page.</comment>
|
||||
</data>
|
||||
<data name="AppSearchBox.PlaceholderText" xml:space="preserve">
|
||||
<value>Search apps</value>
|
||||
<comment>Placeholder text in the search box of the running-app picker.</comment>
|
||||
</data>
|
||||
<data name="AppEmptyText.Text" xml:space="preserve">
|
||||
<value>No running apps found</value>
|
||||
<comment>Shown in the running-app picker when no apps match the search.</comment>
|
||||
</data>
|
||||
<data name="CustomDurationRadio.Content" xml:space="preserve">
|
||||
<value>Duration</value>
|
||||
<comment>Option to keep awake for a custom number of hours and minutes.</comment>
|
||||
</data>
|
||||
<data name="CustomUntilRadio.Content" xml:space="preserve">
|
||||
<value>Until date</value>
|
||||
<comment>Option to keep awake until a specific date and time.</comment>
|
||||
</data>
|
||||
<data name="IntervalHoursInput.Header" xml:space="preserve">
|
||||
<value>Hours</value>
|
||||
<comment>Header for the custom-duration hours input.</comment>
|
||||
</data>
|
||||
<data name="IntervalMinutesInput.Header" xml:space="preserve">
|
||||
<value>Minutes</value>
|
||||
<comment>Header for the custom-duration minutes input.</comment>
|
||||
</data>
|
||||
<data name="ExpirationDatePicker.Header" xml:space="preserve">
|
||||
<value>Date</value>
|
||||
<comment>Header for the keep-awake-until date picker.</comment>
|
||||
</data>
|
||||
<data name="ExpirationTimePicker.Header" xml:space="preserve">
|
||||
<value>Time</value>
|
||||
<comment>Header for the keep-awake-until time picker.</comment>
|
||||
</data>
|
||||
<data name="CustomApplyButton.Content" xml:space="preserve">
|
||||
<value>Apply</value>
|
||||
<comment>Button that confirms the custom duration selection.</comment>
|
||||
</data>
|
||||
<data name="KeepDisplayOnToggle.Content" xml:space="preserve">
|
||||
<value>Keep screen on</value>
|
||||
<comment>Checkbox to also keep the display awake.</comment>
|
||||
</data>
|
||||
<data name="ActionStartText.Text" xml:space="preserve">
|
||||
<value>Start</value>
|
||||
<comment>Text on the action button that starts keeping the PC awake.</comment>
|
||||
</data>
|
||||
<data name="ActionStopText.Text" xml:space="preserve">
|
||||
<value>Stop</value>
|
||||
<comment>Text on the action button that stops keeping the PC awake.</comment>
|
||||
</data>
|
||||
<data name="OpenSettingsButton.[using:Microsoft.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
|
||||
<value>Open settings</value>
|
||||
<comment>Tooltip for the button that opens Awake settings.</comment>
|
||||
</data>
|
||||
<data name="OpenSettingsButton.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||
<value>Open settings</value>
|
||||
<comment>Accessible name for the button that opens Awake settings.</comment>
|
||||
</data>
|
||||
<data name="AwakeBackButton.[using:Microsoft.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
|
||||
<value>Back</value>
|
||||
<comment>Tooltip for the button that returns to the main flyout page.</comment>
|
||||
</data>
|
||||
<data name="AwakeBackButton.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||
<value>Back</value>
|
||||
<comment>Accessible name for the button that returns to the main flyout page.</comment>
|
||||
</data>
|
||||
<data name="CustomTimeTitle.Text" xml:space="preserve">
|
||||
<value>Custom duration</value>
|
||||
<comment>Title of the page where the user sets a custom keep-awake duration or end time.</comment>
|
||||
</data>
|
||||
<data name="AppPickerTitle.Text" xml:space="preserve">
|
||||
<value>While app runs</value>
|
||||
<comment>Title of the page where the user picks an app to keep the PC awake while it runs.</comment>
|
||||
</data>
|
||||
</root>
|
||||
749
src/modules/awake/Awake/ViewModels/AwakeFlyoutViewModel.cs
Normal file
@@ -0,0 +1,749 @@
|
||||
// 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.Globalization;
|
||||
using System.Text;
|
||||
using Awake.Core;
|
||||
using Awake.Properties;
|
||||
using Common.UI;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
|
||||
namespace Awake.ViewModels
|
||||
{
|
||||
/// <summary>
|
||||
/// Identifies which keep-awake duration the user has selected in the flyout. Shared across
|
||||
/// the launch page and the custom-time page so the selection survives frame navigation.
|
||||
/// </summary>
|
||||
public enum FlyoutSelectionKind
|
||||
{
|
||||
Timed,
|
||||
Forever,
|
||||
Custom,
|
||||
WhileApp,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Backs the Awake tray flyout. Reads the current Awake state from <see cref="Manager"/>
|
||||
/// (which is the single source of truth) and delegates user actions back to the same
|
||||
/// <c>SetXxxKeepAwake</c> APIs that the old HMENU click handlers used.
|
||||
/// </summary>
|
||||
public sealed partial class AwakeFlyoutViewModel : ObservableObject, IDisposable
|
||||
{
|
||||
private static readonly CompositeFormat StatusTimedFormat =
|
||||
CompositeFormat.Parse(Resources.AWAKE_FLYOUT_STATUS_TIMED);
|
||||
|
||||
private static readonly CompositeFormat StatusExpirableFormat =
|
||||
CompositeFormat.Parse(Resources.AWAKE_FLYOUT_STATUS_EXPIRABLE);
|
||||
|
||||
private static readonly CompositeFormat AwakeUntilFormat =
|
||||
CompositeFormat.Parse(Resources.AWAKE_FLYOUT_AWAKE_UNTIL);
|
||||
|
||||
private static readonly CompositeFormat AwakeWhileAppFormat =
|
||||
CompositeFormat.Parse(Resources.AWAKE_FLYOUT_WHILE_APP_RUNS);
|
||||
|
||||
private static readonly CompositeFormat CustomUntilCardFormat =
|
||||
CompositeFormat.Parse(Resources.AWAKE_FLYOUT_CARD_CUSTOM_UNTIL);
|
||||
|
||||
private readonly SettingsUtils _settingsUtils;
|
||||
private bool _suppressApply;
|
||||
|
||||
[ObservableProperty]
|
||||
private AwakeMode _mode;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _keepDisplayOn;
|
||||
|
||||
[ObservableProperty]
|
||||
private DateTimeOffset _expirationDate;
|
||||
|
||||
[ObservableProperty]
|
||||
private TimeSpan _expirationTime;
|
||||
|
||||
[ObservableProperty]
|
||||
private uint _intervalHours;
|
||||
|
||||
[ObservableProperty]
|
||||
private uint _intervalMinutes;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _statusText = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _countdownTime = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _offAtText = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private Microsoft.UI.Xaml.Visibility _offAtVisibility = Microsoft.UI.Xaml.Visibility.Collapsed;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(ActiveAppIconVisibility))]
|
||||
[NotifyPropertyChangedFor(nameof(ActiveCountdownVisibility))]
|
||||
private bool _isProcessBound;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _boundAppName = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _customCardText = Resources.AWAKE_FLYOUT_CUSTOM;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _whileAppCardText = Resources.AWAKE_FLYOUT_CARD_WHILE_APP;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(ActiveAppIconVisibility))]
|
||||
[NotifyPropertyChangedFor(nameof(ActiveCountdownVisibility))]
|
||||
private Microsoft.UI.Xaml.Media.ImageSource? _whileAppCardIcon;
|
||||
|
||||
// In the active header, show the bound app's icon (instead of the infinity glyph) whenever
|
||||
// Awake is tracking an app and we actually have an icon for it.
|
||||
public Microsoft.UI.Xaml.Visibility ActiveAppIconVisibility =>
|
||||
IsProcessBound && WhileAppCardIcon != null
|
||||
? Microsoft.UI.Xaml.Visibility.Visible
|
||||
: Microsoft.UI.Xaml.Visibility.Collapsed;
|
||||
|
||||
public Microsoft.UI.Xaml.Visibility ActiveCountdownVisibility =>
|
||||
IsProcessBound && WhileAppCardIcon != null
|
||||
? Microsoft.UI.Xaml.Visibility.Collapsed
|
||||
: Microsoft.UI.Xaml.Visibility.Visible;
|
||||
|
||||
public bool KeepDisplayOnEnabled => Mode != AwakeMode.PASSIVE;
|
||||
|
||||
// True while a keep-awake session is running; drives the header's active visual state.
|
||||
public bool IsActive => Mode != AwakeMode.PASSIVE;
|
||||
|
||||
public Microsoft.UI.Xaml.Visibility StopButtonVisibility =>
|
||||
Mode != AwakeMode.PASSIVE ? Microsoft.UI.Xaml.Visibility.Visible : Microsoft.UI.Xaml.Visibility.Collapsed;
|
||||
|
||||
public Microsoft.UI.Xaml.Visibility TimedSectionVisibility =>
|
||||
Mode == AwakeMode.TIMED ? Microsoft.UI.Xaml.Visibility.Visible : Microsoft.UI.Xaml.Visibility.Collapsed;
|
||||
|
||||
public Microsoft.UI.Xaml.Visibility ExpirableSectionVisibility =>
|
||||
Mode == AwakeMode.EXPIRABLE ? Microsoft.UI.Xaml.Visibility.Visible : Microsoft.UI.Xaml.Visibility.Collapsed;
|
||||
|
||||
public AwakeFlyoutViewModel(SettingsUtils settingsUtils, bool startedFromPowerToys)
|
||||
{
|
||||
_settingsUtils = settingsUtils ?? throw new ArgumentNullException(nameof(settingsUtils));
|
||||
|
||||
_ = startedFromPowerToys;
|
||||
|
||||
Manager.ModeChanged += OnManagerModeChanged;
|
||||
Refresh();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Re-reads the current Awake state from <see cref="Manager"/> and the
|
||||
/// on-disk settings and updates all bindable properties. Safe to call repeatedly.
|
||||
/// </summary>
|
||||
public void Refresh()
|
||||
{
|
||||
try
|
||||
{
|
||||
_suppressApply = true;
|
||||
|
||||
// Set the process-bound state *before* Mode. Setting Mode raises PropertyChanged
|
||||
// synchronously, and listeners (e.g. the launch page) re-run SyncPendingFromMode,
|
||||
// which needs the correct IsProcessBound to map INDEFINITE to "While app" vs. "Forever".
|
||||
IsProcessBound = Manager.IsProcessBound;
|
||||
BoundAppName = Manager.BoundProcessName;
|
||||
Mode = Manager.CurrentOperatingMode;
|
||||
KeepDisplayOn = Manager.IsDisplayOn;
|
||||
|
||||
var expireAt = Manager.ExpireAt;
|
||||
if (expireAt <= DateTimeOffset.Now)
|
||||
{
|
||||
expireAt = DateTimeOffset.Now.AddMinutes(30);
|
||||
}
|
||||
|
||||
ExpirationDate = new DateTimeOffset(expireAt.Date, expireAt.Offset);
|
||||
ExpirationTime = expireAt.TimeOfDay;
|
||||
|
||||
LoadIntervalFromSettings();
|
||||
UpdateStatusText();
|
||||
UpdateCountdown();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_suppressApply = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadIntervalFromSettings()
|
||||
{
|
||||
try
|
||||
{
|
||||
var settings = _settingsUtils.GetSettings<AwakeSettings>(Core.Constants.AppName);
|
||||
if (settings is not null)
|
||||
{
|
||||
IntervalHours = settings.Properties.IntervalHours;
|
||||
IntervalMinutes = settings.Properties.IntervalMinutes;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning($"Failed to load interval from settings: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnManagerModeChanged(object? sender, EventArgs e)
|
||||
{
|
||||
// Manager raises this off the dispatcher; marshal back to the UI thread
|
||||
// via the app's main window dispatcher if we have one.
|
||||
var dq = AwakeApp.Current?.MainWindow?.DispatcherQueue;
|
||||
if (dq is not null)
|
||||
{
|
||||
dq.TryEnqueue(Refresh);
|
||||
}
|
||||
else
|
||||
{
|
||||
Refresh();
|
||||
}
|
||||
}
|
||||
|
||||
partial void OnModeChanged(AwakeMode value)
|
||||
{
|
||||
// Notify the computed/derived properties so XAML one-way bindings refresh.
|
||||
OnPropertyChanged(nameof(KeepDisplayOnEnabled));
|
||||
OnPropertyChanged(nameof(IsActive));
|
||||
OnPropertyChanged(nameof(StopButtonVisibility));
|
||||
OnPropertyChanged(nameof(TimedSectionVisibility));
|
||||
OnPropertyChanged(nameof(ExpirableSectionVisibility));
|
||||
|
||||
if (_suppressApply)
|
||||
{
|
||||
UpdateStatusText();
|
||||
UpdateCountdown();
|
||||
return;
|
||||
}
|
||||
|
||||
ApplyMode(value);
|
||||
UpdateStatusText();
|
||||
UpdateCountdown();
|
||||
}
|
||||
|
||||
partial void OnKeepDisplayOnChanged(bool value)
|
||||
{
|
||||
if (_suppressApply)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// SetDisplay toggles the persisted value when running under PT config; otherwise
|
||||
// it directly drives the executor. Either way it always re-applies the current mode.
|
||||
if (value != Manager.IsDisplayOn)
|
||||
{
|
||||
Manager.SetDisplay();
|
||||
}
|
||||
}
|
||||
|
||||
partial void OnIntervalHoursChanged(uint value)
|
||||
{
|
||||
OnIntervalChanged();
|
||||
}
|
||||
|
||||
partial void OnIntervalMinutesChanged(uint value)
|
||||
{
|
||||
OnIntervalChanged();
|
||||
}
|
||||
|
||||
partial void OnExpirationDateChanged(DateTimeOffset value)
|
||||
{
|
||||
OnExpirationChanged();
|
||||
}
|
||||
|
||||
partial void OnExpirationTimeChanged(TimeSpan value)
|
||||
{
|
||||
OnExpirationChanged();
|
||||
}
|
||||
|
||||
private void OnIntervalChanged()
|
||||
{
|
||||
if (_suppressApply)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Mode == AwakeMode.TIMED)
|
||||
{
|
||||
ApplyTimedFromInterval();
|
||||
}
|
||||
|
||||
UpdateStatusText();
|
||||
}
|
||||
|
||||
private void OnExpirationChanged()
|
||||
{
|
||||
if (_suppressApply)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Mode == AwakeMode.EXPIRABLE)
|
||||
{
|
||||
ApplyExpirableFromPickers();
|
||||
}
|
||||
|
||||
UpdateStatusText();
|
||||
}
|
||||
|
||||
private void ApplyTimedFromInterval()
|
||||
{
|
||||
uint seconds = (IntervalHours * 3600u) + (IntervalMinutes * 60u);
|
||||
if (seconds == 0)
|
||||
{
|
||||
// 0/0 would resolve to an instantaneous expiration; ignore until the user
|
||||
// provides a non-zero interval.
|
||||
return;
|
||||
}
|
||||
|
||||
Manager.SetTimedKeepAwake(seconds, KeepDisplayOn);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies a one-tap timed preset (e.g. the 30m / 1h / 2h / 4h chips). Switches into
|
||||
/// timed mode if necessary and starts the keep-awake session immediately, applying the
|
||||
/// hours/minutes in a single shot so we don't kick off two redundant timers.
|
||||
/// </summary>
|
||||
public void ApplyTimedPreset(uint hours, uint minutes)
|
||||
{
|
||||
try
|
||||
{
|
||||
_suppressApply = true;
|
||||
IntervalHours = hours;
|
||||
IntervalMinutes = minutes;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_suppressApply = false;
|
||||
}
|
||||
|
||||
// Setting Mode raises OnModeChanged which applies the timed interval; if we're
|
||||
// already in timed mode that path doesn't run, so apply directly.
|
||||
if (Mode != AwakeMode.TIMED)
|
||||
{
|
||||
Mode = AwakeMode.TIMED;
|
||||
}
|
||||
else
|
||||
{
|
||||
ApplyTimedFromInterval();
|
||||
UpdateStatusText();
|
||||
}
|
||||
|
||||
UpdateCountdown();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies the custom "Until date" selection from the date/time pickers. Switches into
|
||||
/// expirable mode if necessary; if already expirable, re-applies so edited values take effect.
|
||||
/// </summary>
|
||||
public void ApplyUntilDate()
|
||||
{
|
||||
if (Mode != AwakeMode.EXPIRABLE)
|
||||
{
|
||||
Mode = AwakeMode.EXPIRABLE;
|
||||
}
|
||||
else
|
||||
{
|
||||
ApplyExpirableFromPickers();
|
||||
UpdateStatusText();
|
||||
}
|
||||
|
||||
UpdateCountdown();
|
||||
}
|
||||
|
||||
// The duration the user has selected in the flyout but may not have applied yet (the
|
||||
// Start button applies it). Kept on the view model so the launch page and the custom-time
|
||||
// page share one source of truth across frame navigation.
|
||||
public FlyoutSelectionKind PendingSelection { get; set; } = FlyoutSelectionKind.Timed;
|
||||
|
||||
public uint PendingMinutes { get; set; } = 60;
|
||||
|
||||
public bool PendingCustomIsUntil { get; set; }
|
||||
|
||||
public int PendingProcessId { get; set; }
|
||||
|
||||
public string PendingProcessName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Records a custom duration / until-date selection (without starting it) and updates the
|
||||
/// Custom card label. The session only starts when the user presses Start, except when a
|
||||
/// session is already running, in which case it is re-applied live.
|
||||
/// </summary>
|
||||
public void SetPendingCustom(bool isUntil)
|
||||
{
|
||||
PendingSelection = FlyoutSelectionKind.Custom;
|
||||
PendingCustomIsUntil = isUntil;
|
||||
CustomCardText = FormatCustomCardLabel();
|
||||
ApplyPendingIfActive();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records a "while app runs" selection (without starting it) and updates the While-app
|
||||
/// card label/icon. Starts on Start, or re-applies live if a session is already running.
|
||||
/// </summary>
|
||||
public void SetPendingApp(int processId, string processName, Microsoft.UI.Xaml.Media.ImageSource? icon)
|
||||
{
|
||||
PendingSelection = FlyoutSelectionKind.WhileApp;
|
||||
PendingProcessId = processId;
|
||||
PendingProcessName = processName ?? string.Empty;
|
||||
WhileAppCardText = string.IsNullOrEmpty(processName) ? Resources.AWAKE_FLYOUT_CARD_WHILE_APP : processName;
|
||||
WhileAppCardIcon = icon;
|
||||
ApplyPendingIfActive();
|
||||
}
|
||||
|
||||
private string FormatCustomCardLabel()
|
||||
{
|
||||
if (PendingCustomIsUntil)
|
||||
{
|
||||
var target = new DateTimeOffset(
|
||||
ExpirationDate.Year,
|
||||
ExpirationDate.Month,
|
||||
ExpirationDate.Day,
|
||||
ExpirationTime.Hours,
|
||||
ExpirationTime.Minutes,
|
||||
0,
|
||||
DateTimeOffset.Now.Offset);
|
||||
|
||||
string when = target.LocalDateTime.Date == DateTime.Now.Date
|
||||
? target.ToString("t", CultureInfo.CurrentCulture)
|
||||
: target.ToString("g", CultureInfo.CurrentCulture);
|
||||
|
||||
return string.Format(CultureInfo.CurrentCulture, CustomUntilCardFormat, when);
|
||||
}
|
||||
|
||||
return FormatInterval(IntervalHours, IntervalMinutes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Realigns <see cref="PendingSelection"/> with the running mode so the launch page
|
||||
/// highlights the matching card after a refresh or a fresh open. Non-preset durations and
|
||||
/// expirations map to the Custom card; a process binding maps to the While-app card.
|
||||
/// </summary>
|
||||
public void SyncPendingFromMode()
|
||||
{
|
||||
switch (Mode)
|
||||
{
|
||||
case AwakeMode.INDEFINITE when IsProcessBound:
|
||||
PendingSelection = FlyoutSelectionKind.WhileApp;
|
||||
PendingProcessName = BoundAppName;
|
||||
PendingProcessId = ProcessIdOrZero();
|
||||
if (!string.IsNullOrEmpty(BoundAppName))
|
||||
{
|
||||
WhileAppCardText = BoundAppName;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case AwakeMode.INDEFINITE:
|
||||
PendingSelection = FlyoutSelectionKind.Forever;
|
||||
break;
|
||||
|
||||
case AwakeMode.EXPIRABLE:
|
||||
PendingSelection = FlyoutSelectionKind.Custom;
|
||||
PendingCustomIsUntil = true;
|
||||
CustomCardText = FormatCustomCardLabel();
|
||||
break;
|
||||
|
||||
case AwakeMode.TIMED:
|
||||
uint minutes = (IntervalHours * 60) + IntervalMinutes;
|
||||
if (minutes is 30 or 60 or 120)
|
||||
{
|
||||
PendingSelection = FlyoutSelectionKind.Timed;
|
||||
PendingMinutes = minutes;
|
||||
}
|
||||
else
|
||||
{
|
||||
PendingSelection = FlyoutSelectionKind.Custom;
|
||||
PendingCustomIsUntil = false;
|
||||
CustomCardText = FormatInterval(IntervalHours, IntervalMinutes);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
PendingSelection = FlyoutSelectionKind.Timed;
|
||||
PendingMinutes = 60;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static int ProcessIdOrZero() => Manager.IsProcessBound ? Manager.ProcessId : 0;
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes only the Custom sub-mode (duration vs. until-date) from the running session so
|
||||
/// that reopening the custom page lands on the tab that matches the active mode. Leaves the
|
||||
/// flag untouched when nothing is running, preserving the user's last in-flyout choice.
|
||||
/// </summary>
|
||||
public void RefreshPendingCustomSubMode()
|
||||
{
|
||||
switch (Mode)
|
||||
{
|
||||
case AwakeMode.EXPIRABLE:
|
||||
PendingCustomIsUntil = true;
|
||||
break;
|
||||
case AwakeMode.TIMED:
|
||||
PendingCustomIsUntil = false;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies the pending selection, starting (or restarting) a keep-awake session.
|
||||
/// </summary>
|
||||
public void ApplyPendingSelection()
|
||||
{
|
||||
switch (PendingSelection)
|
||||
{
|
||||
case FlyoutSelectionKind.Forever:
|
||||
Mode = AwakeMode.INDEFINITE;
|
||||
break;
|
||||
|
||||
case FlyoutSelectionKind.WhileApp:
|
||||
if (PendingProcessId != 0)
|
||||
{
|
||||
ApplyProcessBinding(PendingProcessId, PendingProcessName);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case FlyoutSelectionKind.Custom when PendingCustomIsUntil:
|
||||
ApplyUntilDate();
|
||||
break;
|
||||
|
||||
case FlyoutSelectionKind.Custom:
|
||||
ApplyTimedPreset(IntervalHours, IntervalMinutes);
|
||||
break;
|
||||
|
||||
default:
|
||||
ApplyTimedPreset(PendingMinutes / 60, PendingMinutes % 60);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Re-applies the pending selection only when a session is already running, so changing
|
||||
/// the selection updates the live session without forcing a stop and restart.
|
||||
/// </summary>
|
||||
public void ApplyPendingIfActive()
|
||||
{
|
||||
if (Mode != AwakeMode.PASSIVE)
|
||||
{
|
||||
ApplyPendingSelection();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Binds keep-awake to a running process: keep the system awake while the target app
|
||||
/// runs and automatically revert to passive when it exits. Delegates to
|
||||
/// <see cref="Manager.SetProcessBoundKeepAwake"/> (same mechanism as the CLI <c>--pid</c>
|
||||
/// path). <see cref="Manager.ModeChanged"/> triggers a <see cref="Refresh"/>, which reads
|
||||
/// the bound-process state back into the bindable properties.
|
||||
/// </summary>
|
||||
public void ApplyProcessBinding(int processId, string appName)
|
||||
{
|
||||
try
|
||||
{
|
||||
Manager.SetProcessBoundKeepAwake(processId, appName, KeepDisplayOn);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to bind keep-awake to process {processId}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <see cref="Manager.ModeStartedAt"/> / <see cref="Manager.ExpireAt"/>. Intended to be
|
||||
/// called once per second by the flyout while it is visible.
|
||||
/// </summary>
|
||||
public void UpdateCountdown()
|
||||
{
|
||||
bool countdownMode = Mode == AwakeMode.TIMED || Mode == AwakeMode.EXPIRABLE;
|
||||
|
||||
if (countdownMode && Manager.ExpireAt > DateTimeOffset.Now)
|
||||
{
|
||||
TimeSpan remaining = Manager.ExpireAt - DateTimeOffset.Now;
|
||||
|
||||
CountdownTime = FormatRemaining(remaining);
|
||||
OffAtText = FormatAwakeUntil(Manager.ExpireAt);
|
||||
OffAtVisibility = Microsoft.UI.Xaml.Visibility.Visible;
|
||||
}
|
||||
else if (Mode == AwakeMode.INDEFINITE)
|
||||
{
|
||||
// No finite end: surface the infinity glyph instead of a countdown.
|
||||
CountdownTime = "\u221E";
|
||||
OffAtText = IsProcessBound
|
||||
? string.Format(CultureInfo.CurrentCulture, AwakeWhileAppFormat, BoundAppName)
|
||||
: Resources.AWAKE_FLYOUT_AWAKE_INDEFINITELY;
|
||||
OffAtVisibility = Microsoft.UI.Xaml.Visibility.Visible;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Passive (off) or an already-expired session.
|
||||
CountdownTime = Resources.AWAKE_FLYOUT_MODE_OFF;
|
||||
OffAtText = string.Empty;
|
||||
OffAtVisibility = Microsoft.UI.Xaml.Visibility.Collapsed;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Formats the big gauge value using compact unit suffixes (e.g. "5h 10m 10s"), showing
|
||||
/// only the relevant units so it stays readable and visibly ticks down.
|
||||
/// </summary>
|
||||
private static string FormatRemaining(TimeSpan remaining)
|
||||
{
|
||||
if (remaining.TotalDays >= 1)
|
||||
{
|
||||
return string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"{0}d {1}h",
|
||||
(int)remaining.TotalDays,
|
||||
remaining.Hours);
|
||||
}
|
||||
|
||||
if (remaining.TotalHours >= 1)
|
||||
{
|
||||
return string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"{0}h {1}m {2}s",
|
||||
(int)remaining.TotalHours,
|
||||
remaining.Minutes,
|
||||
remaining.Seconds);
|
||||
}
|
||||
|
||||
if (remaining.TotalMinutes >= 1)
|
||||
{
|
||||
return string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"{0}m {1}s",
|
||||
remaining.Minutes,
|
||||
remaining.Seconds);
|
||||
}
|
||||
|
||||
return string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"{0}s",
|
||||
remaining.Seconds);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the "Awake until …" line shown beneath the gauge. Uses a short time for sessions
|
||||
/// ending today and a full date+time otherwise so multi-day sessions are unambiguous.
|
||||
/// </summary>
|
||||
private static string FormatAwakeUntil(DateTimeOffset expireAt)
|
||||
{
|
||||
string when = expireAt.LocalDateTime.Date == DateTime.Now.Date
|
||||
? expireAt.ToString("t", CultureInfo.CurrentCulture)
|
||||
: expireAt.ToString("g", CultureInfo.CurrentCulture);
|
||||
|
||||
return string.Format(CultureInfo.CurrentCulture, AwakeUntilFormat, when);
|
||||
}
|
||||
|
||||
private void ApplyMode(AwakeMode mode)
|
||||
{
|
||||
try
|
||||
{
|
||||
switch (mode)
|
||||
{
|
||||
case AwakeMode.PASSIVE:
|
||||
Manager.SetPassiveKeepAwake();
|
||||
break;
|
||||
|
||||
case AwakeMode.INDEFINITE:
|
||||
Manager.SetIndefiniteKeepAwake(KeepDisplayOn);
|
||||
break;
|
||||
|
||||
case AwakeMode.TIMED:
|
||||
ApplyTimedFromInterval();
|
||||
break;
|
||||
|
||||
case AwakeMode.EXPIRABLE:
|
||||
ApplyExpirableFromPickers();
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"AwakeFlyoutViewModel.ApplyMode failed: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyExpirableFromPickers()
|
||||
{
|
||||
var target = new DateTimeOffset(
|
||||
ExpirationDate.Year,
|
||||
ExpirationDate.Month,
|
||||
ExpirationDate.Day,
|
||||
ExpirationTime.Hours,
|
||||
ExpirationTime.Minutes,
|
||||
0,
|
||||
DateTimeOffset.Now.Offset);
|
||||
|
||||
if (target <= DateTimeOffset.Now)
|
||||
{
|
||||
Logger.LogWarning("Expirable target is in the past; ignoring.");
|
||||
return;
|
||||
}
|
||||
|
||||
Manager.SetExpirableKeepAwake(target, KeepDisplayOn);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void OpenSettings()
|
||||
{
|
||||
try
|
||||
{
|
||||
SettingsDeepLink.OpenSettings(SettingsDeepLink.SettingsWindow.Awake);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"OpenSettings failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateStatusText()
|
||||
{
|
||||
StatusText = Mode switch
|
||||
{
|
||||
AwakeMode.INDEFINITE => IsProcessBound
|
||||
? string.Format(CultureInfo.CurrentCulture, AwakeWhileAppFormat, BoundAppName)
|
||||
: Resources.AWAKE_FLYOUT_STATUS_INDEFINITE,
|
||||
AwakeMode.TIMED => string.Format(
|
||||
CultureInfo.CurrentCulture,
|
||||
StatusTimedFormat,
|
||||
FormatInterval(IntervalHours, IntervalMinutes)),
|
||||
AwakeMode.EXPIRABLE => string.Format(
|
||||
CultureInfo.CurrentCulture,
|
||||
StatusExpirableFormat,
|
||||
Manager.ExpireAt.ToString("g", CultureInfo.CurrentCulture)),
|
||||
_ => Resources.AWAKE_FLYOUT_STATUS_OFF,
|
||||
};
|
||||
}
|
||||
|
||||
private static string FormatInterval(uint hours, uint minutes)
|
||||
{
|
||||
if (hours > 0 && minutes > 0)
|
||||
{
|
||||
return string.Format(CultureInfo.CurrentCulture, "{0}h {1}m", hours, minutes);
|
||||
}
|
||||
|
||||
if (hours > 0)
|
||||
{
|
||||
return string.Format(CultureInfo.CurrentCulture, "{0}h", hours);
|
||||
}
|
||||
|
||||
return string.Format(CultureInfo.CurrentCulture, "{0}m", minutes);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Manager.ModeChanged -= OnManagerModeChanged;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,32 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
|
||||
<asmv3:application>
|
||||
<asmv3:windowsSettings>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
|
||||
<assemblyIdentity version="1.0.0.0" name="PowerToys.Awake.app"/>
|
||||
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
|
||||
<security>
|
||||
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<!-- Windows 10 -->
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||
<!-- Windows 11 -->
|
||||
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />
|
||||
</application>
|
||||
</compatibility>
|
||||
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<!-- The combination of below two tags have the following effect:
|
||||
1) Per-Monitor for >= Windows 10 Anniversary Update
|
||||
2) System < Windows 10 Anniversary Update
|
||||
-->
|
||||
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/PM</dpiAware>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||
</asmv3:windowsSettings>
|
||||
</asmv3:application>
|
||||
</assembly>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2, PerMonitor</dpiAwareness>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
</assembly>
|
||||
|
||||
@@ -57,7 +57,7 @@ private:
|
||||
unsigned long powertoys_pid = GetCurrentProcessId();
|
||||
|
||||
std::wstring executable_args = L"--use-pt-config --pid " + std::to_wstring(powertoys_pid);
|
||||
std::wstring application_path = L"PowerToys.Awake.exe";
|
||||
std::wstring application_path = L"WinUI3Apps\\PowerToys.Awake.exe";
|
||||
std::wstring full_command_path = application_path + L" " + executable_args.data();
|
||||
Logger::trace(L"PowerToys Awake launching with parameters: " + executable_args);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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! };
|
||||
|
||||
|
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 |
@@ -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..];
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||