From e42bccd79b68dfad5aea0a7c5b046a96cf71a283 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 29 May 2021 11:47:37 +0200 Subject: [PATCH 001/127] Added TabIndex property. And also added `IsTabStop` property to `InputElement`. --- src/Avalonia.Input/InputElement.cs | 31 +++++++++++++++++++++++ src/Avalonia.Input/KeyboardNavigation.cs | 32 ++++++++++++++++++++++-- 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Input/InputElement.cs b/src/Avalonia.Input/InputElement.cs index 65b9acae76..63080e74e4 100644 --- a/src/Avalonia.Input/InputElement.cs +++ b/src/Avalonia.Input/InputElement.cs @@ -71,6 +71,12 @@ namespace Avalonia.Input public static readonly DirectProperty IsPointerOverProperty = AvaloniaProperty.RegisterDirect(nameof(IsPointerOver), o => o.IsPointerOver); + /// + /// Defines the property. + /// + public static readonly StyledProperty IsTabStopProperty = + KeyboardNavigation.IsTabStopProperty.AddOwner(); + /// /// Defines the event. /// @@ -99,6 +105,12 @@ namespace Avalonia.Input "KeyUp", RoutingStrategies.Tunnel | RoutingStrategies.Bubble); + /// + /// Defines the property. + /// + public static readonly StyledProperty TabIndexProperty = + KeyboardNavigation.TabIndexProperty.AddOwner(); + /// /// Defines the event. /// @@ -426,6 +438,15 @@ namespace Avalonia.Input internal set { SetAndRaise(IsPointerOverProperty, ref _isPointerOver, value); } } + /// + /// Gets or sets a value that indicates whether the control is included in tab navigation. + /// + public bool IsTabStop + { + get => GetValue(IsTabStopProperty); + set => SetValue(IsTabStopProperty, value); + } + /// public bool IsEffectivelyEnabled { @@ -437,6 +458,16 @@ namespace Avalonia.Input } } + /// + /// Gets or sets a value that determines the order in which elements receive focus when the + /// user navigates through controls by pressing the Tab key. + /// + public int TabIndex + { + get => GetValue(TabIndexProperty); + set => SetValue(TabIndexProperty, value); + } + public List KeyBindings { get; } = new List(); /// diff --git a/src/Avalonia.Input/KeyboardNavigation.cs b/src/Avalonia.Input/KeyboardNavigation.cs index 6ef3c4fd60..ffccd8f7a0 100644 --- a/src/Avalonia.Input/KeyboardNavigation.cs +++ b/src/Avalonia.Input/KeyboardNavigation.cs @@ -5,6 +5,14 @@ namespace Avalonia.Input /// public static class KeyboardNavigation { + /// + /// Defines the TabIndex attached property. + /// + public static readonly AttachedProperty TabIndexProperty = + AvaloniaProperty.RegisterAttached( + "TabIndex", + typeof(KeyboardNavigation)); + /// /// Defines the TabNavigation attached property. /// @@ -42,6 +50,26 @@ namespace Avalonia.Input typeof(KeyboardNavigation), true); + /// + /// Gets the for an element. + /// + /// The container. + /// The for the container. + public static int GetTabIndex(IInputElement element) + { + return ((IAvaloniaObject)element).GetValue(TabIndexProperty); + } + + /// + /// Sets the for an element. + /// + /// The element. + /// The tab index. + public static void SetTabIndex(IInputElement element, int value) + { + ((IAvaloniaObject)element).SetValue(TabIndexProperty, value); + } + /// /// Gets the for a container. /// @@ -83,7 +111,7 @@ namespace Avalonia.Input } /// - /// Sets the for a container. + /// Sets the for an element. /// /// The container. /// Value indicating whether the container is a tab stop. @@ -93,7 +121,7 @@ namespace Avalonia.Input } /// - /// Gets the for a container. + /// Gets the for an element. /// /// The container. /// Whether the container is a tab stop. From 4684237331021c30a021255f92050f44d9881ea7 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 29 May 2021 22:00:58 +0200 Subject: [PATCH 002/127] Ported tab navigation code from WPF. --- src/Avalonia.Input/FocusManager.cs | 13 + src/Avalonia.Input/KeyboardNavigation.cs | 3 +- .../KeyboardNavigationHandler.cs | 20 +- .../Navigation/TabNavigation.cs | 709 +++++++++++++----- .../KeyboardNavigationTests_Tab.cs | 29 + 5 files changed, 589 insertions(+), 185 deletions(-) diff --git a/src/Avalonia.Input/FocusManager.cs b/src/Avalonia.Input/FocusManager.cs index 474b212a21..1432092ba1 100644 --- a/src/Avalonia.Input/FocusManager.cs +++ b/src/Avalonia.Input/FocusManager.cs @@ -92,6 +92,17 @@ namespace Avalonia.Input } } + public IInputElement? GetFocusedElement(IInputElement e) + { + if (e is IFocusScope scope) + { + _focusScopes.TryGetValue(scope, out var result); + return result; + } + + return null; + } + /// /// Sets the currently focused element in the specified scope. /// @@ -151,6 +162,8 @@ namespace Avalonia.Input Focus(e); } + public static bool GetIsFocusScope(IInputElement e) => e is IFocusScope; + /// /// Checks if the specified element can be focused. /// diff --git a/src/Avalonia.Input/KeyboardNavigation.cs b/src/Avalonia.Input/KeyboardNavigation.cs index ffccd8f7a0..a25aed6811 100644 --- a/src/Avalonia.Input/KeyboardNavigation.cs +++ b/src/Avalonia.Input/KeyboardNavigation.cs @@ -11,7 +11,8 @@ namespace Avalonia.Input public static readonly AttachedProperty TabIndexProperty = AvaloniaProperty.RegisterAttached( "TabIndex", - typeof(KeyboardNavigation)); + typeof(KeyboardNavigation), + int.MaxValue); /// /// Defines the TabNavigation attached property. diff --git a/src/Avalonia.Input/KeyboardNavigationHandler.cs b/src/Avalonia.Input/KeyboardNavigationHandler.cs index dbefe63789..53c9b008ff 100644 --- a/src/Avalonia.Input/KeyboardNavigationHandler.cs +++ b/src/Avalonia.Input/KeyboardNavigationHandler.cs @@ -64,7 +64,13 @@ namespace Avalonia.Input } else if (direction == NavigationDirection.Next || direction == NavigationDirection.Previous) { - return TabNavigation.GetNextInTabOrder((IInputElement)customHandler, direction, true); + var e = (IInputElement)customHandler; + return direction switch + { + NavigationDirection.Next => TabNavigation.GetNextTab(e, false), + NavigationDirection.Previous => TabNavigation.GetPrevTab(e, null, false), + _ => throw new NotSupportedException(), + }; } else { @@ -73,14 +79,12 @@ namespace Avalonia.Input } } - if (direction == NavigationDirection.Next || direction == NavigationDirection.Previous) + return direction switch { - return TabNavigation.GetNextInTabOrder(element, direction); - } - else - { - throw new NotSupportedException(); - } + NavigationDirection.Next => TabNavigation.GetNextTab(element, false), + NavigationDirection.Previous => TabNavigation.GetPrevTab(element, null, false), + _ => throw new NotSupportedException(), + }; } /// diff --git a/src/Avalonia.Input/Navigation/TabNavigation.cs b/src/Avalonia.Input/Navigation/TabNavigation.cs index 6f6d68940b..d1862c2fa5 100644 --- a/src/Avalonia.Input/Navigation/TabNavigation.cs +++ b/src/Avalonia.Input/Navigation/TabNavigation.cs @@ -10,277 +10,634 @@ namespace Avalonia.Input.Navigation /// internal static class TabNavigation { - /// - /// Gets the next control in the specified tab direction. - /// - /// The element. - /// The tab direction. Must be Next or Previous. - /// - /// If true will not descend into to find next control. - /// - /// - /// The next element in the specified direction, or null if - /// was the last in the requested direction. - /// - public static IInputElement? GetNextInTabOrder( - IInputElement element, - NavigationDirection direction, - bool outsideElement = false) + public static IInputElement? GetNextTab(IInputElement e, bool goDownOnly) { - element = element ?? throw new ArgumentNullException(nameof(element)); - - if (direction != NavigationDirection.Next && direction != NavigationDirection.Previous) - { - throw new ArgumentException("Invalid direction: must be Next or Previous."); - } + return GetNextTab(e, GetGroupParent(e), goDownOnly); + } - var container = element.GetVisualParent(); + public static IInputElement? GetNextTab(IInputElement? e, IInputElement container, bool goDownOnly) + { + var tabbingType = GetKeyNavigationMode(container); - if (container != null) + if (e == null) { - var mode = KeyboardNavigation.GetTabNavigation((InputElement)container); + if (IsTabStop(container)) + return container; - switch (mode) + // Using ActiveElement if set + var activeElement = GetActiveElement(container); + if (activeElement != null) + return GetNextTab(null, activeElement, true); + } + else + { + if (tabbingType == KeyboardNavigationMode.Once || tabbingType == KeyboardNavigationMode.None) { - case KeyboardNavigationMode.Continue: - return GetNextInContainer(element, container, direction, outsideElement) ?? - GetFirstInNextContainer(element, element, direction); - case KeyboardNavigationMode.Cycle: - return GetNextInContainer(element, container, direction, outsideElement) ?? - GetFocusableDescendant(container, direction); - case KeyboardNavigationMode.Contained: - return GetNextInContainer(element, container, direction, outsideElement); - default: - return GetFirstInNextContainer(element, container, direction); + if (container != e) + { + if (goDownOnly) + return null; + var parentContainer = GetGroupParent(container); + return GetNextTab(container, parentContainer, goDownOnly); + } } } - else + + // All groups + IInputElement? loopStartElement = null; + var nextTabElement = e; + var currentTabbingType = tabbingType; + + // Search down inside the container + while ((nextTabElement = GetNextTabInGroup(nextTabElement, container, currentTabbingType)) != null) { - return GetFocusableDescendants(element, direction).FirstOrDefault(); + // Avoid the endless loop here for Cycle groups + if (loopStartElement == nextTabElement) + break; + if (loopStartElement == null) + loopStartElement = nextTabElement; + + var firstTabElementInside = GetNextTab(null, nextTabElement, true); + if (firstTabElementInside != null) + return firstTabElementInside; + + // If we want to continue searching inside the Once groups, we should change the navigation mode + if (currentTabbingType == KeyboardNavigationMode.Once) + currentTabbingType = KeyboardNavigationMode.Contained; } - } - /// - /// Gets the first or last focusable descendant of the specified element. - /// - /// The element. - /// The direction to search. - /// The element or null if not found.## - private static IInputElement GetFocusableDescendant(IInputElement container, NavigationDirection direction) - { - return direction == NavigationDirection.Next ? - GetFocusableDescendants(container, direction).FirstOrDefault() : - GetFocusableDescendants(container, direction).LastOrDefault(); + // If there is no next element in the group (nextTabElement == null) + + // Search up in the tree if allowed + // consider: Use original tabbingType instead of currentTabbingType + if (!goDownOnly && currentTabbingType != KeyboardNavigationMode.Contained && GetParent(container) != null) + { + return GetNextTab(container, GetGroupParent(container), false); + } + + return null; } - /// - /// Gets the focusable descendants of the specified element. - /// - /// The element. - /// The tab direction. Must be Next or Previous. - /// The element's focusable descendants. - private static IEnumerable GetFocusableDescendants(IInputElement element, - NavigationDirection direction) + public static IInputElement? GetPrevTab(IInputElement? e, IInputElement? container, bool goDownOnly) { - var mode = KeyboardNavigation.GetTabNavigation((InputElement)element); + if (e is null && container is null) + throw new InvalidOperationException("Either 'e' or 'container' must be non-null."); - if (mode == KeyboardNavigationMode.None) - { - yield break; - } + if (container is null) + container = GetGroupParent(e!); - var children = element.GetVisualChildren().OfType(); + KeyboardNavigationMode tabbingType = GetKeyNavigationMode(container); - if (mode == KeyboardNavigationMode.Once) + if (e == null) { - var active = KeyboardNavigation.GetTabOnceActiveElement((InputElement)element); - - if (active != null) + // Using ActiveElement if set + var activeElement = GetActiveElement(container); + if (activeElement != null) + return GetPrevTab(null, activeElement, true); + else { - yield return active; - yield break; + // If we Shift+Tab on a container with KeyboardNavigationMode=Once, and ActiveElement is null + // then we want to go to the first item (not last) within the container + if (tabbingType == KeyboardNavigationMode.Once) + { + var firstTabElement = GetNextTabInGroup(null, container, tabbingType); + if (firstTabElement == null) + { + if (IsTabStop(container)) + return container; + if (goDownOnly) + return null; + + return GetPrevTab(container, null, false); + } + else + { + return GetPrevTab(null, firstTabElement, true); + } + } } - else + } + else + { + if (tabbingType == KeyboardNavigationMode.Once || tabbingType == KeyboardNavigationMode.None) { - children = children.Take(1); + if (goDownOnly || container == e) + return null; + + // FocusedElement should not be e otherwise we will delegate focus to the same element + if (IsTabStop(container)) + return container; + + return GetPrevTab(container, null, false); } } - foreach (var child in children) + // All groups (except Once) - continue + IInputElement? loopStartElement = null; + IInputElement? nextTabElement = e; + + // Look for element with the same TabIndex before the current element + while ((nextTabElement = GetPrevTabInGroup(nextTabElement, container, tabbingType)) != null) { - var customNext = GetCustomNext(child, direction); + // At this point nextTabElement is TabStop or TabGroup + // In case it is a TabStop only return the element + if (IsTabStop(nextTabElement) && !IsGroup(nextTabElement)) + return nextTabElement; + + // Avoid the endless loop here + if (loopStartElement == nextTabElement) + break; + if (loopStartElement == null) + loopStartElement = nextTabElement; + + // At this point nextTabElement is TabGroup + var lastTabElementInside = GetPrevTab(null, nextTabElement, true); + if (lastTabElementInside != null) + return lastTabElementInside; + } - if (customNext.handled) - { - yield return customNext.next!; - } - else + if (tabbingType == KeyboardNavigationMode.Contained) + return null; + + if (e != container && IsTabStop(container)) + return container; + + // If end of the subtree is reached or there no other elements above + if (!goDownOnly && GetParent(container) != null) + { + return GetPrevTab(container, null, false); + } + + return null; + } + + private static IInputElement? FocusedElement(IInputElement e) + { + var iie = e; + // Focus delegation is enabled only if keyboard focus is outside the container + if (iie != null && !iie.IsKeyboardFocusWithin) + { + var focusedElement = (FocusManager.Instance as FocusManager)?.GetFocusedElement(e); + if (focusedElement != null) { - if (child.CanFocus() && KeyboardNavigation.GetIsTabStop((InputElement)child)) + if (!IsFocusScope(e)) { - yield return child; + // Verify if focusedElement is a visual descendant of e + if (focusedElement is IVisual visualFocusedElement && + visualFocusedElement != e && + e.IsVisualAncestorOf(visualFocusedElement)) + { + return focusedElement; + } } + } + } - if (child.CanFocusDescendants()) + return null; + } + + private static IInputElement? GetFirstChild(IInputElement e) + { + // If the element has a FocusedElement it should be its first child + if (FocusedElement(e) is IInputElement focusedElement) + return focusedElement; + + // Return the first visible element. + var uiElement = e as InputElement; + + if (uiElement is null || uiElement.IsVisible) + { + if (e is IVisual elementAsVisual) + { + var children = elementAsVisual.VisualChildren; + var count = children.Count; + + for (int i = 0; i < count; i++) { - foreach (var descendant in GetFocusableDescendants(child, direction)) + if (children[i] is InputElement ie) { - if (KeyboardNavigation.GetIsTabStop((InputElement)descendant)) + if (ie.IsVisible) + return ie; + else { - yield return descendant; + var firstChild = GetFirstChild(ie); + if (firstChild != null) + return firstChild; } } } } } + + return null; } - /// - /// Gets the next item that should be focused in the specified container. - /// - /// The starting element/ - /// The container. - /// The direction. - /// - /// If true will not descend into to find next control. - /// - /// The next element, or null if the element is the last. - private static IInputElement? GetNextInContainer( - IInputElement element, - IInputElement container, - NavigationDirection direction, - bool outsideElement) + private static IInputElement? GetLastChild(IInputElement e) { - IInputElement? e = element; - - if (direction == NavigationDirection.Next && !outsideElement) - { - var descendant = GetFocusableDescendants(element, direction).FirstOrDefault(); + // If the element has a FocusedElement it should be its last child + if (FocusedElement(e) is IInputElement focusedElement) + return focusedElement; - if (descendant != null) - { - return descendant; - } - } + // Return the last visible element. + var uiElement = e as InputElement; - if (container != null) + if (uiElement == null || uiElement.IsVisible) { - var navigable = container as INavigableContainer; + var elementAsVisual = e as IVisual; - // TODO: Do a spatial search here if the container doesn't implement - // INavigableContainer. - if (navigable != null) + if (elementAsVisual != null) { - while (e != null) - { - e = navigable.GetControl(direction, e, false); + var children = elementAsVisual.VisualChildren; + var count = children.Count; - if (e != null && - e.CanFocus() && - KeyboardNavigation.GetIsTabStop((InputElement)e)) + for (int i = count - 1; i >= 0; i--) + { + if (children[i] is InputElement ie) { - break; + if (ie.IsVisible) + return ie; + else + { + var lastChild = GetLastChild(ie); + if (lastChild != null) + return lastChild; + } } } } - else + } + + return null; + } + + private static IInputElement? GetFirstTabInGroup(IInputElement container) + { + IInputElement? firstTabElement = null; + int minIndexFirstTab = int.MinValue; + + var currElement = container; + while ((currElement = GetNextInTree(currElement, container)) != null) + { + if (IsTabStopOrGroup(currElement)) { - // TODO: Do a spatial search here if the container doesn't implement - // INavigableContainer. - e = null; + int currPriority = KeyboardNavigation.GetTabIndex(currElement); + + if (currPriority < minIndexFirstTab || firstTabElement == null) + { + minIndexFirstTab = currPriority; + firstTabElement = currElement; + } } + } + return firstTabElement; + } + + private static IInputElement? GetLastInTree(IInputElement container) + { + IInputElement? result; + IInputElement? c = container; + + do + { + result = c; + c = GetLastChild(c); + } while (c != null && !IsGroup(c)); + + if (c != null) + return c; + + return result; + } - if (e != null && direction == NavigationDirection.Previous) + private static IInputElement? GetLastTabInGroup(IInputElement container) + { + IInputElement? lastTabElement = null; + int maxIndexFirstTab = int.MaxValue; + var currElement = GetLastInTree(container); + while (currElement != null && currElement != container) + { + if (IsTabStopOrGroup(currElement)) { - var descendant = GetFocusableDescendants(e, direction).LastOrDefault(); + int currPriority = KeyboardNavigation.GetTabIndex(currElement); - if (descendant != null) + if (currPriority > maxIndexFirstTab || lastTabElement == null) { - return descendant; + maxIndexFirstTab = currPriority; + lastTabElement = currElement; } } + currElement = GetPreviousInTree(currElement, container); + } + return lastTabElement; + } + + private static IInputElement? GetNextInTree(IInputElement e, IInputElement container) + { + IInputElement? result = null; - return e; + if (e == container || !IsGroup(e)) + result = GetFirstChild(e); + + if (result != null || e == container) + return result; + + IInputElement? parent = e; + do + { + var sibling = GetNextSibling(parent); + if (sibling != null) + return sibling; + + parent = GetParent(parent); + } while (parent != null && parent != container); + + return null; + } + + private static IInputElement? GetNextSibling(IInputElement e) + { + if (GetParent(e) is IVisual parentAsVisual && e is IVisual elementAsVisual) + { + var children = parentAsVisual.VisualChildren; + var count = children.Count; + var i = 0; + + //go till itself + for (; i < count; i++) + { + var vchild = children[i]; + if (vchild == elementAsVisual) + break; + } + i++; + //search ahead + for (; i < count; i++) + { + var visual = children[i]; + if (visual is IInputElement ie) + return ie; + } } return null; } - /// - /// Gets the first item that should be focused in the next container. - /// - /// The element being navigated away from. - /// The container. - /// The direction of the search. - /// The first element, or null if there are no more elements. - private static IInputElement? GetFirstInNextContainer( - IInputElement element, - IInputElement container, - NavigationDirection direction) + private static IInputElement? GetNextTabInGroup(IInputElement? e, IInputElement container, KeyboardNavigationMode tabbingType) { - var parent = container.GetVisualParent(); - IInputElement? next = null; + // None groups: Tab navigation is not supported + if (tabbingType == KeyboardNavigationMode.None) + return null; + + // e == null or e == container -> return the first TabStopOrGroup + if (e == null || e == container) + { + return GetFirstTabInGroup(container); + } + + if (tabbingType == KeyboardNavigationMode.Once) + return null; + + var nextTabElement = GetNextTabWithSameIndex(e, container); + if (nextTabElement != null) + return nextTabElement; + + return GetNextTabWithNextIndex(e, container, tabbingType); + } - if (parent != null) + private static IInputElement? GetNextTabWithSameIndex(IInputElement e, IInputElement container) + { + var elementTabPriority = KeyboardNavigation.GetTabIndex(e); + var currElement = e; + while ((currElement = GetNextInTree(currElement, container)) != null) { - if (direction == NavigationDirection.Previous && - parent.CanFocus() && - KeyboardNavigation.GetIsTabStop((InputElement) parent)) + if (IsTabStopOrGroup(currElement) && KeyboardNavigation.GetTabIndex(currElement) == elementTabPriority) { - return parent; + return currElement; } + } - var allSiblings = parent.GetVisualChildren() - .OfType() - .Where(FocusExtensions.CanFocusDescendants); - var siblings = direction == NavigationDirection.Next ? - allSiblings.SkipWhile(x => x != container).Skip(1) : - allSiblings.TakeWhile(x => x != container).Reverse(); + return null; + } - foreach (var sibling in siblings) + private static IInputElement? GetNextTabWithNextIndex(IInputElement e, IInputElement container, KeyboardNavigationMode tabbingType) + { + // Find the next min index in the tree + // min (index>currentTabIndex) + IInputElement? nextTabElement = null; + IInputElement? firstTabElement = null; + int minIndexFirstTab = int.MinValue; + int minIndex = int.MinValue; + int elementTabPriority = KeyboardNavigation.GetTabIndex(e); + + IInputElement? currElement = container; + while ((currElement = GetNextInTree(currElement, container)) != null) + { + if (IsTabStopOrGroup(currElement)) { - var customNext = GetCustomNext(sibling, direction); - if (customNext.handled) + int currPriority = KeyboardNavigation.GetTabIndex(currElement); + if (currPriority > elementTabPriority) { - return customNext.next; + if (currPriority < minIndex || nextTabElement == null) + { + minIndex = currPriority; + nextTabElement = currElement; + } } - if (sibling.CanFocus() && KeyboardNavigation.GetIsTabStop((InputElement) sibling)) + if (currPriority < minIndexFirstTab || firstTabElement == null) { - return sibling; + minIndexFirstTab = currPriority; + firstTabElement = currElement; } + } + } + + // Cycle groups: if not found - return first element + if (tabbingType == KeyboardNavigationMode.Cycle && nextTabElement == null) + nextTabElement = firstTabElement; - next = direction == NavigationDirection.Next ? - GetFocusableDescendants(sibling, direction).FirstOrDefault() : - GetFocusableDescendants(sibling, direction).LastOrDefault(); + return nextTabElement; + } - if (next != null) + private static IInputElement? GetPrevTabInGroup(IInputElement? e, IInputElement container, KeyboardNavigationMode tabbingType) + { + // None groups: Tab navigation is not supported + if (tabbingType == KeyboardNavigationMode.None) + return null; + + // Search the last index inside the group + if (e == null) + { + return GetLastTabInGroup(container); + } + + if (tabbingType == KeyboardNavigationMode.Once) + return null; + + if (e == container) + return null; + + var nextTabElement = GetPrevTabWithSameIndex(e, container); + if (nextTabElement != null) + return nextTabElement; + + return GetPrevTabWithPrevIndex(e, container, tabbingType); + } + + private static IInputElement? GetPrevTabWithSameIndex(IInputElement e, IInputElement container) + { + int elementTabPriority = KeyboardNavigation.GetTabIndex(e); + var currElement = GetPreviousInTree(e, container); + while (currElement != null) + { + if (IsTabStopOrGroup(currElement) && KeyboardNavigation.GetTabIndex(currElement) == elementTabPriority && currElement != container) + { + return currElement; + } + currElement = GetPreviousInTree(currElement, container); + } + return null; + } + + private static IInputElement? GetPrevTabWithPrevIndex(IInputElement e, IInputElement container, KeyboardNavigationMode tabbingType) + { + // Find the next max index in the tree + // max (index maxIndex || nextTabElement == null) + { + maxIndex = currPriority; + nextTabElement = currElement; + } + } + + if (currPriority > maxIndexFirstTab || lastTabElement == null) + { + maxIndexFirstTab = currPriority; + lastTabElement = currElement; } } - next = GetFirstInNextContainer(element, parent, direction); + currElement = GetPreviousInTree(currElement, container); + } + + // Cycle groups: if not found - return first element + if (tabbingType == KeyboardNavigationMode.Cycle && nextTabElement == null) + nextTabElement = lastTabElement; + + return nextTabElement; + } + + private static IInputElement? GetPreviousInTree(IInputElement e, IInputElement container) + { + if (e == container) + return null; + + var result = GetPreviousSibling(e); + + if (result != null) + { + if (IsGroup(result)) + return result; + else + return GetLastInTree(result); } else + return GetParent(e); + } + + private static IInputElement? GetPreviousSibling(IInputElement e) + { + if (GetParent(e) is IVisual parentAsVisual && e is IVisual elementAsVisual) { - next = direction == NavigationDirection.Next ? - GetFocusableDescendants(container, direction).FirstOrDefault() : - GetFocusableDescendants(container, direction).LastOrDefault(); + var children = parentAsVisual.VisualChildren; + var count = children.Count; + IInputElement? prev = null; + + for (int i = 0; i < count; i++) + { + var vchild = children[i]; + if (vchild == elementAsVisual) + break; + if (vchild.IsVisible == true && vchild is IInputElement ie) + prev = ie; + } + return prev; } + return null; + } - return next; + private static IInputElement? GetActiveElement(IInputElement e) + { + return ((IAvaloniaObject)e).GetValue(KeyboardNavigation.TabOnceActiveElementProperty); } - private static (bool handled, IInputElement? next) GetCustomNext(IInputElement element, - NavigationDirection direction) + private static IInputElement GetGroupParent(IInputElement e) => GetGroupParent(e, false); + + private static IInputElement GetGroupParent(IInputElement element, bool includeCurrent) { - if (element is ICustomKeyboardNavigation custom) + var result = element; // Keep the last non null element + var e = element; + + // If we don't want to include the current element, + // start at the parent of the element. If the element + // is the root, then just return it as the group parent. + if (!includeCurrent) + { + result = e; + e = GetParent(e); + if (e == null) + return result; + } + + while (e != null) { - return custom.GetNext(element, direction); + if (IsGroup(e)) + return e; + + result = e; + e = GetParent(e); } - return (false, null); + return result; + } + + private static IInputElement? GetParent(IInputElement e) + { + // For Visual - go up the visual parent chain until we find Visual. + if (e is IVisual v) + return v.FindAncestorOfType(); + + // This will need to be implemented when we have non-visual input elements. + throw new NotSupportedException(); + } + + private static KeyboardNavigationMode GetKeyNavigationMode(IInputElement e) + { + return ((IAvaloniaObject)e).GetValue(KeyboardNavigation.TabNavigationProperty); + } + private static bool IsFocusScope(IInputElement e) => FocusManager.GetIsFocusScope(e) || GetParent(e) == null; + private static bool IsGroup(IInputElement e) => GetKeyNavigationMode(e) != KeyboardNavigationMode.Continue; + + private static bool IsTabStop(IInputElement e) + { + if (e is InputElement ie) + return ie.Focusable && KeyboardNavigation.GetIsTabStop(ie) && ie.IsVisible && ie.IsEnabled; + return false; } + + private static bool IsTabStopOrGroup(IInputElement e) => IsTabStop(e) || IsGroup(e); } } diff --git a/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Tab.cs b/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Tab.cs index 1efbbed2e8..f471ae4029 100644 --- a/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Tab.cs +++ b/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Tab.cs @@ -13,6 +13,7 @@ namespace Avalonia.Input.UnitTests var top = new StackPanel { + [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle, Children = { new StackPanel @@ -49,6 +50,7 @@ namespace Avalonia.Input.UnitTests var top = new StackPanel { + [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle, Children = { new StackPanel @@ -85,6 +87,7 @@ namespace Avalonia.Input.UnitTests var top = new StackPanel { + [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle, Children = { new StackPanel @@ -122,6 +125,7 @@ namespace Avalonia.Input.UnitTests var top = new StackPanel { + [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle, Children = { new StackPanel @@ -165,6 +169,7 @@ namespace Avalonia.Input.UnitTests var top = new StackPanel { + [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle, Children = { new StackPanel @@ -193,6 +198,7 @@ namespace Avalonia.Input.UnitTests var top = new StackPanel { + [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle, Children = { new StackPanel @@ -222,6 +228,7 @@ namespace Avalonia.Input.UnitTests var top = new StackPanel { + [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle, Children = { new StackPanel @@ -263,6 +270,7 @@ namespace Avalonia.Input.UnitTests var top = new StackPanel { + [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle, Children = { (next = new Button { Name = "Button1" }), @@ -282,6 +290,7 @@ namespace Avalonia.Input.UnitTests var top = new StackPanel { + [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle, Children = { new StackPanel @@ -324,6 +333,7 @@ namespace Avalonia.Input.UnitTests var top = new StackPanel { + [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle, Children = { new StackPanel @@ -361,6 +371,7 @@ namespace Avalonia.Input.UnitTests var top = new StackPanel { + [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle, Children = { new StackPanel @@ -398,6 +409,7 @@ namespace Avalonia.Input.UnitTests var top = new StackPanel { + [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle, Children = { new StackPanel @@ -434,6 +446,7 @@ namespace Avalonia.Input.UnitTests var top = new StackPanel { + [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle, Children = { new StackPanel @@ -471,6 +484,7 @@ namespace Avalonia.Input.UnitTests var top = new StackPanel { + [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle, Children = { new StackPanel @@ -509,6 +523,7 @@ namespace Avalonia.Input.UnitTests var top = new StackPanel { + [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle, Children = { (container = new StackPanel @@ -548,6 +563,7 @@ namespace Avalonia.Input.UnitTests var top = new StackPanel { + [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle, Children = { new StackPanel @@ -586,6 +602,7 @@ namespace Avalonia.Input.UnitTests var top = new StackPanel { + [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle, Children = { (container = new StackPanel @@ -625,6 +642,7 @@ namespace Avalonia.Input.UnitTests var top = new StackPanel { + [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle, Children = { new StackPanel @@ -661,6 +679,7 @@ namespace Avalonia.Input.UnitTests var top = new StackPanel { + [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle, Children = { new StackPanel @@ -697,6 +716,7 @@ namespace Avalonia.Input.UnitTests var top = new StackPanel { + [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle, Children = { new StackPanel @@ -725,6 +745,7 @@ namespace Avalonia.Input.UnitTests var top = new StackPanel { + [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle, Children = { new StackPanel @@ -767,6 +788,7 @@ namespace Avalonia.Input.UnitTests var top = new StackPanel { + [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle, Children = { new StackPanel @@ -828,6 +850,7 @@ namespace Avalonia.Input.UnitTests var top = new StackPanel { + [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle, Children = { new StackPanel @@ -865,6 +888,7 @@ namespace Avalonia.Input.UnitTests var top = new StackPanel { + [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle, Children = { new StackPanel @@ -902,6 +926,7 @@ namespace Avalonia.Input.UnitTests var top = new StackPanel { + [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle, Children = { new StackPanel @@ -938,6 +963,7 @@ namespace Avalonia.Input.UnitTests var top = new StackPanel { + [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle, Children = { new StackPanel @@ -975,6 +1001,7 @@ namespace Avalonia.Input.UnitTests var top = new StackPanel { + [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle, Children = { new StackPanel @@ -1013,6 +1040,7 @@ namespace Avalonia.Input.UnitTests var top = new StackPanel { + [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle, Children = { (container = new StackPanel @@ -1052,6 +1080,7 @@ namespace Avalonia.Input.UnitTests var top = new StackPanel { + [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle, Children = { new StackPanel From ec5131831507b7d8f8b9cd55a218d647caea6378 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 29 May 2021 23:53:09 +0200 Subject: [PATCH 003/127] Make custom keyboard navigation work again. --- src/Avalonia.Input/Avalonia.Input.csproj | 3 + .../ICustomKeyboardNavigation.cs | 17 ++- .../KeyboardNavigationHandler.cs | 111 +++++++++++++----- .../Navigation/TabNavigation.cs | 26 ++++ .../KeyboardNavigationTests_Custom.cs | 33 ++++++ tests/Avalonia.UnitTests/TestRoot.cs | 1 + 6 files changed, 157 insertions(+), 34 deletions(-) diff --git a/src/Avalonia.Input/Avalonia.Input.csproj b/src/Avalonia.Input/Avalonia.Input.csproj index c39c81a965..69a80290d1 100644 --- a/src/Avalonia.Input/Avalonia.Input.csproj +++ b/src/Avalonia.Input/Avalonia.Input.csproj @@ -4,6 +4,9 @@ Enable CS8600;CS8602;CS8603 + + + diff --git a/src/Avalonia.Input/ICustomKeyboardNavigation.cs b/src/Avalonia.Input/ICustomKeyboardNavigation.cs index 3d2927c632..357395c42f 100644 --- a/src/Avalonia.Input/ICustomKeyboardNavigation.cs +++ b/src/Avalonia.Input/ICustomKeyboardNavigation.cs @@ -1,4 +1,5 @@ - +#nullable enable + namespace Avalonia.Input { /// @@ -6,6 +7,18 @@ namespace Avalonia.Input /// public interface ICustomKeyboardNavigation { - (bool handled, IInputElement next) GetNext(IInputElement element, NavigationDirection direction); + /// + /// Gets the next element in the specified navigation direction. + /// + /// The element being navigated from. + /// The navigation direction. + /// + /// A tuple consisting of: + /// - A boolean indicating whether the request was handled. If false is returned then + /// custom navigation will be ignored and default navigation will take place. + /// - If handled is true: the next element in the navigation direction, or null if default + /// navigation should continue outside the element. + /// + (bool handled, IInputElement? next) GetNext(IInputElement element, NavigationDirection direction); } } diff --git a/src/Avalonia.Input/KeyboardNavigationHandler.cs b/src/Avalonia.Input/KeyboardNavigationHandler.cs index 53c9b008ff..6493777105 100644 --- a/src/Avalonia.Input/KeyboardNavigationHandler.cs +++ b/src/Avalonia.Input/KeyboardNavigationHandler.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Linq; using Avalonia.Input.Navigation; using Avalonia.VisualTree; @@ -48,43 +49,24 @@ namespace Avalonia.Input { element = element ?? throw new ArgumentNullException(nameof(element)); - var customHandler = element.GetSelfAndVisualAncestors() - .OfType() - .FirstOrDefault(); + // If there's a custom keyboard navigation handler as an ancestor, use that. + var custom = element.FindAncestorOfType(true); + if (custom is object && HandlePreCustomNavigation(custom, element, direction, out var ce)) + return ce; - if (customHandler != null) - { - var (handled, next) = customHandler.GetNext(element, direction); - - if (handled) - { - if (next != null) - { - return next; - } - else if (direction == NavigationDirection.Next || direction == NavigationDirection.Previous) - { - var e = (IInputElement)customHandler; - return direction switch - { - NavigationDirection.Next => TabNavigation.GetNextTab(e, false), - NavigationDirection.Previous => TabNavigation.GetPrevTab(e, null, false), - _ => throw new NotSupportedException(), - }; - } - else - { - return null; - } - } - } - - return direction switch + var result = direction switch { NavigationDirection.Next => TabNavigation.GetNextTab(element, false), NavigationDirection.Previous => TabNavigation.GetPrevTab(element, null, false), _ => throw new NotSupportedException(), }; + + // If there wasn't a custom navigation handler as an ancestor of the current element, + // but there is one as an ancestor of the new element, use that. + if (custom is null && HandlePostCustomNavigation(element, result, direction, out ce)) + return ce; + + return result; } /// @@ -94,7 +76,7 @@ namespace Avalonia.Input /// The direction to move. /// Any key modifiers active at the time of focus. public void Move( - IInputElement element, + IInputElement element, NavigationDirection direction, KeyModifiers keyModifiers = KeyModifiers.None) { @@ -128,5 +110,70 @@ namespace Avalonia.Input e.Handled = true; } } + + private static bool HandlePreCustomNavigation( + ICustomKeyboardNavigation customHandler, + IInputElement element, + NavigationDirection direction, + [NotNullWhen(true)] out IInputElement? result) + { + if (customHandler != null) + { + var (handled, next) = customHandler.GetNext(element, direction); + + if (handled) + { + if (next != null) + { + result = next; + return true; + } + else if (direction == NavigationDirection.Next || direction == NavigationDirection.Previous) + { + var r = direction switch + { + NavigationDirection.Next => TabNavigation.GetNextTabOutside(customHandler), + NavigationDirection.Previous => TabNavigation.GetPrevTabOutside(customHandler), + _ => throw new NotSupportedException(), + }; + + if (r is object) + { + result = r; + return true; + } + } + } + } + + result = null; + return false; + } + + private static bool HandlePostCustomNavigation( + IInputElement element, + IInputElement? newElement, + NavigationDirection direction, + [NotNullWhen(true)] out IInputElement? result) + { + if (newElement is object) + { + var customHandler = newElement.FindAncestorOfType(true); + + if (customHandler is object) + { + var (handled, next) = customHandler.GetNext(element, direction); + + if (handled && next is object) + { + result = next; + return true; + } + } + } + + result = null; + return false; + } } } diff --git a/src/Avalonia.Input/Navigation/TabNavigation.cs b/src/Avalonia.Input/Navigation/TabNavigation.cs index d1862c2fa5..12842e4f40 100644 --- a/src/Avalonia.Input/Navigation/TabNavigation.cs +++ b/src/Avalonia.Input/Navigation/TabNavigation.cs @@ -78,6 +78,19 @@ namespace Avalonia.Input.Navigation return null; } + public static IInputElement? GetNextTabOutside(ICustomKeyboardNavigation e) + { + if (e is IInputElement container) + { + var last = GetLastInTree(container); + + if (last is object) + return GetNextTab(last, false); + } + + return null; + } + public static IInputElement? GetPrevTab(IInputElement? e, IInputElement? container, bool goDownOnly) { if (e is null && container is null) @@ -171,6 +184,19 @@ namespace Avalonia.Input.Navigation return null; } + public static IInputElement? GetPrevTabOutside(ICustomKeyboardNavigation e) + { + if (e is IInputElement container) + { + var first = GetFirstChild(container); + + if (first is object) + return GetPrevTab(first, null, false); + } + + return null; + } + private static IInputElement? FocusedElement(IInputElement e) { var iie = e; diff --git a/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Custom.cs b/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Custom.cs index f72d6ba9c9..f9c85ee4ca 100644 --- a/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Custom.cs +++ b/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Custom.cs @@ -95,6 +95,7 @@ namespace Avalonia.Input.UnitTests var root = new StackPanel { + [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle, Children = { target, @@ -125,6 +126,7 @@ namespace Avalonia.Input.UnitTests var root = new StackPanel { + [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle, Children = { (current = new Button { Content = "Outside" }), @@ -137,6 +139,36 @@ namespace Avalonia.Input.UnitTests Assert.Same(next, result); } + [Fact] + public void ShiftTab_Should_Navigate_Outside_When_Null_Returned_As_Next() + { + Button current; + Button next; + var target = new CustomNavigatingStackPanel + { + Children = + { + new Button { Content = "Button 1" }, + (current = new Button { Content = "Button 2" }), + new Button { Content = "Button 3" }, + }, + }; + + var root = new StackPanel + { + [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle, + Children = + { + target, + (next = new Button { Content = "Outside" }), + } + }; + + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Previous); + + Assert.Same(next, result); + } + [Fact] public void Tab_Should_Navigate_Outside_When_Null_Returned_As_Next() { @@ -154,6 +186,7 @@ namespace Avalonia.Input.UnitTests var root = new StackPanel { + [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle, Children = { target, diff --git a/tests/Avalonia.UnitTests/TestRoot.cs b/tests/Avalonia.UnitTests/TestRoot.cs index b69bf990d9..4601dd7e5b 100644 --- a/tests/Avalonia.UnitTests/TestRoot.cs +++ b/tests/Avalonia.UnitTests/TestRoot.cs @@ -21,6 +21,7 @@ namespace Avalonia.UnitTests Renderer = Mock.Of(); LayoutManager = new LayoutManager(this); IsVisible = true; + KeyboardNavigation.SetTabNavigation(this, KeyboardNavigationMode.Cycle); } public TestRoot(IControl child) From 90ddccc54e3d9e3ad0b720c225d42ca6a967d2ad Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 29 May 2021 23:58:01 +0200 Subject: [PATCH 004/127] Toplevels now need to explicitly specify tab cycle mode. --- src/Avalonia.Controls/TopLevel.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 7a92836ddf..90d6fb1f63 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -83,6 +83,7 @@ namespace Avalonia.Controls /// static TopLevel() { + KeyboardNavigation.TabNavigationProperty.OverrideDefaultValue(KeyboardNavigationMode.Cycle); AffectsMeasure(ClientSizeProperty); TransparencyLevelHintProperty.Changed.AddClassHandler( From 91ef2124349047fa3e150a73071400fe3024a1c7 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 30 May 2021 14:19:36 +0200 Subject: [PATCH 005/127] Added a couple of TabIndex tests. --- .../KeyboardNavigationTests_Tab.cs | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Tab.cs b/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Tab.cs index f471ae4029..edcbf75a1d 100644 --- a/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Tab.cs +++ b/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Tab.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using Avalonia.Controls; using Xunit; @@ -1132,5 +1133,97 @@ namespace Avalonia.Input.UnitTests Assert.Null(result); } + + [Fact] + public void Respects_TabIndex_Moving_Forwards() + { + Button start; + + var top = new StackPanel + { + [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle, + Children = + { + new StackPanel + { + Children = + { + new Button { Name = "Button1", TabIndex = 5 }, + (start = new Button { Name = "Button2", TabIndex = 2 }), + new Button { Name = "Button3", TabIndex = 1 }, + } + }, + new StackPanel + { + Children = + { + new Button { Name = "Button4", TabIndex = 3 }, + new Button { Name = "Button5", TabIndex = 6 }, + new Button { Name = "Button6", TabIndex = 4 }, + } + }, + } + }; + + var result = new List(); + var current = (IInputElement)start; + + do + { + result.Add(((IControl)current).Name); + current = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Next); + } while (current is object && current != start); + + Assert.Equal(new[] + { + "Button2", "Button4", "Button6", "Button1", "Button5", "Button3" + }, result); + } + + [Fact] + public void Respects_TabIndex_Moving_Backwards() + { + Button start; + + var top = new StackPanel + { + [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle, + Children = + { + new StackPanel + { + Children = + { + new Button { Name = "Button1", TabIndex = 5 }, + (start = new Button { Name = "Button2", TabIndex = 2 }), + new Button { Name = "Button3", TabIndex = 1 }, + } + }, + new StackPanel + { + Children = + { + new Button { Name = "Button4", TabIndex = 3 }, + new Button { Name = "Button5", TabIndex = 6 }, + new Button { Name = "Button6", TabIndex = 4 }, + } + }, + } + }; + + var result = new List(); + var current = (IInputElement)start; + + do + { + result.Add(((IControl)current).Name); + current = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Previous); + } while (current is object && current != start); + + Assert.Equal(new[] + { + "Button2", "Button3", "Button5", "Button1", "Button6", "Button4" + }, result); + } } } From ca4dee94fd2ab4884031072da4a92c9f00a5fcd2 Mon Sep 17 00:00:00 2001 From: Deadpikle Date: Tue, 8 Jun 2021 18:00:04 -0400 Subject: [PATCH 006/127] Remove timer from undo helper, improve u/r for textbox --- src/Avalonia.Controls/TextBox.cs | 57 ++++++++++++------- src/Avalonia.Controls/Utils/UndoRedoHelper.cs | 11 +--- 2 files changed, 40 insertions(+), 28 deletions(-) diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 1bee15bccd..31ec12c22f 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -31,7 +31,7 @@ namespace Avalonia.Controls public static KeyGesture PasteGesture { get; } = AvaloniaLocator.Current .GetService()?.Paste.FirstOrDefault(); - + public static readonly StyledProperty AcceptsReturnProperty = AvaloniaProperty.Register(nameof(AcceptsReturn)); @@ -117,7 +117,7 @@ namespace Avalonia.Controls public static readonly StyledProperty RevealPasswordProperty = AvaloniaProperty.Register(nameof(RevealPassword)); - + public static readonly DirectProperty CanCutProperty = AvaloniaProperty.RegisterDirect( nameof(CanCut), @@ -135,7 +135,7 @@ namespace Avalonia.Controls public static readonly StyledProperty IsUndoEnabledProperty = AvaloniaProperty.Register( - nameof(IsUndoEnabled), + nameof(IsUndoEnabled), defaultValue: true); public static readonly DirectProperty UndoLimitProperty = @@ -174,6 +174,10 @@ namespace Avalonia.Controls private string _newLine = Environment.NewLine; private static readonly string[] invalidCharacters = new String[1] { "\u007f" }; + private int _selectedTextChangesMadeSinceLastUndoSnapshot; + private bool _hasDoneSnapshotOnce; + private const int _maxCharsBeforeUndoSnapshot = 7; + static TextBox() { FocusableProperty.OverrideDefaultValue(typeof(TextBox), true); @@ -202,7 +206,8 @@ namespace Avalonia.Controls horizontalScrollBarVisibility, BindingPriority.Style); _undoRedoHelper = new UndoRedoHelper(this); - + _selectedTextChangesMadeSinceLastUndoSnapshot = 0; + _hasDoneSnapshotOnce = false; UpdatePseudoclasses(); } @@ -331,6 +336,7 @@ namespace Avalonia.Controls if (SetAndRaise(TextProperty, ref _text, value) && IsUndoEnabled && !_isUndoingRedoing) { _undoRedoHelper.Clear(); + SnapshotUndoRedo(); // so we always have an initial state } } } @@ -341,7 +347,6 @@ namespace Avalonia.Controls get { return GetSelection(); } set { - SnapshotUndoRedo(); if (string.IsNullOrEmpty(value)) { DeleteSelection(); @@ -350,7 +355,6 @@ namespace Avalonia.Controls { HandleTextInput(value); } - SnapshotUndoRedo(); } } @@ -422,7 +426,7 @@ namespace Avalonia.Controls get { return _newLine; } set { SetAndRaise(NewLineProperty, ref _newLine, value); } } - + /// /// Clears the current selection, maintaining the /// @@ -480,11 +484,13 @@ namespace Avalonia.Controls var oldValue = _undoRedoHelper.Limit; _undoRedoHelper.Limit = value; RaisePropertyChanged(UndoLimitProperty, oldValue, value); - } + } // from docs at // https://docs.microsoft.com/en-us/dotnet/api/system.windows.controls.primitives.textboxbase.isundoenabled: // "Setting UndoLimit clears the undo queue." _undoRedoHelper.Clear(); + _selectedTextChangesMadeSinceLastUndoSnapshot = 0; + _hasDoneSnapshotOnce = false; } } @@ -515,6 +521,8 @@ namespace Avalonia.Controls // Therefore, if you disable undo and then re-enable it, undo commands still do not work // because the undo stack was emptied when you disabled undo." _undoRedoHelper.Clear(); + _selectedTextChangesMadeSinceLastUndoSnapshot = 0; + _hasDoneSnapshotOnce = false; } } @@ -577,23 +585,25 @@ namespace Avalonia.Controls { return; } - + input = RemoveInvalidCharacters(input); - + if (string.IsNullOrEmpty(input)) { return; } - + _selectedTextChangesMadeSinceLastUndoSnapshot++; + SnapshotUndoRedo(ignoreChangeCount: false); + string text = Text ?? string.Empty; int caretIndex = CaretIndex; int newLength = input.Length + text.Length - Math.Abs(SelectionStart - SelectionEnd); - + if (MaxLength > 0 && newLength > MaxLength) { input = input.Remove(Math.Max(0, input.Length - (newLength - MaxLength))); } - + if (!string.IsNullOrEmpty(input)) { DeleteSelection(); @@ -696,6 +706,7 @@ namespace Avalonia.Controls { try { + SnapshotUndoRedo(); _isUndoingRedoing = true; _undoRedoHelper.Undo(); } @@ -830,7 +841,6 @@ namespace Avalonia.Controls CaretIndex -= removedCharacters; ClearSelection(); } - SnapshotUndoRedo(); handled = true; break; @@ -858,7 +868,6 @@ namespace Avalonia.Controls SetTextInternal(text.Substring(0, caretIndex) + text.Substring(caretIndex + removedCharacters)); } - SnapshotUndoRedo(); handled = true; break; @@ -868,7 +877,6 @@ namespace Avalonia.Controls { SnapshotUndoRedo(); HandleTextInput(NewLine); - SnapshotUndoRedo(); handled = true; } @@ -879,7 +887,6 @@ namespace Avalonia.Controls { SnapshotUndoRedo(); HandleTextInput("\t"); - SnapshotUndoRedo(); handled = true; } else @@ -889,6 +896,10 @@ namespace Avalonia.Controls break; + case Key.Space: + SnapshotUndoRedo(); // always snapshot in between words + break; + default: handled = false; break; @@ -1306,11 +1317,19 @@ namespace Avalonia.Controls } } - private void SnapshotUndoRedo() + private void SnapshotUndoRedo(bool ignoreChangeCount = true) { if (IsUndoEnabled) { - _undoRedoHelper.Snapshot(); + if (ignoreChangeCount || + !_hasDoneSnapshotOnce || + (!ignoreChangeCount && + _selectedTextChangesMadeSinceLastUndoSnapshot >= _maxCharsBeforeUndoSnapshot)) + { + _undoRedoHelper.Snapshot(); + _selectedTextChangesMadeSinceLastUndoSnapshot = 0; + _hasDoneSnapshotOnce = true; + } } } } diff --git a/src/Avalonia.Controls/Utils/UndoRedoHelper.cs b/src/Avalonia.Controls/Utils/UndoRedoHelper.cs index 7374f20a0c..fd1ca54b57 100644 --- a/src/Avalonia.Controls/Utils/UndoRedoHelper.cs +++ b/src/Avalonia.Controls/Utils/UndoRedoHelper.cs @@ -7,7 +7,7 @@ using Avalonia.Utilities; namespace Avalonia.Controls.Utils { - class UndoRedoHelper : WeakTimer.IWeakTimerSubscriber where TState : struct, IEquatable + class UndoRedoHelper { private readonly IUndoRedoHost _host; @@ -31,7 +31,6 @@ namespace Avalonia.Controls.Utils public UndoRedoHelper(IUndoRedoHost host) { _host = host; - WeakTimer.StartWeakTimer(this, TimeSpan.FromSeconds(1)); } public void Undo() @@ -61,7 +60,7 @@ namespace Avalonia.Controls.Utils if (_states.Last != null) { _states.Last.Value = state; - } + } } public void UpdateLastState() @@ -103,11 +102,5 @@ namespace Avalonia.Controls.Utils _states.Clear(); _currentNode = null; } - - bool WeakTimer.IWeakTimerSubscriber.Tick() - { - Snapshot(); - return true; - } } } From 2c215d1b3917af0c31e55cb22e2ba07db203b535 Mon Sep 17 00:00:00 2001 From: Deadpikle Date: Tue, 8 Jun 2021 18:02:29 -0400 Subject: [PATCH 007/127] Make sure to snapshot undo before delete selected --- src/Avalonia.Controls/TextBox.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 31ec12c22f..23c0d70f20 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -349,6 +349,8 @@ namespace Avalonia.Controls { if (string.IsNullOrEmpty(value)) { + _selectedTextChangesMadeSinceLastUndoSnapshot++; + SnapshotUndoRedo(ignoreChangeCount: false); DeleteSelection(); } else From 11138b1fc5937bb268460b36dd259a64c6791caf Mon Sep 17 00:00:00 2001 From: Deadpikle Date: Tue, 8 Jun 2021 18:05:02 -0400 Subject: [PATCH 008/127] Remove extra snapshots after cut, paste --- src/Avalonia.Controls/TextBox.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 23c0d70f20..dad2401921 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -639,7 +639,6 @@ namespace Avalonia.Controls SnapshotUndoRedo(); Copy(); DeleteSelection(); - SnapshotUndoRedo(); } public async void Copy() @@ -659,7 +658,6 @@ namespace Avalonia.Controls SnapshotUndoRedo(); HandleTextInput(text); - SnapshotUndoRedo(); } protected override void OnKeyDown(KeyEventArgs e) From 2fe25f8acf3567ad1869942eb20c6bc068b44387 Mon Sep 17 00:00:00 2001 From: Deadpikle Date: Tue, 8 Jun 2021 18:48:12 -0400 Subject: [PATCH 009/127] Fix equality for UndoRedoState In UndoRedoHelper, the .Equals call in Snapshot() did not call Equals(UndoRedoState other). It called the default Equals(object obj) implementation. This could throw of the undo redo snapshotting. Fix that by overriding Equals(object obj). --- src/Avalonia.Controls/TextBox.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index dad2401921..edd268a86a 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -157,6 +157,13 @@ namespace Avalonia.Controls } public bool Equals(UndoRedoState other) => ReferenceEquals(Text, other.Text) || Equals(Text, other.Text); + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (obj.GetType() != GetType()) return false; + return Equals((UndoRedoState)obj); + } } private string _text; From f92804ff05b6361a0e355ad894e59c453927c6c7 Mon Sep 17 00:00:00 2001 From: Deadpikle Date: Tue, 8 Jun 2021 18:57:18 -0400 Subject: [PATCH 010/127] Improve UndoRedoState.Equals(object obj) impl --- src/Avalonia.Controls/TextBox.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index edd268a86a..b89bd22038 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -158,12 +158,9 @@ namespace Avalonia.Controls public bool Equals(UndoRedoState other) => ReferenceEquals(Text, other.Text) || Equals(Text, other.Text); - public override bool Equals(object obj) - { - if (ReferenceEquals(null, obj)) return false; - if (obj.GetType() != GetType()) return false; - return Equals((UndoRedoState)obj); - } + public override bool Equals(object obj) => obj is UndoRedoState other && Equals(other); + + public override int GetHashCode() => Text.GetHashCode(); } private string _text; From 4523450e0447928a11557618154df550b0c9051a Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 12 Jun 2021 15:16:25 -0400 Subject: [PATCH 011/127] Improve context flyout/menu sample pages --- .../Pages/ContextFlyoutPage.axaml | 102 ------------- .../Pages/ContextFlyoutPage.axaml.cs | 45 ------ .../Pages/ContextFlyoutPage.xaml | 143 ++++++++++++++++++ .../Pages/ContextFlyoutPage.xaml.cs | 91 +++++++++++ .../ControlCatalog/Pages/ContextMenuPage.xaml | 137 ++++++++++------- .../Pages/ContextMenuPage.xaml.cs | 42 ++++- .../ViewModels/ContextFlyoutPageViewModel.cs | 78 ---------- ...geViewModel.cs => ContextPageViewModel.cs} | 4 +- 8 files changed, 357 insertions(+), 285 deletions(-) delete mode 100644 samples/ControlCatalog/Pages/ContextFlyoutPage.axaml delete mode 100644 samples/ControlCatalog/Pages/ContextFlyoutPage.axaml.cs create mode 100644 samples/ControlCatalog/Pages/ContextFlyoutPage.xaml create mode 100644 samples/ControlCatalog/Pages/ContextFlyoutPage.xaml.cs delete mode 100644 samples/ControlCatalog/ViewModels/ContextFlyoutPageViewModel.cs rename samples/ControlCatalog/ViewModels/{ContextMenuPageViewModel.cs => ContextPageViewModel.cs} (96%) diff --git a/samples/ControlCatalog/Pages/ContextFlyoutPage.axaml b/samples/ControlCatalog/Pages/ContextFlyoutPage.axaml deleted file mode 100644 index f0e079ad91..0000000000 --- a/samples/ControlCatalog/Pages/ContextFlyoutPage.axaml +++ /dev/null @@ -1,102 +0,0 @@ - - - - - - - Context Flyout - A right click Flyout that can be applied to any control. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/samples/ControlCatalog/Pages/ContextFlyoutPage.axaml.cs b/samples/ControlCatalog/Pages/ContextFlyoutPage.axaml.cs deleted file mode 100644 index e64d4a2cdd..0000000000 --- a/samples/ControlCatalog/Pages/ContextFlyoutPage.axaml.cs +++ /dev/null @@ -1,45 +0,0 @@ -using Avalonia; -using Avalonia.Controls; -using Avalonia.Markup.Xaml; -using ControlCatalog.ViewModels; -using Avalonia.Interactivity; -namespace ControlCatalog.Pages -{ - public class ContextFlyoutPage : UserControl - { - private TextBox _textBox; - - public ContextFlyoutPage() - { - InitializeComponent(); - - var vm = new ContextFlyoutPageViewModel(); - vm.View = this; - DataContext = vm; - - _textBox = this.FindControl("TextBox"); - - var cutButton = this.FindControl + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/ContextFlyoutPage.xaml.cs b/samples/ControlCatalog/Pages/ContextFlyoutPage.xaml.cs new file mode 100644 index 0000000000..5126ae91bf --- /dev/null +++ b/samples/ControlCatalog/Pages/ContextFlyoutPage.xaml.cs @@ -0,0 +1,91 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using ControlCatalog.ViewModels; +using Avalonia.Interactivity; +using System; +using System.ComponentModel; + +namespace ControlCatalog.Pages +{ + public class ContextFlyoutPage : UserControl + { + private TextBox _textBox; + + public ContextFlyoutPage() + { + InitializeComponent(); + + DataContext = new ContextPageViewModel(); + + _textBox = this.FindControl("TextBox"); + + var cutButton = this.FindControl public event EventHandler? Opened; + internal event EventHandler? Closing; + public IPopupHost? Host => _openState?.PopupHost; public bool WindowManagerAddShadowHint @@ -567,6 +570,13 @@ namespace Avalonia.Controls.Primitives private void CloseCore() { + var closingArgs = new CancelEventArgs(); + Closing?.Invoke(this, closingArgs); + if (closingArgs.Cancel) + { + return; + } + _isOpenRequested = false; if (_openState is null) { From 8ced83c601caf7bb96c9350b0845277208a41b97 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sun, 13 Jun 2021 02:42:46 -0400 Subject: [PATCH 014/127] Update sample pages --- .../Pages/ContextFlyoutPage.xaml | 43 ++++++++++++------- .../Pages/ContextFlyoutPage.xaml.cs | 2 +- .../Pages/ContextMenuPage.xaml.cs | 2 +- 3 files changed, 30 insertions(+), 17 deletions(-) diff --git a/samples/ControlCatalog/Pages/ContextFlyoutPage.xaml b/samples/ControlCatalog/Pages/ContextFlyoutPage.xaml index cc17d9c747..0d9026bdd5 100644 --- a/samples/ControlCatalog/Pages/ContextFlyoutPage.xaml +++ b/samples/ControlCatalog/Pages/ContextFlyoutPage.xaml @@ -13,22 +13,22 @@ + + + + Context Flyout A right click Flyout that can be applied to any control. - - - - - + @@ -56,7 +56,7 @@ - + + + + + + + @@ -181,6 +256,9 @@ + + + diff --git a/samples/RenderDemo/Pages/TransitionsPage.xaml b/samples/RenderDemo/Pages/TransitionsPage.xaml index 1985074b0f..71b6ea0713 100644 --- a/samples/RenderDemo/Pages/TransitionsPage.xaml +++ b/samples/RenderDemo/Pages/TransitionsPage.xaml @@ -167,13 +167,80 @@ + + + + + + + + + + + + @@ -202,6 +269,15 @@ + + + + + + + + + From 9f7a5de29edfced86e19a72d1fe015e01bc81fb5 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sun, 4 Jul 2021 03:51:28 -0400 Subject: [PATCH 027/127] Add RelativePointAnimator --- .../Animators/RelativePointAnimator.cs | 20 +++++++++++++++++++ .../Transitions/RelativePointTransition.cs | 11 ++++++++++ src/Avalonia.Visuals/RelativePoint.cs | 7 +++++++ 3 files changed, 38 insertions(+) create mode 100644 src/Avalonia.Visuals/Animation/Animators/RelativePointAnimator.cs create mode 100644 src/Avalonia.Visuals/Animation/Transitions/RelativePointTransition.cs diff --git a/src/Avalonia.Visuals/Animation/Animators/RelativePointAnimator.cs b/src/Avalonia.Visuals/Animation/Animators/RelativePointAnimator.cs new file mode 100644 index 0000000000..40fa4503f0 --- /dev/null +++ b/src/Avalonia.Visuals/Animation/Animators/RelativePointAnimator.cs @@ -0,0 +1,20 @@ +namespace Avalonia.Animation.Animators +{ + /// + /// Animator that handles properties. + /// + public class RelativePointAnimator : Animator + { + private static readonly PointAnimator s_pointAnimator = new PointAnimator(); + + public override RelativePoint Interpolate(double progress, RelativePoint oldValue, RelativePoint newValue) + { + if (oldValue.Unit != newValue.Unit) + { + return progress >= 1 ? newValue : oldValue; + } + + return new RelativePoint(s_pointAnimator.Interpolate(progress, oldValue.Point, newValue.Point), oldValue.Unit); + } + } +} diff --git a/src/Avalonia.Visuals/Animation/Transitions/RelativePointTransition.cs b/src/Avalonia.Visuals/Animation/Transitions/RelativePointTransition.cs new file mode 100644 index 0000000000..4a7bfa8384 --- /dev/null +++ b/src/Avalonia.Visuals/Animation/Transitions/RelativePointTransition.cs @@ -0,0 +1,11 @@ +using Avalonia.Animation.Animators; + +namespace Avalonia.Animation +{ + /// + /// Transition class that handles with type. + /// + public class RelativePointTransition : AnimatorDrivenTransition + { + } +} diff --git a/src/Avalonia.Visuals/RelativePoint.cs b/src/Avalonia.Visuals/RelativePoint.cs index 097ea69be4..497820ec65 100644 --- a/src/Avalonia.Visuals/RelativePoint.cs +++ b/src/Avalonia.Visuals/RelativePoint.cs @@ -1,5 +1,7 @@ using System; using System.Globalization; + +using Avalonia.Animation.Animators; using Avalonia.Utilities; namespace Avalonia @@ -45,6 +47,11 @@ namespace Avalonia private readonly RelativeUnit _unit; + static RelativePoint() + { + Animation.Animation.RegisterAnimator(prop => typeof(RelativePoint).IsAssignableFrom(prop.PropertyType)); + } + /// /// Initializes a new instance of the struct. /// From ab071f1ba0f77e020a0f943be897e63fdf8dd8bc Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sun, 4 Jul 2021 03:52:11 -0400 Subject: [PATCH 028/127] Add IGradientBrushAnimator --- .../Animation/Animators/BaseBrushAnimator.cs | 35 ++++++--- .../Animators/GradientBrushAnimator.cs | 74 +++++++++++++++++++ .../Animators/SolidColorBrushAnimator.cs | 4 +- .../Animation/Transitions/BrushTransition.cs | 32 ++++---- src/Avalonia.Visuals/Media/GradientBrush.cs | 3 + 5 files changed, 120 insertions(+), 28 deletions(-) create mode 100644 src/Avalonia.Visuals/Animation/Animators/GradientBrushAnimator.cs diff --git a/src/Avalonia.Visuals/Animation/Animators/BaseBrushAnimator.cs b/src/Avalonia.Visuals/Animation/Animators/BaseBrushAnimator.cs index 508891fd72..be674269bf 100644 --- a/src/Avalonia.Visuals/Animation/Animators/BaseBrushAnimator.cs +++ b/src/Avalonia.Visuals/Animation/Animators/BaseBrushAnimator.cs @@ -4,6 +4,8 @@ using System.Reactive.Disposables; using Avalonia.Logging; using Avalonia.Media; +#nullable enable + namespace Avalonia.Animation.Animators { /// @@ -12,9 +14,9 @@ namespace Avalonia.Animation.Animators /// redirect them to the properly registered /// animators in this class. /// - public class BaseBrushAnimator : Animator + public class BaseBrushAnimator : Animator { - private IAnimator _targetAnimator; + private IAnimator? _targetAnimator; private static readonly List<(Func Match, Type AnimatorType)> _brushAnimators = new List<(Func Match, Type AnimatorType)>(); @@ -31,7 +33,7 @@ namespace Avalonia.Animation.Animators /// The type of the animator to instantiate. /// public static void RegisterBrushAnimator(Func condition) - where TAnimator : IAnimator + where TAnimator : IAnimator, new() { _brushAnimators.Insert(0, (condition, typeof(TAnimator))); } @@ -40,20 +42,18 @@ namespace Avalonia.Animation.Animators public override IDisposable Apply(Animation animation, Animatable control, IClock clock, IObservable match, Action onComplete) { - foreach (var valueType in _brushAnimators) - { - if (!valueType.Match(this[0].Value.GetType())) continue; - - _targetAnimator = (IAnimator)Activator.CreateInstance(valueType.AnimatorType); + _targetAnimator = CreateAnimatorFromType(this[0].Value.GetType()); + if (_targetAnimator != null) + { foreach (var keyframe in this) { _targetAnimator.Add(keyframe); } _targetAnimator.Property = this.Property; - - return _targetAnimator.Apply(animation, control, clock, match, onComplete); + + return _targetAnimator.Apply(animation, control, clock, match, onComplete); } Logger.TryGet(LogEventLevel.Error, LogArea.Animations)?.Log( @@ -64,6 +64,19 @@ namespace Avalonia.Animation.Animators } /// - public override IBrush Interpolate(double progress, IBrush oldValue, IBrush newValue) => null; + public override IBrush? Interpolate(double progress, IBrush? oldValue, IBrush? newValue) => null; + + internal static IAnimator? CreateAnimatorFromType(Type type) + { + foreach (var (match, animatorType) in _brushAnimators) + { + if (!match(type)) + continue; + + return (IAnimator)Activator.CreateInstance(animatorType); + } + + return null; + } } } diff --git a/src/Avalonia.Visuals/Animation/Animators/GradientBrushAnimator.cs b/src/Avalonia.Visuals/Animation/Animators/GradientBrushAnimator.cs new file mode 100644 index 0000000000..e51103b9b5 --- /dev/null +++ b/src/Avalonia.Visuals/Animation/Animators/GradientBrushAnimator.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +using Avalonia.Data; +using Avalonia.Media; +using Avalonia.Media.Immutable; + +namespace Avalonia.Animation.Animators +{ + /// + /// Animator that handles values. + /// + public class IGradientBrushAnimator : Animator + { + private static readonly RelativePointAnimator s_relativePointAnimator = new RelativePointAnimator(); + private static readonly DoubleAnimator s_doubleAnimator = new DoubleAnimator(); + + public override IGradientBrush Interpolate(double progress, IGradientBrush oldValue, IGradientBrush newValue) + { + if (oldValue is null || newValue is null + || oldValue.GradientStops.Count != oldValue.GradientStops.Count) + { + return progress >= 1 ? newValue : oldValue; + } + + switch (oldValue) + { + case IRadialGradientBrush oldRadial when newValue is IRadialGradientBrush newRadial: + return new ImmutableRadialGradientBrush( + InterpolateStops(progress, oldValue.GradientStops, newValue.GradientStops), + s_doubleAnimator.Interpolate(progress, oldValue.Opacity, newValue.Opacity), + oldValue.SpreadMethod, + s_relativePointAnimator.Interpolate(progress, oldRadial.Center, newRadial.Center), + s_relativePointAnimator.Interpolate(progress, oldRadial.GradientOrigin, newRadial.GradientOrigin), + s_doubleAnimator.Interpolate(progress, oldRadial.Radius, newRadial.Radius)); + + case IConicGradientBrush oldConic when newValue is IConicGradientBrush newConic: + return new ImmutableConicGradientBrush( + InterpolateStops(progress, oldValue.GradientStops, newValue.GradientStops), + s_doubleAnimator.Interpolate(progress, oldValue.Opacity, newValue.Opacity), + oldValue.SpreadMethod, + s_relativePointAnimator.Interpolate(progress, oldConic.Center, newConic.Center), + s_doubleAnimator.Interpolate(progress, oldConic.Angle, newConic.Angle)); + + case ILinearGradientBrush oldLinear when newValue is ILinearGradientBrush newLinear: + return new ImmutableLinearGradientBrush( + InterpolateStops(progress, oldValue.GradientStops, newValue.GradientStops), + s_doubleAnimator.Interpolate(progress, oldValue.Opacity, newValue.Opacity), + oldValue.SpreadMethod, + s_relativePointAnimator.Interpolate(progress, oldLinear.StartPoint, newLinear.StartPoint), + s_relativePointAnimator.Interpolate(progress, oldLinear.EndPoint, newLinear.EndPoint)); + + default: + return progress >= 1 ? newValue : oldValue; + } + } + + public override IDisposable BindAnimation(Animatable control, IObservable instance) + { + return control.Bind((AvaloniaProperty)Property, instance, BindingPriority.Animation); + } + + private IReadOnlyList InterpolateStops(double progress, IReadOnlyList oldValue, IReadOnlyList newValue) + { + // pool + return oldValue + .Zip(newValue, (f, s) => new ImmutableGradientStop( + s_doubleAnimator.Interpolate(progress, f.Offset, s.Offset), + ColorAnimator.InterpolateCore(progress, f.Color, s.Color))) + .ToArray(); + } + } +} diff --git a/src/Avalonia.Visuals/Animation/Animators/SolidColorBrushAnimator.cs b/src/Avalonia.Visuals/Animation/Animators/SolidColorBrushAnimator.cs index a56cc1de8c..ba2f2ae766 100644 --- a/src/Avalonia.Visuals/Animation/Animators/SolidColorBrushAnimator.cs +++ b/src/Avalonia.Visuals/Animation/Animators/SolidColorBrushAnimator.cs @@ -14,7 +14,7 @@ namespace Avalonia.Animation.Animators { if (oldValue is null || newValue is null) { - return oldValue; + return progress >= 1 ? newValue : oldValue; } return new ImmutableSolidColorBrush(ColorAnimator.InterpolateCore(progress, oldValue.Color, newValue.Color)); @@ -26,7 +26,7 @@ namespace Avalonia.Animation.Animators } } - [Obsolete] + [Obsolete("Use ISolidColorBrushAnimator instead")] public class SolidColorBrushAnimator : Animator { public override SolidColorBrush Interpolate(double progress, SolidColorBrush oldValue, SolidColorBrush newValue) diff --git a/src/Avalonia.Visuals/Animation/Transitions/BrushTransition.cs b/src/Avalonia.Visuals/Animation/Transitions/BrushTransition.cs index cc5af1b4b1..7cc3f597b5 100644 --- a/src/Avalonia.Visuals/Animation/Transitions/BrushTransition.cs +++ b/src/Avalonia.Visuals/Animation/Transitions/BrushTransition.cs @@ -1,4 +1,6 @@ using System; +using System.Linq; + using Avalonia.Animation.Animators; using Avalonia.Animation.Easings; using Avalonia.Media; @@ -9,34 +11,34 @@ namespace Avalonia.Animation { /// /// Transition class that handles with type. - /// Only values of will transition correctly at the moment. /// public class BrushTransition : Transition { - private static readonly ISolidColorBrushAnimator s_animator = new ISolidColorBrushAnimator(); - public override IObservable DoTransition(IObservable progress, IBrush? oldValue, IBrush? newValue) { - var oldSolidColorBrush = TryGetSolidColorBrush(oldValue); - var newSolidColorBrush = TryGetSolidColorBrush(newValue); + var type = oldValue?.GetType() ?? newValue?.GetType(); + if (type == null) + { + return new IncompatibleTransitionObservable(progress, Easing, oldValue, newValue); + } - if (oldSolidColorBrush != null && newSolidColorBrush != null) + var animator = BaseBrushAnimator.CreateAnimatorFromType(type); + if (animator == null) { - return new AnimatorTransitionObservable( - s_animator, progress, Easing, oldSolidColorBrush, newSolidColorBrush); + return new IncompatibleTransitionObservable(progress, Easing, oldValue, newValue); } - return new IncompatibleTransitionObservable(progress, Easing, oldValue, newValue); - } + var animatorType = animator.GetType(); + var animatorGenericArgument = animatorType.BaseType.GetGenericArguments().FirstOrDefault() ?? type; - private static ISolidColorBrush? TryGetSolidColorBrush(IBrush? brush) - { - if (brush is null) + var observableType = typeof(AnimatorTransitionObservable<,>).MakeGenericType(animatorGenericArgument, animatorType); + var observable = Activator.CreateInstance(observableType, animator, progress, Easing, oldValue, newValue) as IObservable; + if (observable == null) { - return Brushes.Transparent; + return new IncompatibleTransitionObservable(progress, Easing, oldValue, newValue); } - return brush as ISolidColorBrush; + return observable; } private class IncompatibleTransitionObservable : TransitionObservableBase diff --git a/src/Avalonia.Visuals/Media/GradientBrush.cs b/src/Avalonia.Visuals/Media/GradientBrush.cs index 99923b8e06..4fb753a9de 100644 --- a/src/Avalonia.Visuals/Media/GradientBrush.cs +++ b/src/Avalonia.Visuals/Media/GradientBrush.cs @@ -2,6 +2,8 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; + +using Avalonia.Animation.Animators; using Avalonia.Collections; using Avalonia.Metadata; @@ -28,6 +30,7 @@ namespace Avalonia.Media static GradientBrush() { + BaseBrushAnimator.RegisterBrushAnimator(match => typeof(IGradientBrush).IsAssignableFrom(match)); GradientStopsProperty.Changed.Subscribe(GradientStopsChanged); AffectsRender(SpreadMethodProperty); } From a885e673c831fe660323b1ff6e9e2ddce34e6d12 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sun, 4 Jul 2021 20:37:48 -0400 Subject: [PATCH 029/127] Reflection free implementation with automatic convertion from solid color brush to gradient --- samples/RenderDemo/Pages/AnimationsPage.xaml | 34 ++++- .../Animation/Animators/BaseBrushAnimator.cs | 129 +++++++++++++++--- .../Animators/GradientBrushAnimator.cs | 65 +++++++-- .../Animators/SolidColorBrushAnimator.cs | 18 +-- .../Animation/Transitions/BrushTransition.cs | 40 +++--- src/Avalonia.Visuals/Media/GradientBrush.cs | 1 - src/Avalonia.Visuals/Media/SolidColorBrush.cs | 1 - 7 files changed, 225 insertions(+), 63 deletions(-) diff --git a/samples/RenderDemo/Pages/AnimationsPage.xaml b/samples/RenderDemo/Pages/AnimationsPage.xaml index 48fca61d09..3981f7b51b 100644 --- a/samples/RenderDemo/Pages/AnimationsPage.xaml +++ b/samples/RenderDemo/Pages/AnimationsPage.xaml @@ -168,6 +168,9 @@ IterationCount="Infinite" PlaybackDirection="Alternate"> + + + @@ -175,6 +178,9 @@ + + + @@ -188,6 +194,31 @@ + + - @@ -226,16 +237,20 @@ - + + - + - + + + + @@ -247,10 +262,34 @@ + PlaybackDirection="Normal"> - + + + + + + + + + + + + + + + + + + + + + + + + + @@ -258,9 +297,9 @@ - - - + + + diff --git a/src/Avalonia.Visuals/Animation/Animators/GradientBrushAnimator.cs b/src/Avalonia.Visuals/Animation/Animators/GradientBrushAnimator.cs index 25b4a2826c..6481f815de 100644 --- a/src/Avalonia.Visuals/Animation/Animators/GradientBrushAnimator.cs +++ b/src/Avalonia.Visuals/Animation/Animators/GradientBrushAnimator.cs @@ -19,8 +19,7 @@ namespace Avalonia.Animation.Animators public override IGradientBrush? Interpolate(double progress, IGradientBrush? oldValue, IGradientBrush? newValue) { - if (oldValue is null || newValue is null - || oldValue.GradientStops.Count != newValue.GradientStops.Count) + if (oldValue is null || newValue is null) { return progress >= 0.5 ? newValue : oldValue; } @@ -64,13 +63,26 @@ namespace Avalonia.Animation.Animators private IReadOnlyList InterpolateStops(double progress, IReadOnlyList oldValue, IReadOnlyList newValue) { - var stops = new ImmutableGradientStop[oldValue.Count]; - for (int index = 0; index < oldValue.Count; index++) + var resultCount = Math.Max(oldValue.Count, newValue.Count); + var stops = new ImmutableGradientStop[resultCount]; + + for (int index = 0, oldIndex = 0, newIndex = 0; index < resultCount; index++) { stops[index] = new ImmutableGradientStop( - s_doubleAnimator.Interpolate(progress, oldValue[index].Offset, newValue[index].Offset), - ColorAnimator.InterpolateCore(progress, oldValue[index].Color, newValue[index].Color)); + s_doubleAnimator.Interpolate(progress, oldValue[oldIndex].Offset, newValue[newIndex].Offset), + ColorAnimator.InterpolateCore(progress, oldValue[oldIndex].Color, newValue[newIndex].Color)); + + if (oldIndex < oldValue.Count - 1) + { + oldIndex++; + } + + if (newIndex < newValue.Count - 1) + { + newIndex++; + } } + return stops; } @@ -80,29 +92,29 @@ namespace Avalonia.Animation.Animators { case IRadialGradientBrush oldRadial: return new ImmutableRadialGradientBrush( - CreateStopsFromSolidColorBrush(solidColorBrush, oldRadial), solidColorBrush.Opacity, + CreateStopsFromSolidColorBrush(solidColorBrush, oldRadial.GradientStops), solidColorBrush.Opacity, oldRadial.SpreadMethod, oldRadial.Center, oldRadial.GradientOrigin, oldRadial.Radius); case IConicGradientBrush oldConic: return new ImmutableConicGradientBrush( - CreateStopsFromSolidColorBrush(solidColorBrush, oldConic), solidColorBrush.Opacity, + CreateStopsFromSolidColorBrush(solidColorBrush, oldConic.GradientStops), solidColorBrush.Opacity, oldConic.SpreadMethod, oldConic.Center, oldConic.Angle); case ILinearGradientBrush oldLinear: return new ImmutableLinearGradientBrush( - CreateStopsFromSolidColorBrush(solidColorBrush, oldLinear), solidColorBrush.Opacity, + CreateStopsFromSolidColorBrush(solidColorBrush, oldLinear.GradientStops), solidColorBrush.Opacity, oldLinear.SpreadMethod, oldLinear.StartPoint, oldLinear.EndPoint); default: throw new NotSupportedException($"Gradient of type {gradientBrush?.GetType()} is not supported"); } - static IReadOnlyList CreateStopsFromSolidColorBrush(ISolidColorBrush solidColorBrush, IGradientBrush baseGradient) + static IReadOnlyList CreateStopsFromSolidColorBrush(ISolidColorBrush solidColorBrush, IReadOnlyList baseStops) { - var stops = new ImmutableGradientStop[baseGradient.GradientStops.Count]; - for (int index = 0; index < baseGradient.GradientStops.Count; index++) + var stops = new ImmutableGradientStop[baseStops.Count]; + for (int index = 0; index < baseStops.Count; index++) { - stops[index] = new ImmutableGradientStop(baseGradient.GradientStops[index].Offset, solidColorBrush.Color); + stops[index] = new ImmutableGradientStop(baseStops[index].Offset, solidColorBrush.Color); } return stops; } From cd51d638e93bad0eeb40408509a2e074ab16b56e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 5 Jul 2021 08:51:29 +0200 Subject: [PATCH 031/127] Pass dialog flag to window impl Show. Not doing anything with it yet. --- .../Platform/IWindowBaseImpl.cs | 4 +++- src/Avalonia.Controls/Window.cs | 4 ++-- src/Avalonia.Controls/WindowBase.cs | 2 +- .../Remote/PreviewerWindowImpl.cs | 6 +----- src/Avalonia.DesignerSupport/Remote/Stubs.cs | 2 +- src/Avalonia.Headless/HeadlessWindowImpl.cs | 7 +------ src/Avalonia.Native/PopupImpl.cs | 4 ++-- src/Avalonia.Native/WindowImplBase.cs | 2 +- src/Avalonia.X11/X11Window.cs | 2 +- src/Windows/Avalonia.Win32/PopupImpl.cs | 2 +- src/Windows/Avalonia.Win32/Win32Platform.cs | 2 +- src/Windows/Avalonia.Win32/WindowImpl.cs | 4 ++-- .../ContextMenuTests.cs | 16 ++++++++-------- .../WindowBaseTests.cs | 2 +- .../Avalonia.UnitTests/MockWindowingPlatform.cs | 2 +- 15 files changed, 27 insertions(+), 34 deletions(-) diff --git a/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs b/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs index 0d303a6666..5172569726 100644 --- a/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs +++ b/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs @@ -7,7 +7,9 @@ namespace Avalonia.Platform /// /// Shows the window. /// - void Show(bool activate); + /// Whether to activate the shown window. + /// Whether the window is being shown as a dialog. + void Show(bool activate, bool isDialog); /// /// Hides the window. diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 700c3d9bad..f5a86b8b21 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -671,7 +671,7 @@ namespace Avalonia.Controls SetWindowStartupLocation(Owner?.PlatformImpl); - PlatformImpl?.Show(ShowActivated); + PlatformImpl?.Show(ShowActivated, false); Renderer?.Start(); } OnOpened(EventArgs.Empty); @@ -743,7 +743,7 @@ namespace Avalonia.Controls SetWindowStartupLocation(owner.PlatformImpl); - PlatformImpl?.Show(ShowActivated); + PlatformImpl?.Show(ShowActivated, true); Renderer?.Start(); diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs index cdcb499e98..3080def895 100644 --- a/src/Avalonia.Controls/WindowBase.cs +++ b/src/Avalonia.Controls/WindowBase.cs @@ -162,7 +162,7 @@ namespace Avalonia.Controls LayoutManager.ExecuteInitialLayoutPass(); _hasExecutedInitialLayoutPass = true; } - PlatformImpl?.Show(true); + PlatformImpl?.Show(true, false); Renderer?.Start(); OnOpened(EventArgs.Empty); } diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs index 787f44887f..e82ec0fb0f 100644 --- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs +++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs @@ -20,7 +20,7 @@ namespace Avalonia.DesignerSupport.Remote ClientSize = new Size(1, 1); } - public void Show(bool activate) + public void Show(bool activate, bool isDialog) { } @@ -99,10 +99,6 @@ namespace Avalonia.DesignerSupport.Remote { } - public void ShowDialog(IWindowImpl parent) - { - } - public void SetSystemDecorations(SystemDecorations enabled) { } diff --git a/src/Avalonia.DesignerSupport/Remote/Stubs.cs b/src/Avalonia.DesignerSupport/Remote/Stubs.cs index eedfc52d9d..e5fd5a91e6 100644 --- a/src/Avalonia.DesignerSupport/Remote/Stubs.cs +++ b/src/Avalonia.DesignerSupport/Remote/Stubs.cs @@ -77,7 +77,7 @@ namespace Avalonia.DesignerSupport.Remote { } - public void Show(bool activate) + public void Show(bool activate, bool isDialog) { } diff --git a/src/Avalonia.Headless/HeadlessWindowImpl.cs b/src/Avalonia.Headless/HeadlessWindowImpl.cs index af522f3e36..3c0dc5f6e2 100644 --- a/src/Avalonia.Headless/HeadlessWindowImpl.cs +++ b/src/Avalonia.Headless/HeadlessWindowImpl.cs @@ -75,7 +75,7 @@ namespace Avalonia.Headless public Action Closed { get; set; } public IMouseDevice MouseDevice { get; } - public void Show(bool activate) + public void Show(bool activate, bool isDialog) { if (activate) Dispatcher.UIThread.Post(() => Activated?.Invoke(), DispatcherPriority.Input); @@ -147,11 +147,6 @@ namespace Avalonia.Headless } - public void ShowDialog(IWindowImpl parent) - { - Show(true); - } - public void SetSystemDecorations(bool enabled) { diff --git a/src/Avalonia.Native/PopupImpl.cs b/src/Avalonia.Native/PopupImpl.cs index c36675afcd..8740dd6f12 100644 --- a/src/Avalonia.Native/PopupImpl.cs +++ b/src/Avalonia.Native/PopupImpl.cs @@ -60,14 +60,14 @@ namespace Avalonia.Native } } - public override void Show(bool activate) + public override void Show(bool activate, bool isDialog) { var parent = _parent; while (parent is PopupImpl p) parent = p._parent; if (parent is WindowImpl w) w.Native.TakeFocusFromChildren(); - base.Show(false); + base.Show(false, isDialog); } public override IPopupImpl CreatePopup() => new PopupImpl(_factory, _opts, _glFeature, this); diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index f716464d14..f444db5bc1 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -351,7 +351,7 @@ namespace Avalonia.Native } - public virtual void Show(bool activate) + public virtual void Show(bool activate, bool isDialog) { _native.Show(activate.AsComBool()); } diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 37260aa78b..26edf10671 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -808,7 +808,7 @@ namespace Avalonia.X11 XSetTransientForHint(_x11.Display, _handle, parent.Handle.Handle); } - public void Show(bool activate) + public void Show(bool activate, bool isDialog) { _wasMappedAtLeastOnce = true; XMapWindow(_x11.Display, _handle); diff --git a/src/Windows/Avalonia.Win32/PopupImpl.cs b/src/Windows/Avalonia.Win32/PopupImpl.cs index dd3fd1342c..7b2d20a986 100644 --- a/src/Windows/Avalonia.Win32/PopupImpl.cs +++ b/src/Windows/Avalonia.Win32/PopupImpl.cs @@ -17,7 +17,7 @@ namespace Avalonia.Win32 [ThreadStatic] private static IntPtr s_parentHandle; - public override void Show(bool activate) + public override void Show(bool activate, bool isDialog) { // Popups are always shown non-activated. UnmanagedMethods.ShowWindow(Handle.Handle, UnmanagedMethods.ShowWindowCommand.ShowNoActivate); diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index d92bd08d01..e3b01e8071 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -246,7 +246,7 @@ namespace Avalonia.Win32 public IWindowImpl CreateEmbeddableWindow() { var embedded = new EmbeddedWindowImpl(); - embedded.Show(true); + embedded.Show(true, false); return embedded; } diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 3a3342fd14..c46cbb3274 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -570,7 +570,7 @@ namespace Avalonia.Win32 _shown = false; } - public virtual void Show(bool activate) + public virtual void Show(bool activate, bool isDialog) { SetParent(_parent); ShowWindow(_showWindowState, activate); @@ -1120,7 +1120,7 @@ namespace Avalonia.Win32 SetParent(null); if (shown) - Show(activated); + Show(activated, false); } } else diff --git a/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs b/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs index f3a1316c7d..3d0c3b4c84 100644 --- a/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs @@ -128,7 +128,7 @@ namespace Avalonia.Controls.UnitTests { using (Application()) { - popupImpl.Setup(x => x.Show(true)).Verifiable(); + popupImpl.Setup(x => x.Show(true, false)).Verifiable(); popupImpl.Setup(x => x.Hide()).Verifiable(); var sut = new ContextMenu(); @@ -148,7 +148,7 @@ namespace Avalonia.Controls.UnitTests _mouse.Click(target); Assert.False(sut.IsOpen); - popupImpl.Verify(x => x.Show(true), Times.Once); + popupImpl.Verify(x => x.Show(true, false), Times.Once); popupImpl.Verify(x => x.Hide(), Times.Once); } } @@ -158,7 +158,7 @@ namespace Avalonia.Controls.UnitTests { using (Application()) { - popupImpl.Setup(x => x.Show(true)).Verifiable(); + popupImpl.Setup(x => x.Show(true, false)).Verifiable(); popupImpl.Setup(x => x.Hide()).Verifiable(); var sut = new ContextMenu(); @@ -179,7 +179,7 @@ namespace Avalonia.Controls.UnitTests Assert.True(sut.IsOpen); popupImpl.Verify(x => x.Hide(), Times.Once); - popupImpl.Verify(x => x.Show(true), Times.Exactly(2)); + popupImpl.Verify(x => x.Show(true, false), Times.Exactly(2)); } } @@ -226,7 +226,7 @@ namespace Avalonia.Controls.UnitTests { using (Application()) { - popupImpl.Setup(x => x.Show(true)).Verifiable(); + popupImpl.Setup(x => x.Show(true, false)).Verifiable(); bool eventCalled = false; var sut = new ContextMenu(); @@ -242,7 +242,7 @@ namespace Avalonia.Controls.UnitTests Assert.True(eventCalled); Assert.False(sut.IsOpen); - popupImpl.Verify(x => x.Show(true), Times.Never); + popupImpl.Verify(x => x.Show(true, false), Times.Never); } } @@ -346,7 +346,7 @@ namespace Avalonia.Controls.UnitTests { using (Application()) { - popupImpl.Setup(x => x.Show(true)).Verifiable(); + popupImpl.Setup(x => x.Show(true, false)).Verifiable(); popupImpl.Setup(x => x.Hide()).Verifiable(); bool eventCalled = false; @@ -370,7 +370,7 @@ namespace Avalonia.Controls.UnitTests Assert.True(eventCalled); Assert.True(sut.IsOpen); - popupImpl.Verify(x => x.Show(true), Times.Once()); + popupImpl.Verify(x => x.Show(true, false), Times.Once()); popupImpl.Verify(x => x.Hide(), Times.Never); } } diff --git a/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs b/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs index 8109b037c5..1b4214e0c7 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs @@ -137,7 +137,7 @@ namespace Avalonia.Controls.UnitTests var target = new TestWindowBase(windowImpl.Object); target.IsVisible = true; - windowImpl.Verify(x => x.Show(true)); + windowImpl.Verify(x => x.Show(true, false)); } } diff --git a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs index 8a24a8366f..713283aa2b 100644 --- a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs +++ b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs @@ -58,7 +58,7 @@ namespace Avalonia.UnitTests windowImpl.Object.Resized?.Invoke(clientSize); }); - windowImpl.Setup(x => x.Show(true)).Callback(() => + windowImpl.Setup(x => x.Show(true, It.IsAny())).Callback(() => { windowImpl.Object.Activated?.Invoke(); }); From 32c0eac41529158153ce0bc1294f73e5010ada4c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 5 Jul 2021 09:27:46 +0200 Subject: [PATCH 032/127] Prevent dialog parent from becoming key window. --- native/Avalonia.Native/src/OSX/window.h | 1 + native/Avalonia.Native/src/OSX/window.mm | 42 +++++++++++++++++++++--- src/Avalonia.Native/WindowImplBase.cs | 2 +- src/Avalonia.Native/avn.idl | 2 +- 4 files changed, 40 insertions(+), 7 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/window.h b/native/Avalonia.Native/src/OSX/window.h index b1f64bca88..e2f69c8359 100644 --- a/native/Avalonia.Native/src/OSX/window.h +++ b/native/Avalonia.Native/src/OSX/window.h @@ -34,6 +34,7 @@ class WindowBaseImpl; -(double) getScaling; -(double) getExtendedTitleBarHeight; -(void) setIsExtended:(bool)value; +-(bool) isDialog; @end struct INSWindowHolder diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index d206d63a89..4d8303a173 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -105,7 +105,7 @@ public: return Window; } - virtual HRESULT Show(bool activate) override + virtual HRESULT Show(bool activate, bool isDialog) override { @autoreleasepool { @@ -488,6 +488,11 @@ public: return S_OK; } + virtual bool IsDialog() + { + return false; + } + protected: virtual NSWindowStyleMask GetStyle() { @@ -518,6 +523,7 @@ private: NSRect _preZoomSize; bool _transitioningWindowState; bool _isClientAreaExtended; + bool _isDialog; AvnExtendClientAreaChromeHints _extendClientHints; FORWARD_IUNKNOWN() @@ -573,11 +579,12 @@ private: } } - virtual HRESULT Show (bool activate) override + virtual HRESULT Show (bool activate, bool isDialog) override { @autoreleasepool - { - WindowBaseImpl::Show(activate); + { + _isDialog = isDialog; + WindowBaseImpl::Show(activate, isDialog); HideOrShowTrafficLights(); @@ -1070,6 +1077,11 @@ private: } } + virtual bool IsDialog() override + { + return _isDialog; + } + protected: virtual NSWindowStyleMask GetStyle() override { @@ -1858,6 +1870,11 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent _isExtended = value; } +-(bool) isDialog +{ + return _parent->IsDialog(); +} + -(double) getScaling { return _lastScaling; @@ -2047,7 +2064,22 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent -(BOOL)canBecomeKeyWindow { - return _canBecomeKeyAndMain; + if (_canBecomeKeyAndMain) + { + // If the window has a child window being shown as a dialog then don't allow it to become the key window. + for(NSWindow* uch in [self childWindows]) + { + auto ch = objc_cast(uch); + if(ch == nil) + continue; + if (ch.isDialog) + return false; + } + + return true; + } + + return false; } -(BOOL)canBecomeMainWindow diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index f444db5bc1..0dba11af5a 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -353,7 +353,7 @@ namespace Avalonia.Native public virtual void Show(bool activate, bool isDialog) { - _native.Show(activate.AsComBool()); + _native.Show(activate.AsComBool(), isDialog.AsComBool()); } diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl index adcbeb2d3a..697b66f21a 100644 --- a/src/Avalonia.Native/avn.idl +++ b/src/Avalonia.Native/avn.idl @@ -430,7 +430,7 @@ interface IAvnString : IUnknown [uuid(e5aca675-02b7-4129-aa79-d6e417210bda)] interface IAvnWindowBase : IUnknown { - HRESULT Show(bool activate); + HRESULT Show(bool activate, bool isDialog); HRESULT Hide(); HRESULT Close(); HRESULT Activate(); From f0e72f73f62e234c9afc79034f01420ab3a44e73 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 5 Jul 2021 10:50:47 +0200 Subject: [PATCH 033/127] Update ApiCompatBaseline.txt --- src/Avalonia.Controls/ApiCompatBaseline.txt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/ApiCompatBaseline.txt b/src/Avalonia.Controls/ApiCompatBaseline.txt index 46c12ebd39..1b2fbc5144 100644 --- a/src/Avalonia.Controls/ApiCompatBaseline.txt +++ b/src/Avalonia.Controls/ApiCompatBaseline.txt @@ -12,4 +12,7 @@ EnumValuesMustMatch : Enum value 'Avalonia.Platform.ExtendClientAreaChromeHints InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.ICursorImpl)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' is present in the contract but not in the implementation. MembersMustExist : Member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract. -Total Issues: 13 +InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IWindowBaseImpl.Show(System.Boolean)' is present in the contract but not in the implementation. +MembersMustExist : Member 'public void Avalonia.Platform.IWindowBaseImpl.Show(System.Boolean)' does not exist in the implementation but it does exist in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IWindowBaseImpl.Show(System.Boolean, System.Boolean)' is present in the implementation but not in the contract. +Total Issues: 16 From 05361cdb748a29d67d480246702544165fa1c0bd Mon Sep 17 00:00:00 2001 From: Luis von der Eltz Date: Mon, 5 Jul 2021 15:56:34 +0200 Subject: [PATCH 034/127] Adding option to inspect Popup visual tree --- .../Diagnostics/ViewModels/MainViewModel.cs | 1 - .../Diagnostics/ViewModels/VisualTreeNode.cs | 30 ++++-- .../Diagnostics/Views/MainWindow.xaml.cs | 91 ++++++++++++------- 3 files changed, 81 insertions(+), 41 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs index 3f367165ac..07ae222a9c 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs @@ -1,6 +1,5 @@ using System; using System.ComponentModel; - using Avalonia.Controls; using Avalonia.Diagnostics.Models; using Avalonia.Input; diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs index 48fa636664..b9981babf7 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs @@ -1,5 +1,6 @@ using System; using Avalonia.Collections; +using Avalonia.Controls.Primitives; using Avalonia.Styling; using Avalonia.VisualTree; @@ -24,8 +25,7 @@ namespace Avalonia.Diagnostics.ViewModels public static VisualTreeNode[] Create(object control) { - var visual = control as IVisual; - return visual != null ? new[] { new VisualTreeNode(visual, null) } : Array.Empty(); + return control is IVisual visual ? new[] { new VisualTreeNode(visual, null) } : Array.Empty(); } internal class VisualTreeNodeCollection : TreeNodeCollection @@ -46,10 +46,28 @@ namespace Avalonia.Diagnostics.ViewModels protected override void Initialize(AvaloniaList nodes) { - _subscription = _control.VisualChildren.ForEachItem( - (i, item) => nodes.Insert(i, new VisualTreeNode(item, Owner)), - (i, item) => nodes.RemoveAt(i), - () => nodes.Clear()); + if (_control is Popup p) + { + _subscription = p.GetObservable(Popup.ChildProperty).Subscribe(child => + { + if (child != null) + { + nodes.Add(new VisualTreeNode(child, Owner)); + } + else + { + nodes.Clear(); + } + + }); + } + else + { + _subscription = _control.VisualChildren.ForEachItem( + (i, item) => nodes.Insert(i, new VisualTreeNode(item, Owner)), + (i, item) => nodes.RemoveAt(i), + () => nodes.Clear()); + } } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs index d1232b749a..9d37b5acdc 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs @@ -27,19 +27,19 @@ namespace Avalonia.Diagnostics.Views EventHandler? lh = default; lh = (s, e) => - { - this.Opened -= lh; - if ((DataContext as MainViewModel)?.StartupScreenIndex is int index) - { - var screens = this.Screens; - if (index > -1 && index < screens.ScreenCount) - { - var screen = screens.All[index]; - this.Position = screen.Bounds.TopLeft; - this.WindowState = WindowState.Maximized; - } - } - }; + { + this.Opened -= lh; + if ((DataContext as MainViewModel)?.StartupScreenIndex is { } index) + { + var screens = this.Screens; + if (index > -1 && index < screens.ScreenCount) + { + var screen = screens.All[index]; + this.Position = screen.Bounds.TopLeft; + this.WindowState = WindowState.Maximized; + } + } + }; this.Opened += lh; } @@ -91,6 +91,24 @@ namespace Avalonia.Diagnostics.Views AvaloniaXamlLoader.Load(this); } + private IControl? GetHoveredControl(TopLevel topLevel) + { +#pragma warning disable CS0618 // Type or member is obsolete + var point = (topLevel as IInputRoot)?.MouseDevice?.GetPosition(topLevel) ?? default; +#pragma warning restore CS0618 // Type or member is obsolete + + return (IControl?)topLevel.GetVisualsAt(point, x => + { + if (x is AdornerLayer || !x.IsVisible) + { + return false; + } + + return !(x is IInputElement ie) || ie.IsHitTestVisible; + }) + .FirstOrDefault(); + } + private void RawKeyDown(RawKeyEventArgs e) { var vm = (MainViewModel?)DataContext; @@ -99,34 +117,39 @@ namespace Avalonia.Diagnostics.Views return; } - const RawInputModifiers modifiers = RawInputModifiers.Control | RawInputModifiers.Shift; - - if (e.Modifiers == modifiers) + switch (e.Modifiers) { -#pragma warning disable CS0618 // Type or member is obsolete - var point = (Root as IInputRoot)?.MouseDevice?.GetPosition(Root) ?? default; -#pragma warning restore CS0618 // Type or member is obsolete + case RawInputModifiers.Control | RawInputModifiers.Shift: + { + IControl? control = null; - var control = Root.GetVisualsAt(point, x => + foreach (var popup in Root.GetVisualDescendants().OfType()) { - if (x is AdornerLayer || !x.IsVisible) return false; - if (!(x is IInputElement ie)) return true; - return ie.IsHitTestVisible; - }) - .FirstOrDefault(); + if (popup.Host?.HostedVisualTreeRoot is PopupRoot popupRoot) + { + control = GetHoveredControl(popupRoot); + + if (control != null) + { + break; + } + } + } - if (control != null) - { - vm.SelectControl((IControl)control); + control ??= GetHoveredControl(Root); + + if (control != null) + { + vm.SelectControl(control); + } + + break; } - } - else if (e.Modifiers == RawInputModifiers.Alt) - { - if (e.Key == Key.S || e.Key == Key.D) + case RawInputModifiers.Alt when e.Key == Key.S || e.Key == Key.D: { - var enable = e.Key == Key.S; + vm.EnableSnapshotStyles(e.Key == Key.S); - vm.EnableSnapshotStyles(enable); + break; } } } From e8da78cd41fffb23453567dc3c391f78101a095e Mon Sep 17 00:00:00 2001 From: Max Katz Date: Mon, 5 Jul 2021 14:18:12 -0400 Subject: [PATCH 035/127] Update relative point animation --- .../Animation/Animators/RelativePointAnimator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Visuals/Animation/Animators/RelativePointAnimator.cs b/src/Avalonia.Visuals/Animation/Animators/RelativePointAnimator.cs index 40fa4503f0..348a2e4a35 100644 --- a/src/Avalonia.Visuals/Animation/Animators/RelativePointAnimator.cs +++ b/src/Avalonia.Visuals/Animation/Animators/RelativePointAnimator.cs @@ -11,7 +11,7 @@ { if (oldValue.Unit != newValue.Unit) { - return progress >= 1 ? newValue : oldValue; + return progress >= 0.5 ? newValue : oldValue; } return new RelativePoint(s_pointAnimator.Interpolate(progress, oldValue.Point, newValue.Point), oldValue.Unit); From 31d80a9fb67165417d327e008190bb0344563aae Mon Sep 17 00:00:00 2001 From: Luis von der Eltz Date: Tue, 6 Jul 2021 10:30:24 +0200 Subject: [PATCH 036/127] Make tree roots bold --- .../Diagnostics/ViewModels/TreeNode.cs | 25 ++++++------------- .../Diagnostics/Views/TreePageView.xaml | 2 +- 2 files changed, 9 insertions(+), 18 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs index 4cb470eeac..14d704db93 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs @@ -4,24 +4,28 @@ using System.Collections.Specialized; using System.Reactive; using System.Reactive.Linq; using Avalonia.Controls; +using Avalonia.Controls.Primitives; using Avalonia.LogicalTree; +using Avalonia.Media; using Avalonia.VisualTree; namespace Avalonia.Diagnostics.ViewModels { internal abstract class TreeNode : ViewModelBase, IDisposable { - private IDisposable? _classesSubscription; + private readonly IDisposable? _classesSubscription; private string _classes; private bool _isExpanded; - public TreeNode(IVisual visual, TreeNode? parent) + protected TreeNode(IVisual visual, TreeNode? parent) { Parent = parent; Type = visual.GetType().Name; Visual = visual; _classes = string.Empty; + FontWeight = Visual is TopLevel or Popup ? FontWeight.Bold : FontWeight.Normal; + if (visual is IControl control) { ElementName = control.Name; @@ -52,6 +56,8 @@ namespace Avalonia.Diagnostics.ViewModels } } + public FontWeight FontWeight { get; } + public abstract TreeNodeCollection Children { get; @@ -95,20 +101,5 @@ namespace Avalonia.Diagnostics.ViewModels _classesSubscription?.Dispose(); Children.Dispose(); } - - private static int IndexOf(IReadOnlyList collection, TreeNode item) - { - var count = collection.Count; - - for (var i = 0; i < count; ++i) - { - if (collection[i] == item) - { - return i; - } - } - - throw new AvaloniaInternalException("TreeNode was not present in parent Children collection."); - } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml index a5328716fc..bb661f7f4c 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml @@ -11,7 +11,7 @@ - + From cd56e1a9fc6e914571b2b38ab042fcc0275feaeb Mon Sep 17 00:00:00 2001 From: Luis von der Eltz Date: Tue, 6 Jul 2021 10:40:07 +0200 Subject: [PATCH 037/127] Remove "or" --- src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs index 14d704db93..f4c04dbca6 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs @@ -24,7 +24,7 @@ namespace Avalonia.Diagnostics.ViewModels Visual = visual; _classes = string.Empty; - FontWeight = Visual is TopLevel or Popup ? FontWeight.Bold : FontWeight.Normal; + FontWeight = Visual is TopLevel || Visual is Popup ? FontWeight.Bold : FontWeight.Normal; if (visual is IControl control) { From cee1a24e0330d4daedea663e858ee33d761d2227 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 7 Jul 2021 17:06:59 +0200 Subject: [PATCH 038/127] Only try to close non-owned windows on shutdown. Owned windows will be closed by their owners. --- .../ClassicDesktopStyleApplicationLifetime.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs index 2c43f13c82..2256f4cb54 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs @@ -132,8 +132,12 @@ namespace Avalonia.Controls.ApplicationLifetimes private void ShutdownRequested(object sender, CancelEventArgs e) { + // 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) - w.Close(); + if (w.Owner is null) + w.Close(); if (Windows.Count > 0) e.Cancel = true; } From 8d0a4ff5ea5ca84304891a528f481f0c410bc2a1 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Thu, 8 Jul 2021 02:31:39 -0400 Subject: [PATCH 039/127] Rename GradientBrushAnimator --- .../Animation/Animators/BaseBrushAnimator.cs | 8 ++++---- .../Animation/Animators/GradientBrushAnimator.cs | 2 +- .../Animation/Transitions/BrushTransition.cs | 12 ++++++------ 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Avalonia.Visuals/Animation/Animators/BaseBrushAnimator.cs b/src/Avalonia.Visuals/Animation/Animators/BaseBrushAnimator.cs index 38da394088..ba7a3868c7 100644 --- a/src/Avalonia.Visuals/Animation/Animators/BaseBrushAnimator.cs +++ b/src/Avalonia.Visuals/Animation/Animators/BaseBrushAnimator.cs @@ -78,21 +78,21 @@ namespace Avalonia.Animation.Animators return false; } - var gradientAnimator = new IGradientBrushAnimator(); + var gradientAnimator = new GradientBrushAnimator(); gradientAnimator.Property = Property; foreach (var keyframe in this) { if (keyframe.Value is ISolidColorBrush solidColorBrush) { - gradientAnimator.Add(new AnimatorKeyFrame(typeof(IGradientBrushAnimator), keyframe.Cue, keyframe.KeySpline) + gradientAnimator.Add(new AnimatorKeyFrame(typeof(GradientBrushAnimator), keyframe.Cue, keyframe.KeySpline) { - Value = IGradientBrushAnimator.ConvertSolidColorBrushToGradient(firstGradient, solidColorBrush) + Value = GradientBrushAnimator.ConvertSolidColorBrushToGradient(firstGradient, solidColorBrush) }); } else if (keyframe.Value is IGradientBrush) { - gradientAnimator.Add(new AnimatorKeyFrame(typeof(IGradientBrushAnimator), keyframe.Cue, keyframe.KeySpline) + gradientAnimator.Add(new AnimatorKeyFrame(typeof(GradientBrushAnimator), keyframe.Cue, keyframe.KeySpline) { Value = keyframe.Value }); diff --git a/src/Avalonia.Visuals/Animation/Animators/GradientBrushAnimator.cs b/src/Avalonia.Visuals/Animation/Animators/GradientBrushAnimator.cs index 6481f815de..864e12413f 100644 --- a/src/Avalonia.Visuals/Animation/Animators/GradientBrushAnimator.cs +++ b/src/Avalonia.Visuals/Animation/Animators/GradientBrushAnimator.cs @@ -12,7 +12,7 @@ namespace Avalonia.Animation.Animators /// /// Animator that handles values. /// - public class IGradientBrushAnimator : Animator + public class GradientBrushAnimator : Animator { private static readonly RelativePointAnimator s_relativePointAnimator = new RelativePointAnimator(); private static readonly DoubleAnimator s_doubleAnimator = new DoubleAnimator(); diff --git a/src/Avalonia.Visuals/Animation/Transitions/BrushTransition.cs b/src/Avalonia.Visuals/Animation/Transitions/BrushTransition.cs index 2229d6edc6..4d9c8af4d5 100644 --- a/src/Avalonia.Visuals/Animation/Transitions/BrushTransition.cs +++ b/src/Avalonia.Visuals/Animation/Transitions/BrushTransition.cs @@ -13,7 +13,7 @@ namespace Avalonia.Animation /// public class BrushTransition : Transition { - private static readonly IGradientBrushAnimator s_gradientAnimator = new IGradientBrushAnimator(); + private static readonly GradientBrushAnimator s_gradientAnimator = new GradientBrushAnimator(); private static readonly ISolidColorBrushAnimator s_solidColorBrushAnimator = new ISolidColorBrushAnimator(); public override IObservable DoTransition(IObservable progress, IBrush? oldValue, IBrush? newValue) @@ -27,18 +27,18 @@ namespace Avalonia.Animation { if (newValue is IGradientBrush newGradient) { - return new AnimatorTransitionObservable(s_gradientAnimator, progress, Easing, oldGradient, newGradient); + return new AnimatorTransitionObservable(s_gradientAnimator, progress, Easing, oldGradient, newGradient); } else if (newValue is ISolidColorBrush newSolidColorBrushToConvert) { - var convertedSolidColorBrush = IGradientBrushAnimator.ConvertSolidColorBrushToGradient(oldGradient, newSolidColorBrushToConvert); - return new AnimatorTransitionObservable(s_gradientAnimator, progress, Easing, oldGradient, convertedSolidColorBrush); + var convertedSolidColorBrush = GradientBrushAnimator.ConvertSolidColorBrushToGradient(oldGradient, newSolidColorBrushToConvert); + return new AnimatorTransitionObservable(s_gradientAnimator, progress, Easing, oldGradient, convertedSolidColorBrush); } } else if (newValue is IGradientBrush newGradient && oldValue is ISolidColorBrush oldSolidColorBrushToConvert) { - var convertedSolidColorBrush = IGradientBrushAnimator.ConvertSolidColorBrushToGradient(newGradient, oldSolidColorBrushToConvert); - return new AnimatorTransitionObservable(s_gradientAnimator, progress, Easing, convertedSolidColorBrush, newGradient); + var convertedSolidColorBrush = GradientBrushAnimator.ConvertSolidColorBrushToGradient(newGradient, oldSolidColorBrushToConvert); + return new AnimatorTransitionObservable(s_gradientAnimator, progress, Easing, convertedSolidColorBrush, newGradient); } if (oldValue is ISolidColorBrush oldSolidColorBrush && newValue is ISolidColorBrush newSolidColorBrush) From ac455785755a54811966dc565f3de95878cf7a12 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 8 Jul 2021 11:37:28 +0100 Subject: [PATCH 040/127] initial attempt at unit test --- .../TextBoxTests.cs | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs index 64c0020f92..0031d005e7 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs @@ -556,6 +556,37 @@ namespace Avalonia.Controls.UnitTests Assert.True(true); } } + + [Fact] + public void Textbox_Cannot_Focus_When_not_Visible() + { + using (UnitTestApplication.Start(FocusServices)) + { + var target1 = new TextBox + { + Template = CreateTemplate(), + Text = "1234", + IsVisible = true + }; + + target1.ApplyTemplate(); + + var root = new TestRoot { Child = target1 }; + + var gfcount = 0; + var lfcount = 0; + + target1.GotFocus += (s, e) => gfcount++; + + target1.Focus(); + Assert.True(target1.IsFocused); + + RaiseKeyEvent(target1, Key.Up, KeyModifiers.None); + + Assert.Equal(1, gfcount); + Assert.Equal(1, lfcount); + } + } [Fact] public void TextBox_GotFocus_And_LostFocus_Work_Properly() From 73aaefcc32d9c9853ffaf5daf88606e09e81bdb6 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 8 Jul 2021 12:29:59 +0100 Subject: [PATCH 041/127] add a failing unit test to demonstrate the issue. --- .../TextBoxTests.cs | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs index 0031d005e7..dec9daa73e 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs @@ -8,6 +8,7 @@ using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Input; using Avalonia.Input.Platform; +using Avalonia.Layout; using Avalonia.Media; using Avalonia.Platform; using Avalonia.UnitTests; @@ -558,7 +559,7 @@ namespace Avalonia.Controls.UnitTests } [Fact] - public void Textbox_Cannot_Focus_When_not_Visible() + public void Textbox_doesnt_crash_when_Receives_input_and_hidden() { using (UnitTestApplication.Start(FocusServices)) { @@ -566,25 +567,17 @@ namespace Avalonia.Controls.UnitTests { Template = CreateTemplate(), Text = "1234", - IsVisible = true + IsVisible = false }; - target1.ApplyTemplate(); - var root = new TestRoot { Child = target1 }; - var gfcount = 0; - var lfcount = 0; - - target1.GotFocus += (s, e) => gfcount++; + root.Measure(new Size(1000, 1000)); target1.Focus(); Assert.True(target1.IsFocused); RaiseKeyEvent(target1, Key.Up, KeyModifiers.None); - - Assert.Equal(1, gfcount); - Assert.Equal(1, lfcount); } } @@ -794,6 +787,9 @@ namespace Avalonia.Controls.UnitTests keyboardDevice: () => new KeyboardDevice(), keyboardNavigation: new KeyboardNavigationHandler(), inputManager: new InputManager(), + renderInterface: new MockPlatformRenderInterface(), + fontManagerImpl: new MockFontManagerImpl(), + textShaperImpl: new MockTextShaperImpl(), standardCursorFactory: Mock.Of()); private static TestServices Services => TestServices.MockThreadingInterface.With( From a295cac93050cb3020225702472a8243d892005a Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 8 Jul 2021 12:33:13 +0100 Subject: [PATCH 042/127] test more keys. --- tests/Avalonia.Controls.UnitTests/TextBoxTests.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs index dec9daa73e..3e71c38335 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs @@ -558,8 +558,12 @@ namespace Avalonia.Controls.UnitTests } } - [Fact] - public void Textbox_doesnt_crash_when_Receives_input_and_hidden() + [Theory] + [InlineData(Key.Up)] + [InlineData(Key.Down)] + [InlineData(Key.Home)] + [InlineData(Key.End)] + public void Textbox_doesnt_crash_when_Receives_input_and_hidden(Key key) { using (UnitTestApplication.Start(FocusServices)) { @@ -577,7 +581,7 @@ namespace Avalonia.Controls.UnitTests target1.Focus(); Assert.True(target1.IsFocused); - RaiseKeyEvent(target1, Key.Up, KeyModifiers.None); + RaiseKeyEvent(target1, key, KeyModifiers.None); } } From d0133ebb8cf485e54a43e0cc7aeebeb34b9db8b3 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 8 Jul 2021 12:35:09 +0100 Subject: [PATCH 043/127] check we have a presenter before allowing navigation inside textbox. --- src/Avalonia.Controls/TextBox.cs | 45 ++++++++++++++++++++------------ 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index c1516613b3..40c075c7af 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -1098,29 +1098,35 @@ namespace Avalonia.Controls private bool MoveVertical(int count) { - var formattedText = _presenter.FormattedText; - var lines = formattedText.GetLines().ToList(); - var caretIndex = CaretIndex; - var lineIndex = GetLine(caretIndex, lines) + count; - - if (lineIndex >= 0 && lineIndex < lines.Count) - { - var line = lines[lineIndex]; - var rect = formattedText.HitTestTextPosition(caretIndex); - var y = count < 0 ? rect.Y : rect.Bottom; - var point = new Point(rect.X, y + (count * (line.Height / 2))); - var hit = formattedText.HitTestPoint(point); - CaretIndex = hit.TextPosition + (hit.IsTrailing ? 1 : 0); - return true; - } - else + if (_presenter != null) { - return false; + var formattedText = _presenter.FormattedText; + var lines = formattedText.GetLines().ToList(); + var caretIndex = CaretIndex; + var lineIndex = GetLine(caretIndex, lines) + count; + + if (lineIndex >= 0 && lineIndex < lines.Count) + { + var line = lines[lineIndex]; + var rect = formattedText.HitTestTextPosition(caretIndex); + var y = count < 0 ? rect.Y : rect.Bottom; + var point = new Point(rect.X, y + (count * (line.Height / 2))); + var hit = formattedText.HitTestPoint(point); + CaretIndex = hit.TextPosition + (hit.IsTrailing ? 1 : 0); + return true; + } } + + return false; } private void MoveHome(bool document) { + if (_presenter == null) + { + return; + } + var text = Text ?? string.Empty; var caretIndex = CaretIndex; @@ -1151,6 +1157,11 @@ namespace Avalonia.Controls private void MoveEnd(bool document) { + if (_presenter == null) + { + return; + } + var text = Text ?? string.Empty; var caretIndex = CaretIndex; From b250fce59c1c51724c57046c3ed51d059adabf6b Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 8 Jul 2021 12:56:16 +0100 Subject: [PATCH 044/127] rename test to describe the actual issue. --- tests/Avalonia.Controls.UnitTests/TextBoxTests.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs index 3e71c38335..cb20071860 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs @@ -563,7 +563,7 @@ namespace Avalonia.Controls.UnitTests [InlineData(Key.Down)] [InlineData(Key.Home)] [InlineData(Key.End)] - public void Textbox_doesnt_crash_when_Receives_input_and_hidden(Key key) + public void Textbox_doesnt_crash_when_Receives_input_and_template_not_applied(Key key) { using (UnitTestApplication.Start(FocusServices)) { @@ -576,8 +576,6 @@ namespace Avalonia.Controls.UnitTests var root = new TestRoot { Child = target1 }; - root.Measure(new Size(1000, 1000)); - target1.Focus(); Assert.True(target1.IsFocused); From dc3b323dc1b0852f0d071b9fd284b5e5563e35fe Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 8 Jul 2021 12:56:41 +0100 Subject: [PATCH 045/127] refactor a little. --- src/Avalonia.Controls/TextBox.cs | 36 +++++++++++++++++--------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 40c075c7af..3aef2abac5 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -1098,23 +1098,25 @@ namespace Avalonia.Controls private bool MoveVertical(int count) { - if (_presenter != null) + if (_presenter is null) { - var formattedText = _presenter.FormattedText; - var lines = formattedText.GetLines().ToList(); - var caretIndex = CaretIndex; - var lineIndex = GetLine(caretIndex, lines) + count; + return false; + } - if (lineIndex >= 0 && lineIndex < lines.Count) - { - var line = lines[lineIndex]; - var rect = formattedText.HitTestTextPosition(caretIndex); - var y = count < 0 ? rect.Y : rect.Bottom; - var point = new Point(rect.X, y + (count * (line.Height / 2))); - var hit = formattedText.HitTestPoint(point); - CaretIndex = hit.TextPosition + (hit.IsTrailing ? 1 : 0); - return true; - } + var formattedText = _presenter.FormattedText; + var lines = formattedText.GetLines().ToList(); + var caretIndex = CaretIndex; + var lineIndex = GetLine(caretIndex, lines) + count; + + if (lineIndex >= 0 && lineIndex < lines.Count) + { + var line = lines[lineIndex]; + var rect = formattedText.HitTestTextPosition(caretIndex); + var y = count < 0 ? rect.Y : rect.Bottom; + var point = new Point(rect.X, y + (count * (line.Height / 2))); + var hit = formattedText.HitTestPoint(point); + CaretIndex = hit.TextPosition + (hit.IsTrailing ? 1 : 0); + return true; } return false; @@ -1122,7 +1124,7 @@ namespace Avalonia.Controls private void MoveHome(bool document) { - if (_presenter == null) + if (_presenter is null) { return; } @@ -1157,7 +1159,7 @@ namespace Avalonia.Controls private void MoveEnd(bool document) { - if (_presenter == null) + if (_presenter is null) { return; } From 39394bceb6988571ceacd7f4fb85e142d41b5965 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 9 Jul 2021 09:29:58 +0200 Subject: [PATCH 046/127] Don't try to shutdown if e.Cancel already set. This allows users to add their own `ShutdownRequested` handlers which can override the lifetime's handler. --- .../ClassicDesktopStyleApplicationLifetime.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs index 2256f4cb54..65f1f0fff2 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs @@ -132,6 +132,9 @@ namespace Avalonia.Controls.ApplicationLifetimes private void ShutdownRequested(object sender, CancelEventArgs 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. From a55a6ec1ff6ade0cc471f0fc4415c33a341f2a20 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Sat, 10 Jul 2021 15:05:18 +0200 Subject: [PATCH 047/127] Fix RightToLeft TextWrapping --- src/Avalonia.Visuals/Media/GlyphRun.cs | 2 +- .../TextFormatting/ShapedTextCharacters.cs | 87 +++++++++++++------ .../Media/TextFormatting/TextCharacters.cs | 9 +- .../Media/TextFormatting/TextFormatterImpl.cs | 86 ++++++++++++------ .../TextFormatting/TextFormatterTests.cs | 35 ++++++++ 5 files changed, 161 insertions(+), 58 deletions(-) diff --git a/src/Avalonia.Visuals/Media/GlyphRun.cs b/src/Avalonia.Visuals/Media/GlyphRun.cs index 2b787462e4..234122f6f5 100644 --- a/src/Avalonia.Visuals/Media/GlyphRun.cs +++ b/src/Avalonia.Visuals/Media/GlyphRun.cs @@ -582,7 +582,7 @@ namespace Avalonia.Media { var cluster = _glyphClusters[i]; - var codepointIndex = cluster - _characters.Start; + var codepointIndex = IsLeftToRight ? cluster - _characters.Start : _characters.End - cluster; var codepoint = Codepoint.ReadAt(_characters, codepointIndex, out _); diff --git a/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs b/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs index b304b19910..64befe2e5c 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs @@ -90,7 +90,9 @@ namespace Avalonia.Media.TextFormatting /// The split result. public SplitTextCharactersResult Split(int length) { - var glyphCount = GlyphRun.FindGlyphIndex(GlyphRun.Characters.Start + length); + var glyphCount = GlyphRun.IsLeftToRight ? + GlyphRun.FindGlyphIndex(GlyphRun.Characters.Start + length) : + GlyphRun.FindGlyphIndex(GlyphRun.Characters.End - length); if (GlyphRun.Characters.Length == length) { @@ -102,31 +104,64 @@ namespace Avalonia.Media.TextFormatting return new SplitTextCharactersResult(this, null); } - var firstGlyphRun = new GlyphRun( - Properties.Typeface.GlyphTypeface, - Properties.FontRenderingEmSize, - GlyphRun.GlyphIndices.Take(glyphCount), - GlyphRun.GlyphAdvances.Take(glyphCount), - GlyphRun.GlyphOffsets.Take(glyphCount), - GlyphRun.Characters.Take(length), - GlyphRun.GlyphClusters.Take(glyphCount), - GlyphRun.BiDiLevel); - - var firstTextRun = new ShapedTextCharacters(firstGlyphRun, Properties); - - var secondGlyphRun = new GlyphRun( - Properties.Typeface.GlyphTypeface, - Properties.FontRenderingEmSize, - GlyphRun.GlyphIndices.Skip(glyphCount), - GlyphRun.GlyphAdvances.Skip(glyphCount), - GlyphRun.GlyphOffsets.Skip(glyphCount), - GlyphRun.Characters.Skip(length), - GlyphRun.GlyphClusters.Skip(glyphCount), - GlyphRun.BiDiLevel); - - var secondTextRun = new ShapedTextCharacters(secondGlyphRun, Properties); - - return new SplitTextCharactersResult(firstTextRun, secondTextRun); + if (GlyphRun.IsLeftToRight) + { + var firstGlyphRun = new GlyphRun( + Properties.Typeface.GlyphTypeface, + Properties.FontRenderingEmSize, + GlyphRun.GlyphIndices.Take(glyphCount), + GlyphRun.GlyphAdvances.Take(glyphCount), + GlyphRun.GlyphOffsets.Take(glyphCount), + GlyphRun.Characters.Take(length), + GlyphRun.GlyphClusters.Take(glyphCount), + GlyphRun.BiDiLevel); + + var firstTextRun = new ShapedTextCharacters(firstGlyphRun, Properties); + + var secondGlyphRun = new GlyphRun( + Properties.Typeface.GlyphTypeface, + Properties.FontRenderingEmSize, + GlyphRun.GlyphIndices.Skip(glyphCount), + GlyphRun.GlyphAdvances.Skip(glyphCount), + GlyphRun.GlyphOffsets.Skip(glyphCount), + GlyphRun.Characters.Skip(length), + GlyphRun.GlyphClusters.Skip(glyphCount), + GlyphRun.BiDiLevel); + + var secondTextRun = new ShapedTextCharacters(secondGlyphRun, Properties); + + return new SplitTextCharactersResult(firstTextRun, secondTextRun); + } + else + { + var take = GlyphRun.GlyphIndices.Length - glyphCount; + + var firstGlyphRun = new GlyphRun( + Properties.Typeface.GlyphTypeface, + Properties.FontRenderingEmSize, + GlyphRun.GlyphIndices.Take(take), + GlyphRun.GlyphAdvances.Take(take), + GlyphRun.GlyphOffsets.Take(take), + GlyphRun.Characters.Skip(length), + GlyphRun.GlyphClusters.Take(take), + GlyphRun.BiDiLevel); + + var firstTextRun = new ShapedTextCharacters(firstGlyphRun, Properties); + + var secondGlyphRun = new GlyphRun( + Properties.Typeface.GlyphTypeface, + Properties.FontRenderingEmSize, + GlyphRun.GlyphIndices.Skip(take), + GlyphRun.GlyphAdvances.Skip(take), + GlyphRun.GlyphOffsets.Skip(take), + GlyphRun.Characters.Take(length), + GlyphRun.GlyphClusters.Skip(take), + GlyphRun.BiDiLevel); + + var secondTextRun = new ShapedTextCharacters(secondGlyphRun, Properties); + + return new SplitTextCharactersResult(secondTextRun,firstTextRun); + } } public readonly struct SplitTextCharactersResult diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs index 0779716ec8..cfca8f9ab2 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs @@ -134,7 +134,7 @@ namespace Avalonia.Media.TextFormatting var isFallback = typeface != defaultTypeface; count = 0; - var script = Script.Common; + var script = Script.Unknown; var direction = BiDiClass.LeftToRight; var font = typeface.GlyphTypeface; @@ -161,7 +161,7 @@ namespace Avalonia.Media.TextFormatting if (currentScript != script) { - if (script == Script.Inherited || script == Script.Common) + if (script is Script.Unknown) { script = currentScript; } @@ -174,13 +174,16 @@ namespace Avalonia.Media.TextFormatting } } - if (currentScript != Script.Common && currentScript != Script.Inherited) + //Only handle non whitespace here + if (!currentGrapheme.FirstCodepoint.IsWhiteSpace) { + //Stop at the first glyph that is present in the default typeface. if (isFallback && defaultFont.TryGetGlyph(currentGrapheme.FirstCodepoint, out _)) { break; } + //Stop at the first missing glyph if (!font.TryGetGlyph(currentGrapheme.FirstCodepoint, out _)) { break; diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs index 6533c34ba0..df63b00c25 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs @@ -70,34 +70,74 @@ namespace Avalonia.Media.TextFormatting { var glyphTypeface = glyphRun.GlyphTypeface; - for (var i = 0; i < glyphRun.GlyphClusters.Length; i++) + if (glyphRun.IsLeftToRight) { - var glyph = glyphRun.GlyphIndices[i]; + foreach (var glyph in glyphRun.GlyphIndices) + { + var advance = glyphTypeface.GetGlyphAdvance(glyph) * glyphRun.Scale; - var advance = glyphTypeface.GetGlyphAdvance(glyph) * glyphRun.Scale; + if (currentWidth + advance > availableWidth) + { + break; + } - if (currentWidth + advance > availableWidth) - { - break; + currentWidth += advance; + + glyphCount++; } + } + else + { + for (var index = glyphRun.GlyphClusters.Length - 1; index > 0; index--) + { + var glyph = glyphRun.GlyphIndices[index]; + + var advance = glyphTypeface.GetGlyphAdvance(glyph) * glyphRun.Scale; - currentWidth += advance; + if (currentWidth + advance > availableWidth) + { + break; + } - glyphCount++; + currentWidth += advance; + + glyphCount++; + } } } else { - foreach (var advance in glyphRun.GlyphAdvances) + if (glyphRun.IsLeftToRight) { - if (currentWidth + advance > availableWidth) + for (var index = 0; index < glyphRun.GlyphAdvances.Length; index++) { - break; + var advance = glyphRun.GlyphAdvances[index]; + + if (currentWidth + advance > availableWidth) + { + break; + } + + currentWidth += advance; + + glyphCount++; } + } + else + { + for (var index = glyphRun.GlyphAdvances.Length - 1; index > 0; index--) + { + var advance = glyphRun.GlyphAdvances[index]; + + if (currentWidth + advance > availableWidth) + { + break; + } - currentWidth += advance; + currentWidth += advance; - glyphCount++; + glyphCount++; + } } } @@ -475,24 +515,14 @@ namespace Avalonia.Media.TextFormatting var remainingCharacters = splitResult.Second; - if (currentLineBreak?.RemainingCharacters != null) + var lineBreak = remainingCharacters?.Count > 0 ? new TextLineBreak(remainingCharacters) : null; + + if (lineBreak is null && currentLineBreak.TextEndOfLine != null) { - if (remainingCharacters != null) - { - remainingCharacters.AddRange(currentLineBreak.RemainingCharacters); - } - else - { - remainingCharacters = new List(currentLineBreak.RemainingCharacters); - } + lineBreak = new TextLineBreak(currentLineBreak.TextEndOfLine); } - var lineBreak = remainingCharacters != null && remainingCharacters.Count > 0 ? - new TextLineBreak(remainingCharacters) : - null; - - return new TextLineImpl(splitResult.First, textRange, paragraphWidth, paragraphProperties, - lineBreak); + return new TextLineImpl(splitResult.First, textRange, paragraphWidth, paragraphProperties, lineBreak); } /// diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs index 9c2a1953f1..a19f97e74e 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using Avalonia.Media; using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting.Unicode; @@ -203,6 +204,40 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.Equal(expectedNumberOfLines, numberOfLines); } } + + [Fact] + public void Should_Wrap_RightToLeft() + { + using (Start()) + { + const string text = + "قطاعات الصناعة على الشبكة العالمية انترنيت ويونيكود، حيث ستتم، على الصعيدين الدولي والمحلي على حد سواء"; + + var defaultProperties = new GenericTextRunProperties(Typeface.Default); + + var textSource = new SingleBufferTextSource(text, defaultProperties); + + var formatter = new TextFormatterImpl(); + + var currentTextSourceIndex = 0; + + while (currentTextSourceIndex < text.Length) + { + var textLine = + formatter.FormatLine(textSource, currentTextSourceIndex, 50, + new GenericTextParagraphProperties(defaultProperties, textWrap: TextWrapping.Wrap)); + + var glyphClusters = textLine.TextRuns.Cast() + .SelectMany(x => x.GlyphRun.GlyphClusters).ToArray(); + + Assert.True(glyphClusters[0] >= glyphClusters[^1]); + + Assert.Equal(currentTextSourceIndex, glyphClusters[^1]); + + currentTextSourceIndex += textLine.TextRange.Length; + } + } + } [InlineData("Whether to turn off HTTPS. This option only applies if Individual, " + "IndividualB2C, SingleOrg, or MultiOrg aren't used for ‑‑auth." From 97c33f432a32b6a5f6f5bc0273e6bbd29089caf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miha=20Marki=C4=8D?= Date: Sat, 10 Jul 2021 15:46:08 +0200 Subject: [PATCH 048/127] Applies spell checking on mostly comments, but also non public members and arguments --- src/Avalonia.Base/AvaloniaObject.cs | 2 +- src/Avalonia.Base/AvaloniaProperty.cs | 4 ++-- .../AvaloniaPropertyChangedEventArgs.cs | 4 ++-- .../Collections/Pooled/PooledList.cs | 2 +- .../Data/Core/Plugins/IDataValidationPlugin.cs | 2 +- src/Avalonia.Base/DirectPropertyBase.cs | 6 +++--- src/Avalonia.Base/DirectPropertyMetadata`1.cs | 2 +- .../Collections/DataGridCollectionView.cs | 8 ++++---- src/Avalonia.Controls.DataGrid/DataGrid.cs | 2 +- src/Avalonia.Controls.DataGrid/DataGridCell.cs | 2 +- .../DataGridCellCoordinates.cs | 2 +- .../DataGridClipboard.cs | 2 +- .../DataGridColumn.cs | 2 +- .../DataGridDisplayData.cs | 8 ++++---- src/Avalonia.Controls.DataGrid/DataGridRow.cs | 6 +++--- src/Avalonia.Controls.DataGrid/DataGridRows.cs | 18 +++++++++--------- .../DataGridColumnHeadersPresenter.cs | 2 +- src/Avalonia.Controls/AppBuilderBase.cs | 2 +- .../Converters/MarginMultiplierConverter.cs | 10 +++++----- .../MenuScrollingVisibilityConverter.cs | 4 ++-- .../DateTimePickers/DatePicker.cs | 2 +- .../DateTimePickers/DateTimePickerPanel.cs | 12 ++++++------ src/Avalonia.Controls/DefinitionBase.cs | 8 ++++---- src/Avalonia.Controls/Flyouts/FlyoutBase.cs | 2 +- src/Avalonia.Controls/Grid.cs | 18 +++++++++--------- src/Avalonia.Controls/ListBox.cs | 2 +- .../NativeMenuItemSeparator.cs | 2 +- .../Platform/ExtendClientAreaChromeHints.cs | 2 +- .../PopupPositioning/ManagedPopupPositioner.cs | 2 +- src/Avalonia.Controls/Primitives/RangeBase.cs | 2 +- .../Primitives/SelectingItemsControl.cs | 2 +- .../Repeater/IElementFactory.cs | 2 +- .../Repeater/ItemsRepeater.cs | 4 ++-- src/Avalonia.Controls/Repeater/ViewManager.cs | 2 +- .../Repeater/ViewportManager.cs | 2 +- src/Avalonia.Controls/ScrollViewer.cs | 2 +- .../Selection/SelectionModel.cs | 2 +- src/Avalonia.Controls/TickBar.cs | 8 ++++---- src/Avalonia.Controls/TopLevel.cs | 2 +- src/Avalonia.Controls/WindowBase.cs | 2 +- src/Avalonia.Input/Cursor.cs | 2 +- .../ScrollGestureRecognizer.cs | 2 +- src/Avalonia.Input/ICommandSource.cs | 2 +- .../TextInput/ITextInputMethodClient.cs | 4 ++-- src/Avalonia.Interactivity/Interactive.cs | 2 +- src/Avalonia.Layout/ElementManager.cs | 2 +- src/Avalonia.Layout/FlowLayoutAlgorithm.cs | 4 ++-- .../AvaloniaNativeMenuExporter.cs | 4 ++-- src/Avalonia.Native/IAvnMenu.cs | 8 ++++---- src/Avalonia.Native/IAvnMenuItem.cs | 6 +++--- .../Unicode/LineBreakEnumerator.cs | 2 +- .../Transformation/InterpolationUtilities.cs | 10 +++++----- 52 files changed, 109 insertions(+), 109 deletions(-) diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 6a9cff6b71..ce5b37043f 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -861,7 +861,7 @@ namespace Avalonia } /// - /// Logs a mesage if the notification represents a binding error. + /// Logs a message if the notification represents a binding error. /// /// The property being bound. /// The binding notification. diff --git a/src/Avalonia.Base/AvaloniaProperty.cs b/src/Avalonia.Base/AvaloniaProperty.cs index 5117fdb170..94aefb8869 100644 --- a/src/Avalonia.Base/AvaloniaProperty.cs +++ b/src/Avalonia.Base/AvaloniaProperty.cs @@ -465,9 +465,9 @@ namespace Avalonia /// Uses the visitor pattern to resolve an untyped property to a typed property. /// /// The type of user data passed. - /// The visitor which will accept the typed property. + /// The visitor which will accept the typed property. /// The user data to pass. - public abstract void Accept(IAvaloniaPropertyVisitor vistor, ref TData data) + public abstract void Accept(IAvaloniaPropertyVisitor visitor, ref TData data) where TData : struct; /// diff --git a/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs b/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs index c1a2832fde..896d86e29d 100644 --- a/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs +++ b/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs @@ -58,8 +58,8 @@ namespace Avalonia /// /// This will usually be true, except in /// - /// which recieves notifications for all changes to property values, whether a value with a higher - /// priority is present or not. When this property is false, the change that is being signalled + /// which receives notifications for all changes to property values, whether a value with a higher + /// priority is present or not. When this property is false, the change that is being signaled /// has not resulted in a change to the property value on the object. /// public bool IsEffectiveValueChange { get; private set; } diff --git a/src/Avalonia.Base/Collections/Pooled/PooledList.cs b/src/Avalonia.Base/Collections/Pooled/PooledList.cs index e50e100d32..2cd9758f12 100644 --- a/src/Avalonia.Base/Collections/Pooled/PooledList.cs +++ b/src/Avalonia.Base/Collections/Pooled/PooledList.cs @@ -1271,7 +1271,7 @@ namespace Avalonia.Collections.Pooled /// Reverses the elements in a range of this list. Following a call to this /// method, an element in the range given by index and count /// which was previously located at index i will now be located at - /// index index + (index + count - i - 1). + /// index + (index + count - i - 1). /// public void Reverse(int index, int count) { diff --git a/src/Avalonia.Base/Data/Core/Plugins/IDataValidationPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/IDataValidationPlugin.cs index 324279e9f0..2a580fe75f 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/IDataValidationPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/IDataValidationPlugin.cs @@ -20,7 +20,7 @@ namespace Avalonia.Data.Core.Plugins /// /// A weak reference to the object. /// The property name. - /// The inner property accessor used to aceess the property. + /// The inner property accessor used to access the property. /// /// An interface through which future interactions with the /// property will be made. diff --git a/src/Avalonia.Base/DirectPropertyBase.cs b/src/Avalonia.Base/DirectPropertyBase.cs index e6cc1edfdf..a057ad2254 100644 --- a/src/Avalonia.Base/DirectPropertyBase.cs +++ b/src/Avalonia.Base/DirectPropertyBase.cs @@ -13,7 +13,7 @@ namespace Avalonia /// The type of the property's value. /// /// Whereas is typed on the owner type, this base - /// class provides a non-owner-typed interface to a direct poperty. + /// class provides a non-owner-typed interface to a direct property. /// public abstract class DirectPropertyBase : AvaloniaProperty { @@ -123,9 +123,9 @@ namespace Avalonia } /// - public override void Accept(IAvaloniaPropertyVisitor vistor, ref TData data) + public override void Accept(IAvaloniaPropertyVisitor visitor, ref TData data) { - vistor.Visit(this, ref data); + visitor.Visit(this, ref data); } /// diff --git a/src/Avalonia.Base/DirectPropertyMetadata`1.cs b/src/Avalonia.Base/DirectPropertyMetadata`1.cs index 205967984d..eabdef05ed 100644 --- a/src/Avalonia.Base/DirectPropertyMetadata`1.cs +++ b/src/Avalonia.Base/DirectPropertyMetadata`1.cs @@ -38,7 +38,7 @@ namespace Avalonia /// /// Data validation is validation performed at the target of a binding, for example in a /// view model using the INotifyDataErrorInfo interface. Only certain properties on a - /// control (such as a TextBox's Text property) will be interested in recieving data + /// control (such as a TextBox's Text property) will be interested in receiving data /// validation messages so this feature must be explicitly enabled by setting this flag. /// public bool? EnableDataValidation { get; private set; } diff --git a/src/Avalonia.Controls.DataGrid/Collections/DataGridCollectionView.cs b/src/Avalonia.Controls.DataGrid/Collections/DataGridCollectionView.cs index 5b1e43b8f4..fe6acdc532 100644 --- a/src/Avalonia.Controls.DataGrid/Collections/DataGridCollectionView.cs +++ b/src/Avalonia.Controls.DataGrid/Collections/DataGridCollectionView.cs @@ -877,7 +877,7 @@ namespace Avalonia.Collections if (!CheckFlag(CollectionViewFlags.IsMoveToPageDeferred)) { // if the temporaryGroup was not created yet and is out of sync - // then create it so that we can use it as a refernce while paging. + // then create it so that we can use it as a reference while paging. if (IsGrouping && _temporaryGroup.ItemCount != InternalList.Count) { PrepareTemporaryGroups(); @@ -889,7 +889,7 @@ namespace Avalonia.Collections else if (IsGrouping) { // if the temporaryGroup was not created yet and is out of sync - // then create it so that we can use it as a refernce while paging. + // then create it so that we can use it as a reference while paging. if (_temporaryGroup.ItemCount != InternalList.Count) { // update the groups that get created for the @@ -1951,7 +1951,7 @@ namespace Avalonia.Collections EnsureCollectionInSync(); VerifyRefreshNotDeferred(); - // for indicies larger than the count + // for indices larger than the count if (index >= Count || index < 0) { throw new ArgumentOutOfRangeException("index"); @@ -3800,7 +3800,7 @@ namespace Avalonia.Collections /// /// /// This method can be called from a constructor - it does not call - /// any virtuals. The 'count' parameter is substitute for the real Count, + /// any virtuals. The 'count' parameter is substitute for the real Count, /// used only when newItem is null. /// In that case, this method sets IsCurrentAfterLast to true if and only /// if newPosition >= count. This distinguishes between a null belonging diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index 83f13fe199..21585bd9fc 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -5731,7 +5731,7 @@ namespace Avalonia.Controls { if (SelectionMode == DataGridSelectionMode.Single || !ctrl) { - // Unselect the currectly selected rows except the new selected row + // Unselect the currently selected rows except the new selected row action = DataGridSelectionAction.SelectCurrent; } else diff --git a/src/Avalonia.Controls.DataGrid/DataGridCell.cs b/src/Avalonia.Controls.DataGrid/DataGridCell.cs index 445dc541a7..0de4612958 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridCell.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridCell.cs @@ -197,7 +197,7 @@ namespace Avalonia.Controls } // Makes sure the right gridline has the proper stroke and visibility. If lastVisibleColumn is specified, the - // right gridline will be collapsed if this cell belongs to the lastVisibileColumn and there is no filler column + // right gridline will be collapsed if this cell belongs to the lastVisibleColumn and there is no filler column internal void EnsureGridLine(DataGridColumn lastVisibleColumn) { if (OwningGrid != null && _rightGridLine != null) diff --git a/src/Avalonia.Controls.DataGrid/DataGridCellCoordinates.cs b/src/Avalonia.Controls.DataGrid/DataGridCellCoordinates.cs index 2f723154be..26f0b53952 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridCellCoordinates.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridCellCoordinates.cs @@ -40,7 +40,7 @@ namespace Avalonia.Controls return false; } - // There is build warning if this is missiing + // There is build warning if this is missing public override int GetHashCode() { return base.GetHashCode(); diff --git a/src/Avalonia.Controls.DataGrid/DataGridClipboard.cs b/src/Avalonia.Controls.DataGrid/DataGridClipboard.cs index a4bab8b304..ee69f1c768 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridClipboard.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridClipboard.cs @@ -189,7 +189,7 @@ namespace Avalonia.Controls } /// - /// DataGrid row item used for proparing the ClipboardRowContent. + /// DataGrid row item used for preparing the ClipboardRowContent. /// public object Item { diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridColumn.cs index 9141fb2463..0849f48686 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridColumn.cs @@ -787,7 +787,7 @@ namespace Avalonia.Controls } /// - /// If the DataGrid is using using layout rounding, the pixel snapping will force all widths to + /// If the DataGrid is using layout rounding, the pixel snapping will force all widths to /// whole numbers. Since the column widths aren't visual elements, they don't go through the normal /// rounding process, so we need to do it ourselves. If we don't, then we'll end up with some /// pixel gaps and/or overlaps between columns. diff --git a/src/Avalonia.Controls.DataGrid/DataGridDisplayData.cs b/src/Avalonia.Controls.DataGrid/DataGridDisplayData.cs index e659438b43..2b8055dd22 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridDisplayData.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridDisplayData.cs @@ -79,7 +79,7 @@ namespace Avalonia.Controls set; } - internal void AddRecylableRow(DataGridRow row) + internal void AddRecyclableRow(DataGridRow row) { Debug.Assert(!_recyclableRows.Contains(row)); row.DetachFromDataGrid(true); @@ -120,7 +120,7 @@ namespace Avalonia.Controls { if (row.IsRecyclable) { - AddRecylableRow(row); + AddRecyclableRow(row); } else { @@ -193,7 +193,7 @@ namespace Avalonia.Controls internal void FullyRecycleElements() { - // Fully recycle Recycleable rows and transfer them to Recycled rows + // Fully recycle Recyclable rows and transfer them to Recycled rows while (_recyclableRows.Count > 0) { DataGridRow row = _recyclableRows.Pop(); @@ -202,7 +202,7 @@ namespace Avalonia.Controls Debug.Assert(!_fullyRecycledRows.Contains(row)); _fullyRecycledRows.Push(row); } - // Fully recycle Recycleable GroupHeaders and transfer them to Recycled GroupHeaders + // Fully recycle Recyclable GroupHeaders and transfer them to Recycled GroupHeaders while (_recyclableGroupHeaders.Count > 0) { DataGridRowGroupHeader groupHeader = _recyclableGroupHeaders.Pop(); diff --git a/src/Avalonia.Controls.DataGrid/DataGridRow.cs b/src/Avalonia.Controls.DataGrid/DataGridRow.cs index c3562c53a4..7546970498 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRow.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRow.cs @@ -392,7 +392,7 @@ namespace Avalonia.Controls set; } - // Height that the row will eventually end up at after a possible detalis animation has completed + // Height that the row will eventually end up at after a possible details animation has completed internal double TargetHeight { get @@ -517,7 +517,7 @@ namespace Avalonia.Controls return base.MeasureOverride(availableSize); } - //Allow the DataGrid specific componets to adjust themselves based on new values + //Allow the DataGrid specific components to adjust themselves based on new values if (_headerElement != null) { _headerElement.InvalidateMeasure(); @@ -722,7 +722,7 @@ namespace Avalonia.Controls if (_bottomGridLine != null) { // It looks like setting Visibility sometimes has side effects so make sure the value is actually - // diffferent before setting it + // different before setting it bool newVisibility = OwningGrid.GridLinesVisibility == DataGridGridLinesVisibility.Horizontal || OwningGrid.GridLinesVisibility == DataGridGridLinesVisibility.All; if (newVisibility != _bottomGridLine.IsVisible) diff --git a/src/Avalonia.Controls.DataGrid/DataGridRows.cs b/src/Avalonia.Controls.DataGrid/DataGridRows.cs index 4bfbd7d818..1d5c899993 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRows.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRows.cs @@ -1193,7 +1193,7 @@ namespace Avalonia.Controls else { groupHeader = element as DataGridRowGroupHeader; - Debug.Assert(groupHeader != null); // Nothig other and Rows and RowGroups now + Debug.Assert(groupHeader != null); // Nothing other and Rows and RowGroups now if (groupHeader != null) { groupHeader.TotalIndent = (groupHeader.Level == 0) ? 0 : RowGroupSublevelIndents[groupHeader.Level - 1]; @@ -1636,7 +1636,7 @@ namespace Avalonia.Controls if (slot >= DisplayData.FirstScrollingSlot && slot <= DisplayData.LastScrollingSlot) { - // Additional row takes the spot of a displayed row - it is necessarilly displayed + // Additional row takes the spot of a displayed row - it is necessarily displayed return true; } else if (DisplayData.FirstScrollingSlot == -1 && @@ -1825,7 +1825,7 @@ namespace Avalonia.Controls if (MathUtilities.LessThan(firstRowHeight, NegVerticalOffset)) { // We've scrolled off more of the first row than what's possible. This can happen - // if the first row got shorter (Ex: Collpasing RowDetails) or if the user has a recycling + // if the first row got shorter (Ex: Collapsing RowDetails) or if the user has a recycling // cleanup issue. In this case, simply try to display the next row as the first row instead if (newFirstScrollingSlot < SlotCount - 1) { @@ -2014,7 +2014,7 @@ namespace Avalonia.Controls if (recycleRow) { - DisplayData.AddRecylableRow(dataGridRow); + DisplayData.AddRecyclableRow(dataGridRow); } else { @@ -2265,7 +2265,7 @@ namespace Avalonia.Controls if (parentGroupInfo.LastSubItemSlot - parentGroupInfo.Slot == 1) { // We just added the first item to a RowGroup so the header should transition from Empty to either Expanded or Collapsed - EnsureAnscestorsExpanderButtonChecked(parentGroupInfo); + EnsureAncestorsExpanderButtonChecked(parentGroupInfo); } } } @@ -2407,7 +2407,7 @@ namespace Avalonia.Controls return treeCount; } - private void EnsureAnscestorsExpanderButtonChecked(DataGridRowGroupInfo parentGroupInfo) + private void EnsureAncestorsExpanderButtonChecked(DataGridRowGroupInfo parentGroupInfo) { if (IsSlotVisible(parentGroupInfo.Slot)) { @@ -2789,11 +2789,11 @@ namespace Avalonia.Controls return null; } - internal void OnRowGroupHeaderToggled(DataGridRowGroupHeader groupHeader, bool newIsVisibile, bool setCurrent) + internal void OnRowGroupHeaderToggled(DataGridRowGroupHeader groupHeader, bool newIsVisible, bool setCurrent) { Debug.Assert(groupHeader.RowGroupInfo.CollectionViewGroup.ItemCount > 0); - if (WaitForLostFocus(delegate { OnRowGroupHeaderToggled(groupHeader, newIsVisibile, setCurrent); }) || !CommitEdit()) + if (WaitForLostFocus(delegate { OnRowGroupHeaderToggled(groupHeader, newIsVisible, setCurrent); }) || !CommitEdit()) { return; } @@ -2804,7 +2804,7 @@ namespace Avalonia.Controls UpdateSelectionAndCurrency(CurrentColumnIndex, groupHeader.RowGroupInfo.Slot, DataGridSelectionAction.SelectCurrent, scrollIntoView: false); } - UpdateRowGroupVisibility(groupHeader.RowGroupInfo, newIsVisibile, isDisplayed: true); + UpdateRowGroupVisibility(groupHeader.RowGroupInfo, newIsVisible, isDisplayed: true); ComputeScrollBarsLayout(); // We need force arrange since our Scrollings Rows could update without automatically triggering layout diff --git a/src/Avalonia.Controls.DataGrid/Primitives/DataGridColumnHeadersPresenter.cs b/src/Avalonia.Controls.DataGrid/Primitives/DataGridColumnHeadersPresenter.cs index 1c350a4f14..4eed119240 100644 --- a/src/Avalonia.Controls.DataGrid/Primitives/DataGridColumnHeadersPresenter.cs +++ b/src/Avalonia.Controls.DataGrid/Primitives/DataGridColumnHeadersPresenter.cs @@ -140,7 +140,7 @@ namespace Avalonia.Controls.Primitives if (dataGridColumn.IsFrozen) { columnHeader.Arrange(new Rect(frozenLeftEdge, 0, dataGridColumn.LayoutRoundedWidth, finalSize.Height)); - columnHeader.Clip = null; // The layout system could have clipped this becaues it's not aware of our render transform + columnHeader.Clip = null; // The layout system could have clipped this because it's not aware of our render transform if (DragColumn == dataGridColumn && DragIndicator != null) { dragIndicatorLeftEdge = frozenLeftEdge + DragIndicatorOffset; diff --git a/src/Avalonia.Controls/AppBuilderBase.cs b/src/Avalonia.Controls/AppBuilderBase.cs index f616a42cac..d44b2ab0db 100644 --- a/src/Avalonia.Controls/AppBuilderBase.cs +++ b/src/Avalonia.Controls/AppBuilderBase.cs @@ -273,7 +273,7 @@ namespace Avalonia.Controls } /// - /// Sets up the platform-speciic services for the . + /// Sets up the platform-specific services for the . /// private void Setup() { diff --git a/src/Avalonia.Controls/Converters/MarginMultiplierConverter.cs b/src/Avalonia.Controls/Converters/MarginMultiplierConverter.cs index 9f3a6da9da..46affcbe33 100644 --- a/src/Avalonia.Controls/Converters/MarginMultiplierConverter.cs +++ b/src/Avalonia.Controls/Converters/MarginMultiplierConverter.cs @@ -26,13 +26,13 @@ namespace Avalonia.Controls.Converters Right ? Indent * scalarDepth : 0, Bottom ? Indent * scalarDepth : 0); } - else if (value is Thickness thinknessDepth) + else if (value is Thickness thicknessDepth) { return new Thickness( - Left ? Indent * thinknessDepth.Left : 0, - Top ? Indent * thinknessDepth.Top : 0, - Right ? Indent * thinknessDepth.Right : 0, - Bottom ? Indent * thinknessDepth.Bottom : 0); + Left ? Indent * thicknessDepth.Left : 0, + Top ? Indent * thicknessDepth.Top : 0, + Right ? Indent * thicknessDepth.Right : 0, + Bottom ? Indent * thicknessDepth.Bottom : 0); } return new Thickness(0); diff --git a/src/Avalonia.Controls/Converters/MenuScrollingVisibilityConverter.cs b/src/Avalonia.Controls/Converters/MenuScrollingVisibilityConverter.cs index e6420fe342..65f95808ff 100644 --- a/src/Avalonia.Controls/Converters/MenuScrollingVisibilityConverter.cs +++ b/src/Avalonia.Controls/Converters/MenuScrollingVisibilityConverter.cs @@ -16,7 +16,7 @@ namespace Avalonia.Controls.Converters if (parameter == null || values == null || values.Count != 4 || - !(values[0] is ScrollBarVisibility visiblity) || + !(values[0] is ScrollBarVisibility visibility) || !(values[1] is double offset) || !(values[2] is double extent) || !(values[3] is double viewport)) @@ -24,7 +24,7 @@ namespace Avalonia.Controls.Converters return AvaloniaProperty.UnsetValue; } - if (visiblity == ScrollBarVisibility.Auto) + if (visibility == ScrollBarVisibility.Auto) { if (extent == viewport) { diff --git a/src/Avalonia.Controls/DateTimePickers/DatePicker.cs b/src/Avalonia.Controls/DateTimePickers/DatePicker.cs index 43bc7d1df9..5893a02b04 100644 --- a/src/Avalonia.Controls/DateTimePickers/DatePicker.cs +++ b/src/Avalonia.Controls/DateTimePickers/DatePicker.cs @@ -71,7 +71,7 @@ namespace Avalonia.Controls x => x.MonthVisible, (x, v) => x.MonthVisible = v); /// - /// Defiens the Property + /// Defines the Property /// public static readonly DirectProperty YearFormatProperty = AvaloniaProperty.RegisterDirect(nameof(YearFormat), diff --git a/src/Avalonia.Controls/DateTimePickers/DateTimePickerPanel.cs b/src/Avalonia.Controls/DateTimePickers/DateTimePickerPanel.cs index a0e8c03195..e16e609a15 100644 --- a/src/Avalonia.Controls/DateTimePickers/DateTimePickerPanel.cs +++ b/src/Avalonia.Controls/DateTimePickers/DateTimePickerPanel.cs @@ -220,15 +220,15 @@ namespace Avalonia.Controls.Primitives if (dy > 0) // Scroll Down { - int numContsToMove = 0; + int numCountsToMove = 0; for (int i = 0; i < children.Count; i++) { if (children[i].Bounds.Bottom - dy < 0) - numContsToMove++; + numCountsToMove++; else break; } - children.MoveRange(0, numContsToMove, children.Count); + children.MoveRange(0, numCountsToMove, children.Count); var scrollHeight = _extent.Height - Viewport.Height; if (ShouldLoop && value.Y >= scrollHeight - _extentOne) @@ -236,15 +236,15 @@ namespace Avalonia.Controls.Primitives } else if (dy < 0) // Scroll Up { - int numContsToMove = 0; + int numCountsToMove = 0; for (int i = children.Count - 1; i >= 0; i--) { if (children[i].Bounds.Top - dy > Bounds.Height) - numContsToMove++; + numCountsToMove++; else break; } - children.MoveRange(children.Count - numContsToMove, numContsToMove, 0); + children.MoveRange(children.Count - numCountsToMove, numCountsToMove, 0); if (ShouldLoop && value.Y < _extentOne) _offset = new Vector(0, value.Y + (_extentOne * 50)); } diff --git a/src/Avalonia.Controls/DefinitionBase.cs b/src/Avalonia.Controls/DefinitionBase.cs index 3237f6f37b..37b8691ce9 100644 --- a/src/Avalonia.Controls/DefinitionBase.cs +++ b/src/Avalonia.Controls/DefinitionBase.cs @@ -35,7 +35,7 @@ namespace Avalonia.Controls if (_sharedState == null) { // start with getting SharedSizeGroup value. - // this property is NOT inhereted which should result in better overall perf. + // this property is NOT inherited which should result in better overall perf. string sharedSizeGroupId = SharedSizeGroup; if (sharedSizeGroupId != null) { @@ -52,7 +52,7 @@ namespace Avalonia.Controls } /// - /// Callback to notify about exitting model tree. + /// Callback to notify about exiting model tree. /// internal void OnExitParentTree() { @@ -458,7 +458,7 @@ namespace Avalonia.Controls private Grid.LayoutTimeSizeType _sizeType; // layout-time user size type. it may differ from _userSizeValueCache.UnitType when calculating "to-content" private double _minSize; // used during measure to accumulate size for "Auto" and "Star" DefinitionBase's - private double _measureSize; // size, calculated to be the input contstraint size for Child.Measure + private double _measureSize; // size, calculated to be the input constraint size for Child.Measure private double _sizeCache; // cache used for various purposes (sorting, caching, etc) during calculations private double _offset; // offset of the DefinitionBase from left / top corner (assuming LTR case) @@ -556,7 +556,7 @@ namespace Avalonia.Controls } /// - /// Propogates invalidations for all registered definitions. + /// Propagates invalidations for all registered definitions. /// Resets its own state. /// internal void Invalidate() diff --git a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs index e4b68c62fd..b9da1138d1 100644 --- a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs +++ b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs @@ -485,7 +485,7 @@ namespace Avalonia.Controls.Primitives internal static void SetPresenterClasses(IControl presenter, Classes classes) { - //Remove any classes no longer in use, ignoring pseudoclasses + //Remove any classes no longer in use, ignoring pseudo classes for (int i = presenter.Classes.Count - 1; i >= 0; i--) { if (!classes.Contains(presenter.Classes[i]) && diff --git a/src/Avalonia.Controls/Grid.cs b/src/Avalonia.Controls/Grid.cs index a14df1eb43..4e60b52f83 100644 --- a/src/Avalonia.Controls/Grid.cs +++ b/src/Avalonia.Controls/Grid.cs @@ -330,7 +330,7 @@ namespace Avalonia.Controls // value of Auto column), "cell 2 1" needs to be calculated first, // as it contributes to the Auto column's calculated value. // At the same time in order to accurately calculate constraint - // height for "cell 2 1", "cell 1 2" needs to be calcualted first, + // height for "cell 2 1", "cell 1 2" needs to be calculated first, // as it contributes to Auto row height, which is used in the // computation of Star row resolved height. // @@ -405,11 +405,11 @@ namespace Avalonia.Controls // // where: // * all [Measure GroupN] - regular children measure process - - // each cell is measured given contraint size as an input + // each cell is measured given constraint size as an input // and each cell's desired size is accumulated on the // corresponding column / row; // * [Measure Group2'] - is when each cell is measured with - // infinit height as a constraint and a cell's desired + // infinite height as a constraint and a cell's desired // height is ignored; // * [Measure Groups''] - is when each cell is measured (second // time during single Grid.MeasureOverride) regularly but its @@ -780,7 +780,7 @@ namespace Avalonia.Controls } /// - /// Initializes DefinitionsU memeber either to user supplied ColumnDefinitions collection + /// Initializes DefinitionsU member either to user supplied ColumnDefinitions collection /// or to a default single element collection. DefinitionsU gets trimmed to size. /// /// @@ -821,7 +821,7 @@ namespace Avalonia.Controls } /// - /// Initializes DefinitionsV memeber either to user supplied RowDefinitions collection + /// Initializes DefinitionsV member either to user supplied RowDefinitions collection /// or to a default single element collection. DefinitionsV gets trimmed to size. /// /// @@ -2132,7 +2132,7 @@ namespace Avalonia.Controls // // Fortunately, our scenarios tend to have a small number of columns (~10 or fewer) // each being allocated a large number of pixels (~50 or greater), and - // people don't even notice the kind of 1-pixel anomolies that are + // people don't even notice the kind of 1-pixel anomalies that are // theoretically inevitable, or don't care if they do. At least they shouldn't // care - no one should be using the results WPF's grid layout to make // quantitative decisions; its job is to produce a reasonable display, not @@ -2597,7 +2597,7 @@ namespace Avalonia.Controls if (scale < 0.0) { // if one of the *-weights is Infinity, adjust the weights by mapping - // Infinty to 1.0 and everything else to 0.0: the infinite items share the + // Infinity to 1.0 and everything else to 0.0: the infinite items share the // available space equally, everyone else gets nothing. return (Double.IsPositiveInfinity(def.UserSize.Value)) ? 1.0 : 0.0; } @@ -2655,7 +2655,7 @@ namespace Avalonia.Controls private enum Flags { // - // the foolowing flags let grid tracking dirtiness in more granular manner: + // the following flags let grid tracking dirtiness in more granular manner: // * Valid???Structure flags indicate that elements were added or removed. // * Valid???Layout flags indicate that layout time portion of the information // stored on the objects should be updated. @@ -2684,7 +2684,7 @@ namespace Avalonia.Controls /// /// ShowGridLines property. This property is used mostly - /// for simplification of visual debuggig. When it is set + /// for simplification of visual debugging. When it is set /// to true grid lines are drawn to visualize location /// of grid lines. /// diff --git a/src/Avalonia.Controls/ListBox.cs b/src/Avalonia.Controls/ListBox.cs index f4185650bb..43b4908482 100644 --- a/src/Avalonia.Controls/ListBox.cs +++ b/src/Avalonia.Controls/ListBox.cs @@ -89,7 +89,7 @@ namespace Avalonia.Controls /// /// /// Note that the selection mode only applies to selections made via user interaction. - /// Multiple selections can be made programatically regardless of the value of this property. + /// Multiple selections can be made programmatically regardless of the value of this property. /// public new SelectionMode SelectionMode { diff --git a/src/Avalonia.Controls/NativeMenuItemSeparator.cs b/src/Avalonia.Controls/NativeMenuItemSeparator.cs index d3d3721c89..49b36e714d 100644 --- a/src/Avalonia.Controls/NativeMenuItemSeparator.cs +++ b/src/Avalonia.Controls/NativeMenuItemSeparator.cs @@ -3,7 +3,7 @@ namespace Avalonia.Controls { - [Obsolete("This class exists to maintain backwards compatiblity with existing code. Use NativeMenuItemSeparator instead")] + [Obsolete("This class exists to maintain backwards compatibility with existing code. Use NativeMenuItemSeparator instead")] public class NativeMenuItemSeperator : NativeMenuItemSeparator { } diff --git a/src/Avalonia.Controls/Platform/ExtendClientAreaChromeHints.cs b/src/Avalonia.Controls/Platform/ExtendClientAreaChromeHints.cs index bb3c0288eb..8513dd1697 100644 --- a/src/Avalonia.Controls/Platform/ExtendClientAreaChromeHints.cs +++ b/src/Avalonia.Controls/Platform/ExtendClientAreaChromeHints.cs @@ -25,7 +25,7 @@ namespace Avalonia.Platform /// /// Use system chrome where possible. OSX system chrome is used, Windows managed chrome is used. - /// This is because Windows Chrome can not be shown ontop of user content. + /// This is because Windows Chrome can not be shown on top of user content. /// PreferSystemChrome = 0x02, diff --git a/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs b/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs index dd839a0e9b..0f0dd7311d 100644 --- a/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs +++ b/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs @@ -27,7 +27,7 @@ namespace Avalonia.Controls.Primitives.PopupPositioning /// /// An implementation for platforms on which a popup can be - /// aritrarily positioned. + /// arbitrarily positioned. /// public class ManagedPopupPositioner : IPopupPositioner { diff --git a/src/Avalonia.Controls/Primitives/RangeBase.cs b/src/Avalonia.Controls/Primitives/RangeBase.cs index 0b716ec1ca..acb8e0f006 100644 --- a/src/Avalonia.Controls/Primitives/RangeBase.cs +++ b/src/Avalonia.Controls/Primitives/RangeBase.cs @@ -170,7 +170,7 @@ namespace Avalonia.Controls.Primitives } /// - /// Checks if the double value is not inifinity nor NaN. + /// Checks if the double value is not infinity nor NaN. /// /// The value. private static bool ValidateDouble(double value) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 34d3347434..402c34e3e5 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -310,7 +310,7 @@ namespace Avalonia.Controls.Primitives /// /// /// Note that the selection mode only applies to selections made via user interaction. - /// Multiple selections can be made programatically regardless of the value of this property. + /// Multiple selections can be made programmatically regardless of the value of this property. /// protected SelectionMode SelectionMode { diff --git a/src/Avalonia.Controls/Repeater/IElementFactory.cs b/src/Avalonia.Controls/Repeater/IElementFactory.cs index 6a899a6f26..f424ae29b7 100644 --- a/src/Avalonia.Controls/Repeater/IElementFactory.cs +++ b/src/Avalonia.Controls/Repeater/IElementFactory.cs @@ -46,7 +46,7 @@ namespace Avalonia.Controls } /// - /// A data template that supports creating and recyling elements for an . + /// A data template that supports creating and recycling elements for an . /// public interface IElementFactory : IDataTemplate { diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs index fb2da09e73..01200e87e3 100644 --- a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs +++ b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs @@ -441,9 +441,9 @@ namespace Avalonia.Controls base.OnPropertyChanged(change); } - internal IControl GetElementImpl(int index, bool forceCreate, bool supressAutoRecycle) + internal IControl GetElementImpl(int index, bool forceCreate, bool suppressAutoRecycle) { - var element = _viewManager.GetElement(index, forceCreate, supressAutoRecycle); + var element = _viewManager.GetElement(index, forceCreate, suppressAutoRecycle); return element; } diff --git a/src/Avalonia.Controls/Repeater/ViewManager.cs b/src/Avalonia.Controls/Repeater/ViewManager.cs index cf2066b373..a7b6cf7f18 100644 --- a/src/Avalonia.Controls/Repeater/ViewManager.cs +++ b/src/Avalonia.Controls/Repeater/ViewManager.cs @@ -174,7 +174,7 @@ namespace Avalonia.Controls } else { - // We could not find a candiate. + // We could not find a candidate. _lastFocusedElement = null; } } diff --git a/src/Avalonia.Controls/Repeater/ViewportManager.cs b/src/Avalonia.Controls/Repeater/ViewportManager.cs index da3c2b15e6..1a90da5830 100644 --- a/src/Avalonia.Controls/Repeater/ViewportManager.cs +++ b/src/Avalonia.Controls/Repeater/ViewportManager.cs @@ -186,7 +186,7 @@ namespace Avalonia.Controls _expectedViewportShift.X + _layoutExtent.X - extent.X, _expectedViewportShift.Y + _layoutExtent.Y - extent.Y); - // We tolerate viewport imprecisions up to 1 pixel to avoid invaliding layout too much. + // We tolerate viewport imprecisions up to 1 pixel to avoid invalidating layout too much. if (Math.Abs(_expectedViewportShift.X) > 1 || Math.Abs(_expectedViewportShift.Y) > 1) { Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Expecting viewport shift of ({Shift})", diff --git a/src/Avalonia.Controls/ScrollViewer.cs b/src/Avalonia.Controls/ScrollViewer.cs index 559edeb204..eee6216587 100644 --- a/src/Avalonia.Controls/ScrollViewer.cs +++ b/src/Avalonia.Controls/ScrollViewer.cs @@ -653,7 +653,7 @@ namespace Avalonia.Controls private void CalculatedPropertiesChanged() { // Pass old values of 0 here because we don't have the old values at this point, - // and it shouldn't matter as only the template uses these properies. + // and it shouldn't matter as only the template uses these properties. RaisePropertyChanged(HorizontalScrollBarMaximumProperty, 0, HorizontalScrollBarMaximum); RaisePropertyChanged(HorizontalScrollBarValueProperty, 0, HorizontalScrollBarValue); RaisePropertyChanged(HorizontalScrollBarViewportSizeProperty, 0, HorizontalScrollBarViewportSize); diff --git a/src/Avalonia.Controls/Selection/SelectionModel.cs b/src/Avalonia.Controls/Selection/SelectionModel.cs index 6ae53a4d59..138a765b43 100644 --- a/src/Avalonia.Controls/Selection/SelectionModel.cs +++ b/src/Avalonia.Controls/Selection/SelectionModel.cs @@ -345,7 +345,7 @@ namespace Avalonia.Controls.Selection private protected override void OnSelectionChanged(IReadOnlyList deselectedItems) { // Note: We're *not* putting this in a using scope. A collection update is still in progress - // so the operation won't get commited by normal means: we have to commit it manually. + // so the operation won't get committed by normal means: we have to commit it manually. var update = BatchUpdate(); update.Operation.DeselectedItems = deselectedItems; diff --git a/src/Avalonia.Controls/TickBar.cs b/src/Avalonia.Controls/TickBar.cs index 237bc2ce1d..12ae766052 100644 --- a/src/Avalonia.Controls/TickBar.cs +++ b/src/Avalonia.Controls/TickBar.cs @@ -193,7 +193,7 @@ namespace Avalonia.Controls /// /// TickBar will use ReservedSpaceProperty for left and right spacing (for horizontal orientation) or - /// top and bottom spacing (for vertical orienation). + /// top and bottom spacing (for vertical orientation). /// The space on both sides of TickBar is half of specified ReservedSpace. /// This property has type of . /// @@ -210,7 +210,7 @@ namespace Avalonia.Controls /// This function also draw selection-tick(s) if IsSelectionRangeEnabled is 'true' and /// SelectionStart and SelectionEnd are valid. /// - /// The primary ticks (for Mininum and Maximum value) height will be 100% of TickBar's render size (use Width or Height + /// The primary ticks (for Minimum and Maximum value) height will be 100% of TickBar's render size (use Width or Height /// depends on Placement property). /// /// The secondary ticks (all other ticks, including selection-tics) height will be 75% of TickBar's render size. @@ -221,7 +221,7 @@ namespace Avalonia.Controls { var size = new Size(Bounds.Width, Bounds.Height); var range = Maximum - Minimum; - var tickLen = 0.0d; // Height for Primary Tick (for Mininum and Maximum value) + var tickLen = 0.0d; // Height for Primary Tick (for Minimum and Maximum value) var tickLen2 = 0.0d; // Height for Secondary Tick var logicalToPhysical = 1.0; var startPoint = new Point(); @@ -285,7 +285,7 @@ namespace Avalonia.Controls tickLen2 = tickLen * 0.75; - // Invert direciton of the ticks + // Invert direction of the ticks if (IsDirectionReversed) { logicalToPhysical *= -1; diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 7028dca769..5b0590c6f0 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -224,7 +224,7 @@ namespace Avalonia.Controls } /// - /// Gets the acheived that the platform was able to provide. + /// Gets the achieved that the platform was able to provide. /// public WindowTransparencyLevel ActualTransparencyLevel { diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs index 2b31cef8bd..72b6e4543b 100644 --- a/src/Avalonia.Controls/WindowBase.cs +++ b/src/Avalonia.Controls/WindowBase.cs @@ -264,7 +264,7 @@ namespace Avalonia.Controls } /// - /// Called durung the arrange pass to set the size of the window. + /// Called during the arrange pass to set the size of the window. /// /// The requested size of the window. /// The actual size of the window. diff --git a/src/Avalonia.Input/Cursor.cs b/src/Avalonia.Input/Cursor.cs index 2b99c51472..122838f682 100644 --- a/src/Avalonia.Input/Cursor.cs +++ b/src/Avalonia.Input/Cursor.cs @@ -37,7 +37,7 @@ namespace Avalonia.Input BottomSize = BottomSide // Not available in GTK directly, see http://www.pixelbeat.org/programming/x_cursors/ - // We might enable them later, preferably, by loading pixmax direclty from theme with fallback image + // We might enable them later, preferably, by loading pixmax directly from theme with fallback image // SizeNorthWestSouthEast, // SizeNorthEastSouthWest, } diff --git a/src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs b/src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs index 3858cc04f2..84a26a0cc3 100644 --- a/src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs +++ b/src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs @@ -78,7 +78,7 @@ namespace Avalonia.Input.GestureRecognizers // Arbitrary chosen value, probably need to move that to platform settings or something private const double ScrollStartDistance = 30; - // Pixels per second speed that is considered to be the stop of inertiall scroll + // Pixels per second speed that is considered to be the stop of inertial scroll private const double InertialScrollSpeedEnd = 5; public void PointerMoved(PointerEventArgs e) diff --git a/src/Avalonia.Input/ICommandSource.cs b/src/Avalonia.Input/ICommandSource.cs index ba2e8eed4e..eed71759d5 100644 --- a/src/Avalonia.Input/ICommandSource.cs +++ b/src/Avalonia.Input/ICommandSource.cs @@ -22,7 +22,7 @@ namespace Avalonia.Input /// - /// Bor the bheavior CanExecuteChanged + /// Bor the behavior CanExecuteChanged /// /// /// diff --git a/src/Avalonia.Input/TextInput/ITextInputMethodClient.cs b/src/Avalonia.Input/TextInput/ITextInputMethodClient.cs index d385f5b162..2b5d8958cc 100644 --- a/src/Avalonia.Input/TextInput/ITextInputMethodClient.cs +++ b/src/Avalonia.Input/TextInput/ITextInputMethodClient.cs @@ -22,11 +22,11 @@ namespace Avalonia.Input.TextInput /// event EventHandler TextViewVisualChanged; /// - /// Indicates if TextViewVisual is capable of displaying non-commited input on the cursor position + /// Indicates if TextViewVisual is capable of displaying non-committed input on the cursor position /// bool SupportsPreedit { get; } /// - /// Sets the non-commited input string + /// Sets the non-committed input string /// void SetPreeditText(string text); /// diff --git a/src/Avalonia.Interactivity/Interactive.cs b/src/Avalonia.Interactivity/Interactive.cs index 4cd810af20..2497150b1a 100644 --- a/src/Avalonia.Interactivity/Interactive.cs +++ b/src/Avalonia.Interactivity/Interactive.cs @@ -143,7 +143,7 @@ namespace Avalonia.Interactivity /// The routed event. /// An describing the route. /// - /// Usually, calling is sufficent to raise a routed + /// Usually, calling is sufficient to raise a routed /// event, however there are situations in which the construction of the event args is expensive /// and should be avoided if there are no handlers for an event. In these cases you can call /// this method to build the event route and check the diff --git a/src/Avalonia.Layout/ElementManager.cs b/src/Avalonia.Layout/ElementManager.cs index 3f106708e6..c2f829d020 100644 --- a/src/Avalonia.Layout/ElementManager.cs +++ b/src/Avalonia.Layout/ElementManager.cs @@ -40,7 +40,7 @@ namespace Avalonia.Layout { if (IsVirtualizingContext) { - // We proactively clear elements laid out outside of the realizaton + // We proactively clear elements laid out outside of the realization // rect so that they are available for reuse during the current // measure pass. // This is useful during fast panning scenarios in which the realization diff --git a/src/Avalonia.Layout/FlowLayoutAlgorithm.cs b/src/Avalonia.Layout/FlowLayoutAlgorithm.cs index eace54d2e0..63343fd1a7 100644 --- a/src/Avalonia.Layout/FlowLayoutAlgorithm.cs +++ b/src/Avalonia.Layout/FlowLayoutAlgorithm.cs @@ -22,7 +22,7 @@ namespace Avalonia.Layout private int _firstRealizedDataIndexInsideRealizationWindow = -1; private int _lastRealizedDataIndexInsideRealizationWindow = -1; - // If the scroll orientation is the same as the folow orientation + // If the scroll orientation is the same as the follow orientation // we will only have one line since we will never wrap. In that case // we do not want to align the line. We could potentially switch the // meaning of line alignment in this case, but I'll hold off on that @@ -429,7 +429,7 @@ namespace Avalonia.Layout // If we did not reach the top or bottom of the extent, we realized one // extra item before we knew we were outside the realization window. Do not - // account for that element in the indicies inside the realization window. + // account for that element in the indices inside the realization window. if (direction == GenerateDirection.Forward) { int dataCount = _context.ItemCount; diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs index 2e3408eca5..89efa6af0c 100644 --- a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -138,7 +138,7 @@ namespace Avalonia.Native { _nativeMenu = (__MicroComIAvnMenuProxy)__MicroComIAvnMenuProxy.Create(_factory); - _nativeMenu.Initialise(this, appMenuHolder, ""); + _nativeMenu.Initialize(this, appMenuHolder, ""); setMenu = true; } @@ -159,7 +159,7 @@ namespace Avalonia.Native { _nativeMenu = __MicroComIAvnMenuProxy.Create(_factory); - _nativeMenu.Initialise(this, menu, ""); + _nativeMenu.Initialize(this, menu, ""); setMenu = true; } diff --git a/src/Avalonia.Native/IAvnMenu.cs b/src/Avalonia.Native/IAvnMenu.cs index f76e9450fc..0e6fdd2df0 100644 --- a/src/Avalonia.Native/IAvnMenu.cs +++ b/src/Avalonia.Native/IAvnMenu.cs @@ -96,7 +96,7 @@ namespace Avalonia.Native.Interop.Impl _menuItems.Remove(item); RemoveItem(item); - item.Deinitialise(); + item.Deinitialize(); item.Dispose(); } @@ -113,7 +113,7 @@ namespace Avalonia.Native.Interop.Impl { var result = CreateNew(factory, item); - result.Initialise(item); + result.Initialize(item); _menuItemLookup.Add(result.ManagedMenuItem, result); _menuItems.Insert(index, result); @@ -133,7 +133,7 @@ namespace Avalonia.Native.Interop.Impl return nativeItem; } - internal void Initialise(AvaloniaNativeMenuExporter exporter, NativeMenu managedMenu, string title) + internal void Initialize(AvaloniaNativeMenuExporter exporter, NativeMenu managedMenu, string title) { _exporter = exporter; ManagedMenu = managedMenu; @@ -150,7 +150,7 @@ namespace Avalonia.Native.Interop.Impl foreach (var item in _menuItems) { - item.Deinitialise(); + item.Deinitialize(); item.Dispose(); } } diff --git a/src/Avalonia.Native/IAvnMenuItem.cs b/src/Avalonia.Native/IAvnMenuItem.cs index 97838f8dea..ca99cbea4b 100644 --- a/src/Avalonia.Native/IAvnMenuItem.cs +++ b/src/Avalonia.Native/IAvnMenuItem.cs @@ -85,7 +85,7 @@ namespace Avalonia.Native.Interop.Impl SetAction(action, callback); } - internal void Initialise(NativeMenuItemBase nativeMenuItem) + internal void Initialize(NativeMenuItemBase nativeMenuItem) { ManagedMenuItem = nativeMenuItem; @@ -123,7 +123,7 @@ namespace Avalonia.Native.Interop.Impl } } - internal void Deinitialise() + internal void Deinitialize() { if (_subMenu != null) { @@ -150,7 +150,7 @@ namespace Avalonia.Native.Interop.Impl { _subMenu = __MicroComIAvnMenuProxy.Create(factory); - _subMenu.Initialise(exporter, item.Menu, item.Header); + _subMenu.Initialize(exporter, item.Menu, item.Header); SetSubMenu(_subMenu); } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreakEnumerator.cs b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreakEnumerator.cs index 4d02f94cad..40891a700d 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreakEnumerator.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreakEnumerator.cs @@ -284,7 +284,7 @@ namespace Avalonia.Media.TextFormatting.Unicode // - U+0028 (Left Opening Parenthesis) // - U+005B (Opening Square Bracket) // - U+007B (Left Curly Bracket) - // See custom colums|rules in the text pair table. + // See custom columns|rules in the text pair table. // https://www.unicode.org/Public/13.0.0/ucd/auxiliary/LineBreakTest.html _lb30 = _alphaNumericCount > 0 && cls == LineBreakClass.OpenPunctuation diff --git a/src/Avalonia.Visuals/Media/Transformation/InterpolationUtilities.cs b/src/Avalonia.Visuals/Media/Transformation/InterpolationUtilities.cs index 1e80eabfc8..742bb9c804 100644 --- a/src/Avalonia.Visuals/Media/Transformation/InterpolationUtilities.cs +++ b/src/Avalonia.Visuals/Media/Transformation/InterpolationUtilities.cs @@ -25,14 +25,14 @@ namespace Avalonia.Media.Transformation Matrix.CreateScale(decomposed.Scale); } - public static Matrix.Decomposed InterpolateDecomposedTransforms(ref Matrix.Decomposed from, ref Matrix.Decomposed to, double progres) + public static Matrix.Decomposed InterpolateDecomposedTransforms(ref Matrix.Decomposed from, ref Matrix.Decomposed to, double progress) { Matrix.Decomposed result = default; - result.Translate = InterpolateVectors(from.Translate, to.Translate, progres); - result.Scale = InterpolateVectors(from.Scale, to.Scale, progres); - result.Skew = InterpolateVectors(from.Skew, to.Skew, progres); - result.Angle = InterpolateScalars(from.Angle, to.Angle, progres); + result.Translate = InterpolateVectors(from.Translate, to.Translate, progress); + result.Scale = InterpolateVectors(from.Scale, to.Scale, progress); + result.Skew = InterpolateVectors(from.Skew, to.Skew, progress); + result.Angle = InterpolateScalars(from.Angle, to.Angle, progress); return result; } From 4212d3c6955307718bc29cda305881d00defaed6 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sun, 11 Jul 2021 19:47:53 +0200 Subject: [PATCH 049/127] Allow for disabling clipping of adorners. Cleanup AdornerLayer class. --- .../Primitives/AdornerLayer.cs | 67 +++++++++++++------ 1 file changed, 48 insertions(+), 19 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/AdornerLayer.cs b/src/Avalonia.Controls/Primitives/AdornerLayer.cs index a397608aba..52c8273b6c 100644 --- a/src/Avalonia.Controls/Primitives/AdornerLayer.cs +++ b/src/Avalonia.Controls/Primitives/AdornerLayer.cs @@ -1,19 +1,33 @@ using System; using System.Collections.Specialized; -using System.Linq; -using System.Resources; using Avalonia.Media; using Avalonia.Rendering; -using Avalonia.Utilities; using Avalonia.VisualTree; +#nullable enable + namespace Avalonia.Controls.Primitives { - // TODO: Need to track position of adorned elements and move the adorner if they move. + /// + /// Represents a surface for showing adorners. + /// Adorners are always on top of the adorned element and are positioned to stay relative to the adorned element. + /// + /// + /// TODO: Need to track position of adorned elements and move the adorner if they move. + /// public class AdornerLayer : Canvas, ICustomSimpleHitTest { - public static readonly AttachedProperty AdornedElementProperty = - AvaloniaProperty.RegisterAttached("AdornedElement"); + /// + /// Allows for getting and setting of the adorned element. + /// + public static readonly AttachedProperty AdornedElementProperty = + AvaloniaProperty.RegisterAttached("AdornedElement"); + + /// + /// Allows for controlling clipping of the adorner. + /// + public static readonly AttachedProperty IsClipEnabledProperty = + AvaloniaProperty.RegisterAttached("IsClipEnabled", true); private static readonly AttachedProperty s_adornedElementInfoProperty = AvaloniaProperty.RegisterAttached("AdornedElementInfo"); @@ -28,7 +42,7 @@ namespace Avalonia.Controls.Primitives Children.CollectionChanged += ChildrenCollectionChanged; } - public static Visual GetAdornedElement(Visual adorner) + public static Visual? GetAdornedElement(Visual adorner) { return adorner.GetValue(AdornedElementProperty); } @@ -38,12 +52,19 @@ namespace Avalonia.Controls.Primitives adorner.SetValue(AdornedElementProperty, adorned); } - public static AdornerLayer GetAdornerLayer(IVisual visual) + public static AdornerLayer? GetAdornerLayer(IVisual visual) + { + return visual.FindAncestorOfType()?.AdornerLayer; + } + + public static bool GetIsClipEnabled(Visual adorner) { - return visual.GetVisualAncestors() - .OfType() - .FirstOrDefault() - ?.AdornerLayer; + return adorner.GetValue(IsClipEnabledProperty); + } + + public static void SetIsClipEnabled(Visual adorner, bool isClipEnabled) + { + adorner.SetValue(IsClipEnabledProperty, isClipEnabled); } protected override Size MeasureOverride(Size availableSize) @@ -70,12 +91,13 @@ namespace Avalonia.Controls.Primitives foreach (var child in Children) { var info = child.GetValue(s_adornedElementInfoProperty); + var isClipEnabled = child.GetValue(IsClipEnabledProperty); if (info != null && info.Bounds.HasValue) { child.RenderTransform = new MatrixTransform(info.Bounds.Value.Transform); child.RenderTransformOrigin = new RelativePoint(new Point(0,0), RelativeUnit.Absolute); - UpdateClip(child, info.Bounds.Value); + UpdateClip(child, info.Bounds.Value, isClipEnabled); child.Arrange(info.Bounds.Value.Bounds); } else @@ -87,16 +109,23 @@ namespace Avalonia.Controls.Primitives return finalSize; } - private static void AdornedElementChanged(AvaloniaPropertyChangedEventArgs e) + private static void AdornedElementChanged(AvaloniaPropertyChangedEventArgs e) { var adorner = (Visual)e.Sender; - var adorned = (Visual)e.NewValue; + var adorned = e.NewValue.GetValueOrDefault(); var layer = adorner.GetVisualParent(); layer?.UpdateAdornedElement(adorner, adorned); } - private void UpdateClip(IControl control, TransformedBounds bounds) + private void UpdateClip(IControl control, TransformedBounds bounds, bool isEnabled) { + if (!isEnabled) + { + control.Clip = null; + + return; + } + if (!(control.Clip is RectangleGeometry clip)) { clip = new RectangleGeometry(); @@ -129,13 +158,13 @@ namespace Avalonia.Controls.Primitives InvalidateArrange(); } - private void UpdateAdornedElement(Visual adorner, Visual adorned) + private void UpdateAdornedElement(Visual adorner, Visual? adorned) { var info = adorner.GetValue(s_adornedElementInfoProperty); if (info != null) { - info.Subscription.Dispose(); + info.Subscription!.Dispose(); if (adorned == null) { @@ -163,7 +192,7 @@ namespace Avalonia.Controls.Primitives private class AdornedElementInfo { - public IDisposable Subscription { get; set; } + public IDisposable? Subscription { get; set; } public TransformedBounds? Bounds { get; set; } } From 298dd4e2997b9aab1f0f203f210355ee7e354197 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 12 Jul 2021 17:36:45 +0200 Subject: [PATCH 050/127] Added ShutdownRequested to IClassicDesktopStyleApplicationLifetime. --- .../ClassicDesktopStyleApplicationLifetime.cs | 10 +++++-- ...IClassicDesktopStyleApplicationLifetime.cs | 12 ++++++++ .../DesktopStyleApplicationLifetimeTests.cs | 30 ++++++++++++++++++- 3 files changed, 49 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs index 65f1f0fff2..79780dbd0b 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs @@ -46,6 +46,10 @@ namespace Avalonia.Controls.ApplicationLifetimes /// public event EventHandler Startup; + + /// + public event EventHandler ShutdownRequested; + /// public event EventHandler Exit; @@ -115,7 +119,7 @@ namespace Avalonia.Controls.ApplicationLifetimes var lifetimeEvents = AvaloniaLocator.Current.GetService(); if (lifetimeEvents != null) - lifetimeEvents.ShutdownRequested += ShutdownRequested; + lifetimeEvents.ShutdownRequested += OnShutdownRequested; _cts = new CancellationTokenSource(); MainWindow?.Show(); @@ -130,8 +134,10 @@ namespace Avalonia.Controls.ApplicationLifetimes _activeLifetime = null; } - private void ShutdownRequested(object sender, CancelEventArgs e) + private void OnShutdownRequested(object sender, CancelEventArgs e) { + ShutdownRequested?.Invoke(this, e); + if (e.Cancel) return; diff --git a/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs index 212f0b8617..ecf8a0358f 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; namespace Avalonia.Controls.ApplicationLifetimes { @@ -34,5 +35,16 @@ namespace Avalonia.Controls.ApplicationLifetimes Window MainWindow { get; set; } IReadOnlyList Windows { get; } + + /// + /// Raised by the platform when a shutdown is requested. + /// + /// + /// Raised on on OSX via the Quit menu or right-clicking on the application icon and selecting Quit. This event + /// provides a first-chance to cancel application shutdown; if shutdown is not canceled at this point the application + /// will try to close each non-owned open window, invoking the event on each and allowing + /// each window to cancel the shutdown. + /// + event EventHandler ShutdownRequested; } } diff --git a/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs b/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs index 84f02aeda5..38713834c3 100644 --- a/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs +++ b/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Platform; using Avalonia.Threading; @@ -209,6 +210,33 @@ namespace Avalonia.Controls.UnitTests Assert.Empty(lifetime.Windows); } } + + [Fact] + public void Should_Allow_Canceling_Shutdown_Via_ShutdownRequested_Event() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + using (var lifetime = new ClassicDesktopStyleApplicationLifetime()) + { + var lifetimeEvents = new Mock(); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(lifetimeEvents.Object); + lifetime.Start(Array.Empty()); + + var window = new Window(); + var raised = 0; + + window.Show(); + + lifetime.ShutdownRequested += (s, e) => + { + e.Cancel = true; + ++raised; + }; + + lifetimeEvents.Raise(x => x.ShutdownRequested += null, new CancelEventArgs()); + + Assert.Equal(1, raised); + Assert.Equal(new[] { window }, lifetime.Windows); + } + } } - } From 70c207da994e7fe3cd2d6f1c2852e5143be3d16a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 12 Jul 2021 17:38:34 +0200 Subject: [PATCH 051/127] Update ApiCompatBaseline.txt --- src/Avalonia.Controls/ApiCompatBaseline.txt | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/ApiCompatBaseline.txt b/src/Avalonia.Controls/ApiCompatBaseline.txt index 1abf4fd6ff..596b17c943 100644 --- a/src/Avalonia.Controls/ApiCompatBaseline.txt +++ b/src/Avalonia.Controls/ApiCompatBaseline.txt @@ -6,12 +6,15 @@ InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Control InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.INativeMenuExporterEventsImplBridge.RaiseOpening()' is present in the implementation but not in the contract. MembersMustExist : Member 'public Avalonia.StyledProperty Avalonia.StyledProperty Avalonia.Controls.ScrollViewer.AllowAutoHideProperty' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.AvaloniaProperty Avalonia.AvaloniaProperty Avalonia.Controls.Viewbox.StretchProperty' does not exist in the implementation but it does exist in the contract. +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. MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.AvaloniaProperty Avalonia.AvaloniaProperty Avalonia.Controls.Notifications.NotificationCard.CloseOnClickProperty' does not exist in the implementation but it does exist in the contract. EnumValuesMustMatch : Enum value 'Avalonia.Platform.ExtendClientAreaChromeHints Avalonia.Platform.ExtendClientAreaChromeHints.Default' is (System.Int32)2 in the implementation but (System.Int32)1 in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public System.Nullable Avalonia.Platform.ITopLevelImpl.FrameSize' is present in the implementation but not in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public System.Nullable Avalonia.Platform.ITopLevelImpl.FrameSize.get()' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.ICursorImpl)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' is present in the contract but not in the implementation. MembersMustExist : Member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract. -InterfacesShouldHaveSameMembers : Interface member 'public System.Nullable Avalonia.Platform.ITopLevelImpl.FrameSize' is present in the implementation but not in the contract. -InterfacesShouldHaveSameMembers : Interface member 'public System.Nullable Avalonia.Platform.ITopLevelImpl.FrameSize.get()' is present in the implementation but not in the contract. -Total Issues: 15 +Total Issues: 18 From e69af3fa212017b317f4ca16120c85b038082a69 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 12 Jul 2021 17:43:27 +0200 Subject: [PATCH 052/127] Undo changes to AppCompatBaseline.txt --- src/Avalonia.Visuals/ApiCompatBaseline.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Visuals/ApiCompatBaseline.txt b/src/Avalonia.Visuals/ApiCompatBaseline.txt index c917902dc3..39a4c3004c 100644 --- a/src/Avalonia.Visuals/ApiCompatBaseline.txt +++ b/src/Avalonia.Visuals/ApiCompatBaseline.txt @@ -5,6 +5,7 @@ InterfacesShouldHaveSameMembers : Interface member 'public System.Threading.Task MembersMustExist : Member 'public System.Threading.Tasks.Task Avalonia.Animation.IPageTransition.Start(Avalonia.Visual, Avalonia.Visual, System.Boolean)' does not exist in the implementation but it does exist in the contract. InterfacesShouldHaveSameMembers : Interface member 'public System.Threading.Tasks.Task Avalonia.Animation.IPageTransition.Start(Avalonia.Visual, Avalonia.Visual, System.Boolean, System.Threading.CancellationToken)' is present in the implementation but not in the contract. MembersMustExist : Member 'public System.Threading.Tasks.Task Avalonia.Animation.PageSlide.Start(Avalonia.Visual, Avalonia.Visual, System.Boolean)' does not exist in the implementation but it does exist in the contract. +TypeCannotChangeClassification : Type 'Avalonia.Media.Immutable.ImmutableSolidColorBrush' is a 'class' in the implementation but is a 'struct' in the contract. MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.DrawableTextRun.Draw(Avalonia.Media.DrawingContext)' does not exist in the implementation but it does exist in the contract. CannotAddAbstractMembers : Member 'public void Avalonia.Media.TextFormatting.DrawableTextRun.Draw(Avalonia.Media.DrawingContext, Avalonia.Point)' is abstract in the implementation but is missing in the contract. CannotSealType : Type 'Avalonia.Media.TextFormatting.GenericTextParagraphProperties' is actually (has the sealed modifier) sealed in the implementation but not sealed in the contract. @@ -73,4 +74,4 @@ InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IWr InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IWriteableBitmapImpl Avalonia.Platform.IPlatformRenderInterface.LoadWriteableBitmap(System.String)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IWriteableBitmapImpl Avalonia.Platform.IPlatformRenderInterface.LoadWriteableBitmapToHeight(System.IO.Stream, System.Int32, Avalonia.Visuals.Media.Imaging.BitmapInterpolationMode)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IWriteableBitmapImpl Avalonia.Platform.IPlatformRenderInterface.LoadWriteableBitmapToWidth(System.IO.Stream, System.Int32, Avalonia.Visuals.Media.Imaging.BitmapInterpolationMode)' is present in the implementation but not in the contract. -Total Issues: 74 +Total Issues: 75 From 0bfbf285ed8604e44d2934dcd815813140333cb0 Mon Sep 17 00:00:00 2001 From: megasys Date: Mon, 12 Jul 2021 22:02:40 +0200 Subject: [PATCH 053/127] Fix: Thumb should handle mouse capture loss --- src/Avalonia.Controls/Primitives/Thumb.cs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/Avalonia.Controls/Primitives/Thumb.cs b/src/Avalonia.Controls/Primitives/Thumb.cs index 348922b71d..91141058e4 100644 --- a/src/Avalonia.Controls/Primitives/Thumb.cs +++ b/src/Avalonia.Controls/Primitives/Thumb.cs @@ -56,6 +56,26 @@ namespace Avalonia.Controls.Primitives { } + protected override void OnPointerCaptureLost(PointerCaptureLostEventArgs e) + { + if (_lastPoint.HasValue) + { + var ev = new VectorEventArgs + { + RoutedEvent = DragCompletedEvent, + Vector = _lastPoint.Value, + }; + + _lastPoint = null; + + RaiseEvent(ev); + } + + PseudoClasses.Remove(":pressed"); + + base.OnPointerCaptureLost(e); + } + protected override void OnPointerMoved(PointerEventArgs e) { if (_lastPoint.HasValue) From 2433435ac92eb505f7c1651ce449126e9ae05f91 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 13 Jul 2021 12:23:11 +0100 Subject: [PATCH 054/127] OSX - window state - keep field in sync. --- native/Avalonia.Native/src/OSX/window.mm | 1 + 1 file changed, 1 insertion(+) diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 4be1419f78..8360795036 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -757,6 +757,7 @@ private: } _lastWindowState = state; + _actualWindowState = state; WindowEvents->WindowStateChanged(state); } } From 551380e71d817a4efb9bd7855c0bb603c3eab86d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 13 Jul 2021 16:43:14 +0200 Subject: [PATCH 055/127] Added failing test for #4392. --- .../CompiledBindingExtensionTests.cs | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs index 6f549a4ffa..8cd5cf7c13 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs @@ -938,6 +938,58 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions } } + [Fact] + public void SupportsEmptyPath() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var textBlock = window.FindControl("textBlock"); + + var dataContext = new TestDataContext + { + StringProperty = "foobar" + }; + + window.DataContext = dataContext; + + Assert.Equal(typeof(TestDataContext).FullName, textBlock.Text); + } + } + + [Fact] + public void SupportsDotPath() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var textBlock = window.FindControl("textBlock"); + + var dataContext = new TestDataContext + { + StringProperty = "foobar" + }; + + window.DataContext = dataContext; + + Assert.Equal(typeof(TestDataContext).FullName, textBlock.Text); + } + } + void Throws(string type, Action cb) { try From 25d2ac986bf330d5dae1999e6c6cbc94ef19384e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 13 Jul 2021 16:44:19 +0200 Subject: [PATCH 056/127] Fix "." (empty) paths with compiled bindings. Fixes #4392 --- .../Transformers/AvaloniaXamlIlBindingPathParser.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathParser.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathParser.cs index 24cded1d22..93aab596c6 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathParser.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathParser.cs @@ -9,7 +9,6 @@ using XamlX.Ast; using XamlX.Transform; using XamlX.Transform.Transformers; using XamlX.TypeSystem; - using XamlParseException = XamlX.XamlParseException; namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers @@ -21,6 +20,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers if (node is XamlAstObjectNode binding && binding.Type.GetClrType().Equals(context.GetAvaloniaTypes().CompiledBindingExtension)) { var convertedNode = ConvertLongFormPropertiesToBindingExpressionNode(context, binding); + var isEmptyExpression = false; if (binding.Arguments.Count > 0 && binding.Arguments[0] is XamlAstTextNode bindingPathText) { @@ -32,9 +32,18 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers nodes.Insert(nodes.TakeWhile(x => x is BindingExpressionGrammar.ITransformNode).Count(), convertedNode); } - binding.Arguments[0] = new ParsedBindingPathNode(bindingPathText, context.GetAvaloniaTypes().CompiledBindingPath, nodes); + if (nodes.Count == 1 && nodes[0] is BindingExpressionGrammar.EmptyExpressionNode) + { + isEmptyExpression = true; + binding.Arguments.Clear(); + } + else + binding.Arguments[0] = new ParsedBindingPathNode(bindingPathText, context.GetAvaloniaTypes().CompiledBindingPath, nodes); } else + isEmptyExpression = true; + + if (isEmptyExpression) { var bindingPathAssignment = binding.Children.OfType() .FirstOrDefault(v => v.Property.GetClrProperty().Name == "Path"); From 911262bf3f25b3be61b7af7440d13f81aac1bda4 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 13 Jul 2021 17:12:43 +0200 Subject: [PATCH 057/127] Correctly handle long-form bindings with empty path. --- .../AvaloniaXamlIlBindingPathParser.cs | 25 +++++---- .../CompiledBindingExtensionTests.cs | 52 +++++++++++++++++++ 2 files changed, 68 insertions(+), 9 deletions(-) diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathParser.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathParser.cs index 93aab596c6..890cbb69bf 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathParser.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathParser.cs @@ -20,7 +20,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers if (node is XamlAstObjectNode binding && binding.Type.GetClrType().Equals(context.GetAvaloniaTypes().CompiledBindingExtension)) { var convertedNode = ConvertLongFormPropertiesToBindingExpressionNode(context, binding); - var isEmptyExpression = false; + var foundPath = false; if (binding.Arguments.Count > 0 && binding.Arguments[0] is XamlAstTextNode bindingPathText) { @@ -34,16 +34,16 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers if (nodes.Count == 1 && nodes[0] is BindingExpressionGrammar.EmptyExpressionNode) { - isEmptyExpression = true; - binding.Arguments.Clear(); + binding.Arguments.RemoveAt(0); } else + { binding.Arguments[0] = new ParsedBindingPathNode(bindingPathText, context.GetAvaloniaTypes().CompiledBindingPath, nodes); + foundPath = true; + } } - else - isEmptyExpression = true; - if (isEmptyExpression) + if (!foundPath) { var bindingPathAssignment = binding.Children.OfType() .FirstOrDefault(v => v.Property.GetClrProperty().Name == "Path"); @@ -53,12 +53,19 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers var reader = new CharacterReader(pathValue.Text.AsSpan()); var (nodes, _) = BindingExpressionGrammar.Parse(ref reader); - if (convertedNode != null) + if (nodes.Count == 1 && nodes[0] is BindingExpressionGrammar.EmptyExpressionNode) { - nodes.Insert(nodes.TakeWhile(x => x is BindingExpressionGrammar.ITransformNode).Count(), convertedNode); + bindingPathAssignment.Values.RemoveAt(0); } + else + { + if (convertedNode != null) + { + nodes.Insert(nodes.TakeWhile(x => x is BindingExpressionGrammar.ITransformNode).Count(), convertedNode); + } - bindingPathAssignment.Values[0] = new ParsedBindingPathNode(pathValue, context.GetAvaloniaTypes().CompiledBindingPath, nodes); + bindingPathAssignment.Values[0] = new ParsedBindingPathNode(pathValue, context.GetAvaloniaTypes().CompiledBindingPath, nodes); + } } } } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs index 8cd5cf7c13..8a61458030 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs @@ -964,6 +964,32 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions } } + [Fact] + public void SupportsEmptyPathWithStringFormat() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var textBlock = window.FindControl("textBlock"); + + var dataContext = new TestDataContext + { + StringProperty = "foobar" + }; + + window.DataContext = dataContext; + + Assert.Equal("bar-" + typeof(TestDataContext).FullName, textBlock.Text); + } + } + [Fact] public void SupportsDotPath() { @@ -990,6 +1016,32 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions } } + [Fact] + public void SupportsExplicitDotPathWithStringFormat() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var textBlock = window.FindControl("textBlock"); + + var dataContext = new TestDataContext + { + StringProperty = "foobar" + }; + + window.DataContext = dataContext; + + Assert.Equal("bar-" + typeof(TestDataContext).FullName, textBlock.Text); + } + } + void Throws(string type, Action cb) { try From 1a0780a7a43a5b8239919da60070cbc758a54613 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 14 Jul 2021 00:26:28 +0100 Subject: [PATCH 058/127] prevent recursive calls during resize. --- native/Avalonia.Native/src/OSX/window.mm | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 8360795036..68e87f38f5 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -29,10 +29,12 @@ public: IAvnMenu* _mainMenu; bool _shown; + bool _inResize; WindowBaseImpl(IAvnWindowBaseEvents* events, IAvnGlContext* gl) { _shown = false; + _inResize = false; _mainMenu = nullptr; BaseEvents = events; _glContext = gl; @@ -277,6 +279,14 @@ public: virtual HRESULT Resize(double x, double y) override { + + if(_inResize) + { + return S_OK; + } + + _inResize = true; + START_COM_CALL; @autoreleasepool @@ -312,6 +322,8 @@ public: [StandardContainer setFrameSize:NSSize{x,y}]; [Window setContentSize:NSSize{x, y}]; + _inResize = false; + return S_OK; } } From 1f8b90925771c64e54afa1a264938de0fd5e3c70 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 14 Jul 2021 00:27:52 +0100 Subject: [PATCH 059/127] fix flicker... make nswindow handle the content resizing automatically. --- native/Avalonia.Native/src/OSX/window.mm | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 68e87f38f5..6a35447b30 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -319,7 +319,6 @@ public: BaseEvents->Resized(AvnSize{x,y}); } - [StandardContainer setFrameSize:NSSize{x,y}]; [Window setContentSize:NSSize{x, y}]; _inResize = false; @@ -1289,6 +1288,9 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent [_blurBehind setWantsLayer:true]; _blurBehind.hidden = true; + [_blurBehind setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable]; + [_content setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable]; + [self addSubview:_blurBehind]; [self addSubview:_content]; @@ -1322,10 +1324,6 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent } _settingSize = true; - [super setFrameSize:newSize]; - - [_blurBehind setFrameSize:newSize]; - [_content setFrameSize:newSize]; auto window = objc_cast([self window]); @@ -1342,6 +1340,8 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent [_titleBarMaterial setFrame:tbar]; tbar.size.height = height < 1 ? 0 : 1; [_titleBarUnderline setFrame:tbar]; + + [super setFrameSize:newSize]; _settingSize = false; } @@ -2391,7 +2391,6 @@ protected: { if (Window != nullptr) { - [StandardContainer setFrameSize:NSSize{x,y}]; [Window setContentSize:NSSize{x, y}]; [Window setFrameTopLeftPoint:ToNSPoint(ConvertPointY(lastPositionSet))]; From a28ffc08a34c19605747ecb6cbfeed3fe0b530a5 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 14 Jul 2021 00:37:38 +0100 Subject: [PATCH 060/127] whitespace. --- native/Avalonia.Native/src/OSX/window.mm | 1 - 1 file changed, 1 deletion(-) diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 6a35447b30..7a76930070 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -279,7 +279,6 @@ public: virtual HRESULT Resize(double x, double y) override { - if(_inResize) { return S_OK; From 11c2a0f5f4cae7c5d474daf041f6ce423e484345 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 14 Jul 2021 00:37:50 +0100 Subject: [PATCH 061/127] restore change. --- native/Avalonia.Native/src/OSX/window.mm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 7a76930070..c6e5f0b447 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -1323,6 +1323,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent } _settingSize = true; + [super setFrameSize:newSize]; auto window = objc_cast([self window]); @@ -1339,8 +1340,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent [_titleBarMaterial setFrame:tbar]; tbar.size.height = height < 1 ? 0 : 1; [_titleBarUnderline setFrame:tbar]; - - [super setFrameSize:newSize]; + _settingSize = false; } From 47006ff110de88135230e75dc0a9b40a51e40f6b Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 14 Jul 2021 00:38:11 +0100 Subject: [PATCH 062/127] add missing COM retainer. --- native/Avalonia.Native/src/OSX/window.mm | 2 ++ 1 file changed, 2 insertions(+) diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index c6e5f0b447..888d3c3e98 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -2386,6 +2386,8 @@ protected: virtual HRESULT Resize(double x, double y) override { + START_COM_CALL; + @autoreleasepool { if (Window != nullptr) From c3f34825a0d875f96d68d8b0ae8f9890f3aed29c Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 13 Jul 2021 23:55:21 -0400 Subject: [PATCH 063/127] Fix DataGrid column sorting with inherited interface property --- .../Utils/ReflectionHelper.cs | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid/Utils/ReflectionHelper.cs b/src/Avalonia.Controls.DataGrid/Utils/ReflectionHelper.cs index 47d1ac1c0b..38e18b57de 100644 --- a/src/Avalonia.Controls.DataGrid/Utils/ReflectionHelper.cs +++ b/src/Avalonia.Controls.DataGrid/Utils/ReflectionHelper.cs @@ -340,10 +340,30 @@ namespace Avalonia.Controls.Utils internal static PropertyInfo GetPropertyOrIndexer(this Type type, string propertyPath, out object[] index) { index = null; + // Return the default value of GetProperty if the first character is not an indexer token. if (string.IsNullOrEmpty(propertyPath) || propertyPath[0] != LeftIndexerToken) { - // Return the default value of GetProperty if the first character is not an indexer token. - return type.GetProperty(propertyPath); + var property = type.GetProperty(propertyPath); + if (property != null) + { + return property; + } + + // GetProperty does not return inherited interface properties, + // so we need to enumerate them manually. + if (type.IsInterface) + { + foreach (var typeInterface in type.GetInterfaces()) + { + property = type.GetProperty(propertyPath); + if (property != null) + { + return property; + } + } + } + + return null; } if (propertyPath.Length < 2 || propertyPath[propertyPath.Length - 1] != RightIndexerToken) From b2c95a10c503b42505faacb77c7fc5c814001ccd Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 14 Jul 2021 09:45:45 +0100 Subject: [PATCH 064/127] add a try finally. --- native/Avalonia.Native/src/OSX/window.mm | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 888d3c3e98..7df9b76425 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -313,14 +313,19 @@ public: y = maxSize.height; } - if(!_shown) + @try { - BaseEvents->Resized(AvnSize{x,y}); + if(!_shown) + { + BaseEvents->Resized(AvnSize{x,y}); + } + + [Window setContentSize:NSSize{x, y}]; + } + @finally + { + _inResize = false; } - - [Window setContentSize:NSSize{x, y}]; - - _inResize = false; return S_OK; } From ba369a9059ccee6379e2809d495815b606212944 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 14 Jul 2021 11:26:31 +0200 Subject: [PATCH 065/127] Fix auto-scaling on win32. - Adds a "resize reason" to platform `Resized` events - Which is used by the auto-sizing code to determine whether to reset `SizeToContent` - Also other improvements to the reset logic for `SizeToContent`: - Don't reset it if the size hasn't changed - Don't reset it if `CanResize == false` - Only reset the dimensions whose size has changed - Obsolete the `BeginAutoSizing`/`AutoSizing` feature in `WindowBase` --- native/Avalonia.Native/src/OSX/window.h | 21 +++ native/Avalonia.Native/src/OSX/window.mm | 21 ++- .../Offscreen/OffscreenTopLevelImpl.cs | 4 +- .../Platform/ITopLevelImpl.cs | 37 ++++- src/Avalonia.Controls/Platform/IWindowImpl.cs | 4 +- src/Avalonia.Controls/Primitives/PopupRoot.cs | 9 +- src/Avalonia.Controls/TopLevel.cs | 3 +- src/Avalonia.Controls/Window.cs | 142 +++++++++--------- src/Avalonia.Controls/WindowBase.cs | 27 +--- .../Remote/PreviewerWindowImpl.cs | 2 +- src/Avalonia.DesignerSupport/Remote/Stubs.cs | 6 +- src/Avalonia.Headless/HeadlessWindowImpl.cs | 6 +- src/Avalonia.Native/PopupImpl.cs | 2 +- src/Avalonia.Native/WindowImplBase.cs | 12 +- src/Avalonia.Native/avn.idl | 13 +- src/Avalonia.X11/X11Window.cs | 16 +- .../FramebufferToplevelImpl.cs | 2 +- .../Wpf/WpfTopLevelImpl.cs | 6 +- src/Windows/Avalonia.Win32/PopupImpl.cs | 2 +- .../Avalonia.Win32/WindowImpl.AppWndProc.cs | 36 +++-- src/Windows/Avalonia.Win32/WindowImpl.cs | 29 +++- src/iOS/Avalonia.iOS/AvaloniaView.cs | 2 +- .../TopLevelTests.cs | 2 +- .../WindowTests.cs | 120 ++++++++++++++- .../MockWindowingPlatform.cs | 7 +- 25 files changed, 377 insertions(+), 154 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/window.h b/native/Avalonia.Native/src/OSX/window.h index b1f64bca88..039d09f663 100644 --- a/native/Avalonia.Native/src/OSX/window.h +++ b/native/Avalonia.Native/src/OSX/window.h @@ -10,6 +10,8 @@ class WindowBaseImpl; -(void) setSwRenderedFrame: (AvnFramebuffer* _Nonnull) fb dispose: (IUnknown* _Nonnull) dispose; -(void) onClosed; -(AvnPixelSize) getPixelSize; +-(AvnPlatformResizeReason) getResizeReason; +-(void) setResizeReason:(AvnPlatformResizeReason)reason; @end @interface AutoFitContentView : NSView @@ -50,4 +52,23 @@ struct IWindowStateChanged virtual AvnWindowState WindowState () = 0; }; +class ResizeScope +{ +public: + ResizeScope(AvnView* _Nonnull view, AvnPlatformResizeReason reason) + { + _view = view; + _restore = [view getResizeReason]; + [view setResizeReason:reason]; + } + + ~ResizeScope() + { + [_view setResizeReason:_restore]; + } +private: + AvnView* _Nonnull _view; + AvnPlatformResizeReason _restore; +}; + #endif /* window_h */ diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 8360795036..55fca743bf 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -275,9 +275,10 @@ public: } } - virtual HRESULT Resize(double x, double y) override + virtual HRESULT Resize(double x, double y, AvnPlatformResizeReason reason) override { START_COM_CALL; + auto resizeBlock = ResizeScope(View, reason); @autoreleasepool { @@ -306,7 +307,7 @@ public: if(!_shown) { - BaseEvents->Resized(AvnSize{x,y}); + BaseEvents->Resized(AvnSize{x,y}, reason); } [StandardContainer setFrameSize:NSSize{x,y}]; @@ -1357,6 +1358,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent bool _lastKeyHandled; AvnPixelSize _lastPixelSize; NSObject* _renderTarget; + AvnPlatformResizeReason _resizeReason; } - (void)onClosed @@ -1468,7 +1470,8 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent _lastPixelSize.Height = (int)fsize.height; [self updateRenderTarget]; - _parent->BaseEvents->Resized(AvnSize{newSize.width, newSize.height}); + auto reason = [self inLiveResize] ? ResizeUser : _resizeReason; + _parent->BaseEvents->Resized(AvnSize{newSize.width, newSize.height}, reason); } } @@ -1967,6 +1970,16 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent } +- (AvnPlatformResizeReason)getResizeReason +{ + return _resizeReason; +} + +- (void)setResizeReason:(AvnPlatformResizeReason)reason +{ + _resizeReason = reason; +} + @end @@ -2373,7 +2386,7 @@ protected: return NSWindowStyleMaskBorderless; } - virtual HRESULT Resize(double x, double y) override + virtual HRESULT Resize(double x, double y, AvnPlatformResizeReason reason) override { @autoreleasepool { diff --git a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs index 83470f161d..e2afbd3bdc 100644 --- a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs +++ b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs @@ -31,7 +31,7 @@ namespace Avalonia.Controls.Embedding.Offscreen set { _clientSize = value; - Resized?.Invoke(value); + Resized?.Invoke(value, PlatformResizeReason.Unspecified); } } @@ -49,7 +49,7 @@ namespace Avalonia.Controls.Embedding.Offscreen public Action Input { get; set; } public Action Paint { get; set; } - public Action Resized { get; set; } + public Action Resized { get; set; } public Action ScalingChanged { get; set; } public Action TransparencyLevelChanged { get; set; } diff --git a/src/Avalonia.Controls/Platform/ITopLevelImpl.cs b/src/Avalonia.Controls/Platform/ITopLevelImpl.cs index 6e53233898..f4f4d29168 100644 --- a/src/Avalonia.Controls/Platform/ITopLevelImpl.cs +++ b/src/Avalonia.Controls/Platform/ITopLevelImpl.cs @@ -3,11 +3,46 @@ using System.Collections.Generic; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Input.Raw; +using Avalonia.Layout; using Avalonia.Rendering; using JetBrains.Annotations; namespace Avalonia.Platform { + /// + /// Describes the reason for a message. + /// + public enum PlatformResizeReason + { + /// + /// The resize reason is unknown or unspecified. + /// + Unspecified, + + /// + /// The resize was due to the user resizing the window, for example by dragging the + /// window frame. + /// + User, + + /// + /// The resize was initiated by the application, for example by setting one of the sizing- + /// related properties on such as or + /// . + /// + Application, + + /// + /// The resize was initiated by the layout system. + /// + Layout, + + /// + /// The resize was due to a change in DPI. + /// + DpiChange, + } + /// /// Defines a platform-specific top-level window implementation. /// @@ -57,7 +92,7 @@ namespace Avalonia.Platform /// /// Gets or sets a method called when the toplevel is resized. /// - Action Resized { get; set; } + Action Resized { get; set; } /// /// Gets or sets a method called when the toplevel's scaling changes. diff --git a/src/Avalonia.Controls/Platform/IWindowImpl.cs b/src/Avalonia.Controls/Platform/IWindowImpl.cs index 8a1554d344..17a90eddfe 100644 --- a/src/Avalonia.Controls/Platform/IWindowImpl.cs +++ b/src/Avalonia.Controls/Platform/IWindowImpl.cs @@ -110,7 +110,9 @@ namespace Avalonia.Platform /// /// Sets the client size of the top level. /// - void Resize(Size clientSize); + /// The new client size. + /// The reason for the resize. + void Resize(Size clientSize, PlatformResizeReason reason = PlatformResizeReason.Application); /// /// Sets the client size of the top level. diff --git a/src/Avalonia.Controls/Primitives/PopupRoot.cs b/src/Avalonia.Controls/Primitives/PopupRoot.cs index da7352b77f..1a11778db2 100644 --- a/src/Avalonia.Controls/Primitives/PopupRoot.cs +++ b/src/Avalonia.Controls/Primitives/PopupRoot.cs @@ -161,12 +161,9 @@ namespace Avalonia.Controls.Primitives protected override sealed Size ArrangeSetBounds(Size size) { - using (BeginAutoSizing()) - { - _positionerParameters.Size = size; - UpdatePosition(); - return ClientSize; - } + _positionerParameters.Size = size; + UpdatePosition(); + return ClientSize; } } } diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 7028dca769..c5e7a7204f 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -380,7 +380,8 @@ namespace Avalonia.Controls /// Handles a resize notification from . /// /// The new client size. - protected virtual void HandleResized(Size clientSize) + /// The reason for the resize. + protected virtual void HandleResized(Size clientSize, PlatformResizeReason reason) { ClientSize = clientSize; FrameSize = PlatformImpl.FrameSize; diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index ae314a33ce..132944a765 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -244,7 +244,7 @@ namespace Avalonia.Controls impl.WindowStateChanged = HandleWindowStateChanged; _maxPlatformClientSize = PlatformImpl?.MaxAutoSizeHint ?? default(Size); impl.ExtendClientAreaToDecorationsChanged = ExtendClientAreaToDecorationsChanged; - this.GetObservable(ClientSizeProperty).Skip(1).Subscribe(x => PlatformImpl?.Resize(x)); + this.GetObservable(ClientSizeProperty).Skip(1).Subscribe(x => PlatformImpl?.Resize(x, PlatformResizeReason.Application)); PlatformImpl?.ShowTaskbarIcon(ShowInTaskbar); } @@ -258,6 +258,18 @@ namespace Avalonia.Controls /// /// Gets or sets a value indicating how the window will size itself to fit its content. /// + /// + /// If has a value other than , + /// is automatically set to + /// if a user resizes the window by using the resize grip or dragging the border. + /// + /// NOTE: Because of a limitation of X11, will be reset on X11 to + /// on any resize - including the resize that happens when + /// the window is first shown. This is because X11 resize notifications are asynchronous and + /// there is no way to know whether a resize came from the user or the layout system. To avoid + /// this, consider setting to false, which will disable user resizing + /// of the window. + /// public SizeToContent SizeToContent { get { return GetValue(SizeToContentProperty); } @@ -583,28 +595,23 @@ namespace Avalonia.Controls return; } - using (BeginAutoSizing()) - { - Renderer?.Stop(); + Renderer?.Stop(); - if (Owner is Window owner) - { - owner.RemoveChild(this); - } + if (Owner is Window owner) + { + owner.RemoveChild(this); + } - if (_children.Count > 0) + if (_children.Count > 0) + { + foreach (var child in _children.ToArray()) { - foreach (var child in _children.ToArray()) - { - child.child.Hide(); - } + child.child.Hide(); } - - Owner = null; - - PlatformImpl?.Hide(); } + Owner = null; + PlatformImpl?.Hide(); IsVisible = false; } @@ -675,29 +682,23 @@ namespace Avalonia.Controls if (initialSize != ClientSize) { - using (BeginAutoSizing()) - { - PlatformImpl?.Resize(initialSize); - } + PlatformImpl?.Resize(initialSize, PlatformResizeReason.Layout); } LayoutManager.ExecuteInitialLayoutPass(); - using (BeginAutoSizing()) + if (parent != null) { - if (parent != null) - { - PlatformImpl?.SetParent(parent.PlatformImpl); - } - - Owner = parent; - parent?.AddChild(this, false); - - SetWindowStartupLocation(Owner?.PlatformImpl); - - PlatformImpl?.Show(ShowActivated); - Renderer?.Start(); + PlatformImpl?.SetParent(parent.PlatformImpl); } + + Owner = parent; + parent?.AddChild(this, false); + + SetWindowStartupLocation(Owner?.PlatformImpl); + + PlatformImpl?.Show(ShowActivated); + Renderer?.Start(); OnOpened(EventArgs.Empty); } @@ -760,41 +761,34 @@ namespace Avalonia.Controls if (initialSize != ClientSize) { - using (BeginAutoSizing()) - { - PlatformImpl?.Resize(initialSize); - } + PlatformImpl?.Resize(initialSize, PlatformResizeReason.Layout); } LayoutManager.ExecuteInitialLayoutPass(); var result = new TaskCompletionSource(); - using (BeginAutoSizing()) - { - PlatformImpl?.SetParent(owner.PlatformImpl); - Owner = owner; - owner.AddChild(this, true); - - SetWindowStartupLocation(owner.PlatformImpl); - - PlatformImpl?.Show(ShowActivated); + PlatformImpl?.SetParent(owner.PlatformImpl); + Owner = owner; + owner.AddChild(this, true); - Renderer?.Start(); + SetWindowStartupLocation(owner.PlatformImpl); - Observable.FromEventPattern( - x => Closed += x, - x => Closed -= x) - .Take(1) - .Subscribe(_ => - { - owner.Activate(); - result.SetResult((TResult)(_dialogResult ?? default(TResult))); - }); + PlatformImpl?.Show(ShowActivated); - OnOpened(EventArgs.Empty); - } + Renderer?.Start(); + Observable.FromEventPattern( + x => Closed += x, + x => Closed -= x) + .Take(1) + .Subscribe(_ => + { + owner.Activate(); + result.SetResult((TResult)(_dialogResult ?? default(TResult))); + }); + + OnOpened(EventArgs.Empty); return result.Task; } @@ -937,11 +931,8 @@ namespace Avalonia.Controls protected sealed override Size ArrangeSetBounds(Size size) { - using (BeginAutoSizing()) - { - PlatformImpl?.Resize(size); - return ClientSize; - } + PlatformImpl?.Resize(size, PlatformResizeReason.Layout); + return ClientSize; } protected sealed override void HandleClosed() @@ -959,17 +950,32 @@ namespace Avalonia.Controls } /// - protected sealed override void HandleResized(Size clientSize) + protected sealed override void HandleResized(Size clientSize, PlatformResizeReason reason) { - if (!AutoSizing) + if (ClientSize == clientSize) + return; + + var sizeToContent = SizeToContent; + + // If auto-sizing is enabled, and the resize came from a user resize (or the reason was + // unspecified) then turn off auto-resizing for any window dimension that is not equal + // to the requested size. + if (sizeToContent != SizeToContent.Manual && + CanResize && + reason == PlatformResizeReason.Unspecified || + reason == PlatformResizeReason.User) { - SizeToContent = SizeToContent.Manual; + if (clientSize.Width != ClientSize.Width) + sizeToContent &= ~SizeToContent.Width; + if (clientSize.Height != ClientSize.Height) + sizeToContent &= ~SizeToContent.Height; + SizeToContent = sizeToContent; } Width = clientSize.Width; Height = clientSize.Height; - base.HandleResized(clientSize); + base.HandleResized(clientSize, reason); } /// diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs index 2b31cef8bd..e64eaec563 100644 --- a/src/Avalonia.Controls/WindowBase.cs +++ b/src/Avalonia.Controls/WindowBase.cs @@ -39,7 +39,6 @@ namespace Avalonia.Controls public static readonly StyledProperty TopmostProperty = AvaloniaProperty.Register(nameof(Topmost)); - private int _autoSizing; private bool _hasExecutedInitialLayoutPass; private bool _isActive; private bool _ignoreVisibilityChange; @@ -95,10 +94,8 @@ namespace Avalonia.Controls public Screens Screens { get; private set; } - /// - /// Whether an auto-size operation is in progress. - /// - protected bool AutoSizing => _autoSizing > 0; + [Obsolete("No longer used. Always returns false.")] + protected bool AutoSizing => false; /// /// Gets or sets the owner of the window. @@ -172,20 +169,9 @@ namespace Avalonia.Controls } } - /// - /// Begins an auto-resize operation. - /// - /// A disposable used to finish the operation. - /// - /// When an auto-resize operation is in progress any resize events received will not be - /// cause the new size to be written to the and - /// properties. - /// - protected IDisposable BeginAutoSizing() - { - ++_autoSizing; - return Disposable.Create(() => --_autoSizing); - } + + [Obsolete("No longer used. Has no effect.")] + protected IDisposable BeginAutoSizing() => Disposable.Empty; /// /// Ensures that the window is initialized. @@ -219,7 +205,8 @@ namespace Avalonia.Controls /// Handles a resize notification from . /// /// The new client size. - protected override void HandleResized(Size clientSize) + /// The reason for the resize. + protected override void HandleResized(Size clientSize, PlatformResizeReason reason) { ClientSize = clientSize; FrameSize = PlatformImpl.FrameSize; diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs index 787f44887f..946cb5f9a8 100644 --- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs +++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs @@ -58,7 +58,7 @@ namespace Avalonia.DesignerSupport.Remote base.OnMessage(transport, obj); } - public void Resize(Size clientSize) + public void Resize(Size clientSize, PlatformResizeReason reason) { _transport.Send(new RequestViewportResizeMessage { diff --git a/src/Avalonia.DesignerSupport/Remote/Stubs.cs b/src/Avalonia.DesignerSupport/Remote/Stubs.cs index c8203686f9..1f5272fa5d 100644 --- a/src/Avalonia.DesignerSupport/Remote/Stubs.cs +++ b/src/Avalonia.DesignerSupport/Remote/Stubs.cs @@ -27,7 +27,7 @@ namespace Avalonia.DesignerSupport.Remote public IEnumerable Surfaces { get; } public Action Input { get; set; } public Action Paint { get; set; } - public Action Resized { get; set; } + public Action Resized { get; set; } public Action ScalingChanged { get; set; } public Func Closing { get; set; } public Action Closed { get; set; } @@ -54,7 +54,7 @@ namespace Avalonia.DesignerSupport.Remote PopupPositioner = new ManagedPopupPositioner(new ManagedPopupPositionerPopupImplHelper(parent, (_, size, __) => { - Resize(size); + Resize(size, PlatformResizeReason.Unspecified); })); } @@ -98,7 +98,7 @@ namespace Avalonia.DesignerSupport.Remote { } - public void Resize(Size clientSize) + public void Resize(Size clientSize, PlatformResizeReason reason) { } diff --git a/src/Avalonia.Headless/HeadlessWindowImpl.cs b/src/Avalonia.Headless/HeadlessWindowImpl.cs index 7f4b9face4..3a74b1b15c 100644 --- a/src/Avalonia.Headless/HeadlessWindowImpl.cs +++ b/src/Avalonia.Headless/HeadlessWindowImpl.cs @@ -47,7 +47,7 @@ namespace Avalonia.Headless public IEnumerable Surfaces { get; } public Action Input { get; set; } public Action Paint { get; set; } - public Action Resized { get; set; } + public Action Resized { get; set; } public Action ScalingChanged { get; set; } public IRenderer CreateRenderer(IRenderRoot root) @@ -108,7 +108,7 @@ namespace Avalonia.Headless public Action Activated { get; set; } public IPlatformHandle Handle { get; } = new PlatformHandle(IntPtr.Zero, "STUB"); public Size MaxClientSize { get; } = new Size(1920, 1280); - public void Resize(Size clientSize) + public void Resize(Size clientSize, PlatformResizeReason reason) { // Emulate X11 behavior here if (IsPopup) @@ -126,7 +126,7 @@ namespace Avalonia.Headless if (ClientSize != clientSize) { ClientSize = clientSize; - Resized?.Invoke(clientSize); + Resized?.Invoke(clientSize, PlatformResizeReason.Unspecified); } } diff --git a/src/Avalonia.Native/PopupImpl.cs b/src/Avalonia.Native/PopupImpl.cs index c36675afcd..f357da3a4f 100644 --- a/src/Avalonia.Native/PopupImpl.cs +++ b/src/Avalonia.Native/PopupImpl.cs @@ -32,7 +32,7 @@ namespace Avalonia.Native private void MoveResize(PixelPoint position, Size size, double scaling) { Position = position; - Resize(size); + Resize(size, PlatformResizeReason.Layout); //TODO: We ignore the scaling override for now } diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index ced9cea3a8..6010ce22c7 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -87,7 +87,7 @@ namespace Avalonia.Native var monitor = Screen.AllScreens.OrderBy(x => x.PixelDensity) .FirstOrDefault(m => m.Bounds.Contains(Position)); - Resize(new Size(monitor.WorkingArea.Width * 0.75d, monitor.WorkingArea.Height * 0.7d)); + Resize(new Size(monitor.WorkingArea.Width * 0.75d, monitor.WorkingArea.Height * 0.7d), PlatformResizeReason.Layout); } public Size ClientSize @@ -146,7 +146,7 @@ namespace Avalonia.Native public Action LostFocus { get; set; } public Action Paint { get; set; } - public Action Resized { get; set; } + public Action Resized { get; set; } public Action Closed { get; set; } public IMouseDevice MouseDevice => _mouse; public abstract IPopupImpl CreatePopup(); @@ -186,13 +186,13 @@ namespace Avalonia.Native _parent.Paint?.Invoke(new Rect(0, 0, s.Width, s.Height)); } - void IAvnWindowBaseEvents.Resized(AvnSize* size) + void IAvnWindowBaseEvents.Resized(AvnSize* size, AvnPlatformResizeReason reason) { if (_parent?._native != null) { var s = new Size(size->Width, size->Height); _parent._savedLogicalSize = s; - _parent.Resized?.Invoke(s); + _parent.Resized?.Invoke(s, (PlatformResizeReason)reason); } } @@ -320,9 +320,9 @@ namespace Avalonia.Native } } - public void Resize(Size clientSize) + public void Resize(Size clientSize, PlatformResizeReason reason) { - _native.Resize(clientSize.Width, clientSize.Height); + _native.Resize(clientSize.Width, clientSize.Height, (AvnPlatformResizeReason)reason); } public IRenderer CreateRenderer(IRenderRoot root) diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl index 89e20463d8..19facf6e96 100644 --- a/src/Avalonia.Native/avn.idl +++ b/src/Avalonia.Native/avn.idl @@ -400,6 +400,15 @@ enum AvnExtendClientAreaChromeHints AvnDefaultChrome = AvnPreferSystemChrome, } +enum AvnPlatformResizeReason +{ + ResizeUnspecified, + ResizeUser, + ResizeApplication, + ResizeLayout, + ResizeDpiChange, +} + [uuid(809c652e-7396-11d2-9771-00a0c9b4d50c)] interface IAvaloniaNativeFactory : IUnknown { @@ -438,7 +447,7 @@ interface IAvnWindowBase : IUnknown HRESULT GetFrameSize(AvnSize*ret); HRESULT GetScaling(double*ret); HRESULT SetMinMaxSize(AvnSize minSize, AvnSize maxSize); - HRESULT Resize(double width, double height); + HRESULT Resize(double width, double height, AvnPlatformResizeReason reason); HRESULT Invalidate(AvnRect rect); HRESULT BeginMoveDrag(); HRESULT BeginResizeDrag(AvnWindowEdge edge); @@ -492,7 +501,7 @@ interface IAvnWindowBaseEvents : IUnknown void Closed(); void Activated(); void Deactivated(); - void Resized([const] AvnSize& size); + void Resized([const] AvnSize& size, AvnPlatformResizeReason reason); void PositionChanged(AvnPoint position); void RawMouseEvent(AvnRawMouseEventType type, uint timeStamp, diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index bcb655245a..3debd7ff0b 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -336,7 +336,7 @@ namespace Avalonia.X11 public IEnumerable Surfaces { get; } public Action Input { get; set; } public Action Paint { get; set; } - public Action Resized { get; set; } + public Action Resized { get; set; } //TODO public Action ScalingChanged { get; set; } public Action Deactivated { get; set; } @@ -510,7 +510,7 @@ namespace Avalonia.X11 UpdateImePosition(); if (changedSize && !updatedSizeViaScaling && !_popup) - Resized?.Invoke(ClientSize); + Resized?.Invoke(ClientSize, PlatformResizeReason.Unspecified); Dispatcher.UIThread.RunJobs(DispatcherPriority.Layout); }, DispatcherPriority.Layout); @@ -565,7 +565,7 @@ namespace Avalonia.X11 UpdateImePosition(); SetMinMaxSize(_scaledMinMaxSize.minSize, _scaledMinMaxSize.maxSize); if(!skipResize) - Resize(oldScaledSize, true); + Resize(oldScaledSize, true, PlatformResizeReason.DpiChange); return true; } @@ -616,7 +616,7 @@ namespace Avalonia.X11 { // Occurs once the window has been mapped, which is the earliest the extents // can be retrieved, so invoke event to force update of TopLevel.FrameSize. - Resized.Invoke(ClientSize); + Resized.Invoke(ClientSize, PlatformResizeReason.Unspecified); } if (atom == _x11.Atoms._NET_WM_STATE) @@ -862,19 +862,19 @@ namespace Avalonia.X11 } - public void Resize(Size clientSize) => Resize(clientSize, false); + public void Resize(Size clientSize, PlatformResizeReason reason) => Resize(clientSize, false, reason); public void Move(PixelPoint point) => Position = point; private void MoveResize(PixelPoint position, Size size, double scaling) { Move(position); _scalingOverride = scaling; UpdateScaling(true); - Resize(size, true); + Resize(size, true, PlatformResizeReason.Layout); } PixelSize ToPixelSize(Size size) => new PixelSize((int)(size.Width * RenderScaling), (int)(size.Height * RenderScaling)); - void Resize(Size clientSize, bool force) + void Resize(Size clientSize, bool force, PlatformResizeReason reason) { if (!force && clientSize == ClientSize) return; @@ -891,7 +891,7 @@ namespace Avalonia.X11 if (force || !_wasMappedAtLeastOnce || (_popup && needImmediatePopupResize)) { _realSize = pixelSize; - Resized?.Invoke(ClientSize); + Resized?.Invoke(ClientSize, reason); } } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs index 5a1da9058a..b097e0917f 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs @@ -69,7 +69,7 @@ namespace Avalonia.LinuxFramebuffer public IEnumerable Surfaces { get; } public Action Input { get; set; } public Action Paint { get; set; } - public Action Resized { get; set; } + public Action Resized { get; set; } public Action ScalingChanged { get; set; } public Action TransparencyLevelChanged { get; set; } diff --git a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs index 73e46b9e13..d36db107e3 100644 --- a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs +++ b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs @@ -44,7 +44,7 @@ namespace Avalonia.Win32.Interop.Wpf ((FrameworkElement)PlatformImpl)?.InvalidateMeasure(); } - protected override void HandleResized(Size clientSize) + protected override void HandleResized(Size clientSize, PlatformResizeReason reason) { ClientSize = clientSize; LayoutManager.ExecuteLayoutPass(); @@ -114,7 +114,7 @@ namespace Avalonia.Win32.Interop.Wpf if (_finalSize == _previousSize) return finalSize; _previousSize = _finalSize; - _ttl.Resized?.Invoke(finalSize.ToAvaloniaSize()); + _ttl.Resized?.Invoke(finalSize.ToAvaloniaSize(), PlatformResizeReason.Unspecified); return base.ArrangeOverride(finalSize); } @@ -236,7 +236,7 @@ namespace Avalonia.Win32.Interop.Wpf Action ITopLevelImpl.Input { get; set; } //TODO Action ITopLevelImpl.Paint { get; set; } - Action ITopLevelImpl.Resized { get; set; } + Action ITopLevelImpl.Resized { get; set; } Action ITopLevelImpl.ScalingChanged { get; set; } Action ITopLevelImpl.TransparencyLevelChanged { get; set; } diff --git a/src/Windows/Avalonia.Win32/PopupImpl.cs b/src/Windows/Avalonia.Win32/PopupImpl.cs index dd3fd1342c..800f884c45 100644 --- a/src/Windows/Avalonia.Win32/PopupImpl.cs +++ b/src/Windows/Avalonia.Win32/PopupImpl.cs @@ -135,7 +135,7 @@ namespace Avalonia.Win32 private void MoveResize(PixelPoint position, Size size, double scaling) { Move(position); - Resize(size); + Resize(size, PlatformResizeReason.Layout); //TODO: We ignore the scaling override for now } diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index 8e755a33bc..d163b3d068 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -2,9 +2,10 @@ using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; using Avalonia.Controls; -using Avalonia.Controls.Platform; +using Avalonia.Controls.Remote; using Avalonia.Input; using Avalonia.Input.Raw; +using Avalonia.Platform; using Avalonia.Win32.Input; using static Avalonia.Win32.Interop.UnmanagedMethods; @@ -18,7 +19,6 @@ namespace Avalonia.Win32 { const double wheelDelta = 120.0; uint timestamp = unchecked((uint)GetMessageTime()); - RawInputEventArgs e = null; var shouldTakeFocus = false; @@ -94,14 +94,19 @@ namespace Avalonia.Win32 var newDisplayRect = Marshal.PtrToStructure(lParam); _scaling = dpi / 96.0; ScalingChanged?.Invoke(_scaling); - SetWindowPos(hWnd, - IntPtr.Zero, - newDisplayRect.left, - newDisplayRect.top, - newDisplayRect.right - newDisplayRect.left, - newDisplayRect.bottom - newDisplayRect.top, - SetWindowPosFlags.SWP_NOZORDER | - SetWindowPosFlags.SWP_NOACTIVATE); + + using (SetResizeReason(PlatformResizeReason.DpiChange)) + { + SetWindowPos(hWnd, + IntPtr.Zero, + newDisplayRect.left, + newDisplayRect.top, + newDisplayRect.right - newDisplayRect.left, + newDisplayRect.bottom - newDisplayRect.top, + SetWindowPosFlags.SWP_NOZORDER | + SetWindowPosFlags.SWP_NOACTIVATE); + } + return IntPtr.Zero; } @@ -364,6 +369,11 @@ namespace Avalonia.Win32 return IntPtr.Zero; } + + case WindowsMessage.WM_ENTERSIZEMOVE: + _resizeReason = PlatformResizeReason.User; + break; + case WindowsMessage.WM_SIZE: { using(NonPumpingSyncContext.Use()) @@ -379,7 +389,7 @@ namespace Avalonia.Win32 size == SizeCommand.Maximized)) { var clientSize = new Size(ToInt32(lParam) & 0xffff, ToInt32(lParam) >> 16); - Resized(clientSize / RenderScaling); + Resized(clientSize / RenderScaling, _resizeReason); } var windowState = size == SizeCommand.Maximized ? @@ -403,6 +413,10 @@ namespace Avalonia.Win32 return IntPtr.Zero; } + case WindowsMessage.WM_EXITSIZEMOVE: + _resizeReason = PlatformResizeReason.Unspecified; + break; + case WindowsMessage.WM_MOVE: { PositionChanged?.Invoke(new PixelPoint((short)(ToInt32(lParam) & 0xffff), diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 646a6f5739..89ff58c7e2 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -53,6 +53,7 @@ namespace Avalonia.Win32 private double _extendTitleBarHint = -1; private bool _isUsingComposition; private IBlurHost _blurHost; + private PlatformResizeReason _resizeReason; #if USE_MANAGED_DRAG private readonly ManagedWindowResizeDragHelper _managedDrag; @@ -160,7 +161,7 @@ namespace Avalonia.Win32 public Action Paint { get; set; } - public Action Resized { get; set; } + public Action Resized { get; set; } public Action ScalingChanged { get; set; } @@ -482,7 +483,7 @@ namespace Avalonia.Win32 : new ImmediateRenderer(root); } - public void Resize(Size value) + public void Resize(Size value, PlatformResizeReason reason) { int requestedClientWidth = (int)(value.Width * RenderScaling); int requestedClientHeight = (int)(value.Height * RenderScaling); @@ -494,6 +495,7 @@ namespace Avalonia.Win32 { GetWindowRect(_hwnd, out var windowRect); + using var scope = SetResizeReason(reason); SetWindowPos( _hwnd, IntPtr.Zero, @@ -930,7 +932,7 @@ namespace Avalonia.Win32 _offScreenMargin = new Thickness(); _extendedMargins = new Thickness(); - Resize(new Size(rcWindow.Width/ RenderScaling, rcWindow.Height / RenderScaling)); + Resize(new Size(rcWindow.Width/ RenderScaling, rcWindow.Height / RenderScaling), PlatformResizeReason.Layout); } if(!_isClientAreaExtended || (_extendChromeHints.HasAllFlags(ExtendClientAreaChromeHints.SystemChrome) && @@ -1311,6 +1313,13 @@ namespace Avalonia.Win32 /// public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 0.8, 0); + private ResizeReasonScope SetResizeReason(PlatformResizeReason reason) + { + var old = _resizeReason; + _resizeReason = reason; + return new ResizeReasonScope(this, old); + } + private struct SavedWindowInfo { public WindowStyles Style { get; set; } @@ -1325,5 +1334,19 @@ namespace Avalonia.Win32 public SystemDecorations Decorations; public bool IsFullScreen; } + + private struct ResizeReasonScope : IDisposable + { + private readonly WindowImpl _owner; + private readonly PlatformResizeReason _restore; + + public ResizeReasonScope(WindowImpl owner, PlatformResizeReason restore) + { + _owner = owner; + _restore = restore; + } + + public void Dispose() => _owner._resizeReason = _restore; + } } } diff --git a/src/iOS/Avalonia.iOS/AvaloniaView.cs b/src/iOS/Avalonia.iOS/AvaloniaView.cs index 0371a7759a..bf990a51ce 100644 --- a/src/iOS/Avalonia.iOS/AvaloniaView.cs +++ b/src/iOS/Avalonia.iOS/AvaloniaView.cs @@ -96,7 +96,7 @@ namespace Avalonia.iOS public IEnumerable Surfaces { get; set; } public Action Input { get; set; } public Action Paint { get; set; } - public Action Resized { get; set; } + public Action Resized { get; set; } public Action ScalingChanged { get; set; } public Action TransparencyLevelChanged { get; set; } public Action Closed { get; set; } diff --git a/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs b/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs index 6b30aed257..9c2d760733 100644 --- a/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs @@ -139,7 +139,7 @@ namespace Avalonia.Controls.UnitTests // The user has resized the window, so we can no longer auto-size. var target = new TestTopLevel(impl.Object); - impl.Object.Resized(new Size(100, 200)); + impl.Object.Resized(new Size(100, 200), PlatformResizeReason.Unspecified); Assert.Equal(100, target.Width); Assert.Equal(200, target.Height); diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index 6b9921d83d..eb128ef038 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -663,10 +663,11 @@ namespace Avalonia.Controls.UnitTests var clientSize = new Size(200, 200); var maxClientSize = new Size(480, 480); - windowImpl.Setup(x => x.Resize(It.IsAny())).Callback(size => + windowImpl.Setup(x => x.Resize(It.IsAny(), It.IsAny())) + .Callback((size, reason) => { clientSize = size.Constrain(maxClientSize); - windowImpl.Object.Resized?.Invoke(clientSize); + windowImpl.Object.Resized?.Invoke(clientSize, reason); }); windowImpl.Setup(x => x.ClientSize).Returns(() => clientSize); @@ -739,6 +740,36 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void SizeToContent_Should_Not_Be_Lost_On_Scaling_Change() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var child = new Canvas + { + Width = 209, + Height = 117, + }; + + var target = new Window() + { + SizeToContent = SizeToContent.WidthAndHeight, + Content = child + }; + + Show(target); + + // Size before and after DPI change is a real-world example, with size after DPI + // change coming from Win32 WM_DPICHANGED. + target.PlatformImpl.ScalingChanged(1.5); + target.PlatformImpl.Resized( + new Size(210.66666666666666, 118.66666666666667), + PlatformResizeReason.DpiChange); + + Assert.Equal(SizeToContent.WidthAndHeight, target.SizeToContent); + } + } + [Fact] public void Width_Height_Should_Be_Updated_When_SizeToContent_Is_WidthAndHeight() { @@ -791,8 +822,91 @@ namespace Avalonia.Controls.UnitTests target.LayoutManager.ExecuteLayoutPass(); var windowImpl = Mock.Get(target.PlatformImpl); - windowImpl.Verify(x => x.Resize(new Size(410, 800))); + windowImpl.Verify(x => x.Resize(new Size(410, 800), PlatformResizeReason.Application)); + Assert.Equal(410, target.Width); + } + } + + + [Fact] + public void User_Resize_Of_Window_Width_Should_Reset_SizeToContent() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var target = new Window() + { + SizeToContent = SizeToContent.WidthAndHeight, + Content = new Canvas + { + Width = 400, + Height = 800, + }, + }; + + Show(target); + Assert.Equal(400, target.Width); + Assert.Equal(800, target.Height); + + target.PlatformImpl.Resized(new Size(410, 800), PlatformResizeReason.User); + Assert.Equal(410, target.Width); + Assert.Equal(800, target.Height); + Assert.Equal(SizeToContent.Height, target.SizeToContent); + } + } + + [Fact] + public void User_Resize_Of_Window_Height_Should_Reset_SizeToContent() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var target = new Window() + { + SizeToContent = SizeToContent.WidthAndHeight, + Content = new Canvas + { + Width = 400, + Height = 800, + }, + }; + + Show(target); + Assert.Equal(400, target.Width); + Assert.Equal(800, target.Height); + + target.PlatformImpl.Resized(new Size(400, 810), PlatformResizeReason.User); + + Assert.Equal(400, target.Width); + Assert.Equal(810, target.Height); + Assert.Equal(SizeToContent.Width, target.SizeToContent); + } + } + + [Fact] + public void Window_Resize_Should_Not_Reset_SizeToContent_If_CanResize_False() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var target = new Window() + { + SizeToContent = SizeToContent.WidthAndHeight, + CanResize = false, + Content = new Canvas + { + Width = 400, + Height = 800, + }, + }; + + Show(target); + Assert.Equal(400, target.Width); + Assert.Equal(800, target.Height); + + target.PlatformImpl.Resized(new Size(410, 810), PlatformResizeReason.Unspecified); + + Assert.Equal(400, target.Width); + Assert.Equal(800, target.Height); + Assert.Equal(SizeToContent.WidthAndHeight, target.SizeToContent); } } diff --git a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs index 8a24a8366f..1beafb011c 100644 --- a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs +++ b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs @@ -52,10 +52,11 @@ namespace Avalonia.UnitTests windowImpl.Object.PositionChanged?.Invoke(x); }); - windowImpl.Setup(x => x.Resize(It.IsAny())).Callback(x => + windowImpl.Setup(x => x.Resize(It.IsAny(), It.IsAny())) + .Callback((x, y) => { clientSize = x.Constrain(s_screenSize); - windowImpl.Object.Resized?.Invoke(clientSize); + windowImpl.Object.Resized?.Invoke(clientSize, y); }); windowImpl.Setup(x => x.Show(true)).Callback(() => @@ -75,7 +76,7 @@ namespace Avalonia.UnitTests { clientSize = size.Constrain(s_screenSize); popupImpl.Object.PositionChanged?.Invoke(pos); - popupImpl.Object.Resized?.Invoke(clientSize); + popupImpl.Object.Resized?.Invoke(clientSize, PlatformResizeReason.Unspecified); }); var positioner = new ManagedPopupPositioner(positionerHelper); From 21a96b16f588eb78d9682ffdcfed158a15392dea Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 14 Jul 2021 11:28:51 +0200 Subject: [PATCH 066/127] Update ApiCompatBaseline.txt --- src/Avalonia.Controls/ApiCompatBaseline.txt | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/ApiCompatBaseline.txt b/src/Avalonia.Controls/ApiCompatBaseline.txt index bb133900d9..78fe2fc6d1 100644 --- a/src/Avalonia.Controls/ApiCompatBaseline.txt +++ b/src/Avalonia.Controls/ApiCompatBaseline.txt @@ -29,13 +29,27 @@ MembersMustExist : Member 'public void Avalonia.Controls.NumericUpDownValueChang MembersMustExist : Member 'public System.Double Avalonia.Controls.NumericUpDownValueChangedEventArgs.NewValue.get()' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public System.Double Avalonia.Controls.NumericUpDownValueChangedEventArgs.OldValue.get()' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.StyledProperty Avalonia.StyledProperty Avalonia.Controls.ScrollViewer.AllowAutoHideProperty' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'protected void Avalonia.Controls.TopLevel.HandleResized(Avalonia.Size)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.AvaloniaProperty Avalonia.AvaloniaProperty Avalonia.Controls.Viewbox.StretchProperty' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'protected void Avalonia.Controls.Window.HandleResized(Avalonia.Size)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'protected void Avalonia.Controls.WindowBase.HandleResized(Avalonia.Size)' does not exist in the implementation but it does exist 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. MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.AvaloniaProperty Avalonia.AvaloniaProperty Avalonia.Controls.Notifications.NotificationCard.CloseOnClickProperty' does not exist in the implementation but it does exist in the contract. EnumValuesMustMatch : Enum value 'Avalonia.Platform.ExtendClientAreaChromeHints Avalonia.Platform.ExtendClientAreaChromeHints.Default' is (System.Int32)2 in the implementation but (System.Int32)1 in the contract. InterfacesShouldHaveSameMembers : Interface member 'public System.Nullable Avalonia.Platform.ITopLevelImpl.FrameSize' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public System.Nullable Avalonia.Platform.ITopLevelImpl.FrameSize.get()' is present in the implementation but not in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public System.Action Avalonia.Platform.ITopLevelImpl.Resized.get()' is present in the implementation but not in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public System.Action Avalonia.Platform.ITopLevelImpl.Resized.get()' is present in the contract but not in the implementation. +MembersMustExist : Member 'public System.Action Avalonia.Platform.ITopLevelImpl.Resized.get()' does not exist in the implementation but it does exist in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.Resized.set(System.Action)' is present in the implementation but not in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.Resized.set(System.Action)' is present in the contract but not in the implementation. +MembersMustExist : Member 'public void Avalonia.Platform.ITopLevelImpl.Resized.set(System.Action)' does not exist in the implementation but it does exist in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.ICursorImpl)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' is present in the contract but not in the implementation. MembersMustExist : Member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract. -Total Issues: 39 +InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IWindowImpl.Resize(Avalonia.Size)' is present in the contract but not in the implementation. +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. +Total Issues: 53 From 8b3afa18d098e39e85b472dc02ac6d79cca869b6 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 14 Jul 2021 11:44:36 +0200 Subject: [PATCH 067/127] Update mobile backends with changed API. --- .../Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs | 4 ++-- src/iOS/Avalonia.iOS/AvaloniaView.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index a72742580c..0afb1db141 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -67,7 +67,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform public Action Paint { get; set; } - public Action Resized { get; set; } + public Action Resized { get; set; } public Action ScalingChanged { get; set; } @@ -134,7 +134,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform protected virtual void OnResized(Size size) { - Resized?.Invoke(size); + Resized?.Invoke(size, PlatformResizeReason.Unspecified); } class ViewImpl : InvalidationAwareSurfaceView, ISurfaceHolderCallback, IInitEditorInfo diff --git a/src/iOS/Avalonia.iOS/AvaloniaView.cs b/src/iOS/Avalonia.iOS/AvaloniaView.cs index bf990a51ce..5bb2f64879 100644 --- a/src/iOS/Avalonia.iOS/AvaloniaView.cs +++ b/src/iOS/Avalonia.iOS/AvaloniaView.cs @@ -127,7 +127,7 @@ namespace Avalonia.iOS public override void LayoutSubviews() { - _topLevelImpl.Resized?.Invoke(_topLevelImpl.ClientSize); + _topLevelImpl.Resized?.Invoke(_topLevelImpl.ClientSize, PlatformResizeReason.Layout); base.LayoutSubviews(); } From ea269261a7a7bc1bf725ba2e09cabf2fc6903509 Mon Sep 17 00:00:00 2001 From: dvdsvdv vrfsv Date: Thu, 15 Jul 2021 13:15:42 +0300 Subject: [PATCH 068/127] match OSX behavior with Win behavior --- src/Avalonia.Native/WindowImpl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Native/WindowImpl.cs b/src/Avalonia.Native/WindowImpl.cs index f3b60f07be..f740be44a2 100644 --- a/src/Avalonia.Native/WindowImpl.cs +++ b/src/Avalonia.Native/WindowImpl.cs @@ -114,7 +114,7 @@ namespace Avalonia.Native { var visual = (_inputRoot as Window).Renderer.HitTestFirst(e.Position, _inputRoot as Window, x => { - if (x is IInputElement ie && !ie.IsHitTestVisible) + if (x is IInputElement ie && (!ie.IsHitTestVisible || !ie.IsVisible)) { return false; } From a9affd64bf67d2ff39e3853b7f0adc168be71f3f Mon Sep 17 00:00:00 2001 From: Luis von der Eltz Date: Thu, 15 Jul 2021 15:46:47 +0200 Subject: [PATCH 069/127] Add support for Flyouts, ToolTips & ContextMenus --- .../Diagnostics/IPopupHostProvider.cs | 23 +++++ .../Diagnostics/ToolTipDiagnostics.cs | 12 +++ src/Avalonia.Controls/Flyouts/FlyoutBase.cs | 25 ++++- src/Avalonia.Controls/ToolTip.cs | 40 +++++--- .../Diagnostics/ViewModels/TreeNode.cs | 11 ++- .../Diagnostics/ViewModels/VisualTreeNode.cs | 97 ++++++++++++++----- 6 files changed, 163 insertions(+), 45 deletions(-) create mode 100644 src/Avalonia.Controls/Diagnostics/IPopupHostProvider.cs create mode 100644 src/Avalonia.Controls/Diagnostics/ToolTipDiagnostics.cs diff --git a/src/Avalonia.Controls/Diagnostics/IPopupHostProvider.cs b/src/Avalonia.Controls/Diagnostics/IPopupHostProvider.cs new file mode 100644 index 0000000000..11d3b1792a --- /dev/null +++ b/src/Avalonia.Controls/Diagnostics/IPopupHostProvider.cs @@ -0,0 +1,23 @@ +using System; +using Avalonia.Controls.Primitives; + +#nullable enable + +namespace Avalonia.Controls.Diagnostics +{ + /// + /// Diagnostics interface to retrieve an associated . + /// + public interface IPopupHostProvider + { + /// + /// The popup host. + /// + IPopupHost? PopupHost { get; } + + /// + /// Raised when the popup host changes. + /// + event Action? PopupHostChanged; + } +} diff --git a/src/Avalonia.Controls/Diagnostics/ToolTipDiagnostics.cs b/src/Avalonia.Controls/Diagnostics/ToolTipDiagnostics.cs new file mode 100644 index 0000000000..58174b1039 --- /dev/null +++ b/src/Avalonia.Controls/Diagnostics/ToolTipDiagnostics.cs @@ -0,0 +1,12 @@ +#nullable enable + +namespace Avalonia.Controls.Diagnostics +{ + /// + /// Helper class to provide some diagnostics insides into . + /// + public static class ToolTipDiagnostics + { + public static AvaloniaProperty ToolTipProperty = ToolTip.ToolTipProperty; + } +} diff --git a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs index e4b68c62fd..6b72b8c887 100644 --- a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs +++ b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs @@ -1,16 +1,16 @@ using System; using System.ComponentModel; +using Avalonia.Controls.Diagnostics; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Layout; using Avalonia.Logging; -using Avalonia.Rendering; #nullable enable namespace Avalonia.Controls.Primitives { - public abstract class FlyoutBase : AvaloniaObject + public abstract class FlyoutBase : AvaloniaObject, IPopupHostProvider { static FlyoutBase() { @@ -55,6 +55,7 @@ namespace Avalonia.Controls.Primitives private Rect? _enlargedPopupRect; private PixelRect? _enlargePopupRectScreenPixelRect; private IDisposable? _transientDisposable; + private Action? _popupHostChangedHandler; protected Popup? Popup { get; private set; } @@ -94,6 +95,14 @@ namespace Avalonia.Controls.Primitives private set => SetAndRaise(TargetProperty, ref _target, value); } + IPopupHost? IPopupHostProvider.PopupHost => Popup?.Host; + + event Action? IPopupHostProvider.PopupHostChanged + { + add => _popupHostChangedHandler += value; + remove => _popupHostChangedHandler -= value; + } + public event EventHandler? Closed; public event EventHandler? Closing; public event EventHandler? Opened; @@ -322,9 +331,11 @@ namespace Avalonia.Controls.Primitives private void InitPopup() { - Popup = new Popup(); - Popup.WindowManagerAddShadowHint = false; - Popup.IsLightDismissEnabled = true; + Popup = new Popup + { + WindowManagerAddShadowHint = false, + IsLightDismissEnabled = true + }; Popup.Opened += OnPopupOpened; Popup.Closed += OnPopupClosed; @@ -333,11 +344,15 @@ namespace Avalonia.Controls.Primitives private void OnPopupOpened(object sender, EventArgs e) { IsOpen = true; + + _popupHostChangedHandler?.Invoke(Popup!.Host); } private void OnPopupClosed(object sender, EventArgs e) { HideCore(); + + _popupHostChangedHandler?.Invoke(null); } private void PositionPopup(bool showAtPointer) diff --git a/src/Avalonia.Controls/ToolTip.cs b/src/Avalonia.Controls/ToolTip.cs index ab507d07a2..ab310d60ef 100644 --- a/src/Avalonia.Controls/ToolTip.cs +++ b/src/Avalonia.Controls/ToolTip.cs @@ -1,9 +1,8 @@ #nullable enable using System; -using System.Reactive.Linq; +using Avalonia.Controls.Diagnostics; using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; -using Avalonia.VisualTree; namespace Avalonia.Controls { @@ -17,7 +16,7 @@ namespace Avalonia.Controls /// assigning the content that you want displayed. /// [PseudoClasses(":open")] - public class ToolTip : ContentControl + public class ToolTip : ContentControl, IPopupHostProvider { /// /// Defines the ToolTip.Tip attached property. @@ -61,7 +60,8 @@ namespace Avalonia.Controls internal static readonly AttachedProperty ToolTipProperty = AvaloniaProperty.RegisterAttached("ToolTip"); - private IPopupHost? _popup; + private IPopupHost? _popupHost; + private Action? _popupHostChangedHandler; /// /// Initializes static members of the class. @@ -251,35 +251,45 @@ namespace Avalonia.Controls tooltip.RecalculatePosition(control); } + + IPopupHost? IPopupHostProvider.PopupHost => _popupHost; + + event Action? IPopupHostProvider.PopupHostChanged + { + add => _popupHostChangedHandler += value; + remove => _popupHostChangedHandler -= value; + } internal void RecalculatePosition(Control control) { - _popup?.ConfigurePosition(control, GetPlacement(control), new Point(GetHorizontalOffset(control), GetVerticalOffset(control))); + _popupHost?.ConfigurePosition(control, GetPlacement(control), new Point(GetHorizontalOffset(control), GetVerticalOffset(control))); } private void Open(Control control) { Close(); - _popup = OverlayPopupHost.CreatePopupHost(control, null); - _popup.SetChild(this); - ((ISetLogicalParent)_popup).SetParent(control); + _popupHost = OverlayPopupHost.CreatePopupHost(control, null); + _popupHost.SetChild(this); + ((ISetLogicalParent)_popupHost).SetParent(control); - _popup.ConfigurePosition(control, GetPlacement(control), + _popupHost.ConfigurePosition(control, GetPlacement(control), new Point(GetHorizontalOffset(control), GetVerticalOffset(control))); - WindowManagerAddShadowHintChanged(_popup, false); + WindowManagerAddShadowHintChanged(_popupHost, false); - _popup.Show(); + _popupHost.Show(); + _popupHostChangedHandler?.Invoke(_popupHost); } private void Close() { - if (_popup != null) + if (_popupHost != null) { - _popup.SetChild(null); - _popup.Dispose(); - _popup = null; + _popupHost.SetChild(null); + _popupHost.Dispose(); + _popupHost = null; + _popupHostChangedHandler?.Invoke(null); } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs index f4c04dbca6..9667751e54 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs @@ -4,6 +4,7 @@ using System.Collections.Specialized; using System.Reactive; using System.Reactive.Linq; using Avalonia.Controls; +using Avalonia.Controls.Diagnostics; using Avalonia.Controls.Primitives; using Avalonia.LogicalTree; using Avalonia.Media; @@ -19,12 +20,11 @@ namespace Avalonia.Diagnostics.ViewModels protected TreeNode(IVisual visual, TreeNode? parent) { + _classes = string.Empty; Parent = parent; Type = visual.GetType().Name; Visual = visual; - _classes = string.Empty; - - FontWeight = Visual is TopLevel || Visual is Popup ? FontWeight.Bold : FontWeight.Normal; + FontWeight = IsRoot ? FontWeight.Bold : FontWeight.Normal; if (visual is IControl control) { @@ -56,6 +56,11 @@ namespace Avalonia.Diagnostics.ViewModels } } + private bool IsRoot => Visual is TopLevel || + Visual is Popup || + Visual is ContextMenu || + Visual is IPopupHost; + public FontWeight FontWeight { get; } public abstract TreeNodeCollection Children diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs index b9981babf7..6d803a1a7b 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs @@ -1,5 +1,10 @@ using System; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Security.Cryptography.X509Certificates; using Avalonia.Collections; +using Avalonia.Controls; +using Avalonia.Controls.Diagnostics; using Avalonia.Controls.Primitives; using Avalonia.Styling; using Avalonia.VisualTree; @@ -13,7 +18,7 @@ namespace Avalonia.Diagnostics.ViewModels { Children = new VisualTreeNodeCollection(this, visual); - if ((Visual is IStyleable styleable)) + if (Visual is IStyleable styleable) { IsInTemplate = styleable.TemplatedParent != null; } @@ -25,13 +30,15 @@ namespace Avalonia.Diagnostics.ViewModels public static VisualTreeNode[] Create(object control) { - return control is IVisual visual ? new[] { new VisualTreeNode(visual, null) } : Array.Empty(); + return control is IVisual visual ? + new[] { new VisualTreeNode(visual, null) } : + Array.Empty(); } internal class VisualTreeNodeCollection : TreeNodeCollection { private readonly IVisual _control; - private IDisposable? _subscription; + private readonly CompositeDisposable _subscriptions = new CompositeDisposable(2); public VisualTreeNodeCollection(TreeNode owner, IVisual control) : base(owner) @@ -41,33 +48,79 @@ namespace Avalonia.Diagnostics.ViewModels public override void Dispose() { - _subscription?.Dispose(); + _subscriptions.Dispose(); } - protected override void Initialize(AvaloniaList nodes) + private static IObservable? GetHostedPopupRootObservable(IVisual visual) { - if (_control is Popup p) + static IObservable GetPopupHostObservable(IPopupHostProvider popupHostProvider) { - _subscription = p.GetObservable(Popup.ChildProperty).Subscribe(child => - { - if (child != null) - { - nodes.Add(new VisualTreeNode(child, Owner)); - } - else - { - nodes.Clear(); - } - - }); + return Observable.FromEvent( + x => popupHostProvider.PopupHostChanged += x, + x => popupHostProvider.PopupHostChanged -= x) + .StartWith(popupHostProvider.PopupHost) + .Select(x => x is IControl c ? c : null); } - else + + return visual switch { - _subscription = _control.VisualChildren.ForEachItem( + Popup p => p.GetObservable(Popup.ChildProperty), + Control c => Observable.CombineLatest( + c.GetObservable(Control.ContextFlyoutProperty), + c.GetObservable(Control.ContextMenuProperty), + c.GetObservable(FlyoutBase.AttachedFlyoutProperty), + c.GetObservable(ToolTipDiagnostics.ToolTipProperty), + (ContextFlyout, ContextMenu, AttachedFlyout, ToolTip) => + { + if (ContextMenu != null) + { + //Note: ContextMenus are special since all the items are added as visual children. + //So we don't need to go via Popup + return Observable.Return(ContextMenu); + } + + if ((ContextFlyout ?? (IPopupHostProvider?) AttachedFlyout ?? ToolTip) is { } popupHostProvider) + { + return GetPopupHostObservable(popupHostProvider); + } + + return Observable.Return(null); + }) + .Switch(), + _ => null + }; + } + + protected override void Initialize(AvaloniaList nodes) + { + _subscriptions.Clear(); + + if (GetHostedPopupRootObservable(_control) is { } popupRootObservable) + { + VisualTreeNode? childNode = null; + + _subscriptions.Add( + popupRootObservable + .Subscribe(root => + { + if (root != null) + { + childNode = new VisualTreeNode(root, Owner); + + nodes.Add(childNode); + } + else if (childNode != null) + { + nodes.Remove(childNode); + } + })); + } + + _subscriptions.Add( + _control.VisualChildren.ForEachItem( (i, item) => nodes.Insert(i, new VisualTreeNode(item, Owner)), (i, item) => nodes.RemoveAt(i), - () => nodes.Clear()); - } + () => nodes.Clear())); } } } From 4d9131091e04db53b53622238ade11f635a751e1 Mon Sep 17 00:00:00 2001 From: Luis von der Eltz Date: Thu, 15 Jul 2021 16:19:56 +0200 Subject: [PATCH 070/127] Add custom names to PopupRoot --- src/Avalonia.Controls/Primitives/Popup.cs | 17 ++++- .../Diagnostics/ViewModels/TreeNode.cs | 6 +- .../Diagnostics/ViewModels/VisualTreeNode.cs | 72 ++++++++++++------- 3 files changed, 65 insertions(+), 30 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index b445de0472..bdc71f9a62 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -1,6 +1,6 @@ using System; -using System.Linq; using System.Reactive.Disposables; +using Avalonia.Controls.Diagnostics; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives.PopupPositioning; using Avalonia.Input; @@ -17,7 +17,7 @@ namespace Avalonia.Controls.Primitives /// /// Displays a popup window. /// - public class Popup : Control, IVisualTreeHost + public class Popup : Control, IVisualTreeHost, IPopupHostProvider { public static readonly StyledProperty WindowManagerAddShadowHintProperty = AvaloniaProperty.Register(nameof(WindowManagerAddShadowHint), true); @@ -133,6 +133,7 @@ namespace Avalonia.Controls.Primitives private bool _ignoreIsOpenChanged; private PopupOpenState? _openState; private IInputElement _overlayInputPassThroughElement; + private Action? _popupHostChangedHandler; /// /// Initializes static members of the class. @@ -348,6 +349,14 @@ namespace Avalonia.Controls.Primitives /// IVisual? IVisualTreeHost.Root => _openState?.PopupHost.HostedVisualTreeRoot; + IPopupHost? IPopupHostProvider.PopupHost => Host; + + event Action? IPopupHostProvider.PopupHostChanged + { + add => _popupHostChangedHandler += value; + remove => _popupHostChangedHandler -= value; + } + /// /// Opens the popup. /// @@ -479,6 +488,8 @@ namespace Avalonia.Controls.Primitives } Opened?.Invoke(this, EventArgs.Empty); + + _popupHostChangedHandler?.Invoke(Host); } /// @@ -581,6 +592,8 @@ namespace Avalonia.Controls.Primitives _openState.Dispose(); _openState = null; + _popupHostChangedHandler?.Invoke(null); + using (BeginIgnoringIsOpen()) { IsOpen = false; diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs index 9667751e54..4b957c2382 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs @@ -1,10 +1,8 @@ using System; -using System.Collections.Generic; using System.Collections.Specialized; using System.Reactive; using System.Reactive.Linq; using Avalonia.Controls; -using Avalonia.Controls.Diagnostics; using Avalonia.Controls.Primitives; using Avalonia.LogicalTree; using Avalonia.Media; @@ -18,11 +16,11 @@ namespace Avalonia.Diagnostics.ViewModels private string _classes; private bool _isExpanded; - protected TreeNode(IVisual visual, TreeNode? parent) + protected TreeNode(IVisual visual, TreeNode? parent, string? customName = null) { _classes = string.Empty; Parent = parent; - Type = visual.GetType().Name; + Type = customName ?? visual.GetType().Name; Visual = visual; FontWeight = IsRoot ? FontWeight.Bold : FontWeight.Normal; diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs index 6d803a1a7b..f03f16d18f 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs @@ -1,7 +1,7 @@ using System; +using System.Linq; using System.Reactive.Disposables; using System.Reactive.Linq; -using System.Security.Cryptography.X509Certificates; using Avalonia.Collections; using Avalonia.Controls; using Avalonia.Controls.Diagnostics; @@ -13,18 +13,15 @@ namespace Avalonia.Diagnostics.ViewModels { internal class VisualTreeNode : TreeNode { - public VisualTreeNode(IVisual visual, TreeNode? parent) - : base(visual, parent) + public VisualTreeNode(IVisual visual, TreeNode? parent, string? customName = null) + : base(visual, parent, customName) { Children = new VisualTreeNodeCollection(this, visual); - if (Visual is IStyleable styleable) - { - IsInTemplate = styleable.TemplatedParent != null; - } + if (Visual is IStyleable styleable) IsInTemplate = styleable.TemplatedParent != null; } - public bool IsInTemplate { get; private set; } + public bool IsInTemplate { get; } public override TreeNodeCollection Children { get; } @@ -51,20 +48,30 @@ namespace Avalonia.Diagnostics.ViewModels _subscriptions.Dispose(); } - private static IObservable? GetHostedPopupRootObservable(IVisual visual) + private static IObservable? GetHostedPopupRootObservable(IVisual visual) { - static IObservable GetPopupHostObservable(IPopupHostProvider popupHostProvider) + static IObservable GetPopupHostObservable( + IPopupHostProvider popupHostProvider, + string? providerName = null) { return Observable.FromEvent( x => popupHostProvider.PopupHostChanged += x, x => popupHostProvider.PopupHostChanged -= x) .StartWith(popupHostProvider.PopupHost) - .Select(x => x is IControl c ? c : null); + .Select(popupHost => + { + if (popupHost is IControl control) + return new PopupRoot( + control, + providerName != null ? $"{providerName} ({control.GetType().Name})" : null); + + return (PopupRoot?)null; + }); } return visual switch { - Popup p => p.GetObservable(Popup.ChildProperty), + Popup p => GetPopupHostObservable(p), Control c => Observable.CombineLatest( c.GetObservable(Control.ContextFlyoutProperty), c.GetObservable(Control.ContextMenuProperty), @@ -73,18 +80,20 @@ namespace Avalonia.Diagnostics.ViewModels (ContextFlyout, ContextMenu, AttachedFlyout, ToolTip) => { if (ContextMenu != null) - { //Note: ContextMenus are special since all the items are added as visual children. //So we don't need to go via Popup - return Observable.Return(ContextMenu); - } + return Observable.Return(new PopupRoot(ContextMenu)); - if ((ContextFlyout ?? (IPopupHostProvider?) AttachedFlyout ?? ToolTip) is { } popupHostProvider) - { - return GetPopupHostObservable(popupHostProvider); - } + if (ContextFlyout != null) + return GetPopupHostObservable(ContextFlyout, "ContextFlyout"); + + if (AttachedFlyout != null) + return GetPopupHostObservable(AttachedFlyout, "AttachedFlyout"); + + if (ToolTip != null) + return GetPopupHostObservable(ToolTip, "ToolTip"); - return Observable.Return(null); + return Observable.Return(null); }) .Switch(), _ => null @@ -101,18 +110,21 @@ namespace Avalonia.Diagnostics.ViewModels _subscriptions.Add( popupRootObservable - .Subscribe(root => + .Subscribe(popupRoot => { - if (root != null) + if (popupRoot != null) { - childNode = new VisualTreeNode(root, Owner); + childNode = new VisualTreeNode( + popupRoot.Value.Root, + Owner, + popupRoot.Value.CustomName); nodes.Add(childNode); } else if (childNode != null) { nodes.Remove(childNode); - } + }s })); } @@ -122,6 +134,18 @@ namespace Avalonia.Diagnostics.ViewModels (i, item) => nodes.RemoveAt(i), () => nodes.Clear())); } + + private struct PopupRoot + { + public PopupRoot(IControl root, string? customName = null) + { + Root = root; + CustomName = customName; + } + + public IControl Root { get; } + public string? CustomName { get; } + } } } } From dfc5669507d6910c510a4a7619f377e0b02a5100 Mon Sep 17 00:00:00 2001 From: Luis von der Eltz Date: Thu, 15 Jul 2021 16:20:22 +0200 Subject: [PATCH 071/127] Typo --- .../Diagnostics/ViewModels/VisualTreeNode.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs index f03f16d18f..5524d06f1b 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs @@ -124,7 +124,7 @@ namespace Avalonia.Diagnostics.ViewModels else if (childNode != null) { nodes.Remove(childNode); - }s + } })); } From 650ae63bd641868decb2bce426a21aa88ad94496 Mon Sep 17 00:00:00 2001 From: Luis von der Eltz Date: Thu, 15 Jul 2021 17:46:00 +0200 Subject: [PATCH 072/127] make "ctrl+shift+click" work --- src/Avalonia.Controls/ContextMenu.cs | 17 ++++++- .../Diagnostics/ViewModels/VisualTreeNode.cs | 4 +- .../Diagnostics/Views/MainWindow.xaml.cs | 46 +++++++++++++++---- 3 files changed, 55 insertions(+), 12 deletions(-) diff --git a/src/Avalonia.Controls/ContextMenu.cs b/src/Avalonia.Controls/ContextMenu.cs index ead5b0d9f3..0c8fc31df1 100644 --- a/src/Avalonia.Controls/ContextMenu.cs +++ b/src/Avalonia.Controls/ContextMenu.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; - +using Avalonia.Controls.Diagnostics; using Avalonia.Controls.Generators; using Avalonia.Controls.Platform; using Avalonia.Controls.Primitives; @@ -21,7 +21,7 @@ namespace Avalonia.Controls /// /// A control context menu. /// - public class ContextMenu : MenuBase, ISetterValue + public class ContextMenu : MenuBase, ISetterValue, IPopupHostProvider { /// /// Defines the property. @@ -82,6 +82,7 @@ namespace Avalonia.Controls private Popup? _popup; private List? _attachedControls; private IInputElement? _previousFocus; + private Action? _popupHostChangedHandler; /// /// Initializes a new instance of the class. @@ -304,6 +305,14 @@ namespace Avalonia.Controls } } + IPopupHost? IPopupHostProvider.PopupHost => _popup?.Host; + + event Action? IPopupHostProvider.PopupHostChanged + { + add => _popupHostChangedHandler += value; + remove => _popupHostChangedHandler -= value; + } + protected override IItemContainerGenerator CreateItemContainerGenerator() { return new MenuItemContainerGenerator(this); @@ -364,6 +373,8 @@ namespace Avalonia.Controls { _previousFocus = FocusManager.Instance?.Current; Focus(); + + _popupHostChangedHandler?.Invoke(_popup!.Host); } private void PopupClosing(object sender, CancelEventArgs e) @@ -397,6 +408,8 @@ namespace Avalonia.Controls RoutedEvent = MenuClosedEvent, Source = this, }); + + _popupHostChangedHandler?.Invoke(null); } private void PopupKeyUp(object sender, KeyEventArgs e) diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs index 5524d06f1b..6a430897ba 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs @@ -1,5 +1,4 @@ using System; -using System.Linq; using System.Reactive.Disposables; using System.Reactive.Linq; using Avalonia.Collections; @@ -18,7 +17,8 @@ namespace Avalonia.Diagnostics.ViewModels { Children = new VisualTreeNodeCollection(this, visual); - if (Visual is IStyleable styleable) IsInTemplate = styleable.TemplatedParent != null; + if (Visual is IStyleable styleable) + IsInTemplate = styleable.TemplatedParent != null; } public bool IsInTemplate { get; } diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs index 9d37b5acdc..26e197d790 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs @@ -1,7 +1,9 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Reactive.Linq; using Avalonia.Controls; +using Avalonia.Controls.Diagnostics; using Avalonia.Controls.Primitives; using Avalonia.Diagnostics.ViewModels; using Avalonia.Input; @@ -109,6 +111,37 @@ namespace Avalonia.Diagnostics.Views .FirstOrDefault(); } + private static IEnumerable GetPopupRoots(IVisual root) + { + foreach (var control in root.GetVisualDescendants().OfType()) + { + if (control is Popup { Host: PopupRoot r0 }) + { + yield return r0; + } + + if (control.GetValue(ContextFlyoutProperty) is IPopupHostProvider { PopupHost: PopupRoot r1 }) + { + yield return r1; + } + + if (control.GetValue(FlyoutBase.AttachedFlyoutProperty) is IPopupHostProvider { PopupHost: PopupRoot r2 }) + { + yield return r2; + } + + if (control.GetValue(ToolTipDiagnostics.ToolTipProperty) is IPopupHostProvider { PopupHost: PopupRoot r3 }) + { + yield return r3; + } + + if (control.GetValue(ContextMenuProperty) is IPopupHostProvider { PopupHost: PopupRoot r4 }) + { + yield return r4; + } + } + } + private void RawKeyDown(RawKeyEventArgs e) { var vm = (MainViewModel?)DataContext; @@ -123,16 +156,13 @@ namespace Avalonia.Diagnostics.Views { IControl? control = null; - foreach (var popup in Root.GetVisualDescendants().OfType()) + foreach (var popupRoot in GetPopupRoots(Root)) { - if (popup.Host?.HostedVisualTreeRoot is PopupRoot popupRoot) - { - control = GetHoveredControl(popupRoot); + control = GetHoveredControl(popupRoot); - if (control != null) - { - break; - } + if (control != null) + { + break; } } From 838626d763a9e446844244636c3cc51d55cbd946 Mon Sep 17 00:00:00 2001 From: Luis von der Eltz Date: Fri, 16 Jul 2021 10:33:51 +0200 Subject: [PATCH 073/127] remove c#9 features, clean up --- .../Diagnostics/Views/MainWindow.xaml.cs | 38 +++++++++---------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs index 26e197d790..cfb9e2ead9 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs @@ -111,35 +111,33 @@ namespace Avalonia.Diagnostics.Views .FirstOrDefault(); } - private static IEnumerable GetPopupRoots(IVisual root) + private static List GetPopupRoots(IVisual root) { - foreach (var control in root.GetVisualDescendants().OfType()) - { - if (control is Popup { Host: PopupRoot r0 }) - { - yield return r0; - } - - if (control.GetValue(ContextFlyoutProperty) is IPopupHostProvider { PopupHost: PopupRoot r1 }) - { - yield return r1; - } + var popupRoots = new List(); - if (control.GetValue(FlyoutBase.AttachedFlyoutProperty) is IPopupHostProvider { PopupHost: PopupRoot r2 }) + void ProcessProperty(IControl control, AvaloniaProperty property) + { + if (control.GetValue(property) is IPopupHostProvider popupProvider + && popupProvider.PopupHost is PopupRoot popupRoot) { - yield return r2; + popupRoots.Add(popupRoot); } + } - if (control.GetValue(ToolTipDiagnostics.ToolTipProperty) is IPopupHostProvider { PopupHost: PopupRoot r3 }) + foreach (var control in root.GetVisualDescendants().OfType()) + { + if (control is Popup p && p.Host is PopupRoot popupRoot) { - yield return r3; + popupRoots.Add(popupRoot); } - if (control.GetValue(ContextMenuProperty) is IPopupHostProvider { PopupHost: PopupRoot r4 }) - { - yield return r4; - } + ProcessProperty(control, ContextFlyoutProperty); + ProcessProperty(control, ContextMenuProperty); + ProcessProperty(control, FlyoutBase.AttachedFlyoutProperty); + ProcessProperty(control, ToolTipDiagnostics.ToolTipProperty); } + + return popupRoots; } private void RawKeyDown(RawKeyEventArgs e) From f589965b2df0f391d752b3ae0f11f615aa1fb672 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 16 Jul 2021 16:47:32 +0200 Subject: [PATCH 074/127] OSX: Fix showing window with no specified size. A Window without a `Width`/`Height` specified was not getting shown on OSX since 1f8b90925771c64e54afa1a264938de0fd5e3c70. Set the content view in the constructor to fix this. --- native/Avalonia.Native/src/OSX/window.mm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 7df9b76425..3c1a370195 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -52,6 +52,7 @@ public: [Window setBackingType:NSBackingStoreBuffered]; [Window setOpaque:false]; + [Window setContentView: StandardContainer]; } virtual HRESULT ObtainNSWindowHandle(void** ret) override @@ -124,7 +125,6 @@ public: SetPosition(lastPositionSet); UpdateStyle(); - [Window setContentView: StandardContainer]; [Window setTitle:_lastTitle]; if(ShouldTakeFocusOnShow() && activate) From 9b0287bfd6e397f4150820defa0edf598214167a Mon Sep 17 00:00:00 2001 From: mat1jaczyyy Date: Sat, 17 Jul 2021 04:19:31 +0200 Subject: [PATCH 075/127] Fix existing and add missing osx-specific key shortcuts --- src/Avalonia.Native/AvaloniaNativePlatform.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs index 82a845ffc1..6ff5294d4a 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs @@ -109,10 +109,16 @@ namespace Avalonia.Native .Bind().ToConstant(new RenderLoop()) .Bind().ToConstant(new DefaultRenderTimer(60)) .Bind().ToConstant(new SystemDialogs(_factory.CreateSystemDialogs())) - .Bind().ToConstant(new PlatformHotkeyConfiguration(KeyModifiers.Meta)) + .Bind().ToConstant(new PlatformHotkeyConfiguration(KeyModifiers.Meta, wholeWordTextActionModifiers: KeyModifiers.Alt)) .Bind().ToConstant(new MacOSMountedVolumeInfoProvider()) .Bind().ToConstant(new AvaloniaNativeDragSource(_factory)); + var hotkeys = AvaloniaLocator.Current.GetService(); + hotkeys.MoveCursorToTheStartOfLine.Add(new KeyGesture(Key.Left, hotkeys.CommandModifiers)); + hotkeys.MoveCursorToTheStartOfLineWithSelection.Add(new KeyGesture(Key.Left, hotkeys.CommandModifiers | hotkeys.SelectionModifiers)); + hotkeys.MoveCursorToTheEndOfLine.Add(new KeyGesture(Key.Right, hotkeys.CommandModifiers)); + hotkeys.MoveCursorToTheEndOfLineWithSelection.Add(new KeyGesture(Key.Right, hotkeys.CommandModifiers | hotkeys.SelectionModifiers)); + if (_options.UseGpu) { try From c01c9ae7b662342b653bda4d01a720c57ae886e0 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Mon, 19 Jul 2021 12:19:57 +0200 Subject: [PATCH 076/127] Disable text search by default. Only ComboBox needs it enabled by default. --- src/Avalonia.Controls/ComboBox.cs | 1 + src/Avalonia.Controls/Primitives/SelectingItemsControl.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index 89cfb5fa8f..57c07916db 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -91,6 +91,7 @@ namespace Avalonia.Controls FocusableProperty.OverrideDefaultValue(true); SelectedItemProperty.Changed.AddClassHandler((x,e) => x.SelectedItemChanged(e)); KeyDownEvent.AddClassHandler((x, e) => x.OnKeyDown(e), Interactivity.RoutingStrategies.Tunnel); + IsTextSearchEnabledProperty.OverrideDefaultValue(true); } /// diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 2fd08ef77c..336ff711c0 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -96,7 +96,7 @@ namespace Avalonia.Controls.Primitives /// Defines the property. /// public static readonly StyledProperty IsTextSearchEnabledProperty = - AvaloniaProperty.Register(nameof(IsTextSearchEnabled), true); + AvaloniaProperty.Register(nameof(IsTextSearchEnabled), false); /// /// Event that should be raised by items that implement to From 0c80f885b764869c6a544f148efb3503d50d9218 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Mon, 19 Jul 2021 12:56:17 +0200 Subject: [PATCH 077/127] Implement indexed sort for visual children. --- .../Rendering/SceneGraph/SceneBuilder.cs | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs index f2a09b815e..43246c8e9d 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using Avalonia.Media; using Avalonia.Platform; using Avalonia.Threading; @@ -258,14 +259,25 @@ namespace Avalonia.Rendering.SceneGraph } else if (visualChildren.Count > 1) { - var sortedChildren = new IVisual[visualChildren.Count]; - visualChildren.CopyTo(sortedChildren, 0); + var count = visualChildren.Count; + var sortedChildren = new (IVisual visual, int index)[count]; - Array.Sort(sortedChildren, ZIndexComparer.ComparisonInstance); + for (var i = 0; i < count; i++) + { + sortedChildren[i] = (visualChildren[i], i); + } + + // Regular Array.Sort is unstable, we need to provide indices as well to avoid reshuffling elements. + Array.Sort(sortedChildren, (lhs, rhs) => + { + var result = ZIndexComparer.Instance.Compare(lhs.visual, rhs.visual); + + return result == 0 ? lhs.index.CompareTo(rhs.index) : result; + }); foreach (var child in sortedChildren) { - var childNode = GetOrCreateChildNode(scene, child, node); + var childNode = GetOrCreateChildNode(scene, child.Item1, node); Update(context, scene, (VisualNode)childNode, clip, forceRecurse); } } From c598b3883adb6f6908173ff18ca83465335955e3 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Mon, 19 Jul 2021 13:12:28 +0200 Subject: [PATCH 078/127] Add unit test that verifies ordering. --- .../Rendering/SceneGraph/SceneBuilderTests.cs | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs index 42e573c8a5..e317b43efc 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs @@ -245,6 +245,34 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph } } + [Fact] + public void Should_Respect_Uniform_ZIndex() + { + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) + { + Panel panel; + + var tree = new TestRoot + { + Child = panel = new Panel() + }; + + for (var i = 0; i < 128; i++) + { + panel.Children.Add(new Border()); + } + + var result = new Scene(tree); + var sceneBuilder = new SceneBuilder(); + sceneBuilder.UpdateAll(result); + + var panelNode = result.FindNode(tree.Child); + var expected = panel.Children.ToArray(); + var actual = panelNode.Children.OfType().Select(x => x.Visual).ToArray(); + Assert.Equal(expected, actual); + } + } + [Fact] public void ClipBounds_Should_Be_In_Global_Coordinates() { From 13a244fed53c36c091cb87d9fd1478dc8603d32c Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Mon, 19 Jul 2021 13:14:17 +0200 Subject: [PATCH 079/127] Remove usage. --- src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs index 43246c8e9d..c6cdf474bb 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using Avalonia.Media; using Avalonia.Platform; using Avalonia.Threading; From c45b5c042b8a0e28c6aaf34af9f0cd8bc0e93db5 Mon Sep 17 00:00:00 2001 From: dvdsvdv vrfsv Date: Mon, 19 Jul 2021 21:10:05 +0300 Subject: [PATCH 080/127] fix --- src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml b/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml index 0c4f8249ce..f2dbf42196 100644 --- a/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml +++ b/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml @@ -6,8 +6,8 @@ 0.6 0.8 - M1875 1011l-787 787v-1798h-128v1798l-787 -787l-90 90l941 941l941 -941z - M1965 947l-941 -941l-941 941l90 90l787 -787v1798h128v-1798l787 787z + M1875 1011l-787 787v-1798h-128v1798l-787 -787l-90 90l941 941l941 -941z + M1965 947l-941 -941l-941 941l90 90l787 -787v1798h128v-1798l787 787z M515 93l930 931l-930 931l90 90l1022 -1021l-1022 -1021z M1939 1581l90 -90l-1005 -1005l-1005 1005l90 90l915 -915z From 87ea2915f8e335daf2d96fd2993b4703e1ca87d6 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Tue, 20 Jul 2021 19:55:05 +0200 Subject: [PATCH 081/127] fixes(DataGrid): Fix binding IsVisible --- .../DataGridColumn.cs | 42 +++++++++++-------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridColumn.cs index 9141fb2463..5c09bab678 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridColumn.cs @@ -27,7 +27,6 @@ namespace Avalonia.Controls private double? _minWidth; private bool _settingWidthInternally; private int _displayIndexWithFiller; - private bool _isVisible; private object _header; private DataGridColumnHeader _headerCell; private IControl _editingElement; @@ -40,7 +39,6 @@ namespace Avalonia.Controls /// protected internal DataGridColumn() { - _isVisible = true; _displayIndexWithFiller = -1; IsInitialDesiredWidthDetermined = false; InheritsWidth = true; @@ -174,32 +172,42 @@ namespace Avalonia.Controls get => _editBinding; } + + /// + /// Defines the property. + /// + public static StyledProperty IsVisibleProperty = + Control.IsVisibleProperty.AddOwner(); + /// /// Determines whether or not this column is visible. /// public bool IsVisible { - get - { - return _isVisible; - } - set - { - if (value != IsVisible) - { - OwningGrid?.OnColumnVisibleStateChanging(this); - _isVisible = value; + get => GetValue(IsVisibleProperty); + set => SetValue(IsVisibleProperty, value); + } - if (_headerCell != null) - { - _headerCell.IsVisible = value; - } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); - OwningGrid?.OnColumnVisibleStateChanged(this); + if (change.Property == IsVisibleProperty) + { + OwningGrid?.OnColumnVisibleStateChanging(this); + var isVisible = (change as AvaloniaPropertyChangedEventArgs).NewValue.Value; + + if (_headerCell != null) + { + _headerCell.IsVisible = isVisible; } + + OwningGrid?.OnColumnVisibleStateChanged(this); + NotifyPropertyChanged(change.Property.Name); } } + /// /// Actual visible width after Width, MinWidth, and MaxWidth setting at the Column level and DataGrid level /// have been taken into account From b9fcee5c3fd6218c8bb7e97a801926406c90bb0b Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Tue, 20 Jul 2021 20:01:53 +0200 Subject: [PATCH 082/127] Sample: Add sample for binding DataGridColumn IsVisible --- .../ControlCatalog/Pages/DataGridPage.xaml | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/samples/ControlCatalog/Pages/DataGridPage.xaml b/samples/ControlCatalog/Pages/DataGridPage.xaml index 323eaa3463..820c1324e3 100644 --- a/samples/ControlCatalog/Pages/DataGridPage.xaml +++ b/samples/ControlCatalog/Pages/DataGridPage.xaml @@ -23,16 +23,21 @@ - - - - - - - - - - + + + + + + + + + + + + + From eeb593f041728a45e8195e75376c730b379783bf Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 20 Jul 2021 19:40:35 -0400 Subject: [PATCH 083/127] Remove TextBox margin hack --- src/Avalonia.Themes.Fluent/Controls/TextBox.xaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/TextBox.xaml b/src/Avalonia.Themes.Fluent/Controls/TextBox.xaml index 5db9398448..fca4a3bd45 100644 --- a/src/Avalonia.Themes.Fluent/Controls/TextBox.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/TextBox.xaml @@ -76,9 +76,7 @@ IsVisible="{TemplateBinding Text, Converter={x:Static StringConverters.IsNullOrEmpty}}" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/> - Date: Wed, 21 Jul 2021 18:34:14 +0200 Subject: [PATCH 084/127] Add option to freez popups (Alt + Ctrl + F) --- .../Diagnostics/ViewModels/MainViewModel.cs | 7 +++++++ .../Diagnostics/Views/MainWindow.xaml.cs | 17 +++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs index 07ae222a9c..d0a4ad38c5 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs @@ -21,6 +21,7 @@ namespace Avalonia.Diagnostics.ViewModels private bool _shouldVisualizeMarginPadding = true; private bool _shouldVisualizeDirtyRects; private bool _showFpsOverlay; + private bool _freezePopups; #nullable disable // Remove "nullable disable" after MemberNotNull will work on our CI. @@ -40,6 +41,12 @@ namespace Avalonia.Diagnostics.ViewModels Console = new ConsoleViewModel(UpdateConsoleContext); } + public bool FreezePopups + { + get => _freezePopups; + set => RaiseAndSetIfChanged(ref _freezePopups, value); + } + public bool ShouldVisualizeMarginPadding { get => _shouldVisualizeMarginPadding; diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs index cfb9e2ead9..810a630f46 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Reactive.Linq; +using System.Reactive.Subjects; using Avalonia.Controls; using Avalonia.Controls.Diagnostics; using Avalonia.Controls.Primitives; @@ -173,6 +174,22 @@ namespace Avalonia.Diagnostics.Views break; } + + case RawInputModifiers.Control | RawInputModifiers.Alt when e.Key == Key.F: + { + vm.FreezePopups = !vm.FreezePopups; + + foreach (var popupRoot in GetPopupRoots(Root)) + { + if (popupRoot.Parent is Popup popup) + { + popup.IsLightDismissEnabled = !vm.FreezePopups; + } + } + + break; + } + case RawInputModifiers.Alt when e.Key == Key.S || e.Key == Key.D: { vm.EnableSnapshotStyles(e.Key == Key.S); From df2079454079e43a59ca3b8032ac5837a8cf8fb6 Mon Sep 17 00:00:00 2001 From: Luis von der Eltz Date: Thu, 22 Jul 2021 12:01:20 +0200 Subject: [PATCH 085/127] Set LightDimissEnabled via SetValue() --- .../Diagnostics/Views/MainWindow.xaml.cs | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs index 810a630f46..1791672940 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs @@ -1,15 +1,18 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reactive.Disposables; using System.Reactive.Linq; using System.Reactive.Subjects; using Avalonia.Controls; using Avalonia.Controls.Diagnostics; using Avalonia.Controls.Primitives; +using Avalonia.Data; using Avalonia.Diagnostics.ViewModels; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Markup.Xaml; +using Avalonia.Media; using Avalonia.Styling; using Avalonia.VisualTree; @@ -18,6 +21,7 @@ namespace Avalonia.Diagnostics.Views internal class MainWindow : Window, IStyleHost { private readonly IDisposable _keySubscription; + private readonly Dictionary _frozenPopupStates; private TopLevel? _root; public MainWindow() @@ -28,6 +32,8 @@ namespace Avalonia.Diagnostics.Views .OfType() .Subscribe(RawKeyDown); + _frozenPopupStates = new Dictionary(); + EventHandler? lh = default; lh = (s, e) => { @@ -183,7 +189,25 @@ namespace Avalonia.Diagnostics.Views { if (popupRoot.Parent is Popup popup) { - popup.IsLightDismissEnabled = !vm.FreezePopups; + if (vm.FreezePopups) + { + var lightDismissEnabledState = popup.SetValue( + Popup.IsLightDismissEnabledProperty, + !vm.FreezePopups, + BindingPriority.Animation); + + if (lightDismissEnabledState != null) + { + _frozenPopupStates[popup] = lightDismissEnabledState; + } + } + else + { + if (_frozenPopupStates.TryGetValue(popup, out var state)) + { + state.Dispose(); + } + } } } From cabe6daa3f6f13d0991655465f6d286029eb1e27 Mon Sep 17 00:00:00 2001 From: Luis von der Eltz Date: Fri, 23 Jul 2021 10:57:19 +0200 Subject: [PATCH 086/127] Review changes --- .../Diagnostics/Views/MainWindow.xaml.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs index 1791672940..a85fe6c0c4 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs @@ -86,6 +86,13 @@ namespace Avalonia.Diagnostics.Views base.OnClosed(e); _keySubscription.Dispose(); + foreach (var state in _frozenPopupStates) + { + state.Value.Dispose(); + } + + _frozenPopupStates.Clear(); + if (_root != null) { _root.Closed -= RootClosed; @@ -203,9 +210,11 @@ namespace Avalonia.Diagnostics.Views } else { - if (_frozenPopupStates.TryGetValue(popup, out var state)) + //TODO Use Dictionary.Remove(Key, out Value) in netstandard 2.1 + if (_frozenPopupStates.ContainsKey(popup)) { - state.Dispose(); + _frozenPopupStates[popup].Dispose(); + _frozenPopupStates.Remove(popup); } } } From 57ffcb85f7df6967c802029303345a71d1afe9b6 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 28 Jul 2021 14:38:49 +0100 Subject: [PATCH 087/127] initial implementation of win32 shutdown cancelling. --- src/Windows/Avalonia.Win32/Win32Platform.cs | 23 +++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index 84e61ca007..67f0246cdc 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -65,7 +65,7 @@ namespace Avalonia namespace Avalonia.Win32 { - class Win32Platform : IPlatformThreadingInterface, IPlatformSettings, IWindowingPlatform, IPlatformIconLoader + class Win32Platform : IPlatformThreadingInterface, IPlatformSettings, IWindowingPlatform, IPlatformIconLoader, IPlatformLifetimeEventsImpl { private static readonly Win32Platform s_instance = new Win32Platform(); private static Thread _uiThread; @@ -122,7 +122,8 @@ namespace Avalonia.Win32 }) .Bind().ToConstant(s_instance) .Bind().ToConstant(new NonPumpingSyncContext.HelperImpl()) - .Bind().ToConstant(new WindowsMountedVolumeInfoProvider()); + .Bind().ToConstant(new WindowsMountedVolumeInfoProvider()) + .Bind().ToConstant(s_instance); Win32GlManager.Initialize(); @@ -207,6 +208,8 @@ namespace Avalonia.Win32 public event Action Signaled; + public event EventHandler ShutdownRequested; + [SuppressMessage("Microsoft.StyleCop.CSharp.NamingRules", "SA1305:FieldNamesMustNotUseHungarianNotation", Justification = "Using Win32 naming for consistency.")] private IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) { @@ -214,6 +217,22 @@ namespace Avalonia.Win32 { Signaled?.Invoke(null); } + + if(msg == (uint)WindowsMessage.WM_QUERYENDSESSION) + { + if (ShutdownRequested != null) + { + var e = new CancelEventArgs(); + + ShutdownRequested(this, e); + + if(e.Cancel) + { + return IntPtr.Zero; + } + } + } + return UnmanagedMethods.DefWindowProc(hWnd, msg, wParam, lParam); } From 851baa6537859ea6df8d774911003c97aa1e1bf8 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 28 Jul 2021 14:39:00 +0100 Subject: [PATCH 088/127] sandbox demo of shutdown cancel. --- samples/Sandbox/App.axaml.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/samples/Sandbox/App.axaml.cs b/samples/Sandbox/App.axaml.cs index 7eb8345784..c61ff5f0d5 100644 --- a/samples/Sandbox/App.axaml.cs +++ b/samples/Sandbox/App.axaml.cs @@ -16,7 +16,14 @@ namespace Sandbox if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime) { desktopLifetime.MainWindow = new MainWindow(); + + desktopLifetime.ShutdownRequested += DesktopLifetime_ShutdownRequested; } } + + private void DesktopLifetime_ShutdownRequested(object sender, System.ComponentModel.CancelEventArgs e) + { + e.Cancel = true; + } } } From 6c3ba87cab3ffa9aedfffe6fedbbd8dc790443d2 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 28 Jul 2021 17:29:06 +0100 Subject: [PATCH 089/127] Revert "sandbox demo of shutdown cancel." This reverts commit 851baa6537859ea6df8d774911003c97aa1e1bf8. --- samples/Sandbox/App.axaml.cs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/samples/Sandbox/App.axaml.cs b/samples/Sandbox/App.axaml.cs index c61ff5f0d5..7eb8345784 100644 --- a/samples/Sandbox/App.axaml.cs +++ b/samples/Sandbox/App.axaml.cs @@ -16,14 +16,7 @@ namespace Sandbox if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime) { desktopLifetime.MainWindow = new MainWindow(); - - desktopLifetime.ShutdownRequested += DesktopLifetime_ShutdownRequested; } } - - private void DesktopLifetime_ShutdownRequested(object sender, System.ComponentModel.CancelEventArgs e) - { - e.Cancel = true; - } } } From d0fd73729a4c7a2dfa7915537bcfc2aeb69cde0d Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 28 Jul 2021 17:55:41 +0100 Subject: [PATCH 090/127] clarify functionality on various platforms in event comments. --- .../IClassicDesktopStyleApplicationLifetime.cs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs index ecf8a0358f..de6ee150db 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs @@ -37,13 +37,20 @@ namespace Avalonia.Controls.ApplicationLifetimes IReadOnlyList Windows { get; } /// - /// Raised by the platform when a shutdown is requested. + /// Raised by the platform when an application shutdown is requested. /// /// - /// Raised on on OSX via the Quit menu or right-clicking on the application icon and selecting Quit. This event - /// provides a first-chance to cancel application shutdown; if shutdown is not canceled at this point the application + /// Application Shutdown can be requested for various reasons like OS shutdown. + /// + /// On Windows this will be called when an OS Session (logout or shutdown) terminates. Cancelling the eventargs will + /// block OS shutdown. + /// + /// On OSX this has the same behavior as on Windows and in addition: + /// This event is raised via the Quit menu or right-clicking on the application icon and selecting Quit. + /// + /// This event provides a first-chance to cancel application shutdown; if shutdown is not canceled at this point the application /// will try to close each non-owned open window, invoking the event on each and allowing - /// each window to cancel the shutdown. + /// each window to cancel the shutdown of the application. Windows cannot however prevent OS shutdown. /// event EventHandler ShutdownRequested; } From de3fdbeaf151deb6473aac9e5f3d2aa268e36925 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Wed, 28 Jul 2021 20:55:13 +0200 Subject: [PATCH 091/127] Add ClipGeometry and OpacityMask properties to DrawingGroup --- src/Avalonia.Visuals/Media/DrawingGroup.cs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/Avalonia.Visuals/Media/DrawingGroup.cs b/src/Avalonia.Visuals/Media/DrawingGroup.cs index e581c8c553..eeb6318ebd 100644 --- a/src/Avalonia.Visuals/Media/DrawingGroup.cs +++ b/src/Avalonia.Visuals/Media/DrawingGroup.cs @@ -12,6 +12,12 @@ namespace Avalonia.Media public static readonly StyledProperty TransformProperty = AvaloniaProperty.Register(nameof(Transform)); + public static readonly StyledProperty ClipGeometryProperty = + AvaloniaProperty.Register(nameof(ClipGeometry)); + + public static readonly StyledProperty OpacityMaskProperty = + AvaloniaProperty.Register(nameof(OpacityMask)); + public double Opacity { get => GetValue(OpacityProperty); @@ -24,6 +30,18 @@ namespace Avalonia.Media set => SetValue(TransformProperty, value); } + public Geometry ClipGeometry + { + get => GetValue(ClipGeometryProperty); + set => SetValue(ClipGeometryProperty, value); + } + + public IBrush OpacityMask + { + get => GetValue(OpacityMaskProperty); + set => SetValue(OpacityMaskProperty, value); + } + [Content] public AvaloniaList Children { get; } = new AvaloniaList(); @@ -31,6 +49,8 @@ namespace Avalonia.Media { using (context.PushPreTransform(Transform?.Value ?? Matrix.Identity)) using (context.PushOpacity(Opacity)) + using (ClipGeometry != null ? context.PushGeometryClip(ClipGeometry) : default(DrawingContext.PushedState)) + using (OpacityMask != null ? context.PushOpacityMask(OpacityMask, GetBounds()) : default(DrawingContext.PushedState)) { foreach (var drawing in Children) { From f1c87b4043db8996a866c89876a58e9c98a434c4 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 29 Jul 2021 10:43:41 +0200 Subject: [PATCH 092/127] Added KeyboardNavigationMode.Local. --- src/Avalonia.Input/KeyboardNavigationMode.cs | 7 ++++++- src/Avalonia.Input/Navigation/TabNavigation.cs | 3 +++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Input/KeyboardNavigationMode.cs b/src/Avalonia.Input/KeyboardNavigationMode.cs index 41e778bf49..e01ebf0330 100644 --- a/src/Avalonia.Input/KeyboardNavigationMode.cs +++ b/src/Avalonia.Input/KeyboardNavigationMode.cs @@ -36,5 +36,10 @@ namespace Avalonia.Input /// The container's children will not be focused when using the tab key. /// None, + + /// + /// TabIndexes are considered on local subtree only inside this container + /// + Local, } -} \ No newline at end of file +} diff --git a/src/Avalonia.Input/Navigation/TabNavigation.cs b/src/Avalonia.Input/Navigation/TabNavigation.cs index 12842e4f40..ed7df67bf2 100644 --- a/src/Avalonia.Input/Navigation/TabNavigation.cs +++ b/src/Avalonia.Input/Navigation/TabNavigation.cs @@ -152,6 +152,9 @@ namespace Avalonia.Input.Navigation // Look for element with the same TabIndex before the current element while ((nextTabElement = GetPrevTabInGroup(nextTabElement, container, tabbingType)) != null) { + if (nextTabElement == container && tabbingType == KeyboardNavigationMode.Local) + break; + // At this point nextTabElement is TabStop or TabGroup // In case it is a TabStop only return the element if (IsTabStop(nextTabElement) && !IsGroup(nextTabElement)) From c9458e5be833237ea3865cd30b4185445af5bb5a Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 29 Jul 2021 14:23:27 +0100 Subject: [PATCH 093/127] use a ShutdownRequestedEventArgs class to future proof the event args. --- src/Avalonia.Controls/ApiCompatBaseline.txt | 6 +++--- .../ClassicDesktopStyleApplicationLifetime.cs | 4 ++-- .../IClassicDesktopStyleApplicationLifetime.cs | 2 +- .../ShutdownRequestedCancelEventArgs.cs | 9 +++++++++ .../Platform/IPlatformLifetimeEventsImpl.cs | 3 ++- src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs | 5 +++-- src/Windows/Avalonia.Win32/Win32Platform.cs | 8 +++----- .../DesktopStyleApplicationLifetimeTests.cs | 2 +- 8 files changed, 24 insertions(+), 15 deletions(-) create mode 100644 src/Avalonia.Controls/ApplicationLifetimes/ShutdownRequestedCancelEventArgs.cs diff --git a/src/Avalonia.Controls/ApiCompatBaseline.txt b/src/Avalonia.Controls/ApiCompatBaseline.txt index ea62a8d843..5614b30304 100644 --- a/src/Avalonia.Controls/ApiCompatBaseline.txt +++ b/src/Avalonia.Controls/ApiCompatBaseline.txt @@ -30,9 +30,9 @@ MembersMustExist : Member 'public System.Double Avalonia.Controls.NumericUpDownV MembersMustExist : Member 'public System.Double Avalonia.Controls.NumericUpDownValueChangedEventArgs.OldValue.get()' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.StyledProperty Avalonia.StyledProperty Avalonia.Controls.ScrollViewer.AllowAutoHideProperty' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.AvaloniaProperty Avalonia.AvaloniaProperty Avalonia.Controls.Viewbox.StretchProperty' does not exist in the implementation but it does exist in the contract. -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.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. MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.AvaloniaProperty Avalonia.AvaloniaProperty Avalonia.Controls.Notifications.NotificationCard.CloseOnClickProperty' does not exist in the implementation but it does exist in the contract. EnumValuesMustMatch : Enum value 'Avalonia.Platform.ExtendClientAreaChromeHints Avalonia.Platform.ExtendClientAreaChromeHints.Default' is (System.Int32)2 in the implementation but (System.Int32)1 in the contract. diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs index 79780dbd0b..38e4a10033 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs @@ -48,7 +48,7 @@ namespace Avalonia.Controls.ApplicationLifetimes public event EventHandler Startup; /// - public event EventHandler ShutdownRequested; + public event EventHandler ShutdownRequested; /// public event EventHandler Exit; @@ -134,7 +134,7 @@ namespace Avalonia.Controls.ApplicationLifetimes _activeLifetime = null; } - private void OnShutdownRequested(object sender, CancelEventArgs e) + private void OnShutdownRequested(object sender, ShutdownRequestedCancelEventArgs e) { ShutdownRequested?.Invoke(this, e); diff --git a/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs index de6ee150db..722c275df8 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs @@ -52,6 +52,6 @@ namespace Avalonia.Controls.ApplicationLifetimes /// will try to close each non-owned open window, invoking the event on each and allowing /// each window to cancel the shutdown of the application. Windows cannot however prevent OS shutdown. /// - event EventHandler ShutdownRequested; + event EventHandler ShutdownRequested; } } diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ShutdownRequestedCancelEventArgs.cs b/src/Avalonia.Controls/ApplicationLifetimes/ShutdownRequestedCancelEventArgs.cs new file mode 100644 index 0000000000..3dd609f0d9 --- /dev/null +++ b/src/Avalonia.Controls/ApplicationLifetimes/ShutdownRequestedCancelEventArgs.cs @@ -0,0 +1,9 @@ +using System.ComponentModel; + +namespace Avalonia.Controls.ApplicationLifetimes +{ + public class ShutdownRequestedCancelEventArgs : CancelEventArgs + { + + } +} diff --git a/src/Avalonia.Controls/Platform/IPlatformLifetimeEventsImpl.cs b/src/Avalonia.Controls/Platform/IPlatformLifetimeEventsImpl.cs index 8e660777e9..e41362f13c 100644 --- a/src/Avalonia.Controls/Platform/IPlatformLifetimeEventsImpl.cs +++ b/src/Avalonia.Controls/Platform/IPlatformLifetimeEventsImpl.cs @@ -1,5 +1,6 @@ using System; using System.ComponentModel; +using Avalonia.Controls.ApplicationLifetimes; namespace Avalonia.Platform { @@ -11,6 +12,6 @@ namespace Avalonia.Platform /// /// Raised on on OSX via the Quit menu or right-clicking on the application icon and selecting Quit. /// - event EventHandler ShutdownRequested; + event EventHandler ShutdownRequested; } } diff --git a/src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs b/src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs index 77c0794d04..99bc857597 100644 --- a/src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs @@ -1,5 +1,6 @@ using System; using System.ComponentModel; +using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Native.Interop; using Avalonia.Platform; @@ -7,7 +8,7 @@ namespace Avalonia.Native { internal class AvaloniaNativeApplicationPlatform : CallbackBase, IAvnApplicationEvents, IPlatformLifetimeEventsImpl { - public event EventHandler ShutdownRequested; + public event EventHandler ShutdownRequested; void IAvnApplicationEvents.FilesOpened(IAvnStringArray urls) { @@ -17,7 +18,7 @@ namespace Avalonia.Native public int TryShutdown() { if (ShutdownRequested is null) return 1; - var e = new CancelEventArgs(); + var e = new ShutdownRequestedCancelEventArgs(); ShutdownRequested(this, e); return (!e.Cancel).AsComBool(); } diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index 67f0246cdc..d9b42d79ef 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -6,18 +6,16 @@ using System.IO; using System.Reactive.Disposables; using System.Runtime.InteropServices; using System.Threading; -using Avalonia.Animation; using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.Platform; using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.OpenGL; -using Avalonia.OpenGL.Egl; using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Threading; using Avalonia.Utilities; -using Avalonia.Win32; using Avalonia.Win32.Input; using Avalonia.Win32.Interop; using static Avalonia.Win32.Interop.UnmanagedMethods; @@ -208,7 +206,7 @@ namespace Avalonia.Win32 public event Action Signaled; - public event EventHandler ShutdownRequested; + public event EventHandler ShutdownRequested; [SuppressMessage("Microsoft.StyleCop.CSharp.NamingRules", "SA1305:FieldNamesMustNotUseHungarianNotation", Justification = "Using Win32 naming for consistency.")] private IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) @@ -222,7 +220,7 @@ namespace Avalonia.Win32 { if (ShutdownRequested != null) { - var e = new CancelEventArgs(); + var e = new ShutdownRequestedCancelEventArgs(); ShutdownRequested(this, e); diff --git a/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs b/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs index 38713834c3..17002c4958 100644 --- a/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs +++ b/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs @@ -232,7 +232,7 @@ namespace Avalonia.Controls.UnitTests ++raised; }; - lifetimeEvents.Raise(x => x.ShutdownRequested += null, new CancelEventArgs()); + lifetimeEvents.Raise(x => x.ShutdownRequested += null, new ShutdownRequestedCancelEventArgs()); Assert.Equal(1, raised); Assert.Equal(new[] { window }, lifetime.Windows); From 2a4830bf5b401535d92449ea66eb39dbc0e3c44a Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 29 Jul 2021 14:59:38 +0100 Subject: [PATCH 094/127] fix tests. --- tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs b/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs index 6f7e7bc8e7..ba01f3db40 100644 --- a/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs @@ -230,7 +230,7 @@ namespace Avalonia.Controls.UnitTests { using (Application()) { - popupImpl.Setup(x => x.Show(true)).Verifiable(); + popupImpl.Setup(x => x.Show(true, false)).Verifiable(); popupImpl.Setup(x => x.Hide()).Verifiable(); var window = PreparedWindow(); @@ -268,7 +268,7 @@ namespace Avalonia.Controls.UnitTests Assert.True(c.IsOpen); popupImpl.Verify(x => x.Hide(), Times.Never); - popupImpl.Verify(x => x.Show(true), Times.Exactly(1)); + popupImpl.Verify(x => x.Show(true, false), Times.Exactly(1)); } } @@ -277,7 +277,7 @@ namespace Avalonia.Controls.UnitTests { using (Application()) { - popupImpl.Setup(x => x.Show(true)).Verifiable(); + popupImpl.Setup(x => x.Show(true, false)).Verifiable(); popupImpl.Setup(x => x.Hide()).Verifiable(); var window = PreparedWindow(); @@ -306,7 +306,7 @@ namespace Avalonia.Controls.UnitTests Assert.False(c.IsOpen); popupImpl.Verify(x => x.Hide(), Times.Exactly(1)); - popupImpl.Verify(x => x.Show(true), Times.Exactly(1)); + popupImpl.Verify(x => x.Show(true, false), Times.Exactly(1)); } } From 0ddc444d2c5e7c1fdb1df460f1cf66ede39f8d1a Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 29 Jul 2021 15:07:31 +0100 Subject: [PATCH 095/127] Rename eventargs class. --- src/Avalonia.Controls/ApiCompatBaseline.txt | 6 +++--- .../ClassicDesktopStyleApplicationLifetime.cs | 4 ++-- .../IClassicDesktopStyleApplicationLifetime.cs | 2 +- ...stedCancelEventArgs.cs => ShutdownRequestedEventArgs.cs} | 2 +- .../Platform/IPlatformLifetimeEventsImpl.cs | 2 +- src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs | 4 ++-- src/Windows/Avalonia.Win32/Win32Platform.cs | 4 ++-- .../DesktopStyleApplicationLifetimeTests.cs | 2 +- 8 files changed, 13 insertions(+), 13 deletions(-) rename src/Avalonia.Controls/ApplicationLifetimes/{ShutdownRequestedCancelEventArgs.cs => ShutdownRequestedEventArgs.cs} (59%) diff --git a/src/Avalonia.Controls/ApiCompatBaseline.txt b/src/Avalonia.Controls/ApiCompatBaseline.txt index 5614b30304..188623c320 100644 --- a/src/Avalonia.Controls/ApiCompatBaseline.txt +++ b/src/Avalonia.Controls/ApiCompatBaseline.txt @@ -30,9 +30,9 @@ MembersMustExist : Member 'public System.Double Avalonia.Controls.NumericUpDownV MembersMustExist : Member 'public System.Double Avalonia.Controls.NumericUpDownValueChangedEventArgs.OldValue.get()' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.StyledProperty Avalonia.StyledProperty Avalonia.Controls.ScrollViewer.AllowAutoHideProperty' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.AvaloniaProperty Avalonia.AvaloniaProperty Avalonia.Controls.Viewbox.StretchProperty' does not exist in the implementation but it does exist in the contract. -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.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. MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.AvaloniaProperty Avalonia.AvaloniaProperty Avalonia.Controls.Notifications.NotificationCard.CloseOnClickProperty' does not exist in the implementation but it does exist in the contract. EnumValuesMustMatch : Enum value 'Avalonia.Platform.ExtendClientAreaChromeHints Avalonia.Platform.ExtendClientAreaChromeHints.Default' is (System.Int32)2 in the implementation but (System.Int32)1 in the contract. diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs index 38e4a10033..2a42d99ac5 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs @@ -48,7 +48,7 @@ namespace Avalonia.Controls.ApplicationLifetimes public event EventHandler Startup; /// - public event EventHandler ShutdownRequested; + public event EventHandler ShutdownRequested; /// public event EventHandler Exit; @@ -134,7 +134,7 @@ namespace Avalonia.Controls.ApplicationLifetimes _activeLifetime = null; } - private void OnShutdownRequested(object sender, ShutdownRequestedCancelEventArgs e) + private void OnShutdownRequested(object sender, ShutdownRequestedEventArgs e) { ShutdownRequested?.Invoke(this, e); diff --git a/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs index 722c275df8..a70d5dd2f1 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs @@ -52,6 +52,6 @@ namespace Avalonia.Controls.ApplicationLifetimes /// will try to close each non-owned open window, invoking the event on each and allowing /// each window to cancel the shutdown of the application. Windows cannot however prevent OS shutdown. /// - event EventHandler ShutdownRequested; + event EventHandler ShutdownRequested; } } diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ShutdownRequestedCancelEventArgs.cs b/src/Avalonia.Controls/ApplicationLifetimes/ShutdownRequestedEventArgs.cs similarity index 59% rename from src/Avalonia.Controls/ApplicationLifetimes/ShutdownRequestedCancelEventArgs.cs rename to src/Avalonia.Controls/ApplicationLifetimes/ShutdownRequestedEventArgs.cs index 3dd609f0d9..62bc3a8904 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/ShutdownRequestedCancelEventArgs.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/ShutdownRequestedEventArgs.cs @@ -2,7 +2,7 @@ namespace Avalonia.Controls.ApplicationLifetimes { - public class ShutdownRequestedCancelEventArgs : CancelEventArgs + public class ShutdownRequestedEventArgs : CancelEventArgs { } diff --git a/src/Avalonia.Controls/Platform/IPlatformLifetimeEventsImpl.cs b/src/Avalonia.Controls/Platform/IPlatformLifetimeEventsImpl.cs index e41362f13c..4cd6640453 100644 --- a/src/Avalonia.Controls/Platform/IPlatformLifetimeEventsImpl.cs +++ b/src/Avalonia.Controls/Platform/IPlatformLifetimeEventsImpl.cs @@ -12,6 +12,6 @@ namespace Avalonia.Platform /// /// Raised on on OSX via the Quit menu or right-clicking on the application icon and selecting Quit. /// - event EventHandler ShutdownRequested; + event EventHandler ShutdownRequested; } } diff --git a/src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs b/src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs index 99bc857597..8084e06d28 100644 --- a/src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs @@ -8,7 +8,7 @@ namespace Avalonia.Native { internal class AvaloniaNativeApplicationPlatform : CallbackBase, IAvnApplicationEvents, IPlatformLifetimeEventsImpl { - public event EventHandler ShutdownRequested; + public event EventHandler ShutdownRequested; void IAvnApplicationEvents.FilesOpened(IAvnStringArray urls) { @@ -18,7 +18,7 @@ namespace Avalonia.Native public int TryShutdown() { if (ShutdownRequested is null) return 1; - var e = new ShutdownRequestedCancelEventArgs(); + var e = new ShutdownRequestedEventArgs(); ShutdownRequested(this, e); return (!e.Cancel).AsComBool(); } diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index d9b42d79ef..2265ed2322 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -206,7 +206,7 @@ namespace Avalonia.Win32 public event Action Signaled; - public event EventHandler ShutdownRequested; + public event EventHandler ShutdownRequested; [SuppressMessage("Microsoft.StyleCop.CSharp.NamingRules", "SA1305:FieldNamesMustNotUseHungarianNotation", Justification = "Using Win32 naming for consistency.")] private IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) @@ -220,7 +220,7 @@ namespace Avalonia.Win32 { if (ShutdownRequested != null) { - var e = new ShutdownRequestedCancelEventArgs(); + var e = new ShutdownRequestedEventArgs(); ShutdownRequested(this, e); diff --git a/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs b/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs index 17002c4958..f7a3bdea1c 100644 --- a/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs +++ b/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs @@ -232,7 +232,7 @@ namespace Avalonia.Controls.UnitTests ++raised; }; - lifetimeEvents.Raise(x => x.ShutdownRequested += null, new ShutdownRequestedCancelEventArgs()); + lifetimeEvents.Raise(x => x.ShutdownRequested += null, new ShutdownRequestedEventArgs()); Assert.Equal(1, raised); Assert.Equal(new[] { window }, lifetime.Windows); From 4d8d31dfe94cde993c0786ec9a1260fa5bb4611e Mon Sep 17 00:00:00 2001 From: GMIKE Date: Fri, 30 Jul 2021 00:09:35 +0300 Subject: [PATCH 096/127] Properties of PointerEventArgs in TappedEventArgs (#6322) * Properties of PointerEventArgs in TappedEventArgs * remove InputModifiers and Device properties * move properties * remove whitespace --- src/Avalonia.Input/TappedEventArgs.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Avalonia.Input/TappedEventArgs.cs b/src/Avalonia.Input/TappedEventArgs.cs index 02add509cd..daaab70632 100644 --- a/src/Avalonia.Input/TappedEventArgs.cs +++ b/src/Avalonia.Input/TappedEventArgs.cs @@ -13,6 +13,10 @@ namespace Avalonia.Input this.lastPointerEventArgs = lastPointerEventArgs; } + public IPointer Pointer => lastPointerEventArgs.Pointer; + public KeyModifiers KeyModifiers => lastPointerEventArgs.KeyModifiers; + public ulong Timestamp => lastPointerEventArgs.Timestamp; + public Point GetPosition(IVisual? relativeTo) => lastPointerEventArgs.GetPosition(relativeTo); } } From 08566c36e2435e1b861d91d592c8ca796c42e254 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 30 Jul 2021 14:36:13 +0200 Subject: [PATCH 097/127] Add in obsolete HandleResized for ApiCompat. --- src/Avalonia.Controls/ApiCompatBaseline.txt | 3 +-- src/Avalonia.Controls/Window.cs | 3 +++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/ApiCompatBaseline.txt b/src/Avalonia.Controls/ApiCompatBaseline.txt index a1cf9a445c..026ee970c3 100644 --- a/src/Avalonia.Controls/ApiCompatBaseline.txt +++ b/src/Avalonia.Controls/ApiCompatBaseline.txt @@ -31,7 +31,6 @@ MembersMustExist : Member 'public System.Double Avalonia.Controls.NumericUpDownV MembersMustExist : Member 'public Avalonia.StyledProperty Avalonia.StyledProperty Avalonia.Controls.ScrollViewer.AllowAutoHideProperty' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'protected void Avalonia.Controls.TopLevel.HandleResized(Avalonia.Size)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.AvaloniaProperty Avalonia.AvaloniaProperty Avalonia.Controls.Viewbox.StretchProperty' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'protected void Avalonia.Controls.Window.HandleResized(Avalonia.Size)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'protected void Avalonia.Controls.WindowBase.HandleResized(Avalonia.Size)' does not exist in the implementation but it does exist in the contract. 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. @@ -58,4 +57,4 @@ InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platfor InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IWindowImpl.Resize(Avalonia.Size)' is present in the contract but not in the implementation. 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. -Total Issues: 59 +Total Issues: 58 diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 2acfa545b9..ac3dace35e 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -949,6 +949,9 @@ namespace Avalonia.Controls Owner = null; } + [Obsolete("Use HandleResized(Size, PlatformResizeReason)")] + protected void HandleResized(Size clientSize) => HandleResized(clientSize, PlatformResizeReason.Unspecified); + /// protected sealed override void HandleResized(Size clientSize, PlatformResizeReason reason) { From 59e18e3517bc1d2884ba0f87b18427823815560c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 30 Jul 2021 14:40:07 +0200 Subject: [PATCH 098/127] Add obsolete HandleResized to TopLevel/WindowBase too. --- src/Avalonia.Controls/ApiCompatBaseline.txt | 4 +--- src/Avalonia.Controls/TopLevel.cs | 3 +++ src/Avalonia.Controls/Window.cs | 2 +- src/Avalonia.Controls/WindowBase.cs | 3 +++ 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Controls/ApiCompatBaseline.txt b/src/Avalonia.Controls/ApiCompatBaseline.txt index 026ee970c3..fac5923db5 100644 --- a/src/Avalonia.Controls/ApiCompatBaseline.txt +++ b/src/Avalonia.Controls/ApiCompatBaseline.txt @@ -29,9 +29,7 @@ MembersMustExist : Member 'public void Avalonia.Controls.NumericUpDownValueChang MembersMustExist : Member 'public System.Double Avalonia.Controls.NumericUpDownValueChangedEventArgs.NewValue.get()' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public System.Double Avalonia.Controls.NumericUpDownValueChangedEventArgs.OldValue.get()' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.StyledProperty Avalonia.StyledProperty Avalonia.Controls.ScrollViewer.AllowAutoHideProperty' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'protected void Avalonia.Controls.TopLevel.HandleResized(Avalonia.Size)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.AvaloniaProperty Avalonia.AvaloniaProperty Avalonia.Controls.Viewbox.StretchProperty' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'protected void Avalonia.Controls.WindowBase.HandleResized(Avalonia.Size)' does not exist in the implementation but it does exist in the contract. 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. @@ -57,4 +55,4 @@ InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platfor InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IWindowImpl.Resize(Avalonia.Size)' is present in the contract but not in the implementation. 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. -Total Issues: 58 +Total Issues: 56 diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index c5e7a7204f..116e276a3a 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -376,6 +376,9 @@ namespace Avalonia.Controls LayoutManager?.Dispose(); } + [Obsolete("Use HandleResized(Size, PlatformResizeReason)")] + protected virtual void HandleResized(Size clientSize) => HandleResized(clientSize, PlatformResizeReason.Unspecified); + /// /// Handles a resize notification from . /// diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index ac3dace35e..0ca28ca196 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -950,7 +950,7 @@ namespace Avalonia.Controls } [Obsolete("Use HandleResized(Size, PlatformResizeReason)")] - protected void HandleResized(Size clientSize) => HandleResized(clientSize, PlatformResizeReason.Unspecified); + protected sealed override void HandleResized(Size clientSize) => HandleResized(clientSize, PlatformResizeReason.Unspecified); /// protected sealed override void HandleResized(Size clientSize, PlatformResizeReason reason) diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs index 5a06b908e5..7cfc5f8be8 100644 --- a/src/Avalonia.Controls/WindowBase.cs +++ b/src/Avalonia.Controls/WindowBase.cs @@ -201,6 +201,9 @@ namespace Avalonia.Controls } } + [Obsolete("Use HandleResized(Size, PlatformResizeReason)")] + protected override void HandleResized(Size clientSize) => HandleResized(clientSize, PlatformResizeReason.Unspecified); + /// /// Handles a resize notification from . /// From c285a0e6a80faab1388d4dafcb9002a2f164f3ec Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 31 Jul 2021 17:27:22 -0400 Subject: [PATCH 099/127] Fix DataGrid wheel scroll calculation --- src/Avalonia.Controls.DataGrid/DataGrid.cs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index 83f13fe199..3887bb3380 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -67,7 +67,7 @@ namespace Avalonia.Controls private const double DATAGRID_minimumColumnHeaderHeight = 4; internal const double DATAGRID_maximumStarColumnWidth = 10000; internal const double DATAGRID_minimumStarColumnWidth = 0.001; - private const double DATAGRID_mouseWheelDelta = 72.0; + private const double DATAGRID_mouseWheelDelta = 50.0; private const double DATAGRID_maxHeadersThickness = 32768; private const double DATAGRID_defaultRowHeight = 22; @@ -2217,20 +2217,23 @@ namespace Avalonia.Controls if (IsEnabled && !e.Handled && DisplayData.NumDisplayedScrollingElements > 0) { double scrollHeight = 0; - if (e.Delta.Y > 0) + var delta = DATAGRID_mouseWheelDelta * e.Delta.Y; + var deltaAbs = Math.Abs(delta); + + if (delta > 0) { - scrollHeight = Math.Max(-_verticalOffset, -DATAGRID_mouseWheelDelta); + scrollHeight = Math.Max(-_verticalOffset, -deltaAbs); } - else if (e.Delta.Y < 0) + else if (delta < 0) { if (_vScrollBar != null && VerticalScrollBarVisibility == ScrollBarVisibility.Visible) { - scrollHeight = Math.Min(Math.Max(0, _vScrollBar.Maximum - _verticalOffset), DATAGRID_mouseWheelDelta); + scrollHeight = Math.Min(Math.Max(0, _vScrollBar.Maximum - _verticalOffset), deltaAbs); } else { double maximum = EdgedRowsHeightCalculated - CellsHeight; - scrollHeight = Math.Min(Math.Max(0, maximum - _verticalOffset), DATAGRID_mouseWheelDelta); + scrollHeight = Math.Min(Math.Max(0, maximum - _verticalOffset), deltaAbs); } } if (scrollHeight != 0) From ca2acc951612ad7ba536d2399f60de5a053a4c84 Mon Sep 17 00:00:00 2001 From: mat1jaczyyy Date: Sun, 1 Aug 2021 03:19:23 +0200 Subject: [PATCH 100/127] WindowImpl: Don't change z-order when Position is set --- src/Windows/Avalonia.Win32/WindowImpl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index f20dae3e50..0b7bd13082 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -451,7 +451,7 @@ namespace Avalonia.Win32 value.Y, 0, 0, - SetWindowPosFlags.SWP_NOSIZE | SetWindowPosFlags.SWP_NOACTIVATE); + SetWindowPosFlags.SWP_NOSIZE | SetWindowPosFlags.SWP_NOACTIVATE | SetWindowPosFlags.SWP_NOZORDER); } } From 98a436481064952a4176f190820cacadcc47db16 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sun, 1 Aug 2021 01:16:38 -0400 Subject: [PATCH 101/127] Handle horizontal wheel scrolling in DataGrid --- .../ControlCatalog/Pages/DataGridPage.xaml | 2 +- src/Avalonia.Controls.DataGrid/DataGrid.cs | 57 +++++++++++++++---- 2 files changed, 48 insertions(+), 11 deletions(-) diff --git a/samples/ControlCatalog/Pages/DataGridPage.xaml b/samples/ControlCatalog/Pages/DataGridPage.xaml index 820c1324e3..340b3376f5 100644 --- a/samples/ControlCatalog/Pages/DataGridPage.xaml +++ b/samples/ControlCatalog/Pages/DataGridPage.xaml @@ -28,7 +28,7 @@ DockPanel.Dock="Top"/> - + diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index 3887bb3380..63e6950ae8 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -25,6 +25,7 @@ using System.ComponentModel.DataAnnotations; using Avalonia.Controls.Utils; using Avalonia.Layout; using Avalonia.Controls.Metadata; +using Avalonia.Input.GestureRecognizers; namespace Avalonia.Controls { @@ -2214,35 +2215,71 @@ namespace Avalonia.Controls /// PointerWheelEventArgs protected override void OnPointerWheelChanged(PointerWheelEventArgs e) { - if (IsEnabled && !e.Handled && DisplayData.NumDisplayedScrollingElements > 0) + e.Handled = e.Handled || UpdateScroll(e.Delta * DATAGRID_mouseWheelDelta); + } + + internal bool UpdateScroll(Vector delta) + { + if (IsEnabled && DisplayData.NumDisplayedScrollingElements > 0) { - double scrollHeight = 0; - var delta = DATAGRID_mouseWheelDelta * e.Delta.Y; - var deltaAbs = Math.Abs(delta); + var handled = false; + var scrollHeight = 0d; - if (delta > 0) + // Vertical scroll handling + if (delta.Y > 0) { - scrollHeight = Math.Max(-_verticalOffset, -deltaAbs); + scrollHeight = Math.Max(-_verticalOffset, -delta.Y); } - else if (delta < 0) + else if (delta.Y < 0) { if (_vScrollBar != null && VerticalScrollBarVisibility == ScrollBarVisibility.Visible) { - scrollHeight = Math.Min(Math.Max(0, _vScrollBar.Maximum - _verticalOffset), deltaAbs); + scrollHeight = Math.Min(Math.Max(0, _vScrollBar.Maximum - _verticalOffset), -delta.Y); } else { double maximum = EdgedRowsHeightCalculated - CellsHeight; - scrollHeight = Math.Min(Math.Max(0, maximum - _verticalOffset), deltaAbs); + scrollHeight = Math.Min(Math.Max(0, maximum - _verticalOffset), -delta.Y); } } + if (scrollHeight != 0) { DisplayData.PendingVerticalScrollHeight = scrollHeight; + handled = true; + } + + // Horizontal scroll handling + if (delta.X != 0) + { + var originalHorizontalOffset = HorizontalOffset; + var horizontalOffset = originalHorizontalOffset - delta.X; + var widthNotVisible = Math.Max(0, ColumnsInternal.VisibleEdgedColumnsWidth - CellsWidth); + + if (horizontalOffset < 0) + { + horizontalOffset = 0; + } + if (horizontalOffset > widthNotVisible) + { + horizontalOffset = widthNotVisible; + } + + if (horizontalOffset != originalHorizontalOffset) + { + HorizontalOffset = horizontalOffset; + handled = true; + } + } + + if (handled) + { InvalidateRowsMeasure(invalidateIndividualElements: false); - e.Handled = true; + return true; } } + + return false; } /// From 7e3b6ecff5dc187c0b4a6c535673556ad8d1b168 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sun, 1 Aug 2021 01:16:55 -0400 Subject: [PATCH 102/127] Handle touch scrolling in DataGrid --- src/Avalonia.Controls.DataGrid/DataGridCell.cs | 2 +- .../Primitives/DataGridRowsPresenter.cs | 13 +++++++++++++ src/Avalonia.Controls.DataGrid/Themes/Default.xaml | 6 +++++- src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml | 6 +++++- 4 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid/DataGridCell.cs b/src/Avalonia.Controls.DataGrid/DataGridCell.cs index 445dc541a7..47bf7b906c 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridCell.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridCell.cs @@ -173,7 +173,7 @@ namespace Avalonia.Controls } if (OwningRow != null) { - e.Handled = OwningGrid.UpdateStateOnMouseLeftButtonDown(e, ColumnIndex, OwningRow.Slot, !e.Handled); + OwningGrid.UpdateStateOnMouseLeftButtonDown(e, ColumnIndex, OwningRow.Slot, !e.Handled); OwningGrid.UpdatedStateOnMouseLeftButtonDown = true; } } diff --git a/src/Avalonia.Controls.DataGrid/Primitives/DataGridRowsPresenter.cs b/src/Avalonia.Controls.DataGrid/Primitives/DataGridRowsPresenter.cs index 0d19f4c479..308ebc69d4 100644 --- a/src/Avalonia.Controls.DataGrid/Primitives/DataGridRowsPresenter.cs +++ b/src/Avalonia.Controls.DataGrid/Primitives/DataGridRowsPresenter.cs @@ -5,6 +5,9 @@ using System; using System.Diagnostics; + +using Avalonia.Input; +using Avalonia.Input.GestureRecognizers; using Avalonia.Layout; using Avalonia.Media; @@ -16,6 +19,11 @@ namespace Avalonia.Controls.Primitives /// public sealed class DataGridRowsPresenter : Panel { + public DataGridRowsPresenter() + { + AddHandler(Gestures.ScrollGestureEvent, OnScrollGesture); + } + internal DataGrid OwningGrid { get; @@ -176,6 +184,11 @@ namespace Avalonia.Controls.Primitives return new Size(totalCellsWidth + headerWidth, totalHeight); } + private void OnScrollGesture(object sender, ScrollGestureEventArgs e) + { + e.Handled = e.Handled || OwningGrid.UpdateScroll(-e.Delta); + } + #if DEBUG internal void PrintChildren() { diff --git a/src/Avalonia.Controls.DataGrid/Themes/Default.xaml b/src/Avalonia.Controls.DataGrid/Themes/Default.xaml index 09d19c8e43..ca0873e183 100644 --- a/src/Avalonia.Controls.DataGrid/Themes/Default.xaml +++ b/src/Avalonia.Controls.DataGrid/Themes/Default.xaml @@ -247,7 +247,11 @@ - + + + + + diff --git a/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml b/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml index f2dbf42196..8ccf717d94 100644 --- a/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml +++ b/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml @@ -601,7 +601,11 @@ + Grid.ColumnSpan="3"> + + + + Date: Sun, 1 Aug 2021 02:57:57 -0400 Subject: [PATCH 103/127] Handle DataGridCell_PointerPressed if not touch --- src/Avalonia.Controls.DataGrid/DataGridCell.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls.DataGrid/DataGridCell.cs b/src/Avalonia.Controls.DataGrid/DataGridCell.cs index 241d543ec3..7dda936317 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridCell.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridCell.cs @@ -173,7 +173,15 @@ namespace Avalonia.Controls } if (OwningRow != null) { - OwningGrid.UpdateStateOnMouseLeftButtonDown(e, ColumnIndex, OwningRow.Slot, !e.Handled); + var handled = OwningGrid.UpdateStateOnMouseLeftButtonDown(e, ColumnIndex, OwningRow.Slot, !e.Handled); + + // Do not handle PointerPressed with touch, + // so we can start scroll gesture on the same event. + if (e.Pointer.Type != PointerType.Touch) + { + e.Handled = handled; + } + OwningGrid.UpdatedStateOnMouseLeftButtonDown = true; } } From 64922a34a937757380700199fcb555f8f39f6e90 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sun, 1 Aug 2021 21:45:07 -0400 Subject: [PATCH 104/127] Add CornerRadius to TemplatedControl and use it where possible --- samples/ControlCatalog/SideBar.xaml | 1 - samples/RenderDemo/SideBar.xaml | 1 - .../Flyouts/FlyoutPresenter.cs | 9 ----- .../Primitives/TemplatedControl.cs | 40 ++++++++++++++----- .../AutoCompleteBox.xaml | 1 + src/Avalonia.Themes.Default/Button.xaml | 3 +- .../ButtonSpinner.xaml | 2 + src/Avalonia.Themes.Default/Calendar.xaml | 3 +- .../CalendarDatePicker.xaml | 3 +- src/Avalonia.Themes.Default/CalendarItem.xaml | 5 ++- src/Avalonia.Themes.Default/Carousel.xaml | 3 +- src/Avalonia.Themes.Default/CheckBox.xaml | 1 + src/Avalonia.Themes.Default/ComboBox.xaml | 3 +- src/Avalonia.Themes.Default/ComboBoxItem.xaml | 1 + .../ContentControl.xaml | 3 +- src/Avalonia.Themes.Default/ContextMenu.xaml | 1 + .../DataValidationErrors.xaml | 1 + src/Avalonia.Themes.Default/DatePicker.xaml | 3 ++ src/Avalonia.Themes.Default/Expander.xaml | 5 ++- src/Avalonia.Themes.Default/GridSplitter.xaml | 1 + src/Avalonia.Themes.Default/ItemsControl.xaml | 1 + src/Avalonia.Themes.Default/Label.xaml | 1 + src/Avalonia.Themes.Default/ListBox.xaml | 3 +- src/Avalonia.Themes.Default/ListBoxItem.xaml | 1 + src/Avalonia.Themes.Default/Menu.xaml | 3 +- src/Avalonia.Themes.Default/MenuItem.xaml | 6 ++- .../NotificationCard.xaml | 1 + .../NumericUpDown.xaml | 2 +- src/Avalonia.Themes.Default/ProgressBar.xaml | 2 +- src/Avalonia.Themes.Default/RepeatButton.xaml | 1 + src/Avalonia.Themes.Default/Separator.xaml | 1 + src/Avalonia.Themes.Default/TabControl.xaml | 2 +- src/Avalonia.Themes.Default/TabItem.xaml | 2 +- src/Avalonia.Themes.Default/TabStripItem.xaml | 3 +- src/Avalonia.Themes.Default/TextBox.xaml | 3 +- src/Avalonia.Themes.Default/TimePicker.xaml | 3 ++ src/Avalonia.Themes.Default/ToggleButton.xaml | 3 +- src/Avalonia.Themes.Default/ToolTip.xaml | 3 +- src/Avalonia.Themes.Default/TreeView.xaml | 3 +- src/Avalonia.Themes.Default/TreeViewItem.xaml | 1 + src/Avalonia.Themes.Default/UserControl.xaml | 1 + .../Controls/AutoCompleteBox.xaml | 4 +- .../Controls/Button.xaml | 6 +-- .../Controls/ButtonSpinner.xaml | 3 +- .../Controls/Calendar.xaml | 10 ++++- .../Controls/CalendarDatePicker.xaml | 2 + .../Controls/CalendarItem.xaml | 8 +++- .../Controls/Carousel.xaml | 3 +- .../Controls/CheckBox.xaml | 13 ++---- .../Controls/ComboBox.xaml | 13 ++---- .../Controls/ComboBoxItem.xaml | 1 + .../Controls/ContentControl.xaml | 3 +- .../Controls/ContextMenu.xaml | 3 +- .../Controls/DataValidationErrors.xaml | 2 + .../Controls/DatePicker.xaml | 16 ++++++-- .../Controls/Expander.xaml | 29 +++++++------- .../Controls/GridSplitter.xaml | 1 + .../Controls/ItemsControl.xaml | 1 + .../Controls/Label.xaml | 3 +- .../Controls/ListBox.xaml | 8 ++-- .../Controls/ListBoxItem.xaml | 1 + src/Avalonia.Themes.Fluent/Controls/Menu.xaml | 1 + .../Controls/MenuItem.xaml | 6 ++- .../Controls/NotificationCard.xaml | 6 ++- .../Controls/NumericUpDown.xaml | 2 + .../Controls/ProgressBar.xaml | 3 +- .../Controls/RadioButton.xaml | 11 ++--- .../Controls/RepeatButton.xaml | 2 + .../Controls/Separator.xaml | 1 + .../Controls/Slider.xaml | 4 +- .../Controls/TabControl.xaml | 4 +- .../Controls/TabItem.xaml | 1 + .../Controls/TabStrip.xaml | 14 +++++-- .../Controls/TabStripItem.xaml | 1 + .../Controls/TextBox.xaml | 6 +-- .../Controls/TimePicker.xaml | 9 +++-- .../Controls/ToggleButton.xaml | 2 + .../Controls/ToolTip.xaml | 3 +- .../Controls/TreeView.xaml | 3 +- .../Controls/TreeViewItem.xaml | 1 + .../Controls/UserControl.xaml | 1 + 81 files changed, 223 insertions(+), 124 deletions(-) diff --git a/samples/ControlCatalog/SideBar.xaml b/samples/ControlCatalog/SideBar.xaml index 7c911e91e9..2b5215a3fe 100644 --- a/samples/ControlCatalog/SideBar.xaml +++ b/samples/ControlCatalog/SideBar.xaml @@ -16,7 +16,6 @@ diff --git a/samples/RenderDemo/SideBar.xaml b/samples/RenderDemo/SideBar.xaml index fd23067f61..b82a7b0514 100644 --- a/samples/RenderDemo/SideBar.xaml +++ b/samples/RenderDemo/SideBar.xaml @@ -7,7 +7,6 @@ diff --git a/src/Avalonia.Controls/Flyouts/FlyoutPresenter.cs b/src/Avalonia.Controls/Flyouts/FlyoutPresenter.cs index 10f97794d7..0f257224dd 100644 --- a/src/Avalonia.Controls/Flyouts/FlyoutPresenter.cs +++ b/src/Avalonia.Controls/Flyouts/FlyoutPresenter.cs @@ -6,15 +6,6 @@ namespace Avalonia.Controls { public class FlyoutPresenter : ContentControl { - public static readonly StyledProperty CornerRadiusProperty = - Border.CornerRadiusProperty.AddOwner(); - - public CornerRadius CornerRadius - { - get => GetValue(CornerRadiusProperty); - set => SetValue(CornerRadiusProperty, value); - } - protected override void OnKeyDown(KeyEventArgs e) { if (e.Key == Key.Escape) diff --git a/src/Avalonia.Controls/Primitives/TemplatedControl.cs b/src/Avalonia.Controls/Primitives/TemplatedControl.cs index 9c73ff2411..59975b072d 100644 --- a/src/Avalonia.Controls/Primitives/TemplatedControl.cs +++ b/src/Avalonia.Controls/Primitives/TemplatedControl.cs @@ -5,7 +5,8 @@ using Avalonia.Logging; using Avalonia.LogicalTree; using Avalonia.Media; using Avalonia.Styling; -using Avalonia.VisualTree; + +#nullable enable namespace Avalonia.Controls.Primitives { @@ -17,13 +18,13 @@ namespace Avalonia.Controls.Primitives /// /// Defines the property. /// - public static readonly StyledProperty BackgroundProperty = + public static readonly StyledProperty BackgroundProperty = Border.BackgroundProperty.AddOwner(); /// /// Defines the property. /// - public static readonly StyledProperty BorderBrushProperty = + public static readonly StyledProperty BorderBrushProperty = Border.BorderBrushProperty.AddOwner(); /// @@ -32,6 +33,12 @@ namespace Avalonia.Controls.Primitives public static readonly StyledProperty BorderThicknessProperty = Border.BorderThicknessProperty.AddOwner(); + /// + /// Defines the property. + /// + public static readonly StyledProperty CornerRadiusProperty = + Border.CornerRadiusProperty.AddOwner(); + /// /// Defines the property. /// @@ -59,7 +66,7 @@ namespace Avalonia.Controls.Primitives /// /// Defines the property. /// - public static readonly StyledProperty ForegroundProperty = + public static readonly StyledProperty ForegroundProperty = TextBlock.ForegroundProperty.AddOwner(); /// @@ -71,8 +78,8 @@ namespace Avalonia.Controls.Primitives /// /// Defines the property. /// - public static readonly StyledProperty TemplateProperty = - AvaloniaProperty.Register(nameof(Template)); + public static readonly StyledProperty TemplateProperty = + AvaloniaProperty.Register(nameof(Template)); /// /// Defines the IsTemplateFocusTarget attached property. @@ -88,7 +95,7 @@ namespace Avalonia.Controls.Primitives "TemplateApplied", RoutingStrategies.Direct); - private IControlTemplate _appliedTemplate; + private IControlTemplate? _appliedTemplate; /// /// Initializes static members of the class. @@ -111,7 +118,7 @@ namespace Avalonia.Controls.Primitives /// /// Gets or sets the brush used to draw the control's background. /// - public IBrush Background + public IBrush? Background { get { return GetValue(BackgroundProperty); } set { SetValue(BackgroundProperty, value); } @@ -120,7 +127,7 @@ namespace Avalonia.Controls.Primitives /// /// Gets or sets the brush used to draw the control's border. /// - public IBrush BorderBrush + public IBrush? BorderBrush { get { return GetValue(BorderBrushProperty); } set { SetValue(BorderBrushProperty, value); } @@ -135,6 +142,15 @@ namespace Avalonia.Controls.Primitives set { SetValue(BorderThicknessProperty, value); } } + /// + /// Gets or sets the radius of the border rounded corners. + /// + public CornerRadius CornerRadius + { + get { return GetValue(CornerRadiusProperty); } + set { SetValue(CornerRadiusProperty, value); } + } + /// /// Gets or sets the font family used to draw the control's text. /// @@ -174,7 +190,7 @@ namespace Avalonia.Controls.Primitives /// /// Gets or sets the brush used to draw the control's text and other foreground elements. /// - public IBrush Foreground + public IBrush? Foreground { get { return GetValue(ForegroundProperty); } set { SetValue(ForegroundProperty, value); } @@ -192,7 +208,7 @@ namespace Avalonia.Controls.Primitives /// /// Gets or sets the template that defines the control's appearance. /// - public IControlTemplate Template + public IControlTemplate? Template { get { return GetValue(TemplateProperty); } set { SetValue(TemplateProperty, value); } @@ -265,7 +281,9 @@ namespace Avalonia.Controls.Primitives var e = new TemplateAppliedEventArgs(nameScope); OnApplyTemplate(e); +#pragma warning disable CS0618 // Type or member is obsolete OnTemplateApplied(e); +#pragma warning restore CS0618 // Type or member is obsolete RaiseEvent(e); } diff --git a/src/Avalonia.Themes.Default/AutoCompleteBox.xaml b/src/Avalonia.Themes.Default/AutoCompleteBox.xaml index 66d0f17ede..fe4cd48e72 100644 --- a/src/Avalonia.Themes.Default/AutoCompleteBox.xaml +++ b/src/Avalonia.Themes.Default/AutoCompleteBox.xaml @@ -11,6 +11,7 @@ Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" + CornerRadius="{TemplateBinding CornerRadius}" Padding="{TemplateBinding Padding}" Watermark="{TemplateBinding Watermark}" DataValidationErrors.Errors="{TemplateBinding (DataValidationErrors.Errors)}" /> diff --git a/src/Avalonia.Themes.Default/Button.xaml b/src/Avalonia.Themes.Default/Button.xaml index 698ddec2a8..81d96aaa14 100644 --- a/src/Avalonia.Themes.Default/Button.xaml +++ b/src/Avalonia.Themes.Default/Button.xaml @@ -13,6 +13,7 @@ Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" + CornerRadius="{TemplateBinding CornerRadius}" ContentTemplate="{TemplateBinding ContentTemplate}" Content="{TemplateBinding Content}" Padding="{TemplateBinding Padding}" @@ -31,4 +32,4 @@ - \ No newline at end of file + diff --git a/src/Avalonia.Themes.Default/ButtonSpinner.xaml b/src/Avalonia.Themes.Default/ButtonSpinner.xaml index 89fbb9d64d..ce2b85d2b5 100644 --- a/src/Avalonia.Themes.Default/ButtonSpinner.xaml +++ b/src/Avalonia.Themes.Default/ButtonSpinner.xaml @@ -47,6 +47,7 @@ Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" + CornerRadius="{TemplateBinding CornerRadius}" Margin="{TemplateBinding Padding}" HorizontalAlignment="{TemplateBinding HorizontalAlignment}" VerticalAlignment="{TemplateBinding VerticalAlignment}"> @@ -73,6 +74,7 @@ Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" + CornerRadius="{TemplateBinding CornerRadius}" Margin="{TemplateBinding Padding}" HorizontalAlignment="{TemplateBinding HorizontalAlignment}" VerticalAlignment="{TemplateBinding VerticalAlignment}"> diff --git a/src/Avalonia.Themes.Default/Calendar.xaml b/src/Avalonia.Themes.Default/Calendar.xaml index 6bbee4ef17..4b67aa232b 100644 --- a/src/Avalonia.Themes.Default/Calendar.xaml +++ b/src/Avalonia.Themes.Default/Calendar.xaml @@ -22,10 +22,11 @@ Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" + CornerRadius="{TemplateBinding CornerRadius}" HeaderBackground="{TemplateBinding HeaderBackground}"/> - \ No newline at end of file + diff --git a/src/Avalonia.Themes.Default/CalendarDatePicker.xaml b/src/Avalonia.Themes.Default/CalendarDatePicker.xaml index aab7d06c46..57b77f70ea 100644 --- a/src/Avalonia.Themes.Default/CalendarDatePicker.xaml +++ b/src/Avalonia.Themes.Default/CalendarDatePicker.xaml @@ -88,7 +88,8 @@ \ No newline at end of file + diff --git a/src/Avalonia.Themes.Default/ContextMenu.xaml b/src/Avalonia.Themes.Default/ContextMenu.xaml index 9b84253c8a..0df4866184 100644 --- a/src/Avalonia.Themes.Default/ContextMenu.xaml +++ b/src/Avalonia.Themes.Default/ContextMenu.xaml @@ -9,6 +9,7 @@ diff --git a/src/Avalonia.Themes.Default/DatePicker.xaml b/src/Avalonia.Themes.Default/DatePicker.xaml index da878c88e2..c6c117138d 100644 --- a/src/Avalonia.Themes.Default/DatePicker.xaml +++ b/src/Avalonia.Themes.Default/DatePicker.xaml @@ -134,6 +134,7 @@ Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" + CornerRadius="{TemplateBinding CornerRadius}" IsEnabled="{TemplateBinding IsEnabled}" MinWidth="{DynamicResource DatePickerThemeMinWidth}" MaxWidth="{DynamicResource DatePickerThemeMaxWidth}" @@ -148,6 +149,7 @@ BorderBrush="{TemplateBinding BorderBrush}" Background="{TemplateBinding Background}" BorderThickness="{TemplateBinding BorderThickness}" + CornerRadius="{TemplateBinding CornerRadius}" Content="{TemplateBinding Content}" TextBlock.Foreground="{TemplateBinding Foreground}" HorizontalContentAlignment="Stretch" @@ -242,6 +244,7 @@ diff --git a/src/Avalonia.Themes.Default/Expander.xaml b/src/Avalonia.Themes.Default/Expander.xaml index 08d8b4c995..5e0958c54c 100644 --- a/src/Avalonia.Themes.Default/Expander.xaml +++ b/src/Avalonia.Themes.Default/Expander.xaml @@ -10,7 +10,10 @@ \ No newline at end of file + diff --git a/src/Avalonia.Themes.Default/MenuItem.xaml b/src/Avalonia.Themes.Default/MenuItem.xaml index 4bfae4c223..18bf79ce6c 100644 --- a/src/Avalonia.Themes.Default/MenuItem.xaml +++ b/src/Avalonia.Themes.Default/MenuItem.xaml @@ -14,7 +14,8 @@ + BorderThickness="{TemplateBinding BorderThickness}" + CornerRadius="{TemplateBinding CornerRadius}"> @@ -96,7 +97,8 @@ + BorderThickness="{TemplateBinding BorderThickness}" + CornerRadius="{TemplateBinding CornerRadius}"> diff --git a/src/Avalonia.Themes.Default/NumericUpDown.xaml b/src/Avalonia.Themes.Default/NumericUpDown.xaml index 025e822404..6740be69bb 100644 --- a/src/Avalonia.Themes.Default/NumericUpDown.xaml +++ b/src/Avalonia.Themes.Default/NumericUpDown.xaml @@ -9,6 +9,7 @@ - + diff --git a/src/Avalonia.Themes.Default/RepeatButton.xaml b/src/Avalonia.Themes.Default/RepeatButton.xaml index 702e4e6ebd..a9a03c8ed5 100644 --- a/src/Avalonia.Themes.Default/RepeatButton.xaml +++ b/src/Avalonia.Themes.Default/RepeatButton.xaml @@ -20,6 +20,7 @@ Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" + CornerRadius="{TemplateBinding CornerRadius}" ContentTemplate="{TemplateBinding ContentTemplate}" Content="{TemplateBinding Content}" Padding="{TemplateBinding Padding}" diff --git a/src/Avalonia.Themes.Default/Separator.xaml b/src/Avalonia.Themes.Default/Separator.xaml index cf0db16ee6..6a318d2e85 100644 --- a/src/Avalonia.Themes.Default/Separator.xaml +++ b/src/Avalonia.Themes.Default/Separator.xaml @@ -6,6 +6,7 @@ diff --git a/src/Avalonia.Themes.Default/TabControl.xaml b/src/Avalonia.Themes.Default/TabControl.xaml index ed2e67df28..afb5010baa 100644 --- a/src/Avalonia.Themes.Default/TabControl.xaml +++ b/src/Avalonia.Themes.Default/TabControl.xaml @@ -3,9 +3,9 @@ diff --git a/src/Avalonia.Themes.Default/TabItem.xaml b/src/Avalonia.Themes.Default/TabItem.xaml index 6e344ce58e..c7748299a0 100644 --- a/src/Avalonia.Themes.Default/TabItem.xaml +++ b/src/Avalonia.Themes.Default/TabItem.xaml @@ -12,11 +12,11 @@ Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" + CornerRadius="{TemplateBinding CornerRadius}" ContentTemplate="{TemplateBinding HeaderTemplate}" Content="{TemplateBinding Header}" HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}" - Margin="{TemplateBinding Margin}" Padding="{TemplateBinding Padding}"/> diff --git a/src/Avalonia.Themes.Default/TabStripItem.xaml b/src/Avalonia.Themes.Default/TabStripItem.xaml index 28c4c68a3d..61eecc0395 100644 --- a/src/Avalonia.Themes.Default/TabStripItem.xaml +++ b/src/Avalonia.Themes.Default/TabStripItem.xaml @@ -9,6 +9,7 @@ Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" + CornerRadius="{TemplateBinding CornerRadius}" ContentTemplate="{TemplateBinding ContentTemplate}" Content="{TemplateBinding Content}" HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" @@ -20,4 +21,4 @@ - \ No newline at end of file + diff --git a/src/Avalonia.Themes.Default/TextBox.xaml b/src/Avalonia.Themes.Default/TextBox.xaml index 12df4b6213..9471beaaeb 100644 --- a/src/Avalonia.Themes.Default/TextBox.xaml +++ b/src/Avalonia.Themes.Default/TextBox.xaml @@ -30,7 +30,8 @@ + BorderThickness="{TemplateBinding BorderThickness}" + CornerRadius="{TemplateBinding CornerRadius}"> diff --git a/src/Avalonia.Themes.Default/TimePicker.xaml b/src/Avalonia.Themes.Default/TimePicker.xaml index c76f900cfe..a58fd62a99 100644 --- a/src/Avalonia.Themes.Default/TimePicker.xaml +++ b/src/Avalonia.Themes.Default/TimePicker.xaml @@ -58,6 +58,7 @@ Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" + CornerRadius="{TemplateBinding CornerRadius}" IsEnabled="{TemplateBinding IsEnabled}" MinWidth="{DynamicResource TimePickerThemeMinWidth}" MaxWidth="{DynamicResource TimePickerThemeMaxWidth}" @@ -71,6 +72,7 @@ BorderBrush="{TemplateBinding BorderBrush}" Background="{TemplateBinding Background}" BorderThickness="{TemplateBinding BorderThickness}" + CornerRadius="{TemplateBinding CornerRadius}" Content="{TemplateBinding Content}" TextBlock.Foreground="{TemplateBinding Foreground}" HorizontalContentAlignment="Stretch" @@ -178,6 +180,7 @@ Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" + CornerRadius="{TemplateBinding CornerRadius}" Padding="{DynamicResource DateTimeFlyoutBorderPadding}" MaxHeight="398"> diff --git a/src/Avalonia.Themes.Default/ToggleButton.xaml b/src/Avalonia.Themes.Default/ToggleButton.xaml index 9e05c38eef..ffebd4f63d 100644 --- a/src/Avalonia.Themes.Default/ToggleButton.xaml +++ b/src/Avalonia.Themes.Default/ToggleButton.xaml @@ -13,6 +13,7 @@ Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" + CornerRadius="{TemplateBinding CornerRadius}" ContentTemplate="{TemplateBinding ContentTemplate}" Content="{TemplateBinding Content}" Padding="{TemplateBinding Padding}" @@ -35,4 +36,4 @@ - \ No newline at end of file + diff --git a/src/Avalonia.Themes.Default/ToolTip.xaml b/src/Avalonia.Themes.Default/ToolTip.xaml index 1fc0202dd3..35c1dceb8d 100644 --- a/src/Avalonia.Themes.Default/ToolTip.xaml +++ b/src/Avalonia.Themes.Default/ToolTip.xaml @@ -9,9 +9,10 @@ Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" + CornerRadius="{TemplateBinding CornerRadius}" ContentTemplate="{TemplateBinding ContentTemplate}" Content="{TemplateBinding Content}" Padding="{TemplateBinding Padding}"/> - \ No newline at end of file + diff --git a/src/Avalonia.Themes.Default/TreeView.xaml b/src/Avalonia.Themes.Default/TreeView.xaml index 026bed5899..990d5d0823 100644 --- a/src/Avalonia.Themes.Default/TreeView.xaml +++ b/src/Avalonia.Themes.Default/TreeView.xaml @@ -8,7 +8,8 @@ + BorderThickness="{TemplateBinding BorderThickness}" + CornerRadius="{TemplateBinding CornerRadius}"> - + + @@ -36,6 +37,7 @@ Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" + CornerRadius="{TemplateBinding CornerRadius}" FontSize="{TemplateBinding FontSize}" FontFamily="{TemplateBinding FontFamily}" FontWeight="{TemplateBinding FontWeight}" diff --git a/src/Avalonia.Themes.Fluent/Controls/Button.xaml b/src/Avalonia.Themes.Fluent/Controls/Button.xaml index 597f5d00ec..53d53ef127 100644 --- a/src/Avalonia.Themes.Fluent/Controls/Button.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/Button.xaml @@ -16,6 +16,7 @@ + @@ -29,6 +30,7 @@ Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" + CornerRadius="{TemplateBinding CornerRadius}" Content="{TemplateBinding Content}" ContentTemplate="{TemplateBinding ContentTemplate}" Padding="{TemplateBinding Padding}" @@ -93,8 +95,4 @@ - - diff --git a/src/Avalonia.Themes.Fluent/Controls/ButtonSpinner.xaml b/src/Avalonia.Themes.Fluent/Controls/ButtonSpinner.xaml index 12b4845522..d228c37912 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ButtonSpinner.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ButtonSpinner.xaml @@ -58,6 +58,7 @@ + @@ -69,7 +70,7 @@ - + + + + + + - - - - - - - - \ No newline at end of file + diff --git a/src/Avalonia.Themes.Fluent/Controls/ContextMenu.xaml b/src/Avalonia.Themes.Fluent/Controls/ContextMenu.xaml index 5110d70a80..df800b4a06 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ContextMenu.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ContextMenu.xaml @@ -39,6 +39,7 @@ + @@ -55,7 +56,7 @@ MaxWidth="{TemplateBinding MaxWidth}" MinHeight="{TemplateBinding MinHeight}" HorizontalAlignment="{TemplateBinding HorizontalAlignment}" - CornerRadius="{DynamicResource OverlayCornerRadius}"> + CornerRadius="{TemplateBinding CornerRadius}"> @@ -88,6 +89,7 @@ Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" + CornerRadius="{TemplateBinding CornerRadius}" Content="{TemplateBinding Content}" ContentTemplate="{TemplateBinding ContentTemplate}" /> diff --git a/src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml b/src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml index 032fdd9ae4..3e4471cada 100644 --- a/src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml @@ -8,6 +8,12 @@ + + + + + + 0,0,0,4 40 @@ -50,6 +56,7 @@ + diff --git a/src/Avalonia.Themes.Fluent/Controls/Expander.xaml b/src/Avalonia.Themes.Fluent/Controls/Expander.xaml index 3f70939953..d5d44e1270 100644 --- a/src/Avalonia.Themes.Fluent/Controls/Expander.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/Expander.xaml @@ -1,23 +1,23 @@ - - + + Expanded content - + Expanded content - + Expanded content - + Expanded content @@ -51,6 +51,7 @@ + @@ -140,31 +141,31 @@ - - - - - diff --git a/src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml b/src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml index 1c52c6272c..3320fc9a41 100644 --- a/src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml @@ -37,6 +37,7 @@ + @@ -59,6 +60,7 @@ Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" + CornerRadius="{TemplateBinding CornerRadius}" IsEnabled="{TemplateBinding IsEnabled}" MinWidth="{DynamicResource TimePickerThemeMinWidth}" MaxWidth="{DynamicResource TimePickerThemeMaxWidth}" @@ -72,11 +74,11 @@ BorderBrush="{TemplateBinding BorderBrush}" Background="{TemplateBinding Background}" BorderThickness="{TemplateBinding BorderThickness}" + CornerRadius="{TemplateBinding CornerRadius}" Content="{TemplateBinding Content}" TextBlock.Foreground="{TemplateBinding Foreground}" HorizontalContentAlignment="Stretch" - VerticalContentAlignment="Stretch" - CornerRadius="{DynamicResource ControlCornerRadius}" /> + VerticalContentAlignment="Stretch" /> @@ -176,13 +178,14 @@ + diff --git a/src/Avalonia.Themes.Fluent/Controls/ToggleButton.xaml b/src/Avalonia.Themes.Fluent/Controls/ToggleButton.xaml index dd8e51e4e5..b1d07059b8 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ToggleButton.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ToggleButton.xaml @@ -18,6 +18,7 @@ + @@ -29,6 +30,7 @@ Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" + CornerRadius="{TemplateBinding CornerRadius}" Content="{TemplateBinding Content}" ContentTemplate="{TemplateBinding ContentTemplate}" Padding="{TemplateBinding Padding}" diff --git a/src/Avalonia.Themes.Fluent/Controls/ToolTip.xaml b/src/Avalonia.Themes.Fluent/Controls/ToolTip.xaml index f7a1ebbc6b..debdfb2772 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ToolTip.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ToolTip.xaml @@ -49,6 +49,7 @@ + @@ -61,7 +62,7 @@ Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" Padding="{TemplateBinding Padding}" - CornerRadius="{DynamicResource OverlayCornerRadius}"> + CornerRadius="{TemplateBinding CornerRadius}"> + BorderThickness="{TemplateBinding BorderThickness}" + CornerRadius="{TemplateBinding CornerRadius}"> Date: Mon, 2 Aug 2021 10:38:40 +0300 Subject: [PATCH 105/127] add comments --- .../AvaloniaNativePlatformExtensions.cs | 35 ++++++++++++++- src/Avalonia.X11/X11Platform.cs | 43 ++++++++++++++++++- src/Windows/Avalonia.Win32/Win32Platform.cs | 35 ++++++++++++++- 3 files changed, 109 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs b/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs index 76cb7a8057..51d3556dfd 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs @@ -26,19 +26,52 @@ namespace Avalonia } } + /// + /// OSX backend options. + /// public class AvaloniaNativePlatformOptions { + /// + /// Deferred renderer would be used on Windows when set to true. Immediate renderer would be used when set to false. The default value is true. + /// + /// + /// Avalonia has two different renderers - Immediate and Deferred. + /// Immediate re-renders the whole scene when some element is changed on the scene. Deferred re-renders only changed elements. + /// public bool UseDeferredRendering { get; set; } = true; + + /// + /// Determines whether to use GPU for rendering in your project. The default value is true. + /// public bool UseGpu { get; set; } = true; + + /// + /// Embeds popups to the window when set to true. The default value is false. + /// public bool OverlayPopups { get; set; } + + /// + /// This property should be used in case you want to build Avalonia OSX native part by yourself + /// and make your Avalonia app run with it. The default value is null. + /// public string AvaloniaNativeLibraryPath { get; set; } } // ReSharper disable once InconsistentNaming + /// + /// OSX front-end options. + /// public class MacOSPlatformOptions { + /// + /// Determines whether to show your application in the dock when it runs. The default value is true. + /// public bool ShowInDock { get; set; } = true; - + + /// + /// By default, Avalonia adds items like Quit, Hide to the OSX Application Menu. + /// You can prevent Avalonia from adding those items to the OSX Application Menu with this property. The default value is false. + /// public bool DisableDefaultApplicationMenuItems { get; set; } } } diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index a57bdbdf87..0eb716badd 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -162,14 +162,48 @@ namespace Avalonia.X11 namespace Avalonia { - + /// + /// Platform-specific options which apply to Linux. + /// public class X11PlatformOptions { + /// + /// Enables native Linux EGL when set to true. The default value is false. + /// public bool UseEGL { get; set; } + + /// + /// Determines whether to use GPU for rendering in your project. The default value is true. + /// public bool UseGpu { get; set; } = true; + + /// + /// Embeds popups to the window when set to true. The default value is false. + /// public bool OverlayPopups { get; set; } + + /// + /// Enables global KDE menu. The default value is false. + /// public bool UseDBusMenu { get; set; } + + /// + /// Deferred renderer would be used on Windows when set to true. Immediate renderer would be used when set to false. The default value is true. + /// + /// + /// Avalonia has two different renderers - Immediate and Deferred. + /// Immediate re-renders the whole scene when some element is changed on the scene. Deferred re-renders only changed elements. + /// public bool UseDeferredRendering { get; set; } = true; + + /// + /// Determines whether to use IME. + /// IME would be enabled by default if user input languages contain one of the following languages: Mandarin Chinese, Japanese, Vietnamese, Korean. + /// + /// + /// Input method editor is a component that enables users to generate characters not natively available + /// on their input devices by using sequences of characters or mouse operations that are natively available on their input devices. + /// public bool? EnableIme { get; set; } public IList GlProfiles { get; set; } = new List @@ -190,6 +224,13 @@ namespace Avalonia "llvmpipe" }; public string WmClass { get; set; } = Assembly.GetEntryAssembly()?.GetName()?.Name ?? "AvaloniaApplication"; + + /// + /// Enables multitouch support. The default value is false. + /// + /// + /// Multitouch allows a surface (a touchpad or touchscreen) to recognize the presence of more than one point of contact with the surface at the same time. + /// public bool? EnableMultiTouch { get; set; } } public static class AvaloniaX11PlatformExtensions diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index 10a6db0b57..cf46db69eb 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -35,15 +35,46 @@ namespace Avalonia } } + /// + /// Platform-specific options which apply to Windows. + /// public class Win32PlatformOptions { + /// + /// Deferred renderer would be used on Windows when set to true. Immediate renderer would be used when set to false. The default value is true. + /// + /// + /// Avalonia has two different renderers - Immediate and Deferred. + /// Immediate re-renders the whole scene when some element is changed on the scene. Deferred re-renders only changed elements. + /// public bool UseDeferredRendering { get; set; } = true; - + + /// + /// Enables ANGLE for Windows. For every Windows which is above Win 7, the default is true,otherwise-false. + /// + /// + /// GPU would not be used for rendering if you would set that to false. + /// public bool? AllowEglInitialization { get; set; } - + + /// + /// Enables multitouch support. The default value is false. + /// + /// + /// Multitouch allows a surface (a touchpad or touchscreen) to recognize the presence of more than one point of contact with the surface at the same time. + /// public bool? EnableMultitouch { get; set; } + + /// + /// Embeds popups to the window when set to true. The default value is false. + /// public bool OverlayPopups { get; set; } + + /// + /// Avalonia would try to use native Widows OpenGL when set to true. The default value is false. + /// public bool UseWgl { get; set; } + public IList WglProfiles { get; set; } = new List { new GlVersion(GlProfileType.OpenGL, 4, 0), From 01b87381b7d56414e9e718132a0733aedf94fc62 Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Tue, 3 Aug 2021 12:22:37 +0300 Subject: [PATCH 106/127] enable multitouch --- src/Avalonia.X11/X11Platform.cs | 2 +- src/Windows/Avalonia.Win32/Win32Platform.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index a57bdbdf87..4561993279 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -190,7 +190,7 @@ namespace Avalonia "llvmpipe" }; public string WmClass { get; set; } = Assembly.GetEntryAssembly()?.GetName()?.Name ?? "AvaloniaApplication"; - public bool? EnableMultiTouch { get; set; } + public bool? EnableMultiTouch { get; set; } = true; } public static class AvaloniaX11PlatformExtensions { diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index 10a6db0b57..8cfb910e4b 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -40,8 +40,8 @@ namespace Avalonia public bool UseDeferredRendering { get; set; } = true; public bool? AllowEglInitialization { get; set; } - - public bool? EnableMultitouch { get; set; } + + public bool? EnableMultitouch { get; set; } = true; public bool OverlayPopups { get; set; } public bool UseWgl { get; set; } public IList WglProfiles { get; set; } = new List From c2c97901118dee637bd84925af7119ed77f430ea Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Wed, 4 Aug 2021 12:21:56 +0300 Subject: [PATCH 107/127] update --- src/Avalonia.X11/X11Platform.cs | 2 +- src/Windows/Avalonia.Win32/Win32Platform.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index f4a1079455..df1c542159 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -226,7 +226,7 @@ namespace Avalonia public string WmClass { get; set; } = Assembly.GetEntryAssembly()?.GetName()?.Name ?? "AvaloniaApplication"; /// - /// Enables multitouch support. The default value is false. + /// Enables multitouch support. The default value is true. /// /// /// Multitouch allows a surface (a touchpad or touchscreen) to recognize the presence of more than one point of contact with the surface at the same time. diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index 86f348bd01..0240655761 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -58,7 +58,7 @@ namespace Avalonia public bool? AllowEglInitialization { get; set; } /// - /// Enables multitouch support. The default value is false. + /// Enables multitouch support. The default value is true. /// /// /// Multitouch allows a surface (a touchpad or touchscreen) to recognize the presence of more than one point of contact with the surface at the same time. From f283921e281077c74fd6c3b135e624b00b848293 Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Wed, 4 Aug 2021 12:50:15 +0300 Subject: [PATCH 108/127] fix --- src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs | 4 ++-- src/Avalonia.X11/X11Platform.cs | 6 +++--- src/Windows/Avalonia.Win32/Win32Platform.cs | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs b/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs index 51d3556dfd..eef765e7ec 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs @@ -32,10 +32,10 @@ namespace Avalonia public class AvaloniaNativePlatformOptions { /// - /// Deferred renderer would be used on Windows when set to true. Immediate renderer would be used when set to false. The default value is true. + /// Deferred renderer would be used when set to true. Immediate renderer when set to false. The default value is true. /// /// - /// Avalonia has two different renderers - Immediate and Deferred. + /// Avalonia has two rendering modes: Immediate and Deferred rendering. /// Immediate re-renders the whole scene when some element is changed on the scene. Deferred re-renders only changed elements. /// public bool UseDeferredRendering { get; set; } = true; diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index df1c542159..1f9c91aa2b 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -188,17 +188,17 @@ namespace Avalonia public bool UseDBusMenu { get; set; } /// - /// Deferred renderer would be used on Windows when set to true. Immediate renderer would be used when set to false. The default value is true. + /// Deferred renderer would be used when set to true. Immediate renderer when set to false. The default value is true. /// /// - /// Avalonia has two different renderers - Immediate and Deferred. + /// Avalonia has two rendering modes: Immediate and Deferred rendering. /// Immediate re-renders the whole scene when some element is changed on the scene. Deferred re-renders only changed elements. /// public bool UseDeferredRendering { get; set; } = true; /// /// Determines whether to use IME. - /// IME would be enabled by default if user input languages contain one of the following languages: Mandarin Chinese, Japanese, Vietnamese, Korean. + /// IME would be enabled by default if the current user input language is one of the following: Mandarin, Japanese, Vietnamese or Korean. /// /// /// Input method editor is a component that enables users to generate characters not natively available diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index 0240655761..a881c45cd0 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -41,19 +41,19 @@ namespace Avalonia public class Win32PlatformOptions { /// - /// Deferred renderer would be used on Windows when set to true. Immediate renderer would be used when set to false. The default value is true. + /// Deferred renderer would be used when set to true. Immediate renderer when set to false. The default value is true. /// /// - /// Avalonia has two different renderers - Immediate and Deferred. + /// Avalonia has two rendering modes: Immediate and Deferred rendering. /// Immediate re-renders the whole scene when some element is changed on the scene. Deferred re-renders only changed elements. /// public bool UseDeferredRendering { get; set; } = true; /// - /// Enables ANGLE for Windows. For every Windows which is above Win 7, the default is true,otherwise-false. + /// Enables ANGLE for Windows. For every Windows version that is above Windows 7, the default is true otherwise it's false. /// /// - /// GPU would not be used for rendering if you would set that to false. + /// GPU rendering will not be enabled if this is set to false. /// public bool? AllowEglInitialization { get; set; } From 037047299791cee0338a17f241614300bdc96e85 Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Wed, 4 Aug 2021 13:13:36 +0300 Subject: [PATCH 109/127] update --- src/Avalonia.X11/X11Platform.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index 1f9c91aa2b..3a919c8814 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -183,7 +183,8 @@ namespace Avalonia public bool OverlayPopups { get; set; } /// - /// Enables global KDE menu. The default value is false. + /// Enables global menu support on Linux desktop environments where it's supported (e. g. XFCE and MATE with plugin, KDE, etc). + /// The default value is false. /// public bool UseDBusMenu { get; set; } From dbb8427e3a13580ac2cc53558e40d2d50ec21396 Mon Sep 17 00:00:00 2001 From: Luis von der Eltz Date: Wed, 4 Aug 2021 15:01:14 +0200 Subject: [PATCH 110/127] Make event actually keydown --- src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs index a85fe6c0c4..ea06c33e4d 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs @@ -1,9 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Reactive.Disposables; using System.Reactive.Linq; -using System.Reactive.Subjects; using Avalonia.Controls; using Avalonia.Controls.Diagnostics; using Avalonia.Controls.Primitives; @@ -12,7 +10,6 @@ using Avalonia.Diagnostics.ViewModels; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Markup.Xaml; -using Avalonia.Media; using Avalonia.Styling; using Avalonia.VisualTree; @@ -30,6 +27,7 @@ namespace Avalonia.Diagnostics.Views _keySubscription = InputManager.Instance.Process .OfType() + .Where(x => x.Type == RawKeyEventType.KeyDown) .Subscribe(RawKeyDown); _frozenPopupStates = new Dictionary(); From 5fb8126d2dfbb55ed62772a718f1610276794961 Mon Sep 17 00:00:00 2001 From: Luis von der Eltz Date: Wed, 4 Aug 2021 15:41:31 +0200 Subject: [PATCH 111/127] Add label for frozen popups to status bar --- .../Diagnostics/Views/MainView.xaml | 56 +++++++++++-------- 1 file changed, 33 insertions(+), 23 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml index 8c4db33f91..6f2ac96a66 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml @@ -5,14 +5,14 @@ - + + IsEnabled="False" /> @@ -21,58 +21,68 @@ + IsEnabled="False" /> + IsEnabled="False" /> - + + IsEnabled="False" /> - + - - - + + + + Content="{Binding Content}" /> - + IsVisible="False" /> + + IsVisible="{Binding IsVisible}" /> + - - Hold Ctrl+Shift over a control to inspect. - - Focused: - - - Pointer Over: - - + + + Hold Ctrl+Shift over a control to inspect. + + Focused: + + + Pointer Over: + + + + + + From 7e6d59d6702e1c679c5cd77193bda8f8f3bd3812 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 5 Aug 2021 11:04:52 +0200 Subject: [PATCH 112/127] Respect CancellationToken in RunLoop. --- .../Platform/InternalPlatformThreadingInterface.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs b/src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs index cb1291410a..a5495fdfc9 100644 --- a/src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs +++ b/src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs @@ -21,10 +21,12 @@ namespace Avalonia.Controls.Platform public void RunLoop(CancellationToken cancellationToken) { - while (true) + var handles = new[] { _signaled, cancellationToken.WaitHandle }; + + while (!cancellationToken.IsCancellationRequested) { Signaled?.Invoke(null); - _signaled.WaitOne(); + WaitHandle.WaitAny(handles); } } From dc404b545e534ea904911ca9647f312f77e7ed6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Fri, 6 Aug 2021 08:49:05 +0200 Subject: [PATCH 113/127] Fix skia OpacityMask push and pop methods --- src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 2352b8b076..ea4c35d6e7 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -534,7 +534,11 @@ namespace Avalonia.Skia public void PushOpacityMask(IBrush mask, Rect bounds) { // TODO: This should be disposed - var paint = new SKPaint(); + var paint = new SKPaint() + { + IsAntialias = true, + Style = SKPaintStyle.StrokeAndFill + }; Canvas.SaveLayer(paint); _maskStack.Push(CreatePaint(paint, mask, bounds, true)); @@ -543,7 +547,14 @@ namespace Avalonia.Skia /// public void PopOpacityMask() { - using (var paint = new SKPaint { BlendMode = SKBlendMode.DstIn }) + using (var paint = new SKPaint + { + IsAntialias = true, + Style = SKPaintStyle.StrokeAndFill, + BlendMode = SKBlendMode.DstIn, + Color = new SKColor(0, 0, 0, 255), + ColorFilter = SKColorFilter.CreateLumaColor() + }) { Canvas.SaveLayer(paint); using (var paintWrapper = _maskStack.Pop()) From 39e7d362b2fffc02c88c306301951b844d3849d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Fri, 6 Aug 2021 09:07:18 +0200 Subject: [PATCH 114/127] Enable antialiasing for PushGeometryClip --- src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 2 +- src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 2352b8b076..1f186396ab 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -506,7 +506,7 @@ namespace Avalonia.Skia public void PushGeometryClip(IGeometryImpl clip) { Canvas.Save(); - Canvas.ClipPath(((GeometryImpl)clip).EffectivePath); + Canvas.ClipPath(((GeometryImpl)clip).EffectivePath, SKClipOperation.Intersect, true); } /// diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index 9336c9a7bb..09e5b7c71a 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs @@ -541,7 +541,8 @@ namespace Avalonia.Direct2D1.Media ContentBounds = PrimitiveExtensions.RectangleInfinite, MaskTransform = PrimitiveExtensions.Matrix3x2Identity, Opacity = 1, - GeometricMask = ((GeometryImpl)clip).Geometry + GeometricMask = ((GeometryImpl)clip).Geometry, + MaskAntialiasMode = AntialiasMode.PerPrimitive }; var layer = _layerPool.Count != 0 ? _layerPool.Pop() : new Layer(_deviceContext); _deviceContext.PushLayer(ref parameters, layer); From 5b9f06865e212be05e8ec5c2d1657029fe0f074a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Fri, 6 Aug 2021 20:10:03 +0200 Subject: [PATCH 115/127] Update expected test files --- .../CustomRender/GeometryClip.expected.png | Bin 2258 -> 4082 bytes .../Geometry_Clip_Clips_Path.expected.png | Bin 728 -> 660 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/tests/TestFiles/Skia/Controls/CustomRender/GeometryClip.expected.png b/tests/TestFiles/Skia/Controls/CustomRender/GeometryClip.expected.png index 535d411cc7a02d6f26a14b9ef1392a9a7cc928d6..e2604d28362214beb03612498936b8c90a420418 100644 GIT binary patch literal 4082 zcmYk92UJtdv%u+pdIt$cI!Z60H)#<9Qlx{40-+-%fFfYPpr6!$bV8MmbO_xjCQqRo$tK&=A3(X@7)9^_EYXPYTNoH;X_ z$Hj`@7PFag!N8LYtZc@qo8Mi=!rd)83#xrqg{Af^&Z21jxBcR_it$p{tt@j$SzS|* zt*))cg!o!J7mGGITCO*5qSH~z)Tdpo+=f96U`@smljDj>ARwOxe===}RJI<0*L!iO zH5LZ7%`@QDr$R)>5!c48%}Pn3wUF{b6jdcx-ssmW1k1G7)Q!7_*ftc>z4{x z1Jn~~BPM-uD{d{k2URq`jB0~os|EjH#z5pw*Rff)*zR*5>a;A%biAfn zefYDyQHh(_MN{_&%2!9yM^s}6TbIrB)-_z)E-9lNn8Jw;3qGc^zPQX8TS@3>>4NKB zLh~|uyc~tJ!%2b;FoC~6d6OIMpYo3)i8+hhvw{7UL%&ge<)0S@8_Wb`+n^?9>mT74 zInKFsraNk@O;}Z-x1EG11fY07q>@C&m9Y+N**atLI_7LoYEpd<# zO9tk)lLw9^v-eGgU1}smeep;Tw*h}QRK9@!v{OXZNsHzTZA5>E5uj~Q4U0D$@JCEF z91Y27y)RDzHjY&Y`~<5h2F$~z5L>!76EM)^SStht^gPlYhQhpYJbNEryzK*3*z~Z@ z*WoX}|JV(Y%l^r)1@LZW*_&l=+vju$mfMArM5my&zET|+HST1ZxgmBrB1?4@*|ltu zt0_EY9V09Ve4Qvy_r*!W1XMMa~m@ zq^<{PG)$O|7geP2ZJS8T2Z?9aGWaDE=*nV(yLx?(KdHs7hztM5zH0A8g`5bb?1=(W zlF&uZd_zAi?-q+k(=`{UbzneSk%C&mmMXJjE*H_4R_gdB5;GBG@6_pOzyi@)?1-5nC99_PSSlU&r#Bd~E64UYen*!Of`Ri;|xb8fdSA zF#-534HZE5nEh+0%(aoe3L(~=t~w!2%x!M>2p2Yg+Ed!cl0f)9(Y~{1IONswY@V_l zs+eXIc`m8cI}om;42=kon}jr>bfF3XaaXn?xGk&m7#_kE=6TrTGYoQ1@|8CG zisb^YoG|Wd1=y&I1?NhvJzgM&11+Tq!Vy+RI?jkU8%E$WRSfB~B6iqLY=UP^(o+)1 z=`;@*u^HJ{j@jl}qhAE{#*%N(LVk9JNhhR%tlRgo)Q0m27ij_%_8I|Mi|lbKTsr&m zNU!H4#F#@jtkeFUf79K~-tS0#rbw8j3};r?j+ff`U^%I*ffn0$_k-;VL%5r6WO%&v zy~SxeJ@}uP#I)9hfM(DVoa8Q4?pK-wbKpvtNx6L$3NT>MO8)b97h3DpX|a+r<0Cr# zWxLq}$nM`DR=)tl0sNbCC5z&8Fo%!1;`0MqniDCxn<@`Nx&oT2F=YsdU+bqNwGQ>t zKkh*GRQ2IQ{xJz5>72BWUq!6aee{5@^i`G6D}KSVSrFy?FxEX1a|tO^Ouu4-ofeF> zxNb~40IhZP^JFsAX%XjSF`S(|P4?>YYf@D*TS>vs^xPQ;WZ0Atdb*NV$j=$iUpd`U zibCSOEl1(X_5{+FKr@qW4et>#A!GirK~{6_5_#o54IMl3Pn7i{@WBf zhiaMp^zZLF6l7{&Txbq%VttD3v(^)<;$}~k+hi7*q@@LyF_BFMW1Z$u+!H_}L6PE+ zVE;-Mh~?c#A0si}eAql=oV%P6O;5E8jJ0r;y;3NQ4iN^uYDs`Bn7euywL9pE)wuJg zw2Tu~Tr_VkjVQ!458i@`9G5HX6mNEPrKREB6jTpbF1D~b9Jq_Z%h3}{f>MnEQBp-YBxZ}NDS8gQ52maME#D~HN}^cPrRt8M8(ICBts{CGOoE(hMeI%vbX)@^0job|sB#|XMuup1`kuTc_D zA{2D(g{0{>-^;BaLdf;a*h9B3pe)8}4MALkAAibb3d>_t9~gHPUm07WULsQc#x73>i8l9bO=O?fegh4? zW_#irUj^E7>htJ|?Tw#sNMSw72)en8&AZTNmS_0*YVqyxY4o{X(gNdTF*@Qtk&(=l zHMH>^g)RFa2ggN!M<#kb+AlI5bc-7BW|sJ(A3#@0rP@pX)GsIsSYM*{;Aq=?N?dqa zUT4A;|Mt}Tj^o!b1ITSl=R3DbiHf={gK#c(Y9i&2=8I9Q*}6YDe+h&!j`BdgJ(rs_ z)ooRckd754c6W*&#(%nZIVC_Ok^KYZy*Nt}aJ7`>J^UId0 zu3#xBg1ad;yCcoG zGyRjgAMRR^WVUM5V0UAhB=@8{?wYYkcb}$}3J+{J1lhkl8*mGx@J}7t#$}@AUr^j> zcf3(ul&gCn>)#1qv%bIHfD{d0L5PG>Mqe%m>kuOAofXWUT^W%55)W_h)BJe%dN7ye zgJB*;t3~Sqi6Ce#m$|#_QO2Hv?%ukG3qEfNxF&p&nS_o*sv}*`O*8?Yy_gl3y{bs6 z*tV{5>jQw{j#`?&hn=3LuS%vTb8Fi?A(!8(pViw3c6CmF7 zC6AgY{6oe6Vf$bDoUZ?B3wg{%67_5q(RsNAo0(bTUxqmF3xLm_t4ryKC_`I^TbJEMp<>&u=SE$?*acIJ9hv!IRiFN!Is|wi%jML4SnsDKoLd7~% z$7itcFlJ07;H$Y@PuXQdLMC@iVp#gN(@3lKmH7?R@O5Lo+-T&oW{C|lv^QgVw%8bb<(U9U->GsJ0|zw(XUYTAdRJ?P-n_)R&e$r~SV?kfLF|4-V^1kXiA WkKxzPlE_Isg~=@oaJ_-cv;P8IdBoQM literal 2258 zcmb_eYfuwc6wX6LQ$VW*7r{ zEgK_<3bJe%P)Y$&*&SYA>H9^}*|5uv0KgJ&mr8Imw3Y7U8XGWWBA zI0x`PJbC_#R1(Rh30`FFx?fh4NW&5qgwGLfT=n#72gal zto+<0Vcrm%|9-dPripA`)T-EFzXkd8%` z{Brn)qpl!s>DG!xMZTv%6{_6Jnyjg_GJ&jmZTxMXvXBcxJ(5-=7DcxB0N0+A=0M|7 z%LqWKHDYcZw+2C47Iv?@V|rR9$Xo6->q6GuJFx)e9hSSkDD9OSa+P{^+(E8^{yvWL zShK+QqzR=Dc-_PfLr6>FLmBwq3lgZ>Tbn~YlkW{eo!)m7;I9G^ErC{!OK(zohhF(m z=I-9mYux8|v5UpZIlul${bX0|UdoZ>o-=5BBpy1E(>iaJHtTK_ zRN;1WSCFx$vpAq7)kE&(m_Fm9(l$rEA%#`e0YO8ydxLtP`GL}Mi?sms=WbBCw1e%^ z`^*OBxyLgB(6FFX7wa=)f+uqRDjSlT8y$P`*vj`JpY{eSl`lW7Yuy z1df>!hIoc%0AVwBs1=vZR!12BxfekROwjotK$?nX+QYn81OePQhFGS_*pGsN=5XC? zjY0s>GnPtqBM@MbinctP$wj?nG(mB#c-u6@mxMe${=W0P>o!Uj{rTsjsfb?bPk$cx z&b}#Tv_)uQP~$8)FRWK$RTL;w_7qeO-1`IG#9RXhrX0GFq>pwqH(7= zh5CsdNi+^Or%XWOBY3kPv=N=R5Y)4M}ydxpS|5&PKNYl(}~b9%sdj(o5l-@$r@}A6!3$S#~?KAV|pO^ zO>qpY8TUf;v)J^d@wyzOtNp>&!p~MPgO9>&BH|bsO*j>~+aHfYf-_y@g7hoQQb#x7 zThRfNAU$4evW5f6Y-enbJ65QUM`}WfJ2f&eP-SZkdfh)l8abd!5`pf}-PN+UDd2hu zHvF6F#6$h%1G2%v>X0)+k81(XGlB!2{RLP=Bz2nYy#2xN!=00K)%L_t(|obB4XP3=GsfZ^Xc-O#{k z;sQqksXZog2`ruzd16>X|PFsNu_jeK9v^wP0Q1~3S1^FjPyRhTAa{4i+%jjbpG0WCzL3{f z0dz9sE>B)m#8(nUg=K#CWu8J zAG}nsg~`nSr&X|lJkX-=sGc=i^!-e-M2o(gSypJ#_cF}_E&5L8X`@Bo$0bd)=)1V3 zg%*7e*K{<{qVM3|RcH0pHZAnByRCr$Pm(fw=Knz5k!G=NzOD2#Rp%4-w7-_c+X4Pvv;~Ci= z&8vG~jMb=1S9Lci<0yZ+tGpGaupf4HKf4?MG+OG!-ho=>*v7+ zwk}rBhYf96ti263xK*+8cG&P1#kyr+0h6Dfx3D7WESBM+gsuERW} zZkT0D=zV39>55sngq~L>dG45nx9WLilIPFc+b~`(cJVEGU72Lry4-beNLnM*6Q&3Z^Kv_I0U}b6QMcwXaK+oKy2t zmdACew|~@=<91!@J@VvuU6+c;^TU>_A>!JAEm&GawF6tSq=;z?wrD94(H?Bs5@LLt zu!XmZ(e1*P-Xg}f4O=`{jBFpae2y3w16Dw)7!?OrLW&p@3s%HhF(MwUj5Xr(n6N_r z6Q9O~m9iv0iw!I0nfRo`%3 Date: Sat, 7 Aug 2021 13:12:09 +0200 Subject: [PATCH 116/127] Allow for controling delay of scrollbar hide/show. --- src/Avalonia.Controls/Primitives/ScrollBar.cs | 34 +++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/ScrollBar.cs b/src/Avalonia.Controls/Primitives/ScrollBar.cs index d264ed76cc..d5d6af8bfa 100644 --- a/src/Avalonia.Controls/Primitives/ScrollBar.cs +++ b/src/Avalonia.Controls/Primitives/ScrollBar.cs @@ -57,6 +57,18 @@ namespace Avalonia.Controls.Primitives public static readonly StyledProperty AllowAutoHideProperty = AvaloniaProperty.Register(nameof(AllowAutoHide), true); + /// + /// Defines the property. + /// + public static readonly StyledProperty HideDelayProperty = + AvaloniaProperty.Register(nameof(HideDelay), TimeSpan.FromSeconds(2)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty ShowDelayProperty = + AvaloniaProperty.Register(nameof(ShowDelay), TimeSpan.FromSeconds(0.5)); + private Button _lineUpButton; private Button _lineDownButton; private Button _pageUpButton; @@ -126,6 +138,24 @@ namespace Avalonia.Controls.Primitives get => GetValue(AllowAutoHideProperty); set => SetValue(AllowAutoHideProperty, value); } + + /// + /// Gets a value that determines how long will be the hide delay after user stops interacting with the scrollbar. + /// + public TimeSpan HideDelay + { + get => GetValue(HideDelayProperty); + set => SetValue(HideDelayProperty, value); + } + + /// + /// Gets a value that determines how long will be the show delay when user starts interacting with the scrollbar. + /// + public TimeSpan ShowDelay + { + get => GetValue(ShowDelayProperty); + set => SetValue(ShowDelayProperty, value); + } public event EventHandler Scroll; @@ -296,12 +326,12 @@ namespace Avalonia.Controls.Primitives private void CollapseAfterDelay() { - InvokeAfterDelay(Collapse, TimeSpan.FromSeconds(2)); + InvokeAfterDelay(Collapse, HideDelay); } private void ExpandAfterDelay() { - InvokeAfterDelay(Expand, TimeSpan.FromMilliseconds(400)); + InvokeAfterDelay(Expand, ShowDelay); } private void Collapse() From da06a15d16d9fb0fbe12e970aceebedd0a7611ad Mon Sep 17 00:00:00 2001 From: Sergey Mikolaytis Date: Sun, 8 Aug 2021 02:15:21 +0300 Subject: [PATCH 117/127] [OverlayPopupHost] remove render white rect override --- src/Avalonia.Controls/Primitives/OverlayPopupHost.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs b/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs index 762d8d37a6..403902f676 100644 --- a/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs +++ b/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs @@ -140,10 +140,5 @@ namespace Avalonia.Controls.Primitives return new OverlayPopupHost(overlayLayer); } - - public override void Render(DrawingContext context) - { - context.FillRectangle(Brushes.White, new Rect(default, Bounds.Size)); - } } } From ae222e25e6b27b0035421d2583c2b37a4036fff1 Mon Sep 17 00:00:00 2001 From: Luis von der Eltz Date: Tue, 10 Aug 2021 11:30:19 +0200 Subject: [PATCH 118/127] Mark IVisualTreeHost [Obsolete] --- src/Avalonia.Visuals/VisualTree/IVisualTreeHost.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Avalonia.Visuals/VisualTree/IVisualTreeHost.cs b/src/Avalonia.Visuals/VisualTree/IVisualTreeHost.cs index b63c49bd5a..01212b238d 100644 --- a/src/Avalonia.Visuals/VisualTree/IVisualTreeHost.cs +++ b/src/Avalonia.Visuals/VisualTree/IVisualTreeHost.cs @@ -1,8 +1,11 @@ +using System; + namespace Avalonia.VisualTree { /// /// Interface for controls that host their own separate visual tree, such as popups. /// + [Obsolete] public interface IVisualTreeHost { /// From d666a4263c580b21c7221a972ee34aab65c86c6a Mon Sep 17 00:00:00 2001 From: "Luis v.d.Eltz" Date: Tue, 10 Aug 2021 11:30:42 +0200 Subject: [PATCH 119/127] Update comment Co-authored-by: Steven Kirk --- src/Avalonia.Controls/Diagnostics/ToolTipDiagnostics.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Diagnostics/ToolTipDiagnostics.cs b/src/Avalonia.Controls/Diagnostics/ToolTipDiagnostics.cs index 58174b1039..4dfe5eb174 100644 --- a/src/Avalonia.Controls/Diagnostics/ToolTipDiagnostics.cs +++ b/src/Avalonia.Controls/Diagnostics/ToolTipDiagnostics.cs @@ -3,7 +3,7 @@ namespace Avalonia.Controls.Diagnostics { /// - /// Helper class to provide some diagnostics insides into . + /// Helper class to provide diagnostics information for . /// public static class ToolTipDiagnostics { From 08849539a72ccb7163bee028fd70fea20aa13b67 Mon Sep 17 00:00:00 2001 From: "Luis v.d.Eltz" Date: Tue, 10 Aug 2021 11:31:39 +0200 Subject: [PATCH 120/127] Update comment Co-authored-by: Steven Kirk --- src/Avalonia.Controls/Diagnostics/ToolTipDiagnostics.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Avalonia.Controls/Diagnostics/ToolTipDiagnostics.cs b/src/Avalonia.Controls/Diagnostics/ToolTipDiagnostics.cs index 4dfe5eb174..4acf2a217f 100644 --- a/src/Avalonia.Controls/Diagnostics/ToolTipDiagnostics.cs +++ b/src/Avalonia.Controls/Diagnostics/ToolTipDiagnostics.cs @@ -7,6 +7,9 @@ namespace Avalonia.Controls.Diagnostics /// public static class ToolTipDiagnostics { + /// + /// Provides access to the internal for use in DevTools. + /// public static AvaloniaProperty ToolTipProperty = ToolTip.ToolTipProperty; } } From 71ac5e3db8990c3b5a10bdf809dad8d68ceca4e9 Mon Sep 17 00:00:00 2001 From: Luis von der Eltz Date: Tue, 10 Aug 2021 11:41:05 +0200 Subject: [PATCH 121/127] Only IPopupHost should be root visual --- src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs index 4b957c2382..94707ac189 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs @@ -54,8 +54,7 @@ namespace Avalonia.Diagnostics.ViewModels } } - private bool IsRoot => Visual is TopLevel || - Visual is Popup || + private bool IsRoot => Visual is TopLevel || Visual is ContextMenu || Visual is IPopupHost; From 346015d804e777a1ea2f32f2252841a9135845ae Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 11 Aug 2021 21:03:52 +0200 Subject: [PATCH 122/127] Call WindowStateChanged when setting WindowState via code. Fixes #6399 --- native/Avalonia.Native/src/OSX/window.mm | 1 + 1 file changed, 1 insertion(+) diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 9c6a0e6187..0a78558d27 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -1204,6 +1204,7 @@ private: } _actualWindowState = _lastWindowState; + WindowEvents->WindowStateChanged(_actualWindowState); } From 24c8af1b626dbc2376cd67006f847b8464ef40b4 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 11 Aug 2021 21:04:37 +0200 Subject: [PATCH 123/127] Manually restore parent windows before showing child. --- native/Avalonia.Native/src/OSX/window.mm | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 0a78558d27..14fe60ab0b 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -713,6 +713,12 @@ private: if(cparent == nullptr) return E_INVALIDARG; + // If one tries to show a child window with a minimized parent window, then the parent window will be + // restored but MacOS isn't kind enough to *tell* us that, so the window will be left in a non-interactive + // state. Detect this and explicitly restore the parent window ourselves to avoid this situation. + if (cparent->WindowState() == Minimized) + cparent->SetWindowState(Normal); + [cparent->Window addChildWindow:Window ordered:NSWindowAbove]; UpdateStyle(); From 964121312c29c43ef743f7a2d0076ec268696838 Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Sat, 14 Aug 2021 13:53:49 +0300 Subject: [PATCH 124/127] fix --- build/MicroCom.targets | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build/MicroCom.targets b/build/MicroCom.targets index b48e377fd4..49d2cdce72 100644 --- a/build/MicroCom.targets +++ b/build/MicroCom.targets @@ -15,7 +15,8 @@ Inputs="@(AvnComIdl);$(MSBuildThisFileDirectory)../src/tools/MicroComGenerator/**/*.cs" Outputs="%(AvnComIdl.OutputFile)"> - + From 29f4806cfdeb1e59c36686b3f9d8264c009cfd19 Mon Sep 17 00:00:00 2001 From: Sergey Mikolaytis Date: Mon, 16 Aug 2021 09:01:07 +0300 Subject: [PATCH 125/127] [OSX] [Native] fix tab shortcuts in NativeMenu --- native/Avalonia.Native/src/OSX/KeyTransform.mm | 1 + 1 file changed, 1 insertion(+) diff --git a/native/Avalonia.Native/src/OSX/KeyTransform.mm b/native/Avalonia.Native/src/OSX/KeyTransform.mm index 6b7d95b619..4817ad0ccf 100644 --- a/native/Avalonia.Native/src/OSX/KeyTransform.mm +++ b/native/Avalonia.Native/src/OSX/KeyTransform.mm @@ -222,6 +222,7 @@ std::map s_QwertyKeyMap = { 45, "n" }, { 46, "m" }, { 47, "." }, + { 48, "\t" }, { 49, " " }, { 50, "`" }, { 51, "" }, From 08dea6498ede1422991635994409ff6eb9df81bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Mon, 16 Aug 2021 19:46:08 +0200 Subject: [PATCH 126/127] Check for TextBox.Text null in AutoCompleteBox --- src/Avalonia.Controls/AutoCompleteBox.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs index c656ba6f6c..5a6e78f441 100644 --- a/src/Avalonia.Controls/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox.cs @@ -2005,7 +2005,7 @@ namespace Avalonia.Controls // The TextBox.TextChanged event was not firing immediately and // was causing an immediate update, even with wrapping. If there is // a selection currently, no update should happen. - if (IsTextCompletionEnabled && TextBox != null && TextBoxSelectionLength > 0 && TextBoxSelectionStart != TextBox.Text.Length) + if (IsTextCompletionEnabled && TextBox != null && TextBoxSelectionLength > 0 && TextBoxSelectionStart != (TextBox.Text?.Length ?? 0)) { return; } @@ -2303,7 +2303,7 @@ namespace Avalonia.Controls { if (IsTextCompletionEnabled && TextBox != null && userInitiated) { - int currentLength = TextBox.Text.Length; + int currentLength = TextBox.Text?.Length ?? 0; int selectionStart = TextBoxSelectionStart; if (selectionStart == text.Length && selectionStart > _textSelectionStart) { From 834f3b01b8601790ed1e4b6bbe8b35fca47c81bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Mon, 16 Aug 2021 20:25:46 +0200 Subject: [PATCH 127/127] Add Not value converter to BoolConverters --- src/Avalonia.Base/Data/Converters/BoolConverters.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Avalonia.Base/Data/Converters/BoolConverters.cs b/src/Avalonia.Base/Data/Converters/BoolConverters.cs index 9329cdd6af..3985c5e32f 100644 --- a/src/Avalonia.Base/Data/Converters/BoolConverters.cs +++ b/src/Avalonia.Base/Data/Converters/BoolConverters.cs @@ -18,5 +18,11 @@ namespace Avalonia.Data.Converters /// public static readonly IMultiValueConverter Or = new FuncMultiValueConverter(x => x.Any(y => y)); + + /// + /// A value converter that returns true when input is false and false when input is true. + /// + public static readonly IValueConverter Not = + new FuncValueConverter(x => !x); } }