SelectingItemsControl.cs 41 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212
  1. // Copyright (c) The Avalonia Project. All rights reserved.
  2. // Licensed under the MIT license. See licence.md file in the project root for full license information.
  3. using System;
  4. using System.Collections;
  5. using System.Collections.Generic;
  6. using System.Collections.Specialized;
  7. using System.Linq;
  8. using Avalonia.Collections;
  9. using Avalonia.Controls.Generators;
  10. using Avalonia.Data;
  11. using Avalonia.Input;
  12. using Avalonia.Input.Platform;
  13. using Avalonia.Interactivity;
  14. using Avalonia.Logging;
  15. using Avalonia.Styling;
  16. using Avalonia.VisualTree;
  17. namespace Avalonia.Controls.Primitives
  18. {
  19. /// <summary>
  20. /// An <see cref="ItemsControl"/> that maintains a selection.
  21. /// </summary>
  22. /// <remarks>
  23. /// <para>
  24. /// <see cref="SelectingItemsControl"/> provides a base class for <see cref="ItemsControl"/>s
  25. /// that maintain a selection (single or multiple). By default only its
  26. /// <see cref="SelectedIndex"/> and <see cref="SelectedItem"/> properties are visible; the
  27. /// current multiple selection <see cref="SelectedItems"/> together with the
  28. /// <see cref="SelectionMode"/> properties are protected, however a derived class can expose
  29. /// these if it wishes to support multiple selection.
  30. /// </para>
  31. /// <para>
  32. /// <see cref="SelectingItemsControl"/> maintains a selection respecting the current
  33. /// <see cref="SelectionMode"/> but it does not react to user input; this must be handled in a
  34. /// derived class. It does, however, respond to <see cref="IsSelectedChangedEvent"/> events
  35. /// from items and updates the selection accordingly.
  36. /// </para>
  37. /// </remarks>
  38. public class SelectingItemsControl : ItemsControl
  39. {
  40. /// <summary>
  41. /// Defines the <see cref="AutoScrollToSelectedItem"/> property.
  42. /// </summary>
  43. public static readonly StyledProperty<bool> AutoScrollToSelectedItemProperty =
  44. AvaloniaProperty.Register<SelectingItemsControl, bool>(
  45. nameof(AutoScrollToSelectedItem),
  46. defaultValue: true);
  47. /// <summary>
  48. /// Defines the <see cref="SelectedIndex"/> property.
  49. /// </summary>
  50. public static readonly DirectProperty<SelectingItemsControl, int> SelectedIndexProperty =
  51. AvaloniaProperty.RegisterDirect<SelectingItemsControl, int>(
  52. nameof(SelectedIndex),
  53. o => o.SelectedIndex,
  54. (o, v) => o.SelectedIndex = v,
  55. unsetValue: -1,
  56. defaultBindingMode: BindingMode.TwoWay);
  57. /// <summary>
  58. /// Defines the <see cref="SelectedItem"/> property.
  59. /// </summary>
  60. public static readonly DirectProperty<SelectingItemsControl, object> SelectedItemProperty =
  61. AvaloniaProperty.RegisterDirect<SelectingItemsControl, object>(
  62. nameof(SelectedItem),
  63. o => o.SelectedItem,
  64. (o, v) => o.SelectedItem = v,
  65. defaultBindingMode: BindingMode.TwoWay);
  66. /// <summary>
  67. /// Defines the <see cref="SelectedItems"/> property.
  68. /// </summary>
  69. protected static readonly DirectProperty<SelectingItemsControl, IList> SelectedItemsProperty =
  70. AvaloniaProperty.RegisterDirect<SelectingItemsControl, IList>(
  71. nameof(SelectedItems),
  72. o => o.SelectedItems,
  73. (o, v) => o.SelectedItems = v);
  74. /// <summary>
  75. /// Defines the <see cref="SelectionMode"/> property.
  76. /// </summary>
  77. protected static readonly StyledProperty<SelectionMode> SelectionModeProperty =
  78. AvaloniaProperty.Register<SelectingItemsControl, SelectionMode>(
  79. nameof(SelectionMode));
  80. /// <summary>
  81. /// Event that should be raised by items that implement <see cref="ISelectable"/> to
  82. /// notify the parent <see cref="SelectingItemsControl"/> that their selection state
  83. /// has changed.
  84. /// </summary>
  85. public static readonly RoutedEvent<RoutedEventArgs> IsSelectedChangedEvent =
  86. RoutedEvent.Register<SelectingItemsControl, RoutedEventArgs>(
  87. "IsSelectedChanged",
  88. RoutingStrategies.Bubble);
  89. /// <summary>
  90. /// Defines the <see cref="SelectionChanged"/> event.
  91. /// </summary>
  92. public static readonly RoutedEvent<SelectionChangedEventArgs> SelectionChangedEvent =
  93. RoutedEvent.Register<SelectingItemsControl, SelectionChangedEventArgs>(
  94. "SelectionChanged",
  95. RoutingStrategies.Bubble);
  96. private static readonly IList Empty = Array.Empty<object>();
  97. private readonly Selection _selection = new Selection();
  98. private int _selectedIndex = -1;
  99. private object _selectedItem;
  100. private IList _selectedItems;
  101. private bool _ignoreContainerSelectionChanged;
  102. private bool _syncingSelectedItems;
  103. private int _updateCount;
  104. private int _updateSelectedIndex;
  105. private object _updateSelectedItem;
  106. /// <summary>
  107. /// Initializes static members of the <see cref="SelectingItemsControl"/> class.
  108. /// </summary>
  109. static SelectingItemsControl()
  110. {
  111. IsSelectedChangedEvent.AddClassHandler<SelectingItemsControl>((x, e) => x.ContainerSelectionChanged(e));
  112. }
  113. /// <summary>
  114. /// Occurs when the control's selection changes.
  115. /// </summary>
  116. public event EventHandler<SelectionChangedEventArgs> SelectionChanged
  117. {
  118. add { AddHandler(SelectionChangedEvent, value); }
  119. remove { RemoveHandler(SelectionChangedEvent, value); }
  120. }
  121. /// <summary>
  122. /// Gets or sets a value indicating whether to automatically scroll to newly selected items.
  123. /// </summary>
  124. public bool AutoScrollToSelectedItem
  125. {
  126. get { return GetValue(AutoScrollToSelectedItemProperty); }
  127. set { SetValue(AutoScrollToSelectedItemProperty, value); }
  128. }
  129. /// <summary>
  130. /// Gets or sets the index of the selected item.
  131. /// </summary>
  132. public int SelectedIndex
  133. {
  134. get
  135. {
  136. return _selectedIndex;
  137. }
  138. set
  139. {
  140. if (_updateCount == 0)
  141. {
  142. var effective = (value >= 0 && value < ItemCount) ? value : -1;
  143. UpdateSelectedItem(effective);
  144. }
  145. else
  146. {
  147. _updateSelectedIndex = value;
  148. _updateSelectedItem = null;
  149. }
  150. }
  151. }
  152. /// <summary>
  153. /// Gets or sets the selected item.
  154. /// </summary>
  155. public object SelectedItem
  156. {
  157. get
  158. {
  159. return _selectedItem;
  160. }
  161. set
  162. {
  163. if (_updateCount == 0)
  164. {
  165. UpdateSelectedItem(IndexOf(Items, value));
  166. }
  167. else
  168. {
  169. _updateSelectedItem = value;
  170. _updateSelectedIndex = int.MinValue;
  171. }
  172. }
  173. }
  174. /// <summary>
  175. /// Gets the selected items.
  176. /// </summary>
  177. protected IList SelectedItems
  178. {
  179. get
  180. {
  181. if (_selectedItems == null)
  182. {
  183. _selectedItems = new AvaloniaList<object>();
  184. SubscribeToSelectedItems();
  185. }
  186. return _selectedItems;
  187. }
  188. set
  189. {
  190. if (value?.IsFixedSize == true || value?.IsReadOnly == true)
  191. {
  192. throw new NotSupportedException(
  193. "Cannot use a fixed size or read-only collection as SelectedItems.");
  194. }
  195. UnsubscribeFromSelectedItems();
  196. _selectedItems = value ?? new AvaloniaList<object>();
  197. SubscribeToSelectedItems();
  198. }
  199. }
  200. /// <summary>
  201. /// Gets or sets the selection mode.
  202. /// </summary>
  203. /// <remarks>
  204. /// Note that the selection mode only applies to selections made via user interaction.
  205. /// Multiple selections can be made programatically regardless of the value of this property.
  206. /// </remarks>
  207. protected SelectionMode SelectionMode
  208. {
  209. get { return GetValue(SelectionModeProperty); }
  210. set { SetValue(SelectionModeProperty, value); }
  211. }
  212. /// <summary>
  213. /// Gets a value indicating whether <see cref="SelectionMode.AlwaysSelected"/> is set.
  214. /// </summary>
  215. protected bool AlwaysSelected => (SelectionMode & SelectionMode.AlwaysSelected) != 0;
  216. /// <inheritdoc/>
  217. public override void BeginInit()
  218. {
  219. base.BeginInit();
  220. ++_updateCount;
  221. _updateSelectedIndex = int.MinValue;
  222. }
  223. /// <inheritdoc/>
  224. public override void EndInit()
  225. {
  226. if (--_updateCount == 0)
  227. {
  228. UpdateFinished();
  229. }
  230. base.EndInit();
  231. }
  232. /// <summary>
  233. /// Scrolls the specified item into view.
  234. /// </summary>
  235. /// <param name="item">The item.</param>
  236. public void ScrollIntoView(object item) => Presenter?.ScrollIntoView(item);
  237. /// <summary>
  238. /// Tries to get the container that was the source of an event.
  239. /// </summary>
  240. /// <param name="eventSource">The control that raised the event.</param>
  241. /// <returns>The container or null if the event did not originate in a container.</returns>
  242. protected IControl GetContainerFromEventSource(IInteractive eventSource)
  243. {
  244. var item = ((IVisual)eventSource).GetSelfAndVisualAncestors()
  245. .OfType<IControl>()
  246. .FirstOrDefault(x => x.LogicalParent == this && ItemContainerGenerator?.IndexFromContainer(x) != -1);
  247. return item;
  248. }
  249. /// <inheritdoc/>
  250. protected override void ItemsChanged(AvaloniaPropertyChangedEventArgs e)
  251. {
  252. base.ItemsChanged(e);
  253. if (_updateCount == 0)
  254. {
  255. var newIndex = -1;
  256. if (SelectedIndex != -1)
  257. {
  258. newIndex = IndexOf((IEnumerable)e.NewValue, SelectedItem);
  259. }
  260. if (AlwaysSelected && Items != null && Items.Cast<object>().Any())
  261. {
  262. newIndex = 0;
  263. }
  264. SelectedIndex = newIndex;
  265. }
  266. }
  267. /// <inheritdoc/>
  268. protected override void ItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
  269. {
  270. if (_updateCount > 0)
  271. {
  272. base.ItemsCollectionChanged(sender, e);
  273. return;
  274. }
  275. switch (e.Action)
  276. {
  277. case NotifyCollectionChangedAction.Add:
  278. _selection.ItemsInserted(e.NewStartingIndex, e.NewItems.Count);
  279. break;
  280. case NotifyCollectionChangedAction.Remove:
  281. _selection.ItemsRemoved(e.OldStartingIndex, e.OldItems.Count);
  282. break;
  283. }
  284. base.ItemsCollectionChanged(sender, e);
  285. switch (e.Action)
  286. {
  287. case NotifyCollectionChangedAction.Add:
  288. if (AlwaysSelected && SelectedIndex == -1)
  289. {
  290. SelectedIndex = 0;
  291. }
  292. else
  293. {
  294. UpdateSelectedItem(_selection.First(), false);
  295. }
  296. break;
  297. case NotifyCollectionChangedAction.Remove:
  298. UpdateSelectedItem(_selection.First(), false);
  299. ResetSelectedItems();
  300. break;
  301. case NotifyCollectionChangedAction.Replace:
  302. UpdateSelectedItem(SelectedIndex, false);
  303. ResetSelectedItems();
  304. break;
  305. case NotifyCollectionChangedAction.Move:
  306. case NotifyCollectionChangedAction.Reset:
  307. SelectedIndex = IndexOf(Items, SelectedItem);
  308. if (AlwaysSelected && SelectedIndex == -1 && ItemCount > 0)
  309. {
  310. SelectedIndex = 0;
  311. }
  312. break;
  313. }
  314. }
  315. /// <inheritdoc/>
  316. protected override void OnContainersMaterialized(ItemContainerEventArgs e)
  317. {
  318. base.OnContainersMaterialized(e);
  319. var resetSelectedItems = false;
  320. foreach (var container in e.Containers)
  321. {
  322. if ((container.ContainerControl as ISelectable)?.IsSelected == true)
  323. {
  324. if (SelectedIndex == -1)
  325. {
  326. SelectedIndex = container.Index;
  327. }
  328. else
  329. {
  330. if (_selection.Add(container.Index))
  331. {
  332. resetSelectedItems = true;
  333. }
  334. }
  335. MarkContainerSelected(container.ContainerControl, true);
  336. }
  337. else if (_selection.Contains(container.Index))
  338. {
  339. MarkContainerSelected(container.ContainerControl, true);
  340. }
  341. }
  342. if (resetSelectedItems)
  343. {
  344. ResetSelectedItems();
  345. }
  346. }
  347. /// <inheritdoc/>
  348. protected override void OnContainersDematerialized(ItemContainerEventArgs e)
  349. {
  350. base.OnContainersDematerialized(e);
  351. var panel = (InputElement)Presenter.Panel;
  352. if (panel != null)
  353. {
  354. foreach (var container in e.Containers)
  355. {
  356. if (KeyboardNavigation.GetTabOnceActiveElement(panel) == container.ContainerControl)
  357. {
  358. KeyboardNavigation.SetTabOnceActiveElement(panel, null);
  359. break;
  360. }
  361. }
  362. }
  363. }
  364. protected override void OnContainersRecycled(ItemContainerEventArgs e)
  365. {
  366. foreach (var i in e.Containers)
  367. {
  368. if (i.ContainerControl != null && i.Item != null)
  369. {
  370. bool selected = _selection.Contains(i.Index);
  371. MarkContainerSelected(i.ContainerControl, selected);
  372. }
  373. }
  374. }
  375. /// <inheritdoc/>
  376. protected override void OnDataContextBeginUpdate()
  377. {
  378. base.OnDataContextBeginUpdate();
  379. ++_updateCount;
  380. }
  381. /// <inheritdoc/>
  382. protected override void OnDataContextEndUpdate()
  383. {
  384. base.OnDataContextEndUpdate();
  385. if (--_updateCount == 0)
  386. {
  387. UpdateFinished();
  388. }
  389. }
  390. protected override void OnKeyDown(KeyEventArgs e)
  391. {
  392. base.OnKeyDown(e);
  393. if (!e.Handled)
  394. {
  395. var keymap = AvaloniaLocator.Current.GetService<PlatformHotkeyConfiguration>();
  396. bool Match(List<KeyGesture> gestures) => gestures.Any(g => g.Matches(e));
  397. if (ItemCount > 0 &&
  398. Match(keymap.SelectAll) &&
  399. (((SelectionMode & SelectionMode.Multiple) != 0) ||
  400. (SelectionMode & SelectionMode.Toggle) != 0))
  401. {
  402. SelectAll();
  403. e.Handled = true;
  404. }
  405. }
  406. }
  407. /// <summary>
  408. /// Moves the selection in the specified direction relative to the current selection.
  409. /// </summary>
  410. /// <param name="direction">The direction to move.</param>
  411. /// <param name="wrap">Whether to wrap when the selection reaches the first or last item.</param>
  412. /// <returns>True if the selection was moved; otherwise false.</returns>
  413. protected bool MoveSelection(NavigationDirection direction, bool wrap)
  414. {
  415. var from = SelectedIndex != -1 ? ItemContainerGenerator.ContainerFromIndex(SelectedIndex) : null;
  416. return MoveSelection(from, direction, wrap);
  417. }
  418. /// <summary>
  419. /// Moves the selection in the specified direction relative to the specified container.
  420. /// </summary>
  421. /// <param name="from">The container which serves as a starting point for the movement.</param>
  422. /// <param name="direction">The direction to move.</param>
  423. /// <param name="wrap">Whether to wrap when the selection reaches the first or last item.</param>
  424. /// <returns>True if the selection was moved; otherwise false.</returns>
  425. protected bool MoveSelection(IControl from, NavigationDirection direction, bool wrap)
  426. {
  427. if (Presenter?.Panel is INavigableContainer container &&
  428. GetNextControl(container, direction, from, wrap) is IControl next)
  429. {
  430. var index = ItemContainerGenerator.IndexFromContainer(next);
  431. if (index != -1)
  432. {
  433. SelectedIndex = index;
  434. return true;
  435. }
  436. }
  437. return false;
  438. }
  439. /// <summary>
  440. /// Selects all items in the control.
  441. /// </summary>
  442. protected void SelectAll()
  443. {
  444. UpdateSelectedItems(() =>
  445. {
  446. _selection.Clear();
  447. for (var i = 0; i < ItemCount; ++i)
  448. {
  449. _selection.Add(i);
  450. }
  451. UpdateSelectedItem(0, false);
  452. foreach (var container in ItemContainerGenerator.Containers)
  453. {
  454. MarkItemSelected(container.Index, true);
  455. }
  456. ResetSelectedItems();
  457. });
  458. }
  459. /// <summary>
  460. /// Deselects all items in the control.
  461. /// </summary>
  462. protected void UnselectAll() => UpdateSelectedItem(-1);
  463. /// <summary>
  464. /// Updates the selection for an item based on user interaction.
  465. /// </summary>
  466. /// <param name="index">The index of the item.</param>
  467. /// <param name="select">Whether the item should be selected or unselected.</param>
  468. /// <param name="rangeModifier">Whether the range modifier is enabled (i.e. shift key).</param>
  469. /// <param name="toggleModifier">Whether the toggle modifier is enabled (i.e. ctrl key).</param>
  470. /// <param name="rightButton">Whether the event is a right-click.</param>
  471. protected void UpdateSelection(
  472. int index,
  473. bool select = true,
  474. bool rangeModifier = false,
  475. bool toggleModifier = false,
  476. bool rightButton = false)
  477. {
  478. if (index != -1)
  479. {
  480. if (select)
  481. {
  482. var mode = SelectionMode;
  483. var multi = (mode & SelectionMode.Multiple) != 0;
  484. var toggle = (toggleModifier || (mode & SelectionMode.Toggle) != 0);
  485. var range = multi && rangeModifier;
  486. if (rightButton)
  487. {
  488. if (!_selection.Contains(index))
  489. {
  490. UpdateSelectedItem(index);
  491. }
  492. }
  493. else if (range)
  494. {
  495. UpdateSelectedItems(() =>
  496. {
  497. var start = SelectedIndex != -1 ? SelectedIndex : 0;
  498. var step = start < index ? 1 : -1;
  499. _selection.Clear();
  500. for (var i = start; i != index; i += step)
  501. {
  502. _selection.Add(i);
  503. }
  504. _selection.Add(index);
  505. var first = Math.Min(start, index);
  506. var last = Math.Max(start, index);
  507. foreach (var container in ItemContainerGenerator.Containers)
  508. {
  509. MarkItemSelected(
  510. container.Index,
  511. container.Index >= first && container.Index <= last);
  512. }
  513. ResetSelectedItems();
  514. });
  515. }
  516. else if (multi && toggle)
  517. {
  518. UpdateSelectedItems(() =>
  519. {
  520. if (!_selection.Contains(index))
  521. {
  522. _selection.Add(index);
  523. MarkItemSelected(index, true);
  524. SelectedItems.Add(ElementAt(Items, index));
  525. }
  526. else
  527. {
  528. _selection.Remove(index);
  529. MarkItemSelected(index, false);
  530. if (index == _selectedIndex)
  531. {
  532. UpdateSelectedItem(_selection.First(), false);
  533. }
  534. SelectedItems.Remove(ElementAt(Items, index));
  535. }
  536. });
  537. }
  538. else if (toggle)
  539. {
  540. SelectedIndex = (SelectedIndex == index) ? -1 : index;
  541. }
  542. else
  543. {
  544. UpdateSelectedItem(index);
  545. }
  546. if (Presenter?.Panel != null)
  547. {
  548. var container = ItemContainerGenerator.ContainerFromIndex(index);
  549. KeyboardNavigation.SetTabOnceActiveElement(
  550. (InputElement)Presenter.Panel,
  551. container);
  552. }
  553. }
  554. else
  555. {
  556. LostSelection();
  557. }
  558. }
  559. }
  560. /// <summary>
  561. /// Updates the selection for a container based on user interaction.
  562. /// </summary>
  563. /// <param name="container">The container.</param>
  564. /// <param name="select">Whether the container should be selected or unselected.</param>
  565. /// <param name="rangeModifier">Whether the range modifier is enabled (i.e. shift key).</param>
  566. /// <param name="toggleModifier">Whether the toggle modifier is enabled (i.e. ctrl key).</param>
  567. /// <param name="rightButton">Whether the event is a right-click.</param>
  568. protected void UpdateSelection(
  569. IControl container,
  570. bool select = true,
  571. bool rangeModifier = false,
  572. bool toggleModifier = false,
  573. bool rightButton = false)
  574. {
  575. var index = ItemContainerGenerator?.IndexFromContainer(container) ?? -1;
  576. if (index != -1)
  577. {
  578. UpdateSelection(index, select, rangeModifier, toggleModifier, rightButton);
  579. }
  580. }
  581. /// <summary>
  582. /// Updates the selection based on an event that may have originated in a container that
  583. /// belongs to the control.
  584. /// </summary>
  585. /// <param name="eventSource">The control that raised the event.</param>
  586. /// <param name="select">Whether the container should be selected or unselected.</param>
  587. /// <param name="rangeModifier">Whether the range modifier is enabled (i.e. shift key).</param>
  588. /// <param name="toggleModifier">Whether the toggle modifier is enabled (i.e. ctrl key).</param>
  589. /// <param name="rightButton">Whether the event is a right-click.</param>
  590. /// <returns>
  591. /// True if the event originated from a container that belongs to the control; otherwise
  592. /// false.
  593. /// </returns>
  594. protected bool UpdateSelectionFromEventSource(
  595. IInteractive eventSource,
  596. bool select = true,
  597. bool rangeModifier = false,
  598. bool toggleModifier = false,
  599. bool rightButton = false)
  600. {
  601. var container = GetContainerFromEventSource(eventSource);
  602. if (container != null)
  603. {
  604. UpdateSelection(container, select, rangeModifier, toggleModifier, rightButton);
  605. return true;
  606. }
  607. return false;
  608. }
  609. /// <summary>
  610. /// Gets a range of items from an IEnumerable.
  611. /// </summary>
  612. /// <param name="items">The items.</param>
  613. /// <param name="first">The index of the first item.</param>
  614. /// <param name="last">The index of the last item.</param>
  615. /// <returns>The items.</returns>
  616. private static List<object> GetRange(IEnumerable items, int first, int last)
  617. {
  618. var list = (items as IList) ?? items.Cast<object>().ToList();
  619. var step = first > last ? -1 : 1;
  620. var result = new List<object>();
  621. for (int i = first; i != last; i += step)
  622. {
  623. result.Add(list[i]);
  624. }
  625. result.Add(list[last]);
  626. return result;
  627. }
  628. /// <summary>
  629. /// Called when a container raises the <see cref="IsSelectedChangedEvent"/>.
  630. /// </summary>
  631. /// <param name="e">The event.</param>
  632. private void ContainerSelectionChanged(RoutedEventArgs e)
  633. {
  634. if (!_ignoreContainerSelectionChanged)
  635. {
  636. var control = e.Source as IControl;
  637. var selectable = e.Source as ISelectable;
  638. if (control != null &&
  639. selectable != null &&
  640. control.LogicalParent == this &&
  641. ItemContainerGenerator?.IndexFromContainer(control) != -1)
  642. {
  643. UpdateSelection(control, selectable.IsSelected);
  644. }
  645. }
  646. if (e.Source != this)
  647. {
  648. e.Handled = true;
  649. }
  650. }
  651. /// <summary>
  652. /// Called when the currently selected item is lost and the selection must be changed
  653. /// depending on the <see cref="SelectionMode"/> property.
  654. /// </summary>
  655. private void LostSelection()
  656. {
  657. var items = Items?.Cast<object>();
  658. var index = -1;
  659. if (items != null && AlwaysSelected)
  660. {
  661. index = Math.Min(SelectedIndex, items.Count() - 1);
  662. }
  663. SelectedIndex = index;
  664. }
  665. /// <summary>
  666. /// Sets a container's 'selected' class or <see cref="ISelectable.IsSelected"/>.
  667. /// </summary>
  668. /// <param name="container">The container.</param>
  669. /// <param name="selected">Whether the control is selected</param>
  670. /// <returns>The previous selection state.</returns>
  671. private bool MarkContainerSelected(IControl container, bool selected)
  672. {
  673. try
  674. {
  675. var selectable = container as ISelectable;
  676. bool result;
  677. _ignoreContainerSelectionChanged = true;
  678. if (selectable != null)
  679. {
  680. result = selectable.IsSelected;
  681. selectable.IsSelected = selected;
  682. }
  683. else
  684. {
  685. result = container.Classes.Contains(":selected");
  686. ((IPseudoClasses)container.Classes).Set(":selected", selected);
  687. }
  688. return result;
  689. }
  690. finally
  691. {
  692. _ignoreContainerSelectionChanged = false;
  693. }
  694. }
  695. /// <summary>
  696. /// Sets an item container's 'selected' class or <see cref="ISelectable.IsSelected"/>.
  697. /// </summary>
  698. /// <param name="index">The index of the item.</param>
  699. /// <param name="selected">Whether the item should be selected or deselected.</param>
  700. private void MarkItemSelected(int index, bool selected)
  701. {
  702. var container = ItemContainerGenerator?.ContainerFromIndex(index);
  703. if (container != null)
  704. {
  705. MarkContainerSelected(container, selected);
  706. }
  707. }
  708. /// <summary>
  709. /// Sets an item container's 'selected' class or <see cref="ISelectable.IsSelected"/>.
  710. /// </summary>
  711. /// <param name="item">The item.</param>
  712. /// <param name="selected">Whether the item should be selected or deselected.</param>
  713. private int MarkItemSelected(object item, bool selected)
  714. {
  715. var index = IndexOf(Items, item);
  716. if (index != -1)
  717. {
  718. MarkItemSelected(index, selected);
  719. }
  720. return index;
  721. }
  722. private void ResetSelectedItems()
  723. {
  724. UpdateSelectedItems(() =>
  725. {
  726. SelectedItems.Clear();
  727. foreach (var i in _selection)
  728. {
  729. SelectedItems.Add(ElementAt(Items, i));
  730. }
  731. });
  732. }
  733. /// <summary>
  734. /// Called when the <see cref="SelectedItems"/> CollectionChanged event is raised.
  735. /// </summary>
  736. /// <param name="sender">The event sender.</param>
  737. /// <param name="e">The event args.</param>
  738. private void SelectedItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
  739. {
  740. if (_syncingSelectedItems)
  741. {
  742. return;
  743. }
  744. void Add(IList newItems, IList addedItems = null)
  745. {
  746. foreach (var item in newItems)
  747. {
  748. var index = MarkItemSelected(item, true);
  749. if (index != -1 && _selection.Add(index) && addedItems != null)
  750. {
  751. addedItems.Add(item);
  752. }
  753. }
  754. }
  755. void UpdateSelection()
  756. {
  757. if ((SelectedIndex != -1 && !_selection.Contains(SelectedIndex)) ||
  758. (SelectedIndex == -1 && _selection.HasItems))
  759. {
  760. _selectedIndex = _selection.First();
  761. _selectedItem = ElementAt(Items, _selectedIndex);
  762. RaisePropertyChanged(SelectedIndexProperty, -1, _selectedIndex, BindingPriority.LocalValue);
  763. RaisePropertyChanged(SelectedItemProperty, null, _selectedItem, BindingPriority.LocalValue);
  764. }
  765. }
  766. IList added = null;
  767. IList removed = null;
  768. switch (e.Action)
  769. {
  770. case NotifyCollectionChangedAction.Add:
  771. {
  772. Add(e.NewItems);
  773. UpdateSelection();
  774. added = e.NewItems;
  775. }
  776. break;
  777. case NotifyCollectionChangedAction.Remove:
  778. if (SelectedItems.Count == 0)
  779. {
  780. SelectedIndex = -1;
  781. }
  782. foreach (var item in e.OldItems)
  783. {
  784. var index = MarkItemSelected(item, false);
  785. _selection.Remove(index);
  786. }
  787. removed = e.OldItems;
  788. break;
  789. case NotifyCollectionChangedAction.Replace:
  790. throw new NotSupportedException("Replacing items in a SelectedItems collection is not supported.");
  791. case NotifyCollectionChangedAction.Move:
  792. throw new NotSupportedException("Moving items in a SelectedItems collection is not supported.");
  793. case NotifyCollectionChangedAction.Reset:
  794. {
  795. removed = new List<object>();
  796. added = new List<object>();
  797. foreach (var index in _selection.ToList())
  798. {
  799. var item = ElementAt(Items, index);
  800. if (!SelectedItems.Contains(item))
  801. {
  802. MarkItemSelected(index, false);
  803. removed.Add(item);
  804. _selection.Remove(index);
  805. }
  806. }
  807. Add(SelectedItems, added);
  808. UpdateSelection();
  809. }
  810. break;
  811. }
  812. if (added?.Count > 0 || removed?.Count > 0)
  813. {
  814. var changed = new SelectionChangedEventArgs(
  815. SelectionChangedEvent,
  816. added ?? Empty,
  817. removed ?? Empty);
  818. RaiseEvent(changed);
  819. }
  820. }
  821. /// <summary>
  822. /// Subscribes to the <see cref="SelectedItems"/> CollectionChanged event, if any.
  823. /// </summary>
  824. private void SubscribeToSelectedItems()
  825. {
  826. var incc = _selectedItems as INotifyCollectionChanged;
  827. if (incc != null)
  828. {
  829. incc.CollectionChanged += SelectedItemsCollectionChanged;
  830. }
  831. SelectedItemsCollectionChanged(
  832. _selectedItems,
  833. new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
  834. }
  835. /// <summary>
  836. /// Unsubscribes from the <see cref="SelectedItems"/> CollectionChanged event, if any.
  837. /// </summary>
  838. private void UnsubscribeFromSelectedItems()
  839. {
  840. var incc = _selectedItems as INotifyCollectionChanged;
  841. if (incc != null)
  842. {
  843. incc.CollectionChanged -= SelectedItemsCollectionChanged;
  844. }
  845. }
  846. /// <summary>
  847. /// Updates the selection due to a change to <see cref="SelectedIndex"/> or
  848. /// <see cref="SelectedItem"/>.
  849. /// </summary>
  850. /// <param name="index">The new selected index.</param>
  851. /// <param name="clear">Whether to clear existing selection.</param>
  852. private void UpdateSelectedItem(int index, bool clear = true)
  853. {
  854. var oldIndex = _selectedIndex;
  855. var oldItem = _selectedItem;
  856. if (index == -1 && AlwaysSelected)
  857. {
  858. index = Math.Min(SelectedIndex, ItemCount - 1);
  859. }
  860. var item = ElementAt(Items, index);
  861. var itemChanged = !Equals(item, oldItem);
  862. var added = -1;
  863. HashSet<int> removed = null;
  864. _selectedIndex = index;
  865. _selectedItem = item;
  866. if (oldIndex != index || itemChanged || _selection.HasMultiple)
  867. {
  868. if (clear)
  869. {
  870. removed = _selection.Clear();
  871. }
  872. if (index != -1)
  873. {
  874. if (_selection.Add(index))
  875. {
  876. added = index;
  877. }
  878. if (removed?.Contains(index) == true)
  879. {
  880. removed.Remove(index);
  881. added = -1;
  882. }
  883. }
  884. if (removed != null)
  885. {
  886. foreach (var i in removed)
  887. {
  888. MarkItemSelected(i, false);
  889. }
  890. }
  891. MarkItemSelected(index, true);
  892. RaisePropertyChanged(
  893. SelectedIndexProperty,
  894. oldIndex,
  895. index);
  896. }
  897. if (itemChanged)
  898. {
  899. RaisePropertyChanged(
  900. SelectedItemProperty,
  901. oldItem,
  902. item);
  903. }
  904. if (removed != null && index != -1)
  905. {
  906. removed.Remove(index);
  907. }
  908. if (added != -1 || removed?.Count > 0)
  909. {
  910. ResetSelectedItems();
  911. var e = new SelectionChangedEventArgs(
  912. SelectionChangedEvent,
  913. added != -1 ? new[] { ElementAt(Items, added) } : Array.Empty<object>(),
  914. removed?.Select(x => ElementAt(Items, x)).ToArray() ?? Array.Empty<object>());
  915. RaiseEvent(e);
  916. }
  917. if (AutoScrollToSelectedItem && _selectedIndex != -1)
  918. {
  919. ScrollIntoView(_selectedItem);
  920. }
  921. }
  922. private void UpdateSelectedItems(Action action)
  923. {
  924. try
  925. {
  926. _syncingSelectedItems = true;
  927. action();
  928. }
  929. catch (Exception ex)
  930. {
  931. Logger.TryGet(LogEventLevel.Error)?.Log(
  932. LogArea.Property,
  933. this,
  934. "Error thrown updating SelectedItems: {Error}",
  935. ex);
  936. }
  937. finally
  938. {
  939. _syncingSelectedItems = false;
  940. }
  941. }
  942. private void UpdateFinished()
  943. {
  944. if (_updateSelectedItem != null)
  945. {
  946. SelectedItem = _updateSelectedItem;
  947. }
  948. else
  949. {
  950. if (ItemCount == 0 && SelectedIndex != -1)
  951. {
  952. SelectedIndex = -1;
  953. }
  954. else
  955. {
  956. if (_updateSelectedIndex != int.MinValue)
  957. {
  958. SelectedIndex = _updateSelectedIndex;
  959. }
  960. if (AlwaysSelected && SelectedIndex == -1)
  961. {
  962. SelectedIndex = 0;
  963. }
  964. }
  965. }
  966. }
  967. private class Selection : IEnumerable<int>
  968. {
  969. private readonly List<int> _list = new List<int>();
  970. private HashSet<int> _set = new HashSet<int>();
  971. public bool HasItems => _set.Count > 0;
  972. public bool HasMultiple => _set.Count > 1;
  973. public bool Add(int index)
  974. {
  975. if (index == -1)
  976. {
  977. throw new ArgumentException("Invalid index", "index");
  978. }
  979. if (_set.Add(index))
  980. {
  981. _list.Add(index);
  982. return true;
  983. }
  984. return false;
  985. }
  986. public bool Remove(int index)
  987. {
  988. if (_set.Remove(index))
  989. {
  990. _list.RemoveAll(x => x == index);
  991. return true;
  992. }
  993. return false;
  994. }
  995. public HashSet<int> Clear()
  996. {
  997. var result = _set;
  998. _list.Clear();
  999. _set = new HashSet<int>();
  1000. return result;
  1001. }
  1002. public void ItemsInserted(int index, int count)
  1003. {
  1004. _set = new HashSet<int>();
  1005. for (var i = 0; i < _list.Count; ++i)
  1006. {
  1007. var ix = _list[i];
  1008. if (ix >= index)
  1009. {
  1010. var newIndex = ix + count;
  1011. _list[i] = newIndex;
  1012. _set.Add(newIndex);
  1013. }
  1014. else
  1015. {
  1016. _set.Add(ix);
  1017. }
  1018. }
  1019. }
  1020. public void ItemsRemoved(int index, int count)
  1021. {
  1022. var last = (index + count) - 1;
  1023. _set = new HashSet<int>();
  1024. for (var i = 0; i < _list.Count; ++i)
  1025. {
  1026. var ix = _list[i];
  1027. if (ix >= index && ix <= last)
  1028. {
  1029. _list.RemoveAt(i--);
  1030. }
  1031. else if (ix > last)
  1032. {
  1033. var newIndex = ix - count;
  1034. _list[i] = newIndex;
  1035. _set.Add(newIndex);
  1036. }
  1037. else
  1038. {
  1039. _set.Add(ix);
  1040. }
  1041. }
  1042. }
  1043. public bool Contains(int index) => _set.Contains(index);
  1044. public int First() => HasItems ? _list[0] : -1;
  1045. public IEnumerator<int> GetEnumerator() => _set.GetEnumerator();
  1046. IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
  1047. }
  1048. }
  1049. }