// (c) Copyright Microsoft Corporation. // This source is subject to the Microsoft Public License (Ms-PL). // Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. using Avalonia.Controls.Primitives; using Avalonia.Data; using Avalonia.Input; using Avalonia.Collections; using Avalonia.Media; using System.ComponentModel; using System.Diagnostics; using Avalonia.Utilities; using System; using Avalonia.Controls.Utils; namespace Avalonia.Controls { /// /// Represents an individual column header. /// public class DataGridColumnHeader : ContentControl { private enum DragMode { None = 0, MouseDown = 1, Drag = 2, Resize = 3, Reorder = 4 } private const int DATAGRIDCOLUMNHEADER_resizeRegionWidth = 5; private const double DATAGRIDCOLUMNHEADER_separatorThickness = 1; private bool _areHandlersSuspended; private static DragMode _dragMode; private static Point? _lastMousePositionHeaders; private static Cursor _originalCursor; private static double _originalHorizontalOffset; private static double _originalWidth; private bool _desiredSeparatorVisibility; private static Point? _dragStart; private static DataGridColumn _dragColumn; private static double _frozenColumnsWidth; private static Lazy _resizeCursor = new Lazy(() => new Cursor(StandardCursorType.SizeWestEast)); public static readonly StyledProperty SeparatorBrushProperty = AvaloniaProperty.Register(nameof(SeparatorBrush)); public IBrush SeparatorBrush { get { return GetValue(SeparatorBrushProperty); } set { SetValue(SeparatorBrushProperty, value); } } public static readonly StyledProperty AreSeparatorsVisibleProperty = AvaloniaProperty.Register( nameof(AreSeparatorsVisible), defaultValue: true); public bool AreSeparatorsVisible { get { return GetValue(AreSeparatorsVisibleProperty); } set { SetValue(AreSeparatorsVisibleProperty, value); } } static DataGridColumnHeader() { AreSeparatorsVisibleProperty.Changed.AddClassHandler((x,e) => x.OnAreSeparatorsVisibleChanged(e)); } /// /// Initializes a new instance of the class. /// //TODO Implement public DataGridColumnHeader() { PointerPressed += DataGridColumnHeader_PointerPressed; PointerReleased += DataGridColumnHeader_PointerReleased; PointerMoved += DataGridColumnHeader_PointerMove; PointerEnter += DataGridColumnHeader_PointerEnter; PointerLeave += DataGridColumnHeader_PointerLeave; } private void OnAreSeparatorsVisibleChanged(AvaloniaPropertyChangedEventArgs e) { if (!_areHandlersSuspended) { _desiredSeparatorVisibility = (bool)e.NewValue; if (OwningGrid != null) { UpdateSeparatorVisibility(OwningGrid.ColumnsInternal.LastVisibleColumn); } else { UpdateSeparatorVisibility(null); } } } internal DataGridColumn OwningColumn { get; set; } internal DataGrid OwningGrid => OwningColumn?.OwningGrid; internal int ColumnIndex { get { if (OwningColumn == null) { return -1; } return OwningColumn.Index; } } internal ListSortDirection? CurrentSortingState { get; private set; } private bool IsMouseOver { get; set; } private bool IsPressed { get; set; } private void SetValueNoCallback(AvaloniaProperty property, T value, BindingPriority priority = BindingPriority.LocalValue) { _areHandlersSuspended = true; try { SetValue(property, value, priority); } finally { _areHandlersSuspended = false; } } //TODO Implement internal void ApplyState() { CurrentSortingState = null; if (OwningGrid != null && OwningGrid.DataConnection != null && OwningGrid.DataConnection.AllowSort) { var sort = OwningColumn.GetSortDescription(); if(sort != null) { CurrentSortingState = sort.Descending ? ListSortDirection.Descending : ListSortDirection.Ascending; } } PseudoClasses.Set(":sortascending", CurrentSortingState.HasValue && CurrentSortingState.Value == ListSortDirection.Ascending); PseudoClasses.Set(":sortdescending", CurrentSortingState.HasValue && CurrentSortingState.Value == ListSortDirection.Descending); } internal void UpdateSeparatorVisibility(DataGridColumn lastVisibleColumn) { bool newVisibility = _desiredSeparatorVisibility; // Collapse separator for the last column if there is no filler column if (OwningColumn != null && OwningGrid != null && _desiredSeparatorVisibility && OwningColumn == lastVisibleColumn && !OwningGrid.ColumnsInternal.FillerColumn.IsActive) { newVisibility = false; } // Update the public property if it has changed if (AreSeparatorsVisible != newVisibility) { SetValueNoCallback(AreSeparatorsVisibleProperty, newVisibility); } } internal void OnMouseLeftButtonUp_Click(InputModifiers inputModifiers, ref bool handled) { // completed a click without dragging, so we're sorting InvokeProcessSort(inputModifiers); handled = true; } internal void InvokeProcessSort(InputModifiers inputModifiers) { Debug.Assert(OwningGrid != null); if (OwningGrid.WaitForLostFocus(() => InvokeProcessSort(inputModifiers))) { return; } if (OwningGrid.CommitEdit(DataGridEditingUnit.Row, exitEditingMode: true)) { Avalonia.Threading.Dispatcher.UIThread.Post(() => ProcessSort(inputModifiers)); } } //TODO GroupSorting internal void ProcessSort(InputModifiers inputModifiers) { // if we can sort: // - DataConnection.AllowSort is true, and // - AllowUserToSortColumns and CanSort are true, and // - OwningColumn is bound, and // - SortDescriptionsCollection exists, and // - the column's data type is comparable // then try to sort if (OwningColumn != null && OwningGrid != null && OwningGrid.EditingRow == null && OwningColumn != OwningGrid.ColumnsInternal.FillerColumn && OwningGrid.DataConnection.AllowSort && OwningGrid.CanUserSortColumns && OwningColumn.CanUserSort && OwningGrid.DataConnection.SortDescriptions != null) { DataGrid owningGrid = OwningGrid; DataGridSortDescription newSort; KeyboardHelper.GetMetaKeyState(inputModifiers, out bool ctrl, out bool shift); DataGridSortDescription sort = OwningColumn.GetSortDescription(); IDataGridCollectionView collectionView = owningGrid.DataConnection.CollectionView; Debug.Assert(collectionView != null); using (collectionView.DeferRefresh()) { // if shift is held down, we multi-sort, therefore if it isn't, we'll clear the sorts beforehand if (!shift || owningGrid.DataConnection.SortDescriptions.Count == 0) { owningGrid.DataConnection.SortDescriptions.Clear(); } if (sort != null) { newSort = sort.SwitchSortDirection(); // changing direction should not affect sort order, so we replace this column's // sort description instead of just adding it to the end of the collection int oldIndex = owningGrid.DataConnection.SortDescriptions.IndexOf(sort); if (oldIndex >= 0) { owningGrid.DataConnection.SortDescriptions.Remove(sort); owningGrid.DataConnection.SortDescriptions.Insert(oldIndex, newSort); } else { owningGrid.DataConnection.SortDescriptions.Add(newSort); } } else { string propertyName = OwningColumn.GetSortPropertyName(); // no-opt if we couldn't find a property to sort on if (string.IsNullOrEmpty(propertyName)) { return; } newSort = DataGridSortDescription.FromPath(propertyName, culture: collectionView.Culture); owningGrid.DataConnection.SortDescriptions.Add(newSort); } } } } private bool CanReorderColumn(DataGridColumn column) { return OwningGrid.CanUserReorderColumns && !(column is DataGridFillerColumn) && (column.CanUserReorderInternal.HasValue && column.CanUserReorderInternal.Value || !column.CanUserReorderInternal.HasValue); } /// /// Determines whether a column can be resized by dragging the border of its header. If star sizing /// is being used, there are special conditions that can prevent a column from being resized: /// 1. The column is the last visible column. /// 2. All columns are constrained by either their maximum or minimum values. /// /// Column to check. /// Whether or not the column can be resized by dragging its header. private static bool CanResizeColumn(DataGridColumn column) { if (column.OwningGrid != null && column.OwningGrid.ColumnsInternal != null && column.OwningGrid.UsesStarSizing && (column.OwningGrid.ColumnsInternal.LastVisibleColumn == column || !DoubleUtil.AreClose(column.OwningGrid.ColumnsInternal.VisibleEdgedColumnsWidth, column.OwningGrid.CellsWidth))) { return false; } return column.ActualCanUserResize; } private static bool TrySetResizeColumn(DataGridColumn column) { // If datagrid.CanUserResizeColumns == false, then the column can still override it if (CanResizeColumn(column)) { _dragColumn = column; _dragMode = DragMode.Resize; return true; } return false; } //TODO DragDrop internal void OnMouseLeftButtonDown(ref bool handled, PointerEventArgs args, Point mousePosition) { IsPressed = true; if (OwningGrid != null && OwningGrid.ColumnHeaders != null) { args.Device.Capture(this); _dragMode = DragMode.MouseDown; _frozenColumnsWidth = OwningGrid.ColumnsInternal.GetVisibleFrozenEdgedColumnsWidth(); _lastMousePositionHeaders = this.Translate(OwningGrid.ColumnHeaders, mousePosition); double distanceFromLeft = mousePosition.X; double distanceFromRight = Bounds.Width - distanceFromLeft; DataGridColumn currentColumn = OwningColumn; DataGridColumn previousColumn = null; if (!(OwningColumn is DataGridFillerColumn)) { previousColumn = OwningGrid.ColumnsInternal.GetPreviousVisibleNonFillerColumn(currentColumn); } if (_dragMode == DragMode.MouseDown && _dragColumn == null && (distanceFromRight <= DATAGRIDCOLUMNHEADER_resizeRegionWidth)) { handled = TrySetResizeColumn(currentColumn); } else if (_dragMode == DragMode.MouseDown && _dragColumn == null && distanceFromLeft <= DATAGRIDCOLUMNHEADER_resizeRegionWidth && previousColumn != null) { handled = TrySetResizeColumn(previousColumn); } if (_dragMode == DragMode.Resize && _dragColumn != null) { _dragStart = _lastMousePositionHeaders; _originalWidth = _dragColumn.ActualWidth; _originalHorizontalOffset = OwningGrid.HorizontalOffset; handled = true; } } } //TODO DragEvents //TODO MouseCapture internal void OnMouseLeftButtonUp(ref bool handled, PointerEventArgs args, Point mousePosition, Point mousePositionHeaders) { IsPressed = false; if (OwningGrid != null && OwningGrid.ColumnHeaders != null) { if (_dragMode == DragMode.MouseDown) { OnMouseLeftButtonUp_Click(args.InputModifiers, ref handled); } else if (_dragMode == DragMode.Reorder) { // Find header we're hovering over int targetIndex = GetReorderingTargetDisplayIndex(mousePositionHeaders); if (((!OwningColumn.IsFrozen && targetIndex >= OwningGrid.FrozenColumnCount) || (OwningColumn.IsFrozen && targetIndex < OwningGrid.FrozenColumnCount))) { OwningColumn.DisplayIndex = targetIndex; DataGridColumnEventArgs ea = new DataGridColumnEventArgs(OwningColumn); OwningGrid.OnColumnReordered(ea); } } SetDragCursor(mousePosition); // Variables that track drag mode states get reset in DataGridColumnHeader_LostMouseCapture args.Device.Capture(null); OnLostMouseCapture(); _dragMode = DragMode.None; handled = true; } } //TODO DragEvents internal void OnMouseMove(ref bool handled, Point mousePosition, Point mousePositionHeaders) { if (handled || OwningGrid == null || OwningGrid.ColumnHeaders == null) { return; } Debug.Assert(OwningGrid.Parent is InputElement); double distanceFromLeft = mousePosition.X; double distanceFromRight = Bounds.Width - distanceFromLeft; OnMouseMove_Resize(ref handled, mousePositionHeaders); OnMouseMove_Reorder(ref handled, mousePosition, mousePositionHeaders, distanceFromLeft, distanceFromRight); // if we still haven't done anything about moving the mouse while // the button is down, we remember that we're dragging, but we don't // claim to have actually handled the event if (_dragMode == DragMode.MouseDown) { _dragMode = DragMode.Drag; } _lastMousePositionHeaders = mousePositionHeaders; SetDragCursor(mousePosition); } private void DataGridColumnHeader_PointerEnter(object sender, PointerEventArgs e) { if (!IsEnabled) { return; } Point mousePosition = e.GetPosition(this); OnMouseEnter(mousePosition); ApplyState(); } private void DataGridColumnHeader_PointerLeave(object sender, PointerEventArgs e) { if (!IsEnabled) { return; } OnMouseLeave(); ApplyState(); } private void DataGridColumnHeader_PointerPressed(object sender, PointerPressedEventArgs e) { if (OwningColumn == null || e.Handled || !IsEnabled || e.MouseButton != MouseButton.Left) { return; } Point mousePosition = e.GetPosition(this); bool handled = e.Handled; OnMouseLeftButtonDown(ref handled, e, mousePosition); e.Handled = handled; ApplyState(); } private void DataGridColumnHeader_PointerReleased(object sender, PointerReleasedEventArgs e) { if (OwningColumn == null || e.Handled || !IsEnabled || e.InitialPressMouseButton != MouseButton.Left) { return; } Point mousePosition = e.GetPosition(this); Point mousePositionHeaders = e.GetPosition(OwningGrid.ColumnHeaders); bool handled = e.Handled; OnMouseLeftButtonUp(ref handled, e, mousePosition, mousePositionHeaders); e.Handled = handled; ApplyState(); } private void DataGridColumnHeader_PointerMove(object sender, PointerEventArgs e) { if (OwningGrid == null || !IsEnabled) { return; } Point mousePosition = e.GetPosition(this); Point mousePositionHeaders = e.GetPosition(OwningGrid.ColumnHeaders); bool handled = false; OnMouseMove(ref handled, mousePosition, mousePositionHeaders); } /// /// Returns the column against whose top-left the reordering caret should be positioned /// /// Mouse position within the ColumnHeadersPresenter /// Whether or not to scroll horizontally when a column is dragged out of bounds /// If scroll is true, returns the horizontal amount that was scrolled /// private DataGridColumn GetReorderingTargetColumn(Point mousePositionHeaders, bool scroll, out double scrollAmount) { scrollAmount = 0; double leftEdge = OwningGrid.ColumnsInternal.RowGroupSpacerColumn.IsRepresented ? OwningGrid.ColumnsInternal.RowGroupSpacerColumn.ActualWidth : 0; double rightEdge = OwningGrid.CellsWidth; if (OwningColumn.IsFrozen) { rightEdge = Math.Min(rightEdge, _frozenColumnsWidth); } else if (OwningGrid.FrozenColumnCount > 0) { leftEdge = _frozenColumnsWidth; } if (mousePositionHeaders.X < leftEdge) { if (scroll && OwningGrid.HorizontalScrollBar != null && OwningGrid.HorizontalScrollBar.IsVisible && OwningGrid.HorizontalScrollBar.Value > 0) { double newVal = mousePositionHeaders.X - leftEdge; scrollAmount = Math.Min(newVal, OwningGrid.HorizontalScrollBar.Value); OwningGrid.UpdateHorizontalOffset(scrollAmount + OwningGrid.HorizontalScrollBar.Value); } mousePositionHeaders = mousePositionHeaders.WithX(leftEdge); } else if (mousePositionHeaders.X >= rightEdge) { if (scroll && OwningGrid.HorizontalScrollBar != null && OwningGrid.HorizontalScrollBar.IsVisible && OwningGrid.HorizontalScrollBar.Value < OwningGrid.HorizontalScrollBar.Maximum) { double newVal = mousePositionHeaders.X - rightEdge; scrollAmount = Math.Min(newVal, OwningGrid.HorizontalScrollBar.Maximum - OwningGrid.HorizontalScrollBar.Value); OwningGrid.UpdateHorizontalOffset(scrollAmount + OwningGrid.HorizontalScrollBar.Value); } mousePositionHeaders = mousePositionHeaders.WithX(rightEdge - 1); } foreach (DataGridColumn column in OwningGrid.ColumnsInternal.GetDisplayedColumns()) { Point mousePosition = OwningGrid.ColumnHeaders.Translate(column.HeaderCell, mousePositionHeaders); double columnMiddle = column.HeaderCell.Bounds.Width / 2; if (mousePosition.X >= 0 && mousePosition.X <= columnMiddle) { return column; } else if (mousePosition.X > columnMiddle && mousePosition.X < column.HeaderCell.Bounds.Width) { return OwningGrid.ColumnsInternal.GetNextVisibleColumn(column); } } return null; } /// /// Returns the display index to set the column to /// /// Mouse position relative to the column headers presenter /// private int GetReorderingTargetDisplayIndex(Point mousePositionHeaders) { DataGridColumn targetColumn = GetReorderingTargetColumn(mousePositionHeaders, false /*scroll*/, out double scrollAmount); if (targetColumn != null) { return targetColumn.DisplayIndex > OwningColumn.DisplayIndex ? targetColumn.DisplayIndex - 1 : targetColumn.DisplayIndex; } else { return OwningGrid.Columns.Count - 1; } } /// /// Returns true if the mouse is /// - to the left of the element, or within the left half of the element /// and /// - within the vertical range of the element, or ignoreVertical == true /// /// /// /// /// private bool IsReorderTargeted(Point mousePosition, Control element, bool ignoreVertical) { Point position = this.Translate(element, mousePosition); return (position.X < 0 || (position.X >= 0 && position.X <= element.Bounds.Width / 2)) && (ignoreVertical || (position.Y >= 0 && position.Y <= element.Bounds.Height)); } /// /// Resets the static DataGridColumnHeader properties when a header loses mouse capture /// private void OnLostMouseCapture() { // When we stop interacting with the column headers, we need to reset the drag mode // and close any popups if they are open. if (_dragColumn != null && _dragColumn.HeaderCell != null) { _dragColumn.HeaderCell.Cursor = _originalCursor; } _dragMode = DragMode.None; _dragColumn = null; _dragStart = null; _lastMousePositionHeaders = null; if (OwningGrid != null && OwningGrid.ColumnHeaders != null) { OwningGrid.ColumnHeaders.DragColumn = null; OwningGrid.ColumnHeaders.DragIndicator = null; OwningGrid.ColumnHeaders.DropLocationIndicator = null; } } /// /// Sets up the DataGridColumnHeader for the MouseEnter event /// /// mouse position relative to the DataGridColumnHeader private void OnMouseEnter(Point mousePosition) { IsMouseOver = true; SetDragCursor(mousePosition); } /// /// Sets up the DataGridColumnHeader for the MouseLeave event /// private void OnMouseLeave() { IsMouseOver = false; } //TODO Styles DragIndicator private void OnMouseMove_BeginReorder(Point mousePosition) { DataGridColumnHeader dragIndicator = new DataGridColumnHeader { OwningColumn = OwningColumn, IsEnabled = false, Content = Content, ContentTemplate = ContentTemplate }; dragIndicator.PseudoClasses.Add(":dragIndicator"); IControl dropLocationIndicator = OwningGrid.DropLocationIndicatorTemplate?.Build(); // If the user didn't style the dropLocationIndicator's Height, default to the column header's height if (dropLocationIndicator != null && double.IsNaN(dropLocationIndicator.Height) && dropLocationIndicator is Control element) { element.Height = Bounds.Height; } // pass the caret's data template to the user for modification DataGridColumnReorderingEventArgs columnReorderingEventArgs = new DataGridColumnReorderingEventArgs(OwningColumn) { DropLocationIndicator = dropLocationIndicator, DragIndicator = dragIndicator }; OwningGrid.OnColumnReordering(columnReorderingEventArgs); if (columnReorderingEventArgs.Cancel) { return; } // The user didn't cancel, so prepare for the reorder _dragColumn = OwningColumn; _dragMode = DragMode.Reorder; _dragStart = mousePosition; // Display the reordering thumb OwningGrid.ColumnHeaders.DragColumn = OwningColumn; OwningGrid.ColumnHeaders.DragIndicator = columnReorderingEventArgs.DragIndicator; OwningGrid.ColumnHeaders.DropLocationIndicator = columnReorderingEventArgs.DropLocationIndicator; // If the user didn't style the dragIndicator's Width, default it to the column header's width if (double.IsNaN(dragIndicator.Width)) { dragIndicator.Width = Bounds.Width; } } //TODO DragEvents private void OnMouseMove_Reorder(ref bool handled, Point mousePosition, Point mousePositionHeaders, double distanceFromLeft, double distanceFromRight) { if (handled) { return; } //handle entry into reorder mode if (_dragMode == DragMode.MouseDown && _dragColumn == null && (distanceFromRight > DATAGRIDCOLUMNHEADER_resizeRegionWidth && distanceFromLeft > DATAGRIDCOLUMNHEADER_resizeRegionWidth)) { handled = CanReorderColumn(OwningColumn); if (handled) { OnMouseMove_BeginReorder(mousePosition); } } //handle reorder mode (eg, positioning of the popup) if (_dragMode == DragMode.Reorder && OwningGrid.ColumnHeaders.DragIndicator != null) { // Find header we're hovering over DataGridColumn targetColumn = GetReorderingTargetColumn(mousePositionHeaders, !OwningColumn.IsFrozen /*scroll*/, out double scrollAmount); OwningGrid.ColumnHeaders.DragIndicatorOffset = mousePosition.X - _dragStart.Value.X + scrollAmount; OwningGrid.ColumnHeaders.InvalidateArrange(); if (OwningGrid.ColumnHeaders.DropLocationIndicator != null) { Point targetPosition = new Point(0, 0); if (targetColumn == null || targetColumn == OwningGrid.ColumnsInternal.FillerColumn || targetColumn.IsFrozen != OwningColumn.IsFrozen) { targetColumn = OwningGrid.ColumnsInternal.GetLastColumn( isVisible: true, isFrozen: OwningColumn.IsFrozen, isReadOnly: null); targetPosition = targetColumn.HeaderCell.Translate(OwningGrid.ColumnHeaders, targetPosition); targetPosition = targetPosition.WithX(targetPosition.X + targetColumn.ActualWidth); } else { targetPosition = targetColumn.HeaderCell.Translate(OwningGrid.ColumnHeaders, targetPosition); } OwningGrid.ColumnHeaders.DropLocationIndicatorOffset = targetPosition.X - scrollAmount; } handled = true; } } private void OnMouseMove_Resize(ref bool handled, Point mousePositionHeaders) { if (handled) { return; } if (_dragMode == DragMode.Resize && _dragColumn != null && _dragStart.HasValue) { // resize column double mouseDelta = mousePositionHeaders.X - _dragStart.Value.X; double desiredWidth = _originalWidth + mouseDelta; desiredWidth = Math.Max(_dragColumn.ActualMinWidth, Math.Min(_dragColumn.ActualMaxWidth, desiredWidth)); _dragColumn.Resize(_dragColumn.Width.Value, _dragColumn.Width.UnitType, _dragColumn.Width.DesiredValue, desiredWidth, true); OwningGrid.UpdateHorizontalOffset(_originalHorizontalOffset); handled = true; } } private void SetDragCursor(Point mousePosition) { if (_dragMode != DragMode.None || OwningGrid == null || OwningColumn == null) { return; } // set mouse if we can resize column double distanceFromLeft = mousePosition.X; double distanceFromRight = Bounds.Width - distanceFromLeft; DataGridColumn currentColumn = OwningColumn; DataGridColumn previousColumn = null; if (!(OwningColumn is DataGridFillerColumn)) { previousColumn = OwningGrid.ColumnsInternal.GetPreviousVisibleNonFillerColumn(currentColumn); } if ((distanceFromRight <= DATAGRIDCOLUMNHEADER_resizeRegionWidth && currentColumn != null && CanResizeColumn(currentColumn)) || (distanceFromLeft <= DATAGRIDCOLUMNHEADER_resizeRegionWidth && previousColumn != null && CanResizeColumn(previousColumn))) { var resizeCursor = _resizeCursor.Value; if (Cursor != resizeCursor) { _originalCursor = Cursor; Cursor = resizeCursor; } } else { Cursor = _originalCursor; } } } }