Compare commits

..
316 Commits
Author SHA1 Message Date
John Colvin 4a2007d60c check point before flying to chapel hill 2019-11-06 23:54:20 -06:00
John Colvin 49d0eb812b fix main 2019-11-06 23:23:52 -06:00
John Colvin ae49a8ed94 tweak HyperlinkCell focus/mouse capture logic 2019-11-06 22:43:35 -06:00
John Colvin 67a17a69eb clean up DIYGrid TextColumn (mimic recent changes in HyperlinkColumn) 2019-11-06 22:43:35 -06:00
John Colvin 9e74c0c4ae fix TextCell alignment issues 2019-11-06 22:43:35 -06:00
John Colvin df5409453e SP-257 mview working orders popup 2019-11-06 22:43:35 -06:00
John Colvin f9b682b1f8 hamburger spruce 2019-11-06 22:43:35 -06:00
John Colvin ae77d6f273 add 'Freeze' prop to BrushExtension 2019-11-06 22:43:35 -06:00
John Colvin f62f6fde5e add ITimeAndSalesGridSettingsMigrationRule 2019-11-06 22:43:35 -06:00
John Colvin 3109640723 DIYGridCellHoverAdorner 2019-11-06 22:43:35 -06:00
John Colvin a7755f7ee1 refactor DIYGrid.SelectionHighlight to be Color instead of Brush (also fixes drag rect adorner bug, where the color wouldn't respond to settings changes) 2019-11-06 22:43:35 -06:00
John Colvin 5ab6d6d8c0 HighPerformanceRow does not depend on TColumn 2019-11-06 22:43:35 -06:00
John Colvin a8e71de207 fix typo in ColumnGeometry (was assigning 'left' to 'unclicpped_left') 2019-11-06 22:43:34 -06:00
John Colvin 0254392a63 add 'HyperlinkClickMode' to HyperlinkCell to support cmd on click anywhere 2019-11-06 22:43:34 -06:00
John Colvin 55cc76e202 fix HyperlinkCell HorizontalAlignment binding 2019-11-06 22:43:34 -06:00
John Colvin af39a762cb market view row 'inside working orders' comparison to bbo was backwards 2019-11-06 22:43:34 -06:00
John Colvin 3b0e47b598 MarketViewVM does not derive from SecurityMDGrid 2019-11-06 22:43:29 -06:00
John Colvin 03f7766e4e clip mview animations to scroll content presnter bounds 2019-11-06 22:40:54 -06:00
John Colvin ecb5f4c781 factor out pnl auxiliary popups for general use (mview) 2019-11-06 22:40:54 -06:00
John Colvin f8e619d6c5 introduce WindowStyle.UnsafeWindow dp 2019-11-06 22:40:54 -06:00
John Colvin 28496ec58f OrderTypeItem template uses Rectangle instead of Badge (the latter has some unresolved rounding issues) 2019-11-06 22:40:54 -06:00
John Colvin 7a1a5f6db7 WPFHelper.measure_content() rounds to layout correctly 2019-11-06 22:40:54 -06:00
John Colvin bbab947fe7 fix WPFHelper.measure_visual_content - 'left edge' calculation for right align; 'bottom edge' for bottom align 2019-11-06 22:40:54 -06:00
John Colvin 2337a9f012 fix HyperlinkCell Alignment issues 2019-11-06 22:40:54 -06:00
John Colvin 455506ff7a DIYGrid CellRender.draw_text with layout rounding 2019-11-06 22:40:54 -06:00
Taha Afzal 4d15fdef8a Modified tests 2019-11-06 22:40:54 -06:00
Jake Norbut 7029b74916 removing OJ and KC IFU entries; regen cme ref data file 2019-11-06 22:40:54 -06:00
Jake Norbut 63b887b610 updating reference data with latest for RodrigoSGU and CNH 2019-11-06 22:40:54 -06:00
Michael Harding 4dac6dce7c Delete ex_sim_orders_venue_sim in system tests 2019-11-06 22:40:53 -06:00
Kevin Darby 3bc7afdec0 make sim datapaths pretty again 2019-11-06 22:40:53 -06:00
Eric Harding b2577fba64 Tool to test c# api latency 2019-11-06 22:40:53 -06:00
Taha Afzal 3ec6192e03 Dynamic Algos are now loaded only when explicitely enabled by name in Toggles config 2019-11-06 22:40:53 -06:00
Taha Afzal a1b2b765aa Hiding pyramid, ignoring timer (which was faking market status) 2019-11-06 22:40:53 -06:00
Taha Afzal 37830213b3 Added logs for TAS Algo 2019-11-06 22:40:53 -06:00
Taha Afzal 4c3aefc98a Clean up 2019-11-06 22:40:53 -06:00
Taha Afzal 23a07c444b Some cleanup remains, mostlty done 2019-11-06 22:40:53 -06:00
Taha Afzal a847afa216 Done 2019-11-06 22:40:53 -06:00
Taha Afzal 931d670a99 Corner cases fixed 2019-11-06 22:40:53 -06:00
John Colvin 65b1950a87 bring back DefaultAlgoVM for 'None' case (dynamic children placeholders do not have specific impl) 2019-11-06 22:40:53 -06:00
Taha Afzal 72712296fc made the child orders configurable 2019-11-06 22:40:53 -06:00
Taha Afzal 8bf04a8fb6 hedge is a lean now 2019-11-06 22:40:53 -06:00
Taha Afzal 64b4f6cae0 Top Spread is also a security (forgot to mark it as a security, so it was showing as a uint in the ui) 2019-11-06 22:40:53 -06:00
John Colvin 6749cf40fa add security map to view's resources for dynamic security key param's ContractPickerPopup 2019-11-06 22:40:53 -06:00
John Colvin 8dc575ab8e clean up DynamicOrderParamVM - don't store value with the descriptor, make 'param' prop readonly 2019-11-06 22:40:52 -06:00
John Colvin 32b46d57e3 refactor some Disposable.replace calls so that the prev hook is disposed before the new one is created 2019-11-06 22:40:52 -06:00
John Colvin 4e439763a1 dynamic order SecurityKey DataTemplate 2019-11-06 22:40:52 -06:00
John Colvin b678193576 AlgoStudio dynamic order 'SecurityKey' template 2019-11-06 22:40:52 -06:00
John Colvin dcc798b8c6 ladder/ticket/algo studio views store SecurityMap in resource dictionary 2019-11-06 22:40:52 -06:00
John Colvin 6eda42122a DynamicOrderVM 'SecurityKey' descriptor impl 2019-11-06 22:40:52 -06:00
John Colvin 6a119231d2 AlgoParamVM template key impl 2019-11-06 22:40:52 -06:00
John Colvin 737b6f7e30 add SelectedSecurityKey dp to ContractPickerPopup 2019-11-06 22:40:52 -06:00
John Colvin adda65357e migrate common ManualFills resource keys into central location 2019-11-06 22:40:52 -06:00
John Colvin e643768176 [non-functional] normalize whitespace in AlgoContentTemplateSelector.xaml 2019-11-06 22:40:52 -06:00
John Colvin 606e69e583 PyramidAlgo uses 'SecurityKey' dynamic param descriptor 2019-11-06 22:40:51 -06:00
John Colvin b07d2640ee server side impl for 'SecurityKey' dynamic param descriptor 2019-11-06 22:40:51 -06:00
John Colvin c11a69da72 add 'SecurityKey' dynamic param descriptor 2019-11-06 22:40:51 -06:00
Taha Afzal 1d0dc20063 price changes dont have the algo params filled in 2019-11-06 22:40:51 -06:00
Taha Afzal 61c4a28f89 Code complete 2019-11-06 22:40:51 -06:00
Taha Afzal 0eec5b30bb Code complete, need cleanup and testing 2019-11-06 22:40:51 -06:00
Taha Afzal 803e890e84 Adding logs 2019-11-06 22:40:51 -06:00
Taha Afzal 8f6140dada Organized the code better 2019-11-06 22:40:51 -06:00
Taha Afzal 4d8408fd7c odds and ends 2019-11-06 22:40:51 -06:00
Taha Afzal 7cca35373c payup logic implemented 2019-11-06 22:40:51 -06:00
Taha Afzal 85590d8d0d Fixed a bug with reporting 2019-11-06 22:40:51 -06:00
Taha Afzal 05aeb78026 Cleaning up 2019-11-06 22:40:50 -06:00
Taha Afzal f543a61758 Adding comments to SpreaderUtils 2019-11-06 22:40:50 -06:00
Taha Afzal ea850fcf59 Delete parameter added 2019-11-06 22:40:50 -06:00
Taha Afzal 8e93378b4e Some error handling (originating the algo from the wrong top spread) 2019-11-06 22:40:50 -06:00
Taha Afzal 0dd2cb2593 Need more robust error checking, functionality works 2019-11-06 22:40:50 -06:00
Taha Afzal 5eb89afe09 Almost done, need to hook up hedging (cover) and parameter change 2019-11-06 22:40:50 -06:00
Taha Afzal c1f6cb9b35 Basic scaffolding in place for pyramid algo, shows up in the UI 2019-11-06 22:40:50 -06:00
Kevin Darby 62cef7c2f7 release notes 2019-10-17 09:44:53 -04:00
Kevin Darby 62e97364fb Merged in ice_exec3 (pull request #230)
Ice execution

Approved-by: Kevin Darby <kevin.darby@bluetradesys.com>
2019-10-16 18:47:10 +00:00
Kevin Darby 0c5a039226 tested buffered cancel and replace with lean_controlled.txt, verified in fix 2019-10-11 14:04:32 -04:00
Kevin Darby 2677144bfa buffer cancel and updates because ice sucks 2019-10-10 17:43:28 -04:00
Kevin Darby 6b30c42c0d added gt_cert jetfix rule to conf per Pete 2019-10-10 14:56:32 -04:00
Kevin Darby 2f62d29f66 Merge branch 'master' of bitbucket.org:bluetradesys/spark into ice_exec3 2019-10-09 14:03:11 -04:00
John Colvin dc8e65923d fix adminModel typo '&&' -> '||' (users need admin OR trade permissions to see account) 2019-10-09 12:00:25 -05:00
Eric Harding 1469373b9c fix Sparkcmd -h help printer 2019-10-08 15:59:15 -04:00
Kevin Darby 820e937336 Merge branch 'master' of bitbucket.org:bluetradesys/spark into ice_exec3 2019-10-08 12:49:58 -04:00
John Colvin 8b73878782 null check for TradingPermissions (fixes login crash) 2019-10-08 11:46:32 -05:00
Kevin Darby b15106dac4 Merge branch 'master' of bitbucket.org:bluetradesys/spark into ice_exec3 2019-10-08 11:43:22 -04:00
Michael Harding 100316b791 SP-246: Fix System Tests 2019-10-08 11:06:41 -04:00
Kevin Darby 3c98425226 Merge branch 'master' of bitbucket.org:bluetradesys/spark into ice_exec3 2019-10-07 12:53:44 -04:00
John Colvin 1aa9c46367 fix build (bad using directives after merge) 2019-10-07 10:48:12 -05:00
Kevin Darby 5211cab351 Merge branch 'master' of bitbucket.org:bluetradesys/spark into ice_exec3 2019-10-07 10:24:36 -04:00
Pedro Pinto e89a4cce53 Merged in sim_prorata (pull request #350)
RTM. Documented OrderMatcher better.
2019-10-07 13:38:41 +00:00
Eric Harding cb56b90737 Merged in SP-229-shell-crash-dumps (pull request #346)
RTM: SP-229 shell crash dumps

Approved-by: Jake Norbut <jake.norbut@bluetradesys.com>
2019-10-07 13:38:00 +00:00
Pedro Pinto 761a89f052 Updated release notes 2019-10-07 09:37:00 -04:00
Pedro Pinto cab6b56606 Merged master 2019-10-07 09:36:21 -04:00
Pedro Pinto 8a743a91a5 Updated release notes. 2019-10-07 09:29:30 -04:00
Taha Afzal 5dec5dc392 Merged in SP-232 (pull request #345)
RTM. Fixing SP-232

Approved-by: Pedro Pinto <pedro.pinto+atlassian@bluetradesys.com>
2019-10-07 13:28:30 +00:00
Eric Harding 27aa4fca26 Merged in system-tests (pull request #349)
RTM: System tests

Approved-by: Michael Harding <michael.harding@bluetradesys.com>
Approved-by: Pedro Pinto <pedro.pinto+atlassian@bluetradesys.com>
2019-10-07 13:20:35 +00:00
Pedro Pinto 84a2e50bf7 Updated release notes. 2019-10-07 09:19:18 -04:00
John Colvin 297eb5b25b Merged in SP-181-no-spreads-in-man-fills (pull request #352)
RTM SP-181 Remove the option to enter manual fills of spreads

Approved-by: Jake Norbut <jake.norbut@bluetradesys.com>
2019-10-07 13:18:26 +00:00
Pedro Pinto d9d0e1f4ef Updated release notes. 2019-10-07 09:15:21 -04:00
Eric Harding 6ec0cc8bbe Merged in SP-236-websocketpp (pull request #353)
RTM: SP-236 Disable websocketpp open_handshake timeout
2019-10-07 13:13:30 +00:00
Pedro Pinto 795b77ea3f Updated release notes. 2019-10-07 09:12:45 -04:00
John Colvin 91c97d32bb Merged in SP-223-som-synth-crash (pull request #355)
RTM add dispatcher thread name to assert log

Approved-by: Eric Harding <eric.harding@bluetradesys.com>
2019-10-07 13:11:57 +00:00
John Colvin e0c50703d7 Merged in SPARK-2534-fills-time-filter-bug (pull request #357)
RTM SPARK-2534 fills time filter bug

Approved-by: Jake Norbut <jake.norbut@bluetradesys.com>
2019-10-07 13:11:03 +00:00
Pedro Pinto dab1e76766 Updated release notes. 2019-10-07 09:10:32 -04:00
Pedro Pinto b8c927df27 Merge branch 'master' into SPARK-2534-fills-time-filter-bug 2019-10-07 09:02:25 -04:00
Pedro Pinto 2684d25f09 Updated release notes 2019-10-07 09:01:48 -04:00
John Colvin 7c95a77ae2 Merged in update-naggle-test (pull request #360)
RTM unit test for update.naggle fix
2019-10-07 13:00:06 +00:00
John Colvin 31b115d13b Merged in bts-admin-permissions (pull request #356)
RTM Bts admin permissions
2019-10-07 12:58:42 +00:00
Pedro Pinto d681a6924b Updated release notes. 2019-10-07 08:39:01 -04:00
Pedro Pinto d0cce84d2e Merge branch 'master' into bts-admin-permissions 2019-10-07 08:38:07 -04:00
Pedro Pinto 532591c34f Updated release notes. 2019-10-07 08:36:14 -04:00
John Colvin 7f187224c1 add .Replay(1).RefCount() to AdminModel.on_accounts 2019-10-02 13:40:29 -05:00
John Colvin 79b2121f5e OrderBookVM disposes order_mod_vm 2019-10-02 13:40:28 -05:00
John Colvin 0bd477594c FillsVM disposes filters 2019-10-02 13:36:58 -05:00
John Colvin 149cd90fb4 apply permission filter to pnl stream 2019-10-02 11:54:38 -05:00
John Colvin f74e719a9f sort accounts/users in filter dropdowns for OrderBook/Fills/PNL 2019-10-02 11:54:38 -05:00
John Colvin 8b68beae96 Merge remote-tracking branch 'origin/master' into bts-admin-permissions 2019-10-02 10:52:45 -05:00
Pedro Pinto 9cc0b471f2 Added support for tag 1031, defaults to Y. 2019-10-02 10:32:23 -04:00
Kevin Darby 50e61604ba move default keycode exp to 12/31/2019 to fix unit tests 2019-09-30 11:53:44 -04:00
Kevin Darby 6bd23f106f Merged in fix_nan_pnl (pull request #361)
add temporary config to ignore nan day pnl to stop the loss limit from keeping traders out of market
2019-09-30 15:07:11 +00:00
Kevin Darby dfd8348724 add temporary config to ignore nan day pnl to stop the loss limit from keeping traders out of market 2019-09-30 11:03:03 -04:00
John Colvin d2e9352e48 unit test for update.naggle fix 2019-09-25 12:01:31 -05:00
Kevin Darby da42e95754 fixed externals and docker 2019-09-25 10:59:58 -04:00
Kevin Darby a878a55ac1 moved externals 2019-09-25 10:00:20 -04:00
John Colvin 4305d560aa Merged in pnl-rounding-error (pull request #359)
RTM Pnl rounding error

Approved-by: Jake Norbut <jake.norbut@bluetradesys.com>
Approved-by: Eric Harding <eric.harding@bluetradesys.com>
2019-09-25 13:57:52 +00:00
John Colvin 055f0015e7 Update.naggle pr edit: 'TryPop' is an innappropriate name (just use 'Remove') 2019-09-24 17:28:06 -05:00
Kevin Darby a39d91ceb3 Merge branch 'pnl-rounding-error' of bitbucket.org:bluetradesys/spark into ice_exec3 2019-09-24 14:38:26 -04:00
John Colvin ddc50e1fba pnl initial calc type was not being applied to data 2019-09-24 13:32:54 -05:00
John Colvin ffb571d1cf fix pnl rounding issues (use 9 decimal places) 2019-09-24 13:32:53 -05:00
John Colvin 2a36c05d75 fix PNLRowData Equals impl (was not comparing market prices) 2019-09-24 13:32:53 -05:00
John Colvin a3730eb203 Update.naggle merge maintains changes (could get into state where changes became adds/removes) 2019-09-24 13:32:53 -05:00
John Colvin d033275694 remove unused member OrdersModel.on_account_pnl 2019-09-24 13:32:53 -05:00
Jake Norbut 016ddf9b69 fixing pay less definition 2019-09-24 08:21:56 -05:00
John Colvin 0cc5817932 expand synthetic leg columns to fit name 2019-09-24 01:04:28 -05:00
Michael Harding 032836dea4 Test updates 2019-09-23 15:54:33 -04:00
Kevin Darby a06461eb5e Merge branch 'master' of bitbucket.org:bluetradesys/spark into ice_exec3 2019-09-23 13:58:22 -04:00
Kevin Darby 34e7d6a617 remove ice bust handling 2019-09-23 13:57:43 -04:00
Kevin Darby 31e44d8c10 Merge branch 'master' of bitbucket.org:bluetradesys/spark into ice_exec3 2019-09-23 13:03:20 -04:00
Kevin Darby 8fdab4a510 note about expired securities in sod tests 2019-09-23 12:47:48 -04:00
Kevin Darby 385adaa1b0 Merge branch 'fix_expired_fill_crash' of bitbucket.org:bluetradesys/spark into ice_exec3 2019-09-23 12:36:16 -04:00
Kevin Darby f9506bc9ad Merge branch 'master' of bitbucket.org:bluetradesys/spark into ice_exec3 2019-09-23 12:36:00 -04:00
Kevin Darby 6b5b517d70 slight nit in GLBXExecutionGateway 2019-09-23 12:29:38 -04:00
John Colvin 80850146c1 TableUpdate.AggregateLatest shouldn't share seed across subscriptions 2019-09-23 00:08:12 -05:00
John Colvin 0680dddab4 normalize line endings in TickUpDown.xaml 2019-09-22 15:22:57 -05:00
John Colvin 70aa62c0cf add border to DateTimePicker 'time' part view 2019-09-22 15:22:20 -05:00
John Colvin 06165b3842 SPARK-2534 Fills Form seems to always have SOD orders in there 2019-09-22 15:06:03 -05:00
John Colvin f21902d60d remove unused files AdminModelWrapper/PermissionFilter.cs 2019-09-22 14:46:52 -05:00
John Colvin a27f383b62 remove LoginContainer MEF Export consts (replace usages with UserTuple) 2019-09-22 12:58:46 -05:00
John Colvin 35d4fcc513 make UserTuple public (it's referenced in multiple assemblies) 2019-09-22 12:58:46 -05:00
John Colvin 8a96b00f64 add 'User' column to Synthetic Management window 2019-09-22 12:58:46 -05:00
John Colvin 2d06fd3d92 [non-functional] style cop/whitespace changes to SyntheticManagementDialogVM.cs 2019-09-22 12:58:45 -05:00
John Colvin 76050e0722 SyntheticManagement vm uses 'DataGridFilterHelper.hook_all' method to eliminate risk of omitting column filters 2019-09-22 12:58:45 -05:00
John Colvin a3f0c2fbf6 pass UserTuple to SyntheticManagement row (for imminent user column), remove dependency on securitymd 2019-09-22 12:58:45 -05:00
John Colvin 1ccc4c3746 add 'ShowLadderAutoLookupTag' to eschew securitymd depependency 2019-09-22 12:58:45 -05:00
John Colvin e5d85db888 SP-151 BTS user can view/edit all synths/algos 2019-09-22 12:11:15 -05:00
John Colvin e47b1c3048 [non-functional] style cop/whitespace changes in MDSource/Daemon.cs 2019-09-22 12:11:15 -05:00
John Colvin 039791a296 simplify AdminModel, api 'permission filters' use new UserTuple.Permissions enum 2019-09-22 12:11:15 -05:00
John Colvin e6884b6654 add 'Permissions' enum to UserTuple 2019-09-22 11:39:26 -05:00
John Colvin 8cbae28dd8 move UserTuple.cs into spark_api 2019-09-22 11:39:26 -05:00
John Colvin 010a1fecb1 remove unused function 'OrderFactory.subscribe_user_tuples' 2019-09-22 11:39:26 -05:00
Kevin Darby 96bec55ee7 fix build, we will have to handle these ice cpps someplace else 2019-09-20 15:01:56 -04:00
Kevin Darby 424bb36fc7 update jakes script for ice cpps 2019-09-20 14:30:27 -04:00
Kevin Darby c24700195f fixed cpp coming from market data for ice, added the obligatory overrides 2019-09-20 14:15:27 -04:00
Jake Norbut f7184b0949 updating tests 2019-09-20 09:13:01 -05:00
Eric Harding 999973c349 upgrade websocketpp 0.7.0 to 0.8.1 (externals) 2019-09-19 14:59:09 -04:00
Eric Harding 4663ae9e4d SP-236 Disable websocketpp open_handshake timeout 2019-09-19 14:38:00 -04:00
Kevin Darby 34fc6af137 fix mview test 2019-09-19 14:17:53 -04:00
Kevin Darby 6cc25a7337 Merge branch 'master' of bitbucket.org:bluetradesys/spark into ice_exec3 2019-09-19 13:56:32 -04:00
Kevin Darby 48b62f1cab new externals 2019-09-19 13:40:01 -04:00
Kevin Darby 5a0ceafa66 Merge branch 'master' of bitbucket.org:bluetradesys/spark into ice_exec3 2019-09-19 13:28:31 -04:00
Kevin Darby d31969606e Merge branch 'master' of bitbucket.org:bluetradesys/spark into ice_exec3 2019-09-19 13:27:52 -04:00
John Colvin d7d170f228 SP-181 Remove the option to enter manual fills of spreads 2019-09-19 11:23:36 -05:00
Eric Harding 1d538b4cb0 Code review feedback. Checked DeleteValue 2019-09-18 17:04:38 -04:00
Pedro Pinto b42febdf60 Documented OrderMatcher better. 2019-09-18 16:11:16 -04:00
Eric Harding 71915de4b0 sparkcmd CaseInsensitiveEnumValues 2019-09-18 15:47:53 -04:00
Kevin Darby facb3249d0 Merge branch 'minimal_api_tests' of bitbucket.org:bluetradesys/spark into ice_exec3 2019-09-18 15:24:51 -04:00
Kevin Darby f323efc61a Merge branch 'master' of bitbucket.org:bluetradesys/spark into ice_exec3 2019-09-18 15:24:14 -04:00
Eric Harding be5f54326e sparkcmd test should always respect test type (unit/system) 2019-09-18 15:16:28 -04:00
Michael Harding 65cf042639 Merge branch 'system-tests' into ice_exec3 2019-09-18 13:45:26 -04:00
Michael Harding 57526a5df5 merge system-tests into ice_exec3 2019-09-18 13:12:38 -04:00
Michael Harding 7d26ff641e more ports 2019-09-18 12:20:26 -04:00
Michael Harding a8ffe3a6ec logging ports 2019-09-18 11:20:13 -04:00
Michael Harding bda135c435 Add more logging to system tests 2019-09-18 10:12:40 -04:00
Kevin Darby 0fa97aeb56 added a service call to clear the auth sessions list in case of keycode trouble 2019-09-17 16:43:24 -04:00
Kevin Darby d1422ddc58 fix auth tests 2019-09-17 16:36:16 -04:00
Kevin Darby 51ffa7e7f7 set_mic for synthetics 2019-09-17 16:15:17 -04:00
Kevin Darby 2a89c3721b Merge branch 'master' of bitbucket.org:bluetradesys/spark into ice_exec3 2019-09-17 15:20:57 -04:00
Kevin Darby 0156bbfc46 fix id corner case 2019-09-17 15:06:04 -04:00
Taha Afzal f89fb76cb2 Merge branch 'master' into SP-232 2019-09-17 14:11:26 -04:00
Taha Afzal 16adb9ee18 Fixing SP-232 2019-09-17 14:09:34 -04:00
Eric Harding 154a637ee1 Merge branch 'master' of bitbucket.org:bluetradesys/spark into SP-229-shell-crash-dumps 2019-09-16 17:34:36 -04:00
Kevin Darby 8b583fcbec Merge branch 'fix_server_bounce' of bitbucket.org:bluetradesys/spark into ice_exec3 2019-09-16 17:11:00 -04:00
Kevin Darby 48dfded132 Merge branch 'master' of bitbucket.org:bluetradesys/spark into ice_exec3 2019-09-16 17:09:52 -04:00
Kevin Darby baaa3c4f98 Fixed case where a non-super user shell denied market data after bring left up through a server restart 2019-09-16 16:28:53 -04:00
Kevin Darby 838550d78a entitlements are tricky 2019-09-16 16:15:38 -04:00
Kevin Darby bf8323e79b fixed ice entitlements 2019-09-16 15:53:50 -04:00
Eric Harding 399e4bf654 SP-229 Detect unclean shell shutdown and automatically upload an error report 2019-09-16 13:48:12 -04:00
Eric Harding 5908b4478e Error reports should be logged on the client 2019-09-16 13:20:03 -04:00
Kevin Darby e6ab7d825a fix fs build 2019-09-16 11:15:56 -04:00
Kevin Darby c78e6e8315 rather large master -> ice merge 2019-09-16 10:43:19 -04:00
Eric Harding 44571a5af5 spark shell should dispose daemons 2019-09-16 10:41:58 -04:00
Eric Harding 62274f5129 Append windows event log and crash dumps to error reports 2019-09-12 17:18:47 -04:00
Eric Harding b5b5101faf SP-229 Allow ErrorReportService to accept an arbitrary list of files 2019-09-12 17:17:57 -04:00
Eric Harding bb232d8f1e Add easy convert to immutable function c# 2019-09-12 17:17:15 -04:00
John Colvin aaca502500 add dispatcher thread name to assert log 2019-09-09 11:42:11 -05:00
Kevin Darby 7d2695e602 Merge branch 'master' of bitbucket.org:bluetradesys/spark into ice_exec3 2019-09-06 11:46:40 -04:00
Eric Harding 14cbd4a174 ICE: make settlement price conditional on onMarketSnapshot 2019-08-19 16:30:37 -04:00
Kevin Darby cc3353e243 messed up the externals 2019-08-14 15:42:16 -04:00
Kevin Darby 683b85c66b subrepos 2019-08-14 15:25:27 -04:00
Kevin Darby 789710d841 Merge branch 'master' of bitbucket.org:bluetradesys/spark into ice_exec3 2019-08-14 15:25:19 -04:00
Kevin Darby 1a9ff09f43 Merge branch 'master' of bitbucket.org:bluetradesys/spark into ice_exec3 2019-08-08 16:58:31 -04:00
Michael Harding 13b09ffc12 add 3 minute delay to system tests 2019-08-07 10:42:41 -04:00
Kevin Darby 83a2a01789 Merge branch 'master' of bitbucket.org:bluetradesys/spark into ice_exec3 2019-08-06 12:06:46 -04:00
Kevin Darby e1137c4e1d go back to statically linked CME 2019-08-06 11:27:38 -04:00
Kevin Darby 0a6cc0cdf1 Merge branch 'master' of bitbucket.org:bluetradesys/spark into ice_exec3 2019-08-01 15:11:04 -04:00
Kevin Darby e96d9112ef SIMICE causing some trouble sharing replay and MBO 2019-08-01 14:25:17 -04:00
Kevin Darby 72aa028bd6 ice orders works again 2019-07-04 14:31:57 -04:00
Kevin Darby f8d5b9bace bookmark initial clordid 2019-07-03 18:20:11 -04:00
Kevin Darby e861e2916f Merge branch 'ice_exec3' of bitbucket.org:bluetradesys/spark into ice_exec3 2019-07-03 17:28:03 -04:00
Kevin Darby 23738b109d changes to use non-static onixs 2019-07-03 17:26:46 -04:00
Pedro Pinto 713e11d146 Merge branch 'ice_exec3' of bitbucket.org:bluetradesys/spark into ice_exec3 2019-07-03 16:26:34 -04:00
Pedro Pinto 444e3154b3 Made exchange id gen asynchronous, improved concurrency. 2019-07-03 16:26:07 -04:00
Kevin Darby 270ee60df9 nonstatic cme 2019-07-03 19:39:22 +00:00
Kevin Darby 1ce041c5e5 called Pedro's new id generator 2019-07-03 15:23:38 -04:00
Pedro Pinto 50940bfd13 Merged 2019-07-03 13:46:48 -04:00
Pedro Pinto 8cb4151f63 Added id generator to order services. 2019-06-30 20:27:48 -04:00
Kevin Darby 7a47b7a85a Merge branch 'master' of bitbucket.org:bluetradesys/spark into ice_exec3 2019-06-28 13:08:44 -04:00
Pedro Pinto a60a792dfa Ran clang-format. 2019-06-27 14:17:08 -04:00
Pedro Pinto a73a1cc4f5 CMakeDeps updates. 2019-06-27 14:16:05 -04:00
Pedro Pinto 32fbdf8a21 Better handling of unresolved securities. 2019-06-27 14:15:40 -04:00
Pedro Pinto 75c54bbe3e Refactored order factory. 2019-06-27 14:11:40 -04:00
Pedro Pinto 38575039b9 Fixed clang build. 2019-06-27 14:11:23 -04:00
Kevin Darby 43a49b9399 Merge branch 'master' of bitbucket.org:bluetradesys/spark into ice_exec3 2019-06-10 10:40:56 -04:00
Kevin Darby 618c41ce97 synthesized Pedro's cme changes for ice 2019-06-07 17:49:33 -04:00
Kevin Darby bd8c9c354b Merge branch 'master' of bitbucket.org:bluetradesys/spark into ice_exec3 2019-06-07 17:49:11 -04:00
Kevin Darby 8f5d062a3b Merge branch 'master' of bitbucket.org:bluetradesys/spark into ice_exec3 2019-05-21 16:58:35 -04:00
Kevin Darby e1a3950051 Merge branch 'master' of bitbucket.org:bluetradesys/spark into ice_exec3 2019-05-20 17:04:53 -04:00
Kevin Darby 7e816c1503 Merge branch 'master' of bitbucket.org:bluetradesys/spark into ice_exec3 2019-05-16 17:56:07 -04:00
Kevin Darby 15d9afb2de added a comment about ice reverse_routing_record 2019-05-16 15:26:21 -04:00
Kevin Darby b5468e5565 changes for ice certification perf testing and to recover an order from an exec report 2019-05-16 15:02:04 -04:00
Kevin Darby a4c3a61f13 fixed ice specific bug with attach detach 2019-05-14 17:55:52 -04:00
Kevin Darby 7ce101f7f7 messed up docker linux pointer 2019-05-14 16:41:49 -04:00
Kevin Darby b61cab397d Merge branch 'master' of bitbucket.org:bluetradesys/spark into ice_exec3 2019-05-14 16:36:12 -04:00
Kevin Darby 222e1a51dc fixed SOD time for dev
fixed bugs in mview when settle/last/low/high change without a bid or offer changing
2019-05-14 15:50:53 -04:00
Kevin Darby 9fa3464188 ice busts and corrects, ugh... 2019-05-14 09:55:05 -04:00
Kevin Darby fee695d3d9 Merge branch 'SPARK-1944-attach/detach/update' of bitbucket.org:bluetradesys/spark into ice_exec3 2019-05-13 16:02:56 -04:00
Kevin Darby 140f1d88ea ice entitlements 2019-05-05 16:41:13 -04:00
Kevin Darby c4eeaf2ec8 this should do it for bust / adjust 2019-05-03 16:28:37 -04:00
Kevin Darby 54dd30245f Merge branch 'SPARK-1944-attach/detach/update' of bitbucket.org:bluetradesys/spark into ice_exec3 2019-05-03 15:54:07 -04:00
Kevin Darby 263ff63a37 added concept of active and busted fill to support ice busts 2019-05-03 15:31:23 -04:00
Kevin Darby f3d16502de going a new way on the busts 2019-05-03 13:39:35 -04:00
Kevin Darby 4b7db298c4 something up with settlement re-price 2019-05-03 10:39:54 -04:00
Kevin Darby 65f41452fb work around onixs assert for us financials that does not have an implieds feed 2019-05-02 18:33:54 -04:00
Kevin Darby d7a2ae4b09 added spread marker to last trade display....lotta code for an S 2019-05-02 17:55:46 -04:00
Kevin Darby 61ac626542 ice wants tag 1031 2019-05-02 12:36:14 -04:00
Michael Harding 8d7a2f342e Add Custom Order Instructions 2019-05-02 12:14:50 -04:00
Kevin Darby 0db037175f fixed ice forced logout issue 2019-05-02 12:07:09 -04:00
Kevin Darby 98aa463b4e Merge branch 'SPARK-1944-attach/detach/update' of bitbucket.org:bluetradesys/spark into ice_exec3 2019-05-02 11:51:28 -04:00
Kevin Darby 0b40d3971d lining up with ice downtime 2019-05-01 14:05:02 -04:00
Kevin Darby a21a769909 cleaned up todos 2019-04-30 17:43:23 -04:00
Kevin Darby ca8e540453 volume is tricky 2019-04-30 15:09:53 -04:00
Kevin Darby 134950747f bugs found during cert 2019-04-30 14:36:54 -04:00
Kevin Darby 5abc17adf3 settles 2019-04-29 17:58:53 -04:00
Kevin Darby f3cd3a402e ice spread config, workaround for onixs bug 2019-04-29 16:53:50 -04:00
Kevin Darby c7217b8475 Merge branch 'SPARK-1944-attach/detach/update' of bitbucket.org:bluetradesys/spark into ice_exec3 2019-04-29 16:47:26 -04:00
Kevin Darby 99c5129ea7 ice spreads are weird 2019-04-29 16:25:54 -04:00
Kevin Darby d025ee0c89 seems to be an onixs bug that exits a loop early if not provisioned for a market_type 2019-04-29 15:09:56 -04:00
Kevin Darby c24ddaa190 Merge branch 'SPARK-1944-attach/detach/update' of bitbucket.org:bluetradesys/spark into ice_exec3 2019-04-29 10:21:39 -04:00
Kevin Darby 442aa156ac trying to get orance juice market 2019-04-29 10:20:27 -04:00
Kevin Darby e906d41cc2 Merge branch 'SPARK-1944-attach/detach/update' of bitbucket.org:bluetradesys/spark into ice_exec3 2019-04-26 14:48:20 -04:00
Kevin Darby 6e09d9e949 small changes for ice cert 2019-04-26 14:35:36 -04:00
Kevin Darby aea02e212a cleaned up cancel / reject messages 2019-04-25 16:36:07 -04:00
Kevin Darby a94a66234a ice puids 2019-04-25 15:57:04 -04:00
Kevin Darby 4cbbf6972a add, update, remove orders work across logouts 2019-04-25 15:25:07 -04:00
Kevin Darby caa9671c96 ids... 2019-04-24 18:37:45 -04:00
Kevin Darby 20eb3ed39d matched Pedro's ICE deferred cancel / update for ice 2019-04-24 14:07:21 -04:00
Kevin Darby 772a898433 Merge branch 'SPARK-1944-attach/detach/update' of bitbucket.org:bluetradesys/spark into ice_exec3 2019-04-24 14:07:09 -04:00
Kevin Darby b5997d326e f# 64 bit ints, maybe 2019-04-23 18:22:44 -04:00
Kevin Darby f8626f81cd throwing this back to michael for int64 2019-04-23 18:14:14 -04:00
Kevin Darby bc208d2421 Merge branch 'ice_exec3' of bitbucket.org:bluetradesys/spark into ice_exec3 2019-04-23 17:22:26 -04:00
Kevin Darby 625ebcc762 got a fill 2019-04-23 17:21:52 -04:00
Michael Harding 0a163badf0 SPARK-2289: Add MIFID fields to Spark Admin 2019-04-23 15:45:11 -04:00
Kevin Darby dea95455a0 Merge branch 'ice_exec3' of bitbucket.org:bluetradesys/spark into ice_exec3 2019-04-23 13:28:45 -04:00
Kevin Darby f0f5988abf almost sending orders 2019-04-23 13:27:50 -04:00
Michael Harding db5f16985c SPARK-2289: Remove IFUnset from ICEMics 2019-04-23 11:00:38 -04:00
Michael Harding f0105867be SPARK-2289: Implement Eric's Map suggestion 2019-04-23 10:03:58 -04:00
Michael Harding 2c0b314428 SPARK-2289: Additional ICE fields 2019-04-23 10:03:12 -04:00
Kevin Darby 3cd74f33f1 changed trader login per Pedro 2019-04-22 16:51:28 -04:00
Kevin Darby a6ee36f4bc typo 2019-04-22 15:16:12 -04:00
Kevin Darby 9cadf635fb Merge branch 'ice_exec3' of bitbucket.org:bluetradesys/spark into ice_exec3 2019-04-22 14:42:10 -04:00
Kevin Darby 43e043b044 more ice 2019-04-22 14:41:39 -04:00
Michael Harding 6bac75d7ab SPARK-2289: Add ICE to Accounts in Spark Admin 2019-04-22 10:58:26 -04:00
Michael Harding 7ceb170c5f SPARK-2289: Add ICE User data to Spark Admin 2019-04-22 10:36:35 -04:00
Kevin Darby 47c7ee71b4 Merge branch 'SPARK-1944-attach/detach/update' of bitbucket.org:bluetradesys/spark into ice_exec3 2019-04-18 17:46:34 -04:00
Kevin Darby 4f58452771 stop using legacy spread 2019-04-17 18:14:00 -04:00
Kevin Darby d5bf15cc36 ice secondary login 2019-04-17 17:12:25 -04:00
Kevin Darby 9736541385 logon rejected; we're going places 2019-04-16 15:59:45 -04:00
Kevin Darby 5591c8316d about to send a login message 2019-04-16 13:18:39 -04:00
Kevin Darby a44d3a2c26 Merge branch 'master' of bitbucket.org:bluetradesys/spark into ice_exec3 2019-04-15 13:22:42 -04:00
Kevin Darby dceb300324 Merge branch 'SPARK-2241-add-support-for-multi-char-me' of bitbucket.org:bluetradesys/spark into ice_exec3 2019-04-15 13:20:56 -04:00
Pedro Pinto 84b9ca3096 Added jetfix suppport for multi char message types. 2019-04-08 17:07:40 -04:00
Pedro Pinto 17e9f1ea96 Ran clang-format. 2019-04-08 13:43:20 -04:00
Kevin Darby 946ec36840 Merge branch 'SPARK-1944-attach/detach/update' of bitbucket.org:bluetradesys/spark into ice_exec3 2019-04-06 22:18:21 -04:00
Kevin Darby 11425a898f honing in on simice 2019-04-05 17:58:28 -04:00
Kevin Darby d0029e7f88 sim doesn't crash, but doesn't give exec reports either 2019-04-05 17:39:04 -04:00
Kevin Darby bdbdda947f Merge branch 'SPARK-1944-attach/detach/update' of bitbucket.org:bluetradesys/spark into ice_exec3 2019-04-05 15:51:51 -04:00
Kevin Darby f88ab57fa0 got the basic structure down, still waiting on ICE for a few things 2019-04-05 15:49:37 -04:00
Kevin Darby 36442a037e more ice 2019-04-03 17:12:52 -04:00
Kevin Darby 5969568dee ice native order 2019-04-02 17:36:12 -04:00
Kevin Darby 96265ea187 Merge branch 'SPARK-1944-attach/detach/update' of bitbucket.org:bluetradesys/spark into ice_exec3 2019-04-02 17:34:54 -04:00
Kevin Darby 9676b1fdca Merge branch 'SPARK-1944-attach/detach/update' of bitbucket.org:bluetradesys/spark into ice_exec3 2019-04-02 16:21:51 -04:00
Kevin Darby 556132c2dd ice exec moving along 2019-04-01 18:11:48 -04:00
Kevin Darby a22b7b7189 more ice 2019-03-29 16:52:50 -04:00
Kevin Darby 8e41a42ced more ice 2019-03-28 16:18:10 -04:00
Kevin Darby e0f7bdfc13 fix conf 2019-03-27 17:42:23 -04:00
Kevin Darby ec0b96215f Merge branch 'SPARK-1944-attach/detach/update' of bitbucket.org:bluetradesys/spark into ice_exec 2019-03-27 17:29:32 -04:00
Kevin Darby e27a8be526 ICEExecutionGateway 2019-03-27 17:01:07 -04:00
Kevin Darby 70cd9f185e Merge branch 'ice_md' of bitbucket.org:bluetradesys/spark into ice_exec 2019-03-26 18:44:16 -04:00
Kevin Darby 33eaf6f86d beginnings of ICE exec and SIMICE 2019-03-26 18:38:05 -04:00
321 changed files with 11248 additions and 2771 deletions

No files matched your search

@@ -154,7 +154,7 @@ insert_anchor_links = "right"
- Pay Less - If enabled by checking the checkbox, the spreader will
attempt to pay less when the quantity at the level of the calculated
hedge price is more than the threshold defined.  The hedge order is
hedge price is less than the threshold defined.  The hedge order is
paid less by the “Payless Ticks” as defined per leg.
- Pay Up - If enabled by checking the checkbox, the spreader will pay
up when the quantity at the level of the calculated hedge price is
+60 -41
View File
@@ -4,17 +4,36 @@ weight = 3
sort_by = "weight"
insert_anchor_links = "right"
+++
## 2.12.0
### User Visible Changes
- Added support for ICE market data and order execution
### Internal Changes
- Lots of permission / admin changes to facilitate ICE
## 2.11.0
### User Visible Changes
- Fixed fills form time filter to also apply to SOD fills
- Addressed an issue that could prevent the web Admin UI from displaying over slow HTTP connections
- Removed the option to enter manual spread fills
- Fixed an issue that could cause erroneous fill account attribution in the event of a crash
### Internal Changes
- Added support for tag 1031. Currently defaults to Y, can it be override through config.
- To facilitate support, allowed the bts user to view and edit algos from all other users
- Improved system tests
- Improved log messages in UI to include dispatcher thread name
- Added support for Windows core dumps in the UI
## 2.10.0
### User Visible Changes
- Fills now include information about the algo that originated the fill
- A new, more powerful PnL form is now available
### API Changes
- Added security state information (pre open, halted, normal, closed, etc)
- Added security state information (pre open, halted, normal, closed, etc)
### Bug Fixes
- Brought back the "select all" check box in the mini order book
- Fixed an issue in the market data subsystem that on rare occasions could result in a startup crash
### Internal Changes
- Fixed an issue that was stopping us from interpreting SOD times as local rather than GMT
- Fixed an issue that was stopping us from interpreting SOD times as local rather than GMT
## 2.9.3
### Internal Changes
- Fixed system test scripts
@@ -30,7 +49,7 @@ insert_anchor_links = "right"
- Avoided shipping production configuration defaults that lead to trying to download a position file for the development sftp server.
### Api
- C# API sample now uses synchronous calls.
## 2.9.0
### User Visible Changes
- Improved Workspace management User Interface
@@ -38,11 +57,11 @@ insert_anchor_links = "right"
- The Spark UI Window group now has a title in the Windows task bar
- Added sound alerts for trade events, see Launcher -> Settings -> Alerts
- Allowed composite algos that are attached to existing orders to identify fills that were already present prior to attachment
- Added import facilities for GMI position files
- Added import facilities for GMI position files
- Added the ability for risk managers to enter manual fills
- Added "raw" P&L columns, that is columns that *do not* include spread legs in their accounting
### Bug Fixes
- Fixed the settings action menu (Load/Save/Reset to Factory)
- Fixed the settings action menu (Load/Save/Reset to Factory)
- Fixed the method used to determine if a security is expired to take into account the start of day time
### API Changes
- Fixed an issue in the Client SDK that could lead to occasional failures to connect to the spark server as well as excessive memory usage.
@@ -79,10 +98,10 @@ insert_anchor_links = "right"
- Made it easier to generate simple UI's for new order types
### API Changes
- Dynamic orders can now use QuantityMeasure parameter types
## 2.7.2
### User Visible Changes
- Changed the login dialog to avoid leaking workspace names from other users.
- Changed the login dialog to avoid leaking workspace names from other users.
- Added new Splitter algo. From the manual: The splitter is useful
for pro-rata markets where you want to vary your exposure
independent of the order quantity that you want to fill. The
@@ -90,13 +109,13 @@ insert_anchor_links = "right"
the order quantity but it will put exposed_qty total size on the
market. Splitter favors order size over queue position.
### Internal Changes
- Made spark more resilient to lack of connectivity to the CME web site for downloading of templates.
- Made spark more resilient to lack of connectivity to the CME web site for downloading of templates.
## 2.7.1
### User Visible Changes
- Fixed some issues with the Cover algo, where in some circumstances
it would erroneously cancel launched hedges on completion.
## 2.7.0
### User Visible Changes
- Allowed algos to process "in-flight" fills while shutting down
@@ -115,25 +134,25 @@ insert_anchor_links = "right"
### User Visible Changes
- Fixed an issue in the ladder where in some cases the mini order book could not be brought up from a badge
- Fixed a rare race condition that could lead to erroneous limit enforcement
- Fixed an issue with the margin limits where an error could occur if an account name with more than 12 chars was used
- Fixed an issue with the margin limits where an error could occur if an account name with more than 12 chars was used
## 2.6.1
### User Visible Changes
- Fixed an issue in the ladder where an error could occur when trying to change ladder settings for the first time
## 2.6.0
### User Visible Changes
- In the ladder made last trade more distinctive when overlayed on the volumn column
- In the ladder, corrected display of volume tool tip
- In the ladder, corrected display of volume tool tip
- In the ladder, changed the consolidation of last trade to consider only the price ignoring aggressor changes
- Added an option for the Top Algo to delete itself if within N ticks from top of book
- Address a rare issue that could lead to an error report on UI exit
- Added margin risk limits
- Added the "Offset" algo. Offset prices a child algo at a parameterizable distance from the top of the book
- Avoided switching the current account and order type on open ladders when limits are changed
- Avoided switching the current account and order type on open ladders when limits are changed
### Internal Changes
- Ensure that admin logins are always allowed to login, regardless of keycode status.
## 2.5.0
### User Visible Changes
- Added loss limit risk check
@@ -163,13 +182,13 @@ insert_anchor_links = "right"
- Made Cover order more independent from its hedging chidren:
- Cover continues to operate even if an hedging child is canceled
- When a cover is canceled it will detach from any running hedging children, instead of canceling them
- Changed Lean, Join and Cover algos to hide themselves in the ladder once they have completed their work
- Changed Lean, Join and Cover algos to hide themselves in the ladder once they have completed their work
- Changed manual fills to appear in day PnL rather than in the SOD records
- Add buy/sell coloring to fill rows in fills window
- Streamlined the mini order book:
- Display of the mini order book popup is quicker
- Only exchange orders are displayed
- Update buttons are only displayed for orders that are under control of an algo with the update applying to the entire algo, not just the exchange order
- Update buttons are only displayed for orders that are under control of an algo with the update applying to the entire algo, not just the exchange order
- We can now select multiple rows to attach, detach, update or cancel multiple orders in a batch
- A checkbox column has been added to make clearer which rows are selected
### API Changes
@@ -179,7 +198,7 @@ insert_anchor_links = "right"
- Added system requirements to Spark documentation
- Implemented keycodes
- Temporarily rejected stop and stop limit orders, pending bug fixes
## 2.2.0
### User Visible Changes
- Made our orders easier to spot in MBO display
@@ -190,30 +209,30 @@ insert_anchor_links = "right"
- Improved handling of orders with undiscovered securities
- Added feedback to the ladder when the user attempts to middle click on bid/ask columns using an order that does not have parameters
- Fixed display of algo studio algo types in order book
- Hid net change setting in synthetic ladders
- Hid net change setting in synthetic ladders
- Fixed a number of problems with filtering on the Fills form
- On the Lean algo, OnToward and Away are now calculated with respect to the opposite inside market at time of the order launch
- Iceberg Spreaders with queue holders are not erroneously cancelled after the first fill
- Added security symbol column to fills window
- Re-organized ladder settings dialog to make it clearer and easier to use
## 2.1.4
### User Visible Changes
- Fixed handling of cancel button in security picker
## 2.1.3
### User Visible Changes
- Fixed a simulator bug in the handling of 0 sized orders
- Fixed a simulator bug in the handling of 0 sized orders
## 2.2.1
### User Visible Changes
- Fixed order book form exception when display a spreader order for whichs the legs were not present yet
## 2.1.1
### User Visible Changes
- Added PIQ to GLBX Iceberg orders
- Corrected handling of stop limit orders
## 2.1.0
### User Visible Changes
- Added support for the GLBX Iceberg order type
@@ -224,7 +243,7 @@ insert_anchor_links = "right"
- Added PIQ to the mini order book
- Fixed a rare bug that could lead to server failure when requesting market data
- Fixed a bug that could cause incorrect price displays for certain types of synthetic securities
## 2.0.5
### User Visible Changes
- Fixed a bug in the handling of rejected QHO orders
@@ -234,19 +253,19 @@ insert_anchor_links = "right"
## 2.0.4
### User Visible Changes
- Fixed performance regression in order manager
### Internal changes
- Improved market data diagnostics for bad configurations
- Changed market data default buffer size
## 2.0.3
### Internal changes
- Improved backwards compatibility in UI settings
## 2.0.2
### User Visible Changes
- Middle click the ladder price column to show the orders for that price level
- Middle click the ladder price column to show the orders for that price level
- Middle click the bid or ask column in the ladder no longer sends an order
### Internal changes
@@ -274,8 +293,8 @@ insert_anchor_links = "right"
- Improved settings manager tool output
- Added diagnostics in the event MD memory corruption is detected
- Added systems tests for join,lean, iceberg and spreader orders
- Made exchange order recovery more resilient in the presence of database loss
- Made exchange order recovery more resilient in the presence of database loss
## 1.6.2
### User Visible Changes
### Exchange simulator
@@ -314,21 +333,21 @@ insert_anchor_links = "right"
- These columns can be restored by toggling their visibility within the PnL's Settings form
- A new Traded Today column has been added to the PnL
- Filtering on this column will allow a user to restrict the report to only show today's position
### Internal changes
- Improved organization of the shell notification architecture
### Internal changes
- Improved organization of the shell notification architecture
- Added logging to help troubleshoot shell exit hang
## 1.5.6
### User Visible Changes
- Fixed a bug that could cause limits to be erroneously enforced
- Fixed a bug that could cause limits to be erroneously enforced
- Improved algo documentation
- Changed how the spreader uses net change
- Simplified trigger algo user interface
### Internal changes
### Internal changes
- Defaulted to not recording sim market data
- Improved documentation build
- Improved documentation build
- Added additional performance tracing
## 1.5.5
- Fixed Market View 'stale working orders' bug
@@ -336,11 +355,11 @@ insert_anchor_links = "right"
- Avoided logging when warming up cache
## 1.5.3
- Fixed sounds startup issue
- Fixed sounds startup issue
## 1.5.2
- Added better logging to AlgoDefinitionService
## 1.5.1
- Fixed missing securities in the Fills and P&L windows.
@@ -361,7 +380,7 @@ insert_anchor_links = "right"
#### Bug Fixes
- Fixed the "Since Open" time filter to use the exchange open time rather than 8:30 AM
- Fixed a bug in the order book where sometimes parent orders would not update their last message column.
#### Algos
- Added OCO Algo
- An OCO order creates a relationship between a child order and a 'stop' or 'other' order such that when one of these is cancelled, both cancel, or when one is filled, the other order's size is debited in the amount of the fill. Composing this order with Cover simulates scalping functionality
@@ -369,7 +388,7 @@ insert_anchor_links = "right"
- Improved SDK documentation and usability
- Dynamic orders are now properly identified in the order book
#### Documentation
- Information on the Spark API has been added to the Spark manual
- Information on the Spark API has been added to the Spark manual
- The documentation has been updated and reorganized to make information easier to find
### Internal Changes
- Improved robustness of limit system test
+53 -62
View File
@@ -1,83 +1,74 @@
using bts;
using bts.api.admin;
using bts.api.core;
using bts.utils;
using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.Linq;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using bts.api.core;
using mbts.ui.shell;
namespace mbts.spark {
[Export]
class AdminModel : IDisposable {
public readonly UserTuple user;
readonly BehaviorSubject<IReadOnlyDictionary<uint, string>>
_accounts_subject = new BehaviorSubject<IReadOnlyDictionary<uint, string>>(new Dictionary<uint, string>()),
_users_subject = new BehaviorSubject<IReadOnlyDictionary<uint, string>>(new Dictionary<uint, string>());
IReadOnlyDictionary<uint, string>
_account_map = new Dictionary<uint, string>(),
_user_map = new Dictionary<uint, string>();
readonly BehaviorSubject<AdminModelWrapper> _wrapper_subject = new BehaviorSubject<AdminModelWrapper>(new AdminModelWrapper());
IDisposable _dispose;
IDisposable _dispose;
IReadOnlyDictionary<uint, string> _account_map = DictionaryHelper.empty<uint, string>();
[ImportingConstructor]
AdminModel(AdminService service, [Import]UserTuple user) {
AdminModel(AdminService service, UserTuple user) {
this.user = user;
_dispose = service.subscribei_admin_data().Select(update => {
_wrapper_subject.OnNext(new AdminModelWrapper(update.all.TryFirst(), user.name));
IEnumerable<BTSUserImmutable> active_users =
from admin_data in update.all
from bts_user in admin_data.Users
where bts_user.Name == user.name && bts_user.HasUserData && bts_user.UserData.HasAccountPermissions
select bts_user;
IEnumerable<BTSAccountImmutable> active_accounts =
from admin_data in update.all
from bts_account in admin_data.Accounts
where bts_account.IsPresent && _wrapper_subject.Value.can_view_account(bts_account.Name)
join account_id in (
from bts_user in active_users
from account_id in bts_user.UserData.AccountPermissions.AccountPermissions.Keys
select account_id
) on bts_account.Id equals account_id
select bts_account;
var users = active_users.ToDictionary(u => u.Id, u => u.Name);
var accounts = active_accounts.ToDictionary(a => a.Id, a => a.Name);
return (users, accounts);
}).Subscribe(t => {
_account_map = t.accounts;
_user_map = t.users;
_users_subject.OnNext(_user_map = t.users);
_accounts_subject.OnNext(_account_map = t.accounts);
});
// accounts
on_accounts = service.subscribei_admin_data()
.Select(up => up.all.TryGetFirst(out var ad) && ad.Users.Where(u => u.Name == user.name).TryGetSingle(out var usr) && usr.HasUserData && usr.UserData.HasAccountPermissions ?
usr.UserData.AccountPermissions.AccountPermissions
.Where(kv => (kv.Value.TradingPermission != null && kv.Value.TradingPermission.HasPermissions) || kv.Value.AdminPermission.HasPermissions)
.Select(kv => ad.Accounts.Where(a => a.Id == kv.Key).TryGetSingle(out var acc) ? acc : (BTSAccountImmutable?)null)
.Where(acc => acc.HasValue)
.Select(acc => acc.Value)
.ToDictionary(acc => acc.Id, acc => acc.Name) :
new Dictionary<uint, string>(0))
.Replay(1)
.RefCount();
// users
on_users = Observable.Return(DictionaryHelper.wrap_value(user.id, user.name));
all_user_tuples = user.permissions == UserTuple.Permissions.Admin ?
service.subscribei_admin_data()
.Select(u => u.all.TryFirst())
.Where(ad => ad.HasValue)
.Select(ad => ad.Value.Users
.Where(u => user.can_access_user_data(u.Id))
.Select(u => new UserTuple(u.Id, u.Name))
.ToDictionary(UserTuple.key_fn))
.Table(UserTuple.key_fn)
.Cache() :
Observable.Return(TableUpdate<UserTuple, uint>.mk_empty(UserTuple.key_fn).with_added(new[] { user }));
_dispose = on_accounts.Subscribe(map => _account_map = map);
}
public IObservable<IReadOnlyDictionary<uint, string>> on_users => _users_subject.AsObservable();
public IObservable<IReadOnlyDictionary<uint, string>> on_accounts => _accounts_subject.AsObservable();
public IObservable<IReadOnlyList<UserTuple>> on_user_tuples => on_users.Select(d => d.Select(kv => new UserTuple(kv.Key, kv.Value)).ToList());
public IObservable<IReadOnlyList<AccountTuple>> on_account_tuples => on_accounts.Select(d => d.Select(kv => new AccountTuple(kv.Key, kv.Value)).ToList());
public IObservable<AdminModelWrapper> on_admin_data => _wrapper_subject.AsObservable();
public IObservable<IReadOnlyDictionary<uint, string>> on_users { get; }
public IObservable<IReadOnlyDictionary<uint, string>> on_accounts { get; }
public IObservable<TableUpdate<UserTuple, uint>> all_user_tuples { get; }
public IObservable<IReadOnlyList<AccountTuple>> on_account_tuples => on_accounts.Select(d => d.Select(kv => new AccountTuple(kv.Key, kv.Value)).ToListSorted(a => a.name));
public AccountTuple? get_default_account() {
foreach (var kv in _account_map) {
return new AccountTuple(kv.Key, kv.Value);
}
return null;
}
public uint? get_user_id(string user) => _id_for_name(user, _user_map);
public uint? get_account_id(string account) => _id_for_name(account, _account_map);
public void Dispose() => Disposable.dispose(ref _dispose);
public IObservable<Update<T, TKey>> permission_filter<T, TKey>(IObservable<Update<T, TKey>> positions, Func<T, uint> get_account_id) where TKey : IEquatable<TKey> =>
user.permissions == UserTuple.Permissions.Admin ?
positions :
positions.Filter(on_accounts.Select(map => new Func<T, bool>(p => map.ContainsKey(get_account_id(p)))));
static uint? _id_for_name(string name, IReadOnlyDictionary<uint, string> map) {
foreach (var kv in map) {
if (kv.Value == name)
return kv.Key;
}
return null;
}
public IObservable<Update<T, TKey>> permission_filter<T, TKey>(IObservable<Update<T, TKey>> positions, Func<T, string> get_account_name) where TKey : IEquatable<TKey> =>
user.permissions == UserTuple.Permissions.Admin ?
positions :
positions.Filter(on_accounts.Select(map => {
var hash = new HashSet<string>(map.Values);
return new Func<T, bool>(v => hash.Contains(get_account_name(v)));
}));
public AccountTuple? get_default_account() =>
_account_map.Select(kv => new AccountTuple(kv.Key, kv.Value)).TryFirst();
public void Dispose() =>
Disposable.dispose(ref _dispose);
}
}
+3 -8
View File
@@ -3,15 +3,10 @@ using mbts;
using mbts.config;
using mbts.spark;
using mbts.ui;
using mbts.ui.shell;
using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.Diagnostics;
using System.Linq;
using System.Reactive.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
@@ -23,8 +18,8 @@ namespace bts.ui {
IDisposable _dispose;
[ImportingConstructor]
public AboutDaemon(IBus bus, [Import(LoginContainer.Exports.UserName)]string username) {
_user = username;
public AboutDaemon(IBus bus, UserTuple user) {
_user = user.name;
_bus = bus;
_dispose = bus.subscribe(SparkTags.About).OnDispatcher().Subscribe(_ => show_about());
}
@@ -63,4 +58,4 @@ namespace bts.ui {
Disposable.dispose(ref _dispose);
}
}
}
}
+44 -7
View File
@@ -7,13 +7,11 @@ using mbts.ui;
using System;
using System.ComponentModel.Composition;
using System.Reactive.Linq;
using System.Windows.Threading;
using System.Windows;
using System.Threading.Tasks;
using mbts.ui.shell;
using System.Windows;
using System.Windows.Threading;
namespace bts.spark
{
namespace bts.spark {
[Export, Export(typeof(IShellDaemon))]
public class ErrorReporter : IShellDaemon, IDisposable {
readonly ErrorReportService _svc;
@@ -21,9 +19,9 @@ namespace bts.spark
IDisposable _dispose;
[ImportingConstructor]
public ErrorReporter(ErrorReportService svc, IBus bus, [Import(LoginContainer.Exports.UserName)] string username) {
public ErrorReporter(ErrorReportService svc, IBus bus, UserTuple user) {
_svc = svc;
_username = username;
_username = user.name;
_dispose = bus.subscribe(SparkTags.Support).OnDispatcher().Subscribe(msg => show_support_window(msg, "Report a problem"));
}
@@ -48,6 +46,45 @@ namespace bts.spark
ErrorMessage = message,
});
}
public async Task report_silently(string contact, string description, string userdata) {
var request = append_logs(new ErrorReport {
User = _username,
ErrorMessage = description,
UserDescription = userdata,
Contact = contact
}.immutable());
var reply = await _svc.send_report_async(request);
}
public static ErrorReportImmutable append_logs(ErrorReportImmutable src) {
var result = src.CloneToMutable();
var windows_log = LogUtil.read_windows_event_log();
if (!windows_log.IsEmpty) {
result.Files.Add(new FileData {
Name = "Application.evtx.gz",
Contents = windows_log
});
}
var (data,date) = LogUtil.read_latest_crash_dump();
if (!data.IsEmpty) {
result.Files.Add(new FileData {
Name = $"spark-{date.ToString("s")}.dmp.gz",
Contents = data
});
}
var spark_log = LogUtil.read_logs_compressed();
if (!spark_log.IsEmpty) {
result.Files.Add(new FileData {
Name = "log.gz",
Contents = spark_log
});
}
return result.immutable();
}
}
static class ErrorReporterExtensions {
+13 -21
View File
@@ -1,5 +1,6 @@
using bts.api;
using bts.api.synthetic;
using bts.md;
using mbts;
using mbts.co2;
using mbts.spark;
@@ -7,15 +8,10 @@ using mbts.ui;
using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.Linq;
using System.Reactive.Linq;
using System.Threading.Tasks;
using System.Linq;
using SecurityKey = System.UInt32;
using System.Collections.Concurrent;
using System.Threading;
using bts.api.user;
using bts.md;
using mbts.ui.shell;
namespace bts {
[Export(typeof(IInitializable))]
@@ -25,17 +21,21 @@ namespace bts {
readonly Lazy<SyntheticSecurityService> _ss;
readonly Dictionary<SecurityKey, SecurityMD> _deleted_synthetics = new Dictionary<SecurityKey, SecurityMD>();
static uint get_naggle() => mbts.config.Configuration.get<md.RelayOptions>().naggle_ms;
[ImportingConstructor]
public MDDaemon(Lazy<IBus> bus, Lazy<AuthTokenImmutable> token, Lazy<SyntheticSecurityService> ss, [Import(LoginContainer.Exports.UserID)]uint user_id) :
base(get_naggle(), MDSourceOptions.DeferInit, user_id) {
public MDDaemon(Lazy<IBus> bus, Lazy<AuthTokenImmutable> token, Lazy<SyntheticSecurityService> ss, UserTuple user)
: base(mbts.config.Configuration.get<md.RelayOptions>().naggle_ms, MDSourceOptions.DeferInit, user) {
_bus = bus;
_token = token;
_ss = ss;
_ss = ss;
}
override public void initialize() {
public int priority => 55;
[Export]
MDConsumer consumer => _consumer;
IBus bus => _bus.Value;
public override void initialize() {
_relay.set_token(_token.Value);
_dispose.Add(
_ss.Value.subscribei_metadata()
@@ -60,16 +60,8 @@ namespace bts {
// Post full dictionary of syntheics here
bus.post(SparkTags.SyntheticstrategiesTag, t);
}));
_dispose.Add( this.on_securities.Subscribe( (md) => {
bus.post(SparkTags.SecuritiesTag, md);
}));
_dispose.Add(on_securities.Subscribe(md => bus.post(SparkTags.SecuritiesTag, md)));
Task.Run(run);
}
[Export]
MDConsumer consumer => _consumer;
public int priority => 55;
IBus bus => _bus.Value;
}
}
+55
View File
@@ -0,0 +1,55 @@
using System;
using System.ComponentModel.Composition;
using mbts.ui;
using mbts;
using Newtonsoft.Json;
using System.Threading.Tasks;
namespace bts.spark {
[Export(typeof(IShellDaemon))]
class OopsDaemon : IShellDaemon, IDisposable {
[ImportingConstructor]
public OopsDaemon(ErrorReporter e) {
var pid = get_pid();
if (pid.HasValue) {
handle_unclean_shutdown(e, pid.Value);
}
save_pid();
}
public void Dispose() {
clear_pid();
}
void handle_unclean_shutdown(ErrorReporter e, int oldpid) {
string extra = "";
try {
var conf = mbts.config.Configuration.get<bts.config.System>();
extra = JsonConvert.SerializeObject(conf, Formatting.Indented);
}
catch { }
Task.Run(() => e.report_silently(
$"Host: {Environment.MachineName}",
$"pid: {oldpid}\n{extra}",
"Automated crash report")
);
}
const string _subkey = @"Software\BTS";
private void save_pid() {
using (var key = Microsoft.Win32.Registry.CurrentUser.CreateSubKey(_subkey)) {
key.SetValue("pid", System.Diagnostics.Process.GetCurrentProcess().Id, Microsoft.Win32.RegistryValueKind.DWord);
}
}
private void clear_pid() {
using (var key = Microsoft.Win32.Registry.CurrentUser.CreateSubKey(_subkey)) {
key.DeleteValue("pid",false);
}
}
private int? get_pid() {
using (var key = Microsoft.Win32.Registry.CurrentUser.CreateSubKey(_subkey)) {
return key.GetValue("pid") as int?;
}
}
}
}
@@ -43,6 +43,11 @@ namespace bts.shell {
void _show_order_window(Window window) {
foreach (var kv in AlgoStudioModelUtils.get_dynamic_resources(_model.algo_model.ui_configuration))
window.Resources[kv.Key] = kv.Value;
// the dynamic algo 'SecurityKey' template has a ContractPickerPopup that expects the security map to be available as a DynamicResource.
var sub = _bus.subscribe(SparkTags.SecuritiesTag)
.OnDispatcher()
.Subscribe(sec_map => window.Resources[ResourceKeys.Securities] = sec_map);
window.Closed += (s, e) => Disposable.dispose(ref sub);
window.Show(-200, -160, true);
}
@@ -3,17 +3,13 @@ using bts.metrics;
using mbts;
using mbts.config;
using mbts.ui;
using mbts.ui.shell;
using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.Linq;
using System.Net.NetworkInformation;
using System.Text;
using System.Threading.Tasks;
namespace bts.perf {
[Export, Export(typeof(IShellDaemon))]
class ShellLatencyDaemon : IShellDaemon, IDisposable {
readonly ShellLatencyService _service;
@@ -24,10 +20,10 @@ namespace bts.perf {
Ping _pinger = new Ping();
[ImportingConstructor]
public ShellLatencyDaemon(ShellLatencyService service, [Import(LoginContainer.Exports.UserName)] string user) {
public ShellLatencyDaemon(ShellLatencyService service, UserTuple user) {
_service = service;
_version = Configuration.get<bts.config.System>().version;
_username = user;
_username = user.name;
_hostname = Environment.MachineName;
_forever_ping_host(_pinger);
@@ -105,4 +101,4 @@ namespace bts.perf {
Disposable.dispose(ref _subs);
}
}
}
}
+2
View File
@@ -40,6 +40,7 @@ namespace mbts.ui {
EditDocumentAltGeometryKey = nameof(EditDocumentAltGeometryKey),
EditDocumentGeometryKey = nameof(EditDocumentGeometryKey),
GearGeometryKey = nameof(GearGeometryKey),
HamburgerMenuGeometryKey = nameof(HamburgerMenuGeometryKey),
HornGeometryKey = nameof(HornGeometryKey),
KeyGeometryKey = nameof(KeyGeometryKey),
LetterAKey = nameof(LetterAKey),
@@ -172,6 +173,7 @@ namespace mbts.ui {
public static ImageSource FilterActive { get { return load_resource_and_freeze("FilterActive"); } }
public static ImageSource FilterError { get { return load_resource_and_freeze("FilterError"); } }
public static ImageSource Gear => create_icon(GearGeometryKey);
public static ImageSource HamburgerMenu => create_icon(HamburgerMenuGeometryKey);
public static ImageSource Horn => create_icon(HornGeometryKey);
public static ImageSource Import => load_resource_and_freeze("Import");
public static ImageSource Key => create_icon(KeyGeometryKey);
@@ -272,6 +272,10 @@
M16.000015,9.6000061C12.500012,9.6000061 9.5999844,12.5 9.5999844,16 9.5999844,19.5 12.500012,22.399994 16.000015,22.399994 19.500019,22.399994 22.400015,19.5 22.400015,16 22.400015,12.5 19.500019,9.6000061 16.000015,9.6000061z M14.199995,0L17.800005,0C18.199999,0,18.599993,0.29998779,18.599993,0.79998779L18.599993,4.7999878C19.900013,5.1000061,21.000019,5.6000061,22.099997,6.2999878L25.000023,3.3999939C25.099999,3.2999878 25.300011,3.1999817 25.500025,3.1999817 25.700007,3.1999817 25.900019,3.2999878 26.000025,3.3999939L28.600033,6C28.900021,6.2999878,28.900021,6.7999878,28.600033,7.1000061L25.700007,10C26.400019,11.100006,26.900019,12.299988,27.200007,13.5L31.20001,13.5C31.600037,13.5,31.999998,13.799988,31.999998,14.299988L31.999998,17.899994C31.999998,18.299988,31.700012,18.699982,31.20001,18.699982L27.200007,18.699982C26.900019,20,26.400019,21.100006,25.700007,22.199982L28.600033,25.100006C28.700009,25.199982 28.799984,25.399994 28.799984,25.600006 28.799984,25.799988 28.700009,26 28.600033,26.100006L26.000025,28.600006C25.900019,28.699982 25.700007,28.799988 25.500025,28.799988 25.300011,28.799988 25.099999,28.699982 25.000023,28.600006L22.099997,25.699982C21.000019,26.399994,19.800007,26.899994,18.599993,27.199982L18.599993,31.199982C18.599993,31.600006,18.300005,32,17.800005,32L14.199995,32C13.8,32,13.400006,31.699982,13.400006,31.199982L13.400006,27.199982C12.099987,26.899994,11.000011,26.399994,9.9000027,25.699982L7.0000064,28.600006C6.9000003,28.699982 6.6999881,28.799988 6.5000059,28.799988 6.2999937,28.799988 6.099981,28.699982 6.0000054,28.600006L3.3999967,26C3.0999784,25.699982,3.0999784,25.199982,3.3999967,24.899994L6.2999937,22C5.5999805,20.899994,5.0999801,19.699982,4.7999923,18.5L0.79998828,18.5C0.39999409,18.5,-1.933513E-07,18.199982,0,17.699982L0,14.100006C-1.933513E-07,13.699982,0.2999879,13.299988,0.79998828,13.299988L4.7999923,13.299988C5.0999801,12,5.5999805,10.899994,6.2999937,9.7999878L3.3999967,7C3.2999908,6.8999939 3.1999845,6.6999817 3.1999845,6.5 3.1999845,6.2999878 3.2999908,6.1000061 3.3999967,6L6.0000054,3.3999939C6.099981,3.2999878 6.2999937,3.1999817 6.5000059,3.1999817 6.6999881,3.1999817 6.9000003,3.2999878 7.0000064,3.3999939L9.9000027,6.2999878C11.000011,5.6000061,12.199993,5.1000061,13.400006,4.7999878L13.400006,0.79998779C13.400006,0.29998779,13.8,0,14.199995,0z
</sys:String>
<sys:String x:Key="{x:Static mbts:Icons.HamburgerMenuGeometryKey}">
M2.384186E-06,24L32.000004,24 32.000004,26 2.384186E-06,26z M0,11.999998L32,11.999998 32,13.999998 0,13.999998z M2.384186E-06,0L32.000004,0 32.000004,2 2.384186E-06,2z
</sys:String>
<sys:String x:Key="{x:Static mbts:Icons.HornGeometryKey}">
M4.0499969,7.0870913C2.9519978,7.0870913 2.0249984,8.4780906 2.0249987,10.125091 2.0249984,11.771091 2.9519978,13.162091 4.0499969,13.162091 5.1479961,13.162091 6.0749954,11.771091 6.0749955,10.125091 6.0749954,8.4780906 5.1479961,7.0870913 4.0499969,7.0870913z M29.975016,3.316155C23.883831,8.2016557,11.829888,8.9691842,8.1184387,9.0896781L8.0151367,9.0928462 8.0175586,9.1059306C8.0716033,9.4352315 8.0999939,9.7760909 8.0999942,10.125091 8.0999939,10.473966 8.0716033,10.814716 8.0175586,11.143921L8.0098858,11.185364 8.193316,11.195448C12.163719,11.420399,23.994581,12.458921,29.975016,17.005057z M31.176758,0.00019051835C31.280359,-0.0022594665 31.391857,0.01889403 31.514975,0.065146674 31.908983,0.21413228 31.999985,0.59114302 31.999985,1.0121596L31.999985,19.236969C31.999985,19.653958 31.913988,20.028956 31.525991,20.179955 31.405998,20.226952 31.450981,20.250023 31.32699,20.250023 31.050991,20.250023 30.778992,20.13601 30.583986,19.925928 25.229074,14.140572 9.6001122,13.269665 7.2865356,13.172559L7.2813506,13.172345 7.1739591,13.343056C6.4304952,14.468533 5.3060585,15.187091 4.0499969,15.187091 1.8169985,15.187091 0,12.916091 0,10.125091 0,7.3330911 1.8169985,5.0620912 4.0499969,5.0620912 5.3060585,5.0620912 6.4304952,5.78065 7.1739591,6.9063042L7.2852713,7.0832756 7.3087114,7.0828188C9.6077337,7.036146 25.141474,6.4939421 30.567997,0.34315929 30.75913,0.12521249 30.948835,0.005580858 31.176758,0.00019051835z
</sys:String>
@@ -5,7 +5,6 @@ using mbts.co2;
using mbts.persist;
using mbts.spark;
using mbts.ui;
using mbts.ui.shell;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.IO;
@@ -13,13 +12,11 @@ using System.Threading.Tasks;
using System.Windows.Media;
using static mbts.ui.shell.ProtoBufSerializationHelper;
namespace bts.shell
{
namespace bts.shell {
[Export(typeof(IShellModule))]
class MarketShellModule : IShellModule
{
class MarketShellModule : IShellModule {
[ImportingConstructor]
public MarketShellModule(IBus bus, MDConsumer consumer, UserSettingsHub settings_hub, OrdersModel orders_model, [Import(LoginContainer.Exports.UserName)]string user_name) {
public MarketShellModule(IBus bus, MDConsumer consumer, UserSettingsHub settings_hub, OrdersModel orders_model) {
_tsf = new TimeAndSalesWindowFactory(this, bus, consumer, settings_hub);
_mvf = new MarketViewWindowFactory(this, bus, settings_hub, orders_model);
}
@@ -0,0 +1,13 @@
using mbts.spark;
namespace mbts.ui.spark {
readonly struct MarketViewCell {
public readonly MarketViewVM.Row row;
public readonly MarketViewView.Columns column;
public MarketViewCell(MarketViewVM.Row row, MarketViewView.Columns column) {
this.row = row;
this.column = column;
}
}
}
@@ -5,6 +5,7 @@ using mbts.co2;
using mbts.spark.utilities;
using mbts.ui;
using mbts.ui.controls;
using mbts.ui.shell;
using mbts.ui.spark;
using mbts.ui.utility;
using mbts.utils;
@@ -18,7 +19,7 @@ using System.Windows.Input;
namespace mbts.spark {
partial class MarketViewVM {
public class Row : SecurityMDRow, IDisposable, IEquatable<Row>, DIYGrid.IRowVM, DIYGrid.IActionableRowVM {
public class Row : DisposableViewModelBase, ISecurityMDRow, IEquatable<Row>, DIYGrid.IRowVM, DIYGrid.IActionableRowVM {
readonly IBus _bus;
readonly OrdersModel _orders_model;
readonly AnimationManager _animation = new AnimationManager();
@@ -26,6 +27,7 @@ namespace mbts.spark {
readonly ObservableFilterCollection<OrderRecordImmutable> _working_orders, _working_bids, _working_asks;
readonly string _security_text, _security_key_text;
readonly Property<double> _bid_price, _ask_price, _last_price, _day_change, _settlement, _session_high, _session_low;
readonly Property<bool> _last_price_from_spread;
readonly CellProperty<int> _position;
readonly CellProperty<uint> _last_size;
readonly CellProperty<ulong> _volume;
@@ -39,9 +41,11 @@ namespace mbts.spark {
double _raw_day_position;
string _bid_merged_size_text, _ask_merged_size_text;
ulong _bbo_ref, _last_ref;
bool _is_visible;
public Row(IBus bus, SecurityMD smd, OrdersModel orders_model) : base(smd) {
public Row(IBus bus, SecurityMD smd, OrdersModel orders_model) {
_bus = bus;
security_md = smd;
_orders_model = orders_model;
_security_text = smd.security.ToString();
_security_key_text = security_key.ToString();
@@ -54,6 +58,7 @@ namespace mbts.spark {
_day_change = backing(nameof(day_change), double.NaN);
_session_low = backing(nameof(session_low), double.NaN);
_session_high = backing(nameof(session_high), double.NaN);
_last_price_from_spread = backing(nameof(last_price_from_spread), false);
_settlement = backing(nameof(settlement), double.NaN);
_volume = new CellProperty<ulong>(this, nameof(volume), 0);
_inside_bids = new CellProperty<bool>(this, nameof(inside_bids));
@@ -103,12 +108,23 @@ namespace mbts.spark {
public event Action invalidated;
public SecurityMD security_md { get; }
public SecurityImmutable security => security_md.security;
public uint security_key => security_md.security.Id;
SecurityMD ISecurityMDRow.md => security_md;
public bool is_visible {
get => _is_visible;
set => Set(ref _is_visible, value);
}
#region Bound Properties
public bool inside_bids => _inside_bids.value;
public bool inside_asks => _inside_asks.value;
public int position => _position.value;
public double raw_day_position => _raw_day_position;
public double last_price => _last_price.Value;
public bool last_price_from_spread => _last_price_from_spread.Value;
public uint last_size => _last_size.value;
public double bid_price => _bid_price.Value;
public uint bid_direct_size => _bid_direct_size.Value;
@@ -155,27 +171,30 @@ namespace mbts.spark {
public IReadOnlyList<DIYGrid.IGridCellStore> cell_stores => new DIYGrid.IGridCellStore[0];
#endregion
MDConsumerHandle _md => security_md.md;
public void raise_invalidated() => invalidated?.Invoke();
public void refresh() {
bool dirty = false;
if (md.md.get_bbo(ref _bbo_ref, out NativePriceSize b, out NativePriceSize a)) {
if (_md.get_bbo(ref _bbo_ref, out NativePriceSize b, out NativePriceSize a)) {
_bid_price.set(b.price);
refresh_size(ref b, _bid_direct_size, _bid_merged_size, ref _bid_merged_size_text, nameof(bid_merged_size_text));
_ask_price.set(a.price);
refresh_size(ref a, _ask_direct_size, _ask_merged_size, ref _ask_merged_size_text, nameof(ask_merged_size_text));
_inside_bids.on_next_distinct(_bid_price.Value >= _working_best_bid.value, ref dirty);
_inside_asks.on_next_distinct(_ask_price.Value <= _working_best_ask.value, ref dirty);
_refresh_inside_working_orers(ref dirty);
}
if (md.md.get_last_trade(ref _last_ref, out NativePriceSize ps, out DateTime ts, out ulong v)) {
if (_md.get_last_trade(ref _last_ref, out NativePriceSize ps, out DateTime ts, out ulong v, out bool from_spread)) {
_last_price.set(ps.price);
_last_price_from_spread.set(from_spread);
_last_size.on_next_distinct(ps.size, ref dirty);
_volume.on_next_distinct(v, ref dirty);
}
_settlement.set(md.md.settlement_price);
_session_low.set(md.md.session_low);
_session_high.set(md.md.session_high);
_day_change.set(last_price - settlement);
dirty |=
_settlement.set_new(_md.settlement_price) |
_session_low.set_new(_md.session_low) |
_session_high.set_new(_md.session_high) |
_day_change.set_new(last_price - settlement);
if (dirty)
invalidated?.Invoke();
@@ -201,7 +220,7 @@ namespace mbts.spark {
security_key.GetHashCode();
#region Show Windows
void _show_ladder() => _bus.post(SparkTags.ShowLadderTag, md);
void _show_ladder() => _bus.post(SparkTags.ShowLadderTag, security_md);
void _show_order_book(MarketViewView.Columns col) {
switch (col) {
@@ -275,9 +294,7 @@ namespace mbts.spark {
_working_ask_volume.on_next_distinct(_order_summarizer.working_ask_qty, ref dirty);
_working_best_bid.on_next_distinct(_order_summarizer.best_working_bid, ref dirty);
_working_best_ask.on_next_distinct(_order_summarizer.best_working_ask, ref dirty);
_inside_bids.on_next_distinct(_order_summarizer.best_working_bid >= bid_price, ref dirty);
_inside_asks.on_next_distinct(_order_summarizer.best_working_ask <= ask_price, ref dirty);
_refresh_inside_working_orers(ref dirty);
RaisePropertyChanged(
// Implied might not be ever set, so it probably does not RaisePropertyChanged either
// This might be part of the filter funk?
@@ -287,6 +304,11 @@ namespace mbts.spark {
nameof(working_asks_text)
);
}
void _refresh_inside_working_orers(ref bool dirty) {
_inside_bids.on_next_distinct(!double.IsNaN(working_best_bid) && OrderDataHelper.PriceComparer.Compare(working_best_bid, bid_price) >= 0, ref dirty);
_inside_asks.on_next_distinct(!double.IsNaN(working_best_ask) && OrderDataHelper.PriceComparer.Compare(working_best_ask, ask_price) <= 0, ref dirty);
}
#endregion
class MyOrderSummarizer {
@@ -362,4 +384,4 @@ namespace mbts.spark {
}
}
}
}
}
@@ -1,15 +1,29 @@
using bts;
using bts.api;
using bts.api.core;
using bts.api.orderman;
using bts.collections;
using bts.utils;
using mbts.persist;
using mbts.spark.filters;
using mbts.ui;
using mbts.ui.controls;
using mbts.ui.converters;
using mbts.ui.settings;
using mbts.ui.shell;
using mbts.ui.utility;
using mbts.utils;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Reactive.Linq;
using System.Text.RegularExpressions;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Media;
using static bts.shell.MarketViewWindowFactory;
@@ -17,45 +31,94 @@ using static mbts.ui.controls.DIYGrid;
using static mbts.ui.spark.MarketViewView;
namespace mbts.spark {
partial class MarketViewVM : SecurityMDGrid<MarketViewVM.Row, SecurityMD> {
partial class MarketViewVM : DisposableViewModelBase, IHandleRowSelection {
/// <summary>
/// Viewmodel class designed for <see cref="ui.spark.MarketViewView"/>
/// </summary>
static readonly IReadOnlyList<Func<MarketViewVM, IDataGridColumnFilter<Row>>> _filter_getters =
ObjectSnapshot.property_delegates_of_type<MarketViewVM, IDataGridColumnFilter<Row>>().ToReadOnlyList();
static readonly IComparer<SecurityMD> _sec_md_key_comparer = Comparer<uint>.Default.wrap(SecurityMD.key_fn);
static readonly IComparer<Row> _row_key_comparer = _sec_md_key_comparer.wrap<SecurityMD, Row>(r => r.security_md);
readonly IBus _bus;
readonly OrdersModel _orders_model;
IWindowSettingsManager<GridSettings> _settings_manager;
readonly ObservableCollection<Row> _rows = new ObservableCollection<Row>();
readonly IList<SecurityMD> _wrapped_rows;
readonly ListCollectionView _rows_view;
readonly Dictionary<uint, Row> _cache = new Dictionary<uint, Row>();
readonly Property<Row> _current_row;
readonly DeferrableActionWrapper _refresh_filter;
Func<Row, bool> _filter;
IReadOnlyList<(Columns key, ListSortDirection direction)> _sorting_hierarchy;
Color _selection_highlight;
public MarketViewVM(IBus bus, OrdersModel orders_model, IObservable<StartupData> state, IWindowSettingsManager<GridSettings> settings_manager )
: this(bus, orders_model, state, state.SelectManyInSeries(s => s.security_criteria.subscribe_md(bus)), settings_manager) { }
// securities_connectable should NOT be connected already.
MarketViewVM(IBus bus, OrdersModel orders_model, IObservable<StartupData> state, IObservable<IEnumerable<SecurityMD>> securities_source, IWindowSettingsManager<GridSettings> settings_manager)
: base(RXCollection.Create(securities_source, SecurityDataHelper.EqualityComparer.MDComparer), null) {
/// <summary>
/// Initializes an instance of the <see cref="ui.spark.MarketViewView"/> class.
/// </summary>
/// <param name="bus"></param>
/// <param name="orders_model"></param>
/// <param name="security_mds_source"></param>
/// <param name="use_mouse_state_transitions">Indicates whether or not hyperlink cells should use visual states to animate mouse over (seems to be slower than style triggers, should investigate to be certain).</param>
public MarketViewVM(
IBus bus,
OrdersModel orders_model,
IObservable<TableUpdate<SecurityMD, uint>> security_mds_source,
bool use_mouse_state_transitions = false) {
_bus = bus;
_orders_model = orders_model;
security_filter = new RegexFilterVM<Row>(r => r.security.ToString(), nameof(Row.security));
_settings_manager = settings_manager;
comparer = DIYGridSortHelper.build_comparer(sorting_hierarchy, get_column_comparer, Columns.SecurityKey);
// rows
_wrapped_rows = _rows.WrapSelector(r => r.security_md, s => {
var r = new Row(_bus, s, _orders_model);
r.is_visible = _filter == null || _filter(r);
return r;
});
_current_row = backing<Row>(nameof(current_row));
_rows_view = new ListCollectionView(_rows) {
Filter = obj => obj is Row r && r.is_visible,
IsLiveFiltering = true
};
_rows_view.LiveFilteringProperties.Add(nameof(Row.is_visible));
// Columns
const string merged_size_col_header = "Qty", direct_size_col_header = "Direct", implied_size_col_header = "Implied", working_col_header = "Working";
const double working_orders_col_width = 105;
IMultiValueConverter price_converter = new ui.spark.converters.TickDisplayConverter();
add_col(Columns.Security, "Security", r => r.security.ToString(), nameof(Row.security), DefaultColumnWidths.Security, security_filter, DataGridFilterKnownTemplates.Regex);
add_col(Columns.Volume, "Volume", r => r.volume.ToString(), nameof(Row.volume), DefaultColumnWidths.Volume, volume_filter, DataGridFilterKnownTemplates.ULongMinMax);
add_col(Columns.LastSpd, "Spd", r => r.last_price_from_spread ? "S" : "", nameof(Row.last_price_from_spread), DefaultColumnWidths.Side, last_price_from_spread_filter, DataGridFilterKnownTemplates.Regex);
add_col(Columns.Last, "Last", r => r.format_price(r.last_price), nameof(Row.last_price), DefaultColumnWidths.Price, last_price_filter, DataGridFilterKnownTemplates.DoubleMinMax);
add_col(Columns.LastSize, merged_size_col_header, r => r.last_size.ToString(), nameof(Row.last_size), DefaultColumnWidths.Size, last_size_filter, DataGridFilterKnownTemplates.UIntMinMax, h_alignment: System.Windows.HorizontalAlignment.Right);
add_col(Columns.Position, "Pos", r => r.position.ToString(), nameof(Row.position), DefaultColumnWidths.Size, position_filter, DataGridFilterKnownTemplates.IntMinMax, true, h_alignment: System.Windows.HorizontalAlignment.Right);
add_col(Columns.RawDayPosition, "Raw Day Pos", r => SparkUtils.fmt_position(r.raw_day_position), nameof(Row.raw_day_position), DefaultColumnWidths.Size, raw_day_position_filter, DataGridFilterKnownTemplates.DoubleMinMax, true, h_alignment: System.Windows.HorizontalAlignment.Right);
add_hl_working_orders_col(Columns.WorkingBids, working_col_header, nameof(Row.working_bids_text), nameof(Row.working_best_bid), nameof(Row.working_bids_click), working_bid_volume_filter, BTSColors.MarketData.bid, System.Windows.HorizontalAlignment.Right);
add_col(Columns.BidImpliedSize, implied_size_col_header, r => r.bid_implied_size.ToString(), nameof(Row.bid_implied_size), DefaultColumnWidths.ImpliedDirectMergedSize, bid_implied_size_filter, DataGridFilterKnownTemplates.UIntMinMax, font_color: BTSColors.MarketData.bid, h_alignment: System.Windows.HorizontalAlignment.Right);
add_col(Columns.BidDirectSize, direct_size_col_header, r => r.bid_direct_size.ToString(), nameof(Row.bid_direct_size), DefaultColumnWidths.ImpliedDirectMergedSize, bid_direct_size_filter, DataGridFilterKnownTemplates.UIntMinMax, font_color: BTSColors.MarketData.bid, h_alignment: System.Windows.HorizontalAlignment.Right);
add_hl_merged_size_col(Columns.BidMergedSize, merged_size_col_header, nameof(Row.bid_merged_size), nameof(Row.bid_merged_size_text), nameof(Row.bid_merged_size_click), bid_merged_size_filter, BTSColors.MarketData.bid, h_alignment: System.Windows.HorizontalAlignment.Right);
add_hl_price_col(Columns.Bid, "Bid", nameof(Row.bid_price), nameof(Row.bid_price_click), bid_price_filter, BTSColors.header_text, BTSColors.MarketData.bid, h_alignment: System.Windows.HorizontalAlignment.Right);
add_hl_price_col(Columns.Ask, "Ask", nameof(Row.ask_price), nameof(Row.ask_price_click), ask_price_filter, BTSColors.header_text, BTSColors.MarketData.ask, h_alignment: System.Windows.HorizontalAlignment.Right);
add_hl_merged_size_col(Columns.AskMergedSize, merged_size_col_header, nameof(Row.ask_merged_size), nameof(Row.ask_merged_size_text), nameof(Row.ask_merged_size_click), ask_merged_size_filter, BTSColors.MarketData.ask, h_alignment: System.Windows.HorizontalAlignment.Right);
add_col(Columns.AskDirectSize, direct_size_col_header, r => r.ask_direct_size.ToString(), nameof(Row.ask_direct_size), DefaultColumnWidths.ImpliedDirectMergedSize, ask_direct_size_filter, DataGridFilterKnownTemplates.UIntMinMax, font_color: BTSColors.MarketData.ask, h_alignment: System.Windows.HorizontalAlignment.Right);
add_col(Columns.AskImpliedSize, implied_size_col_header, r => r.ask_implied_size.ToString(), nameof(Row.ask_implied_size), DefaultColumnWidths.ImpliedDirectMergedSize, ask_implied_size_filter, DataGridFilterKnownTemplates.UIntMinMax, font_color: BTSColors.MarketData.ask, h_alignment: System.Windows.HorizontalAlignment.Right);
add_hl_working_orders_col(Columns.WorkingAsks, working_col_header, nameof(Row.working_asks_text), nameof(Row.working_best_ask), nameof(Row.working_asks_click), working_ask_volume_filter, BTSColors.MarketData.ask, System.Windows.HorizontalAlignment.Right);
add_col(Columns.Settlement, "Settlement", r => r.format_price(r.settlement), nameof(Row.settlement), DefaultColumnWidths.Settlement, settlement_filter, DataGridFilterKnownTemplates.DoubleMinMax, h_alignment: System.Windows.HorizontalAlignment.Right);
add_col(Columns.DayChange, "Change", r => r.format_price(r.day_change), nameof(Row.day_change), DefaultColumnWidths.Price, day_change_filter, DataGridFilterKnownTemplates.DoubleMinMax, true, h_alignment: System.Windows.HorizontalAlignment.Right);
add_col(Columns.SessionLow, "Low", r => r.format_price(r.session_low), nameof(Row.session_low), DefaultColumnWidths.Price, session_low_filter, DataGridFilterKnownTemplates.DoubleMinMax, h_alignment: System.Windows.HorizontalAlignment.Right);
add_col(Columns.SessionHigh, "High", r => r.format_price(r.session_high), nameof(Row.session_high), DefaultColumnWidths.Price, session_high_filter, DataGridFilterKnownTemplates.DoubleMinMax, h_alignment: System.Windows.HorizontalAlignment.Right);
add_col(Columns.LastSize, merged_size_col_header, r => r.last_size.ToString(), nameof(Row.last_size), DefaultColumnWidths.Size, last_size_filter, DataGridFilterKnownTemplates.UIntMinMax);
add_col(Columns.Position, "Pos", r => r.position.ToString(), nameof(Row.position), DefaultColumnWidths.Size, position_filter, DataGridFilterKnownTemplates.IntMinMax);
add_col(Columns.RawDayPosition, "Raw Day Pos", r => SparkUtils.fmt_position(r.raw_day_position), nameof(Row.raw_day_position), DefaultColumnWidths.Size, raw_day_position_filter, DataGridFilterKnownTemplates.DoubleMinMax, true);
add_hl_working_orders_col(
Columns.WorkingBids,
working_col_header,
nameof(Row.working_bids_text),
nameof(Row.working_best_bid),
working_bid_volume_filter,
BTSColors.MarketData.bid);
add_col(Columns.BidImpliedSize, implied_size_col_header, r => r.bid_implied_size.ToString(), nameof(Row.bid_implied_size), DefaultColumnWidths.ImpliedDirectMergedSize, bid_implied_size_filter, DataGridFilterKnownTemplates.UIntMinMax, font_color: BTSColors.MarketData.bid);
add_col(Columns.BidDirectSize, direct_size_col_header, r => r.bid_direct_size.ToString(), nameof(Row.bid_direct_size), DefaultColumnWidths.ImpliedDirectMergedSize, bid_direct_size_filter, DataGridFilterKnownTemplates.UIntMinMax, font_color: BTSColors.MarketData.bid);
add_hl_merged_size_col(Columns.BidMergedSize, nameof(Row.bid_merged_size), nameof(Row.bid_merged_size_text), bid_merged_size_filter, BTSColors.MarketData.bid);
add_hl_price_col(Columns.Bid, "Bid", nameof(Row.bid_price), bid_price_filter, BTSColors.header_text, BTSColors.MarketData.bid);
add_hl_price_col(Columns.Ask, "Ask", nameof(Row.ask_price), ask_price_filter, BTSColors.header_text, BTSColors.MarketData.ask);
add_hl_merged_size_col(Columns.AskMergedSize, nameof(Row.ask_merged_size), nameof(Row.ask_merged_size_text), ask_merged_size_filter, BTSColors.MarketData.ask);
add_col(Columns.AskDirectSize, direct_size_col_header, r => r.ask_direct_size.ToString(), nameof(Row.ask_direct_size), DefaultColumnWidths.ImpliedDirectMergedSize, ask_direct_size_filter, DataGridFilterKnownTemplates.UIntMinMax, font_color: BTSColors.MarketData.ask);
add_col(Columns.AskImpliedSize, implied_size_col_header, r => r.ask_implied_size.ToString(), nameof(Row.ask_implied_size), DefaultColumnWidths.ImpliedDirectMergedSize, ask_implied_size_filter, DataGridFilterKnownTemplates.UIntMinMax, font_color: BTSColors.MarketData.ask);
add_hl_working_orders_col(
Columns.WorkingAsks,
working_col_header,
nameof(Row.working_asks_text),
nameof(Row.working_best_ask),
working_ask_volume_filter,
BTSColors.MarketData.ask);
add_col(Columns.Settlement, "Settlement", r => r.format_price(r.settlement), nameof(Row.settlement), DefaultColumnWidths.Settlement, settlement_filter, DataGridFilterKnownTemplates.DoubleMinMax);
add_col(Columns.DayChange, "Change", r => r.format_price(r.day_change), nameof(Row.day_change), DefaultColumnWidths.Price, day_change_filter, DataGridFilterKnownTemplates.DoubleMinMax, true);
add_col(Columns.SessionLow, "Low", r => r.format_price(r.session_low), nameof(Row.session_low), DefaultColumnWidths.Price, session_low_filter, DataGridFilterKnownTemplates.DoubleMinMax);
add_col(Columns.SessionHigh, "High", r => r.format_price(r.session_high), nameof(Row.session_high), DefaultColumnWidths.Price, session_high_filter, DataGridFilterKnownTemplates.DoubleMinMax);
add_col(Columns.SecurityKey, "Security Key", r => r.security_key.ToString(), nameof(Row.security_key), DefaultColumnWidths.SecurityKey, security_key_filter, DataGridFilterKnownTemplates.Regex);
add_col(Columns.MarketDescription, "Market Description", r => r.security.market_description(), nameof(Row.market_description), DefaultColumnWidths.SecurityKey, market_description_filter, DataGridFilterKnownTemplates.Regex);
@@ -65,8 +128,8 @@ namespace mbts.spark {
add_col(Columns.StripName, "Strip Name", r => r.security.strip_name(), nameof(Row.strip_name), DefaultColumnWidths.SecurityKey, strip_name_filter, DataGridFilterKnownTemplates.Regex);
add_col(Columns.MicCode, "Mic Code", r => r.security.mic_code(), nameof(Row.mic_code), DefaultColumnWidths.SecurityKey, mic_code_filter, DataGridFilterKnownTemplates.Regex);
//
void add_col(Columns col_key, string header, Func<Row, string> get_text, string prop, double width, IDataGridColumnFilter filter, string filter_template, bool is_animatable = false, Color? font_color = null, System.Windows.HorizontalAlignment h_alignment = System.Windows.HorizontalAlignment.Left) =>
columns.Add(new RenderTextColumn<Row, Columns>(col_key, get_text, filter, filter_template) {
RenderTextColumn<Row> add_col(Columns col_key, string header, Func<Row, string> get_text, string prop, double width, IDataGridColumnFilter filter, string filter_template, bool is_animatable = false, Color? font_color = null, HorizontalAlignment h_alignment = HorizontalAlignment.Right) {
var col = new RenderTextColumn<Row, Columns>(col_key, get_text, filter, filter_template) {
// it's necessary to have this id populated for grid settings
id = bts.shell.MarketViewWindowFactory.col_id(col_key),
Header = header,
@@ -75,31 +138,85 @@ namespace mbts.spark {
padding = is_animatable ? AnimatableCellPadding : DefaultCellPadding,
font_color_override = font_color,
horizontal_alignment = h_alignment
});
};
columns.Add(col);
return col;
}
void add_hl_price_col(Columns col_key, string header, string prop, string command_prop, IDataGridColumnFilter filter, Color foreground, Color? background, System.Windows.HorizontalAlignment h_alignment) =>
add_hl_col(col_key, header, price_binding(prop), prop, command_prop, filter, DataGridFilterKnownTemplates.DoubleMinMax, DefaultColumnWidths.MViewPrice, foreground, background, h_alignment);
void add_hl_price_col(Columns col_key, string header, string prop, IDataGridColumnFilter filter, Color foreground, Color? background) =>
add_hl_col(
col_key,
header,
price_binding(prop),
prop,
filter,
DataGridFilterKnownTemplates.DoubleMinMax,
DefaultColumnWidths.MViewPrice,
foreground,
background,
tooltip: _order_ticket_tooltip(col_key));
void add_hl_merged_size_col(Columns col_key, string header, string sort_prop, string text_prop, string command_prop, IDataGridColumnFilter filter, Color foreground, System.Windows.HorizontalAlignment h_alignment) =>
add_hl_col(col_key, header, new Binding(text_prop), sort_prop, command_prop, filter, DataGridFilterKnownTemplates.UIntMinMax, DefaultColumnWidths.ImpliedDirectMergedSize, foreground, null, h_alignment);
void add_hl_merged_size_col(Columns col_key, string sort_prop, string text_prop, IDataGridColumnFilter filter, Color foreground) =>
add_hl_col(
col_key,
merged_size_col_header,
new Binding(text_prop),
sort_prop,
filter,
DataGridFilterKnownTemplates.UIntMinMax,
DefaultColumnWidths.ImpliedDirectMergedSize,
foreground,
tooltip: _order_ticket_tooltip(col_key));
void add_hl_working_orders_col(Columns col_key, string header, string text_prop, string sort_prop, string command_prop, IDataGridColumnFilter filter, Color foreground, System.Windows.HorizontalAlignment h_alignment) =>
add_hl_col(col_key, header, new Binding(text_prop), sort_prop, command_prop, filter, DataGridFilterKnownTemplates.IntMinMax, working_orders_col_width, foreground, null, h_alignment);
HyperlinkElementColumn<Columns> add_hl_working_orders_col(Columns col_key, string header, string text_prop, string sort_prop, IDataGridColumnFilter filter, Color foreground) =>
add_hl_col(
col_key,
header,
new Binding(text_prop),
sort_prop,
filter,
DataGridFilterKnownTemplates.IntMinMax,
working_orders_col_width,
foreground,
tooltip: $"Show {_orderbook_title(col_key).desc}");
void add_hl_col(
Columns col_key, string header, BindingBase binding, string prop, string command_prop,
IDataGridColumnFilter filter, string filter_template, double width, Color foreground, Color? background, System.Windows.HorizontalAlignment h_alignment = System.Windows.HorizontalAlignment.Left) =>
columns.Add(new HyperlinkElementColumn<Row, Columns>(
col_key, binding, new Binding(command_prop), filter, filter_template) {
// it's necessary to have this id populated for grid settings
id = bts.shell.MarketViewWindowFactory.col_id(col_key),
Header = header,
sort_member_path = prop,
font_color_override = foreground,
background_color_override = background,
Width = DefaultColumnWidths.Price,
horizontal_alignment = h_alignment
});
HyperlinkElementColumn<Columns> add_hl_col(
Columns col_key,
string header,
BindingBase binding,
string prop,
IDataGridColumnFilter filter,
string filter_template,
double width,
Color? foreground = null,
Color? background = null,
HorizontalAlignment h_alignment = HorizontalAlignment.Right,
string tooltip = null) {
var col = new HyperlinkElementColumn<Columns>(
col_key, filter, filter_template) {
// it's necessary to have this id populated for grid settings
id = bts.shell.MarketViewWindowFactory.col_id(col_key),
Header = header,
sort_member_path = prop,
font_color_override = foreground,
background_color_override = background,
TextBinding = binding,
Width = DefaultColumnWidths.Price,
horizontal_alignment = h_alignment,
HyperlinkClickMode = HyperlinkCellClickMode.Anywhere
};
col.view_generated += on_view_generated;
columns.Add(col);
return col;
//
void on_view_generated(HyperlinkCell cell_view) {
cell_view.SetValue(ColumnKeyProperty, col_key);
cell_view.ToolTip = tooltip;
if (use_mouse_state_transitions)
init_hyperlink_hover_animation(cell_view, col);
}
}
BindingBase price_binding(string prop) {
var mb = new MultiBinding { Converter = price_converter };
@@ -107,78 +224,134 @@ namespace mbts.spark {
mb.Bindings.Add(new Binding($"{nameof(Row.security)}.{nameof(SecurityImmutable.ContractSpec)}") { Mode = BindingMode.OneWay });
return mb;
}
// row vm filter
// Althoguh securities are filtered at the row 'model' level (SecurityMD), the column base filters have to be applied at the row vm level,
// because they access market data, which is polled periodically and cached in the row vm.
_refresh_filter = new DeferrableActionWrapper(() => _filter_changed?.Invoke());
var row_filter_source = Observable.Create<Func<Row, bool>>(o => {
notify();
_filter_changed += notify;
return Disposable.Create(() => _filter_changed -= notify);
void notify() =>
o.OnNext(get_filter_states().build_predicate());
});
foreach (var f in _all_filters) {
f.filter_changed += _refresh_filter.execute;
}
//
// make font-color immutable for Side, Size, (the font-color rule is hard-coded: MarketData.bid for Buy orders, MarketData.ask for Sells)
var col_settings_flags = new Dictionary<string, GridColumnSettingsFeatureFlags> {
{ Columns.Bid.ToString(), GridColumnSettingsFeatureFlags.ImmutableFontColor | GridColumnSettingsFeatureFlags.ImmutableBackgroundColor },
{ Columns.Ask.ToString(), GridColumnSettingsFeatureFlags.ImmutableFontColor | GridColumnSettingsFeatureFlags.ImmutableBackgroundColor},
{ Columns.WorkingAsks.ToString(), GridColumnSettingsFeatureFlags.ImmutableFontColor },
{ Columns.WorkingBids.ToString(), GridColumnSettingsFeatureFlags.ImmutableFontColor }
};
grid_settings_manager = new SimpleGridSettingsManager<Row>(columns, col_settings_flags, row_vms);
// this will initialize the immutable properties that correspond to the col_settings_flags above (e.g. Bid/Ask font_color_override, etc.)
grid_settings_manager.load_from(settings_manager.current_settings());
//
_resources.AddRange(
state.OnDispatcher().Subscribe(s => {
using (rows_view.DeferRefresh()) {
if (s.grid != null) {
// this means we are not using defaults,
// Note: settings_manager in turn notifies grid_settings_manager of this update
_settings_manager.instance_settings = s.grid;
}
if (s.filters is FilterStates filters) {
security_filter.load_state(filters.security);
bid_price_filter.load_state(filters.bid_price);
bid_merged_size_filter.load_state(filters.bid_merged_size);
bid_direct_size_filter.load_state(filters.bid_direct_size);
bid_implied_size_filter.load_state(filters.bid_implied_size);
ask_price_filter.load_state(filters.ask_price);
ask_merged_size_filter.load_state(filters.ask_merged_size);
ask_direct_size_filter.load_state(filters.ask_direct_size);
ask_implied_size_filter.load_state(filters.ask_implied_size);
last_price_filter.load_state(filters.last_price);
last_size_filter.load_state(filters.last_size);
day_change_filter.load_state(filters.day_change);
settlement_filter.load_state(filters.settlement);
session_low_filter.load_state(filters.session_low);
session_high_filter.load_state(filters.session_high);
position_filter.load_state(filters.position);
raw_day_position_filter.load_state(filters.raw_day_position);
working_bid_volume_filter.load_state(filters.working_bid_volume);
working_ask_volume_filter.load_state(filters.working_ask_volume);
volume_filter.load_state(filters.volume);
security_key_filter.load_state(filters.security_key);
}
rows_view.set_sort_descriptions(s.sorting_criterion, columns);
}
}), DataGridFilterHelper.hook_filters(rows_view, security_filter,
bid_price_filter, bid_merged_size_filter, bid_direct_size_filter, bid_implied_size_filter,
ask_price_filter, ask_merged_size_filter, ask_direct_size_filter, ask_implied_size_filter,
last_price_filter, last_size_filter, day_change_filter, settlement_filter,
session_low_filter, session_high_filter, position_filter, raw_day_position_filter, working_bid_volume_filter, working_ask_volume_filter, volume_filter, security_key_filter),
settings_manager.subscribe_current().OnDispatcher().Subscribe(grid_settings_manager),
_dispose = Disposable.Create(
row_filter_source.Subscribe(f => {
// the rows are polled every 100 ms, just set the filter and let the refresh take care of the rest.
_filter = f;
}),
security_mds_source.Subscribe(_on_securities),
ObservableAutoWrapper.connect_item_hooks(_rows, r => r, true),
WorkLoop.Start("MarketView", _refresh)
);
void init_hyperlink_hover_animation(HyperlinkCell cell_view, ColumnBase col) {
// background timeline
var hover_bg_timeline = VisualStateHelper.create_color_animation(
cell_view,
new PropertyPath("(0).(1)", HyperlinkCell.BackgroundProperty, SolidColorBrush.ColorProperty),
new Binding(nameof(ColumnBase.background_color)) {
Source = col,
Converter = new ColorBlendConverter { Overlay = Colors.Black.with(0x1A) }
});
// background pressed timeline
var pressed_bg_timeline = VisualStateHelper.create_color_animation(
cell_view,
new PropertyPath("(0).(1)", HyperlinkCell.BackgroundProperty, SolidColorBrush.ColorProperty),
new Binding(nameof(ColumnBase.background_color)) {
Source = col,
Converter = new ColorBlendConverter { Overlay = Colors.Black.with(0x25) }
});
// border timeline
var bd_brush = new SolidColorBrush();
BindingOperations.SetBinding(bd_brush, SolidColorBrush.ColorProperty, new Binding(nameof(ColumnBase.background_color)) { Source = col });
cell_view.BorderBrush = bd_brush;
cell_view.BorderThickness = new Thickness(1);
var hover_bd_timeline = VisualStateHelper.create_color_animation(
cell_view, new PropertyPath("(0).(1)", HyperlinkCell.BorderBrushProperty, SolidColorBrush.ColorProperty), Colors.Black.with(0x60));
// mouse states
VisualStateManager.GetVisualStateGroups(cell_view).Add(
VisualStateHelper.create_group("MouseStates",
new VisualState { Name = "Normal" },
VisualStateHelper.create_state("MouseOver", hover_bg_timeline, hover_bd_timeline),
VisualStateHelper.create_state("Pressed", pressed_bg_timeline, hover_bd_timeline)));
// state groups manager
StateManager.SetGroups(cell_view, new StateGroupsManager {
StateDefinitions = new StateDefinitionCollection {
new MouseStateDefinition { MouseSource = cell_view }
}
});
}
}
public event PropertyChange<Row> current_row_changed { add => _current_row.Changed += value; remove => _current_row.Changed -= value; }
event Action _filter_changed;
public ICollectionView rows_view => _rows_view;
public ObservableCollection<Row> rows => _rows;
public ObservableCollection<Row> selected_rows { get; } = new ObservableCollection<Row>();
public Color selection_highlight { get => _selection_highlight; set => Set(ref _selection_highlight, value); }
public ObservableCollection<ColumnBase> columns { get; } = new ObservableCollection<ColumnBase>();
public Row current_row { get => _current_row.Value; set => _current_row.set(value); }
public SimpleGridSettingsManager<Row> grid_settings_manager { get; }
internal IBus bus => _bus;
internal OrdersModel orders_model => _orders_model;
public GridSettings get_grid_save_state() {
if (_settings_manager.instance_settings == null)
return null;
else
return grid_settings_manager.create();
public IReadOnlyList<(Columns key, ListSortDirection direction)> sorting_hierarchy {
get => _sorting_hierarchy ?? (_sorting_hierarchy = Array.Empty<(Columns, ListSortDirection)>());
set {
_sorting_hierarchy = value;
comparer = DIYGridSortHelper.build_comparer(value, get_column_comparer, Columns.SecurityKey);
DIYGridSortHelper.reset_sorting(columns, value);
_rows_view.CustomSort = comparer.to_non_generic();
}
}
public IComparer<Row> comparer { get; private set; }
public IDataGridColumnPersistentFilter<Row, string> security_filter { get; }
public IDataGridColumnPersistentFilter<Row, DoubleMinMaxState> bid_price_filter { get; } = new DoubleMinMax<Row>(r => r.bid_price, nameof(Row.bid_price));
public IDataGridColumnPersistentFilter<Row, UIntMinMaxState> bid_merged_size_filter { get; } = new UIntMinMax<Row>(r => r.bid_merged_size, nameof(Row.bid_merged_size));
public IDataGridColumnPersistentFilter<Row, UIntMinMaxState> bid_direct_size_filter { get; } = new UIntMinMax<Row>(r => r.bid_direct_size, nameof(Row.bid_direct_size));
public IDataGridColumnPersistentFilter<Row, UIntMinMaxState> bid_implied_size_filter { get; } = new UIntMinMax<Row>(r => r.bid_implied_size, nameof(Row.bid_implied_size));
public IDataGridColumnPersistentFilter<Row, DoubleMinMaxState> ask_price_filter { get; } = new DoubleMinMax<Row>(r => r.ask_price, nameof(Row.ask_price));
public IDataGridColumnPersistentFilter<Row, UIntMinMaxState> ask_merged_size_filter { get; } = new UIntMinMax<Row>(r => r.ask_merged_size, nameof(Row.ask_merged_size));
public IDataGridColumnPersistentFilter<Row, UIntMinMaxState> ask_direct_size_filter { get; } = new UIntMinMax<Row>(r => r.ask_direct_size, nameof(Row.ask_direct_size));
public IDataGridColumnPersistentFilter<Row, UIntMinMaxState> ask_implied_size_filter { get; } = new UIntMinMax<Row>(r => r.ask_implied_size, nameof(Row.ask_implied_size));
public IDataGridColumnPersistentFilter<Row, DoubleMinMaxState> last_price_filter { get; } = new DoubleMinMax<Row>(r => r.last_price, nameof(Row.last_price));
public IDataGridColumnPersistentFilter<Row, string> last_price_from_spread_filter { get; } = new RegexFilterVM<Row>(r => r.last_price_from_spread.ToString(), nameof(Row.last_price_from_spread));
public IDataGridColumnPersistentFilter<Row, UIntMinMaxState> last_size_filter { get; } = new UIntMinMax<Row>(r => r.last_size, nameof(Row.last_size));
public IDataGridColumnPersistentFilter<Row, DoubleMinMaxState> day_change_filter { get; } = new DoubleMinMax<Row>(r => r.day_change, nameof(Row.day_change));
public IDataGridColumnPersistentFilter<Row, DoubleMinMaxState> settlement_filter { get; } = new DoubleMinMax<Row>(r => r.settlement, nameof(Row.settlement));
public IDataGridColumnPersistentFilter<Row, DoubleMinMaxState> session_low_filter { get; } = new DoubleMinMax<Row>(r => r.session_low, nameof(Row.session_low));
public IDataGridColumnPersistentFilter<Row, DoubleMinMaxState> session_high_filter { get; } = new DoubleMinMax<Row>(r => r.session_high, nameof(Row.session_high));
public IDataGridColumnPersistentFilter<Row, IntMinMaxState> position_filter { get; } = new IntMinMax<Row>(r => r.position, nameof(Row.position));
public IDataGridColumnPersistentFilter<Row, DoubleMinMaxState> raw_day_position_filter { get; } = new DoubleMinMax<Row>(r => r.raw_day_position, nameof(Row.raw_day_position));
public IDataGridColumnPersistentFilter<Row, IntMinMaxState> working_bid_volume_filter { get; } = new IntMinMax<Row>(r => r.working_bid_volume, nameof(Row.working_bid_volume));
public IDataGridColumnPersistentFilter<Row, IntMinMaxState> working_ask_volume_filter { get; } = new IntMinMax<Row>(r => r.working_ask_volume, nameof(Row.working_ask_volume));
public IDataGridColumnPersistentFilter<Row, ULongMinMaxState> volume_filter { get; } = new ULongMinMax<Row>(r => r.volume, nameof(Row.volume));
public IDataGridColumnPersistentFilter<Row, string> security_key_filter { get; } = new RegexFilterVM<Row>(r => r.security_key.ToString(), nameof(Row.security_key));
public IDataGridColumnPersistentFilter<Row, string> market_description_filter { get; } = new RegexFilterVM<Row>(r => r.security.HasIce ? r.security.Ice.MarketDescription : "", nameof(Row.market_description));
public IDataGridColumnPersistentFilter<Row, string> cleared_alias_filter { get; } = new RegexFilterVM<Row>(r => r.security.HasIce ? r.security.Ice.ClearedAlias : "", nameof(Row.cleared_alias));
public IDataGridColumnPersistentFilter<Row, string> product_description_filter { get; } = new RegexFilterVM<Row>(r => r.security.HasIce ? r.security.Ice.ProductName : "", nameof(Row.product_description));
public IDataGridColumnPersistentFilter<Row, string> hub_alias_filter { get; } = new RegexFilterVM<Row>(r => r.security.HasIce ? r.security.Ice.HubAlias : "", nameof(Row.hub_alias));
public IDataGridColumnPersistentFilter<Row, string> strip_name_filter { get; } = new RegexFilterVM<Row>(r => r.security.HasIce ? r.security.Ice.StripName : "", nameof(Row.strip_name));
public IDataGridColumnPersistentFilter<Row, string> mic_code_filter { get; } = new RegexFilterVM<Row>(r => r.security.HasIce ? r.security.Ice.MicCode : "", nameof(Row.mic_code));
IEnumerable<IDataGridColumnFilter<Row>> _all_filters =>
_filter_getters.Select(g => g(this));
public static IComparer<Row> get_column_comparer(Columns column) {
switch (column) {
case Columns.Security: return wrap(r => r.security);
case Columns.SecurityKey: return wrap(r => r.security.Id);
case Columns.Volume: return wrap(r => r.volume);
case Columns.Last: return wrap(r => r.last_price);
case Columns.LastSpd: return wrap(r => r.last_price_from_spread);
case Columns.LastSize: return wrap(r => r.last_size);
case Columns.Position: return wrap(r => r.position);
case Columns.WorkingBids: return wrap(r => r.working_bid_volume);
@@ -207,36 +380,21 @@ namespace mbts.spark {
Comparer<T>.Default.wrap(selector);
}
#region Filters
public IDataGridColumnPersistentFilter<Row, string> security_filter { get; }
public IDataGridColumnPersistentFilter<Row, DoubleMinMaxState> bid_price_filter { get; } = new DoubleMinMax<Row>(r => r.bid_price, nameof(Row.bid_price));
public IDataGridColumnPersistentFilter<Row, UIntMinMaxState> bid_merged_size_filter { get; } = new UIntMinMax<Row>(r => r.bid_merged_size, nameof(Row.bid_merged_size));
public IDataGridColumnPersistentFilter<Row, UIntMinMaxState> bid_direct_size_filter { get; } = new UIntMinMax<Row>(r => r.bid_direct_size, nameof(Row.bid_direct_size));
public IDataGridColumnPersistentFilter<Row, UIntMinMaxState> bid_implied_size_filter { get; } = new UIntMinMax<Row>(r => r.bid_implied_size, nameof(Row.bid_implied_size));
public IDataGridColumnPersistentFilter<Row, DoubleMinMaxState> ask_price_filter { get; } = new DoubleMinMax<Row>(r => r.ask_price, nameof(Row.ask_price));
public IDataGridColumnPersistentFilter<Row, UIntMinMaxState> ask_merged_size_filter { get; } = new UIntMinMax<Row>(r => r.ask_merged_size, nameof(Row.ask_merged_size));
public IDataGridColumnPersistentFilter<Row, UIntMinMaxState> ask_direct_size_filter { get; } = new UIntMinMax<Row>(r => r.ask_direct_size, nameof(Row.ask_direct_size));
public IDataGridColumnPersistentFilter<Row, UIntMinMaxState> ask_implied_size_filter { get; } = new UIntMinMax<Row>(r => r.ask_implied_size, nameof(Row.ask_implied_size));
public IDataGridColumnPersistentFilter<Row, DoubleMinMaxState> last_price_filter { get; } = new DoubleMinMax<Row>(r => r.last_price, nameof(Row.last_price));
public IDataGridColumnPersistentFilter<Row, UIntMinMaxState> last_size_filter { get; } = new UIntMinMax<Row>(r => r.last_size, nameof(Row.last_size));
public IDataGridColumnPersistentFilter<Row, DoubleMinMaxState> day_change_filter { get; } = new DoubleMinMax<Row>(r => r.day_change, nameof(Row.day_change));
public IDataGridColumnPersistentFilter<Row, DoubleMinMaxState> settlement_filter { get; } = new DoubleMinMax<Row>(r => r.settlement, nameof(Row.settlement));
public IDataGridColumnPersistentFilter<Row, DoubleMinMaxState> session_low_filter { get; } = new DoubleMinMax<Row>(r => r.session_low, nameof(Row.session_low));
public IDataGridColumnPersistentFilter<Row, DoubleMinMaxState> session_high_filter { get; } = new DoubleMinMax<Row>(r => r.session_high, nameof(Row.session_high));
public IDataGridColumnPersistentFilter<Row, IntMinMaxState> position_filter { get; } = new IntMinMax<Row>(r => r.position, nameof(Row.position));
public IDataGridColumnPersistentFilter<Row, DoubleMinMaxState> raw_day_position_filter { get; } = new DoubleMinMax<Row>(r => r.raw_day_position, nameof(Row.raw_day_position));
public IDataGridColumnPersistentFilter<Row, IntMinMaxState> working_bid_volume_filter { get; } = new IntMinMax<Row>(r => r.working_bid_volume, nameof(Row.working_bid_volume));
public IDataGridColumnPersistentFilter<Row, IntMinMaxState> working_ask_volume_filter { get; } = new IntMinMax<Row>(r => r.working_ask_volume, nameof(Row.working_ask_volume));
public IDataGridColumnPersistentFilter<Row, ULongMinMaxState> volume_filter { get; } = new ULongMinMax<Row>(r => r.volume, nameof(Row.volume));
public IDataGridColumnPersistentFilter<Row, string> security_key_filter { get; } = new RegexFilterVM<Row>(r => r.security_key.ToString(), nameof(Row.security_key));
public IDataGridColumnPersistentFilter<Row, string> market_description_filter { get; } = new RegexFilterVM<Row>(r => r.security.HasIce ? r.security.Ice.MarketDescription : "", nameof(Row.market_description));
public IDataGridColumnPersistentFilter<Row, string> cleared_alias_filter { get; } = new RegexFilterVM<Row>(r => r.security.HasIce ? r.security.Ice.ClearedAlias : "", nameof(Row.cleared_alias));
public IDataGridColumnPersistentFilter<Row, string> product_description_filter { get; } = new RegexFilterVM<Row>(r => r.security.HasIce ? r.security.Ice.ProductName : "", nameof(Row.product_description));
public IDataGridColumnPersistentFilter<Row, string> hub_alias_filter { get; } = new RegexFilterVM<Row>(r => r.security.HasIce ? r.security.Ice.HubAlias : "", nameof(Row.hub_alias));
public IDataGridColumnPersistentFilter<Row, string> strip_name_filter { get; } = new RegexFilterVM<Row>(r => r.security.HasIce ? r.security.Ice.StripName : "", nameof(Row.strip_name));
public IDataGridColumnPersistentFilter<Row, string> mic_code_filter { get; } = new RegexFilterVM<Row>(r => r.security.HasIce ? r.security.Ice.MicCode : "", nameof(Row.mic_code));
public (OrderBookVM vm, string context, string title, Color fg) orderbook_params(SecurityImmutable security, Columns column) {
var (title_desc, fg) = _orderbook_title(column);
var filters = new bts.shell.orderbook.FilterStates {
security_criteria = SecurityCriteria.from_keys(new[] { security.Id })
};
switch (column) {
case Columns.WorkingBids: filters.sides = new[] { Side.Buy }; break;
case Columns.WorkingAsks: filters.sides = new[] { Side.Sell }; break;
}
var vm = _mk_orderbook_vm_common(filters, new[] {
OrderBookGridView.ColumnKeys.Security,
OrderBookGridView.ColumnKeys.Side
});
return (vm, $"{security}", title_desc, fg);
}
public FilterStates get_filter_states() {
return new FilterStates {
@@ -250,6 +408,7 @@ namespace mbts.spark {
ask_direct_size = ask_direct_size_filter.get_state(),
ask_implied_size = ask_implied_size_filter.get_state(),
last_price = last_price_filter.get_state(),
last_price_from_spread = last_price_from_spread_filter.get_state(),
last_size = last_size_filter.get_state(),
day_change = day_change_filter.get_state(),
settlement = settlement_filter.get_state(),
@@ -269,14 +428,102 @@ namespace mbts.spark {
mic_code = mic_code_filter.get_state(),
};
}
#endregion
protected override Row row_factory(SecurityMD smd) => new Row(_bus, smd, _orders_model);
protected override void on_row_removed(Row row) => row.Dispose();
protected override IEnumerable<SortingCriteria> get_default_sorting() { yield return new SortingCriteria(nameof(Row.security), true); }
public void load_filters(FilterStates filters) {
if (filters != null) {
using (rows_view.DeferRefresh()) {
security_filter.load_state(filters.security);
bid_price_filter.load_state(filters.bid_price);
bid_merged_size_filter.load_state(filters.bid_merged_size);
bid_direct_size_filter.load_state(filters.bid_direct_size);
bid_implied_size_filter.load_state(filters.bid_implied_size);
ask_price_filter.load_state(filters.ask_price);
ask_merged_size_filter.load_state(filters.ask_merged_size);
ask_direct_size_filter.load_state(filters.ask_direct_size);
ask_implied_size_filter.load_state(filters.ask_implied_size);
last_price_filter.load_state(filters.last_price);
last_price_from_spread_filter.load_state(filters.last_price_from_spread);
last_size_filter.load_state(filters.last_size);
day_change_filter.load_state(filters.day_change);
settlement_filter.load_state(filters.settlement);
session_low_filter.load_state(filters.session_low);
session_high_filter.load_state(filters.session_high);
position_filter.load_state(filters.position);
raw_day_position_filter.load_state(filters.raw_day_position);
working_bid_volume_filter.load_state(filters.working_bid_volume);
working_ask_volume_filter.load_state(filters.working_ask_volume);
volume_filter.load_state(filters.volume);
security_key_filter.load_state(filters.security_key);
}
}
}
static (string desc, Color fg) _orderbook_title(Columns key) {
switch (key) {
case Columns.WorkingBids: return ("Working Buy Orders", BTSColors.MarketData.bid);
case Columns.WorkingAsks: return ("Working Sell Orders", BTSColors.MarketData.ask);
default: return ("Working Orders", Colors.Black);
}
}
static GridSettings _hide_cols(GridSettings grid, IEnumerable<OrderBookGridView.ColumnKeys> columns) =>
grid.with(_hide_cols(grid.columns, columns));
static IEnumerable<GridColumnSettings> _hide_cols(IEnumerable<GridColumnSettings> cols, IEnumerable<OrderBookGridView.ColumnKeys> columns) {
// hide columns who's data is implied by the grouping
var col_hash = new HashSet<string>(columns.Select(k => k.ToString()));
foreach (var c in cols)
yield return col_hash.Contains(c.id) ? c.with(is_visible: false) : c;
}
static string _order_ticket_tooltip(Columns key) {
var bid_or_ask = Regex.Match(key.ToString(), "(Bid|Ask)");
return bid_or_ask.Success ? $"Order Ticket @ {bid_or_ask.Value}" : "Order Ticket";
}
OrderBookVM _mk_orderbook_vm_common(bts.shell.orderbook.FilterStates filters, IEnumerable<OrderBookGridView.ColumnKeys> hide_cols, IObservable<TableUpdate<OrderSecurityRootPair, uint>> orders_source = null) {
var state = new bts.shell.orderbook.OrderBookStartupData(filters, bts.shell.orderbook.OrderBookStartupData.default_mini_grid, use_defaults: false);
state.orderbook_grid.general = _hide_cols(state.orderbook_grid.general, hide_cols);
var settings_man = new WindowSettingsManager<bts.shell.orderbook.OrderBookGridSettings>.DisconnectedImpl(state.orderbook_grid);
var obvm = orders_source == null ?
new OrderBookVM(_bus, null, _orders_model, settings_man, true) :
new OrderBookVM(_bus, null, _orders_model, orders_source, settings_man, true);
foreach (var c in OrderBookVM.OrderBookColumnCollection.editable_columns)
obvm.columns.Insert(0, c);
obvm.load_from(state);
return obvm;
}
void _refresh() {
foreach (var r in _row_vms) r.refresh();
foreach (var r in _rows) {
r.refresh();
r.is_visible = _filter == null || _filter(r);
}
}
Row _row_factory(SecurityMD smd) => new Row(_bus, smd, _orders_model);
void IHandleRowSelection.row_selection_changed(SelectionChangedEventArgs e) {
selected_rows.RemoveSorted(e.RemovedItems.OfType<Row>(), _row_key_comparer);
selected_rows.InsertSorted(e.AddedItems.OfType<Row>(), _row_key_comparer);
}
void _on_securities(TableUpdate<SecurityMD, uint> table) {
var sw = new Stopwatch();
sw.Start();
foreach (var smd in table.removed) {
int i = CollectionHelper.binary_search(_wrapped_rows, smd, _sec_md_key_comparer);
Log.assert(i >= 0);
_wrapped_rows.RemoveAt(i);
}
foreach (var smd in table.added) {
int i = CollectionHelper.binary_search(_wrapped_rows, smd, _sec_md_key_comparer);
Log.assert(i < 0);
_wrapped_rows.Insert(~i, smd);
}
sw.Stop();
Log.Ui.info($"MarketView update took {sw.ElapsedMilliseconds:0.00}ms.");
}
}
}
@@ -3,8 +3,13 @@
x:ClassModifier="internal"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:bv="clr-namespace:bts.view"
xmlns:u="clr-namespace:mbts.ui"
xmlns:c="clr-namespace:mbts.ui.controls"
xmlns:cv="clr-namespace:mbts.ui.converters"
xmlns:usp="clr-namespace:mbts.ui.spark"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
u:WindowStyle.UnsafeWindow="{Binding Path='(u:WindowStyle.UnsafeWindow)', RelativeSource={RelativeSource AncestorType=bv:SparkWindowView}}"
FocusManager.IsFocusScope="True"
Background="{StaticResource CommonBackground}"
>
@@ -13,8 +18,54 @@
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="/view/Controls/DataGridEx/Filters/Views/FilterTemplates.xaml"/>
</ResourceDictionary.MergedDictionaries>
<Style TargetType="c:HyperlinkCell">
<Setter Property="Background" Value="{Binding Path='(c:DIYGrid.Column).background_brush', RelativeSource={RelativeSource Self}}"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Command" Value="{x:Static usp:MarketViewView.CellClickCommand}"/>
<Setter Property="CommandParameter">
<Setter.Value>
<MultiBinding Converter="{x:Static usp:MarketViewView.CellConverter}">
<Binding Path=""/>
<Binding Path="(c:DIYGrid.ColumnKey)" RelativeSource="{RelativeSource Self}"/>
</MultiBinding>
</Setter.Value>
</Setter>
<Setter Property="CommandTarget" Value="{Binding RelativeSource={RelativeSource AncestorType=usp:MarketViewView}}"/>
<Style.Triggers>
<Trigger Property="IsMouseOverClickArea" Value="True">
<Setter Property="Background">
<Setter.Value>
<SolidColorBrush>
<SolidColorBrush.Color>
<MultiBinding Converter="{x:Static cv:ColorBlendMultiConverter.Instance}">
<Binding Path="SelectionHighlight" RelativeSource="{RelativeSource AncestorType=c:DIYGrid}"/>
<Binding Path="(c:DIYGrid.Column).background_color" RelativeSource="{RelativeSource AncestorType=c:HyperlinkCell}"/>
</MultiBinding>
</SolidColorBrush.Color>
</SolidColorBrush>
</Setter.Value>
</Setter>
<Setter Property="BorderBrush">
<Setter.Value>
<Binding Path="SelectionHighlight" RelativeSource="{RelativeSource AncestorType=c:DIYGrid}">
<Binding.Converter>
<cv:ValueConverterPipe>
<cv:ColorWithConverter A="255"/>
<cv:ColorToBrushConverter/>
</cv:ValueConverterPipe>
</Binding.Converter>
</Binding>
</Setter.Value>
</Setter>
<Setter Property="BorderThickness" Value="1"/>
</Trigger>
</Style.Triggers>
</Style>
</ResourceDictionary>
</UserControl.Resources>
<UserControl.CommandBindings>
<CommandBinding Command="{x:Static usp:MarketViewView.CellClickCommand}" Executed="{x:Static usp:MarketViewView.CellClickExecuted}"/>
</UserControl.CommandBindings>
<AdornerDecorator>
<Grid>
<Grid.RowDefinitions>
@@ -22,13 +73,14 @@
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<c:DIYGrid
x:Name="_grid"
Grid.Row="0"
Columns="{Binding columns}"
CurrentRow="{Binding current_row}"
Rows="{Binding rows_view}"
SelectionHighlight="{Binding grid_settings_manager.selection_highlight, Mode=OneWay}"
>
x:Name="_grid"
Grid.Row="0"
Columns="{Binding columns}"
CurrentRow="{Binding current_row}"
Rows="{Binding rows_view}"
SelectionHighlight="{Binding selection_highlight, Mode=OneWay}"
SyncColumnSortDirectionWithSortDescriptions="False"
>
<c:DIYGrid.InputBindings>
<MouseBinding MouseAction="LeftDoubleClick" Command="{x:Static c:DIYGrid.DefaultRowCommand}"/>
<KeyBinding Key="Return" Command="{x:Static c:DIYGrid.DefaultRowCommand}"/>
@@ -1,17 +1,40 @@
using mbts.spark;
using bts.api;
using mbts.spark;
using mbts.ui.controls;
using mbts.ui.converters;
using mbts.ui.extensions;
using System.Linq;
using System.Reactive.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;
namespace mbts.ui.spark {
/// <summary>
/// Interaction logic for MarketSheetsView.xaml
/// </summary>
partial class MarketViewView : UserControl {
public static readonly RoutedCommand CellClickCommand = new RoutedCommand();
MarketViewVM _vm;
DIYGridPopupManager _popup_man_backing;
public MarketViewView() {
InitializeComponent();
this.ConnectDataContext<MarketViewVM>(vm => _vm = vm);
AdornerHelper.attach_continuously_on_loaded(
() => new GridAnimation.DIYGridAnimationAdorner<Columns>(
_grid, _grid.descendants_of_type<DIYGridRowsPresenter>().First()),
_grid);
}
public enum Columns {
None,
Security,
Volume,
Last,
LastSpd,
LastSize,
Position,
RawDayPosition,
@@ -38,14 +61,100 @@ namespace mbts.ui.spark {
MicCode
}
public MarketViewView() {
InitializeComponent();
AdornerHelper.attach_continuously_on_loaded(
() => new GridAnimation.DIYGridAnimationAdorner<Columns>(
_grid, _grid.descendants_of_type<DIYGridRowsPresenter>().First()),
_grid);
public static ExecutedRoutedEventHandler
CellClickExecuted => CommandImpl.cell_click;
public static IMultiValueConverter CellConverter =>
ValueConverter.create<MarketViewVM.Row, Columns, MarketViewCell>((r, c) => new MarketViewCell(r, c));
IBus _bus => _vm.bus;
OrdersModel _model => _vm.orders_model;
DIYGridPopupManager _popup_man =>
_popup_man_backing ?? (_popup_man_backing = DIYGridPopupManager.create(this));
public void show_mini_orderbook(SecurityMD security_md) =>
_show_mini_orderbook(security_md, Columns.None);
protected override void OnLostMouseCapture(MouseEventArgs e) {
if (e.LeftButton == MouseButtonState.Pressed && Mouse.Captured == null) {
if (this.ChildAtPoint(e.GetPosition(this), out DIYGridRowsPresenter presenter))
presenter.begin_explicit_drag();
}
}
void _on_cell_click(MarketViewVM.Row row, Columns col) {
switch (col) {
case Columns.WorkingAsks:
case Columns.WorkingBids:
_show_mini_orderbook(row.security_md, col);
break;
case Columns.Bid:
_show_order_ticket(row.security, row.bid_price);
break;
case Columns.BidMergedSize: _show_order_ticket(row.security, row.bid_price, row.bid_merged_size); break;
case Columns.BidDirectSize: _show_order_ticket(row.security, row.bid_price, row.bid_direct_size); break;
case Columns.BidImpliedSize: _show_order_ticket(row.security, row.bid_price, row.bid_implied_size); break;
case Columns.Ask:
_show_order_ticket(row.security, row.ask_price);
break;
case Columns.AskMergedSize: _show_order_ticket(row.security, row.ask_price, row.ask_merged_size); break;
case Columns.AskDirectSize: _show_order_ticket(row.security, row.ask_price, row.ask_direct_size); break;
case Columns.AskImpliedSize: _show_order_ticket(row.security, row.ask_price, row.ask_implied_size); break;
case Columns.Last:
_show_order_ticket(row.security, row.last_price);
break;
case Columns.LastSize:
_show_order_ticket(row.security, row.last_price, row.last_size);
break;
}
}
void _show_mini_orderbook(SecurityMD security_md, Columns col) {
var (vm, context, title, fg) = _vm.orderbook_params(security_md.security, col);
_show_popup(vm, new AuxiliaryDataHeader {
context = context,
title = title,
title_foreground = fg
});
}
async void _show_order_ticket(SecurityImmutable security, double price = double.NaN, uint size = 0) {
var sec_md = await _bus.subscribe(SparkTags.SecuritiesTag)
.Select(sec_map => sec_map.TryGetValue(security.Id, out var smd) ? smd : (SecurityMD?)null)
.SkipWhile(smd => !smd.HasValue)
.FirstOrDefaultAsync();
if (sec_md.HasValue) {
var resources = new OrderTicketResources();
resources.MergedDictionaries.Add(new AuxiliaryDataResources());
var ticket_vm = new NewOrderTicketVM(
_model, sec_md.Value, _model.factory.order(security), price, size, Observable.Return(mbts.spark.LadderSettings.Default.calculator_buttons));
_show_popup(ticket_vm, new OrderTicketHeader { bbo = ticket_vm.bbo }, resources);
}
}
void _show_popup(object content, object header) =>
_show_popup(content, header, new AuxiliaryDataResources());
void _show_popup(object content, object header, ResourceDictionary resources) {
var view = new HeaderedContentControl {
Content = content,
Header = header,
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Top,
Resources = resources ?? new AuxiliaryDataResources()
};
view.Style = view.Resources[AuxiliaryDataResources.PopupChildStyleKey] as Style;
_popup_man.show(view);
}
static class CommandImpl {
public static void cell_click(object sender, ExecutedRoutedEventArgs e) {
if (sender is MarketViewView mview && e.Parameter is MarketViewCell cell)
mview._on_cell_click(cell.row, cell.column);
e.Handled = true;
}
}
}
class MarketViewRowView : DIYGrid.HighPerformanceRow<MarketViewVM.Row, MarketViewView.Columns> { }
class MarketViewRowView : DIYGrid.HighPerformanceRow<MarketViewVM.Row> { }
}
@@ -1,18 +1,19 @@
using bts;
using bts.api;
using bts.api.core;
using bts.collections;
using bts.shell;
using bts.shell.fills;
using bts.spark;
using bts.utils;
using mbts.persist;
using mbts.ui;
using mbts.ui.settings;
using mbts.ui.shell;
using mbts.ui.spark;
using mbts.ui.utility;
using mbts.utils;
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq;
@@ -21,6 +22,14 @@ using System.Windows;
namespace mbts.spark {
class MarketViewWindow : SparkWindow<MarketViewWindowFactory.StartupData>, ISharedWindow, ISlowMeasureContentSize {
const string _window_type = "MarketView", _window_type_display_name = "Market View";
// make font-color immutable for Side, Size, (the font-color rule is hard-coded: MarketData.bid for Buy orders, MarketData.ask for Sells)
static readonly IReadOnlyDictionary<string, GridColumnSettingsFeatureFlags> _col_settings_flags = new Dictionary<string, GridColumnSettingsFeatureFlags> {
{ MarketViewView.Columns.Bid.ToString(), GridColumnSettingsFeatureFlags.ImmutableFontColor | GridColumnSettingsFeatureFlags.ImmutableBackgroundColor },
{ MarketViewView.Columns.Ask.ToString(), GridColumnSettingsFeatureFlags.ImmutableFontColor | GridColumnSettingsFeatureFlags.ImmutableBackgroundColor},
{ MarketViewView.Columns.WorkingAsks.ToString(), GridColumnSettingsFeatureFlags.ImmutableFontColor },
{ MarketViewView.Columns.WorkingBids.ToString(), GridColumnSettingsFeatureFlags.ImmutableFontColor }
};
readonly OrdersModel _orders_model;
readonly AllowOnce _init_guard = new AllowOnce("MarketView");
readonly IWindowSettingsManager<GridSettings> _settings_manager;
@@ -32,6 +41,7 @@ namespace mbts.spark {
SecurityCriteria
_selected_security_criteria_backing,
_sorted_selected_security_criteria_backing;
SimpleGridSettingsManager<MarketViewVM.Row> _grid_settings_manager;
public MarketViewWindow(MarketViewWindowFactory factory, IBus bus, OrdersModel orders_model, MarketViewWindowFactory.StartupData state, IWindowSettingsManager<GridSettings> settings_manager)
: base("Market View", bus, factory, SparkCommands.ShowMarketView.icon) {
@@ -42,13 +52,16 @@ namespace mbts.spark {
thread_id = config.Configuration.get<bts.config.MultiThreadedWindows>().market_view ? "MarketView" : null;
}
event Action<MarketViewWindowFactory.StartupData> _state_changed;
public override string window_type => _window_type;
public override UIElement Content => _view;
public override IEnumerable<SparkCommand> Commands => _commands;
public override MarketViewWindowFactory.StartupData SaveState =>
_state = new MarketViewWindowFactory.StartupData(_state.security_criteria, _vm.get_filter_states(), _vm.get_grid_save_state(), _vm.rows_view.get_sorting_criteria(_vm.columns));
_state = new MarketViewWindowFactory.StartupData(
_state.security_criteria,
_vm.get_filter_states(),
_settings_manager.instance_settings,
_vm.sorting_hierarchy.Select(tup => new SortingCriteria(tup.key.ToString(), tup.direction == System.ComponentModel.ListSortDirection.Ascending))
.ToList());
public override double MinWidth => MarketViewWindowFactory.StartupData.DefaultGridSettings.columns.Where(c => c.is_visible).Sum(gcs => gcs.width)
+ SPARK_MENU_COLLAPSED_WIDTH
@@ -68,43 +81,33 @@ namespace mbts.spark {
return _selected_security_criteria_backing;
}
}
SecurityCriteria _sorted_selected_security_criteria {
get {
if (_sorted_selected_security_criteria_backing == null) {
if (_vm != null) {
var sorting_criteria = _vm.rows_view.get_sorting_criteria(_vm.columns);
var comp_builder = new ComparerBuilder<MarketViewVM.Row>();
foreach (var sc in sorting_criteria) {
if (_vm.columns.TryGetFirst(col => string.Equals(col.id, sc.col_name, StringComparison.CurrentCultureIgnoreCase), out var match)) {
if (match is ui.controls.DIYGrid.IKeyedColumn<MarketViewView.Columns> kcol) {
var col_comp = MarketViewVM.get_column_comparer(kcol.key);
if (col_comp != null) {
comp_builder.add(sc.is_ascending ? col_comp : col_comp.reverse());
} else {
Log.Ui.warn($"{_window_type} is unable to find the row comparer for the '{sc.col_name}' column.");
}
} else {
Log.Ui.warn($"{_window_type} is unable find a column with id '{sc.col_name}.'");
}
}
}
var comp = comp_builder.to_comparer();
_sorted_selected_security_criteria_backing = new SecurityCriteria(_vm.selected_rows.Ordered(comp).Select(r => r.security));
}
}
return _sorted_selected_security_criteria_backing;
}
}
SecurityCriteria _sorted_selected_security_criteria => _sorted_selected_security_criteria_backing ?? (
_sorted_selected_security_criteria_backing = _vm != null ?
new SecurityCriteria(_vm.selected_rows.Ordered(_vm.comparer).Select(r => r.security)) :
null);
bool _has_selected_rows() => _vm != null && _vm.selected_rows.Count > 0;
public void thread_init() {
_init_guard.check_access();
Log.notnull(_state);
var state_src = Observable.FromEvent<Action<MarketViewWindowFactory.StartupData>, MarketViewWindowFactory.StartupData>(cb => _state_changed += cb, cb => _state_changed -= cb).StartWith(_state);
_vm = new MarketViewVM(_bus, _orders_model, state_src, _settings_manager);
var update_securities_cmd = SparkCommands.change_securities_callback(_window_type_display_name);
_vm = new MarketViewVM(
_bus,
_orders_model,
_bus.subscribe(update_securities_cmd.tag)
.StartWith(_state.security_criteria)
.SelectManyInSeries(c => c.subscribe_md_map(_bus))
.BufferLatest(TimeSpan.FromSeconds(1))
.Table(SecurityMD.key_fn)
.Where(t => t.has_changes())
.OnDispatcher());
_vm.load_filters(_state.filters);
_view = new MarketViewView { DataContext = _vm };
_view._grid.Sorting += on_grid_sorting;
_grid_settings_manager = new SimpleGridSettingsManager<MarketViewVM.Row>(_vm.columns, _col_settings_flags, _vm.rows);
_grid_settings_manager.load_from(_settings_manager.current_settings());
/// The 'CanExecute' plumbing for the cmds below is designed to avoid potentially costly .ToList() operations on the selected_rows/securities.
/// Some of the cmds post ITags with data that is, or has, a SecurityCriteria member; the SecurityCriteria constructor creates a new List of the specified securities.
/// For these commands, the objective is to evaluate 'CanExecute' without instantiating the SecurityCriteria (or more importantly, without creating a new List).
@@ -135,11 +138,22 @@ namespace mbts.spark {
var show_synth_create_actual_cmd = SparkCommands.ShowSyntheticCreate();
var show_synth_create_cmd_wrapper = new SparkCommandCommand(show_synth_create_actual_cmd.title, show_synth_create_actual_cmd.icon, show_synth_create_cmd, SparkCommands.KeyGestures.ShowSyntheticCreate);
var update_securities_cmd = SparkCommands.change_securities_callback(_window_type_display_name);
var show_mini_ob_cmd = ObservantCommand.create<SecurityMD?, SecurityMD>(_view.show_mini_orderbook, s => s.HasValue, s => s.Value, out var show_mini_ob_observer);
// commands
var current_sec_md_src = Observable.Create<SecurityMD?>(o => {
notify(null, null);
_vm.current_row_changed += notify;
return Disposable.Create(() => _vm.current_row_changed -= notify);
void notify(MarketViewVM.Row oldval, MarketViewVM.Row newval) {
if (newval != null)
o.OnNext(newval.security_md);
else o.OnNext(null);
}
});
_commands = new List<SparkCommand> {
SparkCommands.ShowLadders(SparkCommands.KeyGestures.ShowLadder).complete(_vm.selected_security_mds_observable(), smds => smds.Count > 0),
SparkCommands.ShowOrderTicketFromSecurity(SparkCommands.KeyGestures.ShowOrderTicket).complete(_vm.current_security_md, md => md.HasValue, ConvertUtils.ValueOrDefault),
SparkCommands.ShowLadders(SparkCommands.KeyGestures.ShowLadder).complete(_vm.selected_rows.Observe().Select(_ => _vm.selected_rows.WrapSelectorRO(r => r.security_md)), smds => smds.Count > 0),
SparkCommands.ShowOrderTicketFromSecurity(SparkCommands.KeyGestures.ShowOrderTicket).complete(current_sec_md_src, md => md.HasValue, ConvertUtils.ValueOrDefault),
new SparkCommandCommand("Working Orders", Icons.Orders, show_mini_ob_cmd),
show_fills_cmd_wrapper,
show_tas_cmd_wrapper,
show_synth_create_cmd_wrapper,
@@ -150,7 +164,7 @@ namespace mbts.spark {
_settings_manager,
us => us.market_view_grid,
UserSettings.FileExtensions.GRID,
s => _vm.grid_settings_manager.create_vm(
s => _grid_settings_manager.create_vm(
// we need to get the most up to date GridSettings when launching b/c users may
// have changed some settings on the grid directly, like column widths.
s.with(_vm.columns.Select(c => c.create()))
@@ -163,16 +177,19 @@ namespace mbts.spark {
}))
};
//
_vm.selection_changed += on_selection_changed;
_subscriptions = new CompositeDisposable(_bus.subscribe(update_securities_cmd.tag).OnDispatcher().Subscribe(c =>
_state_changed?.Invoke(_state = new MarketViewWindowFactory.StartupData(c, _vm.get_filter_states(), _vm.grid_settings_manager.create())))) {
_vm.selected_rows.CollectionChanged += on_selection_changed;
_subscriptions = new CompositeDisposable(
current_sec_md_src.Subscribe(show_mini_ob_observer),
_settings_manager.subscribe_current().OnDispatcher().Subscribe(new CompositeObserver<GridSettings>(_grid_settings_manager) {
s => _vm.selection_highlight = s.selection_highlight
})) {
() => {
_vm.selection_changed -= on_selection_changed;
_vm.selected_rows.CollectionChanged -= on_selection_changed;
_view._grid.Sorting -= on_grid_sorting;
}
};
void on_selection_changed((IEnumerable<MarketViewVM.Row> removed, IEnumerable<MarketViewVM.Row> added) change) {
void on_selection_changed(object sender, NotifyCollectionChangedEventArgs e) {
// invalidate selected security criteria
_selected_security_criteria_backing = null;
_sorted_selected_security_criteria_backing = null;
@@ -1,6 +1,7 @@
using bts.api;
using bts.proto;
using bts.settings;
using bts.utils;
using mbts.persist;
using mbts.spark;
using mbts.spark.filters;
@@ -100,6 +101,7 @@ namespace bts.shell {
public static GridColumnSettings security => new GridColumnSettings(col_id(Columns.Security), DefaultColumnHeaders.Security, width: DefaultColumnWidths.Security);
public static GridColumnSettings volume => new GridColumnSettings(col_id(Columns.Volume), DefaultColumnHeaders.Volume, width: DefaultColumnWidths.Volume);
public static GridColumnSettings last => new GridColumnSettings(col_id(Columns.Last), DefaultColumnHeaders.Last, width: DefaultColumnWidths.Price);
public static GridColumnSettings last_from_spread => new GridColumnSettings(col_id(Columns.LastSpd), DefaultColumnHeaders.S, width: DefaultColumnWidths.Price, is_visible: false);
public static GridColumnSettings last_size => new GridColumnSettings(col_id(Columns.LastSize), DefaultColumnHeaders.Size, width: DefaultColumnWidths.Size, horizontal_alignment: System.Windows.HorizontalAlignment.Right);
public static GridColumnSettings position => new GridColumnSettings(col_id(Columns.Position), DefaultColumnHeaders.Pos, width: DefaultColumnWidths.Size, horizontal_alignment: System.Windows.HorizontalAlignment.Right);
public static GridColumnSettings raw_day_position => new GridColumnSettings(col_id(Columns.RawDayPosition), DefaultColumnHeaders.RawDayPos, width: DefaultColumnWidths.Size, horizontal_alignment: System.Windows.HorizontalAlignment.Right, is_visible: false);
@@ -138,6 +140,7 @@ namespace bts.shell {
yield return security;
yield return volume;
yield return last;
yield return last_from_spread;
yield return last_size;
yield return position;
yield return raw_day_position;
@@ -224,7 +227,49 @@ namespace bts.shell {
[ProtoMember(25)]
public string strip_name;
[ProtoMember(26)]
public string last_price_from_spread;
[ProtoMember(28)]
public string mic_code;
public Func<MarketViewVM.Row, bool> build_predicate() {
var pb = new ColumnPredicateBuilder<MarketViewVM.Row>();
// security
pb.add_regex(security, r => r.security.ToString());
pb.add_regex(security_key, r => r.security_key.ToString());
// bid
pb.add_numeric(bid_price, r => r.bid_price);
pb.add_numeric(bid_merged_size, r => r.bid_merged_size);
pb.add_numeric(bid_direct_size, r => r.bid_direct_size);
pb.add_numeric(bid_implied_size, r => r.bid_implied_size);
pb.add_numeric(working_bid_volume, r => r.working_bid_volume);
// ask
pb.add_numeric(ask_price, r => r.ask_price);
pb.add_numeric(ask_implied_size, r => r.ask_implied_size);
pb.add_numeric(ask_direct_size, r => r.ask_direct_size);
pb.add_numeric(ask_merged_size, r => r.ask_merged_size);
pb.add_numeric(working_ask_volume, r => r.working_ask_volume);
// last
pb.add_numeric(last_price, r => r.last_price);
pb.add_regex(last_price_from_spread, r => r.last_price_from_spread.ToString());
pb.add_numeric(last_size, r => r.last_size);
// position
pb.add_numeric(position, r => r.position);
pb.add_numeric(raw_day_position, r => r.raw_day_position);
// market
pb.add_numeric(day_change, r => r.day_change);
pb.add_numeric(settlement, r => r.settlement);
pb.add_numeric(session_low, r => r.session_low);
pb.add_numeric(session_high, r => r.session_high);
pb.add_numeric(volume, r => r.volume);
// ice
pb.add_regex(market_description, r => r.market_description);
pb.add_regex(cleared_alias, r => r.cleared_alias);
pb.add_regex(product_description, r => r.product_description);
pb.add_regex(hub_alias, r => r.hub_alias);
pb.add_regex(strip_name, r => r.strip_name);
pb.add_regex(mic_code, r => r.mic_code);
return pb.to_predicate();
}
}
// migration rules
@@ -267,5 +312,11 @@ namespace bts.shell {
return false;
}
}
[Export(typeof(IMarketViewGridSettingsMigrationRule))]
class EnsureSelectionHighlightTranslucency : IMarketViewGridSettingsMigrationRule {
public bool apply(GridSettings settings, out GridSettings new_settings) =>
GridSettingsMigrationRules.SelectionHighlightIsTranslucent.apply(settings, out new_settings);
}
}
}
@@ -13,5 +13,5 @@ namespace mbts.ui.spark {
InitializeComponent();
}
class TimeAndSalesRowView : DIYGrid.HighPerformanceRow<TimeAndSalesVM.Row, TimeAndSalesView.Columns> { }
class TimeAndSalesRowView : DIYGrid.HighPerformanceRow<TimeAndSalesVM.Row> { }
}
@@ -1,4 +1,5 @@
using bts.api;
using bts.settings;
using bts.spark;
using mbts;
using mbts.co2;
@@ -10,6 +11,7 @@ using mbts.ui.settings;
using ProtoBuf;
using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.Linq;
using System.Reactive.Linq;
using System.Threading.Tasks;
@@ -38,7 +40,7 @@ namespace bts.shell {
public override Task<IShellWindow> load(StartupData state) =>
Task.FromResult<IShellWindow>(new TimeAndSalesWindow(this, _bus, _consumer, state, SparkCommands.PickTimeAndSales.icon,
new WindowSettingsManager<GridSettings>(_settings_hub, state.grid, s => s.time_and_sales_grid, (s, g) => s.with(time_and_sales:g))));
_settings_hub.create_time_and_sales_settings_manager(state.grid)));
public async void show(SecurityCriteria criteria) {
var w = await load(new StartupData(criteria));
@@ -122,5 +124,11 @@ namespace bts.shell {
[ProtoMember(7)]
public co2.AggressorSide[] aggressor_sides;
}
[Export(typeof(ITimeAndSalesGridSettingsMigrationRule))]
class EnsureSelectionHighlightTranslucency : ITimeAndSalesGridSettingsMigrationRule {
public bool apply(GridSettings settings, out GridSettings new_settings) =>
GridSettingsMigrationRules.SelectionHighlightIsTranslucent.apply(settings, out new_settings);
}
}
}
@@ -227,7 +227,7 @@ namespace mbts.ui.spark {
}
DataGridColumn _column_factory(LegSpreaderParamsTuple l) {
var col = new DataGridColumnInternal(l.leg.Security, ParameterCellTemplateConverter, ParameterCellEditingTemplateConverter) { Width = 75, };
var col = new DataGridColumnInternal(l.leg.Security, ParameterCellTemplateConverter, ParameterCellEditingTemplateConverter);
col.HeaderTemplate = (DataTemplate)FindResource(IsNew ? RemovableColumnHeaderTemplateKey : NonRemovableColumnHeaderTemplateKey);
DataGridEx.SetRightBorder(col, new DataGridExVerticalLine(false, true, Colors.DarkGray, 1));
return col;
@@ -1,7 +1,9 @@
using bts.api;
using bts;
using bts.api;
using bts.api.core;
using bts.api.synthetic;
using bts.collections;
using bts.spark;
using mbts.ui.shell;
using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
@@ -18,11 +20,36 @@ namespace mbts.spark {
readonly uint _user_id;
[ImportingConstructor]
public SpreaderModel(IBus bus, SyntheticSecurityService service, ErrorReporter error, [Import(LoginContainer.Exports.UserID)]uint user_id) {
public SpreaderModel(IBus bus, SyntheticSecurityService service, AdminModel admin_model, ErrorReporter error, UserTuple user) {
_bus = bus;
_service = service;
_error = error;
_user_id = user_id;
_user_id = user.id;
// synthetic tuples
var strategy_source = _bus.subscribe(SparkTags.SyntheticstrategiesTag)
.Select(map => MappedDictionary.create<uint, ExecutionStrategyImmutable, (uint security_id, ExecutionStrategyImmutable strategy)>(map, (k, v) => (k, v)))
.Table(tup => tup.security_id);
var synthetic_source = _bus.subscribe(SparkTags.SecuritiesTag)
.Select(map => new FilteredDictionary<uint, SecurityMD>(map, smd => smd.security.ContractCase == Security.ContractOneofCase.Synthetic))
.Table(smd => smd.security.Id);
var synthetic_user_join = synthetic_source.LeftJoin(
admin_model.all_user_tuples,
SecurityMD.key_fn,
UserTuple.key_fn,
smd => smd.security.Synthetic.UserId,
(smd, sid, ut, uid) => new SyntheticTuple(smd.security, ut.name, null),
(smd, sid, uid) => new SyntheticTuple(smd.security, null, null),
SyntheticTuple.key_fn);
synthetic_tuples = synthetic_user_join.LeftJoin(
strategy_source,
SyntheticTuple.key_fn,
tup => tup.security_id,
(synth_tup, id, strat_tup) => new SyntheticTuple(synth_tup.security, synth_tup.user_name, strat_tup.strategy),
(synth_tup, id) => synth_tup,
SyntheticTuple.key_fn);
}
public IObservable<IReadOnlyDictionary<uint, ExecutionStrategyImmutable>> strategy => _bus.subscribe(SparkTags.SyntheticstrategiesTag);
@@ -36,6 +63,8 @@ namespace mbts.spark {
.LeftJoin(strategies, (security, strategy) => (new ExecutionStrategyImmutable?(strategy), security), security => (new ExecutionStrategyImmutable?(), security))
);
public IObservable<TableUpdate<SyntheticTuple, uint>> synthetic_tuples { get; }
public IObservable<ExecutionStrategyImmutable> security_strategy(uint security_key) =>
strategy.TrySelect(s => {
bool success = s.TryGetValue(security_key, out ExecutionStrategyImmutable result);
@@ -7,11 +7,18 @@ namespace mbts.ui.spark {
/// Interaction logic for SyntheticManagementView.xaml
/// </summary>
partial class SyntheticManagementView : UserControl {
public enum Columns {
Name,
Description,
n_legs,
n_working_orders,
SecurityKey,
User
}
public enum Columns { Name, Description, n_legs, n_working_orders, SecurityKey}
public SyntheticManagementView() =>
InitializeComponent();
}
class SyntheticManagementRowView : DIYGrid.HighPerformanceRow<SyntheticManagementDialogVM.Row, SyntheticManagementView.Columns> { }
class SyntheticManagementRowView : DIYGrid.HighPerformanceRow<SyntheticManagementDialogVM.Row> { }
}
@@ -10,6 +10,7 @@ using mbts.ui.utility;
using mbts.utils;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Reactive.Linq;
using System.Windows;
@@ -27,7 +28,7 @@ namespace mbts.spark {
readonly IWindowSettingsManager<GridSettings> _settings_manager;
public SyntheticManagementWindow(SyntheticManagementWindowFactory factory, IBus bus, OrdersModel orders_model, StartupData state, WindowSettingsManager<GridSettings> settings_manager)
public SyntheticManagementWindow(SyntheticManagementWindowFactory factory, IBus bus, OrdersModel orders_model, StartupData state, IWindowSettingsManager<GridSettings> settings_manager)
: base("Synthetic Management", bus, factory, SparkCommands.ShowSyntheticManagement.icon) {
_orders_model = orders_model;
_state = state;
@@ -66,8 +67,8 @@ namespace mbts.spark {
), SparkCommand.create("Clone", Icons.CloneSynthetic, SparkTags.ShowSyntheticCloneTag).complete(
current_row_connectable_builder.create(observe_row_strategy),
rs => rs.HasValue, select_row_strategy_tuple
), SparkCommands.ShowLadder(SparkCommands.KeyGestures.ShowLadder).complete(
vm_connectable_builder.create(vm => vm.current_security_md), md => md.HasValue, ConvertUtils.ValueOrDefault
), SparkCommands.ShowLadderAutoLookup(SparkCommands.KeyGestures.ShowLadder).complete(
vm_connectable_builder.create(vm => vm.current_security_observable.Select(s => s.HasValue ? ImmutableHashSet<uint>.Empty.Add(s.Value.Id) : null)), id => id != null
), SparkCommands.ShowOrders.complete(() => {
var ob_settings = bts.shell.orderbook.OrderBookStartupData.Default;
ob_settings.filters = new bts.shell.orderbook.FilterStates {
@@ -1,4 +1,5 @@
using bts.api;
using bts.settings;
using mbts.persist;
using mbts.spark;
using mbts.spark.filters;
@@ -7,6 +8,7 @@ using mbts.ui.settings;
using ProtoBuf;
using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.Reactive.Linq;
using System.Threading.Tasks;
using static mbts.ui.controls.DIYGrid;
@@ -33,13 +35,8 @@ namespace bts.shell {
public async void show(SparkTags.Nothing nothing) =>
_bus.post(SparkTags.ShowWindow, await load(new StartupData()));
IShellWindow _create(StartupData state) {
var settings_manager = new WindowSettingsManager<GridSettings>(_settings_hub,
state.grid,
get: us => us.synthetic_management_grid,
set: (us, gs) => us.with(provided_synthetic_management_gridsettings: gs));
return new SyntheticManagementWindow(this, _bus, _orders_model, state, settings_manager);
}
IShellWindow _create(StartupData state) =>
new SyntheticManagementWindow(this, _bus, _orders_model, state, _settings_hub.create_synth_settings_manager(state.grid));
[ProtoContract]
public class StartupData {
@@ -67,6 +64,7 @@ namespace bts.shell {
public static GridColumnSettings n_legs => new GridColumnSettings(col_id(Columns.n_legs), DefaultColumnHeaders.N_Legs, width: DefaultColumnWidths.N_Legs);
public static GridColumnSettings n_working_orders => new GridColumnSettings(col_id(Columns.n_working_orders), DefaultColumnHeaders.N_Working_Orders, width: DefaultColumnWidths.N_Working_Orders);
public static GridColumnSettings security_key => new GridColumnSettings(col_id(Columns.SecurityKey), DefaultColumnHeaders.SecurityKey, is_visible: false, width: DefaultColumnWidths.SecurityKey);
public static GridColumnSettings user => new GridColumnSettings(col_id(Columns.User), DefaultColumnHeaders.User, is_visible: false, width: DefaultColumnWidths.User);
public static IEnumerable<SortingCriteria> default_sorting_criterion {
get {
@@ -81,6 +79,7 @@ namespace bts.shell {
yield return n_legs;
yield return n_working_orders;
yield return security_key;
yield return user;
}
}
@@ -101,6 +100,15 @@ namespace bts.shell {
public UIntMinMaxState n_working_orders;
[ProtoMember(6)]
public string security_key;
[ProtoMember(7)]
public uint[] user;
}
// migration rules
[Export(typeof(ISyntheticManagementMigrationRule))]
class AddMissingColumnsMigrationRule : ISyntheticManagementMigrationRule {
public bool apply(GridSettings settings, out GridSettings new_settings) =>
settings.add_missing_columns(StartupData.DefaultGridSettings.columns, out new_settings);
}
}
}
}
@@ -1,12 +1,15 @@
using bts.api;
using bts;
using bts.api;
using bts.collections;
using bts.spark;
using bts.utils;
using mbts.persist;
using mbts.spark.filters;
using mbts.ui;
using mbts.ui.settings;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive.Linq;
@@ -17,43 +20,45 @@ using static mbts.ui.controls.DIYGrid;
using static mbts.ui.spark.SyntheticManagementView;
namespace mbts.spark {
class SyntheticManagementDialogVM : SecurityMDGrid<SyntheticManagementDialogVM.Row, (ExecutionStrategyImmutable? strategy, SecurityMD md)> {
class SyntheticManagementDialogVM : SecurityGrid<SyntheticManagementDialogVM.Row, SyntheticTuple> {
readonly IBus _bus;
readonly OrdersModel _orders_model;
IWindowSettingsManager<GridSettings> _settings_manager;
readonly IWindowSettingsManager<GridSettings> _settings_manager;
public SyntheticManagementDialogVM(IBus bus, OrdersModel orders_model, IWindowSettingsManager<GridSettings> settings_manager) : this(bus, orders_model, settings_manager, null, null) { }
public SyntheticManagementDialogVM(IBus bus, OrdersModel orders_model, IWindowSettingsManager<GridSettings> settings_manager, IReadOnlyCollection<SortingCriteria> sorting, FilterStates filters)
: base(orders_model.spreader.strategy_tuple.Select(map => map.Values), sorting) {
: base(orders_model.spreader.synthetic_tuples.Select(t => t.state.Values), sorting) {
_bus = bus;
_orders_model = orders_model;
_settings_manager = settings_manager;
// init filters
user_filter = DiscreteValuesFilterVM.CreateTableUpdate<Row, UserTuple, uint>(r => r.user_tuple, UserTuple.key_fn, out var user_filter_items_observer, out var user_filter_items_view, nameof(Row.user_tuple));
user_filter_items_view.CustomSort = Comparer<string>.Default.wrap<string, UserTuple>(ut => ut.name).to_non_generic();
if (filters != null) {
create_time_filter.load_state(filters.create_time);
name_filter.load_state(filters.name);
description_filter.load_state(filters.description);
n_legs_filter.load_state(filters.n_legs);
n_working_orders_filter.load_state(filters.n_working_orders);
security_key_filter.load_state(filters.security_key);
security_key_filter.load_state(filters.security_key);
user_filter.load_state(filters.user);
}
_settings_manager = settings_manager;
// columns
add_col(Columns.Name, DefaultColumnHeaders.Name, r => r.name, nameof(Row.name), name_filter, DataGridFilterKnownTemplates.Regex, DefaultColumnWidths.Security);
add_col(Columns.Description, DefaultColumnHeaders.Description, r => r.description, nameof(Row.description), description_filter, DataGridFilterKnownTemplates.Regex, DefaultColumnWidths.Description);
add_col(Columns.n_legs, DefaultColumnHeaders.N_Legs, r => r.n_legs.ToString(), nameof(Row.n_legs), n_legs_filter, DataGridFilterKnownTemplates.IntMinMax, DefaultColumnWidths.N_Legs);
columns.Add(new HyperlinkElementColumn<Row, Columns>(
Columns.n_working_orders,
new Binding(nameof(Row.n_working_orders)),
new Binding(nameof(Row.show_order_book)),
columns.Add(new HyperlinkElementColumn<Columns>(
Columns.n_working_orders,
n_working_orders_filter,
DataGridFilterKnownTemplates.UIntMinMax) {
id = col_id(Columns.n_working_orders),
CommandBinding = new Binding(nameof(Row.show_order_book)),
TextBinding = new Binding(nameof(Row.n_working_orders)),
Header = DefaultColumnHeaders.N_Working_Orders,
Width = DefaultColumnWidths.N_Working_Orders
}
);
});
add_col(Columns.SecurityKey, DefaultColumnHeaders.SecurityKey, r => r.security_key.ToString(), nameof(Row.security_key), security_key_filter, DataGridFilterKnownTemplates.Regex, DefaultColumnWidths.SecurityKey);
add_col(Columns.User, DefaultColumnHeaders.User, r => r.user_name, nameof(Row.user_name), user_filter, DataGridFilterKnownTemplates.DiscreteValues, DefaultColumnWidths.User);
//
void add_col(Columns col_key, string header, Func<Row, string> get_text, string prop, IDataGridColumnFilter filter, string filter_template, double width) =>
columns.Add(new RenderTextColumn<Row, Columns>(col_key, get_text, filter, filter_template) {
@@ -63,42 +68,39 @@ namespace mbts.spark {
Width = width
});
//
grid_settings_manager = new SimpleGridSettingsManager<Row>(columns, new Dictionary<string, GridColumnSettingsFeatureFlags> { }, row_vms);
grid_settings_manager = new SimpleGridSettingsManager<Row>(columns, DictionaryHelper.empty<string, GridColumnSettingsFeatureFlags>(), row_vms);
// this will initialize the immutable properties that correspond to the col_settings_flags above (e.g. Bid/Ask font_color_override, etc.)
grid_settings_manager.load_from(settings_manager.current_settings());
_resources.AddRange(
DataGridFilterHelper.hook_filters(rows_view, create_time_filter, name_filter, description_filter, n_legs_filter, n_working_orders_filter, security_key_filter),
orders_model.admin_model.all_user_tuples.OnDispatcher().Subscribe(user_filter_items_observer),
DataGridFilterHelper.hook_all<SyntheticManagementDialogVM, Row>(this, rows_view),
settings_manager.subscribe_current().OnDispatcher().Subscribe(grid_settings_manager)
);
}
protected override Row row_factory((ExecutionStrategyImmutable? strategy, SecurityMD md) row_model) =>
new Row(_bus, row_model.md, row_model.strategy, _orders_model);
protected override void on_row_removed(Row row) => row.Dispose();
public ObservableCollection<ColumnBase> columns { get; } = new ObservableCollection<ColumnBase>();
public ObservableCollection<ColumnBase> columns { get; } = new ObservableCollection<ColumnBase>();
public SimpleGridSettingsManager<Row> grid_settings_manager { get; }
public GridSettings get_grid_save_state() {
if (_settings_manager.instance_settings == null)
return null;
else
return grid_settings_manager.create();
}
public StartupData create() =>
new StartupData(rows_view.get_sorting_criteria(columns), _get_filter_states(), get_grid_save_state());
#region Filters
public IDataGridColumnPersistentFilter<Row, TimeFilterTuple> create_time_filter { get; } = new TimeFilterVM<Row>(r => r.create_time, nameof(Row.create_time));
public IDataGridColumnPersistentFilter<Row, string> name_filter { get; } = new RegexFilterVM<Row>(r => r.name, nameof(Row.name));
public IDataGridColumnPersistentFilter<Row, string> description_filter { get; } = new RegexFilterVM<Row>(r => r.description, nameof(Row.description));
public IDataGridColumnPersistentFilter<Row, IntMinMaxState> n_legs_filter { get; } = new IntMinMax<Row>(r => r.n_legs, nameof(Row.n_legs));
public IDataGridColumnPersistentFilter<Row, UIntMinMaxState> n_working_orders_filter { get; } = new UIntMinMax<Row>(r => r.n_working_orders, nameof(Row.n_legs));
public IDataGridColumnPersistentFilter<Row, string> security_key_filter { get; } = new RegexFilterVM<Row>(r => r.security_key.ToString(), nameof(Row.security_key));
public IDataGridColumnPersistentFilter<Row, uint[]> user_filter { get; }
public GridSettings get_grid_save_state() =>
_settings_manager.instance_settings == null ? null : grid_settings_manager.create();
public StartupData create() =>
new StartupData(rows_view.get_sorting_criteria(columns), _get_filter_states(), get_grid_save_state());
protected override Row row_factory(SyntheticTuple row_model) =>
new Row(_bus, row_model, _orders_model);
protected override void on_row_removed(Row row) =>
row.Dispose();
FilterStates _get_filter_states() =>
new FilterStates {
@@ -107,19 +109,19 @@ namespace mbts.spark {
name = name_filter.get_state(),
n_legs = n_legs_filter.get_state(),
n_working_orders = n_working_orders_filter.get_state(),
security_key = security_key_filter.get_state()
security_key = security_key_filter.get_state(),
user = user_filter.get_state()
};
#endregion
public class Row : SecurityMDRow, IEquatable<Row>, IRowVM, IActionableRowVM {
public readonly ExecutionStrategyImmutable? strategy;
public class Row : SecurityRow, IEquatable<Row>, IRowVM, IActionableRowVM {
public readonly SyntheticTuple synthetic_tuple;
readonly IBus _bus;
readonly Property<uint> _n_working_orders;
public Row(IBus bus, SecurityMD security_md, ExecutionStrategyImmutable? strategy, OrdersModel orders_model) : base(security_md) {
public Row(IBus bus, SyntheticTuple synthetic_tuple, OrdersModel orders_model) : base(synthetic_tuple.security) {
_bus = bus;
this.strategy = strategy;
this.synthetic_tuple = synthetic_tuple;
_n_working_orders = backing(nameof(n_working_orders),
orders_model.on_security_orders(security_key)
.Select(t => (uint)(t.state
@@ -133,25 +135,23 @@ namespace mbts.spark {
show_order_book = new DelegateCommand(_show_order_book);
}
public event Action invalidated;
public DateTime create_time => DateTime.MinValue; //TODO
public string name { get; }
public string user_name => synthetic_tuple.user_name;
public UserTuple user_tuple => new UserTuple(synthetic_tuple.security.Synthetic.UserId, user_name);
public int n_legs { get; }
public string description { get; }
public ExecutionStrategyImmutable? strategy => synthetic_tuple.strategy;
public bool strategy_present => strategy.HasValue;
public uint n_working_orders => _n_working_orders.Value;
public ICommand show_ladder { get; }
public ICommand show_order_book { get; }
public ICommand default_command => show_ladder;
public ObservableCollection<RowAnimation> row_animations => new ObservableCollection<RowAnimation>();
public IReadOnlyList<IGridCellStore> cell_stores => new IGridCellStore[0];
public event Action invalidated;
public void raise_invalidated() => invalidated?.Invoke();
public void raise_invalidated() =>
invalidated?.Invoke();
public bool Equals(Row other) =>
other != null &&
@@ -163,10 +163,8 @@ namespace mbts.spark {
public override int GetHashCode() =>
security_key.GetHashCode();
void _show_ladder() =>
_bus.post(SparkTags.ShowLadderTag, md);
_bus.post(SparkTags.ShowLadderAutoLookupTag, ImmutableHashSet<uint>.Empty.Add(security.Id));
void _show_order_book() {
var ob_settings = bts.shell.orderbook.OrderBookStartupData.Default;
@@ -177,4 +175,4 @@ namespace mbts.spark {
}
}
}
}
}
@@ -62,7 +62,10 @@ namespace mbts.spark {
_vm = new AlgoStudioVM(_model);
_vm.load_from(_state);
_view = new AlgoStudioView { DataContext = _vm };
_resources = new CompositeDisposable();
_resources = new CompositeDisposable(
_bus.subscribe(SparkTags.SecuritiesTag)
.OnDispatcher()
.Subscribe(sec_map => _view.Resources[ResourceKeys.Securities] = sec_map));
// add definition
_commands.Add(new SparkCommandCommand("Add Algo", Icons.AddBoxes, _vm.add_definition_cmd));
// clone definition
@@ -27,7 +27,7 @@ namespace mbts.spark {
_error_reporter = error_reporter;
definitions =
service.subscribei_algo_definitions()
.Filter(r => r.Metadata.IsActive && r.Metadata.CreatorUserId == _admin_model.user.id)
.Filter(r => r.Metadata.IsActive && _admin_model.user.can_access_user_data(r.Metadata.CreatorUserId))
.SelectKeyValue(_definition_data, d => d.key)
.Table()
.Cache();
@@ -13,6 +13,9 @@ namespace mbts.spark {
string _template_key;
ICommand _set_value_cmd;
public AlgoParamRowVM(IAlgoVM algo, IAlgoParamVM param, AlgoParamRowVM parent)
: this(algo, param, parent, (param as ITemplateSelectorKey)?.key) { }
public AlgoParamRowVM(IAlgoVM algo, IAlgoParamVM param, AlgoParamRowVM parent, string template_key = null) {
this.param = param;
this.algo = algo;
@@ -247,6 +247,31 @@
</TextBlock>
</DataTemplate>
</u:TemplateSelectorKeyValue>
<u:TemplateSelectorKeyValue Key="{x:Static u:TemplateKeys.SecurityKey}">
<DataTemplate>
<Grid>
<!--Note: Ideally, this 'securities BindingProxy' would be a static resource, but it only seems to detect the dynamic securities when it is in the visual tree.-->
<!--<Grid.Resources>
<u:BindingProxy x:Key="SecuritiesBindingProxy" x:Shared="False" Value="{DynamicResource {x:Static u:ResourceKeys.Securities}}"/>
</Grid.Resources>-->
<u:BindingProxy x:Name="SecuritiesBindingProxy" Value="{DynamicResource {x:Static u:ResourceKeys.Securities}}"/>
<TextBlock>
<TextBlock.Text>
<MultiBinding>
<MultiBinding.Converter>
<cv:MultiValueConverterPipe>
<cv:DictionaryLookupConverter/>
<x:Static MemberType="cv:ValueConverters" Member="SecuritySelectorText"/>
</cv:MultiValueConverterPipe>
</MultiBinding.Converter>
<Binding Path="Value" ElementName="SecuritiesBindingProxy"/>
<Binding Path="param.value"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</Grid>
</DataTemplate>
</u:TemplateSelectorKeyValue>
</usp:AlgoParamTemplateSelector.KeyedTemplates>
<!--Bool-->
<usp:AlgoParamTemplateSelector.BoolTemplate>
@@ -320,6 +345,21 @@
</usp:PriceEntryControl>
</DataTemplate>
</u:TemplateSelectorKeyValue>
<u:TemplateSelectorKeyValue Key="{x:Static u:TemplateKeys.SecurityKey}">
<DataTemplate>
<c:ContractPickerPopup
CloseOnCancel="True"
CloseOnOK="True"
Header="{Binding SelectedSecurity, RelativeSource={RelativeSource Self}, Converter={x:Static cv:ValueConverters.SecuritySelectorText}}"
IsEditMode="False"
IsLazy="True"
SecurityMap="{DynamicResource {x:Static u:ResourceKeys.Securities}}"
SelectedSecurityKey="{Binding param.value}"
ShowDialogSkirt="True"
OKClick="{x:Static c:ContractPickerPopup.FlushSelectedSecurityKeyHandler}"
/>
</DataTemplate>
</u:TemplateSelectorKeyValue>
</usp:AlgoParamTemplateSelector.KeyedTemplates>
<!--Bool-->
<usp:AlgoParamTemplateSelector.BoolTemplate>
@@ -118,13 +118,14 @@ namespace mbts.spark {
add_col_expl(Columns.Side, new SideColumn<Row, Columns>(Columns.Side, r => r.side, side_filter) { id = col_id(Columns.Side) }, side_filter);
// all others (easily identified by the enum 'Columns.Foo')
add_col(Columns.FillTime, DefaultColumnHeaders.FillTime, r => r.fill_time.ToString("M/d h:mm:ss tt"), new TimeFilterVM<FillTuple>(r => r.fill.Timestamp.ToDateTimeOffset().LocalDateTime), DataGridFilterKnownTemplates.TimeFilter, DefaultColumnWidths.Time);
add_col(Columns.User, DefaultColumnHeaders.User, r => r.user, dv_filter_rx(_model.factory.subscribe_users().Select(d => d.Values), r => r.order?.user() ?? string.Empty), DataGridFilterKnownTemplates.DiscreteValues, DefaultColumnWidths.User);
add_col(Columns.Account, DefaultColumnHeaders.Account, r => r.account, dv_filter_rx(_model.factory.subscribe_accounts().Select(d => d.Values), r => r.account), DataGridFilterKnownTemplates.DiscreteValues, DefaultColumnWidths.Account);
add_col(Columns.User, DefaultColumnHeaders.User, r => r.user, dv_filter_rx(_model.factory.subscribe_users().Select(d => d.Values.Ordered()), r => r.order?.user() ?? string.Empty), DataGridFilterKnownTemplates.DiscreteValues, DefaultColumnWidths.User);
add_col(Columns.Account, DefaultColumnHeaders.Account, r => r.account, dv_filter_rx(_model.factory.subscribe_accounts().Select(d => d.Values.Ordered()), r => r.account), DataGridFilterKnownTemplates.DiscreteValues, DefaultColumnWidths.Account);
add_col(Columns.Exchange, DefaultColumnHeaders.Exchange, r => r.exchange.ToString(), dv_filter(EnumHelper.get_values<Venue>().ToArray(), r => r.security.exchange_or_default()), DataGridFilterKnownTemplates.DiscreteValues, DefaultColumnWidths.Exchange);
add_col(Columns.TimeInForce, DefaultColumnHeaders.TimeInForce, r => r.time_in_force.ToString(), dv_filter(EnumHelper.get_values<TimeInForce>().ToArray(), r => r.order?.time_in_force() ?? TimeInForce.Undefined), DataGridFilterKnownTemplates.DiscreteValues, DefaultColumnWidths.TimeInForce);
add_col(Columns.Size, DefaultColumnHeaders.Size, r => r.size.ToString(), new DoubleMinMax<FillTuple>(r => r.fill.LastShares), DataGridFilterKnownTemplates.UIntMinMax, DefaultColumnWidths.Size);
add_col(Columns.Price, DefaultColumnHeaders.Price, r => r.format_price(r.price), new DoubleMinMax<FillTuple>(r => r.fill.LastPrice), DataGridFilterKnownTemplates.DoubleMinMax, DefaultColumnWidths.Price);
add_col(Columns.FillKind, DefaultColumnHeaders.FillKind, r => r.fill_kind.ToString(), dv_filter(EnumHelper.get_values<FillKind>().ToArray(), r => r.fill.FillKind), DataGridFilterKnownTemplates.DiscreteValues, DefaultColumnWidths.FillKind);
add_col(Columns.FillStatus, DefaultColumnHeaders.FillStatus, r => r.fill_status.ToString(), dv_filter(EnumHelper.get_values<FillStatus>().ToArray(), r => r.fill.FillStatus), DataGridFilterKnownTemplates.DiscreteValues, DefaultColumnWidths.FillKind);
add_col(Columns.FillKey, DefaultColumnHeaders.FillKey, r => r.fill_key.ToString(), new RegexFilterVM<FillTuple>(r => r.fill.FillKey), DataGridFilterKnownTemplates.Regex, DefaultColumnWidths.FillKey);
add_col(Columns.OrderKey, DefaultColumnHeaders.OrderKey, r => r.order_key.ToString(), new RegexFilterVM<FillTuple>(r => r.fill.OrderKey.ToString()), DataGridFilterKnownTemplates.Regex, DefaultColumnWidths.OrderKey);
add_col(Columns.SecurityKey, DefaultColumnHeaders.SecurityKey, r => r.security_key.ToString(), new RegexFilterVM<FillTuple>(r => r.fill.SecurityKey.ToString()), DataGridFilterKnownTemplates.Regex, DefaultColumnWidths.SecurityKey);
@@ -146,6 +147,7 @@ namespace mbts.spark {
});
_dispose = new CompositeDisposable(
ObservableAutoWrapper.connect(col_tups, columns, ct => ct.col),
ObservableAutoWrapper.connect_item_hooks(col_tups, ct => ct.filter as IDisposable),
RXCollection.connect(rows, fills_buffer.Filter(filter_src, FillTuple.key_fn), comp_src, ft => new Row(ft, model), r => r.fill_tuple),
fills_source.OnDispatcher().Subscribe(t => {
last_unfiltered_fills = t;
@@ -227,6 +229,7 @@ namespace mbts.spark {
case Columns.Exchange: return wrap_default(ft => ft.security.exchange_or_default());
case Columns.FillKey: return wrap(ft => ft.fill.FillKey);
case Columns.FillKind: return wrap_default(ft => ft.fill.FillKind);
case Columns.FillStatus: return wrap_default(ft => ft.fill.FillStatus);
case Columns.FillTime: return wrap(ft => ft.fill.Timestamp.to_local_time());
case Columns.OrderKey: return wrap(ft => ft.fill.OrderKey);
case Columns.OrderType: return wrap_default(ft => ft.order?.order_type() ?? OrderType.None);
@@ -293,6 +296,7 @@ namespace mbts.spark {
public double price => _fill.LastPrice;
public double size => _fill.LastShares;
public FillKind fill_kind => _fill.FillKind;
public FillStatus fill_status => _fill.FillStatus;
public string fill_key => _fill.FillKey;
public string order_key => _order.Key.ToString();
@@ -21,6 +21,7 @@ namespace mbts.ui.spark {
Price,
Size,
FillKind,
FillStatus,
FillKey,
OrderKey,
SecurityKey
@@ -30,5 +31,5 @@ namespace mbts.ui.spark {
InitializeComponent();
}
class FillsRowView : DIYGrid.HighPerformanceRow<FillsVM.Row, FillsView.Columns> { }
class FillsRowView : DIYGrid.HighPerformanceRow<FillsVM.Row> { }
}
@@ -37,7 +37,8 @@ namespace bts.shell {
{ nameof(FillsVM.Row.side), Columns.Side },
{ nameof(FillsVM.Row.size), Columns.Size },
{ nameof(FillsVM.Row.time_in_force), Columns.TimeInForce },
{ nameof(FillsVM.Row.user), Columns.User }
{ nameof(FillsVM.Row.user), Columns.User },
{ nameof(FillsVM.Row.fill_status ), Columns.FillStatus },
};
public FillsWindowFactory(TradingShellModule module, IBus bus, UserSettingsHub settings_hub, OrdersModel orders_model)
@@ -71,5 +72,11 @@ namespace bts.shell {
public bool apply(GridSettings settings, out GridSettings new_settings) =>
settings.add_missing_columns(FillsSettings.DefaultGridSettings.columns, out new_settings);
}
[Export(typeof(IFillsGridSettingsMigrationRule))]
class EnsureSelectionHighlightTranslucency : IFillsGridSettingsMigrationRule {
public bool apply(GridSettings settings, out GridSettings new_settings) =>
GridSettingsMigrationRules.SelectionHighlightIsTranslucent.apply(settings, out new_settings);
}
}
}
@@ -74,6 +74,10 @@ namespace bts.shell.fills {
[ProtoMember(15)]
[DIYGridColumnID(nameof(Columns.Symbol))]
public string[] symbol { get; set; }
[ProtoMember(17)]
[DIYGridColumnID(nameof(Columns.FillStatus))]
public Fill.Types.FillStatus[] fill_status { get; set; }
}
static class FillsFilterStatesExtensions {
@@ -96,6 +100,7 @@ namespace bts.shell.fills {
pb.add_discrete_values(fs.side, ft => ft.fill.Side);
pb.add_discrete_values(fs.fill_kind, ft => ft.fill.FillKind);
pb.add_discrete_values(fs.symbol, ft => ft.security?.Symbol ?? string.Empty);
pb.add_discrete_values(fs.fill_status, ft => ft.fill.FillStatus);
// numeric
pb.add_numeric(fs.size, ft => ft.fill.LastShares);
pb.add_numeric(fs.price, ft => ft.fill.LastPrice);
@@ -1,4 +1,5 @@
using bts.api;
using bts.collections;
using bts.md;
using mbts;
using mbts.config;
@@ -50,6 +51,7 @@ namespace bts.shell {
_mbo = mbo;
_dispose = Disposable.Create(
bus.subscribe(SparkTags.SecuritiesTag).Subscribe(s => _securities = s),
bus.subscribe(SparkTags.ShowLadderAutoLookupTag).Subscribe(_show_windows),
bus.subscribe(SparkTags.ShowLadderTag).OnDispatcher().Subscribe(s => show_windows(new[] { s })),
bus.subscribe(SparkTags.ShowLaddersTag).OnDispatcher().Subscribe(show_windows)
);
@@ -273,7 +275,21 @@ namespace bts.shell {
});
}
void show_windows(IReadOnlyCollection<SecurityMD> securities) {
async void _show_windows(IImmutableSet<uint> securities) {
if (securities == null)
return;
var start_time = DateTime.Now;
do {
var found = securities.KeyJoin(_securities).ToList();
securities = securities.RemoveRange(found.Select(SecurityMD.key_fn));
show_windows(found);
if (securities.Count == 0)
break;
await Task.Delay(500);
} while ((DateTime.Now - start_time) < _ladder_conf.security_timeout);
}
void show_windows(IEnumerable<SecurityMD> securities) {
int n_allowed = (int)_ladder_conf.max_open_ladders - _wm.list_windows().Where(w => w.window_type == _window_type).Count();
int n_opened = 0;
foreach (var s in securities) {
@@ -12,7 +12,6 @@ using mbts.ui.spark.algo;
using mbts.ui.utility.text;
using mbts.utils;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
@@ -256,9 +255,8 @@ namespace mbts.spark {
DependencyProperty.Register("Orders", typeof(ObservableCollection<LadderOrderVM>), typeof(LadderGridHost), new PropertyMetadata(null, orders_changed));
static void orders_changed(DependencyObject d, DependencyPropertyChangedEventArgs e) {
LadderGridHost lgh = d as LadderGridHost;
WPFHelper.manage_collection_change(e, lgh.orders_collection_changed, src => lgh._ordershost.clear_orders(),
src => lgh.orders_collection_changed(lgh, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, (IList)src)));
if (d is LadderGridHost lgh)
WPFHelper.manage_collection_change(e, lgh.orders_collection_changed);
}
void orders_collection_changed(object sender, NotifyCollectionChangedEventArgs e) {
@@ -1,9 +1,13 @@
using bts;
using bts.collections;
using bts.utils;
using mbts.ui;
using mbts.ui.converters;
using mbts.ui.extensions;
using mbts.utils;
using System;
using System.Linq;
using System.Reactive.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
@@ -80,6 +84,7 @@ namespace mbts.spark {
_ladder.visible_columns_changed += () => _column_headers_presenter.Columns.SetItems(
_ladder.visible_column_types.Select(t => ColumnFactory.create(_ladder, t)));
this.vm = vm;
this.ConnectDataContext<LadderVM>(lvm => lvm.bus.subscribe(SparkTags.SecuritiesTag).OnDispatcher().Subscribe(sec_map => Resources[ResourceKeys.Securities] = sec_map));
DataContext = vm;
DataContextChanged += datacontext_changed;
_ui_scaler.MouseDoubleClick += _ui_scaler_MouseDoubleClick;
@@ -62,15 +62,15 @@
Header="{Binding security}"
IsEditMode="False"
IsLazy="True"
SecurityMap="{DynamicResource ResourceKey={x:Static usp:ManualFillRowEditor.SecuritiesKey}}"
SecurityMap="{DynamicResource ResourceKey={x:Static u:ResourceKeys.Securities}}"
SelectedSecurity="{Binding security, UpdateSourceTrigger=Explicit}"
ShowDialogSkirt="True"
ToolTip="{Binding SelectedCriteria, RelativeSource={RelativeSource Self}, Converter={x:Static c:ContractPickerPopup.SecurityCriteriaTextConverter}, Mode=OneWay}"
OKClick="{x:Static c:ContractPickerPopup.FlushSelectedSecurityHandler}"
/>
<ComboBox Grid.Row="2" Grid.Column="2" ItemsSource="{DynamicResource ResourceKey={x:Static usp:ManualFillRowEditor.AccountsKey}}" SelectedItem="{Binding account}"/>
<ComboBox Grid.Row="2" Grid.Column="4" ItemsSource="{DynamicResource ResourceKey={x:Static usp:ManualFillRowEditor.UsersKey}}" SelectedItem="{Binding user}"/>
<ComboBox Grid.Row="2" Grid.Column="6" ItemsSource="{DynamicResource ResourceKey={x:Static usp:ManualFillRowEditor.SidesKey}}" SelectedItem="{Binding side}" Width="48"/>
<ComboBox Grid.Row="2" Grid.Column="2" ItemsSource="{DynamicResource ResourceKey={x:Static u:ResourceKeys.Accounts}}" SelectedItem="{Binding account}"/>
<ComboBox Grid.Row="2" Grid.Column="4" ItemsSource="{DynamicResource ResourceKey={x:Static u:ResourceKeys.Users}}" SelectedItem="{Binding user}"/>
<ComboBox Grid.Row="2" Grid.Column="6" ItemsSource="{DynamicResource ResourceKey={x:Static u:ResourceKeys.Sides}}" SelectedItem="{Binding side}" Width="48"/>
<c:TickUpDownUInt Grid.Row="2" Grid.Column="8" MinWidth="40" Value="{Binding size}"/>
<usp:PriceEntryControl Grid.Row="2" Grid.Column="10" MinWidth="55" Value="{Binding price}" ContractSpec="{Binding spec}"/>
</Grid>
@@ -6,12 +6,6 @@ namespace mbts.ui.spark {
/// Interaction logic for ManualFillRowEditor.xaml
/// </summary>
partial class ManualFillRowEditor : UserControl {
public const string
SecuritiesKey = nameof(SecuritiesKey),
AccountsKey = nameof(AccountsKey),
UsersKey = nameof(UsersKey),
SidesKey = nameof(SidesKey);
public static readonly RoutedCommand
CommitCommand = new RoutedCommand(),
CancelCommand = new RoutedCommand();
@@ -70,7 +70,7 @@ namespace bts.shell.manual_fills {
mfd => new ManualFillRow(mfd),
mfr => mfr.data),
_bus.subscribe(SparkTags.SecuritiesTag).OnDispatcher().Subscribe(m => {
security_map = m;
security_map = new FilteredDictionary<uint, SecurityMD>(m, smd => smd.security.ContractCase == Security.ContractOneofCase.Future);
RaisePropertyChanged(nameof(security_map));
}),
ObservableAutoWrapper.connect_item_hooks(_persisted_rows, r => Disposable.Create(dirty_mon.join(r), selection_mon.join(r))),
@@ -50,11 +50,11 @@ namespace mbts.ui.spark {
row_editor.CommandBindings.Add(new CommandBinding(ManualFillRowEditor.CommitCommand, commit_executed));
row_editor.CommandBindings.Add(new CommandBinding(ManualFillRowEditor.CancelCommand, cancel_executed));
// dynamic resources
row_editor.Resources[ManualFillRowEditor.AccountsKey] = vm.accounts;
row_editor.Resources[ManualFillRowEditor.UsersKey] = vm.users;
row_editor.Resources[ManualFillRowEditor.SidesKey] = vm.sides;
row_editor.Resources[ResourceKeys.Accounts] = vm.accounts;
row_editor.Resources[ResourceKeys.Users] = vm.users;
row_editor.Resources[ResourceKeys.Sides] = vm.sides;
FocusManager.SetIsFocusScope(row_editor, true);
var sec_hook = vm.as_demuxer().subscribe(nameof(ManualFillsVM.security_map)).OnDispatcher().Subscribe(v => row_editor.Resources[ManualFillRowEditor.SecuritiesKey] = v);
var sec_hook = vm.as_demuxer().subscribe(nameof(ManualFillsVM.security_map)).OnDispatcher().Subscribe(v => row_editor.Resources[ResourceKeys.Securities] = v);
// popup adorner
var popup_adorner = new PopupAdorner(
data_grid, data_grid,
@@ -22,7 +22,7 @@ namespace mbts.spark {
DataContext = vm;
}
class MiniOrderBookRowView : DIYGrid.HighPerformanceRow<OrderBookRowVM, OrderBookGridView.ColumnKeys> {
class MiniOrderBookRowView : DIYGrid.HighPerformanceRow<OrderBookRowVM> {
public MiniOrderBookRowView() : base() {
FocusManager.SetIsFocusScope(this, true);
KeyboardNavigation.SetTabNavigation(this, KeyboardNavigationMode.Cycle);
@@ -121,7 +121,7 @@ namespace mbts.spark {
r => r.user,
DefaultColumnWidths.User,
DiscreteValuesFilterVM.Create<OrderSecurityRootPair, string>(
model.factory.subscribe_users().Select(d => d.Values),
model.factory.subscribe_users().Select(d => d.Values.Ordered()),
ort => ort.order.order.user(),
nameof(OrderBookRowVM.user)),
DataGridFilterKnownTemplates.DiscreteValues,
@@ -133,7 +133,7 @@ namespace mbts.spark {
r => r.account,
DefaultColumnWidths.Account,
DiscreteValuesFilterVM.Create<OrderSecurityRootPair, string, string[]>(
model.factory.subscribe_accounts().Select(d => d.Values),
model.factory.subscribe_accounts().Select(d => d.Values.Ordered()),
ort => ort.order.order.account(),
vals => vals.ToArray(),
vals => Observable.Return(vals),
@@ -650,12 +650,13 @@ namespace mbts.spark {
ListSortDirection? _cached_sort_direction;
public OrderBookLastMessageColumn(IDataGridColumnPersistentFilter<OrderSecurityRootPair, string> filter)
: base(Columns.LastMessage, new Binding(nameof(OrderBookRowVM.last_msg)), filter, DataGridFilterKnownTemplates.Regex) {
: base(Columns.LastMessage, filter, DataGridFilterKnownTemplates.Regex) {
id = key.ToString();
this.filter = filter;
Header = "Last Message";
Width = DefaultColumnWidths.LastMessage;
tooltip_binding = new Binding(nameof(OrderBookRowVM.last_msg));
TextBinding = new Binding(nameof(OrderBookRowVM.last_msg));
ToolTipBinding = new Binding(nameof(OrderBookRowVM.last_msg));
}
public new IDataGridColumnPersistentFilter<OrderSecurityRootPair, string> filter { get; }
@@ -239,7 +239,7 @@ namespace mbts.spark {
grid_settings_manager,
order_mod_vm.algo_vm.connect_data(Observable.Return(model)),
order_mod_vm.algo_vm.connect_data(_selected_params_mon.subscribe_spreader_security()),
order_mod_vm.algo_vm,
order_mod_vm,
attach_vm,
_show_fills.observe_current(true)
.Subscribe(is_visible => {
@@ -735,17 +735,13 @@ namespace mbts.spark {
_size_permissions = AlgoParamPermissions.ReadOnly | AlgoParamPermissions.Write;
SecurityMD? _security_md;
public OrderParamsVM(OrderBookVM owner, AlgoFormats init_format, AlgoEditingPermissions init_permissions)
: this(owner,
AlgoDefinitionVM.create(
owner._model,
AlgoData.create_unpersisted(OrderDataHelper.Defaults.CustomOrderDefinitionImmutable(CustomOrderDefinition.CustomOneofCase.Iceberg), CustomOrderDefinition.CustomOneofCase.Iceberg.display_name()),
init_format: init_format,
editing_permissions: init_permissions)) { }
OrderParamsVM(OrderBookVM owner, AlgoDefinitionVM algo_vm) {
public OrderParamsVM(OrderBookVM owner, AlgoFormats init_format, AlgoEditingPermissions init_permissions) {
_owner = owner;
this.algo_vm = algo_vm;
algo_vm = AlgoDefinitionVM.create(
owner._model,
AlgoData.create_unpersisted(OrderDataHelper.Defaults.CustomOrderDefinitionImmutable(CustomOrderDefinition.CustomOneofCase.Iceberg), CustomOrderDefinition.CustomOneofCase.Iceberg.display_name()),
init_format: init_format,
editing_permissions: init_permissions);
_accounts = backing(
nameof(selected_account),
_model.factory.subscribe_account_tuples(),
@@ -784,7 +780,8 @@ namespace mbts.spark {
appended_param_rows.insert_front(new AlgoParamRowVM(root, _price_param, null, AlgoParamTemplateKeys.Price));
}
}),
ObservableAutoWrapper.connect_item_hooks(param_rows, dirty_params_mon.join));
ObservableAutoWrapper.connect_item_hooks(param_rows, dirty_params_mon.join),
algo_vm);
// payup methods
void payup_bid(int ticks) => payup(bbo.bid, -ticks);
void payup_ask(int ticks) => payup(bbo.ask, ticks);
@@ -66,6 +66,10 @@ namespace mbts.spark {
_vm = new OrderBookVM(_bus, _notification_service, _model, _settings_manager);
_vm.load_from(_state);
_view = new OrderBookView(_vm);
_vm.SubscribeDisposed(
_bus.subscribe(SparkTags.SecuritiesTag)
.OnDispatcher()
.Subscribe(sec_map => _view.Resources[ResourceKeys.Securities] = sec_map));
var show_fills_command = SparkCommands.ShowFills(SparkCommands.KeyGestures.ShowFills).complete(() => new FillsSettings(_vm.selected_orders.Select(r => r.Key).ToArray()));
@@ -99,7 +99,7 @@ namespace mbts.spark {
}
}
class OrderBookRowView : DIYGrid.HighPerformanceRow<OrderBookRowVM, OrderBookGridView.ColumnKeys> {
class OrderBookRowView : DIYGrid.HighPerformanceRow<OrderBookRowVM> {
public OrderBookRowView() : base() { }
}
}
@@ -501,6 +501,12 @@ namespace bts.shell.pnl {
account_name == other.account_name &&
sod == other.sod &&
day == other.day &&
bid_price.Equals(other.bid_price) &&
ask_price.Equals(other.ask_price) &&
last_price.Equals(other.last_price) &&
mid_price.Equals(other.mid_price) &&
settlement_price.Equals(other.settlement_price) &&
calc_type.Equals(other.calc_type) &&
raw_day == other.raw_day &&
working.summary == other.working.summary;
@@ -97,22 +97,22 @@ namespace bts.shell.pnl {
if (security.HasValue && security.Value.ContractCase != Security.ContractOneofCase.Future) {
day_long = day_short = sod_long = sod_short = 0;
} else {
day_long = (decimal)DoubleUtil.nan_to_zero(data.day.long_pos);
day_short = (decimal)DoubleUtil.nan_to_zero(data.day.short_pos);
sod_long = (decimal)DoubleUtil.nan_to_zero(data.sod.long_pos);
sod_short = (decimal)DoubleUtil.nan_to_zero(data.sod.short_pos);
day_long = DoubleUtil.round_to_decimal(data.day.long_pos);
day_short = DoubleUtil.round_to_decimal(data.day.short_pos);
sod_long = DoubleUtil.round_to_decimal(data.sod.long_pos);
sod_short = DoubleUtil.round_to_decimal(data.sod.short_pos);
}
var nd = new PNLNormalizedData(
(decimal)DoubleUtil.nan_to_zero(day_pnl.value),
DoubleUtil.round_to_decimal(day_pnl.value),
day_long,
day_short,
(decimal)DoubleUtil.nan_to_zero(data.raw_day.long_pos),
(decimal)DoubleUtil.nan_to_zero(data.raw_day.short_pos),
(decimal)DoubleUtil.nan_to_zero(sod_pnl.value),
DoubleUtil.round_to_decimal(data.raw_day.long_pos),
DoubleUtil.round_to_decimal(data.raw_day.short_pos),
DoubleUtil.round_to_decimal(sod_pnl.value),
sod_long,
sod_short,
(decimal)DoubleUtil.nan_to_zero(data.working_long_pos),
(decimal)DoubleUtil.nan_to_zero(data.working_short_pos));
DoubleUtil.round_to_decimal(data.working_long_pos),
DoubleUtil.round_to_decimal(data.working_short_pos));
if (_normalized_data != nd) {
_normalized_data = nd;
updated?.Invoke(nd);
@@ -49,8 +49,9 @@ namespace bts.shell.pnl {
PNLHeaderRowDisplay _header_row_display = PNLHeaderRowDisplay.Total;
PNLRowCollectionView _rows_view;
IReadOnlyList<(ColumnKeys key, ListSortDirection direction)> _sorting_hierarchy;
Brush _selection_highlight;
Color _grid_background_color = Colors.Transparent;
Color
_grid_background_color = Colors.Transparent,
_selection_highlight;
bool _is_paused;
// default appearances
ImmutableDictionary<ColumnKeys, PNLCellAppearance>
@@ -103,7 +104,7 @@ namespace bts.shell.pnl {
load_from(settings);
_dispose = Disposable.Create(
pnl_source.Operate(
_calculation_type.observe_current(),
_calculation_type.observe_current(true),
(data, calc) => data.with(calc_type: calc)
).FilterWithRaw(filter_src, PNLRowData.key_fn, null)
.OnDispatcher()
@@ -175,7 +176,7 @@ namespace bts.shell.pnl {
}
}
public Brush selection_highlight { get => _selection_highlight; set => Set(ref _selection_highlight, value); }
public Color selection_highlight { get => _selection_highlight; set => Set(ref _selection_highlight, value); }
public Color grid_background_color { get => _grid_background_color; set => Set(ref _grid_background_color, value); }
public IValueConverter group_background_converter => _group_background_converter;
@@ -291,7 +292,7 @@ namespace bts.shell.pnl {
};
public void load_from(PNLSettings settings) {
selection_highlight = settings.grid.selection_highlight.to_brush();
selection_highlight = settings.grid.selection_highlight;
grid_background_color = settings.grid.background;
calculation_type = settings.calculation_type;
if (settings.filters != null) {
@@ -31,6 +31,8 @@ namespace bts.shell.pnl {
}
partial class PNLView : UserControl {
DIYGridPopupManager _popup_man_backing;
public enum Columns {
Account = 1,
// Security
@@ -240,6 +242,15 @@ namespace bts.shell.pnl {
}
#endregion
DIYGridPopupManager _popup_man {
get {
if (_popup_man_backing == null) {
_popup_man_backing = DIYGridPopupManager.create(this);
}
return _popup_man_backing;
}
}
void _cell_click(PNLCell cell) {
Log.Ui.info($"PNLCell Click: {cell}.");
try {
@@ -357,41 +368,20 @@ namespace bts.shell.pnl {
}
}
async void _show_auxiliary_popup<T>(T content, string context_desc, string title_desc, Color fg) {
// vm
dynamic header = new ExpandoObject();
header.context = context_desc;
header.title = title_desc;
header.title_foreground = fg;
header.keep_open = false;
// view
var view = new PNLAuxiliaryPopupView {
void _show_auxiliary_popup<T>(T content, string context_desc, string title_desc, Color fg) {
var view = new HeaderedContentControl {
Content = content,
Header = header,
Header = new AuxiliaryDataHeader {
context = context_desc,
title = title_desc,
title_foreground = fg
},
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Top
VerticalAlignment = VerticalAlignment.Top,
Resources = new AuxiliaryDataResources()
};
await Task.Delay(100); // Wait for obvm to load rows collection before showing UI to avoid a flash
view.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
// popup adorner
// tho unlikely, better not assume target cell is non-null
var target_cell = _grid.ChildAtPoint<HyperlinkCell>(Mouse.GetPosition(_grid));
var popup_adorner = new PopupAdorner(_grid, target_cell ?? (FrameworkElement)_grid, view, HorizontalPosition.Middle, VerticalPosition.Bottom, 0, 0) {
OpenRelativeToMouse = target_cell == null,
EnableDragMove = true,
ClipToBounds = true
};
popup_adorner.SetBinding(PopupAdorner.StaysOpenProperty, new Binding("keep_open") { Source = header });
popup_adorner.PopupElement.InputBindings.Add(new KeyBinding(ApplicationCommands.Close, Key.Escape, ModifierKeys.None));
popup_adorner.PopupElement.CommandBindings.Add(new CommandBinding(ApplicationCommands.Close, (s, e) => {
Log.assert(popup_adorner != null);
popup_adorner.detach();
popup_adorner = null;
}));
popup_adorner.attach();
popup_adorner.IsOpen = true;
FocusManager.SetIsFocusScope(popup_adorner.PopupElement, true);
popup_adorner.AdornedElement.Focus();
view.Style = view.Resources[AuxiliaryDataResources.PopupChildStyleKey] as Style;
_popup_man.show(view);
}
class PNLGroupDescription : PropertyGroupDescription, IEquatable<PNLGroupDescription> {
+11 -6
View File
@@ -1,4 +1,5 @@
using mbts.ui.shell;
using mbts.ui;
using mbts.ui.shell;
using System;
using System.Collections.Generic;
using System.ComponentModel;
@@ -6,8 +7,9 @@ using System.Reactive.Linq;
using System.Runtime.CompilerServices;
namespace mbts.spark {
class AlgoParamVM<T> : IAlgoParamVM<T>, INotifyPropertyChanged {
class AlgoParamVM<T> : IAlgoParamVM<T>, INotifyPropertyChanged, ITemplateSelectorKey {
readonly IEqualityComparer<T> _comparer;
readonly string _template_key;
AlgoParamPermissions _permissions;
bool _is_enabled, _has_value;
T _committed_value, _value;
@@ -15,15 +17,16 @@ namespace mbts.spark {
public AlgoParamVM(string id, string display_name)
: this(id, display_name, default, EqualityComparer<T>.Default) { }
public AlgoParamVM(string id, string display_name, T value)
: this(id, display_name, value, EqualityComparer<T>.Default) { }
public AlgoParamVM(string id, string display_name, T value, string template_key = null)
: this(id, display_name, value, EqualityComparer<T>.Default, template_key) { }
public AlgoParamVM(string id, string display_name, T value, IEqualityComparer<T> comparer) {
public AlgoParamVM(string id, string display_name, T value, IEqualityComparer<T> comparer, string template_key = null) {
this.id = id;
this.display_name = display_name;
_value = value;
_committed_value = value;
_comparer = comparer;
_template_key = template_key;
}
public event PropertyChangedEventHandler PropertyChanged;
@@ -50,7 +53,9 @@ namespace mbts.spark {
public bool is_enabled { get => _is_enabled; set => _set(ref _is_enabled, value); }
public bool has_value { get => _has_value; set => _set(ref _has_value, value); }
public bool is_dirty { get; private set; }
object IAlgoParamVM.value { get => value; set => this.value = (T)value; }
object IAlgoParamVM.value { get => value; set => this.value = (T)value; }
string ITemplateSelectorKey.key => _template_key;
public void revert() {
if (is_dirty) {
@@ -67,7 +67,6 @@ namespace mbts.spark.utilities {
public IObservable<IReadOnlyDictionary<uint, string>> subscribe_accounts() => _admin_model.on_accounts;
public IObservable<IReadOnlyDictionary<uint, string>> subscribe_users() => _admin_model.on_users;
public IObservable<IReadOnlyList<UserTuple>> subscribe_user_tuples() => _admin_model.on_user_tuples;
public IObservable<IReadOnlyList<AccountTuple>> subscribe_account_tuples() => _admin_model.on_account_tuples;
#region Defaults
@@ -62,6 +62,7 @@ namespace mbts.spark {
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveInt: param.AsInt32 = 0; break;
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveOrderDef: param.AsOrderDef = new CustomOrderDefinition(); break;
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveString: param.AsString = string.Empty; break;
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveSecurityKey:
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveUint: param.AsUint32 = 0u; break;
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveQuantity: param.AsQuantity = new bts.spark.QuantityMeasure(); break;
}
@@ -12,7 +12,7 @@ using System.Reactive.Linq;
namespace mbts.spark {
interface IDynamicOrderParamVM {
DynamicOrderParamDescriptor descriptor { get; }
DynamicOrderParam param { get; set; }
DynamicOrderParam param { get; }
}
interface IDynamicOrderParamVM<T> : IDynamicOrderParamVM {
@@ -21,26 +21,16 @@ namespace mbts.spark {
}
abstract class DynamicOrderParamVM : DisposableViewModelBase, IDynamicOrderParamVM {
DynamicOrderParam _param;
protected DynamicOrderParamVM(DynamicOrderParamDescriptor descriptor)
: this(descriptor, DynamicOrderHelper.init_param(descriptor)) { }
private DynamicOrderParamVM(DynamicOrderParamDescriptor descriptor, DynamicOrderParam param) {
this.descriptor = descriptor;
_param = param;
this.param = param;
}
public DynamicOrderParamDescriptor descriptor { get; set; }
public DynamicOrderParam param {
get => _param;
set {
_param = value;
on_param_changed();
}
}
protected abstract void on_param_changed();
public DynamicOrderParam param { get; }
}
class DynamicOrderParamVM<TView, TModel> : DynamicOrderParamVM, IDynamicOrderParamVM<TView> {
@@ -53,14 +43,12 @@ namespace mbts.spark {
_field_desc = field_descriptor;
_convert = convert;
_convert_back = convert_back;
value = _convert(ConvertUtils.try_convert<TModel>(_field_desc.Accessor.GetValue(param)));
}
public TView value {
get => _value;
set {
_field_desc.Accessor.SetValue(param, _convert_back(_value = value));
RaisePropertyChanged();
}
set => Set(ref _value, value);
}
public IObservable<TView> subscribe_value() =>
@@ -70,11 +58,6 @@ namespace mbts.spark {
void on_prop_changed(object sender, PropertyChangedEventArgs e) =>
o.OnNext(value);
});
protected override void on_param_changed() {
_value = _convert(ConvertUtils.try_convert<TModel>(_field_desc.Accessor.GetValue(param)));
RaisePropertyChanged(nameof(value));
}
}
class DynamicOrderEnumeratedVM : DynamicOrderParamVM, IDynamicOrderParamVM<string> {
@@ -105,11 +88,6 @@ namespace mbts.spark {
o.OnNext(selected_element);
});
protected override void on_param_changed() {
_sync_elements();
RaisePropertyChanged(nameof(selected_element));
}
static IEnumerable<string> _get_elements(DynamicOrderParamDescriptor descriptor) =>
descriptor.ParamType?.DescriptorCase == DynamicOrderTypeDescriptor.DescriptorOneofCase.Enumerated ?
descriptor.ParamType.Enumerated.Element : Enumerable.Empty<string>();
@@ -148,10 +126,5 @@ namespace mbts.spark {
o.OnNext(quantity);
});
}
protected override void on_param_changed()
{
RaisePropertyChanged(nameof(quantity));
}
}
}
@@ -20,6 +20,7 @@ namespace mbts.ui.spark
public DataTemplate StringTemplate { get; set; }
public DataTemplate OrderDefTemplate { get; set; }
public DataTemplate QuantityMeasureTemplate { get; set; }
public DataTemplate SecurityKeyTemplate { get; set; }
public DataTemplate EnumTemplate { get; set; }
@@ -38,6 +39,7 @@ namespace mbts.ui.spark
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveDouble: return DoubleTemplate;
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveInt: return IntTemplate;
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveUint: return UIntTemplate;
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveSecurityKey: return SecurityKeyTemplate;
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveString: return StringTemplate;
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveOrderDef: return OrderDefTemplate;
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveQuantity: return QuantityMeasureTemplate;
@@ -4,6 +4,7 @@ using bts.collections;
using bts.spark;
using mbts.collections;
using mbts.spark.utilities;
using mbts.ui;
using mbts.ui.utility;
using mbts.utils;
using System;
@@ -73,9 +74,9 @@ namespace mbts.spark {
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveDouble: return mk<double, double>(DynamicOrderParam.AsDoubleFieldNumber, v => v, v => v, double.NaN);
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveInt: return mk<int?, int>(DynamicOrderParam.AsInt32FieldNumber, v => v, v => v.GetValueOrDefault());
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveUint: return mk<uint?, uint>(DynamicOrderParam.AsUint32FieldNumber, v => v, v => v.GetValueOrDefault());
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveSecurityKey: return mk<uint?, uint>(DynamicOrderParam.AsUint32FieldNumber, v => v, v => v.GetValueOrDefault(), template_key: TemplateKeys.SecurityKey);
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveString: return mk<string, string>(DynamicOrderParam.AsStringFieldNumber, v => v, v => v);
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveQuantity:
{
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveQuantity: {
var qm = new DynamicOrderQuantityVM(descriptor);
return attached(qm, DynamicOrderHelper.get_quantity_value, DynamicOrderHelper.set_quantity_value, new QuantityMeasure { Ratio = 1.0, Absolute = 1 });
}
@@ -88,16 +89,16 @@ namespace mbts.spark {
default: throw ExceptionBuilder.Argument.unexpected_case(descriptor.ParamType.DescriptorCase, nameof(descriptor));
}
DynamicOrderParamVM<TView, TModel> mk<TView, TModel>(int field_number, Func<TModel, TView> convert, Func<TView, TModel> convert_back, TView default_value = default) {
DynamicOrderParamVM<TView, TModel> mk<TView, TModel>(int field_number, Func<TModel, TView> convert, Func<TView, TModel> convert_back, TView default_value = default, string template_key = null) {
var fdesc = DynamicOrderParam.Descriptor.FindFieldByNumber(field_number);
var pvm = new DynamicOrderParamVM<TView, TModel>(descriptor, fdesc, convert, convert_back);
return attached<TView, DynamicOrderParamVM<TView, TModel>>(pvm, get_from_param, assign_to_param, default_value);
return attached<TView, DynamicOrderParamVM<TView, TModel>>(pvm, get_from_param, assign_to_param, default_value, template_key);
TView get_from_param(DynamicOrderParam param) => convert(ConvertUtils.try_convert<TModel>(fdesc.Accessor.GetValue(param)));
void assign_to_param(DynamicOrderParam param, TView value) => fdesc.Accessor.SetValue(param, convert_back(value));
}
TParam attached<T, TParam>(TParam pvm, Func<DynamicOrderParam, T> selector, Action<DynamicOrderParam, T> assign_to_param, T default_value = default) where TParam : DynamicOrderParamVM, IDynamicOrderParamVM<T> {
TParam attached<T, TParam>(TParam pvm, Func<DynamicOrderParam, T> selector, Action<DynamicOrderParam, T> assign_to_param, T default_value = default, string template_key = null) where TParam : DynamicOrderParamVM, IDynamicOrderParamVM<T> {
add_property<T>(
pvm.descriptor.ParamName,
mdef => mdef.OrderParams.TryGetValue(pvm.descriptor.ParamName, out DynamicOrderParam p) ? selector(p) : default_value,
@@ -105,7 +106,8 @@ namespace mbts.spark {
() => pvm.value,
v => pvm.value = v,
pvm.subscribe_value(),
default_value);
default_value,
template_key: template_key);
return pvm;
}
}
@@ -89,6 +89,7 @@ namespace mbts.spark {
case AlgoType.Dynamic: vm = dynamic(key, name, name, editing_permissions); break;
case AlgoType.Splitter: vm = splitter(key,name,editing_permissions); break;
case AlgoType.Composite: vm = composite(key, name, editing_permissions); break;
case AlgoType.None: vm = new DefaultAlgoVM(); break;
default: vm = generic(key, name, algo_type, editing_permissions); break;
//default:
// Log.Ui.debug_fatal($"Failed to create {typeof(IAlgoVM)}: unknown algo type '{algo_type}.'");
@@ -166,7 +167,8 @@ namespace mbts.spark {
};
void subscribe_new_algo(IAlgoVM old_algo, IAlgoVM new_algo) {
RaisePropertyChanged(sd.prop_name);
Disposable.replace(ref def_sub, new CompositeDisposable());
Disposable.dispose(ref def_sub);
def_sub = new CompositeDisposable();
if (assert(new_algo != null, sd.prop_name, "No algo specified.")) {
def_sub.Add(new_algo.subscribe_definition().Subscribe(_ => _def_change_notifier.execute()));
if (new_algo is INotifyDataErrorInfo ndei)
@@ -255,14 +257,14 @@ namespace mbts.spark {
_security_changed += o.OnNext;
return Disposable.Create(() => _security_changed -= o.OnNext);
});
Disposable.replace(ref _pending_load, Disposable.Create(
Disposable.dispose(ref _pending_load);
_pending_load = Disposable.Create(
// load children
ObservableAutoWrapper.connect_item_hooks(
_child_algo_selector_data.AsCollection(),
sd => security_src.SubscribeEach(_ => ObservableAutoWrapper.connect_item_actions(_child_algo_selector_data.AsCollection(), selector_load_impl))),
// load regular props
ObservableAutoWrapper.connect_item_actions(_auto_props.AsCollection(), p => p.load(algom))));
ObservableAutoWrapper.connect_item_actions(_auto_props.AsCollection(), p => p.load(algom)));
}
void selector_load_impl(AlgoSelectorData sd) {
@@ -298,11 +300,12 @@ namespace mbts.spark {
_security_changed += o.OnNext;
return Disposable.Create(() => _security_changed -= o.OnNext);
});
Disposable.replace(ref _pending_load, Disposable.Create(
Disposable.dispose(ref _pending_load);
_pending_load = Disposable.Create(
ObservableAutoWrapper.connect_item_hooks(
_child_algo_selector_data.AsCollection(),
sd => security_src.Subscribe(_ => ObservableAutoWrapper.connect_item_actions(_child_algo_selector_data.AsCollection(), selector_load_impl))),
ObservableAutoWrapper.connect_item_actions(_auto_props.AsCollection(), p => p.load_many(algos))));
ObservableAutoWrapper.connect_item_actions(_auto_props.AsCollection(), p => p.load_many(algos)));
void selector_load_impl(AlgoSelectorData sd) {
// **assume that algos has more than 1 element
@@ -542,8 +545,19 @@ namespace mbts.spark {
}
}
protected void add_property<TValue>(string prop_name, Func<TMutable, TValue> get_from_def, Action<TMutable, TValue> assign_to_def, Func<TValue> get_local, Action<TValue> set_local, IObservable<TValue> subscribe_local, TValue non_uniform_value = default, AlgoParamPermissions permissions = AlgoParamPermissions.ReadWrite) {
var param = new AlgoParamVM<TValue>(prop_name, DisplayNameHelper.snake_to_title_case(prop_name), get_local()) { permissions = permissions };
protected void add_property<TValue>(
string prop_name,
Func<TMutable, TValue> get_from_def,
Action<TMutable, TValue> assign_to_def,
Func<TValue> get_local,
Action<TValue> set_local,
IObservable<TValue> subscribe_local,
TValue non_uniform_value = default,
AlgoParamPermissions permissions = AlgoParamPermissions.ReadWrite,
string template_key = null) {
var param = new AlgoParamVM<TValue>(prop_name, DisplayNameHelper.snake_to_title_case(prop_name), get_local(), template_key) {
permissions = permissions
};
_algo_params.Add(param);
_auto_props[prop_name] = new AutoProp(prop_name,
def => {
@@ -816,12 +830,11 @@ namespace mbts.spark {
var default_algo = AlgoKey.from_known_type(OrderDataHelper.get_default_algo(OrderDataHelper.get_algo_format(security.Value.ContractCase)));
foreach (var sd in _child_algo_selectors)
sd.load_algo_data_recursive(algo_data, default_algo);
Disposable.replace(
ref _selector_data_hooks,
ObservableAutoWrapper.connect_item_hooks(child_algo_selectors, s => {
s.load_algo_data_recursive(algo_data, default_algo);
return Disposable.empty;
}));
Disposable.dispose(ref _selector_data_hooks);
_selector_data_hooks = ObservableAutoWrapper.connect_item_hooks(child_algo_selectors, s => {
s.load_algo_data_recursive(algo_data, default_algo);
return Disposable.empty;
});
}
}
+22 -15
View File
@@ -59,23 +59,30 @@ namespace mbts.spark {
this.md_auxiliary_model = md_auxiliary_model;
var scheduler = ThreadPoolScheduler.Instance;
var interval = TimeSpan.FromMilliseconds(100);
on_orders =
_service
.subscribei_orders()
.Naggle(interval, scheduler)
.PermissionFilter(admin_model.on_admin_data)
.join(sstore, order => order.security_key())
on_orders = admin_model
.permission_filter(
_service.subscribei_orders()
.Naggle(interval, scheduler),
o => o.account_id()
).join(sstore, order => order.security_key())
.Table()
.Cache();
on_positions = _position_service.subscribei_position_data().Naggle(interval, scheduler).PermissionFilter(admin_model.on_admin_data).join(sstore, pos => pos.SecurityId).Table().Cache();
on_pnl = _position_service.subscribei_pnl_data()
.Naggle(interval, scheduler)
on_positions = admin_model
.permission_filter(
_position_service.subscribei_position_data().Naggle(interval, scheduler),
p => p.AccountId)
.join(sstore, pos => pos.SecurityId)
.Table()
.Cache();
on_pnl = admin_model
.permission_filter(
_position_service.subscribei_pnl_data().Naggle(interval, scheduler),
p => p.Pos.AccountId)
.join(sstore, p => p.Pos.SecurityId)
.TableCheckedAutoCorrect(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(
@@ -174,10 +181,11 @@ namespace mbts.spark {
.Where(t => t.has_changes())
.Cache();
// fill tuples
var on_fills =
_service.subscribei_fills()
.Naggle(interval, scheduler)
.PermissionFilter(admin_model.on_admin_data)
var on_fills = admin_model
.permission_filter(
_service.subscribei_fills()
.Naggle(interval, scheduler),
f => f.Account)
.join(sstore, f => f.SecurityKey)
.Table().Cache();
var fill_order_tuples_join = on_fills.LeftJoin(
@@ -265,7 +273,6 @@ namespace mbts.spark {
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
@@ -41,6 +41,7 @@
<ResourceDictionary Source="/view/Controls/DataGridEx/Filters/Views/DoubleMinMaxFilterControl.xaml"/>
<ResourceDictionary Source="/view/Controls/DataGridEx/Filters/Views/DiscreteValuesFilterControl.xaml"/>
<ResourceDictionary Source="/view/Controls/DataGridEx/Filters/Views/TimeFilterControl.xaml"/>
<ResourceDictionary Source="/view/Controls/ActionMenu/ActionMenu.xaml"/>
<ResourceDictionary Source="/Modules/Spreader/ExecutionStrategyEditor.xaml"/>
<ResourceDictionary Source="/view/Controls/Order/AlgoContentTemplateSelector.xaml"/>
<ResourceDictionary Source="/view/Controls/Order/AlgoPickerNodeView.xaml"/>
@@ -8,6 +8,7 @@ using mbts.spark;
using mbts.ui;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Windows.Input;
namespace bts.spark {
@@ -41,6 +42,9 @@ namespace bts.spark {
SparkCommand.create("P&L (2.0)", Icons.Accounting, SparkTags.ShowPNLTag);
// Ladder
public static PartialCommand<IImmutableSet<uint>> ShowLadderAutoLookup(KeyGesture key_gesture = null) =>
SparkCommand.create("Ladder", Icons.Ladder, SparkTags.ShowLadderAutoLookupTag, key_gesture);
public static PartialCommand<SecurityMD> ShowLadder(KeyGesture key_gesture = null) =>
SparkCommand.create("Ladder", Icons.Ladder, SparkTags.ShowLadderTag, key_gesture);
+3
View File
@@ -7,6 +7,7 @@ using bts.spark;
using mbts.ui;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Threading.Tasks;
namespace mbts.spark {
@@ -55,6 +56,8 @@ namespace mbts.spark {
// The owners of the various SparkWindows may remove dependencies on the former at their leasure.
// For now, both tags go through the same logic in the LadderWindowFactory anyways.
public static ITag<SecurityMD> ShowLadderTag { get; } = new SparkTag<SecurityMD>("Show Ladder");
// TODO: get rid of the SecurityMD ShowLadderTag. This has created burdensome dependency issues.
public static ITag<IImmutableSet<uint>> ShowLadderAutoLookupTag { get; } = new SparkTag<IImmutableSet<uint>>("Show Ladder Auto Lookup");
public static ITag<IReadOnlyList<SecurityMD>> ShowLaddersTag { get; } = new SparkTag<IReadOnlyList<SecurityMD>>(nameof(ShowLaddersTag));
public static ITag<IObservable<SecurityMD>> ShowLinkedLadder { get; } = new SparkTag<IObservable<SecurityMD>>("Linked Ladder");
public static ITag<SecurityCriteria> ShowMarketViewTag { get; } = new SparkTag<SecurityCriteria>("Show MarketView");
@@ -70,5 +70,21 @@ namespace mbts.ui {
}, () => Disposable.dispose(ref can_execute_sub));
return cmd;
}
public static ICommand create<T, TResult>(Action<TResult> execute, Func<T, bool> get_can_execute, Func<T, TResult> convert, out IObserver<T> observer) {
TResult param = default;
bool can_execute = false;
var cmd = new DelegateCommand(() => execute(param), () => can_execute);
IDisposable can_execute_sub = null;
observer = new AnonymousObserver<T>(val => {
var could_execute = can_execute;
can_execute = get_can_execute(val);
if (can_execute)
param = convert(val);
if (can_execute != could_execute)
cmd.RaiseCanExecuteChanged();
}, () => Disposable.dispose(ref can_execute_sub));
return cmd;
}
}
}
@@ -5,6 +5,7 @@ namespace mbts {
public static bool IsNaNOrInfinity(double d) => double.IsNaN(d) || double.IsInfinity(d);
public static bool IsNaNOrInfinityOr(double d, params double[] values) => IsNaNOrInfinity(d) || values.Contains(d);
public static bool IsOneOf(double d, params double[] values) => values.Contains(d);
public static bool IsRealNonZero(this double d) => d != 0 && !double.IsNaN(d) && !double.IsInfinity(d);
public static double CoerceNaN(double value) => CoerceNaN(value, 0.0);
public static double CoerceNaN(double value, double if_nan) => double.IsNaN(value) ? if_nan : value;
public static double CoerceNaNOrInfinity(double value) => CoerceNaNOrInfinity(value, 0.0);
@@ -131,6 +131,11 @@ namespace mbts.ui.native
[DllImport("user32.dll")]
public static extern bool GetCursorPos(out Win32Point pt);
public static readonly IntPtr HWND_TOPMOST = new IntPtr(-1);
public static readonly IntPtr HWND_NOTOPMOST = new IntPtr(-2);
public static readonly IntPtr HWND_TOP = new IntPtr(0);
public static readonly IntPtr HWND_BOTTOM = new IntPtr(1);
[DllImport("user32.dll")]
public static extern bool SetWindowPos(IntPtr hwnd, IntPtr hwnd_insert_after, int x, int y, int cx, int cy, SWP flags);
@@ -255,7 +260,20 @@ namespace mbts.ui.native
{
SWP_NOSIZE = 1,
SWP_NOMOVE = 2,
SWP_FRAMECHANGED = 0x20
SWP_NOZORDER = 0x0004,
SWP_NOREDRAW = 0x0008,
SWP_NOACTIVATE = 0x0010,
// The frame changed: send WM_NCCALCSIZE
SWP_FRAMECHANGED = 0x0020,
SWP_SHOWWINDOW = 0x0040,
SWP_HIDEWINDOW = 0x0080,
SWP_NOCOPYBITS = 0x0100,
// Don’t do owner Z ordering
SWP_NOOWNERZORDER = 0x0200,
// Don’t send WM_WINDOWPOSCHANGING
SWP_NOSENDCHANGING = 0x0400,
TOPMOST_FLAGS = SWP_NOACTIVATE | SWP_NOOWNERZORDER | SWP_NOSIZE | SWP_NOMOVE | SWP_NOREDRAW | SWP_NOSENDCHANGING
}
public enum SPI : uint
@@ -0,0 +1,5 @@
using System;
namespace mbts.ui.native {
}
@@ -138,6 +138,7 @@ namespace mbts.ui.native
NCMBUTTONDOWN = 0x00A7,
NCMBUTTONUP = 0x00A8,
NCMOUSEMOVE = 0x00A0,
NCMOUSELEAVE = 0x02A2,
NCPAINT = 0x0085,
NCRBUTTONDBLCLK = 0x00A6,
NCRBUTTONDOWN = 0x00A4,
@@ -11,6 +11,7 @@ using bts.api.persistence;
using System.Text.RegularExpressions;
using System.Reactive.Linq;
using bts.spark;
using bts;
namespace mbts.persist
{
@@ -146,16 +147,18 @@ namespace mbts.persist
}
[Export(typeof(IUserPersist))]
public class UserUISettings : UISettingsBase
{
public class UserUISettings : UISettingsBase {
string _user;
[ImportingConstructor]
public UserUISettings([Import("userid")]uint user, [Import("settings")]PersistenceService persist, ErrorReporter err)
public UserUISettings(uint user_id, [Import("settings")]PersistenceService persist, ErrorReporter err)
: base(persist,err) {
_user = $"User {user}";
_user = $"User {user_id}";
}
[ImportingConstructor]
UserUISettings(UserTuple user, [Import("settings")]PersistenceService persist, ErrorReporter err)
: this(user.id, persist, err) { }
protected override string user() { return _user; }
}
}
@@ -15,8 +15,10 @@ namespace bts.settings {
// determine the various windows to which such rules apply.
interface IFillsGridSettingsMigrationRule : ISettingsMigrationRule<GridSettings> { }
interface IMarketViewGridSettingsMigrationRule : ISettingsMigrationRule<GridSettings> { }
interface ITimeAndSalesGridSettingsMigrationRule : ISettingsMigrationRule<GridSettings> { }
interface IPAndLGridSettingsMigrationRule : ISettingsMigrationRule<GridSettings> { }
interface IPNLSettingsMigrationRule : ISettingsMigrationRule<PNLSettings> { }
interface ISyntheticManagementMigrationRule : ISettingsMigrationRule<GridSettings> { }
static class SettingsMigrationHelper {
public static bool apply<T>(T settings, IEnumerable<ISettingsMigrationRule<T>> migration_rules, out T new_settings) {
@@ -25,7 +25,9 @@ namespace mbts.persist {
static readonly IImmutableProperty<UserSettings, GridSettings>
_fills_propi = ImmutableProperty.create<UserSettings, GridSettings>(us => us.fills_grid, (us, gs) => us.with(provided_fills_gridsettings: gs)),
_mview_propi = ImmutableProperty.create<UserSettings, GridSettings>(us => us.market_view_grid, (us, gs) => us.with(provided_market_view_gridsettings: gs)),
_pandl_propi = ImmutableProperty.create<UserSettings, GridSettings>(us => us.pnl_grid, (us, gs) => us.with(provided_pnl_gridsettings: gs));
_pandl_propi = ImmutableProperty.create<UserSettings, GridSettings>(us => us.pnl_grid, (us, gs) => us.with(provided_pnl_gridsettings: gs)),
_synth_propi = ImmutableProperty.create<UserSettings, GridSettings>(us => us.synthetic_management_grid, (us, gs) => us.with(provided_synthetic_management_gridsettings: gs)),
_time_and_sales_propi = ImmutableProperty.create<UserSettings, GridSettings>(us => us.time_and_sales_grid, (us, gs) => us.with(time_and_sales: gs));
static readonly IImmutableProperty<UserSettings, PNLSettings>
_pnl_propi = ImmutableProperty.create<UserSettings, PNLSettings>(us => us.pnl, (us, pnl) => us.with(pnl: pnl));
@@ -36,6 +38,8 @@ namespace mbts.persist {
readonly IEnumerable<IMarketViewGridSettingsMigrationRule> _mview_migration_rules;
readonly IEnumerable<IPAndLGridSettingsMigrationRule> _pandl_migration_rules;
readonly IEnumerable<IPNLSettingsMigrationRule> _pnl_migration_rules;
readonly IEnumerable<ISyntheticManagementMigrationRule> _synth_migration_rules;
readonly IEnumerable<ITimeAndSalesGridSettingsMigrationRule> _time_and_sales_migration_rules;
readonly IUserPersist _persist;
UserSettings _default_settings;
@@ -46,13 +50,17 @@ namespace mbts.persist {
[ImportMany]IEnumerable<IFillsGridSettingsMigrationRule> fills_migration_rules,
[ImportMany]IEnumerable<IMarketViewGridSettingsMigrationRule> mview_migration_rules,
[ImportMany]IEnumerable<IPAndLGridSettingsMigrationRule> pandl_migration_rules,
[ImportMany]IEnumerable<IPNLSettingsMigrationRule> pnl_migration_rules) {
[ImportMany]IEnumerable<IPNLSettingsMigrationRule> pnl_migration_rules,
[ImportMany]IEnumerable<ISyntheticManagementMigrationRule> synth_migration_rules,
[ImportMany]IEnumerable<ITimeAndSalesGridSettingsMigrationRule> time_and_sales_migration_rules) {
//
_persist = persist;
_fills_migration_rules = fills_migration_rules ?? Enumerable.Empty<IFillsGridSettingsMigrationRule>();
_mview_migration_rules = mview_migration_rules ?? Enumerable.Empty<IMarketViewGridSettingsMigrationRule>();
_pandl_migration_rules = pandl_migration_rules ?? Enumerable.Empty<IPAndLGridSettingsMigrationRule>();
_pnl_migration_rules = pnl_migration_rules ?? Enumerable.Empty<IPNLSettingsMigrationRule>();
_synth_migration_rules = synth_migration_rules ?? Enumerable.Empty<ISyntheticManagementMigrationRule>();
_time_and_sales_migration_rules = time_and_sales_migration_rules ?? Enumerable.Empty<ITimeAndSalesGridSettingsMigrationRule>();
init();
async void init() {
UserSettings s;
@@ -62,7 +70,10 @@ namespace mbts.persist {
var wrapped_rules = new[] {
_fills_migration_rules.merge().wrap(_fills_propi),
_mview_migration_rules.merge().wrap(_mview_propi),
_pandl_migration_rules.merge().wrap(_pandl_propi)
_pandl_migration_rules.merge().wrap(_pandl_propi),
_pnl_migration_rules.merge().wrap(_pnl_propi),
_synth_migration_rules.merge().wrap(_synth_propi),
_time_and_sales_migration_rules.merge().wrap(_time_and_sales_propi)
};
SettingsMigrationHelper.apply_quiet(ref s, wrapped_rules);
// TODO: move ladder sanitization logic into ISettingsMigrationRule
@@ -136,6 +147,18 @@ namespace mbts.persist {
_pnl_migration_rules,
_pnl_propi);
public IWindowSettingsManager<GridSettings> create_synth_settings_manager(GridSettings instance_settings) =>
_create_settings_manager(
instance_settings,
_synth_migration_rules,
_synth_propi);
public IWindowSettingsManager<GridSettings> create_time_and_sales_settings_manager(GridSettings instance_settings) =>
_create_settings_manager(
instance_settings,
_time_and_sales_migration_rules,
_time_and_sales_propi);
public async Task save_default(UserSettings value) {
// notify observers before persisting so the changes are reflected immediately in the ui
lock (_default_settings_lock) {
@@ -15,12 +15,8 @@ namespace bts.utils {
}
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 IReadOnlyList<Func<TSource, TValue>> property_delegates_of_type<TSource, TValue>() =>
PropertyGetterStore<TSource, TValue>.value;
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));
@@ -162,6 +158,18 @@ namespace bts.utils {
}
}
static class PropertyGetterStore<TSource, TValue> {
public static readonly IReadOnlyList<Func<TSource, TValue>> value;
static PropertyGetterStore() {
var tval = typeof(TValue);
value = typeof(TSource).GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(p => tval.IsAssignableFrom(p.PropertyType))
.Select(_prop_getter<TSource, TValue>)
.ToReadOnlyList();
}
}
class PropNode {
public readonly PropertyInfo prop;
@@ -0,0 +1,46 @@
using bts.api;
using bts.spark;
using bts.utils;
using System;
namespace bts {
struct SyntheticTuple : IEquatable<SyntheticTuple>, IComparable<SyntheticTuple>, IComparable {
public static readonly Func<SyntheticTuple, uint> key_fn = a => a.security.Id;
public static readonly Func<SyntheticTuple?, uint> nullable_key_fn = a => a.HasValue ? a.Value.security.Id : 0u;
public readonly SecurityImmutable security;
public readonly string user_name;
public readonly ExecutionStrategyImmutable? strategy;
public readonly int? num_working_orders;
public SyntheticTuple(SecurityImmutable security, string user_name, ExecutionStrategyImmutable? strategy, int? num_working_orders = null) {
this.security = security;
this.user_name = user_name;
this.strategy = strategy;
this.num_working_orders = num_working_orders;
}
public int CompareTo(SyntheticTuple other) =>
SecurityHelper.Comparer.Compare(security, other.security);
int IComparable.CompareTo(object obj) =>
obj is SyntheticTuple other ? CompareTo(other) : 1;
public bool Equals(SyntheticTuple other) =>
security == other.security &&
user_name == other.user_name &&
EqualityComparer.nullable_equals(strategy, other.strategy);
public override bool Equals(object obj) =>
EqualityComparer.equals_obj(this, obj);
public override int GetHashCode() =>
security.Id.GetHashCode();
public override string ToString() =>
user_name == null ? security.ToString() : $"{user_name}:{security}";
public static bool operator ==(SyntheticTuple a, SyntheticTuple b) => a.Equals(b);
public static bool operator !=(SyntheticTuple a, SyntheticTuple b) => !a.Equals(b);
}
}
+15 -1
View File
@@ -5,9 +5,23 @@
xmlns:ui="clr-namespace:mbts.ui"
>
<!--Control Base Colors-->
<Color x:Key="Control.SuperLightGrayColor">#F6F6F6</Color>
<Color x:Key="Control.LightGrayColor">#E5E5E5</Color>
<Color x:Key="Control.LightMediumGrayColor">#CFCFCF</Color>
<Color x:Key="Control.MediumGrayColor">#B3B3B3</Color>
<Color x:Key="Control.MediumDarkGrayColor">#ACACAC</Color>
<Color x:Key="Control.AlphaLightGrayColor">#10000000</Color>
<Color x:Key="Control.AlphaLightMediumGrayColor">#24000000</Color>
<Color x:Key="Control.AlphaMediumGrayColor">#3A000000</Color>
<Color x:Key="Control.AlphaMediumGrayColor2">#FF0000</Color>
<Color x:Key="Control.DarkMediumGrayColor">#FF959595</Color>
<Color x:Key="Control.DarkGrayColor">#707070</Color>
<Color x:Key="Text.Prompt.Foreground.Color">#FF999999</Color>
<SolidColorBrush x:Key="Control.AlphaLightGrayBrush" Color="{StaticResource Control.AlphaLightGrayColor}"/>
<SolidColorBrush x:Key="Control.AlphaLightMediumGrayBrush" Color="{StaticResource Control.AlphaLightMediumGrayColor}"/>
<SolidColorBrush x:Key="Control.LightGrayBrush" Color="{StaticResource Control.LightGrayColor}"/>
<SolidColorBrush x:Key="Control.LightMediumGrayBrush" Color="{StaticResource Control.LightMediumGrayColor}"/>
<SolidColorBrush x:Key="Control.MediumGrayBrush" Color="{StaticResource Control.MediumGrayColor}"/>
<SolidColorBrush x:Key="CommonBackground" Color="{ui:BTSColor common_background}" />
<SolidColorBrush x:Key="ControlBackground" Color="{x:Static mbts:BTSColors.common_background}" />
@@ -130,4 +144,4 @@
<SolidColorBrush x:Key="ListBoxItem.Selected.Inactive.Border" Color="{StaticResource ListBoxItem.Selected.Inactive.Border.Color}"/>
<SolidColorBrush x:Key="ListBoxItem.MouseOver.Background" Color="{StaticResource ListBoxItem.MouseOver.Background.Color}"/>
<SolidColorBrush x:Key="ListBoxItem.MouseOver.Border" Color="{StaticResource ListBoxItem.MouseOver.Border.Color}"/>
</ResourceDictionary>
</ResourceDictionary>
@@ -1,78 +1,102 @@
<c:PopupControl
x:Class="mbts.ui.controls.ActionMenu"
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:u="clr-namespace:mbts.ui"
xmlns:c="clr-namespace:mbts.ui.controls"
Margin="1"
Background="Transparent"
BorderBrush="Transparent"
Padding="2"
PopupPlacement="Bottom"
ShowExpanderArrow="False"
Cursor="Hand"
xmlns:cv="clr-namespace:mbts.ui.converters"
>
<c:PopupControl.Resources>
<CollectionViewSource x:Key="ItemsSourceView" Source="{Binding ItemsSource, RelativeSource={RelativeSource AncestorType=c:ActionMenu}}">
<CollectionViewSource.GroupDescriptions>
<PropertyGroupDescription PropertyName="group"/>
</CollectionViewSource.GroupDescriptions>
</CollectionViewSource>
</c:PopupControl.Resources>
<c:PopupControl.Header>
<Border Width="16">
<SolidColorBrush x:Key="SeparatorFill" Color="{DynamicResource Control.AlphaMediumGrayColor}"/>
<ControlTemplate x:Key="ActionMenuTemplate" TargetType="c:ActionMenu">
<Border
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
VerticalAlignment="{TemplateBinding VerticalAlignment}"
Width="{TemplateBinding Width}"
Height="{TemplateBinding Height}"
>
<u:StateManager.Groups>
<u:StateGroupsManager AttachToMembers="True">
<u:StateDefinitionCollection>
<u:MouseStateDefinition MouseSource="{Binding Group.Target, RelativeSource={RelativeSource Self}}"/>
</u:StateDefinitionCollection>
</u:StateGroupsManager>
</u:StateManager.Groups>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup Name="MouseStates">
<VisualState Name="Normal"/>
<VisualState Name="MouseOver" Storyboard="{DynamicResource Button.MouseOver.Storyboard}"/>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Rectangle Grid.Row="0" Fill="Black" Height="2" HorizontalAlignment="Stretch"/>
<Rectangle Grid.Row="1" Fill="Black" Height="2" Margin="0,4,0,0" HorizontalAlignment="Stretch"/>
<Rectangle Grid.Row="2" Fill="Black" Height="2" Margin="0,4,0,0" HorizontalAlignment="Stretch"/>
<Button
x:Name="button"
Content="{TemplateBinding Header}"
ContentTemplate="{TemplateBinding HeaderTemplate}"
ContentTemplateSelector="{TemplateBinding HeaderTemplateSelector}"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Padding="2.5,2"
Style="{DynamicResource SparkToolBarButton}"
>
<Button.Background>
<SolidColorBrush/>
</Button.Background>
</Button>
<Popup c:PopupViewController.ButtonSource="{Binding ElementName=button}" Placement="Bottom" PlacementTarget="{Binding ElementName=button}" StaysOpen="False">
<Border Background="{DynamicResource Control.LightGrayBrush}" BorderThickness="1">
<Border.BorderBrush>
<SolidColorBrush Color="{DynamicResource Control.DarkMediumGrayColor}"/>
</Border.BorderBrush>
<ItemsPresenter/>
</Border>
</Popup>
</Grid>
</Border>
</c:PopupControl.Header>
<Menu ItemsSource="{Binding Source={StaticResource ItemsSourceView}}">
<Menu.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical"/>
</ItemsPanelTemplate>
</Menu.ItemsPanel>
<Menu.GroupStyle>
<GroupStyle>
<GroupStyle.ContainerStyle>
<Style TargetType="GroupItem">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="GroupItem">
<StackPanel>
<ItemsPresenter/>
<Rectangle HorizontalAlignment="Stretch" Height="1" Margin="22,0,0,0" Fill="LightGray"/>
</StackPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</GroupStyle.ContainerStyle>
</GroupStyle>
</Menu.GroupStyle>
<Menu.ItemContainerStyle>
<Style TargetType="MenuItem" BasedOn="{StaticResource {x:Type MenuItem}}">
<!--NOTE: CommandParameter binding must be declared BEFORE Command,
otherwise, CanExecuted is called before the parameter value is realized...I know, it sucks...-->
<Setter Property="CommandParameter" Value="{Binding parameter}"/>
<Setter Property="Command" Value="{Binding command}"/>
<Setter Property="Header" Value="{Binding name}"/>
<Setter Property="HeaderTemplate">
<Setter.Value>
<DataTemplate>
<TextBlock Text="{Binding}" VerticalAlignment="Center"/>
</DataTemplate>
</Setter.Value>
</Setter>
<Setter Property="Icon" Value="{Binding icon, Converter={StaticResource IconConverter}}"/>
<Setter Property="MinWidth" Value="167"/>
</Style>
</Menu.ItemContainerStyle>
</Menu>
</c:PopupControl>
</ControlTemplate>
<DataTemplate x:Key="HamburgerButtonTemplate">
<Image
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Source="{x:Static u:Icons.HamburgerMenu}"
/>
</DataTemplate>
<Style x:Key="ActionMenuBase" TargetType="c:ActionMenu">
<Setter Property="Background">
<Setter.Value>
<SolidColorBrush/>
</Setter.Value>
</Setter>
<Setter Property="BorderBrush">
<Setter.Value>
<SolidColorBrush/>
</Setter.Value>
</Setter>
<Setter Property="Template" Value="{StaticResource ActionMenuTemplate}"/>
</Style>
<DataTemplate DataType="{x:Type u:MenuItemVM}">
<TextBlock Text="{Binding}" VerticalAlignment="Center"/>
</DataTemplate>
<Style TargetType="c:ActionMenu" BasedOn="{StaticResource ActionMenuBase}">
<Setter Property="Panel.ZIndex" Value="1"/>
<Setter Property="HorizontalAlignment" Value="Right"/>
<Setter Property="VerticalAlignment" Value="Top"/>
<Setter Property="Margin" Value="1"/>
<Setter Property="HeaderTemplate" Value="{StaticResource HamburgerButtonTemplate}"/>
<Setter Property="Width" Value="22"/>
<Setter Property="ItemContainerStyle">
<Setter.Value>
<Style TargetType="MenuItem">
<Setter Property="MinWidth" Value="167"/>
<Setter Property="CommandParameter" Value="{Binding parameter}"/>
<Setter Property="Command" Value="{Binding command}"/>
<Setter Property="Header" Value="{Binding name}"/>
<Setter Property="Icon" Value="{Binding icon, Converter={x:Static cv:ImageSourceIconConverter.Instance}}"/>
</Style>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
@@ -1,22 +1,15 @@
using System.Collections;
using System.Linq;
using System.Windows;
using System.Windows;
using System.Windows.Controls;
namespace mbts.ui.controls {
/// <summary>
/// Interaction logic for ActionMenu.xaml
/// </summary>
partial class ActionMenu : PopupControl {
public ActionMenu() =>
InitializeComponent();
class ActionMenu : HeaderedItemsControl {
static ActionMenu() =>
DefaultStyleKeyProperty.OverrideMetadata(typeof(ActionMenu), new FrameworkPropertyMetadata(typeof(ActionMenu)));
public static readonly DependencyProperty ItemsSourceProperty =
WPFHelper.create_dp<IEnumerable, ActionMenu>(
nameof(ItemsSource), Enumerable.Empty<IMenuItemVM>());
public IEnumerable ItemsSource {
get => GetValue(ItemsSourceProperty) as IEnumerable;
set => SetValue(ItemsSourceProperty, value);
}
protected override DependencyObject GetContainerForItemOverride() =>
new MenuItem();
}
}
@@ -0,0 +1,59 @@
using System;
using System.Windows;
using System.Windows.Documents;
using System.Windows.Media;
namespace mbts.ui.controls.adorners {
class AdornerHost : Adorner {
UIElement _child;
public AdornerHost(UIElement adorned_element)
: base(adorned_element) { }
public static readonly DependencyProperty ChildProperty =
WPFHelper.create_dp<UIElement, AdornerHost>(
nameof(Child), (o, v) => {
if (o._child != null)
o.RemoveVisualChild(o._child);
o._child = v;
if (v != null)
o.AddVisualChild(v);
});
public UIElement Child {
get => _child;
set => SetValue(ChildProperty, value);
}
protected override int VisualChildrenCount => _child != null ? 1 : 0;
protected override Visual GetVisualChild(int index) =>
index == 0 && _child != null ? _child : throw new ArgumentOutOfRangeException();
protected override Size ArrangeOverride(Size finalSize) {
if (_child == null) {
return base.ArrangeOverride(finalSize);
}
_child.Arrange(new Rect(finalSize));
return _child.DesiredSize;
}
protected override Size MeasureOverride(Size constraint) {
if (_child == null) {
return base.MeasureOverride(constraint);
}
_child.Measure(constraint);
return _child.DesiredSize;
}
}
class AdornerHost<TChild> : AdornerHost where TChild : UIElement {
public AdornerHost(UIElement adorned_element)
: base(adorned_element) { }
public new TChild Child {
get => base.Child as TChild;
set => base.Child = value;
}
}
}
@@ -0,0 +1,126 @@
using mbts.ui.dpi;
using mbts.ui.extensions;
using System;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace mbts.ui.controls.adorners {
class DIYGridCellHoverAdorner : Adorner<ListBox>, IDisposable {
Brush _row_fill, _cell_fill;
Pen _row_stroke, _cell_stroke;
DIYGridRowsPresenter _rows_presenter;
ScrollViewer _scroll_viewer;
ScrollContentPresenter _scroll_content_backing;
DIYGrid _grid;
IDisposable _dispose;
DIYGrid.CellGeometry? _hover_cell;
AdornerHost _menu_host = null;
ActionMenu _menu => _menu_host.Child as ActionMenu;
#region RowFill
public static readonly DependencyProperty RowFillProperty =
WPFHelper.create_dp<Brush, DIYGridCellHoverAdorner>(
nameof(RowFill),
FrameworkPropertyMetadataOptions.AffectsRender,
(o, v) => o._row_fill = v,
FrozenBrushCache.get_brush(DIYGrid.DefaultSelectionHighlight));
public SolidColorBrush RowFill {
get => GetValue(RowFillProperty) as SolidColorBrush;
set => SetValue(RowFillProperty, value);
}
#endregion
#region RowStroke
public static readonly DependencyProperty RowStrokeProperty =
WPFHelper.create_dp<Pen, DIYGridCellHoverAdorner>(
nameof(RowStroke),
FrameworkPropertyMetadataOptions.AffectsRender,
(o, v) => o._row_stroke = v);
public Pen RowStroke {
get => GetValue(RowStrokeProperty) as Pen;
set => SetValue(RowStrokeProperty, value);
}
#endregion
#region CellFill
public static readonly DependencyProperty CellFillProperty =
WPFHelper.create_dp<Brush, DIYGridCellHoverAdorner>(
nameof(CellFill),
FrameworkPropertyMetadataOptions.AffectsRender,
(o, v) => o._cell_fill = v);
public SolidColorBrush CellFill {
get => GetValue(CellFillProperty) as SolidColorBrush;
set => SetValue(CellFillProperty, value);
}
#endregion
#region CellStroke
public static readonly DependencyProperty CellStrokeProperty =
WPFHelper.create_dp<Pen, DIYGridCellHoverAdorner>(
nameof(CellStroke),
FrameworkPropertyMetadataOptions.AffectsRender,
(o, v) => o._cell_stroke = v);
public Pen CellStroke {
get => GetValue(CellStrokeProperty) as Pen;
set => SetValue(CellStrokeProperty, value);
}
#endregion
public DIYGridCellHoverAdorner(DIYGridRowsPresenter adorned_element, ScrollViewer scroll_viewer)
: base(adorned_element) {
_row_fill = RowFill;
_row_stroke = RowStroke;
_cell_fill = CellFill;
_cell_stroke = CellStroke;
_scroll_viewer = scroll_viewer;
IsHitTestVisible = false;
_rows_presenter = adorned_element;
_grid = _rows_presenter.find_ancestor_of_type<DIYGrid>();
_dispose = Disposable.Create(
_grid.SubscribeMouseHoverCell().Subscribe(cell => {
_hover_cell = cell;
if (_hover_cell.HasValue) {
var (row, col) = _hover_cell.Value;
var row_tl = row.TranslatePoint(default, this);
}
InvalidateVisual();
}));
}
ScrollContentPresenter _scroll_content =>
_scroll_content_backing ?? (_scroll_content_backing = _scroll_viewer.descendants_of_type<ScrollContentPresenter>().FirstOrDefault());
protected override void OnRender(DrawingContext drawingContext) {
base.OnRender(drawingContext);
if (_hover_cell.HasValue) {
(var row, var cell) = _hover_cell.Value;
if (row != null) {
var row_tl = new Lazy<Point>(() => row.TranslatePoint(default, this));
if (_row_fill != null || _row_stroke != null) {
var row_rect = new Rect(row_tl.Value, row.RenderSize);
if (_scroll_content != null) {
var content_bounds = this.GetChildBounds(_scroll_content);
row_rect.Intersect(content_bounds);
}
drawingContext.DrawRectangle(_row_fill, _row_stroke, row_rect, true);
}
if (_cell_fill != null || _cell_stroke != null) {
var cell_rect = cell.to_visible_rect(row.ActualHeight, row_tl.Value.Y);
drawingContext.DrawRectangle(_cell_fill, _cell_stroke, cell_rect, true);
}
}
}
}
public void Dispose() =>
Disposable.dispose(ref _dispose);
}
}
@@ -24,7 +24,7 @@ namespace mbts.ui.controls.adorners {
nameof(Fill),
FrameworkPropertyMetadataOptions.AffectsRender,
(o, v) => o._fill = v,
DIYGrid.DefaultSelectionHighlight);
FrozenBrushCache.get_brush(DIYGrid.DefaultSelectionHighlight));
public SolidColorBrush Fill {
get => GetValue(FillProperty) as SolidColorBrush;
@@ -38,7 +38,7 @@ namespace mbts.ui.controls.adorners {
nameof(Stroke),
FrameworkPropertyMetadataOptions.AffectsRender,
(o, v) => o._stroke = v,
DIYGrid.DefaultSelectionStroke);
FrozenPenCache.get_pen(DIYGrid.DefaultSelectionHighlight));
public Pen Stroke {
get => GetValue(StrokeProperty) as Pen;
@@ -8,8 +8,8 @@ using System.Windows.Media;
namespace mbts.ui.controls.adorners {
public class DragRectAdorner : Adorner, IDisposable {
Brush _fill = DIYGrid.DefaultSelectionHighlight;
Pen _stroke = DIYGrid.DefaultSelectionStroke;
Brush _fill;
Pen _stroke;
readonly ScrollViewerTuple _scroll_viewer_tup;
readonly double _drift_multiplier;
Point? _drag_start;
@@ -20,7 +20,7 @@ namespace mbts.ui.controls.adorners {
nameof(Fill),
FrameworkPropertyMetadataOptions.AffectsRender,
(o, v) => o._fill = v,
DIYGrid.DefaultSelectionHighlight);
FrozenBrushCache.get_brush(DIYGrid.DefaultSelectionHighlight));
public SolidColorBrush Fill {
get => GetValue(FillProperty) as SolidColorBrush;
@@ -32,7 +32,7 @@ namespace mbts.ui.controls.adorners {
nameof(Stroke),
FrameworkPropertyMetadataOptions.AffectsRender,
(o, v) => o._stroke = v,
DIYGrid.DefaultSelectionStroke);
FrozenPenCache.get_pen(DIYGrid.DefaultSelectionHighlight));
public Pen Stroke {
get => GetValue(StrokeProperty) as Pen;
@@ -48,6 +48,8 @@ namespace mbts.ui.controls.adorners {
/// <param name="scroll_viewer">The element's ScrollViewer.</param>
/// <param name="drift_multiplier">The number of pixels per scroll unit.</param>
public DragRectAdorner(FrameworkElement adorned_element, ScrollViewer scroll_viewer = null, double drift_multiplier = 1) : base(adorned_element) {
_fill = Fill;
_stroke = Stroke;
_drift_multiplier = drift_multiplier;
AdornedElement.PreviewMouseDown += _on_preview_mouse_down;
AdornedElement.PreviewMouseMove += _on_preview_mouse_move;
@@ -1,18 +1,16 @@
using System;
using mbts.ui.extensions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using bts.utils;
using mbts.ui.extensions;
namespace mbts.ui.controls.adorners {
public class RowSelectionAdorner : Adorner, DIYGrid.ISelectionHighlightListener, IDisposable {
Brush _fill = DIYGrid.DefaultSelectionHighlight;
Pen _stroke = DIYGrid.DefaultSelectionStroke;
class RowSelectionAdorner : Adorner<ListBox>, IDisposable {
Brush _fill;
Pen _stroke;
DIYGridRowsPresenter _diy_rp;
ScrollViewer _diy_scroll;
@@ -25,20 +23,21 @@ namespace mbts.ui.controls.adorners {
nameof(Fill),
FrameworkPropertyMetadataOptions.AffectsRender,
(o, v) => o._fill = v,
DIYGrid.DefaultSelectionHighlight);
FrozenBrushCache.get_brush(DIYGrid.DefaultSelectionHighlight));
public SolidColorBrush Fill {
get => GetValue(FillProperty) as SolidColorBrush;
set => SetValue(FillProperty, value);
}
#endregion
#region Stroke
public static readonly DependencyProperty StrokeProperty =
WPFHelper.create_dp<Pen, RowSelectionAdorner>(
nameof(Stroke),
FrameworkPropertyMetadataOptions.AffectsRender,
(o, v) => o._stroke = v,
DIYGrid.DefaultSelectionStroke);
FrozenPenCache.get_pen(DIYGrid.DefaultSelectionHighlight.with(255)));
public Pen Stroke {
get => GetValue(StrokeProperty) as Pen;
@@ -46,7 +45,9 @@ namespace mbts.ui.controls.adorners {
}
#endregion
internal RowSelectionAdorner(DIYGridRowsPresenter adorned_element, ScrollViewer scroll_viewer) : base(adorned_element) {
public RowSelectionAdorner(DIYGridRowsPresenter adorned_element, ScrollViewer scroll_viewer) : base(adorned_element) {
_fill = Fill;
_stroke = Stroke;
// we want our clicks to go through to the underlying grid
IsHitTestVisible = false;
@@ -60,7 +61,6 @@ namespace mbts.ui.controls.adorners {
_disposables.Add(_diy_scroll.SubscribeScrollChanged((s, e) => InvalidateVisual()));
}
protected override void OnRender(DrawingContext drawingContext) {
base.OnRender(drawingContext);
if (AdornedElement is ListBox lb && lb.SelectedItems != null && lb.SelectedItems.Count > 0) {
@@ -1,4 +1,5 @@
using mbts.ui.converters;
using mbts.ui.dpi;
using mbts.ui.converters;
using mbts.ui.shell;
using mbts.ui.utility.text;
using System;
@@ -10,6 +11,7 @@ using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Media;
using bts;
namespace mbts.ui.controls {
partial class DIYGrid {
@@ -83,79 +85,39 @@ namespace mbts.ui.controls {
}
}
public static Rect? get_clip(Size content_size, double row_height, double left, double unclipped_left, double width) =>
content_size == null ? get_clip(0, 0, row_height, left, unclipped_left, width) :
get_clip(content_size.Width, content_size.Height, row_height, left, unclipped_left, width);
public static Rect? get_clip(double content_width, double content_height, double row_height, double left, double unclipped_left, double width) {
if (content_width > width || unclipped_left < left)
return new Rect(left, 0, width - left + unclipped_left, row_height);
return null;
}
public static IDisposable draw_with_clip(DrawingContext dc, double raw_content_width, double raw_content_height, Thickness padding, double row_height, ColumnGeometry position) =>
draw_with_clip(dc,
raw_content_width + padding.Left + padding.Right,
raw_content_height + padding.Top + padding.Bottom,
row_height, position.left, position.unclipped_left, position.width);
public static IDisposable draw_with_clip(DrawingContext dc, double content_width, double content_height, double row_height, double left, double unclipped_left, double width) {
var clip = get_clip(content_width, content_height, row_height, left, unclipped_left, width);
if (clip.HasValue) {
dc.PushClip(new RectangleGeometry(clip.Value));
return Disposable.Create(dc.Pop);
} else return Disposable.Create(() => { });
}
public static void draw_text(
DrawingContext dc, GlyphContext glyph_context, double font_size, string text, ColumnGeometry position, double row_height,
Brush foreground, Brush background, HorizontalAlignment x_align, VerticalAlignment y_align, Thickness padding) {
DrawingContext dc,
GlyphContext glyph_context,
double font_size,
string text,
ColumnGeometry position,
double row_height,
Brush foreground,
Brush background,
HorizontalAlignment x_align,
VerticalAlignment y_align,
Thickness padding,
bool round_to_layout = true) {
//
if (!string.IsNullOrEmpty(text)) {
var gr = glyph_context.get_glyph_run(text, font_size, position.unclipped_left, 0, position.right, row_height, x_align, y_align, padding);
draw_text(dc, gr, gr.AdvanceWidths.Sum(), font_size, position, padding, row_height, foreground, background);
}
var gr = glyph_context.get_glyph_run(text, font_size, position.unclipped_left, 0, position.right, row_height, x_align, y_align, padding, round_to_layout);
var gwidth = gr == null ? 0 : gr.ComputeAlignmentBox().Width;
draw_text(dc, gr, gwidth, font_size, position, padding, row_height, foreground, background);
}
public static void draw_text(DrawingContext dc, GlyphRun glyph_run, double glyph_width, double font_size, ColumnGeometry position, Thickness padding, double row_height, Brush foreground, Brush background) {
using (draw_with_clip(dc, glyph_width, font_size, padding, row_height, position)) {
if (background != null)
dc.DrawRectangle(background, null, new Rect(position.left, 0, position.width, row_height));
dc.DrawGlyphRun(foreground, glyph_run);
if (glyph_run == null && background == null)
return;
var rect = position.to_rect(row_height);
if (background != null) {
var guidelines = rect.AntiAliasGuidelines(0);
dc.PushGuidelineSet(guidelines);
dc.DrawRectangle(background, null, rect);
dc.Pop();
}
}
public static void draw_text(
DrawingContext dc, GlyphContext glyph_context, double font_size, string text, ColumnGeometry position, double row_height,
Brush foreground, Brush background, HorizontalAlignment x_align, VerticalAlignment y_align, Thickness padding, IEnumerable<CellAnimation> trigger_actions) {
//
using (position.clip_draw(dc, row_height)) {
double adjusted_left = position.unclipped_left;
double available_width = position.width;
if (background != null)
dc.DrawRectangle(background, null, new Rect(position.left, 0, position.width, row_height));
// store any trigger-glyphs in a list, to be drawn after iterating through all triggers (must draw background first, so glyphs will not be covered up).
List<(Brush brush, GlyphRun glyph)> glyphs = new List<(Brush brush, GlyphRun glyph)>(1);
if (trigger_actions != null) {
foreach (CellAnimation ta in trigger_actions) {
switch (ta.animation_type_case) {
case CellAnimation.AnimationTypeOneofCase.BackgroundAnimation:
dc.DrawRectangle(ta.background_animation.brush, ta.background_animation.pen, new Rect(position.left, 0, position.width, row_height));
break;
case CellAnimation.AnimationTypeOneofCase.SymbolAnimation:
var (symbol_text, is_sideways) = _get_symbol_animation_args(ta.symbol_animation);
GlyphRun symbol_glyph = glyph_context.get_glyph_run(symbol_text, font_size, position.width, row_height, HorizontalAlignment.Left, adjusted_left, y_align, 0, is_sideways: is_sideways);
glyphs.Add((ta.symbol_animation.brush, symbol_glyph));
adjusted_left += symbol_glyph.AdvanceWidths.Sum();
break;
}
}
}
foreach (var (brush, glyph) in glyphs) {
dc.DrawGlyphRun(brush, glyph);
}
GlyphRun main_content = glyph_context.get_glyph_run(text, font_size, adjusted_left, 0, position.right, row_height, x_align, y_align, padding);
draw_text(dc, main_content, main_content.AdvanceWidths.Sum(), font_size, position, padding, row_height, foreground, background);
if (glyph_run != null) {
dc.PushClip(new RectangleGeometry(rect));
dc.DrawGlyphRun(foreground, glyph_run);
dc.Pop();
}
}
@@ -165,14 +127,6 @@ namespace mbts.ui.controls {
public static void change_position(FrameworkElement cell_view, double row_height, ColumnGeometry? position) {
if (position.HasValue) {
WPFHelper.set_if_changed(cell_view, VisibilityProperty, Visibility.Visible);
var clip = get_clip(cell_view.DesiredSize, row_height, 0, position.Value.unclipped_left - position.Value.left, position.Value.width);
var el_clip = cell_view.Clip as RectangleGeometry;
if (clip.HasValue) {
if (el_clip == null || !el_clip.Rect.Equals(clip.Value))
cell_view.Clip = new RectangleGeometry(clip.Value);
} else if (cell_view.Clip != null) {
cell_view.Clip = null;
}
} else {
WPFHelper.set_if_changed(cell_view, VisibilityProperty, Visibility.Collapsed);
}
@@ -1,16 +1,28 @@
using mbts.ui.utilities;
using mbts.spark;
using mbts.ui.dpi;
using mbts.ui.utilities;
using mbts.ui.utility.text;
using System;
using System.ComponentModel;
using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Threading;
using FontFamily = System.Windows.Media.FontFamily;
namespace mbts.ui.controls {
enum HyperlinkCellClickMode {
Text,
Anywhere
}
class HyperlinkCell : FrameworkElement, ICommandSource {
// these fields each represent properties that are accessed frequently,
// they are stored locally to reduce repetative DependencyProperty lookups
@@ -18,10 +30,35 @@ namespace mbts.ui.controls {
Brush _background, _foreground_backing, _border_brush_backing;
double _glyph_width => _gcr.glyph_run.AdvanceWidths.Sum();
GlyphRun _glyph_run => _gcr.glyph_run;
bool _is_mouse_over_text, _can_execute;
bool
_is_mouse_over_text,
_is_mouse_over_click_area_backing,
_can_execute,
_snaps_to_device_pixels;
Pen _underline_pen_backing, _border_stroke_pen_backing;
GlyphContentRect _gcr = new GlyphContentRect();
GlyphContentRect _gcr = new GlyphContentRect {
// add 2 pixels to account for underline (1 for space, 1 for the line itself)
content_height_offset = HighDpiExtensions.pixel_unit * 2
};
IDisposable _mouse_over_click_area_op;
bool _is_mouse_over_click_area {
get => _is_mouse_over_click_area_backing;
set {
if (_is_mouse_over_click_area_backing != value) {
Disposable.dispose(ref _mouse_over_click_area_op);
_is_mouse_over_click_area_backing = value;
if (value) {
_mouse_over_click_area_op = WorkLoop.Start(
$"Evaluate {nameof(HyperlinkCell)} {nameof(IsMouseOverClickArea)}",
TimeSpan.FromMilliseconds(10),
DispatcherPriority.Input,
_on_mouse_changed,
WorkLoop.default_excessive_cycle_ratio);
}
}
}
}
Brush _foreground {
get => _foreground_backing;
set {
@@ -37,13 +74,23 @@ namespace mbts.ui.controls {
}
}
Thickness _border_thickness {
get => _gcr.border_thickness;
get => _snaps_to_device_pixels ? _gcr.border_thickness.RoundToLayout() : _gcr.border_thickness;
set {
_gcr.border_thickness = new Thickness(value.Left);
_border_stroke_pen_backing = null;
}
}
Pen _underline_pen => _underline_pen_backing ?? (_foreground == null ? null : (_underline_pen_backing = new Pen(_foreground, 1)));
Pen _underline_pen {
get {
if (_underline_pen_backing == null) {
var fg = _foreground;
if (fg != null) {
_underline_pen_backing = new Pen(fg, _snaps_to_device_pixels ? HighDpiExtensions.pixel_unit : 1);
}
}
return _underline_pen_backing;
}
}
Pen _border_stroke_pen =>
// border_thickness.Left is used as the uniform border thickness. This can be changed if need be.
_border_stroke_pen_backing == null && _border_brush != null && _border_thickness.Left > 0 ?
@@ -51,6 +98,8 @@ namespace mbts.ui.controls {
_border_stroke_pen_backing;
static HyperlinkCell() {
IsMouseOverClickAreaProperty = IsMouseOverClickAreaPropertyKey.DependencyProperty;
DefaultStyleKeyProperty.OverrideMetadata(typeof(HyperlinkCell), new FrameworkPropertyMetadata(typeof(HyperlinkCell)));
WPFHelper.override_metadata<HorizontalAlignment, HyperlinkCell>(
HorizontalAlignmentProperty,
FrameworkPropertyMetadataOptions.AffectsArrange |
@@ -64,6 +113,11 @@ namespace mbts.ui.controls {
FrameworkPropertyMetadataOptions.AffectsRender,
(o, v) => o._gcr.width = v,
double.NaN);
WPFHelper.override_metadata<bool, HyperlinkCell>(
SnapsToDevicePixelsProperty,
FrameworkPropertyMetadataOptions.AffectsRender,
(o, v) => o._snaps_to_device_pixels = v,
true);
}
public HyperlinkCell() {
@@ -73,7 +127,7 @@ namespace mbts.ui.controls {
_gcr.min_height = MinHeight;
_gcr.height = Height;
_gcr.max_height = Height;
_gcr.padding = _underline_padding(Padding);
_gcr.padding = Padding;
_gcr.border_thickness = BorderThickness;
_gcr.horizontal_alignment = HorizontalAlignment;
_gcr.horizontal_content_alignment = HorizontalContentAlignment;
@@ -86,8 +140,39 @@ namespace mbts.ui.controls {
if (Text is string str && !string.IsNullOrEmpty(str))
_gcr.text_lines = new[] { str };
_gcr.glyph_context = GlyphContext.create(FontFamily, FontWeight);
_snaps_to_device_pixels = SnapsToDevicePixels;
}
#region IsMouseOverClickArea
static readonly DependencyPropertyKey IsMouseOverClickAreaPropertyKey =
DependencyProperty.RegisterReadOnly(
"IsMouseOverClickArea",
typeof(bool),
typeof(HyperlinkCell),
new FrameworkPropertyMetadata(
false,
FrameworkPropertyMetadataOptions.AffectsRender,
OnIsMouseOverClickAreaChanged,
CoerceIsMouseOverClickArea));
public static readonly DependencyProperty IsMouseOverClickAreaProperty;
static void OnIsMouseOverClickAreaChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
if (d is HyperlinkCell hlc)
hlc._is_mouse_over_click_area = e.NewValue is bool b && b;
}
static object CoerceIsMouseOverClickArea(DependencyObject d, object baseValue) =>
d is HyperlinkCell hlc ?
hlc._is_mouse_over_text || (hlc.IsMouseOver && hlc.HyperlinkClickMode == HyperlinkCellClickMode.Anywhere) :
baseValue;
public bool IsMouseOverClickArea {
get => (bool)GetValue(IsMouseOverClickAreaProperty);
private set => SetValue(IsMouseOverClickAreaPropertyKey, value);
}
#endregion
#region BorderThickness
public static readonly DependencyProperty BorderThicknessProperty =
Control.BorderThicknessProperty.AddOwner(typeof(HyperlinkCell),
@@ -135,7 +220,8 @@ namespace mbts.ui.controls {
public static readonly DependencyProperty VerticalContentAlignmentProperty = Control.VerticalContentAlignmentProperty.AddOwner(typeof(HyperlinkCell),
new FrameworkPropertyMetadata(
VerticalAlignment.Center,
FrameworkPropertyMetadataOptions.AffectsArrange,
FrameworkPropertyMetadataOptions.AffectsArrange |
FrameworkPropertyMetadataOptions.AffectsRender,
(d, e) => ((HyperlinkCell)d)._gcr.vertical_content_alignment = (VerticalAlignment)e.NewValue));
public VerticalAlignment VerticalContentAlignment {
@@ -215,7 +301,7 @@ namespace mbts.ui.controls {
nameof(Padding),
FrameworkPropertyMetadataOptions.AffectsMeasure |
FrameworkPropertyMetadataOptions.AffectsRender,
(p, v) => p._gcr.padding = _underline_padding(v), DIYGrid.DefaultCellPadding);
(p, v) => p._gcr.padding = default, default);
public Thickness Padding { get => (Thickness)GetValue(PaddingProperty); set => SetValue(PaddingProperty, value); }
#endregion
@@ -231,6 +317,18 @@ namespace mbts.ui.controls {
public string Text { get => GetValue(TextProperty) as string; set => SetValue(TextProperty, value); }
#endregion
#region HyperlinkClickMode
public static readonly DependencyProperty HyperlinkClickModeProperty =
WPFHelper.create_dp<HyperlinkCellClickMode, HyperlinkCell>(
nameof(HyperlinkClickMode),
(o, v) => o.CoerceValue(IsMouseOverClickAreaProperty));
public HyperlinkCellClickMode HyperlinkClickMode {
get => (HyperlinkCellClickMode)GetValue(HyperlinkClickModeProperty);
set => SetValue(HyperlinkClickModeProperty, value);
}
#endregion
#region ICommandSource
#region Command
@@ -297,8 +395,8 @@ namespace mbts.ui.controls {
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e) {
base.OnMouseLeftButtonDown(e);
_check_mouse_over_text(e);
if (_is_mouse_over_text) {
_on_mouse_changed(e);
if (_is_mouse_over_click_area) {
// Hyperlink should take focus when left mouse button is clicked on it
// This is consistent with all ButtonBase controls and current Win32 behavior
Focus();
@@ -308,6 +406,8 @@ namespace mbts.ui.controls {
if (e.ButtonState == MouseButtonState.Pressed) {
// Capture the mouse, and make sure we got it.
CaptureMouse();
} else {
ReleaseMouseCapture();
}
e.Handled = true;
}
@@ -315,23 +415,25 @@ namespace mbts.ui.controls {
protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e) {
base.OnMouseLeftButtonUp(e);
_check_mouse_over_text(e);
if (IsMouseCaptured)
_on_mouse_changed(e);
if (IsMouseCaptured) {
ReleaseMouseCapture();
if (_can_execute && _is_mouse_over_text)
CommandHelper.ExecuteCommandSource(this);
e.Handled = true;
if (_can_execute)
CommandHelper.ExecuteCommandSource(this);
_on_mouse_changed(e);
e.Handled = true;
}
}
#endregion
protected override void OnIsMouseDirectlyOverChanged(DependencyPropertyChangedEventArgs e) {
if (_is_mouse_over_text != IsMouseDirectlyOver)
_toggle_mouse_over_text();
protected override void OnIsMouseDirectlyOverChanged(DependencyPropertyChangedEventArgs e) => _on_mouse_changed();
protected override void OnPreviewMouseMove(MouseEventArgs e) {
_on_mouse_changed(e);
e.Handled = IsMouseCaptured;
}
protected override void OnPreviewMouseMove(MouseEventArgs e) => _check_mouse_over_text(e);
protected override void OnMouseEnter(MouseEventArgs e) => _check_mouse_over_text(e);
protected override void OnMouseLeave(MouseEventArgs e) => _check_mouse_over_text(e);
protected override void OnMouseEnter(MouseEventArgs e) => _on_mouse_changed(e);
protected override void OnMouseLeave(MouseEventArgs e) => _on_mouse_changed(e);
protected override Size ArrangeOverride(Size finalSize) => finalSize;
@@ -347,45 +449,32 @@ namespace mbts.ui.controls {
var rs = RenderSize;
var rect = new Rect(0, 0, rs.Width, rs.Height);
var bg = Background;
if (bg != null) {
drawingContext.DrawRectangle(bg, _border_stroke_pen, rect);
}
if (_glyph_run != null) {
drawingContext.DrawRectangle(bg, _border_stroke_pen, rect, _snaps_to_device_pixels);
var gr = _glyph_run;
if (gr != null) {
drawingContext.PushClip(new RectangleGeometry(rect));
drawingContext.DrawGlyphRun(_foreground, _glyph_run);
if (_is_mouse_over_text) {
drawingContext.DrawGlyphRun(_foreground, gr);
if (IsMouseOverClickArea) {
// draw underline
double x = _gcr.glyph_run.BaselineOrigin.X;
double y = _gcr.glyph_run.BaselineOrigin.Y + 2;
drawingContext.DrawLine(_underline_pen, new Point(x, y), new Point(x + _gcr.glyph_run.AdvanceWidths.Sum(), y));
var ul_pen = _underline_pen;
if (ul_pen != null) {
var line_start = new Point(gr.BaselineOrigin.X, gr.BaselineOrigin.Y + 2 * ul_pen.Thickness);
drawingContext.DrawHorizontalLine(ul_pen, line_start, gr.AdvanceWidths.Sum(), _snaps_to_device_pixels);
}
}
drawingContext.Pop();
}
}
static Thickness _underline_padding(Thickness padding) =>
new Thickness(padding.Left, padding.Top, padding.Right, padding.Bottom + 2);
void _on_mouse_changed() =>
_on_mouse_changed(Mouse.GetPosition(this));
void _check_mouse_over_text(MouseEventArgs e) {
var pt = e.GetPosition(this);
var orig = _gcr.content_area;
var exp = orig.Expanded(1);
if (_is_mouse_over_text != exp.Contains(pt)) {
bts.Log.assert(_is_mouse_over_text != _gcr.content_area.Expanded(1).Contains(e.GetPosition(this)));
_toggle_mouse_over_text();
return;
}
if (_is_mouse_over_text != _gcr.content_area.Expanded(1).Contains(e.GetPosition(this)))
_toggle_mouse_over_text();
}
void _on_mouse_changed(MouseEventArgs e) =>
_on_mouse_changed(e.GetPosition(this));
void _toggle_mouse_over_text() {
_is_mouse_over_text = !_is_mouse_over_text;
if (_is_mouse_over_text)
SetCurrentValue(CursorProperty, Cursors.Hand);
else
ClearValue(CursorProperty);
InvalidateVisual();
void _on_mouse_changed(Point pt) {
_is_mouse_over_text = _gcr.content_area.Expanded(1).Contains(pt);
CoerceValue(IsMouseOverClickAreaProperty);
}
}
}
@@ -1,5 +1,5 @@
using mbts.ui.utility.text;
using System;
using mbts.ui.dpi;
using mbts.ui.utility.text;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
@@ -15,6 +15,7 @@ namespace mbts.ui.controls {
Brush _background, _foreground, _border_brush_backing;
double _glyph_width => _gcr.glyph_run.AdvanceWidths.Sum();
GlyphRun _glyph_run => _gcr.glyph_run;
bool _snaps_to_device_pixels;
Pen _border_stroke_pen_backing;
GlyphContentRect _gcr = new GlyphContentRect();
@@ -26,7 +27,7 @@ namespace mbts.ui.controls {
}
}
Thickness _border_thickness {
get => _gcr.border_thickness;
get => _snaps_to_device_pixels ? _gcr.border_thickness.RoundToLayout() : _gcr.border_thickness;
set {
_gcr.border_thickness = new Thickness(value.Left);
_border_stroke_pen_backing = null;
@@ -52,6 +53,11 @@ namespace mbts.ui.controls {
FrameworkPropertyMetadataOptions.AffectsRender,
(o, v) => o._gcr.width = v,
double.NaN);
WPFHelper.override_metadata<bool, TextCell>(
SnapsToDevicePixelsProperty,
FrameworkPropertyMetadataOptions.AffectsRender,
(o, v) => o._snaps_to_device_pixels = v,
true);
}
public TextCell() {
@@ -74,6 +80,7 @@ namespace mbts.ui.controls {
if (Text is string str && !string.IsNullOrEmpty(str))
_gcr.text_lines = new[] { str };
_gcr.glyph_context = GlyphContext.create(FontFamily, FontWeight);
_snaps_to_device_pixels = SnapsToDevicePixels;
}
#region BorderThickness
@@ -111,14 +118,7 @@ namespace mbts.ui.controls {
new FrameworkPropertyMetadata(
HorizontalAlignment.Center,
FrameworkPropertyMetadataOptions.AffectsArrange,
(d, e) => {
if (e.OldValue is HorizontalAlignment oldha) {
if (oldha == HorizontalAlignment.Right) {
}
}
((TextCell)d)._gcr.horizontal_content_alignment = (HorizontalAlignment)e.NewValue;
}));
(d, e) => ((TextCell)d)._gcr.horizontal_content_alignment = (HorizontalAlignment)e.NewValue));
public HorizontalAlignment HorizontalContentAlignment {
get => (HorizontalAlignment)GetValue(HorizontalContentAlignmentProperty);
@@ -130,7 +130,8 @@ namespace mbts.ui.controls {
public static readonly DependencyProperty VerticalContentAlignmentProperty = Control.VerticalContentAlignmentProperty.AddOwner(typeof(TextCell),
new FrameworkPropertyMetadata(
VerticalAlignment.Center,
FrameworkPropertyMetadataOptions.AffectsArrange,
FrameworkPropertyMetadataOptions.AffectsArrange |
FrameworkPropertyMetadataOptions.AffectsRender,
(d, e) => ((TextCell)d)._gcr.vertical_content_alignment = (VerticalAlignment)e.NewValue));
public VerticalAlignment VerticalContentAlignment {
@@ -210,8 +211,7 @@ namespace mbts.ui.controls {
nameof(Padding),
FrameworkPropertyMetadataOptions.AffectsMeasure |
FrameworkPropertyMetadataOptions.AffectsRender,
(p, v) => p._gcr.padding = v,
DIYGrid.DefaultCellPadding);
(p, v) => p._gcr.padding = default, default);
public Thickness Padding { get => (Thickness)GetValue(PaddingProperty); set => SetValue(PaddingProperty, value); }
#endregion
@@ -237,35 +237,17 @@ namespace mbts.ui.controls {
return _gcr.layout_size;
}
double bl_x = 0;
protected override void OnRender(DrawingContext drawingContext) {
var rs = RenderSize;
var rect = new Rect(0, 0, rs.Width, rs.Height);
var bg = Background;
if (bg != null) {
drawingContext.DrawRectangle(bg, _border_stroke_pen, rect);
}
if (_glyph_run != null) {
var new_x = _glyph_run.BaselineOrigin.X;
if (Math.Abs(bl_x - new_x) > 5) {
if (new_x < bl_x) {
}
}
if (Text == "-3,375.000") {
}
bl_x = new_x;
drawingContext.DrawRectangle(bg, _border_stroke_pen, rect, _snaps_to_device_pixels);
var gr = _glyph_run;
if (gr != null) {
drawingContext.PushClip(new RectangleGeometry(rect));
drawingContext.DrawGlyphRun(_foreground, _glyph_run);
drawingContext.DrawGlyphRun(_foreground, gr);
drawingContext.Pop();
}
if (bg != null) {
var bgo = DIYGrid.GetBackgroundOverlay(this);
if (bgo != null)
drawingContext.DrawRectangle(bgo, null, rect);
}
}
}
}
}
@@ -114,6 +114,7 @@ namespace mbts.ui.controls {
Aggressor = "Aggressor",
Volume = "Volume",
Last = "Last",
S = "S",
Pos = "Pos",
Working = "Working",
Bid = "Bid",
@@ -124,6 +125,7 @@ namespace mbts.ui.controls {
SessionLow = "Low",
SessionHigh = "High",
FillKind = "Fill Kind",
FillStatus = "Fill Status",
Name = "Name",
Description = "Description",
N_Legs = "# Legs",
@@ -1,30 +1,76 @@
using System;
using bts.utils;
using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Media;
using System.Windows.Controls;
namespace mbts.ui.controls {
// Column types are nested inside the DIYGrid
partial class DIYGrid {
public struct ColumnGeometry {
public ColumnBase column;
public DIYGridSuperHeader super_column;
public double unclipped_left; /* If the column was unclipped */
public double left;
public double width { get { return column.ActualWidth; } }
public double right { get { return unclipped_left + width; } }
public readonly struct CellGeometry {
public CellGeometry(ColumnGeometry column, ListBoxItem row) {
this.column = column;
this.row = row;
}
public ColumnGeometry column { get; }
public ListBoxItem row { get; }
public void Deconstruct(out ListBoxItem row, out ColumnGeometry column) {
row = this.row;
column = this.column;
}
public Rect to_rect(double y) =>
column.to_visible_rect(row.ActualHeight, y);
public static implicit operator (ListBoxItem, ColumnGeometry) (CellGeometry cell) =>
(cell.row, cell.column);
}
public readonly struct ColumnGeometry : IComparable<ColumnGeometry> {
public static readonly IEqualityComparer<ColumnGeometry> PositionEqualityComparer =
EqualityComparer.Create<ColumnGeometry, (double, double)>(c => (c.left, c.right));
public readonly ColumnBase column;
public readonly DIYGridSuperHeader super_column;
public readonly double
left,
// If the column was unclipped
unclipped_left,
right;
public ColumnGeometry(ColumnBase column, DIYGridSuperHeader super_column, double left, double right)
: this(column, super_column, left, left, right) { }
public ColumnGeometry(ColumnBase column, DIYGridSuperHeader super_column, double left, double unclipped_left, double right) {
this.column = column;
this.super_column = super_column;
this.left = left;
this.unclipped_left = unclipped_left;
this.right = right;
}
public double width => right - left;
public bool is_partially_obscured { get { return unclipped_left < left; } }
public double visible_width => Math.Max(0, width - left + unclipped_left);
public string group { get { return super_column == null ? "" : super_column.FullHeader; } }
public Rect to_rect(double row_height) =>
new Rect(left, 0, width, row_height);
public Rect to_visible_rect(double row_height, double y) =>
new Rect(unclipped_left, y, visible_width, row_height);
public override string ToString() {
return "{3} {0:0.0},{1:0.0} w{2:0.0}".fmt(left, right, width, column.Header);
}
public string group { get { return super_column == null ? "" : super_column.FullHeader; } }
public RectangleGeometry get_clip(double row_height) =>
new RectangleGeometry(new Rect(left, 0, width - left + unclipped_left, row_height));
public IDisposable clip_draw(DrawingContext dc, double row_height) {
dc.PushClip(get_clip(row_height));
return Disposable.Create(dc.Pop);
public int CompareTo(ColumnGeometry other) {
int comp = left.CompareTo(other.left);
if (comp != 0)
return comp;
return right.CompareTo(other.right);
}
}
}
@@ -1,40 +1,72 @@
using mbts.spark.filters;
using mbts.ui.converters;
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Media;
namespace mbts.ui.controls {
partial class DIYGrid {
internal class HyperlinkElementColumn<TRowVM, TKey> : FrameworkElementColumnBase<TKey>, IFrameworkElementColumn, IFilterableColumn, IKeyedColumn<TKey> {
internal class HyperlinkElementColumn<TKey> : ColumnBase<TKey>, IFrameworkElementColumn, IFilterableColumn, IKeyedColumn<TKey> {
static readonly Thickness _default_padding = new Thickness(6, 0, 6, 0);
readonly BindingBase _binding, _command_binding;
public HyperlinkElementColumn(
TKey key, BindingBase binding, BindingBase command_binding, IDataGridColumnFilter filter, string filter_template) : base(key) {
public HyperlinkElementColumn(TKey key, IDataGridColumnFilter filter, string filter_template)
: base(key) {
this.filter = filter;
this.filter_template = filter_template;
_binding = binding;
_command_binding = command_binding;
}
public IDataGridColumnFilter filter { get; }
public string filter_template { get; }
public event Action<HyperlinkCell> view_generated;
public Style ElementStyle { get; set; }
public IDataGridColumnFilter filter { get; set; }
public string filter_template { get; set; }
public HyperlinkCellClickMode HyperlinkClickMode { get; set; }
public Thickness padding { get; set; } = _default_padding;
public override FrameworkElement view_factory() {
var hl = new HyperlinkCell { Padding = padding };
// bindings
public BindingBase CommandBinding { get; set; }
public BindingBase CommandParameterBinding { get; set; }
public BindingBase TextBinding { get; set; }
public FrameworkElement view_factory() {
var hl = new HyperlinkCell {
HyperlinkClickMode = HyperlinkClickMode,
Padding = padding
};
hl.SetCurrentValue(DIYGrid.ColumnProperty, this);
// bindings
if (TextBinding != null)
hl.SetBinding(HyperlinkCell.TextProperty, TextBinding);
if (CommandBinding != null)
hl.SetBinding(HyperlinkCell.CommandProperty, CommandBinding);
if (CommandParameterBinding != null)
hl.SetBinding(HyperlinkCell.CommandParameterProperty, CommandParameterBinding);
hl.SetBinding(HyperlinkCell.ForegroundProperty, new Binding(nameof(font_brush)) { Source = this });
hl.SetBinding(HyperlinkCell.TextProperty, _binding);
hl.SetBinding(HyperlinkCell.FontFamilyProperty, new Binding(nameof(font_family)) { Source = this });
hl.SetBinding(HyperlinkCell.FontSizeProperty, new Binding(nameof(font_size)) { Source = this });
hl.SetBinding(HyperlinkCell.HorizontalAlignmentProperty, new Binding(nameof(horizontal_alignment)) { Source = this });
hl.SetBinding(HyperlinkCell.CommandProperty, _command_binding);
hl.SetBinding(HyperlinkCell.WidthProperty, new Binding(nameof(ActualWidth)) { Source = this });
hl.SetBinding(HyperlinkCell.BackgroundProperty, new Binding(nameof(background_brush)) { Source = this });
hl.SetBinding(HyperlinkCell.HorizontalContentAlignmentProperty, new Binding(nameof(horizontal_alignment)) { Source = this });
hl.SetBinding(FrameworkElement.WidthProperty, new Binding(nameof(ActualWidth)) { Source = this });
hl.SetBinding(
CursorProperty,
new Binding {
Source = hl,
Path = new PropertyPath(HyperlinkCell.IsMouseOverClickAreaProperty),
Converter = new BooleanToObjectConverter {
ValueForFalse = Cursors.Arrow,
ValueForTrue = Cursors.Hand
}
});
// background
var bg_brush = new SolidColorBrush();
BindingOperations.SetBinding(bg_brush, SolidColorBrush.ColorProperty, new Binding(nameof(background_color)) { Source = this });
hl.SetCurrentValue(HyperlinkCell.BackgroundProperty, bg_brush);
// style
if (ElementStyle != null)
hl.Style = ElementStyle;
view_generated?.Invoke(hl);
return hl;
}
}
}
}
}
@@ -1,44 +1,50 @@
using mbts.spark.filters;
using System.Windows;
using System.Windows.Data;
using System.Windows.Media;
namespace mbts.ui.controls {
partial class DIYGrid {
internal class TextColumn<TColKey> : FrameworkElementColumnBase<TColKey>, IFilterableColumn {
readonly BindingBase _binding;
internal class TextColumn<TKey> : FrameworkElementColumnBase<TKey>, IFrameworkElementColumn, IFilterableColumn, IKeyedColumn<TKey> {
static readonly Thickness _default_padding = new Thickness(6, 0, 6, 0);
public TextColumn(TColKey key, BindingBase binding, IDataGridColumnFilter filter, string filter_template) : base(key) {
public TextColumn(
TKey key, IDataGridColumnFilter filter, string filter_template) : base(key) {
this.filter = filter;
this.filter_template = filter_template;
_binding = binding;
}
public Style ElementStyle { get; set; }
public IDataGridColumnFilter filter { get; }
public string filter_template { get; }
public BindingBase background_binding { get; set; }
public BindingBase font_weight_binding { get; set; }
public BindingBase foreground_binding { get; set; }
public BindingBase tooltip_binding { get; set; }
public VerticalAlignment vertical_alignment { get; set; } = VerticalAlignment.Center;
public Thickness padding { get; set; } = _default_padding;
// bindings
public BindingBase TextBinding { get; set; }
public BindingBase ToolTipBinding { get; set; }
public override FrameworkElement view_factory() {
var v = new TextCell { Foreground = font_brush, VerticalAlignment = vertical_alignment };
v.SetBinding(TextCell.TextProperty, _binding);
var v = new TextCell {
Padding = padding
};
// bindings
if (TextBinding != null)
v.SetBinding(TextCell.TextProperty, TextBinding);
if (ToolTipBinding != null)
v.SetBinding(ToolTipProperty, ToolTipBinding);
v.SetBinding(TextCell.ForegroundProperty, new Binding(nameof(font_brush)) { Source = this });
v.SetBinding(TextCell.FontFamilyProperty, new Binding(nameof(font_family)) { Source = this });
v.SetBinding(TextCell.FontSizeProperty, new Binding(nameof(font_size)) { Source = this });
v.SetBinding(TextCell.HorizontalAlignmentProperty, new Binding(nameof(horizontal_alignment)) { Source = this });
v.SetBinding(TextCell.WidthProperty, new Binding(nameof(ActualWidth)) { Source = this });
try_set_binding(TextCell.BackgroundProperty, background_binding);
try_set_binding(TextCell.ForegroundProperty, foreground_binding);
try_set_binding(TextCell.FontWeightProperty, font_weight_binding);
try_set_binding(TextCell.ToolTipProperty, tooltip_binding);
v.SetBinding(TextCell.HorizontalContentAlignmentProperty, new Binding(nameof(horizontal_alignment)) { Source = this });
v.SetBinding(FrameworkElement.WidthProperty, new Binding(nameof(ActualWidth)) { Source = this });
// background
var bg_brush = new SolidColorBrush();
BindingOperations.SetBinding(bg_brush, SolidColorBrush.ColorProperty, new Binding(nameof(background_color)) { Source = this });
v.Background = bg_brush;
// style
if (ElementStyle != null)
v.Style = ElementStyle;
return v;
//
void try_set_binding(DependencyProperty p, BindingBase b) {
if (b != null)
v.SetBinding(p, b);
}
}
}
}
}
}
+451 -78
View File
@@ -1,9 +1,12 @@
using bts;
using bts.api.core;
using bts.collections;
using bts.utils;
using mbts.spark.filters;
using mbts.ui.controls.adorners;
using mbts.ui.converters;
using mbts.ui.dpi;
using mbts.ui.extensions;
using mbts.ui.settings;
using mbts.ui.utility;
using mbts.utils;
using System;
@@ -22,6 +25,7 @@ using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;
namespace mbts.ui.controls {
interface IHandleGridSorting {
@@ -74,6 +78,26 @@ namespace mbts.ui.controls {
}
}
public static void reset_sorting<TKey>(IEnumerable<DIYGrid.ColumnBase> columns, IEnumerable<SortingCriteria> sorting_criterion) where TKey : struct =>
reset_sorting(columns, convert_sorting_criterion<TKey>(sorting_criterion));
public static void reset_sorting<TKey>(IEnumerable<DIYGrid.ColumnBase> columns, IEnumerable<(TKey key, ListSortDirection direction)> sorting_hierarchy) where TKey : struct {
// TODO: incorporate the sort hierarchy/index, for nested sorting
var active_cols = sorting_hierarchy.empty_if_null()
.Select((k, i) => (k, i))
.ToDictionary(tup => tup.k.key, tup => (tup.k.direction, tup.i));
// columns are dispatcher objects, and might throw if they are accessed after the view has been shut down
try {
foreach (var c in columns) {
if (Enum.TryParse(c.id, true, out TKey key) && active_cols.TryGetValue(key, out var tup)) {
c.SortDirection = tup.direction;
} else {
c.SortDirection = null;
}
}
} catch { }
}
public static IObservable<ImmutableList<(TKey key, IComparer<TRow> comparer, ListSortDirection direction)>> observe_active_comparers<TKey, TRow>(
DIYGrid grid, Func<TKey, IComparer<TRow>> get_comparer, bool mark_handled = true) =>
//
@@ -206,10 +230,11 @@ namespace mbts.ui.controls {
double _row_height = DIYGrid.DefaultRowHeight;
ScrollContentPresenter _content_presenter_backing;
DIYGrid _grid;
DragRectAdorner _drag_rect_adorner;
AdornerHost<Rectangle> _drag_rect_adorner;
RowSelectionAdorner _rsa;
DIYGridCellHoverAdorner _cell_hover_adorner;
ScrollViewer _scroll_viewer;
Point? _drag_start;
Point? _drag_start, _drag_stop;
double _drag_start_vertical_drift;
bool _support_group_items;
int _drag_start_index;
@@ -219,7 +244,10 @@ namespace mbts.ui.controls {
DefaultStyleKeyProperty.OverrideMetadata(typeof(DIYGridRowsPresenter), new FrameworkPropertyMetadata(typeof(DIYGridRowsPresenter)));
public DIYGridRowsPresenter() {
Loaded += (s, e) => {
Loaded += on_loaded;
Unloaded += on_unloaded;
//
void on_loaded(object sender, RoutedEventArgs e) {
if (_grid != null)
_grid.RowHeightChanged -= on_row_height_changed;
_grid = this.find_ancestor_of_type<DIYGrid>();
@@ -228,31 +256,111 @@ namespace mbts.ui.controls {
_support_group_items = _grid.SupportGroupItems;
_grid.SupportGroupItemsChanged += on_support_group_items_changed;
_scroll_viewer = this.descendants_of_type<ScrollViewer>().First();
if (SelectionMode != SelectionMode.Single) {
_drag_rect_adorner = new DragRectAdorner(this, _scroll_viewer, _row_height);
_drag_rect_adorner.SetResourceReference(DragRectAdorner.FillProperty, DIYGrid.ActiveSelectionHighlightKey);
_drag_rect_adorner.SetResourceReference(DragRectAdorner.StrokeProperty, DIYGrid.SelectionStrokeKey);
_drag_rect_adorner.attach();
}
var explicit_drag_src = Observable.Create<bool>(o => {
_begin_explicit_drag += o.OnNext;
return Disposable.Create(() => _begin_explicit_drag -= o.OnNext);
});
DragDropHelper.subscribe_drag_coordinates(this, init_element: _content_presenter, use_preview_events: true, explicit_drag: explicit_drag_src).Subscribe(pt => {
if (_drag_start.HasValue) {
if (pt.HasValue) {
_drag_stop = pt.Value;
_refresh_drag_rect();
} else {
if (_drag_stop.HasValue) {
_end_drag(_drag_stop.Value);
}
_drag_start = null;
_drag_stop = null;
}
} else if (pt.HasValue) {
_begin_drag_selection(pt.Value);
}
});
// cell hover adorner
_cell_hover_adorner = new DIYGridCellHoverAdorner(this, _scroll_viewer);
_cell_hover_adorner.SetBinding(VisibilityProperty, new Binding {
Path = new PropertyPath(IsMouseOverProperty),
Converter = new converters.BooleanToVisibilityConverter(),
Source = this
});
// Uncomment call below to activate cell hover feedback.
// In the meantime, cells handle the mouse hover feedback individually.
//set_selection_display_bindings(
// _cell_hover_adorner,
// fill_prop: DIYGridCellHoverAdorner.CellFillProperty,
// fill_converter: DIYGrid.CellHoverHighlightBrushConverter,
// DIYGridCellHoverAdorner.CellStrokeProperty,
// DIYGrid.CellHoverHighlightPenConverter,
// bind_opacity: false);
_set_selection_display_bindings(
_cell_hover_adorner,
fill_prop: DIYGridCellHoverAdorner.RowFillProperty,
fill_converter: DIYGrid.RowHoverHighlightBrushConverter,
//stroke_prop: DIYGridCellHoverAdorner.RowStrokeProperty,
//stroke_converter: DIYGrid.CellHoverHighlightPenConverter,
bind_opacity: false);
_cell_hover_adorner.attach();
// row selection adorner
_rsa = new RowSelectionAdorner(this, _scroll_viewer);
_set_selection_display_bindings(
_rsa,
fill_prop: RowSelectionAdorner.FillProperty,
fill_converter: DIYGrid.SelectionHighlightBrushConverter,
stroke_prop: RowSelectionAdorner.StrokeProperty,
stroke_converter: DIYGrid.SelectionHighlightPenConverter);
_rsa.attach();
};
Unloaded += (s, e) => {
}
void on_unloaded(object sender, RoutedEventArgs e) {
if (_drag_rect_adorner != null) {
_drag_rect_adorner.detach();
Disposable.dispose(ref _drag_rect_adorner);
_drag_rect_adorner = null;
}
if (_rsa != null) {
_rsa.detach();
Disposable.dispose(ref _rsa);
}
};
if (_cell_hover_adorner != null) {
_cell_hover_adorner.detach();
Disposable.dispose(ref _cell_hover_adorner);
}
}
void on_row_height_changed(double v) => _row_height = v;
void on_support_group_items_changed(bool v) => _support_group_items = v;
}
void _refresh_drag_rect() {
var pres = _content_presenter;
if (pres == null)
return;
//var left = Math.Min(_drag_start.Value.X, _drag_stop.Value.X);
//var top = Math.Min(_drag_start.Value.Y, _drag_stop.Value.Y);
//var width = Math.Abs(_drag_start.Value.X - _drag_stop.Value.X);
//var height = Math.Abs(_drag_start.Value.Y - _drag_stop.Value.Y);
//var clipped_rect = this.GetChildBounds(pres);
//clipped_rect.Intersect(new Rect(left, top, width, height));
//_drag_rect_adorner.Child.Margin = new Thickness(clipped_rect.Left, clipped_rect.Top, 0, 0);
//_drag_rect_adorner.Child.Width = clipped_rect.Width;
//_drag_rect_adorner.Child.Height = clipped_rect.Height;
var left = Math.Min(_drag_start.Value.X, _drag_stop.Value.X);
var top = Math.Min(_drag_start.Value.Y, _drag_stop.Value.Y);
//var tl = TranslatePoint(new Point(left, top), pres);
//var br = TranslatePoint(new Point(
// tl.X + Math.Abs(_drag_start.Value.X - _drag_stop.Value.X),
// tl.Y + Math.Abs(_drag_start.Value.Y - _drag_stop.Value.Y)), pres);
double vdiff = _drag_start_vertical_drift - _scroll_viewer.VerticalOffset;
_drag_rect_adorner.Child.Margin = new Thickness(left, top, 0, 0);
_drag_rect_adorner.Child.Width = Math.Abs(_drag_start.Value.X - _drag_stop.Value.X);
_drag_rect_adorner.Child.Height = Math.Abs(_drag_start.Value.Y - _drag_stop.Value.Y) + Math.Abs(vdiff);
}
public event GroupItemSelectionPivotEventHandler GroupItemSelectionPivot;
event Action<bool> _begin_explicit_drag;
ScrollContentPresenter _content_presenter => _content_presenter_backing ??
(_content_presenter_backing = this.descendants_of_type<ScrollContentPresenter>().FirstOrDefault());
@@ -264,8 +372,28 @@ namespace mbts.ui.controls {
base.UnselectAll();
}
protected override void OnPreviewMouseLeftButtonDown(MouseButtonEventArgs e) {
var pt = e.GetPosition(this);
public void begin_explicit_drag() {
if (_begin_explicit_drag != null) {
Mouse.Capture(this, CaptureMode.SubTree);
if (!_drag_start.HasValue) {
if (_begin_explicit_drag == null) {
// capturing the mouse caused listener(s) to unhook from the event
// release capture since the drag selection has been aborted.
ReleaseMouseCapture();
} else {
var pt = Mouse.GetPosition(this);
if (this.ChildAtPoint(pt, out ListBoxItem row)) {
row.Focus();
Keyboard.Focus(row);
}
_begin_explicit_drag(true);
}
}
}
}
void _begin_drag_selection(Point pt) {
_drag_rect_adorner?.detach();
if (InputHitTest(pt) is UIElement el) {
var sb = new StringBuilderEx("beginning drag selection on ");
OneOf.MatchFirst<DependencyObject, ListBoxItem, GroupItem>(el.VisualAncestors().Prepend(el)).Match(init_common, on_group);
@@ -279,22 +407,45 @@ namespace mbts.ui.controls {
}
}
void init_common(ListBoxItem item_container) {
// drag state
_drag_start = pt;
_drag_stop = pt;
_drag_start_item = ItemContainerGenerator.ItemFromContainer(item_container);
_drag_start_index = ItemContainerGenerator.IndexFromContainer(item_container);
_drag_start_vertical_drift = _scroll_viewer.VerticalOffset;
if (!IsKeyboardFocusWithin)
Keyboard.Focus(item_container);
// drag rect
_drag_rect_adorner = new AdornerHost<Rectangle>(_content_presenter ?? (UIElement)this) {
Child = new Rectangle {
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Top,
StrokeThickness = HighDpiExtensions.pixel_unit
}
};
_set_selection_display_bindings(
_drag_rect_adorner.Child,
fill_prop: Shape.FillProperty,
fill_converter: DIYGrid.SelectionHighlightBrushConverter,
stroke_prop: Shape.StrokeProperty,
stroke_converter: ValueConverter.create<Color, Brush>(c => new SolidColorBrush(c.with(255))));
_refresh_drag_rect();
_drag_rect_adorner.attach();
// log
sb.Add($"'{_drag_start_item}' (selection mode {SelectionMode}).");
_log.info(sb.ToString());
}
}
base.OnPreviewMouseLeftButtonDown(e);
}
protected override void OnPreviewMouseUp(MouseButtonEventArgs e) {
void _end_drag(Point drag_stop) {
if (_drag_rect_adorner != null) {
_drag_rect_adorner.detach();
_drag_rect_adorner = null;
}
if (_drag_start.HasValue) {
_log.info("ending drag selection on mouse up.");
if (SelectionMode != SelectionMode.Single && _content_presenter != null) {
var drag_stop = e.GetPosition(this);
drag_stop.X = MathUtils.within_range(0, _content_presenter.RenderSize.Width - 1, drag_stop.X);
drag_stop.Y = MathUtils.within_range(0, _content_presenter.RenderSize.Height - 1, drag_stop.Y);
double vertical_diff = drag_stop.Y + (_row_height * (_scroll_viewer.VerticalOffset - _drag_start_vertical_drift)) - _drag_start.Value.Y;
@@ -356,7 +507,6 @@ namespace mbts.ui.controls {
_drag_start = null;
_drag_start_item = null;
}
base.OnPreviewMouseUp(e);
}
void _select_items_between(object inclusive_begin, object inclusive_end) {
@@ -375,6 +525,36 @@ namespace mbts.ui.controls {
.BookEnd(inclusive_begin, inclusive_end));
}
}
void _set_selection_display_bindings(DependencyObject target, DependencyProperty fill_prop = null, IValueConverter fill_converter = null, DependencyProperty stroke_prop = null, IValueConverter stroke_converter = null, bool bind_opacity = true) {
// fill
if (fill_prop != null) {
BindingOperations.SetBinding(target, fill_prop,
new Binding {
Source = _grid,
Path = new PropertyPath(DIYGrid.SelectionHighlightProperty),
Converter = fill_converter
});
}
// stroke
if (stroke_prop != null) {
BindingOperations.SetBinding(target, stroke_prop,
new Binding {
Source = _grid,
Path = new PropertyPath(DIYGrid.SelectionHighlightProperty),
Converter = stroke_converter
});
}
// opacity
if (bind_opacity) {
BindingOperations.SetBinding(target, OpacityProperty,
new Binding {
Source = this,
Path = new PropertyPath(IsSelectionActiveProperty),
Converter = ValueConverter.create<bool, double>(is_active => is_active ? 1.0 : 0.5)
});
}
}
}
partial class DIYGrid : Control {
@@ -411,55 +591,48 @@ namespace mbts.ui.controls {
// TODO: SelectionFill/Stroke should not be hardcoded, for now this serves as the central location for DragRectAdorner,
// and FrameworkElementColumns that paint the selection brush themselves
// (e.g MarketView bid/ask columns, bc their background is not transparent, it covers the ListBoxItem selection-highlight).
public static readonly SolidColorBrush DefaultSelectionHighlight = new SolidColorBrush(Colors.MediumSlateBlue) { Opacity = 0.4 };
public static readonly Pen DefaultSelectionStroke = new Pen(Brushes.MediumSlateBlue, 1);
public static readonly Color DefaultSelectionHighlight = Colors.MediumSlateBlue.with(0x63);
readonly NotifyCollectionChangedEventHandler _sort_descriptions_collection_changed_cb;
double _inactive_selection_opacity = 0.2;
DataContextHooks _data_context_hooks;
public static readonly DependencyProperty SelectionHighlightProperty =
WPFHelper.create_dp<SolidColorBrush, DIYGrid>(
nameof(SelectionHighlight),
(o, v) => {
o.Resources[ActiveSelectionHighlightKey] = v;
o._reset_inactive_selection_brush();
o.Resources[SelectionStrokeKey] = new Pen(o.SelectionHighlight.Color.to_solid_brush().frozen(), 1).frozen();
foreach (var col in o.Columns.empty_if_null().OfType<ISelectionHighlightListener>())
col.on_selection_fill(v);
var ad_layer = System.Windows.Documents.AdornerLayer.GetAdornerLayer(o);
if (ad_layer != null) {
foreach (var ad in ad_layer.descendants_of_type<ISelectionHighlightListener>()) {
ad.on_selection_fill(v);
}
}
}, (o, v) => v as SolidColorBrush ?? DefaultSelectionHighlight,
DefaultSelectionHighlight);
WPFHelper.create_dp<Color, DIYGrid>(nameof(SelectionHighlight), DefaultSelectionHighlight);
public SolidColorBrush SelectionHighlight {
get => GetValue(SelectionHighlightProperty) as SolidColorBrush;
public Color SelectionHighlight {
get => (Color)GetValue(SelectionHighlightProperty);
set => SetValue(SelectionHighlightProperty, value);
}
static DIYGrid() {
DefaultStyleKeyProperty.OverrideMetadata(typeof(DIYGrid), new FrameworkPropertyMetadata(typeof(DIYGrid)));
DefaultSelectionHighlight.Freeze();
DefaultSelectionStroke.Freeze();
UseLayoutRoundingProperty.OverrideMetadata(typeof(DIYGrid), new FrameworkPropertyMetadata(OnUseLayoutRoundingChanged));
void OnUseLayoutRoundingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) =>
d.CoerceValue(RowHeightProperty);
}
public DIYGrid() {
var sort_map = new Dictionary<string, ColumnBase>();
_sort_descriptions_collection_changed_cb = WPFHelper.create_collection_changed_cb<SortDescription>(
sd => {
if (!SyncColumnSortDirectionWithSortDescriptions)
return;
if (Columns.TryGetFirst(c => c.sort_member_path == sd.PropertyName, out ColumnBase col)) {
sort_map[sd.PropertyName] = col;
col.SortDirection = sd.Direction;
}
}, sd => {
if (!SyncColumnSortDirectionWithSortDescriptions)
return;
if (Columns.TryGetFirst(c => c.sort_member_path == sd.PropertyName, out ColumnBase col)) {
col.SortDirection = null;
sort_map.Remove(sd.PropertyName);
}
}, s => {
if (!SyncColumnSortDirectionWithSortDescriptions)
return;
sort_map.Clear();
if (Columns is IEnumerable<ColumnBase> cols) {
foreach (var c in cols)
@@ -505,7 +678,6 @@ namespace mbts.ui.controls {
}
}
}));
Resources[ActiveSelectionHighlightKey] = SelectionHighlight;
_reset_inactive_selection_brush();
_data_context_hooks = new DataContextHooks(this, DataContext);
DataContextChanged += OnDataContextChanged;
@@ -514,6 +686,21 @@ namespace mbts.ui.controls {
public event DIYGridSortingEventHandler Sorting;
public event Action<bool> SupportGroupItemsChanged;
public static IValueConverter SelectionHighlightPenConverter =>
ValueConverter.create<Color, Pen>(c => FrozenPenCache.get_pen(c.with(255)));
public static IValueConverter SelectionHighlightBrushConverter =>
ValueConverter.create<Color, Brush>(FrozenBrushCache.get_brush);
public static IValueConverter RowHoverHighlightBrushConverter =>
ValueConverter.create<Color, Brush>(c => FrozenBrushCache.get_brush(c, 0.35));
public static IValueConverter CellHoverHighlightPenConverter =>
ValueConverter.create<Color, Pen>(c => FrozenPenCache.get_pen(c.with(255)));
public static IValueConverter CellHoverHighlightBrushConverter =>
ValueConverter.create<Color, Brush>(c => FrozenBrushCache.get_brush(c, 0.5));
public double InactiveSelectionOpacity {
get => _inactive_selection_opacity;
set {
@@ -534,6 +721,8 @@ namespace mbts.ui.controls {
}
}
public bool SyncColumnSortDirectionWithSortDescriptions { get; set; } = true;
public ItemContainerGenerator RowContainerGenerator =>
_row_presenter.ItemContainerGenerator;
@@ -833,6 +1022,14 @@ namespace mbts.ui.controls {
// Public Properties
#region Public Properties
#region Column
public static readonly DependencyProperty ColumnProperty =
DependencyProperty.RegisterAttached("Column", typeof(ColumnBase), typeof(DIYGrid));
public static ColumnBase GetColumn(DependencyObject d) => d.GetValue(ColumnProperty) as ColumnBase;
public static void SetColumn(DependencyObject d, ColumnBase value) => d.SetValue(ColumnProperty, value);
#endregion
#region ColumnKey
public static readonly DependencyProperty ColumnKeyProperty =
DependencyProperty.RegisterAttached("ColumnKey",
@@ -1069,7 +1266,7 @@ namespace mbts.ui.controls {
if (v < min_row_height)
return min_row_height;
}
return v;
return o.UseLayoutRounding ? v.RoundLayoutValue() : v;
}, DefaultRowHeight);
public event Action<double> RowHeightChanged;
@@ -1090,14 +1287,11 @@ namespace mbts.ui.controls {
public static readonly DependencyProperty RowsProperty =
WPFHelper.create_dp<IEnumerable, DIYGrid>(
nameof(Rows),
(s, oldval, newval) => {
WPFHelper.manage_collection_change(
(oldval as ICollectionView)?.SortDescriptions,
(newval as ICollectionView)?.SortDescriptions,
s._sort_descriptions_collection_changed_cb,
old_ncc => s._sort_descriptions_collection_changed_cb(old_ncc, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)),
new_ncc => s._sort_descriptions_collection_changed_cb(new_ncc, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, (IList)new_ncc)));
},
(s, oldval, newval) =>
WPFHelper.manage_collection_change(
(oldval as ICollectionView)?.SortDescriptions,
(newval as ICollectionView)?.SortDescriptions,
s._sort_descriptions_collection_changed_cb),
Enumerable.Empty<object>());
public DataTemplate RowTemplate {
@@ -1146,33 +1340,30 @@ namespace mbts.ui.controls {
double frozen_offset = 0;
int vis_frozen_col_count = _get_visible_frozen_column_count();
foreach (var col in _visible_columns.Take(vis_frozen_col_count)) {
yield return new ColumnGeometry() {
column = col,
left = frozen_offset,
unclipped_left = frozen_offset,
};
double right = frozen_offset + col.ActualWidth;
right = right.RoundLayoutValue();
yield return new ColumnGeometry(col, null, frozen_offset, frozen_offset, right);
frozen_offset += col.ActualWidth;
}
// frozen_offset will be off by 2 pixels because of the GridViewHeaderRowPresenter PaddingHeader (see c_PaddingHeaderMinWidth in referencesource)
// We could use the _header_frozen.ActualWidth but due to the async nature of layout it get updated slightly too late
frozen_offset += 2;
if (_frozen_column_count > 0) {
// frozen_offset will be off by 2 pixels because of the GridViewHeaderRowPresenter PaddingHeader (see c_PaddingHeaderMinWidth in referencesource)
// We could use the _header_frozen.ActualWidth but due to the async nature of layout it get updated slightly too late
frozen_offset += 2;
}
// Adjust by scroll amount
double offset = frozen_offset - (_scroll_horizontal.Value);
double offset = frozen_offset - _scroll_horizontal.Value;
offset = offset.RoundLayoutValue();
int count = 0;
foreach (var col in _visible_columns.Skip(vis_frozen_col_count)) {
double left = Math.Max(offset, frozen_offset);
double right = offset + col.ActualWidth;
right = right.RoundLayoutValue();
// If right < frozen_offset column is completely obscured
if (include_obscured_cols || (right > frozen_offset && left <= ActualWidth)) {
// fully visible
yield return new ColumnGeometry() {
column = col,
super_column = _superheader_column_index.ElementAtOrDefault(count),
unclipped_left = offset,
left = left
};
yield return new ColumnGeometry(col, _superheader_column_index.ElementAtOrDefault(count), left, offset, right);
}
offset += col.ActualWidth;
offset = right;
++count;
}
}
@@ -1419,19 +1610,201 @@ namespace mbts.ui.controls {
public static IObservable<T> SubscribeMouseHoverRow<T>(this DIYGrid grid, IEqualityComparer<T> comparer) where T : class =>
Observable.Create<T>(o => {
T curr_row = null;
T last_row = null;
return grid.SubscribeMouseHoverRowContainer().Subscribe(row_view => {
if (grid.RowContainerGenerator.ItemFromContainer(row_view) is T row && !comparer.Equals(row, last_row))
o.OnNext(last_row = row);
});
});
public static IObservable<ListBoxItem> SubscribeMouseHoverRowContainer(this DIYGrid grid) =>
Observable.Create<ListBoxItem>(o => {
bool busy = false, dirty = false;
ListBoxItem curr_row = null;
return Disposable.Create(
grid.SubscribeMouseEnter(on_mouse_enter),
grid.SubscribeMouseLeave(on_mouse_leave),
grid.SubscribePreviewMouseMove(on_mouse_move));
void on_mouse_enter(object sender, MouseEventArgs e) => refresh();
void on_mouse_move(object sender, MouseEventArgs e) => refresh();
void on_mouse_leave(object sender, MouseEventArgs e) => refresh();
grid.SubscribeMouseEnter(on_mouse),
grid.SubscribeMouseLeave(on_mouse),
grid.SubscribeMouseLeftButtonDown(on_mouse),
grid.SubscribeMouseLeftButtonUp(on_mouse),
grid.SubscribeMouseMove(on_mouse));
void on_mouse(object sender, MouseEventArgs e) => refresh();
void refresh() {
T row = grid.ChildAtPoint<ListBoxItem>(Mouse.GetPosition(grid)) is var rview && rview != null ? grid.RowContainerGenerator.ItemFromContainer(rview) as T : null;
if (!comparer.Equals(curr_row, row))
o.OnNext(curr_row = row);
if (busy) {
dirty = true;
} else {
dirty = false;
busy = true;
do {
grid.Dispatcher.InvokeAsync(() => {
var row = grid.ChildAtPoint<ListBoxItem>(Mouse.GetPosition(grid));
if (row != curr_row)
o.OnNext(curr_row = row);
dirty = false;
});
} while (dirty);
busy = false;
}
}
});
public static IObservable<DIYGrid.CellGeometry?> SubscribeMouseHoverCell(this DIYGrid grid) {
return Observable.Create<DIYGrid.CellGeometry?>(o => {
IDisposable row_mouse_hooks = null;
DIYGrid.CellGeometry? last_value = null;
var layout = grid.GetColumnGeometry().ToList();
var layout_x_pts = layout.WrapSelector(cg => cg.left, null);
ListBoxItem hover_row = null;
grid.ColumnLayoutChanged += on_layout;
var hover_sub = grid.SubscribeMouseHoverRowContainer().Subscribe(on_row_view);
return Disposable.Create(dispose);
//
void notify(MouseEventArgs e = null) {
if (hover_row == null || layout.Count == 0)
on_cell(null);
else {
var pt = e?.GetPosition(hover_row) ?? Mouse.GetPosition(hover_row);
int index = CollectionHelper.binary_search(layout_x_pts, pt.X);
if (index < 0)
index = ~index - 1;
if (index >= 0 && index < layout.Count) {
on_cell(new DIYGrid.CellGeometry(layout[index], hover_row));
} else {
on_cell(null);
}
}
}
void on_cell(DIYGrid.CellGeometry? cell) {
if (!cell.Equals(last_value))
o.OnNext(last_value = cell);
}
void on_row_view(ListBoxItem row_view) {
row_mouse_hooks?.Dispose();
hover_row = row_view;
row_mouse_hooks = row_view?.SubscribeMouseMove(on_mouse_change);
notify();
}
void on_layout(List<DIYGrid.ColumnGeometry> obj) {
layout = grid.GetColumnGeometry().ToList();
layout_x_pts = layout.WrapSelector(cg => cg.left, null);
notify();
}
void on_mouse_change(object sender, MouseEventArgs e) =>
notify(e);
void dispose() {
Disposable.dispose(ref hover_sub);
grid.ColumnLayoutChanged -= on_layout;
}
});
}
public static IObservable<(IEnumerable<DIYGrid.ColumnGeometry> columns, int row_start, int row_count)?> SubscribeCellDragRect(this DIYGrid grid) {
return Observable.Create<(IEnumerable<DIYGrid.ColumnGeometry> columns, int row_start, int row_count)?>(o => {
return grid.ConnectOnLoaded(init);
IDisposable init() {
if (!( grid.try_find_descendent_of_type(out DIYGridRowsPresenter presenter) && presenter.try_find_descendent_of_type(out ScrollViewer scroll) && scroll.try_find_descendent_of_type(out ScrollContentPresenter scroll_content)))
return null;
// init state
var value_container = new DistinctSubject<(IEnumerable<DIYGrid.ColumnGeometry> columns, int row_start, int row_count)?>();
double y_start_drift = 0;
var layout = new List<DIYGrid.ColumnGeometry>();
var layout_x_pts = layout.WrapSelector(cg => cg.left, null);
object _drag_start_item = null;
int _drag_start_index = -1;
Point? drag_start = null, drag_curr = null;
//
scroll.ScrollChanged += on_scroll;
grid.ColumnLayoutChanged += on_layout;
var drag_hook = DragDropHelper.subscribe_drag_coordinates(presenter, grid, use_preview_events: false, mouse_button: MouseButton.Right).Subscribe(pt => {
if (drag_start.HasValue) {
if (!pt.HasValue) {
drag_start = null;
drag_curr = null;
} else {
drag_curr = pt;
}
} else {
if (pt.HasValue) {
var item_container = presenter.ChildAtPoint<ListBoxItem>(pt.Value);
if (item_container == null) {
pt = null;
} else {
_drag_start_item = grid.RowContainerGenerator.ItemFromContainer(item_container);
_drag_start_index = grid.RowContainerGenerator.IndexFromContainer(item_container);
y_start_drift = scroll.VerticalOffset;
}
}
drag_start = pt;
drag_curr = pt;
}
refresh();
});
grid.PreviewMouseUp += (s, e) => e.Handled = e.ChangedButton == MouseButton.Right;
var sub = value_container.Subscribe(o);
return Disposable.Create(dispose);
void on_layout(List<DIYGrid.ColumnGeometry> new_layout) {
layout = new_layout;
layout_x_pts = layout.WrapSelector(cg => cg.left, null);
}
void on_scroll(object sender, ScrollChangedEventArgs e) {
if (drag_start.HasValue)
refresh();
}
void refresh() {
if (!drag_start.HasValue) {
value_container.OnNext(null);
return;
}
var (xstart, ystart) = drag_start.Value.deconstruct();
var (xstop, ystop) = drag_curr.Value.deconstruct();
xstart = MathUtils.within_range(0, presenter.ActualWidth, xstart);
var row_height = grid.RowHeight;
xstop = MathUtils.within_range(0, scroll_content.RenderSize.Width - 1, xstop);
ystop = MathUtils.within_range(0, scroll_content.RenderSize.Height - 1, ystop);
double vertical_diff = ystop + (row_height * (scroll.VerticalOffset - y_start_drift)) - ystart;
int start_index, n_items;
if (vertical_diff > 0) {
double first_item_partial_height = row_height - (ystart % row_height);
n_items = (int)Math.Ceiling((vertical_diff - first_item_partial_height) / row_height) + 1;
start_index = _drag_start_index;
int overflow = start_index + n_items - presenter.Items.Count;
if (overflow > 0)
n_items -= overflow;
} else {
double first_item_partial_height = ystart % row_height;
n_items = -(int)Math.Floor((vertical_diff + first_item_partial_height) / row_height) + 1;
start_index = _drag_start_index - n_items + 1;
if (start_index < 0) {
n_items += start_index;
start_index = 0;
}
}
if (n_items > 0) {
var colstart = find_col(xstart);
var colstop = find_col(xstop);
(colstart, colstop) = MathUtils.get_outliers(colstart, colstop);
(ystart, ystop) = MathUtils.get_outliers(ystart, ystop);
var cols = layout.WrapSubRange(colstart, colstop - colstart + 1);
value_container.OnNext((cols, start_index, n_items));
}
}
int find_col(double x) {
if (layout.Count == 0)
return -1;
int i = CollectionHelper.binary_search(layout, x, l => l.left);
if (i < 0) {
i = ~i - 1;
}
return MathUtils.within_range(0, layout.Count - 1, i);
}
void dispose() {
Disposable.dispose(ref sub);
grid.ColumnLayoutChanged -= on_layout;
}
}
});
}
}
}
@@ -0,0 +1,125 @@
using bts.utils;
using mbts.ui.controls;
using mbts.ui.dpi;
using mbts.ui.extensions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Media;
namespace mbts.ui.spark {
class DIYGridPopupManager {
readonly DIYGrid _grid;
readonly FrameworkElement _owner;
readonly ScrollContentPresenter _scroll_content;
readonly ICollection<Visual> _children;
Popup _active_popup;
public static DIYGridPopupManager create(FrameworkElement owner) =>
create(owner, new VisualCollectionWrapper(new VisualCollection(owner)));
public static DIYGridPopupManager create(FrameworkElement owner, ICollection<Visual> visual_children) =>
owner.try_find_descendent_of_type(out DIYGrid grid) &&
grid.try_find_descendent_of_type(out DIYGridRowsPresenter rows_presenter) &&
rows_presenter.try_find_descendent_of_type(out ScrollContentPresenter scroll_content) ?
new DIYGridPopupManager(owner, grid, scroll_content, visual_children) :
null;
DIYGridPopupManager(FrameworkElement owner, DIYGrid grid, ScrollContentPresenter scroll_content, ICollection<Visual> children) {
_owner = owner ?? throw new ArgumentNullException(nameof(owner));
_grid = grid ?? throw new ArgumentNullException(nameof(grid));
_scroll_content = scroll_content ?? throw new ArgumentNullException(nameof(scroll_content));
_children = children;
}
public Popup active_popup {
get => _active_popup;
set {
if (_active_popup != value) {
_active_popup = value;
if (_active_popup != null) {
_active_popup.IsOpen = false;
_active_popup.IsOpen = true;
}
}
}
}
public async void show(FrameworkElement view, int delay_measure_ms = 100) {
var layout = _grid.GetColumnGeometry().ToList();
if (layout.Count == 0)
return;
var x_origin = layout[0].unclipped_left;
var mouse_pos = Mouse.GetPosition(_scroll_content);
var col_index = CollectionHelper.binary_search(layout, mouse_pos.X + x_origin, cg => cg.left);
if (col_index < 0)
col_index = ~col_index - 1;
col_index = MathUtils.within_range(0, layout.Count - 1, col_index);
var col = layout[col_index];
// determine top left
var top_left = _scroll_content.PointToScreenPixels(
col.unclipped_left - x_origin,
Math.Ceiling(mouse_pos.Y / _grid.RowHeight) * _grid.RowHeight);
if (delay_measure_ms > 0)
await Task.Delay(delay_measure_ms);
view.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
// popup
var popup = new ZPopup {
PlacementTarget = _grid,
Placement = PlacementMode.Absolute,
Child = view,
IsOpen = true,
HorizontalOffset = top_left.X,
VerticalOffset = top_left.Y,
StaysOpen = false
};
popup.Child.IsMouseCaptureWithinChanged += (s, e) => {
if (!popup.StaysOpen) {
var pt = Mouse.GetPosition(popup.Child);
if (!popup.Child.IsWithinBounds(pt)) {
popup.IsOpen = false;
if (_grid.try_find_descendent_of_type(out DIYGridRowsPresenter pres))
pres.CaptureMouse();
}
}
};
popup.SetBinding(WindowStyle.UnsafeWindowProperty, new Binding {
Source = _owner,
Path = new PropertyPath(WindowStyle.UnsafeWindowProperty)
});
view.InputBindings.Add(new KeyBinding(ApplicationCommands.Close, Key.Escape, ModifierKeys.None));
view.CommandBindings.Add(new CommandBinding(ApplicationCommands.Close, (s, e) => popup.IsOpen = false));
view.PreviewMouseDown += on_view_preview_mouse_down;
var drag_hooks = PopupViewController.enable_drag_move(_owner, popup);
_active_popup = popup;
_children.Add(popup);
_owner.ReleaseMouseCapture();
popup.Closed += on_closed;
FocusManager.SetIsFocusScope(view, true);
view.Focus();
//
void on_closed(object sender, EventArgs e) {
if (popup == _active_popup) {
return;
}
view.PreviewMouseDown -= on_view_preview_mouse_down;
Disposable.dispose(ref drag_hooks);
popup.ReleaseMouseCapture();
if (Mouse.Captured == _owner)
_owner.ReleaseMouseCapture();
popup.Closed -= on_closed;
_children.Remove(popup);
}
void on_view_preview_mouse_down(object sender, MouseButtonEventArgs e) =>
active_popup = popup;
}
}
}
@@ -55,6 +55,7 @@
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Focusable" Value="False"/>
<Setter Property="SelectionMode" Value="Extended"/>
<Setter Property="UseLayoutRounding" Value="True"/>
<Setter Property="ColumnHeaderStyle">
<Setter.Value>
<Style TargetType="GridViewColumnHeader">
@@ -1,10 +1,9 @@
using System;
using System.Linq;
namespace mbts.ui.controls
{
internal class DoubleUtil
{
namespace mbts.ui.controls {
static class DoubleUtil {
public const int API_DECIMAL_PRECISION = 9;
/* smallest such that 1.0+DBL_EPSILON != 1.0 */
private const double EPSILON = 2.2204460492503131e-016;
private const double BIG_EPSILON = EPSILON * 10;
@@ -36,6 +35,9 @@ namespace mbts.ui.controls
(double.IsNaN(right) ? double.NaN : -right) :
(double.IsNaN(right) ? left : left - right);
public static decimal round_to_decimal(double value, int decimals = API_DECIMAL_PRECISION) =>
double.IsNaN(value) ? 0 : (decimal)Math.Round(value, decimals, MidpointRounding.AwayFromZero);
internal static bool AreClose(double v1, double v2) {
if (v1 == v2) return true;
double delta = Math.Abs(v1 - v2);
@@ -26,12 +26,12 @@ namespace mbts.ui.controls {
}
}
public class HighPerformanceRow<TRowVM, TColKey> : HighPerformanceRowBase where TRowVM : class, IRowVM {
public class HighPerformanceRow<TRowVM> : HighPerformanceRowBase where TRowVM : class, IRowVM {
double _row_height;
ObservableCollection<ColumnBase> _columns;
List<ColumnGeometry> _column_layout = new List<ColumnGeometry>();
Dictionary<TColKey, ColumnGeometry> _column_layout_map = new Dictionary<TColKey, ColumnGeometry>();
VisualDictionary<TColKey, FrameworkElement> _element_cells;
Dictionary<string, ColumnGeometry> _column_layout_map = new Dictionary<string, ColumnGeometry>();
VisualDictionary<string, FrameworkElement> _element_cells;
RecycleBin<DIYGrid> _grid_backing;
RecycleBin<ListBoxItem> _container_backing;
TRowVM _row;
@@ -39,7 +39,7 @@ namespace mbts.ui.controls {
public HighPerformanceRow() : this(DefaultRowHeight) { }
public HighPerformanceRow(double row_height) {
_row_height = row_height;
_element_cells = new VisualDictionary<TColKey, FrameworkElement>(this);
_element_cells = new VisualDictionary<string, FrameworkElement>(this);
_grid_backing = new RecycleBin<DIYGrid>(
g => { g.ColumnLayoutChanged += _on_column_layout_changed; g.RowHeightChanged += _on_row_height_changed; },
g => { g.ColumnLayoutChanged -= _on_column_layout_changed; g.RowHeightChanged -= _on_row_height_changed; });
@@ -123,7 +123,7 @@ namespace mbts.ui.controls {
protected override Size ArrangeOverride(Size finalSize) {
foreach (var kv in _column_layout_map) {
if (_element_cells.TryGetValue(kv.Key, out FrameworkElement ec))
ec.Arrange(new Rect(new Point(kv.Value.left, 0), new Size(kv.Value.width, _row_height)));
ec.Arrange(kv.Value.to_rect(_row_height));
}
return finalSize;
}
@@ -148,16 +148,18 @@ namespace mbts.ui.controls {
}
void _add_columns(IList items) {
foreach (var col in items.OfType<FrameworkElementColumnBase<TColKey>>()) {
var cell_view = col.view_factory();
CellRender.change_position(cell_view, _row_height, _column_layout_map.GetNullable(col.key));
_element_cells.Add(col.key, cell_view);
foreach (ColumnBase col in items) {
if (col is IFrameworkElementColumn el_col) {
var cell_view = el_col.view_factory();
CellRender.change_position(cell_view, _row_height, _column_layout_map.GetNullable(col.id));
_element_cells.Add(col.id, cell_view);
}
}
}
void _remove_columns(IList items) {
foreach (var col in items.OfType<FrameworkElementColumnBase<TColKey>>())
_element_cells.Remove(col.key);
foreach (ColumnBase col in items)
_element_cells.Remove(col.id);
}
void _on_column_layout_changed(List<ColumnGeometry> column_layout) {
@@ -171,10 +173,10 @@ namespace mbts.ui.controls {
}
void _set_column_layout_map() {
_column_layout_map = new Dictionary<TColKey, ColumnGeometry>();
_column_layout_map = new Dictionary<string, ColumnGeometry>();
foreach (var cg in _column_layout) {
if (cg.column is FrameworkElementColumnBase<TColKey> elc)
_column_layout_map.Add(elc.key, cg);
if (cg.column is IFrameworkElementColumn)
_column_layout_map.Add(cg.column.id, cg);
}
}
#endregion
@@ -53,11 +53,7 @@ namespace mbts.ui.controls {
if (col.is_partially_obscured) {
var obscured_width = col.left - col.unclipped_left;
grid.scroll_horizontal(-obscured_width);
return new ColumnGeometry() {
column = col.column,
left = col.left,
unclipped_left = col.left
};
return new ColumnGeometry(col.column, col.super_column, col.left, col.left, col.right);
}
return col;
}
@@ -1,6 +1,5 @@
using bts.proto;
using bts.utils;
using mbts.ui.controls;
using mbts.ui.converters;
using ProtoBuf;
using System;
@@ -24,7 +23,7 @@ namespace mbts.ui.settings {
public static GridSettings Default =>
new GridSettings(
selection_highlight: new ColorOpacityProto(FallbackValues.selection_highlight.Color, FallbackValues.selection_highlight.Opacity),
selection_highlight: FallbackValues.selection_highlight.rgb_color(),
background: _default_background_color, //new ColorOpacityProto(FallbackValues.background_color.Color, FallbackValues.background_color.Opacity));
enable_background_color: _default_enable_background_color);
@@ -45,11 +44,10 @@ namespace mbts.ui.settings {
public static double font_size => _default_font_size;
public static Color font_color => _default_font_color.rgb_color();
public static FontWeight font_weight => FontWeight.FromOpenTypeWeight(_default_font_weight);
public static SolidColorBrush selection_highlight => DIYGrid.DefaultSelectionHighlight;
public static Color selection_highlight => Colors.MediumSlateBlue.with(0x63);
public static uint background_color => _default_background;
}
public static IValueConverter selection_highlight_converter => ValueConverter.create<GridSettings, SolidColorBrush>(gs => gs.selection_highlight.to_brush().frozen());
public static IValueConverter font_family_converter => ValueConverter.create<GridSettings, FontFamily>(gs => new FontFamily(gs.font_family));
public static IValueConverter font_weight_converter => ValueConverter.create<GridSettings, FontWeight>(gs => gs.font_weight);
public static IValueConverter font_size_converter => ValueConverter.create<GridSettings, double>(gs => gs.font_size);
@@ -71,8 +69,29 @@ namespace mbts.ui.settings {
[DefaultValue(_default_font_color)]
readonly uint _font_color;
// selection highlight
ColorOpacityProto _obsolete_selection_highlight_backing;
uint _selection_highlight_backing;
[ProtoMember(5)]
readonly ColorOpacityProto _selection_highlight;
ColorOpacityProto _obsolete_selection_highlight {
get => _obsolete_selection_highlight_backing;
set {
_obsolete_selection_highlight_backing = value;
// only overwrite non-obsolete selection highlight if it is 0 (assume that means 'unassigned').
if (_selection_highlight_backing == 0) {
var color = value.argb_color;
// ensure reasonable opacity
color.A = MathUtils.within_range(color.A,
GridSettingsMigrationRules.SelectionHighlightIsTranslucent.min_alpha,
GridSettingsMigrationRules.SelectionHighlightIsTranslucent.max_alpha);
_selection_highlight_backing = color.rgb_color();
}
}
}
[ProtoMember(10)]
uint _selection_highlight {
get => _selection_highlight_backing;
set => _selection_highlight_backing = value;
}
// columns
[ProtoMember(6)]
List<GridColumnSettings> _columns;
@@ -101,7 +120,7 @@ namespace mbts.ui.settings {
double font_size = _default_font_size,
FontWeight font_weight = default,
uint font_color = _default_font_color,
ColorOpacityProto? selection_highlight = null,
uint selection_highlight = 0,
IEnumerable<GridColumnSettings> columns = null,
IEnumerable<SortingCriteria> sorting_criterion = null,
uint background = _default_background,
@@ -111,7 +130,7 @@ namespace mbts.ui.settings {
_font_size = font_size;
_font_color = font_color;
_font_weight = (ushort)font_weight.ToOpenTypeWeight();
_selection_highlight = selection_highlight ?? new ColorOpacityProto(0, 1);
_selection_highlight = selection_highlight;
_columns = new List<GridColumnSettings>(columns.empty_if_null());
_sorting_criterion = new List<SortingCriteria>(sorting_criterion.empty_if_null());
_background = background;
@@ -123,29 +142,27 @@ namespace mbts.ui.settings {
public uint font_color => _font_color;
public FontWeight font_weight => FontWeight.FromOpenTypeWeight(_font_weight);
public Color background => _background.argb_color();
public ColorOpacityProto selection_highlight => _selection_highlight;
public Color selection_highlight => _selection_highlight.argb_color();
public uint background_color => _background;
public bool enable_background_color => _enable_background_color;
public IReadOnlyList<GridColumnSettings> columns => _columns ?? (_columns = new List<GridColumnSettings>(0));
[Obsolete] // TODO: remove sorting_criterion from GridSettings entirely. This should be part of IShellWindow's StartupData, since it is an instance-specific setting, and prob not part of the shared defaults
public IReadOnlyList<SortingCriteria> sorting_criterion => _sorting_criterion ?? (_sorting_criterion = new List<SortingCriteria>(0));
public static GridSettings get_default() =>
new GridSettings(_default_font_family, _default_font_size, FontWeights.Normal, _default_font_color, new ColorOpacityProto(Colors.MediumSlateBlue, 0.4));
public GridSettings with(
IEnumerable<GridColumnSettings> columns = null,
IEnumerable<SortingCriteria> sorting_criterion = null,
string font_family = null,
double font_size = double.NaN,
uint? background = null,
bool? enable_background_color = null) =>
bool? enable_background_color = null,
uint? selection_highlight = null) =>
new GridSettings(
font_family ?? _font_family,
DoubleHelper.IsNaNOrInfinityOr(font_size, 0) ? _font_size : font_size,
font_weight,
_font_color,
_selection_highlight,
selection_highlight ?? _selection_highlight,
columns ?? _columns,
sorting_criterion ?? _sorting_criterion,
background ?? _background,
@@ -1,4 +1,6 @@
using bts.collections;
using bts.settings;
using bts.utils;
using mbts.ui.controls;
using System;
using System.Collections.Generic;
@@ -116,4 +118,26 @@ namespace mbts.ui.settings {
return false;
}
}
static class GridSettingsMigrationRules {
public class SelectionHighlightIsTranslucent : ISettingsMigrationRule<GridSettings> {
public const byte min_alpha = 30, max_alpha = 200;
public static bool apply(GridSettings settings, out GridSettings new_settings) {
if (settings != null) {
var sl = settings.selection_highlight;
var a = MathUtils.within_range(sl.A, (byte)30, (byte)200);
if (sl.A != a) {
sl.A = a;
new_settings = settings.with(selection_highlight: sl.rgb_color());
return true;
}
}
new_settings = null;
return false;
}
bool ISettingsMigrationRule<GridSettings>.apply(GridSettings settings, out GridSettings new_settings) =>
apply(settings, out new_settings);
}
}
}
@@ -30,7 +30,8 @@ namespace mbts.ui {
readonly DelegateProperty<FontFamily> _font_family;
readonly DelegateProperty<FontWeight> _font_weight;
readonly DelegateProperty<double> _font_size;
readonly DelegateProperty<SolidColorBrush> _font_color, _selection_highlight, _background_color;
readonly DelegateProperty<SolidColorBrush> _font_color, _background_color;
readonly DelegateProperty<Color> _selection_highlight;
readonly SetMap<IPredicate<TRowData>, AnimationDefinitionWrapper<TRow>> _animation_defs = new SetMap<IPredicate<TRowData>, AnimationDefinitionWrapper<TRow>>();
readonly Func<TPredicateData> _predicate_data_factory;
readonly Func<TPredicateData, IPredicate<TRowData>> _predicate_factory;
@@ -76,7 +77,7 @@ namespace mbts.ui {
_font_weight = backing(nameof(font_weight), () => grid_settings.font_weight, nameof(grid_settings));
_font_size = backing(nameof(font_size), () => grid_settings.font_size, nameof(font_size));
_font_color = backing(nameof(font_color), () => grid_settings.font_color.rgb_solid_brush().frozen(), nameof(grid_settings));
_selection_highlight = backing(nameof(selection_highlight), () => grid_settings.selection_highlight.to_brush().frozen(), nameof(grid_settings));
_selection_highlight = backing(nameof(selection_highlight), () => grid_settings.selection_highlight, nameof(grid_settings));
_background_color = backing(nameof(background_color), () => grid_settings.background_color.argb_color().to_solid_brush().frozen(), nameof(grid_settings));
}
@@ -88,7 +89,7 @@ namespace mbts.ui {
public FontWeight font_weight => _font_weight.Value;
public double font_size => _font_size.Value;
public SolidColorBrush font_color => _font_color.Value;
public SolidColorBrush selection_highlight => _selection_highlight.Value;
public Color selection_highlight => _selection_highlight.Value;
public SolidColorBrush background_color => _background_color.Value;
public GridSettingsVM<TRowData, TPredicateData, TSettingsData> create_vm(GridSettings settings) {
@@ -134,7 +134,7 @@ namespace mbts.spark {
foreach (IGrouping<TPredicateData, CellAnimation> g in animations)
animation_map.AddOrUpdate(g.Key, p => animation_tuple(c: g), (p, t) => t.cell_animations.AddRange(g));
}
return _create_settings_obj(new GridSettings(font_family.name(), font_size, font_weight, font_color.color.rgb_color(), selection_highlight.to_proto(), col_settings,
return _create_settings_obj(new GridSettings(font_family.name(), font_size, font_weight, font_color.color.rgb_color(), selection_highlight.color.rgb_color(), col_settings,
background: background_color.color.with((byte)(MathUtils.within_range(0, 1, background_color.opacity) * 255.0)).rgb_color()),
animation_map.Select(kv => PredicateAnimations.create_data<TRowData, TPredicateData>(kv.Key, kv.Value.cell_animations, kv.Value.row_animations, kv.Value.sounds)));
//
@@ -146,7 +146,7 @@ namespace mbts.spark {
_font_family.set(new FontFamily(sd.general.font_family));
_font_weight.set(sd.general.font_weight);
font_color.load_from(sd.general.font_color.rgb_solid_brush());
selection_highlight.load_from(sd.general.selection_highlight.to_brush());
selection_highlight.color = sd.general.selection_highlight;
_show_background_color.set(sd.general.enable_background_color);
if (sd.general.enable_background_color) {
// note that when the OB is moved to allow background colors
@@ -20,7 +20,8 @@ namespace mbts.ui {
readonly DelegateProperty<FontFamily> _font_family;
readonly DelegateProperty<FontWeight> _font_weight;
readonly DelegateProperty<double> _font_size;
readonly DelegateProperty<SolidColorBrush> _font_color, _selection_highlight, _background_color;
readonly DelegateProperty<SolidColorBrush> _font_color, _background_color;
readonly DelegateProperty<Color> _selection_highlight;
readonly IReadOnlyDictionary<string, ImmutableColumnSettingStore> _column_flags;
public SimpleGridSettingsManager(ObservableCollection<ColumnBase> columns)
@@ -34,7 +35,7 @@ namespace mbts.ui {
_font_weight = backing(nameof(font_weight), () => grid_settings.font_weight, nameof(grid_settings));
_font_size = backing(nameof(font_size), () => grid_settings.font_size, nameof(font_size));
_font_color = backing(nameof(font_color), () => grid_settings.font_color.rgb_solid_brush().frozen(), nameof(grid_settings));
_selection_highlight = backing(nameof(selection_highlight), () => grid_settings.selection_highlight.to_brush().frozen(), nameof(grid_settings));
_selection_highlight = backing(nameof(selection_highlight), () => grid_settings.selection_highlight, nameof(grid_settings));
_background_color = backing(nameof(background_color), () => grid_settings.background_color.argb_color().to_solid_brush().frozen(), nameof(grid_settings));
_row_vms = row_vms;
}
@@ -44,8 +45,8 @@ namespace mbts.ui {
public FontWeight font_weight => _font_weight.Value;
public double font_size => _font_size.Value;
public SolidColorBrush font_color => _font_color.Value;
public SolidColorBrush selection_highlight => _selection_highlight.Value;
public SolidColorBrush background_color => _background_color.Value;
public Color selection_highlight => _selection_highlight.Value;
public SimpleGridSettingsVM create_vm(GridSettings settings) {
var vm = new SimpleGridSettingsVM(_column_flags.ToDictionary(kv => kv.Key, kv => kv.Value.flags));
Loaded 100 of 321 files, more files were not shown because too many files have changed in this diff. Show more