diff --git a/src/DynamoCoreWpf/Commands/NoteCommands.cs b/src/DynamoCoreWpf/Commands/NoteCommands.cs index c325645c409..910527efd06 100644 --- a/src/DynamoCoreWpf/Commands/NoteCommands.cs +++ b/src/DynamoCoreWpf/Commands/NoteCommands.cs @@ -1,9 +1,11 @@ -using Dynamo.UI.Commands; +using Dynamo.Controls; +using Dynamo.UI.Commands; +using Dynamo.Utilities; using Newtonsoft.Json; namespace Dynamo.ViewModels { - public partial class NoteViewModel + public partial class NoteViewModel: IWorkspaceElement { private DelegateCommand _selectCommand; [JsonIgnore] @@ -93,5 +95,22 @@ public DelegateCommand UnpinFromNodeCommand return unpinFromNodeCommand; } } + + /// + /// This signifies if the note should be rendered + /// + [JsonIgnore] + public bool IsVisibleInCanvas + { + get => isVisibleInCanvas; + set + { + isVisibleInCanvas = value; + RaisePropertyChanged(nameof(isVisibleInCanvas)); + } + } + private bool isVisibleInCanvas = false; + + public Rect2D Rect => Model.Rect; } } diff --git a/src/DynamoCoreWpf/Controls/DeferredContent.cs b/src/DynamoCoreWpf/Controls/DeferredContent.cs new file mode 100644 index 00000000000..6e308f755cf --- /dev/null +++ b/src/DynamoCoreWpf/Controls/DeferredContent.cs @@ -0,0 +1,132 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Threading; +using Dynamo.ViewModels; + +namespace Dynamo.Controls +{ + /// + /// https://stackoverflow.com/a/26543731 + /// + public class DeferredContent : ContentPresenter + { + public static void Focus(double x, double y) => Priority.Focus(x, y); + private static DeferredContentRenderPriority Priority = new(); + + public DataTemplate DeferredContentTemplate + { + get => (DataTemplate)GetValue(DeferredContentTemplateProperty); + set => SetValue(DeferredContentTemplateProperty, value); + } + + public static readonly DependencyProperty DeferredContentTemplateProperty = + DependencyProperty.Register(nameof(DeferredContentTemplate), + typeof(DataTemplate), typeof(DeferredContent), null); + + public DeferredContent() + { + IsVisibleChanged += DeferredContent_IsVisibleChanged; + } + + private void DeferredContent_IsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e) + { + if (true.Equals(e.NewValue)) + { + IsVisibleChanged -= DeferredContent_IsVisibleChanged; + if (DataContext == null) + { + DataContextChanged += DeferredContent_DataContextChanged; + return; + } + //Dispatcher.BeginInvoke(ShowDeferredContent, DispatcherPriority.Background); + Priority.Enqueue(this); + } + } + + private void DeferredContent_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e) + { + DataContextChanged -= DeferredContent_DataContextChanged; + // new node added,jump the queue + Dispatcher.BeginInvoke(ShowDeferredContent, DispatcherPriority.Render); + } + + public void ShowDeferredContent() + { + if (DeferredContentTemplate != null) + { + base.Content = DeferredContentTemplate.LoadContent(); + RaiseDeferredContentLoaded(); + } + } + + private void RaiseDeferredContentLoaded() + { + DeferredContentLoaded?.Invoke(this, new RoutedEventArgs()); + } + + public event EventHandler DeferredContentLoaded; + + private class DeferredContentRenderPriority + { + private bool isRunning = false; + private Point canvasCenter = default; + private readonly List<(DeferredContent, DispatcherOperation)> scheduled = []; + private readonly PriorityQueue pending = + new PriorityQueue(); + + public void Enqueue(DeferredContent control) + { + if (control.DataContext is not NodeViewModel nvm) return; + + var dist = (canvasCenter - new Point(nvm.X, nvm.Y)).Length; + pending.Enqueue(control, dist); + + if (!isRunning) + { + isRunning = true; + control.Dispatcher.BeginInvoke(SchedulePending, DispatcherPriority.ContextIdle); + } + } + + public void Focus(double x, double y) + { + isRunning = false; + canvasCenter = new Point(x, y); + + var toReschedule = new List(); + if (pending.Count > 0) + { + toReschedule.AddRange(pending.UnorderedItems.Select(i => i.Element)); + pending.Clear(); + } + + foreach (var (control, task) in scheduled) + { + if (task.Status != DispatcherOperationStatus.Completed && task.Abort()) + { + toReschedule.Add(control); + } + } + scheduled.Clear(); + + foreach (var control in toReschedule) + { + Enqueue(control); + } + } + + public void SchedulePending() + { + while (isRunning && pending.TryDequeue(out var control, out _)) + { + scheduled.Add((control, control.Dispatcher.BeginInvoke(control.ShowDeferredContent, DispatcherPriority.Background))); + } + + isRunning = false; + } + } + } +} diff --git a/src/DynamoCoreWpf/Controls/IWorkspaceElement.cs b/src/DynamoCoreWpf/Controls/IWorkspaceElement.cs new file mode 100644 index 00000000000..4f52c12fb0b --- /dev/null +++ b/src/DynamoCoreWpf/Controls/IWorkspaceElement.cs @@ -0,0 +1,10 @@ +using Dynamo.Utilities; + +namespace Dynamo.Controls +{ + internal interface IWorkspaceElement + { + bool IsVisibleInCanvas { get; set; } + Rect2D Rect { get; } + } +} diff --git a/src/DynamoCoreWpf/DynamoCoreWpf.csproj b/src/DynamoCoreWpf/DynamoCoreWpf.csproj index 25a013533bf..c159cc6e434 100644 --- a/src/DynamoCoreWpf/DynamoCoreWpf.csproj +++ b/src/DynamoCoreWpf/DynamoCoreWpf.csproj @@ -224,6 +224,7 @@ InstalledPackagesControl.xaml + NodeAutoCompleteSearchControl.xaml @@ -456,6 +457,7 @@ AboutWindow.xaml + ConnectorAnchorView.xaml diff --git a/src/DynamoCoreWpf/UI/Themes/Modern/DataTemplates.xaml b/src/DynamoCoreWpf/UI/Themes/Modern/DataTemplates.xaml index efbad42a4aa..c504727e6fa 100644 --- a/src/DynamoCoreWpf/UI/Themes/Modern/DataTemplates.xaml +++ b/src/DynamoCoreWpf/UI/Themes/Modern/DataTemplates.xaml @@ -1,4 +1,4 @@ - - - + + + + + + + + + + + + @@ -27,4 +46,4 @@ - \ No newline at end of file + diff --git a/src/DynamoCoreWpf/ViewModels/Core/AnnotationViewModel.cs b/src/DynamoCoreWpf/ViewModels/Core/AnnotationViewModel.cs index 6d8f87c6fee..02de5ac870e 100644 --- a/src/DynamoCoreWpf/ViewModels/Core/AnnotationViewModel.cs +++ b/src/DynamoCoreWpf/ViewModels/Core/AnnotationViewModel.cs @@ -6,6 +6,7 @@ using System.Windows.Input; using System.Windows.Media; using Dynamo.Configuration; +using Dynamo.Controls; using Dynamo.Graph; using Dynamo.Graph.Annotations; using Dynamo.Graph.Nodes; @@ -20,7 +21,7 @@ namespace Dynamo.ViewModels { - public class AnnotationViewModel : ViewModelBase + public class AnnotationViewModel : ViewModelBase, IWorkspaceElement { private AnnotationModel annotationModel; private IEnumerable originalInPorts; @@ -371,6 +372,24 @@ public ObservableCollection GroupStyleList RaisePropertyChanged(nameof(GroupStyleList)); } } + + /// + /// This signifies if the node should be rendered + /// + [JsonIgnore] + public bool IsVisibleInCanvas + { + get => isVisibleInCanvas; + set + { + isVisibleInCanvas = value; + RaisePropertyChanged(nameof(isVisibleInCanvas)); + } + } + private bool isVisibleInCanvas = false; + + public Rect2D Rect => AnnotationModel.Rect; + #endregion #region Commands diff --git a/src/DynamoCoreWpf/ViewModels/Core/NodeViewModel.cs b/src/DynamoCoreWpf/ViewModels/Core/NodeViewModel.cs index e45034ac7e0..c21c98a253d 100644 --- a/src/DynamoCoreWpf/ViewModels/Core/NodeViewModel.cs +++ b/src/DynamoCoreWpf/ViewModels/Core/NodeViewModel.cs @@ -11,6 +11,7 @@ using System.Windows.Controls.Primitives; using System.Windows.Media; using Dynamo.Configuration; +using Dynamo.Controls; using Dynamo.Engine.CodeGeneration; using Dynamo.Graph; using Dynamo.Graph.Nodes; @@ -20,6 +21,7 @@ using Dynamo.Logging; using Dynamo.Models; using Dynamo.Selection; +using Dynamo.Utilities; using Dynamo.Wpf.ViewModels.Core; using Newtonsoft.Json; using Point = System.Windows.Point; @@ -31,7 +33,7 @@ namespace Dynamo.ViewModels /// Interaction logic for dynControl.xaml /// - public partial class NodeViewModel : ViewModelBase + public partial class NodeViewModel : ViewModelBase, IWorkspaceElement { #region delegates public delegate void SetToolTipDelegate(string message); @@ -573,6 +575,23 @@ public bool NodeHoveringState } } + /// + /// This signifies if the node should be rendered + /// + [JsonIgnore] + public bool IsVisibleInCanvas + { + get => isVisibleInCanvas; + set + { + isVisibleInCanvas = value; + RaisePropertyChanged(nameof(isVisibleInCanvas)); + } + } + private bool isVisibleInCanvas = false; + + public Rect2D Rect => NodeModel.Rect; + private bool isNodeNewlyAdded; private ImageSource imageSource; private string imgGlyphOneSource; diff --git a/src/DynamoCoreWpf/ViewModels/Core/WorkspaceViewModel.cs b/src/DynamoCoreWpf/ViewModels/Core/WorkspaceViewModel.cs index 209a89ea37b..1456679c7bd 100644 --- a/src/DynamoCoreWpf/ViewModels/Core/WorkspaceViewModel.cs +++ b/src/DynamoCoreWpf/ViewModels/Core/WorkspaceViewModel.cs @@ -10,6 +10,7 @@ using System.Windows.Data; using System.Windows.Input; using Dynamo.Configuration; +using Dynamo.Controls; using Dynamo.Engine; using Dynamo.Graph; using Dynamo.Graph.Annotations; @@ -560,6 +561,8 @@ internal GeometryScalingViewModel GeoScalingViewModel } } + private bool FinishedLoading = false; + #endregion public WorkspaceViewModel(WorkspaceModel model, DynamoViewModel dynamoViewModel) @@ -634,6 +637,13 @@ public WorkspaceViewModel(WorkspaceModel model, DynamoViewModel dynamoViewModel) foreach (AnnotationModel annotation in Model.Annotations) Model_AnnotationAdded(annotation); foreach (ConnectorModel connector in Model.Connectors) Connectors_ConnectorAdded(connector); + FinishedLoading = true; + + NodeAutoCompleteSearchViewModel = new NodeAutoCompleteSearchViewModel(DynamoViewModel) + { + Visible = true + }; + geoScalingViewModel = new GeometryScalingViewModel(this.DynamoViewModel); geoScalingViewModel.ScaleValue = Convert.ToInt32(Math.Log10(Model.ScaleFactor)); @@ -879,6 +889,11 @@ private void Model_NoteAdded(NoteModel note) { var viewModel = new NoteViewModel(this, note); Notes.Add(viewModel); + + if (FinishedLoading) + { + viewModel.IsVisibleInCanvas = true; + } } private void Model_NoteRemoved(NoteModel note) @@ -901,6 +916,11 @@ private void Model_AnnotationAdded(AnnotationModel annotation) { var viewModel = new AnnotationViewModel(this, annotation); Annotations.Add(viewModel); + + if (FinishedLoading) + { + viewModel.IsVisibleInCanvas = true; + } } private void Model_AnnotationRemoved(AnnotationModel annotation) @@ -967,12 +987,21 @@ void Model_NodeAdded(NodeModel node) var nodeViewModel = new NodeViewModel(this, node); nodeViewModel.SnapInputEvent += nodeViewModel_SnapInputEvent; nodeViewModel.NodeLogic.Modified += OnNodeModified; + lock (Nodes) { Nodes.Add(nodeViewModel); } if (nodeViewModel.ErrorBubble != null) + { + nodeViewModel.ErrorBubble.IsVisibleInCanvas = FinishedLoading; Errors.Add(nodeViewModel.ErrorBubble); + } + + if (FinishedLoading) + { + nodeViewModel.IsVisibleInCanvas = true; + } PostNodeChangeActions(); @@ -1923,7 +1952,38 @@ internal IEnumerable GetViewModelsInternal(IEnumerable mode return foundModels; } - + internal void UpdateWorkspaceElementVisibility(double visibleWidth, double visibleHeight) + { + var width = visibleWidth / Zoom; + var height = visibleHeight / Zoom; + var left = -X / Zoom; + var top = -Y / Zoom; + + DeferredContent.Focus(left + width/2, top + height/2); + + var visibleRect = new Rect2D(left, top, width, height); + var itemsToTest = Nodes.Cast() + .Concat(Notes) + .Concat(Annotations); + + foreach (var item in itemsToTest) + { + item.IsVisibleInCanvas = visibleRect.IntersectsWith(item.Rect); + + if (item is NodeViewModel nvm && item.IsVisibleInCanvas && nvm.ErrorBubble != null) + { + nvm.ErrorBubble.IsVisibleInCanvas = true; + } + } + +#if DEBUG + var total = Nodes.Count + Notes.Count + Annotations.Count; + var hidden = Nodes.Count(n => !n.IsVisibleInCanvas) + + Notes.Count(n => !n.IsVisibleInCanvas) + + Annotations.Count(a => !a.IsVisibleInCanvas); + Debug.WriteLine($"{hidden}/{total} workspace elements hidden"); +#endif + } } public class ViewModelEventArgs : EventArgs diff --git a/src/DynamoCoreWpf/ViewModels/Preview/InfoBubbleViewModel.cs b/src/DynamoCoreWpf/ViewModels/Preview/InfoBubbleViewModel.cs index 7961145c27c..daa420d1f1c 100644 --- a/src/DynamoCoreWpf/ViewModels/Preview/InfoBubbleViewModel.cs +++ b/src/DynamoCoreWpf/ViewModels/Preview/InfoBubbleViewModel.cs @@ -4,7 +4,9 @@ using System.Linq; using System.Text.RegularExpressions; using System.Windows; +using Dynamo.Controls; using Dynamo.Logging; +using Dynamo.Utilities; using Dynamo.Wpf.ViewModels.Core; namespace Dynamo.ViewModels @@ -26,7 +28,7 @@ public InfoBubbleEventArgs(Request request) public delegate void InfoBubbleEventHandler(object sender, InfoBubbleEventArgs e); - public partial class InfoBubbleViewModel : ViewModelBase + public partial class InfoBubbleViewModel : ViewModelBase, IWorkspaceElement { public enum Style { @@ -444,7 +446,23 @@ public bool NodeWarningsIteratorVisible /// and cannot have their Width (or Visibility) set manually. /// public bool NodeErrorsIteratorVisible => GetMessagesOfStyle(NodeMessages, Style.Error).Count > 1; - + + /// + /// This signifies if the node should be rendered + /// + public bool IsVisibleInCanvas + { + get => isVisibleInCanvas; + set + { + isVisibleInCanvas = value; + RaisePropertyChanged(nameof(isVisibleInCanvas)); + } + } + private bool isVisibleInCanvas = false; + + public Rect2D Rect => default; + #endregion #region Event Handlers diff --git a/src/DynamoCoreWpf/Views/Core/WorkspaceView.xaml b/src/DynamoCoreWpf/Views/Core/WorkspaceView.xaml index 7d3f61daa72..35c86b4419a 100644 --- a/src/DynamoCoreWpf/Views/Core/WorkspaceView.xaml +++ b/src/DynamoCoreWpf/Views/Core/WorkspaceView.xaml @@ -185,6 +185,9 @@ Value="{Binding Top}" /> + diff --git a/src/DynamoCoreWpf/Views/Core/WorkspaceView.xaml.cs b/src/DynamoCoreWpf/Views/Core/WorkspaceView.xaml.cs index a64660908c0..3afabced695 100644 --- a/src/DynamoCoreWpf/Views/Core/WorkspaceView.xaml.cs +++ b/src/DynamoCoreWpf/Views/Core/WorkspaceView.xaml.cs @@ -149,6 +149,8 @@ private void RemoveViewModelsubscriptions(WorkspaceViewModel ViewModel) ViewModel.Model.CurrentOffsetChanged -= vm_CurrentOffsetChanged; DynamoSelection.Instance.Selection.CollectionChanged -= OnSelectionCollectionChanged; infiniteGridView.DetachFromZoomBorder(zoomBorder); + + zoomBorder.ViewSettingsChanged -= CallUpdateWorkspaceElementVisibility; } /// @@ -178,6 +180,24 @@ private void AttachViewModelsubscriptions(WorkspaceViewModel ViewModel) ViewModel.Model.CurrentOffsetChanged += vm_CurrentOffsetChanged; DynamoSelection.Instance.Selection.CollectionChanged += OnSelectionCollectionChanged; infiniteGridView.AttachToZoomBorder(zoomBorder); + + zoomBorder.ViewSettingsChanged += CallUpdateWorkspaceElementVisibility; + // load the initially visible nodes as soon as the context is loaded + Dispatcher.BeginInvoke(UpdateWorkspaceElementVisibility, System.Windows.Threading.DispatcherPriority.ContextIdle); + } + + private void CallUpdateWorkspaceElementVisibility(ViewSettingsChangedEventArgs _) => UpdateWorkspaceElementVisibility(); + + private void UpdateWorkspaceElementVisibility() + { + double visibleWidth = outerCanvas?.ActualWidth ?? 0; + double visibleHeight = outerCanvas?.ActualHeight ?? 0; + if (visibleWidth <= 0 || visibleHeight <= 0) + { + return; + } + + ViewModel.UpdateWorkspaceElementVisibility(visibleWidth, visibleHeight); } private void ShowHideNodeAutoCompleteControl(ShowHideFlags flag)