From 2033a43b9b904573483173b97d7b8956a4d47eed Mon Sep 17 00:00:00 2001 From: ahopper Date: Fri, 13 Sep 2019 11:16:36 +0100 Subject: [PATCH 01/57] fix pointerover on mouse leaving window --- src/Windows/Avalonia.Win32/WindowImpl.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 9bd58c10bc..45daeaaef9 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -619,7 +619,7 @@ namespace Avalonia.Win32 timestamp, _owner, RawPointerEventType.LeaveWindow, - new Point(), WindowsKeyboardDevice.Instance.Modifiers); + new Point(-1,-1), WindowsKeyboardDevice.Instance.Modifiers); break; case UnmanagedMethods.WindowsMessage.WM_NCLBUTTONDOWN: @@ -634,7 +634,7 @@ namespace Avalonia.Win32 : msg == (int)UnmanagedMethods.WindowsMessage.WM_NCRBUTTONDOWN ? RawPointerEventType.RightButtonDown : RawPointerEventType.MiddleButtonDown, - new Point(0, 0), GetMouseModifiers(wParam)); + PointToClient(PointFromLParam(lParam)), GetMouseModifiers(wParam)); break; case WindowsMessage.WM_TOUCH: var touchInputs = new TOUCHINPUT[wParam.ToInt32()]; From 00263f71e4b7e3a9acb93444e07b10b5e35fa1a4 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sat, 26 Oct 2019 16:49:10 +0200 Subject: [PATCH 02/57] Port GridSplitter from WPF. --- src/Avalonia.Controls/GridSplitter.cs | 872 +++++++++++++++--- src/Avalonia.Themes.Default/GridSplitter.xaml | 56 +- .../GridSplitterTests.cs | 313 ++++--- 3 files changed, 955 insertions(+), 286 deletions(-) diff --git a/src/Avalonia.Controls/GridSplitter.cs b/src/Avalonia.Controls/GridSplitter.cs index 28b9b3a38f..e5749c8ed9 100644 --- a/src/Avalonia.Controls/GridSplitter.cs +++ b/src/Avalonia.Controls/GridSplitter.cs @@ -1,210 +1,848 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. +// This source file is adapted from the Windows Presentation Foundation project. +// (https://github.com/dotnet/wpf/) +// +// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. using System; -using System.Collections.Generic; -using System.Linq; +using System.Diagnostics; +using Avalonia.Collections; using Avalonia.Controls.Primitives; using Avalonia.Input; +using Avalonia.Interactivity; using Avalonia.Layout; -using Avalonia.VisualTree; +using Avalonia.Media; +using Avalonia.Utilities; namespace Avalonia.Controls { /// - /// Represents the control that redistributes space between columns or rows of a Grid control. + /// Enum to indicate whether GridSplitter resizes Columns or Rows. + /// + public enum GridResizeDirection + { + /// + /// Determines whether to resize rows or columns based on its Alignment and + /// width compared to height. + /// + Auto, + + /// + /// Resize columns when dragging Splitter. + /// + Columns, + + /// + /// Resize rows when dragging Splitter. + /// + Rows + } + + /// + /// Enum to indicate what Columns or Rows the GridSplitter resizes. + /// + public enum GridResizeBehavior + { + /// + /// Determine which columns or rows to resize based on its Alignment. + /// + BasedOnAlignment, + + /// + /// Resize the current and next Columns or Rows. + /// + CurrentAndNext, + + /// + /// Resize the previous and current Columns or Rows. + /// + PreviousAndCurrent, + + /// + /// Resize the previous and next Columns or Rows. + /// + PreviousAndNext + } + + /// + /// Represents the control that redistributes space between columns or rows of a Grid control. /// - /// - /// Unlike WPF GridSplitter, Avalonia GridSplitter has only one Behavior, GridResizeBehavior.PreviousAndNext. - /// public class GridSplitter : Thumb { - private List _definitions; + /// + /// Defines the property. + /// + public static readonly AvaloniaProperty ResizeDirectionProperty = + AvaloniaProperty.Register(nameof(ResizeDirection)); - private Grid _grid; + /// + /// Defines the property. + /// + public static readonly AvaloniaProperty ResizeBehaviorProperty = + AvaloniaProperty.Register(nameof(ResizeBehavior)); - private DefinitionBase _nextDefinition; + /// + /// Defines the property. + /// + public static readonly AvaloniaProperty ShowsPreviewProperty = + AvaloniaProperty.Register(nameof(ShowsPreview)); - private Orientation _orientation; + /// + /// Defines the property. + /// + public static readonly AvaloniaProperty KeyboardIncrementProperty = + AvaloniaProperty.Register(nameof(KeyboardIncrement), 10d); - private DefinitionBase _prevDefinition; + /// + /// Defines the property. + /// + public static readonly AvaloniaProperty DragIncrementProperty = + AvaloniaProperty.Register(nameof(DragIncrement), 1d); - private void GetDeltaConstraints(out double min, out double max) + /// + /// Defines the property. + /// + public static readonly AvaloniaProperty> PreviewContentProperty = + AvaloniaProperty.Register>(nameof(PreviewContent)); + + private static readonly Cursor s_columnSplitterCursor = new Cursor(StandardCursorType.SizeWestEast); + private static readonly Cursor s_rowSplitterCursor = new Cursor(StandardCursorType.SizeNorthSouth); + + private ResizeData _resizeData; + + /// + /// Indicates whether the Splitter resizes the Columns, Rows, or Both. + /// + public GridResizeDirection ResizeDirection { - var prevDefinitionLen = GetActualLength(_prevDefinition); - var prevDefinitionMin = GetMinLength(_prevDefinition); - var prevDefinitionMax = GetMaxLength(_prevDefinition); + get => GetValue(ResizeDirectionProperty); + set => SetValue(ResizeDirectionProperty, value); + } - var nextDefinitionLen = GetActualLength(_nextDefinition); - var nextDefinitionMin = GetMinLength(_nextDefinition); - var nextDefinitionMax = GetMaxLength(_nextDefinition); - // Determine the minimum and maximum the columns can be resized - min = -Math.Min(prevDefinitionLen - prevDefinitionMin, nextDefinitionMax - nextDefinitionLen); - max = Math.Min(prevDefinitionMax - prevDefinitionLen, nextDefinitionLen - nextDefinitionMin); + /// + /// Indicates which Columns or Rows the Splitter resizes. + /// + public GridResizeBehavior ResizeBehavior + { + get => GetValue(ResizeBehaviorProperty); + set => SetValue(ResizeBehaviorProperty, value); } - protected override void OnDragDelta(VectorEventArgs e) + /// + /// Indicates whether to Preview the column resizing without updating layout. + /// + public bool ShowsPreview + { + get => GetValue(ShowsPreviewProperty); + set => SetValue(ShowsPreviewProperty, value); + } + + /// + /// The Distance to move the splitter when pressing the keyboard arrow keys. + /// + public double KeyboardIncrement + { + get => GetValue(KeyboardIncrementProperty); + set => SetValue(KeyboardIncrementProperty, value); + } + + /// + /// Restricts splitter to move a multiple of the specified units. + /// + public double DragIncrement + { + get => GetValue(DragIncrementProperty); + set => SetValue(DragIncrementProperty, value); + } + + /// + /// Gets or sets content that will be shown when is enabled and user starts resize operation. + /// + public ITemplate PreviewContent { - // WPF doesn't change anything when spliter is in the last row/column - // but resizes the splitter row/column when it's the first one. - // this is different, but more internally consistent. - if (_prevDefinition == null || _nextDefinition == null) - return; + get => GetValue(PreviewContentProperty); + set => SetValue(PreviewContentProperty, value); + } - var delta = _orientation == Orientation.Vertical ? e.Vector.X : e.Vector.Y; - double max; - double min; - GetDeltaConstraints(out min, out max); - delta = Math.Min(Math.Max(delta, min), max); + /// + /// Converts BasedOnAlignment direction to Rows, Columns, or Both depending on its width/height. + /// + internal GridResizeDirection GetEffectiveResizeDirection() + { + GridResizeDirection direction = ResizeDirection; - var prevIsStar = IsStar(_prevDefinition); - var nextIsStar = IsStar(_nextDefinition); + if (direction != GridResizeDirection.Auto) + { + return direction; + } - if (prevIsStar && nextIsStar) + // When HorizontalAlignment is Left, Right or Center, resize Columns. + if (HorizontalAlignment != HorizontalAlignment.Stretch) + { + direction = GridResizeDirection.Columns; + } + else if (VerticalAlignment != VerticalAlignment.Stretch) { - foreach (var definition in _definitions) + direction = GridResizeDirection.Rows; + } + else if (Bounds.Width <= Bounds.Height) // Fall back to Width vs Height. + { + direction = GridResizeDirection.Columns; + } + else + { + direction = GridResizeDirection.Rows; + } + + return direction; + } + + /// + /// Convert BasedOnAlignment to Next/Prev/Both depending on alignment and Direction. + /// + private GridResizeBehavior GetEffectiveResizeBehavior(GridResizeDirection direction) + { + GridResizeBehavior resizeBehavior = ResizeBehavior; + + if (resizeBehavior == GridResizeBehavior.BasedOnAlignment) + { + if (direction == GridResizeDirection.Columns) { - if (definition == _prevDefinition) + switch (HorizontalAlignment) { - SetLengthInStars(_prevDefinition, GetActualLength(_prevDefinition) + delta); + case HorizontalAlignment.Left: + resizeBehavior = GridResizeBehavior.PreviousAndCurrent; + break; + case HorizontalAlignment.Right: + resizeBehavior = GridResizeBehavior.CurrentAndNext; + break; + default: + resizeBehavior = GridResizeBehavior.PreviousAndNext; + break; } - else if (definition == _nextDefinition) + } + else + { + switch (VerticalAlignment) { - SetLengthInStars(_nextDefinition, GetActualLength(_nextDefinition) - delta); + case VerticalAlignment.Top: + resizeBehavior = GridResizeBehavior.PreviousAndCurrent; + break; + case VerticalAlignment.Bottom: + resizeBehavior = GridResizeBehavior.CurrentAndNext; + break; + default: + resizeBehavior = GridResizeBehavior.PreviousAndNext; + break; } - else if (IsStar(definition)) + } + } + + return resizeBehavior; + } + + /// + /// Removes preview adorner from the grid. + /// + private void RemovePreviewAdorner() + { + if (_resizeData.Adorner != null) + { + AdornerLayer layer = AdornerLayer.GetAdornerLayer(this); + layer.Children.Remove(_resizeData.Adorner); + } + } + + /// + /// Initialize the data needed for resizing. + /// + private void InitializeData(bool showsPreview) + { + // If not in a grid or can't resize, do nothing. + if (Parent is Grid grid) + { + GridResizeDirection resizeDirection = GetEffectiveResizeDirection(); + + // Setup data used for resizing. + _resizeData = new ResizeData + { + Grid = grid, + ShowsPreview = showsPreview, + ResizeDirection = resizeDirection, + SplitterLength = Math.Min(Bounds.Width, Bounds.Height), + ResizeBehavior = GetEffectiveResizeBehavior(resizeDirection) + }; + + // Store the rows and columns to resize on drag events. + if (!SetupDefinitionsToResize()) + { + // Unable to resize, clear data. + _resizeData = null; + return; + } + + // Setup the preview in the adorner if ShowsPreview is true. + SetupPreviewAdorner(); + } + } + + /// + /// Returns true if GridSplitter can resize rows/columns. + /// + private bool SetupDefinitionsToResize() + { + int gridSpan = GetValue(_resizeData.ResizeDirection == GridResizeDirection.Columns ? + Grid.ColumnSpanProperty : + Grid.RowSpanProperty); + + if (gridSpan == 1) + { + var splitterIndex = GetValue(_resizeData.ResizeDirection == GridResizeDirection.Columns ? + Grid.ColumnProperty : + Grid.RowProperty); + + // Select the columns based on behavior. + int index1, index2; + + switch (_resizeData.ResizeBehavior) + { + case GridResizeBehavior.PreviousAndCurrent: + // Get current and previous. + index1 = splitterIndex - 1; + index2 = splitterIndex; + break; + case GridResizeBehavior.CurrentAndNext: + // Get current and next. + index1 = splitterIndex; + index2 = splitterIndex + 1; + break; + default: // GridResizeBehavior.PreviousAndNext. + // Get previous and next. + index1 = splitterIndex - 1; + index2 = splitterIndex + 1; + break; + } + + // Get count of rows/columns in the resize direction. + int count = _resizeData.ResizeDirection == GridResizeDirection.Columns ? + _resizeData.Grid.ColumnDefinitions.Count : + _resizeData.Grid.RowDefinitions.Count; + + if (index1 >= 0 && index2 < count) + { + _resizeData.SplitterIndex = splitterIndex; + + _resizeData.Definition1Index = index1; + _resizeData.Definition1 = GetGridDefinition(_resizeData.Grid, index1, _resizeData.ResizeDirection); + _resizeData.OriginalDefinition1Length = + _resizeData.Definition1.UserSizeValueCache; // Save Size if user cancels. + _resizeData.OriginalDefinition1ActualLength = GetActualLength(_resizeData.Definition1); + + _resizeData.Definition2Index = index2; + _resizeData.Definition2 = GetGridDefinition(_resizeData.Grid, index2, _resizeData.ResizeDirection); + _resizeData.OriginalDefinition2Length = + _resizeData.Definition2.UserSizeValueCache; // Save Size if user cancels. + _resizeData.OriginalDefinition2ActualLength = GetActualLength(_resizeData.Definition2); + + // Determine how to resize the columns. + bool isStar1 = IsStar(_resizeData.Definition1); + bool isStar2 = IsStar(_resizeData.Definition2); + + if (isStar1 && isStar2) { - SetLengthInStars(definition, GetActualLength(definition)); // same size but in stars. + // If they are both stars, resize both. + _resizeData.SplitBehavior = SplitBehavior.Split; } + else + { + // One column is fixed width, resize the first one that is fixed. + _resizeData.SplitBehavior = !isStar1 ? SplitBehavior.Resize1 : SplitBehavior.Resize2; + } + + return true; } } - else if (prevIsStar) + + return false; + } + + /// + /// Create the preview adorner and add it to the adorner layer. + /// + private void SetupPreviewAdorner() + { + if (_resizeData.ShowsPreview) { - SetLength(_nextDefinition, GetActualLength(_nextDefinition) - delta); + // Get the adorner layer and add an adorner to it. + var adornerLayer = AdornerLayer.GetAdornerLayer(_resizeData.Grid); + + var previewContent = PreviewContent; + + // Can't display preview. + if (adornerLayer == null) + { + return; + } + + IControl builtPreviewContent = previewContent?.Build(); + + _resizeData.Adorner = new PreviewAdorner(builtPreviewContent); + + AdornerLayer.SetAdornedElement(_resizeData.Adorner, this); + + adornerLayer.Children.Add(_resizeData.Adorner); + + // Get constraints on preview's translation. + GetDeltaConstraints(out _resizeData.MinChange, out _resizeData.MaxChange); } - else if (nextIsStar) + } + + protected override void OnPointerEnter(PointerEventArgs e) + { + base.OnPointerEnter(e); + + GridResizeDirection direction = GetEffectiveResizeDirection(); + + switch (direction) { - SetLength(_prevDefinition, GetActualLength(_prevDefinition) + delta); + case GridResizeDirection.Columns: + Cursor = s_columnSplitterCursor; + break; + case GridResizeDirection.Rows: + Cursor = s_rowSplitterCursor; + break; } - else + } + + protected override void OnLostFocus(RoutedEventArgs e) + { + base.OnLostFocus(e); + + if (_resizeData != null) { - SetLength(_prevDefinition, GetActualLength(_prevDefinition) + delta); - SetLength(_nextDefinition, GetActualLength(_nextDefinition) - delta); + CancelResize(); } } - private double GetActualLength(DefinitionBase definition) + protected override void OnDragStarted(VectorEventArgs e) { - if (definition == null) - return 0; - var columnDefinition = definition as ColumnDefinition; - return columnDefinition?.ActualWidth ?? ((RowDefinition)definition).ActualHeight; + base.OnDragStarted(e); + + Debug.Assert(_resizeData == null, "_resizeData is not null, DragCompleted was not called"); + + InitializeData(ShowsPreview); } - private double GetMinLength(DefinitionBase definition) + protected override void OnDragDelta(VectorEventArgs e) { - if (definition == null) - return 0; - var columnDefinition = definition as ColumnDefinition; - return columnDefinition?.MinWidth ?? ((RowDefinition)definition).MinHeight; + base.OnDragDelta(e); + + if (_resizeData != null) + { + double horizontalChange = e.Vector.X; + double verticalChange = e.Vector.Y; + + // Round change to nearest multiple of DragIncrement. + double dragIncrement = DragIncrement; + horizontalChange = Math.Round(horizontalChange / dragIncrement) * dragIncrement; + verticalChange = Math.Round(verticalChange / dragIncrement) * dragIncrement; + + if (_resizeData.ShowsPreview) + { + // Set the Translation of the Adorner to the distance from the thumb. + if (_resizeData.ResizeDirection == GridResizeDirection.Columns) + { + _resizeData.Adorner.OffsetX = Math.Min( + Math.Max(horizontalChange, _resizeData.MinChange), + _resizeData.MaxChange); + } + else + { + _resizeData.Adorner.OffsetY = Math.Min( + Math.Max(verticalChange, _resizeData.MinChange), + _resizeData.MaxChange); + } + } + else + { + // Directly update the grid. + MoveSplitter(horizontalChange, verticalChange); + } + } } - private double GetMaxLength(DefinitionBase definition) + protected override void OnDragCompleted(VectorEventArgs e) { - if (definition == null) - return 0; - var columnDefinition = definition as ColumnDefinition; - return columnDefinition?.MaxWidth ?? ((RowDefinition)definition).MaxHeight; + base.OnDragCompleted(e); + + if (_resizeData != null) + { + if (_resizeData.ShowsPreview) + { + // Update the grid. + MoveSplitter(_resizeData.Adorner.OffsetX, _resizeData.Adorner.OffsetY); + RemovePreviewAdorner(); + } + + _resizeData = null; + } } - private bool IsStar(DefinitionBase definition) + protected override void OnKeyDown(KeyEventArgs e) { - var columnDefinition = definition as ColumnDefinition; - return columnDefinition?.Width.IsStar ?? ((RowDefinition)definition).Height.IsStar; + Key key = e.Key; + + switch (key) + { + case Key.Escape: + if (_resizeData != null) + { + CancelResize(); + e.Handled = true; + } + + break; + + case Key.Left: + e.Handled = KeyboardMoveSplitter(-KeyboardIncrement, 0); + break; + case Key.Right: + e.Handled = KeyboardMoveSplitter(KeyboardIncrement, 0); + break; + case Key.Up: + e.Handled = KeyboardMoveSplitter(0, -KeyboardIncrement); + break; + case Key.Down: + e.Handled = KeyboardMoveSplitter(0, KeyboardIncrement); + break; + } } - private void SetLengthInStars(DefinitionBase definition, double value) + /// + /// Cancels the resize operation. + /// + private void CancelResize() { - var columnDefinition = definition as ColumnDefinition; - if (columnDefinition != null) + // Restore original column/row lengths. + if (_resizeData.ShowsPreview) { - columnDefinition.Width = new GridLength(value, GridUnitType.Star); + RemovePreviewAdorner(); } - else + else // Reset the columns/rows lengths to the saved values. { - ((RowDefinition)definition).Height = new GridLength(value, GridUnitType.Star); + SetDefinitionLength(_resizeData.Definition1, _resizeData.OriginalDefinition1Length); + SetDefinitionLength(_resizeData.Definition2, _resizeData.OriginalDefinition2Length); } + + _resizeData = null; } - private void SetLength(DefinitionBase definition, double value) + /// + /// Returns true if the row/column has a star length. + /// + private static bool IsStar(DefinitionBase definition) { - var columnDefinition = definition as ColumnDefinition; - if (columnDefinition != null) + return definition.UserSizeValueCache.IsStar; + } + + /// + /// Gets Column or Row definition at index from grid based on resize direction. + /// + private static DefinitionBase GetGridDefinition(Grid grid, int index, GridResizeDirection direction) + { + return direction == GridResizeDirection.Columns ? + (DefinitionBase)grid.ColumnDefinitions[index] : + (DefinitionBase)grid.RowDefinitions[index]; + } + + /// + /// Retrieves the ActualWidth or ActualHeight of the definition depending on its type Column or Row. + /// + private double GetActualLength(DefinitionBase definition) + { + var column = definition as ColumnDefinition; + + return column?.ActualWidth ?? ((RowDefinition)definition).ActualHeight; + } + + /// + /// Gets Column or Row definition at index from grid based on resize direction. + /// + private static void SetDefinitionLength(DefinitionBase definition, GridLength length) + { + definition.SetValue( + definition is ColumnDefinition ? ColumnDefinition.WidthProperty : RowDefinition.HeightProperty, length); + } + + /// + /// Get the minimum and maximum Delta can be given definition constraints (MinWidth/MaxWidth). + /// + private void GetDeltaConstraints(out double minDelta, out double maxDelta) + { + double definition1Len = GetActualLength(_resizeData.Definition1); + double definition1Min = _resizeData.Definition1.UserMinSizeValueCache; + double definition1Max = _resizeData.Definition1.UserMaxSizeValueCache; + + double definition2Len = GetActualLength(_resizeData.Definition2); + double definition2Min = _resizeData.Definition2.UserMinSizeValueCache; + double definition2Max = _resizeData.Definition2.UserMaxSizeValueCache; + + // Set MinWidths to be greater than width of splitter. + if (_resizeData.SplitterIndex == _resizeData.Definition1Index) + { + definition1Min = Math.Max(definition1Min, _resizeData.SplitterLength); + } + else if (_resizeData.SplitterIndex == _resizeData.Definition2Index) + { + definition2Min = Math.Max(definition2Min, _resizeData.SplitterLength); + } + + if (_resizeData.SplitBehavior == SplitBehavior.Split) { - columnDefinition.Width = new GridLength(value); + // Determine the minimum and maximum the columns can be resized. + minDelta = -Math.Min(definition1Len - definition1Min, definition2Max - definition2Len); + maxDelta = Math.Min(definition1Max - definition1Len, definition2Len - definition2Min); + } + else if (_resizeData.SplitBehavior == SplitBehavior.Resize1) + { + minDelta = definition1Min - definition1Len; + maxDelta = definition1Max - definition1Len; } else { - ((RowDefinition)definition).Height = new GridLength(value); + minDelta = definition2Len - definition2Max; + maxDelta = definition2Len - definition2Min; } } - protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + /// + /// Sets the length of definition1 and definition2. + /// + private void SetLengths(double definition1Pixels, double definition2Pixels) { - base.OnAttachedToVisualTree(e); - _grid = this.GetVisualParent(); + // For the case where both definition1 and 2 are stars, update all star values to match their current pixel values. + if (_resizeData.SplitBehavior == SplitBehavior.Split) + { + var definitions = _resizeData.ResizeDirection == GridResizeDirection.Columns ? + (IAvaloniaReadOnlyList)_resizeData.Grid.ColumnDefinitions : + (IAvaloniaReadOnlyList)_resizeData.Grid.RowDefinitions; - _orientation = DetectOrientation(); + var definitionsCount = definitions.Count; - int definitionIndex; //row or col - if (_orientation == Orientation.Vertical) + for (var i = 0; i < definitionsCount; i++) + { + DefinitionBase definition = definitions[i]; + + // For each definition, if it is a star, set is value to ActualLength in stars + // This makes 1 star == 1 pixel in length + if (i == _resizeData.Definition1Index) + { + SetDefinitionLength(definition, new GridLength(definition1Pixels, GridUnitType.Star)); + } + else if (i == _resizeData.Definition2Index) + { + SetDefinitionLength(definition, new GridLength(definition2Pixels, GridUnitType.Star)); + } + else if (IsStar(definition)) + { + SetDefinitionLength(definition, new GridLength(GetActualLength(definition), GridUnitType.Star)); + } + } + } + else if (_resizeData.SplitBehavior == SplitBehavior.Resize1) { - Cursor = new Cursor(StandardCursorType.SizeWestEast); - _definitions = _grid.ColumnDefinitions.Cast().ToList(); - definitionIndex = GetValue(Grid.ColumnProperty); - PseudoClasses.Add(":vertical"); + SetDefinitionLength(_resizeData.Definition1, new GridLength(definition1Pixels)); } else { - Cursor = new Cursor(StandardCursorType.SizeNorthSouth); - definitionIndex = GetValue(Grid.RowProperty); - _definitions = _grid.RowDefinitions.Cast().ToList(); - PseudoClasses.Add(":horizontal"); + SetDefinitionLength(_resizeData.Definition2, new GridLength(definition2Pixels)); } + } - if (definitionIndex > 0) - _prevDefinition = _definitions[definitionIndex - 1]; + /// + /// Move the splitter by the given Delta's in the horizontal and vertical directions. + /// + private void MoveSplitter(double horizontalChange, double verticalChange) + { + Debug.Assert(_resizeData != null, "_resizeData should not be null when calling MoveSplitter"); + + // Calculate the offset to adjust the splitter. + var delta = _resizeData.ResizeDirection == GridResizeDirection.Columns ? horizontalChange : verticalChange; + + DefinitionBase definition1 = _resizeData.Definition1; + DefinitionBase definition2 = _resizeData.Definition2; + + if (definition1 != null && definition2 != null) + { + double actualLength1 = GetActualLength(definition1); + double actualLength2 = GetActualLength(definition2); + + // When splitting, Check to see if the total pixels spanned by the definitions + // is the same asbefore starting resize. If not cancel the drag + if (_resizeData.SplitBehavior == SplitBehavior.Split && + !MathUtilities.AreClose( + actualLength1 + actualLength2, + _resizeData.OriginalDefinition1ActualLength + _resizeData.OriginalDefinition2ActualLength)) + { + CancelResize(); - if (definitionIndex < _definitions.Count - 1) - _nextDefinition = _definitions[definitionIndex + 1]; + return; + } + + GetDeltaConstraints(out var min, out var max); + + // Constrain Delta to Min/MaxWidth of columns + delta = Math.Min(Math.Max(delta, min), max); + + double definition1LengthNew = actualLength1 + delta; + double definition2LengthNew = actualLength1 + actualLength2 - definition1LengthNew; + + SetLengths(definition1LengthNew, definition2LengthNew); + } } - private Orientation DetectOrientation() + /// + /// Move the splitter using the Keyboard (Don't show preview). + /// + private bool KeyboardMoveSplitter(double horizontalChange, double verticalChange) { - if (!_grid.ColumnDefinitions.Any()) - return Orientation.Horizontal; - if (!_grid.RowDefinitions.Any()) - return Orientation.Vertical; + // If moving with the mouse, ignore keyboard motion. + if (_resizeData != null) + { + return false; // Don't handle the event. + } - var col = GetValue(Grid.ColumnProperty); - var row = GetValue(Grid.RowProperty); - var width = _grid.ColumnDefinitions[col].Width; - var height = _grid.RowDefinitions[row].Height; - if (width.IsAuto && !height.IsAuto) + // Don't show preview. + InitializeData(false); + + // Check that we are actually able to resize. + if (_resizeData == null) { - return Orientation.Vertical; + return false; // Don't handle the event. } - if (!width.IsAuto && height.IsAuto) + + MoveSplitter(horizontalChange, verticalChange); + + _resizeData = null; + + return true; + } + + /// + /// This adorner draws the preview for the . + /// It also positions the adorner. + /// + private sealed class PreviewAdorner : Decorator + { + private readonly TranslateTransform _translation; + private readonly Decorator _decorator; + + public PreviewAdorner(IControl previewControl) { - return Orientation.Horizontal; + // Add a decorator to perform translations. + _translation = new TranslateTransform(); + + _decorator = new Decorator + { + Child = previewControl, + RenderTransform = _translation + }; + + Child = _decorator; + } + + /// + /// The Preview's Offset in the X direction from the GridSplitter. + /// + public double OffsetX + { + get => _translation.X; + set => _translation.X = value; + } + + /// + /// The Preview's Offset in the Y direction from the GridSplitter. + /// + public double OffsetY + { + get => _translation.Y; + set => _translation.Y = value; } - if (_grid.Children.OfType() // Decision based on other controls in the same column - .Where(c => Grid.GetColumn(c) == col) - .Any(c => c.GetType() != typeof(GridSplitter))) + + protected override Size ArrangeOverride(Size finalSize) { - return Orientation.Horizontal; + // Adorners always get clipped to the owner control. In this case we want + // to constrain size to the splitter size but draw on top of the parent grid. + Clip = null; + + return base.ArrangeOverride(finalSize); } - return Orientation.Vertical; + } + + /// + /// has special Behavior when columns are fixed. + /// If the left column is fixed, splitter will only resize that column. + /// Else if the right column is fixed, splitter will only resize the right column. + /// + private enum SplitBehavior + { + /// + /// Both columns/rows are star lengths. + /// + Split, + + /// + /// Resize 1 only. + /// + Resize1, + + /// + /// Resize 2 only. + /// + Resize2 + } + + /// + /// Stores data during the resizing operation. + /// + private class ResizeData + { + public bool ShowsPreview; + public PreviewAdorner Adorner; + + // The constraints to keep the Preview within valid ranges. + public double MinChange; + public double MaxChange; + + // The grid to Resize. + public Grid Grid; + + // Cache of Resize Direction and Behavior. + public GridResizeDirection ResizeDirection; + public GridResizeBehavior ResizeBehavior; + + // The columns/rows to resize. + public DefinitionBase Definition1; + public DefinitionBase Definition2; + + // Are the columns/rows star lengths. + public SplitBehavior SplitBehavior; + + // The index of the splitter. + public int SplitterIndex; + + // The indices of the columns/rows. + public int Definition1Index; + public int Definition2Index; + + // The original lengths of Definition1 and Definition2 (to restore lengths if user cancels resize). + public GridLength OriginalDefinition1Length; + public GridLength OriginalDefinition2Length; + public double OriginalDefinition1ActualLength; + public double OriginalDefinition2ActualLength; + + // The minimum of Width/Height of Splitter. Used to ensure splitter + // isn't hidden by resizing a row/column smaller than the splitter. + public double SplitterLength; } } } diff --git a/src/Avalonia.Themes.Default/GridSplitter.xaml b/src/Avalonia.Themes.Default/GridSplitter.xaml index 64349222ea..cfab5dab56 100644 --- a/src/Avalonia.Themes.Default/GridSplitter.xaml +++ b/src/Avalonia.Themes.Default/GridSplitter.xaml @@ -1,51 +1,21 @@ - - - - - - - - + + - - - - - - - + - + diff --git a/tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs b/tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs index a790d2fca1..15d62e9140 100644 --- a/tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs +++ b/tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs @@ -2,9 +2,7 @@ using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Platform; using Avalonia.UnitTests; - using Moq; - using Xunit; namespace Avalonia.Controls.UnitTests @@ -21,183 +19,246 @@ namespace Avalonia.Controls.UnitTests public void Detects_Horizontal_Orientation() { GridSplitter splitter; - var grid = new Grid() - { - RowDefinitions = new RowDefinitions("*,Auto,*"), - ColumnDefinitions = new ColumnDefinitions("*,*"), - Children = - { - new Border { [Grid.RowProperty] = 0 }, - (splitter = new GridSplitter { [Grid.RowProperty] = 1 }), - new Border { [Grid.RowProperty] = 2 } - } - }; + + var grid = new Grid + { + RowDefinitions = new RowDefinitions("*,Auto,*"), + ColumnDefinitions = new ColumnDefinitions("*,*"), + Children = + { + new Border { [Grid.RowProperty] = 0 }, + (splitter = new GridSplitter { [Grid.RowProperty] = 1 }), + new Border { [Grid.RowProperty] = 2 } + } + }; var root = new TestRoot { Child = grid }; root.Measure(new Size(100, 300)); root.Arrange(new Rect(0, 0, 100, 300)); - Assert.Contains(splitter.Classes, ":horizontal".Equals); + Assert.Equal(GridResizeDirection.Rows, splitter.GetEffectiveResizeDirection()); } [Fact] public void Detects_Vertical_Orientation() { GridSplitter splitter; - var grid = new Grid() - { - ColumnDefinitions = new ColumnDefinitions("*,Auto,*"), - RowDefinitions = new RowDefinitions("*,*"), - Children = - { - new Border { [Grid.ColumnProperty] = 0 }, - (splitter = new GridSplitter { [Grid.ColumnProperty] = 1}), - new Border { [Grid.ColumnProperty] = 2 }, - } - }; + + var grid = new Grid + { + ColumnDefinitions = new ColumnDefinitions("*,Auto,*"), + RowDefinitions = new RowDefinitions("*,*"), + Children = + { + new Border { [Grid.ColumnProperty] = 0 }, + (splitter = new GridSplitter { [Grid.ColumnProperty] = 1 }), + new Border { [Grid.ColumnProperty] = 2 }, + } + }; var root = new TestRoot { Child = grid }; root.Measure(new Size(100, 300)); root.Arrange(new Rect(0, 0, 100, 300)); - Assert.Contains(splitter.Classes, ":vertical".Equals); + Assert.Equal(GridResizeDirection.Columns, splitter.GetEffectiveResizeDirection()); } [Fact] public void Detects_With_Both_Auto() { GridSplitter splitter; - var grid = new Grid() - { - ColumnDefinitions = new ColumnDefinitions("Auto,Auto,Auto"), - RowDefinitions = new RowDefinitions("Auto,Auto"), - Children = - { - new Border { [Grid.ColumnProperty] = 0 }, - (splitter = new GridSplitter { [Grid.ColumnProperty] = 1}), - new Border { [Grid.ColumnProperty] = 2 }, - } - }; + + var grid = new Grid + { + ColumnDefinitions = new ColumnDefinitions("Auto,Auto,Auto"), + RowDefinitions = new RowDefinitions("Auto,Auto"), + Children = + { + new Border { [Grid.ColumnProperty] = 0 }, + (splitter = new GridSplitter { [Grid.ColumnProperty] = 1 }), + new Border { [Grid.ColumnProperty] = 2 }, + } + }; var root = new TestRoot { Child = grid }; root.Measure(new Size(100, 300)); root.Arrange(new Rect(0, 0, 100, 300)); - Assert.Contains(splitter.Classes, ":vertical".Equals); + Assert.Equal(GridResizeDirection.Columns, splitter.GetEffectiveResizeDirection()); } [Fact] - public void Horizontal_Stays_Within_Constraints() + public void In_First_Position_Doesnt_Throw_Exception() + { + GridSplitter splitter; + var grid = new Grid + { + ColumnDefinitions = new ColumnDefinitions("Auto,*,*"), + RowDefinitions = new RowDefinitions("*,*"), + Children = + { + (splitter = new GridSplitter { [Grid.ColumnProperty] = 0 }), + new Border { [Grid.ColumnProperty] = 1 }, + new Border { [Grid.ColumnProperty] = 2 }, + } + }; + + var root = new TestRoot { Child = grid }; + root.Measure(new Size(100, 300)); + root.Arrange(new Rect(0, 0, 100, 300)); + + splitter.RaiseEvent( + new VectorEventArgs { RoutedEvent = Thumb.DragStartedEvent }); + + splitter.RaiseEvent(new VectorEventArgs + { + RoutedEvent = Thumb.DragDeltaEvent, Vector = new Vector(100, 1000) + }); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void Horizontal_Stays_Within_Constraints(bool showsPreview) { var control1 = new Border { [Grid.RowProperty] = 0 }; - var splitter = new GridSplitter - { - [Grid.RowProperty] = 1, - }; + var splitter = new GridSplitter { [Grid.RowProperty] = 1, ShowsPreview = showsPreview}; var control2 = new Border { [Grid.RowProperty] = 2 }; - var rowDefinitions = new RowDefinitions() - { - new RowDefinition(1, GridUnitType.Star) { MinHeight = 70, MaxHeight = 110 }, - new RowDefinition(GridLength.Auto), - new RowDefinition(1, GridUnitType.Star) { MinHeight = 10, MaxHeight = 140 }, - }; - - var grid = new Grid() - { - RowDefinitions = rowDefinitions, - Children = - { - control1, splitter, control2 - } - }; + var rowDefinitions = new RowDefinitions + { + new RowDefinition(1, GridUnitType.Star) { MinHeight = 70, MaxHeight = 110 }, + new RowDefinition(GridLength.Auto), + new RowDefinition(1, GridUnitType.Star) { MinHeight = 10, MaxHeight = 140 }, + }; + + var grid = new Grid { RowDefinitions = rowDefinitions, Children = { control1, splitter, control2 } }; + + var root = new TestRoot + { + Child = new VisualLayerManager + { + Child = grid + } + }; - var root = new TestRoot { Child = grid }; root.Measure(new Size(100, 200)); root.Arrange(new Rect(0, 0, 100, 200)); + splitter.RaiseEvent( + new VectorEventArgs { RoutedEvent = Thumb.DragStartedEvent }); + splitter.RaiseEvent(new VectorEventArgs - { - RoutedEvent = Thumb.DragDeltaEvent, - Vector = new Vector(0, -100) - }); - Assert.Equal(rowDefinitions[0].Height, new GridLength(70, GridUnitType.Star)); - Assert.Equal(rowDefinitions[2].Height, new GridLength(130, GridUnitType.Star)); + { + RoutedEvent = Thumb.DragDeltaEvent, + Vector = new Vector(0, -100) + }); + + if (showsPreview) + { + Assert.Equal(rowDefinitions[0].Height, new GridLength(1, GridUnitType.Star)); + Assert.Equal(rowDefinitions[2].Height, new GridLength(1, GridUnitType.Star)); + } + else + { + Assert.Equal(rowDefinitions[0].Height, new GridLength(70, GridUnitType.Star)); + Assert.Equal(rowDefinitions[2].Height, new GridLength(130, GridUnitType.Star)); + } + splitter.RaiseEvent(new VectorEventArgs - { - RoutedEvent = Thumb.DragDeltaEvent, - Vector = new Vector(0, 100) - }); - Assert.Equal(rowDefinitions[0].Height, new GridLength(110, GridUnitType.Star)); - Assert.Equal(rowDefinitions[2].Height, new GridLength(90, GridUnitType.Star)); - } + { + RoutedEvent = Thumb.DragDeltaEvent, + Vector = new Vector(0, 100) + }); - [Fact] - public void In_First_Position_Doesnt_Throw_Exception() - { - GridSplitter splitter; - var grid = new Grid() - { - ColumnDefinitions = new ColumnDefinitions("Auto,*,*"), - RowDefinitions = new RowDefinitions("*,*"), - Children = - { - (splitter = new GridSplitter { [Grid.ColumnProperty] = 0} ), - new Border { [Grid.ColumnProperty] = 1 }, - new Border { [Grid.ColumnProperty] = 2 }, - } - }; + if (showsPreview) + { + Assert.Equal(rowDefinitions[0].Height, new GridLength(1, GridUnitType.Star)); + Assert.Equal(rowDefinitions[2].Height, new GridLength(1, GridUnitType.Star)); + } + else + { + Assert.Equal(rowDefinitions[0].Height, new GridLength(110, GridUnitType.Star)); + Assert.Equal(rowDefinitions[2].Height, new GridLength(90, GridUnitType.Star)); + } - var root = new TestRoot { Child = grid }; - root.Measure(new Size(100, 300)); - root.Arrange(new Rect(0, 0, 100, 300)); splitter.RaiseEvent(new VectorEventArgs - { - RoutedEvent = Thumb.DragDeltaEvent, - Vector = new Vector(100, 1000) - }); + { + RoutedEvent = Thumb.DragCompletedEvent + }); + + Assert.Equal(rowDefinitions[0].Height, new GridLength(110, GridUnitType.Star)); + Assert.Equal(rowDefinitions[2].Height, new GridLength(90, GridUnitType.Star)); } - [Fact] - public void Vertical_Stays_Within_Constraints() + [Theory] + [InlineData(false)] + [InlineData(true)] + public void Vertical_Stays_Within_Constraints(bool showsPreview) { var control1 = new Border { [Grid.ColumnProperty] = 0 }; - var splitter = new GridSplitter - { - [Grid.ColumnProperty] = 1, - }; + var splitter = new GridSplitter { [Grid.ColumnProperty] = 1, ShowsPreview = showsPreview}; var control2 = new Border { [Grid.ColumnProperty] = 2 }; - var columnDefinitions = new ColumnDefinitions() - { - new ColumnDefinition(1, GridUnitType.Star) { MinWidth = 10, MaxWidth = 190 }, - new ColumnDefinition(GridLength.Auto), - new ColumnDefinition(1, GridUnitType.Star) { MinWidth = 80, MaxWidth = 120 }, - }; - - var grid = new Grid() - { - ColumnDefinitions = columnDefinitions, - Children = - { - control1, splitter, control2 - } - }; + var columnDefinitions = new ColumnDefinitions + { + new ColumnDefinition(1, GridUnitType.Star) { MinWidth = 10, MaxWidth = 190 }, + new ColumnDefinition(GridLength.Auto), + new ColumnDefinition(1, GridUnitType.Star) { MinWidth = 80, MaxWidth = 120 }, + }; - var root = new TestRoot { Child = grid }; + var grid = new Grid { ColumnDefinitions = columnDefinitions, Children = { control1, splitter, control2 } }; + + var root = new TestRoot + { + Child = new VisualLayerManager + { + Child = grid + } + }; root.Measure(new Size(200, 100)); root.Arrange(new Rect(0, 0, 200, 100)); + splitter.RaiseEvent( + new VectorEventArgs { RoutedEvent = Thumb.DragStartedEvent }); + splitter.RaiseEvent(new VectorEventArgs - { - RoutedEvent = Thumb.DragDeltaEvent, - Vector = new Vector(-100, 0) - }); - Assert.Equal(columnDefinitions[0].Width, new GridLength(80, GridUnitType.Star)); - Assert.Equal(columnDefinitions[2].Width, new GridLength(120, GridUnitType.Star)); + { + RoutedEvent = Thumb.DragDeltaEvent, + Vector = new Vector(-100, 0) + }); + + if (showsPreview) + { + Assert.Equal(columnDefinitions[0].Width, new GridLength(1, GridUnitType.Star)); + Assert.Equal(columnDefinitions[2].Width, new GridLength(1, GridUnitType.Star)); + } + else + { + Assert.Equal(columnDefinitions[0].Width, new GridLength(80, GridUnitType.Star)); + Assert.Equal(columnDefinitions[2].Width, new GridLength(120, GridUnitType.Star)); + } + splitter.RaiseEvent(new VectorEventArgs - { - RoutedEvent = Thumb.DragDeltaEvent, - Vector = new Vector(100, 0) - }); + { + RoutedEvent = Thumb.DragDeltaEvent, + Vector = new Vector(100, 0) + }); + + if (showsPreview) + { + Assert.Equal(columnDefinitions[0].Width, new GridLength(1, GridUnitType.Star)); + Assert.Equal(columnDefinitions[2].Width, new GridLength(1, GridUnitType.Star)); + } + else + { + Assert.Equal(columnDefinitions[0].Width, new GridLength(120, GridUnitType.Star)); + Assert.Equal(columnDefinitions[2].Width, new GridLength(80, GridUnitType.Star)); + } + + splitter.RaiseEvent(new VectorEventArgs + { + RoutedEvent = Thumb.DragCompletedEvent + }); + Assert.Equal(columnDefinitions[0].Width, new GridLength(120, GridUnitType.Star)); Assert.Equal(columnDefinitions[2].Width, new GridLength(80, GridUnitType.Star)); } From bf04d22856d36ffeded87fedd211de21c36a66f3 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sun, 27 Oct 2019 19:49:20 +0100 Subject: [PATCH 03/57] Comment fixes. --- src/Avalonia.Controls/GridSplitter.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/GridSplitter.cs b/src/Avalonia.Controls/GridSplitter.cs index e5749c8ed9..3fc1ed3ea5 100644 --- a/src/Avalonia.Controls/GridSplitter.cs +++ b/src/Avalonia.Controls/GridSplitter.cs @@ -16,7 +16,7 @@ using Avalonia.Utilities; namespace Avalonia.Controls { /// - /// Enum to indicate whether GridSplitter resizes Columns or Rows. + /// Enum to indicate whether resizes Columns or Rows. /// public enum GridResizeDirection { @@ -38,7 +38,7 @@ namespace Avalonia.Controls } /// - /// Enum to indicate what Columns or Rows the GridSplitter resizes. + /// Enum to indicate what Columns or Rows the resizes. /// public enum GridResizeBehavior { From 14b3683127db6661c127a98b79b7842d2d44d894 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro?= Date: Sat, 26 Oct 2019 20:47:10 +0100 Subject: [PATCH 04/57] Replaced obsolete API usages. --- src/Avalonia.Controls/ListBox.cs | 20 ++++++++++++------- .../NumericUpDown/NumericUpDown.cs | 4 ++-- src/Avalonia.Controls/Primitives/TabStrip.cs | 10 ++++++++-- src/Avalonia.Controls/Primitives/Thumb.cs | 2 -- src/Avalonia.Controls/TextBox.cs | 8 ++++---- src/Avalonia.Controls/TreeView.cs | 19 +++++++++++------- src/Avalonia.Input/AccessKeyHandler.cs | 2 +- src/Avalonia.Input/FocusManager.cs | 2 +- 8 files changed, 41 insertions(+), 26 deletions(-) diff --git a/src/Avalonia.Controls/ListBox.cs b/src/Avalonia.Controls/ListBox.cs index 449ca18465..b15a6ce668 100644 --- a/src/Avalonia.Controls/ListBox.cs +++ b/src/Avalonia.Controls/ListBox.cs @@ -7,6 +7,7 @@ using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Input; +using Avalonia.VisualTree; namespace Avalonia.Controls { @@ -132,14 +133,19 @@ namespace Avalonia.Controls { base.OnPointerPressed(e); - if (e.MouseButton == MouseButton.Left || e.MouseButton == MouseButton.Right) + if (e.Source is IVisual source) { - e.Handled = UpdateSelectionFromEventSource( - e.Source, - true, - (e.InputModifiers & InputModifiers.Shift) != 0, - (e.InputModifiers & InputModifiers.Control) != 0, - e.MouseButton == MouseButton.Right); + var point = e.GetCurrentPoint(source); + + if (point.Properties.IsLeftButtonPressed || point.Properties.IsRightButtonPressed) + { + e.Handled = UpdateSelectionFromEventSource( + e.Source, + true, + (e.KeyModifiers & KeyModifiers.Shift) != 0, + (e.KeyModifiers & KeyModifiers.Control) != 0, + point.Properties.IsRightButtonPressed); + } } } diff --git a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs index f8ae5c9690..6d450a0155 100644 --- a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs +++ b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs @@ -804,9 +804,9 @@ namespace Avalonia.Controls private void TextBoxOnPointerPressed(object sender, PointerPressedEventArgs e) { - if (e.Device.Captured != Spinner) + if (e.Pointer.Captured != Spinner) { - Dispatcher.UIThread.InvokeAsync(() => { e.Device.Capture(Spinner); }, DispatcherPriority.Input); + Dispatcher.UIThread.InvokeAsync(() => { e.Pointer.Capture(Spinner); }, DispatcherPriority.Input); } } diff --git a/src/Avalonia.Controls/Primitives/TabStrip.cs b/src/Avalonia.Controls/Primitives/TabStrip.cs index ec0dbd124c..e1a6cf79bb 100644 --- a/src/Avalonia.Controls/Primitives/TabStrip.cs +++ b/src/Avalonia.Controls/Primitives/TabStrip.cs @@ -5,6 +5,7 @@ using Avalonia.Controls.Generators; using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Layout; +using Avalonia.VisualTree; namespace Avalonia.Controls.Primitives { @@ -44,9 +45,14 @@ namespace Avalonia.Controls.Primitives { base.OnPointerPressed(e); - if (e.MouseButton == MouseButton.Left) + if (e.Source is IVisual source) { - e.Handled = UpdateSelectionFromEventSource(e.Source); + var point = e.GetCurrentPoint(source); + + if (point.Properties.IsLeftButtonPressed) + { + e.Handled = UpdateSelectionFromEventSource(e.Source); + } } } } diff --git a/src/Avalonia.Controls/Primitives/Thumb.cs b/src/Avalonia.Controls/Primitives/Thumb.cs index 9f5ddb666c..d88018cf32 100644 --- a/src/Avalonia.Controls/Primitives/Thumb.cs +++ b/src/Avalonia.Controls/Primitives/Thumb.cs @@ -73,7 +73,6 @@ namespace Avalonia.Controls.Primitives protected override void OnPointerPressed(PointerPressedEventArgs e) { - e.Device.Capture(this); e.Handled = true; _lastPoint = e.GetPosition(this); @@ -92,7 +91,6 @@ namespace Avalonia.Controls.Primitives { if (_lastPoint.HasValue) { - e.Device.Capture(null); e.Handled = true; _lastPoint = null; diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index fdc9d153e2..3d472fca18 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -677,13 +677,13 @@ namespace Avalonia.Controls } } - e.Device.Capture(_presenter); + e.Pointer.Capture(_presenter); e.Handled = true; } protected override void OnPointerMoved(PointerEventArgs e) { - if (_presenter != null && e.Device.Captured == _presenter) + if (_presenter != null && e.Pointer.Captured == _presenter) { var point = e.GetPosition(_presenter); @@ -694,9 +694,9 @@ namespace Avalonia.Controls protected override void OnPointerReleased(PointerReleasedEventArgs e) { - if (_presenter != null && e.Device.Captured == _presenter) + if (_presenter != null && e.Pointer.Captured == _presenter) { - e.Device.Capture(null); + e.Pointer.Capture(null); } } diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index 8907137ecb..59844be8a6 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -507,14 +507,19 @@ namespace Avalonia.Controls { base.OnPointerPressed(e); - if (e.MouseButton == MouseButton.Left || e.MouseButton == MouseButton.Right) + if (e.Source is IVisual source) { - e.Handled = UpdateSelectionFromEventSource( - e.Source, - true, - (e.InputModifiers & InputModifiers.Shift) != 0, - (e.InputModifiers & InputModifiers.Control) != 0, - e.MouseButton == MouseButton.Right); + var point = e.GetCurrentPoint(source); + + if (point.Properties.IsLeftButtonPressed || point.Properties.IsRightButtonPressed) + { + e.Handled = UpdateSelectionFromEventSource( + e.Source, + true, + (e.KeyModifiers & KeyModifiers.Shift) != 0, + (e.KeyModifiers & KeyModifiers.Control) != 0, + point.Properties.IsRightButtonPressed); + } } } diff --git a/src/Avalonia.Input/AccessKeyHandler.cs b/src/Avalonia.Input/AccessKeyHandler.cs index 9e4b2b84e0..aa009770f6 100644 --- a/src/Avalonia.Input/AccessKeyHandler.cs +++ b/src/Avalonia.Input/AccessKeyHandler.cs @@ -182,7 +182,7 @@ namespace Avalonia.Input { bool menuIsOpen = MainMenu?.IsOpen == true; - if ((e.Modifiers & InputModifiers.Alt) != 0 || menuIsOpen) + if ((e.KeyModifiers & KeyModifiers.Alt) != 0 || menuIsOpen) { // If any other key is pressed with the Alt key held down, or the main menu is open, // find all controls who have registered that access key. diff --git a/src/Avalonia.Input/FocusManager.cs b/src/Avalonia.Input/FocusManager.cs index 80e18cb7bf..104ac9cb61 100644 --- a/src/Avalonia.Input/FocusManager.cs +++ b/src/Avalonia.Input/FocusManager.cs @@ -180,7 +180,7 @@ namespace Avalonia.Input if (sender == e.Source && ev.MouseButton == MouseButton.Left) { - var element = (ev.Device?.Captured as IInputElement) ?? (e.Source as IInputElement); + var element = (ev.Pointer?.Captured as IInputElement) ?? (e.Source as IInputElement); if (element == null || !CanFocus(element)) { From b315d5024f28d82f8eb51b0a46a86085d13d24d9 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sun, 27 Oct 2019 21:42:59 +0100 Subject: [PATCH 05/57] Move enums lower. Add workaround for thumb raising multiple started events. --- src/Avalonia.Controls/GridSplitter.cs | 106 ++++++++++++++------------ 1 file changed, 56 insertions(+), 50 deletions(-) diff --git a/src/Avalonia.Controls/GridSplitter.cs b/src/Avalonia.Controls/GridSplitter.cs index 3fc1ed3ea5..56a28e15c2 100644 --- a/src/Avalonia.Controls/GridSplitter.cs +++ b/src/Avalonia.Controls/GridSplitter.cs @@ -16,55 +16,7 @@ using Avalonia.Utilities; namespace Avalonia.Controls { /// - /// Enum to indicate whether resizes Columns or Rows. - /// - public enum GridResizeDirection - { - /// - /// Determines whether to resize rows or columns based on its Alignment and - /// width compared to height. - /// - Auto, - - /// - /// Resize columns when dragging Splitter. - /// - Columns, - - /// - /// Resize rows when dragging Splitter. - /// - Rows - } - - /// - /// Enum to indicate what Columns or Rows the resizes. - /// - public enum GridResizeBehavior - { - /// - /// Determine which columns or rows to resize based on its Alignment. - /// - BasedOnAlignment, - - /// - /// Resize the current and next Columns or Rows. - /// - CurrentAndNext, - - /// - /// Resize the previous and current Columns or Rows. - /// - PreviousAndCurrent, - - /// - /// Resize the previous and next Columns or Rows. - /// - PreviousAndNext - } - - /// - /// Represents the control that redistributes space between columns or rows of a Grid control. + /// Represents the control that redistributes space between columns or rows of a control. /// public class GridSplitter : Thumb { @@ -427,7 +379,13 @@ namespace Avalonia.Controls { base.OnDragStarted(e); - Debug.Assert(_resizeData == null, "_resizeData is not null, DragCompleted was not called"); + // TODO: Looks like that sometimes thumb will raise multiple drag started events. + // Debug.Assert(_resizeData == null, "_resizeData is not null, DragCompleted was not called"); + + if (_resizeData != null) + { + return; + } InitializeData(ShowsPreview); } @@ -845,4 +803,52 @@ namespace Avalonia.Controls public double SplitterLength; } } + + /// + /// Enum to indicate whether resizes Columns or Rows. + /// + public enum GridResizeDirection + { + /// + /// Determines whether to resize rows or columns based on its Alignment and + /// width compared to height. + /// + Auto, + + /// + /// Resize columns when dragging Splitter. + /// + Columns, + + /// + /// Resize rows when dragging Splitter. + /// + Rows + } + + /// + /// Enum to indicate what Columns or Rows the resizes. + /// + public enum GridResizeBehavior + { + /// + /// Determine which columns or rows to resize based on its Alignment. + /// + BasedOnAlignment, + + /// + /// Resize the current and next Columns or Rows. + /// + CurrentAndNext, + + /// + /// Resize the previous and current Columns or Rows. + /// + PreviousAndCurrent, + + /// + /// Resize the previous and next Columns or Rows. + /// + PreviousAndNext + } } From 1d7feade1b6b8dffbb83487a0f0c588988200467 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sun, 27 Oct 2019 23:32:20 +0100 Subject: [PATCH 06/57] Add keyboard input tests. --- .../GridSplitterTests.cs | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs b/tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs index 15d62e9140..f2b6b0db4b 100644 --- a/tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs +++ b/tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs @@ -262,5 +262,123 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(columnDefinitions[0].Width, new GridLength(120, GridUnitType.Star)); Assert.Equal(columnDefinitions[2].Width, new GridLength(80, GridUnitType.Star)); } + + [Theory] + [InlineData(Key.Up, 90, 110)] + [InlineData(Key.Down, 110, 90)] + public void Vertical_Keyboard_Input_Can_Move_Splitter(Key key, double expectedHeightFirst, double expectedHeightSecond) + { + var control1 = new Border { [Grid.RowProperty] = 0 }; + var splitter = new GridSplitter { [Grid.RowProperty] = 1, KeyboardIncrement = 10d }; + var control2 = new Border { [Grid.RowProperty] = 2 }; + + var rowDefinitions = new RowDefinitions + { + new RowDefinition(1, GridUnitType.Star), + new RowDefinition(GridLength.Auto), + new RowDefinition(1, GridUnitType.Star) + }; + + var grid = new Grid { RowDefinitions = rowDefinitions, Children = { control1, splitter, control2 } }; + + var root = new TestRoot + { + Child = grid + }; + + root.Measure(new Size(200, 200)); + root.Arrange(new Rect(0, 0, 200, 200)); + + splitter.RaiseEvent(new KeyEventArgs + { + RoutedEvent = InputElement.KeyDownEvent, + Key = key + }); + + Assert.Equal(rowDefinitions[0].Height, new GridLength(expectedHeightFirst, GridUnitType.Star)); + Assert.Equal(rowDefinitions[2].Height, new GridLength(expectedHeightSecond, GridUnitType.Star)); + } + + [Theory] + [InlineData(Key.Left, 90, 110)] + [InlineData(Key.Right, 110, 90)] + public void Horizontal_Keyboard_Input_Can_Move_Splitter(Key key, double expectedWidthFirst, double expectedWidthSecond) + { + var control1 = new Border { [Grid.ColumnProperty] = 0 }; + var splitter = new GridSplitter { [Grid.ColumnProperty] = 1, KeyboardIncrement = 10d }; + var control2 = new Border { [Grid.ColumnProperty] = 2 }; + + var columnDefinitions = new ColumnDefinitions + { + new ColumnDefinition(1, GridUnitType.Star), + new ColumnDefinition(GridLength.Auto), + new ColumnDefinition(1, GridUnitType.Star) + }; + + var grid = new Grid { ColumnDefinitions = columnDefinitions, Children = { control1, splitter, control2 } }; + + var root = new TestRoot + { + Child = grid + }; + + root.Measure(new Size(200, 200)); + root.Arrange(new Rect(0, 0, 200, 200)); + + splitter.RaiseEvent(new KeyEventArgs + { + RoutedEvent = InputElement.KeyDownEvent, + Key = key + }); + + Assert.Equal(columnDefinitions[0].Width, new GridLength(expectedWidthFirst, GridUnitType.Star)); + Assert.Equal(columnDefinitions[2].Width, new GridLength(expectedWidthSecond, GridUnitType.Star)); + } + + [Fact] + public void Pressing_Escape_Key_Cancels_Resizing() + { + var control1 = new Border { [Grid.ColumnProperty] = 0 }; + var splitter = new GridSplitter { [Grid.ColumnProperty] = 1, KeyboardIncrement = 10d }; + var control2 = new Border { [Grid.ColumnProperty] = 2 }; + + var columnDefinitions = new ColumnDefinitions + { + new ColumnDefinition(1, GridUnitType.Star), + new ColumnDefinition(GridLength.Auto), + new ColumnDefinition(1, GridUnitType.Star) + }; + + var grid = new Grid { ColumnDefinitions = columnDefinitions, Children = { control1, splitter, control2 } }; + + var root = new TestRoot + { + Child = grid + }; + + root.Measure(new Size(200, 200)); + root.Arrange(new Rect(0, 0, 200, 200)); + + splitter.RaiseEvent( + new VectorEventArgs { RoutedEvent = Thumb.DragStartedEvent }); + + splitter.RaiseEvent(new VectorEventArgs + { + RoutedEvent = Thumb.DragDeltaEvent, + Vector = new Vector(-100, 0) + }); + + Assert.Equal(columnDefinitions[0].Width, new GridLength(0, GridUnitType.Star)); + Assert.Equal(columnDefinitions[2].Width, new GridLength(200, GridUnitType.Star)); + + splitter.RaiseEvent(new KeyEventArgs + { + RoutedEvent = InputElement.KeyDownEvent, + Key = Key.Escape + }); + + Assert.Equal(columnDefinitions[0].Width, new GridLength(1, GridUnitType.Star)); + Assert.Equal(columnDefinitions[2].Width, new GridLength(1, GridUnitType.Star)); + } } } From 6dd6f336da5509d517c3e0c1935ea322ca2e7f0e Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sun, 27 Oct 2019 23:59:17 +0100 Subject: [PATCH 07/57] Restore original delta constraints algorithm. --- src/Avalonia.Controls/GridSplitter.cs | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/src/Avalonia.Controls/GridSplitter.cs b/src/Avalonia.Controls/GridSplitter.cs index 56a28e15c2..a2fefa0548 100644 --- a/src/Avalonia.Controls/GridSplitter.cs +++ b/src/Avalonia.Controls/GridSplitter.cs @@ -554,22 +554,9 @@ namespace Avalonia.Controls definition2Min = Math.Max(definition2Min, _resizeData.SplitterLength); } - if (_resizeData.SplitBehavior == SplitBehavior.Split) - { - // Determine the minimum and maximum the columns can be resized. - minDelta = -Math.Min(definition1Len - definition1Min, definition2Max - definition2Len); - maxDelta = Math.Min(definition1Max - definition1Len, definition2Len - definition2Min); - } - else if (_resizeData.SplitBehavior == SplitBehavior.Resize1) - { - minDelta = definition1Min - definition1Len; - maxDelta = definition1Max - definition1Len; - } - else - { - minDelta = definition2Len - definition2Max; - maxDelta = definition2Len - definition2Min; - } + // Determine the minimum and maximum the columns can be resized. + minDelta = -Math.Min(definition1Len - definition1Min, definition2Max - definition2Len); + maxDelta = Math.Min(definition1Max - definition1Len, definition2Len - definition2Min); } /// From 4d14218a653d2cdd0655d38d47b4af84092fc4f1 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Wed, 30 Oct 2019 06:18:22 +0100 Subject: [PATCH 08/57] Failing unit tests #3180 --- .../TabControlTests.cs | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs index ddf7e7a0fa..1e2c2084bd 100644 --- a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs @@ -4,6 +4,7 @@ using System; using System.Collections.ObjectModel; using System.Linq; +using Avalonia.Collections; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; @@ -44,6 +45,31 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(selected, target.SelectedItem); } + [Fact] + public void Pre_Selecting_TabItem_Should_Set_SelectedContent_After_It_Was_Added() + { + var target = new TabControl + { + Template = TabControlTemplate(), + }; + + const string secondContent = "Second"; + + var items = new AvaloniaList + { + new TabItem { Header = "First"}, + new TabItem { Header = "Second", Content = secondContent, IsSelected = true } + }; + + target.Items = items; + + target.ApplyTemplate(); + + target.Measure(Size.Infinity); + + Assert.Equal(secondContent, target.SelectedContent); + } + [Fact] public void Logical_Children_Should_Be_TabItems() { From b20f3f4cdfa0f2de0a63e8caf9891a0e9af1e215 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Wed, 30 Oct 2019 09:20:50 +0100 Subject: [PATCH 09/57] Update selected context when selected index changes --- .../Primitives/SelectingItemsControl.cs | 2 +- src/Avalonia.Controls/TabControl.cs | 59 +++++++++++++++++++ src/Avalonia.Controls/TabItem.cs | 19 ------ 3 files changed, 60 insertions(+), 20 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 7fddee1012..a030e2cd9a 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -358,7 +358,7 @@ namespace Avalonia.Controls.Primitives { if ((container.ContainerControl as ISelectable)?.IsSelected == true) { - if (SelectedIndex == -1) + if (SelectionMode.HasFlag(SelectionMode.Single)) { SelectedIndex = container.Index; } diff --git a/src/Avalonia.Controls/TabControl.cs b/src/Avalonia.Controls/TabControl.cs index 61ac0822b0..a2227d66e8 100644 --- a/src/Avalonia.Controls/TabControl.cs +++ b/src/Avalonia.Controls/TabControl.cs @@ -70,6 +70,7 @@ namespace Avalonia.Controls SelectionModeProperty.OverrideDefaultValue(SelectionMode.AlwaysSelected); ItemsPanelProperty.OverrideDefaultValue(DefaultPanel); AffectsMeasure(TabStripPlacementProperty); + SelectedIndexProperty.Changed.AddClassHandler((x, e) => x.UpdateSelectedContent(e)); } /// @@ -145,6 +146,64 @@ namespace Avalonia.Controls return RegisterContentPresenter(presenter); } + private void UpdateSelectedContent(AvaloniaPropertyChangedEventArgs e) + { + var index = (int)e.NewValue; + + if (index == -1) + { + SelectedContentTemplate = null; + + SelectedContent = null; + + return; + } + + var container = (TabItem)ItemContainerGenerator.ContainerFromIndex(index); + + if (container == null) + { + if (Items is AvaloniaList items) + { + container = items[index] as TabItem; + } + } + + if (container == null) + { + return; + } + + UpdateSelectedContent(container); + } + + private void UpdateSelectedContent(TabItem item) + { + if (SelectedContentTemplate != item.ContentTemplate) + { + SelectedContentTemplate = item.ContentTemplate; + } + + if (SelectedContent != item.Content) + { + SelectedContent = item.Content; + } + } + + protected override Size MeasureOverride(Size availableSize) + { + var size = base.MeasureOverride(availableSize); + + if (SelectedIndex != -1 && SelectedContent == null) + { + var container = (TabItem)ItemContainerGenerator.ContainerFromIndex(SelectedIndex); + + UpdateSelectedContent(container); + } + + return size; + } + /// /// Called when an is registered with the control. /// diff --git a/src/Avalonia.Controls/TabItem.cs b/src/Avalonia.Controls/TabItem.cs index fca1e022aa..0160bfcec1 100644 --- a/src/Avalonia.Controls/TabItem.cs +++ b/src/Avalonia.Controls/TabItem.cs @@ -30,7 +30,6 @@ namespace Avalonia.Controls { SelectableMixin.Attach(IsSelectedProperty); FocusableProperty.OverrideDefaultValue(typeof(TabItem), true); - IsSelectedProperty.Changed.AddClassHandler((x, e) => x.UpdateSelectedContent(e)); DataContextProperty.Changed.AddClassHandler((x, e) => x.UpdateHeader(e)); } @@ -83,23 +82,5 @@ namespace Avalonia.Controls } } } - - private void UpdateSelectedContent(AvaloniaPropertyChangedEventArgs e) - { - if (!IsSelected) - { - return; - } - - if (ParentTabControl.SelectedContentTemplate != ContentTemplate) - { - ParentTabControl.SelectedContentTemplate = ContentTemplate; - } - - if (ParentTabControl.SelectedContent != Content) - { - ParentTabControl.SelectedContent = Content; - } - } } } From 16f2bcdb9bc95b8e16b502c398d540dbf038603f Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Wed, 30 Oct 2019 09:27:10 +0100 Subject: [PATCH 10/57] Remove redundant ParentTabControl property --- src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs | 2 -- src/Avalonia.Controls/TabItem.cs | 2 -- 2 files changed, 4 deletions(-) diff --git a/src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs index a6a64e570b..1c5419735b 100644 --- a/src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs @@ -19,8 +19,6 @@ namespace Avalonia.Controls.Generators { var tabItem = (TabItem)base.CreateContainer(item); - tabItem.ParentTabControl = Owner; - tabItem[~TabControl.TabStripPlacementProperty] = Owner[~TabControl.TabStripPlacementProperty]; if (tabItem.HeaderTemplate == null) diff --git a/src/Avalonia.Controls/TabItem.cs b/src/Avalonia.Controls/TabItem.cs index 0160bfcec1..e27977bf3d 100644 --- a/src/Avalonia.Controls/TabItem.cs +++ b/src/Avalonia.Controls/TabItem.cs @@ -53,8 +53,6 @@ namespace Avalonia.Controls set { SetValue(IsSelectedProperty, value); } } - internal TabControl ParentTabControl { get; set; } - private void UpdateHeader(AvaloniaPropertyChangedEventArgs obj) { if (Header == null) From 5df55e5c9f42721a4917fb6228bead4349e9d450 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Thu, 31 Oct 2019 13:21:46 +0100 Subject: [PATCH 11/57] Fix multiselection --- .../Primitives/SelectingItemsControl.cs | 10 +++--- src/Avalonia.Controls/TabControl.cs | 33 +++++++++++-------- .../SelectingItemsControlTests_Multiple.cs | 1 + .../TabControlTests.cs | 4 +-- 4 files changed, 26 insertions(+), 22 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index a030e2cd9a..5a176d733d 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -358,17 +358,17 @@ namespace Avalonia.Controls.Primitives { if ((container.ContainerControl as ISelectable)?.IsSelected == true) { - if (SelectionMode.HasFlag(SelectionMode.Single)) - { - SelectedIndex = container.Index; - } - else + if (SelectionMode.HasFlag(SelectionMode.Multiple)) { if (_selection.Add(container.Index)) { resetSelectedItems = true; } } + else + { + SelectedIndex = container.Index; + } MarkContainerSelected(container.ContainerControl, true); } diff --git a/src/Avalonia.Controls/TabControl.cs b/src/Avalonia.Controls/TabControl.cs index a2227d66e8..87ba282db0 100644 --- a/src/Avalonia.Controls/TabControl.cs +++ b/src/Avalonia.Controls/TabControl.cs @@ -146,6 +146,25 @@ namespace Avalonia.Controls return RegisterContentPresenter(presenter); } + protected override void OnContainersMaterialized(ItemContainerEventArgs e) + { + base.OnContainersMaterialized(e); + + if (SelectedContent != null || SelectedIndex == -1) + { + return; + } + + var container = (TabItem)ItemContainerGenerator.ContainerFromIndex(SelectedIndex); + + if (container == null) + { + return; + } + + UpdateSelectedContent(container); + } + private void UpdateSelectedContent(AvaloniaPropertyChangedEventArgs e) { var index = (int)e.NewValue; @@ -190,20 +209,6 @@ namespace Avalonia.Controls } } - protected override Size MeasureOverride(Size availableSize) - { - var size = base.MeasureOverride(availableSize); - - if (SelectedIndex != -1 && SelectedContent == null) - { - var container = (TabItem)ItemContainerGenerator.ContainerFromIndex(SelectedIndex); - - UpdateSelectedContent(container); - } - - return size; - } - /// /// Called when an is registered with the control. /// diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs index be0f4272a5..952a00a14e 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs @@ -1080,6 +1080,7 @@ namespace Avalonia.Controls.UnitTests.Primitives var target = new TestSelector { Items = items, + SelectionMode = SelectionMode.Multiple, Template = Template(), }; diff --git a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs index 1e2c2084bd..2f8c974802 100644 --- a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs @@ -63,9 +63,7 @@ namespace Avalonia.Controls.UnitTests target.Items = items; - target.ApplyTemplate(); - - target.Measure(Size.Infinity); + ApplyTemplate(target); Assert.Equal(secondContent, target.SelectedContent); } From 38ae0140c253475d9695474e75587382f56969f3 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Fri, 1 Nov 2019 17:03:12 +0100 Subject: [PATCH 12/57] Remove special case that checks for AvaloniaList --- src/Avalonia.Controls/TabControl.cs | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/Avalonia.Controls/TabControl.cs b/src/Avalonia.Controls/TabControl.cs index 87ba282db0..8b1cd48379 100644 --- a/src/Avalonia.Controls/TabControl.cs +++ b/src/Avalonia.Controls/TabControl.cs @@ -4,7 +4,6 @@ using System.Linq; using Avalonia.Collections; using Avalonia.Controls.Generators; -using Avalonia.Controls.Mixins; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; @@ -180,14 +179,6 @@ namespace Avalonia.Controls var container = (TabItem)ItemContainerGenerator.ContainerFromIndex(index); - if (container == null) - { - if (Items is AvaloniaList items) - { - container = items[index] as TabItem; - } - } - if (container == null) { return; @@ -196,7 +187,7 @@ namespace Avalonia.Controls UpdateSelectedContent(container); } - private void UpdateSelectedContent(TabItem item) + private void UpdateSelectedContent(IContentControl item) { if (SelectedContentTemplate != item.ContentTemplate) { From 75142ce5fa71fc89991eeb4753352b50a6648eec Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sat, 2 Nov 2019 21:43:01 +0100 Subject: [PATCH 13/57] Set grid splitter min width and height to avoid invisible splitters. Make splitter background lighter. --- src/Avalonia.Diagnostics/Views/TreePageView.xaml | 4 ++-- src/Avalonia.Themes.Default/GridSplitter.xaml | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Diagnostics/Views/TreePageView.xaml b/src/Avalonia.Diagnostics/Views/TreePageView.xaml index ca7314264a..2619fd744a 100644 --- a/src/Avalonia.Diagnostics/Views/TreePageView.xaml +++ b/src/Avalonia.Diagnostics/Views/TreePageView.xaml @@ -2,7 +2,7 @@ xmlns:vm="clr-namespace:Avalonia.Diagnostics.ViewModels" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="Avalonia.Diagnostics.Views.TreePageView"> - + - + diff --git a/src/Avalonia.Themes.Default/GridSplitter.xaml b/src/Avalonia.Themes.Default/GridSplitter.xaml index cfab5dab56..dc5cd002dc 100644 --- a/src/Avalonia.Themes.Default/GridSplitter.xaml +++ b/src/Avalonia.Themes.Default/GridSplitter.xaml @@ -2,7 +2,9 @@ - - - - - - - - - - - - diff --git a/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs index 4fc63ea054..7adceddacf 100644 --- a/samples/ControlCatalog/App.xaml.cs +++ b/samples/ControlCatalog/App.xaml.cs @@ -13,15 +13,6 @@ namespace ControlCatalog public override void Initialize() { AvaloniaXamlLoader.Load(this); - - Name = "Avalonia"; - - _recentMenu = (NativeMenu.GetMenu(this).Items[1] as NativeMenuItem).Menu; - } - - public void OnOpenClicked(object sender, EventArgs args) - { - _recentMenu.Items.Insert(0, new NativeMenuItem("Item " + (_recentMenu.Items.Count + 1))); } public override void OnFrameworkInitializationCompleted() From 3e8138cfe4782e5ca007f96abb5db41083a43d76 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 6 Nov 2019 11:38:21 +0000 Subject: [PATCH 29/57] Set a default Avalonia application name. --- src/Avalonia.Controls/Application.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index ce60a0f0b9..59c6c47ed9 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -48,6 +48,14 @@ namespace Avalonia /// public event EventHandler ResourcesChanged; + /// + /// Creates an instance of the class. + /// + public Application() + { + Name = "Avalonia Application"; + } + /// /// Gets the current instance of the class. /// From 48234ad3ab8aea1c8ff5801ff20c4ea565f2e615 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Wed, 6 Nov 2019 20:21:15 +0800 Subject: [PATCH 30/57] Add a Avalonia about window default implementation. --- src/Avalonia.Dialogs/DefaultAboutWindow.xaml | 109 ++++++++++++++++++ .../DefaultAboutWindow.xaml.cs | 16 +++ 2 files changed, 125 insertions(+) create mode 100644 src/Avalonia.Dialogs/DefaultAboutWindow.xaml create mode 100644 src/Avalonia.Dialogs/DefaultAboutWindow.xaml.cs diff --git a/src/Avalonia.Dialogs/DefaultAboutWindow.xaml b/src/Avalonia.Dialogs/DefaultAboutWindow.xaml new file mode 100644 index 0000000000..8d666f7e54 --- /dev/null +++ b/src/Avalonia.Dialogs/DefaultAboutWindow.xaml @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Dialogs/DefaultAboutWindow.xaml.cs b/src/Avalonia.Dialogs/DefaultAboutWindow.xaml.cs new file mode 100644 index 0000000000..b5adab5b7c --- /dev/null +++ b/src/Avalonia.Dialogs/DefaultAboutWindow.xaml.cs @@ -0,0 +1,16 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Avalonia.Dialogs +{ + public class DefaultAboutWindow : Window + { + public DefaultAboutWindow() + { + AvaloniaXamlLoader.Load(this); + } + } +} From 5da9bcd72304537e9f4eb6978775f6a1bfde729f Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Wed, 6 Nov 2019 22:51:47 +0800 Subject: [PATCH 31/57] Change to AboutAvaloniaDialog --- .../{DefaultAboutWindow.xaml => AboutAvaloniaDialog.xaml} | 2 +- ...DefaultAboutWindow.xaml.cs => AboutAvaloniaDialog.xaml.cs} | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename src/Avalonia.Dialogs/{DefaultAboutWindow.xaml => AboutAvaloniaDialog.xaml} (98%) rename src/Avalonia.Dialogs/{DefaultAboutWindow.xaml.cs => AboutAvaloniaDialog.xaml.cs} (79%) diff --git a/src/Avalonia.Dialogs/DefaultAboutWindow.xaml b/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml similarity index 98% rename from src/Avalonia.Dialogs/DefaultAboutWindow.xaml rename to src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml index 8d666f7e54..fd62bddd3c 100644 --- a/src/Avalonia.Dialogs/DefaultAboutWindow.xaml +++ b/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml @@ -6,7 +6,7 @@ MinHeight="220" Title="About Avalonia" Background="Purple" - x:Class="Avalonia.Dialogs.DefaultAboutWindow"> + x:Class="Avalonia.Dialogs.AboutAvaloniaDialog"> - + + + + + - + - + - + - + - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + - - + - + \ No newline at end of file From 08bb2399f6944c6fbcc444b1582a932a289641b1 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 7 Nov 2019 13:41:38 +0000 Subject: [PATCH 40/57] implement File -> Exit and Help -> About in control catalog. --- samples/ControlCatalog/MainWindow.xaml | 4 ++-- .../ViewModels/MainWindowViewModel.cs | 20 +++++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/samples/ControlCatalog/MainWindow.xaml b/samples/ControlCatalog/MainWindow.xaml index c45548e439..248f94082d 100644 --- a/samples/ControlCatalog/MainWindow.xaml +++ b/samples/ControlCatalog/MainWindow.xaml @@ -45,10 +45,10 @@ - + - + diff --git a/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs b/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs index adf0345a70..89e7653618 100644 --- a/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs +++ b/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs @@ -1,5 +1,7 @@ using System.Reactive; +using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.Notifications; +using Avalonia.Dialogs; using ReactiveUI; namespace ControlCatalog.ViewModels @@ -26,6 +28,20 @@ namespace ControlCatalog.ViewModels { NotificationManager.Show(new Avalonia.Controls.Notifications.Notification("Error", "Native Notifications are not quite ready. Coming soon.", NotificationType.Error)); }); + + AboutCommand = ReactiveCommand.CreateFromTask(async () => + { + var dialog = new AboutAvaloniaDialog(); + + var mainWindow = (App.Current.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.MainWindow; + + await dialog.ShowDialog(mainWindow); + }); + + ExitCommand = ReactiveCommand.Create(() => + { + (App.Current.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime).Shutdown(); + }); } public IManagedNotificationManager NotificationManager @@ -39,5 +55,9 @@ namespace ControlCatalog.ViewModels public ReactiveCommand ShowManagedNotificationCommand { get; } public ReactiveCommand ShowNativeNotificationCommand { get; } + + public ReactiveCommand AboutCommand { get; } + + public ReactiveCommand ExitCommand { get; } } } From 868b125848f2380f80adb659cd6ac697d41581c7 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 7 Nov 2019 14:13:14 +0000 Subject: [PATCH 41/57] add hyper links. --- src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml | 12 +++- .../AboutAvaloniaDialog.xaml.cs | 55 +++++++++++++++++++ 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml b/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml index d7fcb22b95..ffc23cc5b0 100644 --- a/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml +++ b/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml @@ -73,6 +73,14 @@ - + + +