From 5158fec54f187f9febb96a9efd8b301166f04900 Mon Sep 17 00:00:00 2001 From: michael-hawker <24302614+michael-hawker@users.noreply.github.com> Date: Tue, 18 Apr 2023 15:06:25 -0700 Subject: [PATCH 1/5] Initial creation of Sizers component from template --- components/Sizers/OpenSolution.bat | 3 + components/Sizers/samples/Dependencies.props | 31 ++++ .../Sizers/samples/Sizers.Samples.csproj | 8 ++ components/Sizers/samples/Sizers.md | 64 +++++++++ .../Sizers/samples/SizersCustomSample.xaml | 25 ++++ .../Sizers/samples/SizersCustomSample.xaml.cs | 30 ++++ .../Sizers/samples/SizersTemplatedSample.xaml | 16 +++ .../samples/SizersTemplatedSample.xaml.cs | 21 +++ .../SizersTemplatedStyleCustomSample.xaml | 26 ++++ .../SizersTemplatedStyleCustomSample.xaml.cs | 21 +++ .../samples/SizersXbindBackedSample.xaml | 16 +++ .../samples/SizersXbindBackedSample.xaml.cs | 21 +++ .../SizersXbindBackedStyleCustomSample.xaml | 26 ++++ ...SizersXbindBackedStyleCustomSample.xaml.cs | 21 +++ .../Sizers/src/AdditionalAssemblyInfo.cs | 13 ++ ...munityToolkit.WinUI.Controls.Sizers.csproj | 13 ++ components/Sizers/src/Dependencies.props | 31 ++++ components/Sizers/src/MultiTarget.props | 9 ++ components/Sizers/src/Sizers.cs | 108 ++++++++++++++ .../src/SizersStyle_ClassicBinding.xaml | 62 ++++++++ components/Sizers/src/SizersStyle_xBind.xaml | 69 +++++++++ .../Sizers/src/SizersStyle_xBind.xaml.cs | 20 +++ .../Sizers/src/Sizers_ClassicBinding.cs | 94 ++++++++++++ components/Sizers/src/Sizers_xBind.cs | 71 +++++++++ components/Sizers/src/Themes/Generic.xaml | 10 ++ .../Sizers/tests/ExampleSizersTestClass.cs | 136 ++++++++++++++++++ .../Sizers/tests/ExampleSizersTestPage.xaml | 14 ++ .../tests/ExampleSizersTestPage.xaml.cs | 16 +++ .../Sizers/tests/Sizers.Tests.projitems | 23 +++ components/Sizers/tests/Sizers.Tests.shproj | 13 ++ 30 files changed, 1031 insertions(+) create mode 100644 components/Sizers/OpenSolution.bat create mode 100644 components/Sizers/samples/Dependencies.props create mode 100644 components/Sizers/samples/Sizers.Samples.csproj create mode 100644 components/Sizers/samples/Sizers.md create mode 100644 components/Sizers/samples/SizersCustomSample.xaml create mode 100644 components/Sizers/samples/SizersCustomSample.xaml.cs create mode 100644 components/Sizers/samples/SizersTemplatedSample.xaml create mode 100644 components/Sizers/samples/SizersTemplatedSample.xaml.cs create mode 100644 components/Sizers/samples/SizersTemplatedStyleCustomSample.xaml create mode 100644 components/Sizers/samples/SizersTemplatedStyleCustomSample.xaml.cs create mode 100644 components/Sizers/samples/SizersXbindBackedSample.xaml create mode 100644 components/Sizers/samples/SizersXbindBackedSample.xaml.cs create mode 100644 components/Sizers/samples/SizersXbindBackedStyleCustomSample.xaml create mode 100644 components/Sizers/samples/SizersXbindBackedStyleCustomSample.xaml.cs create mode 100644 components/Sizers/src/AdditionalAssemblyInfo.cs create mode 100644 components/Sizers/src/CommunityToolkit.WinUI.Controls.Sizers.csproj create mode 100644 components/Sizers/src/Dependencies.props create mode 100644 components/Sizers/src/MultiTarget.props create mode 100644 components/Sizers/src/Sizers.cs create mode 100644 components/Sizers/src/SizersStyle_ClassicBinding.xaml create mode 100644 components/Sizers/src/SizersStyle_xBind.xaml create mode 100644 components/Sizers/src/SizersStyle_xBind.xaml.cs create mode 100644 components/Sizers/src/Sizers_ClassicBinding.cs create mode 100644 components/Sizers/src/Sizers_xBind.cs create mode 100644 components/Sizers/src/Themes/Generic.xaml create mode 100644 components/Sizers/tests/ExampleSizersTestClass.cs create mode 100644 components/Sizers/tests/ExampleSizersTestPage.xaml create mode 100644 components/Sizers/tests/ExampleSizersTestPage.xaml.cs create mode 100644 components/Sizers/tests/Sizers.Tests.projitems create mode 100644 components/Sizers/tests/Sizers.Tests.shproj diff --git a/components/Sizers/OpenSolution.bat b/components/Sizers/OpenSolution.bat new file mode 100644 index 00000000..814a56d4 --- /dev/null +++ b/components/Sizers/OpenSolution.bat @@ -0,0 +1,3 @@ +@ECHO OFF + +powershell ..\..\tooling\ProjectHeads\GenerateSingleSampleHeads.ps1 -componentPath %CD% %* \ No newline at end of file diff --git a/components/Sizers/samples/Dependencies.props b/components/Sizers/samples/Dependencies.props new file mode 100644 index 00000000..e622e1df --- /dev/null +++ b/components/Sizers/samples/Dependencies.props @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/Sizers/samples/Sizers.Samples.csproj b/components/Sizers/samples/Sizers.Samples.csproj new file mode 100644 index 00000000..6f0b9aeb --- /dev/null +++ b/components/Sizers/samples/Sizers.Samples.csproj @@ -0,0 +1,8 @@ + + + Sizers + + + + + diff --git a/components/Sizers/samples/Sizers.md b/components/Sizers/samples/Sizers.md new file mode 100644 index 00000000..a6796af6 --- /dev/null +++ b/components/Sizers/samples/Sizers.md @@ -0,0 +1,64 @@ +--- +title: Sizers +author: githubaccount +description: TODO: Your experiment's description here +keywords: Sizers, Control, Layout +dev_langs: + - csharp +category: Controls +subcategory: Layout +discussion-id: 0 +issue-id: 0 +--- + + + + + + + + + +# Sizers + +TODO: Fill in information about this experiment and how to get started here... + +## Custom Control + +You can inherit from an existing component as well, like `Panel`, this example shows a control without a +XAML Style that will be more light-weight to consume by an app developer: + +> [!Sample SizersCustomSample] + +## Templated Controls + +The Toolkit is built with templated controls. This provides developers a flexible way to restyle components +easily while still inheriting the general functionality a control provides. The examples below show +how a component can use a default style and then get overridden by the end developer. + +TODO: Two types of templated control building methods are shown. Delete these if you're building a custom component. +Otherwise, pick one method for your component and delete the files related to the unchosen `_ClassicBinding` or `_xBind` +classes (and the custom non-suffixed one as well). Then, rename your component to just be your component name. + +The `_ClassicBinding` class shows the traditional method used to develop components with best practices. + +### Implict style + +> [!SAMPLE SizersTemplatedSample] + +### Custom style + +> [!SAMPLE SizersTemplatedStyleCustomSample] + +## Templated Controls with x:Bind + +This is an _experimental_ new way to define components which allows for the use of x:Bind within the style. + +### Implict style + +> [!SAMPLE SizersXbindBackedSample] + +### Custom style + +> [!SAMPLE SizersXbindBackedStyleCustomSample] + diff --git a/components/Sizers/samples/SizersCustomSample.xaml b/components/Sizers/samples/SizersCustomSample.xaml new file mode 100644 index 00000000..ca8a5adc --- /dev/null +++ b/components/Sizers/samples/SizersCustomSample.xaml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + diff --git a/components/Sizers/samples/SizersCustomSample.xaml.cs b/components/Sizers/samples/SizersCustomSample.xaml.cs new file mode 100644 index 00000000..8530818f --- /dev/null +++ b/components/Sizers/samples/SizersCustomSample.xaml.cs @@ -0,0 +1,30 @@ +// 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.Controls; + +namespace SizersExperiment.Samples; + +/// +/// An example sample page of a custom control inheriting from Panel. +/// +[ToolkitSampleTextOption("TitleText", "This is a title", Title = "Input the text")] +[ToolkitSampleMultiChoiceOption("LayoutOrientation", "Horizontal", "Vertical", Title = "Orientation")] + +[ToolkitSample(id: nameof(SizersCustomSample), "Custom control", description: $"A sample for showing how to create and use a {nameof(Sizers)} custom control.")] +public sealed partial class SizersCustomSample : Page +{ + public SizersCustomSample() + { + this.InitializeComponent(); + } + + // TODO: See https://github.com/CommunityToolkit/Labs-Windows/issues/149 + public static Orientation ConvertStringToOrientation(string orientation) => orientation switch + { + "Vertical" => Orientation.Vertical, + "Horizontal" => Orientation.Horizontal, + _ => throw new System.NotImplementedException(), + }; +} diff --git a/components/Sizers/samples/SizersTemplatedSample.xaml b/components/Sizers/samples/SizersTemplatedSample.xaml new file mode 100644 index 00000000..a4b96fe4 --- /dev/null +++ b/components/Sizers/samples/SizersTemplatedSample.xaml @@ -0,0 +1,16 @@ + + + + + + + diff --git a/components/Sizers/samples/SizersTemplatedSample.xaml.cs b/components/Sizers/samples/SizersTemplatedSample.xaml.cs new file mode 100644 index 00000000..daa7febf --- /dev/null +++ b/components/Sizers/samples/SizersTemplatedSample.xaml.cs @@ -0,0 +1,21 @@ +// 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 SizersExperiment.Samples; + +[ToolkitSampleBoolOption("IsTextVisible", true, Title = "IsVisible")] +// Single values without a colon are used for both label and value. +// To provide a different label for the value, separate with a colon surrounded by a single space on both sides ("label : value"). +[ToolkitSampleNumericOption("TextSize", 12, 8, 48, 2, false, Title = "FontSize")] +[ToolkitSampleMultiChoiceOption("TextFontFamily", "Segoe UI", "Arial", "Consolas", Title = "Font family")] +[ToolkitSampleMultiChoiceOption("TextForeground", "Teal : #0ddc8c", "Sand : #e7a676", "Dull green : #5d7577", Title = "Text foreground")] + +[ToolkitSample(id: nameof(SizersTemplatedSample), "Templated control", description: "A sample for showing how to create and use a templated control.")] +public sealed partial class SizersTemplatedSample : Page +{ + public SizersTemplatedSample() + { + this.InitializeComponent(); + } +} diff --git a/components/Sizers/samples/SizersTemplatedStyleCustomSample.xaml b/components/Sizers/samples/SizersTemplatedStyleCustomSample.xaml new file mode 100644 index 00000000..08a95cb6 --- /dev/null +++ b/components/Sizers/samples/SizersTemplatedStyleCustomSample.xaml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + diff --git a/components/Sizers/samples/SizersTemplatedStyleCustomSample.xaml.cs b/components/Sizers/samples/SizersTemplatedStyleCustomSample.xaml.cs new file mode 100644 index 00000000..09b36297 --- /dev/null +++ b/components/Sizers/samples/SizersTemplatedStyleCustomSample.xaml.cs @@ -0,0 +1,21 @@ +// 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 SizersExperiment.Samples; + +[ToolkitSampleBoolOption("IsTextVisible", true, Title = "IsVisible")] +// Single values without a colon are used for both label and value. +// To provide a different label for the value, separate with a colon surrounded by a single space on both sides ("label : value"). +[ToolkitSampleNumericOption("TextSize", 12, 8, 48, 2, true, Title = "FontSize")] +[ToolkitSampleMultiChoiceOption("TextFontFamily", "Segoe UI", "Arial", "Consolas", Title = "Font family")] +[ToolkitSampleMultiChoiceOption("TextForeground", "Teal : #0ddc8c", "Sand : #e7a676", "Dull green : #5d7577", Title = "Text foreground")] + +[ToolkitSample(id: nameof(SizersTemplatedStyleCustomSample), "Templated control (restyled)", description: "A sample for showing how to create a use and templated control with a custom style.")] +public sealed partial class SizersTemplatedStyleCustomSample : Page +{ + public SizersTemplatedStyleCustomSample() + { + this.InitializeComponent(); + } +} diff --git a/components/Sizers/samples/SizersXbindBackedSample.xaml b/components/Sizers/samples/SizersXbindBackedSample.xaml new file mode 100644 index 00000000..643d151e --- /dev/null +++ b/components/Sizers/samples/SizersXbindBackedSample.xaml @@ -0,0 +1,16 @@ + + + + + + + diff --git a/components/Sizers/samples/SizersXbindBackedSample.xaml.cs b/components/Sizers/samples/SizersXbindBackedSample.xaml.cs new file mode 100644 index 00000000..f4144634 --- /dev/null +++ b/components/Sizers/samples/SizersXbindBackedSample.xaml.cs @@ -0,0 +1,21 @@ +// 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 SizersExperiment.Samples; + +[ToolkitSampleBoolOption("IsTextVisible", true, Title = "IsVisible")] +// Single values without a colon are used for both label and value. +// To provide a different label for the value, separate with a colon surrounded by a single space on both sides ("label : value"). +[ToolkitSampleNumericOption("TextSize", 12, 8, 48, 2, false, Title = "FontSize")] +[ToolkitSampleMultiChoiceOption("TextFontFamily", "Segoe UI", "Arial", "Consolas", Title = "Font family")] +[ToolkitSampleMultiChoiceOption("TextForeground", "Teal : #0ddc8c", "Sand : #e7a676", "Dull green : #5d7577", Title = "Text foreground")] + +[ToolkitSample(id: nameof(SizersXbindBackedSample), "Backed templated control", description: "A sample for showing how to create and use a templated control with a backed resource dictionary.")] +public sealed partial class SizersXbindBackedSample : Page +{ + public SizersXbindBackedSample() + { + this.InitializeComponent(); + } +} diff --git a/components/Sizers/samples/SizersXbindBackedStyleCustomSample.xaml b/components/Sizers/samples/SizersXbindBackedStyleCustomSample.xaml new file mode 100644 index 00000000..2f1b4fa3 --- /dev/null +++ b/components/Sizers/samples/SizersXbindBackedStyleCustomSample.xaml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + diff --git a/components/Sizers/samples/SizersXbindBackedStyleCustomSample.xaml.cs b/components/Sizers/samples/SizersXbindBackedStyleCustomSample.xaml.cs new file mode 100644 index 00000000..612c34e5 --- /dev/null +++ b/components/Sizers/samples/SizersXbindBackedStyleCustomSample.xaml.cs @@ -0,0 +1,21 @@ +// 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 SizersExperiment.Samples; + +[ToolkitSampleBoolOption("IsTextVisible", true, Title = "IsVisible")] +// Single values without a colon are used for both label and value. +// To provide a different label for the value, separate with a colon surrounded by a single space on both sides ("label : value"). +[ToolkitSampleNumericOption("TextSize", 12, 8, 48, 2, true, Title = "FontSize")] +[ToolkitSampleMultiChoiceOption("TextFontFamily", "Segoe UI", "Arial", "Consolas", Title = "Font family")] +[ToolkitSampleMultiChoiceOption("TextForeground", "Teal : #0ddc8c", "Sand : #e7a676", "Dull green : #5d7577", Title = "Text foreground")] + +[ToolkitSample(id: nameof(SizersXbindBackedStyleCustomSample), "Backed templated control (restyled)", description: "A sample for showing how to create and use a templated control with a backed resource dictionary and a custom style.")] +public sealed partial class SizersXbindBackedStyleCustomSample : Page +{ + public SizersXbindBackedStyleCustomSample() + { + this.InitializeComponent(); + } +} diff --git a/components/Sizers/src/AdditionalAssemblyInfo.cs b/components/Sizers/src/AdditionalAssemblyInfo.cs new file mode 100644 index 00000000..aa283849 --- /dev/null +++ b/components/Sizers/src/AdditionalAssemblyInfo.cs @@ -0,0 +1,13 @@ +// 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.Runtime.CompilerServices; + +// These `InternalsVisibleTo` calls are intended to make it easier for +// for any internal code to be testable in all the different test projects +// used with the Labs infrastructure. +[assembly: InternalsVisibleTo("Sizers.Tests.Uwp")] +[assembly: InternalsVisibleTo("Sizers.Tests.WinAppSdk")] +[assembly: InternalsVisibleTo("CommunityToolkit.Tests.Uwp")] +[assembly: InternalsVisibleTo("CommunityToolkit.Tests.WinAppSdk")] diff --git a/components/Sizers/src/CommunityToolkit.WinUI.Controls.Sizers.csproj b/components/Sizers/src/CommunityToolkit.WinUI.Controls.Sizers.csproj new file mode 100644 index 00000000..96524544 --- /dev/null +++ b/components/Sizers/src/CommunityToolkit.WinUI.Controls.Sizers.csproj @@ -0,0 +1,13 @@ + + + Sizers + This package contains Sizers. + 0.0.1 + + + CommunityToolkit.WinUI.Controls.SizersRns + + + + + diff --git a/components/Sizers/src/Dependencies.props b/components/Sizers/src/Dependencies.props new file mode 100644 index 00000000..e622e1df --- /dev/null +++ b/components/Sizers/src/Dependencies.props @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/Sizers/src/MultiTarget.props b/components/Sizers/src/MultiTarget.props new file mode 100644 index 00000000..b11c1942 --- /dev/null +++ b/components/Sizers/src/MultiTarget.props @@ -0,0 +1,9 @@ + + + + uwp;wasdk;wpf;wasm;linuxgtk;macos;ios;android; + + \ No newline at end of file diff --git a/components/Sizers/src/Sizers.cs b/components/Sizers/src/Sizers.cs new file mode 100644 index 00000000..a788ee09 --- /dev/null +++ b/components/Sizers/src/Sizers.cs @@ -0,0 +1,108 @@ +// 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.WinUI.Controls; + +/// +/// This is an example control based off of the BoxPanel sample here: https://docs.microsoft.com/windows/apps/design/layout/boxpanel-example-custom-panel. If you need this similar sort of layout component for an application, see UniformGrid in the Toolkit. +/// It is provided as an example of how to inherit from another control like . +/// You can choose to start here or from the or example components. Remove unused components and rename as appropriate. +/// +public partial class Sizers : Panel +{ + /// + /// Identifies the property. + /// + public static readonly DependencyProperty OrientationProperty = + DependencyProperty.Register(nameof(Orientation), typeof(Orientation), typeof(Sizers), new PropertyMetadata(null, OnOrientationChanged)); + + /// + /// Gets the preference of the rows/columns when there are a non-square number of children. Defaults to Vertical. + /// + public Orientation Orientation + { + get { return (Orientation)GetValue(OrientationProperty); } + set { SetValue(OrientationProperty, value); } + } + + // Invalidate our layout when the property changes. + private static void OnOrientationChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs args) + { + if (dependencyObject is Sizers panel) + { + panel.InvalidateMeasure(); + } + } + + // Store calculations we want to use between the Measure and Arrange methods. + int _columnCount; + double _cellWidth, _cellHeight; + + protected override Size MeasureOverride(Size availableSize) + { + // Determine the square that can contain this number of items. + var maxrc = (int)Math.Ceiling(Math.Sqrt(Children.Count)); + // Get an aspect ratio from availableSize, decides whether to trim row or column. + var aspectratio = availableSize.Width / availableSize.Height; + if (Orientation == Orientation.Vertical) { aspectratio = 1 / aspectratio; } + + int rowcount; + + // Now trim this square down to a rect, many times an entire row or column can be omitted. + if (aspectratio > 1) + { + rowcount = maxrc; + _columnCount = (maxrc > 2 && Children.Count <= maxrc * (maxrc - 1)) ? maxrc - 1 : maxrc; + } + else + { + rowcount = (maxrc > 2 && Children.Count <= maxrc * (maxrc - 1)) ? maxrc - 1 : maxrc; + _columnCount = maxrc; + } + + // Now that we have a column count, divide available horizontal, that's our cell width. + _cellWidth = (int)Math.Floor(availableSize.Width / _columnCount); + // Next get a cell height, same logic of dividing available vertical by rowcount. + _cellHeight = Double.IsInfinity(availableSize.Height) ? Double.PositiveInfinity : availableSize.Height / rowcount; + + double maxcellheight = 0; + + foreach (UIElement child in Children) + { + child.Measure(new Size(_cellWidth, _cellHeight)); + maxcellheight = (child.DesiredSize.Height > maxcellheight) ? child.DesiredSize.Height : maxcellheight; + } + + return LimitUnboundedSize(availableSize, maxcellheight); + } + + // This method limits the panel height when no limit is imposed by the panel's parent. + // That can happen to height if the panel is close to the root of main app window. + // In this case, base the height of a cell on the max height from desired size + // and base the height of the panel on that number times the #rows. + Size LimitUnboundedSize(Size input, double maxcellheight) + { + if (Double.IsInfinity(input.Height)) + { + input.Height = maxcellheight * _columnCount; + _cellHeight = maxcellheight; + } + return input; + } + + protected override Size ArrangeOverride(Size finalSize) + { + int count = 1; + double x, y; + foreach (UIElement child in Children) + { + x = (count - 1) % _columnCount * _cellWidth; + y = ((int)(count - 1) / _columnCount) * _cellHeight; + Point anchorPoint = new Point(x, y); + child.Arrange(new Rect(anchorPoint, child.DesiredSize)); + count++; + } + return finalSize; + } +} diff --git a/components/Sizers/src/SizersStyle_ClassicBinding.xaml b/components/Sizers/src/SizersStyle_ClassicBinding.xaml new file mode 100644 index 00000000..c613a591 --- /dev/null +++ b/components/Sizers/src/SizersStyle_ClassicBinding.xaml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + 4,4,4,4 + + + + + + + + diff --git a/components/Sizers/src/SizersStyle_xBind.xaml b/components/Sizers/src/SizersStyle_xBind.xaml new file mode 100644 index 00000000..840b5c58 --- /dev/null +++ b/components/Sizers/src/SizersStyle_xBind.xaml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + 4,4,4,4 + + + + + + + + diff --git a/components/Sizers/src/SizersStyle_xBind.xaml.cs b/components/Sizers/src/SizersStyle_xBind.xaml.cs new file mode 100644 index 00000000..b9d217e0 --- /dev/null +++ b/components/Sizers/src/SizersStyle_xBind.xaml.cs @@ -0,0 +1,20 @@ +// 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.WinUI.Controls; + +/// +/// Backing code for this resource dictionary. +/// +public sealed partial class SizersStyle_xBind : ResourceDictionary +{ + // NOTICE + // This file only exists to enable x:Bind in the resource dictionary. + // Do not add code here. + // Instead, add code-behind to your templated control. + public SizersStyle_xBind() + { + this.InitializeComponent(); + } +} diff --git a/components/Sizers/src/Sizers_ClassicBinding.cs b/components/Sizers/src/Sizers_ClassicBinding.cs new file mode 100644 index 00000000..8022f147 --- /dev/null +++ b/components/Sizers/src/Sizers_ClassicBinding.cs @@ -0,0 +1,94 @@ +// 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.WinUI.Controls; + +/// +/// An example templated control. +/// +[TemplatePart(Name = nameof(PART_HelloWorld), Type = typeof(TextBlock))] +public partial class Sizers_ClassicBinding : Control +{ + /// + /// Creates a new instance of the class. + /// + public Sizers_ClassicBinding() + { + this.DefaultStyleKey = typeof(Sizers_ClassicBinding); + } + + /// + /// The primary text block that displays "Hello world". + /// + protected TextBlock? PART_HelloWorld { get; private set; } + + /// + protected override void OnApplyTemplate() + { + base.OnApplyTemplate(); + + // Detach all attached events when a new template is applied. + if (PART_HelloWorld is not null) + { + PART_HelloWorld.PointerEntered -= Element_PointerEntered; + } + + // Attach events when the template is applied and the control is loaded. + PART_HelloWorld = GetTemplateChild(nameof(PART_HelloWorld)) as TextBlock; + + if (PART_HelloWorld is not null) + { + PART_HelloWorld.PointerEntered += Element_PointerEntered; + } + } + + /// + /// The backing for the property. + /// + public static readonly DependencyProperty ItemPaddingProperty = DependencyProperty.Register( + nameof(ItemPadding), + typeof(Thickness), + typeof(Sizers_ClassicBinding), + new PropertyMetadata(defaultValue: new Thickness(0))); + + /// + /// The backing for the property. + /// + public static readonly DependencyProperty MyPropertyProperty = DependencyProperty.Register( + nameof(MyProperty), + typeof(string), + typeof(Sizers_ClassicBinding), + new PropertyMetadata(defaultValue: string.Empty, (d, e) => ((Sizers_ClassicBinding)d).OnMyPropertyChanged((string)e.OldValue, (string)e.NewValue))); + + /// + /// Gets or sets an example string. A basic DependencyProperty example. + /// + public string MyProperty + { + get => (string)GetValue(MyPropertyProperty); + set => SetValue(MyPropertyProperty, value); + } + + /// + /// Gets or sets a padding for an item. A basic DependencyProperty example. + /// + public Thickness ItemPadding + { + get => (Thickness)GetValue(ItemPaddingProperty); + set => SetValue(ItemPaddingProperty, value); + } + + protected virtual void OnMyPropertyChanged(string oldValue, string newValue) + { + // Do something with the changed value. + } + + public void Element_PointerEntered(object sender, PointerRoutedEventArgs e) + { + if (sender is TextBlock text) + { + text.Opacity = 1; + } + } +} diff --git a/components/Sizers/src/Sizers_xBind.cs b/components/Sizers/src/Sizers_xBind.cs new file mode 100644 index 00000000..5dc2c552 --- /dev/null +++ b/components/Sizers/src/Sizers_xBind.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. + +namespace CommunityToolkit.WinUI.Controls; + +/// +/// An example templated control. +/// +public partial class Sizers_xBind: Control +{ + /// + /// Creates a new instance of the class. + /// + public Sizers_xBind() + { + this.DefaultStyleKey = typeof(Sizers_xBind); + + // Allows directly using this control as the x:DataType in the template. + this.DataContext = this; + } + + /// + /// The backing for the property. + /// + public static readonly DependencyProperty ItemPaddingProperty = DependencyProperty.Register( + nameof(ItemPadding), + typeof(Thickness), + typeof(Sizers_xBind), + new PropertyMetadata(defaultValue: new Thickness(0))); + + /// + /// The backing for the property. + /// + public static readonly DependencyProperty MyPropertyProperty = DependencyProperty.Register( + nameof(MyProperty), + typeof(string), + typeof(Sizers_xBind), + new PropertyMetadata(defaultValue: string.Empty, (d, e) => ((Sizers_xBind)d).OnMyPropertyChanged((string)e.OldValue, (string)e.NewValue))); + + /// + /// Gets or sets an example string. A basic DependencyProperty example. + /// + public string MyProperty + { + get => (string)GetValue(MyPropertyProperty); + set => SetValue(MyPropertyProperty, value); + } + + /// + /// Gets or sets a padding for an item. A basic DependencyProperty example. + /// + public Thickness ItemPadding + { + get => (Thickness)GetValue(ItemPaddingProperty); + set => SetValue(ItemPaddingProperty, value); + } + + protected virtual void OnMyPropertyChanged(string oldValue, string newValue) + { + // Do something with the changed value. + } + + public void Element_PointerEntered(object sender, PointerRoutedEventArgs e) + { + if (sender is TextBlock text) + { + text.Opacity = 1; + } + } +} diff --git a/components/Sizers/src/Themes/Generic.xaml b/components/Sizers/src/Themes/Generic.xaml new file mode 100644 index 00000000..d9dfe7ea --- /dev/null +++ b/components/Sizers/src/Themes/Generic.xaml @@ -0,0 +1,10 @@ + + + + + + + + diff --git a/components/Sizers/tests/ExampleSizersTestClass.cs b/components/Sizers/tests/ExampleSizersTestClass.cs new file mode 100644 index 00000000..a143f027 --- /dev/null +++ b/components/Sizers/tests/ExampleSizersTestClass.cs @@ -0,0 +1,136 @@ +// 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.Tooling.TestGen; +using CommunityToolkit.Tests; +using CommunityToolkit.Tests.Internal; // TODO: For CompositionTargetHelper until ported over into package. +using CommunityToolkit.WinUI; +using CommunityToolkit.WinUI.Controls; + +namespace SizersExperiment.Tests; + +[TestClass] +public partial class ExampleSizersTestClass : VisualUITestBase +{ + // If you don't need access to UI objects directly or async code, use this pattern. + [TestMethod] + public void SimpleSynchronousExampleTest() + { + var assembly = typeof(Sizers).Assembly; + var type = assembly.GetType(typeof(Sizers).FullName ?? string.Empty); + + Assert.IsNotNull(type, "Could not find Sizers type."); + Assert.AreEqual(typeof(Sizers), type, "Type of Sizers does not match expected type."); + } + + // If you don't need access to UI objects directly, use this pattern. + [TestMethod] + public async Task SimpleAsyncExampleTest() + { + await Task.Delay(250); + + Assert.IsTrue(true); + } + + // Example that shows how to check for exception throwing. + [TestMethod] + public void SimpleExceptionCheckTest() + { + // If you need to check exceptions occur for invalid inputs, etc... + // Use Assert.ThrowsException to limit the scope to where you expect the error to occur. + // Otherwise, using the ExpectedException attribute could swallow or + // catch other issues in setup code. + Assert.ThrowsException(() => throw new NotImplementedException()); + } + + // The UIThreadTestMethod automatically dispatches to the UI for us to work with UI objects. + [UIThreadTestMethod] + public void SimpleUIAttributeExampleTest() + { + var component = new Sizers(); + Assert.IsNotNull(component); + } + + // The UIThreadTestMethod can also easily grab a XAML Page for us by passing its type as a parameter. + // This lets us actually test a control as it would behave within an actual application. + // The page will already be loaded by the time your test is called. + [UIThreadTestMethod] + public void SimpleUIExamplePageTest(ExampleSizersTestPage page) + { + // You can use the Toolkit Visual Tree helpers here to find the component by type or name: + var component = page.FindDescendant(); + + Assert.IsNotNull(component); + + var componentByName = page.FindDescendant("SizersControl"); + + Assert.IsNotNull(componentByName); + } + + // You can still do async work with a UIThreadTestMethod as well. + [UIThreadTestMethod] + public async Task SimpleAsyncUIExamplePageTest(ExampleSizersTestPage page) + { + // This helper can be used to wait for a rendering pass to complete. + // Note, this is already done by loading a Page with the [UIThreadTestMethod] helper. + await CompositionTargetHelper.ExecuteAfterCompositionRenderingAsync(() => { }); + + var component = page.FindDescendant(); + + Assert.IsNotNull(component); + } + + //// ----------------------------- ADVANCED TEST SCENARIOS ----------------------------- + + // If you need to use DataRow, you can use this pattern with the UI dispatch still. + // Otherwise, checkout the UIThreadTestMethod attribute above. + // See https://github.com/CommunityToolkit/Labs-Windows/issues/186 + [TestMethod] + public async Task ComplexAsyncUIExampleTest() + { + await EnqueueAsync(() => + { + var component = new Sizers_ClassicBinding(); + Assert.IsNotNull(component); + }); + } + + // If you want to load other content not within a XAML page using the UIThreadTestMethod above. + // Then you can do that using the Load/UnloadTestContentAsync methods. + [TestMethod] + public async Task ComplexAsyncLoadUIExampleTest() + { + await EnqueueAsync(async () => + { + var component = new Sizers_ClassicBinding(); + Assert.IsNotNull(component); + Assert.IsFalse(component.IsLoaded); + + await LoadTestContentAsync(component); + + Assert.IsTrue(component.IsLoaded); + + await UnloadTestContentAsync(component); + + Assert.IsFalse(component.IsLoaded); + }); + } + + // You can still use the UIThreadTestMethod to remove the extra layer for the dispatcher as well: + [UIThreadTestMethod] + public async Task ComplexAsyncLoadUIExampleWithoutDispatcherTest() + { + var component = new Sizers_ClassicBinding(); + Assert.IsNotNull(component); + Assert.IsFalse(component.IsLoaded); + + await LoadTestContentAsync(component); + + Assert.IsTrue(component.IsLoaded); + + await UnloadTestContentAsync(component); + + Assert.IsFalse(component.IsLoaded); + } +} diff --git a/components/Sizers/tests/ExampleSizersTestPage.xaml b/components/Sizers/tests/ExampleSizersTestPage.xaml new file mode 100644 index 00000000..1df112c1 --- /dev/null +++ b/components/Sizers/tests/ExampleSizersTestPage.xaml @@ -0,0 +1,14 @@ + + + + + + + diff --git a/components/Sizers/tests/ExampleSizersTestPage.xaml.cs b/components/Sizers/tests/ExampleSizersTestPage.xaml.cs new file mode 100644 index 00000000..d02dd467 --- /dev/null +++ b/components/Sizers/tests/ExampleSizersTestPage.xaml.cs @@ -0,0 +1,16 @@ +// 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 SizersExperiment.Tests; + +/// +/// An empty page that can be used on its own or navigated to within a Frame. +/// +public sealed partial class ExampleSizersTestPage : Page +{ + public ExampleSizersTestPage() + { + this.InitializeComponent(); + } +} diff --git a/components/Sizers/tests/Sizers.Tests.projitems b/components/Sizers/tests/Sizers.Tests.projitems new file mode 100644 index 00000000..86d4fc6d --- /dev/null +++ b/components/Sizers/tests/Sizers.Tests.projitems @@ -0,0 +1,23 @@ + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + true + FE1BF6B1-E2B8-4F75-9629-97B5AA077FAA + + + SizersExperiment.Tests + + + + + ExampleSizersTestPage.xaml + + + + + Designer + MSBuild:Compile + + + \ No newline at end of file diff --git a/components/Sizers/tests/Sizers.Tests.shproj b/components/Sizers/tests/Sizers.Tests.shproj new file mode 100644 index 00000000..ef8fa866 --- /dev/null +++ b/components/Sizers/tests/Sizers.Tests.shproj @@ -0,0 +1,13 @@ + + + + FE1BF6B1-E2B8-4F75-9629-97B5AA077FAA + 14.0 + + + + + + + + From 006543f1a71cc141849311141d451407ff307629 Mon Sep 17 00:00:00 2001 From: michael-hawker <24302614+michael-hawker@users.noreply.github.com> Date: Tue, 18 Apr 2023 16:18:28 -0700 Subject: [PATCH 2/5] Import current SizerBase component from WCT Labs to 'Sizers' and update namespace to CommunityToolkit.WinUI.Controls --- components/Sizers/samples/ContentSizer.md | 26 ++ .../samples/ContentSizerLeftShelfPage.xaml | 32 +++ .../samples/ContentSizerLeftShelfPage.xaml.cs | 14 + .../samples/ContentSizerTopShelfPage.xaml | 43 +++ .../samples/ContentSizerTopShelfPage.xaml.cs | 14 + components/Sizers/samples/GridSplitter.md | 21 ++ .../Sizers/samples/GridSplitterPage.xaml | 106 ++++++++ .../Sizers/samples/GridSplitterPage.xaml.cs | 17 ++ components/Sizers/samples/PropertySizer.md | 24 ++ .../PropertySizerNavigationViewPage.xaml | 72 +++++ .../PropertySizerNavigationViewPage.xaml.cs | 14 + ...amples.csproj => SizerBase.Samples.csproj} | 0 components/Sizers/samples/SizerControls.md | 30 +++ .../Sizers/samples/SizerCursorPage.xaml | 26 ++ .../Sizers/samples/SizerCursorPage.xaml.cs | 14 + components/Sizers/samples/Sizers.md | 64 ----- .../Sizers/samples/SizersCustomSample.xaml | 25 -- .../Sizers/samples/SizersCustomSample.xaml.cs | 30 --- .../Sizers/samples/SizersTemplatedSample.xaml | 16 -- .../samples/SizersTemplatedSample.xaml.cs | 21 -- .../SizersTemplatedStyleCustomSample.xaml | 26 -- .../SizersTemplatedStyleCustomSample.xaml.cs | 21 -- .../samples/SizersXbindBackedSample.xaml | 16 -- .../samples/SizersXbindBackedSample.xaml.cs | 21 -- .../SizersXbindBackedStyleCustomSample.xaml | 26 -- ...SizersXbindBackedStyleCustomSample.xaml.cs | 21 -- .../Sizers/src/AdditionalAssemblyInfo.cs | 4 +- ...munityToolkit.WinUI.Controls.Sizers.csproj | 4 +- .../src/ContentSizer/ContentSizer.Events.cs | 74 ++++++ .../ContentSizer/ContentSizer.Properties.cs | 62 +++++ .../Sizers/src/ContentSizer/ContentSizer.cs | 12 + .../src/GridSplitter/GridSplitter.Data.cs | 56 ++++ .../src/GridSplitter/GridSplitter.Events.cs | 169 ++++++++++++ .../src/GridSplitter/GridSplitter.Helpers.cs | 245 ++++++++++++++++++ .../GridSplitter/GridSplitter.Properties.cs | 78 ++++++ .../Sizers/src/GridSplitter/GridSplitter.cs | 145 +++++++++++ .../src/PropertySizer/PropertySizer.Events.cs | 66 +++++ .../PropertySizer/PropertySizer.Properties.cs | 75 ++++++ .../Sizers/src/PropertySizer/PropertySizer.cs | 12 + components/Sizers/src/SizerAutomationPeer.cs | 72 +++++ components/Sizers/src/SizerBase.Events.cs | 178 +++++++++++++ components/Sizers/src/SizerBase.Helpers.cs | 69 +++++ components/Sizers/src/SizerBase.Properties.cs | 128 +++++++++ components/Sizers/src/SizerBase.cs | 130 ++++++++++ components/Sizers/src/SizerBase.xaml | 85 ++++++ components/Sizers/src/Sizers.cs | 108 -------- .../src/SizersStyle_ClassicBinding.xaml | 62 ----- components/Sizers/src/SizersStyle_xBind.xaml | 69 ----- .../Sizers/src/SizersStyle_xBind.xaml.cs | 20 -- .../Sizers/src/Sizers_ClassicBinding.cs | 94 ------- components/Sizers/src/Sizers_xBind.cs | 71 ----- .../Sizers/src/Strings/en-US/Resources.resw | 124 +++++++++ components/Sizers/src/Themes/Generic.xaml | 11 +- .../Sizers/src/Toolkit/ConverterTools.cs | 45 ++++ .../src/Toolkit/DependencyObjectExtensions.cs | 225 ++++++++++++++++ .../FrameworkElementExtensions.Mouse.cs | 113 ++++++++ .../Toolkit/OrientationToObjectConverter.cs | 76 ++++++ .../src/Toolkit/TypeToObjectConverter.cs | 91 +++++++ .../Sizers/tests/ExampleSizerBaseTestClass.cs | 61 +++++ .../Sizers/tests/ExampleSizersTestClass.cs | 136 ---------- .../Sizers/tests/ExampleSizersTestPage.xaml | 14 - .../PropertySizerTestInitialBinding.xaml | 28 ++ ...> PropertySizerTestInitialBinding.xaml.cs} | 4 +- .../Sizers/tests/Sizers.Tests.projitems | 8 +- 64 files changed, 2886 insertions(+), 878 deletions(-) create mode 100644 components/Sizers/samples/ContentSizer.md create mode 100644 components/Sizers/samples/ContentSizerLeftShelfPage.xaml create mode 100644 components/Sizers/samples/ContentSizerLeftShelfPage.xaml.cs create mode 100644 components/Sizers/samples/ContentSizerTopShelfPage.xaml create mode 100644 components/Sizers/samples/ContentSizerTopShelfPage.xaml.cs create mode 100644 components/Sizers/samples/GridSplitter.md create mode 100644 components/Sizers/samples/GridSplitterPage.xaml create mode 100644 components/Sizers/samples/GridSplitterPage.xaml.cs create mode 100644 components/Sizers/samples/PropertySizer.md create mode 100644 components/Sizers/samples/PropertySizerNavigationViewPage.xaml create mode 100644 components/Sizers/samples/PropertySizerNavigationViewPage.xaml.cs rename components/Sizers/samples/{Sizers.Samples.csproj => SizerBase.Samples.csproj} (100%) create mode 100644 components/Sizers/samples/SizerControls.md create mode 100644 components/Sizers/samples/SizerCursorPage.xaml create mode 100644 components/Sizers/samples/SizerCursorPage.xaml.cs delete mode 100644 components/Sizers/samples/Sizers.md delete mode 100644 components/Sizers/samples/SizersCustomSample.xaml delete mode 100644 components/Sizers/samples/SizersCustomSample.xaml.cs delete mode 100644 components/Sizers/samples/SizersTemplatedSample.xaml delete mode 100644 components/Sizers/samples/SizersTemplatedSample.xaml.cs delete mode 100644 components/Sizers/samples/SizersTemplatedStyleCustomSample.xaml delete mode 100644 components/Sizers/samples/SizersTemplatedStyleCustomSample.xaml.cs delete mode 100644 components/Sizers/samples/SizersXbindBackedSample.xaml delete mode 100644 components/Sizers/samples/SizersXbindBackedSample.xaml.cs delete mode 100644 components/Sizers/samples/SizersXbindBackedStyleCustomSample.xaml delete mode 100644 components/Sizers/samples/SizersXbindBackedStyleCustomSample.xaml.cs create mode 100644 components/Sizers/src/ContentSizer/ContentSizer.Events.cs create mode 100644 components/Sizers/src/ContentSizer/ContentSizer.Properties.cs create mode 100644 components/Sizers/src/ContentSizer/ContentSizer.cs create mode 100644 components/Sizers/src/GridSplitter/GridSplitter.Data.cs create mode 100644 components/Sizers/src/GridSplitter/GridSplitter.Events.cs create mode 100644 components/Sizers/src/GridSplitter/GridSplitter.Helpers.cs create mode 100644 components/Sizers/src/GridSplitter/GridSplitter.Properties.cs create mode 100644 components/Sizers/src/GridSplitter/GridSplitter.cs create mode 100644 components/Sizers/src/PropertySizer/PropertySizer.Events.cs create mode 100644 components/Sizers/src/PropertySizer/PropertySizer.Properties.cs create mode 100644 components/Sizers/src/PropertySizer/PropertySizer.cs create mode 100644 components/Sizers/src/SizerAutomationPeer.cs create mode 100644 components/Sizers/src/SizerBase.Events.cs create mode 100644 components/Sizers/src/SizerBase.Helpers.cs create mode 100644 components/Sizers/src/SizerBase.Properties.cs create mode 100644 components/Sizers/src/SizerBase.cs create mode 100644 components/Sizers/src/SizerBase.xaml delete mode 100644 components/Sizers/src/Sizers.cs delete mode 100644 components/Sizers/src/SizersStyle_ClassicBinding.xaml delete mode 100644 components/Sizers/src/SizersStyle_xBind.xaml delete mode 100644 components/Sizers/src/SizersStyle_xBind.xaml.cs delete mode 100644 components/Sizers/src/Sizers_ClassicBinding.cs delete mode 100644 components/Sizers/src/Sizers_xBind.cs create mode 100644 components/Sizers/src/Strings/en-US/Resources.resw create mode 100644 components/Sizers/src/Toolkit/ConverterTools.cs create mode 100644 components/Sizers/src/Toolkit/DependencyObjectExtensions.cs create mode 100644 components/Sizers/src/Toolkit/FrameworkElementExtensions.Mouse.cs create mode 100644 components/Sizers/src/Toolkit/OrientationToObjectConverter.cs create mode 100644 components/Sizers/src/Toolkit/TypeToObjectConverter.cs create mode 100644 components/Sizers/tests/ExampleSizerBaseTestClass.cs delete mode 100644 components/Sizers/tests/ExampleSizersTestClass.cs delete mode 100644 components/Sizers/tests/ExampleSizersTestPage.xaml create mode 100644 components/Sizers/tests/PropertySizerTestInitialBinding.xaml rename components/Sizers/tests/{ExampleSizersTestPage.xaml.cs => PropertySizerTestInitialBinding.xaml.cs} (78%) diff --git a/components/Sizers/samples/ContentSizer.md b/components/Sizers/samples/ContentSizer.md new file mode 100644 index 00000000..6c206ee8 --- /dev/null +++ b/components/Sizers/samples/ContentSizer.md @@ -0,0 +1,26 @@ +--- +title: ContentSizer +author: mhawker +description: The ContentSizer is a control which can be used to resize any element, usually its parent. +keywords: ContentSizer, SizerBase, Control, Layout, Expander, Splitter +dev_langs: + - csharp +category: Controls +subcategory: Layout +discussion-id: 96 +issue-id: 101 +--- + +# ContentSizer + +The ContentSizer is a control which can be used to resize any element, usually its parent. If you are using a `Grid`, use [GridSplitter](GridSplitter.md) instead. + +The main use-case for a ContentSizer is to create an expandable shelf for your application. This allows the `Expander` itself to remember its opening/closing sizes. + +A GridSplitter would be insufficient as it would force the grid to remember the row size and maintain its position when the `Expander` collapses. + +> [!SAMPLE ContentSizerTopShelfPage] + +The following example shows how to use the ContentSizer to create a left-side shelf; however, this scenario can also be accomplished with a `GridSplitter`. + +> [!SAMPLE ContentSizerLeftShelfPage] diff --git a/components/Sizers/samples/ContentSizerLeftShelfPage.xaml b/components/Sizers/samples/ContentSizerLeftShelfPage.xaml new file mode 100644 index 00000000..7bb1de95 --- /dev/null +++ b/components/Sizers/samples/ContentSizerLeftShelfPage.xaml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + diff --git a/components/Sizers/samples/ContentSizerLeftShelfPage.xaml.cs b/components/Sizers/samples/ContentSizerLeftShelfPage.xaml.cs new file mode 100644 index 00000000..6c6a375a --- /dev/null +++ b/components/Sizers/samples/ContentSizerLeftShelfPage.xaml.cs @@ -0,0 +1,14 @@ +// 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 SizersExperiment.Samples; + +[ToolkitSample(id: nameof(ContentSizerLeftShelfPage), "Left-side Shelf", description: "Shows how to create an expandable shelf on the left-side of your app.")] +public sealed partial class ContentSizerLeftShelfPage : Page +{ + public ContentSizerLeftShelfPage() + { + this.InitializeComponent(); + } +} diff --git a/components/Sizers/samples/ContentSizerTopShelfPage.xaml b/components/Sizers/samples/ContentSizerTopShelfPage.xaml new file mode 100644 index 00000000..2f2b4b0c --- /dev/null +++ b/components/Sizers/samples/ContentSizerTopShelfPage.xaml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/components/Sizers/samples/ContentSizerTopShelfPage.xaml.cs b/components/Sizers/samples/ContentSizerTopShelfPage.xaml.cs new file mode 100644 index 00000000..3142311f --- /dev/null +++ b/components/Sizers/samples/ContentSizerTopShelfPage.xaml.cs @@ -0,0 +1,14 @@ +// 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 SizersExperiment.Samples; + +[ToolkitSample(id: nameof(ContentSizerTopShelfPage), "Top Shelf", description: "Shows how to create an expandable shelf on the top of your app.")] +public sealed partial class ContentSizerTopShelfPage : Page +{ + public ContentSizerTopShelfPage() + { + this.InitializeComponent(); + } +} diff --git a/components/Sizers/samples/GridSplitter.md b/components/Sizers/samples/GridSplitter.md new file mode 100644 index 00000000..0a5207c3 --- /dev/null +++ b/components/Sizers/samples/GridSplitter.md @@ -0,0 +1,21 @@ +--- +title: GridSplitter +author: mhawker +description:The GridSplitter control provides an easy-to-use Splitter that redistributes space between columns or rows of a Grid Control. +keywords: ContentSizer, SizerBase, Control, Layout, Expander +dev_langs: + - csharp +category: Controls +subcategory: Layout +discussion-id: 96 +issue-id: 101 +--- + +# GridSplitter + +The control automatically detects the targeted columns/rows to resize, while dragging the control it starts to resize the columns/rows and redistributes space between columns/rows, +you can manually specify the `ResizeDirection` (`Auto` / `Column` / `Row`) and the `ResizeBehavior` to select which columns/rows to resize. + +`GridSplitter` control will resize the targeted rows or columns + +> [!SAMPLE GridSplitterPage] diff --git a/components/Sizers/samples/GridSplitterPage.xaml b/components/Sizers/samples/GridSplitterPage.xaml new file mode 100644 index 00000000..2c3a2f60 --- /dev/null +++ b/components/Sizers/samples/GridSplitterPage.xaml @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/Sizers/samples/GridSplitterPage.xaml.cs b/components/Sizers/samples/GridSplitterPage.xaml.cs new file mode 100644 index 00000000..bbe748ef --- /dev/null +++ b/components/Sizers/samples/GridSplitterPage.xaml.cs @@ -0,0 +1,17 @@ +// 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 SizersExperiment.Samples; + +/// +/// An empty page that can be used on its own or navigated to within a Frame. +/// +[ToolkitSample(id: nameof(GridSplitterPage), "GridSplitter Example", description: "Splitter that redistributes space between columns or rows of a Grid Control")] +public sealed partial class GridSplitterPage : Page +{ + public GridSplitterPage() + { + this.InitializeComponent(); + } +} diff --git a/components/Sizers/samples/PropertySizer.md b/components/Sizers/samples/PropertySizer.md new file mode 100644 index 00000000..90c59c06 --- /dev/null +++ b/components/Sizers/samples/PropertySizer.md @@ -0,0 +1,24 @@ +--- +title: PropertySizer +author: mhawker +description: The PropertySizer is a control which can be used to manipulate the value of another double based property. +keywords: PropertySizer, SizerBase, Control, Layout, NavigationView, Splitter +dev_langs: + - csharp +category: Controls +subcategory: Layout +discussion-id: 96 +issue-id: 101 +--- + +# PropertySizer + +The PropertySizer 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 `Grid`, use `GridSplitter` instead. + +# Examples + +The main use-case is for `PropertySizer` to allow you to manipulate the `OpenPaneLength` property of a `NavigationView` control to create a user customizable size shelf. This is handy when using `NavigationView` with a tree of items that represents some project or folder structure for your application. + +Both `GridSplitter` and `ContentSizer` are insufficient as they would force the `NavigationView` to a specific size and not allow it to remember its size when it expands or collapses. + +> [!SAMPLE PropertySizerNavigationViewPage] diff --git a/components/Sizers/samples/PropertySizerNavigationViewPage.xaml b/components/Sizers/samples/PropertySizerNavigationViewPage.xaml new file mode 100644 index 00000000..b8f17c61 --- /dev/null +++ b/components/Sizers/samples/PropertySizerNavigationViewPage.xaml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/Sizers/samples/PropertySizerNavigationViewPage.xaml.cs b/components/Sizers/samples/PropertySizerNavigationViewPage.xaml.cs new file mode 100644 index 00000000..b46da254 --- /dev/null +++ b/components/Sizers/samples/PropertySizerNavigationViewPage.xaml.cs @@ -0,0 +1,14 @@ +// 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 SizersExperiment.Samples; + +[ToolkitSample(id: nameof(PropertySizerNavigationViewPage), "NavigationView Shelf", description: "Shows how to create an expandable shelf using a NavigationView and PropertySizer.")] +public sealed partial class PropertySizerNavigationViewPage : Page +{ + public PropertySizerNavigationViewPage() + { + this.InitializeComponent(); + } +} diff --git a/components/Sizers/samples/Sizers.Samples.csproj b/components/Sizers/samples/SizerBase.Samples.csproj similarity index 100% rename from components/Sizers/samples/Sizers.Samples.csproj rename to components/Sizers/samples/SizerBase.Samples.csproj diff --git a/components/Sizers/samples/SizerControls.md b/components/Sizers/samples/SizerControls.md new file mode 100644 index 00000000..fc3912c4 --- /dev/null +++ b/components/Sizers/samples/SizerControls.md @@ -0,0 +1,30 @@ +--- +title: Sizer Controls +author: mhawker +description: The Sizer controls allow users to resize various parts of your UI easily in a consistent fashion. +keywords: GridSplitter, ContentSizer, PropertySizer, SizerBase, Control, Layout, Expander, Grid, Splitter +dev_langs: + - csharp +category: Controls +subcategory: Layout +discussion-id: 96 +issue-id: 101 +--- + +# Sizer Controls + +The Sizer controls consist of the following: + +- GridSplitter +- ContentSizer +- PropertySizer + +They each provide an ability for your users to manipulate different parts of your UI experiences. + +This document provides information about common settings you can set on any of these controls. + +## Custom Mouse Cursor + +You may want to change the cursor that is shown when hovering over your element like this: + +> [!SAMPLE SizerCursorPage] diff --git a/components/Sizers/samples/SizerCursorPage.xaml b/components/Sizers/samples/SizerCursorPage.xaml new file mode 100644 index 00000000..d06b8ed3 --- /dev/null +++ b/components/Sizers/samples/SizerCursorPage.xaml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + diff --git a/components/Sizers/samples/SizerCursorPage.xaml.cs b/components/Sizers/samples/SizerCursorPage.xaml.cs new file mode 100644 index 00000000..b12501ea --- /dev/null +++ b/components/Sizers/samples/SizerCursorPage.xaml.cs @@ -0,0 +1,14 @@ +// 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 SizersExperiment.Samples; + +[ToolkitSample(id: nameof(SizerCursorPage), "Custom Mouse Cursor", description: "Shows how to change the cursor of a Sizer control.")] +public sealed partial class SizerCursorPage : Page +{ + public SizerCursorPage() + { + this.InitializeComponent(); + } +} diff --git a/components/Sizers/samples/Sizers.md b/components/Sizers/samples/Sizers.md deleted file mode 100644 index a6796af6..00000000 --- a/components/Sizers/samples/Sizers.md +++ /dev/null @@ -1,64 +0,0 @@ ---- -title: Sizers -author: githubaccount -description: TODO: Your experiment's description here -keywords: Sizers, Control, Layout -dev_langs: - - csharp -category: Controls -subcategory: Layout -discussion-id: 0 -issue-id: 0 ---- - - - - - - - - - -# Sizers - -TODO: Fill in information about this experiment and how to get started here... - -## Custom Control - -You can inherit from an existing component as well, like `Panel`, this example shows a control without a -XAML Style that will be more light-weight to consume by an app developer: - -> [!Sample SizersCustomSample] - -## Templated Controls - -The Toolkit is built with templated controls. This provides developers a flexible way to restyle components -easily while still inheriting the general functionality a control provides. The examples below show -how a component can use a default style and then get overridden by the end developer. - -TODO: Two types of templated control building methods are shown. Delete these if you're building a custom component. -Otherwise, pick one method for your component and delete the files related to the unchosen `_ClassicBinding` or `_xBind` -classes (and the custom non-suffixed one as well). Then, rename your component to just be your component name. - -The `_ClassicBinding` class shows the traditional method used to develop components with best practices. - -### Implict style - -> [!SAMPLE SizersTemplatedSample] - -### Custom style - -> [!SAMPLE SizersTemplatedStyleCustomSample] - -## Templated Controls with x:Bind - -This is an _experimental_ new way to define components which allows for the use of x:Bind within the style. - -### Implict style - -> [!SAMPLE SizersXbindBackedSample] - -### Custom style - -> [!SAMPLE SizersXbindBackedStyleCustomSample] - diff --git a/components/Sizers/samples/SizersCustomSample.xaml b/components/Sizers/samples/SizersCustomSample.xaml deleted file mode 100644 index ca8a5adc..00000000 --- a/components/Sizers/samples/SizersCustomSample.xaml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/components/Sizers/samples/SizersCustomSample.xaml.cs b/components/Sizers/samples/SizersCustomSample.xaml.cs deleted file mode 100644 index 8530818f..00000000 --- a/components/Sizers/samples/SizersCustomSample.xaml.cs +++ /dev/null @@ -1,30 +0,0 @@ -// 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.Controls; - -namespace SizersExperiment.Samples; - -/// -/// An example sample page of a custom control inheriting from Panel. -/// -[ToolkitSampleTextOption("TitleText", "This is a title", Title = "Input the text")] -[ToolkitSampleMultiChoiceOption("LayoutOrientation", "Horizontal", "Vertical", Title = "Orientation")] - -[ToolkitSample(id: nameof(SizersCustomSample), "Custom control", description: $"A sample for showing how to create and use a {nameof(Sizers)} custom control.")] -public sealed partial class SizersCustomSample : Page -{ - public SizersCustomSample() - { - this.InitializeComponent(); - } - - // TODO: See https://github.com/CommunityToolkit/Labs-Windows/issues/149 - public static Orientation ConvertStringToOrientation(string orientation) => orientation switch - { - "Vertical" => Orientation.Vertical, - "Horizontal" => Orientation.Horizontal, - _ => throw new System.NotImplementedException(), - }; -} diff --git a/components/Sizers/samples/SizersTemplatedSample.xaml b/components/Sizers/samples/SizersTemplatedSample.xaml deleted file mode 100644 index a4b96fe4..00000000 --- a/components/Sizers/samples/SizersTemplatedSample.xaml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - diff --git a/components/Sizers/samples/SizersTemplatedSample.xaml.cs b/components/Sizers/samples/SizersTemplatedSample.xaml.cs deleted file mode 100644 index daa7febf..00000000 --- a/components/Sizers/samples/SizersTemplatedSample.xaml.cs +++ /dev/null @@ -1,21 +0,0 @@ -// 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 SizersExperiment.Samples; - -[ToolkitSampleBoolOption("IsTextVisible", true, Title = "IsVisible")] -// Single values without a colon are used for both label and value. -// To provide a different label for the value, separate with a colon surrounded by a single space on both sides ("label : value"). -[ToolkitSampleNumericOption("TextSize", 12, 8, 48, 2, false, Title = "FontSize")] -[ToolkitSampleMultiChoiceOption("TextFontFamily", "Segoe UI", "Arial", "Consolas", Title = "Font family")] -[ToolkitSampleMultiChoiceOption("TextForeground", "Teal : #0ddc8c", "Sand : #e7a676", "Dull green : #5d7577", Title = "Text foreground")] - -[ToolkitSample(id: nameof(SizersTemplatedSample), "Templated control", description: "A sample for showing how to create and use a templated control.")] -public sealed partial class SizersTemplatedSample : Page -{ - public SizersTemplatedSample() - { - this.InitializeComponent(); - } -} diff --git a/components/Sizers/samples/SizersTemplatedStyleCustomSample.xaml b/components/Sizers/samples/SizersTemplatedStyleCustomSample.xaml deleted file mode 100644 index 08a95cb6..00000000 --- a/components/Sizers/samples/SizersTemplatedStyleCustomSample.xaml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/components/Sizers/samples/SizersTemplatedStyleCustomSample.xaml.cs b/components/Sizers/samples/SizersTemplatedStyleCustomSample.xaml.cs deleted file mode 100644 index 09b36297..00000000 --- a/components/Sizers/samples/SizersTemplatedStyleCustomSample.xaml.cs +++ /dev/null @@ -1,21 +0,0 @@ -// 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 SizersExperiment.Samples; - -[ToolkitSampleBoolOption("IsTextVisible", true, Title = "IsVisible")] -// Single values without a colon are used for both label and value. -// To provide a different label for the value, separate with a colon surrounded by a single space on both sides ("label : value"). -[ToolkitSampleNumericOption("TextSize", 12, 8, 48, 2, true, Title = "FontSize")] -[ToolkitSampleMultiChoiceOption("TextFontFamily", "Segoe UI", "Arial", "Consolas", Title = "Font family")] -[ToolkitSampleMultiChoiceOption("TextForeground", "Teal : #0ddc8c", "Sand : #e7a676", "Dull green : #5d7577", Title = "Text foreground")] - -[ToolkitSample(id: nameof(SizersTemplatedStyleCustomSample), "Templated control (restyled)", description: "A sample for showing how to create a use and templated control with a custom style.")] -public sealed partial class SizersTemplatedStyleCustomSample : Page -{ - public SizersTemplatedStyleCustomSample() - { - this.InitializeComponent(); - } -} diff --git a/components/Sizers/samples/SizersXbindBackedSample.xaml b/components/Sizers/samples/SizersXbindBackedSample.xaml deleted file mode 100644 index 643d151e..00000000 --- a/components/Sizers/samples/SizersXbindBackedSample.xaml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - diff --git a/components/Sizers/samples/SizersXbindBackedSample.xaml.cs b/components/Sizers/samples/SizersXbindBackedSample.xaml.cs deleted file mode 100644 index f4144634..00000000 --- a/components/Sizers/samples/SizersXbindBackedSample.xaml.cs +++ /dev/null @@ -1,21 +0,0 @@ -// 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 SizersExperiment.Samples; - -[ToolkitSampleBoolOption("IsTextVisible", true, Title = "IsVisible")] -// Single values without a colon are used for both label and value. -// To provide a different label for the value, separate with a colon surrounded by a single space on both sides ("label : value"). -[ToolkitSampleNumericOption("TextSize", 12, 8, 48, 2, false, Title = "FontSize")] -[ToolkitSampleMultiChoiceOption("TextFontFamily", "Segoe UI", "Arial", "Consolas", Title = "Font family")] -[ToolkitSampleMultiChoiceOption("TextForeground", "Teal : #0ddc8c", "Sand : #e7a676", "Dull green : #5d7577", Title = "Text foreground")] - -[ToolkitSample(id: nameof(SizersXbindBackedSample), "Backed templated control", description: "A sample for showing how to create and use a templated control with a backed resource dictionary.")] -public sealed partial class SizersXbindBackedSample : Page -{ - public SizersXbindBackedSample() - { - this.InitializeComponent(); - } -} diff --git a/components/Sizers/samples/SizersXbindBackedStyleCustomSample.xaml b/components/Sizers/samples/SizersXbindBackedStyleCustomSample.xaml deleted file mode 100644 index 2f1b4fa3..00000000 --- a/components/Sizers/samples/SizersXbindBackedStyleCustomSample.xaml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/components/Sizers/samples/SizersXbindBackedStyleCustomSample.xaml.cs b/components/Sizers/samples/SizersXbindBackedStyleCustomSample.xaml.cs deleted file mode 100644 index 612c34e5..00000000 --- a/components/Sizers/samples/SizersXbindBackedStyleCustomSample.xaml.cs +++ /dev/null @@ -1,21 +0,0 @@ -// 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 SizersExperiment.Samples; - -[ToolkitSampleBoolOption("IsTextVisible", true, Title = "IsVisible")] -// Single values without a colon are used for both label and value. -// To provide a different label for the value, separate with a colon surrounded by a single space on both sides ("label : value"). -[ToolkitSampleNumericOption("TextSize", 12, 8, 48, 2, true, Title = "FontSize")] -[ToolkitSampleMultiChoiceOption("TextFontFamily", "Segoe UI", "Arial", "Consolas", Title = "Font family")] -[ToolkitSampleMultiChoiceOption("TextForeground", "Teal : #0ddc8c", "Sand : #e7a676", "Dull green : #5d7577", Title = "Text foreground")] - -[ToolkitSample(id: nameof(SizersXbindBackedStyleCustomSample), "Backed templated control (restyled)", description: "A sample for showing how to create and use a templated control with a backed resource dictionary and a custom style.")] -public sealed partial class SizersXbindBackedStyleCustomSample : Page -{ - public SizersXbindBackedStyleCustomSample() - { - this.InitializeComponent(); - } -} diff --git a/components/Sizers/src/AdditionalAssemblyInfo.cs b/components/Sizers/src/AdditionalAssemblyInfo.cs index aa283849..dc2a92d0 100644 --- a/components/Sizers/src/AdditionalAssemblyInfo.cs +++ b/components/Sizers/src/AdditionalAssemblyInfo.cs @@ -7,7 +7,7 @@ // These `InternalsVisibleTo` calls are intended to make it easier for // for any internal code to be testable in all the different test projects // used with the Labs infrastructure. -[assembly: InternalsVisibleTo("Sizers.Tests.Uwp")] -[assembly: InternalsVisibleTo("Sizers.Tests.WinAppSdk")] +[assembly: InternalsVisibleTo("SizerBase.Tests.Uwp")] +[assembly: InternalsVisibleTo("SizerBase.Tests.WinAppSdk")] [assembly: InternalsVisibleTo("CommunityToolkit.Tests.Uwp")] [assembly: InternalsVisibleTo("CommunityToolkit.Tests.WinAppSdk")] diff --git a/components/Sizers/src/CommunityToolkit.WinUI.Controls.Sizers.csproj b/components/Sizers/src/CommunityToolkit.WinUI.Controls.Sizers.csproj index 96524544..94d9f669 100644 --- a/components/Sizers/src/CommunityToolkit.WinUI.Controls.Sizers.csproj +++ b/components/Sizers/src/CommunityToolkit.WinUI.Controls.Sizers.csproj @@ -1,8 +1,8 @@ Sizers - This package contains Sizers. - 0.0.1 + This package contains SizerBase. + 8.0.0-beta.1 CommunityToolkit.WinUI.Controls.SizersRns diff --git a/components/Sizers/src/ContentSizer/ContentSizer.Events.cs b/components/Sizers/src/ContentSizer/ContentSizer.Events.cs new file mode 100644 index 00000000..138dfea8 --- /dev/null +++ b/components/Sizers/src/ContentSizer/ContentSizer.Events.cs @@ -0,0 +1,74 @@ +// 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.Controls.SizerBaseLocal; + +namespace CommunityToolkit.WinUI.Controls; + +// 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/components/Sizers/src/ContentSizer/ContentSizer.Properties.cs b/components/Sizers/src/ContentSizer/ContentSizer.Properties.cs new file mode 100644 index 00000000..4bcf58aa --- /dev/null +++ b/components/Sizers/src/ContentSizer/ContentSizer.Properties.cs @@ -0,0 +1,62 @@ +// 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.WinUI.Controls; + +// 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/components/Sizers/src/ContentSizer/ContentSizer.cs b/components/Sizers/src/ContentSizer/ContentSizer.cs new file mode 100644 index 00000000..3de9fb05 --- /dev/null +++ b/components/Sizers/src/ContentSizer/ContentSizer.cs @@ -0,0 +1,12 @@ +// 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.WinUI.Controls; + +/// +/// 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/components/Sizers/src/GridSplitter/GridSplitter.Data.cs b/components/Sizers/src/GridSplitter/GridSplitter.Data.cs new file mode 100644 index 00000000..d525d1d4 --- /dev/null +++ b/components/Sizers/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.WinUI.Controls; + +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/components/Sizers/src/GridSplitter/GridSplitter.Events.cs b/components/Sizers/src/GridSplitter/GridSplitter.Events.cs new file mode 100644 index 00000000..508472f7 --- /dev/null +++ b/components/Sizers/src/GridSplitter/GridSplitter.Events.cs @@ -0,0 +1,169 @@ +// 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.WinUI.Controls; + +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/components/Sizers/src/GridSplitter/GridSplitter.Helpers.cs b/components/Sizers/src/GridSplitter/GridSplitter.Helpers.cs new file mode 100644 index 00000000..361fd77f --- /dev/null +++ b/components/Sizers/src/GridSplitter/GridSplitter.Helpers.cs @@ -0,0 +1,245 @@ +// 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.WinUI.Controls; + +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/components/Sizers/src/GridSplitter/GridSplitter.Properties.cs b/components/Sizers/src/GridSplitter/GridSplitter.Properties.cs new file mode 100644 index 00000000..ec211c9e --- /dev/null +++ b/components/Sizers/src/GridSplitter/GridSplitter.Properties.cs @@ -0,0 +1,78 @@ +// 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.WinUI.Controls; + +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/components/Sizers/src/GridSplitter/GridSplitter.cs b/components/Sizers/src/GridSplitter/GridSplitter.cs new file mode 100644 index 00000000..b52dfeea --- /dev/null +++ b/components/Sizers/src/GridSplitter/GridSplitter.cs @@ -0,0 +1,145 @@ +// 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.WinUI.Controls; + +/// +/// 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/components/Sizers/src/PropertySizer/PropertySizer.Events.cs b/components/Sizers/src/PropertySizer/PropertySizer.Events.cs new file mode 100644 index 00000000..85d5bf07 --- /dev/null +++ b/components/Sizers/src/PropertySizer/PropertySizer.Events.cs @@ -0,0 +1,66 @@ +// 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.WinUI.Controls; + +// 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/components/Sizers/src/PropertySizer/PropertySizer.Properties.cs b/components/Sizers/src/PropertySizer/PropertySizer.Properties.cs new file mode 100644 index 00000000..25591d36 --- /dev/null +++ b/components/Sizers/src/PropertySizer/PropertySizer.Properties.cs @@ -0,0 +1,75 @@ +// 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.WinUI.Controls; + +// 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/components/Sizers/src/PropertySizer/PropertySizer.cs b/components/Sizers/src/PropertySizer/PropertySizer.cs new file mode 100644 index 00000000..db6bd140 --- /dev/null +++ b/components/Sizers/src/PropertySizer/PropertySizer.cs @@ -0,0 +1,12 @@ +// 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.WinUI.Controls; + +/// +/// 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/components/Sizers/src/SizerAutomationPeer.cs b/components/Sizers/src/SizerAutomationPeer.cs new file mode 100644 index 00000000..16462c7f --- /dev/null +++ b/components/Sizers/src/SizerAutomationPeer.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. + +namespace CommunityToolkit.WinUI.Controls.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/components/Sizers/src/SizerBase.Events.cs b/components/Sizers/src/SizerBase.Events.cs new file mode 100644 index 00000000..8b7ae4a9 --- /dev/null +++ b/components/Sizers/src/SizerBase.Events.cs @@ -0,0 +1,178 @@ +// 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.WinUI.Controls; + +/// +/// 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 !HAS_UNO + if (this.FlowDirection == FlowDirection.RightToLeft) + { + horizontalChange *= -1; + } +#endif + + 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 !HAS_UNO + if (this.FlowDirection == FlowDirection.RightToLeft) + { + horizontalChange *= -1; + } +#endif + + 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/components/Sizers/src/SizerBase.Helpers.cs b/components/Sizers/src/SizerBase.Helpers.cs new file mode 100644 index 00000000..e87e09e8 --- /dev/null +++ b/components/Sizers/src/SizerBase.Helpers.cs @@ -0,0 +1,69 @@ +// 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.WinUI.Controls; + +/// +/// 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/components/Sizers/src/SizerBase.Properties.cs b/components/Sizers/src/SizerBase.Properties.cs new file mode 100644 index 00000000..c307e400 --- /dev/null +++ b/components/Sizers/src/SizerBase.Properties.cs @@ -0,0 +1,128 @@ +// 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 CursorEnum = Windows.UI.Core.CoreCursorType; +#else +using Microsoft.UI.Input; +using CursorEnum = Microsoft.UI.Input.InputSystemCursorShape; +#endif + +namespace CommunityToolkit.WinUI.Controls; + +/// +/// Properties for +/// +public partial class SizerBase : Control +{ + /// + /// Gets or sets the cursor to use when hovering over the gripper bar. If left as null, the control will manage the cursor automatically based on the property value. + /// + public CursorEnum Cursor + { + get { return (CursorEnum)GetValue(CursorProperty); } + set { SetValue(CursorProperty, value); } + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty CursorProperty = + DependencyProperty.Register(nameof(Cursor), typeof(CursorEnum), typeof(SizerBase), new PropertyMetadata(null, OnOrientationPropertyChanged)); + + /// + /// Gets or sets the incremental amount of change for dragging 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) + { + CursorEnum cursorToUse = gripper.Orientation == Orientation.Vertical ? CursorEnum.SizeWestEast : CursorEnum.SizeNorthSouth; + + // See if there's been a cursor override, otherwise we'll pick + var cursor = gripper.ReadLocalValue(CursorProperty); + if (cursor == DependencyProperty.UnsetValue || cursor == null) + { + cursor = cursorToUse; + + // On UWP, we use the extension in XAML to control this behavior, + // so we'll update it here (and maintain binding). + // We'll keep it in-sync to maintain behavior for WinUI 3 as well. + gripper.SetValue(CursorProperty, cursor); + + // We return here, as the Cursor will trigger this function again anyway to set for WinUI 3 + return; + } + + // TODO: [UNO] Only supported on certain platforms + // See ProtectedCursor here: https://github.com/unoplatform/uno/blob/3fe3862b270b99dbec4d830b547942af61b1a1d9/src/Uno.UI/UI/Xaml/UIElement.cs#L1015-L1023 +#if WINAPPSDK && !HAS_UNO + // Need to wait until we're at least applying template step of loading before setting Cursor + // See https://github.com/microsoft/microsoft-ui-xaml/issues/7062 + if (gripper._applyingTemplate && + cursor is CursorEnum cursorToSet && + (gripper.ProtectedCursor == null || + (gripper.ProtectedCursor is InputSystemCursor current && + current.CursorShape != cursorToSet))) + { + gripper.ProtectedCursor = InputSystemCursor.Create(cursorToSet); + } +#endif + } + } +} diff --git a/components/Sizers/src/SizerBase.cs b/components/Sizers/src/SizerBase.cs new file mode 100644 index 00000000..f1f1071b --- /dev/null +++ b/components/Sizers/src/SizerBase.cs @@ -0,0 +1,130 @@ +// 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.Controls.Automation.Peers; + +namespace CommunityToolkit.WinUI.Controls; + +/// +/// 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 +{ + /// + /// 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); + } + +// On Uno the ProtectedCursor isn't supported yet, so we don't need this value. +#if WINAPPSDK && !HAS_UNO + // Used to track when we're in the OnApplyTemplateStep to change ProtectedCursor value. + private bool _applyingTemplate = false; +#endif + + /// + 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!); +#if WINAPPSDK && !HAS_UNO + // On WinAppSDK, we'll trigger this to setup the initial ProtectedCursor value. + _applyingTemplate = true; +#endif + // On UWP, we'll check the current Orientation and set the Cursor property to use here still. + OnOrientationPropertyChanged(this, null!); + } + + private void SizerBase_Loaded(object sender, RoutedEventArgs e) + { + Loaded -= SizerBase_Loaded; + + OnLoaded(e); + } +} diff --git a/components/Sizers/src/SizerBase.xaml b/components/Sizers/src/SizerBase.xaml new file mode 100644 index 00000000..1fe4942d --- /dev/null +++ b/components/Sizers/src/SizerBase.xaml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/Sizers/src/Sizers.cs b/components/Sizers/src/Sizers.cs deleted file mode 100644 index a788ee09..00000000 --- a/components/Sizers/src/Sizers.cs +++ /dev/null @@ -1,108 +0,0 @@ -// 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.WinUI.Controls; - -/// -/// This is an example control based off of the BoxPanel sample here: https://docs.microsoft.com/windows/apps/design/layout/boxpanel-example-custom-panel. If you need this similar sort of layout component for an application, see UniformGrid in the Toolkit. -/// It is provided as an example of how to inherit from another control like . -/// You can choose to start here or from the or example components. Remove unused components and rename as appropriate. -/// -public partial class Sizers : Panel -{ - /// - /// Identifies the property. - /// - public static readonly DependencyProperty OrientationProperty = - DependencyProperty.Register(nameof(Orientation), typeof(Orientation), typeof(Sizers), new PropertyMetadata(null, OnOrientationChanged)); - - /// - /// Gets the preference of the rows/columns when there are a non-square number of children. Defaults to Vertical. - /// - public Orientation Orientation - { - get { return (Orientation)GetValue(OrientationProperty); } - set { SetValue(OrientationProperty, value); } - } - - // Invalidate our layout when the property changes. - private static void OnOrientationChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs args) - { - if (dependencyObject is Sizers panel) - { - panel.InvalidateMeasure(); - } - } - - // Store calculations we want to use between the Measure and Arrange methods. - int _columnCount; - double _cellWidth, _cellHeight; - - protected override Size MeasureOverride(Size availableSize) - { - // Determine the square that can contain this number of items. - var maxrc = (int)Math.Ceiling(Math.Sqrt(Children.Count)); - // Get an aspect ratio from availableSize, decides whether to trim row or column. - var aspectratio = availableSize.Width / availableSize.Height; - if (Orientation == Orientation.Vertical) { aspectratio = 1 / aspectratio; } - - int rowcount; - - // Now trim this square down to a rect, many times an entire row or column can be omitted. - if (aspectratio > 1) - { - rowcount = maxrc; - _columnCount = (maxrc > 2 && Children.Count <= maxrc * (maxrc - 1)) ? maxrc - 1 : maxrc; - } - else - { - rowcount = (maxrc > 2 && Children.Count <= maxrc * (maxrc - 1)) ? maxrc - 1 : maxrc; - _columnCount = maxrc; - } - - // Now that we have a column count, divide available horizontal, that's our cell width. - _cellWidth = (int)Math.Floor(availableSize.Width / _columnCount); - // Next get a cell height, same logic of dividing available vertical by rowcount. - _cellHeight = Double.IsInfinity(availableSize.Height) ? Double.PositiveInfinity : availableSize.Height / rowcount; - - double maxcellheight = 0; - - foreach (UIElement child in Children) - { - child.Measure(new Size(_cellWidth, _cellHeight)); - maxcellheight = (child.DesiredSize.Height > maxcellheight) ? child.DesiredSize.Height : maxcellheight; - } - - return LimitUnboundedSize(availableSize, maxcellheight); - } - - // This method limits the panel height when no limit is imposed by the panel's parent. - // That can happen to height if the panel is close to the root of main app window. - // In this case, base the height of a cell on the max height from desired size - // and base the height of the panel on that number times the #rows. - Size LimitUnboundedSize(Size input, double maxcellheight) - { - if (Double.IsInfinity(input.Height)) - { - input.Height = maxcellheight * _columnCount; - _cellHeight = maxcellheight; - } - return input; - } - - protected override Size ArrangeOverride(Size finalSize) - { - int count = 1; - double x, y; - foreach (UIElement child in Children) - { - x = (count - 1) % _columnCount * _cellWidth; - y = ((int)(count - 1) / _columnCount) * _cellHeight; - Point anchorPoint = new Point(x, y); - child.Arrange(new Rect(anchorPoint, child.DesiredSize)); - count++; - } - return finalSize; - } -} diff --git a/components/Sizers/src/SizersStyle_ClassicBinding.xaml b/components/Sizers/src/SizersStyle_ClassicBinding.xaml deleted file mode 100644 index c613a591..00000000 --- a/components/Sizers/src/SizersStyle_ClassicBinding.xaml +++ /dev/null @@ -1,62 +0,0 @@ - - - - - - - - - - - - - - - - 4,4,4,4 - - - - - - - - diff --git a/components/Sizers/src/SizersStyle_xBind.xaml b/components/Sizers/src/SizersStyle_xBind.xaml deleted file mode 100644 index 840b5c58..00000000 --- a/components/Sizers/src/SizersStyle_xBind.xaml +++ /dev/null @@ -1,69 +0,0 @@ - - - - - - - - - - - - - - - - 4,4,4,4 - - - - - - - - diff --git a/components/Sizers/src/SizersStyle_xBind.xaml.cs b/components/Sizers/src/SizersStyle_xBind.xaml.cs deleted file mode 100644 index b9d217e0..00000000 --- a/components/Sizers/src/SizersStyle_xBind.xaml.cs +++ /dev/null @@ -1,20 +0,0 @@ -// 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.WinUI.Controls; - -/// -/// Backing code for this resource dictionary. -/// -public sealed partial class SizersStyle_xBind : ResourceDictionary -{ - // NOTICE - // This file only exists to enable x:Bind in the resource dictionary. - // Do not add code here. - // Instead, add code-behind to your templated control. - public SizersStyle_xBind() - { - this.InitializeComponent(); - } -} diff --git a/components/Sizers/src/Sizers_ClassicBinding.cs b/components/Sizers/src/Sizers_ClassicBinding.cs deleted file mode 100644 index 8022f147..00000000 --- a/components/Sizers/src/Sizers_ClassicBinding.cs +++ /dev/null @@ -1,94 +0,0 @@ -// 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.WinUI.Controls; - -/// -/// An example templated control. -/// -[TemplatePart(Name = nameof(PART_HelloWorld), Type = typeof(TextBlock))] -public partial class Sizers_ClassicBinding : Control -{ - /// - /// Creates a new instance of the class. - /// - public Sizers_ClassicBinding() - { - this.DefaultStyleKey = typeof(Sizers_ClassicBinding); - } - - /// - /// The primary text block that displays "Hello world". - /// - protected TextBlock? PART_HelloWorld { get; private set; } - - /// - protected override void OnApplyTemplate() - { - base.OnApplyTemplate(); - - // Detach all attached events when a new template is applied. - if (PART_HelloWorld is not null) - { - PART_HelloWorld.PointerEntered -= Element_PointerEntered; - } - - // Attach events when the template is applied and the control is loaded. - PART_HelloWorld = GetTemplateChild(nameof(PART_HelloWorld)) as TextBlock; - - if (PART_HelloWorld is not null) - { - PART_HelloWorld.PointerEntered += Element_PointerEntered; - } - } - - /// - /// The backing for the property. - /// - public static readonly DependencyProperty ItemPaddingProperty = DependencyProperty.Register( - nameof(ItemPadding), - typeof(Thickness), - typeof(Sizers_ClassicBinding), - new PropertyMetadata(defaultValue: new Thickness(0))); - - /// - /// The backing for the property. - /// - public static readonly DependencyProperty MyPropertyProperty = DependencyProperty.Register( - nameof(MyProperty), - typeof(string), - typeof(Sizers_ClassicBinding), - new PropertyMetadata(defaultValue: string.Empty, (d, e) => ((Sizers_ClassicBinding)d).OnMyPropertyChanged((string)e.OldValue, (string)e.NewValue))); - - /// - /// Gets or sets an example string. A basic DependencyProperty example. - /// - public string MyProperty - { - get => (string)GetValue(MyPropertyProperty); - set => SetValue(MyPropertyProperty, value); - } - - /// - /// Gets or sets a padding for an item. A basic DependencyProperty example. - /// - public Thickness ItemPadding - { - get => (Thickness)GetValue(ItemPaddingProperty); - set => SetValue(ItemPaddingProperty, value); - } - - protected virtual void OnMyPropertyChanged(string oldValue, string newValue) - { - // Do something with the changed value. - } - - public void Element_PointerEntered(object sender, PointerRoutedEventArgs e) - { - if (sender is TextBlock text) - { - text.Opacity = 1; - } - } -} diff --git a/components/Sizers/src/Sizers_xBind.cs b/components/Sizers/src/Sizers_xBind.cs deleted file mode 100644 index 5dc2c552..00000000 --- a/components/Sizers/src/Sizers_xBind.cs +++ /dev/null @@ -1,71 +0,0 @@ -// 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.WinUI.Controls; - -/// -/// An example templated control. -/// -public partial class Sizers_xBind: Control -{ - /// - /// Creates a new instance of the class. - /// - public Sizers_xBind() - { - this.DefaultStyleKey = typeof(Sizers_xBind); - - // Allows directly using this control as the x:DataType in the template. - this.DataContext = this; - } - - /// - /// The backing for the property. - /// - public static readonly DependencyProperty ItemPaddingProperty = DependencyProperty.Register( - nameof(ItemPadding), - typeof(Thickness), - typeof(Sizers_xBind), - new PropertyMetadata(defaultValue: new Thickness(0))); - - /// - /// The backing for the property. - /// - public static readonly DependencyProperty MyPropertyProperty = DependencyProperty.Register( - nameof(MyProperty), - typeof(string), - typeof(Sizers_xBind), - new PropertyMetadata(defaultValue: string.Empty, (d, e) => ((Sizers_xBind)d).OnMyPropertyChanged((string)e.OldValue, (string)e.NewValue))); - - /// - /// Gets or sets an example string. A basic DependencyProperty example. - /// - public string MyProperty - { - get => (string)GetValue(MyPropertyProperty); - set => SetValue(MyPropertyProperty, value); - } - - /// - /// Gets or sets a padding for an item. A basic DependencyProperty example. - /// - public Thickness ItemPadding - { - get => (Thickness)GetValue(ItemPaddingProperty); - set => SetValue(ItemPaddingProperty, value); - } - - protected virtual void OnMyPropertyChanged(string oldValue, string newValue) - { - // Do something with the changed value. - } - - public void Element_PointerEntered(object sender, PointerRoutedEventArgs e) - { - if (sender is TextBlock text) - { - text.Opacity = 1; - } - } -} diff --git a/components/Sizers/src/Strings/en-US/Resources.resw b/components/Sizers/src/Strings/en-US/Resources.resw new file mode 100644 index 00000000..3e0ca58b --- /dev/null +++ b/components/Sizers/src/Strings/en-US/Resources.resw @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Sizer + Narrator Resource for SizerBase controls and similar + + \ No newline at end of file diff --git a/components/Sizers/src/Themes/Generic.xaml b/components/Sizers/src/Themes/Generic.xaml index d9dfe7ea..87d3e588 100644 --- a/components/Sizers/src/Themes/Generic.xaml +++ b/components/Sizers/src/Themes/Generic.xaml @@ -1,10 +1,7 @@ - - + + - - + - diff --git a/components/Sizers/src/Toolkit/ConverterTools.cs b/components/Sizers/src/Toolkit/ConverterTools.cs new file mode 100644 index 00000000..c32869b1 --- /dev/null +++ b/components/Sizers/src/Toolkit/ConverterTools.cs @@ -0,0 +1,45 @@ +// 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.WinUI.Controls.SizerBaseLocal; + +/// +/// Static class used to provide internal tools +/// +internal static class ConverterTools +{ + /// + /// Helper method to safely cast an object to a boolean + /// + /// Parameter to cast to a boolean + /// Bool value or false if cast failed + internal static bool TryParseBool(object parameter) + { + var parsed = false; + if (parameter != null) + { + bool.TryParse(parameter.ToString(), out parsed); + } + + return parsed; + } + + /// + /// Helper method to convert a value from a source type to a target type. + /// + /// The value to convert + /// The target type + /// The converted value + internal static object Convert(object value, Type targetType) + { + if (targetType.IsInstanceOfType(value)) + { + return value; + } + else + { + return XamlBindingHelper.ConvertValue(targetType, value); + } + } +} diff --git a/components/Sizers/src/Toolkit/DependencyObjectExtensions.cs b/components/Sizers/src/Toolkit/DependencyObjectExtensions.cs new file mode 100644 index 00000000..63b731e7 --- /dev/null +++ b/components/Sizers/src/Toolkit/DependencyObjectExtensions.cs @@ -0,0 +1,225 @@ +// 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. + +// We want this to be private/local to our component. +namespace CommunityToolkit.WinUI.Controls.SizerBaseLocal; + +//// 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/components/Sizers/src/Toolkit/FrameworkElementExtensions.Mouse.cs b/components/Sizers/src/Toolkit/FrameworkElementExtensions.Mouse.cs new file mode 100644 index 00000000..fa852c6a --- /dev/null +++ b/components/Sizers/src/Toolkit/FrameworkElementExtensions.Mouse.cs @@ -0,0 +1,113 @@ +// 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; + +namespace CommunityToolkit.WinUI.Controls.SizerBaseLocal; + +/// +public static partial class FrameworkElementExtensions +{ + private static readonly object _cursorLock = new object(); + private static readonly CoreCursor _defaultCursor = new CoreCursor(CoreCursorType.Arrow, 1); + private static readonly Dictionary _cursors = + new Dictionary { { CoreCursorType.Arrow, _defaultCursor } }; + + /// + /// Dependency property for specifying the target to be shown + /// over the target . + /// + public static readonly DependencyProperty CursorProperty = + DependencyProperty.RegisterAttached("Cursor", typeof(CoreCursorType), typeof(FrameworkElementExtensions), new PropertyMetadata(CoreCursorType.Arrow, CursorChanged)); + + /// + /// Set the target . + /// + /// Object where the selector cursor type should be shown. + /// Target cursor type value. + public static void SetCursor(FrameworkElement element, CoreCursorType value) + { + element.SetValue(CursorProperty, value); + } + + /// + /// Get the current . + /// + /// Object where the selector cursor type should be shown. + /// Cursor type set on target element. + public static CoreCursorType GetCursor(FrameworkElement element) + { + return (CoreCursorType)element.GetValue(CursorProperty); + } + + private static void CursorChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + // TODO: How do we want to indicate this isn't supported on the WinAppSDK? +#if !WINAPPSDK + var element = d as FrameworkElement; + if (element == null) + { + throw new NullReferenceException(nameof(element)); + } + + var value = (CoreCursorType)e.NewValue; + + // lock ensures CoreCursor creation and event handlers attachment/detachment is atomic + lock (_cursorLock) + { + if (!_cursors.ContainsKey(value)) + { + _cursors[value] = new CoreCursor(value, 1); + } + + // make sure event handlers are not attached twice to element + element.PointerEntered -= Element_PointerEntered; + element.PointerEntered += Element_PointerEntered; + element.PointerExited -= Element_PointerExited; + element.PointerExited += Element_PointerExited; + element.Unloaded -= ElementOnUnloaded; + element.Unloaded += ElementOnUnloaded; + } +#endif + } + +#if !WINAPPSDK + private static void Element_PointerEntered(object sender, PointerRoutedEventArgs e) + { + // TODO: [UNO] Only supported on certain platforms + // See PointerCursor here: https://github.com/unoplatform/uno/blob/3fe3862b270b99dbec4d830b547942af61b1a1d9/src/Uno.UWP/UI/Core/CoreWindow.cs#L71-L77 +#if NETFX_CORE || WASM || __MACOS__ || __SKIA__ + CoreCursorType cursor = GetCursor((FrameworkElement)sender); + Window.Current.CoreWindow.PointerCursor = _cursors[cursor]; +#endif + } + + private static void Element_PointerExited(object sender, PointerRoutedEventArgs e) + { +#if NETFX_CORE || WASM || __MACOS__ || __SKIA__ + // when exiting change the cursor to the target Mouse.Cursor value of the new element + CoreCursor cursor; + if (sender != e.OriginalSource && e.OriginalSource is FrameworkElement newElement) + { + cursor = _cursors[GetCursor(newElement)]; + } + else + { + cursor = _defaultCursor; + } + + Window.Current.CoreWindow.PointerCursor = cursor; +#endif + } + + private static void ElementOnUnloaded(object sender, RoutedEventArgs routedEventArgs) + { +#if NETFX_CORE || __WASM__ || __MACOS__ || __SKIA__ + // when the element is programatically unloaded, reset the cursor back to default + // this is necessary when click triggers immediate change in layout and PointerExited is not called + Window.Current.CoreWindow.PointerCursor = _defaultCursor; +#endif + } +#endif +} diff --git a/components/Sizers/src/Toolkit/OrientationToObjectConverter.cs b/components/Sizers/src/Toolkit/OrientationToObjectConverter.cs new file mode 100644 index 00000000..b668362b --- /dev/null +++ b/components/Sizers/src/Toolkit/OrientationToObjectConverter.cs @@ -0,0 +1,76 @@ +// 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.WinUI.Controls.SizerBaseLocal; + +/// +/// This class returns a value depending on the of the value provided to the converter. In case of default will return the . +/// +[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1001:Types that own disposable fields should be disposable", Justification = "Internal Uno Generator Issue: https://github.com/unoplatform/uno/pull/8743")] +public partial class OrientationToObjectConverter : DependencyObject, IValueConverter +{ + /// + /// Identifies the property. + /// + public static readonly DependencyProperty HorizontalValueProperty = + DependencyProperty.Register(nameof(HorizontalValue), typeof(object), typeof(OrientationToObjectConverter), new PropertyMetadata(null)); + + /// + /// Identifies the property. + /// + public static readonly DependencyProperty VerticalValueProperty = + DependencyProperty.Register(nameof(VerticalValue), typeof(object), typeof(OrientationToObjectConverter), new PropertyMetadata(null)); + + /// + /// Gets or sets the value to be returned when the of the provided value is . + /// + public object HorizontalValue + { + get { return GetValue(HorizontalValueProperty); } + set { SetValue(HorizontalValueProperty, value); } + } + + /// + /// Gets or sets the value to be returned when the of the provided value is . + /// + public object VerticalValue + { + get { return GetValue(VerticalValueProperty); } + set { SetValue(VerticalValueProperty, value); } + } + + /// + /// Convert the 's Orientation to an other object. + /// + /// The source data being passed to the target. + /// The type of the target property, as a type reference. + /// An optional parameter to be used to invert the converter logic. + /// The language of the conversion. + /// The value to be passed to the target dependency property. + public object Convert(object value, Type targetType, object parameter, string language) + { + var isHorizontal = value != null && value is Orientation orientation && orientation == Orientation.Horizontal; + + // Negate if needed + if (ConverterTools.TryParseBool(parameter)) + { + isHorizontal = !isHorizontal; + } + + return ConverterTools.Convert(isHorizontal ? HorizontalValue : VerticalValue, targetType); + } + + /// + /// Not implemented. + /// + /// The source data being passed to the target. + /// The type of the target property, as a type reference. + /// Optional parameter. Not used. + /// The language of the conversion. Not used. + /// The value to be passed to the target dependency property. + public object ConvertBack(object value, Type targetType, object parameter, string language) + { + throw new NotImplementedException(); + } +} diff --git a/components/Sizers/src/Toolkit/TypeToObjectConverter.cs b/components/Sizers/src/Toolkit/TypeToObjectConverter.cs new file mode 100644 index 00000000..14d6e5b1 --- /dev/null +++ b/components/Sizers/src/Toolkit/TypeToObjectConverter.cs @@ -0,0 +1,91 @@ +// 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.WinUI.Controls.SizerBaseLocal; + +/// +/// This class returns an object or another, depending on whether the type of the provided value matches another provided Type. +/// +[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1001:Types that own disposable fields should be disposable", Justification = "Internal Uno Generator Issue: https://github.com/unoplatform/uno/pull/8743")] +public partial class TypeToObjectConverter : DependencyObject, IValueConverter +{ + /// + /// Identifies the property. + /// + public static readonly DependencyProperty TrueValueProperty = + DependencyProperty.Register(nameof(TrueValue), typeof(object), typeof(TypeToObjectConverter), new PropertyMetadata(null)); + + /// + /// Identifies the property. + /// + public static readonly DependencyProperty FalseValueProperty = + DependencyProperty.Register(nameof(FalseValue), typeof(object), typeof(TypeToObjectConverter), new PropertyMetadata(null)); + + /// + /// Identifies the property. + /// + public static readonly DependencyProperty TypeProperty = + DependencyProperty.Register(nameof(Type), typeof(Type), typeof(TypeToObjectConverter), new PropertyMetadata(typeof(object))); + + /// + /// Gets or sets the value to be returned when the type of the provided value matches . + /// + public object TrueValue + { + get { return GetValue(TrueValueProperty); } + set { SetValue(TrueValueProperty, value); } + } + + /// + /// Gets or sets the value to be returned when the type of the provided value does not match . + /// + public object FalseValue + { + get { return GetValue(FalseValueProperty); } + set { SetValue(FalseValueProperty, value); } + } + + /// + /// Gets or sets the Type used to compare the type of the provided value. + /// + public Type Type + { + get { return (Type)GetValue(TypeProperty); } + set { SetValue(TypeProperty, value); } + } + + /// + /// Convert the 's Type to an other object. + /// + /// The source data being passed to the target. + /// The type of the target property, as a type reference. + /// An optional parameter to be used to invert the converter logic. + /// The language of the conversion. + /// The value to be passed to the target dependency property. + public object Convert(object value, Type targetType, object parameter, string language) + { + var typeMatches = value != null && Type.Equals(value.GetType()); + + // Negate if needed + if (ConverterTools.TryParseBool(parameter)) + { + typeMatches = !typeMatches; + } + + return ConverterTools.Convert(typeMatches ? TrueValue : FalseValue, targetType); + } + + /// + /// Not implemented. + /// + /// The source data being passed to the target. + /// The type of the target property, as a type reference. + /// Optional parameter. Not used. + /// The language of the conversion. Not used. + /// The value to be passed to the target dependency property. + public object ConvertBack(object value, Type targetType, object parameter, string language) + { + throw new NotImplementedException(); + } +} diff --git a/components/Sizers/tests/ExampleSizerBaseTestClass.cs b/components/Sizers/tests/ExampleSizerBaseTestClass.cs new file mode 100644 index 00000000..4d0385e7 --- /dev/null +++ b/components/Sizers/tests/ExampleSizerBaseTestClass.cs @@ -0,0 +1,61 @@ +// 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.Controls; +using CommunityToolkit.Tooling.TestGen; +using CommunityToolkit.Tests; +using CommunityToolkit.WinUI.Controls.Automation.Peers; + +namespace SizersExperiment.Tests; + +[TestClass] +public partial class ExampleSizerBaseTestClass : VisualUITestBase +{ + [TestMethod] + public async Task ShouldConfigureGridSplitterAutomationPeer() + { + await EnqueueAsync(() => + { + const string automationName = "MyCustomAutomationName"; + const string name = "Sizer"; + + var gridSplitter = new GridSplitter(); + var gridSplitterAutomationPeer = FrameworkElementAutomationPeer.CreatePeerForElement(gridSplitter) as SizerAutomationPeer; + + Assert.IsNotNull(gridSplitterAutomationPeer, "Verify that the AutomationPeer is SizerAutomationPeer."); + + gridSplitter.Name = name; + Assert.IsTrue(gridSplitterAutomationPeer.GetName().Contains(name), "Verify that the UIA name contains the given Name of the GridSplitter (Sizer)."); + + gridSplitter.SetValue(AutomationProperties.NameProperty, automationName); + Assert.IsTrue(gridSplitterAutomationPeer.GetName().Contains(automationName), "Verify that the UIA name contains the customized AutomationProperties.Name of the GridSplitter."); + }); + } + + [UIThreadTestMethod] + public void PropertySizer_TestInitialBinding(PropertySizerTestInitialBinding testControl) + { + var propertySizer = testControl.FindDescendant(); + + Assert.IsNotNull(propertySizer, "Could not find PropertySizer control."); + + // Set in XAML Page LINK: PropertySizerTestInitialBinding.xaml#L14 + Assert.AreEqual(300, propertySizer.Binding, "Property Sizer not at expected initial value."); + } + + [UIThreadTestMethod] + public void PropertySizer_TestChangeBinding(PropertySizerTestInitialBinding testControl) + { + var propertySizer = testControl.FindDescendant(); + var navigationView = testControl.FindDescendant(); + + Assert.IsNotNull(propertySizer, "Could not find PropertySizer control."); + Assert.IsNotNull(navigationView, "Could not find NavigationView control."); + + navigationView.OpenPaneLength = 200; + + // Set in XAML Page LINK: PropertySizerTestInitialBinding.xaml#L14 + Assert.AreEqual(200, propertySizer.Binding, "Property Sizer not at expected changed value."); + } +} diff --git a/components/Sizers/tests/ExampleSizersTestClass.cs b/components/Sizers/tests/ExampleSizersTestClass.cs deleted file mode 100644 index a143f027..00000000 --- a/components/Sizers/tests/ExampleSizersTestClass.cs +++ /dev/null @@ -1,136 +0,0 @@ -// 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.Tooling.TestGen; -using CommunityToolkit.Tests; -using CommunityToolkit.Tests.Internal; // TODO: For CompositionTargetHelper until ported over into package. -using CommunityToolkit.WinUI; -using CommunityToolkit.WinUI.Controls; - -namespace SizersExperiment.Tests; - -[TestClass] -public partial class ExampleSizersTestClass : VisualUITestBase -{ - // If you don't need access to UI objects directly or async code, use this pattern. - [TestMethod] - public void SimpleSynchronousExampleTest() - { - var assembly = typeof(Sizers).Assembly; - var type = assembly.GetType(typeof(Sizers).FullName ?? string.Empty); - - Assert.IsNotNull(type, "Could not find Sizers type."); - Assert.AreEqual(typeof(Sizers), type, "Type of Sizers does not match expected type."); - } - - // If you don't need access to UI objects directly, use this pattern. - [TestMethod] - public async Task SimpleAsyncExampleTest() - { - await Task.Delay(250); - - Assert.IsTrue(true); - } - - // Example that shows how to check for exception throwing. - [TestMethod] - public void SimpleExceptionCheckTest() - { - // If you need to check exceptions occur for invalid inputs, etc... - // Use Assert.ThrowsException to limit the scope to where you expect the error to occur. - // Otherwise, using the ExpectedException attribute could swallow or - // catch other issues in setup code. - Assert.ThrowsException(() => throw new NotImplementedException()); - } - - // The UIThreadTestMethod automatically dispatches to the UI for us to work with UI objects. - [UIThreadTestMethod] - public void SimpleUIAttributeExampleTest() - { - var component = new Sizers(); - Assert.IsNotNull(component); - } - - // The UIThreadTestMethod can also easily grab a XAML Page for us by passing its type as a parameter. - // This lets us actually test a control as it would behave within an actual application. - // The page will already be loaded by the time your test is called. - [UIThreadTestMethod] - public void SimpleUIExamplePageTest(ExampleSizersTestPage page) - { - // You can use the Toolkit Visual Tree helpers here to find the component by type or name: - var component = page.FindDescendant(); - - Assert.IsNotNull(component); - - var componentByName = page.FindDescendant("SizersControl"); - - Assert.IsNotNull(componentByName); - } - - // You can still do async work with a UIThreadTestMethod as well. - [UIThreadTestMethod] - public async Task SimpleAsyncUIExamplePageTest(ExampleSizersTestPage page) - { - // This helper can be used to wait for a rendering pass to complete. - // Note, this is already done by loading a Page with the [UIThreadTestMethod] helper. - await CompositionTargetHelper.ExecuteAfterCompositionRenderingAsync(() => { }); - - var component = page.FindDescendant(); - - Assert.IsNotNull(component); - } - - //// ----------------------------- ADVANCED TEST SCENARIOS ----------------------------- - - // If you need to use DataRow, you can use this pattern with the UI dispatch still. - // Otherwise, checkout the UIThreadTestMethod attribute above. - // See https://github.com/CommunityToolkit/Labs-Windows/issues/186 - [TestMethod] - public async Task ComplexAsyncUIExampleTest() - { - await EnqueueAsync(() => - { - var component = new Sizers_ClassicBinding(); - Assert.IsNotNull(component); - }); - } - - // If you want to load other content not within a XAML page using the UIThreadTestMethod above. - // Then you can do that using the Load/UnloadTestContentAsync methods. - [TestMethod] - public async Task ComplexAsyncLoadUIExampleTest() - { - await EnqueueAsync(async () => - { - var component = new Sizers_ClassicBinding(); - Assert.IsNotNull(component); - Assert.IsFalse(component.IsLoaded); - - await LoadTestContentAsync(component); - - Assert.IsTrue(component.IsLoaded); - - await UnloadTestContentAsync(component); - - Assert.IsFalse(component.IsLoaded); - }); - } - - // You can still use the UIThreadTestMethod to remove the extra layer for the dispatcher as well: - [UIThreadTestMethod] - public async Task ComplexAsyncLoadUIExampleWithoutDispatcherTest() - { - var component = new Sizers_ClassicBinding(); - Assert.IsNotNull(component); - Assert.IsFalse(component.IsLoaded); - - await LoadTestContentAsync(component); - - Assert.IsTrue(component.IsLoaded); - - await UnloadTestContentAsync(component); - - Assert.IsFalse(component.IsLoaded); - } -} diff --git a/components/Sizers/tests/ExampleSizersTestPage.xaml b/components/Sizers/tests/ExampleSizersTestPage.xaml deleted file mode 100644 index 1df112c1..00000000 --- a/components/Sizers/tests/ExampleSizersTestPage.xaml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - diff --git a/components/Sizers/tests/PropertySizerTestInitialBinding.xaml b/components/Sizers/tests/PropertySizerTestInitialBinding.xaml new file mode 100644 index 00000000..ef3ab0da --- /dev/null +++ b/components/Sizers/tests/PropertySizerTestInitialBinding.xaml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + diff --git a/components/Sizers/tests/ExampleSizersTestPage.xaml.cs b/components/Sizers/tests/PropertySizerTestInitialBinding.xaml.cs similarity index 78% rename from components/Sizers/tests/ExampleSizersTestPage.xaml.cs rename to components/Sizers/tests/PropertySizerTestInitialBinding.xaml.cs index d02dd467..f495c475 100644 --- a/components/Sizers/tests/ExampleSizersTestPage.xaml.cs +++ b/components/Sizers/tests/PropertySizerTestInitialBinding.xaml.cs @@ -7,9 +7,9 @@ namespace SizersExperiment.Tests; /// /// An empty page that can be used on its own or navigated to within a Frame. /// -public sealed partial class ExampleSizersTestPage : Page +public sealed partial class PropertySizerTestInitialBinding : Page { - public ExampleSizersTestPage() + public PropertySizerTestInitialBinding() { this.InitializeComponent(); } diff --git a/components/Sizers/tests/Sizers.Tests.projitems b/components/Sizers/tests/Sizers.Tests.projitems index 86d4fc6d..0773d66f 100644 --- a/components/Sizers/tests/Sizers.Tests.projitems +++ b/components/Sizers/tests/Sizers.Tests.projitems @@ -9,13 +9,13 @@ SizersExperiment.Tests - - - ExampleSizersTestPage.xaml + + + PropertySizerTestInitialBinding.xaml - + Designer MSBuild:Compile From 62682ffbcb1bfc7cf1ebd6275ac64988783348ac Mon Sep 17 00:00:00 2001 From: michael-hawker <24302614+michael-hawker@users.noreply.github.com> Date: Fri, 14 Apr 2023 17:44:31 -0700 Subject: [PATCH 3/5] Fixes #388 SizerBase Orientation Property Removes Mouse Extension from XAML Template and does in code-behind as to remove the need to set the Cursor property directly via binding By setting the Cursor DependencyProperty we were losing our detection mechanism, now if it's set it's an explicit override, otherwise we use our logic based on Orientation property Also, switches to new CommunityToolkit.*.Extensions Dependency vs. old copies of Tree helpers Fixes some doc typos Bumps version, tested on UWP, WASDK, and Uno.UI/WASM --- ...munityToolkit.WinUI.Controls.Sizers.csproj | 4 + .../src/ContentSizer/ContentSizer.Events.cs | 2 +- components/Sizers/src/SizerBase.Events.cs | 2 +- components/Sizers/src/SizerBase.Properties.cs | 35 +-- components/Sizers/src/SizerBase.cs | 12 +- components/Sizers/src/SizerBase.xaml | 1 - .../src/Toolkit/DependencyObjectExtensions.cs | 225 ------------------ .../FrameworkElementExtensions.Mouse.cs | 113 --------- .../Toolkit/OrientationToObjectConverter.cs | 2 + .../src/Toolkit/TypeToObjectConverter.cs | 91 ------- 10 files changed, 34 insertions(+), 453 deletions(-) delete mode 100644 components/Sizers/src/Toolkit/DependencyObjectExtensions.cs delete mode 100644 components/Sizers/src/Toolkit/FrameworkElementExtensions.Mouse.cs delete mode 100644 components/Sizers/src/Toolkit/TypeToObjectConverter.cs diff --git a/components/Sizers/src/CommunityToolkit.WinUI.Controls.Sizers.csproj b/components/Sizers/src/CommunityToolkit.WinUI.Controls.Sizers.csproj index 94d9f669..d3c03e39 100644 --- a/components/Sizers/src/CommunityToolkit.WinUI.Controls.Sizers.csproj +++ b/components/Sizers/src/CommunityToolkit.WinUI.Controls.Sizers.csproj @@ -10,4 +10,8 @@ + + + + diff --git a/components/Sizers/src/ContentSizer/ContentSizer.Events.cs b/components/Sizers/src/ContentSizer/ContentSizer.Events.cs index 138dfea8..0778459a 100644 --- a/components/Sizers/src/ContentSizer/ContentSizer.Events.cs +++ b/components/Sizers/src/ContentSizer/ContentSizer.Events.cs @@ -2,7 +2,7 @@ // 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.Controls.SizerBaseLocal; +using CommunityToolkit.WinUI; namespace CommunityToolkit.WinUI.Controls; diff --git a/components/Sizers/src/SizerBase.Events.cs b/components/Sizers/src/SizerBase.Events.cs index 8b7ae4a9..f741c5f7 100644 --- a/components/Sizers/src/SizerBase.Events.cs +++ b/components/Sizers/src/SizerBase.Events.cs @@ -72,7 +72,7 @@ protected override void OnManipulationStarting(ManipulationStartingRoutedEventAr /// protected override void OnManipulationDelta(ManipulationDeltaRoutedEventArgs e) { - // We use Trancate here to provide 'snapping' points with the DragIncrement property + // We use Truncate 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 = diff --git a/components/Sizers/src/SizerBase.Properties.cs b/components/Sizers/src/SizerBase.Properties.cs index c307e400..5fa06214 100644 --- a/components/Sizers/src/SizerBase.Properties.cs +++ b/components/Sizers/src/SizerBase.Properties.cs @@ -2,6 +2,8 @@ // 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; + #if !WINAPPSDK using CursorEnum = Windows.UI.Core.CoreCursorType; #else @@ -17,7 +19,7 @@ namespace CommunityToolkit.WinUI.Controls; public partial class SizerBase : Control { /// - /// Gets or sets the cursor to use when hovering over the gripper bar. If left as null, the control will manage the cursor automatically based on the property value. + /// Gets or sets the cursor to use when hovering over the gripper bar. If left as null, the control will manage the cursor automatically based on the property value (default). /// public CursorEnum Cursor { @@ -38,7 +40,7 @@ public CursorEnum Cursor /// 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. + /// This value is independent 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 { @@ -92,35 +94,38 @@ private static void OnOrientationPropertyChanged(DependencyObject d, DependencyP { if (d is SizerBase gripper) { - CursorEnum cursorToUse = gripper.Orientation == Orientation.Vertical ? CursorEnum.SizeWestEast : CursorEnum.SizeNorthSouth; + CursorEnum cursorByOrientation = gripper.Orientation == Orientation.Vertical ? CursorEnum.SizeWestEast : CursorEnum.SizeNorthSouth; // See if there's been a cursor override, otherwise we'll pick var cursor = gripper.ReadLocalValue(CursorProperty); if (cursor == DependencyProperty.UnsetValue || cursor == null) { - cursor = cursorToUse; - - // On UWP, we use the extension in XAML to control this behavior, - // so we'll update it here (and maintain binding). - // We'll keep it in-sync to maintain behavior for WinUI 3 as well. - gripper.SetValue(CursorProperty, cursor); + cursor = cursorByOrientation; + } - // We return here, as the Cursor will trigger this function again anyway to set for WinUI 3 - return; +#if !WINAPPSDK + // On UWP, we use our XAML extension to control this behavior, + // so we'll update it here (and maintain any cursor override). + if (cursor is CursorEnum cursorValue) + { + FrameworkElementExtensions.SetCursor(gripper, cursorValue); } + return; +#endif + // TODO: [UNO] Only supported on certain platforms // See ProtectedCursor here: https://github.com/unoplatform/uno/blob/3fe3862b270b99dbec4d830b547942af61b1a1d9/src/Uno.UI/UI/Xaml/UIElement.cs#L1015-L1023 #if WINAPPSDK && !HAS_UNO // Need to wait until we're at least applying template step of loading before setting Cursor // See https://github.com/microsoft/microsoft-ui-xaml/issues/7062 - if (gripper._applyingTemplate && - cursor is CursorEnum cursorToSet && + if (gripper._appliedTemplate && + cursor is CursorEnum cursorValue && (gripper.ProtectedCursor == null || (gripper.ProtectedCursor is InputSystemCursor current && - current.CursorShape != cursorToSet))) + current.CursorShape != cursorValue))) { - gripper.ProtectedCursor = InputSystemCursor.Create(cursorToSet); + gripper.ProtectedCursor = InputSystemCursor.Create(cursorValue); } #endif } diff --git a/components/Sizers/src/SizerBase.cs b/components/Sizers/src/SizerBase.cs index f1f1071b..fabb1635 100644 --- a/components/Sizers/src/SizerBase.cs +++ b/components/Sizers/src/SizerBase.cs @@ -21,7 +21,7 @@ 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. + /// Implementer should record current state of manipulated target at this point in time. /// They will receive the cumulative change in or /// based on the property. /// @@ -42,7 +42,7 @@ protected virtual void OnLoaded(RoutedEventArgs e) /// 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 + /// or . The implementer /// just needs to use the provided value to manipulate their baseline stored /// in to provide the desired change. /// @@ -57,7 +57,7 @@ protected virtual void OnLoaded(RoutedEventArgs e) /// 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 + /// . The implementer just needs /// to use the provided value to manipulate their baseline stored /// in to provide the desired change. /// @@ -83,7 +83,7 @@ protected override AutomationPeer OnCreateAutomationPeer() // On Uno the ProtectedCursor isn't supported yet, so we don't need this value. #if WINAPPSDK && !HAS_UNO // Used to track when we're in the OnApplyTemplateStep to change ProtectedCursor value. - private bool _applyingTemplate = false; + private bool _appliedTemplate = false; #endif /// @@ -115,9 +115,9 @@ protected override void OnApplyTemplate() SizerBase_IsEnabledChanged(this, null!); #if WINAPPSDK && !HAS_UNO // On WinAppSDK, we'll trigger this to setup the initial ProtectedCursor value. - _applyingTemplate = true; + _appliedTemplate = true; #endif - // On UWP, we'll check the current Orientation and set the Cursor property to use here still. + // Ensure we have the proper cursor value setup, as we can only set now for WinUI 3 OnOrientationPropertyChanged(this, null!); } diff --git a/components/Sizers/src/SizerBase.xaml b/components/Sizers/src/SizerBase.xaml index 1fe4942d..8645f92b 100644 --- a/components/Sizers/src/SizerBase.xaml +++ b/components/Sizers/src/SizerBase.xaml @@ -48,7 +48,6 @@ diff --git a/components/Sizers/src/Toolkit/DependencyObjectExtensions.cs b/components/Sizers/src/Toolkit/DependencyObjectExtensions.cs deleted file mode 100644 index 63b731e7..00000000 --- a/components/Sizers/src/Toolkit/DependencyObjectExtensions.cs +++ /dev/null @@ -1,225 +0,0 @@ -// 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. - -// We want this to be private/local to our component. -namespace CommunityToolkit.WinUI.Controls.SizerBaseLocal; - -//// 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/components/Sizers/src/Toolkit/FrameworkElementExtensions.Mouse.cs b/components/Sizers/src/Toolkit/FrameworkElementExtensions.Mouse.cs deleted file mode 100644 index fa852c6a..00000000 --- a/components/Sizers/src/Toolkit/FrameworkElementExtensions.Mouse.cs +++ /dev/null @@ -1,113 +0,0 @@ -// 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; - -namespace CommunityToolkit.WinUI.Controls.SizerBaseLocal; - -/// -public static partial class FrameworkElementExtensions -{ - private static readonly object _cursorLock = new object(); - private static readonly CoreCursor _defaultCursor = new CoreCursor(CoreCursorType.Arrow, 1); - private static readonly Dictionary _cursors = - new Dictionary { { CoreCursorType.Arrow, _defaultCursor } }; - - /// - /// Dependency property for specifying the target to be shown - /// over the target . - /// - public static readonly DependencyProperty CursorProperty = - DependencyProperty.RegisterAttached("Cursor", typeof(CoreCursorType), typeof(FrameworkElementExtensions), new PropertyMetadata(CoreCursorType.Arrow, CursorChanged)); - - /// - /// Set the target . - /// - /// Object where the selector cursor type should be shown. - /// Target cursor type value. - public static void SetCursor(FrameworkElement element, CoreCursorType value) - { - element.SetValue(CursorProperty, value); - } - - /// - /// Get the current . - /// - /// Object where the selector cursor type should be shown. - /// Cursor type set on target element. - public static CoreCursorType GetCursor(FrameworkElement element) - { - return (CoreCursorType)element.GetValue(CursorProperty); - } - - private static void CursorChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - // TODO: How do we want to indicate this isn't supported on the WinAppSDK? -#if !WINAPPSDK - var element = d as FrameworkElement; - if (element == null) - { - throw new NullReferenceException(nameof(element)); - } - - var value = (CoreCursorType)e.NewValue; - - // lock ensures CoreCursor creation and event handlers attachment/detachment is atomic - lock (_cursorLock) - { - if (!_cursors.ContainsKey(value)) - { - _cursors[value] = new CoreCursor(value, 1); - } - - // make sure event handlers are not attached twice to element - element.PointerEntered -= Element_PointerEntered; - element.PointerEntered += Element_PointerEntered; - element.PointerExited -= Element_PointerExited; - element.PointerExited += Element_PointerExited; - element.Unloaded -= ElementOnUnloaded; - element.Unloaded += ElementOnUnloaded; - } -#endif - } - -#if !WINAPPSDK - private static void Element_PointerEntered(object sender, PointerRoutedEventArgs e) - { - // TODO: [UNO] Only supported on certain platforms - // See PointerCursor here: https://github.com/unoplatform/uno/blob/3fe3862b270b99dbec4d830b547942af61b1a1d9/src/Uno.UWP/UI/Core/CoreWindow.cs#L71-L77 -#if NETFX_CORE || WASM || __MACOS__ || __SKIA__ - CoreCursorType cursor = GetCursor((FrameworkElement)sender); - Window.Current.CoreWindow.PointerCursor = _cursors[cursor]; -#endif - } - - private static void Element_PointerExited(object sender, PointerRoutedEventArgs e) - { -#if NETFX_CORE || WASM || __MACOS__ || __SKIA__ - // when exiting change the cursor to the target Mouse.Cursor value of the new element - CoreCursor cursor; - if (sender != e.OriginalSource && e.OriginalSource is FrameworkElement newElement) - { - cursor = _cursors[GetCursor(newElement)]; - } - else - { - cursor = _defaultCursor; - } - - Window.Current.CoreWindow.PointerCursor = cursor; -#endif - } - - private static void ElementOnUnloaded(object sender, RoutedEventArgs routedEventArgs) - { -#if NETFX_CORE || __WASM__ || __MACOS__ || __SKIA__ - // when the element is programatically unloaded, reset the cursor back to default - // this is necessary when click triggers immediate change in layout and PointerExited is not called - Window.Current.CoreWindow.PointerCursor = _defaultCursor; -#endif - } -#endif -} diff --git a/components/Sizers/src/Toolkit/OrientationToObjectConverter.cs b/components/Sizers/src/Toolkit/OrientationToObjectConverter.cs index b668362b..f670fb7d 100644 --- a/components/Sizers/src/Toolkit/OrientationToObjectConverter.cs +++ b/components/Sizers/src/Toolkit/OrientationToObjectConverter.cs @@ -4,6 +4,8 @@ namespace CommunityToolkit.WinUI.Controls.SizerBaseLocal; +//// TODO: Make this part of the WCT converters package? + /// /// This class returns a value depending on the of the value provided to the converter. In case of default will return the . /// diff --git a/components/Sizers/src/Toolkit/TypeToObjectConverter.cs b/components/Sizers/src/Toolkit/TypeToObjectConverter.cs deleted file mode 100644 index 14d6e5b1..00000000 --- a/components/Sizers/src/Toolkit/TypeToObjectConverter.cs +++ /dev/null @@ -1,91 +0,0 @@ -// 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.WinUI.Controls.SizerBaseLocal; - -/// -/// This class returns an object or another, depending on whether the type of the provided value matches another provided Type. -/// -[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1001:Types that own disposable fields should be disposable", Justification = "Internal Uno Generator Issue: https://github.com/unoplatform/uno/pull/8743")] -public partial class TypeToObjectConverter : DependencyObject, IValueConverter -{ - /// - /// Identifies the property. - /// - public static readonly DependencyProperty TrueValueProperty = - DependencyProperty.Register(nameof(TrueValue), typeof(object), typeof(TypeToObjectConverter), new PropertyMetadata(null)); - - /// - /// Identifies the property. - /// - public static readonly DependencyProperty FalseValueProperty = - DependencyProperty.Register(nameof(FalseValue), typeof(object), typeof(TypeToObjectConverter), new PropertyMetadata(null)); - - /// - /// Identifies the property. - /// - public static readonly DependencyProperty TypeProperty = - DependencyProperty.Register(nameof(Type), typeof(Type), typeof(TypeToObjectConverter), new PropertyMetadata(typeof(object))); - - /// - /// Gets or sets the value to be returned when the type of the provided value matches . - /// - public object TrueValue - { - get { return GetValue(TrueValueProperty); } - set { SetValue(TrueValueProperty, value); } - } - - /// - /// Gets or sets the value to be returned when the type of the provided value does not match . - /// - public object FalseValue - { - get { return GetValue(FalseValueProperty); } - set { SetValue(FalseValueProperty, value); } - } - - /// - /// Gets or sets the Type used to compare the type of the provided value. - /// - public Type Type - { - get { return (Type)GetValue(TypeProperty); } - set { SetValue(TypeProperty, value); } - } - - /// - /// Convert the 's Type to an other object. - /// - /// The source data being passed to the target. - /// The type of the target property, as a type reference. - /// An optional parameter to be used to invert the converter logic. - /// The language of the conversion. - /// The value to be passed to the target dependency property. - public object Convert(object value, Type targetType, object parameter, string language) - { - var typeMatches = value != null && Type.Equals(value.GetType()); - - // Negate if needed - if (ConverterTools.TryParseBool(parameter)) - { - typeMatches = !typeMatches; - } - - return ConverterTools.Convert(typeMatches ? TrueValue : FalseValue, targetType); - } - - /// - /// Not implemented. - /// - /// The source data being passed to the target. - /// The type of the target property, as a type reference. - /// Optional parameter. Not used. - /// The language of the conversion. Not used. - /// The value to be passed to the target dependency property. - public object ConvertBack(object value, Type targetType, object parameter, string language) - { - throw new NotImplementedException(); - } -} From 28b36caa8562ae975bef335c3aebfac01da05bb2 Mon Sep 17 00:00:00 2001 From: Niels Laute Date: Mon, 17 Apr 2023 20:55:31 +0200 Subject: [PATCH 4/5] Adding modern styles (#2) * Adding modern style * Address feedback * Fixing wrong state names --- .../Sizers/samples/GridSplitterPage.xaml | 4 +- components/Sizers/src/SizerBase.Events.cs | 16 +-- components/Sizers/src/SizerBase.Properties.cs | 25 ++++ components/Sizers/src/SizerBase.cs | 23 ++++ components/Sizers/src/SizerBase.xaml | 119 ++++++++++++------ .../Sizers/src/Toolkit/ConverterTools.cs | 45 ------- .../Toolkit/OrientationToObjectConverter.cs | 78 ------------ 7 files changed, 138 insertions(+), 172 deletions(-) delete mode 100644 components/Sizers/src/Toolkit/ConverterTools.cs delete mode 100644 components/Sizers/src/Toolkit/OrientationToObjectConverter.cs diff --git a/components/Sizers/samples/GridSplitterPage.xaml b/components/Sizers/samples/GridSplitterPage.xaml index 2c3a2f60..bba2d2ec 100644 --- a/components/Sizers/samples/GridSplitterPage.xaml +++ b/components/Sizers/samples/GridSplitterPage.xaml @@ -88,7 +88,7 @@ ResizeBehavior="BasedOnAlignment" ResizeDirection="Auto"> - + @@ -98,7 +98,7 @@ Height="16" VerticalAlignment="Top"> - + diff --git a/components/Sizers/src/SizerBase.Events.cs b/components/Sizers/src/SizerBase.Events.cs index f741c5f7..0ded7189 100644 --- a/components/Sizers/src/SizerBase.Events.cs +++ b/components/Sizers/src/SizerBase.Events.cs @@ -117,7 +117,7 @@ private void SizerBase_PointerReleased(object sender, PointerRoutedEventArgs e) if (IsEnabled) { - VisualStateManager.GoToState(this, _pointerEntered ? "PointerOver" : "Normal", true); + VisualStateManager.GoToState(this, _pointerEntered ? PointerOverState : NormalState, true); } } @@ -127,7 +127,7 @@ private void SizerBase_PointerPressed(object sender, PointerRoutedEventArgs e) if (IsEnabled) { - VisualStateManager.GoToState(this, "Pressed", true); + VisualStateManager.GoToState(this, PointerOverState, true); } } @@ -137,7 +137,7 @@ private void SizerBase_PointerExited(object sender, PointerRoutedEventArgs e) if (!_pressed && !_dragging && IsEnabled) { - VisualStateManager.GoToState(this, "Normal", true); + VisualStateManager.GoToState(this, NormalState, true); } } @@ -147,7 +147,7 @@ private void SizerBase_PointerEntered(object sender, PointerRoutedEventArgs e) if (!_pressed && !_dragging && IsEnabled) { - VisualStateManager.GoToState(this, "PointerOver", true); + VisualStateManager.GoToState(this, PointerOverState, true); } } @@ -155,24 +155,24 @@ private void SizerBase_ManipulationCompleted(object sender, ManipulationComplete { _dragging = false; _pressed = false; - VisualStateManager.GoToState(this, _pointerEntered ? "PointerOver" : "Normal", true); + VisualStateManager.GoToState(this, _pointerEntered ? PointerOverState : NormalState, true); } private void SizerBase_ManipulationStarted(object sender, ManipulationStartedRoutedEventArgs e) { _dragging = true; - VisualStateManager.GoToState(this, "Pressed", true); + VisualStateManager.GoToState(this, PressedState, true); } private void SizerBase_IsEnabledChanged(object sender, DependencyPropertyChangedEventArgs e) { if (!IsEnabled) { - VisualStateManager.GoToState(this, "Disabled", true); + VisualStateManager.GoToState(this, DisabledState, true); } else { - VisualStateManager.GoToState(this, _pointerEntered ? "PointerOver" : "Normal", true); + VisualStateManager.GoToState(this, _pointerEntered ? PointerOverState : NormalState, true); } } } diff --git a/components/Sizers/src/SizerBase.Properties.cs b/components/Sizers/src/SizerBase.Properties.cs index 5fa06214..3688c72a 100644 --- a/components/Sizers/src/SizerBase.Properties.cs +++ b/components/Sizers/src/SizerBase.Properties.cs @@ -90,10 +90,28 @@ public Orientation Orientation public static readonly DependencyProperty OrientationProperty = DependencyProperty.Register(nameof(Orientation), typeof(Orientation), typeof(SizerBase), new PropertyMetadata(Orientation.Vertical, OnOrientationPropertyChanged)); + /// + /// Gets or sets if the Thumb is visible. If not visible, only the background and cursor will be shown on MouseOver or Pressed states. + /// + public bool IsThumbVisible + { + get { return (bool)GetValue(IsThumbVisibleProperty); } + set { SetValue(IsThumbVisibleProperty, value); } + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty IsThumbVisibleProperty = + DependencyProperty.Register(nameof(IsThumbVisible), typeof(bool), typeof(SizerBase), new PropertyMetadata(true, OnIsThumbVisiblePropertyChanged)); + + private static void OnOrientationPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is SizerBase gripper) { + VisualStateManager.GoToState(gripper, gripper.Orientation == Orientation.Vertical ? VerticalState : HorizontalState, true); + CursorEnum cursorByOrientation = gripper.Orientation == Orientation.Vertical ? CursorEnum.SizeWestEast : CursorEnum.SizeNorthSouth; // See if there's been a cursor override, otherwise we'll pick @@ -130,4 +148,11 @@ cursor is CursorEnum cursorValue && #endif } } + private static void OnIsThumbVisiblePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is SizerBase gripper) + { + VisualStateManager.GoToState(gripper, gripper.IsThumbVisible ? VisibleState : CollapsedState, true); + } + } } diff --git a/components/Sizers/src/SizerBase.cs b/components/Sizers/src/SizerBase.cs index fabb1635..4c925054 100644 --- a/components/Sizers/src/SizerBase.cs +++ b/components/Sizers/src/SizerBase.cs @@ -9,8 +9,28 @@ namespace CommunityToolkit.WinUI.Controls; /// /// 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. /// + +[TemplateVisualState(Name = NormalState, GroupName = CommonStates)] +[TemplateVisualState(Name = PointerOverState, GroupName = CommonStates)] +[TemplateVisualState(Name = PressedState, GroupName = CommonStates)] +[TemplateVisualState(Name = DisabledState, GroupName = CommonStates)] +[TemplateVisualState(Name = HorizontalState, GroupName = OrientationStates)] +[TemplateVisualState(Name = VerticalState, GroupName = OrientationStates)] +[TemplateVisualState(Name = VisibleState, GroupName = ThumbVisibilityStates)] +[TemplateVisualState(Name = CollapsedState, GroupName = ThumbVisibilityStates)] public abstract partial class SizerBase : Control { + internal const string CommonStates = "CommonStates"; + internal const string NormalState = "Normal"; + internal const string PointerOverState = "PointerOver"; + internal const string PressedState = "Pressed"; + internal const string DisabledState = "Disabled"; + internal const string OrientationStates = "OrientationStates"; + internal const string HorizontalState = "Horizontal"; + internal const string VerticalState = "Vertical"; + internal const string ThumbVisibilityStates = "ThumbVisibilityStates"; + internal const string VisibleState = "Visible"; + internal const string CollapsedState = "Collapsed"; /// /// Called when the control has been initialized. /// @@ -119,6 +139,9 @@ protected override void OnApplyTemplate() #endif // Ensure we have the proper cursor value setup, as we can only set now for WinUI 3 OnOrientationPropertyChanged(this, null!); + + // Ensure we set the Thumb visiblity + OnIsThumbVisiblePropertyChanged(this, null!); } private void SizerBase_Loaded(object sender, RoutedEventArgs e) diff --git a/components/Sizers/src/SizerBase.xaml b/components/Sizers/src/SizerBase.xaml index 8645f92b..45d6bacf 100644 --- a/components/Sizers/src/SizerBase.xaml +++ b/components/Sizers/src/SizerBase.xaml @@ -1,35 +1,50 @@ - + + xmlns:controls="using:CommunityToolkit.WinUI.Controls"> - - - + + + + + + + + + + + + - - - + + + + + - - - - + 24 + 4 + 2 + 4