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 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 SearchTermProperty = AvaloniaProperty.Register( nameof(SearchTerm)); public string SearchTerm { get => GetValue(SearchTermProperty); set => SetValue(SearchTermProperty, value); } public static readonly StyledProperty SelectAllProperty = AvaloniaProperty.Register( 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 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 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 newResults, List 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 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 e) { CommandSearchControl control = ((CommandSearchControl)e.Sender); control.UpdateSearchResults(); control.PropertyChanged?.Invoke(control, new PropertyChangedEventArgs(nameof(control.SearchTerm))); } }