Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add combo box and list box controls #12

Merged
merged 2 commits into from
Mar 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/Bonsai.Gui/ComboBoxBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System.ComponentModel;

namespace Bonsai.Gui
{
/// <summary>
/// Represents an operator that interfaces with a combo box control and generates
/// a sequence of notifications whenever the selection changes.
/// </summary>
[TypeVisualizer(typeof(ComboBoxVisualizer))]
[Description("Interfaces with a combo box control and generates a sequence of notifications whenever the selection changes.")]
public class ComboBoxBuilder : ListControlBuilderBase
{
}
}
14 changes: 14 additions & 0 deletions src/Bonsai.Gui/ComboBoxDataSourceBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System.ComponentModel;

namespace Bonsai.Gui
{
/// <summary>
/// Represents an operator that interfaces with a combo box control bound to each data
/// source in the sequence and generates notifications whenever the selection changes.
/// </summary>
[TypeVisualizer(typeof(ComboBoxDataSourceVisualizer))]
[Description("Interfaces with a combo box control bound to each data source in the sequence and generates notifications whenever the selection changes.")]
public class ComboBoxDataSourceBuilder : ListControlDataSourceBuilderBase
{
}
}
30 changes: 30 additions & 0 deletions src/Bonsai.Gui/ComboBoxDataSourceVisualizer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using System;
using System.Drawing;
using System.Windows.Forms;

namespace Bonsai.Gui
{
/// <summary>
/// Provides a type visualizer representing a combo box control bound
/// to an arbitrary data source.
/// </summary>
public class ComboBoxDataSourceVisualizer : ControlVisualizerBase<ComboBox, ComboBoxDataSourceBuilder>
{
/// <inheritdoc/>
protected override ComboBox CreateControl(IServiceProvider provider, ComboBoxDataSourceBuilder builder)
{
var comboBox = new ComboBox();
comboBox.Dock = DockStyle.Fill;
comboBox.Size = new Size(300, 150);
comboBox.SubscribeTo(builder._DisplayMember, value => comboBox.DisplayMember = value);
comboBox.SubscribeTo(builder._DataSource, value => comboBox.DataSource = value);
comboBox.SelectedIndexChanged += (sender, e) =>
{
var index = comboBox.SelectedIndex;
var selectedValue = index < 0 ? null : comboBox.Items[index];
builder._SelectedItem.OnNext(selectedValue);
};
return comboBox;
}
}
}
35 changes: 35 additions & 0 deletions src/Bonsai.Gui/ComboBoxVisualizer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using System;
using System.Drawing;
using System.Windows.Forms;

namespace Bonsai.Gui
{
/// <summary>
/// Provides a type visualizer representing a combo box control.
/// </summary>
public class ComboBoxVisualizer : ControlVisualizerBase<ComboBox, ComboBoxBuilder>
{
/// <inheritdoc/>
protected override ComboBox CreateControl(IServiceProvider provider, ComboBoxBuilder builder)
{
var comboBox = new ComboBox();
comboBox.Dock = DockStyle.Fill;
comboBox.Size = new Size(300, 150);
comboBox.DataSource = builder._Items;
comboBox.SubscribeTo(builder._Items, values => comboBox.DataSource = values);
comboBox.SubscribeTo(builder._SelectedItem, value =>
{
var index = value == null ? -1 : comboBox.Items.IndexOf(value);
if (index < 0 && comboBox.Items.Count > 0) index = 0;
comboBox.SelectedIndex = index;
});
comboBox.SelectedIndexChanged += (sender, e) =>
{
var index = comboBox.SelectedIndex;
var selectedValue = index < 0 ? string.Empty : comboBox.Items[index];
builder._SelectedItem.OnNext((string)selectedValue);
};
return comboBox;
}
}
}
22 changes: 22 additions & 0 deletions src/Bonsai.Gui/DataMemberSelectorEditor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using Bonsai.Design;

namespace Bonsai.Gui
{
internal class DataMemberSelectorEditor : MemberSelectorEditor
{
public DataMemberSelectorEditor()
: base(GetDataElementType, allowMultiSelection: false)
{
}

static Type GetDataElementType(Expression expression)
{
var parameterType = expression.Type.GetGenericArguments()[0];
return ExpressionHelper.GetGenericTypeBindings(typeof(IList<>), parameterType).FirstOrDefault() ?? parameterType;
}
}
}
50 changes: 50 additions & 0 deletions src/Bonsai.Gui/DataSourceControlBuilderBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;

namespace Bonsai.Gui
{
/// <summary>
/// Provides an abstract base class for UI controls which can be bound to each
/// data source from an observable sequence.
/// </summary>
public abstract class DataSourceControlBuilderBase : ControlBuilderBase
{
static readonly Range<int> argumentRange = Range.Create(lowerBound: 1, 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.First();
var sourceType = source.Type.GetGenericArguments()[0];
var valueType = ExpressionHelper.GetGenericTypeBindings(typeof(IList<>), sourceType);
return Expression.Call(Expression.Constant(this), nameof(Generate), valueType, source);
}

/// <summary>
/// Generates an observable sequence of values containing the currently
/// selected item from the data source whenever the selection changes.
/// </summary>
/// <typeparam name="TValue">
/// The type of the values in the data source.
/// </typeparam>
/// <param name="source">
/// A sequence of collections representing the data sources to bind to
/// the UI control. Only one collection is bound at any one time.
/// </param>
/// <returns>
/// A sequence of values representing the currently selected item in
/// the UI control.
/// </returns>
protected abstract IObservable<TValue> Generate<TValue>(IObservable<IList<TValue>> source);
}
}
14 changes: 14 additions & 0 deletions src/Bonsai.Gui/ListBoxBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System.ComponentModel;

namespace Bonsai.Gui
{
/// <summary>
/// Represents an operator that interfaces with a list box control and generates
/// a sequence of notifications whenever the selection changes.
/// </summary>
[TypeVisualizer(typeof(ListBoxVisualizer))]
[Description("Interfaces with a list box control and generates a sequence of notifications whenever the selection changes.")]
public class ListBoxBuilder : ListControlBuilderBase
{
}
}
14 changes: 14 additions & 0 deletions src/Bonsai.Gui/ListBoxDataSourceBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System.ComponentModel;

namespace Bonsai.Gui
{
/// <summary>
/// Represents an operator that interfaces with a list box control bound to each data
/// source in the sequence and generates notifications whenever the selection changes.
/// </summary>
[TypeVisualizer(typeof(ListBoxDataSourceVisualizer))]
[Description("Interfaces with a list box control bound to each data source in the sequence and generates notifications whenever the selection changes.")]
public class ListBoxDataSourceBuilder : ListControlDataSourceBuilderBase
{
}
}
30 changes: 30 additions & 0 deletions src/Bonsai.Gui/ListBoxDataSourceVisualizer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using System;
using System.Drawing;
using System.Windows.Forms;

namespace Bonsai.Gui
{
/// <summary>
/// Provides a type visualizer representing a list box control bound
/// to an arbitrary data source.
/// </summary>
public class ListBoxDataSourceVisualizer : ControlVisualizerBase<ListBox, ListBoxDataSourceBuilder>
{
/// <inheritdoc/>
protected override ListBox CreateControl(IServiceProvider provider, ListBoxDataSourceBuilder builder)
{
var listBox = new ListBox();
listBox.Dock = DockStyle.Fill;
listBox.Size = new Size(300, 150);
listBox.SubscribeTo(builder._DisplayMember, value => listBox.DisplayMember = value);
listBox.SubscribeTo(builder._DataSource, value => listBox.DataSource = value);
listBox.SelectedIndexChanged += (sender, e) =>
{
var index = listBox.SelectedIndex;
var selectedValue = index < 0 ? null : listBox.Items[index];
builder._SelectedItem.OnNext(selectedValue);
};
return listBox;
}
}
}
35 changes: 35 additions & 0 deletions src/Bonsai.Gui/ListBoxVisualizer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using System;
using System.Drawing;
using System.Windows.Forms;

namespace Bonsai.Gui
{
/// <summary>
/// Provides a type visualizer representing a list box control.
/// </summary>
public class ListBoxVisualizer : ControlVisualizerBase<ListBox, ListBoxBuilder>
{
/// <inheritdoc/>
protected override ListBox CreateControl(IServiceProvider provider, ListBoxBuilder builder)
{
var listBox = new ListBox();
listBox.Dock = DockStyle.Fill;
listBox.Size = new Size(300, 150);
listBox.DataSource = builder._Items;
listBox.SubscribeTo(builder._Items, values => listBox.DataSource = values);
listBox.SubscribeTo(builder._SelectedItem, value =>
{
var index = value == null ? -1 : listBox.Items.IndexOf(value);
if (index < 0 && listBox.Items.Count > 0) index = 0;
listBox.SelectedIndex = index;
});
listBox.SelectedIndexChanged += (sender, e) =>
{
var index = listBox.SelectedIndex;
var selectedValue = index < 0 ? string.Empty : listBox.Items[index];
builder._SelectedItem.OnNext((string)selectedValue);
};
return listBox;
}
}
}
48 changes: 48 additions & 0 deletions src/Bonsai.Gui/ListControlBuilderBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Reactive.Subjects;

namespace Bonsai.Gui
{
/// <summary>
/// Provides an abstract base class for interfacing with combo box and list box controls.
/// </summary>
public abstract class ListControlBuilderBase : ControlBuilderBase<string>
{
internal readonly ObservableCollection<string> _Items = new();
internal readonly BehaviorSubject<string> _SelectedItem = new(string.Empty);

/// <summary>
/// Gets the collection of items contained in the list control.
/// </summary>
[Description("The collection of items contained in the list control.")]
public Collection<string> Items
{
get => _Items;
}

/// <summary>
/// Gets or sets the currently selected item in the list control.
/// </summary>
[Description("The currently selected item in the list control.")]
public string SelectedItem
{
get => _SelectedItem.Value;
set => _SelectedItem.OnNext(value);
}

/// <summary>
/// Generates an observable sequence of values containing the currently
/// selected item in the list control whenever the selection changes.
/// </summary>
/// <returns>
/// A sequence of <see cref="string"/> values representing the currently
/// selected item in the list control.
/// </returns>
protected override IObservable<string> Generate()
{
return _SelectedItem;
}
}
}
70 changes: 70 additions & 0 deletions src/Bonsai.Gui/ListControlDataSourceBuilderBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing.Design;
using System.Linq;
using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Reactive.Subjects;

namespace Bonsai.Gui
{
/// <summary>
/// Provides an abstract base class for interfacing with combo box and list box controls
/// bound to each data source in an observable sequence.
/// </summary>
public class ListControlDataSourceBuilderBase : DataSourceControlBuilderBase
{
internal readonly BehaviorSubject<string> _DisplayMember = new(string.Empty);
internal readonly BehaviorSubject<object> _DataSource = new(null);
internal readonly BehaviorSubject<object> _SelectedItem = new(null);

/// <summary>
/// Gets or sets the property to display for this list control.
/// </summary>
[Editor(typeof(DataMemberSelectorEditor), typeof(UITypeEditor))]
[Description("The property to display for this list control.")]
public string DisplayMember
{
get => _DisplayMember.Value;
set => _DisplayMember.OnNext(value);
}

/// <summary>
/// Generates an observable sequence of values containing the currently
/// selected item in the list control whenever the selection changes.
/// </summary>
/// <typeparam name="TValue">
/// The type of the values in the data source.
/// </typeparam>
/// <param name="source">
/// A sequence of collections representing the data sources to bind to
/// the list control. Only one collection is bound at any one time.
/// </param>
/// <returns>
/// A sequence of values representing the currently selected item in
/// the list control.
/// </returns>
protected override IObservable<TValue> Generate<TValue>(IObservable<IList<TValue>> source)
{
return Observable.Create<TValue>(observer =>
{
var sourceObserver = Observer.Create<IList<TValue>>(collection =>
{
_SelectedItem.OnNext(null);
_DataSource.OnNext(collection);
}, observer.OnError);
var selectedItem = _SelectedItem.Where(value => value is TValue).Cast<TValue>();
return new CompositeDisposable(
selectedItem.SubscribeSafe(observer),
source.SubscribeSafe(sourceObserver),
Disposable.Create(() =>
{
_DataSource.OnNext(null);
_SelectedItem.OnNext(null);
}));
});
}
}
}
Loading