diff --git a/labs/SizerBase/src/CommunityToolkit.Labs.WinUI.SizerBase.csproj b/labs/SizerBase/src/CommunityToolkit.Labs.WinUI.SizerBase.csproj index b2e21b3ea..c7f038663 100644 --- a/labs/SizerBase/src/CommunityToolkit.Labs.WinUI.SizerBase.csproj +++ b/labs/SizerBase/src/CommunityToolkit.Labs.WinUI.SizerBase.csproj @@ -12,4 +12,8 @@ 0.0.1 + + + + diff --git a/labs/SizerBase/src/ContentSizer/ContentSizer.Events.cs b/labs/SizerBase/src/ContentSizer/ContentSizer.Events.cs new file mode 100644 index 000000000..00b7ec796 --- /dev/null +++ b/labs/SizerBase/src/ContentSizer/ContentSizer.Events.cs @@ -0,0 +1,82 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.UI; + +#if !WINAPPSDK +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +#else +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +#endif + +namespace CommunityToolkit.Labs.WinUI; + +// Events for ContentSizer. +public partial class ContentSizer +{ + /// + protected override void OnLoaded(RoutedEventArgs e) + { + if (TargetControl == null) + { + TargetControl = this.FindAscendant(); + } + } + + private double _currentSize; + + /// + protected override void OnDragStarting() + { + if (TargetControl != null) + { + _currentSize = + Orientation == Orientation.Vertical ? + TargetControl.ActualWidth : + TargetControl.ActualHeight; + } + } + + /// + protected override bool OnDragHorizontal(double horizontalChange) + { + if (TargetControl == null) + { + return true; + } + + horizontalChange = IsDragInverted ? -horizontalChange : horizontalChange; + + if (!IsValidWidth(TargetControl, _currentSize + horizontalChange, ActualWidth)) + { + return false; + } + + TargetControl.Width = _currentSize + horizontalChange; + + return true; + } + + /// + protected override bool OnDragVertical(double verticalChange) + { + if (TargetControl == null) + { + return false; + } + + verticalChange = IsDragInverted ? -verticalChange : verticalChange; + + if (!IsValidHeight(TargetControl, _currentSize + verticalChange, ActualHeight)) + { + return false; + } + + TargetControl.Height = _currentSize + verticalChange; + + return true; + } +} diff --git a/labs/SizerBase/src/ContentSizer/ContentSizer.Properties.cs b/labs/SizerBase/src/ContentSizer/ContentSizer.Properties.cs new file mode 100644 index 000000000..d24d88ddb --- /dev/null +++ b/labs/SizerBase/src/ContentSizer/ContentSizer.Properties.cs @@ -0,0 +1,71 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if !WINAPPSDK +using System; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +#else +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +#endif + +namespace CommunityToolkit.Labs.WinUI; + +// Properties for ContentSizer. +public partial class ContentSizer +{ + /// + /// Gets or sets a value indicating whether the control is resizing in the opposite direction. + /// + public bool IsDragInverted + { + get { return (bool)GetValue(IsDragInvertedProperty); } + set { SetValue(IsDragInvertedProperty, value); } + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty IsDragInvertedProperty = + DependencyProperty.Register(nameof(IsDragInverted), typeof(bool), typeof(ContentSizer), new PropertyMetadata(false)); + + /// + /// Gets or sets the control that the is resizing. Be default, this will be the visual ancestor of the . + /// + public FrameworkElement? TargetControl + { + get { return (FrameworkElement?)GetValue(TargetControlProperty); } + set { SetValue(TargetControlProperty, value); } + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty TargetControlProperty = + DependencyProperty.Register(nameof(TargetControl), typeof(FrameworkElement), typeof(ContentSizer), new PropertyMetadata(null, OnTargetControlChanged)); + + private static void OnTargetControlChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + // TODO: Should we do this after the TargetControl is Loaded? (And use ActualWidth?) + // Or should we just do it in the manipulation event if Width is null? + + // Check if our width can be manipulated + if (d is SizerBase splitterBase && e.NewValue is FrameworkElement element) + { + // TODO: For Auto ResizeDirection we might want to do detection logic (TBD) here first? + if (splitterBase.Orientation != Orientation.Horizontal && double.IsNaN(element.Width)) + { + // We need to set the Width or Height somewhere, + // as if it's NaN we won't be able to manipulate it. + element.Width = element.DesiredSize.Width; + } + + if (splitterBase.Orientation != Orientation.Vertical && double.IsNaN(element.Height)) + { + element.Height = element.DesiredSize.Height; + } + } + } +} diff --git a/labs/SizerBase/src/ContentSizer/ContentSizer.cs b/labs/SizerBase/src/ContentSizer/ContentSizer.cs new file mode 100644 index 000000000..a69481856 --- /dev/null +++ b/labs/SizerBase/src/ContentSizer/ContentSizer.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if !WINAPPSDK +using Windows.UI.Xaml.Controls; +#else +using Microsoft.UI.Xaml.Controls; +#endif + +namespace CommunityToolkit.Labs.WinUI; + +/// +/// The is a control which can be used to resize any element, usually its parent. If you are using a , use instead. +/// +public partial class ContentSizer : SizerBase +{ +} diff --git a/labs/SizerBase/src/Dependencies/DependencyObjectExtensions.cs b/labs/SizerBase/src/Dependencies/DependencyObjectExtensions.cs new file mode 100644 index 000000000..33d62b07e --- /dev/null +++ b/labs/SizerBase/src/Dependencies/DependencyObjectExtensions.cs @@ -0,0 +1,235 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; + +#if !WINAPPSDK +using Windows.UI.Xaml; +using Windows.UI.Xaml.Media; +#else +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Media; +#endif + +namespace CommunityToolkit.WinUI.UI; + +//// IMPORTANT NOTE: This is the old 6.1.1 version of the extensions as I had issues with TPredicate with the new ones here for some reason and just wanted to get this working for now. + +/// +/// Defines a collection of extensions methods for UI. +/// +public static class VisualTree +{ + /// + /// Find descendant control using its name. + /// + /// Parent element. + /// Name of the control to find + /// Descendant control or null if not found. + public static FrameworkElement? FindDescendantByName(this DependencyObject element, string name) + { + if (element == null || string.IsNullOrWhiteSpace(name)) + { + return null; + } + + if (name.Equals((element as FrameworkElement)?.Name, StringComparison.OrdinalIgnoreCase)) + { + return element as FrameworkElement; + } + + var childCount = VisualTreeHelper.GetChildrenCount(element); + for (int i = 0; i < childCount; i++) + { + var result = VisualTreeHelper.GetChild(element, i).FindDescendantByName(name); + if (result != null) + { + return result; + } + } + + return null; + } + + /// + /// Find first descendant control of a specified type. + /// + /// Type to search for. + /// Parent element. + /// Descendant control or null if not found. + public static T? FindDescendant(this DependencyObject element) + where T : DependencyObject + { + T? retValue = default(T); + var childrenCount = VisualTreeHelper.GetChildrenCount(element); + + for (var i = 0; i < childrenCount; i++) + { + var child = VisualTreeHelper.GetChild(element, i); + if (child is T type) + { + retValue = type; + break; + } + + retValue = FindDescendant(child); + + if (retValue != null) + { + break; + } + } + + return retValue; + } + + /// + /// Find first descendant control of a specified type. + /// + /// Parent element. + /// Type of descendant. + /// Descendant control or null if not found. + public static object? FindDescendant(this DependencyObject element, Type type) + { + object? retValue = null; + var childrenCount = VisualTreeHelper.GetChildrenCount(element); + + for (var i = 0; i < childrenCount; i++) + { + var child = VisualTreeHelper.GetChild(element, i); + if (child.GetType() == type) + { + retValue = child; + break; + } + + retValue = FindDescendant(child, type); + + if (retValue != null) + { + break; + } + } + + return retValue; + } + + /// + /// Find all descendant controls of the specified type. + /// + /// Type to search for. + /// Parent element. + /// Descendant controls or empty if not found. + public static IEnumerable FindDescendants(this DependencyObject element) + where T : DependencyObject + { + var childrenCount = VisualTreeHelper.GetChildrenCount(element); + + for (var i = 0; i < childrenCount; i++) + { + var child = VisualTreeHelper.GetChild(element, i); + if (child is T type) + { + yield return type; + } + + foreach (T childofChild in child.FindDescendants()) + { + yield return childofChild; + } + } + } + + /// + /// Find visual ascendant control using its name. + /// + /// Parent element. + /// Name of the control to find + /// Descendant control or null if not found. + public static FrameworkElement? FindAscendantByName(this DependencyObject element, string name) + { + if (element == null || string.IsNullOrWhiteSpace(name)) + { + return null; + } + + var parent = VisualTreeHelper.GetParent(element); + + if (parent == null) + { + return null; + } + + if (name.Equals((parent as FrameworkElement)?.Name, StringComparison.OrdinalIgnoreCase)) + { + return parent as FrameworkElement; + } + + return parent.FindAscendantByName(name); + } + + /// + /// Find first visual ascendant control of a specified type. + /// + /// Type to search for. + /// Child element. + /// Ascendant control or null if not found. + public static T? FindAscendant(this DependencyObject element) + where T : DependencyObject + { + var parent = VisualTreeHelper.GetParent(element); + + if (parent == null) + { + return default(T); + } + + if (parent is T rtn) + { + return rtn; + } + + return parent.FindAscendant(); + } + + /// + /// Find first visual ascendant control of a specified type. + /// + /// Child element. + /// Type of ascendant to look for. + /// Ascendant control or null if not found. + public static object? FindAscendant(this DependencyObject element, Type type) + { + var parent = VisualTreeHelper.GetParent(element); + + if (parent == null) + { + return null; + } + + if (parent.GetType() == type) + { + return parent; + } + + return parent.FindAscendant(type); + } + + /// + /// Find all visual ascendants for the element. + /// + /// Child element. + /// A collection of parent elements or null if none found. + public static IEnumerable FindAscendants(this DependencyObject element) + { + var parent = VisualTreeHelper.GetParent(element); + + while (parent != null) + { + yield return parent; + parent = VisualTreeHelper.GetParent(parent); + } + } +} diff --git a/labs/SizerBase/src/GridSplitter/GridSplitter.Data.cs b/labs/SizerBase/src/GridSplitter/GridSplitter.Data.cs new file mode 100644 index 000000000..65c48ca80 --- /dev/null +++ b/labs/SizerBase/src/GridSplitter/GridSplitter.Data.cs @@ -0,0 +1,56 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.Labs.WinUI; + +public partial class GridSplitter +{ + /// + /// Enum to indicate whether GridSplitter resizes Columns or Rows + /// + public enum GridResizeDirection + { + /// + /// Determines whether to resize rows or columns based on its Alignment and + /// width compared to height + /// + Auto, + + /// + /// Resize columns when dragging Splitter. + /// + Columns, + + /// + /// Resize rows when dragging Splitter. + /// + Rows + } + + /// + /// Enum to indicate what Columns or Rows the GridSplitter resizes + /// + public enum GridResizeBehavior + { + /// + /// Determine which columns or rows to resize based on its Alignment. + /// + BasedOnAlignment, + + /// + /// Resize the current and next Columns or Rows. + /// + CurrentAndNext, + + /// + /// Resize the previous and current Columns or Rows. + /// + PreviousAndCurrent, + + /// + /// Resize the previous and next Columns or Rows. + /// + PreviousAndNext + } +} diff --git a/labs/SizerBase/src/GridSplitter/GridSplitter.Events.cs b/labs/SizerBase/src/GridSplitter/GridSplitter.Events.cs new file mode 100644 index 000000000..0de655a45 --- /dev/null +++ b/labs/SizerBase/src/GridSplitter/GridSplitter.Events.cs @@ -0,0 +1,177 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if !WINAPPSDK +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +#else +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +#endif + +namespace CommunityToolkit.Labs.WinUI; + +public partial class GridSplitter +{ + /// + protected override void OnLoaded(RoutedEventArgs e) + { + _resizeDirection = GetResizeDirection(); + Orientation = _resizeDirection == GridResizeDirection.Rows ? + Orientation.Horizontal : Orientation.Vertical; + _resizeBehavior = GetResizeBehavior(); + } + + private double _currentSize; + private double _siblingSize; + + /// + protected override void OnDragStarting() + { + _resizeDirection = GetResizeDirection(); + Orientation = _resizeDirection == GridResizeDirection.Rows ? + Orientation.Horizontal : Orientation.Vertical; + _resizeBehavior = GetResizeBehavior(); + + // Record starting points + if (Orientation == Orientation.Horizontal) + { + _currentSize = CurrentRow?.ActualHeight ?? -1; + _siblingSize = SiblingRow?.ActualHeight ?? -1; + } + else + { + _currentSize = CurrentColumn?.ActualWidth ?? -1; + _siblingSize = SiblingColumn?.ActualWidth ?? -1; + } + } + + /// + protected override bool OnDragVertical(double verticalChange) + { + if (CurrentRow == null || SiblingRow == null || Resizable == null) + { + return false; + } + + var currentChange = _currentSize + verticalChange; + var siblingChange = _siblingSize + (verticalChange * -1); // sibling moves opposite + + // if current row has fixed height then resize it + if (!IsStarRow(CurrentRow)) + { + // No need to check for the row Min height because it is automatically respected + return SetRowHeight(CurrentRow, currentChange, GridUnitType.Pixel); + } + + // if sibling row has fixed width then resize it + else if (!IsStarRow(SiblingRow)) + { + // Would adding to this column make the current column violate the MinWidth? + if (IsValidRowHeight(CurrentRow, currentChange) == false) + { + return false; + } + + return SetRowHeight(SiblingRow, siblingChange, GridUnitType.Pixel); + } + + // if both row haven't fixed height (auto *) + else + { + // change current row height to the new height with respecting the auto + // change sibling row height to the new height relative to current row + // respect the other star row height by setting it's height to it's actual height with stars + + // We need to validate current and sibling height to not cause any unexpected behavior + if (!IsValidRowHeight(CurrentRow, currentChange) || + !IsValidRowHeight(SiblingRow, siblingChange)) + { + return false; + } + + foreach (var rowDefinition in Resizable.RowDefinitions) + { + if (rowDefinition == CurrentRow) + { + SetRowHeight(CurrentRow, currentChange, GridUnitType.Star); + } + else if (rowDefinition == SiblingRow) + { + SetRowHeight(SiblingRow, siblingChange, GridUnitType.Star); + } + else if (IsStarRow(rowDefinition)) + { + rowDefinition.Height = new GridLength(rowDefinition.ActualHeight, GridUnitType.Star); + } + } + + return true; + } + } + + /// + protected override bool OnDragHorizontal(double horizontalChange) + { + if (CurrentColumn == null || SiblingColumn == null || Resizable == null) + { + return false; + } + + var currentChange = _currentSize + horizontalChange; + var siblingChange = _siblingSize + (horizontalChange * -1); // sibling moves opposite + + // if current column has fixed width then resize it + if (!IsStarColumn(CurrentColumn)) + { + // No need to check for the Column Min width because it is automatically respected + return SetColumnWidth(CurrentColumn, currentChange, GridUnitType.Pixel); + } + + // if sibling column has fixed width then resize it + else if (!IsStarColumn(SiblingColumn)) + { + // Would adding to this column make the current column violate the MinWidth? + if (IsValidColumnWidth(CurrentColumn, currentChange) == false) + { + return false; + } + + return SetColumnWidth(SiblingColumn, siblingChange, GridUnitType.Pixel); + } + + // if both column haven't fixed width (auto *) + else + { + // change current column width to the new width with respecting the auto + // change sibling column width to the new width relative to current column + // respect the other star column width by setting it's width to it's actual width with stars + + // We need to validate current and sibling width to not cause any unexpected behavior + if (!IsValidColumnWidth(CurrentColumn, currentChange) || + !IsValidColumnWidth(SiblingColumn, siblingChange)) + { + return false; + } + + foreach (var columnDefinition in Resizable.ColumnDefinitions) + { + if (columnDefinition == CurrentColumn) + { + SetColumnWidth(CurrentColumn, currentChange, GridUnitType.Star); + } + else if (columnDefinition == SiblingColumn) + { + SetColumnWidth(SiblingColumn, siblingChange, GridUnitType.Star); + } + else if (IsStarColumn(columnDefinition)) + { + columnDefinition.Width = new GridLength(columnDefinition.ActualWidth, GridUnitType.Star); + } + } + + return true; + } + } +} diff --git a/labs/SizerBase/src/GridSplitter/GridSplitter.Helpers.cs b/labs/SizerBase/src/GridSplitter/GridSplitter.Helpers.cs new file mode 100644 index 000000000..6af39fc53 --- /dev/null +++ b/labs/SizerBase/src/GridSplitter/GridSplitter.Helpers.cs @@ -0,0 +1,253 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if !WINAPPSDK +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +#else +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +#endif + +namespace CommunityToolkit.Labs.WinUI; + +public partial class GridSplitter +{ + private static bool IsStarColumn(ColumnDefinition definition) + { + return ((GridLength)definition.GetValue(ColumnDefinition.WidthProperty)).IsStar; + } + + private static bool IsStarRow(RowDefinition definition) + { + return ((GridLength)definition.GetValue(RowDefinition.HeightProperty)).IsStar; + } + + private bool SetColumnWidth(ColumnDefinition columnDefinition, double newWidth, GridUnitType unitType) + { + var minWidth = columnDefinition.MinWidth; + if (!double.IsNaN(minWidth) && newWidth < minWidth) + { + newWidth = minWidth; + } + + var maxWidth = columnDefinition.MaxWidth; + if (!double.IsNaN(maxWidth) && newWidth > maxWidth) + { + newWidth = maxWidth; + } + + if (newWidth > ActualWidth) + { + columnDefinition.Width = new GridLength(newWidth, unitType); + return true; + } + + return false; + } + + private bool IsValidColumnWidth(ColumnDefinition columnDefinition, double newWidth) + { + var minWidth = columnDefinition.MinWidth; + if (!double.IsNaN(minWidth) && newWidth < minWidth) + { + return false; + } + + var maxWidth = columnDefinition.MaxWidth; + if (!double.IsNaN(maxWidth) && newWidth > maxWidth) + { + return false; + } + + if (newWidth <= ActualWidth) + { + return false; + } + + return true; + } + + private bool SetRowHeight(RowDefinition rowDefinition, double newHeight, GridUnitType unitType) + { + var minHeight = rowDefinition.MinHeight; + if (!double.IsNaN(minHeight) && newHeight < minHeight) + { + newHeight = minHeight; + } + + var maxWidth = rowDefinition.MaxHeight; + if (!double.IsNaN(maxWidth) && newHeight > maxWidth) + { + newHeight = maxWidth; + } + + if (newHeight > ActualHeight) + { + rowDefinition.Height = new GridLength(newHeight, unitType); + return true; + } + + return false; + } + + private bool IsValidRowHeight(RowDefinition rowDefinition, double newHeight) + { + var minHeight = rowDefinition.MinHeight; + if (!double.IsNaN(minHeight) && newHeight < minHeight) + { + return false; + } + + var maxHeight = rowDefinition.MaxHeight; + if (!double.IsNaN(maxHeight) && newHeight > maxHeight) + { + return false; + } + + if (newHeight <= ActualHeight) + { + return false; + } + + return true; + } + + // Return the targeted Column based on the resize behavior + private int GetTargetedColumn() + { + var currentIndex = Grid.GetColumn(TargetControl); + return GetTargetIndex(currentIndex); + } + + // Return the sibling Row based on the resize behavior + private int GetTargetedRow() + { + var currentIndex = Grid.GetRow(TargetControl); + return GetTargetIndex(currentIndex); + } + + // Return the sibling Column based on the resize behavior + private int GetSiblingColumn() + { + var currentIndex = Grid.GetColumn(TargetControl); + return GetSiblingIndex(currentIndex); + } + + // Return the sibling Row based on the resize behavior + private int GetSiblingRow() + { + var currentIndex = Grid.GetRow(TargetControl); + return GetSiblingIndex(currentIndex); + } + + // Gets index based on resize behavior for first targeted row/column + private int GetTargetIndex(int currentIndex) + { + switch (_resizeBehavior) + { + case GridResizeBehavior.CurrentAndNext: + return currentIndex; + case GridResizeBehavior.PreviousAndNext: + return currentIndex - 1; + case GridResizeBehavior.PreviousAndCurrent: + return currentIndex - 1; + default: + return -1; + } + } + + // Gets index based on resize behavior for second targeted row/column + private int GetSiblingIndex(int currentIndex) + { + switch (_resizeBehavior) + { + case GridResizeBehavior.CurrentAndNext: + return currentIndex + 1; + case GridResizeBehavior.PreviousAndNext: + return currentIndex + 1; + case GridResizeBehavior.PreviousAndCurrent: + return currentIndex; + default: + return -1; + } + } + + // Checks the control alignment and Width/Height to detect the control resize direction columns/rows + private GridResizeDirection GetResizeDirection() + { + GridResizeDirection direction = ResizeDirection; + + if (direction == GridResizeDirection.Auto) + { + // When HorizontalAlignment is Left, Right or Center, resize Columns + if (HorizontalAlignment != HorizontalAlignment.Stretch) + { + direction = GridResizeDirection.Columns; + } + + // When VerticalAlignment is Top, Bottom or Center, resize Rows + else if (VerticalAlignment != VerticalAlignment.Stretch) + { + direction = GridResizeDirection.Rows; + } + + // Check Width vs Height + else if (ActualWidth <= ActualHeight) + { + direction = GridResizeDirection.Columns; + } + else + { + direction = GridResizeDirection.Rows; + } + } + + return direction; + } + + // Get the resize behavior (Which columns/rows should be resized) based on alignment and Direction + private GridResizeBehavior GetResizeBehavior() + { + GridResizeBehavior resizeBehavior = ResizeBehavior; + + if (resizeBehavior == GridResizeBehavior.BasedOnAlignment) + { + if (_resizeDirection == GridResizeDirection.Columns) + { + switch (HorizontalAlignment) + { + case HorizontalAlignment.Left: + resizeBehavior = GridResizeBehavior.PreviousAndCurrent; + break; + case HorizontalAlignment.Right: + resizeBehavior = GridResizeBehavior.CurrentAndNext; + break; + default: + resizeBehavior = GridResizeBehavior.PreviousAndNext; + break; + } + } + + // resize direction is vertical + else + { + switch (VerticalAlignment) + { + case VerticalAlignment.Top: + resizeBehavior = GridResizeBehavior.PreviousAndCurrent; + break; + case VerticalAlignment.Bottom: + resizeBehavior = GridResizeBehavior.CurrentAndNext; + break; + default: + resizeBehavior = GridResizeBehavior.PreviousAndNext; + break; + } + } + } + + return resizeBehavior; + } +} diff --git a/labs/SizerBase/src/GridSplitter/GridSplitter.Properties.cs b/labs/SizerBase/src/GridSplitter/GridSplitter.Properties.cs new file mode 100644 index 000000000..1e6c47ab0 --- /dev/null +++ b/labs/SizerBase/src/GridSplitter/GridSplitter.Properties.cs @@ -0,0 +1,86 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if !WINAPPSDK +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +#else +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +#endif + +namespace CommunityToolkit.Labs.WinUI; + +public partial class GridSplitter +{ + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty ResizeDirectionProperty + = DependencyProperty.Register( + nameof(ResizeDirection), + typeof(GridResizeDirection), + typeof(GridSplitter), + new PropertyMetadata(GridResizeDirection.Auto, OnResizeDirectionPropertyChanged)); + + private static void OnResizeDirectionPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is GridSplitter splitter && e.NewValue is GridResizeDirection direction && + direction != GridResizeDirection.Auto) + { + // Update base classes property based on specific polyfill for GridSplitter + splitter.Orientation = + direction == GridResizeDirection.Rows ? + Orientation.Horizontal : + Orientation.Vertical; + } + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty ResizeBehaviorProperty + = DependencyProperty.Register( + nameof(ResizeBehavior), + typeof(GridResizeBehavior), + typeof(GridSplitter), + new PropertyMetadata(GridResizeBehavior.BasedOnAlignment)); + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty ParentLevelProperty + = DependencyProperty.Register( + nameof(ParentLevel), + typeof(int), + typeof(GridSplitter), + new PropertyMetadata(default(int))); + + /// + /// Gets or sets whether the Splitter resizes the Columns, Rows, or Both. + /// + public GridResizeDirection ResizeDirection + { + get { return (GridResizeDirection)GetValue(ResizeDirectionProperty); } + set { SetValue(ResizeDirectionProperty, value); } + } + + /// + /// Gets or sets which Columns or Rows the Splitter resizes. + /// + public GridResizeBehavior ResizeBehavior + { + get { return (GridResizeBehavior)GetValue(ResizeBehaviorProperty); } + set { SetValue(ResizeBehaviorProperty, value); } + } + + /// + /// Gets or sets the level of the parent grid to resize + /// + public int ParentLevel + { + get { return (int)GetValue(ParentLevelProperty); } + set { SetValue(ParentLevelProperty, value); } + } +} diff --git a/labs/SizerBase/src/GridSplitter/GridSplitter.cs b/labs/SizerBase/src/GridSplitter/GridSplitter.cs new file mode 100644 index 000000000..c7aa17b16 --- /dev/null +++ b/labs/SizerBase/src/GridSplitter/GridSplitter.cs @@ -0,0 +1,153 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if !WINAPPSDK +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +#else +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +#endif + +namespace CommunityToolkit.Labs.WinUI; + +/// +/// Represents the control that redistributes space between columns or rows of a Grid control. +/// +public partial class GridSplitter : SizerBase +{ + private GridResizeDirection _resizeDirection; + private GridResizeBehavior _resizeBehavior; + + /// + /// Gets the target parent grid from level + /// + private FrameworkElement? TargetControl + { + get + { + if (ParentLevel == 0) + { + return this; + } + + // TODO: Can we just use our Visual/Logical Tree extensions for this? + var parent = Parent; + for (int i = 2; i < ParentLevel; i++) // TODO: Why is this 2? We need better documentation on ParentLevel + { + if (parent is FrameworkElement frameworkElement) + { + parent = frameworkElement.Parent; + } + else + { + break; + } + } + + return parent as FrameworkElement; + } + } + + /// + /// Gets GridSplitter Container Grid + /// + private Grid? Resizable => TargetControl?.Parent as Grid; + + /// + /// Gets the current Column definition of the parent Grid + /// + private ColumnDefinition? CurrentColumn + { + get + { + if (Resizable == null) + { + return null; + } + + var gridSplitterTargetedColumnIndex = GetTargetedColumn(); + + if ((gridSplitterTargetedColumnIndex >= 0) + && (gridSplitterTargetedColumnIndex < Resizable.ColumnDefinitions.Count)) + { + return Resizable.ColumnDefinitions[gridSplitterTargetedColumnIndex]; + } + + return null; + } + } + + /// + /// Gets the Sibling Column definition of the parent Grid + /// + private ColumnDefinition? SiblingColumn + { + get + { + if (Resizable == null) + { + return null; + } + + var gridSplitterSiblingColumnIndex = GetSiblingColumn(); + + if ((gridSplitterSiblingColumnIndex >= 0) + && (gridSplitterSiblingColumnIndex < Resizable.ColumnDefinitions.Count)) + { + return Resizable.ColumnDefinitions[gridSplitterSiblingColumnIndex]; + } + + return null; + } + } + + /// + /// Gets the current Row definition of the parent Grid + /// + private RowDefinition? CurrentRow + { + get + { + if (Resizable == null) + { + return null; + } + + var gridSplitterTargetedRowIndex = GetTargetedRow(); + + if ((gridSplitterTargetedRowIndex >= 0) + && (gridSplitterTargetedRowIndex < Resizable.RowDefinitions.Count)) + { + return Resizable.RowDefinitions[gridSplitterTargetedRowIndex]; + } + + return null; + } + } + + /// + /// Gets the Sibling Row definition of the parent Grid + /// + private RowDefinition? SiblingRow + { + get + { + if (Resizable == null) + { + return null; + } + + var gridSplitterSiblingRowIndex = GetSiblingRow(); + + if ((gridSplitterSiblingRowIndex >= 0) + && (gridSplitterSiblingRowIndex < Resizable.RowDefinitions.Count)) + { + return Resizable.RowDefinitions[gridSplitterSiblingRowIndex]; + } + + return null; + } + } +} diff --git a/labs/SizerBase/src/PropertySizer/PropertySizer.Events.cs b/labs/SizerBase/src/PropertySizer/PropertySizer.Events.cs new file mode 100644 index 000000000..c073ce851 --- /dev/null +++ b/labs/SizerBase/src/PropertySizer/PropertySizer.Events.cs @@ -0,0 +1,72 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if !WINAPPSDK +using Windows.UI.Xaml; +#else +using Microsoft.UI.Xaml; +#endif + +namespace CommunityToolkit.Labs.WinUI; + +// Events for PropertySizer. +public partial class PropertySizer +{ + private double _currentSize; + + /// + protected override void OnDragStarting() + { + // We grab the current size of the bound value when we start a drag + // and we manipulate from that set point. + if (ReadLocalValue(BindingProperty) != DependencyProperty.UnsetValue) + { + _currentSize = Binding; + } + } + + /// + protected override bool OnDragHorizontal(double horizontalChange) + { + // We use a central function for both horizontal/vertical as + // a general property has no notion of direction when we + // manipulate it, so the logic is abstracted. + return ApplySizeChange(horizontalChange); + } + + /// + protected override bool OnDragVertical(double verticalChange) + { + return ApplySizeChange(verticalChange); + } + + private bool ApplySizeChange(double newSize) + { + newSize = IsDragInverted ? -newSize : newSize; + + // We want to be checking the modified final value for bounds checks. + newSize += _currentSize; + + // Check if we hit the min/max value, as we should use that if we're on the edge + if (ReadLocalValue(MinimumProperty) != DependencyProperty.UnsetValue && + newSize < Minimum) + { + // We use SetValue here as that'll update our bound property vs. overwriting the binding itself. + SetValue(BindingProperty, Minimum); + } + else if (ReadLocalValue(MaximumProperty) != DependencyProperty.UnsetValue && + newSize > Maximum) + { + SetValue(BindingProperty, Maximum); + } + else + { + // Otherwise, we use the value provided. + SetValue(BindingProperty, newSize); + } + + // We're always manipulating the value effectively. + return true; + } +} diff --git a/labs/SizerBase/src/PropertySizer/PropertySizer.Properties.cs b/labs/SizerBase/src/PropertySizer/PropertySizer.Properties.cs new file mode 100644 index 000000000..1d2959f33 --- /dev/null +++ b/labs/SizerBase/src/PropertySizer/PropertySizer.Properties.cs @@ -0,0 +1,81 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if !WINAPPSDK +using Windows.UI.Xaml; +#else +using Microsoft.UI.Xaml; +#endif + +namespace CommunityToolkit.Labs.WinUI; + +// Properties for PropertySizer. +public partial class PropertySizer +{ + /// + /// Gets or sets a value indicating whether the control is resizing in the opposite direction. + /// + public bool IsDragInverted + { + get { return (bool)GetValue(IsDragInvertedProperty); } + set { SetValue(IsDragInvertedProperty, value); } + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty IsDragInvertedProperty = + DependencyProperty.Register(nameof(IsDragInverted), typeof(bool), typeof(PropertySizer), new PropertyMetadata(false)); + + /// + /// Gets or sets a two-way binding to a double value that the is manipulating. + /// + /// + /// Note that the binding should be configured to be a TwoWay binding in order for the control to notify the source of the changed value. + /// + /// + /// <controls:PropertySizer Binding="{Binding OpenPaneLength, ElementName=ViewPanel, Mode=TwoWay}"> + /// + public double Binding + { + get { return (double)GetValue(BindingProperty); } + set { SetValue(BindingProperty, value); } + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty BindingProperty = + DependencyProperty.Register(nameof(Binding), typeof(double), typeof(PropertySizer), new PropertyMetadata(null)); + + /// + /// Gets or sets the minimum allowed value for the to allow for the value. Ignored if not provided. + /// + public double Minimum + { + get { return (double)GetValue(MinimumProperty); } + set { SetValue(MinimumProperty, value); } + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty MinimumProperty = + DependencyProperty.Register(nameof(Minimum), typeof(double), typeof(PropertySizer), new PropertyMetadata(0)); + + /// + /// Gets or sets the maximum allowed value for the to allow for the value. Ignored if not provided. + /// + public double Maximum + { + get { return (double)GetValue(MaximumProperty); } + set { SetValue(MaximumProperty, value); } + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty MaximumProperty = + DependencyProperty.Register(nameof(Maximum), typeof(double), typeof(PropertySizer), new PropertyMetadata(0)); +} diff --git a/labs/SizerBase/src/PropertySizer/PropertySizer.cs b/labs/SizerBase/src/PropertySizer/PropertySizer.cs new file mode 100644 index 000000000..aa4377c5f --- /dev/null +++ b/labs/SizerBase/src/PropertySizer/PropertySizer.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if !WINAPPSDK +using Windows.UI.Xaml.Controls; +#else +using Microsoft.UI.Xaml.Controls; +#endif + +namespace CommunityToolkit.Labs.WinUI; + +/// +/// The is a control which can be used to manipulate the value of another double based property. For instance manipulating the OpenPaneLength of a NavigationView control. If you are using a , use instead. +/// +public partial class PropertySizer : SizerBase +{ +} diff --git a/labs/SizerBase/src/SizerAutomationPeer.cs b/labs/SizerBase/src/SizerAutomationPeer.cs new file mode 100644 index 000000000..5d59103d1 --- /dev/null +++ b/labs/SizerBase/src/SizerAutomationPeer.cs @@ -0,0 +1,83 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.Labs.WinUI; + +#if !WINAPPSDK +using Windows.UI.Xaml.Automation; +using Windows.UI.Xaml.Automation.Peers; +#else +using Microsoft.UI.Xaml.Automation; +using Microsoft.UI.Xaml.Automation.Peers; +#endif + + +namespace CommunityToolkit.Labs.WinUI.Automation.Peers; + +/// +/// Defines a framework element automation peer for the controls. +/// +public class SizerAutomationPeer : FrameworkElementAutomationPeer +{ + /// + /// Initializes a new instance of the class. + /// + /// + /// The that is associated with this . + /// + public SizerAutomationPeer(SizerBase owner) + : base(owner) + { + } + + private SizerBase OwningSizer + { + get + { + return (Owner as SizerBase)!; + } + } + + /// + /// Called by GetClassName that gets a human readable name that, in addition to AutomationControlType, + /// differentiates the control represented by this AutomationPeer. + /// + /// The string that contains the name. + protected override string GetClassNameCore() + { + return Owner.GetType().Name; + } + + /// + /// Called by GetName. + /// + /// + /// Returns the first of these that is not null or empty: + /// - Value returned by the base implementation + /// - Name of the owning ContentSizer + /// - ContentSizer class name + /// + protected override string GetNameCore() + { + string name = AutomationProperties.GetName(this.OwningSizer); + if (!string.IsNullOrEmpty(name)) + { + return name; + } + + name = this.OwningSizer.Name; + if (!string.IsNullOrEmpty(name)) + { + return name; + } + + name = base.GetNameCore(); + if (!string.IsNullOrEmpty(name)) + { + return name; + } + + return string.Empty; + } +} diff --git a/labs/SizerBase/src/SizerBase.Events.cs b/labs/SizerBase/src/SizerBase.Events.cs new file mode 100644 index 000000000..38d3f2893 --- /dev/null +++ b/labs/SizerBase/src/SizerBase.Events.cs @@ -0,0 +1,190 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +#if !WINAPPSDK +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Input; +#else +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Input; +#endif + +namespace CommunityToolkit.Labs.WinUI; + +/// +/// Event implementations for . +/// +public partial class SizerBase +{ + /// + protected override void OnKeyDown(KeyRoutedEventArgs e) + { + // If we're manipulating with mouse/touch, we ignore keyboard inputs. + if (_dragging) + { + return; + } + + //// TODO: Do we want Ctrl/Shift to be a small increment (kind of inverse to old GridSplitter logic)? + //// var ctrl = Window.Current.CoreWindow.GetKeyState(VirtualKey.Control); + //// if (ctrl.HasFlag(CoreVirtualKeyStates.Down)) + //// Note: WPF doesn't do anything here. + //// I think if we did anything, we'd create a SmallKeyboardIncrement property? + + // Initialize a drag event for this keyboard interaction. + OnDragStarting(); + + if (Orientation == Orientation.Vertical) + { + var horizontalChange = KeyboardIncrement; + + // Important: adjust for RTL language flow settings and invert horizontal axis + if (this.FlowDirection == FlowDirection.RightToLeft) + { + horizontalChange *= -1; + } + + if (e.Key == Windows.System.VirtualKey.Left) + { + OnDragHorizontal(-horizontalChange); + } + else if (e.Key == Windows.System.VirtualKey.Right) + { + OnDragHorizontal(horizontalChange); + } + } + else + { + if (e.Key == Windows.System.VirtualKey.Up) + { + OnDragVertical(-KeyboardIncrement); + } + else if (e.Key == Windows.System.VirtualKey.Down) + { + OnDragVertical(KeyboardIncrement); + } + } + } + + /// + protected override void OnManipulationStarting(ManipulationStartingRoutedEventArgs e) + { + base.OnManipulationStarting(e); + + OnDragStarting(); + } + + /// + protected override void OnManipulationDelta(ManipulationDeltaRoutedEventArgs e) + { + // We use Trancate here to provide 'snapping' points with the DragIncrement property + // It works for both our negative and positive values, as otherwise we'd need to use + // Ceiling when negative and Floor when positive to maintain the correct behavior. + var horizontalChange = + Math.Truncate(e.Cumulative.Translation.X / DragIncrement) * DragIncrement; + var verticalChange = + Math.Truncate(e.Cumulative.Translation.Y / DragIncrement) * DragIncrement; + + // Important: adjust for RTL language flow settings and invert horizontal axis + if (this.FlowDirection == FlowDirection.RightToLeft) + { + horizontalChange *= -1; + } + + if (Orientation == Orientation.Vertical) + { + if (!OnDragHorizontal(horizontalChange)) + { + return; + } + } + else if (Orientation == Orientation.Horizontal) + { + if (!OnDragVertical(verticalChange)) + { + return; + } + } + + base.OnManipulationDelta(e); + } + + // private helper bools for Visual States + private bool _pressed = false; + private bool _dragging = false; + private bool _pointerEntered = false; + + private void SizerBase_PointerReleased(object sender, PointerRoutedEventArgs e) + { + _pressed = false; + + if (IsEnabled) + { + VisualStateManager.GoToState(this, _pointerEntered ? "PointerOver" : "Normal", true); + } + } + + private void SizerBase_PointerPressed(object sender, PointerRoutedEventArgs e) + { + _pressed = true; + + if (IsEnabled) + { + VisualStateManager.GoToState(this, "Pressed", true); + } + } + + private void SizerBase_PointerExited(object sender, PointerRoutedEventArgs e) + { + _pointerEntered = false; + + if (!_pressed && !_dragging && IsEnabled) + { + VisualStateManager.GoToState(this, "Normal", true); + } + } + + private void SizerBase_PointerEntered(object sender, PointerRoutedEventArgs e) + { + _pointerEntered = true; + + if (!_pressed && !_dragging && IsEnabled) + { + VisualStateManager.GoToState(this, "PointerOver", true); + } + } + + private void SizerBase_ManipulationCompleted(object sender, ManipulationCompletedRoutedEventArgs e) + { + _dragging = false; + _pressed = false; + VisualStateManager.GoToState(this, _pointerEntered ? "PointerOver" : "Normal", true); + } + + private void SizerBase_ManipulationStarted(object sender, ManipulationStartedRoutedEventArgs e) + { + _dragging = true; + VisualStateManager.GoToState(this, "Pressed", true); + } + + private void SizerBase_IsEnabledChanged(object sender, DependencyPropertyChangedEventArgs e) + { + if (!IsEnabled) + { + VisualStateManager.GoToState(this, "Disabled", true); + } + else + { + VisualStateManager.GoToState(this, _pointerEntered ? "PointerOver" : "Normal", true); + } + } +} diff --git a/labs/SizerBase/src/SizerBase.Helpers.cs b/labs/SizerBase/src/SizerBase.Helpers.cs new file mode 100644 index 000000000..3fa67feec --- /dev/null +++ b/labs/SizerBase/src/SizerBase.Helpers.cs @@ -0,0 +1,77 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if !WINAPPSDK +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +#else +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +#endif + +namespace CommunityToolkit.Labs.WinUI; + +/// +/// Protected helper methods for and subclasses. +/// +public partial class SizerBase : Control +{ + /// + /// Check for new requested vertical size is valid or not + /// + /// Target control being resized + /// The requested new height + /// The parent control's ActualHeight + /// Bool result if requested vertical change is valid or not + protected static bool IsValidHeight(FrameworkElement target, double newHeight, double parentActualHeight) + { + var minHeight = target.MinHeight; + if (newHeight < 0 || (!double.IsNaN(minHeight) && newHeight < minHeight)) + { + return false; + } + + var maxHeight = target.MaxHeight; + if (!double.IsNaN(maxHeight) && newHeight > maxHeight) + { + return false; + } + + if (newHeight <= parentActualHeight) + { + return false; + } + + return true; + } + + /// + /// Check for new requested horizontal size is valid or not + /// + /// Target control being resized + /// The requested new width + /// The parent control's ActualWidth + /// Bool result if requested horizontal change is valid or not + protected static bool IsValidWidth(FrameworkElement target, double newWidth, double parentActualWidth) + { + var minWidth = target.MinWidth; + if (newWidth < 0 || (!double.IsNaN(minWidth) && newWidth < minWidth)) + { + return false; + } + + var maxWidth = target.MaxWidth; + if (!double.IsNaN(maxWidth) && newWidth > maxWidth) + { + return false; + } + + if (newWidth <= parentActualWidth) + { + return false; + } + + return true; + } +} diff --git a/labs/SizerBase/src/SizerBase.Properties.cs b/labs/SizerBase/src/SizerBase.Properties.cs new file mode 100644 index 000000000..aa7011ab9 --- /dev/null +++ b/labs/SizerBase/src/SizerBase.Properties.cs @@ -0,0 +1,102 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Windows.UI.Core; + +#if !WINAPPSDK +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +#else +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +#endif + +namespace CommunityToolkit.Labs.WinUI; + +/// +/// Properties for +/// +public partial class SizerBase : Control +{ + /// + /// Gets or sets the cursor to use when hovering over the gripper bar. + /// + public CoreCursorType Cursor + { + get { return (CoreCursorType)GetValue(CursorProperty); } + set { SetValue(CursorProperty, value); } + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty CursorProperty = + DependencyProperty.Register(nameof(Cursor), typeof(CoreCursorType), typeof(SizerBase), new PropertyMetadata(CoreCursorType.SizeWestEast)); + + /// + /// Gets or sets the incremental amount of change for draging with the mouse or touch of a sizer control. Effectively a snapping increment for changes. The default is 1. + /// + /// + /// For instance, if the DragIncrement is set to 16. Then when a component is resized with the sizer, it will only increase or decrease in size in that increment. I.e. -16, 0, 16, 32, 48, etc... + /// + /// + /// This value is indepedent of the property. If you need to provide consistent snapping when moving regardless of input device, set these properties to the same value. + /// + public double DragIncrement + { + get { return (double)GetValue(DragIncrementProperty); } + set { SetValue(DragIncrementProperty, value); } + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty DragIncrementProperty = + DependencyProperty.Register(nameof(DragIncrement), typeof(double), typeof(SizerBase), new PropertyMetadata(1d)); + + /// + /// Gets or sets the distance each press of an arrow key moves a sizer control. The default is 8. + /// + /// + /// This value is independent of the setting when using mouse/touch. If you want a consistent behavior regardless of input device, set them to the same value if snapping is required. + /// + public double KeyboardIncrement + { + get { return (double)GetValue(KeyboardIncrementProperty); } + set { SetValue(KeyboardIncrementProperty, value); } + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty KeyboardIncrementProperty = + DependencyProperty.Register(nameof(KeyboardIncrement), typeof(double), typeof(SizerBase), new PropertyMetadata(8d)); + + /// + /// Gets or sets the orientation the sizer will be and how it will interact with other elements. Defaults to . + /// + /// + /// Note if using , use the property instead. + /// + public Orientation Orientation + { + get { return (Orientation)GetValue(OrientationProperty); } + set { SetValue(OrientationProperty, value); } + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty OrientationProperty = + DependencyProperty.Register(nameof(Orientation), typeof(Orientation), typeof(SizerBase), new PropertyMetadata(Orientation.Vertical, OnOrientationPropertyChanged)); + + private static void OnOrientationPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is SizerBase gripper) + { + // TODO: For WinUI 3, we will just be setting the ProtectedCursor property directly. + gripper.Cursor = gripper.Orientation == Orientation.Vertical ? CoreCursorType.SizeWestEast : CoreCursorType.SizeNorthSouth; + } + } +} diff --git a/labs/SizerBase/src/SizerBase.cs b/labs/SizerBase/src/SizerBase.cs index c6ddf05bd..690220209 100644 --- a/labs/SizerBase/src/SizerBase.cs +++ b/labs/SizerBase/src/SizerBase.cs @@ -2,14 +2,129 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System; -using System.Collections.Generic; +using CommunityToolkit.Labs.WinUI.Automation.Peers; + +#if !WINAPPSDK +using Windows.UI.Xaml; +using Windows.UI.Xaml.Automation.Peers; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Input; +#else +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Automation.Peers; using Microsoft.UI.Xaml.Controls; -using Windows.Foundation; +using Microsoft.UI.Xaml.Input; +#endif + +namespace CommunityToolkit.Labs.WinUI; -namespace CommunityToolkit.Labs.WinUI +/// +/// Base class for splitting/resizing type controls like and . Acts similar to an enlarged type control, but with keyboard support. Subclasses should override the various abstract methods here to implement their behavior. +/// +public abstract partial class SizerBase : Control { - public class SizerBase + /// + /// Called when the control has been initialized. + /// + /// Loaded event args. + protected virtual void OnLoaded(RoutedEventArgs e) + { + } + + /// + /// Called when the control starts to be dragged by the user. + /// Implementor should record current state of manipulated target at this point in time. + /// They will receive the cumulative change in or + /// based on the property. + /// + /// + /// This method is also called at the start of a keyboard interaction. Keyboard strokes use the same pattern to emulate a mouse movement for a single change. The appropriate + /// or + /// method will also be called after when the keyboard is used. + /// + protected abstract void OnDragStarting(); + + /// + /// Method to process the requested horizontal resize. + /// + /// The horizontal change amount from the start in device-independent pixels DIP. + /// indicates if a change was made + /// + /// The value provided here is the cumulative change from the beginning of the + /// manipulation. This method will be used regardless of input device. It will already + /// be adjusted for RightToLeft of the containing + /// layout/settings. It will also already account for any settings such as + /// or . The implementor + /// just needs to use the provided value to manipulate their baseline stored + /// in to provide the desired change. + /// + protected abstract bool OnDragHorizontal(double horizontalChange); + + /// + /// Method to process the requested vertical resize. + /// + /// The vertical change amount from the start in device-independent pixels DIP. + /// indicates if a change was made + /// + /// The value provided here is the cumulative change from the beginning of the + /// manipulation. This method will be used regardless of input device. It will also + /// already account for any settings such as or + /// . The implementor just needs + /// to use the provided value to manipulate their baseline stored + /// in to provide the desired change. + /// + protected abstract bool OnDragVertical(double verticalChange); + + /// + /// Initializes a new instance of the class. + /// + public SizerBase() + { + this.DefaultStyleKey = typeof(SizerBase); + } + + /// + /// Creates AutomationPeer () + /// + /// An automation peer for this . + protected override AutomationPeer OnCreateAutomationPeer() { + return new SizerAutomationPeer(this); + } + + /// + protected override void OnApplyTemplate() + { + base.OnApplyTemplate(); + + // Unregister Events + Loaded -= SizerBase_Loaded; + PointerEntered -= SizerBase_PointerEntered; + PointerExited -= SizerBase_PointerExited; + PointerPressed -= SizerBase_PointerPressed; + PointerReleased -= SizerBase_PointerReleased; + ManipulationStarted -= SizerBase_ManipulationStarted; + ManipulationCompleted -= SizerBase_ManipulationCompleted; + IsEnabledChanged -= SizerBase_IsEnabledChanged; + + // Register Events + Loaded += SizerBase_Loaded; + PointerEntered += SizerBase_PointerEntered; + PointerExited += SizerBase_PointerExited; + PointerPressed += SizerBase_PointerPressed; + PointerReleased += SizerBase_PointerReleased; + ManipulationStarted += SizerBase_ManipulationStarted; + ManipulationCompleted += SizerBase_ManipulationCompleted; + IsEnabledChanged += SizerBase_IsEnabledChanged; + + // Trigger initial state transition based on if we're Enabled or not currently. + SizerBase_IsEnabledChanged(this, null!); + } + + private void SizerBase_Loaded(object sender, RoutedEventArgs e) + { + Loaded -= SizerBase_Loaded; + + OnLoaded(e); } } diff --git a/labs/SizerBase/src/SizerBase.xaml b/labs/SizerBase/src/SizerBase.xaml new file mode 100644 index 000000000..cb172b317 --- /dev/null +++ b/labs/SizerBase/src/SizerBase.xaml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/labs/SizerBase/src/Strings/en-US/Resources.resw b/labs/SizerBase/src/Strings/en-US/Resources.resw new file mode 100644 index 000000000..d9201f3ce --- /dev/null +++ b/labs/SizerBase/src/Strings/en-US/Resources.resw @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Collapse Blade + Narrator Resource for BladeView collapsed status + + + Expand Blade + Narrator Resource for BladeView expanded status + + + Sizer + Narrator Resource for SizerBase controls and similar + + \ No newline at end of file diff --git a/labs/SizerBase/src/Themes/Generic.xaml b/labs/SizerBase/src/Themes/Generic.xaml new file mode 100644 index 000000000..82fb0ba4b --- /dev/null +++ b/labs/SizerBase/src/Themes/Generic.xaml @@ -0,0 +1,6 @@ + + + + +