diff --git a/src/client/spark/Modules/Market/MarketView/MarketViewCell.cs b/src/client/spark/Modules/Market/MarketView/MarketViewCell.cs
new file mode 100644
index 000000000..bcb66e60e
--- /dev/null
+++ b/src/client/spark/Modules/Market/MarketView/MarketViewCell.cs
@@ -0,0 +1,13 @@
+using mbts.spark;
+
+namespace mbts.ui.spark {
+ readonly struct MarketViewCell {
+ public readonly MarketViewVM.Row row;
+ public readonly MarketViewView.Columns column;
+
+ public MarketViewCell(MarketViewVM.Row row, MarketViewView.Columns column) {
+ this.row = row;
+ this.column = column;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/client/spark/Modules/Market/MarketView/MarketViewVM.cs b/src/client/spark/Modules/Market/MarketView/MarketViewVM.cs
index d69e28280..3a74fcfc2 100644
--- a/src/client/spark/Modules/Market/MarketView/MarketViewVM.cs
+++ b/src/client/spark/Modules/Market/MarketView/MarketViewVM.cs
@@ -1,11 +1,14 @@
+using bts;
using bts.api;
using bts.api.core;
+using bts.api.orderman;
using bts.collections;
using bts.utils;
using mbts.persist;
using mbts.spark.filters;
using mbts.ui;
using mbts.ui.controls;
+using mbts.ui.converters;
using mbts.ui.settings;
using mbts.ui.shell;
using mbts.ui.utility;
@@ -15,17 +18,22 @@ using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
+using System.Diagnostics;
using System.Linq;
using System.Reactive.Linq;
+using System.Text.RegularExpressions;
+using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
-using System.Windows.Input;
using System.Windows.Media;
using static bts.shell.MarketViewWindowFactory;
using static mbts.ui.controls.DIYGrid;
using static mbts.ui.spark.MarketViewView;
namespace mbts.spark {
+ ///
+ /// Viewmodel class designed for
+ ///
partial class MarketViewVM : DisposableViewModelBase, IHandleRowSelection {
static readonly IReadOnlyList>> _filter_getters =
ObjectSnapshot.property_delegates_of_type>().ToReadOnlyList();
@@ -43,16 +51,30 @@ namespace mbts.spark {
readonly DeferrableActionWrapper _refresh_filter;
Func _filter;
IReadOnlyList<(Columns key, ListSortDirection direction)> _sorting_hierarchy;
- IWindowSettingsManager _settings_manager;
+ Color _selection_highlight;
- public MarketViewVM(IBus bus, OrdersModel orders_model, IObservable state, IWindowSettingsManager settings_manager) {
+ ///
+ /// Initializes an instance of the class.
+ ///
+ ///
+ ///
+ ///
+ /// Indicates whether or not hyperlink cells should use visual states to animate mouse over (seems to be slower than style triggers, should investigate to be certain).
+ public MarketViewVM(
+ IBus bus,
+ OrdersModel orders_model,
+ IObservable> security_mds_source,
+ bool use_mouse_state_transitions = false) {
_bus = bus;
_orders_model = orders_model;
security_filter = new RegexFilterVM(r => r.security.ToString(), nameof(Row.security));
- _settings_manager = settings_manager;
comparer = DIYGridSortHelper.build_comparer(sorting_hierarchy, get_column_comparer, Columns.Security);
// rows
- _wrapped_rows = _rows.WrapSelector(r => r.security_md, s => new Row(_bus, s, _orders_model));
+ _wrapped_rows = _rows.WrapSelector(r => r.security_md, s => {
+ var r = new Row(_bus, s, _orders_model);
+ r.is_visible = _filter == null || _filter(r);
+ return r;
+ });
_current_row = backing(nameof(current_row));
_rows_view = new ListCollectionView(_rows) {
Filter = obj => obj is Row r && r.is_visible,
@@ -67,23 +89,54 @@ namespace mbts.spark {
add_col(Columns.Volume, "Volume", r => r.volume.ToString(), nameof(Row.volume), DefaultColumnWidths.Volume, volume_filter, DataGridFilterKnownTemplates.ULongMinMax);
add_col(Columns.LastSpd, "Spd", r => r.last_price_from_spread ? "S" : "", nameof(Row.last_price_from_spread), DefaultColumnWidths.Side, last_price_from_spread_filter, DataGridFilterKnownTemplates.Regex);
add_col(Columns.Last, "Last", r => r.format_price(r.last_price), nameof(Row.last_price), DefaultColumnWidths.Price, last_price_filter, DataGridFilterKnownTemplates.DoubleMinMax);
- add_col(Columns.LastSize, merged_size_col_header, r => r.last_size.ToString(), nameof(Row.last_size), DefaultColumnWidths.Size, last_size_filter, DataGridFilterKnownTemplates.UIntMinMax, h_alignment: System.Windows.HorizontalAlignment.Right);
- add_col(Columns.Position, "Pos", r => r.position.ToString(), nameof(Row.position), DefaultColumnWidths.Size, position_filter, DataGridFilterKnownTemplates.IntMinMax, true, h_alignment: System.Windows.HorizontalAlignment.Right);
- add_col(Columns.RawDayPosition, "Raw Day Pos", r => SparkUtils.fmt_position(r.raw_day_position), nameof(Row.raw_day_position), DefaultColumnWidths.Size, raw_day_position_filter, DataGridFilterKnownTemplates.DoubleMinMax, true, h_alignment: System.Windows.HorizontalAlignment.Right);
- add_hl_working_orders_col(Columns.WorkingBids, working_col_header, nameof(Row.working_bids_text), nameof(Row.working_best_bid), nameof(Row.working_bids_click), working_bid_volume_filter, BTSColors.MarketData.bid, System.Windows.HorizontalAlignment.Right);
- add_col(Columns.BidImpliedSize, implied_size_col_header, r => r.bid_implied_size.ToString(), nameof(Row.bid_implied_size), DefaultColumnWidths.ImpliedDirectMergedSize, bid_implied_size_filter, DataGridFilterKnownTemplates.UIntMinMax, font_color: BTSColors.MarketData.bid, h_alignment: System.Windows.HorizontalAlignment.Right);
- add_col(Columns.BidDirectSize, direct_size_col_header, r => r.bid_direct_size.ToString(), nameof(Row.bid_direct_size), DefaultColumnWidths.ImpliedDirectMergedSize, bid_direct_size_filter, DataGridFilterKnownTemplates.UIntMinMax, font_color: BTSColors.MarketData.bid, h_alignment: System.Windows.HorizontalAlignment.Right);
- add_hl_merged_size_col(Columns.BidMergedSize, merged_size_col_header, nameof(Row.bid_merged_size), nameof(Row.bid_merged_size_text), nameof(Row.bid_merged_size_click), bid_merged_size_filter, BTSColors.MarketData.bid, h_alignment: System.Windows.HorizontalAlignment.Right);
- add_hl_price_col(Columns.Bid, "Bid", nameof(Row.bid_price), nameof(Row.bid_price_click), bid_price_filter, BTSColors.header_text, BTSColors.MarketData.bid, h_alignment: System.Windows.HorizontalAlignment.Right);
- add_hl_price_col(Columns.Ask, "Ask", nameof(Row.ask_price), nameof(Row.ask_price_click), ask_price_filter, BTSColors.header_text, BTSColors.MarketData.ask, h_alignment: System.Windows.HorizontalAlignment.Right);
- add_hl_merged_size_col(Columns.AskMergedSize, merged_size_col_header, nameof(Row.ask_merged_size), nameof(Row.ask_merged_size_text), nameof(Row.ask_merged_size_click), ask_merged_size_filter, BTSColors.MarketData.ask, h_alignment: System.Windows.HorizontalAlignment.Right);
- add_col(Columns.AskDirectSize, direct_size_col_header, r => r.ask_direct_size.ToString(), nameof(Row.ask_direct_size), DefaultColumnWidths.ImpliedDirectMergedSize, ask_direct_size_filter, DataGridFilterKnownTemplates.UIntMinMax, font_color: BTSColors.MarketData.ask, h_alignment: System.Windows.HorizontalAlignment.Right);
- add_col(Columns.AskImpliedSize, implied_size_col_header, r => r.ask_implied_size.ToString(), nameof(Row.ask_implied_size), DefaultColumnWidths.ImpliedDirectMergedSize, ask_implied_size_filter, DataGridFilterKnownTemplates.UIntMinMax, font_color: BTSColors.MarketData.ask, h_alignment: System.Windows.HorizontalAlignment.Right);
- add_hl_working_orders_col(Columns.WorkingAsks, working_col_header, nameof(Row.working_asks_text), nameof(Row.working_best_ask), nameof(Row.working_asks_click), working_ask_volume_filter, BTSColors.MarketData.ask, System.Windows.HorizontalAlignment.Right);
- add_col(Columns.Settlement, "Settlement", r => r.format_price(r.settlement), nameof(Row.settlement), DefaultColumnWidths.Settlement, settlement_filter, DataGridFilterKnownTemplates.DoubleMinMax, h_alignment: System.Windows.HorizontalAlignment.Right);
- add_col(Columns.DayChange, "Change", r => r.format_price(r.day_change), nameof(Row.day_change), DefaultColumnWidths.Price, day_change_filter, DataGridFilterKnownTemplates.DoubleMinMax, true, h_alignment: System.Windows.HorizontalAlignment.Right);
- add_col(Columns.SessionLow, "Low", r => r.format_price(r.session_low), nameof(Row.session_low), DefaultColumnWidths.Price, session_low_filter, DataGridFilterKnownTemplates.DoubleMinMax, h_alignment: System.Windows.HorizontalAlignment.Right);
- add_col(Columns.SessionHigh, "High", r => r.format_price(r.session_high), nameof(Row.session_high), DefaultColumnWidths.Price, session_high_filter, DataGridFilterKnownTemplates.DoubleMinMax, h_alignment: System.Windows.HorizontalAlignment.Right);
+ add_col(Columns.LastSize, merged_size_col_header, r => r.last_size.ToString(), nameof(Row.last_size), DefaultColumnWidths.Size, last_size_filter, DataGridFilterKnownTemplates.UIntMinMax);
+ add_hl_col(
+ Columns.Position,
+ "Pos",
+ new Binding(nameof(Row.position)),
+ nameof(Row.position),
+ position_filter,
+ DataGridFilterKnownTemplates.IntMinMax,
+ DefaultColumnWidths.Size,
+ tooltip: $"Show {_fills_title(Columns.Position).desc}");
+ add_hl_col(
+ Columns.RawDayPosition,
+ "Raw Day Pos",
+ new Binding(nameof(Row.raw_day_position)) {
+ Converter = ValueConverter.create(SparkUtils.fmt_position)
+ },
+ nameof(Row.raw_day_position),
+ raw_day_position_filter,
+ DataGridFilterKnownTemplates.DoubleMinMax,
+ DefaultColumnWidths.Size,
+ tooltip: $"Show {_fills_title(Columns.RawDayPosition).desc}");
+ add_hl_working_orders_col(
+ Columns.WorkingBids,
+ working_col_header,
+ nameof(Row.working_bids_text),
+ nameof(Row.working_best_bid),
+ working_bid_volume_filter,
+ BTSColors.MarketData.bid);
+ add_col(Columns.BidImpliedSize, implied_size_col_header, r => r.bid_implied_size.ToString(), nameof(Row.bid_implied_size), DefaultColumnWidths.ImpliedDirectMergedSize, bid_implied_size_filter, DataGridFilterKnownTemplates.UIntMinMax, font_color: BTSColors.MarketData.bid);
+ add_col(Columns.BidDirectSize, direct_size_col_header, r => r.bid_direct_size.ToString(), nameof(Row.bid_direct_size), DefaultColumnWidths.ImpliedDirectMergedSize, bid_direct_size_filter, DataGridFilterKnownTemplates.UIntMinMax, font_color: BTSColors.MarketData.bid);
+
+ add_hl_merged_size_col(Columns.BidMergedSize, nameof(Row.bid_merged_size), nameof(Row.bid_merged_size_text), bid_merged_size_filter, BTSColors.MarketData.bid);
+ add_hl_price_col(Columns.Bid, "Bid", nameof(Row.bid_price), bid_price_filter, BTSColors.header_text, BTSColors.MarketData.bid);
+ add_hl_price_col(Columns.Ask, "Ask", nameof(Row.ask_price), ask_price_filter, BTSColors.header_text, BTSColors.MarketData.ask);
+ add_hl_merged_size_col(Columns.AskMergedSize, nameof(Row.ask_merged_size), nameof(Row.ask_merged_size_text), ask_merged_size_filter, BTSColors.MarketData.ask);
+ add_col(Columns.AskDirectSize, direct_size_col_header, r => r.ask_direct_size.ToString(), nameof(Row.ask_direct_size), DefaultColumnWidths.ImpliedDirectMergedSize, ask_direct_size_filter, DataGridFilterKnownTemplates.UIntMinMax, font_color: BTSColors.MarketData.ask);
+ add_col(Columns.AskImpliedSize, implied_size_col_header, r => r.ask_implied_size.ToString(), nameof(Row.ask_implied_size), DefaultColumnWidths.ImpliedDirectMergedSize, ask_implied_size_filter, DataGridFilterKnownTemplates.UIntMinMax, font_color: BTSColors.MarketData.ask);
+ add_hl_working_orders_col(
+ Columns.WorkingAsks,
+ working_col_header,
+ nameof(Row.working_asks_text),
+ nameof(Row.working_best_ask),
+ working_ask_volume_filter,
+ BTSColors.MarketData.ask);
+ add_col(Columns.Settlement, "Settlement", r => r.format_price(r.settlement), nameof(Row.settlement), DefaultColumnWidths.Settlement, settlement_filter, DataGridFilterKnownTemplates.DoubleMinMax);
+ add_col(Columns.DayChange, "Change", r => r.format_price(r.day_change), nameof(Row.day_change), DefaultColumnWidths.Price, day_change_filter, DataGridFilterKnownTemplates.DoubleMinMax, true);
+ add_col(Columns.SessionLow, "Low", r => r.format_price(r.session_low), nameof(Row.session_low), DefaultColumnWidths.Price, session_low_filter, DataGridFilterKnownTemplates.DoubleMinMax);
+ add_col(Columns.SessionHigh, "High", r => r.format_price(r.session_high), nameof(Row.session_high), DefaultColumnWidths.Price, session_high_filter, DataGridFilterKnownTemplates.DoubleMinMax);
add_col(Columns.SecurityKey, "Security Key", r => r.security_key.ToString(), nameof(Row.security_key), DefaultColumnWidths.SecurityKey, security_key_filter, DataGridFilterKnownTemplates.Regex);
add_col(Columns.MarketDescription, "Market Description", r => r.security.market_description(), nameof(Row.market_description), DefaultColumnWidths.SecurityKey, market_description_filter, DataGridFilterKnownTemplates.Regex);
@@ -93,8 +146,8 @@ namespace mbts.spark {
add_col(Columns.StripName, "Strip Name", r => r.security.strip_name(), nameof(Row.strip_name), DefaultColumnWidths.SecurityKey, strip_name_filter, DataGridFilterKnownTemplates.Regex);
add_col(Columns.MicCode, "Mic Code", r => r.security.mic_code(), nameof(Row.mic_code), DefaultColumnWidths.SecurityKey, mic_code_filter, DataGridFilterKnownTemplates.Regex);
//
- void add_col(Columns col_key, string header, Func get_text, string prop, double width, IDataGridColumnFilter filter, string filter_template, bool is_animatable = false, Color? font_color = null, System.Windows.HorizontalAlignment h_alignment = System.Windows.HorizontalAlignment.Left) =>
- columns.Add(new RenderTextColumn(col_key, get_text, filter, filter_template) {
+ RenderTextColumn add_col(Columns col_key, string header, Func get_text, string prop, double width, IDataGridColumnFilter filter, string filter_template, bool is_animatable = false, Color? font_color = null, HorizontalAlignment h_alignment = HorizontalAlignment.Right) {
+ var col = new RenderTextColumn(col_key, get_text, filter, filter_template) {
// it's necessary to have this id populated for grid settings
id = bts.shell.MarketViewWindowFactory.col_id(col_key),
Header = header,
@@ -103,31 +156,85 @@ namespace mbts.spark {
padding = is_animatable ? AnimatableCellPadding : DefaultCellPadding,
font_color_override = font_color,
horizontal_alignment = h_alignment
- });
+ };
+ columns.Add(col);
+ return col;
+ }
- void add_hl_price_col(Columns col_key, string header, string prop, string command_prop, IDataGridColumnFilter filter, Color foreground, Color? background, System.Windows.HorizontalAlignment h_alignment) =>
- add_hl_col(col_key, header, price_binding(prop), prop, command_prop, filter, DataGridFilterKnownTemplates.DoubleMinMax, DefaultColumnWidths.MViewPrice, foreground, background, h_alignment);
+ void add_hl_price_col(Columns col_key, string header, string prop, IDataGridColumnFilter filter, Color foreground, Color? background) =>
+ add_hl_col(
+ col_key,
+ header,
+ price_binding(prop),
+ prop,
+ filter,
+ DataGridFilterKnownTemplates.DoubleMinMax,
+ DefaultColumnWidths.MViewPrice,
+ foreground,
+ background,
+ tooltip: _order_ticket_tooltip(col_key));
- void add_hl_merged_size_col(Columns col_key, string header, string sort_prop, string text_prop, string command_prop, IDataGridColumnFilter filter, Color foreground, System.Windows.HorizontalAlignment h_alignment) =>
- add_hl_col(col_key, header, new Binding(text_prop), sort_prop, command_prop, filter, DataGridFilterKnownTemplates.UIntMinMax, DefaultColumnWidths.ImpliedDirectMergedSize, foreground, null, h_alignment);
+ void add_hl_merged_size_col(Columns col_key, string sort_prop, string text_prop, IDataGridColumnFilter filter, Color foreground) =>
+ add_hl_col(
+ col_key,
+ merged_size_col_header,
+ new Binding(text_prop),
+ sort_prop,
+ filter,
+ DataGridFilterKnownTemplates.UIntMinMax,
+ DefaultColumnWidths.ImpliedDirectMergedSize,
+ foreground,
+ tooltip: _order_ticket_tooltip(col_key));
- void add_hl_working_orders_col(Columns col_key, string header, string text_prop, string sort_prop, string command_prop, IDataGridColumnFilter filter, Color foreground, System.Windows.HorizontalAlignment h_alignment) =>
- add_hl_col(col_key, header, new Binding(text_prop), sort_prop, command_prop, filter, DataGridFilterKnownTemplates.IntMinMax, working_orders_col_width, foreground, null, h_alignment);
+ HyperlinkElementColumn add_hl_working_orders_col(Columns col_key, string header, string text_prop, string sort_prop, IDataGridColumnFilter filter, Color foreground) =>
+ add_hl_col(
+ col_key,
+ header,
+ new Binding(text_prop),
+ sort_prop,
+ filter,
+ DataGridFilterKnownTemplates.IntMinMax,
+ working_orders_col_width,
+ foreground,
+ tooltip: $"Show {_orderbook_title(col_key).desc}");
- void add_hl_col(
- Columns col_key, string header, BindingBase binding, string prop, string command_prop,
- IDataGridColumnFilter filter, string filter_template, double width, Color foreground, Color? background, System.Windows.HorizontalAlignment h_alignment = System.Windows.HorizontalAlignment.Left) =>
- columns.Add(new HyperlinkElementColumn(
- col_key, binding, new Binding(command_prop), filter, filter_template) {
- // it's necessary to have this id populated for grid settings
- id = bts.shell.MarketViewWindowFactory.col_id(col_key),
- Header = header,
- sort_member_path = prop,
- font_color_override = foreground,
- background_color_override = background,
- Width = DefaultColumnWidths.Price,
- horizontal_alignment = h_alignment
- });
+ HyperlinkElementColumn add_hl_col(
+ Columns col_key,
+ string header,
+ BindingBase binding,
+ string prop,
+ IDataGridColumnFilter filter,
+ string filter_template,
+ double width,
+ Color? foreground = null,
+ Color? background = null,
+ HorizontalAlignment h_alignment = HorizontalAlignment.Right,
+ string tooltip = null) {
+
+ var col = new HyperlinkElementColumn(
+ col_key, filter, filter_template) {
+ // it's necessary to have this id populated for grid settings
+ id = bts.shell.MarketViewWindowFactory.col_id(col_key),
+ Header = header,
+ sort_member_path = prop,
+ font_color_override = foreground,
+ background_color_override = background,
+ TextBinding = binding,
+ Width = DefaultColumnWidths.Price,
+ horizontal_alignment = h_alignment,
+ HyperlinkClickMode = HyperlinkCellClickMode.Anywhere
+ };
+ col.view_generated += on_view_generated;
+ columns.Add(col);
+ return col;
+ //
+ void on_view_generated(HyperlinkCell cell_view) {
+ cell_view.SetValue(ColumnKeyProperty, col_key);
+ cell_view.ToolTip = tooltip;
+ if (use_mouse_state_transitions)
+ init_hyperlink_hover_animation(cell_view, col);
+ }
+ }
BindingBase price_binding(string prop) {
var mb = new MultiBinding { Converter = price_converter };
@@ -135,14 +242,6 @@ namespace mbts.spark {
mb.Bindings.Add(new Binding($"{nameof(Row.security)}.{nameof(SecurityImmutable.ContractSpec)}") { Mode = BindingMode.OneWay });
return mb;
}
- //
- // make font-color immutable for Side, Size, (the font-color rule is hard-coded: MarketData.bid for Buy orders, MarketData.ask for Sells)
- var col_settings_flags = new Dictionary {
- { Columns.Bid.ToString(), GridColumnSettingsFeatureFlags.ImmutableFontColor | GridColumnSettingsFeatureFlags.ImmutableBackgroundColor },
- { Columns.Ask.ToString(), GridColumnSettingsFeatureFlags.ImmutableFontColor | GridColumnSettingsFeatureFlags.ImmutableBackgroundColor},
- { Columns.WorkingAsks.ToString(), GridColumnSettingsFeatureFlags.ImmutableFontColor },
- { Columns.WorkingBids.ToString(), GridColumnSettingsFeatureFlags.ImmutableFontColor }
- };
// row vm filter
// Althoguh securities are filtered at the row 'model' level (SecurityMD), the column base filters have to be applied at the row vm level,
// because they access market data, which is polled periodically and cached in the row vm.
@@ -158,75 +257,66 @@ namespace mbts.spark {
f.filter_changed += _refresh_filter.execute;
}
//
- grid_settings_manager = new SimpleGridSettingsManager(columns, col_settings_flags, _rows);
- // this will initialize the immutable properties that correspond to the col_settings_flags above (e.g. Bid/Ask font_color_override, etc.)
- grid_settings_manager.load_from(settings_manager.current_settings());
- //
_dispose = Disposable.Create(
row_filter_source.Subscribe(f => {
// the rows are polled every 100 ms, just set the filter and let the refresh take care of the rest.
_filter = f;
}),
- state.SelectManyInSeries(s => s.security_criteria.subscribe_md_map(bus))
- .Table(SecurityMD.key_fn)
- .Where(t => t.has_changes())
- .OnDispatcher()
- .Subscribe(table => {
- _wrapped_rows.RemoveSorted(table.removed.Concat(table.changes.Old()), _sec_md_key_comparer);
- _wrapped_rows.InsertSorted(table.added.Concat(table.changes.New()), _sec_md_key_comparer);
- }),
- state.OnDispatcher().Subscribe(s => {
- using (Disposable.empty) {
- if (s.grid != null) {
- // this means we are not using defaults,
- // Note: settings_manager in turn notifies grid_settings_manager of this update
- _settings_manager.instance_settings = s.grid;
- }
- if (s.filters is FilterStates filters) {
- using (_refresh_filter.defer_execution()) {
- security_filter.load_state(filters.security);
- bid_price_filter.load_state(filters.bid_price);
- bid_merged_size_filter.load_state(filters.bid_merged_size);
- bid_direct_size_filter.load_state(filters.bid_direct_size);
- bid_implied_size_filter.load_state(filters.bid_implied_size);
- ask_price_filter.load_state(filters.ask_price);
- ask_merged_size_filter.load_state(filters.ask_merged_size);
- ask_direct_size_filter.load_state(filters.ask_direct_size);
- ask_implied_size_filter.load_state(filters.ask_implied_size);
- last_price_filter.load_state(filters.last_price);
- last_price_from_spread_filter.load_state(filters.last_price_from_spread);
- last_size_filter.load_state(filters.last_size);
- day_change_filter.load_state(filters.day_change);
- settlement_filter.load_state(filters.settlement);
- session_low_filter.load_state(filters.session_low);
- session_high_filter.load_state(filters.session_high);
- position_filter.load_state(filters.position);
- raw_day_position_filter.load_state(filters.raw_day_position);
- working_bid_volume_filter.load_state(filters.working_bid_volume);
- working_ask_volume_filter.load_state(filters.working_ask_volume);
- volume_filter.load_state(filters.volume);
- security_key_filter.load_state(filters.security_key);
- }
- }
- }
- }),
+ security_mds_source.Subscribe(_on_securities),
ObservableAutoWrapper.connect_item_hooks(_rows, r => r, true),
- settings_manager.subscribe_current().OnDispatcher().Subscribe(grid_settings_manager),
WorkLoop.Start("MarketView", _refresh)
);
+ void init_hyperlink_hover_animation(HyperlinkCell cell_view, ColumnBase col) {
+ // background timeline
+ var hover_bg_timeline = VisualStateHelper.create_color_animation(
+ cell_view,
+ new PropertyPath("(0).(1)", HyperlinkCell.BackgroundProperty, SolidColorBrush.ColorProperty),
+ new Binding(nameof(ColumnBase.background_color)) {
+ Source = col,
+ Converter = new ColorBlendConverter { Overlay = Colors.Black.with(0x1A) }
+ });
+ // background pressed timeline
+ var pressed_bg_timeline = VisualStateHelper.create_color_animation(
+ cell_view,
+ new PropertyPath("(0).(1)", HyperlinkCell.BackgroundProperty, SolidColorBrush.ColorProperty),
+ new Binding(nameof(ColumnBase.background_color)) {
+ Source = col,
+ Converter = new ColorBlendConverter { Overlay = Colors.Black.with(0x25) }
+ });
+ // border timeline
+ var bd_brush = new SolidColorBrush();
+ BindingOperations.SetBinding(bd_brush, SolidColorBrush.ColorProperty, new Binding(nameof(ColumnBase.background_color)) { Source = col });
+ cell_view.BorderBrush = bd_brush;
+ cell_view.BorderThickness = new Thickness(1);
+ var hover_bd_timeline = VisualStateHelper.create_color_animation(
+ cell_view, new PropertyPath("(0).(1)", HyperlinkCell.BorderBrushProperty, SolidColorBrush.ColorProperty), Colors.Black.with(0x60));
+ // mouse states
+ VisualStateManager.GetVisualStateGroups(cell_view).Add(
+ VisualStateHelper.create_group("MouseStates",
+ new VisualState { Name = "Normal" },
+ VisualStateHelper.create_state("MouseOver", hover_bg_timeline, hover_bd_timeline),
+ VisualStateHelper.create_state("Pressed", pressed_bg_timeline, hover_bd_timeline)));
+ // state groups manager
+ StateManager.SetGroups(cell_view, new StateGroupsManager {
+ StateDefinitions = new StateDefinitionCollection {
+ new MouseStateDefinition { MouseSource = cell_view }
+ }
+ });
+ }
}
public event PropertyChange current_row_changed { add => _current_row.Changed += value; remove => _current_row.Changed -= value; }
event Action _filter_changed;
public ICollectionView rows_view => _rows_view;
+ public ObservableCollection rows => _rows;
public ObservableCollection selected_rows { get; } = new ObservableCollection();
+ public Color selection_highlight { get => _selection_highlight; set => Set(ref _selection_highlight, value); }
public ObservableCollection columns { get; } = new ObservableCollection();
public Row current_row { get => _current_row.Value; set => _current_row.set(value); }
- public SimpleGridSettingsManager grid_settings_manager { get; }
- public ICommand insert_group_cmd { get; }
- public ICommand remove_partition_cmd { get; }
+ internal IBus bus => _bus;
+ internal OrdersModel orders_model => _orders_model;
public IReadOnlyList<(Columns key, ListSortDirection direction)> sorting_hierarchy {
get => _sorting_hierarchy ?? (_sorting_hierarchy = Array.Empty<(Columns, ListSortDirection)>());
@@ -308,11 +398,45 @@ namespace mbts.spark {
Comparer.Default.wrap(selector);
}
- public GridSettings get_grid_save_state() {
- if (_settings_manager.instance_settings == null)
- return null;
- else
- return grid_settings_manager.create();
+ public (FillsVM vm, string context, string title, Color fg) fills_params(SecurityImmutable security, Columns column) {
+ var (title_desc, fg) = _fills_title(column);
+ var state = StartupData.DefaultAuxiliaryFills;
+ if (state.filters == null)
+ state.filters = new bts.shell.fills.FillsFilterStates();
+ state.filters.security = SecurityCriteria.from_keys(new[] { security.Id });
+ switch (column) {
+ case Columns.RawDayPosition:
+ state.filters.fill_kind = new[] {
+ Fill.Types.FillKind.Standard
+ };
+ break;
+ case Columns.Position:
+ state.filters.fill_kind = new[] {
+ Fill.Types.FillKind.Standard,
+ Fill.Types.FillKind.Sod
+ };
+ break;
+ }
+ state.grid = GridSettingsHelper.hide_cols(state.grid, new[] { ui.spark.FillsView.Columns.Security });
+ var vm = new FillsVM(
+ _bus, _orders_model, Observable.Return(state), new WindowSettingsManager.DisconnectedImpl(state.grid));
+ return (vm, $"{security}", title_desc, fg);
+ }
+
+ public (OrderBookVM vm, string context, string title, Color fg) orderbook_params(SecurityImmutable security, Columns column) {
+ var (title_desc, fg) = _orderbook_title(column);
+ var filters = new bts.shell.orderbook.FilterStates {
+ security_criteria = SecurityCriteria.from_keys(new[] { security.Id })
+ };
+ switch (column) {
+ case Columns.WorkingBids: filters.sides = new[] { Side.Buy }; break;
+ case Columns.WorkingAsks: filters.sides = new[] { Side.Sell }; break;
+ }
+ var vm = _mk_orderbook_vm_common(filters, new[] {
+ OrderBookGridView.ColumnKeys.Security,
+ OrderBookGridView.ColumnKeys.Side
+ });
+ return (vm, $"{security}", title_desc, fg);
}
public FilterStates get_filter_states() {
@@ -348,6 +472,79 @@ namespace mbts.spark {
};
}
+ public void load_filters(FilterStates filters) {
+ if (filters != null) {
+ using (rows_view.DeferRefresh()) {
+ security_filter.load_state(filters.security);
+ bid_price_filter.load_state(filters.bid_price);
+ bid_merged_size_filter.load_state(filters.bid_merged_size);
+ bid_direct_size_filter.load_state(filters.bid_direct_size);
+ bid_implied_size_filter.load_state(filters.bid_implied_size);
+ ask_price_filter.load_state(filters.ask_price);
+ ask_merged_size_filter.load_state(filters.ask_merged_size);
+ ask_direct_size_filter.load_state(filters.ask_direct_size);
+ ask_implied_size_filter.load_state(filters.ask_implied_size);
+ last_price_filter.load_state(filters.last_price);
+ last_price_from_spread_filter.load_state(filters.last_price_from_spread);
+ last_size_filter.load_state(filters.last_size);
+ day_change_filter.load_state(filters.day_change);
+ settlement_filter.load_state(filters.settlement);
+ session_low_filter.load_state(filters.session_low);
+ session_high_filter.load_state(filters.session_high);
+ position_filter.load_state(filters.position);
+ raw_day_position_filter.load_state(filters.raw_day_position);
+ working_bid_volume_filter.load_state(filters.working_bid_volume);
+ working_ask_volume_filter.load_state(filters.working_ask_volume);
+ volume_filter.load_state(filters.volume);
+ security_key_filter.load_state(filters.security_key);
+ }
+ }
+ }
+
+ static (string desc, Color fg) _fills_title(Columns key) {
+ switch (key) {
+ case Columns.Position: return ("Day Fills", Colors.Black);
+ case Columns.RawDayPosition: return ("Raw Day Fills", Colors.Black);
+ default: return ("Fills", Colors.Black);
+ }
+ }
+
+ static (string desc, Color fg) _orderbook_title(Columns key) {
+ switch (key) {
+ case Columns.WorkingBids: return ("Working Buy Orders", BTSColors.MarketData.bid);
+ case Columns.WorkingAsks: return ("Working Sell Orders", BTSColors.MarketData.ask);
+ default: return ("Working Orders", Colors.Black);
+ }
+ }
+
+ static GridSettings _hide_cols(GridSettings grid, IEnumerable columns) =>
+ grid.with(_hide_cols(grid.columns, columns));
+
+ static IEnumerable _hide_cols(IEnumerable cols, IEnumerable columns) {
+ // hide columns who's data is implied by the grouping
+ var col_hash = new HashSet(columns.Select(k => k.ToString()));
+ foreach (var c in cols)
+ yield return col_hash.Contains(c.id) ? c.with(is_visible: false) : c;
+ }
+
+ static string _order_ticket_tooltip(Columns key) {
+ var bid_or_ask = Regex.Match(key.ToString(), "(Bid|Ask)");
+ return bid_or_ask.Success ? $"Order Ticket @ {bid_or_ask.Value}" : "Order Ticket";
+ }
+
+ OrderBookVM _mk_orderbook_vm_common(bts.shell.orderbook.FilterStates filters, IEnumerable hide_cols, IObservable> orders_source = null) {
+ var state = new bts.shell.orderbook.OrderBookStartupData(filters, bts.shell.orderbook.OrderBookStartupData.default_mini_grid, use_defaults: false);
+ state.orderbook_grid.general = _hide_cols(state.orderbook_grid.general, hide_cols);
+ var settings_man = new WindowSettingsManager.DisconnectedImpl(state.orderbook_grid);
+ var obvm = orders_source == null ?
+ new OrderBookVM(_bus, null, _orders_model, settings_man, true) :
+ new OrderBookVM(_bus, null, _orders_model, orders_source, settings_man, true);
+ foreach (var c in OrderBookVM.OrderBookColumnCollection.editable_columns)
+ obvm.columns.Insert(0, c);
+ obvm.load_from(state);
+ return obvm;
+ }
+
void _refresh() {
foreach (var r in _rows) {
r.refresh();
@@ -361,5 +558,22 @@ namespace mbts.spark {
selected_rows.RemoveSorted(e.RemovedItems.OfType(), _row_key_comparer);
selected_rows.InsertSorted(e.AddedItems.OfType(), _row_key_comparer);
}
+
+ void _on_securities(TableUpdate table) {
+ var sw = new Stopwatch();
+ sw.Start();
+ foreach (var smd in table.removed) {
+ int i = CollectionHelper.binary_search(_wrapped_rows, smd, _sec_md_key_comparer);
+ Log.assert(i >= 0);
+ _wrapped_rows.RemoveAt(i);
+ }
+ foreach (var smd in table.added) {
+ int i = CollectionHelper.binary_search(_wrapped_rows, smd, _sec_md_key_comparer);
+ Log.assert(i < 0);
+ _wrapped_rows.Insert(~i, smd);
+ }
+ sw.Stop();
+ Log.Ui.info($"MarketView update took {sw.ElapsedMilliseconds:0.00}ms.");
+ }
}
}
diff --git a/src/client/spark/Modules/Market/MarketView/MarketViewView.xaml b/src/client/spark/Modules/Market/MarketView/MarketViewView.xaml
index 03151c623..2d44c491a 100644
--- a/src/client/spark/Modules/Market/MarketView/MarketViewView.xaml
+++ b/src/client/spark/Modules/Market/MarketView/MarketViewView.xaml
@@ -3,8 +3,13 @@
x:ClassModifier="internal"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:bv="clr-namespace:bts.view"
+ xmlns:u="clr-namespace:mbts.ui"
xmlns:c="clr-namespace:mbts.ui.controls"
+ xmlns:cv="clr-namespace:mbts.ui.converters"
xmlns:usp="clr-namespace:mbts.ui.spark"
+ xmlns:sys="clr-namespace:System;assembly=mscorlib"
+ u:WindowStyle.UnsafeWindow="{Binding Path='(u:WindowStyle.UnsafeWindow)', RelativeSource={RelativeSource AncestorType=bv:SparkWindowView}}"
FocusManager.IsFocusScope="True"
Background="{StaticResource CommonBackground}"
>
@@ -13,8 +18,54 @@
+
+
+
+
@@ -27,7 +78,7 @@
Columns="{Binding columns}"
CurrentRow="{Binding current_row}"
Rows="{Binding rows_view}"
- SelectionHighlight="{Binding grid_settings_manager.selection_highlight, Mode=OneWay}"
+ SelectionHighlight="{Binding selection_highlight, Mode=OneWay}"
SyncColumnSortDirectionWithSortDescriptions="False"
>
diff --git a/src/client/spark/Modules/Market/MarketView/MarketViewView.xaml.cs b/src/client/spark/Modules/Market/MarketView/MarketViewView.xaml.cs
index 9a820ab1e..cef648d65 100644
--- a/src/client/spark/Modules/Market/MarketView/MarketViewView.xaml.cs
+++ b/src/client/spark/Modules/Market/MarketView/MarketViewView.xaml.cs
@@ -1,14 +1,36 @@
-using mbts.spark;
+using bts.api;
+using mbts.spark;
using mbts.ui.controls;
+using mbts.ui.converters;
+using mbts.ui.extensions;
using System.Linq;
+using System.Reactive.Linq;
+using System.Windows;
using System.Windows.Controls;
+using System.Windows.Data;
+using System.Windows.Input;
namespace mbts.ui.spark {
///
/// Interaction logic for MarketSheetsView.xaml
///
partial class MarketViewView : UserControl {
+ public static readonly RoutedCommand CellClickCommand = new RoutedCommand();
+
+ MarketViewVM _vm;
+ DIYGridPopupManager _popup_man_backing;
+
+ public MarketViewView() {
+ InitializeComponent();
+ this.ConnectDataContext(vm => _vm = vm);
+ AdornerHelper.attach_continuously_on_loaded(
+ () => new GridAnimation.DIYGridAnimationAdorner(
+ _grid, _grid.descendants_of_type().First()),
+ _grid);
+ }
+
public enum Columns {
+ None,
Security,
Volume,
Last,
@@ -39,12 +61,105 @@ namespace mbts.ui.spark {
MicCode
}
- public MarketViewView() {
- InitializeComponent();
- AdornerHelper.attach_continuously_on_loaded(
- () => new GridAnimation.DIYGridAnimationAdorner(
- _grid, _grid.descendants_of_type().First()),
- _grid);
+ public static ExecutedRoutedEventHandler
+ CellClickExecuted => CommandImpl.cell_click;
+
+ public static IMultiValueConverter CellConverter =>
+ ValueConverter.create((r, c) => new MarketViewCell(r, c));
+
+ IBus _bus => _vm.bus;
+ OrdersModel _model => _vm.orders_model;
+ DIYGridPopupManager _popup_man =>
+ _popup_man_backing ?? (_popup_man_backing = DIYGridPopupManager.create(this));
+
+ public void show_mini_orderbook(SecurityMD security_md) =>
+ _show_mini_orderbook(security_md, Columns.None);
+
+ void _on_cell_click(MarketViewVM.Row row, Columns col) {
+ switch (col) {
+ case Columns.WorkingAsks:
+ case Columns.WorkingBids:
+ _show_mini_orderbook(row.security_md, col);
+ break;
+ case Columns.Bid:
+ _show_order_ticket(row.security, row.bid_price);
+ break;
+ case Columns.BidMergedSize: _show_order_ticket(row.security, row.bid_price, row.bid_merged_size); break;
+ case Columns.BidDirectSize: _show_order_ticket(row.security, row.bid_price, row.bid_direct_size); break;
+ case Columns.BidImpliedSize: _show_order_ticket(row.security, row.bid_price, row.bid_implied_size); break;
+ case Columns.Ask:
+ _show_order_ticket(row.security, row.ask_price);
+ break;
+ case Columns.AskMergedSize: _show_order_ticket(row.security, row.ask_price, row.ask_merged_size); break;
+ case Columns.AskDirectSize: _show_order_ticket(row.security, row.ask_price, row.ask_direct_size); break;
+ case Columns.AskImpliedSize: _show_order_ticket(row.security, row.ask_price, row.ask_implied_size); break;
+ case Columns.Last:
+ _show_order_ticket(row.security, row.last_price);
+ break;
+ case Columns.LastSize:
+ _show_order_ticket(row.security, row.last_price, row.last_size);
+ break;
+ case Columns.Position:
+ case Columns.RawDayPosition:
+ _show_mini_fills(row.security, col);
+ break;
+ }
+ }
+
+ void _show_mini_orderbook(SecurityMD security_md, Columns col) {
+ var (vm, context, title, fg) = _vm.orderbook_params(security_md.security, col);
+ _show_popup(vm, new AuxiliaryDataHeader {
+ context = context,
+ title = title,
+ title_foreground = fg
+ });
+ }
+
+ void _show_mini_fills(SecurityImmutable security, Columns col) {
+ var (vm, context, title, fg) = _vm.fills_params(security, col);
+ _show_popup(vm, new AuxiliaryDataHeader {
+ context = context,
+ title = title,
+ title_foreground = fg
+ });
+ }
+
+ async void _show_order_ticket(SecurityImmutable security, double price = double.NaN, uint size = 0) {
+ var sec_md = await _bus.subscribe(SparkTags.SecuritiesTag)
+ .Select(sec_map => sec_map.TryGetValue(security.Id, out var smd) ? smd : (SecurityMD?)null)
+ .SkipWhile(smd => !smd.HasValue)
+ .FirstOrDefaultAsync();
+ if (sec_md.HasValue) {
+ var resources = new OrderTicketResources();
+ resources.MergedDictionaries.Add(new AuxiliaryDataResources());
+ var ticket_vm = new NewOrderTicketVM(
+ _model, sec_md.Value, _model.factory.order(security), price, size, Observable.Return(mbts.spark.LadderSettings.Default.calculator_buttons));
+ _show_popup(ticket_vm, new OrderTicketHeader { bbo = ticket_vm.bbo }, resources);
+ }
+ }
+
+ void _show_popup(object content, object header) =>
+ _show_popup(content, header, new AuxiliaryDataResources());
+
+ void _show_popup(object content, object header, ResourceDictionary resources) {
+ var view = new HeaderedContentControl {
+ DataContext = content,
+ Content = content,
+ Header = header,
+ HorizontalAlignment = HorizontalAlignment.Left,
+ VerticalAlignment = VerticalAlignment.Top,
+ Resources = resources ?? new AuxiliaryDataResources()
+ };
+ view.Style = view.Resources[AuxiliaryDataResources.PopupChildStyleKey] as Style;
+ _popup_man.show_async(view).ignore();
+ }
+
+ static class CommandImpl {
+ public static void cell_click(object sender, ExecutedRoutedEventArgs e) {
+ if (sender is MarketViewView mview && e.Parameter is MarketViewCell cell)
+ mview._on_cell_click(cell.row, cell.column);
+ e.Handled = true;
+ }
}
}
diff --git a/src/client/spark/Modules/Market/MarketView/MarketViewWindow.cs b/src/client/spark/Modules/Market/MarketView/MarketViewWindow.cs
index 57ae270b8..335644589 100644
--- a/src/client/spark/Modules/Market/MarketView/MarketViewWindow.cs
+++ b/src/client/spark/Modules/Market/MarketView/MarketViewWindow.cs
@@ -1,12 +1,12 @@
using bts;
using bts.api;
+using bts.api.core;
using bts.collections;
using bts.shell;
using bts.shell.fills;
using bts.spark;
using mbts.persist;
using mbts.ui;
-using mbts.ui.controls;
using mbts.ui.settings;
using mbts.ui.spark;
using mbts.ui.utility;
@@ -22,6 +22,14 @@ using System.Windows;
namespace mbts.spark {
class MarketViewWindow : SparkWindow, ISharedWindow, ISlowMeasureContentSize {
const string _window_type = "MarketView", _window_type_display_name = "Market View";
+ // make font-color immutable for Side, Size, (the font-color rule is hard-coded: MarketData.bid for Buy orders, MarketData.ask for Sells)
+ static readonly IReadOnlyDictionary _col_settings_flags = new Dictionary {
+ { MarketViewView.Columns.Bid.ToString(), GridColumnSettingsFeatureFlags.ImmutableFontColor | GridColumnSettingsFeatureFlags.ImmutableBackgroundColor },
+ { MarketViewView.Columns.Ask.ToString(), GridColumnSettingsFeatureFlags.ImmutableFontColor | GridColumnSettingsFeatureFlags.ImmutableBackgroundColor},
+ { MarketViewView.Columns.WorkingAsks.ToString(), GridColumnSettingsFeatureFlags.ImmutableFontColor },
+ { MarketViewView.Columns.WorkingBids.ToString(), GridColumnSettingsFeatureFlags.ImmutableFontColor }
+ };
+
readonly OrdersModel _orders_model;
readonly AllowOnce _init_guard = new AllowOnce("MarketView");
readonly IWindowSettingsManager _settings_manager;
@@ -33,6 +41,7 @@ namespace mbts.spark {
SecurityCriteria
_selected_security_criteria_backing,
_sorted_selected_security_criteria_backing;
+ SimpleGridSettingsManager _grid_settings_manager;
public MarketViewWindow(MarketViewWindowFactory factory, IBus bus, OrdersModel orders_model, MarketViewWindowFactory.StartupData state, IWindowSettingsManager settings_manager)
: base("Market View", bus, factory, SparkCommands.ShowMarketView.icon) {
@@ -43,8 +52,6 @@ namespace mbts.spark {
thread_id = config.Configuration.get().market_view ? "MarketView" : null;
}
- event Action _state_changed;
-
public override string window_type => _window_type;
public override UIElement Content => _view;
public override IEnumerable Commands => _commands;
@@ -52,7 +59,7 @@ namespace mbts.spark {
_state = new MarketViewWindowFactory.StartupData(
_state.security_criteria,
_vm.get_filter_states(),
- _vm.get_grid_save_state(),
+ _settings_manager.instance_settings,
_vm.sorting_hierarchy.Select(tup => new SortingCriteria(tup.key.ToString(), tup.direction == System.ComponentModel.ListSortDirection.Ascending))
.ToList());
@@ -85,15 +92,22 @@ namespace mbts.spark {
public void thread_init() {
_init_guard.check_access();
Log.notnull(_state);
- var state_src = Observable.FromEvent, MarketViewWindowFactory.StartupData>(cb => _state_changed += cb, cb => _state_changed -= cb).StartWith(_state);
- _vm = new MarketViewVM(_bus, _orders_model, state_src, _settings_manager);
+ var update_securities_cmd = SparkCommands.change_securities_callback(_window_type_display_name);
+ _vm = new MarketViewVM(
+ _bus,
+ _orders_model,
+ _bus.subscribe(update_securities_cmd.tag)
+ .StartWith(_state.security_criteria)
+ .SelectManyInSeries(c => c.subscribe_md_map(_bus))
+ .BufferLatest(TimeSpan.FromSeconds(1))
+ .Table(SecurityMD.key_fn)
+ .Where(t => t.has_changes())
+ .OnDispatcher());
+ _vm.load_filters(_state.filters);
_view = new MarketViewView { DataContext = _vm };
_view._grid.Sorting += on_grid_sorting;
- _vm.sorting_hierarchy = DIYGridSortHelper.convert_sorting_criterion(_state.sorting_criterion).ToList();
- _vm.SubscribeDisposed(
- DIYGridSortHelper.observe_active_comparers(_view._grid, MarketViewVM.get_column_comparer)
- .OnDispatcher()
- .Subscribe(sh => _vm.sorting_hierarchy = sh.Select(s => (s.key, s.direction)).ToList()));
+ _grid_settings_manager = new SimpleGridSettingsManager(_vm.columns, _col_settings_flags, _vm.rows);
+ _grid_settings_manager.load_from(_settings_manager.current_settings());
/// The 'CanExecute' plumbing for the cmds below is designed to avoid potentially costly .ToList() operations on the selected_rows/securities.
/// Some of the cmds post ITags with data that is, or has, a SecurityCriteria member; the SecurityCriteria constructor creates a new List of the specified securities.
/// For these commands, the objective is to evaluate 'CanExecute' without instantiating the SecurityCriteria (or more importantly, without creating a new List).
@@ -124,7 +138,7 @@ namespace mbts.spark {
var show_synth_create_actual_cmd = SparkCommands.ShowSyntheticCreate();
var show_synth_create_cmd_wrapper = new SparkCommandCommand(show_synth_create_actual_cmd.title, show_synth_create_actual_cmd.icon, show_synth_create_cmd, SparkCommands.KeyGestures.ShowSyntheticCreate);
- var update_securities_cmd = SparkCommands.change_securities_callback(_window_type_display_name);
+ var show_mini_ob_cmd = ObservantCommand.create(_view.show_mini_orderbook, s => s.HasValue, s => s.Value, out var show_mini_ob_observer);
// commands
var current_sec_md_src = Observable.Create(o => {
notify(null, null);
@@ -139,6 +153,7 @@ namespace mbts.spark {
_commands = new List {
SparkCommands.ShowLadders(SparkCommands.KeyGestures.ShowLadder).complete(_vm.selected_rows.Observe().Select(_ => _vm.selected_rows.WrapSelectorRO(r => r.security_md)), smds => smds.Count > 0),
SparkCommands.ShowOrderTicketFromSecurity(SparkCommands.KeyGestures.ShowOrderTicket).complete(current_sec_md_src, md => md.HasValue, ConvertUtils.ValueOrDefault),
+ new SparkCommandCommand("Working Orders", Icons.Orders, show_mini_ob_cmd),
show_fills_cmd_wrapper,
show_tas_cmd_wrapper,
show_synth_create_cmd_wrapper,
@@ -149,7 +164,7 @@ namespace mbts.spark {
_settings_manager,
us => us.market_view_grid,
UserSettings.FileExtensions.GRID,
- s => _vm.grid_settings_manager.create_vm(
+ s => _grid_settings_manager.create_vm(
// we need to get the most up to date GridSettings when launching b/c users may
// have changed some settings on the grid directly, like column widths.
s.with(_vm.columns.Select(c => c.create()))
@@ -163,8 +178,11 @@ namespace mbts.spark {
};
//
_vm.selected_rows.CollectionChanged += on_selection_changed;
- _subscriptions = new CompositeDisposable(_bus.subscribe(update_securities_cmd.tag).OnDispatcher().Subscribe(c =>
- _state_changed?.Invoke(_state = new MarketViewWindowFactory.StartupData(c, _vm.get_filter_states(), _vm.grid_settings_manager.create())))) {
+ _subscriptions = new CompositeDisposable(
+ current_sec_md_src.Subscribe(show_mini_ob_observer),
+ _settings_manager.subscribe_current().OnDispatcher().Subscribe(new CompositeObserver(_grid_settings_manager) {
+ s => _vm.selection_highlight = s.selection_highlight
+ })) {
() => {
_vm.selected_rows.CollectionChanged -= on_selection_changed;
_view._grid.Sorting -= on_grid_sorting;
diff --git a/src/client/spark/Modules/Market/MarketView/MarketViewWindowFactory.cs b/src/client/spark/Modules/Market/MarketView/MarketViewWindowFactory.cs
index cc77faafa..9d15abb2e 100644
--- a/src/client/spark/Modules/Market/MarketView/MarketViewWindowFactory.cs
+++ b/src/client/spark/Modules/Market/MarketView/MarketViewWindowFactory.cs
@@ -89,7 +89,19 @@ namespace bts.shell {
}
- public static GridSettings DefaultGridSettings => GridSettings.Default.with(DefaultColumns.all, DefaultColumns.default_sorting_criterion, enable_background_color: true);
+ public static GridSettings DefaultGridSettings =>
+ GridSettings.Default.with(DefaultColumns.all, DefaultColumns.default_sorting_criterion, enable_background_color: true);
+
+ public static fills.FillsSettings DefaultAuxiliaryFills {
+ get {
+ var col_keys = new HashSet(
+ fills.FillsSettings.mini_column_keys);
+ col_keys.Add(mbts.ui.spark.FillsView.Columns.Account);
+ var rv = fills.FillsSettings.Default();
+ rv.grid = fills.FillsSettings.create_grid(col_keys);
+ return rv;
+ }
+ }
public IReadOnlyList sorting_criterion => _sorting_criteria ?? (_sorting_criteria = DefaultColumns.default_sorting_criterion.ToList());
diff --git a/src/client/spark/Modules/Spreader/VM/SyntheticManagementDialogVM.cs b/src/client/spark/Modules/Spreader/VM/SyntheticManagementDialogVM.cs
index 1f63cb046..8792e94e9 100644
--- a/src/client/spark/Modules/Spreader/VM/SyntheticManagementDialogVM.cs
+++ b/src/client/spark/Modules/Spreader/VM/SyntheticManagementDialogVM.cs
@@ -47,13 +47,13 @@ namespace mbts.spark {
add_col(Columns.Name, DefaultColumnHeaders.Name, r => r.name, nameof(Row.name), name_filter, DataGridFilterKnownTemplates.Regex, DefaultColumnWidths.Security);
add_col(Columns.Description, DefaultColumnHeaders.Description, r => r.description, nameof(Row.description), description_filter, DataGridFilterKnownTemplates.Regex, DefaultColumnWidths.Description);
add_col(Columns.n_legs, DefaultColumnHeaders.N_Legs, r => r.n_legs.ToString(), nameof(Row.n_legs), n_legs_filter, DataGridFilterKnownTemplates.IntMinMax, DefaultColumnWidths.N_Legs);
- columns.Add(new HyperlinkElementColumn(
+ columns.Add(new HyperlinkElementColumn(
Columns.n_working_orders,
- new Binding(nameof(Row.n_working_orders)),
- new Binding(nameof(Row.show_order_book)),
n_working_orders_filter,
DataGridFilterKnownTemplates.UIntMinMax) {
id = col_id(Columns.n_working_orders),
+ CommandBinding = new Binding(nameof(Row.show_order_book)),
+ TextBinding = new Binding(nameof(Row.n_working_orders)),
Header = DefaultColumnHeaders.N_Working_Orders,
Width = DefaultColumnWidths.N_Working_Orders
});
diff --git a/src/client/spark/Modules/Trading/Fills/Settings/FillsSettings.cs b/src/client/spark/Modules/Trading/Fills/Settings/FillsSettings.cs
index d6a24363d..d059efcf7 100644
--- a/src/client/spark/Modules/Trading/Fills/Settings/FillsSettings.cs
+++ b/src/client/spark/Modules/Trading/Fills/Settings/FillsSettings.cs
@@ -1,4 +1,5 @@
-using mbts.ui;
+using bts.collections;
+using mbts.ui;
using mbts.ui.settings;
using ProtoBuf;
using System;
@@ -34,10 +35,26 @@ namespace bts.shell.fills {
public static GridSettings DefaultGridSettings => GridSettings.Default.with(DefaultColumns.all, DefaultColumns.default_sorting_criterion);
public static GridSettings MiniGridSettings => GridSettings.Default.with(DefaultColumns.Mini.all, DefaultColumns.Mini.sorting_criterion);
+ public static IEnumerable mini_column_keys => DefaultColumns.Mini.mini_cols_data.Keys;
+
public IReadOnlyList sorting_criterion => _sorting_criteria ?? (_sorting_criteria = DefaultColumns.default_sorting_criterion.ToList());
public static FillsSettings Default() =>
new FillsSettings(null, null, new FillsFilterStates(), null);
+ public static GridSettings create_grid(IEnumerable keys) {
+ var key_set = keys as ISet ?? new HashSet(keys.empty_if_null());
+ var grid = DefaultGridSettings;
+ var cols = new LazyMutatorList(grid.columns);
+ for (int i = 0; i < cols.Count; i++) {
+ var c = cols[i];
+ if (c.is_visible != key_set.Contains(c.strong_id()))
+ cols[i] = c.with(is_visible: !c.is_visible);
+ }
+ if (cols.IsMutated)
+ grid = grid.with(cols);
+ return grid;
+ }
+
static class DefaultColumns {
public static GridColumnSettings security => new GridColumnSettings(col_id(Columns.Security), DefaultColumnHeaders.Security, width: DefaultColumnWidths.Security);
public static GridColumnSettings symbol => new GridColumnSettings(col_id(Columns.Symbol), DefaultColumnHeaders.Symbol, width: DefaultColumnWidths.Symbol);
@@ -83,21 +100,21 @@ namespace bts.shell.fills {
}
public static class Mini {
- static readonly IReadOnlyDictionary _mini_cols_data =
+ public static readonly IReadOnlyDictionary mini_cols_data =
new Dictionary {
{ Columns.Security, (true, null) },
{ Columns.FillTime, (true, ShortColumnWidths.Time) },
{ Columns.OrderType, (true, ShortColumnWidths.OrderType) },
{ Columns.Side, (true, ShortColumnWidths.Side) },
{ Columns.Size, (true, ShortColumnWidths.Size) },
- { Columns.Price, (true, ShortColumnWidths.Price) },
+ { Columns.Price, (true, ShortColumnWidths.Price) }
};
public static IEnumerable all => DefaultColumns.all.Select(_convert);
public static IEnumerable sorting_criterion => default_sorting_criterion.Where(c => _try_get_data(c.col_name, out (bool is_visible, double? _) d) && d.is_visible);
static bool _try_get_data(string id_str, out (bool is_visible, double? width_override) data) {
- if (Enum.TryParse(id_str, out Columns k) && _mini_cols_data.TryGetValue(k, out data))
+ if (Enum.TryParse(id_str, out Columns k) && mini_cols_data.TryGetValue(k, out data))
return true;
data = default;
return false;
@@ -110,4 +127,4 @@ namespace bts.shell.fills {
}
}
}
-}
\ No newline at end of file
+}
diff --git a/src/client/spark/core/Commands/ObservantCommand.cs b/src/client/spark/core/Commands/ObservantCommand.cs
index ccf8d19c0..05b74014a 100644
--- a/src/client/spark/core/Commands/ObservantCommand.cs
+++ b/src/client/spark/core/Commands/ObservantCommand.cs
@@ -70,5 +70,21 @@ namespace mbts.ui {
}, () => Disposable.dispose(ref can_execute_sub));
return cmd;
}
+
+ public static ICommand create(Action execute, Func get_can_execute, Func convert, out IObserver observer) {
+ TResult param = default;
+ bool can_execute = false;
+ var cmd = new DelegateCommand(() => execute(param), () => can_execute);
+ IDisposable can_execute_sub = null;
+ observer = new AnonymousObserver(val => {
+ var could_execute = can_execute;
+ can_execute = get_can_execute(val);
+ if (can_execute)
+ param = convert(val);
+ if (can_execute != could_execute)
+ cmd.RaiseCanExecuteChanged();
+ }, () => Disposable.dispose(ref can_execute_sub));
+ return cmd;
+ }
}
}
\ No newline at end of file
diff --git a/src/client/spark/view/Controls/DIYGrid/CellRender/HyperlinkCell.cs b/src/client/spark/view/Controls/DIYGrid/CellRender/HyperlinkCell.cs
index 1f254988c..4d9e351e9 100644
--- a/src/client/spark/view/Controls/DIYGrid/CellRender/HyperlinkCell.cs
+++ b/src/client/spark/view/Controls/DIYGrid/CellRender/HyperlinkCell.cs
@@ -74,6 +74,7 @@ namespace mbts.ui.controls {
_border_stroke_pen_backing;
static HyperlinkCell() {
+ DefaultStyleKeyProperty.OverrideMetadata(typeof(HyperlinkCell), new FrameworkPropertyMetadata(typeof(HyperlinkCell)));
IsMouseOverClickAreaProperty = IsMouseOverClickAreaPropertyKey.DependencyProperty;
WPFHelper.override_metadata(
HorizontalAlignmentProperty,
diff --git a/src/client/spark/view/Controls/DIYGrid/Columns/FrameworkElementColumns/HyperlinkColumn.cs b/src/client/spark/view/Controls/DIYGrid/Columns/FrameworkElementColumns/HyperlinkColumn.cs
index 1ed7eabb9..d93c6377e 100644
--- a/src/client/spark/view/Controls/DIYGrid/Columns/FrameworkElementColumns/HyperlinkColumn.cs
+++ b/src/client/spark/view/Controls/DIYGrid/Columns/FrameworkElementColumns/HyperlinkColumn.cs
@@ -1,41 +1,52 @@
using mbts.spark.filters;
using mbts.ui.converters;
+using System;
using System.Windows;
using System.Windows.Data;
using System.Windows.Input;
+using System.Windows.Media;
namespace mbts.ui.controls {
partial class DIYGrid {
- internal class HyperlinkElementColumn : FrameworkElementColumnBase, IFrameworkElementColumn, IFilterableColumn, IKeyedColumn {
+ internal class HyperlinkElementColumn : ColumnBase, IFrameworkElementColumn, IFilterableColumn, IKeyedColumn {
static readonly Thickness _default_padding = new Thickness(6, 0, 6, 0);
- readonly BindingBase _binding, _command_binding;
- public HyperlinkElementColumn(
- TKey key, BindingBase binding, BindingBase command_binding, IDataGridColumnFilter filter, string filter_template) : base(key) {
+ public HyperlinkElementColumn(TKey key, IDataGridColumnFilter filter, string filter_template)
+ : base(key) {
this.filter = filter;
this.filter_template = filter_template;
- _binding = binding;
- _command_binding = command_binding;
}
- public IDataGridColumnFilter filter { get; }
- public string filter_template { get; }
- public Thickness padding { get; set; } = _default_padding;
- public HyperlinkCellClickMode HyperlinkClickMode { get; set; }
+ public event Action view_generated;
- public override FrameworkElement view_factory() {
+ public Style ElementStyle { get; set; }
+ public IDataGridColumnFilter filter { get; set; }
+ public string filter_template { get; set; }
+ public HyperlinkCellClickMode HyperlinkClickMode { get; set; }
+ public Thickness padding { get; set; } = _default_padding;
+ // bindings
+ public BindingBase CommandBinding { get; set; }
+ public BindingBase CommandParameterBinding { get; set; }
+ public BindingBase TextBinding { get; set; }
+
+ public FrameworkElement view_factory() {
var hl = new HyperlinkCell {
HyperlinkClickMode = HyperlinkClickMode,
Padding = padding
};
+ hl.SetCurrentValue(DIYGrid.ColumnProperty, this);
+ // bindings
+ if (TextBinding != null)
+ hl.SetBinding(HyperlinkCell.TextProperty, TextBinding);
+ if (CommandBinding != null)
+ hl.SetBinding(HyperlinkCell.CommandProperty, CommandBinding);
+ if (CommandParameterBinding != null)
+ hl.SetBinding(HyperlinkCell.CommandParameterProperty, CommandParameterBinding);
hl.SetBinding(HyperlinkCell.ForegroundProperty, new Binding(nameof(font_brush)) { Source = this });
- hl.SetBinding(HyperlinkCell.TextProperty, _binding);
hl.SetBinding(HyperlinkCell.FontFamilyProperty, new Binding(nameof(font_family)) { Source = this });
hl.SetBinding(HyperlinkCell.FontSizeProperty, new Binding(nameof(font_size)) { Source = this });
hl.SetBinding(HyperlinkCell.HorizontalContentAlignmentProperty, new Binding(nameof(horizontal_alignment)) { Source = this });
- hl.SetBinding(HyperlinkCell.CommandProperty, _command_binding);
- hl.SetBinding(HyperlinkCell.WidthProperty, new Binding(nameof(ActualWidth)) { Source = this });
- hl.SetBinding(HyperlinkCell.BackgroundProperty, new Binding(nameof(background_brush)) { Source = this });
+ hl.SetBinding(FrameworkElement.WidthProperty, new Binding(nameof(ActualWidth)) { Source = this });
hl.SetBinding(
CursorProperty,
new Binding {
@@ -46,6 +57,14 @@ namespace mbts.ui.controls {
ValueForTrue = Cursors.Hand
}
});
+ // background
+ var bg_brush = new SolidColorBrush();
+ BindingOperations.SetBinding(bg_brush, SolidColorBrush.ColorProperty, new Binding(nameof(background_color)) { Source = this });
+ hl.SetCurrentValue(HyperlinkCell.BackgroundProperty, bg_brush);
+ // style
+ if (ElementStyle != null)
+ hl.Style = ElementStyle;
+ view_generated?.Invoke(hl);
return hl;
}
}
diff --git a/src/client/spark/view/Controls/DIYGrid/DIYGrid.cs b/src/client/spark/view/Controls/DIYGrid/DIYGrid.cs
index 09c798b6c..7e2f0079c 100644
--- a/src/client/spark/view/Controls/DIYGrid/DIYGrid.cs
+++ b/src/client/spark/view/Controls/DIYGrid/DIYGrid.cs
@@ -269,7 +269,7 @@ namespace mbts.ui.controls {
_rsa = new RowSelectionAdorner(this, _scroll_viewer);
_rsa.bind(RowSelectionAdorner.FillProperty, _highlight_path, DIYGrid.SelectionHighlightBrushConverter, _grid);
_rsa.bind(RowSelectionAdorner.StrokeProperty, _highlight_path, DIYGrid.SelectionHighlightPenConverter, _grid);
- _rsa.bind(OpacityProperty, _active_path, DIYGrid.SelectionHighlightOpacityConverter, _grid);
+ _rsa.bind(OpacityProperty, _active_path, DIYGrid.SelectionHighlightOpacityConverter, this);
_rsa.attach();
// cell hover adorner
_cell_hover_adorner = new DIYGridCellHoverAdorner(this, _scroll_viewer);
@@ -924,6 +924,14 @@ namespace mbts.ui.controls {
// Public Properties
#region Public Properties
+ #region Column
+ public static readonly DependencyProperty ColumnProperty =
+ DependencyProperty.RegisterAttached("Column", typeof(ColumnBase), typeof(DIYGrid));
+
+ public static ColumnBase GetColumn(DependencyObject d) => d.GetValue(ColumnProperty) as ColumnBase;
+ public static void SetColumn(DependencyObject d, ColumnBase value) => d.SetValue(ColumnProperty, value);
+ #endregion
+
#region ColumnKey
public static readonly DependencyProperty ColumnKeyProperty =
DependencyProperty.RegisterAttached("ColumnKey",
diff --git a/src/client/spark/view/Controls/Order/OrderTicketResources.xaml b/src/client/spark/view/Controls/Order/OrderTicketResources.xaml
new file mode 100644
index 000000000..92f3748cc
--- /dev/null
+++ b/src/client/spark/view/Controls/Order/OrderTicketResources.xaml
@@ -0,0 +1,144 @@
+
+
+
+
+
+ 8.0
+ 4.0
+ 22.0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/client/spark/view/Controls/Order/OrderTicketResources.xaml.cs b/src/client/spark/view/Controls/Order/OrderTicketResources.xaml.cs
new file mode 100644
index 000000000..e2cb0fec2
--- /dev/null
+++ b/src/client/spark/view/Controls/Order/OrderTicketResources.xaml.cs
@@ -0,0 +1,10 @@
+using mbts.ui.utility;
+using System.Windows;
+
+namespace mbts.ui.spark {
+ partial class OrderTicketResources : ResourceDictionary {
+ public OrderTicketResources() {
+ InitializeComponent();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/client/spark/view/Converters/ColorBlendConverter.cs b/src/client/spark/view/Converters/ColorBlendConverter.cs
new file mode 100644
index 000000000..5f337d0ee
--- /dev/null
+++ b/src/client/spark/view/Converters/ColorBlendConverter.cs
@@ -0,0 +1,37 @@
+using System;
+using System.Globalization;
+using System.Linq;
+using System.Windows.Data;
+using System.Windows.Media;
+
+namespace mbts.ui.converters {
+ class ColorBlendConverter : IValueConverter {
+ public Color Overlay { get; set; }
+
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture) =>
+ value is Color color ? color.blend(Overlay) : Overlay;
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new NotImplementedException();
+ }
+
+ class ColorWithConverter : IValueConverter {
+ public byte? A { get; set; }
+ public byte? R { get; set; }
+ public byte? G { get; set; }
+ public byte? B { get; set; }
+
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture) =>
+ value is Color c ? c.with(A, R, G, B) : value;
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new NotImplementedException();
+ }
+
+ class ColorBlendMultiConverter : IMultiValueConverter {
+ public static ColorBlendMultiConverter Instance => new ColorBlendMultiConverter();
+
+ public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) =>
+ values.OfType().Aggregate(Colors.Transparent, WPFExtensions.blend);
+
+ public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) => throw new NotImplementedException();
+ }
+}
\ No newline at end of file
diff --git a/src/client/spark/view/DragDrop/DragDropHelper.cs b/src/client/spark/view/DragDrop/DragDropHelper.cs
index 17bb79633..92f670c24 100644
--- a/src/client/spark/view/DragDrop/DragDropHelper.cs
+++ b/src/client/spark/view/DragDrop/DragDropHelper.cs
@@ -72,6 +72,7 @@ namespace mbts.ui {
public static IObservable subscribe_drag_coordinates(
FrameworkElement element,
FrameworkElement init_element = null,
+ Rect? drag_start_bounds = null,
bool use_preview_events = false,
MouseButton mouse_button = MouseButton.Left) {
if (element == null)
@@ -80,7 +81,6 @@ namespace mbts.ui {
init_element = element;
return Observable.Create(o => {
Point? mouse_down = null, drag_start = null;
- bool is_explict_drag = false;
if (use_preview_events) {
init_element.PreviewMouseDown += on_mouse_down;
element.PreviewMouseMove += on_mouse_move;
@@ -104,7 +104,6 @@ namespace mbts.ui {
// callbacks
void on_mouse_up(object sender, MouseButtonEventArgs e) {
mouse_down = null;
- is_explict_drag = false;
if (drag_start.HasValue)
o.OnNext(drag_start = null);
}
@@ -118,15 +117,15 @@ namespace mbts.ui {
e.Handled = true;
drag_start = curr_pt;
mouse_down = null;
- is_explict_drag = false;
o.OnNext(curr_pt);
}
}
}
void on_mouse_down(object sender, MouseButtonEventArgs e) {
- if (!is_explict_drag) {
+ var pt = e.GetPosition(init_element);
+ if (!drag_start_bounds.HasValue || drag_start_bounds.Value.Contains(pt)) {
drag_start = null;
- mouse_down = e.GetPosition(element);
+ mouse_down = pt;
}
}
});
diff --git a/src/client/spark/view/Extensions/FrameworkElementExtensions.cs b/src/client/spark/view/Extensions/FrameworkElementExtensions.cs
index bf6a56292..5a656e916 100644
--- a/src/client/spark/view/Extensions/FrameworkElementExtensions.cs
+++ b/src/client/spark/view/Extensions/FrameworkElementExtensions.cs
@@ -146,10 +146,14 @@ namespace mbts.ui.extensions {
element.Loaded += on_loaded;
if (disconnect_on_unloaded)
element.Unloaded += on_unloaded;
+ if (element.IsLoaded)
+ connection = connect();
return Disposable.Create(dispose);
- void on_loaded(object sender, RoutedEventArgs e) =>
- Disposable.replace(ref connection, connect());
+ void on_loaded(object sender, RoutedEventArgs e) {
+ connection?.Dispose();
+ connection = connect();
+ }
void on_unloaded(object sender, RoutedEventArgs e) =>
Disposable.dispose(ref connection);
diff --git a/src/client/spark/view/Extensions/WPFExtensions.cs b/src/client/spark/view/Extensions/WPFExtensions.cs
index 08cfd803e..d076e4628 100644
--- a/src/client/spark/view/Extensions/WPFExtensions.cs
+++ b/src/client/spark/view/Extensions/WPFExtensions.cs
@@ -132,8 +132,23 @@ namespace mbts.ui {
return brush;
}
- public static Color with(this Color color, byte alpha) =>
- Color.FromArgb(alpha, color.R, color.G, color.B);
+ public static Color blend(this Color color, Color other) {
+ // it doesn't make sense to avg the Alpha value (layering transparent over an opaque color should yield the opaque color).
+ // just apply the max Alpha to the resultant color
+ if (color.A == 0)
+ return other;
+ if (other.A == 0)
+ return color;
+ var color_opac = color.A / 255.0;
+ var other_opac = other.A / 255.0;
+ var avg_red = (byte)new[] { (color.R, color.A), (other.R, other.A) }.weighted_average(tup => tup.R, tup => tup.A);
+ var avg_green = (byte)new[] { (color.G, color.A), (other.G, other.A) }.weighted_average(tup => tup.G, tup => tup.A);
+ var avg_blue = (byte)new[] { (color.B, color.A), (other.B, other.A) }.weighted_average(tup => tup.B, tup => tup.A);
+ return Color.FromArgb(Math.Max(color.A, other.A), avg_red, avg_green, avg_blue);
+ }
+
+ public static Color with(this Color color, byte? alpha = null, byte? red = null, byte? green = null, byte? blue = null) =>
+ Color.FromArgb(alpha ?? color.A, red ?? color.R, green ?? color.G, blue ?? color.B);
public static SolidColorBrush with(this SolidColorBrush brush, double opacity) {
brush.Opacity = opacity;
diff --git a/src/client/spark/view/Resources/Buttons.xaml b/src/client/spark/view/Resources/Buttons.xaml
index c7e524120..438b153cf 100644
--- a/src/client/spark/view/Resources/Buttons.xaml
+++ b/src/client/spark/view/Resources/Buttons.xaml
@@ -595,6 +595,54 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+