瀏覽代碼

Added ability to select multiple branches at once

tznind 4 年之前
父節點
當前提交
06bb01a68f
共有 1 個文件被更改,包括 96 次插入10 次删除
  1. 96 10
      Terminal.Gui/Views/TreeView.cs

+ 96 - 10
Terminal.Gui/Views/TreeView.cs

@@ -269,7 +269,7 @@ namespace Terminal.Gui {
 		public bool LeaveLastRow {get;set;}
 		public bool LeaveLastRow {get;set;}
 
 
 	}
 	}
-
+	
 	/// <summary>
 	/// <summary>
 	/// Hierarchical tree view with expandable branches.  Branch objects are dynamically determined when expanded using a user defined <see cref="ITreeBuilder{T}"/>
 	/// Hierarchical tree view with expandable branches.  Branch objects are dynamically determined when expanded using a user defined <see cref="ITreeBuilder{T}"/>
 	/// </summary>
 	/// </summary>
@@ -295,8 +295,15 @@ namespace Terminal.Gui {
 		/// </summary>
 		/// </summary>
 		public TreeStyle Style {get;set;} = new TreeStyle();
 		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>
 		/// <summary>
-		/// The currently selected object in the tree
+		/// The currently selected object in the tree.  When <see cref="MultiSelect"/> is true this is the object at which the cursor is at
 		/// </summary>
 		/// </summary>
 		public T SelectedObject { 
 		public T SelectedObject { 
 			get => selectedObject; 
 			get => selectedObject; 
@@ -308,7 +315,12 @@ namespace Terminal.Gui {
 					SelectionChanged?.Invoke(this,new SelectionChangedEventArgs<T>(this,oldValue,value));
 					SelectionChanged?.Invoke(this,new SelectionChangedEventArgs<T>(this,oldValue,value));
 			}
 			}
 		}
 		}
-		
+
+		/// <summary>
+		/// Secondary selected regions of tree when <see cref="MultiSelect"/> is true
+		/// </summary>
+		private Stack<TreeSelection<T>> _multiSelectedRegions = new Stack<TreeSelection<T>>();
+
 		/// <summary>
 		/// <summary>
 		/// Called when the <see cref="SelectedObject"/> changes
 		/// Called when the <see cref="SelectedObject"/> changes
 		/// </summary>
 		/// </summary>
@@ -392,6 +404,7 @@ namespace Terminal.Gui {
 		public void ClearObjects()
 		public void ClearObjects()
 		{
 		{
 			SelectedObject = default(T);
 			SelectedObject = default(T);
+			_multiSelectedRegions.Clear();
 			roots = new Dictionary<T, Branch<T>>();
 			roots = new Dictionary<T, Branch<T>>();
 			SetNeedsDisplay();
 			SetNeedsDisplay();
 		}
 		}
@@ -600,17 +613,21 @@ namespace Terminal.Gui {
 				break;
 				break;
 			
 			
 				case Key.CursorUp:
 				case Key.CursorUp:
-					AdjustSelection(-1);
+				case Key.CursorUp | Key.ShiftMask:
+					AdjustSelection(-1,keyEvent.Key.HasFlag(Key.ShiftMask));
 				break;
 				break;
 				case Key.CursorDown:
 				case Key.CursorDown:
-					AdjustSelection(1);
+				case Key.CursorDown | Key.ShiftMask:
+					AdjustSelection(1,keyEvent.Key.HasFlag(Key.ShiftMask));
 				break;
 				break;
 				case Key.PageUp:
 				case Key.PageUp:
-					AdjustSelection(-Bounds.Height);
+				case Key.PageUp | Key.ShiftMask:
+					AdjustSelection(-Bounds.Height,keyEvent.Key.HasFlag(Key.ShiftMask));
 				break;
 				break;
 				
 				
 				case Key.PageDown:
 				case Key.PageDown:
-					AdjustSelection(Bounds.Height);
+				case Key.PageDown | Key.ShiftMask:
+					AdjustSelection(Bounds.Height,keyEvent.Key.HasFlag(Key.ShiftMask));
 				break;
 				break;
 				case Key.Home:
 				case Key.Home:
 					GoToFirst();
 					GoToFirst();
@@ -779,8 +796,13 @@ namespace Terminal.Gui {
 		/// </summary>
 		/// </summary>
 		/// <remarks>If nothing is currently selected the first root is selected.  If the selected object is no longer in the tree the first object is selected</remarks>
 		/// <remarks>If nothing is currently selected the first root is selected.  If the selected object is no longer in the tree the first object is selected</remarks>
 		/// <param name="offset"></param>
 		/// <param name="offset"></param>
-		public void AdjustSelection (int offset)
+		/// <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){
 			if(SelectedObject == null){
 				SelectedObject = roots.Keys.FirstOrDefault();
 				SelectedObject = roots.Keys.FirstOrDefault();
 			}
 			}
@@ -796,7 +818,26 @@ namespace Terminal.Gui {
 				}
 				}
 				else {
 				else {
 					var newIdx = Math.Min(Math.Max(0,idx+offset),map.Length-1);
 					var newIdx = Math.Min(Math.Max(0,idx+offset),map.Length-1);
-					SelectedObject = map[newIdx].Model;
+					
+					var newBranch = map[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[idx],newIdx,map));
+						}
+					}
+
+					SelectedObject = newBranch.Model;
 
 
  					/*this -1 allows for possible horizontal scroll bar in the last row of the control*/
  					/*this -1 allows for possible horizontal scroll bar in the last row of the control*/
 					int leaveSpace = Style.LeaveLastRow ? 1 :0;
 					int leaveSpace = Style.LeaveLastRow ? 1 :0;
@@ -872,6 +913,51 @@ namespace Terminal.Gui {
 		{
 		{
 			return BuildLineMap().FirstOrDefault(o=>o.Model.Equals(toFind));
 			return BuildLineMap().FirstOrDefault(o=>o.Model.Equals(toFind));
 		}
 		}
+
+		/// <summary>
+		/// Returns true if the <paramref name="model"/> is either the <see cref="SelectedObject"/> or part of a <see cref="MultiSelectedObjects"/>
+		/// </summary>
+		/// <param name="model"></param>
+		/// <returns></returns>
+		public bool IsSelected (T model)
+		{
+			return SelectedObject == model ||
+				(MultiSelect && _multiSelectedRegions.Any(s=>s.Contains(model)));
+		}
+	}
+
+	class TreeSelection<T> where T : class {
+
+		public Branch<T> Origin {get;}
+
+		private HashSet<Branch<T>> alsoIncluded = new HashSet<Branch<T>>();
+
+		/// <summary>
+		/// Creates a new selection between two branches in the tree
+		/// </summary>
+		/// <param name="from"></param>
+		/// <param name="toIndex"></param>
+		/// <param name="map"></param>
+		public TreeSelection(Branch<T> from, int toIndex, Branch<T>[] map )
+		{
+			Origin = from;
+			var oldIdx = Array.IndexOf(map,from);
+
+			var lowIndex = Math.Min(oldIdx,toIndex);
+			var highIndex = Math.Max(oldIdx,toIndex);
+
+			// Select everything between the old and new indexes
+			foreach(var alsoInclude in map.Skip(lowIndex).Take(highIndex-lowIndex)){
+				alsoIncluded.Add(alsoInclude);
+			}
+		
+		}
+		public bool Contains(T model)
+		{
+			return 
+			Equals(Origin.Model,model) || 
+			alsoIncluded.Any(b=>Equals(b.Model,model));
+		}
 	}
 	}
 
 
 	class Branch<T> where T:class
 	class Branch<T> where T:class
@@ -956,7 +1042,7 @@ namespace Terminal.Gui {
 		public virtual void Draw(ConsoleDriver driver,ColorScheme colorScheme, int y, int availableWidth)
 		public virtual void Draw(ConsoleDriver driver,ColorScheme colorScheme, int y, int availableWidth)
 		{
 		{
 			// true if the current line of the tree is the selected one and control has focus
 			// true if the current line of the tree is the selected one and control has focus
-			bool isSelected = tree.SelectedObject == Model && tree.HasFocus;
+			bool isSelected = tree.IsSelected(Model) && tree.HasFocus;
 			Attribute lineColor = isSelected? colorScheme.Focus : colorScheme.Normal;
 			Attribute lineColor = isSelected? colorScheme.Focus : colorScheme.Normal;
 
 
 			driver.SetAttribute(lineColor);
 			driver.SetAttribute(lineColor);