CommandSearchControl.axaml.cs 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. using System.Collections.Generic;
  2. using System.Collections.ObjectModel;
  3. using System.ComponentModel;
  4. using System.Linq;
  5. using System.Text;
  6. using Avalonia;
  7. using Avalonia.Controls;
  8. using Avalonia.Input;
  9. using Avalonia.Threading;
  10. using CommunityToolkit.Mvvm.Input;
  11. using PixiEditor.AvaloniaUI.Helpers.Extensions;
  12. using PixiEditor.AvaloniaUI.Models.Commands;
  13. using PixiEditor.AvaloniaUI.Models.Commands.Search;
  14. using PixiEditor.AvaloniaUI.Models.Input;
  15. using PixiEditor.DrawingApi.Core.ColorsImpl;
  16. namespace PixiEditor.AvaloniaUI.Views.Main.CommandSearch;
  17. #nullable enable
  18. internal partial class CommandSearchControl : UserControl, INotifyPropertyChanged
  19. {
  20. public static readonly StyledProperty<string> SearchTermProperty =
  21. AvaloniaProperty.Register<CommandSearchControl, string>(
  22. nameof(SearchTerm));
  23. public string SearchTerm
  24. {
  25. get => GetValue(SearchTermProperty);
  26. set => SetValue(SearchTermProperty, value);
  27. }
  28. public static readonly StyledProperty<bool> SelectAllProperty = AvaloniaProperty.Register<CommandSearchControl, bool>(
  29. nameof(SelectAll));
  30. public bool SelectAll
  31. {
  32. get => GetValue(SelectAllProperty);
  33. set => SetValue(SelectAllProperty, value);
  34. }
  35. private string warnings = "";
  36. public string Warnings
  37. {
  38. get => warnings;
  39. set
  40. {
  41. warnings = value;
  42. PropertyChanged?.Invoke(this, new(nameof(Warnings)));
  43. PropertyChanged?.Invoke(this, new(nameof(HasWarnings)));
  44. }
  45. }
  46. public bool HasWarnings => Warnings != string.Empty;
  47. public RelayCommand ButtonClickedCommand { get; }
  48. public event PropertyChangedEventHandler? PropertyChanged;
  49. private SearchResult? selectedResult;
  50. public SearchResult? SelectedResult
  51. {
  52. get => selectedResult;
  53. private set
  54. {
  55. if (selectedResult is not null)
  56. selectedResult.IsSelected = false;
  57. if (value is not null)
  58. value.IsSelected = true;
  59. selectedResult = value;
  60. PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SelectedResult)));
  61. }
  62. }
  63. private SearchResult? mouseSelectedResult;
  64. public SearchResult? MouseSelectedResult
  65. {
  66. get => mouseSelectedResult;
  67. private set
  68. {
  69. if (mouseSelectedResult is not null)
  70. mouseSelectedResult.IsMouseSelected = false;
  71. if (value is not null)
  72. value.IsMouseSelected = true;
  73. mouseSelectedResult = value;
  74. }
  75. }
  76. public ObservableCollection<SearchResult> Results { get; } = new();
  77. static CommandSearchControl()
  78. {
  79. SearchTermProperty.Changed.Subscribe(OnSearchTermChange);
  80. }
  81. public CommandSearchControl()
  82. {
  83. ButtonClickedCommand = new RelayCommand(() =>
  84. {
  85. Hide();
  86. MouseSelectedResult?.Execute();
  87. MouseSelectedResult = null;
  88. });
  89. InitializeComponent();
  90. PointerPressed += OnPointerDown;
  91. KeyDown += OnPreviewKeyDown;
  92. Loaded += (_, _) => UpdateSearchResults();
  93. }
  94. private static void OnIsVisibleChanged(AvaloniaPropertyChangedEventArgs<bool> e)
  95. {
  96. CommandSearchControl control = ((CommandSearchControl)e.Sender);
  97. if (e.NewValue.Value)
  98. {
  99. Dispatcher.UIThread.Invoke(
  100. () =>
  101. {
  102. control.textBox.Focus();
  103. control.UpdateSearchResults();
  104. // TODO: Mouse capture
  105. /*Mouse.Capture(this, CaptureMode.SubTree);*/
  106. if (!control.SelectAll)
  107. {
  108. control.textBox.CaretIndex = control.SearchTerm?.Length ?? 0;
  109. }
  110. }, DispatcherPriority.Render);
  111. }
  112. }
  113. private void OnPointerDown(object sender, PointerPressedEventArgs e)
  114. {
  115. var pos = e.GetPosition(this);
  116. bool outside = pos.X < 0 || pos.Y < 0 || pos.X > Bounds.Width || pos.Y > Bounds.Height;
  117. if (outside)
  118. Hide();
  119. }
  120. private void UpdateSearchResults()
  121. {
  122. Results.Clear();
  123. (List<SearchResult> newResults, List<string> warnings) = CommandSearchControlHelper.ConstructSearchResults(SearchTerm);
  124. foreach (var result in newResults)
  125. Results.Add(result);
  126. Warnings = warnings.Aggregate(new StringBuilder(), static (builder, item) =>
  127. {
  128. builder.AppendLine(item);
  129. return builder;
  130. }).ToString();
  131. SelectedResult = Results.FirstOrDefault(x => x.CanExecute);
  132. }
  133. private void Hide()
  134. {
  135. // TODO: This
  136. /*FocusManager.SetFocusedElement(FocusManager.GetFocusScope(textBox), null);
  137. Keyboard.ClearFocus();*/
  138. IsVisible = false;
  139. //ReleaseMouseCapture();
  140. }
  141. private void OnPreviewKeyDown(object? sender, KeyEventArgs e)
  142. {
  143. e.Handled = true;
  144. OneOf<Color, Error, None> result;
  145. if (e.Key == Key.Enter && SelectedResult is not null)
  146. {
  147. Hide();
  148. SelectedResult.Execute();
  149. SelectedResult = null;
  150. }
  151. else if (e.Key is Key.Down or Key.PageDown)
  152. {
  153. MoveSelection(1);
  154. }
  155. else if (e.Key is Key.Up or Key.PageUp)
  156. {
  157. MoveSelection(-1);
  158. }
  159. else if (e.Key == Key.Escape ||
  160. CommandController.Current.Commands["PixiEditor.Search.Toggle"].Shortcut
  161. == new KeyCombination(e.Key, e.KeyModifiers))
  162. {
  163. Hide();
  164. }
  165. else if (e.Key == Key.R && e.KeyModifiers == KeyModifiers.Control)
  166. {
  167. SearchTerm = "rgb(,,)";
  168. textBox.CaretIndex = 4;
  169. /*TODO: Validate below, length should be 0*/
  170. textBox.SelectionStart = 4;
  171. textBox.SelectionEnd = 4;
  172. }
  173. else if (e.Key == Key.Space && SearchTerm.StartsWith("rgb") && textBox.CaretIndex > 0 && char.IsDigit(SearchTerm[textBox.CaretIndex - 1]))
  174. {
  175. var prev = textBox.CaretIndex;
  176. if (SearchTerm.Length == textBox.CaretIndex || SearchTerm[textBox.CaretIndex] != ',')
  177. {
  178. SearchTerm = SearchTerm.Insert(textBox.CaretIndex, ",");
  179. }
  180. textBox.CaretIndex = prev + 1;
  181. }
  182. else if (e is { Key: Key.S, KeyModifiers: KeyModifiers.Control } &&
  183. (result = CommandSearchControlHelper.MaybeParseColor(SearchTerm)).IsT0)
  184. {
  185. SwitchColor(result.AsT0);
  186. }
  187. else if (e is { Key: Key.D, KeyModifiers: KeyModifiers.Control })
  188. {
  189. SearchTerm = "~/Documents/";
  190. textBox.CaretIndex = SearchTerm.Length;
  191. }
  192. else if (e is { Key: Key.P, KeyModifiers: KeyModifiers.Control })
  193. {
  194. SearchTerm = "~/Pictures/";
  195. textBox.CaretIndex = SearchTerm.Length;
  196. }
  197. else
  198. {
  199. e.Handled = false;
  200. }
  201. }
  202. private void SwitchColor(Color color)
  203. {
  204. if (SearchTerm.StartsWith('#'))
  205. {
  206. if (color.A == 255)
  207. {
  208. SearchTerm = $"rgb({color.R},{color.G},{color.B})";
  209. textBox.CaretIndex = 4;
  210. }
  211. else
  212. {
  213. SearchTerm = $"rgba({color.R},{color.G},{color.B},{color.A})";
  214. textBox.CaretIndex = 5;
  215. }
  216. }
  217. else
  218. {
  219. if (color.A == 255)
  220. {
  221. SearchTerm = $"#{color.R:X2}{color.G:X2}{color.B:X2}";
  222. textBox.CaretIndex = 1;
  223. }
  224. else
  225. {
  226. SearchTerm = $"#{color.R:X2}{color.G:X2}{color.B:X2}{color.A:X2}";
  227. textBox.CaretIndex = 1;
  228. }
  229. }
  230. }
  231. private void MoveSelection(int delta)
  232. {
  233. if (delta == 0)
  234. return;
  235. if (SelectedResult is null)
  236. {
  237. SelectedResult = Results.FirstOrDefault(x => x.CanExecute);
  238. return;
  239. }
  240. int newIndex = Results.IndexOf(SelectedResult) + delta;
  241. newIndex = (newIndex % Results.Count + Results.Count) % Results.Count;
  242. SelectedResult = delta > 0 ? Results.IndexOrNext(x => x.CanExecute, newIndex) : Results.IndexOrPrevious(x => x.CanExecute, newIndex);
  243. itemscontrol.ItemContainerGenerator.ContainerFromIndex(newIndex)?.BringIntoView();
  244. }
  245. private void Button_MouseMove(object sender, PointerEventArgs e)
  246. {
  247. var searchResult = ((Button)sender).DataContext as SearchResult;
  248. MouseSelectedResult = searchResult;
  249. }
  250. private static void OnSearchTermChange(AvaloniaPropertyChangedEventArgs<string> e)
  251. {
  252. CommandSearchControl control = ((CommandSearchControl)e.Sender);
  253. control.UpdateSearchResults();
  254. control.PropertyChanged?.Invoke(control, new PropertyChangedEventArgs(nameof(control.SearchTerm)));
  255. }
  256. }