diff --git a/FModel/Settings/UserSettings.cs b/FModel/Settings/UserSettings.cs
index d1a9f007..6d3d9d42 100644
--- a/FModel/Settings/UserSettings.cs
+++ b/FModel/Settings/UserSettings.cs
@@ -36,7 +36,8 @@ namespace FModel.Settings
private static bool _bSave = true;
public static void Save()
{
- if (!_bSave || Default == null) return;
+ if (!_bSave || Default == null)
+ return;
Default.PerDirectory[Default.CurrentDir.GameDirectory] = Default.CurrentDir;
File.WriteAllText(FilePath, JsonConvert.SerializeObject(Default, Formatting.Indented));
}
@@ -147,7 +148,7 @@ namespace FModel.Settings
set => SetProperty(ref _isLoggerExpanded, value);
}
- private GridLength _avalonImageSize = new (200);
+ private GridLength _avalonImageSize = new(200);
public GridLength AvalonImageSize
{
get => _avalonImageSize;
@@ -300,7 +301,7 @@ namespace FModel.Settings
set => SetProperty(ref _manualGames, value);
}
- private AuthResponse _lastAuthResponse = new() {AccessToken = "", ExpiresAt = DateTime.Now};
+ private AuthResponse _lastAuthResponse = new() { AccessToken = "", ExpiresAt = DateTime.Now };
public AuthResponse LastAuthResponse
{
get => _lastAuthResponse;
diff --git a/FModel/Views/Resources/Controls/ImagePopout.xaml b/FModel/Views/Resources/Controls/ImagePopout.xaml
index 99847efd..f8044756 100644
--- a/FModel/Views/Resources/Controls/ImagePopout.xaml
+++ b/FModel/Views/Resources/Controls/ImagePopout.xaml
@@ -1,16 +1,14 @@
-
-
-
-
-
+
+
-
-
+
+
-
+
diff --git a/FModel/Views/Resources/Controls/ImagePopout.xaml.cs b/FModel/Views/Resources/Controls/ImagePopout.xaml.cs
index 32287b88..01682f61 100644
--- a/FModel/Views/Resources/Controls/ImagePopout.xaml.cs
+++ b/FModel/Views/Resources/Controls/ImagePopout.xaml.cs
@@ -1,9 +1,12 @@
+using Avalonia.Controls;
+
namespace FModel.Views.Resources.Controls;
-public partial class ImagePopout
+public partial class ImagePopout : Window
{
public ImagePopout()
{
InitializeComponent();
+ MagnifierManager.SetMagnifier(RootPanel, new Magnifier { Radius = 150, ZoomFactor = 0.7 });
}
-}
\ No newline at end of file
+}
diff --git a/FModel/Views/Resources/Controls/Mgn/Magnifier.cs b/FModel/Views/Resources/Controls/Mgn/Magnifier.cs
index b58ce24b..e3fbb830 100644
--- a/FModel/Views/Resources/Controls/Mgn/Magnifier.cs
+++ b/FModel/Views/Resources/Controls/Mgn/Magnifier.cs
@@ -1,176 +1,230 @@
-using System.Windows;
-using System.Windows.Controls;
-using System.Windows.Media;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Media;
namespace FModel.Views.Resources.Controls;
-[TemplatePart(Name = PART_VisualBrush, Type = typeof(VisualBrush))]
public class Magnifier : Control
{
private const double DEFAULT_SIZE = 100d;
- private const string PART_VisualBrush = "PART_VisualBrush";
- private VisualBrush _visualBrush = new();
- public static readonly DependencyProperty FrameTypeProperty =
- DependencyProperty.Register("FrameType", typeof(EFrameType), typeof(Magnifier), new UIPropertyMetadata(EFrameType.Circle, OnFrameTypeChanged));
+ // -----------------------------------------------------------------------
+ // Styled properties
+ // -----------------------------------------------------------------------
+
+ public static readonly StyledProperty FrameTypeProperty =
+ AvaloniaProperty.Register(nameof(FrameType), defaultValue: EFrameType.Circle);
public EFrameType FrameType
{
- get => (EFrameType) GetValue(FrameTypeProperty);
+ get => GetValue(FrameTypeProperty);
set => SetValue(FrameTypeProperty, value);
}
- private static void OnFrameTypeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
- {
- var m = (Magnifier) d;
- m.OnFrameTypeChanged((EFrameType) e.OldValue, (EFrameType) e.NewValue);
- }
-
- protected virtual void OnFrameTypeChanged(EFrameType oldValue, EFrameType newValue)
- {
- UpdateSizeFromRadius();
- }
-
- public static readonly DependencyProperty IsUsingZoomOnMouseWheelProperty =
- DependencyProperty.Register("IsUsingZoomOnMouseWheel", typeof(bool), typeof(Magnifier), new UIPropertyMetadata(true));
+ public static readonly StyledProperty IsUsingZoomOnMouseWheelProperty =
+ AvaloniaProperty.Register(nameof(IsUsingZoomOnMouseWheel), defaultValue: true);
public bool IsUsingZoomOnMouseWheel
{
- get => (bool) GetValue(IsUsingZoomOnMouseWheelProperty);
+ get => GetValue(IsUsingZoomOnMouseWheelProperty);
set => SetValue(IsUsingZoomOnMouseWheelProperty, value);
}
- public bool IsFrozen { get; private set; }
-
- public static readonly DependencyProperty RadiusProperty =
- DependencyProperty.Register("Radius", typeof(double), typeof(Magnifier), new FrameworkPropertyMetadata(DEFAULT_SIZE / 2, OnRadiusPropertyChanged));
+ public static readonly StyledProperty RadiusProperty =
+ AvaloniaProperty.Register(nameof(Radius), defaultValue: DEFAULT_SIZE / 2);
public double Radius
{
- get => (double) GetValue(RadiusProperty);
+ get => GetValue(RadiusProperty);
set => SetValue(RadiusProperty, value);
}
- private static void OnRadiusPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ public static readonly StyledProperty TargetProperty =
+ AvaloniaProperty.Register(nameof(Target));
+ public Control? Target
{
- var m = (Magnifier) d;
- m.OnRadiusChanged(e);
- }
-
- protected virtual void OnRadiusChanged(DependencyPropertyChangedEventArgs e)
- {
- UpdateSizeFromRadius();
- }
-
- public static readonly DependencyProperty TargetProperty = DependencyProperty.Register("Target", typeof(UIElement), typeof(Magnifier));
- public UIElement Target
- {
- get => (UIElement) GetValue(TargetProperty);
+ get => GetValue(TargetProperty);
set => SetValue(TargetProperty, value);
}
- public Rect ViewBox
- {
- get => _visualBrush.Viewbox;
- set => _visualBrush.Viewbox = value;
- }
-
- public static readonly DependencyProperty ZoomFactorProperty =
- DependencyProperty.Register("ZoomFactor", typeof(double), typeof(Magnifier), new FrameworkPropertyMetadata(0.5, OnZoomFactorPropertyChanged), OnValidationCallback);
+ public static readonly StyledProperty ZoomFactorProperty =
+ AvaloniaProperty.Register(nameof(ZoomFactor), defaultValue: 0.5,
+ validate: v => v >= 0);
public double ZoomFactor
{
- get => (double) GetValue(ZoomFactorProperty);
+ get => GetValue(ZoomFactorProperty);
set => SetValue(ZoomFactorProperty, value);
}
- private static bool OnValidationCallback(object baseValue)
- {
- return (double) baseValue >= 0;
- }
-
- private static void OnZoomFactorPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
- {
- var m = (Magnifier) d;
- m.OnZoomFactorChanged(e);
- }
-
- protected virtual void OnZoomFactorChanged(DependencyPropertyChangedEventArgs e)
- {
- UpdateViewBox();
- }
-
- public static readonly DependencyProperty ZoomFactorOnMouseWheelProperty =
- DependencyProperty.Register("ZoomFactorOnMouseWheel", typeof(double), typeof(Magnifier), new FrameworkPropertyMetadata(0.1d, OnZoomFactorOnMouseWheelPropertyChanged), OnZoomFactorOnMouseWheelValidationCallback);
+ public static readonly StyledProperty ZoomFactorOnMouseWheelProperty =
+ AvaloniaProperty.Register(nameof(ZoomFactorOnMouseWheel), defaultValue: 0.1d,
+ validate: v => v >= 0);
public double ZoomFactorOnMouseWheel
{
- get => (double) GetValue(ZoomFactorOnMouseWheelProperty);
+ get => GetValue(ZoomFactorOnMouseWheelProperty);
set => SetValue(ZoomFactorOnMouseWheelProperty, value);
}
- private static bool OnZoomFactorOnMouseWheelValidationCallback(object baseValue)
+ public static readonly StyledProperty BackgroundProperty =
+ AvaloniaProperty.Register(nameof(Background));
+ public IBrush? Background
{
- return (double) baseValue >= 0;
+ get => GetValue(BackgroundProperty);
+ set => SetValue(BackgroundProperty, value);
}
- private static void OnZoomFactorOnMouseWheelPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ public static readonly StyledProperty BorderBrushProperty =
+ AvaloniaProperty.Register(nameof(BorderBrush));
+ public IBrush? BorderBrush
{
- var m = (Magnifier) d;
- m.OnZoomFactorOnMouseWheelChanged(e);
+ get => GetValue(BorderBrushProperty);
+ set => SetValue(BorderBrushProperty, value);
}
- protected virtual void OnZoomFactorOnMouseWheelChanged(DependencyPropertyChangedEventArgs e)
+ public static readonly StyledProperty BorderThicknessProperty =
+ AvaloniaProperty.Register(nameof(BorderThickness));
+ public Thickness BorderThickness
{
+ get => GetValue(BorderThicknessProperty);
+ set => SetValue(BorderThicknessProperty, value);
}
+ // ViewBox is not a styled property — it is driven programmatically by the adorner.
+ public Rect ViewBox { get; set; }
+
+ public bool IsFrozen { get; private set; }
+
+ // VisualBrush paints the magnified region; rebuilt whenever Target changes.
+ private VisualBrush? _visualBrush;
+
+ // -----------------------------------------------------------------------
+ // Static ctor — wire property-changed callbacks
+ // -----------------------------------------------------------------------
static Magnifier()
{
- DefaultStyleKeyProperty.OverrideMetadata(typeof(Magnifier), new FrameworkPropertyMetadata(typeof(Magnifier)));
- HeightProperty.OverrideMetadata(typeof(Magnifier), new FrameworkPropertyMetadata(DEFAULT_SIZE));
- WidthProperty.OverrideMetadata(typeof(Magnifier), new FrameworkPropertyMetadata(DEFAULT_SIZE));
- }
+ FrameTypeProperty.Changed.AddClassHandler((m, _) => m.OnFrameTypeChanged());
+ RadiusProperty.Changed.AddClassHandler((m, _) => m.OnRadiusChanged());
+ ZoomFactorProperty.Changed.AddClassHandler((m, _) => m.UpdateViewBox());
+ // Rebuild the VisualBrush whenever the Target control changes.
+ TargetProperty.Changed.AddClassHandler((m, _) => m.RebuildBrush());
- public Magnifier()
- {
- SizeChanged += OnSizeChangedEvent;
- }
+ // Default Width / Height
+ WidthProperty.OverrideDefaultValue(DEFAULT_SIZE);
+ HeightProperty.OverrideDefaultValue(DEFAULT_SIZE);
- private void OnSizeChangedEvent(object sender, SizeChangedEventArgs e)
- {
- UpdateViewBox();
- }
-
- private void UpdateSizeFromRadius()
- {
- if (FrameType != EFrameType.Circle) return;
-
- var newSize = Radius * 2;
- if (!Helper.AreVirtuallyEqual(Width, newSize))
+ // Class-level handler — must live in static ctor, not the instance ctor, to avoid
+ // registering N handlers for N instances (AddClassHandler is class-wide).
+ BoundsProperty.Changed.AddClassHandler((m, e) =>
{
- Width = newSize;
+ if (e.OldValue is Rect oldBounds && e.NewValue is Rect newBounds && oldBounds.Size == newBounds.Size)
+ return;
+ m.UpdateViewBox(); // UpdateViewBox already calls InvalidateVisual().
+ });
+ }
+
+ public Magnifier() { }
+
+ // -----------------------------------------------------------------------
+ // Overrides
+ // -----------------------------------------------------------------------
+ public override void Render(DrawingContext context)
+ {
+ if (_visualBrush == null || Bounds.Width <= 0 || Bounds.Height <= 0)
+ return;
+
+ var strokeThickness = (BorderThickness.Left + BorderThickness.Top +
+ BorderThickness.Right + BorderThickness.Bottom) / 4;
+ var pen = BorderBrush != null ? new Pen(BorderBrush, strokeThickness) : null;
+
+ if (FrameType == EFrameType.Circle)
+ {
+ var center = new Point(Bounds.Width / 2, Bounds.Height / 2);
+ var rx = Bounds.Width / 2;
+ var ry = Bounds.Height / 2;
+ if (Background != null)
+ context.DrawEllipse(Background, null, center, rx, ry);
+ context.DrawEllipse(_visualBrush, pen, center, rx, ry);
}
-
- if (!Helper.AreVirtuallyEqual(Height, newSize))
+ else
{
- Height = newSize;
+ var rect = new Rect(0, 0, Bounds.Width, Bounds.Height);
+ if (Background != null)
+ context.DrawRectangle(Background, null, rect);
+ context.DrawRectangle(_visualBrush, pen, rect);
}
}
- public override void OnApplyTemplate()
- {
- base.OnApplyTemplate();
-
- var newBrush = GetTemplateChild(PART_VisualBrush) as VisualBrush ?? new VisualBrush();
- newBrush.Viewbox = _visualBrush.Viewbox;
- _visualBrush = newBrush;
- }
-
+ // -----------------------------------------------------------------------
+ // Helpers
+ // -----------------------------------------------------------------------
public void Freeze(bool freeze)
{
IsFrozen = freeze;
}
- private void UpdateViewBox()
+ private void OnFrameTypeChanged()
{
- if (!IsInitialized)
+ UpdateSizeFromRadius();
+ InvalidateVisual();
+ }
+
+ private void OnRadiusChanged()
+ {
+ UpdateSizeFromRadius();
+ }
+
+ private void UpdateSizeFromRadius()
+ {
+ if (FrameType != EFrameType.Circle)
return;
- ViewBox = new Rect(ViewBox.Location, new Size(ActualWidth * ZoomFactor, ActualHeight * ZoomFactor));
+ var newSize = Radius * 2;
+ if (!Helper.AreVirtuallyEqual(Width, newSize))
+ Width = newSize;
+ if (!Helper.AreVirtuallyEqual(Height, newSize))
+ Height = newSize;
}
-}
\ No newline at end of file
+
+ internal void UpdateViewBox()
+ {
+ // Prefer actual layout size; fall back to the Width/Height styled properties
+ // (default 100) so that ViewBox.Size is non-zero even before the first layout
+ // pass. This ensures CalculateViewBoxLocation() can center the viewport under
+ // the cursor on the very first PointerPressed before Bounds are measured.
+ var w = Bounds.Width > 0 ? Bounds.Width : Width;
+ var h = Bounds.Height > 0 ? Bounds.Height : Height;
+ if (w <= 0 || h <= 0)
+ return;
+
+ ViewBox = new Rect(ViewBox.X, ViewBox.Y, w * ZoomFactor, h * ZoomFactor);
+ UpdateBrushViewBox();
+ InvalidateVisual();
+ }
+
+ private void RebuildBrush()
+ {
+ if (Target == null)
+ {
+ _visualBrush = null;
+ InvalidateVisual();
+ return;
+ }
+
+ _visualBrush = new VisualBrush
+ {
+ SourceControl = Target,
+ Stretch = Stretch.Fill,
+ TileMode = TileMode.None,
+ AlignmentX = AlignmentX.Left,
+ AlignmentY = AlignmentY.Top,
+ DestinationRect = new RelativeRect(0, 0, 1, 1, RelativeUnit.Relative),
+ };
+ UpdateBrushViewBox();
+ InvalidateVisual();
+ }
+
+ private void UpdateBrushViewBox()
+ {
+ if (_visualBrush == null)
+ return;
+ // SourceRect maps which region of Target is painted — the magnified "window".
+ _visualBrush.SourceRect = new RelativeRect(ViewBox, RelativeUnit.Absolute);
+ }
+}
diff --git a/FModel/Views/Resources/Controls/Mgn/MagnifierAdorner.cs b/FModel/Views/Resources/Controls/Mgn/MagnifierAdorner.cs
index 04d0bd68..f4790429 100644
--- a/FModel/Views/Resources/Controls/Mgn/MagnifierAdorner.cs
+++ b/FModel/Views/Resources/Controls/Mgn/MagnifierAdorner.cs
@@ -1,85 +1,121 @@
-using System.Windows;
-using System.Windows.Documents;
-using System.Windows.Input;
-using System.Windows.Media;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Input;
namespace FModel.Views.Resources.Controls;
-public class MagnifierAdorner : Adorner
+///
+/// Avalonia replacement for WPF MagnifierAdorner.
+/// Placed in the AdornerLayer as a Canvas that positions the at the cursor.
+///
+public class MagnifierAdorner : Canvas
{
- private Magnifier _magnifier;
- private Point _currentMousePosition;
+ private readonly Control _adornedElement;
+ private readonly Magnifier _magnifier;
+ private Point _currentPointerPosition;
+ private Point _currentElementPosition;
private double _currentZoomFactor;
- public MagnifierAdorner(UIElement element, Magnifier magnifier) : base(element)
+ public MagnifierAdorner(Control adornedElement, Magnifier magnifier)
{
+ _adornedElement = adornedElement;
_magnifier = magnifier;
- _currentZoomFactor = _magnifier.ZoomFactor;
+ _currentZoomFactor = magnifier.ZoomFactor;
+ // The canvas must be transparent to hit-testing so pointer events reach the adorned control.
+ IsHitTestVisible = false;
+ // Start hidden so the first ShowAdorner() call triggers IsVisible false→true,
+ // which fires OnPropertyChanged and subscribes PointerMoved correctly.
+ IsVisible = false;
+
+ Children.Add(_magnifier);
UpdateViewBox();
- AddVisualChild(_magnifier);
- Loaded += (_, _) => InputManager.Current.PostProcessInput += OnProcessInput;
- Unloaded += (_, _) => InputManager.Current.PostProcessInput -= OnProcessInput;
+ // Subscribe to pointer events on the adorned element.
+ // PointerPressed fires first (manager handles ShowAdorner before this handler runs,
+ // so the adorner is already in the visual tree when we call e.GetPosition(this)).
+ // PointerMoved is subscribed/unsubscribed dynamically via OnPropertyChanged(IsVisible).
+ _adornedElement.PointerPressed += OnAdornedElementPointerPressed;
}
- private void OnProcessInput(object sender, ProcessInputEventArgs e)
+ public void Detach()
{
- var pt = Mouse.GetPosition(this);
- if (_currentMousePosition == pt && _magnifier.ZoomFactor == _currentZoomFactor)
+ _adornedElement.PointerPressed -= OnAdornedElementPointerPressed;
+ _adornedElement.PointerMoved -= OnAdornedElementPointerMoved;
+ }
+
+ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
+ {
+ base.OnPropertyChanged(change);
+ if (change.Property == IsVisibleProperty)
+ {
+ if (change.GetNewValue())
+ _adornedElement.PointerMoved += OnAdornedElementPointerMoved;
+ else
+ _adornedElement.PointerMoved -= OnAdornedElementPointerMoved;
+ }
+ }
+
+ private void OnAdornedElementPointerPressed(object? sender, PointerPressedEventArgs e)
+ => HandlePointerEvent(e);
+
+ private void OnAdornedElementPointerMoved(object? sender, PointerEventArgs e)
+ => HandlePointerEvent(e);
+
+ private void HandlePointerEvent(PointerEventArgs e)
+ {
+ // Adorner-canvas-relative position (used for magnifier placement on the canvas).
+ var pt = e.GetPosition(this);
+
+ if (_currentPointerPosition == pt && _magnifier.ZoomFactor == _currentZoomFactor)
return;
if (_magnifier.IsFrozen)
return;
- _currentMousePosition = pt;
+ _currentPointerPosition = pt;
+ // Element-relative position (used for viewbox origin calculation — avoids PointToScreen round-trip).
+ _currentElementPosition = e.GetPosition(_adornedElement);
_currentZoomFactor = _magnifier.ZoomFactor;
UpdateViewBox();
- InvalidateArrange();
+ PositionMagnifier();
}
public void UpdateViewBox()
{
- var viewBoxLocation = CalculateViewBoxLocation();
- _magnifier.ViewBox = new Rect(viewBoxLocation, _magnifier.ViewBox.Size);
+ var location = CalculateViewBoxLocation();
+ _magnifier.ViewBox = new Rect(location, _magnifier.ViewBox.Size);
+ _magnifier.UpdateViewBox();
}
private Point CalculateViewBoxLocation()
{
- double offsetX, offsetY;
- var adorner = Mouse.GetPosition(this);
- var element = Mouse.GetPosition(AdornedElement);
+ // offsetX/offsetY = coordinate delta between adorner-canvas space and adorned-element space.
+ // Both positions come from the same PointerEventArgs so they share the same root transform,
+ // making this DPI-safe without any PointToScreen / PointToClient round-trip.
+ var offsetX = _currentElementPosition.X - _currentPointerPosition.X;
+ var offsetY = _currentElementPosition.Y - _currentPointerPosition.Y;
- offsetX = element.X - adorner.X;
- offsetY = element.Y - adorner.Y;
+ // Account for the target control's offset within its parent coordinate space.
+ Point parentOffset = default;
+ if (_magnifier.Target != null)
+ {
+ var offsetVec = _magnifier.Target.TranslatePoint(default, _adornedElement);
+ if (offsetVec.HasValue)
+ parentOffset = offsetVec.Value;
+ }
- var parentOffsetVector = VisualTreeHelper.GetOffset(_magnifier.Target);
- var parentOffset = new Point(parentOffsetVector.X, parentOffsetVector.Y);
-
- var left = _currentMousePosition.X - (_magnifier.ViewBox.Width / 2 + offsetX) + parentOffset.X;
- var top = _currentMousePosition.Y - (_magnifier.ViewBox.Height / 2 + offsetY) + parentOffset.Y;
+ var left = _currentPointerPosition.X - (_magnifier.ViewBox.Width / 2 + offsetX) + parentOffset.X;
+ var top = _currentPointerPosition.Y - (_magnifier.ViewBox.Height / 2 + offsetY) + parentOffset.Y;
return new Point(left, top);
}
- protected override Visual GetVisualChild(int index)
+ private void PositionMagnifier()
{
- return _magnifier;
+ var x = _currentPointerPosition.X - _magnifier.Width / 2;
+ var y = _currentPointerPosition.Y - _magnifier.Height / 2;
+ SetLeft(_magnifier, x);
+ SetTop(_magnifier, y);
}
-
- protected override int VisualChildrenCount => 1;
-
- protected override Size MeasureOverride(Size constraint)
- {
- _magnifier.Measure(constraint);
- return base.MeasureOverride(constraint);
- }
-
- protected override Size ArrangeOverride(Size finalSize)
- {
- var x = _currentMousePosition.X - _magnifier.Width / 2;
- var y = _currentMousePosition.Y - _magnifier.Height / 2;
- _magnifier.Arrange(new Rect(x, y, _magnifier.Width, _magnifier.Height));
- return base.ArrangeOverride(finalSize);
- }
-}
\ No newline at end of file
+}
diff --git a/FModel/Views/Resources/Controls/Mgn/MagnifierManager.cs b/FModel/Views/Resources/Controls/Mgn/MagnifierManager.cs
index ec0c9b14..eec7d872 100644
--- a/FModel/Views/Resources/Controls/Mgn/MagnifierManager.cs
+++ b/FModel/Views/Resources/Controls/Mgn/MagnifierManager.cs
@@ -1,102 +1,167 @@
using System;
-using System.Windows;
-using System.Windows.Documents;
-using System.Windows.Input;
+using System.Runtime.CompilerServices;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Input;
namespace FModel.Views.Resources.Controls;
-public class MagnifierManager : DependencyObject
+public class MagnifierManager
{
- private MagnifierAdorner _adorner;
- private UIElement _element;
+ private MagnifierAdorner? _adorner;
+ private Control? _element;
- public static readonly DependencyProperty CurrentProperty =
- DependencyProperty.RegisterAttached("Magnifier", typeof(Magnifier), typeof(UIElement), new FrameworkPropertyMetadata(null, OnMagnifierChanged));
+ // Stores active managers so OnMagnifierChanged can detach the old one when the property changes.
+ private static readonly ConditionalWeakTable _managers = new();
- public static void SetMagnifier(UIElement element, Magnifier value)
+ // -----------------------------------------------------------------------
+ // Attached property — usage:
+ // -----------------------------------------------------------------------
+ public static readonly AttachedProperty MagnifierProperty =
+ AvaloniaProperty.RegisterAttached("Magnifier");
+
+ public static void SetMagnifier(Control element, Magnifier? value)
+ => element.SetValue(MagnifierProperty, value);
+
+ public static Magnifier? GetMagnifier(Control element)
+ => element.GetValue(MagnifierProperty);
+
+ static MagnifierManager()
{
- element.SetValue(CurrentProperty, value);
+ MagnifierProperty.Changed.Subscribe(OnMagnifierChanged);
}
- public static Magnifier GetMagnifier(UIElement element)
+ private static void OnMagnifierChanged(AvaloniaPropertyChangedEventArgs e)
{
- return (Magnifier) element.GetValue(CurrentProperty);
- }
+ if (e.Sender is not Control target)
+ throw new ArgumentException("Magnifier can only be attached to a Control.");
- private static void OnMagnifierChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
- {
- if (d is not UIElement target)
- throw new ArgumentException("Magnifier can only be attached to a UIElement.");
-
- new MagnifierManager().AttachToMagnifier(target, e.NewValue as Magnifier);
- }
-
- private void ElementOnMouseLeftButtonUp(object sender, MouseEventArgs e)
- {
- if (GetMagnifier(_element) is { IsFrozen: true })
- return;
-
- HideAdorner();
- }
-
- private void ElementOnMouseLeftButtonDown(object sender, MouseEventArgs e)
- {
- ShowAdorner();
- }
-
- private void ElementOnMouseWheel(object sender, MouseWheelEventArgs e)
- {
- if (GetMagnifier(_element) is not { IsUsingZoomOnMouseWheel: true } magnifier) return;
-
- switch (e.Delta)
+ // Detach any previously registered manager on this element before attaching a new one.
+ if (_managers.TryGetValue(target, out var old))
{
- case < 0:
- {
- var newValue = magnifier.ZoomFactor + magnifier.ZoomFactorOnMouseWheel;
- magnifier.SetCurrentValue(Magnifier.ZoomFactorProperty, newValue);
- break;
- }
- case > 0:
- {
- var newValue = magnifier.ZoomFactor >= magnifier.ZoomFactorOnMouseWheel ? magnifier.ZoomFactor - magnifier.ZoomFactorOnMouseWheel : 0d;
- magnifier.SetCurrentValue(Magnifier.ZoomFactorProperty, newValue);
- break;
- }
+ old.Detach();
+ _managers.Remove(target);
}
- _adorner.UpdateViewBox();
+ var magnifier = e.NewValue.GetValueOrDefault();
+ if (magnifier != null)
+ {
+ var manager = new MagnifierManager();
+ manager.AttachToMagnifier(target, magnifier);
+ _managers.Add(target, manager);
+ }
}
- private void AttachToMagnifier(UIElement element, Magnifier magnifier)
+ // -----------------------------------------------------------------------
+ // Instance — manages one control/magnifier pair
+ // -----------------------------------------------------------------------
+
+ private void AttachToMagnifier(Control element, Magnifier magnifier)
{
_element = element;
- _element.MouseLeftButtonDown += ElementOnMouseLeftButtonDown;
- _element.MouseLeftButtonUp += ElementOnMouseLeftButtonUp;
- _element.MouseWheel += ElementOnMouseWheel;
+ _element.PointerPressed += ElementOnPointerPressed;
+ _element.PointerReleased += ElementOnPointerReleased;
+ _element.PointerWheelChanged += ElementOnPointerWheelChanged;
+ _element.DetachedFromVisualTree += OnElementDetached;
magnifier.Target = _element;
_adorner = new MagnifierAdorner(_element, magnifier);
}
+ private void Detach()
+ {
+ if (_element != null)
+ {
+ _element.PointerPressed -= ElementOnPointerPressed;
+ _element.PointerReleased -= ElementOnPointerReleased;
+ _element.PointerWheelChanged -= ElementOnPointerWheelChanged;
+ _element.DetachedFromVisualTree -= OnElementDetached;
+
+ // Remove the adorner control from the AdornerLayer so it is not left
+ // as an invisible orphan child for the lifetime of the host window.
+ // Use the static API to match the add path in VerifyAdornerLayer.
+ AdornerLayer.SetAdornment(_element, null);
+ }
+ _adorner?.Detach();
+ _adorner = null;
+ _element = null;
+ }
+
+ private void OnElementDetached(object? sender, VisualTreeAttachmentEventArgs e)
+ {
+ // Full teardown so that if the element is ever re-attached and SetMagnifier is
+ // called again, a fresh manager/adorner pair is created from scratch.
+ var element = _element;
+ Detach();
+ if (element != null)
+ _managers.Remove(element);
+ }
+
+ private void ElementOnPointerReleased(object? sender, PointerReleasedEventArgs e)
+ {
+ if (e.GetCurrentPoint(null).Properties.PointerUpdateKind != PointerUpdateKind.LeftButtonReleased)
+ return;
+ if (_element != null && GetMagnifier(_element) is { IsFrozen: true })
+ return;
+
+ HideAdorner();
+ }
+
+ private void ElementOnPointerPressed(object? sender, PointerPressedEventArgs e)
+ {
+ if (e.GetCurrentPoint(null).Properties.PointerUpdateKind == PointerUpdateKind.LeftButtonPressed)
+ ShowAdorner();
+ }
+
+ private void ElementOnPointerWheelChanged(object? sender, PointerWheelEventArgs e)
+ {
+ if (_element == null)
+ return;
+ if (GetMagnifier(_element) is not { IsUsingZoomOnMouseWheel: true } magnifier)
+ return;
+
+ // WPF Delta < 0 = scroll down = zoom out (larger viewbox); Delta > 0 = scroll up = zoom in.
+ // Avalonia Delta.Y > 0 = scroll up.
+ if (e.Delta.Y < 0)
+ {
+ var newValue = magnifier.ZoomFactor + magnifier.ZoomFactorOnMouseWheel;
+ magnifier.SetCurrentValue(Magnifier.ZoomFactorProperty, newValue);
+ }
+ else if (e.Delta.Y > 0)
+ {
+ var newValue = magnifier.ZoomFactor >= magnifier.ZoomFactorOnMouseWheel
+ ? magnifier.ZoomFactor - magnifier.ZoomFactorOnMouseWheel
+ : 0d;
+ magnifier.SetCurrentValue(Magnifier.ZoomFactorProperty, newValue);
+ }
+
+ _adorner?.UpdateViewBox();
+ }
+
private void ShowAdorner()
{
+ if (_adorner == null || _element == null)
+ return;
VerifyAdornerLayer();
- _adorner.Visibility = Visibility.Visible;
+ _adorner.IsVisible = true;
}
private void VerifyAdornerLayer()
{
- if (_adorner.Parent != null) return;
+ if (_adorner == null || _element == null)
+ return;
+ if (_adorner.Parent != null)
+ return;
+
var layer = AdornerLayer.GetAdornerLayer(_element);
- layer?.Add(_adorner);
+ if (layer != null)
+ AdornerLayer.SetAdornment(_element, _adorner);
}
private void HideAdorner()
{
- if (_adorner.Visibility == Visibility.Visible)
- {
- _adorner.Visibility = Visibility.Collapsed;
- }
+ if (_adorner is { IsVisible: true })
+ _adorner.IsVisible = false;
}
-}
\ No newline at end of file
+}