|
|
@@ -1,210 +1,841 @@
|
|
|
-// 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
|
|
|
{
|
|
|
/// <summary>
|
|
|
- /// 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 <see cref="Grid"/> control.
|
|
|
/// </summary>
|
|
|
- /// <remarks>
|
|
|
- /// Unlike WPF GridSplitter, Avalonia GridSplitter has only one Behavior, GridResizeBehavior.PreviousAndNext.
|
|
|
- /// </remarks>
|
|
|
public class GridSplitter : Thumb
|
|
|
{
|
|
|
- private List<DefinitionBase> _definitions;
|
|
|
+ /// <summary>
|
|
|
+ /// Defines the <see cref="ResizeDirection"/> property.
|
|
|
+ /// </summary>
|
|
|
+ public static readonly AvaloniaProperty<GridResizeDirection> ResizeDirectionProperty =
|
|
|
+ AvaloniaProperty.Register<GridSplitter, GridResizeDirection>(nameof(ResizeDirection));
|
|
|
|
|
|
- private Grid _grid;
|
|
|
+ /// <summary>
|
|
|
+ /// Defines the <see cref="ResizeBehavior"/> property.
|
|
|
+ /// </summary>
|
|
|
+ public static readonly AvaloniaProperty<GridResizeBehavior> ResizeBehaviorProperty =
|
|
|
+ AvaloniaProperty.Register<GridSplitter, GridResizeBehavior>(nameof(ResizeBehavior));
|
|
|
|
|
|
- private DefinitionBase _nextDefinition;
|
|
|
+ /// <summary>
|
|
|
+ /// Defines the <see cref="ShowsPreview"/> property.
|
|
|
+ /// </summary>
|
|
|
+ public static readonly AvaloniaProperty<bool> ShowsPreviewProperty =
|
|
|
+ AvaloniaProperty.Register<GridSplitter, bool>(nameof(ShowsPreview));
|
|
|
|
|
|
- private Orientation _orientation;
|
|
|
+ /// <summary>
|
|
|
+ /// Defines the <see cref="KeyboardIncrement"/> property.
|
|
|
+ /// </summary>
|
|
|
+ public static readonly AvaloniaProperty<double> KeyboardIncrementProperty =
|
|
|
+ AvaloniaProperty.Register<GridSplitter, double>(nameof(KeyboardIncrement), 10d);
|
|
|
|
|
|
- private DefinitionBase _prevDefinition;
|
|
|
+ /// <summary>
|
|
|
+ /// Defines the <see cref="DragIncrement"/> property.
|
|
|
+ /// </summary>
|
|
|
+ public static readonly AvaloniaProperty<double> DragIncrementProperty =
|
|
|
+ AvaloniaProperty.Register<GridSplitter, double>(nameof(DragIncrement), 1d);
|
|
|
|
|
|
- private void GetDeltaConstraints(out double min, out double max)
|
|
|
+ /// <summary>
|
|
|
+ /// Defines the <see cref="PreviewContent"/> property.
|
|
|
+ /// </summary>
|
|
|
+ public static readonly AvaloniaProperty<ITemplate<IControl>> PreviewContentProperty =
|
|
|
+ AvaloniaProperty.Register<GridSplitter, ITemplate<IControl>>(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;
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Indicates whether the Splitter resizes the Columns, Rows, or Both.
|
|
|
+ /// </summary>
|
|
|
+ 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);
|
|
|
+ /// <summary>
|
|
|
+ /// Indicates which Columns or Rows the Splitter resizes.
|
|
|
+ /// </summary>
|
|
|
+ public GridResizeBehavior ResizeBehavior
|
|
|
+ {
|
|
|
+ get => GetValue(ResizeBehaviorProperty);
|
|
|
+ set => SetValue(ResizeBehaviorProperty, value);
|
|
|
}
|
|
|
|
|
|
- protected override void OnDragDelta(VectorEventArgs e)
|
|
|
+ /// <summary>
|
|
|
+ /// Indicates whether to Preview the column resizing without updating layout.
|
|
|
+ /// </summary>
|
|
|
+ public bool ShowsPreview
|
|
|
{
|
|
|
- // 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(ShowsPreviewProperty);
|
|
|
+ set => SetValue(ShowsPreviewProperty, value);
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// The Distance to move the splitter when pressing the keyboard arrow keys.
|
|
|
+ /// </summary>
|
|
|
+ public double KeyboardIncrement
|
|
|
+ {
|
|
|
+ get => GetValue(KeyboardIncrementProperty);
|
|
|
+ set => SetValue(KeyboardIncrementProperty, value);
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Restricts splitter to move a multiple of the specified units.
|
|
|
+ /// </summary>
|
|
|
+ public double DragIncrement
|
|
|
+ {
|
|
|
+ get => GetValue(DragIncrementProperty);
|
|
|
+ set => SetValue(DragIncrementProperty, value);
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Gets or sets content that will be shown when <see cref="ShowsPreview"/> is enabled and user starts resize operation.
|
|
|
+ /// </summary>
|
|
|
+ public ITemplate<IControl> PreviewContent
|
|
|
+ {
|
|
|
+ get => GetValue(PreviewContentProperty);
|
|
|
+ set => SetValue(PreviewContentProperty, value);
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Converts BasedOnAlignment direction to Rows, Columns, or Both depending on its width/height.
|
|
|
+ /// </summary>
|
|
|
+ internal GridResizeDirection GetEffectiveResizeDirection()
|
|
|
+ {
|
|
|
+ GridResizeDirection direction = ResizeDirection;
|
|
|
+
|
|
|
+ if (direction != GridResizeDirection.Auto)
|
|
|
+ {
|
|
|
+ return direction;
|
|
|
+ }
|
|
|
+
|
|
|
+ // When HorizontalAlignment is Left, Right or Center, resize Columns.
|
|
|
+ if (HorizontalAlignment != HorizontalAlignment.Stretch)
|
|
|
+ {
|
|
|
+ direction = GridResizeDirection.Columns;
|
|
|
+ }
|
|
|
+ else if (VerticalAlignment != VerticalAlignment.Stretch)
|
|
|
+ {
|
|
|
+ direction = GridResizeDirection.Rows;
|
|
|
+ }
|
|
|
+ else if (Bounds.Width <= Bounds.Height) // Fall back to Width vs Height.
|
|
|
+ {
|
|
|
+ direction = GridResizeDirection.Columns;
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ direction = GridResizeDirection.Rows;
|
|
|
+ }
|
|
|
|
|
|
- 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);
|
|
|
+ return direction;
|
|
|
+ }
|
|
|
|
|
|
- var prevIsStar = IsStar(_prevDefinition);
|
|
|
- var nextIsStar = IsStar(_nextDefinition);
|
|
|
+ /// <summary>
|
|
|
+ /// Convert BasedOnAlignment to Next/Prev/Both depending on alignment and Direction.
|
|
|
+ /// </summary>
|
|
|
+ private GridResizeBehavior GetEffectiveResizeBehavior(GridResizeDirection direction)
|
|
|
+ {
|
|
|
+ GridResizeBehavior resizeBehavior = ResizeBehavior;
|
|
|
|
|
|
- if (prevIsStar && nextIsStar)
|
|
|
+ if (resizeBehavior == GridResizeBehavior.BasedOnAlignment)
|
|
|
{
|
|
|
- foreach (var definition in _definitions)
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Removes preview adorner from the grid.
|
|
|
+ /// </summary>
|
|
|
+ private void RemovePreviewAdorner()
|
|
|
+ {
|
|
|
+ if (_resizeData.Adorner != null)
|
|
|
+ {
|
|
|
+ AdornerLayer layer = AdornerLayer.GetAdornerLayer(this);
|
|
|
+ layer.Children.Remove(_resizeData.Adorner);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Initialize the data needed for resizing.
|
|
|
+ /// </summary>
|
|
|
+ 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();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Returns true if GridSplitter can resize rows/columns.
|
|
|
+ /// </summary>
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Create the preview adorner and add it to the adorner layer.
|
|
|
+ /// </summary>
|
|
|
+ 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);
|
|
|
+
|
|
|
+ // 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);
|
|
|
}
|
|
|
|
|
|
- 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)
|
|
|
+ /// <summary>
|
|
|
+ /// Cancels the resize operation.
|
|
|
+ /// </summary>
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Returns true if the row/column has a star length.
|
|
|
+ /// </summary>
|
|
|
+ private static bool IsStar(DefinitionBase definition)
|
|
|
+ {
|
|
|
+ return definition.UserSizeValueCache.IsStar;
|
|
|
}
|
|
|
|
|
|
- private void SetLength(DefinitionBase definition, double value)
|
|
|
+ /// <summary>
|
|
|
+ /// Gets Column or Row definition at index from grid based on resize direction.
|
|
|
+ /// </summary>
|
|
|
+ private static DefinitionBase GetGridDefinition(Grid grid, int index, GridResizeDirection direction)
|
|
|
{
|
|
|
- var columnDefinition = definition as ColumnDefinition;
|
|
|
- if (columnDefinition != null)
|
|
|
+ return direction == GridResizeDirection.Columns ?
|
|
|
+ (DefinitionBase)grid.ColumnDefinitions[index] :
|
|
|
+ (DefinitionBase)grid.RowDefinitions[index];
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Retrieves the ActualWidth or ActualHeight of the definition depending on its type Column or Row.
|
|
|
+ /// </summary>
|
|
|
+ private double GetActualLength(DefinitionBase definition)
|
|
|
+ {
|
|
|
+ var column = definition as ColumnDefinition;
|
|
|
+
|
|
|
+ return column?.ActualWidth ?? ((RowDefinition)definition).ActualHeight;
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Gets Column or Row definition at index from grid based on resize direction.
|
|
|
+ /// </summary>
|
|
|
+ private static void SetDefinitionLength(DefinitionBase definition, GridLength length)
|
|
|
+ {
|
|
|
+ definition.SetValue(
|
|
|
+ definition is ColumnDefinition ? ColumnDefinition.WidthProperty : RowDefinition.HeightProperty, length);
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Get the minimum and maximum Delta can be given definition constraints (MinWidth/MaxWidth).
|
|
|
+ /// </summary>
|
|
|
+ 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)
|
|
|
{
|
|
|
- columnDefinition.Width = new GridLength(value);
|
|
|
+ definition1Min = Math.Max(definition1Min, _resizeData.SplitterLength);
|
|
|
}
|
|
|
- else
|
|
|
+ else if (_resizeData.SplitterIndex == _resizeData.Definition2Index)
|
|
|
{
|
|
|
- ((RowDefinition)definition).Height = new GridLength(value);
|
|
|
+ definition2Min = Math.Max(definition2Min, _resizeData.SplitterLength);
|
|
|
}
|
|
|
+
|
|
|
+ // Determine the minimum and maximum the columns can be resized.
|
|
|
+ minDelta = -Math.Min(definition1Len - definition1Min, definition2Max - definition2Len);
|
|
|
+ maxDelta = Math.Min(definition1Max - definition1Len, definition2Len - definition2Min);
|
|
|
}
|
|
|
|
|
|
- protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
|
|
|
+ /// <summary>
|
|
|
+ /// Sets the length of definition1 and definition2.
|
|
|
+ /// </summary>
|
|
|
+ private void SetLengths(double definition1Pixels, double definition2Pixels)
|
|
|
{
|
|
|
- base.OnAttachedToVisualTree(e);
|
|
|
- _grid = this.GetVisualParent<Grid>();
|
|
|
+ // 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<DefinitionBase>)_resizeData.Grid.ColumnDefinitions :
|
|
|
+ (IAvaloniaReadOnlyList<DefinitionBase>)_resizeData.Grid.RowDefinitions;
|
|
|
|
|
|
- _orientation = DetectOrientation();
|
|
|
+ var definitionsCount = definitions.Count;
|
|
|
+
|
|
|
+ for (var i = 0; i < definitionsCount; i++)
|
|
|
+ {
|
|
|
+ DefinitionBase definition = definitions[i];
|
|
|
|
|
|
- int definitionIndex; //row or col
|
|
|
- if (_orientation == Orientation.Vertical)
|
|
|
+ // 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<DefinitionBase>().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<DefinitionBase>().ToList();
|
|
|
- PseudoClasses.Add(":horizontal");
|
|
|
+ SetDefinitionLength(_resizeData.Definition2, new GridLength(definition2Pixels));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Move the splitter by the given Delta's in the horizontal and vertical directions.
|
|
|
+ /// </summary>
|
|
|
+ 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();
|
|
|
+
|
|
|
+ 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);
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- if (definitionIndex > 0)
|
|
|
- _prevDefinition = _definitions[definitionIndex - 1];
|
|
|
+ /// <summary>
|
|
|
+ /// Move the splitter using the Keyboard (Don't show preview).
|
|
|
+ /// </summary>
|
|
|
+ private bool KeyboardMoveSplitter(double horizontalChange, double verticalChange)
|
|
|
+ {
|
|
|
+ // If moving with the mouse, ignore keyboard motion.
|
|
|
+ if (_resizeData != null)
|
|
|
+ {
|
|
|
+ return false; // Don't handle the event.
|
|
|
+ }
|
|
|
+
|
|
|
+ // Don't show preview.
|
|
|
+ InitializeData(false);
|
|
|
+
|
|
|
+ // Check that we are actually able to resize.
|
|
|
+ if (_resizeData == null)
|
|
|
+ {
|
|
|
+ return false; // Don't handle the event.
|
|
|
+ }
|
|
|
|
|
|
- if (definitionIndex < _definitions.Count - 1)
|
|
|
- _nextDefinition = _definitions[definitionIndex + 1];
|
|
|
+ MoveSplitter(horizontalChange, verticalChange);
|
|
|
+
|
|
|
+ _resizeData = null;
|
|
|
+
|
|
|
+ return true;
|
|
|
}
|
|
|
|
|
|
- private Orientation DetectOrientation()
|
|
|
+ /// <summary>
|
|
|
+ /// This adorner draws the preview for the <see cref="GridSplitter"/>.
|
|
|
+ /// It also positions the adorner.
|
|
|
+ /// </summary>
|
|
|
+ private sealed class PreviewAdorner : Decorator
|
|
|
{
|
|
|
- if (!_grid.ColumnDefinitions.Any())
|
|
|
- return Orientation.Horizontal;
|
|
|
- if (!_grid.RowDefinitions.Any())
|
|
|
- return Orientation.Vertical;
|
|
|
+ private readonly TranslateTransform _translation;
|
|
|
+ private readonly Decorator _decorator;
|
|
|
+
|
|
|
+ public PreviewAdorner(IControl previewControl)
|
|
|
+ {
|
|
|
+ // Add a decorator to perform translations.
|
|
|
+ _translation = new TranslateTransform();
|
|
|
+
|
|
|
+ _decorator = new Decorator
|
|
|
+ {
|
|
|
+ Child = previewControl,
|
|
|
+ RenderTransform = _translation
|
|
|
+ };
|
|
|
+
|
|
|
+ Child = _decorator;
|
|
|
+ }
|
|
|
|
|
|
- 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)
|
|
|
+ /// <summary>
|
|
|
+ /// The Preview's Offset in the X direction from the GridSplitter.
|
|
|
+ /// </summary>
|
|
|
+ public double OffsetX
|
|
|
{
|
|
|
- return Orientation.Vertical;
|
|
|
+ get => _translation.X;
|
|
|
+ set => _translation.X = value;
|
|
|
}
|
|
|
- if (!width.IsAuto && height.IsAuto)
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// The Preview's Offset in the Y direction from the GridSplitter.
|
|
|
+ /// </summary>
|
|
|
+ public double OffsetY
|
|
|
{
|
|
|
- return Orientation.Horizontal;
|
|
|
+ get => _translation.Y;
|
|
|
+ set => _translation.Y = value;
|
|
|
}
|
|
|
- if (_grid.Children.OfType<Control>() // 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;
|
|
|
}
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// <see cref="GridSplitter"/> 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.
|
|
|
+ /// </summary>
|
|
|
+ private enum SplitBehavior
|
|
|
+ {
|
|
|
+ /// <summary>
|
|
|
+ /// Both columns/rows are star lengths.
|
|
|
+ /// </summary>
|
|
|
+ Split,
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Resize 1 only.
|
|
|
+ /// </summary>
|
|
|
+ Resize1,
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Resize 2 only.
|
|
|
+ /// </summary>
|
|
|
+ Resize2
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Stores data during the resizing operation.
|
|
|
+ /// </summary>
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Enum to indicate whether <see cref="GridSplitter"/> resizes Columns or Rows.
|
|
|
+ /// </summary>
|
|
|
+ public enum GridResizeDirection
|
|
|
+ {
|
|
|
+ /// <summary>
|
|
|
+ /// Determines whether to resize rows or columns based on its Alignment and
|
|
|
+ /// width compared to height.
|
|
|
+ /// </summary>
|
|
|
+ Auto,
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Resize columns when dragging Splitter.
|
|
|
+ /// </summary>
|
|
|
+ Columns,
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Resize rows when dragging Splitter.
|
|
|
+ /// </summary>
|
|
|
+ Rows
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Enum to indicate what Columns or Rows the <see cref="GridSplitter"/> resizes.
|
|
|
+ /// </summary>
|
|
|
+ public enum GridResizeBehavior
|
|
|
+ {
|
|
|
+ /// <summary>
|
|
|
+ /// Determine which columns or rows to resize based on its Alignment.
|
|
|
+ /// </summary>
|
|
|
+ BasedOnAlignment,
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Resize the current and next Columns or Rows.
|
|
|
+ /// </summary>
|
|
|
+ CurrentAndNext,
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Resize the previous and current Columns or Rows.
|
|
|
+ /// </summary>
|
|
|
+ PreviousAndCurrent,
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Resize the previous and next Columns or Rows.
|
|
|
+ /// </summary>
|
|
|
+ PreviousAndNext
|
|
|
}
|
|
|
}
|