csharpc-sharpdotnetxamlavaloniauicross-platformcross-platform-xamlavaloniaguimulti-platformuser-interfacedotnetcore
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
948 lines
36 KiB
948 lines
36 KiB
// 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.Diagnostics;
|
|
using Avalonia.Collections;
|
|
using Avalonia.Controls.Primitives;
|
|
using Avalonia.Controls.Presenters;
|
|
using Avalonia.Input;
|
|
using Avalonia.Interactivity;
|
|
using Avalonia.Layout;
|
|
using Avalonia.Media;
|
|
using Avalonia.Utilities;
|
|
|
|
namespace Avalonia.Controls
|
|
{
|
|
/// <summary>
|
|
/// Represents the control that redistributes space between columns or rows of a <see cref="Grid"/> control.
|
|
/// </summary>
|
|
public class GridSplitter : Thumb
|
|
{
|
|
/// <summary>
|
|
/// Defines the <see cref="ResizeDirection"/> property.
|
|
/// </summary>
|
|
public static readonly StyledProperty<GridResizeDirection> ResizeDirectionProperty =
|
|
AvaloniaProperty.Register<GridSplitter, GridResizeDirection>(nameof(ResizeDirection));
|
|
|
|
/// <summary>
|
|
/// Defines the <see cref="ResizeBehavior"/> property.
|
|
/// </summary>
|
|
public static readonly StyledProperty<GridResizeBehavior> ResizeBehaviorProperty =
|
|
AvaloniaProperty.Register<GridSplitter, GridResizeBehavior>(nameof(ResizeBehavior));
|
|
|
|
/// <summary>
|
|
/// Defines the <see cref="ShowsPreview"/> property.
|
|
/// </summary>
|
|
public static readonly StyledProperty<bool> ShowsPreviewProperty =
|
|
AvaloniaProperty.Register<GridSplitter, bool>(nameof(ShowsPreview));
|
|
|
|
/// <summary>
|
|
/// Defines the <see cref="KeyboardIncrement"/> property.
|
|
/// </summary>
|
|
public static readonly StyledProperty<double> KeyboardIncrementProperty =
|
|
AvaloniaProperty.Register<GridSplitter, double>(nameof(KeyboardIncrement), 10d);
|
|
|
|
/// <summary>
|
|
/// Defines the <see cref="DragIncrement"/> property.
|
|
/// </summary>
|
|
public static readonly StyledProperty<double> DragIncrementProperty =
|
|
AvaloniaProperty.Register<GridSplitter, double>(nameof(DragIncrement), 1d);
|
|
|
|
/// <summary>
|
|
/// Defines the <see cref="PreviewContent"/> property.
|
|
/// </summary>
|
|
public static readonly StyledProperty<ITemplate<Control>> PreviewContentProperty =
|
|
AvaloniaProperty.Register<GridSplitter, ITemplate<Control>>(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;
|
|
private bool _isFocusEngaged;
|
|
|
|
/// <summary>
|
|
/// Indicates whether the Splitter resizes the Columns, Rows, or Both.
|
|
/// </summary>
|
|
public GridResizeDirection ResizeDirection
|
|
{
|
|
get => GetValue(ResizeDirectionProperty);
|
|
set => SetValue(ResizeDirectionProperty, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Indicates which Columns or Rows the Splitter resizes.
|
|
/// </summary>
|
|
public GridResizeBehavior ResizeBehavior
|
|
{
|
|
get => GetValue(ResizeBehaviorProperty);
|
|
set => SetValue(ResizeBehaviorProperty, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Indicates whether to Preview the column resizing without updating layout.
|
|
/// </summary>
|
|
public bool ShowsPreview
|
|
{
|
|
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<Control> 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;
|
|
}
|
|
|
|
return direction;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Convert BasedOnAlignment to Next/Prev/Both depending on alignment and Direction.
|
|
/// </summary>
|
|
private GridResizeBehavior GetEffectiveResizeBehavior(GridResizeDirection direction)
|
|
{
|
|
GridResizeBehavior resizeBehavior = ResizeBehavior;
|
|
|
|
if (resizeBehavior == GridResizeBehavior.BasedOnAlignment)
|
|
{
|
|
if (direction == GridResizeDirection.Columns)
|
|
{
|
|
switch (HorizontalAlignment)
|
|
{
|
|
case HorizontalAlignment.Left:
|
|
resizeBehavior = GridResizeBehavior.PreviousAndCurrent;
|
|
break;
|
|
case HorizontalAlignment.Right:
|
|
resizeBehavior = GridResizeBehavior.CurrentAndNext;
|
|
break;
|
|
default:
|
|
resizeBehavior = GridResizeBehavior.PreviousAndNext;
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
switch (VerticalAlignment)
|
|
{
|
|
case VerticalAlignment.Top:
|
|
resizeBehavior = GridResizeBehavior.PreviousAndCurrent;
|
|
break;
|
|
case VerticalAlignment.Bottom:
|
|
resizeBehavior = GridResizeBehavior.CurrentAndNext;
|
|
break;
|
|
default:
|
|
resizeBehavior = GridResizeBehavior.PreviousAndNext;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
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.
|
|
var grid = GetParentGrid();
|
|
|
|
if (grid != null)
|
|
{
|
|
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),
|
|
Scaling = (VisualRoot as ILayoutRoot)?.LayoutScaling ?? 1,
|
|
};
|
|
|
|
// 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()
|
|
{
|
|
// Get properties values from ContentPresenter if Grid it's used in ItemsControl as ItemsPanel otherwise directly from GridSplitter.
|
|
var sourceControl = GetPropertiesValueSource();
|
|
int gridSpan = sourceControl.GetValue(_resizeData!.ResizeDirection == GridResizeDirection.Columns ?
|
|
Grid.ColumnSpanProperty :
|
|
Grid.RowSpanProperty);
|
|
|
|
if (gridSpan == 1)
|
|
{
|
|
var splitterIndex = sourceControl.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)
|
|
{
|
|
// 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;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create the preview adorner and add it to the adorner layer.
|
|
/// </summary>
|
|
private void SetupPreviewAdorner()
|
|
{
|
|
if (_resizeData!.ShowsPreview)
|
|
{
|
|
// 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;
|
|
}
|
|
|
|
Control? builtPreviewContent = previewContent?.Build();
|
|
|
|
_resizeData.Adorner = new PreviewAdorner(builtPreviewContent);
|
|
|
|
AdornerLayer.SetAdornedElement(_resizeData.Adorner, this);
|
|
AdornerLayer.SetIsClipEnabled(_resizeData.Adorner, false);
|
|
|
|
adornerLayer.Children.Add(_resizeData.Adorner);
|
|
|
|
// Get constraints on preview's translation.
|
|
GetDeltaConstraints(out _resizeData.MinChange, out _resizeData.MaxChange);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retrieves the <see cref="Grid"/> that ultimately hosts this
|
|
/// <see cref="GridSplitter"/> in the visual/logical tree.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// A splitter can be placed directly inside a <see cref="Grid"/> or
|
|
/// indirectly inside an <see cref="ItemsControl"/> that uses a
|
|
/// <see cref="Grid"/> as its <see cref="ItemsControl.ItemsPanel"/>.
|
|
/// In the latter case the first logical parent is usually an
|
|
/// <see cref="ContentPresenter"/> (or the items control itself),
|
|
/// so the method walks these intermediate containers to locate the
|
|
/// underlying grid.
|
|
/// </remarks>
|
|
/// <returns>
|
|
/// The containing <see cref="Grid"/> if one is found; otherwise
|
|
/// <c>null</c>.
|
|
/// </returns>
|
|
protected virtual Grid? GetParentGrid()
|
|
{
|
|
// When GridSplitter is used inside an ItemsControl with Grid as
|
|
// its ItemsPanel, its immediate parent is usually a ItemsControl or ContentPresenter.
|
|
switch (Parent)
|
|
{
|
|
case Grid grid:
|
|
{
|
|
return grid;
|
|
}
|
|
case ItemsControl itemsControl:
|
|
{
|
|
if (itemsControl.ItemsPanelRoot is Grid grid)
|
|
{
|
|
return grid;
|
|
}
|
|
|
|
break;
|
|
}
|
|
case ContentPresenter { Parent: ItemsControl presenterItemsControl }:
|
|
{
|
|
if (presenterItemsControl.ItemsPanelRoot is Grid grid)
|
|
{
|
|
return grid;
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the element that carries the grid-attached properties
|
|
/// (<see cref="Grid.RowProperty"/>, <see cref="Grid.ColumnProperty"/>, etc.) relevant
|
|
/// to this <see cref="GridSplitter"/>.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// When the splitter is generated as part of an <see cref="ItemsControl"/>
|
|
/// template, the attached properties are set on the surrounding
|
|
/// <see cref="ContentPresenter"/> rather than on the splitter itself.
|
|
/// This helper selects that presenter when appropriate so subsequent
|
|
/// property look-ups read the correct values; otherwise it simply
|
|
/// returns <c>this</c>.
|
|
/// </remarks>
|
|
/// <returns>
|
|
/// The <see cref="StyledElement"/> from which grid-attached properties
|
|
/// should be read—either the parent <see cref="ContentPresenter"/> or
|
|
/// the splitter instance.
|
|
/// </returns>
|
|
protected virtual StyledElement GetPropertiesValueSource()
|
|
{
|
|
return Parent is ContentPresenter
|
|
? Parent
|
|
: this;
|
|
}
|
|
|
|
protected override void OnPointerEntered(PointerEventArgs e)
|
|
{
|
|
base.OnPointerEntered(e);
|
|
|
|
GridResizeDirection direction = GetEffectiveResizeDirection();
|
|
|
|
switch (direction)
|
|
{
|
|
case GridResizeDirection.Columns:
|
|
Cursor = s_columnSplitterCursor;
|
|
break;
|
|
case GridResizeDirection.Rows:
|
|
Cursor = s_rowSplitterCursor;
|
|
break;
|
|
}
|
|
}
|
|
|
|
protected override void OnLostFocus(RoutedEventArgs e)
|
|
{
|
|
base.OnLostFocus(e);
|
|
|
|
if (_resizeData != null)
|
|
{
|
|
CancelResize();
|
|
}
|
|
}
|
|
|
|
protected override void OnDragStarted(VectorEventArgs e)
|
|
{
|
|
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);
|
|
}
|
|
|
|
protected override void OnDragDelta(VectorEventArgs e)
|
|
{
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
protected override void OnDragCompleted(VectorEventArgs e)
|
|
{
|
|
base.OnDragCompleted(e);
|
|
|
|
if (_resizeData != null)
|
|
{
|
|
if (_resizeData.ShowsPreview)
|
|
{
|
|
// Update the grid.
|
|
MoveSplitter(_resizeData.Adorner!.OffsetX, _resizeData.Adorner.OffsetY);
|
|
RemovePreviewAdorner();
|
|
}
|
|
|
|
_resizeData = null;
|
|
}
|
|
}
|
|
|
|
protected override void OnKeyDown(KeyEventArgs e)
|
|
{
|
|
var usingXyNavigation = this.IsAllowedXYNavigationMode(e.KeyDeviceType);
|
|
var allowArrowKeys = _isFocusEngaged || !usingXyNavigation;
|
|
|
|
switch (e.Key)
|
|
{
|
|
case Key.Enter when usingXyNavigation:
|
|
_isFocusEngaged = !_isFocusEngaged;
|
|
e.Handled = true;
|
|
break;
|
|
case Key.Escape:
|
|
_isFocusEngaged = false;
|
|
if (_resizeData != null)
|
|
{
|
|
CancelResize();
|
|
e.Handled = true;
|
|
}
|
|
|
|
break;
|
|
|
|
case Key.Left when allowArrowKeys:
|
|
e.Handled = KeyboardMoveSplitter(-KeyboardIncrement, 0);
|
|
break;
|
|
case Key.Right when allowArrowKeys:
|
|
e.Handled = KeyboardMoveSplitter(KeyboardIncrement, 0);
|
|
break;
|
|
case Key.Up when allowArrowKeys:
|
|
e.Handled = KeyboardMoveSplitter(0, -KeyboardIncrement);
|
|
break;
|
|
case Key.Down when allowArrowKeys:
|
|
e.Handled = KeyboardMoveSplitter(0, KeyboardIncrement);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Cancels the resize operation.
|
|
/// </summary>
|
|
private void CancelResize()
|
|
{
|
|
// Restore original column/row lengths.
|
|
if (_resizeData!.ShowsPreview)
|
|
{
|
|
RemovePreviewAdorner();
|
|
}
|
|
else // Reset the columns/rows lengths to the saved values.
|
|
{
|
|
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;
|
|
}
|
|
|
|
/// <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)
|
|
{
|
|
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 static 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)
|
|
{
|
|
definition1Min = Math.Max(definition1Min, _resizeData.SplitterLength);
|
|
}
|
|
else if (_resizeData.SplitterIndex == _resizeData.Definition2Index)
|
|
{
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the length of definition1 and definition2.
|
|
/// </summary>
|
|
private void SetLengths(double definition1Pixels, double definition2Pixels)
|
|
{
|
|
// 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;
|
|
|
|
var definitionsCount = definitions.Count;
|
|
|
|
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)
|
|
{
|
|
SetDefinitionLength(_resizeData.Definition1!, new GridLength(definition1Pixels));
|
|
}
|
|
else
|
|
{
|
|
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. If layout rounding is enabled, we
|
|
// need to round to an integer physical pixel value to avoid round-ups of children that
|
|
// expand the bounds of the Grid. In practice this only happens in high dpi because
|
|
// horizontal/vertical offsets here are never fractional (they correspond to mouse movement
|
|
// across logical pixels). Rounding error only creeps in when converting to a physical
|
|
// display with something other than the logical 96 dpi.
|
|
double delta = _resizeData.ResizeDirection == GridResizeDirection.Columns ? horizontalChange : verticalChange;
|
|
|
|
if (UseLayoutRounding)
|
|
{
|
|
delta = LayoutHelper.RoundLayoutValue(delta, LayoutHelper.GetLayoutScale(this));
|
|
}
|
|
|
|
DefinitionBase? definition1 = _resizeData.Definition1;
|
|
DefinitionBase? definition2 = _resizeData.Definition2;
|
|
|
|
if (definition1 != null && definition2 != null)
|
|
{
|
|
double actualLength1 = GetActualLength(definition1);
|
|
double actualLength2 = GetActualLength(definition2);
|
|
double pixelLength = 1 / _resizeData.Scaling;
|
|
double epsilon = pixelLength + LayoutHelper.LayoutEpsilon;
|
|
|
|
// When splitting, Check to see if the total pixels spanned by the definitions
|
|
// is the same as before starting resize. If not cancel the drag. We need to account for
|
|
// layout rounding here, so ignore differences of less than a device pixel to avoid problems
|
|
// that WPF has, such as https://stackoverflow.com/questions/28464843.
|
|
if (_resizeData.SplitBehavior == SplitBehavior.Split &&
|
|
!MathUtilities.AreClose(
|
|
actualLength1 + actualLength2,
|
|
_resizeData.OriginalDefinition1ActualLength + _resizeData.OriginalDefinition2ActualLength, epsilon))
|
|
{
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <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.
|
|
}
|
|
|
|
MoveSplitter(horizontalChange, verticalChange);
|
|
|
|
_resizeData = null;
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// This adorner draws the preview for the <see cref="GridSplitter"/>.
|
|
/// It also positions the adorner.
|
|
/// </summary>
|
|
private sealed class PreviewAdorner : Decorator
|
|
{
|
|
private readonly TranslateTransform _translation;
|
|
private readonly Decorator _decorator;
|
|
|
|
[System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", "AVP1012", Justification = "Private object")]
|
|
public PreviewAdorner(Control? previewControl)
|
|
{
|
|
// Add a decorator to perform translations.
|
|
_translation = new TranslateTransform();
|
|
|
|
_decorator = new Decorator
|
|
{
|
|
Child = previewControl,
|
|
RenderTransform = _translation
|
|
};
|
|
|
|
Child = _decorator;
|
|
}
|
|
|
|
/// <summary>
|
|
/// The Preview's Offset in the X direction from the GridSplitter.
|
|
/// </summary>
|
|
public double OffsetX
|
|
{
|
|
get => _translation.X;
|
|
set => _translation.X = value;
|
|
}
|
|
|
|
/// <summary>
|
|
/// The Preview's Offset in the Y direction from the GridSplitter.
|
|
/// </summary>
|
|
public double OffsetY
|
|
{
|
|
get => _translation.Y;
|
|
set => _translation.Y = value;
|
|
}
|
|
|
|
protected override Size ArrangeOverride(Size finalSize)
|
|
{
|
|
// 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);
|
|
}
|
|
}
|
|
|
|
/// <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;
|
|
|
|
// The current layout scaling factor.
|
|
public double Scaling;
|
|
}
|
|
}
|
|
|
|
/// <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
|
|
}
|
|
}
|
|
|