Compare commits
71
Commits
ls
...
mview-popups
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a2007d60c | ||
|
|
49d0eb812b | ||
|
|
ae49a8ed94 | ||
|
|
67a17a69eb | ||
|
|
9e74c0c4ae | ||
|
|
df5409453e | ||
|
|
f9b682b1f8 | ||
|
|
ae77d6f273 | ||
|
|
f62f6fde5e | ||
|
|
3109640723 | ||
|
|
a7755f7ee1 | ||
|
|
5ab6d6d8c0 | ||
|
|
a8e71de207 | ||
|
|
0254392a63 | ||
|
|
55cc76e202 | ||
|
|
af39a762cb | ||
|
|
3b0e47b598 | ||
|
|
03f7766e4e | ||
|
|
ecb5f4c781 | ||
|
|
f8e619d6c5 | ||
|
|
28496ec58f | ||
|
|
7a1a5f6db7 | ||
|
|
bbab947fe7 | ||
|
|
2337a9f012 | ||
|
|
455506ff7a | ||
|
|
4d15fdef8a | ||
|
|
7029b74916 | ||
|
|
63b887b610 | ||
|
|
4dac6dce7c | ||
|
|
3bc7afdec0 | ||
|
|
b2577fba64 | ||
|
|
3ec6192e03 | ||
|
|
a1b2b765aa | ||
|
|
37830213b3 | ||
|
|
4c3aefc98a | ||
|
|
23a07c444b | ||
|
|
a847afa216 | ||
|
|
931d670a99 | ||
|
|
65b1950a87 | ||
|
|
72712296fc | ||
|
|
8bf04a8fb6 | ||
|
|
64b4f6cae0 | ||
|
|
6749cf40fa | ||
|
|
8dc575ab8e | ||
|
|
32b46d57e3 | ||
|
|
4e439763a1 | ||
|
|
b678193576 | ||
|
|
dcc798b8c6 | ||
|
|
6eda42122a | ||
|
|
6a119231d2 | ||
|
|
737b6f7e30 | ||
|
|
adda65357e | ||
|
|
e643768176 | ||
|
|
606e69e583 | ||
|
|
b07d2640ee | ||
|
|
c11a69da72 | ||
|
|
1d0dc20063 | ||
|
|
61c4a28f89 | ||
|
|
0eec5b30bb | ||
|
|
803e890e84 | ||
|
|
8f6140dada | ||
|
|
4d8408fd7c | ||
|
|
7cca35373c | ||
|
|
85590d8d0d | ||
|
|
05aeb78026 | ||
|
|
f543a61758 | ||
|
|
ea850fcf59 | ||
|
|
8e93378b4e | ||
|
|
0dd2cb2593 | ||
|
|
5eb89afe09 | ||
|
|
c1f6cb9b35 |
No files matched your search
@@ -43,6 +43,11 @@ namespace bts.shell {
|
||||
void _show_order_window(Window window) {
|
||||
foreach (var kv in AlgoStudioModelUtils.get_dynamic_resources(_model.algo_model.ui_configuration))
|
||||
window.Resources[kv.Key] = kv.Value;
|
||||
// the dynamic algo 'SecurityKey' template has a ContractPickerPopup that expects the security map to be available as a DynamicResource.
|
||||
var sub = _bus.subscribe(SparkTags.SecuritiesTag)
|
||||
.OnDispatcher()
|
||||
.Subscribe(sec_map => window.Resources[ResourceKeys.Securities] = sec_map);
|
||||
window.Closed += (s, e) => Disposable.dispose(ref sub);
|
||||
window.Show(-200, -160, true);
|
||||
}
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ namespace mbts.ui {
|
||||
EditDocumentAltGeometryKey = nameof(EditDocumentAltGeometryKey),
|
||||
EditDocumentGeometryKey = nameof(EditDocumentGeometryKey),
|
||||
GearGeometryKey = nameof(GearGeometryKey),
|
||||
HamburgerMenuGeometryKey = nameof(HamburgerMenuGeometryKey),
|
||||
HornGeometryKey = nameof(HornGeometryKey),
|
||||
KeyGeometryKey = nameof(KeyGeometryKey),
|
||||
LetterAKey = nameof(LetterAKey),
|
||||
@@ -172,6 +173,7 @@ namespace mbts.ui {
|
||||
public static ImageSource FilterActive { get { return load_resource_and_freeze("FilterActive"); } }
|
||||
public static ImageSource FilterError { get { return load_resource_and_freeze("FilterError"); } }
|
||||
public static ImageSource Gear => create_icon(GearGeometryKey);
|
||||
public static ImageSource HamburgerMenu => create_icon(HamburgerMenuGeometryKey);
|
||||
public static ImageSource Horn => create_icon(HornGeometryKey);
|
||||
public static ImageSource Import => load_resource_and_freeze("Import");
|
||||
public static ImageSource Key => create_icon(KeyGeometryKey);
|
||||
|
||||
@@ -272,6 +272,10 @@
|
||||
M16.000015,9.6000061C12.500012,9.6000061 9.5999844,12.5 9.5999844,16 9.5999844,19.5 12.500012,22.399994 16.000015,22.399994 19.500019,22.399994 22.400015,19.5 22.400015,16 22.400015,12.5 19.500019,9.6000061 16.000015,9.6000061z M14.199995,0L17.800005,0C18.199999,0,18.599993,0.29998779,18.599993,0.79998779L18.599993,4.7999878C19.900013,5.1000061,21.000019,5.6000061,22.099997,6.2999878L25.000023,3.3999939C25.099999,3.2999878 25.300011,3.1999817 25.500025,3.1999817 25.700007,3.1999817 25.900019,3.2999878 26.000025,3.3999939L28.600033,6C28.900021,6.2999878,28.900021,6.7999878,28.600033,7.1000061L25.700007,10C26.400019,11.100006,26.900019,12.299988,27.200007,13.5L31.20001,13.5C31.600037,13.5,31.999998,13.799988,31.999998,14.299988L31.999998,17.899994C31.999998,18.299988,31.700012,18.699982,31.20001,18.699982L27.200007,18.699982C26.900019,20,26.400019,21.100006,25.700007,22.199982L28.600033,25.100006C28.700009,25.199982 28.799984,25.399994 28.799984,25.600006 28.799984,25.799988 28.700009,26 28.600033,26.100006L26.000025,28.600006C25.900019,28.699982 25.700007,28.799988 25.500025,28.799988 25.300011,28.799988 25.099999,28.699982 25.000023,28.600006L22.099997,25.699982C21.000019,26.399994,19.800007,26.899994,18.599993,27.199982L18.599993,31.199982C18.599993,31.600006,18.300005,32,17.800005,32L14.199995,32C13.8,32,13.400006,31.699982,13.400006,31.199982L13.400006,27.199982C12.099987,26.899994,11.000011,26.399994,9.9000027,25.699982L7.0000064,28.600006C6.9000003,28.699982 6.6999881,28.799988 6.5000059,28.799988 6.2999937,28.799988 6.099981,28.699982 6.0000054,28.600006L3.3999967,26C3.0999784,25.699982,3.0999784,25.199982,3.3999967,24.899994L6.2999937,22C5.5999805,20.899994,5.0999801,19.699982,4.7999923,18.5L0.79998828,18.5C0.39999409,18.5,-1.933513E-07,18.199982,0,17.699982L0,14.100006C-1.933513E-07,13.699982,0.2999879,13.299988,0.79998828,13.299988L4.7999923,13.299988C5.0999801,12,5.5999805,10.899994,6.2999937,9.7999878L3.3999967,7C3.2999908,6.8999939 3.1999845,6.6999817 3.1999845,6.5 3.1999845,6.2999878 3.2999908,6.1000061 3.3999967,6L6.0000054,3.3999939C6.099981,3.2999878 6.2999937,3.1999817 6.5000059,3.1999817 6.6999881,3.1999817 6.9000003,3.2999878 7.0000064,3.3999939L9.9000027,6.2999878C11.000011,5.6000061,12.199993,5.1000061,13.400006,4.7999878L13.400006,0.79998779C13.400006,0.29998779,13.8,0,14.199995,0z
|
||||
</sys:String>
|
||||
|
||||
<sys:String x:Key="{x:Static mbts:Icons.HamburgerMenuGeometryKey}">
|
||||
M2.384186E-06,24L32.000004,24 32.000004,26 2.384186E-06,26z M0,11.999998L32,11.999998 32,13.999998 0,13.999998z M2.384186E-06,0L32.000004,0 32.000004,2 2.384186E-06,2z
|
||||
</sys:String>
|
||||
|
||||
<sys:String x:Key="{x:Static mbts:Icons.HornGeometryKey}">
|
||||
M4.0499969,7.0870913C2.9519978,7.0870913 2.0249984,8.4780906 2.0249987,10.125091 2.0249984,11.771091 2.9519978,13.162091 4.0499969,13.162091 5.1479961,13.162091 6.0749954,11.771091 6.0749955,10.125091 6.0749954,8.4780906 5.1479961,7.0870913 4.0499969,7.0870913z M29.975016,3.316155C23.883831,8.2016557,11.829888,8.9691842,8.1184387,9.0896781L8.0151367,9.0928462 8.0175586,9.1059306C8.0716033,9.4352315 8.0999939,9.7760909 8.0999942,10.125091 8.0999939,10.473966 8.0716033,10.814716 8.0175586,11.143921L8.0098858,11.185364 8.193316,11.195448C12.163719,11.420399,23.994581,12.458921,29.975016,17.005057z M31.176758,0.00019051835C31.280359,-0.0022594665 31.391857,0.01889403 31.514975,0.065146674 31.908983,0.21413228 31.999985,0.59114302 31.999985,1.0121596L31.999985,19.236969C31.999985,19.653958 31.913988,20.028956 31.525991,20.179955 31.405998,20.226952 31.450981,20.250023 31.32699,20.250023 31.050991,20.250023 30.778992,20.13601 30.583986,19.925928 25.229074,14.140572 9.6001122,13.269665 7.2865356,13.172559L7.2813506,13.172345 7.1739591,13.343056C6.4304952,14.468533 5.3060585,15.187091 4.0499969,15.187091 1.8169985,15.187091 0,12.916091 0,10.125091 0,7.3330911 1.8169985,5.0620912 4.0499969,5.0620912 5.3060585,5.0620912 6.4304952,5.78065 7.1739591,6.9063042L7.2852713,7.0832756 7.3087114,7.0828188C9.6077337,7.036146 25.141474,6.4939421 30.567997,0.34315929 30.75913,0.12521249 30.948835,0.005580858 31.176758,0.00019051835z
|
||||
</sys:String>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ using mbts.co2;
|
||||
using mbts.spark.utilities;
|
||||
using mbts.ui;
|
||||
using mbts.ui.controls;
|
||||
using mbts.ui.shell;
|
||||
using mbts.ui.spark;
|
||||
using mbts.ui.utility;
|
||||
using mbts.utils;
|
||||
@@ -18,7 +19,7 @@ using System.Windows.Input;
|
||||
|
||||
namespace mbts.spark {
|
||||
partial class MarketViewVM {
|
||||
public class Row : SecurityMDRow, IDisposable, IEquatable<Row>, DIYGrid.IRowVM, DIYGrid.IActionableRowVM {
|
||||
public class Row : DisposableViewModelBase, ISecurityMDRow, IEquatable<Row>, DIYGrid.IRowVM, DIYGrid.IActionableRowVM {
|
||||
readonly IBus _bus;
|
||||
readonly OrdersModel _orders_model;
|
||||
readonly AnimationManager _animation = new AnimationManager();
|
||||
@@ -40,9 +41,11 @@ namespace mbts.spark {
|
||||
double _raw_day_position;
|
||||
string _bid_merged_size_text, _ask_merged_size_text;
|
||||
ulong _bbo_ref, _last_ref;
|
||||
bool _is_visible;
|
||||
|
||||
public Row(IBus bus, SecurityMD smd, OrdersModel orders_model) : base(smd) {
|
||||
public Row(IBus bus, SecurityMD smd, OrdersModel orders_model) {
|
||||
_bus = bus;
|
||||
security_md = smd;
|
||||
_orders_model = orders_model;
|
||||
_security_text = smd.security.ToString();
|
||||
_security_key_text = security_key.ToString();
|
||||
@@ -105,6 +108,16 @@ namespace mbts.spark {
|
||||
|
||||
public event Action invalidated;
|
||||
|
||||
public SecurityMD security_md { get; }
|
||||
public SecurityImmutable security => security_md.security;
|
||||
public uint security_key => security_md.security.Id;
|
||||
SecurityMD ISecurityMDRow.md => security_md;
|
||||
|
||||
public bool is_visible {
|
||||
get => _is_visible;
|
||||
set => Set(ref _is_visible, value);
|
||||
}
|
||||
|
||||
#region Bound Properties
|
||||
public bool inside_bids => _inside_bids.value;
|
||||
public bool inside_asks => _inside_asks.value;
|
||||
@@ -158,29 +171,30 @@ namespace mbts.spark {
|
||||
public IReadOnlyList<DIYGrid.IGridCellStore> cell_stores => new DIYGrid.IGridCellStore[0];
|
||||
#endregion
|
||||
|
||||
MDConsumerHandle _md => security_md.md;
|
||||
|
||||
public void raise_invalidated() => invalidated?.Invoke();
|
||||
|
||||
public void refresh() {
|
||||
bool dirty = false;
|
||||
if (md.md.get_bbo(ref _bbo_ref, out NativePriceSize b, out NativePriceSize a)) {
|
||||
if (_md.get_bbo(ref _bbo_ref, out NativePriceSize b, out NativePriceSize a)) {
|
||||
_bid_price.set(b.price);
|
||||
refresh_size(ref b, _bid_direct_size, _bid_merged_size, ref _bid_merged_size_text, nameof(bid_merged_size_text));
|
||||
_ask_price.set(a.price);
|
||||
refresh_size(ref a, _ask_direct_size, _ask_merged_size, ref _ask_merged_size_text, nameof(ask_merged_size_text));
|
||||
_inside_bids.on_next_distinct(_bid_price.Value >= _working_best_bid.value, ref dirty);
|
||||
_inside_asks.on_next_distinct(_ask_price.Value <= _working_best_ask.value, ref dirty);
|
||||
_refresh_inside_working_orers(ref dirty);
|
||||
}
|
||||
if (md.md.get_last_trade(ref _last_ref, out NativePriceSize ps, out DateTime ts, out ulong v, out bool from_spread)) {
|
||||
if (_md.get_last_trade(ref _last_ref, out NativePriceSize ps, out DateTime ts, out ulong v, out bool from_spread)) {
|
||||
_last_price.set(ps.price);
|
||||
_last_price_from_spread.set(from_spread);
|
||||
_last_size.on_next_distinct(ps.size, ref dirty);
|
||||
_volume.on_next_distinct(v, ref dirty);
|
||||
}
|
||||
dirty |= _settlement.set_new(md.md.settlement_price);
|
||||
dirty |= _session_low.set_new(md.md.session_low);
|
||||
dirty |= _session_high.set_new(md.md.session_high);
|
||||
dirty |= _day_change.set_new(last_price - settlement);
|
||||
_volume.on_next_distinct(md.md.total_volume, ref dirty);
|
||||
dirty |=
|
||||
_settlement.set_new(_md.settlement_price) |
|
||||
_session_low.set_new(_md.session_low) |
|
||||
_session_high.set_new(_md.session_high) |
|
||||
_day_change.set_new(last_price - settlement);
|
||||
if (dirty)
|
||||
invalidated?.Invoke();
|
||||
|
||||
@@ -206,7 +220,7 @@ namespace mbts.spark {
|
||||
security_key.GetHashCode();
|
||||
|
||||
#region Show Windows
|
||||
void _show_ladder() => _bus.post(SparkTags.ShowLadderTag, md);
|
||||
void _show_ladder() => _bus.post(SparkTags.ShowLadderTag, security_md);
|
||||
|
||||
void _show_order_book(MarketViewView.Columns col) {
|
||||
switch (col) {
|
||||
@@ -280,9 +294,7 @@ namespace mbts.spark {
|
||||
_working_ask_volume.on_next_distinct(_order_summarizer.working_ask_qty, ref dirty);
|
||||
_working_best_bid.on_next_distinct(_order_summarizer.best_working_bid, ref dirty);
|
||||
_working_best_ask.on_next_distinct(_order_summarizer.best_working_ask, ref dirty);
|
||||
_inside_bids.on_next_distinct(_order_summarizer.best_working_bid >= bid_price, ref dirty);
|
||||
_inside_asks.on_next_distinct(_order_summarizer.best_working_ask <= ask_price, ref dirty);
|
||||
|
||||
_refresh_inside_working_orers(ref dirty);
|
||||
RaisePropertyChanged(
|
||||
// Implied might not be ever set, so it probably does not RaisePropertyChanged either
|
||||
// This might be part of the filter funk?
|
||||
@@ -292,6 +304,11 @@ namespace mbts.spark {
|
||||
nameof(working_asks_text)
|
||||
);
|
||||
}
|
||||
|
||||
void _refresh_inside_working_orers(ref bool dirty) {
|
||||
_inside_bids.on_next_distinct(!double.IsNaN(working_best_bid) && OrderDataHelper.PriceComparer.Compare(working_best_bid, bid_price) >= 0, ref dirty);
|
||||
_inside_asks.on_next_distinct(!double.IsNaN(working_best_ask) && OrderDataHelper.PriceComparer.Compare(working_best_ask, ask_price) <= 0, ref dirty);
|
||||
}
|
||||
#endregion
|
||||
|
||||
class MyOrderSummarizer {
|
||||
@@ -367,4 +384,4 @@ namespace mbts.spark {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,29 @@
|
||||
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;
|
||||
using mbts.utils;
|
||||
using System;
|
||||
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.Media;
|
||||
using static bts.shell.MarketViewWindowFactory;
|
||||
@@ -17,21 +31,56 @@ using static mbts.ui.controls.DIYGrid;
|
||||
using static mbts.ui.spark.MarketViewView;
|
||||
|
||||
namespace mbts.spark {
|
||||
partial class MarketViewVM : SecurityMDGrid<MarketViewVM.Row, SecurityMD> {
|
||||
partial class MarketViewVM : DisposableViewModelBase, IHandleRowSelection {
|
||||
/// <summary>
|
||||
/// Viewmodel class designed for <see cref="ui.spark.MarketViewView"/>
|
||||
/// </summary>
|
||||
static readonly IReadOnlyList<Func<MarketViewVM, IDataGridColumnFilter<Row>>> _filter_getters =
|
||||
ObjectSnapshot.property_delegates_of_type<MarketViewVM, IDataGridColumnFilter<Row>>().ToReadOnlyList();
|
||||
|
||||
static readonly IComparer<SecurityMD> _sec_md_key_comparer = Comparer<uint>.Default.wrap(SecurityMD.key_fn);
|
||||
static readonly IComparer<Row> _row_key_comparer = _sec_md_key_comparer.wrap<SecurityMD, Row>(r => r.security_md);
|
||||
|
||||
readonly IBus _bus;
|
||||
readonly OrdersModel _orders_model;
|
||||
IWindowSettingsManager<GridSettings> _settings_manager;
|
||||
readonly ObservableCollection<Row> _rows = new ObservableCollection<Row>();
|
||||
readonly IList<SecurityMD> _wrapped_rows;
|
||||
readonly ListCollectionView _rows_view;
|
||||
readonly Dictionary<uint, Row> _cache = new Dictionary<uint, Row>();
|
||||
readonly Property<Row> _current_row;
|
||||
readonly DeferrableActionWrapper _refresh_filter;
|
||||
Func<Row, bool> _filter;
|
||||
IReadOnlyList<(Columns key, ListSortDirection direction)> _sorting_hierarchy;
|
||||
Color _selection_highlight;
|
||||
|
||||
public MarketViewVM(IBus bus, OrdersModel orders_model, IObservable<StartupData> state, IWindowSettingsManager<GridSettings> settings_manager )
|
||||
: this(bus, orders_model, state, state.SelectManyInSeries(s => s.security_criteria.subscribe_md(bus)), settings_manager) { }
|
||||
|
||||
// securities_connectable should NOT be connected already.
|
||||
MarketViewVM(IBus bus, OrdersModel orders_model, IObservable<StartupData> state, IObservable<IEnumerable<SecurityMD>> securities_source, IWindowSettingsManager<GridSettings> settings_manager)
|
||||
: base(RXCollection.Create(securities_source, SecurityDataHelper.EqualityComparer.MDComparer), null) {
|
||||
/// <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.SecurityKey);
|
||||
// rows
|
||||
_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,
|
||||
IsLiveFiltering = true
|
||||
};
|
||||
_rows_view.LiveFilteringProperties.Add(nameof(Row.is_visible));
|
||||
// Columns
|
||||
const string merged_size_col_header = "Qty", direct_size_col_header = "Direct", implied_size_col_header = "Implied", working_col_header = "Working";
|
||||
const double working_orders_col_width = 105;
|
||||
@@ -39,24 +88,37 @@ namespace mbts.spark {
|
||||
add_col(Columns.Security, "Security", r => r.security.ToString(), nameof(Row.security), DefaultColumnWidths.Security, security_filter, DataGridFilterKnownTemplates.Regex);
|
||||
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.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);
|
||||
add_col(Columns.Position, "Pos", r => r.position.ToString(), nameof(Row.position), DefaultColumnWidths.Size, position_filter, DataGridFilterKnownTemplates.IntMinMax);
|
||||
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);
|
||||
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);
|
||||
@@ -66,8 +128,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,
|
||||
@@ -76,31 +138,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 };
|
||||
@@ -108,108 +224,94 @@ namespace mbts.spark {
|
||||
mb.Bindings.Add(new Binding($"{nameof(Row.security)}.{nameof(SecurityImmutable.ContractSpec)}") { Mode = BindingMode.OneWay });
|
||||
return mb;
|
||||
}
|
||||
// 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.
|
||||
_refresh_filter = new DeferrableActionWrapper(() => _filter_changed?.Invoke());
|
||||
var row_filter_source = Observable.Create<Func<Row, bool>>(o => {
|
||||
notify();
|
||||
_filter_changed += notify;
|
||||
return Disposable.Create(() => _filter_changed -= notify);
|
||||
void notify() =>
|
||||
o.OnNext(get_filter_states().build_predicate());
|
||||
});
|
||||
foreach (var f in _all_filters) {
|
||||
f.filter_changed += _refresh_filter.execute;
|
||||
}
|
||||
//
|
||||
// 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 }
|
||||
};
|
||||
grid_settings_manager = new SimpleGridSettingsManager<Row>(columns, col_settings_flags, row_vms);
|
||||
// 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());
|
||||
//
|
||||
_resources.AddRange(
|
||||
state.OnDispatcher().Subscribe(s => {
|
||||
using (rows_view.DeferRefresh()) {
|
||||
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) {
|
||||
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);
|
||||
}
|
||||
rows_view.set_sort_descriptions(s.sorting_criterion, columns);
|
||||
}
|
||||
}), DataGridFilterHelper.hook_filters(rows_view, security_filter,
|
||||
bid_price_filter, bid_merged_size_filter, bid_direct_size_filter, bid_implied_size_filter,
|
||||
ask_price_filter, ask_merged_size_filter, ask_direct_size_filter, ask_implied_size_filter,
|
||||
last_price_filter, last_price_from_spread_filter, last_size_filter, day_change_filter, settlement_filter,
|
||||
session_low_filter, session_high_filter, position_filter, raw_day_position_filter, working_bid_volume_filter, working_ask_volume_filter, volume_filter, security_key_filter),
|
||||
settings_manager.subscribe_current().OnDispatcher().Subscribe(grid_settings_manager),
|
||||
_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;
|
||||
}),
|
||||
security_mds_source.Subscribe(_on_securities),
|
||||
ObservableAutoWrapper.connect_item_hooks(_rows, r => r, true),
|
||||
WorkLoop.Start("MarketView", _refresh)
|
||||
);
|
||||
}
|
||||
|
||||
public ObservableCollection<ColumnBase> columns { get; } = new ObservableCollection<ColumnBase>();
|
||||
|
||||
public SimpleGridSettingsManager<Row> grid_settings_manager { get; }
|
||||
|
||||
public GridSettings get_grid_save_state() {
|
||||
if (_settings_manager.instance_settings == null)
|
||||
return null;
|
||||
else
|
||||
return grid_settings_manager.create();
|
||||
}
|
||||
|
||||
public static IComparer<Row> get_column_comparer(Columns column) {
|
||||
switch (column) {
|
||||
case Columns.Security: return wrap(r => r.security);
|
||||
case Columns.SecurityKey: return wrap(r => r.security.Id);
|
||||
case Columns.Volume: return wrap(r => r.volume);
|
||||
case Columns.Last: return wrap(r => r.last_price);
|
||||
case Columns.LastSize: return wrap(r => r.last_size);
|
||||
case Columns.Position: return wrap(r => r.position);
|
||||
case Columns.WorkingBids: return wrap(r => r.working_bid_volume);
|
||||
case Columns.BidImpliedSize: return wrap(r => r.bid_implied_size);
|
||||
case Columns.BidDirectSize: return wrap(r => r.bid_direct_size);
|
||||
case Columns.BidMergedSize: return wrap(r => r.bid_merged_size);
|
||||
case Columns.Bid: return wrap(r => r.bid_price);
|
||||
case Columns.Ask: return wrap(r => r.ask_price);
|
||||
case Columns.AskMergedSize: return wrap(r => r.ask_merged_size);
|
||||
case Columns.AskDirectSize: return wrap(r => r.ask_direct_size);
|
||||
case Columns.AskImpliedSize: return wrap(r => r.ask_implied_size);
|
||||
case Columns.WorkingAsks: return wrap(r => r.working_ask_volume);
|
||||
case Columns.Settlement: return wrap(r => r.settlement);
|
||||
case Columns.DayChange: return wrap(r => r.day_change);
|
||||
case Columns.SessionLow: return wrap(r => r.session_low);
|
||||
case Columns.SessionHigh: return wrap(r => r.session_high);
|
||||
case Columns.MarketDescription: return wrap(r => r.market_description);
|
||||
case Columns.ClearedAlias: return wrap(r => r.security.cleared_alias());
|
||||
case Columns.ProductDescription: return wrap(r => r.security.product_name());
|
||||
case Columns.HubAlias: return wrap(r => r.security.hub_alias());
|
||||
case Columns.StripName: return wrap(r => r.security.strip_name());
|
||||
case Columns.MicCode: return wrap(r => r.security.mic_code());
|
||||
default: return null;
|
||||
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 }
|
||||
}
|
||||
});
|
||||
}
|
||||
IComparer<Row> wrap<T>(Func<Row, T> selector) =>
|
||||
Comparer<T>.Default.wrap(selector);
|
||||
}
|
||||
|
||||
#region Filters
|
||||
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); }
|
||||
|
||||
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)>());
|
||||
set {
|
||||
_sorting_hierarchy = value;
|
||||
comparer = DIYGridSortHelper.build_comparer(value, get_column_comparer, Columns.SecurityKey);
|
||||
DIYGridSortHelper.reset_sorting(columns, value);
|
||||
_rows_view.CustomSort = comparer.to_non_generic();
|
||||
}
|
||||
}
|
||||
|
||||
public IComparer<Row> comparer { get; private set; }
|
||||
|
||||
public IDataGridColumnPersistentFilter<Row, string> security_filter { get; }
|
||||
public IDataGridColumnPersistentFilter<Row, DoubleMinMaxState> bid_price_filter { get; } = new DoubleMinMax<Row>(r => r.bid_price, nameof(Row.bid_price));
|
||||
public IDataGridColumnPersistentFilter<Row, UIntMinMaxState> bid_merged_size_filter { get; } = new UIntMinMax<Row>(r => r.bid_merged_size, nameof(Row.bid_merged_size));
|
||||
@@ -239,7 +341,60 @@ namespace mbts.spark {
|
||||
public IDataGridColumnPersistentFilter<Row, string> hub_alias_filter { get; } = new RegexFilterVM<Row>(r => r.security.HasIce ? r.security.Ice.HubAlias : "", nameof(Row.hub_alias));
|
||||
public IDataGridColumnPersistentFilter<Row, string> strip_name_filter { get; } = new RegexFilterVM<Row>(r => r.security.HasIce ? r.security.Ice.StripName : "", nameof(Row.strip_name));
|
||||
public IDataGridColumnPersistentFilter<Row, string> mic_code_filter { get; } = new RegexFilterVM<Row>(r => r.security.HasIce ? r.security.Ice.MicCode : "", nameof(Row.mic_code));
|
||||
|
||||
|
||||
IEnumerable<IDataGridColumnFilter<Row>> _all_filters =>
|
||||
_filter_getters.Select(g => g(this));
|
||||
|
||||
public static IComparer<Row> get_column_comparer(Columns column) {
|
||||
switch (column) {
|
||||
case Columns.Security: return wrap(r => r.security);
|
||||
case Columns.SecurityKey: return wrap(r => r.security.Id);
|
||||
case Columns.Volume: return wrap(r => r.volume);
|
||||
case Columns.Last: return wrap(r => r.last_price);
|
||||
case Columns.LastSpd: return wrap(r => r.last_price_from_spread);
|
||||
case Columns.LastSize: return wrap(r => r.last_size);
|
||||
case Columns.Position: return wrap(r => r.position);
|
||||
case Columns.WorkingBids: return wrap(r => r.working_bid_volume);
|
||||
case Columns.BidImpliedSize: return wrap(r => r.bid_implied_size);
|
||||
case Columns.BidDirectSize: return wrap(r => r.bid_direct_size);
|
||||
case Columns.BidMergedSize: return wrap(r => r.bid_merged_size);
|
||||
case Columns.Bid: return wrap(r => r.bid_price);
|
||||
case Columns.Ask: return wrap(r => r.ask_price);
|
||||
case Columns.AskMergedSize: return wrap(r => r.ask_merged_size);
|
||||
case Columns.AskDirectSize: return wrap(r => r.ask_direct_size);
|
||||
case Columns.AskImpliedSize: return wrap(r => r.ask_implied_size);
|
||||
case Columns.WorkingAsks: return wrap(r => r.working_ask_volume);
|
||||
case Columns.Settlement: return wrap(r => r.settlement);
|
||||
case Columns.DayChange: return wrap(r => r.day_change);
|
||||
case Columns.SessionLow: return wrap(r => r.session_low);
|
||||
case Columns.SessionHigh: return wrap(r => r.session_high);
|
||||
case Columns.MarketDescription: return wrap(r => r.market_description);
|
||||
case Columns.ClearedAlias: return wrap(r => r.security.cleared_alias());
|
||||
case Columns.ProductDescription: return wrap(r => r.security.product_name());
|
||||
case Columns.HubAlias: return wrap(r => r.security.hub_alias());
|
||||
case Columns.StripName: return wrap(r => r.security.strip_name());
|
||||
case Columns.MicCode: return wrap(r => r.security.mic_code());
|
||||
default: return null;
|
||||
}
|
||||
IComparer<Row> wrap<T>(Func<Row, T> selector) =>
|
||||
Comparer<T>.Default.wrap(selector);
|
||||
}
|
||||
|
||||
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() {
|
||||
return new FilterStates {
|
||||
@@ -273,14 +428,102 @@ namespace mbts.spark {
|
||||
mic_code = mic_code_filter.get_state(),
|
||||
};
|
||||
}
|
||||
#endregion
|
||||
|
||||
protected override Row row_factory(SecurityMD smd) => new Row(_bus, smd, _orders_model);
|
||||
protected override void on_row_removed(Row row) => row.Dispose();
|
||||
protected override IEnumerable<SortingCriteria> get_default_sorting() { yield return new SortingCriteria(nameof(Row.security), true); }
|
||||
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) _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 _row_vms) r.refresh();
|
||||
foreach (var r in _rows) {
|
||||
r.refresh();
|
||||
r.is_visible = _filter == null || _filter(r);
|
||||
}
|
||||
}
|
||||
|
||||
Row _row_factory(SecurityMD smd) => new Row(_bus, smd, _orders_model);
|
||||
|
||||
void IHandleRowSelection.row_selection_changed(SelectionChangedEventArgs e) {
|
||||
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>
|
||||
@@ -22,13 +73,14 @@
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
<c:DIYGrid
|
||||
x:Name="_grid"
|
||||
Grid.Row="0"
|
||||
Columns="{Binding columns}"
|
||||
CurrentRow="{Binding current_row}"
|
||||
Rows="{Binding rows_view}"
|
||||
SelectionHighlight="{Binding grid_settings_manager.selection_highlight, Mode=OneWay}"
|
||||
>
|
||||
x:Name="_grid"
|
||||
Grid.Row="0"
|
||||
Columns="{Binding columns}"
|
||||
CurrentRow="{Binding current_row}"
|
||||
Rows="{Binding rows_view}"
|
||||
SelectionHighlight="{Binding selection_highlight, Mode=OneWay}"
|
||||
SyncColumnSortDirectionWithSortDescriptions="False"
|
||||
>
|
||||
<c:DIYGrid.InputBindings>
|
||||
<MouseBinding MouseAction="LeftDoubleClick" Command="{x:Static c:DIYGrid.DefaultRowCommand}"/>
|
||||
<KeyBinding Key="Return" Command="{x:Static c:DIYGrid.DefaultRowCommand}"/>
|
||||
|
||||
@@ -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,14 +61,100 @@ 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);
|
||||
|
||||
protected override void OnLostMouseCapture(MouseEventArgs e) {
|
||||
if (e.LeftButton == MouseButtonState.Pressed && Mouse.Captured == null) {
|
||||
if (this.ChildAtPoint(e.GetPosition(this), out DIYGridRowsPresenter presenter))
|
||||
presenter.begin_explicit_drag();
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
});
|
||||
}
|
||||
|
||||
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 {
|
||||
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(view);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class MarketViewRowView : DIYGrid.HighPerformanceRow<MarketViewVM.Row, MarketViewView.Columns> { }
|
||||
class MarketViewRowView : DIYGrid.HighPerformanceRow<MarketViewVM.Row> { }
|
||||
}
|
||||
@@ -1,18 +1,19 @@
|
||||
using bts;
|
||||
using bts.api;
|
||||
using bts.api.core;
|
||||
using bts.collections;
|
||||
using bts.shell;
|
||||
using bts.shell.fills;
|
||||
using bts.spark;
|
||||
using bts.utils;
|
||||
using mbts.persist;
|
||||
using mbts.ui;
|
||||
using mbts.ui.settings;
|
||||
using mbts.ui.shell;
|
||||
using mbts.ui.spark;
|
||||
using mbts.ui.utility;
|
||||
using mbts.utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Linq;
|
||||
using System.Reactive.Disposables;
|
||||
using System.Reactive.Linq;
|
||||
@@ -21,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;
|
||||
@@ -32,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) {
|
||||
@@ -42,13 +52,16 @@ 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;
|
||||
public override MarketViewWindowFactory.StartupData SaveState =>
|
||||
_state = new MarketViewWindowFactory.StartupData(_state.security_criteria, _vm.get_filter_states(), _vm.get_grid_save_state(), _vm.rows_view.get_sorting_criteria(_vm.columns));
|
||||
_state = new MarketViewWindowFactory.StartupData(
|
||||
_state.security_criteria,
|
||||
_vm.get_filter_states(),
|
||||
_settings_manager.instance_settings,
|
||||
_vm.sorting_hierarchy.Select(tup => new SortingCriteria(tup.key.ToString(), tup.direction == System.ComponentModel.ListSortDirection.Ascending))
|
||||
.ToList());
|
||||
|
||||
public override double MinWidth => MarketViewWindowFactory.StartupData.DefaultGridSettings.columns.Where(c => c.is_visible).Sum(gcs => gcs.width)
|
||||
+ SPARK_MENU_COLLAPSED_WIDTH
|
||||
@@ -68,43 +81,33 @@ namespace mbts.spark {
|
||||
return _selected_security_criteria_backing;
|
||||
}
|
||||
}
|
||||
SecurityCriteria _sorted_selected_security_criteria {
|
||||
get {
|
||||
if (_sorted_selected_security_criteria_backing == null) {
|
||||
if (_vm != null) {
|
||||
var sorting_criteria = _vm.rows_view.get_sorting_criteria(_vm.columns);
|
||||
var comp_builder = new ComparerBuilder<MarketViewVM.Row>();
|
||||
foreach (var sc in sorting_criteria) {
|
||||
if (_vm.columns.TryGetFirst(col => string.Equals(col.id, sc.col_name, StringComparison.CurrentCultureIgnoreCase), out var match)) {
|
||||
if (match is ui.controls.DIYGrid.IKeyedColumn<MarketViewView.Columns> kcol) {
|
||||
var col_comp = MarketViewVM.get_column_comparer(kcol.key);
|
||||
if (col_comp != null) {
|
||||
comp_builder.add(sc.is_ascending ? col_comp : col_comp.reverse());
|
||||
} else {
|
||||
Log.Ui.warn($"{_window_type} is unable to find the row comparer for the '{sc.col_name}' column.");
|
||||
}
|
||||
} else {
|
||||
Log.Ui.warn($"{_window_type} is unable find a column with id '{sc.col_name}.'");
|
||||
}
|
||||
}
|
||||
}
|
||||
var comp = comp_builder.to_comparer();
|
||||
_sorted_selected_security_criteria_backing = new SecurityCriteria(_vm.selected_rows.Ordered(comp).Select(r => r.security));
|
||||
}
|
||||
}
|
||||
return _sorted_selected_security_criteria_backing;
|
||||
}
|
||||
}
|
||||
|
||||
SecurityCriteria _sorted_selected_security_criteria => _sorted_selected_security_criteria_backing ?? (
|
||||
_sorted_selected_security_criteria_backing = _vm != null ?
|
||||
new SecurityCriteria(_vm.selected_rows.Ordered(_vm.comparer).Select(r => r.security)) :
|
||||
null);
|
||||
|
||||
bool _has_selected_rows() => _vm != null && _vm.selected_rows.Count > 0;
|
||||
|
||||
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;
|
||||
_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).
|
||||
@@ -135,11 +138,22 @@ 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);
|
||||
_vm.current_row_changed += notify;
|
||||
return Disposable.Create(() => _vm.current_row_changed -= notify);
|
||||
void notify(MarketViewVM.Row oldval, MarketViewVM.Row newval) {
|
||||
if (newval != null)
|
||||
o.OnNext(newval.security_md);
|
||||
else o.OnNext(null);
|
||||
}
|
||||
});
|
||||
_commands = new List<SparkCommand> {
|
||||
SparkCommands.ShowLadders(SparkCommands.KeyGestures.ShowLadder).complete(_vm.selected_security_mds_observable(), smds => smds.Count > 0),
|
||||
SparkCommands.ShowOrderTicketFromSecurity(SparkCommands.KeyGestures.ShowOrderTicket).complete(_vm.current_security_md, md => md.HasValue, ConvertUtils.ValueOrDefault),
|
||||
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,
|
||||
@@ -150,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,16 +177,19 @@ namespace mbts.spark {
|
||||
}))
|
||||
};
|
||||
//
|
||||
_vm.selection_changed += 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())))) {
|
||||
_vm.selected_rows.CollectionChanged += on_selection_changed;
|
||||
_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.selection_changed -= on_selection_changed;
|
||||
_vm.selected_rows.CollectionChanged -= on_selection_changed;
|
||||
_view._grid.Sorting -= on_grid_sorting;
|
||||
}
|
||||
};
|
||||
|
||||
void on_selection_changed((IEnumerable<MarketViewVM.Row> removed, IEnumerable<MarketViewVM.Row> added) change) {
|
||||
void on_selection_changed(object sender, NotifyCollectionChangedEventArgs e) {
|
||||
// invalidate selected security criteria
|
||||
_selected_security_criteria_backing = null;
|
||||
_sorted_selected_security_criteria_backing = null;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using bts.api;
|
||||
using bts.proto;
|
||||
using bts.settings;
|
||||
using bts.utils;
|
||||
using mbts.persist;
|
||||
using mbts.spark;
|
||||
using mbts.spark.filters;
|
||||
@@ -229,6 +230,46 @@ namespace bts.shell {
|
||||
public string last_price_from_spread;
|
||||
[ProtoMember(28)]
|
||||
public string mic_code;
|
||||
|
||||
public Func<MarketViewVM.Row, bool> build_predicate() {
|
||||
var pb = new ColumnPredicateBuilder<MarketViewVM.Row>();
|
||||
// security
|
||||
pb.add_regex(security, r => r.security.ToString());
|
||||
pb.add_regex(security_key, r => r.security_key.ToString());
|
||||
// bid
|
||||
pb.add_numeric(bid_price, r => r.bid_price);
|
||||
pb.add_numeric(bid_merged_size, r => r.bid_merged_size);
|
||||
pb.add_numeric(bid_direct_size, r => r.bid_direct_size);
|
||||
pb.add_numeric(bid_implied_size, r => r.bid_implied_size);
|
||||
pb.add_numeric(working_bid_volume, r => r.working_bid_volume);
|
||||
// ask
|
||||
pb.add_numeric(ask_price, r => r.ask_price);
|
||||
pb.add_numeric(ask_implied_size, r => r.ask_implied_size);
|
||||
pb.add_numeric(ask_direct_size, r => r.ask_direct_size);
|
||||
pb.add_numeric(ask_merged_size, r => r.ask_merged_size);
|
||||
pb.add_numeric(working_ask_volume, r => r.working_ask_volume);
|
||||
// last
|
||||
pb.add_numeric(last_price, r => r.last_price);
|
||||
pb.add_regex(last_price_from_spread, r => r.last_price_from_spread.ToString());
|
||||
pb.add_numeric(last_size, r => r.last_size);
|
||||
// position
|
||||
pb.add_numeric(position, r => r.position);
|
||||
pb.add_numeric(raw_day_position, r => r.raw_day_position);
|
||||
// market
|
||||
pb.add_numeric(day_change, r => r.day_change);
|
||||
pb.add_numeric(settlement, r => r.settlement);
|
||||
pb.add_numeric(session_low, r => r.session_low);
|
||||
pb.add_numeric(session_high, r => r.session_high);
|
||||
pb.add_numeric(volume, r => r.volume);
|
||||
// ice
|
||||
pb.add_regex(market_description, r => r.market_description);
|
||||
pb.add_regex(cleared_alias, r => r.cleared_alias);
|
||||
pb.add_regex(product_description, r => r.product_description);
|
||||
pb.add_regex(hub_alias, r => r.hub_alias);
|
||||
pb.add_regex(strip_name, r => r.strip_name);
|
||||
pb.add_regex(mic_code, r => r.mic_code);
|
||||
return pb.to_predicate();
|
||||
}
|
||||
}
|
||||
|
||||
// migration rules
|
||||
@@ -271,5 +312,11 @@ namespace bts.shell {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
[Export(typeof(IMarketViewGridSettingsMigrationRule))]
|
||||
class EnsureSelectionHighlightTranslucency : IMarketViewGridSettingsMigrationRule {
|
||||
public bool apply(GridSettings settings, out GridSettings new_settings) =>
|
||||
GridSettingsMigrationRules.SelectionHighlightIsTranslucent.apply(settings, out new_settings);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,5 +13,5 @@ namespace mbts.ui.spark {
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
class TimeAndSalesRowView : DIYGrid.HighPerformanceRow<TimeAndSalesVM.Row, TimeAndSalesView.Columns> { }
|
||||
class TimeAndSalesRowView : DIYGrid.HighPerformanceRow<TimeAndSalesVM.Row> { }
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using bts.api;
|
||||
using bts.settings;
|
||||
using bts.spark;
|
||||
using mbts;
|
||||
using mbts.co2;
|
||||
@@ -10,6 +11,7 @@ using mbts.ui.settings;
|
||||
using ProtoBuf;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.Composition;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Threading.Tasks;
|
||||
@@ -38,7 +40,7 @@ namespace bts.shell {
|
||||
|
||||
public override Task<IShellWindow> load(StartupData state) =>
|
||||
Task.FromResult<IShellWindow>(new TimeAndSalesWindow(this, _bus, _consumer, state, SparkCommands.PickTimeAndSales.icon,
|
||||
new WindowSettingsManager<GridSettings>(_settings_hub, state.grid, s => s.time_and_sales_grid, (s, g) => s.with(time_and_sales:g))));
|
||||
_settings_hub.create_time_and_sales_settings_manager(state.grid)));
|
||||
|
||||
public async void show(SecurityCriteria criteria) {
|
||||
var w = await load(new StartupData(criteria));
|
||||
@@ -122,5 +124,11 @@ namespace bts.shell {
|
||||
[ProtoMember(7)]
|
||||
public co2.AggressorSide[] aggressor_sides;
|
||||
}
|
||||
|
||||
[Export(typeof(ITimeAndSalesGridSettingsMigrationRule))]
|
||||
class EnsureSelectionHighlightTranslucency : ITimeAndSalesGridSettingsMigrationRule {
|
||||
public bool apply(GridSettings settings, out GridSettings new_settings) =>
|
||||
GridSettingsMigrationRules.SelectionHighlightIsTranslucent.apply(settings, out new_settings);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,5 +20,5 @@ namespace mbts.ui.spark {
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
class SyntheticManagementRowView : DIYGrid.HighPerformanceRow<SyntheticManagementDialogVM.Row, SyntheticManagementView.Columns> { }
|
||||
class SyntheticManagementRowView : DIYGrid.HighPerformanceRow<SyntheticManagementDialogVM.Row> { }
|
||||
}
|
||||
@@ -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
|
||||
});
|
||||
|
||||
@@ -62,7 +62,10 @@ namespace mbts.spark {
|
||||
_vm = new AlgoStudioVM(_model);
|
||||
_vm.load_from(_state);
|
||||
_view = new AlgoStudioView { DataContext = _vm };
|
||||
_resources = new CompositeDisposable();
|
||||
_resources = new CompositeDisposable(
|
||||
_bus.subscribe(SparkTags.SecuritiesTag)
|
||||
.OnDispatcher()
|
||||
.Subscribe(sec_map => _view.Resources[ResourceKeys.Securities] = sec_map));
|
||||
// add definition
|
||||
_commands.Add(new SparkCommandCommand("Add Algo", Icons.AddBoxes, _vm.add_definition_cmd));
|
||||
// clone definition
|
||||
|
||||
@@ -13,6 +13,9 @@ namespace mbts.spark {
|
||||
string _template_key;
|
||||
ICommand _set_value_cmd;
|
||||
|
||||
public AlgoParamRowVM(IAlgoVM algo, IAlgoParamVM param, AlgoParamRowVM parent)
|
||||
: this(algo, param, parent, (param as ITemplateSelectorKey)?.key) { }
|
||||
|
||||
public AlgoParamRowVM(IAlgoVM algo, IAlgoParamVM param, AlgoParamRowVM parent, string template_key = null) {
|
||||
this.param = param;
|
||||
this.algo = algo;
|
||||
|
||||
+40
@@ -247,6 +247,31 @@
|
||||
</TextBlock>
|
||||
</DataTemplate>
|
||||
</u:TemplateSelectorKeyValue>
|
||||
<u:TemplateSelectorKeyValue Key="{x:Static u:TemplateKeys.SecurityKey}">
|
||||
<DataTemplate>
|
||||
<Grid>
|
||||
<!--Note: Ideally, this 'securities BindingProxy' would be a static resource, but it only seems to detect the dynamic securities when it is in the visual tree.-->
|
||||
<!--<Grid.Resources>
|
||||
<u:BindingProxy x:Key="SecuritiesBindingProxy" x:Shared="False" Value="{DynamicResource {x:Static u:ResourceKeys.Securities}}"/>
|
||||
</Grid.Resources>-->
|
||||
<u:BindingProxy x:Name="SecuritiesBindingProxy" Value="{DynamicResource {x:Static u:ResourceKeys.Securities}}"/>
|
||||
<TextBlock>
|
||||
<TextBlock.Text>
|
||||
<MultiBinding>
|
||||
<MultiBinding.Converter>
|
||||
<cv:MultiValueConverterPipe>
|
||||
<cv:DictionaryLookupConverter/>
|
||||
<x:Static MemberType="cv:ValueConverters" Member="SecuritySelectorText"/>
|
||||
</cv:MultiValueConverterPipe>
|
||||
</MultiBinding.Converter>
|
||||
<Binding Path="Value" ElementName="SecuritiesBindingProxy"/>
|
||||
<Binding Path="param.value"/>
|
||||
</MultiBinding>
|
||||
</TextBlock.Text>
|
||||
</TextBlock>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</u:TemplateSelectorKeyValue>
|
||||
</usp:AlgoParamTemplateSelector.KeyedTemplates>
|
||||
<!--Bool-->
|
||||
<usp:AlgoParamTemplateSelector.BoolTemplate>
|
||||
@@ -320,6 +345,21 @@
|
||||
</usp:PriceEntryControl>
|
||||
</DataTemplate>
|
||||
</u:TemplateSelectorKeyValue>
|
||||
<u:TemplateSelectorKeyValue Key="{x:Static u:TemplateKeys.SecurityKey}">
|
||||
<DataTemplate>
|
||||
<c:ContractPickerPopup
|
||||
CloseOnCancel="True"
|
||||
CloseOnOK="True"
|
||||
Header="{Binding SelectedSecurity, RelativeSource={RelativeSource Self}, Converter={x:Static cv:ValueConverters.SecuritySelectorText}}"
|
||||
IsEditMode="False"
|
||||
IsLazy="True"
|
||||
SecurityMap="{DynamicResource {x:Static u:ResourceKeys.Securities}}"
|
||||
SelectedSecurityKey="{Binding param.value}"
|
||||
ShowDialogSkirt="True"
|
||||
OKClick="{x:Static c:ContractPickerPopup.FlushSelectedSecurityKeyHandler}"
|
||||
/>
|
||||
</DataTemplate>
|
||||
</u:TemplateSelectorKeyValue>
|
||||
</usp:AlgoParamTemplateSelector.KeyedTemplates>
|
||||
<!--Bool-->
|
||||
<usp:AlgoParamTemplateSelector.BoolTemplate>
|
||||
|
||||
@@ -31,5 +31,5 @@ namespace mbts.ui.spark {
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
class FillsRowView : DIYGrid.HighPerformanceRow<FillsVM.Row, FillsView.Columns> { }
|
||||
class FillsRowView : DIYGrid.HighPerformanceRow<FillsVM.Row> { }
|
||||
}
|
||||
@@ -72,5 +72,11 @@ namespace bts.shell {
|
||||
public bool apply(GridSettings settings, out GridSettings new_settings) =>
|
||||
settings.add_missing_columns(FillsSettings.DefaultGridSettings.columns, out new_settings);
|
||||
}
|
||||
|
||||
[Export(typeof(IFillsGridSettingsMigrationRule))]
|
||||
class EnsureSelectionHighlightTranslucency : IFillsGridSettingsMigrationRule {
|
||||
public bool apply(GridSettings settings, out GridSettings new_settings) =>
|
||||
GridSettingsMigrationRules.SelectionHighlightIsTranslucent.apply(settings, out new_settings);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,6 @@ using mbts.ui.spark.algo;
|
||||
using mbts.ui.utility.text;
|
||||
using mbts.utils;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Specialized;
|
||||
@@ -256,9 +255,8 @@ namespace mbts.spark {
|
||||
DependencyProperty.Register("Orders", typeof(ObservableCollection<LadderOrderVM>), typeof(LadderGridHost), new PropertyMetadata(null, orders_changed));
|
||||
|
||||
static void orders_changed(DependencyObject d, DependencyPropertyChangedEventArgs e) {
|
||||
LadderGridHost lgh = d as LadderGridHost;
|
||||
WPFHelper.manage_collection_change(e, lgh.orders_collection_changed, src => lgh._ordershost.clear_orders(),
|
||||
src => lgh.orders_collection_changed(lgh, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, (IList)src)));
|
||||
if (d is LadderGridHost lgh)
|
||||
WPFHelper.manage_collection_change(e, lgh.orders_collection_changed);
|
||||
}
|
||||
|
||||
void orders_collection_changed(object sender, NotifyCollectionChangedEventArgs e) {
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
using bts;
|
||||
using bts.collections;
|
||||
using bts.utils;
|
||||
using mbts.ui;
|
||||
using mbts.ui.converters;
|
||||
using mbts.ui.extensions;
|
||||
using mbts.utils;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
@@ -80,6 +84,7 @@ namespace mbts.spark {
|
||||
_ladder.visible_columns_changed += () => _column_headers_presenter.Columns.SetItems(
|
||||
_ladder.visible_column_types.Select(t => ColumnFactory.create(_ladder, t)));
|
||||
this.vm = vm;
|
||||
this.ConnectDataContext<LadderVM>(lvm => lvm.bus.subscribe(SparkTags.SecuritiesTag).OnDispatcher().Subscribe(sec_map => Resources[ResourceKeys.Securities] = sec_map));
|
||||
DataContext = vm;
|
||||
DataContextChanged += datacontext_changed;
|
||||
_ui_scaler.MouseDoubleClick += _ui_scaler_MouseDoubleClick;
|
||||
|
||||
@@ -62,15 +62,15 @@
|
||||
Header="{Binding security}"
|
||||
IsEditMode="False"
|
||||
IsLazy="True"
|
||||
SecurityMap="{DynamicResource ResourceKey={x:Static usp:ManualFillRowEditor.SecuritiesKey}}"
|
||||
SecurityMap="{DynamicResource ResourceKey={x:Static u:ResourceKeys.Securities}}"
|
||||
SelectedSecurity="{Binding security, UpdateSourceTrigger=Explicit}"
|
||||
ShowDialogSkirt="True"
|
||||
ToolTip="{Binding SelectedCriteria, RelativeSource={RelativeSource Self}, Converter={x:Static c:ContractPickerPopup.SecurityCriteriaTextConverter}, Mode=OneWay}"
|
||||
OKClick="{x:Static c:ContractPickerPopup.FlushSelectedSecurityHandler}"
|
||||
/>
|
||||
<ComboBox Grid.Row="2" Grid.Column="2" ItemsSource="{DynamicResource ResourceKey={x:Static usp:ManualFillRowEditor.AccountsKey}}" SelectedItem="{Binding account}"/>
|
||||
<ComboBox Grid.Row="2" Grid.Column="4" ItemsSource="{DynamicResource ResourceKey={x:Static usp:ManualFillRowEditor.UsersKey}}" SelectedItem="{Binding user}"/>
|
||||
<ComboBox Grid.Row="2" Grid.Column="6" ItemsSource="{DynamicResource ResourceKey={x:Static usp:ManualFillRowEditor.SidesKey}}" SelectedItem="{Binding side}" Width="48"/>
|
||||
<ComboBox Grid.Row="2" Grid.Column="2" ItemsSource="{DynamicResource ResourceKey={x:Static u:ResourceKeys.Accounts}}" SelectedItem="{Binding account}"/>
|
||||
<ComboBox Grid.Row="2" Grid.Column="4" ItemsSource="{DynamicResource ResourceKey={x:Static u:ResourceKeys.Users}}" SelectedItem="{Binding user}"/>
|
||||
<ComboBox Grid.Row="2" Grid.Column="6" ItemsSource="{DynamicResource ResourceKey={x:Static u:ResourceKeys.Sides}}" SelectedItem="{Binding side}" Width="48"/>
|
||||
<c:TickUpDownUInt Grid.Row="2" Grid.Column="8" MinWidth="40" Value="{Binding size}"/>
|
||||
<usp:PriceEntryControl Grid.Row="2" Grid.Column="10" MinWidth="55" Value="{Binding price}" ContractSpec="{Binding spec}"/>
|
||||
</Grid>
|
||||
|
||||
@@ -6,12 +6,6 @@ namespace mbts.ui.spark {
|
||||
/// Interaction logic for ManualFillRowEditor.xaml
|
||||
/// </summary>
|
||||
partial class ManualFillRowEditor : UserControl {
|
||||
public const string
|
||||
SecuritiesKey = nameof(SecuritiesKey),
|
||||
AccountsKey = nameof(AccountsKey),
|
||||
UsersKey = nameof(UsersKey),
|
||||
SidesKey = nameof(SidesKey);
|
||||
|
||||
public static readonly RoutedCommand
|
||||
CommitCommand = new RoutedCommand(),
|
||||
CancelCommand = new RoutedCommand();
|
||||
|
||||
@@ -50,11 +50,11 @@ namespace mbts.ui.spark {
|
||||
row_editor.CommandBindings.Add(new CommandBinding(ManualFillRowEditor.CommitCommand, commit_executed));
|
||||
row_editor.CommandBindings.Add(new CommandBinding(ManualFillRowEditor.CancelCommand, cancel_executed));
|
||||
// dynamic resources
|
||||
row_editor.Resources[ManualFillRowEditor.AccountsKey] = vm.accounts;
|
||||
row_editor.Resources[ManualFillRowEditor.UsersKey] = vm.users;
|
||||
row_editor.Resources[ManualFillRowEditor.SidesKey] = vm.sides;
|
||||
row_editor.Resources[ResourceKeys.Accounts] = vm.accounts;
|
||||
row_editor.Resources[ResourceKeys.Users] = vm.users;
|
||||
row_editor.Resources[ResourceKeys.Sides] = vm.sides;
|
||||
FocusManager.SetIsFocusScope(row_editor, true);
|
||||
var sec_hook = vm.as_demuxer().subscribe(nameof(ManualFillsVM.security_map)).OnDispatcher().Subscribe(v => row_editor.Resources[ManualFillRowEditor.SecuritiesKey] = v);
|
||||
var sec_hook = vm.as_demuxer().subscribe(nameof(ManualFillsVM.security_map)).OnDispatcher().Subscribe(v => row_editor.Resources[ResourceKeys.Securities] = v);
|
||||
// popup adorner
|
||||
var popup_adorner = new PopupAdorner(
|
||||
data_grid, data_grid,
|
||||
|
||||
@@ -22,7 +22,7 @@ namespace mbts.spark {
|
||||
DataContext = vm;
|
||||
}
|
||||
|
||||
class MiniOrderBookRowView : DIYGrid.HighPerformanceRow<OrderBookRowVM, OrderBookGridView.ColumnKeys> {
|
||||
class MiniOrderBookRowView : DIYGrid.HighPerformanceRow<OrderBookRowVM> {
|
||||
public MiniOrderBookRowView() : base() {
|
||||
FocusManager.SetIsFocusScope(this, true);
|
||||
KeyboardNavigation.SetTabNavigation(this, KeyboardNavigationMode.Cycle);
|
||||
|
||||
@@ -650,12 +650,13 @@ namespace mbts.spark {
|
||||
ListSortDirection? _cached_sort_direction;
|
||||
|
||||
public OrderBookLastMessageColumn(IDataGridColumnPersistentFilter<OrderSecurityRootPair, string> filter)
|
||||
: base(Columns.LastMessage, new Binding(nameof(OrderBookRowVM.last_msg)), filter, DataGridFilterKnownTemplates.Regex) {
|
||||
: base(Columns.LastMessage, filter, DataGridFilterKnownTemplates.Regex) {
|
||||
id = key.ToString();
|
||||
this.filter = filter;
|
||||
Header = "Last Message";
|
||||
Width = DefaultColumnWidths.LastMessage;
|
||||
tooltip_binding = new Binding(nameof(OrderBookRowVM.last_msg));
|
||||
TextBinding = new Binding(nameof(OrderBookRowVM.last_msg));
|
||||
ToolTipBinding = new Binding(nameof(OrderBookRowVM.last_msg));
|
||||
}
|
||||
|
||||
public new IDataGridColumnPersistentFilter<OrderSecurityRootPair, string> filter { get; }
|
||||
|
||||
@@ -66,6 +66,10 @@ namespace mbts.spark {
|
||||
_vm = new OrderBookVM(_bus, _notification_service, _model, _settings_manager);
|
||||
_vm.load_from(_state);
|
||||
_view = new OrderBookView(_vm);
|
||||
_vm.SubscribeDisposed(
|
||||
_bus.subscribe(SparkTags.SecuritiesTag)
|
||||
.OnDispatcher()
|
||||
.Subscribe(sec_map => _view.Resources[ResourceKeys.Securities] = sec_map));
|
||||
|
||||
var show_fills_command = SparkCommands.ShowFills(SparkCommands.KeyGestures.ShowFills).complete(() => new FillsSettings(_vm.selected_orders.Select(r => r.Key).ToArray()));
|
||||
|
||||
|
||||
@@ -99,7 +99,7 @@ namespace mbts.spark {
|
||||
}
|
||||
}
|
||||
|
||||
class OrderBookRowView : DIYGrid.HighPerformanceRow<OrderBookRowVM, OrderBookGridView.ColumnKeys> {
|
||||
class OrderBookRowView : DIYGrid.HighPerformanceRow<OrderBookRowVM> {
|
||||
public OrderBookRowView() : base() { }
|
||||
}
|
||||
}
|
||||
@@ -49,8 +49,9 @@ namespace bts.shell.pnl {
|
||||
PNLHeaderRowDisplay _header_row_display = PNLHeaderRowDisplay.Total;
|
||||
PNLRowCollectionView _rows_view;
|
||||
IReadOnlyList<(ColumnKeys key, ListSortDirection direction)> _sorting_hierarchy;
|
||||
Brush _selection_highlight;
|
||||
Color _grid_background_color = Colors.Transparent;
|
||||
Color
|
||||
_grid_background_color = Colors.Transparent,
|
||||
_selection_highlight;
|
||||
bool _is_paused;
|
||||
// default appearances
|
||||
ImmutableDictionary<ColumnKeys, PNLCellAppearance>
|
||||
@@ -175,7 +176,7 @@ namespace bts.shell.pnl {
|
||||
}
|
||||
}
|
||||
|
||||
public Brush selection_highlight { get => _selection_highlight; set => Set(ref _selection_highlight, value); }
|
||||
public Color selection_highlight { get => _selection_highlight; set => Set(ref _selection_highlight, value); }
|
||||
public Color grid_background_color { get => _grid_background_color; set => Set(ref _grid_background_color, value); }
|
||||
public IValueConverter group_background_converter => _group_background_converter;
|
||||
|
||||
@@ -291,7 +292,7 @@ namespace bts.shell.pnl {
|
||||
};
|
||||
|
||||
public void load_from(PNLSettings settings) {
|
||||
selection_highlight = settings.grid.selection_highlight.to_brush();
|
||||
selection_highlight = settings.grid.selection_highlight;
|
||||
grid_background_color = settings.grid.background;
|
||||
calculation_type = settings.calculation_type;
|
||||
if (settings.filters != null) {
|
||||
|
||||
@@ -31,6 +31,8 @@ namespace bts.shell.pnl {
|
||||
}
|
||||
|
||||
partial class PNLView : UserControl {
|
||||
DIYGridPopupManager _popup_man_backing;
|
||||
|
||||
public enum Columns {
|
||||
Account = 1,
|
||||
// Security
|
||||
@@ -240,6 +242,15 @@ namespace bts.shell.pnl {
|
||||
}
|
||||
#endregion
|
||||
|
||||
DIYGridPopupManager _popup_man {
|
||||
get {
|
||||
if (_popup_man_backing == null) {
|
||||
_popup_man_backing = DIYGridPopupManager.create(this);
|
||||
}
|
||||
return _popup_man_backing;
|
||||
}
|
||||
}
|
||||
|
||||
void _cell_click(PNLCell cell) {
|
||||
Log.Ui.info($"PNLCell Click: {cell}.");
|
||||
try {
|
||||
@@ -357,41 +368,20 @@ namespace bts.shell.pnl {
|
||||
}
|
||||
}
|
||||
|
||||
async void _show_auxiliary_popup<T>(T content, string context_desc, string title_desc, Color fg) {
|
||||
// vm
|
||||
dynamic header = new ExpandoObject();
|
||||
header.context = context_desc;
|
||||
header.title = title_desc;
|
||||
header.title_foreground = fg;
|
||||
header.keep_open = false;
|
||||
// view
|
||||
var view = new PNLAuxiliaryPopupView {
|
||||
void _show_auxiliary_popup<T>(T content, string context_desc, string title_desc, Color fg) {
|
||||
var view = new HeaderedContentControl {
|
||||
Content = content,
|
||||
Header = header,
|
||||
Header = new AuxiliaryDataHeader {
|
||||
context = context_desc,
|
||||
title = title_desc,
|
||||
title_foreground = fg
|
||||
},
|
||||
HorizontalAlignment = HorizontalAlignment.Left,
|
||||
VerticalAlignment = VerticalAlignment.Top
|
||||
VerticalAlignment = VerticalAlignment.Top,
|
||||
Resources = new AuxiliaryDataResources()
|
||||
};
|
||||
await Task.Delay(100); // Wait for obvm to load rows collection before showing UI to avoid a flash
|
||||
view.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
|
||||
// popup adorner
|
||||
// tho unlikely, better not assume target cell is non-null
|
||||
var target_cell = _grid.ChildAtPoint<HyperlinkCell>(Mouse.GetPosition(_grid));
|
||||
var popup_adorner = new PopupAdorner(_grid, target_cell ?? (FrameworkElement)_grid, view, HorizontalPosition.Middle, VerticalPosition.Bottom, 0, 0) {
|
||||
OpenRelativeToMouse = target_cell == null,
|
||||
EnableDragMove = true,
|
||||
ClipToBounds = true
|
||||
};
|
||||
popup_adorner.SetBinding(PopupAdorner.StaysOpenProperty, new Binding("keep_open") { Source = header });
|
||||
popup_adorner.PopupElement.InputBindings.Add(new KeyBinding(ApplicationCommands.Close, Key.Escape, ModifierKeys.None));
|
||||
popup_adorner.PopupElement.CommandBindings.Add(new CommandBinding(ApplicationCommands.Close, (s, e) => {
|
||||
Log.assert(popup_adorner != null);
|
||||
popup_adorner.detach();
|
||||
popup_adorner = null;
|
||||
}));
|
||||
popup_adorner.attach();
|
||||
popup_adorner.IsOpen = true;
|
||||
FocusManager.SetIsFocusScope(popup_adorner.PopupElement, true);
|
||||
popup_adorner.AdornedElement.Focus();
|
||||
view.Style = view.Resources[AuxiliaryDataResources.PopupChildStyleKey] as Style;
|
||||
_popup_man.show(view);
|
||||
}
|
||||
|
||||
class PNLGroupDescription : PropertyGroupDescription, IEquatable<PNLGroupDescription> {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using mbts.ui.shell;
|
||||
using mbts.ui;
|
||||
using mbts.ui.shell;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
@@ -6,8 +7,9 @@ using System.Reactive.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace mbts.spark {
|
||||
class AlgoParamVM<T> : IAlgoParamVM<T>, INotifyPropertyChanged {
|
||||
class AlgoParamVM<T> : IAlgoParamVM<T>, INotifyPropertyChanged, ITemplateSelectorKey {
|
||||
readonly IEqualityComparer<T> _comparer;
|
||||
readonly string _template_key;
|
||||
AlgoParamPermissions _permissions;
|
||||
bool _is_enabled, _has_value;
|
||||
T _committed_value, _value;
|
||||
@@ -15,15 +17,16 @@ namespace mbts.spark {
|
||||
public AlgoParamVM(string id, string display_name)
|
||||
: this(id, display_name, default, EqualityComparer<T>.Default) { }
|
||||
|
||||
public AlgoParamVM(string id, string display_name, T value)
|
||||
: this(id, display_name, value, EqualityComparer<T>.Default) { }
|
||||
public AlgoParamVM(string id, string display_name, T value, string template_key = null)
|
||||
: this(id, display_name, value, EqualityComparer<T>.Default, template_key) { }
|
||||
|
||||
public AlgoParamVM(string id, string display_name, T value, IEqualityComparer<T> comparer) {
|
||||
public AlgoParamVM(string id, string display_name, T value, IEqualityComparer<T> comparer, string template_key = null) {
|
||||
this.id = id;
|
||||
this.display_name = display_name;
|
||||
_value = value;
|
||||
_committed_value = value;
|
||||
_comparer = comparer;
|
||||
_template_key = template_key;
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
@@ -50,7 +53,9 @@ namespace mbts.spark {
|
||||
public bool is_enabled { get => _is_enabled; set => _set(ref _is_enabled, value); }
|
||||
public bool has_value { get => _has_value; set => _set(ref _has_value, value); }
|
||||
public bool is_dirty { get; private set; }
|
||||
object IAlgoParamVM.value { get => value; set => this.value = (T)value; }
|
||||
object IAlgoParamVM.value { get => value; set => this.value = (T)value; }
|
||||
|
||||
string ITemplateSelectorKey.key => _template_key;
|
||||
|
||||
public void revert() {
|
||||
if (is_dirty) {
|
||||
|
||||
@@ -62,6 +62,7 @@ namespace mbts.spark {
|
||||
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveInt: param.AsInt32 = 0; break;
|
||||
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveOrderDef: param.AsOrderDef = new CustomOrderDefinition(); break;
|
||||
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveString: param.AsString = string.Empty; break;
|
||||
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveSecurityKey:
|
||||
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveUint: param.AsUint32 = 0u; break;
|
||||
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveQuantity: param.AsQuantity = new bts.spark.QuantityMeasure(); break;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ using System.Reactive.Linq;
|
||||
namespace mbts.spark {
|
||||
interface IDynamicOrderParamVM {
|
||||
DynamicOrderParamDescriptor descriptor { get; }
|
||||
DynamicOrderParam param { get; set; }
|
||||
DynamicOrderParam param { get; }
|
||||
}
|
||||
|
||||
interface IDynamicOrderParamVM<T> : IDynamicOrderParamVM {
|
||||
@@ -21,26 +21,16 @@ namespace mbts.spark {
|
||||
}
|
||||
|
||||
abstract class DynamicOrderParamVM : DisposableViewModelBase, IDynamicOrderParamVM {
|
||||
DynamicOrderParam _param;
|
||||
|
||||
protected DynamicOrderParamVM(DynamicOrderParamDescriptor descriptor)
|
||||
: this(descriptor, DynamicOrderHelper.init_param(descriptor)) { }
|
||||
|
||||
private DynamicOrderParamVM(DynamicOrderParamDescriptor descriptor, DynamicOrderParam param) {
|
||||
this.descriptor = descriptor;
|
||||
_param = param;
|
||||
this.param = param;
|
||||
}
|
||||
|
||||
public DynamicOrderParamDescriptor descriptor { get; set; }
|
||||
public DynamicOrderParam param {
|
||||
get => _param;
|
||||
set {
|
||||
_param = value;
|
||||
on_param_changed();
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void on_param_changed();
|
||||
public DynamicOrderParam param { get; }
|
||||
}
|
||||
|
||||
class DynamicOrderParamVM<TView, TModel> : DynamicOrderParamVM, IDynamicOrderParamVM<TView> {
|
||||
@@ -53,14 +43,12 @@ namespace mbts.spark {
|
||||
_field_desc = field_descriptor;
|
||||
_convert = convert;
|
||||
_convert_back = convert_back;
|
||||
value = _convert(ConvertUtils.try_convert<TModel>(_field_desc.Accessor.GetValue(param)));
|
||||
}
|
||||
|
||||
public TView value {
|
||||
get => _value;
|
||||
set {
|
||||
_field_desc.Accessor.SetValue(param, _convert_back(_value = value));
|
||||
RaisePropertyChanged();
|
||||
}
|
||||
set => Set(ref _value, value);
|
||||
}
|
||||
|
||||
public IObservable<TView> subscribe_value() =>
|
||||
@@ -70,11 +58,6 @@ namespace mbts.spark {
|
||||
void on_prop_changed(object sender, PropertyChangedEventArgs e) =>
|
||||
o.OnNext(value);
|
||||
});
|
||||
|
||||
protected override void on_param_changed() {
|
||||
_value = _convert(ConvertUtils.try_convert<TModel>(_field_desc.Accessor.GetValue(param)));
|
||||
RaisePropertyChanged(nameof(value));
|
||||
}
|
||||
}
|
||||
|
||||
class DynamicOrderEnumeratedVM : DynamicOrderParamVM, IDynamicOrderParamVM<string> {
|
||||
@@ -105,11 +88,6 @@ namespace mbts.spark {
|
||||
o.OnNext(selected_element);
|
||||
});
|
||||
|
||||
protected override void on_param_changed() {
|
||||
_sync_elements();
|
||||
RaisePropertyChanged(nameof(selected_element));
|
||||
}
|
||||
|
||||
static IEnumerable<string> _get_elements(DynamicOrderParamDescriptor descriptor) =>
|
||||
descriptor.ParamType?.DescriptorCase == DynamicOrderTypeDescriptor.DescriptorOneofCase.Enumerated ?
|
||||
descriptor.ParamType.Enumerated.Element : Enumerable.Empty<string>();
|
||||
@@ -148,10 +126,5 @@ namespace mbts.spark {
|
||||
o.OnNext(quantity);
|
||||
});
|
||||
}
|
||||
|
||||
protected override void on_param_changed()
|
||||
{
|
||||
RaisePropertyChanged(nameof(quantity));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ namespace mbts.ui.spark
|
||||
public DataTemplate StringTemplate { get; set; }
|
||||
public DataTemplate OrderDefTemplate { get; set; }
|
||||
public DataTemplate QuantityMeasureTemplate { get; set; }
|
||||
public DataTemplate SecurityKeyTemplate { get; set; }
|
||||
|
||||
public DataTemplate EnumTemplate { get; set; }
|
||||
|
||||
@@ -38,6 +39,7 @@ namespace mbts.ui.spark
|
||||
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveDouble: return DoubleTemplate;
|
||||
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveInt: return IntTemplate;
|
||||
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveUint: return UIntTemplate;
|
||||
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveSecurityKey: return SecurityKeyTemplate;
|
||||
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveString: return StringTemplate;
|
||||
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveOrderDef: return OrderDefTemplate;
|
||||
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveQuantity: return QuantityMeasureTemplate;
|
||||
|
||||
@@ -4,6 +4,7 @@ using bts.collections;
|
||||
using bts.spark;
|
||||
using mbts.collections;
|
||||
using mbts.spark.utilities;
|
||||
using mbts.ui;
|
||||
using mbts.ui.utility;
|
||||
using mbts.utils;
|
||||
using System;
|
||||
@@ -73,9 +74,9 @@ namespace mbts.spark {
|
||||
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveDouble: return mk<double, double>(DynamicOrderParam.AsDoubleFieldNumber, v => v, v => v, double.NaN);
|
||||
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveInt: return mk<int?, int>(DynamicOrderParam.AsInt32FieldNumber, v => v, v => v.GetValueOrDefault());
|
||||
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveUint: return mk<uint?, uint>(DynamicOrderParam.AsUint32FieldNumber, v => v, v => v.GetValueOrDefault());
|
||||
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveSecurityKey: return mk<uint?, uint>(DynamicOrderParam.AsUint32FieldNumber, v => v, v => v.GetValueOrDefault(), template_key: TemplateKeys.SecurityKey);
|
||||
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveString: return mk<string, string>(DynamicOrderParam.AsStringFieldNumber, v => v, v => v);
|
||||
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveQuantity:
|
||||
{
|
||||
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveQuantity: {
|
||||
var qm = new DynamicOrderQuantityVM(descriptor);
|
||||
return attached(qm, DynamicOrderHelper.get_quantity_value, DynamicOrderHelper.set_quantity_value, new QuantityMeasure { Ratio = 1.0, Absolute = 1 });
|
||||
}
|
||||
@@ -88,16 +89,16 @@ namespace mbts.spark {
|
||||
default: throw ExceptionBuilder.Argument.unexpected_case(descriptor.ParamType.DescriptorCase, nameof(descriptor));
|
||||
}
|
||||
|
||||
DynamicOrderParamVM<TView, TModel> mk<TView, TModel>(int field_number, Func<TModel, TView> convert, Func<TView, TModel> convert_back, TView default_value = default) {
|
||||
DynamicOrderParamVM<TView, TModel> mk<TView, TModel>(int field_number, Func<TModel, TView> convert, Func<TView, TModel> convert_back, TView default_value = default, string template_key = null) {
|
||||
var fdesc = DynamicOrderParam.Descriptor.FindFieldByNumber(field_number);
|
||||
var pvm = new DynamicOrderParamVM<TView, TModel>(descriptor, fdesc, convert, convert_back);
|
||||
return attached<TView, DynamicOrderParamVM<TView, TModel>>(pvm, get_from_param, assign_to_param, default_value);
|
||||
return attached<TView, DynamicOrderParamVM<TView, TModel>>(pvm, get_from_param, assign_to_param, default_value, template_key);
|
||||
|
||||
TView get_from_param(DynamicOrderParam param) => convert(ConvertUtils.try_convert<TModel>(fdesc.Accessor.GetValue(param)));
|
||||
void assign_to_param(DynamicOrderParam param, TView value) => fdesc.Accessor.SetValue(param, convert_back(value));
|
||||
}
|
||||
|
||||
TParam attached<T, TParam>(TParam pvm, Func<DynamicOrderParam, T> selector, Action<DynamicOrderParam, T> assign_to_param, T default_value = default) where TParam : DynamicOrderParamVM, IDynamicOrderParamVM<T> {
|
||||
TParam attached<T, TParam>(TParam pvm, Func<DynamicOrderParam, T> selector, Action<DynamicOrderParam, T> assign_to_param, T default_value = default, string template_key = null) where TParam : DynamicOrderParamVM, IDynamicOrderParamVM<T> {
|
||||
add_property<T>(
|
||||
pvm.descriptor.ParamName,
|
||||
mdef => mdef.OrderParams.TryGetValue(pvm.descriptor.ParamName, out DynamicOrderParam p) ? selector(p) : default_value,
|
||||
@@ -105,7 +106,8 @@ namespace mbts.spark {
|
||||
() => pvm.value,
|
||||
v => pvm.value = v,
|
||||
pvm.subscribe_value(),
|
||||
default_value);
|
||||
default_value,
|
||||
template_key: template_key);
|
||||
return pvm;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,6 +89,7 @@ namespace mbts.spark {
|
||||
case AlgoType.Dynamic: vm = dynamic(key, name, name, editing_permissions); break;
|
||||
case AlgoType.Splitter: vm = splitter(key,name,editing_permissions); break;
|
||||
case AlgoType.Composite: vm = composite(key, name, editing_permissions); break;
|
||||
case AlgoType.None: vm = new DefaultAlgoVM(); break;
|
||||
default: vm = generic(key, name, algo_type, editing_permissions); break;
|
||||
//default:
|
||||
// Log.Ui.debug_fatal($"Failed to create {typeof(IAlgoVM)}: unknown algo type '{algo_type}.'");
|
||||
@@ -166,7 +167,8 @@ namespace mbts.spark {
|
||||
};
|
||||
void subscribe_new_algo(IAlgoVM old_algo, IAlgoVM new_algo) {
|
||||
RaisePropertyChanged(sd.prop_name);
|
||||
Disposable.replace(ref def_sub, new CompositeDisposable());
|
||||
Disposable.dispose(ref def_sub);
|
||||
def_sub = new CompositeDisposable();
|
||||
if (assert(new_algo != null, sd.prop_name, "No algo specified.")) {
|
||||
def_sub.Add(new_algo.subscribe_definition().Subscribe(_ => _def_change_notifier.execute()));
|
||||
if (new_algo is INotifyDataErrorInfo ndei)
|
||||
@@ -255,14 +257,14 @@ namespace mbts.spark {
|
||||
_security_changed += o.OnNext;
|
||||
return Disposable.Create(() => _security_changed -= o.OnNext);
|
||||
});
|
||||
Disposable.replace(ref _pending_load, Disposable.Create(
|
||||
Disposable.dispose(ref _pending_load);
|
||||
_pending_load = Disposable.Create(
|
||||
// load children
|
||||
ObservableAutoWrapper.connect_item_hooks(
|
||||
_child_algo_selector_data.AsCollection(),
|
||||
sd => security_src.SubscribeEach(_ => ObservableAutoWrapper.connect_item_actions(_child_algo_selector_data.AsCollection(), selector_load_impl))),
|
||||
// load regular props
|
||||
ObservableAutoWrapper.connect_item_actions(_auto_props.AsCollection(), p => p.load(algom))));
|
||||
|
||||
ObservableAutoWrapper.connect_item_actions(_auto_props.AsCollection(), p => p.load(algom)));
|
||||
}
|
||||
|
||||
void selector_load_impl(AlgoSelectorData sd) {
|
||||
@@ -298,11 +300,12 @@ namespace mbts.spark {
|
||||
_security_changed += o.OnNext;
|
||||
return Disposable.Create(() => _security_changed -= o.OnNext);
|
||||
});
|
||||
Disposable.replace(ref _pending_load, Disposable.Create(
|
||||
Disposable.dispose(ref _pending_load);
|
||||
_pending_load = Disposable.Create(
|
||||
ObservableAutoWrapper.connect_item_hooks(
|
||||
_child_algo_selector_data.AsCollection(),
|
||||
sd => security_src.Subscribe(_ => ObservableAutoWrapper.connect_item_actions(_child_algo_selector_data.AsCollection(), selector_load_impl))),
|
||||
ObservableAutoWrapper.connect_item_actions(_auto_props.AsCollection(), p => p.load_many(algos))));
|
||||
ObservableAutoWrapper.connect_item_actions(_auto_props.AsCollection(), p => p.load_many(algos)));
|
||||
|
||||
void selector_load_impl(AlgoSelectorData sd) {
|
||||
// **assume that algos has more than 1 element
|
||||
@@ -542,8 +545,19 @@ namespace mbts.spark {
|
||||
}
|
||||
}
|
||||
|
||||
protected void add_property<TValue>(string prop_name, Func<TMutable, TValue> get_from_def, Action<TMutable, TValue> assign_to_def, Func<TValue> get_local, Action<TValue> set_local, IObservable<TValue> subscribe_local, TValue non_uniform_value = default, AlgoParamPermissions permissions = AlgoParamPermissions.ReadWrite) {
|
||||
var param = new AlgoParamVM<TValue>(prop_name, DisplayNameHelper.snake_to_title_case(prop_name), get_local()) { permissions = permissions };
|
||||
protected void add_property<TValue>(
|
||||
string prop_name,
|
||||
Func<TMutable, TValue> get_from_def,
|
||||
Action<TMutable, TValue> assign_to_def,
|
||||
Func<TValue> get_local,
|
||||
Action<TValue> set_local,
|
||||
IObservable<TValue> subscribe_local,
|
||||
TValue non_uniform_value = default,
|
||||
AlgoParamPermissions permissions = AlgoParamPermissions.ReadWrite,
|
||||
string template_key = null) {
|
||||
var param = new AlgoParamVM<TValue>(prop_name, DisplayNameHelper.snake_to_title_case(prop_name), get_local(), template_key) {
|
||||
permissions = permissions
|
||||
};
|
||||
_algo_params.Add(param);
|
||||
_auto_props[prop_name] = new AutoProp(prop_name,
|
||||
def => {
|
||||
@@ -816,12 +830,11 @@ namespace mbts.spark {
|
||||
var default_algo = AlgoKey.from_known_type(OrderDataHelper.get_default_algo(OrderDataHelper.get_algo_format(security.Value.ContractCase)));
|
||||
foreach (var sd in _child_algo_selectors)
|
||||
sd.load_algo_data_recursive(algo_data, default_algo);
|
||||
Disposable.replace(
|
||||
ref _selector_data_hooks,
|
||||
ObservableAutoWrapper.connect_item_hooks(child_algo_selectors, s => {
|
||||
s.load_algo_data_recursive(algo_data, default_algo);
|
||||
return Disposable.empty;
|
||||
}));
|
||||
Disposable.dispose(ref _selector_data_hooks);
|
||||
_selector_data_hooks = ObservableAutoWrapper.connect_item_hooks(child_algo_selectors, s => {
|
||||
s.load_algo_data_recursive(algo_data, default_algo);
|
||||
return Disposable.empty;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
<ResourceDictionary Source="/view/Controls/DataGridEx/Filters/Views/DoubleMinMaxFilterControl.xaml"/>
|
||||
<ResourceDictionary Source="/view/Controls/DataGridEx/Filters/Views/DiscreteValuesFilterControl.xaml"/>
|
||||
<ResourceDictionary Source="/view/Controls/DataGridEx/Filters/Views/TimeFilterControl.xaml"/>
|
||||
<ResourceDictionary Source="/view/Controls/ActionMenu/ActionMenu.xaml"/>
|
||||
<ResourceDictionary Source="/Modules/Spreader/ExecutionStrategyEditor.xaml"/>
|
||||
<ResourceDictionary Source="/view/Controls/Order/AlgoContentTemplateSelector.xaml"/>
|
||||
<ResourceDictionary Source="/view/Controls/Order/AlgoPickerNodeView.xaml"/>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ namespace mbts {
|
||||
public static bool IsNaNOrInfinity(double d) => double.IsNaN(d) || double.IsInfinity(d);
|
||||
public static bool IsNaNOrInfinityOr(double d, params double[] values) => IsNaNOrInfinity(d) || values.Contains(d);
|
||||
public static bool IsOneOf(double d, params double[] values) => values.Contains(d);
|
||||
public static bool IsRealNonZero(this double d) => d != 0 && !double.IsNaN(d) && !double.IsInfinity(d);
|
||||
public static double CoerceNaN(double value) => CoerceNaN(value, 0.0);
|
||||
public static double CoerceNaN(double value, double if_nan) => double.IsNaN(value) ? if_nan : value;
|
||||
public static double CoerceNaNOrInfinity(double value) => CoerceNaNOrInfinity(value, 0.0);
|
||||
|
||||
@@ -131,6 +131,11 @@ namespace mbts.ui.native
|
||||
[DllImport("user32.dll")]
|
||||
public static extern bool GetCursorPos(out Win32Point pt);
|
||||
|
||||
public static readonly IntPtr HWND_TOPMOST = new IntPtr(-1);
|
||||
public static readonly IntPtr HWND_NOTOPMOST = new IntPtr(-2);
|
||||
public static readonly IntPtr HWND_TOP = new IntPtr(0);
|
||||
public static readonly IntPtr HWND_BOTTOM = new IntPtr(1);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
public static extern bool SetWindowPos(IntPtr hwnd, IntPtr hwnd_insert_after, int x, int y, int cx, int cy, SWP flags);
|
||||
|
||||
@@ -255,7 +260,20 @@ namespace mbts.ui.native
|
||||
{
|
||||
SWP_NOSIZE = 1,
|
||||
SWP_NOMOVE = 2,
|
||||
SWP_FRAMECHANGED = 0x20
|
||||
SWP_NOZORDER = 0x0004,
|
||||
SWP_NOREDRAW = 0x0008,
|
||||
SWP_NOACTIVATE = 0x0010,
|
||||
// The frame changed: send WM_NCCALCSIZE
|
||||
SWP_FRAMECHANGED = 0x0020,
|
||||
SWP_SHOWWINDOW = 0x0040,
|
||||
SWP_HIDEWINDOW = 0x0080,
|
||||
SWP_NOCOPYBITS = 0x0100,
|
||||
// Don’t do owner Z ordering
|
||||
SWP_NOOWNERZORDER = 0x0200,
|
||||
// Don’t send WM_WINDOWPOSCHANGING
|
||||
SWP_NOSENDCHANGING = 0x0400,
|
||||
|
||||
TOPMOST_FLAGS = SWP_NOACTIVATE | SWP_NOOWNERZORDER | SWP_NOSIZE | SWP_NOMOVE | SWP_NOREDRAW | SWP_NOSENDCHANGING
|
||||
}
|
||||
|
||||
public enum SPI : uint
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
using System;
|
||||
|
||||
namespace mbts.ui.native {
|
||||
|
||||
}
|
||||
@@ -138,6 +138,7 @@ namespace mbts.ui.native
|
||||
NCMBUTTONDOWN = 0x00A7,
|
||||
NCMBUTTONUP = 0x00A8,
|
||||
NCMOUSEMOVE = 0x00A0,
|
||||
NCMOUSELEAVE = 0x02A2,
|
||||
NCPAINT = 0x0085,
|
||||
NCRBUTTONDBLCLK = 0x00A6,
|
||||
NCRBUTTONDOWN = 0x00A4,
|
||||
|
||||
@@ -15,6 +15,7 @@ namespace bts.settings {
|
||||
// determine the various windows to which such rules apply.
|
||||
interface IFillsGridSettingsMigrationRule : ISettingsMigrationRule<GridSettings> { }
|
||||
interface IMarketViewGridSettingsMigrationRule : ISettingsMigrationRule<GridSettings> { }
|
||||
interface ITimeAndSalesGridSettingsMigrationRule : ISettingsMigrationRule<GridSettings> { }
|
||||
interface IPAndLGridSettingsMigrationRule : ISettingsMigrationRule<GridSettings> { }
|
||||
interface IPNLSettingsMigrationRule : ISettingsMigrationRule<PNLSettings> { }
|
||||
interface ISyntheticManagementMigrationRule : ISettingsMigrationRule<GridSettings> { }
|
||||
|
||||
@@ -26,7 +26,8 @@ namespace mbts.persist {
|
||||
_fills_propi = ImmutableProperty.create<UserSettings, GridSettings>(us => us.fills_grid, (us, gs) => us.with(provided_fills_gridsettings: gs)),
|
||||
_mview_propi = ImmutableProperty.create<UserSettings, GridSettings>(us => us.market_view_grid, (us, gs) => us.with(provided_market_view_gridsettings: gs)),
|
||||
_pandl_propi = ImmutableProperty.create<UserSettings, GridSettings>(us => us.pnl_grid, (us, gs) => us.with(provided_pnl_gridsettings: gs)),
|
||||
_synth_propi = ImmutableProperty.create<UserSettings, GridSettings>(us => us.synthetic_management_grid, (us, gs) => us.with(provided_synthetic_management_gridsettings: gs));
|
||||
_synth_propi = ImmutableProperty.create<UserSettings, GridSettings>(us => us.synthetic_management_grid, (us, gs) => us.with(provided_synthetic_management_gridsettings: gs)),
|
||||
_time_and_sales_propi = ImmutableProperty.create<UserSettings, GridSettings>(us => us.time_and_sales_grid, (us, gs) => us.with(time_and_sales: gs));
|
||||
|
||||
static readonly IImmutableProperty<UserSettings, PNLSettings>
|
||||
_pnl_propi = ImmutableProperty.create<UserSettings, PNLSettings>(us => us.pnl, (us, pnl) => us.with(pnl: pnl));
|
||||
@@ -38,6 +39,7 @@ namespace mbts.persist {
|
||||
readonly IEnumerable<IPAndLGridSettingsMigrationRule> _pandl_migration_rules;
|
||||
readonly IEnumerable<IPNLSettingsMigrationRule> _pnl_migration_rules;
|
||||
readonly IEnumerable<ISyntheticManagementMigrationRule> _synth_migration_rules;
|
||||
readonly IEnumerable<ITimeAndSalesGridSettingsMigrationRule> _time_and_sales_migration_rules;
|
||||
readonly IUserPersist _persist;
|
||||
|
||||
UserSettings _default_settings;
|
||||
@@ -49,7 +51,8 @@ namespace mbts.persist {
|
||||
[ImportMany]IEnumerable<IMarketViewGridSettingsMigrationRule> mview_migration_rules,
|
||||
[ImportMany]IEnumerable<IPAndLGridSettingsMigrationRule> pandl_migration_rules,
|
||||
[ImportMany]IEnumerable<IPNLSettingsMigrationRule> pnl_migration_rules,
|
||||
[ImportMany]IEnumerable<ISyntheticManagementMigrationRule> synth_migration_rules) {
|
||||
[ImportMany]IEnumerable<ISyntheticManagementMigrationRule> synth_migration_rules,
|
||||
[ImportMany]IEnumerable<ITimeAndSalesGridSettingsMigrationRule> time_and_sales_migration_rules) {
|
||||
//
|
||||
_persist = persist;
|
||||
_fills_migration_rules = fills_migration_rules ?? Enumerable.Empty<IFillsGridSettingsMigrationRule>();
|
||||
@@ -57,6 +60,7 @@ namespace mbts.persist {
|
||||
_pandl_migration_rules = pandl_migration_rules ?? Enumerable.Empty<IPAndLGridSettingsMigrationRule>();
|
||||
_pnl_migration_rules = pnl_migration_rules ?? Enumerable.Empty<IPNLSettingsMigrationRule>();
|
||||
_synth_migration_rules = synth_migration_rules ?? Enumerable.Empty<ISyntheticManagementMigrationRule>();
|
||||
_time_and_sales_migration_rules = time_and_sales_migration_rules ?? Enumerable.Empty<ITimeAndSalesGridSettingsMigrationRule>();
|
||||
init();
|
||||
async void init() {
|
||||
UserSettings s;
|
||||
@@ -68,7 +72,8 @@ namespace mbts.persist {
|
||||
_mview_migration_rules.merge().wrap(_mview_propi),
|
||||
_pandl_migration_rules.merge().wrap(_pandl_propi),
|
||||
_pnl_migration_rules.merge().wrap(_pnl_propi),
|
||||
_synth_migration_rules.merge().wrap(_synth_propi)
|
||||
_synth_migration_rules.merge().wrap(_synth_propi),
|
||||
_time_and_sales_migration_rules.merge().wrap(_time_and_sales_propi)
|
||||
};
|
||||
SettingsMigrationHelper.apply_quiet(ref s, wrapped_rules);
|
||||
// TODO: move ladder sanitization logic into ISettingsMigrationRule
|
||||
@@ -148,6 +153,12 @@ namespace mbts.persist {
|
||||
_synth_migration_rules,
|
||||
_synth_propi);
|
||||
|
||||
public IWindowSettingsManager<GridSettings> create_time_and_sales_settings_manager(GridSettings instance_settings) =>
|
||||
_create_settings_manager(
|
||||
instance_settings,
|
||||
_time_and_sales_migration_rules,
|
||||
_time_and_sales_propi);
|
||||
|
||||
public async Task save_default(UserSettings value) {
|
||||
// notify observers before persisting so the changes are reflected immediately in the ui
|
||||
lock (_default_settings_lock) {
|
||||
|
||||
@@ -5,9 +5,23 @@
|
||||
xmlns:ui="clr-namespace:mbts.ui"
|
||||
>
|
||||
<!--Control Base Colors-->
|
||||
<Color x:Key="Control.SuperLightGrayColor">#F6F6F6</Color>
|
||||
<Color x:Key="Control.LightGrayColor">#E5E5E5</Color>
|
||||
<Color x:Key="Control.LightMediumGrayColor">#CFCFCF</Color>
|
||||
<Color x:Key="Control.MediumGrayColor">#B3B3B3</Color>
|
||||
<Color x:Key="Control.MediumDarkGrayColor">#ACACAC</Color>
|
||||
<Color x:Key="Control.AlphaLightGrayColor">#10000000</Color>
|
||||
<Color x:Key="Control.AlphaLightMediumGrayColor">#24000000</Color>
|
||||
<Color x:Key="Control.AlphaMediumGrayColor">#3A000000</Color>
|
||||
<Color x:Key="Control.AlphaMediumGrayColor2">#FF0000</Color>
|
||||
<Color x:Key="Control.DarkMediumGrayColor">#FF959595</Color>
|
||||
<Color x:Key="Control.DarkGrayColor">#707070</Color>
|
||||
<Color x:Key="Text.Prompt.Foreground.Color">#FF999999</Color>
|
||||
|
||||
<SolidColorBrush x:Key="Control.AlphaLightGrayBrush" Color="{StaticResource Control.AlphaLightGrayColor}"/>
|
||||
<SolidColorBrush x:Key="Control.AlphaLightMediumGrayBrush" Color="{StaticResource Control.AlphaLightMediumGrayColor}"/>
|
||||
<SolidColorBrush x:Key="Control.LightGrayBrush" Color="{StaticResource Control.LightGrayColor}"/>
|
||||
<SolidColorBrush x:Key="Control.LightMediumGrayBrush" Color="{StaticResource Control.LightMediumGrayColor}"/>
|
||||
<SolidColorBrush x:Key="Control.MediumGrayBrush" Color="{StaticResource Control.MediumGrayColor}"/>
|
||||
<SolidColorBrush x:Key="CommonBackground" Color="{ui:BTSColor common_background}" />
|
||||
<SolidColorBrush x:Key="ControlBackground" Color="{x:Static mbts:BTSColors.common_background}" />
|
||||
@@ -130,4 +144,4 @@
|
||||
<SolidColorBrush x:Key="ListBoxItem.Selected.Inactive.Border" Color="{StaticResource ListBoxItem.Selected.Inactive.Border.Color}"/>
|
||||
<SolidColorBrush x:Key="ListBoxItem.MouseOver.Background" Color="{StaticResource ListBoxItem.MouseOver.Background.Color}"/>
|
||||
<SolidColorBrush x:Key="ListBoxItem.MouseOver.Border" Color="{StaticResource ListBoxItem.MouseOver.Border.Color}"/>
|
||||
</ResourceDictionary>
|
||||
</ResourceDictionary>
|
||||
@@ -1,78 +1,102 @@
|
||||
<c:PopupControl
|
||||
x:Class="mbts.ui.controls.ActionMenu"
|
||||
<ResourceDictionary
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:u="clr-namespace:mbts.ui"
|
||||
xmlns:c="clr-namespace:mbts.ui.controls"
|
||||
Margin="1"
|
||||
Background="Transparent"
|
||||
BorderBrush="Transparent"
|
||||
Padding="2"
|
||||
PopupPlacement="Bottom"
|
||||
ShowExpanderArrow="False"
|
||||
Cursor="Hand"
|
||||
xmlns:cv="clr-namespace:mbts.ui.converters"
|
||||
>
|
||||
<c:PopupControl.Resources>
|
||||
<CollectionViewSource x:Key="ItemsSourceView" Source="{Binding ItemsSource, RelativeSource={RelativeSource AncestorType=c:ActionMenu}}">
|
||||
<CollectionViewSource.GroupDescriptions>
|
||||
<PropertyGroupDescription PropertyName="group"/>
|
||||
</CollectionViewSource.GroupDescriptions>
|
||||
</CollectionViewSource>
|
||||
</c:PopupControl.Resources>
|
||||
<c:PopupControl.Header>
|
||||
<Border Width="16">
|
||||
<SolidColorBrush x:Key="SeparatorFill" Color="{DynamicResource Control.AlphaMediumGrayColor}"/>
|
||||
|
||||
<ControlTemplate x:Key="ActionMenuTemplate" TargetType="c:ActionMenu">
|
||||
<Border
|
||||
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
|
||||
VerticalAlignment="{TemplateBinding VerticalAlignment}"
|
||||
Width="{TemplateBinding Width}"
|
||||
Height="{TemplateBinding Height}"
|
||||
>
|
||||
<u:StateManager.Groups>
|
||||
<u:StateGroupsManager AttachToMembers="True">
|
||||
<u:StateDefinitionCollection>
|
||||
<u:MouseStateDefinition MouseSource="{Binding Group.Target, RelativeSource={RelativeSource Self}}"/>
|
||||
</u:StateDefinitionCollection>
|
||||
</u:StateGroupsManager>
|
||||
</u:StateManager.Groups>
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup Name="MouseStates">
|
||||
<VisualState Name="Normal"/>
|
||||
<VisualState Name="MouseOver" Storyboard="{DynamicResource Button.MouseOver.Storyboard}"/>
|
||||
</VisualStateGroup>
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition/>
|
||||
<RowDefinition/>
|
||||
<RowDefinition/>
|
||||
</Grid.RowDefinitions>
|
||||
<Rectangle Grid.Row="0" Fill="Black" Height="2" HorizontalAlignment="Stretch"/>
|
||||
<Rectangle Grid.Row="1" Fill="Black" Height="2" Margin="0,4,0,0" HorizontalAlignment="Stretch"/>
|
||||
<Rectangle Grid.Row="2" Fill="Black" Height="2" Margin="0,4,0,0" HorizontalAlignment="Stretch"/>
|
||||
<Button
|
||||
x:Name="button"
|
||||
Content="{TemplateBinding Header}"
|
||||
ContentTemplate="{TemplateBinding HeaderTemplate}"
|
||||
ContentTemplateSelector="{TemplateBinding HeaderTemplateSelector}"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Padding="2.5,2"
|
||||
Style="{DynamicResource SparkToolBarButton}"
|
||||
>
|
||||
<Button.Background>
|
||||
<SolidColorBrush/>
|
||||
</Button.Background>
|
||||
</Button>
|
||||
<Popup c:PopupViewController.ButtonSource="{Binding ElementName=button}" Placement="Bottom" PlacementTarget="{Binding ElementName=button}" StaysOpen="False">
|
||||
<Border Background="{DynamicResource Control.LightGrayBrush}" BorderThickness="1">
|
||||
<Border.BorderBrush>
|
||||
<SolidColorBrush Color="{DynamicResource Control.DarkMediumGrayColor}"/>
|
||||
</Border.BorderBrush>
|
||||
<ItemsPresenter/>
|
||||
</Border>
|
||||
</Popup>
|
||||
</Grid>
|
||||
</Border>
|
||||
</c:PopupControl.Header>
|
||||
<Menu ItemsSource="{Binding Source={StaticResource ItemsSourceView}}">
|
||||
<Menu.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Orientation="Vertical"/>
|
||||
</ItemsPanelTemplate>
|
||||
</Menu.ItemsPanel>
|
||||
<Menu.GroupStyle>
|
||||
<GroupStyle>
|
||||
<GroupStyle.ContainerStyle>
|
||||
<Style TargetType="GroupItem">
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="GroupItem">
|
||||
<StackPanel>
|
||||
<ItemsPresenter/>
|
||||
<Rectangle HorizontalAlignment="Stretch" Height="1" Margin="22,0,0,0" Fill="LightGray"/>
|
||||
</StackPanel>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</GroupStyle.ContainerStyle>
|
||||
</GroupStyle>
|
||||
</Menu.GroupStyle>
|
||||
<Menu.ItemContainerStyle>
|
||||
<Style TargetType="MenuItem" BasedOn="{StaticResource {x:Type MenuItem}}">
|
||||
<!--NOTE: CommandParameter binding must be declared BEFORE Command,
|
||||
otherwise, CanExecuted is called before the parameter value is realized...I know, it sucks...-->
|
||||
<Setter Property="CommandParameter" Value="{Binding parameter}"/>
|
||||
<Setter Property="Command" Value="{Binding command}"/>
|
||||
<Setter Property="Header" Value="{Binding name}"/>
|
||||
<Setter Property="HeaderTemplate">
|
||||
<Setter.Value>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding}" VerticalAlignment="Center"/>
|
||||
</DataTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
<Setter Property="Icon" Value="{Binding icon, Converter={StaticResource IconConverter}}"/>
|
||||
<Setter Property="MinWidth" Value="167"/>
|
||||
</Style>
|
||||
</Menu.ItemContainerStyle>
|
||||
</Menu>
|
||||
</c:PopupControl>
|
||||
</ControlTemplate>
|
||||
|
||||
<DataTemplate x:Key="HamburgerButtonTemplate">
|
||||
<Image
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Source="{x:Static u:Icons.HamburgerMenu}"
|
||||
/>
|
||||
</DataTemplate>
|
||||
|
||||
<Style x:Key="ActionMenuBase" TargetType="c:ActionMenu">
|
||||
<Setter Property="Background">
|
||||
<Setter.Value>
|
||||
<SolidColorBrush/>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
<Setter Property="BorderBrush">
|
||||
<Setter.Value>
|
||||
<SolidColorBrush/>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
<Setter Property="Template" Value="{StaticResource ActionMenuTemplate}"/>
|
||||
</Style>
|
||||
|
||||
<DataTemplate DataType="{x:Type u:MenuItemVM}">
|
||||
<TextBlock Text="{Binding}" VerticalAlignment="Center"/>
|
||||
</DataTemplate>
|
||||
|
||||
<Style TargetType="c:ActionMenu" BasedOn="{StaticResource ActionMenuBase}">
|
||||
<Setter Property="Panel.ZIndex" Value="1"/>
|
||||
<Setter Property="HorizontalAlignment" Value="Right"/>
|
||||
<Setter Property="VerticalAlignment" Value="Top"/>
|
||||
<Setter Property="Margin" Value="1"/>
|
||||
<Setter Property="HeaderTemplate" Value="{StaticResource HamburgerButtonTemplate}"/>
|
||||
<Setter Property="Width" Value="22"/>
|
||||
<Setter Property="ItemContainerStyle">
|
||||
<Setter.Value>
|
||||
<Style TargetType="MenuItem">
|
||||
<Setter Property="MinWidth" Value="167"/>
|
||||
<Setter Property="CommandParameter" Value="{Binding parameter}"/>
|
||||
<Setter Property="Command" Value="{Binding command}"/>
|
||||
<Setter Property="Header" Value="{Binding name}"/>
|
||||
<Setter Property="Icon" Value="{Binding icon, Converter={x:Static cv:ImageSourceIconConverter.Instance}}"/>
|
||||
</Style>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
@@ -1,22 +1,15 @@
|
||||
using System.Collections;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace mbts.ui.controls {
|
||||
/// <summary>
|
||||
/// Interaction logic for ActionMenu.xaml
|
||||
/// </summary>
|
||||
partial class ActionMenu : PopupControl {
|
||||
public ActionMenu() =>
|
||||
InitializeComponent();
|
||||
class ActionMenu : HeaderedItemsControl {
|
||||
static ActionMenu() =>
|
||||
DefaultStyleKeyProperty.OverrideMetadata(typeof(ActionMenu), new FrameworkPropertyMetadata(typeof(ActionMenu)));
|
||||
|
||||
public static readonly DependencyProperty ItemsSourceProperty =
|
||||
WPFHelper.create_dp<IEnumerable, ActionMenu>(
|
||||
nameof(ItemsSource), Enumerable.Empty<IMenuItemVM>());
|
||||
|
||||
public IEnumerable ItemsSource {
|
||||
get => GetValue(ItemsSourceProperty) as IEnumerable;
|
||||
set => SetValue(ItemsSourceProperty, value);
|
||||
}
|
||||
protected override DependencyObject GetContainerForItemOverride() =>
|
||||
new MenuItem();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace mbts.ui.controls.adorners {
|
||||
class AdornerHost : Adorner {
|
||||
UIElement _child;
|
||||
|
||||
public AdornerHost(UIElement adorned_element)
|
||||
: base(adorned_element) { }
|
||||
|
||||
public static readonly DependencyProperty ChildProperty =
|
||||
WPFHelper.create_dp<UIElement, AdornerHost>(
|
||||
nameof(Child), (o, v) => {
|
||||
if (o._child != null)
|
||||
o.RemoveVisualChild(o._child);
|
||||
o._child = v;
|
||||
if (v != null)
|
||||
o.AddVisualChild(v);
|
||||
});
|
||||
|
||||
public UIElement Child {
|
||||
get => _child;
|
||||
set => SetValue(ChildProperty, value);
|
||||
}
|
||||
|
||||
protected override int VisualChildrenCount => _child != null ? 1 : 0;
|
||||
|
||||
protected override Visual GetVisualChild(int index) =>
|
||||
index == 0 && _child != null ? _child : throw new ArgumentOutOfRangeException();
|
||||
|
||||
protected override Size ArrangeOverride(Size finalSize) {
|
||||
if (_child == null) {
|
||||
return base.ArrangeOverride(finalSize);
|
||||
}
|
||||
_child.Arrange(new Rect(finalSize));
|
||||
return _child.DesiredSize;
|
||||
}
|
||||
|
||||
protected override Size MeasureOverride(Size constraint) {
|
||||
if (_child == null) {
|
||||
return base.MeasureOverride(constraint);
|
||||
}
|
||||
_child.Measure(constraint);
|
||||
return _child.DesiredSize;
|
||||
}
|
||||
}
|
||||
|
||||
class AdornerHost<TChild> : AdornerHost where TChild : UIElement {
|
||||
public AdornerHost(UIElement adorned_element)
|
||||
: base(adorned_element) { }
|
||||
|
||||
public new TChild Child {
|
||||
get => base.Child as TChild;
|
||||
set => base.Child = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
using mbts.ui.dpi;
|
||||
using mbts.ui.extensions;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace mbts.ui.controls.adorners {
|
||||
class DIYGridCellHoverAdorner : Adorner<ListBox>, IDisposable {
|
||||
Brush _row_fill, _cell_fill;
|
||||
Pen _row_stroke, _cell_stroke;
|
||||
|
||||
DIYGridRowsPresenter _rows_presenter;
|
||||
ScrollViewer _scroll_viewer;
|
||||
ScrollContentPresenter _scroll_content_backing;
|
||||
DIYGrid _grid;
|
||||
IDisposable _dispose;
|
||||
DIYGrid.CellGeometry? _hover_cell;
|
||||
AdornerHost _menu_host = null;
|
||||
ActionMenu _menu => _menu_host.Child as ActionMenu;
|
||||
|
||||
#region RowFill
|
||||
public static readonly DependencyProperty RowFillProperty =
|
||||
WPFHelper.create_dp<Brush, DIYGridCellHoverAdorner>(
|
||||
nameof(RowFill),
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(o, v) => o._row_fill = v,
|
||||
FrozenBrushCache.get_brush(DIYGrid.DefaultSelectionHighlight));
|
||||
|
||||
public SolidColorBrush RowFill {
|
||||
get => GetValue(RowFillProperty) as SolidColorBrush;
|
||||
set => SetValue(RowFillProperty, value);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region RowStroke
|
||||
public static readonly DependencyProperty RowStrokeProperty =
|
||||
WPFHelper.create_dp<Pen, DIYGridCellHoverAdorner>(
|
||||
nameof(RowStroke),
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(o, v) => o._row_stroke = v);
|
||||
|
||||
public Pen RowStroke {
|
||||
get => GetValue(RowStrokeProperty) as Pen;
|
||||
set => SetValue(RowStrokeProperty, value);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region CellFill
|
||||
public static readonly DependencyProperty CellFillProperty =
|
||||
WPFHelper.create_dp<Brush, DIYGridCellHoverAdorner>(
|
||||
nameof(CellFill),
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(o, v) => o._cell_fill = v);
|
||||
|
||||
public SolidColorBrush CellFill {
|
||||
get => GetValue(CellFillProperty) as SolidColorBrush;
|
||||
set => SetValue(CellFillProperty, value);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region CellStroke
|
||||
public static readonly DependencyProperty CellStrokeProperty =
|
||||
WPFHelper.create_dp<Pen, DIYGridCellHoverAdorner>(
|
||||
nameof(CellStroke),
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(o, v) => o._cell_stroke = v);
|
||||
|
||||
public Pen CellStroke {
|
||||
get => GetValue(CellStrokeProperty) as Pen;
|
||||
set => SetValue(CellStrokeProperty, value);
|
||||
}
|
||||
#endregion
|
||||
|
||||
public DIYGridCellHoverAdorner(DIYGridRowsPresenter adorned_element, ScrollViewer scroll_viewer)
|
||||
: base(adorned_element) {
|
||||
_row_fill = RowFill;
|
||||
_row_stroke = RowStroke;
|
||||
_cell_fill = CellFill;
|
||||
_cell_stroke = CellStroke;
|
||||
_scroll_viewer = scroll_viewer;
|
||||
IsHitTestVisible = false;
|
||||
_rows_presenter = adorned_element;
|
||||
|
||||
_grid = _rows_presenter.find_ancestor_of_type<DIYGrid>();
|
||||
_dispose = Disposable.Create(
|
||||
_grid.SubscribeMouseHoverCell().Subscribe(cell => {
|
||||
_hover_cell = cell;
|
||||
if (_hover_cell.HasValue) {
|
||||
var (row, col) = _hover_cell.Value;
|
||||
var row_tl = row.TranslatePoint(default, this);
|
||||
}
|
||||
InvalidateVisual();
|
||||
}));
|
||||
}
|
||||
|
||||
ScrollContentPresenter _scroll_content =>
|
||||
_scroll_content_backing ?? (_scroll_content_backing = _scroll_viewer.descendants_of_type<ScrollContentPresenter>().FirstOrDefault());
|
||||
|
||||
protected override void OnRender(DrawingContext drawingContext) {
|
||||
base.OnRender(drawingContext);
|
||||
if (_hover_cell.HasValue) {
|
||||
(var row, var cell) = _hover_cell.Value;
|
||||
if (row != null) {
|
||||
var row_tl = new Lazy<Point>(() => row.TranslatePoint(default, this));
|
||||
if (_row_fill != null || _row_stroke != null) {
|
||||
var row_rect = new Rect(row_tl.Value, row.RenderSize);
|
||||
if (_scroll_content != null) {
|
||||
var content_bounds = this.GetChildBounds(_scroll_content);
|
||||
row_rect.Intersect(content_bounds);
|
||||
}
|
||||
drawingContext.DrawRectangle(_row_fill, _row_stroke, row_rect, true);
|
||||
}
|
||||
if (_cell_fill != null || _cell_stroke != null) {
|
||||
var cell_rect = cell.to_visible_rect(row.ActualHeight, row_tl.Value.Y);
|
||||
drawingContext.DrawRectangle(_cell_fill, _cell_stroke, cell_rect, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose() =>
|
||||
Disposable.dispose(ref _dispose);
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@ namespace mbts.ui.controls.adorners {
|
||||
nameof(Fill),
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(o, v) => o._fill = v,
|
||||
DIYGrid.DefaultSelectionHighlight);
|
||||
FrozenBrushCache.get_brush(DIYGrid.DefaultSelectionHighlight));
|
||||
|
||||
public SolidColorBrush Fill {
|
||||
get => GetValue(FillProperty) as SolidColorBrush;
|
||||
@@ -38,7 +38,7 @@ namespace mbts.ui.controls.adorners {
|
||||
nameof(Stroke),
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(o, v) => o._stroke = v,
|
||||
DIYGrid.DefaultSelectionStroke);
|
||||
FrozenPenCache.get_pen(DIYGrid.DefaultSelectionHighlight));
|
||||
|
||||
public Pen Stroke {
|
||||
get => GetValue(StrokeProperty) as Pen;
|
||||
|
||||
@@ -8,8 +8,8 @@ using System.Windows.Media;
|
||||
|
||||
namespace mbts.ui.controls.adorners {
|
||||
public class DragRectAdorner : Adorner, IDisposable {
|
||||
Brush _fill = DIYGrid.DefaultSelectionHighlight;
|
||||
Pen _stroke = DIYGrid.DefaultSelectionStroke;
|
||||
Brush _fill;
|
||||
Pen _stroke;
|
||||
readonly ScrollViewerTuple _scroll_viewer_tup;
|
||||
readonly double _drift_multiplier;
|
||||
Point? _drag_start;
|
||||
@@ -20,7 +20,7 @@ namespace mbts.ui.controls.adorners {
|
||||
nameof(Fill),
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(o, v) => o._fill = v,
|
||||
DIYGrid.DefaultSelectionHighlight);
|
||||
FrozenBrushCache.get_brush(DIYGrid.DefaultSelectionHighlight));
|
||||
|
||||
public SolidColorBrush Fill {
|
||||
get => GetValue(FillProperty) as SolidColorBrush;
|
||||
@@ -32,7 +32,7 @@ namespace mbts.ui.controls.adorners {
|
||||
nameof(Stroke),
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(o, v) => o._stroke = v,
|
||||
DIYGrid.DefaultSelectionStroke);
|
||||
FrozenPenCache.get_pen(DIYGrid.DefaultSelectionHighlight));
|
||||
|
||||
public Pen Stroke {
|
||||
get => GetValue(StrokeProperty) as Pen;
|
||||
@@ -48,6 +48,8 @@ namespace mbts.ui.controls.adorners {
|
||||
/// <param name="scroll_viewer">The element's ScrollViewer.</param>
|
||||
/// <param name="drift_multiplier">The number of pixels per scroll unit.</param>
|
||||
public DragRectAdorner(FrameworkElement adorned_element, ScrollViewer scroll_viewer = null, double drift_multiplier = 1) : base(adorned_element) {
|
||||
_fill = Fill;
|
||||
_stroke = Stroke;
|
||||
_drift_multiplier = drift_multiplier;
|
||||
AdornedElement.PreviewMouseDown += _on_preview_mouse_down;
|
||||
AdornedElement.PreviewMouseMove += _on_preview_mouse_move;
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
using System;
|
||||
using mbts.ui.extensions;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using bts.utils;
|
||||
using mbts.ui.extensions;
|
||||
|
||||
namespace mbts.ui.controls.adorners {
|
||||
public class RowSelectionAdorner : Adorner, DIYGrid.ISelectionHighlightListener, IDisposable {
|
||||
Brush _fill = DIYGrid.DefaultSelectionHighlight;
|
||||
Pen _stroke = DIYGrid.DefaultSelectionStroke;
|
||||
class RowSelectionAdorner : Adorner<ListBox>, IDisposable {
|
||||
Brush _fill;
|
||||
Pen _stroke;
|
||||
|
||||
DIYGridRowsPresenter _diy_rp;
|
||||
ScrollViewer _diy_scroll;
|
||||
@@ -25,20 +23,21 @@ namespace mbts.ui.controls.adorners {
|
||||
nameof(Fill),
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(o, v) => o._fill = v,
|
||||
DIYGrid.DefaultSelectionHighlight);
|
||||
FrozenBrushCache.get_brush(DIYGrid.DefaultSelectionHighlight));
|
||||
|
||||
public SolidColorBrush Fill {
|
||||
get => GetValue(FillProperty) as SolidColorBrush;
|
||||
set => SetValue(FillProperty, value);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Stroke
|
||||
public static readonly DependencyProperty StrokeProperty =
|
||||
WPFHelper.create_dp<Pen, RowSelectionAdorner>(
|
||||
nameof(Stroke),
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(o, v) => o._stroke = v,
|
||||
DIYGrid.DefaultSelectionStroke);
|
||||
FrozenPenCache.get_pen(DIYGrid.DefaultSelectionHighlight.with(255)));
|
||||
|
||||
public Pen Stroke {
|
||||
get => GetValue(StrokeProperty) as Pen;
|
||||
@@ -46,7 +45,9 @@ namespace mbts.ui.controls.adorners {
|
||||
}
|
||||
#endregion
|
||||
|
||||
internal RowSelectionAdorner(DIYGridRowsPresenter adorned_element, ScrollViewer scroll_viewer) : base(adorned_element) {
|
||||
public RowSelectionAdorner(DIYGridRowsPresenter adorned_element, ScrollViewer scroll_viewer) : base(adorned_element) {
|
||||
_fill = Fill;
|
||||
_stroke = Stroke;
|
||||
// we want our clicks to go through to the underlying grid
|
||||
IsHitTestVisible = false;
|
||||
|
||||
@@ -60,7 +61,6 @@ namespace mbts.ui.controls.adorners {
|
||||
_disposables.Add(_diy_scroll.SubscribeScrollChanged((s, e) => InvalidateVisual()));
|
||||
}
|
||||
|
||||
|
||||
protected override void OnRender(DrawingContext drawingContext) {
|
||||
base.OnRender(drawingContext);
|
||||
if (AdornedElement is ListBox lb && lb.SelectedItems != null && lb.SelectedItems.Count > 0) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using mbts.ui.converters;
|
||||
using mbts.ui.dpi;
|
||||
using mbts.ui.converters;
|
||||
using mbts.ui.shell;
|
||||
using mbts.ui.utility.text;
|
||||
using System;
|
||||
@@ -10,6 +11,7 @@ using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Media;
|
||||
using bts;
|
||||
|
||||
namespace mbts.ui.controls {
|
||||
partial class DIYGrid {
|
||||
@@ -83,79 +85,39 @@ namespace mbts.ui.controls {
|
||||
}
|
||||
}
|
||||
|
||||
public static Rect? get_clip(Size content_size, double row_height, double left, double unclipped_left, double width) =>
|
||||
content_size == null ? get_clip(0, 0, row_height, left, unclipped_left, width) :
|
||||
get_clip(content_size.Width, content_size.Height, row_height, left, unclipped_left, width);
|
||||
|
||||
public static Rect? get_clip(double content_width, double content_height, double row_height, double left, double unclipped_left, double width) {
|
||||
if (content_width > width || unclipped_left < left)
|
||||
return new Rect(left, 0, width - left + unclipped_left, row_height);
|
||||
return null;
|
||||
}
|
||||
|
||||
public static IDisposable draw_with_clip(DrawingContext dc, double raw_content_width, double raw_content_height, Thickness padding, double row_height, ColumnGeometry position) =>
|
||||
draw_with_clip(dc,
|
||||
raw_content_width + padding.Left + padding.Right,
|
||||
raw_content_height + padding.Top + padding.Bottom,
|
||||
row_height, position.left, position.unclipped_left, position.width);
|
||||
|
||||
public static IDisposable draw_with_clip(DrawingContext dc, double content_width, double content_height, double row_height, double left, double unclipped_left, double width) {
|
||||
var clip = get_clip(content_width, content_height, row_height, left, unclipped_left, width);
|
||||
if (clip.HasValue) {
|
||||
dc.PushClip(new RectangleGeometry(clip.Value));
|
||||
return Disposable.Create(dc.Pop);
|
||||
} else return Disposable.Create(() => { });
|
||||
}
|
||||
|
||||
public static void draw_text(
|
||||
DrawingContext dc, GlyphContext glyph_context, double font_size, string text, ColumnGeometry position, double row_height,
|
||||
Brush foreground, Brush background, HorizontalAlignment x_align, VerticalAlignment y_align, Thickness padding) {
|
||||
DrawingContext dc,
|
||||
GlyphContext glyph_context,
|
||||
double font_size,
|
||||
string text,
|
||||
ColumnGeometry position,
|
||||
double row_height,
|
||||
Brush foreground,
|
||||
Brush background,
|
||||
HorizontalAlignment x_align,
|
||||
VerticalAlignment y_align,
|
||||
Thickness padding,
|
||||
bool round_to_layout = true) {
|
||||
//
|
||||
if (!string.IsNullOrEmpty(text)) {
|
||||
var gr = glyph_context.get_glyph_run(text, font_size, position.unclipped_left, 0, position.right, row_height, x_align, y_align, padding);
|
||||
draw_text(dc, gr, gr.AdvanceWidths.Sum(), font_size, position, padding, row_height, foreground, background);
|
||||
}
|
||||
var gr = glyph_context.get_glyph_run(text, font_size, position.unclipped_left, 0, position.right, row_height, x_align, y_align, padding, round_to_layout);
|
||||
var gwidth = gr == null ? 0 : gr.ComputeAlignmentBox().Width;
|
||||
draw_text(dc, gr, gwidth, font_size, position, padding, row_height, foreground, background);
|
||||
}
|
||||
|
||||
public static void draw_text(DrawingContext dc, GlyphRun glyph_run, double glyph_width, double font_size, ColumnGeometry position, Thickness padding, double row_height, Brush foreground, Brush background) {
|
||||
using (draw_with_clip(dc, glyph_width, font_size, padding, row_height, position)) {
|
||||
if (background != null)
|
||||
dc.DrawRectangle(background, null, new Rect(position.left, 0, position.width, row_height));
|
||||
dc.DrawGlyphRun(foreground, glyph_run);
|
||||
if (glyph_run == null && background == null)
|
||||
return;
|
||||
var rect = position.to_rect(row_height);
|
||||
if (background != null) {
|
||||
var guidelines = rect.AntiAliasGuidelines(0);
|
||||
dc.PushGuidelineSet(guidelines);
|
||||
dc.DrawRectangle(background, null, rect);
|
||||
dc.Pop();
|
||||
}
|
||||
}
|
||||
|
||||
public static void draw_text(
|
||||
DrawingContext dc, GlyphContext glyph_context, double font_size, string text, ColumnGeometry position, double row_height,
|
||||
Brush foreground, Brush background, HorizontalAlignment x_align, VerticalAlignment y_align, Thickness padding, IEnumerable<CellAnimation> trigger_actions) {
|
||||
//
|
||||
using (position.clip_draw(dc, row_height)) {
|
||||
double adjusted_left = position.unclipped_left;
|
||||
double available_width = position.width;
|
||||
if (background != null)
|
||||
dc.DrawRectangle(background, null, new Rect(position.left, 0, position.width, row_height));
|
||||
// store any trigger-glyphs in a list, to be drawn after iterating through all triggers (must draw background first, so glyphs will not be covered up).
|
||||
List<(Brush brush, GlyphRun glyph)> glyphs = new List<(Brush brush, GlyphRun glyph)>(1);
|
||||
if (trigger_actions != null) {
|
||||
foreach (CellAnimation ta in trigger_actions) {
|
||||
switch (ta.animation_type_case) {
|
||||
case CellAnimation.AnimationTypeOneofCase.BackgroundAnimation:
|
||||
dc.DrawRectangle(ta.background_animation.brush, ta.background_animation.pen, new Rect(position.left, 0, position.width, row_height));
|
||||
break;
|
||||
case CellAnimation.AnimationTypeOneofCase.SymbolAnimation:
|
||||
var (symbol_text, is_sideways) = _get_symbol_animation_args(ta.symbol_animation);
|
||||
GlyphRun symbol_glyph = glyph_context.get_glyph_run(symbol_text, font_size, position.width, row_height, HorizontalAlignment.Left, adjusted_left, y_align, 0, is_sideways: is_sideways);
|
||||
glyphs.Add((ta.symbol_animation.brush, symbol_glyph));
|
||||
adjusted_left += symbol_glyph.AdvanceWidths.Sum();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach (var (brush, glyph) in glyphs) {
|
||||
dc.DrawGlyphRun(brush, glyph);
|
||||
}
|
||||
GlyphRun main_content = glyph_context.get_glyph_run(text, font_size, adjusted_left, 0, position.right, row_height, x_align, y_align, padding);
|
||||
draw_text(dc, main_content, main_content.AdvanceWidths.Sum(), font_size, position, padding, row_height, foreground, background);
|
||||
if (glyph_run != null) {
|
||||
dc.PushClip(new RectangleGeometry(rect));
|
||||
dc.DrawGlyphRun(foreground, glyph_run);
|
||||
dc.Pop();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,14 +127,6 @@ namespace mbts.ui.controls {
|
||||
public static void change_position(FrameworkElement cell_view, double row_height, ColumnGeometry? position) {
|
||||
if (position.HasValue) {
|
||||
WPFHelper.set_if_changed(cell_view, VisibilityProperty, Visibility.Visible);
|
||||
var clip = get_clip(cell_view.DesiredSize, row_height, 0, position.Value.unclipped_left - position.Value.left, position.Value.width);
|
||||
var el_clip = cell_view.Clip as RectangleGeometry;
|
||||
if (clip.HasValue) {
|
||||
if (el_clip == null || !el_clip.Rect.Equals(clip.Value))
|
||||
cell_view.Clip = new RectangleGeometry(clip.Value);
|
||||
} else if (cell_view.Clip != null) {
|
||||
cell_view.Clip = null;
|
||||
}
|
||||
} else {
|
||||
WPFHelper.set_if_changed(cell_view, VisibilityProperty, Visibility.Collapsed);
|
||||
}
|
||||
|
||||
@@ -1,16 +1,28 @@
|
||||
using mbts.ui.utilities;
|
||||
using mbts.spark;
|
||||
using mbts.ui.dpi;
|
||||
using mbts.ui.utilities;
|
||||
using mbts.ui.utility.text;
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Reactive.Disposables;
|
||||
using System.Reactive.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Threading;
|
||||
using FontFamily = System.Windows.Media.FontFamily;
|
||||
|
||||
namespace mbts.ui.controls {
|
||||
enum HyperlinkCellClickMode {
|
||||
Text,
|
||||
Anywhere
|
||||
}
|
||||
|
||||
class HyperlinkCell : FrameworkElement, ICommandSource {
|
||||
// these fields each represent properties that are accessed frequently,
|
||||
// they are stored locally to reduce repetative DependencyProperty lookups
|
||||
@@ -18,10 +30,35 @@ namespace mbts.ui.controls {
|
||||
Brush _background, _foreground_backing, _border_brush_backing;
|
||||
double _glyph_width => _gcr.glyph_run.AdvanceWidths.Sum();
|
||||
GlyphRun _glyph_run => _gcr.glyph_run;
|
||||
bool _is_mouse_over_text, _can_execute;
|
||||
bool
|
||||
_is_mouse_over_text,
|
||||
_is_mouse_over_click_area_backing,
|
||||
_can_execute,
|
||||
_snaps_to_device_pixels;
|
||||
Pen _underline_pen_backing, _border_stroke_pen_backing;
|
||||
GlyphContentRect _gcr = new GlyphContentRect();
|
||||
GlyphContentRect _gcr = new GlyphContentRect {
|
||||
// add 2 pixels to account for underline (1 for space, 1 for the line itself)
|
||||
content_height_offset = HighDpiExtensions.pixel_unit * 2
|
||||
};
|
||||
IDisposable _mouse_over_click_area_op;
|
||||
|
||||
bool _is_mouse_over_click_area {
|
||||
get => _is_mouse_over_click_area_backing;
|
||||
set {
|
||||
if (_is_mouse_over_click_area_backing != value) {
|
||||
Disposable.dispose(ref _mouse_over_click_area_op);
|
||||
_is_mouse_over_click_area_backing = value;
|
||||
if (value) {
|
||||
_mouse_over_click_area_op = WorkLoop.Start(
|
||||
$"Evaluate {nameof(HyperlinkCell)} {nameof(IsMouseOverClickArea)}",
|
||||
TimeSpan.FromMilliseconds(10),
|
||||
DispatcherPriority.Input,
|
||||
_on_mouse_changed,
|
||||
WorkLoop.default_excessive_cycle_ratio);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Brush _foreground {
|
||||
get => _foreground_backing;
|
||||
set {
|
||||
@@ -37,13 +74,23 @@ namespace mbts.ui.controls {
|
||||
}
|
||||
}
|
||||
Thickness _border_thickness {
|
||||
get => _gcr.border_thickness;
|
||||
get => _snaps_to_device_pixels ? _gcr.border_thickness.RoundToLayout() : _gcr.border_thickness;
|
||||
set {
|
||||
_gcr.border_thickness = new Thickness(value.Left);
|
||||
_border_stroke_pen_backing = null;
|
||||
}
|
||||
}
|
||||
Pen _underline_pen => _underline_pen_backing ?? (_foreground == null ? null : (_underline_pen_backing = new Pen(_foreground, 1)));
|
||||
Pen _underline_pen {
|
||||
get {
|
||||
if (_underline_pen_backing == null) {
|
||||
var fg = _foreground;
|
||||
if (fg != null) {
|
||||
_underline_pen_backing = new Pen(fg, _snaps_to_device_pixels ? HighDpiExtensions.pixel_unit : 1);
|
||||
}
|
||||
}
|
||||
return _underline_pen_backing;
|
||||
}
|
||||
}
|
||||
Pen _border_stroke_pen =>
|
||||
// border_thickness.Left is used as the uniform border thickness. This can be changed if need be.
|
||||
_border_stroke_pen_backing == null && _border_brush != null && _border_thickness.Left > 0 ?
|
||||
@@ -51,6 +98,8 @@ namespace mbts.ui.controls {
|
||||
_border_stroke_pen_backing;
|
||||
|
||||
static HyperlinkCell() {
|
||||
IsMouseOverClickAreaProperty = IsMouseOverClickAreaPropertyKey.DependencyProperty;
|
||||
DefaultStyleKeyProperty.OverrideMetadata(typeof(HyperlinkCell), new FrameworkPropertyMetadata(typeof(HyperlinkCell)));
|
||||
WPFHelper.override_metadata<HorizontalAlignment, HyperlinkCell>(
|
||||
HorizontalAlignmentProperty,
|
||||
FrameworkPropertyMetadataOptions.AffectsArrange |
|
||||
@@ -64,6 +113,11 @@ namespace mbts.ui.controls {
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(o, v) => o._gcr.width = v,
|
||||
double.NaN);
|
||||
WPFHelper.override_metadata<bool, HyperlinkCell>(
|
||||
SnapsToDevicePixelsProperty,
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(o, v) => o._snaps_to_device_pixels = v,
|
||||
true);
|
||||
}
|
||||
|
||||
public HyperlinkCell() {
|
||||
@@ -73,7 +127,7 @@ namespace mbts.ui.controls {
|
||||
_gcr.min_height = MinHeight;
|
||||
_gcr.height = Height;
|
||||
_gcr.max_height = Height;
|
||||
_gcr.padding = _underline_padding(Padding);
|
||||
_gcr.padding = Padding;
|
||||
_gcr.border_thickness = BorderThickness;
|
||||
_gcr.horizontal_alignment = HorizontalAlignment;
|
||||
_gcr.horizontal_content_alignment = HorizontalContentAlignment;
|
||||
@@ -86,8 +140,39 @@ namespace mbts.ui.controls {
|
||||
if (Text is string str && !string.IsNullOrEmpty(str))
|
||||
_gcr.text_lines = new[] { str };
|
||||
_gcr.glyph_context = GlyphContext.create(FontFamily, FontWeight);
|
||||
_snaps_to_device_pixels = SnapsToDevicePixels;
|
||||
}
|
||||
|
||||
#region IsMouseOverClickArea
|
||||
static readonly DependencyPropertyKey IsMouseOverClickAreaPropertyKey =
|
||||
DependencyProperty.RegisterReadOnly(
|
||||
"IsMouseOverClickArea",
|
||||
typeof(bool),
|
||||
typeof(HyperlinkCell),
|
||||
new FrameworkPropertyMetadata(
|
||||
false,
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
OnIsMouseOverClickAreaChanged,
|
||||
CoerceIsMouseOverClickArea));
|
||||
|
||||
public static readonly DependencyProperty IsMouseOverClickAreaProperty;
|
||||
|
||||
static void OnIsMouseOverClickAreaChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
|
||||
if (d is HyperlinkCell hlc)
|
||||
hlc._is_mouse_over_click_area = e.NewValue is bool b && b;
|
||||
}
|
||||
|
||||
static object CoerceIsMouseOverClickArea(DependencyObject d, object baseValue) =>
|
||||
d is HyperlinkCell hlc ?
|
||||
hlc._is_mouse_over_text || (hlc.IsMouseOver && hlc.HyperlinkClickMode == HyperlinkCellClickMode.Anywhere) :
|
||||
baseValue;
|
||||
|
||||
public bool IsMouseOverClickArea {
|
||||
get => (bool)GetValue(IsMouseOverClickAreaProperty);
|
||||
private set => SetValue(IsMouseOverClickAreaPropertyKey, value);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region BorderThickness
|
||||
public static readonly DependencyProperty BorderThicknessProperty =
|
||||
Control.BorderThicknessProperty.AddOwner(typeof(HyperlinkCell),
|
||||
@@ -135,7 +220,8 @@ namespace mbts.ui.controls {
|
||||
public static readonly DependencyProperty VerticalContentAlignmentProperty = Control.VerticalContentAlignmentProperty.AddOwner(typeof(HyperlinkCell),
|
||||
new FrameworkPropertyMetadata(
|
||||
VerticalAlignment.Center,
|
||||
FrameworkPropertyMetadataOptions.AffectsArrange,
|
||||
FrameworkPropertyMetadataOptions.AffectsArrange |
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(d, e) => ((HyperlinkCell)d)._gcr.vertical_content_alignment = (VerticalAlignment)e.NewValue));
|
||||
|
||||
public VerticalAlignment VerticalContentAlignment {
|
||||
@@ -215,7 +301,7 @@ namespace mbts.ui.controls {
|
||||
nameof(Padding),
|
||||
FrameworkPropertyMetadataOptions.AffectsMeasure |
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(p, v) => p._gcr.padding = _underline_padding(v), DIYGrid.DefaultCellPadding);
|
||||
(p, v) => p._gcr.padding = default, default);
|
||||
|
||||
public Thickness Padding { get => (Thickness)GetValue(PaddingProperty); set => SetValue(PaddingProperty, value); }
|
||||
#endregion
|
||||
@@ -231,6 +317,18 @@ namespace mbts.ui.controls {
|
||||
public string Text { get => GetValue(TextProperty) as string; set => SetValue(TextProperty, value); }
|
||||
#endregion
|
||||
|
||||
#region HyperlinkClickMode
|
||||
public static readonly DependencyProperty HyperlinkClickModeProperty =
|
||||
WPFHelper.create_dp<HyperlinkCellClickMode, HyperlinkCell>(
|
||||
nameof(HyperlinkClickMode),
|
||||
(o, v) => o.CoerceValue(IsMouseOverClickAreaProperty));
|
||||
|
||||
public HyperlinkCellClickMode HyperlinkClickMode {
|
||||
get => (HyperlinkCellClickMode)GetValue(HyperlinkClickModeProperty);
|
||||
set => SetValue(HyperlinkClickModeProperty, value);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region ICommandSource
|
||||
|
||||
#region Command
|
||||
@@ -297,8 +395,8 @@ namespace mbts.ui.controls {
|
||||
|
||||
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e) {
|
||||
base.OnMouseLeftButtonDown(e);
|
||||
_check_mouse_over_text(e);
|
||||
if (_is_mouse_over_text) {
|
||||
_on_mouse_changed(e);
|
||||
if (_is_mouse_over_click_area) {
|
||||
// Hyperlink should take focus when left mouse button is clicked on it
|
||||
// This is consistent with all ButtonBase controls and current Win32 behavior
|
||||
Focus();
|
||||
@@ -308,6 +406,8 @@ namespace mbts.ui.controls {
|
||||
if (e.ButtonState == MouseButtonState.Pressed) {
|
||||
// Capture the mouse, and make sure we got it.
|
||||
CaptureMouse();
|
||||
} else {
|
||||
ReleaseMouseCapture();
|
||||
}
|
||||
e.Handled = true;
|
||||
}
|
||||
@@ -315,23 +415,25 @@ namespace mbts.ui.controls {
|
||||
|
||||
protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e) {
|
||||
base.OnMouseLeftButtonUp(e);
|
||||
_check_mouse_over_text(e);
|
||||
if (IsMouseCaptured)
|
||||
_on_mouse_changed(e);
|
||||
if (IsMouseCaptured) {
|
||||
ReleaseMouseCapture();
|
||||
if (_can_execute && _is_mouse_over_text)
|
||||
CommandHelper.ExecuteCommandSource(this);
|
||||
e.Handled = true;
|
||||
if (_can_execute)
|
||||
CommandHelper.ExecuteCommandSource(this);
|
||||
_on_mouse_changed(e);
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
protected override void OnIsMouseDirectlyOverChanged(DependencyPropertyChangedEventArgs e) {
|
||||
if (_is_mouse_over_text != IsMouseDirectlyOver)
|
||||
_toggle_mouse_over_text();
|
||||
protected override void OnIsMouseDirectlyOverChanged(DependencyPropertyChangedEventArgs e) => _on_mouse_changed();
|
||||
protected override void OnPreviewMouseMove(MouseEventArgs e) {
|
||||
_on_mouse_changed(e);
|
||||
e.Handled = IsMouseCaptured;
|
||||
}
|
||||
protected override void OnPreviewMouseMove(MouseEventArgs e) => _check_mouse_over_text(e);
|
||||
protected override void OnMouseEnter(MouseEventArgs e) => _check_mouse_over_text(e);
|
||||
protected override void OnMouseLeave(MouseEventArgs e) => _check_mouse_over_text(e);
|
||||
protected override void OnMouseEnter(MouseEventArgs e) => _on_mouse_changed(e);
|
||||
protected override void OnMouseLeave(MouseEventArgs e) => _on_mouse_changed(e);
|
||||
|
||||
protected override Size ArrangeOverride(Size finalSize) => finalSize;
|
||||
|
||||
@@ -347,45 +449,32 @@ namespace mbts.ui.controls {
|
||||
var rs = RenderSize;
|
||||
var rect = new Rect(0, 0, rs.Width, rs.Height);
|
||||
var bg = Background;
|
||||
if (bg != null) {
|
||||
drawingContext.DrawRectangle(bg, _border_stroke_pen, rect);
|
||||
}
|
||||
if (_glyph_run != null) {
|
||||
drawingContext.DrawRectangle(bg, _border_stroke_pen, rect, _snaps_to_device_pixels);
|
||||
var gr = _glyph_run;
|
||||
if (gr != null) {
|
||||
drawingContext.PushClip(new RectangleGeometry(rect));
|
||||
drawingContext.DrawGlyphRun(_foreground, _glyph_run);
|
||||
if (_is_mouse_over_text) {
|
||||
drawingContext.DrawGlyphRun(_foreground, gr);
|
||||
if (IsMouseOverClickArea) {
|
||||
// draw underline
|
||||
double x = _gcr.glyph_run.BaselineOrigin.X;
|
||||
double y = _gcr.glyph_run.BaselineOrigin.Y + 2;
|
||||
drawingContext.DrawLine(_underline_pen, new Point(x, y), new Point(x + _gcr.glyph_run.AdvanceWidths.Sum(), y));
|
||||
var ul_pen = _underline_pen;
|
||||
if (ul_pen != null) {
|
||||
var line_start = new Point(gr.BaselineOrigin.X, gr.BaselineOrigin.Y + 2 * ul_pen.Thickness);
|
||||
drawingContext.DrawHorizontalLine(ul_pen, line_start, gr.AdvanceWidths.Sum(), _snaps_to_device_pixels);
|
||||
}
|
||||
}
|
||||
drawingContext.Pop();
|
||||
}
|
||||
}
|
||||
|
||||
static Thickness _underline_padding(Thickness padding) =>
|
||||
new Thickness(padding.Left, padding.Top, padding.Right, padding.Bottom + 2);
|
||||
void _on_mouse_changed() =>
|
||||
_on_mouse_changed(Mouse.GetPosition(this));
|
||||
|
||||
void _check_mouse_over_text(MouseEventArgs e) {
|
||||
var pt = e.GetPosition(this);
|
||||
var orig = _gcr.content_area;
|
||||
var exp = orig.Expanded(1);
|
||||
if (_is_mouse_over_text != exp.Contains(pt)) {
|
||||
bts.Log.assert(_is_mouse_over_text != _gcr.content_area.Expanded(1).Contains(e.GetPosition(this)));
|
||||
_toggle_mouse_over_text();
|
||||
return;
|
||||
}
|
||||
if (_is_mouse_over_text != _gcr.content_area.Expanded(1).Contains(e.GetPosition(this)))
|
||||
_toggle_mouse_over_text();
|
||||
}
|
||||
void _on_mouse_changed(MouseEventArgs e) =>
|
||||
_on_mouse_changed(e.GetPosition(this));
|
||||
|
||||
void _toggle_mouse_over_text() {
|
||||
_is_mouse_over_text = !_is_mouse_over_text;
|
||||
if (_is_mouse_over_text)
|
||||
SetCurrentValue(CursorProperty, Cursors.Hand);
|
||||
else
|
||||
ClearValue(CursorProperty);
|
||||
InvalidateVisual();
|
||||
void _on_mouse_changed(Point pt) {
|
||||
_is_mouse_over_text = _gcr.content_area.Expanded(1).Contains(pt);
|
||||
CoerceValue(IsMouseOverClickAreaProperty);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
using mbts.ui.utility.text;
|
||||
using System;
|
||||
using mbts.ui.dpi;
|
||||
using mbts.ui.utility.text;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
@@ -15,6 +15,7 @@ namespace mbts.ui.controls {
|
||||
Brush _background, _foreground, _border_brush_backing;
|
||||
double _glyph_width => _gcr.glyph_run.AdvanceWidths.Sum();
|
||||
GlyphRun _glyph_run => _gcr.glyph_run;
|
||||
bool _snaps_to_device_pixels;
|
||||
Pen _border_stroke_pen_backing;
|
||||
GlyphContentRect _gcr = new GlyphContentRect();
|
||||
|
||||
@@ -26,7 +27,7 @@ namespace mbts.ui.controls {
|
||||
}
|
||||
}
|
||||
Thickness _border_thickness {
|
||||
get => _gcr.border_thickness;
|
||||
get => _snaps_to_device_pixels ? _gcr.border_thickness.RoundToLayout() : _gcr.border_thickness;
|
||||
set {
|
||||
_gcr.border_thickness = new Thickness(value.Left);
|
||||
_border_stroke_pen_backing = null;
|
||||
@@ -52,6 +53,11 @@ namespace mbts.ui.controls {
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(o, v) => o._gcr.width = v,
|
||||
double.NaN);
|
||||
WPFHelper.override_metadata<bool, TextCell>(
|
||||
SnapsToDevicePixelsProperty,
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(o, v) => o._snaps_to_device_pixels = v,
|
||||
true);
|
||||
}
|
||||
|
||||
public TextCell() {
|
||||
@@ -74,6 +80,7 @@ namespace mbts.ui.controls {
|
||||
if (Text is string str && !string.IsNullOrEmpty(str))
|
||||
_gcr.text_lines = new[] { str };
|
||||
_gcr.glyph_context = GlyphContext.create(FontFamily, FontWeight);
|
||||
_snaps_to_device_pixels = SnapsToDevicePixels;
|
||||
}
|
||||
|
||||
#region BorderThickness
|
||||
@@ -111,14 +118,7 @@ namespace mbts.ui.controls {
|
||||
new FrameworkPropertyMetadata(
|
||||
HorizontalAlignment.Center,
|
||||
FrameworkPropertyMetadataOptions.AffectsArrange,
|
||||
(d, e) => {
|
||||
if (e.OldValue is HorizontalAlignment oldha) {
|
||||
if (oldha == HorizontalAlignment.Right) {
|
||||
|
||||
}
|
||||
}
|
||||
((TextCell)d)._gcr.horizontal_content_alignment = (HorizontalAlignment)e.NewValue;
|
||||
}));
|
||||
(d, e) => ((TextCell)d)._gcr.horizontal_content_alignment = (HorizontalAlignment)e.NewValue));
|
||||
|
||||
public HorizontalAlignment HorizontalContentAlignment {
|
||||
get => (HorizontalAlignment)GetValue(HorizontalContentAlignmentProperty);
|
||||
@@ -130,7 +130,8 @@ namespace mbts.ui.controls {
|
||||
public static readonly DependencyProperty VerticalContentAlignmentProperty = Control.VerticalContentAlignmentProperty.AddOwner(typeof(TextCell),
|
||||
new FrameworkPropertyMetadata(
|
||||
VerticalAlignment.Center,
|
||||
FrameworkPropertyMetadataOptions.AffectsArrange,
|
||||
FrameworkPropertyMetadataOptions.AffectsArrange |
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(d, e) => ((TextCell)d)._gcr.vertical_content_alignment = (VerticalAlignment)e.NewValue));
|
||||
|
||||
public VerticalAlignment VerticalContentAlignment {
|
||||
@@ -210,8 +211,7 @@ namespace mbts.ui.controls {
|
||||
nameof(Padding),
|
||||
FrameworkPropertyMetadataOptions.AffectsMeasure |
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(p, v) => p._gcr.padding = v,
|
||||
DIYGrid.DefaultCellPadding);
|
||||
(p, v) => p._gcr.padding = default, default);
|
||||
|
||||
public Thickness Padding { get => (Thickness)GetValue(PaddingProperty); set => SetValue(PaddingProperty, value); }
|
||||
#endregion
|
||||
@@ -237,35 +237,17 @@ namespace mbts.ui.controls {
|
||||
return _gcr.layout_size;
|
||||
}
|
||||
|
||||
double bl_x = 0;
|
||||
protected override void OnRender(DrawingContext drawingContext) {
|
||||
var rs = RenderSize;
|
||||
var rect = new Rect(0, 0, rs.Width, rs.Height);
|
||||
var bg = Background;
|
||||
if (bg != null) {
|
||||
drawingContext.DrawRectangle(bg, _border_stroke_pen, rect);
|
||||
}
|
||||
if (_glyph_run != null) {
|
||||
var new_x = _glyph_run.BaselineOrigin.X;
|
||||
if (Math.Abs(bl_x - new_x) > 5) {
|
||||
|
||||
if (new_x < bl_x) {
|
||||
|
||||
}
|
||||
}
|
||||
if (Text == "-3,375.000") {
|
||||
|
||||
}
|
||||
bl_x = new_x;
|
||||
drawingContext.DrawRectangle(bg, _border_stroke_pen, rect, _snaps_to_device_pixels);
|
||||
var gr = _glyph_run;
|
||||
if (gr != null) {
|
||||
drawingContext.PushClip(new RectangleGeometry(rect));
|
||||
drawingContext.DrawGlyphRun(_foreground, _glyph_run);
|
||||
drawingContext.DrawGlyphRun(_foreground, gr);
|
||||
drawingContext.Pop();
|
||||
}
|
||||
if (bg != null) {
|
||||
var bgo = DIYGrid.GetBackgroundOverlay(this);
|
||||
if (bgo != null)
|
||||
drawingContext.DrawRectangle(bgo, null, rect);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,30 +1,76 @@
|
||||
using System;
|
||||
using bts.utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace mbts.ui.controls {
|
||||
// Column types are nested inside the DIYGrid
|
||||
partial class DIYGrid {
|
||||
public struct ColumnGeometry {
|
||||
public ColumnBase column;
|
||||
public DIYGridSuperHeader super_column;
|
||||
public double unclipped_left; /* If the column was unclipped */
|
||||
public double left;
|
||||
public double width { get { return column.ActualWidth; } }
|
||||
public double right { get { return unclipped_left + width; } }
|
||||
public readonly struct CellGeometry {
|
||||
public CellGeometry(ColumnGeometry column, ListBoxItem row) {
|
||||
this.column = column;
|
||||
this.row = row;
|
||||
}
|
||||
|
||||
public ColumnGeometry column { get; }
|
||||
public ListBoxItem row { get; }
|
||||
|
||||
public void Deconstruct(out ListBoxItem row, out ColumnGeometry column) {
|
||||
row = this.row;
|
||||
column = this.column;
|
||||
}
|
||||
|
||||
public Rect to_rect(double y) =>
|
||||
column.to_visible_rect(row.ActualHeight, y);
|
||||
|
||||
public static implicit operator (ListBoxItem, ColumnGeometry) (CellGeometry cell) =>
|
||||
(cell.row, cell.column);
|
||||
}
|
||||
|
||||
public readonly struct ColumnGeometry : IComparable<ColumnGeometry> {
|
||||
public static readonly IEqualityComparer<ColumnGeometry> PositionEqualityComparer =
|
||||
EqualityComparer.Create<ColumnGeometry, (double, double)>(c => (c.left, c.right));
|
||||
|
||||
public readonly ColumnBase column;
|
||||
public readonly DIYGridSuperHeader super_column;
|
||||
public readonly double
|
||||
left,
|
||||
// If the column was unclipped
|
||||
unclipped_left,
|
||||
right;
|
||||
|
||||
public ColumnGeometry(ColumnBase column, DIYGridSuperHeader super_column, double left, double right)
|
||||
: this(column, super_column, left, left, right) { }
|
||||
|
||||
public ColumnGeometry(ColumnBase column, DIYGridSuperHeader super_column, double left, double unclipped_left, double right) {
|
||||
this.column = column;
|
||||
this.super_column = super_column;
|
||||
this.left = left;
|
||||
this.unclipped_left = unclipped_left;
|
||||
this.right = right;
|
||||
}
|
||||
|
||||
public double width => right - left;
|
||||
public bool is_partially_obscured { get { return unclipped_left < left; } }
|
||||
public double visible_width => Math.Max(0, width - left + unclipped_left);
|
||||
public string group { get { return super_column == null ? "" : super_column.FullHeader; } }
|
||||
|
||||
public Rect to_rect(double row_height) =>
|
||||
new Rect(left, 0, width, row_height);
|
||||
|
||||
public Rect to_visible_rect(double row_height, double y) =>
|
||||
new Rect(unclipped_left, y, visible_width, row_height);
|
||||
|
||||
public override string ToString() {
|
||||
return "{3} {0:0.0},{1:0.0} w{2:0.0}".fmt(left, right, width, column.Header);
|
||||
}
|
||||
public string group { get { return super_column == null ? "" : super_column.FullHeader; } }
|
||||
|
||||
public RectangleGeometry get_clip(double row_height) =>
|
||||
new RectangleGeometry(new Rect(left, 0, width - left + unclipped_left, row_height));
|
||||
|
||||
public IDisposable clip_draw(DrawingContext dc, double row_height) {
|
||||
dc.PushClip(get_clip(row_height));
|
||||
return Disposable.Create(dc.Pop);
|
||||
public int CompareTo(ColumnGeometry other) {
|
||||
int comp = left.CompareTo(other.left);
|
||||
if (comp != 0)
|
||||
return comp;
|
||||
return right.CompareTo(other.right);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+50
-18
@@ -1,40 +1,72 @@
|
||||
using mbts.spark.filters;
|
||||
using mbts.ui.converters;
|
||||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
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 event Action<HyperlinkCell> view_generated;
|
||||
|
||||
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;
|
||||
|
||||
public override FrameworkElement view_factory() {
|
||||
var hl = new HyperlinkCell { Padding = 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.HorizontalAlignmentProperty, 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(HyperlinkCell.HorizontalContentAlignmentProperty, new Binding(nameof(horizontal_alignment)) { Source = this });
|
||||
hl.SetBinding(FrameworkElement.WidthProperty, new Binding(nameof(ActualWidth)) { Source = this });
|
||||
hl.SetBinding(
|
||||
CursorProperty,
|
||||
new Binding {
|
||||
Source = hl,
|
||||
Path = new PropertyPath(HyperlinkCell.IsMouseOverClickAreaProperty),
|
||||
Converter = new BooleanToObjectConverter {
|
||||
ValueForFalse = Cursors.Arrow,
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+29
-23
@@ -1,44 +1,50 @@
|
||||
using mbts.spark.filters;
|
||||
using System.Windows;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace mbts.ui.controls {
|
||||
partial class DIYGrid {
|
||||
internal class TextColumn<TColKey> : FrameworkElementColumnBase<TColKey>, IFilterableColumn {
|
||||
readonly BindingBase _binding;
|
||||
internal class TextColumn<TKey> : FrameworkElementColumnBase<TKey>, IFrameworkElementColumn, IFilterableColumn, IKeyedColumn<TKey> {
|
||||
static readonly Thickness _default_padding = new Thickness(6, 0, 6, 0);
|
||||
|
||||
public TextColumn(TColKey key, BindingBase binding, IDataGridColumnFilter filter, string filter_template) : base(key) {
|
||||
public TextColumn(
|
||||
TKey key, IDataGridColumnFilter filter, string filter_template) : base(key) {
|
||||
this.filter = filter;
|
||||
this.filter_template = filter_template;
|
||||
_binding = binding;
|
||||
}
|
||||
|
||||
public Style ElementStyle { get; set; }
|
||||
public IDataGridColumnFilter filter { get; }
|
||||
public string filter_template { get; }
|
||||
public BindingBase background_binding { get; set; }
|
||||
public BindingBase font_weight_binding { get; set; }
|
||||
public BindingBase foreground_binding { get; set; }
|
||||
public BindingBase tooltip_binding { get; set; }
|
||||
public VerticalAlignment vertical_alignment { get; set; } = VerticalAlignment.Center;
|
||||
public Thickness padding { get; set; } = _default_padding;
|
||||
// bindings
|
||||
public BindingBase TextBinding { get; set; }
|
||||
public BindingBase ToolTipBinding { get; set; }
|
||||
|
||||
public override FrameworkElement view_factory() {
|
||||
var v = new TextCell { Foreground = font_brush, VerticalAlignment = vertical_alignment };
|
||||
v.SetBinding(TextCell.TextProperty, _binding);
|
||||
var v = new TextCell {
|
||||
Padding = padding
|
||||
};
|
||||
// bindings
|
||||
if (TextBinding != null)
|
||||
v.SetBinding(TextCell.TextProperty, TextBinding);
|
||||
if (ToolTipBinding != null)
|
||||
v.SetBinding(ToolTipProperty, ToolTipBinding);
|
||||
v.SetBinding(TextCell.ForegroundProperty, new Binding(nameof(font_brush)) { Source = this });
|
||||
v.SetBinding(TextCell.FontFamilyProperty, new Binding(nameof(font_family)) { Source = this });
|
||||
v.SetBinding(TextCell.FontSizeProperty, new Binding(nameof(font_size)) { Source = this });
|
||||
v.SetBinding(TextCell.HorizontalAlignmentProperty, new Binding(nameof(horizontal_alignment)) { Source = this });
|
||||
v.SetBinding(TextCell.WidthProperty, new Binding(nameof(ActualWidth)) { Source = this });
|
||||
try_set_binding(TextCell.BackgroundProperty, background_binding);
|
||||
try_set_binding(TextCell.ForegroundProperty, foreground_binding);
|
||||
try_set_binding(TextCell.FontWeightProperty, font_weight_binding);
|
||||
try_set_binding(TextCell.ToolTipProperty, tooltip_binding);
|
||||
v.SetBinding(TextCell.HorizontalContentAlignmentProperty, new Binding(nameof(horizontal_alignment)) { Source = this });
|
||||
v.SetBinding(FrameworkElement.WidthProperty, new Binding(nameof(ActualWidth)) { Source = this });
|
||||
// background
|
||||
var bg_brush = new SolidColorBrush();
|
||||
BindingOperations.SetBinding(bg_brush, SolidColorBrush.ColorProperty, new Binding(nameof(background_color)) { Source = this });
|
||||
v.Background = bg_brush;
|
||||
// style
|
||||
if (ElementStyle != null)
|
||||
v.Style = ElementStyle;
|
||||
return v;
|
||||
//
|
||||
void try_set_binding(DependencyProperty p, BindingBase b) {
|
||||
if (b != null)
|
||||
v.SetBinding(p, b);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,12 @@
|
||||
using bts;
|
||||
using bts.api.core;
|
||||
using bts.collections;
|
||||
using bts.utils;
|
||||
using mbts.spark.filters;
|
||||
using mbts.ui.controls.adorners;
|
||||
using mbts.ui.converters;
|
||||
using mbts.ui.dpi;
|
||||
using mbts.ui.extensions;
|
||||
using mbts.ui.settings;
|
||||
using mbts.ui.utility;
|
||||
using mbts.utils;
|
||||
using System;
|
||||
@@ -22,6 +25,7 @@ using System.Windows.Controls.Primitives;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Shapes;
|
||||
|
||||
namespace mbts.ui.controls {
|
||||
interface IHandleGridSorting {
|
||||
@@ -74,6 +78,26 @@ namespace mbts.ui.controls {
|
||||
}
|
||||
}
|
||||
|
||||
public static void reset_sorting<TKey>(IEnumerable<DIYGrid.ColumnBase> columns, IEnumerable<SortingCriteria> sorting_criterion) where TKey : struct =>
|
||||
reset_sorting(columns, convert_sorting_criterion<TKey>(sorting_criterion));
|
||||
|
||||
public static void reset_sorting<TKey>(IEnumerable<DIYGrid.ColumnBase> columns, IEnumerable<(TKey key, ListSortDirection direction)> sorting_hierarchy) where TKey : struct {
|
||||
// TODO: incorporate the sort hierarchy/index, for nested sorting
|
||||
var active_cols = sorting_hierarchy.empty_if_null()
|
||||
.Select((k, i) => (k, i))
|
||||
.ToDictionary(tup => tup.k.key, tup => (tup.k.direction, tup.i));
|
||||
// columns are dispatcher objects, and might throw if they are accessed after the view has been shut down
|
||||
try {
|
||||
foreach (var c in columns) {
|
||||
if (Enum.TryParse(c.id, true, out TKey key) && active_cols.TryGetValue(key, out var tup)) {
|
||||
c.SortDirection = tup.direction;
|
||||
} else {
|
||||
c.SortDirection = null;
|
||||
}
|
||||
}
|
||||
} catch { }
|
||||
}
|
||||
|
||||
public static IObservable<ImmutableList<(TKey key, IComparer<TRow> comparer, ListSortDirection direction)>> observe_active_comparers<TKey, TRow>(
|
||||
DIYGrid grid, Func<TKey, IComparer<TRow>> get_comparer, bool mark_handled = true) =>
|
||||
//
|
||||
@@ -206,10 +230,11 @@ namespace mbts.ui.controls {
|
||||
double _row_height = DIYGrid.DefaultRowHeight;
|
||||
ScrollContentPresenter _content_presenter_backing;
|
||||
DIYGrid _grid;
|
||||
DragRectAdorner _drag_rect_adorner;
|
||||
AdornerHost<Rectangle> _drag_rect_adorner;
|
||||
RowSelectionAdorner _rsa;
|
||||
DIYGridCellHoverAdorner _cell_hover_adorner;
|
||||
ScrollViewer _scroll_viewer;
|
||||
Point? _drag_start;
|
||||
Point? _drag_start, _drag_stop;
|
||||
double _drag_start_vertical_drift;
|
||||
bool _support_group_items;
|
||||
int _drag_start_index;
|
||||
@@ -219,7 +244,10 @@ namespace mbts.ui.controls {
|
||||
DefaultStyleKeyProperty.OverrideMetadata(typeof(DIYGridRowsPresenter), new FrameworkPropertyMetadata(typeof(DIYGridRowsPresenter)));
|
||||
|
||||
public DIYGridRowsPresenter() {
|
||||
Loaded += (s, e) => {
|
||||
Loaded += on_loaded;
|
||||
Unloaded += on_unloaded;
|
||||
//
|
||||
void on_loaded(object sender, RoutedEventArgs e) {
|
||||
if (_grid != null)
|
||||
_grid.RowHeightChanged -= on_row_height_changed;
|
||||
_grid = this.find_ancestor_of_type<DIYGrid>();
|
||||
@@ -228,31 +256,111 @@ namespace mbts.ui.controls {
|
||||
_support_group_items = _grid.SupportGroupItems;
|
||||
_grid.SupportGroupItemsChanged += on_support_group_items_changed;
|
||||
_scroll_viewer = this.descendants_of_type<ScrollViewer>().First();
|
||||
if (SelectionMode != SelectionMode.Single) {
|
||||
_drag_rect_adorner = new DragRectAdorner(this, _scroll_viewer, _row_height);
|
||||
_drag_rect_adorner.SetResourceReference(DragRectAdorner.FillProperty, DIYGrid.ActiveSelectionHighlightKey);
|
||||
_drag_rect_adorner.SetResourceReference(DragRectAdorner.StrokeProperty, DIYGrid.SelectionStrokeKey);
|
||||
_drag_rect_adorner.attach();
|
||||
}
|
||||
var explicit_drag_src = Observable.Create<bool>(o => {
|
||||
_begin_explicit_drag += o.OnNext;
|
||||
return Disposable.Create(() => _begin_explicit_drag -= o.OnNext);
|
||||
});
|
||||
DragDropHelper.subscribe_drag_coordinates(this, init_element: _content_presenter, use_preview_events: true, explicit_drag: explicit_drag_src).Subscribe(pt => {
|
||||
if (_drag_start.HasValue) {
|
||||
if (pt.HasValue) {
|
||||
_drag_stop = pt.Value;
|
||||
_refresh_drag_rect();
|
||||
} else {
|
||||
if (_drag_stop.HasValue) {
|
||||
_end_drag(_drag_stop.Value);
|
||||
}
|
||||
_drag_start = null;
|
||||
_drag_stop = null;
|
||||
}
|
||||
} else if (pt.HasValue) {
|
||||
_begin_drag_selection(pt.Value);
|
||||
}
|
||||
});
|
||||
// cell hover adorner
|
||||
_cell_hover_adorner = new DIYGridCellHoverAdorner(this, _scroll_viewer);
|
||||
_cell_hover_adorner.SetBinding(VisibilityProperty, new Binding {
|
||||
Path = new PropertyPath(IsMouseOverProperty),
|
||||
Converter = new converters.BooleanToVisibilityConverter(),
|
||||
Source = this
|
||||
});
|
||||
// Uncomment call below to activate cell hover feedback.
|
||||
// In the meantime, cells handle the mouse hover feedback individually.
|
||||
//set_selection_display_bindings(
|
||||
// _cell_hover_adorner,
|
||||
// fill_prop: DIYGridCellHoverAdorner.CellFillProperty,
|
||||
// fill_converter: DIYGrid.CellHoverHighlightBrushConverter,
|
||||
// DIYGridCellHoverAdorner.CellStrokeProperty,
|
||||
// DIYGrid.CellHoverHighlightPenConverter,
|
||||
// bind_opacity: false);
|
||||
_set_selection_display_bindings(
|
||||
_cell_hover_adorner,
|
||||
fill_prop: DIYGridCellHoverAdorner.RowFillProperty,
|
||||
fill_converter: DIYGrid.RowHoverHighlightBrushConverter,
|
||||
//stroke_prop: DIYGridCellHoverAdorner.RowStrokeProperty,
|
||||
//stroke_converter: DIYGrid.CellHoverHighlightPenConverter,
|
||||
bind_opacity: false);
|
||||
_cell_hover_adorner.attach();
|
||||
// row selection adorner
|
||||
_rsa = new RowSelectionAdorner(this, _scroll_viewer);
|
||||
_set_selection_display_bindings(
|
||||
_rsa,
|
||||
fill_prop: RowSelectionAdorner.FillProperty,
|
||||
fill_converter: DIYGrid.SelectionHighlightBrushConverter,
|
||||
stroke_prop: RowSelectionAdorner.StrokeProperty,
|
||||
stroke_converter: DIYGrid.SelectionHighlightPenConverter);
|
||||
_rsa.attach();
|
||||
};
|
||||
Unloaded += (s, e) => {
|
||||
}
|
||||
|
||||
void on_unloaded(object sender, RoutedEventArgs e) {
|
||||
if (_drag_rect_adorner != null) {
|
||||
_drag_rect_adorner.detach();
|
||||
Disposable.dispose(ref _drag_rect_adorner);
|
||||
_drag_rect_adorner = null;
|
||||
}
|
||||
if (_rsa != null) {
|
||||
_rsa.detach();
|
||||
Disposable.dispose(ref _rsa);
|
||||
}
|
||||
};
|
||||
if (_cell_hover_adorner != null) {
|
||||
_cell_hover_adorner.detach();
|
||||
Disposable.dispose(ref _cell_hover_adorner);
|
||||
}
|
||||
}
|
||||
|
||||
void on_row_height_changed(double v) => _row_height = v;
|
||||
void on_support_group_items_changed(bool v) => _support_group_items = v;
|
||||
}
|
||||
|
||||
void _refresh_drag_rect() {
|
||||
var pres = _content_presenter;
|
||||
if (pres == null)
|
||||
return;
|
||||
//var left = Math.Min(_drag_start.Value.X, _drag_stop.Value.X);
|
||||
//var top = Math.Min(_drag_start.Value.Y, _drag_stop.Value.Y);
|
||||
//var width = Math.Abs(_drag_start.Value.X - _drag_stop.Value.X);
|
||||
//var height = Math.Abs(_drag_start.Value.Y - _drag_stop.Value.Y);
|
||||
|
||||
//var clipped_rect = this.GetChildBounds(pres);
|
||||
//clipped_rect.Intersect(new Rect(left, top, width, height));
|
||||
//_drag_rect_adorner.Child.Margin = new Thickness(clipped_rect.Left, clipped_rect.Top, 0, 0);
|
||||
//_drag_rect_adorner.Child.Width = clipped_rect.Width;
|
||||
//_drag_rect_adorner.Child.Height = clipped_rect.Height;
|
||||
|
||||
var left = Math.Min(_drag_start.Value.X, _drag_stop.Value.X);
|
||||
var top = Math.Min(_drag_start.Value.Y, _drag_stop.Value.Y);
|
||||
//var tl = TranslatePoint(new Point(left, top), pres);
|
||||
//var br = TranslatePoint(new Point(
|
||||
// tl.X + Math.Abs(_drag_start.Value.X - _drag_stop.Value.X),
|
||||
// tl.Y + Math.Abs(_drag_start.Value.Y - _drag_stop.Value.Y)), pres);
|
||||
double vdiff = _drag_start_vertical_drift - _scroll_viewer.VerticalOffset;
|
||||
_drag_rect_adorner.Child.Margin = new Thickness(left, top, 0, 0);
|
||||
_drag_rect_adorner.Child.Width = Math.Abs(_drag_start.Value.X - _drag_stop.Value.X);
|
||||
_drag_rect_adorner.Child.Height = Math.Abs(_drag_start.Value.Y - _drag_stop.Value.Y) + Math.Abs(vdiff);
|
||||
}
|
||||
|
||||
public event GroupItemSelectionPivotEventHandler GroupItemSelectionPivot;
|
||||
|
||||
event Action<bool> _begin_explicit_drag;
|
||||
|
||||
ScrollContentPresenter _content_presenter => _content_presenter_backing ??
|
||||
(_content_presenter_backing = this.descendants_of_type<ScrollContentPresenter>().FirstOrDefault());
|
||||
|
||||
@@ -264,8 +372,28 @@ namespace mbts.ui.controls {
|
||||
base.UnselectAll();
|
||||
}
|
||||
|
||||
protected override void OnPreviewMouseLeftButtonDown(MouseButtonEventArgs e) {
|
||||
var pt = e.GetPosition(this);
|
||||
public void begin_explicit_drag() {
|
||||
if (_begin_explicit_drag != null) {
|
||||
Mouse.Capture(this, CaptureMode.SubTree);
|
||||
if (!_drag_start.HasValue) {
|
||||
if (_begin_explicit_drag == null) {
|
||||
// capturing the mouse caused listener(s) to unhook from the event
|
||||
// release capture since the drag selection has been aborted.
|
||||
ReleaseMouseCapture();
|
||||
} else {
|
||||
var pt = Mouse.GetPosition(this);
|
||||
if (this.ChildAtPoint(pt, out ListBoxItem row)) {
|
||||
row.Focus();
|
||||
Keyboard.Focus(row);
|
||||
}
|
||||
_begin_explicit_drag(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _begin_drag_selection(Point pt) {
|
||||
_drag_rect_adorner?.detach();
|
||||
if (InputHitTest(pt) is UIElement el) {
|
||||
var sb = new StringBuilderEx("beginning drag selection on ");
|
||||
OneOf.MatchFirst<DependencyObject, ListBoxItem, GroupItem>(el.VisualAncestors().Prepend(el)).Match(init_common, on_group);
|
||||
@@ -279,22 +407,45 @@ namespace mbts.ui.controls {
|
||||
}
|
||||
}
|
||||
void init_common(ListBoxItem item_container) {
|
||||
// drag state
|
||||
_drag_start = pt;
|
||||
_drag_stop = pt;
|
||||
_drag_start_item = ItemContainerGenerator.ItemFromContainer(item_container);
|
||||
_drag_start_index = ItemContainerGenerator.IndexFromContainer(item_container);
|
||||
_drag_start_vertical_drift = _scroll_viewer.VerticalOffset;
|
||||
if (!IsKeyboardFocusWithin)
|
||||
Keyboard.Focus(item_container);
|
||||
// drag rect
|
||||
_drag_rect_adorner = new AdornerHost<Rectangle>(_content_presenter ?? (UIElement)this) {
|
||||
Child = new Rectangle {
|
||||
HorizontalAlignment = HorizontalAlignment.Left,
|
||||
VerticalAlignment = VerticalAlignment.Top,
|
||||
StrokeThickness = HighDpiExtensions.pixel_unit
|
||||
}
|
||||
};
|
||||
_set_selection_display_bindings(
|
||||
_drag_rect_adorner.Child,
|
||||
fill_prop: Shape.FillProperty,
|
||||
fill_converter: DIYGrid.SelectionHighlightBrushConverter,
|
||||
stroke_prop: Shape.StrokeProperty,
|
||||
stroke_converter: ValueConverter.create<Color, Brush>(c => new SolidColorBrush(c.with(255))));
|
||||
_refresh_drag_rect();
|
||||
_drag_rect_adorner.attach();
|
||||
// log
|
||||
sb.Add($"'{_drag_start_item}' (selection mode {SelectionMode}).");
|
||||
_log.info(sb.ToString());
|
||||
}
|
||||
}
|
||||
base.OnPreviewMouseLeftButtonDown(e);
|
||||
}
|
||||
|
||||
protected override void OnPreviewMouseUp(MouseButtonEventArgs e) {
|
||||
void _end_drag(Point drag_stop) {
|
||||
if (_drag_rect_adorner != null) {
|
||||
_drag_rect_adorner.detach();
|
||||
_drag_rect_adorner = null;
|
||||
}
|
||||
if (_drag_start.HasValue) {
|
||||
_log.info("ending drag selection on mouse up.");
|
||||
if (SelectionMode != SelectionMode.Single && _content_presenter != null) {
|
||||
var drag_stop = e.GetPosition(this);
|
||||
drag_stop.X = MathUtils.within_range(0, _content_presenter.RenderSize.Width - 1, drag_stop.X);
|
||||
drag_stop.Y = MathUtils.within_range(0, _content_presenter.RenderSize.Height - 1, drag_stop.Y);
|
||||
double vertical_diff = drag_stop.Y + (_row_height * (_scroll_viewer.VerticalOffset - _drag_start_vertical_drift)) - _drag_start.Value.Y;
|
||||
@@ -356,7 +507,6 @@ namespace mbts.ui.controls {
|
||||
_drag_start = null;
|
||||
_drag_start_item = null;
|
||||
}
|
||||
base.OnPreviewMouseUp(e);
|
||||
}
|
||||
|
||||
void _select_items_between(object inclusive_begin, object inclusive_end) {
|
||||
@@ -375,6 +525,36 @@ namespace mbts.ui.controls {
|
||||
.BookEnd(inclusive_begin, inclusive_end));
|
||||
}
|
||||
}
|
||||
|
||||
void _set_selection_display_bindings(DependencyObject target, DependencyProperty fill_prop = null, IValueConverter fill_converter = null, DependencyProperty stroke_prop = null, IValueConverter stroke_converter = null, bool bind_opacity = true) {
|
||||
// fill
|
||||
if (fill_prop != null) {
|
||||
BindingOperations.SetBinding(target, fill_prop,
|
||||
new Binding {
|
||||
Source = _grid,
|
||||
Path = new PropertyPath(DIYGrid.SelectionHighlightProperty),
|
||||
Converter = fill_converter
|
||||
});
|
||||
}
|
||||
// stroke
|
||||
if (stroke_prop != null) {
|
||||
BindingOperations.SetBinding(target, stroke_prop,
|
||||
new Binding {
|
||||
Source = _grid,
|
||||
Path = new PropertyPath(DIYGrid.SelectionHighlightProperty),
|
||||
Converter = stroke_converter
|
||||
});
|
||||
}
|
||||
// opacity
|
||||
if (bind_opacity) {
|
||||
BindingOperations.SetBinding(target, OpacityProperty,
|
||||
new Binding {
|
||||
Source = this,
|
||||
Path = new PropertyPath(IsSelectionActiveProperty),
|
||||
Converter = ValueConverter.create<bool, double>(is_active => is_active ? 1.0 : 0.5)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
partial class DIYGrid : Control {
|
||||
@@ -411,55 +591,48 @@ namespace mbts.ui.controls {
|
||||
// TODO: SelectionFill/Stroke should not be hardcoded, for now this serves as the central location for DragRectAdorner,
|
||||
// and FrameworkElementColumns that paint the selection brush themselves
|
||||
// (e.g MarketView bid/ask columns, bc their background is not transparent, it covers the ListBoxItem selection-highlight).
|
||||
public static readonly SolidColorBrush DefaultSelectionHighlight = new SolidColorBrush(Colors.MediumSlateBlue) { Opacity = 0.4 };
|
||||
public static readonly Pen DefaultSelectionStroke = new Pen(Brushes.MediumSlateBlue, 1);
|
||||
public static readonly Color DefaultSelectionHighlight = Colors.MediumSlateBlue.with(0x63);
|
||||
|
||||
readonly NotifyCollectionChangedEventHandler _sort_descriptions_collection_changed_cb;
|
||||
double _inactive_selection_opacity = 0.2;
|
||||
DataContextHooks _data_context_hooks;
|
||||
|
||||
public static readonly DependencyProperty SelectionHighlightProperty =
|
||||
WPFHelper.create_dp<SolidColorBrush, DIYGrid>(
|
||||
nameof(SelectionHighlight),
|
||||
(o, v) => {
|
||||
o.Resources[ActiveSelectionHighlightKey] = v;
|
||||
o._reset_inactive_selection_brush();
|
||||
o.Resources[SelectionStrokeKey] = new Pen(o.SelectionHighlight.Color.to_solid_brush().frozen(), 1).frozen();
|
||||
foreach (var col in o.Columns.empty_if_null().OfType<ISelectionHighlightListener>())
|
||||
col.on_selection_fill(v);
|
||||
var ad_layer = System.Windows.Documents.AdornerLayer.GetAdornerLayer(o);
|
||||
if (ad_layer != null) {
|
||||
foreach (var ad in ad_layer.descendants_of_type<ISelectionHighlightListener>()) {
|
||||
ad.on_selection_fill(v);
|
||||
}
|
||||
}
|
||||
}, (o, v) => v as SolidColorBrush ?? DefaultSelectionHighlight,
|
||||
DefaultSelectionHighlight);
|
||||
WPFHelper.create_dp<Color, DIYGrid>(nameof(SelectionHighlight), DefaultSelectionHighlight);
|
||||
|
||||
public SolidColorBrush SelectionHighlight {
|
||||
get => GetValue(SelectionHighlightProperty) as SolidColorBrush;
|
||||
public Color SelectionHighlight {
|
||||
get => (Color)GetValue(SelectionHighlightProperty);
|
||||
set => SetValue(SelectionHighlightProperty, value);
|
||||
}
|
||||
|
||||
static DIYGrid() {
|
||||
DefaultStyleKeyProperty.OverrideMetadata(typeof(DIYGrid), new FrameworkPropertyMetadata(typeof(DIYGrid)));
|
||||
DefaultSelectionHighlight.Freeze();
|
||||
DefaultSelectionStroke.Freeze();
|
||||
UseLayoutRoundingProperty.OverrideMetadata(typeof(DIYGrid), new FrameworkPropertyMetadata(OnUseLayoutRoundingChanged));
|
||||
|
||||
void OnUseLayoutRoundingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) =>
|
||||
d.CoerceValue(RowHeightProperty);
|
||||
}
|
||||
|
||||
public DIYGrid() {
|
||||
var sort_map = new Dictionary<string, ColumnBase>();
|
||||
_sort_descriptions_collection_changed_cb = WPFHelper.create_collection_changed_cb<SortDescription>(
|
||||
sd => {
|
||||
if (!SyncColumnSortDirectionWithSortDescriptions)
|
||||
return;
|
||||
if (Columns.TryGetFirst(c => c.sort_member_path == sd.PropertyName, out ColumnBase col)) {
|
||||
sort_map[sd.PropertyName] = col;
|
||||
col.SortDirection = sd.Direction;
|
||||
}
|
||||
}, sd => {
|
||||
if (!SyncColumnSortDirectionWithSortDescriptions)
|
||||
return;
|
||||
if (Columns.TryGetFirst(c => c.sort_member_path == sd.PropertyName, out ColumnBase col)) {
|
||||
col.SortDirection = null;
|
||||
sort_map.Remove(sd.PropertyName);
|
||||
}
|
||||
}, s => {
|
||||
if (!SyncColumnSortDirectionWithSortDescriptions)
|
||||
return;
|
||||
sort_map.Clear();
|
||||
if (Columns is IEnumerable<ColumnBase> cols) {
|
||||
foreach (var c in cols)
|
||||
@@ -505,7 +678,6 @@ namespace mbts.ui.controls {
|
||||
}
|
||||
}
|
||||
}));
|
||||
Resources[ActiveSelectionHighlightKey] = SelectionHighlight;
|
||||
_reset_inactive_selection_brush();
|
||||
_data_context_hooks = new DataContextHooks(this, DataContext);
|
||||
DataContextChanged += OnDataContextChanged;
|
||||
@@ -514,6 +686,21 @@ namespace mbts.ui.controls {
|
||||
public event DIYGridSortingEventHandler Sorting;
|
||||
public event Action<bool> SupportGroupItemsChanged;
|
||||
|
||||
public static IValueConverter SelectionHighlightPenConverter =>
|
||||
ValueConverter.create<Color, Pen>(c => FrozenPenCache.get_pen(c.with(255)));
|
||||
|
||||
public static IValueConverter SelectionHighlightBrushConverter =>
|
||||
ValueConverter.create<Color, Brush>(FrozenBrushCache.get_brush);
|
||||
|
||||
public static IValueConverter RowHoverHighlightBrushConverter =>
|
||||
ValueConverter.create<Color, Brush>(c => FrozenBrushCache.get_brush(c, 0.35));
|
||||
|
||||
public static IValueConverter CellHoverHighlightPenConverter =>
|
||||
ValueConverter.create<Color, Pen>(c => FrozenPenCache.get_pen(c.with(255)));
|
||||
|
||||
public static IValueConverter CellHoverHighlightBrushConverter =>
|
||||
ValueConverter.create<Color, Brush>(c => FrozenBrushCache.get_brush(c, 0.5));
|
||||
|
||||
public double InactiveSelectionOpacity {
|
||||
get => _inactive_selection_opacity;
|
||||
set {
|
||||
@@ -534,6 +721,8 @@ namespace mbts.ui.controls {
|
||||
}
|
||||
}
|
||||
|
||||
public bool SyncColumnSortDirectionWithSortDescriptions { get; set; } = true;
|
||||
|
||||
public ItemContainerGenerator RowContainerGenerator =>
|
||||
_row_presenter.ItemContainerGenerator;
|
||||
|
||||
@@ -833,6 +1022,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",
|
||||
@@ -1069,7 +1266,7 @@ namespace mbts.ui.controls {
|
||||
if (v < min_row_height)
|
||||
return min_row_height;
|
||||
}
|
||||
return v;
|
||||
return o.UseLayoutRounding ? v.RoundLayoutValue() : v;
|
||||
}, DefaultRowHeight);
|
||||
|
||||
public event Action<double> RowHeightChanged;
|
||||
@@ -1090,14 +1287,11 @@ namespace mbts.ui.controls {
|
||||
public static readonly DependencyProperty RowsProperty =
|
||||
WPFHelper.create_dp<IEnumerable, DIYGrid>(
|
||||
nameof(Rows),
|
||||
(s, oldval, newval) => {
|
||||
WPFHelper.manage_collection_change(
|
||||
(oldval as ICollectionView)?.SortDescriptions,
|
||||
(newval as ICollectionView)?.SortDescriptions,
|
||||
s._sort_descriptions_collection_changed_cb,
|
||||
old_ncc => s._sort_descriptions_collection_changed_cb(old_ncc, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)),
|
||||
new_ncc => s._sort_descriptions_collection_changed_cb(new_ncc, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, (IList)new_ncc)));
|
||||
},
|
||||
(s, oldval, newval) =>
|
||||
WPFHelper.manage_collection_change(
|
||||
(oldval as ICollectionView)?.SortDescriptions,
|
||||
(newval as ICollectionView)?.SortDescriptions,
|
||||
s._sort_descriptions_collection_changed_cb),
|
||||
Enumerable.Empty<object>());
|
||||
|
||||
public DataTemplate RowTemplate {
|
||||
@@ -1146,33 +1340,30 @@ namespace mbts.ui.controls {
|
||||
double frozen_offset = 0;
|
||||
int vis_frozen_col_count = _get_visible_frozen_column_count();
|
||||
foreach (var col in _visible_columns.Take(vis_frozen_col_count)) {
|
||||
yield return new ColumnGeometry() {
|
||||
column = col,
|
||||
left = frozen_offset,
|
||||
unclipped_left = frozen_offset,
|
||||
};
|
||||
double right = frozen_offset + col.ActualWidth;
|
||||
right = right.RoundLayoutValue();
|
||||
yield return new ColumnGeometry(col, null, frozen_offset, frozen_offset, right);
|
||||
frozen_offset += col.ActualWidth;
|
||||
}
|
||||
// frozen_offset will be off by 2 pixels because of the GridViewHeaderRowPresenter PaddingHeader (see c_PaddingHeaderMinWidth in referencesource)
|
||||
// We could use the _header_frozen.ActualWidth but due to the async nature of layout it get updated slightly too late
|
||||
frozen_offset += 2;
|
||||
if (_frozen_column_count > 0) {
|
||||
// frozen_offset will be off by 2 pixels because of the GridViewHeaderRowPresenter PaddingHeader (see c_PaddingHeaderMinWidth in referencesource)
|
||||
// We could use the _header_frozen.ActualWidth but due to the async nature of layout it get updated slightly too late
|
||||
frozen_offset += 2;
|
||||
}
|
||||
// Adjust by scroll amount
|
||||
double offset = frozen_offset - (_scroll_horizontal.Value);
|
||||
double offset = frozen_offset - _scroll_horizontal.Value;
|
||||
offset = offset.RoundLayoutValue();
|
||||
int count = 0;
|
||||
foreach (var col in _visible_columns.Skip(vis_frozen_col_count)) {
|
||||
double left = Math.Max(offset, frozen_offset);
|
||||
double right = offset + col.ActualWidth;
|
||||
right = right.RoundLayoutValue();
|
||||
// If right < frozen_offset column is completely obscured
|
||||
if (include_obscured_cols || (right > frozen_offset && left <= ActualWidth)) {
|
||||
// fully visible
|
||||
yield return new ColumnGeometry() {
|
||||
column = col,
|
||||
super_column = _superheader_column_index.ElementAtOrDefault(count),
|
||||
unclipped_left = offset,
|
||||
left = left
|
||||
};
|
||||
yield return new ColumnGeometry(col, _superheader_column_index.ElementAtOrDefault(count), left, offset, right);
|
||||
}
|
||||
offset += col.ActualWidth;
|
||||
offset = right;
|
||||
++count;
|
||||
}
|
||||
}
|
||||
@@ -1419,19 +1610,201 @@ namespace mbts.ui.controls {
|
||||
|
||||
public static IObservable<T> SubscribeMouseHoverRow<T>(this DIYGrid grid, IEqualityComparer<T> comparer) where T : class =>
|
||||
Observable.Create<T>(o => {
|
||||
T curr_row = null;
|
||||
T last_row = null;
|
||||
return grid.SubscribeMouseHoverRowContainer().Subscribe(row_view => {
|
||||
if (grid.RowContainerGenerator.ItemFromContainer(row_view) is T row && !comparer.Equals(row, last_row))
|
||||
o.OnNext(last_row = row);
|
||||
});
|
||||
});
|
||||
|
||||
public static IObservable<ListBoxItem> SubscribeMouseHoverRowContainer(this DIYGrid grid) =>
|
||||
Observable.Create<ListBoxItem>(o => {
|
||||
bool busy = false, dirty = false;
|
||||
ListBoxItem curr_row = null;
|
||||
return Disposable.Create(
|
||||
grid.SubscribeMouseEnter(on_mouse_enter),
|
||||
grid.SubscribeMouseLeave(on_mouse_leave),
|
||||
grid.SubscribePreviewMouseMove(on_mouse_move));
|
||||
void on_mouse_enter(object sender, MouseEventArgs e) => refresh();
|
||||
void on_mouse_move(object sender, MouseEventArgs e) => refresh();
|
||||
void on_mouse_leave(object sender, MouseEventArgs e) => refresh();
|
||||
grid.SubscribeMouseEnter(on_mouse),
|
||||
grid.SubscribeMouseLeave(on_mouse),
|
||||
grid.SubscribeMouseLeftButtonDown(on_mouse),
|
||||
grid.SubscribeMouseLeftButtonUp(on_mouse),
|
||||
grid.SubscribeMouseMove(on_mouse));
|
||||
void on_mouse(object sender, MouseEventArgs e) => refresh();
|
||||
void refresh() {
|
||||
T row = grid.ChildAtPoint<ListBoxItem>(Mouse.GetPosition(grid)) is var rview && rview != null ? grid.RowContainerGenerator.ItemFromContainer(rview) as T : null;
|
||||
if (!comparer.Equals(curr_row, row))
|
||||
o.OnNext(curr_row = row);
|
||||
if (busy) {
|
||||
dirty = true;
|
||||
} else {
|
||||
dirty = false;
|
||||
busy = true;
|
||||
do {
|
||||
grid.Dispatcher.InvokeAsync(() => {
|
||||
var row = grid.ChildAtPoint<ListBoxItem>(Mouse.GetPosition(grid));
|
||||
if (row != curr_row)
|
||||
o.OnNext(curr_row = row);
|
||||
dirty = false;
|
||||
});
|
||||
} while (dirty);
|
||||
busy = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
public static IObservable<DIYGrid.CellGeometry?> SubscribeMouseHoverCell(this DIYGrid grid) {
|
||||
return Observable.Create<DIYGrid.CellGeometry?>(o => {
|
||||
IDisposable row_mouse_hooks = null;
|
||||
DIYGrid.CellGeometry? last_value = null;
|
||||
var layout = grid.GetColumnGeometry().ToList();
|
||||
var layout_x_pts = layout.WrapSelector(cg => cg.left, null);
|
||||
ListBoxItem hover_row = null;
|
||||
grid.ColumnLayoutChanged += on_layout;
|
||||
var hover_sub = grid.SubscribeMouseHoverRowContainer().Subscribe(on_row_view);
|
||||
return Disposable.Create(dispose);
|
||||
//
|
||||
void notify(MouseEventArgs e = null) {
|
||||
if (hover_row == null || layout.Count == 0)
|
||||
on_cell(null);
|
||||
else {
|
||||
var pt = e?.GetPosition(hover_row) ?? Mouse.GetPosition(hover_row);
|
||||
int index = CollectionHelper.binary_search(layout_x_pts, pt.X);
|
||||
if (index < 0)
|
||||
index = ~index - 1;
|
||||
if (index >= 0 && index < layout.Count) {
|
||||
on_cell(new DIYGrid.CellGeometry(layout[index], hover_row));
|
||||
} else {
|
||||
on_cell(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
void on_cell(DIYGrid.CellGeometry? cell) {
|
||||
if (!cell.Equals(last_value))
|
||||
o.OnNext(last_value = cell);
|
||||
}
|
||||
void on_row_view(ListBoxItem row_view) {
|
||||
row_mouse_hooks?.Dispose();
|
||||
hover_row = row_view;
|
||||
row_mouse_hooks = row_view?.SubscribeMouseMove(on_mouse_change);
|
||||
notify();
|
||||
}
|
||||
void on_layout(List<DIYGrid.ColumnGeometry> obj) {
|
||||
layout = grid.GetColumnGeometry().ToList();
|
||||
layout_x_pts = layout.WrapSelector(cg => cg.left, null);
|
||||
notify();
|
||||
}
|
||||
void on_mouse_change(object sender, MouseEventArgs e) =>
|
||||
notify(e);
|
||||
void dispose() {
|
||||
Disposable.dispose(ref hover_sub);
|
||||
grid.ColumnLayoutChanged -= on_layout;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static IObservable<(IEnumerable<DIYGrid.ColumnGeometry> columns, int row_start, int row_count)?> SubscribeCellDragRect(this DIYGrid grid) {
|
||||
return Observable.Create<(IEnumerable<DIYGrid.ColumnGeometry> columns, int row_start, int row_count)?>(o => {
|
||||
return grid.ConnectOnLoaded(init);
|
||||
IDisposable init() {
|
||||
if (!( grid.try_find_descendent_of_type(out DIYGridRowsPresenter presenter) && presenter.try_find_descendent_of_type(out ScrollViewer scroll) && scroll.try_find_descendent_of_type(out ScrollContentPresenter scroll_content)))
|
||||
return null;
|
||||
// init state
|
||||
var value_container = new DistinctSubject<(IEnumerable<DIYGrid.ColumnGeometry> columns, int row_start, int row_count)?>();
|
||||
double y_start_drift = 0;
|
||||
var layout = new List<DIYGrid.ColumnGeometry>();
|
||||
var layout_x_pts = layout.WrapSelector(cg => cg.left, null);
|
||||
object _drag_start_item = null;
|
||||
int _drag_start_index = -1;
|
||||
Point? drag_start = null, drag_curr = null;
|
||||
//
|
||||
scroll.ScrollChanged += on_scroll;
|
||||
grid.ColumnLayoutChanged += on_layout;
|
||||
var drag_hook = DragDropHelper.subscribe_drag_coordinates(presenter, grid, use_preview_events: false, mouse_button: MouseButton.Right).Subscribe(pt => {
|
||||
if (drag_start.HasValue) {
|
||||
if (!pt.HasValue) {
|
||||
drag_start = null;
|
||||
drag_curr = null;
|
||||
} else {
|
||||
drag_curr = pt;
|
||||
}
|
||||
} else {
|
||||
if (pt.HasValue) {
|
||||
var item_container = presenter.ChildAtPoint<ListBoxItem>(pt.Value);
|
||||
if (item_container == null) {
|
||||
pt = null;
|
||||
} else {
|
||||
_drag_start_item = grid.RowContainerGenerator.ItemFromContainer(item_container);
|
||||
_drag_start_index = grid.RowContainerGenerator.IndexFromContainer(item_container);
|
||||
y_start_drift = scroll.VerticalOffset;
|
||||
}
|
||||
}
|
||||
drag_start = pt;
|
||||
drag_curr = pt;
|
||||
}
|
||||
refresh();
|
||||
});
|
||||
grid.PreviewMouseUp += (s, e) => e.Handled = e.ChangedButton == MouseButton.Right;
|
||||
var sub = value_container.Subscribe(o);
|
||||
return Disposable.Create(dispose);
|
||||
void on_layout(List<DIYGrid.ColumnGeometry> new_layout) {
|
||||
layout = new_layout;
|
||||
layout_x_pts = layout.WrapSelector(cg => cg.left, null);
|
||||
}
|
||||
void on_scroll(object sender, ScrollChangedEventArgs e) {
|
||||
if (drag_start.HasValue)
|
||||
refresh();
|
||||
}
|
||||
|
||||
void refresh() {
|
||||
if (!drag_start.HasValue) {
|
||||
value_container.OnNext(null);
|
||||
return;
|
||||
}
|
||||
var (xstart, ystart) = drag_start.Value.deconstruct();
|
||||
var (xstop, ystop) = drag_curr.Value.deconstruct();
|
||||
xstart = MathUtils.within_range(0, presenter.ActualWidth, xstart);
|
||||
var row_height = grid.RowHeight;
|
||||
xstop = MathUtils.within_range(0, scroll_content.RenderSize.Width - 1, xstop);
|
||||
ystop = MathUtils.within_range(0, scroll_content.RenderSize.Height - 1, ystop);
|
||||
double vertical_diff = ystop + (row_height * (scroll.VerticalOffset - y_start_drift)) - ystart;
|
||||
int start_index, n_items;
|
||||
if (vertical_diff > 0) {
|
||||
double first_item_partial_height = row_height - (ystart % row_height);
|
||||
n_items = (int)Math.Ceiling((vertical_diff - first_item_partial_height) / row_height) + 1;
|
||||
start_index = _drag_start_index;
|
||||
int overflow = start_index + n_items - presenter.Items.Count;
|
||||
if (overflow > 0)
|
||||
n_items -= overflow;
|
||||
} else {
|
||||
double first_item_partial_height = ystart % row_height;
|
||||
n_items = -(int)Math.Floor((vertical_diff + first_item_partial_height) / row_height) + 1;
|
||||
start_index = _drag_start_index - n_items + 1;
|
||||
if (start_index < 0) {
|
||||
n_items += start_index;
|
||||
start_index = 0;
|
||||
}
|
||||
}
|
||||
if (n_items > 0) {
|
||||
var colstart = find_col(xstart);
|
||||
var colstop = find_col(xstop);
|
||||
(colstart, colstop) = MathUtils.get_outliers(colstart, colstop);
|
||||
(ystart, ystop) = MathUtils.get_outliers(ystart, ystop);
|
||||
var cols = layout.WrapSubRange(colstart, colstop - colstart + 1);
|
||||
value_container.OnNext((cols, start_index, n_items));
|
||||
}
|
||||
}
|
||||
|
||||
int find_col(double x) {
|
||||
if (layout.Count == 0)
|
||||
return -1;
|
||||
int i = CollectionHelper.binary_search(layout, x, l => l.left);
|
||||
if (i < 0) {
|
||||
i = ~i - 1;
|
||||
}
|
||||
return MathUtils.within_range(0, layout.Count - 1, i);
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
Disposable.dispose(ref sub);
|
||||
grid.ColumnLayoutChanged -= on_layout;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
using bts.utils;
|
||||
using mbts.ui.controls;
|
||||
using mbts.ui.dpi;
|
||||
using mbts.ui.extensions;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Controls.Primitives;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace mbts.ui.spark {
|
||||
class DIYGridPopupManager {
|
||||
readonly DIYGrid _grid;
|
||||
readonly FrameworkElement _owner;
|
||||
readonly ScrollContentPresenter _scroll_content;
|
||||
readonly ICollection<Visual> _children;
|
||||
Popup _active_popup;
|
||||
|
||||
public static DIYGridPopupManager create(FrameworkElement owner) =>
|
||||
create(owner, new VisualCollectionWrapper(new VisualCollection(owner)));
|
||||
|
||||
public static DIYGridPopupManager create(FrameworkElement owner, ICollection<Visual> visual_children) =>
|
||||
owner.try_find_descendent_of_type(out DIYGrid grid) &&
|
||||
grid.try_find_descendent_of_type(out DIYGridRowsPresenter rows_presenter) &&
|
||||
rows_presenter.try_find_descendent_of_type(out ScrollContentPresenter scroll_content) ?
|
||||
new DIYGridPopupManager(owner, grid, scroll_content, visual_children) :
|
||||
null;
|
||||
|
||||
DIYGridPopupManager(FrameworkElement owner, DIYGrid grid, ScrollContentPresenter scroll_content, ICollection<Visual> children) {
|
||||
_owner = owner ?? throw new ArgumentNullException(nameof(owner));
|
||||
_grid = grid ?? throw new ArgumentNullException(nameof(grid));
|
||||
_scroll_content = scroll_content ?? throw new ArgumentNullException(nameof(scroll_content));
|
||||
_children = children;
|
||||
}
|
||||
|
||||
public Popup active_popup {
|
||||
get => _active_popup;
|
||||
set {
|
||||
if (_active_popup != value) {
|
||||
_active_popup = value;
|
||||
if (_active_popup != null) {
|
||||
_active_popup.IsOpen = false;
|
||||
_active_popup.IsOpen = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async void show(FrameworkElement view, int delay_measure_ms = 100) {
|
||||
var layout = _grid.GetColumnGeometry().ToList();
|
||||
if (layout.Count == 0)
|
||||
return;
|
||||
var x_origin = layout[0].unclipped_left;
|
||||
var mouse_pos = Mouse.GetPosition(_scroll_content);
|
||||
var col_index = CollectionHelper.binary_search(layout, mouse_pos.X + x_origin, cg => cg.left);
|
||||
if (col_index < 0)
|
||||
col_index = ~col_index - 1;
|
||||
col_index = MathUtils.within_range(0, layout.Count - 1, col_index);
|
||||
var col = layout[col_index];
|
||||
// determine top left
|
||||
var top_left = _scroll_content.PointToScreenPixels(
|
||||
col.unclipped_left - x_origin,
|
||||
Math.Ceiling(mouse_pos.Y / _grid.RowHeight) * _grid.RowHeight);
|
||||
if (delay_measure_ms > 0)
|
||||
await Task.Delay(delay_measure_ms);
|
||||
view.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
|
||||
// popup
|
||||
var popup = new ZPopup {
|
||||
PlacementTarget = _grid,
|
||||
Placement = PlacementMode.Absolute,
|
||||
Child = view,
|
||||
IsOpen = true,
|
||||
HorizontalOffset = top_left.X,
|
||||
VerticalOffset = top_left.Y,
|
||||
StaysOpen = false
|
||||
};
|
||||
popup.Child.IsMouseCaptureWithinChanged += (s, e) => {
|
||||
if (!popup.StaysOpen) {
|
||||
var pt = Mouse.GetPosition(popup.Child);
|
||||
if (!popup.Child.IsWithinBounds(pt)) {
|
||||
popup.IsOpen = false;
|
||||
if (_grid.try_find_descendent_of_type(out DIYGridRowsPresenter pres))
|
||||
pres.CaptureMouse();
|
||||
}
|
||||
}
|
||||
};
|
||||
popup.SetBinding(WindowStyle.UnsafeWindowProperty, new Binding {
|
||||
Source = _owner,
|
||||
Path = new PropertyPath(WindowStyle.UnsafeWindowProperty)
|
||||
});
|
||||
view.InputBindings.Add(new KeyBinding(ApplicationCommands.Close, Key.Escape, ModifierKeys.None));
|
||||
view.CommandBindings.Add(new CommandBinding(ApplicationCommands.Close, (s, e) => popup.IsOpen = false));
|
||||
|
||||
view.PreviewMouseDown += on_view_preview_mouse_down;
|
||||
var drag_hooks = PopupViewController.enable_drag_move(_owner, popup);
|
||||
_active_popup = popup;
|
||||
_children.Add(popup);
|
||||
_owner.ReleaseMouseCapture();
|
||||
popup.Closed += on_closed;
|
||||
FocusManager.SetIsFocusScope(view, true);
|
||||
view.Focus();
|
||||
//
|
||||
void on_closed(object sender, EventArgs e) {
|
||||
if (popup == _active_popup) {
|
||||
return;
|
||||
}
|
||||
view.PreviewMouseDown -= on_view_preview_mouse_down;
|
||||
Disposable.dispose(ref drag_hooks);
|
||||
popup.ReleaseMouseCapture();
|
||||
if (Mouse.Captured == _owner)
|
||||
_owner.ReleaseMouseCapture();
|
||||
popup.Closed -= on_closed;
|
||||
_children.Remove(popup);
|
||||
}
|
||||
|
||||
void on_view_preview_mouse_down(object sender, MouseButtonEventArgs e) =>
|
||||
active_popup = popup;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -55,6 +55,7 @@
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="Focusable" Value="False"/>
|
||||
<Setter Property="SelectionMode" Value="Extended"/>
|
||||
<Setter Property="UseLayoutRounding" Value="True"/>
|
||||
<Setter Property="ColumnHeaderStyle">
|
||||
<Setter.Value>
|
||||
<Style TargetType="GridViewColumnHeader">
|
||||
|
||||
@@ -26,12 +26,12 @@ namespace mbts.ui.controls {
|
||||
}
|
||||
}
|
||||
|
||||
public class HighPerformanceRow<TRowVM, TColKey> : HighPerformanceRowBase where TRowVM : class, IRowVM {
|
||||
public class HighPerformanceRow<TRowVM> : HighPerformanceRowBase where TRowVM : class, IRowVM {
|
||||
double _row_height;
|
||||
ObservableCollection<ColumnBase> _columns;
|
||||
List<ColumnGeometry> _column_layout = new List<ColumnGeometry>();
|
||||
Dictionary<TColKey, ColumnGeometry> _column_layout_map = new Dictionary<TColKey, ColumnGeometry>();
|
||||
VisualDictionary<TColKey, FrameworkElement> _element_cells;
|
||||
Dictionary<string, ColumnGeometry> _column_layout_map = new Dictionary<string, ColumnGeometry>();
|
||||
VisualDictionary<string, FrameworkElement> _element_cells;
|
||||
RecycleBin<DIYGrid> _grid_backing;
|
||||
RecycleBin<ListBoxItem> _container_backing;
|
||||
TRowVM _row;
|
||||
@@ -39,7 +39,7 @@ namespace mbts.ui.controls {
|
||||
public HighPerformanceRow() : this(DefaultRowHeight) { }
|
||||
public HighPerformanceRow(double row_height) {
|
||||
_row_height = row_height;
|
||||
_element_cells = new VisualDictionary<TColKey, FrameworkElement>(this);
|
||||
_element_cells = new VisualDictionary<string, FrameworkElement>(this);
|
||||
_grid_backing = new RecycleBin<DIYGrid>(
|
||||
g => { g.ColumnLayoutChanged += _on_column_layout_changed; g.RowHeightChanged += _on_row_height_changed; },
|
||||
g => { g.ColumnLayoutChanged -= _on_column_layout_changed; g.RowHeightChanged -= _on_row_height_changed; });
|
||||
@@ -123,7 +123,7 @@ namespace mbts.ui.controls {
|
||||
protected override Size ArrangeOverride(Size finalSize) {
|
||||
foreach (var kv in _column_layout_map) {
|
||||
if (_element_cells.TryGetValue(kv.Key, out FrameworkElement ec))
|
||||
ec.Arrange(new Rect(new Point(kv.Value.left, 0), new Size(kv.Value.width, _row_height)));
|
||||
ec.Arrange(kv.Value.to_rect(_row_height));
|
||||
}
|
||||
return finalSize;
|
||||
}
|
||||
@@ -148,16 +148,18 @@ namespace mbts.ui.controls {
|
||||
}
|
||||
|
||||
void _add_columns(IList items) {
|
||||
foreach (var col in items.OfType<FrameworkElementColumnBase<TColKey>>()) {
|
||||
var cell_view = col.view_factory();
|
||||
CellRender.change_position(cell_view, _row_height, _column_layout_map.GetNullable(col.key));
|
||||
_element_cells.Add(col.key, cell_view);
|
||||
foreach (ColumnBase col in items) {
|
||||
if (col is IFrameworkElementColumn el_col) {
|
||||
var cell_view = el_col.view_factory();
|
||||
CellRender.change_position(cell_view, _row_height, _column_layout_map.GetNullable(col.id));
|
||||
_element_cells.Add(col.id, cell_view);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _remove_columns(IList items) {
|
||||
foreach (var col in items.OfType<FrameworkElementColumnBase<TColKey>>())
|
||||
_element_cells.Remove(col.key);
|
||||
foreach (ColumnBase col in items)
|
||||
_element_cells.Remove(col.id);
|
||||
}
|
||||
|
||||
void _on_column_layout_changed(List<ColumnGeometry> column_layout) {
|
||||
@@ -171,10 +173,10 @@ namespace mbts.ui.controls {
|
||||
}
|
||||
|
||||
void _set_column_layout_map() {
|
||||
_column_layout_map = new Dictionary<TColKey, ColumnGeometry>();
|
||||
_column_layout_map = new Dictionary<string, ColumnGeometry>();
|
||||
foreach (var cg in _column_layout) {
|
||||
if (cg.column is FrameworkElementColumnBase<TColKey> elc)
|
||||
_column_layout_map.Add(elc.key, cg);
|
||||
if (cg.column is IFrameworkElementColumn)
|
||||
_column_layout_map.Add(cg.column.id, cg);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
@@ -53,11 +53,7 @@ namespace mbts.ui.controls {
|
||||
if (col.is_partially_obscured) {
|
||||
var obscured_width = col.left - col.unclipped_left;
|
||||
grid.scroll_horizontal(-obscured_width);
|
||||
return new ColumnGeometry() {
|
||||
column = col.column,
|
||||
left = col.left,
|
||||
unclipped_left = col.left
|
||||
};
|
||||
return new ColumnGeometry(col.column, col.super_column, col.left, col.left, col.right);
|
||||
}
|
||||
return col;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using bts.proto;
|
||||
using bts.utils;
|
||||
using mbts.ui.controls;
|
||||
using mbts.ui.converters;
|
||||
using ProtoBuf;
|
||||
using System;
|
||||
@@ -24,7 +23,7 @@ namespace mbts.ui.settings {
|
||||
|
||||
public static GridSettings Default =>
|
||||
new GridSettings(
|
||||
selection_highlight: new ColorOpacityProto(FallbackValues.selection_highlight.Color, FallbackValues.selection_highlight.Opacity),
|
||||
selection_highlight: FallbackValues.selection_highlight.rgb_color(),
|
||||
background: _default_background_color, //new ColorOpacityProto(FallbackValues.background_color.Color, FallbackValues.background_color.Opacity));
|
||||
enable_background_color: _default_enable_background_color);
|
||||
|
||||
@@ -45,11 +44,10 @@ namespace mbts.ui.settings {
|
||||
public static double font_size => _default_font_size;
|
||||
public static Color font_color => _default_font_color.rgb_color();
|
||||
public static FontWeight font_weight => FontWeight.FromOpenTypeWeight(_default_font_weight);
|
||||
public static SolidColorBrush selection_highlight => DIYGrid.DefaultSelectionHighlight;
|
||||
public static Color selection_highlight => Colors.MediumSlateBlue.with(0x63);
|
||||
public static uint background_color => _default_background;
|
||||
}
|
||||
|
||||
public static IValueConverter selection_highlight_converter => ValueConverter.create<GridSettings, SolidColorBrush>(gs => gs.selection_highlight.to_brush().frozen());
|
||||
public static IValueConverter font_family_converter => ValueConverter.create<GridSettings, FontFamily>(gs => new FontFamily(gs.font_family));
|
||||
public static IValueConverter font_weight_converter => ValueConverter.create<GridSettings, FontWeight>(gs => gs.font_weight);
|
||||
public static IValueConverter font_size_converter => ValueConverter.create<GridSettings, double>(gs => gs.font_size);
|
||||
@@ -71,8 +69,29 @@ namespace mbts.ui.settings {
|
||||
[DefaultValue(_default_font_color)]
|
||||
readonly uint _font_color;
|
||||
// selection highlight
|
||||
ColorOpacityProto _obsolete_selection_highlight_backing;
|
||||
uint _selection_highlight_backing;
|
||||
[ProtoMember(5)]
|
||||
readonly ColorOpacityProto _selection_highlight;
|
||||
ColorOpacityProto _obsolete_selection_highlight {
|
||||
get => _obsolete_selection_highlight_backing;
|
||||
set {
|
||||
_obsolete_selection_highlight_backing = value;
|
||||
// only overwrite non-obsolete selection highlight if it is 0 (assume that means 'unassigned').
|
||||
if (_selection_highlight_backing == 0) {
|
||||
var color = value.argb_color;
|
||||
// ensure reasonable opacity
|
||||
color.A = MathUtils.within_range(color.A,
|
||||
GridSettingsMigrationRules.SelectionHighlightIsTranslucent.min_alpha,
|
||||
GridSettingsMigrationRules.SelectionHighlightIsTranslucent.max_alpha);
|
||||
_selection_highlight_backing = color.rgb_color();
|
||||
}
|
||||
}
|
||||
}
|
||||
[ProtoMember(10)]
|
||||
uint _selection_highlight {
|
||||
get => _selection_highlight_backing;
|
||||
set => _selection_highlight_backing = value;
|
||||
}
|
||||
// columns
|
||||
[ProtoMember(6)]
|
||||
List<GridColumnSettings> _columns;
|
||||
@@ -101,7 +120,7 @@ namespace mbts.ui.settings {
|
||||
double font_size = _default_font_size,
|
||||
FontWeight font_weight = default,
|
||||
uint font_color = _default_font_color,
|
||||
ColorOpacityProto? selection_highlight = null,
|
||||
uint selection_highlight = 0,
|
||||
IEnumerable<GridColumnSettings> columns = null,
|
||||
IEnumerable<SortingCriteria> sorting_criterion = null,
|
||||
uint background = _default_background,
|
||||
@@ -111,7 +130,7 @@ namespace mbts.ui.settings {
|
||||
_font_size = font_size;
|
||||
_font_color = font_color;
|
||||
_font_weight = (ushort)font_weight.ToOpenTypeWeight();
|
||||
_selection_highlight = selection_highlight ?? new ColorOpacityProto(0, 1);
|
||||
_selection_highlight = selection_highlight;
|
||||
_columns = new List<GridColumnSettings>(columns.empty_if_null());
|
||||
_sorting_criterion = new List<SortingCriteria>(sorting_criterion.empty_if_null());
|
||||
_background = background;
|
||||
@@ -123,29 +142,27 @@ namespace mbts.ui.settings {
|
||||
public uint font_color => _font_color;
|
||||
public FontWeight font_weight => FontWeight.FromOpenTypeWeight(_font_weight);
|
||||
public Color background => _background.argb_color();
|
||||
public ColorOpacityProto selection_highlight => _selection_highlight;
|
||||
public Color selection_highlight => _selection_highlight.argb_color();
|
||||
public uint background_color => _background;
|
||||
public bool enable_background_color => _enable_background_color;
|
||||
public IReadOnlyList<GridColumnSettings> columns => _columns ?? (_columns = new List<GridColumnSettings>(0));
|
||||
[Obsolete] // TODO: remove sorting_criterion from GridSettings entirely. This should be part of IShellWindow's StartupData, since it is an instance-specific setting, and prob not part of the shared defaults
|
||||
public IReadOnlyList<SortingCriteria> sorting_criterion => _sorting_criterion ?? (_sorting_criterion = new List<SortingCriteria>(0));
|
||||
|
||||
public static GridSettings get_default() =>
|
||||
new GridSettings(_default_font_family, _default_font_size, FontWeights.Normal, _default_font_color, new ColorOpacityProto(Colors.MediumSlateBlue, 0.4));
|
||||
|
||||
public GridSettings with(
|
||||
IEnumerable<GridColumnSettings> columns = null,
|
||||
IEnumerable<SortingCriteria> sorting_criterion = null,
|
||||
string font_family = null,
|
||||
double font_size = double.NaN,
|
||||
uint? background = null,
|
||||
bool? enable_background_color = null) =>
|
||||
bool? enable_background_color = null,
|
||||
uint? selection_highlight = null) =>
|
||||
new GridSettings(
|
||||
font_family ?? _font_family,
|
||||
DoubleHelper.IsNaNOrInfinityOr(font_size, 0) ? _font_size : font_size,
|
||||
font_weight,
|
||||
_font_color,
|
||||
_selection_highlight,
|
||||
selection_highlight ?? _selection_highlight,
|
||||
columns ?? _columns,
|
||||
sorting_criterion ?? _sorting_criterion,
|
||||
background ?? _background,
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using bts.collections;
|
||||
using bts.settings;
|
||||
using bts.utils;
|
||||
using mbts.ui.controls;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@@ -116,4 +118,26 @@ namespace mbts.ui.settings {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static class GridSettingsMigrationRules {
|
||||
public class SelectionHighlightIsTranslucent : ISettingsMigrationRule<GridSettings> {
|
||||
public const byte min_alpha = 30, max_alpha = 200;
|
||||
public static bool apply(GridSettings settings, out GridSettings new_settings) {
|
||||
if (settings != null) {
|
||||
var sl = settings.selection_highlight;
|
||||
var a = MathUtils.within_range(sl.A, (byte)30, (byte)200);
|
||||
if (sl.A != a) {
|
||||
sl.A = a;
|
||||
new_settings = settings.with(selection_highlight: sl.rgb_color());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
new_settings = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ISettingsMigrationRule<GridSettings>.apply(GridSettings settings, out GridSettings new_settings) =>
|
||||
apply(settings, out new_settings);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,8 @@ namespace mbts.ui {
|
||||
readonly DelegateProperty<FontFamily> _font_family;
|
||||
readonly DelegateProperty<FontWeight> _font_weight;
|
||||
readonly DelegateProperty<double> _font_size;
|
||||
readonly DelegateProperty<SolidColorBrush> _font_color, _selection_highlight, _background_color;
|
||||
readonly DelegateProperty<SolidColorBrush> _font_color, _background_color;
|
||||
readonly DelegateProperty<Color> _selection_highlight;
|
||||
readonly SetMap<IPredicate<TRowData>, AnimationDefinitionWrapper<TRow>> _animation_defs = new SetMap<IPredicate<TRowData>, AnimationDefinitionWrapper<TRow>>();
|
||||
readonly Func<TPredicateData> _predicate_data_factory;
|
||||
readonly Func<TPredicateData, IPredicate<TRowData>> _predicate_factory;
|
||||
@@ -76,7 +77,7 @@ namespace mbts.ui {
|
||||
_font_weight = backing(nameof(font_weight), () => grid_settings.font_weight, nameof(grid_settings));
|
||||
_font_size = backing(nameof(font_size), () => grid_settings.font_size, nameof(font_size));
|
||||
_font_color = backing(nameof(font_color), () => grid_settings.font_color.rgb_solid_brush().frozen(), nameof(grid_settings));
|
||||
_selection_highlight = backing(nameof(selection_highlight), () => grid_settings.selection_highlight.to_brush().frozen(), nameof(grid_settings));
|
||||
_selection_highlight = backing(nameof(selection_highlight), () => grid_settings.selection_highlight, nameof(grid_settings));
|
||||
_background_color = backing(nameof(background_color), () => grid_settings.background_color.argb_color().to_solid_brush().frozen(), nameof(grid_settings));
|
||||
}
|
||||
|
||||
@@ -88,7 +89,7 @@ namespace mbts.ui {
|
||||
public FontWeight font_weight => _font_weight.Value;
|
||||
public double font_size => _font_size.Value;
|
||||
public SolidColorBrush font_color => _font_color.Value;
|
||||
public SolidColorBrush selection_highlight => _selection_highlight.Value;
|
||||
public Color selection_highlight => _selection_highlight.Value;
|
||||
public SolidColorBrush background_color => _background_color.Value;
|
||||
|
||||
public GridSettingsVM<TRowData, TPredicateData, TSettingsData> create_vm(GridSettings settings) {
|
||||
|
||||
@@ -134,7 +134,7 @@ namespace mbts.spark {
|
||||
foreach (IGrouping<TPredicateData, CellAnimation> g in animations)
|
||||
animation_map.AddOrUpdate(g.Key, p => animation_tuple(c: g), (p, t) => t.cell_animations.AddRange(g));
|
||||
}
|
||||
return _create_settings_obj(new GridSettings(font_family.name(), font_size, font_weight, font_color.color.rgb_color(), selection_highlight.to_proto(), col_settings,
|
||||
return _create_settings_obj(new GridSettings(font_family.name(), font_size, font_weight, font_color.color.rgb_color(), selection_highlight.color.rgb_color(), col_settings,
|
||||
background: background_color.color.with((byte)(MathUtils.within_range(0, 1, background_color.opacity) * 255.0)).rgb_color()),
|
||||
animation_map.Select(kv => PredicateAnimations.create_data<TRowData, TPredicateData>(kv.Key, kv.Value.cell_animations, kv.Value.row_animations, kv.Value.sounds)));
|
||||
//
|
||||
@@ -146,7 +146,7 @@ namespace mbts.spark {
|
||||
_font_family.set(new FontFamily(sd.general.font_family));
|
||||
_font_weight.set(sd.general.font_weight);
|
||||
font_color.load_from(sd.general.font_color.rgb_solid_brush());
|
||||
selection_highlight.load_from(sd.general.selection_highlight.to_brush());
|
||||
selection_highlight.color = sd.general.selection_highlight;
|
||||
_show_background_color.set(sd.general.enable_background_color);
|
||||
if (sd.general.enable_background_color) {
|
||||
// note that when the OB is moved to allow background colors
|
||||
|
||||
@@ -20,7 +20,8 @@ namespace mbts.ui {
|
||||
readonly DelegateProperty<FontFamily> _font_family;
|
||||
readonly DelegateProperty<FontWeight> _font_weight;
|
||||
readonly DelegateProperty<double> _font_size;
|
||||
readonly DelegateProperty<SolidColorBrush> _font_color, _selection_highlight, _background_color;
|
||||
readonly DelegateProperty<SolidColorBrush> _font_color, _background_color;
|
||||
readonly DelegateProperty<Color> _selection_highlight;
|
||||
readonly IReadOnlyDictionary<string, ImmutableColumnSettingStore> _column_flags;
|
||||
|
||||
public SimpleGridSettingsManager(ObservableCollection<ColumnBase> columns)
|
||||
@@ -34,7 +35,7 @@ namespace mbts.ui {
|
||||
_font_weight = backing(nameof(font_weight), () => grid_settings.font_weight, nameof(grid_settings));
|
||||
_font_size = backing(nameof(font_size), () => grid_settings.font_size, nameof(font_size));
|
||||
_font_color = backing(nameof(font_color), () => grid_settings.font_color.rgb_solid_brush().frozen(), nameof(grid_settings));
|
||||
_selection_highlight = backing(nameof(selection_highlight), () => grid_settings.selection_highlight.to_brush().frozen(), nameof(grid_settings));
|
||||
_selection_highlight = backing(nameof(selection_highlight), () => grid_settings.selection_highlight, nameof(grid_settings));
|
||||
_background_color = backing(nameof(background_color), () => grid_settings.background_color.argb_color().to_solid_brush().frozen(), nameof(grid_settings));
|
||||
_row_vms = row_vms;
|
||||
}
|
||||
@@ -44,8 +45,8 @@ namespace mbts.ui {
|
||||
public FontWeight font_weight => _font_weight.Value;
|
||||
public double font_size => _font_size.Value;
|
||||
public SolidColorBrush font_color => _font_color.Value;
|
||||
public SolidColorBrush selection_highlight => _selection_highlight.Value;
|
||||
public SolidColorBrush background_color => _background_color.Value;
|
||||
public Color selection_highlight => _selection_highlight.Value;
|
||||
|
||||
public SimpleGridSettingsVM create_vm(GridSettings settings) {
|
||||
var vm = new SimpleGridSettingsVM(_column_flags.ToDictionary(kv => kv.Key, kv => kv.Value.flags));
|
||||
|
||||
@@ -120,7 +120,7 @@ namespace mbts.spark {
|
||||
font_size,
|
||||
font_weight,
|
||||
font_color.color.rgb_color(),
|
||||
selection_highlight.to_proto(),
|
||||
selection_highlight.color.rgb_color(),
|
||||
columns.Select(c => c.create()));
|
||||
if (show_background_color)
|
||||
rv = rv.with(background: background_color.color.rgb_color());
|
||||
@@ -134,7 +134,7 @@ namespace mbts.spark {
|
||||
_font_family.set(new FontFamily(value.font_family));
|
||||
_font_weight.set(value.font_weight);
|
||||
font_color.load_from(value.font_color.rgb_solid_brush());
|
||||
selection_highlight.color = value.selection_highlight.argb_color;
|
||||
selection_highlight.color = value.selection_highlight;
|
||||
_show_background_color.set(value.enable_background_color);
|
||||
if (value.enable_background_color) {
|
||||
background_color.color = value.background;
|
||||
|
||||
@@ -1145,6 +1145,30 @@
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</usp:DynamicOrderParamTemplateSelector.UIntTemplate>
|
||||
<usp:DynamicOrderParamTemplateSelector.SecurityKeyTemplate>
|
||||
<DataTemplate>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="c0"/>
|
||||
<ColumnDefinition Width="{Binding Source={StaticResource HorizontalPush3}}"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock VerticalAlignment="Center" Text="{Binding descriptor.ParamName, Converter={x:Static ut:DisplayNameHelper.SnakeToTitleCaseConverter}}"/>
|
||||
<c:ContractPickerPopup
|
||||
Grid.Column="2"
|
||||
CloseOnCancel="True"
|
||||
CloseOnOK="True"
|
||||
Header="{Binding SelectedSecurity, RelativeSource={RelativeSource Self}, Converter={x:Static cv:ValueConverters.SecuritySelectorText}}"
|
||||
IsEditMode="False"
|
||||
IsEnabled="{Binding DataContext.editing_permissions, RelativeSource={RelativeSource AncestorType=ItemsControl}, Converter={StaticResource CanEditValuesConverter}}"
|
||||
IsLazy="True"
|
||||
SecurityMap="{DynamicResource ResourceKey={x:Static u:ResourceKeys.Securities}}"
|
||||
SelectedSecurityKey="{Binding value}"
|
||||
ShowDialogSkirt="True"
|
||||
/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</usp:DynamicOrderParamTemplateSelector.SecurityKeyTemplate>
|
||||
<usp:DynamicOrderParamTemplateSelector.StringTemplate>
|
||||
<DataTemplate>
|
||||
<Grid>
|
||||
@@ -1181,30 +1205,35 @@
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</usp:DynamicOrderParamTemplateSelector.EnumTemplate>
|
||||
<usp:DynamicOrderParamTemplateSelector.QuantityMeasureTemplate>
|
||||
<DataTemplate>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="c0"/>
|
||||
<ColumnDefinition Width="{Binding Source={StaticResource HorizontalPush3}}"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock VerticalAlignment="Center"
|
||||
Text="{Binding descriptor.ParamName, Converter={x:Static ut:DisplayNameHelper.SnakeToTitleCaseConverter}}"/>
|
||||
<uag:QuantityMeasureControl
|
||||
Grid.Column="2" Focusable="False" HorizontalAlignment="Stretch"
|
||||
QuantityMeasure="{Binding quantity, UpdateSourceTrigger=PropertyChanged}" ArrowKeyTickModifiers="Ctrl">
|
||||
<uag:QuantityMeasureControl.CommandBindings>
|
||||
<CommandBinding Command="{x:Static u:NavigationHelper.MoveFocusCommand}" Executed="{x:Static u:NavigationHelper.KeyboardMoveFocusExecuted}"/>
|
||||
</uag:QuantityMeasureControl.CommandBindings>
|
||||
<uag:QuantityMeasureControl.InputBindings>
|
||||
<KeyBinding Key="Up" Command="{x:Static u:NavigationHelper.MoveFocusCommand}" CommandParameter="{x:Static FocusNavigationDirection.Up}"/>
|
||||
<KeyBinding Key="Down" Command="{x:Static u:NavigationHelper.MoveFocusCommand}" CommandParameter="{x:Static FocusNavigationDirection.Down}"/>
|
||||
</uag:QuantityMeasureControl.InputBindings>
|
||||
</uag:QuantityMeasureControl>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</usp:DynamicOrderParamTemplateSelector.QuantityMeasureTemplate>
|
||||
<usp:DynamicOrderParamTemplateSelector.QuantityMeasureTemplate>
|
||||
<DataTemplate>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="c0"/>
|
||||
<ColumnDefinition Width="{Binding Source={StaticResource HorizontalPush3}}"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
Text="{Binding descriptor.ParamName, Converter={x:Static ut:DisplayNameHelper.SnakeToTitleCaseConverter}}"
|
||||
/>
|
||||
<uag:QuantityMeasureControl
|
||||
Grid.Column="2"
|
||||
Focusable="False"
|
||||
HorizontalAlignment="Stretch"
|
||||
QuantityMeasure="{Binding quantity, UpdateSourceTrigger=PropertyChanged}" ArrowKeyTickModifiers="Ctrl"
|
||||
>
|
||||
<uag:QuantityMeasureControl.CommandBindings>
|
||||
<CommandBinding Command="{x:Static u:NavigationHelper.MoveFocusCommand}" Executed="{x:Static u:NavigationHelper.KeyboardMoveFocusExecuted}"/>
|
||||
</uag:QuantityMeasureControl.CommandBindings>
|
||||
<uag:QuantityMeasureControl.InputBindings>
|
||||
<KeyBinding Key="Up" Command="{x:Static u:NavigationHelper.MoveFocusCommand}" CommandParameter="{x:Static FocusNavigationDirection.Up}"/>
|
||||
<KeyBinding Key="Down" Command="{x:Static u:NavigationHelper.MoveFocusCommand}" CommandParameter="{x:Static FocusNavigationDirection.Down}"/>
|
||||
</uag:QuantityMeasureControl.InputBindings>
|
||||
</uag:QuantityMeasureControl>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</usp:DynamicOrderParamTemplateSelector.QuantityMeasureTemplate>
|
||||
</usp:DynamicOrderParamTemplateSelector>
|
||||
</ItemsControl.ItemTemplateSelector>
|
||||
</ItemsControl>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:c="clr-namespace:mbts.ui.controls"
|
||||
xmlns:usp="clr-namespace:mbts.ui.spark"
|
||||
xmlns:uspcv="clr-namespace:mbts.ui.spark.converters">
|
||||
<ResourceDictionary
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:c="clr-namespace:mbts.ui.controls"
|
||||
xmlns:usp="clr-namespace:mbts.ui.spark"
|
||||
xmlns:uspcv="clr-namespace:mbts.ui.spark.converters"
|
||||
>
|
||||
<uspcv:OrderTypeBrushConverter x:Key="OrderTypeBrushConverter"/>
|
||||
<uspcv:OrderTypeDisplayTextConverter x:Key="OrderTypeDisplayTextConverter"/>
|
||||
<Style TargetType="usp:OrderTypeItem">
|
||||
@@ -26,15 +28,17 @@
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<c:Badge
|
||||
<Rectangle
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
Background="{Binding BadgeFill, RelativeSource={RelativeSource TemplatedParent}}"
|
||||
BorderThickness="1"
|
||||
Fill="{Binding BadgeFill, RelativeSource={RelativeSource TemplatedParent}}"
|
||||
Stroke="Black"
|
||||
StrokeThickness="1"
|
||||
Width="15"
|
||||
Height="10"
|
||||
Focusable="False"
|
||||
IsTabStop="False"
|
||||
RadiusX="{x:Static c:Badge.DefaultCornerRadius}"
|
||||
RadiusY="{x:Static c:Badge.DefaultCornerRadius}"
|
||||
/>
|
||||
<TextBlock
|
||||
x:Name="text"
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
<ResourceDictionary
|
||||
x:Class="mbts.ui.spark.AuxiliaryDataResources"
|
||||
x:ClassModifier="internal"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:usp="clr-namespace:mbts.ui.spark"
|
||||
xmlns:sp="clr-namespace:mbts.spark"
|
||||
>
|
||||
<!--Fills-->
|
||||
<DataTemplate DataType="{x:Type sp:FillsVM}">
|
||||
<usp:FillsView />
|
||||
</DataTemplate>
|
||||
<!--OrderBook-->
|
||||
<DataTemplate DataType="{x:Type sp:OrderBookVM}">
|
||||
<sp:MiniOrderBookView/>
|
||||
</DataTemplate>
|
||||
<!--Header (Title Bar)-->
|
||||
<DataTemplate DataType="{x:Type usp:AuxiliaryDataHeader}">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock VerticalAlignment="Center" Margin="4,0,0,0" FontWeight="Bold" Text="{Binding context}"/>
|
||||
<TextBlock Grid.Column="1" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="4,0,0,0" FontWeight="Bold" Text="{Binding title}">
|
||||
<TextBlock.Foreground>
|
||||
<SolidColorBrush Color="{Binding title_foreground}"/>
|
||||
</TextBlock.Foreground>
|
||||
</TextBlock>
|
||||
<CheckBox Grid.Column="2" VerticalAlignment="Center" Margin="4,0,0,0" Focusable="False" IsChecked="{Binding StaysOpen, RelativeSource={RelativeSource AncestorType=Popup}}">Keep Open</CheckBox>
|
||||
<Button Grid.Column="3" HorizontalAlignment="Right" Margin="4" Command="{x:Static ApplicationCommands.Close}" CommandTarget="{Binding RelativeSource={RelativeSource AncestorType=HeaderedContentControl}}" Style="{StaticResource RoundCloseButton}"/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate DataType="{x:Type usp:OrderTicketHeader}">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<ContentControl Content="{Binding bbo}"/>
|
||||
<CheckBox Grid.Column="2" VerticalAlignment="Center" Margin="4,0,0,0" Focusable="False" IsChecked="{Binding StaysOpen, RelativeSource={RelativeSource AncestorType=Popup}}">Keep Open</CheckBox>
|
||||
<Button Grid.Column="3" HorizontalAlignment="Right" Margin="4" Command="{x:Static ApplicationCommands.Close}" CommandTarget="{Binding RelativeSource={RelativeSource AncestorType=HeaderedContentControl}}" Style="{StaticResource RoundCloseButton}"/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
|
||||
<ControlTemplate x:Key="PopupChildTemplate" TargetType="HeaderedContentControl">
|
||||
<Border
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
>
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
<ContentPresenter ContentSource="Header"/>
|
||||
<ContentPresenter Grid.Row="1" Grid.ColumnSpan="4"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
|
||||
<Style x:Key="PopupChildStyleKey" TargetType="HeaderedContentControl">
|
||||
<Setter Property="Background" Value="{DynamicResource CommonBackground}"/>
|
||||
<Setter Property="BorderBrush" Value="Black"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="MaxHeight" Value="300"/>
|
||||
<Setter Property="Template" Value="{StaticResource PopupChildTemplate}"/>
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
@@ -0,0 +1,32 @@
|
||||
using mbts.spark;
|
||||
using mbts.ui.shell;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace mbts.ui.spark {
|
||||
partial class AuxiliaryDataResources : ResourceDictionary {
|
||||
public AuxiliaryDataResources() =>
|
||||
InitializeComponent();
|
||||
|
||||
public static string PopupChildStyleKey => nameof(PopupChildStyleKey);
|
||||
}
|
||||
|
||||
class AuxiliaryDataHeader : ViewModelBase {
|
||||
string
|
||||
_title,
|
||||
_context;
|
||||
Color _title_foreground;
|
||||
bool _keep_open;
|
||||
|
||||
public Color title_foreground { get => _title_foreground; set => Set(ref _title_foreground, value); }
|
||||
public string title { get => _title; set => Set(ref _title, value); }
|
||||
public string context { get => _context; set => Set(ref _context, value); }
|
||||
public bool keep_open { get => _keep_open; set => Set(ref _keep_open, value); }
|
||||
}
|
||||
|
||||
class OrderTicketHeader : ViewModelBase {
|
||||
bool _keep_open;
|
||||
public BBOVM bbo { get; set; }
|
||||
public bool keep_open { get => _keep_open; set => Set(ref _keep_open, value); }
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,29 @@
|
||||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls.Primitives;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace mbts.ui.controls {
|
||||
static class PopupViewController {
|
||||
#region ButtonSource
|
||||
public static readonly DependencyProperty ButtonSourceProperty =
|
||||
DependencyProperty.RegisterAttached("ButtonSource", typeof(ButtonBase), typeof(PopupViewController),
|
||||
new PropertyMetadata(OnButtonSourceChanged));
|
||||
|
||||
static void OnButtonSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
|
||||
if (d is Popup popup)
|
||||
popup.SetValue(ButtonSourceDependencyPropertyKey,
|
||||
e.NewValue is ButtonBase button ? attach(popup, button) : null);
|
||||
}
|
||||
|
||||
public static bool GetButtonSource(DependencyObject d) => (bool)d.GetValue(ButtonSourceProperty);
|
||||
public static void SetButtonSource(DependencyObject d, bool value) => d.SetValue(ButtonSourceProperty, value);
|
||||
|
||||
static readonly DependencyPropertyKey ButtonSourceDependencyPropertyKey =
|
||||
DependencyProperty.RegisterAttachedReadOnly("ButtonSourceDependency", typeof(IDisposable), typeof(PopupViewController),
|
||||
new PropertyMetadata(FrameworkElementLifeDependencyHelper.DisposeOld));
|
||||
#endregion
|
||||
|
||||
public static IDisposable attach(Popup popup, ButtonBase open_cmd_source, bool auto_focus_popup_root_on_open = false) {
|
||||
/// There is a paradoxical corner case handled here that occurs when the user clicks the button to 'close' the popup.
|
||||
/// Conventional logic might dictate that a <see cref="ToggleButton "/> be used to bind <see cref="ToggleButton.IsChecked"/> to <see cref="Popup.IsOpen"/>,
|
||||
@@ -34,5 +54,38 @@ namespace mbts.ui.controls {
|
||||
ignore_open_cmd = true;
|
||||
}
|
||||
}
|
||||
|
||||
public static IDisposable enable_drag_move(FrameworkElement surface, Popup popup) {
|
||||
Point? last_drag_pt = null;
|
||||
return DragDropHelper.subscribe_drag_coordinates(surface, popup.Child as FrameworkElement).Subscribe(drag_pt => {
|
||||
if (drag_pt.HasValue) {
|
||||
var d = drag_pt.Value;
|
||||
var rs = surface.RenderSize;
|
||||
if (last_drag_pt.HasValue) {
|
||||
popup.HorizontalOffset += d.X - last_drag_pt.Value.X;
|
||||
popup.VerticalOffset += d.Y - last_drag_pt.Value.Y;
|
||||
last_drag_pt = d;
|
||||
} else {
|
||||
popup.SetCurrentValue(Popup.StaysOpenProperty, true);
|
||||
last_drag_pt = d;
|
||||
Mouse.Capture(surface);
|
||||
}
|
||||
} else {
|
||||
last_drag_pt = null;
|
||||
if (popup.StaysOpen) {
|
||||
// popup.Child must be focused BEFORE the StaysOpen binding target (i.e., the popup itself) is updated;
|
||||
// Otherwise, if StaysOpen becomes false as a result of the update,
|
||||
// the popup would close in response to the 'LostFocus' event generated by the late Child.Focus call.
|
||||
// A reasonable person may ask, "Well why must the Popup.Child be focused at all?"
|
||||
// The answer is that the popup must be focused, to automatically close when it loses focus.
|
||||
popup.Child.Focus();
|
||||
surface.ReleaseMouseCapture();
|
||||
popup.GetBindingExpression(Popup.StaysOpenProperty)?.UpdateTarget();
|
||||
} else {
|
||||
surface.ReleaseMouseCapture();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
using mbts.ui.native;
|
||||
using System;
|
||||
using System.Reactive.Linq;
|
||||
using System.Threading;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls.Primitives;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Interop;
|
||||
|
||||
namespace mbts.ui.controls {
|
||||
/// <summary>
|
||||
/// Popup with an adjustable 'Z' order (ordinary Popup control is always top-most).
|
||||
/// </summary>
|
||||
class ZPopup : Popup {
|
||||
IDisposable _window_sub;
|
||||
Window _window;
|
||||
bool _is_top_most;
|
||||
|
||||
static ZPopup() =>
|
||||
ChildProperty.OverrideMetadata(typeof(ZPopup), new FrameworkPropertyMetadata(OnChildChanged));
|
||||
|
||||
public ZPopup() =>
|
||||
WindowStyle.AddUnsafeWindowChangedHandler(this, (s, e) => _on_window(e.NewValue));
|
||||
|
||||
#region IsTopMost
|
||||
public static readonly DependencyProperty IsTopMostProperty =
|
||||
WPFHelper.create_dp<bool, ZPopup>(nameof(IsTopMost), (o, v) => o._set_top_most_state(v));
|
||||
|
||||
public bool IsTopMost {
|
||||
get { return (bool)GetValue(IsTopMostProperty); }
|
||||
set { SetValue(IsTopMostProperty, value); }
|
||||
}
|
||||
#endregion
|
||||
|
||||
protected override void OnOpened(EventArgs e) =>
|
||||
_set_top_most_state(IsTopMost);
|
||||
|
||||
static void OnChildChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
|
||||
if (d is ZPopup zp) {
|
||||
if (e.OldValue is UIElement old_child)
|
||||
old_child.PreviewMouseLeftButtonDown -= zp.OnChildPreviewMouseLeftButtonDown;
|
||||
if (e.NewValue is UIElement new_child)
|
||||
new_child.PreviewMouseLeftButtonDown += zp.OnChildPreviewMouseLeftButtonDown;
|
||||
}
|
||||
}
|
||||
|
||||
void OnChildPreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e) =>
|
||||
SetCurrentValue(IsTopMostProperty, true);
|
||||
|
||||
void _on_window(Window win) {
|
||||
Disposable.dispose(ref _window_sub);
|
||||
_window = win;
|
||||
Thread.MemoryBarrier();
|
||||
if (win != null) {
|
||||
var src = Observable.Create<bool>(o => {
|
||||
o.OnNext(win.IsActive);
|
||||
win.Activated += on_activated;
|
||||
win.Deactivated += on_deactivated;
|
||||
return Disposable.Create(() => {
|
||||
win.Activated -= on_activated;
|
||||
win.Deactivated -= on_deactivated;
|
||||
});
|
||||
void on_activated(object sender, EventArgs e) => o.OnNext(true);
|
||||
void on_deactivated(object sender, EventArgs e) => o.OnNext(false);
|
||||
})
|
||||
.SubscribeOn(win.Dispatcher)
|
||||
.OnDispatcher();
|
||||
_window_sub = src.Subscribe(v => {
|
||||
if (v == _is_top_most) {
|
||||
/// <see cref="IsTopMost"/> is already equal to this value;
|
||||
/// explicitly refresh the child's z index.
|
||||
_set_top_most_state(v);
|
||||
} else {
|
||||
/// <see cref="_set_top_most_state(bool)"/> will be called in the dependency prop changed callback
|
||||
SetCurrentValue(IsTopMostProperty, v);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async void _set_top_most_state(bool value) {
|
||||
_is_top_most = value;
|
||||
if (Child != null && (PresentationSource.FromVisual(Child) is HwndSource hwnd_source)) {
|
||||
var hwnd = hwnd_source.Handle;
|
||||
if (NativeMethods.GetWindowRect(hwnd, out var rect)) {
|
||||
if (value) {
|
||||
var win = _window;
|
||||
if (win != null) {
|
||||
// bring popup to the front by activating parent window
|
||||
await win.Dispatcher.InvokeAsync(() => {
|
||||
// as this is an async op,
|
||||
// check that the window hasn't changed, and IsTopMost is still true before activating
|
||||
if (win == _window && _is_top_most)
|
||||
win.Activate();
|
||||
});
|
||||
} else {
|
||||
// bring popup [alone]to the front.
|
||||
NativeMethods.SetWindowPos(hwnd, NativeMethods.HWND_TOPMOST, rect.Left, rect.Top, (int)Width, (int)Height, SWP.TOPMOST_FLAGS);
|
||||
}
|
||||
} else {
|
||||
NativeMethods.SetWindowPos(hwnd, NativeMethods.HWND_NOTOPMOST, rect.Left, rect.Top, (int)Width, (int)Height, SWP.TOPMOST_FLAGS);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -55,6 +55,7 @@
|
||||
<SolidColorBrush Color="{StaticResource MouseOverBorderLightColor}"/>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
<Setter Property="Background" Value="{u:BTSColor UI.Button.mouseover_background}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter Property="Opacity" Value="0.5"/>
|
||||
@@ -62,6 +63,67 @@
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="SparkToolBarButton" TargetType="Button">
|
||||
<Setter Property="ToolTipService.ShowOnDisabled" Value="True"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="Height" Value="22"/>
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
|
||||
<Setter Property="Padding" Value="2"/>
|
||||
<Setter Property="VerticalContentAlignment" Value="Stretch"/>
|
||||
<Setter Property="Width" Value="22"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border
|
||||
x:Name="Border"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
Padding="{TemplateBinding Padding}"
|
||||
Height="{TemplateBinding Height}"
|
||||
Width="{TemplateBinding Width}"
|
||||
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"
|
||||
Opacity="1"
|
||||
>
|
||||
<Border.BorderBrush>
|
||||
<SolidColorBrush/>
|
||||
</Border.BorderBrush>
|
||||
<Border.Background>
|
||||
<SolidColorBrush/>
|
||||
</Border.Background>
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup Name="CommonStates">
|
||||
<VisualState Name="Normal"/>
|
||||
<VisualState Name="MouseOver">
|
||||
<Storyboard>
|
||||
<ColorAnimation Storyboard.TargetProperty="Background.Color" BeginTime="0" Duration="0" To="{DynamicResource Control.AlphaLightMediumGrayColor}"/>
|
||||
<ColorAnimation Storyboard.TargetProperty="BorderBrush.Color" BeginTime="0" Duration="0" To="{DynamicResource Control.DarkGrayColor}"/>
|
||||
</Storyboard>
|
||||
</VisualState>
|
||||
<VisualState Name="Pressed">
|
||||
<Storyboard>
|
||||
<ColorAnimation Storyboard.TargetProperty="Background.Color" BeginTime="0" Duration="0" To="{DynamicResource Control.AlphaMediumGrayColor}"/>
|
||||
<ColorAnimation Storyboard.TargetProperty="BorderBrush.Color" BeginTime="0" Duration="0" To="{DynamicResource Control.DarkGrayColor}"/>
|
||||
</Storyboard>
|
||||
</VisualState>
|
||||
<VisualState Name="Disabled">
|
||||
<Storyboard>
|
||||
<DoubleAnimation Storyboard.TargetProperty="Opacity" BeginTime="0" Duration="0" To="0.52"/>
|
||||
</Storyboard>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
<ContentPresenter
|
||||
Name="presenter"
|
||||
Grid.Column="1"
|
||||
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
|
||||
RecognizesAccessKey="True"
|
||||
/>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="ToolBarThumbStyle" TargetType="{x:Type Thumb}">
|
||||
<Setter Property="OverridesDefaultStyle" Value="True"/>
|
||||
<Setter Property="Cursor" Value="SizeAll"/>
|
||||
@@ -222,4 +284,4 @@
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
</ResourceDictionary>
|
||||
@@ -28,9 +28,17 @@ namespace mbts.ui.converters {
|
||||
IntTextConverter = new IntTextConverter(),
|
||||
UIntTextConverter = new UIntTextConverter(),
|
||||
QuantityMeasureTextConverter = ValueConverter.create<QuantityMeasureImmutable?, string>(fmt),
|
||||
SecuritySelectorText,
|
||||
SimpleString = new AnonymousValueConverter(obj => obj?.ToString()),
|
||||
ThicknessToDouble = new ThicknessToDoubleConverter();
|
||||
|
||||
static ValueConverters() {
|
||||
SecuritySelectorText = new NullSubstitutionConverterWrapper {
|
||||
Converter = SimpleString,
|
||||
ValueForNull = "(None)"
|
||||
};
|
||||
}
|
||||
|
||||
public static readonly IMultiValueConverter
|
||||
AreEqualConverter = new AreEqualConverter();
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace mbts.ui.converters {
|
||||
class DictionaryLookupConverter : IMultiValueConverter {
|
||||
[ThreadStatic]
|
||||
Dictionary<Type, Lookup> _type_impls_backing;
|
||||
Dictionary<Type, Lookup> _type_impls => _type_impls_backing ?? (_type_impls_backing = new Dictionary<Type, Lookup>());
|
||||
|
||||
delegate bool Lookup(object source, object key, out object result);
|
||||
|
||||
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) {
|
||||
if (values.Length == 2 && values[0] != null && values[1] != null) {
|
||||
var lookup_type = values[0].GetType();
|
||||
var lookup = _type_impls.GetOrAdd(lookup_type, TypeImplResolver.resolve);
|
||||
if (lookup(values[0], values[1], out var result))
|
||||
return result;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) =>
|
||||
throw new NotImplementedException();
|
||||
|
||||
static class TypeImplResolver {
|
||||
public static Lookup resolve(Type lookup_type) =>
|
||||
_prioritized_impls(lookup_type)
|
||||
.OrderBy(tup => tup.priority)
|
||||
.Select(tup => tup.impl)
|
||||
.FirstOrDefault() ?? _lookup_fail;
|
||||
|
||||
static bool _lookup_fail(object source, object key, out object result) {
|
||||
result = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
static IEnumerable<(int priority, Lookup impl)> _prioritized_impls(Type lookup_type) {
|
||||
foreach (var itype in lookup_type.GetInterfaces()) {
|
||||
if (itype.IsGenericType) {
|
||||
var gen_def = itype.GetGenericTypeDefinition();
|
||||
if (gen_def == typeof(IDictionary<,>))
|
||||
yield return (1, _get_generic_impl(nameof(_GenericImpls<int, int>.idictionary), itype));
|
||||
if (gen_def == typeof(IReadOnlyDictionary<,>))
|
||||
yield return (2, _get_generic_impl(nameof(_GenericImpls<int, int>.ireadonly_dictionary), itype));
|
||||
if (gen_def == typeof(ILookup<,>))
|
||||
yield return (3, _get_generic_impl(nameof(_GenericImpls<int, int>.ilookup), itype));
|
||||
}
|
||||
if (itype == typeof(IDictionary))
|
||||
yield return (4, _NonGenericImpls.idictionary);
|
||||
}
|
||||
}
|
||||
|
||||
static Lookup _get_generic_impl(string impl_name, Type impl_type) =>
|
||||
_get_generic_impl(impl_name, impl_type.GetGenericArguments());
|
||||
|
||||
static Lookup _get_generic_impl(string impl_name, Type[] key_value_types) =>
|
||||
(Lookup)typeof(_GenericImpls<,>)
|
||||
.MakeGenericType(key_value_types)
|
||||
.GetField(nameof(_GenericImpls<int, int>.idictionary))
|
||||
.GetValue(null);
|
||||
|
||||
static class _GenericImpls<TKey, TValue> {
|
||||
public static readonly Lookup
|
||||
idictionary = _idictionary,
|
||||
ireadonly_dictionary = _ireadonly_dictionary,
|
||||
ilookup = _ilookup;
|
||||
|
||||
static bool _idictionary(object lookup, object key, out object result) {
|
||||
if (key is TKey k && lookup is IDictionary<TKey, TValue> d && d.TryGetValue(k, out var value)) {
|
||||
result = value;
|
||||
return true;
|
||||
}
|
||||
result = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool _ireadonly_dictionary(object lookup, object key, out object result) {
|
||||
if (key is TKey k && lookup is IReadOnlyDictionary<TKey, TValue> rod && rod.TryGetValue(k, out var value)) {
|
||||
result = value;
|
||||
return true;
|
||||
}
|
||||
result = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool _ilookup(object lookup, object key, out object result) {
|
||||
if ((key is TKey k) && lookup is ILookup<TKey, TValue> l && l.Contains(k)) {
|
||||
result = l[k];
|
||||
return true;
|
||||
}
|
||||
result = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static class _NonGenericImpls {
|
||||
public static readonly Lookup
|
||||
idictionary = _idictionary;
|
||||
|
||||
static bool _idictionary(object lookup, object key, out object result) {
|
||||
var dict = (IDictionary)lookup;
|
||||
if (dict.Contains(key)) {
|
||||
result = dict[key];
|
||||
return true;
|
||||
}
|
||||
result = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,11 +5,11 @@ using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace mbts.ui.converters
|
||||
{
|
||||
namespace mbts.ui.converters {
|
||||
[ValueConversion(typeof(ImageSource), typeof(Image))]
|
||||
internal class ImageSourceIconConverter : IValueConverter
|
||||
{
|
||||
class ImageSourceIconConverter : IValueConverter {
|
||||
public static ImageSourceIconConverter Instance => new ImageSourceIconConverter();
|
||||
|
||||
public Thickness Margin { get; set; } = new Thickness(0);
|
||||
public double Width { get; set; } = double.NaN;
|
||||
public double Height { get; set; } = double.NaN;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using bts.utils;
|
||||
using mbts.ui.extensions;
|
||||
using mbts.ui.shell;
|
||||
using mbts.ui.utilities;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Reactive.Linq;
|
||||
@@ -68,26 +69,57 @@ namespace mbts.ui {
|
||||
}
|
||||
});
|
||||
|
||||
public static IObservable<Point?> subscribe_drag_coordinates(FrameworkElement element) =>
|
||||
Observable.Create<Point?>(o => {
|
||||
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,
|
||||
IObservable<bool> explicit_drag = null) {
|
||||
if (element == null)
|
||||
throw new ArgumentNullException(nameof(element));
|
||||
if (init_element == null)
|
||||
init_element = element;
|
||||
return Observable.Create<Point?>(o => {
|
||||
Point? mouse_down = null, drag_start = null;
|
||||
element.MouseDown += on_mouse_down;
|
||||
element.MouseMove += on_mouse_move;
|
||||
element.MouseUp += on_mouse_up;
|
||||
bool is_explict_drag = false;
|
||||
if (use_preview_events) {
|
||||
init_element.PreviewMouseDown += on_mouse_down;
|
||||
element.PreviewMouseMove += on_mouse_move;
|
||||
element.PreviewMouseUp += on_mouse_up;
|
||||
} else {
|
||||
init_element.MouseDown += on_mouse_down;
|
||||
element.MouseMove += on_mouse_move;
|
||||
element.MouseUp += on_mouse_up;
|
||||
}
|
||||
IDisposable explicit_drag_hook = explicit_drag?.Subscribe(begin_drag => {
|
||||
if (begin_drag && drag_start == null && mouse_down == null) {
|
||||
mouse_down = Mouse.GetPosition(element);
|
||||
is_explict_drag = true;
|
||||
}
|
||||
});
|
||||
return Disposable.Create(() => {
|
||||
element.MouseDown -= on_mouse_down;
|
||||
element.MouseMove -= on_mouse_move;
|
||||
element.MouseUp -= on_mouse_up;
|
||||
if (use_preview_events) {
|
||||
init_element.PreviewMouseDown -= on_mouse_down;
|
||||
element.PreviewMouseMove -= on_mouse_move;
|
||||
element.PreviewMouseUp -= on_mouse_up;
|
||||
} else {
|
||||
init_element.MouseDown -= on_mouse_down;
|
||||
element.MouseMove -= on_mouse_move;
|
||||
element.MouseUp -= on_mouse_up;
|
||||
}
|
||||
Disposable.dispose(ref explicit_drag_hook);
|
||||
});
|
||||
// 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);
|
||||
}
|
||||
void on_mouse_move(object sender, MouseEventArgs e) {
|
||||
if (drag_start.HasValue) {
|
||||
o.OnNext(Mouse.LeftButton == MouseButtonState.Pressed ? e.GetPosition(element) : (drag_start = null));
|
||||
o.OnNext(MouseHelper.button_state(mouse_button) == MouseButtonState.Pressed ? e.GetPosition(element) : (drag_start = null));
|
||||
e.Handled = true;
|
||||
} else if (mouse_down.HasValue) {
|
||||
var curr_pt = e.GetPosition(element);
|
||||
@@ -95,15 +127,22 @@ 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) {
|
||||
drag_start = null;
|
||||
mouse_down = e.GetPosition(element);
|
||||
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 = pt;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static IDisposable connect_horizontal_insertion_adorner(ItemsControl container, FrameworkElement root = null) {
|
||||
HorizontalInsertionAdorner insertion_adorner = null;
|
||||
@@ -339,6 +378,7 @@ namespace mbts.ui {
|
||||
}
|
||||
new_window.move_to_mouse();
|
||||
new_window.Visibility = Visibility.Visible;
|
||||
new_window.Activate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,10 +123,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);
|
||||
|
||||
@@ -1,18 +1,26 @@
|
||||
using mbts.config;
|
||||
using bts.utils;
|
||||
using mbts.config;
|
||||
using mbts.ui.controls;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Drawing2D;
|
||||
using System.Windows;
|
||||
using System.Windows.Forms;
|
||||
using System.Windows.Media;
|
||||
using Point = System.Windows.Point;
|
||||
|
||||
namespace mbts.ui.dpi {
|
||||
public static class HighDpiExtensions {
|
||||
public static readonly double dpi_ratio;
|
||||
public static readonly double
|
||||
dpi_ratio,
|
||||
pixel_unit;
|
||||
public static readonly float fdpi_ratio;
|
||||
|
||||
static HighDpiExtensions() {
|
||||
dpi_ratio = Configuration.get<bts.config.System>().dpi_ratio;
|
||||
pixel_unit = 1 / dpi_ratio;
|
||||
fdpi_ratio = (float)dpi_ratio;
|
||||
}
|
||||
|
||||
@@ -119,5 +127,126 @@ namespace mbts.ui.dpi {
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
public static Point PointToScreenPixels(this Visual visual, double x, double y) =>
|
||||
PointToScreenPixels(visual, new Point(x, y));
|
||||
|
||||
public static Point PointToScreenPixels(this Visual visual, Point point) {
|
||||
var pt = visual.PointToScreen(point);
|
||||
return new Point(pt.X * pixel_unit, pt.Y * pixel_unit);
|
||||
}
|
||||
/// <summary>
|
||||
/// Calculates the value to be used for layout rounding at high DPI.
|
||||
/// </summary>
|
||||
/// <param name="value">Input value to be rounded.</param>
|
||||
/// <param name="dpiScale">Ratio of screen's DPI to layout DPI</param>
|
||||
/// <returns>Adjusted value that will produce layout rounding on screen at high dpi.</returns>
|
||||
/// <remarks>This is a layout helper method. It takes DPI into account and also does not return
|
||||
/// the rounded value if it is unacceptable for layout, e.g. Infinity or NaN. It's a helper associated with
|
||||
/// UseLayoutRounding property and should not be used as a general rounding utility.</remarks>
|
||||
public static double RoundLayoutValue(this double value) =>
|
||||
RoundLayoutValue(value, dpi_ratio);
|
||||
|
||||
public static double RoundLayoutValue(double value, double dpiScale) {
|
||||
if (DoubleUtil.AreClose(dpiScale, 1.0))
|
||||
return Math.Round(value);
|
||||
double result = Math.Round(value * dpiScale) / dpiScale;
|
||||
return DoubleHelper.IsNaNOrInfinity(result) || DoubleUtil.AreClose(result, double.MaxValue) ? value : result;
|
||||
}
|
||||
|
||||
public static Point RoundToLayout(this Point point) =>
|
||||
new Point(RoundLayoutValue(point.X), RoundLayoutValue(point.Y));
|
||||
|
||||
public static Rect RoundToLayout(this Rect rect) =>
|
||||
new Rect(rect.TopLeft.RoundToLayout(), rect.Size.RoundToLayout());
|
||||
|
||||
public static System.Windows.Size RoundToLayout(this System.Windows.Size size) =>
|
||||
new System.Windows.Size(size.Width.RoundLayoutValue(), size.Height.RoundLayoutValue());
|
||||
|
||||
public static Thickness RoundToLayout(this Thickness thickness) =>
|
||||
new Thickness(
|
||||
thickness.Left.RoundLayoutValue(),
|
||||
thickness.Top.RoundLayoutValue(),
|
||||
thickness.Right.RoundLayoutValue(),
|
||||
thickness.Bottom.RoundLayoutValue());
|
||||
|
||||
public static GuidelineSet AntiAliasGuidelines(this Rect rect) =>
|
||||
rect.AntiAliasGuidelines(dpi_ratio);
|
||||
|
||||
public static GuidelineSet AntiAliasGuidelines(this Rect rect, double pen_thickness) {
|
||||
var half_pen_thickness = pen_thickness / 2;
|
||||
//half_pen_thickness = 0;
|
||||
var guidelines = new GuidelineSet(
|
||||
new[] { rect.Left.RoundLayoutValue() - half_pen_thickness, rect.Right.RoundLayoutValue() + half_pen_thickness },
|
||||
new[] { rect.Top.RoundLayoutValue() - half_pen_thickness, rect.Bottom.RoundLayoutValue() + half_pen_thickness });
|
||||
return guidelines;
|
||||
}
|
||||
|
||||
public static void DrawHorizontalLine(this DrawingContext drawing_context, System.Windows.Media.Pen pen, Point start, double length, bool snap_to_device_pixels) {
|
||||
if (pen == null || !length.IsRealNonZero()) {
|
||||
// no need to flip out and throw an exception
|
||||
return;
|
||||
}
|
||||
if (snap_to_device_pixels) {
|
||||
start = start.RoundToLayout();
|
||||
var half_pen = pen.Thickness / 2;
|
||||
var stop = new Point((start.X + length).RoundLayoutValue(), start.Y);
|
||||
var (left_edge, right_edge) = MathUtils.get_outliers(start.X, stop.X);
|
||||
var guidelines = new GuidelineSet(
|
||||
new[] { left_edge + half_pen, right_edge - half_pen },
|
||||
new[] { start.Y - half_pen });
|
||||
drawing_context.PushGuidelineSet(guidelines);
|
||||
drawing_context.DrawLine(pen, start, stop);
|
||||
drawing_context.Pop();
|
||||
} else {
|
||||
drawing_context.DrawLine(pen, start, new Point(start.X + length, start.Y));
|
||||
}
|
||||
}
|
||||
|
||||
public static void DrawLine(this DrawingContext drawing_context, System.Windows.Media.Pen pen, Point point0, Point point1, bool snap_to_device_pixels) {
|
||||
if (pen == null) {
|
||||
// no need to flip out and throw an exception
|
||||
return;
|
||||
}
|
||||
if (snap_to_device_pixels) {
|
||||
point0 = point0.RoundToLayout();
|
||||
point1 = point1.RoundToLayout();
|
||||
var (left_edge, right_edge) = MathUtils.get_outliers(point0.X, point1.X);
|
||||
var (top_edge, bottom_edge) = MathUtils.get_outliers(point0.Y, point1.Y);
|
||||
var x = point0.X;
|
||||
var y = point0.Y;
|
||||
var half_pen = pen.Thickness / 2;
|
||||
drawing_context.PushGuidelineSet(new GuidelineSet(
|
||||
new[] { left_edge + half_pen, right_edge - half_pen },
|
||||
new[] { top_edge + half_pen, bottom_edge - half_pen }));
|
||||
drawing_context.DrawLine(pen, point0, point1);
|
||||
drawing_context.Pop();
|
||||
} else {
|
||||
drawing_context.DrawLine(pen, point0, point1);
|
||||
}
|
||||
}
|
||||
|
||||
public static void DrawRectangle(this DrawingContext drawing_context, System.Windows.Media.Brush brush, System.Windows.Media.Pen pen, Rect rectangle, bool snap_to_device_pixels) {
|
||||
if ((brush == null && pen == null) || rectangle.IsEmpty) {
|
||||
// no need to flip out and throw an exception
|
||||
return;
|
||||
}
|
||||
if (snap_to_device_pixels) {
|
||||
if (pen != null) {
|
||||
if (rectangle.Width > pen.Thickness && rectangle.Height > pen.Thickness) {
|
||||
var half_pen = pen.Thickness / 2;
|
||||
rectangle = rectangle.RoundToLayout();
|
||||
rectangle = new Rect(rectangle.Left + half_pen, rectangle.Top + half_pen, rectangle.Width - pen.Thickness, rectangle.Height - pen.Thickness);
|
||||
drawing_context.DrawRectangle(brush, pen, rectangle);
|
||||
}
|
||||
} else {
|
||||
drawing_context.PushGuidelineSet(rectangle.AntiAliasGuidelines(0));
|
||||
drawing_context.DrawRectangle(brush, pen, rectangle);
|
||||
drawing_context.Pop();
|
||||
}
|
||||
} else {
|
||||
drawing_context.DrawRectangle(brush, pen, rectangle);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,17 +2,12 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Markup;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace mbts.ui
|
||||
{
|
||||
namespace mbts.ui {
|
||||
[MarkupExtensionReturnType(typeof(Brush))]
|
||||
public class BrushExtension : MarkupExtension
|
||||
{
|
||||
class BrushExtension : MarkupExtension {
|
||||
public BrushExtension() : this(Colors.Black) { }
|
||||
public BrushExtension(Color Color) : this(Color, 1.0) { }
|
||||
public BrushExtension(Color Color, double Opacity) { this.Color = Color; this.Opacity = Opacity; }
|
||||
@@ -20,11 +15,14 @@ namespace mbts.ui
|
||||
[TypeConverter(typeof(ColorTypeConverter))]
|
||||
[ConstructorArgument("Color")]
|
||||
public Color Color { get; set; }
|
||||
|
||||
[ConstructorArgument("Opacity")]
|
||||
public double Opacity { get; set; }
|
||||
public override object ProvideValue(IServiceProvider serviceProvider) {
|
||||
return new SolidColorBrush(Color) { Opacity = Opacity };
|
||||
}
|
||||
|
||||
public bool Freeze { get; set; }
|
||||
|
||||
public override object ProvideValue(IServiceProvider serviceProvider) =>
|
||||
Freeze ? FrozenBrushCache.get_brush(Color, Opacity) : new SolidColorBrush(Color) { Opacity = Opacity };
|
||||
}
|
||||
|
||||
public class ColorTypeConverter : TypeConverter
|
||||
|
||||
@@ -11,26 +11,21 @@ using System.Windows.Media;
|
||||
namespace mbts.ui.extensions {
|
||||
public static class UIElementExtensions {
|
||||
public static bool ChildAtPoint<T>(this UIElement target, Point pt, out T result) where T : DependencyObject {
|
||||
if (target.InputHitTest(pt) is UIElement source) {
|
||||
result = source as T ?? FindAncestor<T>(source);
|
||||
return result != null;
|
||||
}
|
||||
result = default;
|
||||
return false;
|
||||
result = target.ChildAtPoint<T>(pt);
|
||||
return result != null;
|
||||
}
|
||||
|
||||
public static T ChildAtPoint<T>(this UIElement target, Point pt) where T : DependencyObject {
|
||||
var source = target.InputHitTest(pt) as UIElement;
|
||||
if (source == null) return null;
|
||||
if (source is T) return source as T;
|
||||
return FindAncestor<T>(source);
|
||||
}
|
||||
public static T ChildAtPoint<T>(this UIElement element, Point pt) where T : DependencyObject =>
|
||||
element.IsWithinBounds(pt) && element.InputHitTest(pt) is UIElement hit ?
|
||||
(hit == element ? null : hit as T ?? FindAncestor<T>(hit)) :
|
||||
null;
|
||||
|
||||
public static T FindAncestor<T>(this DependencyObject target) where T : DependencyObject {
|
||||
if (target == null) return null;
|
||||
if (target is T) return (T)target;
|
||||
return FindAncestor<T>(VisualTreeHelper.GetParent(target)) ?? FindAncestor<T>(LogicalTreeHelper.GetParent(target));
|
||||
}
|
||||
public static T FindAncestor<T>(this DependencyObject dependency_object) where T : DependencyObject =>
|
||||
dependency_object == null ? null :
|
||||
dependency_object as T ?? FindAncestor<T>(VisualTreeHelper.GetParent(dependency_object));
|
||||
|
||||
public static bool IsWithinBounds(this UIElement target, Point pt) =>
|
||||
pt.X >= 0 && pt.X < target.RenderSize.Width && pt.Y >= 0 && pt.Y < target.RenderSize.Height;
|
||||
|
||||
public static IEnumerable<DependencyObject> VisualAncestors(this DependencyObject dependency_object) {
|
||||
var parent = VisualTreeHelper.GetParent(dependency_object);
|
||||
@@ -77,6 +72,11 @@ namespace mbts.ui.extensions {
|
||||
return Disposable.Create(() => target.MouseUp -= callback);
|
||||
}
|
||||
|
||||
public static IDisposable SubscribeMouseLeftButtonDown(this UIElement target, MouseButtonEventHandler callback) {
|
||||
target.MouseLeftButtonDown += callback;
|
||||
return Disposable.Create(() => target.MouseLeftButtonDown -= callback);
|
||||
}
|
||||
|
||||
public static IDisposable SubscribeMouseLeftButtonUp(this UIElement target, MouseButtonEventHandler callback) {
|
||||
target.MouseLeftButtonUp += callback;
|
||||
return Disposable.Create(() => target.MouseLeftButtonUp -= callback);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using bts;
|
||||
using bts.collections;
|
||||
using bts.utils;
|
||||
using mbts.ui.utility;
|
||||
using mbts.utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@@ -19,6 +18,10 @@ using System.Windows.Media;
|
||||
|
||||
namespace mbts.ui {
|
||||
static class WPFExtensions {
|
||||
public static Color blend(this Color color) => default;
|
||||
public static (double x, double y) deconstruct(this Point point) =>
|
||||
(point.X, point.Y);
|
||||
|
||||
public static object get_template_child(this Control control, string name) {
|
||||
if (control == null) throw new ArgumentNullException(nameof(control));
|
||||
return control?.Template?.FindName(name, control);
|
||||
@@ -110,8 +113,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;
|
||||
var other_opac = other.A / 255;
|
||||
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,9 @@
|
||||
namespace mbts.ui {
|
||||
static class ResourceKeys {
|
||||
public const string
|
||||
Securities = nameof(Securities),
|
||||
Accounts = nameof(Accounts),
|
||||
Users = nameof(Users),
|
||||
Sides = nameof(Sides);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace mbts.ui {
|
||||
static class TemplateKeys {
|
||||
public const string
|
||||
SecurityKey = nameof(SecurityKey);
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,30 @@ namespace mbts.ui {
|
||||
MinimizeWindowExecutedHandler = WindowCallbackHelper.command_executed_handler(w => w.WindowState = WindowState.Minimized),
|
||||
MaximizeWindowExecutedHandler = WindowCallbackHelper.command_executed_handler(w => w.WindowState = w.WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized);
|
||||
|
||||
#region UnsafeWindow
|
||||
// UnsafeWindow is described as 'Unsafe' because the window may be on a different dispatcher than the DependencyObject target.
|
||||
public static readonly DependencyProperty UnsafeWindowProperty =
|
||||
DependencyProperty.RegisterAttached("UnsafeWindow", typeof(Window), typeof(WindowStyle), new PropertyMetadata(OnUnsafeWindowChanged));
|
||||
|
||||
static void OnUnsafeWindowChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
|
||||
if (d is UIElement el)
|
||||
el.RaiseEvent(new RoutedPropertyChangedEventArgs<Window>(e.OldValue as Window, e.NewValue as Window, UnsafeWindowChangedEvent));
|
||||
}
|
||||
|
||||
public static Window GetUnsafeWindow(DependencyObject d) => d.GetValue(UnsafeWindowProperty) as Window;
|
||||
public static void SetUnsafeWindow(DependencyObject d, Window value) => d.SetValue(UnsafeWindowProperty, value);
|
||||
#endregion
|
||||
|
||||
#region UnsafeWindowChanged (Event)
|
||||
internal static readonly RoutedEvent UnsafeWindowChangedEvent = EventManager.RegisterRoutedEvent("UnsafeWindowChanged", RoutingStrategy.Direct, typeof(RoutedPropertyChangedEventHandler<Window>), typeof(WindowStyle));
|
||||
|
||||
internal static void AddUnsafeWindowChangedHandler(UIElement el, RoutedPropertyChangedEventHandler<Window> handler) =>
|
||||
el.AddHandler(UnsafeWindowChangedEvent, handler);
|
||||
|
||||
internal static void RemoveUnsafeWindowChangedHandler(UIElement el, RoutedPropertyChangedEventHandler<Window> handler) =>
|
||||
el.RemoveHandler(UnsafeWindowChangedEvent, handler);
|
||||
#endregion
|
||||
|
||||
static class WindowCallbackHelper {
|
||||
public static ExecutedRoutedEventHandler safe_command_executed_handler(Action<Window> action, string action_name) =>
|
||||
(s, e) => _safe_execute_on_window(s, action, action_name);
|
||||
|
||||
@@ -109,6 +109,7 @@ namespace mbts.ui.shell
|
||||
this.Items.Remove(document);
|
||||
document.ViewModel.Closing -= TabClosing;
|
||||
document.ViewModel.host_id = string.Empty;
|
||||
document.ViewModel.owner = null;
|
||||
if (IsEmpty && !IsClosed) {
|
||||
this.Close();
|
||||
}
|
||||
@@ -117,6 +118,7 @@ namespace mbts.ui.shell
|
||||
// if is_loaded is true, will NOT set width/height
|
||||
public async Task add_document(TabDocument document, int index, bool is_loaded=false) {
|
||||
Log.assert(document.Parent == null);
|
||||
document.ViewModel.owner = this;
|
||||
// Manually copy over the initial size if this is a new Window
|
||||
if (_tabs.Items.Count == 0 && !is_loaded)
|
||||
await _size_to_content(document);
|
||||
|
||||
@@ -361,7 +361,6 @@
|
||||
<scm:SortDescription PropertyName="id"/>
|
||||
</CollectionViewSource.SortDescriptions>
|
||||
</CollectionViewSource>
|
||||
<SolidColorBrush x:Key="SeparatorFill" Color="Black"/>
|
||||
</Grid.Resources>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
|
||||
@@ -11,9 +11,11 @@ namespace mbts.ui.shell {
|
||||
public class ShellWindowVM : ViewModelBase, IDisposable {
|
||||
IShellWindow _model;
|
||||
UIElement _ui;
|
||||
ShellWindowUI document;
|
||||
IWindowManager _wm;
|
||||
Dispatcher _dispatcher;
|
||||
HwndContentHost _hwnd_host;
|
||||
FloatingWindow _owner;
|
||||
|
||||
public ShellWindowVM(IShellWindow model, IWindowManager wm, IShellThreadFactory threads, string host = "") {
|
||||
Log.assert(model != null);
|
||||
@@ -50,6 +52,15 @@ namespace mbts.ui.shell {
|
||||
|
||||
public string host_id { get; set; }
|
||||
public Dispatcher Dispatcher { get { return _dispatcher; } }
|
||||
public FloatingWindow owner {
|
||||
get => _owner;
|
||||
set {
|
||||
if (_owner != value) {
|
||||
_owner = value;
|
||||
_try_set_wpf_bus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string Title { get { return HasCustomTitle? TitleOverride : _model.Title; } }
|
||||
|
||||
@@ -125,7 +136,6 @@ namespace mbts.ui.shell {
|
||||
}
|
||||
|
||||
private UIElement create_contents() {
|
||||
ShellWindowUI document = null;
|
||||
string error_text = null;
|
||||
|
||||
_dispatcher.Invoke(() => {
|
||||
@@ -166,6 +176,7 @@ namespace mbts.ui.shell {
|
||||
_dispatcher.Invoke(() => {
|
||||
var interop_window = new WPFPanel(document.Content);
|
||||
retval = new System.Runtime.InteropServices.HandleRef(interop_window, interop_window.Handle);
|
||||
//WindowStyle.SetWPFBus(document.Content);
|
||||
}, DispatcherPriority.Send);
|
||||
_hwnd_host = new HwndContentHost(retval);
|
||||
InitialHeight = _hwnd_host.InitialHeight;
|
||||
@@ -206,8 +217,6 @@ namespace mbts.ui.shell {
|
||||
public bool IsShared { get { return _model is ISharedWindow; } }
|
||||
public bool HasTabAffinity { get { return _model is ITabAffineWindow; } }
|
||||
|
||||
|
||||
|
||||
public void Dispose() {
|
||||
_model.TitleChanged -= title_changed;
|
||||
_model.IconChanged -= icon_changed;
|
||||
@@ -228,5 +237,11 @@ namespace mbts.ui.shell {
|
||||
Disposable.dispose(ref _hwnd_host);
|
||||
}
|
||||
|
||||
void _try_set_wpf_bus() {
|
||||
if (_owner != null && document?.Content != null) {
|
||||
document.Content.Dispatcher.InvokeAsync(() =>
|
||||
WindowStyle.SetUnsafeWindow(document.Content, _owner));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,8 +20,8 @@ namespace mbts.spark {
|
||||
readonly IRaiseCanExecuteChangedCommand _add_criteria_command, _remove_criteria_command, _ok_button_command;
|
||||
|
||||
bool _allow_empty, _is_edit_mode;
|
||||
SecurityImmutable? _pending_sec;
|
||||
(SecurityCriteria criteria, SecurityImmutable? sec, SecurityMD? md)? _snapshot;
|
||||
uint? _pending_sec_key;
|
||||
(SecurityCriteria criteria, uint? sec, SecurityMD? md)? _snapshot;
|
||||
|
||||
public ContractPickerModel(IBus bus, bool edit_mode, SecurityCriteria existing_criteria, SecurityCriteria world_view_filter = null)
|
||||
: this(bus.subscribe(SecuritiesTag), edit_mode, existing_criteria, world_view_filter) { }
|
||||
@@ -209,9 +209,9 @@ namespace mbts.spark {
|
||||
_update_security_types();
|
||||
_update_symbols();
|
||||
_update_securities();
|
||||
if (_pending_sec.HasValue && changed_securities.ContainsKey(_pending_sec.Value.Id)) {
|
||||
select_security_request?.Invoke(_pending_sec.Value);
|
||||
_pending_sec = null;
|
||||
if (_pending_sec_key.HasValue && changed_securities.TryGetValue(_pending_sec_key.Value, out var smd)) {
|
||||
select_security_request?.Invoke(smd.security);
|
||||
_pending_sec_key = null;
|
||||
}
|
||||
}
|
||||
internal void criteria_selection_changed(IEnumerable<SecurityCriteriaClause> selected_clauses) {
|
||||
@@ -229,6 +229,8 @@ namespace mbts.spark {
|
||||
security_criteria_clause_changed?.Invoke(this, e);
|
||||
}
|
||||
|
||||
public bool is_editing => _snapshot.HasValue;
|
||||
|
||||
public ObservableCollection<Venue> all_exchanges { get; } = new ObservableCollection<Venue>();
|
||||
|
||||
public List<Venue> selected_exchanges = new List<Venue>();
|
||||
@@ -298,7 +300,22 @@ namespace mbts.spark {
|
||||
if (all_securities.ContainsKey(value.Value.security.Id)) {
|
||||
select_security_request?.Invoke(value.Value.security);
|
||||
} else {
|
||||
_pending_sec = value.Value.security;
|
||||
_pending_sec_key = value.Value.security.Id;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public uint? selected_security_key {
|
||||
get => selected_securities.Count == 1 && all_securities.TryGetValue(selected_securities[0], out var smd) ? smd.security.Id : (uint?)null;
|
||||
set {
|
||||
if (value != selected_security_key) {
|
||||
if (value.HasValue) {
|
||||
if (all_securities.TryGetValue(value.Value, out var smd)) {
|
||||
select_security_request?.Invoke(smd.security);
|
||||
} else {
|
||||
_pending_sec_key = value.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -313,7 +330,7 @@ namespace mbts.spark {
|
||||
if (all_securities.ContainsKey(value.Value.Id)) {
|
||||
select_security_request?.Invoke(value.Value);
|
||||
} else {
|
||||
_pending_sec = value.Value;
|
||||
_pending_sec_key = value.Value.Id;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -336,18 +353,19 @@ namespace mbts.spark {
|
||||
}
|
||||
|
||||
#region IEditableObject
|
||||
public void BeginEdit() => _snapshot = (result_criteria, _pending_sec ?? selected_security, selected_security_md);
|
||||
public void BeginEdit() => _snapshot = (result_criteria, _pending_sec_key ?? selected_security_key, selected_security_md);
|
||||
public void EndEdit() => _snapshot = null;
|
||||
public void CancelEdit() {
|
||||
if (_snapshot.HasValue) {
|
||||
var (criteria, sec, md) = _snapshot.Value;
|
||||
security_criteria_clauses.SetItems(criteria.clauses);
|
||||
selected_security = sec;
|
||||
selected_security_key = sec;
|
||||
/// A <see cref="_pending_sec"/> may have been used to create the snapshot when <see cref="BeginEdit"/> was called (this happens when securities are loaded lazily).
|
||||
/// If this is the case, pending sec was assigned to the <see cref="_snapshot"/> security;
|
||||
/// check if the security is now present in <see cref="all_securities"/>, if so, use that value instead of the null.
|
||||
selected_security_md = sec.HasValue && !md.HasValue && all_securities.TryGetValue(sec.Value.Id, out var md_match) ? md_match : md;
|
||||
selected_security_md = sec.HasValue && !md.HasValue && all_securities.TryGetValue(sec.Value, out var md_match) ? md_match : md;
|
||||
_snapshot = null;
|
||||
RaisePropertyChanged(nameof(selected_security));
|
||||
_add_criteria_command.RaiseCanExecuteChanged();
|
||||
_remove_criteria_command.RaiseCanExecuteChanged();
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using mbts.spark;
|
||||
using mbts.ui.converters;
|
||||
using mbts.ui.shell;
|
||||
using mbts.ui.utility;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
@@ -16,8 +17,11 @@ namespace mbts.ui.controls {
|
||||
partial class ContractPickerPopup : PopupControl {
|
||||
public static readonly EventHandler
|
||||
FlushSelectedCriteriaHandler = (s, e) => (s as ContractPickerPopup)?.GetBindingExpression(SelectedCriteriaProperty)?.UpdateSource(),
|
||||
FlushSelectedSecurityKeyHandler = (s, e) => (s as ContractPickerPopup)?.GetBindingExpression(SelectedSecurityKeyProperty)?.UpdateSource(),
|
||||
FlushSelectedSecurityHandler = (s, e) => (s as ContractPickerPopup)?.GetBindingExpression(SelectedSecurityProperty)?.UpdateSource();
|
||||
|
||||
readonly PropertyUpdateGate _selection_sync_gate = new PropertyUpdateGate();
|
||||
|
||||
IReadOnlyDictionary<uint, SecurityMD> _cached_security_map = DictionaryHelper.empty<uint, SecurityMD>();
|
||||
ContractPickerPopupController _controller;
|
||||
bool _allow_empty, _is_lazy;
|
||||
@@ -97,12 +101,55 @@ namespace mbts.ui.controls {
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region SelectedSecurityKey
|
||||
public static readonly DependencyProperty SelectedSecurityKeyProperty =
|
||||
WPFHelper.create_dp<uint?, ContractPickerPopup>(
|
||||
nameof(SelectedSecurityKey),
|
||||
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
|
||||
(o, v) => {
|
||||
o.Controller.model.selected_security_key = v;
|
||||
o._selection_sync_gate.try_update(() => o._sync_selection_props(o.SelectedSecurityMD, o.SelectedSecurity, v, v));
|
||||
});
|
||||
//(o, base_value) => base_value is uint u && u != 0 ? u : (uint?)null);
|
||||
|
||||
public uint? SelectedSecurityKey {
|
||||
get => GetValue(SelectedSecurityKeyProperty) as uint?;
|
||||
set => SetValue(SelectedSecurityKeyProperty, value);
|
||||
}
|
||||
#endregion
|
||||
|
||||
enum SecurityProp {
|
||||
Security,
|
||||
SecurityMD,
|
||||
ID
|
||||
}
|
||||
|
||||
void _sync_selection_props(SecurityMD? curr_sec_md, SecurityImmutable? curr_sec, uint? curr_sec_key, uint? target_sec_key) {
|
||||
if (curr_sec_key != target_sec_key) {
|
||||
SetCurrentValue(SelectedSecurityKeyProperty, target_sec_key);
|
||||
}
|
||||
bool sec_md_dirty = target_sec_key != (curr_sec_md?.security)?.Id;
|
||||
bool sec_dirty = target_sec_key != curr_sec?.Id;
|
||||
if (sec_md_dirty || sec_dirty) {
|
||||
curr_sec_md = target_sec_key.HasValue && _cached_security_map.TryGetValue(target_sec_key.Value, out var smd) ? smd : (SecurityMD?)null;
|
||||
if (sec_md_dirty)
|
||||
SetCurrentValue(SelectedSecurityMDProperty, curr_sec_md);
|
||||
if (sec_dirty)
|
||||
SetCurrentValue(SelectedSecurityProperty, curr_sec_md?.security);
|
||||
}
|
||||
if (!IsEditMode)
|
||||
SetCurrentValue(SelectedCriteriaProperty, target_sec_key.HasValue ? SecurityCriteria.from_keys(new[] { target_sec_key.Value }) : null);
|
||||
}
|
||||
|
||||
#region SelectedSecurity
|
||||
public static readonly DependencyProperty SelectedSecurityProperty =
|
||||
WPFHelper.create_dp<SecurityImmutable?, ContractPickerPopup>(
|
||||
nameof(SelectedSecurity),
|
||||
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
|
||||
(o, v) => o.Controller.model.selected_security = v);
|
||||
(o, v) => {
|
||||
o.Controller.model.selected_security = v;
|
||||
o._selection_sync_gate.try_update(() => o._sync_selection_props(o.SelectedSecurityMD, v, o.SelectedSecurityKey, v?.Id));
|
||||
});
|
||||
|
||||
public SecurityImmutable? SelectedSecurity {
|
||||
get => GetValue(SelectedCriteriaProperty) as SecurityImmutable?;
|
||||
@@ -115,7 +162,10 @@ namespace mbts.ui.controls {
|
||||
WPFHelper.create_dp<SecurityMD?, ContractPickerPopup>(
|
||||
nameof(SelectedSecurityMD),
|
||||
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
|
||||
(o, v) => o.Controller.model.selected_security_md = v);
|
||||
(o, v) => {
|
||||
o.Controller.model.selected_security_md = v;
|
||||
o._selection_sync_gate.try_update(() => o._sync_selection_props(v, o.SelectedSecurity, o.SelectedSecurityKey, (v?.security)?.Id));
|
||||
});
|
||||
|
||||
public SecurityMD? SelectedSecurityMD {
|
||||
get => GetValue(SelectedCriteriaProperty) as SecurityMD?;
|
||||
@@ -248,12 +298,10 @@ namespace mbts.ui.controls {
|
||||
}
|
||||
void on_cancel() {
|
||||
model.CancelEdit();
|
||||
propagate_to_owner();
|
||||
owner.CancelClick?.Invoke(owner, EventArgs.Empty);
|
||||
}
|
||||
void propagate_to_owner() {
|
||||
owner.set_current_if_changed(SelectedSecurityMDProperty, owner.Controller.model.selected_security_md);
|
||||
owner.set_current_if_changed(SelectedSecurityProperty, owner.Controller.model.selected_security);
|
||||
owner.set_current_if_changed(SelectedSecurityKeyProperty, owner.Controller.model.selected_security_key);
|
||||
owner.set_current_if_changed(SelectedCriteriaProperty, owner.Controller.model.result_criteria);
|
||||
}
|
||||
}
|
||||
@@ -262,4 +310,4 @@ namespace mbts.ui.controls {
|
||||
public SecurityCriteria selected_criteria => _selected_criteria.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,5 +11,5 @@ namespace mbts.spark {
|
||||
}
|
||||
}
|
||||
|
||||
class PnLRowView : DIYGrid.HighPerformanceRow<PnLSummaryRow, PAndLView.Columns> { }
|
||||
class PnLRowView : DIYGrid.HighPerformanceRow<PnLSummaryRow> { }
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
using bts.collections;
|
||||
using bts.shell.pnl;
|
||||
using mbts.ui.controls;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
@@ -20,6 +18,7 @@ namespace mbts.ui.spark {
|
||||
IList<IDisposable> _grid_hooks = new List<IDisposable>();
|
||||
IDictionary<DataGridColumn, IDisposable> _column_hooks = new Dictionary<DataGridColumn, IDisposable>();
|
||||
ScrollViewer _scroll_viewer;
|
||||
ScrollContentPresenter _scroll_presenter_backing;
|
||||
IDictionary<object, IDictionary<object, IEnumerable<CellAnimation>>> _row_animation_map = new Dictionary<object, IDictionary<object, IEnumerable<CellAnimation>>>();
|
||||
IDictionary<TColKey, DIYGrid.ColumnGeometry> _column_layout = new Dictionary<TColKey, DIYGrid.ColumnGeometry>();
|
||||
|
||||
@@ -54,14 +53,20 @@ namespace mbts.ui.spark {
|
||||
}
|
||||
}
|
||||
|
||||
ScrollContentPresenter _scroll_presenter =>
|
||||
_scroll_presenter_backing ?? (_scroll_presenter_backing = _scroll_viewer?.descendants_of_type<ScrollContentPresenter>().FirstOrDefault());
|
||||
|
||||
public void Dispose() {
|
||||
_dispose_hooks();
|
||||
RemoveRowAnimationChangedHandler(_grid, _grid_RowAnimationChanged);
|
||||
}
|
||||
|
||||
protected override void OnRender(DrawingContext drawingContext) {
|
||||
if (_row_animation_map.Count == 0)
|
||||
var sp = _scroll_presenter;
|
||||
if (sp != null && _row_animation_map.Count == 0)
|
||||
return;
|
||||
var sp_bounds = new Rect(sp.TranslatePoint(new Point(0, 0), this), sp.RenderSize);
|
||||
drawingContext.PushClip(new RectangleGeometry(sp_bounds));
|
||||
foreach (var row_kv in _row_animation_map) {
|
||||
if (_rows_presenter.ItemContainerGenerator.ContainerFromItem(row_kv.Key) is FrameworkElement row) {
|
||||
if (!row.IsVisible)
|
||||
@@ -69,6 +74,7 @@ namespace mbts.ui.spark {
|
||||
animate_row(drawingContext, row_kv.Key, row_kv.Value, row);
|
||||
}
|
||||
}
|
||||
drawingContext.Pop();
|
||||
}
|
||||
|
||||
protected void animate_row(DrawingContext drawing_context, object row_vm, IDictionary<object, IEnumerable<CellAnimation>> cell_animations, FrameworkElement row_view) {
|
||||
|
||||
@@ -71,5 +71,17 @@ namespace mbts.spark {
|
||||
work();
|
||||
}
|
||||
}
|
||||
|
||||
public static IDisposable DoWhile(string name, TimeSpan interval, Func<bool> work, DispatcherPriority priority, double excessive_cycle_ratio = default_excessive_cycle_ratio) {
|
||||
IDisposable loop = null;
|
||||
loop = Start(name, interval, priority, impl, excessive_cycle_ratio);
|
||||
return Disposable.Create(dispose);
|
||||
void impl() {
|
||||
if (!work())
|
||||
Disposable.dispose(ref loop);
|
||||
}
|
||||
void dispose() =>
|
||||
Disposable.dispose(ref loop);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loaded 100 of 137 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user