From 9eadd9769380995066fbc0c261bf206a77b5e270 Mon Sep 17 00:00:00 2001 From: Niels Laute Date: Sat, 22 Jul 2023 21:01:13 +0200 Subject: [PATCH 1/5] Init --- components/Segmented/OpenSolution.bat | 3 + .../Segmented/samples/Assets/Segmented.png | Bin 0 -> 2366 bytes .../Segmented/samples/Dependencies.props | 31 + .../samples/Segmented.Samples.csproj | 8 + components/Segmented/samples/Segmented.md | 28 + .../samples/SegmentedBasicSample.xaml | 63 + .../samples/SegmentedBasicSample.xaml.cs | 40 + .../samples/SegmentedStylesSample.xaml | 55 + .../samples/SegmentedStylesSample.xaml.cs | 25 + .../Segmented/src/AdditionalAssemblyInfo.cs | 13 + ...ityToolkit.WinUI.Controls.Segmented.csproj | 17 + components/Segmented/src/Dependencies.props | 31 + components/Segmented/src/EqualPanel.cs | 101 ++ .../Segmented/src/Helpers/ControlHelpers.cs | 10 + .../src/Helpers/SegmentedMarginConverter.cs | 79 ++ components/Segmented/src/MultiTarget.props | 9 + .../Segmented/src/Segmented/Segmented.cs | 145 +++ .../Segmented/src/Segmented/Segmented.xaml | 121 ++ .../SegmentedItem/SegmentedItem.Properties.cs | 26 + .../src/SegmentedItem/SegmentedItem.cs | 60 + .../src/SegmentedItem/SegmentedItem.xaml | 1098 +++++++++++++++++ components/Segmented/src/Themes/Generic.xaml | 10 + .../tests/ExampleSegmentedTestClass.cs | 133 ++ .../tests/ExampleSegmentedTestPage.xaml | 14 + .../tests/ExampleSegmentedTestPage.xaml.cs | 16 + .../Segmented/tests/Segmented.Tests.projitems | 23 + .../Segmented/tests/Segmented.Tests.shproj | 13 + 27 files changed, 2172 insertions(+) create mode 100644 components/Segmented/OpenSolution.bat create mode 100644 components/Segmented/samples/Assets/Segmented.png create mode 100644 components/Segmented/samples/Dependencies.props create mode 100644 components/Segmented/samples/Segmented.Samples.csproj create mode 100644 components/Segmented/samples/Segmented.md create mode 100644 components/Segmented/samples/SegmentedBasicSample.xaml create mode 100644 components/Segmented/samples/SegmentedBasicSample.xaml.cs create mode 100644 components/Segmented/samples/SegmentedStylesSample.xaml create mode 100644 components/Segmented/samples/SegmentedStylesSample.xaml.cs create mode 100644 components/Segmented/src/AdditionalAssemblyInfo.cs create mode 100644 components/Segmented/src/CommunityToolkit.WinUI.Controls.Segmented.csproj create mode 100644 components/Segmented/src/Dependencies.props create mode 100644 components/Segmented/src/EqualPanel.cs create mode 100644 components/Segmented/src/Helpers/ControlHelpers.cs create mode 100644 components/Segmented/src/Helpers/SegmentedMarginConverter.cs create mode 100644 components/Segmented/src/MultiTarget.props create mode 100644 components/Segmented/src/Segmented/Segmented.cs create mode 100644 components/Segmented/src/Segmented/Segmented.xaml create mode 100644 components/Segmented/src/SegmentedItem/SegmentedItem.Properties.cs create mode 100644 components/Segmented/src/SegmentedItem/SegmentedItem.cs create mode 100644 components/Segmented/src/SegmentedItem/SegmentedItem.xaml create mode 100644 components/Segmented/src/Themes/Generic.xaml create mode 100644 components/Segmented/tests/ExampleSegmentedTestClass.cs create mode 100644 components/Segmented/tests/ExampleSegmentedTestPage.xaml create mode 100644 components/Segmented/tests/ExampleSegmentedTestPage.xaml.cs create mode 100644 components/Segmented/tests/Segmented.Tests.projitems create mode 100644 components/Segmented/tests/Segmented.Tests.shproj diff --git a/components/Segmented/OpenSolution.bat b/components/Segmented/OpenSolution.bat new file mode 100644 index 00000000..814a56d4 --- /dev/null +++ b/components/Segmented/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/Segmented/samples/Assets/Segmented.png b/components/Segmented/samples/Assets/Segmented.png new file mode 100644 index 0000000000000000000000000000000000000000..ba9ad789deff1e197472c2d763f8128cf53a1fc3 GIT binary patch literal 2366 zcmV-E3BmS>P);M1&0drDELIAGL9O(c600d`2O+f$vv5yP|-DM*vCFT!-4w0 zum0?x@6L!%fbsMC2q$^SY;*F#DE~17+Xk7|?=p!D1Wc1XZ$zLgSbvetAQLl_A4Ej9 zg>_EXHJzAbe-nwaw5doD9AYTv<t z$Thd<`)^%{qq0OHzlC92qkSRyo{W+jm`3~g=uT$(k%)SK5e$A=cV^_mh%rQi7dgC$ z;6gHDZe&CV%1DB~CY+)3Cd#9Mm(dGl0|5g<%%|MX2xw;oT1%NW#I=m zE)7aZIv-S`ZiMRH&77fFA50n!;t__HQ)WoBqYQGU zq2+u;wggcyo^U9+77K9U6*~KXaX4Qjz6npZ8rFqY70T+5A00EWCMg<&8I zXHE*}qPtMn67)!g6EMsG8V-ph!=z@5Ud)?uYr17nW>h|sim5!B-owsZZlw8`9OXoj zJk)Dir)@|lF*J6FY3{V3ssRC9WTbKlB3c5yX8na1zceUwBO+&omAHqzhdw%#iij~7 zauAYrAD&m4{N(UVrb~N*``zhdLRFdn`&pVKQ2aXxU=CM<~tO z9CykPoq%J)BD6FGy)B|jEX;!KEtaSu#B*)w%>c_8ovAXH9VXd-8d1^niW2ZS|`RSeM_!1cdpF;^v`Gb?LS1o z`Hbtoo&%iDY)VpL7&MU(nspINc-uT8-+4a5tH)o0X*w|8yoBj=gD6o!ca|3~G1QZh zA(JthM03dc;7wSsu0y6fbWzIImF;|n_rCoWzOkqWn?*>CPy+JLG*Pz^XDGJ;pKBpK68dg(V>|8xyym* z0POdCcLVDe-tuA{ZSo>^h-E9#-JM1Z*0q_Zo3%FMXBk|75m~sN8noLRz}t^;UhgVN zvDb_aDXip#DJfQ2=k6oq;}y^6R{^b*5lk0MHp)z>)^puEA0koBIDyJp6hEK&zyG^| zH?QVU+6~gfM}zzsZ;XBM@hfr8E#mpab2}r}!7wCvC{YS}(oZ?Ro&d!$Ab;px1t4#V z>egZ4@-zD8Fgn?hI89!}tphP78J_;~FYx4>k3sKJ6h&NfE59e9=pKsaFK<`)-3PuO z9$W){^g_6OH8{~d1&gwWh{(rsfb-Quk$b{B3Fq;2iahiDg4Z}{G+Ll1``=LZ)Y_mt zmPK4|qLbw248Q(6!LR=Mfmn3%D(`7w1vUCWE4JBtf#e;`GtzXF5FACT1KfS_ZD555 zQFS35N@TS!Bga~=G+kmjGd3Z$BCqeMb>ANE#){SdMJ+w0<^^I8+JfaI~X<7G*N;1oXMri*ihsjOc|_55MVKTjsu(I;Zo-H}dNNtC2jl zfD4EWW^I~Nxi4a>1w=|w zG!@=+B2nIYvninL5-^jW>za_air(OQpBDx=NpTW~|1}u;G4LO~-bKF@J!>ZzzabEp zVbrQa;e0pI88`bb?l)zs8?!5$jbyb;;{w{`g|Bas9(m_+f*5+c?uda^y=RQM-UCA2 z;UIJ|>CK{%q0>@;8FEU(pkmt6H)s=vD@TxoHI06^8t58QP{J+QDWk}nh~Y-@s)O;e zCW|`o#XjP(;3oC%Z`v#uP%+m60N zd}~;oTgC@m^YU6OOi{##NKwCdVcf2{dv=H#)E%gMaB~nBLslMeL5yK@acmvB1I=eH zM1EWvmU4!Cj&;Vu!IqW^87*g)xF1n<0)8dv6ugbZXdu^}Du^IzS-&A;&G9EdlroYC zd0&DJ^l<5j&Mtl$6Sb#7js!s+h!vf|MNC2tKISf(dCb!xuMHH)I( z`IuYWRtCqE*ZUqGFRETT!T|vH$%s5PAxHf41>lF$L_D!_Y0rQ7$FB&BzAIhwBhY^Y zS+&cE9LobT=j>2`ez<;J;{dw9=HB|)GoG=L^}6#e=ctx!#w3OTK7(I8{SfxCkA3W8 kAN$zHKK8MXeSEs{e-eMuvMAKBHvj+t07*qoM6N<$f;)b2mjD0& literal 0 HcmV?d00001 diff --git a/components/Segmented/samples/Dependencies.props b/components/Segmented/samples/Dependencies.props new file mode 100644 index 00000000..e622e1df --- /dev/null +++ b/components/Segmented/samples/Dependencies.props @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/Segmented/samples/Segmented.Samples.csproj b/components/Segmented/samples/Segmented.Samples.csproj new file mode 100644 index 00000000..bb7b1ed2 --- /dev/null +++ b/components/Segmented/samples/Segmented.Samples.csproj @@ -0,0 +1,8 @@ + + + Segmented + + + + + diff --git a/components/Segmented/samples/Segmented.md b/components/Segmented/samples/Segmented.md new file mode 100644 index 00000000..39afa292 --- /dev/null +++ b/components/Segmented/samples/Segmented.md @@ -0,0 +1,28 @@ +--- +title: Segmented +author: niels9001 +description: A common UI control to configure a view or setting. +keywords: SegmentedControl, Control, Layout, Segmented +dev_langs: + - csharp +category: Controls +subcategory: Input +discussion-id: 314 +issue-id: 392 +icon: Assets/Segmented.png +--- + +## The basics + +The `Segmented` control is best used with 2-5 items and does not support overflow. The `Icon` and `Content` property can be set on the `SegmentedItems`. + +> [!Sample SegmentedBasicSample] + +## Selection +`Segmented` supports single and multi-selection. When `SelectionMode` is set to `Single` the first item will be selected by default. This can be overriden by settings `AutoSelection` to `false`. + +## Other styles + +The `Segmented` control contains various additional styles, to match the look and feel of your application. The `PivotSegmentedStyle` matches a modern `Pivot` style while the `ButtonSegmentedStyle` represents buttons. + +> [!SAMPLE SegmentedStylesSample] diff --git a/components/Segmented/samples/SegmentedBasicSample.xaml b/components/Segmented/samples/SegmentedBasicSample.xaml new file mode 100644 index 00000000..7751848f --- /dev/null +++ b/components/Segmented/samples/SegmentedBasicSample.xaml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/Segmented/samples/SegmentedBasicSample.xaml.cs b/components/Segmented/samples/SegmentedBasicSample.xaml.cs new file mode 100644 index 00000000..9ebd0725 --- /dev/null +++ b/components/Segmented/samples/SegmentedBasicSample.xaml.cs @@ -0,0 +1,40 @@ +// 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 SegmentedExperiment.Samples; + +/// +/// An example sample page of a Segmented control. +/// +[ToolkitSampleMultiChoiceOption("SelectionMode", "Single", "Multiple", Title = "Selection mode")] +[ToolkitSampleMultiChoiceOption("Alignment", "Left", "Center", "Right", "Stretch", Title = "Horizontal alignment")] + +[ToolkitSample(id: nameof(SegmentedBasicSample), "Basics", description: $"A sample for showing how to create and use a {nameof(Segmented)} custom control.")] +public sealed partial class SegmentedBasicSample : Page +{ + public SegmentedBasicSample() + { + this.InitializeComponent(); + } + + // TODO: See https://github.com/CommunityToolkit/Labs-Windows/issues/149 + public static ListViewSelectionMode ConvertStringToSelectionMode(string selectionMode) => selectionMode switch + { + "Single" => ListViewSelectionMode.Single, + "Multiple" => ListViewSelectionMode.Multiple, + _ => throw new System.NotImplementedException(), + }; + + public static HorizontalAlignment ConvertStringToHorizontalAlignment(string alignment) => alignment switch + { + "Left" => HorizontalAlignment.Left, + "Center" => HorizontalAlignment.Center, + "Right" => HorizontalAlignment.Right, + "Stretch" => HorizontalAlignment.Stretch, + _ => throw new System.NotImplementedException(), + }; +} + diff --git a/components/Segmented/samples/SegmentedStylesSample.xaml b/components/Segmented/samples/SegmentedStylesSample.xaml new file mode 100644 index 00000000..8139b6bb --- /dev/null +++ b/components/Segmented/samples/SegmentedStylesSample.xaml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + Item 1 + Item 2 + Item with long label + Item 4 + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/Segmented/samples/SegmentedStylesSample.xaml.cs b/components/Segmented/samples/SegmentedStylesSample.xaml.cs new file mode 100644 index 00000000..f3fac18d --- /dev/null +++ b/components/Segmented/samples/SegmentedStylesSample.xaml.cs @@ -0,0 +1,25 @@ +// 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 SegmentedExperiment.Samples; + +/// +/// An sample that shows how the Segmented control has multiple built-in styles. +/// +[ToolkitSampleMultiChoiceOption("SelectionMode", "Single", "Multiple", Title = "Selection mode")] + +[ToolkitSample(id: nameof(SegmentedStylesSample), "Additional styles", description: "A sample on how to use different built-in styles.")] +public sealed partial class SegmentedStylesSample : Page +{ + public SegmentedStylesSample() + { + this.InitializeComponent(); + } + public static ListViewSelectionMode ConvertStringToSelectionMode(string selectionMode) => selectionMode switch + { + "Single" => ListViewSelectionMode.Single, + "Multiple" => ListViewSelectionMode.Multiple, + _ => throw new System.NotImplementedException(), + }; +} diff --git a/components/Segmented/src/AdditionalAssemblyInfo.cs b/components/Segmented/src/AdditionalAssemblyInfo.cs new file mode 100644 index 00000000..b9bcb166 --- /dev/null +++ b/components/Segmented/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("Segmented.Tests.Uwp")] +[assembly: InternalsVisibleTo("Segmented.Tests.WinAppSdk")] +[assembly: InternalsVisibleTo("CommunityToolkit.Tests.Uwp")] +[assembly: InternalsVisibleTo("CommunityToolkit.Tests.WinAppSdk")] diff --git a/components/Segmented/src/CommunityToolkit.WinUI.Controls.Segmented.csproj b/components/Segmented/src/CommunityToolkit.WinUI.Controls.Segmented.csproj new file mode 100644 index 00000000..0091a696 --- /dev/null +++ b/components/Segmented/src/CommunityToolkit.WinUI.Controls.Segmented.csproj @@ -0,0 +1,17 @@ + + + Segmented + This package contains Segmented. + 8.0.0-beta.1 + + + CommunityToolkit.WinUI.Controls.SegmentedRns + + + + + + + + + diff --git a/components/Segmented/src/Dependencies.props b/components/Segmented/src/Dependencies.props new file mode 100644 index 00000000..e622e1df --- /dev/null +++ b/components/Segmented/src/Dependencies.props @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/Segmented/src/EqualPanel.cs b/components/Segmented/src/EqualPanel.cs new file mode 100644 index 00000000..9e2eb548 --- /dev/null +++ b/components/Segmented/src/EqualPanel.cs @@ -0,0 +1,101 @@ +// 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.Data; + +namespace CommunityToolkit.WinUI.Controls; + +public partial class EqualPanel : Panel +{ + private double _maxItemWidth = 0; + private double _maxItemHeight = 0; + private int _visibleItemsCount = 0; + public double Spacing + { + get { return (double)GetValue(SpacingProperty); } + set { SetValue(SpacingProperty, value); } + } + + /// + /// Identifies the Spacing dependency property. + /// + /// The identifier for the dependency property. + public static readonly DependencyProperty SpacingProperty = DependencyProperty.Register( + nameof(Spacing), + typeof(double), + typeof(EqualPanel), + new PropertyMetadata(default(double), OnSpacingChanged)); + + public EqualPanel() + { + RegisterPropertyChangedCallback(HorizontalAlignmentProperty, OnHorizontalAlignmentChanged); + } + + protected override Size MeasureOverride(Size availableSize) + { + _maxItemWidth = 0; + _maxItemHeight = 0; + + var elements = Children.Where(static e => e.Visibility == Visibility.Visible); + _visibleItemsCount = elements.Count(); + + foreach (var child in elements) + { + child.Measure(availableSize); + _maxItemWidth = Math.Max(_maxItemWidth, child.DesiredSize.Width); + _maxItemHeight = Math.Max(_maxItemHeight, child.DesiredSize.Height); + } + + if (_visibleItemsCount > 0) + { + // Return equal widths based on the widest item + // In very specific edge cases the AvailableWidth might be infinite resulting in a crash. + if (HorizontalAlignment != HorizontalAlignment.Stretch || double.IsInfinity(availableSize.Width)) + { + return new Size((_maxItemWidth * _visibleItemsCount) + (Spacing * (_visibleItemsCount - 1)), _maxItemHeight); + } + else + { + // Equal columns based on the available width, adjust for spacing + double totalWidth = availableSize.Width - (Spacing * (_visibleItemsCount - 1)); + _maxItemWidth = totalWidth / _visibleItemsCount; + return new Size(availableSize.Width, _maxItemHeight); + } + } + else + { + return new Size(0, 0); + } + } + + protected override Size ArrangeOverride(Size finalSize) + { + double x = 0; + + // Check if there's more width available - if so, recalculate (e.g. whenever Grid.Column is set to Auto) + if (finalSize.Width > _visibleItemsCount * _maxItemWidth + (Spacing * (_visibleItemsCount - 1))) + { + MeasureOverride(finalSize); + } + + var elements = Children.Where(static e => e.Visibility == Visibility.Visible); + foreach (var child in elements) + { + child.Arrange(new Rect(x, 0, _maxItemWidth, _maxItemHeight)); + x += _maxItemWidth + Spacing; + } + return finalSize; + } + + private void OnHorizontalAlignmentChanged(DependencyObject sender, DependencyProperty dp) + { + InvalidateMeasure(); + } + + private static void OnSpacingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var panel = (EqualPanel)d; + panel.InvalidateMeasure(); + } +} diff --git a/components/Segmented/src/Helpers/ControlHelpers.cs b/components/Segmented/src/Helpers/ControlHelpers.cs new file mode 100644 index 00000000..28c3c8af --- /dev/null +++ b/components/Segmented/src/Helpers/ControlHelpers.cs @@ -0,0 +1,10 @@ +// 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; + +internal static partial class ControlHelpers +{ + internal static bool IsXamlRootAvailable { get; } = Windows.Foundation.Metadata.ApiInformation.IsPropertyPresent("Windows.UI.Xaml.UIElement", "XamlRoot"); +} diff --git a/components/Segmented/src/Helpers/SegmentedMarginConverter.cs b/components/Segmented/src/Helpers/SegmentedMarginConverter.cs new file mode 100644 index 00000000..9de95ded --- /dev/null +++ b/components/Segmented/src/Helpers/SegmentedMarginConverter.cs @@ -0,0 +1,79 @@ +// 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 SegmentedMarginConverter : DependencyObject, IValueConverter +{ + /// + /// Identifies the property. + /// + public static readonly DependencyProperty LeftItemMarginProperty = + DependencyProperty.Register(nameof(LeftItemMargin), typeof(Thickness), typeof(SegmentedMarginConverter), new PropertyMetadata(null)); + + /// + /// Gets or sets the margin of the first item + /// + public Thickness LeftItemMargin + { + get { return (Thickness)GetValue(LeftItemMarginProperty); } + set { SetValue(LeftItemMarginProperty, value); } + } + + /// + /// Identifies the property. + /// + public static readonly DependencyProperty MiddleItemMarginProperty = + DependencyProperty.Register(nameof(MiddleItemMargin), typeof(Thickness), typeof(SegmentedMarginConverter), new PropertyMetadata(null)); + + /// + /// Gets or sets the margin of any middle item + /// + public Thickness MiddleItemMargin + { + get { return (Thickness)GetValue(MiddleItemMarginProperty); } + set { SetValue(MiddleItemMarginProperty, value); } + } + + /// + /// Identifies the property. + /// + public static readonly DependencyProperty RightItemMarginProperty = + DependencyProperty.Register(nameof(RightItemMargin), typeof(Thickness), typeof(SegmentedMarginConverter), new PropertyMetadata(null)); + + /// + /// Gets or sets the margin of the last item + /// + public Thickness RightItemMargin + { + get { return (Thickness)GetValue(RightItemMarginProperty); } + set { SetValue(RightItemMarginProperty, value); } + } + + public object Convert(object value, Type targetType, object parameter, string language) + { + var segmentedItem = value as SegmentedItem; + var listView = ItemsControl.ItemsControlFromItemContainer(segmentedItem); + + int index = listView.IndexFromContainer(segmentedItem); + + if (index == 0) + { + return LeftItemMargin; + } + else if (index == listView.Items.Count - 1) + { + return RightItemMargin; + } + else + { + return MiddleItemMargin; + } + } + + public object ConvertBack(object value, Type targetType, object parameter, string language) + { + return value; + } +} diff --git a/components/Segmented/src/MultiTarget.props b/components/Segmented/src/MultiTarget.props new file mode 100644 index 00000000..b11c1942 --- /dev/null +++ b/components/Segmented/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/Segmented/src/Segmented/Segmented.cs b/components/Segmented/src/Segmented/Segmented.cs new file mode 100644 index 00000000..75e0fb0f --- /dev/null +++ b/components/Segmented/src/Segmented/Segmented.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. + +using Windows.System; + +namespace CommunityToolkit.WinUI.Controls; + +public partial class Segmented : ListViewBase +{ + private int _internalSelectedIndex = -1; + private bool _hasLoaded = false; + + public Segmented() + { + this.DefaultStyleKey = typeof(Segmented); + + RegisterPropertyChangedCallback(SelectedIndexProperty, OnSelectedIndexChanged); + } + + protected override DependencyObject GetContainerForItemOverride() => new SegmentedItem(); + + protected override bool IsItemItsOwnContainerOverride(object item) + { + return item is SegmentedItem; + } + + protected override void OnApplyTemplate() + { + base.OnApplyTemplate(); + if (!_hasLoaded) + { + SelectedIndex = _internalSelectedIndex; + _hasLoaded = true; + } + PreviewKeyDown -= Segmented_PreviewKeyDown; + PreviewKeyDown += Segmented_PreviewKeyDown; + } + + protected override void PrepareContainerForItemOverride(DependencyObject element, object item) + { + base.PrepareContainerForItemOverride(element, item); + if (element is SegmentedItem segmentedItem) + { + segmentedItem.Loaded += SegmentedItem_Loaded; + } + } + + private void Segmented_PreviewKeyDown(object sender, KeyRoutedEventArgs e) + { + switch (e.Key) + { + case VirtualKey.Left: e.Handled = MoveFocus(MoveDirection.Previous); break; + case VirtualKey.Right: e.Handled = MoveFocus(MoveDirection.Next); break; + } + } + + private void SegmentedItem_Loaded(object sender, RoutedEventArgs e) + { + if (sender is SegmentedItem segmentedItem) + { + segmentedItem.Loaded -= SegmentedItem_Loaded; + } + } + + protected override void OnItemsChanged(object e) + { + base.OnItemsChanged(e); + } + + private enum MoveDirection + { + Next, + Previous + } + + /// + /// Adjust the selected item and range based on keyboard input. + /// This is used to override the ListView behaviors for up/down arrow manipulation vs left/right for a horizontal control + /// + /// direction to move the selection + /// True if the focus was moved, false otherwise + private bool MoveFocus(MoveDirection direction) + { + bool retVal = false; + var currentContainerItem = GetCurrentContainerItem(); + + if (currentContainerItem != null) + { + var currentItem = ItemFromContainer(currentContainerItem); + var previousIndex = Items.IndexOf(currentItem); + var index = previousIndex; + + if (direction == MoveDirection.Previous) + { + if (previousIndex > 0) + { + index -= 1; + } + else + { + retVal = true; + } + } + else if (direction == MoveDirection.Next) + { + if (previousIndex < Items.Count - 1) + { + index += 1; + } + } + + // Only do stuff if the index is actually changing + if (index != previousIndex && ContainerFromIndex(index) is SegmentedItem newItem) + { + newItem.Focus(FocusState.Keyboard); + retVal = true; + } + } + + return retVal; + } + + private SegmentedItem? GetCurrentContainerItem() + { + if (ControlHelpers.IsXamlRootAvailable && XamlRoot != null) + { + return FocusManager.GetFocusedElement(XamlRoot) as SegmentedItem; + } + else + { + return FocusManager.GetFocusedElement() as SegmentedItem; + } + } + + private void OnSelectedIndexChanged(DependencyObject sender, DependencyProperty dp) + { + // This is a workaround for https://github.com/microsoft/microsoft-ui-xaml/issues/8257 + if (_internalSelectedIndex == -1 && SelectedIndex > -1) + { + // We catch the correct SelectedIndex and save it. + _internalSelectedIndex = SelectedIndex; + } + } +} diff --git a/components/Segmented/src/Segmented/Segmented.xaml b/components/Segmented/src/Segmented/Segmented.xaml new file mode 100644 index 00000000..3db401c9 --- /dev/null +++ b/components/Segmented/src/Segmented/Segmented.xaml @@ -0,0 +1,121 @@ + + + + + + + + + + 1 + + + + + 1 + + + + + 1 + + + + + 1 + 2 + + + + + + + diff --git a/components/Segmented/src/SegmentedItem/SegmentedItem.Properties.cs b/components/Segmented/src/SegmentedItem/SegmentedItem.Properties.cs new file mode 100644 index 00000000..d7bf2824 --- /dev/null +++ b/components/Segmented/src/SegmentedItem/SegmentedItem.Properties.cs @@ -0,0 +1,26 @@ +// 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 SegmentedItem : ListViewItem +{ + /// + /// The backing for the property. + /// + public static readonly DependencyProperty IconProperty = DependencyProperty.Register( + nameof(Icon), + typeof(IconElement), + typeof(SegmentedItem), + new PropertyMetadata(defaultValue: null, (d, e) => ((SegmentedItem)d).OnIconPropertyChanged((IconElement)e.OldValue, (IconElement)e.NewValue))); + + /// + /// Gets or sets the icon. + /// + public IconElement Icon + { + get => (IconElement)GetValue(IconProperty); + set => SetValue(IconProperty, value); + } +} diff --git a/components/Segmented/src/SegmentedItem/SegmentedItem.cs b/components/Segmented/src/SegmentedItem/SegmentedItem.cs new file mode 100644 index 00000000..94a9769f --- /dev/null +++ b/components/Segmented/src/SegmentedItem/SegmentedItem.cs @@ -0,0 +1,60 @@ +// 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; + +[ContentProperty(Name = nameof(Content))] +public partial class SegmentedItem : ListViewItem +{ + internal const string IconLeftState = "IconLeft"; + internal const string IconOnlyState = "IconOnly"; + internal const string ContentOnlyState = "ContentOnly"; + + public SegmentedItem() + { + this.DefaultStyleKey = typeof(SegmentedItem); + } + + protected override void OnApplyTemplate() + { + base.OnApplyTemplate(); + OnIconChanged(); + ContentChanged(); + } + + protected override void OnContentChanged(object oldContent, object newContent) + { + base.OnContentChanged(oldContent, newContent); + ContentChanged(); + } + + private void ContentChanged() + { + if (Content != null) + { + VisualStateManager.GoToState(this, IconLeftState, true); + } + else + { + VisualStateManager.GoToState(this, IconOnlyState, true); + } + } + + protected virtual void OnIconPropertyChanged(IconElement oldValue, IconElement newValue) + { + OnIconChanged(); + } + + private void OnIconChanged() + { + if (Icon != null) + { + VisualStateManager.GoToState(this, IconLeftState, true); + } + else + { + VisualStateManager.GoToState(this, ContentOnlyState, true); + } + } +} diff --git a/components/Segmented/src/SegmentedItem/SegmentedItem.xaml b/components/Segmented/src/SegmentedItem/SegmentedItem.xaml new file mode 100644 index 00000000..693030c7 --- /dev/null +++ b/components/Segmented/src/SegmentedItem/SegmentedItem.xaml @@ -0,0 +1,1098 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + 0.55 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + 0.55 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + 0.55 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 00:00:00.167 + + + + + + + + diff --git a/components/Segmented/src/Themes/Generic.xaml b/components/Segmented/src/Themes/Generic.xaml new file mode 100644 index 00000000..49299fb7 --- /dev/null +++ b/components/Segmented/src/Themes/Generic.xaml @@ -0,0 +1,10 @@ + + + + + + + + diff --git a/components/Segmented/tests/ExampleSegmentedTestClass.cs b/components/Segmented/tests/ExampleSegmentedTestClass.cs new file mode 100644 index 00000000..c2e040b9 --- /dev/null +++ b/components/Segmented/tests/ExampleSegmentedTestClass.cs @@ -0,0 +1,133 @@ +// 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; + +namespace SegmentedExperiment.Tests; + +[TestClass] +public partial class ExampleSegmentedTestClass : VisualUITestBase +{ + // If you don't need access to UI objects directly or async code, use this pattern. + [TestMethod] + public void SimpleSynchronousExampleTest() + { + var assembly = typeof(Segmented).Assembly; + var type = assembly.GetType(typeof(Segmented).FullName ?? string.Empty); + + Assert.IsNotNull(type, "Could not find Segmented control type."); + Assert.AreEqual(typeof(Segmented), type, "Type of Segmented control 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 Segmented(); + 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(ExampleSegmentedTestPage 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("SegmentedControl"); + + Assert.IsNotNull(componentByName); + } + + // You can still do async work with a UIThreadTestMethod as well. + [UIThreadTestMethod] + public async Task SimpleAsyncUIExamplePageTest(ExampleSegmentedTestPage page) + { + // This helper can be used to wait for a rendering pass to complete. + 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 Segmented(); + 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 Segmented(); + 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 Segmented(); + 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/Segmented/tests/ExampleSegmentedTestPage.xaml b/components/Segmented/tests/ExampleSegmentedTestPage.xaml new file mode 100644 index 00000000..3716d1dd --- /dev/null +++ b/components/Segmented/tests/ExampleSegmentedTestPage.xaml @@ -0,0 +1,14 @@ + + + + + + + diff --git a/components/Segmented/tests/ExampleSegmentedTestPage.xaml.cs b/components/Segmented/tests/ExampleSegmentedTestPage.xaml.cs new file mode 100644 index 00000000..23376e02 --- /dev/null +++ b/components/Segmented/tests/ExampleSegmentedTestPage.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 SegmentedExperiment.Tests; + +/// +/// An empty page that can be used on its own or navigated to within a Frame. +/// +public sealed partial class ExampleSegmentedTestPage : Page +{ + public ExampleSegmentedTestPage() + { + this.InitializeComponent(); + } +} diff --git a/components/Segmented/tests/Segmented.Tests.projitems b/components/Segmented/tests/Segmented.Tests.projitems new file mode 100644 index 00000000..9c0e88b3 --- /dev/null +++ b/components/Segmented/tests/Segmented.Tests.projitems @@ -0,0 +1,23 @@ + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + true + AA9FB2D1-9A19-4442-A2FC-B62003F99558 + + + SegmentedExperiment.Tests + + + + + ExampleSegmentedTestPage.xaml + + + + + Designer + MSBuild:Compile + + + \ No newline at end of file diff --git a/components/Segmented/tests/Segmented.Tests.shproj b/components/Segmented/tests/Segmented.Tests.shproj new file mode 100644 index 00000000..9c2cfd0e --- /dev/null +++ b/components/Segmented/tests/Segmented.Tests.shproj @@ -0,0 +1,13 @@ + + + + AA9FB2D1-9A19-4442-A2FC-B62003F99558 + 14.0 + + + + + + + + From 89961e421179c9bf3158a773cf536c0b7ff3ee9d Mon Sep 17 00:00:00 2001 From: Niels Laute Date: Sat, 22 Jul 2023 21:01:54 +0200 Subject: [PATCH 2/5] XAML styler --- components/Segmented/samples/SegmentedBasicSample.xaml | 5 +++-- components/Segmented/samples/SegmentedStylesSample.xaml | 5 +++-- components/Segmented/src/Segmented/Segmented.xaml | 4 ++-- components/Segmented/src/SegmentedItem/SegmentedItem.xaml | 6 +++--- components/Segmented/src/Themes/Generic.xaml | 2 +- components/Segmented/tests/ExampleSegmentedTestPage.xaml | 4 ++-- 6 files changed, 14 insertions(+), 12 deletions(-) diff --git a/components/Segmented/samples/SegmentedBasicSample.xaml b/components/Segmented/samples/SegmentedBasicSample.xaml index 7751848f..981e7609 100644 --- a/components/Segmented/samples/SegmentedBasicSample.xaml +++ b/components/Segmented/samples/SegmentedBasicSample.xaml @@ -1,4 +1,4 @@ - + - diff --git a/components/Segmented/samples/SegmentedStylesSample.xaml b/components/Segmented/samples/SegmentedStylesSample.xaml index 8139b6bb..511c7eb2 100644 --- a/components/Segmented/samples/SegmentedStylesSample.xaml +++ b/components/Segmented/samples/SegmentedStylesSample.xaml @@ -1,4 +1,4 @@ - + - + + tk:FrameworkElementExtensions.AncestorType="local:Segmented" + Spacing="{ThemeResource SegmentedItemSpacing}" /> diff --git a/components/Segmented/src/SegmentedItem/SegmentedItem.xaml b/components/Segmented/src/SegmentedItem/SegmentedItem.xaml index 693030c7..f4cfc4d5 100644 --- a/components/Segmented/src/SegmentedItem/SegmentedItem.xaml +++ b/components/Segmented/src/SegmentedItem/SegmentedItem.xaml @@ -366,9 +366,9 @@ TargetType="local:SegmentedItem" /> + LeftItemMargin="{StaticResource LeftItemHoverMargin}" + MiddleItemMargin="{StaticResource MiddleItemHoverMargin}" + RightItemMargin="{StaticResource RightItemHoverMargin}" /> 3, 3, 1, 3 1, 3, 1, 3 diff --git a/components/Segmented/src/Themes/Generic.xaml b/components/Segmented/src/Themes/Generic.xaml index 49299fb7..acf563f4 100644 --- a/components/Segmented/src/Themes/Generic.xaml +++ b/components/Segmented/src/Themes/Generic.xaml @@ -3,7 +3,7 @@ xmlns:controls="using:CommunityToolkit.WinUI.Controls"> - + diff --git a/components/Segmented/tests/ExampleSegmentedTestPage.xaml b/components/Segmented/tests/ExampleSegmentedTestPage.xaml index 3716d1dd..3223a11a 100644 --- a/components/Segmented/tests/ExampleSegmentedTestPage.xaml +++ b/components/Segmented/tests/ExampleSegmentedTestPage.xaml @@ -1,9 +1,9 @@ - + From d980525120129de752d29e5345ddac9d7ee48a99 Mon Sep 17 00:00:00 2001 From: Niels Laute Date: Mon, 24 Jul 2023 18:51:40 +0200 Subject: [PATCH 3/5] Update SegmentedBasicSample.xaml --- components/Segmented/samples/SegmentedBasicSample.xaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/components/Segmented/samples/SegmentedBasicSample.xaml b/components/Segmented/samples/SegmentedBasicSample.xaml index 981e7609..47d31e96 100644 --- a/components/Segmented/samples/SegmentedBasicSample.xaml +++ b/components/Segmented/samples/SegmentedBasicSample.xaml @@ -1,4 +1,4 @@ - + From 267fd52b99f95076deec5a28fc268fbe349afde2 Mon Sep 17 00:00:00 2001 From: michael-hawker <24302614+michael-hawker@users.noreply.github.com> Date: Mon, 31 Jul 2023 10:37:43 -0700 Subject: [PATCH 4/5] Remove reference from Helpers and only reference Extensions --- .../src/CommunityToolkit.WinUI.Controls.Segmented.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/Segmented/src/CommunityToolkit.WinUI.Controls.Segmented.csproj b/components/Segmented/src/CommunityToolkit.WinUI.Controls.Segmented.csproj index 0091a696..8c4f28ca 100644 --- a/components/Segmented/src/CommunityToolkit.WinUI.Controls.Segmented.csproj +++ b/components/Segmented/src/CommunityToolkit.WinUI.Controls.Segmented.csproj @@ -8,8 +8,8 @@ CommunityToolkit.WinUI.Controls.SegmentedRns - - + + From b83c388c2bc8db4ba6a9d23cb75d6d65f8b3e8dc Mon Sep 17 00:00:00 2001 From: michael-hawker <24302614+michael-hawker@users.noreply.github.com> Date: Thu, 3 Aug 2023 10:49:27 -0700 Subject: [PATCH 5/5] Remove unneeded assembly info and version tag from Segmented Control Also ensure PackageId is setup correctly --- components/Segmented/src/AdditionalAssemblyInfo.cs | 13 ------------- ...CommunityToolkit.WinUI.Controls.Segmented.csproj | 5 ++++- 2 files changed, 4 insertions(+), 14 deletions(-) delete mode 100644 components/Segmented/src/AdditionalAssemblyInfo.cs diff --git a/components/Segmented/src/AdditionalAssemblyInfo.cs b/components/Segmented/src/AdditionalAssemblyInfo.cs deleted file mode 100644 index b9bcb166..00000000 --- a/components/Segmented/src/AdditionalAssemblyInfo.cs +++ /dev/null @@ -1,13 +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 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("Segmented.Tests.Uwp")] -[assembly: InternalsVisibleTo("Segmented.Tests.WinAppSdk")] -[assembly: InternalsVisibleTo("CommunityToolkit.Tests.Uwp")] -[assembly: InternalsVisibleTo("CommunityToolkit.Tests.WinAppSdk")] diff --git a/components/Segmented/src/CommunityToolkit.WinUI.Controls.Segmented.csproj b/components/Segmented/src/CommunityToolkit.WinUI.Controls.Segmented.csproj index 8c4f28ca..5cc12018 100644 --- a/components/Segmented/src/CommunityToolkit.WinUI.Controls.Segmented.csproj +++ b/components/Segmented/src/CommunityToolkit.WinUI.Controls.Segmented.csproj @@ -2,7 +2,6 @@ Segmented This package contains Segmented. - 8.0.0-beta.1 CommunityToolkit.WinUI.Controls.SegmentedRns @@ -14,4 +13,8 @@ + + + $(PackageIdPrefix).$(PackageIdVariant).Controls.$(ToolkitComponentName) +