committed by
GitHub
4 changed files with 1071 additions and 289 deletions
@ -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 |
|||
} |
|||
} |
|||
|
|||
@ -1,51 +1,23 @@ |
|||
<Styles xmlns="https://github.com/avaloniaui"> |
|||
<Style Selector="GridSplitter:vertical"> |
|||
<Setter Property="Width" Value="6"/> |
|||
<Setter Property="Background" Value="{DynamicResource ThemeControlLowBrush}"/> |
|||
<Setter Property="Template"> |
|||
<ControlTemplate> |
|||
<Border Background="{TemplateBinding Background}"> |
|||
<StackPanel Orientation="Vertical" HorizontalAlignment="Center" VerticalAlignment="Center"> |
|||
<StackPanel.Styles> |
|||
<Style Selector="Ellipse"> |
|||
<Setter Property="HorizontalAlignment" Value="Center"/> |
|||
<Setter Property="Width" Value="4"/> |
|||
<Setter Property="Height" Value="4"/> |
|||
<Setter Property="Fill" Value="{DynamicResource ThemeControlMidBrush}"/> |
|||
<Setter Property="Margin" Value="1"/> |
|||
</Style> |
|||
</StackPanel.Styles> |
|||
<Ellipse/> |
|||
<Ellipse/> |
|||
<Ellipse/> |
|||
</StackPanel> |
|||
</Border> |
|||
</ControlTemplate> |
|||
|
|||
<Style Selector="GridSplitter"> |
|||
<Setter Property="Focusable" Value="True" /> |
|||
<Setter Property="MinWidth" Value="6" /> |
|||
<Setter Property="MinHeight" Value="6" /> |
|||
<Setter Property="Background" Value="{DynamicResource ThemeControlMidBrush}" /> |
|||
<Setter Property="PreviewContent"> |
|||
<Template> |
|||
<Rectangle Fill="{DynamicResource HighlightBrush}" /> |
|||
</Template> |
|||
</Setter> |
|||
</Style> |
|||
<Style Selector="GridSplitter:horizontal"> |
|||
<Setter Property="Height" Value="6"/> |
|||
<Setter Property="Background" Value="{DynamicResource ThemeControlLowBrush}"/> |
|||
<Setter Property="Template"> |
|||
<ControlTemplate> |
|||
<Border Background="{TemplateBinding Background}"> |
|||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center"> |
|||
<StackPanel.Styles> |
|||
<Style Selector="Ellipse"> |
|||
<Setter Property="VerticalAlignment" Value="Center"/> |
|||
<Setter Property="Width" Value="4"/> |
|||
<Setter Property="Height" Value="4"/> |
|||
<Setter Property="Fill" Value="{DynamicResource ThemeControlMidBrush}"/> |
|||
<Setter Property="Margin" Value="1"/> |
|||
</Style> |
|||
</StackPanel.Styles> |
|||
<Ellipse/> |
|||
<Ellipse/> |
|||
<Ellipse/> |
|||
</StackPanel> |
|||
</Border> |
|||
<Border |
|||
BorderBrush="{TemplateBinding BorderBrush}" |
|||
BorderThickness="{TemplateBinding BorderThickness}" |
|||
Background="{TemplateBinding Background}"/> |
|||
</ControlTemplate> |
|||
</Setter> |
|||
</Style> |
|||
</Styles> |
|||
|
|||
</Styles> |
|||
|
|||
Loading…
Reference in new issue