Compare commits

...
Author SHA1 Message Date
John Colvin 61a4c0ae3e checkpoint before flying to chapel hill (pnl settings profiles) 2019-08-21 11:38:21 -05:00
John Colvin 31a28727f7 SettingsProfileService needs 'get_next_id' delegate 2019-08-18 23:27:30 -05:00
John Colvin 11033d68cd ColorPickerPopup disabled animation 2019-08-18 17:45:21 -05:00
John Colvin e228db7ffa organize ComboBox color resources in Colors.xaml 2019-08-18 17:45:21 -05:00
John Colvin b249aa019b set ColorPickerWithOpacitySlider label column MinWidth, so slider's width doesn't react to text width changes 2019-08-18 17:45:21 -05:00
John Colvin b8cd9651f6 guard against potential null ref 2019-08-18 17:45:21 -05:00
John Colvin 7349c33c7c impl new PNL settings dialog 2019-08-18 17:45:21 -05:00
John Colvin f9f1c31037 added DataTypeTemplateSelector 2019-08-18 17:45:21 -05:00
John Colvin d507ea6d16 KeyedTemplateSelector 'TryMatchType' fallback behavior 2019-08-18 17:45:20 -05:00
John Colvin 7bdd60e744 ColorPickerPopupVM initailizes palettes lazily 2019-08-18 17:45:20 -05:00
John Colvin 9f2c701ff9 added ColorPickerPopup.bind_opacity_to_alpha flag and behavior impl 2019-08-18 17:45:20 -05:00
John Colvin fb185a590f PopupControl Width TemplateBinding 2019-08-18 17:45:20 -05:00
John Colvin bfa0046d2d config to toggle animations, and pause/resume buttons in new pnl window 2019-08-18 17:45:20 -05:00
John Colvin 0160c2f33b add comment explaining why ColumnBase.create() should not access dp store 2019-08-18 17:45:20 -05:00
John Colvin 1d01c0af35 SP-47 Revamp PnL form 2019-08-18 17:45:20 -05:00
John Colvin 6ce35597f7 made ExpressionSnakeToTitleCaseConverter (for pnl rules text) 2019-08-18 17:45:20 -05:00
John Colvin cab86fa922 CoreExpressionParser casts decimals to doubles (for pnl rules, wrt PNLNormalizedData) 2019-08-18 17:45:14 -05:00
John Colvin 26e58d2e5c added CoreExpressionEvaluator.try_create/validate methods (for pnl rules, and potential use down the road) 2019-08-18 14:52:12 -05:00
John Colvin dc86b62d5f ToArray() extension for IList that accepts selector param 2019-08-14 00:24:36 -05:00
John Colvin 1518655cb7 added static 'editable_columns' prop to OrderBookColumns, for easier miniob creation 2019-08-14 00:24:36 -05:00
John Colvin b7fe59e04a added OrderBookVM constructor to specify iobservable orders_source arg 2019-08-14 00:24:36 -05:00
John Colvin 489892a8e7 added OrderDataHelper.PriceComparers for double and decimal 2019-08-14 00:24:36 -05:00
John Colvin ac811be188 added error margin/epsilon comparer to MathUtils 2019-08-14 00:24:36 -05:00
John Colvin c6ac0d75a3 added 'RealPrice' dp to PriceEntryCommitControl, which updates the 'prospective price' if the control doesn't have KeyboardFocus 2019-08-14 00:24:35 -05:00
John Colvin f199a3795e (Simple)GridColumnSettingsVM were not properly clearing font/color weight overrides 2019-08-14 00:24:35 -05:00
John Colvin 639fe9f6f5 ColumnGeometry.visible_width should not return negative value 2019-08-14 00:24:35 -05:00
John Colvin 37cf61bfc7 hook up DIYGrid.ColumnBase 'is_visible' prop 2019-08-14 00:24:35 -05:00
John Colvin cb477552eb correct ScrollViewerCorrector 2019-08-14 00:24:35 -05:00
John Colvin d4853c777d add ScrollViewer to DragDropListBox ControlTemplate 2019-08-14 00:24:35 -05:00
John Colvin c58e892b0a added TriggerAnimation facilities for DIYGrid 2019-08-14 00:24:35 -05:00
John Colvin d780777c7c DIYGridAnimationAdorner supports GroupItems 2019-08-14 00:24:35 -05:00
John Colvin c531fb4ade refactor MappedDictionary.create, to avoid ambiguity in untyped lambdas 2019-08-14 00:24:35 -05:00
John Colvin 86695a8719 add background_override prop to GridColumnSettings 2019-08-14 00:24:35 -05:00
John Colvin d6789e8e7a add argb prop to NullableColorProto 2019-08-14 00:24:35 -05:00
John Colvin e68b626ec4 added 'argb_color' uint extension (like rgb_color, but includes alpha byte) 2019-08-14 00:24:35 -05:00
John Colvin 915e0d38c3 add pause icon 2019-08-14 00:24:35 -05:00
John Colvin 553cdcb80f Price formatting delegates 2019-08-14 00:24:35 -05:00
John Colvin bb70e67f36 DIYGrid '_header_frozen' needs to be hidden when no frozen cols (still 1.6 pixels wide) 2019-08-14 00:24:35 -05:00
John Colvin 577d2fa742 normalize namespaces in DIYGrid.xaml 2019-08-14 00:24:35 -05:00
John Colvin b32c836f34 DIYGrid ColumnHeaders don't hide HeaderContent behind filter/sort icons 2019-08-14 00:24:35 -05:00
John Colvin eb3c64a5be refactor ObservableAutoWrapper.connect/connect_item_hooks 2019-08-14 00:24:34 -05:00
John Colvin 57f607ef6e added MutableWrapper impl to MappedDictionary 2019-08-14 00:24:34 -05:00
John Colvin 097a048ba8 added 'wrap_selector()' util to CollectionHelper (the wrapper is an ICollection impl) 2019-08-14 00:24:34 -05:00
John Colvin 6b37c87fcc added 'FromEventThrottle()' to ObservableExtensions 2019-08-14 00:24:34 -05:00
John Colvin 32b9c0db10 added 'ReverseWrapper()' to ListExtensions 2019-08-14 00:24:34 -05:00
John Colvin df6b065e43 make ExpanderToggleButton customizable by overriding default togglebutton style key 2019-08-14 00:24:34 -05:00
John Colvin 981b429a4e TableUpdate 'Operate' extension 2019-08-14 00:24:34 -05:00
John Colvin d87de14dd4 TableUpdate 'BufferTable' extension 2019-08-14 00:24:34 -05:00
John Colvin 7d2ccc7ae1 TableUpdate 'GroupTable' extension 2019-08-14 00:24:34 -05:00
John Colvin 2db21b6927 made System.Linq style 'Cast/Select' extensions for ObservableCollection 2019-08-14 00:24:34 -05:00
John Colvin 6b52d225ba WPFHelper.measure_visual_content should never return infinity for width/height 2019-08-14 00:24:34 -05:00
John Colvin 6c5d084e20 HelpButton shouldn't overwrite the ToolTip ContentTemplate (bind style to dynamicresource to support override without breaking existing use) 2019-08-14 00:24:34 -05:00
John Colvin dacf2034a1 impl TableUpdate FullJoin 2019-08-14 00:24:34 -05:00
John Colvin 60593cadc1 added some TableUpdate diagnostic utils 2019-08-14 00:24:34 -05:00
John Colvin 05d7d9357d NumericSignBrushConverter uses new UniversalNumericComparer to support cross type comparisons 2019-08-14 00:24:34 -05:00
John Colvin 5267e59f61 move NumericSignBrushConverter.cs into Converters\Numeric dir 2019-08-14 00:24:34 -05:00
John Colvin a2dc1ebbf7 fix TextCell (remake) 2019-08-14 00:24:34 -05:00
John Colvin fcff996025 redo HyperlinkCell (alignment/measurement doesn't work right outside of the HighPerformanceRow) 2019-08-14 00:24:34 -05:00
John Colvin 084e99325d add PopupAdorner.StayOpen dp 2019-08-14 00:24:34 -05:00
John Colvin ae771f6927 fix typo in PopupAdorner: 'Botton' -> 'Bottom' 2019-08-14 00:24:34 -05:00
John Colvin 20e10f49e4 adjust mini ob 'Select All' checkbox alignment after stretching DIYGrid ColumnHeader content 2019-08-14 00:24:33 -05:00
John Colvin 3224e0bce7 abstracted out ladder's thread id logic into ShellWindowThreadIDProvider (max thread count = Environment.ProcessorCount for each window type) 2019-08-14 00:24:33 -05:00
John Colvin 623d6e3136 made 'property change detector' that uses prop getter delegate rather than PropertyInfo.GetValue for better performance 2019-08-14 00:24:33 -05:00
John Colvin 682195720e ObjectSnapshot.property_table initializes observer on Subscribe 2019-08-14 00:24:33 -05:00
John Colvin 5c190ebc94 DIYGrid features for pnl: 1) select GroupItems, 2) SyncWithColumnGeometry behavior allows Grid(panels) to mirror the DIYGrid's column layout 2019-08-14 00:24:33 -05:00
John Colvin 74ac34bb88 rename 'GroupItem' -> 'HierarchicalGroupItem' to avoid name conflict with System.Windows.Controls.GroupItem 2019-08-13 19:50:55 -05:00
John Colvin 66cb884bee vertically align min/max/close buttons in BTSWindowTitleTemplate 2019-08-13 19:50:55 -05:00
John Colvin fcf573b32d remove unused 'hook_filters' fns 2019-08-13 19:50:55 -05:00
John Colvin 03788f04d6 IntMinMax (filter) protos impl IEquatable 2019-08-13 19:50:55 -05:00
John Colvin 9f6f51938d added some common DiscreteValuesFilterVM factory fns 2019-08-13 19:50:55 -05:00
John Colvin 7a0e9dc098 fix vertical alignments in GridSettingsView 2019-08-13 19:50:54 -05:00
138 changed files with 13744 additions and 3249 deletions

No files matched your search

+5
View File
@@ -40,9 +40,11 @@ namespace mbts.ui {
EditDocumentAltGeometryKey = nameof(EditDocumentAltGeometryKey),
EditDocumentGeometryKey = nameof(EditDocumentGeometryKey),
GearGeometryKey = nameof(GearGeometryKey),
KeyGeometryKey = nameof(KeyGeometryKey),
LetterAKey = nameof(LetterAKey),
LetterDKey = nameof(LetterDKey),
OrdersGeometryKey = nameof(OrdersGeometryKey),
PauseGeometryKey = nameof(PauseGeometryKey),
PaymentGeometryKey = nameof(PaymentGeometryKey),
PencilGeometryKey = nameof(PencilGeometryKey),
PlayGeometryKey = nameof(PlayGeometryKey),
@@ -169,9 +171,12 @@ namespace mbts.ui {
public static ImageSource FilterError { get { return load_resource_and_freeze("FilterError"); } }
public static ImageSource Gear => create_icon(GearGeometryKey);
public static ImageSource Import => load_resource_and_freeze("Import");
public static ImageSource Key => create_icon(KeyGeometryKey);
public static ImageSource Knight { get { return load_resource_and_freeze("Knight"); } }
public static ImageSource Pause => create_icon(PauseGeometryKey);
public static ImageSource Payment => create_icon(PaymentGeometryKey);
public static ImageSource Pencil => create_icon(PencilGeometryKey);
public static ImageSource Play => create_icon(PlayGeometryKey);
public static ImageSource Prohibited => create_icon(ProhibitedGeometryKey);
public static ImageSource RubberStamp => create_icon(RubberStampGeometryKey);
public static ImageSource RedoCircular => create_icon(UndoCircularGeometryKey, Brushes.Black, null, new ScaleTransform(-1, 1));
@@ -274,10 +274,18 @@
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.KeyGeometryKey}">
M22.399981,2.4000006C22.099982,2.4000006 21.899982,2.5 21.599982,2.6999998 21.199983,3.1000004 21.199983,3.8000002 21.599982,4.2000003L26.199979,8.9000001C26.599978,9.3000002 27.299977,9.3000002 27.699977,8.9000001 28.099977,8.5000002 28.099977,7.8000002 27.699977,7.4000006L23.099981,2.6999998C22.899981,2.5,22.699981,2.4000006,22.399981,2.4000006z M21.499982,0C22.299982,0,23.199981,0.30000019,23.79998,0.90000057L29.299976,6.4000001C30.599975,7.7000003,30.599975,9.7000003,29.299976,11L22.599981,17.7C21.299983,19,19.299984,19,17.999985,17.7L17.299985,17 12.699989,21.6 10.399991,20.900001 10.899991,23.400001 8.4999928,22.8 8.9999924,25.5 6.2999945,25.1 6.7999945,27.6 4.2999964,27 4.7999954,29.6 2.4999971,32 0,26.6 13.499989,13.1 16.999986,16.6 12.699989,12.3C11.399991,11,11.399991,9.0000002,12.699989,7.7000003L19.399984,1C19.899983,0.30000019,20.699983,0,21.499982,0z
</sys:String>
<sys:String x:Key="{x:Static mbts:Icons.OrdersGeometryKey}">
M50.14375,33.389126C50.416863,33.394688,50.653496,33.587773,50.705998,33.864617L50.015995,45.114388C49.810295,46.291462,49.130394,45.097489,49.130394,45.097489L47.65659,42.640538C42.747776,49.635197 34.633152,54.221001 25.433924,54.221001 14.884193,54.221001 5.7437552,48.204125 1.2449617,39.419205 0.80087067,38.552021 0.21358696,37.705738 0.052086769,36.736958 -0.49735315,33.490925 3.4507786,32.901036 4.8049124,34.432404 5.3674143,35.067692 6.034036,36.16407 6.5079177,37.026051 10.106778,43.799416 17.2321,48.413819 25.433924,48.413819 32.656544,48.413819 39.026364,44.826695 42.903977,39.351506L40.229367,38.854115C40.229367,38.854115,38.677464,38.647118,39.742466,37.996033L50.024895,33.398526C50.064972,33.391365,50.104733,33.388333,50.14375,33.389126z M23.030291,9.0310004L27.889918,9.0310004 27.889918,13.028299C30.539786,13.205498,32.324932,13.958096,33.464161,14.528294L33.964176,14.775793 32.417435,20.038878 31.706417,19.68068C30.579987,19.11958 28.839241,18.244583 26.149073,18.244583 24.006916,18.244583 22.723183,19.122181 22.723183,20.590876 22.723183,21.894272 23.943314,22.804369 27.551308,24.187165 32.706444,26.124659 34.911,28.702853 34.911,32.817341 34.911,36.892929 32.090527,40.05172 27.673611,40.989117L27.673611,45.540001 22.767485,45.540001 22.767485,41.330414C20.252919,41.155815,17.695551,40.424118,16.101811,39.404721L15.683999,39.137619 17.221739,33.731438 17.969059,34.180634C19.088988,34.851235 21.402748,35.97873 24.340325,35.97873 26.734886,35.97873 28.276728,34.921635 28.276728,33.291336 28.276728,31.986641 27.547409,30.853846 23.986016,29.57255 20.083815,28.144253 16.095511,26.04406 16.095511,21.239174 16.095511,17.280985 18.737379,14.291395 23.030291,13.329098z M25.268765,0C35.819447,2.7347414E-07 44.961449,6.0155495 49.458798,14.804573 49.902695,15.669173 50.490094,16.516972 50.651287,17.48427 51.198281,20.734265 47.255726,21.318962 45.901341,19.789065 45.336346,19.153567 44.669552,18.051968 44.19566,17.19517 40.5957,10.421881 33.473275,5.8072398 25.268765,5.8072393 18.047545,5.8072398 11.678013,9.3918336 7.8003562,14.867073L10.474627,15.367072C10.474627,15.367072,12.026909,15.574172,10.962921,16.223872L0.67933281,20.826762C0.36037083,20.880062,0.057029126,20.669165,0.00099972012,20.354063L0.69093342,9.1118043C0.89552094,7.9296158,1.5738537,9.124924,1.5738539,9.124924L3.0452772,11.583379C7.9566038,4.584462,16.073666,2.7347414E-07,25.268765,0z
</sys:String>
<sys:String x:Key="{x:Static mbts:Icons.PauseGeometryKey}">
M17.94,0L29.150001,0 29.150001,32 17.94,32z M0,0L11.210002,0 11.210002,32 0,32z
</sys:String>
<sys:String x:Key="{x:Static mbts:Icons.PaymentGeometryKey}">
M28.000001,6.0000026L28.000001,15 6.0010006,15 6.0010006,17.000003 30.001,17.000003 30.001,6.0000026z M14.231,2.9540056L14.979999,2.9540056 14.979999,3.987005C15.704998,4.009005,16.200997,4.178005,16.550998,4.3600049L16.248998,5.0620046C15.994998,4.9340047 15.499998,4.7010047 14.725999,4.7010047 13.796,4.7010047 13.445001,5.1900046 13.445001,5.6160046 13.445001,6.1710043 13.893,6.4470041 14.943999,6.8310037 16.187998,7.2780037 16.816998,7.8320036 16.816998,8.7800031 16.816998,9.6210027 16.151998,10.409003 14.907999,10.591002L14.907999,11.698002 14.146,11.698002 14.146,10.622003C13.445001,10.611002,12.720001,10.420002,12.297002,10.154003L12.587001,9.4400029C13.022001,9.696003 13.651,9.8980026 14.328,9.8980026 15.185999,9.8980026 15.765998,9.4620028 15.765998,8.8540034 15.765998,8.2680035 15.293999,7.9060035 14.4,7.5870035 13.167001,7.1610038 12.406001,6.6710041 12.406001,5.7440041 12.406001,4.8600047 13.119,4.189005 14.231,4.0290052z M2,2.0000002L2,13 26.000001,13 26.000001,2.0000002z M0,0L28.000001,0 28.000001,4.0000031 32.001,4.0000031 32.001,19.000003 4.0010006,19.000003 4.0010006,15 0,15z
</sys:String>
@@ -21,7 +21,7 @@ namespace mbts.ui.spark {
class AlgoParamTemplateSelector : DataTemplateSelector {
public static readonly Type generic_algo_param_itype = typeof(IAlgoParamVM<>);
KeyedCollection<TemplateSelectorKeyValue, string> _keyed_templates = new KeyedCollection<TemplateSelectorKeyValue, string>(tsk => tsk.Key);
KeyedCollection<TemplateSelectorKeyValue, string> _keyed_templates = new KeyedCollection<TemplateSelectorKeyValue, string>(tsk => tsk.Key.ToString());
public DataTemplate BoolTemplate { get; set; }
public DataTemplate NullableBoolTemplate { get; set; }
@@ -15,7 +15,6 @@ using System.IO;
using System.Linq;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using LadderSettings = mbts.spark.LadderSettings;
@@ -30,13 +29,14 @@ namespace bts.shell {
readonly MBOService _mbo;
readonly UserSettingsHub _settings_hub;
readonly OrdersModel _orders_model;
readonly IShellWindowThreadIDProvider _thread_id_provider;
IReadOnlyDictionary<uint, SecurityMD> _securities = new Dictionary<uint, SecurityMD>();
public LadderWindowFactory(
IShellModule owner,
IBus bus,
IWindowManager wm,
UserSettingsHub settings_hub,
IShellModule owner,
IBus bus,
IWindowManager wm,
UserSettingsHub settings_hub,
OrdersModel orders_model,
MBOService mbo)
@@ -46,6 +46,7 @@ namespace bts.shell {
_settings_hub = settings_hub;
_orders_model = orders_model;
_ladder_conf = new LadderConfig(Configuration.get<bts.config.LadderKnobs>(), Configuration.get<bts.config.MultiThreadedWindows>().ladder);
_thread_id_provider = ShellWindowThreadIDFactory.create("Ladder", _ladder_conf.ladders_on_threads);
_mbo = mbo;
_dispose = Disposable.Create(
bus.subscribe(SparkTags.SecuritiesTag).Subscribe(s => _securities = s),
@@ -88,10 +89,8 @@ namespace bts.shell {
return new LadderSaveState { security_key = vm.security.Id, settings = s, settings_id = id };
}
int _ladder_count = 0;
IShellWindow create_ladder_window(SecurityMD md, LadderSettings last_value, uint settings_id = 0) {
IDisposable active_settings_dialog = null;
var thread_id = Interlocked.Increment(ref _ladder_count) % Environment.ProcessorCount;
// on_shell_window_vm should behave like an event (do not set to null)
Action<ShellWindowVM> on_shell_window_vm = _ => { };
ShellWindowVM shell_window_vm = null;
@@ -104,7 +103,8 @@ namespace bts.shell {
us => ImmutableDictionary<uint, SettingsProfile<LadderSettings>>.Empty.AddRange(us.ladder_profiles.ToDictionary(
lp => lp.id,
lp => new SettingsProfile<LadderSettings>(lp.id, lp.name, LadderSettings.sanitize(lp.value), lp.security_criteria))),
(us, lps) => us.with(ladder_profiles: lps.Values.Select(lp => new LadderSettingsProfile(lp.id, lp.name, lp.value, lp.security_criteria)).ToArray()));
(us, lps) => us.with(ladder_profiles: lps.Values.Select(lp => new LadderSettingsProfile(lp.id, lp.name, lp.value, lp.security_criteria)).ToArray()),
us => us.next_ladder_profile_id);
last_value?.sanitize();
/// When settings are adjusted outside of the settings dialog, the vm should raise the <see cref="LadderVM.settings_invalidated"/> event.
/// In response to that event, 'needs_resync' is set to true. This causes 'last_value' to be refreshed when the next caller requests the latest settings
@@ -263,12 +263,11 @@ namespace bts.shell {
Log.Ui.warn($"LadderSettingsDialog error {action_desc}: {error}.");
}
});
string thread = _ladder_conf.ladders_on_threads ? $"Ladder{thread_id:00}" : "";
return shell_window = ShellWindow.create(
module, md.security.ToString(), _window_type, view,
(v, s) => save(_get_state(((LadderView)v).vm, settings_id), s),
icon: Icons.Ladder,
thread_id: thread,
thread_id: _thread_id_provider.next_id(),
vm => {
shell_window_vm = vm;
title_source.connect();
@@ -313,4 +312,4 @@ namespace bts.shell {
[ProtoMember(3)]
public uint settings_id;
}
}
}
@@ -136,7 +136,7 @@ namespace mbts.spark {
foreach (var c in _observable_components)
c.PropertyChanged -= on_prop_changed;
}
AccountTuple? account_factory(KeyValuePair<uint, string> kv) => new AccountTuple(kv.Key, kv.Value);
AccountTuple? account_factory(uint key, string name) => new AccountTuple(key, name);
AlgoDefinitionVM algo_factory(AlgoData data) => AlgoDefinitionVM.create(_model, data);
}
@@ -79,8 +79,8 @@ namespace bts.shell.manual_fills {
public ManualFillsModel(OrdersModel model)
: this(model, model.on_manual_fill_tuples,
model.factory.subscribe_accounts().Select(d => MappedDictionary.create(d, kv => new AccountTuple(kv.Key, kv.Value))).Table(u => u.id),
model.factory.subscribe_users().Select(d => MappedDictionary.create(d, kv => new UserTuple(kv.Key, kv.Value))).Table(a => a.id)) { }
model.factory.subscribe_accounts().Select(d => MappedDictionary.create(d, (k, v) => new AccountTuple(k, v))).Table(u => u.id),
model.factory.subscribe_users().Select(d => MappedDictionary.create(d, (k, v) => new UserTuple(k, v))).Table(a => a.id)) { }
public ManualFillsModel(OrdersModel model, IObservable<TableUpdate<ManualFillTuple, int>> manual_fills, IObservable<TableUpdate<AccountTuple, uint>> accounts, IObservable<TableUpdate<UserTuple, uint>> users) {
_model = model;
@@ -73,7 +73,7 @@ namespace bts.shell.manual_fills {
// The security desc lookup could be better optimized, but the speed at which fills are imported is probably not a paramount concern.
public static IEnumerable<ParsedManualFill> parse_rows(IEnumerable<string> rows, IReadOnlyDictionary<uint, SecurityImmutable> security_map, IReadOnlyDictionary<uint, string> account_map, IReadOnlyDictionary<uint, string> user_map) =>
parse_rows(rows,
reverse_lookup(MappedDictionary.create(security_map, kv => kv.Value.ToString())),
reverse_lookup(MappedDictionary.create(security_map, s => s.ToString())),
reverse_lookup(account_map),
reverse_lookup(user_map));
@@ -157,7 +157,7 @@ namespace bts.shell.manual_fills {
var lines = File.ReadAllLines(file_name);
var parsed_fills = ManualFillsParser.parse_rows(
lines,
MappedDictionary.create(security_map, kv => kv.Value.security),
MappedDictionary.create(security_map, smd => smd.security),
accounts.ToDictionary(a => a.Value.id, a => a.Value.name),
users.ToDictionary(u => u.Value.id, u => u.Value.name)
).ToList();
@@ -327,6 +327,18 @@ namespace mbts.spark {
public event Action filter_changed;
public static IEnumerable<ColumnBase> editable_columns {
get {
yield return new AttachDetachColumn(typeof(MiniOrderBookView));
yield return new CommitColumn();
yield return new EditablePriceColumn();
yield return new EditableQtyColumn();
yield return new UpdateColumn(typeof(MiniOrderBookView));
yield return new CancelColumn(nameof(OrderBookRowVM.cancel_command));
yield return new CheckColumn(nameof(OrderBookRowVM.is_selected));
}
}
public SecurityColumn security_column { get; }
public SimpleTextColumn<string[]> symbol_column { get; }
public SimpleTextColumn<TimeFilterTuple> time_created_column { get; }
@@ -731,8 +743,11 @@ namespace mbts.spark {
Width = 30;
_checked_binding = new Binding(checked_property);
var header_cb = new CheckBox();
header_cb.ToolTip = "Select / Unselect All";
var header_cb = new CheckBox {
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
ToolTip = "Select / Unselect All"
};
header_cb.SetBinding(CheckBox.IsCheckedProperty, new Binding(nameof(OrderBookVM.all_rows_selected)));
this.Header = header_cb;
}
@@ -1164,6 +1179,7 @@ namespace mbts.spark {
public override FrameworkElement view_factory() {
var price_up_down = new PriceEntryCommitControl();
price_up_down.SetBinding(PriceEntryCommitControl.RealPriceProperty, new Binding(nameof(OrderBookRowVM.price)));
price_up_down.SetBinding(PriceEntryCommitControl.SourceValueProperty, nameof(OrderBookRowVM.prospective_price));
price_up_down.SetBinding(PriceEntryCommitControl.SourceContractSpecProperty, nameof(OrderBookRowVM.contract_spec));
price_up_down.SetBinding(PriceEntryCommitControl.OnCommitProperty, nameof(OrderBookRowVM.modify_order_quantity_andor_price));
@@ -69,10 +69,15 @@ namespace mbts.spark {
_detachable_selected_orders = new HashSet<uint>();
FilterData _cached_filter_data;
public OrderBookVM(
IBus bus, INotificationService notification_service, OrdersModel model, IWindowSettingsManager<OrderBookGridSettings> settings_manager, bool rows_are_initially_expanded = false)
: this(bus, notification_service, model, OrdersModel.piq_enabled ? model.on_order_tuples_with_root_and_piq : model.on_order_tuples_with_root, settings_manager, rows_are_initially_expanded) { }
public OrderBookVM(
IBus bus,
INotificationService notification_service,
OrdersModel model,
IObservable<TableUpdate<OrderSecurityRootPair, uint>> orders_source,
IWindowSettingsManager<OrderBookGridSettings> settings_manager,
bool rows_are_initially_expanded = false) {
@@ -157,7 +162,6 @@ namespace mbts.spark {
begin_attaching += on_begin_attaching;
end_attaching += on_end_attaching;
// switch between orders and/or orders w/ piq
var orders_source = OrdersModel.piq_enabled ? model.on_order_tuples_with_root_and_piq : model.on_order_tuples_with_root;
concat_disposable(
connect_order_mod_vm(),
_can_attach_selected_orders.observe_current().Subscribe(send_attached_orders_async_cmd, begin_attach_cmd),
@@ -761,7 +765,7 @@ namespace mbts.spark {
_size_param = new AlgoParamVM<uint?>("size", "Size", owner._selected_params_mon.size) {
permissions = _size_permissions & _param_permissions
};
var price_param = new AlgoParamVM<double>("price", "Price", owner._selected_params_mon.price, spec?.tick_equality_comparer() ?? MathUtils.epsilon_comparer) {
var price_param = new AlgoParamVM<double>("price", "Price", owner._selected_params_mon.price, spec?.tick_equality_comparer() ?? MathUtils.epsilon_eq_comparer) {
permissions = _price_permissions & _param_permissions
};
_price_param = price_param;
@@ -54,7 +54,7 @@ namespace bts.shell.orderbook {
pvm.selected_predicate_type = _corresponding_predicate_type(EnumHelper.parse_or_default<Columns>(col_id));
return pvm;
},
(gen, anims) => new OrderBookGridSettings(gen, anims.Select(a => new OrderRecordTuplePredicateAnimations(a.predicate_data, a.cell_animations, a.row_animations, a.sound_notifications))) { algo_tree_display_mode = _algo_tree_display_mode },
(gen, anims) => new OrderBookGridSettings(gen, anims.empty_if_null().Select(a => new OrderRecordTuplePredicateAnimations(a.predicate_data, a.cell_animations, a.row_animations, a.sound_notifications))) { algo_tree_display_mode = _algo_tree_display_mode },
p => p.create_predicate(notification_service.data_cache),
p => p.create_notification_predicate(notification_service.data_cache),
notification_service == null ? new Func<IPredicate<OrderSecurity>, SoundNotification, IDisposable>((p, sn) => Disposable.empty) : notification_service.link_to_order,
@@ -1,168 +1,168 @@
using bts.api;
using bts.api.orderman;
using bts.spark;
using mbts.persist;
using mbts.spark;
using mbts.spark.filters;
using mbts.ui;
using mbts.ui.settings;
using mbts.ui.spark;
using ProtoBuf;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
using System.Threading.Tasks;
using static mbts.spark.PAndLView;
using static mbts.ui.controls.DIYGrid;
namespace bts.shell {
internal class PAndLWindowFactory : ShellWindowFactory<PAndLWindowFactory.PAndLStartupData> {
readonly IBus _bus;
readonly OrdersModel _orders_model;
readonly UserSettingsHub _settings_hub;
public PAndLWindowFactory(IShellModule owner, IBus bus, UserSettingsHub settings_hub, OrdersModel orders_model) : base(owner, "SparkPAndL") {
_bus = bus;
_orders_model = orders_model;
_settings_hub = settings_hub;
_dispose = bus.subscribe(SparkTags.ShowPAndLTag).OnDispatcher().Subscribe(show);
}
public static string col_id(Columns col) => col.ToString();
public override Task<IShellWindow> load(PAndLStartupData state) {
return Task.FromResult(_create(state));
}
async void show(PAndLStartupData startup_data) {
var w = await load(startup_data);
_bus.post(SparkTags.ShowWindow, w);
}
IShellWindow _create(PAndLStartupData state) {
var settings_manager = new WindowSettingsManager<GridSettings>(_settings_hub,
state.grid,
get: us => us.pnl_grid,
set: (us, gs) => us.with(provided_pnl_gridsettings: gs));
return new PAndLWindow(_orders_model, this, _bus, state, SparkCommands.ShowPAndL.icon, settings_manager);
}
[ProtoContract]
public class PAndLStartupData {
/// <summary>
/// Parameterless constructor for protobuf
/// </summary>
public PAndLStartupData() { }
public PAndLStartupData(List<SortingCriteria> sorting_criteria, FilterStates filters, pnl_calc_type calc_type, GridSettings grid = default) {
_sort_criteria = sorting_criteria;
this.filters = filters;
this.calc_type = calc_type;
this.grid = grid;
}
public static GridSettings DefaultGridSettings => GridSettings.Default.with(DefaultColumns.all, DefaultColumns.default_sorting_criterion);
public IReadOnlyList<SortingCriteria> sorting_criterion => _sort_criteria ?? (_sort_criteria = DefaultColumns.default_sorting_criterion.ToList());
public static PAndLStartupData Default() =>
new PAndLStartupData(null, new FilterStates(), pnl_calc_type.midpoint);
[ProtoMember(5)]
List<SortingCriteria> _sort_criteria;
[ProtoMember(10)]
public FilterStates filters;
[ProtoMember(15)]
public pnl_calc_type calc_type;
//[ProtoMember(20)]
// ProtoMember 20 cannot be used further
[ProtoMember(25)]
public GridSettings grid;
static class DefaultColumns {
public static GridColumnSettings account => new GridColumnSettings(col_id(Columns.Account), DefaultColumnHeaders.Account, width: DefaultColumnWidths.Account);
public static GridColumnSettings exchange => new GridColumnSettings(col_id(Columns.Exchange), DefaultColumnHeaders.Exchange, false, width: DefaultColumnWidths.Exchange);
public static GridColumnSettings symbol => new GridColumnSettings(col_id(Columns.Symbol), DefaultColumnHeaders.Symbol, width: DefaultColumnWidths.Symbol);
public static GridColumnSettings security => new GridColumnSettings(col_id(Columns.Security), DefaultColumnHeaders.Security, width: DefaultColumnWidths.Security);
public static GridColumnSettings security_type => new GridColumnSettings(col_id(Columns.SecurityType), DefaultColumnHeaders.SecurityType, width: DefaultColumnWidths.SecurityType);
public static GridColumnSettings start_net_position => new GridColumnSettings(col_id(Columns.StartNetPos), DefaultColumnHeaders.StartNetPos, width: DefaultColumnWidths.Start_Net_Pos, horizontal_alignment: System.Windows.HorizontalAlignment.Right);
public static GridColumnSettings start_pnl => new GridColumnSettings(col_id(Columns.StartPNL), DefaultColumnHeaders.StartPnL, false, width: DefaultColumnWidths.PnL_Generic, horizontal_alignment: System.Windows.HorizontalAlignment.Right);
public static GridColumnSettings day_long_pnl => new GridColumnSettings(col_id(Columns.DayLong), DefaultColumnHeaders.DayLong, width: DefaultColumnWidths.PnL_Generic, horizontal_alignment: System.Windows.HorizontalAlignment.Right);
public static GridColumnSettings day_short_pnl => new GridColumnSettings(col_id(Columns.DayShort), DefaultColumnHeaders.DayShort, width: DefaultColumnWidths.PnL_Generic, horizontal_alignment: System.Windows.HorizontalAlignment.Right);
public static GridColumnSettings day_pnl => new GridColumnSettings(col_id(Columns.DayPNL), DefaultColumnHeaders.DayPnL, width: DefaultColumnWidths.PnL_Generic, horizontal_alignment: System.Windows.HorizontalAlignment.Right);
public static GridColumnSettings total_net_pos => new GridColumnSettings(col_id(Columns.TotalNetPos), DefaultColumnHeaders.TotalNetPos, false, width: DefaultColumnWidths.Total_Net_Pos, horizontal_alignment: System.Windows.HorizontalAlignment.Right);
public static GridColumnSettings total_pnl => new GridColumnSettings(col_id(Columns.TotalPNL), DefaultColumnHeaders.TotalPnL, false, width: DefaultColumnWidths.PnL_Generic, horizontal_alignment: System.Windows.HorizontalAlignment.Right);
public static GridColumnSettings pnl_price => new GridColumnSettings(col_id(Columns.PNLPrice), DefaultColumnHeaders.PnLPrice, width: DefaultColumnWidths.PnL_Generic, horizontal_alignment: System.Windows.HorizontalAlignment.Right);
public static IEnumerable<SortingCriteria> default_sorting_criterion {
get {
yield return new SortingCriteria(account.id, true);
yield return new SortingCriteria(exchange.id, true);
yield return new SortingCriteria(symbol.id, true);
yield return new SortingCriteria(security.id, true);
}
}
public static IEnumerable<GridColumnSettings> all {
get {
yield return account;
yield return exchange;
yield return symbol;
yield return security;
yield return security_type;
yield return start_net_position;
yield return start_pnl;
yield return day_long_pnl;
yield return day_short_pnl;
yield return day_pnl;
yield return total_net_pos;
yield return total_pnl;
yield return pnl_price;
}
}
}
}
[ProtoContract]
public class FilterStates {
[ProtoMember(1)]
public string account_regex;
[ProtoMember(2)]
public string exchange_regex;
[ProtoMember(3)]
public string symbol_regex;
[ProtoMember(4)]
public string security_regex;
[ProtoMember(5)]
public IntMinMaxState start_net_pos;
[ProtoMember(6)]
public DoubleMinMaxState start_pnl;
[ProtoMember(7)]
public UIntMinMaxState day_long_pos;
[ProtoMember(8)]
public UIntMinMaxState day_short_pos;
[ProtoMember(9)]
public DoubleMinMaxState day_pnl;
[ProtoMember(10)]
public IntMinMaxState total_net_pos;
[ProtoMember(11)]
public DoubleMinMaxState total_pnl;
[ProtoMember(12)]
public DoubleMinMaxState pnl_price;
// [ProtoMember(13)]
// ProtoMember 13 cannot be used further
[ProtoMember(14)]
public SecurityType[] security_types;
[ProtoMember(15)]
public bool[] has_day_trades;
}
}
using bts.api;
using bts.api.orderman;
using bts.spark;
using mbts.persist;
using mbts.spark;
using mbts.spark.filters;
using mbts.ui;
using mbts.ui.settings;
using mbts.ui.spark;
using ProtoBuf;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
using System.Threading.Tasks;
using static mbts.spark.PAndLView;
using static mbts.ui.controls.DIYGrid;
namespace bts.shell {
internal class PAndLWindowFactory : ShellWindowFactory<PAndLWindowFactory.PAndLStartupData> {
readonly IBus _bus;
readonly OrdersModel _orders_model;
readonly UserSettingsHub _settings_hub;
public PAndLWindowFactory(IShellModule owner, IBus bus, UserSettingsHub settings_hub, OrdersModel orders_model) : base(owner, "SparkPAndL") {
_bus = bus;
_orders_model = orders_model;
_settings_hub = settings_hub;
_dispose = bus.subscribe(SparkTags.ShowPAndLTag).OnDispatcher().Subscribe(show);
}
public static string col_id(Columns col) => col.ToString();
public override Task<IShellWindow> load(PAndLStartupData state) {
return Task.FromResult(_create(state));
}
async void show(PAndLStartupData startup_data) {
var w = await load(startup_data);
_bus.post(SparkTags.ShowWindow, w);
}
IShellWindow _create(PAndLStartupData state) {
var settings_manager = new WindowSettingsManager<GridSettings>(_settings_hub,
state.grid,
get: us => us.pnl_grid,
set: (us, gs) => us.with(provided_pnl_gridsettings: gs));
return new PAndLWindow(_orders_model, this, _bus, state, SparkCommands.ShowPAndL.icon, settings_manager);
}
[ProtoContract]
public class PAndLStartupData {
/// <summary>
/// Parameterless constructor for protobuf
/// </summary>
public PAndLStartupData() { }
public PAndLStartupData(List<SortingCriteria> sorting_criteria, FilterStates filters, pnl_calc_type calc_type, GridSettings grid = default) {
_sort_criteria = sorting_criteria;
this.filters = filters;
this.calc_type = calc_type;
this.grid = grid;
}
public static GridSettings DefaultGridSettings => GridSettings.Default.with(DefaultColumns.all, DefaultColumns.default_sorting_criterion);
public IReadOnlyList<SortingCriteria> sorting_criterion => _sort_criteria ?? (_sort_criteria = DefaultColumns.default_sorting_criterion.ToList());
public static PAndLStartupData Default() =>
new PAndLStartupData(null, new FilterStates(), pnl_calc_type.midpoint);
[ProtoMember(5)]
List<SortingCriteria> _sort_criteria;
[ProtoMember(10)]
public FilterStates filters;
[ProtoMember(15)]
public pnl_calc_type calc_type;
//[ProtoMember(20)]
// ProtoMember 20 cannot be used further
[ProtoMember(25)]
public GridSettings grid;
static class DefaultColumns {
public static GridColumnSettings account => new GridColumnSettings(col_id(Columns.Account), DefaultColumnHeaders.Account, width: DefaultColumnWidths.Account);
public static GridColumnSettings exchange => new GridColumnSettings(col_id(Columns.Exchange), DefaultColumnHeaders.Exchange, false, width: DefaultColumnWidths.Exchange);
public static GridColumnSettings symbol => new GridColumnSettings(col_id(Columns.Symbol), DefaultColumnHeaders.Symbol, width: DefaultColumnWidths.Symbol);
public static GridColumnSettings security => new GridColumnSettings(col_id(Columns.Security), DefaultColumnHeaders.Security, width: DefaultColumnWidths.Security);
public static GridColumnSettings security_type => new GridColumnSettings(col_id(Columns.SecurityType), DefaultColumnHeaders.SecurityType, width: DefaultColumnWidths.SecurityType);
public static GridColumnSettings start_net_position => new GridColumnSettings(col_id(Columns.StartNetPos), DefaultColumnHeaders.StartNetPos, width: DefaultColumnWidths.Start_Net_Pos, horizontal_alignment: System.Windows.HorizontalAlignment.Right);
public static GridColumnSettings start_pnl => new GridColumnSettings(col_id(Columns.StartPNL), DefaultColumnHeaders.StartPnL, false, width: DefaultColumnWidths.PnL_Generic, horizontal_alignment: System.Windows.HorizontalAlignment.Right);
public static GridColumnSettings day_long_pnl => new GridColumnSettings(col_id(Columns.DayLong), DefaultColumnHeaders.DayLong, width: DefaultColumnWidths.PnL_Generic, horizontal_alignment: System.Windows.HorizontalAlignment.Right);
public static GridColumnSettings day_short_pnl => new GridColumnSettings(col_id(Columns.DayShort), DefaultColumnHeaders.DayShort, width: DefaultColumnWidths.PnL_Generic, horizontal_alignment: System.Windows.HorizontalAlignment.Right);
public static GridColumnSettings day_pnl => new GridColumnSettings(col_id(Columns.DayPNL), DefaultColumnHeaders.DayPnL, width: DefaultColumnWidths.PnL_Generic, horizontal_alignment: System.Windows.HorizontalAlignment.Right);
public static GridColumnSettings total_net_pos => new GridColumnSettings(col_id(Columns.TotalNetPos), DefaultColumnHeaders.TotalNetPos, false, width: DefaultColumnWidths.Total_Net_Pos, horizontal_alignment: System.Windows.HorizontalAlignment.Right);
public static GridColumnSettings total_pnl => new GridColumnSettings(col_id(Columns.TotalPNL), DefaultColumnHeaders.TotalPnL, false, width: DefaultColumnWidths.PnL_Generic, horizontal_alignment: System.Windows.HorizontalAlignment.Right);
public static GridColumnSettings pnl_price => new GridColumnSettings(col_id(Columns.PNLPrice), DefaultColumnHeaders.PnLPrice, width: DefaultColumnWidths.PnL_Generic, horizontal_alignment: System.Windows.HorizontalAlignment.Right);
public static IEnumerable<SortingCriteria> default_sorting_criterion {
get {
yield return new SortingCriteria(account.id, true);
yield return new SortingCriteria(exchange.id, true);
yield return new SortingCriteria(symbol.id, true);
yield return new SortingCriteria(security.id, true);
}
}
public static IEnumerable<GridColumnSettings> all {
get {
yield return account;
yield return exchange;
yield return symbol;
yield return security;
yield return security_type;
yield return start_net_position;
yield return start_pnl;
yield return day_long_pnl;
yield return day_short_pnl;
yield return day_pnl;
yield return total_net_pos;
yield return total_pnl;
yield return pnl_price;
}
}
}
}
[ProtoContract]
public class FilterStates {
[ProtoMember(1)]
public string account_regex;
[ProtoMember(2)]
public string exchange_regex;
[ProtoMember(3)]
public string symbol_regex;
[ProtoMember(4)]
public string security_regex;
[ProtoMember(5)]
public IntMinMaxState start_net_pos;
[ProtoMember(6)]
public DoubleMinMaxState start_pnl;
[ProtoMember(7)]
public UIntMinMaxState day_long_pos;
[ProtoMember(8)]
public UIntMinMaxState day_short_pos;
[ProtoMember(9)]
public DoubleMinMaxState day_pnl;
[ProtoMember(10)]
public IntMinMaxState total_net_pos;
[ProtoMember(11)]
public DoubleMinMaxState total_pnl;
[ProtoMember(12)]
public DoubleMinMaxState pnl_price;
// [ProtoMember(13)]
// ProtoMember 13 cannot be used further
[ProtoMember(14)]
public SecurityType[] security_types;
[ProtoMember(15)]
public bool[] has_day_trades;
}
}
}
@@ -0,0 +1,541 @@
using bts.api;
using bts.api.orderman;
using bts.api.position;
using bts.utils;
using mbts;
using mbts.spark;
using mbts.spark.utilities;
using mbts.ui.controls;
using mbts.utils;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
namespace bts.shell.pnl {
public enum PNLCalculationType {
None,
Bid,
Ask,
Opposite,
Same,
Waterfall,
Last,
Mid,
Settle
}
readonly struct PositionPriceSize : IEquatable<PositionPriceSize> {
public static readonly PositionPriceSize empty = new PositionPriceSize(0, 0, 0, 0);
public readonly double long_pos, short_pos, long_avg, short_avg;
public double net_pos => long_pos - short_pos;
public bool has_position => long_pos > 0 || short_pos > 0;
public PositionPriceSize(double long_pos, double long_avg, double short_pos, double short_avg) {
this.long_pos = long_pos;
this.long_avg = long_avg;
this.short_pos = short_pos;
this.short_avg = short_avg;
}
public bool Equals(PositionPriceSize other) =>
MathUtils.epsilon_equals(long_pos, other.long_pos) &&
MathUtils.epsilon_equals(long_avg, other.long_avg) &&
MathUtils.epsilon_equals(short_pos, other.short_pos) &&
MathUtils.epsilon_equals(short_avg, other.short_avg);
public override bool Equals(object obj) =>
obj is PositionPriceSize other && Equals(other);
public override int GetHashCode() =>
EqualityComparer.aggregate_hash(
long_pos.GetHashCode(),
long_avg.GetHashCode(),
short_pos.GetHashCode(),
short_avg.GetHashCode());
public static bool operator ==(PositionPriceSize a, PositionPriceSize b) => a.Equals(b);
public static bool operator !=(PositionPriceSize a, PositionPriceSize b) => !a.Equals(b);
}
readonly struct PNLRowKey : IEquatable<PNLRowKey>, IComparable<PNLRowKey>, IComparable {
public static readonly PNLRowKey empty = new PNLRowKey();
static readonly IComparer<PNLRowKey> _comparer_inst = new ComparerBuilder<PNLRowKey>(
new[] {
Comparer<uint>.Default.wrap<uint, PNLRowKey>(r => r.account ?? 0),
Comparer<uint>.Default.wrap<uint, PNLRowKey>(r => r.security ?? 0)
}).to_comparer();
public readonly Venue? exchange;
public readonly Security.ContractOneofCase? security_type;
public readonly string symbol;
public readonly PNLKey data_key;
public uint? security => data_key.security_id;
public uint? account => data_key.account_id;
public PNLRowKey(
Venue? exchange = null,
Security.ContractOneofCase? security_type = null,
string symbol = null,
uint? security = null,
uint? account = null) {
//
this.exchange = exchange;
this.security_type = security_type;
this.symbol = symbol;
data_key = new PNLKey(account.Value, security.Value);
}
int IComparable.CompareTo(object obj) =>
obj is PNLRowKey other ? CompareTo(other) : 1;
public int CompareTo(PNLRowKey other) =>
_comparer_inst.Compare(this, other);
public bool Equals(PNLRowKey other) =>
exchange == other.exchange &&
security_type == other.security_type &&
symbol == other.symbol &&
security == other.security &&
account == other.account;
public override bool Equals(object obj) =>
obj is PNLRowKey other && Equals(other);
public override int GetHashCode() =>
EqualityComparer.aggregate_hash(
exchange?.GetHashCode() ?? 0,
security_type?.GetHashCode() ?? 0,
symbol?.GetHashCode() ?? 0,
security?.GetHashCode() ?? 0,
account?.GetHashCode() ?? 0);
public static bool operator ==(PNLRowKey a, PNLRowKey b) => a.Equals(b);
public static bool operator !=(PNLRowKey a, PNLRowKey b) => !a.Equals(b);
}
readonly struct WorkingOrderData {
public readonly WorkingOrderSideData long_data, short_data;
WorkingOrderData(WorkingOrderSideData long_data, WorkingOrderSideData short_data) {
this.long_data = long_data;
this.short_data = short_data;
}
public static WorkingOrderData Empty =>
new WorkingOrderData(WorkingOrderSideData.EmptyLong, WorkingOrderSideData.EmptyShort);
public double top_bid =>
long_data.data.Count > 0 ?
long_data.data.First().Key :
double.NaN;
public double top_ask =>
short_data.data.Count > 0 ?
short_data.data.First().Key :
double.NaN;
public PositionPriceSize summary =>
new PositionPriceSize(long_data.working_total, long_data.avg_price, short_data.working_total, short_data.avg_price);
public static WorkingOrderData Create(IEnumerable<OrderRecordImmutable> orders) =>
orders.Aggregate(Empty, (seed, order) => seed.Add(order));
public WorkingOrderData Add(OrderRecordImmutable order) {
if (order.working_quantity() < 0)
return new WorkingOrderData(long_data, short_data);
switch (order.side()) {
case Side.Buy: return new WorkingOrderData(long_data.Add(order), short_data);
case Side.Sell: return new WorkingOrderData(long_data, short_data.Add(order));
default: throw ExceptionBuilder.Argument.unexpected_case(order.side(), "order.side");
}
}
public WorkingOrderData Remove(OrderRecordImmutable order) {
if (order.working_quantity() < 0)
return new WorkingOrderData(long_data, short_data);
switch (order.side()) {
case Side.Buy: return new WorkingOrderData(long_data.Remove(order.Key, order.price()), short_data);
case Side.Sell: return new WorkingOrderData(long_data, short_data.Remove(order.Key, order.price()));
default: throw ExceptionBuilder.Argument.unexpected_case(order.side(), "order.side");
}
}
}
readonly struct WorkingOrderSideData {
public readonly double avg_price;
public readonly ImmutableSortedDictionary<double, ImmutableDictionary<uint, OrderRecordImmutable>> data;
public readonly int working_total;
WorkingOrderSideData(double avg_price, int working_total, ImmutableSortedDictionary<double, ImmutableDictionary<uint, OrderRecordImmutable>> data) {
this.avg_price = avg_price;
this.working_total = working_total;
this.data = data;
}
public static WorkingOrderSideData EmptyLong => new WorkingOrderSideData(double.NaN, 0, ImmutableSortedDictionary.Create<double, ImmutableDictionary<uint, OrderRecordImmutable>>(new ReverseComparer<double>(OrderDataHelper.PriceComparer)));
public static WorkingOrderSideData EmptyShort => new WorkingOrderSideData(double.NaN, 0, ImmutableSortedDictionary.Create<double, ImmutableDictionary<uint, OrderRecordImmutable>>(OrderDataHelper.PriceComparer));
public double top_price =>
data.Count > 0 ?
data.First().Key :
double.NaN;
public int top_volume =>
data.Count > 0 ?
data.Values.First().Values.Sum(o => o.working_quantity()) :
0;
public WorkingOrderSideData Add(OrderRecordImmutable order_record) {
uint order = order_record.Key;
int working_qty = order_record.working_quantity();
double price = order_record.price();
if (working_qty <= 0)
throw new ArgumentException(nameof(working_qty));
if (working_total == 0) {
return new WorkingOrderSideData(price, working_qty, data.Add(price, ImmutableDictionary<uint, OrderRecordImmutable>.Empty.Add(order, order_record)));
} else return new WorkingOrderSideData(
avg_price + ((price - avg_price) * ((double)working_qty / (working_total + working_qty))),
working_total + working_qty,
data.SetItem(price, data.TryGetValue(price, out var curr) ?
curr.Add(order, order_record) :
ImmutableDictionary<uint, OrderRecordImmutable>.Empty.Add(order, order_record)));
}
public WorkingOrderSideData Remove(uint order, double price) {
var tmp_data = data;
if (!tmp_data.TryGetValue(price, out var wdata))
throw new KeyNotFoundException(nameof(price));
if (!wdata.TryGetValue(order, out var worder))
throw new KeyNotFoundException(nameof(order));
var wqty = worder.working_quantity();
wdata = wdata.Remove(order);
if (wdata.Count == 0)
tmp_data = tmp_data.Remove(price);
if (tmp_data.Count == 0)
return new WorkingOrderSideData(double.NaN, 0, ImmutableSortedDictionary.Create<double, ImmutableDictionary<uint, OrderRecordImmutable>>(data.KeyComparer));
return new WorkingOrderSideData(
avg_price - ((price - avg_price) * (wqty / (working_total - wqty))),
working_total - wqty,
tmp_data);
}
public override string ToString() {
if (data.Count > 0) {
var best_order = data.Values.First().Values.First();
return OrderUtils.order_collection_summary(best_order.price(), avg_price, best_order.working_quantity(), working_total, SecurityDataHelper.Defaults.ContractSpecificationImmutable);
}
return string.Empty;
}
}
readonly struct PNLRowData : IEquatable<PNLRowData> {
public static readonly Func<PNLRowData, PNLKey> key_fn = d => d.key;
public static IComparer<PNLRowData> key_comparer = Comparer<PNLKey>.Default.wrap<PNLKey, PNLRowData>(d => d.key);
static readonly WaterfallPriceFlags[] _waterfall_preferences = new[] {
WaterfallPriceFlags.LastInsideSpread,
WaterfallPriceFlags.Mid,
WaterfallPriceFlags.LastGTEBid,
WaterfallPriceFlags.Bid,
WaterfallPriceFlags.LastLTEAsk,
WaterfallPriceFlags.Ask,
WaterfallPriceFlags.Last,
WaterfallPriceFlags.Settle
};
public readonly PNLKey key;
public readonly SecurityImmutable? security;
public readonly string account_name;
public readonly PNLCalculationType calc_type;
public readonly PositionPriceSize day, sod;
public readonly WorkingOrderData working;
public readonly double
cpp,
bid_price,
ask_price,
last_price,
settlement_price;
public PNLRowData(
PNLKey key,
double cpp = 0,
double bid_price = 0,
double ask_price = 0,
double last_price = 0,
double settlement_price = 0,
PositionPriceSize? sod = null,
PositionPriceSize? day = null,
WorkingOrderData? working = null,
SecurityImmutable? security = null,
string account_name = null,
PNLCalculationType calc_type = PNLCalculationType.Mid) {
//
this.key = key;
this.cpp = cpp;
this.bid_price = bid_price;
this.ask_price = ask_price;
this.last_price = last_price;
this.settlement_price = settlement_price;
this.sod = sod ?? PositionPriceSize.empty;
this.day = day ?? PositionPriceSize.empty;
this.working = working ?? WorkingOrderData.Empty;
this.security = security;
this.account_name = account_name;
this.calc_type = calc_type;
}
[Flags]
enum WaterfallPriceFlags {
None = 0,
Bid = 1,
Ask = 1 << 1,
Last = 1 << 2,
Settle = 1 << 3,
Mid = Bid | Ask,
LastGTEBid = Bid | Last | 1 << 4,
LastLTEAsk = Ask | Last | 1 << 5,
LastInsideSpread = LastGTEBid | LastLTEAsk,
BidGTESettle = Bid | Settle | 1 << 6,
AskLTESettle = Ask | Settle | 1 << 7
}
/// Market/Calc
public double mid_price => (bid_price + ask_price) / 2;
public double pnl_price {
get {
if (!day.has_position && !sod.has_position)
return double.NaN;
switch (calc_type) {
case PNLCalculationType.Bid: return bid_price;
case PNLCalculationType.Ask: return ask_price;
case PNLCalculationType.Mid: return mid_price;
case PNLCalculationType.Last: return last_price;
// waited averages must be used when the pnl price varies, depending on the side.
case PNLCalculationType.Same: return MathUtils.weighted_average(bid_price, day_long_pos, ask_price, day_short_pos);
case PNLCalculationType.Opposite: return MathUtils.weighted_average(ask_price, day_long_pos, bid_price, day_short_pos);
case PNLCalculationType.Settle: return settlement_price;
case PNLCalculationType.Waterfall:
default: return _waterfall_price();
}
}
}
public double long_pnl_price {
get {
switch (calc_type) {
case PNLCalculationType.Same: return bid_price;
case PNLCalculationType.Opposite: return ask_price;
default: return pnl_price;
}
}
}
public double short_pnl_price {
get {
switch (calc_type) {
case PNLCalculationType.Same: return ask_price;
case PNLCalculationType.Opposite: return bid_price;
default: return pnl_price;
}
}
}
/// Sod
public double sod_pnl => cpp *
DoubleUtil.nan_to_zero_sum(
sod_long_avg_price_delta_raw * sod_long_pos,
sod_short_avg_price_delta_raw * sod_short_pos
);
// long
public double sod_long_pos => sod.long_pos;
public double sod_long_avg_price => cpp * sod.long_avg;
public double sod_long_avg_price_raw => sod.long_avg;
public double sod_long_avg_price_delta => cpp * sod_long_avg_price_delta_raw;
public double sod_long_avg_price_delta_raw => sod.long_pos == 0 ? double.NaN : long_pnl_price - sod.long_avg;
// short
public double sod_short_pos => sod.short_pos;
public double sod_short_avg_price => cpp * sod.short_avg;
public double sod_short_avg_price_raw => sod.short_avg;
public double sod_short_avg_price_delta => cpp * sod_short_avg_price_delta_raw;
public double sod_short_avg_price_delta_raw => sod.short_pos == 0 ? double.NaN : sod.short_avg - short_pnl_price;
// net
public double sod_net_pos => sod.net_pos;
public double sod_net_avg_price_delta => cpp * sod_net_avg_price_delta_raw;
public double sod_net_avg_price_delta_raw =>
MathUtils.weighted_average(
sod_long_avg_price_delta_raw, sod.long_pos,
sod_short_avg_price_delta_raw, sod.short_pos);
/// Day
public double day_pnl => cpp *
DoubleUtil.nan_to_zero_sum(
day_long_avg_price_delta_raw * day_long_pos,
day_short_avg_price_delta_raw * day_short_pos
);
// long
public double day_long_pos => day.long_pos;
public double day_long_avg_price => cpp * day.long_avg;
public double day_long_avg_price_raw => day.long_avg;
public double day_long_avg_price_delta => cpp * day_long_avg_price_delta_raw;
public double day_long_avg_price_delta_raw => day.long_pos == 0 ? double.NaN : long_pnl_price - day.long_avg;
// short
public double day_short_pos => day.short_pos;
public double day_short_avg_price => cpp * day.short_avg;
public double day_short_avg_price_raw => day.short_avg;
public double day_short_avg_price_delta => cpp * day_short_avg_price_delta_raw;
public double day_short_avg_price_delta_raw => day.short_pos == 0 ? double.NaN : day.short_avg - short_pnl_price;
public bool has_day_trades => day.long_pos > 0 || day.short_pos > 0;
// net
public double day_net_pos => day.net_pos;
public double day_net_avg_price_delta => cpp * day_net_avg_price_delta_raw;
public double day_net_avg_price_delta_raw =>
MathUtils.weighted_average(
day_long_avg_price_delta_raw, day.long_pos,
day_short_avg_price_delta_raw, day.short_pos);
/// Total
public double total_pnl => DoubleUtil.nan_to_zero_sum(sod_pnl, day_pnl);
// long
public double total_long_pos => DoubleUtil.nan_to_zero_sum(sod.long_pos, day.long_pos);
public double total_long_avg_price => cpp * total_long_avg_price_raw;
public double total_long_avg_price_raw => DoubleUtil.nan_to_zero_sum(sod.long_avg, day.long_avg);
public double total_long_avg_price_delta => cpp * total_long_avg_price_delta_raw;
public double total_long_avg_price_delta_raw =>
MathUtils.weighted_average(
sod_long_avg_price_delta_raw, sod.long_pos,
day_long_avg_price_delta_raw, day.long_pos);
// short
public double total_short_pos => DoubleUtil.nan_to_zero_sum(sod.short_pos, day.short_pos);
public double total_short_avg_price => cpp * total_short_avg_price_raw;
public double total_short_avg_price_raw => DoubleUtil.nan_to_zero_sum(sod.short_avg, day.short_avg);
public double total_short_avg_price_delta => cpp * total_short_avg_price_delta_raw;
public double total_short_avg_price_delta_raw =>
MathUtils.weighted_average(
sod_short_avg_price_delta_raw, sod.short_pos,
day_short_avg_price_delta_raw, day.short_pos);
// net
public double total_net_pos => DoubleUtil.nan_to_zero_sum(sod.long_pos, -sod.short_pos, day.long_pos, -day.short_pos);
public double total_net_avg_price_delta => cpp * total_net_avg_price_delta_raw;
public double total_net_avg_price_delta_raw =>
MathUtils.weighted_average(
total_long_avg_price_delta_raw, total_long_pos,
total_short_avg_price_delta_raw, total_short_pos);
#region Working
// long
public double working_long_pos => working.long_data.working_total;
public double working_long_avg_price => cpp * working_long_avg_price_raw;
public double working_long_top_price => cpp * working_long_top_price_raw;
public double working_long_top_price_raw => working.top_bid;
public double working_long_avg_price_raw => working.long_data.avg_price;
public double working_long_avg_price_delta => cpp * working_long_avg_price_delta_raw;
public double working_long_avg_price_delta_raw => working_long_pos == 0 ? double.NaN : long_pnl_price - working_long_avg_price_raw;
// short
public double working_short_pos => working.short_data.working_total;
public double working_short_avg_price => cpp * working_short_avg_price_raw;
public double working_short_top_price => cpp * working_short_top_price_raw;
public double working_short_top_price_raw => working.top_ask;
public double working_short_avg_price_raw => working.short_data.avg_price;
public double working_short_avg_price_delta => cpp * working_short_avg_price_delta_raw;
public double working_short_avg_price_delta_raw => working_short_pos == 0 ? double.NaN : working_short_avg_price_raw - short_pnl_price;
// net
public double working_net_pos => working_long_pos - working_short_pos;
public double working_net_avg_price_delta => cpp * working_net_avg_price_delta_raw;
public double working_net_avg_price_delta_raw =>
MathUtils.weighted_average(
working_long_avg_price_delta_raw, working_long_pos,
working_short_avg_price_delta_raw, working_short_pos);
#endregion
WaterfallPriceFlags _waterfall_flags() {
var flags = WaterfallPriceFlags.None;
if (!double.IsNaN(bid_price)) flags |= WaterfallPriceFlags.Bid;
if (!double.IsNaN(ask_price)) flags |= WaterfallPriceFlags.Ask;
if (!double.IsNaN(last_price)) flags |= WaterfallPriceFlags.Last;
if (last_price >= bid_price) flags |= WaterfallPriceFlags.LastGTEBid;
if (last_price <= ask_price) flags |= WaterfallPriceFlags.LastLTEAsk;
if (!double.IsNaN(settlement_price)) flags |= WaterfallPriceFlags.Settle;
if (bid_price >= settlement_price) flags |= WaterfallPriceFlags.BidGTESettle;
if (ask_price <= settlement_price) flags |= WaterfallPriceFlags.AskLTESettle;
return flags;
}
double _waterfall_price() {
var flags = _waterfall_flags();
// return price for the best match
for (int i = 0; i < _waterfall_preferences.Length; i++) {
if ((_waterfall_preferences[i] & flags) == _waterfall_preferences[i])
return _waterfall_price(_waterfall_preferences[i]);
}
return double.NaN;
}
double _waterfall_price(WaterfallPriceFlags ltp) {
switch (ltp) {
case WaterfallPriceFlags.Last:
case WaterfallPriceFlags.LastInsideSpread:
case WaterfallPriceFlags.LastGTEBid:
case WaterfallPriceFlags.LastLTEAsk: return last_price;
case WaterfallPriceFlags.Mid: return mid_price;
case WaterfallPriceFlags.Bid:
case WaterfallPriceFlags.BidGTESettle: return bid_price;
case WaterfallPriceFlags.Ask:
case WaterfallPriceFlags.AskLTESettle: return ask_price;
case WaterfallPriceFlags.Settle: return settlement_price;
default: return double.NaN;
}
}
public bool Equals(PNLRowData other) =>
key == other.key &&
cpp == other.cpp &&
security.HasValue == other.security.HasValue &&
account_name == other.account_name &&
sod == other.sod &&
day == other.day &&
working.summary == other.working.summary;
public override bool Equals(object obj) =>
obj is PNLRowData other && Equals(other);
// assuming callers will not combine different values with the same key,
// the other components shouldn't be included in the hash calculation.
public override int GetHashCode() =>
key.GetHashCode();
public static bool operator ==(PNLRowData a, PNLRowData b) => a.Equals(b);
public static bool operator !=(PNLRowData a, PNLRowData b) => !a.Equals(b);
}
static class PNLRowDataFactory {
public static PNLRowData create((SecurityImmutable? sec, PNLUnitImmutable value) t) =>
new PNLRowData(new PNLKey(t.value.Pos.AccountId, t.value.Pos.SecurityId),
t.value.CashValue,
t.value.BidPrice,
t.value.AskPrice,
t.value.LastPrice,
t.value.SettlementPrice,
new PositionPriceSize(t.value.Pos.SodLongPos, t.value.Pos.SodLongAvgPrice, t.value.Pos.SodShortPos, t.value.Pos.SodShortAvgPrice),
new PositionPriceSize(t.value.Pos.DayLongPos, t.value.Pos.DayLongAvgPrice, t.value.Pos.DayShortPos, t.value.Pos.DayShortAvgPrice),
WorkingOrderData.Empty,
t.sec,
t.value.Pos.Account);
}
static class PNLDataExtensions {
// Note: PNLRowData is passed by ref because it is fairly large struct, and since it is a readonly struct, we know it cannot be modified (so might as well avoid excessive copying).
public static PNLRowData with(this PNLRowData pnl_data, SecurityImmutable? security = null, string account_name = null, WorkingOrderData? working = null, PNLCalculationType? calc_type = null) =>
new PNLRowData(
pnl_data.key,
pnl_data.cpp,
pnl_data.bid_price,
pnl_data.ask_price,
pnl_data.last_price,
pnl_data.settlement_price,
pnl_data.sod,
pnl_data.day,
working ?? pnl_data.working,
security ?? pnl_data.security,
account_name ?? pnl_data.account_name,
calc_type ?? pnl_data.calc_type);
}
}
@@ -0,0 +1,40 @@
using bts.utils;
using System;
namespace bts.shell.pnl {
readonly struct PNLKey : IEquatable<PNLKey>, IComparable<PNLKey>, IComparable {
public readonly uint account_id, security_id;
public PNLKey(uint account_id, uint security_id) {
this.account_id = account_id;
this.security_id = security_id;
}
public int CompareTo(PNLKey other) {
int c = account_id.CompareTo(other.account_id);
if (c == 0)
c = security_id.CompareTo(other.security_id);
return c;
}
int IComparable.CompareTo(object obj) =>
obj is PNLKey other ? CompareTo(other) : 1;
public bool Equals(PNLKey other) =>
security_id == other.security_id &&
account_id == other.account_id;
public override bool Equals(object obj) =>
obj is PNLKey other && Equals(other);
public override int GetHashCode() =>
(security_id.GetHashCode() % MathUtils.Primes.P_65449) |
((account_id.GetHashCode() % MathUtils.Primes.P_65449) << 16);
public override string ToString() =>
$"Account: {account_id}, Security: {security_id}";
public static bool operator ==(PNLKey a, PNLKey b) => a.Equals(b);
public static bool operator !=(PNLKey a, PNLKey b) => !a.Equals(b);
}
}
@@ -0,0 +1,123 @@
using bts.utils;
using System;
namespace bts.shell.pnl {
readonly struct PNLNormalizedData : IEquatable<PNLNormalizedData> {
public static readonly PNLNormalizedData zero =
new PNLNormalizedData(0, 0, 0, 0, 0, 0);
public static PNLNormalizedData create_nanless(
decimal day_pnl,
decimal day_long_pos,
decimal day_short_pos,
decimal sod_pnl,
decimal sod_long_pos,
decimal sod_short_pos,
decimal working_long_pos,
decimal working_short_pos) =>
new PNLNormalizedData(
day_pnl,
day_long_pos,
day_short_pos,
sod_pnl,
sod_long_pos,
sod_short_pos,
working_long_pos,
working_short_pos);
public PNLNormalizedData(
decimal day_pnl,
decimal day_long_pos,
decimal day_short_pos,
decimal sod_pnl,
decimal sod_long_pos,
decimal sod_short_pos,
decimal working_long_pos = 0,
decimal working_short_pos = 0) {
//
this.day_pnl = day_pnl;
this.day_long_pos = day_long_pos;
this.day_short_pos = day_short_pos;
this.sod_pnl = sod_pnl;
this.sod_long_pos = sod_long_pos;
this.sod_short_pos = sod_short_pos;
this.working_long_pos = working_long_pos;
this.working_short_pos = working_short_pos;
}
// sod
public decimal sod_pnl { get; }
public decimal sod_long_pos { get; }
public decimal sod_short_pos { get; }
public decimal sod_net_pos => sod_long_pos - sod_short_pos;
// day
public decimal day_pnl { get; }
public decimal day_long_pos { get; }
public decimal day_short_pos { get; }
public decimal day_net_pos => day_long_pos - day_short_pos;
// total
public decimal total_pnl => sod_pnl + day_pnl;
public decimal total_net_pos => total_long_pos - total_short_pos;
public decimal total_long_pos => sod_long_pos + day_long_pos;
public decimal total_short_pos => sod_short_pos + day_short_pos;
// working
public decimal working_long_pos { get; }
public decimal working_net_pos => working_long_pos - working_short_pos;
public decimal working_short_pos { get; }
public bool Equals(PNLNormalizedData other) =>
day_pnl == other.day_pnl &&
day_long_pos == other.day_long_pos &&
day_short_pos == other.day_short_pos &&
sod_pnl == other.sod_pnl &&
sod_long_pos == other.sod_long_pos &&
sod_short_pos == other.sod_short_pos &&
working_long_pos == other.working_long_pos &&
working_short_pos == other.working_short_pos;
public override bool Equals(object obj) =>
obj is PNLNormalizedData other && Equals(other);
public override int GetHashCode() =>
EqualityComparer.aggregate_hash(
day_pnl.GetHashCode(),
sod_pnl.GetHashCode());
public override string ToString() =>
string.Join(",", new[] {
$"SOD PNL : {sod_pnl}",
$"SOD Long : {sod_pnl}",
$"SOD Short: {sod_pnl}",
$"Day PNL : {day_pnl}",
$"Day Long : {day_pnl}",
$"Day Short: {day_pnl}",
$"Work Long : {working_long_pos}",
$"Work Short: {working_short_pos}",
});
public static bool operator ==(PNLNormalizedData a, PNLNormalizedData b) => a.Equals(b);
public static bool operator !=(PNLNormalizedData a, PNLNormalizedData b) => !a.Equals(b);
public static PNLNormalizedData operator +(PNLNormalizedData a, PNLNormalizedData b) =>
new PNLNormalizedData(
a.day_pnl + b.day_pnl,
a.day_long_pos + b.day_long_pos,
a.day_short_pos + b.day_short_pos,
a.sod_pnl + b.sod_pnl,
a.sod_long_pos + b.sod_long_pos,
a.sod_short_pos + b.sod_short_pos,
a.working_long_pos + b.working_long_pos,
a.working_short_pos + b.working_short_pos);
public static PNLNormalizedData operator -(PNLNormalizedData a, PNLNormalizedData b) =>
new PNLNormalizedData(
a.day_pnl - b.day_pnl,
a.day_long_pos - b.day_long_pos,
a.day_short_pos - b.day_short_pos,
a.sod_pnl - b.sod_pnl,
a.sod_long_pos - b.sod_long_pos,
a.sod_short_pos - b.sod_short_pos,
a.working_long_pos - b.working_long_pos,
a.working_short_pos - b.working_short_pos);
}
}
@@ -0,0 +1,316 @@
using bts.api;
using bts.collections;
using bts.shell.pnl;
using bts.spark;
using bts.utils;
using mbts;
using mbts.config;
using mbts.persist;
using mbts.spark;
using mbts.ui;
using mbts.ui.controls;
using mbts.ui.settings;
using mbts.ui.spark;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Collections.Specialized;
using System.Linq;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media;
namespace bts.shell {
class PNLWindowFactory : ShellWindowFactory<PNLSaveState> {
const string _window_type = "PNL", _window_title = "PNL";
readonly IBus _bus;
readonly OrdersModel _orders_model;
readonly UserSettingsHub _settings_hub;
readonly PNLConfig _pnl_conf;
readonly IShellWindowThreadIDProvider _thread_id_provider;
public PNLWindowFactory(IShellModule owner, IBus bus, UserSettingsHub settings_hub, OrdersModel orders_model) : base(owner, _window_type) {
_bus = bus;
_orders_model = orders_model;
_settings_hub = settings_hub;
_pnl_conf = new PNLConfig(Configuration.get<bts.config.PNLOptions>());
_thread_id_provider = ShellWindowThreadIDFactory.create(_window_type, Configuration.get<bts.config.MultiThreadedWindows>().pnl);
_dispose = bus.subscribe(SparkTags.ShowPNLTag).OnDispatcher().Subscribe(_show);
}
public override Task<IShellWindow> load(PNLSaveState state) =>
Task.FromResult(_create(state.settings));
void _show(PNLSettings settings) =>
_bus.post(SparkTags.ShowWindow, _create(settings));
IShellWindow _create(PNLSettings settings, uint settings_id = 0) {
if (settings != null) {
var def_grid = PNLSettings.DefaultGridSettings;
// add missing columns (i.e., if new cols were added after this workspace was saved)
if (settings.grid.columns.Count != def_grid.columns.Count) {
settings.grid.columns.difference_hard(def_grid.columns, out var added, out var removed, EqualityComparer.Create<GridColumnSettings, string>(c => c.id));
var new_cols = settings.grid.columns.ToList();
for (int i = 0; i < added.Count; i++) {
var col = added[i];
new_cols.Add(col);
}
settings.grid = settings.grid.with(columns: new_cols);
}
}
IDisposable active_settings_dialog = null;
// Need to keep a local copy of the columns, since they are defined in xaml (not the viewmodel), and get_save_state may be called on a non-dispatcher thread.
// If that happens, we would be unable to access _grid.Columns, and thus unable to get the current column settings.
// Use an ImmutableList to avoid any threading issues when responding to CollectionChanged events on the dispatcher thread.
ImmutableList<DIYGrid.ColumnBase> cached_columns = ImmutableList<DIYGrid.ColumnBase>.Empty;
return SparkWindow<PNLSaveState>.create(_window_type, _window_title, _bus, this, Icons.Accounting, _thread_id_provider.next_id(), () => {
var settings_subject = new Subject<PNLSettings>();
IEnumerable<SortingCriteria> last_sorting_criterion = null;
var vm = new PNLVM(_bus, _orders_model, settings ?? PNLSettings.Default);
var content = new PNLView {
DataContext = vm
};
cached_columns = ImmutableList<DIYGrid.ColumnBase>.Empty.AddRange(content._grid.Columns);
var profile_service = new SettingsProfileService<PNLSettings>(_settings_hub,
us => ImmutableDictionary<uint, SettingsProfile<PNLSettings>>.Empty.AddRange(us.pnl_profiles.ToDictionary(
psp => psp.id,
psp => new SettingsProfile<PNLSettings>(psp.id, psp.name, psp.value, psp.security_criteria))),
(us, psps) => us.with(pnl_profiles: psps.Values.Select(psp => new PNLSettingsProfile(psp.id, psp.name, psp.value, psp.security_criteria)).ToArray()),
us => us.next_pnl_profile_id);
Func<PNLSaveState> get_save_state = () => {
if (last_sorting_criterion == null)
last_sorting_criterion = PNLSettings.Default.sorting_criterion;
var cols = cached_columns.ToArray(c => c.create());
var filters = vm.get_filters_states();
var calc_type = vm.calculation_type;
PNLSettings state = settings;
//var state = settings_manager.instance_settings;
//if (state == null) {
// don't use defaults if the user manipulated the window state outside of the settings dialog (e.g., resized column(s) via drag, or changed sort/filter)
//var defaults = settings_manager.default_settings;
//if (calc_type == defaults.calculation_type &&
// defaults.sorting_criterion.SequenceEqual(last_sorting_criterion) &&
// defaults.grid.columns.SequenceEqual(cols) && defaults.filters.safe_equals(filters)) {
// // the user's current state still matches the defaults (i.e., no outside manipulation was performed); return null so this window will use the latest default settings when restored
// return new PNLSaveState { settings_id = settings_id, settings = null };
//}
//state = defaults;
//}
// assign latest values, as they may have be modified outside of the settings dialog (in which case the settings manager would be out of date)
state = state.with(
last_sorting_criterion,
vm.anchors,
filters,
state.grid.with(columns: cols),
calc_type,
vm.header_row_display);
if (settings_id != 0) {
if (!_settings_hub.current.ladder_profiles.TryGetFirst(lp => lp.id == settings_id, out var result)) {
// profile was deleted
settings_id = 0;
} else {
if (!state.Equals(result.value)) {
// ladder's settings diverged from the profile (this happens when the user changes settings outside of the settings dialog, such as column widths, left panel width, etc.)
settings_id = 0;
}
}
}
return new PNLSaveState { settings_id = settings_id, settings = state };
};
IEnumerable<SparkCommand> commands = new[] {
new SparkCommandCommand("Collapse All", Icons.RightArrow, new DelegateCommand(() => vm.set_all_expanded(false))),
new SparkCommandCommand("Expand All", Icons.DownArrow, new DelegateCommand(() => vm.set_all_expanded(true))),
new SparkCommandCommand("Play", Icons.Play, new DelegateCommand(() => vm.is_paused = false)) {
show_in_context_menu = _pnl_conf.show_pause_resume,
show_in_toolbar = _pnl_conf.show_pause_resume
},
new SparkCommandCommand("Pause", Icons.Pause, new DelegateCommand(() => vm.is_paused = true)) {
show_in_context_menu = _pnl_conf.show_pause_resume,
show_in_toolbar = _pnl_conf.show_pause_resume
},
SparkCommands.window_settings_command(new DialogCommand(open_settings_dialog))
};
bool initialized = false;
var settings_subscription = _settings_hub.subscribe_default_settings().Subscribe(on_user_settings);
vm.OnDisposed += disposing => {
Disposable.dispose(ref settings_subscription);
Disposable.dispose(ref active_settings_dialog);
};
void on_user_settings(UserSettings us) {
if (initialized) {
if (settings_id == 0) {
// not on a profile, don't respond to default settings changes
return;
} else if (us.pnl_profiles.TryGetFirst(p => p.id == settings_id, out var profile)) {
if (!settings.Equals(get_save_state().settings)) {
settings_id = 0;
} else {
settings_subject.OnNext(settings = profile.value);
}
} else {
// profile was deleted; keep the current settings and switch to the 'Local' profile.
settings_id = 0;
}
} else if (_settings_hub.initialized) {
initialized = true;
if (settings_id != 0) {
if (us.pnl_profiles.TryGetFirst(p => p.id == settings_id, out var profile)) {
settings = profile.value;
} else {
settings_id = 0;
}
}
if (settings_id == 0 && settings == null) {
settings = _settings_hub.current.pnl;
}
//vm.load_from(settings);
settings_subject.OnNext(settings);
}
}
IDisposable dispose = Disposable.Create(
settings_subject.OnDispatcher().Subscribe(s => {
vm.load_from(s);
var col_setting_map = s.grid.columns.ToDictionary(c => c.id);
var fallback_family = new FontFamily(s.grid.font_family);
// group
var group_col_setting_map = s.group_columns.ToDictionary(c => c.id);
var group_appearance = s.group_cell_appearance.to_appearance();
// header
var header_col_setting_map = s.header_columns.ToDictionary(c => c.id);
var header_appearance = s.header_cell_appearance.to_appearance();
//
foreach (var c in content._grid.Columns.OfType<PNLColumn>()) {
if (col_setting_map.TryGetValue(c.id, out var cs)) {
c.load_from(cs);
}
if (group_col_setting_map.TryGetValue(c.id, out var gcs)) {
c.load_group_params(
group_appearance.font_size,
gcs.font_weight_override ?? group_appearance.font_weight,
gcs.font_color_override ?? group_appearance.foreground,
group_appearance.font_family);
}
if (header_col_setting_map.TryGetValue(c.id, out var hcs)) {
c.load_header_params(
header_appearance.font_size,
hcs.font_weight_override ?? header_appearance.font_weight,
hcs.font_color_override ?? header_appearance.foreground,
header_appearance.font_family);
}
}
vm.rows_view.set_sort_descriptions(s.sorting_criterion, content._grid.Columns);
CollectionHelper.sync_with_never_duplicate(content._grid.Columns, s.grid.columns, cs => {
throw new InvalidOperationException();
}, c => c.id.ToLower(), cs => cs.id.ToLower());
}),
vm.rows_view
.subscribe_sorting_criterion(sm => content._grid.Columns.TryGetFirst(dc => dc.sort_member_path == sm, out var match) ? match.id : null)
.Subscribe(sc => last_sorting_criterion = sc),
content._grid.Columns.Subscribe((s, e) => {
switch (e.Action) {
case NotifyCollectionChangedAction.Add: add_new(); break;
case NotifyCollectionChangedAction.Remove: remove_old(); break;
case NotifyCollectionChangedAction.Replace:
case NotifyCollectionChangedAction.Move:
remove_old();
add_new();
break;
case NotifyCollectionChangedAction.Reset:
cached_columns = ImmutableList<DIYGrid.ColumnBase>.Empty.AddRange(content._grid.Columns);
break;
}
void add_new() =>
cached_columns = e.NewStartingIndex >= 0 ?
cached_columns.InsertRange(e.NewStartingIndex, e.NewItems.OfType<DIYGrid.ColumnBase>()) :
cached_columns.AddRange(e.NewItems.OfType<DIYGrid.ColumnBase>());
void remove_old() =>
cached_columns = e.OldStartingIndex >= 0 ?
cached_columns.RemoveRange(e.OldStartingIndex, e.OldItems.Count) :
cached_columns.RemoveRange(e.OldItems.OfType<DIYGrid.ColumnBase>());
}),
Disposable.wrap_collection(commands.OfType<IDisposable>()),
vm);
settings_subject.OnNext(settings ?? PNLSettings.Default ?? _settings_hub.current.pnl);
return (content, commands, get_save_state, dispose);
ShellSettingsDialog open_settings_dialog() {
// dialog vm
var dialog_vm = new ProfiledShellSettingsDialogVM<PNLSettings, PNLSettingsVM, PNLSettingsProfile>(
profile_service,
us => us.pnl ?? PNLSettings.Default,
s => new PNLSettingsVM(s),
_ => _,
get_save_state().settings ?? _settings_hub.current.pnl,
UserSettings.FileExtensions.LADDER,
settings_id);
dialog_vm.applied += apply;
// dialog
var dialog = new ShellSettingsDialog { DataContext = dialog_vm };
dialog_vm.RequestClose += on_request_close;
dialog.Closed += on_closed;
Disposable.replace(ref active_settings_dialog, Disposable.Create(destroy_all));
dialog.move_to_mouse(-500, -500);
dialog.Show();
return dialog;
// local methods
void apply(SettingsProfile<PNLSettings> profile) {
settings_id = profile.id;
settings = profile.value;
//settings_subject.OnNext(profile.value);
vm.load_from(profile.value);
}
// idempotent
void destroy_all() {
destroy_window();
destroy_vm();
}
// idempotent
void destroy_window() {
if (dialog == null) return;
try {
var temp = dialog;
dialog = null;
temp.Closed -= on_closed;
temp.Close();
} catch (Exception ex) {
on_error("closing window", ex);
}
}
// idempotent
void destroy_vm() {
if (dialog_vm == null) return;
try {
var temp = dialog_vm;
dialog_vm = null;
temp.applied -= apply;
temp.RequestClose -= on_request_close;
temp.Dispose();
} catch (Exception ex) {
on_error("disposing vm", ex);
}
}
void on_closed(object sender, EventArgs e) => destroy_all();
void on_request_close(object sender, EventArgs e) => destroy_all();
void on_error(string action_desc, Exception error) =>
Log.Ui.warn($"LadderSettingsDialog error {action_desc}: {error}.");
}
},
min_width_offset: SystemParameters.HorizontalScrollBarButtonWidth + 8,
min_height_offset: 60);
}
readonly struct PNLConfig {
public readonly bool
enable_animations,
show_pause_resume;
public PNLConfig(bts.config.PNLOptions options) {
enable_animations = options.enable_animations;
show_pause_resume = options.show_pause_resume;
}
}
}
}
@@ -0,0 +1,78 @@
using bts.utils;
using mbts.expression;
using mbts.ui;
using ProtoBuf;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using static bts.shell.pnl.PNLView;
namespace bts.shell.pnl {
interface IPNLAnimationTrigger<T> {
IReadOnlyDictionary<Columns, PNLCellAppearance> trigger(T value);
}
readonly struct PNLAnimationTrigger<T> : IPNLAnimationTrigger<T> {
public readonly Func<T, bool> predicate;
public readonly IReadOnlyDictionary<Columns, PNLCellAppearance> animations;
public PNLAnimationTrigger(Func<T, bool> predicate, IReadOnlyDictionary<Columns, PNLCellAppearance> animations) {
this.predicate = predicate;
this.animations = animations;
}
public IReadOnlyDictionary<Columns, PNLCellAppearance> trigger(T value) =>
predicate(value) ? animations : DictionaryHelper.empty<Columns, PNLCellAppearance>();
}
[ProtoContract]
class PNLAnimationTriggerData : IEquatable<PNLAnimationTriggerData> {
[ProtoMember(1)]
public string expression { get; set; }
[ProtoMember(2)]
public PNLAnimation[] animations { get; set; }
public IPNLAnimationTrigger<T> to_trigger<T>() =>
new PNLAnimationTrigger<T>(
CoreExpressionEvaluator<T>.try_create(expression, out var eval) ?
eval.apply : new Func<T, bool>(_ => true),
animations.ToDictionary(a => a.column, a => a.to_appearance()));
public bool Equals(PNLAnimationTriggerData other) =>
other != null &&
expression == other.expression &&
EqualityComparer.safe_sequence_equals(animations, other.animations);
public override bool Equals(object obj) =>
Equals(obj as PNLAnimationTriggerData);
public override int GetHashCode() =>
73451;
}
[ProtoContract]
class PNLAnimation : IEquatable<PNLAnimation> {
[ProtoMember(1)]
public Columns column { get; set; }
[ProtoMember(2)]
public int? font_weight { get; set; }
[ProtoMember(3)]
public uint? foreground { get; set; }
[ProtoMember(4)]
public uint? background { get; set; }
public PNLCellAppearance to_appearance() =>
new PNLCellAppearance(
foreground?.argb_color(),
background?.argb_color(),
font_weight.HasValue ? FontWeight.FromOpenTypeWeight(font_weight.Value) : (FontWeight?)null);
public bool Equals(PNLAnimation other) =>
other != null &&
column == other.column &&
font_weight == other.font_weight &&
foreground == other.foreground &&
background == other.background;
}
}
@@ -0,0 +1,87 @@
using bts.proto;
using mbts;
using mbts.ui;
using mbts.ui.utility.text;
using ProtoBuf;
using System;
using System.Windows;
using System.Windows.Media;
using FontFamily = System.Windows.Media.FontFamily;
namespace bts.shell.pnl {
readonly struct PNLCellAppearance : IEquatable<PNLCellAppearance> {
public readonly FontWeight? font_weight;
public readonly Color? foreground, background;
public readonly double font_size;
public readonly FontFamily font_family;
public PNLCellAppearance(Color? foreground = null, Color? background = null, FontWeight? font_weight = null, FontFamily font_family = null, double font_size = double.NaN) {
this.foreground = foreground;
this.background = background;
this.font_weight = font_weight;
this.font_family = font_family;
this.font_size = font_size;
}
/// <summary>
/// Uses the specified appearance to replace null/absent values.
/// </summary>
public PNLCellAppearance merge(PNLCellAppearance other) =>
new PNLCellAppearance(
foreground ?? other.foreground,
background ?? other.background,
font_weight ?? other.font_weight,
font_family ?? other.font_family,
double.IsNaN(font_size) || font_size == 0 ? other.font_size : font_size);
public bool Equals(PNLCellAppearance other) =>
font_weight == other.font_weight &&
foreground == other.foreground &&
background == other.background &&
font_size.Equals(other.font_size) &&
Equals(font_family, other.font_family);
}
[ProtoContract]
class PNLCellAppearanceProto : IEquatable<PNLCellAppearanceProto> {
[ProtoMember(1)]
public FontParamsProto font_params { get; set; }
[ProtoMember(2)]
public uint? foreground { get; set; }
[ProtoMember(3)]
public uint? background { get; set; }
public static PNLCellAppearanceProto from_appearance(PNLCellAppearance cell_appearance) =>
new PNLCellAppearanceProto {
font_params = new FontParamsProto {
font_family = cell_appearance.font_family?.name(),
font_color = cell_appearance.foreground?.rgb_color() ?? 0,
font_size = cell_appearance.font_size,
font_weight = (cell_appearance.font_weight ?? FontWeights.Normal).ToOpenTypeWeight()
},
background = cell_appearance.background?.rgb_color()
};
public PNLCellAppearance to_appearance() {
var font = font_params?.to_wpf_params();
return new PNLCellAppearance(
foreground?.rgb_color(),
background?.argb_color(),
font?.weight,
font?.family,
font?.size ?? double.NaN);
}
public bool Equals(PNLCellAppearanceProto other) =>
other != null &&
foreground == other.foreground &&
background == other.background &&
font_params.safe_equals(other.font_params);
public override bool Equals(object obj) =>
Equals(obj as PNLCellAppearanceProto);
public override int GetHashCode() =>
17437;
}
}
@@ -0,0 +1,328 @@
using bts.collections;
using bts.proto;
using bts.utils;
using mbts;
using mbts.spark;
using mbts.ui;
using mbts.ui.controls;
using mbts.ui.settings;
using mbts.ui.shell;
using mbts.ui.utility;
using mbts.ui.utility.text;
using mbts.utils;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Reactive.Linq;
using System.Windows;
using System.Windows.Media;
using FontFamily = System.Windows.Media.FontFamily;
namespace bts.shell.pnl {
class PNLCellAppearanceParamsVM : DisposableViewModelBase, IValueFactoryResultChanged<PNLCellAppearance> {
static readonly IReadOnlyList<Func<PNLCellAppearanceParamsVM, INotifyPropertyChanged>> _observable_component_getters =
ObjectSnapshot.property_delegates_of_type<PNLCellAppearanceParamsVM, INotifyPropertyChanged>()
.ToArray();
readonly DeferrableActionWrapper _update;
FontWeight _font_weight;
FontFamily _font_family;
double _font_size = double.NaN;
public PNLCellAppearanceParamsVM() {
_update = new DeferrableActionWrapper(() => result_changed?.Invoke());
_dispose = ObjectSnapshot.connect_property_changed(_observable_component_getters.Select(g => g(this)).Append(this), (s, e) => _update.execute());
}
public event Action result_changed;
public ColorPickerPopupVM background { get; } = new ColorPickerPopupVM { bind_opacity_to_alpha = true, opacity = 1 };
public ColorPickerPopupVM font_color { get; } = new ColorPickerPopupVM();
public FontWeight font_weight { get => _font_weight; set => Set(ref _font_weight, value); }
public FontFamily font_family { get => _font_family; set => Set(ref _font_family, value); }
public double font_size { get => _font_size; set => Set(ref _font_size, value); }
public PNLCellAppearance create() =>
new PNLCellAppearance(font_color.color, background.color, font_weight, font_family, font_size);
public void load_from(PNLCellAppearance appearance) {
using (_update.defer_execution()) {
if (appearance.background.HasValue)
background.color = appearance.background.Value;
if (appearance.font_family != null)
font_family = appearance.font_family;
if (appearance.font_weight.HasValue)
font_weight = appearance.font_weight.Value;
if (appearance.font_size > 0)
font_size = appearance.font_size;
}
}
}
class PNLColumnParamsVM : DisposableViewModelBase, IResultChanged {
static readonly IReadOnlyList<Func<PNLColumnParamsVM, INotifyPropertyChanged>> _observable_component_getters =
ObjectSnapshot.property_delegates_of_type<PNLColumnParamsVM, INotifyPropertyChanged>(p => !typeof(IResultChanged).IsAssignableFrom(p.PropertyType))
.ToArray();
static readonly IReadOnlyList<Func<PNLColumnParamsVM, IResultChanged>> _result_changed_component_getters =
ObjectSnapshot.property_delegates_of_type<PNLColumnParamsVM, IResultChanged>()
.ToArray();
readonly DeferrableActionWrapper _update;
bool
_is_background_overridden,
_is_font_color_overridden,
_is_font_weight_overridden,
_is_font_family_overridden,
_is_font_size_overridden;
FontWeight _font_weight_override;
FontFamily _font_family_override;
double _font_size_override = double.NaN;
public PNLColumnParamsVM() {
_update = new DeferrableActionWrapper(() => result_changed?.Invoke());
_dispose = Disposable.Create(
ObjectSnapshot.connect_property_changed(_observable_component_getters.Select(g => g(this)).Append(this), (s, e) => _update.execute()),
ObjectSnapshot.connect_result_changed(_result_changed_component_getters.Select(g => g(this)), _update.execute));
}
public event Action result_changed;
// background
public bool is_background_overridden { get => _is_background_overridden; set => Set(ref _is_background_overridden, value); }
public ColorPickerPopupVM background_override { get; } = new ColorPickerPopupVM {
bind_opacity_to_alpha = true,
opacity = 1
};
// font color
public bool is_font_color_overridden { get => _is_font_color_overridden; set => Set(ref _is_font_color_overridden, value); }
public ColorPickerPopupVM font_color_override { get; } = new ColorPickerPopupVM();
// font weight
public bool is_font_weight_overridden { get => _is_font_weight_overridden; set => Set(ref _is_font_weight_overridden, value); }
public FontWeight font_weight_override { get => _font_weight_override; set => Set(ref _font_weight_override, value); }
// font family
public bool is_font_family_overriden { get => _is_font_family_overridden; set => Set(ref _is_font_family_overridden, value); }
public FontFamily font_family_override { get => _font_family_override; set => Set(ref _font_family_override, value); }
// font size
public bool is_font_size_overriden { get => _is_font_size_overridden; set => Set(ref _is_font_size_overridden, value); }
public double font_size_override { get => _font_size_override; set => Set(ref _font_size_override, value); }
public GridColumnSettings apply_to(GridColumnSettings target) =>
target.with(
font_weight_override: new NullableFontWeightProto(is_font_weight_overridden ? font_weight_override : (FontWeight?)null),
font_color_override: new NullableColorProto(is_font_color_overridden ? font_color_override.color : (Color?)null),
background_override: new NullableColorProto(is_background_overridden ? background_override.color : (Color?)null));
public void load(GridColumnSettings settings) {
if (settings != null) {
using (_update.defer_execution())
load(settings.font_weight_override, settings.font_color_override, settings.background_override);
}
}
public void load(FontParamsProto font_params) {
using (_update.defer_execution()) {
if (font_params == null)
return;
// font weight
is_font_weight_overridden = font_params.font_weight > 0;
if (is_font_weight_overridden)
font_weight_override = FontWeight.FromOpenTypeWeight(font_params.font_weight);
// font color
is_font_color_overridden = font_params.font_color > 0;
if (is_font_color_overridden)
font_color_override.color = font_params.font_color.rgb_color();
// font family
if (!string.IsNullOrEmpty(font_params.font_family)) {
font_family_override = new FontFamily(font_params.font_family);
}
if (!double.IsNaN(font_params.font_size) && font_params.font_size > 0) {
font_size_override = font_params.font_size;
} else {
font_size_override = double.NaN;
}
}
}
public void load(PNLAnimation animation) {
if (animation != null) {
using (_update.defer_execution()) {
load(
animation.font_weight.HasValue ? FontWeight.FromOpenTypeWeight(animation.font_weight.Value) : (FontWeight?)null,
animation.foreground?.argb_color(),
animation.background?.argb_color());
}
}
}
public void load(FontWeight? font_weight, Color? font_color, Color? background) {
using (_update.defer_execution()) {
// font weight
is_font_weight_overridden = font_weight.HasValue;
if (is_font_weight_overridden)
font_weight_override = font_weight.Value;
// font color
is_font_color_overridden = font_color.HasValue;
if (is_font_color_overridden)
font_color_override.color = font_color.Value;
// background
is_background_overridden = background.HasValue;
if (background.HasValue)
background_override.color = background.Value;
}
}
public PNLAnimation to_animation(PNLView.Columns key) =>
new PNLAnimation {
column = key,
foreground = is_font_color_overridden ? font_color_override.color.rgb_color() : (uint?)null,
background = is_background_overridden ? background_override.color.rgb_color() : (uint?)null,
font_weight = is_font_weight_overridden ? font_weight_override.ToOpenTypeWeight() : (int?)null
};
public PNLCellAppearanceProto to_appearance_proto() =>
new PNLCellAppearanceProto {
background = is_background_overridden ? background_override.color.rgb_color() : (uint?)null,
foreground = is_font_color_overridden ? font_color_override.color.rgb_color() : (uint?)null,
font_params = to_font_params()
};
public FontParamsProto to_font_params() =>
new FontParamsProto {
font_color = is_font_color_overridden ? font_color_override.color.rgb_color() : 0,
font_weight = is_font_weight_overridden ? font_weight_override.ToOpenTypeWeight() : 0,
font_family = font_family_override?.name(),
font_size = font_size_override
};
}
class PNLColumnVM : DisposableViewModelBase, ISimpleColumnVM {
static readonly Func<PNLColumnVM, Action, IDisposable> _observe_changes_del = ObjectSnapshot.auto_change_observer_factory<PNLColumnVM>();
readonly SimpleGridSettingsVM _owner;
readonly PNLView.Columns _pnl_id;
readonly DeferrableActionWrapper _update;
object _selected_item;
string _display_name;
bool _is_visible;
double _width;
HorizontalAlignment _horizontal_alignment;
public PNLColumnVM(SimpleGridSettingsVM owner, PNLView.Columns id) {
_owner = owner;
_pnl_id = id;
_update = new DeferrableActionWrapper(() => result_changed?.Invoke());
_dispose = Disposable.Create(
_observe_changes_del(this, _update.execute),
ObservableAutoWrapper.connect_item_hooks(rule_columns, c => ObjectSnapshot.connect_property_changed(c, on_prop_changed)));
void on_prop_changed(object sender, PropertyChangedEventArgs e) =>
_update.execute();
}
public event Action result_changed;
public PNLColumnParamsVM default_column_vm { get; } = new PNLColumnParamsVM();
public PNLColumnParamsVM group_column_vm { get; } = new PNLColumnParamsVM();
public PNLColumnParamsVM header_column_vm { get; } = new PNLColumnParamsVM();
//
public ObservableCollection<PNLRuleColumnVM> rule_columns { get; } = new ObservableCollection<PNLRuleColumnVM>();
public object selected_item {
get => _selected_item;
set {
if (value != null)
Set(ref _selected_item, value);
}
}
public string id { get => _pnl_id.ToString(); set => Log.assert(value == _pnl_id.ToString()); }
public string display_name { get => _display_name; set => Set(ref _display_name, value); }
public double width { get => _width; set => Set(ref _width, value); }
public bool is_visible { get => _is_visible; set => Set(ref _is_visible, value); }
public bool is_font_color_overridden {
get => default_column_vm.is_font_color_overridden;
set => default_column_vm.is_font_color_overridden = value;
}
public HorizontalAlignment horizontal_alignment { get => _horizontal_alignment; set => Set(ref _horizontal_alignment, value); }
public ColorPickerPopupVM font_color_override => default_column_vm.font_color_override;
public bool is_font_weight_overridden { get => default_column_vm.is_font_weight_overridden; set => default_column_vm.is_font_weight_overridden = value; }
public FontWeight font_weight_override { get => default_column_vm.font_weight_override; set => default_column_vm.font_weight_override = value; }
IEnumerable<INotifyPropertyChanged> _observable_components =>
_observable_component_getters.Select(g => g(this)).Append(this);
public void apply_flags(GridColumnSettingsFeatureFlags flags) { }
public GridColumnSettings create() {
var result = new GridColumnSettings(id: id, display_name: display_name, is_visible: is_visible, horizontal_alignment: horizontal_alignment, width: width);
result = default_column_vm.apply_to(result);
return result;
}
public void load_from(GridColumnSettings value) {
using (_update.defer_execution()) {
_assert_id(value.id);
default_column_vm.load(value);
display_name = value.display_name;
width = value.width;
is_visible = value.is_visible;
horizontal_alignment = value.horizontal_alignment;
}
}
public void load_from(GridColumnSettings value, IReadOnlyDictionary<string, PNLAnimation> triggers, GridColumnSettings group, IReadOnlyDictionary<string, PNLAnimation> group_triggers, IReadOnlyDictionary<string, PNLAnimation> header_triggers, GridColumnSettings header) {
using (_update.defer_execution()) {
_assert_id(value.id);
load_from(value);
if (group != null)
group_column_vm.load(group);
if (header != null)
header_column_vm.load(header);
rule_columns.SetItems(triggers.Select(kv => {
var rule_col = new PNLRuleColumnVM(kv.Key);
rule_col.load_from(kv.Value, group_triggers.GetValueOrDefault(kv.Key), header_triggers.GetValueOrDefault(kv.Key));
return rule_col;
}));
}
}
public IEnumerable<(string expression, PNLAnimation row, PNLAnimation group, PNLAnimation header)> get_triggers() {
foreach (var col in rule_columns)
yield return (col.rule_text, col.row_params.to_animation(_pnl_id), col.group_params.to_animation(_pnl_id), col.header_params.to_animation(_pnl_id));
}
void _assert_id(string id) =>
Log.assert(id == _pnl_id.ToString(), $"PNLColumn id string '{id}' does not match the expected {typeof(PNLView.Columns)} key '{_pnl_id}.'");
public override string ToString() => "Default";
}
class PNLRuleColumnVM : ViewModelBaseWithValidation, IEquatable<PNLRuleColumnVM> {
public PNLRuleColumnVM(string rule_text) =>
this.rule_text = rule_text;
public string rule_text { get; }
public PNLColumnParamsVM row_params { get; } = new PNLColumnParamsVM();
public PNLColumnParamsVM group_params { get; } = new PNLColumnParamsVM();
public PNLColumnParamsVM header_params { get; } = new PNLColumnParamsVM();
public void load_from(PNLAnimation row, PNLAnimation group, PNLAnimation header) {
row_params.load(row);
group_params.load(group);
header_params.load(header);
}
public bool Equals(PNLRuleColumnVM other) =>
other != null && rule_text == other.rule_text;
public override bool Equals(object obj) => Equals(obj as PNLRuleColumnVM);
public override int GetHashCode() => rule_text.GetHashCode();
public override string ToString() => rule_text;
}
}
@@ -0,0 +1,282 @@
using bts.api;
using mbts;
using mbts.spark;
using mbts.spark.filters;
using ProtoBuf;
using System;
using System.Linq;
namespace bts.shell.pnl {
[ProtoContract]
class FilterStates : IEquatable<FilterStates> {
// account
[ProtoMember(1)]
uint[] _account;
public uint[] account {
get => _account ?? Array.Empty<uint>();
set => _account = value;
}
// exchange
[ProtoMember(2)]
Venue[] _exchange;
public Venue[] exchange {
get => _exchange ?? Array.Empty<Venue>();
set => _exchange = value;
}
// symbol
[ProtoMember(3)]
string[] _symbol;
public string[] symbol {
get => _symbol ?? Array.Empty<string>();
set => _symbol = value;
}
// security
[ProtoMember(4)]
public string security { get; set; }
// security type
[ProtoMember(5)]
Security.ContractOneofCase[] _security_type;
public Security.ContractOneofCase[] security_type {
get => _security_type ?? Array.Empty<Security.ContractOneofCase>();
set => _security_type = value;
}
/// sod
[ProtoMember(6)]
public DoubleMinMaxState sod_pnl { get; set; }
// sod long
[ProtoMember(7)]
public DoubleMinMaxState sod_long_pos { get; set; }
[ProtoMember(8)]
public DoubleMinMaxState sod_long_avg_price { get; set; }
[ProtoMember(9)]
public DoubleMinMaxState sod_long_avg_price_delta { get; set; }
// sod short
[ProtoMember(10)]
public DoubleMinMaxState sod_short_pos { get; set; }
[ProtoMember(11)]
public DoubleMinMaxState sod_short_avg_price { get; set; }
[ProtoMember(12)]
public DoubleMinMaxState sod_short_avg_price_delta { get; set; }
// sod net
[ProtoMember(13)]
public DoubleMinMaxState sod_net_pos { get; set; }
[ProtoMember(14)]
public DoubleMinMaxState sod_net_avg_price_delta { get; set; }
/// day
[ProtoMember(15)]
public DoubleMinMaxState day_pnl { get; set; }
// day long
[ProtoMember(16)]
public DoubleMinMaxState day_long_pos { get; set; }
[ProtoMember(17)]
public DoubleMinMaxState day_long_avg_price { get; set; }
[ProtoMember(18)]
public DoubleMinMaxState day_long_avg_price_delta { get; set; }
// day short
[ProtoMember(19)]
public DoubleMinMaxState day_short_pos { get; set; }
[ProtoMember(20)]
public DoubleMinMaxState day_short_avg_price { get; set; }
[ProtoMember(21)]
public DoubleMinMaxState day_short_avg_price_delta { get; set; }
// day net
[ProtoMember(22)]
public DoubleMinMaxState day_net_pos { get; set; }
[ProtoMember(23)]
public DoubleMinMaxState day_net_avg_price_delta { get; set; }
/// total
[ProtoMember(24)]
public DoubleMinMaxState total_pnl { get; set; }
// total long
[ProtoMember(25)]
public DoubleMinMaxState total_long_pos { get; set; }
[ProtoMember(26)]
public DoubleMinMaxState total_long_avg_price { get; set; }
[ProtoMember(27)]
public DoubleMinMaxState total_long_avg_price_delta { get; set; }
// total short
[ProtoMember(28)]
public DoubleMinMaxState total_short_pos { get; set; }
[ProtoMember(29)]
public DoubleMinMaxState total_short_avg_price { get; set; }
[ProtoMember(30)]
public DoubleMinMaxState total_short_avg_price_delta { get; set; }
// total net
[ProtoMember(31)]
public DoubleMinMaxState total_net_pos { get; set; }
[ProtoMember(32)]
public DoubleMinMaxState total_net_avg_price_delta { get; set; }
/// market
[ProtoMember(33)]
public DoubleMinMaxState bid_price { get; set; }
[ProtoMember(34)]
public DoubleMinMaxState mid_price { get; set; }
[ProtoMember(35)]
public DoubleMinMaxState ask_price { get; set; }
[ProtoMember(36)]
public DoubleMinMaxState last_price { get; set; }
[ProtoMember(37)]
public DoubleMinMaxState settlement_price { get; set; }
[ProtoMember(38)]
public DoubleMinMaxState pnl_price { get; set; }
/// working (long)
[ProtoMember(39)]
public DoubleMinMaxState working_long_pos { get; set; }
[ProtoMember(40)]
public DoubleMinMaxState working_long_avg_price { get; set; }
[ProtoMember(41)]
public DoubleMinMaxState working_long_avg_price_delta { get; set; }
[ProtoMember(42)]
// working short
public DoubleMinMaxState working_short_pos { get; set; }
[ProtoMember(43)]
public DoubleMinMaxState working_short_avg_price { get; set; }
[ProtoMember(44)]
public DoubleMinMaxState working_short_avg_price_delta { get; set; }
// working net
[ProtoMember(45)]
public DoubleMinMaxState working_net_pos { get; set; }
[ProtoMember(46)]
public DoubleMinMaxState working_net_avg_price_delta { get; set; }
// has day trades
[ProtoMember(47)]
bool[] _has_day_trades { get; set; }
public bool[] has_day_trades {
get => _has_day_trades ?? Array.Empty<bool>();
set => _has_day_trades = value;
}
// cpp
[ProtoMember(48)]
public DoubleMinMaxState cpp { get; set; }
public override bool Equals(object obj) =>
Equals(obj as FilterStates);
public override int GetHashCode() => -113;
public bool Equals(FilterStates other) =>
other != null &&
account.SequenceEqual(other.account) &&
symbol.SequenceEqual(other.symbol) &&
exchange.SequenceEqual(other.exchange) &&
security == other.security &&
security_type.SequenceEqual(other.security_type) &&
/// sod
sod_pnl.safe_equals(other.sod_pnl) &&
sod_long_pos.safe_equals(other.sod_long_pos) &&
sod_long_avg_price.safe_equals(other.sod_long_avg_price) &&
sod_long_avg_price_delta.safe_equals(other.sod_long_avg_price_delta) &&
sod_short_pos.safe_equals(other.sod_short_pos) &&
sod_short_avg_price.safe_equals(other.sod_short_avg_price) &&
sod_short_avg_price_delta.safe_equals(other.sod_short_avg_price_delta) &&
sod_net_pos.safe_equals(other.sod_net_pos) &&
sod_net_avg_price_delta.safe_equals(other.sod_net_avg_price_delta) &&
/// day
day_pnl.safe_equals(other.day_pnl) &&
day_long_pos.safe_equals(other.day_long_pos) &&
day_long_avg_price.safe_equals(other.day_long_avg_price) &&
day_long_avg_price_delta.safe_equals(other.day_long_avg_price_delta) &&
day_short_pos.safe_equals(other.day_short_pos) &&
day_short_avg_price.safe_equals(other.day_short_avg_price) &&
day_short_avg_price_delta.safe_equals(other.day_short_avg_price_delta) &&
day_net_pos.safe_equals(other.day_net_pos) &&
day_net_avg_price_delta.safe_equals(other.day_net_avg_price_delta) &&
/// market
bid_price.safe_equals(other.bid_price) &&
ask_price.safe_equals(other.ask_price) &&
mid_price.safe_equals(other.mid_price) &&
last_price.safe_equals(other.last_price) &&
settlement_price.safe_equals(other.settlement_price) &&
pnl_price.safe_equals(other.pnl_price) &&
/// total
total_pnl.safe_equals(other.total_pnl) &&
total_long_pos.safe_equals(other.total_long_pos) &&
total_long_avg_price.safe_equals(other.total_long_avg_price) &&
total_long_avg_price_delta.safe_equals(other.total_long_avg_price_delta) &&
total_short_pos.safe_equals(other.total_short_pos) &&
total_short_avg_price.safe_equals(other.total_short_avg_price) &&
total_short_avg_price_delta.safe_equals(other.total_short_avg_price_delta) &&
total_net_pos.safe_equals(other.total_net_pos) &&
total_net_avg_price_delta.safe_equals(other.total_net_avg_price_delta) &&
has_day_trades.SequenceEqual(other.has_day_trades) &&
cpp.safe_equals(other.cpp);
}
static class PNLFilterStatesExtensions {
public static Func<PNLRowData, bool> build_predicate(this FilterStates fs) {
var bd = new ColumnPredicateBuilder<PNLRowData>();
bd.add_discrete_values(fs.account, d => d.key.account_id);
bd.add_discrete_values(fs.exchange, d => d.security.exchange_or_default());
bd.add_discrete_values(fs.symbol, d => d.security.symbol_or_default());
bd.add_regex(fs.security, d => d.security.ToString());
bd.add_discrete_values(fs.security_type, d => d.security.HasValue ? d.security.Value.ContractCase : Security.ContractOneofCase.None);
/// sod
bd.add_numeric(fs.sod_pnl, d => d.sod_pnl);
// sod long
bd.add_numeric(fs.sod_long_pos, d => d.sod_long_pos);
bd.add_numeric(fs.sod_long_avg_price, d => d.sod_long_avg_price_raw);
bd.add_numeric(fs.sod_long_avg_price_delta, d => d.sod_long_avg_price_delta_raw);
// sod short
bd.add_numeric(fs.sod_short_pos, d => d.sod_short_pos);
bd.add_numeric(fs.sod_short_avg_price, d => d.sod_short_avg_price_raw);
bd.add_numeric(fs.sod_short_avg_price_delta, d => d.sod_short_avg_price_delta_raw);
// sod net
bd.add_numeric(fs.sod_net_pos, d => d.sod_net_pos);
bd.add_numeric(fs.sod_net_avg_price_delta, d => d.sod_net_avg_price_delta_raw);
/// day
bd.add_numeric(fs.day_pnl, d => d.day_pnl);
// day long
bd.add_numeric(fs.day_long_pos, d => d.day_long_pos);
bd.add_numeric(fs.day_long_avg_price, d => d.day_long_avg_price_raw);
bd.add_numeric(fs.day_long_avg_price_delta, d => d.day_long_avg_price_delta_raw);
// day short
bd.add_numeric(fs.day_short_pos, d => d.day_short_pos);
bd.add_numeric(fs.day_short_avg_price, d => d.day_short_avg_price_raw);
bd.add_numeric(fs.day_short_avg_price_delta, d => d.day_short_avg_price_delta_raw);
// day net
bd.add_numeric(fs.day_net_pos, d => d.day_net_pos);
bd.add_numeric(fs.day_net_avg_price_delta, d => d.day_net_avg_price_delta_raw);
/// total
bd.add_numeric(fs.total_pnl, d => d.total_pnl);
// total long
bd.add_numeric(fs.total_long_pos, d => d.total_long_pos);
bd.add_numeric(fs.total_long_avg_price, d => d.total_long_avg_price_raw);
bd.add_numeric(fs.total_long_avg_price_delta, d => d.total_long_avg_price_delta_raw);
// total short
bd.add_numeric(fs.total_short_pos, d => d.total_short_pos);
bd.add_numeric(fs.total_short_avg_price, d => d.total_short_avg_price_raw);
bd.add_numeric(fs.total_short_avg_price_delta, d => d.total_short_avg_price_delta_raw);
// total net
bd.add_numeric(fs.total_net_pos, d => d.total_net_pos);
bd.add_numeric(fs.total_net_avg_price_delta, d => d.total_net_avg_price_delta_raw);
/// market
bd.add_numeric(fs.bid_price, d => d.bid_price);
bd.add_numeric(fs.ask_price, d => d.ask_price);
bd.add_numeric(fs.mid_price, d => d.mid_price);
bd.add_numeric(fs.last_price, d => d.last_price);
bd.add_numeric(fs.settlement_price, d => d.settlement_price);
bd.add_numeric(fs.pnl_price, d => d.pnl_price);
/// working (long)
bd.add_numeric(fs.working_long_pos, d => d.working_long_pos);
bd.add_numeric(fs.working_long_avg_price, d => d.working_long_avg_price_raw);
bd.add_numeric(fs.working_long_avg_price_delta, d => d.working_long_avg_price_delta_raw);
// working short
bd.add_numeric(fs.working_short_pos, d => d.working_short_pos);
bd.add_numeric(fs.working_short_avg_price, d => d.working_short_avg_price_raw);
bd.add_numeric(fs.working_short_avg_price_delta, d => d.working_short_avg_price_delta_raw);
// working net
bd.add_numeric(fs.working_net_pos, d => d.working_net_pos);
bd.add_numeric(fs.working_net_avg_price_delta, d => d.working_net_avg_price_delta_raw);
// misc
bd.add_discrete_values(fs.has_day_trades, d => d.has_day_trades);
bd.add_numeric(fs.cpp, d => d.cpp);
return bd.to_predicate();
}
}
}
@@ -0,0 +1,484 @@
using bts.proto;
using mbts;
using mbts.ui;
using mbts.ui.settings;
using ProtoBuf;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Media;
using static bts.shell.pnl.PNLView;
using static mbts.ui.controls.DIYGrid;
using Headers = mbts.ui.controls.DIYGrid.DefaultColumnHeaders.PNLHeaders;
using Widths = mbts.ui.controls.DIYGrid.DefaultColumnWidths.PNL;
namespace bts.shell.pnl {
[ProtoContract]
class PNLSaveState : IEquatable<PNLSaveState> {
[ProtoMember(1)]
public uint settings_id { get; set; }
[ProtoMember(2)]
public PNLSettings settings { get; set; }
public bool Equals(PNLSaveState other) =>
other != null &&
settings_id == other.settings_id &&
settings.safe_equals(other.settings);
}
[ProtoContract]
class PNLSettings : IEquatable<PNLSettings> {
const double _default_group_font_size = double.NaN;
// bold
const int _default_group_font_weight = 700;
public static readonly IReadOnlyList<PNLHeaderRowDisplay> header_row_display_options =
new[] {
PNLHeaderRowDisplay.Total,
PNLHeaderRowDisplay.SelectedTotal,
PNLHeaderRowDisplay.Total | PNLHeaderRowDisplay.SelectedTotal
};
public static readonly IReadOnlyList<Columns> anchorable_columns =
new[] {
Columns.Account,
Columns.Exchange,
Columns.Symbol,
Columns.Security
};
[ProtoMember(1)]
SortingCriteria[] _sorting_criterion;
[ProtoMember(5)]
Columns[] _anchors;
[ProtoMember(7)]
PNLCellAppearanceProto _group_cell_appearance;
[ProtoMember(8)]
PNLCellAppearanceProto _header_cell_appearance;
// don't store in proto yet (this type may change)
[ProtoMember(9)]
PNLAnimationTriggerData[] _animations;
[ProtoMember(10)]
PNLAnimationTriggerData[] _group_header_animations;
[ProtoMember(11)]
GridColumnSettings[] _group_columns;
[ProtoMember(12)]
GridColumnSettings[] _header_columns;
[ProtoMember(13)]
PNLAnimationTriggerData[] _header_row_animations;
PNLCellAppearanceProto _grid_cell_appearance_backing;
public static PNLSettings Default =>
new PNLSettings {
_sorting_criterion = new[] {
new SortingCriteria(nameof(Columns.Account), true),
new SortingCriteria(nameof(Columns.Security), true)
},
grid = DefaultGridSettings,
calculation_type = PNLCalculationType.Waterfall,
header_row_display = PNLHeaderRowDisplay.Total | PNLHeaderRowDisplay.SelectedTotal,
_anchors = new[] { Columns.Account, Columns.Symbol },
_group_cell_appearance = new PNLCellAppearanceProto {
foreground = Colors.Black.rgb_color(),
font_params = new FontParamsProto {
font_family = GridSettings.Default.font_family,
font_weight = FontWeights.Bold.ToOpenTypeWeight(),
font_size = 11
},
background = Colors.Transparent.rgb_color()
},
_header_cell_appearance = new PNLCellAppearanceProto {
foreground = Colors.Black.rgb_color(),
font_params = new FontParamsProto {
font_family = GridSettings.Default.font_family,
font_weight = FontWeights.Bold.ToOpenTypeWeight(),
font_size = 12
},
background = Colors.Transparent.rgb_color()
}
};
public static GridSettings DefaultGridSettings => GridSettings.Default.with(DefaultColumns.all, DefaultColumns.default_sorting_criterion);
public static fills.FillsSettings DefaultAuxiliaryFills {
get {
var grid = fills.FillsSettings.MiniGridSettings;
if (grid.columns.TryGetFirst(c => c.id == nameof(mbts.ui.spark.FillsView.Columns.Side), out var side_col, out int side_col_pos)) {
var cols = grid.columns.ToList();
cols[side_col_pos] = side_col.with(is_visible: false);
grid = grid.with(cols);
}
var rv = fills.FillsSettings.Default();
rv.grid = grid;
return rv;
}
}
public static IEnumerable<PNLAnimationTriggerData> default_animations {
get {
yield return _trigger($"{nameof(PNLRowData.sod_pnl)} > 0", _animation(foreground: BTSColors.PNL.profit_color, font_weight: FontWeights.Bold, column: Columns.SODPNL));
yield return _trigger($"{nameof(PNLRowData.sod_pnl)} < 0", _animation(foreground: BTSColors.PNL.loss_color, font_weight: FontWeights.Bold, column: Columns.SODPNL));
yield return _trigger($"{nameof(PNLRowData.sod_net_pos)} > 0", _animation(foreground: BTSColors.MarketData.bid_color, column: Columns.SODNetPos));
yield return _trigger($"{nameof(PNLRowData.sod_net_pos)} < 0", _animation(foreground: BTSColors.MarketData.ask_color, column: Columns.SODNetPos));
// day
yield return _trigger($"{nameof(PNLRowData.day_pnl)} > 0", _animation(foreground: BTSColors.PNL.profit_color, font_weight: FontWeights.Bold, column: Columns.DayPNL));
yield return _trigger($"{nameof(PNLRowData.day_pnl)} < 0", _animation(foreground: BTSColors.PNL.loss_color, font_weight: FontWeights.Bold, column: Columns.DayPNL));
yield return _trigger($"{nameof(PNLRowData.day_net_pos)} > 0", _animation(foreground: BTSColors.MarketData.bid_color, column: Columns.DayNetPos));
yield return _trigger($"{nameof(PNLRowData.day_net_pos)} < 0", _animation(foreground: BTSColors.MarketData.ask_color, column: Columns.DayNetPos));
// total
yield return _trigger($"{nameof(PNLRowData.total_pnl)} > 0", _animation(foreground: BTSColors.PNL.profit_color, font_weight: FontWeights.Bold, column: Columns.TotalPNL));
yield return _trigger($"{nameof(PNLRowData.total_pnl)} < 0", _animation(foreground: BTSColors.PNL.loss_color, font_weight: FontWeights.Bold, column: Columns.TotalPNL));
yield return _trigger($"{nameof(PNLRowData.total_net_pos)} > 0", _animation(foreground: BTSColors.MarketData.bid_color, column: Columns.TotalNetPos));
yield return _trigger($"{nameof(PNLRowData.total_net_pos)} < 0", _animation(foreground: BTSColors.MarketData.ask_color, column: Columns.TotalNetPos));
// working
yield return _trigger($"{nameof(PNLRowData.working_net_pos)} > 0", _animation(foreground: BTSColors.MarketData.bid_color, column: Columns.WorkingNetPos));
yield return _trigger($"{nameof(PNLRowData.working_net_pos)} < 0", _animation(foreground: BTSColors.MarketData.ask_color, column: Columns.WorkingNetPos));
}
}
public static IEnumerable<PNLAnimationTriggerData> default_group_header_animations {
get {
yield return _trigger($"{nameof(PNLNormalizedData.sod_pnl)} > 0", _animation(background: BTSColors.PNL.profit_color.with(OpacityLevels.Light), column: Columns.SODPNL));
yield return _trigger($"{nameof(PNLNormalizedData.sod_pnl)} < 0", _animation(background: BTSColors.PNL.loss_color.with(OpacityLevels.Light), column: Columns.SODPNL));
yield return _trigger($"{nameof(PNLNormalizedData.sod_net_pos)} > 0", _animation(background: BTSColors.MarketData.bid_color.with(OpacityLevels.Medium), column: Columns.SODNetPos));
yield return _trigger($"{nameof(PNLNormalizedData.sod_net_pos)} < 0", _animation(background: BTSColors.MarketData.ask_color.with(OpacityLevels.Medium), column: Columns.SODNetPos));
// day
yield return _trigger($"{nameof(PNLNormalizedData.day_pnl)} > 0", _animation(background: BTSColors.PNL.profit_color.with(OpacityLevels.Light), column: Columns.DayPNL));
yield return _trigger($"{nameof(PNLNormalizedData.day_pnl)} < 0", _animation(background: BTSColors.PNL.loss_color.with(OpacityLevels.Light), column: Columns.DayPNL));
yield return _trigger($"{nameof(PNLNormalizedData.day_net_pos)} > 0", _animation(background: BTSColors.MarketData.bid_color.with(OpacityLevels.Medium), column: Columns.DayNetPos));
yield return _trigger($"{nameof(PNLNormalizedData.day_net_pos)} < 0", _animation(background: BTSColors.MarketData.ask_color.with(OpacityLevels.Medium), column: Columns.DayNetPos));
// total
yield return _trigger($"{nameof(PNLNormalizedData.total_pnl)} > 0", _animation(background: BTSColors.PNL.profit_color.with(OpacityLevels.Light), column: Columns.TotalPNL));
yield return _trigger($"{nameof(PNLNormalizedData.total_pnl)} < 0", _animation(background: BTSColors.PNL.loss_color.with(OpacityLevels.Light), column: Columns.TotalPNL));
yield return _trigger($"{nameof(PNLNormalizedData.total_net_pos)} > 0", _animation(background: BTSColors.MarketData.bid_color.with(OpacityLevels.Medium), column: Columns.TotalNetPos));
yield return _trigger($"{nameof(PNLNormalizedData.total_net_pos)} < 0", _animation(background: BTSColors.MarketData.ask_color.with(OpacityLevels.Medium), column: Columns.TotalNetPos));
// working
yield return _trigger($"{nameof(PNLNormalizedData.working_net_pos)} > 0", _animation(background: BTSColors.MarketData.bid_color.with(OpacityLevels.Medium), column: Columns.WorkingNetPos));
yield return _trigger($"{nameof(PNLNormalizedData.working_net_pos)} < 0", _animation(background: BTSColors.MarketData.ask_color.with(OpacityLevels.Medium), column: Columns.WorkingNetPos));
}
}
public static IEnumerable<PNLAnimationTriggerData> default_header_row_animations {
get {
yield return _trigger($"{nameof(PNLNormalizedData.sod_pnl)} > 0", _animation(background: BTSColors.PNL.profit_color.with(OpacityLevels.Light), column: Columns.SODPNL));
yield return _trigger($"{nameof(PNLNormalizedData.sod_pnl)} < 0", _animation(background: BTSColors.PNL.loss_color.with(OpacityLevels.Light), column: Columns.SODPNL));
yield return _trigger($"{nameof(PNLNormalizedData.sod_net_pos)} > 0", _animation(background: BTSColors.MarketData.bid_color.with(OpacityLevels.Medium), column: Columns.SODNetPos));
yield return _trigger($"{nameof(PNLNormalizedData.sod_net_pos)} < 0", _animation(background: BTSColors.MarketData.ask_color.with(OpacityLevels.Medium), column: Columns.SODNetPos));
// day
yield return _trigger($"{nameof(PNLNormalizedData.day_pnl)} > 0", _animation(background: BTSColors.PNL.profit_color.with(OpacityLevels.Light), column: Columns.DayPNL));
yield return _trigger($"{nameof(PNLNormalizedData.day_pnl)} < 0", _animation(background: BTSColors.PNL.loss_color.with(OpacityLevels.Light), column: Columns.DayPNL));
yield return _trigger($"{nameof(PNLNormalizedData.day_net_pos)} > 0", _animation(background: BTSColors.MarketData.bid_color.with(OpacityLevels.Medium), column: Columns.DayNetPos));
yield return _trigger($"{nameof(PNLNormalizedData.day_net_pos)} < 0", _animation(background: BTSColors.MarketData.ask_color.with(OpacityLevels.Medium), column: Columns.DayNetPos));
// total
yield return _trigger($"{nameof(PNLNormalizedData.total_pnl)} > 0", _animation(background: BTSColors.PNL.profit_color.with(OpacityLevels.Light), column: Columns.TotalPNL));
yield return _trigger($"{nameof(PNLNormalizedData.total_pnl)} < 0", _animation(background: BTSColors.PNL.loss_color.with(OpacityLevels.Light), column: Columns.TotalPNL));
yield return _trigger($"{nameof(PNLNormalizedData.total_net_pos)} > 0", _animation(background: BTSColors.MarketData.bid_color.with(OpacityLevels.Medium), column: Columns.TotalNetPos));
yield return _trigger($"{nameof(PNLNormalizedData.total_net_pos)} < 0", _animation(background: BTSColors.MarketData.ask_color.with(OpacityLevels.Medium), column: Columns.TotalNetPos));
// working
yield return _trigger($"{nameof(PNLNormalizedData.working_net_pos)} > 0", _animation(background: BTSColors.MarketData.bid_color.with(OpacityLevels.Medium), column: Columns.WorkingNetPos));
yield return _trigger($"{nameof(PNLNormalizedData.working_net_pos)} < 0", _animation(background: BTSColors.MarketData.ask_color.with(OpacityLevels.Medium), column: Columns.WorkingNetPos));
}
}
[ProtoMember(2)]
public FilterStates filters { get; set; }
[ProtoMember(3)]
public PNLCalculationType calculation_type { get; set; }
[ProtoMember(4)]
public GridSettings grid { get; set; }
[ProtoMember(6)]
public PNLHeaderRowDisplay header_row_display { get; set; }
public PNLCellAppearance grid_defaults =>
new PNLCellAppearance(
font_family: new FontFamily(grid.font_family),
font_size: grid.font_size,
font_weight: grid.font_weight,
background: grid.background,
foreground: grid.font_color.rgb_color());
public PNLCellAppearanceProto group_cell_appearance {
get => _group_cell_appearance ?? _grid_cell_appearance;
set => _group_cell_appearance = value;
}
public PNLCellAppearanceProto header_cell_appearance {
get => _header_cell_appearance ?? _grid_cell_appearance;
set => _header_cell_appearance = value;
}
public IReadOnlyList<SortingCriteria> sorting_criterion => _sorting_criterion ?? (_sorting_criterion = DefaultColumns.default_sorting_criterion.ToArray());
public IReadOnlyList<Columns> anchors => _anchors ?? (_anchors = Array.Empty<Columns>());
public IReadOnlyList<PNLAnimationTriggerData> animations => _animations ?? (_animations = default_animations.ToArray());
public IReadOnlyList<PNLAnimationTriggerData> group_header_animations => _group_header_animations ?? (_group_header_animations = default_group_header_animations.ToArray());
public IReadOnlyList<PNLAnimationTriggerData> header_row_animations => _header_row_animations ?? (_header_row_animations = default_header_row_animations.ToArray());
public IReadOnlyList<GridColumnSettings> group_columns => _group_columns ?? (_group_columns = DefaultColumns.group_columns.ToArray());
public IReadOnlyList<GridColumnSettings> header_columns => _header_columns ?? (_header_columns = DefaultColumns.header_columns.ToArray());
PNLCellAppearanceProto _grid_cell_appearance => _grid_cell_appearance_backing ?? (
_grid_cell_appearance_backing = new PNLCellAppearanceProto {
background = grid.background.rgb_color(),
foreground = grid.font_color,
font_params = new FontParamsProto {
//font_color = grid.font_color,
font_family = grid.font_family,
font_weight = grid.font_weight.ToOpenTypeWeight(),
font_size = grid.font_size
}
});
public PNLSettings with(
IEnumerable<SortingCriteria> sorting_criterion = null,
IEnumerable<Columns> anchors = null,
FilterStates filters = null,
GridSettings grid = null,
PNLCalculationType? calculation_type = null,
PNLHeaderRowDisplay? header_row_display = null,
IEnumerable<GridColumnSettings> group_columns = null,
IEnumerable<PNLAnimationTriggerData> animations = null,
IEnumerable<PNLAnimationTriggerData> group_header_animations = null,
IEnumerable<PNLAnimationTriggerData> header_row_animations = null,
IEnumerable<GridColumnSettings> header_columns = null) =>
new PNLSettings {
_sorting_criterion = (sorting_criterion ?? _sorting_criterion.empty_if_null()).ToArray(),
_anchors = (anchors ?? _anchors.empty_if_null()).ToArray(),
filters = filters ?? this.filters,
grid = grid ?? this.grid,
calculation_type = calculation_type ?? this.calculation_type,
header_row_display = header_row_display ?? this.header_row_display,
_group_columns = group_columns?.ToArray() ?? _group_columns,
_header_columns = header_columns?.ToArray() ?? _header_columns,
_animations = animations?.ToArray() ?? _animations,
_group_header_animations = group_header_animations?.ToArray() ?? _group_header_animations,
_header_row_animations = header_row_animations?.ToArray() ?? _header_row_animations,
_group_cell_appearance = _group_cell_appearance,
_header_cell_appearance = _header_cell_appearance
};
public bool Equals_orig(PNLSettings other) =>
other != null &&
calculation_type == other.calculation_type &&
filters.safe_equals(other.filters) &&
header_row_display == other.header_row_display &&
header_cell_appearance.safe_equals(other.header_cell_appearance) &&
group_cell_appearance.safe_equals(other.group_cell_appearance) &&
anchors.SequenceEqual(other.anchors) &&
group_columns.SequenceEqual(other.group_columns) &&
animations.SequenceEqual(other.animations) &&
group_header_animations.SequenceEqual(other.group_header_animations) &&
grid.safe_equals(other.grid);
public bool Equals(PNLSettings other) {
if (other == null)
return false;
if (calculation_type != other.calculation_type)
return false;
if (!filters.safe_equals(other.filters))
return false;
if (!header_cell_appearance.safe_equals(other.header_cell_appearance))
return false;
if (!group_cell_appearance.safe_equals(other.group_cell_appearance))
return false;
if (!group_columns.SequenceEqual(other.group_columns))
return false;
if (!anchors.SequenceEqual(other.anchors))
return false;
if (!animations.SequenceEqual(other.animations))
return false;
if (!Equals_orig(other))
return false;
return true;
}
public override bool Equals(object obj) =>
Equals(obj as PNLSettings);
public override int GetHashCode() => 583;
static PNLAnimationTriggerData _trigger(string expression, params PNLAnimation[] animations) =>
new PNLAnimationTriggerData {
expression = expression,
animations = animations
};
static PNLAnimation _animation(
Columns column,
Color? foreground = null,
Color? background = null,
FontWeight? font_weight = null) =>
new PNLAnimation {
column = column,
foreground = foreground?.rgb_color(),
background = background?.rgb_color(),
font_weight = font_weight?.ToOpenTypeWeight()
};
static class DefaultColumns {
public static GridColumnSettings account => new GridColumnSettings(nameof(Columns.Account), DefaultColumnHeaders.Account, width: Widths.Account);
public static GridColumnSettings exchange => new GridColumnSettings(nameof(Columns.Exchange), DefaultColumnHeaders.Exchange, false, width: Widths.Exchange);
public static GridColumnSettings symbol => new GridColumnSettings(nameof(Columns.Symbol), DefaultColumnHeaders.Symbol, width: Widths.Symbol);
public static GridColumnSettings security => new GridColumnSettings(nameof(Columns.Security), DefaultColumnHeaders.Security, width: Widths.Security);
public static GridColumnSettings security_type => new GridColumnSettings(nameof(Columns.SecurityType), Headers.SecurityType, width: Widths.SecurityType);
// sod
public static GridColumnSettings sod_pnl => new GridColumnSettings(nameof(Columns.SODPNL), Headers.SOD.PNL, false, width: Widths.PriceBig, horizontal_alignment: HorizontalAlignment.Right);
public static GridColumnSettings sod_long_pos => new GridColumnSettings(nameof(Columns.SODLongPos), Headers.SOD.Long, false, width: Widths.Qty);
public static GridColumnSettings sod_long_avg_price => new GridColumnSettings(nameof(Columns.SODLongAvgPrice), Headers.SOD.LongAvg, false, width: Widths.Price, horizontal_alignment: HorizontalAlignment.Right);
public static GridColumnSettings sod_long_avg_price_delta => new GridColumnSettings(nameof(Columns.SODLongAvgPriceDelta), Headers.SOD.LongAvgDelta, false, width: Widths.Price, horizontal_alignment: HorizontalAlignment.Right);
public static GridColumnSettings sod_short_pos => new GridColumnSettings(nameof(Columns.SODShortPos), Headers.SOD.Short, false, width: Widths.Qty);
public static GridColumnSettings sod_short_avg_price => new GridColumnSettings(nameof(Columns.SODShortAvgPrice), Headers.SOD.ShortAvg, false, width: Widths.Price, horizontal_alignment: HorizontalAlignment.Right);
public static GridColumnSettings sod_short_avg_price_delta => new GridColumnSettings(nameof(Columns.SODShortAvgPriceDelta), Headers.SOD.ShortAvgDelta, false, width: Widths.Price, horizontal_alignment: HorizontalAlignment.Right);
public static GridColumnSettings sod_net_pos => new GridColumnSettings(nameof(Columns.SODNetPos), Headers.SOD.Net, false, width: Widths.Qty);
public static GridColumnSettings sod_net_avg_price_delta => new GridColumnSettings(nameof(Columns.SODNetAvgPriceDelta), Headers.SOD.NetAvgDelta, false, width: Widths.Price, horizontal_alignment: HorizontalAlignment.Right);
// day
public static GridColumnSettings day_pnl => new GridColumnSettings(nameof(Columns.DayPNL), Headers.Day.PNL, width: Widths.PriceBig, horizontal_alignment: HorizontalAlignment.Right);
public static GridColumnSettings day_long_pos => new GridColumnSettings(nameof(Columns.DayLongPos), Headers.Day.Long, false, width: Widths.Qty);
public static GridColumnSettings day_long_avg_price => new GridColumnSettings(nameof(Columns.DayLongAvgPrice), Headers.Day.LongAvg, false, width: Widths.Price, horizontal_alignment: HorizontalAlignment.Right);
public static GridColumnSettings day_long_avg_price_delta => new GridColumnSettings(nameof(Columns.DayLongAvgPriceDelta), Headers.Day.LongAvgDelta, false, width: Widths.Price, horizontal_alignment: HorizontalAlignment.Right);
public static GridColumnSettings day_short_pos => new GridColumnSettings(nameof(Columns.DayShortPos), Headers.Day.Short, false, width: Widths.Qty);
public static GridColumnSettings day_short_avg_price => new GridColumnSettings(nameof(Columns.DayShortAvgPrice), Headers.Day.ShortAvg, false, width: Widths.Price, horizontal_alignment: HorizontalAlignment.Right);
public static GridColumnSettings day_short_avg_price_delta => new GridColumnSettings(nameof(Columns.DayShortAvgPriceDelta), Headers.Day.ShortAvgDelta, false, width: Widths.Price, horizontal_alignment: HorizontalAlignment.Right);
public static GridColumnSettings day_net_pos => new GridColumnSettings(nameof(Columns.DayNetPos), Headers.Day.Net, width: Widths.Qty);
public static GridColumnSettings day_net_avg_price_delta => new GridColumnSettings(nameof(Columns.DayNetAvgPriceDelta), Headers.Day.NetAvgDelta, false, width: Widths.Price, horizontal_alignment: HorizontalAlignment.Right);
// working
public static GridColumnSettings working_long_pos => new GridColumnSettings(nameof(Columns.WorkingLongPos), Headers.Working.Long, false, width: Widths.WorkingQty, font_color_override: BTSColors.MarketData.bid);
public static GridColumnSettings working_long_avg_price => new GridColumnSettings(nameof(Columns.WorkingLongAvgPrice), Headers.Working.LongAvg, false, width: Widths.Qty, horizontal_alignment: HorizontalAlignment.Right);
public static GridColumnSettings working_long_avg_price_delta => new GridColumnSettings(nameof(Columns.WorkingLongAvgPriceDelta), Headers.Working.LongAvgDelta, false, width: Widths.Qty, horizontal_alignment: HorizontalAlignment.Right);
public static GridColumnSettings working_short_pos => new GridColumnSettings(nameof(Columns.WorkingShortPos), Headers.Working.Short, false, width: Widths.WorkingQty, font_color_override: BTSColors.MarketData.ask);
public static GridColumnSettings working_short_avg_price => new GridColumnSettings(nameof(Columns.WorkingShortAvgPrice), Headers.Working.ShortAvg, false, width: Widths.Qty, horizontal_alignment: HorizontalAlignment.Right);
public static GridColumnSettings working_short_avg_price_delta => new GridColumnSettings(nameof(Columns.WorkingShortAvgPriceDelta), Headers.Working.ShortAvgDelta, false, width: Widths.Qty, horizontal_alignment: HorizontalAlignment.Right);
public static GridColumnSettings working_net_pos => new GridColumnSettings(nameof(Columns.WorkingNetPos), Headers.Working.Net, width: Widths.WorkingQty);
public static GridColumnSettings working_net_avg_price_delta => new GridColumnSettings(nameof(Columns.WorkingNetAvgPriceDelta), Headers.Working.NetAvgDelta, false, width: Widths.Qty, horizontal_alignment: HorizontalAlignment.Right);
// total
public static GridColumnSettings total_pnl => new GridColumnSettings(nameof(Columns.TotalPNL), Headers.Total.PNL, false, width: Widths.PriceBig, horizontal_alignment: HorizontalAlignment.Right);
public static GridColumnSettings total_long_pos => new GridColumnSettings(nameof(Columns.TotalLongPos), Headers.Total.Long, false, width: Widths.Qty);
public static GridColumnSettings total_long_avg_price => new GridColumnSettings(nameof(Columns.TotalLongAvgPrice), Headers.Total.LongAvg, false, width: Widths.Price, horizontal_alignment: HorizontalAlignment.Right);
public static GridColumnSettings total_long_avg_price_delta => new GridColumnSettings(nameof(Columns.TotalLongAvgPriceDelta), Headers.Total.LongAvgDelta, false, width: Widths.Price, horizontal_alignment: HorizontalAlignment.Right);
public static GridColumnSettings total_short_pos => new GridColumnSettings(nameof(Columns.TotalShortPos), Headers.Total.Short, false, width: Widths.Qty);
public static GridColumnSettings total_short_avg_price => new GridColumnSettings(nameof(Columns.TotalShortAvgPrice), Headers.Total.ShortAvg, false, width: Widths.Price, horizontal_alignment: HorizontalAlignment.Right);
public static GridColumnSettings total_short_avg_price_delta => new GridColumnSettings(nameof(Columns.TotalShortAvgPriceDelta), Headers.Total.ShortAvgDelta, false, width: Widths.Price, horizontal_alignment: HorizontalAlignment.Right);
public static GridColumnSettings total_net_pos => new GridColumnSettings(nameof(Columns.TotalNetPos), Headers.Total.Net, false, width: Widths.Qty);
public static GridColumnSettings total_net_avg_price_delta => new GridColumnSettings(nameof(Columns.TotalNetAvgPriceDelta), Headers.Total.NetAvgDelta, false, width: Widths.Price, horizontal_alignment: HorizontalAlignment.Right);
// market
public static GridColumnSettings bid_price => new GridColumnSettings(nameof(Columns.BidPrice), DefaultColumnHeaders.Bid, false, width: DefaultColumnWidths.Price, font_color_override: BTSColors.MarketData.bid, horizontal_alignment: HorizontalAlignment.Right);
public static GridColumnSettings ask_price => new GridColumnSettings(nameof(Columns.AskPrice), DefaultColumnHeaders.Ask, false, width: DefaultColumnWidths.Price, font_color_override: BTSColors.MarketData.ask, horizontal_alignment: HorizontalAlignment.Right);
public static GridColumnSettings mid_price => new GridColumnSettings(nameof(Columns.MidPrice), DefaultColumnHeaders.Mid, false, width: DefaultColumnWidths.Price, horizontal_alignment: HorizontalAlignment.Right);
public static GridColumnSettings last_price => new GridColumnSettings(nameof(Columns.LastPrice), DefaultColumnHeaders.Last, false, width: DefaultColumnWidths.Price, horizontal_alignment: HorizontalAlignment.Right);
public static GridColumnSettings settlement_price => new GridColumnSettings(nameof(Columns.SettlementPrice), DefaultColumnHeaders.Settlement, false, width: DefaultColumnWidths.Settlement, horizontal_alignment: HorizontalAlignment.Right);
public static GridColumnSettings pnl_price => new GridColumnSettings(nameof(Columns.PNLPrice), DefaultColumnHeaders.PnLPrice, width: DefaultColumnWidths.Price, horizontal_alignment: HorizontalAlignment.Right);
public static GridColumnSettings cpp => new GridColumnSettings(nameof(Columns.CPP), "CPP", false, width: 50, horizontal_alignment: HorizontalAlignment.Right);
// misc
public static GridColumnSettings has_day_trades => new GridColumnSettings(nameof(Columns.HasDayTrades), Headers.HasDayTrades, false, width: Widths.HasDayTrades);
public static IEnumerable<SortingCriteria> default_sorting_criterion {
get {
yield return new SortingCriteria(account.id, true);
yield return new SortingCriteria(security.id, true);
}
}
public static IEnumerable<GridColumnSettings> all {
get {
yield return account;
yield return exchange;
yield return symbol;
yield return security;
yield return security_type;
yield return total_pnl; // hidden
yield return sod_pnl;
yield return day_pnl;
/// total
yield return total_net_pos;
yield return total_net_avg_price_delta; // hidden
// total long
yield return total_long_pos; // hidden
yield return total_long_avg_price; // hidden
yield return total_long_avg_price_delta; // hidden
// total short
yield return total_short_pos; // hidden
yield return total_short_avg_price; // hidden
yield return total_short_avg_price_delta; // hidden
/// sod
yield return sod_net_pos;
yield return sod_net_avg_price_delta; // hidden
// sod long
yield return sod_long_pos; // hidden
yield return sod_long_avg_price; // hidden
yield return sod_long_avg_price_delta; // hidden
// sod short
yield return sod_short_pos; // hidden
yield return sod_short_avg_price; // hidden
yield return sod_short_avg_price_delta; // hidden
/// day
yield return day_net_pos;
yield return day_net_avg_price_delta; // hidden
// day long
yield return day_long_pos; // hidden
yield return day_long_avg_price; // hidden
yield return day_long_avg_price_delta; // hidden
// day short
yield return day_short_pos; // hidden
yield return day_short_avg_price; // hidden
yield return day_short_avg_price_delta; // hidden
yield return has_day_trades;
/// working
// working net
yield return working_net_pos;
yield return working_net_avg_price_delta;
// working long
yield return working_long_pos;
yield return working_long_avg_price;
yield return working_long_avg_price_delta;
// working short
yield return working_short_pos;
yield return working_short_avg_price;
yield return working_short_avg_price_delta;
/// market
yield return bid_price;
yield return ask_price;
yield return mid_price;
yield return last_price;
yield return settlement_price;
yield return pnl_price;
yield return cpp;
}
}
public static IEnumerable<GridColumnSettings> group_columns {
get {
var rv = all.Select(cs => cs.with(font_weight_override: new NullableFontWeightProto(FontWeights.Bold))).ToList();
GridSettingsHelper.try_replace_column(rv, Columns.SODPNL, _override_color);
GridSettingsHelper.try_replace_column(rv, Columns.DayPNL, _override_color);
return rv;
}
}
public static IEnumerable<GridColumnSettings> header_columns {
get {
var rv = all.Select(cs => cs.with(font_weight_override: new NullableFontWeightProto(FontWeights.Bold))).ToList();
GridSettingsHelper.try_replace_column(rv, Columns.SODPNL, _override_color);
GridSettingsHelper.try_replace_column(rv, Columns.DayPNL, _override_color);
return rv;
}
}
static GridColumnSettings _override_color(GridColumnSettings col_settings) =>
_override_color(col_settings, 0);
static GridColumnSettings _override_color(GridColumnSettings col_settings, uint color) =>
col_settings.with(font_color_override: new NullableColorProto(color));
}
static class OpacityLevels {
public const byte Light = 0x55, Medium = 0x6A;
}
}
static class PNLSettingsExtensions {
public static IReadOnlyList<IPNLAnimationTrigger<PNLRowData>> triggers(this PNLSettings settings) => settings.animations.Select(a => a.to_trigger<PNLRowData>()).ToList();
public static IReadOnlyList<IPNLAnimationTrigger<PNLNormalizedData>> group_triggers(this PNLSettings settings) => settings.group_header_animations.Select(a => a.to_trigger<PNLNormalizedData>()).ToList();
public static IReadOnlyList<IPNLAnimationTrigger<PNLNormalizedData>> header_triggers(this PNLSettings settings) => settings.header_row_animations.Select(a => a.to_trigger<PNLNormalizedData>()).ToList();
}
}
@@ -0,0 +1,283 @@
using bts.collections;
using bts.utils;
using mbts;
using mbts.collections;
using mbts.expression;
using mbts.spark;
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;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Dynamic;
using System.Linq;
using System.Reflection;
using ColumnKeys = bts.shell.pnl.PNLView.Columns;
namespace bts.shell.pnl {
class PNLSettingsVM : DisposableViewModelBase, IWindowSettingsVM<PNLSettings>, IValueFactory<PNLSettings>, IObservable<PNLSettings>, ITemplateSelectorKey {
readonly IReadOnlyDictionary<ColumnKeys, AnchorItemVM> _anchor_cache;
readonly DeferrableActionWrapper
_sync_anchors,
_update;
PNLCalculationType _calculation_type;
PNLHeaderRowDisplay _header_row_display;
PNLSettings _last_loaded;
public PNLSettingsVM(PNLSettings settings) {
// init anchor cache, deferrable resync
IDictionary<string, object> raw_anchor_cache = new ExpandoObject();
raw_anchor_cache.AddRange(PNLSettings.anchorable_columns
.Select(k => new KeyValuePair<string, object>(k.ToString(), new AnchorItemVM(this, k))));
_anchor_cache =
MappedDictionary.create(
new ReadOnlyDictionary<string, object>(anchor_vm = raw_anchor_cache),
ConvertUtils.CastDelegate<object, AnchorItemVM>.value,
EnumHelper.parse<ColumnKeys>,
k => k.ToString());
_sync_anchors = new DeferrableActionWrapper(() => {
for (int i = 0; i < anchor_items.Count; i++) {
anchor_items[i].anchor_index = i + 1;
anchor_items[i].refresh();
}
});
_update = new DeferrableActionWrapper(() => result_changed?.Invoke());
core_settings_vm = new SimpleGridSettingsVM(column_factory: col_settings => {
var col_vm = new PNLColumnVM(core_settings_vm, EnumHelper.parse<ColumnKeys>(col_settings.id));
col_vm.load_from(col_settings);
return col_vm;
}) {
can_edit_background = true
};
core_settings_vm.selection_highlight.bind_opacity_to_alpha = true;
core_settings_vm.result_changed += _update.execute;
_dispose = Disposable.Create(
anchor_items.Subscribe((s, e) => {
_update.execute();
_sync_anchors.execute();
}),
ObjectSnapshot.connect_property_changed(_observable_components, (s, e) => _update.execute()),
Disposable.Create(() => core_settings_vm.result_changed -= _update.execute));
load(settings);
}
public event Action result_changed;
public dynamic anchor_vm { get; }
public ObservableCollection<AnchorItemVM> anchor_items { get; } = new ObservableCollection<AnchorItemVM>();
public SimpleGridSettingsVM core_settings_vm { get; }
public PNLCellAppearanceParamsVM group_row_defaults { get; } = new PNLCellAppearanceParamsVM();
public PNLCellAppearanceParamsVM header_row_defaults { get; } = new PNLCellAppearanceParamsVM();
public PNLCalculationType calculation_type { get => _calculation_type; set => Set(ref _calculation_type, value); }
public IEnumerable<PNLCalculationType> calculation_types { get; } = EnumHelper.get_values_except(PNLCalculationType.None);
public PNLHeaderRowDisplay header_row_display { get => _header_row_display; set => Set(ref _header_row_display, value); }
public IEnumerable<PNLHeaderRowDisplay> header_row_display_options => PNLSettings.header_row_display_options;
IEnumerable<INotifyPropertyChanged> _observable_components {
get {
yield return core_settings_vm;
yield return group_row_defaults;
yield return header_row_defaults;
yield return group_row_defaults.background;
yield return header_row_defaults.background;
yield return this;
}
}
string ITemplateSelectorKey.key => GridSettingsTemplateKeys.PNL;
public PNLSettings get_result() {
var s = _last_loaded == null ? new PNLSettings() : SparkUtils.clone_proto(_last_loaded);
s.calculation_type = _calculation_type;
s.grid = core_settings_vm.create();
var trigger_anims = core_settings_vm.columns
.OfType<PNLColumnVM>()
.SelectMany(c => c.get_triggers())
.GroupBy(tup => tup.expression)
.Where(g => CoreExpressionEvaluator<PNLRowData>.validate(g.Key))
.Select(g => (
expression: g.Key,
animations: g.ToArray()))
.ToList();
var normalized_anims = trigger_anims
.Where(tup => CoreExpressionEvaluator<PNLNormalizedData>.validate(tup.expression))
.ToList();
s = s.with(anchors: anchor_items.Select(vm => vm.key),
group_columns: core_settings_vm.columns.OfType<PNLColumnVM>().Select(c => {
var col_settings = c.create();
col_settings = c.group_column_vm.apply_to(col_settings);
return col_settings;
}),
header_columns: core_settings_vm.columns.OfType<PNLColumnVM>().Select(c => {
var col_settings = c.create();
col_settings = c.header_column_vm.apply_to(col_settings);
return col_settings;
}),
animations: trigger_anims.Select(
ta => new PNLAnimationTriggerData {
expression = ta.expression,
animations = Array.ConvertAll(ta.animations, t => t.row)
}),
group_header_animations: normalized_anims
.Select(
ta => new PNLAnimationTriggerData {
expression = ta.expression,
animations = Array.ConvertAll(ta.animations, t => t.group)
}),
header_row_animations: normalized_anims.Select(
ta => new PNLAnimationTriggerData {
expression = ta.expression,
animations = Array.ConvertAll(ta.animations, t => t.header)
}));
s.header_row_display = header_row_display;
s.header_cell_appearance = PNLCellAppearanceProto.from_appearance(header_row_defaults.create());
s.group_cell_appearance = PNLCellAppearanceProto.from_appearance(group_row_defaults.create());
return s;
}
public void load(PNLSettings value) {
using (_update.defer_execution()) {
_last_loaded = value;
header_row_defaults.load_from(value.header_cell_appearance.to_appearance());
group_row_defaults.load_from(value.group_cell_appearance.to_appearance());
var column_map = _map_column_data(value.grid.columns, value.animations);
var group_column_map = _map_column_data(value.group_columns, value.group_header_animations);
var header_column_map = _map_column_data(value.header_columns, value.header_row_animations);
core_settings_vm.load_from(value.grid);
for (int i = 0; i < core_settings_vm.columns.Count; i++) {
var col_vm = core_settings_vm.columns[i] as PNLColumnVM ?? throw new InvalidOperationException($"In {nameof(PNLSettingsVM)}, all columns should be of type {typeof(PNLColumnVM)}.");
var key = EnumHelper.parse<ColumnKeys>(col_vm.id);
var (col_settings, trigger_animations) = column_map[key];
var group_data = group_column_map.GetNullable(key);
var header_data = header_column_map.GetNullable(key);
col_vm.load_from(col_settings, trigger_animations, group_data?.settings, group_data?.triggers, header_data?.triggers, header_data?.settings);
}
using (_sync_anchors.defer_execution())
anchor_items.WrapSelector(a => a.key, k => _anchor_cache[k]).sync_with(value.anchors.Intersect(PNLSettings.anchorable_columns));
header_row_display = value.header_row_display;
calculation_type = value.calculation_type;
}
//var result = get_result();
//if (!value.Equals(result)) {
// compare_vals(result, value, typeof(PNLSettings));
//}
//void compare_vals(object a, object b, Type t) {
// if (a == null || b == null) {
// return;
// }
// var props = t.GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
// foreach (var p in props) {
// var av = p.GetValue(a);
// var bv = p.GetValue(b);
// if (av is IEnumerable avals) {
// if (bv is IEnumerable bvals) {
// var gen_t = ReflectionHelper.GetGenericTypeArgs(p.PropertyType, typeof(IEnumerable<>));
// if (gen_t != null) {
// var aen = avals.GetEnumerator();
// var ben = bvals.GetEnumerator();
// while (aen.MoveNext()) {
// if (!ben.MoveNext()) {
// goto execute;
// }
// compare_vals(aen.Current, ben.Current, gen_t.First());
// }
// goto finish;
// execute:
// continue;
// finish:
// if (ben.MoveNext()) {
// }
// continue;
// }
// }
// }
// if (!Equals(av, bv)) {
// if (!Equals(av, bv)) {
// compare_vals(av, bv, p.PropertyType);
// }
// }
// }
//}
}
PNLSettings IValueFactory<PNLSettings>.create() => get_result();
void IValueFactory<PNLSettings>.load_from(PNLSettings value) => load(value);
IDisposable IObservable<PNLSettings>.Subscribe(IObserver<PNLSettings> observer) {
result_changed += notify;
return Disposable.Create(() => result_changed -= notify);
void notify() => observer.OnNext(get_result());
}
static Dictionary<ColumnKeys, (GridColumnSettings settings, Dictionary<string, PNLAnimation> triggers)> _map_column_data(IEnumerable<GridColumnSettings> columns, IEnumerable<PNLAnimationTriggerData> triggers) {
var map = columns.ToDictionary(
c => c.strong_id<ColumnKeys>(),
c => (settings: c, triggers: new Dictionary<string, PNLAnimation>()));
foreach (var trig in triggers) {
foreach (var anim in trig.animations) {
if (map.TryGetValue(anim.column, out var col_data))
col_data.triggers[trig.expression] = anim;
}
}
return map;
}
void _append_or_remove_anchor(ColumnKeys key, bool include) {
if (!_anchor_cache.TryGetValue(key, out var vm))
throw new KeyNotFoundException($"Column key '{key}' is not anchorable (expected one of: {string.Join(", ", PNLSettings.anchorable_columns)}).");
if (vm.include == include)
return;
if (include) {
if (anchor_items.Any(a => a.key == key))
throw new InvalidOperationException($"{nameof(anchor_items)} already contains an element with key '{key}.'");
anchor_items.Add(vm);
vm.anchor_index = anchor_items.Count;
} else {
if (!anchor_items.Remove(vm))
throw new InvalidOperationException($"{nameof(anchor_items)} does not contain an element with key '{key}.'");
vm.anchor_index = null;
}
_sync_anchors.execute();
}
public class AnchorItemVM : ViewModelBase, IEquatable<AnchorItemVM> {
readonly PNLSettingsVM _owner;
int? _anchor_index;
public AnchorItemVM(PNLSettingsVM owner, ColumnKeys key) {
_owner = owner;
this.key = key;
}
public ColumnKeys key { get; }
public bool include { get => anchor_index.HasValue; set => _owner._append_or_remove_anchor(key, value); }
public int? anchor_index { get => _anchor_index; set => Set(ref _anchor_index, value); }
public void refresh() =>
RaisePropertyChanged(nameof(include), nameof(anchor_index));
public bool Equals(AnchorItemVM other) =>
other != null && key == other.key;
public override bool Equals(object obj) =>
Equals(obj as AnchorItemVM);
public override int GetHashCode() =>
key.GetHashCode();
public override string ToString() =>
EnumHelper.get_display_name(key);
}
}
}
@@ -0,0 +1,39 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using ColumnKeys = bts.shell.pnl.PNLView.Columns;
namespace bts.shell.pnl {
class PNLCellEmbeddedAnimationModel<TRow> {
IReadOnlyList<IPNLAnimationTrigger<TRow>> _triggers;
ImmutableDictionary<ColumnKeys, PNLCellAppearance> _defaults;
TRow _row;
public PNLCellEmbeddedAnimationModel(ImmutableDictionary<ColumnKeys, PNLCellAppearance> defaults, IReadOnlyList<IPNLAnimationTrigger<TRow>> triggers) {
_defaults = defaults;
_triggers = triggers;
}
public IReadOnlyDictionary<ColumnKeys, PNLCellAppearance> cell_appearances { get; private set; }
public void on_settings(ImmutableDictionary<ColumnKeys, PNLCellAppearance> defaults, IReadOnlyList<IPNLAnimationTrigger<TRow>> triggers) {
_defaults = defaults;
_triggers = triggers;
on_row(_row);
}
public void on_row(TRow data) {
_row = data;
var bd = _defaults.ToBuilder();
foreach (var t in _triggers) {
foreach (var kv in t.trigger(data)) {
if (!bd.TryGetValue(kv.Key, out var app)) {
app = _defaults[kv.Key];
}
app = kv.Value.merge(app);
bd[kv.Key] = app;
}
}
cell_appearances = bd.ToImmutable();
}
}
}
@@ -0,0 +1,147 @@
using mbts.spark;
using mbts.spark.utilities;
using mbts.ui;
using mbts.ui.spark;
using mbts.utils;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Windows.Media;
namespace bts.shell.pnl {
class PNLCellIndexBuilder<TRow> {
public readonly DIYGridAnimationModel<PNLView.Columns, TRow> animation_model = new DIYGridAnimationModel<PNLView.Columns, TRow>(TimeSpan.FromSeconds(0.3));
static readonly Func<WorkingOrderSideData, string> _fmt_working_order_data =
d => OrderUtils.order_collection_summary(d.top_price, d.top_volume, d.working_total);
readonly Dictionary<PNLView.Columns, IPNLCellVM<TRow>> _cell_index = new Dictionary<PNLView.Columns, IPNLCellVM<TRow>>();
public PNLCellVM<TRow, T> add<T>(PNLView.Columns key, Func<TRow, T> get, Func<T, string> fmt = null) =>
add(key, get, fmt, EqualityComparer<T>.Default, Comparer<T>.Default);
public PNLCellVM<TRow, T> add_pnl<T>(PNLView.Columns key, Func<TRow, T> get) where T : struct, IComparable<T>, IFormattable =>
add_delta(key, BTSColors.PNL.profit_color, BTSColors.PNL.loss_color, get, FormattingDelegates.Price<T>.Value);
public PNLCellVM<TRow, decimal> add_pnl_decimal(PNLView.Columns key, Func<TRow, decimal> get) =>
add_delta<decimal>(key, BTSColors.PNL.profit_color, BTSColors.PNL.loss_color, OrderDataHelper.PriceComparerDecimal, get, FormattingDelegates.Price<decimal>.Value);
public PNLCellVM<TRow, double> add_pnl_double(PNLView.Columns key, Func<TRow, double> get) =>
add_delta(key, BTSColors.PNL.profit_color, BTSColors.PNL.loss_color, OrderDataHelper.PriceComparer, get, FormattingDelegates.Price<double>.Value);
public PNLCellVM<TRow, double> add_net_price(PNLView.Columns key, Func<TRow, double> get) =>
add_delta(key, BTSColors.MarketData.bid, BTSColors.MarketData.ask, OrderDataHelper.PriceComparer, get, FormattingDelegates.Price<double>.Value);
public PNLCellVM<TRow, T> add_net_pos<T>(PNLView.Columns key, Func<TRow, T> get, Func<T, string> fmt = null) where T : struct, IComparable<T> =>
add_delta(key, BTSColors.MarketData.bid, BTSColors.MarketData.ask, get, fmt);
public PNLCellVM<TRow, double> add_long_price(PNLView.Columns key, Func<TRow, double> get) =>
add_delta(key, BTSColors.MarketData.bid, BTSColors.MarketData.bid, OrderDataHelper.PriceComparer, get, FormattingDelegates.Price<double>.Value);
public PNLCellVM<TRow, T> add_long_pos<T>(PNLView.Columns key, Func<TRow, T> get, Func<T, string> fmt = null) where T : struct, IComparable<T> =>
add_delta(key, BTSColors.MarketData.bid, BTSColors.MarketData.ask, get, fmt);
public PNLCellVM<TRow, double> add_short_price(PNLView.Columns key, Func<TRow, double> get) =>
add_delta(key, BTSColors.MarketData.ask, BTSColors.MarketData.ask, OrderDataHelper.PriceComparer, get, FormattingDelegates.Price<double>.Value);
public PNLCellVM<TRow, T> add_short_pos<T>(PNLView.Columns key, Func<TRow, T> get, Func<T, string> fmt = null) where T : struct, IComparable<T> =>
add_delta(key, BTSColors.MarketData.ask, BTSColors.MarketData.bid, get, fmt);
public PNLCellVM<TRow, T> add_price<T>(PNLView.Columns key, Func<TRow, T> get) where T : struct, IComparable<T>, IFormattable =>
add_delta(key, BTSColors.PNL.profit_color, BTSColors.PNL.loss_color, get, FormattingDelegates.Price<T>.Value);
public PNLCellVM<TRow, double> add_price_double(PNLView.Columns key, Func<TRow, double> get) =>
add_delta(key, BTSColors.PNL.profit_color, BTSColors.PNL.loss_color, OrderDataHelper.PriceComparer, get, FormattingDelegates.Price<double>.Value);
public PNLCellVM<TRow, WorkingOrderSideData> add_working_long(PNLView.Columns key, Func<TRow, WorkingOrderSideData> get, Func<TRow, double> get_market) =>
_add_working(key, BTSColors.MarketData.bid, BTSColors.MarketData.ask, get, get_market, OrderDataHelper.PriceComparer);
public PNLCellVM<TRow, WorkingOrderSideData> add_working_short(PNLView.Columns key, Func<TRow, WorkingOrderSideData> get, Func<TRow, double> get_market) =>
_add_working(key, BTSColors.MarketData.ask, BTSColors.MarketData.bid, get, get_market, new ReverseComparer<double>(OrderDataHelper.PriceComparer));
PNLCellVM<TRow, WorkingOrderSideData> _add_working(PNLView.Columns key, Color increase, Color decrease, Func<TRow, WorkingOrderSideData> get, Func<TRow, double> get_market, IComparer<double> price_comparer) {
var delta_trig = DIYGridTriggerAnimations.delta_trigger<int, PNLView.Columns>(key, increase, decrease)
.wrap<int, TRow, PNLView.Columns>(r => get(r).working_total);
var top_of_book_trig = new TopOfBookTrigger<int, double>(key, increase,
r => get(r).working_total,
r => get(r).top_price,
get_market,
price_comparer);
var zipped_trig = delta_trig.zip_indefinite_frame(r => {
var frames = top_of_book_trig.on_data(r);
return frames.TryGetFirst(out var f) ? f.animation : null;
});
var cell = add(key, get, _fmt_working_order_data);
animation_model.add_trigger(delta_trig);
animation_model.add_trigger(top_of_book_trig);
return cell;
}
public PNLCellVM<TRow, T> add_delta<T>(PNLView.Columns key, Color increase, Color decrease, Func<TRow, T> get, Func<T, string> fmt = null) where T : struct, IComparable<T> =>
add_delta(key, increase, decrease, Comparer<T>.Default, get, fmt);
public PNLCellVM<TRow, T> add_delta<T>(PNLView.Columns key, Color increase, Color decrease, IComparer<T> comparer, Func<TRow, T> get, Func<T, string> fmt = null) where T : struct {
var cell = add(key, get, fmt, EqualityComparer<T>.Default, Comparer<T>.Default);
animation_model.add_trigger(
DIYGridTriggerAnimations.delta_trigger(key, increase, decrease, comparer)
.wrap(get));
return cell;
}
public PNLCellVM<TRow, T> add<T>(PNLView.Columns key, Func<TRow, T> get, Func<T, string> fmt, IEqualityComparer<T> eq_comparer, IComparer<T> comparer) {
var cell = new PNLCellVM<TRow, T>(get, fmt, eq_comparer, comparer);
_cell_index[key] = cell;
return cell;
}
public void on_row(TRow row) {
foreach (var cell in _cell_index.Values)
cell.on_row(row);
animation_model.on_row(row);
}
public void set_appearances(IReadOnlyDictionary<PNLView.Columns, PNLCellAppearance> appearances) {
foreach (var kv in appearances) {
if (_cell_index.TryGetValue(kv.Key, out var c))
c.apply_appearance(kv.Value);
}
}
class TopOfBookTrigger<TPosition, TPrice> : ITriggerAnimation<TRow, PNLView.Columns> where TPosition : struct, IComparable<TPosition> where TPrice : struct, IComparable<TPrice> {
readonly PNLView.Columns _column;
readonly Func<TRow, TPosition> _get_pos;
readonly Func<TRow, TPrice> _get_price, _get_market;
readonly IComparer<TPrice> _comparer;
readonly Color _border_color;
public TopOfBookTrigger(
PNLView.Columns column,
Color border_color,
Func<TRow, TPosition> get_pos,
Func<TRow, TPrice> get_price,
Func<TRow, TPrice> get_market,
IComparer<TPrice> comparer) {
_column = column;
_border_color = border_color;
_get_pos = get_pos;
_get_price = get_price;
_get_market = get_market;
_comparer = comparer;
}
public IEnumerable<AnimationFrame<PNLView.Columns>> on_data(TRow value) {
var pos = _get_pos(value);
if (pos.CompareTo(default) > 0) {
var price = _get_price(value);
var market = _get_market(value);
if (_comparer.Compare(price, market) >= 0)
yield return new AnimationFrame<PNLView.Columns>(
TimeSpan.Zero,
ImmutableDictionary<SymbolKey<PNLView.Columns>, Animation>.Empty.Add(
new SymbolKey<PNLView.Columns>(_column, mbts.ui.spark.CellAnimation.Type.Border),
new Animation(mbts.ui.spark.CellAnimation.Type.Border, _border_color)));
}
}
}
}
}
@@ -0,0 +1,119 @@
using mbts.ui;
using mbts.ui.shell;
using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Media;
namespace bts.shell.pnl {
interface IPNLCellVM {
object value { get; }
FontWeight font_weight { get; set; }
Color foreground { get; set; }
Color background { get; set; }
Color border { get; set; }
}
interface IPNLCellVM<TRow> : IPNLCellVM {
void on_row(TRow row);
}
class PNLCellVM<TRow, TValue> : ViewModelBase, IObserver<PNLAnimation>, IPNLCellVM<TRow>, IComparable<PNLCellVM<TRow, TValue>>, IComparable {
readonly Func<TValue, string> _fmt;
readonly Func<TRow, TValue> _get;
readonly IEqualityComparer<TValue> _eq_comparer;
readonly IComparer<TValue> _comparer;
Color
_foreground = Colors.Black,
_background = Colors.Transparent,
_border = Colors.Transparent;
string _text;
TValue _value;
public PNLCellVM(Func<TRow, TValue> get)
: this(get, null, EqualityComparer<TValue>.Default, Comparer<TValue>.Default) { }
public PNLCellVM(Func<TRow, TValue> get, Func<TValue, string> fmt)
: this(get, fmt, EqualityComparer<TValue>.Default, Comparer<TValue>.Default) { }
public PNLCellVM(Func<TRow, TValue> get, Func<TValue, string> fmt, IEqualityComparer<TValue> eq_comparer, IComparer<TValue> comparer) {
_get = get;
_fmt = fmt;
_eq_comparer = eq_comparer;
_comparer = comparer;
}
object IPNLCellVM.value => value;
public TValue value {
get => _value;
set {
if (Set(ref _value, value)) {
_text = null;
RaisePropertyChanged(nameof(text));
}
}
}
public string text {
get => _text ?? (_text = _fmt?.Invoke(value) ?? value?.ToString());
set => Set(ref _text, value);
}
public Color foreground {
get => _foreground;
set => Set(ref _foreground, value);
}
public Color background {
get => _background;
set => Set(ref _background, value);
}
public Color border {
get => _border;
set => Set(ref _border, value);
}
FontWeight _font_weight;
public FontWeight font_weight {
get => _font_weight;
set => Set(ref _font_weight, value);
}
public void on_row(TRow row) =>
value = _get(row);
public void OnNext(PNLAnimation animation) {
if (animation.foreground.HasValue)
foreground = animation.foreground.Value.argb_color();
if (animation.background.HasValue)
background = animation.background.Value.argb_color();
if (animation.font_weight.HasValue)
font_weight = FontWeight.FromOpenTypeWeight(animation.font_weight.Value);
}
public void OnError(Exception error) { }
public void OnCompleted() { }
public override string ToString() =>
text;
int IComparable<PNLCellVM<TRow, TValue>>.CompareTo(PNLCellVM<TRow, TValue> other) =>
other == null ? throw new ArgumentNullException(nameof(other)) : _comparer.Compare(value, other.value);
int IComparable.CompareTo(object obj) =>
obj is PNLCellVM<TRow, TValue> other ?
_comparer.Compare(value, other.value) :
throw new ArgumentException(nameof(obj), $"The specified object is not of type {typeof(PNLCellVM<TRow, TValue>)} (actually {obj?.GetType()}).");
}
static class PNLCellVMExtensions {
public static void apply_appearance(this IPNLCellVM cell, PNLCellAppearance animation) {
if (animation.foreground.HasValue)
cell.foreground = animation.foreground.Value;
if (animation.background.HasValue)
cell.background = animation.background.Value;
if (animation.font_weight.HasValue)
cell.font_weight = animation.font_weight.Value;
// TODO: font family, size
}
}
}
@@ -0,0 +1,137 @@
using bts.utils;
using mbts.ui.shell;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel;
using ColumnKeys = bts.shell.pnl.PNLView.Columns;
namespace bts.shell.pnl {
class PNLNormalizedDataVM : IRaiseNotifyPropertyChanged, INotifyPropertyChanged {
static readonly IPropertyChangedDetector<PNLNormalizedDataVM> _change_detector =
ObjectSnapshot.build_detector_except<PNLNormalizedDataVM>(nameof(data));
PNLNormalizedData _data;
public event PropertyChangedEventHandler PropertyChanged;
public event Action data_changed;
public PNLNormalizedData data {
get => _data;
set {
if (_data != value) {
using (_change_detector.detect_changes(this))
_data = value;
data_changed?.Invoke();
}
}
}
// total aggregate pnl
public decimal day_pnl => _data.day_pnl;
public decimal sod_pnl => _data.sod_pnl;
public decimal total_pnl => _data.day_pnl + _data.sod_pnl;
// day pos
public decimal day_long_pos => _data.day_long_pos;
public decimal day_short_pos => _data.day_short_pos;
public decimal day_net_pos => _data.day_long_pos - _data.day_short_pos;
// sod pos
public decimal sod_long_pos => _data.sod_long_pos;
public decimal sod_short_pos => _data.sod_short_pos;
public decimal sod_net_pos => _data.sod_long_pos - _data.sod_short_pos;
// total pos
public decimal total_net_pos => _data.total_net_pos;
public decimal total_long_pos => _data.day_long_pos + _data.sod_long_pos;
public decimal total_short_pos => _data.day_short_pos + _data.sod_short_pos;
// working
public decimal working_long_pos => _data.working_long_pos;
public decimal working_short_pos => _data.working_short_pos;
public decimal working_net_pos => working_long_pos - working_short_pos;
void IRaiseNotifyPropertyChanged.raise_notify_property_changed(string prop) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(prop));
}
class PNLHeaderRowVM : IRaiseNotifyPropertyChanged, INotifyPropertyChanged {
static readonly IPropertyChangedDetector<PNLHeaderRowVM> _change_detector =
ObjectSnapshot.build_detector_except<PNLHeaderRowVM>(nameof(data));
readonly PNLCellIndexBuilder<PNLNormalizedData> _cell_map = new PNLCellIndexBuilder<PNLNormalizedData>();
readonly PNLCellEmbeddedAnimationModel<PNLNormalizedData> _animator;
ImmutableDictionary<ColumnKeys, PNLCellAppearance> _default_appearances;
IReadOnlyList<IPNLAnimationTrigger<PNLNormalizedData>> _triggers;
PNLNormalizedData _data = PNLNormalizedData.zero;
public PNLHeaderRowVM(ImmutableDictionary<ColumnKeys, PNLCellAppearance> default_appearances, IReadOnlyList<IPNLAnimationTrigger<PNLNormalizedData>> triggers) {
_default_appearances = default_appearances;
_triggers = triggers;
_animator = new PNLCellEmbeddedAnimationModel<PNLNormalizedData>(default_appearances, triggers);
_cell_map.set_appearances(default_appearances);
// sod
sod_pnl = _cell_map.add_pnl_decimal(ColumnKeys.SODPNL, d => d.sod_pnl);
sod_net_pos = _cell_map.add_net_pos(ColumnKeys.SODNetPos, d => d.sod_net_pos);
sod_long_pos = _cell_map.add_long_pos(ColumnKeys.SODLongPos, d => d.sod_long_pos);
sod_short_pos = _cell_map.add_short_pos(ColumnKeys.SODShortPos, d => d.sod_short_pos);
// day
day_pnl = _cell_map.add_pnl(ColumnKeys.DayPNL, d => d.day_pnl);
day_net_pos = _cell_map.add_net_pos(ColumnKeys.DayNetPos, d => d.day_net_pos);
day_long_pos = _cell_map.add_long_pos(ColumnKeys.DayLongPos, d => d.day_long_pos);
day_short_pos = _cell_map.add_short_pos(ColumnKeys.DayShortPos, d => d.day_short_pos);
// working
working_net_pos = _cell_map.add_net_pos(ColumnKeys.WorkingNetPos, d => d.working_net_pos);
working_long_pos = _cell_map.add_long_pos(ColumnKeys.WorkingLongPos, d => d.working_long_pos);
working_short_pos = _cell_map.add_short_pos(ColumnKeys.WorkingShortPos, r => r.working_short_pos);
// total
total_pnl = _cell_map.add_pnl_decimal(ColumnKeys.TotalPNL, d => d.total_pnl);
total_net_pos = _cell_map.add_net_pos(ColumnKeys.TotalNetPos, d => d.total_net_pos);
total_long_pos = _cell_map.add_long_pos(ColumnKeys.TotalLongPos, d => d.total_long_pos);
total_short_pos = _cell_map.add_short_pos(ColumnKeys.TotalShortPos, d => d.total_short_pos);
}
public event PropertyChangedEventHandler PropertyChanged;
public event Action data_changed;
public PNLNormalizedData data {
get => _data;
set {
if (value != _data) {
_data = value;
_cell_map.on_row(value);
_animator.on_row(value);
_cell_map.set_appearances(_animator.cell_appearances);
data_changed?.Invoke();
}
}
}
// day
public PNLCellVM<PNLNormalizedData, decimal> day_pnl { get; }
public PNLCellVM<PNLNormalizedData, decimal> day_long_pos { get; }
public PNLCellVM<PNLNormalizedData, decimal> day_short_pos { get; }
public PNLCellVM<PNLNormalizedData, decimal> day_net_pos { get; }
// sod
public PNLCellVM<PNLNormalizedData, decimal> sod_pnl { get; }
public PNLCellVM<PNLNormalizedData, decimal> sod_long_pos { get; }
public PNLCellVM<PNLNormalizedData, decimal> sod_short_pos { get; }
public PNLCellVM<PNLNormalizedData, decimal> sod_net_pos { get; }
// total
public PNLCellVM<PNLNormalizedData, decimal> total_pnl { get; }
public PNLCellVM<PNLNormalizedData, decimal> total_net_pos { get; }
public PNLCellVM<PNLNormalizedData, decimal> total_long_pos { get; }
public PNLCellVM<PNLNormalizedData, decimal> total_short_pos { get; }
// working
public PNLCellVM<PNLNormalizedData, decimal> working_long_pos { get; }
public PNLCellVM<PNLNormalizedData, decimal> working_short_pos { get; }
public PNLCellVM<PNLNormalizedData, decimal> working_net_pos { get; }
public void on_settings(ImmutableDictionary<ColumnKeys, PNLCellAppearance> default_appearances, IReadOnlyList<IPNLAnimationTrigger<PNLNormalizedData>> triggers) {
_default_appearances = default_appearances;
_triggers = triggers;
_animator.on_settings(_default_appearances, triggers);
_cell_map.set_appearances(_animator.cell_appearances);
}
void IRaiseNotifyPropertyChanged.raise_notify_property_changed(string prop) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(prop));
}
}
@@ -0,0 +1,215 @@
using bts.api;
using bts.utils;
using mbts;
using mbts.spark;
using mbts.ui;
using mbts.ui.controls;
using mbts.ui.shell;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Reactive.Linq;
using ColumnKeys = bts.shell.pnl.PNLView.Columns;
namespace bts.shell.pnl {
class PNLRowVM : DisposableViewModelBase, IEquatable<PNLRowVM> {
public static readonly IComparer<PNLRowVM> key_comparer = Comparer<PNLKey>.Default.wrap<PNLKey, PNLRowVM>(r => r.key);
static readonly Func<bool, string> _fmt_has_day_trades = hdt => hdt ? "Yes" : "No";
//
readonly PNLCellIndexBuilder<PNLRowData> _cell_map = new PNLCellIndexBuilder<PNLRowData>();
readonly PNLCellEmbeddedAnimationModel<PNLRowData> _animator;
bool _refresh_normalized_data = true, _is_selected;
PNLNormalizedData _normalized_data;
public PNLRowVM(PNLRowData data, ImmutableDictionary<ColumnKeys, PNLCellAppearance> default_appearances, IReadOnlyList<IPNLAnimationTrigger<PNLRowData>> triggers) {
this.data = data;
_animator = new PNLCellEmbeddedAnimationModel<PNLRowData>(default_appearances, triggers);
_animator.on_row(data);
pnl_price = _cell_map.add_price_double(ColumnKeys.PNLPrice, d => d.pnl_price);
account_name_cell = _cell_map.add(ColumnKeys.Account, d => d.account_name);
security_cell = _cell_map.add(ColumnKeys.Security, d => d.security, null, EqualityComparer<SecurityImmutable?>.Default, Comparer<SecurityImmutable>.Default.to_nullable());
security_type_cell = _cell_map.add(ColumnKeys.SecurityType, d => d.security?.ContractCase ?? Security.ContractOneofCase.None);
exchange_cell = _cell_map.add(ColumnKeys.Exchange, d => d.security?.Exchange ?? Venue.Unset);
symbol_cell = _cell_map.add(ColumnKeys.Symbol, d => d.security?.Symbol ?? string.Empty);
// sod
sod_pnl = _cell_map.add_pnl_double(ColumnKeys.SODPNL, d => d.sod_pnl);
sod_net_pos = _cell_map.add_net_pos(ColumnKeys.SODNetPos, d => d.sod_net_pos);
sod_net_avg_price_delta = _cell_map.add_net_price(ColumnKeys.SODNetAvgPriceDelta, d => d.sod_net_avg_price_delta_raw);
sod_long_pos = _cell_map.add_long_pos(ColumnKeys.SODLongPos, d => d.sod_long_pos);
sod_long_avg_price = _cell_map.add_long_price(ColumnKeys.SODLongAvgPrice, d => d.sod_long_avg_price_raw);
sod_long_avg_price_delta = _cell_map.add_long_price(ColumnKeys.SODLongAvgPriceDelta, d => d.sod_long_avg_price_delta_raw);
sod_short_pos = _cell_map.add_short_pos(ColumnKeys.SODShortPos, d => d.sod_short_pos);
sod_short_avg_price = _cell_map.add_short_price(ColumnKeys.SODShortAvgPrice, d => d.sod_short_avg_price_raw);
sod_short_avg_price_delta = _cell_map.add_short_price(ColumnKeys.SODShortAvgPriceDelta, d => d.sod_short_avg_price_delta_raw);
// day
day_pnl = _cell_map.add_pnl_double(ColumnKeys.DayPNL, d => d.day_pnl);
day_net_pos = _cell_map.add_net_pos(ColumnKeys.DayNetPos, d => d.day_net_pos);
day_net_avg_price_delta = _cell_map.add_net_price(ColumnKeys.DayNetAvgPriceDelta, d => d.day_net_avg_price_delta_raw);
day_long_pos = _cell_map.add_long_pos(ColumnKeys.DayLongPos, d => d.day_long_pos);
day_long_avg_price = _cell_map.add_long_price(ColumnKeys.DayLongAvgPrice, d => d.day_long_avg_price_raw);
day_long_avg_price_delta = _cell_map.add_long_price(ColumnKeys.DayLongAvgPriceDelta, d => d.day_long_avg_price_delta_raw);
day_short_pos = _cell_map.add_short_pos(ColumnKeys.DayShortPos, d => d.day_short_pos);
day_short_avg_price = _cell_map.add_short_price(ColumnKeys.DayShortAvgPrice, d => d.day_short_avg_price_raw);
day_short_avg_price_delta = _cell_map.add_short_price(ColumnKeys.DayShortAvgPriceDelta, d => d.day_short_avg_price_delta_raw);
// working
working_net_pos = _cell_map.add_net_pos(ColumnKeys.WorkingNetPos, d => d.working_net_pos);
working_net_avg_price_delta = _cell_map.add_net_price(ColumnKeys.WorkingNetAvgPriceDelta, d => d.working_net_avg_price_delta_raw);
working_long_pos = _cell_map.add_working_long(ColumnKeys.WorkingLongPos, d => d.working.long_data, d => d.bid_price);
working_long_avg_price = _cell_map.add_long_price(ColumnKeys.WorkingLongAvgPrice, d => d.working_long_avg_price_raw);
working_long_avg_price_delta = _cell_map.add_long_price(ColumnKeys.WorkingLongAvgPriceDelta, d => d.working_long_avg_price_delta_raw);
working_short_pos = _cell_map.add_working_short(ColumnKeys.WorkingShortPos, r => r.working.short_data, r => r.ask_price);
working_short_avg_price = _cell_map.add_short_price(ColumnKeys.WorkingShortAvgPrice, d => d.working_short_avg_price_raw);
working_short_avg_price_delta = _cell_map.add_short_price(ColumnKeys.WorkingShortAvgPriceDelta, d => d.working_short_avg_price_delta_raw);
// total
total_pnl = _cell_map.add_pnl_double(ColumnKeys.TotalPNL, d => d.total_pnl);
total_net_pos = _cell_map.add_net_pos(ColumnKeys.TotalNetPos, d => d.total_net_pos);
total_net_avg_price_delta = _cell_map.add_net_price(ColumnKeys.TotalNetAvgPriceDelta, d => d.total_net_avg_price_delta_raw);
total_long_pos = _cell_map.add_long_pos(ColumnKeys.TotalLongPos, d => d.total_long_pos);
total_long_avg_price = _cell_map.add_long_price(ColumnKeys.TotalLongAvgPrice, d => d.total_long_avg_price_raw);
total_long_avg_price_delta = _cell_map.add_long_price(ColumnKeys.TotalLongAvgPriceDelta, d => d.total_long_avg_price_delta_raw);
total_short_pos = _cell_map.add_short_pos(ColumnKeys.TotalShortPos, d => d.total_short_pos);
total_short_avg_price = _cell_map.add_short_price(ColumnKeys.TotalShortAvgPrice, d => d.total_short_avg_price_raw);
total_short_avg_price_delta = _cell_map.add_short_price(ColumnKeys.TotalShortAvgPriceDelta, d => d.total_short_avg_price_delta_raw);
// market
bid_price = _cell_map.add_price_double(ColumnKeys.BidPrice, d => d.bid_price);
ask_price = _cell_map.add_price_double(ColumnKeys.AskPrice, d => d.ask_price);
mid_price = _cell_map.add_price_double(ColumnKeys.MidPrice, d => d.mid_price);
last_price = _cell_map.add_price_double(ColumnKeys.LastPrice, d => d.last_price);
settlement_price = _cell_map.add_price_double(ColumnKeys.SettlementPrice, d => d.settlement_price);
// misc
has_day_trades = _cell_map.add(ColumnKeys.HasDayTrades, d => d.has_day_trades, _fmt_has_day_trades);
cpp = _cell_map.add(ColumnKeys.CPP, d => d.cpp);
_cell_map.on_row(data);
_cell_map.set_appearances(_animator.cell_appearances);
_dispose = _cell_map.animation_model.OnDispatcher().Subscribe(new_animations => {
animations = new_animations.Select(kv => new mbts.ui.spark.CellAnimation(kv.Key.column, kv.Key.type, kv.Value.color.to_brush())).ToList();
RaisePropertyChanged(nameof(animations));
});
}
// events
public event Action<PNLNormalizedData> updated;
public event Action<bool> is_selected_changed;
// data
public PNLKey key => data.key;
public PNLRowData data { get; private set; }
public PNLNormalizedData normalized_data {
get {
if (_refresh_normalized_data) {
_refresh_normalized_data = false;
var nd = new PNLNormalizedData(
(decimal)DoubleUtil.nan_to_zero(day_pnl.value),
(decimal)DoubleUtil.nan_to_zero(data.day.long_pos),
(decimal)DoubleUtil.nan_to_zero(data.day.short_pos),
(decimal)DoubleUtil.nan_to_zero(sod_pnl.value),
(decimal)DoubleUtil.nan_to_zero(data.sod.long_pos),
(decimal)DoubleUtil.nan_to_zero(data.sod.short_pos),
(decimal)DoubleUtil.nan_to_zero(data.working_long_pos),
(decimal)DoubleUtil.nan_to_zero(data.working_short_pos));
if (_normalized_data != nd) {
_normalized_data = nd;
updated?.Invoke(nd);
}
}
return _normalized_data;
}
}
// security
public string account_name => data.account_name;
public SecurityImmutable? security => data.security;
public Security.ContractOneofCase security_type => security?.ContractCase ?? Security.ContractOneofCase.None;
public Venue exchange => security?.Exchange ?? Venue.Unset;
public string symbol => security.symbol_or_default();
// cells
public PNLCellVM<PNLRowData, string> account_name_cell { get; }
public PNLCellVM<PNLRowData, SecurityImmutable?> security_cell { get; }
public PNLCellVM<PNLRowData, Security.ContractOneofCase> security_type_cell { get; }
public PNLCellVM<PNLRowData, Venue> exchange_cell { get; }
public PNLCellVM<PNLRowData, string> symbol_cell { get; }
// sod
public PNLCellVM<PNLRowData, double> sod_pnl { get; }
public PNLCellVM<PNLRowData, double> sod_net_pos { get; }
public PNLCellVM<PNLRowData, double> sod_net_avg_price_delta { get; }
public PNLCellVM<PNLRowData, double> sod_long_pos { get; }
public PNLCellVM<PNLRowData, double> sod_long_avg_price { get; }
public PNLCellVM<PNLRowData, double> sod_long_avg_price_delta { get; }
public PNLCellVM<PNLRowData, double> sod_short_pos { get; }
public PNLCellVM<PNLRowData, double> sod_short_avg_price { get; }
public PNLCellVM<PNLRowData, double> sod_short_avg_price_delta { get; }
// (market)
public PNLCellVM<PNLRowData, double> bid_price { get; }
public PNLCellVM<PNLRowData, double> mid_price { get; }
public PNLCellVM<PNLRowData, double> ask_price { get; }
public PNLCellVM<PNLRowData, double> last_price { get; }
public PNLCellVM<PNLRowData, double> settlement_price { get; }
// pnl price
public PNLCellVM<PNLRowData, double> pnl_price { get; }
// day
public PNLCellVM<PNLRowData, double> day_pnl { get; }
public PNLCellVM<PNLRowData, double> day_net_pos { get; }
public PNLCellVM<PNLRowData, double> day_net_avg_price_delta { get; }
public PNLCellVM<PNLRowData, double> day_long_pos { get; }
public PNLCellVM<PNLRowData, double> day_long_avg_price { get; }
public PNLCellVM<PNLRowData, double> day_long_avg_price_delta { get; }
public PNLCellVM<PNLRowData, double> day_short_pos { get; }
public PNLCellVM<PNLRowData, double> day_short_avg_price { get; }
public PNLCellVM<PNLRowData, double> day_short_avg_price_delta { get; }
// working
public PNLCellVM<PNLRowData, double> working_net_pos { get; }
public PNLCellVM<PNLRowData, double> working_net_avg_price_delta { get; }
public PNLCellVM<PNLRowData, WorkingOrderSideData> working_long_pos { get; }
public PNLCellVM<PNLRowData, double> working_long_avg_price { get; }
public PNLCellVM<PNLRowData, double> working_long_avg_price_delta { get; }
public PNLCellVM<PNLRowData, WorkingOrderSideData> working_short_pos { get; }
public PNLCellVM<PNLRowData, double> working_short_avg_price { get; }
public PNLCellVM<PNLRowData, double> working_short_avg_price_delta { get; }
// total
public PNLCellVM<PNLRowData, double> total_pnl { get; }
public PNLCellVM<PNLRowData, double> total_net_pos { get; }
public PNLCellVM<PNLRowData, double> total_net_avg_price_delta { get; }
public PNLCellVM<PNLRowData, double> total_long_pos { get; }
public PNLCellVM<PNLRowData, double> total_long_avg_price { get; }
public PNLCellVM<PNLRowData, double> total_long_avg_price_delta { get; }
public PNLCellVM<PNLRowData, double> total_short_pos { get; }
public PNLCellVM<PNLRowData, double> total_short_avg_price { get; }
public PNLCellVM<PNLRowData, double> total_short_avg_price_delta { get; }
public PNLCellVM<PNLRowData, bool> has_day_trades { get; }
public PNLCellVM<PNLRowData, double> cpp { get; }
public IList<mbts.ui.spark.CellAnimation> animations { get; private set; }
public bool is_selected {
get => _is_selected;
set {
if (Set(ref _is_selected, value))
is_selected_changed?.Invoke(value);
}
}
public void update(PNLRowData data) {
_refresh_normalized_data = true;
this.data = data;
_cell_map.on_row(data);
_animator.on_row(data);
_cell_map.set_appearances(_animator.cell_appearances);
}
public void on_settings(ImmutableDictionary<ColumnKeys, PNLCellAppearance> defaults, IReadOnlyList<IPNLAnimationTrigger<PNLRowData>> triggers) {
_animator.on_settings(defaults, triggers);
_cell_map.set_appearances(_animator.cell_appearances);
}
public bool Equals(PNLRowVM other) =>
other != null && key == other.key;
public override int GetHashCode() =>
key.GetHashCode();
public override string ToString() =>
$"account: {account_name} exchange: {exchange} symbol: {symbol} security: {security} buy_quantity: {day_long_pos} sell_quantity: {day_short_pos} pnl: {total_pnl}";
}
}
@@ -0,0 +1,674 @@
using bts.api;
using bts.api.core;
using bts.api.orderman;
using bts.collections;
using bts.utils;
using mbts;
using mbts.collections;
using mbts.persist;
using mbts.spark;
using mbts.spark.filters;
using mbts.ui;
using mbts.ui.controls;
using mbts.ui.settings;
using mbts.ui.shell;
using mbts.ui.utility;
using mbts.utils;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Reactive.Linq;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using ColumnKeys = bts.shell.pnl.PNLView.Columns;
using SecurityType = bts.api.Security.ContractOneofCase;
namespace bts.shell.pnl {
class PNLVM : DisposableViewModelBase, IHandleRowSelection {
static readonly IReadOnlyList<Func<PNLVM, IDataGridColumnFilter<PNLRowData>>> _filter_getters =
ObjectSnapshot.property_delegates_of_type<PNLVM, IDataGridColumnFilter<PNLRowData>>().ToReadOnlyList();
static readonly TimeSpan _auxiliary_grid_update_throttle = TimeSpan.FromSeconds(0.2);
readonly IBus _bus;
readonly OrdersModel _orders_model;
readonly IObservableCollectionAddRemoveOnly<PNLRowVM> _selected_rows = new ObservableSortedCollection<PNLRowVM>(PNLRowVM.key_comparer);
readonly IList<PNLRowData> _rows_data_wrapper;
readonly IList<PNLKey> _rows_key_wrapper;
readonly DeferrableActionWrapper _refresh_filter;
//
readonly Property<PNLCalculationType> _calculation_type;
PNLHeaderRowDisplay _header_row_display = PNLHeaderRowDisplay.Total;
PNLRowCollectionView _rows_view;
Brush _selection_highlight;
bool _is_paused;
// default appearances
ImmutableDictionary<ColumnKeys, PNLCellAppearance>
_default_appearances = ImmutableDictionary<ColumnKeys, PNLCellAppearance>.Empty,
_default_group_appearances = ImmutableDictionary<ColumnKeys, PNLCellAppearance>.Empty,
_default_header_appearances = ImmutableDictionary<ColumnKeys, PNLCellAppearance>.Empty;
// triggers
IReadOnlyList<IPNLAnimationTrigger<PNLRowData>>
_triggers = Array.Empty<IPNLAnimationTrigger<PNLRowData>>();
IReadOnlyList<IPNLAnimationTrigger<PNLNormalizedData>>
_group_triggers = Array.Empty<IPNLAnimationTrigger<PNLNormalizedData>>(),
_header_triggers = Array.Empty<IPNLAnimationTrigger<PNLNormalizedData>>();
// pause/resume
(TableUpdate<PNLRowData, PNLKey> filtered, TableUpdate<PNLRowData, PNLKey> raw)? _resume;
public PNLVM(IBus bus, OrdersModel orders_model, PNLSettings settings)
: this(bus, orders_model, settings, orders_model.security_account_pnl) { }
PNLVM(IBus bus, OrdersModel orders_model, PNLSettings settings, IObservable<TableUpdate<PNLRowData, PNLKey>> pnl_source) {
_bus = bus;
_orders_model = orders_model;
_rows_data_wrapper = rows.WrapSelector(r => r.data, d => new PNLRowVM(d, _default_appearances, _triggers));
_rows_key_wrapper = rows.WrapSelector(r => r.key, null);
_calculation_type = backing(nameof(calculation_type), PNLCalculationType.Waterfall);
anchors.AddRange(settings.anchors);
// commands
set_anchor_cmd = new DelegateCommand<ColumnKeys>(key => anchors.SetItems(new[] { key }));
append_anchor_cmd = new DelegateCommand<ColumnKeys>(key => {
if (!anchors.Remove(key))
anchors.Add(key);
});
// filters
account_filter = DiscreteValuesFilterVM.Common.account<PNLRowData>(orders_model, r => new AccountTuple(r.key.account_id, r.account_name), nameof(PNLRowVM.account_name));
_refresh_filter = new DeferrableActionWrapper(() => _filter_changed?.Invoke(get_filters_states().build_predicate()));
var filter_src = Observable.Create<Func<PNLRowData, bool>>(o => {
o.OnNext(get_filters_states().build_predicate());
_filter_changed += o.OnNext;
return Disposable.Create(() => _filter_changed -= o.OnNext);
});
foreach (var f in _all_filters)
f.filter_changed += _refresh_filter.execute;
// totals/selected headers
total_aggregate = new PNLHeaderRowVM(_default_header_appearances, _header_triggers);
selected_aggregate = new PNLHeaderRowVM(_default_header_appearances, _header_triggers);
// load/subscribe to data
load_from(settings);
_rows_view = new PNLRowCollectionView(rows, _default_group_appearances, _group_triggers);
_dispose = Disposable.Create(
pnl_source.Operate(
_calculation_type.observe_current(),
(data, calc) => data.with(calc_type: calc)
).FilterWithRaw(filter_src, PNLRowData.key_fn, null)
.OnDispatcher()
.Subscribe(_update),
Disposable.Create(() => {
foreach (var f in _all_filters)
f.filter_changed -= _refresh_filter.execute;
rows.DisposeAll();
_rows_view.Groups.OfType<IDisposable>().DisposeAll();
}));
}
event Action<SelectionChangedEventArgs> _selection_changed;
event Action<TableUpdate<PNLRowData, PNLKey>> _updated;
event Action<Func<PNLRowData, bool>> _filter_changed;
// rows/selected rows
public ObservableCollection<PNLRowVM> rows { get; } = new ObservableCollection<PNLRowVM>();
public ICollectionView rows_view => _rows_view;
public int filtered_row_count => rows.Count;
public int total_row_count { get; private set; }
public IReadOnlyObservableCollection<PNLRowVM> selected_rows => _selected_rows;
// display/grouping
public PNLHeaderRowDisplay header_row_display { get => _header_row_display; set => Set(ref _header_row_display, value); }
public ObservableCollection<ColumnKeys> anchors { get; } = new ObservableCollection<ColumnKeys>();
// calc
public PNLCalculationType calculation_type { get => _calculation_type.Value; set => _calculation_type.set(value); }
public PNLHeaderRowVM total_aggregate { get; }
public PNLHeaderRowVM selected_aggregate { get; }
public bool is_paused {
get => _is_paused;
set {
if (Set(ref _is_paused, value) && !value && _resume.HasValue) {
var curr_state = ImmutableDictionary<PNLKey, PNLRowData>.Empty.AddRange(rows.Select(r => new KeyValuePair<PNLKey, PNLRowData>(r.key, r.data)));
var reset = TableUpdate<PNLRowData, PNLKey>.update(
new TableUpdate<PNLRowData, PNLKey>(
curr_state,
curr_state.Values,
Enumerable.Empty<Change<PNLRowData>>(),
Enumerable.Empty<PNLRowData>(),
PNLRowData.key_fn),
_resume.Value.filtered.state,
EqualityComparer<PNLRowData>.Default);
_update((reset, _resume.Value.raw));
_resume = null;
}
}
}
public Brush selection_highlight { get => _selection_highlight; set => Set(ref _selection_highlight, value); }
// commands
public ICommand set_anchor_cmd { get; }
public ICommand append_anchor_cmd { get; }
public ICommand show_working_orders_cmd { get; }
public void set_all_expanded(bool is_expanded) =>
_rows_view.set_all_expanded(is_expanded);
#region Filters
/// security
public IDataGridColumnPersistentFilter<PNLRowData, uint[]> account_filter { get; }
public IDataGridColumnPersistentFilter<PNLRowData, Venue[]> exchange_filter { get; } = DiscreteValuesFilterVM.Common.exchange<PNLRowData>(r => r.security.exchange_or_default(), nameof(PNLRowVM.exchange));
public IDataGridColumnPersistentFilter<PNLRowData, string[]> symbol_filter { get; } = DiscreteValuesFilterVM.Common.symbol<PNLRowData>(r => r.security.symbol_or_default(), nameof(PNLRowVM.symbol));
public IDataGridColumnPersistentFilter<PNLRowData, string> security_filter { get; } = new RegexFilterVM<PNLRowData>(r => r.security?.ToString(), nameof(PNLRowData.security));
public IDataGridColumnPersistentFilter<PNLRowData, SecurityType[]> security_type_filter { get; } = DiscreteValuesFilterVM.Create<PNLRowData, SecurityType>(
EnumHelper.get_values_except(SecurityType.Option, SecurityType.Synthetic).ToArray(),
r => r.security?.ContractCase ?? SecurityType.None,
nameof(PNLRowData.security));
/// sod
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> sod_pnl_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.sod_pnl);
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> sod_long_pos_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.sod.long_pos);
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> sod_long_avg_price_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.sod.long_avg);
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> sod_long_avg_price_delta_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.sod_long_avg_price_delta);
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> sod_short_pos_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.sod.short_pos);
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> sod_short_avg_price_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.sod.short_avg);
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> sod_short_avg_price_delta_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.sod_short_avg_price_delta);
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> sod_net_pos_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.sod.net_pos);
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> sod_net_avg_price_delta_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.sod_net_avg_price_delta);
/// day
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> day_pnl_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.day_pnl);
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> day_long_pos_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.day.long_pos);
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> day_long_avg_price_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.day_long_avg_price_raw);
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> day_long_avg_price_delta_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.day_long_avg_price_delta);
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> day_short_pos_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.day.short_pos);
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> day_short_avg_price_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.day_short_avg_price_raw);
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> day_short_avg_price_delta_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.day_short_avg_price_delta_raw);
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> day_net_pos_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.day.net_pos);
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> day_net_avg_price_delta_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.day_net_avg_price_delta_raw);
/// working (long)
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> working_long_pos_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.working_long_pos);
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> working_long_avg_price_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.working_long_avg_price_raw);
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> working_long_avg_price_delta_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.working_long_avg_price_delta_raw);
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> working_short_pos_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.working_short_pos);
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> working_short_avg_price_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.working_short_avg_price_raw);
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> working_short_avg_price_delta_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.working_short_avg_price_delta_raw);
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> working_net_pos_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.working_net_pos);
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> working_net_avg_price_delta_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.working_net_avg_price_delta_raw);
/// total
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> total_pnl_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.total_pnl);
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> total_long_pos_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.total_long_pos);
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> total_long_avg_price_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.total_long_avg_price);
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> total_long_avg_price_delta_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.total_long_avg_price_delta);
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> total_short_pos_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.total_short_pos);
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> total_short_avg_price_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.total_short_avg_price);
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> total_short_avg_price_delta_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.total_short_avg_price_delta);
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> total_net_pos_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.total_net_pos);
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> total_net_avg_price_delta_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.total_net_avg_price_delta);
/// market
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> bid_price_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.bid_price);
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> ask_price_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.ask_price);
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> mid_price_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.mid_price);
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> last_price_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.last_price);
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> settlement_price_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.settlement_price);
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> pnl_price_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.pnl_price);
/// misc
public IDataGridColumnPersistentFilter<PNLRowData, bool[]> has_day_trades_filter { get; } = DiscreteValuesFilterVM.Create<PNLRowData, bool>(new[] { true, false }, r => r.has_day_trades);
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> cpp_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.cpp);
IEnumerable<IDataGridColumnFilter<PNLRowData>> _all_filters =>
_filter_getters.Select(get => get(this));
public FilterStates get_filters_states() => new FilterStates {
account = account_filter.get_state(),
exchange = exchange_filter.get_state(),
symbol = symbol_filter.get_state(),
security = security_filter.get_state(),
security_type = security_type_filter.get_state(),
/// sod
sod_pnl = sod_pnl_filter.get_state(),
sod_long_pos = sod_long_pos_filter.get_state(),
sod_long_avg_price = sod_long_avg_price_filter.get_state(),
sod_long_avg_price_delta = sod_long_avg_price_delta_filter.get_state(),
sod_short_pos = sod_short_pos_filter.get_state(),
sod_short_avg_price = sod_short_avg_price_filter.get_state(),
sod_short_avg_price_delta = sod_short_avg_price_delta_filter.get_state(),
sod_net_pos = sod_net_pos_filter.get_state(),
sod_net_avg_price_delta = sod_net_avg_price_delta_filter.get_state(),
/// day
day_pnl = day_pnl_filter.get_state(),
day_long_pos = day_long_pos_filter.get_state(),
day_long_avg_price = day_long_avg_price_filter.get_state(),
day_long_avg_price_delta = day_long_avg_price_delta_filter.get_state(),
day_short_pos = day_short_pos_filter.get_state(),
day_short_avg_price = day_short_avg_price_filter.get_state(),
day_short_avg_price_delta = day_short_avg_price_delta_filter.get_state(),
day_net_pos = day_net_pos_filter.get_state(),
day_net_avg_price_delta = day_net_avg_price_delta_filter.get_state(),
/// total
total_pnl = total_pnl_filter.get_state(),
total_long_pos = total_long_pos_filter.get_state(),
total_long_avg_price = total_long_avg_price_filter.get_state(),
total_long_avg_price_delta = total_long_avg_price_delta_filter.get_state(),
total_short_pos = total_short_pos_filter.get_state(),
total_short_avg_price = total_short_avg_price_filter.get_state(),
total_short_avg_price_delta = total_short_avg_price_delta_filter.get_state(),
total_net_pos = total_net_pos_filter.get_state(),
total_net_avg_price_delta = total_net_avg_price_delta_filter.get_state(),
/// working
working_long_pos = working_long_pos_filter.get_state(),
working_long_avg_price = working_long_avg_price_filter.get_state(),
working_long_avg_price_delta = working_long_avg_price_delta_filter.get_state(),
working_short_pos = working_short_pos_filter.get_state(),
working_short_avg_price = working_short_avg_price_filter.get_state(),
working_short_avg_price_delta = working_short_avg_price_delta_filter.get_state(),
working_net_pos = working_net_pos_filter.get_state(),
working_net_avg_price_delta = working_net_avg_price_delta_filter.get_state(),
/// market
bid_price = bid_price_filter.get_state(),
ask_price = ask_price_filter.get_state(),
mid_price = mid_price_filter.get_state(),
last_price = last_price_filter.get_state(),
settlement_price = settlement_price_filter.get_state(),
pnl_price = pnl_price_filter.get_state(),
/// misc
has_day_trades = has_day_trades_filter.get_state()
};
#endregion
public void load_from(PNLSettings settings) {
selection_highlight = settings.grid.selection_highlight.to_brush();
calculation_type = settings.calculation_type;
if (settings.filters != null) {
using (_refresh_filter.defer_execution()) {
var f = settings.filters;
/// security
account_filter.load_state(f.account);
exchange_filter.load_state(f.exchange);
symbol_filter.load_state(f.symbol);
security_filter.load_state(f.security);
security_type_filter.load_state(f.security_type);
/// sod
sod_pnl_filter.load_state(f.sod_pnl);
sod_long_pos_filter.load_state(f.sod_long_pos);
sod_long_avg_price_filter.load_state(f.sod_long_avg_price);
sod_long_avg_price_delta_filter.load_state(f.sod_long_avg_price_delta);
sod_short_pos_filter.load_state(f.sod_short_pos);
sod_short_avg_price_filter.load_state(f.sod_short_avg_price);
sod_short_avg_price_delta_filter.load_state(f.sod_short_avg_price_delta);
sod_net_pos_filter.load_state(f.sod_net_pos);
sod_net_avg_price_delta_filter.load_state(f.sod_net_avg_price_delta);
/// day
day_pnl_filter.load_state(f.day_pnl);
day_long_pos_filter.load_state(f.day_long_pos);
day_long_avg_price_filter.load_state(f.day_long_avg_price);
day_long_avg_price_delta_filter.load_state(f.day_long_avg_price_delta);
day_short_pos_filter.load_state(f.day_short_pos);
day_short_avg_price_filter.load_state(f.day_short_avg_price);
day_short_avg_price_delta_filter.load_state(f.day_short_avg_price_delta);
day_net_pos_filter.load_state(f.day_net_pos);
day_net_avg_price_delta_filter.load_state(f.day_net_avg_price_delta);
/// total
total_pnl_filter.load_state(f.total_pnl);
total_long_pos_filter.load_state(f.total_long_pos);
total_long_avg_price_filter.load_state(f.total_long_avg_price);
total_long_avg_price_delta_filter.load_state(f.total_long_avg_price_delta);
total_short_pos_filter.load_state(f.total_short_pos);
total_short_avg_price_filter.load_state(f.total_short_avg_price);
total_short_avg_price_delta_filter.load_state(f.total_short_avg_price_delta);
total_net_pos_filter.load_state(f.total_net_pos);
/// working
working_long_pos_filter.load_state(f.working_long_pos);
working_long_avg_price_filter.load_state(f.working_long_avg_price);
working_long_avg_price_delta_filter.load_state(f.working_long_avg_price_delta);
working_short_pos_filter.load_state(f.working_short_pos);
working_short_avg_price_filter.load_state(f.working_short_avg_price);
working_short_avg_price_delta_filter.load_state(f.working_short_avg_price_delta);
working_net_pos_filter.load_state(f.working_net_pos);
working_net_avg_price_delta_filter.load_state(f.working_net_avg_price_delta);
/// market
bid_price_filter.load_state(f.bid_price);
mid_price_filter.load_state(f.mid_price);
ask_price_filter.load_state(f.ask_price);
last_price_filter.load_state(f.last_price);
settlement_price_filter.load_state(f.settlement_price);
pnl_price_filter.load_state(f.pnl_price);
/// misc
has_day_trades_filter.load_state(f.has_day_trades);
cpp_filter.load_state(f.cpp);
}
}
anchors.sync_with(settings.anchors);
header_row_display = settings.header_row_display;
//
map_appearances(ref _default_appearances, settings.grid.columns, settings.grid_defaults);
map_appearances(ref _default_group_appearances, settings.group_columns, settings.group_cell_appearance.to_appearance().merge(settings.grid_defaults));
map_appearances(ref _default_header_appearances, settings.header_columns, settings.header_cell_appearance.to_appearance().merge(settings.grid_defaults));
_triggers = settings.triggers();
_group_triggers = settings.group_triggers();
_header_triggers = settings.header_triggers();
foreach (var r in rows)
r.on_settings(_default_appearances, _triggers);
total_aggregate.on_settings(_default_header_appearances, _header_triggers);
selected_aggregate.on_settings(_default_header_appearances, _header_triggers);
void map_appearances(ref ImmutableDictionary<ColumnKeys, PNLCellAppearance> from, IEnumerable<GridColumnSettings> col_settings, PNLCellAppearance fallback_appearance) {
var dabd = from.ToBuilder();
foreach (var k in EnumHelper.get_values<ColumnKeys>()) {
if (GridSettingsHelper.try_find_column(col_settings, k, out var match))
dabd[k] = new PNLCellAppearance(match.font_color_override, match.background_override, match.font_weight_override).merge(fallback_appearance);
}
from = dabd.ToImmutable();
}
if (_rows_view == null)
_rows_view = new PNLRowCollectionView(rows, _default_group_appearances, _group_triggers);
else _rows_view.on_settings(_default_group_appearances, _group_triggers);
}
#region FillsVM Factory
public FillsVM mk_fills_vm(PNLRowVM row, ColumnKeys key) {
var state = PNLSettings.DefaultAuxiliaryFills;
_append_key_filters(state.filters, key);
_append_row_filters(state.filters, row);
state.grid = _hide_cols(state.grid, anchors);
var src = _orders_model.on_fill_tuples.Filter(ft => ft.fill.SecurityKey == row.key.security_id && ft.account_id == row.key.account_id);
return new FillsVM(_bus, _orders_model, Observable.Return(state), src, new WindowSettingsManager<mbts.ui.settings.GridSettings>.DisconnectedImpl(state.grid));
}
public FillsVM mk_fills_vm(PNLRowCollectionViewGroup group, ColumnKeys key) {
var state = PNLSettings.DefaultAuxiliaryFills;
_append_key_filters(state.filters, key);
_append_group_filters(state.filters, group, anchors);
// hide all columns with implicit data (i.e., the anchors)
state.grid = _hide_cols(state.grid, anchors.WrapSubRange(0, group.level));
return new FillsVM(_bus, _orders_model, Observable.Return(state), new WindowSettingsManager<mbts.ui.settings.GridSettings>.DisconnectedImpl(state.grid));
}
public FillsVM mk_fills_vm(PNLHeaderRowDisplay header, ColumnKeys key) {
var state = fills.FillsSettings.Default();
state.grid = orderbook.OrderBookStartupData.DefaultFillsGrid.general;
state.filters = new fills.FillsFilterStates();
state.grid = _hide_cols(state.grid, anchors);
_append_key_filters(state.filters, key);
var src = _orders_model.on_fill_tuples;
switch (header) {
case PNLHeaderRowDisplay.SelectedTotal: {
var filter_src = System.Reactive.Linq.ObservableExtensions.FromEventThrottle<SelectionChangedEventArgs, Func<FillTuple, bool>>(
cb => _selection_changed += cb,
cb => _selection_changed -= cb,
_ => predicate_factory(_selected_rows.Select(r => r.key)),
_auxiliary_grid_update_throttle)
.StartWith(() => predicate_factory(_selected_rows.Select(r => r.key)));
src = src.Filter(filter_src, FillTuple.key_fn);
}
break;
case PNLHeaderRowDisplay.Total: {
var filter_src = System.Reactive.Linq.ObservableExtensions.FromEventThrottle<TableUpdate<PNLRowData, PNLKey>, Func<FillTuple, bool>>(
cb => _updated += cb,
cb => _updated -= cb,
t => predicate_factory(t.state.Keys),
_auxiliary_grid_update_throttle)
.StartWith(() => predicate_factory(rows.Select(r => r.key)));
src = src.Filter(filter_src, FillTuple.key_fn);
}
break;
}
return new FillsVM(_bus, _orders_model, Observable.Return(state), src, new WindowSettingsManager<mbts.ui.settings.GridSettings>.DisconnectedImpl(state.grid));
//
Func<FillTuple, bool> predicate_factory(IEnumerable<PNLKey> keys) {
var key_set = new HashSet<PNLKey>(keys);
return new Func<FillTuple, bool>(ft => key_set.Contains(new PNLKey(ft.account_id, ft.fill.SecurityKey)));
}
}
#endregion
#region OrderBookVM Factory
public OrderBookVM mk_orderbook_vm(PNLHeaderRowDisplay header, ColumnKeys key) {
var state = new orderbook.OrderBookStartupData(
new orderbook.FilterStates(),
orderbook.OrderBookStartupData.default_mini_grid,
use_defaults: false);
_append_key_filters(state.filters, key);
state.orderbook_grid.general = _hide_cols(state.orderbook_grid.general, anchors);
var src = _orders_model.on_order_tuples_with_root_and_piq;
switch (header) {
case PNLHeaderRowDisplay.SelectedTotal: {
var filter_src = System.Reactive.Linq.ObservableExtensions.FromEventThrottle<SelectionChangedEventArgs, Func<OrderSecurityRootPair, bool>>(
cb => _selection_changed += cb,
cb => _selection_changed -= cb,
_ => predicate_factory(_selected_rows.Select(r => r.key)),
_auxiliary_grid_update_throttle);
src = src.Filter(filter_src, OrderSecurityHelper.root_pair_key_fn);
}
break;
case PNLHeaderRowDisplay.Total: {
var filter_src = System.Reactive.Linq.ObservableExtensions.FromEventThrottle<TableUpdate<PNLRowData, PNLKey>, Func<OrderSecurityRootPair, bool>>(
cb => _updated += cb,
cb => _updated -= cb,
t => predicate_factory(t.state.Keys),
_auxiliary_grid_update_throttle)
.StartWith(() => predicate_factory(rows.Select(r => r.key)));
src = src.Filter(filter_src, OrderSecurityHelper.root_pair_key_fn);
}
break;
}
var settings_man = new WindowSettingsManager<orderbook.OrderBookGridSettings>.DisconnectedImpl(state.orderbook_grid);
var obvm = new OrderBookVM(_bus, null, _orders_model, src, settings_man, true);
obvm.columns.InsertRange(0, OrderBookVM.OrderBookColumnCollection.editable_columns);
return obvm;
Func<OrderSecurityRootPair, bool> predicate_factory(IEnumerable<PNLKey> keys) {
var key_set = new HashSet<PNLKey>(keys);
return new Func<OrderSecurityRootPair, bool>(o => key_set.Contains(new PNLKey(o.order.order.account_id(), o.order.order.security_key())));
}
}
public OrderBookVM mk_orderbook_vm(PNLRowCollectionViewGroup group, ColumnKeys key) {
var filters = new orderbook.FilterStates();
_append_group_filters(filters, group, anchors);
return _mk_orderbook_vm_common(filters, anchors.WrapSubRange(0, group.level));
}
public OrderBookVM mk_orderbook_vm(PNLRowVM row, ColumnKeys key) {
var filters = new orderbook.FilterStates();
_append_key_filters(filters, key);
_append_row_filters(filters, row);
return _mk_orderbook_vm_common(filters, anchors.Concat(new[] { ColumnKeys.Account, ColumnKeys.Symbol, ColumnKeys.Security }).Distinct());
}
OrderBookVM _mk_orderbook_vm_common(orderbook.FilterStates filters, IEnumerable<ColumnKeys> hide_counterparts) {
var state = new orderbook.OrderBookStartupData(filters, orderbook.OrderBookStartupData.default_mini_grid, use_defaults: false);
state.orderbook_grid.general = _hide_cols(state.orderbook_grid.general, hide_counterparts);
var settings_man = new WindowSettingsManager<orderbook.OrderBookGridSettings>.DisconnectedImpl(state.orderbook_grid);
var obvm = new OrderBookVM(_bus, null, _orders_model, settings_man, true);
foreach (var c in OrderBookVM.OrderBookColumnCollection.editable_columns)
obvm.columns.Insert(0, c);
obvm.load_from(state);
return obvm;
}
#endregion
static GridSettings _hide_cols(GridSettings grid, IEnumerable<ColumnKeys> columns) =>
grid.with(_hide_cols(grid.columns, columns));
static IEnumerable<GridColumnSettings> _hide_cols(IEnumerable<GridColumnSettings> cols, IEnumerable<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;
}
#region Fills Popup Utils
static void _append_key_filters(fills.FillsFilterStates f, ColumnKeys key) {
switch (key) {
case ColumnKeys.SODNetPos:
case ColumnKeys.SODLongPos:
case ColumnKeys.SODShortPos:
f.fill_kind = new[] { Fill.Types.FillKind.Sod };
break;
case ColumnKeys.DayNetPos:
case ColumnKeys.DayLongPos:
case ColumnKeys.DayShortPos:
f.fill_kind = new[] { Fill.Types.FillKind.Standard, Fill.Types.FillKind.Leg };
break;
}
switch (key) {
case ColumnKeys.SODLongPos:
case ColumnKeys.DayLongPos:
case ColumnKeys.TotalLongPos:
f.side = new[] { Side.Buy };
break;
case ColumnKeys.SODShortPos:
case ColumnKeys.DayShortPos:
case ColumnKeys.TotalShortPos:
f.side = new[] { Side.Sell };
break;
}
}
static void _append_group_filters(fills.FillsFilterStates f, PNLRowCollectionViewGroup group, IReadOnlyList<ColumnKeys> anchors) {
if (group != null) {
int anchor_pos = group.level - 1;
if (anchor_pos < 0)
throw new InvalidOperationException("Group level is less than 1.");
if (anchor_pos >= anchors.Count)
throw new InvalidOperationException("Group level is greater than the number of anchors.");
_append_fills_filter(f, group, anchors[anchor_pos]);
group = group.parent;
while (anchor_pos-- > 0 && group != null) {
_append_fills_filter(f, group, anchors[anchor_pos]);
group = group.parent;
}
}
}
static void _append_fills_filter(fills.FillsFilterStates filter, PNLRowCollectionViewGroup group, ColumnKeys key) {
switch (key) {
case ColumnKeys.Account: filter.account = new[] { group.Name.ToString() }; break;
case ColumnKeys.Symbol: filter.symbol = new[] { group.Name.ToString() }; break;
case ColumnKeys.Exchange: filter.exchange = new[] { (Venue)group.Name }; break;
case ColumnKeys.Security:
if (group.Name is SecurityImmutable sec)
filter.security = new SecurityCriteria(sec);
break;
default: throw new NotImplementedException();
}
}
static void _append_row_filters(fills.FillsFilterStates f, PNLRowVM row) {
f.account = new[] { row.account_name };
f.security = SecurityCriteria.from_keys(new[] { row.key.security_id });
}
static void _append_key_filters(orderbook.FilterStates f, ColumnKeys key) {
switch (key) {
case ColumnKeys.WorkingLongPos:
case ColumnKeys.DayLongPos:
case ColumnKeys.TotalLongPos:
f.sides = new[] { Side.Buy };
break;
case ColumnKeys.WorkingShortPos:
case ColumnKeys.DayShortPos:
case ColumnKeys.TotalShortPos:
f.sides = new[] { Side.Sell };
break;
}
switch (key) {
case ColumnKeys.WorkingLongPos:
case ColumnKeys.WorkingShortPos:
case ColumnKeys.WorkingNetPos:
f.statuses = new[] { OrderStatus.PendingWorking, OrderStatus.Working };
break;
}
}
static void _append_group_filters(orderbook.FilterStates f, PNLRowCollectionViewGroup group, IReadOnlyList<ColumnKeys> anchors) {
if (group != null) {
int anchor_pos = group.level - 1;
if (anchor_pos < 0)
throw new InvalidOperationException("Group level is less than 1.");
if (anchor_pos >= anchors.Count)
throw new InvalidOperationException("Group level is greater than the number of anchors.");
_append_orderbook_filter(f, group, anchors[anchor_pos]);
group = group.parent;
while (anchor_pos-- > 0 && group != null) {
_append_orderbook_filter(f, group, anchors[anchor_pos]);
group = group.parent;
}
}
}
static void _append_row_filters(orderbook.FilterStates f, PNLRowVM row) {
f.accounts = new[] { row.account_name };
f.security_criteria = SecurityCriteria.from_keys(new[] { row.key.security_id });
}
static void _append_orderbook_filter(orderbook.FilterStates filter, PNLRowCollectionViewGroup g, ColumnKeys key) {
switch (key) {
case ColumnKeys.Account: filter.accounts = new[] { g.Name.ToString() }; break;
case ColumnKeys.Symbol: filter.symbols = new[] { g.Name.ToString() }; break;
case ColumnKeys.Exchange: filter.exchanges = new[] { (Venue)g.Name }; break;
case ColumnKeys.Security:
if (g.Name is SecurityImmutable sec)
filter.security_criteria = new SecurityCriteria(sec);
break;
default: throw new NotImplementedException();
}
}
#endregion
void _update((TableUpdate<PNLRowData, PNLKey> filtered, TableUpdate<PNLRowData, PNLKey> raw) tup) {
if (_is_paused) {
_resume = tup;
return;
}
var (filtered, raw) = tup;
int old_total_row_count = total_row_count;
int old_filtered_row_count = filtered_row_count;
var total_pnl = total_aggregate.data;
// don't adjust selected_pnl incrementally - it may be possible for the selection to change throughout the update, making incremental calculations tricky
foreach (var v in filtered.removed) {
int i = CollectionHelper.binary_search(_rows_data_wrapper, v, PNLRowData.key_comparer);
Log.assert(i >= 0, $"{typeof(PNLVM)} received an update with a removed item that was not found via binary search.");
if (i >= 0) {
if (rows[i].security_type == SecurityType.Future)
total_pnl -= rows[i].normalized_data;
rows.RemoveAt(i);
}
}
foreach (var v in filtered.added) {
var r = new PNLRowVM(v.with(calc_type: calculation_type), _default_appearances, _triggers);
rows.InsertSorted(r, PNLRowVM.key_comparer);
if (r.security_type == SecurityType.Future)
total_pnl += r.normalized_data;
}
foreach (var d in filtered.changed) {
int i = CollectionHelper.binary_search(_rows_data_wrapper, d, PNLRowData.key_comparer);
if (i >= 0) {
var r = rows[i];
if (r.security_type == SecurityType.Future)
total_pnl -= r.normalized_data;
r.update(d);
if (r.security_type == SecurityType.Future)
total_pnl += r.normalized_data;
}
}
if (_selected_rows.Count > 0) {
selected_aggregate.data = _selected_rows
.Where(r => r.security_type == SecurityType.Future)
.Aggregate(PNLNormalizedData.zero, (d, r) => d + r.normalized_data);
}
//
total_aggregate.data = total_pnl;
total_row_count = tup.raw.state.Count;
if (total_row_count != old_total_row_count)
RaisePropertyChanged(nameof(total_row_count));
if (filtered_row_count != old_filtered_row_count)
RaisePropertyChanged(nameof(filtered_row_count));
_updated?.Invoke(filtered);
}
void IHandleRowSelection.row_selection_changed(SelectionChangedEventArgs e) {
RowSelectionHelper.apply_changes(_selected_rows, e);
// adding/subtracting pnl incrementally can lose precision over time, due to rounding errors.
// do the full calculation on each selection change, as long as it doesn't hurt perf (which seems unlikely).
selected_aggregate.data = _selected_rows.Aggregate(PNLNormalizedData.zero, (d, r) => d + r.normalized_data);
_selection_changed?.Invoke(e);
}
}
}
@@ -0,0 +1,33 @@
using bts.collections;
using mbts.ui.utility;
using System.Collections.Generic;
using System.Dynamic;
namespace bts.shell.pnl {
class AnchorIndex {
readonly IDictionary<PNLView.Columns, bool> _wrapped_anchors;
public AnchorIndex() {
IDictionary<string, object> anchors_dict = anchors = new ExpandoObject();
_wrapped_anchors = MappedDictionary.create_mutable(
anchors_dict,
EnumHelper.parse<PNLView.Columns>,
key => key.ToString(),
obj => (bool)obj,
_ => _);
// initialize the dictionary with every key (the keys should never change)
foreach (var k in _keys)
_wrapped_anchors[k] = false;
}
public dynamic anchors { get; }
IReadOnlyList<PNLView.Columns> _keys => EnumHelper.get_values<PNLView.Columns>();
public void reset(IEnumerable<PNLView.Columns> anchor_list) {
var anchor_set = new HashSet<PNLView.Columns>(anchor_list.empty_if_null());
foreach (var key in _keys)
_wrapped_anchors[key] = anchor_set.Contains(key);
}
}
}
@@ -0,0 +1,52 @@
namespace bts.shell.pnl {
readonly struct PNLCell {
public readonly object item;
public readonly PNLView.Columns column;
public PNLCell(object item, PNLView.Columns column) {
this.item = item;
this.column = column;
}
public override string ToString() =>
$"Item: {item}, Column: {column}";
}
readonly struct PNLRowCell {
public readonly PNLRowVM row;
public readonly PNLView.Columns column;
public PNLRowCell(PNLRowVM row, PNLView.Columns column) {
this.row = row;
this.column = column;
}
}
readonly struct PNLGroupCell {
public readonly PNLRowCollectionViewGroup group;
public readonly PNLView.Columns column;
public PNLGroupCell(PNLRowCollectionViewGroup group, PNLView.Columns column) {
this.group = group;
this.column = column;
}
}
readonly struct PNLHeaderRowCell {
public readonly PNLHeaderRowDisplay header_row;
public readonly PNLView.Columns column;
public PNLHeaderRowCell(PNLHeaderRowDisplay header_row, PNLView.Columns column) {
this.header_row = header_row;
this.column = column;
}
}
static class PNLCellExtensions {
public static OneOf<PNLRowCell, PNLGroupCell, PNLHeaderRowCell> oneof(this PNLCell cell) =>
OneOf<PNLRowVM, PNLRowCollectionViewGroup, PNLHeaderRowDisplay>.infer(cell.item).Extend(
r => new PNLRowCell(r, cell.column),
g => new PNLGroupCell(g, cell.column),
h => new PNLHeaderRowCell(h, cell.column));
}
}
@@ -0,0 +1,84 @@
using bts.utils;
using mbts.ui.controls;
using mbts.ui.shell;
using mbts.ui.utility.text;
using System.Windows;
using System.Windows.Media;
using FontFamily = System.Windows.Media.FontFamily;
namespace bts.shell.pnl {
class PNLColumn : DIYGrid.ColumnBase<PNLView.Columns>, IRaiseNotifyPropertyChanged {
static readonly IPropertyChangedDetector<PNLColumn>
_group_settings_change_detector = ObjectSnapshot.build_detector<PNLColumn>(
nameof(group_font_weight_override),
nameof(group_font_weight),
nameof(group_font_color_override),
nameof(group_font_color),
nameof(group_font_family),
nameof(group_font_size)),
_header_settings_change_detector = ObjectSnapshot.build_detector<PNLColumn>(
nameof(header_font_weight_override),
nameof(header_font_weight),
nameof(header_font_color_override),
nameof(header_font_color),
nameof(header_font_family),
nameof(header_font_size));
double _group_font_size = 11, _header_font_size = 12;
int? _anchor_index;
FontFamily
_group_font_family = TextUtils.StandardFontFamily,
_header_font_family = TextUtils.StandardFontFamily;
public PNLColumn() {
// default to be hidden, so columns are not rendered until their settings are loaded,
// in which case, they will only be rendered if 'settings.is_visible' is true.
is_visible = false;
}
public int? anchor_index {
get => _anchor_index;
set => set(ref _anchor_index, value);
}
// group
public FontWeight? group_font_weight_override { get; private set; }
public Color? group_font_color_override { get; private set; }
public FontFamily group_font_family => _group_font_family ?? font_family;
public double group_font_size => double.IsNaN(_group_font_size) || _group_font_size == 0 ? font_size : _group_font_size;
public FontWeight group_font_weight => group_font_weight_override ?? font_weight;
public Color group_font_color => group_font_color_override ?? font_color;
public FontWeight? header_font_weight_override { get; private set; }
public Color? header_font_color_override { get; private set; }
public FontFamily header_font_family => _header_font_family ?? font_family;
public double header_font_size => double.IsNaN(_header_font_size) || _header_font_size == 0 ? font_size : _header_font_size;
public FontWeight header_font_weight => header_font_weight_override ?? font_weight;
public Color header_font_color => header_font_color_override ?? font_color;
public void load_group_params(
double group_font_size = double.NaN, FontWeight? group_font_weight = null, Color? group_font_color = null, FontFamily group_font_family = null) {
using (_group_settings_change_detector.detect_changes(this)) {
_group_font_size = group_font_size;
_group_font_family = group_font_family;
group_font_weight_override = group_font_weight;
group_font_color_override = group_font_color;
}
}
public void load_header_params(
double header_font_size = double.NaN, FontWeight? header_font_weight = null, Color? header_font_color = null, FontFamily header_font_family = null) {
using (_header_settings_change_detector.detect_changes(this)) {
_header_font_size = header_font_size;
_header_font_family = header_font_family;
header_font_weight_override = header_font_weight;
header_font_color_override = header_font_color;
}
}
void IRaiseNotifyPropertyChanged.raise_notify_property_changed(string prop) =>
raise_prop_changed(prop);
protected override void on_key_initialized(PNLView.Columns key) =>
id = key.ToString();
}
}
@@ -0,0 +1,458 @@
using bts.collections;
using bts.utils;
using mbts;
using mbts.ui;
using mbts.ui.shell;
using mbts.ui.utility;
using mbts.utils;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Reactive.Linq;
using System.Runtime.CompilerServices;
using System.Windows.Data;
using CellAnimation = mbts.ui.spark.CellAnimation;
using ColumnKeys = bts.shell.pnl.PNLView.Columns;
namespace bts.shell.pnl {
class PNLRowCollectionView : ListCollectionView {
ReadOnlyObservableCollection<object> _base_groups, _groups_wrapper;
ObservableAutoWrapper<object, object> _groups;
// appearance/triggers
ImmutableDictionary<ColumnKeys, PNLCellAppearance> _defaults_appearances;
IReadOnlyList<IPNLAnimationTrigger<PNLNormalizedData>> _triggers;
public PNLRowCollectionView(IList list, ImmutableDictionary<ColumnKeys, PNLCellAppearance> default_appearances, IReadOnlyList<IPNLAnimationTrigger<PNLNormalizedData>> triggers) : base(list) {
_defaults_appearances = default_appearances;
_triggers = triggers;
}
public override bool CanFilter => true;
public override bool CanGroup => true;
public override ReadOnlyObservableCollection<object> Groups {
get {
if (_groups_wrapper == null) {
_base_groups = base.Groups;
if (_base_groups == null)
return null;
_groups = new ObservableAutoWrapper<object, object>(_base_groups, obj => new PNLRowCollectionViewGroup((CollectionViewGroup)obj, this, _defaults_appearances, _triggers));
_groups_wrapper = new ReadOnlyObservableCollection<object>(_groups);
}
return _groups_wrapper;
}
}
public void on_settings(ImmutableDictionary<ColumnKeys, PNLCellAppearance> default_appearances, IReadOnlyList<IPNLAnimationTrigger<PNLNormalizedData>> triggers) {
_defaults_appearances = default_appearances;
_triggers = triggers;
foreach (var g in _groups_wrapper.OfTypeSafe<PNLRowCollectionViewGroup>())
g.on_settings(default_appearances, triggers);
}
public void set_all_expanded(bool is_expanded) {
foreach (var g in Groups.OfTypeSafe<PNLRowCollectionViewGroup>())
g.set_all_expanded(is_expanded);
}
}
class PNLRowCollectionViewGroup : CollectionViewGroup, IRaiseNotifyPropertyChanged, IEquatable<PNLRowCollectionViewGroup> {
readonly PNLRowCollectionView _root;
readonly CollectionViewGroup _from;
readonly Dictionary<object, InnerGroupSubscription> _inner_groups = new Dictionary<object, InnerGroupSubscription>();
readonly Dictionary<PNLKey, IDisposable> _rows = new Dictionary<PNLKey, IDisposable>();
readonly VersionContainer<bool?> _is_all_selected;
readonly DeferrableActionWrapper _refresh_is_all_selected;
readonly HashSet<PNLRowVM> _selected_rows = new HashSet<PNLRowVM>();
readonly PNLCellIndexBuilder<PNLNormalizedData> _cell_map = new PNLCellIndexBuilder<PNLNormalizedData>();
readonly PNLCellEmbeddedAnimationModel<PNLNormalizedData> _animator;
ImmutableDictionary<ColumnKeys, PNLCellAppearance> _defaults_appearances;
IReadOnlyList<IPNLAnimationTrigger<PNLNormalizedData>> _triggers;
IDisposable _dispose;
bool _is_expanded = true;
int _selected_item_count_backing;
public PNLRowCollectionViewGroup(CollectionViewGroup from, PNLRowCollectionView root, ImmutableDictionary<ColumnKeys, PNLCellAppearance> default_appearances, IReadOnlyList<IPNLAnimationTrigger<PNLNormalizedData>> triggers) : base(from.Name) {
_root = root;
_from = from;
_defaults_appearances = default_appearances;
_triggers = triggers;
_animator = new PNLCellEmbeddedAnimationModel<PNLNormalizedData>(default_appearances, triggers);
_animator.on_row(normalized_data.data);
normalized_data.data_changed += () => {
_cell_map.on_row(normalized_data.data);
_animator.on_row(normalized_data.data);
_cell_map.set_appearances(_animator.cell_appearances);
};
//sod_net_pos
_is_all_selected = new VersionContainer<bool?>(() => {
if (_selected_item_count == 0)
return false;
if (_selected_item_count == ProtectedItemCount)
return true;
return null;
});
bool? last_published_all_selected = _is_all_selected.value;
_refresh_is_all_selected = new DeferrableActionWrapper(() => {
if (_is_all_selected.value != last_published_all_selected) {
last_published_all_selected = is_all_selected;
OnPropertyChanged(new PropertyChangedEventArgs(nameof(is_all_selected)));
}
});
// sod
sod_pnl = _cell_map.add_pnl(ColumnKeys.SODPNL, d => d.sod_pnl);
sod_net_pos = _cell_map.add_net_pos(ColumnKeys.SODNetPos, d => d.sod_net_pos);
sod_long_pos = _cell_map.add_long_pos(ColumnKeys.SODLongPos, d => d.sod_long_pos);
sod_short_pos = _cell_map.add_short_pos(ColumnKeys.SODShortPos, d => d.sod_short_pos);
// day
day_pnl = _cell_map.add_pnl(ColumnKeys.DayPNL, d => d.day_pnl);
day_net_pos = _cell_map.add_net_pos(ColumnKeys.DayNetPos, d => d.day_net_pos);
day_long_pos = _cell_map.add_long_pos(ColumnKeys.DayLongPos, d => d.day_long_pos);
day_short_pos = _cell_map.add_short_pos(ColumnKeys.DayShortPos, d => d.day_short_pos);
// total
total_pnl = _cell_map.add(ColumnKeys.TotalPNL, d => d.total_pnl, FormattingDelegates.Price<decimal>.Value);
total_net_pos = _cell_map.add(ColumnKeys.TotalNetPos, d => d.total_net_pos);
total_long_pos = _cell_map.add(ColumnKeys.TotalLongPos, d => d.total_long_pos);
total_short_pos = _cell_map.add(ColumnKeys.TotalShortPos, d => d.total_short_pos);
// working
working_net_pos = _cell_map.add(ColumnKeys.WorkingNetPos, d => d.working_net_pos);
working_long_pos = _cell_map.add(ColumnKeys.WorkingLongPos, d => d.working_long_pos);
working_short_pos = _cell_map.add(ColumnKeys.WorkingShortPos, d => d.working_short_pos);
_cell_map.on_row(normalized_data.data);
_cell_map.set_appearances(_animator.cell_appearances);
((INotifyCollectionChanged)_from.Items).CollectionChanged += _from_items_CollectionChanged;
_from_items_CollectionChanged(_from.Items, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, _from.Items, 0));
_dispose = _cell_map.animation_model.OnDispatcher().Subscribe(new_animations => {
//stopwatch.Start();
animations = new_animations.Select(kv => new CellAnimation(kv.Key.column, kv.Key.type, kv.Value.color.to_brush())).ToList();
OnPropertyChanged(new PropertyChangedEventArgs(nameof(animations)));
});
}
public IList<CellAnimation> animations { get; private set; }
public event Action ProtectedItemCountChanged, selected_item_count_changed;
public PNLNormalizedDataVM normalized_data { get; } = new PNLNormalizedDataVM();
// sod
public PNLCellVM<PNLNormalizedData, decimal> sod_pnl { get; }
public PNLCellVM<PNLNormalizedData, decimal> sod_net_pos { get; }
public PNLCellVM<PNLNormalizedData, decimal> sod_long_pos { get; }
public PNLCellVM<PNLNormalizedData, decimal> sod_short_pos { get; }
// day
public PNLCellVM<PNLNormalizedData, decimal> day_pnl { get; }
public PNLCellVM<PNLNormalizedData, decimal> day_net_pos { get; }
public PNLCellVM<PNLNormalizedData, decimal> day_long_pos { get; }
public PNLCellVM<PNLNormalizedData, decimal> day_short_pos { get; }
// total
public PNLCellVM<PNLNormalizedData, decimal> total_pnl { get; }
public PNLCellVM<PNLNormalizedData, decimal> total_net_pos { get; }
public PNLCellVM<PNLNormalizedData, decimal> total_long_pos { get; }
public PNLCellVM<PNLNormalizedData, decimal> total_short_pos { get; }
// working
public PNLCellVM<PNLNormalizedData, decimal> working_net_pos { get; }
public PNLCellVM<PNLNormalizedData, decimal> working_long_pos { get; }
public PNLCellVM<PNLNormalizedData, decimal> working_short_pos { get; }
//
public PNLRowCollectionViewGroup parent { get; private set; }
public int level => parent == null ? 1 : parent.level + 1;
public override bool IsBottomLevel => _from.IsBottomLevel;
public IEnumerable<PNLRowCollectionViewGroup> ancestors {
get {
if (parent != null) {
var p = parent;
do {
yield return p;
p = p.parent;
} while (p != null);
}
}
}
public IReadOnlyList<object> GroupNamePath {
get {
var path = new List<object> { Name };
foreach (var a in ancestors)
path.Add(a.Name);
return path.WrapReverseRO();
}
}
public bool is_expanded { get => _is_expanded; set => _set(ref _is_expanded, value); }
public bool? is_all_selected {
get => _is_all_selected.value;
set {
// The setter should not be called while is_all_selected deferred.
// However, it is good to be defensive, so let's add a conditional check anyways:
// if 'is_all_selected' is deferred, the value must be blindly propagated,
// bc the equality comparison is erroneous, as the current state is unknown.
if (value.HasValue && value != is_all_selected) {
using (_refresh_is_all_selected.defer_execution()) {
foreach (var i in Items) {
if (i is PNLRowCollectionViewGroup g)
g.is_all_selected = value;
else if (i is PNLRowVM r)
r.is_selected = value.Value;
}
}
}
}
}
new int ProtectedItemCount {
get => base.ProtectedItemCount;
set {
if (base.ProtectedItemCount != value) {
base.ProtectedItemCount = value;
_is_all_selected.increment_version();
ProtectedItemCountChanged?.Invoke();
_refresh_is_all_selected.execute();
}
}
}
int _selected_item_count {
get => _selected_item_count_backing;
set {
Log.assert(value >= 0, $"{nameof(_selected_item_count)} cannot be negative.");
_selected_item_count_backing = value;
_is_all_selected.increment_version();
_refresh_is_all_selected.execute();
selected_item_count_changed?.Invoke();
}
}
public void on_settings(ImmutableDictionary<ColumnKeys, PNLCellAppearance> default_appearances, IReadOnlyList<IPNLAnimationTrigger<PNLNormalizedData>> triggers) {
_defaults_appearances = default_appearances;
_triggers = triggers;
if (Name.Equals("GC")) {
}
_animator.on_settings(default_appearances, triggers);
_cell_map.set_appearances(_animator.cell_appearances);
foreach (var g in ProtectedItems.OfTypeSafe<PNLRowCollectionViewGroup>())
g.on_settings(default_appearances, triggers);
}
public void set_all_expanded(bool is_expanded) {
this.is_expanded = is_expanded;
foreach (var g in Items.OfTypeSafe<PNLRowCollectionViewGroup>())
g.set_all_expanded(is_expanded);
}
public bool Equals(PNLRowCollectionViewGroup other) =>
other != null &&
level == other.level &&
Equals(Name, other.Name);
void _select_all() {
foreach (var i in Items) {
if (i is PNLRowCollectionViewGroup cvg) {
cvg._select_all();
} else if (i is PNLRowVM row) {
}
}
}
IDisposable _detect_is_all_selected_changed() {
bool? was_all_selected = is_all_selected;
return Disposable.Create(() => {
if (is_all_selected != was_all_selected)
OnPropertyChanged(new PropertyChangedEventArgs(nameof(is_all_selected)));
});
}
void _add_item(int index, object item) {
Log.assert(index >= 0, "Index is less than 0.");
if (item is CollectionViewGroup group) {
var inner_group = new PNLRowCollectionViewGroup(group, _root, _defaults_appearances, _triggers);
_inner_groups.Add(group.Name, new InnerGroupSubscription(this, inner_group));
ProtectedItems.Insert(index, inner_group);
} else {
var row = (PNLRowVM)item;
ProtectedItems.Insert(index, item);
_rows.Add(row.key, _connect_row(row));
}
}
void _remove_item(int index, object item) {
if (item is PNLRowCollectionViewGroup inner_group) {
if (!_inner_groups.TryPopValue(inner_group.Name, out var ig_sub))
throw new InvalidOperationException($"Inner groups cache does not have an entry for the specified name '{inner_group.Name}' (entire tree structure may be compromised).");
ig_sub.Dispose();
Disposable.dispose(ref inner_group._dispose);
} else {
var row = (PNLRowVM)item;
if (!_rows.TryPopValue(row.key, out var row_sub))
throw new InvalidOperationException($"Inner rows cache does not have an entry for the specified irowstem key '{row.key}' (entire tree structure may be compromised).");
row_sub.Dispose();
}
ProtectedItems.RemoveAt(index);
}
void _from_items_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) {
using (_refresh_is_all_selected.defer_execution()) {
switch (e.Action) {
case NotifyCollectionChangedAction.Add:
for (int i = 0; i < e.NewItems.Count; i++)
_add_item(e.NewStartingIndex + i, e.NewItems[i]);
break;
case NotifyCollectionChangedAction.Remove:
for (int i = 0; i < e.OldItems.Count; i++)
_remove_item(e.OldStartingIndex, ProtectedItems[e.OldStartingIndex]);
break;
case NotifyCollectionChangedAction.Replace:
for (int i = 0; i < e.OldItems.Count; i++)
_remove_item(e.OldStartingIndex, ProtectedItems[e.OldStartingIndex]);
for (int i = 0; i < e.NewItems.Count; i++)
_add_item(e.NewStartingIndex + i, e.NewItems[i]);
break;
case NotifyCollectionChangedAction.Reset:
while (ProtectedItems.Count > 0)
_remove_item(0, ProtectedItems[0]);
for (int i = 0; i < _from.Items.Count; i++)
_add_item(i, _from.Items[i]);
break;
}
_is_all_selected.increment_version();
_refresh_is_all_selected.execute();
}
}
IDisposable _connect_row(PNLRowVM row) {
using (_detect_is_all_selected_changed()) {
if (Name.Equals("GC")) {
}
var last_data = PNLNormalizedData.zero;
on_updated(row.normalized_data);
row.updated += on_updated;
row.is_selected_changed += on_is_selected_changed;
if (row.is_selected) {
Log.assert(_selected_rows.Add(row), $"Selected Rows out of sync ({Name}): hash set already contained selected row that was just added to group ({row}).");
_selected_item_count++;
}
ProtectedItemCount++;
return Disposable.Create(() => {
row.updated -= on_updated;
row.is_selected_changed -= on_is_selected_changed;
using (_refresh_is_all_selected.defer_execution()) {
if (row.is_selected) {
Log.assert(_selected_rows.Remove(row), $"Selected Rows out of sync ({Name}): hash set did not contain selected row that was just removed from group ({row}).");
_selected_item_count--;
}
var newval = normalized_data.data - row.normalized_data;
normalized_data.data = newval;
ProtectedItemCount--;
}
});
void on_updated(PNLNormalizedData new_data) {
if (Name.Equals("GC")) {
}
var less_last = normalized_data.data - last_data;
var plus_new = less_last + new_data;
normalized_data.data += new_data - last_data;
if (plus_new != normalized_data.data) {
}
Log.assert(plus_new == normalized_data.data);
last_data = new_data;
}
void on_is_selected_changed(bool is_selected) {
if (is_selected) {
Log.assert(_selected_rows.Add(row), $"Selected Rows out of sync ({Name}): hash set already contained row that was just selected ({row}).");
_selected_item_count++;
} else {
Log.assert(_selected_rows.Remove(row), $"Selected Rows out of sync ({Name}): hash set did not contain row that was just unselected ({row}).");
_selected_item_count--;
}
}
}
}
void _set<T>(ref T backing, T new_value, [CallerMemberName]string prop = "") =>
_set(ref backing, new_value, EqualityComparer<T>.Default, prop);
void _set<T>(ref T backing, T new_value, IEqualityComparer<T> comparer, [CallerMemberName]string prop = "") {
if (!comparer.Equals(backing, new_value)) {
backing = new_value;
OnPropertyChanged(new PropertyChangedEventArgs(prop));
}
}
void IRaiseNotifyPropertyChanged.raise_notify_property_changed(string prop) =>
OnPropertyChanged(new PropertyChangedEventArgs(prop));
class InnerGroupSubscription : IDisposable {
public readonly PNLRowCollectionViewGroup _inner_group;
readonly PNLRowCollectionViewGroup _parent;
PNLNormalizedData _last_data = PNLNormalizedData.zero;
int _last_count, _last_selected_item_count;
public InnerGroupSubscription(PNLRowCollectionViewGroup parent, PNLRowCollectionViewGroup inner_group) {
_parent = parent;
_inner_group = inner_group;
_inner_group.parent = parent;
// init item count
_on_item_count_changed();
// listen to item count changes
_inner_group.ProtectedItemCountChanged += _on_item_count_changed;
// init selected item count
_on_selected_item_count_changed();
// listen to selected item count changes
_inner_group.selected_item_count_changed += _on_selected_item_count_changed;
// init data
_on_normalized_data_changed();
// listen to data changes
_inner_group.normalized_data.data_changed += _on_normalized_data_changed;
_parent._selected_item_count += _inner_group._selected_item_count;
}
public void Dispose() {
_inner_group.ProtectedItemCountChanged -= _on_item_count_changed;
_inner_group.selected_item_count_changed -= _on_selected_item_count_changed;
_inner_group.normalized_data.data_changed -= _on_normalized_data_changed;
_inner_group.parent = null;
// make sure the state is valid (if a bad state goes by undetected here, nefarious/difficult-to-track bugs could arise)
if (_last_count != _inner_group.ProtectedItemCount) {
Log.Ui.warn("Cached inner count is not equal to the inner group's ItemCount (entire tree structure may be compromised).");
}
if (_last_data != _inner_group.normalized_data.data) {
Log.Ui.warn($"Cached normalized data is not equal to the inner group's normalized data (aggregated pnl data may be compromised).");
}
_parent.ProtectedItemCount -= _last_count;
_parent.normalized_data.data -= _last_data;
_parent._selected_item_count -= _last_selected_item_count;
}
void _on_item_count_changed() {
// add the delta
_parent.ProtectedItemCount += (_inner_group.ProtectedItemCount - _last_count);
// reset the local cache
_last_count = _inner_group.ProtectedItemCount;
}
void _on_selected_item_count_changed() {
_parent._selected_item_count += (_inner_group._selected_item_count - _last_selected_item_count);
_last_selected_item_count = _inner_group._selected_item_count;
}
void _on_normalized_data_changed() {
var new_data = _inner_group.normalized_data.data;
_parent.normalized_data.data += new_data - _last_data;
_last_data = new_data;
}
}
}
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,525 @@
using bts.collections;
using mbts;
using mbts.collections;
using mbts.spark;
using mbts.ui;
using mbts.ui.controls;
using mbts.ui.controls.adorners;
using mbts.ui.converters;
using mbts.ui.extensions;
using mbts.ui.spark;
using mbts.ui.utility;
using mbts.utils;
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Media;
namespace bts.shell.pnl {
enum PNLHeaderRowDisplay {
None,
Total,
SelectedTotal
}
partial class PNLView : UserControl {
public enum Columns {
Account = 1,
Exchange,
Symbol,
Security,
SecurityType,
// SOD
SODPNL,
SODLongPos,
SODLongAvgPrice,
SODLongAvgPriceDelta,
SODShortPos,
SODShortAvgPrice,
SODShortAvgPriceDelta,
SODNetPos,
SODNetAvgPriceDelta,
// Day
DayPNL,
DayLongPos,
DayLongAvgPrice,
DayLongAvgPriceDelta,
DayShortAvgPriceDelta,
DayShortAvgPrice,
DayShortPos,
DayCalcType,
DayNetPos,
DayNetAvgPriceDelta,
// Working
WorkingLongPos,
WorkingLongAvgPrice,
WorkingLongAvgPriceDelta,
WorkingShortPos,
WorkingShortAvgPrice,
WorkingShortAvgPriceDelta,
WorkingNetPos,
WorkingNetAvgPriceDelta,
// Total
TotalPNL,
TotalLongPos,
TotalLongAvgPrice,
TotalLongAvgPriceDelta,
TotalShortPos,
TotalShortAvgPrice,
TotalShortAvgPriceDelta,
TotalNetPos,
TotalNetAvgPriceDelta,
// Market
BidPrice,
MidPrice,
AskPrice,
LastPrice,
SettlementPrice,
PNLPrice,
// Misc
HasDayTrades,
CPP
}
public static readonly IValueConverter
IncrementalGroupLevelConverter = ValueConverter.create<int, int>(level => level + 1),
GroupLevelMarginConverter = ValueConverter.create<int, Thickness>(level => new Thickness(24 * level, 0, 0, 0)),
ExpandedVisualStateConverter = ValueConverter.create<bool, string>(is_expanded => is_expanded ? "Expanded" : "Collapsed");
public static readonly IMultiValueConverter
TotalHeaderRowDisplayTextConverter = ValueConverter.create<int, int, string>((filtered, total) => $"Total ({(filtered == total ? total.ToString() : $"{filtered}/{total}")})"),
CellParamsConverter = ValueConverter.create<object, Columns, PNLCell>((item, col) => new PNLCell(item, col));
public static readonly RoutedCommand CellClickCommand = new RoutedCommand();
const double _auxiliary_popup_height = 300;
static readonly string _node_path_separator = $" {SpecialCharacters.Dot} ";
IDisposable _anchors_hook, _animations_hook;
int _anchor_version;
public PNLView() {
SetCurrentValue(AnchorIndexProperty, new AnchorIndex());
InitializeComponent();
SetBinding(AnchorsProperty, new Binding(nameof(PNLVM.anchors)));
_grid.ConnectOnLoaded(() => {
if (_grid.try_find_descendent_of_type(out DIYGridRowsPresenter presenter)) {
presenter.GroupItemSelectionPivot += on_group_item_selection_pivot;
return Disposable.Create(() => presenter.GroupItemSelectionPivot -= on_group_item_selection_pivot);
}
return null;
});
void on_group_item_selection_pivot(object sender, GroupItemSelectionPivotEventArgs e) =>
e.Handled = e.group_item is PNLRowCollectionViewGroup pnl_group && pnl_group.is_expanded;
}
public new PNLVM DataContext {
get => base.DataContext as PNLVM;
set => base.DataContext = value;
}
public bool EnableAnimations {
get => _animations_hook != null;
set {
if (EnableAnimations != value) {
if (_animations_hook == null) {
_animations_hook = AdornerHelper.attach_continuously_on_loaded(
() => new GridAnimation.DIYGridAnimationAdorner<Columns>(_grid, _grid.descendants_of_type<DIYGridRowsPresenter>().First()),
_grid);
} else {
_animations_hook.Dispose();
_animations_hook = null;
}
}
}
}
#region AnchorIndex
public static readonly DependencyProperty AnchorIndexProperty =
WPFHelper.create_dp<AnchorIndex, PNLView>(
nameof(AnchorIndex),
(o, v) => v?.reset(o?.Anchors));
public AnchorIndex AnchorIndex {
get => GetValue(AnchorIndexProperty) as AnchorIndex;
set => SetValue(AnchorIndexProperty, value);
}
#endregion
#region GroupLevel
public static readonly DependencyProperty GroupLevelProperty =
DependencyProperty.RegisterAttached(
"GroupLevel",
typeof(int),
typeof(PNLView),
new PropertyMetadata(0, (d, e) => {
if (d is GroupItem group && e.NewValue is int level)
group.SetBinding(DIYGrid.ColumnKeyProperty, new Binding($"{nameof(Anchors)}[{level}]") { RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor, typeof(PNLView), 1) });
}));
public static int GetGroupLevel(DependencyObject d) => (int)d.GetValue(GroupLevelProperty);
public static void SetGroupLevel(DependencyObject d, int value) => d.SetValue(GroupLevelProperty, value);
void _grid_loaded(object sender, RoutedEventArgs e) {
// this is a hack to make room for the 'totals row' between the column headers and the regular rows.
if (_grid.try_find_descendent_of_type(out DIYGridRowsPresenter rp)) {
rp.SetBinding(MarginProperty, new Binding(nameof(PNLVM.header_row_display)) {
Converter = ValueConverter.create<PNLHeaderRowDisplay, Thickness>(hrd => {
double top = 0;
if ((hrd & PNLHeaderRowDisplay.Total) != 0)
top += 24;
if ((hrd & PNLHeaderRowDisplay.SelectedTotal) != 0)
top += 24;
return new Thickness(0, top, 0, 0);
}),
Source = DataContext
});
}
}
#endregion
#region Anchors
public static readonly DependencyProperty AnchorsProperty =
WPFHelper.create_dp<IEnumerable<Columns>, PNLView>(
nameof(Anchors),
(o, v) => {
var cols = o?._grid?.Columns;
if (cols == null)
return;
if (!(o._grid.Rows is ICollectionView rows_view))
return;
if (!(v is INotifyCollectionChanged anchor_notifier && v is IReadOnlyList<Columns> anchor_keys))
return;
int version = ++o._anchor_version;
bool busy = false;
var pnl_cols = cols.CastObservable<DIYGrid.ColumnBase, PNLColumn>();
Disposable.replace(ref o._anchors_hook, Disposable.Create(
anchor_notifier.Subscribe((_, __) => {
if (busy)
return;
// invoke async to avoid noisy consecutive resets due to batch changes
busy = true;
o.Dispatcher.InvokeAsync(() => {
sync_anchor_cols();
busy = false;
});
})));
o.AnchorIndex?.reset(anchor_keys);
sync_anchor_cols();
void sync_anchor_cols() {
// since reset is invoked asynchronously, need to check we are still the latest version.
if (o._anchor_version != version)
return;
o.AnchorIndex?.reset(anchor_keys);
// move all the anchor columns to the left, ordered by anchor position (grouping hierarchy).
for (int i = 0; i < anchor_keys.Count; i++) {
if (pnl_cols.TryGetFirst(c => c.key == anchor_keys[i], out var match, out int curr_pos)) {
match.is_visible = true;
if (curr_pos != i) {
cols.Move(curr_pos, i);
}
match.anchor_index = i + 1;
}
}
for (int i = anchor_keys.Count; i < pnl_cols.Count; i++) {
pnl_cols[i].anchor_index = null;
}
using (rows_view.DeferRefresh()) {
rows_view.GroupDescriptions.Clear();
var anchor_cols = from key in anchor_keys
join col in pnl_cols on key equals col.key
select new PNLGroupDescription(col);
rows_view.GroupDescriptions.AddRange(anchor_cols);
}
}
});
public IEnumerable<Columns> Anchors {
get => GetValue(AnchorsProperty) as IEnumerable<Columns>;
set => SetValue(AnchorsProperty, value);
}
#endregion
void _cell_click(PNLCell cell) {
Log.Ui.info($"PNLCell Click: {cell}.");
try {
switch (cell.column) {
case Columns.SODNetPos:
case Columns.SODLongPos:
case Columns.SODShortPos:
case Columns.DayNetPos:
case Columns.DayLongPos:
case Columns.DayShortPos:
case Columns.TotalNetPos:
case Columns.TotalLongPos:
case Columns.TotalShortPos: {
var oneof = cell.oneof();
if (oneof == null)
return;
var (vm, context, title, fg) = cell.oneof().Match(_row_fills, _group_fills, _header_fills);
_show_fills_grid(vm, context, title, fg);
}
break;
case Columns.WorkingNetPos:
case Columns.WorkingLongPos:
case Columns.WorkingShortPos: {
var (vm, context, title, fg) = cell.oneof().Match(_orderbook_params, _orderbook_params, _orderbook_params);
_show_orderbook(vm, context, title, fg);
}
break;
}
} catch (Exception ex) {
Log.Ui.warn($"{nameof(PNLView)} error on cell click: {ex}.");
}
}
#region Fills
(FillsVM vm, string context, string title, Color fg) _row_fills(PNLRowCell cell) {
var (title_desc, fg) = _fills_title(cell.column);
return (DataContext.mk_fills_vm(cell.row, cell.column), $"{cell.row.account_name} {cell.row.security}", title_desc, fg);
}
(FillsVM vm, string context, string title, Color fg) _group_fills(PNLGroupCell cell) {
var (title_desc, fg) = _fills_title(cell.column);
return (DataContext.mk_fills_vm(cell.group, cell.column), string.Join(_node_path_separator, cell.group.GroupNamePath), title_desc, fg);
}
(FillsVM vm, string context, string title, Color fg) _header_fills(PNLHeaderRowCell cell) {
var (title_desc, fg) = _fills_title(cell.column);
return (DataContext.mk_fills_vm(cell.header_row, cell.column), EnumHelper.get_display_name(cell.header_row), title_desc, fg);
}
static (string desc, Color fg) _fills_title(Columns key) {
const uint net_fg = 0x333333u;
switch (key) {
case Columns.SODLongPos: return ("Open Buy Fills", BTSColors.MarketData.bid);
case Columns.SODShortPos: return ("Open Sell Fills", BTSColors.MarketData.ask);
case Columns.SODNetPos: return ("Open Fills", net_fg.rgb_color());
case Columns.DayLongPos: return ("Day Buy Fills", BTSColors.MarketData.bid);
case Columns.DayShortPos: return ("Day Sell Fills", BTSColors.MarketData.ask);
case Columns.DayNetPos: return ("Day Fills", net_fg.rgb_color());
case Columns.TotalLongPos: return ("All Buy Fills", BTSColors.MarketData.bid);
case Columns.TotalShortPos: return ("All Sell Fills", BTSColors.MarketData.ask);
case Columns.TotalNetPos: return ("All Fills", net_fg.rgb_color());
default: throw ExceptionBuilder.Argument.unexpected_case(key, nameof(key));
}
}
async void _show_fills_grid(FillsVM fills_vm, string context_desc, string title_desc, Color fg) {
var fills_view = new FillsView { DataContext = fills_vm, MaxHeight = _auxiliary_popup_height };
await Task.Delay(100); // Wait for obvm to load rows collection before showing UI to avoid a flash
fills_view.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
// banner
var group_header = new TextBlock {
Margin = new Thickness(4, 0, 0, 0),
VerticalAlignment = VerticalAlignment.Center,
FontWeight = FontWeights.Bold,
Text = context_desc
};
var title = new TextBlock {
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
FontWeight = FontWeights.Bold,
Foreground = fg.to_brush(),
Text = title_desc
};
var keep_open_checkbox = new CheckBox {
VerticalAlignment = VerticalAlignment.Center,
Content = "Keep Open",
Focusable = false
};
var close_bn = new Button {
HorizontalAlignment = HorizontalAlignment.Right,
Margin = new Thickness(8, 4, 4, 4),
Command = ApplicationCommands.Close,
Style = TryFindResource("RoundCloseButton") as Style
};
var banner_panel_rt = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Right };
banner_panel_rt.Children.Add(keep_open_checkbox);
banner_panel_rt.Children.Add(close_bn);
// grid panel
var panel = new Grid();
panel.Children.Add(group_header);
panel.Children.Add(title);
panel.Children.Add(banner_panel_rt);
panel.Children.Add(fills_view);
panel.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
panel.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });
Grid.SetRow(fills_view, 1);
// popup adorner
var popup_adorner = new PopupAdorner(
_grid, _grid,
new Border {
Child = panel,
Background = BTSColors.common_background_brush,
BorderBrush = Brushes.Black,
BorderThickness = new Thickness(1),
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Top
},
HorizontalPosition.Middle,
VerticalPosition.Bottom,
0,
0) {
OpenRelativeToMouse = true,
EnableDragMove = true,
ClipToBounds = true
};
close_bn.CommandTarget = popup_adorner.PopupElement;
popup_adorner.SetBinding(PopupAdorner.StaysOpenProperty, new Binding(nameof(CheckBox.IsChecked)) { Source = keep_open_checkbox });
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);
fills_view.Focus();
popup_adorner.AdornedElement.Focus();
}
#endregion
#region OrderBook
(OrderBookVM vm, string context, string title, Color fg) _orderbook_params(PNLRowCell cell) {
var (title_desc, fg) = _orderbook_title(cell.column);
return (DataContext.mk_orderbook_vm(cell.row, cell.column), $"{cell.row.account_name} {cell.row.security}", title_desc, fg);
}
(OrderBookVM vm, string context, string title, Color fg) _orderbook_params(PNLGroupCell cell) {
var (title_desc, fg) = _orderbook_title(cell.column);
return (DataContext.mk_orderbook_vm(cell.group, cell.column), string.Join(_node_path_separator, cell.group.GroupNamePath), title_desc, fg);
}
(OrderBookVM vm, string context, string title, Color fg) _orderbook_params(PNLHeaderRowCell cell) {
var (title_desc, fg) = _orderbook_title(cell.column);
return (DataContext.mk_orderbook_vm(cell.header_row, cell.column), EnumHelper.get_display_name(cell.header_row), title_desc, fg);
}
static (string desc, Color fg) _orderbook_title(Columns key) {
switch (key) {
case Columns.WorkingLongPos: return ("Working Buy Orders", BTSColors.MarketData.bid);
case Columns.WorkingShortPos: return ("Working Sell Orders", BTSColors.MarketData.ask);
case Columns.WorkingNetPos: return ("Working Orders", 0x333333u.rgb_color());
default: throw ExceptionBuilder.Argument.unexpected_case(key, nameof(key));
}
}
async void _show_orderbook(OrderBookVM obvm, string context_desc, string title_desc, Color fg) {
var mini_ob = new MiniOrderBookView(obvm) { MaxHeight = _auxiliary_popup_height };
await Task.Delay(100); // Wait for obvm to load rows collection before showing UI to avoid a flash
mini_ob.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
// banner
var group_header = new TextBlock {
Margin = new Thickness(4, 0, 0, 0),
VerticalAlignment = VerticalAlignment.Center,
FontWeight = FontWeights.Bold,
Text = context_desc
};
var title = new TextBlock {
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
FontWeight = FontWeights.Bold,
Foreground = fg.to_brush(),
Text = title_desc
};
var keep_open_checkbox = new CheckBox {
VerticalAlignment = VerticalAlignment.Center,
Content = "Keep Open",
Focusable = false
};
var close_bn = new Button {
HorizontalAlignment = HorizontalAlignment.Right,
Margin = new Thickness(8, 4, 4, 4),
Command = ApplicationCommands.Close,
//CommandTarget = mini_ob,
Style = TryFindResource("RoundCloseButton") as Style
};
var banner_panel_rt = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Right };
banner_panel_rt.Children.Add(keep_open_checkbox);
banner_panel_rt.Children.Add(close_bn);
// grid panel
var panel = new Grid();
panel.Children.Add(group_header);
panel.Children.Add(title);
panel.Children.Add(banner_panel_rt);
panel.Children.Add(mini_ob);
panel.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
panel.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });
Grid.SetRow(mini_ob, 1);
// popup adorner
var popup_adorner = new PopupAdorner(
_grid, _grid,
new Border {
Child = panel,
Background = BTSColors.common_background_brush,
BorderBrush = Brushes.Black,
BorderThickness = new Thickness(1),
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Top
},
HorizontalPosition.Middle,
VerticalPosition.Bottom,
0,
0) {
OpenRelativeToMouse = true,
EnableDragMove = true,
ClipToBounds = true
};
close_bn.CommandTarget = popup_adorner.PopupElement;
popup_adorner.SetBinding(PopupAdorner.StaysOpenProperty, new Binding(nameof(CheckBox.IsChecked)) { Source = keep_open_checkbox });
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);
mini_ob.Focus();
popup_adorner.AdornedElement.Focus();
}
#endregion
void _cell_click_executed(object sender, ExecutedRoutedEventArgs e) {
if (e.Parameter is PNLCell cell) {
_cell_click(cell);
} else Log.Ui.warn($"{nameof(PNLView)} '{nameof(_cell_click_executed)}' parameter should be a {typeof(PNLCell)} (actually '{e.Parameter}').");
}
void _on_group_item_mouse_down(object sender, MouseButtonEventArgs e) {
if (!e.Handled && sender is FrameworkElement el && el.DataContext is PNLRowCollectionViewGroup group) {
group.is_all_selected = !group.is_all_selected.GetValueOrDefault();
e.Handled = true;
}
}
class PNLGroupDescription : PropertyGroupDescription, IEquatable<PNLGroupDescription> {
public PNLGroupDescription(PNLColumn column) : base(column.data_member_path.Path) =>
key = column.key;
public Columns key { get; }
public bool Equals(PNLGroupDescription other) =>
other != null && key == other.key;
public override int GetHashCode() =>
key.GetHashCode();
public override bool Equals(object obj) =>
Equals(obj as PNLGroupDescription);
}
}
}
@@ -19,7 +19,8 @@ namespace bts.shell {
LadderWindowFactory _lwf;
OrderBookWindowFactory _obwf;
FillsWindowFactory _fwf;
PAndLWindowFactory _plwf;
PAndLWindowFactory _old_plwf;
PNLWindowFactory _plwf;
AlgoStudioWindowFactory _aswf;
ManualFillsWindowFactory _mfwf;
@@ -36,7 +37,11 @@ namespace bts.shell {
_lwf = new LadderWindowFactory(this, bus, wm, settings_hub, orders_model, mbo);
_obwf = new OrderBookWindowFactory(this, bus, notification_service, notification_service_data_cache, orders_model, settings_hub);
_fwf = new FillsWindowFactory(this, bus, settings_hub, orders_model);
_plwf = new PAndLWindowFactory(this, bus, settings_hub, orders_model);
var pnl_conf = mbts.config.Configuration.get<bts.config.PNLOptions>();
if (pnl_conf.show_new_pnl)
_plwf = new PNLWindowFactory(this, bus, settings_hub, orders_model);
if (pnl_conf.show_old_pnl)
_old_plwf = new PAndLWindowFactory(this, bus, settings_hub, orders_model);
_aswf = new AlgoStudioWindowFactory(this, bus, orders_model, error_reporter);
_mfwf = new ManualFillsWindowFactory(this, bus, orders_model);
}
@@ -50,6 +55,7 @@ namespace bts.shell {
public IEnumerable<SparkCommand> actions => new [] {
SparkCommands.ShowOrders.complete(() => bts.shell.orderbook.OrderBookStartupData.Default),
SparkCommands.ShowFills(null).complete(() => new FillsSettings()),
SparkCommands.ShowPNL.complete(() => null),
SparkCommands.ShowPAndL.complete(() => new PAndLWindowFactory.PAndLStartupData()),
SparkCommands.PickLadder,
SparkCommands.PickOrderTicket,
@@ -61,7 +67,10 @@ namespace bts.shell {
yield return _lwf;
yield return _obwf;
yield return _fwf;
yield return _plwf;
if (_plwf != null)
yield return _plwf;
if (_old_plwf != null)
yield return _old_plwf;
yield return _aswf;
yield return _mfwf;
}
+5
View File
@@ -33,6 +33,11 @@ namespace mbts.spark {
}
}
public uint account_id =>
fill.FillKind != FillKind.Sod && order.HasValue ?
order.Value.account_id() :
fill.InternalAccountId;
public bool Equals(FillTuple other) => other != null
&& other.order.HasValue == order.HasValue
&& other.security.HasValue == security.HasValue
@@ -304,6 +304,29 @@ namespace mbts.spark.utilities {
public static OneofDescriptor oneof_descriptor => _oneof_desc;
const double _price_error_margin = 0.0000001;
const decimal _price_error_margin_decimal = 0.0000001m;
public static readonly IComparer<double> PriceComparer = Comparer<double>.Create((a, b) => {
if (double.IsNaN(a)) {
return double.IsNaN(b) ? 0 : -1;
} else if (double.IsNaN(b)) {
return 1;
} else {
double diff = a - b;
if (diff <= _price_error_margin && diff >= -_price_error_margin)
return 0;
return diff > 0 ? 1 : -1;
}
});
public static readonly IComparer<decimal> PriceComparerDecimal = Comparer<decimal>.Create((a, b) => {
decimal diff = a - b;
if (diff <= _price_error_margin_decimal && diff >= -_price_error_margin_decimal)
return 0;
return diff > 0 ? 1 : -1;
});
const string _primary_child_field = "child";
// algo-type-case maps
// Note: obv this will break if we change the name of the 'CustomOneofCase' field
+11
View File
@@ -268,6 +268,9 @@ namespace mbts.spark {
public static Color badge_color(AlgoDefinitionMetadata? algo_meta, OrderMetadata order_meta) =>
algo_meta?.badge_color ?? order_meta.Color.rgb_color();
public static string order_collection_summary(double best_price, int best_price_volume, int total_volume) =>
order_collection_summary(best_price, best_price_volume, total_volume, SecurityDataHelper.Defaults.ContractSpecificationImmutable);
public static string order_collection_summary(double best_price, int best_price_volume, int total_volume, ContractSpecificationImmutable spec) {
if (total_volume == 0)
return string.Empty;
@@ -276,6 +279,14 @@ namespace mbts.spark {
$"{best_price_volume} ({total_volume}) @ {spec.format(best_price)}";
}
public static string order_collection_summary(double best_price, double avg_price, int best_price_volume, int total_volume, ContractSpecificationImmutable spec) {
if (total_volume == 0)
return string.Empty;
return total_volume == best_price_volume ?
$"{best_price_volume} @ {spec.format(best_price)}" :
$"{best_price_volume} @ {spec.format(best_price)} ({total_volume} @ {spec.format(avg_price)})";
}
public static string past_tense(this Side side) {
switch (side) {
case Side.Buy: return "Bought";
+49 -5
View File
@@ -5,6 +5,7 @@ using bts.api.orderman;
using bts.api.position;
using bts.collections;
using bts.md;
using bts.shell.pnl;
using bts.spark;
using bts.utils;
using mbts.config;
@@ -68,13 +69,21 @@ namespace mbts.spark {
.Cache();
on_positions = _position_service.subscribei_position_data().Naggle(interval, scheduler).PermissionFilter(admin_model.on_admin_data).join(sstore, pos => pos.SecurityId).Table().Cache();
working_orders =
on_pnl = _position_service.subscribei_pnl_data()
.Naggle(interval, scheduler)
.join(sstore, p => p.Pos.SecurityId)
.TableChecked(EqualityComparer<(SecurityImmutable? sec, PNLUnitImmutable data)>.Default)
.Where(t => t.has_changes())
.Cache();
on_account_pnl = _position_service.subscribei_pnl_account_data().Naggle(interval, scheduler).Table().Cache();
var working_orders = on_orders.Filter(tup => tup.order.is_active());
this.working_orders =
on_orders.Scan(
ImmutableDictionary<uint,OrderSecurity>.Empty,
ImmutableDictionary<uint, OrderSecurity>.Empty,
(workers, table) => _update_working_orders(workers, table))
.Replay(1).RefCount();
on_order_securities =
on_order_securities =
on_orders
.as_iincremental()
.filter(t => t.sec.HasValue)
@@ -132,6 +141,38 @@ namespace mbts.spark {
OrderSecurityRootPair.from_order_security,
OrderSecurityHelper.root_pair_key_fn
).Cache();
// working account orders
var working_account_order_groups = working_orders.GroupTable(t => t.order.Key, t => new PNLKey(t.order.account_id(), t.order.security_key()));
// security account pnl
security_account_pnl = on_pnl
#if DEBUG
.AssertIncremental(p => new PNLKey(p.Item2.Pos.AccountId, p.Item2.Pos.SecurityId))
#endif
.SelectKeyValue(PNLRowDataFactory.create, PNLRowData.key_fn)
.FullJoin(
working_account_order_groups,
PNLRowData.key_fn,
acc_orders => acc_orders.group_key,
(pnl_row_data, acc_orders, pnl_key) => pnl_row_data.with(working: WorkingOrderData.Create(acc_orders.table.state.Values.Select(t => t.order))),
(pnl_row_data, pnl_id) => pnl_row_data,
(acc_orders, pnl_id) => {
string account;
SecurityImmutable? security;
if (acc_orders.table.state.Values.TryGetFirst(out var tup)) {
security = tup.sec;
account = tup.order.account();
} else {
security = null;
account = null;
}
return new PNLRowData(pnl_id,
security: security,
account_name: account,
working: WorkingOrderData.Create(acc_orders.table.state.Values.Select(t => t.order)));
},
PNLRowData.key_fn)
.Where(t => t.has_changes())
.Cache();
// fills
var on_fills =
_service.subscribei_fills()
@@ -154,7 +195,7 @@ namespace mbts.spark {
.join(sstore, mf => mf.SecurityKey)
.Table().Cache();
var manual_fill_tuples_account_join = on_manual_fills_securities.LeftJoin(
admin_model.on_accounts.Select(d => MappedDictionary.create(d, kv => new AccountTuple(kv.Key, kv.Value))).Table(a => a.id),
admin_model.on_accounts.Select(d => MappedDictionary.create(d, (k, v) => new AccountTuple(k, v))).Table(a => a.id),
mfs => mfs.Item2.RecordId,
AccountTuple.key_fn,
mfs => mfs.Item2.AccountId,
@@ -162,7 +203,7 @@ namespace mbts.spark {
(mfs, mf_id, a_id) => new ManualFillTuple(mfs.Item2, mfs.Item1, null, null),
ManualFillTuple.key_fn);
on_manual_fill_tuples = manual_fill_tuples_account_join.LeftJoin(
admin_model.on_users.Select(d => MappedDictionary.create(d, kv => new UserTuple(kv.Key, kv.Value))).Table(a => a.id),
admin_model.on_users.Select(d => MappedDictionary.create(d, (k, v) => new UserTuple(k, v))).Table(a => a.id),
ManualFillTuple.key_fn,
UserTuple.key_fn,
mft => mft.manual_fill.UserId,
@@ -212,6 +253,9 @@ namespace mbts.spark {
public IObservable<IIncrementalUpdate<SecurityImmutable, uint>> on_order_securities { get; }
public IObservable<TableUpdate<FillTuple, string>> on_fill_tuples { get; }
public IObservable<IReadOnlyDictionary<uint,OrderSecurity>> working_orders { get; }
public IObservable<TableUpdate<(SecurityImmutable?, PNLUnitImmutable), int>> on_pnl { get; }
public IObservable<TableUpdate<PNLRowData, PNLKey>> security_account_pnl { get; }
public IObservable<TableUpdate<AccountPNLImmutable, uint>> on_account_pnl { get; }
public IObservable<TableUpdate<(SecurityImmutable?, PositionUnitImmutable), int>> on_positions { get; }
public IObservable<TableUpdate<(AlgoData algo, AlgoDefinitionMetadata? meta), AlgoKey>> studio_algos_with_meta { get; }
public IObservable<TableUpdate<(SecurityImmutable?, ManualFillImmutable), int>> on_manual_fills_securities { get; }
+1
View File
@@ -16,6 +16,7 @@
<ResourceDictionary Source="/view/Resources/MenuItems.xaml" />
<ResourceDictionary Source="/view/Controls/ToolBar/ToolBar.xaml" />
<ResourceDictionary Source="/view/Controls/EditableTextBlock/EditableTextBlock.xaml"/>
<ResourceDictionary Source="/view/Controls/Buttons/ExpanderToggleButton.xaml"/>
<ResourceDictionary Source="/view/Controls/DragDropItemsControl/DragDropItemsControl.xaml" />
<ResourceDictionary Source="/view/Controls/DragDropItemsControl/DraggableContentControl.xaml" />
<ResourceDictionary Source="/view/Controls/DragDropItemsControl/DragDropListBox.xaml" />
+4 -1
View File
@@ -35,7 +35,10 @@ namespace bts.spark {
SparkCommand.create("Manual Fills", Icons.DataEdit, SparkTags.ShowManualFillsTag, key_gesture);
public static PartialCommand<PAndLWindowFactory.PAndLStartupData> ShowPAndL =>
SparkCommand.create("P&L", Icons.Accounting, SparkTags.ShowPAndLTag);
SparkCommand.create("P&L (1.0)", Icons.Accounting, SparkTags.ShowPAndLTag);
public static PartialCommand<shell.pnl.PNLSettings> ShowPNL =>
SparkCommand.create("P&L (2.0)", Icons.Accounting, SparkTags.ShowPNLTag);
// Ladder
public static PartialCommand<SecurityMD> ShowLadder(KeyGesture key_gesture = null) =>
+4 -6
View File
@@ -8,12 +8,9 @@ using mbts.ui;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using static mbts.spark.SparkBusData;
namespace mbts.spark
{
static class SparkTags
{
namespace mbts.spark {
static class SparkTags {
#region Core Data
public static ITag<IReadOnlyDictionary<uint, SecurityMD>> SecuritiesTag { get; } = new SparkTag<IReadOnlyDictionary<uint, SecurityMD>>("Securities");
public static ITag<IReadOnlyDictionary<uint, ExecutionStrategyImmutable>> SyntheticstrategiesTag { get; } = new SparkTag<IReadOnlyDictionary<uint, ExecutionStrategyImmutable>>("SyntheticSecuritiesTag");
@@ -64,6 +61,7 @@ namespace mbts.spark
public static ITag<SecurityCriteria> ShowMarketViewTag { get; } = new SparkTag<SecurityCriteria>("Show MarketView");
public static ITag<bts.shell.orderbook.OrderBookStartupData> ShowOrderBookTag { get; } = new SparkTag<bts.shell.orderbook.OrderBookStartupData>(nameof(ShowOrderBookTag));
public static ITag<PAndLWindowFactory.PAndLStartupData> ShowPAndLTag { get; } = new SparkTag<PAndLWindowFactory.PAndLStartupData>(nameof(ShowPAndLTag));
public static ITag<bts.shell.pnl.PNLSettings> ShowPNLTag { get; } = new SparkTag<bts.shell.pnl.PNLSettings>(nameof(ShowPNLTag));
public static ITag<(SecurityImmutable security, ExecutionStrategyImmutable strategy)> ShowSyntheticCloneTag { get; } = new SparkTag<(SecurityImmutable security, ExecutionStrategyImmutable strategy)>(nameof(ShowSyntheticCloneTag));
public static ITag<SecurityCriteria> ShowSyntheticCreateTag { get; } = new SparkTag<SecurityCriteria>(nameof(ShowSyntheticCreateTag));
public static ITag<SecurityImmutable> ShowSyntheticStrategyTag { get; } = new SparkTag<SecurityImmutable>(nameof(ShowSyntheticStrategyTag));
@@ -89,4 +87,4 @@ namespace mbts.spark
public override string ToString() => Name;
}
}
}
}
@@ -22,6 +22,12 @@ namespace mbts.ui.utility {
public static IReadOnlyObservableCollection<T> create_sorted_wrapper<T>(ObservableCollection<T> source, IComparer<T> comparer) =>
new SortedWrapper<T>(source, source, comparer);
public static IReadOnlyObservableCollection<TResult> CastObservable<T, TResult>(this ObservableCollection<T> source) =>
new SelectorWrapper<T, TResult>(source, source, ConvertUtils.CastDelegate<T, TResult>.value);
public static IReadOnlyObservableCollection<TResult> SelectObservable<T, TResult>(this ObservableCollection<T> source, Func<T, TResult> selector) =>
new SelectorWrapper<T, TResult>(source, source, selector);
public static IDisposable connect_sorted<T>(ObservableCollection<T> source, ObservableCollection<T> target, IComparer<T> comparer) {
target.sync_with(source.Ordered(comparer));
return source.Subscribe(on_source_collection_changed);
@@ -87,7 +93,9 @@ namespace mbts.ui.utility {
connect<ObservableCollection<T>, T, TResult>(source, target, create, null);
public static IDisposable connect<TCollection, T, TResult>(TCollection source, IList<TResult> target, Func<T, TResult> create, Action<TResult> on_removed = null)
where TCollection : IReadOnlyList<T>, INotifyCollectionChanged {
where TCollection : IReadOnlyList<T>, INotifyCollectionChanged => connect(source, source, target, create, on_removed);
public static IDisposable connect<T, TResult>(IReadOnlyList<T> source_list, INotifyCollectionChanged source_notifier, IList<TResult> target, Func<T, TResult> create, Action<TResult> on_removed = null) {
Action reset_cb;
Action<IList, int> removed_cb,
added_cb = (new_items, i) => {
@@ -100,7 +108,7 @@ namespace mbts.ui.utility {
}
};
if (on_removed == null) {
reset_cb = () => target.SetItems(source.Select(create));
reset_cb = () => target.SetItems(source_list.Select(create));
removed_cb = (old_items, i) => {
if (i < 0) {
throw new InvalidOperationException("Source collection invoked a CollectionChanged 'Removed' event without a valid 'OldStartingIndex.'");
@@ -113,7 +121,7 @@ namespace mbts.ui.utility {
reset_cb = () => {
foreach (var v in target)
on_removed(v);
target.SetItems(source.Select(create));
target.SetItems(source_list.Select(create));
};
removed_cb = (old_items, i) => {
if (i < 0) {
@@ -134,8 +142,8 @@ namespace mbts.ui.utility {
added_cb(new_items, new_i);
},
on_reset: reset_cb);
collection_changed_cb(source, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, (IList)source));
return source.Subscribe(collection_changed_cb);
reset_cb();
return source_notifier.Subscribe(collection_changed_cb);
}
public static IDisposable connect<T>(IObservable<ObservableCollection<T>> source, ObservableCollection<T> target) {
@@ -209,11 +217,21 @@ namespace mbts.ui.utility {
return notifier.Subscribe((s, e) => CollectionHelper.process_new_items(collection, action, e));
}
public static IDisposable connect_item_hooks<T>(IReadOnlyObservableCollection<T> source, Func<T, IDisposable> get_item_hook) {
var item_hooks = source.Select(get_item_hook).ToList();
public static IDisposable connect_item_hooks<T>(IReadOnlyObservableCollection<T> source, Func<T, IDisposable> get_item_hook) =>
connect_item_hooks(source, source, get_item_hook);
public static IDisposable connect_item_hooks<T>(IReadOnlyList<T> source_list, INotifyCollectionChanged source_notifier, Func<T, IDisposable> get_item_hook) {
var item_hooks = source_list.Select(get_item_hook).ToList();
return new CompositeDisposable(
Disposable.wrap_collection(item_hooks),
source.Subscribe((s, e) => CollectionHelper.reflect_change(item_hooks, source, get_item_hook, d => d.Dispose(), e)));
source_notifier.Subscribe((s, e) => CollectionHelper.reflect_change(item_hooks, source_list, get_item_hook, d => d.Dispose(), e)));
}
public static IDisposable connect_item_hooks<T>(IReadOnlyList<T> source_list, INotifyCollectionChanged source_notifier, Func<T, int, IDisposable> get_item_hook) {
var item_hooks = source_list.Select(get_item_hook).ToList();
return new CompositeDisposable(
Disposable.wrap_collection(item_hooks),
source_notifier.Subscribe((s, e) => CollectionHelper.reflect_change(item_hooks, source_list, get_item_hook, d => d.Dispose(), e)));
}
public static IDisposable connect_item_hooks<T>(ObservableCollection<T> source, Func<T, IDisposable> get_item_hook, bool clear_source_on_dispose = false) {
@@ -352,6 +370,46 @@ namespace mbts.ui.utility {
public IEnumerator<T> GetEnumerator() => _source.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => _source.GetEnumerator();
}
class SelectorWrapper<T, TResult> : IReadOnlyObservableCollection<TResult> {
readonly IReadOnlyList<T> _source_list;
readonly Func<T, TResult> _selector;
public SelectorWrapper(IReadOnlyList<T> source_list, INotifyCollectionChanged source_notifier, Func<T, TResult> selector) {
_source_list = source_list;
_selector = selector;
source_notifier.CollectionChanged += on_collection_changed;
void on_collection_changed(object sender, NotifyCollectionChangedEventArgs e) {
var cc = CollectionChanged;
if (cc == null)
return;
NotifyCollectionChangedEventArgs re;
switch (e.Action) {
case NotifyCollectionChangedAction.Add: re = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, e.NewItems.WrapSelectorHidden(from_obj), e.NewStartingIndex); break;
case NotifyCollectionChangedAction.Remove: re = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, e.OldItems.WrapSelectorHidden(from_obj), e.OldStartingIndex); break;
case NotifyCollectionChangedAction.Move: re = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Move, e.NewItems.WrapSelectorHidden(from_obj), e.NewStartingIndex, e.OldStartingIndex); break;
case NotifyCollectionChangedAction.Replace: re = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, e.NewItems.WrapSelectorHidden(from_obj), e.OldItems.WrapSelectorHidden(from_obj), e.NewStartingIndex); break;
case NotifyCollectionChangedAction.Reset: re = e; break;
default: throw ExceptionBuilder.Argument.unexpected_case(e.Action, nameof(e));
}
cc(this, re);
}
TResult from_obj(object obj) =>
selector((T)obj);
}
public TResult this[int index] => _selector(_source_list[index]);
public int Count => _source_list.Count;
public event NotifyCollectionChangedEventHandler CollectionChanged;
public IEnumerator<TResult> GetEnumerator() {
foreach (var v in _source_list)
yield return _selector(v);
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}
public class ObservableAutoWrapper<View, Model> : ObservableCollection<View>
@@ -1,15 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using System.Threading.Tasks;
namespace mbts.expression
{
namespace mbts.expression {
// Implements the language used in bts_core/Config
public class CoreExpressionEvaluator<T> : IExpressionEvaluator<T>
{
public class CoreExpressionEvaluator<T> : IExpressionEvaluator<T> {
private Func<T, bool> _apply;
public CoreExpressionEvaluator(string expression) {
@@ -29,10 +22,35 @@ namespace mbts.expression
}
}
CoreExpressionEvaluator(Func<T, bool> apply, string expression, string parsed_expression) {
_apply = apply;
Expression = expression;
ParsedExpression = parsed_expression;
}
public static bool try_create(string expression, out CoreExpressionEvaluator<T> result) {
try {
var parameter = System.Linq.Expressions.Expression.Parameter(typeof(T));
var parser = new CoreExpressionParser();
if (parser.try_parse(expression, out var ast)) {
var body = ast.create_expression(parameter);
var parsed = body.ToString();
var lambda = System.Linq.Expressions.Expression.Lambda<Func<T, bool>>(body, parameter);
result = new CoreExpressionEvaluator<T>(lambda.Compile(), expression, parsed);
return true;
}
} catch { }
result = null;
return false;
}
public static bool validate(string expression) =>
try_create(expression, out var _);
public string Expression { get; private set; }
public string ParsedExpression { get; private set; }
public bool apply(T arg) {
return _apply(arg);
}
}
}
}
@@ -4,17 +4,28 @@ using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using System.Threading.Tasks;
namespace mbts.expression
{
namespace mbts.expression {
// This is a re-implementation of the bts_core Expression parser in c#.
// It compiles down to LINQ expression trees which means the code is JITed at runtime for very fast execution
class CoreExpressionParser
{
class CoreExpressionParser {
// A cached stringbuilder
StringBuilder result = new StringBuilder();
internal bool try_parse(string str, out Token result) {
if (string.IsNullOrWhiteSpace(str)) {
result = null;
} else {
var l = lex(str);
try {
result = tree(l, false);
} catch {
result = null;
}
}
return result != null;
}
internal Token parse(string str) {
if (string.IsNullOrWhiteSpace(str)) return null;
var l = lex(str);
@@ -158,22 +169,22 @@ namespace mbts.expression
}
return new Number(result.ToString());
}
#endregion
#endregion
#region Recurively convert the Token list to an expression tree
private Token tree(IEnumerable<Token> tokens) {
private Token tree(IEnumerable<Token> tokens, bool assert_valid = true) {
var t = tokens.ToList();
// Handle groups by recursion
for (int x = 0; x < t.Count; ++x) {
if (t[x] is GroupToken) {
t[x] = tree(((GroupToken)t[x]).Items);
t[x] = tree(((GroupToken)t[x]).Items, assert_valid);
}
}
// This has to be a separate pass for "True == !False"
for (int x = t.Count-1; x >= 0; --x) {
for (int x = t.Count - 1; x >= 0; --x) {
if (t[x] is NotOperator) {
NotOperator no = (NotOperator)t[x];
no.Child = t[x + 1];
@@ -187,7 +198,11 @@ namespace mbts.expression
if (t[x] is CompareOperator) {
CompareOperator b = (CompareOperator)t[x];
if (b.Left != null) {
Log.assert(b.Right != null);
if (b.Right == null) {
if (assert_valid) {
Log.assert(false);
} else throw new InvalidOperationException("CompareOperator right side operand is null.");
}
continue;
}
b.Left = t[x - 1];
@@ -213,8 +228,11 @@ namespace mbts.expression
x--;
}
}
Log.assert(t.Count == 1);
if (t.Count != 1) {
if (assert_valid) {
Log.assert(false);
} else throw new InvalidOperationException("Expression tree was not reduced to a single token.");
}
return t.First();
}
#endregion
@@ -289,7 +307,8 @@ namespace mbts.expression
typeof(UInt32),
typeof(Int64),
typeof(UInt64),
typeof(float)
typeof(float),
typeof(decimal)
};
if (numeric_types.Contains(expr.Type)) {
@@ -19,13 +19,16 @@ namespace mbts.ui {
[DisplayInfo(name = "Default Header")]
public static BTSColor header { get { return BTSCoreColors.base_blue; } }
public static Color header_color => header;
public static Brush header_brush { get { return _header_brush; } }
public static BTSColor header_text { get { return BTSCoreColors.white; } }
public static Color header_text_color => header_text;
public static BTSColor header_error_text { get; } = new BTSColor(0xFE, 0x6A, 0x6E);
public static Brush header_error_text_brush => _header_error_text_brush;
public static BTSColor header_border { get { return new BTSColor(0x63,0x8A,0xCC); } }
public static Brush header_text_brush { get { return _header_text_brush; } }
public static BTSColor header_alternate { get { return BTSCoreColors.blue_grey; } }
public static Color header_alternate_color => header_alternate;
public static BTSColor disabled_text { get { return BTSCoreColors.gray_light; } }
public static BTSColor pinkalicious { get { return new BTSColor(0xff, 0x33, 0x99); } }
@@ -98,6 +101,8 @@ namespace mbts.ui {
public static Brush semi_transparent_bid_brush => _semi_transparent_bid_brush;
public static Brush semi_transparent_ask_brush => _semi_transparent_ask_brush;
public static Color bid_color => bid;
public static Color ask_color => ask;
[DisplayInfo(name = "Settlement")]
public static BTSColor settlement_background { get { return Grid.default_column_background; } }
@@ -139,19 +144,10 @@ namespace mbts.ui {
}
public static class RiskReport
{
// Base Colors
[DisplayInfo(name = "Risk Report")]
public static BTSColor default_background { get { return BTSCoreColors.blue_light_dark; } }
public static BTSColor default_text { get { return BTSCoreColors.black; } }
[DisplayInfo(name = "Risk PnL")]
public static BTSColor pnl_background { get { return Grid.default_column_background; } }
// Alerts
public static BTSColor pnl_loss_text { get { return BTSCoreColors.red; } }
public static BTSColor pnl_profit_text { get { return text_default; } }
public static class PNL {
public static Color anchor_color => Colors.Gold;
public static Color profit_color => Colors.Green;
public static Color loss_color => BTSColors.red;
}
public static class Orders {
@@ -81,6 +81,10 @@ namespace mbts {
added = newval.Except(current, comp);
removed = current.Except(newval, comp);
}
public static void difference_hard<T>(this IEnumerable<T> current, IEnumerable<T> newval, out IList<T> added, out IList<T> removed, IEqualityComparer<T> comp) {
added = newval.Except(current, comp).ToList();
removed = current.Except(newval, comp).ToList();
}
public static int indexOf<T>(this IEnumerable me, T value) {
IEnumerator enumerator = me.GetEnumerator();
int i = 0;
@@ -23,11 +23,17 @@ namespace mbts.persist {
readonly UserSettingsHub _hub;
readonly Func<UserSettings, ImmutableDictionary<uint, SettingsProfile<T>>> _get_profiles;
readonly Func<UserSettings, IReadOnlyDictionary<uint, SettingsProfile<T>>, UserSettings> _set_profiles;
readonly Func<UserSettings, uint> _get_next_id;
public SettingsProfileService(UserSettingsHub hub, Func<UserSettings, ImmutableDictionary<uint, SettingsProfile<T>>> get_profiles, Func<UserSettings, IReadOnlyDictionary<uint, SettingsProfile<T>>, UserSettings> set_profiles) {
public SettingsProfileService(
UserSettingsHub hub,
Func<UserSettings, ImmutableDictionary<uint, SettingsProfile<T>>> get_profiles,
Func<UserSettings, IReadOnlyDictionary<uint, SettingsProfile<T>>, UserSettings> set_profiles,
Func<UserSettings, uint> get_next_id) {
_hub = hub;
_get_profiles = get_profiles;
_set_profiles = set_profiles;
_get_next_id = get_next_id;
}
// Note: this operation is atomic, but does not protect against overwrites.
@@ -37,7 +43,7 @@ namespace mbts.persist {
var curr = _hub.current;
var mdata = _get_profiles(curr);
if (value.id == 0)
value = value.with(curr.next_ladder_profile_id);
value = value.with(_get_next_id(curr));
_hub.save_default(_set_profiles(curr, mdata.SetItem(value.id, value))).ignore();
return value.id;
}
@@ -1,79 +1,117 @@
using ProtoBuf;
using System;
namespace mbts.spark {
interface ISettingsProfile<T> {
uint id { get; }
string name { get; }
T value { get; }
SecurityCriteria security_criteria { get; }
}
struct SettingsProfile<T> : IEquatable<SettingsProfile<T>> where T : class, IEquatable<T> {
public static readonly Func<SettingsProfile<T>, uint> key_fn = ns => ns.id;
public readonly uint id;
public readonly string name;
public readonly T value;
public readonly SecurityCriteria security_criteria;
public SettingsProfile(uint id, string name, T value, SecurityCriteria security_criteria = null) {
this.id = id;
this.name = name;
this.value = value;
this.security_criteria = security_criteria;
}
public SettingsProfile<T> with(uint? id = null, string name = null, T value = null, SecurityCriteria security_criteria = null) =>
new SettingsProfile<T>(id ?? this.id, name ?? this.name, value ?? this.value, security_criteria ?? this.security_criteria);
public bool Equals(SettingsProfile<T> other) =>
id == other.id &&
name == other.name &&
value.safe_equals(other.value) &&
security_criteria.safe_equals(other.security_criteria);
public override bool Equals(object obj) =>
obj is SettingsProfile<T> other && Equals(other);
public override int GetHashCode() =>
id.GetHashCode();
}
[ProtoContract]
class LadderSettingsProfile : ISettingsProfile<LadderSettings>, IEquatable<LadderSettingsProfile> {
public LadderSettingsProfile() { }
public LadderSettingsProfile(uint id, string name, LadderSettings value, SecurityCriteria security_criteria = null) {
this.id = id;
this.name = name;
this.value = value;
this.security_criteria = security_criteria;
}
[ProtoMember(1)]
public uint id { get; }
[ProtoMember(2)]
public string name { get; }
[ProtoMember(3)]
public LadderSettings value { get; }
[ProtoMember(4)]
public SecurityCriteria security_criteria { get; }
public LadderSettingsProfile with(uint? id = null, LadderSettings value = null) =>
new LadderSettingsProfile(id ?? this.id, name, value ?? this.value, security_criteria);
public bool Equals(LadderSettingsProfile other) =>
other != null &&
id == other.id &&
name == other.name &&
value.safe_equals(other.value) &&
security_criteria.safe_equals(other.security_criteria);
public override bool Equals(object obj) => Equals(obj as LadderSettingsProfile);
public override int GetHashCode() => id.GetHashCode();
}
using bts.shell.pnl;
using ProtoBuf;
using System;
namespace mbts.spark {
interface ISettingsProfile<T> {
uint id { get; }
string name { get; }
T value { get; }
SecurityCriteria security_criteria { get; }
}
struct SettingsProfile<T> : IEquatable<SettingsProfile<T>> where T : class, IEquatable<T> {
public static readonly Func<SettingsProfile<T>, uint> key_fn = ns => ns.id;
public readonly uint id;
public readonly string name;
public readonly T value;
public readonly SecurityCriteria security_criteria;
public SettingsProfile(uint id, string name, T value, SecurityCriteria security_criteria = null) {
this.id = id;
this.name = name;
this.value = value;
this.security_criteria = security_criteria;
}
public SettingsProfile<T> with(uint? id = null, string name = null, T value = null, SecurityCriteria security_criteria = null) =>
new SettingsProfile<T>(id ?? this.id, name ?? this.name, value ?? this.value, security_criteria ?? this.security_criteria);
public bool Equals(SettingsProfile<T> other) =>
id == other.id &&
name == other.name &&
value.safe_equals(other.value) &&
security_criteria.safe_equals(other.security_criteria);
public override bool Equals(object obj) =>
obj is SettingsProfile<T> other && Equals(other);
public override int GetHashCode() =>
id.GetHashCode();
}
[ProtoContract]
class LadderSettingsProfile : ISettingsProfile<LadderSettings>, IEquatable<LadderSettingsProfile> {
public LadderSettingsProfile() { }
public LadderSettingsProfile(uint id, string name, LadderSettings value, SecurityCriteria security_criteria = null) {
this.id = id;
this.name = name;
this.value = value;
this.security_criteria = security_criteria;
}
[ProtoMember(1)]
public uint id { get; }
[ProtoMember(2)]
public string name { get; }
[ProtoMember(3)]
public LadderSettings value { get; }
[ProtoMember(4)]
public SecurityCriteria security_criteria { get; }
public LadderSettingsProfile with(uint? id = null, LadderSettings value = null) =>
new LadderSettingsProfile(id ?? this.id, name, value ?? this.value, security_criteria);
public bool Equals(LadderSettingsProfile other) =>
other != null &&
id == other.id &&
name == other.name &&
value.safe_equals(other.value) &&
security_criteria.safe_equals(other.security_criteria);
public override bool Equals(object obj) => Equals(obj as LadderSettingsProfile);
public override int GetHashCode() => id.GetHashCode();
}
[ProtoContract]
class PNLSettingsProfile : ISettingsProfile<PNLSettings>, IEquatable<PNLSettingsProfile> {
public PNLSettingsProfile() { }
public PNLSettingsProfile(uint id, string name, PNLSettings value, SecurityCriteria security_criteria = null) {
this.id = id;
this.name = name;
this.value = value;
this.security_criteria = security_criteria;
}
[ProtoMember(1)]
public uint id { get; }
[ProtoMember(2)]
public string name { get; }
[ProtoMember(3)]
public PNLSettings value { get; }
[ProtoMember(4)]
public SecurityCriteria security_criteria { get; }
public PNLSettingsProfile with(uint? id = null, PNLSettings value = null) =>
new PNLSettingsProfile(id ?? this.id, name, value ?? this.value, security_criteria);
public bool Equals(PNLSettingsProfile other) =>
other != null &&
id == other.id &&
name == other.name &&
value.safe_equals(other.value) &&
security_criteria.safe_equals(other.security_criteria);
public override bool Equals(object obj) => Equals(obj as PNLSettingsProfile);
public override int GetHashCode() => id.GetHashCode();
}
}
@@ -1,4 +1,5 @@
using Google.Protobuf.WellKnownTypes;
using bts.shell.pnl;
using Google.Protobuf.WellKnownTypes;
using mbts.ui.settings;
using ProtoBuf;
using System;
@@ -40,6 +41,8 @@ namespace mbts.spark {
/// Anytime a profile is added, it shall use <see cref="next_ladder_profile_id"/>.
[ProtoMember(12)]
uint _last_ladder_profile_id;
[ProtoMember(14)]
uint _last_pnl_profile_id;
// removing NotificationSettings these from proto-serialization, since they are disabled by default for now.
// This way, when they are enabled, there won't be any backwards compatability issues to worry about, as no clients will have persisted them yet.
NotificationSettings _notification;
@@ -56,11 +59,19 @@ namespace mbts.spark {
GridSettings _pnl_grid;
[ProtoMember(10)]
bts.shell.orderbook.OrderBookGridSettings _order_book_grid;
[ProtoMember(13)]
PNLSettings _pnl;
[ProtoMember(15)]
readonly PNLSettingsProfile[] _pnl_profiles;
public UserSettings() { }
public UserSettings(LadderSettingsProfile[] ladder_profiles) =>
public UserSettings(
LadderSettingsProfile[] ladder_profiles,
PNLSettingsProfile[] pnl_profiles) {
_ladder_profiles = ladder_profiles;
_pnl_profiles = pnl_profiles;
}
public LadderSettings ladder => _ladder ?? (_ladder = LadderSettings.Default);
public IReadOnlyList<LadderSettingsProfile> ladder_profiles => _ladder_profiles ?? Array.Empty<LadderSettingsProfile>();
@@ -71,6 +82,9 @@ namespace mbts.spark {
public GridSettings fills_grid => _fills_grid ?? (_fills_grid = bts.shell.fills.FillsSettings.DefaultGridSettings);
public GridSettings synthetic_management_grid => _synthetic_management_grid ?? (_synthetic_management_grid = bts.shell.SyntheticManagementWindowFactory.StartupData.DefaultGridSettings);
public GridSettings pnl_grid => _pnl_grid ?? (_pnl_grid = bts.shell.PAndLWindowFactory.PAndLStartupData.DefaultGridSettings);
public PNLSettings pnl => _pnl ?? (_pnl = PNLSettings.Default);
public uint next_pnl_profile_id => _last_pnl_profile_id + 1;
public IReadOnlyList<PNLSettingsProfile> pnl_profiles => _pnl_profiles ?? Array.Empty<PNLSettingsProfile>();
public bts.shell.orderbook.OrderBookGridSettings order_book_grid => _order_book_grid ?? (_order_book_grid = bts.shell.orderbook.OrderBookStartupData.Default.grid_settings_wrapper());
public UserSettings with(LadderSettings ladder = null,
@@ -81,25 +95,32 @@ namespace mbts.spark {
GridSettings provided_fills_gridsettings = null,
GridSettings provided_synthetic_management_gridsettings = null,
GridSettings provided_pnl_gridsettings = null,
PNLSettings pnl = null,
PNLSettingsProfile[] pnl_profiles = null,
bts.shell.orderbook.OrderBookGridSettings order_book_grid = null) {
var us = new UserSettings(ladder_profiles ?? _ladder_profiles) {
var us = new UserSettings(
ladder_profiles ?? _ladder_profiles,
pnl_profiles ?? _pnl_profiles) {
_ladder = ladder ?? _ladder,
_notification = notification ?? _notification,
_market_view_grid = provided_market_view_gridsettings ?? _market_view_grid,
_time_and_sales_grid = time_and_sales ?? _time_and_sales_grid,
_fills_grid = provided_fills_gridsettings ?? _fills_grid,
_synthetic_management_grid = provided_synthetic_management_gridsettings ?? _synthetic_management_grid,
_pnl_grid = provided_pnl_gridsettings ?? _pnl_grid,
_pnl_grid = provided_pnl_gridsettings ?? _pnl_grid,
_pnl = pnl ?? _pnl,
_order_book_grid = order_book_grid ?? _order_book_grid,
};
us._last_ladder_profile_id = Math.Max(_last_ladder_profile_id, us.ladder_profiles.Select(p => p.id).MaxOrDefault());
us._last_pnl_profile_id = Math.Max(_last_pnl_profile_id, us.pnl_profiles.Select(p => p.id).MaxOrDefault());
return us;
}
public static class FileExtensions {
public const string
GRID = "grid",
LADDER = "ladder",
GRID = "grid";
PNL = "pnl";
}
}
@@ -1,4 +1,5 @@
using System;
using System.Globalization;
namespace mbts.utils {
delegate bool TryGetValue<T>(out T value);
@@ -11,4 +12,18 @@ namespace mbts.utils {
XOr = (a, b) => a ? !b : b,
None = (a, b) => !a && !b;
}
static class FormattingDelegates {
public static class Price {
public static readonly Func<double, string> Value =
p => double.IsNaN(p) ? "" : p.ToString("#,0.00");
}
public static class Price<T> where T : struct, IFormattable {
public static readonly Func<T, string> Value =
typeof(T) == typeof(double) ?
(Func<T, string>)(object)Price.Value :
p => p.ToString("#,0.00", CultureInfo.CurrentCulture);
}
}
}
@@ -31,6 +31,13 @@ namespace mbts.spark {
void load_from(T value);
}
interface IResultChanged {
event Action result_changed;
}
interface IValueFactoryResultChanged<T> : IValueFactory<T>, IResultChanged {
}
interface IMutableValueWrapperVM<T> : INotifyPropertyChanged {
T value { get; set; }
}
@@ -1,84 +1,84 @@
using bts.api;
using bts.api.orderman;
using bts.api.position;
using mbts.co2;
using System;
using System.Collections.Generic;
using System.Linq;
namespace mbts.spark {
enum trading_session_state {
PreOpen = 21,
ReadyToTrade = 17,
TradingHalt = 2,
NotAvailableForTrading = 18,
Close = 4,
PostClose = 26
}
class MarketSchedule {
static readonly double _cme_gen_open_hrs;
const string cst_id = "Central Standard Time";
// the sod time in config says it's stored in UTC, but it really represents 16:30 Central Daylight Time
// i.e. 21:30 UTC corresponds to 16:30 Central (4:30 PM), but only during *CDT*
// CDT it UTC -5
// CST is UTC -6
static readonly int sod_hh = (int)config.Configuration.get<bts.config.SODTime>().sod_hh;
static readonly int sod_mm = (int)config.Configuration.get<bts.config.SODTime>().sod_mm;
static readonly TimeZoneInfo central_tzi = TimeZoneInfo.FindSystemTimeZoneById(cst_id);
static MarketSchedule() {
var cme_schedule = config.Configuration.get<bts.md.CMETradingSchedule>();
_cme_gen_open_hrs = cme_schedule?.gen_open_hour ?? 8.5;
}
// for object initializers
public MarketSchedule() { }
/// <summary>
/// Returns the CME general open time of the most recent trading date (i.e today, or last Friday if today is not a weekday).
/// </summary>
public static DateTime get_cme_gen_open() {
var now = DateTime.Now;
DateTime last_open = now.Date.AddHours(
now.TimeOfDay.TotalHours > _cme_gen_open_hrs ?
_cme_gen_open_hrs :
_cme_gen_open_hrs - 24);
switch (last_open.DayOfWeek) {
case DayOfWeek.Saturday: last_open = last_open.AddDays(-1); break;
case DayOfWeek.Sunday: last_open = last_open.AddDays(-2); break;
}
return last_open;
}
// DateTime? used here b/c we can't set a default parameter with DateTime.Now
// since it's not a compile-time constant
public static DateTime get_start_of_trading_day(DateTime? target_datetime = null, DateTimeKind dtk = DateTimeKind.Local ) {
if (target_datetime == null) target_datetime = DateTime.Now;
var dst_adjusted_central_sod = new DateTimeOffset(
new DateTime(target_datetime.Value.Date.Add(new TimeSpan(sod_hh, sod_mm, 0)).Ticks, DateTimeKind.Unspecified),
central_tzi.BaseUtcOffset
);
// if we're not currently past the start of the trading day, then the start must be yesterday
var temp = dst_adjusted_central_sod;
if (temp >= TimeZoneInfo.ConvertTime(target_datetime.Value, central_tzi)) {
temp = dst_adjusted_central_sod.AddDays(-1);
}
var local_time_zone_sod = TimeZoneInfo.ConvertTime(temp.DateTime, central_tzi, TimeZoneInfo.Local);
if (dtk == DateTimeKind.Utc) {
return local_time_zone_sod.ToUniversalTime();
}
return local_time_zone_sod;
}
public static DateTime get_utctimestamp_as_localtime(string s) {
return DateTimeOffset.ParseExact(s, "yyyyMMddHHmmssffffff", null, System.Globalization.DateTimeStyles.AssumeUniversal).ToLocalTime().LocalDateTime;
}
}
}
using bts.api;
using bts.api.orderman;
using bts.api.position;
using mbts.co2;
using System;
using System.Collections.Generic;
using System.Linq;
namespace mbts.spark {
enum trading_session_state {
PreOpen = 21,
ReadyToTrade = 17,
TradingHalt = 2,
NotAvailableForTrading = 18,
Close = 4,
PostClose = 26
}
class MarketSchedule {
static readonly double _cme_gen_open_hrs;
const string cst_id = "Central Standard Time";
// the sod time in config says it's stored in UTC, but it really represents 16:30 Central Daylight Time
// i.e. 21:30 UTC corresponds to 16:30 Central (4:30 PM), but only during *CDT*
// CDT it UTC -5
// CST is UTC -6
static readonly int sod_hh = (int)config.Configuration.get<bts.config.SODTime>().sod_hh;
static readonly int sod_mm = (int)config.Configuration.get<bts.config.SODTime>().sod_mm;
static readonly TimeZoneInfo central_tzi = TimeZoneInfo.FindSystemTimeZoneById(cst_id);
static MarketSchedule() {
var cme_schedule = config.Configuration.get<bts.md.CMETradingSchedule>();
_cme_gen_open_hrs = cme_schedule?.gen_open_hour ?? 8.5;
}
// for object initializers
public MarketSchedule() { }
/// <summary>
/// Returns the CME general open time of the most recent trading date (i.e today, or last Friday if today is not a weekday).
/// </summary>
public static DateTime get_cme_gen_open() {
var now = DateTime.Now;
DateTime last_open = now.Date.AddHours(
now.TimeOfDay.TotalHours > _cme_gen_open_hrs ?
_cme_gen_open_hrs :
_cme_gen_open_hrs - 24);
switch (last_open.DayOfWeek) {
case DayOfWeek.Saturday: last_open = last_open.AddDays(-1); break;
case DayOfWeek.Sunday: last_open = last_open.AddDays(-2); break;
}
return last_open;
}
// DateTime? used here b/c we can't set a default parameter with DateTime.Now
// since it's not a compile-time constant
public static DateTime get_start_of_trading_day(DateTime? target_datetime = null, DateTimeKind dtk = DateTimeKind.Local ) {
if (target_datetime == null) target_datetime = DateTime.Now;
var dst_adjusted_central_sod = new DateTimeOffset(
new DateTime(target_datetime.Value.Date.Add(new TimeSpan(sod_hh, sod_mm, 0)).Ticks, DateTimeKind.Unspecified),
central_tzi.BaseUtcOffset
);
// if we're not currently past the start of the trading day, then the start must be yesterday
var temp = dst_adjusted_central_sod;
if (temp >= TimeZoneInfo.ConvertTime(target_datetime.Value, central_tzi)) {
temp = dst_adjusted_central_sod.AddDays(-1);
}
var local_time_zone_sod = TimeZoneInfo.ConvertTime(temp.DateTime, central_tzi, TimeZoneInfo.Local);
if (dtk == DateTimeKind.Utc) {
return local_time_zone_sod.ToUniversalTime();
}
return local_time_zone_sod;
}
public static DateTime get_utctimestamp_as_localtime(string s) {
return DateTimeOffset.ParseExact(s, "yyyyMMddHHmmssffffff", null, System.Globalization.DateTimeStyles.AssumeUniversal).ToLocalTime().LocalDateTime;
}
}
}
+201 -12
View File
@@ -1,6 +1,10 @@
using bts.api.core;
using mbts;
using mbts.spark;
using mbts.ui.shell;
using mbts.utils;
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
@@ -8,14 +12,90 @@ using System.Reactive.Linq;
using System.Reflection;
namespace bts.utils {
static class ObjectSnapshot {
public static IObservable<TableUpdate<(string name, object current, object original), string>> property_table<T>(T instance, params string[] prop_names) where T : INotifyPropertyChanged =>
property_table(instance, Array.ConvertAll(prop_names, Impl<T>.type.GetProperty));
interface IPropertyChangedDetector<T> where T : IRaiseNotifyPropertyChanged {
IDisposable detect_changes(T target);
}
public static IObservable<TableUpdate<(string name, object current, object original), string>> property_table<T>(T instance, params PropertyInfo[] props) where T : INotifyPropertyChanged =>
static class ObjectSnapshot {
public static IEnumerable<Func<TSource, TValue>> property_delegates_of_type<TSource, TValue>() where TSource : class where TValue : class {
var tval = typeof(TValue);
return typeof(TSource).GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(p => tval.IsAssignableFrom(p.PropertyType))
.Select(_prop_getter<TSource, TValue>);
}
public static IEnumerable<Func<TSource, TValue>> property_delegates_of_type<TSource, TValue>(params string[] except_props) where TSource : class where TValue : class {
var except = new HashSet<string>(except_props);
return property_delegates_of_type<TSource, TValue>(p => !except.Contains(p.Name));
}
public static IEnumerable<Func<TSource, TValue>> property_delegates_of_type<TSource, TValue>(Func<PropertyInfo, bool> prop_filter) where TSource : class where TValue : class {
if (prop_filter == null)
return property_delegates_of_type<TSource, TValue>();
var tval = typeof(TValue);
return typeof(TSource).GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(p => tval.IsAssignableFrom(p.PropertyType) && prop_filter(p))
.Select(_prop_getter<TSource, TValue>);
}
public static IDisposable connect_property_changed(INotifyPropertyChanged component, PropertyChangedEventHandler callback) {
component.PropertyChanged += callback;
return Disposable.Create(() => component.PropertyChanged -= callback);
}
public static IDisposable connect_property_changed(IEnumerable<INotifyPropertyChanged> components, PropertyChangedEventHandler callback) {
foreach (var c in components)
c.PropertyChanged += callback;
return Disposable.Create(() => {
foreach (var c in components)
c.PropertyChanged -= callback;
});
}
public static IDisposable connect_result_changed(IResultChanged source, Action callback) {
source.result_changed += callback;
return Disposable.Create(() => source.result_changed -= callback);
}
public static IDisposable connect_result_changed(IEnumerable<IResultChanged> components, Action callback) {
foreach (var c in components)
c.result_changed += callback;
return Disposable.Create(() => {
foreach (var c in components)
c.result_changed -= callback;
});
}
public static Func<TSource, Action, IDisposable> auto_change_observer_factory<TSource>(params string[] except_props) where TSource : class {
var except = new HashSet<string>(except_props);
var del = SourceChangeObserver<TSource>.observe_changes_del;
return impl;
IDisposable impl(TSource source, Action callback) {
var hook = del(source, callback);
if (source is IResultChanged rc) {
rc.result_changed += callback;
hook = Disposable.Create(hook, connect_result_changed(rc, callback));
} else if (source is INotifyPropertyChanged npc) {
npc.PropertyChanged += on_prop_changed;
hook = Disposable.Create(hook, connect_property_changed(npc, on_prop_changed));
void on_prop_changed(object sender, PropertyChangedEventArgs e) =>
callback();
}
return hook;
}
}
public static IObservable<TableUpdate<(string name, object current, object original), string>> property_table<T>(T instance, params string[] prop_names) where T : INotifyPropertyChanged =>
_property_table(instance, Array.ConvertAll(prop_names, PropertyInfoStore<T>.type.GetProperty));
public static IObservable<TableUpdate<(string name, object current, object original), string>> property_table(INotifyPropertyChanged instance, params string[] prop_names) =>
_property_table(instance, Array.ConvertAll(prop_names, instance.GetType().GetProperty));
static IObservable<TableUpdate<(string name, object current, object original), string>> _property_table(INotifyPropertyChanged instance, params PropertyInfo[] props) =>
Observable.Create<TableUpdate<(string name, object current, object original), string>>(o => {
var cache = props.ToDictionary(p => p.Name, p => new PropNode(p, p.GetValue(instance)));
var table = TableUpdate<(string name, object current, object original), string>.mk_empty(t => t.name).with_added(cache.Values.Select(pn => (pn.prop.Name, pn.current, pn.original)));
o.OnNext(table);
instance.PropertyChanged += on_prop_changed;
return Disposable.Create(() => instance.PropertyChanged -= on_prop_changed);
void on_prop_changed(object sender, PropertyChangedEventArgs e) {
@@ -28,11 +108,20 @@ namespace bts.utils {
}
});
public static IPropertyChangedDetector<T> build_detector<T>(params string[] props) where T : IRaiseNotifyPropertyChanged =>
new DiscretePropertyChangedDetector<T>(props);
public static IPropertyChangedDetector<T> build_detector_except<T>(params string[] except_props) where T : IRaiseNotifyPropertyChanged {
// convert to a hashset for quicker lookups
var exclude = new HashSet<string>(except_props);
return new DiscretePropertyChangedDetector<T>(PropertyInfoStore<T>.all_props.Where(p => !exclude.Contains(p.Name)).Select(p => p.Name).ToArray());
}
public static IObservable<bool> dirty_monitor<T>(T instance, params string[] prop_names) where T : INotifyPropertyChanged =>
dirty_monitor(instance, Array.ConvertAll(prop_names, Impl<T>.type.GetProperty));
dirty_monitor(instance, Array.ConvertAll(prop_names, PropertyInfoStore<T>.type.GetProperty));
public static IObservable<bool> dirty_monitor<T>(T instance, out IDisposable snapshot, params string[] prop_names) where T : INotifyPropertyChanged =>
dirty_monitor(instance, out snapshot, Array.ConvertAll(prop_names, Impl<T>.type.GetProperty));
dirty_monitor(instance, out snapshot, Array.ConvertAll(prop_names, PropertyInfoStore<T>.type.GetProperty));
public static IObservable<bool> dirty_monitor<T>(T instance, out IDisposable snapshot, params PropertyInfo[] props) where T : INotifyPropertyChanged {
var prop_map = props.ToDictionary(p => p.Name, p => new PropNode(p, p.GetValue(instance)));
@@ -67,7 +156,7 @@ namespace bts.utils {
});
public static IDisposable create<T>(T instance) =>
create(instance, Impl<T>.mutable_props);
create(instance, PropertyInfoStore<T>.mutable_props);
public static IDisposable create<T>(T instance, PropertyInfo[] props) {
var snapshot = Array.ConvertAll(props, p => p.GetValue(instance));
@@ -77,13 +166,88 @@ namespace bts.utils {
});
}
static class Impl<T> {
static Func<T, TValue> _prop_getter<T, TValue>(PropertyInfo prop) =>
(Func<T, TValue>)prop.GetGetMethod().CreateDelegate(typeof(Func<T, TValue>));
static class PropertyInfoStore<T> {
public static readonly Type type = typeof(T);
public static readonly PropertyInfo[] mutable_props =
type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(p => p.GetSetMethod() != null)
.ToArray();
public static readonly PropertyInfo[]
all_props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance),
mutable_props;
static PropertyInfoStore() =>
mutable_props = all_props.Where(p => p.GetGetMethod() != null).ToArray();
}
// DelegateStore maintains a cache of property getter/setter delegates,
// which perform much better than the corresponding PropertyInfo.GetValue/SetValue.
static class DelegateStore<T> {
public static readonly Type type = typeof(T);
static readonly IReadOnlyDictionary<string, (Func<T, object> get, Action<T, object> set)> _property_delegates;
public static Func<T, object> getter(string prop_name) =>
_property_delegates.TryGetValue(prop_name, out var dels) ? dels.get : null;
public static Action<T, object> setter(string prop_name) =>
_property_delegates.TryGetValue(prop_name, out var dels) ? dels.set : null;
static DelegateStore() {
var prop_dels = new Dictionary<string, (Func<T, object> get, Action<T, object> set)>();
foreach (var p in PropertyInfoStore<T>.all_props) {
Func<T, object> wrapped_getter = null;
Action<T, object> wrapped_setter = null;
var getter = p.GetGetMethod();
var setter = p.GetSetMethod();
if (getter == null && setter == null)
continue;
var del_types = (typeof(Func<,>).MakeGenericType(typeof(T), p.PropertyType), typeof(Action<,>).MakeGenericType(typeof(T), p.PropertyType));
var twrapper = typeof(Wrapper<>).MakeGenericType(typeof(T), p.PropertyType);
if (getter != null) {
wrapped_getter = (Func<T, object>)
twrapper.GetMethod(nameof(Wrapper<T>.getter), BindingFlags.Static | BindingFlags.Public)
.Invoke(null, new[] { getter.CreateDelegate(del_types.Item1) });
}
if (setter != null) {
wrapped_setter = (Action<T, object>)
twrapper.GetMethod(nameof(Wrapper<T>.setter), BindingFlags.Static | BindingFlags.Public)
.Invoke(null, new[] { setter.CreateDelegate(del_types.Item2) });
}
prop_dels[p.Name] = (wrapped_getter, wrapped_setter);
}
_property_delegates = prop_dels;
}
static class Wrapper<TValue> {
public static Func<T, object> getter(Func<T, TValue> getter) => src => getter(src);
public static Action<T, object> setter(Action<T, TValue> setter) => (src, val) => setter(src, (TValue)val);
}
}
static class SourceChangeObserver<TSource> where TSource : class {
public static readonly Func<TSource, Action, IDisposable> observe_changes_del;
static readonly IReadOnlyList<Func<TSource, INotifyPropertyChanged>> _observable_component_getters =
property_delegates_of_type<TSource, INotifyPropertyChanged>(p => typeof(IResultChanged).IsAssignableFrom(p.PropertyType))
.ToArray();
static readonly IReadOnlyList<Func<TSource, IResultChanged>> _result_changed_component_getters =
property_delegates_of_type<TSource, IResultChanged>()
.ToArray();
// initialize in constructor to avoid dependency issues (wrt to other static fields)
static SourceChangeObserver() =>
observe_changes_del = build_observe_changes_del(null);
public static Func<TSource, Action, IDisposable> build_observe_changes_del(Func<PropertyInfo, bool> prop_filter) {
var npc_getters = property_delegates_of_type<TSource, INotifyPropertyChanged>(prop_filter);
var result_getters = property_delegates_of_type<TSource, IResultChanged>(prop_filter);
return (source, callback) =>
Disposable.Create(
connect_property_changed(npc_getters.Select(g => g(source)), (s, e) => callback()),
connect_result_changed(result_getters.Select(g => g(source)), callback));
}
}
class PropNode {
@@ -99,5 +263,30 @@ namespace bts.utils {
public object original { get; set; }
public object current { get; set; }
}
class DiscretePropertyChangedDetector<T> : IPropertyChangedDetector<T> where T : IRaiseNotifyPropertyChanged {
static readonly Converter<string, (string name, Func<T, object> get)> _realize_prop =
(prop_name) => (prop_name, DelegateStore<T>.getter(prop_name));
readonly (string name, Func<T, object> get)[] _target_props;
public DiscretePropertyChangedDetector()
: this(Array.ConvertAll(PropertyInfoStore<T>.all_props, p => p.Name)) { }
public DiscretePropertyChangedDetector(params string[] props) =>
_target_props = Array.ConvertAll(props, _realize_prop);
public IDisposable detect_changes(T target) {
var get_target_val = new Converter<(string name, Func<T, object> get), object>(tup => tup.get(target));
var orig_state = Array.ConvertAll(_target_props, get_target_val);
return Disposable.Create(() => {
var curr_state = Array.ConvertAll(_target_props, get_target_val);
for (int i = 0; i < curr_state.Length; i++) {
if (!Equals(curr_state[i], orig_state[i]))
target.raise_notify_property_changed(_target_props[i].name);
}
});
}
}
}
}
+26
View File
@@ -44,6 +44,16 @@ namespace bts {
}
}
public static OneOf<A, B> MatchFirst<TSource, A, B>(IEnumerable<TSource> source) {
foreach (var v in source) {
if (v is A va) {
return new OneOf<A, B>(va);
} else if (v is B vb)
return new OneOf<A, B>(vb);
}
return new OneOf<A, B>();
}
public static T infer<T, A, B>(object value, Func<A, T> a, Func<B, T> b) =>
OneOf<A, B>.infer(value).Match(a, b);
}
@@ -101,6 +111,13 @@ namespace bts {
public OneOf(B b) { _b = b; _tag = 1; }
public OneOf(C c) { _c = c; _tag = 2; }
public static OneOf<A, B, C> infer(object obj) {
if (obj is A a) return new OneOf<A, B, C>(a);
if (obj is B b) return new OneOf<A, B, C>(b);
if (obj is C c) return new OneOf<A, B, C>(c);
return new OneOf<A, B, C>();
}
public void Match(Action<A> a, Action<B> b, Action<C> c) {
switch (_tag) {
case 0: a(_a); break;
@@ -118,6 +135,15 @@ namespace bts {
}
}
public OneOf<A2, B2, C2> Extend<A2, B2, C2>(Func<A, A2> a, Func<B, B2> b, Func<C, C2> c) {
switch (_tag) {
case 0: return new OneOf<A2, B2, C2>(a(_a));
case 1: return new OneOf<A2, B2, C2>(b(_b));
case 2: return new OneOf<A2, B2, C2>(c(_c));
default: return null;
}
}
public object value {
get {
switch (_tag) {
@@ -0,0 +1,106 @@
using System;
namespace bts.utils {
/// <summary>
/// An IComparable implementation that supports cross type comparisons of any combination of int, uint, double, and decimal.
/// </summary>
readonly struct UniversalNumericComparer : IComparable {
readonly int? _int;
readonly uint? _uint;
readonly double? _double;
readonly decimal? _decimal;
public UniversalNumericComparer(int i) {
_int = i;
_uint = null;
_double = null;
_decimal = null;
}
public UniversalNumericComparer(double d) {
_int = null;
_uint = null;
_double = d;
_decimal = null;
}
public UniversalNumericComparer(uint u) {
_int = null;
_uint = u;
_double = null;
_decimal = null;
}
public UniversalNumericComparer(decimal d) {
_int = null;
_uint = null;
_double = null;
_decimal = d;
}
public int CompareTo(object obj) {
if (obj is int i) {
return -(i.CompareTo(this));
} else if (obj is uint u) {
return -(u.CompareTo(this));
} else if (obj is double d)
return -(d.CompareTo(this));
else if (obj is decimal dec)
return -(dec.CompareTo(this));
throw new ArgumentException("Obj is not of a supported numeric type (int, uint, double).", nameof(obj));
}
public static implicit operator int(UniversalNumericComparer w) {
if (w._int.HasValue)
return w._int.Value;
else if (w._double.HasValue)
return (int)w._double.Value;
else if (w._uint.HasValue)
return (int)w._uint.Value;
else if (w._decimal.HasValue)
return (int)w._decimal.Value;
return 0;
}
public static implicit operator uint(UniversalNumericComparer w) {
if (w._uint.HasValue)
return w._uint.Value;
else if (w._int.HasValue)
return (uint)w._int.Value;
else if (w._double.HasValue)
return (uint)w._double.Value;
else if (w._decimal.HasValue)
return (uint)w._decimal.Value;
return 0u;
}
public static implicit operator double(UniversalNumericComparer w) {
if (w._double.HasValue)
return w._double.Value;
else if (w._int.HasValue)
return w._int.Value;
else if (w._uint.HasValue)
return w._uint.Value;
else if (w._decimal.HasValue)
return (double)w._decimal.Value;
return 0.0;
}
public static implicit operator decimal(UniversalNumericComparer w) {
if (w._double.HasValue)
return (decimal)w._double.Value;
else if (w._int.HasValue)
return w._int.Value;
else if (w._uint.HasValue)
return w._uint.Value;
else if (w._decimal.HasValue)
return w._decimal.Value;
return 0;
}
public static implicit operator UniversalNumericComparer(int i) => new UniversalNumericComparer(i);
public static implicit operator UniversalNumericComparer(uint u) => new UniversalNumericComparer(u);
public static implicit operator UniversalNumericComparer(double d) => new UniversalNumericComparer(d);
public static implicit operator UniversalNumericComparer(decimal d) => new UniversalNumericComparer(d);
}
}
@@ -0,0 +1,55 @@
using mbts.ui;
using mbts.ui.utility.text;
using ProtoBuf;
using System;
using System.Windows;
using System.Windows.Media;
using FontFamily = System.Windows.Media.FontFamily;
namespace bts.proto {
[ProtoContract]
class FontParamsProto : IEquatable<FontParamsProto> {
[ProtoMember(1)]
public string font_family { get; set; }
[ProtoMember(2)]
public int font_weight { get; set; }
[ProtoMember(3)]
public uint font_color { get; set; }
[ProtoMember(4)]
public double font_size { get; set; }
public (FontFamily family, FontWeight? weight, Color? color, double size) to_wpf_params() =>
(string.IsNullOrEmpty(font_family) ? null : new FontFamily(font_family),
font_weight == 0 ? (FontWeight?)null : FontWeight.FromOpenTypeWeight(font_weight),
font_color == 0 ? (Color?)null : font_color.argb_color(),
// zero is an invalid font size, convert to nan to follow convention
font_size == 0 ? double.NaN : font_size);
public (FontFamily family, FontWeight weight, Color color, double size) to_wpf_params_safe(
FontFamily fallback_family = null,
FontWeight? fallback_weight = null,
Color? fallback_color = null,
double fallback_size = double.NaN) {
//
var (family, weight, color, size) = to_wpf_params();
return (
family ?? fallback_family ?? TextUtils.StandardFontFamily,
weight ?? fallback_weight ?? FontWeights.Normal,
color ?? fallback_color ?? Colors.Black,
double.IsNaN(font_size) ? double.IsNaN(fallback_size) ? FontSizes.Small : fallback_size : font_size);
}
public bool Equals(FontParamsProto other) =>
other != null &&
font_family == other.font_family &&
font_weight == other.font_weight &&
font_color == other.font_color &&
font_size.Equals(other.font_size);
public override bool Equals(object obj) =>
Equals(obj as FontParamsProto);
public override int GetHashCode() =>
872259;
}
}
@@ -1,6 +1,5 @@
using bts.utils;
using mbts.ui;
using mbts.utils;
using ProtoBuf;
using System;
using System.Windows.Media;
@@ -17,13 +16,8 @@ namespace bts.proto {
public NullableColorProto(uint value) =>
_value = value;
public Color? value {
get {
if (_value.HasValue)
return _value.Value.rgb_color();
return null;
}
}
public Color? value => _value?.rgb_color();
public Color? argb => _value?.argb_color();
public bool Equals(NullableColorProto other) =>
EqualityComparer.nullable_equals(_value, other._value);
+15 -7
View File
@@ -61,11 +61,19 @@
<SolidColorBrush x:Key="CheckBox.Pressed.BorderBrush" Color="{StaticResource CheckBox.Pressed.BorderColor}"/>
<!--ComboBox-->
<Color x:Key="ComboBoxBackgroundLightColor">#FFE5E5E5</Color>
<Color x:Key="ComboBoxBackgroundMediumColor">#FFF0F0F0</Color>
<Color x:Key="ComboBoxBorderLightColor">#FFACACAC</Color>
<Color x:Key="ComboBoxBorderMediumColor">#FF7EB4EA</Color>
<Color x:Key="ComboBoxBorderDarkColor">#FF569DE5</Color>
<Color x:Key="ComboBoxDisabledBorderColor">#FFD9D9D9</Color>
<LinearGradientBrush x:Key="ComboBox.Static.Background" EndPoint="0,1" StartPoint="0,0">
<GradientStop Color="#FFF0F0F0" Offset="0.0"/>
<GradientStop Color="#FFE5E5E5" Offset="1.0"/>
<GradientStop Color="{StaticResource ComboBoxBackgroundLightColor}" Offset="0.0"/>
<GradientStop Color="{StaticResource ComboBoxBackgroundMediumColor}" Offset="1.0"/>
</LinearGradientBrush>
<SolidColorBrush x:Key="ComboBox.Static.Border" Color="#FFACACAC"/>
<SolidColorBrush x:Key="ComboBox.Static.Border" Color="{StaticResource ComboBoxBorderLightColor}"/>
<SolidColorBrush x:Key="ComboBox.Static.Editable.Background" Color="#FFFFFFFF"/>
<SolidColorBrush x:Key="ComboBox.Static.Editable.Border" Color="#FFABADB3"/>
<SolidColorBrush x:Key="ComboBox.Static.Editable.Button.Background" Color="Transparent"/>
@@ -75,7 +83,7 @@
<GradientStop Color="#FFECF4FC" Offset="0.0"/>
<GradientStop Color="#FFDCECFC" Offset="1.0"/>
</LinearGradientBrush>
<SolidColorBrush x:Key="ComboBox.MouseOver.Border" Color="#FF7EB4EA"/>
<SolidColorBrush x:Key="ComboBox.MouseOver.Border" Color="{StaticResource ComboBoxBorderMediumColor}"/>
<SolidColorBrush x:Key="ComboBox.MouseOver.Editable.Background" Color="#FFFFFFFF"/>
<SolidColorBrush x:Key="ComboBox.MouseOver.Editable.Border" Color="#FF7EB4EA"/>
<LinearGradientBrush x:Key="ComboBox.MouseOver.Editable.Button.Background" EndPoint="0,1" StartPoint="0,0">
@@ -88,7 +96,7 @@
<GradientStop Color="#FFDAECFC" Offset="0.0"/>
<GradientStop Color="#FFC4E0FC" Offset="1.0"/>
</LinearGradientBrush>
<SolidColorBrush x:Key="ComboBox.Pressed.Border" Color="#FF569DE5"/>
<SolidColorBrush x:Key="ComboBox.Pressed.Border" Color="{StaticResource ComboBoxBorderDarkColor}"/>
<SolidColorBrush x:Key="ComboBox.Pressed.Editable.Background" Color="#FFFFFFFF"/>
<SolidColorBrush x:Key="ComboBox.Pressed.Editable.Border" Color="#FF569DE5"/>
<LinearGradientBrush x:Key="ComboBox.Pressed.Editable.Button.Background" EndPoint="0,1" StartPoint="0,0">
@@ -97,8 +105,8 @@
</LinearGradientBrush>
<SolidColorBrush x:Key="ComboBox.Pressed.Editable.Button.Border" Color="#FF569DE5"/>
<SolidColorBrush x:Key="ComboBox.Disabled.Glyph" Color="#FFBFBFBF"/>
<SolidColorBrush x:Key="ComboBox.Disabled.Background" Color="#FFF0F0F0"/>
<SolidColorBrush x:Key="ComboBox.Disabled.Border" Color="#FFD9D9D9"/>
<SolidColorBrush x:Key="ComboBox.Disabled.Background" Color="{StaticResource ComboBoxBackgroundMediumColor}"/>
<SolidColorBrush x:Key="ComboBox.Disabled.Border" Color="{StaticResource ComboBoxDisabledBorderColor}"/>
<SolidColorBrush x:Key="ComboBox.Disabled.Editable.Background" Color="#FFFFFFFF"/>
<SolidColorBrush x:Key="ComboBox.Disabled.Editable.Border" Color="#FFBFBFBF"/>
<SolidColorBrush x:Key="ComboBox.Disabled.Editable.Button.Background" Color="Transparent"/>
@@ -0,0 +1,66 @@
using mbts.ui.extensions;
using mbts.utils;
using System;
using System.Reactive.Disposables;
using System.Windows;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
namespace mbts.ui.controls.adorners {
class MouseTraceAdorner : Adorner {
readonly Visual _trace_visual;
readonly Size _trace_visual_size;
MouseTraceAdorner(UIElement adorned_element, Visual trace_visual, Size trace_visual_size)
: base(adorned_element) {
_trace_visual = trace_visual;
_trace_visual_size = trace_visual_size;
AddVisualChild(trace_visual);
IsHitTestVisible = false;
HorizontalAlignment = HorizontalAlignment.Left;
VerticalAlignment = VerticalAlignment.Top;
}
protected override int VisualChildrenCount => 1;
public static IDisposable trace(FrameworkElement target, Visual trace_visual, Size trace_visual_size) {
var mta = new MouseTraceAdorner(target, trace_visual, trace_visual_size);
move_to(Mouse.GetPosition(target));
mta.attach();
return new CompositeDisposable {
target.SubscribePreviewMouseMove(on_mouse),
target.SubscribeMouseEnter(on_mouse),
target.SubscribeMouseLeave(on_mouse),
mta.detach
};
void on_mouse(object s, MouseEventArgs e) =>
move_to(e.GetPosition(target));
void move_to(Point pt) =>
mta.RenderTransform = new TranslateTransform(pt.X, pt.Y);
}
public static IDisposable trace_drag(FrameworkElement target, Visual trace_visual, Size trace_visual_size) {
var mta = new MouseTraceAdorner(target, trace_visual, trace_visual_size);
move_to(Mouse.GetPosition(target));
mta.attach();
return new CompositeDisposable {
target.SubscribePreviewDragOver(on_drag),
target.SubscribePreviewDragLeave(on_drag),
target.SubscribePreviewDragEnter(on_drag),
mta.detach
};
void on_drag(object s, DragEventArgs e) =>
move_to(e.GetPosition(target));
void move_to(Point pt) =>
mta.RenderTransform = new TranslateTransform(pt.X, pt.Y);
}
protected override Visual GetVisualChild(int index) =>
index == 0 ? _trace_visual : throw new ArgumentOutOfRangeException(nameof(index));
protected override Size MeasureOverride(Size constraint) => _trace_visual_size;
}
}
@@ -1,20 +1,24 @@
using bts.utils;
using bts;
using bts.utils;
using mbts.ui.extensions;
using mbts.utils;
using System;
using System.Windows;
using System.Windows.Controls.Primitives;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
namespace mbts.ui.controls.adorners {
internal enum HorizontalPosition { Left, Middle, Right }
internal enum VerticalPosition { Top, Middle, Botton }
internal enum VerticalPosition { Top, Middle, Bottom }
class PopupAdorner : Adorner {
bool _enable_drag_move;
IDisposable _drag_move_hook;
IDisposable _drag_move_hook, _auto_close_hook;
public PopupAdorner(UIElement adornedElement, FrameworkElement popupTarget)
: this(adornedElement, popupTarget, null, HorizontalPosition.Middle, VerticalPosition.Botton) { }
: this(adornedElement, popupTarget, null, HorizontalPosition.Middle, VerticalPosition.Bottom) { }
public PopupAdorner(UIElement adornedElement, FrameworkElement popupTarget, FrameworkElement popupElement, HorizontalPosition horizontalPosition, VerticalPosition verticalPosition)
: this(adornedElement, popupTarget, popupElement, horizontalPosition, verticalPosition, 0.0, 0.0) { }
@@ -68,6 +72,37 @@ namespace mbts.ui.controls.adorners {
#endregion
#region StaysOpen
public static readonly DependencyProperty StaysOpenProperty =
Popup.StaysOpenProperty.AddOwner(typeof(PopupAdorner),
new FrameworkPropertyMetadata(
true,
(d, e) => {
var popa = ExceptionAssert.of_type<PopupAdorner>(d, nameof(d));
Disposable.dispose(ref popa._auto_close_hook);
if (!(bool)e.NewValue) {
FocusManager.SetIsFocusScope(popa.AdornedElement, true);
popa.AdornedElement.Focus();
popa.AdornedElement.PreviewGotKeyboardFocus += on_focus_changed;
popa.AdornedElement.GotFocus += on_focus_changed;
popa._auto_close_hook = Disposable.Create(() => {
popa.AdornedElement.PreviewGotKeyboardFocus -= on_focus_changed;
popa.AdornedElement.GotFocus -= on_focus_changed;
});
// callbacks
void on_focus_changed(object sender, RoutedEventArgs _e) {
if (!popa.PopupElement.IsKeyboardFocusWithin && FocusManager.GetFocusedElement(popa.PopupElement) == null)
popa.SetCurrentValue(IsOpenProperty, false);
};
}
}));
public bool StaysOpen {
get => (bool)GetValue(StaysOpenProperty);
set => SetValue(StaysOpenProperty, value);
}
#endregion
#region Opened Event
public static readonly RoutedEvent OpenedEvent = EventManager.RegisterRoutedEvent("Opened", RoutingStrategy.Direct, typeof(RoutedEventHandler), typeof(PopupAdorner));
@@ -129,7 +164,7 @@ namespace mbts.ui.controls.adorners {
switch (_verticalPosition) {
case VerticalPosition.Top: rltv_y = -el_height; break;
case VerticalPosition.Middle: rltv_y = -(0.5 * el_height); break;
case VerticalPosition.Botton: rltv_y = 0; break;
case VerticalPosition.Bottom: rltv_y = 0; break;
default: throw new NotImplementedException();
}
tl = new Point(tl.X + rltv_x + _horizontalOffset, tl.Y + rltv_y + _verticalOffset);
@@ -145,7 +180,7 @@ namespace mbts.ui.controls.adorners {
switch (_verticalPosition) {
case VerticalPosition.Top: rltv_y = -el_height; break;
case VerticalPosition.Middle: rltv_y = 0.5 * (_popupTarget.ActualHeight - el_height); break;
case VerticalPosition.Botton: rltv_y = _popupTarget.ActualHeight + el_height; break;
case VerticalPosition.Bottom: rltv_y = _popupTarget.ActualHeight + el_height; break;
default: throw new NotImplementedException();
}
tl = new Point(tl.X + rltv_x + _horizontalOffset, tl.Y + rltv_y + _verticalOffset);
@@ -1,56 +1,72 @@
<ToggleButton
x:Class="mbts.ui.controls.ExpanderToggleButton"
x:ClassModifier="internal"
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
BorderBrush="Black"
Padding="3,0,0,0"
xmlns:c="clr-namespace:mbts.ui.controls"
xmlns:cv="clr-namespace:mbts.ui.converters"
>
<ToggleButton.Resources>
<SolidColorBrush x:Key="Path.Static.Brush" Color="#FF333333"/>
<SolidColorBrush x:Key="Ellipse.MouseOver.Brush" Color="#FF5593FF"/>
<SolidColorBrush x:Key="Ellipse.Pressed.Brush" Color="#FF3C77DD"/>
<SolidColorBrush x:Key="Ellipse.Disabled.Brush" Color="Gray"/>
</ToggleButton.Resources>
<ToggleButton.Template>
<ControlTemplate TargetType="ToggleButton">
<Grid Background="Transparent">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Ellipse x:Name="ellipse" Width="20" Height="20" Stroke="{TemplateBinding BorderBrush}"/>
<Path
x:Name="arrow"
Data="M1,1.5L4.5,5 8,1.5"
HorizontalAlignment="Center"
SnapsToDevicePixels="False"
Stroke="{StaticResource Path.Static.Brush}"
StrokeThickness="2"
VerticalAlignment="Center"
/>
<ContentPresenter
Grid.Column="1"
ContentSource="{TemplateBinding Content}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Margin="{TemplateBinding Padding}"
/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter Property="Data" TargetName="arrow" Value="M1,4.5L4.5,1 8,4.5"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Stroke" TargetName="ellipse" Value="{StaticResource Ellipse.MouseOver.Brush}"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Stroke" TargetName="ellipse" Value="{StaticResource Ellipse.Pressed.Brush}"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Foreground" Value="Gray"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</ToggleButton.Template>
</ToggleButton>
<SolidColorBrush x:Key="Path.Static.Brush" Color="#FF333333"/>
<SolidColorBrush x:Key="Ellipse.MouseOver.Brush" Color="#FF5593FF"/>
<SolidColorBrush x:Key="Ellipse.Pressed.Brush" Color="#FF3C77DD"/>
<SolidColorBrush x:Key="Ellipse.Disabled.Brush" Color="Gray"/>
<Style TargetType="c:ExpanderToggleButton" BasedOn="{StaticResource {x:Type ToggleButton}}">
<Setter Property="BorderBrush" Value="Black"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Padding" Value="3,0,0,0"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ToggleButton">
<Border>
<Grid Background="Transparent">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Ellipse x:Name="ellipse" Width="20" Height="20" Stroke="{TemplateBinding BorderBrush}" StrokeThickness="{Binding BorderThickness, RelativeSource={RelativeSource TemplatedParent}, Converter={x:Static cv:ValueConverters.ThicknessToDouble}}"/>
<Path
x:Name="arrow"
Data="M0,0L3.5,3.5 0,7"
HorizontalAlignment="Center"
SnapsToDevicePixels="False"
Stroke="{StaticResource Path.Static.Brush}"
StrokeThickness="2"
VerticalAlignment="Center"
/>
<ContentPresenter
Grid.Column="1"
ContentSource="{TemplateBinding Content}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Margin="{TemplateBinding Padding}"
/>
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter Property="Data" TargetName="arrow" Value="M0,0L3.5,3.5 7,0"/>
</Trigger>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding StrokeThickness, ElementName=ellipse}" Value="0"/>
<Condition Binding="{Binding IsMouseOver, RelativeSource={RelativeSource Self}}" Value="True"/>
</MultiDataTrigger.Conditions>
<Setter TargetName="arrow" Property="Stroke" Value="{StaticResource Ellipse.MouseOver.Brush}"/>
</MultiDataTrigger>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding StrokeThickness, ElementName=ellipse, Converter={x:Static cv:ValueConverters.DoubleGreaterThanZero}}" Value="True"/>
<Condition Binding="{Binding IsMouseOver, RelativeSource={RelativeSource Self}}" Value="True"/>
</MultiDataTrigger.Conditions>
<Setter TargetName="ellipse" Property="Stroke" Value="{StaticResource Ellipse.MouseOver.Brush}"/>
</MultiDataTrigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Stroke" TargetName="ellipse" Value="{StaticResource Ellipse.Pressed.Brush}"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Foreground" Value="Gray"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
@@ -1,11 +1,12 @@
using System.Windows.Controls.Primitives;
using System.Windows;
using System.Windows.Controls.Primitives;
namespace mbts.ui.controls {
/// <summary>
/// Interaction logic for ExpanderToggleButton.xaml
/// </summary>
partial class ExpanderToggleButton : ToggleButton {
public ExpanderToggleButton() =>
InitializeComponent();
class ExpanderToggleButton : ToggleButton {
static ExpanderToggleButton() =>
DefaultStyleKeyProperty.OverrideMetadata(typeof(ExpanderToggleButton), new FrameworkPropertyMetadata(typeof(ExpanderToggleButton)));
}
}
@@ -3,12 +3,48 @@
x:ClassModifier="internal"
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"
xmlns:cv="clr-namespace:mbts.ui.converters"
xmlns:cp="clr-namespace:mbts.ui.controls.color_picker"
Background="Transparent"
Header="{Binding SelectedColor, RelativeSource={RelativeSource Self}, Mode=OneWay}"
ShowExpanderArrow="False"
>
<c:PopupControl.Background>
<LinearGradientBrush EndPoint="0,1" StartPoint="0,0">
<GradientStop Color="{StaticResource ComboBoxBackgroundLightColor}" Offset="0.0"/>
<GradientStop Color="{StaticResource ComboBoxBackgroundMediumColor}" Offset="1.0"/>
</LinearGradientBrush>
</c:PopupControl.Background>
<c:PopupControl.BorderBrush>
<SolidColorBrush Color="{StaticResource ComboBoxBorderLightColor}"/>
</c:PopupControl.BorderBrush>
<u:StateManager.Groups>
<u:StateGroupsManager AttachToMembers="True">
<u:StateDefinitionCollection>
<u:StateDefinition>
<Binding Path="Group.Target.IsEnabled" RelativeSource="{RelativeSource Self}">
<Binding.Converter>
<cv:BooleanToObjectConverter ValueForFalse="Disabled" ValueForTrue="Enabled"/>
</Binding.Converter>
</Binding>
</u:StateDefinition>
</u:StateDefinitionCollection>
</u:StateGroupsManager>
</u:StateManager.Groups>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup Name="EnabledStates">
<VisualState Name="Enabled"/>
<VisualState Name="Disabled">
<Storyboard>
<ColorAnimation Storyboard.TargetProperty="(c:PopupControl.Background).(LinearGradientBrush.GradientStops)[0].(GradientStop.Color)" Duration="0" To="{StaticResource ComboBoxBackgroundMediumColor}"/>
<ColorAnimation Storyboard.TargetProperty="(c:PopupControl.Background).(LinearGradientBrush.GradientStops)[1].(GradientStop.Color)" Duration="0" To="{StaticResource ComboBoxBackgroundMediumColor}"/>
<ColorAnimation Storyboard.TargetProperty="(c:PopupControl.BorderBrush).(SolidColorBrush.Color)" Duration="0" To="{StaticResource ComboBoxDisabledBorderColor}"/>
<DoubleAnimation Storyboard.TargetProperty="(c:PopupControl.Opacity)" Duration="0" To="0.5"/>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<c:PopupControl.HeaderTemplate>
<DataTemplate>
<Rectangle Stroke="LightBlue" StrokeThickness="1" MinWidth="25" MinHeight="18" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
@@ -1,7 +1,9 @@
using bts.proto;
using bts.utils;
using mbts.spark;
using mbts.ui.controls.color_picker;
using mbts.ui.shell;
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows;
@@ -38,16 +40,42 @@ namespace mbts.ui.controls {
class ColorPickerPopupVM : ViewModelBase, IValueFactory<SolidColorBrush> {
readonly Property<Color> _color;
readonly Property<double> _opacity;
ObservableCollection<ColorPaletteVM> _palettes;
bool _bind_opacity_to_alpha;
IDisposable _opacity_alpha_binding;
public ColorPickerPopupVM() {
_color = backing(nameof(color), Colors.Black);
_opacity = backing(nameof(opacity), 1.0);
palettes = new ObservableCollection<ColorPaletteVM>(ColorPalette.all_presets().Select(p => new ColorPaletteVM(p)));
}
public Color color { get => _color.Value; set => _color.set(value); }
public double opacity { get => _opacity.Value; set => _opacity.set(value); }
public ObservableCollection<ColorPaletteVM> palettes { get; }
public ObservableCollection<ColorPaletteVM> palettes => _palettes ?? (_palettes = new ObservableCollection<ColorPaletteVM>(ColorPalette.all_presets().Select(p => new ColorPaletteVM(p))));
public bool bind_opacity_to_alpha {
get => _bind_opacity_to_alpha;
set {
if (_bind_opacity_to_alpha != value) {
Disposable.dispose(ref _opacity_alpha_binding);
_bind_opacity_to_alpha = value;
if (value) {
_opacity.Changed += opacity_changed;
_color.Changed += color_changed;
color_changed(default, color);
void opacity_changed(double oldval, double newval) {
_color.Changed -= color_changed;
_color.set(color.with((byte)(MathUtils.within_range(0, 1, newval) * 255.0)));
_color.Changed += color_changed;
}
void color_changed(Color oldval, Color newval) {
_opacity.Changed -= opacity_changed;
_opacity.set(newval.A / 255.0);
_opacity.Changed += opacity_changed;
}
}
}
}
}
public SolidColorBrush create() => new SolidColorBrush(color) { Opacity = opacity }.frozen();
public void load_from(SolidColorBrush brush) {
@@ -80,7 +80,7 @@ namespace mbts.ui.controls {
new[] {
new ColumnDefinition { Width = new GridLength(visual_split.HorizontalSplit.Left, GridUnitType.Star) },
new ColumnDefinition { Width = new GridLength(visual_split.HorizontalSplit.Right, GridUnitType.Star) },
new ColumnDefinition { Width = GridLength.Auto }
new ColumnDefinition { MinWidth = 30, Width = GridLength.Auto }
});
break;
default:
@@ -4,344 +4,393 @@ using System;
using System.ComponentModel;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using FontFamily = System.Windows.Media.FontFamily;
namespace mbts.ui.controls {
partial class DIYGrid {
public class HyperlinkCell : FrameworkElement, ICommandSource {
// these fields each represent properties that are accessed frequently,
// they are stored locally to reduce repetative DependencyProperty lookups
Thickness _padding;
HorizontalAlignment _horizontal_alignment;
Brush _background, _foreground;
double _glyph_width, _font_size;
string _text;
GlyphRun _glyph_run;
GlyphContext _glyph_context;
bool _is_mouse_over_text, _can_execute;
Pen _underline_pen;
class HyperlinkCell : FrameworkElement, ICommandSource {
// these fields each represent properties that are accessed frequently,
// they are stored locally to reduce repetative DependencyProperty lookups
Thickness _padding => _gcr.padding;
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;
Pen _underline_pen_backing, _border_stroke_pen_backing;
GlyphContentRect _gcr = new GlyphContentRect();
static HyperlinkCell() {
WPFHelper.override_metadata<HorizontalAlignment, HyperlinkCell>(
HorizontalAlignmentProperty,
FrameworkPropertyMetadataOptions.AffectsArrange |
FrameworkPropertyMetadataOptions.AffectsRender,
(o, v) => {
o._horizontal_alignment = v;
o._glyph_run = null;
}, HorizontalAlignment.Left);
WPFHelper.override_metadata<double, HyperlinkCell>(
WidthProperty,
FrameworkPropertyMetadataOptions.AffectsArrange |
FrameworkPropertyMetadataOptions.AffectsMeasure |
FrameworkPropertyMetadataOptions.AffectsRender,
(o, v) => o._glyph_run = null);
Brush _foreground {
get => _foreground_backing;
set {
_foreground_backing = value;
_underline_pen_backing = null;
}
public HyperlinkCell() {
_background = Background;
_foreground = Foreground;
_horizontal_alignment = HorizontalAlignment;
_padding = Padding;
_font_size = FontSize;
_text = Text;
_glyph_context = GlyphContext.create(FontFamily, FontWeight);
IsMouseDirectlyOverChanged += (s, e) => {
if (_is_mouse_over_text != IsMouseDirectlyOver)
_toggle_mouse_over_text();
};
}
Brush _border_brush {
get => _border_brush_backing;
set {
_border_brush_backing = value;
_border_stroke_pen_backing = null;
}
}
Thickness _border_thickness {
get => _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 _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 ?
(_border_stroke_pen_backing = _border_stroke_pen_backing = new Pen(_border_brush, _border_thickness.Left)) :
_border_stroke_pen_backing;
#region Background
public static readonly DependencyProperty BackgroundProperty =
WPFHelper.create_dp<Brush, HyperlinkCell>(
nameof(Background),
static HyperlinkCell() {
WPFHelper.override_metadata<HorizontalAlignment, HyperlinkCell>(
HorizontalAlignmentProperty,
FrameworkPropertyMetadataOptions.AffectsArrange |
FrameworkPropertyMetadataOptions.AffectsMeasure |
FrameworkPropertyMetadataOptions.AffectsRender,
(o, v) => o._gcr.horizontal_alignment = v, HorizontalAlignment.Left);
WPFHelper.override_metadata<double, HyperlinkCell>(
WidthProperty,
FrameworkPropertyMetadataOptions.AffectsArrange |
FrameworkPropertyMetadataOptions.AffectsMeasure |
FrameworkPropertyMetadataOptions.AffectsRender,
(o, v) => o._gcr.width = v,
double.NaN);
}
public HyperlinkCell() {
_gcr.min_width = MinWidth;
_gcr.width = Width;
_gcr.max_width = MaxWidth;
_gcr.min_height = MinHeight;
_gcr.height = Height;
_gcr.max_height = Height;
_gcr.padding = _underline_padding(Padding);
_gcr.border_thickness = BorderThickness;
_gcr.horizontal_alignment = HorizontalAlignment;
_gcr.horizontal_content_alignment = HorizontalContentAlignment;
_gcr.vertical_alignment = VerticalAlignment;
_gcr.vertical_content_alignment = VerticalContentAlignment;
_background = Background;
_foreground_backing = Foreground;
_border_brush = BorderBrush;
_gcr.font_size = FontSize;
if (Text is string str && !string.IsNullOrEmpty(str))
_gcr.text_lines = new[] { str };
_gcr.glyph_context = GlyphContext.create(FontFamily, FontWeight);
}
#region BorderThickness
public static readonly DependencyProperty BorderThicknessProperty =
Control.BorderThicknessProperty.AddOwner(typeof(HyperlinkCell),
new FrameworkPropertyMetadata(
new Thickness(0),
FrameworkPropertyMetadataOptions.AffectsMeasure |
FrameworkPropertyMetadataOptions.AffectsArrange,
(d, e) => ((HyperlinkCell)d)._gcr.border_thickness = (Thickness)e.NewValue));
public Thickness BorderThickness {
get => (Thickness)GetValue(BorderThicknessProperty);
set => SetValue(BorderThicknessProperty, value);
}
#endregion
#region BorderBrush
public static readonly DependencyProperty BorderBrushProperty =
Control.BorderBrushProperty.AddOwner(typeof(HyperlinkCell),
new FrameworkPropertyMetadata(
null,
FrameworkPropertyMetadataOptions.AffectsRender,
(o, v) => o._background = v,
Brushes.Transparent);
(d, e) => ((HyperlinkCell)d)._border_brush = e.NewValue as Brush));
public Brush Background { get => GetValue(BackgroundProperty) as Brush; set => SetValue(BackgroundProperty, value); }
#endregion
public Brush BorderBrush {
get => (Brush)GetValue(BorderBrushProperty);
set => SetValue(BorderBrushProperty, value);
}
#endregion
#region Foreground
public static readonly DependencyProperty ForegroundProperty =
WPFHelper.create_dp<Brush, HyperlinkCell>(
nameof(Foreground),
FrameworkPropertyMetadataOptions.AffectsRender,
(p, v) => {
p._foreground = v;
p._underline_pen = new Pen(v, 1);
}, Brushes.Black);
#region HorizontalContentAlignment
public static readonly DependencyProperty HorizontalContentAlignmentProperty =
Control.HorizontalContentAlignmentProperty.AddOwner(typeof(HyperlinkCell),
new FrameworkPropertyMetadata(
HorizontalAlignment.Center,
FrameworkPropertyMetadataOptions.AffectsArrange,
(d, e) => ((HyperlinkCell)d)._gcr.horizontal_content_alignment = (HorizontalAlignment)e.NewValue));
public Brush Foreground { get => (Brush)GetValue(ForegroundProperty); set => SetValue(ForegroundProperty, value); }
#endregion
public HorizontalAlignment HorizontalContentAlignment {
get => (HorizontalAlignment)GetValue(HorizontalContentAlignmentProperty);
set => SetValue(HorizontalContentAlignmentProperty, value);
}
#endregion
#region FontWeight
public static readonly DependencyProperty FontWeightProperty =
TextElement.FontWeightProperty.AddOwner(
typeof(HyperlinkCell),
new FrameworkPropertyMetadata(
FontWeights.Normal,
FrameworkPropertyMetadataOptions.AffectsMeasure |
FrameworkPropertyMetadataOptions.AffectsRender,
WPFHelper.prop_changed_callback<FontWeight, HyperlinkCell>(
(p, v) => {
p._glyph_context = p._glyph_context.with(v);
p._glyph_run = null;
})));
#region VerticalContentAlignment
public static readonly DependencyProperty VerticalContentAlignmentProperty = Control.VerticalContentAlignmentProperty.AddOwner(typeof(HyperlinkCell),
new FrameworkPropertyMetadata(
VerticalAlignment.Center,
FrameworkPropertyMetadataOptions.AffectsArrange,
(d, e) => ((HyperlinkCell)d)._gcr.vertical_content_alignment = (VerticalAlignment)e.NewValue));
public FontWeight FontWeight { get => (FontWeight)GetValue(FontWeightProperty); set => SetValue(FontWeightProperty, value); }
#endregion
public VerticalAlignment VerticalContentAlignment {
get => (VerticalAlignment)GetValue(VerticalContentAlignmentProperty);
set => SetValue(VerticalContentAlignmentProperty, value);
}
#endregion
#region FontFamily
public static readonly DependencyProperty FontFamilyProperty =
TextElement.FontFamilyProperty.AddOwner(
typeof(HyperlinkCell),
new FrameworkPropertyMetadata(
TextUtils.StandardFontFamily,
FrameworkPropertyMetadataOptions.AffectsMeasure |
FrameworkPropertyMetadataOptions.AffectsRender,
WPFHelper.prop_changed_callback<FontFamily, HyperlinkCell>(
(p, v) => {
p._glyph_context = p._glyph_context.with(v);
p._glyph_run = null;
})));
#region Background
public static readonly DependencyProperty BackgroundProperty =
WPFHelper.create_dp<Brush, HyperlinkCell>(
nameof(Background),
FrameworkPropertyMetadataOptions.AffectsRender,
(o, v) => o._background = v,
Brushes.Transparent);
public FontFamily FontFamily { get => GetValue(FontFamilyProperty) as FontFamily; set => SetValue(FontFamilyProperty, value); }
#endregion
public Brush Background { get => GetValue(BackgroundProperty) as Brush; set => SetValue(BackgroundProperty, value); }
#endregion
#region FontSize
public static readonly DependencyProperty FontSizeProperty =
TextElement.FontSizeProperty.AddOwner(
typeof(HyperlinkCell),
new FrameworkPropertyMetadata(
FontSizes.Small,
FrameworkPropertyMetadataOptions.AffectsMeasure |
FrameworkPropertyMetadataOptions.AffectsRender |
FrameworkPropertyMetadataOptions.Inherits,
WPFHelper.prop_changed_callback<double, HyperlinkCell>(
(p, v) => {
p._font_size = v;
p._glyph_run = null;
})));
#region Foreground
public static readonly DependencyProperty ForegroundProperty =
WPFHelper.create_dp<Brush, HyperlinkCell>(
nameof(Foreground),
FrameworkPropertyMetadataOptions.AffectsRender,
(p, v) => p._foreground = v,
Brushes.Black);
public double FontSize { get => (double)GetValue(FontSizeProperty); set => SetValue(FontSizeProperty, value); }
#endregion
public Brush Foreground { get => (Brush)GetValue(ForegroundProperty); set => SetValue(ForegroundProperty, value); }
#endregion
#region Padding
public static readonly DependencyProperty PaddingProperty =
WPFHelper.create_dp<Thickness, HyperlinkCell>(
nameof(Padding),
#region FontWeight
public static readonly DependencyProperty FontWeightProperty =
TextElement.FontWeightProperty.AddOwner(
typeof(HyperlinkCell),
new FrameworkPropertyMetadata(
FontWeights.Normal,
FrameworkPropertyMetadataOptions.AffectsMeasure |
FrameworkPropertyMetadataOptions.AffectsRender,
(p, v) => {
p._padding = v;
p._glyph_run = null;
}, DefaultCellPadding);
WPFHelper.prop_changed_callback<FontWeight, HyperlinkCell>(
(p, v) => p._gcr.glyph_context = p._gcr.glyph_context.with(v))));
public Thickness Padding { get => (Thickness)GetValue(PaddingProperty); set => SetValue(PaddingProperty, value); }
#endregion
public FontWeight FontWeight { get => (FontWeight)GetValue(FontWeightProperty); set => SetValue(FontWeightProperty, value); }
#endregion
#region Text
public static readonly DependencyProperty TextProperty =
WPFHelper.create_dp<string, HyperlinkCell>(
nameof(Text),
#region FontFamily
public static readonly DependencyProperty FontFamilyProperty =
TextElement.FontFamilyProperty.AddOwner(
typeof(HyperlinkCell),
new FrameworkPropertyMetadata(
TextUtils.StandardFontFamily,
FrameworkPropertyMetadataOptions.AffectsMeasure |
FrameworkPropertyMetadataOptions.AffectsRender,
(o, v) => {
o._text = v;
o._glyph_run = null;
}, string.Empty);
WPFHelper.prop_changed_callback<FontFamily, HyperlinkCell>(
(p, v) => p._gcr.glyph_context = p._gcr.glyph_context.with(v))));
public string Text { get => GetValue(TextProperty) as string; set => SetValue(TextProperty, value); }
#endregion
#region ICommandSource
public FontFamily FontFamily { get => GetValue(FontFamilyProperty) as FontFamily; set => SetValue(FontFamilyProperty, value); }
#endregion
#region Command
public static readonly DependencyProperty CommandProperty =
WPFHelper.create_dp<ICommand, HyperlinkCell>(
nameof(Command),
(s, old_val, new_val) => {
if (old_val != null)
CanExecuteChangedEventManager.RemoveHandler(old_val, s.OnCanExecuteChanged);
if (new_val != null)
CanExecuteChangedEventManager.AddHandler(new_val, s.OnCanExecuteChanged);
s.UpdateCanExecute();
});
#region FontSize
public static readonly DependencyProperty FontSizeProperty =
TextElement.FontSizeProperty.AddOwner(
typeof(HyperlinkCell),
new FrameworkPropertyMetadata(
FontSizes.Small,
FrameworkPropertyMetadataOptions.AffectsMeasure |
FrameworkPropertyMetadataOptions.AffectsRender |
FrameworkPropertyMetadataOptions.Inherits,
WPFHelper.prop_changed_callback<double, HyperlinkCell>(
(p, v) => p._gcr.font_size = v)));
[Bindable(true), Category("Action")]
[Localizability(LocalizationCategory.NeverLocalize)]
public ICommand Command { get => (ICommand)GetValue(CommandProperty); set => SetValue(CommandProperty, value); }
#endregion
public double FontSize { get => (double)GetValue(FontSizeProperty); set => SetValue(FontSizeProperty, value); }
#endregion
#region CommandParameter
public static readonly DependencyProperty CommandParameterProperty =
DependencyProperty.Register(
nameof(CommandParameter),
typeof(object),
typeof(HyperlinkCell));
#region Padding
public static readonly DependencyProperty PaddingProperty =
WPFHelper.create_dp<Thickness, HyperlinkCell>(
nameof(Padding),
FrameworkPropertyMetadataOptions.AffectsMeasure |
FrameworkPropertyMetadataOptions.AffectsRender,
(p, v) => p._gcr.padding = _underline_padding(v), DIYGrid.DefaultCellPadding);
[Bindable(true), Category("Action")]
[Localizability(LocalizationCategory.NeverLocalize)]
public object CommandParameter { get => GetValue(CommandParameterProperty); set => SetValue(CommandParameterProperty, value); }
#endregion
public Thickness Padding { get => (Thickness)GetValue(PaddingProperty); set => SetValue(PaddingProperty, value); }
#endregion
#region CommandTarget
public static readonly DependencyProperty CommandTargetProperty =
DependencyProperty.Register(
nameof(CommandTarget),
typeof(IInputElement),
typeof(HyperlinkCell));
#region Text
public static readonly DependencyProperty TextProperty =
WPFHelper.create_dp<string, HyperlinkCell>(
nameof(Text),
FrameworkPropertyMetadataOptions.AffectsMeasure |
FrameworkPropertyMetadataOptions.AffectsRender,
(p, v) => p._gcr.text_lines = string.IsNullOrEmpty(v) ? null : new[] { v }, string.Empty);
[Bindable(true), Category("Action")]
public IInputElement CommandTarget { get => (IInputElement)GetValue(CommandTargetProperty); set => SetValue(CommandTargetProperty, value); }
#endregion
public string Text { get => GetValue(TextProperty) as string; set => SetValue(TextProperty, value); }
#endregion
protected override bool IsEnabledCore => base.IsEnabledCore && CanExecute;
#region ICommandSource
bool CanExecute {
get => _can_execute;
set {
if (_can_execute != value) {
_can_execute = value;
CoerceValue(IsEnabledProperty);
}
#region Command
public static readonly DependencyProperty CommandProperty =
WPFHelper.create_dp<ICommand, HyperlinkCell>(
nameof(Command),
(s, old_val, new_val) => {
if (old_val != null)
CanExecuteChangedEventManager.RemoveHandler(old_val, s.OnCanExecuteChanged);
if (new_val != null)
CanExecuteChangedEventManager.AddHandler(new_val, s.OnCanExecuteChanged);
s.UpdateCanExecute();
});
[Bindable(true), Category("Action")]
[Localizability(LocalizationCategory.NeverLocalize)]
public ICommand Command { get => (ICommand)GetValue(CommandProperty); set => SetValue(CommandProperty, value); }
#endregion
#region CommandParameter
public static readonly DependencyProperty CommandParameterProperty =
DependencyProperty.Register(
nameof(CommandParameter),
typeof(object),
typeof(HyperlinkCell));
[Bindable(true), Category("Action")]
[Localizability(LocalizationCategory.NeverLocalize)]
public object CommandParameter { get => GetValue(CommandParameterProperty); set => SetValue(CommandParameterProperty, value); }
#endregion
#region CommandTarget
public static readonly DependencyProperty CommandTargetProperty =
DependencyProperty.Register(
nameof(CommandTarget),
typeof(IInputElement),
typeof(HyperlinkCell));
[Bindable(true), Category("Action")]
public IInputElement CommandTarget { get => (IInputElement)GetValue(CommandTargetProperty); set => SetValue(CommandTargetProperty, value); }
#endregion
protected override bool IsEnabledCore => base.IsEnabledCore && CanExecute;
bool CanExecute {
get => _can_execute;
set {
if (_can_execute != value) {
_can_execute = value;
CoerceValue(IsEnabledProperty);
}
}
}
void OnCanExecuteChanged(object sender, EventArgs e) =>
UpdateCanExecute();
void OnCanExecuteChanged(object sender, EventArgs e) =>
UpdateCanExecute();
void UpdateCanExecute() =>
CanExecute = Command == null || CommandHelper.CanExecuteCommandSource(this);
void UpdateCanExecute() =>
CanExecute = Command == null || CommandHelper.CanExecuteCommandSource(this);
#endregion
#endregion
#region Paraphrased/Simplified from https://referencesource.microsoft.com/#PresentationFramework/src/Framework/System/Windows/Documents/Hyperlink.cs
#region Paraphrased/Simplified from https://referencesource.microsoft.com/#PresentationFramework/src/Framework/System/Windows/Documents/Hyperlink.cs
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e) {
base.OnMouseLeftButtonDown(e);
_check_mouse_over_text(e);
if (_is_mouse_over_text) {
// Hyperlink should take focus when left mouse button is clicked on it
// This is consistent with all ButtonBase controls and current Win32 behavior
Focus();
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e) {
base.OnMouseLeftButtonDown(e);
_check_mouse_over_text(e);
if (_is_mouse_over_text) {
// Hyperlink should take focus when left mouse button is clicked on it
// This is consistent with all ButtonBase controls and current Win32 behavior
Focus();
// It is possible that the mouse state could have changed during all of
// the call-outs that have happened so far.
if (e.ButtonState == MouseButtonState.Pressed) {
// Capture the mouse, and make sure we got it.
CaptureMouse();
}
e.Handled = true;
// It is possible that the mouse state could have changed during all of
// the call-outs that have happened so far.
if (e.ButtonState == MouseButtonState.Pressed) {
// Capture the mouse, and make sure we got it.
CaptureMouse();
}
}
protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e) {
base.OnMouseLeftButtonUp(e);
_check_mouse_over_text(e);
if (IsMouseCaptured)
ReleaseMouseCapture();
if (_can_execute && _is_mouse_over_text)
CommandHelper.ExecuteCommandSource(this);
e.Handled = true;
}
}
#endregion
protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e) {
base.OnMouseLeftButtonUp(e);
_check_mouse_over_text(e);
if (IsMouseCaptured)
ReleaseMouseCapture();
if (_can_execute && _is_mouse_over_text)
CommandHelper.ExecuteCommandSource(this);
e.Handled = true;
}
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);
#endregion
protected override Size ArrangeOverride(Size finalSize) {
if (_glyph_run == null)
_set_glyph(finalSize);
return finalSize;
protected override void OnIsMouseDirectlyOverChanged(DependencyPropertyChangedEventArgs e) {
if (_is_mouse_over_text != IsMouseDirectlyOver)
_toggle_mouse_over_text();
}
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 Size ArrangeOverride(Size finalSize) => finalSize;
protected override Size MeasureOverride(Size availableSize) {
_gcr.available_size = availableSize;
if (double.IsInfinity(_gcr.layout_size.Height) || double.IsInfinity(_gcr.layout_size.Width)) {
_gcr.remeasure();
}
return _gcr.layout_size;
}
protected override Size MeasureOverride(Size availableSize) {
if (_glyph_run == null)
_set_glyph(availableSize);
return new Size(Math.Max(availableSize.Width, _padding.Left + _glyph_width + _padding.Right),
Math.Max(availableSize.Height, _padding.Top + _font_size + _padding.Bottom));
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);
}
void _set_glyph(Size size) {
_glyph_run = _glyph_context.get_glyph_run(_text, _font_size, size, _horizontal_alignment, VerticalAlignment.Center, _padding);
_glyph_width = _glyph_run == null ? 0 : _glyph_run.AdvanceWidths.Sum();
}
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, null, rect);
if (_glyph_run != null) {
drawingContext.PushClip(new RectangleGeometry(rect));
drawingContext.DrawGlyphRun(_foreground, _glyph_run);
if (_is_mouse_over_text) {
// draw underline
Point pt;
switch (_horizontal_alignment) {
case HorizontalAlignment.Left:
pt = new Point(Padding.Left, 0.5 * (ActualHeight + _font_size) + 1);
break;
case HorizontalAlignment.Right:
pt = new Point(ActualWidth - Padding.Right - _glyph_width, 0.5 * (ActualHeight + _font_size) + 1);
break;
case HorizontalAlignment.Center:
default:
pt = new Point(0.5 * (ActualWidth - _glyph_width), 0.5 * (ActualHeight + _font_size) + 1);
break;
}
drawingContext.DrawLine(_underline_pen, pt, new Point(pt.X + _glyph_width, pt.Y));
}
drawingContext.Pop();
}
if (bg != null) {
var bgo = GetBackgroundOverlay(this);
if (bgo != null)
drawingContext.DrawRectangle(bgo, null, rect);
if (_glyph_run != null) {
drawingContext.PushClip(new RectangleGeometry(rect));
drawingContext.DrawGlyphRun(_foreground, _glyph_run);
if (_is_mouse_over_text) {
// 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));
}
drawingContext.Pop();
}
if (bg != null) {
var bgo = DIYGrid.GetBackgroundOverlay(this);
if (bgo != null)
drawingContext.DrawRectangle(bgo, null, rect);
}
}
bool _point_horizontally_over_text(double x) {
switch (_horizontal_alignment) {
case HorizontalAlignment.Left:
return x >= _padding.Left && x <= _padding.Left + _glyph_width;
case HorizontalAlignment.Right:
return x >= ActualWidth - Padding.Right - _glyph_width && x <= ActualWidth - _padding.Right;
case HorizontalAlignment.Center:
default:
return x >= 0.5 * (ActualWidth - _glyph_width) && x <= 0.5 * (ActualWidth - _glyph_width) + _glyph_width;
}
}
static Thickness _underline_padding(Thickness padding) =>
new Thickness(padding.Left, padding.Top, padding.Right, padding.Bottom + 2);
void _check_mouse_over_text(MouseEventArgs e) {
var pt = e.GetPosition(this);
bool over_text =
_glyph_run != null &&
_point_horizontally_over_text(pt.X) &&
pt.Y >= _padding.Top &&
pt.Y <= _padding.Top + _font_size;
if (_is_mouse_over_text != over_text)
_toggle_mouse_over_text();
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 _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 _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();
}
}
}
@@ -8,178 +8,263 @@ using System.Windows.Media;
using FontFamily = System.Windows.Media.FontFamily;
namespace mbts.ui.controls {
partial class DIYGrid : Control {
public class TextCell : FrameworkElement {
// these fields each represent properties that are accessed frequently,
// they are stored locally to reduce repetative DependencyProperty lookups
Thickness _padding;
HorizontalAlignment _horizontal_alignment;
Brush _background, _foreground;
double _glyph_width, _font_size;
string _text;
GlyphRun _glyph_run;
GlyphContext _glyph_context;
class TextCell : FrameworkElement {
// these fields each represent properties that are accessed frequently,
// they are stored locally to reduce repetative DependencyProperty lookups
Thickness _padding => _gcr.padding;
Brush _background, _foreground, _border_brush_backing;
double _glyph_width => _gcr.glyph_run.AdvanceWidths.Sum();
GlyphRun _glyph_run => _gcr.glyph_run;
Pen _border_stroke_pen_backing;
GlyphContentRect _gcr = new GlyphContentRect();
static TextCell() {
WPFHelper.override_metadata<HorizontalAlignment, TextCell>(
HorizontalAlignmentProperty,
FrameworkPropertyMetadataOptions.AffectsArrange |
Brush _border_brush {
get => _border_brush_backing;
set {
_border_brush_backing = value;
_border_stroke_pen_backing = null;
}
}
Thickness _border_thickness {
get => _gcr.border_thickness;
set {
_gcr.border_thickness = new Thickness(value.Left);
_border_stroke_pen_backing = null;
}
}
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 ?
(_border_stroke_pen_backing = _border_stroke_pen_backing = new Pen(_border_brush, _border_thickness.Left)) :
_border_stroke_pen_backing;
static TextCell() {
WPFHelper.override_metadata<HorizontalAlignment, TextCell>(
HorizontalAlignmentProperty,
FrameworkPropertyMetadataOptions.AffectsArrange |
FrameworkPropertyMetadataOptions.AffectsMeasure |
FrameworkPropertyMetadataOptions.AffectsRender,
(o, v) => o._gcr.horizontal_alignment = v, HorizontalAlignment.Left);
WPFHelper.override_metadata<double, TextCell>(
WidthProperty,
FrameworkPropertyMetadataOptions.AffectsArrange |
FrameworkPropertyMetadataOptions.AffectsMeasure |
FrameworkPropertyMetadataOptions.AffectsRender,
(o, v) => o._gcr.width = v,
double.NaN);
}
public TextCell() {
_gcr.min_width = MinWidth;
_gcr.width = Width;
_gcr.max_width = MaxWidth;
_gcr.min_height = MinHeight;
_gcr.height = Height;
_gcr.max_height = Height;
_gcr.padding = Padding;
_gcr.border_thickness = BorderThickness;
_gcr.horizontal_alignment = HorizontalAlignment;
_gcr.horizontal_content_alignment = HorizontalContentAlignment;
_gcr.vertical_alignment = VerticalAlignment;
_gcr.vertical_content_alignment = VerticalContentAlignment;
_background = Background;
_foreground = Foreground;
_border_brush = BorderBrush;
_gcr.font_size = FontSize;
if (Text is string str && !string.IsNullOrEmpty(str))
_gcr.text_lines = new[] { str };
_gcr.glyph_context = GlyphContext.create(FontFamily, FontWeight);
}
#region BorderThickness
public static readonly DependencyProperty BorderThicknessProperty =
Control.BorderThicknessProperty.AddOwner(typeof(TextCell),
new FrameworkPropertyMetadata(
new Thickness(0),
FrameworkPropertyMetadataOptions.AffectsMeasure |
FrameworkPropertyMetadataOptions.AffectsArrange,
(d, e) => ((TextCell)d)._gcr.border_thickness = (Thickness)e.NewValue));
public Thickness BorderThickness {
get => (Thickness)GetValue(BorderThicknessProperty);
set => SetValue(BorderThicknessProperty, value);
}
#endregion
#region BorderBrush
public static readonly DependencyProperty BorderBrushProperty =
Control.BorderBrushProperty.AddOwner(typeof(TextCell),
new FrameworkPropertyMetadata(
null,
FrameworkPropertyMetadataOptions.AffectsRender,
(d, e) => ((TextCell)d)._border_brush = e.NewValue as Brush));
public Brush BorderBrush {
get => (Brush)GetValue(BorderBrushProperty);
set => SetValue(BorderBrushProperty, value);
}
#endregion
#region HorizontalContentAlignment
public static readonly DependencyProperty HorizontalContentAlignmentProperty =
Control.HorizontalContentAlignmentProperty.AddOwner(typeof(TextCell),
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;
}));
public HorizontalAlignment HorizontalContentAlignment {
get => (HorizontalAlignment)GetValue(HorizontalContentAlignmentProperty);
set => SetValue(HorizontalContentAlignmentProperty, value);
}
#endregion
#region VerticalContentAlignment
public static readonly DependencyProperty VerticalContentAlignmentProperty = Control.VerticalContentAlignmentProperty.AddOwner(typeof(TextCell),
new FrameworkPropertyMetadata(
VerticalAlignment.Center,
FrameworkPropertyMetadataOptions.AffectsArrange,
(d, e) => ((TextCell)d)._gcr.vertical_content_alignment = (VerticalAlignment)e.NewValue));
public VerticalAlignment VerticalContentAlignment {
get => (VerticalAlignment)GetValue(VerticalContentAlignmentProperty);
set => SetValue(VerticalContentAlignmentProperty, value);
}
#endregion
#region Background
public static readonly DependencyProperty BackgroundProperty =
WPFHelper.create_dp<Brush, TextCell>(
nameof(Background),
FrameworkPropertyMetadataOptions.AffectsRender,
(o, v) => o._background = v,
Brushes.Transparent);
public Brush Background { get => GetValue(BackgroundProperty) as Brush; set => SetValue(BackgroundProperty, value); }
#endregion
#region Foreground
public static readonly DependencyProperty ForegroundProperty =
WPFHelper.create_dp<Brush, TextCell>(
nameof(Foreground),
FrameworkPropertyMetadataOptions.AffectsRender,
(p, v) => p._foreground = v,
Brushes.Black);
public Brush Foreground { get => (Brush)GetValue(ForegroundProperty); set => SetValue(ForegroundProperty, value); }
#endregion
#region FontWeight
public static readonly DependencyProperty FontWeightProperty =
TextElement.FontWeightProperty.AddOwner(
typeof(TextCell),
new FrameworkPropertyMetadata(
FontWeights.Normal,
FrameworkPropertyMetadataOptions.AffectsMeasure |
FrameworkPropertyMetadataOptions.AffectsRender,
(o, v) => {
o._horizontal_alignment = v;
o._glyph_run = null;
},
HorizontalAlignment.Left);
WPFHelper.override_metadata<double, TextCell>(
WidthProperty,
FrameworkPropertyMetadataOptions.AffectsArrange |
WPFHelper.prop_changed_callback<FontWeight, TextCell>(
(p, v) => p._gcr.glyph_context = p._gcr.glyph_context.with(v))));
public FontWeight FontWeight { get => (FontWeight)GetValue(FontWeightProperty); set => SetValue(FontWeightProperty, value); }
#endregion
#region FontFamily
public static readonly DependencyProperty FontFamilyProperty =
TextElement.FontFamilyProperty.AddOwner(
typeof(TextCell),
new FrameworkPropertyMetadata(
TextUtils.StandardFontFamily,
FrameworkPropertyMetadataOptions.AffectsMeasure |
FrameworkPropertyMetadataOptions.AffectsRender,
(o, v) => o._glyph_run = null,
double.NaN);
}
WPFHelper.prop_changed_callback<FontFamily, TextCell>(
(p, v) => p._gcr.glyph_context = p._gcr.glyph_context.with(v))));
public TextCell() {
_background = Background;
_foreground = Foreground;
_horizontal_alignment = HorizontalAlignment;
_padding = Padding;
_font_size = FontSize;
_text = Text;
_glyph_context = GlyphContext.create(FontFamily, FontWeight);
}
public FontFamily FontFamily { get => GetValue(FontFamilyProperty) as FontFamily; set => SetValue(FontFamilyProperty, value); }
#endregion
#region Background
public static readonly DependencyProperty BackgroundProperty =
WPFHelper.create_dp<Brush, TextCell>(
nameof(Background),
FrameworkPropertyMetadataOptions.AffectsRender,
(p, v) => p._background = v,
null);
public Brush Background { get => (Brush)GetValue(BackgroundProperty); set => SetValue(BackgroundProperty, value); }
#endregion
#region Foreground
public static readonly DependencyProperty ForegroundProperty =
WPFHelper.create_dp<Brush, TextCell>(
nameof(Foreground),
FrameworkPropertyMetadataOptions.AffectsRender,
(p, v) => p._foreground = v,
Brushes.Black);
public Brush Foreground { get => (Brush)GetValue(ForegroundProperty); set => SetValue(ForegroundProperty, value); }
#endregion
#region FontFamily
public static readonly DependencyProperty FontFamilyProperty =
TextElement.FontFamilyProperty.AddOwner(
typeof(TextCell),
new FrameworkPropertyMetadata(
TextUtils.StandardFontFamily,
FrameworkPropertyMetadataOptions.AffectsMeasure |
FrameworkPropertyMetadataOptions.AffectsRender,
WPFHelper.prop_changed_callback<FontFamily, TextCell>(
(p, v) => {
p._glyph_context = p._glyph_context.with(v);
p._glyph_run = null;
})));
public FontFamily FontFamily { get => GetValue(FontFamilyProperty) as FontFamily; set => SetValue(FontFamilyProperty, value); }
#endregion
#region FontWeight
public static readonly DependencyProperty FontWeightProperty =
TextElement.FontWeightProperty.AddOwner(
typeof(TextCell),
new FrameworkPropertyMetadata(
FontWeights.Normal,
FrameworkPropertyMetadataOptions.AffectsMeasure |
FrameworkPropertyMetadataOptions.AffectsRender,
WPFHelper.prop_changed_callback<FontWeight, TextCell>(
(p, v) => {
p._glyph_context = p._glyph_context.with(v);
p._glyph_run = null;
})));
public FontWeight FontWeight { get => (FontWeight)GetValue(FontWeightProperty); set => SetValue(FontWeightProperty, value); }
#endregion
#region FontSize
public static readonly DependencyProperty FontSizeProperty =
TextElement.FontSizeProperty.AddOwner(
typeof(TextCell),
new FrameworkPropertyMetadata(
FontSizes.Small,
FrameworkPropertyMetadataOptions.AffectsMeasure |
FrameworkPropertyMetadataOptions.AffectsRender |
FrameworkPropertyMetadataOptions.Inherits,
WPFHelper.prop_changed_callback<double, TextCell>(
(p, v) => {
p._font_size = v;
p._glyph_run = null;
})));
public double FontSize { get => (double)GetValue(FontSizeProperty); set => SetValue(FontSizeProperty, value); }
#endregion
#region Padding
public static readonly DependencyProperty PaddingProperty =
WPFHelper.create_dp<Thickness, TextCell>(
nameof(Padding),
#region FontSize
public static readonly DependencyProperty FontSizeProperty =
TextElement.FontSizeProperty.AddOwner(
typeof(TextCell),
new FrameworkPropertyMetadata(
FontSizes.Small,
FrameworkPropertyMetadataOptions.AffectsMeasure |
FrameworkPropertyMetadataOptions.AffectsRender,
(p, v) => {
p._padding = v;
p._glyph_run = null;
}, DefaultCellPadding);
FrameworkPropertyMetadataOptions.AffectsRender |
FrameworkPropertyMetadataOptions.Inherits,
WPFHelper.prop_changed_callback<double, TextCell>(
(p, v) => p._gcr.font_size = v)));
public Thickness Padding { get => (Thickness)GetValue(PaddingProperty); set => SetValue(PaddingProperty, value); }
#endregion
public double FontSize { get => (double)GetValue(FontSizeProperty); set => SetValue(FontSizeProperty, value); }
#endregion
#region Text
public static readonly DependencyProperty TextProperty =
WPFHelper.create_dp<string, TextCell>(
nameof(Text),
FrameworkPropertyMetadataOptions.AffectsMeasure |
FrameworkPropertyMetadataOptions.AffectsRender,
(p, v) => {
p._text = v;
p._glyph_run = null;
});
#region Padding
public static readonly DependencyProperty PaddingProperty =
WPFHelper.create_dp<Thickness, TextCell>(
nameof(Padding),
FrameworkPropertyMetadataOptions.AffectsMeasure |
FrameworkPropertyMetadataOptions.AffectsRender,
(p, v) => p._gcr.padding = v,
DIYGrid.DefaultCellPadding);
public string Text { get => GetValue(TextProperty) as string; set => SetValue(TextProperty, value); }
#endregion
public Thickness Padding { get => (Thickness)GetValue(PaddingProperty); set => SetValue(PaddingProperty, value); }
#endregion
protected override Size ArrangeOverride(Size finalSize) {
if (_glyph_run == null)
_set_glyph(finalSize);
return finalSize;
#region Text
public static readonly DependencyProperty TextProperty =
WPFHelper.create_dp<string, TextCell>(
nameof(Text),
FrameworkPropertyMetadataOptions.AffectsMeasure |
FrameworkPropertyMetadataOptions.AffectsRender,
(p, v) => p._gcr.text_lines = string.IsNullOrEmpty(v) ? null : new[] { v }, string.Empty);
public string Text { get => GetValue(TextProperty) as string; set => SetValue(TextProperty, value); }
#endregion
protected override Size ArrangeOverride(Size finalSize) => finalSize;
protected override Size MeasureOverride(Size availableSize) {
_gcr.available_size = availableSize;
if (double.IsInfinity(_gcr.layout_size.Height) || double.IsInfinity(_gcr.layout_size.Width)) {
_gcr.remeasure();
}
return _gcr.layout_size;
}
protected override Size MeasureOverride(Size availableSize) {
if (_glyph_run == null)
_set_glyph(availableSize);
return new Size(Math.Max(availableSize.Width, _padding.Left + _glyph_width + _padding.Right),
Math.Max(availableSize.Height, _padding.Top + _font_size + _padding.Bottom));
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) {
void _set_glyph(Size size) {
_glyph_run = _glyph_context.get_glyph_run(_text, _font_size, size, _horizontal_alignment, VerticalAlignment.Center, _padding);
_glyph_width = _glyph_run == null ? 0 : _glyph_run.AdvanceWidths.Sum();
}
if (new_x < bl_x) {
protected override void OnRender(DrawingContext drawingContext) {
var rs = RenderSize;
var rect = new Rect(0, 0, rs.Width, rs.Height);
if (_background != null)
drawingContext.DrawRectangle(_background, null, rect);
if (_glyph_run != null) {
drawingContext.PushClip(new RectangleGeometry(rect));
drawingContext.DrawGlyphRun(_foreground, _glyph_run);
drawingContext.Pop();
}
}
if (Text == "-3,375.000") {
}
bl_x = new_x;
drawingContext.PushClip(new RectangleGeometry(rect));
drawingContext.DrawGlyphRun(_foreground, _glyph_run);
drawingContext.Pop();
}
if (bg != null) {
var bgo = DIYGrid.GetBackgroundOverlay(this);
if (bgo != null)
drawingContext.DrawRectangle(bgo, null, rect);
}
}
}
@@ -1,17 +1,18 @@
using mbts.spark;
using bts;
using mbts.spark;
using mbts.spark.filters;
using mbts.ui.settings;
using mbts.ui.shell;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace mbts.ui.controls {
namespace mbts.ui.controls {
// Column types are nested inside the DIYGrid
partial class DIYGrid {
public static class DefaultColumnWidths {
@@ -53,13 +54,28 @@ namespace mbts.ui.controls {
public static class MiniOrderBook {
public const double
Cancel = 37,
Cancel = 41,
AttachDetach = 45,
Update = 45,
EditableSize = 40,
EditablePrice = 64,
Commit = 20;
}
public static class PNL {
public const double
// 'PNL Account' is a bit wider than normal Account cols, bc it is a 'grouping' column, so it has additional content/icons
Account = 100,
Exchange = 85,
Symbol = 72,
Security = 87,
Qty = 42,
Price = 64,
PriceBig = 92,
SecurityType = 64,
WorkingQty = 54,
HasDayTrades = 74;
}
}
public static class ShortColumnWidths {
@@ -100,6 +116,7 @@ namespace mbts.ui.controls {
Working = "Working",
Bid = "Bid",
Ask = "Ask",
Mid = "Mid",
Settlement = "Settlement",
DayChange = "Change",
SessionLow = "Low",
@@ -109,8 +126,8 @@ namespace mbts.ui.controls {
Description = "Description",
N_Legs = "# Legs",
N_Working_Orders = "# Working Orders",
StartNetPos = "Start Net Pos",
StartPnL = "Start PnL",
StartNetPos = "Open Pos",
StartPnL = "Open PnL",
DayLong = "Day Long",
DayShort = "Day Short",
DayPnL = "Day PnL",
@@ -129,6 +146,41 @@ namespace mbts.ui.controls {
PositionInQueue = "PIQ",
RootAlgoType = "Root Algo"
;
public static class PNLHeaders {
public const string
PNL = "PnL",
Long = "Long",
Short = "Short",
Net = "Pos",
AvgPrice = "Avg",
AvgPriceDelta = "Avg Delta",
HasDayTrades = "Traded Today",
SecurityType = "Type";
public static readonly Part
SOD = new Part("Open"),
Day = new Part("Day"),
Total = new Part("Total"),
Working = new Part("Working");
public readonly struct Part {
public readonly string prefix;
public Part(string prefix) =>
this.prefix = prefix;
public string PNL => $"{prefix} PNL";
public string Net => $"{prefix} {PNLHeaders.Net}";
public string NetAvgDelta => $"{prefix} Net {AvgPriceDelta}";
public string Long => $"{prefix} {PNLHeaders.Long}";
public string LongAvg => $"{Long} {AvgPrice}";
public string LongAvgDelta => $"{Long} {AvgPriceDelta}";
public string Short => $"{prefix} {PNLHeaders.Short}";
public string ShortAvg => $"{Short} {AvgPrice}";
public string ShortAvgDelta => $"{Short} {AvgPriceDelta}";
}
}
}
public interface IKeyedColumn<TColKey> {
@@ -171,6 +223,7 @@ namespace mbts.ui.controls {
// To include extra data with your column type you are encouraged to derive from DIYGridColumn
public abstract class ColumnBase : GridViewColumn, IValueFactory<GridColumnSettings>, INotifyPropertyChanged {
//readonly ExternalProperty<FontWeight?> _font_weigh
GridColumnSettings _settings;
FontWeight _fallback_font_weight;
Color _fallback_font_color;
double _fallback_font_size;
@@ -178,6 +231,7 @@ namespace mbts.ui.controls {
FontWeight? _font_weight_override;
Color? _font_color_override;
object _header_copy;
bool _is_visible;
protected ColumnBase() {
var gs = GridSettings.Default;
@@ -199,13 +253,23 @@ namespace mbts.ui.controls {
public event Action<ColumnBase, ListSortDirection?> sort_direction_changed;
public event Action font_params_changed;
public event Action<bool> is_visible_changed;
public string id { get; set; }
public string sort_member_path { get; set; }
public PropertyPath data_member_path { get; set; }
public double original_width { get; set; }
public bool is_collapsed { get; set; }
// TODO: hook up visibility
public bool is_visible { get; set; } = true;
public bool is_visible {
get => _is_visible;
set {
if (_is_visible != value) {
_is_visible = value;
is_visible_changed?.Invoke(value);
raise_prop_changed();
}
}
}
public FontWeight? font_weight_override { get => _font_weight_override; set => _set_font_param(ref _font_weight_override, value); }
public Color? font_color_override { get => _font_color_override; set => _set_font_param(ref _font_color_override, value); }
public HorizontalAlignment horizontal_alignment { get; set; }
@@ -215,6 +279,7 @@ namespace mbts.ui.controls {
public FontWeight font_weight => font_weight_override ?? _fallback_font_weight;
public Color font_color => font_color_override ?? _fallback_font_color;
public Brush font_brush => new SolidColorBrush(font_color);
public GridColumnSettings settings => _settings ?? (_settings = create());
// TODO: find a better way to store the header-property in a thread-safe manner - a value for GridSettings.display_name, more specifically.
// Since save_state is called from a different thread in IShared/SlowWindows, it is not safe to access the DependencyProperty store from that method.
// By updating a local _header_copy field in the Header setter, the DependencyProperty lookup may be bypassed when appropriate (see create method below).
@@ -223,6 +288,8 @@ namespace mbts.ui.controls {
set => base.Header = (_header_copy = value);
}
/// important: don't access any dependency props in here.
/// this may be called from a thread other than that of this dispatcher (i.e., when <see cref="IShellWindow.GetState(System.IO.Stream)"/> is called).
public GridColumnSettings create() =>
new GridColumnSettings(id, _header_copy?.ToString(), is_visible, is_collapsed, font_weight_override, font_color_override, ActualWidth, horizontal_alignment, horizontal_offset);
@@ -234,7 +301,8 @@ namespace mbts.ui.controls {
public virtual void load_from(GridColumnSettings settings) {
if (settings == null)
return;
using (ReflectionHelper.detect_property_changes(this, _raise_prop_changed, nameof(is_visible), nameof(is_collapsed), nameof(font_weight_override), nameof(font_weight),
_settings = settings;
using (ReflectionHelper.detect_property_changes(this, raise_prop_changed, nameof(is_visible), nameof(is_collapsed), nameof(font_weight_override), nameof(font_weight),
nameof(font_color_override), nameof(font_color), nameof(font_brush), nameof(font_family), nameof(font_size), nameof(horizontal_alignment), nameof(horizontal_offset))) {
//
id = settings.id;
@@ -252,11 +320,12 @@ namespace mbts.ui.controls {
Width = original_width;
}
//
raise_prop_changed(nameof(ColumnBase.settings));
}
// 'fallback' font-params (aka 'grid-level' font-params)
public void set_fallback_font_params(FontFamily font_family, double font_size, Color font_color, FontWeight font_weight) {
using (ReflectionHelper.detect_property_changes(this, _raise_prop_changed, nameof(font_family), nameof(font_size), nameof(font_color), nameof(font_weight))) {
using (ReflectionHelper.detect_property_changes(this, raise_prop_changed, nameof(font_family), nameof(font_size), nameof(font_color), nameof(font_weight))) {
_fallback_font_family = font_family;
_fallback_font_size = font_size;
_fallback_font_color = font_color;
@@ -268,14 +337,42 @@ namespace mbts.ui.controls {
protected virtual void OnSortDirectionChanged(ListSortDirection? new_value) =>
sort_direction_changed?.Invoke(this, new_value);
void _raise_prop_changed(string prop) =>
protected void raise_prop_changed([CallerMemberName]string prop = "") =>
OnPropertyChanged(new PropertyChangedEventArgs(prop));
protected void set<T>(ref T backing, T new_value, [CallerMemberName]string prop = "") =>
set(ref backing, new_value, EqualityComparer<T>.Default, prop);
protected void set<T>(ref T backing, T new_value, IEqualityComparer<T> comparer, [CallerMemberName]string prop = "") {
if (!comparer.Equals(backing, new_value)) {
backing = new_value;
OnPropertyChanged(new PropertyChangedEventArgs(prop));
}
}
public override string ToString() =>
id.ToString();
}
internal abstract class ColumnBase<TColKey> : ColumnBase, IKeyedColumn<TColKey>, IGridColumn {
TColKey _key;
bool _key_initialized;
// need a default constructor to support declaration in xaml
protected ColumnBase() { }
protected ColumnBase(TColKey key) => this.key = key;
public TColKey key { get; }
public TColKey key {
get => _key;
set {
if (Equals(_key, value))
return;
Log.assert(!_key_initialized, "Column key already initialized.");
_key_initialized = true;
_key = value;
on_key_initialized(value);
}
}
protected virtual void on_key_initialized(TColKey key) { }
}
}
}
@@ -13,7 +13,7 @@ namespace mbts.ui.controls {
public double width { get { return column.ActualWidth; } }
public double right { get { return unclipped_left + width; } }
public bool is_partially_obscured { get { return unclipped_left < left; } }
public double visible_width => width - left + unclipped_left;
public double visible_width => Math.Max(0, width - left + unclipped_left);
public override string ToString() {
return "{3} {0:0.0},{1:0.0} w{2:0.0}".fmt(left, right, width, column.Header);
}
+423 -26
View File
@@ -1,9 +1,11 @@
using bts;
using bts.utils;
using mbts.spark.filters;
using mbts.ui.controls.adorners;
using mbts.ui.extensions;
using mbts.ui.settings;
using mbts.ui.utility;
using bts.utils;
using mbts.utils;
using System;
using System.Collections;
using System.Collections.Generic;
@@ -12,14 +14,13 @@ using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Reactive.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Media;
using mbts.utils;
using System.Reactive.Linq;
namespace mbts.ui.controls {
interface IHandleGridSorting {
@@ -32,7 +33,21 @@ namespace mbts.ui.controls {
public DIYGrid.ColumnBase Column { get; }
public bool Handled { get; set; }
}
class CachedColumnParam {
public string SourcePropertyName { get; set; }
public DependencyProperty TargetProperty { get; set; }
}
class GroupItemSelectionPivotEventArgs : RoutedEventArgs {
public GroupItemSelectionPivotEventArgs(RoutedEvent routed_event, object source, CollectionViewGroup group_item)
: base(routed_event, source) => this.group_item = group_item;
public CollectionViewGroup group_item { get; }
}
delegate void DIYGridSortingEventHandler(object sender, DIYGridSortingEventArgs e);
delegate void GroupItemSelectionPivotEventHandler(object sender, GroupItemSelectionPivotEventArgs e);
static class DIYGridSortHelper {
/// Important: All columns MUST IMPLEMENT <see cref="DIYGrid.IKeyedColumn{TColKey}"/>.
@@ -123,13 +138,17 @@ namespace mbts.ui.controls {
}
class DIYGridRowsPresenter : ListBox {
readonly LoggerContext _log = new LoggerContext(Log.Ui, $"{nameof(DIYGridRowsPresenter)} ", null);
double _row_height = DIYGrid.DefaultRowHeight;
ScrollContentPresenter _content_presenter_backing;
DIYGrid _grid;
DragRectAdorner _drag_rect_adorner;
ScrollViewer _scroll_viewer;
Point? _drag_start;
double _drag_start_vertical_drift;
bool _support_group_items;
int _drag_start_index;
object _drag_start_item;
static DIYGridRowsPresenter() =>
DefaultStyleKeyProperty.OverrideMetadata(typeof(DIYGridRowsPresenter), new FrameworkPropertyMetadata(typeof(DIYGridRowsPresenter)));
@@ -141,6 +160,8 @@ namespace mbts.ui.controls {
_grid = this.find_ancestor_of_type<DIYGrid>();
_row_height = _grid.RowHeight;
_grid.RowHeightChanged += on_row_height_changed;
_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);
@@ -156,8 +177,14 @@ namespace mbts.ui.controls {
}
};
void on_row_height_changed(double v) => _row_height = v;
void on_support_group_items_changed(bool v) => _support_group_items = v;
}
public event GroupItemSelectionPivotEventHandler GroupItemSelectionPivot;
ScrollContentPresenter _content_presenter => _content_presenter_backing ??
(_content_presenter_backing = this.descendants_of_type<ScrollContentPresenter>().FirstOrDefault());
public new void SetSelectedItems(IEnumerable items) {
base.SetSelectedItems(items);
}
@@ -168,22 +195,37 @@ namespace mbts.ui.controls {
protected override void OnPreviewMouseLeftButtonDown(MouseButtonEventArgs e) {
var pt = e.GetPosition(this);
var container = this.ChildAtPoint<ListBoxItem>(pt);
if (container != null) {
var item = ItemContainerGenerator.ItemFromContainer(container);
if (item != null) {
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);
void on_group(GroupItem g) {
if (g.DataContext is CollectionViewGroup gvm && gvm.FlatItems().Select(ItemContainerGenerator.ContainerFromItem).OfType<ListBoxItem>().TryGetFirst(out var lbi)) {
// ListBox calls this internally when a ListBoxItem is clicked;
// it needs to be called explicitly in this case, since the click target is a GroupItem
Mouse.Capture(this, CaptureMode.SubTree);
sb.Add($"{nameof(GroupItem)} '{gvm.Name}' with actual item ");
init_common(lbi);
}
}
void init_common(ListBoxItem item_container) {
_drag_start = pt;
_drag_start_index = Items.IndexOf(item);
_drag_start_item = ItemContainerGenerator.ItemFromContainer(item_container);
_drag_start_index = ItemContainerGenerator.IndexFromContainer(item_container);
_drag_start_vertical_drift = _scroll_viewer.VerticalOffset;
sb.Add($"'{_drag_start_item}' (selection mode {SelectionMode}).");
_log.info(sb.ToString());
}
}
base.OnPreviewMouseLeftButtonDown(e);
}
protected override void OnPreviewMouseUp(MouseButtonEventArgs e) {
if (SelectionMode != SelectionMode.Single) {
if (_drag_start.HasValue) {
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;
int start_index, n_items;
if (vertical_diff > 0) {
@@ -203,14 +245,65 @@ namespace mbts.ui.controls {
}
}
if (n_items > 1) {
var selection = Enumerable.Range(start_index, n_items).Select(i => Items[i]).ToList();
SetSelectedItems(selection);
if (_support_group_items) {
if (this.ChildAtPoint(drag_stop, out ListBoxItem drag_stop_container)) {
_select_items_between(_drag_start_item, ItemContainerGenerator.ItemFromContainer(drag_stop_container));
} else if (this.ChildAtPoint(drag_stop, out GroupItem gi) && gi.DataContext is CollectionViewGroup cvg && cvg.ItemCount > 0) {
if ((ItemContainerGenerator.ContainerFromItem(_drag_start_item)?.VisualAncestors<GroupItem>()).empty_if_null().FirstOrDefault() == gi) {
_select_items_between(_drag_start_item, cvg.Items.FirstOrDefault());
} else {
var gisp_args = new GroupItemSelectionPivotEventArgs(PreviewMouseUpEvent, this, cvg);
GroupItemSelectionPivot?.Invoke(this, gisp_args);
if (gisp_args.Handled) {
// don't select the inner group items
if (vertical_diff < 0) {
_select_items_between(_drag_start_item, cvg.FlatItems().FirstOrDefault());
} else {
var end = cvg.FlatItems().FirstOrDefault();
var selection = Items.Cast<object>()
.Between(_drag_start_item, cvg.FlatItems().FirstOrDefault())
.Prepend(_drag_start_item);
_log.info($"selecting item range (inclusive begin '{_drag_start_item}' to exclusive end {end}).");
SetSelectedItems(selection);
}
} else {
/// Execute default behavior (select all the inner items in the GroupItem)
/// The way <see cref="_select_items_between(object, object)"/> works, is two items play the role of 'bookends' - every item in between them is selected.
/// Since the drag_start item is the first bookend, depending on the drag direction, either the first, or last item in the group will be the other bookend.
_select_items_between(_drag_start_item, vertical_diff > 0 ? cvg.Items.Last() : cvg.Items.First());
}
}
}
} else {
var selection = Enumerable.Range(start_index, n_items).Select(i => Items[i]).ToList();
Log.assert(selection.Count > 0);
_log.info($"selecting item range (size {selection.Count}) determined by drag distance '{vertical_diff}' x row height '{_row_height}' (inclusive begin '{selection[0]}' to inclusive end '{selection[selection.Count - 1]}').");
SetSelectedItems(selection);
}
}
_drag_start = null;
}
_drag_start = null;
_drag_start_item = null;
}
base.OnPreviewMouseUp(e);
}
void _select_items_between(object inclusive_begin, object inclusive_end) {
if (inclusive_begin == null || inclusive_end == null) {
// One could argue that an exception should be thrown here, as null is a bogus value for either parameter.
// However, clearing the selection seems reasonable enough, and does not risk crashing (assuming nothing particularly nefarious is happening).
_log.warn($"clearing the selection because the specified item range cannot be determined (inclusive begin '{inclusive_begin}' to inclusive end '{inclusive_end}').");
UnselectAll();
} else if (inclusive_begin == inclusive_end) {
_log.info($"selecting single item '{inclusive_begin}.'");
SetSelectedItems(new[] { inclusive_begin });
} else {
_log.info($"selecting item range (inclusive begin '{inclusive_begin}' to inclusive end '{inclusive_end}').");
SetSelectedItems(Items.Cast<object>()
.Between(inclusive_begin, inclusive_end)
.BookEnd(inclusive_begin, inclusive_end));
}
}
}
partial class DIYGrid : Control {
@@ -225,6 +318,7 @@ namespace mbts.ui.controls {
DefaultCellPadding = new Thickness(4, 2, 4, 2),
AnimatableCellPadding = new Thickness(8, 2, 4, 2);
public static readonly Visibility DefaultRowsHorizontalScrollBarVisibility = Visibility.Visible;
bool _support_group_items;
#region RoutedCommands
public static readonly RoutedCommand SortCommand = new RoutedCommand(), DefaultRowCommand = new RoutedCommand();
@@ -337,6 +431,7 @@ namespace mbts.ui.controls {
}
public event DIYGridSortingEventHandler Sorting;
public event Action<bool> SupportGroupItemsChanged;
public double InactiveSelectionOpacity {
get => _inactive_selection_opacity;
@@ -348,9 +443,21 @@ namespace mbts.ui.controls {
}
}
public bool SupportGroupItems {
get => _support_group_items;
set {
if (_support_group_items != value) {
_support_group_items = value;
SupportGroupItemsChanged?.Invoke(value);
}
}
}
public ItemContainerGenerator RowContainerGenerator =>
_row_presenter.ItemContainerGenerator;
public ObservableCollection<GroupStyle> GroupStyle { get; } = new ObservableCollection<GroupStyle>();
private void OnCopy(object sender, ExecutedRoutedEventArgs e) {
// TODO: implement excel-style copy rows/cells
}
@@ -407,7 +514,7 @@ namespace mbts.ui.controls {
}
#region Column Handling
private void invalidate_column_layout(object sender = null, EventArgs args = null) {
public void invalidate_column_layout(object sender = null, EventArgs args = null) {
var geometry = GetColumnGeometry().ToList();
var clc = ColumnLayoutChanged;
if (clc != null) {
@@ -482,26 +589,70 @@ namespace mbts.ui.controls {
_superheader_column_index = SuperHeaders.SelectMany(sh => Enumerable.Repeat(sh, sh.Width)).ToArray();
}
ObservableCollection<ColumnBase> _visible_columns = new ObservableCollection<ColumnBase>();
IDisposable _visible_columns_filter_hook;
readonly ObservableCollection<ColumnBase> _visible_columns = new ObservableCollection<ColumnBase>();
IDisposable _columns_hooks;
private static void columns_changed(DependencyObject d, DependencyPropertyChangedEventArgs args) {
if (d is DIYGrid grid) {
grid.on_columns_collection_changed(args.OldValue as ObservableCollection<ColumnBase>, args.NewValue as ObservableCollection<ColumnBase>);
grid._reset_columns(args.NewValue as ObservableCollection<ColumnBase>);
}
}
void _reset_columns() =>
_reset_columns(Columns);
// Called when the columns COLLECTION has been replaced entirely
private void on_columns_collection_changed(ObservableCollection<ColumnBase> oldval, ObservableCollection<ColumnBase> newval) {
if (oldval != null) {
Disposable.dispose(ref _visible_columns_filter_hook);
_visible_columns.CollectionChanged -= on_columns_changed;
}
void _reset_columns(ObservableCollection<ColumnBase> newval) {
Disposable.dispose(ref _columns_hooks);
if (newval != null) {
_visible_columns_filter_hook = ObservableFilterCollection.connect_never_duplicate(newval, _visible_columns, c => c.is_visible, nameof(ColumnBase.is_visible));
_visible_columns.CollectionChanged += on_columns_changed;
// When column.is_visible changes, visible_columns will be refreshed asynchronously,
// via Dispatcher.InvokeAsync, to drown out noisy batch changes.
// The vis_cols_sync_queued flag indicates that such a refresh has already been queued.
bool vis_cols_sync_queued = false;
// vis_cols_disposed, when true, indicates that the Columns dp has changed.
// This flag is checked in the Dispatcher.InvokeAsync callback before resetting visible_columns, to avoid overwriting the latest value.
bool vis_cols_disposed = false;
var vis_cols_hook = new DisposableContainer();
reset_visible_columns();
_columns_hooks = Disposable.Create(
Disposable.Create(() => vis_cols_disposed = true),
ObservableAutoWrapper.connect_item_hooks(newval, newval, c => {
c.is_visible_changed += column_is_visible_changed;
return Disposable.Create(() => c.is_visible_changed -= column_is_visible_changed);
}),
vis_cols_hook,
_visible_columns.Subscribe(on_columns_changed));
if (GetCachedColumnParamsContainer(this) is FrameworkElement params_container && CachedColumnParams is IEnumerable<string> ccp) {
var ccp_array = ccp.ToArray();
_columns_hooks = Disposable.Create(
_columns_hooks,
ObservableAutoWrapper.connect_item_hooks(newval, c =>
ObjectSnapshot.property_table((INotifyPropertyChanged)c, ccp_array)
.Subscribe((bts.api.core.TableUpdate<(string name, object current, object original), string> table) => {
foreach (var (name, current, original) in table.added.Concat(table.changed))
params_container.Resources[ColumnResourceHelper.col_prop_resource_id(c.id, name)] = current;
})
));
}
// local methods
void reset_visible_columns() =>
vis_cols_hook.replace(ObservableFilterCollection.connect_never_duplicate(newval, _visible_columns, c => c.is_visible, nameof(ColumnBase.is_visible)));
void column_is_visible_changed(bool is_visible) {
if (!vis_cols_sync_queued) {
vis_cols_sync_queued = true;
Dispatcher.InvokeAsync(() => {
if (!vis_cols_disposed) {
reset_visible_columns();
vis_cols_sync_queued = false;
}
});
}
}
}
on_columns_changed(_visible_columns, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
// Called when items are added or removed from the columns collection
private void on_columns_changed(object sender, NotifyCollectionChangedEventArgs e) {
if (sender is ObservableCollection<ColumnBase> columns) {
@@ -552,6 +703,8 @@ namespace mbts.ui.controls {
void _on_col_font_params_changed() => CoerceValue(RowHeightProperty);
void _try_set_column_filter_props(ColumnBase column) {
if (column.HeaderTemplate == null && !(column.Header is Visual))
column.HeaderTemplate = TryFindResource("DefaultColumnHeaderContentTemplate") as DataTemplate;
if (column is IFilterableColumn fc) {
DataGridFilterProperties.SetFilter(column, fc.filter);
DataGridFilterProperties.SetFilterTemplate(column, FindResource(fc.filter_template) as DataTemplate);
@@ -562,6 +715,11 @@ namespace mbts.ui.controls {
public static readonly DependencyProperty HeaderTotalSizeHackProperty = DependencyProperty.Register("HeaderTotalSizeHack", typeof(double), typeof(DIYGrid), new FrameworkPropertyMetadata(0.0, dp_invalidate_column_layout));
public double HeaderTotalSizeHack {
get => (double)GetValue(HeaderTotalSizeHackProperty);
set => SetValue(HeaderTotalSizeHackProperty, value);
}
private static void dp_invalidate_column_layout(DependencyObject d, DependencyPropertyChangedEventArgs unused) {
var grid = d as DIYGrid;
if (grid != null) {
@@ -594,6 +752,125 @@ namespace mbts.ui.controls {
// Public Properties
#region Public Properties
#region ColumnKey
public static readonly DependencyProperty ColumnKeyProperty =
DependencyProperty.RegisterAttached("ColumnKey",
typeof(object),
typeof(DIYGrid),
new PropertyMetadata((d, e) => {
if (!(d is UIElement child))
return;
if (VisualTreeHelper.GetParent(child) is Grid panel) {
if (GetSyncWithColumnGeometrySource(d) is DIYGrid diy_grid || panel.try_find_ancestor_of_type(out diy_grid))
ColumnGeometrySyncHelper.reset_panel_child(child, diy_grid, e.NewValue);
}
if (d is FrameworkElement child_el) {
var curr_params = GetObservedCachedColumnParams(d);
if (curr_params != null) {
// clear observed params with old key, then reset with new key
ColumnResourceHelper.reset_cell_resource_references(child_el, e.OldValue, curr_params, null);
ColumnResourceHelper.reset_cell_resource_references(child_el, e.NewValue, null, curr_params);
}
}
}));
public static object GetColumnKey(DependencyObject d) => d.GetValue(ColumnKeyProperty);
public static void SetColumnKey(DependencyObject d, object value) => d.SetValue(ColumnKeyProperty, value);
#endregion
#region SyncWithColumnGeometrySource
public static readonly DependencyProperty SyncWithColumnGeometrySourceProperty =
DependencyProperty.RegisterAttached(
"SyncWithColumnGeometrySource",
typeof(DIYGrid),
typeof(DIYGrid),
new PropertyMetadata(ColumnGeometrySyncHelper.reset_sync));
public static DIYGrid GetSyncWithColumnGeometrySource(DependencyObject d) => d.GetValue(SyncWithColumnGeometrySourceProperty) as DIYGrid;
public static void SetSyncWithColumnGeometrySource(DependencyObject d, DIYGrid value) => d.SetValue(SyncWithColumnGeometrySourceProperty, value);
#endregion
#region SyncWithColumnGeometry
public static readonly DependencyProperty SyncWithColumnGeometryProperty =
DependencyProperty.RegisterAttached(
"SyncWithColumnGeometry",
typeof(bool),
typeof(DIYGrid),
new PropertyMetadata(false, ColumnGeometrySyncHelper.reset_sync));
public static bool GetSyncWithColumnGeometry(DependencyObject d) => (bool)d.GetValue(SyncWithColumnGeometryProperty);
public static void SetSyncWithColumnGeometry(DependencyObject d, bool value) => d.SetValue(SyncWithColumnGeometryProperty, value);
static readonly DependencyPropertyKey SyncWithColumnGeometryHookPropertyKey =
DependencyProperty.RegisterAttachedReadOnly(
"SyncWithColumnGeometryHook",
typeof(IDisposable),
typeof(DIYGrid),
new PropertyMetadata(FrameworkElementLifeDependencyHelper.DisposeOld));
internal static IDisposable GetSyncWithColumnGeometryHook(DependencyObject d) =>
d.GetValue(SyncWithColumnGeometryHookPropertyKey.DependencyProperty) as IDisposable;
static void SetSyncWithColumnGeometryHook(DependencyObject d, IDisposable value) =>
d.SetValue(SyncWithColumnGeometryHookPropertyKey, value);
#endregion
#region CachedColumnParamsContainer
// This is the element who's ResourceDictionary will be used to store the observed column params.
// By allowing callers to specify this element (as opposed to simply putting them in the DIYGrid's resources),
// elements that are not visual children of the DIYGrid can bind to these params as well.
// Once assigned, this value should not change during the lifetime of the DIYGrid.
// This could create conflicting references among the binding targets,
// as resources added to the old CachedColumnParamsContainer would not be removed.
public static readonly DependencyProperty CachedColumnParamsContainerProperty =
DependencyProperty.RegisterAttached(
"CachedColumnParamsContainer",
typeof(FrameworkElement),
typeof(DIYGrid),
new PropertyMetadata((d, e) => (d as DIYGrid)?._reset_columns()));
public static FrameworkElement GetCachedColumnParamsContainer(DependencyObject d) => d.GetValue(CachedColumnParamsContainerProperty) as FrameworkElement;
public static void SetCachedColumnParamsContainer(DependencyObject d, ResourceDictionary value) => d.SetValue(CachedColumnParamsContainerProperty, value);
#endregion
#region CachedColumnParams
public static readonly DependencyProperty CachedColumnParamsProperty =
WPFHelper.create_dp<IEnumerable<string>, DIYGrid>(
nameof(CachedColumnParams),
(o, v) => {
o?._reset_columns();
});
public IEnumerable<string> CachedColumnParams {
get => GetValue(CachedColumnParamsProperty) as IEnumerable<string>;
set => SetValue(CachedColumnParamsProperty, value);
}
#endregion
#region ObservedCachedColumnParams
public static readonly DependencyProperty ObservedCachedColumnParamsProperty =
DependencyProperty.RegisterAttached(
"ObservedCachedColumnParams",
typeof(IEnumerable<CachedColumnParam>),
typeof(DIYGrid),
new PropertyMetadata(ColumnResourceHelper.ObservedCachedParamsChanged));
public static IEnumerable<CachedColumnParam> GetObservedCachedColumnParams(DependencyObject d) => d.GetValue(ObservedCachedColumnParamsProperty) as IEnumerable<CachedColumnParam>;
public static void SetObservedCachedColumnParams(DependencyObject d, IEnumerable<CachedColumnParam> value) => d.SetValue(ObservedCachedColumnParamsProperty, value);
#endregion
#region ColumnSettings
public static readonly DependencyProperty ColumnSettingsProperty =
DependencyProperty.RegisterAttached("ColumnSettings", typeof(settings.GridColumnSettings), typeof(DIYGrid),
new FrameworkPropertyMetadata((d, e) => {
if (d is ColumnBase c && e.NewValue is settings.GridColumnSettings s)
c.load_from(s);
}));
public static settings.GridColumnSettings GetColumnSettings(DependencyObject d) => d.GetValue(ColumnSettingsProperty) as settings.GridColumnSettings;
public static void SetColumnSettings(DependencyObject d, settings.GridColumnSettings value) => d.SetValue(ColumnSettingsProperty, value);
#endregion
public static readonly DependencyProperty BackgroundOverlayProperty =
DependencyProperty.RegisterAttached(
"BackgroundOverlay",
@@ -783,7 +1060,7 @@ namespace mbts.ui.controls {
/// Compute the geometry of all visible (or partially visible columns) columns
/// This effectively replaces the entire Measure/Arrange pass of the grid rows (and enables column virtualization)
/// Mouse events will be hit tested against this as well
internal IEnumerable<ColumnGeometry> GetColumnGeometry() {
internal IEnumerable<ColumnGeometry> GetColumnGeometry(bool include_obscured_cols = false) {
if (_header_scroll_viewer != null && ActualWidth > 0) {
double frozen_offset = 0;
int vis_frozen_col_count = _get_visible_frozen_column_count();
@@ -805,7 +1082,7 @@ namespace mbts.ui.controls {
double left = Math.Max(offset, frozen_offset);
double right = offset + col.ActualWidth;
// If right < frozen_offset column is completely obscured
if (right > frozen_offset && left <= ActualWidth) {
if (include_obscured_cols || (right > frozen_offset && left <= ActualWidth)) {
// fully visible
yield return new ColumnGeometry() {
column = col,
@@ -826,6 +1103,7 @@ namespace mbts.ui.controls {
#endregion
IDisposable _group_style_row_presenter_link;
// Overrides
#region Overrides
public override void OnApplyTemplate() {
@@ -838,6 +1116,8 @@ namespace mbts.ui.controls {
_scroll_horizontal = (ScrollBar)GetTemplateChild("_scroll_horiz");
_row_presenter = (ListBox)GetTemplateChild("_row_presenter");
Log.assert_d(!_row_presenter.IsLoaded);
// (re)wrap group style
Disposable.replace(ref _group_style_row_presenter_link, ObservableAutoWrapper.connect_two_way(GroupStyle, _row_presenter.GroupStyle));
_try_set_row_style();
_header_scroll_viewer = (ScrollViewer)GetTemplateChild("_header_scroll_viewer");
Log.assert_d(!_header_scroll_viewer.IsLoaded);
@@ -883,6 +1163,123 @@ namespace mbts.ui.controls {
}
#endregion
static class ColumnGeometrySyncHelper {
public static void reset_panel_child(UIElement child, DIYGrid diy_grid) =>
reset_panel_child(child, diy_grid, GetColumnKey(child));
public static void reset_panel_child(UIElement child, DIYGrid diy_grid, object col_key) {
if (col_key == null)
return;
var key_str = col_key.ToString();
if (diy_grid._visible_columns.TryGetFirst(c => string.Equals(c.id, key_str, StringComparison.CurrentCultureIgnoreCase), out var target_col, out int target_pos)) {
// set clip to null in case this child was hidden before (see 'else' block).
child.Clip = null;
Grid.SetColumn(child, target_pos);
} else {
// hide element by setting clip to 0x0 rect, rather than visibility to collapsed,
// to avoid incidentally breaking visibility bindings, triggers, animations, etc.
child.Clip = new RectangleGeometry(new Rect(0, 0, 0, 0));
}
}
public static void reset_sync(DependencyObject d, DependencyPropertyChangedEventArgs e) {
if (!GetSyncWithColumnGeometry(d))
SetSyncWithColumnGeometryHook(d, null);
if (!(d is Grid g))
return;
var hook = new DisposableContainer();
if (g.IsLoaded)
hook.replace(impl());
SetSyncWithColumnGeometryHook(d, Disposable.Create(g.SubscribeLoaded(on_loaded), hook));
//
void on_loaded(object sender, RoutedEventArgs loaded_e) {
if (GetSyncWithColumnGeometry(d))
hook.replace(impl());
}
IDisposable impl() {
// Avoid resetting children on every collection change, as that could be very noisy and hurt perf.
// In the visible columns CollectionChanged handler, call reset_children inside a Dispatcher.InvokeAsync request
// to effectively wait for potential batch-changes to finish.
bool resetting_children = false;
if (!(GetSyncWithColumnGeometrySource(d) is DIYGrid diy_grid) && !g.try_find_ancestor_of_type(out diy_grid))
return null;
g.SetBinding(Grid.WidthProperty, new Binding(nameof(HeaderTotalSizeHack)) { Source = diy_grid });
reset_children();
on_layout_changed(diy_grid.GetColumnGeometry(true).ToList());
diy_grid.ColumnLayoutChanged += on_layout_changed;
diy_grid._visible_columns.CollectionChanged += on_vis_columns_changed;
return Disposable.Create(() => {
diy_grid.ColumnLayoutChanged -= on_layout_changed;
diy_grid._visible_columns.CollectionChanged -= on_vis_columns_changed;
});
//
void on_vis_columns_changed(object sender, NotifyCollectionChangedEventArgs vce) {
if (!resetting_children) {
// avoid resetting children on every collection change, as that could be very noisy and hurt perf.
resetting_children = true;
diy_grid.Dispatcher.InvokeAsync(() => {
on_layout_changed(diy_grid.GetColumnGeometry().ToList());
reset_children();
resetting_children = false;
});
}
}
void on_layout_changed(List<ColumnGeometry> layout) {
// TODO: figure out how to make the ColumnLayoutChanged event lazy, or somehow avoid making 2 lists of columns
// (the layout param does not include obscured cols, so we have to create a new list of ColumnGeoemtry to satisfy our needs).
layout = (from col in diy_grid._visible_columns
join pos in diy_grid.GetColumnGeometry(true)
on col.id equals pos.column.id
select pos).ToList();
int n_removed = g.ColumnDefinitions.Count - layout.Count;
if (n_removed > 0)
g.ColumnDefinitions.RemoveRange(g.ColumnDefinitions.Count - n_removed, n_removed);
int i = 0;
for (; i < g.ColumnDefinitions.Count; i++) {
var col_pos = layout[i];
var col = g.ColumnDefinitions[i];
if (col.Width.GridUnitType == GridUnitType.Pixel && col.Width.Value == col_pos.visible_width)
continue;
col.Width = new GridLength(col_pos.visible_width, GridUnitType.Pixel);
}
for (; i < layout.Count; i++) {
g.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(layout[i].visible_width, GridUnitType.Pixel) });
}
}
void reset_children() {
foreach (UIElement child in g.Children)
reset_panel_child(child, diy_grid);
}
}
}
}
static class ColumnResourceHelper {
public static string col_resource_id(object col_id) =>
$"Columns_{col_id}";
public static string col_prop_resource_id(object col_id, string prop) =>
$"{col_resource_id(col_id)}_{prop}";
public static void ObservedCachedParamsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) =>
reset_cell_resource_references(d as FrameworkElement, e.OldValue as IEnumerable<CachedColumnParam>, e.NewValue as IEnumerable<CachedColumnParam>);
public static void reset_cell_resource_references(FrameworkElement target, IEnumerable<CachedColumnParam> old_params, IEnumerable<CachedColumnParam> new_params) =>
reset_cell_resource_references(target, GetColumnKey(target), old_params, new_params);
public static void reset_cell_resource_references(FrameworkElement target, object col_id, IEnumerable<CachedColumnParam> old_params, IEnumerable<CachedColumnParam> new_params) {
if (target == null || col_id == null)
return;
foreach (var p in old_params.empty_if_null()) {
target.ClearValue(p.TargetProperty);
}
foreach (var p in new_params.empty_if_null()) {
target.SetResourceReference(p.TargetProperty, col_prop_resource_id(col_id, p.SourcePropertyName));
}
}
}
class DataContextHooks : IDisposable {
List<IDisposable> _hooks = new List<IDisposable>(4);
@@ -1,9 +1,9 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mbts="clr-namespace:mbts.ui"
xmlns:ui="clr-namespace:mbts.ui"
xmlns:bts="clr-namespace:mbts.ui.controls"
xmlns:u="clr-namespace:mbts.ui"
xmlns:c="clr-namespace:mbts.ui.controls"
xmlns:cv="clr-namespace:mbts.ui.converters"
>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="/view/Controls/Popup/PopupControl.xaml"/>
@@ -35,14 +35,14 @@
<Condition Property="Selector.IsSelectionActive" Value="False"/>
<Condition Property="IsSelected" Value="True"/>
</MultiTrigger.Conditions>
<Setter Property="Background" TargetName="Bd" Value="{DynamicResource {x:Static bts:DIYGrid.InactiveSelectionHighlightKey}}"/>
<Setter Property="Background" TargetName="Bd" Value="{DynamicResource {x:Static c:DIYGrid.InactiveSelectionHighlightKey}}"/>
</MultiTrigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="Selector.IsSelectionActive" Value="True"/>
<Condition Property="IsSelected" Value="True"/>
</MultiTrigger.Conditions>
<Setter Property="Background" TargetName="Bd" Value="{Binding SelectionHighlight, RelativeSource={RelativeSource AncestorType=bts:DIYGrid}}"/>
<Setter Property="Background" TargetName="Bd" Value="{Binding SelectionHighlight, RelativeSource={RelativeSource AncestorType=c:DIYGrid}}"/>
</MultiTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
@@ -50,17 +50,22 @@
</Setter>
<Style.Triggers>
<Trigger Property="ItemsControl.AlternationIndex" Value="0">
<Setter Property="Background" Value="{ui:Brush {mbts:BTSColor Grid.input_cell_background}}"/>
<Setter Property="Background" Value="{u:Brush {u:BTSColor Grid.input_cell_background}}"/>
</Trigger>
<Trigger Property="ItemsControl.AlternationIndex" Value="1">
<Setter Property="Background" Value="{ui:Brush {mbts:BTSColor Grid.alternate_row_background}}"/>
<Setter Property="Background" Value="{u:Brush {u:BTSColor Grid.alternate_row_background}}"/>
</Trigger>
</Style.Triggers>
</Style>
<Style TargetType="bts:DIYGridRowsPresenter" BasedOn="{StaticResource {x:Type ListBox}}">
<Style TargetType="c:DIYGridRowsPresenter" BasedOn="{StaticResource {x:Type ListBox}}">
<Setter Property="IsTextSearchEnabled" Value="False"/>
</Style>
<Style TargetType="bts:DIYGrid">
<DataTemplate x:Key="DefaultColumnHeaderContentTemplate">
<Grid>
<TextBlock HorizontalAlignment="Center" VerticalAlignment="Center" Text="{Binding}" TextWrapping="WrapWithOverflow" TextAlignment="Center"/>
</Grid>
</DataTemplate>
<Style TargetType="c:DIYGrid">
<Setter Property="Focusable" Value="False"/>
<Setter Property="SelectionMode" Value="Extended"/>
<Setter Property="ColumnHeaderStyle">
@@ -68,14 +73,15 @@
<Style TargetType="GridViewColumnHeader">
<!-- Background - note that we're settings this property on the containing stackpanel in which the GridViewHeaderRowPresenter resides -->
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Foreground" Value="{mbts:BTSColor header_text}"/>
<Setter Property="BorderBrush" Value="{mbts:BTSColor header}"/>
<Setter Property="Foreground" Value="{u:BTSColor header_text}"/>
<Setter Property="BorderBrush" Value="{u:BTSColor header}"/>
<Setter Property="BorderThickness" Value="0,1,1,0"/>
<Setter Property="Command" Value="{x:Static bts:DIYGrid.SortCommand}"/>
<Setter Property="Command" Value="{x:Static c:DIYGrid.SortCommand}"/>
<Setter Property="CommandParameter" Value="{Binding RelativeSource={RelativeSource Self}}"/>
<Setter Property="CommandTarget" Value="{Binding RelativeSource={RelativeSource AncestorType=bts:DIYGrid}}"/>
<Setter Property="HorizontalContentAlignment" Value="Center"/>
<Setter Property="Height" Value="{x:Static bts:DIYGrid.DefaultColumnHeaderHeight}"/>
<Setter Property="CommandTarget" Value="{Binding RelativeSource={RelativeSource AncestorType=c:DIYGrid}}"/>
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="VerticalContentAlignment" Value="Stretch"/>
<Setter Property="Height" Value="{x:Static c:DIYGrid.DefaultColumnHeaderHeight}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type GridViewColumnHeader}">
@@ -106,7 +112,7 @@
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
/>
<Border x:Name="OperatorsBorder" Grid.Column="1" Background="{mbts:BTSColor Name=header}">
<Border x:Name="OperatorsBorder" Grid.Column="1" Background="{u:BTSColor Name=header}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
@@ -126,7 +132,7 @@
Visibility="Collapsed"
Width="8"
/>
<bts:PopupControl
<c:PopupControl
x:Name="FilterPopup"
Grid.Column="1"
Margin="2,0,0,0"
@@ -138,44 +144,44 @@
ShowExpanderArrow="False"
PopupPlacement="Bottom"
PlacementTarget="{Binding RelativeSource={RelativeSource AncestorType=GridViewColumnHeader}}"
IsOpen="{Binding Path='Column.(bts:DataGridFilterProperties.FilterOpen)', Mode=TwoWay, RelativeSource={RelativeSource AncestorType=GridViewColumnHeader}}"
ContentTemplate="{Binding Path='Column.(bts:DataGridFilterProperties.FilterTemplate)', RelativeSource={RelativeSource AncestorType=GridViewColumnHeader}}"
Content="{Binding Path='Column.(bts:DataGridFilterProperties.Filter)', RelativeSource={RelativeSource AncestorType=GridViewColumnHeader}}"
IsOpen="{Binding Path='Column.(c:DataGridFilterProperties.FilterOpen)', Mode=TwoWay, RelativeSource={RelativeSource AncestorType=GridViewColumnHeader}}"
ContentTemplate="{Binding Path='Column.(c:DataGridFilterProperties.FilterTemplate)', RelativeSource={RelativeSource AncestorType=GridViewColumnHeader}}"
Content="{Binding Path='Column.(c:DataGridFilterProperties.Filter)', RelativeSource={RelativeSource AncestorType=GridViewColumnHeader}}"
>
<bts:PopupControl.InputBindings>
<c:PopupControl.InputBindings>
<KeyBinding
Key="Return"
CommandParameter="{Binding Path='Column.(bts:DataGridFilterProperties.Filter)', RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=GridViewColumnHeader}}"
Command="{x:Static bts:DataGridFilterProperties.CommitCommand}"
CommandParameter="{Binding Path='Column.(c:DataGridFilterProperties.Filter)', RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=GridViewColumnHeader}}"
Command="{x:Static c:DataGridFilterProperties.CommitCommand}"
/>
<KeyBinding Key="Esc" Command="{x:Static bts:PopupControl.CloseCommand}"/>
</bts:PopupControl.InputBindings>
<bts:PopupControl.CommandBindings>
<KeyBinding Key="Esc" Command="{x:Static c:PopupControl.CloseCommand}"/>
</c:PopupControl.InputBindings>
<c:PopupControl.CommandBindings>
<CommandBinding
Command="{x:Static bts:DataGridFilterProperties.CommitCommand}"
Executed="{x:Static bts:DataGridFilterProperties.commit_executed}"
CanExecute="{x:Static bts:DataGridFilterProperties.commit_can_execute}"
Command="{x:Static c:DataGridFilterProperties.CommitCommand}"
Executed="{x:Static c:DataGridFilterProperties.commit_executed}"
CanExecute="{x:Static c:DataGridFilterProperties.commit_can_execute}"
/>
<CommandBinding
Command="{x:Static bts:DataGridFilterProperties.ClearCommand}"
Executed="{x:Static bts:DataGridFilterProperties.clear_executed}"
CanExecute="{x:Static bts:DataGridFilterProperties.clear_can_execute}"
Command="{x:Static c:DataGridFilterProperties.ClearCommand}"
Executed="{x:Static c:DataGridFilterProperties.clear_executed}"
CanExecute="{x:Static c:DataGridFilterProperties.clear_can_execute}"
/>
</bts:PopupControl.CommandBindings>
<bts:PopupControl.Header>
</c:PopupControl.CommandBindings>
<c:PopupControl.Header>
<Grid>
<Image Name="FilterPopupImage" Height="9" Width="9" Margin="3">
<Image.Style>
<Style TargetType="Image">
<Setter Property="Source" Value="{x:Static mbts:Icons.Filter}"/>
<Setter Property="Source" Value="{x:Static u:Icons.Filter}"/>
<Setter Property="Opacity" Value="0.0"/>
</Style>
</Image.Style>
</Image>
</Grid>
</bts:PopupControl.Header>
<bts:PopupControl.Style>
<Style TargetType="bts:PopupControl">
</c:PopupControl.Header>
<c:PopupControl.Style>
<Style TargetType="c:PopupControl">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="BorderBrush" Value="LightGray"/>
@@ -186,8 +192,8 @@
</Trigger>
</Style.Triggers>
</Style>
</bts:PopupControl.Style>
</bts:PopupControl>
</c:PopupControl.Style>
</c:PopupControl>
</Grid>
</Border>
</Grid>
@@ -229,8 +235,8 @@
<Setter Property="Grid.Column" TargetName="SortArrow" Value="0"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Background" TargetName="HeaderBorder" Value="{mbts:BTSColor Name=header_alternate}"/>
<Setter Property="Background" TargetName="OperatorsBorder" Value="{mbts:BTSColor Name=header_alternate}"/>
<Setter Property="Background" TargetName="HeaderBorder" Value="{u:BTSColor Name=header_alternate}"/>
<Setter Property="Background" TargetName="OperatorsBorder" Value="{u:BTSColor Name=header_alternate}"/>
<Setter Property="Visibility" TargetName="PART_HeaderGripper" Value="Hidden"/>
<Setter Property="Margin" TargetName="HeaderContent" Value="1,1,0,0"/>
</Trigger>
@@ -238,21 +244,21 @@
<Setter Property="MinHeight" Value="20"/>
</Trigger>
<!--Filter Active-->
<DataTrigger Binding="{Binding Path='Column.(bts:DataGridFilterProperties.FilterActive)', RelativeSource={RelativeSource Self}}" Value="True">
<Setter TargetName="HeaderContent" Property="Panel.ZIndex" Value="0"/>
<Setter TargetName="FilterPopupImage" Property="Source" Value="{x:Static mbts:Icons.FilterActive}"/>
<DataTrigger Binding="{Binding Path='Column.(c:DataGridFilterProperties.FilterActive)', RelativeSource={RelativeSource Self}}" Value="True">
<Setter TargetName="HeaderContent" Property="Margin" Value="0,0,11,0"/>
<Setter TargetName="FilterPopupImage" Property="Source" Value="{x:Static u:Icons.FilterActive}"/>
<Setter TargetName="FilterPopupImage" Property="Opacity" Value="1.0"/>
<Setter Property="Grid.Column" TargetName="SortArrow" Value="0"/>
</DataTrigger>
<DataTrigger Binding="{Binding Path='Column.(bts:DataGridFilterProperties.FilterActive)', RelativeSource={RelativeSource Self}}" Value="{x:Null}">
<Setter TargetName="HeaderContent" Property="Panel.ZIndex" Value="0"/>
<Setter TargetName="FilterPopupImage" Property="Source" Value="{x:Static mbts:Icons.FilterError}"/>
<DataTrigger Binding="{Binding Path='Column.(c:DataGridFilterProperties.FilterActive)', RelativeSource={RelativeSource Self}}" Value="{x:Null}">
<Setter TargetName="HeaderContent" Property="Margin" Value="0,0,11,0"/>
<Setter TargetName="FilterPopupImage" Property="Source" Value="{x:Static u:Icons.FilterError}"/>
<Setter TargetName="FilterPopupImage" Property="Opacity" Value="1.0"/>
<Setter Property="Grid.Column" TargetName="SortArrow" Value="0"/>
</DataTrigger>
<!--Sort Active-->
<DataTrigger Binding="{Binding Column.SortDirection, RelativeSource={RelativeSource Self}}" Value="Ascending">
<Setter TargetName="HeaderContent" Property="Panel.ZIndex" Value="0"/>
<Setter TargetName="HeaderContent" Property="Margin" Value="0,0,11,0"/>
<Setter TargetName="SortArrow" Property="Visibility" Value="Visible"/>
<Setter TargetName="SortArrow" Property="RenderTransform">
<Setter.Value>
@@ -261,7 +267,7 @@
</Setter>
</DataTrigger>
<DataTrigger Binding="{Binding Column.SortDirection, RelativeSource={RelativeSource Self}}" Value="Descending">
<Setter TargetName="HeaderContent" Property="Panel.ZIndex" Value="0"/>
<Setter TargetName="HeaderContent" Property="Margin" Value="0,0,11,0"/>
<Setter TargetName="SortArrow" Property="Visibility" Value="Visible"/>
</DataTrigger>
</ControlTemplate.Triggers>
@@ -275,10 +281,10 @@
<Setter.Value>
<Style TargetType="{x:Type ToggleButton}">
<Setter Property="Focusable" Value="False"/>
<Setter Property="Background" Value="{mbts:BTSColor Name=header}"/>
<Setter Property="Foreground" Value="{mbts:BTSColor Name=header_text}"/>
<Setter Property="Background" Value="{u:BTSColor Name=header}"/>
<Setter Property="Foreground" Value="{u:BTSColor Name=header_text}"/>
<Setter Property="HorizontalContentAlignment" Value="Center"/>
<Setter Property="BorderBrush" Value="{ui:Brush Color={ui:Color Name=Grid.grid_vertical_group_separator}}"/>
<Setter Property="BorderBrush" Value="{u:Brush Color={u:Color Name=Grid.grid_vertical_group_separator}}"/>
<Setter Property="BorderThickness" Value="0,0,2,0"/>
<Setter Property="Margin" Value="0,0,0,-1"/>
<Setter Property="FontWeight" Value="Bold"/>
@@ -303,8 +309,32 @@
</Setter>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type bts:DIYGrid}">
<ControlTemplate TargetType="{x:Type c:DIYGrid}">
<Grid>
<u:StateManager.VisualState>
<Binding Path="FrozenColumnCount" RelativeSource="{RelativeSource TemplatedParent}">
<Binding.Converter>
<cv:ValueMap Default="Active">
<cv:KeyValue Key="{u:Int 0}" Value="Inactive"/>
</cv:ValueMap>
</Binding.Converter>
</Binding>
</u:StateManager.VisualState>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup Name="FrozenColumnHeaderStates">
<VisualState Name="Active"/>
<VisualState Name="Inactive">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" Storyboard.TargetName="_header_frozen">
<DiscreteObjectKeyFrame KeyTime="0" Value="{x:Static Visibility.Collapsed}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" Storyboard.TargetName="_super_header_frozen">
<DiscreteObjectKeyFrame KeyTime="0" Value="{x:Static Visibility.Collapsed}"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Grid.ColumnDefinitions>
<!-- Frozen -->
<ColumnDefinition Width="Auto"/>
@@ -317,9 +347,9 @@
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- fill the space above the frozen columns -->
<Rectangle Fill="{mbts:BTSColor Name=header}" Height="{Binding Path=ActualHeight, ElementName=_super_headers}" HorizontalAlignment="Stretch" Margin="0,0,-1,-1"/>
<Rectangle x:Name="_super_header_frozen" Fill="{u:BTSColor Name=header}" Height="{Binding Path=ActualHeight, ElementName=_super_headers}" HorizontalAlignment="Stretch" Margin="0,0,-1,-1"/>
<!-- colums set in code behind -->
<GridViewHeaderRowPresenter x:Name="_header_frozen" Grid.Row="1" Grid.Column="0" AllowsColumnReorder="False" VerticalAlignment="Bottom" ColumnHeaderContainerStyle="{TemplateBinding ColumnHeaderStyle}" UseLayoutRounding="True"/>
<GridViewHeaderRowPresenter x:Name="_header_frozen" Grid.Row="1" Grid.Column="0" AllowsColumnReorder="False" VerticalAlignment="Bottom" ColumnHeaderContainerStyle="{TemplateBinding ColumnHeaderStyle}"/>
<ScrollViewer
x:Name="_header_scroll_viewer"
Grid.Column="1"
@@ -331,18 +361,21 @@
>
<!-- setting the bg color for this stackpanel is a work-around for the vertical white lines that appear on high-dpi displays between some columns -->
<!-- in reality, there is some high dpi rounding thing going on, but that's a battle for another day -->
<StackPanel Background="{mbts:BTSColor Name=header}">
<StackPanel Background="{u:BTSColor Name=header}">
<!-- We don't need super headers over the fixed columns -->
<StackPanel Orientation="Horizontal" x:Name="_super_headers"/>
<GridViewHeaderRowPresenter x:Name="_header_scroll" AllowsColumnReorder="False" ColumnHeaderContainerStyle="{TemplateBinding ColumnHeaderStyle}" UseLayoutRounding="True"/>
</StackPanel>
</ScrollViewer>
<bts:DIYGridRowsPresenter
<c:DIYGridRowsPresenter
x:Name="_row_presenter"
Grid.Row="2"
Grid.ColumnSpan="2"
ScrollViewer.HorizontalScrollBarVisibility="Hidden"
VirtualizingPanel.IsVirtualizingWhenGrouping="True"
VirtualizingPanel.VirtualizationMode="Recycling"
ScrollViewer.CanContentScroll="True"
VirtualizingPanel.ScrollUnit="{TemplateBinding VirtualizingPanel.ScrollUnit}"
AlternationCount="2"
Background="{TemplateBinding Background}"
BorderThickness="0"
@@ -351,21 +384,21 @@
SelectedItem="{Binding CurrentRow, RelativeSource={RelativeSource TemplatedParent}}"
SelectionMode="{TemplateBinding SelectionMode}"
>
<bts:DIYGridRowsPresenter.Resources>
<c:DIYGridRowsPresenter.Resources>
<Style TargetType="ListBoxItem" BasedOn="{StaticResource DIYGridRowStyle}"/>
</bts:DIYGridRowsPresenter.Resources>
<bts:DIYGridRowsPresenter.Template>
<ControlTemplate TargetType="{x:Type bts:DIYGridRowsPresenter}">
</c:DIYGridRowsPresenter.Resources>
<c:DIYGridRowsPresenter.Template>
<ControlTemplate TargetType="{x:Type c:DIYGridRowsPresenter}">
<ScrollViewer Focusable="False">
<ItemsPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
</ScrollViewer>
</ControlTemplate>
</bts:DIYGridRowsPresenter.Template>
</bts:DIYGridRowsPresenter>
</c:DIYGridRowsPresenter.Template>
</c:DIYGridRowsPresenter>
<ScrollBar Grid.Row="3" Grid.Column="1" Orientation="Horizontal" LargeChange="10" x:Name="_scroll_horiz" Visibility="{TemplateBinding RowsHorizontalScrollBarVisibility}"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
</ResourceDictionary>
@@ -1,4 +1,5 @@
using System;
using System.Linq;
namespace mbts.ui.controls
{
@@ -7,6 +8,34 @@ namespace mbts.ui.controls
/* smallest such that 1.0+DBL_EPSILON != 1.0 */
private const double EPSILON = 2.2204460492503131e-016;
private const double BIG_EPSILON = EPSILON * 10;
public static double nan_to_zero(double value) =>
double.IsNaN(value) ? 0.0 : value;
/// <summary>
/// Returns <see cref="double.NaN"/> if both <paramref name="v1"/> and <paramref name="v2"/> are nan, otherwise returns the sum of the non-nan values (if only one value is nan, the other value is returned).
/// </summary>
/// <param name="v1">One of the two values to add.</param>
/// <param name="v2">One of the two values to add.</param>
/// <returns>The sum of the two values, with special handling for nans.</returns>
public static double nan_to_zero_sum(double v1, double v2) =>
double.IsNaN(v1) ?
(double.IsNaN(v2) ? double.NaN : v2) :
(double.IsNaN(v2) ? v1 : v1 + v2);
public static double nan_to_zero_sum(params double[] values) =>
values.Aggregate(0.0, (sum, d) => double.IsNaN(d) ? sum : sum + d);
/// <summary>
/// Returns <see cref="double.NaN"/> if both <paramref name="left"/> and <paramref name="right"/> are nan. Otherwise, if only one is nan, it is substituted for zero, and the difference of the resultant values is returned.
/// </summary>
/// <param name="left">The left operand (minuend) in the subtraction op.</param>
/// <param name="right">The right operand (subtrahend) to be used in the subtraction op.</param>
/// <returns>The difference of the two values, with special handling for nans.</returns>
public static double nan_to_zero_diff(double left, double right) =>
double.IsNaN(left) ?
(double.IsNaN(right) ? double.NaN : -right) :
(double.IsNaN(right) ? left : left - right);
internal static bool AreClose(double v1, double v2) {
if (v1 == v2) return true;
double delta = Math.Abs(v1 - v2);
@@ -60,6 +60,9 @@ namespace mbts.ui.settings {
[DefaultValue(_default_horizontal_offset)]
readonly double _horizontal_offset;
[ProtoMember(10)]
readonly NullableColorProto _background_override;
public GridColumnSettings() {
_is_visible = _default_is_visible;
_is_collapsed = _default_is_collapsed;
@@ -69,7 +72,7 @@ namespace mbts.ui.settings {
}
public GridColumnSettings(string id, string display_name = null, bool is_visible = _default_is_visible, bool is_collapsed = _default_is_collapsed, FontWeight? font_weight_override = null, Color? font_color_override = null,
double width = _default_width, HorizontalAlignment horizontal_alignment = _default_horizontal_alignment, double horizontal_offset = _default_horizontal_offset) {
double width = _default_width, HorizontalAlignment horizontal_alignment = _default_horizontal_alignment, double horizontal_offset = _default_horizontal_offset, Color? background_override = null) {
_id = id;
_display_name = display_name;
_is_visible = is_visible;
@@ -79,6 +82,7 @@ namespace mbts.ui.settings {
_width = width;
_horizontal_alignment = horizontal_alignment;
_horizontal_offset = horizontal_offset;
_background_override = new NullableColorProto(background_override);
}
public string id => _id;
@@ -86,6 +90,7 @@ namespace mbts.ui.settings {
public bool is_collapsed => _is_collapsed;
public FontWeight? font_weight_override => _font_weight_override.value;
public Color? font_color_override => _font_color_override.value;
public Color? background_override => _background_override.argb;
public double width => _width;
public HorizontalAlignment horizontal_alignment => _horizontal_alignment;
public double horizontal_offset => _horizontal_offset;
@@ -93,7 +98,7 @@ namespace mbts.ui.settings {
public GridColumnSettings with(
string display_name = null, bool? is_visible = null, bool? is_collapsed = null, NullableFontWeightProto? font_weight_override = null, NullableColorProto? font_color_override = null,
double? width = null, HorizontalAlignment? horizontal_alignment = null, double? horizontal_offset = null) =>
double? width = null, HorizontalAlignment? horizontal_alignment = null, double? horizontal_offset = null, NullableColorProto? background_override = null) =>
//
new GridColumnSettings(_id,
display_name ?? this.display_name,
@@ -103,7 +108,8 @@ namespace mbts.ui.settings {
(font_color_override ?? _font_color_override).value,
width ?? this.width,
horizontal_alignment ?? this.horizontal_alignment,
horizontal_offset ?? this.horizontal_offset);
horizontal_offset ?? this.horizontal_offset,
(background_override ?? _background_override).argb);
public bool Equals(GridColumnSettings other) =>
other != null &&
@@ -138,9 +144,5 @@ namespace mbts.ui.settings {
public override string ToString() {
return $"id: {id} display_name: {display_name}";
}
}
}
@@ -63,6 +63,11 @@ namespace mbts.spark {
_dispose = Disposable.Create(() => predicate_animations.OfType<IDisposable>().DisposeAll());
}
// TODO: hook up/impl
// - currently, this is only used in the orderbook settings dialog
// - will need to hook up if/when profiles are added to orderbook (or any grid settings dialog that uses this column type)
event Action ISimpleColumnVM.result_changed { add { } remove { } }
public string id { get => _id; private set => Set(ref _id, value); }
public double width { get => _width; set => Set(ref _width, value); }
public HorizontalAlignment horizontal_alignment { get => _horizontal_alignment; set => Set(ref _horizontal_alignment, value); }
@@ -86,8 +91,8 @@ namespace mbts.spark {
can_override_display_name ? display_name : null,
is_visible,
null,
is_font_weight_overridden ? new NullableFontWeightProto(font_weight_override) : default,
is_font_color_overridden ? new NullableColorProto(font_color_override.color) : default,
is_font_weight_overridden ? new NullableFontWeightProto(font_weight_override) : new NullableFontWeightProto(null),
is_font_color_overridden ? new NullableColorProto(font_color_override.color) : new NullableColorProto(null),
width,
horizontal_alignment);
var animations_lookup = predicate_animations.ToFlatLookup(pa => pa.predicate.create_assigned(owner._predicate_data_factory), pa => pa.animations.Select(a => a.create()));
@@ -15,7 +15,9 @@ namespace mbts.ui.settings {
class GridSettings : IEquatable<GridSettings> {
const string _default_font_family = "Segoe UI";
const double _default_font_size = 11;
const uint _default_font_color = 0;
const uint
_default_font_color = 0,
_default_background = 0;
const ushort _default_font_weight = 400;
public static GridSettings Default =>
@@ -46,36 +48,53 @@ namespace mbts.ui.settings {
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);
public static IValueConverter font_color_converter => ValueConverter.create<GridSettings, SolidColorBrush>(gs => new SolidColorBrush(gs.font_color.rgb_color()).frozen());
// font family
[ProtoMember(1)]
[DefaultValue(_default_font_family)]
readonly string _font_family;
// font size
[ProtoMember(2)]
[DefaultValue(_default_font_size)]
readonly double _font_size;
// font weight
[ProtoMember(3)]
[DefaultValue(_default_font_weight)]
readonly ushort _font_weight;
// font color
[ProtoMember(4)]
[DefaultValue(_default_font_color)]
readonly uint _font_color;
// selection highlight
[ProtoMember(5)]
readonly ColorOpacityProto _selection_highlight;
// columns
[ProtoMember(6)]
List<GridColumnSettings> _columns;
// sorting criterion
[ProtoMember(7)]
List<SortingCriteria> _sorting_criterion;
// background
[ProtoMember(8)]
[DefaultValue(_default_background)]
uint _background;
public GridSettings() {
_font_family = _default_font_family;
_font_size = _default_font_size;
_font_weight = _default_font_weight;
_font_color = _default_font_color;
_background = _default_background;
}
public GridSettings(
string font_family = _default_font_family, double font_size = _default_font_size, FontWeight font_weight = default, uint font_color = _default_font_color,
ColorOpacityProto? selection_highlight = null, IEnumerable<GridColumnSettings> columns = null, IEnumerable<SortingCriteria> sorting_criterion = null) {
string font_family = _default_font_family,
double font_size = _default_font_size,
FontWeight font_weight = default,
uint font_color = _default_font_color,
ColorOpacityProto? selection_highlight = null,
IEnumerable<GridColumnSettings> columns = null,
IEnumerable<SortingCriteria> sorting_criterion = null,
uint background = _default_background) {
//
_font_family = font_family;
_font_size = font_size;
@@ -84,12 +103,14 @@ namespace mbts.ui.settings {
_selection_highlight = selection_highlight ?? new ColorOpacityProto(0, 1);
_columns = new List<GridColumnSettings>(columns.empty_if_null());
_sorting_criterion = new List<SortingCriteria>(sorting_criterion.empty_if_null());
_background = background;
}
public string font_family => _font_family;
public double font_size => _font_size;
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 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
@@ -98,7 +119,13 @@ namespace mbts.ui.settings {
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) =>
public GridSettings with(
IEnumerable<GridColumnSettings> columns = null,
IEnumerable<SortingCriteria> sorting_criterion = null,
string font_family = null,
double font_size = double.NaN,
uint? background = null) =>
//
new GridSettings(
font_family ?? _font_family,
DoubleHelper.IsNaNOrInfinityOr(font_size, 0) ? _font_size : font_size,
@@ -106,7 +133,8 @@ namespace mbts.ui.settings {
_font_color,
_selection_highlight,
columns ?? _columns,
sorting_criterion ?? _sorting_criterion);
sorting_criterion ?? _sorting_criterion,
background ?? _background);
public GridSettings clone() => with();
@@ -117,6 +145,7 @@ namespace mbts.ui.settings {
font_weight == other.font_weight &&
font_color == other.font_color &&
selection_highlight == other.selection_highlight &&
_background == other._background &&
EqualityComparer.safe_sequence_equals(columns, other.columns) &&
#pragma warning disable CS0612 // Type or member is obsolete
EqualityComparer.safe_sequence_equals(sorting_criterion, other.sorting_criterion);
@@ -132,6 +161,7 @@ namespace mbts.ui.settings {
font_weight.GetHashCode(),
font_color.GetHashCode(),
selection_highlight.GetHashCode(),
_background.GetHashCode(),
EqualityComparer.safe_aggregate_hash(columns),
#pragma warning disable CS0612 // Type or member is obsolete
EqualityComparer.safe_aggregate_hash(sorting_criterion));
@@ -1,27 +1,80 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace mbts.ui.settings {
static class GridSettingsHelper {
public static TColKey? parse_col_id<TColKey>(string id) where TColKey : struct =>
Enum.TryParse(id, true, out TColKey key) ? key : (TColKey?)null;
public static string fmt_col_id<TColKey>(TColKey key) where TColKey : struct =>
key.ToString();
public static void normalize_sorting_criterion<TColKey>(IEnumerable<SortingCriteria> sorting_criterion, IReadOnlyDictionary<string, TColKey> sort_member_to_id) where TColKey : struct =>
normalize_sorting_criterion(sorting_criterion, new Func<string, TColKey?>[] {
parse_col_id<TColKey>,
col_name => sort_member_to_id.TryGetValue(col_name, out var k) ? k : (TColKey?)null
});
public static void normalize_sorting_criterion<TColKey>(IEnumerable<SortingCriteria> sorting_criterion, IEnumerable<Func<string, TColKey?>> conversion_dels) where TColKey : struct {
foreach (var sc in sorting_criterion) {
sc.col_name = conversion_dels
.Select(del => del(sc.col_name))
.FirstOrDefault(k => k.HasValue)?.ToString() ?? sc.col_name;
}
}
}
using mbts.ui.controls;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Media;
namespace mbts.ui.settings {
static class GridSettingsHelper {
public static TColKey? parse_col_id<TColKey>(string id) where TColKey : struct =>
Enum.TryParse(id, true, out TColKey key) ? key : (TColKey?)null;
public static TColKey strong_id<TColKey>(this GridColumnSettings column_settings) where TColKey : struct =>
parse_col_id<TColKey>(column_settings.id) ?? throw new ArgumentException($"Failed to parse column settings' id '{column_settings.id}' to type '{typeof(TColKey)}.'");
public static string fmt_col_id<TColKey>(TColKey key) where TColKey : struct =>
key.ToString();
public static void normalize_sorting_criterion<TColKey>(IEnumerable<SortingCriteria> sorting_criterion, IReadOnlyDictionary<string, TColKey> sort_member_to_id) where TColKey : struct =>
normalize_sorting_criterion(sorting_criterion, new Func<string, TColKey?>[] {
parse_col_id<TColKey>,
col_name => sort_member_to_id.TryGetValue(col_name, out var k) ? k : (TColKey?)null
});
public static void normalize_sorting_criterion<TColKey>(IEnumerable<SortingCriteria> sorting_criterion, IEnumerable<Func<string, TColKey?>> conversion_dels) where TColKey : struct {
foreach (var sc in sorting_criterion) {
sc.col_name = conversion_dels
.Select(del => del(sc.col_name))
.FirstOrDefault(k => k.HasValue)?.ToString() ?? sc.col_name;
}
}
public static void load_column_settings(IEnumerable<DIYGrid.ColumnBase> columns, GridSettings grid_settings) =>
load_column_settings(columns, grid_settings.columns, new FontFamily(grid_settings.font_family), grid_settings.font_size, grid_settings.font_color.rgb_color(), grid_settings.font_weight);
public static void load_column_settings(IEnumerable<DIYGrid.ColumnBase> columns, IEnumerable<GridColumnSettings> settings, FontFamily grid_font_family, double grid_font_size, Color grid_font_color, FontWeight grid_font_weight) =>
load_column_settings(columns, settings, grid_font_family, grid_font_size, grid_font_color, grid_font_weight, (c, s) => c.load_from(s));
public static void load_column_settings<TColumn>(
IEnumerable<TColumn> columns,
IEnumerable<GridColumnSettings> settings,
FontFamily grid_font_family,
double grid_font_size,
Color grid_font_color,
FontWeight grid_font_weight,
Action<TColumn, GridColumnSettings> load_impl)
where TColumn : DIYGrid.ColumnBase {
//
var join =
from stg in settings
join col in columns on stg.id equals col.id
select new { col, stg };
foreach (var tup in join) {
load_impl(tup.col, tup.stg);
tup.col.set_fallback_font_params(grid_font_family, grid_font_size, grid_font_color, grid_font_weight);
}
}
public static bool try_find_column<T>(IEnumerable<GridColumnSettings> columns, T col_key, out GridColumnSettings result) where T : struct {
var key_str = col_key.ToString();
return columns.TryGetFirst(c => c.id == key_str, out result);
}
public static bool try_find_column<T>(IEnumerable<GridColumnSettings> columns, T col_key, out GridColumnSettings result, out int index) where T : struct {
var key_str = col_key.ToString();
return columns.TryGetFirst(c => c.id == key_str, out result, out index);
}
public static bool try_replace_column<T>(IList<GridColumnSettings> columns, T col_key, Func<GridColumnSettings, GridColumnSettings> replace) {
var key_str = col_key.ToString();
for (int i = 0; i < columns.Count; i++) {
if (columns[i].id == key_str) {
columns[i] = replace(columns[i]);
return true;
}
}
return false;
}
}
}
@@ -4,6 +4,8 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:proto="clr-namespace:bts.proto"
xmlns:m="clr-namespace:mbts"
xmlns:sp="clr-namespace:mbts.spark"
xmlns:pnl="clr-namespace:bts.shell.pnl"
xmlns:u="clr-namespace:mbts.ui"
xmlns:c="clr-namespace:mbts.ui.controls"
xmlns:cv="clr-namespace:mbts.ui.converters"
@@ -314,6 +316,357 @@
/>
</Grid>
</DataTemplate>
<DataTemplate x:Key="DefaultCellContentTemplate">
<Grid>
<u:StateManager.Groups>
<u:StateGroupsManager AttachToMembers="True">
<u:StateDefinitionCollection>
<u:StateDefinition State="{Binding Path='Group.Target.DataContext.can_override_display_name', RelativeSource={RelativeSource Self}, Converter={x:Static usp:GridSettingsView.DisplayNameVisualStateConverter}}"/>
</u:StateDefinitionCollection>
</u:StateGroupsManager>
</u:StateManager.Groups>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup Name="{x:Static GroupName, MemberType={u:Nested usp:GridSettingsView.VisualStateNames.DisplayName}}">
<VisualState Name="{x:Static Active, MemberType={u:Nested usp:GridSettingsView.VisualStateNames.DisplayName}}"/>
<VisualState Name="{x:Static Inactive, MemberType={u:Nested usp:GridSettingsView.VisualStateNames.DisplayName}}">
<Storyboard>
<BooleanAnimationUsingKeyFrames Storyboard.TargetName="display_name_text_box" Storyboard.TargetProperty="(UIElement.IsEnabled)">
<DiscreteBooleanKeyFrame KeyTime="0" Value="False"/>
</BooleanAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="display_name_text_box" Storyboard.TargetProperty="(FrameworkElement.ToolTip)">
<DiscreteObjectKeyFrame KeyTime="0" Value="(N/A)"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="{Binding Source={StaticResource VerticalPush}}"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="{Binding Source={StaticResource VerticalPush}}"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="{Binding Source={StaticResource VerticalPush}}"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="{Binding Source={StaticResource VerticalPush}}"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="{Binding Source={StaticResource HorizontalSplit}}" />
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock>Header</TextBlock>
<TextBox x:Name="display_name_text_box" Grid.Column="2" ToolTipService.ShowOnDisabled="True" Text="{Binding display_name}"/>
<TextBlock Grid.Row="2">Width</TextBlock>
<c:TickUpDownDouble Grid.Row="2" Grid.Column="2" Value="{Binding width}" Format="0.##"/>
<TextBlock Grid.Row="4">Horizontal Alignment</TextBlock>
<ComboBox Grid.Row="4" Grid.Column="2" ItemsSource="{u:EnumBindingSource Type=HorizontalAlignment}" SelectedItem="{Binding horizontal_alignment}"/>
<CheckBox Grid.Row="6" VerticalAlignment="Center" IsEnabled="{Binding can_override_font_weight}" IsChecked="{Binding is_font_weight_overridden}">Override Font Weight</CheckBox>
<ComboBox
Grid.Row="6"
Grid.Column="2"
Visibility="{Binding is_font_weight_overridden, Converter={StaticResource BooleanToVisibilityConverter}}"
ItemsSource="{Binding DataContext.font_family, Converter={StaticResource FontFamilyWeightsConverter}, RelativeSource={RelativeSource AncestorType=usp:GridSettingsView}}"
SelectedItem="{Binding font_weight_override}"
/>
<ComboBox Grid.Row="6" Grid.Column="2" Visibility="{Binding is_font_weight_overridden, Converter={StaticResource FalseVisibilityConverter}}" Style="{StaticResource DisabledPlaceholderComboBox}"/>
<CheckBox Grid.Row="8" VerticalAlignment="Top" IsEnabled="{Binding can_override_font_color}" IsChecked="{Binding is_font_color_overridden}">Override Font Color</CheckBox>
<c:ColorPickerPopup
Grid.Row="8"
Grid.Column="2"
DataContext="{Binding font_color_override}"
VerticalAlignment="Top"
Visibility="{Binding DataContext.current_column.is_font_color_overridden, Converter={StaticResource BooleanToVisibilityConverter}, RelativeSource={RelativeSource AncestorType=usp:GridSettingsView}}"
/>
<ComboBox
Grid.Row="8"
Grid.Column="2"
VerticalAlignment="Top"
Visibility="{Binding is_font_color_overridden, Converter={StaticResource FalseVisibilityConverter}}"
Style="{StaticResource DisabledPlaceholderComboBox}"
/>
</Grid>
</DataTemplate>
<DataTemplate DataType="{x:Type pnl:PNLColumnParamsVM}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="{Binding Source={StaticResource VerticalPush}}"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="{Binding Source={StaticResource VerticalPush}}"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="name_col"/>
<ColumnDefinition Width="{Binding Source={StaticResource HorizontalSplit}}"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<CheckBox Grid.Row="0" VerticalAlignment="Center" IsEnabled="{Binding can_override_font_weight}" IsChecked="{Binding is_font_weight_overridden}">Override Font Weight</CheckBox>
<ComboBox
Grid.Row="0"
Grid.Column="2"
Visibility="{Binding is_font_weight_overridden, Converter={StaticResource BooleanToVisibilityConverter}}"
ItemsSource="{Binding DataContext.font_family, RelativeSource={RelativeSource AncestorType=usp:GridSettingsView}, Converter={StaticResource FontFamilyWeightsConverter}}"
SelectedItem="{Binding font_weight_override}"
/>
<ComboBox Grid.Row="0" Grid.Column="2" Visibility="{Binding is_font_weight_overridden, Converter={StaticResource FalseVisibilityConverter}}" Style="{StaticResource DisabledPlaceholderComboBox}"/>
<CheckBox Grid.Row="2" VerticalAlignment="Top" IsChecked="{Binding is_font_color_overridden}">Override Font Color</CheckBox>
<c:ColorPickerPopup
Grid.Row="2"
Grid.Column="2"
SelectedColor="{Binding font_color_override.color}"
Palettes="{Binding font_color_override.palettes}"
Visibility="{Binding is_font_color_overridden, Converter={StaticResource BooleanToVisibilityConverter}}"
/>
<ComboBox
Grid.Row="2"
Grid.Column="2"
Visibility="{Binding is_font_color_overridden, Converter={StaticResource FalseVisibilityConverter}}"
Style="{StaticResource DisabledPlaceholderComboBox}"
/>
<CheckBox Grid.Row="4" VerticalAlignment="Top" IsChecked="{Binding is_background_overridden}">Override Background</CheckBox>
<c:ColorPickerPopup
Grid.Row="4"
Grid.Column="2"
SelectedColor="{Binding background_override.color}"
Palettes="{Binding background_override.palettes}"
IsEnabled="{Binding is_background_overridden}"
/>
</Grid>
</DataTemplate>
<DataTemplate x:Key="PNLColumnTemplate" DataType="{x:Type pnl:PNLColumnVM}">
<StackPanel Grid.IsSharedSizeScope="True">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="{Binding Source={StaticResource VerticalPush}}"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="{Binding Source={StaticResource VerticalPush}}"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="name_col"/>
<ColumnDefinition Width="{Binding Source={StaticResource HorizontalSplit}}"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock>Header</TextBlock>
<TextBox x:Name="display_name_text_box" Grid.Column="2" ToolTipService.ShowOnDisabled="True" Text="{Binding display_name}"/>
<TextBlock Grid.Row="2">Width</TextBlock>
<c:TickUpDownDouble Grid.Row="2" Grid.Column="2" Value="{Binding width}" Format="0.##"/>
<TextBlock Grid.Row="4">Horizontal Alignment</TextBlock>
<ComboBox Grid.Row="4" Grid.Column="2" ItemsSource="{u:EnumBindingSource Type=HorizontalAlignment}" SelectedItem="{Binding horizontal_alignment}"/>
</Grid>
<GroupBox Margin="0,8,0,0" Header="Rows" Content="{Binding default_column_vm}"/>
<GroupBox Margin="0,8,0,0" Header="Group Header Rows" Content="{Binding group_column_vm}"/>
<GroupBox Margin="0,8,0,0" Header="Total/Selected Rows" Content="{Binding header_column_vm}"/>
</StackPanel>
</DataTemplate>
<DataTemplate DataType="{x:Type pnl:PNLRuleColumnVM}">
<StackPanel Grid.IsSharedSizeScope="True">
<GroupBox Header="Rows" DataContext="{Binding row_params}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="{Binding Source={StaticResource VerticalPush}}"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="{Binding Source={StaticResource VerticalPush}}"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="name_col"/>
<ColumnDefinition Width="{Binding Source={StaticResource HorizontalSplit}}" />
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<CheckBox Grid.Row="0" VerticalAlignment="Center" IsEnabled="{Binding can_override_font_weight}" IsChecked="{Binding is_font_weight_overridden}">Override Font Weight</CheckBox>
<ComboBox
Grid.Row="0"
Grid.Column="2"
Visibility="{Binding is_font_weight_overridden, Converter={StaticResource BooleanToVisibilityConverter}}"
ItemsSource="{Binding DataContext.font_family, RelativeSource={RelativeSource AncestorType=usp:GridSettingsView}, Converter={StaticResource FontFamilyWeightsConverter}}"
SelectedItem="{Binding font_weight_override}"
/>
<!--Group-->
<ComboBox Grid.Row="0" Grid.Column="2" Visibility="{Binding is_font_weight_overridden, Converter={StaticResource FalseVisibilityConverter}}" Style="{StaticResource DisabledPlaceholderComboBox}"/>
<CheckBox Grid.Row="2" VerticalAlignment="Top" IsChecked="{Binding is_font_color_overridden}">Override Font Color</CheckBox>
<c:ColorPickerPopup
Grid.Row="2"
Grid.Column="2"
VerticalAlignment="Top"
SelectedColor="{Binding font_color_override.color}"
Palettes="{Binding font_color_override.palettes}"
Visibility="{Binding is_font_color_overridden, Converter={StaticResource BooleanToVisibilityConverter}}"
/>
<ComboBox
Grid.Row="2"
Grid.Column="2"
VerticalAlignment="Top"
Visibility="{Binding is_font_color_overridden, Converter={StaticResource FalseVisibilityConverter}}"
Style="{StaticResource DisabledPlaceholderComboBox}"
/>
<CheckBox Grid.Row="4" VerticalAlignment="Top" IsChecked="{Binding is_background_overridden}">Override Background</CheckBox>
<c:ColorPickerPopup
Grid.Row="4"
Grid.Column="2"
VerticalAlignment="Top"
SelectedColor="{Binding background_override.color}"
Palettes="{Binding background_override.palettes}"
Visibility="{Binding is_background_overridden, Converter={StaticResource BooleanToVisibilityConverter}}"
/>
</Grid>
</GroupBox>
<GroupBox Header="Group Header Rows" DataContext="{Binding group_params}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="{Binding Source={StaticResource VerticalPush}}"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="{Binding Source={StaticResource VerticalPush}}"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="name_col"/>
<ColumnDefinition Width="{Binding Source={StaticResource HorizontalSplit}}" />
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<CheckBox Grid.Row="0" VerticalAlignment="Center" IsEnabled="{Binding can_override_font_weight}" IsChecked="{Binding is_font_weight_overridden}">Override Font Weight</CheckBox>
<ComboBox
Grid.Row="0"
Grid.Column="2"
Visibility="{Binding is_font_weight_overridden, Converter={StaticResource BooleanToVisibilityConverter}}"
ItemsSource="{Binding DataContext.font_family, RelativeSource={RelativeSource AncestorType=usp:GridSettingsView}, Converter={StaticResource FontFamilyWeightsConverter}}"
SelectedItem="{Binding font_weight_override}"
/>
<!--Group-->
<ComboBox Grid.Row="0" Grid.Column="2" Visibility="{Binding is_font_weight_overridden, Converter={StaticResource FalseVisibilityConverter}}" Style="{StaticResource DisabledPlaceholderComboBox}"/>
<CheckBox Grid.Row="2" VerticalAlignment="Top" IsChecked="{Binding is_font_color_overridden}">Override Font Color</CheckBox>
<c:ColorPickerPopup
Grid.Row="2"
Grid.Column="2"
VerticalAlignment="Top"
SelectedColor="{Binding font_color_override.color}"
Palettes="{Binding font_color_override.palettes}"
Visibility="{Binding is_font_color_overridden, Converter={StaticResource BooleanToVisibilityConverter}}"
/>
<ComboBox
Grid.Row="2"
Grid.Column="2"
VerticalAlignment="Top"
Visibility="{Binding is_font_color_overridden, Converter={StaticResource FalseVisibilityConverter}}"
Style="{StaticResource DisabledPlaceholderComboBox}"
/>
<CheckBox Grid.Row="4" VerticalAlignment="Top" IsChecked="{Binding is_background_overridden}">Override Background</CheckBox>
<c:ColorPickerPopup
Grid.Row="4"
Grid.Column="2"
VerticalAlignment="Top"
SelectedColor="{Binding background_override.color}"
Palettes="{Binding background_override.palettes}"
Visibility="{Binding is_background_overridden, Converter={StaticResource BooleanToVisibilityConverter}}"
/>
</Grid>
</GroupBox>
<GroupBox Header="Total/Selected Rows" DataContext="{Binding header_params}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="{Binding Source={StaticResource VerticalPush}}"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="{Binding Source={StaticResource VerticalPush}}"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="name_col"/>
<ColumnDefinition Width="{Binding Source={StaticResource HorizontalSplit}}" />
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<CheckBox Grid.Row="0" VerticalAlignment="Center" IsEnabled="{Binding can_override_font_weight}" IsChecked="{Binding is_font_weight_overridden}">Override Font Weight</CheckBox>
<ComboBox
Grid.Row="0"
Grid.Column="2"
Visibility="{Binding is_font_weight_overridden, Converter={StaticResource BooleanToVisibilityConverter}}"
ItemsSource="{Binding DataContext.font_family, RelativeSource={RelativeSource AncestorType=usp:GridSettingsView}, Converter={StaticResource FontFamilyWeightsConverter}}"
SelectedItem="{Binding font_weight_override}"
/>
<!--Group-->
<ComboBox Grid.Row="0" Grid.Column="2" Visibility="{Binding is_font_weight_overridden, Converter={StaticResource FalseVisibilityConverter}}" Style="{StaticResource DisabledPlaceholderComboBox}"/>
<CheckBox Grid.Row="2" VerticalAlignment="Top" IsChecked="{Binding is_font_color_overridden}">Override Font Color</CheckBox>
<c:ColorPickerPopup
Grid.Row="2"
Grid.Column="2"
VerticalAlignment="Top"
SelectedColor="{Binding font_color_override.color}"
Palettes="{Binding font_color_override.palettes}"
Visibility="{Binding is_font_color_overridden, Converter={StaticResource BooleanToVisibilityConverter}}"
/>
<ComboBox
Grid.Row="2"
Grid.Column="2"
VerticalAlignment="Top"
Visibility="{Binding is_font_color_overridden, Converter={StaticResource FalseVisibilityConverter}}"
Style="{StaticResource DisabledPlaceholderComboBox}"
/>
<CheckBox Grid.Row="4" VerticalAlignment="Top" IsChecked="{Binding is_background_overridden}">Override Background</CheckBox>
<c:ColorPickerPopup
Grid.Row="4"
Grid.Column="2"
VerticalAlignment="Top"
SelectedColor="{Binding background_override.color}"
Palettes="{Binding background_override.palettes}"
Visibility="{Binding is_background_overridden, Converter={StaticResource BooleanToVisibilityConverter}}"
/>
<ComboBox
Grid.Row="2"
Grid.Column="2"
VerticalAlignment="Top"
Visibility="{Binding is_background_overridden, Converter={StaticResource FalseVisibilityConverter}}"
Style="{StaticResource DisabledPlaceholderComboBox}"
/>
</Grid>
</GroupBox>
</StackPanel>
</DataTemplate>
<DataTemplate x:Key="PNLCellContentTemplate" DataType="{x:Type pnl:PNLColumnVM}">
<!--The 'Delay' addresses a side effect (it's a wpf bug) when changing the SelectedItem in TabControls,
where null values are propagated to binding targets of the previous SelectedItem -->
<TabControl x:Name="column_tabs" Padding="8" SelectedItem="{Binding selected_item, Delay=100}">
<TabControl.ItemContainerStyle>
<Style TargetType="TabItem">
<Setter Property="HeaderTemplateSelector">
<Setter.Value>
<u:DataTypeTemplateSelector>
<DataTemplate DataType="{x:Type pnl:PNLRuleColumnVM}">
<TextBlock Text="{Binding rule_text, Converter={x:Static ut:DisplayNameHelper.ExpressionSnakeToTitleCaseConverter}}"/>
</DataTemplate>
<u:DataTypeTemplateSelector.Default>
<DataTemplate>
<TextBlock Text="{Binding}"/>
</DataTemplate>
</u:DataTypeTemplateSelector.Default>
</u:DataTypeTemplateSelector>
</Setter.Value>
</Setter>
</Style>
</TabControl.ItemContainerStyle>
<TabControl.ItemsSource>
<CompositeCollection>
<CollectionContainer>
<CollectionContainer.Collection>
<x:Array Type="{x:Type TabItem}">
<TabItem Header="Default" Content="{Binding}" ContentTemplate="{StaticResource PNLColumnTemplate}"/>
</x:Array>
</CollectionContainer.Collection>
</CollectionContainer>
<CollectionContainer Collection="{Binding DataContext.rule_columns, Source={u:DirectReference column_tabs}}"/>
</CompositeCollection>
</TabControl.ItemsSource>
</TabControl>
</DataTemplate>
</ResourceDictionary>
</UserControl.Resources>
<Grid Margin="{StaticResource FullPadding}">
@@ -554,7 +907,7 @@
<ColumnDefinition Width="{Binding Source={StaticResource HorizontalGap}}" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<c:DragDropListBox Grid.RowSpan="3" MinWidth="200" ItemsSource="{Binding columns}" SelectedItem="{Binding current_column}">
<c:DragDropListBox Grid.RowSpan="3" MinWidth="200" MaxHeight="500" ItemsSource="{Binding columns}" SelectedItem="{Binding current_column}">
<c:DragDropListBox.ItemContainerStyle>
<Style TargetType="c:DraggableListBoxItem">
<Setter Property="Cursor" Value="Arrow"/>
@@ -590,10 +943,10 @@
<ColumnDefinition Width="{Binding Source={StaticResource HorizontalPush}}"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<CheckBox x:Name="visibility_toggle" ToolTipService.ShowOnDisabled="True" IsChecked="{Binding is_visible}"/>
<StackPanel Grid.Column="2" Orientation="Horizontal">
<TextBlock Text="{Binding id, Converter={x:Static ut:DisplayNameHelper.CamelCaseSpacesConverter}}"/>
<TextBlock Margin="2,0,0,0" FontStyle="Italic" Text="{Binding display_name, Mode=OneWay, StringFormat='{} ({0})'}">
<CheckBox x:Name="visibility_toggle" ToolTipService.ShowOnDisabled="True" IsChecked="{Binding is_visible}" Focusable="False"/>
<StackPanel Grid.Column="2" Orientation="Horizontal" Focusable="False">
<TextBlock Text="{Binding id, Converter={x:Static ut:DisplayNameHelper.CamelCaseSpacesConverter}}" Focusable="False"/>
<TextBlock Margin="2,0,0,0" FontStyle="Italic" Text="{Binding display_name, Mode=OneWay, StringFormat='{} ({0})'}" Focusable="False">
<TextBlock.Visibility>
<MultiBinding Converter="{x:Static usp:GridSettingsView.ColumnDisplayNameVisibilityConverter}">
<Binding Path="id" Converter="{x:Static ut:DisplayNameHelper.CamelCaseSpacesConverter}"/>
@@ -606,73 +959,10 @@
</DataTemplate>
</c:DragDropListBox.ItemTemplate>
</c:DragDropListBox>
<GroupBox Grid.Column="2" MinWidth="280">
<GroupBox Grid.Column="2" MinWidth="280" Content="{Binding current_column}" ContentTemplate="{Binding CellContentTemplate, RelativeSource={RelativeSource AncestorType=usp:GridSettingsView}}">
<GroupBox.Header>
<TextBlock Text="{Binding current_column.display_name, StringFormat='{}\'{0}\' Cell Appearance'}"/>
</GroupBox.Header>
<Grid x:Name="column_content_root">
<u:StateManager.Groups>
<u:StateGroupsManager>
<u:StateDefinitionCollection>
<u:StateDefinition State="{Binding DataContext.current_column.can_override_display_name, Source={u:DirectReference column_content_root}, Converter={x:Static usp:GridSettingsView.DisplayNameVisualStateConverter}}"/>
</u:StateDefinitionCollection>
</u:StateGroupsManager>
</u:StateManager.Groups>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup Name="{x:Static GroupName, MemberType={u:Nested usp:GridSettingsView.VisualStateNames.DisplayName}}">
<VisualState Name="{x:Static Active, MemberType={u:Nested usp:GridSettingsView.VisualStateNames.DisplayName}}"/>
<VisualState Name="{x:Static Inactive, MemberType={u:Nested usp:GridSettingsView.VisualStateNames.DisplayName}}">
<Storyboard>
<BooleanAnimationUsingKeyFrames Storyboard.TargetName="display_name_text_box" Storyboard.TargetProperty="(UIElement.IsEnabled)">
<DiscreteBooleanKeyFrame KeyTime="0" Value="False"/>
</BooleanAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="display_name_text_box" Storyboard.TargetProperty="(FrameworkElement.ToolTip)">
<DiscreteObjectKeyFrame KeyTime="0" Value="(N/A)"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="{Binding Source={StaticResource VerticalPush}}"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="{Binding Source={StaticResource VerticalPush}}"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="{Binding Source={StaticResource VerticalPush}}"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="{Binding Source={StaticResource VerticalPush}}"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="{Binding Source={StaticResource HorizontalSplit}}" />
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock>Header</TextBlock>
<TextBox x:Name="display_name_text_box" Grid.Column="2" ToolTipService.ShowOnDisabled="True" Text="{Binding current_column.display_name}"/>
<TextBlock Grid.Row="2">Width</TextBlock>
<c:TickUpDownDouble Grid.Row="2" Grid.Column="2" Value="{Binding current_column.width}"/>
<TextBlock Grid.Row="4">Horizontal Alignment</TextBlock>
<ComboBox Grid.Row="4" Grid.Column="2" ItemsSource="{u:EnumBindingSource Type=HorizontalAlignment}" SelectedItem="{Binding current_column.horizontal_alignment}"/>
<CheckBox Grid.Row="6" VerticalAlignment="Center" IsEnabled="{Binding current_column.can_override_font_weight}" IsChecked="{Binding current_column.is_font_weight_overridden}">Override Font Weight</CheckBox>
<ComboBox
Grid.Row="6"
Grid.Column="2"
Visibility="{Binding current_column.is_font_weight_overridden, Converter={StaticResource BooleanToVisibilityConverter}}"
ItemsSource="{Binding font_family, Converter={StaticResource FontFamilyWeightsConverter}}"
SelectedItem="{Binding current_column.font_weight_override}"
/>
<ComboBox Grid.Row="6" Grid.Column="2" Style="{StaticResource DisabledPlaceholderComboBox}" Visibility="{Binding current_column.is_font_weight_overridden, Converter={StaticResource FalseVisibilityConverter}}"/>
<CheckBox Grid.Row="8" VerticalAlignment="Center" IsEnabled="{Binding current_column.can_override_font_color}" IsChecked="{Binding current_column.is_font_color_overridden}">Override Font Color</CheckBox>
<c:ColorPickerPopup
Grid.Row="8"
Grid.Column="2"
DataContext="{Binding current_column.font_color_override}"
Visibility="{Binding DataContext.current_column.is_font_color_overridden, Converter={StaticResource BooleanToVisibilityConverter}, RelativeSource={RelativeSource AncestorType=usp:GridSettingsView}}"
/>
<ComboBox Grid.Row="8" Grid.Column="2" Style="{StaticResource DisabledPlaceholderComboBox}" Visibility="{Binding current_column.is_font_color_overridden, Converter={StaticResource FalseVisibilityConverter}}"/>
</Grid>
</GroupBox>
<!--TODO: Make this visible when cell animations are ready-->
<GroupBox Grid.Row="2" Grid.Column="2" VerticalAlignment="Top" Visibility="Collapsed">
@@ -38,6 +38,16 @@ namespace mbts.ui.spark {
}
#endregion
#region CellContentTemplate
public static readonly DependencyProperty CellContentTemplateProperty =
WPFHelper.create_dp<DataTemplate, GridSettingsView>(nameof(CellContentTemplate));
public DataTemplate CellContentTemplate {
get => GetValue(CellContentTemplateProperty) as DataTemplate;
set => SetValue(CellContentTemplateProperty, value);
}
#endregion
public static IMultiValueConverter ColumnDisplayNameVisibilityConverter =>
ValueConverter.create<string, string, Visibility>((id, header) => id == header ? Visibility.Collapsed : Visibility.Visible);
@@ -1,15 +1,25 @@
using bts.proto;
using bts.utils;
using mbts.ui.controls;
using mbts.ui.settings;
using mbts.ui.shell;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Windows;
using System.Windows.Media;
namespace mbts.spark {
partial class SimpleGridSettingsVM {
public class SimpleColumnVM : ViewModelBase, ISimpleColumnVM {
public class SimpleColumnVM : DisposableViewModelBase, ISimpleColumnVM {
static readonly IReadOnlyList<Func<SimpleColumnVM, INotifyPropertyChanged>> _observable_component_getters =
ObjectSnapshot.property_delegates_of_type<SimpleColumnVM, INotifyPropertyChanged>()
.ToArray();
public readonly SimpleGridSettingsVM owner;
readonly DeferrableActionWrapper _update;
string
_id = string.Empty,
_display_name = string.Empty;
@@ -25,8 +35,13 @@ namespace mbts.spark {
FontWeight _font_weight_override;
GridColumnSettings _settings;
public SimpleColumnVM(SimpleGridSettingsVM owner) =>
public SimpleColumnVM(SimpleGridSettingsVM owner) {
this.owner = owner;
_update = new DeferrableActionWrapper(() => result_changed?.Invoke());
_dispose = ObjectSnapshot.connect_property_changed(_observable_components, (s, e) => _update.execute());
}
public event Action result_changed;
public string id { get => _id; private set => Set(ref _id, value); }
public double width { get => _width; set => Set(ref _width, value); }
@@ -44,13 +59,15 @@ namespace mbts.spark {
public bool is_font_weight_overridden { get => _is_font_weight_overridden; set => Set(ref _is_font_weight_overridden, value); }
public FontWeight font_weight_override { get => _font_weight_override; set => Set(ref _font_weight_override, value); }
IEnumerable<INotifyPropertyChanged> _observable_components => _observable_component_getters.Select(g => g(this)).Append(this);
public GridColumnSettings create() =>
_settings.with(
can_override_display_name ? display_name : null,
is_visible,
null,
is_font_weight_overridden ? new NullableFontWeightProto(font_weight_override) : (NullableFontWeightProto?)null,
is_font_color_overridden ? new NullableColorProto(font_color_override.color) : (NullableColorProto?)null,
is_font_weight_overridden ? new NullableFontWeightProto(font_weight_override) : new NullableFontWeightProto(null),
is_font_color_overridden ? new NullableColorProto(font_color_override.color) : new NullableColorProto(null),
width,
horizontal_alignment);
@@ -28,6 +28,7 @@ namespace mbts.spark {
bool is_font_weight_overridden { get; set; }
FontWeight font_weight_override { get; set; }
void apply_flags(GridColumnSettingsFeatureFlags flags);
event Action result_changed;
}
partial class SimpleGridSettingsVM : DisposableViewModelBase, IValueFactory<GridSettings>, IWindowSettingsVM<GridSettings>, ITemplateSelectorKey {
@@ -39,9 +40,13 @@ namespace mbts.spark {
readonly Property<bool> _show_predicate_animations, _show_cell_predicate_animations;
readonly DeferrableActionWrapper _update;
readonly IReadOnlyDictionary<string, GridColumnSettingsFeatureFlags> _column_flags;
readonly Func<GridColumnSettings, ISimpleColumnVM> _column_factory;
bool _can_edit_background;
ColorPickerPopupVM _background;
public SimpleGridSettingsVM(IReadOnlyDictionary<string, GridColumnSettingsFeatureFlags> column_flags = null) {
public SimpleGridSettingsVM(IReadOnlyDictionary<string, GridColumnSettingsFeatureFlags> column_flags = null, Func<GridColumnSettings, ISimpleColumnVM> column_factory = null) {
_column_flags = column_flags ?? new Dictionary<string, GridColumnSettingsFeatureFlags>();
_column_factory = column_factory ?? _default_column_factory;
_font_family = backing(nameof(font_family), TextUtils.StandardFontFamily);
_font_size = backing(nameof(font_size), FontSizes.Small);
_font_weight = backing(nameof(font_weight), FontWeights.Normal);
@@ -55,10 +60,11 @@ namespace mbts.spark {
c.PropertyChanged += on_prop_changed;
_dispose = new CompositeDisposable(
ObservableAutoWrapper.connect_item_hooks(columns, c => {
if (c is INotifyPropertyChanged npc) {
npc.PropertyChanged += on_prop_changed;
return Disposable.Create(() => npc.PropertyChanged -= on_prop_changed);
} else return Disposable.empty;
c.result_changed += _update.execute;
var hook = Disposable.Create(() => c.result_changed -= _update.execute);
if (c is IDisposable d)
hook = Disposable.Create(hook, d);
return hook;
})) {
() => {
foreach (var c in _observable_components)
@@ -79,8 +85,10 @@ namespace mbts.spark {
public FontWeight font_weight { get => _font_weight.Value; set => _font_weight.set(value); }
public ColorPickerPopupVM font_color { get; } = new ColorPickerPopupVM();
public ColorPickerPopupVM selection_highlight { get; } = new ColorPickerPopupVM();
public ColorPickerPopupVM background => _background ?? (_background = new ColorPickerPopupVM { bind_opacity_to_alpha = true, opacity = 1 });
public double font_size { get => _font_size.Value; set => _font_size.set(value); }
public bool enable_sound_triggers => false;
public bool can_edit_background { get => _can_edit_background; set => Set(ref _can_edit_background, value); }
public bool show_predicate_animations { get => _show_predicate_animations.Value; set => _show_predicate_animations.set(value); }
public bool show_cell_predicate_animations { get => _show_cell_predicate_animations.Value; set => _show_cell_predicate_animations.set(value); }
public IReadOnlyList<DialogSkirtItem> dialog_skirt_items => new[] {
@@ -102,8 +110,18 @@ namespace mbts.spark {
}
}
public GridSettings create() =>
new GridSettings(font_family.name(), font_size, font_weight, font_color.color.rgb_color(), selection_highlight.to_proto(), columns.Select(c => c.create()));
public GridSettings create() {
var rv = new GridSettings(
font_family.name(),
font_size,
font_weight,
font_color.color.rgb_color(),
selection_highlight.to_proto(),
columns.Select(c => c.create()));
if (can_edit_background)
rv = rv.with(background: background.color.rgb_color());
return rv;
}
public void load_from(GridSettings value) {
if (value == null)
@@ -115,22 +133,26 @@ namespace mbts.spark {
selection_highlight.load_from(value.selection_highlight.to_brush());
_font_size.set(value.font_size);
var curr_col_id = current_column?.id;
columns.SetItems(value.columns.Select(col => {
var vm = new SimpleColumnVM(this);
vm.load_from(col);
// reapply our favorite column flags
// this comes in handy when performing Load From Factory - the editor will contain
// a consistent view of what can be overridden and what cannot
if (_column_flags.TryGetValue(vm.id, out GridColumnSettingsFeatureFlags flags))
vm.apply_flags(flags);
return vm;
}));
columns.SetItems(value.columns.Select(_column_factory));
if (curr_col_id == null || !columns.TryGetFirst(c => c.id == curr_col_id, out ISimpleColumnVM curr_column))
curr_column = columns.FirstOrDefault();
_current_column.set(curr_column);
if (can_edit_background)
background.color = value.background;
}
}
ISimpleColumnVM _default_column_factory(GridColumnSettings col_settings) {
var vm = new SimpleColumnVM(this);
vm.load_from(col_settings);
// reapply our favorite column flags
// this comes in handy when performing Load From Factory - the editor will contain
// a consistent view of what can be overridden and what cannot
if (_column_flags.TryGetValue(vm.id, out GridColumnSettingsFeatureFlags flags))
vm.apply_flags(flags);
return vm;
}
void IWindowSettingsVM<GridSettings>.load(GridSettings value) => load_from(value);
GridSettings IWindowSettingsVM<GridSettings>.get_result() => create();
}
@@ -0,0 +1,34 @@
using mbts.ui.spark;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
namespace bts.shell.pnl {
readonly struct AnimationFrame<TColumn> where TColumn : struct {
public readonly TimeSpan timestamp;
public readonly ImmutableDictionary<SymbolKey<TColumn>, Animation> animation;
public AnimationFrame(TimeSpan timestamp) {
this.timestamp = timestamp;
animation = ImmutableDictionary<SymbolKey<TColumn>, Animation>.Empty;
}
public AnimationFrame(TimeSpan timestamp, ImmutableDictionary<SymbolKey<TColumn>, Animation> animation) {
this.timestamp = timestamp;
this.animation = animation;
}
}
static class AnimationFrameFactory {
public static IEnumerable<AnimationFrame<TColumn>> flash<TColumn>(TColumn column, params Animation[] animation) where TColumn : struct =>
flash(column, TimeSpan.FromSeconds(2), animation);
public static IEnumerable<AnimationFrame<TColumn>> flash<TColumn>(TColumn column, TimeSpan duration, params Animation[] animation) where TColumn : struct {
var anims = ImmutableDictionary<SymbolKey<TColumn>, Animation>.Empty;
for (int i = 0; i < animation.Length; i++)
anims = anims.Add(new SymbolKey<TColumn>(column, animation[i].type), animation[i]);
yield return new AnimationFrame<TColumn>(TimeSpan.Zero, anims);
yield return new AnimationFrame<TColumn>(duration);
}
}
}
@@ -0,0 +1,133 @@
using mbts.ui.spark;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using System.Threading.Tasks;
using System.Windows.Threading;
namespace bts.shell.pnl {
class DIYGridAnimationModel<TColumn, TRow> : IObservable<ImmutableDictionary<SymbolKey<TColumn>, Animation>> where TColumn : struct {
readonly Dictionary<int, ITriggerAnimation<TRow, TColumn>> _triggers = new Dictionary<int, ITriggerAnimation<TRow, TColumn>>();
readonly Subject<ImmutableDictionary<SymbolKey<TColumn>, Animation>> _subject = new Subject<ImmutableDictionary<SymbolKey<TColumn>, Animation>>();
readonly TimeSpan? _delay;
TRow _pending_update;
bool _is_busy, _is_dirty;
ImmutableDictionary<int, int> _versions = ImmutableDictionary<int, int>.Empty;
ImmutableDictionary<SymbolKey<TColumn>, Animation> _full_state = ImmutableDictionary<SymbolKey<TColumn>, Animation>.Empty;
// Since animations may begin before the previous one is finished, there may be symbol use conflicts (i.e., animation units, like arrow, border, etc.).
// The Symbol Access map below indicates which symbols are in use, and which trigger is using them (value).
// When the a particular trigger begins a new animation timeline (in response to quick data changes), before the previous one completed,
// the symbols being used by the previous animation are removed from the overall state.
ImmutableDictionary<SymbolKey<TColumn>, int> _symbol_access = ImmutableDictionary<SymbolKey<TColumn>, int>.Empty;
public DIYGridAnimationModel() { }
public DIYGridAnimationModel(TimeSpan delay) {
_delay = delay;
}
int _trigger_id = 0;
public void add_trigger(ITriggerAnimation<TRow, TColumn> trigger) =>
_triggers.Add(++_trigger_id, trigger);
public void add_trigger(int id, ITriggerAnimation<TRow, TColumn> trigger) =>
_triggers.Add(id, trigger);
public async void on_row(TRow r) {
if (_delay.HasValue) {
if (_is_busy) {
_is_dirty = true;
_pending_update = r;
} else {
_is_busy = true;
_pending_update = r;
do {
_is_dirty = false;
int thread = System.Threading.Thread.CurrentThread.ManagedThreadId;
await Task.Delay(_delay.Value);
int thread_after = System.Threading.Thread.CurrentThread.ManagedThreadId;
Log.assert(thread == thread_after, "Delayed execution returned on a different thread.");
on_row(_pending_update, _triggers);
} while (_is_dirty);
_is_busy = false;
}
} else {
on_row(r, _triggers);
}
}
public void on_row(TRow r, IEnumerable<KeyValuePair<int, ITriggerAnimation<TRow, TColumn>>> triggers) {
var timeline = ImmutableSortedDictionary<TimeSpan, ImmutableDictionary<int, ImmutableDictionary<SymbolKey<TColumn>, Animation>>>.Empty;
foreach (var kv in triggers) {
var frames = kv.Value.on_data(r).ToList();
if (frames.Count > 0) {
// clear previous animation-state for this trigger
foreach (var sym_kv in _symbol_access) {
if (sym_kv.Value == kv.Key) {
_symbol_access = _symbol_access.Remove(sym_kv.Key);
_full_state = _full_state.Remove(sym_kv.Key);
}
}
foreach (var f in frames) {
if (timeline.TryGetValue(f.timestamp, out var tanims)) {
tanims = tanims.Add(kv.Key, f.animation);
timeline = timeline.SetItem(f.timestamp, tanims);
} else {
timeline = timeline.Add(f.timestamp, ImmutableDictionary<int, ImmutableDictionary<SymbolKey<TColumn>, Animation>>.Empty.Add(kv.Key, f.animation));
}
}
_versions = _versions.SetItem(kv.Key, _versions.TryGetValue(kv.Key, out int vers) ? vers + 1 : 1);
}
}
if (timeline.Count > 0)
_animate(timeline);
}
void _animate(ImmutableSortedDictionary<TimeSpan, ImmutableDictionary<int, ImmutableDictionary<SymbolKey<TColumn>, Animation>>> timeline) {
var active = ImmutableDictionary<SymbolKey<TColumn>, int>.Empty;
var snapshot = _versions;
Log.assert(timeline.First().Key == TimeSpan.Zero);
// ObserveOnDispatcher() is critical here, since all the animation timelines share access to a finite set of symbols (n per cell).
// Unless this demonstrably hurts performance, it is probably not worth the trouble of implementing concurrent symbol sharing.
var begin = timeline.GetEnumerator();
DispatcherObservable.ObserveOnDispatcher(
Observable.Generate(begin,
en => begin.MoveNext() && begin.Current.Key < TimeSpan.MaxValue,
en => en,
en => begin.Current.Value,
en => begin.Current.Key),
DispatcherPriority.ApplicationIdle
).Subscribe(on_animation);
void on_animation(ImmutableDictionary<int, ImmutableDictionary<SymbolKey<TColumn>, Animation>> changes) {
var abd = active.ToBuilder();
foreach (var kv in active) {
if (changes.TryGetValue(kv.Value, out var new_trig_state)) {
if (!new_trig_state.ContainsKey(kv.Key)) {
abd.Remove(kv.Key);
_symbol_access = _symbol_access.Remove(kv.Key);
_full_state = _full_state.Remove(kv.Key);
}
}
}
foreach (var kv in changes) {
if (_versions.TryGetValue(kv.Key, out int vers) && vers == snapshot[kv.Key]) {
foreach (var akv in kv.Value) {
_symbol_access = _symbol_access.SetItem(akv.Key, kv.Key);
_full_state = _full_state.SetItem(akv.Key, akv.Value);
abd[akv.Key] = kv.Key;
}
}
}
active = abd.ToImmutable();
_subject.OnNext(_full_state);
}
}
public IDisposable Subscribe(IObserver<ImmutableDictionary<SymbolKey<TColumn>, Animation>> observer) =>
_subject.Subscribe(observer);
}
}
@@ -0,0 +1,271 @@
using mbts.ui.spark;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Reactive.Linq;
using System.Windows.Media;
using CellAnimationType = mbts.ui.spark.CellAnimation.Type;
namespace bts.shell.pnl {
interface ITriggerAnimation<T, TColumn> where TColumn : struct {
IEnumerable<AnimationFrame<TColumn>> on_data(T value);
}
static class DIYGridTriggerAnimations {
static readonly TimeSpan _default_flash_duration = TimeSpan.FromSeconds(2);
public static ITriggerAnimation<T, TColumn> create<T, TColumn>(Func<T, IEnumerable<AnimationFrame<TColumn>>> animate) where TColumn : struct =>
new AnonymousAnimator<T, TColumn>(animate);
public static ITriggerAnimation<T, TColumn> delta_trigger<T, TColumn>(TColumn column, Color increase, Color decrease) where T : struct, IComparable<T> where TColumn : struct =>
new DeltaTimeline<T, TColumn>(column, increase, decrease, _default_flash_duration);
public static ITriggerAnimation<T, TColumn> delta_trigger<T, TColumn>(TColumn column, Color increase, Color decrease, IComparer<T> comparer) where T : struct where TColumn : struct =>
new DeltaTimeline<T, TColumn>(column, increase, decrease, _default_flash_duration, comparer);
public static ITriggerAnimation<T, TColumn> delta_trigger<T, TColumn>(TColumn column, Color increase, Color decrease, TimeSpan flash_duration) where T : struct, IComparable<T> where TColumn : struct =>
new DeltaTimeline<T, TColumn>(column, increase, decrease, flash_duration);
public static ITriggerAnimation<T, TColumn> append<T, TColumn>(this ITriggerAnimation<T, TColumn> first, ITriggerAnimation<T, TColumn> second) where TColumn : struct =>
new AppendTrigger<T, TColumn>(first, second);
public static ITriggerAnimation<T, TColumn> zip_indefinite_frame<T, TColumn>(this ITriggerAnimation<T, TColumn> trigger, Func<T, ImmutableDictionary<SymbolKey<TColumn>, Animation>> get_indefinite) where TColumn : struct =>
new IndefiniteFrameZipper<T, TColumn>(trigger, get_indefinite);
public static ITriggerAnimation<T, TColumn> where<T, TColumn>(this ITriggerAnimation<T, TColumn> trigger, Func<T, bool> predicate) where T : struct, IComparable<T> where TColumn : struct =>
new WhereTrigger<T, TColumn>(trigger, predicate);
class DeltaTimeline<T, TColumn> : ITriggerAnimation<T, TColumn> where T : struct where TColumn : struct {
readonly TColumn _column;
readonly Color _increase_color, _decrease_color;
readonly TimeSpan _flash_duration;
readonly IComparer<T> _comparer;
T _last_value;
public DeltaTimeline(TColumn column, Color increase, Color decrease, TimeSpan flash_duration)
: this(column, increase, decrease, flash_duration, Comparer<T>.Default) { }
public DeltaTimeline(TColumn column, Color increase, Color decrease, TimeSpan flash_duration, IComparer<T> comparer) {
_column = column;
_increase_color = increase;
_decrease_color = decrease;
_flash_duration = flash_duration;
_comparer = comparer;
}
public IEnumerable<AnimationFrame<TColumn>> on_data(T value) {
int delta = _comparer.Compare(value, _last_value);
var temp = _last_value;
_last_value = value;
if (delta > 0) {
return AnimationFrameFactory.flash(_column,
new Animation(CellAnimationType.UpArrow, _increase_color));
} else if (delta < 0) {
return AnimationFrameFactory.flash(_column,
new Animation(CellAnimationType.DownArrow, _decrease_color));
} else {
return Enumerable.Empty<AnimationFrame<TColumn>>();
}
}
}
class WhereTrigger<T, TColumn> : ITriggerAnimation<T, TColumn> where T : struct, IComparable<T> where TColumn : struct {
readonly ITriggerAnimation<T, TColumn> _trigger;
readonly Func<T, bool> _predicate;
public WhereTrigger(ITriggerAnimation<T, TColumn> trigger, Func<T, bool> predicate) {
_trigger = trigger;
_predicate = predicate;
}
public IEnumerable<AnimationFrame<TColumn>> on_data(T value) =>
_predicate(value) ? _trigger.on_data(value) : Enumerable.Empty<AnimationFrame<TColumn>>();
}
class AppendTrigger<T, TColumn> : ITriggerAnimation<T, TColumn> where TColumn : struct {
readonly ITriggerAnimation<T, TColumn> _first, _second;
public AppendTrigger(ITriggerAnimation<T, TColumn> first, ITriggerAnimation<T, TColumn> second) {
_first = first;
_second = second;
}
public IEnumerable<AnimationFrame<TColumn>> on_data(T value) {
//_predicate(value) ? _first.on_data(value) : Enumerable.Empty<(ImmutableDictionary<SymbolKey<TColumn>, Animation> animation, TimeSpan timestamp)>();
var first_anims = _first.on_data(value);
var second_anims = _second.on_data(value);
var first_en = _first.on_data(value).GetEnumerator();
var second_en = _second.on_data(value).GetEnumerator();
while (first_en.MoveNext()) {
if (second_en.MoveNext()) {
while (first_en.Current.timestamp < second_en.Current.timestamp) {
yield return first_en.Current;
if (!first_en.MoveNext())
goto finish_with_second;
}
while (second_en.Current.timestamp < first_en.Current.timestamp) {
yield return second_en.Current;
if (!second_en.MoveNext())
goto finish_with_first;
}
if (first_en.Current.timestamp == second_en.Current.timestamp) {
var combined_anims = first_en.Current.animation;
foreach (var kv in second_en.Current.animation) {
if (!combined_anims.ContainsKey(kv.Key))
combined_anims = combined_anims.Add(kv.Key, kv.Value);
}
yield return new AnimationFrame<TColumn>(first_en.Current.timestamp, combined_anims);
}
} else {
goto finish_with_first;
}
}
if (second_en.MoveNext())
goto finish_with_second;
yield break;
finish_with_first:
do yield return first_en.Current;
while (first_en.MoveNext());
yield break;
finish_with_second:
do yield return second_en.Current;
while (second_en.MoveNext());
yield break;
}
}
class AnonymousAnimator<T, TColumn> : ITriggerAnimation<T, TColumn> where TColumn : struct {
readonly Func<T, IEnumerable<AnimationFrame<TColumn>>> _on_data;
public AnonymousAnimator(Func<T, IEnumerable<AnimationFrame<TColumn>>> on_data) =>
_on_data = on_data;
public IEnumerable<AnimationFrame<TColumn>> on_data(T value) =>
_on_data(value);
}
/// <summary>
/// When c
/// </summary>
/// <typeparam name="T"></typeparam>
/// <typeparam name="TColumn"></typeparam>
class IndefiniteFrameZipper<T, TColumn> : ITriggerAnimation<T, TColumn> where TColumn : struct {
readonly ITriggerAnimation<T, TColumn> _trigger;
readonly Func<T, ImmutableDictionary<SymbolKey<TColumn>, Animation>> _get_indefinite;
bool _active;
public IndefiniteFrameZipper(ITriggerAnimation<T, TColumn> trigger, Func<T, ImmutableDictionary<SymbolKey<TColumn>, Animation>> get_indefinite) {
_get_indefinite = get_indefinite;
_trigger = trigger;
}
public IEnumerable<AnimationFrame<TColumn>> on_data(T value) {
var rv = _trigger.on_data(value);
var indef = _get_indefinite(value);
if (indef != null && indef.Count > 0) {
_active = true;
if (rv == null || !rv.Any()) {
return new[] { new AnimationFrame<TColumn>(TimeSpan.Zero, indef) };
} else {
return rv.Select(f => new AnimationFrame<TColumn>(f.timestamp, f.animation.SetItems(indef))).ToList();
}
}
if (_active) {
_active = false;
if (rv == null || !rv.Any()) {
return new[] { new AnimationFrame<TColumn>(TimeSpan.Zero, ImmutableDictionary<SymbolKey<TColumn>, Animation>.Empty) };
} else {
return rv;
}
}
return rv;
}
}
}
static class DIYGridAnimationExtensions {
public static ITriggerAnimation<TOuter, TColumn> wrap<T, TOuter, TColumn>(this ITriggerAnimation<T, TColumn> timeline, Func<TOuter, T> selector) where TColumn : struct =>
new SelectorWrapper<T, TOuter, TColumn>(timeline, selector);
public static ITriggerAnimation<T, TColumn> aggregate<T, TAccum, TColumn>(this ITriggerAnimation<TAccum, TColumn> timeline, Func<TAccum, T, TAccum> accumulate, TAccum seed = default) where TColumn : struct =>
new AggregatorWrapper<T, TAccum, TColumn>(timeline, accumulate, seed);
public static ITriggerAnimation<T, TColumn> delta<T, TColumn>(this ITriggerAnimation<T, TColumn> animator, Func<T, T, T> get_delta) where TColumn : struct =>
new DeltaWrapper<T, TColumn>(animator, get_delta);
class ComparisonAnimation<T, TColumn> : ITriggerAnimation<T, TColumn> where T : struct, IComparable<T> where TColumn : struct {
readonly TColumn _column;
readonly Animation[] _animations;
readonly T[] _sorted_values;
IComparer<T> _comparer;
public ComparisonAnimation(TColumn column, params (T value, Animation animation)[] sorted_animation_buckets)
: this(column, Comparer<T>.Default, sorted_animation_buckets) { }
public ComparisonAnimation(TColumn column, IComparer<T> comparer, params (T value, Animation animation)[] sorted_animation_buckets) {
_column = column;
_comparer = comparer;
_animations = Array.ConvertAll(sorted_animation_buckets, a => a.animation);
_sorted_values = Array.ConvertAll(sorted_animation_buckets, a => a.value);
}
public IEnumerable<AnimationFrame<TColumn>> on_data(T value) {
int pos = Array.BinarySearch(_sorted_values, value);
if (pos < 0)
pos = ~pos;
var anim = pos == _animations.Length ? _animations[_animations.Length - 1] : _animations[pos];
yield return new AnimationFrame<TColumn>(
TimeSpan.Zero,
ImmutableDictionary<SymbolKey<TColumn>, Animation>.Empty.Add(new SymbolKey<TColumn>(_column, anim.type), anim));
}
}
class SelectorWrapper<T, TOuter, TColumn> : ITriggerAnimation<TOuter, TColumn> where TColumn : struct {
readonly ITriggerAnimation<T, TColumn> _timeline;
readonly Func<TOuter, T> _selector;
public SelectorWrapper(ITriggerAnimation<T, TColumn> timeline, Func<TOuter, T> selector) {
_timeline = timeline;
_selector = selector;
}
public IEnumerable<AnimationFrame<TColumn>> on_data(TOuter value) =>
_timeline.on_data(_selector(value));
}
class AggregatorWrapper<T, TAccum, TColumn> : ITriggerAnimation<T, TColumn> where TColumn : struct {
readonly ITriggerAnimation<TAccum, TColumn> _timeline;
readonly Func<TAccum, T, TAccum> _accumulate;
TAccum _seed;
public AggregatorWrapper(ITriggerAnimation<TAccum, TColumn> timeline, Func<TAccum, T, TAccum> accumulate, TAccum seed = default) {
_timeline = timeline;
_accumulate = accumulate;
_seed = seed;
}
public IEnumerable<AnimationFrame<TColumn>> on_data(T value) =>
_timeline.on_data(_seed = _accumulate(_seed, value));
}
class DeltaWrapper<T, TColumn> : ITriggerAnimation<T, TColumn> where TColumn : struct {
readonly Func<T, T, T> _get_delta;
readonly ITriggerAnimation<T, TColumn> _animator;
T _last_value;
public DeltaWrapper(ITriggerAnimation<T, TColumn> animator, Func<T, T, T> get_delta, T default_value = default) {
_animator = animator;
_get_delta = get_delta;
_last_value = default_value;
}
public IEnumerable<AnimationFrame<TColumn>> on_data(T value) {
T delta = _get_delta(_last_value, value);
_last_value = value;
return _animator.on_data(delta);
}
}
}
}
@@ -0,0 +1,26 @@
using bts.utils;
using System;
using CellAnimationType = mbts.ui.spark.CellAnimation.Type;
namespace bts.shell.pnl {
readonly struct SymbolKey<T> : IEquatable<SymbolKey<T>> where T : struct {
public readonly T column;
public readonly CellAnimationType type;
public SymbolKey(T column, CellAnimationType type) {
this.column = column;
this.type = type;
}
public bool Equals(SymbolKey<T> other) =>
column.Equals(other.column) &&
type == other.type;
public override bool Equals(object obj) =>
obj is SymbolKey<T> other && Equals(other);
public override int GetHashCode() =>
(column.GetHashCode() % MathUtils.Primes.P_65449) |
((type.GetHashCode() % MathUtils.Primes.P_65449) << 16);
}
}
@@ -1,26 +1,10 @@
using bts.collections;
using mbts.ui.utility;
using System;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
namespace mbts.spark.filters {
static class DataGridFilterHelper {
public static IDisposable hook_filters<TRow>(ObservableFilterCollection<TRow> rows, Action filter_action, params IDataGridColumnFilter<TRow>[] filters) =>
_hook_filters(filter_action, filters);
public static IDisposable hook_filters<TRow>(ObservableFilterCollection<TRow> rows, params IDataGridColumnFilter<TRow>[] filters) {
return _hook_filters(set_filter, filters);
void set_filter() {
using (rows.defer_refresh()) {
var active_filters = _get_active_filters(filters);
rows.live_filter_properties.SetItems(active_filters.Select(f => f.prop_name));
rows.filter = r => active_filters.All(f => f.filter(r));
}
}
}
public static IDisposable hook_filters<TRow>(ICollectionView rows, params IDataGridColumnFilter<TRow>[] filters) {
var icvls = (ICollectionViewLiveShaping)rows;
icvls.IsLiveFiltering = true;
@@ -1,4 +1,6 @@
using bts.api.core;
using bts;
using bts.api;
using bts.api.core;
using bts.collections;
using bts.utils;
using mbts.collections;
@@ -339,5 +341,47 @@ namespace mbts.spark.filters {
_selected_items.sync_with(_selected_items_snapshot.Intersect(_items));
}
}
public static class Common {
// Account
public static IDataGridColumnPersistentFilter<TRow, uint[]> account<TRow>(OrdersModel model, Func<TRow, AccountTuple> get_row_account, string prop_name) =>
account(model.admin_model.on_account_tuples, get_row_account, prop_name);
public static IDataGridColumnPersistentFilter<TRow, uint[]> account<TRow>(IObservable<IEnumerable<AccountTuple>> source, Func<TRow, AccountTuple> get_row_account, string prop_name) =>
Create(
source,
get_row_account,
vals => vals.Select(v => v.id).ToArray(),
vals => {
var id_set = new HashSet<uint>(vals.empty_if_null());
return source.Select(acc => acc.Where(a => id_set.Contains(a.id)));
},
prop_name);
public static IDataGridColumnPersistentFilter<TRow, uint[]> account<TRow>(IObservable<TableUpdate<AccountTuple, uint>> source, Func<TRow, AccountTuple> get_row_account, string prop_name) =>
Create(
source.Select(t => t.state.Values),
get_row_account,
vals => vals.Select(v => v.id).ToArray(),
vals => {
var id_set = new HashSet<uint>(vals.empty_if_null());
return source.Filter(acc => id_set.Contains(acc.id)).Select(t => t.state.Values);
},
prop_name);
// Exchange
public static IDataGridColumnPersistentFilter<TRow, Venue[]> exchange<TRow>(Func<TRow, Venue> get_row_exchange, string prop_name) =>
Create(EnumHelper.get_values_except(Venue.Unset).ToList(), get_row_exchange, prop_name);
// Symbol
public static IDataGridColumnPersistentFilter<TRow, string[]> symbol<TRow>(Func<TRow, string> get_row_symbol, string prop_name) =>
Create(co2.UserSymbol.get_all().Select(u => u.symbol).Distinct().ToList(), get_row_symbol, prop_name);
// Security Type
public static IDataGridColumnPersistentFilter<TRow, Security.ContractOneofCase[]> security_type<TRow>(Func<TRow, Security.ContractOneofCase> get_row_security_type, string prop_name) =>
Create(EnumHelper.get_values_except(
Security.ContractOneofCase.None,
Security.ContractOneofCase.Option,
Security.ContractOneofCase.Synthetic).ToList(),
get_row_security_type,
prop_name);
}
}
}
@@ -1,4 +1,5 @@
using ProtoBuf;
using bts.utils;
using ProtoBuf;
using System;
namespace mbts.spark.filters {
@@ -8,7 +9,7 @@ namespace mbts.spark.filters {
}
[ProtoContract]
class IntStatus : INullableStructProto<int> {
class IntStatus : INullableStructProto<int>, IEquatable<IntStatus> {
[ProtoMember(1)]
bool _active;
[ProtoMember(2)]
@@ -19,10 +20,23 @@ namespace mbts.spark.filters {
get => _active ? new int?(_value) : new int?();
set { if (_active = value.HasValue) _value = value.Value; }
}
public bool Equals(IntStatus other) =>
other != null &&
_active == other._active &&
_value == other._value;
public override bool Equals(object obj) =>
Equals(obj as IntStatus);
public override int GetHashCode() =>
EqualityComparer.aggregate_hash(
_active.GetHashCode(),
_value.GetHashCode());
}
[ProtoContract]
class IntMinMaxState : IMinMaxState<int> {
class IntMinMaxState : IMinMaxState<int>, IEquatable<IntMinMaxState> {
[ProtoMember(1)]
readonly IntStatus _minimum;
[ProtoMember(2)]
@@ -35,5 +49,19 @@ namespace mbts.spark.filters {
public INullableStructProto<int> minimum => _minimum;
public INullableStructProto<int> maximum => _maximum;
public bool Equals(IntMinMaxState other) =>
other != null &&
_minimum.safe_equals(other._minimum) &&
_maximum.safe_equals(other._maximum);
public override bool Equals(object obj) =>
Equals(obj as IntMinMaxState);
public override int GetHashCode() =>
EqualityComparer.aggregate_hash(
_minimum?.GetHashCode() ?? 0,
_maximum?.GetHashCode() ?? 0);
}
}
@@ -13,11 +13,13 @@
<Border BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Background="{TemplateBinding Background}">
<ItemsPresenter />
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
<ItemsPresenter/>
</ScrollViewer>
</Border>
</AdornerDecorator>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
</ResourceDictionary>
@@ -30,7 +30,7 @@
<HierarchicalDataTemplate DataType="{x:Type local:LaunchItem}">
<Button Style="{DynamicResource StartMenuButton}" Content="{Binding name}" Command="{Binding command}" CommandParameter="{Binding args}" MinWidth="100" Padding="4" />
</HierarchicalDataTemplate>
<HierarchicalDataTemplate DataType="{x:Type local:GroupItem}" ItemsSource="{Binding Value}" >
<HierarchicalDataTemplate DataType="{x:Type local:HierarchicalGroupItem}" ItemsSource="{Binding Value}" >
<TextBlock Text="{Binding Key}" Margin="4" />
</HierarchicalDataTemplate>
</TreeView.Resources>
@@ -35,10 +35,10 @@ namespace mbts.ui.controls {
private Dictionary<string, ObservableCollection<object>> _data;
public Dictionary<string, ObservableCollection<object>> Data { get { return _data; } set { _data = value; RaisePropertyChanged("InternalData"); } }
// A hack to provide a non-generic type for template binding
public IEnumerable<GroupItem> InternalData {
public IEnumerable<HierarchicalGroupItem> InternalData {
get {
if (_data == null) return null;
return _data.Select(k => new GroupItem() { Key = k.Key, Value = k.Value }); }
return _data.Select(k => new HierarchicalGroupItem() { Key = k.Key, Value = k.Value }); }
}
string _btext = string.Empty;
public string ButtonText { get { return _btext; } set { _btext = value; RaisePropertyChanged("ButtonText"); RaisePropertyChanged("ButtonVisibility"); } }
@@ -46,7 +46,7 @@ namespace mbts.ui.controls {
public ICommand Command { get; set; }
}
public class GroupItem
public class HierarchicalGroupItem
{
public string Key { get; set; }
public object Value { get; set; }
@@ -33,6 +33,7 @@
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Background="{TemplateBinding Background}"
Width="{TemplateBinding Width}"
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
VerticalAlignment="{TemplateBinding VerticalAlignment}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
@@ -20,6 +20,19 @@ namespace mbts.ui.controls {
/// Interaction logic for PriceEntryCommitControl.xaml
/// </summary>
public partial class PriceEntryCommitControl : UserControl {
public static readonly DependencyProperty RealPriceProperty =
WPFHelper.create_dp<double, PriceEntryCommitControl>(
nameof(RealPrice),
(o, v) => {
if (!double.IsNaN(v) && !o.IsKeyboardFocusWithin)
o.SetCurrentValue(SourceValueProperty, v);
},
double.NaN);
public double RealPrice {
get => (double)GetValue(RealPriceProperty);
set => SetValue(RealPriceProperty, value);
}
public static readonly DependencyProperty SourceValueProperty =
@@ -153,7 +153,7 @@
</Setter>
</Style>
<Style x:Key="SparkToolBar" TargetType="ToolBar">
<Style x:Key="SparkToolBarBase" TargetType="ToolBar">
<Setter Property="ToolBarTray.IsLocked" Value="True"/>
<Setter Property="Background" Value="LightGray"/>
<Setter Property="BorderBrush" Value="DarkGray"/>
@@ -162,7 +162,6 @@
<Setter Property="SnapsToDevicePixels" Value="True"/>
<Setter Property="OverridesDefaultStyle" Value="True"/>
<Setter Property="UseLayoutRounding" Value="True"/>
<Setter Property="ItemContainerStyle" Value="{StaticResource SparkToolBarItem}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ToolBar}">
@@ -223,4 +222,8 @@
</Setter.Value>
</Setter>
</Style>
<Style x:Key="SparkToolBar" TargetType="ToolBar" BasedOn="{StaticResource SparkToolBarBase}">
<Setter Property="ItemContainerStyle" Value="{StaticResource SparkToolBarItem}"/>
</Style>
</ResourceDictionary>
@@ -21,10 +21,12 @@ namespace mbts.ui.converters {
static class ValueConverters {
public static readonly IValueConverter
DoubleTextConverter = new DoubleTextConverter(),
DoubleGreaterThanZero = new DoubleGreaterThanConverter { Value = 0 },
IntTextConverter = new IntTextConverter(),
UIntTextConverter = new UIntTextConverter(),
QuantityMeasureTextConverter = ValueConverter.create<QuantityMeasureImmutable?, string>(fmt),
SimpleString = new AnonymousValueConverter(obj => obj?.ToString());
SimpleString = new AnonymousValueConverter(obj => obj?.ToString()),
ThicknessToDouble = new ThicknessToDoubleConverter();
public static readonly IMultiValueConverter
AreEqualConverter = new AreEqualConverter();
@@ -4,6 +4,7 @@ using System.Windows;
using System.Windows.Data;
namespace mbts.ui.converters {
[ValueConversion(typeof(double), typeof(Thickness))]
class DoubleToThicknessConverter : IValueConverter {
public double LeftMultiplier { get; set; }
public double TopMultiplier { get; set; }
@@ -15,4 +16,14 @@ namespace mbts.ui.converters {
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new NotImplementedException();
}
[ValueConversion(typeof(Thickness), typeof(double))]
class ThicknessToDoubleConverter : IValueConverter {
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
if (value is Thickness th)
return th.Left;
return 0.0;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new NotImplementedException();
}
}
@@ -0,0 +1,21 @@
using System;
using System.Globalization;
using System.Windows.Controls;
using System.Windows.Data;
namespace mbts.ui.converters {
[ValueConversion(typeof(double), typeof(bool))]
class DoubleGreaterThanConverter : IValueConverter {
public double Value { get; set; }
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
if (value is double d)
return d > Value;
return new ValidationResult(false, "Not an double.");
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) =>
throw new NotImplementedException();
}
}
@@ -1,4 +1,5 @@
using System;
using bts.utils;
using System;
using System.Globalization;
using System.Windows.Data;
using System.Windows.Media;
@@ -26,6 +27,7 @@ namespace mbts.ui.converters {
public static SignedDataStore<T> with_positive<T>(this SignedDataStore<T> store, T positive) => new SignedDataStore<T>(positive, store.neutral, store.negative, store.error);
public static SignedDataStore<T> with_neutral<T>(this SignedDataStore<T> store, T neutral) => new SignedDataStore<T>(store.positive, neutral, store.negative, store.error);
public static SignedDataStore<T> with_negative<T>(this SignedDataStore<T> store, T negative) => new SignedDataStore<T>(store.positive, store.neutral, negative, store.error);
public static SignedDataStore<T> with_error<T>(this SignedDataStore<T> store, T error) => new SignedDataStore<T>(store.positive, store.neutral, store.negative, error);
}
class NumericSignBrushConverter : IValueConverter {
@@ -35,6 +37,7 @@ namespace mbts.ui.converters {
public Color PositiveColor { get => _brushes.positive.Color; set => _brushes = _brushes.with_positive(new SolidColorBrush(value) { Opacity = _opacity }.frozen()); }
public Color NeutralColor { get => _brushes.neutral.Color; set => _brushes = _brushes.with_neutral(new SolidColorBrush(value) { Opacity = _opacity }.frozen()); }
public Color NegativeColor { get => _brushes.negative.Color; set => _brushes = _brushes.with_negative(new SolidColorBrush(value) { Opacity = _opacity }.frozen()); }
public Color ErrorColor { get => _brushes.error.Color; set => _brushes = _brushes.with_error(new SolidColorBrush(value) { Opacity = _opacity }.frozen()); }
double _opacity = 1.0;
public double Opacity {
@@ -42,19 +45,20 @@ namespace mbts.ui.converters {
set {
_opacity = value;
_brushes = new SignedDataStore<SolidColorBrush>(
_brushes.positive.with(value).frozen(),
_brushes.neutral.with(value).frozen(),
_brushes.negative.with(value).frozen(),
_brushes.error.with(value).frozen());
_brushes.positive.Clone().with(value).frozen(),
_brushes.neutral.Clone().with(value).frozen(),
_brushes.negative.Clone().with(value).frozen(),
_brushes.error.Clone().with(value).frozen());
}
}
public IComparable Zero { get; set; } = 0.0;
public IComparable Zero { get; set; } = new UniversalNumericComparer(0.0);
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
var comparable = value as IComparable;
if (comparable == null || comparable is double && double.IsNaN((double)value))
return _brushes.error;
int comp = comparable.CompareTo(Zero);
/// [reverse]compare with <see cref="Zero"/> bc it is the <see cref="UniversalNumericComparer"/>, and supports comparisons with most numeric types.
int comp = -Zero.CompareTo(comparable);
return comp == 0 ? _brushes.neutral : comp > 0 ? _brushes.positive : _brushes.negative;
}
@@ -1,4 +1,6 @@
using bts;
using bts.utils;
using mbts.ui.extensions;
using mbts.ui.shell;
using System;
using System.Diagnostics;
@@ -33,6 +35,39 @@ namespace mbts.ui {
private static DragDropDocumentPreview _dddp;
public static IObservable<Point> subscribe_drag_start(FrameworkElement element) =>
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;
return Disposable.Create(() => {
element.MouseDown -= on_mouse_down;
element.MouseMove -= on_mouse_move;
element.MouseUp -= on_mouse_up;
});
// callbacks
void on_mouse_up(object sender, MouseButtonEventArgs e) {
mouse_down = null;
drag_start = null;
}
void on_mouse_move(object sender, MouseEventArgs e) {
if (mouse_down.HasValue) {
var curr_pt = e.GetPosition(element);
if (is_sufficient_delta(mouse_down.Value, curr_pt)) {
e.Handled = true;
drag_start = curr_pt;
mouse_down = null;
o.OnNext(curr_pt);
}
}
}
void on_mouse_down(object sender, MouseButtonEventArgs e) {
drag_start = null;
mouse_down = e.GetPosition(element);
}
});
public static IObservable<Point?> subscribe_drag_coordinates(FrameworkElement element) =>
Observable.Create<Point?>(o => {
Point? mouse_down = null, drag_start = null;
@@ -70,6 +105,57 @@ namespace mbts.ui {
}
});
public static IDisposable connect_horizontal_insertion_adorner(ItemsControl container, FrameworkElement root = null) {
HorizontalInsertionAdorner insertion_adorner = null;
UIElement target = null;
TargetSide target_side = TargetSide.LEFT;
if (root == null)
root = container;
return Disposable.Create(
root.SubscribePreviewDragEnter(on_drag_enter),
root.SubscribePreviewDragLeave((s, e) => remove_adorner()),
root.SubscribePreviewDragOver(on_drag_over),
root.SubscribePreviewDrop((s, e) => remove_adorner()),
Disposable.Create(remove_adorner));
void on_drag_over(object sender, DragEventArgs e) {
_determine_drop_target(e);
_update_insertion_adorner();
e.Effects = DragDropEffects.Move;
e.Handled = true;
}
void on_drag_enter(object sender, DragEventArgs e) {
_determine_drop_target(e);
_update_insertion_adorner();
e.Effects = e.AllowedEffects;
}
// Draw an indicator where the drop will insert the item
void _update_insertion_adorner() {
if (insertion_adorner == null) {
insertion_adorner = new HorizontalInsertionAdorner(target, target_side);
} else insertion_adorner.Side = target_side;
}
void _determine_drop_target(DragEventArgs e) {
target = null;
var pt = e.GetPosition(container);
pt.X = MathUtils.within_range(0, container.ActualWidth - 2, pt.X);
pt.Y = MathUtils.within_range(0, container.ActualHeight - 2, pt.Y);
//if (e.OriginalSource is DependencyObject) {
if (container.InputHitTest(pt) is DependencyObject hit) {
target = container.ContainerFromElement(hit) as UIElement;
if (target != null) {
Point target_position = e.GetPosition(target);
target_side = target_position.X < (target.RenderSize.Width / 2) ? TargetSide.LEFT : TargetSide.RIGHT;
}
}
}
void remove_adorner() {
if (insertion_adorner != null) {
insertion_adorner.Detach();
insertion_adorner = null;
}
}
}
[Conditional("DEBUG")]
private static void log(string message, params object[] args) {
Console.WriteLine(message, args);
@@ -117,5 +117,26 @@ namespace mbts.ui.extensions {
tcs.TrySetResult(e);
}
}
public static IDisposable ConnectOnLoaded(this FrameworkElement element, Func<IDisposable> connect, bool disconnect_on_unloaded = true) {
IDisposable connection = null;
element.Loaded += on_loaded;
if (disconnect_on_unloaded)
element.Unloaded += on_unloaded;
return Disposable.Create(dispose);
void on_loaded(object sender, RoutedEventArgs e) =>
Disposable.replace(ref connection, connect());
void on_unloaded(object sender, RoutedEventArgs e) =>
Disposable.dispose(ref connection);
void dispose() {
Disposable.dispose(ref connection);
element.Loaded -= on_loaded;
if (disconnect_on_unloaded)
element.Unloaded -= on_unloaded;
}
}
}
}
Loaded 100 of 138 files, more files were not shown because too many files have changed in this diff. Show more