From b85bba7d2dbb912cfa88cc9b1bd3ace7ce775cb3 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Wed, 30 Jun 2021 18:51:18 +0200 Subject: [PATCH 01/23] fixes: Dialog cancel consistency on windows --- src/Windows/Avalonia.Win32/SystemDialogImpl.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Windows/Avalonia.Win32/SystemDialogImpl.cs b/src/Windows/Avalonia.Win32/SystemDialogImpl.cs index ad81cc1778..f595a58c91 100644 --- a/src/Windows/Avalonia.Win32/SystemDialogImpl.cs +++ b/src/Windows/Avalonia.Win32/SystemDialogImpl.cs @@ -19,7 +19,7 @@ namespace Avalonia.Win32 var hWnd = parent?.PlatformImpl?.Handle?.Handle ?? IntPtr.Zero; return Task.Factory.StartNew(() => { - var result = Array.Empty(); + string[] result = default; Guid clsid = dialog is OpenFileDialog ? UnmanagedMethods.ShellIds.OpenFileDialog : UnmanagedMethods.ShellIds.SaveFileDialog; Guid iid = UnmanagedMethods.ShellIds.IFileDialog; @@ -100,7 +100,7 @@ namespace Avalonia.Win32 { return Task.Factory.StartNew(() => { - string result = string.Empty; + string result = default; var hWnd = parent?.PlatformImpl?.Handle?.Handle ?? IntPtr.Zero; Guid clsid = UnmanagedMethods.ShellIds.OpenFileDialog; @@ -164,7 +164,7 @@ namespace Avalonia.Win32 } } } - return ""; + return default; } } } From f6fce366cdec5da49ba04d1417354903be5ea378 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 14 Jul 2021 15:52:17 +0200 Subject: [PATCH 02/23] Added failing tests for TextLayout when used with a transform. --- .../Media/TextFormatting/TextLayoutTests.cs | 97 ++++++++++++++++++ .../TextLayout/TextLayout_Basic.expected.png | Bin 0 -> 2177 bytes .../TextLayout_Rotated.expected.png | Bin 0 -> 2628 bytes .../TextLayout/TextLayout_Basic.expected.png | Bin 0 -> 2013 bytes .../TextLayout_Rotated.expected.png | Bin 0 -> 2468 bytes 5 files changed, 97 insertions(+) create mode 100644 tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs create mode 100644 tests/TestFiles/Direct2D1/Media/TextFormatting/TextLayout/TextLayout_Basic.expected.png create mode 100644 tests/TestFiles/Direct2D1/Media/TextFormatting/TextLayout/TextLayout_Rotated.expected.png create mode 100644 tests/TestFiles/Skia/Media/TextFormatting/TextLayout/TextLayout_Basic.expected.png create mode 100644 tests/TestFiles/Skia/Media/TextFormatting/TextLayout/TextLayout_Rotated.expected.png diff --git a/tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs new file mode 100644 index 0000000000..c0a27eb9ca --- /dev/null +++ b/tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs @@ -0,0 +1,97 @@ +using Avalonia.Controls; +using Avalonia.Media; +using Avalonia.Media.TextFormatting; +using Avalonia.Platform; +using Avalonia.Utilities; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +#if AVALONIA_SKIA +namespace Avalonia.Skia.RenderTests +#else + +using Avalonia.Direct2D1.RenderTests; + +namespace Avalonia.Direct2D1.RenderTests.Media +#endif +{ + public class TextLayoutTests : TestBase + { + public TextLayoutTests() + : base(@"Media\TextFormatting\TextLayout") + { + } + + [Fact] + public async Task TextLayout_Basic() + { + var t = new TextLayout( + "Avalonia!", + new Typeface(TestFontFamily), + 24, + Brushes.Black); + + var target = new Border + { + Width = 200, + Height = 200, + Background = Brushes.White, + Child = new DrawnControl(c => + { + var textRect = new Rect(t.Size); + var bounds = new Rect(0, 0, 200, 200); + var rect = bounds.CenterRect(textRect); + c.DrawRectangle(Brushes.Yellow, null, rect); + t.Draw(c, rect.Position); + }), + }; + + await RenderToFile(target); + CompareImages(); + } + + [Fact] + public async Task TextLayout_Rotated() + { + var t = new TextLayout( + "Avalonia!", + new Typeface(TestFontFamily), + 24, + Brushes.Black); + + var target = new Border + { + Width = 200, + Height = 200, + Background = Brushes.White, + Child = new DrawnControl(c => + { + var textRect = new Rect(t.Size); + var bounds = new Rect(0, 0, 200, 200); + var rect = bounds.CenterRect(textRect); + var rotate = Matrix.CreateTranslation(-100, -100) * + Matrix.CreateRotation(MathUtilities.Deg2Rad(90)) * + Matrix.CreateTranslation(100, 100); + using var transform = c.PushPreTransform(rotate); + c.DrawRectangle(Brushes.Yellow, null, rect); + t.Draw(c, rect.Position); + }), + }; + + await RenderToFile(target); + CompareImages(); + } + + private class DrawnControl : Control + { + private readonly Action _render; + public DrawnControl(Action render) => _render = render; + public override void Render(DrawingContext context) => _render(context); + } + } +} diff --git a/tests/TestFiles/Direct2D1/Media/TextFormatting/TextLayout/TextLayout_Basic.expected.png b/tests/TestFiles/Direct2D1/Media/TextFormatting/TextLayout/TextLayout_Basic.expected.png new file mode 100644 index 0000000000000000000000000000000000000000..215e83106f449cded8d491e55c90a8633372394b GIT binary patch literal 2177 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 literal 0 HcmV?d00001 diff --git a/tests/TestFiles/Skia/Media/TextFormatting/TextLayout/TextLayout_Basic.expected.png b/tests/TestFiles/Skia/Media/TextFormatting/TextLayout/TextLayout_Basic.expected.png new file mode 100644 index 0000000000000000000000000000000000000000..220932ff41084338bb176fec16989ad4a51f1d21 GIT binary patch literal 2013 zcmd^==~t2o6vioQG|8dVvQ?~uS+-cYi%N?|T1h$PRtk-Z85A`oC4#oOlo^ewWR#4y z*rE`UT3JwPW2H!_X^Qe@i=-^BvGY32oHKt#_x$dCK0Wt5=YF{tc88&DtQT2BAP}37 z?LpyYee-Wy{bHU~(d<&Q%s9R!B*MyERI9{X2*k25BxrNQsT;~Z0l_|HN%I&<#fIj1 zmCtXuaQ3L{p5^y)qgR5sTl2jxR_kE4YnohF*%P~5vu^M8!X87o z=Yflkn|jZdnBpd#cHhGa6`FF(v8@>MX~gFa`X0n*&I_=ze;|4{>4>v~&as;D9|%i3 zJA6O@3j2=TReqRMzp%lqBpTQPhR&?*&p zr|Ub3WHQZ>M@y=nTL`Y~>bud?cC!kQ$%M7U7+_CTQ|qN3&;%YYDJJgZrI1xOdZaJ# znh}mDA#CThxcWg_c+>hGB6DiRHMRE5nN|+PMFQKm$!+Wv!V2;|mn=9_G0@zCzyw~WoT#>i`5&^yb zr9@${VGIp*8FvVgNG5-3^xA;t(1(JoEY{SzKIy^R&5KK! zCC){mMv_dn_I|gi9gNX|#-J{SG0}KP(d|gwWib5%`avbi*=DYRHiKr?zWzTjy zst~#dswN&|>mI$gc9$Qkw?RBFBh*Zu>V>!e@6WkYzGM$M9o70^FRFIx^J`IE&Qg$<9OYG9<5OuIzk?C6=zvrrp{O|6 zW|=R|FT+G30h~nLdD@j-%r5+zwAF{5287g6oL;{*hk8BLL*DMp%J#;s_x#X10MxmG zb;7$NO)yx+$?M&pZ+B@%0{dSe(@T{Sntm+7sOIyj&5sNHi!r$=qSBg$3DVGo=6FeX z@^FMc>&YoGN91=sb^JDl1cbrgru}$V5yftTPrQbrQpjWF9IiL%$jzmk?uoSc4F~dL z$-!$`&6#A+`e6M%OqeB{K?rxk=_r>8yxh#^k^%Fs|9chiw?< zCwUZ0)jLlRKyXR(Y~b&WUU(vQKan=P6jeI)g%c!gNye`LpB06RbHwex8hwI%HEdi&y}Jw$%IpkOAsTl@L4r?@F}%H-VVS~ z5!l+s=XWHQvk0AbE|XNoI-N_?E%X>B+dY{|kr$I3I}h4COD*m__*J-QmD%o`pnN`Zgn^OWF-hi($)ebpEA3P1 zbBw^~$mY{F=Qcsk{7+dTnyVE(e3cn*LtH@oecJRhke`zVR3+W`sY9FdPe*lXVSIGbX literal 0 HcmV?d00001 diff --git a/tests/TestFiles/Skia/Media/TextFormatting/TextLayout/TextLayout_Rotated.expected.png b/tests/TestFiles/Skia/Media/TextFormatting/TextLayout/TextLayout_Rotated.expected.png new file mode 100644 index 0000000000000000000000000000000000000000..26fe2b0c91ad39e98f97b40711fcd8cc436186af GIT binary patch literal 2468 zcmcguX;72N7G+|P03*AE0olfgxDZ4UG!P6BG{F&dL_mSCIfxHrQ6LZ!F(9(ZVifZb zB_hEESw*%GHrYl4s8K^u0wS9qVUa-CA@hOO`}KaldR^7sReh_vZ=F8p+?#m9;m9UA zO*ttksZDqs_D^vCdu_rtf@`5mQ8u_iFWcZ9Vc;Oc&Lv1m$-3gP){YVP#s$ax+=g`8 zraX+1$ibL7=^fE(PEUWU#2IqvSrvxijWz|GX1pDsYCuu)79GQ(DS)!)9*vpucQlWPF|!U5@!a|xPlt4SeU^%n;eHB>nRzJ%KTZR#sS`cdd^n8Szc!3n8Vv z<3HZuZP;OMzMZmofpz+6tqyg*!4fC4GkU9JyfUN4wL5?v61Eir=uJ;^o8mtnseFD0 z5D@daJm?BKH}+Ko+UY58mgBB00bP^qTdP@ys0~!8Q_@#qab9NT>%Xw?B4LOUIw61g zt|3Z`ip8ic$xxxQBMfXQBN&bI>}V~m7(Tzn?^kN*gF>SVq_e0H8`5|=KNkob^A1Zu z+;novPYX=B(X6EkL$J#C6N^qS40;-Db!Qvau2h81__0f=n1}pNv^oia6tZ z9p-sCIT1d#+aR)IwR1a(Irj17`0LE+^cX}Hbhx-2h3GOgthqFJtptN3zI*-vKjG@zMBKHz4bIk<>$P^XB z_Cu!+D<>%4D}ZVI8=rpVoV5YF)0>Mqh5d|ElIx zVV9CR6>4UY)fqK-DX`av;?4^1(4H{L%nqLlZgg~ms_pd+gbUj`m|Lc^YQ534zo5RZXqX`~x$7bxdfA0#e}JH*A9 zn)LTpG0<99Zs1H#YVbeYeo+mqqRpIY$s@K_#%|va8GLA~b|ILq-u_Gi>GV3o7y00; z+iqQc4762}It%ZO{f{aUCFD906B}vNAEWpCaw(p0`8`TF=)gdr{M*+uFZI%43c9Ii zh-uOf15lXW*t!(I5@)n-3=K#p?52xa)Gg4XEED2~UyDFgwV$A_+2J#Mz+)XHc!Olp2_7(+^4e)Ih zFo>0cN7*`+I&;F5%u(w2>vS##9`XG}_Jd*I3U zzeMn3q&8OQQ8RSDoO#oF-NHJ=_weGd7O=dqmgSZf(L-^r%c)ab8QrP}nihkcli;tn zjGmwy*JREX*CnB86NmM*^B=bz(;(FAL?Hwz7!-dtd`$T&2K9mGL=vFB_8U5RE-X(7 z3ySQ`gILiF)DY3KBt(sqJZp(Le-6u{WW5*oc+0D}AiMY^Tic0P*VX10^=A3bVG7~9 zf%dxnr#FZqpPooS%YhVt2q%M<0~6zHeB$H!6{(>mLw~{5zUnJAfHnx_aJW+gHN|sZ ztcSYdVxq7wb8NYdu~%BJ+qcI?yvxp

aLFHOmrhtN%Dmnhzd13j*4JmUC-)!KV80;FtzQ_>cLLfha?h``n)`I$Vm%m|(AYTYnxde< zdZ45T(vX8s({--DB4XBjG(^c$q2U1S^UksGYdi5hM!0>MKZ)6J)$|~cdZMGFI?Hj= z(GaJqI+3!}UAA_kY8>_0vAi@dEv;vSy6L1QdH8agtT+UD2VUrLDXYWYxv0v>!RG*+ zBLi}3sy=rmZtlOlQCW3<}P#s`w5fA$hz4DY~=aj4Dx#D{{2TM8Qd7Rdjl(Ew$)Dx Date: Wed, 14 Jul 2021 15:53:07 +0200 Subject: [PATCH 03/23] Use pre-transform when drawing TextLayout. Using a post-transform breaks when there's a transform applied to the drawing context. --- .../Media/TextFormatting/ShapedTextCharacters.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs b/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs index b304b19910..3c17dabbb0 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs @@ -47,7 +47,7 @@ namespace Avalonia.Media.TextFormatting /// public override void Draw(DrawingContext drawingContext, Point origin) { - using (drawingContext.PushPostTransform(Matrix.CreateTranslation(origin))) + using (drawingContext.PushPreTransform(Matrix.CreateTranslation(origin))) { if (GlyphRun.GlyphIndices.Length == 0) { From 7aa6152b340d3c65ffbdcde5761f9fc56459f2fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Mon, 23 Aug 2021 16:53:07 +0200 Subject: [PATCH 04/23] Revert "Fix skia OpacityMask push and pop methods" This reverts commit dc404b545e534ea904911ca9647f312f77e7ed6d. --- src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index c9a5544672..1f186396ab 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -534,11 +534,7 @@ namespace Avalonia.Skia public void PushOpacityMask(IBrush mask, Rect bounds) { // TODO: This should be disposed - var paint = new SKPaint() - { - IsAntialias = true, - Style = SKPaintStyle.StrokeAndFill - }; + var paint = new SKPaint(); Canvas.SaveLayer(paint); _maskStack.Push(CreatePaint(paint, mask, bounds, true)); @@ -547,14 +543,7 @@ namespace Avalonia.Skia /// public void PopOpacityMask() { - using (var paint = new SKPaint - { - IsAntialias = true, - Style = SKPaintStyle.StrokeAndFill, - BlendMode = SKBlendMode.DstIn, - Color = new SKColor(0, 0, 0, 255), - ColorFilter = SKColorFilter.CreateLumaColor() - }) + using (var paint = new SKPaint { BlendMode = SKBlendMode.DstIn }) { Canvas.SaveLayer(paint); using (var paintWrapper = _maskStack.Pop()) From 51862d1b4b6cfc346c63eafd39369ca85259d6dc Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 23 Aug 2021 17:15:41 +0200 Subject: [PATCH 05/23] Don't display warning when TryCreateAndRegister succeeds. --- .../WinRT/Composition/WinUICompositorConnection.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs index 1c3c959acf..bc8f5a606c 100644 --- a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs +++ b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs @@ -144,6 +144,7 @@ namespace Avalonia.Win32.WinRT.Composition try { TryCreateAndRegisterCore(angle); + return; } catch (Exception e) { From 9718a77fa95d86f3b6224f50234a2f17a4150313 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 23 Aug 2021 23:42:13 +0200 Subject: [PATCH 06/23] Skip TextLayout render tests on OSX. They were failing due to slightly different text rendering on that platform. --- .../Media/TextFormatting/TextLayoutTests.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs index c0a27eb9ca..b1a2d00b00 100644 --- a/tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs +++ b/tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs @@ -10,6 +10,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; using Xunit; +using System.Runtime.InteropServices; #if AVALONIA_SKIA namespace Avalonia.Skia.RenderTests @@ -30,6 +31,10 @@ namespace Avalonia.Direct2D1.RenderTests.Media [Fact] public async Task TextLayout_Basic() { + // Skip test on OSX: text rendering is subtly different. + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + return; + var t = new TextLayout( "Avalonia!", new Typeface(TestFontFamily), @@ -58,6 +63,10 @@ namespace Avalonia.Direct2D1.RenderTests.Media [Fact] public async Task TextLayout_Rotated() { + // Skip test on OSX: text rendering is subtly different. + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + return; + var t = new TextLayout( "Avalonia!", new Typeface(TestFontFamily), From 0f83ccb4b026d3fec9ee4864c9874aca971feff3 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Mon, 23 Aug 2021 18:30:18 -0400 Subject: [PATCH 07/23] ContentPresenter should create child without content, if template was set (#6226) * Fix #6224 * Fix data templates Match * Do not use preview features * Do not create Child if Content is null and DataTemplate was set * Update src/Avalonia.Base/Utilities/TypeUtilities.cs * Update src/Avalonia.Controls/Presenters/ContentPresenter.cs * Update src/Avalonia.Controls/Presenters/ContentPresenter.cs --- .../ControlCatalog/Pages/ComboBoxPage.xaml | 32 ++++++- src/Avalonia.Base/Utilities/TypeUtilities.cs | 34 +++++--- .../Presenters/ContentPresenter.cs | 6 +- .../Templates/FuncDataTemplate`1.cs | 6 +- .../Templates/DataTemplate.cs | 1 - .../ContentPresenterTests_Standalone.cs | 86 +++++++++++++++++++ 6 files changed, 147 insertions(+), 18 deletions(-) diff --git a/samples/ControlCatalog/Pages/ComboBoxPage.xaml b/samples/ControlCatalog/Pages/ComboBoxPage.xaml index 025b85492c..d440b7cce3 100644 --- a/samples/ControlCatalog/Pages/ComboBoxPage.xaml +++ b/samples/ControlCatalog/Pages/ComboBoxPage.xaml @@ -1,12 +1,20 @@ + xmlns:sys="using:System" + xmlns:col="using:System.Collections"> ComboBox A drop-down list. - + + + + Inline Items Inline Item 2 @@ -14,6 +22,24 @@ Inline Item 4 + + + + + Hello + World + + + + + + + + + + + + @@ -46,7 +72,7 @@ - + diff --git a/src/Avalonia.Base/Utilities/TypeUtilities.cs b/src/Avalonia.Base/Utilities/TypeUtilities.cs index 9f2308a062..179ded3549 100644 --- a/src/Avalonia.Base/Utilities/TypeUtilities.cs +++ b/src/Avalonia.Base/Utilities/TypeUtilities.cs @@ -93,13 +93,25 @@ namespace Avalonia.Utilities return !type.IsValueType || IsNullableType(type); } + ///

+ /// Returns a value indicating whether value can be casted to the specified type. + /// If value is null, checks if instances of that type can be null. + /// + /// The type to cast to + /// The value to check if cast possible + /// True if the cast is possible, otherwise false. + public static bool CanCast(object value) + { + return value is T || (value is null && AcceptsNull(typeof(T))); + } + /// /// Try to convert a value to a type by any means possible. /// - /// The type to cast to. - /// The value to cast. + /// The type to convert to. + /// The value to convert. /// The culture to use. - /// If successful, contains the cast value. + /// If successful, contains the convert value. /// True if the cast was successful, otherwise false. public static bool TryConvert(Type to, object value, CultureInfo culture, out object result) { @@ -216,10 +228,10 @@ namespace Avalonia.Utilities /// Try to convert a value to a type using the implicit conversions allowed by the C# /// language. /// - /// The type to cast to. - /// The value to cast. - /// If successful, contains the cast value. - /// True if the cast was successful, otherwise false. + /// The type to convert to. + /// The value to convert. + /// If successful, contains the converted value. + /// True if the convert was successful, otherwise false. public static bool TryConvertImplicit(Type to, object value, out object result) { if (value == null) @@ -278,8 +290,8 @@ namespace Avalonia.Utilities /// Convert a value to a type by any means possible, returning the default for that type /// if the value could not be converted. /// - /// The value to cast. - /// The type to cast to.. + /// The value to convert. + /// The type to convert to.. /// The culture to use. /// A value of . public static object ConvertOrDefault(object value, Type type, CultureInfo culture) @@ -291,8 +303,8 @@ namespace Avalonia.Utilities /// Convert a value to a type using the implicit conversions allowed by the C# language or /// return the default for the type if the value could not be converted. /// - /// The value to cast. - /// The type to cast to.. + /// The value to convert. + /// The type to convert to. /// A value of . public static object ConvertImplicitOrDefault(object value, Type type) { diff --git a/src/Avalonia.Controls/Presenters/ContentPresenter.cs b/src/Avalonia.Controls/Presenters/ContentPresenter.cs index 472727823a..1a46d84558 100644 --- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ContentPresenter.cs @@ -327,7 +327,11 @@ namespace Avalonia.Controls.Presenters var oldChild = Child; var newChild = content as IControl; - if (content != null && newChild == null) + // We want to allow creating Child from the Template, if Content is null. + // But it's important to not use DataTemplates, otherwise we will break content presenters in many places, + // otherwise it will blow up every ContentPresenter without Content set. + if (newChild == null + && (content != null || ContentTemplate != null)) { var dataTemplate = this.FindDataTemplate(content, ContentTemplate) ?? ( diff --git a/src/Avalonia.Controls/Templates/FuncDataTemplate`1.cs b/src/Avalonia.Controls/Templates/FuncDataTemplate`1.cs index 4a6a1c6cfb..8e7b290247 100644 --- a/src/Avalonia.Controls/Templates/FuncDataTemplate`1.cs +++ b/src/Avalonia.Controls/Templates/FuncDataTemplate`1.cs @@ -1,5 +1,7 @@ using System; +using Avalonia.Utilities; + namespace Avalonia.Controls.Templates { /// @@ -16,7 +18,7 @@ namespace Avalonia.Controls.Templates /// /// Whether the control can be recycled. public FuncDataTemplate(Func build, bool supportsRecycling = false) - : base(typeof(T), CastBuild(build), supportsRecycling) + : base(o => TypeUtilities.CanCast(o), CastBuild(build), supportsRecycling) { } @@ -63,7 +65,7 @@ namespace Avalonia.Controls.Templates /// The weakly typed function. private static Func CastMatch(Func f) { - return o => (o is T) && f((T)o); + return o => TypeUtilities.CanCast(o) && f((T)o); } /// diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs index 650534b347..b7db1a3fbb 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs @@ -9,7 +9,6 @@ namespace Avalonia.Markup.Xaml.Templates { public Type DataType { get; set; } - //we need content to be object otherwise portable.xaml is crashing [Content] [TemplateContent] public object Content { get; set; } diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Standalone.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Standalone.cs index c7aa583b6f..6b744ed79c 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Standalone.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Standalone.cs @@ -264,5 +264,91 @@ namespace Avalonia.Controls.UnitTests.Presenters // InheritanceParent is exposed via StylingParent. Assert.Same(logicalParent, ((IStyledElement)child).StylingParent); } + + [Fact] + public void Should_Create_Child_Even_With_Null_Content_When_ContentTemplate_Is_Set() + { + var target = new ContentPresenter + { + ContentTemplate = new FuncDataTemplate(_ => true, (_, __) => new TextBlock + { + Text = "Hello World" + }), + Content = null + }; + + target.UpdateChild(); + + var textBlock = Assert.IsType(target.Child); + Assert.Equal("Hello World", textBlock.Text); + } + + [Fact] + public void Should_Not_Create_Child_Even_With_Null_Content_And_DataTemplates_InsteadOf_ContentTemplate() + { + var target = new ContentPresenter + { + DataTemplates = + { + new FuncDataTemplate(_ => true, (_, __) => new TextBlock + { + Text = "Hello World" + }) + }, + Content = null + }; + + target.UpdateChild(); + + Assert.Null(target.Child); + } + + [Fact] + public void Should_Not_Create_Child_When_Content_And_Template_Are_Null() + { + var target = new ContentPresenter + { + ContentTemplate = null, + Content = null + }; + + target.UpdateChild(); + + Assert.Null(target.Child); + } + + [Fact] + public void Should_Not_Create_When_Child_Content_Is_Null_But_Expected_ValueType_With_FuncDataTemplate() + { + var target = new ContentPresenter + { + ContentTemplate = new FuncDataTemplate(_ => true, (_, __) => new TextBlock + { + Text = "Hello World" + }), + Content = null + }; + + target.UpdateChild(); + + Assert.Null(target.Child); + } + + [Fact] + public void Should_Create_Child_When_Content_Is_Null_And_Expected_NullableValueType_With_FuncDataTemplate() + { + var target = new ContentPresenter + { + ContentTemplate = new FuncDataTemplate(_ => true, (_, __) => new TextBlock + { + Text = "Hello World" + }), + Content = null + }; + + target.UpdateChild(); + + Assert.NotNull(target.Child); + } } } From 5df9e5760c8eb76dd6f3b49db77635b4eb3d4619 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 24 Aug 2021 14:04:40 +0200 Subject: [PATCH 08/23] Add #nullable and XML doc comments to system dialogs. --- src/Avalonia.Controls/SystemDialog.cs | 99 ++++++++++++++++++++++++--- 1 file changed, 89 insertions(+), 10 deletions(-) diff --git a/src/Avalonia.Controls/SystemDialog.cs b/src/Avalonia.Controls/SystemDialog.cs index e74b950f23..d81926ecc7 100644 --- a/src/Avalonia.Controls/SystemDialog.cs +++ b/src/Avalonia.Controls/SystemDialog.cs @@ -4,30 +4,65 @@ using System.Linq; using System.Threading.Tasks; using Avalonia.Controls.Platform; +#nullable enable + namespace Avalonia.Controls { + /// + /// Base class for system file dialogs. + /// public abstract class FileDialog : FileSystemDialog { + /// + /// Gets or sets a collection of filters which determine the types of files displayed in an + /// or an . + /// public List Filters { get; set; } = new List(); - public string InitialFileName { get; set; } + + /// + /// Gets or sets initial file name that is displayed when the dialog is opened. + /// + public string? InitialFileName { get; set; } } + /// + /// Base class for system file and directory dialogs. + /// public abstract class FileSystemDialog : SystemDialog { [Obsolete("Use Directory")] - public string InitialDirectory + public string? InitialDirectory { get => Directory; set => Directory = value; } - public string Directory { get; set; } + + /// + /// Gets or sets the initial directory that will be displayed when the file system dialog + /// is opened. + /// + public string? Directory { get; set; } } + /// + /// Represents a system dialog that prompts the user to select a location for saving a file. + /// public class SaveFileDialog : FileDialog { - public string DefaultExtension { get; set; } + /// + /// Gets or sets the default extension to be used to save the file (including the period "."). + /// + public string? DefaultExtension { get; set; } - public async Task ShowAsync(Window parent) + /// + /// Shows the save file dialog. + /// + /// The parent window. + /// + /// A task that on completion contains the full path of the save location, or null if the + /// dialog was canceled. + /// + public async Task ShowAsync(Window parent) { if(parent == null) throw new ArgumentNullException(nameof(parent)); @@ -37,11 +72,25 @@ namespace Avalonia.Controls } } + /// + /// Represents a system dialog that allows the user to select one or more files to open. + /// public class OpenFileDialog : FileDialog { + /// + /// Gets or sets a value indicating whether the user can select multiple files. + /// public bool AllowMultiple { get; set; } - public Task ShowAsync(Window parent) + /// + /// Shows the open file dialog. + /// + /// The parent window. + /// + /// A task that on completion returns an array containing the full path to the selected + /// files, or null if the dialog was canceled. + /// + public Task ShowAsync(Window parent) { if(parent == null) throw new ArgumentNullException(nameof(parent)); @@ -49,15 +98,27 @@ namespace Avalonia.Controls } } + /// + /// Represents a system dialog that allows the user to select a directory. + /// public class OpenFolderDialog : FileSystemDialog { [Obsolete("Use Directory")] - public string DefaultDirectory + public string? DefaultDirectory { get => Directory; set => Directory = value; } - public Task ShowAsync(Window parent) + + /// + /// Shows the open folder dialog. + /// + /// The parent window. + /// + /// A task that on completion returns the full path of the selected directory, or null if the + /// dialog was canceled. + /// + public Task ShowAsync(Window parent) { if(parent == null) throw new ArgumentNullException(nameof(parent)); @@ -65,14 +126,32 @@ namespace Avalonia.Controls } } + + /// + /// Base class for system dialogs. + /// public abstract class SystemDialog { - public string Title { get; set; } + /// + /// Gets or sets the dialog title. + /// + public string? Title { get; set; } } + /// + /// Represents a filter in an or an . + /// public class FileDialogFilter { - public string Name { get; set; } + /// + /// Gets or sets the name of the filter, e.g. ("Text files (.txt)"). + /// + public string? Name { get; set; } + + /// + /// Gets or sets a list of file extensions matched by the filter (e.g. "txt" or "*" for all + /// files). + /// public List Extensions { get; set; } = new List(); } } From 1a95905efe9a3c00f712c0e68c9fefaceb23103e Mon Sep 17 00:00:00 2001 From: Sergey Mikolaytis Date: Wed, 25 Aug 2021 03:37:06 +0300 Subject: [PATCH 09/23] [Menu] [Interaction] Allow end user to change menu show delay globally (#6392) Co-authored-by: Max Katz --- src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs index 984faa4d60..209feb351c 100644 --- a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs +++ b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs @@ -115,7 +115,7 @@ namespace Avalonia.Controls.Platform protected IMenu? Menu { get; private set; } - protected static TimeSpan MenuShowDelay { get; } = TimeSpan.FromMilliseconds(400); + public static TimeSpan MenuShowDelay { get; set; } = TimeSpan.FromMilliseconds(400); protected internal virtual void GotFocus(object sender, GotFocusEventArgs e) { From dcc033f8197dd857ab8b02faef5ce81f3873fb37 Mon Sep 17 00:00:00 2001 From: kaminova <45092470+kaminova@users.noreply.github.com> Date: Wed, 25 Aug 2021 04:13:54 +0200 Subject: [PATCH 10/23] Close popup if combobox is not visible (#6404) * Close popup if combobox is not visible * fix unsupported feature Co-authored-by: kaminova Co-authored-by: Max Katz --- src/Avalonia.Controls/ComboBox.cs | 30 ++++++++++---- .../Mixins/DisposableMixin.cs | 38 +++++++++++++++++ src/Avalonia.Controls/Primitives/Popup.cs | 41 ++++++++----------- 3 files changed, 77 insertions(+), 32 deletions(-) create mode 100644 src/Avalonia.Controls/Mixins/DisposableMixin.cs diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index 57c07916db..274696d501 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -1,6 +1,8 @@ using System; using System.Linq; +using System.Reactive.Disposables; using Avalonia.Controls.Generators; +using Avalonia.Controls.Mixins; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Shapes; @@ -80,7 +82,7 @@ namespace Avalonia.Controls private bool _isDropDownOpen; private Popup _popup; private object _selectionBoxItem; - private IDisposable _subscriptionsOnOpen; + private readonly CompositeDisposable _subscriptionsOnOpen = new CompositeDisposable(); /// /// Initializes static members of the class. @@ -291,6 +293,7 @@ namespace Avalonia.Controls _popup = e.NameScope.Get("PART_Popup"); _popup.Opened += PopupOpened; + _popup.Closed += PopupClosed; } internal void ItemFocused(ComboBoxItem dropDownItem) @@ -303,8 +306,7 @@ namespace Avalonia.Controls private void PopupClosed(object sender, EventArgs e) { - _subscriptionsOnOpen?.Dispose(); - _subscriptionsOnOpen = null; + _subscriptionsOnOpen.Clear(); if (CanFocus(this)) { @@ -316,20 +318,34 @@ namespace Avalonia.Controls { TryFocusSelectedItem(); - _subscriptionsOnOpen?.Dispose(); - _subscriptionsOnOpen = null; + _subscriptionsOnOpen.Clear(); var toplevel = this.GetVisualRoot() as TopLevel; if (toplevel != null) { - _subscriptionsOnOpen = toplevel.AddDisposableHandler(PointerWheelChangedEvent, (s, ev) => + toplevel.AddDisposableHandler(PointerWheelChangedEvent, (s, ev) => { //eat wheel scroll event outside dropdown popup while it's open if (IsDropDownOpen && (ev.Source as IVisual).GetVisualRoot() == toplevel) { ev.Handled = true; } - }, Interactivity.RoutingStrategies.Tunnel); + }, Interactivity.RoutingStrategies.Tunnel).DisposeWith(_subscriptionsOnOpen); + } + + this.GetObservable(IsVisibleProperty).Subscribe(IsVisibleChanged).DisposeWith(_subscriptionsOnOpen); + + foreach (var parent in this.GetVisualAncestors().OfType()) + { + parent.GetObservable(IsVisibleProperty).Subscribe(IsVisibleChanged).DisposeWith(_subscriptionsOnOpen); + } + } + + private void IsVisibleChanged(bool isVisible) + { + if (!isVisible && IsDropDownOpen) + { + IsDropDownOpen = false; } } diff --git a/src/Avalonia.Controls/Mixins/DisposableMixin.cs b/src/Avalonia.Controls/Mixins/DisposableMixin.cs new file mode 100644 index 0000000000..9b30b4ba4c --- /dev/null +++ b/src/Avalonia.Controls/Mixins/DisposableMixin.cs @@ -0,0 +1,38 @@ +using System; +using System.Reactive.Disposables; + +namespace Avalonia.Controls.Mixins +{ + /// + /// Extension methods associated with the IDisposable interface. + /// + public static class DisposableMixin + { + /// + /// Ensures the provided disposable is disposed with the specified . + /// + /// + /// The type of the disposable. + /// + /// + /// The disposable we are going to want to be disposed by the CompositeDisposable. + /// + /// + /// The to which will be added. + /// + /// + /// The disposable. + /// + public static T DisposeWith(this T item, CompositeDisposable compositeDisposable) + where T : IDisposable + { + if (compositeDisposable is null) + { + throw new ArgumentNullException(nameof(compositeDisposable)); + } + + compositeDisposable.Add(item); + return item; + } + } +} diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index d5fb69a672..e804c4b4a9 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -2,6 +2,7 @@ using System; using System.ComponentModel; using System.Linq; using System.Reactive.Disposables; +using Avalonia.Controls.Mixins; using Avalonia.Controls.Diagnostics; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives.PopupPositioning; @@ -393,18 +394,8 @@ namespace Avalonia.Controls.Primitives var handlerCleanup = new CompositeDisposable(5); - void DeferCleanup(IDisposable? disposable) - { - if (disposable is null) - { - return; - } - - handlerCleanup.Add(disposable); - } - - DeferCleanup(popupHost.BindConstraints(this, WidthProperty, MinWidthProperty, MaxWidthProperty, - HeightProperty, MinHeightProperty, MaxHeightProperty, TopmostProperty)); + popupHost.BindConstraints(this, WidthProperty, MinWidthProperty, MaxWidthProperty, + HeightProperty, MinHeightProperty, MaxHeightProperty, TopmostProperty).DisposeWith(handlerCleanup); popupHost.SetChild(Child); ((ISetLogicalParent)popupHost).SetParent(this); @@ -418,19 +409,19 @@ namespace Avalonia.Controls.Primitives PlacementConstraintAdjustment, PlacementRect); - DeferCleanup(SubscribeToEventHandler>(popupHost, RootTemplateApplied, + SubscribeToEventHandler>(popupHost, RootTemplateApplied, (x, handler) => x.TemplateApplied += handler, - (x, handler) => x.TemplateApplied -= handler)); + (x, handler) => x.TemplateApplied -= handler).DisposeWith(handlerCleanup); if (topLevel is Window window) { - DeferCleanup(SubscribeToEventHandler(window, WindowDeactivated, + SubscribeToEventHandler(window, WindowDeactivated, (x, handler) => x.Deactivated += handler, - (x, handler) => x.Deactivated -= handler)); + (x, handler) => x.Deactivated -= handler).DisposeWith(handlerCleanup); - DeferCleanup(SubscribeToEventHandler(window.PlatformImpl, WindowLostFocus, + SubscribeToEventHandler(window.PlatformImpl, WindowLostFocus, (x, handler) => x.LostFocus += handler, - (x, handler) => x.LostFocus -= handler)); + (x, handler) => x.LostFocus -= handler).DisposeWith(handlerCleanup); } else { @@ -438,13 +429,13 @@ namespace Avalonia.Controls.Primitives if (parentPopupRoot?.Parent is Popup popup) { - DeferCleanup(SubscribeToEventHandler>(popup, ParentClosed, + SubscribeToEventHandler>(popup, ParentClosed, (x, handler) => x.Closed += handler, - (x, handler) => x.Closed -= handler)); + (x, handler) => x.Closed -= handler).DisposeWith(handlerCleanup); } } - DeferCleanup(InputManager.Instance?.Process.Subscribe(ListenForNonClientClick)); + InputManager.Instance?.Process.Subscribe(ListenForNonClientClick).DisposeWith(handlerCleanup); var cleanupPopup = Disposable.Create((popupHost, handlerCleanup), state => { @@ -466,17 +457,17 @@ namespace Avalonia.Controls.Primitives dismissLayer.IsVisible = true; dismissLayer.InputPassThroughElement = _overlayInputPassThroughElement; - DeferCleanup(Disposable.Create(() => + Disposable.Create(() => { dismissLayer.IsVisible = false; dismissLayer.InputPassThroughElement = null; - })); + }).DisposeWith(handlerCleanup); - DeferCleanup(SubscribeToEventHandler>( + SubscribeToEventHandler>( dismissLayer, PointerPressedDismissOverlay, (x, handler) => x.PointerPressed += handler, - (x, handler) => x.PointerPressed -= handler)); + (x, handler) => x.PointerPressed -= handler).DisposeWith(handlerCleanup); } } From cd80536d58834f52e6b2746463dbb2b9c0cc4759 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 25 Aug 2021 18:21:34 +0200 Subject: [PATCH 11/23] Added failing test for #6439. --- .../KeyboardNavigationTests_Tab.cs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Tab.cs b/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Tab.cs index edcbf75a1d..9a117bb71d 100644 --- a/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Tab.cs +++ b/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Tab.cs @@ -1225,5 +1225,32 @@ namespace Avalonia.Input.UnitTests "Button2", "Button3", "Button5", "Button1", "Button6", "Button4" }, result); } + + [Fact] + public void Cannot_Focus_Child_Of_Disabled_Control() + { + Button start; + Button expected; + + var top = new StackPanel + { + [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle, + Children = + { + (start = new Button { Name = "Button1" }), + new Border + { + IsEnabled = false, + Child = new Button { Name = "Button2" }, + }, + (expected = new Button { Name = "Button3" }), + } + }; + + var current = (IInputElement)start; + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Next); + + Assert.Same(expected, result); + } } } From 341e435321df26178dcaddfea768eaaa65a8b4d6 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 25 Aug 2021 19:10:18 +0200 Subject: [PATCH 12/23] Check enabled state as well as visibility. WPF and Avalonia's `IsEnabled` properties are slightly different. In WPF if reflects both the enabled state of the actual control and the effectively enabled state which comes from ancestor controls. In Avalonia that effectively enabled state is exposed on a different property: `IsEffectivelyEnabled`. When I ported the tab navigation code from WPF, I didn't take that into account. WPF's visibility property however doesn't reflect the state of a control's owners and so tab navigation for invisible controls works correctly. Take advantage of this fact by changing any checks for `IsVisible` to also check `IsEnabled`. --- src/Avalonia.Input/Navigation/TabNavigation.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Input/Navigation/TabNavigation.cs b/src/Avalonia.Input/Navigation/TabNavigation.cs index ed7df67bf2..c8290cb3b7 100644 --- a/src/Avalonia.Input/Navigation/TabNavigation.cs +++ b/src/Avalonia.Input/Navigation/TabNavigation.cs @@ -234,7 +234,7 @@ namespace Avalonia.Input.Navigation // Return the first visible element. var uiElement = e as InputElement; - if (uiElement is null || uiElement.IsVisible) + if (uiElement is null || IsVisibleAndEnabled(uiElement)) { if (e is IVisual elementAsVisual) { @@ -245,7 +245,7 @@ namespace Avalonia.Input.Navigation { if (children[i] is InputElement ie) { - if (ie.IsVisible) + if (IsVisibleAndEnabled(ie)) return ie; else { @@ -270,7 +270,7 @@ namespace Avalonia.Input.Navigation // Return the last visible element. var uiElement = e as InputElement; - if (uiElement == null || uiElement.IsVisible) + if (uiElement == null || IsVisibleAndEnabled(uiElement)) { var elementAsVisual = e as IVisual; @@ -283,7 +283,7 @@ namespace Avalonia.Input.Navigation { if (children[i] is InputElement ie) { - if (ie.IsVisible) + if (IsVisibleAndEnabled(ie)) return ie; else { @@ -600,7 +600,7 @@ namespace Avalonia.Input.Navigation var vchild = children[i]; if (vchild == elementAsVisual) break; - if (vchild.IsVisible == true && vchild is IInputElement ie) + if (vchild is IInputElement ie && IsVisibleAndEnabled(ie)) prev = ie; } return prev; @@ -668,5 +668,6 @@ namespace Avalonia.Input.Navigation } private static bool IsTabStopOrGroup(IInputElement e) => IsTabStop(e) || IsGroup(e); + private static bool IsVisibleAndEnabled(IInputElement e) => e.IsVisible && e.IsEnabled; } } From 8dbf3bf86e2ecdb3432a18ad02ab4c45189f37d2 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Thu, 26 Aug 2021 17:57:35 +0200 Subject: [PATCH 13/23] fixes(Win32.Interop): field 'Direct2DImageSurface._oldDpi' is never assigned to, and will always have its default value --- src/Windows/Avalonia.Win32.Interop/Wpf/Direct2DImageSurface.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Windows/Avalonia.Win32.Interop/Wpf/Direct2DImageSurface.cs b/src/Windows/Avalonia.Win32.Interop/Wpf/Direct2DImageSurface.cs index 5b04c5d7ff..bc0a399d7f 100644 --- a/src/Windows/Avalonia.Win32.Interop/Wpf/Direct2DImageSurface.cs +++ b/src/Windows/Avalonia.Win32.Interop/Wpf/Direct2DImageSurface.cs @@ -150,6 +150,7 @@ namespace Avalonia.Win32.Interop.Wpf if (_image == null || _oldDpi.X != dpi.X || _oldDpi.Y != dpi.Y) { _image = new D3DImage(dpi.X, dpi.Y); + _oldDpi = dpi; } _impl.ImageSource = _image; From 3867a68ade09cd12910afc5256e7b73c05703571 Mon Sep 17 00:00:00 2001 From: Tako <53405089+Takoooooo@users.noreply.github.com> Date: Thu, 26 Aug 2021 22:07:58 +0300 Subject: [PATCH 14/23] Fix EntryPointNotFound on Windows 8 due to missing version check (#6471) * Prevent calls to unavailable entrypoint 'GetDpiForMonitor' on Win8 (#5357) * Introduce Windows8 platform constant * wip Co-authored-by: Tim Schneeberger Co-authored-by: Tim Schneeberger Co-authored-by: Dan Walmsley --- src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj | 1 + src/Windows/Avalonia.Direct2D1/HwndRenderTarget.cs | 3 ++- src/Windows/Avalonia.Win32/FramebufferManager.cs | 2 +- src/Windows/Avalonia.Win32/PlatformConstants.cs | 7 ++++++- src/Windows/Avalonia.Win32/Win32GlManager.cs | 4 +--- src/Windows/Avalonia.Win32/Win32Platform.cs | 2 +- src/Windows/Avalonia.Win32/WindowImpl.cs | 4 ++-- 7 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj b/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj index cda95d2ebb..8533e1e176 100644 --- a/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj +++ b/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj @@ -12,6 +12,7 @@ + diff --git a/src/Windows/Avalonia.Direct2D1/HwndRenderTarget.cs b/src/Windows/Avalonia.Direct2D1/HwndRenderTarget.cs index 589f85f208..0597e88ed6 100644 --- a/src/Windows/Avalonia.Direct2D1/HwndRenderTarget.cs +++ b/src/Windows/Avalonia.Direct2D1/HwndRenderTarget.cs @@ -1,4 +1,5 @@ using Avalonia.Platform; +using Avalonia.Win32; using Avalonia.Win32.Interop; using SharpDX; using SharpDX.DXGI; @@ -21,7 +22,7 @@ namespace Avalonia.Direct2D1 protected override Size2F GetWindowDpi() { - if (UnmanagedMethods.ShCoreAvailable) + if (UnmanagedMethods.ShCoreAvailable && Win32Platform.WindowsVersion > PlatformConstants.Windows8) { uint dpix, dpiy; diff --git a/src/Windows/Avalonia.Win32/FramebufferManager.cs b/src/Windows/Avalonia.Win32/FramebufferManager.cs index 699dc7c25d..0240ac9701 100644 --- a/src/Windows/Avalonia.Win32/FramebufferManager.cs +++ b/src/Windows/Avalonia.Win32/FramebufferManager.cs @@ -87,7 +87,7 @@ namespace Avalonia.Win32 private Vector GetCurrentDpi() { - if (UnmanagedMethods.ShCoreAvailable) + if (UnmanagedMethods.ShCoreAvailable && Win32Platform.WindowsVersion > PlatformConstants.Windows8) { var monitor = UnmanagedMethods.MonitorFromWindow(_hwnd, UnmanagedMethods.MONITOR.MONITOR_DEFAULTTONEAREST); diff --git a/src/Windows/Avalonia.Win32/PlatformConstants.cs b/src/Windows/Avalonia.Win32/PlatformConstants.cs index af1eca29be..9dd4780637 100644 --- a/src/Windows/Avalonia.Win32/PlatformConstants.cs +++ b/src/Windows/Avalonia.Win32/PlatformConstants.cs @@ -1,8 +1,13 @@ +using System; + namespace Avalonia.Win32 { - static class PlatformConstants + public static class PlatformConstants { public const string WindowHandleType = "HWND"; public const string CursorHandleType = "HCURSOR"; + + public static readonly Version Windows8 = new Version(6, 2); + public static readonly Version Windows7 = new Version(6, 1); } } diff --git a/src/Windows/Avalonia.Win32/Win32GlManager.cs b/src/Windows/Avalonia.Win32/Win32GlManager.cs index e159ba3b78..e70ea52106 100644 --- a/src/Windows/Avalonia.Win32/Win32GlManager.cs +++ b/src/Windows/Avalonia.Win32/Win32GlManager.cs @@ -1,4 +1,3 @@ -using System; using Avalonia.OpenGL; using Avalonia.OpenGL.Angle; using Avalonia.OpenGL.Egl; @@ -9,7 +8,6 @@ namespace Avalonia.Win32 { static class Win32GlManager { - private static readonly Version Windows7 = new Version(6, 1); public static void Initialize() { @@ -22,7 +20,7 @@ namespace Avalonia.Win32 return wgl; } - if (opts?.AllowEglInitialization ?? Win32Platform.WindowsVersion > Windows7) + if (opts?.AllowEglInitialization ?? Win32Platform.WindowsVersion > PlatformConstants.Windows7) { var egl = EglPlatformOpenGlInterface.TryCreate(() => new AngleWin32EglDisplay()); diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index a881c45cd0..c011a458c3 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -94,7 +94,7 @@ namespace Avalonia namespace Avalonia.Win32 { - class Win32Platform : IPlatformThreadingInterface, IPlatformSettings, IWindowingPlatform, IPlatformIconLoader, IPlatformLifetimeEventsImpl + public class Win32Platform : IPlatformThreadingInterface, IPlatformSettings, IWindowingPlatform, IPlatformIconLoader, IPlatformLifetimeEventsImpl { private static readonly Win32Platform s_instance = new Win32Platform(); private static Thread _uiThread; diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 0b7bd13082..3b7d3efa2f 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -764,8 +764,8 @@ namespace Avalonia.Win32 RegisterTouchWindow(_hwnd, 0); } - if (ShCoreAvailable) - { + if (ShCoreAvailable && Win32Platform.WindowsVersion > PlatformConstants.Windows8) + { var monitor = MonitorFromWindow( _hwnd, MONITOR.MONITOR_DEFAULTTONEAREST); From 8b6f264379637b3b2c0a770327171765e880edc5 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Fri, 27 Aug 2021 15:50:55 +0200 Subject: [PATCH 15/23] ref(test): removed warning CS0105 The using directive for 'System' appeared previously in this namespace --- tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs b/tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs index 2a3db9992c..b0eb694944 100644 --- a/tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs @@ -10,7 +10,6 @@ using Avalonia.Styling; using Avalonia.UnitTests; using Moq; using Xunit; -using System; using Avalonia.Input.Raw; using Factory = System.Func, Avalonia.Controls.Window, Avalonia.AvaloniaObject>; From 7d8e6ea75f908f334f4413af18780f7946ac1a9f Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Fri, 27 Aug 2021 15:54:33 +0200 Subject: [PATCH 16/23] ref(tests): warning CS0067 The event 'KeyboardDeviceTests.DelegateCommand.CanExecuteChanged' is never used --- tests/Avalonia.Input.UnitTests/KeyboardDeviceTests.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/Avalonia.Input.UnitTests/KeyboardDeviceTests.cs b/tests/Avalonia.Input.UnitTests/KeyboardDeviceTests.cs index 7730cee78c..c354dbe72e 100644 --- a/tests/Avalonia.Input.UnitTests/KeyboardDeviceTests.cs +++ b/tests/Avalonia.Input.UnitTests/KeyboardDeviceTests.cs @@ -2,7 +2,6 @@ using System.Windows.Input; using Avalonia.Controls; using Avalonia.Input.Raw; -using Avalonia.Interactivity; using Avalonia.UnitTests; using Moq; using Xunit; @@ -126,7 +125,7 @@ namespace Avalonia.Input.UnitTests { private readonly Action _action; public DelegateCommand(Action action) => _action = action; - public event EventHandler CanExecuteChanged; + public event EventHandler CanExecuteChanged { add { } remove { } } public bool CanExecute(object parameter) => true; public void Execute(object parameter) => _action(); } From df5250e155410e7cbb238fdc75a9f7ee71816aaf Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Fri, 27 Aug 2021 16:16:06 +0200 Subject: [PATCH 17/23] ref(tests): warning CS0067 The event 'TrackingResourceProvider.OwnerChanged' is never used --- .../MarkupExtensions/DynamicResourceExtensionTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs index e7f0230254..592dbfc0d1 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs @@ -923,7 +923,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions public bool HasResources => true; public List RequestedResources { get; } = new List(); - public event EventHandler OwnerChanged; + public event EventHandler OwnerChanged { add { } remove { } } public void AddOwner(IResourceHost owner) => Owner = owner; public void RemoveOwner(IResourceHost owner) => Owner = null; From 7ee5446ee4838b06d483e1cfe47e69812219029a Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Fri, 27 Aug 2021 18:12:06 +0200 Subject: [PATCH 18/23] fixes(DataGrid): Warning CS0649 Field 'DataGridPathGroupDescription._valueConverter' is never assigned to, and will always have its default value null --- .../Collections/DataGridGroupDescription.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid/Collections/DataGridGroupDescription.cs b/src/Avalonia.Controls.DataGrid/Collections/DataGridGroupDescription.cs index 9d8ebbfac1..587dd228a3 100644 --- a/src/Avalonia.Controls.DataGrid/Collections/DataGridGroupDescription.cs +++ b/src/Avalonia.Controls.DataGrid/Collections/DataGridGroupDescription.cs @@ -83,8 +83,9 @@ namespace Avalonia.Collections if (key == null) key = item; - if (_valueConverter != null) - key = _valueConverter.Convert(key, typeof(object), level, culture); + var valueConverter = ValueConverter; + if (valueConverter != null) + key = valueConverter.Convert(key, typeof(object), level, culture); return key; } @@ -99,6 +100,8 @@ namespace Avalonia.Collections } public override string PropertyName => _propertyPath; + public IValueConverter ValueConverter { get => _valueConverter; set => _valueConverter = value; } + private Type GetPropertyType(object o) { return o.GetType().GetNestedPropertyType(_propertyPath); From 56709c9982d78063984c4865278a7afd913e571b Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sun, 29 Aug 2021 14:59:29 -0400 Subject: [PATCH 19/23] Add text box clipboard events --- src/Avalonia.Controls/TextBox.cs | 47 +++++++++++++++---- .../TextBoxClipboardEventArgs.cs | 18 +++++++ 2 files changed, 57 insertions(+), 8 deletions(-) create mode 100644 src/Avalonia.Controls/TextBoxClipboardEventArgs.cs diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 0eade8d6df..657d8fae8f 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -500,6 +500,10 @@ namespace Avalonia.Controls } } + public event EventHandler CopyingToClipboard; + public event EventHandler CuttingToClipboard; + public event EventHandler PastingFromClipboard; + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { _presenter = e.NameScope.Get("PART_TextPresenter"); @@ -638,27 +642,54 @@ namespace Avalonia.Controls public async void Cut() { var text = GetSelection(); - if (text is null) return; + if (string.IsNullOrEmpty(text)) + { + return; + } - SnapshotUndoRedo(); - Copy(); - DeleteSelection(); + var eventArgs = new TextBoxClipboardEventArgs(); + CuttingToClipboard?.Invoke(this, eventArgs); + if (!eventArgs.Handled) + { + SnapshotUndoRedo(); + await ((IClipboard)AvaloniaLocator.Current.GetService(typeof(IClipboard))) + .SetTextAsync(text); + DeleteSelection(); + } } public async void Copy() { var text = GetSelection(); - if (text is null) return; + if (string.IsNullOrEmpty(text)) + { + return; + } - await ((IClipboard)AvaloniaLocator.Current.GetService(typeof(IClipboard))) - .SetTextAsync(text); + var eventArgs = new TextBoxClipboardEventArgs(); + CopyingToClipboard?.Invoke(this, eventArgs); + if (!eventArgs.Handled) + { + await ((IClipboard)AvaloniaLocator.Current.GetService(typeof(IClipboard))) + .SetTextAsync(text); + } } public async void Paste() { + var eventArgs = new TextBoxClipboardEventArgs(); + PastingFromClipboard?.Invoke(this, eventArgs); + if (eventArgs.Handled) + { + return; + } + var text = await ((IClipboard)AvaloniaLocator.Current.GetService(typeof(IClipboard))).GetTextAsync(); - if (text is null) return; + if (string.IsNullOrEmpty(text)) + { + return; + } SnapshotUndoRedo(); HandleTextInput(text); diff --git a/src/Avalonia.Controls/TextBoxClipboardEventArgs.cs b/src/Avalonia.Controls/TextBoxClipboardEventArgs.cs new file mode 100644 index 0000000000..0fe4cadf39 --- /dev/null +++ b/src/Avalonia.Controls/TextBoxClipboardEventArgs.cs @@ -0,0 +1,18 @@ +using System; + +namespace Avalonia.Controls +{ + /// + /// Provides event data for the , and events. + /// + /// + /// If you perform any action in the handler for a clipboard event, set the Handled property to true; otherwise, the default action is performed. + /// + public class TextBoxClipboardEventArgs : EventArgs + { + /// + /// Gets or sets a value that marks the event as handled. A true value for Handled prevents most handlers along the event from handling the same event again. + /// + public bool Handled { get; set; } + } +} From 825ddc9ccaa2e3d7e002564f96f22c3b1d23a2d8 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sun, 29 Aug 2021 18:29:56 -0400 Subject: [PATCH 20/23] Use routed events --- src/Avalonia.Controls/TextBox.cs | 44 +++++++++++++++---- .../TextBoxClipboardEventArgs.cs | 18 -------- 2 files changed, 35 insertions(+), 27 deletions(-) delete mode 100644 src/Avalonia.Controls/TextBoxClipboardEventArgs.cs diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 657d8fae8f..9eae928eeb 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -145,6 +145,18 @@ namespace Avalonia.Controls (o, v) => o.UndoLimit = v, unsetValue: -1); + public static readonly RoutedEvent CopyingToClipboardEvent = + RoutedEvent.Register( + "CopyingToClipboard", RoutingStrategies.Bubble); + + public static readonly RoutedEvent CuttingToClipboardEvent = + RoutedEvent.Register( + "CuttingToClipboard", RoutingStrategies.Bubble); + + public static readonly RoutedEvent PastingFromClipboardEvent = + RoutedEvent.Register( + "PastingFromClipboard", RoutingStrategies.Bubble); + readonly struct UndoRedoState : IEquatable { public string Text { get; } @@ -500,9 +512,23 @@ namespace Avalonia.Controls } } - public event EventHandler CopyingToClipboard; - public event EventHandler CuttingToClipboard; - public event EventHandler PastingFromClipboard; + public event EventHandler CopyingToClipboard + { + add => AddHandler(CopyingToClipboardEvent, value); + remove => RemoveHandler(CopyingToClipboardEvent, value); + } + + public event EventHandler CuttingToClipboard + { + add => AddHandler(CuttingToClipboardEvent, value); + remove => RemoveHandler(CuttingToClipboardEvent, value); + } + + public event EventHandler PastingFromClipboard + { + add => AddHandler(PastingFromClipboardEvent, value); + remove => RemoveHandler(PastingFromClipboardEvent, value); + } protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { @@ -647,8 +673,8 @@ namespace Avalonia.Controls return; } - var eventArgs = new TextBoxClipboardEventArgs(); - CuttingToClipboard?.Invoke(this, eventArgs); + var eventArgs = new RoutedEventArgs(CuttingToClipboardEvent); + RaiseEvent(eventArgs); if (!eventArgs.Handled) { SnapshotUndoRedo(); @@ -666,8 +692,8 @@ namespace Avalonia.Controls return; } - var eventArgs = new TextBoxClipboardEventArgs(); - CopyingToClipboard?.Invoke(this, eventArgs); + var eventArgs = new RoutedEventArgs(CopyingToClipboardEvent); + RaiseEvent(eventArgs); if (!eventArgs.Handled) { await ((IClipboard)AvaloniaLocator.Current.GetService(typeof(IClipboard))) @@ -677,8 +703,8 @@ namespace Avalonia.Controls public async void Paste() { - var eventArgs = new TextBoxClipboardEventArgs(); - PastingFromClipboard?.Invoke(this, eventArgs); + var eventArgs = new RoutedEventArgs(PastingFromClipboardEvent); + RaiseEvent(eventArgs); if (eventArgs.Handled) { return; diff --git a/src/Avalonia.Controls/TextBoxClipboardEventArgs.cs b/src/Avalonia.Controls/TextBoxClipboardEventArgs.cs deleted file mode 100644 index 0fe4cadf39..0000000000 --- a/src/Avalonia.Controls/TextBoxClipboardEventArgs.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; - -namespace Avalonia.Controls -{ - /// - /// Provides event data for the , and events. - /// - /// - /// If you perform any action in the handler for a clipboard event, set the Handled property to true; otherwise, the default action is performed. - /// - public class TextBoxClipboardEventArgs : EventArgs - { - /// - /// Gets or sets a value that marks the event as handled. A true value for Handled prevents most handlers along the event from handling the same event again. - /// - public bool Handled { get; set; } - } -} From 10e3fc78283e3c6beff5ce7d066e8b703eeed8ef Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Mon, 30 Aug 2021 10:39:17 +0200 Subject: [PATCH 21/23] fixes(DataGrid): Warning CS0414 The field 'CellEditBinding.SubjectWrapper._settingSourceValue' is assigned but its value is never used --- .../Utils/CellEditBinding.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid/Utils/CellEditBinding.cs b/src/Avalonia.Controls.DataGrid/Utils/CellEditBinding.cs index 6ac77fbb99..1d1a595ccf 100644 --- a/src/Avalonia.Controls.DataGrid/Utils/CellEditBinding.cs +++ b/src/Avalonia.Controls.DataGrid/Utils/CellEditBinding.cs @@ -1,10 +1,8 @@ using Avalonia.Data; using Avalonia.Reactive; using System; -using System.ComponentModel.DataAnnotations; using System.Collections.Generic; using System.Reactive.Subjects; -using System.Text; namespace Avalonia.Controls.Utils { @@ -67,11 +65,14 @@ namespace Avalonia.Controls.Utils private void SetSourceValue(object value) { - _settingSourceValue = true; + if (!_settingSourceValue) + { + _settingSourceValue = true; - _sourceSubject.OnNext(value); + _sourceSubject.OnNext(value); - _settingSourceValue = false; + _settingSourceValue = false; + } } private void SetControlValue(object value) { @@ -157,4 +158,4 @@ namespace Avalonia.Controls.Utils } } } -} \ No newline at end of file +} From c8b97e358e5aac8c8a320ef45d64fe76f4a118af Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Mon, 30 Aug 2021 17:39:02 +0200 Subject: [PATCH 22/23] fixes(Doc): fixes AvaloniaList XML Comments --- src/Avalonia.Base/Collections/AvaloniaList.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Base/Collections/AvaloniaList.cs b/src/Avalonia.Base/Collections/AvaloniaList.cs index 2c7f34c5be..2f1cb2888e 100644 --- a/src/Avalonia.Base/Collections/AvaloniaList.cs +++ b/src/Avalonia.Base/Collections/AvaloniaList.cs @@ -280,8 +280,8 @@ namespace Avalonia.Collections /// /// Gets a range of items from the collection. /// - /// The first index to remove. - /// The number of items to remove. + /// The zero-based index at which the range starts. + /// The number of elements in the range. public IEnumerable GetRange(int index, int count) { return _inner.GetRange(index, count); @@ -455,7 +455,7 @@ namespace Avalonia.Collections } /// - /// Ensures that the capacity of the list is at least . + /// Ensures that the capacity of the list is at least . /// /// The capacity. public void EnsureCapacity(int capacity) From 3aa38398ff28012d7d296966a6df7d86e42f1424 Mon Sep 17 00:00:00 2001 From: Sergey Mikolaytis Date: Tue, 31 Aug 2021 23:47:25 +0300 Subject: [PATCH 23/23] Fix alt down shortcuts and allow alt down handling for end users (#6491) * [Menu] [Interaction] Allow end user to change menu show delay globally * Fix all alt down handle = true by AccessKeyHandler Co-authored-by: Max Katz --- src/Avalonia.Input/AccessKeyHandler.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Input/AccessKeyHandler.cs b/src/Avalonia.Input/AccessKeyHandler.cs index 5c4af68d79..5082265ea6 100644 --- a/src/Avalonia.Input/AccessKeyHandler.cs +++ b/src/Avalonia.Input/AccessKeyHandler.cs @@ -157,10 +157,9 @@ namespace Avalonia.Input _restoreFocusElement?.Focus(); _restoreFocusElement = null; + + e.Handled = true; } - - // We always handle the Alt key. - e.Handled = true; } else if (_altIsDown) {