diff --git a/src/Bonsai.Gui/ComboBoxBuilder.cs b/src/Bonsai.Gui/ComboBoxBuilder.cs new file mode 100644 index 0000000..d1dcf2c --- /dev/null +++ b/src/Bonsai.Gui/ComboBoxBuilder.cs @@ -0,0 +1,14 @@ +using System.ComponentModel; + +namespace Bonsai.Gui +{ + /// + /// Represents an operator that interfaces with a combo box control and generates + /// a sequence of notifications whenever the selection changes. + /// + [TypeVisualizer(typeof(ComboBoxVisualizer))] + [Description("Interfaces with a combo box control and generates a sequence of notifications whenever the selection changes.")] + public class ComboBoxBuilder : ListControlBuilderBase + { + } +} diff --git a/src/Bonsai.Gui/ComboBoxDataSourceBuilder.cs b/src/Bonsai.Gui/ComboBoxDataSourceBuilder.cs new file mode 100644 index 0000000..5a7e91e --- /dev/null +++ b/src/Bonsai.Gui/ComboBoxDataSourceBuilder.cs @@ -0,0 +1,14 @@ +using System.ComponentModel; + +namespace Bonsai.Gui +{ + /// + /// 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. + /// + [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 + { + } +} diff --git a/src/Bonsai.Gui/ComboBoxDataSourceVisualizer.cs b/src/Bonsai.Gui/ComboBoxDataSourceVisualizer.cs new file mode 100644 index 0000000..fd223c9 --- /dev/null +++ b/src/Bonsai.Gui/ComboBoxDataSourceVisualizer.cs @@ -0,0 +1,30 @@ +using System; +using System.Drawing; +using System.Windows.Forms; + +namespace Bonsai.Gui +{ + /// + /// Provides a type visualizer representing a combo box control bound + /// to an arbitrary data source. + /// + public class ComboBoxDataSourceVisualizer : ControlVisualizerBase + { + /// + 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; + } + } +} diff --git a/src/Bonsai.Gui/ComboBoxVisualizer.cs b/src/Bonsai.Gui/ComboBoxVisualizer.cs new file mode 100644 index 0000000..1fe8805 --- /dev/null +++ b/src/Bonsai.Gui/ComboBoxVisualizer.cs @@ -0,0 +1,35 @@ +using System; +using System.Drawing; +using System.Windows.Forms; + +namespace Bonsai.Gui +{ + /// + /// Provides a type visualizer representing a combo box control. + /// + public class ComboBoxVisualizer : ControlVisualizerBase + { + /// + 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; + } + } +} diff --git a/src/Bonsai.Gui/DataMemberSelectorEditor.cs b/src/Bonsai.Gui/DataMemberSelectorEditor.cs new file mode 100644 index 0000000..155a577 --- /dev/null +++ b/src/Bonsai.Gui/DataMemberSelectorEditor.cs @@ -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; + } + } +} diff --git a/src/Bonsai.Gui/DataSourceControlBuilderBase.cs b/src/Bonsai.Gui/DataSourceControlBuilderBase.cs new file mode 100644 index 0000000..b99feb6 --- /dev/null +++ b/src/Bonsai.Gui/DataSourceControlBuilderBase.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; + +namespace Bonsai.Gui +{ + /// + /// Provides an abstract base class for UI controls which can be bound to each + /// data source from an observable sequence. + /// + public abstract class DataSourceControlBuilderBase : ControlBuilderBase + { + static readonly Range argumentRange = Range.Create(lowerBound: 1, upperBound: 1); + + /// + /// Gets the range of input arguments that this expression builder accepts. + /// + public override Range ArgumentRange => argumentRange; + + /// + /// Builds the expression tree for configuring and calling the UI control. + /// + /// + public override Expression Build(IEnumerable 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); + } + + /// + /// Generates an observable sequence of values containing the currently + /// selected item from the data source whenever the selection changes. + /// + /// + /// The type of the values in the data source. + /// + /// + /// A sequence of collections representing the data sources to bind to + /// the UI control. Only one collection is bound at any one time. + /// + /// + /// A sequence of values representing the currently selected item in + /// the UI control. + /// + protected abstract IObservable Generate(IObservable> source); + } +} diff --git a/src/Bonsai.Gui/ListBoxBuilder.cs b/src/Bonsai.Gui/ListBoxBuilder.cs new file mode 100644 index 0000000..52cbc2c --- /dev/null +++ b/src/Bonsai.Gui/ListBoxBuilder.cs @@ -0,0 +1,14 @@ +using System.ComponentModel; + +namespace Bonsai.Gui +{ + /// + /// Represents an operator that interfaces with a list box control and generates + /// a sequence of notifications whenever the selection changes. + /// + [TypeVisualizer(typeof(ListBoxVisualizer))] + [Description("Interfaces with a list box control and generates a sequence of notifications whenever the selection changes.")] + public class ListBoxBuilder : ListControlBuilderBase + { + } +} diff --git a/src/Bonsai.Gui/ListBoxDataSourceBuilder.cs b/src/Bonsai.Gui/ListBoxDataSourceBuilder.cs new file mode 100644 index 0000000..4dae5ac --- /dev/null +++ b/src/Bonsai.Gui/ListBoxDataSourceBuilder.cs @@ -0,0 +1,14 @@ +using System.ComponentModel; + +namespace Bonsai.Gui +{ + /// + /// 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. + /// + [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 + { + } +} diff --git a/src/Bonsai.Gui/ListBoxDataSourceVisualizer.cs b/src/Bonsai.Gui/ListBoxDataSourceVisualizer.cs new file mode 100644 index 0000000..b72083e --- /dev/null +++ b/src/Bonsai.Gui/ListBoxDataSourceVisualizer.cs @@ -0,0 +1,30 @@ +using System; +using System.Drawing; +using System.Windows.Forms; + +namespace Bonsai.Gui +{ + /// + /// Provides a type visualizer representing a list box control bound + /// to an arbitrary data source. + /// + public class ListBoxDataSourceVisualizer : ControlVisualizerBase + { + /// + 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; + } + } +} diff --git a/src/Bonsai.Gui/ListBoxVisualizer.cs b/src/Bonsai.Gui/ListBoxVisualizer.cs new file mode 100644 index 0000000..67442ff --- /dev/null +++ b/src/Bonsai.Gui/ListBoxVisualizer.cs @@ -0,0 +1,35 @@ +using System; +using System.Drawing; +using System.Windows.Forms; + +namespace Bonsai.Gui +{ + /// + /// Provides a type visualizer representing a list box control. + /// + public class ListBoxVisualizer : ControlVisualizerBase + { + /// + 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; + } + } +} diff --git a/src/Bonsai.Gui/ListControlBuilderBase.cs b/src/Bonsai.Gui/ListControlBuilderBase.cs new file mode 100644 index 0000000..eeeb2f1 --- /dev/null +++ b/src/Bonsai.Gui/ListControlBuilderBase.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Reactive.Subjects; + +namespace Bonsai.Gui +{ + /// + /// Provides an abstract base class for interfacing with combo box and list box controls. + /// + public abstract class ListControlBuilderBase : ControlBuilderBase + { + internal readonly ObservableCollection _Items = new(); + internal readonly BehaviorSubject _SelectedItem = new(string.Empty); + + /// + /// Gets the collection of items contained in the list control. + /// + [Description("The collection of items contained in the list control.")] + public Collection Items + { + get => _Items; + } + + /// + /// Gets or sets the currently selected item in the list control. + /// + [Description("The currently selected item in the list control.")] + public string SelectedItem + { + get => _SelectedItem.Value; + set => _SelectedItem.OnNext(value); + } + + /// + /// Generates an observable sequence of values containing the currently + /// selected item in the list control whenever the selection changes. + /// + /// + /// A sequence of values representing the currently + /// selected item in the list control. + /// + protected override IObservable Generate() + { + return _SelectedItem; + } + } +} diff --git a/src/Bonsai.Gui/ListControlDataSourceBuilderBase.cs b/src/Bonsai.Gui/ListControlDataSourceBuilderBase.cs new file mode 100644 index 0000000..1fd3622 --- /dev/null +++ b/src/Bonsai.Gui/ListControlDataSourceBuilderBase.cs @@ -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 +{ + /// + /// Provides an abstract base class for interfacing with combo box and list box controls + /// bound to each data source in an observable sequence. + /// + public class ListControlDataSourceBuilderBase : DataSourceControlBuilderBase + { + internal readonly BehaviorSubject _DisplayMember = new(string.Empty); + internal readonly BehaviorSubject _DataSource = new(null); + internal readonly BehaviorSubject _SelectedItem = new(null); + + /// + /// Gets or sets the property to display for this list control. + /// + [Editor(typeof(DataMemberSelectorEditor), typeof(UITypeEditor))] + [Description("The property to display for this list control.")] + public string DisplayMember + { + get => _DisplayMember.Value; + set => _DisplayMember.OnNext(value); + } + + /// + /// Generates an observable sequence of values containing the currently + /// selected item in the list control whenever the selection changes. + /// + /// + /// The type of the values in the data source. + /// + /// + /// A sequence of collections representing the data sources to bind to + /// the list control. Only one collection is bound at any one time. + /// + /// + /// A sequence of values representing the currently selected item in + /// the list control. + /// + protected override IObservable Generate(IObservable> source) + { + return Observable.Create(observer => + { + var sourceObserver = Observer.Create>(collection => + { + _SelectedItem.OnNext(null); + _DataSource.OnNext(collection); + }, observer.OnError); + var selectedItem = _SelectedItem.Where(value => value is TValue).Cast(); + return new CompositeDisposable( + selectedItem.SubscribeSafe(observer), + source.SubscribeSafe(sourceObserver), + Disposable.Create(() => + { + _DataSource.OnNext(null); + _SelectedItem.OnNext(null); + })); + }); + } + } +}