SP-257 mview popups for working orders & day fills

This commit is contained in:
John Colvin committed 2019-12-09 14:56:11 -06:00
1 parent 7875b939e6
commit 2f59900a69
22 files changed
+1061 -166

No files matched your search

@@ -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;
}
}
}
@@ -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 {
/// <summary>
/// Viewmodel class designed for <see cref="ui.spark.MarketViewView"/>
/// </summary>
partial class MarketViewVM : DisposableViewModelBase, IHandleRowSelection {
static readonly IReadOnlyList<Func<MarketViewVM, IDataGridColumnFilter<Row>>> _filter_getters =
ObjectSnapshot.property_delegates_of_type<MarketViewVM, IDataGridColumnFilter<Row>>().ToReadOnlyList();
@@ -43,16 +51,30 @@ namespace mbts.spark {
readonly DeferrableActionWrapper _refresh_filter;
Func<Row, bool> _filter;
IReadOnlyList<(Columns key, ListSortDirection direction)> _sorting_hierarchy;
IWindowSettingsManager<GridSettings> _settings_manager;
Color _selection_highlight;
public MarketViewVM(IBus bus, OrdersModel orders_model, IObservable<StartupData> state, IWindowSettingsManager<GridSettings> settings_manager) {
/// <summary>
/// Initializes an instance of the <see cref="ui.spark.MarketViewView"/> class.
/// </summary>
/// <param name="bus"></param>
/// <param name="orders_model"></param>
/// <param name="security_mds_source"></param>
/// <param name="use_mouse_state_transitions">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).</param>
public MarketViewVM(
IBus bus,
OrdersModel orders_model,
IObservable<TableUpdate<SecurityMD, uint>> security_mds_source,
bool use_mouse_state_transitions = false) {
_bus = bus;
_orders_model = orders_model;
security_filter = new RegexFilterVM<Row>(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<Row>(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<double, string>(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<Row, string> 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<Row, Columns>(col_key, get_text, filter, filter_template) {
RenderTextColumn<Row> add_col(Columns col_key, string header, Func<Row, string> 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<Row, Columns>(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<Columns> 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<Row, Columns>(
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<Columns> 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<Columns>(
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<string, GridColumnSettingsFeatureFlags> {
{ 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<Row>(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<Row> 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<Row> rows => _rows;
public ObservableCollection<Row> selected_rows { get; } = new ObservableCollection<Row>();
public Color selection_highlight { get => _selection_highlight; set => Set(ref _selection_highlight, value); }
public ObservableCollection<ColumnBase> columns { get; } = new ObservableCollection<ColumnBase>();
public Row current_row { get => _current_row.Value; set => _current_row.set(value); }
public SimpleGridSettingsManager<Row> 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<T>.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<GridSettings>.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<OrderBookGridView.ColumnKeys> columns) =>
grid.with(_hide_cols(grid.columns, columns));
static IEnumerable<GridColumnSettings> _hide_cols(IEnumerable<GridColumnSettings> cols, IEnumerable<OrderBookGridView.ColumnKeys> columns) {
// hide columns who's data is implied by the grouping
var col_hash = new HashSet<string>(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<OrderBookGridView.ColumnKeys> hide_cols, IObservable<TableUpdate<OrderSecurityRootPair, uint>> 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<bts.shell.orderbook.OrderBookGridSettings>.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>(), _row_key_comparer);
selected_rows.InsertSorted(e.AddedItems.OfType<Row>(), _row_key_comparer);
}
void _on_securities(TableUpdate<SecurityMD, uint> 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.");
}
}
}
@@ -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 @@
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="/view/Controls/DataGridEx/Filters/Views/FilterTemplates.xaml"/>
</ResourceDictionary.MergedDictionaries>
<Style TargetType="c:HyperlinkCell">
<Setter Property="Background" Value="{Binding Path='(c:DIYGrid.Column).background_brush', RelativeSource={RelativeSource Self}}"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Command" Value="{x:Static usp:MarketViewView.CellClickCommand}"/>
<Setter Property="CommandParameter">
<Setter.Value>
<MultiBinding Converter="{x:Static usp:MarketViewView.CellConverter}">
<Binding Path=""/>
<Binding Path="(c:DIYGrid.ColumnKey)" RelativeSource="{RelativeSource Self}"/>
</MultiBinding>
</Setter.Value>
</Setter>
<Setter Property="CommandTarget" Value="{Binding RelativeSource={RelativeSource AncestorType=usp:MarketViewView}}"/>
<Style.Triggers>
<Trigger Property="IsMouseOverClickArea" Value="True">
<Setter Property="Background">
<Setter.Value>
<SolidColorBrush>
<SolidColorBrush.Color>
<MultiBinding Converter="{x:Static cv:ColorBlendMultiConverter.Instance}">
<Binding Path="SelectionHighlight" RelativeSource="{RelativeSource AncestorType=c:DIYGrid}"/>
<Binding Path="(c:DIYGrid.Column).background_color" RelativeSource="{RelativeSource AncestorType=c:HyperlinkCell}"/>
</MultiBinding>
</SolidColorBrush.Color>
</SolidColorBrush>
</Setter.Value>
</Setter>
<Setter Property="BorderBrush">
<Setter.Value>
<Binding Path="SelectionHighlight" RelativeSource="{RelativeSource AncestorType=c:DIYGrid}">
<Binding.Converter>
<cv:ValueConverterPipe>
<cv:ColorWithConverter A="255"/>
<cv:ColorToBrushConverter/>
</cv:ValueConverterPipe>
</Binding.Converter>
</Binding>
</Setter.Value>
</Setter>
<Setter Property="BorderThickness" Value="1"/>
</Trigger>
</Style.Triggers>
</Style>
</ResourceDictionary>
</UserControl.Resources>
<UserControl.CommandBindings>
<CommandBinding Command="{x:Static usp:MarketViewView.CellClickCommand}" Executed="{x:Static usp:MarketViewView.CellClickExecuted}"/>
</UserControl.CommandBindings>
<AdornerDecorator>
<Grid>
<Grid.RowDefinitions>
@@ -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"
>
<c:DIYGrid.InputBindings>
@@ -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 {
/// <summary>
/// Interaction logic for MarketSheetsView.xaml
/// </summary>
partial class MarketViewView : UserControl {
public static readonly RoutedCommand CellClickCommand = new RoutedCommand();
MarketViewVM _vm;
DIYGridPopupManager _popup_man_backing;
public MarketViewView() {
InitializeComponent();
this.ConnectDataContext<MarketViewVM>(vm => _vm = vm);
AdornerHelper.attach_continuously_on_loaded(
() => new GridAnimation.DIYGridAnimationAdorner<Columns>(
_grid, _grid.descendants_of_type<DIYGridRowsPresenter>().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<Columns>(
_grid, _grid.descendants_of_type<DIYGridRowsPresenter>().First()),
_grid);
public static ExecutedRoutedEventHandler
CellClickExecuted => CommandImpl.cell_click;
public static IMultiValueConverter CellConverter =>
ValueConverter.create<MarketViewVM.Row, Columns, MarketViewCell>((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;
}
}
}
@@ -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<MarketViewWindowFactory.StartupData>, 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<string, GridColumnSettingsFeatureFlags> _col_settings_flags = new Dictionary<string, GridColumnSettingsFeatureFlags> {
{ 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<GridSettings> _settings_manager;
@@ -33,6 +41,7 @@ namespace mbts.spark {
SecurityCriteria
_selected_security_criteria_backing,
_sorted_selected_security_criteria_backing;
SimpleGridSettingsManager<MarketViewVM.Row> _grid_settings_manager;
public MarketViewWindow(MarketViewWindowFactory factory, IBus bus, OrdersModel orders_model, MarketViewWindowFactory.StartupData state, IWindowSettingsManager<GridSettings> settings_manager)
: base("Market View", bus, factory, SparkCommands.ShowMarketView.icon) {
@@ -43,8 +52,6 @@ namespace mbts.spark {
thread_id = config.Configuration.get<bts.config.MultiThreadedWindows>().market_view ? "MarketView" : null;
}
event Action<MarketViewWindowFactory.StartupData> _state_changed;
public override string window_type => _window_type;
public override UIElement Content => _view;
public override IEnumerable<SparkCommand> 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<Action<MarketViewWindowFactory.StartupData>, 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<MarketViewView.Columns>(_state.sorting_criterion).ToList();
_vm.SubscribeDisposed(
DIYGridSortHelper.observe_active_comparers<MarketViewView.Columns, MarketViewVM.Row>(_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<MarketViewVM.Row>(_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<SecurityMD?, SecurityMD>(_view.show_mini_orderbook, s => s.HasValue, s => s.Value, out var show_mini_ob_observer);
// commands
var current_sec_md_src = Observable.Create<SecurityMD?>(o => {
notify(null, null);
@@ -139,6 +153,7 @@ namespace mbts.spark {
_commands = new List<SparkCommand> {
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<GridSettings>(_grid_settings_manager) {
s => _vm.selection_highlight = s.selection_highlight
})) {
() => {
_vm.selected_rows.CollectionChanged -= on_selection_changed;
_view._grid.Sorting -= on_grid_sorting;
@@ -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<mbts.ui.spark.FillsView.Columns>(
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<SortingCriteria> sorting_criterion => _sorting_criteria ?? (_sorting_criteria = DefaultColumns.default_sorting_criterion.ToList());
@@ -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<Row, Columns>(
columns.Add(new HyperlinkElementColumn<Columns>(
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
});
@@ -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<Columns> mini_column_keys => DefaultColumns.Mini.mini_cols_data.Keys;
public IReadOnlyList<SortingCriteria> 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<Columns> keys) {
var key_set = keys as ISet<Columns> ?? new HashSet<Columns>(keys.empty_if_null());
var grid = DefaultGridSettings;
var cols = new LazyMutatorList<GridColumnSettings>(grid.columns);
for (int i = 0; i < cols.Count; i++) {
var c = cols[i];
if (c.is_visible != key_set.Contains(c.strong_id<Columns>()))
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<Columns, (bool is_visible, double? width_override)> _mini_cols_data =
public static readonly IReadOnlyDictionary<Columns, (bool is_visible, double? width_override)> mini_cols_data =
new Dictionary<Columns, (bool is_visible, double? width_override)> {
{ 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<GridColumnSettings> all => DefaultColumns.all.Select(_convert);
public static IEnumerable<SortingCriteria> 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 {
}
}
}
}
}
@@ -70,5 +70,21 @@ namespace mbts.ui {
}, () => Disposable.dispose(ref can_execute_sub));
return cmd;
}
public static ICommand create<T, TResult>(Action<TResult> execute, Func<T, bool> get_can_execute, Func<T, TResult> convert, out IObserver<T> observer) {
TResult param = default;
bool can_execute = false;
var cmd = new DelegateCommand(() => execute(param), () => can_execute);
IDisposable can_execute_sub = null;
observer = new AnonymousObserver<T>(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;
}
}
}
@@ -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<HorizontalAlignment, HyperlinkCell>(
HorizontalAlignmentProperty,
@@ -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<TRowVM, TKey> : FrameworkElementColumnBase<TKey>, IFrameworkElementColumn, IFilterableColumn, IKeyedColumn<TKey> {
internal class HyperlinkElementColumn<TKey> : ColumnBase<TKey>, IFrameworkElementColumn, IFilterableColumn, IKeyedColumn<TKey> {
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<HyperlinkCell> 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;
}
}
@@ -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",
@@ -0,0 +1,144 @@
<ResourceDictionary
x:Class="mbts.ui.spark.OrderTicketResources"
x:ClassModifier="internal"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:msys="clr-namespace:bts.api;assembly=spark_api"
xmlns:sp="clr-namespace:mbts.spark"
xmlns:u="clr-namespace:mbts.ui"
xmlns:usp="clr-namespace:mbts.ui.spark"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="/view/Resources/Buttons.xaml"/>
<ResourceDictionary Source="/view/Resources/Converters.xaml"/>
</ResourceDictionary.MergedDictionaries>
<sys:Double x:Key="HorizontalSpacing">8.0</sys:Double>
<sys:Double x:Key="VerticalSpacing">4.0</sys:Double>
<sys:Double x:Key="ButtonHeight">22.0</sys:Double>
<Style x:Key="SellButton" TargetType="Button" BasedOn="{StaticResource {x:Type Button}}">
<Setter Property="Background" Value="{u:BTSColor MarketData.ask}"/>
<Setter Property="BorderBrush" Value="{u:BTSColor UI.Button.normal_border}"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="MinHeight" Value="40"/>
<Setter Property="FontWeight" Value="Bold"/>
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<Setter Property="VerticalAlignment" Value="Stretch"/>
<Setter Property="Content">
<Setter.Value>
<TextBlock VerticalAlignment="Center" Foreground="{u:BTSColor UI.alternate_text}">Sell</TextBlock>
</Setter.Value>
</Setter>
<Setter Property="Template" Value="{StaticResource SellButtonTemplate}"/>
</Style>
<Style x:Key="BuyButton" TargetType="Button" BasedOn="{StaticResource {x:Type Button}}">
<Setter Property="Background" Value="{u:BTSColor MarketData.bid}"/>
<Setter Property="BorderBrush" Value="{u:BTSColor UI.Button.normal_border}"/>
<Setter Property="Padding" Value="2"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="MinHeight" Value="40"/>
<Setter Property="FontWeight" Value="Bold"/>
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<Setter Property="VerticalAlignment" Value="Stretch"/>
<Setter Property="Content">
<Setter.Value>
<TextBlock VerticalAlignment="Center" Foreground="{u:BTSColor UI.alternate_text}">Buy</TextBlock>
</Setter.Value>
</Setter>
<Setter Property="Template" Value="{StaticResource BuyButtonTemplate}"/>
</Style>
<DataTemplate DataType="{x:Type sp:BBOVM}">
<TextBlock Margin="2">
<TextBlock.Inlines>
<Run Text="{Binding security, Mode=OneWay}" FontWeight="Bold"/>
<Run Foreground="{u:BTSColor MarketData.bid}" FontSize="10" Text="{Binding bid_size, Mode=OneWay, StringFormat='{}({0})'}"/>
<Run Foreground="{u:BTSColor MarketData.bid}" FontWeight="Bold">
<Run.Text>
<MultiBinding Converter="{StaticResource TickDisplayConverter}" Mode="OneWay">
<Binding Path="bid"/>
<Binding Path="security.ContractSpec"/>
</MultiBinding>
</Run.Text>
</Run>
<Run Foreground="{u:BTSColor MarketData.ask}" FontWeight="Bold">
<Run.Text>
<MultiBinding Converter="{StaticResource TickDisplayConverter}" Mode="OneWay">
<Binding Path="ask"/>
<Binding Path="security.ContractSpec"/>
</MultiBinding>
</Run.Text>
</Run>
<Run Foreground="{u:BTSColor MarketData.ask}" FontSize="10" Text="{Binding ask_size, Mode=OneWay, StringFormat='{}({0})'}"/>
</TextBlock.Inlines>
</TextBlock>
</DataTemplate>
<DataTemplate DataType="{x:Type sp:NewOrderTicketVM}">
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="{Binding Source={StaticResource VerticalSpacing}}"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition MinWidth="120"/>
<ColumnDefinition Width="{Binding Source={StaticResource HorizontalSpacing}}" MaxWidth="{Binding Source={StaticResource HorizontalSpacing}}"/>
<ColumnDefinition MinWidth="120"/>
</Grid.ColumnDefinitions>
<usp:OrderModificationControl Grid.ColumnSpan="3"/>
<Button Grid.Row="2" Style="{StaticResource BuyButton}" Command="{Binding send_order}" CommandParameter="{u:Enum Buy, Type=msys:Side}"/>
<Button Grid.Row="2" Grid.Column="2" Style="{StaticResource SellButton}" Command="{Binding send_order}" CommandParameter="{u:Enum Sell, Type=msys:Side}"/>
</Grid>
</DataTemplate>
<DataTemplate DataType="{x:Type sp:ExistingOrderTicketVM}">
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="{Binding Source={StaticResource VerticalSpacing}}"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition MinWidth="120"/>
<ColumnDefinition Width="{Binding Source={StaticResource HorizontalSpacing}}"/>
<ColumnDefinition MinWidth="120"/>
</Grid.ColumnDefinitions>
<usp:OrderModificationControl Grid.ColumnSpan="3"/>
<Button
Grid.Row="2"
Command="{Binding modify_order}"
IsDefault="True"
MinHeight="40"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
>
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock HorizontalAlignment="Center" FontWeight="Bold">OK</TextBlock>
<TextBlock
Grid.Row="1" HorizontalAlignment="Center" Margin="0,2,0,0" FontSize="9" Text="{Binding dirty_items_text}"
Visibility="{Binding dirty_items_text, Converter={StaticResource DirtyItemsVisibilityConverter}}"
/>
</Grid>
</Button>
<Button
Grid.Row="2"
Grid.Column="2"
Command="ApplicationCommands.Close"
FontWeight="Bold"
IsCancel="True"
MinHeight="40"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
>
Cancel
</Button>
</Grid>
</DataTemplate>
</ResourceDictionary>
@@ -0,0 +1,10 @@
using mbts.ui.utility;
using System.Windows;
namespace mbts.ui.spark {
partial class OrderTicketResources : ResourceDictionary {
public OrderTicketResources() {
InitializeComponent();
}
}
}
@@ -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<Color>().Aggregate(Colors.Transparent, WPFExtensions.blend);
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) => throw new NotImplementedException();
}
}
@@ -72,6 +72,7 @@ namespace mbts.ui {
public static IObservable<Point?> 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<Point?>(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;
}
}
});
@@ -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);
@@ -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;
@@ -595,6 +595,54 @@
</Style.Triggers>
</Style>
<ControlTemplate x:Key="BuyButtonTemplate" TargetType="Button">
<Border
x:Name="Border"
BorderThickness="{TemplateBinding BorderThickness}"
BorderBrush="{TemplateBinding BorderBrush}"
Background="{TemplateBinding Background}"
Padding="{TemplateBinding Padding}"
SnapsToDevicePixels="True"
>
<ContentPresenter
Name="button_content"
HorizontalAlignment="Center"
VerticalAlignment="Center"
RecognizesAccessKey="True"
/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Border" Property="Background" Value="#99167ed3"/>
<Setter TargetName="Border" Property="BorderBrush" Value="{mbts:BTSColor UI.Button.mouseover_border}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
<ControlTemplate x:Key="SellButtonTemplate" TargetType="Button">
<Border
x:Name="Border"
BorderThickness="{TemplateBinding BorderThickness}"
BorderBrush="{TemplateBinding BorderBrush}"
Background="{TemplateBinding Background}"
Padding="{TemplateBinding Padding}"
SnapsToDevicePixels="True"
>
<ContentPresenter
Name="button_content"
HorizontalAlignment="Center"
VerticalAlignment="Center"
RecognizesAccessKey="True"
/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Border" Property="Background" Value="#99c00000" />
<Setter TargetName="Border" Property="BorderBrush" Value="{mbts:BTSColor UI.Button.mouseover_border}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
<Style x:Key="CommonButton" BasedOn="{StaticResource {x:Type Button}}" TargetType="{x:Type Button}">
<Setter Property="MinWidth" Value="60" />
<Setter Property="Margin" Value="4" />
@@ -0,0 +1,48 @@
using System;
using System.Windows;
using System.Windows.Data;
using System.Windows.Media;
using System.Windows.Media.Animation;
namespace mbts.ui {
static class VisualStateHelper {
public static VisualStateGroup create_group(string name, params VisualState[] states) {
var vsg = new VisualStateGroup { Name = name };
for (int i = 0; i < states.Length; i++)
vsg.States.Add(states[i]);
return vsg;
}
public static VisualState create_state(string name, params Timeline[] timelines) {
var sb = new Storyboard();
for (int i = 0; i < timelines.Length; i++)
sb.Children.Add(timelines[i]);
return new VisualState {
Name = name,
Storyboard = sb
};
}
public static ColorAnimation create_color_animation(UIElement target, PropertyPath target_path) {
var timeline = new ColorAnimation {
BeginTime = TimeSpan.Zero,
Duration = TimeSpan.Zero
};
Storyboard.SetTarget(timeline, target);
Storyboard.SetTargetProperty(timeline, target_path);
return timeline;
}
public static ColorAnimation create_color_animation(UIElement target, PropertyPath target_path, Color color) {
var timeline = create_color_animation(target, target_path);
timeline.To = color;
return timeline;
}
public static ColorAnimation create_color_animation(UIElement target, PropertyPath target_path, BindingBase color_binding) {
var timeline = create_color_animation(target, target_path);
BindingOperations.SetBinding(timeline, ColorAnimation.ToProperty, color_binding);
return timeline;
}
}
}
@@ -17,6 +17,28 @@ namespace mbts.ui {
static class WPFHelper {
public delegate void PropertyChangedCallback<TValue, TOwner>(TOwner owner, TValue old_value, TValue new_value);
public static CoerceValueCallback coerce_value_callback<T>(T fallback = default) =>
(d, base_value) => base_value is T v ? v : fallback;
public static CoerceValueCallback coerce_mutable_freezable<T>(T fallback = default) where T : Freezable {
Func<T> get_fallback;
if (fallback == null) {
get_fallback = () => null;
} else {
fallback.Freeze();
get_fallback = () => (T)fallback.Clone();
}
return coerce_mutable_freezable(get_fallback);
}
public static CoerceValueCallback coerce_mutable_freezable<T>(Func<T> get_fallback) where T : Freezable =>
(d, base_value) => {
var rv = base_value as T ?? get_fallback();
if (rv != null && rv.IsFrozen)
rv = (T)rv.Clone();
return rv;
};
public static DependencyProperty create_dp<TOwner>(string name, Action<TOwner, object> callback, object default_value = null) =>
DependencyProperty.Register(name, typeof(object), typeof(TOwner), new PropertyMetadata(default_value, (d, e) => callback(ConvertUtils.try_convert<TOwner>(d), e.NewValue)));
@@ -0,0 +1,84 @@
using System;
using System.Collections;
using System.Collections.Generic;
namespace bts.collections {
/// <summary>
/// Implements both <see cref="IReadOnlyList{T}"/> and <see cref="IList{T}"/>, as a wrapper around a source list.
/// When (not before) a mutation method of the latter interface is called (for the first time), the source list will be copied to a new instance of <see cref="List{T}"/>,
/// which will become the target of all subsequent calls to methods of either interface.
/// </summary>
/// <typeparam name="T">The type of elements in the list.</typeparam>
class LazyMutatorList<T> : IList<T>, IReadOnlyList<T> {
IReadOnlyList<T> _result;
List<T> _mutable_backing;
public LazyMutatorList(IReadOnlyList<T> source) =>
_result = source ?? throw new ArgumentNullException(nameof(source));
public T this[int index] {
get => _result[index];
set => _mutable[index] = value;
}
public int Count => _result.Count;
public bool IsReadOnly => false;
public bool IsMutated => _mutable_backing != null;
public IReadOnlyList<T> Result => _result;
public List<T> MutatedResult => _mutable_backing ?? throw new InvalidOperationException("List has not been mutated.");
IList<T> _mutable {
get {
if (_mutable_backing == null) {
_mutable_backing = new List<T>(_result.Count);
for (int i = 0; i < _result.Count; i++)
_mutable.Add(_result[i]);
_result = _mutable_backing;
}
return _mutable_backing;
}
}
public void Add(T item) =>
_mutable.Add(item);
public void Clear() =>
_mutable.Clear();
public bool Contains(T item) =>
IndexOf(item) >= 0;
public void CopyTo(T[] array, int arrayIndex) {
var r = _result;
for (int i = 0; i < r.Count; i++)
array[i + arrayIndex] = r[i];
}
public int IndexOf(T item) {
var r = _result;
for (int i = 0; i < r.Count; i++) {
if (EqualityComparer<T>.Default.Equals(r[i], item))
return i;
}
return -1;
}
public void Insert(int index, T item) =>
_mutable.Insert(index, item);
public bool Remove(T item) =>
_mutable.Remove(item);
public void RemoveAt(int index) =>
_mutable.RemoveAt(index);
public IEnumerator<T> GetEnumerator() =>
(_mutable_backing ?? _result).GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}