|
@@ -0,0 +1,441 @@
|
|
|
+using InABox.Core;
|
|
|
+using InABox.WPF;
|
|
|
+using Syncfusion.Data.Extensions;
|
|
|
+using System;
|
|
|
+using System.Collections.Generic;
|
|
|
+using System.Linq;
|
|
|
+using System.Text;
|
|
|
+using System.Threading.Tasks;
|
|
|
+using System.Windows;
|
|
|
+using System.Windows.Controls;
|
|
|
+using System.Windows.Controls.Primitives;
|
|
|
+using System.Windows.Input;
|
|
|
+using System.Windows.Markup.Localizer;
|
|
|
+using System.Windows.Media;
|
|
|
+
|
|
|
+namespace InABox.DynamicGrid;
|
|
|
+
|
|
|
+public class TagsEditorControl : DynamicEditorControl<string, TagsEditor>
|
|
|
+{
|
|
|
+ private List<string> Tags = new();
|
|
|
+ private string CurrentTag = "";
|
|
|
+ private string _editorText = "";
|
|
|
+
|
|
|
+ private int? _selectedTagIdx;
|
|
|
+ private string EditorText
|
|
|
+ {
|
|
|
+ get => _editorText;
|
|
|
+ set
|
|
|
+ {
|
|
|
+ _editorText = value;
|
|
|
+ Editor.Text = value;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private TextBox Editor = null!;
|
|
|
+ private Popup? Popup = null;
|
|
|
+ private ListBox? TagList = null;
|
|
|
+
|
|
|
+ public List<string> RecentTags = new();
|
|
|
+
|
|
|
+ public event Action<IEnumerable<string>>? OnRecentTagsChanged;
|
|
|
+
|
|
|
+ protected override FrameworkElement CreateEditor()
|
|
|
+ {
|
|
|
+ Editor = new TextBox
|
|
|
+ {
|
|
|
+ VerticalAlignment = VerticalAlignment.Stretch,
|
|
|
+ VerticalContentAlignment = VerticalAlignment.Center,
|
|
|
+ HorizontalAlignment = HorizontalAlignment.Stretch,
|
|
|
+ AcceptsTab = true
|
|
|
+ };
|
|
|
+ Editor.TextChanged += Editor_TextChanged;
|
|
|
+ Editor.PreviewKeyDown += Editor_PreviewKeyDown;
|
|
|
+ Editor.SelectionChanged += Editor_SelectionChanged;
|
|
|
+
|
|
|
+ return Editor;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void CreatePopup()
|
|
|
+ {
|
|
|
+ if(Popup is not null && !Popup.IsOpen)
|
|
|
+ {
|
|
|
+ ClosePopup();
|
|
|
+ }
|
|
|
+ if(Popup is null)
|
|
|
+ {
|
|
|
+ Popup = new Popup();
|
|
|
+ Popup.PlacementTarget = Editor;
|
|
|
+ Popup.Placement = PlacementMode.Bottom;
|
|
|
+ Popup.PopupAnimation = PopupAnimation.Slide;
|
|
|
+
|
|
|
+ TagList = new ListBox();
|
|
|
+ TagList.Width = Editor.ActualWidth;
|
|
|
+ TagList.Height = 100;
|
|
|
+ TagList.ItemsSource = RecentTags;
|
|
|
+ TagList.MouseUp += TagList_MouseUp;
|
|
|
+
|
|
|
+ Popup.Child = TagList;
|
|
|
+ Popup.StaysOpen = false;
|
|
|
+
|
|
|
+ Popup.IsOpen = true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void TagList_MouseUp(object sender, MouseButtonEventArgs e)
|
|
|
+ {
|
|
|
+ AddSelectedTag();
|
|
|
+ Editor.Focus();
|
|
|
+ }
|
|
|
+
|
|
|
+ private void ClosePopup()
|
|
|
+ {
|
|
|
+ if(Popup is not null)
|
|
|
+ {
|
|
|
+ Popup.IsOpen = false;
|
|
|
+ Popup = null;
|
|
|
+ TagList = null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Update if one writes a separator.
|
|
|
+
|
|
|
+ private (int?, int) GetTagIdx(int idx)
|
|
|
+ {
|
|
|
+ for(int i = 0; i < Tags.Count; ++i)
|
|
|
+ {
|
|
|
+ var tagStr = EditorDefinition.Prefix + Tags[i] + EditorDefinition.Separator + 1;
|
|
|
+ if(idx < tagStr.Length)
|
|
|
+ {
|
|
|
+ return (i, idx);
|
|
|
+ }
|
|
|
+ idx -= tagStr.Length;
|
|
|
+
|
|
|
+ if(idx < 0)
|
|
|
+ {
|
|
|
+ return (null, idx + tagStr.Length);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return (null, idx);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void AddTag(string tag)
|
|
|
+ {
|
|
|
+ if (!Tags.Contains(tag))
|
|
|
+ {
|
|
|
+ Tags.Add(tag);
|
|
|
+ }
|
|
|
+ if (!RecentTags.Contains(tag))
|
|
|
+ {
|
|
|
+ RecentTags.Add(tag);
|
|
|
+ OnRecentTagsChanged?.Invoke(RecentTags);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void UpdateTagList()
|
|
|
+ {
|
|
|
+ if (TagList is null) return;
|
|
|
+
|
|
|
+ var current = CurrentTag;
|
|
|
+ var currentSelected = TagList.SelectedItem;
|
|
|
+
|
|
|
+ TagList.ItemsSource = RecentTags.Where(x => x.StartsWith(current))
|
|
|
+ .Concat(RecentTags.Where(x => !x.StartsWith(current) && x.Contains(current)));
|
|
|
+ TagList.SelectedItem = currentSelected;
|
|
|
+ if(TagList.SelectedIndex == -1)
|
|
|
+ {
|
|
|
+ TagList.SelectedIndex = 0;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void UpdateEditorText(int? cursorIdx)
|
|
|
+ {
|
|
|
+ EditorText = string.Join("", Tags.Select(x => EditorDefinition.Prefix + x + EditorDefinition.Separator + " ")) + CurrentTag;
|
|
|
+
|
|
|
+ if (cursorIdx.HasValue)
|
|
|
+ {
|
|
|
+ var startIdx = 0;
|
|
|
+ for(int i = 0; i < Tags.Count; ++i)
|
|
|
+ {
|
|
|
+ startIdx += EditorDefinition.Prefix.Length + Tags[i].Length + EditorDefinition.Separator.Length + 1;
|
|
|
+ }
|
|
|
+ Editor.CaretIndex = startIdx + cursorIdx.Value;
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ Editor.CaretIndex = Editor.Text.Length;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void AddSelectedTag()
|
|
|
+ {
|
|
|
+ if (TagList is null || TagList.SelectedIndex == -1)
|
|
|
+ {
|
|
|
+ CurrentTag += EditorDefinition.Separator;
|
|
|
+ AddCurrentTag();
|
|
|
+ CheckChanged();
|
|
|
+ ClosePopup();
|
|
|
+
|
|
|
+ UpdateEditorText(null);
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ var tag = TagList.SelectedItem?.ToString() ?? "";
|
|
|
+ if (!tag.IsNullOrWhiteSpace())
|
|
|
+ {
|
|
|
+ AddTag(tag);
|
|
|
+ }
|
|
|
+
|
|
|
+ CheckChanged();
|
|
|
+ CurrentTag = "";
|
|
|
+ ClosePopup();
|
|
|
+ UpdateEditorText(null);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private bool _updatingSelection;
|
|
|
+
|
|
|
+ private void SelectTag(int idx)
|
|
|
+ {
|
|
|
+ _selectedTagIdx = idx;
|
|
|
+
|
|
|
+ var startIdx = 0;
|
|
|
+ for(int i = 0; i < idx; ++i)
|
|
|
+ {
|
|
|
+ startIdx += EditorDefinition.Prefix.Length + Tags[i].Length + EditorDefinition.Separator.Length + 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ try
|
|
|
+ {
|
|
|
+ _updatingSelection = true;
|
|
|
+ Editor.Select(startIdx, EditorDefinition.Prefix.Length + Tags[idx].Length + EditorDefinition.Separator.Length);
|
|
|
+ }
|
|
|
+ finally
|
|
|
+ {
|
|
|
+ _updatingSelection = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void Editor_SelectionChanged(object sender, RoutedEventArgs e)
|
|
|
+ {
|
|
|
+ if (_updatingSelection) return;
|
|
|
+
|
|
|
+ var idx = Editor.CaretIndex;
|
|
|
+ var (tagIdx, cIdx) = GetTagIdx(idx);
|
|
|
+ if (tagIdx.HasValue)
|
|
|
+ {
|
|
|
+ SelectTag(tagIdx.Value);
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ _selectedTagIdx = null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void Editor_PreviewKeyDown(object sender, System.Windows.Input.KeyEventArgs e)
|
|
|
+ {
|
|
|
+ if(e.Key == Key.Down)
|
|
|
+ {
|
|
|
+ if(TagList is null) return;
|
|
|
+ if(TagList.SelectedIndex >= TagList.Items.Count - 1)
|
|
|
+ {
|
|
|
+ TagList.SelectedIndex = 0;
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ ++TagList.SelectedIndex;
|
|
|
+ }
|
|
|
+ TagList.ScrollIntoView(TagList.SelectedItem);
|
|
|
+ e.Handled = true;
|
|
|
+ }
|
|
|
+ else if(e.Key == Key.Up)
|
|
|
+ {
|
|
|
+ if(TagList is null) return;
|
|
|
+ if(TagList.SelectedIndex <= 0)
|
|
|
+ {
|
|
|
+ TagList.SelectedIndex = TagList.Items.Count - 1;
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ --TagList.SelectedIndex;
|
|
|
+ }
|
|
|
+ TagList.ScrollIntoView(TagList.SelectedItem);
|
|
|
+ e.Handled = true;
|
|
|
+ }
|
|
|
+ else if(e.Key == Key.Left)
|
|
|
+ {
|
|
|
+ if (_selectedTagIdx.HasValue && _selectedTagIdx.Value > 0)
|
|
|
+ {
|
|
|
+ SelectTag(_selectedTagIdx.Value - 1);
|
|
|
+ e.Handled = true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ else if(e.Key == Key.Right)
|
|
|
+ {
|
|
|
+ if (_selectedTagIdx.HasValue)
|
|
|
+ {
|
|
|
+ if(_selectedTagIdx.Value < Tags.Count - 1)
|
|
|
+ {
|
|
|
+ SelectTag(_selectedTagIdx.Value + 1);
|
|
|
+ e.Handled = true;
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ _selectedTagIdx = null;
|
|
|
+ Editor.CaretIndex = EditorText.Length;
|
|
|
+ e.Handled = true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ else if(e.Key == Key.Tab || e.Key == Key.Enter)
|
|
|
+ {
|
|
|
+ AddSelectedTag();
|
|
|
+
|
|
|
+ e.Handled = true;
|
|
|
+ }
|
|
|
+ else if(e.Key == Key.Back)
|
|
|
+ {
|
|
|
+ if (_selectedTagIdx.HasValue)
|
|
|
+ {
|
|
|
+ var selectedIdx = _selectedTagIdx.Value;
|
|
|
+ Tags.RemoveAt(selectedIdx);
|
|
|
+ CheckChanged();
|
|
|
+ UpdateEditorText(null);
|
|
|
+ if(selectedIdx == 0)
|
|
|
+ {
|
|
|
+ if(Tags.Count > 0)
|
|
|
+ {
|
|
|
+ SelectTag(0);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ SelectTag(selectedIdx - 1);
|
|
|
+ }
|
|
|
+ e.Handled = true;
|
|
|
+ }
|
|
|
+ else if (CurrentTag.IsNullOrWhiteSpace() && Tags.Count > 0)
|
|
|
+ {
|
|
|
+ Tags.RemoveAt(Tags.Count - 1);
|
|
|
+ CheckChanged();
|
|
|
+ ClosePopup();
|
|
|
+ UpdateEditorText(null);
|
|
|
+ e.Handled = true;
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ var (tagIdx, cIdx) = GetTagIdx(Editor.CaretIndex);
|
|
|
+ if(tagIdx is null)
|
|
|
+ {
|
|
|
+ if(cIdx == 0)
|
|
|
+ {
|
|
|
+ e.Handled = true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ Tags.RemoveAt(tagIdx.Value);
|
|
|
+ CheckChanged();
|
|
|
+ UpdateEditorText(null);
|
|
|
+ e.Handled = true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private bool AddCurrentTag()
|
|
|
+ {
|
|
|
+ if (CurrentTag.Contains(EditorDefinition.Separator))
|
|
|
+ {
|
|
|
+ int startIdx = 0;
|
|
|
+ while(startIdx < CurrentTag.Length)
|
|
|
+ {
|
|
|
+ var idx = CurrentTag.IndexOf(EditorDefinition.Separator, startIdx);
|
|
|
+ if(idx == -1)
|
|
|
+ {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ var tag = CurrentTag[startIdx..idx].Trim();
|
|
|
+ while (tag.StartsWith(EditorDefinition.Prefix))
|
|
|
+ {
|
|
|
+ tag = tag[EditorDefinition.Prefix.Length..].Trim();
|
|
|
+ }
|
|
|
+ if (!tag.IsNullOrWhiteSpace())
|
|
|
+ {
|
|
|
+ AddTag(tag);
|
|
|
+ }
|
|
|
+
|
|
|
+ startIdx = idx + 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ CurrentTag = CurrentTag[startIdx..].Trim();
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void Editor_TextChanged(object sender, TextChangedEventArgs e)
|
|
|
+ {
|
|
|
+ var (tagIdx, cIdx) = GetTagIdx(Editor.CaretIndex);
|
|
|
+ int? curIdx = cIdx;
|
|
|
+
|
|
|
+ if (tagIdx is null)
|
|
|
+ {
|
|
|
+ var idx = 0;
|
|
|
+ for(int i = 0; i < Tags.Count; ++i)
|
|
|
+ {
|
|
|
+ idx += EditorDefinition.Prefix.Length + Tags[i].Length + EditorDefinition.Separator.Length + 1;
|
|
|
+ }
|
|
|
+ CurrentTag = Editor.Text[idx..]; // If idx is -1, then idx + 1 is 0, which we want.
|
|
|
+ }
|
|
|
+ if (AddCurrentTag())
|
|
|
+ {
|
|
|
+ curIdx = null;
|
|
|
+ }
|
|
|
+
|
|
|
+ CreatePopup();
|
|
|
+ UpdateTagList();
|
|
|
+ UpdateEditorText(curIdx);
|
|
|
+ }
|
|
|
+
|
|
|
+ public override void Configure()
|
|
|
+ {
|
|
|
+ }
|
|
|
+
|
|
|
+ public override int DesiredHeight()
|
|
|
+ {
|
|
|
+ return 25;
|
|
|
+ }
|
|
|
+
|
|
|
+ public override int DesiredWidth()
|
|
|
+ {
|
|
|
+ return int.MaxValue;
|
|
|
+ }
|
|
|
+
|
|
|
+ public override void SetColor(Color color)
|
|
|
+ {
|
|
|
+ Editor.Background = color.ToBrush();
|
|
|
+ }
|
|
|
+
|
|
|
+ public override void SetFocus()
|
|
|
+ {
|
|
|
+ Editor.Focus();
|
|
|
+ Editor.CaretIndex = Editor.Text.Length;
|
|
|
+ }
|
|
|
+
|
|
|
+ protected override string RetrieveValue()
|
|
|
+ {
|
|
|
+ return EditorDefinition.WriteTags(Tags);
|
|
|
+ }
|
|
|
+
|
|
|
+ protected override void UpdateValue(string value)
|
|
|
+ {
|
|
|
+ Tags = EditorDefinition.ReadTags(value).ToList();
|
|
|
+ UpdateEditorText(null);
|
|
|
+ }
|
|
|
+}
|