Skip to content

Commit 4cd78a4

Browse files
committed
feat: Add Tag styles and demos.
1 parent 5bfbbe1 commit 4cd78a4

7 files changed

Lines changed: 499 additions & 1 deletion

File tree

src/Avalonia/HandyControlDemo_Avalonia/Data/DemoInfo.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@
5454
[ "UniformSpacingPanel", "UniformSpacingPanelDemo", "Brush.IncreaseHorizontalSpacing", "", "Layout" ],
5555
[ "ProgressBar", "ProgressBarDemo", "Brush.ProgressBar", "", "Feedback" ],
5656
[ "Rate", "RateDemo", "Brush.Favorite", "", "Feedback" ],
57-
[ "Loading", "LoadingDemo", "Brush.Loading", "", "Feedback" ]
57+
[ "Loading", "LoadingDemo", "Brush.Loading", "", "Feedback" ],
58+
[ "Tag", "TagDemo", "Brush.Tag", "", "DataDisplay" ]
5859
]
5960
}
6061
]
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<UserControl xmlns="https://github.com/avaloniaui"
2+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
3+
xmlns:hc="https://handyorg.github.io/handycontrol"
4+
x:Class="HandyControlDemo.UserControl.TagDemo"
5+
Background="{DynamicResource RegionBrush}">
6+
<UserControl.Resources>
7+
<ControlTheme x:Key="TagCustomStyle"
8+
BasedOn="{StaticResource {x:Type hc:Tag}}"
9+
TargetType="hc:Tag">
10+
<Setter Property="Content"
11+
Value="{Binding Name}" />
12+
<Setter Property="Margin"
13+
Value="5" />
14+
<Setter Property="IsSelected"
15+
Value="{Binding IsSelected}" />
16+
</ControlTheme>
17+
</UserControl.Resources>
18+
<ScrollViewer>
19+
<StackPanel Margin="11">
20+
<hc:TagContainer Margin="11"
21+
MaxWidth="420"
22+
Padding="5">
23+
<hc:Tag Header="H"
24+
hc:TitleElement.Background="{DynamicResource PrimaryBrush}"
25+
Margin="5"
26+
ShowCloseButton="False"
27+
Content="TextText" />
28+
<hc:Tag Margin="5"
29+
Selectable="True"
30+
Content="TextTextText" />
31+
<hc:Tag Margin="5"
32+
ShowCloseButton="False"
33+
Content="TextTextTextText" />
34+
<hc:Tag Margin="5"
35+
Content="TextTextTextTextText" />
36+
<hc:Tag Margin="5"
37+
IsSelected="True"
38+
Selectable="True"
39+
ShowCloseButton="False"
40+
Content="TextTextTextText" />
41+
<hc:Tag Margin="5"
42+
Content="TextTextText" />
43+
<hc:Tag Margin="5"
44+
ShowCloseButton="False"
45+
Content="TextText">
46+
<hc:Tag.Header>
47+
<Image Source="/Resources/Img/Album/1.jpg" />
48+
</hc:Tag.Header>
49+
</hc:Tag>
50+
<hc:Tag Margin="5"
51+
IsSelected="True"
52+
Selectable="True"
53+
Content="TextTextText">
54+
<hc:Tag.Header>
55+
<Image Source="/Resources/Img/Avatar/avatar2.png" />
56+
</hc:Tag.Header>
57+
</hc:Tag>
58+
<hc:Tag Margin="5"
59+
ShowCloseButton="False"
60+
Content="TextTextTextText" />
61+
<hc:Tag Margin="5"
62+
Content="TextTextTextTextText" />
63+
</hc:TagContainer>
64+
<hc:TagContainer Name="BoundContainer"
65+
Margin="11"
66+
MaxWidth="420"
67+
Padding="5"
68+
ItemContainerTheme="{StaticResource TagCustomStyle}" />
69+
<DockPanel LastChildFill="True"
70+
Margin="11,0,11,11">
71+
<Button Name="AddButton"
72+
Margin="10,0,0,0"
73+
DockPanel.Dock="Right"
74+
Content="Add an item" />
75+
<TextBox Name="TagNameBox" />
76+
</DockPanel>
77+
</StackPanel>
78+
</ScrollViewer>
79+
</UserControl>
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
using System.Collections.ObjectModel;
2+
using Avalonia.Interactivity;
3+
4+
namespace HandyControlDemo.UserControl;
5+
6+
public class TagDemoItem : Avalonia.AvaloniaObject
7+
{
8+
public static readonly Avalonia.StyledProperty<string?> NameProperty =
9+
Avalonia.AvaloniaProperty.Register<TagDemoItem, string?>(nameof(Name));
10+
11+
public static readonly Avalonia.StyledProperty<bool> IsSelectedProperty =
12+
Avalonia.AvaloniaProperty.Register<TagDemoItem, bool>(nameof(IsSelected));
13+
14+
public string? Name
15+
{
16+
get => GetValue(NameProperty);
17+
set => SetValue(NameProperty, value);
18+
}
19+
20+
public bool IsSelected
21+
{
22+
get => GetValue(IsSelectedProperty);
23+
set => SetValue(IsSelectedProperty, value);
24+
}
25+
}
26+
27+
public partial class TagDemo : Avalonia.Controls.UserControl
28+
{
29+
public ObservableCollection<TagDemoItem> DataList { get; } = new();
30+
31+
public TagDemo()
32+
{
33+
InitializeComponent();
34+
35+
for (var i = 1; i <= 10; i++)
36+
{
37+
DataList.Add(new TagDemoItem
38+
{
39+
Name = $"Name{i}",
40+
IsSelected = i % 2 == 0
41+
});
42+
}
43+
44+
BoundContainer.ItemsSource = DataList;
45+
AddButton.Click += AddButton_Click;
46+
}
47+
48+
private void AddButton_Click(object? sender, RoutedEventArgs e)
49+
{
50+
var name = TagNameBox.Text;
51+
if (string.IsNullOrWhiteSpace(name))
52+
{
53+
return;
54+
}
55+
56+
DataList.Insert(0, new TagDemoItem
57+
{
58+
Name = name,
59+
IsSelected = DataList.Count % 2 == 0
60+
});
61+
TagNameBox.Text = string.Empty;
62+
}
63+
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
using Avalonia;
2+
using Avalonia.Controls;
3+
using Avalonia.Controls.Metadata;
4+
using Avalonia.Controls.Primitives;
5+
using Avalonia.Input;
6+
using Avalonia.Interactivity;
7+
8+
namespace HandyControl.Controls;
9+
10+
[TemplatePart(PartCloseButton, typeof(Button))]
11+
public class Tag : HeaderedContentControl
12+
{
13+
public const string PartCloseButton = "PART_CloseButton";
14+
15+
private Button? _closeButton;
16+
17+
public static readonly StyledProperty<bool> ShowCloseButtonProperty =
18+
AvaloniaProperty.Register<Tag, bool>(nameof(ShowCloseButton), true);
19+
20+
public bool ShowCloseButton
21+
{
22+
get => GetValue(ShowCloseButtonProperty);
23+
set => SetValue(ShowCloseButtonProperty, value);
24+
}
25+
26+
public static readonly StyledProperty<bool> SelectableProperty =
27+
AvaloniaProperty.Register<Tag, bool>(nameof(Selectable));
28+
29+
public bool Selectable
30+
{
31+
get => GetValue(SelectableProperty);
32+
set => SetValue(SelectableProperty, value);
33+
}
34+
35+
public static readonly StyledProperty<bool> IsSelectedProperty =
36+
AvaloniaProperty.Register<Tag, bool>(nameof(IsSelected));
37+
38+
public bool IsSelected
39+
{
40+
get => GetValue(IsSelectedProperty);
41+
set => SetValue(IsSelectedProperty, value);
42+
}
43+
44+
public static readonly RoutedEvent<RoutedEventArgs> SelectedEvent =
45+
RoutedEvent.Register<Tag, RoutedEventArgs>(nameof(Selected), RoutingStrategies.Bubble);
46+
47+
public event System.EventHandler<RoutedEventArgs> Selected
48+
{
49+
add => AddHandler(SelectedEvent, value);
50+
remove => RemoveHandler(SelectedEvent, value);
51+
}
52+
53+
public static readonly RoutedEvent<CancelRoutedEventArgs> ClosingEvent =
54+
RoutedEvent.Register<Tag, CancelRoutedEventArgs>(nameof(Closing), RoutingStrategies.Bubble);
55+
56+
public event System.EventHandler<CancelRoutedEventArgs> Closing
57+
{
58+
add => AddHandler(ClosingEvent, value);
59+
remove => RemoveHandler(ClosingEvent, value);
60+
}
61+
62+
public static readonly RoutedEvent<RoutedEventArgs> ClosedEvent =
63+
RoutedEvent.Register<Tag, RoutedEventArgs>(nameof(Closed), RoutingStrategies.Bubble);
64+
65+
public event System.EventHandler<RoutedEventArgs> Closed
66+
{
67+
add => AddHandler(ClosedEvent, value);
68+
remove => RemoveHandler(ClosedEvent, value);
69+
}
70+
71+
static Tag()
72+
{
73+
IsSelectedProperty.Changed.AddClassHandler<Tag>((tag, e) => tag.OnIsSelectedChanged(e));
74+
}
75+
76+
private void OnIsSelectedChanged(AvaloniaPropertyChangedEventArgs e)
77+
{
78+
var newValue = e.GetNewValue<bool>();
79+
PseudoClasses.Set(":selected", newValue);
80+
RaiseEvent(new RoutedEventArgs(SelectedEvent, this));
81+
}
82+
83+
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
84+
{
85+
base.OnApplyTemplate(e);
86+
87+
if (_closeButton != null)
88+
{
89+
_closeButton.Click -= CloseButton_Click;
90+
}
91+
92+
_closeButton = e.NameScope.Find<Button>(PartCloseButton);
93+
94+
if (_closeButton != null)
95+
{
96+
_closeButton.Click += CloseButton_Click;
97+
}
98+
}
99+
100+
private void CloseButton_Click(object? sender, RoutedEventArgs e)
101+
{
102+
RequestClose();
103+
}
104+
105+
internal void RequestClose()
106+
{
107+
var argsClosing = new CancelRoutedEventArgs(ClosingEvent, this);
108+
RaiseEvent(argsClosing);
109+
if (argsClosing.Cancel)
110+
{
111+
return;
112+
}
113+
114+
RaiseEvent(new RoutedEventArgs(ClosedEvent, this));
115+
}
116+
117+
protected override void OnPointerPressed(PointerPressedEventArgs e)
118+
{
119+
base.OnPointerPressed(e);
120+
121+
if (Selectable && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
122+
{
123+
IsSelected = true;
124+
}
125+
}
126+
127+
public void Hide()
128+
{
129+
IsVisible = false;
130+
}
131+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
using System.Collections;
2+
using Avalonia;
3+
using Avalonia.Controls;
4+
using Avalonia.Interactivity;
5+
6+
namespace HandyControl.Controls;
7+
8+
public class TagContainer : ItemsControl
9+
{
10+
public static readonly AttachedProperty<bool> ShowCloseButtonProperty =
11+
AvaloniaProperty.RegisterAttached<TagContainer, AvaloniaObject, bool>(
12+
"ShowCloseButton", defaultValue: true, inherits: true);
13+
14+
public static void SetShowCloseButton(AvaloniaObject element, bool value) =>
15+
element.SetValue(ShowCloseButtonProperty, value);
16+
17+
public static bool GetShowCloseButton(AvaloniaObject element) =>
18+
element.GetValue(ShowCloseButtonProperty);
19+
20+
public TagContainer()
21+
{
22+
AddHandler(HandyControl.Controls.Tag.ClosedEvent, Tag_OnClosed);
23+
}
24+
25+
private void Tag_OnClosed(object? sender, RoutedEventArgs e)
26+
{
27+
if (e.Source is not Tag tag)
28+
{
29+
return;
30+
}
31+
32+
tag.Hide();
33+
34+
if (ItemsSource == null)
35+
{
36+
Items.Remove(tag);
37+
}
38+
else if (ItemsSource is IList list)
39+
{
40+
var item = ItemFromContainer(tag);
41+
if (item != null && list.Contains(item))
42+
{
43+
list.Remove(item);
44+
}
45+
}
46+
}
47+
48+
protected override bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
49+
{
50+
if (item is Tag)
51+
{
52+
recycleKey = null;
53+
return false;
54+
}
55+
56+
recycleKey = typeof(Tag);
57+
return true;
58+
}
59+
60+
protected override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey) => new Tag();
61+
62+
protected override void PrepareContainerForItemOverride(Control container, object? item, int index)
63+
{
64+
base.PrepareContainerForItemOverride(container, item, index);
65+
66+
// ItemsControl auto-binds HeaderedContentControl.Header to the item.
67+
// Tag is a HeaderedContentControl but, in TagContainer, the Header is meant
68+
// for an explicit avatar/icon. Clear the auto-assigned Header to prevent the
69+
// item's ToString() bleeding into the header circle. Skip when the item IS
70+
// the container (Tag declared directly in XAML) so an explicit Header is kept.
71+
if (container is Tag tag && !ReferenceEquals(container, item))
72+
{
73+
tag.ClearValue(Avalonia.Controls.Primitives.HeaderedContentControl.HeaderProperty);
74+
}
75+
}
76+
}

src/Avalonia/HandyControl_Avalonia/Themes/Styles/MergedStyles.axaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
<MergeResourceInclude Source="avares://HandyControl/Themes/Styles/ElementGroup.axaml" />
7171
<MergeResourceInclude Source="avares://HandyControl/Themes/Styles/ButtonGroup.axaml" />
7272
<MergeResourceInclude Source="avares://HandyControl/Themes/Styles/Gravatar.axaml" />
73+
<MergeResourceInclude Source="avares://HandyControl/Themes/Styles/Tag.axaml" />
7374
</ResourceDictionary.MergedDictionaries>
7475
</ResourceDictionary>
7576
</Styles.Resources>

0 commit comments

Comments
 (0)