port from perforce
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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(); };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user