|
|
@@ -0,0 +1,286 @@
|
|
|
+using System.Collections.Generic;
|
|
|
+using System.Collections.ObjectModel;
|
|
|
+using System.ComponentModel;
|
|
|
+using System.Linq;
|
|
|
+using System.Text;
|
|
|
+using Avalonia;
|
|
|
+using Avalonia.Controls;
|
|
|
+using Avalonia.Input;
|
|
|
+using Avalonia.Threading;
|
|
|
+using CommunityToolkit.Mvvm.Input;
|
|
|
+using Hardware.Info;
|
|
|
+using PixiEditor.AvaloniaUI.Helpers.Extensions;
|
|
|
+using PixiEditor.AvaloniaUI.Models.Commands;
|
|
|
+using PixiEditor.AvaloniaUI.Models.Commands.Search;
|
|
|
+using PixiEditor.AvaloniaUI.Models.Input;
|
|
|
+using PixiEditor.DrawingApi.Core.ColorsImpl;
|
|
|
+
|
|
|
+namespace PixiEditor.AvaloniaUI.Views.Main.CommandSearch;
|
|
|
+#nullable enable
|
|
|
+internal partial class CommandSearchControl : UserControl, INotifyPropertyChanged
|
|
|
+{
|
|
|
+ public static readonly StyledProperty<string> SearchTermProperty =
|
|
|
+ AvaloniaProperty.Register<CommandSearchControl, string>(
|
|
|
+ nameof(SearchTerm));
|
|
|
+
|
|
|
+ public string SearchTerm
|
|
|
+ {
|
|
|
+ get => GetValue(SearchTermProperty);
|
|
|
+ set => SetValue(SearchTermProperty, value);
|
|
|
+ }
|
|
|
+
|
|
|
+ public static readonly StyledProperty<bool> SelectAllProperty = AvaloniaProperty.Register<CommandSearchControl, bool>(
|
|
|
+ nameof(SelectAll));
|
|
|
+
|
|
|
+ public bool SelectAll
|
|
|
+ {
|
|
|
+ get => GetValue(SelectAllProperty);
|
|
|
+ set => SetValue(SelectAllProperty, value);
|
|
|
+ }
|
|
|
+
|
|
|
+ private string warnings = "";
|
|
|
+ public string Warnings
|
|
|
+ {
|
|
|
+ get => warnings;
|
|
|
+ set
|
|
|
+ {
|
|
|
+ warnings = value;
|
|
|
+ PropertyChanged?.Invoke(this, new(nameof(Warnings)));
|
|
|
+ PropertyChanged?.Invoke(this, new(nameof(HasWarnings)));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public bool HasWarnings => Warnings != string.Empty;
|
|
|
+ public RelayCommand ButtonClickedCommand { get; }
|
|
|
+
|
|
|
+ public event PropertyChangedEventHandler? PropertyChanged;
|
|
|
+
|
|
|
+ private SearchResult? selectedResult;
|
|
|
+ public SearchResult? SelectedResult
|
|
|
+ {
|
|
|
+ get => selectedResult;
|
|
|
+ private set
|
|
|
+ {
|
|
|
+ if (selectedResult is not null)
|
|
|
+ selectedResult.IsSelected = false;
|
|
|
+ if (value is not null)
|
|
|
+ value.IsSelected = true;
|
|
|
+ selectedResult = value;
|
|
|
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SelectedResult)));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private SearchResult? mouseSelectedResult;
|
|
|
+ public SearchResult? MouseSelectedResult
|
|
|
+ {
|
|
|
+ get => mouseSelectedResult;
|
|
|
+ private set
|
|
|
+ {
|
|
|
+ if (mouseSelectedResult is not null)
|
|
|
+ mouseSelectedResult.IsMouseSelected = false;
|
|
|
+ if (value is not null)
|
|
|
+ value.IsMouseSelected = true;
|
|
|
+ mouseSelectedResult = value;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public ObservableCollection<SearchResult> Results { get; } = new();
|
|
|
+
|
|
|
+ static CommandSearchControl()
|
|
|
+ {
|
|
|
+ SearchTermProperty.Changed.Subscribe(OnSearchTermChange);
|
|
|
+ }
|
|
|
+
|
|
|
+ public CommandSearchControl()
|
|
|
+ {
|
|
|
+ ButtonClickedCommand = new RelayCommand(() =>
|
|
|
+ {
|
|
|
+ Hide();
|
|
|
+ MouseSelectedResult?.Execute();
|
|
|
+ MouseSelectedResult = null;
|
|
|
+ });
|
|
|
+
|
|
|
+ InitializeComponent();
|
|
|
+
|
|
|
+ PointerPressed += OnPointerDown;
|
|
|
+ KeyDown += OnPreviewKeyDown;
|
|
|
+ Loaded += (_, _) => UpdateSearchResults();
|
|
|
+ }
|
|
|
+
|
|
|
+ private static void OnIsVisibleChanged(AvaloniaPropertyChangedEventArgs<bool> e)
|
|
|
+ {
|
|
|
+ CommandSearchControl control = ((CommandSearchControl)e.Sender);
|
|
|
+ if (e.NewValue.Value)
|
|
|
+ {
|
|
|
+ Dispatcher.UIThread.Invoke(
|
|
|
+ () =>
|
|
|
+ {
|
|
|
+ control.textBox.Focus();
|
|
|
+ control.UpdateSearchResults();
|
|
|
+
|
|
|
+ // TODO: Mouse capture
|
|
|
+ /*Mouse.Capture(this, CaptureMode.SubTree);*/
|
|
|
+
|
|
|
+ if (!control.SelectAll)
|
|
|
+ {
|
|
|
+ control.textBox.CaretIndex = control.SearchTerm?.Length ?? 0;
|
|
|
+ }
|
|
|
+ }, DispatcherPriority.Render);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void OnPointerDown(object sender, PointerPressedEventArgs e)
|
|
|
+ {
|
|
|
+ var pos = e.GetPosition(this);
|
|
|
+ bool outside = pos.X < 0 || pos.Y < 0 || pos.X > Bounds.Width || pos.Y > Bounds.Height;
|
|
|
+ if (outside)
|
|
|
+ Hide();
|
|
|
+ }
|
|
|
+
|
|
|
+ private void UpdateSearchResults()
|
|
|
+ {
|
|
|
+ Results.Clear();
|
|
|
+ (List<SearchResult> newResults, List<string> warnings) = CommandSearchControlHelper.ConstructSearchResults(SearchTerm);
|
|
|
+ foreach (var result in newResults)
|
|
|
+ Results.Add(result);
|
|
|
+ Warnings = warnings.Aggregate(new StringBuilder(), static (builder, item) =>
|
|
|
+ {
|
|
|
+ builder.AppendLine(item);
|
|
|
+ return builder;
|
|
|
+ }).ToString();
|
|
|
+ SelectedResult = Results.FirstOrDefault(x => x.CanExecute);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void Hide()
|
|
|
+ {
|
|
|
+ // TODO: This
|
|
|
+ /*FocusManager.SetFocusedElement(FocusManager.GetFocusScope(textBox), null);
|
|
|
+ Keyboard.ClearFocus();*/
|
|
|
+ IsVisible = false;
|
|
|
+ //ReleaseMouseCapture();
|
|
|
+ }
|
|
|
+
|
|
|
+ private void OnPreviewKeyDown(object? sender, KeyEventArgs e)
|
|
|
+ {
|
|
|
+ e.Handled = true;
|
|
|
+
|
|
|
+ OneOf<Color, Error, None> result;
|
|
|
+
|
|
|
+ if (e.Key == Key.Enter && SelectedResult is not null)
|
|
|
+ {
|
|
|
+ Hide();
|
|
|
+ SelectedResult.Execute();
|
|
|
+ SelectedResult = null;
|
|
|
+ }
|
|
|
+ else if (e.Key is Key.Down or Key.PageDown)
|
|
|
+ {
|
|
|
+ MoveSelection(1);
|
|
|
+ }
|
|
|
+ else if (e.Key is Key.Up or Key.PageUp)
|
|
|
+ {
|
|
|
+ MoveSelection(-1);
|
|
|
+ }
|
|
|
+ else if (e.Key == Key.Escape ||
|
|
|
+ CommandController.Current.Commands["PixiEditor.Search.Toggle"].Shortcut
|
|
|
+ == new KeyCombination(e.Key, e.KeyModifiers))
|
|
|
+ {
|
|
|
+ Hide();
|
|
|
+ }
|
|
|
+ else if (e.Key == Key.R && e.KeyModifiers == KeyModifiers.Control)
|
|
|
+ {
|
|
|
+ SearchTerm = "rgb(,,)";
|
|
|
+ textBox.CaretIndex = 4;
|
|
|
+ /*TODO: Validate below, length should be 0*/
|
|
|
+ textBox.SelectionStart = 4;
|
|
|
+ textBox.SelectionEnd = 4;
|
|
|
+ }
|
|
|
+ else if (e.Key == Key.Space && SearchTerm.StartsWith("rgb") && textBox.CaretIndex > 0 && char.IsDigit(SearchTerm[textBox.CaretIndex - 1]))
|
|
|
+ {
|
|
|
+ var prev = textBox.CaretIndex;
|
|
|
+ if (SearchTerm.Length == textBox.CaretIndex || SearchTerm[textBox.CaretIndex] != ',')
|
|
|
+ {
|
|
|
+ SearchTerm = SearchTerm.Insert(textBox.CaretIndex, ",");
|
|
|
+ }
|
|
|
+ textBox.CaretIndex = prev + 1;
|
|
|
+ }
|
|
|
+ else if (e is { Key: Key.S, KeyModifiers: KeyModifiers.Control } &&
|
|
|
+ (result = CommandSearchControlHelper.MaybeParseColor(SearchTerm)).IsT0)
|
|
|
+ {
|
|
|
+ SwitchColor(result.AsT0);
|
|
|
+ }
|
|
|
+ else if (e is { Key: Key.D, KeyModifiers: KeyModifiers.Control })
|
|
|
+ {
|
|
|
+ SearchTerm = "~/Documents/";
|
|
|
+ textBox.CaretIndex = SearchTerm.Length;
|
|
|
+ }
|
|
|
+ else if (e is { Key: Key.P, KeyModifiers: KeyModifiers.Control })
|
|
|
+ {
|
|
|
+ SearchTerm = "~/Pictures/";
|
|
|
+ textBox.CaretIndex = SearchTerm.Length;
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ e.Handled = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void SwitchColor(Color color)
|
|
|
+ {
|
|
|
+ if (SearchTerm.StartsWith('#'))
|
|
|
+ {
|
|
|
+ if (color.A == 255)
|
|
|
+ {
|
|
|
+ SearchTerm = $"rgb({color.R},{color.G},{color.B})";
|
|
|
+ textBox.CaretIndex = 4;
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ SearchTerm = $"rgba({color.R},{color.G},{color.B},{color.A})";
|
|
|
+ textBox.CaretIndex = 5;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ if (color.A == 255)
|
|
|
+ {
|
|
|
+ SearchTerm = $"#{color.R:X2}{color.G:X2}{color.B:X2}";
|
|
|
+ textBox.CaretIndex = 1;
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ SearchTerm = $"#{color.R:X2}{color.G:X2}{color.B:X2}{color.A:X2}";
|
|
|
+ textBox.CaretIndex = 1;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void MoveSelection(int delta)
|
|
|
+ {
|
|
|
+ if (delta == 0)
|
|
|
+ return;
|
|
|
+ if (SelectedResult is null)
|
|
|
+ {
|
|
|
+ SelectedResult = Results.FirstOrDefault(x => x.CanExecute);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ int newIndex = Results.IndexOf(SelectedResult) + delta;
|
|
|
+ newIndex = (newIndex % Results.Count + Results.Count) % Results.Count;
|
|
|
+
|
|
|
+ SelectedResult = delta > 0 ? Results.IndexOrNext(x => x.CanExecute, newIndex) : Results.IndexOrPrevious(x => x.CanExecute, newIndex);
|
|
|
+ itemscontrol.ItemContainerGenerator.ContainerFromIndex(newIndex)?.BringIntoView();
|
|
|
+ }
|
|
|
+
|
|
|
+ private void Button_MouseMove(object sender, PointerEventArgs e)
|
|
|
+ {
|
|
|
+ var searchResult = ((Button)sender).DataContext as SearchResult;
|
|
|
+ MouseSelectedResult = searchResult;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static void OnSearchTermChange(AvaloniaPropertyChangedEventArgs<string> e)
|
|
|
+ {
|
|
|
+ CommandSearchControl control = ((CommandSearchControl)e.Sender);
|
|
|
+ control.UpdateSearchResults();
|
|
|
+ control.PropertyChanged?.Invoke(control, new PropertyChangedEventArgs(nameof(control.SearchTerm)));
|
|
|
+ }
|
|
|
+}
|