port from perforce

This commit is contained in:
2026-04-18 22:31:51 +02:00
commit 8d0ab5b7cc
8409 changed files with 3972376 additions and 0 deletions

View File

@@ -0,0 +1,249 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive;
using System.Text;
using System.Windows;
using DynamicData;
using NodeNetwork.ViewModels;
using ReactiveUI;
namespace NodeNetwork.Toolkit.ContextMenu
{
/// <summary>
/// A viewmodel for a context menu that allows users to add nodes to a network.
/// </summary>
public class AddNodeContextMenuViewModel : SearchableContextMenuViewModel
{
static AddNodeContextMenuViewModel()
{
NNViewRegistrar.AddRegistration(() => new SearchableContextMenuView(), typeof(IViewFor<AddNodeContextMenuViewModel>));
}
#region Network
/// <summary>
/// The network to which the nodes are to be added.
/// </summary>
public NetworkViewModel Network
{
get => _network;
set => this.RaiseAndSetIfChanged(ref _network, value);
}
private NetworkViewModel _network;
#endregion
/// <summary>
/// The format that is used to create labels for the menu entries based on the node name.
/// E.g. "Add {0}"
/// </summary>
public string LabelFormat { get; }
/// <summary>
/// When adding a node to the network,
/// this function is used to determine the position at which it is placed.
/// </summary>
public Func<NodeViewModel, Point> NodePositionFunc { get; set; } = (node) => new Point();
/// <summary>
/// A callback that is called after a node is added to the network through this menu.
/// </summary>
public Action<NodeViewModel> OnNodeAdded { get; set; } = node => { };
/// <summary>
/// An interaction that is used to open contextmenu views given a SearchableContextMenuViewModel.
/// Used in ShowAddNodeForPendingConnectionMenu to display this menu, and a menu for choosing an endpoint.
/// </summary>
public Interaction<SearchableContextMenuViewModel, Unit> OpenContextMenu { get; } = new Interaction<SearchableContextMenuViewModel, Unit>();
private ReactiveCommand<NodeTemplate, Unit> CreateNode { get; }
public AddNodeContextMenuViewModel(string labelFormat = "{0}")
{
LabelFormat = labelFormat;
CreateNode = ReactiveCommand.Create<NodeTemplate, Unit>((template) =>
{
var nodeInstance = template.Factory();
Network.Nodes.Add(nodeInstance);
nodeInstance.Position = NodePositionFunc(nodeInstance);
OnNodeAdded(nodeInstance);
return Unit.Default;
});
}
/// <summary>
/// Adds a new node type to the list.
/// Every time a node is added to a network from this list, the factory function in the template
/// will be called to create a new instance of the viewmodel type.
/// </summary>
/// <param name="template">The template with the node type to add.</param>
public void AddNodeType(NodeTemplate template)
{
Commands.Add(new LabeledCommand
{
Label = string.Format(LabelFormat, template.Instance.Name),
Command = CreateNode,
CommandParameter = template
});
}
public void AddNodeTypes(IEnumerable<NodeTemplate> templates)
{
foreach (var nodeTemplate in templates)
{
AddNodeType(nodeTemplate);
}
}
private void ShowOnlyConnectableNodes(PendingConnectionViewModel testCon)
{
foreach (var cmd in Commands.Items)
{
var curNodeTemplate = (NodeTemplate)cmd.CommandParameter;
bool hasValidEndpoint =
testCon.InputIsLocked ?
GetConnectableOutputs(curNodeTemplate.Instance, testCon).Any() :
GetConnectableInputs(curNodeTemplate.Instance, testCon).Any();
cmd.Visible = hasValidEndpoint;
}
}
public void ShowAddNodeForPendingConnectionMenu(PendingConnectionViewModel pendingCon)
{
var testCon = new PendingConnectionViewModel(pendingCon.Parent) // Copy used to test which inputs/outputs will work with the pending connection
{
Input = pendingCon.Input,
InputIsLocked = pendingCon.InputIsLocked,
Output = pendingCon.Output,
OutputIsLocked = pendingCon.OutputIsLocked
};
ShowOnlyConnectableNodes(testCon);
// After a node type is chosen, pick an endpoint
OnNodeAdded = node =>
{
if (testCon.InputIsLocked)
{
var outputs = GetConnectableOutputs(node, testCon).ToList();
if (outputs.Count == 1)
{
// If only 1 output matches, select this one
Network.Connections.Add(Network.ConnectionFactory(pendingCon.Input, outputs[0]));
Network.RemovePendingConnection();
}
else
{
// Open a menu to let the user choose the desired output to connect to
var chooseEndpointVM = new SearchableContextMenuViewModel();
var cmd = ReactiveCommand.Create<NodeOutputViewModel, Unit>((o) =>
{
Network.Connections.Add(Network.ConnectionFactory(pendingCon.Input, o));
Network.RemovePendingConnection();
return Unit.Default;
});
foreach (var output in outputs)
{
chooseEndpointVM.Commands.Add(new LabeledCommand
{
Command = cmd,
CommandParameter = output,
Label = output.Name
});
}
OpenContextMenu.Handle(chooseEndpointVM).Subscribe();
}
}
else
{
var inputs = GetConnectableInputs(node, testCon).ToList();
if (inputs.Count == 1)
{
Network.Connections.Add(Network.ConnectionFactory(inputs[0], pendingCon.Output));
Network.RemovePendingConnection();
}
else
{
var chooseEndpointVM = new SearchableContextMenuViewModel();
var cmd = ReactiveCommand.Create<NodeInputViewModel, Unit>((i) =>
{
Network.Connections.Add(Network.ConnectionFactory(i, pendingCon.Output));
Network.RemovePendingConnection();
return Unit.Default;
});
foreach (var input in inputs)
{
chooseEndpointVM.Commands.Add(new LabeledCommand
{
Command = cmd,
CommandParameter = input,
Label = input.Name
});
}
OpenContextMenu.Handle(chooseEndpointVM).Subscribe();
}
}
};
OpenContextMenu.Handle(this).Subscribe();
}
/// <summary>
/// Given a set of node templates, return those which have an endpoint
/// that could be connected to the specified pending connection.
/// </summary>
public static IEnumerable<NodeTemplate> GetConnectableNodes(IEnumerable<NodeTemplate> candidateNodeTemplates, PendingConnectionViewModel testCon)
{
foreach (var curNode in candidateNodeTemplates)
{
bool hasValidEndpoint =
testCon.InputIsLocked ?
GetConnectableOutputs(curNode.Instance, testCon).Any() :
GetConnectableInputs(curNode.Instance, testCon).Any();
if (hasValidEndpoint)
{
yield return curNode;
}
}
}
/// <summary>
/// Given a node viewmodel, return the outputs which could be connected to the pending connection.
/// Assumes testCon.Input is set.
/// </summary>
public static IEnumerable<NodeOutputViewModel> GetConnectableOutputs(NodeViewModel node, PendingConnectionViewModel testCon)
{
var validator = testCon.Input.ConnectionValidator;
foreach (var curOutput in node.Outputs.Items)
{
testCon.Output = curOutput;
if (curOutput.MaxConnections > 0 && validator(testCon).IsValid)
{
yield return curOutput;
}
}
}
/// <summary>
/// Given a node viewmodel, return the inputs which could be connected to the pending connection.
/// Assumes testCon.Output is set.
/// </summary>
public static IEnumerable<NodeInputViewModel> GetConnectableInputs(NodeViewModel node, PendingConnectionViewModel testCon)
{
foreach (var curInput in node.Inputs.Items)
{
var validator = curInput.ConnectionValidator;
testCon.Input = curInput;
if (curInput.MaxConnections > 0 && validator(testCon).IsValid)
{
yield return curInput;
}
}
}
}
}

View File

@@ -0,0 +1,33 @@
<ContextMenu x:Class="NodeNetwork.Toolkit.ContextMenu.SearchableContextMenuView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:NodeNetwork.Toolkit.ContextMenu"
xmlns:viewModels="clr-namespace:NodeNetwork.ViewModels;assembly=NodeNetwork"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800" x:Name="self">
<ContextMenu.Resources>
<Style TargetType="{x:Type MenuItem}">
<Style.Setters>
<Setter Property="Command" Value="{Binding Command}"/>
<Setter Property="CommandParameter" Value="{Binding CommandParameter}"/>
</Style.Setters>
</Style>
<DataTemplate DataType="{x:Type local:LabeledCommand}">
<TextBlock><Run Text="{Binding Label}"/></TextBlock>
</DataTemplate>
</ContextMenu.Resources>
<ContextMenu.ItemsSource>
<CompositeCollection>
<MenuItem x:Name="SearchMenuItem" StaysOpenOnClick="True">
<MenuItem.Header>
<TextBox x:Name="SearchTextBox" MinWidth="150"></TextBox>
</MenuItem.Header>
</MenuItem>
<CollectionContainer x:Name="CollectionContainer"/>
<Separator/>
<CollectionContainer x:Name="ContainerBelowSearch"/>
</CompositeCollection>
</ContextMenu.ItemsSource>
</ContextMenu>

View File

@@ -0,0 +1,134 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using DynamicData;
using NodeNetwork.Utilities;
using ReactiveUI;
namespace NodeNetwork.Toolkit.ContextMenu
{
public partial class SearchableContextMenuView : IViewFor<SearchableContextMenuViewModel>
{
#region ViewModel
public static readonly DependencyProperty ViewModelProperty = DependencyProperty.Register(nameof(ViewModel),
typeof(SearchableContextMenuViewModel), typeof(SearchableContextMenuView), new PropertyMetadata(null));
public SearchableContextMenuViewModel ViewModel
{
get => (SearchableContextMenuViewModel)GetValue(ViewModelProperty);
set => SetValue(ViewModelProperty, value);
}
object IViewFor.ViewModel
{
get => ViewModel;
set => ViewModel = (SearchableContextMenuViewModel)value;
}
#endregion
#region ChildrenBelowSearch
public static readonly DependencyProperty ChildrenBelowSearchProperty =
DependencyProperty.Register(nameof(ChildrenBelowSearch), typeof(IEnumerable), typeof(SearchableContextMenuView), new PropertyMetadata(new object[0]));
public IEnumerable ChildrenBelowSearch
{
get => (IEnumerable)GetValue(ChildrenBelowSearchProperty);
set => SetValue(ChildrenBelowSearchProperty, value);
}
#endregion
#region ReferencePointElement
public static readonly DependencyProperty ReferencePointElementProperty =
DependencyProperty.Register(nameof(ReferencePointElement), typeof(IInputElement), typeof(SearchableContextMenuView), new PropertyMetadata(null));
public IInputElement ReferencePointElement
{
get => (IInputElement)GetValue(ReferencePointElementProperty);
set => SetValue(ReferencePointElementProperty, value);
}
#endregion
#region OpenPoint
public static readonly DependencyProperty OpenPointProperty =
DependencyProperty.Register(nameof(OpenPoint), typeof(Point), typeof(SearchableContextMenuView), new PropertyMetadata(new Point()));
public Point OpenPoint
{
get => (Point)GetValue(OpenPointProperty);
private set => SetValue(OpenPointProperty, value);
}
#endregion
public SearchableContextMenuView()
{
InitializeComponent();
this.Bind(ViewModel, vm => vm.SearchQuery, v => v.SearchTextBox.Text);
this.BindList(ViewModel, vm => vm.VisibleCommands, v => v.CollectionContainer.Collection);
Binding myBinding = new Binding(nameof(ChildrenBelowSearch)) { Source = this };
BindingOperations.SetBinding(ContainerBelowSearch, CollectionContainer.CollectionProperty, myBinding);
this.Opened += (sender, args) =>
{
SearchTextBox.Focus();
if (ReferencePointElement != null)
{
OpenPoint = Mouse.GetPosition(ReferencePointElement);
}
};
// This var is needed to ensure both key down and key up of arrow keys happened in the textbox,
// otherwise moving into the textbox will immediately move out again.
bool arrowWasPressedInTextBox = false;
this.SearchTextBox.PreviewKeyDown += (sender, args) =>
{
if (args.Key == Key.Enter || args.Key == Key.Return)
{
if (ViewModel.VisibleCommands.Count > 0)
{
var firstEntry = ViewModel.VisibleCommands.Items.First();
firstEntry.Command.Execute(firstEntry.CommandParameter);
this.IsOpen = false;
}
}
else if (args.Key == Key.Escape && SearchTextBox.Text.Length > 0)
{
SearchTextBox.Text = "";
args.Handled = true;
}
else if (args.Key == Key.Up || args.Key == Key.Down)
{
arrowWasPressedInTextBox = true;
}
};
this.SearchTextBox.PreviewKeyUp += (sender, args) =>
{
if (arrowWasPressedInTextBox && (args.Key == Key.Up || args.Key == Key.Down))
{
arrowWasPressedInTextBox = false;
var dir = args.Key == Key.Up ? FocusNavigationDirection.Previous : FocusNavigationDirection.Next;
var traversalRequest = new TraversalRequest(dir);
var focusedElem = Keyboard.FocusedElement as FrameworkElement;
focusedElem?.MoveFocus(traversalRequest);
}
};
this.SearchMenuItem.GotKeyboardFocus += (sender, args) => { SearchTextBox.Focus(); };
}
}
}

View File

@@ -0,0 +1,126 @@
using DynamicData;
using ReactiveUI;
using System;
using System.Linq;
using System.Reactive.Linq;
using System.Windows.Input;
namespace NodeNetwork.Toolkit.ContextMenu
{
/// <summary>
/// A data type containing a command, parameter and display properties.
/// </summary>
public class LabeledCommand : ReactiveObject
{
#region Label
/// <summary>
/// The label that is displayed in the menu
/// </summary>
public string Label
{
get => _label;
set => this.RaiseAndSetIfChanged(ref _label, value);
}
private string _label = "";
#endregion
#region Visible
/// <summary>
/// Should the command be displayed in the menu?
/// </summary>
public bool Visible
{
get => _visible;
set => this.RaiseAndSetIfChanged(ref _visible, value);
}
private bool _visible = true;
#endregion
#region Command
/// <summary>
/// The command to be executed.
/// </summary>
public ICommand Command
{
get => _command;
set => this.RaiseAndSetIfChanged(ref _command, value);
}
private ICommand _command = null;
#endregion
#region CommandParameter
/// <summary>
/// The parameter to be passed to the command on execution.
/// </summary>
public object CommandParameter
{
get => _commandParameter;
set => this.RaiseAndSetIfChanged(ref _commandParameter, value);
}
private object _commandParameter = null;
#endregion
}
/// <summary>
/// A viewmodel for a context menu in which the entries can be filtered by the user based on a searchquery.
/// </summary>
public class SearchableContextMenuViewModel : ReactiveObject
{
static SearchableContextMenuViewModel()
{
NNViewRegistrar.AddRegistration(() => new SearchableContextMenuView(), typeof(IViewFor<SearchableContextMenuViewModel>));
}
/// <summary>
/// List of all the available commands in the menu.
/// </summary>
public ISourceList<LabeledCommand> Commands { get; } = new SourceList<LabeledCommand>();
/// <summary>
/// List of commands that are actually visible in the menu.
/// This list is based on Commands and SearchQuery.
/// </summary>
public IObservableList<LabeledCommand> VisibleCommands { get; }
#region SearchQuery
/// <summary>
/// The current search string that is used to filter Nodes into VisibleNodes.
/// </summary>
public string SearchQuery
{
get => _searchQuery;
set => this.RaiseAndSetIfChanged(ref _searchQuery, value);
}
private string _searchQuery = "";
#endregion
#region MaxItemsDisplayed
/// <summary>
/// Only the first MaxItemsDisplayed items from Commands that match the query are displayed.
/// </summary>
public int MaxItemsDisplayed
{
get => _maxItemsDisplayed;
set => this.RaiseAndSetIfChanged(ref _maxItemsDisplayed, value);
}
private int _maxItemsDisplayed = int.MaxValue;
#endregion
public SearchableContextMenuViewModel()
{
var onQueryChanged =
this.WhenAnyValue(vm => vm.SearchQuery, vm => vm.MaxItemsDisplayed)
.Throttle(TimeSpan.FromMilliseconds(70), RxApp.MainThreadScheduler)
.Publish();
onQueryChanged.Connect();
VisibleCommands = Commands.Connect()
.AutoRefreshOnObservable(_ => onQueryChanged)
.AutoRefresh(cmd => cmd.Label)
.AutoRefresh(cmd => cmd.Visible)
.Filter(cmd => cmd.Visible && (cmd.Label ?? "").ToUpper().Contains(SearchQuery?.ToUpper() ?? ""))
.Top(MaxItemsDisplayed)
.AsObservableList();
}
}
}