diff --git a/src/Avalonia.Base/Input/InputElement.cs b/src/Avalonia.Base/Input/InputElement.cs
index 565e6afc1a..d15abfbd3f 100644
--- a/src/Avalonia.Base/Input/InputElement.cs
+++ b/src/Avalonia.Base/Input/InputElement.cs
@@ -178,6 +178,14 @@ namespace Avalonia.Input
nameof(PointerReleased),
RoutingStrategies.Tunnel | RoutingStrategies.Bubble);
+ ///
+ /// Defines the routed event.
+ ///
+ internal static readonly RoutedEvent PointerCaptureChangingEvent =
+ RoutedEvent.Register(
+ nameof(PointerCaptureChanging),
+ RoutingStrategies.Direct);
+
///
/// Defines the routed event.
///
@@ -240,6 +248,7 @@ namespace Avalonia.Input
PointerMovedEvent.AddClassHandler((x, e) => x.OnPointerMoved(e));
PointerPressedEvent.AddClassHandler((x, e) => x.OnPointerPressed(e));
PointerReleasedEvent.AddClassHandler((x, e) => x.OnPointerReleased(e));
+ PointerCaptureChangingEvent.AddClassHandler((x, e) => x.OnPointerCaptureChanging(e));
PointerCaptureLostEvent.AddClassHandler((x, e) => x.OnPointerCaptureLost(e));
PointerWheelChangedEvent.AddClassHandler((x, e) => x.OnPointerWheelChanged(e));
@@ -381,9 +390,19 @@ namespace Avalonia.Input
remove { RemoveHandler(PointerReleasedEvent, value); }
}
+ ///
+ /// Occurs when the control or its child control is about to lose capture,
+ /// event will not be triggered for a parent control if capture was transferred to another child of that parent control.
+ ///
+ internal event EventHandler? PointerCaptureChanging
+ {
+ add => AddHandler(PointerCaptureChangingEvent, value);
+ remove => RemoveHandler(PointerCaptureChangingEvent, value);
+ }
+
///
/// Occurs when the control or its child control loses the pointer capture for any reason,
- /// event will not be triggered for a parent control if capture was transferred to another child of that parent control
+ /// event will not be triggered for a parent control if capture was transferred to another child of that parent control.
///
public event EventHandler? PointerCaptureLost
{
@@ -770,6 +789,17 @@ namespace Avalonia.Input
/// Last focusable element if available/>.
protected internal virtual InputElement? GetLastFocusableElementOverride() => null;
+ ///
+ /// Invoked when an unhandled reaches an element in its
+ /// route that is derived from this class. Implement this method to add class handling
+ /// for this event.
+ ///
+ /// Data about the event.
+ internal virtual void OnPointerCaptureChanging(PointerCaptureChangingEventArgs e)
+ {
+
+ }
+
///
/// Invoked when an unhandled reaches an element in its
/// route that is derived from this class. Implement this method to add class handling
diff --git a/src/Avalonia.Base/Input/MouseDevice.cs b/src/Avalonia.Base/Input/MouseDevice.cs
index b09f195656..49945f1e8a 100644
--- a/src/Avalonia.Base/Input/MouseDevice.cs
+++ b/src/Avalonia.Base/Input/MouseDevice.cs
@@ -131,7 +131,7 @@ namespace Avalonia.Input
if (source != null)
{
- _pointer.Capture(source);
+ _pointer.Capture(source, CaptureSource.Implicit);
var settings = ((IInputRoot?)(source as Interactive)?.GetVisualRoot())?.PlatformSettings;
if (settings is not null)
@@ -206,7 +206,7 @@ namespace Avalonia.Input
}
finally
{
- _pointer.Capture(null);
+ _pointer.Capture(null, CaptureSource.Implicit);
_pointer.CaptureGestureRecognizer(null);
_pointer.IsGestureRecognitionSkipped = false;
_lastMouseDownButton = default;
diff --git a/src/Avalonia.Base/Input/Platform/DataObjectToDataTransferWrapper.cs b/src/Avalonia.Base/Input/Platform/DataObjectToDataTransferWrapper.cs
index 8460268ec6..6fe64134d8 100644
--- a/src/Avalonia.Base/Input/Platform/DataObjectToDataTransferWrapper.cs
+++ b/src/Avalonia.Base/Input/Platform/DataObjectToDataTransferWrapper.cs
@@ -28,6 +28,7 @@ internal sealed class DataObjectToDataTransferWrapper(IDataObject dataObject)
var items = new List();
var nonFileFormats = new List();
var nonFileFormatStrings = new List();
+ var hasFiles = false;
foreach (var formatString in DataObject.GetDataFormats())
{
@@ -35,18 +36,28 @@ internal sealed class DataObjectToDataTransferWrapper(IDataObject dataObject)
if (formatString == DataFormats.Files)
{
+ if (hasFiles)
+ continue;
+
// This is not ideal as we're reading the filenames ahead of time to generate the appropriate items.
// We don't really care about that for this legacy wrapper.
if (DataObject.Get(formatString) is IEnumerable storageItems)
{
+ hasFiles = true;
+
foreach (var storageItem in storageItems)
items.Add(PlatformDataTransferItem.Create(DataFormat.File, storageItem));
}
}
else if (formatString == DataFormats.FileNames)
{
+ if (hasFiles)
+ continue;
+
if (DataObject.Get(formatString) is IEnumerable fileNames)
{
+ hasFiles = true;
+
foreach (var fileName in fileNames)
{
if (StorageProviderHelpers.TryCreateBclStorageItem(fileName) is { } storageItem)
diff --git a/src/Avalonia.Base/Input/Pointer.cs b/src/Avalonia.Base/Input/Pointer.cs
index 1f6741a09e..f243a2e382 100644
--- a/src/Avalonia.Base/Input/Pointer.cs
+++ b/src/Avalonia.Base/Input/Pointer.cs
@@ -6,6 +6,13 @@ using Avalonia.VisualTree;
namespace Avalonia.Input
{
+ internal enum CaptureSource
+ {
+ Explicit,
+ Implicit,
+ Platform
+ }
+
public class Pointer : IPointer, IDisposable
{
private static int s_NextFreePointerId = 1000;
@@ -30,46 +37,60 @@ namespace Avalonia.Input
protected virtual void PlatformCapture(IInputElement? element)
{
-
+
}
internal void PlatformCaptureLost()
{
if (Captured != null)
- Capture(null, platformInitiated: true);
+ Capture(null, CaptureSource.Platform);
}
public void Capture(IInputElement? control)
{
- Capture(control, platformInitiated: false);
+ Capture(control, CaptureSource.Explicit);
}
- private void Capture(IInputElement? control, bool platformInitiated)
+ internal void Capture(IInputElement? control, CaptureSource source)
{
var oldCapture = Captured;
if (oldCapture == control)
return;
- if (oldCapture is Visual v1)
- v1.DetachedFromVisualTree -= OnCaptureDetached;
+ var oldVisual = oldCapture as Visual;
+
+ IInputElement? commonParent = null;
+ if (oldVisual != null)
+ {
+ commonParent = FindCommonParent(control, oldCapture);
+ foreach (var notifyTarget in oldVisual.GetSelfAndVisualAncestors().OfType())
+ {
+ if (notifyTarget == commonParent)
+ break;
+ var args = new PointerCaptureChangingEventArgs(notifyTarget, this, control, source);
+ notifyTarget.RaiseEvent(args);
+ if (args.Handled)
+ return;
+ }
+ }
+
+ if (oldVisual != null)
+ oldVisual.DetachedFromVisualTree -= OnCaptureDetached;
Captured = control;
-
- if (!platformInitiated)
+
+ if (source != CaptureSource.Platform)
PlatformCapture(control);
- if (oldCapture is Visual v2)
- {
- var commonParent = FindCommonParent(control, oldCapture);
- foreach (var notifyTarget in v2.GetSelfAndVisualAncestors().OfType())
+ if (oldVisual != null)
+ foreach (var notifyTarget in oldVisual.GetSelfAndVisualAncestors().OfType())
{
if (notifyTarget == commonParent)
break;
notifyTarget.RaiseEvent(new PointerCaptureLostEventArgs(notifyTarget, this));
}
- }
- if (Captured is Visual v3)
- v3.DetachedFromVisualTree += OnCaptureDetached;
+ if (Captured is Visual newVisual)
+ newVisual.DetachedFromVisualTree += OnCaptureDetached;
if (Captured != null)
CaptureGestureRecognizer(null);
@@ -92,7 +113,7 @@ namespace Avalonia.Input
public IInputElement? Captured { get; private set; }
-
+
public PointerType Type { get; }
public bool IsPrimary { get; }
diff --git a/src/Avalonia.Base/Input/PointerEventArgs.cs b/src/Avalonia.Base/Input/PointerEventArgs.cs
index bbcc5cccd8..7682b0fb22 100644
--- a/src/Avalonia.Base/Input/PointerEventArgs.cs
+++ b/src/Avalonia.Base/Input/PointerEventArgs.cs
@@ -202,11 +202,26 @@ namespace Avalonia.Input
{
public IPointer Pointer { get; }
- [Unstable("This constructor might be removed in 12.0. If you need to remove capture, use stable methods on the IPointer instance.,")]
+ [Unstable("This constructor might be removed in 12.0. If you need to remove capture, use stable methods on the IPointer instance.")]
public PointerCaptureLostEventArgs(object source, IPointer pointer) : base(InputElement.PointerCaptureLostEvent)
{
Pointer = pointer;
Source = source;
}
}
+
+ internal class PointerCaptureChangingEventArgs : RoutedEventArgs
+ {
+ public IPointer Pointer { get; }
+ public CaptureSource CaptureSource { get; }
+ public IInputElement? NewValue { get; }
+
+ internal PointerCaptureChangingEventArgs(object source, IPointer pointer, IInputElement? newValue, CaptureSource captureSource) : base(InputElement.PointerCaptureChangingEvent)
+ {
+ Pointer = pointer;
+ Source = source;
+ NewValue = newValue;
+ CaptureSource = captureSource;
+ }
+ }
}
diff --git a/src/Avalonia.Controls/LayoutTransformControl.cs b/src/Avalonia.Controls/LayoutTransformControl.cs
index 6cd2ee14bb..c6eef00a76 100644
--- a/src/Avalonia.Controls/LayoutTransformControl.cs
+++ b/src/Avalonia.Controls/LayoutTransformControl.cs
@@ -145,6 +145,19 @@ namespace Avalonia.Controls
// Return result to allocate enough space for the transformation
return transformedDesiredSize;
}
+
+ protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
+ {
+ base.OnAttachedToVisualTree(e);
+ SubscribeLayoutTransform(LayoutTransform as Transform);
+ ApplyLayoutTransform();
+ }
+
+ protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
+ {
+ base.OnDetachedFromVisualTree(e);
+ UnsubscribeLayoutTransform(LayoutTransform as Transform);
+ }
private IDisposable? _renderTransformChangedEvent;
@@ -224,7 +237,6 @@ namespace Avalonia.Controls
/// Transformation matrix corresponding to _matrixTransform.
///
private Matrix _transformation = Matrix.Identity;
- private IDisposable? _transformChangedEvent;
///
/// Returns true if Size a is smaller than Size b in either dimension.
@@ -424,19 +436,34 @@ namespace Avalonia.Controls
private void OnLayoutTransformChanged(AvaloniaPropertyChangedEventArgs e)
{
- var newTransform = e.NewValue as Transform;
-
- _transformChangedEvent?.Dispose();
- _transformChangedEvent = null;
-
- if (newTransform != null)
+ if (this.IsAttachedToVisualTree)
{
- _transformChangedEvent = Observable.FromEventPattern(
- v => newTransform.Changed += v, v => newTransform.Changed -= v)
- .Subscribe(_ => ApplyLayoutTransform());
+ UnsubscribeLayoutTransform(e.OldValue as Transform);
+ SubscribeLayoutTransform(e.NewValue as Transform);
}
+
+ ApplyLayoutTransform();
+ }
+ private void OnTransformChanged(object? sender, EventArgs e)
+ {
ApplyLayoutTransform();
}
+
+ private void SubscribeLayoutTransform(Transform? transform)
+ {
+ if (transform != null)
+ {
+ transform.Changed += OnTransformChanged;
+ }
+ }
+
+ private void UnsubscribeLayoutTransform(Transform? transform)
+ {
+ if (transform != null)
+ {
+ transform.Changed -= OnTransformChanged;
+ }
+ }
}
}
diff --git a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs
index b19da5ff36..b8ed5d30b4 100644
--- a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs
+++ b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs
@@ -58,6 +58,7 @@ namespace Avalonia.FreeDesktop
_statusNotifierItemDbusObj = new StatusNotifierItemDbusObj(_connection, dbusMenuPath);
_pathHandler.Add(_statusNotifierItemDbusObj);
_connection.AddMethodHandler(_pathHandler);
+ _statusNotifierItemDbusObj.ActivationDelegate += () => OnClicked?.Invoke();
WatchAsync();
}
@@ -112,13 +113,19 @@ namespace Avalonia.FreeDesktop
#endif
var tid = s_trayIconInstanceId++;
+ // make sure not to add the path handle and connection method handler twice
+ if (_statusNotifierItemDbusObj!.PathHandler is null)
+ _pathHandler.Add(_statusNotifierItemDbusObj!);
+
+ _connection.RemoveMethodHandler(_pathHandler.Path);
+ _connection.AddMethodHandler(_pathHandler);
+
_sysTrayServiceName = FormattableString.Invariant($"org.kde.StatusNotifierItem-{pid}-{tid}");
await _dBus!.RequestNameAsync(_sysTrayServiceName, 0);
await _statusNotifierWatcher.RegisterStatusNotifierItemAsync(_sysTrayServiceName);
_statusNotifierItemDbusObj!.SetTitleAndTooltip(_tooltipText);
_statusNotifierItemDbusObj.SetIcon(_icon);
- _statusNotifierItemDbusObj.ActivationDelegate += OnClicked;
}
private void DestroyTrayIcon()
diff --git a/src/Avalonia.X11/Clipboard/X11Clipboard.cs b/src/Avalonia.X11/Clipboard/X11Clipboard.cs
index 21ba3d54f7..a5a56a51d7 100644
--- a/src/Avalonia.X11/Clipboard/X11Clipboard.cs
+++ b/src/Avalonia.X11/Clipboard/X11Clipboard.cs
@@ -327,15 +327,21 @@ namespace Avalonia.X11.Clipboard
{
List? nonFileFormats = null;
var items = new List();
+ var hasFiles = false;
foreach (var format in formats)
{
if (DataFormat.File.Equals(format))
{
+ if (hasFiles)
+ continue;
+
// We're reading the filenames ahead of time to generate the appropriate items.
// This is async, so it should be fine.
if (await reader.TryGetAsync(format) is IEnumerable storageItems)
{
+ hasFiles = true;
+
foreach (var storageItem in storageItems)
items.Add(PlatformDataTransferItem.Create(DataFormat.File, storageItem));
}
diff --git a/src/Avalonia.X11/NativeDialogs/Gtk.cs b/src/Avalonia.X11/NativeDialogs/Gtk.cs
index 70010863ba..3138bdb22f 100644
--- a/src/Avalonia.X11/NativeDialogs/Gtk.cs
+++ b/src/Avalonia.X11/NativeDialogs/Gtk.cs
@@ -61,7 +61,10 @@ namespace Avalonia.X11.NativeDialogs
[DllImport(GtkName)]
public static extern void gtk_file_chooser_set_select_multiple(IntPtr chooser, bool allow);
-
+
+ [DllImport(GtkName)]
+ public static extern void gtk_file_chooser_set_local_only(IntPtr chooser, bool local_only);
+
[DllImport(GtkName)]
public static extern void gtk_file_chooser_set_do_overwrite_confirmation(IntPtr chooser, bool do_overwrite_confirmation);
diff --git a/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs b/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs
index 1c1f8ba38b..75aff32b13 100644
--- a/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs
+++ b/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs
@@ -108,6 +108,7 @@ namespace Avalonia.X11.NativeDialogs
}
gtk_window_set_modal(dlg, true);
+ gtk_file_chooser_set_local_only(dlg, false);
var tcs = new TaskCompletionSource<(string[]?, FilePickerFileType?)>();
List? disposables = null;
diff --git a/src/Skia/Avalonia.Skia/FontManagerImpl.cs b/src/Skia/Avalonia.Skia/FontManagerImpl.cs
index 477348b59a..eb1833193c 100644
--- a/src/Skia/Avalonia.Skia/FontManagerImpl.cs
+++ b/src/Skia/Avalonia.Skia/FontManagerImpl.cs
@@ -50,7 +50,7 @@ namespace Avalonia.Skia
skFontStyle = SKFontStyle.BoldItalic;
break;
default:
- skFontStyle = new SKFontStyle((SKFontStyleWeight)fontWeight, (SKFontStyleWidth)fontStretch, (SKFontStyleSlant)fontStyle);
+ skFontStyle = new SKFontStyle((SKFontStyleWeight)fontWeight, (SKFontStyleWidth)fontStretch, fontStyle.ToSkia());
break;
}
@@ -63,7 +63,12 @@ namespace Avalonia.Skia
if (skTypeface != null)
{
- fontKey = new Typeface(skTypeface.FamilyName, (FontStyle)skTypeface.FontStyle.Slant, (FontWeight)skTypeface.FontStyle.Weight, (FontStretch)skTypeface.FontStyle.Width);
+ // ToDo: create glyph typeface here to get the correct style/weight/stretch
+ fontKey = new Typeface(
+ skTypeface.FamilyName,
+ skTypeface.FontStyle.Slant.ToAvalonia(),
+ (FontWeight)skTypeface.FontStyle.Weight,
+ (FontStretch)skTypeface.FontStyle.Width);
return true;
}
@@ -78,8 +83,7 @@ namespace Avalonia.Skia
{
glyphTypeface = null;
- var fontStyle = new SKFontStyle((SKFontStyleWeight)weight, (SKFontStyleWidth)stretch,
- (SKFontStyleSlant)style);
+ var fontStyle = new SKFontStyle((SKFontStyleWeight)weight, (SKFontStyleWidth)stretch, style.ToSkia());
var skTypeface = _skFontManager.MatchFamily(familyName, fontStyle);
@@ -127,7 +131,7 @@ namespace Avalonia.Skia
var set = _skFontManager.GetFontStyles(familyName);
- if(set.Count == 0)
+ if (set.Count == 0)
{
return false;
}
diff --git a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs
index 2def64c18d..703496a834 100644
--- a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs
+++ b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs
@@ -16,7 +16,6 @@ namespace Avalonia.Skia
internal class GlyphTypefaceImpl : IGlyphTypeface2
{
private bool _isDisposed;
- private readonly SKTypeface _typeface;
private readonly NameTable? _nameTable;
private readonly OS2Table? _os2Table;
private readonly HorizontalHeadTable? _hhTable;
@@ -24,7 +23,7 @@ namespace Avalonia.Skia
public GlyphTypefaceImpl(SKTypeface typeface, FontSimulations fontSimulations)
{
- _typeface = typeface ?? throw new ArgumentNullException(nameof(typeface));
+ SKTypeface = typeface ?? throw new ArgumentNullException(nameof(typeface));
Face = new Face(GetTable) { UnitsPerEm = typeface.UnitsPerEm };
@@ -96,6 +95,11 @@ namespace Avalonia.Skia
var style = _os2Table != null ? GetFontStyle(_os2Table.FontStyle) : FontStyle.Normal;
+ if (typeface.FontStyle.Slant == SKFontStyleSlant.Oblique)
+ {
+ style = FontStyle.Oblique;
+ }
+
Style = (fontSimulations & FontSimulations.Oblique) != 0 ? FontStyle.Italic : style;
var stretch = _os2Table != null ? (FontStretch)_os2Table.WidthClass : FontStretch.Normal;
@@ -205,6 +209,8 @@ namespace Avalonia.Skia
}
}
+ public SKTypeface SKTypeface { get; }
+
public Face Face { get; }
public Font Font { get; }
@@ -300,12 +306,12 @@ namespace Avalonia.Skia
private static FontStyle GetFontStyle(OS2Table.FontStyleSelection styleSelection)
{
- if((styleSelection & OS2Table.FontStyleSelection.ITALIC) != 0)
+ if ((styleSelection & OS2Table.FontStyleSelection.ITALIC) != 0)
{
return FontStyle.Italic;
}
- if((styleSelection & OS2Table.FontStyleSelection.OBLIQUE) != 0)
+ if ((styleSelection & OS2Table.FontStyleSelection.OBLIQUE) != 0)
{
return FontStyle.Oblique;
}
@@ -315,18 +321,18 @@ namespace Avalonia.Skia
private Blob? GetTable(Face face, Tag tag)
{
- var size = _typeface.GetTableSize(tag);
+ var size = SKTypeface.GetTableSize(tag);
var data = Marshal.AllocCoTaskMem(size);
var releaseDelegate = new ReleaseDelegate(() => Marshal.FreeCoTaskMem(data));
- return _typeface.TryGetTableData(tag, 0, size, data) ?
+ return SKTypeface.TryGetTableData(tag, 0, size, data) ?
new Blob(data, size, MemoryMode.ReadOnly, releaseDelegate) : null;
}
public SKFont CreateSKFont(float size)
- => new(_typeface, size, skewX: (FontSimulations & FontSimulations.Oblique) != 0 ? -0.3f : 0.0f)
+ => new(SKTypeface, size, skewX: (FontSimulations & FontSimulations.Oblique) != 0 ? -0.3f : 0.0f)
{
LinearMetrics = true,
Embolden = (FontSimulations & FontSimulations.Bold) != 0
@@ -348,7 +354,7 @@ namespace Avalonia.Skia
Font.Dispose();
Face.Dispose();
- _typeface.Dispose();
+ SKTypeface.Dispose();
}
public void Dispose()
@@ -359,14 +365,14 @@ namespace Avalonia.Skia
public bool TryGetTable(uint tag, out byte[] table)
{
- return _typeface.TryGetTableData(tag, out table);
+ return SKTypeface.TryGetTableData(tag, out table);
}
public bool TryGetStream([NotNullWhen(true)] out Stream? stream)
{
try
{
- var asset = _typeface.OpenStream();
+ var asset = SKTypeface.OpenStream();
var size = asset.Length;
var buffer = new byte[size];
diff --git a/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs b/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs
index 4a9502e49e..fd4e49d012 100644
--- a/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs
+++ b/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs
@@ -1,8 +1,8 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Media;
-using Avalonia.Platform;
using Avalonia.Media.Imaging;
+using Avalonia.Platform;
using SkiaSharp;
namespace Avalonia.Skia
@@ -70,7 +70,7 @@ namespace Avalonia.Skia
{
return new SKPoint((float)p.X, (float)p.Y);
}
-
+
public static SKPoint ToSKPoint(this Vector p)
{
return new SKPoint((float)p.X, (float)p.Y);
@@ -80,17 +80,17 @@ namespace Avalonia.Skia
{
return new SKRect((float)r.X, (float)r.Y, (float)r.Right, (float)r.Bottom);
}
-
+
internal static SKRect ToSKRect(this LtrbRect r)
{
return new SKRect((float)r.Left, (float)r.Right, (float)r.Right, (float)r.Bottom);
}
-
+
public static SKRectI ToSKRectI(this PixelRect r)
{
return new SKRectI(r.X, r.Y, r.Right, r.Bottom);
}
-
+
internal static SKRectI ToSKRectI(this LtrbPixelRect r)
{
return new SKRectI(r.Left, r.Top, r.Right, r.Bottom);
@@ -106,7 +106,7 @@ namespace Avalonia.Skia
{
r.RadiiTopLeft.ToSKPoint(), r.RadiiTopRight.ToSKPoint(),
r.RadiiBottomRight.ToSKPoint(), r.RadiiBottomLeft.ToSKPoint(),
- });
+ });
return result;
}
@@ -115,17 +115,17 @@ namespace Avalonia.Skia
{
return new Rect(r.Left, r.Top, r.Right - r.Left, r.Bottom - r.Top);
}
-
+
internal static LtrbRect ToAvaloniaLtrbRect(this SKRect r)
{
return new LtrbRect(r.Left, r.Top, r.Right, r.Bottom);
}
-
+
public static PixelRect ToAvaloniaPixelRect(this SKRectI r)
{
return new PixelRect(r.Left, r.Top, r.Right - r.Left, r.Bottom - r.Top);
}
-
+
internal static LtrbPixelRect ToAvaloniaLtrbPixelRect(this SKRectI r)
{
return new LtrbPixelRect(r.Left, r.Top, r.Right, r.Bottom);
@@ -173,7 +173,7 @@ namespace Avalonia.Skia
return sm;
}
-
+
internal static Matrix ToAvaloniaMatrix(this SKMatrix m) => new(
m.ScaleX, m.SkewY, m.Persp0,
m.SkewX, m.ScaleY, m.Persp1,
@@ -253,9 +253,12 @@ namespace Avalonia.Skia
switch (m)
{
default:
- case GradientSpreadMethod.Pad: return SKShaderTileMode.Clamp;
- case GradientSpreadMethod.Reflect: return SKShaderTileMode.Mirror;
- case GradientSpreadMethod.Repeat: return SKShaderTileMode.Repeat;
+ case GradientSpreadMethod.Pad:
+ return SKShaderTileMode.Clamp;
+ case GradientSpreadMethod.Reflect:
+ return SKShaderTileMode.Mirror;
+ case GradientSpreadMethod.Repeat:
+ return SKShaderTileMode.Repeat;
}
}
@@ -264,9 +267,12 @@ namespace Avalonia.Skia
switch (a)
{
default:
- case TextAlignment.Left: return SKTextAlign.Left;
- case TextAlignment.Center: return SKTextAlign.Center;
- case TextAlignment.Right: return SKTextAlign.Right;
+ case TextAlignment.Left:
+ return SKTextAlign.Left;
+ case TextAlignment.Center:
+ return SKTextAlign.Center;
+ case TextAlignment.Right:
+ return SKTextAlign.Right;
}
}
@@ -295,9 +301,12 @@ namespace Avalonia.Skia
switch (a)
{
default:
- case SKTextAlign.Left: return TextAlignment.Left;
- case SKTextAlign.Center: return TextAlignment.Center;
- case SKTextAlign.Right: return TextAlignment.Right;
+ case SKTextAlign.Left:
+ return TextAlignment.Left;
+ case SKTextAlign.Center:
+ return TextAlignment.Center;
+ case SKTextAlign.Right:
+ return TextAlignment.Right;
}
}
@@ -308,7 +317,18 @@ namespace Avalonia.Skia
SKFontStyleSlant.Upright => FontStyle.Normal,
SKFontStyleSlant.Italic => FontStyle.Italic,
SKFontStyleSlant.Oblique => FontStyle.Oblique,
- _ => throw new ArgumentOutOfRangeException(nameof (slant), slant, null)
+ _ => throw new ArgumentOutOfRangeException(nameof(slant), slant, null)
+ };
+ }
+
+ public static SKFontStyleSlant ToSkia(this FontStyle style)
+ {
+ return style switch
+ {
+ FontStyle.Normal => SKFontStyleSlant.Upright,
+ FontStyle.Italic => SKFontStyleSlant.Italic,
+ FontStyle.Oblique => SKFontStyleSlant.Oblique,
+ _ => throw new ArgumentOutOfRangeException(nameof(style), style, null)
};
}
diff --git a/src/Windows/Avalonia.Win32/OleDataObjectHelper.cs b/src/Windows/Avalonia.Win32/OleDataObjectHelper.cs
index 820231d16d..1e521e685c 100644
--- a/src/Windows/Avalonia.Win32/OleDataObjectHelper.cs
+++ b/src/Windows/Avalonia.Win32/OleDataObjectHelper.cs
@@ -29,15 +29,15 @@ internal static class OleDataObjectHelper
tymed = TYMED.TYMED_HGLOBAL
};
- public static unsafe object? TryGet(this Win32Com.IDataObject _oleDataObject, DataFormat format)
+ public static unsafe object? TryGet(this Win32Com.IDataObject oleDataObject, DataFormat format)
{
var formatEtc = format.ToFormatEtc();
- if (_oleDataObject.QueryGetData(&formatEtc) != (uint)HRESULT.S_OK)
+ if (oleDataObject.QueryGetData(&formatEtc) != (uint)HRESULT.S_OK)
return null;
var medium = new STGMEDIUM();
- if (_oleDataObject.GetData(&formatEtc, &medium) != (uint)HRESULT.S_OK)
+ if (oleDataObject.GetData(&formatEtc, &medium) != (uint)HRESULT.S_OK)
return null;
try
diff --git a/src/Windows/Avalonia.Win32/OleDataObjectToDataTransferWrapper.cs b/src/Windows/Avalonia.Win32/OleDataObjectToDataTransferWrapper.cs
index 8afc079d20..f5e101e0d0 100644
--- a/src/Windows/Avalonia.Win32/OleDataObjectToDataTransferWrapper.cs
+++ b/src/Windows/Avalonia.Win32/OleDataObjectToDataTransferWrapper.cs
@@ -54,15 +54,21 @@ internal sealed class OleDataObjectToDataTransferWrapper(Win32Com.IDataObject ol
{
List? nonFileFormats = null;
var items = new List();
+ var hasFiles = false;
foreach (var format in Formats)
{
if (DataFormat.File.Equals(format))
{
+ if (hasFiles)
+ continue;
+
// This is not ideal as we're reading the filenames ahead of time to generate the appropriate items.
// However, it's unlikely to be a heavy operation.
if (_oleDataObject.TryGet(format) is IEnumerable storageItems)
{
+ hasFiles = true;
+
foreach (var storageItem in storageItems)
items.Add(PlatformDataTransferItem.Create(DataFormat.File, storageItem));
}
diff --git a/tests/Avalonia.Controls.UnitTests/LayoutTransformControlTests.cs b/tests/Avalonia.Controls.UnitTests/LayoutTransformControlTests.cs
index 2fcc15f434..3574a239e2 100644
--- a/tests/Avalonia.Controls.UnitTests/LayoutTransformControlTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/LayoutTransformControlTests.cs
@@ -306,6 +306,35 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(m.M31, res.M31, 3);
Assert.Equal(m.M32, res.M32, 3);
}
+
+ [Fact]
+ public void Should_Apply_Transform_On_Attach_To_VisualTree()
+ {
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
+ {
+ var transform = new SkewTransform() { AngleX = -45, AngleY = -45 };
+
+ LayoutTransformControl lt = CreateWithChildAndMeasureAndTransform(
+ 100,
+ 100,
+ transform);
+
+ transform.AngleX = 45;
+ transform.AngleY = 45;
+
+ var window = new Window { Content = lt };
+ window.Show();
+
+ Matrix actual = lt.TransformRoot.RenderTransform.Value;
+ Matrix expected = Matrix.CreateSkew(Matrix.ToRadians(45), Matrix.ToRadians(45));
+ Assert.Equal(expected.M11, actual.M11, 3);
+ Assert.Equal(expected.M12, actual.M12, 3);
+ Assert.Equal(expected.M21, actual.M21, 3);
+ Assert.Equal(expected.M22, actual.M22, 3);
+ Assert.Equal(expected.M31, actual.M31, 3);
+ Assert.Equal(expected.M32, actual.M32, 3);
+ }
+ }
private static void TransformMeasureSizeTest(Size size, Transform transform, Size expectedSize)
{
diff --git a/tests/Avalonia.LeakTests/ControlTests.cs b/tests/Avalonia.LeakTests/ControlTests.cs
index c720e4d2f8..5da96511ce 100644
--- a/tests/Avalonia.LeakTests/ControlTests.cs
+++ b/tests/Avalonia.LeakTests/ControlTests.cs
@@ -13,6 +13,7 @@ using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Diagnostics;
using Avalonia.Input;
+using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Rendering;
@@ -1000,6 +1001,54 @@ namespace Avalonia.LeakTests
}
}
+ [Fact]
+ public void LayoutTransformControl_Is_Freed()
+ {
+ using (Start())
+ {
+ var transform = new RotateTransform { Angle = 90 };
+
+ Func run = () =>
+ {
+ var window = new Window
+ {
+ Content = new LayoutTransformControl
+ {
+ LayoutTransform = transform,
+ Child = new Canvas()
+ }
+ };
+
+ window.Show();
+
+ // Do a layout and make sure that LayoutTransformControl gets added to visual tree
+ window.LayoutManager.ExecuteInitialLayoutPass();
+ Assert.IsType(window.Presenter.Child);
+ Assert.NotEmpty(window.Presenter.Child.GetVisualChildren());
+
+ // Clear the content and ensure the LayoutTransformControl is removed.
+ window.Content = null;
+ window.LayoutManager.ExecuteLayoutPass();
+ Assert.Null(window.Presenter.Child);
+
+ return window;
+ };
+
+ var result = run();
+
+ // Process all Loaded events to free control reference(s)
+ Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded);
+
+ dotMemory.Check(memory =>
+ Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount));
+ dotMemory.Check(memory =>
+ Assert.Equal(0, memory.GetObjects(where => where.Type.Is