mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-27 12:54:25 +08:00
feat.数字时钟,白板功能修复
This commit is contained in:
@@ -6,6 +6,7 @@ public static class BuiltInComponentIds
|
||||
public const string DesktopClock = "DesktopClock";
|
||||
public const string DesktopWeatherClock = "DesktopWeatherClock";
|
||||
public const string DesktopWorldClock = "DesktopWorldClock";
|
||||
public const string DesktopStandbyDigitalClock = "DesktopStandbyDigitalClock";
|
||||
public const string DesktopTimer = "DesktopTimer";
|
||||
public const string DesktopWeather = "DesktopWeather";
|
||||
public const string DesktopHourlyWeather = "DesktopHourlyWeather";
|
||||
|
||||
@@ -57,6 +57,15 @@ public sealed class ComponentRegistry
|
||||
MinHeightCells: 2,
|
||||
AllowStatusBarPlacement: false,
|
||||
AllowDesktopPlacement: true),
|
||||
new DesktopComponentDefinition(
|
||||
BuiltInComponentIds.DesktopStandbyDigitalClock,
|
||||
"StandBy Clock",
|
||||
"Clock",
|
||||
"Clock",
|
||||
MinWidthCells: 4,
|
||||
MinHeightCells: 2,
|
||||
AllowStatusBarPlacement: false,
|
||||
AllowDesktopPlacement: true),
|
||||
new DesktopComponentDefinition(
|
||||
BuiltInComponentIds.DesktopTimer,
|
||||
"Timer",
|
||||
|
||||
@@ -5,6 +5,7 @@ using Avalonia.Animation.Easings;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Layout;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Threading;
|
||||
|
||||
namespace LanMountainDesktop.DesktopEditing;
|
||||
|
||||
@@ -51,15 +52,18 @@ internal sealed class DesktopEditGhostView : Border
|
||||
ClipToBounds = true;
|
||||
RenderTransformOrigin = new RelativePoint(0.5, 0.5, RelativeUnit.Relative);
|
||||
RenderTransform = _scaleTransform;
|
||||
Transitions = new Transitions
|
||||
if (Dispatcher.UIThread.CheckAccess())
|
||||
{
|
||||
CreateOpacityTransition(FastDuration)
|
||||
};
|
||||
_scaleTransform.Transitions = new Transitions
|
||||
{
|
||||
CreateScaleTransition(ScaleTransform.ScaleXProperty, FastDuration),
|
||||
CreateScaleTransition(ScaleTransform.ScaleYProperty, FastDuration)
|
||||
};
|
||||
Transitions = new Transitions
|
||||
{
|
||||
CreateOpacityTransition(FastDuration)
|
||||
};
|
||||
_scaleTransform.Transitions = new Transitions
|
||||
{
|
||||
CreateScaleTransition(ScaleTransform.ScaleXProperty, FastDuration),
|
||||
CreateScaleTransition(ScaleTransform.ScaleYProperty, FastDuration)
|
||||
};
|
||||
}
|
||||
|
||||
_accentDot = new Border
|
||||
{
|
||||
|
||||
@@ -66,8 +66,11 @@ internal sealed class DesktopEditOverlayPresenter
|
||||
CornerRadius = new CornerRadius(22),
|
||||
Opacity = 0,
|
||||
RenderTransformOrigin = new RelativePoint(0.5, 0.5, RelativeUnit.Relative),
|
||||
RenderTransform = _candidateScale,
|
||||
Transitions = new Transitions
|
||||
RenderTransform = _candidateScale
|
||||
};
|
||||
if (Dispatcher.UIThread.CheckAccess())
|
||||
{
|
||||
_candidateOutline.Transitions = new Transitions
|
||||
{
|
||||
new DoubleTransition
|
||||
{
|
||||
@@ -75,13 +78,13 @@ internal sealed class DesktopEditOverlayPresenter
|
||||
Duration = FastDuration,
|
||||
Easing = StandardEasing
|
||||
}
|
||||
}
|
||||
};
|
||||
_candidateScale.Transitions = new Transitions
|
||||
{
|
||||
CreateScaleTransition(ScaleTransform.ScaleXProperty, FastDuration),
|
||||
CreateScaleTransition(ScaleTransform.ScaleYProperty, FastDuration)
|
||||
};
|
||||
};
|
||||
_candidateScale.Transitions = new Transitions
|
||||
{
|
||||
CreateScaleTransition(ScaleTransform.ScaleXProperty, FastDuration),
|
||||
CreateScaleTransition(ScaleTransform.ScaleYProperty, FastDuration)
|
||||
};
|
||||
}
|
||||
|
||||
_candidateOutline.SetValue(Panel.ZIndexProperty, 0);
|
||||
_ghostView.SetValue(Panel.ZIndexProperty, 1);
|
||||
@@ -99,10 +102,13 @@ internal sealed class DesktopEditOverlayPresenter
|
||||
}
|
||||
};
|
||||
|
||||
_root.Transitions = new Transitions
|
||||
if (Dispatcher.UIThread.CheckAccess())
|
||||
{
|
||||
CreateOpacityTransition(FastDuration)
|
||||
};
|
||||
_root.Transitions = new Transitions
|
||||
{
|
||||
CreateOpacityTransition(FastDuration)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public Control Root => _root;
|
||||
|
||||
@@ -12,6 +12,8 @@ public interface IAirAppLauncherService
|
||||
{
|
||||
void OpenWorldClock(string? sourcePlacementId);
|
||||
|
||||
void OpenWorldClock(string sourceComponentId, string? sourcePlacementId);
|
||||
|
||||
void OpenWhiteboard(string componentId, string? sourcePlacementId);
|
||||
}
|
||||
|
||||
@@ -24,11 +26,25 @@ internal sealed class AirAppLauncherService : IAirAppLauncherService
|
||||
|
||||
public void OpenWorldClock(string? sourcePlacementId)
|
||||
{
|
||||
_ = OpenAsync(WorldClockAppId, BuiltInComponentIds.DesktopWorldClock, sourcePlacementId);
|
||||
OpenWorldClock(BuiltInComponentIds.DesktopWorldClock, sourcePlacementId);
|
||||
}
|
||||
|
||||
public void OpenWorldClock(string sourceComponentId, string? sourcePlacementId)
|
||||
{
|
||||
var componentId = string.IsNullOrWhiteSpace(sourceComponentId)
|
||||
? BuiltInComponentIds.DesktopWorldClock
|
||||
: sourceComponentId.Trim();
|
||||
AppLogger.Info(
|
||||
"AirAppLauncher",
|
||||
$"World Clock Air APP requested. ComponentId='{componentId}'; PlacementId='{sourcePlacementId ?? string.Empty}'.");
|
||||
_ = OpenAsync(WorldClockAppId, componentId, sourcePlacementId);
|
||||
}
|
||||
|
||||
public void OpenWhiteboard(string componentId, string? sourcePlacementId)
|
||||
{
|
||||
AppLogger.Info(
|
||||
"AirAppLauncher",
|
||||
$"Whiteboard Air APP requested. ComponentId='{componentId}'; PlacementId='{sourcePlacementId ?? string.Empty}'.");
|
||||
_ = OpenAsync(WhiteboardAppId, componentId, sourcePlacementId);
|
||||
}
|
||||
|
||||
|
||||
@@ -53,12 +53,20 @@ public static class AppDataPathProvider
|
||||
private static string? ResolveDataRootFromArgs(string[] args)
|
||||
{
|
||||
const string prefix = "--data-root=";
|
||||
foreach (var arg in args)
|
||||
for (var index = 0; index < args.Length; index++)
|
||||
{
|
||||
var arg = args[index];
|
||||
if (arg.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return arg[prefix.Length..];
|
||||
}
|
||||
|
||||
if (string.Equals(arg, "--data-root", StringComparison.OrdinalIgnoreCase) &&
|
||||
index + 1 < args.Length &&
|
||||
!args[index + 1].StartsWith("--", StringComparison.Ordinal))
|
||||
{
|
||||
return args[index + 1];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Collections.Generic;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Shapes;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Styling;
|
||||
using Avalonia.Threading;
|
||||
@@ -15,7 +16,7 @@ using LanMountainDesktop.Services.Settings;
|
||||
|
||||
namespace LanMountainDesktop.Views.Components;
|
||||
|
||||
public partial class AnalogClockWidget : UserControl, IDesktopComponentWidget, ITimeZoneAwareComponentWidget, IComponentPlacementContextAware
|
||||
public partial class AnalogClockWidget : UserControl, IDesktopComponentWidget, ITimeZoneAwareComponentWidget, IComponentPlacementContextAware, IComponentRuntimeContextAware
|
||||
{
|
||||
private static readonly IReadOnlyDictionary<string, string> ZhCityNames =
|
||||
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
@@ -60,6 +61,7 @@ public partial class AnalogClockWidget : UserControl, IDesktopComponentWidget, I
|
||||
private const double Center = DialSize / 2;
|
||||
private string _componentId = BuiltInComponentIds.DesktopClock;
|
||||
private string _placementId = string.Empty;
|
||||
private DesktopComponentRenderMode _renderMode = DesktopComponentRenderMode.Live;
|
||||
|
||||
private ISettingsService _settingsService = HostSettingsFacadeProvider.GetOrCreate().Settings;
|
||||
private readonly LocalizationService _localizationService = new();
|
||||
@@ -83,6 +85,7 @@ public partial class AnalogClockWidget : UserControl, IDesktopComponentWidget, I
|
||||
AttachedToVisualTree += OnAttachedToVisualTree;
|
||||
DetachedFromVisualTree += OnDetachedFromVisualTree;
|
||||
SizeChanged += OnSizeChanged;
|
||||
PointerReleased += OnPointerReleased;
|
||||
|
||||
InitializeDialIfNeeded();
|
||||
InitializeHandsIfNeeded();
|
||||
@@ -126,6 +129,15 @@ public partial class AnalogClockWidget : UserControl, IDesktopComponentWidget, I
|
||||
RefreshFromSettings();
|
||||
}
|
||||
|
||||
public void SetComponentRuntimeContext(DesktopComponentRuntimeContext context)
|
||||
{
|
||||
_componentId = string.IsNullOrWhiteSpace(context.ComponentId)
|
||||
? BuiltInComponentIds.DesktopClock
|
||||
: context.ComponentId.Trim();
|
||||
_placementId = context.PlacementId?.Trim() ?? string.Empty;
|
||||
_renderMode = context.RenderMode;
|
||||
}
|
||||
|
||||
private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
InitializeDialIfNeeded();
|
||||
@@ -156,6 +168,23 @@ public partial class AnalogClockWidget : UserControl, IDesktopComponentWidget, I
|
||||
UpdateClock();
|
||||
}
|
||||
|
||||
private void OnPointerReleased(object? sender, PointerReleasedEventArgs e)
|
||||
{
|
||||
_ = sender;
|
||||
if (e.InitialPressMouseButton != MouseButton.Left ||
|
||||
_renderMode != DesktopComponentRenderMode.Live ||
|
||||
!string.Equals(_componentId, BuiltInComponentIds.DesktopClock, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
AppLogger.Info(
|
||||
"AirAppLauncher",
|
||||
$"Analog clock component clicked. ComponentId='{_componentId}'; PlacementId='{_placementId}'.");
|
||||
AirAppLauncherServiceProvider.GetOrCreate().OpenWorldClock(_componentId, _placementId);
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void InitializeDialIfNeeded()
|
||||
{
|
||||
if (_dialInitialized)
|
||||
|
||||
@@ -340,6 +340,10 @@ public sealed class DesktopComponentRuntimeRegistry
|
||||
BuiltInComponentIds.DesktopWorldClock,
|
||||
"component.world_clock",
|
||||
() => new WorldClockWidget()),
|
||||
new DesktopComponentRuntimeRegistration(
|
||||
BuiltInComponentIds.DesktopStandbyDigitalClock,
|
||||
"component.standby_digital_clock",
|
||||
() => new StandbyDigitalClockWidget()),
|
||||
new DesktopComponentRuntimeRegistration(
|
||||
BuiltInComponentIds.DesktopTimer,
|
||||
"component.desktop_timer",
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d"
|
||||
d:DesignWidth="384"
|
||||
d:DesignHeight="192"
|
||||
x:Class="LanMountainDesktop.Views.Components.StandbyDigitalClockWidget">
|
||||
|
||||
<Border x:Name="RootBorder"
|
||||
CornerRadius="{DynamicResource DesignCornerRadiusComponent}"
|
||||
ClipToBounds="True"
|
||||
Padding="16">
|
||||
<Viewbox Stretch="Uniform">
|
||||
<Grid Width="420"
|
||||
Height="210">
|
||||
<!-- Irregular free-form digit layout: each digit has unique vertical offset,
|
||||
horizontal offset, and rotation — like stickers casually placed -->
|
||||
<StackPanel VerticalAlignment="Center"
|
||||
HorizontalAlignment="Center"
|
||||
Orientation="Horizontal"
|
||||
Spacing="0">
|
||||
|
||||
<!-- H1 digit — tilted left, shifted right and up -->
|
||||
<Border x:Name="H1Clip"
|
||||
ClipToBounds="True"
|
||||
Width="88"
|
||||
Height="130"
|
||||
Margin="6,-10,0,0">
|
||||
<Border.RenderTransform>
|
||||
<RotateTransform Angle="-4" />
|
||||
</Border.RenderTransform>
|
||||
<StackPanel x:Name="H1Stack"
|
||||
Orientation="Vertical">
|
||||
<TextBlock x:Name="H1Text"
|
||||
Text="0"
|
||||
FontSize="120"
|
||||
FontWeight="Bold"
|
||||
Foreground="{DynamicResource AdaptiveAccentBrush}"
|
||||
Width="88"
|
||||
Height="130"
|
||||
TextAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
LineHeight="130" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- H2 digit — tilted right, shifted left and down -->
|
||||
<Border x:Name="H2Clip"
|
||||
ClipToBounds="True"
|
||||
Width="88"
|
||||
Height="130"
|
||||
Margin="-2,10,0,0">
|
||||
<Border.RenderTransform>
|
||||
<RotateTransform Angle="3" />
|
||||
</Border.RenderTransform>
|
||||
<StackPanel x:Name="H2Stack"
|
||||
Orientation="Vertical">
|
||||
<TextBlock x:Name="H2Text"
|
||||
Text="0"
|
||||
FontSize="120"
|
||||
FontWeight="Bold"
|
||||
Foreground="{DynamicResource AdaptiveAccentBrush}"
|
||||
Width="88"
|
||||
Height="130"
|
||||
TextAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
LineHeight="130" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Colon — slightly rotated, a bit lower -->
|
||||
<TextBlock x:Name="ColonText"
|
||||
Text=":"
|
||||
FontSize="100"
|
||||
FontWeight="Bold"
|
||||
Foreground="{DynamicResource AdaptiveAccentBrush}"
|
||||
Width="36"
|
||||
Height="130"
|
||||
TextAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
LineHeight="130"
|
||||
Margin="0,8,0,0">
|
||||
<TextBlock.RenderTransform>
|
||||
<RotateTransform Angle="-1" />
|
||||
</TextBlock.RenderTransform>
|
||||
</TextBlock>
|
||||
|
||||
<!-- M1 digit — slightly tilted left, shifted right and slightly up -->
|
||||
<Border x:Name="M1Clip"
|
||||
ClipToBounds="True"
|
||||
Width="88"
|
||||
Height="130"
|
||||
Margin="4,-3,0,0">
|
||||
<Border.RenderTransform>
|
||||
<RotateTransform Angle="-2" />
|
||||
</Border.RenderTransform>
|
||||
<StackPanel x:Name="M1Stack"
|
||||
Orientation="Vertical">
|
||||
<TextBlock x:Name="M1Text"
|
||||
Text="0"
|
||||
FontSize="120"
|
||||
FontWeight="Bold"
|
||||
Foreground="{DynamicResource AdaptiveAccentBrush}"
|
||||
Width="88"
|
||||
Height="130"
|
||||
TextAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
LineHeight="130" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- M2 digit — tilted right more, shifted left and down -->
|
||||
<Border x:Name="M2Clip"
|
||||
ClipToBounds="True"
|
||||
Width="88"
|
||||
Height="130"
|
||||
Margin="-2,12,0,0">
|
||||
<Border.RenderTransform>
|
||||
<RotateTransform Angle="5" />
|
||||
</Border.RenderTransform>
|
||||
<StackPanel x:Name="M2Stack"
|
||||
Orientation="Vertical">
|
||||
<TextBlock x:Name="M2Text"
|
||||
Text="0"
|
||||
FontSize="120"
|
||||
FontWeight="Bold"
|
||||
Foreground="{DynamicResource AdaptiveAccentBrush}"
|
||||
Width="88"
|
||||
Height="130"
|
||||
TextAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
LineHeight="130" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Date line -->
|
||||
<TextBlock x:Name="DateTextBlock"
|
||||
FontSize="15"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource AdaptiveTextMutedBrush}"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Bottom"
|
||||
Margin="0,0,0,6" />
|
||||
</Grid>
|
||||
</Viewbox>
|
||||
</Border>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,489 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using Avalonia;
|
||||
using Avalonia.Animation;
|
||||
using Avalonia.Animation.Easings;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Styling;
|
||||
using Avalonia.Threading;
|
||||
using LanMountainDesktop.ComponentSystem;
|
||||
using LanMountainDesktop.Models;
|
||||
using LanMountainDesktop.PluginSdk;
|
||||
using LanMountainDesktop.Services;
|
||||
using LanMountainDesktop.Services.Settings;
|
||||
using LanMountainDesktop.Theme;
|
||||
|
||||
namespace LanMountainDesktop.Views.Components;
|
||||
|
||||
public partial class StandbyDigitalClockWidget : UserControl,
|
||||
IDesktopComponentWidget,
|
||||
ITimeZoneAwareComponentWidget,
|
||||
IComponentPlacementContextAware,
|
||||
IComponentRuntimeContextAware
|
||||
{
|
||||
private const double BaseCellSize = 48d;
|
||||
private const int BaseWidthCells = 4;
|
||||
private const int BaseHeightCells = 2;
|
||||
private const double DigitHeight = 130d;
|
||||
|
||||
private readonly DispatcherTimer _timer = new()
|
||||
{
|
||||
Interval = TimeSpan.FromSeconds(1)
|
||||
};
|
||||
|
||||
private string _componentId = BuiltInComponentIds.DesktopStandbyDigitalClock;
|
||||
private string _placementId = string.Empty;
|
||||
private DesktopComponentRenderMode _renderMode = DesktopComponentRenderMode.Live;
|
||||
private ISettingsService _settingsService = HostSettingsFacadeProvider.GetOrCreate().Settings;
|
||||
private readonly LocalizationService _localizationService = new();
|
||||
private TimeZoneService? _timeZoneService;
|
||||
private double _currentCellSize = 48;
|
||||
private TimeZoneInfo _clockTimeZone = WorldClockTimeZoneCatalog.ResolveTimeZoneOrLocal("China Standard Time");
|
||||
private string _languageCode = "zh-CN";
|
||||
private string? _componentColorScheme;
|
||||
|
||||
// Track previous digit chars to detect changes
|
||||
private char _prevH1, _prevH2, _prevM1, _prevM2;
|
||||
private bool _colonVisible = true;
|
||||
private bool? _isNightModeApplied;
|
||||
|
||||
// Digit state: track the current TextBlock for each digit position
|
||||
private TextBlock _h1Current, _h2Current, _m1Current, _m2Current;
|
||||
private bool _isAnimatingH1, _isAnimatingH2, _isAnimatingM1, _isAnimatingM2;
|
||||
|
||||
public StandbyDigitalClockWidget()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
_h1Current = H1Text;
|
||||
_h2Current = H2Text;
|
||||
_m1Current = M1Text;
|
||||
_m2Current = M2Text;
|
||||
|
||||
_timer.Tick += OnTimerTick;
|
||||
AttachedToVisualTree += OnAttachedToVisualTree;
|
||||
DetachedFromVisualTree += OnDetachedFromVisualTree;
|
||||
PointerReleased += OnPointerReleased;
|
||||
|
||||
LoadClockSettings();
|
||||
InitializeDigitsWithoutAnimation();
|
||||
}
|
||||
|
||||
// ─── Interface implementations ───────────────────────────────
|
||||
|
||||
public void ApplyCellSize(double cellSize)
|
||||
{
|
||||
_currentCellSize = Math.Max(1, cellSize);
|
||||
var mainRectangleCornerRadius = ComponentChromeCornerRadiusHelper.ResolveMainRectangleRadius();
|
||||
RootBorder.CornerRadius = mainRectangleCornerRadius;
|
||||
|
||||
var scale = ResolveScale();
|
||||
RootBorder.Padding = new Thickness(Math.Clamp(14 * scale, 6, 28));
|
||||
ApplyModeVisualIfNeeded();
|
||||
}
|
||||
|
||||
public void SetTimeZoneService(TimeZoneService timeZoneService)
|
||||
{
|
||||
ClearTimeZoneService();
|
||||
_timeZoneService = timeZoneService;
|
||||
_timeZoneService.TimeZoneChanged += OnTimeZoneChanged;
|
||||
UpdateClock();
|
||||
}
|
||||
|
||||
public void ClearTimeZoneService()
|
||||
{
|
||||
if (_timeZoneService is null) return;
|
||||
_timeZoneService.TimeZoneChanged -= OnTimeZoneChanged;
|
||||
_timeZoneService = null;
|
||||
}
|
||||
|
||||
public void SetComponentPlacementContext(string componentId, string? placementId)
|
||||
{
|
||||
_componentId = string.IsNullOrWhiteSpace(componentId)
|
||||
? BuiltInComponentIds.DesktopStandbyDigitalClock
|
||||
: componentId.Trim();
|
||||
_placementId = placementId?.Trim() ?? string.Empty;
|
||||
LoadClockSettings();
|
||||
UpdateClock();
|
||||
}
|
||||
|
||||
public void SetComponentRuntimeContext(DesktopComponentRuntimeContext context)
|
||||
{
|
||||
_componentId = string.IsNullOrWhiteSpace(context.ComponentId)
|
||||
? BuiltInComponentIds.DesktopStandbyDigitalClock
|
||||
: context.ComponentId.Trim();
|
||||
_placementId = context.PlacementId?.Trim() ?? string.Empty;
|
||||
_renderMode = context.RenderMode;
|
||||
}
|
||||
|
||||
// ─── Lifecycle ──────────────────────────────────────────────
|
||||
|
||||
private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
LoadClockSettings();
|
||||
InitializeDigitsWithoutAnimation();
|
||||
_timer.Start();
|
||||
}
|
||||
|
||||
private void OnDetachedFromVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
_timer.Stop();
|
||||
}
|
||||
|
||||
private void OnTimerTick(object? sender, EventArgs e)
|
||||
{
|
||||
UpdateClock();
|
||||
}
|
||||
|
||||
private void OnTimeZoneChanged(object? sender, EventArgs e)
|
||||
{
|
||||
LoadClockSettings();
|
||||
InitializeDigitsWithoutAnimation();
|
||||
}
|
||||
|
||||
private void OnPointerReleased(object? sender, PointerReleasedEventArgs e)
|
||||
{
|
||||
if (e.InitialPressMouseButton != MouseButton.Left ||
|
||||
_renderMode != DesktopComponentRenderMode.Live)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
AppLogger.Info(
|
||||
"AirAppLauncher",
|
||||
$"StandBy digital clock clicked. ComponentId='{_componentId}'; PlacementId='{_placementId}'.");
|
||||
AirAppLauncherServiceProvider.GetOrCreate().OpenWorldClock(_componentId, _placementId);
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
// ─── Clock update ───────────────────────────────────────────
|
||||
|
||||
private void InitializeDigitsWithoutAnimation()
|
||||
{
|
||||
var now = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, _clockTimeZone);
|
||||
var h = now.Hour.ToString("D2", CultureInfo.InvariantCulture);
|
||||
var m = now.Minute.ToString("D2", CultureInfo.InvariantCulture);
|
||||
|
||||
_prevH1 = h[0]; _prevH2 = h[1];
|
||||
_prevM1 = m[0]; _prevM2 = m[1];
|
||||
|
||||
H1Text.Text = h[0].ToString();
|
||||
H2Text.Text = h[1].ToString();
|
||||
M1Text.Text = m[0].ToString();
|
||||
M2Text.Text = m[1].ToString();
|
||||
|
||||
_h1Current = H1Text;
|
||||
_h2Current = H2Text;
|
||||
_m1Current = M1Text;
|
||||
_m2Current = M2Text;
|
||||
|
||||
UpdateDateText(now);
|
||||
ApplyModeVisualIfNeeded();
|
||||
}
|
||||
|
||||
private void UpdateClock()
|
||||
{
|
||||
var now = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, _clockTimeZone);
|
||||
var h = now.Hour.ToString("D2", CultureInfo.InvariantCulture);
|
||||
var m = now.Minute.ToString("D2", CultureInfo.InvariantCulture);
|
||||
|
||||
ApplyModeVisualIfNeeded();
|
||||
|
||||
// Detect digit changes and animate
|
||||
if (h[0] != _prevH1) AnimateDigit(H1Stack, _h1Current, h[0], _isAnimatingH1, value => _h1Current = value, value => _isAnimatingH1 = value);
|
||||
if (h[1] != _prevH2) AnimateDigit(H2Stack, _h2Current, h[1], _isAnimatingH2, value => _h2Current = value, value => _isAnimatingH2 = value);
|
||||
if (m[0] != _prevM1) AnimateDigit(M1Stack, _m1Current, m[0], _isAnimatingM1, value => _m1Current = value, value => _isAnimatingM1 = value);
|
||||
if (m[1] != _prevM2) AnimateDigit(M2Stack, _m2Current, m[1], _isAnimatingM2, value => _m2Current = value, value => _isAnimatingM2 = value);
|
||||
|
||||
_prevH1 = h[0]; _prevH2 = h[1];
|
||||
_prevM1 = m[0]; _prevM2 = m[1];
|
||||
|
||||
// Colon breathing
|
||||
ToggleColonOpacity();
|
||||
|
||||
// Date
|
||||
UpdateDateText(now);
|
||||
}
|
||||
|
||||
// ─── Digit scroll animation ─────────────────────────────────
|
||||
|
||||
private void AnimateDigit(
|
||||
StackPanel stack,
|
||||
TextBlock currentTextBlock,
|
||||
char newDigit,
|
||||
bool isAnimating,
|
||||
Action<TextBlock> setCurrentTextBlock,
|
||||
Action<bool> setAnimating)
|
||||
{
|
||||
if (isAnimating)
|
||||
{
|
||||
// If still animating, just set the text directly and skip animation
|
||||
currentTextBlock.Text = newDigit.ToString();
|
||||
return;
|
||||
}
|
||||
|
||||
setAnimating(true);
|
||||
var oldText = currentTextBlock;
|
||||
|
||||
var newTextBlock = CreateDigitTextBlock(newDigit);
|
||||
stack.Children.Add(newTextBlock);
|
||||
|
||||
// Apply TranslateTransform with transition for smooth animation
|
||||
var transform = new TranslateTransform { Y = 0 };
|
||||
stack.RenderTransform = transform;
|
||||
stack.RenderTransformOrigin = new RelativePoint(0, 0, RelativeUnit.Relative);
|
||||
|
||||
stack.Transitions = new Transitions
|
||||
{
|
||||
new DoubleTransition
|
||||
{
|
||||
Property = TranslateTransform.YProperty,
|
||||
Duration = FluttermotionToken.Standard,
|
||||
Easing = new CubicEaseOut()
|
||||
}
|
||||
};
|
||||
|
||||
// Trigger the animation: slide up by one digit height
|
||||
transform.Y = -DigitHeight;
|
||||
|
||||
// After animation completes, clean up
|
||||
var cleanupTimer = new DispatcherTimer
|
||||
{
|
||||
Interval = FluttermotionToken.Standard + TimeSpan.FromMilliseconds(20)
|
||||
};
|
||||
cleanupTimer.Tick += (_, _) =>
|
||||
{
|
||||
cleanupTimer.Stop();
|
||||
|
||||
// Remove transitions to prevent re-animation on reset
|
||||
stack.Transitions = null;
|
||||
|
||||
// Remove the old TextBlock and reset position
|
||||
stack.Children.Remove(oldText);
|
||||
transform.Y = 0;
|
||||
|
||||
setCurrentTextBlock(newTextBlock);
|
||||
setAnimating(false);
|
||||
};
|
||||
cleanupTimer.Start();
|
||||
}
|
||||
|
||||
private TextBlock CreateDigitTextBlock(char digit)
|
||||
{
|
||||
var accentBrush = ResolveAccentBrush();
|
||||
return new TextBlock
|
||||
{
|
||||
Text = digit.ToString(),
|
||||
FontSize = 120,
|
||||
FontWeight = FontWeight.Bold,
|
||||
Foreground = accentBrush,
|
||||
Width = 88,
|
||||
Height = DigitHeight,
|
||||
TextAlignment = TextAlignment.Center,
|
||||
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center,
|
||||
LineHeight = DigitHeight
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Colon breathing ────────────────────────────────────────
|
||||
|
||||
private void ToggleColonOpacity()
|
||||
{
|
||||
_colonVisible = !_colonVisible;
|
||||
|
||||
if (ColonText.Transitions is null)
|
||||
{
|
||||
ColonText.Transitions = new Transitions
|
||||
{
|
||||
new DoubleTransition
|
||||
{
|
||||
Property = OpacityProperty,
|
||||
Duration = TimeSpan.FromMilliseconds(400),
|
||||
Easing = new CubicEaseInOut()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
ColonText.Opacity = _colonVisible ? 1.0 : 0.25;
|
||||
}
|
||||
|
||||
// ─── Color system ───────────────────────────────────────────
|
||||
|
||||
private IBrush ResolveAccentBrush()
|
||||
{
|
||||
var useMonetColor = ComponentColorSchemeHelper.ShouldUseMonetColor(
|
||||
_componentColorScheme,
|
||||
ComponentColorSchemeHelper.GetCurrentGlobalThemeColorMode());
|
||||
|
||||
var isNight = ResolveIsNightMode();
|
||||
|
||||
if (useMonetColor)
|
||||
{
|
||||
// Use the Monet accent brush from dynamic resources
|
||||
if (this.TryFindResource("AdaptiveAccentBrush", out var accentRes) && accentRes is IBrush accentBrush)
|
||||
{
|
||||
return accentBrush;
|
||||
}
|
||||
|
||||
// Fallback: compute from SystemAccentColor
|
||||
if (this.TryFindResource("SystemAccentColor", out var sysAccent) && sysAccent is Color sysColor)
|
||||
{
|
||||
return new SolidColorBrush(isNight ? Lighten(sysColor, 0.3) : sysColor);
|
||||
}
|
||||
}
|
||||
|
||||
// Native / fallback: warm orange-red accent (iPhone StandBy inspired)
|
||||
return isNight
|
||||
? CreateBrush("#FF8A65")
|
||||
: CreateBrush("#E84530");
|
||||
}
|
||||
|
||||
private static Color Lighten(Color color, double amount)
|
||||
{
|
||||
var r = (byte)Math.Min(255, color.R + (255 - color.R) * amount);
|
||||
var g = (byte)Math.Min(255, color.G + (255 - color.G) * amount);
|
||||
var b = (byte)Math.Min(255, color.B + (255 - color.B) * amount);
|
||||
return new Color(color.A, r, g, b);
|
||||
}
|
||||
|
||||
// ─── Night / Day mode ───────────────────────────────────────
|
||||
|
||||
private void ApplyModeVisualIfNeeded()
|
||||
{
|
||||
var isNightMode = ResolveIsNightMode();
|
||||
if (_isNightModeApplied.HasValue && _isNightModeApplied.Value == isNightMode)
|
||||
return;
|
||||
|
||||
_isNightModeApplied = isNightMode;
|
||||
ApplyModeVisual(isNightMode);
|
||||
}
|
||||
|
||||
private void ApplyModeVisual(bool isNightMode)
|
||||
{
|
||||
RootBorder.Background = isNightMode
|
||||
? CreateLinearGradientBrush("#1F2C4B", "#131B33")
|
||||
: CreateLinearGradientBrush("#EEF2FA", "#E7ECF6");
|
||||
|
||||
var accentBrush = ResolveAccentBrush();
|
||||
|
||||
// Update current digit TextBlocks with accent color
|
||||
foreach (var tb in new[] { _h1Current, _h2Current, _m1Current, _m2Current })
|
||||
{
|
||||
if (tb is not null) tb.Foreground = accentBrush;
|
||||
}
|
||||
|
||||
// Also update the named XAML TextBlocks (in case they haven't been replaced yet)
|
||||
H1Text.Foreground = accentBrush;
|
||||
H2Text.Foreground = accentBrush;
|
||||
M1Text.Foreground = accentBrush;
|
||||
M2Text.Foreground = accentBrush;
|
||||
|
||||
ColonText.Foreground = accentBrush;
|
||||
|
||||
// Date text uses muted brush from dynamic resource
|
||||
if (this.TryFindResource("AdaptiveTextMutedBrush", out var mutedRes) && mutedRes is IBrush mutedBrush)
|
||||
{
|
||||
DateTextBlock.Foreground = mutedBrush;
|
||||
}
|
||||
else
|
||||
{
|
||||
DateTextBlock.Foreground = CreateBrush(isNightMode ? "#7E8593" : "#7E8593");
|
||||
}
|
||||
}
|
||||
|
||||
private bool ResolveIsNightMode()
|
||||
{
|
||||
if (ActualThemeVariant == ThemeVariant.Dark) return true;
|
||||
if (ActualThemeVariant == ThemeVariant.Light) return false;
|
||||
|
||||
if (this.TryFindResource("AdaptiveSurfaceBaseBrush", out var value) &&
|
||||
value is ISolidColorBrush solidBrush)
|
||||
{
|
||||
return CalculateRelativeLuminance(solidBrush.Color) < 0.45;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static double CalculateRelativeLuminance(Color color)
|
||||
{
|
||||
static double ToLinear(double channel)
|
||||
{
|
||||
return channel <= 0.03928
|
||||
? channel / 12.92
|
||||
: Math.Pow((channel + 0.055) / 1.055, 2.4);
|
||||
}
|
||||
|
||||
var r = ToLinear(color.R / 255d);
|
||||
var g = ToLinear(color.G / 255d);
|
||||
var b = ToLinear(color.B / 255d);
|
||||
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
||||
}
|
||||
|
||||
// ─── Date text ──────────────────────────────────────────────
|
||||
|
||||
private void UpdateDateText(DateTime now)
|
||||
{
|
||||
var culture = string.Equals(_languageCode, "zh-CN", StringComparison.OrdinalIgnoreCase)
|
||||
? new CultureInfo("zh-CN")
|
||||
: CultureInfo.CurrentUICulture;
|
||||
|
||||
var dateStr = now.ToString("M", culture);
|
||||
var dayStr = now.ToString("dddd", culture);
|
||||
DateTextBlock.Text = $"{dateStr} {dayStr}";
|
||||
}
|
||||
|
||||
// ─── Settings ───────────────────────────────────────────────
|
||||
|
||||
private void LoadClockSettings()
|
||||
{
|
||||
var appSnapshot = _settingsService.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App);
|
||||
var componentSnapshot = _settingsService.LoadSnapshot<ComponentSettingsSnapshot>(
|
||||
SettingsScope.ComponentInstance,
|
||||
_componentId,
|
||||
_placementId);
|
||||
|
||||
_languageCode = _localizationService.NormalizeLanguageCode(appSnapshot.LanguageCode);
|
||||
|
||||
var configuredTimeZoneId = string.IsNullOrWhiteSpace(componentSnapshot.DesktopClockTimeZoneId)
|
||||
? "China Standard Time"
|
||||
: componentSnapshot.DesktopClockTimeZoneId.Trim();
|
||||
|
||||
_clockTimeZone = WorldClockTimeZoneCatalog.ResolveTimeZoneOrLocal(configuredTimeZoneId);
|
||||
_componentColorScheme = componentSnapshot.ColorSchemeSource;
|
||||
}
|
||||
|
||||
// ─── Scaling ────────────────────────────────────────────────
|
||||
|
||||
private double ResolveScale()
|
||||
{
|
||||
var cellScale = Math.Clamp(_currentCellSize / BaseCellSize, 0.60, 1.90);
|
||||
var heightScale = Bounds.Height > 1 ? Math.Clamp(Bounds.Height / (BaseCellSize * BaseHeightCells), 0.58, 2.0) : 1;
|
||||
var widthScale = Bounds.Width > 1 ? Math.Clamp(Bounds.Width / (BaseCellSize * BaseWidthCells), 0.58, 2.0) : 1;
|
||||
return Math.Clamp(Math.Min(cellScale, Math.Min(heightScale, widthScale) * 1.05), 0.58, 1.95);
|
||||
}
|
||||
|
||||
// ─── Brush helpers ──────────────────────────────────────────
|
||||
|
||||
private static IBrush CreateBrush(string colorHex)
|
||||
{
|
||||
return new SolidColorBrush(Color.Parse(colorHex));
|
||||
}
|
||||
|
||||
private static IBrush CreateLinearGradientBrush(string fromColorHex, string toColorHex)
|
||||
{
|
||||
return new LinearGradientBrush
|
||||
{
|
||||
StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative),
|
||||
EndPoint = new RelativePoint(1, 1, RelativeUnit.Relative),
|
||||
GradientStops = new GradientStops
|
||||
{
|
||||
new GradientStop(Color.Parse(fromColorHex), 0),
|
||||
new GradientStop(Color.Parse(toColorHex), 1)
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,8 @@ public enum WhiteboardWidgetSurfaceMode
|
||||
AirApp
|
||||
}
|
||||
|
||||
internal readonly record struct WhiteboardViewportSizeResolution(Size Size, string Source, bool IsFallback);
|
||||
|
||||
public partial class WhiteboardWidget : UserControl, IDesktopComponentWidget, IComponentPlacementContextAware, IDisposable
|
||||
{
|
||||
private enum WhiteboardToolMode
|
||||
@@ -73,6 +75,9 @@ public partial class WhiteboardWidget : UserControl, IDesktopComponentWidget, IC
|
||||
private int _noteLoadRevision;
|
||||
private WhiteboardWidgetSurfaceMode _surfaceMode = WhiteboardWidgetSurfaceMode.Component;
|
||||
private Action? _airAppCloseAction;
|
||||
private bool _isViewportLayoutSyncQueued;
|
||||
private Size _lastSynchronizedViewportSize = default;
|
||||
private string _lastViewportSizeSource = string.Empty;
|
||||
private bool _disposed;
|
||||
|
||||
public WhiteboardWidget()
|
||||
@@ -95,6 +100,8 @@ public partial class WhiteboardWidget : UserControl, IDesktopComponentWidget, IC
|
||||
AttachedToVisualTree += OnAttachedToVisualTree;
|
||||
DetachedFromVisualTree += OnDetachedFromVisualTree;
|
||||
SizeChanged += OnSizeChanged;
|
||||
ViewportRoot.SizeChanged += OnViewportRootSizeChanged;
|
||||
ColorPickerPopup.Closed += OnColorPickerPopupClosed;
|
||||
ActualThemeVariantChanged += OnActualThemeVariantChanged;
|
||||
_noteSaveTimer.Tick += OnNoteSaveTimerTick;
|
||||
|
||||
@@ -114,7 +121,7 @@ public partial class WhiteboardWidget : UserControl, IDesktopComponentWidget, IC
|
||||
if (InkColorPicker is not null)
|
||||
{
|
||||
InkColorPicker.Color = new Color(
|
||||
_selectedInkColor.Alpha,
|
||||
byte.MaxValue,
|
||||
_selectedInkColor.Red,
|
||||
_selectedInkColor.Green,
|
||||
_selectedInkColor.Blue);
|
||||
@@ -131,8 +138,8 @@ public partial class WhiteboardWidget : UserControl, IDesktopComponentWidget, IC
|
||||
private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
ApplyThemeVisual(force: true);
|
||||
EnsureLogicalCanvasSize(expandToViewport: true);
|
||||
SetViewportState(_viewportState, queueSave: false);
|
||||
SynchronizeViewportLayout("attached");
|
||||
QueueViewportLayoutSync("attached-loaded");
|
||||
SchedulePersistedNoteLoad();
|
||||
}
|
||||
|
||||
@@ -144,8 +151,14 @@ public partial class WhiteboardWidget : UserControl, IDesktopComponentWidget, IC
|
||||
private void OnSizeChanged(object? sender, SizeChangedEventArgs e)
|
||||
{
|
||||
ApplyCellSize(_currentCellSize);
|
||||
EnsureLogicalCanvasSize(expandToViewport: true);
|
||||
SetViewportState(_viewportState, queueSave: false);
|
||||
QueueViewportLayoutSync("widget-size-changed");
|
||||
}
|
||||
|
||||
private void OnViewportRootSizeChanged(object? sender, SizeChangedEventArgs e)
|
||||
{
|
||||
_ = sender;
|
||||
_ = e;
|
||||
QueueViewportLayoutSync("viewport-root-size-changed");
|
||||
}
|
||||
|
||||
private void OnActualThemeVariantChanged(object? sender, EventArgs e)
|
||||
@@ -253,7 +266,7 @@ public partial class WhiteboardWidget : UserControl, IDesktopComponentWidget, IC
|
||||
if (InkColorPicker is not null)
|
||||
{
|
||||
InkColorPicker.Color = new Color(
|
||||
_selectedInkColor.Alpha,
|
||||
byte.MaxValue,
|
||||
_selectedInkColor.Red,
|
||||
_selectedInkColor.Green,
|
||||
_selectedInkColor.Blue);
|
||||
@@ -350,6 +363,8 @@ public partial class WhiteboardWidget : UserControl, IDesktopComponentWidget, IC
|
||||
_disposed = true;
|
||||
_noteSaveTimer.Stop();
|
||||
_noteSaveTimer.Tick -= OnNoteSaveTimerTick;
|
||||
ViewportRoot.SizeChanged -= OnViewportRootSizeChanged;
|
||||
ColorPickerPopup.Closed -= OnColorPickerPopupClosed;
|
||||
InkCanvas.StrokeCollected -= OnInkCanvasStrokeCollected;
|
||||
InkCanvas.StrokeErased -= OnInkCanvasStrokeErased;
|
||||
InkCanvas.PointerReleased -= OnInkCanvasPointerReleased;
|
||||
@@ -461,7 +476,7 @@ public partial class WhiteboardWidget : UserControl, IDesktopComponentWidget, IC
|
||||
|
||||
private void SetInkColor(SKColor color)
|
||||
{
|
||||
_selectedInkColor = color;
|
||||
_selectedInkColor = NormalizeInkColor(color);
|
||||
if (_toolMode == WhiteboardToolMode.Pen)
|
||||
{
|
||||
InkCanvas.AvaloniaSkiaInkCanvas.Settings.InkColor = _selectedInkColor;
|
||||
@@ -478,6 +493,16 @@ public partial class WhiteboardWidget : UserControl, IDesktopComponentWidget, IC
|
||||
}
|
||||
}
|
||||
|
||||
internal static SKColor ToOpaqueInkColor(Color color)
|
||||
{
|
||||
return new SKColor(color.R, color.G, color.B, byte.MaxValue);
|
||||
}
|
||||
|
||||
private static SKColor NormalizeInkColor(SKColor color)
|
||||
{
|
||||
return new SKColor(color.Red, color.Green, color.Blue, byte.MaxValue);
|
||||
}
|
||||
|
||||
private void RefreshToolButtonVisuals()
|
||||
{
|
||||
var isNightMode = _isNightModeApplied ?? ResolveIsNightMode();
|
||||
@@ -543,12 +568,53 @@ public partial class WhiteboardWidget : UserControl, IDesktopComponentWidget, IC
|
||||
|
||||
private void OnColorPickerColorChanged(object? sender, ColorChangedEventArgs e)
|
||||
{
|
||||
var color = e.NewColor;
|
||||
var skColor = new SKColor(color.R, color.G, color.B, color.A);
|
||||
var skColor = ToOpaqueInkColor(e.NewColor);
|
||||
_isUserCustomColor = skColor != SKColors.Black && skColor != SKColors.White;
|
||||
SetInkColor(skColor);
|
||||
}
|
||||
|
||||
private void OnColorPickerPopupClosed(object? sender, EventArgs e)
|
||||
{
|
||||
_ = sender;
|
||||
_ = e;
|
||||
RestoreInkInputAfterToolPopup("color-popup-closed");
|
||||
}
|
||||
|
||||
private void RestoreInkInputAfterToolPopup(string reason, int attempt = 0)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ClearPanZoomPointers();
|
||||
try
|
||||
{
|
||||
SetToolMode(WhiteboardToolMode.Pen);
|
||||
SynchronizeViewportLayout(reason);
|
||||
InkCanvas.Focus(NavigationMethod.Unspecified);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
if (attempt >= 3)
|
||||
{
|
||||
AppLogger.Warn(
|
||||
"Whiteboard",
|
||||
$"Ink input restore gave up because the ink canvas stayed in input processing. Reason='{reason}'.",
|
||||
ex);
|
||||
return;
|
||||
}
|
||||
|
||||
AppLogger.Warn(
|
||||
"Whiteboard",
|
||||
$"Ink input restore was deferred because the ink canvas is still processing input. Reason='{reason}'.",
|
||||
ex);
|
||||
Dispatcher.UIThread.Post(
|
||||
() => RestoreInkInputAfterToolPopup($"{reason}-deferred", attempt + 1),
|
||||
DispatcherPriority.Background);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnInkThicknessSliderValueChanged(object? sender, RangeBaseValueChangedEventArgs e)
|
||||
{
|
||||
SetInkThickness((float)e.NewValue);
|
||||
@@ -1188,6 +1254,54 @@ public partial class WhiteboardWidget : UserControl, IDesktopComponentWidget, IC
|
||||
SetLogicalCanvasSize(normalizedCanvasSize);
|
||||
}
|
||||
|
||||
private void QueueViewportLayoutSync(string reason)
|
||||
{
|
||||
if (_disposed || _isViewportLayoutSyncQueued)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_isViewportLayoutSyncQueued = true;
|
||||
Dispatcher.UIThread.Post(
|
||||
() =>
|
||||
{
|
||||
_isViewportLayoutSyncQueued = false;
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SynchronizeViewportLayout(reason);
|
||||
},
|
||||
DispatcherPriority.Loaded);
|
||||
}
|
||||
|
||||
private void SynchronizeViewportLayout(string reason)
|
||||
{
|
||||
var resolution = ResolveCurrentViewportSize();
|
||||
var previousCanvasSize = _logicalCanvasSize;
|
||||
|
||||
EnsureLogicalCanvasSize(expandToViewport: true);
|
||||
SetViewportState(_viewportState, queueSave: false);
|
||||
|
||||
var canvasExpanded =
|
||||
_logicalCanvasSize.Width > previousCanvasSize.Width + 0.5d ||
|
||||
_logicalCanvasSize.Height > previousCanvasSize.Height + 0.5d;
|
||||
var sourceChanged = !string.Equals(_lastViewportSizeSource, resolution.Source, StringComparison.Ordinal);
|
||||
var viewportChanged = !AreSizesClose(_lastSynchronizedViewportSize, resolution.Size);
|
||||
if (canvasExpanded || sourceChanged || viewportChanged)
|
||||
{
|
||||
AppLogger.Info(
|
||||
"Whiteboard",
|
||||
$"Viewport synchronized. ComponentId='{_componentId}'; PlacementId='{_placementId}'; Reason='{reason}'; " +
|
||||
$"ViewportSize='{resolution.Size.Width:0.##}x{resolution.Size.Height:0.##}'; ViewportSource='{resolution.Source}'; " +
|
||||
$"CanvasSize='{_logicalCanvasSize.Width:0.##}x{_logicalCanvasSize.Height:0.##}'; SurfaceMode='{_surfaceMode}'.");
|
||||
}
|
||||
|
||||
_lastSynchronizedViewportSize = resolution.Size;
|
||||
_lastViewportSizeSource = resolution.Source;
|
||||
}
|
||||
|
||||
private void SetLogicalCanvasSize(Size canvasSize)
|
||||
{
|
||||
_logicalCanvasSize = WhiteboardViewportHelper.NormalizeSize(canvasSize);
|
||||
@@ -1197,14 +1311,53 @@ public partial class WhiteboardWidget : UserControl, IDesktopComponentWidget, IC
|
||||
|
||||
private Size GetViewportSize()
|
||||
{
|
||||
var width = CanvasBorder.Bounds.Width > 1d
|
||||
? CanvasBorder.Bounds.Width
|
||||
: Math.Max(1d, _currentCellSize * _baseWidthCells);
|
||||
var height = CanvasBorder.Bounds.Height > 1d
|
||||
? CanvasBorder.Bounds.Height
|
||||
: Math.Max(1d, Bounds.Height > 1d ? Bounds.Height : _currentCellSize * Math.Max(2, _baseWidthCells));
|
||||
return ResolveCurrentViewportSize().Size;
|
||||
}
|
||||
|
||||
return WhiteboardViewportHelper.NormalizeSize(new Size(width, height));
|
||||
private WhiteboardViewportSizeResolution ResolveCurrentViewportSize()
|
||||
{
|
||||
return ResolveViewportSize(
|
||||
ViewportRoot.Bounds.Size,
|
||||
CanvasBorder.Bounds.Size,
|
||||
Bounds.Size,
|
||||
_currentCellSize,
|
||||
_baseWidthCells);
|
||||
}
|
||||
|
||||
internal static WhiteboardViewportSizeResolution ResolveViewportSize(
|
||||
Size viewportRootSize,
|
||||
Size canvasBorderSize,
|
||||
Size widgetSize,
|
||||
double currentCellSize,
|
||||
int baseWidthCells)
|
||||
{
|
||||
if (HasUsableSize(viewportRootSize))
|
||||
{
|
||||
return new WhiteboardViewportSizeResolution(
|
||||
WhiteboardViewportHelper.NormalizeSize(viewportRootSize),
|
||||
"ViewportRoot",
|
||||
IsFallback: false);
|
||||
}
|
||||
|
||||
if (HasUsableSize(canvasBorderSize))
|
||||
{
|
||||
return new WhiteboardViewportSizeResolution(
|
||||
WhiteboardViewportHelper.NormalizeSize(canvasBorderSize),
|
||||
"CanvasBorder",
|
||||
IsFallback: false);
|
||||
}
|
||||
|
||||
var normalizedCellSize = Math.Max(1d, currentCellSize);
|
||||
var normalizedBaseWidthCells = Math.Max(1, baseWidthCells);
|
||||
var width = normalizedCellSize * normalizedBaseWidthCells;
|
||||
var height = HasUsableLength(widgetSize.Height)
|
||||
? widgetSize.Height
|
||||
: normalizedCellSize * Math.Max(2, normalizedBaseWidthCells);
|
||||
|
||||
return new WhiteboardViewportSizeResolution(
|
||||
WhiteboardViewportHelper.NormalizeSize(new Size(width, height)),
|
||||
"Fallback",
|
||||
IsFallback: true);
|
||||
}
|
||||
|
||||
private void SetViewportState(WhiteboardViewportState nextState, bool queueSave)
|
||||
@@ -1238,6 +1391,23 @@ public partial class WhiteboardWidget : UserControl, IDesktopComponentWidget, IC
|
||||
Math.Abs(first.Offset.Y - second.Offset.Y) <= tolerance;
|
||||
}
|
||||
|
||||
private static bool AreSizesClose(Size first, Size second)
|
||||
{
|
||||
const double tolerance = 0.5d;
|
||||
return Math.Abs(first.Width - second.Width) <= tolerance &&
|
||||
Math.Abs(first.Height - second.Height) <= tolerance;
|
||||
}
|
||||
|
||||
private static bool HasUsableSize(Size size)
|
||||
{
|
||||
return HasUsableLength(size.Width) && HasUsableLength(size.Height);
|
||||
}
|
||||
|
||||
private static bool HasUsableLength(double value)
|
||||
{
|
||||
return double.IsFinite(value) && value > 1d;
|
||||
}
|
||||
|
||||
private WhiteboardNoteSnapshot BuildNoteSnapshot()
|
||||
{
|
||||
EnsureLogicalCanvasSize(expandToViewport: true);
|
||||
|
||||
@@ -342,7 +342,10 @@ public partial class WorldClockWidget : UserControl,
|
||||
return;
|
||||
}
|
||||
|
||||
AirAppLauncherServiceProvider.GetOrCreate().OpenWorldClock(_placementId);
|
||||
AppLogger.Info(
|
||||
"AirAppLauncher",
|
||||
$"World clock component clicked. ComponentId='{_componentId}'; PlacementId='{_placementId}'.");
|
||||
AirAppLauncherServiceProvider.GetOrCreate().OpenWorldClock(_componentId, _placementId);
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -2391,9 +2391,10 @@ public partial class MainWindow : Window
|
||||
new ComponentScaleRule(WidthUnit: 2, HeightUnit: 1, MinScale: 2));
|
||||
}
|
||||
|
||||
if (string.Equals(componentId, BuiltInComponentIds.DesktopWorldClock, StringComparison.OrdinalIgnoreCase))
|
||||
if (string.Equals(componentId, BuiltInComponentIds.DesktopWorldClock, StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(componentId, BuiltInComponentIds.DesktopStandbyDigitalClock, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Keep world clock widget at 2:1 ratio: 4x2, 6x3, 8x4...
|
||||
// Keep world clock / StandBy digital clock widget at 2:1 ratio: 4x2, 6x3, 8x4...
|
||||
return SnapSpanToScaleRules(
|
||||
span,
|
||||
new ComponentScaleRule(WidthUnit: 2, HeightUnit: 1, MinScale: 2));
|
||||
@@ -2875,7 +2876,6 @@ public partial class MainWindow : Window
|
||||
{
|
||||
if (!_isComponentLibraryOpen)
|
||||
{
|
||||
TryOpenAirAppFromDesktopComponent(sender, e);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2923,29 +2923,6 @@ public partial class MainWindow : Window
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void TryOpenAirAppFromDesktopComponent(object? sender, PointerPressedEventArgs e)
|
||||
{
|
||||
if (HasActiveDesktopEditSession ||
|
||||
DesktopPagesViewport is null ||
|
||||
sender is not Border host ||
|
||||
host.Tag is not string placementId ||
|
||||
!e.GetCurrentPoint(host).Properties.IsLeftButtonPressed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var placement = _desktopComponentPlacements.FirstOrDefault(p =>
|
||||
string.Equals(p.PlacementId, placementId, StringComparison.OrdinalIgnoreCase));
|
||||
if (placement is null ||
|
||||
!string.Equals(placement.ComponentId, BuiltInComponentIds.DesktopWorldClock, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_airAppLauncherService.OpenWorldClock(placement.PlacementId);
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void SetSelectedDesktopComponent(Border? host)
|
||||
{
|
||||
ClearSelectedLauncherTile(refreshTaskbar: false);
|
||||
|
||||
Reference in New Issue
Block a user