forked from pipeline-foundation/gui.cs
-
Notifications
You must be signed in to change notification settings - Fork 0
/
TreeView.cs
1771 lines (1471 loc) · 48.8 KB
/
TreeView.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// This code is based on http://objectlistview.sourceforge.net (GPLv3 tree/list controls
// by phillip.piper@gmail.com). Phillip has explicitly granted permission for his design
// and code to be used in this library under the MIT license.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using NStack;
namespace Terminal.Gui {
/// <summary>
/// Interface for all non generic members of <see cref="TreeView{T}"/>
///
/// <a href="https://migueldeicaza.github.io/gui.cs/articles/treeview.html">See TreeView Deep Dive for more information</a>.
/// </summary>
public interface ITreeView {
/// <summary>
/// Contains options for changing how the tree is rendered
/// </summary>
TreeStyle Style { get; set; }
/// <summary>
/// Removes all objects from the tree and clears selection
/// </summary>
void ClearObjects ();
/// <summary>
/// Sets a flag indicating this view needs to be redisplayed because its state has changed.
/// </summary>
void SetNeedsDisplay ();
}
/// <summary>
/// Convenience implementation of generic <see cref="TreeView{T}"/> for any tree were all nodes
/// implement <see cref="ITreeNode"/>.
///
/// <a href="https://migueldeicaza.github.io/gui.cs/articles/treeview.html">See TreeView Deep Dive for more information</a>.
/// </summary>
public class TreeView : TreeView<ITreeNode> {
/// <summary>
/// Creates a new instance of the tree control with absolute positioning and initialises
/// <see cref="TreeBuilder{T}"/> with default <see cref="ITreeNode"/> based builder
/// </summary>
public TreeView ()
{
TreeBuilder = new TreeNodeBuilder ();
AspectGetter = o => o == null ? "Null" : (o.Text ?? o?.ToString () ?? "Unamed Node");
}
}
/// <summary>
/// Hierarchical tree view with expandable branches. Branch objects are dynamically determined
/// when expanded using a user defined <see cref="ITreeBuilder{T}"/>
///
/// <a href="https://migueldeicaza.github.io/gui.cs/articles/treeview.html">See TreeView Deep Dive for more information</a>.
/// </summary>
public class TreeView<T> : View, ITreeView where T : class {
private int scrollOffsetVertical;
private int scrollOffsetHorizontal;
/// <summary>
/// Determines how sub branches of the tree are dynamically built at runtime as the user
/// expands root nodes
/// </summary>
/// <value></value>
public ITreeBuilder<T> TreeBuilder { get; set; }
/// <summary>
/// private variable for <see cref="SelectedObject"/>
/// </summary>
T selectedObject;
/// <summary>
/// Contains options for changing how the tree is rendered
/// </summary>
public TreeStyle Style { get; set; } = new TreeStyle ();
/// <summary>
/// True to allow multiple objects to be selected at once
/// </summary>
/// <value></value>
public bool MultiSelect { get; set; } = true;
/// <summary>
/// True makes a letter key press navigate to the next visible branch that begins with
/// that letter/digit
/// </summary>
/// <value></value>
public bool AllowLetterBasedNavigation { get; set; } = true;
/// <summary>
/// The currently selected object in the tree. When <see cref="MultiSelect"/> is true this
/// is the object at which the cursor is at
/// </summary>
public T SelectedObject {
get => selectedObject;
set {
var oldValue = selectedObject;
selectedObject = value;
if (!ReferenceEquals (oldValue, value)) {
OnSelectionChanged (new SelectionChangedEventArgs<T> (this, oldValue, value));
}
}
}
/// <summary>
/// This event is raised when an object is activated e.g. by double clicking or
/// pressing <see cref="ObjectActivationKey"/>
/// </summary>
public event Action<ObjectActivatedEventArgs<T>> ObjectActivated;
/// <summary>
/// Key which when pressed triggers <see cref="TreeView{T}.ObjectActivated"/>.
/// Defaults to Enter
/// </summary>
public Key ObjectActivationKey { get; set; } = Key.Enter;
/// <summary>
/// Mouse event to trigger <see cref="TreeView{T}.ObjectActivated"/>.
/// Defaults to double click (<see cref="MouseFlags.Button1DoubleClicked"/>).
/// Set to null to disable this feature.
/// </summary>
/// <value></value>
public MouseFlags? ObjectActivationButton { get; set; } = MouseFlags.Button1DoubleClicked;
/// <summary>
/// Secondary selected regions of tree when <see cref="MultiSelect"/> is true
/// </summary>
private Stack<TreeSelection<T>> multiSelectedRegions = new Stack<TreeSelection<T>> ();
/// <summary>
/// Cached result of <see cref="BuildLineMap"/>
/// </summary>
private IReadOnlyCollection<Branch<T>> cachedLineMap;
/// <summary>
/// Error message to display when the control is not properly initialized at draw time
/// (nodes added but no tree builder set)
/// </summary>
public static ustring NoBuilderError = "ERROR: TreeBuilder Not Set";
/// <summary>
/// Called when the <see cref="SelectedObject"/> changes
/// </summary>
public event EventHandler<SelectionChangedEventArgs<T>> SelectionChanged;
/// <summary>
/// The root objects in the tree, note that this collection is of root objects only
/// </summary>
public IEnumerable<T> Objects { get => roots.Keys; }
/// <summary>
/// Map of root objects to the branches under them. All objects have
/// a <see cref="Branch{T}"/> even if that branch has no children
/// </summary>
internal Dictionary<T, Branch<T>> roots { get; set; } = new Dictionary<T, Branch<T>> ();
/// <summary>
/// The amount of tree view that has been scrolled off the top of the screen (by the user
/// scrolling down)
/// </summary>
/// <remarks>Setting a value of less than 0 will result in a offset of 0. To see changes
/// in the UI call <see cref="View.SetNeedsDisplay()"/></remarks>
public int ScrollOffsetVertical {
get => scrollOffsetVertical;
set {
scrollOffsetVertical = Math.Max (0, value);
}
}
/// <summary>
/// The amount of tree view that has been scrolled to the right (horizontally)
/// </summary>
/// <remarks>Setting a value of less than 0 will result in a offset of 0. To see changes
/// in the UI call <see cref="View.SetNeedsDisplay()"/></remarks>
public int ScrollOffsetHorizontal {
get => scrollOffsetHorizontal;
set {
scrollOffsetHorizontal = Math.Max (0, value);
}
}
/// <summary>
/// The current number of rows in the tree (ignoring the controls bounds)
/// </summary>
public int ContentHeight => BuildLineMap ().Count ();
/// <summary>
/// Returns the string representation of model objects hosted in the tree. Default
/// implementation is to call <see cref="object.ToString"/>
/// </summary>
/// <value></value>
public AspectGetterDelegate<T> AspectGetter { get; set; } = (o) => o.ToString () ?? "";
/// <summary>
/// Creates a new tree view with absolute positioning.
/// Use <see cref="AddObjects(IEnumerable{T})"/> to set set root objects for the tree.
/// Children will not be rendered until you set <see cref="TreeBuilder"/>
/// </summary>
public TreeView () : base ()
{
CanFocus = true;
}
/// <summary>
/// Initialises <see cref="TreeBuilder"/>.Creates a new tree view with absolute
/// positioning. Use <see cref="AddObjects(IEnumerable{T})"/> to set set root
/// objects for the tree.
/// </summary>
public TreeView (ITreeBuilder<T> builder) : this ()
{
TreeBuilder = builder;
}
/// <summary>
/// Adds a new root level object unless it is already a root of the tree
/// </summary>
/// <param name="o"></param>
public void AddObject (T o)
{
if (!roots.ContainsKey (o)) {
roots.Add (o, new Branch<T> (this, null, o));
InvalidateLineMap ();
SetNeedsDisplay ();
}
}
/// <summary>
/// Removes all objects from the tree and clears <see cref="SelectedObject"/>
/// </summary>
public void ClearObjects ()
{
SelectedObject = default (T);
multiSelectedRegions.Clear ();
roots = new Dictionary<T, Branch<T>> ();
InvalidateLineMap ();
SetNeedsDisplay ();
}
/// <summary>
/// Removes the given root object from the tree
/// </summary>
/// <remarks>If <paramref name="o"/> is the currently <see cref="SelectedObject"/> then the
/// selection is cleared</remarks>
/// <param name="o"></param>
public void Remove (T o)
{
if (roots.ContainsKey (o)) {
roots.Remove (o);
InvalidateLineMap ();
SetNeedsDisplay ();
if (Equals (SelectedObject, o)) {
SelectedObject = default (T);
}
}
}
/// <summary>
/// Adds many new root level objects. Objects that are already root objects are ignored
/// </summary>
/// <param name="collection">Objects to add as new root level objects</param>
public void AddObjects (IEnumerable<T> collection)
{
bool objectsAdded = false;
foreach (var o in collection) {
if (!roots.ContainsKey (o)) {
roots.Add (o, new Branch<T> (this, null, o));
objectsAdded = true;
}
}
if (objectsAdded) {
InvalidateLineMap ();
SetNeedsDisplay ();
}
}
/// <summary>
/// Refreshes the state of the object <paramref name="o"/> in the tree. This will
/// recompute children, string representation etc
/// </summary>
/// <remarks>This has no effect if the object is not exposed in the tree.</remarks>
/// <param name="o"></param>
/// <param name="startAtTop">True to also refresh all ancestors of the objects branch
/// (starting with the root). False to refresh only the passed node</param>
public void RefreshObject (T o, bool startAtTop = false)
{
var branch = ObjectToBranch (o);
if (branch != null) {
branch.Refresh (startAtTop);
InvalidateLineMap ();
SetNeedsDisplay ();
}
}
/// <summary>
/// Rebuilds the tree structure for all exposed objects starting with the root objects.
/// Call this method when you know there are changes to the tree but don't know which
/// objects have changed (otherwise use <see cref="RefreshObject(T, bool)"/>)
/// </summary>
public void RebuildTree ()
{
foreach (var branch in roots.Values) {
branch.Rebuild ();
}
InvalidateLineMap ();
SetNeedsDisplay ();
}
/// <summary>
/// Returns the currently expanded children of the passed object. Returns an empty
/// collection if the branch is not exposed or not expanded
/// </summary>
/// <param name="o">An object in the tree</param>
/// <returns></returns>
public IEnumerable<T> GetChildren (T o)
{
var branch = ObjectToBranch (o);
if (branch == null || !branch.IsExpanded) {
return new T [0];
}
return branch.ChildBranches?.Values?.Select (b => b.Model)?.ToArray () ?? new T [0];
}
/// <summary>
/// Returns the parent object of <paramref name="o"/> in the tree. Returns null if
/// the object is not exposed in the tree
/// </summary>
/// <param name="o">An object in the tree</param>
/// <returns></returns>
public T GetParent (T o)
{
return ObjectToBranch (o)?.Parent?.Model;
}
///<inheritdoc/>
public override void Redraw (Rect bounds)
{
if (roots == null) {
return;
}
if (TreeBuilder == null) {
Move (0, 0);
Driver.AddStr (NoBuilderError);
return;
}
var map = BuildLineMap ();
for (int line = 0; line < bounds.Height; line++) {
var idxToRender = ScrollOffsetVertical + line;
// Is there part of the tree view to render?
if (idxToRender < map.Count) {
// Render the line
map.ElementAt (idxToRender).Draw (Driver, ColorScheme, line, bounds.Width);
} else {
// Else clear the line to prevent stale symbols due to scrolling etc
Move (0, line);
Driver.SetAttribute (ColorScheme.Normal);
Driver.AddStr (new string (' ', bounds.Width));
}
}
}
/// <summary>
/// Returns the index of the object <paramref name="o"/> if it is currently exposed (it's
/// parent(s) have been expanded). This can be used with <see cref="ScrollOffsetVertical"/>
/// and <see cref="View.SetNeedsDisplay()"/> to scroll to a specific object
/// </summary>
/// <remarks>Uses the Equals method and returns the first index at which the object is found
/// or -1 if it is not found</remarks>
/// <param name="o">An object that appears in your tree and is currently exposed</param>
/// <returns>The index the object was found at or -1 if it is not currently revealed or
/// not in the tree at all</returns>
public int GetScrollOffsetOf (T o)
{
var map = BuildLineMap ();
for (int i = 0; i < map.Count; i++) {
if (map.ElementAt (i).Model.Equals (o)) {
return i;
}
}
//object not found
return -1;
}
/// <summary>
/// Returns the maximum width line in the tree including prefix and expansion symbols
/// </summary>
/// <param name="visible">True to consider only rows currently visible (based on window
/// bounds and <see cref="ScrollOffsetVertical"/>. False to calculate the width of
/// every exposed branch in the tree</param>
/// <returns></returns>
public int GetContentWidth (bool visible)
{
var map = BuildLineMap ();
if (map.Count == 0) {
return 0;
}
if (visible) {
//Somehow we managed to scroll off the end of the control
if (ScrollOffsetVertical >= map.Count) {
return 0;
}
// If control has no height to it then there is no visible area for content
if (Bounds.Height == 0) {
return 0;
}
return map.Skip (ScrollOffsetVertical).Take (Bounds.Height).Max (b => b.GetWidth (Driver));
} else {
return map.Max (b => b.GetWidth (Driver));
}
}
/// <summary>
/// Calculates all currently visible/expanded branches (including leafs) and outputs them
/// by index from the top of the screen
/// </summary>
/// <remarks>Index 0 of the returned array is the first item that should be visible in the
/// top of the control, index 1 is the next etc.</remarks>
/// <returns></returns>
private IReadOnlyCollection<Branch<T>> BuildLineMap ()
{
if (cachedLineMap != null) {
return cachedLineMap;
}
List<Branch<T>> toReturn = new List<Branch<T>> ();
foreach (var root in roots.Values) {
toReturn.AddRange (AddToLineMap (root));
}
return cachedLineMap = new ReadOnlyCollection<Branch<T>> (toReturn);
}
private IEnumerable<Branch<T>> AddToLineMap (Branch<T> currentBranch)
{
yield return currentBranch;
if (currentBranch.IsExpanded) {
foreach (var subBranch in currentBranch.ChildBranches.Values) {
foreach (var sub in AddToLineMap (subBranch)) {
yield return sub;
}
}
}
}
/// <inheritdoc/>
public override bool ProcessKey (KeyEvent keyEvent)
{
if (keyEvent.Key == ObjectActivationKey) {
var o = SelectedObject;
if (o != null) {
OnObjectActivated (new ObjectActivatedEventArgs<T> (this, o));
PositionCursor ();
return true;
}
}
if (keyEvent.KeyValue > 0 && keyEvent.KeyValue < 0xFFFF) {
var character = (char)keyEvent.KeyValue;
// if it is a single character pressed without any control keys
if (char.IsLetterOrDigit (character) && AllowLetterBasedNavigation && !keyEvent.IsShift && !keyEvent.IsAlt && !keyEvent.IsCtrl) {
// search for next branch that begins with that letter
var characterAsStr = character.ToString ();
AdjustSelectionToNext (b => AspectGetter (b.Model).StartsWith (characterAsStr, StringComparison.CurrentCultureIgnoreCase));
PositionCursor ();
return true;
}
}
switch (keyEvent.Key) {
case Key.CursorRight:
Expand (SelectedObject);
break;
case Key.CursorRight | Key.CtrlMask:
ExpandAll (SelectedObject);
break;
case Key.CursorLeft:
case Key.CursorLeft | Key.CtrlMask:
CursorLeft (keyEvent.Key.HasFlag (Key.CtrlMask));
break;
case Key.CursorUp:
case Key.CursorUp | Key.ShiftMask:
AdjustSelection (-1, keyEvent.Key.HasFlag (Key.ShiftMask));
break;
case Key.CursorDown:
case Key.CursorDown | Key.ShiftMask:
AdjustSelection (1, keyEvent.Key.HasFlag (Key.ShiftMask));
break;
case Key.CursorUp | Key.CtrlMask:
AdjustSelectionToBranchStart ();
break;
case Key.CursorDown | Key.CtrlMask:
AdjustSelectionToBranchEnd ();
break;
case Key.PageUp:
case Key.PageUp | Key.ShiftMask:
AdjustSelection (-Bounds.Height, keyEvent.Key.HasFlag (Key.ShiftMask));
break;
case Key.PageDown:
case Key.PageDown | Key.ShiftMask:
AdjustSelection (Bounds.Height, keyEvent.Key.HasFlag (Key.ShiftMask));
break;
case Key.A | Key.CtrlMask:
SelectAll ();
break;
case Key.Home:
GoToFirst ();
break;
case Key.End:
GoToEnd ();
break;
default:
// we don't care about this keystroke
return false;
}
PositionCursor ();
return true;
}
/// <summary>
/// Raises the <see cref="ObjectActivated"/> event
/// </summary>
/// <param name="e"></param>
protected virtual void OnObjectActivated (ObjectActivatedEventArgs<T> e)
{
ObjectActivated?.Invoke (e);
}
///<inheritdoc/>
public override bool MouseEvent (MouseEvent me)
{
// If it is not an event we care about
if (!me.Flags.HasFlag (MouseFlags.Button1Clicked) &&
!me.Flags.HasFlag (ObjectActivationButton ?? MouseFlags.Button1DoubleClicked) &&
!me.Flags.HasFlag (MouseFlags.WheeledDown) &&
!me.Flags.HasFlag (MouseFlags.WheeledUp) &&
!me.Flags.HasFlag (MouseFlags.WheeledRight) &&
!me.Flags.HasFlag (MouseFlags.WheeledLeft)) {
// do nothing
return false;
}
if (!HasFocus && CanFocus) {
SetFocus ();
}
if (me.Flags == MouseFlags.WheeledDown) {
ScrollOffsetVertical++;
SetNeedsDisplay ();
return true;
} else if (me.Flags == MouseFlags.WheeledUp) {
ScrollOffsetVertical--;
SetNeedsDisplay ();
return true;
}
if (me.Flags == MouseFlags.WheeledRight) {
ScrollOffsetHorizontal++;
SetNeedsDisplay ();
return true;
} else if (me.Flags == MouseFlags.WheeledLeft) {
ScrollOffsetHorizontal--;
SetNeedsDisplay ();
return true;
}
if (me.Flags.HasFlag (MouseFlags.Button1Clicked)) {
// The line they clicked on a branch
var clickedBranch = HitTest (me.Y);
if (clickedBranch == null) {
return false;
}
bool isExpandToggleAttempt = clickedBranch.IsHitOnExpandableSymbol (Driver, me.X);
// If we are already selected (double click)
if (Equals (SelectedObject, clickedBranch.Model)) {
isExpandToggleAttempt = true;
}
// if they clicked on the +/- expansion symbol
if (isExpandToggleAttempt) {
if (clickedBranch.IsExpanded) {
clickedBranch.Collapse ();
InvalidateLineMap ();
} else
if (clickedBranch.CanExpand ()) {
clickedBranch.Expand ();
InvalidateLineMap ();
} else {
SelectedObject = clickedBranch.Model; // It is a leaf node
multiSelectedRegions.Clear ();
}
} else {
// It is a first click somewhere in the current line that doesn't look like an expansion/collapse attempt
SelectedObject = clickedBranch.Model;
multiSelectedRegions.Clear ();
}
SetNeedsDisplay ();
return true;
}
// If it is activation via mouse (e.g. double click)
if (ObjectActivationButton.HasValue && me.Flags.HasFlag (ObjectActivationButton.Value)) {
// The line they clicked on a branch
var clickedBranch = HitTest (me.Y);
if (clickedBranch == null) {
return false;
}
// Double click changes the selection to the clicked node as well as triggering
// activation otherwise it feels wierd
SelectedObject = clickedBranch.Model;
SetNeedsDisplay ();
// trigger activation event
OnObjectActivated (new ObjectActivatedEventArgs<T> (this, clickedBranch.Model));
// mouse event is handled.
return true;
}
return false;
}
/// <summary>
/// Returns the branch at the given <paramref name="y"/> client
/// coordinate e.g. following a click event
/// </summary>
/// <param name="y">Client Y position in the controls bounds</param>
/// <returns>The clicked branch or null if outside of tree region</returns>
private Branch<T> HitTest (int y)
{
var map = BuildLineMap ();
var idx = y + ScrollOffsetVertical;
// click is outside any visible nodes
if (idx < 0 || idx >= map.Count) {
return null;
}
// The line they clicked on
return map.ElementAt (idx);
}
/// <summary>
/// Positions the cursor at the start of the selected objects line (if visible)
/// </summary>
public override void PositionCursor ()
{
if (CanFocus && HasFocus && Visible && SelectedObject != null) {
var map = BuildLineMap ();
var idx = map.IndexOf(b => b.Model.Equals (SelectedObject));
// if currently selected line is visible
if (idx - ScrollOffsetVertical >= 0 && idx - ScrollOffsetVertical < Bounds.Height) {
Move (0, idx - ScrollOffsetVertical);
} else {
base.PositionCursor ();
}
} else {
base.PositionCursor ();
}
}
/// <summary>
/// Determines systems behaviour when the left arrow key is pressed. Default behaviour is
/// to collapse the current tree node if possible otherwise changes selection to current
/// branches parent
/// </summary>
protected virtual void CursorLeft (bool ctrl)
{
if (IsExpanded (SelectedObject)) {
if (ctrl) {
CollapseAll (SelectedObject);
} else {
Collapse (SelectedObject);
}
} else {
var parent = GetParent (SelectedObject);
if (parent != null) {
SelectedObject = parent;
AdjustSelection (0);
SetNeedsDisplay ();
}
}
}
/// <summary>
/// Changes the <see cref="SelectedObject"/> to the first root object and resets
/// the <see cref="ScrollOffsetVertical"/> to 0
/// </summary>
public void GoToFirst ()
{
ScrollOffsetVertical = 0;
SelectedObject = roots.Keys.FirstOrDefault ();
SetNeedsDisplay ();
}
/// <summary>
/// Changes the <see cref="SelectedObject"/> to the last object in the tree and scrolls so
/// that it is visible
/// </summary>
public void GoToEnd ()
{
var map = BuildLineMap ();
ScrollOffsetVertical = Math.Max (0, map.Count - Bounds.Height + 1);
SelectedObject = map.Last ().Model;
SetNeedsDisplay ();
}
/// <summary>
/// Changes the <see cref="SelectedObject"/> to <paramref name="toSelect"/> and scrolls to ensure
/// it is visible. Has no effect if <paramref name="toSelect"/> is not exposed in the tree (e.g.
/// its parents are collapsed)
/// </summary>
/// <param name="toSelect"></param>
public void GoTo (T toSelect)
{
if (ObjectToBranch (toSelect) == null) {
return;
}
SelectedObject = toSelect;
EnsureVisible (toSelect);
SetNeedsDisplay ();
}
/// <summary>
/// The number of screen lines to move the currently selected object by. Supports negative
/// <paramref name="offset"/>. Each branch occupies 1 line on screen
/// </summary>
/// <remarks>If nothing is currently selected or the selected object is no longer in the tree
/// then the first object in the tree is selected instead</remarks>
/// <param name="offset">Positive to move the selection down the screen, negative to move it up</param>
/// <param name="expandSelection">True to expand the selection (assuming
/// <see cref="MultiSelect"/> is enabled). False to replace</param>
public void AdjustSelection (int offset, bool expandSelection = false)
{
// if it is not a shift click or we don't allow multi select
if (!expandSelection || !MultiSelect) {
multiSelectedRegions.Clear ();
}
if (SelectedObject == null) {
SelectedObject = roots.Keys.FirstOrDefault ();
} else {
var map = BuildLineMap ();
var idx = map.IndexOf(b => b.Model.Equals (SelectedObject));
if (idx == -1) {
// The current selection has disapeared!
SelectedObject = roots.Keys.FirstOrDefault ();
} else {
var newIdx = Math.Min (Math.Max (0, idx + offset), map.Count - 1);
var newBranch = map.ElementAt(newIdx);
// If it is a multi selection
if (expandSelection && MultiSelect) {
if (multiSelectedRegions.Any ()) {
// expand the existing head selection
var head = multiSelectedRegions.Pop ();
multiSelectedRegions.Push (new TreeSelection<T> (head.Origin, newIdx, map));
} else {
// or start a new multi selection region
multiSelectedRegions.Push (new TreeSelection<T> (map.ElementAt(idx), newIdx, map));
}
}
SelectedObject = newBranch.Model;
EnsureVisible (SelectedObject);
}
}
SetNeedsDisplay ();
}
/// <summary>
/// Moves the selection to the first child in the currently selected level
/// </summary>
public void AdjustSelectionToBranchStart ()
{
var o = SelectedObject;
if (o == null) {
return;
}
var map = BuildLineMap ();
int currentIdx = map.IndexOf(b => Equals (b.Model, o));
if (currentIdx == -1) {
return;
}
var currentBranch = map.ElementAt(currentIdx);
var next = currentBranch;
for (; currentIdx >= 0; currentIdx--) {
//if it is the beginning of the current depth of branch
if (currentBranch.Depth != next.Depth) {
SelectedObject = currentBranch.Model;
EnsureVisible (currentBranch.Model);
SetNeedsDisplay ();
return;
}
// look at next branch up for consideration
currentBranch = next;
next = map.ElementAt(currentIdx);
}
// We ran all the way to top of tree
GoToFirst ();
}
/// <summary>
/// Moves the selection to the last child in the currently selected level
/// </summary>
public void AdjustSelectionToBranchEnd ()
{
var o = SelectedObject;
if (o == null) {
return;
}
var map = BuildLineMap ();
int currentIdx = map.IndexOf(b => Equals (b.Model, o));
if (currentIdx == -1) {
return;
}
var currentBranch = map.ElementAt(currentIdx);
var next = currentBranch;
for (; currentIdx < map.Count; currentIdx++) {
//if it is the end of the current depth of branch
if (currentBranch.Depth != next.Depth) {
SelectedObject = currentBranch.Model;
EnsureVisible (currentBranch.Model);
SetNeedsDisplay ();
return;
}
// look at next branch for consideration
currentBranch = next;
next = map.ElementAt(currentIdx);
}
GoToEnd ();
}
/// <summary>
/// Sets the selection to the next branch that matches the <paramref name="predicate"/>
/// </summary>
/// <param name="predicate"></param>
private void AdjustSelectionToNext (Func<Branch<T>, bool> predicate)
{
var map = BuildLineMap ();
// empty map means we can't select anything anyway
if (map.Count == 0) {
return;
}
// Start searching from the first element in the map
var idxStart = 0;
// or the current selected branch
if (SelectedObject != null) {
idxStart = map.IndexOf(b => Equals (b.Model, SelectedObject));
}
// if currently selected object mysteriously vanished, search from beginning
if (idxStart == -1) {
idxStart = 0;
}
// loop around all indexes and back to first index
for (int idxCur = (idxStart + 1) % map.Count; idxCur != idxStart; idxCur = (idxCur + 1) % map.Count) {
if (predicate (map.ElementAt(idxCur))) {
SelectedObject = map.ElementAt(idxCur).Model;
EnsureVisible (map.ElementAt(idxCur).Model);
SetNeedsDisplay ();
return;
}
}
}
/// <summary>
/// Adjusts the <see cref="ScrollOffsetVertical"/> to ensure the given
/// <paramref name="model"/> is visible. Has no effect if already visible
/// </summary>
public void EnsureVisible (T model)
{
var map = BuildLineMap ();
var idx = map.IndexOf(b => Equals (b.Model, model));
if (idx == -1) {
return;
}
/*this -1 allows for possible horizontal scroll bar in the last row of the control*/
int leaveSpace = Style.LeaveLastRow ? 1 : 0;
if (idx < ScrollOffsetVertical) {
//if user has scrolled up too far to see their selection
ScrollOffsetVertical = idx;
} else if (idx >= ScrollOffsetVertical + Bounds.Height - leaveSpace) {
//if user has scrolled off bottom of visible tree
ScrollOffsetVertical = Math.Max (0, (idx + 1) - (Bounds.Height - leaveSpace));
}
}
/// <summary>
/// Expands the supplied object if it is contained in the tree (either as a root object or
/// as an exposed branch object)
/// </summary>
/// <param name="toExpand">The object to expand</param>
public void Expand (T toExpand)