Skip to content

Commit 00cab14

Browse files
authored
[DYN-8893] Improvements to group behavior (#16334)
1 parent ea37ac1 commit 00cab14

4 files changed

Lines changed: 92 additions & 1 deletion

File tree

src/DynamoCoreWpf/Utilities/WpfUtilities.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,5 +111,18 @@ public static async void DelayInvoke(this Dispatcher ds, int delay, Action callb
111111
await ds.BeginInvoke(callback);
112112
}
113113

114+
/// <summary>
115+
/// Recursively searches up the visual tree from the specified child to find the first parent of type <typeparamref name="T"/>.
116+
/// </summary>
117+
/// <typeparam name="T">The type of parent to search for.</typeparam>
118+
/// <param name="child">The starting element in the visual tree.</param>
119+
/// <returns>The first parent of type <typeparamref name="T"/> if found; otherwise, <c>null</c>.</returns>
120+
public static T FindParent<T>(DependencyObject child) where T : DependencyObject
121+
{
122+
var parent = VisualTreeHelper.GetParent(child);
123+
if (parent == null) return null;
124+
return parent is T typedParent ? typedParent : FindParent<T>(parent);
125+
}
126+
114127
}
115128
}

src/DynamoCoreWpf/ViewModels/Core/StateMachine.cs

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ partial class WorkspaceViewModel
3030

3131
private readonly StateMachine stateMachine = null;
3232
private List<DraggedNode> draggedNodes = new List<DraggedNode>();
33+
private Dictionary<Guid, Point> draggedGroupOriginalPositions = new();
3334

3435
// When a new connector is created or a single connector is selected,
3536
// activeConnectors has array size of 1.
@@ -65,7 +66,8 @@ partial class WorkspaceViewModel
6566
[JsonIgnore]
6667
internal ConnectorViewModel FirstActiveConnector
6768
{
68-
get {
69+
get
70+
{
6971
if (null != activeConnectors && activeConnectors.Count() > 0)
7072
{
7173
return activeConnectors[0];
@@ -149,6 +151,14 @@ internal void BeginDragSelection(Point2D mouseCursor)
149151
{
150152
throw new InvalidOperationException(Wpf.Properties.Resources.InvalidDraggingOperationMessgae);
151153
}
154+
155+
// Track original positions of any selected annotation groups at the beginning of a drag
156+
// This allows us to later determine whether the group was truly moved or just clicked
157+
draggedGroupOriginalPositions.Clear();
158+
foreach (var group in DynamoSelection.Instance.Selection.OfType<AnnotationModel>())
159+
{
160+
draggedGroupOriginalPositions[group.GUID] = new Point(group.X, group.Y);
161+
}
152162
}
153163

154164
internal void UpdateDraggedSelection(Point2D mouseCursor)
@@ -763,6 +773,28 @@ internal bool HandleMouseRelease(object sender, MouseButtonEventArgs e)
763773
.OfType<AnnotationModel>()
764774
.ToList();
765775

776+
// DYN-8893: Prevent accidental grouping when overlapping groups are clicked without dragging
777+
// If a group visually overlaps another and is simply clicked (not dragged),
778+
// grouping can be mistakenly triggered. To avoid this, we record original positions
779+
// of dragged groups and compare them on mouse release. Grouping only proceeds if
780+
// a group was actually moved. This check is skipped if only nodes are selected
781+
bool anyGroupMoved = dragedGroups.All(group =>
782+
{
783+
if (!owningWorkspace.draggedGroupOriginalPositions.TryGetValue(group.GUID, out var originalPos))
784+
return true; // Assume moved if not tracked
785+
786+
var current = group.Position;
787+
return Math.Abs(originalPos.X - current.X) > 0.1 && Math.Abs(originalPos.Y - current.Y) > 0.1;
788+
});
789+
790+
// If we're dealing with groups and none of them moved, we shouldn't group them
791+
if (!anyGroupMoved && dragedGroups.Any())
792+
{
793+
dropGroup.NodeHoveringState = false;
794+
SetCurrentState(State.None);
795+
return false;
796+
}
797+
766798
// We do not want to add dragged groups content twice
767799
// so we filter it out here.
768800
var modelsToAdd = DynamoSelection.Instance.Selection

src/DynamoCoreWpf/ViewModels/Core/WorkspaceViewModel.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1225,6 +1225,26 @@ private bool IsModelInCollapsedGroup(ModelBase model)
12251225
return IsInCollapsedGroup;
12261226
}
12271227

1228+
/// <summary>
1229+
/// Handles double-clicks on annotation groups by creating a CBN at the click position
1230+
/// and adding it to the group if the position intersects with the group's region.
1231+
/// </summary>
1232+
internal void HandleAnnotationDoubleClick(Point position, AnnotationModel annotation)
1233+
{
1234+
if (DynamoViewModel?.Model == null) return;
1235+
1236+
var model = DynamoViewModel.Model;
1237+
1238+
// Create and add code node block
1239+
var newNode = new CodeBlockNodeModel(model.LibraryServices);
1240+
var cmd = new DynamoModel.CreateNodeCommand(newNode, position.X, position.Y, false, true);
1241+
DynamoViewModel.ExecuteCommand(cmd);
1242+
1243+
var updated = annotation.Nodes.ToList();
1244+
updated.Add(newNode);
1245+
annotation.Nodes = updated;
1246+
}
1247+
12281248
private static bool IsInRegion(Rect2D region, ILocatable locatable, bool fullyEnclosed)
12291249
{
12301250
double x0 = locatable.X;

src/DynamoCoreWpf/Views/Core/AnnotationView.xaml.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
using Dynamo.UI.Prompts;
2222
using Dynamo.Utilities;
2323
using Dynamo.ViewModels;
24+
using ModifierKeys = System.Windows.Input.ModifierKeys;
2425
using Dynamo.Views;
2526
using Dynamo.Wpf.Utilities;
2627
using DynCmd = Dynamo.Models.DynamoModel;
@@ -152,6 +153,7 @@ public AnnotationView()
152153
Loaded += AnnotationView_Loaded;
153154
DataContextChanged += AnnotationView_DataContextChanged;
154155
this.groupTextBlock.SizeChanged += GroupTextBlock_SizeChanged;
156+
PreviewMouseDoubleClick += OnAnnotationDoubleClick;
155157

156158
// Because the size of the collapsedAnnotationRectangle doesn't necessarily change
157159
// when going from Visible to collapse (and other way around), we need to also listen
@@ -206,6 +208,7 @@ private void AnnotationView_Unloaded(object sender, RoutedEventArgs e)
206208
{
207209
Loaded -= AnnotationView_Loaded;
208210
DataContextChanged -= AnnotationView_DataContextChanged;
211+
PreviewMouseDoubleClick -= OnAnnotationDoubleClick;
209212
if (groupTextBlock != null)
210213
groupTextBlock.SizeChanged -= GroupTextBlock_SizeChanged;
211214
if (collapsedAnnotationRectangle != null)
@@ -294,6 +297,29 @@ private void AnnotationView_Loaded(object sender, RoutedEventArgs e)
294297
}
295298
}
296299

300+
private void OnAnnotationDoubleClick(object sender, MouseButtonEventArgs e)
301+
{
302+
if (Keyboard.Modifiers == ModifierKeys.Shift || Keyboard.Modifiers == ModifierKeys.Control)
303+
return;
304+
305+
var workspace = WpfUtilities.FindParent<WorkspaceView>(this);
306+
if (workspace == null)
307+
return;
308+
309+
var clickPosition = e.GetPosition(workspace.WorkspaceElements);
310+
var model = ViewModel.AnnotationModel;
311+
312+
// Define the area below the text block where nodes reside
313+
var annoRectArea = new Rect(model.X, model.Y + model.TextBlockHeight, model.Width, model.ModelAreaHeight);
314+
315+
// Only create CBN if click is in model area (not in the title/text area)
316+
if (!annoRectArea.Contains(clickPosition))
317+
return;
318+
319+
workspace.ViewModel?.HandleAnnotationDoubleClick(clickPosition, model);
320+
e.Handled = true;
321+
}
322+
297323
private void OnNodeColorSelectionChanged(object sender, SelectionChangedEventArgs e)
298324
{
299325
if (e.AddedItems == null || (e.AddedItems.Count <= 0))

0 commit comments

Comments
 (0)