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 @@ + + + + + + + + + + + + + + + + + + + + + + + +