Skip to content

Commit 6542f93

Browse files
authored
Merge pull request #373 from CommunityToolkit/niels9001/settingscontrolsfixes
[SettingsControls] Bugfixes and improvements
2 parents 5765909 + af298ff commit 6542f93

File tree

8 files changed

+186
-69
lines changed

8 files changed

+186
-69
lines changed

components/SettingsControls/samples/SettingsExpanderSample.xaml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<!-- 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. -->
1+
<!-- 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. -->
22
<Page x:Class="SettingsControlsExperiment.Samples.SettingsExpanderSample"
33
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
44
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
@@ -11,7 +11,6 @@
1111
<labs:SettingsExpander x:Name="settingsCard"
1212
Description="The SettingsExpander has the same properties as a Card, and you can set SettingsCard as part of the Items collection."
1313
Header="SettingsExpander"
14-
IsEnabled="{x:Bind IsCardEnabled, Mode=OneWay}"
1514
IsExpanded="{x:Bind IsCardExpanded, Mode=OneWay}">
1615
<!-- TODO: This should be TwoWay bound but throws compile error in Uno. -->
1716
<labs:SettingsExpander.HeaderIcon>

components/SettingsControls/samples/SettingsExpanderSample.xaml.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44

55
namespace SettingsControlsExperiment.Samples;
66

7-
[ToolkitSampleBoolOption("IsCardEnabled", true, Title = "Is Enabled")]
87
[ToolkitSampleBoolOption("IsCardExpanded", false, Title = "Is Expanded")]
8+
// [ToolkitSampleBoolOption("IsCardEnabled", true, Title = "Is Enabled")] Disabled for now, see: https://github.com/CommunityToolkit/Labs-Windows/issues/362
9+
910
// Single values without a colon are used for both label and value.
1011
// To provide a different label for the value, separate with a colon surrounded by a single space on both sides ("label : value").
1112
//[ToolkitSampleMultiChoiceOption("TextSize", title: "Text size", "Small : 12", "Normal : 16", "Big : 32")]

components/SettingsControls/src/CommunityToolkit.Labs.WinUI.SettingsControls.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<PropertyGroup>
33
<ToolkitComponentName>SettingsControls</ToolkitComponentName>
44
<Description>This package contains the SettingsCard and SettingsExpander controls.</Description>
5-
<Version>0.0.15</Version>
5+
<Version>0.0.16</Version>
66
<LangVersion>10.0</LangVersion>
77

88
<!-- Rns suffix is required for namespaces shared across projects. See https://github.com/CommunityToolkit/Labs-Windows/issues/152 -->
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System.Collections;
6+
using System.Collections.Specialized;
7+
8+
#if WINAPPSDK
9+
using CommunityToolkit.WinUI.Helpers;
10+
#else
11+
using Microsoft.Toolkit.Uwp.Helpers;
12+
#endif
13+
14+
15+
namespace CommunityToolkit.Labs.WinUI;
16+
17+
/// <summary>
18+
/// Enables a state if an Object is <c>null</c> or a String/IEnumerable is empty
19+
/// </summary>
20+
public class IsNullOrEmptyStateTrigger : StateTriggerBase
21+
{
22+
/// <summary>
23+
/// Gets or sets the value used to check for <c>null</c> or empty.
24+
/// </summary>
25+
public object Value
26+
{
27+
get { return GetValue(ValueProperty); }
28+
set { SetValue(ValueProperty, value); }
29+
}
30+
31+
/// <summary>
32+
/// Identifies the <see cref="Value"/> DependencyProperty
33+
/// </summary>
34+
public static readonly DependencyProperty ValueProperty =
35+
DependencyProperty.Register(nameof(Value), typeof(object), typeof(IsNullOrEmptyStateTrigger), new PropertyMetadata(null, OnValuePropertyChanged));
36+
37+
public IsNullOrEmptyStateTrigger()
38+
{
39+
UpdateTrigger();
40+
}
41+
42+
private void UpdateTrigger()
43+
{
44+
var val = Value;
45+
46+
SetActive(IsNullOrEmpty(val));
47+
48+
if (val == null)
49+
{
50+
return;
51+
}
52+
53+
// Try to listen for various notification events
54+
// Starting with INorifyCollectionChanged
55+
#pragma warning disable CS8622 // Nullability of reference types
56+
var valNotifyCollection = val as INotifyCollectionChanged;
57+
if (valNotifyCollection != null)
58+
{
59+
var weakEvent = new WeakEventListener<IsNullOrEmptyStateTrigger, object, NotifyCollectionChangedEventArgs>(this)
60+
{
61+
OnEventAction = static (instance, source, args) => instance.SetActive(IsNullOrEmpty(source)),
62+
OnDetachAction = (weakEventListener) => valNotifyCollection.CollectionChanged -= weakEventListener.OnEvent
63+
};
64+
65+
valNotifyCollection.CollectionChanged += weakEvent.OnEvent;
66+
#pragma warning restore CS8622
67+
return;
68+
}
69+
70+
// Not INotifyCollectionChanged, try IObservableVector
71+
var valObservableVector = val as IObservableVector<object>;
72+
if (valObservableVector != null)
73+
{
74+
var weakEvent = new WeakEventListener<IsNullOrEmptyStateTrigger, object, IVectorChangedEventArgs>(this)
75+
{
76+
OnEventAction = static (instance, source, args) => instance.SetActive(IsNullOrEmpty(source)),
77+
OnDetachAction = (weakEventListener) => valObservableVector.VectorChanged -= weakEventListener.OnEvent
78+
};
79+
80+
valObservableVector.VectorChanged += weakEvent.OnEvent;
81+
return;
82+
}
83+
84+
// Not INotifyCollectionChanged, try IObservableMap
85+
var valObservableMap = val as IObservableMap<object, object>;
86+
if (valObservableMap != null)
87+
{
88+
var weakEvent = new WeakEventListener<IsNullOrEmptyStateTrigger, object, IMapChangedEventArgs<object>>(this)
89+
{
90+
OnEventAction = static (instance, source, args) => instance.SetActive(IsNullOrEmpty(source)),
91+
OnDetachAction = (weakEventListener) => valObservableMap.MapChanged -= weakEventListener.OnEvent
92+
};
93+
94+
valObservableMap.MapChanged += weakEvent.OnEvent;
95+
}
96+
}
97+
98+
private static void OnValuePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
99+
{
100+
var obj = (IsNullOrEmptyStateTrigger)d;
101+
obj.UpdateTrigger();
102+
}
103+
104+
private static bool IsNullOrEmpty(object val)
105+
{
106+
if (val == null)
107+
{
108+
return true;
109+
}
110+
111+
// Object is not null, check for an empty string
112+
var valString = val as string;
113+
if (valString != null)
114+
{
115+
return valString.Length == 0;
116+
}
117+
118+
// Object is not a string, check for an empty ICollection (faster)
119+
var valCollection = val as ICollection;
120+
if (valCollection != null)
121+
{
122+
return valCollection.Count == 0;
123+
}
124+
125+
// Object is not an ICollection, check for an empty IEnumerable
126+
var valEnumerable = val as IEnumerable;
127+
if (valEnumerable != null)
128+
{
129+
foreach (var item in valEnumerable)
130+
{
131+
// Found an item, not empty
132+
return false;
133+
}
134+
135+
return true;
136+
}
137+
138+
// Not null and not a known type to test for emptiness
139+
return false;
140+
}
141+
}

components/SettingsControls/src/SettingsCard/SettingsCard.xaml

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -294,22 +294,12 @@
294294
</VisualState.Setters>
295295
</VisualState>
296296
</VisualStateGroup>
297+
297298
<VisualStateGroup x:Name="ContentAlignmentStates">
299+
<!-- Default -->
298300
<VisualState x:Name="Right" />
299-
<VisualState x:Name="Left">
300-
<VisualState.StateTriggers>
301-
<labs:IsEqualStateTrigger Value="{Binding ContentAlignment, RelativeSource={RelativeSource TemplatedParent}}"
302-
To="Left" />
303-
</VisualState.StateTriggers>
304-
<VisualState.Setters>
305-
<Setter Target="PART_HeaderIconPresenterHolder.Visibility" Value="Collapsed" />
306-
<Setter Target="PART_DescriptionPresenter.Visibility" Value="Collapsed" />
307-
<Setter Target="PART_HeaderPresenter.Visibility" Value="Collapsed" />
308-
<Setter Target="PART_ContentPresenter.(Grid.Row)" Value="1" />
309-
<Setter Target="PART_ContentPresenter.(Grid.Column)" Value="1" />
310-
<Setter Target="PART_ContentPresenter.HorizontalAlignment" Value="Left" />
311-
</VisualState.Setters>
312-
</VisualState>
301+
302+
<!-- Whenever the control width is less than SettingsCardWrapThreshold, the Content is below the Header/Description -->
313303
<VisualState x:Name="RightWrapped">
314304
<VisualState.StateTriggers>
315305
<labs:ControlSizeTrigger MinWidth="{ThemeResource SettingsCardWrapNoIconThreshold}"
@@ -325,6 +315,8 @@
325315
<Setter Target="HeaderPanel.Margin" Value="0" />
326316
</VisualState.Setters>
327317
</VisualState>
318+
319+
<!-- For even smaller widths: the HeaderIcon is collapsed. -->
328320
<VisualState x:Name="RightWrappedNoIcon">
329321
<VisualState.StateTriggers>
330322
<labs:ControlSizeTrigger MaxWidth="{ThemeResource SettingsCardWrapNoIconThreshold}"
@@ -340,6 +332,24 @@
340332
<Setter Target="HeaderPanel.Margin" Value="0" />
341333
</VisualState.Setters>
342334
</VisualState>
335+
336+
<!-- Header/Description/Icon collapsed, content is to the left. Great for e.g. Checkboxes -->
337+
<VisualState x:Name="Left">
338+
<VisualState.StateTriggers>
339+
<labs:IsEqualStateTrigger Value="{Binding ContentAlignment, RelativeSource={RelativeSource TemplatedParent}}"
340+
To="Left" />
341+
</VisualState.StateTriggers>
342+
<VisualState.Setters>
343+
<Setter Target="PART_HeaderIconPresenterHolder.Visibility" Value="Collapsed" />
344+
<Setter Target="PART_DescriptionPresenter.Visibility" Value="Collapsed" />
345+
<Setter Target="PART_HeaderPresenter.Visibility" Value="Collapsed" />
346+
<Setter Target="PART_ContentPresenter.(Grid.Row)" Value="1" />
347+
<Setter Target="PART_ContentPresenter.(Grid.Column)" Value="1" />
348+
<Setter Target="PART_ContentPresenter.HorizontalAlignment" Value="Left" />
349+
</VisualState.Setters>
350+
</VisualState>
351+
352+
<!-- Similiar to Left, but the HeaderIcon/Header/Description is visible -->
343353
<VisualState x:Name="Vertical">
344354
<VisualState.StateTriggers>
345355
<labs:IsEqualStateTrigger Value="{Binding ContentAlignment, RelativeSource={RelativeSource TemplatedParent}}"
@@ -354,6 +364,19 @@
354364
</VisualState.Setters>
355365
</VisualState>
356366
</VisualStateGroup>
367+
368+
<!-- Collapsing the Content presenter whenever it's empty -->
369+
<VisualStateGroup x:Name="ContentVisibilityStates">
370+
<VisualState x:Name="Visible" />
371+
<VisualState x:Name="Collapsed">
372+
<VisualState.StateTriggers>
373+
<labs:IsNullOrEmptyStateTrigger Value="{Binding Content, RelativeSource={RelativeSource TemplatedParent}}" />
374+
</VisualState.StateTriggers>
375+
<VisualState.Setters>
376+
<Setter Target="PART_ContentPresenter.Visibility" Value="Collapsed" />
377+
</VisualState.Setters>
378+
</VisualState>
379+
</VisualStateGroup>
357380
</VisualStateManager.VisualStateGroups>
358381

359382
<Viewbox x:Name="PART_HeaderIconPresenterHolder"
@@ -463,6 +486,7 @@
463486
Background="{TemplateBinding Background}"
464487
BorderBrush="{TemplateBinding BorderBrush}"
465488
BorderThickness="{TemplateBinding BorderThickness}"
489+
Control.IsTemplateFocusTarget="True"
466490
CornerRadius="{TemplateBinding CornerRadius}">
467491
<VisualStateManager.VisualStateGroups>
468492
<VisualStateGroup x:Name="CommonStates">
@@ -964,7 +988,6 @@
964988
Grid.ColumnSpan="3"
965989
Margin="0,5"
966990
Background="{ThemeResource ToggleSwitchContainerBackground}"
967-
Control.IsTemplateFocusTarget="True"
968991
CornerRadius="{TemplateBinding CornerRadius}" />
969992
<ContentPresenter x:Name="OffContentPresenter"
970993
Grid.RowSpan="3"

components/SettingsControls/src/SettingsExpander/Internal/StyledContentPresenter.cs

Lines changed: 0 additions & 44 deletions
This file was deleted.

components/SettingsControls/src/SettingsExpander/SettingsExpander.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33
// See the LICENSE file in the project root for more information.
44

5-
using CommunityToolkit.Labs.WinUI.Internal;
6-
75
namespace CommunityToolkit.Labs.WinUI;
86

97
//// Note: ItemsRepeater will request all the available horizontal space: https://github.com/microsoft/microsoft-ui-xaml/issues/3842

components/SettingsControls/src/SettingsExpander/SettingsExpander.xaml

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
<Thickness x:Key="SettingsExpanderItemPadding">58,8,44,8</Thickness>
1717
<Thickness x:Key="SettingsExpanderItemBorderThickness">0,1,0,0</Thickness>
1818
<Thickness x:Key="ClickableSettingsExpanderItemPadding">58,8,16,8</Thickness>
19+
<x:Double x:Key="SettingsExpanderContentMinHeight">16</x:Double>
1920

2021
<Style x:Key="DefaultSettingsExpanderItemStyle"
2122
BasedOn="{StaticResource DefaultSettingsCardStyle}"
@@ -101,9 +102,7 @@
101102
ItemTemplate="{Binding ItemTemplate, Mode=OneWay, RelativeSource={RelativeSource TemplatedParent}}"
102103
TabFocusNavigation="Local">
103104
<muxc:ItemsRepeater.Layout>
104-
<muxc:UniformGridLayout ItemsStretch="Fill"
105-
MaximumRowsOrColumns="1"
106-
Orientation="Horizontal" />
105+
<muxc:StackLayout Orientation="Vertical" />
107106
</muxc:ItemsRepeater.Layout>
108107
</muxc:ItemsRepeater>
109108
<ContentPresenter Grid.Row="2"
@@ -263,7 +262,7 @@
263262
<Border x:Name="ExpanderContentClip"
264263
Grid.Row="1">
265264
<Border x:Name="ExpanderContent"
266-
MinHeight="{TemplateBinding MinHeight}"
265+
MinHeight="{ThemeResource SettingsExpanderContentMinHeight}"
267266
Padding="{TemplateBinding Padding}"
268267
HorizontalAlignment="Stretch"
269268
VerticalAlignment="Stretch"

0 commit comments

Comments
 (0)