From bb12a4c791d9cac66658097ebfe4c455f6174b0b Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Mon, 17 Jan 2022 12:00:10 +0200 Subject: [PATCH 01/14] Enable CompiledBindings for FluentTheme. --- src/Avalonia.Themes.Fluent/Controls/ButtonSpinner.xaml | 3 ++- .../Controls/CalendarDatePicker.xaml | 3 ++- src/Avalonia.Themes.Fluent/Controls/CalendarItem.xaml | 4 +++- .../Controls/DataValidationErrors.xaml | 8 +++++--- src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml | 3 ++- src/Avalonia.Themes.Fluent/Controls/Expander.xaml | 8 +++++--- src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml | 5 ++++- src/Avalonia.Themes.Fluent/Controls/NativeMenuBar.xaml | 3 ++- src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml | 8 +++++--- src/Avalonia.Themes.Fluent/Controls/TabItem.xaml | 3 ++- src/Avalonia.Themes.Fluent/Controls/TabStripItem.xaml | 3 ++- src/Avalonia.Themes.Fluent/Controls/TextBox.xaml | 2 +- src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml | 3 ++- src/Avalonia.Themes.Fluent/Controls/ToolTip.xaml | 3 ++- .../Controls/WindowNotificationManager.xaml | 4 +++- 15 files changed, 42 insertions(+), 21 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/ButtonSpinner.xaml b/src/Avalonia.Themes.Fluent/Controls/ButtonSpinner.xaml index f2344ab380..836cc27db3 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ButtonSpinner.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ButtonSpinner.xaml @@ -1,7 +1,8 @@ + xmlns:converters="clr-namespace:Avalonia.Controls.Converters;assembly=Avalonia.Controls" + x:CompileBindings="True"> diff --git a/src/Avalonia.Themes.Fluent/Controls/CalendarDatePicker.xaml b/src/Avalonia.Themes.Fluent/Controls/CalendarDatePicker.xaml index 26c3bbc19f..ffd3972b66 100644 --- a/src/Avalonia.Themes.Fluent/Controls/CalendarDatePicker.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/CalendarDatePicker.xaml @@ -7,7 +7,8 @@ + xmlns:sys="clr-namespace:System;assembly=netstandard" + x:CompileBindings="True"> + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + x:CompileBindings="True" + x:DataType="CalendarItem"> diff --git a/src/Avalonia.Themes.Fluent/Controls/DataValidationErrors.xaml b/src/Avalonia.Themes.Fluent/Controls/DataValidationErrors.xaml index d2fab37206..12e148d2f9 100644 --- a/src/Avalonia.Themes.Fluent/Controls/DataValidationErrors.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/DataValidationErrors.xaml @@ -1,6 +1,8 @@ + xmlns:sys="using:System" + x:CompileBindings="True" + x:DataType="DataValidationErrors"> @@ -27,7 +29,7 @@ diff --git a/src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml b/src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml index 3320fc9a41..9aa73fc52e 100644 --- a/src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml @@ -7,7 +7,8 @@ + xmlns:sys="clr-namespace:System;assembly=netstandard" + x:CompileBindings="True"> 40 1 diff --git a/src/Avalonia.Themes.Fluent/Controls/ToolTip.xaml b/src/Avalonia.Themes.Fluent/Controls/ToolTip.xaml index debdfb2772..2d18be91cb 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ToolTip.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ToolTip.xaml @@ -1,6 +1,7 @@ + xmlns:sys="clr-namespace:System;assembly=netstandard" + x:CompileBindings="True"> + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + x:DataType="WindowNotificationManager" + x:CompileBindings="True"> - \ No newline at end of file + From 36d6222c75048cf552b70ae10a7c883dc74fd650 Mon Sep 17 00:00:00 2001 From: Thehx Date: Wed, 19 Jan 2022 20:19:41 +0300 Subject: [PATCH 04/14] Fixed and exposed PreciseArcTo for ellipses with extreme width:height ratios --- .../Media/PreciseEllipticArcHelper.cs} | 45 ++++++++++---- .../Media/StreamGeometryContext.cs | 24 ++++++++ .../RenderHelpers/RenderHelpers.projitems | 1 - .../Media/StreamGeometryTests.cs | 56 ++++++++++++++++++ ...cs_In_All_Directions.deferred.expected.png | Bin 0 -> 6332 bytes ...s_In_All_Directions.immediate.expected.png | Bin 0 -> 6332 bytes ...cs_In_All_Directions.deferred.expected.png | Bin 0 -> 5828 bytes ...s_In_All_Directions.immediate.expected.png | Bin 0 -> 5828 bytes 8 files changed, 112 insertions(+), 14 deletions(-) rename src/{Shared/RenderHelpers/ArcToHelper.cs => Avalonia.Visuals/Media/PreciseEllipticArcHelper.cs} (97%) create mode 100644 tests/Avalonia.RenderTests/Media/StreamGeometryTests.cs create mode 100644 tests/TestFiles/Direct2D1/Media/StreamGeometry/PreciseEllipticArc_Produces_Valid_Arcs_In_All_Directions.deferred.expected.png create mode 100644 tests/TestFiles/Direct2D1/Media/StreamGeometry/PreciseEllipticArc_Produces_Valid_Arcs_In_All_Directions.immediate.expected.png create mode 100644 tests/TestFiles/Skia/Media/StreamGeometry/PreciseEllipticArc_Produces_Valid_Arcs_In_All_Directions.deferred.expected.png create mode 100644 tests/TestFiles/Skia/Media/StreamGeometry/PreciseEllipticArc_Produces_Valid_Arcs_In_All_Directions.immediate.expected.png diff --git a/src/Shared/RenderHelpers/ArcToHelper.cs b/src/Avalonia.Visuals/Media/PreciseEllipticArcHelper.cs similarity index 97% rename from src/Shared/RenderHelpers/ArcToHelper.cs rename to src/Avalonia.Visuals/Media/PreciseEllipticArcHelper.cs index 0bbf451970..5dd647e8ca 100644 --- a/src/Shared/RenderHelpers/ArcToHelper.cs +++ b/src/Avalonia.Visuals/Media/PreciseEllipticArcHelper.cs @@ -1,5 +1,6 @@ // Copyright © 2003-2004, Luc Maisonobe // 2015 - Alexey Rozanov - Adaptations for Avalonia and oval center computations +// 2022 - Alexey Rozanov - Fix for arcs sometimes drawn in inverted order. // All rights reserved. // // Redistribution and use in source and binary forms, with @@ -49,12 +50,10 @@ // Adapted from http://www.spaceroots.org/documents/ellipse/EllipticalArc.java using System; -using Avalonia.Media; -using Avalonia.Platform; -namespace Avalonia.RenderHelpers +namespace Avalonia.Media { - static class ArcToHelper + static class PreciseEllipticArcHelper { /// /// This class represents an elliptical arc on a 2D plane. @@ -292,6 +291,8 @@ namespace Avalonia.RenderHelpers /// internal double G2; + public bool DrawInOppositeDirection { get; set; } + /// /// Builds an elliptical arc composed of the full unit circle around (0,0) /// @@ -850,7 +851,7 @@ namespace Avalonia.RenderHelpers /// Builds the arc outline using given StreamGeometryContext and default (max) Bezier curve degree and acceptable error of half a pixel (0.5) /// /// A StreamGeometryContext to output the path commands to - public void BuildArc(IStreamGeometryContextImpl path) + public void BuildArc(StreamGeometryContext path) { BuildArc(path, _maxDegree, _defaultFlatness, true); } @@ -862,7 +863,7 @@ namespace Avalonia.RenderHelpers /// degree of the Bezier curve to use /// acceptable error /// if true, a new figure will be started in the specified StreamGeometryContext - public void BuildArc(IStreamGeometryContextImpl path, int degree, double threshold, bool openNewFigure) + public void BuildArc(StreamGeometryContext path, int degree, double threshold, bool openNewFigure) { if (degree < 1 || degree > _maxDegree) throw new ArgumentException($"degree should be between {1} and {_maxDegree}", nameof(degree)); @@ -888,8 +889,18 @@ namespace Avalonia.RenderHelpers } n = n << 1; } - dEta = (Eta2 - Eta1) / n; - etaB = Eta1; + if (!DrawInOppositeDirection) + { + dEta = (Eta2 - Eta1) / n; + etaB = Eta1; + } + else + { + dEta = (Eta1 - Eta2) / n; + etaB = Eta2; + } + + double cosEtaB = Math.Cos(etaB); double sinEtaB = Math.Sin(etaB); double aCosEtaB = A * cosEtaB; @@ -922,6 +933,7 @@ namespace Avalonia.RenderHelpers */ //otherwise we're supposed to be already at the (xB,yB) + double t = Math.Tan(0.5 * dEta); double alpha = Math.Sin(dEta) * (Math.Sqrt(4 + 3 * t * t) - 1) / 3; @@ -1012,7 +1024,7 @@ namespace Avalonia.RenderHelpers /// Ellipse theta (angle measured from the abscissa) /// Large Arc Indicator /// Clockwise direction flag - public static void BuildArc(IStreamGeometryContextImpl path, Point p1, Point p2, Size size, double theta, bool isLargeArc, bool clockwise) + public static void BuildArc(StreamGeometryContext path, Point p1, Point p2, Size size, double theta, bool isLargeArc, bool clockwise) { // var orthogonalizer = new RotateTransform(-theta); @@ -1058,7 +1070,7 @@ namespace Avalonia.RenderHelpers } - double multiplier = Math.Sqrt(numerator / denominator); + double multiplier = Math.Sqrt(Math.Abs(numerator / denominator)); Point mulVec = new Point(rx * p1S.Y / ry, -ry * p1S.X / rx); int sign = (clockwise != isLargeArc) ? 1 : -1; @@ -1104,9 +1116,16 @@ namespace Avalonia.RenderHelpers // path.LineTo(c, true, true); // path.LineTo(clockwise ? p1 : p2, true,true); - path.LineTo(clockwise ? p1 : p2); var arc = new EllipticalArc(c.X, c.Y, rx, ry, theta, thetaStart, thetaEnd, false); + + double ManhattanDistance(Point p1, Point p2) => Math.Abs(p1.X - p2.X) + Math.Abs(p1.Y - p2.Y); + if (ManhattanDistance(p2, new Point(arc.X2, arc.Y2)) > ManhattanDistance(p2, new Point(arc.X1, arc.Y1))) + { + arc.DrawInOppositeDirection = true; + } + arc.BuildArc(path, arc._maxDegree, arc._defaultFlatness, false); + //path.LineTo(p2); //uncomment this to draw a pie //path.LineTo(c, true, true); @@ -1136,9 +1155,9 @@ namespace Avalonia.RenderHelpers } } - public static void ArcTo(IStreamGeometryContextImpl streamGeometryContextImpl, Point currentPoint, Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection) + public static void ArcTo(StreamGeometryContext streamGeometryContextImpl, Point currentPoint, Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection) { - EllipticalArc.BuildArc(streamGeometryContextImpl, currentPoint, point, size, rotationAngle*Math.PI/180, + EllipticalArc.BuildArc(streamGeometryContextImpl, currentPoint, point, size, rotationAngle*(Math.PI/180), isLargeArc, sweepDirection == SweepDirection.Clockwise); } diff --git a/src/Avalonia.Visuals/Media/StreamGeometryContext.cs b/src/Avalonia.Visuals/Media/StreamGeometryContext.cs index 0bfd774c79..88aba8365e 100644 --- a/src/Avalonia.Visuals/Media/StreamGeometryContext.cs +++ b/src/Avalonia.Visuals/Media/StreamGeometryContext.cs @@ -15,6 +15,8 @@ namespace Avalonia.Media { private readonly IStreamGeometryContextImpl _impl; + private Point _currentPoint; + /// /// Initializes a new instance of the class. /// @@ -47,6 +49,24 @@ namespace Avalonia.Media public void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection) { _impl.ArcTo(point, size, rotationAngle, isLargeArc, sweepDirection); + _currentPoint = point; + } + + + /// + /// Draws an arc to the specified point using polylines, quadratic or cubic Bezier curves + /// Significantly more precise when drawing elliptic arcs with extreme width:height ratios. + /// + /// The destination point. + /// The radii of an oval whose perimeter is used to draw the angle. + /// The rotation angle of the oval that specifies the curve. + /// true to draw the arc greater than 180 degrees; otherwise, false. + /// + /// A value that indicates whether the arc is drawn in the Clockwise or Counterclockwise direction. + /// + public void PreciseArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection) + { + PreciseEllipticArcHelper.ArcTo(this, _currentPoint, point, size, rotationAngle, isLargeArc, sweepDirection); } /// @@ -57,6 +77,7 @@ namespace Avalonia.Media public void BeginFigure(Point startPoint, bool isFilled) { _impl.BeginFigure(startPoint, isFilled); + _currentPoint = startPoint; } /// @@ -68,6 +89,7 @@ namespace Avalonia.Media public void CubicBezierTo(Point point1, Point point2, Point point3) { _impl.CubicBezierTo(point1, point2, point3); + _currentPoint = point3; } /// @@ -78,6 +100,7 @@ namespace Avalonia.Media public void QuadraticBezierTo(Point control, Point endPoint) { _impl.QuadraticBezierTo(control, endPoint); + _currentPoint = endPoint; } /// @@ -87,6 +110,7 @@ namespace Avalonia.Media public void LineTo(Point point) { _impl.LineTo(point); + _currentPoint = point; } /// diff --git a/src/Shared/RenderHelpers/RenderHelpers.projitems b/src/Shared/RenderHelpers/RenderHelpers.projitems index c088097a9f..4c80ec50c4 100644 --- a/src/Shared/RenderHelpers/RenderHelpers.projitems +++ b/src/Shared/RenderHelpers/RenderHelpers.projitems @@ -9,7 +9,6 @@ Avalonia.RenderHelpers - \ No newline at end of file diff --git a/tests/Avalonia.RenderTests/Media/StreamGeometryTests.cs b/tests/Avalonia.RenderTests/Media/StreamGeometryTests.cs new file mode 100644 index 0000000000..fc9cbf6a7f --- /dev/null +++ b/tests/Avalonia.RenderTests/Media/StreamGeometryTests.cs @@ -0,0 +1,56 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Controls.Shapes; +using Avalonia.Media; +using Avalonia.Media.Imaging; +using Xunit; + +#if AVALONIA_SKIA +namespace Avalonia.Skia.RenderTests +#else +namespace Avalonia.Direct2D1.RenderTests.Media +#endif +{ + public class StreamGeometryTests : TestBase + { + public StreamGeometryTests() + : base(@"Media\StreamGeometry") + { + } + + [Fact] + public async Task PreciseEllipticArc_Produces_Valid_Arcs_In_All_Directions() + { + var grid = new Avalonia.Controls.Primitives.UniformGrid() { Columns = 2, Rows = 4, Width = 320, Height = 400 }; + foreach (var sweepDirection in new[] { SweepDirection.Clockwise, SweepDirection.CounterClockwise }) + foreach (var isLargeArc in new[] { false, true }) + foreach (var isPrecise in new[] { false, true }) + { + Point Pt(double x, double y) => new Point(x, y); + Size Sz(double w, double h) => new Size(w, h); + var streamGeometry = new StreamGeometry(); + using (var context = streamGeometry.Open()) + { + context.BeginFigure(Pt(20, 20), true); + + if(isPrecise) + context.PreciseArcTo(Pt(40, 40), Sz(20, 20), 0, isLargeArc, sweepDirection); + else + context.ArcTo(Pt(40, 40), Sz(20, 20), 0, isLargeArc, sweepDirection); + context.LineTo(Pt(40, 20)); + context.LineTo(Pt(20, 20)); + context.EndFigure(true); + } + var pathShape = new Avalonia.Controls.Shapes.Path(); + pathShape.Data = streamGeometry; + pathShape.Stroke = new SolidColorBrush(Colors.CornflowerBlue); + pathShape.Fill = new SolidColorBrush(Colors.Gold); + pathShape.StrokeThickness = 2; + pathShape.Margin = new Thickness(20); + grid.Children.Add(pathShape); + } + await RenderToFile(grid); + } + } +} diff --git a/tests/TestFiles/Direct2D1/Media/StreamGeometry/PreciseEllipticArc_Produces_Valid_Arcs_In_All_Directions.deferred.expected.png b/tests/TestFiles/Direct2D1/Media/StreamGeometry/PreciseEllipticArc_Produces_Valid_Arcs_In_All_Directions.deferred.expected.png new file mode 100644 index 0000000000000000000000000000000000000000..825b1d7ea7d2d1356f2798c162fed7572f8eb27b GIT binary patch literal 6332 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 H Date: Thu, 20 Jan 2022 10:23:35 +0000 Subject: [PATCH 05/14] Fix ClassicDesktop Lifetime so that ShutdownRequested event is raised even with programatic calls to Shutdown. --- .../ClassicDesktopStyleApplicationLifetime.cs | 71 +++++++++++-------- 1 file changed, 40 insertions(+), 31 deletions(-) diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs index 3a2fd68af5..536e39cc34 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs @@ -83,29 +83,9 @@ namespace Avalonia.Controls.ApplicationLifetimes public void Shutdown(int exitCode = 0) { - if (_isShuttingDown) - throw new InvalidOperationException("Application is already shutting down."); - - _exitCode = exitCode; - _isShuttingDown = true; - - try - { - foreach (var w in Windows) - w.Close(); - var e = new ControlledApplicationLifetimeExitEventArgs(exitCode); - Exit?.Invoke(this, e); - _exitCode = e.ApplicationExitCode; - } - finally - { - _cts?.Cancel(); - _cts = null; - _isShuttingDown = false; - } + DoShutdown(new ShutdownRequestedEventArgs(), exitCode); } - public int Start(string[] args) { Startup?.Invoke(this, new ControlledApplicationLifetimeStartupEventArgs(args)); @@ -145,23 +125,52 @@ namespace Avalonia.Controls.ApplicationLifetimes if (_activeLifetime == this) _activeLifetime = null; } - - private void OnShutdownRequested(object sender, ShutdownRequestedEventArgs e) + + private void DoShutdown(ShutdownRequestedEventArgs e, int exitCode = 0) { ShutdownRequested?.Invoke(this, e); if (e.Cancel) return; - // When an OS shutdown request is received, try to close all non-owned windows. Windows can cancel - // shutdown by setting e.Cancel = true in the Closing event. Owned windows will be shutdown by their - // owners. - foreach (var w in Windows) - if (w.Owner is null) - w.Close(); - if (Windows.Count > 0) - e.Cancel = true; + if (_isShuttingDown) + throw new InvalidOperationException("Application is already shutting down."); + + _exitCode = exitCode; + _isShuttingDown = true; + + try + { + // When an OS shutdown request is received, try to close all non-owned windows. Windows can cancel + // shutdown by setting e.Cancel = true in the Closing event. Owned windows will be shutdown by their + // owners. + foreach (var w in Windows) + { + if (w.Owner is null) + { + w.Close(); + } + } + + if (Windows.Count > 0) + { + e.Cancel = true; + return; + } + + var e = new ControlledApplicationLifetimeExitEventArgs(exitCode); + Exit?.Invoke(this, e); + _exitCode = e.ApplicationExitCode; + } + finally + { + _cts?.Cancel(); + _cts = null; + _isShuttingDown = false; + } } + + private void OnShutdownRequested(object sender, ShutdownRequestedEventArgs e) => DoShutdown(e); } public class ClassicDesktopStyleApplicationLifetimeOptions From aeee9d165fb8e7414bfe1b21f261639ad7b01b2c Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 20 Jan 2022 10:35:07 +0000 Subject: [PATCH 06/14] add TryShutdown method. --- .../ClassicDesktopStyleApplicationLifetime.cs | 15 +++++++++++---- .../IClassicDesktopStyleApplicationLifetime.cs | 6 ++++++ 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs index 536e39cc34..8006149ee9 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs @@ -83,7 +83,12 @@ namespace Avalonia.Controls.ApplicationLifetimes public void Shutdown(int exitCode = 0) { - DoShutdown(new ShutdownRequestedEventArgs(), exitCode); + DoShutdown(new ShutdownRequestedEventArgs(), true, exitCode); + } + + public bool TryShutdown(int exitCode = 0) + { + return DoShutdown(new ShutdownRequestedEventArgs(), false, exitCode); } public int Start(string[] args) @@ -126,12 +131,12 @@ namespace Avalonia.Controls.ApplicationLifetimes _activeLifetime = null; } - private void DoShutdown(ShutdownRequestedEventArgs e, int exitCode = 0) + private bool DoShutdown(ShutdownRequestedEventArgs e, bool force = false, int exitCode = 0) { ShutdownRequested?.Invoke(this, e); if (e.Cancel) - return; + false; if (_isShuttingDown) throw new InvalidOperationException("Application is already shutting down."); @@ -155,7 +160,7 @@ namespace Avalonia.Controls.ApplicationLifetimes if (Windows.Count > 0) { e.Cancel = true; - return; + return false; } var e = new ControlledApplicationLifetimeExitEventArgs(exitCode); @@ -168,6 +173,8 @@ namespace Avalonia.Controls.ApplicationLifetimes _cts = null; _isShuttingDown = false; } + + return true; } private void OnShutdownRequested(object sender, ShutdownRequestedEventArgs e) => DoShutdown(e); diff --git a/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs index a70d5dd2f1..a83229b732 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs @@ -9,6 +9,12 @@ namespace Avalonia.Controls.ApplicationLifetimes /// public interface IClassicDesktopStyleApplicationLifetime : IControlledApplicationLifetime { + /// + /// Tries to Shutdown the application. event can be used to cancel the shutdown. + /// + /// An integer exit code for an application. The default exit code is 0. + bool TryShutdown(int exitCode = 0); + /// /// Gets the arguments passed to the /// From 15def96af4bca61bffd415cd4af76223f35d3c0f Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 20 Jan 2022 10:37:38 +0000 Subject: [PATCH 07/14] fix quit menu item osx. --- src/Avalonia.Native/AvaloniaNativeMenuExporter.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs index de6ba30a85..09d7247527 100644 --- a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -133,9 +133,9 @@ namespace Avalonia.Native var quitItem = new NativeMenuItem("Quit") { Gesture = new KeyGesture(Key.Q, KeyModifiers.Meta) }; quitItem.Click += (_, _) => { - if (Application.Current is { ApplicationLifetime: IControlledApplicationLifetime lifetime }) + if (Application.Current is { ApplicationLifetime: IClassicDesktopStyleApplicationLifetime lifetime }) { - lifetime.Shutdown(); + lifetime.TryShutdown(); } }; From 2a4a1b28b1a65315caf42d8fec5e0b6e85cf8246 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 20 Jan 2022 10:38:11 +0000 Subject: [PATCH 08/14] add conditional force shutdown. --- src/Avalonia.Native/AvaloniaNativeMenuExporter.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs index 09d7247527..67a27d5500 100644 --- a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -137,6 +137,10 @@ namespace Avalonia.Native { lifetime.TryShutdown(); } + else + { + lifetime.Shutdown(); + } }; appMenu.Add(quitItem); From aae3b701807b44398b1a2dff180c62af3eabd6f4 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 20 Jan 2022 12:02:01 +0000 Subject: [PATCH 09/14] fix build. --- src/Avalonia.Controls/ApiCompatBaseline.txt | 3 +- .../ClassicDesktopStyleApplicationLifetime.cs | 28 +++++++++++-------- .../AvaloniaNativeMenuExporter.cs | 4 +-- 3 files changed, 21 insertions(+), 14 deletions(-) diff --git a/src/Avalonia.Controls/ApiCompatBaseline.txt b/src/Avalonia.Controls/ApiCompatBaseline.txt index 9b7d37e108..a7560c37f2 100644 --- a/src/Avalonia.Controls/ApiCompatBaseline.txt +++ b/src/Avalonia.Controls/ApiCompatBaseline.txt @@ -36,6 +36,7 @@ CannotRemoveBaseTypeOrInterface : Type 'Avalonia.Controls.WindowBase' does not i InterfacesShouldHaveSameMembers : Interface member 'public System.EventHandler Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.ShutdownRequested' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.add_ShutdownRequested(System.EventHandler)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.remove_ShutdownRequested(System.EventHandler)' is present in the implementation but not in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.TryShutdown(System.Int32)' is present in the implementation but not in the contract. CannotRemoveBaseTypeOrInterface : Type 'Avalonia.Controls.Embedding.EmbeddableControlRoot' does not implement interface 'Avalonia.Utilities.IWeakSubscriber' in the implementation but it does in the contract. MembersMustExist : Member 'public System.Action Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.Resized.get()' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.Resized.set(System.Action)' does not exist in the implementation but it does exist in the contract. @@ -62,4 +63,4 @@ InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platfor MembersMustExist : Member 'public void Avalonia.Platform.IWindowImpl.Resize(Avalonia.Size)' does not exist in the implementation but it does exist in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IWindowImpl.Resize(Avalonia.Size, Avalonia.Platform.PlatformResizeReason)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.ITrayIconImpl Avalonia.Platform.IWindowingPlatform.CreateTrayIcon()' is present in the implementation but not in the contract. -Total Issues: 63 +Total Issues: 64 diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs index 8006149ee9..9d30c529fb 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs @@ -99,7 +99,10 @@ namespace Avalonia.Controls.ApplicationLifetimes if(options != null && options.ProcessUrlActivationCommandLine && args.Length > 0) { - ((IApplicationPlatformEvents)Application.Current).RaiseUrlsOpened(args); + if (Application.Current is IApplicationPlatformEvents events) + { + events.RaiseUrlsOpened(args); + } } var lifetimeEvents = AvaloniaLocator.Current.GetService(); @@ -133,14 +136,17 @@ namespace Avalonia.Controls.ApplicationLifetimes private bool DoShutdown(ShutdownRequestedEventArgs e, bool force = false, int exitCode = 0) { - ShutdownRequested?.Invoke(this, e); + if (!force) + { + ShutdownRequested?.Invoke(this, e); + + if (e.Cancel) + return false; - if (e.Cancel) - false; + if (_isShuttingDown) + throw new InvalidOperationException("Application is already shutting down."); + } - if (_isShuttingDown) - throw new InvalidOperationException("Application is already shutting down."); - _exitCode = exitCode; _isShuttingDown = true; @@ -157,15 +163,15 @@ namespace Avalonia.Controls.ApplicationLifetimes } } - if (Windows.Count > 0) + if (!force && Windows.Count > 0) { e.Cancel = true; return false; } - var e = new ControlledApplicationLifetimeExitEventArgs(exitCode); - Exit?.Invoke(this, e); - _exitCode = e.ApplicationExitCode; + var args = new ControlledApplicationLifetimeExitEventArgs(exitCode); + Exit?.Invoke(this, args); + _exitCode = args.ApplicationExitCode; } finally { diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs index 67a27d5500..d8753efe25 100644 --- a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -137,9 +137,9 @@ namespace Avalonia.Native { lifetime.TryShutdown(); } - else + else if(Application.Current is {ApplicationLifetime: IControlledApplicationLifetime controlledLifetime}) { - lifetime.Shutdown(); + controlledLifetime.Shutdown(); } }; From ed35eeeb69ef807628944624d4e6ede99620ddbc Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 20 Jan 2022 12:06:59 +0000 Subject: [PATCH 10/14] allow MainWindow close mode and LastWindowClose mode to be cancellable. --- .../ClassicDesktopStyleApplicationLifetime.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs index 9d30c529fb..edddf31d45 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs @@ -76,9 +76,9 @@ namespace Avalonia.Controls.ApplicationLifetimes return; if (ShutdownMode == ShutdownMode.OnLastWindowClose && _windows.Count == 0) - Shutdown(); - else if (ShutdownMode == ShutdownMode.OnMainWindowClose && window == MainWindow) - Shutdown(); + TryShutdown(); + else if (ShutdownMode == ShutdownMode.OnMainWindowClose && ReferenceEquals(window, MainWindow)) + TryShutdown(); } public void Shutdown(int exitCode = 0) From 00c633ab3d01c1f7b270a42d3d8ac044fc30e29f Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 20 Jan 2022 12:20:04 +0000 Subject: [PATCH 11/14] Add a unit test to show we can now cancel window closing shutdown modes. --- .../DesktopStyleApplicationLifetimeTests.cs | 78 ++++++++++++++++++- 1 file changed, 76 insertions(+), 2 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs b/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs index f7a3bdea1c..6c68ab0249 100644 --- a/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs +++ b/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs @@ -1,9 +1,7 @@ using System; using System.Collections.Generic; -using System.ComponentModel; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Platform; -using Avalonia.Threading; using Avalonia.UnitTests; using Moq; using Xunit; @@ -238,5 +236,81 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(new[] { window }, lifetime.Windows); } } + + [Fact] + public void MainWindow_Closed_Shutdown_Should_Be_Cancellable() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + using(var lifetime = new ClassicDesktopStyleApplicationLifetime()) + { + lifetime.ShutdownMode = ShutdownMode.OnMainWindowClose; + + var hasExit = false; + + lifetime.Exit += (s, e) => hasExit = true; + + var mainWindow = new Window(); + + mainWindow.Show(); + + lifetime.MainWindow = mainWindow; + + var window = new Window(); + + window.Show(); + + var raised = 0; + + lifetime.ShutdownRequested += (s, e) => + { + e.Cancel = true; + ++raised; + }; + + mainWindow.Close(); + + Assert.Equal(1, raised); + Assert.False(hasExit); + } + } + + [Fact] + public void LastWindow_Closed_Shutdown_Should_Be_Cancellable() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + using(var lifetime = new ClassicDesktopStyleApplicationLifetime()) + { + lifetime.ShutdownMode = ShutdownMode.OnLastWindowClose; + + var hasExit = false; + + lifetime.Exit += (s, e) => hasExit = true; + + var windowA = new Window(); + + windowA.Show(); + + var windowB = new Window(); + + windowB.Show(); + + var raised = 0; + + lifetime.ShutdownRequested += (s, e) => + { + e.Cancel = true; + ++raised; + }; + + windowA.Close(); + + Assert.False(hasExit); + + windowB.Close(); + + Assert.Equal(1, raised); + Assert.False(hasExit); + } + } } } From 79fdf5ee5f70f08def6007ec303900d03bbef160 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 20 Jan 2022 12:31:46 +0000 Subject: [PATCH 12/14] Add another unit test to show that windows can cancel the shutdown. --- .../DesktopStyleApplicationLifetimeTests.cs | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs b/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs index 6c68ab0249..4d7a640420 100644 --- a/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs +++ b/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs @@ -312,5 +312,38 @@ namespace Avalonia.Controls.UnitTests Assert.False(hasExit); } } + + [Fact] + public void TryShutdown_Cancellable_By_Preventing_Window_Close() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + using(var lifetime = new ClassicDesktopStyleApplicationLifetime()) + { + var hasExit = false; + + lifetime.Exit += (s, e) => hasExit = true; + + var windowA = new Window(); + + windowA.Show(); + + var windowB = new Window(); + + windowB.Show(); + + var raised = 0; + + windowA.Closing += (sender, e) => + { + e.Cancel = true; + ++raised; + }; + + lifetime.TryShutdown(); + + Assert.Equal(1, raised); + Assert.False(hasExit); + } + } } } From 262f520335b0a5107cd09bcf597b7e419eca6be8 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 20 Jan 2022 12:37:20 +0000 Subject: [PATCH 13/14] Add more unit tests. --- .../DesktopStyleApplicationLifetimeTests.cs | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs b/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs index 4d7a640420..470e24aea7 100644 --- a/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs +++ b/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs @@ -345,5 +345,62 @@ namespace Avalonia.Controls.UnitTests Assert.False(hasExit); } } + + [Fact] + public void Shutdown_NotCancellable_By_Preventing_Window_Close() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + using(var lifetime = new ClassicDesktopStyleApplicationLifetime()) + { + var hasExit = false; + + lifetime.Exit += (s, e) => hasExit = true; + + var windowA = new Window(); + + windowA.Show(); + + var windowB = new Window(); + + windowB.Show(); + + var raised = 0; + + windowA.Closing += (sender, e) => + { + e.Cancel = true; + ++raised; + }; + + lifetime.Shutdown(); + + Assert.Equal(1, raised); + Assert.True(hasExit); + } + } + + [Fact] + public void Shutdown_Doesnt_Raise_Shutdown_Requested() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + using(var lifetime = new ClassicDesktopStyleApplicationLifetime()) + { + var hasExit = false; + + lifetime.Exit += (s, e) => hasExit = true; + + var raised = 0; + + lifetime.ShutdownRequested += (sender, e) => + { + ++raised; + }; + + lifetime.Shutdown(); + + Assert.Equal(0, raised); + Assert.True(hasExit); + } + } } } From 245d23e741b71bd5b0d44a4c37af652af60f50d8 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 20 Jan 2022 13:09:50 +0000 Subject: [PATCH 14/14] use discards where possible. --- .../DesktopStyleApplicationLifetimeTests.cs | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs b/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs index 470e24aea7..3a2e1c08bd 100644 --- a/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs +++ b/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs @@ -55,7 +55,7 @@ namespace Avalonia.Controls.UnitTests var hasExit = false; - lifetime.Exit += (s, e) => hasExit = true; + lifetime.Exit += (_, _) => hasExit = true; var windowA = new Window(); @@ -89,7 +89,7 @@ namespace Avalonia.Controls.UnitTests var hasExit = false; - lifetime.Exit += (s, e) => hasExit = true; + lifetime.Exit += (_, _) => hasExit = true; var mainWindow = new Window(); @@ -117,7 +117,7 @@ namespace Avalonia.Controls.UnitTests var hasExit = false; - lifetime.Exit += (s, e) => hasExit = true; + lifetime.Exit += (_, _) => hasExit = true; var windowA = new Window(); @@ -224,7 +224,7 @@ namespace Avalonia.Controls.UnitTests window.Show(); - lifetime.ShutdownRequested += (s, e) => + lifetime.ShutdownRequested += (_, e) => { e.Cancel = true; ++raised; @@ -247,7 +247,7 @@ namespace Avalonia.Controls.UnitTests var hasExit = false; - lifetime.Exit += (s, e) => hasExit = true; + lifetime.Exit += (_, _) => hasExit = true; var mainWindow = new Window(); @@ -261,7 +261,7 @@ namespace Avalonia.Controls.UnitTests var raised = 0; - lifetime.ShutdownRequested += (s, e) => + lifetime.ShutdownRequested += (_, e) => { e.Cancel = true; ++raised; @@ -284,7 +284,7 @@ namespace Avalonia.Controls.UnitTests var hasExit = false; - lifetime.Exit += (s, e) => hasExit = true; + lifetime.Exit += (_, _) => hasExit = true; var windowA = new Window(); @@ -296,7 +296,7 @@ namespace Avalonia.Controls.UnitTests var raised = 0; - lifetime.ShutdownRequested += (s, e) => + lifetime.ShutdownRequested += (_, e) => { e.Cancel = true; ++raised; @@ -321,7 +321,7 @@ namespace Avalonia.Controls.UnitTests { var hasExit = false; - lifetime.Exit += (s, e) => hasExit = true; + lifetime.Exit += (_, _) => hasExit = true; var windowA = new Window(); @@ -333,7 +333,7 @@ namespace Avalonia.Controls.UnitTests var raised = 0; - windowA.Closing += (sender, e) => + windowA.Closing += (_, e) => { e.Cancel = true; ++raised; @@ -354,7 +354,7 @@ namespace Avalonia.Controls.UnitTests { var hasExit = false; - lifetime.Exit += (s, e) => hasExit = true; + lifetime.Exit += (_, _) => hasExit = true; var windowA = new Window(); @@ -366,7 +366,7 @@ namespace Avalonia.Controls.UnitTests var raised = 0; - windowA.Closing += (sender, e) => + windowA.Closing += (_, e) => { e.Cancel = true; ++raised; @@ -387,11 +387,11 @@ namespace Avalonia.Controls.UnitTests { var hasExit = false; - lifetime.Exit += (s, e) => hasExit = true; + lifetime.Exit += (_, _) => hasExit = true; var raised = 0; - lifetime.ShutdownRequested += (sender, e) => + lifetime.ShutdownRequested += (_, _) => { ++raised; };