From c4340c3c97ede7e8f8bec1c26d1b9a5feb2ad70f Mon Sep 17 00:00:00 2001 From: Robert Date: Thu, 26 Aug 2021 22:09:04 +0200 Subject: [PATCH] Nodes - Added categories Nodes - Added quick node creation Nodes - Added input and output types to NodeAttribute --- src/Artemis.Core/Services/NodeService.cs | 4 +- .../VisualScripting/NodeAttribute.cs | 2 + src/Artemis.Core/VisualScripting/NodeData.cs | 8 +- .../Behaviors/HighlightTermBehavior.cs | 169 ++++++++++++++++++ .../Controls/VisualScriptNodeCreationBox.cs | 50 +++++- .../Editor/Controls/VisualScriptPresenter.cs | 93 ++++++++-- .../Styles/VisualScriptNodeCreationBox.xaml | 53 +++++- .../Editor/Styles/VisualScriptPresenter.xaml | 1 + .../Nodes/BoolOperations.cs | 8 +- .../Nodes/Color/BrightenSKColorNode.cs | 4 +- .../Nodes/Color/DarkenSKColorNode.cs | 4 +- .../Nodes/Color/DesaturateSKColorNode.cs | 7 +- .../Nodes/Color/HslSKColorNode.cs | 2 +- .../Nodes/Color/InvertSKColorNode.cs | 11 +- .../Nodes/Color/RotateHueSkColorNode.cs | 2 +- .../Nodes/Color/SaturateSkColorNode.cs | 7 +- .../Nodes/Color/StaticSKColorValueNode.cs | 2 +- .../Nodes/Color/SumSKColorsNode.cs | 2 +- .../Nodes/ConvertNodes.cs | 8 +- .../Nodes/DataModelNode.cs | 2 +- .../Nodes/Easing/DoubleEasingNode.cs | 2 +- .../Nodes/Easing/EasingTypeNode.cs | 2 +- .../Nodes/Easing/FloatEasingNode.cs | 2 +- .../Nodes/Easing/IntEasingNode.cs | 2 +- .../Nodes/Easing/SKColorEasingNode.cs | 2 +- .../Nodes/LayerPropertyNode.cs | 2 +- .../Nodes/StaticValueNodes.cs | 12 +- .../Nodes/StringFormatNode.cs | 2 +- src/Artemis.VisualScripting/Nodes/SumNode.cs | 6 +- 29 files changed, 389 insertions(+), 82 deletions(-) create mode 100644 src/Artemis.VisualScripting/Behaviors/HighlightTermBehavior.cs diff --git a/src/Artemis.Core/Services/NodeService.cs b/src/Artemis.Core/Services/NodeService.cs index 9d3ba8549..59e4ccb65 100644 --- a/src/Artemis.Core/Services/NodeService.cs +++ b/src/Artemis.Core/Services/NodeService.cs @@ -53,8 +53,8 @@ namespace Artemis.Core.Services string name = nodeAttribute?.Name ?? nodeType.Name; string description = nodeAttribute?.Description ?? string.Empty; string category = nodeAttribute?.Category ?? string.Empty; - - NodeData nodeData = new(plugin, nodeType, name, description, category, (s, e) => CreateNode(s, e, nodeType)); + + NodeData nodeData = new(plugin, nodeType, name, description, category, nodeAttribute?.InputType, nodeAttribute?.OutputType, (s, e) => CreateNode(s, e, nodeType)); return NodeTypeStore.Add(nodeData); } diff --git a/src/Artemis.Core/VisualScripting/NodeAttribute.cs b/src/Artemis.Core/VisualScripting/NodeAttribute.cs index ffa408667..4866bc366 100644 --- a/src/Artemis.Core/VisualScripting/NodeAttribute.cs +++ b/src/Artemis.Core/VisualScripting/NodeAttribute.cs @@ -9,6 +9,8 @@ namespace Artemis.Core public string Name { get; } public string Description { get; set; } public string Category { get; set; } + public Type InputType { get; set; } + public Type OutputType { get; set; } #endregion diff --git a/src/Artemis.Core/VisualScripting/NodeData.cs b/src/Artemis.Core/VisualScripting/NodeData.cs index 8a13d4580..f1c2c0dc9 100644 --- a/src/Artemis.Core/VisualScripting/NodeData.cs +++ b/src/Artemis.Core/VisualScripting/NodeData.cs @@ -13,20 +13,24 @@ namespace Artemis.Core public string Name { get; } public string Description { get; } public string Category { get; } - + public Type? InputType { get; } + public Type? OutputType { get; } + private Func _create; #endregion #region Constructors - internal NodeData(Plugin plugin, Type type, string name, string description, string category, Func? create) + internal NodeData(Plugin plugin, Type type, string name, string description, string category, Type? inputType, Type? outputType, Func? create) { this.Plugin = plugin; this.Type = type; this.Name = name; this.Description = description; this.Category = category; + this.InputType = inputType; + this.OutputType = outputType; this._create = create; } diff --git a/src/Artemis.VisualScripting/Behaviors/HighlightTermBehavior.cs b/src/Artemis.VisualScripting/Behaviors/HighlightTermBehavior.cs new file mode 100644 index 000000000..79cabf9fc --- /dev/null +++ b/src/Artemis.VisualScripting/Behaviors/HighlightTermBehavior.cs @@ -0,0 +1,169 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Documents; +using System.Windows.Media; + +namespace Artemis.VisualScripting.Behaviors +{ + // Source: https://stackoverflow.com/a/60474831/5015269 + // Made some changes to add a foreground and background property + public static class HighlightTermBehavior + { + public static readonly DependencyProperty TextProperty = DependencyProperty.RegisterAttached( + "Text", + typeof(string), + typeof(HighlightTermBehavior), + new FrameworkPropertyMetadata("", OnTextChanged)); + + public static readonly DependencyProperty TermToBeHighlightedProperty = DependencyProperty.RegisterAttached( + "TermToBeHighlighted", + typeof(string), + typeof(HighlightTermBehavior), + new FrameworkPropertyMetadata("", OnTextChanged)); + + public static readonly DependencyProperty HighlightForegroundProperty = DependencyProperty.RegisterAttached( + "HighlightForeground", + typeof(Color?), + typeof(HighlightTermBehavior), + new FrameworkPropertyMetadata(null, OnTextChanged)); + + public static readonly DependencyProperty HighlightBackgroundProperty = DependencyProperty.RegisterAttached( + "HighlightBackground", + typeof(Color?), + typeof(HighlightTermBehavior), + new FrameworkPropertyMetadata(null, OnTextChanged)); + + public static string GetText(FrameworkElement frameworkElement) + { + return (string) frameworkElement.GetValue(TextProperty); + } + + public static void SetText(FrameworkElement frameworkElement, string value) + { + frameworkElement.SetValue(TextProperty, value); + } + + public static string GetTermToBeHighlighted(FrameworkElement frameworkElement) + { + return (string) frameworkElement.GetValue(TermToBeHighlightedProperty); + } + + public static void SetTermToBeHighlighted(FrameworkElement frameworkElement, string value) + { + frameworkElement.SetValue(TermToBeHighlightedProperty, value); + } + + public static void SetHighlightForeground(FrameworkElement frameworkElement, Color? value) + { + frameworkElement.SetValue(HighlightForegroundProperty, value); + } + + public static Color? GetHighlightForeground(FrameworkElement frameworkElement) + { + return (Color?) frameworkElement.GetValue(HighlightForegroundProperty); + } + + public static void SetHighlightBackground(FrameworkElement frameworkElement, Color? value) + { + frameworkElement.SetValue(HighlightBackgroundProperty, value); + } + + public static Color? GetHighlightBackground(FrameworkElement frameworkElement) + { + return (Color?) frameworkElement.GetValue(HighlightBackgroundProperty); + } + + public static List SplitTextIntoTermAndNotTermParts(string text, string term) + { + if (string.IsNullOrEmpty(text)) + return new List {string.Empty}; + + return Regex.Split(text, $@"({Regex.Escape(term)})", RegexOptions.IgnoreCase) + .Where(p => p != string.Empty) + .ToList(); + } + + + private static void OnTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is TextBlock textBlock) + SetTextBlockTextAndHighlightTerm(textBlock, GetText(textBlock), GetTermToBeHighlighted(textBlock)); + } + + private static void SetTextBlockTextAndHighlightTerm(TextBlock textBlock, string text, string termToBeHighlighted) + { + textBlock.Text = string.Empty; + + if (TextIsEmpty(text)) + return; + + if (TextIsNotContainingTermToBeHighlighted(text, termToBeHighlighted)) + { + AddPartToTextBlock(textBlock, text); + return; + } + + List textParts = SplitTextIntoTermAndNotTermParts(text, termToBeHighlighted); + + foreach (string textPart in textParts) + AddPartToTextBlockAndHighlightIfNecessary(textBlock, termToBeHighlighted, textPart); + } + + private static bool TextIsEmpty(string text) + { + return string.IsNullOrEmpty(text); + } + + private static bool TextIsNotContainingTermToBeHighlighted(string text, string termToBeHighlighted) + { + if (text == null || termToBeHighlighted == null) + return true; + return text.Contains(termToBeHighlighted, StringComparison.OrdinalIgnoreCase) == false; + } + + private static void AddPartToTextBlockAndHighlightIfNecessary(TextBlock textBlock, string termToBeHighlighted, string textPart) + { + if (textPart.Equals(termToBeHighlighted, StringComparison.OrdinalIgnoreCase)) + AddHighlightedPartToTextBlock(textBlock, textPart); + else + AddPartToTextBlock(textBlock, textPart); + } + + private static void AddPartToTextBlock(TextBlock textBlock, string part) + { + textBlock.Inlines.Add(new Run {Text = part}); + } + + private static void AddHighlightedPartToTextBlock(TextBlock textBlock, string part) + { + Color? foreground = GetHighlightForeground(textBlock); + Color? background = GetHighlightBackground(textBlock); + + if (background == null) + { + Run run = new() {Text = part, FontWeight = FontWeights.ExtraBold}; + if (foreground != null) + run.Foreground = new SolidColorBrush(foreground.Value); + textBlock.Inlines.Add(run); + return; + } + + Border border = new() + { + Background = new SolidColorBrush(background.Value), + BorderThickness = new Thickness(0), + CornerRadius = new CornerRadius(2), + Child = new TextBlock {Text = part, FontWeight = FontWeights.Bold}, + Padding = new Thickness(1), + Margin = new Thickness(-1, -5, -1, -5) + }; + if (foreground != null) + ((TextBlock) border.Child).Foreground = new SolidColorBrush(foreground.Value); + textBlock.Inlines.Add(border); + } + } +} \ No newline at end of file diff --git a/src/Artemis.VisualScripting/Editor/Controls/VisualScriptNodeCreationBox.cs b/src/Artemis.VisualScripting/Editor/Controls/VisualScriptNodeCreationBox.cs index 496da874c..cd9950722 100644 --- a/src/Artemis.VisualScripting/Editor/Controls/VisualScriptNodeCreationBox.cs +++ b/src/Artemis.VisualScripting/Editor/Controls/VisualScriptNodeCreationBox.cs @@ -6,6 +6,7 @@ using System.Windows.Controls; using System.Windows.Data; using System.Windows.Input; using Artemis.Core; +using Artemis.VisualScripting.Editor.Controls.Wrapper; namespace Artemis.VisualScripting.Editor.Controls { @@ -37,16 +38,26 @@ namespace Artemis.VisualScripting.Editor.Controls public IEnumerable AvailableNodes { - get => (IEnumerable)GetValue(AvailableNodesProperty); + get => (IEnumerable) GetValue(AvailableNodesProperty); set => SetValue(AvailableNodesProperty, value); } + public static readonly DependencyProperty SourcePinProperty = DependencyProperty.Register( + "SourcePin", typeof(VisualScriptPin), typeof(VisualScriptNodeCreationBox), new PropertyMetadata(default(VisualScriptPin), OnSourcePinChanged)); + + public VisualScriptPin SourcePin + { + get => (VisualScriptPin) GetValue(SourcePinProperty); + set => SetValue(SourcePinProperty, value); + } + + public static readonly DependencyProperty CreateNodeCommandProperty = DependencyProperty.Register( "CreateNodeCommand", typeof(ICommand), typeof(VisualScriptNodeCreationBox), new PropertyMetadata(default(ICommand))); public ICommand CreateNodeCommand { - get => (ICommand)GetValue(CreateNodeCommandProperty); + get => (ICommand) GetValue(CreateNodeCommandProperty); set => SetValue(CreateNodeCommandProperty, value); } @@ -63,12 +74,23 @@ namespace Artemis.VisualScripting.Editor.Controls _contentList.IsSynchronizedWithCurrentItem = false; _contentList.SelectionChanged += OnContentListSelectionChanged; _contentList.SelectionMode = SelectionMode.Single; + IsVisibleChanged += OnIsVisibleChanged; _searchBox.Focus(); _contentView?.Refresh(); ItemsSourceChanged(); } + private void OnIsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e) + { + if (e.NewValue is not true) + return; + + _searchBox.Focus(); + _searchBox.SelectionStart = 0; + _searchBox.SelectionLength = _searchBox.Text.Length; + } + private void OnSearchBoxTextChanged(object sender, TextChangedEventArgs args) { _contentView?.Refresh(); @@ -86,7 +108,14 @@ namespace Artemis.VisualScripting.Editor.Controls if (_searchBox == null) return false; if (o is not NodeData nodeData) return false; - return nodeData.Name.Contains(_searchBox.Text, StringComparison.OrdinalIgnoreCase); + bool nameContains = nodeData.Name.Contains(_searchBox.Text, StringComparison.OrdinalIgnoreCase); + + if (SourcePin == null || SourcePin.Pin.Type == typeof(object)) + return nameContains; + + if (SourcePin.Pin.Direction == PinDirection.Input) + return nameContains && (nodeData.OutputType == typeof(object) || nodeData.OutputType == SourcePin.Pin.Type); + return nameContains && (nodeData.InputType == typeof(object) || nodeData.InputType == SourcePin.Pin.Type); } private void ItemsSourceChanged() @@ -100,7 +129,16 @@ namespace Artemis.VisualScripting.Editor.Controls } else { - _collectionViewSource = new CollectionViewSource { Source = AvailableNodes, SortDescriptions = { new SortDescription("Name", ListSortDirection.Ascending)}}; + _collectionViewSource = new CollectionViewSource + { + Source = AvailableNodes, + SortDescriptions = + { + new SortDescription(nameof(NodeData.Category), ListSortDirection.Ascending), + new SortDescription(nameof(NodeData.Name), ListSortDirection.Ascending) + }, + GroupDescriptions = {new PropertyGroupDescription(nameof(NodeData.Category))} + }; _contentView = _collectionViewSource.View; _contentView.Filter += Filter; } @@ -110,6 +148,8 @@ namespace Artemis.VisualScripting.Editor.Controls private static void OnItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs args) => (d as VisualScriptNodeCreationBox)?.ItemsSourceChanged(); + private static void OnSourcePinChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) => (d as VisualScriptNodeCreationBox)?._contentView.Refresh(); + #endregion } -} +} \ No newline at end of file diff --git a/src/Artemis.VisualScripting/Editor/Controls/VisualScriptPresenter.cs b/src/Artemis.VisualScripting/Editor/Controls/VisualScriptPresenter.cs index 1e3a468ad..1e75ba72e 100644 --- a/src/Artemis.VisualScripting/Editor/Controls/VisualScriptPresenter.cs +++ b/src/Artemis.VisualScripting/Editor/Controls/VisualScriptPresenter.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Linq; using System.Runtime.CompilerServices; using System.Windows; using System.Windows.Controls; @@ -65,7 +66,7 @@ namespace Artemis.VisualScripting.Editor.Controls public INodeScript Script { - get => (INodeScript)GetValue(ScriptProperty); + get => (INodeScript) GetValue(ScriptProperty); set => SetValue(ScriptProperty, value); } @@ -74,7 +75,7 @@ namespace Artemis.VisualScripting.Editor.Controls public double Scale { - get => (double)GetValue(ScaleProperty); + get => (double) GetValue(ScaleProperty); set => SetValue(ScaleProperty, value); } @@ -83,7 +84,7 @@ namespace Artemis.VisualScripting.Editor.Controls public double MinScale { - get => (double)GetValue(MinScaleProperty); + get => (double) GetValue(MinScaleProperty); set => SetValue(MinScaleProperty, value); } @@ -92,7 +93,7 @@ namespace Artemis.VisualScripting.Editor.Controls public double MaxScale { - get => (double)GetValue(MaxScaleProperty); + get => (double) GetValue(MaxScaleProperty); set => SetValue(MaxScaleProperty, value); } @@ -101,7 +102,7 @@ namespace Artemis.VisualScripting.Editor.Controls public double ScaleFactor { - get => (double)GetValue(ScaleFactorProperty); + get => (double) GetValue(ScaleFactorProperty); set => SetValue(ScaleFactorProperty, value); } @@ -110,7 +111,7 @@ namespace Artemis.VisualScripting.Editor.Controls public IEnumerable AvailableNodes { - get => (IEnumerable)GetValue(AvailableNodesProperty); + get => (IEnumerable) GetValue(AvailableNodesProperty); set => SetValue(AvailableNodesProperty, value); } @@ -119,16 +120,25 @@ namespace Artemis.VisualScripting.Editor.Controls public bool AlwaysShowValues { - get => (bool)GetValue(AlwaysShowValuesProperty); + get => (bool) GetValue(AlwaysShowValuesProperty); set => SetValue(AlwaysShowValuesProperty, value); } + public static readonly DependencyProperty SourcePinProperty = DependencyProperty.Register( + "SourcePin", typeof(VisualScriptPin), typeof(VisualScriptPresenter), new PropertyMetadata(default(VisualScriptPin))); + + public VisualScriptPin SourcePin + { + get => (VisualScriptPin) GetValue(SourcePinProperty); + set => SetValue(SourcePinProperty, value); + } + public static readonly DependencyProperty CreateNodeCommandProperty = DependencyProperty.Register( "CreateNodeCommand", typeof(ICommand), typeof(VisualScriptPresenter), new PropertyMetadata(default(ICommand))); public ICommand CreateNodeCommand { - get => (ICommand)GetValue(CreateNodeCommandProperty); + get => (ICommand) GetValue(CreateNodeCommandProperty); private set => SetValue(CreateNodeCommandProperty, value); } @@ -137,7 +147,7 @@ namespace Artemis.VisualScripting.Editor.Controls public int GridSize { - get => (int)GetValue(GridSizeProperty); + get => (int) GetValue(GridSizeProperty); set => SetValue(GridSizeProperty, value); } @@ -146,7 +156,7 @@ namespace Artemis.VisualScripting.Editor.Controls public int SurfaceSize { - get => (int)GetValue(SurfaceSizeProperty); + get => (int) GetValue(SurfaceSizeProperty); set => SetValue(SurfaceSizeProperty, value); } @@ -155,7 +165,7 @@ namespace Artemis.VisualScripting.Editor.Controls public bool AutoFitScript { - get => (bool)GetValue(AutoFitScriptProperty); + get => (bool) GetValue(AutoFitScriptProperty); set => SetValue(AutoFitScriptProperty, value); } @@ -194,7 +204,7 @@ namespace Artemis.VisualScripting.Editor.Controls _canvas.MouseMove += OnCanvasMouseMove; _canvas.MouseWheel += OnCanvasMouseWheel; _canvas.DragOver += OnCanvasDragOver; - + _canvas.Drop += OnCanvasDrop; _nodeList.ItemsSource = VisualScript?.Nodes; _cableList.ItemsSource = VisualScript?.Cables; } @@ -387,6 +397,34 @@ namespace Artemis.VisualScripting.Editor.Controls VisualScript.OnDragOver(args.GetPosition(_canvas)); } + private void OnCanvasDrop(object sender, DragEventArgs args) + { + if (!args.Data.GetDataPresent(typeof(VisualScriptPin))) return; + + VisualScriptPin sourcePin = (VisualScriptPin) args.Data.GetData(typeof(VisualScriptPin)); + if (sourcePin == null) return; + + if (_creationBoxParent.ContextMenu != null) + { + SourcePin = sourcePin; + + _lastRightClickLocation = args.GetPosition(_canvas); + _creationBoxParent.ContextMenu.IsOpen = true; + _creationBoxParent.ContextMenu.DataContext = this; + + void ContextMenuOnClosed(object s, RoutedEventArgs e) + { + SourcePin = null; + if (_creationBoxParent.ContextMenu != null) + _creationBoxParent.ContextMenu.Closed -= ContextMenuOnClosed; + } + + _creationBoxParent.ContextMenu.Closed += ContextMenuOnClosed; + } + + args.Handled = true; + } + private void OnCanvasMouseWheel(object sender, MouseWheelEventArgs args) { if (AutoFitScript) @@ -415,8 +453,8 @@ namespace Artemis.VisualScripting.Editor.Controls for (int i = 0; i < _nodeList.Items.Count; i++) { - ContentPresenter nodeControl = (ContentPresenter)_nodeList.ItemContainerGenerator.ContainerFromIndex(i); - VisualScriptNode node = (VisualScriptNode)nodeControl.Content; + ContentPresenter nodeControl = (ContentPresenter) _nodeList.ItemContainerGenerator.ContainerFromIndex(i); + VisualScriptNode node = (VisualScriptNode) nodeControl.Content; double nodeWidth = nodeControl.ActualWidth; double nodeHeight = nodeControl.ActualHeight; @@ -492,14 +530,32 @@ namespace Artemis.VisualScripting.Editor.Controls { if (nodeData == null) return; - if (_creationBoxParent.ContextMenu != null) - _creationBoxParent.ContextMenu.IsOpen = false; - INode node = nodeData.CreateNode(Script, null); node.Initialize(Script); node.X = _lastRightClickLocation.X - VisualScript.LocationOffset; node.Y = _lastRightClickLocation.Y - VisualScript.LocationOffset; + if (SourcePin != null) + { + // Connect to the first matching input or output pin + List pins = node.Pins.ToList(); + pins.AddRange(node.PinCollections.SelectMany(c => c)); + pins = pins.Where(p => p.Type == typeof(object) || p.Type == SourcePin.Pin.Type).OrderBy(p => p.Type != typeof(object)).ToList(); + + IPin preferredPin = SourcePin.Pin.Direction == PinDirection.Input + ? pins.FirstOrDefault(p => p.Direction == PinDirection.Output) + : pins.FirstOrDefault(p => p.Direction == PinDirection.Input); + + if (preferredPin != null) + { + preferredPin.ConnectTo(SourcePin.Pin); + SourcePin.Pin.ConnectTo(preferredPin); + } + } + + if (_creationBoxParent.ContextMenu != null) + _creationBoxParent.ContextMenu.IsOpen = false; + Script.AddNode(node); } @@ -549,9 +605,10 @@ namespace Artemis.VisualScripting.Editor.Controls T result = (child as T) ?? GetChildOfType(child); if (result != null) return result; } + return null; } #endregion } -} +} \ No newline at end of file diff --git a/src/Artemis.VisualScripting/Editor/Styles/VisualScriptNodeCreationBox.xaml b/src/Artemis.VisualScripting/Editor/Styles/VisualScriptNodeCreationBox.xaml index 7b5a11b17..9a38dde97 100644 --- a/src/Artemis.VisualScripting/Editor/Styles/VisualScriptNodeCreationBox.xaml +++ b/src/Artemis.VisualScripting/Editor/Styles/VisualScriptNodeCreationBox.xaml @@ -1,28 +1,35 @@  + xmlns:core="clr-namespace:Artemis.Core;assembly=Artemis.Core" + xmlns:behaviors="clr-namespace:Artemis.VisualScripting.Behaviors"> - + - - + - + BorderThickness="0" + ItemTemplate="{StaticResource TemplateNodeItem}"> + + + + + + + + + + + + + + + + + -