diff --git a/demo/Ursa.Demo/Models/MenuKeys.cs b/demo/Ursa.Demo/Models/MenuKeys.cs
index 2b30912..7f0c655 100644
--- a/demo/Ursa.Demo/Models/MenuKeys.cs
+++ b/demo/Ursa.Demo/Models/MenuKeys.cs
@@ -6,6 +6,7 @@ public static class MenuKeys
public const string MenuKeyBadge = "Badge";
public const string MenuKeyBanner = "Banner";
public const string MenuKeyButtonGroup = "ButtonGroup";
+ public const string MenuKeyClassInput = "Class Input";
public const string MenuKeyDialog = "Dialog";
public const string MenuKeyDivider = "Divider";
public const string MenuKeyDualBadge = "DualBadge";
diff --git a/demo/Ursa.Demo/Pages/BannerDemo.axaml b/demo/Ursa.Demo/Pages/BannerDemo.axaml
index ac539e7..d7a275e 100644
--- a/demo/Ursa.Demo/Pages/BannerDemo.axaml
+++ b/demo/Ursa.Demo/Pages/BannerDemo.axaml
@@ -47,6 +47,5 @@
-
diff --git a/demo/Ursa.Demo/Pages/ClassInputDemo.axaml b/demo/Ursa.Demo/Pages/ClassInputDemo.axaml
new file mode 100644
index 0000000..344b77e
--- /dev/null
+++ b/demo/Ursa.Demo/Pages/ClassInputDemo.axaml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/demo/Ursa.Demo/Pages/ClassInputDemo.axaml.cs b/demo/Ursa.Demo/Pages/ClassInputDemo.axaml.cs
new file mode 100644
index 0000000..30e502a
--- /dev/null
+++ b/demo/Ursa.Demo/Pages/ClassInputDemo.axaml.cs
@@ -0,0 +1,13 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+
+namespace Ursa.Demo.Pages;
+
+public partial class ClassInputDemo : UserControl
+{
+ public ClassInputDemo()
+ {
+ InitializeComponent();
+ }
+}
\ No newline at end of file
diff --git a/demo/Ursa.Demo/ViewModels/ClassInputDemoViewModel.cs b/demo/Ursa.Demo/ViewModels/ClassInputDemoViewModel.cs
new file mode 100644
index 0000000..971131f
--- /dev/null
+++ b/demo/Ursa.Demo/ViewModels/ClassInputDemoViewModel.cs
@@ -0,0 +1,8 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+
+namespace Ursa.Demo.ViewModels;
+
+public class ClassInputDemoViewModel: ObservableObject
+{
+
+}
\ No newline at end of file
diff --git a/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs b/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs
index 50ab100..752530f 100644
--- a/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs
+++ b/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs
@@ -28,6 +28,7 @@ public class MainViewViewModel : ViewModelBase
MenuKeys.MenuKeyBadge => new BadgeDemoViewModel(),
MenuKeys.MenuKeyBanner => new BannerDemoViewModel(),
MenuKeys.MenuKeyButtonGroup => new ButtonGroupDemoViewModel(),
+ MenuKeys.MenuKeyClassInput => new ClassInputDemoViewModel(),
MenuKeys.MenuKeyDialog => new DialogDemoViewModel(),
MenuKeys.MenuKeyDivider => new DividerDemoViewModel(),
MenuKeys.MenuKeyDualBadge => new DualBadgeDemoViewModel(),
diff --git a/demo/Ursa.Demo/ViewModels/MenuViewModel.cs b/demo/Ursa.Demo/ViewModels/MenuViewModel.cs
index 672c5b4..0f7c41e 100644
--- a/demo/Ursa.Demo/ViewModels/MenuViewModel.cs
+++ b/demo/Ursa.Demo/ViewModels/MenuViewModel.cs
@@ -15,6 +15,7 @@ public class MenuViewModel: ViewModelBase
new() { MenuHeader = "Badge", Key = MenuKeys.MenuKeyBadge },
new() { MenuHeader = "Banner", Key = MenuKeys.MenuKeyBanner },
new() { MenuHeader = "Button Group", Key = MenuKeys.MenuKeyButtonGroup, Status = "Updated"},
+ new() { MenuHeader = "Class Input", Key = MenuKeys.MenuKeyClassInput, Status = "New" },
new() { MenuHeader = "Dialog", Key = MenuKeys.MenuKeyDialog },
new() { MenuHeader = "Divider", Key = MenuKeys.MenuKeyDivider },
new() { MenuHeader = "DualBadge", Key = MenuKeys.MenuKeyDualBadge },
diff --git a/src/Ursa.Themes.Semi/Controls/ControlClassesInput.axaml b/src/Ursa.Themes.Semi/Controls/ControlClassesInput.axaml
new file mode 100644
index 0000000..3f5f640
--- /dev/null
+++ b/src/Ursa.Themes.Semi/Controls/ControlClassesInput.axaml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Ursa.Themes.Semi/Controls/_index.axaml b/src/Ursa.Themes.Semi/Controls/_index.axaml
index f21a847..7c21320 100644
--- a/src/Ursa.Themes.Semi/Controls/_index.axaml
+++ b/src/Ursa.Themes.Semi/Controls/_index.axaml
@@ -4,6 +4,7 @@
+
diff --git a/src/Ursa/Controls/ControlClassesInput/ControlClassesInput.cs b/src/Ursa/Controls/ControlClassesInput/ControlClassesInput.cs
new file mode 100644
index 0000000..78cf60d
--- /dev/null
+++ b/src/Ursa/Controls/ControlClassesInput/ControlClassesInput.cs
@@ -0,0 +1,177 @@
+using System.Collections.ObjectModel;
+using System.Collections.Specialized;
+using Avalonia;
+using Avalonia.Collections;
+using Avalonia.Controls;
+using Avalonia.Controls.Primitives;
+
+namespace Ursa.Controls;
+
+public class ControlClassesInput: TemplatedControl
+{
+ LinkedList> _history = new();
+ LinkedList> _undoHistory = new();
+ private bool _disableHistory = false;
+
+ public int CountOfHistoricalRecord { get; set; } = 10;
+
+ public static readonly StyledProperty TargetProperty = AvaloniaProperty.Register(
+ nameof(Target));
+
+ public Control? Target
+ {
+ get => GetValue(TargetProperty);
+ set => SetValue(TargetProperty, value);
+ }
+
+ public static readonly StyledProperty SeparatorProperty =
+ TagInput.SeparatorProperty.AddOwner();
+
+ public string Separator
+ {
+ get => GetValue(SeparatorProperty);
+ set => SetValue(SeparatorProperty, value);
+ }
+
+
+ private ObservableCollection _targetClasses;
+
+ internal static readonly DirectProperty> TargetClassesProperty = AvaloniaProperty.RegisterDirect>(
+ nameof(TargetClasses), o => o.TargetClasses, (o, v) => o.TargetClasses = v);
+
+ internal ObservableCollection TargetClasses
+ {
+ get => _targetClasses;
+ set => SetAndRaise(TargetClassesProperty, ref _targetClasses, value);
+ }
+
+ public static readonly AttachedProperty SourceProperty =
+ AvaloniaProperty.RegisterAttached("Source");
+
+ public static void SetSource(StyledElement obj, ControlClassesInput value) => obj.SetValue(SourceProperty, value);
+ public static ControlClassesInput? GetSource(StyledElement obj) => obj.GetValue(SourceProperty);
+
+ static ControlClassesInput()
+ {
+ TargetClassesProperty.Changed.AddClassHandler>((o,e)=>o.OnClassesChanged(e));
+ SourceProperty.Changed.AddClassHandler(HandleSourceChange);
+ }
+
+ public ControlClassesInput()
+ {
+ TargetClasses = new ObservableCollection();
+ //TargetClasses.CollectionChanged += InccOnCollectionChanged;
+ }
+
+ private List _targets = new();
+
+ private static void HandleSourceChange(StyledElement arg1, AvaloniaPropertyChangedEventArgs arg2)
+ {
+ var newControl = arg2.NewValue.Value;
+ if (newControl is null) return;
+ newControl._targets.Add(arg1);
+ var oldControl = arg2.OldValue.Value;
+ if (oldControl is not null)
+ {
+ newControl._targets.Remove(oldControl);
+ }
+ }
+
+ private void OnClassesChanged(AvaloniaPropertyChangedEventArgs?> args)
+ {
+ var newValue = args.NewValue.Value;
+ if (newValue is null)
+ {
+ SaveHistory(new List(), true);
+ return;
+ }
+ else
+ {
+ var classes = newValue.Where(a => !string.IsNullOrWhiteSpace(a)).Distinct().ToList();
+ SaveHistory(classes, true);
+ if (newValue is INotifyCollectionChanged incc)
+ {
+ incc.CollectionChanged+=InccOnCollectionChanged;
+ }
+ return;
+ }
+ }
+
+ private void InccOnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
+ {
+ if(_disableHistory) return;
+ SaveHistory(TargetClasses?.ToList() ?? new List(), true);
+ }
+
+ private void SaveHistory(List strings, bool fromInput)
+ {
+ _history.AddLast(strings);
+ _undoHistory.Clear();
+ if (_history.Count > CountOfHistoricalRecord)
+ {
+ _history.RemoveFirst();
+ }
+ SetClassesToTarget(fromInput);
+ }
+
+ private void SetClassesToTarget(bool fromInput)
+ {
+ List strings;
+ if (_history.Count == 0)
+ {
+ strings = new List();
+ }
+ else
+ {
+ strings = _history.Last.Value;
+ }
+
+ if (!fromInput)
+ {
+ SetCurrentValue(TargetClassesProperty, new ObservableCollection(strings));
+ }
+ if (Target is not null)
+ {
+ Target.Classes.Replace(strings);
+ }
+ foreach (var target in _targets)
+ {
+ target.Classes.Replace(strings);
+ }
+ }
+
+ public void UnDo()
+ {
+ var node = _history.Last;
+ _history.RemoveLast();
+ _undoHistory.AddFirst(node);
+ _disableHistory = true;
+ TargetClasses.Clear();
+ foreach (var value in _history.Last.Value)
+ {
+ TargetClasses.Add(value);
+ }
+ _disableHistory = false;
+ SetClassesToTarget(false);
+ }
+
+ public void Redo()
+ {
+ var node = _undoHistory.First;
+ _undoHistory.RemoveFirst();
+ _history.AddLast(node);
+ _disableHistory = true;
+ TargetClasses.Clear();
+ foreach (var value in _history.Last.Value)
+ {
+ TargetClasses.Add(value);
+ }
+ _disableHistory = false;
+ SetClassesToTarget(false);
+ }
+
+ public void Clear()
+ {
+ SaveHistory(new List(), false);
+ }
+}
\ No newline at end of file
diff --git a/src/Ursa/Controls/TagInput/TagInput.cs b/src/Ursa/Controls/TagInput/TagInput.cs
index f57e7eb..920d9ac 100644
--- a/src/Ursa/Controls/TagInput/TagInput.cs
+++ b/src/Ursa/Controls/TagInput/TagInput.cs
@@ -190,6 +190,12 @@ public class TagInput : TemplatedControl
}
}
}
+ else if (e.Action == NotifyCollectionChangedAction.Reset)
+ {
+ Items.Clear();
+ Items.Add(_textBox);
+ InvalidateVisual();
+ }
}