Skip to content

Commit

Permalink
Merge pull request #4 from bonsai-rx/feature-dev
Browse files Browse the repository at this point in the history
Add core infrastructure for control visualizers
  • Loading branch information
glopesdev authored Feb 4, 2024
2 parents 8d26f5a + 45ddaab commit 4f4c875
Show file tree
Hide file tree
Showing 7 changed files with 550 additions and 171 deletions.
92 changes: 92 additions & 0 deletions src/Bonsai.Gui/ContainerControlVisualizerBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
using System;
using System.Drawing;
using System.Windows.Forms;
using Bonsai.Design;
using Bonsai.Expressions;

namespace Bonsai.Gui
{
/// <summary>
/// Provides an abstract base class for visualizers representing UI elements which can
/// contain other nested UI elements.
/// </summary>
/// <typeparam name="TControl">
/// The type of the container control exposed by this type visualizer.
/// </typeparam>
/// <typeparam name="TControlBuilder">
/// The type of the workflow element used to create the container control.
/// </typeparam>
public abstract class ContainerControlVisualizerBase<TControl, TControlBuilder>
: MashupControlVisualizerBase<TControl, TControlBuilder>
where TControl : Control
where TControlBuilder : ExpressionBuilder
{
/// <summary>
/// Adds a control to the container at the specified index.
/// </summary>
/// <param name="index">The index of the control, following the order of visualizer sources.</param>
/// <param name="control">The control to add to the container.</param>
protected abstract void AddControl(int index, Control control);

/// <inheritdoc/>
public override void LoadMashups(IServiceProvider provider)
{
for (int i = 0; i < MashupSources.Count; i++)
{
var mashupSource = MashupSources[i];
var containerProvider = new ContainerControlServiceProvider(i, this, mashupSource.Source, provider);
mashupSource.Visualizer.Load(containerProvider);
}
}

/// <inheritdoc/>
public override void UnloadMashups()
{
base.UnloadMashups();
Control.Controls.Clear();
}

/// <inheritdoc/>
public override MashupSource GetMashupSource(int x, int y)
{
if (Control == null) return null;
var panelPoint = Control.PointToClient(new Point(x, y));
var childControl = Control.GetChildAtPoint(panelPoint);
if (childControl != null)
{
var index = Control.Controls.GetChildIndex(childControl);
return MashupSources[index];
}

return null;
}

class ContainerControlServiceProvider : MashupControlServiceProvider, IDialogTypeVisualizerService
{
public ContainerControlServiceProvider(
int index,
ContainerControlVisualizerBase<TControl, TControlBuilder> visualizer,
InspectBuilder source,
IServiceProvider provider)
: base(index, visualizer, source, provider)
{
}

public void AddControl(Control control)
{
var container = (ContainerControlVisualizerBase<TControl, TControlBuilder>)Visualizer;
container.AddControl(Index, control);
}

public override object GetService(Type serviceType)
{
if (serviceType == typeof(IDialogTypeVisualizerService))
{
return this;
}

return base.GetService(serviceType);
}
}
}
}
122 changes: 122 additions & 0 deletions src/Bonsai.Gui/ControlBuilderBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq.Expressions;
using Bonsai.Expressions;
using System.Reactive.Subjects;
using System.Reactive.Linq;
using System.Reactive;
using System.Linq;

namespace Bonsai.Gui
{
/// <summary>
/// Provides an abstract base class with common UI control functionality.
/// </summary>
public abstract class ControlBuilderBase : ZeroArgumentExpressionBuilder, INamedElement
{
internal readonly BehaviorSubject<bool> _Enabled = new(true);
internal readonly BehaviorSubject<bool> _Visible = new(true);

/// <summary>
/// Gets or sets the name of the control.
/// </summary>
[Category(nameof(CategoryAttribute.Design))]
[Description("The name of the control.")]
public string Name { get; set; }

/// <summary>
/// Gets or sets a value specifying whether the control can respond to user interaction.
/// </summary>
[Category(nameof(CategoryAttribute.Behavior))]
[Description("Specifies whether the control can respond to user interaction.")]
public bool Enabled
{
get => _Enabled.Value;
set => _Enabled.OnNext(value);
}

/// <summary>
/// Gets or sets a value specifying whether the control and all its child controls
/// are displayed.
/// </summary>
[Category(nameof(CategoryAttribute.Behavior))]
[Description("Specifies whether the control and all its child controls are displayed.")]
public bool Visible
{
get => _Visible.Value;
set => _Visible.OnNext(value);
}

/// <summary>
/// Builds the expression tree for configuring and calling the UI control.
/// </summary>
/// <inheritdoc/>
public override Expression Build(IEnumerable<Expression> arguments)
{
return Expression.Call(typeof(Observable), nameof(Observable.Never), new[] { typeof(Unit) });
}
}

/// <summary>
/// Provides an abstract base class with common UI event source control functionality.
/// </summary>
/// <typeparam name="TEvent">The type of event notifications emitted by the UI control.</typeparam>
public abstract class ControlBuilderBase<TEvent> : ControlBuilderBase, INamedElement
{
/// <summary>
/// Builds the expression tree for configuring and calling the UI control.
/// </summary>
/// <inheritdoc/>
public override Expression Build(IEnumerable<Expression> arguments)
{
return Expression.Call(Expression.Constant(this), nameof(Generate), null);
}

/// <summary>
/// Generates an observable sequence of event notifications from the UI control.
/// </summary>
/// <returns>
/// An observable sequence of events of type <typeparamref name="TEvent"/>.
/// </returns>
protected abstract IObservable<TEvent> Generate();
}

/// <summary>
/// Provides an abstract base class with common functionality for UI controls that
/// accept an optional sequence of command notifications.
/// </summary>
/// <typeparam name="TCommand">The type of command notifications accepted by the UI control.</typeparam>
/// <typeparam name="TEvent">The type of event notifications emitted by the UI control.</typeparam>
public abstract class ControlBuilderBase<TCommand, TEvent> : ControlBuilderBase<TEvent>, INamedElement
{
static readonly Range<int> argumentRange = Range.Create(lowerBound: 0, upperBound: 1);

/// <summary>
/// Gets the range of input arguments that this expression builder accepts.
/// </summary>
public override Range<int> ArgumentRange => argumentRange;

/// <summary>
/// Builds the expression tree for configuring and calling the UI control.
/// </summary>
/// <inheritdoc/>
public override Expression Build(IEnumerable<Expression> arguments)
{
var source = arguments.SingleOrDefault();
var commands = source == null ? Array.Empty<Expression>() : new[] { source };
return Expression.Call(Expression.Constant(this), nameof(Generate), null, commands);
}

/// <summary>
/// Generates an observable sequence of event notifications from the UI control.
/// </summary>
/// <param name="source">
/// An observable sequence of commands of type <typeparamref name="TCommand"/>.
/// </param>
/// <returns>
/// An observable sequence of events of type <typeparamref name="TEvent"/>.
/// </returns>
protected abstract IObservable<TEvent> Generate(IObservable<TCommand> source);
}
}
89 changes: 89 additions & 0 deletions src/Bonsai.Gui/ControlExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
using System;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Linq;
using System.Reactive.Linq;
using System.Windows.Forms;
using Bonsai.Design;
using Bonsai.Expressions;

namespace Bonsai.Gui
{
/// <summary>
/// Provides a set of static methods for subscribing delegates to
/// observable sequences of event notifications.
/// </summary>
public static class ControlExtensions
{
/// <summary>
/// Subscribes the control to change notifications in the specified
/// observable collection.
/// </summary>
/// <typeparam name="TSource">
/// The type of the elements in the <paramref name="source"/> collection.
/// </typeparam>
/// <param name="control">The control on which to observe notifications.</param>
/// <param name="source">
/// A data collection that provides notifications when items are added,
/// removed, or when the whole list is refreshed.
/// </param>
/// <param name="action">
/// The action to invoke on each new version of the observable collection.
/// </param>
/// <returns>
/// A disposable object used to unsubscribe from the observable sequence.
/// </returns>
public static IDisposable SubscribeTo<TSource>(
this Control control,
ObservableCollection<TSource> source,
Action<TSource[]> action)
{
var collectionChanged = Observable.FromEventPattern<NotifyCollectionChangedEventHandler, NotifyCollectionChangedEventArgs>(
handler => source.CollectionChanged += handler,
handler => source.CollectionChanged -= handler)
.Select(evt => source.ToArray());
return SubscribeTo(control, collectionChanged, action);
}

/// <summary>
/// Subscribes the control to event notifications from the specified
/// observable sequence.
/// </summary>
/// <typeparam name="TSource">
/// The type of the elements in the <paramref name="source"/> sequence.
/// </typeparam>
/// <param name="control">The control on which to observe notifications.</param>
/// <param name="source">The observable sequence of event notifications.</param>
/// <param name="action">The action to invoke on each event notification.</param>
/// <returns>
/// A disposable object used to unsubscribe from the observable sequence.
/// </returns>
public static IDisposable SubscribeTo<TSource>(this Control control, IObservable<TSource> source, Action<TSource> action)
{
var handleCreated = Observable.FromEventPattern<EventHandler, EventArgs>(
handler => control.HandleCreated += handler,
handler => control.HandleCreated -= handler);
var handleDestroyed = Observable.FromEventPattern<EventHandler, EventArgs>(
handler => control.HandleDestroyed += handler,
handler => control.HandleDestroyed -= handler);
var notifications = handleCreated
.SelectMany(_ => source.ObserveOn(control))
.TakeUntil(handleDestroyed);
return notifications.Subscribe(action);
}

internal static void SubscribeTo(this Control control, ExpressionBuilder builder)
{
if (string.IsNullOrEmpty(control.Name))
{
control.Name = ExpressionBuilder.GetElementDisplayName(builder);
}

if (builder is ControlBuilderBase controlBuilder)
{
control.SubscribeTo(controlBuilder._Enabled, value => control.Enabled = value);
control.SubscribeTo(controlBuilder._Visible, value => control.Visible = value);
}
}
}
}
67 changes: 67 additions & 0 deletions src/Bonsai.Gui/ControlVisualizerBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
using System;
using System.Windows.Forms;
using System.Xml.Serialization;
using Bonsai.Design;
using Bonsai.Expressions;

namespace Bonsai.Gui
{
/// <summary>
/// Provides an abstract base class for visualizers representing individual UI elements.
/// </summary>
/// <typeparam name="TControl">The type of the control exposed by this type visualizer.</typeparam>
/// <typeparam name="TControlBuilder">The type of the workflow element used to create the control.</typeparam>
public abstract class ControlVisualizerBase<TControl, TControlBuilder>
: DialogTypeVisualizer
where TControl : Control
where TControlBuilder : ExpressionBuilder
{
/// <summary>
/// Gets the active control exposed by this type visualizer.
/// </summary>
[XmlIgnore]
public TControl Control { get; private set; }

/// <summary>
/// Creates and configures the visual control associated with
/// the specified workflow operator.
/// </summary>
/// <param name="provider">
/// A service provider object which can be used to obtain visualization,
/// runtime inspection, or other editing services.
/// </param>
/// <param name="builder">
/// The <see cref="ExpressionBuilder"/> object used to provide configuration
/// properties to the control.
/// </param>
/// <returns>
/// A new instance of the control class associated with the specified
/// workflow operator.
/// </returns>
protected abstract TControl CreateControl(IServiceProvider provider, TControlBuilder builder);

/// <inheritdoc/>
public override void Load(IServiceProvider provider)
{
var context = (ITypeVisualizerContext)provider.GetService(typeof(ITypeVisualizerContext));
var controlBuilder = (TControlBuilder)ExpressionBuilder.GetVisualizerElement(context.Source).Builder;
Control = CreateControl(provider, controlBuilder);
Control.SubscribeTo(controlBuilder);

var visualizerService = (IDialogTypeVisualizerService)provider.GetService(typeof(IDialogTypeVisualizerService));
visualizerService?.AddControl(Control);
}

/// <inheritdoc/>
public override void Show(object value)
{
}

/// <inheritdoc/>
public override void Unload()
{
Control.Dispose();
Control = null;
}
}
}
Loading

0 comments on commit 4f4c875

Please sign in to comment.