Selaa lähdekoodia

Added TagsEditor and TagsEditorControl

Kenric Nugteren 5 kuukautta sitten
vanhempi
commit
d49678feab

+ 65 - 0
InABox.Core/Objects/Editors/TagsEditor.cs

@@ -0,0 +1,65 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace InABox.Core
+{
+    /// <summary>
+    /// An editor for a field of type <see langword="string"/>, which saves a list of "tags", with a <see cref="Prefix"/> and <see cref="Separator"/>.
+    /// </summary>
+    /// <remarks>
+    /// The separator really is a suffix, because it is appended even to the last tag in the list. For example, with the default prefix and separator,
+    /// the tags "foo", "bar" and "baz" would save as the string <c>"#foo;#bar;#baz"</c>. This allows for filtering easily with
+    /// <see cref="Operator.Contains"/> filters.
+    /// </remarks>
+    public class TagsEditor : BaseEditor
+    {
+        public string Prefix { get; set; }
+
+        public string Separator { get; set; }
+
+        public TagsEditor(string prefix = "#", string separator = ";")
+        {
+            Prefix = prefix;
+            Separator = separator;
+        }
+
+        public IEnumerable<string> ReadTags(string tags)
+        {
+            int idx = 0;
+            while (idx < tags.Length)
+            {
+                var nextIdx = tags.IndexOf(Prefix, idx);
+                if(nextIdx == -1)
+                {
+                    nextIdx = idx;
+                }
+                else
+                {
+                    nextIdx += 1;
+                }
+                var endIdx = tags.IndexOf(Separator, nextIdx);
+                if(endIdx == -1)
+                {
+                    yield return tags[nextIdx..];
+                    yield break;
+                }
+                else
+                {
+                    yield return tags[nextIdx..(endIdx - 1)];
+                    idx = endIdx + 1;
+                }
+            }
+        }
+        public string WriteTags(IEnumerable<string> tags)
+        {
+            return string.Join("", tags.Select(x => $"{Prefix}{x}{Separator}"));
+        }
+
+        protected override BaseEditor DoClone()
+        {
+            return new TagsEditor(Prefix, Separator);
+        }
+    }
+}

+ 441 - 0
inabox.wpf/DynamicGrid/Editors/TagsEditor/TagsEditorControl.cs

@@ -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);
+    }
+}