Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0140aadaf | ||
|
|
276e9ad0ef | ||
|
|
5968ac10d2 | ||
|
|
9bf7666c30 | ||
|
|
b9fd0e2e28 | ||
|
|
ec1d925dbe | ||
|
|
35a69f42bd | ||
|
|
633ebf38a2 | ||
|
|
1eb8352b6b | ||
|
|
33c201b395 | ||
|
|
0c9a9fcb5e | ||
|
|
6c6fb74506 | ||
|
|
a69ed30e70 | ||
|
|
a654f302a2 | ||
|
|
9ca3285888 | ||
|
|
df4c3b9376 | ||
|
|
f21c9f96d1 | ||
|
|
16a71a9f63 | ||
|
|
a8689aee06 | ||
|
|
85b2d257a7 | ||
|
|
f49ca9f7bd | ||
|
|
6aae7831bc | ||
|
|
297233eae6 | ||
|
|
69dcbdd348 | ||
|
|
0114dd0276 | ||
|
|
ffd35aebac | ||
|
|
b92fed4f47 | ||
|
|
fd6eedda87 | ||
|
|
1ef2b2f55d | ||
|
|
e2cd1ec2f3 | ||
|
|
110a5f74de | ||
|
|
6d20c782ca | ||
|
|
fa88ee6f68 | ||
|
|
22ef965840 | ||
|
|
09a19b702c | ||
|
|
c90915fb1c | ||
|
|
0fc2a55378 | ||
|
|
38b466d305 | ||
|
|
0052c88804 | ||
|
|
6d183e0db9 | ||
|
|
ccbf1e1323 | ||
|
|
c3e43beada | ||
|
|
ab44da6a96 | ||
|
|
dbad27e97d | ||
|
|
10656f7071 | ||
|
|
ed00025a01 | ||
|
|
7d076437a5 | ||
|
|
b7ee9aa208 | ||
|
|
601b989e75 | ||
|
|
02c8714cfd | ||
|
|
e25514b75e | ||
|
|
0f05a45d0d | ||
|
|
8107b058b0 | ||
|
|
7d3efdb220 | ||
|
|
e335c54f20 | ||
|
|
5b748feb5c | ||
|
|
a1b9bad321 | ||
|
|
e6ef795de7 | ||
|
|
aa88a0906d | ||
|
|
4b75aa6080 | ||
|
|
7b5155f599 | ||
|
|
657973c2d4 | ||
|
|
c509669de9 | ||
|
|
d79378aa28 | ||
|
|
5799d5ed1e | ||
|
|
05a7f9fa72 | ||
|
|
fef0143ee9 | ||
|
|
ece8a12f4a | ||
|
|
6758c94adf | ||
|
|
8930f59cd6 | ||
|
|
967f789114 | ||
|
|
d1210b06cc | ||
|
|
caeda6c12a | ||
|
|
b6bf02045e | ||
|
|
156561c976 | ||
|
|
6a433b8cef | ||
|
|
1d9f0de409 | ||
|
|
66b06d7190 | ||
|
|
9c160f5e41 | ||
|
|
19f4bfb6ab | ||
|
|
a06dc98b98 | ||
|
|
95b6382a3a | ||
|
|
419e8550c1 | ||
|
|
e29c6a1a22 | ||
|
|
daceee2af9 | ||
|
|
4594282f2a | ||
|
|
58ca003c94 | ||
|
|
70c0d422c8 | ||
|
|
a04871f15f | ||
|
|
f9b67c7ee4 | ||
|
|
69fdec2866 | ||
|
|
269b08e119 | ||
|
|
7eb8f32ff7 | ||
|
|
3bb473f62d | ||
|
|
10fbf30ac4 | ||
|
|
bb0aedeb0d | ||
|
|
e01c7f5372 | ||
|
|
d6cb22bdf8 | ||
|
|
adcb5502b0 | ||
|
|
d6ff13dd00 | ||
|
|
1124829aad | ||
|
|
f9d98787a0 | ||
|
|
5f52d81194 | ||
|
|
792a4bcace | ||
|
|
f012f4d70b | ||
|
|
af7897f9f3 | ||
|
|
0defcce179 | ||
|
|
b69035acf7 | ||
|
|
8e33343f9c | ||
|
|
5f718750a3 | ||
|
|
935227cfa2 | ||
|
|
940705a066 | ||
|
|
d2bfcf7603 | ||
|
|
ae4e6c2221 | ||
|
|
2a85289c6a | ||
|
|
0047890f9d | ||
|
|
dab3b3cd80 | ||
|
|
593533736b | ||
|
|
eb29368b80 | ||
|
|
b93203e4b6 | ||
|
|
f87c04a835 | ||
|
|
a63657e0d5 | ||
|
|
b702d8dc9e | ||
|
|
b0e5a4929f | ||
|
|
edf9fd268f | ||
|
|
9f1bae7d6f | ||
|
|
52a35a56de | ||
|
|
59338635f1 | ||
|
|
0f3c0c2f24 | ||
|
|
fa76cad459 | ||
|
|
b98d51cc55 | ||
|
|
1c10149a2c | ||
|
|
2ab063d5cd | ||
|
|
030cf8a938 | ||
|
|
2b8815c90d | ||
|
|
a7ee3b28ff | ||
|
|
06af24666b | ||
|
|
6bc5b35295 | ||
|
|
562bb0423a | ||
|
|
122a2c6f53 | ||
|
|
dbf6bc1bf8 | ||
|
|
569bcee7e7 | ||
|
|
8e471af048 | ||
|
|
f7c8ceb88d | ||
|
|
48fca3a9cc | ||
|
|
adce1a40a8 | ||
|
|
6f88e7585b | ||
|
|
7b0fd23b5b | ||
|
|
e433de5241 | ||
|
|
8e1e96d9f8 | ||
|
|
ff9c9e2d64 | ||
|
|
23d4e7f78d | ||
|
|
8e2092c3e2 | ||
|
|
51584e5e0f | ||
|
|
cb46e853ee | ||
|
|
9e2ad000d4 | ||
|
|
6e3e4e1385 | ||
|
|
0864925031 | ||
|
|
d122d5704b | ||
|
|
8a1a75098e | ||
|
|
7063035e7b | ||
|
|
0db69f1b1b | ||
|
|
efa07fe96e | ||
|
|
a36f9278d5 | ||
|
|
727f39b6a6 | ||
|
|
d0c978b71c | ||
|
|
6e9d164d32 | ||
|
|
50a96fc22a | ||
|
|
c6499976a9 | ||
|
|
4b8ca8d97e | ||
|
|
7e77e510fc | ||
|
|
bd24381986 | ||
|
|
5830eee68d | ||
|
|
f3928f58dd | ||
|
|
72d57b242e | ||
|
|
9a6e43e4a4 | ||
|
|
ffb443fcb2 | ||
|
|
f22e68f4bc | ||
|
|
09f08f0a57 | ||
|
|
53ca6aade8 | ||
|
|
b968ebe492 | ||
|
|
bbef942f86 | ||
|
|
7859b7923e | ||
|
|
8202b51cb9 | ||
|
|
ddd41ab317 | ||
|
|
d175a53ef8 | ||
|
|
904e238a78 | ||
|
|
0fb1425766 | ||
|
|
2104fd5ad8 | ||
|
|
26a58e82bf | ||
|
|
01e96c8241 | ||
|
|
ab931bb527 | ||
|
|
e756897f88 | ||
|
|
05ea25a86f | ||
|
|
a6ab49a911 | ||
|
|
33ee7ac114 | ||
|
|
61981a0ea1 | ||
|
|
204e9383fa | ||
|
|
d424032757 | ||
|
|
0fcdf3f4ff | ||
|
|
1b48739e1e | ||
|
|
8b96b1d835 | ||
|
|
3569500111 | ||
|
|
05cfcb5648 | ||
|
|
e425064730 | ||
|
|
4a50e7f86d | ||
|
|
15cbacd1d2 | ||
|
|
cdf4a63ae8 | ||
|
|
f35a674b9f | ||
|
|
03a4691bc0 | ||
|
|
a990eac146 | ||
|
|
27f06d0f9d | ||
|
|
7a05ae1714 | ||
|
|
72549a7bcb | ||
|
|
4f46aa5360 | ||
|
|
b07e985e8b | ||
|
|
0c072b018e | ||
|
|
d7456c0528 | ||
|
|
60c5159a4a | ||
|
|
7388472f6a | ||
|
|
c24c31d8d3 | ||
|
|
be006051b5 | ||
|
|
4590dbc056 | ||
|
|
b5e751e625 | ||
|
|
b5a8098d9e | ||
|
|
3d1de2849c | ||
|
|
887850a612 |
No files matched your search
@@ -33,3 +33,4 @@ trace_data/
|
||||
/src/lib/orderman/perf_analysis/log.csv
|
||||
/doc/user/public/*
|
||||
.ionide/*
|
||||
/.ignore
|
||||
@@ -4,10 +4,23 @@ weight = 3
|
||||
sort_by = "weight"
|
||||
insert_anchor_links = "right"
|
||||
+++
|
||||
## 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)
|
||||
### 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
|
||||
## 2.9.3
|
||||
### Internal Changes
|
||||
- Fixed system test scripts
|
||||
## 2.9.2
|
||||
### Bug Fixes
|
||||
- Fixed an issue with the exchange simulator that could lead to crossed markets
|
||||
|
||||
## 2.9.1
|
||||
### Bug Fixes
|
||||
- Fixed an issue with trade sound alerts, where in some cases the use of a "In Between" condition could lead to a UI failure.
|
||||
|
||||
+1
-1
Submodule docker/linux updated: b09d4c035b...e13bb5d8df.
+1
-1
Submodule externals updated: 6b8c745fb2...b5ca264286.
@@ -41,9 +41,11 @@ namespace mbts.ui {
|
||||
EditDocumentGeometryKey = nameof(EditDocumentGeometryKey),
|
||||
GearGeometryKey = nameof(GearGeometryKey),
|
||||
HornGeometryKey = nameof(HornGeometryKey),
|
||||
KeyGeometryKey = nameof(KeyGeometryKey),
|
||||
LetterAKey = nameof(LetterAKey),
|
||||
LetterDKey = nameof(LetterDKey),
|
||||
OrdersGeometryKey = nameof(OrdersGeometryKey),
|
||||
PauseGeometryKey = nameof(PauseGeometryKey),
|
||||
PaymentGeometryKey = nameof(PaymentGeometryKey),
|
||||
PencilGeometryKey = nameof(PencilGeometryKey),
|
||||
PlayGeometryKey = nameof(PlayGeometryKey),
|
||||
@@ -151,7 +153,7 @@ namespace mbts.ui {
|
||||
public static ImageSource CheckMark => create_icon(CheckMarkGeometryKey);
|
||||
public static ImageSource Clock { get { return load_resource_and_freeze("Clock"); } }
|
||||
public static ImageSource CopyWF => create_icon(CopyWFGeometryKey);
|
||||
public static ImageSource DataEdit => create_icon(DataEditGeometryKey, Brushes.White, new Pen(Brushes.Black, 1).frozen());
|
||||
public static ImageSource DataEdit => create_icon(DataEditGeometryKey);
|
||||
public static ImageSource Defrag { get { return load_resource_and_freeze("Defrag"); } }
|
||||
public static ImageSource DetachAlgos => create_icon_and_rotate(DetachAlgosKey, Brushes.Red);
|
||||
public static ImageSource Disk => create_icon(DiskGeometryKey);
|
||||
@@ -172,9 +174,12 @@ namespace mbts.ui {
|
||||
public static ImageSource Gear => create_icon(GearGeometryKey);
|
||||
public static ImageSource Horn => create_icon(HornGeometryKey);
|
||||
public static ImageSource Import => load_resource_and_freeze("Import");
|
||||
public static ImageSource Key => create_icon(KeyGeometryKey);
|
||||
public static ImageSource Knight { get { return load_resource_and_freeze("Knight"); } }
|
||||
public static ImageSource Pause => create_icon(PauseGeometryKey);
|
||||
public static ImageSource Payment => create_icon(PaymentGeometryKey);
|
||||
public static ImageSource Pencil => create_icon(PencilGeometryKey);
|
||||
public static ImageSource Play => create_icon(PlayGeometryKey);
|
||||
public static ImageSource Prohibited => create_icon(ProhibitedGeometryKey);
|
||||
public static ImageSource RubberStamp => create_icon(RubberStampGeometryKey);
|
||||
public static ImageSource RedoCircular => create_icon(UndoCircularGeometryKey, Brushes.Black, null, new ScaleTransform(-1, 1));
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<ResourceDictionary
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:mbts="clr-namespace:mbts.ui"
|
||||
xmlns:sys="clr-namespace:System;assembly=mscorlib"
|
||||
>
|
||||
@@ -18,9 +18,7 @@
|
||||
Ttile: Add free icon
|
||||
Credit: Icon made by Gregor Cresnar from www.flaticon.com-->
|
||||
<sys:String x:Key="{x:Static mbts:Icons.AddBoxesGeometryKey}">
|
||||
M393,480.3c0-5-4.1-9.1-9.1-9.1H18.2V105.5c0-5-4.1-9.1-9.1-9.1S0,100.5,0,105.5v374.8c0,5,4.1,9.1,9.1,9.1h374.8 C388.9,489.4,393,485.3,393,480.3z
|
||||
M373.9,187.5H302v-71.9c0-5-4.1-9.1-9.1-9.1s-9.1,4.1-9.1,9.1v71.9h-71.9c-5,0-9.1,4.1-9.1,9.1s4.1,9.1,9.1,9.1h71.9v71.9 c0,5,4.1,9.1,9.1,9.1s9.1-4.1,9.1-9.1v-71.9h71.9c5,0,9.1-4.1,9.1-9.1C382.9,191.5,378.9,187.5,373.9,187.5z
|
||||
M105.5,393h374.8c5,0,9.1-4.1,9.1-9.1V9.1c0-5-4.1-9.1-9.1-9.1H105.5c-5,0-9.1,4.1-9.1,9.1v374.8 C96.4,388.9,100.5,393,105.5,393z M114.5,18.2h356.7v356.7H114.5V18.2z
|
||||
M 87.5 10 C 80.313542 10 74.400391 15.913151 74.400391 23.099609 V 397.90039 C 74.400391 405.08685 80.313541 411 87.5 411 h 374.80078 c 7.18646 0 13.09961 -5.91315 13.09961 -13.09961 V 23.099609 C 475.40039 15.913151 469.48724 10 462.30078 10 Z m 13 26.199219 H 449.19922 V 384.90039 H 100.5 Z M 21.099609 76.400391 C 13.912943 76.400391 8 82.313333 8 89.5 v 374.80078 c 0 7.18667 5.912943 13.09961 13.099609 13.09961 H 395.90039 c 7.18667 0 13.09961 -5.91294 13.09961 -13.09961 0 -7.18667 -5.91294 -13.10156 -13.09961 -13.10156 H 34.199219 V 89.5 c 0 -7.186667 -5.912943 -13.099609 -13.09961 -13.099609 z M 274.90039 116.5 c -7.18667 0 -13.09961 5.91294 -13.09961 13.09961 V 197.5 h -67.90039 c -7.18667 0 -13.09961 5.91294 -13.09961 13.09961 0 7.18667 5.91294 13.09961 13.09961 13.09961 h 67.90039 v 67.90039 c 0 7.18667 5.91294 13.09961 13.09961 13.09961 7.18667 0 13.09961 -5.91294 13.09961 -13.09961 v -67.90039 h 67.90039 c 7.18667 0 13.09961 -5.91294 13.09961 -13.09961 v -0.0391 -0.0391 C 368.85982 203.37217 363.03372 197.5 355.90039 197.5 H 288 V 129.59961 C 288 122.41294 282.08706 116.5 274.90039 116.5 Z
|
||||
</sys:String>
|
||||
|
||||
<!--File (the icon is free to use if we include this information in our credit section)
|
||||
@@ -116,7 +114,7 @@
|
||||
</sys:String>
|
||||
|
||||
<sys:String x:Key="{x:Static mbts:Icons.DataEditGeometryKey}">
|
||||
M17.997977,17.396268C18.197928,17.396268,18.297904,17.496276,18.497975,17.596284L19.497972,18.295283C19.99797,18.595292,20.697919,19.195308,21.197917,19.495299L22.196937,20.195323C22.696935,20.495315,22.596959,20.995322,22.096961,21.195338L20.997966,21.695346C20.497968,21.89434,19.597947,22.294355,19.097949,22.494355L17.997977,22.994363C17.497979,23.194362,17.097957,22.894355,17.197932,22.394347L17.297908,21.195338C17.398005,20.595323,17.497979,19.695315,17.497979,19.195308L17.597955,17.995277C17.497979,17.695283,17.69793,17.396268,17.997977,17.396268z M1.799981,13.597213L19.697923,13.597213 17.997977,15.896244C17.797906,16.096259 17.69793,16.396253 17.69793,16.69626 17.197932,16.796268 16.79791,17.296275 16.697934,17.895284L16.597959,19.195308 1.799981,19.195308C0.79998477,19.195308,0,18.395291,0,17.396268L0,15.396237C0,14.396221,0.79998477,13.597213,1.799981,13.597213z M1.799981,6.7981106L21.796914,6.7981106C22.696935,6.7981106,23.49698,7.498118,23.596955,8.3981257L20.697919,12.397197 1.799981,12.397197C0.79998477,12.397197,0,11.597181,0,10.597165L0,8.5981418C0,7.598126,0.79998477,6.7981106,1.799981,6.7981106z M27.796891,4.3990708C27.996963,4.3990708,28.096938,4.3990708,28.296889,4.5990711L31.695924,7.0981179C31.995851,7.3981105,32.095948,7.7981258,31.895875,8.0981341L23.296908,19.795308C23.096957,20.095315,22.596959,20.195323,22.196937,19.895314L18.797902,17.396268C18.497975,17.096274,18.398002,16.69626,18.597951,16.396253L27.196918,4.6990786C27.396869,4.4990784,27.59694,4.3990708,27.796891,4.3990708z M1.799981,0L21.796914,0C22.79691,4.9549271E-08,23.596955,0.8000156,23.596955,1.800031L23.596955,3.7990554C23.596955,4.7990709,22.79691,5.5990864,21.796914,5.5990864L1.799981,5.5990864C0.79998477,5.4990941,0,4.6990786,0,3.6990629L0,1.7000236C0,0.8000156,0.79998477,4.9549271E-08,1.799981,0z
|
||||
M 22.589844 14.470703 H 0.82421875 C 0.36896661 14.470586 -1.1747368e-4 14.83967 0 15.294922 v 4.669922 c -1.1747368e-4 0.455252 0.36896661 0.824336 0.82421875 0.824218 H 16.940231 l 0.425003 -1.09375 0.554688 -0.554687 H 1.6484375 V 16.119141 H 20.941406 Z M 27.462891 9.5976562 V 8.0742188 A 0.8240064 0.8240064 0 0 0 26.638672 7.25 H 0.82421875 A 0.8240064 0.8240064 0 0 0 0 8.0742188 v 4.6699222 a 0.8240064 0.8240064 0 0 0 0.82421875 0.824218 H 23.492188 l 1.648437 -1.648437 H 1.6484375 V 8.8984375 H 25.814453 V 11.246094 Z M 0.82421878 2.736881e-8 A 0.8240064 0.8240064 0 0 0 2.7433281e-8 0.82421879 V 5.4941406 A 0.8240064 0.8240064 0 0 0 0.82421878 6.3183594 H 26.638672 A 0.8240064 0.8240064 0 0 0 27.46289 5.4941406 V 0.82421879 A 0.8240064 0.8240064 0 0 0 26.638672 2.736881e-8 Z M 1.6484375 1.6484375 H 25.814453 V 4.6699219 H 1.6484375 Z M 31.628051 7.2558423 19.165575 19.718635 23.70324 24.256618 36.166032 11.793823 Z m -0.0013 3.5965607 0.941422 0.94142 -8.866233 8.866232 -0.94142 -0.94142 z m -13.240122 9.708458 -1.636234 6.110706 6.109329 -1.637606 z
|
||||
</sys:String>
|
||||
<sys:String x:Key="{x:Static mbts:Icons.DiskGeometryKey}">
|
||||
M403.213,20H0v472h512V128.787L403.213,20z M131,50h180v100h10h20h10V50h30v130H131V50z M381,462H131V315
|
||||
@@ -278,10 +276,18 @@
|
||||
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>
|
||||
|
||||
<sys:String x:Key="{x:Static mbts:Icons.KeyGeometryKey}">
|
||||
M22.399981,2.4000006C22.099982,2.4000006 21.899982,2.5 21.599982,2.6999998 21.199983,3.1000004 21.199983,3.8000002 21.599982,4.2000003L26.199979,8.9000001C26.599978,9.3000002 27.299977,9.3000002 27.699977,8.9000001 28.099977,8.5000002 28.099977,7.8000002 27.699977,7.4000006L23.099981,2.6999998C22.899981,2.5,22.699981,2.4000006,22.399981,2.4000006z M21.499982,0C22.299982,0,23.199981,0.30000019,23.79998,0.90000057L29.299976,6.4000001C30.599975,7.7000003,30.599975,9.7000003,29.299976,11L22.599981,17.7C21.299983,19,19.299984,19,17.999985,17.7L17.299985,17 12.699989,21.6 10.399991,20.900001 10.899991,23.400001 8.4999928,22.8 8.9999924,25.5 6.2999945,25.1 6.7999945,27.6 4.2999964,27 4.7999954,29.6 2.4999971,32 0,26.6 13.499989,13.1 16.999986,16.6 12.699989,12.3C11.399991,11,11.399991,9.0000002,12.699989,7.7000003L19.399984,1C19.899983,0.30000019,20.699983,0,21.499982,0z
|
||||
</sys:String>
|
||||
|
||||
<sys:String x:Key="{x:Static mbts:Icons.OrdersGeometryKey}">
|
||||
M50.14375,33.389126C50.416863,33.394688,50.653496,33.587773,50.705998,33.864617L50.015995,45.114388C49.810295,46.291462,49.130394,45.097489,49.130394,45.097489L47.65659,42.640538C42.747776,49.635197 34.633152,54.221001 25.433924,54.221001 14.884193,54.221001 5.7437552,48.204125 1.2449617,39.419205 0.80087067,38.552021 0.21358696,37.705738 0.052086769,36.736958 -0.49735315,33.490925 3.4507786,32.901036 4.8049124,34.432404 5.3674143,35.067692 6.034036,36.16407 6.5079177,37.026051 10.106778,43.799416 17.2321,48.413819 25.433924,48.413819 32.656544,48.413819 39.026364,44.826695 42.903977,39.351506L40.229367,38.854115C40.229367,38.854115,38.677464,38.647118,39.742466,37.996033L50.024895,33.398526C50.064972,33.391365,50.104733,33.388333,50.14375,33.389126z M23.030291,9.0310004L27.889918,9.0310004 27.889918,13.028299C30.539786,13.205498,32.324932,13.958096,33.464161,14.528294L33.964176,14.775793 32.417435,20.038878 31.706417,19.68068C30.579987,19.11958 28.839241,18.244583 26.149073,18.244583 24.006916,18.244583 22.723183,19.122181 22.723183,20.590876 22.723183,21.894272 23.943314,22.804369 27.551308,24.187165 32.706444,26.124659 34.911,28.702853 34.911,32.817341 34.911,36.892929 32.090527,40.05172 27.673611,40.989117L27.673611,45.540001 22.767485,45.540001 22.767485,41.330414C20.252919,41.155815,17.695551,40.424118,16.101811,39.404721L15.683999,39.137619 17.221739,33.731438 17.969059,34.180634C19.088988,34.851235 21.402748,35.97873 24.340325,35.97873 26.734886,35.97873 28.276728,34.921635 28.276728,33.291336 28.276728,31.986641 27.547409,30.853846 23.986016,29.57255 20.083815,28.144253 16.095511,26.04406 16.095511,21.239174 16.095511,17.280985 18.737379,14.291395 23.030291,13.329098z M25.268765,0C35.819447,2.7347414E-07 44.961449,6.0155495 49.458798,14.804573 49.902695,15.669173 50.490094,16.516972 50.651287,17.48427 51.198281,20.734265 47.255726,21.318962 45.901341,19.789065 45.336346,19.153567 44.669552,18.051968 44.19566,17.19517 40.5957,10.421881 33.473275,5.8072398 25.268765,5.8072393 18.047545,5.8072398 11.678013,9.3918336 7.8003562,14.867073L10.474627,15.367072C10.474627,15.367072,12.026909,15.574172,10.962921,16.223872L0.67933281,20.826762C0.36037083,20.880062,0.057029126,20.669165,0.00099972012,20.354063L0.69093342,9.1118043C0.89552094,7.9296158,1.5738537,9.124924,1.5738539,9.124924L3.0452772,11.583379C7.9566038,4.584462,16.073666,2.7347414E-07,25.268765,0z
|
||||
</sys:String>
|
||||
|
||||
<sys:String x:Key="{x:Static mbts:Icons.PauseGeometryKey}">
|
||||
M17.94,0L29.150001,0 29.150001,32 17.94,32z M0,0L11.210002,0 11.210002,32 0,32z
|
||||
</sys:String>
|
||||
|
||||
<sys:String x:Key="{x:Static mbts:Icons.PaymentGeometryKey}">
|
||||
M28.000001,6.0000026L28.000001,15 6.0010006,15 6.0010006,17.000003 30.001,17.000003 30.001,6.0000026z M14.231,2.9540056L14.979999,2.9540056 14.979999,3.987005C15.704998,4.009005,16.200997,4.178005,16.550998,4.3600049L16.248998,5.0620046C15.994998,4.9340047 15.499998,4.7010047 14.725999,4.7010047 13.796,4.7010047 13.445001,5.1900046 13.445001,5.6160046 13.445001,6.1710043 13.893,6.4470041 14.943999,6.8310037 16.187998,7.2780037 16.816998,7.8320036 16.816998,8.7800031 16.816998,9.6210027 16.151998,10.409003 14.907999,10.591002L14.907999,11.698002 14.146,11.698002 14.146,10.622003C13.445001,10.611002,12.720001,10.420002,12.297002,10.154003L12.587001,9.4400029C13.022001,9.696003 13.651,9.8980026 14.328,9.8980026 15.185999,9.8980026 15.765998,9.4620028 15.765998,8.8540034 15.765998,8.2680035 15.293999,7.9060035 14.4,7.5870035 13.167001,7.1610038 12.406001,6.6710041 12.406001,5.7440041 12.406001,4.8600047 13.119,4.189005 14.231,4.0290052z M2,2.0000002L2,13 26.000001,13 26.000001,2.0000002z M0,0L28.000001,0 28.000001,4.0000031 32.001,4.0000031 32.001,19.000003 4.0010006,19.000003 4.0010006,15 0,15z
|
||||
</sys:String>
|
||||
@@ -345,7 +351,7 @@
|
||||
</sys:String>
|
||||
|
||||
<sys:String x:Key="{x:Static mbts:Icons.TrashGeometryKey}">
|
||||
M0,12.568994L23.662999,12.568994 20.910989,31.999999 2.7520113,31.999999z M8.0689915,1.6269979L8.0689915,4.6679968 15.594984,4.6679968 15.594984,1.6269979z M6.4420097,0L17.221997,0 17.221997,4.6679968 23.662999,4.6679968 23.662999,8.7949938 0,8.7949938 0,4.6679968 6.4420097,4.6679968z
|
||||
M15.000001,8.0000272L17,8.0000272 17,27.000028 15.000001,27.000028z M11.000001,8.0000272L13.000001,8.0000272 13.000001,27.000028 11.000001,27.000028z M7.0000005,8.0000272L9.000001,8.0000272 9.000001,27.000028 7.0000005,27.000028z M3.0790101,5.0000274L4.9959994,29.922999 19.000006,30.000026 20.918949,5.0000274z M13.771989,1.9959999L10.198989,2.0000001C10.158989,2.025,9.9989892,2.241,9.9989892,2.6000001L9.9989892,3.0000258 13.998989,3.0000258 13.998989,2.6000001C13.998989,2.241,13.838988,2.025,13.771989,1.9959999z M10.198989,0L13.798988,0C15.031989,0,15.998989,1.142,15.998989,2.6000001L15.998989,3.0000258 23.07898,3.0000258 24,3.0000272 24,5.0000274 22.925121,5.0000274 20.995976,30.076991C20.999027,31.102992,20.100956,32.000026,18.999029,32.000026L4.9990512,32.000026C3.8960255,32.000026,2.9990543,31.102992,2.999054,30.000026L1.073059,5.0000274 0,5.0000274 0,3.0000272 0.91897895,3.0000272 7.9989887,3.0000258 7.9989887,2.6000001C7.9989887,1.142,8.9659892,0,10.198989,0z
|
||||
</sys:String>
|
||||
|
||||
<!--UndoCircular (the icon is free to use if we include this information in our credit/about section)
|
||||
@@ -405,10 +411,10 @@
|
||||
<DrawingGroup>
|
||||
<DrawingGroup.Children>
|
||||
<GeometryDrawing Geometry="
|
||||
M 32.868601,3.8869994 C31.026201,3.8869996 29.534,5.3791947 29.534,7.2190399 29.534,9.0588851 31.026201,10.553 32.868601,10.553 34.7085,10.553 36.202,9.0588851 36.202,7.2190399 36.202,5.3791947 34.7085,3.8869996 32.868601,3.8869994z
|
||||
M 42.868601,3.8869994 C41.026201,3.8869996 39.534,5.3791947 39.534,7.2190399 39.534,9.0588851 41.026201,10.553 42.868601,10.553 44.7085,10.553 46.202,9.0588851 46.202,7.2190399 46.202,5.3791947 44.7085,3.8869996 42.868601,3.8869994z
|
||||
M 52.868601,3.8869994 C51.026201,3.8869996 49.534,5.3791947 49.534,7.2190399 49.534,9.0588851 51.026201,10.553 52.868601,10.553 54.7085,10.553 56.202,9.0588851 56.202,7.2190399 56.202,5.3791947 54.7085,3.8869996 52.868601,3.8869994z
|
||||
M5.2010002,14.812L5.2010002,43.573997 56.098,43.573997 56.098,14.812z
|
||||
M 32.868601,3.8869994 C31.026201,3.8869996 29.534,5.3791947 29.534,7.2190399 29.534,9.0588851 31.026201,10.553 32.868601,10.553 34.7085,10.553 36.202,9.0588851 36.202,7.2190399 36.202,5.3791947 34.7085,3.8869996 32.868601,3.8869994z
|
||||
M 42.868601,3.8869994 C41.026201,3.8869996 39.534,5.3791947 39.534,7.2190399 39.534,9.0588851 41.026201,10.553 42.868601,10.553 44.7085,10.553 46.202,9.0588851 46.202,7.2190399 46.202,5.3791947 44.7085,3.8869996 42.868601,3.8869994z
|
||||
M 52.868601,3.8869994 C51.026201,3.8869996 49.534,5.3791947 49.534,7.2190399 49.534,9.0588851 51.026201,10.553 52.868601,10.553 54.7085,10.553 56.202,9.0588851 56.202,7.2190399 56.202,5.3791947 54.7085,3.8869996 52.868601,3.8869994z
|
||||
M5.2010002,14.812L5.2010002,43.573997 56.098,43.573997 56.098,14.812z
|
||||
M0,0L63.999001,0 63.999001,48.792999 0,48.792999z">
|
||||
<GeometryDrawing.Pen>
|
||||
<Pen Thickness="1" Brush="Black" />
|
||||
@@ -424,10 +430,10 @@ M0,0L63.999001,0 63.999001,48.792999 0,48.792999z">
|
||||
<DrawingGroup>
|
||||
<DrawingGroup.Children>
|
||||
<GeometryDrawing Brush="Black" Geometry="
|
||||
M 32.868601,3.8869994 C31.026201,3.8869996 29.534,5.3791947 29.534,7.2190399 29.534,9.0588851 31.026201,10.553 32.868601,10.553 34.7085,10.553 36.202,9.0588851 36.202,7.2190399 36.202,5.3791947 34.7085,3.8869996 32.868601,3.8869994z
|
||||
M 42.868601,3.8869994 C41.026201,3.8869996 39.534,5.3791947 39.534,7.2190399 39.534,9.0588851 41.026201,10.553 42.868601,10.553 44.7085,10.553 46.202,9.0588851 46.202,7.2190399 46.202,5.3791947 44.7085,3.8869996 42.868601,3.8869994z
|
||||
M 52.868601,3.8869994 C51.026201,3.8869996 49.534,5.3791947 49.534,7.2190399 49.534,9.0588851 51.026201,10.553 52.868601,10.553 54.7085,10.553 56.202,9.0588851 56.202,7.2190399 56.202,5.3791947 54.7085,3.8869996 52.868601,3.8869994z
|
||||
M5.2010002,14.812L5.2010002,43.573997 56.098,43.573997 56.098,14.812z
|
||||
M 32.868601,3.8869994 C31.026201,3.8869996 29.534,5.3791947 29.534,7.2190399 29.534,9.0588851 31.026201,10.553 32.868601,10.553 34.7085,10.553 36.202,9.0588851 36.202,7.2190399 36.202,5.3791947 34.7085,3.8869996 32.868601,3.8869994z
|
||||
M 42.868601,3.8869994 C41.026201,3.8869996 39.534,5.3791947 39.534,7.2190399 39.534,9.0588851 41.026201,10.553 42.868601,10.553 44.7085,10.553 46.202,9.0588851 46.202,7.2190399 46.202,5.3791947 44.7085,3.8869996 42.868601,3.8869994z
|
||||
M 52.868601,3.8869994 C51.026201,3.8869996 49.534,5.3791947 49.534,7.2190399 49.534,9.0588851 51.026201,10.553 52.868601,10.553 54.7085,10.553 56.202,9.0588851 56.202,7.2190399 56.202,5.3791947 54.7085,3.8869996 52.868601,3.8869994z
|
||||
M5.2010002,14.812L5.2010002,43.573997 56.098,43.573997 56.098,14.812z
|
||||
M0,0L63.999001,0 63.999001,48.792999 0,48.792999z">
|
||||
<GeometryDrawing.Pen>
|
||||
<Pen Thickness="1" Brush="Black" />
|
||||
@@ -843,7 +849,7 @@ M0,0L63.999001,0 63.999001,48.792999 0,48.792999z">
|
||||
<DrawingImage.Drawing>
|
||||
<DrawingGroup>
|
||||
<DrawingGroup.Children>
|
||||
<GeometryDrawing
|
||||
<GeometryDrawing
|
||||
Brush="Black"
|
||||
Geometry="M23,14C24.10498,14 25,14.89502 25,16 25,17.10498 24.10498,18 23,18 21.89502,18 21,17.10498 21,16 21,14.89502 21.89502,14 23,14z M16,14C17.105,14 18,14.895 18,16 18,17.105 17.105,18 16,18 14.895,18 14,17.105 14,16 14,14.895 14.895,14 16,14z M9,14C10.105,14 11,14.895 11,16 11,17.105 10.105,18 9,18 7.8950005,18 7,17.105 7,16 7,14.895 7.8950005,14 9,14z M16,2C8.2799997,2 2,8.2800002 2,16 2,23.72 8.2799997,30 16,30 23.72,30 30,23.72 30,16 30,8.2800002 23.72,2 16,2z M16,0C24.822,0 32,7.178 32,16 32,24.822 24.822,32 16,32 7.1780005,32 0,24.822 0,16 0,7.178 7.1780005,0 16,0z" />
|
||||
</DrawingGroup.Children>
|
||||
@@ -856,7 +862,7 @@ M0,0L63.999001,0 63.999001,48.792999 0,48.792999z">
|
||||
<DrawingImage.Drawing>
|
||||
<DrawingGroup>
|
||||
<DrawingGroup.Children>
|
||||
<GeometryDrawing
|
||||
<GeometryDrawing
|
||||
Brush="Black"
|
||||
Geometry="M299.152955025434,0L496.426178902388,0 268.890046089888,280.247772216797 198.190201729536,367.407897949219 199.87503144145,369.495361328125 271.502137154341,457.706451416016 498.999970406294,738 301.717621773481,738 101.234124153852,491.008697509766 99.5304498374462,488.936553955078 98.6409082114697,490.003173828125 0,368.488952636719 98.6409082114697,246.945526123047z" />
|
||||
</DrawingGroup.Children>
|
||||
@@ -868,7 +874,7 @@ M0,0L63.999001,0 63.999001,48.792999 0,48.792999z">
|
||||
<DrawingImage.Drawing>
|
||||
<DrawingGroup>
|
||||
<DrawingGroup.Children>
|
||||
<GeometryDrawing
|
||||
<GeometryDrawing
|
||||
Brush="{mbts:BTSColor MarketData.bid}"
|
||||
Geometry="m 24.706673 31.196264 -3.086182 3.086182 5.965481 5.963348 -5.965481 5.96548 3.086182 3.084049 5.963348 -5.963348 5.96548 5.963348 3.084049 -3.084049 -5.963347 -5.96548 5.963347 -5.963348 -3.084049 -3.086182 -5.96548 5.965481 z M 5.3507926 32.045363 H 17.816449 v 2.401501 H 5.3507926 Z m 0 -4.937233 H 24.132994 v 2.401501 H 5.3507926 Z m 0 9.874466 h 5.5381524 v 2.401501 H 5.3507926 Z M 1.3730469 0 A 1.3741374 1.3741374 0 0 0 0 1.3730469 V 45.630859 a 1.3741374 1.3741374 0 0 0 1.3730469 1.373047 H 16.853516 a 17.906029 17.906029 0 0 1 -0.05859 -1.373047 17.906029 17.906029 0 0 1 0.06836 -1.375 H 2.7480469 V 2.7480469 H 33.326172 V 27.783203 a 17.906029 17.906029 0 0 1 1.375 -0.05859 17.906029 17.906029 0 0 1 1.373047 0.06836 V 1.3730469 A 1.3741374 1.3741374 0 0 0 34.701172 0 Z M 10.662215 24.759265 V 5.1202697 h 7.148923 q 3.286862 0 5.05355 1.2051818 1.766687 1.2051838 1.766687 3.3964241 0 1.5886494 -1.081924 2.7801354 -1.068231 1.191488 -2.739053 1.657126 v 0.05479 q 2.095375 0.26021 3.341644 1.547564 1.259962 1.287354 1.259962 3.136214 0 2.697964 -1.93103 4.286613 -1.931032 1.574956 -5.272673 1.574956 z M 15.085783 8.3797408 v 4.6563852 h 1.944726 q 1.369526 0 2.150155 -0.657372 0.794325 -0.671068 0.794325 -1.835164 0 -2.1638492 -3.23208 -2.1638492 z m 0 7.9432472 v 5.176805 h 2.39667 q 1.533868 0 2.39667 -0.712152 0.876495 -0.712155 0.876495 -1.944728 0 -1.17779 -0.862802 -1.848858 -0.849105 -0.671067 -2.382974 -0.671067 z" />
|
||||
</DrawingGroup.Children>
|
||||
@@ -880,7 +886,7 @@ M0,0L63.999001,0 63.999001,48.792999 0,48.792999z">
|
||||
<DrawingImage.Drawing>
|
||||
<DrawingGroup>
|
||||
<DrawingGroup.Children>
|
||||
<GeometryDrawing
|
||||
<GeometryDrawing
|
||||
Brush="{mbts:BTSColor MarketData.ask}"
|
||||
Geometry="m 19.060265 26.797642 v -2.144583 q 0.583058 0.489233 1.266644 0.737201 0.683586 0.241265 1.380576 0.241265 0.408811 0 0.710393 -0.07372 0.308284 -0.07372 0.509339 -0.201055 0.207756 -0.134036 0.308284 -0.308283 0.100527 -0.18095 0.100527 -0.388706 0 -0.281477 -0.160844 -0.502637 -0.160844 -0.22116 -0.44232 -0.408811 -0.274775 -0.187651 -0.656779 -0.361898 -0.382004 -0.174248 -0.824324 -0.355197 -1.125906 -0.469128 -1.682158 -1.146012 -0.549549 -0.676884 -0.549549 -1.635245 0 -0.750604 0.301582 -1.28675 0.301582 -0.542847 0.817622 -0.891342 0.522743 -0.348495 1.206328 -0.509339 0.683586 -0.167545 1.447594 -0.167545 0.750604 0 1.326961 0.09383 0.583059 0.08712 1.072292 0.274775 v 2.003845 q -0.241266 -0.167545 -0.529444 -0.29488 -0.281477 -0.127335 -0.583059 -0.207757 -0.301582 -0.08712 -0.603164 -0.127334 -0.29488 -0.04021 -0.562953 -0.04021 -0.3686 0 -0.670182 0.07372 -0.301582 0.06702 -0.509339 0.194353 -0.207756 0.127334 -0.321687 0.308284 -0.113931 0.174247 -0.113931 0.395407 0 0.241266 0.127334 0.435619 0.127335 0.187651 0.361899 0.361898 0.234564 0.167546 0.569655 0.335091 0.335091 0.160844 0.757306 0.335091 0.576356 0.241266 1.03208 0.516041 0.462426 0.268073 0.790816 0.609866 0.328389 0.341793 0.502636 0.784113 0.174248 0.435618 0.174248 1.018677 0 0.804219 -0.308284 1.353768 -0.301582 0.542848 -0.824324 0.884641 -0.522743 0.335091 -1.219732 0.482531 -0.690288 0.14744 -1.460998 0.14744 -0.790815 0 -1.50791 -0.134036 -0.710393 -0.134037 -1.233135 -0.40211 z m 6.620223 3.788289 -1.556212 1.556212 3.008102 3.007026 -3.008102 3.008101 1.556212 1.555136 3.007026 -3.007026 3.008101 3.007026 1.555136 -1.555136 -3.007026 -3.008101 3.007026 -3.007026 -1.555136 -1.556212 -3.008101 3.008102 z m -9.76023 2.917766 h 2.792621 v 1.21096 h -2.792621 z m 0 -2.489606 h 6.285825 v 1.21096 h -6.285825 z m 0 -2.489607 h 9.470951 v 1.210961 H 15.920258 Z M 13.914475 14.855173 a 0.69291063 0.69291063 0 0 0 -0.692361 0.692361 v 22.317061 a 0.69291063 0.69291063 0 0 0 0.692361 0.692361 h 7.806047 a 9.0291392 9.0291392 0 0 1 -0.02954 -0.692361 9.0291392 9.0291392 0 0 1 0.03447 -0.693345 H 14.60782 V 16.24088 h 15.419061 v 12.624011 a 9.0291392 9.0291392 0 0 1 0.693346 -0.02954 9.0291392 9.0291392 0 0 1 0.692361 0.03447 V 15.547534 a 0.69291063 0.69291063 0 0 0 -0.692361 -0.692361 z" />
|
||||
</DrawingGroup.Children>
|
||||
@@ -893,7 +899,7 @@ M0,0L63.999001,0 63.999001,48.792999 0,48.792999z">
|
||||
<DrawingImage.Drawing>
|
||||
<DrawingGroup>
|
||||
<DrawingGroup.Children>
|
||||
<GeometryDrawing
|
||||
<GeometryDrawing
|
||||
Brush="Black"
|
||||
Geometry="M 5.3507926 32.045363 H 17.81645 v 2.401501 H 5.3507926 Z m 0 -4.937233 H 24.132995 v 2.401501 H 5.3507926 Z M 18.799228 22.222041 v 2.32929 h -1.567173 v -2.264886 q -2.179014 -0.01073 -4.057475 -0.987533 v -2.973336 q 0.622575 0.504501 1.856993 0.933864 1.234417 0.429362 2.200482 0.493766 v -3.907198 q -2.511771 -0.933863 -3.4349 -2.018003 -0.912395 -1.094874 -0.912395 -2.662047 0 -1.685248 1.191482 -2.8659947 1.19148 -1.1914807 3.155813 -1.3846939 V 4.918734 h 1.567173 v 1.9535992 q 2.264887 0.1073411 3.38123 0.7299162 V 10.500446 Q 20.677689 9.5880506 18.799228 9.3841034 v 4.0682086 q 2.35076 0.847992 3.370495 1.942866 1.019736 1.08414 1.019736 2.629845 0 1.781854 -1.137811 2.876728 -1.13781 1.08414 -3.25242 1.32029 z M 17.232055 12.829737 V 9.4270397 q -1.492034 0.2683516 -1.492034 1.5564393 0 1.105608 1.492034 1.846258 z m 1.567173 3.628113 v 3.25242 q 1.53497 -0.236149 1.53497 -1.53497 0 -1.051939 -1.53497 -1.71745 z m 13.258587 13.65074 h -3.954117 l 0.0014 7.641804 -7.643169 -10e-7 0.0014 3.952751 h 7.640439 l 0.0014 7.641803 h 3.951385 l 0.0014 -7.641802 7.640437 -10e-7 0.0014 -3.952751 -7.643169 10e-7 z M 5.3507926 36.982596 h 5.5381524 v 2.401501 H 5.3507926 Z M 1.3730469 0 A 1.3741374 1.3741374 0 0 0 0 1.3730469 V 45.630859 a 1.3741374 1.3741374 0 0 0 1.3730469 1.373047 H 16.853516 a 17.906029 17.906029 0 0 1 -0.05859 -1.373047 17.906029 17.906029 0 0 1 0.06836 -1.375 H 2.7480469 V 2.7480469 H 33.326172 V 27.783203 a 17.906029 17.906029 0 0 1 1.375 -0.05859 17.906029 17.906029 0 0 1 1.373047 0.06836 V 1.3730469 A 1.3741374 1.3741374 0 0 0 34.701172 0 Z" />
|
||||
</DrawingGroup.Children>
|
||||
@@ -905,7 +911,7 @@ M0,0L63.999001,0 63.999001,48.792999 0,48.792999z">
|
||||
<DrawingImage.Drawing>
|
||||
<DrawingGroup>
|
||||
<DrawingGroup.Children>
|
||||
<GeometryDrawing
|
||||
<GeometryDrawing
|
||||
Brush="Black"
|
||||
Geometry="m 26.463403 27.963318 h -2.359042 l -0.683586 -2.137882 h -3.417929 l -0.676884 2.137882 h -2.345638 l 3.498351 -9.610414 h 2.566798 z M 22.92484 24.163384 21.89276 20.933106 q -0.113931 -0.361899 -0.160844 -0.864535 h -0.05361 q -0.03351 0.422214 -0.167545 0.837727 l -1.045485 3.257086 z m 2.065112 7.055953 -1.51223 1.51223 2.923087 2.922042 -2.923087 2.923086 1.51223 1.511185 2.922042 -2.922042 2.923086 2.922042 1.511185 -1.511185 -2.922041 -2.923086 2.922041 -2.922042 -1.511185 -1.51223 -2.923086 2.923087 z m -9.484387 0.416058 h 6.108175 v 1.176737 h -6.108175 z m 0 -2.419244 h 9.203284 v 1.176735 h -9.203284 z m 0 4.83849 h 2.713696 v 1.176736 H 15.505565 Z M 13.556469 15.93316 a 0.67332766 0.67332766 0 0 0 -0.672794 0.672793 v 21.686339 a 0.67332766 0.67332766 0 0 0 0.672794 0.672793 h 7.585433 a 8.7739586 8.7739586 0 0 1 -0.02871 -0.672793 8.7739586 8.7739586 0 0 1 0.0335 -0.673751 H 14.230219 V 17.279703 h 14.983289 v 12.267233 a 8.7739586 8.7739586 0 0 1 0.67375 -0.02871 8.7739586 8.7739586 0 0 1 0.672794 0.0335 V 16.605953 A 0.67332766 0.67332766 0 0 0 29.887258 15.93316 Z" />
|
||||
</DrawingGroup.Children>
|
||||
@@ -917,7 +923,7 @@ M0,0L63.999001,0 63.999001,48.792999 0,48.792999z">
|
||||
<DrawingImage.Drawing>
|
||||
<DrawingGroup>
|
||||
<DrawingGroup.Children>
|
||||
<GeometryDrawing
|
||||
<GeometryDrawing
|
||||
Brush="Black"
|
||||
Geometry="m 24.706673 31.196264 -3.086181 3.086182 5.96548 5.963348 -5.96548 5.96548 3.086181 3.084049 5.963348 -5.963348 5.96548 5.963348 3.084049 -3.084049 -5.963347 -5.96548 5.963347 -5.963348 -3.084049 -3.086182 -5.96548 5.965481 z M 5.3507926 32.045363 H 17.81645 v 2.401501 H 5.3507926 Z m 0 -4.937233 H 24.132994 v 2.401501 H 5.3507926 Z M 26.434166 6.0031559 14.993666 17.443656 9.6399578 12.089946 6.0100982 15.719802 c 2.9622322 2.959826 6.0772448 6.092263 8.9969908 8.970151 L 30.064121 9.6329208 Z M 5.3507926 36.982596 h 5.5381524 v 2.401501 H 5.3507926 Z M 1.3730469 0 A 1.3741374 1.3741374 0 0 0 0 1.3730469 V 45.630859 a 1.3741374 1.3741374 0 0 0 1.3730469 1.373047 H 16.853516 a 17.906029 17.906029 0 0 1 -0.05859 -1.373047 17.906029 17.906029 0 0 1 0.06836 -1.375 H 2.7480469 V 2.7480469 H 33.326172 V 27.783203 a 17.906029 17.906029 0 0 1 1.375 -0.05859 17.906029 17.906029 0 0 1 1.373047 0.06836 V 1.3730469 A 1.3741374 1.3741374 0 0 0 34.701172 0 Z" />
|
||||
</DrawingGroup.Children>
|
||||
@@ -940,7 +946,7 @@ M0,0L63.999001,0 63.999001,48.792999 0,48.792999z">
|
||||
<DrawingImage.Drawing>
|
||||
<DrawingGroup>
|
||||
<DrawingGroup.Children>
|
||||
<GeometryDrawing
|
||||
<GeometryDrawing
|
||||
Brush="{mbts:BTSColor MarketData.bid}"
|
||||
Geometry="M 9.9417804 -0.02293875 1.9154138 8.0034278 17.4301 23.512569 1.9154138 39.027256 9.9417804 47.048077 25.450922 31.538936 40.965607 47.048077 48.986428 39.027256 33.47729 23.512569 48.986428 8.0034278 40.965607 -0.02293875 25.450922 15.491746 Z" />
|
||||
</DrawingGroup.Children>
|
||||
@@ -952,7 +958,7 @@ M0,0L63.999001,0 63.999001,48.792999 0,48.792999z">
|
||||
<DrawingImage.Drawing>
|
||||
<DrawingGroup>
|
||||
<DrawingGroup.Children>
|
||||
<GeometryDrawing
|
||||
<GeometryDrawing
|
||||
Brush="{mbts:BTSColor MarketData.ask}"
|
||||
Geometry="M 9.9417804 -0.02293875 1.9154138 8.0034278 17.4301 23.512569 1.9154138 39.027256 9.9417804 47.048077 25.450922 31.538936 40.965607 47.048077 48.986428 39.027256 33.47729 23.512569 48.986428 8.0034278 40.965607 -0.02293875 25.450922 15.491746 Z" />
|
||||
</DrawingGroup.Children>
|
||||
@@ -964,7 +970,7 @@ M0,0L63.999001,0 63.999001,48.792999 0,48.792999z">
|
||||
<DrawingImage.Drawing>
|
||||
<DrawingGroup>
|
||||
<DrawingGroup.Children>
|
||||
<GeometryDrawing
|
||||
<GeometryDrawing
|
||||
Brush="Black"
|
||||
Geometry="M 9.9417804 -0.02293875 1.9154138 8.0034278 17.4301 23.512569 1.9154138 39.027256 9.9417804 47.048077 25.450922 31.538936 40.965607 47.048077 48.986428 39.027256 33.47729 23.512569 48.986428 8.0034278 40.965607 -0.02293875 25.450922 15.491746 Z" />
|
||||
</DrawingGroup.Children>
|
||||
@@ -977,7 +983,7 @@ M0,0L63.999001,0 63.999001,48.792999 0,48.792999z">
|
||||
<DrawingImage.Drawing>
|
||||
<DrawingGroup>
|
||||
<DrawingGroup.Children>
|
||||
<GeometryDrawing
|
||||
<GeometryDrawing
|
||||
Brush="Black"
|
||||
Geometry="m 69.641861 87.129943 a 36.813842 36.813842 0 0 0 -36.813776 36.813767 36.813842 36.813842 0 0 0 31.044618 36.31355 l 1.208194 21.35839 1.804543 31.90967 a 41.092964 41.092964 0 0 0 -29.828154 39.50509 41.092964 41.092964 0 0 0 41.09258 41.0931 41.092964 41.092964 0 0 0 41.093104 -41.0931 41.092964 41.092964 0 0 0 -0.10284 -2.9068 41.092964 41.092964 0 0 0 -38.048841 -38.03902 l -2.962092 -52.36321 a 36.813842 36.813842 0 0 0 20.093347 -12.61939 l 33.150946 15.70705 v 5.3e-4 a 27.542416 27.542416 0 0 0 -0.12557 2.49907 27.542416 27.542416 0 0 0 27.54251 27.54249 27.542416 27.542416 0 0 0 27.5425 -27.5425 27.542416 27.542416 0 0 0 -27.5425 -27.5425 27.542416 27.542416 0 0 0 -22.44618 11.61479 l -31.44563 -14.89935 a 36.813842 36.813842 0 0 0 1.55701 -10.53786 36.813842 36.813842 0 0 0 -36.813769 -36.813767 z" />
|
||||
</DrawingGroup.Children>
|
||||
@@ -989,7 +995,7 @@ M0,0L63.999001,0 63.999001,48.792999 0,48.792999z">
|
||||
<DrawingImage.Drawing>
|
||||
<DrawingGroup>
|
||||
<DrawingGroup.Children>
|
||||
<GeometryDrawing
|
||||
<GeometryDrawing
|
||||
Brush="Black"
|
||||
Geometry="M 36.50044 86.999995 A 36.813842 36.813842 0 0 0 -0.3133356 123.81377 36.813842 36.813842 0 0 0 30.731282 160.12732 l 1.208194 21.35839 1.804543 31.90967 a 41.092964 41.092964 0 0 0 -29.8281546 39.50509 41.092964 41.092964 0 0 0 41.0925806 41.0931 41.092964 41.092964 0 0 0 41.093099 -41.0931 41.092964 41.092964 0 0 0 -0.102836 -2.9068 41.092964 41.092964 0 0 0 -38.04884 -38.03902 l -2.962092 -52.36321 a 36.813842 36.813842 0 0 0 20.093347 -12.61939 l 33.150948 15.70705 v 5.3e-4 a 27.542416 27.542416 0 0 0 -0.125574 2.49907 27.542416 27.542416 0 0 0 27.542513 27.54249 27.542416 27.542416 0 0 0 27.5425 -27.5425 27.542416 27.542416 0 0 0 -27.5425 -27.5425 27.542416 27.542416 0 0 0 -22.44618 11.61479 L 71.757201 134.35163 A 36.813842 36.813842 0 0 0 73.314213 123.81377 36.813842 36.813842 0 0 0 36.50044 86.999995 Z m 116.60981 115.628085 0.007 37.48967 h -37.49716 l 0.007 19.39257 h 37.48344 L 153.11751 297 h 19.38493 l 0.007 -37.48968 h 37.48342 L 210 240.11775 h -37.49727 l 0.007 -37.48967 z" />
|
||||
</DrawingGroup.Children>
|
||||
@@ -1014,7 +1020,7 @@ M0,0L63.999001,0 63.999001,48.792999 0,48.792999z">
|
||||
<DrawingImage.Drawing>
|
||||
<DrawingGroup>
|
||||
<DrawingGroup.Children>
|
||||
<GeometryDrawing
|
||||
<GeometryDrawing
|
||||
Brush="Black"
|
||||
Geometry="M 37.03498 86.999995 A 36.813842 36.813842 0 0 0 0.2212037 123.81377 36.813842 36.813842 0 0 0 31.265822 160.12732 l 1.208194 21.35839 1.804543 31.90967 a 41.092964 41.092964 0 0 0 -29.8281553 39.50509 41.092964 41.092964 0 0 0 41.0925813 41.0931 41.092964 41.092964 0 0 0 41.093099 -41.0931 41.092964 41.092964 0 0 0 -0.102836 -2.9068 41.092964 41.092964 0 0 0 -38.04884 -38.03902 l -2.962092 -52.36321 a 36.813842 36.813842 0 0 0 20.093347 -12.61939 l 33.150948 15.70705 v 5.3e-4 a 27.542416 27.542416 0 0 0 -0.125574 2.49907 27.542416 27.542416 0 0 0 27.542503 27.54249 27.542416 27.542416 0 0 0 27.5425 -27.5425 27.542416 27.542416 0 0 0 -27.5425 -27.5425 27.542416 27.542416 0 0 0 -22.44618 11.61479 L 72.291741 134.35163 A 36.813842 36.813842 0 0 0 73.848753 123.81377 36.813842 36.813842 0 0 0 37.03498 86.999995 Z m 145.03189 90.486735 -76.71431 76.71435 27.93311 27.93293 L 210 205.41986 Z m -0.008 22.13769 5.79522 5.79544 -54.57645 54.57644 -5.7952 -5.79522 z m -81.50016 59.76108 -10.071854 37.61451 37.606034 -10.0803 z" />
|
||||
</DrawingGroup.Children>
|
||||
@@ -1026,7 +1032,7 @@ M0,0L63.999001,0 63.999001,48.792999 0,48.792999z">
|
||||
<DrawingImage.Drawing>
|
||||
<DrawingGroup>
|
||||
<DrawingGroup.Children>
|
||||
<GeometryDrawing
|
||||
<GeometryDrawing
|
||||
Brush="Black"
|
||||
Geometry="m 139.80129 180.78831 a 20.783495 20.783495 0 0 0 -20.78345 20.78346 20.783495 20.783495 0 0 0 17.52643 20.50105 l 0.68209 12.05803 1.01877 18.01482 a 23.1993 23.1993 0 0 0 -16.83968 22.30284 23.1993 23.1993 0 0 0 23.1991 23.19938 23.1993 23.1993 0 0 0 23.19935 -23.19938 23.1993 23.1993 0 0 0 -0.058 -1.64105 23.1993 23.1993 0 0 0 -21.4807 -21.47518 l -1.67225 -29.56198 a 20.783495 20.783495 0 0 0 11.34383 -7.12437 l 18.71558 8.86752 v 3.1e-4 a 15.549251 15.549251 0 0 0 -0.0708 1.41086 15.549251 15.549251 0 0 0 15.5493 15.54927 15.549251 15.549251 0 0 0 15.54931 -15.54927 15.549251 15.549251 0 0 0 -15.54931 -15.54932 15.549251 15.549251 0 0 0 -12.67213 6.5572 l -17.75282 -8.41152 a 20.783495 20.783495 0 0 0 0.87902 -5.94921 20.783495 20.783495 0 0 0 -20.78345 -20.78346 z M 31.983713 88.265691 A 30.676234 30.676234 0 0 0 1.3075339 118.94187 30.676234 30.676234 0 0 0 27.17639 149.20122 l 1.006764 17.79752 1.503689 26.58969 A 34.241941 34.241941 0 0 0 4.8316412 226.50722 34.241941 34.241941 0 0 0 39.073263 260.74928 34.241941 34.241941 0 0 0 73.315316 226.50722 34.241941 34.241941 0 0 0 73.229625 224.08505 34.241941 34.241941 0 0 0 41.524291 192.3879 L 39.05604 148.75469 A 30.676234 30.676234 0 0 0 55.799422 138.2392 l 27.624018 13.08837 v 4.4e-4 a 22.950541 22.950541 0 0 0 -0.104638 2.08242 22.950541 22.950541 0 0 0 22.950618 22.9506 22.950541 22.950541 0 0 0 22.95062 -22.9506 22.950541 22.950541 0 0 0 -22.95062 -22.95061 22.950541 22.950541 0 0 0 -18.703946 9.67837 L 61.362463 127.72286 A 30.676234 30.676234 0 0 0 62.65989 118.94187 30.676234 30.676234 0 0 0 31.983713 88.265691 Z" />
|
||||
</DrawingGroup.Children>
|
||||
@@ -1150,7 +1156,7 @@ M0,0L63.999001,0 63.999001,48.792999 0,48.792999z">
|
||||
<DrawingGroup.Children>
|
||||
<GeometryDrawing
|
||||
Brush="Black"
|
||||
Geometry="M17.740002,11.999007C17.254002,11.999007,16.859001,12.394008,16.859001,12.879008L16.859001,24.458791 16.860127,24.479386C16.861003,24.607261 16.813003,24.735511 16.716002,24.834012 16.667502,24.883012 16.611378,24.91995 16.551394,24.944762L16.521562,24.955436 16.507633,24.960536 16.506465,24.960837 16.50574,24.961097 16.49194,24.964574 16.459728,24.972863C16.42719,24.979527,16.393502,24.983027,16.359002,24.983027L16.316763,24.980893 16.315716,24.980851 16.307903,24.980445C16.223895,24.971904,16.146014,24.942538,16.07951,24.897597L16.067752,24.887895 16.047276,24.873078 16.015234,24.844558 16.005502,24.836528 16.001236,24.831357 13.737002,22.600011C13.267003,22.134011 12.502003,22.135011 12.031003,22.600011 11.805003,22.82301 11.680003,23.120011 11.680003,23.436011 11.680003,23.751012 11.805003,24.048011 12.031003,24.272012L16.842003,29.031013C19.311003,31.189013 21.546002,31.119014 22.656003,30.886014 23.939002,30.614013 25.015002,29.846013 25.464002,28.880013 25.857001,28.036012 26.301002,27.080012 26.263001,26.195012 26.262002,26.178012 26.268003,26.074012 26.269002,26.056012L26.268003,20.974011C26.268003,20.55601 25.922002,20.215011 25.496001,20.215011 25.335502,20.215011 25.239001,20.246511 25.179877,20.283135L25.144,20.309762 25.144,21.215013C25.144,21.491012 24.919999,21.715013 24.644,21.715013 24.367999,21.715013 24.144,21.491012 24.144,21.215013L24.144,19.278017C24.144,18.793018 23.749,18.399019 23.263,18.399019 22.808312,18.399019 22.432727,18.745308 22.387555,19.18822L22.383004,19.277944 22.383004,19.716007C22.383004,19.992006 22.159003,20.216004 21.883002,20.216004 21.607003,20.216004 21.383002,19.992006 21.383002,19.716007L21.383002,19.278054 21.383002,18.504013C21.383002,18.018016 20.988002,17.623017 20.502002,17.623017 20.016002,17.623017 19.621001,18.018016 19.621001,18.504013L19.621001,20.24402C19.621001,20.52102 19.397001,20.74402 19.121001,20.74402 18.844001,20.74402 18.621001,20.52102 18.621001,20.24402L18.621001,18.504035 18.621001,12.879008C18.621001,12.394008,18.226001,11.999007,17.740002,11.999007z M17.740002,10.999005C18.777001,10.999005,19.621001,11.843007,19.621001,12.879008L19.621001,16.842839 19.687315,16.808824C19.933955,16.689783 20.210345,16.623022 20.502002,16.623022 21.247346,16.623022 21.892987,17.059031 22.197202,17.689331L22.211675,17.721288 22.212884,17.720384C22.513,17.517568 22.874499,17.399021 23.263,17.399021 24.267594,17.399021 25.091062,18.190155 25.141547,19.181478L25.143222,19.247425 25.174827,19.241074C25.276639,19.223799 25.383876,19.21501 25.496001,19.21501 26.473002,19.21501 27.268003,20.00401 27.268003,20.974011L27.268003,26.087012C27.268003,26.098012 27.262002,26.194013 27.262002,26.205012 27.311002,27.281013 26.790002,28.401013 26.371001,29.302012 25.783002,30.566012 24.472001,31.523013 22.863002,31.864013 22.513001,31.938014 22.067002,31.999013 21.547002,31.999013 20.149003,31.999013 18.216002,31.561013 16.161002,29.763013L11.328003,24.983012C10.910003,24.570012 10.680003,24.020011 10.680003,23.436011 10.680003,22.85101 10.910003,22.302011 11.328003,21.88901 12.187003,21.040011 13.583003,21.041011 14.440002,21.88901L15.859002,23.287391 15.859002,12.879008C15.859002,11.843007,16.703002,10.999005,17.740002,10.999005z M6.8659983,4.0500388L19.083997,4.0500388C20.668996,4.0500388,21.957997,5.3460296,21.957997,6.9390478L21.957997,14.653042C21.957997,14.93005 21.734998,15.153042 21.457997,15.153042 21.181996,15.153042 20.957997,14.93005 20.957997,14.653042L20.957997,6.9390478C20.957997,5.8980318,20.116998,5.0500394,19.083997,5.0500394L6.8659983,5.0500394C5.8319979,5.0500394,4.9909983,5.8980318,4.9909984,6.9390478L4.9909984,26.116061C4.9909983,27.157046,5.8319979,28.00504,6.8659983,28.00504L11.696996,28.00504C11.972997,28.00504 12.196996,28.229039 12.196996,28.50504 12.196996,28.781041 11.972997,29.00504 11.696996,29.00504L6.8659983,29.00504C5.2809954,29.00504,3.9909983,27.70905,3.9909983,26.116061L3.9909983,6.9390478C3.9909983,5.3460296,5.2809954,4.0500388,6.8659983,4.0500388z M6.4360008,0L17.467001,0C17.743001,0 17.967001,0.22300029 17.967001,0.50000024 17.967001,0.77600098 17.743001,1.0000012 17.467001,1.0000012L6.4360008,1.0000012C3.4380004,1.0000012,1.0000002,3.4210042,1.0000002,6.3970079L1.0000002,20.617026C1.0000002,20.893025 0.77600026,21.117026 0.50000048,21.117026 0.22400022,21.117026 2.3841858E-07,20.893025 0,20.617026L0,6.3970079C2.3841858E-07,2.8700035,2.8870004,0,6.4360008,0z"
|
||||
Geometry="M 0.814453 2e-6 C 0.365092 -7.56e-4 3.21e-4 0.36314 0 0.812502 v 23.945313 c 0.788895 1.126154 1.626953 -0.02112 1.626953 -0.02112 V 1.625002 h 16.241843 c 0 0 0.80538 -0.934678 -0.124651 -1.625002 z M 3.750875 3.43624 C 3.301514 3.435482 2.936743 3.799378 2.936422 4.24874 v 23.945313 c -7.6e-4 0.450124 0.364329 0.815213 0.814453 0.814453 h 7.439453 C 11.042745 28.716852 10.90913 28.418337 10.789937 28.113975 10.699431 27.872971 10.617994 27.628659 10.545796 27.381553 H 4.563375 V 5.06124 h 15.304687 v 7.039063 c 0.173002 -0.01023 0.346234 -0.01609 0.519531 -0.01758 0.343731 -2.6e-4 0.68726 0.01669 1.029297 0.05078 0.02606 0.0038 0.0521 0.0077 0.07813 0.01172 V 4.248738 C 21.494699 3.799376 21.129928 3.43548 20.680567 3.436238 Z m 13.332031 6.884766 c -1.226661 0 -2.230469 1.002456 -2.230469 2.228516 v 9.572265 l -0.824219 -0.8125 v 0.002 c -0.991917 -0.9815 -2.60964 -0.982302 -3.603515 0 -0.48293 0.477155 -0.751953 1.119962 -0.751953 1.796875 0 0.67607 0.269215 1.317958 0.751953 1.794922 l 4.841797 4.78711 0.0078 0.0078 c 2.115591 1.851014 4.144077 2.322266 5.615234 2.322266 0.547268 0 1.017683 -0.06414 1.388672 -0.142578 1.701281 -0.360558 3.108561 -1.372782 3.751954 -2.75586 0.419048 -0.9011 0.978455 -2.062137 0.923828 -3.261718 4.09e-4 -0.01025 0.0011 -0.0075 0.002 -0.02344 0.0015 -0.02655 0.0039 -0.01047 0.0039 -0.07813 v -5.113281 c 0 -1.161114 -0.955476 -2.109375 -2.121094 -2.109375 -0.04803 0 -0.09044 0.0088 -0.136719 0.01172 -0.207784 -1.015547 -1.021531 -1.826172 -2.095703 -1.826172 -0.343265 0 -0.63914 0.145124 -0.93164 0.285156 -0.397916 -0.616533 -1.045784 -1.0625 -1.830078 -1.0625 -0.189487 0 -0.354735 0.07615 -0.53125 0.121094 v -3.515625 c 0 -1.22606 -1.003809 -2.228516 -2.230469 -2.228516 z m 0 1.699219 c 0.148476 0 0.278642 0.05997 0.375 0.15625 0.09685 0.09678 0.15625 0.225142 0.15625 0.373047 v 5.625 1.740234 c 0 0.464846 0.384763 0.849609 0.849609 0.849609 0.464527 0 0.84961 -0.383921 0.84961 -0.849609 v -1.740234 c 0 -0.148598 0.05896 -0.275762 0.15625 -0.373047 0.09729 -0.09729 0.228277 -0.158203 0.375 -0.158203 0.146722 0 0.277709 0.06091 0.375 0.158203 0.09729 0.09729 0.15625 0.224449 0.15625 0.373047 v 0.77539 0.4375 c 0 0.465373 0.384236 0.84961 0.849609 0.84961 0.465373 0 0.849609 -0.384239 0.849609 -0.84961 v -0.425781 l 0.0059 -0.06836 c 0.0286 -0.265146 0.249414 -0.472657 0.52539 -0.472657 0.148238 0 0.278481 0.05996 0.375 0.15625 0.09649 0.09627 0.15625 0.225145 0.15625 0.373047 v 1.935547 c 0 0.465371 0.384237 0.84961 0.84961 0.84961 0.465372 0 0.849609 -0.384237 0.849609 -0.84961 v -0.65039 c 0.0016 -9e-6 2.95e-4 0 0.002 0 0.238806 0 0.421875 0.185008 0.421875 0.410156 l 0.002 5.0625 c -4.64e-4 0.0084 -0.0023 0.0401 -0.0039 0.07227 -0.0015 0.03041 -0.0059 0.02687 -0.002 0.101563 0.03253 0.757506 -0.372777 1.677807 -0.765625 2.521484 h -0.002 c -0.194342 0.418116 -0.532821 0.810028 -0.976563 1.126953 -0.443595 0.316821 -0.990203 0.55925 -1.585937 0.685547 -0.515602 0.10823 -1.298166 0.179402 -2.248047 -0.03125 -0.47546 -0.105441 -0.991926 -0.281044 -1.539063 -0.560547 -0.547306 -0.279588 -1.127683 -0.66381 -1.724609 -1.185547 L 11.620061 23.69601 c -0.162198 -0.160763 -0.248047 -0.365899 -0.248047 -0.587891 0 -0.223284 0.08648 -0.426518 0.248047 -0.585937 0.168295 -0.166151 0.386826 -0.249911 0.607422 -0.25 0.220595 -9e-5 0.437432 0.08144 0.605469 0.248047 l 2.240234 2.207031 0.01367 0.02148 0.04687 0.03516 0.03711 0.0332 0.02539 0.01758 0.01563 0.01367 0.01563 0.0098 c 0.114637 0.07747 0.248043 0.126397 0.388672 0.140625 l -0.04883 -0.0078 0.04102 0.0098 0.08594 0.0039 h 0.0078 c 0.06844 0 0.126002 -0.0085 0.169922 -0.01758 l 0.0059 -0.002 0.01953 -0.0039 h 0.02539 l 0.06641 -0.02734 0.02734 -0.0098 0.0098 -0.0039 c 0.102105 -0.04223 0.198238 -0.105586 0.28125 -0.189454 h 0.002 c 0.163006 -0.165519 0.243191 -0.385955 0.242188 -0.599609 v -0.002 -0.01953 -11.580078 c 0 -0.147904 0.0594 -0.276271 0.15625 -0.373047 0.09636 -0.09628 0.226522 -0.15625 0.375 -0.15625 z"
|
||||
/>
|
||||
</DrawingGroup.Children>
|
||||
</DrawingGroup>
|
||||
@@ -1158,10 +1164,10 @@ M0,0L63.999001,0 63.999001,48.792999 0,48.792999z">
|
||||
</DrawingImage>
|
||||
|
||||
|
||||
<!-- Pin
|
||||
<!-- Pin
|
||||
<Path Data="F1M3,11L3,7 0,7 0,6 1,6 1,1 1,0 6,0 6,1 6,6 7,6 7,7 4,7 4,11 3,11z M2,1L2,6 4,6 4,1 2,1z" />
|
||||
Unpin
|
||||
Unpin
|
||||
<Path Data="F1M0,3L4,3 4,0 5,0 5,1 10,1 11,1 11,6 10,6 5,6 5,7 4,7 4,4 0,4 0,3z M10,2L5,2 5,4 10,4 10,2z" />
|
||||
-->
|
||||
|
||||
</ResourceDictionary>
|
||||
</ResourceDictionary>
|
||||
@@ -19,6 +19,8 @@ namespace mbts.spark {
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
public static AlgoData empty => from_custom_def(new CustomOrderDefinitionImmutable(new CustomOrderDefinition()));
|
||||
|
||||
/// <summary>
|
||||
/// This is the only <see cref="AlgoData"/> factory method that allows callers to explicitly specify a name.
|
||||
/// It should only be used within the context of creating, or renaming, studio algos.
|
||||
@@ -65,6 +67,8 @@ namespace mbts.spark {
|
||||
}
|
||||
|
||||
struct AlgoDataVM : IEquatable<AlgoDataVM> {
|
||||
public static readonly Func<AlgoDataVM, AlgoID> id_fn = vm => AlgoID.from_algo_key(vm.data.key);
|
||||
|
||||
public readonly AlgoData data;
|
||||
public readonly AlgoDefinitionMetadata? meta;
|
||||
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ namespace mbts.ui.spark {
|
||||
class AlgoParamTemplateSelector : DataTemplateSelector {
|
||||
public static readonly Type generic_algo_param_itype = typeof(IAlgoParamVM<>);
|
||||
|
||||
KeyedCollection<TemplateSelectorKeyValue, string> _keyed_templates = new KeyedCollection<TemplateSelectorKeyValue, string>(tsk => tsk.Key);
|
||||
KeyedCollection<TemplateSelectorKeyValue, string> _keyed_templates = new KeyedCollection<TemplateSelectorKeyValue, string>(tsk => tsk.Key.ToString());
|
||||
|
||||
public DataTemplate BoolTemplate { get; set; }
|
||||
public DataTemplate NullableBoolTemplate { get; set; }
|
||||
|
||||
@@ -68,13 +68,51 @@ namespace mbts.spark {
|
||||
nameof(OrderBookRowVM.symbol)),
|
||||
DataGridFilterKnownTemplates.DiscreteValues,
|
||||
DefaultColumnWidths.Symbol);
|
||||
// algo items view (common resources)
|
||||
var algo_items_comp = AlgoCollectionHelper.algo_data_vm_comparer.to_non_generic();
|
||||
var algo_items_group_desc = new PropertyGroupDescription(nameof(AlgoDataVM.group));
|
||||
// order type
|
||||
var order_type_filter = dv_filter(OrderDataHelper.all_order_types, r => r.order?.order_type() ?? OrderType.None);
|
||||
var order_type_filter = DiscreteValuesFilterVM.CreateTableUpdate<FillTuple, AlgoDataVM, AlgoID, OrderType[]>(
|
||||
ft => new AlgoDataVM(ft.algo ?? AlgoData.empty),
|
||||
AlgoDataVM.id_fn,
|
||||
ots => Array.ConvertAll(ots ?? Array.Empty<OrderType>(), ot => AlgoID.from_algo_key(AlgoKey.from_known_type(ot))),
|
||||
ids => ids.empty_if_null().Select(AlgoID.to_algo_key).Where(k => k.mode == AlgoKey.Mode.Factory).Select(k => k.factory).ToArray(),
|
||||
out var order_type_observer,
|
||||
out var order_type_items_view);
|
||||
|
||||
order_type_items_view.CustomSort = algo_items_comp;
|
||||
order_type_items_view.GroupDescriptions.Add(algo_items_group_desc);
|
||||
order_type_observer.OnNext(TableUpdate<AlgoDataVM, AlgoID>.mk_empty(AlgoDataVM.id_fn)
|
||||
.with_added(OrderDataHelper.all_order_types
|
||||
.Select(OrderDataHelper.Defaults.CustomOrderDefinitionImmutable)
|
||||
.Select(AlgoData.from_custom_def)
|
||||
.Select(d => new AlgoDataVM(d))));
|
||||
|
||||
add_col_expl(Columns.OrderType,
|
||||
new OrderTypeColumn<Columns>(Columns.OrderType, new Binding(nameof(Row.badge_fill)), new Binding(nameof(Row.display_name)), order_type_filter) {
|
||||
id = col_id(Columns.OrderType),
|
||||
sort_member_path = nameof(Row.order_type)
|
||||
}, order_type_filter);
|
||||
// root algo type
|
||||
var root_algo_type_filter = DiscreteValuesFilterVM.CreateTableUpdate<FillTuple, AlgoDataVM, AlgoID>(
|
||||
ft => new AlgoDataVM(ft.root_algo ?? AlgoData.empty),
|
||||
AlgoDataVM.id_fn,
|
||||
out var root_algo_type_observer,
|
||||
out var root_algo_type_items_view);
|
||||
|
||||
root_algo_type_items_view.CustomSort = algo_items_comp;
|
||||
root_algo_type_items_view.GroupDescriptions.Add(algo_items_group_desc);
|
||||
|
||||
add_col_expl(Columns.RootAlgoType,
|
||||
new OrderTypeColumn<Columns>(
|
||||
Columns.RootAlgoType,
|
||||
new Binding(nameof(Row.root_algo_badge_color)) { Converter = new ui.converters.ColorToBrushConverter() },
|
||||
new Binding(nameof(Row.root_algo_display_name)),
|
||||
root_algo_type_filter,
|
||||
DefaultColumnHeaders.RootAlgoType) {
|
||||
id = col_id(Columns.RootAlgoType),
|
||||
sort_member_path = nameof(Row.root_algo_order_type)
|
||||
}, root_algo_type_filter);
|
||||
// side
|
||||
var side_filter = dv_filter(new[] { Side.Buy, Side.Sell }, r => r.fill.Side);
|
||||
add_col_expl(Columns.Side, new SideColumn<Row, Columns>(Columns.Side, r => r.side, side_filter) { id = col_id(Columns.Side) }, side_filter);
|
||||
@@ -128,6 +166,7 @@ namespace mbts.spark {
|
||||
};
|
||||
_rows_manager.on_settings(fs, s.sorting_criterion);
|
||||
}),
|
||||
model.subscribe_all_algos().SelectKeyValue(AlgoDataVM.from_data, d => AlgoID.from_algo_key(d.data.key)).OnDispatcher().Subscribe(root_algo_type_observer),
|
||||
settings_manager == null ? Disposable.empty : settings_manager.subscribe_current().OnDispatcher().Subscribe(grid_settings_manager)
|
||||
);
|
||||
// local methods
|
||||
@@ -175,8 +214,8 @@ namespace mbts.spark {
|
||||
static IObservable<TableUpdate<FillTuple, string>> _get_observable(IBus bus, OrdersModel model, FillsSettings state) {
|
||||
if (!state.orders.is_null_or_empty()) {
|
||||
var orders_hash = new HashSet<uint>(state.orders);
|
||||
return model.on_fill_tuples.Filter(f => orders_hash.Contains(f.fill.OrderKey));
|
||||
} else return model.on_fill_tuples;
|
||||
return model.on_fill_tuples_with_root.Filter(f => orders_hash.Contains(f.fill.OrderKey));
|
||||
} else return model.on_fill_tuples_with_root;
|
||||
}
|
||||
|
||||
static IObservable<TableUpdate<FillTuple, string>> _get_observable(IBus bus, OrdersModel model, IObservable<FillsSettings> state) =>
|
||||
@@ -191,6 +230,7 @@ namespace mbts.spark {
|
||||
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);
|
||||
case Columns.RootAlgoType: return wrap_default(ft => ft.root_algo.HasValue ? ft.root_algo.Value.value.CustomCase : OrderType.None);
|
||||
case Columns.Price: return wrap(ft => ft.fill.LastPrice);
|
||||
case Columns.Security: return wrap(ft => ft.security ?? SecurityDataHelper.Defaults.SecurityImmutable);
|
||||
case Columns.SecurityKey: return wrap(ft => ft.fill.SecurityKey);
|
||||
@@ -224,7 +264,10 @@ namespace mbts.spark {
|
||||
readonly OrdersModel _model;
|
||||
readonly FillImmutable _fill;
|
||||
readonly OrderRecordImmutable _order;
|
||||
AlgoData? _root_algo => fill_tuple.root_algo;
|
||||
AlgoDefinitionMetadata? _root_algo_meta => fill_tuple.root_algo_meta;
|
||||
Brush _badge_fill;
|
||||
Color? _root_algo_badge_color;
|
||||
OrderMetadata _order_metadata;
|
||||
|
||||
public Row(FillTuple fill_tuple, OrdersModel model) :
|
||||
@@ -239,6 +282,7 @@ namespace mbts.spark {
|
||||
public DateTime fill_time => _fill.Timestamp.to_local_time();
|
||||
public TimeInForce time_in_force => _order.time_in_force();
|
||||
public OrderType order_type => _order.order_type();
|
||||
//public OrderType root_algo_type => _or
|
||||
public string display_name => OrderUtils.display_name(_order, fill_tuple.algo);
|
||||
public OrderMetadata order_metadata => _order_metadata ?? (_order_metadata = _model.factory.get_metadata(_order.Def.CustomDef.CustomCase));
|
||||
public Brush badge_fill => _badge_fill ?? (_badge_fill = OrderUtils.badge_fill(fill_tuple.algo_meta, order_metadata));
|
||||
@@ -252,6 +296,12 @@ namespace mbts.spark {
|
||||
public string fill_key => _fill.FillKey;
|
||||
public string order_key => _order.Key.ToString();
|
||||
|
||||
// Root Algo
|
||||
public OrderType root_algo_order_type => _root_algo.HasValue ? _root_algo.Value.value.CustomCase : OrderType.None;
|
||||
public OrderMetadata root_algo_order_metadata => _model.factory.get_metadata(root_algo_order_type);
|
||||
public Color root_algo_badge_color => _root_algo_badge_color ?? (_root_algo_badge_color = OrderUtils.badge_color(_root_algo_meta, root_algo_order_metadata)).Value;
|
||||
public string root_algo_display_name => _root_algo?.name ?? root_algo_order_type.display_name();
|
||||
|
||||
public bool Equals(Row other) => other != null && other._fill.Equals(_fill) && other._order.Equals(_order);
|
||||
public override bool Equals(object obj) => Equals(obj as Row);
|
||||
public override int GetHashCode() => _fill.FillKey.GetHashCode() ^ _order.Key.GetHashCode();
|
||||
|
||||
@@ -16,6 +16,7 @@ namespace mbts.ui.spark {
|
||||
Symbol,
|
||||
TimeInForce,
|
||||
OrderType,
|
||||
RootAlgoType,
|
||||
Side,
|
||||
Price,
|
||||
Size,
|
||||
|
||||
@@ -1,19 +1,15 @@
|
||||
using bts.api;
|
||||
using bts.api.orderman;
|
||||
using bts.settings;
|
||||
using bts.shell.fills;
|
||||
using mbts.persist;
|
||||
using mbts.spark;
|
||||
using mbts.spark.filters;
|
||||
using mbts.ui;
|
||||
using mbts.ui.controls;
|
||||
using mbts.ui.settings;
|
||||
using ProtoBuf;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.ComponentModel.Composition;
|
||||
using System.Reactive.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using static mbts.ui.controls.DIYGrid;
|
||||
using static mbts.ui.spark.FillsView;
|
||||
|
||||
namespace bts.shell {
|
||||
@@ -34,6 +30,7 @@ namespace bts.shell {
|
||||
{ nameof(FillsVM.Row.fill_time), Columns.FillTime },
|
||||
{ nameof(FillsVM.Row.order_key), Columns.OrderKey },
|
||||
{ nameof(FillsVM.Row.order_type), Columns.OrderType },
|
||||
{ nameof(FillsVM.Row.root_algo_order_type), Columns.RootAlgoType },
|
||||
{ nameof(FillsVM.Row.price), Columns.Price },
|
||||
{ nameof(FillsVM.Row.security), Columns.Security },
|
||||
{ nameof(FillsVM.Row.security_key), Columns.SecurityKey },
|
||||
@@ -67,5 +64,12 @@ namespace bts.shell {
|
||||
set: (us, gs) => us.with(provided_fills_gridsettings: gs));
|
||||
return new FillsWindow(this, _bus, _orders_model, state, settings_manager);
|
||||
}
|
||||
|
||||
// migration rules
|
||||
[Export(typeof(IFillsGridSettingsMigrationRule))]
|
||||
class AddMissingColumnsMigrationRule : IFillsGridSettingsMigrationRule {
|
||||
public bool apply(GridSettings settings, out GridSettings new_settings) =>
|
||||
settings.add_missing_columns(FillsSettings.DefaultGridSettings.columns, out new_settings);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,10 @@ namespace bts.shell.fills {
|
||||
[DIYGridColumnID(nameof(Columns.OrderType))]
|
||||
public CustomOrderDefinition.CustomOneofCase[] order_type { get; set; }
|
||||
|
||||
[ProtoMember(16)]
|
||||
[DIYGridColumnID(nameof(Columns.RootAlgoType))]
|
||||
public AlgoID[] root_algo_type { get; set; }
|
||||
|
||||
[ProtoMember(4)]
|
||||
[DIYGridColumnID(nameof(Columns.Account))]
|
||||
public string[] account { get; set; }
|
||||
@@ -88,6 +92,7 @@ namespace bts.shell.fills {
|
||||
pb.add_discrete_values(fs.exchange, ft => ft.exchange());
|
||||
pb.add_discrete_values(fs.time_in_force, ft => ft.time_in_force());
|
||||
pb.add_discrete_values(fs.order_type, ft => ft.order_type());
|
||||
pb.add_discrete_values(fs.root_algo_type, ft => AlgoID.from_algo_key((ft.root_algo ?? AlgoData.empty).key));
|
||||
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);
|
||||
|
||||
@@ -47,6 +47,7 @@ namespace bts.shell.fills {
|
||||
public static GridColumnSettings exchange => new GridColumnSettings(col_id(Columns.Exchange), DefaultColumnHeaders.Exchange, width: DefaultColumnWidths.Exchange);
|
||||
public static GridColumnSettings time_in_force => new GridColumnSettings(col_id(Columns.TimeInForce), DefaultColumnHeaders.TimeInForce, width: DefaultColumnWidths.TimeInForce);
|
||||
public static GridColumnSettings order_type => new GridColumnSettings(col_id(Columns.OrderType), DefaultColumnHeaders.OrderType, width: DefaultColumnWidths.OrderType);
|
||||
public static GridColumnSettings root_algo_type => new GridColumnSettings(col_id(Columns.RootAlgoType), DefaultColumnHeaders.RootAlgoType, width: DefaultColumnWidths.OrderType);
|
||||
public static GridColumnSettings side => new GridColumnSettings(col_id(Columns.Side), DefaultColumnHeaders.Side, width: DefaultColumnWidths.Side);
|
||||
public static GridColumnSettings quantity => new GridColumnSettings(col_id(Columns.Size), DefaultColumnHeaders.Size, width: DefaultColumnWidths.Size, horizontal_alignment: System.Windows.HorizontalAlignment.Right);
|
||||
public static GridColumnSettings price => new GridColumnSettings(col_id(Columns.Price), DefaultColumnHeaders.Price, width: DefaultColumnWidths.Price, horizontal_alignment: System.Windows.HorizontalAlignment.Right);
|
||||
@@ -70,6 +71,7 @@ namespace bts.shell.fills {
|
||||
yield return exchange;
|
||||
yield return time_in_force;
|
||||
yield return order_type;
|
||||
yield return root_algo_type;
|
||||
yield return side;
|
||||
yield return quantity;
|
||||
yield return price;
|
||||
|
||||
@@ -15,7 +15,6 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Reactive.Subjects;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using LadderSettings = mbts.spark.LadderSettings;
|
||||
@@ -30,13 +29,14 @@ namespace bts.shell {
|
||||
readonly MBOService _mbo;
|
||||
readonly UserSettingsHub _settings_hub;
|
||||
readonly OrdersModel _orders_model;
|
||||
readonly IShellWindowThreadIDProvider _thread_id_provider;
|
||||
IReadOnlyDictionary<uint, SecurityMD> _securities = new Dictionary<uint, SecurityMD>();
|
||||
|
||||
public LadderWindowFactory(
|
||||
IShellModule owner,
|
||||
IBus bus,
|
||||
IWindowManager wm,
|
||||
UserSettingsHub settings_hub,
|
||||
IShellModule owner,
|
||||
IBus bus,
|
||||
IWindowManager wm,
|
||||
UserSettingsHub settings_hub,
|
||||
OrdersModel orders_model,
|
||||
MBOService mbo)
|
||||
|
||||
@@ -46,6 +46,7 @@ namespace bts.shell {
|
||||
_settings_hub = settings_hub;
|
||||
_orders_model = orders_model;
|
||||
_ladder_conf = new LadderConfig(Configuration.get<bts.config.LadderKnobs>(), Configuration.get<bts.config.MultiThreadedWindows>().ladder);
|
||||
_thread_id_provider = ShellWindowThreadIDFactory.create("Ladder", _ladder_conf.ladders_on_threads);
|
||||
_mbo = mbo;
|
||||
_dispose = Disposable.Create(
|
||||
bus.subscribe(SparkTags.SecuritiesTag).Subscribe(s => _securities = s),
|
||||
@@ -88,10 +89,8 @@ namespace bts.shell {
|
||||
return new LadderSaveState { security_key = vm.security.Id, settings = s, settings_id = id };
|
||||
}
|
||||
|
||||
int _ladder_count = 0;
|
||||
IShellWindow create_ladder_window(SecurityMD md, LadderSettings last_value, uint settings_id = 0) {
|
||||
IDisposable active_settings_dialog = null;
|
||||
var thread_id = Interlocked.Increment(ref _ladder_count) % Environment.ProcessorCount;
|
||||
// on_shell_window_vm should behave like an event (do not set to null)
|
||||
Action<ShellWindowVM> on_shell_window_vm = _ => { };
|
||||
ShellWindowVM shell_window_vm = null;
|
||||
@@ -263,12 +262,11 @@ namespace bts.shell {
|
||||
Log.Ui.warn($"LadderSettingsDialog error {action_desc}: {error}.");
|
||||
}
|
||||
});
|
||||
string thread = _ladder_conf.ladders_on_threads ? $"Ladder{thread_id:00}" : "";
|
||||
return shell_window = ShellWindow.create(
|
||||
module, md.security.ToString(), _window_type, view,
|
||||
(v, s) => save(_get_state(((LadderView)v).vm, settings_id), s),
|
||||
icon: Icons.Ladder,
|
||||
thread_id: thread,
|
||||
thread_id: _thread_id_provider.next_id(),
|
||||
vm => {
|
||||
shell_window_vm = vm;
|
||||
title_source.connect();
|
||||
@@ -313,4 +311,4 @@ namespace bts.shell {
|
||||
[ProtoMember(3)]
|
||||
public uint settings_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -137,7 +137,7 @@ namespace mbts.spark {
|
||||
foreach (var c in _observable_components)
|
||||
c.PropertyChanged -= on_prop_changed;
|
||||
}
|
||||
AccountTuple? account_factory(KeyValuePair<uint, string> kv) => new AccountTuple(kv.Key, kv.Value);
|
||||
AccountTuple? account_factory(uint key, string name) => new AccountTuple(key, name);
|
||||
AlgoDefinitionVM algo_factory(AlgoData data) => AlgoDefinitionVM.create(_model, data, is_synthetic ? AlgoFormats.Spreader : AlgoFormats.Exchange);
|
||||
}
|
||||
|
||||
|
||||
@@ -451,7 +451,7 @@ namespace mbts.spark {
|
||||
price = temp_price;
|
||||
}
|
||||
}
|
||||
await _model.order_gateway.send_order(order, price, size);
|
||||
await _model.order_gateway.send_order(security, order, price, size);
|
||||
if (auto_clear)
|
||||
size_calculator.value = 0;
|
||||
var fav_algo = order_properties_vm.selected_favorite_algo_definition;
|
||||
|
||||
@@ -79,8 +79,8 @@ namespace bts.shell.manual_fills {
|
||||
|
||||
public ManualFillsModel(OrdersModel model)
|
||||
: this(model, model.on_manual_fill_tuples,
|
||||
model.factory.subscribe_accounts().Select(d => MappedDictionary.create(d, kv => new AccountTuple(kv.Key, kv.Value))).Table(u => u.id),
|
||||
model.factory.subscribe_users().Select(d => MappedDictionary.create(d, kv => new UserTuple(kv.Key, kv.Value))).Table(a => a.id)) { }
|
||||
model.factory.subscribe_accounts().Select(d => MappedDictionary.create(d, (k, v) => new AccountTuple(k, v))).Table(u => u.id),
|
||||
model.factory.subscribe_users().Select(d => MappedDictionary.create(d, (k, v) => new UserTuple(k, v))).Table(a => a.id)) { }
|
||||
|
||||
public ManualFillsModel(OrdersModel model, IObservable<TableUpdate<ManualFillTuple, int>> manual_fills, IObservable<TableUpdate<AccountTuple, uint>> accounts, IObservable<TableUpdate<UserTuple, uint>> users) {
|
||||
_model = model;
|
||||
|
||||
@@ -73,7 +73,7 @@ namespace bts.shell.manual_fills {
|
||||
// The security desc lookup could be better optimized, but the speed at which fills are imported is probably not a paramount concern.
|
||||
public static IEnumerable<ParsedManualFill> parse_rows(IEnumerable<string> rows, IReadOnlyDictionary<uint, SecurityImmutable> security_map, IReadOnlyDictionary<uint, string> account_map, IReadOnlyDictionary<uint, string> user_map) =>
|
||||
parse_rows(rows,
|
||||
reverse_lookup(MappedDictionary.create(security_map, kv => kv.Value.ToString())),
|
||||
reverse_lookup(MappedDictionary.create(security_map, s => s.ToString())),
|
||||
reverse_lookup(account_map),
|
||||
reverse_lookup(user_map));
|
||||
|
||||
|
||||
@@ -157,7 +157,7 @@ namespace bts.shell.manual_fills {
|
||||
var lines = File.ReadAllLines(file_name);
|
||||
var parsed_fills = ManualFillsParser.parse_rows(
|
||||
lines,
|
||||
MappedDictionary.create(security_map, kv => kv.Value.security),
|
||||
MappedDictionary.create(security_map, smd => smd.security),
|
||||
accounts.ToDictionary(a => a.Value.id, a => a.Value.name),
|
||||
users.ToDictionary(u => u.Value.id, u => u.Value.name)
|
||||
).ToList();
|
||||
|
||||
@@ -15,10 +15,11 @@ namespace mbts.spark {
|
||||
CVRowStyleKey = "CVRowStyle",
|
||||
CVRowTemplateKey = "CVRowTemplateKey";
|
||||
|
||||
public MiniOrderBookView(OrderBookVM vm) {
|
||||
public MiniOrderBookView() =>
|
||||
InitializeComponent();
|
||||
|
||||
public MiniOrderBookView(OrderBookVM vm) : this() =>
|
||||
DataContext = vm;
|
||||
}
|
||||
}
|
||||
|
||||
class MiniOrderBookRowView : DIYGrid.HighPerformanceRow<OrderBookRowVM, OrderBookGridView.ColumnKeys> {
|
||||
|
||||
@@ -327,6 +327,18 @@ namespace mbts.spark {
|
||||
|
||||
public event Action filter_changed;
|
||||
|
||||
public static IEnumerable<ColumnBase> editable_columns {
|
||||
get {
|
||||
yield return new AttachDetachColumn(typeof(MiniOrderBookView));
|
||||
yield return new CommitColumn();
|
||||
yield return new EditablePriceColumn();
|
||||
yield return new EditableQtyColumn();
|
||||
yield return new UpdateColumn(typeof(MiniOrderBookView));
|
||||
yield return new CancelColumn(nameof(OrderBookRowVM.cancel_command));
|
||||
yield return new CheckColumn(nameof(OrderBookRowVM.is_selected));
|
||||
}
|
||||
}
|
||||
|
||||
public SecurityColumn security_column { get; }
|
||||
public SimpleTextColumn<string[]> symbol_column { get; }
|
||||
public SimpleTextColumn<TimeFilterTuple> time_created_column { get; }
|
||||
@@ -731,10 +743,14 @@ namespace mbts.spark {
|
||||
Width = 30;
|
||||
_checked_binding = new Binding(checked_property);
|
||||
|
||||
var header_cb = new CheckBox();
|
||||
header_cb.ToolTip = "Select / Unselect All";
|
||||
var header_cb = new CheckBox {
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
ToolTip = "Select / Unselect All"
|
||||
};
|
||||
header_cb.SetBinding(CheckBox.IsCheckedProperty, new Binding(nameof(OrderBookVM.all_rows_selected)));
|
||||
this.Header = header_cb;
|
||||
is_visible = true;
|
||||
}
|
||||
|
||||
public override FrameworkElement view_factory() {
|
||||
@@ -1164,6 +1180,7 @@ namespace mbts.spark {
|
||||
|
||||
public override FrameworkElement view_factory() {
|
||||
var price_up_down = new PriceEntryCommitControl();
|
||||
price_up_down.SetBinding(PriceEntryCommitControl.RealPriceProperty, new Binding(nameof(OrderBookRowVM.price)));
|
||||
price_up_down.SetBinding(PriceEntryCommitControl.SourceValueProperty, nameof(OrderBookRowVM.prospective_price));
|
||||
price_up_down.SetBinding(PriceEntryCommitControl.SourceContractSpecProperty, nameof(OrderBookRowVM.contract_spec));
|
||||
price_up_down.SetBinding(PriceEntryCommitControl.OnCommitProperty, nameof(OrderBookRowVM.modify_order_quantity_andor_price));
|
||||
|
||||
@@ -69,10 +69,15 @@ namespace mbts.spark {
|
||||
_detachable_selected_orders = new HashSet<uint>();
|
||||
FilterData _cached_filter_data;
|
||||
|
||||
public OrderBookVM(
|
||||
IBus bus, INotificationService notification_service, OrdersModel model, IWindowSettingsManager<OrderBookGridSettings> settings_manager, bool rows_are_initially_expanded = false)
|
||||
: this(bus, notification_service, model, OrdersModel.piq_enabled ? model.on_order_tuples_with_root_and_piq : model.on_order_tuples_with_root, settings_manager, rows_are_initially_expanded) { }
|
||||
|
||||
public OrderBookVM(
|
||||
IBus bus,
|
||||
INotificationService notification_service,
|
||||
OrdersModel model,
|
||||
IObservable<TableUpdate<OrderSecurityRootPair, uint>> orders_source,
|
||||
IWindowSettingsManager<OrderBookGridSettings> settings_manager,
|
||||
bool rows_are_initially_expanded = false) {
|
||||
|
||||
@@ -157,7 +162,6 @@ namespace mbts.spark {
|
||||
begin_attaching += on_begin_attaching;
|
||||
end_attaching += on_end_attaching;
|
||||
// switch between orders and/or orders w/ piq
|
||||
var orders_source = OrdersModel.piq_enabled ? model.on_order_tuples_with_root_and_piq : model.on_order_tuples_with_root;
|
||||
concat_disposable(
|
||||
connect_order_mod_vm(),
|
||||
_can_attach_selected_orders.observe_current().Subscribe(send_attached_orders_async_cmd, begin_attach_cmd),
|
||||
@@ -284,7 +288,14 @@ namespace mbts.spark {
|
||||
double price = attach_vm.price;
|
||||
uint? size = attach_vm.size;
|
||||
uint algo_template_key = attach_vm.algo_vm.algo_root.id.is_persisted ? attach_vm.algo_vm.algo_root.id.persisted : 0u;
|
||||
var order_tups = selected_rows.Select(r => r.order_tuple).ToList();
|
||||
// Don't pass any order tuples that don't have a security defined.
|
||||
// The actual security is passed to the OrdersModel - instead of just the security key -
|
||||
// so it can prevent GlbxIceberg orders from being sent to a non-glbx exchange (such as Ice).
|
||||
/// Impl details: <see cref="OrdersModel.send_order(SecurityImmutable, OrderDefinitionImmutable?, double, uint)"/>
|
||||
var order_tups = selected_rows
|
||||
.Select(r => r.order_tuple)
|
||||
.Where(ot => ot.order.security.HasValue)
|
||||
.ToList();
|
||||
var mod_algo_vm = attach_vm.algo_vm.algo_root;
|
||||
await Task.WhenAll(order_tups.Select(t => {
|
||||
var algo = t.order.order.Def.CustomDef.actual().CloneToMutable();
|
||||
@@ -298,6 +309,7 @@ namespace mbts.spark {
|
||||
};
|
||||
}
|
||||
return _model.send_order(
|
||||
t.order.security.Value,
|
||||
t.order.order.Def.with(
|
||||
attach_target_order_key: t.order.order.Key,
|
||||
custom_def: algo
|
||||
@@ -761,7 +773,7 @@ namespace mbts.spark {
|
||||
_size_param = new AlgoParamVM<uint?>("size", "Size", owner._selected_params_mon.size) {
|
||||
permissions = _size_permissions & _param_permissions
|
||||
};
|
||||
var price_param = new AlgoParamVM<double>("price", "Price", owner._selected_params_mon.price, spec?.tick_equality_comparer() ?? MathUtils.epsilon_comparer) {
|
||||
var price_param = new AlgoParamVM<double>("price", "Price", owner._selected_params_mon.price, spec?.tick_equality_comparer() ?? MathUtils.epsilon_eq_comparer) {
|
||||
permissions = _price_permissions & _param_permissions
|
||||
};
|
||||
_price_param = price_param;
|
||||
|
||||
@@ -54,7 +54,7 @@ namespace bts.shell.orderbook {
|
||||
pvm.selected_predicate_type = _corresponding_predicate_type(EnumHelper.parse_or_default<Columns>(col_id));
|
||||
return pvm;
|
||||
},
|
||||
(gen, anims) => new OrderBookGridSettings(gen, anims.Select(a => new OrderRecordTuplePredicateAnimations(a.predicate_data, a.cell_animations, a.row_animations, a.sound_notifications))) { algo_tree_display_mode = _algo_tree_display_mode },
|
||||
(gen, anims) => new OrderBookGridSettings(gen, anims.empty_if_null().Select(a => new OrderRecordTuplePredicateAnimations(a.predicate_data, a.cell_animations, a.row_animations, a.sound_notifications))) { algo_tree_display_mode = _algo_tree_display_mode },
|
||||
p => p.create_predicate(notification_service.data_cache),
|
||||
p => p.create_notification_predicate(notification_service.data_cache),
|
||||
notification_service == null ? new Func<IPredicate<OrderSecurity>, SoundNotification, IDisposable>((p, sn) => Disposable.empty) : notification_service.link_to_order,
|
||||
|
||||
@@ -82,7 +82,7 @@ namespace bts.shell {
|
||||
[ProtoMember(25)]
|
||||
public GridSettings grid;
|
||||
|
||||
public static class DefaultColumns {
|
||||
static class DefaultColumns {
|
||||
|
||||
public static GridColumnSettings account => new GridColumnSettings(col_id(Columns.Account), DefaultColumnHeaders.Account, width: DefaultColumnWidths.Account);
|
||||
public static GridColumnSettings exchange => new GridColumnSettings(col_id(Columns.Exchange), DefaultColumnHeaders.Exchange, false, width: DefaultColumnWidths.Exchange);
|
||||
@@ -176,4 +176,4 @@ namespace bts.shell {
|
||||
settings.add_missing_columns(PAndLStartupData.DefaultGridSettings.columns, out new_settings);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,553 @@
|
||||
using bts.api;
|
||||
using bts.api.orderman;
|
||||
using bts.api.position;
|
||||
using bts.utils;
|
||||
using mbts;
|
||||
using mbts.spark;
|
||||
using mbts.spark.utilities;
|
||||
using mbts.ui.controls;
|
||||
using mbts.utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
|
||||
namespace bts.shell.pnl {
|
||||
public enum PNLCalculationType {
|
||||
None,
|
||||
Bid,
|
||||
Ask,
|
||||
Opposite,
|
||||
Same,
|
||||
[Description("Cascade")]
|
||||
Waterfall,
|
||||
Last,
|
||||
Mid,
|
||||
Settle
|
||||
}
|
||||
|
||||
readonly struct PositionPriceSize : IEquatable<PositionPriceSize> {
|
||||
public static readonly PositionPriceSize empty = new PositionPriceSize(0, 0, 0, 0);
|
||||
public readonly double long_pos, short_pos, long_avg, short_avg;
|
||||
public double net_pos => long_pos - short_pos;
|
||||
public bool has_position => long_pos > 0 || short_pos > 0;
|
||||
|
||||
public PositionPriceSize(double long_pos, double long_avg, double short_pos, double short_avg) {
|
||||
this.long_pos = long_pos;
|
||||
this.long_avg = long_avg;
|
||||
this.short_pos = short_pos;
|
||||
this.short_avg = short_avg;
|
||||
}
|
||||
|
||||
public bool Equals(PositionPriceSize other) =>
|
||||
MathUtils.epsilon_equals(long_pos, other.long_pos) &&
|
||||
MathUtils.epsilon_equals(long_avg, other.long_avg) &&
|
||||
MathUtils.epsilon_equals(short_pos, other.short_pos) &&
|
||||
MathUtils.epsilon_equals(short_avg, other.short_avg);
|
||||
|
||||
public override bool Equals(object obj) =>
|
||||
obj is PositionPriceSize other && Equals(other);
|
||||
|
||||
public override int GetHashCode() =>
|
||||
EqualityComparer.aggregate_hash(
|
||||
long_pos.GetHashCode(),
|
||||
long_avg.GetHashCode(),
|
||||
short_pos.GetHashCode(),
|
||||
short_avg.GetHashCode());
|
||||
|
||||
public static bool operator ==(PositionPriceSize a, PositionPriceSize b) => a.Equals(b);
|
||||
public static bool operator !=(PositionPriceSize a, PositionPriceSize b) => !a.Equals(b);
|
||||
}
|
||||
|
||||
readonly struct PNLRowKey : IEquatable<PNLRowKey>, IComparable<PNLRowKey>, IComparable {
|
||||
public static readonly PNLRowKey empty = new PNLRowKey();
|
||||
static readonly IComparer<PNLRowKey> _comparer_inst = new ComparerBuilder<PNLRowKey>(
|
||||
new[] {
|
||||
Comparer<uint>.Default.wrap<uint, PNLRowKey>(r => r.account ?? 0),
|
||||
Comparer<uint>.Default.wrap<uint, PNLRowKey>(r => r.security ?? 0)
|
||||
}).to_comparer();
|
||||
|
||||
public readonly Venue? exchange;
|
||||
public readonly Security.ContractOneofCase? security_type;
|
||||
public readonly string symbol;
|
||||
public readonly PNLKey data_key;
|
||||
public uint? security => data_key.security_id;
|
||||
public uint? account => data_key.account_id;
|
||||
|
||||
public PNLRowKey(
|
||||
Venue? exchange = null,
|
||||
Security.ContractOneofCase? security_type = null,
|
||||
string symbol = null,
|
||||
uint? security = null,
|
||||
uint? account = null) {
|
||||
//
|
||||
this.exchange = exchange;
|
||||
this.security_type = security_type;
|
||||
this.symbol = symbol;
|
||||
data_key = new PNLKey(account.Value, security.Value);
|
||||
}
|
||||
|
||||
int IComparable.CompareTo(object obj) =>
|
||||
obj is PNLRowKey other ? CompareTo(other) : 1;
|
||||
|
||||
public int CompareTo(PNLRowKey other) =>
|
||||
_comparer_inst.Compare(this, other);
|
||||
|
||||
public bool Equals(PNLRowKey other) =>
|
||||
exchange == other.exchange &&
|
||||
security_type == other.security_type &&
|
||||
symbol == other.symbol &&
|
||||
security == other.security &&
|
||||
account == other.account;
|
||||
|
||||
public override bool Equals(object obj) =>
|
||||
obj is PNLRowKey other && Equals(other);
|
||||
|
||||
public override int GetHashCode() =>
|
||||
EqualityComparer.aggregate_hash(
|
||||
exchange?.GetHashCode() ?? 0,
|
||||
security_type?.GetHashCode() ?? 0,
|
||||
symbol?.GetHashCode() ?? 0,
|
||||
security?.GetHashCode() ?? 0,
|
||||
account?.GetHashCode() ?? 0);
|
||||
|
||||
public static bool operator ==(PNLRowKey a, PNLRowKey b) => a.Equals(b);
|
||||
public static bool operator !=(PNLRowKey a, PNLRowKey b) => !a.Equals(b);
|
||||
}
|
||||
|
||||
readonly struct WorkingOrderData {
|
||||
public readonly WorkingOrderSideData long_data, short_data;
|
||||
|
||||
WorkingOrderData(WorkingOrderSideData long_data, WorkingOrderSideData short_data) {
|
||||
this.long_data = long_data;
|
||||
this.short_data = short_data;
|
||||
}
|
||||
|
||||
public static WorkingOrderData Empty =>
|
||||
new WorkingOrderData(WorkingOrderSideData.EmptyLong, WorkingOrderSideData.EmptyShort);
|
||||
|
||||
public double top_bid =>
|
||||
long_data.data.Count > 0 ?
|
||||
long_data.data.First().Key :
|
||||
double.NaN;
|
||||
|
||||
public double top_ask =>
|
||||
short_data.data.Count > 0 ?
|
||||
short_data.data.First().Key :
|
||||
double.NaN;
|
||||
|
||||
public PositionPriceSize summary =>
|
||||
new PositionPriceSize(long_data.working_total, long_data.avg_price, short_data.working_total, short_data.avg_price);
|
||||
|
||||
public static WorkingOrderData Create(IEnumerable<OrderRecordImmutable> orders) =>
|
||||
orders.Aggregate(Empty, (seed, order) => seed.Add(order));
|
||||
|
||||
public WorkingOrderData Add(OrderRecordImmutable order) {
|
||||
if (order.working_quantity() <= 0)
|
||||
return new WorkingOrderData(long_data, short_data);
|
||||
switch (order.side()) {
|
||||
case Side.Buy: return new WorkingOrderData(long_data.Add(order), short_data);
|
||||
case Side.Sell: return new WorkingOrderData(long_data, short_data.Add(order));
|
||||
default: throw ExceptionBuilder.Argument.unexpected_case(order.side(), "order.side");
|
||||
}
|
||||
}
|
||||
|
||||
public WorkingOrderData Remove(OrderRecordImmutable order) {
|
||||
if (order.working_quantity() <= 0)
|
||||
return new WorkingOrderData(long_data, short_data);
|
||||
switch (order.side()) {
|
||||
case Side.Buy: return new WorkingOrderData(long_data.Remove(order.Key, order.price()), short_data);
|
||||
case Side.Sell: return new WorkingOrderData(long_data, short_data.Remove(order.Key, order.price()));
|
||||
default: throw ExceptionBuilder.Argument.unexpected_case(order.side(), "order.side");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
readonly struct WorkingOrderSideData {
|
||||
public readonly double avg_price;
|
||||
public readonly ImmutableSortedDictionary<double, ImmutableDictionary<uint, OrderRecordImmutable>> data;
|
||||
public readonly int working_total;
|
||||
|
||||
WorkingOrderSideData(double avg_price, int working_total, ImmutableSortedDictionary<double, ImmutableDictionary<uint, OrderRecordImmutable>> data) {
|
||||
this.avg_price = avg_price;
|
||||
this.working_total = working_total;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public static WorkingOrderSideData EmptyLong => new WorkingOrderSideData(double.NaN, 0, ImmutableSortedDictionary.Create<double, ImmutableDictionary<uint, OrderRecordImmutable>>(new ReverseComparer<double>(OrderDataHelper.PriceComparer)));
|
||||
public static WorkingOrderSideData EmptyShort => new WorkingOrderSideData(double.NaN, 0, ImmutableSortedDictionary.Create<double, ImmutableDictionary<uint, OrderRecordImmutable>>(OrderDataHelper.PriceComparer));
|
||||
|
||||
public double top_price =>
|
||||
data.Count > 0 ?
|
||||
data.First().Key :
|
||||
double.NaN;
|
||||
|
||||
public int top_volume =>
|
||||
data.Count > 0 ?
|
||||
data.Values.First().Values.Sum(o => o.working_quantity()) :
|
||||
0;
|
||||
|
||||
public WorkingOrderSideData Add(OrderRecordImmutable order_record) {
|
||||
uint order = order_record.Key;
|
||||
int working_qty = order_record.working_quantity();
|
||||
double price = order_record.price();
|
||||
if (working_qty <= 0)
|
||||
throw new ArgumentException(nameof(working_qty));
|
||||
if (working_total == 0) {
|
||||
return new WorkingOrderSideData(price, working_qty, data.Add(price, ImmutableDictionary<uint, OrderRecordImmutable>.Empty.Add(order, order_record)));
|
||||
} else return new WorkingOrderSideData(
|
||||
avg_price + ((price - avg_price) * ((double)working_qty / (working_total + working_qty))),
|
||||
working_total + working_qty,
|
||||
data.SetItem(price, data.TryGetValue(price, out var curr) ?
|
||||
curr.Add(order, order_record) :
|
||||
ImmutableDictionary<uint, OrderRecordImmutable>.Empty.Add(order, order_record)));
|
||||
}
|
||||
|
||||
public WorkingOrderSideData Remove(uint order, double price) {
|
||||
var tmp_data = data;
|
||||
if (!tmp_data.TryGetValue(price, out var wdata))
|
||||
throw new KeyNotFoundException(nameof(price));
|
||||
if (!wdata.TryGetValue(order, out var worder))
|
||||
throw new KeyNotFoundException(nameof(order));
|
||||
var wqty = worder.working_quantity();
|
||||
wdata = wdata.Remove(order);
|
||||
if (wdata.Count == 0)
|
||||
tmp_data = tmp_data.Remove(price);
|
||||
if (tmp_data.Count == 0)
|
||||
return new WorkingOrderSideData(double.NaN, 0, ImmutableSortedDictionary.Create<double, ImmutableDictionary<uint, OrderRecordImmutable>>(data.KeyComparer));
|
||||
return new WorkingOrderSideData(
|
||||
avg_price - ((price - avg_price) * (wqty / (working_total - wqty))),
|
||||
working_total - wqty,
|
||||
tmp_data);
|
||||
}
|
||||
|
||||
public override string ToString() {
|
||||
if (data.Count > 0) {
|
||||
var best_order = data.Values.First().Values.First();
|
||||
return OrderUtils.order_collection_summary(best_order.price(), avg_price, best_order.working_quantity(), working_total, SecurityDataHelper.Defaults.ContractSpecificationImmutable);
|
||||
}
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
readonly struct PNLRowData : IEquatable<PNLRowData> {
|
||||
public static readonly Func<PNLRowData, PNLKey> key_fn = d => d.key;
|
||||
public static IComparer<PNLRowData> key_comparer = Comparer<PNLKey>.Default.wrap<PNLKey, PNLRowData>(d => d.key);
|
||||
|
||||
static readonly WaterfallPriceFlags[] _waterfall_preferences = new[] {
|
||||
WaterfallPriceFlags.LastInsideSpread,
|
||||
WaterfallPriceFlags.Mid,
|
||||
WaterfallPriceFlags.LastGTEBid,
|
||||
WaterfallPriceFlags.Bid,
|
||||
WaterfallPriceFlags.LastLTEAsk,
|
||||
WaterfallPriceFlags.Ask,
|
||||
WaterfallPriceFlags.Last,
|
||||
WaterfallPriceFlags.Settle
|
||||
};
|
||||
|
||||
public readonly PNLKey key;
|
||||
public readonly SecurityImmutable? security;
|
||||
public readonly string account_name;
|
||||
public readonly PNLCalculationType calc_type;
|
||||
public readonly PositionPriceSize day, raw_day, sod;
|
||||
public readonly WorkingOrderData working;
|
||||
|
||||
public readonly double
|
||||
cpp,
|
||||
bid_price,
|
||||
ask_price,
|
||||
last_price,
|
||||
settlement_price;
|
||||
|
||||
public PNLRowData(
|
||||
PNLKey key,
|
||||
double cpp = 0,
|
||||
double bid_price = 0,
|
||||
double ask_price = 0,
|
||||
double last_price = 0,
|
||||
double settlement_price = 0,
|
||||
PositionPriceSize? sod = null,
|
||||
PositionPriceSize? day = null,
|
||||
PositionPriceSize? raw_day = null,
|
||||
WorkingOrderData? working = null,
|
||||
SecurityImmutable? security = null,
|
||||
string account_name = null,
|
||||
PNLCalculationType calc_type = PNLCalculationType.Mid) {
|
||||
//
|
||||
this.key = key;
|
||||
this.cpp = cpp;
|
||||
this.bid_price = bid_price;
|
||||
this.ask_price = ask_price;
|
||||
this.last_price = last_price;
|
||||
this.settlement_price = settlement_price;
|
||||
this.sod = sod ?? PositionPriceSize.empty;
|
||||
this.day = day ?? PositionPriceSize.empty;
|
||||
this.raw_day = raw_day ?? PositionPriceSize.empty;
|
||||
this.working = working ?? WorkingOrderData.Empty;
|
||||
this.security = security;
|
||||
this.account_name = account_name;
|
||||
this.calc_type = calc_type;
|
||||
}
|
||||
|
||||
[Flags]
|
||||
enum WaterfallPriceFlags {
|
||||
None = 0,
|
||||
Bid = 1,
|
||||
Ask = 1 << 1,
|
||||
Last = 1 << 2,
|
||||
Settle = 1 << 3,
|
||||
Mid = Bid | Ask,
|
||||
LastGTEBid = Bid | Last | 1 << 4,
|
||||
LastLTEAsk = Ask | Last | 1 << 5,
|
||||
LastInsideSpread = LastGTEBid | LastLTEAsk,
|
||||
BidGTESettle = Bid | Settle | 1 << 6,
|
||||
AskLTESettle = Ask | Settle | 1 << 7
|
||||
}
|
||||
|
||||
/// Market/Calc
|
||||
public double mid_price => (bid_price + ask_price) / 2;
|
||||
public double pnl_price {
|
||||
get {
|
||||
if (!day.has_position && !sod.has_position)
|
||||
return double.NaN;
|
||||
switch (calc_type) {
|
||||
case PNLCalculationType.Bid: return bid_price;
|
||||
case PNLCalculationType.Ask: return ask_price;
|
||||
case PNLCalculationType.Mid: return mid_price;
|
||||
case PNLCalculationType.Last: return last_price;
|
||||
// waited averages must be used when the pnl price varies, depending on the side.
|
||||
case PNLCalculationType.Same: return MathUtils.weighted_average(bid_price, day_long_pos, ask_price, day_short_pos);
|
||||
case PNLCalculationType.Opposite: return MathUtils.weighted_average(ask_price, day_long_pos, bid_price, day_short_pos);
|
||||
case PNLCalculationType.Settle: return settlement_price;
|
||||
case PNLCalculationType.Waterfall:
|
||||
default: return _waterfall_price();
|
||||
}
|
||||
}
|
||||
}
|
||||
public double long_pnl_price {
|
||||
get {
|
||||
switch (calc_type) {
|
||||
case PNLCalculationType.Same: return bid_price;
|
||||
case PNLCalculationType.Opposite: return ask_price;
|
||||
default: return pnl_price;
|
||||
}
|
||||
}
|
||||
}
|
||||
public double short_pnl_price {
|
||||
get {
|
||||
switch (calc_type) {
|
||||
case PNLCalculationType.Same: return ask_price;
|
||||
case PNLCalculationType.Opposite: return bid_price;
|
||||
default: return pnl_price;
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Sod
|
||||
public double sod_pnl => cpp *
|
||||
DoubleUtil.nan_to_zero_sum(
|
||||
sod_long_avg_price_delta_raw * sod_long_pos,
|
||||
sod_short_avg_price_delta_raw * sod_short_pos
|
||||
);
|
||||
// long
|
||||
public double sod_long_pos => sod.long_pos;
|
||||
public double sod_long_avg_price => cpp * sod.long_avg;
|
||||
public double sod_long_avg_price_raw => sod.long_avg;
|
||||
public double sod_long_avg_price_delta => cpp * sod_long_avg_price_delta_raw;
|
||||
public double sod_long_avg_price_delta_raw => sod.long_pos == 0 ? double.NaN : long_pnl_price - sod.long_avg;
|
||||
// short
|
||||
public double sod_short_pos => sod.short_pos;
|
||||
public double sod_short_avg_price => cpp * sod.short_avg;
|
||||
public double sod_short_avg_price_raw => sod.short_avg;
|
||||
public double sod_short_avg_price_delta => cpp * sod_short_avg_price_delta_raw;
|
||||
public double sod_short_avg_price_delta_raw => sod.short_pos == 0 ? double.NaN : sod.short_avg - short_pnl_price;
|
||||
// net
|
||||
public double sod_net_pos => sod.net_pos;
|
||||
public double sod_net_avg_price_delta => cpp * sod_net_avg_price_delta_raw;
|
||||
public double sod_net_avg_price_delta_raw =>
|
||||
MathUtils.weighted_average(
|
||||
sod_long_avg_price_delta_raw, sod.long_pos,
|
||||
sod_short_avg_price_delta_raw, sod.short_pos);
|
||||
|
||||
/// Day
|
||||
public double day_pnl => cpp *
|
||||
DoubleUtil.nan_to_zero_sum(
|
||||
day_long_avg_price_delta_raw * day_long_pos,
|
||||
day_short_avg_price_delta_raw * day_short_pos
|
||||
);
|
||||
// long
|
||||
public double day_long_pos => day.long_pos;
|
||||
public double day_long_avg_price => cpp * day.long_avg;
|
||||
public double day_long_avg_price_raw => day.long_avg;
|
||||
public double day_long_avg_price_delta => cpp * day_long_avg_price_delta_raw;
|
||||
public double day_long_avg_price_delta_raw => day.long_pos == 0 ? double.NaN : long_pnl_price - day.long_avg;
|
||||
// short
|
||||
public double day_short_pos => day.short_pos;
|
||||
public double day_short_avg_price => cpp * day.short_avg;
|
||||
public double day_short_avg_price_raw => day.short_avg;
|
||||
public double day_short_avg_price_delta => cpp * day_short_avg_price_delta_raw;
|
||||
public double day_short_avg_price_delta_raw => day.short_pos == 0 ? double.NaN : day.short_avg - short_pnl_price;
|
||||
public bool has_day_trades => day.long_pos > 0 || day.short_pos > 0;
|
||||
// net
|
||||
public double day_net_pos => day.net_pos;
|
||||
public double day_net_avg_price_delta => cpp * day_net_avg_price_delta_raw;
|
||||
public double day_net_avg_price_delta_raw =>
|
||||
MathUtils.weighted_average(
|
||||
day_long_avg_price_delta_raw, day.long_pos,
|
||||
day_short_avg_price_delta_raw, day.short_pos);
|
||||
|
||||
/// Raw Day
|
||||
public double raw_day_long_pos => raw_day.long_pos;
|
||||
public double raw_day_short_pos => raw_day.short_pos;
|
||||
public double raw_day_net_pos => raw_day.net_pos;
|
||||
|
||||
/// Total
|
||||
public double total_pnl => DoubleUtil.nan_to_zero_sum(sod_pnl, day_pnl);
|
||||
// long
|
||||
public double total_long_pos => DoubleUtil.nan_to_zero_sum(sod.long_pos, day.long_pos);
|
||||
public double total_long_avg_price => cpp * total_long_avg_price_raw;
|
||||
public double total_long_avg_price_raw => DoubleUtil.nan_to_zero_sum(sod.long_avg, day.long_avg);
|
||||
public double total_long_avg_price_delta => cpp * total_long_avg_price_delta_raw;
|
||||
public double total_long_avg_price_delta_raw =>
|
||||
MathUtils.weighted_average(
|
||||
sod_long_avg_price_delta_raw, sod.long_pos,
|
||||
day_long_avg_price_delta_raw, day.long_pos);
|
||||
// short
|
||||
public double total_short_pos => DoubleUtil.nan_to_zero_sum(sod.short_pos, day.short_pos);
|
||||
public double total_short_avg_price => cpp * total_short_avg_price_raw;
|
||||
public double total_short_avg_price_raw => DoubleUtil.nan_to_zero_sum(sod.short_avg, day.short_avg);
|
||||
public double total_short_avg_price_delta => cpp * total_short_avg_price_delta_raw;
|
||||
public double total_short_avg_price_delta_raw =>
|
||||
MathUtils.weighted_average(
|
||||
sod_short_avg_price_delta_raw, sod.short_pos,
|
||||
day_short_avg_price_delta_raw, day.short_pos);
|
||||
// net
|
||||
public double total_net_pos => DoubleUtil.nan_to_zero_sum(sod.long_pos, -sod.short_pos, day.long_pos, -day.short_pos);
|
||||
public double total_net_avg_price_delta => cpp * total_net_avg_price_delta_raw;
|
||||
public double total_net_avg_price_delta_raw =>
|
||||
MathUtils.weighted_average(
|
||||
total_long_avg_price_delta_raw, total_long_pos,
|
||||
total_short_avg_price_delta_raw, total_short_pos);
|
||||
|
||||
#region Working
|
||||
// long
|
||||
public double working_long_pos => working.long_data.working_total;
|
||||
public double working_long_avg_price => cpp * working_long_avg_price_raw;
|
||||
public double working_long_top_price => cpp * working_long_top_price_raw;
|
||||
public double working_long_top_price_raw => working.top_bid;
|
||||
public double working_long_avg_price_raw => working.long_data.avg_price;
|
||||
public double working_long_avg_price_delta => cpp * working_long_avg_price_delta_raw;
|
||||
public double working_long_avg_price_delta_raw => working_long_pos == 0 ? double.NaN : long_pnl_price - working_long_avg_price_raw;
|
||||
// short
|
||||
public double working_short_pos => working.short_data.working_total;
|
||||
public double working_short_avg_price => cpp * working_short_avg_price_raw;
|
||||
public double working_short_top_price => cpp * working_short_top_price_raw;
|
||||
public double working_short_top_price_raw => working.top_ask;
|
||||
public double working_short_avg_price_raw => working.short_data.avg_price;
|
||||
public double working_short_avg_price_delta => cpp * working_short_avg_price_delta_raw;
|
||||
public double working_short_avg_price_delta_raw => working_short_pos == 0 ? double.NaN : working_short_avg_price_raw - short_pnl_price;
|
||||
// net
|
||||
public double working_net_pos => working_long_pos - working_short_pos;
|
||||
public double working_net_avg_price_delta => cpp * working_net_avg_price_delta_raw;
|
||||
public double working_net_avg_price_delta_raw =>
|
||||
MathUtils.weighted_average(
|
||||
working_long_avg_price_delta_raw, working_long_pos,
|
||||
working_short_avg_price_delta_raw, working_short_pos);
|
||||
#endregion
|
||||
|
||||
WaterfallPriceFlags _waterfall_flags() {
|
||||
var flags = WaterfallPriceFlags.None;
|
||||
if (!double.IsNaN(bid_price)) flags |= WaterfallPriceFlags.Bid;
|
||||
if (!double.IsNaN(ask_price)) flags |= WaterfallPriceFlags.Ask;
|
||||
if (!double.IsNaN(last_price)) flags |= WaterfallPriceFlags.Last;
|
||||
if (last_price >= bid_price) flags |= WaterfallPriceFlags.LastGTEBid;
|
||||
if (last_price <= ask_price) flags |= WaterfallPriceFlags.LastLTEAsk;
|
||||
if (!double.IsNaN(settlement_price)) flags |= WaterfallPriceFlags.Settle;
|
||||
if (bid_price >= settlement_price) flags |= WaterfallPriceFlags.BidGTESettle;
|
||||
if (ask_price <= settlement_price) flags |= WaterfallPriceFlags.AskLTESettle;
|
||||
return flags;
|
||||
}
|
||||
|
||||
double _waterfall_price() {
|
||||
var flags = _waterfall_flags();
|
||||
// return price for the best match
|
||||
for (int i = 0; i < _waterfall_preferences.Length; i++) {
|
||||
if ((_waterfall_preferences[i] & flags) == _waterfall_preferences[i])
|
||||
return _waterfall_price(_waterfall_preferences[i]);
|
||||
}
|
||||
return double.NaN;
|
||||
}
|
||||
|
||||
double _waterfall_price(WaterfallPriceFlags ltp) {
|
||||
switch (ltp) {
|
||||
case WaterfallPriceFlags.Last:
|
||||
case WaterfallPriceFlags.LastInsideSpread:
|
||||
case WaterfallPriceFlags.LastGTEBid:
|
||||
case WaterfallPriceFlags.LastLTEAsk: return last_price;
|
||||
case WaterfallPriceFlags.Mid: return mid_price;
|
||||
case WaterfallPriceFlags.Bid:
|
||||
case WaterfallPriceFlags.BidGTESettle: return bid_price;
|
||||
case WaterfallPriceFlags.Ask:
|
||||
case WaterfallPriceFlags.AskLTESettle: return ask_price;
|
||||
case WaterfallPriceFlags.Settle: return settlement_price;
|
||||
default: return double.NaN;
|
||||
}
|
||||
}
|
||||
|
||||
public bool Equals(PNLRowData other) =>
|
||||
key == other.key &&
|
||||
cpp == other.cpp &&
|
||||
security.HasValue == other.security.HasValue &&
|
||||
account_name == other.account_name &&
|
||||
sod == other.sod &&
|
||||
day == other.day &&
|
||||
raw_day == other.raw_day &&
|
||||
working.summary == other.working.summary;
|
||||
|
||||
public override bool Equals(object obj) =>
|
||||
obj is PNLRowData other && Equals(other);
|
||||
|
||||
// assuming callers will not combine different values with the same key,
|
||||
// the other components shouldn't be included in the hash calculation.
|
||||
public override int GetHashCode() =>
|
||||
key.GetHashCode();
|
||||
|
||||
public static bool operator ==(PNLRowData a, PNLRowData b) => a.Equals(b);
|
||||
public static bool operator !=(PNLRowData a, PNLRowData b) => !a.Equals(b);
|
||||
}
|
||||
|
||||
static class PNLRowDataFactory {
|
||||
public static PNLRowData create((SecurityImmutable? sec, PNLUnitImmutable value) t) =>
|
||||
new PNLRowData(new PNLKey(t.value.Pos.AccountId, t.value.Pos.SecurityId),
|
||||
t.value.CashValue,
|
||||
t.value.BidPrice,
|
||||
t.value.AskPrice,
|
||||
t.value.LastPrice,
|
||||
t.value.SettlementPrice,
|
||||
new PositionPriceSize(t.value.Pos.SodLongPos, t.value.Pos.SodLongAvgPrice, t.value.Pos.SodShortPos, t.value.Pos.SodShortAvgPrice),
|
||||
new PositionPriceSize(t.value.Pos.DayLongPos, t.value.Pos.DayLongAvgPrice, t.value.Pos.DayShortPos, t.value.Pos.DayShortAvgPrice),
|
||||
t.value.Pos.raw_day_pps(),
|
||||
WorkingOrderData.Empty,
|
||||
t.sec,
|
||||
t.value.Pos.Account);
|
||||
}
|
||||
|
||||
static class PNLDataExtensions {
|
||||
// Note: PNLRowData is passed by ref because it is fairly large struct, and since it is a readonly struct, we know it cannot be modified (so might as well avoid excessive copying).
|
||||
public static PNLRowData with(this PNLRowData pnl_data, SecurityImmutable? security = null, string account_name = null, WorkingOrderData? working = null, PNLCalculationType? calc_type = null) =>
|
||||
new PNLRowData(
|
||||
pnl_data.key,
|
||||
pnl_data.cpp,
|
||||
pnl_data.bid_price,
|
||||
pnl_data.ask_price,
|
||||
pnl_data.last_price,
|
||||
pnl_data.settlement_price,
|
||||
pnl_data.sod,
|
||||
pnl_data.day,
|
||||
pnl_data.raw_day,
|
||||
working ?? pnl_data.working,
|
||||
security ?? pnl_data.security,
|
||||
account_name ?? pnl_data.account_name,
|
||||
calc_type ?? pnl_data.calc_type);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using bts.utils;
|
||||
using System;
|
||||
|
||||
namespace bts.shell.pnl {
|
||||
readonly struct PNLKey : IEquatable<PNLKey>, IComparable<PNLKey>, IComparable {
|
||||
public readonly uint account_id, security_id;
|
||||
|
||||
public PNLKey(uint account_id, uint security_id) {
|
||||
this.account_id = account_id;
|
||||
this.security_id = security_id;
|
||||
}
|
||||
|
||||
public int CompareTo(PNLKey other) {
|
||||
int c = account_id.CompareTo(other.account_id);
|
||||
if (c == 0)
|
||||
c = security_id.CompareTo(other.security_id);
|
||||
return c;
|
||||
}
|
||||
|
||||
int IComparable.CompareTo(object obj) =>
|
||||
obj is PNLKey other ? CompareTo(other) : 1;
|
||||
|
||||
public bool Equals(PNLKey other) =>
|
||||
security_id == other.security_id &&
|
||||
account_id == other.account_id;
|
||||
|
||||
public override bool Equals(object obj) =>
|
||||
obj is PNLKey other && Equals(other);
|
||||
|
||||
public override int GetHashCode() =>
|
||||
(security_id.GetHashCode() % MathUtils.Primes.P_65449) |
|
||||
((account_id.GetHashCode() % MathUtils.Primes.P_65449) << 16);
|
||||
|
||||
public override string ToString() =>
|
||||
$"Account: {account_id}, Security: {security_id}";
|
||||
|
||||
public static bool operator ==(PNLKey a, PNLKey b) => a.Equals(b);
|
||||
public static bool operator !=(PNLKey a, PNLKey b) => !a.Equals(b);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
using bts.utils;
|
||||
using System;
|
||||
|
||||
namespace bts.shell.pnl {
|
||||
readonly struct PNLNormalizedData : IEquatable<PNLNormalizedData> {
|
||||
public static readonly PNLNormalizedData zero =
|
||||
new PNLNormalizedData(0, 0, 0, 0, 0, 0, 0, 0);
|
||||
|
||||
public PNLNormalizedData(
|
||||
decimal day_pnl,
|
||||
decimal day_long_pos,
|
||||
decimal day_short_pos,
|
||||
decimal raw_day_long_pos,
|
||||
decimal raw_day_short_pos,
|
||||
decimal sod_pnl,
|
||||
decimal sod_long_pos,
|
||||
decimal sod_short_pos,
|
||||
decimal working_long_pos = 0,
|
||||
decimal working_short_pos = 0) {
|
||||
//
|
||||
this.day_pnl = day_pnl;
|
||||
this.day_long_pos = day_long_pos;
|
||||
this.day_short_pos = day_short_pos;
|
||||
this.raw_day_long_pos = raw_day_long_pos;
|
||||
this.raw_day_short_pos = raw_day_short_pos;
|
||||
this.sod_pnl = sod_pnl;
|
||||
this.sod_long_pos = sod_long_pos;
|
||||
this.sod_short_pos = sod_short_pos;
|
||||
this.working_long_pos = working_long_pos;
|
||||
this.working_short_pos = working_short_pos;
|
||||
}
|
||||
|
||||
// sod
|
||||
public decimal sod_pnl { get; }
|
||||
public decimal sod_long_pos { get; }
|
||||
public decimal sod_short_pos { get; }
|
||||
public decimal sod_net_pos => sod_long_pos - sod_short_pos;
|
||||
// day
|
||||
public decimal day_pnl { get; }
|
||||
public decimal day_long_pos { get; }
|
||||
public decimal day_short_pos { get; }
|
||||
public decimal day_net_pos => day_long_pos - day_short_pos;
|
||||
// raw day
|
||||
public decimal raw_day_long_pos { get; }
|
||||
public decimal raw_day_short_pos { get; }
|
||||
public decimal raw_day_net_pos => raw_day_long_pos - raw_day_short_pos;
|
||||
// total
|
||||
public decimal total_pnl => sod_pnl + day_pnl;
|
||||
public decimal total_net_pos => total_long_pos - total_short_pos;
|
||||
public decimal total_long_pos => sod_long_pos + day_long_pos;
|
||||
public decimal total_short_pos => sod_short_pos + day_short_pos;
|
||||
// working
|
||||
public decimal working_long_pos { get; }
|
||||
public decimal working_net_pos => working_long_pos - working_short_pos;
|
||||
public decimal working_short_pos { get; }
|
||||
|
||||
public bool Equals(PNLNormalizedData other) =>
|
||||
day_pnl == other.day_pnl &&
|
||||
day_long_pos == other.day_long_pos &&
|
||||
day_short_pos == other.day_short_pos &&
|
||||
raw_day_long_pos == other.raw_day_long_pos &&
|
||||
raw_day_short_pos == other.raw_day_short_pos &&
|
||||
sod_pnl == other.sod_pnl &&
|
||||
sod_long_pos == other.sod_long_pos &&
|
||||
sod_short_pos == other.sod_short_pos &&
|
||||
working_long_pos == other.working_long_pos &&
|
||||
working_short_pos == other.working_short_pos;
|
||||
|
||||
public override bool Equals(object obj) =>
|
||||
obj is PNLNormalizedData other && Equals(other);
|
||||
|
||||
public override int GetHashCode() =>
|
||||
EqualityComparer.aggregate_hash(
|
||||
day_pnl.GetHashCode(),
|
||||
sod_pnl.GetHashCode());
|
||||
|
||||
public override string ToString() =>
|
||||
string.Join(",", new[] {
|
||||
$"SOD PNL : {sod_pnl}",
|
||||
$"SOD Long : {sod_long_pos}",
|
||||
$"SOD Short: {sod_short_pos}",
|
||||
$"Day PNL : {day_pnl}",
|
||||
$"Day Long : {day_long_pos}",
|
||||
$"Day Short: {day_short_pos}",
|
||||
$"Raw Day Long : {raw_day_long_pos}",
|
||||
$"Raw Day Short: {raw_day_short_pos}",
|
||||
$"Work Long : {working_long_pos}",
|
||||
$"Work Short: {working_short_pos}",
|
||||
});
|
||||
|
||||
public static bool operator ==(PNLNormalizedData a, PNLNormalizedData b) => a.Equals(b);
|
||||
public static bool operator !=(PNLNormalizedData a, PNLNormalizedData b) => !a.Equals(b);
|
||||
|
||||
public static PNLNormalizedData operator +(PNLNormalizedData a, PNLNormalizedData b) =>
|
||||
new PNLNormalizedData(
|
||||
a.day_pnl + b.day_pnl,
|
||||
a.day_long_pos + b.day_long_pos,
|
||||
a.day_short_pos + b.day_short_pos,
|
||||
a.raw_day_long_pos + b.raw_day_long_pos,
|
||||
a.raw_day_short_pos + b.raw_day_short_pos,
|
||||
a.sod_pnl + b.sod_pnl,
|
||||
a.sod_long_pos + b.sod_long_pos,
|
||||
a.sod_short_pos + b.sod_short_pos,
|
||||
a.working_long_pos + b.working_long_pos,
|
||||
a.working_short_pos + b.working_short_pos);
|
||||
|
||||
public static PNLNormalizedData operator -(PNLNormalizedData a, PNLNormalizedData b) =>
|
||||
new PNLNormalizedData(
|
||||
a.day_pnl - b.day_pnl,
|
||||
a.day_long_pos - b.day_long_pos,
|
||||
a.day_short_pos - b.day_short_pos,
|
||||
a.raw_day_long_pos - b.raw_day_long_pos,
|
||||
a.raw_day_short_pos - b.raw_day_short_pos,
|
||||
a.sod_pnl - b.sod_pnl,
|
||||
a.sod_long_pos - b.sod_long_pos,
|
||||
a.sod_short_pos - b.sod_short_pos,
|
||||
a.working_long_pos - b.working_long_pos,
|
||||
a.working_short_pos - b.working_short_pos);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,375 @@
|
||||
using bts.api;
|
||||
using bts.collections;
|
||||
using bts.settings;
|
||||
using bts.shell.pnl;
|
||||
using bts.spark;
|
||||
using bts.utils;
|
||||
using mbts;
|
||||
using mbts.config;
|
||||
using mbts.persist;
|
||||
using mbts.spark;
|
||||
using mbts.ui;
|
||||
using mbts.ui.controls;
|
||||
using mbts.ui.settings;
|
||||
using mbts.ui.spark;
|
||||
using mbts.ui.utility;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Collections.Specialized;
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel.Composition;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace bts.shell {
|
||||
class PNLWindowFactory : ShellWindowFactory<PNLSaveState> {
|
||||
const string _window_type = "PNL", _window_title = "PNL";
|
||||
readonly IBus _bus;
|
||||
readonly OrdersModel _orders_model;
|
||||
readonly UserSettingsHub _settings_hub;
|
||||
readonly PNLConfig _pnl_conf;
|
||||
readonly IShellWindowThreadIDProvider _thread_id_provider;
|
||||
|
||||
public PNLWindowFactory(IShellModule owner, IBus bus, UserSettingsHub settings_hub, OrdersModel orders_model) : base(owner, _window_type) {
|
||||
_bus = bus;
|
||||
_orders_model = orders_model;
|
||||
_settings_hub = settings_hub;
|
||||
_pnl_conf = new PNLConfig(Configuration.get<bts.config.PNLOptions>());
|
||||
_thread_id_provider = ShellWindowThreadIDFactory.create(_window_type, Configuration.get<bts.config.MultiThreadedWindows>().pnl);
|
||||
_dispose = bus.subscribe(SparkTags.ShowPNLTag).OnDispatcher().Subscribe(_show);
|
||||
}
|
||||
|
||||
public override Task<IShellWindow> load(PNLSaveState state) =>
|
||||
Task.FromResult(_create(state.settings));
|
||||
|
||||
void _show(PNLSettings settings) =>
|
||||
_bus.post(SparkTags.ShowWindow, _create(settings));
|
||||
|
||||
IShellWindow _create(PNLSettings settings) {
|
||||
var settings_manager = _settings_hub.create_pnl_settings_manager(settings);
|
||||
// Need to keep a local copy of the columns, since they are defined in xaml (not the viewmodel), and get_save_state may be called on a non-dispatcher thread.
|
||||
// If that happens, we would be unable to access _grid.Columns, and thus unable to get the current column settings.
|
||||
// Use an ImmutableList to avoid any threading issues when responding to CollectionChanged events on the dispatcher thread.
|
||||
ImmutableList<DIYGrid.ColumnBase> cached_columns = ImmutableList<DIYGrid.ColumnBase>.Empty;
|
||||
return SparkWindow<PNLSaveState>.create(_window_type, _window_title, _bus, this, Icons.Accounting, _thread_id_provider.next_id(), () => {
|
||||
var vm = new PNLVM(_bus, _orders_model, settings ?? PNLSettings.Default);
|
||||
var content = new PNLView {
|
||||
DataContext = vm
|
||||
};
|
||||
vm.sorting_changed += sh => {
|
||||
try {
|
||||
DIYGridSortHelper.set_sort_directions(sh, content._grid.Columns);
|
||||
} catch { }
|
||||
};
|
||||
vm.SubscribeDisposed(
|
||||
DIYGridSortHelper.observe_active_comparers<PNLView.Columns, PNLRowVM>(content._grid, PNLVM.get_column_comparer)
|
||||
.OnDispatcher()
|
||||
.Subscribe(sh => vm.sorting_hierarchy = sh.Select(s => (s.key, s.direction)).ToList()));
|
||||
cached_columns = ImmutableList<DIYGrid.ColumnBase>.Empty.AddRange(content._grid.Columns);
|
||||
|
||||
Func<PNLSaveState> get_save_state = () => {
|
||||
var cols = cached_columns.ToArray(c => c.create());
|
||||
var filters = vm.get_filters_states();
|
||||
var sorting_criterion = vm.sorting_hierarchy.Select(tup => new SortingCriteria(tup.key.ToString(), tup.direction == ListSortDirection.Ascending)).ToArray();
|
||||
var calc_type = vm.calculation_type;
|
||||
var state = settings_manager.instance_settings;
|
||||
if (state == null) {
|
||||
// don't use defaults if the user manipulated the window state outside of the settings dialog (e.g., resized column(s) via drag, or changed sort/filter)
|
||||
var defaults = settings_manager.default_settings;
|
||||
if (calc_type == defaults.calculation_type &&
|
||||
defaults.sorting_criterion.SequenceEqual(sorting_criterion) &&
|
||||
defaults.grid.columns.SequenceEqual(cols) && defaults.filters.safe_equals(filters)) {
|
||||
// the user's current state still matches the defaults (i.e., no outside manipulation was performed); return null so this window will use the latest default settings when restored
|
||||
return new PNLSaveState { use_defaults = true, settings = null };
|
||||
}
|
||||
state = defaults;
|
||||
}
|
||||
// assign latest values, as they may have be modified outside of the settings dialog (in which case the settings manager would be out of date)
|
||||
state = state.with(
|
||||
sorting_criterion,
|
||||
vm.anchors,
|
||||
filters,
|
||||
state.grid.with(columns: cols, background: vm.grid_background_color.rgb_color()),
|
||||
calc_type,
|
||||
vm.header_row_display);
|
||||
return new PNLSaveState { use_defaults = false, settings = state };
|
||||
};
|
||||
IEnumerable<SparkCommand> commands = new[] {
|
||||
new SparkCommandCommand("Collapse All", Icons.RightArrow, new DelegateCommand(() => vm.set_all_expanded(false))),
|
||||
new SparkCommandCommand("Expand All", Icons.DownArrow, new DelegateCommand(() => vm.set_all_expanded(true))),
|
||||
new SparkCommandCommand("Play", Icons.Play, new DelegateCommand(() => vm.is_paused = false)) {
|
||||
show_in_context_menu = _pnl_conf.show_pause_resume,
|
||||
show_in_toolbar = _pnl_conf.show_pause_resume
|
||||
},
|
||||
new SparkCommandCommand("Pause", Icons.Pause, new DelegateCommand(() => vm.is_paused = true)) {
|
||||
show_in_context_menu = _pnl_conf.show_pause_resume,
|
||||
show_in_toolbar = _pnl_conf.show_pause_resume
|
||||
},
|
||||
SparkCommands.window_settings_command(new DialogCommand(() => {
|
||||
settings_manager.instance_settings = get_save_state().settings;
|
||||
var dialog_vm = new ShellSettingsDialogVM<PNLSettings, PNLSettingsVM>(
|
||||
settings_manager,
|
||||
us => us.pnl,
|
||||
UserSettings.FileExtensions.PNL,
|
||||
s => new PNLSettingsVM(s));
|
||||
var dialog = new ShellSettingsDialog { DataContext = dialog_vm };
|
||||
dialog_vm.RequestClose += on_request_close;
|
||||
return dialog;
|
||||
void on_request_close(object sender, EventArgs e) =>
|
||||
WindowUtilities.force_close(dialog);
|
||||
}))
|
||||
};
|
||||
IDisposable dispose = Disposable.Create(
|
||||
settings_manager.subscribe_current().OnDispatcher().Subscribe(s => {
|
||||
vm.load_from(s);
|
||||
var col_setting_map = s.grid.columns.ToDictionary(c => c.id);
|
||||
var fallback_family = new FontFamily(s.grid.font_family);
|
||||
// group
|
||||
var group_col_setting_map = s.group_columns.ToDictionary(c => c.id);
|
||||
var group_appearance = s.group_cell_appearance.to_appearance();
|
||||
// header
|
||||
var header_col_setting_map = s.header_columns.ToDictionary(c => c.id);
|
||||
var header_appearance = s.header_cell_appearance.to_appearance();
|
||||
//
|
||||
foreach (var c in content._grid.Columns.OfType<PNLColumn>()) {
|
||||
if (col_setting_map.TryGetValue(c.id, out var cs)) {
|
||||
c.load_from(cs);
|
||||
}
|
||||
if (group_col_setting_map.TryGetValue(c.id, out var gcs)) {
|
||||
c.load_group_params(
|
||||
group_appearance.font_size,
|
||||
gcs.font_weight_override ?? group_appearance.font_weight,
|
||||
gcs.font_color_override ?? group_appearance.foreground,
|
||||
group_appearance.font_family);
|
||||
}
|
||||
if (header_col_setting_map.TryGetValue(c.id, out var hcs)) {
|
||||
c.load_header_params(
|
||||
header_appearance.font_size,
|
||||
hcs.font_weight_override ?? header_appearance.font_weight,
|
||||
hcs.font_color_override ?? header_appearance.foreground,
|
||||
header_appearance.font_family);
|
||||
}
|
||||
}
|
||||
CollectionHelper.sync_with_never_duplicate(content._grid.Columns, s.grid.columns, cs => {
|
||||
throw new InvalidOperationException();
|
||||
}, c => c.id.ToLower(), cs => cs.id.ToLower());
|
||||
}),
|
||||
content._grid.Columns.Subscribe((s, e) => {
|
||||
switch (e.Action) {
|
||||
case NotifyCollectionChangedAction.Add: add_new(); break;
|
||||
case NotifyCollectionChangedAction.Remove: remove_old(); break;
|
||||
case NotifyCollectionChangedAction.Replace:
|
||||
case NotifyCollectionChangedAction.Move:
|
||||
remove_old();
|
||||
add_new();
|
||||
break;
|
||||
case NotifyCollectionChangedAction.Reset:
|
||||
cached_columns = ImmutableList<DIYGrid.ColumnBase>.Empty.AddRange(content._grid.Columns);
|
||||
break;
|
||||
}
|
||||
void add_new() =>
|
||||
cached_columns = e.NewStartingIndex >= 0 ?
|
||||
cached_columns.InsertRange(e.NewStartingIndex, e.NewItems.OfType<DIYGrid.ColumnBase>()) :
|
||||
cached_columns.AddRange(e.NewItems.OfType<DIYGrid.ColumnBase>());
|
||||
|
||||
void remove_old() =>
|
||||
cached_columns = e.OldStartingIndex >= 0 ?
|
||||
cached_columns.RemoveRange(e.OldStartingIndex, e.OldItems.Count) :
|
||||
cached_columns.RemoveRange(e.OldItems.OfType<DIYGrid.ColumnBase>());
|
||||
}),
|
||||
Disposable.wrap_collection(commands.OfType<IDisposable>()),
|
||||
vm);
|
||||
return (content, commands, get_save_state, dispose);
|
||||
},
|
||||
min_width_offset: SystemParameters.HorizontalScrollBarButtonWidth + 8,
|
||||
min_height_offset: 60);
|
||||
}
|
||||
|
||||
readonly struct PNLConfig {
|
||||
public readonly bool
|
||||
enable_animations,
|
||||
show_pause_resume;
|
||||
|
||||
public PNLConfig(bts.config.PNLOptions options) {
|
||||
enable_animations = options.enable_animations;
|
||||
show_pause_resume = options.show_pause_resume;
|
||||
}
|
||||
}
|
||||
|
||||
[Export(typeof(IPNLSettingsMigrationRule))]
|
||||
class AddMissingAnimationsMigrationRule : IPNLSettingsMigrationRule {
|
||||
public bool apply(PNLSettings settings, out PNLSettings new_settings) {
|
||||
if (settings != null && (
|
||||
_apply(ref settings, settings.animations, PNLSettings.default_animations, (s, anims) => s.with(animations: anims)) |
|
||||
_apply(ref settings, settings.group_header_animations, PNLSettings.default_group_header_animations, (s, anims) => s.with(group_header_animations: anims)) |
|
||||
_apply(ref settings, settings.header_row_animations, PNLSettings.default_header_row_animations, (s, anims) => s.with(header_row_animations: anims)))) {
|
||||
//
|
||||
new_settings = settings;
|
||||
return true;
|
||||
}
|
||||
new_settings = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool _apply(ref PNLSettings settings, IEnumerable<PNLAnimationTriggerData> actual_triggers, IEnumerable<PNLAnimationTriggerData> default_triggers, Func<PNLSettings, PNLAnimationTriggerData[], PNLSettings> set_triggers) {
|
||||
if (_apply(actual_triggers, default_triggers, out var result)) {
|
||||
settings = set_triggers(settings, actual_triggers.Concat(result).ToArray());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool _apply(IEnumerable<PNLAnimationTriggerData> actual_triggers, IEnumerable<PNLAnimationTriggerData> default_triggers, out IEnumerable<PNLAnimationTriggerData> result) {
|
||||
var default_trigger_map = default_triggers.ToDictionary(t => t.expression);
|
||||
var actual_trigger_map = actual_triggers.ToImmutableDictionary(t => t.expression);
|
||||
var snapshot = actual_trigger_map;
|
||||
foreach (var kv in snapshot) {
|
||||
if (default_trigger_map.TryGetValue(kv.Key, out var default_anims)) {
|
||||
// assume 1 animation per column
|
||||
var actual_col_anims = kv.Value.animations.ToDictionaryCrushed(a => a.column);
|
||||
var default_col_anims = default_anims.animations.ToDictionaryCrushed(a => a.column);
|
||||
actual_col_anims.Difference(default_col_anims,
|
||||
out var exclusive_actual,
|
||||
out var exclusive_defaults,
|
||||
out var diff);
|
||||
var new_col_anims = exclusive_defaults.ToList();
|
||||
if (new_col_anims.Count > 0) {
|
||||
// add the original col animations to the new ones (combine them)
|
||||
new_col_anims.AddRange(actual_col_anims);
|
||||
actual_trigger_map = actual_trigger_map.SetItem(kv.Key, kv.Value.with(animations: new_col_anims.Select(col_anim => col_anim.Value).ToArray()));
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach (var kv in default_trigger_map) {
|
||||
if (!actual_trigger_map.ContainsKey(kv.Key))
|
||||
actual_trigger_map = actual_trigger_map.Add(kv.Key, kv.Value);
|
||||
}
|
||||
if (actual_trigger_map != snapshot) {
|
||||
result = actual_trigger_map.Values;
|
||||
return true;
|
||||
}
|
||||
result = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
[Export(typeof(IPNLSettingsMigrationRule))]
|
||||
class AddMissingColumnsMigrationRule : IPNLSettingsMigrationRule {
|
||||
public bool apply(PNLSettings settings, out PNLSettings new_settings) {
|
||||
if (settings != null) {
|
||||
var defaults = PNLSettings.Default;
|
||||
if (_apply(ref settings, settings.grid.columns, defaults.grid.columns, (s, cols) => s.with(grid: s.grid.with(cols))) |
|
||||
_apply(ref settings, settings.group_columns, defaults.group_columns, (s, cols) => s.with(group_columns: cols)) |
|
||||
_apply(ref settings, settings.header_columns, defaults.header_columns, (s, cols) => s.with(header_columns: cols))) {
|
||||
new_settings = settings;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
new_settings = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool _apply(ref PNLSettings settings, IEnumerable<GridColumnSettings> actual_columns, IEnumerable<GridColumnSettings> default_columns, Func<PNLSettings, IEnumerable<GridColumnSettings>, PNLSettings> set_columns) {
|
||||
if (GridSettingsHelper.add_missing_columns(actual_columns, default_columns, out var new_cols)) {
|
||||
settings = set_columns(settings, new_cols);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
[Export(typeof(IPNLSettingsMigrationRule))]
|
||||
class RemoveColumnsMigrationRule : IPNLSettingsMigrationRule {
|
||||
public bool apply(PNLSettings settings, out PNLSettings new_settings) {
|
||||
if (settings != null) {
|
||||
if (_apply(ref settings, settings.grid.columns, (s, cols) => s.with(grid: s.grid.with(cols))) |
|
||||
_apply(ref settings, settings.group_columns, (s, cols) => s.with(group_columns: cols)) |
|
||||
_apply(ref settings, settings.header_columns, (s, cols) => s.with(header_columns: cols))) {
|
||||
new_settings = settings;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
new_settings = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool _apply(ref PNLSettings settings, IEnumerable<GridColumnSettings> columns, Func<PNLSettings, IEnumerable<GridColumnSettings>, PNLSettings> set_columns) {
|
||||
if (_apply(columns, out var new_cols)) {
|
||||
settings = set_columns(settings, new_cols);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool _apply(IEnumerable<GridColumnSettings> columns, out IEnumerable<GridColumnSettings> result) {
|
||||
var col_list = columns.ToList();
|
||||
int orig_count = col_list.Count;
|
||||
col_list.RemoveWhere(c => !Enum.TryParse<PNLView.Columns>(c.id, true, out var _));
|
||||
if (col_list.Count < orig_count) {
|
||||
result = col_list;
|
||||
return true;
|
||||
}
|
||||
result = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
[Export(typeof(IPNLSettingsMigrationRule))]
|
||||
class RemoveColumnAnimationsMigrationRule : IPNLSettingsMigrationRule {
|
||||
public bool apply(PNLSettings settings, out PNLSettings new_settings) {
|
||||
if (settings != null) {
|
||||
if (_apply(ref settings, settings.animations, (s, t) => s.with(animations: t)) |
|
||||
_apply(ref settings, settings.group_header_animations, (s, t) => s.with(group_header_animations: t)) |
|
||||
_apply(ref settings, settings.header_row_animations, (s, t) => s.with(header_row_animations: t))) {
|
||||
new_settings = settings;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
new_settings = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool _apply(ref PNLSettings settings, IEnumerable<PNLAnimationTriggerData> triggers, Func<PNLSettings, IEnumerable<PNLAnimationTriggerData>, PNLSettings> set_triggers) {
|
||||
if (_apply(triggers, out var new_triggers)) {
|
||||
settings = set_triggers(settings, new_triggers);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool _apply(IEnumerable<PNLAnimationTriggerData> triggers, out IEnumerable<PNLAnimationTriggerData> result) {
|
||||
var trigger_map = triggers.ToImmutableDictionary(t => t.expression);
|
||||
var snapshot = trigger_map;
|
||||
foreach (var t in trigger_map.Values) {
|
||||
var anims = t.animations.ToList();
|
||||
int orig_count = anims.Count;
|
||||
anims.RemoveWhere(a => !Enum.IsDefined(typeof(PNLView.Columns), a.column));
|
||||
if (anims.Count < orig_count)
|
||||
trigger_map = trigger_map.SetItem(t.expression, t.with(animations: anims.ToArray()));
|
||||
}
|
||||
if (trigger_map != snapshot) {
|
||||
result = trigger_map.Values;
|
||||
return true;
|
||||
}
|
||||
result = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
[Export(typeof(IPNLSettingsMigrationRule))]
|
||||
class EnableGridBackgroundColorMigrationRule : IPNLSettingsMigrationRule {
|
||||
public bool apply(PNLSettings settings, out PNLSettings new_settings) {
|
||||
if (settings != null && !settings.grid.enable_background_color) {
|
||||
new_settings = settings.with(grid: settings.grid.with(enable_background_color: true));
|
||||
return true;
|
||||
}
|
||||
new_settings = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
using bts.utils;
|
||||
using mbts.expression;
|
||||
using mbts.ui;
|
||||
using ProtoBuf;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using static bts.shell.pnl.PNLView;
|
||||
|
||||
namespace bts.shell.pnl {
|
||||
interface IPNLAnimationTrigger<T> {
|
||||
IReadOnlyDictionary<Columns, PNLCellAppearance> trigger(T value);
|
||||
}
|
||||
|
||||
readonly struct PNLAnimationTrigger<T> : IPNLAnimationTrigger<T> {
|
||||
public readonly Func<T, bool> predicate;
|
||||
public readonly IReadOnlyDictionary<Columns, PNLCellAppearance> animations;
|
||||
|
||||
public PNLAnimationTrigger(Func<T, bool> predicate, IReadOnlyDictionary<Columns, PNLCellAppearance> animations) {
|
||||
this.predicate = predicate;
|
||||
this.animations = animations;
|
||||
}
|
||||
|
||||
public IReadOnlyDictionary<Columns, PNLCellAppearance> trigger(T value) =>
|
||||
predicate(value) ? animations : DictionaryHelper.empty<Columns, PNLCellAppearance>();
|
||||
}
|
||||
|
||||
[ProtoContract]
|
||||
class PNLAnimationTriggerData : IEquatable<PNLAnimationTriggerData> {
|
||||
static readonly int _type_hash = typeof(PNLAnimationTriggerData).GetHashCode();
|
||||
|
||||
[ProtoMember(1)]
|
||||
public string expression { get; set; }
|
||||
[ProtoMember(2)]
|
||||
public PNLAnimation[] animations { get; set; }
|
||||
|
||||
public IPNLAnimationTrigger<T> to_trigger<T>() =>
|
||||
new PNLAnimationTrigger<T>(
|
||||
CoreExpressionEvaluator<T>.try_create(expression, out var eval) ?
|
||||
eval.apply : new Func<T, bool>(_ => true),
|
||||
animations.ToDictionary(a => a.column, a => a.to_appearance()));
|
||||
|
||||
public PNLAnimationTriggerData with(string expression = null, PNLAnimation[] animations = null) =>
|
||||
new PNLAnimationTriggerData {
|
||||
expression = expression ?? this.expression,
|
||||
animations = animations ?? this.animations
|
||||
};
|
||||
|
||||
public bool Equals(PNLAnimationTriggerData other) =>
|
||||
other != null &&
|
||||
expression == other.expression &&
|
||||
EqualityComparer.safe_sequence_equals(animations, other.animations);
|
||||
|
||||
public override bool Equals(object obj) =>
|
||||
Equals(obj as PNLAnimationTriggerData);
|
||||
|
||||
public override int GetHashCode() =>
|
||||
_type_hash;
|
||||
}
|
||||
|
||||
[ProtoContract]
|
||||
class PNLAnimation : IEquatable<PNLAnimation> {
|
||||
static readonly int _type_hash = typeof(PNLAnimation).GetHashCode();
|
||||
|
||||
[ProtoMember(1)]
|
||||
public Columns column { get; set; }
|
||||
[ProtoMember(2)]
|
||||
public int? font_weight { get; set; }
|
||||
[ProtoMember(3)]
|
||||
public uint? foreground { get; set; }
|
||||
[ProtoMember(4)]
|
||||
public uint? background { get; set; }
|
||||
|
||||
public PNLCellAppearance to_appearance() =>
|
||||
new PNLCellAppearance(
|
||||
foreground?.argb_color(),
|
||||
background?.argb_color(),
|
||||
font_weight.HasValue ? FontWeight.FromOpenTypeWeight(font_weight.Value) : (FontWeight?)null);
|
||||
|
||||
public bool Equals(PNLAnimation other) =>
|
||||
other != null &&
|
||||
column == other.column &&
|
||||
font_weight == other.font_weight &&
|
||||
foreground == other.foreground &&
|
||||
background == other.background;
|
||||
|
||||
public override bool Equals(object obj) =>
|
||||
Equals(obj as PNLAnimation);
|
||||
|
||||
public override int GetHashCode() =>
|
||||
_type_hash;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
using bts.proto;
|
||||
using bts.utils;
|
||||
using mbts;
|
||||
using mbts.ui;
|
||||
using mbts.ui.utility.text;
|
||||
using ProtoBuf;
|
||||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using FontFamily = System.Windows.Media.FontFamily;
|
||||
|
||||
namespace bts.shell.pnl {
|
||||
readonly struct PNLCellAppearance : IEquatable<PNLCellAppearance> {
|
||||
public readonly FontWeight? font_weight;
|
||||
public readonly Color? foreground, background;
|
||||
public readonly double font_size;
|
||||
public readonly FontFamily font_family;
|
||||
|
||||
public PNLCellAppearance(Color? foreground = null, Color? background = null, FontWeight? font_weight = null, FontFamily font_family = null, double font_size = double.NaN) {
|
||||
this.foreground = foreground;
|
||||
this.background = background;
|
||||
this.font_weight = font_weight;
|
||||
this.font_family = font_family;
|
||||
this.font_size = font_size;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uses the specified appearance to replace null/absent values.
|
||||
/// </summary>
|
||||
public PNLCellAppearance merge(PNLCellAppearance other) =>
|
||||
new PNLCellAppearance(
|
||||
foreground ?? other.foreground,
|
||||
background ?? other.background,
|
||||
font_weight ?? other.font_weight,
|
||||
font_family ?? other.font_family,
|
||||
double.IsNaN(font_size) || font_size == 0 ? other.font_size : font_size);
|
||||
|
||||
public bool Equals(PNLCellAppearance other) =>
|
||||
font_weight == other.font_weight &&
|
||||
foreground == other.foreground &&
|
||||
background == other.background &&
|
||||
font_size.Equals(other.font_size) &&
|
||||
Equals(font_family, other.font_family);
|
||||
|
||||
public override bool Equals(object obj) =>
|
||||
obj is PNLCellAppearance other && Equals(other);
|
||||
|
||||
public override int GetHashCode() =>
|
||||
EqualityComparer.auto_phase(
|
||||
font_weight?.GetHashCode() ?? 0,
|
||||
foreground?.GetHashCode() ?? 0,
|
||||
background?.GetHashCode() ?? 0,
|
||||
font_size.GetHashCode(),
|
||||
font_family?.GetHashCode() ?? 0);
|
||||
}
|
||||
|
||||
[ProtoContract]
|
||||
class PNLCellAppearanceProto : IEquatable<PNLCellAppearanceProto> {
|
||||
static readonly int _type_hash = typeof(PNLCellAppearanceProto).GetHashCode();
|
||||
|
||||
[ProtoMember(1)]
|
||||
public FontParamsProto font_params { get; set; }
|
||||
[ProtoMember(2)]
|
||||
public uint? background { get; set; }
|
||||
|
||||
public static PNLCellAppearanceProto from_appearance(PNLCellAppearance cell_appearance) =>
|
||||
new PNLCellAppearanceProto {
|
||||
font_params = new FontParamsProto {
|
||||
typeface = new TypefaceProto(
|
||||
cell_appearance.font_family?.name(),
|
||||
(cell_appearance.font_weight ?? FontWeights.Normal).ToOpenTypeWeight()),
|
||||
font_color = cell_appearance.foreground?.rgb_color() ?? 0,
|
||||
font_size = cell_appearance.font_size,
|
||||
},
|
||||
background = cell_appearance.background?.rgb_color()
|
||||
};
|
||||
|
||||
public PNLCellAppearance to_appearance() {
|
||||
var font = font_params?.to_wpf_params();
|
||||
return new PNLCellAppearance(
|
||||
font?.color,
|
||||
background?.argb_color(),
|
||||
font?.weight,
|
||||
font?.family,
|
||||
font?.size ?? double.NaN);
|
||||
}
|
||||
|
||||
public bool Equals(PNLCellAppearanceProto other) =>
|
||||
other != null &&
|
||||
background == other.background &&
|
||||
font_params.safe_equals(other.font_params);
|
||||
|
||||
public override bool Equals(object obj) =>
|
||||
Equals(obj as PNLCellAppearanceProto);
|
||||
|
||||
public override int GetHashCode() =>
|
||||
_type_hash;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
using bts.api;
|
||||
using mbts;
|
||||
using mbts.spark;
|
||||
using mbts.spark.filters;
|
||||
using ProtoBuf;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace bts.shell.pnl {
|
||||
[ProtoContract]
|
||||
class FilterStates : IEquatable<FilterStates> {
|
||||
static readonly int _type_hash = typeof(FilterStates).GetHashCode();
|
||||
|
||||
// account
|
||||
[ProtoMember(1)]
|
||||
uint[] _account;
|
||||
public uint[] account {
|
||||
get => _account ?? Array.Empty<uint>();
|
||||
set => _account = value;
|
||||
}
|
||||
// exchange
|
||||
[ProtoMember(2)]
|
||||
Venue[] _exchange;
|
||||
public Venue[] exchange {
|
||||
get => _exchange ?? Array.Empty<Venue>();
|
||||
set => _exchange = value;
|
||||
}
|
||||
// symbol
|
||||
[ProtoMember(3)]
|
||||
string[] _symbol;
|
||||
public string[] symbol {
|
||||
get => _symbol ?? Array.Empty<string>();
|
||||
set => _symbol = value;
|
||||
}
|
||||
// security type
|
||||
[ProtoMember(4)]
|
||||
Security.ContractOneofCase[] _security_type;
|
||||
public Security.ContractOneofCase[] security_type {
|
||||
get => _security_type ?? Array.Empty<Security.ContractOneofCase>();
|
||||
set => _security_type = value;
|
||||
}
|
||||
// security
|
||||
[ProtoMember(5)]
|
||||
public string security { get; set; }
|
||||
[ProtoMember(6)]
|
||||
public string security_key { get; set; }
|
||||
// cpp
|
||||
[ProtoMember(7)]
|
||||
public DoubleMinMaxState cpp { get; set; }
|
||||
|
||||
/// sod
|
||||
[ProtoMember(8)]
|
||||
public DoubleMinMaxState sod_pnl { get; set; }
|
||||
// sod long
|
||||
[ProtoMember(9)]
|
||||
public DoubleMinMaxState sod_long_pos { get; set; }
|
||||
[ProtoMember(10)]
|
||||
public DoubleMinMaxState sod_long_avg_price { get; set; }
|
||||
// sod short
|
||||
[ProtoMember(11)]
|
||||
public DoubleMinMaxState sod_short_pos { get; set; }
|
||||
[ProtoMember(12)]
|
||||
public DoubleMinMaxState sod_short_avg_price { get; set; }
|
||||
// sod net
|
||||
[ProtoMember(13)]
|
||||
public DoubleMinMaxState sod_net_pos { get; set; }
|
||||
|
||||
/// day
|
||||
[ProtoMember(14)]
|
||||
public DoubleMinMaxState day_pnl { get; set; }
|
||||
// day long
|
||||
[ProtoMember(15)]
|
||||
public DoubleMinMaxState day_long_pos { get; set; }
|
||||
[ProtoMember(16)]
|
||||
public DoubleMinMaxState day_long_avg_price { get; set; }
|
||||
// day short
|
||||
[ProtoMember(17)]
|
||||
public DoubleMinMaxState day_short_pos { get; set; }
|
||||
[ProtoMember(18)]
|
||||
public DoubleMinMaxState day_short_avg_price { get; set; }
|
||||
// day net
|
||||
[ProtoMember(19)]
|
||||
public DoubleMinMaxState day_net_pos { get; set; }
|
||||
// has day trades
|
||||
[ProtoMember(20)]
|
||||
bool[] _has_day_trades;
|
||||
public bool[] has_day_trades {
|
||||
get => _has_day_trades ?? Array.Empty<bool>();
|
||||
set => _has_day_trades = value;
|
||||
}
|
||||
|
||||
/// raw day
|
||||
[ProtoMember(21)]
|
||||
public DoubleMinMaxState raw_day_long_pos { get; set; }
|
||||
[ProtoMember(22)]
|
||||
public DoubleMinMaxState raw_day_short_pos { get; set; }
|
||||
[ProtoMember(23)]
|
||||
public DoubleMinMaxState raw_day_net_pos { get; set; }
|
||||
|
||||
/// working (long)
|
||||
[ProtoMember(24)]
|
||||
public DoubleMinMaxState working_long_pos { get; set; }
|
||||
[ProtoMember(25)]
|
||||
public DoubleMinMaxState working_long_avg_price { get; set; }
|
||||
[ProtoMember(26)]
|
||||
// working short
|
||||
public DoubleMinMaxState working_short_pos { get; set; }
|
||||
[ProtoMember(27)]
|
||||
public DoubleMinMaxState working_short_avg_price { get; set; }
|
||||
// working net
|
||||
[ProtoMember(28)]
|
||||
public DoubleMinMaxState working_net_pos { get; set; }
|
||||
|
||||
/// total
|
||||
[ProtoMember(29)]
|
||||
public DoubleMinMaxState total_pnl { get; set; }
|
||||
// total long
|
||||
[ProtoMember(30)]
|
||||
public DoubleMinMaxState total_long_pos { get; set; }
|
||||
[ProtoMember(31)]
|
||||
public DoubleMinMaxState total_long_avg_price { get; set; }
|
||||
// total short
|
||||
[ProtoMember(32)]
|
||||
public DoubleMinMaxState total_short_pos { get; set; }
|
||||
[ProtoMember(33)]
|
||||
public DoubleMinMaxState total_short_avg_price { get; set; }
|
||||
// total net
|
||||
[ProtoMember(34)]
|
||||
public DoubleMinMaxState total_net_pos { get; set; }
|
||||
|
||||
/// market
|
||||
[ProtoMember(35)]
|
||||
public DoubleMinMaxState bid_price { get; set; }
|
||||
[ProtoMember(36)]
|
||||
public DoubleMinMaxState mid_price { get; set; }
|
||||
[ProtoMember(37)]
|
||||
public DoubleMinMaxState ask_price { get; set; }
|
||||
[ProtoMember(38)]
|
||||
public DoubleMinMaxState last_price { get; set; }
|
||||
[ProtoMember(39)]
|
||||
public DoubleMinMaxState settlement_price { get; set; }
|
||||
[ProtoMember(40)]
|
||||
public DoubleMinMaxState pnl_price { get; set; }
|
||||
|
||||
public bool Equals(FilterStates other) =>
|
||||
other != null &&
|
||||
account.SequenceEqual(other.account) &&
|
||||
exchange.SequenceEqual(other.exchange) &&
|
||||
symbol.SequenceEqual(other.symbol) &&
|
||||
security_type.SequenceEqual(other.security_type) &&
|
||||
security == other.security &&
|
||||
security_key == other.security_key &&
|
||||
cpp.safe_equals(other.cpp) &&
|
||||
/// sod
|
||||
sod_pnl.safe_equals(other.sod_pnl) &&
|
||||
sod_long_pos.safe_equals(other.sod_long_pos) &&
|
||||
sod_long_avg_price.safe_equals(other.sod_long_avg_price) &&
|
||||
sod_short_pos.safe_equals(other.sod_short_pos) &&
|
||||
sod_short_avg_price.safe_equals(other.sod_short_avg_price) &&
|
||||
sod_net_pos.safe_equals(other.sod_net_pos) &&
|
||||
/// day
|
||||
day_pnl.safe_equals(other.day_pnl) &&
|
||||
day_long_pos.safe_equals(other.day_long_pos) &&
|
||||
day_long_avg_price.safe_equals(other.day_long_avg_price) &&
|
||||
day_short_pos.safe_equals(other.day_short_pos) &&
|
||||
day_short_avg_price.safe_equals(other.day_short_avg_price) &&
|
||||
day_net_pos.safe_equals(other.day_net_pos) &&
|
||||
has_day_trades.SequenceEqual(other.has_day_trades) &&
|
||||
/// raw day
|
||||
raw_day_long_pos.safe_equals(other.raw_day_long_pos) &&
|
||||
raw_day_short_pos.safe_equals(other.raw_day_short_pos) &&
|
||||
raw_day_net_pos.safe_equals(other.raw_day_net_pos) &&
|
||||
/// working
|
||||
working_long_pos.safe_equals(other.working_long_pos) &&
|
||||
working_long_avg_price.safe_equals(other.working_long_avg_price) &&
|
||||
working_short_pos.safe_equals(other.working_short_pos) &&
|
||||
working_short_avg_price.safe_equals(other.working_short_avg_price) &&
|
||||
working_net_pos.safe_equals(other.working_net_pos) &&
|
||||
/// total
|
||||
total_pnl.safe_equals(other.total_pnl) &&
|
||||
total_long_pos.safe_equals(other.total_long_pos) &&
|
||||
total_long_avg_price.safe_equals(other.total_long_avg_price) &&
|
||||
total_short_pos.safe_equals(other.total_short_pos) &&
|
||||
total_short_avg_price.safe_equals(other.total_short_avg_price) &&
|
||||
total_net_pos.safe_equals(other.total_net_pos) &&
|
||||
/// market
|
||||
bid_price.safe_equals(other.bid_price) &&
|
||||
ask_price.safe_equals(other.ask_price) &&
|
||||
mid_price.safe_equals(other.mid_price) &&
|
||||
last_price.safe_equals(other.last_price) &&
|
||||
settlement_price.safe_equals(other.settlement_price) &&
|
||||
pnl_price.safe_equals(other.pnl_price);
|
||||
|
||||
public override bool Equals(object obj) =>
|
||||
Equals(obj as FilterStates);
|
||||
|
||||
public override int GetHashCode() =>
|
||||
_type_hash;
|
||||
}
|
||||
|
||||
static class PNLFilterStatesExtensions {
|
||||
public static Func<PNLRowData, bool> build_predicate(this FilterStates fs) {
|
||||
var bd = new ColumnPredicateBuilder<PNLRowData>();
|
||||
// impl (add all)
|
||||
bd.add_discrete_values(fs.account, d => d.key.account_id);
|
||||
bd.add_discrete_values(fs.exchange, d => d.security.exchange_or_default());
|
||||
bd.add_discrete_values(fs.symbol, d => d.security.symbol_or_default());
|
||||
bd.add_discrete_values(fs.security_type, d => d.security.HasValue ? d.security.Value.ContractCase : Security.ContractOneofCase.None);
|
||||
bd.add_regex(fs.security, d => d.security.ToString());
|
||||
bd.add_regex(fs.security_key, d => d.key.security_id.ToString());
|
||||
bd.add_numeric(fs.cpp, d => d.cpp);
|
||||
/// sod
|
||||
bd.add_numeric(fs.sod_pnl, d => d.sod_pnl);
|
||||
bd.add_numeric(fs.sod_long_pos, d => d.sod_long_pos);
|
||||
bd.add_numeric(fs.sod_long_avg_price, d => d.sod_long_avg_price_raw);
|
||||
bd.add_numeric(fs.sod_short_pos, d => d.sod_short_pos);
|
||||
bd.add_numeric(fs.sod_short_avg_price, d => d.sod_short_avg_price_raw);
|
||||
bd.add_numeric(fs.sod_net_pos, d => d.sod_net_pos);
|
||||
/// day
|
||||
bd.add_numeric(fs.day_pnl, d => d.day_pnl);
|
||||
bd.add_numeric(fs.day_long_pos, d => d.day_long_pos);
|
||||
bd.add_numeric(fs.day_long_avg_price, d => d.day_long_avg_price_raw);
|
||||
bd.add_numeric(fs.day_short_pos, d => d.day_short_pos);
|
||||
bd.add_numeric(fs.day_short_avg_price, d => d.day_short_avg_price_raw);
|
||||
bd.add_numeric(fs.day_net_pos, d => d.day_net_pos);
|
||||
bd.add_discrete_values(fs.has_day_trades, d => d.has_day_trades);
|
||||
/// raw day
|
||||
bd.add_numeric(fs.raw_day_long_pos, d => d.raw_day_long_pos);
|
||||
bd.add_numeric(fs.raw_day_short_pos, d => d.raw_day_short_pos);
|
||||
bd.add_numeric(fs.raw_day_net_pos, d => d.raw_day_net_pos);
|
||||
/// working
|
||||
bd.add_numeric(fs.working_long_pos, d => d.working_long_pos);
|
||||
bd.add_numeric(fs.working_long_avg_price, d => d.working_long_avg_price_raw);
|
||||
bd.add_numeric(fs.working_short_pos, d => d.working_short_pos);
|
||||
bd.add_numeric(fs.working_short_avg_price, d => d.working_short_avg_price_raw);
|
||||
bd.add_numeric(fs.working_net_pos, d => d.working_net_pos);
|
||||
/// total
|
||||
bd.add_numeric(fs.total_pnl, d => d.total_pnl);
|
||||
bd.add_numeric(fs.total_long_pos, d => d.total_long_pos);
|
||||
bd.add_numeric(fs.total_long_avg_price, d => d.total_long_avg_price_raw);
|
||||
bd.add_numeric(fs.total_short_pos, d => d.total_short_pos);
|
||||
bd.add_numeric(fs.total_short_avg_price, d => d.total_short_avg_price_raw);
|
||||
bd.add_numeric(fs.total_net_pos, d => d.total_net_pos);
|
||||
/// market
|
||||
bd.add_numeric(fs.bid_price, d => d.bid_price);
|
||||
bd.add_numeric(fs.ask_price, d => d.ask_price);
|
||||
bd.add_numeric(fs.mid_price, d => d.mid_price);
|
||||
bd.add_numeric(fs.last_price, d => d.last_price);
|
||||
bd.add_numeric(fs.settlement_price, d => d.settlement_price);
|
||||
bd.add_numeric(fs.pnl_price, d => d.pnl_price);
|
||||
//
|
||||
return bd.to_predicate();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,489 @@
|
||||
using bts.api;
|
||||
using bts.proto;
|
||||
using mbts;
|
||||
using mbts.ui;
|
||||
using mbts.ui.settings;
|
||||
using ProtoBuf;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using static bts.shell.pnl.PNLView;
|
||||
using static mbts.ui.controls.DIYGrid;
|
||||
using Headers = mbts.ui.controls.DIYGrid.DefaultColumnHeaders.PNLHeaders;
|
||||
using Widths = mbts.ui.controls.DIYGrid.DefaultColumnWidths.PNL;
|
||||
|
||||
namespace bts.shell.pnl {
|
||||
[ProtoContract]
|
||||
class PNLSaveState : IEquatable<PNLSaveState> {
|
||||
[ProtoMember(1)]
|
||||
public bool use_defaults { get; set; }
|
||||
[ProtoMember(2)]
|
||||
public PNLSettings settings { get; set; }
|
||||
|
||||
public bool Equals(PNLSaveState other) =>
|
||||
other != null &&
|
||||
use_defaults == other.use_defaults &&
|
||||
(use_defaults || settings.safe_equals(other.settings));
|
||||
}
|
||||
|
||||
[ProtoContract]
|
||||
class PNLSettings : IEquatable<PNLSettings> {
|
||||
const double _default_group_font_size = double.NaN;
|
||||
// bold
|
||||
const int _default_group_font_weight = 700;
|
||||
|
||||
public static readonly IReadOnlyList<PNLHeaderRowDisplay> header_row_display_options =
|
||||
new[] {
|
||||
PNLHeaderRowDisplay.None,
|
||||
PNLHeaderRowDisplay.Total,
|
||||
PNLHeaderRowDisplay.SelectedTotal,
|
||||
PNLHeaderRowDisplay.Total | PNLHeaderRowDisplay.SelectedTotal
|
||||
};
|
||||
|
||||
public static readonly IReadOnlyList<Columns> anchorable_columns =
|
||||
new[] {
|
||||
Columns.Account,
|
||||
Columns.Exchange,
|
||||
Columns.Symbol,
|
||||
Columns.SecurityType,
|
||||
Columns.Security
|
||||
};
|
||||
|
||||
[ProtoMember(1)]
|
||||
SortingCriteria[] _sorting_criterion;
|
||||
[ProtoMember(5)]
|
||||
Columns[] _anchors;
|
||||
[ProtoMember(7)]
|
||||
PNLCellAppearanceProto _group_cell_appearance;
|
||||
[ProtoMember(8)]
|
||||
PNLCellAppearanceProto _header_cell_appearance;
|
||||
// don't store in proto yet (this type may change)
|
||||
[ProtoMember(9)]
|
||||
PNLAnimationTriggerData[] _animations;
|
||||
[ProtoMember(10)]
|
||||
PNLAnimationTriggerData[] _group_header_animations;
|
||||
[ProtoMember(11)]
|
||||
GridColumnSettings[] _group_columns;
|
||||
[ProtoMember(12)]
|
||||
GridColumnSettings[] _header_columns;
|
||||
[ProtoMember(13)]
|
||||
PNLAnimationTriggerData[] _header_row_animations;
|
||||
[ProtoMember(14)]
|
||||
List<uint> _group_background_colors;
|
||||
|
||||
GridSettings _grid_backing;
|
||||
PNLCellAppearanceProto _grid_cell_appearance_backing;
|
||||
|
||||
public static PNLSettings Default =>
|
||||
new PNLSettings {
|
||||
_sorting_criterion = new[] {
|
||||
new SortingCriteria(nameof(Columns.Account), true),
|
||||
new SortingCriteria(nameof(Columns.Security), true)
|
||||
},
|
||||
filters = new FilterStates {
|
||||
security_type = new Security.ContractOneofCase[] {
|
||||
Security.ContractOneofCase.Future
|
||||
},
|
||||
has_day_trades = new[] { true }
|
||||
},
|
||||
grid = DefaultGridSettings,
|
||||
calculation_type = PNLCalculationType.Mid,
|
||||
header_row_display = PNLHeaderRowDisplay.None,
|
||||
_anchors = new[] { Columns.Account, Columns.Symbol },
|
||||
_group_background_colors = BTSColors.PNL.group_alternating_background_colors.Select(c => c.rgb_color()).ToList(),
|
||||
_group_cell_appearance = new PNLCellAppearanceProto {
|
||||
font_params = new FontParamsProto {
|
||||
typeface = new TypefaceProto(
|
||||
GridSettings.Default.font_family,
|
||||
FontWeights.Bold.ToOpenTypeWeight()),
|
||||
font_color = Colors.Black.rgb_color(),
|
||||
font_size = 11
|
||||
},
|
||||
background = Colors.Transparent.rgb_color()
|
||||
},
|
||||
_header_cell_appearance = new PNLCellAppearanceProto {
|
||||
font_params = new FontParamsProto {
|
||||
typeface = new TypefaceProto(
|
||||
GridSettings.Default.font_family,
|
||||
FontWeights.Bold.ToOpenTypeWeight()),
|
||||
font_color = Colors.Black.rgb_color(),
|
||||
font_size = 12
|
||||
},
|
||||
background = BTSColors.PNL.header_row_background_color.rgb_color()
|
||||
}
|
||||
};
|
||||
|
||||
public static GridSettings DefaultGridSettings =>
|
||||
GridSettings.Default.with(
|
||||
DefaultColumns.all,
|
||||
DefaultColumns.default_sorting_criterion,
|
||||
enable_background_color: true);
|
||||
|
||||
public static fills.FillsSettings DefaultAuxiliaryFills {
|
||||
get {
|
||||
var grid = fills.FillsSettings.MiniGridSettings;
|
||||
if (grid.columns.TryGetFirst(c => c.id == nameof(mbts.ui.spark.FillsView.Columns.Side), out var side_col, out int side_col_pos)) {
|
||||
var cols = grid.columns.ToList();
|
||||
cols[side_col_pos] = side_col.with(is_visible: false);
|
||||
grid = grid.with(cols);
|
||||
}
|
||||
var rv = fills.FillsSettings.Default();
|
||||
rv.grid = grid;
|
||||
return rv;
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<PNLAnimationTriggerData> default_animations {
|
||||
get {
|
||||
yield return _trigger($"{nameof(PNLRowData.sod_pnl)} > 0", _animation(foreground: BTSColors.PNL.profit_color, font_weight: FontWeights.Bold, column: Columns.SODPNL));
|
||||
yield return _trigger($"{nameof(PNLRowData.sod_pnl)} < 0", _animation(foreground: BTSColors.PNL.loss_color, font_weight: FontWeights.Bold, column: Columns.SODPNL));
|
||||
yield return _trigger($"{nameof(PNLRowData.sod_net_pos)} > 0", _animation(foreground: BTSColors.MarketData.bid_color, column: Columns.SODNetPos));
|
||||
yield return _trigger($"{nameof(PNLRowData.sod_net_pos)} < 0", _animation(foreground: BTSColors.MarketData.ask_color, column: Columns.SODNetPos));
|
||||
// day
|
||||
yield return _trigger($"{nameof(PNLRowData.day_pnl)} > 0", _animation(foreground: BTSColors.PNL.profit_color, font_weight: FontWeights.Bold, column: Columns.DayPNL));
|
||||
yield return _trigger($"{nameof(PNLRowData.day_pnl)} < 0", _animation(foreground: BTSColors.PNL.loss_color, font_weight: FontWeights.Bold, column: Columns.DayPNL));
|
||||
yield return _trigger($"{nameof(PNLRowData.day_net_pos)} > 0", _animation(foreground: BTSColors.MarketData.bid_color, column: Columns.DayNetPos));
|
||||
yield return _trigger($"{nameof(PNLRowData.day_net_pos)} < 0", _animation(foreground: BTSColors.MarketData.ask_color, column: Columns.DayNetPos));
|
||||
// raw day
|
||||
yield return _trigger($"{nameof(PNLRowData.raw_day_net_pos)} > 0", _animation(foreground: BTSColors.MarketData.bid_color, column: Columns.RawDayNetPos));
|
||||
yield return _trigger($"{nameof(PNLRowData.raw_day_net_pos)} < 0", _animation(foreground: BTSColors.MarketData.ask_color, column: Columns.RawDayNetPos));
|
||||
// total
|
||||
yield return _trigger($"{nameof(PNLRowData.total_pnl)} > 0", _animation(foreground: BTSColors.PNL.profit_color, font_weight: FontWeights.Bold, column: Columns.TotalPNL));
|
||||
yield return _trigger($"{nameof(PNLRowData.total_pnl)} < 0", _animation(foreground: BTSColors.PNL.loss_color, font_weight: FontWeights.Bold, column: Columns.TotalPNL));
|
||||
yield return _trigger($"{nameof(PNLRowData.total_net_pos)} > 0", _animation(foreground: BTSColors.MarketData.bid_color, column: Columns.TotalNetPos));
|
||||
yield return _trigger($"{nameof(PNLRowData.total_net_pos)} < 0", _animation(foreground: BTSColors.MarketData.ask_color, column: Columns.TotalNetPos));
|
||||
// working
|
||||
yield return _trigger($"{nameof(PNLRowData.working_net_pos)} > 0", _animation(foreground: BTSColors.MarketData.bid_color, column: Columns.WorkingNetPos));
|
||||
yield return _trigger($"{nameof(PNLRowData.working_net_pos)} < 0", _animation(foreground: BTSColors.MarketData.ask_color, column: Columns.WorkingNetPos));
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<PNLAnimationTriggerData> default_group_header_animations {
|
||||
get {
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.sod_pnl)} > 0", _animation(foreground: BTSColors.PNL.profit_color, column: Columns.SODPNL));
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.sod_pnl)} < 0", _animation(foreground: BTSColors.PNL.loss_color, column: Columns.SODPNL));
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.sod_net_pos)} > 0", _animation(foreground: BTSColors.MarketData.bid_color, column: Columns.SODNetPos));
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.sod_net_pos)} < 0", _animation(foreground: BTSColors.MarketData.ask_color, column: Columns.SODNetPos));
|
||||
// day
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.day_pnl)} > 0", _animation(foreground: BTSColors.PNL.profit_color, column: Columns.DayPNL));
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.day_pnl)} < 0", _animation(foreground: BTSColors.PNL.loss_color, column: Columns.DayPNL));
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.day_net_pos)} > 0", _animation(foreground: BTSColors.MarketData.bid_color, column: Columns.DayNetPos));
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.day_net_pos)} < 0", _animation(foreground: BTSColors.MarketData.ask_color, column: Columns.DayNetPos));
|
||||
// raw day
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.raw_day_net_pos)} > 0", _animation(foreground: BTSColors.MarketData.bid_color, column: Columns.RawDayNetPos));
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.raw_day_net_pos)} < 0", _animation(foreground: BTSColors.MarketData.ask_color, column: Columns.RawDayNetPos));
|
||||
// total
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.total_pnl)} > 0", _animation(foreground: BTSColors.PNL.profit_color, column: Columns.TotalPNL));
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.total_pnl)} < 0", _animation(foreground: BTSColors.PNL.loss_color, column: Columns.TotalPNL));
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.total_net_pos)} > 0", _animation(foreground: BTSColors.MarketData.bid_color, column: Columns.TotalNetPos));
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.total_net_pos)} < 0", _animation(foreground: BTSColors.MarketData.ask_color, column: Columns.TotalNetPos));
|
||||
// working
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.working_net_pos)} > 0", _animation(foreground: BTSColors.MarketData.bid_color, column: Columns.WorkingNetPos));
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.working_net_pos)} < 0", _animation(foreground: BTSColors.MarketData.ask_color, column: Columns.WorkingNetPos));
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<PNLAnimationTriggerData> default_header_row_animations {
|
||||
get {
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.sod_pnl)} > 0", _animation(foreground: BTSColors.PNL.profit_color, column: Columns.SODPNL));
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.sod_pnl)} < 0", _animation(foreground: BTSColors.PNL.loss_color, column: Columns.SODPNL));
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.sod_net_pos)} > 0", _animation(foreground: BTSColors.MarketData.bid_color, column: Columns.SODNetPos));
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.sod_net_pos)} < 0", _animation(foreground: BTSColors.MarketData.ask_color, column: Columns.SODNetPos));
|
||||
// day
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.day_pnl)} > 0", _animation(foreground: BTSColors.PNL.profit_color, column: Columns.DayPNL));
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.day_pnl)} < 0", _animation(foreground: BTSColors.PNL.loss_color, column: Columns.DayPNL));
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.day_net_pos)} > 0", _animation(foreground: BTSColors.MarketData.bid_color, column: Columns.DayNetPos));
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.day_net_pos)} < 0", _animation(foreground: BTSColors.MarketData.ask_color, column: Columns.DayNetPos));
|
||||
// raw day
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.raw_day_net_pos)} > 0", _animation(foreground: BTSColors.MarketData.bid_color, column: Columns.RawDayNetPos));
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.raw_day_net_pos)} < 0", _animation(foreground: BTSColors.MarketData.ask_color, column: Columns.RawDayNetPos));
|
||||
// total
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.total_pnl)} > 0", _animation(foreground: BTSColors.PNL.profit_color, column: Columns.TotalPNL));
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.total_pnl)} < 0", _animation(foreground: BTSColors.PNL.loss_color, column: Columns.TotalPNL));
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.total_net_pos)} > 0", _animation(foreground: BTSColors.MarketData.bid_color, column: Columns.TotalNetPos));
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.total_net_pos)} < 0", _animation(foreground: BTSColors.MarketData.ask_color, column: Columns.TotalNetPos));
|
||||
// working
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.working_net_pos)} > 0", _animation(foreground: BTSColors.MarketData.bid_color, column: Columns.WorkingNetPos));
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.working_net_pos)} < 0", _animation(foreground: BTSColors.MarketData.ask_color, column: Columns.WorkingNetPos));
|
||||
}
|
||||
}
|
||||
|
||||
[ProtoMember(2)]
|
||||
public FilterStates filters { get; set; }
|
||||
[ProtoMember(3)]
|
||||
public PNLCalculationType calculation_type { get; set; }
|
||||
[ProtoMember(4)]
|
||||
public GridSettings grid {
|
||||
get => _grid_backing;
|
||||
set {
|
||||
_grid_backing = value;
|
||||
_grid_cell_appearance_backing = null;
|
||||
}
|
||||
}
|
||||
[ProtoMember(6)]
|
||||
public PNLHeaderRowDisplay header_row_display { get; set; }
|
||||
|
||||
public IReadOnlyList<Columns> anchors => _anchors ?? (_anchors = Array.Empty<Columns>());
|
||||
public IReadOnlyList<PNLAnimationTriggerData> animations => _animations ?? (_animations = default_animations.ToArray());
|
||||
|
||||
public PNLCellAppearance grid_defaults =>
|
||||
new PNLCellAppearance(
|
||||
font_family: new FontFamily(grid.font_family),
|
||||
font_size: grid.font_size,
|
||||
font_weight: grid.font_weight,
|
||||
background: grid.background,
|
||||
foreground: grid.font_color.rgb_color());
|
||||
|
||||
// Group
|
||||
public List<uint> group_background_colors => _group_background_colors ?? (_group_background_colors = new List<uint>(0));
|
||||
public PNLCellAppearanceProto group_cell_appearance { get => _group_cell_appearance ?? _grid_cell_appearance; set => _group_cell_appearance = value; }
|
||||
public IReadOnlyList<GridColumnSettings> group_columns => _group_columns ?? (_group_columns = DefaultColumns.group_columns.ToArray());
|
||||
public PNLCellAppearance group_defaults => group_cell_appearance.to_appearance().merge(grid_defaults);
|
||||
public IReadOnlyList<PNLAnimationTriggerData> group_header_animations => _group_header_animations ?? (_group_header_animations = default_group_header_animations.ToArray());
|
||||
// Header
|
||||
public Color header_background_color => header_cell_appearance.background?.argb_color() ?? Colors.Transparent;
|
||||
public PNLCellAppearanceProto header_cell_appearance { get => _header_cell_appearance ?? _grid_cell_appearance; set => _header_cell_appearance = value; }
|
||||
public IReadOnlyList<GridColumnSettings> header_columns => _header_columns ?? (_header_columns = DefaultColumns.header_columns.ToArray());
|
||||
public PNLCellAppearance header_defaults => header_cell_appearance.to_appearance().merge(grid_defaults);
|
||||
public IReadOnlyList<PNLAnimationTriggerData> header_row_animations => _header_row_animations ?? (_header_row_animations = default_header_row_animations.ToArray());
|
||||
//
|
||||
public IReadOnlyList<SortingCriteria> sorting_criterion =>
|
||||
_sorting_criterion ?? (_sorting_criterion = DefaultColumns.default_sorting_criterion.ToArray());
|
||||
|
||||
PNLCellAppearanceProto _grid_cell_appearance => _grid_cell_appearance_backing ?? (
|
||||
_grid_cell_appearance_backing = new PNLCellAppearanceProto {
|
||||
background = grid.background.rgb_color(),
|
||||
font_params = new FontParamsProto {
|
||||
font_color = grid.font_color,
|
||||
typeface = new TypefaceProto(
|
||||
grid.font_family,
|
||||
grid.font_weight.ToOpenTypeWeight()),
|
||||
font_size = grid.font_size
|
||||
}
|
||||
});
|
||||
|
||||
public PNLSettings with(
|
||||
IEnumerable<SortingCriteria> sorting_criterion = null,
|
||||
IEnumerable<Columns> anchors = null,
|
||||
FilterStates filters = null,
|
||||
GridSettings grid = null,
|
||||
PNLCalculationType? calculation_type = null,
|
||||
PNLHeaderRowDisplay? header_row_display = null,
|
||||
IEnumerable<GridColumnSettings> group_columns = null,
|
||||
IEnumerable<PNLAnimationTriggerData> animations = null,
|
||||
IEnumerable<PNLAnimationTriggerData> group_header_animations = null,
|
||||
IEnumerable<PNLAnimationTriggerData> header_row_animations = null,
|
||||
IEnumerable<GridColumnSettings> header_columns = null) =>
|
||||
new PNLSettings {
|
||||
calculation_type = calculation_type ?? this.calculation_type,
|
||||
header_row_display = header_row_display ?? this.header_row_display,
|
||||
// row operators (anchors, sorting, filters)
|
||||
_anchors = (anchors ?? _anchors.empty_if_null()).ToArray(),
|
||||
_sorting_criterion = (sorting_criterion ?? _sorting_criterion.empty_if_null()).ToArray(),
|
||||
filters = filters ?? this.filters,
|
||||
// cell appearance (group/header)
|
||||
_group_background_colors = _group_background_colors?.ToList(),
|
||||
_group_cell_appearance = _group_cell_appearance,
|
||||
_header_cell_appearance = _header_cell_appearance,
|
||||
// columns (group/header)
|
||||
_group_columns = group_columns?.ToArray() ?? _group_columns,
|
||||
_header_columns = header_columns?.ToArray() ?? _header_columns,
|
||||
// animations
|
||||
_animations = animations?.ToArray() ?? _animations,
|
||||
_group_header_animations = group_header_animations?.ToArray() ?? _group_header_animations,
|
||||
_header_row_animations = header_row_animations?.ToArray() ?? _header_row_animations,
|
||||
// grid
|
||||
grid = grid ?? this.grid
|
||||
};
|
||||
|
||||
public bool Equals(PNLSettings other) =>
|
||||
other != null &&
|
||||
calculation_type == other.calculation_type &&
|
||||
header_row_display == other.header_row_display &&
|
||||
// row operators (anchors, sorting, filters)
|
||||
anchors.SequenceEqual(other.anchors) &&
|
||||
sorting_criterion.SequenceEqual(other.sorting_criterion) &&
|
||||
filters.safe_equals(other.filters) &&
|
||||
// cell appearance (group/header)
|
||||
group_background_colors.SequenceEqual(other.group_background_colors) &&
|
||||
group_cell_appearance.safe_equals(other.group_cell_appearance) &&
|
||||
header_cell_appearance.safe_equals(other.header_cell_appearance) &&
|
||||
// columns (group/header)
|
||||
group_columns.SequenceEqual(other.group_columns) &&
|
||||
header_columns.SequenceEqual(other.header_columns) &&
|
||||
// animations
|
||||
animations.SequenceEqual(other.animations) &&
|
||||
group_header_animations.SequenceEqual(other.group_header_animations) &&
|
||||
header_row_animations.SequenceEqual(other.header_row_animations) &&
|
||||
// grid
|
||||
grid.safe_equals(other.grid);
|
||||
|
||||
static PNLAnimationTriggerData _trigger(string expression, params PNLAnimation[] animations) =>
|
||||
new PNLAnimationTriggerData {
|
||||
expression = expression,
|
||||
animations = animations
|
||||
};
|
||||
|
||||
static PNLAnimation _animation(
|
||||
Columns column,
|
||||
Color? foreground = null,
|
||||
Color? background = null,
|
||||
FontWeight? font_weight = null) =>
|
||||
new PNLAnimation {
|
||||
column = column,
|
||||
foreground = foreground?.rgb_color(),
|
||||
background = background?.rgb_color(),
|
||||
font_weight = font_weight?.ToOpenTypeWeight()
|
||||
};
|
||||
|
||||
static class DefaultColumns {
|
||||
public static GridColumnSettings account => new GridColumnSettings(nameof(Columns.Account), DefaultColumnHeaders.Account, width: Widths.Account);
|
||||
public static GridColumnSettings exchange => new GridColumnSettings(nameof(Columns.Exchange), DefaultColumnHeaders.Exchange, false, width: Widths.Exchange);
|
||||
public static GridColumnSettings symbol => new GridColumnSettings(nameof(Columns.Symbol), DefaultColumnHeaders.Symbol, width: Widths.Symbol);
|
||||
public static GridColumnSettings security_type => new GridColumnSettings(nameof(Columns.SecurityType), Headers.SecurityType, width: Widths.SecurityType);
|
||||
public static GridColumnSettings security => new GridColumnSettings(nameof(Columns.Security), DefaultColumnHeaders.Security, width: Widths.Security);
|
||||
public static GridColumnSettings security_key => new GridColumnSettings(nameof(Columns.SecurityKey), DefaultColumnHeaders.SecurityKey, false, width: DefaultColumnWidths.SecurityKey);
|
||||
public static GridColumnSettings cpp => new GridColumnSettings(nameof(Columns.CPP), "CPP", false, width: 50, horizontal_alignment: HorizontalAlignment.Right);
|
||||
// sod
|
||||
public static GridColumnSettings sod_pnl => new GridColumnSettings(nameof(Columns.SODPNL), Headers.SOD.PNL, false, width: Widths.PriceBig, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings sod_long_pos => new GridColumnSettings(nameof(Columns.SODLongPos), Headers.SOD.Long, false, width: Widths.Qty, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings sod_long_avg_price => new GridColumnSettings(nameof(Columns.SODLongAvgPrice), Headers.SOD.LongAvg, false, width: Widths.Price, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings sod_short_pos => new GridColumnSettings(nameof(Columns.SODShortPos), Headers.SOD.Short, false, width: Widths.Qty, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings sod_short_avg_price => new GridColumnSettings(nameof(Columns.SODShortAvgPrice), Headers.SOD.ShortAvg, false, width: Widths.Price, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings sod_net_pos => new GridColumnSettings(nameof(Columns.SODNetPos), Headers.SOD.Net, false, width: Widths.Qty, horizontal_alignment: HorizontalAlignment.Right);
|
||||
// day
|
||||
public static GridColumnSettings day_pnl => new GridColumnSettings(nameof(Columns.DayPNL), Headers.Day.PNL, width: Widths.PriceBig, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings day_long_pos => new GridColumnSettings(nameof(Columns.DayLongPos), Headers.Day.Long, false, width: Widths.Qty, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings day_long_avg_price => new GridColumnSettings(nameof(Columns.DayLongAvgPrice), Headers.Day.LongAvg, false, width: Widths.Price, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings day_short_pos => new GridColumnSettings(nameof(Columns.DayShortPos), Headers.Day.Short, false, width: Widths.Qty, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings day_short_avg_price => new GridColumnSettings(nameof(Columns.DayShortAvgPrice), Headers.Day.ShortAvg, false, width: Widths.Price, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings day_net_pos => new GridColumnSettings(nameof(Columns.DayNetPos), Headers.Day.Net, width: Widths.Qty, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings has_day_trades => new GridColumnSettings(nameof(Columns.HasDayTrades), Headers.HasDayTrades, width: Widths.HasDayTrades, horizontal_alignment: HorizontalAlignment.Right);
|
||||
// raw day
|
||||
public static GridColumnSettings raw_day_long_pos => new GridColumnSettings(nameof(Columns.RawDayLongPos), Headers.RawDay.Long, false, width: Widths.RawDayQty, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings raw_day_short_pos => new GridColumnSettings(nameof(Columns.RawDayShortPos), Headers.RawDay.Short, false, width: Widths.RawDayQty, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings raw_day_net_pos => new GridColumnSettings(nameof(Columns.RawDayNetPos), Headers.RawDay.Net, false, width: Widths.RawDayQty, horizontal_alignment: HorizontalAlignment.Right);
|
||||
// working
|
||||
public static GridColumnSettings working_long_pos => new GridColumnSettings(nameof(Columns.WorkingLongPos), Headers.Working.Long, false, width: Widths.WorkingQty, font_color_override: BTSColors.MarketData.bid, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings working_long_avg_price => new GridColumnSettings(nameof(Columns.WorkingLongAvgPrice), Headers.Working.LongAvg, false, width: Widths.Price, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings working_short_pos => new GridColumnSettings(nameof(Columns.WorkingShortPos), Headers.Working.Short, false, width: Widths.WorkingQty, font_color_override: BTSColors.MarketData.ask, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings working_short_avg_price => new GridColumnSettings(nameof(Columns.WorkingShortAvgPrice), Headers.Working.ShortAvg, false, width: Widths.Price, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings working_net_pos => new GridColumnSettings(nameof(Columns.WorkingNetPos), Headers.Working.Net, width: Widths.WorkingQty, horizontal_alignment: HorizontalAlignment.Right);
|
||||
// total
|
||||
public static GridColumnSettings total_pnl => new GridColumnSettings(nameof(Columns.TotalPNL), Headers.Total.PNL, false, width: Widths.PriceBig, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings total_long_pos => new GridColumnSettings(nameof(Columns.TotalLongPos), Headers.Total.Long, false, width: Widths.Qty, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings total_long_avg_price => new GridColumnSettings(nameof(Columns.TotalLongAvgPrice), Headers.Total.LongAvg, false, width: Widths.Price, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings total_short_pos => new GridColumnSettings(nameof(Columns.TotalShortPos), Headers.Total.Short, false, width: Widths.Qty, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings total_short_avg_price => new GridColumnSettings(nameof(Columns.TotalShortAvgPrice), Headers.Total.ShortAvg, false, width: Widths.Price, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings total_net_pos => new GridColumnSettings(nameof(Columns.TotalNetPos), Headers.Total.Net, false, width: Widths.Qty, horizontal_alignment: HorizontalAlignment.Right);
|
||||
// market
|
||||
public static GridColumnSettings bid_price => new GridColumnSettings(nameof(Columns.BidPrice), DefaultColumnHeaders.Bid, false, width: DefaultColumnWidths.Price, font_color_override: BTSColors.MarketData.bid, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings ask_price => new GridColumnSettings(nameof(Columns.AskPrice), DefaultColumnHeaders.Ask, false, width: DefaultColumnWidths.Price, font_color_override: BTSColors.MarketData.ask, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings mid_price => new GridColumnSettings(nameof(Columns.MidPrice), DefaultColumnHeaders.Mid, false, width: DefaultColumnWidths.Price, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings last_price => new GridColumnSettings(nameof(Columns.LastPrice), DefaultColumnHeaders.Last, false, width: DefaultColumnWidths.Price, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings settlement_price => new GridColumnSettings(nameof(Columns.SettlementPrice), DefaultColumnHeaders.Settlement, false, width: DefaultColumnWidths.Settlement, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings pnl_price => new GridColumnSettings(nameof(Columns.PNLPrice), DefaultColumnHeaders.PnLPrice, width: DefaultColumnWidths.Price, horizontal_alignment: HorizontalAlignment.Right);
|
||||
|
||||
public static IEnumerable<SortingCriteria> default_sorting_criterion {
|
||||
get {
|
||||
yield return new SortingCriteria(account.id, true);
|
||||
yield return new SortingCriteria(security.id, true);
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<GridColumnSettings> all {
|
||||
get {
|
||||
yield return account;
|
||||
yield return exchange;
|
||||
yield return symbol;
|
||||
yield return security_type;
|
||||
yield return security;
|
||||
yield return security_key; // hidden
|
||||
|
||||
yield return total_pnl; // hidden
|
||||
yield return sod_pnl;
|
||||
yield return day_pnl;
|
||||
/// total
|
||||
yield return total_net_pos;
|
||||
// total long
|
||||
yield return total_long_pos; // hidden
|
||||
yield return total_long_avg_price; // hidden
|
||||
// total short
|
||||
yield return total_short_pos; // hidden
|
||||
yield return total_short_avg_price; // hidden
|
||||
/// sod
|
||||
yield return sod_net_pos;
|
||||
// sod long
|
||||
yield return sod_long_pos; // hidden
|
||||
yield return sod_long_avg_price; // hidden
|
||||
// sod short
|
||||
yield return sod_short_pos; // hidden
|
||||
yield return sod_short_avg_price; // hidden
|
||||
/// day
|
||||
yield return day_net_pos;
|
||||
// day long
|
||||
yield return day_long_pos; // hidden
|
||||
yield return day_long_avg_price; // hidden
|
||||
// day short
|
||||
yield return day_short_pos; // hidden
|
||||
yield return day_short_avg_price; // hidden
|
||||
/// raw day
|
||||
yield return raw_day_long_pos; // hidden
|
||||
yield return raw_day_short_pos; // hidden
|
||||
yield return raw_day_net_pos; // hidden
|
||||
|
||||
yield return has_day_trades;
|
||||
/// working
|
||||
// working net
|
||||
yield return working_net_pos;
|
||||
// working long
|
||||
yield return working_long_pos;
|
||||
yield return working_long_avg_price;
|
||||
// working short
|
||||
yield return working_short_pos;
|
||||
yield return working_short_avg_price;
|
||||
/// market
|
||||
yield return bid_price;
|
||||
yield return ask_price;
|
||||
yield return mid_price;
|
||||
yield return last_price;
|
||||
yield return settlement_price;
|
||||
yield return pnl_price;
|
||||
yield return cpp;
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<GridColumnSettings> group_columns {
|
||||
get {
|
||||
var rv = all.Select(cs => cs.with(font_weight_override: new NullableFontWeightProto(FontWeights.Bold))).ToList();
|
||||
GridSettingsHelper.try_replace_column(rv, Columns.SODPNL, _override_color);
|
||||
GridSettingsHelper.try_replace_column(rv, Columns.DayPNL, _override_color);
|
||||
return rv;
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<GridColumnSettings> header_columns {
|
||||
get {
|
||||
var rv = all.Select(cs => cs.with(font_weight_override: new NullableFontWeightProto(FontWeights.Bold))).ToList();
|
||||
GridSettingsHelper.try_replace_column(rv, Columns.SODPNL, _override_color);
|
||||
GridSettingsHelper.try_replace_column(rv, Columns.DayPNL, _override_color);
|
||||
return rv;
|
||||
}
|
||||
}
|
||||
|
||||
static GridColumnSettings _override_color(GridColumnSettings col_settings) =>
|
||||
_override_color(col_settings, 0);
|
||||
|
||||
static GridColumnSettings _override_color(GridColumnSettings col_settings, uint color) =>
|
||||
col_settings.with(font_color_override: new NullableColorProto(color));
|
||||
}
|
||||
}
|
||||
|
||||
static class PNLSettingsExtensions {
|
||||
public static IReadOnlyList<IPNLAnimationTrigger<PNLRowData>> triggers(this PNLSettings settings) => settings.animations.Select(a => a.to_trigger<PNLRowData>()).ToList();
|
||||
public static IReadOnlyList<IPNLAnimationTrigger<PNLNormalizedData>> group_triggers(this PNLSettings settings) => settings.group_header_animations.Select(a => a.to_trigger<PNLNormalizedData>()).ToList();
|
||||
public static IReadOnlyList<IPNLAnimationTrigger<PNLNormalizedData>> header_triggers(this PNLSettings settings) => settings.header_row_animations.Select(a => a.to_trigger<PNLNormalizedData>()).ToList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
using bts.proto;
|
||||
using mbts.ui;
|
||||
using mbts.ui.controls;
|
||||
using mbts.ui.settings;
|
||||
using mbts.ui.shell;
|
||||
using mbts.ui.utility.text;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using FontFamily = System.Windows.Media.FontFamily;
|
||||
|
||||
namespace bts.shell.pnl {
|
||||
class PNLCellAppearanceOverrideVM : ViewModelBase {
|
||||
bool
|
||||
_is_background_overridden,
|
||||
_is_font_color_overridden,
|
||||
_is_font_weight_overridden,
|
||||
_is_font_family_overridden,
|
||||
_is_font_size_overridden;
|
||||
|
||||
FontWeight _font_weight_override;
|
||||
FontFamily _font_family_override;
|
||||
double _font_size_override = double.NaN;
|
||||
|
||||
// background
|
||||
public bool is_background_overridden { get => _is_background_overridden; set => Set(ref _is_background_overridden, value); }
|
||||
public ColorPickerPopupVM background_override { get; } = new ColorPickerPopupVM {
|
||||
bind_opacity_to_alpha = true,
|
||||
opacity = 1
|
||||
};
|
||||
// font color
|
||||
public bool is_font_color_overridden { get => _is_font_color_overridden; set => Set(ref _is_font_color_overridden, value); }
|
||||
public ColorPickerPopupVM font_color_override { get; } = new ColorPickerPopupVM();
|
||||
// font weight
|
||||
public bool is_font_weight_overridden { get => _is_font_weight_overridden; set => Set(ref _is_font_weight_overridden, value); }
|
||||
public FontWeight font_weight_override { get => _font_weight_override; set => Set(ref _font_weight_override, value); }
|
||||
// font family
|
||||
public bool is_font_family_overriden { get => _is_font_family_overridden; set => Set(ref _is_font_family_overridden, value); }
|
||||
public FontFamily font_family_override { get => _font_family_override; set => Set(ref _font_family_override, value); }
|
||||
// font size
|
||||
public bool is_font_size_overriden { get => _is_font_size_overridden; set => Set(ref _is_font_size_overridden, value); }
|
||||
public double font_size_override { get => _font_size_override; set => Set(ref _font_size_override, value); }
|
||||
|
||||
public GridColumnSettings apply_to(GridColumnSettings target) =>
|
||||
target.with(
|
||||
font_weight_override: new NullableFontWeightProto(is_font_weight_overridden ? font_weight_override : (FontWeight?)null),
|
||||
font_color_override: new NullableColorProto(is_font_color_overridden ? font_color_override.color : (Color?)null),
|
||||
background_color_override: new NullableColorProto(is_background_overridden ? background_override.color : (Color?)null));
|
||||
|
||||
public void load(GridColumnSettings settings) {
|
||||
if (settings != null)
|
||||
load(settings.font_weight_override, settings.font_color_override, settings.background_color_override);
|
||||
}
|
||||
|
||||
public void load(FontParamsProto font_params) {
|
||||
if (font_params == null)
|
||||
return;
|
||||
// font weight
|
||||
is_font_weight_overridden = font_params.font_weight > 0;
|
||||
if (is_font_weight_overridden)
|
||||
font_weight_override = FontWeight.FromOpenTypeWeight(font_params.font_weight);
|
||||
// font color
|
||||
is_font_color_overridden = font_params.font_color > 0;
|
||||
if (is_font_color_overridden)
|
||||
font_color_override.color = font_params.font_color.rgb_color();
|
||||
// font family
|
||||
if (!string.IsNullOrEmpty(font_params.font_family)) {
|
||||
font_family_override = new FontFamily(font_params.font_family);
|
||||
}
|
||||
if (!double.IsNaN(font_params.font_size) && font_params.font_size > 0) {
|
||||
font_size_override = font_params.font_size;
|
||||
} else {
|
||||
font_size_override = double.NaN;
|
||||
}
|
||||
}
|
||||
|
||||
public void load(PNLAnimation animation) {
|
||||
if (animation != null)
|
||||
load(
|
||||
animation.font_weight.HasValue ? FontWeight.FromOpenTypeWeight(animation.font_weight.Value) : (FontWeight?)null,
|
||||
animation.foreground?.argb_color(),
|
||||
animation.background?.argb_color());
|
||||
}
|
||||
|
||||
public void load(FontWeight? font_weight, Color? font_color, Color? background) {
|
||||
// font weight
|
||||
is_font_weight_overridden = font_weight.HasValue;
|
||||
if (is_font_weight_overridden)
|
||||
font_weight_override = font_weight.Value;
|
||||
// font color
|
||||
is_font_color_overridden = font_color.HasValue;
|
||||
if (is_font_color_overridden)
|
||||
font_color_override.color = font_color.Value;
|
||||
// background
|
||||
is_background_overridden = background.HasValue;
|
||||
if (background.HasValue)
|
||||
background_override.color = background.Value;
|
||||
}
|
||||
|
||||
public PNLAnimation to_animation(PNLView.Columns key) =>
|
||||
new PNLAnimation {
|
||||
column = key,
|
||||
foreground = is_font_color_overridden ? font_color_override.color.rgb_color() : (uint?)null,
|
||||
background = is_background_overridden ? background_override.color.rgb_color() : (uint?)null,
|
||||
font_weight = is_font_weight_overridden ? font_weight_override.ToOpenTypeWeight() : (int?)null
|
||||
};
|
||||
|
||||
public PNLCellAppearanceProto to_appearance_proto() =>
|
||||
new PNLCellAppearanceProto {
|
||||
background = is_background_overridden ? background_override.color.rgb_color() : (uint?)null,
|
||||
font_params = to_font_params()
|
||||
};
|
||||
|
||||
public FontParamsProto to_font_params() =>
|
||||
new FontParamsProto {
|
||||
typeface = new TypefaceProto(
|
||||
font_family_override?.name(),
|
||||
is_font_weight_overridden ? font_weight_override.ToOpenTypeWeight() : 0),
|
||||
font_color = is_font_color_overridden ? font_color_override.color.rgb_color() : 0,
|
||||
font_size = font_size_override
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using mbts.ui.controls;
|
||||
using mbts.ui.shell;
|
||||
using System.Windows;
|
||||
using FontFamily = System.Windows.Media.FontFamily;
|
||||
|
||||
namespace bts.shell.pnl {
|
||||
class PNLCellAppearanceVM : ViewModelBase {
|
||||
FontWeight _font_weight;
|
||||
FontFamily _font_family;
|
||||
double _font_size = double.NaN;
|
||||
|
||||
public ColorPickerPopupVM background { get; } = new ColorPickerPopupVM { bind_opacity_to_alpha = true, opacity = 1 };
|
||||
public ColorPickerPopupVM font_color { get; } = new ColorPickerPopupVM();
|
||||
public FontWeight font_weight { get => _font_weight; set => Set(ref _font_weight, value); }
|
||||
public FontFamily font_family { get => _font_family; set => Set(ref _font_family, value); }
|
||||
public double font_size { get => _font_size; set => Set(ref _font_size, value); }
|
||||
|
||||
public PNLCellAppearance to_appearance() =>
|
||||
new PNLCellAppearance(font_color.color, background.color, font_weight, font_family, font_size);
|
||||
|
||||
public void load(PNLCellAppearance appearance) {
|
||||
if (appearance.background.HasValue)
|
||||
background.color = appearance.background.Value;
|
||||
if (appearance.foreground.HasValue)
|
||||
font_color.color = appearance.foreground.Value;
|
||||
if (appearance.font_family != null)
|
||||
font_family = appearance.font_family;
|
||||
if (appearance.font_weight.HasValue)
|
||||
font_weight = appearance.font_weight.Value;
|
||||
if (appearance.font_size > 0)
|
||||
font_size = appearance.font_size;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
using bts.collections;
|
||||
using mbts.spark;
|
||||
using mbts.ui.controls;
|
||||
using mbts.ui.settings;
|
||||
using mbts.ui.shell;
|
||||
using mbts.utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Windows;
|
||||
|
||||
namespace bts.shell.pnl {
|
||||
class PNLColumnVM : ViewModelBase, ISimpleColumnVM {
|
||||
readonly SimpleGridSettingsVM _owner;
|
||||
readonly PNLView.Columns _pnl_id;
|
||||
object _selected_item;
|
||||
string _display_name;
|
||||
bool _is_visible;
|
||||
double _width;
|
||||
HorizontalAlignment _horizontal_alignment;
|
||||
|
||||
public PNLColumnVM(SimpleGridSettingsVM owner, PNLView.Columns id) {
|
||||
_owner = owner;
|
||||
_pnl_id = id;
|
||||
default_column_vm.PropertyChanged += echo;
|
||||
void echo(object sender, PropertyChangedEventArgs e) {
|
||||
switch (e.PropertyName) {
|
||||
case nameof(is_font_weight_overridden):
|
||||
case nameof(is_font_color_overridden):
|
||||
RaisePropertyChanged(e.PropertyName);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public PNLCellAppearanceOverrideVM default_column_vm { get; } = new PNLCellAppearanceOverrideVM();
|
||||
public PNLCellAppearanceOverrideVM group_column_vm { get; } = new PNLCellAppearanceOverrideVM();
|
||||
public PNLCellAppearanceOverrideVM header_column_vm { get; } = new PNLCellAppearanceOverrideVM();
|
||||
//
|
||||
public ObservableCollection<PNLRuleColumnVM> rule_columns { get; } = new ObservableCollection<PNLRuleColumnVM>();
|
||||
|
||||
public object selected_item {
|
||||
get => _selected_item;
|
||||
set {
|
||||
if (value != null)
|
||||
Set(ref _selected_item, value);
|
||||
}
|
||||
}
|
||||
|
||||
public string id { get => _pnl_id.ToString(); set => Log.assert(value == _pnl_id.ToString()); }
|
||||
public string display_name { get => _display_name; set => Set(ref _display_name, value); }
|
||||
public double width { get => _width; set => Set(ref _width, value); }
|
||||
public bool is_visible { get => _is_visible; set => Set(ref _is_visible, value); }
|
||||
public bool is_font_color_overridden {
|
||||
get => default_column_vm.is_font_color_overridden;
|
||||
set => default_column_vm.is_font_color_overridden = value;
|
||||
}
|
||||
public HorizontalAlignment horizontal_alignment { get => _horizontal_alignment; set => Set(ref _horizontal_alignment, value); }
|
||||
|
||||
public ColorPickerPopupVM font_color_override => default_column_vm.font_color_override;
|
||||
|
||||
public bool is_font_weight_overridden { get => default_column_vm.is_font_weight_overridden; set => default_column_vm.is_font_weight_overridden = value; }
|
||||
public FontWeight font_weight_override { get => default_column_vm.font_weight_override; set => default_column_vm.font_weight_override = value; }
|
||||
|
||||
public void apply_flags(GridColumnSettingsFeatureFlags flags) { }
|
||||
|
||||
public GridColumnSettings create() {
|
||||
var result = new GridColumnSettings(id: id, display_name: display_name, is_visible: is_visible, horizontal_alignment: horizontal_alignment, width: width);
|
||||
result = default_column_vm.apply_to(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
public void load_from(GridColumnSettings value) {
|
||||
_assert_id(value.id);
|
||||
default_column_vm.load(value);
|
||||
display_name = value.display_name;
|
||||
width = value.width;
|
||||
is_visible = value.is_visible;
|
||||
horizontal_alignment = value.horizontal_alignment;
|
||||
}
|
||||
|
||||
public void load_from(GridColumnSettings value, IReadOnlyDictionary<string, PNLAnimation> triggers, GridColumnSettings group, IReadOnlyDictionary<string, PNLAnimation> group_triggers, IReadOnlyDictionary<string, PNLAnimation> header_triggers, GridColumnSettings header) {
|
||||
_assert_id(value.id);
|
||||
load_from(value);
|
||||
if (group != null)
|
||||
group_column_vm.load(group);
|
||||
if (header != null)
|
||||
header_column_vm.load(header);
|
||||
rule_columns.SetItems(triggers.Select(kv => {
|
||||
var rule_col = new PNLRuleColumnVM(kv.Key);
|
||||
rule_col.load_from(kv.Value, group_triggers.GetValueOrDefault(kv.Key), header_triggers.GetValueOrDefault(kv.Key));
|
||||
return rule_col;
|
||||
}));
|
||||
}
|
||||
|
||||
public IEnumerable<(string expression, PNLAnimation row, PNLAnimation group, PNLAnimation header)> get_triggers() {
|
||||
foreach (var col in rule_columns)
|
||||
yield return (col.rule_text, col.row_params.to_animation(_pnl_id), col.group_params.to_animation(_pnl_id), col.header_params.to_animation(_pnl_id));
|
||||
}
|
||||
|
||||
void _assert_id(string id) =>
|
||||
Log.assert(id == _pnl_id.ToString(), $"PNLColumn id string '{id}' does not match the expected {typeof(PNLView.Columns)} key '{_pnl_id}.'");
|
||||
|
||||
public override string ToString() => "Default";
|
||||
}
|
||||
|
||||
class PNLRuleColumnVM : ViewModelBase, IEquatable<PNLRuleColumnVM> {
|
||||
public PNLRuleColumnVM(string rule_text) =>
|
||||
this.rule_text = rule_text;
|
||||
|
||||
public string rule_text { get; }
|
||||
|
||||
public PNLCellAppearanceOverrideVM row_params { get; } = new PNLCellAppearanceOverrideVM();
|
||||
public PNLCellAppearanceOverrideVM group_params { get; } = new PNLCellAppearanceOverrideVM();
|
||||
public PNLCellAppearanceOverrideVM header_params { get; } = new PNLCellAppearanceOverrideVM();
|
||||
|
||||
public void load_from(PNLAnimation row, PNLAnimation group, PNLAnimation header) {
|
||||
row_params.load(row);
|
||||
group_params.load(group);
|
||||
header_params.load(header);
|
||||
}
|
||||
|
||||
public bool Equals(PNLRuleColumnVM other) =>
|
||||
other != null && rule_text == other.rule_text;
|
||||
|
||||
public override bool Equals(object obj) => Equals(obj as PNLRuleColumnVM);
|
||||
public override int GetHashCode() => rule_text.GetHashCode();
|
||||
public override string ToString() => rule_text;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using bts.collections;
|
||||
using mbts.spark;
|
||||
using mbts.ui;
|
||||
using mbts.ui.controls;
|
||||
using mbts.ui.shell;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using FontFamily = System.Windows.Media.FontFamily;
|
||||
|
||||
namespace bts.shell.pnl {
|
||||
class PNLGroupRowAppearanceVM : ViewModelBase, IValueFactory<(PNLCellAppearance appearance, IReadOnlyList<Color> background_colors)> {
|
||||
FontWeight _font_weight;
|
||||
FontFamily _font_family;
|
||||
double _font_size = double.NaN;
|
||||
|
||||
public PNLGroupRowAppearanceVM() {
|
||||
add_background_cmd = new DelegateCommand(_add_background);
|
||||
// 4 elements is enough to alternate through any reasonable number of groups,
|
||||
// and clicking to add new backgrounds seems awkward
|
||||
for (int i = 0; i < 4; i++)
|
||||
_add_background();
|
||||
}
|
||||
|
||||
public ObservableCollection<NamedColorPickerPopupVM> background_colors { get; } = new ObservableCollection<NamedColorPickerPopupVM>();
|
||||
public ColorPickerPopupVM font_color { get; } = new ColorPickerPopupVM();
|
||||
public FontWeight font_weight { get => _font_weight; set => Set(ref _font_weight, value); }
|
||||
public FontFamily font_family { get => _font_family; set => Set(ref _font_family, value); }
|
||||
public double font_size { get => _font_size; set => Set(ref _font_size, value); }
|
||||
public ICommand add_background_cmd { get; }
|
||||
|
||||
public (PNLCellAppearance appearance, IReadOnlyList<Color> background_colors) create() =>
|
||||
(new PNLCellAppearance(font_color.color, Colors.Transparent, font_weight, font_family, font_size),
|
||||
background_colors.WrapSelectorRO(cp => cp.color));
|
||||
|
||||
public void load_from((PNLCellAppearance appearance, IReadOnlyList<Color> background_colors) value) {
|
||||
var (app, bg_colors) = value;
|
||||
// background colors
|
||||
while (background_colors.Count < bg_colors.Count)
|
||||
_add_background();
|
||||
for (int i = 0; i < bg_colors.Count; i++)
|
||||
background_colors[i].color = bg_colors[i];
|
||||
//
|
||||
if (app.foreground.HasValue) font_color.color = app.foreground.Value;
|
||||
if (app.font_family != null) font_family = app.font_family;
|
||||
if (app.font_weight.HasValue) font_weight = app.font_weight.Value;
|
||||
if (app.font_size > 0) font_size = app.font_size;
|
||||
}
|
||||
|
||||
void _add_background() =>
|
||||
background_colors.Add(new NamedColorPickerPopupVM {
|
||||
bind_opacity_to_alpha = true,
|
||||
name = $"Background {background_colors.Count + 1}"
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
using bts.collections;
|
||||
using bts.utils;
|
||||
using mbts;
|
||||
using mbts.collections;
|
||||
using mbts.expression;
|
||||
using mbts.spark;
|
||||
using mbts.ui;
|
||||
using mbts.ui.settings;
|
||||
using mbts.ui.shell;
|
||||
using mbts.ui.spark;
|
||||
using mbts.ui.utility;
|
||||
using mbts.utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Specialized;
|
||||
using System.Dynamic;
|
||||
using System.Linq;
|
||||
using ColumnKeys = bts.shell.pnl.PNLView.Columns;
|
||||
|
||||
namespace bts.shell.pnl {
|
||||
class PNLSettingsVM : DisposableViewModelBase, IWindowSettingsVM<PNLSettings>, IValueFactory<PNLSettings>, IObservable<PNLSettings>, ITemplateSelectorKey {
|
||||
readonly IReadOnlyDictionary<ColumnKeys, AnchorItemVM> _anchor_cache;
|
||||
readonly DeferrableActionWrapper _sync_anchors;
|
||||
|
||||
PNLCalculationType _calculation_type;
|
||||
PNLHeaderRowDisplay _header_row_display;
|
||||
PNLSettings _last_loaded;
|
||||
|
||||
public PNLSettingsVM(PNLSettings settings) {
|
||||
// init anchor cache, deferrable resync
|
||||
_anchor_cache = PNLSettings.anchorable_columns.ToDictionary(k => k, k => new AnchorItemVM(this, k));
|
||||
_sync_anchors = new DeferrableActionWrapper(() => {
|
||||
for (int i = 0; i < anchor_items.Count; i++) {
|
||||
anchor_items[i].anchor_index = i + 1;
|
||||
anchor_items[i].refresh();
|
||||
}
|
||||
});
|
||||
core_settings_vm = new SimpleGridSettingsVM(column_factory: col_settings => {
|
||||
var col_vm = new PNLColumnVM(core_settings_vm, EnumHelper.parse<ColumnKeys>(col_settings.id));
|
||||
col_vm.load_from(col_settings);
|
||||
return col_vm;
|
||||
});
|
||||
core_settings_vm.selection_highlight.bind_opacity_to_alpha = true;
|
||||
_dispose = Disposable.Create(
|
||||
anchor_items.Subscribe((s, e) => _sync_anchors.execute()));
|
||||
load(settings);
|
||||
}
|
||||
|
||||
public IEnumerable<AnchorItemVM> anchor_item_headers => _anchor_cache.Values;
|
||||
public ObservableCollection<AnchorItemVM> anchor_items { get; } = new ObservableCollection<AnchorItemVM>();
|
||||
public SimpleGridSettingsVM core_settings_vm { get; }
|
||||
public PNLGroupRowAppearanceVM group_row_defaults { get; } = new PNLGroupRowAppearanceVM();
|
||||
public PNLCellAppearanceVM header_row_defaults { get; } = new PNLCellAppearanceVM();
|
||||
public PNLCalculationType calculation_type { get => _calculation_type; set => Set(ref _calculation_type, value); }
|
||||
public IEnumerable<PNLCalculationType> calculation_types => EnumHelper.get_values_except(PNLCalculationType.None, PNLCalculationType.Same, PNLCalculationType.Opposite);
|
||||
public PNLHeaderRowDisplay header_row_display { get => _header_row_display; set => Set(ref _header_row_display, value); }
|
||||
public IEnumerable<PNLHeaderRowDisplay> header_row_display_options => PNLSettings.header_row_display_options;
|
||||
|
||||
string ITemplateSelectorKey.key => GridSettingsTemplateKeys.PNL;
|
||||
|
||||
public PNLSettings get_result() {
|
||||
var s = _last_loaded == null ? new PNLSettings() : SparkUtils.clone_proto(_last_loaded);
|
||||
s.calculation_type = _calculation_type;
|
||||
s.grid = core_settings_vm.create();
|
||||
// move anchor cols to the left, order wrt grouping hierarchy
|
||||
var new_cols = new Lazy<List<GridColumnSettings>>(() => s.grid.columns.ToList());
|
||||
for (int i = 0; i < s.grid.columns.Count; i++) {
|
||||
var key = s.grid.columns[i].strong_id<ColumnKeys>();
|
||||
if (_anchor_cache.TryGetValue(key, out var a) && a.anchor_index.HasValue) {
|
||||
// anchor index is 1 based, subtract 1 to get 0 based
|
||||
int anchor_index = a.anchor_index.Value - 1;
|
||||
if (anchor_index != i && anchor_index >= 0 && anchor_index < s.grid.columns.Count) {
|
||||
new_cols.Value.RemoveAt(i);
|
||||
new_cols.Value.Insert(anchor_index, s.grid.columns[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
// rule-based animations
|
||||
var trigger_anims = core_settings_vm.columns
|
||||
.OfType<PNLColumnVM>()
|
||||
.SelectMany(c => c.get_triggers())
|
||||
.GroupBy(tup => tup.expression)
|
||||
.Where(g => CoreExpressionEvaluator<PNLRowData>.validate(g.Key))
|
||||
.Select(g => (
|
||||
expression: g.Key,
|
||||
animations: g.ToArray()))
|
||||
.ToList();
|
||||
|
||||
var normalized_anims = trigger_anims
|
||||
.Where(tup => CoreExpressionEvaluator<PNLNormalizedData>.validate(tup.expression))
|
||||
.ToList();
|
||||
|
||||
s = s.with(anchors: anchor_items.Select(vm => vm.key),
|
||||
grid: new_cols.IsValueCreated ? s.grid.with(columns: new_cols.Value) : null,
|
||||
group_columns: core_settings_vm.columns.OfType<PNLColumnVM>().Select(c => {
|
||||
var col_settings = c.create();
|
||||
col_settings = c.group_column_vm.apply_to(col_settings);
|
||||
return col_settings;
|
||||
}),
|
||||
header_columns: core_settings_vm.columns.OfType<PNLColumnVM>().Select(c => {
|
||||
var col_settings = c.create();
|
||||
col_settings = c.header_column_vm.apply_to(col_settings);
|
||||
return col_settings;
|
||||
}),
|
||||
animations: trigger_anims.Select(
|
||||
ta => new PNLAnimationTriggerData {
|
||||
expression = ta.expression,
|
||||
animations = Array.ConvertAll(ta.animations, t => t.row)
|
||||
}),
|
||||
group_header_animations: normalized_anims
|
||||
.Select(
|
||||
ta => new PNLAnimationTriggerData {
|
||||
expression = ta.expression,
|
||||
animations = Array.ConvertAll(ta.animations, t => t.group)
|
||||
}),
|
||||
header_row_animations: normalized_anims.Select(
|
||||
ta => new PNLAnimationTriggerData {
|
||||
expression = ta.expression,
|
||||
animations = Array.ConvertAll(ta.animations, t => t.header)
|
||||
}));
|
||||
s.header_row_display = header_row_display;
|
||||
s.header_cell_appearance = PNLCellAppearanceProto.from_appearance(header_row_defaults.to_appearance());
|
||||
// group
|
||||
var (group_cell_appearance, group_row_background_colors) = group_row_defaults.create();
|
||||
s.group_cell_appearance = PNLCellAppearanceProto.from_appearance(group_cell_appearance);
|
||||
s.group_background_colors.SetItems(group_row_background_colors.Select(c => c.rgb_color()));
|
||||
//
|
||||
return s;
|
||||
}
|
||||
|
||||
public void load(PNLSettings value) {
|
||||
_last_loaded = value;
|
||||
header_row_defaults.load(value.header_cell_appearance.to_appearance());
|
||||
group_row_defaults.load_from((value.group_cell_appearance.to_appearance(), value.group_background_colors.WrapSelectorRO(c => c.argb_color())));
|
||||
var column_map = _map_column_data(value.grid.columns, value.animations);
|
||||
var group_column_map = _map_column_data(value.group_columns, value.group_header_animations);
|
||||
var header_column_map = _map_column_data(value.header_columns, value.header_row_animations);
|
||||
|
||||
core_settings_vm.load_from(value.grid);
|
||||
|
||||
for (int i = 0; i < core_settings_vm.columns.Count; i++) {
|
||||
var col_vm = core_settings_vm.columns[i] as PNLColumnVM ?? throw new InvalidOperationException($"In {nameof(PNLSettingsVM)}, all columns should be of type {typeof(PNLColumnVM)}.");
|
||||
var key = EnumHelper.parse<ColumnKeys>(col_vm.id);
|
||||
var (col_settings, trigger_animations) = column_map[key];
|
||||
var group_data = group_column_map.GetNullable(key);
|
||||
var header_data = header_column_map.GetNullable(key);
|
||||
col_vm.load_from(col_settings, trigger_animations, group_data?.settings, group_data?.triggers, header_data?.triggers, header_data?.settings);
|
||||
}
|
||||
using (_sync_anchors.defer_execution())
|
||||
anchor_items.WrapSelector(a => a.key, k => _anchor_cache[k]).sync_with(value.anchors.Intersect(PNLSettings.anchorable_columns));
|
||||
header_row_display = value.header_row_display;
|
||||
calculation_type = value.calculation_type;
|
||||
}
|
||||
|
||||
PNLSettings IValueFactory<PNLSettings>.create() => get_result();
|
||||
void IValueFactory<PNLSettings>.load_from(PNLSettings value) => load(value);
|
||||
IDisposable IObservable<PNLSettings>.Subscribe(IObserver<PNLSettings> observer) => throw new NotImplementedException();
|
||||
|
||||
static Dictionary<ColumnKeys, (GridColumnSettings settings, Dictionary<string, PNLAnimation> triggers)> _map_column_data(IEnumerable<GridColumnSettings> columns, IEnumerable<PNLAnimationTriggerData> triggers) {
|
||||
var map = columns.ToDictionary(
|
||||
c => c.strong_id<ColumnKeys>(),
|
||||
c => (settings: c, triggers: new Dictionary<string, PNLAnimation>()));
|
||||
foreach (var trig in triggers) {
|
||||
foreach (var anim in trig.animations) {
|
||||
if (map.TryGetValue(anim.column, out var col_data))
|
||||
col_data.triggers[trig.expression] = anim;
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
void _append_or_remove_anchor(ColumnKeys key, bool include) {
|
||||
if (!_anchor_cache.TryGetValue(key, out var vm))
|
||||
throw new KeyNotFoundException($"Column key '{key}' is not anchorable (expected one of: {string.Join(", ", PNLSettings.anchorable_columns)}).");
|
||||
if (vm.include == include)
|
||||
return;
|
||||
if (include) {
|
||||
if (anchor_items.Any(a => a.key == key))
|
||||
throw new InvalidOperationException($"{nameof(anchor_items)} already contains an element with key '{key}.'");
|
||||
anchor_items.Add(vm);
|
||||
vm.anchor_index = anchor_items.Count;
|
||||
} else {
|
||||
if (!anchor_items.Remove(vm))
|
||||
throw new InvalidOperationException($"{nameof(anchor_items)} does not contain an element with key '{key}.'");
|
||||
vm.anchor_index = null;
|
||||
}
|
||||
_sync_anchors.execute();
|
||||
}
|
||||
|
||||
public class AnchorItemVM : ViewModelBase, IEquatable<AnchorItemVM> {
|
||||
readonly PNLSettingsVM _owner;
|
||||
int? _anchor_index;
|
||||
|
||||
public AnchorItemVM(PNLSettingsVM owner, ColumnKeys key) {
|
||||
_owner = owner;
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
public ColumnKeys key { get; }
|
||||
public bool include { get => anchor_index.HasValue; set => _owner._append_or_remove_anchor(key, value); }
|
||||
public int? anchor_index { get => _anchor_index; set => Set(ref _anchor_index, value); }
|
||||
|
||||
public void refresh() =>
|
||||
RaisePropertyChanged(nameof(include), nameof(anchor_index));
|
||||
|
||||
public bool Equals(AnchorItemVM other) =>
|
||||
other != null && key == other.key;
|
||||
|
||||
public override bool Equals(object obj) =>
|
||||
Equals(obj as AnchorItemVM);
|
||||
|
||||
public override int GetHashCode() =>
|
||||
key.GetHashCode();
|
||||
|
||||
public override string ToString() =>
|
||||
EnumHelper.get_display_name(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using ColumnKeys = bts.shell.pnl.PNLView.Columns;
|
||||
|
||||
namespace bts.shell.pnl {
|
||||
class PNLCellEmbeddedAnimationModel<TRow> {
|
||||
IReadOnlyList<IPNLAnimationTrigger<TRow>> _triggers;
|
||||
ImmutableDictionary<ColumnKeys, PNLCellAppearance> _defaults;
|
||||
TRow _row;
|
||||
|
||||
public PNLCellEmbeddedAnimationModel(ImmutableDictionary<ColumnKeys, PNLCellAppearance> defaults, IReadOnlyList<IPNLAnimationTrigger<TRow>> triggers) {
|
||||
_defaults = defaults;
|
||||
_triggers = triggers;
|
||||
}
|
||||
|
||||
public IReadOnlyDictionary<ColumnKeys, PNLCellAppearance> cell_appearances { get; private set; }
|
||||
|
||||
public void on_settings(ImmutableDictionary<ColumnKeys, PNLCellAppearance> defaults, IReadOnlyList<IPNLAnimationTrigger<TRow>> triggers) {
|
||||
_defaults = defaults;
|
||||
_triggers = triggers;
|
||||
on_row(_row);
|
||||
}
|
||||
|
||||
public void on_row(TRow data) {
|
||||
_row = data;
|
||||
var bd = _defaults.ToBuilder();
|
||||
foreach (var t in _triggers) {
|
||||
foreach (var kv in t.trigger(data)) {
|
||||
if (!bd.TryGetValue(kv.Key, out var app)) {
|
||||
app = _defaults[kv.Key];
|
||||
}
|
||||
app = kv.Value.merge(app);
|
||||
bd[kv.Key] = app;
|
||||
}
|
||||
}
|
||||
cell_appearances = bd.ToImmutable();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
using bts.utils;
|
||||
using mbts.spark;
|
||||
using mbts.spark.utilities;
|
||||
using mbts.ui;
|
||||
using mbts.ui.spark;
|
||||
using mbts.utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace bts.shell.pnl {
|
||||
class PNLCellIndexBuilder<TRow> {
|
||||
public readonly DIYGridAnimationModel<PNLView.Columns, TRow> animation_model = new DIYGridAnimationModel<PNLView.Columns, TRow>(TimeSpan.FromSeconds(0.3));
|
||||
|
||||
static readonly Func<WorkingOrderSideData, string> _fmt_working_order_data =
|
||||
d => OrderUtils.order_collection_summary(d.top_price, d.top_volume, d.working_total);
|
||||
|
||||
static readonly IComparer<WorkingOrderSideData> _working_total_comparer =
|
||||
Comparer<int>.Default.wrap<int, WorkingOrderSideData>(d => d.working_total);
|
||||
|
||||
readonly Dictionary<PNLView.Columns, IPNLCellVM<TRow>> _cell_index = new Dictionary<PNLView.Columns, IPNLCellVM<TRow>>();
|
||||
|
||||
public PNLCellVM<TRow, T> add<T>(PNLView.Columns key, Func<TRow, T> get, Func<T, string> fmt = null) =>
|
||||
add(key, get, fmt, EqualityComparer<T>.Default, Comparer<T>.Default);
|
||||
|
||||
public PNLCellVM<TRow, T> add_pnl<T>(PNLView.Columns key, Func<TRow, T> get) where T : struct, IComparable<T>, IFormattable =>
|
||||
add_delta(key, BTSColors.PNL.profit_color, BTSColors.PNL.loss_color, get, FormattingDelegates.Price<T>.Value);
|
||||
|
||||
public PNLCellVM<TRow, decimal> add_pnl_decimal(PNLView.Columns key, Func<TRow, decimal> get) =>
|
||||
add_delta<decimal>(key, BTSColors.PNL.profit_color, BTSColors.PNL.loss_color, OrderDataHelper.PriceComparerDecimal, get, FormattingDelegates.Price<decimal>.Value);
|
||||
|
||||
public PNLCellVM<TRow, double> add_pnl_double(PNLView.Columns key, Func<TRow, double> get) =>
|
||||
add_delta(key, BTSColors.PNL.profit_color, BTSColors.PNL.loss_color, OrderDataHelper.PriceComparer, get, FormattingDelegates.Price<double>.Value);
|
||||
|
||||
public PNLCellVM<TRow, double> add_net_price(PNLView.Columns key, Func<TRow, double> get) =>
|
||||
add_delta(key, BTSColors.MarketData.bid, BTSColors.MarketData.ask, OrderDataHelper.PriceComparer, get, FormattingDelegates.Price<double>.Value);
|
||||
|
||||
public PNLCellVM<TRow, T> add_net_pos<T>(PNLView.Columns key, Func<TRow, T> get, Func<T, string> fmt = null) where T : struct, IComparable<T> =>
|
||||
add_delta(key, BTSColors.MarketData.bid, BTSColors.MarketData.ask, get, fmt);
|
||||
|
||||
public PNLCellVM<TRow, double> add_long_price(PNLView.Columns key, Func<TRow, double> get) =>
|
||||
add_delta(key, BTSColors.MarketData.bid, BTSColors.MarketData.bid, OrderDataHelper.PriceComparer, get, FormattingDelegates.Price<double>.Value);
|
||||
|
||||
public PNLCellVM<TRow, T> add_long_pos<T>(PNLView.Columns key, Func<TRow, T> get, Func<T, string> fmt = null) where T : struct, IComparable<T> =>
|
||||
add_delta(key, BTSColors.MarketData.bid, BTSColors.MarketData.ask, get, fmt);
|
||||
|
||||
public PNLCellVM<TRow, double> add_short_price(PNLView.Columns key, Func<TRow, double> get) =>
|
||||
add_delta(key, BTSColors.MarketData.ask, BTSColors.MarketData.ask, OrderDataHelper.PriceComparer, get, FormattingDelegates.Price<double>.Value);
|
||||
|
||||
public PNLCellVM<TRow, T> add_short_pos<T>(PNLView.Columns key, Func<TRow, T> get, Func<T, string> fmt = null) where T : struct, IComparable<T> =>
|
||||
add_delta(key, BTSColors.MarketData.ask, BTSColors.MarketData.bid, get, fmt);
|
||||
|
||||
public PNLCellVM<TRow, T> add_price<T>(PNLView.Columns key, Func<TRow, T> get) where T : struct, IComparable<T>, IFormattable =>
|
||||
add_delta(key, BTSColors.PNL.profit_color, BTSColors.PNL.loss_color, get, FormattingDelegates.Price<T>.Value);
|
||||
|
||||
public PNLCellVM<TRow, double> add_price_double(PNLView.Columns key, Func<TRow, double> get) =>
|
||||
add_delta(key, BTSColors.PNL.profit_color, BTSColors.PNL.loss_color, OrderDataHelper.PriceComparer, get, FormattingDelegates.Price<double>.Value);
|
||||
|
||||
public PNLCellVM<TRow, WorkingOrderSideData> add_working_long(PNLView.Columns key, Func<TRow, WorkingOrderSideData> get, Func<TRow, double> get_market) =>
|
||||
_add_working(key, BTSColors.MarketData.bid, BTSColors.MarketData.ask, get, get_market, OrderDataHelper.PriceComparer);
|
||||
|
||||
public PNLCellVM<TRow, WorkingOrderSideData> add_working_short(PNLView.Columns key, Func<TRow, WorkingOrderSideData> get, Func<TRow, double> get_market) =>
|
||||
_add_working(key, BTSColors.MarketData.ask, BTSColors.MarketData.bid, get, get_market, new ReverseComparer<double>(OrderDataHelper.PriceComparer));
|
||||
|
||||
PNLCellVM<TRow, WorkingOrderSideData> _add_working(PNLView.Columns key, Color increase, Color decrease, Func<TRow, WorkingOrderSideData> get, Func<TRow, double> get_market, IComparer<double> price_comparer) {
|
||||
var delta_trig = DIYGridTriggerAnimations.delta_trigger<int, PNLView.Columns>(key, increase, decrease)
|
||||
.wrap<int, TRow, PNLView.Columns>(r => get(r).working_total);
|
||||
var top_of_book_trig = new TopOfBookTrigger<int, double>(key, increase,
|
||||
r => get(r).working_total,
|
||||
r => get(r).top_price,
|
||||
get_market,
|
||||
price_comparer);
|
||||
var zipped_trig = delta_trig.zip_indefinite_frame(r => {
|
||||
var frames = top_of_book_trig.on_data(r);
|
||||
return frames.TryGetFirst(out var f) ? f.animation : null;
|
||||
});
|
||||
var cell = add(key, get, _fmt_working_order_data, EqualityComparer<WorkingOrderSideData>.Default, _working_total_comparer);
|
||||
animation_model.add_trigger(delta_trig);
|
||||
animation_model.add_trigger(top_of_book_trig);
|
||||
return cell;
|
||||
}
|
||||
|
||||
public PNLCellVM<TRow, T> add_delta<T>(PNLView.Columns key, Color increase, Color decrease, Func<TRow, T> get, Func<T, string> fmt = null) where T : struct, IComparable<T> =>
|
||||
add_delta(key, increase, decrease, Comparer<T>.Default, get, fmt);
|
||||
|
||||
public PNLCellVM<TRow, T> add_delta<T>(PNLView.Columns key, Color increase, Color decrease, IComparer<T> comparer, Func<TRow, T> get, Func<T, string> fmt = null) where T : struct {
|
||||
var cell = add(key, get, fmt, EqualityComparer<T>.Default, Comparer<T>.Default);
|
||||
animation_model.add_trigger(
|
||||
DIYGridTriggerAnimations.delta_trigger(key, increase, decrease, comparer)
|
||||
.wrap(get));
|
||||
return cell;
|
||||
}
|
||||
|
||||
public PNLCellVM<TRow, T> add<T>(PNLView.Columns key, Func<TRow, T> get, Func<T, string> fmt, IEqualityComparer<T> eq_comparer, IComparer<T> comparer) {
|
||||
var cell = new PNLCellVM<TRow, T>(key, get, fmt, eq_comparer, comparer);
|
||||
_cell_index[key] = cell;
|
||||
return cell;
|
||||
}
|
||||
|
||||
public void on_row(TRow row) {
|
||||
foreach (var cell in _cell_index.Values)
|
||||
cell.on_row(row);
|
||||
animation_model.on_row(row);
|
||||
}
|
||||
|
||||
public void set_appearances(IReadOnlyDictionary<PNLView.Columns, PNLCellAppearance> appearances) {
|
||||
foreach (var kv in appearances) {
|
||||
if (_cell_index.TryGetValue(kv.Key, out var c))
|
||||
c.apply_appearance(kv.Value);
|
||||
}
|
||||
}
|
||||
|
||||
class TopOfBookTrigger<TPosition, TPrice> : ITriggerAnimation<TRow, PNLView.Columns> where TPosition : struct, IComparable<TPosition> where TPrice : struct, IComparable<TPrice> {
|
||||
readonly PNLView.Columns _column;
|
||||
readonly Func<TRow, TPosition> _get_pos;
|
||||
readonly Func<TRow, TPrice> _get_price, _get_market;
|
||||
readonly IComparer<TPrice> _comparer;
|
||||
readonly Color _border_color;
|
||||
|
||||
public TopOfBookTrigger(
|
||||
PNLView.Columns column,
|
||||
Color border_color,
|
||||
Func<TRow, TPosition> get_pos,
|
||||
Func<TRow, TPrice> get_price,
|
||||
Func<TRow, TPrice> get_market,
|
||||
IComparer<TPrice> comparer) {
|
||||
_column = column;
|
||||
_border_color = border_color;
|
||||
_get_pos = get_pos;
|
||||
_get_price = get_price;
|
||||
_get_market = get_market;
|
||||
_comparer = comparer;
|
||||
}
|
||||
|
||||
public IEnumerable<AnimationFrame<PNLView.Columns>> on_data(TRow value) {
|
||||
var pos = _get_pos(value);
|
||||
if (pos.CompareTo(default) > 0) {
|
||||
var price = _get_price(value);
|
||||
var market = _get_market(value);
|
||||
if (_comparer.Compare(price, market) >= 0)
|
||||
yield return new AnimationFrame<PNLView.Columns>(
|
||||
TimeSpan.Zero,
|
||||
ImmutableDictionary<SymbolKey<PNLView.Columns>, Animation>.Empty.Add(
|
||||
new SymbolKey<PNLView.Columns>(_column, mbts.ui.spark.CellAnimation.Type.Border),
|
||||
new Animation(mbts.ui.spark.CellAnimation.Type.Border, _border_color)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
using bts.utils;
|
||||
using mbts.ui;
|
||||
using mbts.ui.shell;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace bts.shell.pnl {
|
||||
interface IPNLCellVM {
|
||||
object value { get; }
|
||||
IPNLCellData cell_data { get; }
|
||||
FontWeight font_weight { get; set; }
|
||||
Color foreground { get; set; }
|
||||
Color background { get; set; }
|
||||
Color border { get; set; }
|
||||
}
|
||||
|
||||
interface IPNLCellVM<TRow> : IPNLCellVM {
|
||||
void on_row(TRow row);
|
||||
}
|
||||
|
||||
/// <see cref="IPNLCellData"/> is needed for the non-generic rows/group IComparer impl.
|
||||
/// The groups are bound to IPNLCellData, so the column property can be used to retrieve the appropriate comparer for that cell.
|
||||
interface IPNLCellData {
|
||||
PNLView.Columns column { get; }
|
||||
object value { get; }
|
||||
}
|
||||
|
||||
readonly struct PNLCellValue<T> : IPNLCellData {
|
||||
public readonly T value;
|
||||
readonly string _text;
|
||||
|
||||
public PNLCellValue(PNLView.Columns column, T value, string text) {
|
||||
this.value = value;
|
||||
this.column = column;
|
||||
_text = text;
|
||||
}
|
||||
|
||||
public PNLView.Columns column { get; }
|
||||
object IPNLCellData.value => value;
|
||||
|
||||
public bool Equals(PNLCellValue<T> other) =>
|
||||
column == other.column &&
|
||||
Equals(value, other.value);
|
||||
|
||||
public override bool Equals(object obj) =>
|
||||
obj is PNLCellValue<T> other && Equals(other);
|
||||
|
||||
public override int GetHashCode() =>
|
||||
EqualityComparer.auto_phase((int)column, value?.GetHashCode() ?? 0);
|
||||
|
||||
public override string ToString() => _text;
|
||||
}
|
||||
|
||||
class PNLCellVM<TRow, TValue> : ViewModelBase, IObserver<PNLAnimation>, IPNLCellVM<TRow>, IComparable<PNLCellVM<TRow, TValue>>, IComparable {
|
||||
readonly Func<TValue, string> _fmt;
|
||||
readonly Func<TRow, TValue> _get;
|
||||
readonly IEqualityComparer<TValue> _eq_comparer;
|
||||
readonly IComparer<TValue> _comparer;
|
||||
readonly PNLView.Columns _column;
|
||||
IComparer _value_comparer;
|
||||
Color
|
||||
_foreground = Colors.Black,
|
||||
_background = Colors.Transparent,
|
||||
_border = Colors.Transparent;
|
||||
string _text;
|
||||
TValue _value;
|
||||
IPNLCellData _cell_value;
|
||||
|
||||
public PNLCellVM(PNLView.Columns column, Func<TRow, TValue> get)
|
||||
: this(column, get, null, EqualityComparer<TValue>.Default, Comparer<TValue>.Default) { }
|
||||
|
||||
public PNLCellVM(PNLView.Columns column, Func<TRow, TValue> get, Func<TValue, string> fmt)
|
||||
: this(column, get, fmt, EqualityComparer<TValue>.Default, Comparer<TValue>.Default) { }
|
||||
|
||||
public PNLCellVM(PNLView.Columns column, Func<TRow, TValue> get, Func<TValue, string> fmt, IEqualityComparer<TValue> eq_comparer, IComparer<TValue> comparer) {
|
||||
_column = column;
|
||||
_get = get;
|
||||
_fmt = fmt;
|
||||
_eq_comparer = eq_comparer;
|
||||
_comparer = comparer;
|
||||
}
|
||||
|
||||
public IComparer value_comparer => _value_comparer ?? (_value_comparer = _comparer.to_non_generic());
|
||||
|
||||
object IPNLCellVM.value => value;
|
||||
public IPNLCellData cell_data => _cell_value ?? (_cell_value = new PNLCellValue<TValue>(_column, _value, text));
|
||||
public TValue value {
|
||||
get => _value;
|
||||
set {
|
||||
if (Set(ref _value, value)) {
|
||||
_text = null;
|
||||
_cell_value = null;
|
||||
RaisePropertyChanged(nameof(cell_data), nameof(text));
|
||||
}
|
||||
}
|
||||
}
|
||||
public string text {
|
||||
get => _text ?? (_text = _fmt?.Invoke(value) ?? value?.ToString());
|
||||
set => Set(ref _text, value);
|
||||
}
|
||||
|
||||
public Color foreground {
|
||||
get => _foreground;
|
||||
set => Set(ref _foreground, value);
|
||||
}
|
||||
|
||||
public Color background {
|
||||
get => _background;
|
||||
set => Set(ref _background, value);
|
||||
}
|
||||
|
||||
public Color border {
|
||||
get => _border;
|
||||
set => Set(ref _border, value);
|
||||
}
|
||||
|
||||
FontWeight _font_weight;
|
||||
public FontWeight font_weight {
|
||||
get => _font_weight;
|
||||
set => Set(ref _font_weight, value);
|
||||
}
|
||||
|
||||
public void on_row(TRow row) =>
|
||||
value = _get(row);
|
||||
|
||||
public void OnNext(PNLAnimation animation) {
|
||||
if (animation.foreground.HasValue)
|
||||
foreground = animation.foreground.Value.argb_color();
|
||||
if (animation.background.HasValue)
|
||||
background = animation.background.Value.argb_color();
|
||||
if (animation.font_weight.HasValue)
|
||||
font_weight = FontWeight.FromOpenTypeWeight(animation.font_weight.Value);
|
||||
}
|
||||
public void OnError(Exception error) { }
|
||||
public void OnCompleted() { }
|
||||
|
||||
bool Equals(PNLCellVM<TRow, TValue> other) =>
|
||||
Equals(value, other.value);
|
||||
|
||||
public override string ToString() =>
|
||||
text;
|
||||
|
||||
int IComparable<PNLCellVM<TRow, TValue>>.CompareTo(PNLCellVM<TRow, TValue> other) =>
|
||||
other == null ? throw new ArgumentNullException(nameof(other)) : _comparer.Compare(value, other.value);
|
||||
|
||||
int IComparable.CompareTo(object obj) =>
|
||||
obj is PNLCellVM<TRow, TValue> other ?
|
||||
_comparer.Compare(value, other.value) :
|
||||
throw new ArgumentException(nameof(obj), $"The specified object is not of type {typeof(PNLCellVM<TRow, TValue>)} (actually {obj?.GetType()}).");
|
||||
}
|
||||
|
||||
static class PNLCellVMExtensions {
|
||||
public static void apply_appearance(this IPNLCellVM cell, PNLCellAppearance animation) {
|
||||
if (animation.foreground.HasValue)
|
||||
cell.foreground = animation.foreground.Value;
|
||||
if (animation.background.HasValue)
|
||||
cell.background = animation.background.Value;
|
||||
if (animation.font_weight.HasValue)
|
||||
cell.font_weight = animation.font_weight.Value;
|
||||
// TODO: font family, size
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
using bts.utils;
|
||||
using mbts.ui.shell;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.ComponentModel;
|
||||
using System.Windows.Media;
|
||||
using ColumnKeys = bts.shell.pnl.PNLView.Columns;
|
||||
|
||||
namespace bts.shell.pnl {
|
||||
class PNLHeaderRowVM : ViewModelBase, IRaiseNotifyPropertyChanged, INotifyPropertyChanged {
|
||||
static readonly IPropertyChangedDetector<PNLHeaderRowVM> _change_detector =
|
||||
ObjectSnapshot.build_detector_except<PNLHeaderRowVM>(nameof(data));
|
||||
|
||||
readonly PNLCellIndexBuilder<PNLNormalizedData> _cell_map = new PNLCellIndexBuilder<PNLNormalizedData>();
|
||||
readonly PNLCellEmbeddedAnimationModel<PNLNormalizedData> _animator;
|
||||
ImmutableDictionary<ColumnKeys, PNLCellAppearance> _default_appearances;
|
||||
IReadOnlyList<IPNLAnimationTrigger<PNLNormalizedData>> _triggers;
|
||||
PNLNormalizedData _data = PNLNormalizedData.zero;
|
||||
Color _background_color;
|
||||
|
||||
public PNLHeaderRowVM(
|
||||
Color background_color,
|
||||
ImmutableDictionary<ColumnKeys, PNLCellAppearance> default_appearances,
|
||||
IReadOnlyList<IPNLAnimationTrigger<PNLNormalizedData>> triggers) {
|
||||
//
|
||||
_background_color = background_color;
|
||||
_default_appearances = default_appearances;
|
||||
_triggers = triggers;
|
||||
_animator = new PNLCellEmbeddedAnimationModel<PNLNormalizedData>(default_appearances, triggers);
|
||||
_cell_map.set_appearances(default_appearances);
|
||||
// sod
|
||||
sod_pnl = _cell_map.add_pnl_decimal(ColumnKeys.SODPNL, d => d.sod_pnl);
|
||||
sod_net_pos = _cell_map.add_net_pos(ColumnKeys.SODNetPos, d => d.sod_net_pos);
|
||||
sod_long_pos = _cell_map.add_long_pos(ColumnKeys.SODLongPos, d => d.sod_long_pos);
|
||||
sod_short_pos = _cell_map.add_short_pos(ColumnKeys.SODShortPos, d => d.sod_short_pos);
|
||||
// day
|
||||
day_pnl = _cell_map.add_pnl(ColumnKeys.DayPNL, d => d.day_pnl);
|
||||
day_net_pos = _cell_map.add_net_pos(ColumnKeys.DayNetPos, d => d.day_net_pos);
|
||||
day_long_pos = _cell_map.add_long_pos(ColumnKeys.DayLongPos, d => d.day_long_pos);
|
||||
day_short_pos = _cell_map.add_short_pos(ColumnKeys.DayShortPos, d => d.day_short_pos);
|
||||
// raw day
|
||||
raw_day_net_pos = _cell_map.add_net_pos(ColumnKeys.RawDayNetPos, d => d.raw_day_net_pos);
|
||||
raw_day_long_pos = _cell_map.add_long_pos(ColumnKeys.RawDayLongPos, d => d.raw_day_long_pos);
|
||||
raw_day_short_pos = _cell_map.add_short_pos(ColumnKeys.RawDayShortPos, d => d.raw_day_short_pos);
|
||||
// working
|
||||
working_net_pos = _cell_map.add_net_pos(ColumnKeys.WorkingNetPos, d => d.working_net_pos);
|
||||
working_long_pos = _cell_map.add_long_pos(ColumnKeys.WorkingLongPos, d => d.working_long_pos);
|
||||
working_short_pos = _cell_map.add_short_pos(ColumnKeys.WorkingShortPos, r => r.working_short_pos);
|
||||
// total
|
||||
total_pnl = _cell_map.add_pnl_decimal(ColumnKeys.TotalPNL, d => d.total_pnl);
|
||||
total_net_pos = _cell_map.add_net_pos(ColumnKeys.TotalNetPos, d => d.total_net_pos);
|
||||
total_long_pos = _cell_map.add_long_pos(ColumnKeys.TotalLongPos, d => d.total_long_pos);
|
||||
total_short_pos = _cell_map.add_short_pos(ColumnKeys.TotalShortPos, d => d.total_short_pos);
|
||||
}
|
||||
|
||||
public event Action data_changed;
|
||||
|
||||
public PNLNormalizedData data {
|
||||
get => _data;
|
||||
set {
|
||||
if (value != _data) {
|
||||
_data = value;
|
||||
_cell_map.on_row(value);
|
||||
_animator.on_row(value);
|
||||
_cell_map.set_appearances(_animator.cell_appearances);
|
||||
data_changed?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
public Color background_color {
|
||||
get => _background_color;
|
||||
set => Set(ref _background_color, value);
|
||||
}
|
||||
|
||||
// day
|
||||
public PNLCellVM<PNLNormalizedData, decimal> day_pnl { get; }
|
||||
public PNLCellVM<PNLNormalizedData, decimal> day_long_pos { get; }
|
||||
public PNLCellVM<PNLNormalizedData, decimal> day_short_pos { get; }
|
||||
public PNLCellVM<PNLNormalizedData, decimal> day_net_pos { get; }
|
||||
// raw day
|
||||
public PNLCellVM<PNLNormalizedData, decimal> raw_day_long_pos { get; }
|
||||
public PNLCellVM<PNLNormalizedData, decimal> raw_day_short_pos { get; }
|
||||
public PNLCellVM<PNLNormalizedData, decimal> raw_day_net_pos { get; }
|
||||
// sod
|
||||
public PNLCellVM<PNLNormalizedData, decimal> sod_pnl { get; }
|
||||
public PNLCellVM<PNLNormalizedData, decimal> sod_long_pos { get; }
|
||||
public PNLCellVM<PNLNormalizedData, decimal> sod_short_pos { get; }
|
||||
public PNLCellVM<PNLNormalizedData, decimal> sod_net_pos { get; }
|
||||
// total
|
||||
public PNLCellVM<PNLNormalizedData, decimal> total_pnl { get; }
|
||||
public PNLCellVM<PNLNormalizedData, decimal> total_net_pos { get; }
|
||||
public PNLCellVM<PNLNormalizedData, decimal> total_long_pos { get; }
|
||||
public PNLCellVM<PNLNormalizedData, decimal> total_short_pos { get; }
|
||||
// working
|
||||
public PNLCellVM<PNLNormalizedData, decimal> working_long_pos { get; }
|
||||
public PNLCellVM<PNLNormalizedData, decimal> working_short_pos { get; }
|
||||
public PNLCellVM<PNLNormalizedData, decimal> working_net_pos { get; }
|
||||
|
||||
public void on_settings(
|
||||
Color background_color,
|
||||
ImmutableDictionary<ColumnKeys, PNLCellAppearance> default_appearances,
|
||||
IReadOnlyList<IPNLAnimationTrigger<PNLNormalizedData>> triggers) {
|
||||
//
|
||||
this.background_color = background_color;
|
||||
_default_appearances = default_appearances;
|
||||
_triggers = triggers;
|
||||
_animator.on_settings(_default_appearances, triggers);
|
||||
_cell_map.set_appearances(_animator.cell_appearances);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
using bts.utils;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Windows.Data;
|
||||
using ColumnKeys = bts.shell.pnl.PNLView.Columns;
|
||||
|
||||
namespace bts.shell.pnl {
|
||||
interface IPNLColumnComparer {
|
||||
IComparer value_comparer { get; }
|
||||
IComparer<PNLRowVM> row_comparer { get; }
|
||||
}
|
||||
|
||||
class PNLColumnComparer<TInner> : IPNLColumnComparer, IComparer<PNLRowVM>, IComparer {
|
||||
public readonly IComparer<TInner> inner_comparer;
|
||||
IComparer _value_comparer;
|
||||
readonly Func<PNLRowVM, TInner> _get;
|
||||
|
||||
public PNLColumnComparer(Func<PNLRowVM, TInner> get)
|
||||
: this(get, Comparer<TInner>.Default) { }
|
||||
|
||||
public PNLColumnComparer(Func<PNLRowVM, TInner> get, IComparer<TInner> inner_comparer) {
|
||||
_get = get;
|
||||
this.inner_comparer = inner_comparer;
|
||||
}
|
||||
|
||||
public IComparer value_comparer => _value_comparer ?? (_value_comparer = inner_comparer.to_non_generic());
|
||||
public IComparer<PNLRowVM> row_comparer => this;
|
||||
|
||||
public int Compare(object x, object y) =>
|
||||
Compare(x as PNLRowVM, y as PNLRowVM);
|
||||
|
||||
public int Compare(PNLRowVM x, PNLRowVM y) {
|
||||
if (x == null) return y == null ? 0 : -1;
|
||||
if (y == null) return 1;
|
||||
var xval = _get(x);
|
||||
var yval = _get(y);
|
||||
return inner_comparer.Compare(xval, yval);
|
||||
}
|
||||
}
|
||||
|
||||
class ReversePNLColumnComparer : IPNLColumnComparer, IComparer<PNLRowVM>, IComparer {
|
||||
readonly IPNLColumnComparer _comp_tuple;
|
||||
|
||||
public ReversePNLColumnComparer(IPNLColumnComparer comp_tuple) =>
|
||||
_comp_tuple = comp_tuple;
|
||||
|
||||
public IComparer value_comparer => this;
|
||||
public IComparer<PNLRowVM> row_comparer => this;
|
||||
|
||||
int IComparer.Compare(object x, object y) =>
|
||||
_comp_tuple.value_comparer.Compare(y, x);
|
||||
|
||||
int IComparer<PNLRowVM>.Compare(PNLRowVM x, PNLRowVM y) =>
|
||||
_comp_tuple.row_comparer.Compare(y, x);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An <see cref="IComparer"/> that can compare 2 objects of type <see cref="PNLRowVM"/>, and 2 objects of type <see cref="CollectionViewGroup"/>
|
||||
/// (assuming the latter's <see cref="CollectionViewGroup.Name"/> is of type <see cref="IPNLCellData"/>). Cross type comparisons are not supported.
|
||||
/// </summary>
|
||||
class PNLSuperRowComparer : IComparer, IComparer<PNLRowVM>, IComparer<CollectionViewGroup> {
|
||||
readonly Func<ColumnKeys, IPNLColumnComparer> _get_column_comparer;
|
||||
IComparer<PNLRowVM> _row_comp;
|
||||
|
||||
public PNLSuperRowComparer(IComparer<PNLRowVM> row_comp, Func<ColumnKeys, IPNLColumnComparer> get_column_comparer) {
|
||||
_row_comp = row_comp;
|
||||
_get_column_comparer = get_column_comparer;
|
||||
}
|
||||
|
||||
public int Compare(object x, object y) {
|
||||
var rc = _try_compare_rows(x as PNLRowVM, y as PNLRowVM);
|
||||
if (rc.HasValue)
|
||||
return rc.Value;
|
||||
var gc = _try_compare_groups(x as CollectionViewGroup, y as CollectionViewGroup);
|
||||
if (gc.HasValue)
|
||||
return gc.Value;
|
||||
throw new InvalidOperationException($"{nameof(PNLSuperRowComparer)} unexpected comparison '{x}' with '{y}.'");
|
||||
}
|
||||
|
||||
int IComparer<PNLRowVM>.Compare(PNLRowVM x, PNLRowVM y) =>
|
||||
_try_compare_rows(x, y) ?? throw new InvalidOperationException($"Failed to compare '{x}' with '{y}' (expected both to be of type {typeof(PNLRowVM)}).");
|
||||
|
||||
int IComparer<CollectionViewGroup>.Compare(CollectionViewGroup x, CollectionViewGroup y) =>
|
||||
_try_compare_groups(x, y) ?? throw new InvalidOperationException($"Failed to compare '{x}' with '{y}' (expected both to be of type {typeof(CollectionViewGroup)}, with Names of type {typeof(IPNLCellVM)}).");
|
||||
|
||||
int? _try_compare_rows(PNLRowVM x, PNLRowVM y) {
|
||||
if (x == null || y == null) return null;
|
||||
return _row_comp.Compare(x, y);
|
||||
}
|
||||
|
||||
int? _try_compare_groups(CollectionViewGroup x, CollectionViewGroup y) {
|
||||
if (x == null || y == null)
|
||||
return null;
|
||||
var xcell = (IPNLCellData)x.Name;
|
||||
var ycell = (IPNLCellData)y.Name;
|
||||
var comp = _get_column_comparer(xcell.column);
|
||||
return comp.value_comparer.Compare(xcell.value, ycell.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
using bts.api;
|
||||
using bts.utils;
|
||||
using mbts;
|
||||
using mbts.spark;
|
||||
using mbts.ui;
|
||||
using mbts.ui.controls;
|
||||
using mbts.ui.shell;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using ColumnKeys = bts.shell.pnl.PNLView.Columns;
|
||||
|
||||
namespace bts.shell.pnl {
|
||||
class PNLRowVM : DisposableViewModelBase, IEquatable<PNLRowVM> {
|
||||
public static readonly IComparer<PNLRowVM> key_comparer = Comparer<PNLKey>.Default.wrap<PNLKey, PNLRowVM>(r => r.key);
|
||||
static readonly Func<bool, string> _fmt_has_day_trades = hdt => hdt ? "Yes" : "No";
|
||||
//
|
||||
readonly PNLCellIndexBuilder<PNLRowData> _cell_map = new PNLCellIndexBuilder<PNLRowData>();
|
||||
readonly PNLCellEmbeddedAnimationModel<PNLRowData> _animator;
|
||||
bool _refresh_normalized_data = true, _is_selected;
|
||||
PNLNormalizedData _normalized_data;
|
||||
|
||||
public PNLRowVM(PNLRowData data, ImmutableDictionary<ColumnKeys, PNLCellAppearance> default_appearances, IReadOnlyList<IPNLAnimationTrigger<PNLRowData>> triggers) {
|
||||
this.data = data;
|
||||
_animator = new PNLCellEmbeddedAnimationModel<PNLRowData>(default_appearances, triggers);
|
||||
_animator.on_row(data);
|
||||
pnl_price = _cell_map.add_price_double(ColumnKeys.PNLPrice, d => d.pnl_price);
|
||||
account_name_cell = _cell_map.add(ColumnKeys.Account, d => d.account_name);
|
||||
security_type_cell = _cell_map.add(ColumnKeys.SecurityType, d => d.security?.ContractCase ?? Security.ContractOneofCase.None);
|
||||
exchange_cell = _cell_map.add(ColumnKeys.Exchange, d => d.security?.Exchange ?? Venue.Unset);
|
||||
symbol_cell = _cell_map.add(ColumnKeys.Symbol, d => d.security?.Symbol ?? string.Empty);
|
||||
security_cell = _cell_map.add(ColumnKeys.Security, d => d.security, null, EqualityComparer<SecurityImmutable?>.Default, Comparer<SecurityImmutable>.Default.to_nullable());
|
||||
security_key = _cell_map.add(ColumnKeys.SecurityKey, d => d.key.security_id);
|
||||
cpp = _cell_map.add(ColumnKeys.CPP, d => d.cpp);
|
||||
// sod
|
||||
sod_pnl = _cell_map.add_pnl_double(ColumnKeys.SODPNL, d => d.sod_pnl);
|
||||
sod_net_pos = _cell_map.add_net_pos(ColumnKeys.SODNetPos, d => d.sod_net_pos);
|
||||
sod_long_pos = _cell_map.add_long_pos(ColumnKeys.SODLongPos, d => d.sod_long_pos);
|
||||
sod_long_avg_price = _cell_map.add_long_price(ColumnKeys.SODLongAvgPrice, d => d.sod_long_avg_price_raw);
|
||||
sod_short_pos = _cell_map.add_short_pos(ColumnKeys.SODShortPos, d => d.sod_short_pos);
|
||||
sod_short_avg_price = _cell_map.add_short_price(ColumnKeys.SODShortAvgPrice, d => d.sod_short_avg_price_raw);
|
||||
// day
|
||||
day_pnl = _cell_map.add_pnl_double(ColumnKeys.DayPNL, d => d.day_pnl);
|
||||
day_net_pos = _cell_map.add_net_pos(ColumnKeys.DayNetPos, d => d.day_net_pos);
|
||||
day_long_pos = _cell_map.add_long_pos(ColumnKeys.DayLongPos, d => d.day_long_pos);
|
||||
day_long_avg_price = _cell_map.add_long_price(ColumnKeys.DayLongAvgPrice, d => d.day_long_avg_price_raw);
|
||||
day_short_pos = _cell_map.add_short_pos(ColumnKeys.DayShortPos, d => d.day_short_pos);
|
||||
day_short_avg_price = _cell_map.add_short_price(ColumnKeys.DayShortAvgPrice, d => d.day_short_avg_price_raw);
|
||||
has_day_trades = _cell_map.add(ColumnKeys.HasDayTrades, d => d.has_day_trades, _fmt_has_day_trades);
|
||||
// raw day
|
||||
raw_day_net_pos = _cell_map.add_net_pos(ColumnKeys.RawDayNetPos, d => d.raw_day_net_pos);
|
||||
raw_day_long_pos = _cell_map.add_long_pos(ColumnKeys.RawDayLongPos, d => d.raw_day_long_pos);
|
||||
raw_day_short_pos = _cell_map.add_short_pos(ColumnKeys.RawDayShortPos, d => d.raw_day_short_pos);
|
||||
// working
|
||||
working_net_pos = _cell_map.add_net_pos(ColumnKeys.WorkingNetPos, d => d.working_net_pos);
|
||||
working_long_pos = _cell_map.add_working_long(ColumnKeys.WorkingLongPos, d => d.working.long_data, d => d.bid_price);
|
||||
working_long_avg_price = _cell_map.add_long_price(ColumnKeys.WorkingLongAvgPrice, d => d.working_long_avg_price_raw);
|
||||
working_short_pos = _cell_map.add_working_short(ColumnKeys.WorkingShortPos, r => r.working.short_data, r => r.ask_price);
|
||||
working_short_avg_price = _cell_map.add_short_price(ColumnKeys.WorkingShortAvgPrice, d => d.working_short_avg_price_raw);
|
||||
// total
|
||||
total_pnl = _cell_map.add_pnl_double(ColumnKeys.TotalPNL, d => d.total_pnl);
|
||||
total_net_pos = _cell_map.add_net_pos(ColumnKeys.TotalNetPos, d => d.total_net_pos);
|
||||
total_long_pos = _cell_map.add_long_pos(ColumnKeys.TotalLongPos, d => d.total_long_pos);
|
||||
total_long_avg_price = _cell_map.add_long_price(ColumnKeys.TotalLongAvgPrice, d => d.total_long_avg_price_raw);
|
||||
total_short_pos = _cell_map.add_short_pos(ColumnKeys.TotalShortPos, d => d.total_short_pos);
|
||||
total_short_avg_price = _cell_map.add_short_price(ColumnKeys.TotalShortAvgPrice, d => d.total_short_avg_price_raw);
|
||||
// market
|
||||
bid_price = _cell_map.add_price_double(ColumnKeys.BidPrice, d => d.bid_price);
|
||||
ask_price = _cell_map.add_price_double(ColumnKeys.AskPrice, d => d.ask_price);
|
||||
mid_price = _cell_map.add_price_double(ColumnKeys.MidPrice, d => d.mid_price);
|
||||
last_price = _cell_map.add_price_double(ColumnKeys.LastPrice, d => d.last_price);
|
||||
settlement_price = _cell_map.add_price_double(ColumnKeys.SettlementPrice, d => d.settlement_price);
|
||||
|
||||
_cell_map.on_row(data);
|
||||
_cell_map.set_appearances(_animator.cell_appearances);
|
||||
_dispose = _cell_map.animation_model.OnDispatcher().Subscribe(new_animations => {
|
||||
animations = new_animations.Select(kv => new mbts.ui.spark.CellAnimation(kv.Key.column, kv.Key.type, kv.Value.color.to_brush())).ToList();
|
||||
RaisePropertyChanged(nameof(animations));
|
||||
});
|
||||
}
|
||||
|
||||
// events
|
||||
public event Action<PNLNormalizedData> updated;
|
||||
public event Action<bool> is_selected_changed;
|
||||
|
||||
// data
|
||||
public PNLKey key => data.key;
|
||||
public PNLRowData data { get; private set; }
|
||||
public PNLNormalizedData normalized_data {
|
||||
get {
|
||||
if (_refresh_normalized_data) {
|
||||
_refresh_normalized_data = false;
|
||||
// only count futures for [non-raw] positions (they already take spread legs into account)
|
||||
decimal day_long, day_short, sod_long, sod_short;
|
||||
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);
|
||||
}
|
||||
var nd = new PNLNormalizedData(
|
||||
(decimal)DoubleUtil.nan_to_zero(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),
|
||||
sod_long,
|
||||
sod_short,
|
||||
(decimal)DoubleUtil.nan_to_zero(data.working_long_pos),
|
||||
(decimal)DoubleUtil.nan_to_zero(data.working_short_pos));
|
||||
if (_normalized_data != nd) {
|
||||
_normalized_data = nd;
|
||||
updated?.Invoke(nd);
|
||||
}
|
||||
}
|
||||
return _normalized_data;
|
||||
}
|
||||
}
|
||||
// security
|
||||
public string account_name => data.account_name;
|
||||
public Venue exchange => security?.Exchange ?? Venue.Unset;
|
||||
public string symbol => security.symbol_or_default();
|
||||
public Security.ContractOneofCase security_type => security?.ContractCase ?? Security.ContractOneofCase.None;
|
||||
public SecurityImmutable? security => data.security;
|
||||
// cells
|
||||
public PNLCellVM<PNLRowData, string> account_name_cell { get; }
|
||||
public PNLCellVM<PNLRowData, Venue> exchange_cell { get; }
|
||||
public PNLCellVM<PNLRowData, string> symbol_cell { get; }
|
||||
public PNLCellVM<PNLRowData, Security.ContractOneofCase> security_type_cell { get; }
|
||||
public PNLCellVM<PNLRowData, SecurityImmutable?> security_cell { get; }
|
||||
public PNLCellVM<PNLRowData, uint> security_key { get; }
|
||||
public PNLCellVM<PNLRowData, double> cpp { get; }
|
||||
// sod
|
||||
public PNLCellVM<PNLRowData, double> sod_pnl { get; }
|
||||
public PNLCellVM<PNLRowData, double> sod_net_pos { get; }
|
||||
public PNLCellVM<PNLRowData, double> sod_long_pos { get; }
|
||||
public PNLCellVM<PNLRowData, double> sod_long_avg_price { get; }
|
||||
public PNLCellVM<PNLRowData, double> sod_short_pos { get; }
|
||||
public PNLCellVM<PNLRowData, double> sod_short_avg_price { get; }
|
||||
// day
|
||||
public PNLCellVM<PNLRowData, double> day_pnl { get; }
|
||||
public PNLCellVM<PNLRowData, double> day_net_pos { get; }
|
||||
public PNLCellVM<PNLRowData, double> day_long_pos { get; }
|
||||
public PNLCellVM<PNLRowData, double> day_long_avg_price { get; }
|
||||
public PNLCellVM<PNLRowData, double> day_short_pos { get; }
|
||||
public PNLCellVM<PNLRowData, double> day_short_avg_price { get; }
|
||||
public PNLCellVM<PNLRowData, bool> has_day_trades { get; }
|
||||
// raw day
|
||||
public PNLCellVM<PNLRowData, double> raw_day_net_pos { get; }
|
||||
public PNLCellVM<PNLRowData, double> raw_day_long_pos { get; }
|
||||
public PNLCellVM<PNLRowData, double> raw_day_short_pos { get; }
|
||||
// working
|
||||
public PNLCellVM<PNLRowData, double> working_net_pos { get; }
|
||||
public PNLCellVM<PNLRowData, WorkingOrderSideData> working_long_pos { get; }
|
||||
public PNLCellVM<PNLRowData, double> working_long_avg_price { get; }
|
||||
public PNLCellVM<PNLRowData, WorkingOrderSideData> working_short_pos { get; }
|
||||
public PNLCellVM<PNLRowData, double> working_short_avg_price { get; }
|
||||
// total
|
||||
public PNLCellVM<PNLRowData, double> total_pnl { get; }
|
||||
public PNLCellVM<PNLRowData, double> total_net_pos { get; }
|
||||
public PNLCellVM<PNLRowData, double> total_long_pos { get; }
|
||||
public PNLCellVM<PNLRowData, double> total_long_avg_price { get; }
|
||||
public PNLCellVM<PNLRowData, double> total_short_pos { get; }
|
||||
public PNLCellVM<PNLRowData, double> total_short_avg_price { get; }
|
||||
// (market)
|
||||
public PNLCellVM<PNLRowData, double> bid_price { get; }
|
||||
public PNLCellVM<PNLRowData, double> mid_price { get; }
|
||||
public PNLCellVM<PNLRowData, double> ask_price { get; }
|
||||
public PNLCellVM<PNLRowData, double> last_price { get; }
|
||||
public PNLCellVM<PNLRowData, double> settlement_price { get; }
|
||||
public PNLCellVM<PNLRowData, double> pnl_price { get; }
|
||||
|
||||
public IList<mbts.ui.spark.CellAnimation> animations { get; private set; }
|
||||
public bool is_selected {
|
||||
get => _is_selected;
|
||||
set {
|
||||
if (Set(ref _is_selected, value))
|
||||
is_selected_changed?.Invoke(value);
|
||||
}
|
||||
}
|
||||
|
||||
public void update(PNLRowData data) {
|
||||
_refresh_normalized_data = true;
|
||||
this.data = data;
|
||||
_cell_map.on_row(data);
|
||||
_animator.on_row(data);
|
||||
_cell_map.set_appearances(_animator.cell_appearances);
|
||||
}
|
||||
|
||||
public void on_settings(ImmutableDictionary<ColumnKeys, PNLCellAppearance> defaults, IReadOnlyList<IPNLAnimationTrigger<PNLRowData>> triggers) {
|
||||
_animator.on_settings(defaults, triggers);
|
||||
_cell_map.set_appearances(_animator.cell_appearances);
|
||||
}
|
||||
|
||||
public bool Equals(PNLRowVM other) =>
|
||||
other != null && key == other.key;
|
||||
|
||||
public override int GetHashCode() =>
|
||||
key.GetHashCode();
|
||||
|
||||
public override string ToString() =>
|
||||
$"account: {account_name} exchange: {exchange} symbol: {symbol} security: {security} buy_quantity: {day_long_pos} sell_quantity: {day_short_pos} pnl: {total_pnl}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,764 @@
|
||||
using bts.api;
|
||||
using bts.api.core;
|
||||
using bts.api.orderman;
|
||||
using bts.collections;
|
||||
using bts.utils;
|
||||
using mbts;
|
||||
using mbts.collections;
|
||||
using mbts.persist;
|
||||
using mbts.spark;
|
||||
using mbts.spark.filters;
|
||||
using mbts.ui;
|
||||
using mbts.ui.controls;
|
||||
using mbts.ui.settings;
|
||||
using mbts.ui.shell;
|
||||
using mbts.ui.utility;
|
||||
using mbts.utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Specialized;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using ColumnKeys = bts.shell.pnl.PNLView.Columns;
|
||||
using SecurityType = bts.api.Security.ContractOneofCase;
|
||||
|
||||
namespace bts.shell.pnl {
|
||||
class PNLVM : DisposableViewModelBase, IHandleRowSelection {
|
||||
static readonly IReadOnlyList<Func<PNLVM, IDataGridColumnFilter<PNLRowData>>> _filter_getters =
|
||||
ObjectSnapshot.property_delegates_of_type<PNLVM, IDataGridColumnFilter<PNLRowData>>().ToReadOnlyList();
|
||||
static readonly TimeSpan _auxiliary_grid_update_throttle = TimeSpan.FromSeconds(0.2);
|
||||
// delegates
|
||||
static readonly Func<OrderSecurityRootPair, PNLKey> _order_pnl_key_fn = o => new PNLKey(o.order.order.account_id(), o.order.order.security_key());
|
||||
static readonly Func<FillTuple, PNLKey> _fill_pnl_key_fn = f => new PNLKey(f.account_id, f.fill.SecurityKey);
|
||||
|
||||
readonly IBus _bus;
|
||||
readonly OrdersModel _orders_model;
|
||||
readonly IObservableCollectionAddRemoveOnly<PNLRowVM> _selected_rows = new ObservableSortedCollection<PNLRowVM>(PNLRowVM.key_comparer);
|
||||
readonly IList<PNLRowData> _rows_data_wrapper;
|
||||
readonly IList<PNLKey> _rows_key_wrapper;
|
||||
readonly DeferrableActionWrapper _refresh_filter;
|
||||
//
|
||||
readonly Property<PNLCalculationType> _calculation_type;
|
||||
PNLHeaderRowDisplay _header_row_display = PNLHeaderRowDisplay.Total;
|
||||
PNLRowCollectionView _rows_view;
|
||||
IReadOnlyList<(ColumnKeys key, ListSortDirection direction)> _sorting_hierarchy;
|
||||
Brush _selection_highlight;
|
||||
Color _grid_background_color = Colors.Transparent;
|
||||
bool _is_paused;
|
||||
// default appearances
|
||||
ImmutableDictionary<ColumnKeys, PNLCellAppearance>
|
||||
_default_appearances = ImmutableDictionary<ColumnKeys, PNLCellAppearance>.Empty,
|
||||
_default_group_appearances = ImmutableDictionary<ColumnKeys, PNLCellAppearance>.Empty,
|
||||
_default_header_appearances = ImmutableDictionary<ColumnKeys, PNLCellAppearance>.Empty;
|
||||
// triggers
|
||||
IReadOnlyList<IPNLAnimationTrigger<PNLRowData>>
|
||||
_triggers = Array.Empty<IPNLAnimationTrigger<PNLRowData>>();
|
||||
IReadOnlyList<IPNLAnimationTrigger<PNLNormalizedData>>
|
||||
_group_triggers = Array.Empty<IPNLAnimationTrigger<PNLNormalizedData>>(),
|
||||
_header_triggers = Array.Empty<IPNLAnimationTrigger<PNLNormalizedData>>();
|
||||
// pause/resume
|
||||
(TableUpdate<PNLRowData, PNLKey> filtered, TableUpdate<PNLRowData, PNLKey> raw)? _resume;
|
||||
TableUpdate<PNLRowData, PNLKey> _last_table = TableUpdate<PNLRowData, PNLKey>.mk_empty(PNLRowData.key_fn);
|
||||
AlternationConverter _group_background_converter = new AlternationConverter();
|
||||
|
||||
public PNLVM(IBus bus, OrdersModel orders_model, PNLSettings settings)
|
||||
: this(bus, orders_model, settings, orders_model.security_account_pnl) { }
|
||||
|
||||
PNLVM(IBus bus, OrdersModel orders_model, PNLSettings settings, IObservable<TableUpdate<PNLRowData, PNLKey>> pnl_source) {
|
||||
_bus = bus;
|
||||
_orders_model = orders_model;
|
||||
_rows_data_wrapper = rows.WrapSelector(r => r.data, d => new PNLRowVM(d, _default_appearances, _triggers));
|
||||
_rows_key_wrapper = rows.WrapSelector(r => r.key, null);
|
||||
_rows_view = new PNLRowCollectionView(rows, _default_group_appearances, _group_triggers);
|
||||
_calculation_type = backing(nameof(calculation_type), PNLCalculationType.Waterfall);
|
||||
anchors.AddRange(settings.anchors);
|
||||
_group_background_converter.Values.AddRange(BTSColors.PNL.group_alternating_background_colors);
|
||||
// commands
|
||||
set_anchor_cmd = new DelegateCommand<ColumnKeys>(key => anchors.SetItems(new[] { key }));
|
||||
append_anchor_cmd = new DelegateCommand<ColumnKeys>(key => {
|
||||
if (!anchors.Remove(key))
|
||||
anchors.Add(key);
|
||||
});
|
||||
// filters
|
||||
account_filter = DiscreteValuesFilterVM.Common.account<PNLRowData>(orders_model, r => new AccountTuple(r.key.account_id, r.account_name), nameof(PNLRowVM.account_name));
|
||||
_refresh_filter = new DeferrableActionWrapper(() => _filter_changed?.Invoke(get_filters_states().build_predicate()));
|
||||
var filter_src = Observable.Create<Func<PNLRowData, bool>>(o => {
|
||||
o.OnNext(get_filters_states().build_predicate());
|
||||
_filter_changed += o.OnNext;
|
||||
return Disposable.Create(() => _filter_changed -= o.OnNext);
|
||||
});
|
||||
foreach (var f in _all_filters)
|
||||
f.filter_changed += _refresh_filter.execute;
|
||||
// totals/selected headers
|
||||
total_aggregate = new PNLHeaderRowVM(settings.header_background_color, _default_header_appearances, _header_triggers);
|
||||
selected_aggregate = new PNLHeaderRowVM(settings.header_background_color, _default_header_appearances, _header_triggers);
|
||||
// load/subscribe to data
|
||||
load_from(settings);
|
||||
_dispose = Disposable.Create(
|
||||
pnl_source.Operate(
|
||||
_calculation_type.observe_current(),
|
||||
(data, calc) => data.with(calc_type: calc)
|
||||
).FilterWithRaw(filter_src, PNLRowData.key_fn, null)
|
||||
.OnDispatcher()
|
||||
.Subscribe(_update),
|
||||
Disposable.Create(() => {
|
||||
foreach (var f in _all_filters)
|
||||
f.filter_changed -= _refresh_filter.execute;
|
||||
rows.DisposeAll();
|
||||
_rows_view.Groups.OfTypeSafe<IDisposable>().DisposeAll();
|
||||
}));
|
||||
}
|
||||
|
||||
public event Action<IReadOnlyList<(ColumnKeys key, ListSortDirection direction)>> sorting_changed;
|
||||
event Action<SelectionChangedEventArgs> _selection_changed;
|
||||
event Action<TableUpdate<PNLRowData, PNLKey>> _updated;
|
||||
event Action<Func<PNLRowData, bool>> _filter_changed;
|
||||
|
||||
// rows/selected rows
|
||||
public ObservableCollection<PNLRowVM> rows { get; } = new ObservableCollection<PNLRowVM>();
|
||||
public ICollectionView rows_view => _rows_view;
|
||||
public int filtered_row_count => rows.Count;
|
||||
public int total_row_count { get; private set; }
|
||||
public IReadOnlyObservableCollection<PNLRowVM> selected_rows => _selected_rows;
|
||||
// sorting
|
||||
public IReadOnlyList<(ColumnKeys key, ListSortDirection direction)> sorting_hierarchy {
|
||||
get => _sorting_hierarchy ?? (_sorting_hierarchy = Array.Empty<(ColumnKeys, ListSortDirection)>());
|
||||
set {
|
||||
_sorting_hierarchy = value;
|
||||
// use 'sorting_hierarchy' instead of 'value' bc the getter ensures not null
|
||||
comparer = DIYGridSortHelper.build_comparer(sorting_hierarchy, get_column_comparer, ColumnKeys.Account, ColumnKeys.Security);
|
||||
var column_comp_map = sorting_hierarchy.ToDictionary(c => c.key, c => _get_column_comparer(c.key, c.direction));
|
||||
var rows_view_comparer = new PNLSuperRowComparer(comparer, col => {
|
||||
if (column_comp_map.TryGetValue(col, out var comp_tup))
|
||||
return comp_tup;
|
||||
return _get_column_comparer(col);
|
||||
});
|
||||
_rows_view.CustomSort = rows_view_comparer;
|
||||
foreach (var g in _rows_view.GroupDescriptions)
|
||||
g.CustomSort = _rows_view.CustomSort;
|
||||
sorting_changed?.Invoke(sorting_hierarchy);
|
||||
}
|
||||
}
|
||||
|
||||
public IComparer<PNLRowVM> comparer { get; private set; }
|
||||
// display/grouping
|
||||
public PNLHeaderRowDisplay header_row_display { get => _header_row_display; set => Set(ref _header_row_display, value); }
|
||||
public ObservableCollection<ColumnKeys> anchors { get; } = new ObservableCollection<ColumnKeys>();
|
||||
// calc
|
||||
public PNLCalculationType calculation_type { get => _calculation_type.Value; set => _calculation_type.set(value); }
|
||||
public PNLHeaderRowVM total_aggregate { get; }
|
||||
public PNLHeaderRowVM selected_aggregate { get; }
|
||||
public bool is_paused {
|
||||
get => _is_paused;
|
||||
set {
|
||||
if (Set(ref _is_paused, value) && !value && _resume.HasValue) {
|
||||
var curr_state = ImmutableDictionary<PNLKey, PNLRowData>.Empty.AddRange(rows.Select(r => new KeyValuePair<PNLKey, PNLRowData>(r.key, r.data)));
|
||||
var reset = TableUpdate<PNLRowData, PNLKey>.update(
|
||||
new TableUpdate<PNLRowData, PNLKey>(
|
||||
curr_state,
|
||||
curr_state.Values,
|
||||
Enumerable.Empty<Change<PNLRowData>>(),
|
||||
Enumerable.Empty<PNLRowData>(),
|
||||
PNLRowData.key_fn),
|
||||
_resume.Value.filtered.state,
|
||||
EqualityComparer<PNLRowData>.Default);
|
||||
_update((reset, _resume.Value.raw));
|
||||
_resume = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Brush selection_highlight { get => _selection_highlight; set => Set(ref _selection_highlight, value); }
|
||||
public Color grid_background_color { get => _grid_background_color; set => Set(ref _grid_background_color, value); }
|
||||
public IValueConverter group_background_converter => _group_background_converter;
|
||||
|
||||
// commands
|
||||
public ICommand set_anchor_cmd { get; }
|
||||
public ICommand append_anchor_cmd { get; }
|
||||
public ICommand show_working_orders_cmd { get; }
|
||||
|
||||
/// security
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, uint[]> account_filter { get; }
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, Venue[]> exchange_filter { get; } = DiscreteValuesFilterVM.Common.exchange<PNLRowData>(r => r.security.exchange_or_default(), nameof(PNLRowVM.exchange));
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, string[]> symbol_filter { get; } = DiscreteValuesFilterVM.Common.symbol<PNLRowData>(r => r.security.symbol_or_default(), nameof(PNLRowVM.symbol));
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, string> security_filter { get; } = new RegexFilterVM<PNLRowData>(r => r.security?.ToString(), nameof(PNLRowData.security));
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, string> security_key_filter { get; } = new RegexFilterVM<PNLRowData>(r => r.key.security_id.ToString());
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, SecurityType[]> security_type_filter { get; } = DiscreteValuesFilterVM.Create<PNLRowData, SecurityType>(
|
||||
EnumHelper.get_values_except(SecurityType.Option, SecurityType.Synthetic).ToArray(),
|
||||
r => r.security?.ContractCase ?? SecurityType.None,
|
||||
nameof(PNLRowData.security));
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> cpp_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.cpp);
|
||||
/// sod
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> sod_pnl_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.sod_pnl);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> sod_long_pos_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.sod.long_pos);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> sod_long_avg_price_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.sod_long_avg_price_raw);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> sod_short_pos_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.sod.short_pos);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> sod_short_avg_price_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.sod_short_avg_price_raw);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> sod_net_pos_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.sod.net_pos);
|
||||
/// day
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> day_pnl_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.day_pnl);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> day_long_pos_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.day_long_pos);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> day_long_avg_price_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.day_long_avg_price_raw);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> day_short_pos_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.day_short_pos);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> day_short_avg_price_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.day_short_avg_price_raw);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> day_net_pos_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.day_net_pos);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, bool[]> has_day_trades_filter { get; } = DiscreteValuesFilterVM.Create<PNLRowData, bool>(new[] { true, false }, r => r.has_day_trades);
|
||||
/// raw day
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> raw_day_long_pos_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.raw_day_long_pos);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> raw_day_short_pos_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.raw_day_short_pos);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> raw_day_net_pos_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.raw_day_net_pos);
|
||||
/// working (long)
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> working_long_pos_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.working_long_pos);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> working_long_avg_price_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.working_long_avg_price_raw);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> working_short_pos_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.working_short_pos);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> working_short_avg_price_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.working_short_avg_price_raw);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> working_net_pos_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.working_net_pos);
|
||||
/// total
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> total_pnl_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.total_pnl);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> total_long_pos_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.total_long_pos);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> total_long_avg_price_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.total_long_avg_price_raw);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> total_short_pos_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.total_short_pos);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> total_short_avg_price_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.total_short_avg_price_raw);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> total_net_pos_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.total_net_pos);
|
||||
/// market
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> bid_price_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.bid_price);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> ask_price_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.ask_price);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> mid_price_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.mid_price);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> last_price_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.last_price);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> settlement_price_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.settlement_price);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> pnl_price_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.pnl_price);
|
||||
|
||||
IEnumerable<IDataGridColumnFilter<PNLRowData>> _all_filters =>
|
||||
_filter_getters.Select(get => get(this));
|
||||
|
||||
public static IComparer<PNLRowVM> get_column_comparer(ColumnKeys column) =>
|
||||
_get_column_comparer(column).row_comparer;
|
||||
|
||||
public FilterStates get_filters_states() => new FilterStates {
|
||||
account = account_filter.get_state(),
|
||||
exchange = exchange_filter.get_state(),
|
||||
symbol = symbol_filter.get_state(),
|
||||
security_type = security_type_filter.get_state(),
|
||||
security = security_filter.get_state(),
|
||||
security_key = security_key_filter.get_state(),
|
||||
cpp = cpp_filter.get_state(),
|
||||
/// sod
|
||||
sod_pnl = sod_pnl_filter.get_state(),
|
||||
sod_long_pos = sod_long_pos_filter.get_state(),
|
||||
sod_long_avg_price = sod_long_avg_price_filter.get_state(),
|
||||
sod_short_pos = sod_short_pos_filter.get_state(),
|
||||
sod_short_avg_price = sod_short_avg_price_filter.get_state(),
|
||||
sod_net_pos = sod_net_pos_filter.get_state(),
|
||||
/// day
|
||||
day_pnl = day_pnl_filter.get_state(),
|
||||
day_long_pos = day_long_pos_filter.get_state(),
|
||||
day_long_avg_price = day_long_avg_price_filter.get_state(),
|
||||
day_short_pos = day_short_pos_filter.get_state(),
|
||||
day_short_avg_price = day_short_avg_price_filter.get_state(),
|
||||
day_net_pos = day_net_pos_filter.get_state(),
|
||||
has_day_trades = has_day_trades_filter.get_state(),
|
||||
/// raw day
|
||||
raw_day_long_pos = raw_day_long_pos_filter.get_state(),
|
||||
raw_day_short_pos = raw_day_short_pos_filter.get_state(),
|
||||
raw_day_net_pos = raw_day_net_pos_filter.get_state(),
|
||||
/// total
|
||||
total_pnl = total_pnl_filter.get_state(),
|
||||
total_long_pos = total_long_pos_filter.get_state(),
|
||||
total_long_avg_price = total_long_avg_price_filter.get_state(),
|
||||
total_short_pos = total_short_pos_filter.get_state(),
|
||||
total_short_avg_price = total_short_avg_price_filter.get_state(),
|
||||
total_net_pos = total_net_pos_filter.get_state(),
|
||||
/// working
|
||||
working_long_pos = working_long_pos_filter.get_state(),
|
||||
working_long_avg_price = working_long_avg_price_filter.get_state(),
|
||||
working_short_pos = working_short_pos_filter.get_state(),
|
||||
working_short_avg_price = working_short_avg_price_filter.get_state(),
|
||||
working_net_pos = working_net_pos_filter.get_state(),
|
||||
/// market
|
||||
bid_price = bid_price_filter.get_state(),
|
||||
ask_price = ask_price_filter.get_state(),
|
||||
mid_price = mid_price_filter.get_state(),
|
||||
last_price = last_price_filter.get_state(),
|
||||
settlement_price = settlement_price_filter.get_state(),
|
||||
pnl_price = pnl_price_filter.get_state(),
|
||||
};
|
||||
|
||||
public void load_from(PNLSettings settings) {
|
||||
selection_highlight = settings.grid.selection_highlight.to_brush();
|
||||
grid_background_color = settings.grid.background;
|
||||
calculation_type = settings.calculation_type;
|
||||
if (settings.filters != null) {
|
||||
using (_refresh_filter.defer_execution()) {
|
||||
var f = settings.filters;
|
||||
/// security
|
||||
account_filter.load_state(f.account);
|
||||
exchange_filter.load_state(f.exchange);
|
||||
symbol_filter.load_state(f.symbol);
|
||||
security_type_filter.load_state(f.security_type);
|
||||
security_filter.load_state(f.security);
|
||||
security_key_filter.load_state(f.security_key);
|
||||
cpp_filter.load_state(f.cpp);
|
||||
/// sod
|
||||
sod_pnl_filter.load_state(f.sod_pnl);
|
||||
sod_long_pos_filter.load_state(f.sod_long_pos);
|
||||
sod_long_avg_price_filter.load_state(f.sod_long_avg_price);
|
||||
sod_short_pos_filter.load_state(f.sod_short_pos);
|
||||
sod_short_avg_price_filter.load_state(f.sod_short_avg_price);
|
||||
sod_net_pos_filter.load_state(f.sod_net_pos);
|
||||
/// day
|
||||
day_pnl_filter.load_state(f.day_pnl);
|
||||
day_long_pos_filter.load_state(f.day_long_pos);
|
||||
day_long_avg_price_filter.load_state(f.day_long_avg_price);
|
||||
day_short_pos_filter.load_state(f.day_short_pos);
|
||||
day_short_avg_price_filter.load_state(f.day_short_avg_price);
|
||||
day_net_pos_filter.load_state(f.day_net_pos);
|
||||
has_day_trades_filter.load_state(f.has_day_trades);
|
||||
/// raw day
|
||||
raw_day_long_pos_filter.load_state(f.raw_day_long_pos);
|
||||
raw_day_short_pos_filter.load_state(f.raw_day_short_pos);
|
||||
raw_day_net_pos_filter.load_state(f.raw_day_net_pos);
|
||||
/// total
|
||||
total_pnl_filter.load_state(f.total_pnl);
|
||||
total_long_pos_filter.load_state(f.total_long_pos);
|
||||
total_long_avg_price_filter.load_state(f.total_long_avg_price);
|
||||
total_short_pos_filter.load_state(f.total_short_pos);
|
||||
total_short_avg_price_filter.load_state(f.total_short_avg_price);
|
||||
total_net_pos_filter.load_state(f.total_net_pos);
|
||||
/// working
|
||||
working_long_pos_filter.load_state(f.working_long_pos);
|
||||
working_long_avg_price_filter.load_state(f.working_long_avg_price);
|
||||
working_short_pos_filter.load_state(f.working_short_pos);
|
||||
working_short_avg_price_filter.load_state(f.working_short_avg_price);
|
||||
working_net_pos_filter.load_state(f.working_net_pos);
|
||||
/// market
|
||||
bid_price_filter.load_state(f.bid_price);
|
||||
mid_price_filter.load_state(f.mid_price);
|
||||
ask_price_filter.load_state(f.ask_price);
|
||||
last_price_filter.load_state(f.last_price);
|
||||
settlement_price_filter.load_state(f.settlement_price);
|
||||
pnl_price_filter.load_state(f.pnl_price);
|
||||
}
|
||||
}
|
||||
anchors.sync_with(settings.anchors);
|
||||
header_row_display = settings.header_row_display;
|
||||
sorting_hierarchy = DIYGridSortHelper.convert_sorting_criterion<ColumnKeys>(settings.sorting_criterion).ToList();
|
||||
// group background colors
|
||||
_group_background_converter = new AlternationConverter();
|
||||
_group_background_converter.Values.AddRange(settings.group_background_colors.Select(c => c.argb_color()));
|
||||
RaisePropertyChanged(nameof(group_background_converter));
|
||||
//
|
||||
map_appearances(ref _default_appearances, settings.grid.columns, settings.grid_defaults);
|
||||
map_appearances(ref _default_group_appearances, settings.group_columns, settings.group_cell_appearance.to_appearance().merge(settings.group_defaults));
|
||||
map_appearances(ref _default_header_appearances, settings.header_columns, settings.header_cell_appearance.to_appearance().merge(settings.header_defaults));
|
||||
_triggers = settings.triggers();
|
||||
_group_triggers = settings.group_triggers();
|
||||
_header_triggers = settings.header_triggers();
|
||||
foreach (var r in rows)
|
||||
r.on_settings(_default_appearances, _triggers);
|
||||
total_aggregate.on_settings(settings.header_background_color, _default_header_appearances, _header_triggers);
|
||||
selected_aggregate.on_settings(settings.header_background_color,_default_header_appearances, _header_triggers);
|
||||
void map_appearances(ref ImmutableDictionary<ColumnKeys, PNLCellAppearance> from, IEnumerable<GridColumnSettings> col_settings, PNLCellAppearance fallback_appearance) {
|
||||
var dabd = from.ToBuilder();
|
||||
foreach (var k in EnumHelper.get_values<ColumnKeys>()) {
|
||||
if (GridSettingsHelper.try_find_column(col_settings, k, out var match))
|
||||
dabd[k] = new PNLCellAppearance(match.font_color_override, match.background_color_override, match.font_weight_override).merge(fallback_appearance);
|
||||
}
|
||||
from = dabd.ToImmutable();
|
||||
}
|
||||
_rows_view.on_settings(_default_group_appearances, _group_triggers);
|
||||
_rows_view.Refresh();
|
||||
}
|
||||
|
||||
public FillsVM mk_fills_vm(PNLRowVM row, ColumnKeys key) {
|
||||
var state = PNLSettings.DefaultAuxiliaryFills;
|
||||
_append_key_filters(state.filters, key);
|
||||
_append_row_filters(state.filters, row);
|
||||
// hide implicit cols
|
||||
state.grid = _hide_cols(state.grid, anchors);
|
||||
return new FillsVM(
|
||||
_bus, _orders_model, Observable.Return(state), new WindowSettingsManager<GridSettings>.DisconnectedImpl(state.grid));
|
||||
}
|
||||
|
||||
public FillsVM mk_fills_vm(PNLRowCollectionViewGroup group, ColumnKeys key) {
|
||||
var state = PNLSettings.DefaultAuxiliaryFills;
|
||||
_append_key_filters(state.filters, key);
|
||||
_append_group_filters(state.filters, group, anchors);
|
||||
// hide implicit cols
|
||||
state.grid = _hide_cols(state.grid, anchors.WrapSubRange(0, group.level));
|
||||
return new FillsVM(
|
||||
_bus, _orders_model, Observable.Return(state), _visible_fills_source(), new WindowSettingsManager<GridSettings>.DisconnectedImpl(state.grid));
|
||||
}
|
||||
|
||||
public FillsVM mk_fills_vm(PNLHeaderRowDisplay header, ColumnKeys key) {
|
||||
var state = fills.FillsSettings.Default();
|
||||
state.grid = orderbook.OrderBookStartupData.DefaultFillsGrid.general;
|
||||
state.filters = new fills.FillsFilterStates();
|
||||
// hide implicit cols
|
||||
state.grid = _hide_cols(orderbook.OrderBookStartupData.DefaultFillsGrid.general, anchors);
|
||||
_append_key_filters(state.filters, key);
|
||||
var src = _orders_model.on_fill_tuples;
|
||||
switch (header) {
|
||||
case PNLHeaderRowDisplay.SelectedTotal:
|
||||
// throttle selection changes (they have a potentially high frequency)
|
||||
var filter_src = System.Reactive.Linq.ObservableExtensions.FromEventThrottle<SelectionChangedEventArgs, Func<FillTuple, bool>>(
|
||||
cb => _selection_changed += cb,
|
||||
cb => _selection_changed -= cb,
|
||||
_ => _mk_selected_items_filter(_fill_pnl_key_fn),
|
||||
_auxiliary_grid_update_throttle);
|
||||
src = src.Filter(filter_src, FillTuple.key_fn, _mk_selected_items_filter(_fill_pnl_key_fn));
|
||||
break;
|
||||
case PNLHeaderRowDisplay.Total:
|
||||
src = src.Filter(_visible_items_filter_source(_fill_pnl_key_fn), FillTuple.key_fn);
|
||||
break;
|
||||
}
|
||||
return new FillsVM(
|
||||
_bus, _orders_model, Observable.Return(state), src, new WindowSettingsManager<GridSettings>.DisconnectedImpl(state.grid));
|
||||
}
|
||||
|
||||
public OrderBookVM mk_orderbook_vm(PNLHeaderRowDisplay header, ColumnKeys key) {
|
||||
var filters = new orderbook.FilterStates();
|
||||
_append_key_filters(filters, key);
|
||||
var src = _orders_model.on_order_tuples_with_root_and_piq;
|
||||
switch (header) {
|
||||
case PNLHeaderRowDisplay.SelectedTotal:
|
||||
// throttle selection changes (they have a potentially high frequency)
|
||||
var filter_src = System.Reactive.Linq.ObservableExtensions.FromEventThrottle<SelectionChangedEventArgs, Func<OrderSecurityRootPair, bool>>(
|
||||
cb => _selection_changed += cb,
|
||||
cb => _selection_changed -= cb,
|
||||
_ => _mk_selected_items_filter(_order_pnl_key_fn),
|
||||
_auxiliary_grid_update_throttle);
|
||||
src = src.Filter(filter_src, OrderSecurityHelper.root_pair_key_fn, _mk_selected_items_filter(_order_pnl_key_fn));
|
||||
break;
|
||||
case PNLHeaderRowDisplay.Total:
|
||||
src = src.Filter(_visible_items_filter_source(_order_pnl_key_fn), OrderSecurityHelper.root_pair_key_fn);
|
||||
break;
|
||||
}
|
||||
return _mk_orderbook_vm_common(
|
||||
filters, Enumerable.Empty<ColumnKeys>(), src);
|
||||
}
|
||||
|
||||
public OrderBookVM mk_orderbook_vm(PNLRowCollectionViewGroup group, ColumnKeys key) {
|
||||
var filters = new orderbook.FilterStates();
|
||||
_append_key_filters(filters, key);
|
||||
_append_group_filters(filters, group, anchors);
|
||||
return _mk_orderbook_vm_common(
|
||||
filters, anchors.WrapSubRange(0, group.level), _visible_orders_source());
|
||||
}
|
||||
|
||||
public OrderBookVM mk_orderbook_vm(PNLRowVM row, ColumnKeys key) {
|
||||
var filters = new orderbook.FilterStates();
|
||||
_append_key_filters(filters, key);
|
||||
_append_row_filters(filters, row);
|
||||
return _mk_orderbook_vm_common(
|
||||
filters, anchors.Concat(new[] { ColumnKeys.Account, ColumnKeys.Symbol, ColumnKeys.Security }).Distinct());
|
||||
}
|
||||
|
||||
public void set_all_expanded(bool is_expanded) =>
|
||||
_rows_view.set_all_expanded(is_expanded);
|
||||
|
||||
void IHandleRowSelection.row_selection_changed(SelectionChangedEventArgs e) {
|
||||
RowSelectionHelper.apply_changes(_selected_rows, e);
|
||||
// adding/subtracting pnl incrementally can lose precision over time, due to rounding errors.
|
||||
// do the full calculation on each selection change, as long as it doesn't hurt perf (which seems unlikely).
|
||||
selected_aggregate.data = _selected_rows.Aggregate(PNLNormalizedData.zero, (d, r) => d + r.normalized_data);
|
||||
_selection_changed?.Invoke(e);
|
||||
}
|
||||
|
||||
static IPNLColumnComparer _get_column_comparer(ColumnKeys column, ListSortDirection direction) {
|
||||
var comp = _get_column_comparer(column);
|
||||
if (direction == ListSortDirection.Descending)
|
||||
comp = new ReversePNLColumnComparer(comp);
|
||||
return comp;
|
||||
}
|
||||
|
||||
static IPNLColumnComparer _get_column_comparer(ColumnKeys column) {
|
||||
switch (column) {
|
||||
case ColumnKeys.Account: return wrap(r => r.account_name);
|
||||
case ColumnKeys.Exchange: return wrap(r => r.exchange);
|
||||
case ColumnKeys.SecurityType: return wrap(r => r.security?.ContractCase ?? SecurityType.None);
|
||||
case ColumnKeys.Symbol: return wrap(r => r.symbol);
|
||||
case ColumnKeys.Security: return wrap(r => r.security);
|
||||
case ColumnKeys.SecurityKey: return wrap(r => r.security_key);
|
||||
case ColumnKeys.CPP: return wrap(r => r.cpp);
|
||||
/// SOD
|
||||
case ColumnKeys.SODPNL: return wrap(r => r.sod_pnl);
|
||||
case ColumnKeys.SODLongPos: return wrap(r => r.sod_long_pos);
|
||||
case ColumnKeys.SODLongAvgPrice: return wrap(r => r.sod_long_avg_price);
|
||||
case ColumnKeys.SODShortPos: return wrap(r => r.sod_short_pos);
|
||||
case ColumnKeys.SODShortAvgPrice: return wrap(r => r.sod_short_avg_price);
|
||||
case ColumnKeys.SODNetPos: return wrap(r => r.sod_net_pos);
|
||||
/// Day
|
||||
case ColumnKeys.DayPNL: return wrap(r => r.day_pnl);
|
||||
case ColumnKeys.DayLongPos: return wrap(r => r.day_long_pos);
|
||||
case ColumnKeys.DayLongAvgPrice: return wrap(r => r.day_long_avg_price);
|
||||
case ColumnKeys.DayShortPos: return wrap(r => r.day_short_pos);
|
||||
case ColumnKeys.DayShortAvgPrice: return wrap(r => r.day_short_avg_price);
|
||||
case ColumnKeys.DayNetPos: return wrap(r => r.day_net_pos);
|
||||
case ColumnKeys.HasDayTrades: return wrap(r => r.has_day_trades);
|
||||
/// RawDay
|
||||
case ColumnKeys.RawDayLongPos: return wrap(r => r.raw_day_long_pos);
|
||||
case ColumnKeys.RawDayShortPos: return wrap(r => r.raw_day_short_pos);
|
||||
case ColumnKeys.RawDayNetPos: return wrap(r => r.raw_day_net_pos);
|
||||
/// Total
|
||||
case ColumnKeys.TotalPNL: return wrap(r => r.total_pnl);
|
||||
case ColumnKeys.TotalLongPos: return wrap(r => r.total_long_pos);
|
||||
case ColumnKeys.TotalLongAvgPrice: return wrap(r => r.total_long_avg_price);
|
||||
case ColumnKeys.TotalShortPos: return wrap(r => r.total_short_pos);
|
||||
case ColumnKeys.TotalShortAvgPrice: return wrap(r => r.total_short_avg_price);
|
||||
case ColumnKeys.TotalNetPos: return wrap(r => r.total_net_pos);
|
||||
/// Working
|
||||
case ColumnKeys.WorkingLongPos: return wrap(r => r.working_long_pos);
|
||||
case ColumnKeys.WorkingLongAvgPrice: return wrap(r => r.working_long_avg_price);
|
||||
case ColumnKeys.WorkingShortPos: return wrap(r => r.working_short_pos);
|
||||
case ColumnKeys.WorkingShortAvgPrice: return wrap(r => r.working_short_avg_price);
|
||||
case ColumnKeys.WorkingNetPos: return wrap(r => r.working_net_pos);
|
||||
/// Market
|
||||
case ColumnKeys.BidPrice: return wrap(r => r.bid_price);
|
||||
case ColumnKeys.MidPrice: return wrap(r => r.mid_price);
|
||||
case ColumnKeys.AskPrice: return wrap(r => r.ask_price);
|
||||
case ColumnKeys.LastPrice: return wrap(r => r.last_price);
|
||||
case ColumnKeys.SettlementPrice: return wrap(r => r.settlement_price);
|
||||
case ColumnKeys.PNLPrice: return wrap(r => r.pnl_price);
|
||||
default: throw ExceptionBuilder.Argument.unexpected_case(column, nameof(column));
|
||||
}
|
||||
PNLColumnComparer<T> wrap<T>(Func<PNLRowVM, T> selector) =>
|
||||
new PNLColumnComparer<T>(selector);
|
||||
}
|
||||
|
||||
static GridSettings _hide_cols(GridSettings grid, IEnumerable<ColumnKeys> columns) =>
|
||||
grid.with(_hide_cols(grid.columns, columns));
|
||||
|
||||
static IEnumerable<GridColumnSettings> _hide_cols(IEnumerable<GridColumnSettings> cols, IEnumerable<ColumnKeys> columns) {
|
||||
// hide columns who's data is implied by the grouping
|
||||
var col_hash = new HashSet<string>(columns.Select(k => k.ToString()));
|
||||
foreach (var c in cols)
|
||||
yield return col_hash.Contains(c.id) ? c.with(is_visible: false) : c;
|
||||
}
|
||||
|
||||
#region Fills Popup Utils
|
||||
static void _append_key_filters(fills.FillsFilterStates f, ColumnKeys key) {
|
||||
switch (key) {
|
||||
case ColumnKeys.SODNetPos:
|
||||
case ColumnKeys.SODLongPos:
|
||||
case ColumnKeys.SODShortPos:
|
||||
f.fill_kind = new[] { Fill.Types.FillKind.Sod };
|
||||
break;
|
||||
case ColumnKeys.DayNetPos:
|
||||
case ColumnKeys.DayLongPos:
|
||||
case ColumnKeys.DayShortPos:
|
||||
f.fill_kind = new[] { Fill.Types.FillKind.Standard, Fill.Types.FillKind.Leg };
|
||||
break;
|
||||
case ColumnKeys.RawDayNetPos:
|
||||
case ColumnKeys.RawDayLongPos:
|
||||
case ColumnKeys.RawDayShortPos:
|
||||
f.fill_kind = new[] { Fill.Types.FillKind.Standard };
|
||||
break;
|
||||
}
|
||||
switch (key) {
|
||||
case ColumnKeys.SODLongPos:
|
||||
case ColumnKeys.DayLongPos:
|
||||
case ColumnKeys.RawDayLongPos:
|
||||
case ColumnKeys.TotalLongPos:
|
||||
f.side = new[] { Side.Buy };
|
||||
break;
|
||||
case ColumnKeys.SODShortPos:
|
||||
case ColumnKeys.DayShortPos:
|
||||
case ColumnKeys.RawDayShortPos:
|
||||
case ColumnKeys.TotalShortPos:
|
||||
f.side = new[] { Side.Sell };
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void _append_group_filters(fills.FillsFilterStates f, PNLRowCollectionViewGroup group, IReadOnlyList<ColumnKeys> anchors) {
|
||||
if (group != null) {
|
||||
int anchor_pos = group.level - 1;
|
||||
if (anchor_pos < 0)
|
||||
throw new InvalidOperationException("Group level is less than 1.");
|
||||
if (anchor_pos >= anchors.Count)
|
||||
throw new InvalidOperationException("Group level is greater than the number of anchors.");
|
||||
_append_fills_filter(f, group, anchors[anchor_pos]);
|
||||
group = group.parent;
|
||||
while (anchor_pos-- > 0 && group != null) {
|
||||
_append_fills_filter(f, group, anchors[anchor_pos]);
|
||||
group = group.parent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void _append_fills_filter(fills.FillsFilterStates filter, PNLRowCollectionViewGroup group, ColumnKeys key) {
|
||||
switch (key) {
|
||||
case ColumnKeys.Account: filter.account = new[] { group.Name.ToString() }; break;
|
||||
case ColumnKeys.Symbol: filter.symbol = new[] { group.Name.ToString() }; break;
|
||||
case ColumnKeys.Exchange: filter.exchange = new[] { (Venue)group.Name }; break;
|
||||
case ColumnKeys.Security:
|
||||
if (group.Name is SecurityImmutable sec)
|
||||
filter.security = new SecurityCriteria(sec);
|
||||
break;
|
||||
default: throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
static void _append_row_filters(fills.FillsFilterStates f, PNLRowVM row) {
|
||||
f.account = new[] { row.account_name };
|
||||
f.security = SecurityCriteria.from_keys(new[] { row.key.security_id });
|
||||
}
|
||||
|
||||
static void _append_key_filters(orderbook.FilterStates f, ColumnKeys key) {
|
||||
switch (key) {
|
||||
case ColumnKeys.WorkingLongPos:
|
||||
case ColumnKeys.DayLongPos:
|
||||
case ColumnKeys.RawDayLongPos:
|
||||
case ColumnKeys.TotalLongPos:
|
||||
f.sides = new[] { Side.Buy };
|
||||
break;
|
||||
case ColumnKeys.WorkingShortPos:
|
||||
case ColumnKeys.DayShortPos:
|
||||
case ColumnKeys.RawDayShortPos:
|
||||
case ColumnKeys.TotalShortPos:
|
||||
f.sides = new[] { Side.Sell };
|
||||
break;
|
||||
}
|
||||
switch (key) {
|
||||
case ColumnKeys.WorkingLongPos:
|
||||
case ColumnKeys.WorkingShortPos:
|
||||
case ColumnKeys.WorkingNetPos:
|
||||
f.statuses = new[] { OrderStatus.PendingWorking, OrderStatus.Working };
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void _append_group_filters(orderbook.FilterStates f, PNLRowCollectionViewGroup group, IReadOnlyList<ColumnKeys> anchors) {
|
||||
if (group != null) {
|
||||
int anchor_pos = group.level - 1;
|
||||
if (anchor_pos < 0)
|
||||
throw new InvalidOperationException("Group level is less than 1.");
|
||||
if (anchor_pos >= anchors.Count)
|
||||
throw new InvalidOperationException("Group level is greater than the number of anchors.");
|
||||
_append_orderbook_filter(f, group, anchors[anchor_pos]);
|
||||
group = group.parent;
|
||||
while (anchor_pos-- > 0 && group != null) {
|
||||
_append_orderbook_filter(f, group, anchors[anchor_pos]);
|
||||
group = group.parent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void _append_row_filters(orderbook.FilterStates f, PNLRowVM row) {
|
||||
f.accounts = new[] { row.account_name };
|
||||
f.security_criteria = SecurityCriteria.from_keys(new[] { row.key.security_id });
|
||||
}
|
||||
|
||||
static void _append_orderbook_filter(orderbook.FilterStates filter, PNLRowCollectionViewGroup g, ColumnKeys key) {
|
||||
switch (key) {
|
||||
case ColumnKeys.Account: filter.accounts = new[] { g.Name.ToString() }; break;
|
||||
case ColumnKeys.Symbol: filter.symbols = new[] { g.Name.ToString() }; break;
|
||||
case ColumnKeys.Exchange: filter.exchanges = new[] { (Venue)g.Name }; break;
|
||||
case ColumnKeys.Security:
|
||||
if (g.Name is SecurityImmutable sec)
|
||||
filter.security_criteria = new SecurityCriteria(sec);
|
||||
break;
|
||||
default: throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
static Func<T, bool> _mk_items_filter<T>(IEnumerable<PNLKey> keys, Func<T, PNLKey> key_selector) {
|
||||
var key_set = new HashSet<PNLKey>(keys);
|
||||
return v => key_set.Contains(key_selector(v));
|
||||
}
|
||||
|
||||
Func<T, bool> _mk_selected_items_filter<T>(Func<T, PNLKey> key_selector) =>
|
||||
_mk_items_filter(_selected_rows.Select(r => r.key), key_selector);
|
||||
|
||||
OrderBookVM _mk_orderbook_vm_common(orderbook.FilterStates filters, IEnumerable<ColumnKeys> hide_counterparts, IObservable<TableUpdate<OrderSecurityRootPair, uint>> orders_source = null) {
|
||||
var state = new orderbook.OrderBookStartupData(filters, orderbook.OrderBookStartupData.default_mini_grid, use_defaults: false);
|
||||
state.orderbook_grid.general = _hide_cols(state.orderbook_grid.general, hide_counterparts);
|
||||
var settings_man = new WindowSettingsManager<orderbook.OrderBookGridSettings>.DisconnectedImpl(state.orderbook_grid);
|
||||
var obvm = 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;
|
||||
}
|
||||
|
||||
IObservable<TableUpdate<FillTuple, string>> _visible_fills_source() =>
|
||||
_orders_model.on_fill_tuples.Filter(_visible_items_filter_source(_fill_pnl_key_fn), FillTuple.key_fn);
|
||||
|
||||
IObservable<TableUpdate<OrderSecurityRootPair, uint>> _visible_orders_source() =>
|
||||
_orders_model.on_order_tuples_with_root_and_piq.Filter(_visible_items_filter_source(_order_pnl_key_fn), OrderSecurityHelper.root_pair_key_fn);
|
||||
|
||||
IObservable<Func<T, bool>> _visible_items_filter_source<T>(Func<T, PNLKey> key_selector) =>
|
||||
Observable.Create<Func<T, bool>>(o => {
|
||||
o.OnNext(_visible_items_filter_snapshot(key_selector));
|
||||
_updated += on_updated;
|
||||
return Disposable.Create(() => _updated -= on_updated);
|
||||
void on_updated(TableUpdate<PNLRowData, PNLKey> _) =>
|
||||
o.OnNext(_visible_items_filter_snapshot(key_selector));
|
||||
});
|
||||
|
||||
// returns a predicate that takes a snapshot of the current state, and returns true if the item's account/security pair is visible in the snapshot state.
|
||||
// subsequent updates are not reflected in this predicate.
|
||||
// Note: this is a necessary behavior if the predicate is used in a TableUpdate filter stream (it must always return the same value for the same item).
|
||||
Func<T, bool> _visible_items_filter_snapshot<T>(Func<T, PNLKey> key_selector) {
|
||||
var t = _last_table;
|
||||
return o => t.state.ContainsKey(key_selector(o));
|
||||
}
|
||||
|
||||
void _update((TableUpdate<PNLRowData, PNLKey> filtered, TableUpdate<PNLRowData, PNLKey> raw) tup) {
|
||||
if (_is_paused) {
|
||||
_resume = tup;
|
||||
return;
|
||||
}
|
||||
var (filtered, raw) = tup;
|
||||
_last_table = filtered;
|
||||
int old_total_row_count = total_row_count;
|
||||
int old_filtered_row_count = filtered_row_count;
|
||||
var total_pnl = total_aggregate.data;
|
||||
// don't adjust selected_pnl incrementally - it may be possible for the selection to change throughout the update, making incremental calculations tricky
|
||||
foreach (var v in filtered.removed) {
|
||||
int i = CollectionHelper.binary_search(_rows_data_wrapper, v, PNLRowData.key_comparer);
|
||||
Log.assert(i >= 0, $"{typeof(PNLVM)} received an update with a removed item that was not found via binary search.");
|
||||
if (i >= 0) {
|
||||
total_pnl -= rows[i].normalized_data;
|
||||
rows.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
foreach (var v in filtered.added) {
|
||||
var r = new PNLRowVM(v.with(calc_type: calculation_type), _default_appearances, _triggers);
|
||||
rows.InsertSorted(r, PNLRowVM.key_comparer);
|
||||
total_pnl += r.normalized_data;
|
||||
}
|
||||
foreach (var d in filtered.changed) {
|
||||
int i = CollectionHelper.binary_search(_rows_data_wrapper, d, PNLRowData.key_comparer);
|
||||
if (i >= 0) {
|
||||
var r = rows[i];
|
||||
total_pnl -= r.normalized_data;
|
||||
r.update(d);
|
||||
total_pnl += r.normalized_data;
|
||||
}
|
||||
}
|
||||
if (_selected_rows.Count > 0) {
|
||||
selected_aggregate.data = _selected_rows
|
||||
.Aggregate(PNLNormalizedData.zero, (d, r) => d + r.normalized_data);
|
||||
}
|
||||
//
|
||||
total_aggregate.data = total_pnl;
|
||||
total_row_count = tup.raw.state.Count;
|
||||
if (total_row_count != old_total_row_count)
|
||||
RaisePropertyChanged(nameof(total_row_count));
|
||||
if (filtered_row_count != old_filtered_row_count)
|
||||
RaisePropertyChanged(nameof(filtered_row_count));
|
||||
_updated?.Invoke(filtered);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using bts.collections;
|
||||
using mbts.ui.utility;
|
||||
using System.Collections.Generic;
|
||||
using System.Dynamic;
|
||||
|
||||
namespace bts.shell.pnl {
|
||||
class AnchorIndex {
|
||||
readonly IDictionary<PNLView.Columns, bool> _wrapped_anchors;
|
||||
|
||||
public AnchorIndex() {
|
||||
IDictionary<string, object> anchors_dict = anchors = new ExpandoObject();
|
||||
_wrapped_anchors = MappedDictionary.create_mutable(
|
||||
anchors_dict,
|
||||
EnumHelper.parse<PNLView.Columns>,
|
||||
key => key.ToString(),
|
||||
obj => (bool)obj,
|
||||
_ => _);
|
||||
// initialize the dictionary with every key (the keys should never change)
|
||||
foreach (var k in _keys)
|
||||
_wrapped_anchors[k] = false;
|
||||
}
|
||||
|
||||
public dynamic anchors { get; }
|
||||
|
||||
IReadOnlyList<PNLView.Columns> _keys => EnumHelper.get_values<PNLView.Columns>();
|
||||
|
||||
public void reset(IEnumerable<PNLView.Columns> anchor_list) {
|
||||
var anchor_set = new HashSet<PNLView.Columns>(anchor_list.empty_if_null());
|
||||
foreach (var key in _keys)
|
||||
_wrapped_anchors[key] = anchor_set.Contains(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<HeaderedContentControl
|
||||
x:Class="bts.shell.pnl.PNLAuxiliaryPopupView"
|
||||
x:ClassModifier="internal"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:pnl="clr-namespace:bts.shell.pnl"
|
||||
xmlns:u="clr-namespace:mbts.ui"
|
||||
xmlns:usp="clr-namespace:mbts.ui.spark"
|
||||
xmlns:sp="clr-namespace:mbts.spark"
|
||||
Background="{StaticResource CommonBackground}"
|
||||
BorderBrush="Black"
|
||||
BorderThickness="1"
|
||||
MaxHeight="300"
|
||||
>
|
||||
<HeaderedContentControl.Resources>
|
||||
<!--Fills-->
|
||||
<DataTemplate DataType="{x:Type sp:FillsVM}">
|
||||
<usp:FillsView u:Behavior.FocusOnLoaded="True"/>
|
||||
</DataTemplate>
|
||||
<!--OrderBook-->
|
||||
<DataTemplate DataType="{x:Type sp:OrderBookVM}">
|
||||
<sp:MiniOrderBookView u:Behavior.FocusOnLoaded="True"/>
|
||||
</DataTemplate>
|
||||
</HeaderedContentControl.Resources>
|
||||
<!--Header (Title Bar)-->
|
||||
<HeaderedContentControl.HeaderTemplate>
|
||||
<DataTemplate>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock VerticalAlignment="Center" Margin="4,0,0,0" FontWeight="Bold" Text="{Binding context}"/>
|
||||
<TextBlock Grid.Column="1" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="4,0,0,0" FontWeight="Bold" Text="{Binding title}">
|
||||
<TextBlock.Foreground>
|
||||
<SolidColorBrush Color="{Binding title_foreground}"/>
|
||||
</TextBlock.Foreground>
|
||||
</TextBlock>
|
||||
<CheckBox Grid.Column="2" VerticalAlignment="Center" Margin="4,0,0,0" Focusable="False" IsChecked="{Binding keep_open}">Keep Open</CheckBox>
|
||||
<Button Grid.Column="3" HorizontalAlignment="Right" Margin="4" Command="{x:Static ApplicationCommands.Close}" CommandTarget="{Binding RelativeSource={RelativeSource AncestorType=pnl:PNLAuxiliaryPopupView}}" Style="{StaticResource RoundCloseButton}"/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</HeaderedContentControl.HeaderTemplate>
|
||||
<!--Input Bindings-->
|
||||
<HeaderedContentControl.InputBindings>
|
||||
<KeyBinding Key="Esc" Command="{x:Static ApplicationCommands.Close}"/>
|
||||
</HeaderedContentControl.InputBindings>
|
||||
<!--Template-->
|
||||
<HeaderedContentControl.Template>
|
||||
<ControlTemplate TargetType="HeaderedContentControl">
|
||||
<Border
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
>
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
<ContentPresenter ContentSource="Header"/>
|
||||
<ContentPresenter Grid.Row="1" Grid.ColumnSpan="4"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</HeaderedContentControl.Template>
|
||||
</HeaderedContentControl>
|
||||
@@ -0,0 +1,8 @@
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace bts.shell.pnl {
|
||||
partial class PNLAuxiliaryPopupView : HeaderedContentControl {
|
||||
public PNLAuxiliaryPopupView() =>
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
namespace bts.shell.pnl {
|
||||
readonly struct PNLCell {
|
||||
public readonly object item;
|
||||
public readonly PNLView.Columns column;
|
||||
|
||||
public PNLCell(object item, PNLView.Columns column) {
|
||||
this.item = item;
|
||||
this.column = column;
|
||||
}
|
||||
|
||||
public override string ToString() =>
|
||||
$"Item: {item}, Column: {column}";
|
||||
}
|
||||
|
||||
readonly struct PNLRowCell {
|
||||
public readonly PNLRowVM row;
|
||||
public readonly PNLView.Columns column;
|
||||
|
||||
public PNLRowCell(PNLRowVM row, PNLView.Columns column) {
|
||||
this.row = row;
|
||||
this.column = column;
|
||||
}
|
||||
}
|
||||
|
||||
readonly struct PNLGroupCell {
|
||||
public readonly PNLRowCollectionViewGroup group;
|
||||
public readonly PNLView.Columns column;
|
||||
|
||||
public PNLGroupCell(PNLRowCollectionViewGroup group, PNLView.Columns column) {
|
||||
this.group = group;
|
||||
this.column = column;
|
||||
}
|
||||
}
|
||||
|
||||
readonly struct PNLHeaderRowCell {
|
||||
public readonly PNLHeaderRowDisplay header_row;
|
||||
public readonly PNLView.Columns column;
|
||||
|
||||
public PNLHeaderRowCell(PNLHeaderRowDisplay header_row, PNLView.Columns column) {
|
||||
this.header_row = header_row;
|
||||
this.column = column;
|
||||
}
|
||||
}
|
||||
|
||||
static class PNLCellExtensions {
|
||||
public static OneOf<PNLRowCell, PNLGroupCell, PNLHeaderRowCell> oneof(this PNLCell cell) =>
|
||||
OneOf<PNLRowVM, PNLRowCollectionViewGroup, PNLHeaderRowDisplay>.infer(cell.item).Extend(
|
||||
r => new PNLRowCell(r, cell.column),
|
||||
g => new PNLGroupCell(g, cell.column),
|
||||
h => new PNLHeaderRowCell(h, cell.column));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
using bts.utils;
|
||||
using mbts.ui.controls;
|
||||
using mbts.ui.shell;
|
||||
using mbts.ui.utility.text;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using FontFamily = System.Windows.Media.FontFamily;
|
||||
|
||||
namespace bts.shell.pnl {
|
||||
class PNLColumn : DIYGrid.ColumnBase<PNLView.Columns>, IRaiseNotifyPropertyChanged {
|
||||
static readonly IPropertyChangedDetector<PNLColumn>
|
||||
_group_settings_change_detector = ObjectSnapshot.build_detector<PNLColumn>(
|
||||
nameof(group_font_weight_override),
|
||||
nameof(group_font_weight),
|
||||
nameof(group_font_color_override),
|
||||
nameof(group_font_color),
|
||||
nameof(group_font_family),
|
||||
nameof(group_font_size)),
|
||||
_header_settings_change_detector = ObjectSnapshot.build_detector<PNLColumn>(
|
||||
nameof(header_font_weight_override),
|
||||
nameof(header_font_weight),
|
||||
nameof(header_font_color_override),
|
||||
nameof(header_font_color),
|
||||
nameof(header_font_family),
|
||||
nameof(header_font_size));
|
||||
|
||||
double _group_font_size = 11, _header_font_size = 12;
|
||||
int? _anchor_index;
|
||||
FontFamily
|
||||
_group_font_family = TextUtils.StandardFontFamily,
|
||||
_header_font_family = TextUtils.StandardFontFamily;
|
||||
|
||||
public PNLColumn() {
|
||||
// default to be hidden, so columns are not rendered until their settings are loaded,
|
||||
// in which case, they will only be rendered if 'settings.is_visible' is true.
|
||||
is_visible = false;
|
||||
}
|
||||
|
||||
public int? anchor_index {
|
||||
get => _anchor_index;
|
||||
set => set(ref _anchor_index, value);
|
||||
}
|
||||
// group
|
||||
public FontWeight? group_font_weight_override { get; private set; }
|
||||
public Color? group_font_color_override { get; private set; }
|
||||
public FontFamily group_font_family => _group_font_family ?? font_family;
|
||||
public double group_font_size => double.IsNaN(_group_font_size) || _group_font_size == 0 ? font_size : _group_font_size;
|
||||
public FontWeight group_font_weight => group_font_weight_override ?? font_weight;
|
||||
public Color group_font_color => group_font_color_override ?? font_color;
|
||||
|
||||
public FontWeight? header_font_weight_override { get; private set; }
|
||||
public Color? header_font_color_override { get; private set; }
|
||||
public FontFamily header_font_family => _header_font_family ?? font_family;
|
||||
public double header_font_size => double.IsNaN(_header_font_size) || _header_font_size == 0 ? font_size : _header_font_size;
|
||||
public FontWeight header_font_weight => header_font_weight_override ?? font_weight;
|
||||
public Color header_font_color => header_font_color_override ?? font_color;
|
||||
|
||||
public void load_group_params(
|
||||
double group_font_size = double.NaN, FontWeight? group_font_weight = null, Color? group_font_color = null, FontFamily group_font_family = null) {
|
||||
using (_group_settings_change_detector.detect_changes(this)) {
|
||||
_group_font_size = group_font_size;
|
||||
_group_font_family = group_font_family;
|
||||
group_font_weight_override = group_font_weight;
|
||||
group_font_color_override = group_font_color;
|
||||
}
|
||||
}
|
||||
|
||||
public void load_header_params(
|
||||
double header_font_size = double.NaN, FontWeight? header_font_weight = null, Color? header_font_color = null, FontFamily header_font_family = null) {
|
||||
using (_header_settings_change_detector.detect_changes(this)) {
|
||||
_header_font_size = header_font_size;
|
||||
_header_font_family = header_font_family;
|
||||
header_font_weight_override = header_font_weight;
|
||||
header_font_color_override = header_font_color;
|
||||
}
|
||||
}
|
||||
|
||||
void IRaiseNotifyPropertyChanged.raise_notify_property_changed(string prop) =>
|
||||
raise_prop_changed(prop);
|
||||
|
||||
protected override void on_key_initialized(PNLView.Columns key) =>
|
||||
id = key.ToString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,467 @@
|
||||
using bts.collections;
|
||||
using bts.utils;
|
||||
using mbts;
|
||||
using mbts.ui;
|
||||
using mbts.ui.shell;
|
||||
using mbts.ui.utility;
|
||||
using mbts.utils;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Specialized;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Windows.Data;
|
||||
using CellAnimation = mbts.ui.spark.CellAnimation;
|
||||
using ColumnKeys = bts.shell.pnl.PNLView.Columns;
|
||||
|
||||
namespace bts.shell.pnl {
|
||||
class PNLRowCollectionView : ListCollectionView {
|
||||
ReadOnlyObservableCollection<object> _base_groups, _groups_wrapper;
|
||||
ObservableAutoWrapper<object, object> _groups;
|
||||
// appearance/triggers
|
||||
ImmutableDictionary<ColumnKeys, PNLCellAppearance> _defaults_appearances;
|
||||
IReadOnlyList<IPNLAnimationTrigger<PNLNormalizedData>> _triggers;
|
||||
|
||||
public PNLRowCollectionView(IList list, ImmutableDictionary<ColumnKeys, PNLCellAppearance> default_appearances, IReadOnlyList<IPNLAnimationTrigger<PNLNormalizedData>> triggers) : base(list) {
|
||||
_defaults_appearances = default_appearances;
|
||||
_triggers = triggers;
|
||||
}
|
||||
|
||||
public override bool CanFilter => true;
|
||||
public override bool CanGroup => true;
|
||||
|
||||
// Must return null if there are no GroupDescriptions (i.e., instead of an empty collection).
|
||||
// If an empty collection is returned, the view assumes grouping is active and there are no items.
|
||||
public override ReadOnlyObservableCollection<object> Groups {
|
||||
get {
|
||||
if ((GroupDescriptions?.Count ?? 0) == 0)
|
||||
return null;
|
||||
if (_groups_wrapper == null) {
|
||||
_base_groups = base.Groups;
|
||||
if (_base_groups == null)
|
||||
return null;
|
||||
_groups = new ObservableAutoWrapper<object, object>(_base_groups, obj => new PNLRowCollectionViewGroup((CollectionViewGroup)obj, this, _defaults_appearances, _triggers));
|
||||
_groups_wrapper = new ReadOnlyObservableCollection<object>(_groups);
|
||||
}
|
||||
return _groups_wrapper;
|
||||
}
|
||||
}
|
||||
|
||||
public void on_settings(ImmutableDictionary<ColumnKeys, PNLCellAppearance> default_appearances, IReadOnlyList<IPNLAnimationTrigger<PNLNormalizedData>> triggers) {
|
||||
_defaults_appearances = default_appearances;
|
||||
_triggers = triggers;
|
||||
foreach (var g in _groups_wrapper.OfTypeSafe<PNLRowCollectionViewGroup>())
|
||||
g.on_settings(default_appearances, triggers);
|
||||
}
|
||||
|
||||
public void set_all_expanded(bool is_expanded) {
|
||||
foreach (var g in Groups.OfTypeSafe<PNLRowCollectionViewGroup>())
|
||||
g.set_all_expanded(is_expanded);
|
||||
}
|
||||
}
|
||||
|
||||
class PNLRowCollectionViewGroup : CollectionViewGroup, IRaiseNotifyPropertyChanged, IEquatable<PNLRowCollectionViewGroup> {
|
||||
readonly PNLRowCollectionView _root;
|
||||
readonly CollectionViewGroup _from;
|
||||
readonly Dictionary<object, InnerGroupSubscription> _inner_groups = new Dictionary<object, InnerGroupSubscription>();
|
||||
readonly Dictionary<PNLKey, IDisposable> _rows = new Dictionary<PNLKey, IDisposable>();
|
||||
readonly VersionContainer<bool?> _is_all_selected;
|
||||
readonly DeferrableActionWrapper _refresh_is_all_selected;
|
||||
readonly HashSet<PNLRowVM> _selected_rows = new HashSet<PNLRowVM>();
|
||||
readonly PNLCellIndexBuilder<PNLNormalizedData> _cell_map = new PNLCellIndexBuilder<PNLNormalizedData>();
|
||||
readonly PNLCellEmbeddedAnimationModel<PNLNormalizedData> _animator;
|
||||
|
||||
ImmutableDictionary<ColumnKeys, PNLCellAppearance> _defaults_appearances;
|
||||
IReadOnlyList<IPNLAnimationTrigger<PNLNormalizedData>> _triggers;
|
||||
IDisposable _dispose;
|
||||
bool _is_expanded = true;
|
||||
int _selected_item_count_backing;
|
||||
PNLNormalizedData _data;
|
||||
|
||||
public PNLRowCollectionViewGroup(CollectionViewGroup from, PNLRowCollectionView root, ImmutableDictionary<ColumnKeys, PNLCellAppearance> default_appearances, IReadOnlyList<IPNLAnimationTrigger<PNLNormalizedData>> triggers) : base(from.Name) {
|
||||
_root = root;
|
||||
_from = from;
|
||||
_defaults_appearances = default_appearances;
|
||||
_triggers = triggers;
|
||||
_animator = new PNLCellEmbeddedAnimationModel<PNLNormalizedData>(default_appearances, triggers);
|
||||
_animator.on_row(data);
|
||||
data_changed += () => {
|
||||
_cell_map.on_row(data);
|
||||
_animator.on_row(data);
|
||||
_cell_map.set_appearances(_animator.cell_appearances);
|
||||
};
|
||||
//sod_net_pos
|
||||
_is_all_selected = new VersionContainer<bool?>(() => {
|
||||
if (_selected_item_count == 0)
|
||||
return false;
|
||||
if (_selected_item_count == ProtectedItemCount)
|
||||
return true;
|
||||
return null;
|
||||
});
|
||||
bool? last_published_all_selected = _is_all_selected.value;
|
||||
_refresh_is_all_selected = new DeferrableActionWrapper(() => {
|
||||
if (_is_all_selected.value != last_published_all_selected) {
|
||||
last_published_all_selected = is_all_selected;
|
||||
OnPropertyChanged(new PropertyChangedEventArgs(nameof(is_all_selected)));
|
||||
}
|
||||
});
|
||||
// sod
|
||||
sod_pnl = _cell_map.add_pnl(ColumnKeys.SODPNL, d => d.sod_pnl);
|
||||
sod_net_pos = _cell_map.add_net_pos(ColumnKeys.SODNetPos, d => d.sod_net_pos);
|
||||
sod_long_pos = _cell_map.add_long_pos(ColumnKeys.SODLongPos, d => d.sod_long_pos);
|
||||
sod_short_pos = _cell_map.add_short_pos(ColumnKeys.SODShortPos, d => d.sod_short_pos);
|
||||
// day
|
||||
day_pnl = _cell_map.add_pnl(ColumnKeys.DayPNL, d => d.day_pnl);
|
||||
day_net_pos = _cell_map.add_net_pos(ColumnKeys.DayNetPos, d => d.day_net_pos);
|
||||
day_long_pos = _cell_map.add_long_pos(ColumnKeys.DayLongPos, d => d.day_long_pos);
|
||||
day_short_pos = _cell_map.add_short_pos(ColumnKeys.DayShortPos, d => d.day_short_pos);
|
||||
// raw day
|
||||
raw_day_net_pos = _cell_map.add_net_pos(ColumnKeys.RawDayNetPos, d => d.raw_day_net_pos);
|
||||
raw_day_long_pos = _cell_map.add_long_pos(ColumnKeys.RawDayLongPos, d => d.raw_day_long_pos);
|
||||
raw_day_short_pos = _cell_map.add_short_pos(ColumnKeys.RawDayShortPos, d => d.raw_day_short_pos);
|
||||
// total
|
||||
total_pnl = _cell_map.add(ColumnKeys.TotalPNL, d => d.total_pnl, FormattingDelegates.Price<decimal>.Value);
|
||||
total_net_pos = _cell_map.add(ColumnKeys.TotalNetPos, d => d.total_net_pos);
|
||||
total_long_pos = _cell_map.add(ColumnKeys.TotalLongPos, d => d.total_long_pos);
|
||||
total_short_pos = _cell_map.add(ColumnKeys.TotalShortPos, d => d.total_short_pos);
|
||||
// working
|
||||
working_net_pos = _cell_map.add(ColumnKeys.WorkingNetPos, d => d.working_net_pos);
|
||||
working_long_pos = _cell_map.add(ColumnKeys.WorkingLongPos, d => d.working_long_pos);
|
||||
working_short_pos = _cell_map.add(ColumnKeys.WorkingShortPos, d => d.working_short_pos);
|
||||
|
||||
_cell_map.on_row(data);
|
||||
_cell_map.set_appearances(_animator.cell_appearances);
|
||||
((INotifyCollectionChanged)_from.Items).CollectionChanged += _from_items_CollectionChanged;
|
||||
_from_items_CollectionChanged(_from.Items, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, _from.Items, 0));
|
||||
|
||||
_dispose = _cell_map.animation_model.OnDispatcher().Subscribe(new_animations => {
|
||||
animations = new_animations.Select(kv => new CellAnimation(kv.Key.column, kv.Key.type, kv.Value.color.to_brush())).ToList();
|
||||
OnPropertyChanged(new PropertyChangedEventArgs(nameof(animations)));
|
||||
});
|
||||
}
|
||||
|
||||
public IList<CellAnimation> animations { get; private set; }
|
||||
|
||||
public event Action
|
||||
data_changed,
|
||||
ProtectedItemCountChanged,
|
||||
selected_item_count_changed;
|
||||
|
||||
public PNLNormalizedData data {
|
||||
get => _data;
|
||||
set {
|
||||
if (_data != value) {
|
||||
_data = value;
|
||||
data_changed?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sod
|
||||
public PNLCellVM<PNLNormalizedData, decimal> sod_pnl { get; }
|
||||
public PNLCellVM<PNLNormalizedData, decimal> sod_net_pos { get; }
|
||||
public PNLCellVM<PNLNormalizedData, decimal> sod_long_pos { get; }
|
||||
public PNLCellVM<PNLNormalizedData, decimal> sod_short_pos { get; }
|
||||
// day
|
||||
public PNLCellVM<PNLNormalizedData, decimal> day_pnl { get; }
|
||||
public PNLCellVM<PNLNormalizedData, decimal> day_net_pos { get; }
|
||||
public PNLCellVM<PNLNormalizedData, decimal> day_long_pos { get; }
|
||||
public PNLCellVM<PNLNormalizedData, decimal> day_short_pos { get; }
|
||||
// raw day
|
||||
public PNLCellVM<PNLNormalizedData, decimal> raw_day_net_pos { get; }
|
||||
public PNLCellVM<PNLNormalizedData, decimal> raw_day_long_pos { get; }
|
||||
public PNLCellVM<PNLNormalizedData, decimal> raw_day_short_pos { get; }
|
||||
// total
|
||||
public PNLCellVM<PNLNormalizedData, decimal> total_pnl { get; }
|
||||
public PNLCellVM<PNLNormalizedData, decimal> total_net_pos { get; }
|
||||
public PNLCellVM<PNLNormalizedData, decimal> total_long_pos { get; }
|
||||
public PNLCellVM<PNLNormalizedData, decimal> total_short_pos { get; }
|
||||
// working
|
||||
public PNLCellVM<PNLNormalizedData, decimal> working_net_pos { get; }
|
||||
public PNLCellVM<PNLNormalizedData, decimal> working_long_pos { get; }
|
||||
public PNLCellVM<PNLNormalizedData, decimal> working_short_pos { get; }
|
||||
//
|
||||
public PNLRowCollectionViewGroup parent { get; private set; }
|
||||
public int level => parent == null ? 1 : parent.level + 1;
|
||||
public override bool IsBottomLevel => _from.IsBottomLevel;
|
||||
|
||||
public IEnumerable<PNLRowCollectionViewGroup> ancestors {
|
||||
get {
|
||||
if (parent != null) {
|
||||
var p = parent;
|
||||
do {
|
||||
yield return p;
|
||||
p = p.parent;
|
||||
} while (p != null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public IReadOnlyList<object> GroupNamePath {
|
||||
get {
|
||||
var path = new List<object> { Name };
|
||||
foreach (var a in ancestors)
|
||||
path.Add(a.Name);
|
||||
return path.WrapReverseRO();
|
||||
}
|
||||
}
|
||||
|
||||
public bool is_expanded { get => _is_expanded; set => _set(ref _is_expanded, value); }
|
||||
public bool? is_all_selected {
|
||||
get => _is_all_selected.value;
|
||||
set {
|
||||
// The setter should not be called while is_all_selected deferred.
|
||||
// However, it is good to be defensive, so let's add a conditional check anyways:
|
||||
// if 'is_all_selected' is deferred, the value must be blindly propagated,
|
||||
// bc the equality comparison is erroneous, as the current state is unknown.
|
||||
if (value.HasValue && value != is_all_selected) {
|
||||
using (_refresh_is_all_selected.defer_execution()) {
|
||||
foreach (var i in Items) {
|
||||
if (i is PNLRowCollectionViewGroup g)
|
||||
g.is_all_selected = value;
|
||||
else if (i is PNLRowVM r)
|
||||
r.is_selected = value.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
new int ProtectedItemCount {
|
||||
get => base.ProtectedItemCount;
|
||||
set {
|
||||
if (base.ProtectedItemCount != value) {
|
||||
base.ProtectedItemCount = value;
|
||||
_is_all_selected.increment_version();
|
||||
ProtectedItemCountChanged?.Invoke();
|
||||
_refresh_is_all_selected.execute();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int _selected_item_count {
|
||||
get => _selected_item_count_backing;
|
||||
set {
|
||||
Log.assert(value >= 0, $"{nameof(_selected_item_count)} cannot be negative.");
|
||||
_selected_item_count_backing = value;
|
||||
_is_all_selected.increment_version();
|
||||
_refresh_is_all_selected.execute();
|
||||
selected_item_count_changed?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
public void on_settings(ImmutableDictionary<ColumnKeys, PNLCellAppearance> default_appearances, IReadOnlyList<IPNLAnimationTrigger<PNLNormalizedData>> triggers) {
|
||||
_defaults_appearances = default_appearances;
|
||||
_triggers = triggers;
|
||||
_animator.on_settings(default_appearances, triggers);
|
||||
_cell_map.set_appearances(_animator.cell_appearances);
|
||||
foreach (var g in ProtectedItems.OfTypeSafe<PNLRowCollectionViewGroup>())
|
||||
g.on_settings(default_appearances, triggers);
|
||||
}
|
||||
|
||||
public void set_all_expanded(bool is_expanded) {
|
||||
this.is_expanded = is_expanded;
|
||||
foreach (var g in Items.OfTypeSafe<PNLRowCollectionViewGroup>())
|
||||
g.set_all_expanded(is_expanded);
|
||||
}
|
||||
|
||||
public bool Equals(PNLRowCollectionViewGroup other) =>
|
||||
other != null &&
|
||||
level == other.level &&
|
||||
Equals(Name, other.Name);
|
||||
|
||||
void _select_all() {
|
||||
foreach (var cvg in Items.OfType<PNLRowCollectionViewGroup>())
|
||||
cvg._select_all();
|
||||
}
|
||||
|
||||
IDisposable _detect_is_all_selected_changed() {
|
||||
bool? was_all_selected = is_all_selected;
|
||||
return Disposable.Create(() => {
|
||||
if (is_all_selected != was_all_selected)
|
||||
OnPropertyChanged(new PropertyChangedEventArgs(nameof(is_all_selected)));
|
||||
});
|
||||
}
|
||||
|
||||
void _add_item(int index, object item) {
|
||||
Log.assert(index >= 0, "Index is less than 0.");
|
||||
if (item is CollectionViewGroup group) {
|
||||
var inner_group = new PNLRowCollectionViewGroup(group, _root, _defaults_appearances, _triggers);
|
||||
_inner_groups.Add(group.Name, new InnerGroupSubscription(this, inner_group));
|
||||
ProtectedItems.Insert(index, inner_group);
|
||||
} else {
|
||||
var row = (PNLRowVM)item;
|
||||
ProtectedItems.Insert(index, item);
|
||||
_rows.Add(row.key, _connect_row(row));
|
||||
}
|
||||
}
|
||||
|
||||
void _remove_item(int index, object item) {
|
||||
if (item is PNLRowCollectionViewGroup inner_group) {
|
||||
if (!_inner_groups.TryPopValue(inner_group.Name, out var ig_sub))
|
||||
throw new InvalidOperationException($"Inner groups cache does not have an entry for the specified name '{inner_group.Name}' (entire tree structure may be compromised).");
|
||||
ig_sub.Dispose();
|
||||
Disposable.dispose(ref inner_group._dispose);
|
||||
} else {
|
||||
var row = (PNLRowVM)item;
|
||||
if (!_rows.TryPopValue(row.key, out var row_sub))
|
||||
throw new InvalidOperationException($"Inner rows cache does not have an entry for the specified irowstem key '{row.key}' (entire tree structure may be compromised).");
|
||||
row_sub.Dispose();
|
||||
}
|
||||
ProtectedItems.RemoveAt(index);
|
||||
}
|
||||
|
||||
void _from_items_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) {
|
||||
using (_refresh_is_all_selected.defer_execution()) {
|
||||
switch (e.Action) {
|
||||
case NotifyCollectionChangedAction.Add:
|
||||
for (int i = 0; i < e.NewItems.Count; i++)
|
||||
_add_item(e.NewStartingIndex + i, e.NewItems[i]);
|
||||
break;
|
||||
case NotifyCollectionChangedAction.Remove:
|
||||
for (int i = 0; i < e.OldItems.Count; i++)
|
||||
_remove_item(e.OldStartingIndex, ProtectedItems[e.OldStartingIndex]);
|
||||
break;
|
||||
case NotifyCollectionChangedAction.Replace:
|
||||
for (int i = 0; i < e.OldItems.Count; i++)
|
||||
_remove_item(e.OldStartingIndex, ProtectedItems[e.OldStartingIndex]);
|
||||
for (int i = 0; i < e.NewItems.Count; i++)
|
||||
_add_item(e.NewStartingIndex + i, e.NewItems[i]);
|
||||
break;
|
||||
case NotifyCollectionChangedAction.Reset:
|
||||
while (ProtectedItems.Count > 0)
|
||||
_remove_item(0, ProtectedItems[0]);
|
||||
for (int i = 0; i < _from.Items.Count; i++)
|
||||
_add_item(i, _from.Items[i]);
|
||||
break;
|
||||
}
|
||||
_is_all_selected.increment_version();
|
||||
_refresh_is_all_selected.execute();
|
||||
}
|
||||
}
|
||||
|
||||
IDisposable _connect_row(PNLRowVM row) {
|
||||
using (_detect_is_all_selected_changed()) {
|
||||
var last_data = PNLNormalizedData.zero;
|
||||
on_updated(row.normalized_data);
|
||||
row.updated += on_updated;
|
||||
row.is_selected_changed += on_is_selected_changed;
|
||||
if (row.is_selected) {
|
||||
Log.assert(_selected_rows.Add(row), $"Selected Rows out of sync ({Name}): hash set already contained selected row that was just added to group ({row}).");
|
||||
_selected_item_count++;
|
||||
}
|
||||
ProtectedItemCount++;
|
||||
return Disposable.Create(() => {
|
||||
row.updated -= on_updated;
|
||||
row.is_selected_changed -= on_is_selected_changed;
|
||||
using (_refresh_is_all_selected.defer_execution()) {
|
||||
if (row.is_selected) {
|
||||
Log.assert(_selected_rows.Remove(row), $"Selected Rows out of sync ({Name}): hash set did not contain selected row that was just removed from group ({row}).");
|
||||
_selected_item_count--;
|
||||
}
|
||||
var newval = data - row.normalized_data;
|
||||
data = newval;
|
||||
ProtectedItemCount--;
|
||||
}
|
||||
});
|
||||
void on_updated(PNLNormalizedData new_data) {
|
||||
var less_last = data - last_data;
|
||||
var plus_new = less_last + new_data;
|
||||
data += new_data - last_data;
|
||||
if (plus_new != data) {
|
||||
Log.Ui.warn($"Detected PNL arithmetic rounding error in the group sum for '{string.Join(" ", GroupNamePath)}' (this bug is under investigation).");
|
||||
}
|
||||
last_data = new_data;
|
||||
}
|
||||
void on_is_selected_changed(bool is_selected) {
|
||||
if (is_selected) {
|
||||
Log.assert(_selected_rows.Add(row), $"Selected Rows out of sync ({Name}): hash set already contained row that was just selected ({row}).");
|
||||
_selected_item_count++;
|
||||
} else {
|
||||
Log.assert(_selected_rows.Remove(row), $"Selected Rows out of sync ({Name}): hash set did not contain row that was just unselected ({row}).");
|
||||
_selected_item_count--;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _set<T>(ref T backing, T new_value, [CallerMemberName]string prop = "") =>
|
||||
_set(ref backing, new_value, EqualityComparer<T>.Default, prop);
|
||||
|
||||
void _set<T>(ref T backing, T new_value, IEqualityComparer<T> comparer, [CallerMemberName]string prop = "") {
|
||||
if (!comparer.Equals(backing, new_value)) {
|
||||
backing = new_value;
|
||||
OnPropertyChanged(new PropertyChangedEventArgs(prop));
|
||||
}
|
||||
}
|
||||
|
||||
void IRaiseNotifyPropertyChanged.raise_notify_property_changed(string prop) =>
|
||||
OnPropertyChanged(new PropertyChangedEventArgs(prop));
|
||||
|
||||
class InnerGroupSubscription : IDisposable {
|
||||
public readonly PNLRowCollectionViewGroup _inner_group;
|
||||
readonly PNLRowCollectionViewGroup _parent;
|
||||
PNLNormalizedData _last_data = PNLNormalizedData.zero;
|
||||
int _last_count, _last_selected_item_count;
|
||||
|
||||
public InnerGroupSubscription(PNLRowCollectionViewGroup parent, PNLRowCollectionViewGroup inner_group) {
|
||||
_parent = parent;
|
||||
_inner_group = inner_group;
|
||||
_inner_group.parent = parent;
|
||||
// init item count
|
||||
_on_item_count_changed();
|
||||
// listen to item count changes
|
||||
_inner_group.ProtectedItemCountChanged += _on_item_count_changed;
|
||||
// init selected item count
|
||||
_on_selected_item_count_changed();
|
||||
// listen to selected item count changes
|
||||
_inner_group.selected_item_count_changed += _on_selected_item_count_changed;
|
||||
// init data
|
||||
_on_normalized_data_changed();
|
||||
// listen to data changes
|
||||
_inner_group.data_changed += _on_normalized_data_changed;
|
||||
_parent._selected_item_count += _inner_group._selected_item_count;
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
_inner_group.ProtectedItemCountChanged -= _on_item_count_changed;
|
||||
_inner_group.selected_item_count_changed -= _on_selected_item_count_changed;
|
||||
_inner_group.data_changed -= _on_normalized_data_changed;
|
||||
_inner_group.parent = null;
|
||||
// make sure the state is valid (if a bad state goes by undetected here, nefarious/difficult-to-track bugs could arise)
|
||||
if (_last_count != _inner_group.ProtectedItemCount) {
|
||||
Log.Ui.warn("Cached inner count is not equal to the inner group's ItemCount (entire tree structure may be compromised).");
|
||||
}
|
||||
if (_last_data != _inner_group.data) {
|
||||
Log.Ui.warn($"Cached normalized data is not equal to the inner group's normalized data (aggregated pnl data may be compromised).");
|
||||
}
|
||||
_parent.ProtectedItemCount -= _last_count;
|
||||
_parent.data -= _last_data;
|
||||
_parent._selected_item_count -= _last_selected_item_count;
|
||||
}
|
||||
|
||||
void _on_item_count_changed() {
|
||||
// add the delta
|
||||
_parent.ProtectedItemCount += (_inner_group.ProtectedItemCount - _last_count);
|
||||
// reset the local cache
|
||||
_last_count = _inner_group.ProtectedItemCount;
|
||||
}
|
||||
|
||||
void _on_selected_item_count_changed() {
|
||||
_parent._selected_item_count += (_inner_group._selected_item_count - _last_selected_item_count);
|
||||
_last_selected_item_count = _inner_group._selected_item_count;
|
||||
}
|
||||
|
||||
void _on_normalized_data_changed() {
|
||||
var new_data = _inner_group.data;
|
||||
_parent.data += new_data - _last_data;
|
||||
_last_data = new_data;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,413 @@
|
||||
using bts.collections;
|
||||
using mbts;
|
||||
using mbts.collections;
|
||||
using mbts.spark;
|
||||
using mbts.ui;
|
||||
using mbts.ui.controls;
|
||||
using mbts.ui.controls.adorners;
|
||||
using mbts.ui.converters;
|
||||
using mbts.ui.extensions;
|
||||
using mbts.ui.spark;
|
||||
using mbts.ui.utility;
|
||||
using mbts.utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.ComponentModel;
|
||||
using System.Dynamic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace bts.shell.pnl {
|
||||
enum PNLHeaderRowDisplay {
|
||||
None,
|
||||
Total,
|
||||
SelectedTotal
|
||||
}
|
||||
|
||||
partial class PNLView : UserControl {
|
||||
public enum Columns {
|
||||
Account = 1,
|
||||
// Security
|
||||
Exchange,
|
||||
Symbol,
|
||||
SecurityType,
|
||||
Security,
|
||||
SecurityKey,
|
||||
CPP,
|
||||
// SOD
|
||||
SODPNL,
|
||||
SODLongPos,
|
||||
SODLongAvgPrice,
|
||||
SODShortPos,
|
||||
SODShortAvgPrice,
|
||||
SODNetPos,
|
||||
// Day
|
||||
DayPNL,
|
||||
DayLongPos,
|
||||
DayLongAvgPrice,
|
||||
DayShortAvgPrice,
|
||||
DayShortPos,
|
||||
DayCalcType,
|
||||
DayNetPos,
|
||||
HasDayTrades,
|
||||
// Raw Day
|
||||
RawDayLongPos,
|
||||
RawDayShortPos,
|
||||
RawDayNetPos,
|
||||
// Working
|
||||
WorkingLongPos,
|
||||
WorkingLongAvgPrice,
|
||||
WorkingShortPos,
|
||||
WorkingShortAvgPrice,
|
||||
WorkingNetPos,
|
||||
// Total
|
||||
TotalPNL,
|
||||
TotalLongPos,
|
||||
TotalLongAvgPrice,
|
||||
TotalShortPos,
|
||||
TotalShortAvgPrice,
|
||||
TotalNetPos,
|
||||
// Market
|
||||
BidPrice,
|
||||
MidPrice,
|
||||
AskPrice,
|
||||
LastPrice,
|
||||
SettlementPrice,
|
||||
PNLPrice
|
||||
}
|
||||
|
||||
public static readonly IValueConverter
|
||||
IncrementalGroupLevelConverter = ValueConverter.create<int, int>(level => level + 1),
|
||||
GroupLevelMarginConverter = ValueConverter.create<int, Thickness>(level => new Thickness(24 * level, 0, 0, 0)),
|
||||
ExpandedVisualStateConverter = ValueConverter.create<bool, string>(is_expanded => is_expanded ? "Expanded" : "Collapsed");
|
||||
|
||||
public static readonly IMultiValueConverter
|
||||
TotalHeaderRowDisplayTextConverter = ValueConverter.create<int, int, string>((filtered, total) => $"Total ({(filtered == total ? total.ToString() : $"{filtered}/{total}")})"),
|
||||
CellParamsConverter = ValueConverter.create<object, Columns, PNLCell>((item, col) => new PNLCell(item, col));
|
||||
|
||||
public static readonly RoutedCommand CellClickCommand = new RoutedCommand();
|
||||
|
||||
static readonly string _node_path_separator = $" {SpecialCharacters.Dot} ";
|
||||
|
||||
IDisposable _anchors_hook, _animations_hook;
|
||||
int _anchor_version;
|
||||
|
||||
public PNLView() {
|
||||
SetCurrentValue(AnchorIndexProperty, new AnchorIndex());
|
||||
InitializeComponent();
|
||||
SetBinding(AnchorsProperty, new Binding(nameof(PNLVM.anchors)));
|
||||
_grid.ConnectOnLoaded(() => {
|
||||
if (_grid.try_find_descendent_of_type(out DIYGridRowsPresenter presenter)) {
|
||||
presenter.GroupItemSelectionPivot += on_group_item_selection_pivot;
|
||||
return Disposable.Create(() => presenter.GroupItemSelectionPivot -= on_group_item_selection_pivot);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
void on_group_item_selection_pivot(object sender, GroupItemSelectionPivotEventArgs e) =>
|
||||
e.Handled = e.group_item is PNLRowCollectionViewGroup pnl_group && pnl_group.is_expanded;
|
||||
}
|
||||
|
||||
public new PNLVM DataContext {
|
||||
get => base.DataContext as PNLVM;
|
||||
set => base.DataContext = value;
|
||||
}
|
||||
|
||||
public bool EnableAnimations {
|
||||
get => _animations_hook != null;
|
||||
set {
|
||||
if (EnableAnimations != value) {
|
||||
if (_animations_hook == null) {
|
||||
_animations_hook = AdornerHelper.attach_continuously_on_loaded(
|
||||
() => new GridAnimation.DIYGridAnimationAdorner<Columns>(_grid, _grid.descendants_of_type<DIYGridRowsPresenter>().First()),
|
||||
_grid);
|
||||
} else {
|
||||
_animations_hook.Dispose();
|
||||
_animations_hook = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#region AnchorIndex
|
||||
public static readonly DependencyProperty AnchorIndexProperty =
|
||||
WPFHelper.create_dp<AnchorIndex, PNLView>(
|
||||
nameof(AnchorIndex),
|
||||
(o, v) => v?.reset(o?.Anchors));
|
||||
|
||||
public AnchorIndex AnchorIndex {
|
||||
get => GetValue(AnchorIndexProperty) as AnchorIndex;
|
||||
set => SetValue(AnchorIndexProperty, value);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region GroupLevel
|
||||
public static readonly DependencyProperty GroupLevelProperty =
|
||||
DependencyProperty.RegisterAttached(
|
||||
"GroupLevel",
|
||||
typeof(int),
|
||||
typeof(PNLView),
|
||||
new PropertyMetadata(0, (d, e) => {
|
||||
if (d is GroupItem group && e.NewValue is int level)
|
||||
group.SetBinding(DIYGrid.ColumnKeyProperty, new Binding($"{nameof(Anchors)}[{level}]") { RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor, typeof(PNLView), 1) });
|
||||
}));
|
||||
|
||||
public static int GetGroupLevel(DependencyObject d) => (int)d.GetValue(GroupLevelProperty);
|
||||
public static void SetGroupLevel(DependencyObject d, int value) => d.SetValue(GroupLevelProperty, value);
|
||||
|
||||
void _grid_loaded(object sender, RoutedEventArgs e) {
|
||||
// this is a hack to make room for the 'totals row' between the column headers and the regular rows.
|
||||
if (_grid.try_find_descendent_of_type(out DIYGridRowsPresenter rp)) {
|
||||
rp.SetBinding(MarginProperty, new Binding(nameof(PNLVM.header_row_display)) {
|
||||
Converter = ValueConverter.create<PNLHeaderRowDisplay, Thickness>(hrd => {
|
||||
double top = 0;
|
||||
if ((hrd & PNLHeaderRowDisplay.Total) != 0)
|
||||
top += 24;
|
||||
if ((hrd & PNLHeaderRowDisplay.SelectedTotal) != 0)
|
||||
top += 24;
|
||||
return new Thickness(0, top, 0, 0);
|
||||
}),
|
||||
Source = DataContext
|
||||
});
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Anchors
|
||||
public static readonly DependencyProperty AnchorsProperty =
|
||||
WPFHelper.create_dp<IEnumerable<Columns>, PNLView>(
|
||||
nameof(Anchors),
|
||||
(o, v) => {
|
||||
var cols = o?._grid?.Columns;
|
||||
if (cols == null)
|
||||
return;
|
||||
if (!(o._grid.Rows is ICollectionView rows_view))
|
||||
return;
|
||||
if (!(v is INotifyCollectionChanged anchor_notifier && v is IReadOnlyList<Columns> anchor_keys))
|
||||
return;
|
||||
int version = ++o._anchor_version;
|
||||
bool busy = false;
|
||||
var pnl_cols = cols.CastObservable<DIYGrid.ColumnBase, PNLColumn>();
|
||||
Disposable.replace(ref o._anchors_hook, Disposable.Create(
|
||||
anchor_notifier.Subscribe((_, __) => {
|
||||
if (busy)
|
||||
return;
|
||||
// invoke async to avoid noisy consecutive resets due to batch changes
|
||||
busy = true;
|
||||
o.Dispatcher.InvokeAsync(() => {
|
||||
sync_anchor_cols();
|
||||
busy = false;
|
||||
});
|
||||
})));
|
||||
o.AnchorIndex?.reset(anchor_keys);
|
||||
sync_anchor_cols();
|
||||
void sync_anchor_cols() {
|
||||
// since reset is invoked asynchronously, need to check we are still the latest version.
|
||||
if (o._anchor_version != version)
|
||||
return;
|
||||
o.AnchorIndex?.reset(anchor_keys);
|
||||
// move all the anchor columns to the left, ordered by anchor position (grouping hierarchy).
|
||||
for (int i = 0; i < anchor_keys.Count; i++) {
|
||||
if (pnl_cols.TryGetFirst(c => c.key == anchor_keys[i], out var match, out int curr_pos)) {
|
||||
match.is_visible = true;
|
||||
if (curr_pos != i) {
|
||||
cols.Move(curr_pos, i);
|
||||
}
|
||||
match.anchor_index = i + 1;
|
||||
}
|
||||
}
|
||||
for (int i = anchor_keys.Count; i < pnl_cols.Count; i++) {
|
||||
pnl_cols[i].anchor_index = null;
|
||||
}
|
||||
using (rows_view.DeferRefresh()) {
|
||||
rows_view.GroupDescriptions.Clear();
|
||||
var anchor_cols = from key in anchor_keys
|
||||
join col in pnl_cols on key equals col.key
|
||||
select new PNLGroupDescription(col);
|
||||
rows_view.GroupDescriptions.AddRange(anchor_cols);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
public IEnumerable<Columns> Anchors {
|
||||
get => GetValue(AnchorsProperty) as IEnumerable<Columns>;
|
||||
set => SetValue(AnchorsProperty, value);
|
||||
}
|
||||
#endregion
|
||||
|
||||
void _cell_click(PNLCell cell) {
|
||||
Log.Ui.info($"PNLCell Click: {cell}.");
|
||||
try {
|
||||
switch (cell.column) {
|
||||
case Columns.SODNetPos:
|
||||
case Columns.SODLongPos:
|
||||
case Columns.SODShortPos:
|
||||
case Columns.DayNetPos:
|
||||
case Columns.DayLongPos:
|
||||
case Columns.DayShortPos:
|
||||
case Columns.RawDayNetPos:
|
||||
case Columns.RawDayLongPos:
|
||||
case Columns.RawDayShortPos:
|
||||
case Columns.TotalNetPos:
|
||||
case Columns.TotalLongPos:
|
||||
case Columns.TotalShortPos: {
|
||||
var oneof = cell.oneof();
|
||||
if (oneof == null)
|
||||
return;
|
||||
var (vm, context, title, fg) = cell.oneof().Match(_row_fills, _group_fills, _header_fills);
|
||||
_show_auxiliary_popup(vm, context, title, fg);
|
||||
}
|
||||
break;
|
||||
case Columns.WorkingNetPos:
|
||||
case Columns.WorkingLongPos:
|
||||
case Columns.WorkingShortPos: {
|
||||
var (vm, context, title, fg) = cell.oneof().Match(_orderbook_params, _orderbook_params, _orderbook_params);
|
||||
_show_auxiliary_popup(vm, context, title, fg);
|
||||
}
|
||||
break;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Log.Ui.warn($"{nameof(PNLView)} error on cell click: {ex}.");
|
||||
}
|
||||
}
|
||||
|
||||
#region Fills
|
||||
(FillsVM vm, string context, string title, Color fg) _row_fills(PNLRowCell cell) {
|
||||
var (title_desc, fg) = _fills_title(cell.column);
|
||||
return (DataContext.mk_fills_vm(cell.row, cell.column), $"{cell.row.account_name} {cell.row.security}", title_desc, fg);
|
||||
}
|
||||
|
||||
(FillsVM vm, string context, string title, Color fg) _group_fills(PNLGroupCell cell) {
|
||||
var (title_desc, fg) = _fills_title(cell.column);
|
||||
return (DataContext.mk_fills_vm(cell.group, cell.column), string.Join(_node_path_separator, cell.group.GroupNamePath), title_desc, fg);
|
||||
}
|
||||
|
||||
(FillsVM vm, string context, string title, Color fg) _header_fills(PNLHeaderRowCell cell) {
|
||||
var (title_desc, fg) = _fills_title(cell.column);
|
||||
return (DataContext.mk_fills_vm(cell.header_row, cell.column), EnumHelper.get_display_name(cell.header_row), title_desc, fg);
|
||||
}
|
||||
|
||||
static (string desc, Color fg) _fills_title(Columns key) {
|
||||
const uint net_fg = 0x333333u;
|
||||
switch (key) {
|
||||
case Columns.SODLongPos: return ("Open Buy Fills", BTSColors.MarketData.bid);
|
||||
case Columns.SODShortPos: return ("Open Sell Fills", BTSColors.MarketData.ask);
|
||||
case Columns.SODNetPos: return ("Open Fills", net_fg.rgb_color());
|
||||
case Columns.DayLongPos: return ("Day Buy Fills", BTSColors.MarketData.bid);
|
||||
case Columns.DayShortPos: return ("Day Sell Fills", BTSColors.MarketData.ask);
|
||||
case Columns.DayNetPos: return ("Day Fills", net_fg.rgb_color());
|
||||
case Columns.RawDayLongPos: return ("Raw Day Buy Fills", BTSColors.MarketData.bid);
|
||||
case Columns.RawDayShortPos: return ("Raw Day Sell Fills", BTSColors.MarketData.ask);
|
||||
case Columns.RawDayNetPos: return ("Raw Day Fills", net_fg.rgb_color());
|
||||
case Columns.TotalLongPos: return ("All Buy Fills", BTSColors.MarketData.bid);
|
||||
case Columns.TotalShortPos: return ("All Sell Fills", BTSColors.MarketData.ask);
|
||||
case Columns.TotalNetPos: return ("All Fills", net_fg.rgb_color());
|
||||
default: throw ExceptionBuilder.Argument.unexpected_case(key, nameof(key));
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region OrderBook
|
||||
(OrderBookVM vm, string context, string title, Color fg) _orderbook_params(PNLRowCell cell) {
|
||||
var (title_desc, fg) = _orderbook_title(cell.column);
|
||||
return (DataContext.mk_orderbook_vm(cell.row, cell.column), $"{cell.row.account_name} {cell.row.security}", title_desc, fg);
|
||||
}
|
||||
|
||||
(OrderBookVM vm, string context, string title, Color fg) _orderbook_params(PNLGroupCell cell) {
|
||||
var (title_desc, fg) = _orderbook_title(cell.column);
|
||||
return (DataContext.mk_orderbook_vm(cell.group, cell.column), string.Join(_node_path_separator, cell.group.GroupNamePath), title_desc, fg);
|
||||
}
|
||||
|
||||
(OrderBookVM vm, string context, string title, Color fg) _orderbook_params(PNLHeaderRowCell cell) {
|
||||
var (title_desc, fg) = _orderbook_title(cell.column);
|
||||
return (DataContext.mk_orderbook_vm(cell.header_row, cell.column), EnumHelper.get_display_name(cell.header_row), title_desc, fg);
|
||||
}
|
||||
|
||||
static (string desc, Color fg) _orderbook_title(Columns key) {
|
||||
switch (key) {
|
||||
case Columns.WorkingLongPos: return ("Working Buy Orders", BTSColors.MarketData.bid);
|
||||
case Columns.WorkingShortPos: return ("Working Sell Orders", BTSColors.MarketData.ask);
|
||||
case Columns.WorkingNetPos: return ("Working Orders", 0x333333u.rgb_color());
|
||||
default: throw ExceptionBuilder.Argument.unexpected_case(key, nameof(key));
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
void _cell_click_executed(object sender, ExecutedRoutedEventArgs e) {
|
||||
if (e.Parameter is PNLCell cell) {
|
||||
_cell_click(cell);
|
||||
} else Log.Ui.warn($"{nameof(PNLView)} '{nameof(_cell_click_executed)}' parameter should be a {typeof(PNLCell)} (actually '{e.Parameter}').");
|
||||
}
|
||||
|
||||
void _on_group_item_mouse_down(object sender, MouseButtonEventArgs e) {
|
||||
if (!e.Handled && sender is FrameworkElement el && el.DataContext is PNLRowCollectionViewGroup group) {
|
||||
if (e.ClickCount == 2) {
|
||||
group.is_expanded = !group.is_expanded;
|
||||
} else if (el.try_find_ancestor_of_type(out DIYGrid grid) && grid.SelectionMode != SelectionMode.Single) {
|
||||
group.is_all_selected = !group.is_all_selected.GetValueOrDefault();
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
Content = content,
|
||||
Header = header,
|
||||
HorizontalAlignment = HorizontalAlignment.Left,
|
||||
VerticalAlignment = VerticalAlignment.Top
|
||||
};
|
||||
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();
|
||||
}
|
||||
|
||||
class PNLGroupDescription : PropertyGroupDescription, IEquatable<PNLGroupDescription> {
|
||||
public PNLGroupDescription(PNLColumn column) : base(column.data_member_path.Path) =>
|
||||
key = column.key;
|
||||
|
||||
public Columns key { get; }
|
||||
|
||||
public bool Equals(PNLGroupDescription other) =>
|
||||
other != null && key == other.key;
|
||||
|
||||
public override int GetHashCode() =>
|
||||
key.GetHashCode();
|
||||
|
||||
public override bool Equals(object obj) =>
|
||||
Equals(obj as PNLGroupDescription);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ using bts.spark;
|
||||
using mbts.persist;
|
||||
using mbts.spark;
|
||||
using mbts.ui;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.Composition;
|
||||
using System.IO;
|
||||
@@ -16,12 +17,17 @@ using static mbts.ui.shell.ProtoBufSerializationHelper;
|
||||
namespace bts.shell {
|
||||
[Export(typeof(IShellModule))]
|
||||
class TradingShellModule : IShellModule {
|
||||
LadderWindowFactory _lwf;
|
||||
OrderBookWindowFactory _obwf;
|
||||
FillsWindowFactory _fwf;
|
||||
PAndLWindowFactory _plwf;
|
||||
AlgoStudioWindowFactory _aswf;
|
||||
ManualFillsWindowFactory _mfwf;
|
||||
readonly PNLConfig _pnl_conf;
|
||||
// window factories
|
||||
readonly LadderWindowFactory _lwf;
|
||||
readonly OrderBookWindowFactory _obwf;
|
||||
readonly FillsWindowFactory _fwf;
|
||||
readonly PAndLWindowFactory _old_plwf;
|
||||
readonly PNLWindowFactory _plwf;
|
||||
readonly AlgoStudioWindowFactory _aswf;
|
||||
readonly ManualFillsWindowFactory _mfwf;
|
||||
//
|
||||
List<SparkCommand> _actions;
|
||||
|
||||
[ImportingConstructor]
|
||||
public TradingShellModule(
|
||||
@@ -33,10 +39,17 @@ namespace bts.shell {
|
||||
OrdersModel orders_model,
|
||||
md.MBOService mbo,
|
||||
ErrorReporter error_reporter) {
|
||||
/// Unlike our <see cref="SparkCommand"/> members, <see cref="IShellWindowFactory"/> members must be instantiated immediately,
|
||||
/// as they are responsible for launching windows in response to a SparkTag being published to the bus.
|
||||
/// The tags may be published by anything (i.e., not just the spark commands in <see cref="actions"/>).
|
||||
_lwf = new LadderWindowFactory(this, bus, wm, settings_hub, orders_model, mbo);
|
||||
_obwf = new OrderBookWindowFactory(this, bus, notification_service, notification_service_data_cache, orders_model, settings_hub);
|
||||
_fwf = new FillsWindowFactory(this, bus, settings_hub, orders_model);
|
||||
_plwf = new PAndLWindowFactory(this, bus, settings_hub, orders_model);
|
||||
_pnl_conf = new PNLConfig(mbts.config.Configuration.get<bts.config.PNLOptions>());
|
||||
if (_pnl_conf.show_new_pnl)
|
||||
_plwf = new PNLWindowFactory(this, bus, settings_hub, orders_model);
|
||||
if (_pnl_conf.show_old_pnl)
|
||||
_old_plwf = new PAndLWindowFactory(this, bus, settings_hub, orders_model);
|
||||
_aswf = new AlgoStudioWindowFactory(this, bus, orders_model, error_reporter);
|
||||
_mfwf = new ManualFillsWindowFactory(this, bus, orders_model);
|
||||
}
|
||||
@@ -47,31 +60,62 @@ namespace bts.shell {
|
||||
|
||||
public int priority => 20;
|
||||
|
||||
public IEnumerable<SparkCommand> actions => new [] {
|
||||
SparkCommands.ShowOrders.complete(() => bts.shell.orderbook.OrderBookStartupData.Default),
|
||||
SparkCommands.ShowFills(null).complete(() => new FillsSettings()),
|
||||
SparkCommands.ShowPAndL.complete(() => new PAndLWindowFactory.PAndLStartupData()),
|
||||
SparkCommands.PickLadder,
|
||||
SparkCommands.PickOrderTicket,
|
||||
SparkCommands.ShowAlgoStudio.complete(() => new AlgoStudioWindowFactory.StartupData()),
|
||||
SparkCommands.ShowManualFills().complete(() => ManualFillsSettings.Default)
|
||||
};
|
||||
public IEnumerable<SparkCommand> actions {
|
||||
get {
|
||||
if (_actions == null) {
|
||||
_actions = new List<SparkCommand> {
|
||||
SparkCommands.ShowOrders.complete(() => bts.shell.orderbook.OrderBookStartupData.Default),
|
||||
SparkCommands.ShowFills(null).complete(() => new FillsSettings()),
|
||||
};
|
||||
if (_pnl_conf.show_new_pnl)
|
||||
_actions.Add(SparkCommands.ShowPNL.complete(() => null));
|
||||
if (_pnl_conf.show_old_pnl)
|
||||
_actions.Add(SparkCommands.ShowPAndL.complete(() => new PAndLWindowFactory.PAndLStartupData()));
|
||||
_actions.AddRange(new[] {
|
||||
SparkCommands.PickLadder,
|
||||
SparkCommands.PickOrderTicket,
|
||||
SparkCommands.ShowAlgoStudio.complete(() => new AlgoStudioWindowFactory.StartupData()),
|
||||
SparkCommands.ShowManualFills().complete(() => ManualFillsSettings.Default)
|
||||
});
|
||||
}
|
||||
return _actions;
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerable<IShellWindowFactory> window_factories() {
|
||||
yield return _lwf;
|
||||
yield return _obwf;
|
||||
yield return _fwf;
|
||||
yield return _plwf;
|
||||
yield return _aswf;
|
||||
yield return _mfwf;
|
||||
IEnumerable<IShellWindowFactory> _window_factories {
|
||||
get {
|
||||
yield return _lwf;
|
||||
yield return _obwf;
|
||||
yield return _fwf;
|
||||
if (_plwf != null)
|
||||
yield return _plwf;
|
||||
if (_old_plwf != null)
|
||||
yield return _old_plwf;
|
||||
yield return _aswf;
|
||||
yield return _mfwf;
|
||||
}
|
||||
}
|
||||
|
||||
public Task<IShellWindow> create(Stream load_stream) =>
|
||||
dispatch_to_factory(load_stream, window_factories().ToArray());
|
||||
dispatch_to_factory(load_stream, _window_factories.ToArray());
|
||||
|
||||
public void Dispose() {
|
||||
foreach (var i in window_factories())
|
||||
if (i != null) i.Dispose();
|
||||
if (_actions != null) {
|
||||
_actions.OfType<IDisposable>().DisposeAll();
|
||||
_actions = null;
|
||||
}
|
||||
_window_factories.DisposeAll();
|
||||
}
|
||||
|
||||
readonly struct PNLConfig {
|
||||
public readonly bool
|
||||
show_new_pnl,
|
||||
show_old_pnl;
|
||||
|
||||
public PNLConfig(bts.config.PNLOptions options) {
|
||||
show_new_pnl = options.show_new_pnl;
|
||||
show_old_pnl = options.show_old_pnl;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,28 +2,40 @@
|
||||
using bts.api.orderman;
|
||||
using bts.utils;
|
||||
using System;
|
||||
|
||||
using static bts.api.orderman.Fill.Types;
|
||||
|
||||
namespace mbts.spark {
|
||||
class FillTuple : IEquatable<FillTuple> {
|
||||
public static readonly Func<FillTuple, string> key_fn = ft => ft.fill.FillKey;
|
||||
public readonly FillImmutable fill;
|
||||
public readonly OrderRecordImmutable? order;
|
||||
public readonly SecurityImmutable? security;
|
||||
public readonly AlgoData? algo;
|
||||
public readonly AlgoDefinitionMetadata? algo_meta;
|
||||
|
||||
public FillTuple((SecurityImmutable? sec, FillImmutable val) fill, OrderSecurity? order_tuple) {
|
||||
this.fill = fill.val;
|
||||
this.security = fill.sec;
|
||||
if (order_tuple is OrderSecurity osam) {
|
||||
order = osam.order;
|
||||
algo = osam.algo;
|
||||
algo_meta = osam.meta;
|
||||
}
|
||||
OrderSecurityRootPair? _order_tuple;
|
||||
|
||||
public FillTuple(FillImmutable fill, SecurityImmutable? security, OrderSecurityRootPair? order_tuple) {
|
||||
this.fill = fill;
|
||||
this.security = security;
|
||||
_order_tuple = order_tuple;
|
||||
}
|
||||
|
||||
public FillTuple((SecurityImmutable? sec, FillImmutable val) fill, OrderSecurity? order_tuple)
|
||||
: this(fill.val, fill.sec, order_tuple.HasValue ? OrderSecurityRootPair.from_order_security(order_tuple.Value) : (OrderSecurityRootPair?)null) { }
|
||||
|
||||
public AlgoData? algo =>
|
||||
_order_tuple.HasValue ? _order_tuple.Value.order.algo : (AlgoData?)null;
|
||||
|
||||
public AlgoDefinitionMetadata? algo_meta =>
|
||||
_order_tuple.HasValue ? _order_tuple.Value.order.meta : null;
|
||||
|
||||
public OrderRecordImmutable? order =>
|
||||
_order_tuple.HasValue ? _order_tuple.Value.order.order : (OrderRecordImmutable?)null;
|
||||
|
||||
public AlgoData? root_algo =>
|
||||
_order_tuple.HasValue ? _order_tuple.Value.root.algo : (AlgoData?)null;
|
||||
|
||||
public AlgoDefinitionMetadata? root_algo_meta =>
|
||||
_order_tuple.HasValue ? _order_tuple.Value.root.meta : null;
|
||||
|
||||
public string account {
|
||||
get {
|
||||
var account_string = string.Empty;
|
||||
@@ -33,12 +45,19 @@ namespace mbts.spark {
|
||||
}
|
||||
}
|
||||
|
||||
public bool Equals(FillTuple other) => other != null
|
||||
&& other.order.HasValue == order.HasValue
|
||||
&& other.security.HasValue == security.HasValue
|
||||
&& other.fill.Equals(fill);
|
||||
public uint account_id =>
|
||||
fill.FillKind != FillKind.Sod && order.HasValue ?
|
||||
order.Value.account_id() :
|
||||
fill.InternalAccountId;
|
||||
|
||||
public override bool Equals(object obj) => Equals(obj as FillTuple);
|
||||
public bool Equals(FillTuple other) =>
|
||||
other != null &&
|
||||
_order_tuple.Equals(other._order_tuple) &&
|
||||
security.HasValue == other.security.HasValue &&
|
||||
fill.Equals(other.fill);
|
||||
|
||||
public override bool Equals(object obj) =>
|
||||
Equals(obj as FillTuple);
|
||||
|
||||
public override int GetHashCode() =>
|
||||
EqualityComparer.aggregate_hash(
|
||||
@@ -66,4 +85,4 @@ namespace mbts.spark {
|
||||
public static string user(this FillTuple fill_tuple) =>
|
||||
fill_tuple.order.HasValue ? fill_tuple.order.Value.user() : string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -53,7 +53,7 @@ namespace mbts.spark {
|
||||
send_order = new DelegateCommand<Side>(side => {
|
||||
bool price_valid = _validate_price(), size_valid = size_calculator.validate();
|
||||
if (price_valid && size_valid && !HasErrors && !properties_vm.HasErrors) {
|
||||
model.send_order(properties_vm.create(side), price, size_calculator.value.Value).ignore();
|
||||
model.send_order(security, properties_vm.create(side), price, size_calculator.value.Value).ignore();
|
||||
RequestClose?.Invoke(this, new EventArgs());
|
||||
}
|
||||
});
|
||||
|
||||
@@ -304,6 +304,29 @@ namespace mbts.spark.utilities {
|
||||
|
||||
public static OneofDescriptor oneof_descriptor => _oneof_desc;
|
||||
|
||||
const double _price_error_margin = 0.0000001;
|
||||
const decimal _price_error_margin_decimal = 0.0000001m;
|
||||
|
||||
public static readonly IComparer<double> PriceComparer = Comparer<double>.Create((a, b) => {
|
||||
if (double.IsNaN(a)) {
|
||||
return double.IsNaN(b) ? 0 : -1;
|
||||
} else if (double.IsNaN(b)) {
|
||||
return 1;
|
||||
} else {
|
||||
double diff = a - b;
|
||||
if (diff <= _price_error_margin && diff >= -_price_error_margin)
|
||||
return 0;
|
||||
return diff > 0 ? 1 : -1;
|
||||
}
|
||||
});
|
||||
|
||||
public static readonly IComparer<decimal> PriceComparerDecimal = Comparer<decimal>.Create((a, b) => {
|
||||
decimal diff = a - b;
|
||||
if (diff <= _price_error_margin_decimal && diff >= -_price_error_margin_decimal)
|
||||
return 0;
|
||||
return diff > 0 ? 1 : -1;
|
||||
});
|
||||
|
||||
const string _primary_child_field = "child";
|
||||
// algo-type-case maps
|
||||
// Note: obv this will break if we change the name of the 'CustomOneofCase' field
|
||||
|
||||
@@ -200,6 +200,13 @@ namespace mbts.spark {
|
||||
public static string short_description(this OrderRecordImmutable order, ContractSpecificationImmutable? spec) =>
|
||||
$"{(order.HasDef ? order.Def.Side.ToString() : "(unknown-side)")} {(order.HasState ? ($@"{spec.format(order.State.Price)} @ {order.State.Quantity}") : "(unknown-price) @ (unknown-quantity)")}";
|
||||
|
||||
public static string short_description(SecurityImmutable security, Side side, double price, uint quantity) =>
|
||||
new StringBuilderEx(
|
||||
security.ToString(), " ",
|
||||
side.ToString(), " ",
|
||||
price_quantity_description(price, quantity, security.spec_or_default())
|
||||
).ToString();
|
||||
|
||||
public static string short_description(this OrderRecordImmutable order, SecurityImmutable? security) {
|
||||
var spec = security.spec_or_default();
|
||||
return new StringBuilderEx(
|
||||
@@ -269,6 +276,9 @@ namespace mbts.spark {
|
||||
public static Color badge_color(AlgoDefinitionMetadata? algo_meta, OrderMetadata order_meta) =>
|
||||
algo_meta?.badge_color ?? order_meta.Color.rgb_color();
|
||||
|
||||
public static string order_collection_summary(double best_price, int best_price_volume, int total_volume) =>
|
||||
order_collection_summary(best_price, best_price_volume, total_volume, SecurityDataHelper.Defaults.ContractSpecificationImmutable);
|
||||
|
||||
public static string order_collection_summary(double best_price, int best_price_volume, int total_volume, ContractSpecificationImmutable spec) {
|
||||
if (total_volume == 0)
|
||||
return string.Empty;
|
||||
@@ -277,6 +287,14 @@ namespace mbts.spark {
|
||||
$"{best_price_volume} ({total_volume}) @ {spec.format(best_price)}";
|
||||
}
|
||||
|
||||
public static string order_collection_summary(double best_price, double avg_price, int best_price_volume, int total_volume, ContractSpecificationImmutable spec) {
|
||||
if (total_volume == 0)
|
||||
return string.Empty;
|
||||
return total_volume == best_price_volume ?
|
||||
$"{best_price_volume} @ {spec.format(best_price)}" :
|
||||
$"{best_price_volume} @ {spec.format(best_price)} ({total_volume} @ {spec.format(avg_price)})";
|
||||
}
|
||||
|
||||
public static string past_tense(this Side side) {
|
||||
switch (side) {
|
||||
case Side.Buy: return "Bought";
|
||||
|
||||
@@ -5,6 +5,7 @@ using bts.api.orderman;
|
||||
using bts.api.position;
|
||||
using bts.collections;
|
||||
using bts.md;
|
||||
using bts.shell.pnl;
|
||||
using bts.spark;
|
||||
using bts.utils;
|
||||
using mbts.config;
|
||||
@@ -68,13 +69,21 @@ namespace mbts.spark {
|
||||
.Cache();
|
||||
|
||||
on_positions = _position_service.subscribei_position_data().Naggle(interval, scheduler).PermissionFilter(admin_model.on_admin_data).join(sstore, pos => pos.SecurityId).Table().Cache();
|
||||
working_orders =
|
||||
on_pnl = _position_service.subscribei_pnl_data()
|
||||
.Naggle(interval, scheduler)
|
||||
.join(sstore, p => p.Pos.SecurityId)
|
||||
.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(
|
||||
ImmutableDictionary<uint,OrderSecurity>.Empty,
|
||||
ImmutableDictionary<uint, OrderSecurity>.Empty,
|
||||
(workers, table) => _update_working_orders(workers, table))
|
||||
.Replay(1).RefCount();
|
||||
|
||||
on_order_securities =
|
||||
on_order_securities =
|
||||
on_orders
|
||||
.as_iincremental()
|
||||
.filter(t => t.sec.HasValue)
|
||||
@@ -132,8 +141,40 @@ namespace mbts.spark {
|
||||
OrderSecurityRootPair.from_order_security,
|
||||
OrderSecurityHelper.root_pair_key_fn
|
||||
).Cache();
|
||||
// fills
|
||||
var on_fills =
|
||||
// working account orders
|
||||
var working_account_order_groups = working_orders.GroupTable(t => t.order.Key, t => new PNLKey(t.order.account_id(), t.order.security_key()));
|
||||
// security account pnl
|
||||
security_account_pnl = on_pnl
|
||||
#if DEBUG
|
||||
.AssertIncremental(p => new PNLKey(p.Item2.Pos.AccountId, p.Item2.Pos.SecurityId))
|
||||
#endif
|
||||
.SelectKeyValue(PNLRowDataFactory.create, PNLRowData.key_fn)
|
||||
.FullJoin(
|
||||
working_account_order_groups,
|
||||
PNLRowData.key_fn,
|
||||
acc_orders => acc_orders.group_key,
|
||||
(pnl_row_data, acc_orders, pnl_key) => pnl_row_data.with(working: WorkingOrderData.Create(acc_orders.table.state.Values.Select(t => t.order))),
|
||||
(pnl_row_data, pnl_id) => pnl_row_data,
|
||||
(acc_orders, pnl_id) => {
|
||||
string account;
|
||||
SecurityImmutable? security;
|
||||
if (acc_orders.table.state.Values.TryGetFirst(out var tup)) {
|
||||
security = tup.sec;
|
||||
account = tup.order.account();
|
||||
} else {
|
||||
security = null;
|
||||
account = null;
|
||||
}
|
||||
return new PNLRowData(pnl_id,
|
||||
security: security,
|
||||
account_name: account,
|
||||
working: WorkingOrderData.Create(acc_orders.table.state.Values.Select(t => t.order)));
|
||||
},
|
||||
PNLRowData.key_fn)
|
||||
.Where(t => t.has_changes())
|
||||
.Cache();
|
||||
// fill tuples
|
||||
var on_fills =
|
||||
_service.subscribei_fills()
|
||||
.Naggle(interval, scheduler)
|
||||
.PermissionFilter(admin_model.on_admin_data)
|
||||
@@ -148,13 +189,23 @@ namespace mbts.spark {
|
||||
(f, fk, ok) => new FillTuple(f, null),
|
||||
f => f.fill.FillKey);
|
||||
on_fill_tuples = fill_order_tuples_join.Cache();
|
||||
// fill tuples with root (algo)
|
||||
var fill_order_tuples_with_root_join = on_fills.LeftJoin(
|
||||
on_order_tuples_with_root,
|
||||
f => f.val.FillKey,
|
||||
OrderSecurityHelper.root_pair_key_fn,
|
||||
f => f.val.OrderKey,
|
||||
(f, fk, o, ok) => new FillTuple(f.val, f.sec, o),
|
||||
(f, fk, ok) => new FillTuple(f.val, f.sec, null),
|
||||
FillTuple.key_fn);
|
||||
on_fill_tuples_with_root = fill_order_tuples_with_root_join.Cache();
|
||||
// manual fills
|
||||
on_manual_fills_securities = _position_service.subscribei_manual_fills()
|
||||
.Naggle(interval, scheduler)
|
||||
.join(sstore, mf => mf.SecurityKey)
|
||||
.Table().Cache();
|
||||
var manual_fill_tuples_account_join = on_manual_fills_securities.LeftJoin(
|
||||
admin_model.on_accounts.Select(d => MappedDictionary.create(d, kv => new AccountTuple(kv.Key, kv.Value))).Table(a => a.id),
|
||||
admin_model.on_accounts.Select(d => MappedDictionary.create(d, (k, v) => new AccountTuple(k, v))).Table(a => a.id),
|
||||
mfs => mfs.Item2.RecordId,
|
||||
AccountTuple.key_fn,
|
||||
mfs => mfs.Item2.AccountId,
|
||||
@@ -162,7 +213,7 @@ namespace mbts.spark {
|
||||
(mfs, mf_id, a_id) => new ManualFillTuple(mfs.Item2, mfs.Item1, null, null),
|
||||
ManualFillTuple.key_fn);
|
||||
on_manual_fill_tuples = manual_fill_tuples_account_join.LeftJoin(
|
||||
admin_model.on_users.Select(d => MappedDictionary.create(d, kv => new UserTuple(kv.Key, kv.Value))).Table(a => a.id),
|
||||
admin_model.on_users.Select(d => MappedDictionary.create(d, (k, v) => new UserTuple(k, v))).Table(a => a.id),
|
||||
ManualFillTuple.key_fn,
|
||||
UserTuple.key_fn,
|
||||
mft => mft.manual_fill.UserId,
|
||||
@@ -190,7 +241,6 @@ namespace mbts.spark {
|
||||
.Filter(os => os.order.is_active() && os.order.State.DisplayStatus == OrderState.Types.DisplayStatus.Visible, key => key.order.Key)
|
||||
.Subscribe(_security_working_orders_with_piq_demuxer),
|
||||
on_positions.Subscribe(_security_positions_demuxer),
|
||||
_bus.subscribe(SparkTags.CreateOrder).Subscribe(a => send_order(a.definition, a.price, a.size).ignore()),
|
||||
_bus.subscribe(SparkTags.RecreateOrder).Subscribe(a => a.ForEach(x => send_order(x.definition, x.price, x.size).ignore())),
|
||||
_bus.subscribe(SparkTags.CancelOrders).Subscribe(x => cancel_orders(x).ignore()),
|
||||
_bus.subscribe(SparkTags.DetachOrders).Subscribe(x => detach_orders(x).ignore()),
|
||||
@@ -211,7 +261,11 @@ namespace mbts.spark {
|
||||
public IObservable<TableUpdate<KeyValuePair<uint, bool>, uint>> on_orders_can_attach { get; }
|
||||
public IObservable<IIncrementalUpdate<SecurityImmutable, uint>> on_order_securities { get; }
|
||||
public IObservable<TableUpdate<FillTuple, string>> on_fill_tuples { get; }
|
||||
public IObservable<IReadOnlyDictionary<uint,OrderSecurity>> working_orders { get; }
|
||||
public IObservable<TableUpdate<FillTuple, string>> on_fill_tuples_with_root { get; }
|
||||
public IObservable<IReadOnlyDictionary<uint, OrderSecurity>> working_orders { get; }
|
||||
public IObservable<TableUpdate<(SecurityImmutable?, PNLUnitImmutable), int>> on_pnl { get; }
|
||||
public IObservable<TableUpdate<PNLRowData, PNLKey>> security_account_pnl { get; }
|
||||
public IObservable<TableUpdate<AccountPNLImmutable, uint>> on_account_pnl { get; }
|
||||
public IObservable<TableUpdate<(SecurityImmutable?, PositionUnitImmutable), int>> on_positions { get; }
|
||||
public IObservable<TableUpdate<(AlgoData algo, AlgoDefinitionMetadata? meta), AlgoKey>> studio_algos_with_meta { get; }
|
||||
public IObservable<TableUpdate<(SecurityImmutable?, ManualFillImmutable), int>> on_manual_fills_securities { get; }
|
||||
@@ -232,13 +286,33 @@ namespace mbts.spark {
|
||||
#endregion
|
||||
|
||||
#region Input
|
||||
public async Task<uint?> send_order(OrderDefinitionImmutable? order, double price, uint size) {
|
||||
// the security arg is needed for a temporary hack to prevent GlbxIceberg orders from going out in non-glbx exchanges (like Ice)
|
||||
// eventually, we'll need an algo type model to handle this stuff.
|
||||
// This hack leverages the fact that every place that calls 'send_order' already has the security handy (not just the sec id).
|
||||
public Task<uint?> send_order(SecurityImmutable security, OrderDefinitionImmutable? order, double price, uint size) {
|
||||
if (order.HasValue) {
|
||||
var result = (await send_order_get_error(order.Value, price, size));
|
||||
_activity.on_send_order(order.Value, price, size, result.key, result.error);
|
||||
return result.key;
|
||||
#if DEBUG
|
||||
if (security.Exchange != Venue.Glbx && security.Exchange != Venue.Sim) {
|
||||
#else
|
||||
if (security.Exchange != Venue.Glbx) {
|
||||
#endif
|
||||
var algo_structure = OrderDataHelper.enumerate_algo_structure(order.Value.CustomDef);
|
||||
if (algo_structure.Any(t => t.type == CustomOrderDefinition.CustomOneofCase.GlbxIceberg)) {
|
||||
Log.Ui.info($"OrdersModel prevented a GlbxIceberg order from going out to a non-glbx exchange ({OrderUtils.short_description(security, order.Value.Side, price, size)}).");
|
||||
return Task.FromResult<uint?>(null);
|
||||
}
|
||||
}
|
||||
return send_order(order.Value, price, size);
|
||||
}
|
||||
return 0;
|
||||
return Task.FromResult<uint?>(null);
|
||||
}
|
||||
|
||||
/// Note: this method is private to force outside callers to use this overload <see cref="send_order(SecurityImmutable, OrderDefinitionImmutable?, double, uint)"/>.
|
||||
/// This allows us to control which orders can be sent to which security/exchange (e.g., GlbxIceberg orders are only permitted for securities on the Glbx exchange).
|
||||
async Task<uint?> send_order(OrderDefinitionImmutable order, double price, uint size) {
|
||||
var result = (await send_order_get_error(order, price, size));
|
||||
_activity.on_send_order(order, price, size, result.key, result.error);
|
||||
return result.key;
|
||||
}
|
||||
|
||||
async Task<(uint? key, string error)> send_order_get_error(OrderDefinitionImmutable order, double price, uint size) {
|
||||
@@ -250,7 +324,7 @@ namespace mbts.spark {
|
||||
return _handle_error(result, "Error sending order: {0}");
|
||||
}
|
||||
|
||||
Task<(OrderRecordImmutable,bool)[]> cancel_orders(IEnumerable<uint> orders) =>
|
||||
Task<(OrderRecordImmutable, bool)[]> cancel_orders(IEnumerable<uint> orders) =>
|
||||
this.cancel_orders_where(o => orders.Contains(o.Key));
|
||||
|
||||
public async Task<bool> cancel_order(OrderRecordImmutable order) {
|
||||
@@ -334,7 +408,7 @@ namespace mbts.spark {
|
||||
_error.report_error(result.error_message);
|
||||
return result.value.Response.All(r => r.Ok);
|
||||
}
|
||||
#endregion // Manual Fills
|
||||
#endregion
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -388,13 +462,10 @@ namespace mbts.spark {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// usinig new OrdersModel extensions to reduce the amount of code change needed to switch uis over to the new OrdersModel,
|
||||
// without changing any code in the actual new OrdersModel class.
|
||||
static class OrdersModelExtensions {
|
||||
|
||||
#region Cancel Orders
|
||||
|
||||
public static async Task<bool> cancel_order(this OrdersModel orders_model, uint order_id) {
|
||||
var tu = await orders_model.on_orders.FirstAsync();
|
||||
if (tu.state.TryGetValue(order_id, out var o)) {
|
||||
@@ -408,7 +479,7 @@ namespace mbts.spark {
|
||||
var orders =
|
||||
tu.state.Values
|
||||
.Where(o => o.order.is_non_terminal() && pred(o.order))
|
||||
.Select(async o => { return (o.order, await model.cancel_order(o.order)); } );
|
||||
.Select(async o => { return (o.order, await model.cancel_order(o.order)); });
|
||||
return await Task.WhenAll(orders);
|
||||
}
|
||||
|
||||
@@ -420,7 +491,6 @@ namespace mbts.spark {
|
||||
|
||||
public static Task<(OrderRecordImmutable, bool)[]> cancel_security_sell_orders(this OrdersModel model, uint security) =>
|
||||
model.cancel_orders_where(o => o.Def.SecurityKey == security && o.Def.Side == Side.Sell);
|
||||
|
||||
#endregion
|
||||
|
||||
public static IObservable<bool> security_has_working_orders(this OrdersModel orders_model, uint security_key) =>
|
||||
@@ -431,26 +501,15 @@ namespace mbts.spark {
|
||||
|
||||
public static IObservable<double> security_total_position(this OrdersModel orders_model, uint security_key, string account) =>
|
||||
orders_model.on_security_positions(security_key)
|
||||
.Select((t,k) => {
|
||||
.Select((t, k) => {
|
||||
foreach (var kv in t.state) {
|
||||
if (kv.Value.Item2.Account == account)
|
||||
return kv.Value.Item2.total_pos();
|
||||
return kv.Value.Item2.total_pos();
|
||||
}
|
||||
return double.NaN;
|
||||
}).DistinctUntilChanged()
|
||||
.StartWith(double.NaN);
|
||||
|
||||
// DO NOT REMOVE (BEGIN)
|
||||
//public static IObservable<double> security_total_position(this OrdersModel orders_model, uint security_key, string account) =>
|
||||
// // PositionUnit.Account is actually the clearing account; this code assumes that clearing account and account are equivalent.
|
||||
// // However, it still must match account to PositionUnit.Account.Trimmed() because the exchange likes to slap spaces on the end.
|
||||
// orders_model.on_security_positions(security_key)
|
||||
// .filter(p => p.Account == account)
|
||||
// // the state should always have 1 element, since the PositionUnit-identity is security + account
|
||||
// .Scan(0.0, (pos, table) => pos + (table.added.Sum(p => p.total_pos()) - table.removed.Sum(p => p.total_pos())))
|
||||
// .StartWith(double.NaN);
|
||||
// DO NOT REMOVE (END)
|
||||
|
||||
public static IObservable<ITableUpdateTransform<OrderRecordImmutable, uint>> on_security_root_orders(this OrdersModel orders_model, uint security_key) =>
|
||||
orders_model.on_security_orders(security_key).AddRemoveOnly().Scan(new RootOrderData(), (rod, table) => {
|
||||
// ** using HashSet/List pairs, rather than a Dictionary, because TableUpdateTransform calls for IReadOnlyLists, while Dictionary.Values only implements IReadOnlyCollection.
|
||||
@@ -518,14 +577,12 @@ namespace mbts.spark {
|
||||
orders_model.studio_algos_with_meta.MergeTable(Observable.Return(
|
||||
new TableUpdate<(AlgoData algo, AlgoDefinitionMetadata? meta), AlgoKey>(
|
||||
ImmutableDictionary<AlgoKey, (AlgoData algo, AlgoDefinitionMetadata? meta)>.Empty.AddRange(orders_model.factory.default_algos(OrderDataHelper.all_order_types).Select(kv => new KeyValuePair<AlgoKey, (AlgoData algo, AlgoDefinitionMetadata? meta)>(kv.Key, (kv.Value, null)))),
|
||||
Array.Empty<(AlgoData,AlgoDefinitionMetadata?)>(),
|
||||
Array.Empty<Change<(AlgoData,AlgoDefinitionMetadata?)>>(),
|
||||
Array.Empty<(AlgoData,AlgoDefinitionMetadata?)>(),
|
||||
Array.Empty<(AlgoData, AlgoDefinitionMetadata?)>(),
|
||||
Array.Empty<Change<(AlgoData, AlgoDefinitionMetadata?)>>(),
|
||||
Array.Empty<(AlgoData, AlgoDefinitionMetadata?)>(),
|
||||
d => d.algo.key
|
||||
).as_snapshot()));
|
||||
|
||||
//public static IObservable<TableUpdate<AlgoData, AlgoKey>> subscribe_all_algos_with_meta(this OrdersModel orders_model) =>
|
||||
|
||||
public static async Task<OrderRecordImmutable?> order_from_key(this OrdersModel orders_model, uint order_key) {
|
||||
(var tu, var success) = await orders_model.on_orders.FirstAsyncTask(t => t.state.ContainsKey(order_key), TimeSpan.FromSeconds(3));
|
||||
if (success && tu.state.TryGetValue(order_key, out var rec)) {
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
<ResourceDictionary Source="/view/Resources/MenuItems.xaml" />
|
||||
<ResourceDictionary Source="/view/Controls/ToolBar/ToolBar.xaml" />
|
||||
<ResourceDictionary Source="/view/Controls/EditableTextBlock/EditableTextBlock.xaml"/>
|
||||
<ResourceDictionary Source="/view/Controls/Buttons/ExpanderToggleButton.xaml"/>
|
||||
<ResourceDictionary Source="/view/Controls/DragDropItemsControl/DragDropItemsControl.xaml" />
|
||||
<ResourceDictionary Source="/view/Controls/DragDropItemsControl/DraggableContentControl.xaml" />
|
||||
<ResourceDictionary Source="/view/Controls/DragDropItemsControl/DragDropListBox.xaml" />
|
||||
@@ -338,7 +339,7 @@
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Auto"/>
|
||||
<Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto"/>
|
||||
<Setter Property="Padding" Value="3,3,1,3"/>
|
||||
<Setter Property="Padding" Value="6,3,1,3"/>
|
||||
<Setter Property="ScrollViewer.CanContentScroll" Value="true"/>
|
||||
<Setter Property="ScrollViewer.PanningMode" Value="Both"/>
|
||||
<Setter Property="Stylus.IsFlicksEnabled" Value="False"/>
|
||||
|
||||
@@ -35,7 +35,10 @@ namespace bts.spark {
|
||||
SparkCommand.create("Manual Fills", Icons.DataEdit, SparkTags.ShowManualFillsTag, key_gesture);
|
||||
|
||||
public static PartialCommand<PAndLWindowFactory.PAndLStartupData> ShowPAndL =>
|
||||
SparkCommand.create("P&L", Icons.Accounting, SparkTags.ShowPAndLTag);
|
||||
SparkCommand.create("P&L (1.0)", Icons.Accounting, SparkTags.ShowPAndLTag);
|
||||
|
||||
public static PartialCommand<shell.pnl.PNLSettings> ShowPNL =>
|
||||
SparkCommand.create("P&L (2.0)", Icons.Accounting, SparkTags.ShowPNLTag);
|
||||
|
||||
// Ladder
|
||||
public static PartialCommand<SecurityMD> ShowLadder(KeyGesture key_gesture = null) =>
|
||||
|
||||
@@ -8,19 +8,15 @@ using mbts.ui;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using static mbts.spark.SparkBusData;
|
||||
|
||||
namespace mbts.spark
|
||||
{
|
||||
static class SparkTags
|
||||
{
|
||||
namespace mbts.spark {
|
||||
static class SparkTags {
|
||||
#region Core Data
|
||||
public static ITag<IReadOnlyDictionary<uint, SecurityMD>> SecuritiesTag { get; } = new SparkTag<IReadOnlyDictionary<uint, SecurityMD>>("Securities");
|
||||
public static ITag<IReadOnlyDictionary<uint, ExecutionStrategyImmutable>> SyntheticstrategiesTag { get; } = new SparkTag<IReadOnlyDictionary<uint, ExecutionStrategyImmutable>>("SyntheticSecuritiesTag");
|
||||
#endregion
|
||||
|
||||
#region Order
|
||||
public static ITag<(OrderDefinitionImmutable definition, double price, uint size)> CreateOrder { get; } = new SparkTag<(OrderDefinitionImmutable, double, uint)>();
|
||||
public static ITag<IEnumerable<(OrderDefinitionImmutable definition, double price, uint size)>> RecreateOrder { get; } = new SparkTag<IEnumerable<(OrderDefinitionImmutable, double, uint)>>();
|
||||
|
||||
public static ITag<IEnumerable<uint>> CancelOrders { get; } = new SparkTag<IEnumerable<uint>>();
|
||||
@@ -64,6 +60,7 @@ namespace mbts.spark
|
||||
public static ITag<SecurityCriteria> ShowMarketViewTag { get; } = new SparkTag<SecurityCriteria>("Show MarketView");
|
||||
public static ITag<bts.shell.orderbook.OrderBookStartupData> ShowOrderBookTag { get; } = new SparkTag<bts.shell.orderbook.OrderBookStartupData>(nameof(ShowOrderBookTag));
|
||||
public static ITag<PAndLWindowFactory.PAndLStartupData> ShowPAndLTag { get; } = new SparkTag<PAndLWindowFactory.PAndLStartupData>(nameof(ShowPAndLTag));
|
||||
public static ITag<bts.shell.pnl.PNLSettings> ShowPNLTag { get; } = new SparkTag<bts.shell.pnl.PNLSettings>(nameof(ShowPNLTag));
|
||||
public static ITag<(SecurityImmutable security, ExecutionStrategyImmutable strategy)> ShowSyntheticCloneTag { get; } = new SparkTag<(SecurityImmutable security, ExecutionStrategyImmutable strategy)>(nameof(ShowSyntheticCloneTag));
|
||||
public static ITag<SecurityCriteria> ShowSyntheticCreateTag { get; } = new SparkTag<SecurityCriteria>(nameof(ShowSyntheticCreateTag));
|
||||
public static ITag<SecurityImmutable> ShowSyntheticStrategyTag { get; } = new SparkTag<SecurityImmutable>(nameof(ShowSyntheticStrategyTag));
|
||||
@@ -91,4 +88,4 @@ namespace mbts.spark
|
||||
public override string ToString() => Name;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -200,17 +200,20 @@ namespace mbts.collections {
|
||||
public ObservableCollectionEx<T> selected_items { get; } = new ObservableCollectionEx<T>();
|
||||
public IEnumerable<TKey> selected_keys => selected_items.Select(_key_selector);
|
||||
|
||||
public bool try_select(TKey key) {
|
||||
public bool try_select(TKey key) =>
|
||||
try_select(key, true);
|
||||
|
||||
public bool try_select(TKey key, bool reattempt_on_collection_change) {
|
||||
if (items.TryGetFirst(i => _key_selector(i).Equals(key), out T result)) {
|
||||
selected_items.Add(result);
|
||||
return true;
|
||||
} else {
|
||||
} else if (reattempt_on_collection_change) {
|
||||
if (_desired_keys.Count == 0) {
|
||||
items.CollectionChanged += _on_items_CollectionChanged;
|
||||
}
|
||||
_desired_keys.Add(key);
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void _on_items_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) {
|
||||
|
||||
@@ -22,6 +22,12 @@ namespace mbts.ui.utility {
|
||||
public static IReadOnlyObservableCollection<T> create_sorted_wrapper<T>(ObservableCollection<T> source, IComparer<T> comparer) =>
|
||||
new SortedWrapper<T>(source, source, comparer);
|
||||
|
||||
public static IReadOnlyObservableCollection<TResult> CastObservable<T, TResult>(this ObservableCollection<T> source) =>
|
||||
new SelectorWrapper<T, TResult>(source, source, ConvertUtils.CastDelegate<T, TResult>.value);
|
||||
|
||||
public static IReadOnlyObservableCollection<TResult> SelectObservable<T, TResult>(this ObservableCollection<T> source, Func<T, TResult> selector) =>
|
||||
new SelectorWrapper<T, TResult>(source, source, selector);
|
||||
|
||||
public static IDisposable connect_sorted<T>(ObservableCollection<T> source, ObservableCollection<T> target, IComparer<T> comparer) {
|
||||
target.sync_with(source.Ordered(comparer));
|
||||
return source.Subscribe(on_source_collection_changed);
|
||||
@@ -87,7 +93,9 @@ namespace mbts.ui.utility {
|
||||
connect<ObservableCollection<T>, T, TResult>(source, target, create, null);
|
||||
|
||||
public static IDisposable connect<TCollection, T, TResult>(TCollection source, IList<TResult> target, Func<T, TResult> create, Action<TResult> on_removed = null)
|
||||
where TCollection : IReadOnlyList<T>, INotifyCollectionChanged {
|
||||
where TCollection : IReadOnlyList<T>, INotifyCollectionChanged => connect(source, source, target, create, on_removed);
|
||||
|
||||
public static IDisposable connect<T, TResult>(IReadOnlyList<T> source_list, INotifyCollectionChanged source_notifier, IList<TResult> target, Func<T, TResult> create, Action<TResult> on_removed = null) {
|
||||
Action reset_cb;
|
||||
Action<IList, int> removed_cb,
|
||||
added_cb = (new_items, i) => {
|
||||
@@ -100,7 +108,7 @@ namespace mbts.ui.utility {
|
||||
}
|
||||
};
|
||||
if (on_removed == null) {
|
||||
reset_cb = () => target.SetItems(source.Select(create));
|
||||
reset_cb = () => target.SetItems(source_list.Select(create));
|
||||
removed_cb = (old_items, i) => {
|
||||
if (i < 0) {
|
||||
throw new InvalidOperationException("Source collection invoked a CollectionChanged 'Removed' event without a valid 'OldStartingIndex.'");
|
||||
@@ -113,7 +121,7 @@ namespace mbts.ui.utility {
|
||||
reset_cb = () => {
|
||||
foreach (var v in target)
|
||||
on_removed(v);
|
||||
target.SetItems(source.Select(create));
|
||||
target.SetItems(source_list.Select(create));
|
||||
};
|
||||
removed_cb = (old_items, i) => {
|
||||
if (i < 0) {
|
||||
@@ -134,8 +142,8 @@ namespace mbts.ui.utility {
|
||||
added_cb(new_items, new_i);
|
||||
},
|
||||
on_reset: reset_cb);
|
||||
collection_changed_cb(source, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, (IList)source));
|
||||
return source.Subscribe(collection_changed_cb);
|
||||
reset_cb();
|
||||
return source_notifier.Subscribe(collection_changed_cb);
|
||||
}
|
||||
|
||||
public static IDisposable connect<T>(IObservable<ObservableCollection<T>> source, ObservableCollection<T> target) {
|
||||
@@ -209,11 +217,21 @@ namespace mbts.ui.utility {
|
||||
return notifier.Subscribe((s, e) => CollectionHelper.process_new_items(collection, action, e));
|
||||
}
|
||||
|
||||
public static IDisposable connect_item_hooks<T>(IReadOnlyObservableCollection<T> source, Func<T, IDisposable> get_item_hook) {
|
||||
var item_hooks = source.Select(get_item_hook).ToList();
|
||||
public static IDisposable connect_item_hooks<T>(IReadOnlyObservableCollection<T> source, Func<T, IDisposable> get_item_hook) =>
|
||||
connect_item_hooks(source, source, get_item_hook);
|
||||
|
||||
public static IDisposable connect_item_hooks<T>(IReadOnlyList<T> source_list, INotifyCollectionChanged source_notifier, Func<T, IDisposable> get_item_hook) {
|
||||
var item_hooks = source_list.Select(get_item_hook).ToList();
|
||||
return new CompositeDisposable(
|
||||
Disposable.wrap_collection(item_hooks),
|
||||
source.Subscribe((s, e) => CollectionHelper.reflect_change(item_hooks, source, get_item_hook, d => d.Dispose(), e)));
|
||||
source_notifier.Subscribe((s, e) => CollectionHelper.reflect_change(item_hooks, source_list, get_item_hook, d => d.Dispose(), e)));
|
||||
}
|
||||
|
||||
public static IDisposable connect_item_hooks<T>(IReadOnlyList<T> source_list, INotifyCollectionChanged source_notifier, Func<T, int, IDisposable> get_item_hook) {
|
||||
var item_hooks = source_list.Select(get_item_hook).ToList();
|
||||
return new CompositeDisposable(
|
||||
Disposable.wrap_collection(item_hooks),
|
||||
source_notifier.Subscribe((s, e) => CollectionHelper.reflect_change(item_hooks, source_list, get_item_hook, d => d.Dispose(), e)));
|
||||
}
|
||||
|
||||
public static IDisposable connect_item_hooks<T>(ObservableCollection<T> source, Func<T, IDisposable> get_item_hook, bool clear_source_on_dispose = false) {
|
||||
@@ -352,6 +370,46 @@ namespace mbts.ui.utility {
|
||||
public IEnumerator<T> GetEnumerator() => _source.GetEnumerator();
|
||||
IEnumerator IEnumerable.GetEnumerator() => _source.GetEnumerator();
|
||||
}
|
||||
|
||||
class SelectorWrapper<T, TResult> : IReadOnlyObservableCollection<TResult> {
|
||||
readonly IReadOnlyList<T> _source_list;
|
||||
readonly Func<T, TResult> _selector;
|
||||
|
||||
public SelectorWrapper(IReadOnlyList<T> source_list, INotifyCollectionChanged source_notifier, Func<T, TResult> selector) {
|
||||
_source_list = source_list;
|
||||
_selector = selector;
|
||||
source_notifier.CollectionChanged += on_collection_changed;
|
||||
void on_collection_changed(object sender, NotifyCollectionChangedEventArgs e) {
|
||||
var cc = CollectionChanged;
|
||||
if (cc == null)
|
||||
return;
|
||||
NotifyCollectionChangedEventArgs re;
|
||||
switch (e.Action) {
|
||||
case NotifyCollectionChangedAction.Add: re = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, e.NewItems.WrapSelectorHidden(from_obj), e.NewStartingIndex); break;
|
||||
case NotifyCollectionChangedAction.Remove: re = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, e.OldItems.WrapSelectorHidden(from_obj), e.OldStartingIndex); break;
|
||||
case NotifyCollectionChangedAction.Move: re = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Move, e.NewItems.WrapSelectorHidden(from_obj), e.NewStartingIndex, e.OldStartingIndex); break;
|
||||
case NotifyCollectionChangedAction.Replace: re = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, e.NewItems.WrapSelectorHidden(from_obj), e.OldItems.WrapSelectorHidden(from_obj), e.NewStartingIndex); break;
|
||||
case NotifyCollectionChangedAction.Reset: re = e; break;
|
||||
default: throw ExceptionBuilder.Argument.unexpected_case(e.Action, nameof(e));
|
||||
}
|
||||
cc(this, re);
|
||||
}
|
||||
TResult from_obj(object obj) =>
|
||||
selector((T)obj);
|
||||
}
|
||||
|
||||
public TResult this[int index] => _selector(_source_list[index]);
|
||||
|
||||
public int Count => _source_list.Count;
|
||||
|
||||
public event NotifyCollectionChangedEventHandler CollectionChanged;
|
||||
|
||||
public IEnumerator<TResult> GetEnumerator() {
|
||||
foreach (var v in _source_list)
|
||||
yield return _selector(v);
|
||||
}
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
}
|
||||
}
|
||||
|
||||
public class ObservableAutoWrapper<View, Model> : ObservableCollection<View>
|
||||
|
||||
@@ -1,15 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace mbts.expression
|
||||
{
|
||||
namespace mbts.expression {
|
||||
// Implements the language used in bts_core/Config
|
||||
public class CoreExpressionEvaluator<T> : IExpressionEvaluator<T>
|
||||
{
|
||||
public class CoreExpressionEvaluator<T> : IExpressionEvaluator<T> {
|
||||
private Func<T, bool> _apply;
|
||||
|
||||
public CoreExpressionEvaluator(string expression) {
|
||||
@@ -29,10 +22,35 @@ namespace mbts.expression
|
||||
}
|
||||
}
|
||||
|
||||
CoreExpressionEvaluator(Func<T, bool> apply, string expression, string parsed_expression) {
|
||||
_apply = apply;
|
||||
Expression = expression;
|
||||
ParsedExpression = parsed_expression;
|
||||
}
|
||||
|
||||
public static bool try_create(string expression, out CoreExpressionEvaluator<T> result) {
|
||||
try {
|
||||
var parameter = System.Linq.Expressions.Expression.Parameter(typeof(T));
|
||||
var parser = new CoreExpressionParser();
|
||||
if (parser.try_parse(expression, out var ast)) {
|
||||
var body = ast.create_expression(parameter);
|
||||
var parsed = body.ToString();
|
||||
var lambda = System.Linq.Expressions.Expression.Lambda<Func<T, bool>>(body, parameter);
|
||||
result = new CoreExpressionEvaluator<T>(lambda.Compile(), expression, parsed);
|
||||
return true;
|
||||
}
|
||||
} catch { }
|
||||
result = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool validate(string expression) =>
|
||||
try_create(expression, out var _);
|
||||
|
||||
public string Expression { get; private set; }
|
||||
public string ParsedExpression { get; private set; }
|
||||
public bool apply(T arg) {
|
||||
return _apply(arg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,17 +4,28 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace mbts.expression
|
||||
{
|
||||
namespace mbts.expression {
|
||||
// This is a re-implementation of the bts_core Expression parser in c#.
|
||||
// It compiles down to LINQ expression trees which means the code is JITed at runtime for very fast execution
|
||||
class CoreExpressionParser
|
||||
{
|
||||
class CoreExpressionParser {
|
||||
// A cached stringbuilder
|
||||
StringBuilder result = new StringBuilder();
|
||||
|
||||
internal bool try_parse(string str, out Token result) {
|
||||
if (string.IsNullOrWhiteSpace(str)) {
|
||||
result = null;
|
||||
} else {
|
||||
var l = lex(str);
|
||||
try {
|
||||
result = tree(l, false);
|
||||
} catch {
|
||||
result = null;
|
||||
}
|
||||
}
|
||||
return result != null;
|
||||
}
|
||||
|
||||
internal Token parse(string str) {
|
||||
if (string.IsNullOrWhiteSpace(str)) return null;
|
||||
var l = lex(str);
|
||||
@@ -158,22 +169,22 @@ namespace mbts.expression
|
||||
}
|
||||
return new Number(result.ToString());
|
||||
}
|
||||
#endregion
|
||||
#endregion
|
||||
|
||||
#region Recurively convert the Token list to an expression tree
|
||||
|
||||
private Token tree(IEnumerable<Token> tokens) {
|
||||
private Token tree(IEnumerable<Token> tokens, bool assert_valid = true) {
|
||||
var t = tokens.ToList();
|
||||
|
||||
// Handle groups by recursion
|
||||
for (int x = 0; x < t.Count; ++x) {
|
||||
if (t[x] is GroupToken) {
|
||||
t[x] = tree(((GroupToken)t[x]).Items);
|
||||
t[x] = tree(((GroupToken)t[x]).Items, assert_valid);
|
||||
}
|
||||
}
|
||||
|
||||
// This has to be a separate pass for "True == !False"
|
||||
for (int x = t.Count-1; x >= 0; --x) {
|
||||
for (int x = t.Count - 1; x >= 0; --x) {
|
||||
if (t[x] is NotOperator) {
|
||||
NotOperator no = (NotOperator)t[x];
|
||||
no.Child = t[x + 1];
|
||||
@@ -187,7 +198,11 @@ namespace mbts.expression
|
||||
if (t[x] is CompareOperator) {
|
||||
CompareOperator b = (CompareOperator)t[x];
|
||||
if (b.Left != null) {
|
||||
Log.assert(b.Right != null);
|
||||
if (b.Right == null) {
|
||||
if (assert_valid) {
|
||||
Log.assert(false);
|
||||
} else throw new InvalidOperationException("CompareOperator right side operand is null.");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
b.Left = t[x - 1];
|
||||
@@ -213,8 +228,11 @@ namespace mbts.expression
|
||||
x--;
|
||||
}
|
||||
}
|
||||
|
||||
Log.assert(t.Count == 1);
|
||||
if (t.Count != 1) {
|
||||
if (assert_valid) {
|
||||
Log.assert(false);
|
||||
} else throw new InvalidOperationException("Expression tree was not reduced to a single token.");
|
||||
}
|
||||
return t.First();
|
||||
}
|
||||
#endregion
|
||||
@@ -289,7 +307,8 @@ namespace mbts.expression
|
||||
typeof(UInt32),
|
||||
typeof(Int64),
|
||||
typeof(UInt64),
|
||||
typeof(float)
|
||||
typeof(float),
|
||||
typeof(decimal)
|
||||
};
|
||||
|
||||
if (numeric_types.Contains(expr.Type)) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Windows.Media;
|
||||
using System.Collections.Generic;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace mbts.ui {
|
||||
// Usage
|
||||
@@ -19,13 +20,16 @@ namespace mbts.ui {
|
||||
|
||||
[DisplayInfo(name = "Default Header")]
|
||||
public static BTSColor header { get { return BTSCoreColors.base_blue; } }
|
||||
public static Color header_color => header;
|
||||
public static Brush header_brush { get { return _header_brush; } }
|
||||
public static BTSColor header_text { get { return BTSCoreColors.white; } }
|
||||
public static Color header_text_color => header_text;
|
||||
public static BTSColor header_error_text { get; } = new BTSColor(0xFE, 0x6A, 0x6E);
|
||||
public static Brush header_error_text_brush => _header_error_text_brush;
|
||||
public static BTSColor header_border { get { return new BTSColor(0x63,0x8A,0xCC); } }
|
||||
public static Brush header_text_brush { get { return _header_text_brush; } }
|
||||
public static BTSColor header_alternate { get { return BTSCoreColors.blue_grey; } }
|
||||
public static Color header_alternate_color => header_alternate;
|
||||
public static BTSColor disabled_text { get { return BTSCoreColors.gray_light; } }
|
||||
|
||||
public static BTSColor pinkalicious { get { return new BTSColor(0xff, 0x33, 0x99); } }
|
||||
@@ -98,6 +102,8 @@ namespace mbts.ui {
|
||||
public static Brush semi_transparent_bid_brush => _semi_transparent_bid_brush;
|
||||
public static Brush semi_transparent_ask_brush => _semi_transparent_ask_brush;
|
||||
|
||||
public static Color bid_color => bid;
|
||||
public static Color ask_color => ask;
|
||||
|
||||
[DisplayInfo(name = "Settlement")]
|
||||
public static BTSColor settlement_background { get { return Grid.default_column_background; } }
|
||||
@@ -139,19 +145,25 @@ namespace mbts.ui {
|
||||
|
||||
}
|
||||
|
||||
public static class RiskReport
|
||||
{
|
||||
// Base Colors
|
||||
[DisplayInfo(name = "Risk Report")]
|
||||
public static BTSColor default_background { get { return BTSCoreColors.blue_light_dark; } }
|
||||
public static BTSColor default_text { get { return BTSCoreColors.black; } }
|
||||
public static class PNL {
|
||||
public static Color anchor_color => Colors.Gold;
|
||||
public static Color profit_color => Colors.Green;
|
||||
public static Color loss_color => BTSColors.red;
|
||||
public static Color header_row_background_color => 0xFFAAAAAA.argb_color();
|
||||
// group background colors
|
||||
public static Color group_row_background_color1 => 0xFFE9E9E9.argb_color();
|
||||
public static Color group_row_background_color2 => 0xFFCACACA.argb_color();
|
||||
public static Color group_row_background_color3 => 0xFF9DB0C1.argb_color();
|
||||
public static Color group_row_background_color4 => group_row_background_color1;
|
||||
|
||||
[DisplayInfo(name = "Risk PnL")]
|
||||
public static BTSColor pnl_background { get { return Grid.default_column_background; } }
|
||||
|
||||
// Alerts
|
||||
public static BTSColor pnl_loss_text { get { return BTSCoreColors.red; } }
|
||||
public static BTSColor pnl_profit_text { get { return text_default; } }
|
||||
public static IEnumerable<Color> group_alternating_background_colors {
|
||||
get {
|
||||
yield return group_row_background_color1;
|
||||
yield return group_row_background_color2;
|
||||
yield return group_row_background_color3;
|
||||
yield return group_row_background_color4;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class Orders {
|
||||
|
||||
@@ -81,6 +81,10 @@ namespace mbts {
|
||||
added = newval.Except(current, comp);
|
||||
removed = current.Except(newval, comp);
|
||||
}
|
||||
public static void difference_hard<T>(this IEnumerable<T> current, IEnumerable<T> newval, out IList<T> added, out IList<T> removed, IEqualityComparer<T> comp) {
|
||||
added = newval.Except(current, comp).ToList();
|
||||
removed = current.Except(newval, comp).ToList();
|
||||
}
|
||||
public static int indexOf<T>(this IEnumerable me, T value) {
|
||||
IEnumerator enumerator = me.GetEnumerator();
|
||||
int i = 0;
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using mbts.ui.settings;
|
||||
using bts.shell.pnl;
|
||||
using bts.utils;
|
||||
using mbts.ui.settings;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
@@ -11,8 +13,10 @@ namespace bts.settings {
|
||||
// should be exposed through explicitly named interfaces that inherit ISettingsMigrationRule<T>.
|
||||
// This avoids magic const names in the MEF import/export contracts, which would otherwise be needed to
|
||||
// determine the various windows to which such rules apply.
|
||||
interface IPAndLGridSettingsMigrationRule : ISettingsMigrationRule<GridSettings> { }
|
||||
interface IFillsGridSettingsMigrationRule : ISettingsMigrationRule<GridSettings> { }
|
||||
interface IMarketViewGridSettingsMigrationRule : ISettingsMigrationRule<GridSettings> { }
|
||||
interface IPAndLGridSettingsMigrationRule : ISettingsMigrationRule<GridSettings> { }
|
||||
interface IPNLSettingsMigrationRule : ISettingsMigrationRule<PNLSettings> { }
|
||||
|
||||
static class SettingsMigrationHelper {
|
||||
public static bool apply<T>(T settings, IEnumerable<ISettingsMigrationRule<T>> migration_rules, out T new_settings) {
|
||||
@@ -35,6 +39,9 @@ namespace bts.settings {
|
||||
public static ISettingsMigrationRule<T> merge<T>(this IEnumerable<ISettingsMigrationRule<T>> rules) =>
|
||||
new MergeWrapper<T>(rules);
|
||||
|
||||
public static ISettingsMigrationRule<TOuter> wrap<TInner, TOuter>(this ISettingsMigrationRule<TInner> rule, IImmutableProperty<TOuter, TInner> propi) =>
|
||||
new SelectorWrapper<TInner, TOuter>(rule, propi.get, propi.set);
|
||||
|
||||
public static ISettingsMigrationRule<TOuter> wrap<TInner, TOuter>(this ISettingsMigrationRule<TInner> rule, Func<TOuter, TInner> get, Func<TOuter, TInner, TOuter> set) =>
|
||||
new SelectorWrapper<TInner, TOuter>(rule, get, set);
|
||||
|
||||
@@ -69,10 +76,12 @@ namespace bts.settings {
|
||||
}
|
||||
|
||||
public bool apply(TOuter settings, out TOuter new_settings) {
|
||||
var ival = _get(settings);
|
||||
if (_rule.apply(ival, out var new_ival)) {
|
||||
new_settings = _set(settings, new_ival);
|
||||
return true;
|
||||
if (settings != null) {
|
||||
var ival = _get(settings);
|
||||
if (_rule.apply(ival, out var new_ival)) {
|
||||
new_settings = _set(settings, new_ival);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
new_settings = default;
|
||||
return false;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Google.Protobuf.WellKnownTypes;
|
||||
using bts.shell.pnl;
|
||||
using Google.Protobuf.WellKnownTypes;
|
||||
using mbts.ui.settings;
|
||||
using ProtoBuf;
|
||||
using System;
|
||||
@@ -56,9 +57,8 @@ namespace mbts.spark {
|
||||
GridSettings _pnl_grid;
|
||||
[ProtoMember(10)]
|
||||
bts.shell.orderbook.OrderBookGridSettings _order_book_grid;
|
||||
// Placeholder for PNLSettings (will be merged to master soon)
|
||||
// [ProtoMember(13)]
|
||||
// PNLSettings _pnl;
|
||||
[ProtoMember(13)]
|
||||
PNLSettings _pnl;
|
||||
[ProtoMember(14)]
|
||||
GlobalAlertSettings _alerts;
|
||||
|
||||
@@ -76,6 +76,7 @@ namespace mbts.spark {
|
||||
public GridSettings fills_grid => _fills_grid ?? (_fills_grid = bts.shell.fills.FillsSettings.DefaultGridSettings);
|
||||
public GridSettings synthetic_management_grid => _synthetic_management_grid ?? (_synthetic_management_grid = bts.shell.SyntheticManagementWindowFactory.StartupData.DefaultGridSettings);
|
||||
public GridSettings pnl_grid => _pnl_grid ?? (_pnl_grid = bts.shell.PAndLWindowFactory.PAndLStartupData.DefaultGridSettings);
|
||||
public PNLSettings pnl => _pnl ?? (_pnl = PNLSettings.Default);
|
||||
public bts.shell.orderbook.OrderBookGridSettings order_book_grid => _order_book_grid ?? (_order_book_grid = bts.shell.orderbook.OrderBookStartupData.Default.grid_settings_wrapper());
|
||||
|
||||
public GlobalAlertSettings alerts => _alerts ?? (_alerts = new GlobalAlertSettings());
|
||||
@@ -88,6 +89,7 @@ namespace mbts.spark {
|
||||
GridSettings provided_fills_gridsettings = null,
|
||||
GridSettings provided_synthetic_management_gridsettings = null,
|
||||
GridSettings provided_pnl_gridsettings = null,
|
||||
PNLSettings pnl = null,
|
||||
bts.shell.orderbook.OrderBookGridSettings order_book_grid = null,
|
||||
GlobalAlertSettings alerts = null) {
|
||||
var us = new UserSettings(ladder_profiles ?? _ladder_profiles) {
|
||||
@@ -98,6 +100,7 @@ namespace mbts.spark {
|
||||
_fills_grid = provided_fills_gridsettings ?? _fills_grid,
|
||||
_synthetic_management_grid = provided_synthetic_management_gridsettings ?? _synthetic_management_grid,
|
||||
_pnl_grid = provided_pnl_gridsettings ?? _pnl_grid,
|
||||
_pnl = pnl ?? _pnl,
|
||||
_order_book_grid = order_book_grid ?? _order_book_grid,
|
||||
_alerts = alerts ?? _alerts
|
||||
};
|
||||
@@ -184,4 +187,4 @@ namespace mbts.spark {
|
||||
public TimeSpan? duration => _mode == Mode.Flash ? _duration?.ToTimeSpan() ?? TimeSpan.FromSeconds(5) : new TimeSpan?();
|
||||
public double max_height => _max_height;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using bts;
|
||||
using bts.settings;
|
||||
using bts.shell.pnl;
|
||||
using bts.utils;
|
||||
using mbts.spark;
|
||||
using mbts.ui.settings;
|
||||
@@ -20,22 +21,38 @@ namespace mbts.persist {
|
||||
class UserSettingsHub {
|
||||
// do NOT CHANGE these consts, they represent a file path on the server
|
||||
const string NAME_SPACE = "settings", DEFAULT_KEY = "default";
|
||||
IUserPersist _persist;
|
||||
UserSettings _default_settings;
|
||||
readonly BehaviorSubject<UserSettings> _default_settings_subject = new BehaviorSubject<UserSettings>(UserSettings.factory_defaults);
|
||||
|
||||
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));
|
||||
|
||||
static readonly IImmutableProperty<UserSettings, PNLSettings>
|
||||
_pnl_propi = ImmutableProperty.create<UserSettings, PNLSettings>(us => us.pnl, (us, pnl) => us.with(pnl: pnl));
|
||||
|
||||
readonly object _default_settings_lock = new object();
|
||||
readonly IEnumerable<IPAndLGridSettingsMigrationRule> _pandl_migration_rules;
|
||||
readonly BehaviorSubject<UserSettings> _default_settings_subject = new BehaviorSubject<UserSettings>(UserSettings.factory_defaults);
|
||||
readonly IEnumerable<IFillsGridSettingsMigrationRule> _fills_migration_rules;
|
||||
readonly IEnumerable<IMarketViewGridSettingsMigrationRule> _mview_migration_rules;
|
||||
readonly IEnumerable<IPAndLGridSettingsMigrationRule> _pandl_migration_rules;
|
||||
readonly IEnumerable<IPNLSettingsMigrationRule> _pnl_migration_rules;
|
||||
readonly IUserPersist _persist;
|
||||
|
||||
UserSettings _default_settings;
|
||||
|
||||
[ImportingConstructor]
|
||||
public UserSettingsHub(
|
||||
IUserPersist persist,
|
||||
[ImportMany]IEnumerable<IFillsGridSettingsMigrationRule> fills_migration_rules,
|
||||
[ImportMany]IEnumerable<IMarketViewGridSettingsMigrationRule> mview_migration_rules,
|
||||
[ImportMany]IEnumerable<IPAndLGridSettingsMigrationRule> pandl_migration_rules,
|
||||
[ImportMany]IEnumerable<IMarketViewGridSettingsMigrationRule> mview_migration_rules) {
|
||||
[ImportMany]IEnumerable<IPNLSettingsMigrationRule> pnl_migration_rules) {
|
||||
//
|
||||
_persist = persist;
|
||||
_pandl_migration_rules = pandl_migration_rules ?? Enumerable.Empty<IPAndLGridSettingsMigrationRule>();
|
||||
_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>();
|
||||
init();
|
||||
async void init() {
|
||||
UserSettings s;
|
||||
@@ -43,8 +60,9 @@ namespace mbts.persist {
|
||||
s = await _persist.get<UserSettings>(NAME_SPACE, DEFAULT_KEY);
|
||||
if (s != null) {
|
||||
var wrapped_rules = new[] {
|
||||
_pandl_migration_rules.merge().wrap<GridSettings, UserSettings>(us => us.pnl_grid, (us, gs) => us.with(provided_pnl_gridsettings: gs)),
|
||||
_mview_migration_rules.merge().wrap<GridSettings, UserSettings>(us => us.market_view_grid, (us, gs) => us.with(provided_market_view_gridsettings: gs))
|
||||
_fills_migration_rules.merge().wrap(_fills_propi),
|
||||
_mview_migration_rules.merge().wrap(_mview_propi),
|
||||
_pandl_migration_rules.merge().wrap(_pandl_propi)
|
||||
};
|
||||
SettingsMigrationHelper.apply_quiet(ref s, wrapped_rules);
|
||||
// TODO: move ladder sanitization logic into ISettingsMigrationRule
|
||||
@@ -68,7 +86,7 @@ namespace mbts.persist {
|
||||
if (!sorted_lps.SequenceEqual(s.ladder_profiles))
|
||||
await _persist.set(NAME_SPACE, DEFAULT_KEY, s = s.with(ladder_profiles: sorted_lps));
|
||||
}
|
||||
} catch(Exception ex) {
|
||||
} catch (Exception ex) {
|
||||
Log.Persist.warn($"Error loading {typeof(UserSettings)} from namespace '{NAME_SPACE}', will restore factory defaults and continue as usual: {ex}");
|
||||
await save_default(s = UserSettings.factory_defaults);
|
||||
}
|
||||
@@ -83,10 +101,6 @@ namespace mbts.persist {
|
||||
// indicates whether or not the settings have been loaded from the server (this happens once, on startup - see the constructor for details)
|
||||
public bool initialized { get; private set; }
|
||||
|
||||
/// Subscribing via _persist.subscribe may be cleaner in theory, however using a local event allows observers to be notified immediately.
|
||||
/// Assuming there aren't any rogue agents saving UserSettings by themselves, there should never be any discrepancies.
|
||||
public IObservable<UserSettings> subscribe_default_settings() => _default_settings_subject.AsObservable();
|
||||
|
||||
internal UserSettings current {
|
||||
get {
|
||||
lock (_default_settings_lock)
|
||||
@@ -94,19 +108,33 @@ namespace mbts.persist {
|
||||
}
|
||||
}
|
||||
|
||||
public IWindowSettingsManager<GridSettings> create_pandl_settings_manager(GridSettings instance_settings) =>
|
||||
/// Subscribing via _persist.subscribe may be cleaner in theory, however using a local event allows observers to be notified immediately.
|
||||
/// Assuming there aren't any rogue agents saving UserSettings by themselves, there should never be any discrepancies.
|
||||
public IObservable<UserSettings> subscribe_default_settings() => _default_settings_subject.AsObservable();
|
||||
|
||||
public IWindowSettingsManager<GridSettings> create_fills_settings_manager(GridSettings instance_settings) =>
|
||||
_create_settings_manager(
|
||||
instance_settings,
|
||||
_pandl_migration_rules,
|
||||
us => us.pnl_grid,
|
||||
(us, gs) => us.with(provided_pnl_gridsettings: gs));
|
||||
_fills_migration_rules,
|
||||
_fills_propi);
|
||||
|
||||
public IWindowSettingsManager<GridSettings> create_mview_settings_manager(GridSettings instance_settings) =>
|
||||
_create_settings_manager(
|
||||
instance_settings,
|
||||
_mview_migration_rules,
|
||||
us => us.market_view_grid,
|
||||
(us, gs) => us.with(provided_market_view_gridsettings: gs));
|
||||
_mview_propi);
|
||||
|
||||
public IWindowSettingsManager<GridSettings> create_pandl_settings_manager(GridSettings instance_settings) =>
|
||||
_create_settings_manager(
|
||||
instance_settings,
|
||||
_pandl_migration_rules,
|
||||
_pandl_propi);
|
||||
|
||||
public IWindowSettingsManager<PNLSettings> create_pnl_settings_manager(PNLSettings instance_settings) =>
|
||||
_create_settings_manager(
|
||||
instance_settings,
|
||||
_pnl_migration_rules,
|
||||
_pnl_propi);
|
||||
|
||||
public async Task save_default(UserSettings value) {
|
||||
// notify observers before persisting so the changes are reflected immediately in the ui
|
||||
@@ -116,17 +144,17 @@ namespace mbts.persist {
|
||||
await _persist_async();
|
||||
}
|
||||
|
||||
IWindowSettingsManager<T> _create_settings_manager<T>(T instance_settings, IEnumerable<ISettingsMigrationRule<T>> migration_rules, Func<UserSettings, T> get, Func<UserSettings, T, UserSettings> set) where T : class {
|
||||
IWindowSettingsManager<T> _create_settings_manager<T>(T instance_settings, IEnumerable<ISettingsMigrationRule<T>> migration_rules, IImmutableProperty<UserSettings, T> propi) where T : class {
|
||||
if (instance_settings != null && migration_rules.merge().apply(instance_settings, out var new_settings))
|
||||
instance_settings = new_settings;
|
||||
return new WindowSettingsManager<T>(this, instance_settings, get, set);
|
||||
return new WindowSettingsManager<T>(this, instance_settings, propi.get, propi.set);
|
||||
}
|
||||
|
||||
async Task _persist_async() {
|
||||
try {
|
||||
await _persist.set(NAME_SPACE, DEFAULT_KEY, _default_settings);
|
||||
} catch(Exception ex) {
|
||||
Log.Persist.warn($"Error persisting {typeof(UserSettings)} in namespace '{NAME_SPACE}': {ex}");
|
||||
} catch (Exception ex) {
|
||||
Log.Persist.warn($"Error persisting {typeof(UserSettings)} in namespace '{NAME_SPACE}': {ex}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
|
||||
namespace mbts.utils {
|
||||
delegate bool TryGetValue<T>(out T value);
|
||||
@@ -11,4 +12,18 @@ namespace mbts.utils {
|
||||
XOr = (a, b) => a ? !b : b,
|
||||
None = (a, b) => !a && !b;
|
||||
}
|
||||
|
||||
static class FormattingDelegates {
|
||||
public static class Price {
|
||||
public static readonly Func<double, string> Value =
|
||||
p => double.IsNaN(p) ? "" : p.ToString("#,0.00");
|
||||
}
|
||||
|
||||
public static class Price<T> where T : struct, IFormattable {
|
||||
public static readonly Func<T, string> Value =
|
||||
typeof(T) == typeof(double) ?
|
||||
(Func<T, string>)(object)Price.Value :
|
||||
p => p.ToString("#,0.00", CultureInfo.CurrentCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using System;
|
||||
|
||||
namespace bts.utils {
|
||||
/// <summary>
|
||||
/// A property-like interface (with get/set accessors) in the immutable pattern. The 'set' method returns a new instance of <typeparamref name="TSource"/>, rather than mutating the original.
|
||||
/// </summary>
|
||||
/// <typeparam name="TSource">The type of source, from which the value is read, and to which it is written.</typeparam>
|
||||
/// <typeparam name="T">The type of value read from, and written to, the source.</typeparam>
|
||||
interface IImmutableProperty<TSource, T> {
|
||||
T get(TSource source);
|
||||
TSource set(TSource source, T value);
|
||||
}
|
||||
|
||||
static class ImmutableProperty {
|
||||
public static IImmutableProperty<TSource, T> create<TSource, T>(Func<TSource, T> get, Func<TSource, T, TSource> set) =>
|
||||
new Impl<TSource, T>(get, set);
|
||||
|
||||
class Impl<TSource, T> : IImmutableProperty<TSource, T> {
|
||||
readonly Func<TSource, T> _get;
|
||||
readonly Func<TSource, T, TSource> _set;
|
||||
|
||||
public Impl(Func<TSource, T> get, Func<TSource, T, TSource> set) {
|
||||
_get = get;
|
||||
_set = set;
|
||||
}
|
||||
|
||||
public T get(TSource source) => _get(source);
|
||||
public TSource set(TSource source, T value) => _set(source, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
+84
-84
@@ -1,84 +1,84 @@
|
||||
using bts.api;
|
||||
using bts.api.orderman;
|
||||
using bts.api.position;
|
||||
using mbts.co2;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace mbts.spark {
|
||||
|
||||
enum trading_session_state {
|
||||
PreOpen = 21,
|
||||
ReadyToTrade = 17,
|
||||
TradingHalt = 2,
|
||||
NotAvailableForTrading = 18,
|
||||
Close = 4,
|
||||
PostClose = 26
|
||||
}
|
||||
|
||||
class MarketSchedule {
|
||||
|
||||
static readonly double _cme_gen_open_hrs;
|
||||
|
||||
const string cst_id = "Central Standard Time";
|
||||
// the sod time in config says it's stored in UTC, but it really represents 16:30 Central Daylight Time
|
||||
// i.e. 21:30 UTC corresponds to 16:30 Central (4:30 PM), but only during *CDT*
|
||||
// CDT it UTC -5
|
||||
// CST is UTC -6
|
||||
static readonly int sod_hh = (int)config.Configuration.get<bts.config.SODTime>().sod_hh;
|
||||
static readonly int sod_mm = (int)config.Configuration.get<bts.config.SODTime>().sod_mm;
|
||||
static readonly TimeZoneInfo central_tzi = TimeZoneInfo.FindSystemTimeZoneById(cst_id);
|
||||
|
||||
static MarketSchedule() {
|
||||
var cme_schedule = config.Configuration.get<bts.md.CMETradingSchedule>();
|
||||
_cme_gen_open_hrs = cme_schedule?.gen_open_hour ?? 8.5;
|
||||
}
|
||||
// for object initializers
|
||||
public MarketSchedule() { }
|
||||
|
||||
/// <summary>
|
||||
/// Returns the CME general open time of the most recent trading date (i.e today, or last Friday if today is not a weekday).
|
||||
/// </summary>
|
||||
public static DateTime get_cme_gen_open() {
|
||||
var now = DateTime.Now;
|
||||
DateTime last_open = now.Date.AddHours(
|
||||
now.TimeOfDay.TotalHours > _cme_gen_open_hrs ?
|
||||
_cme_gen_open_hrs :
|
||||
_cme_gen_open_hrs - 24);
|
||||
switch (last_open.DayOfWeek) {
|
||||
case DayOfWeek.Saturday: last_open = last_open.AddDays(-1); break;
|
||||
case DayOfWeek.Sunday: last_open = last_open.AddDays(-2); break;
|
||||
}
|
||||
return last_open;
|
||||
}
|
||||
|
||||
// DateTime? used here b/c we can't set a default parameter with DateTime.Now
|
||||
// since it's not a compile-time constant
|
||||
public static DateTime get_start_of_trading_day(DateTime? target_datetime = null, DateTimeKind dtk = DateTimeKind.Local ) {
|
||||
if (target_datetime == null) target_datetime = DateTime.Now;
|
||||
var dst_adjusted_central_sod = new DateTimeOffset(
|
||||
new DateTime(target_datetime.Value.Date.Add(new TimeSpan(sod_hh, sod_mm, 0)).Ticks, DateTimeKind.Unspecified),
|
||||
central_tzi.BaseUtcOffset
|
||||
);
|
||||
|
||||
// if we're not currently past the start of the trading day, then the start must be yesterday
|
||||
var temp = dst_adjusted_central_sod;
|
||||
if (temp >= TimeZoneInfo.ConvertTime(target_datetime.Value, central_tzi)) {
|
||||
temp = dst_adjusted_central_sod.AddDays(-1);
|
||||
}
|
||||
var local_time_zone_sod = TimeZoneInfo.ConvertTime(temp.DateTime, central_tzi, TimeZoneInfo.Local);
|
||||
if (dtk == DateTimeKind.Utc) {
|
||||
return local_time_zone_sod.ToUniversalTime();
|
||||
}
|
||||
return local_time_zone_sod;
|
||||
}
|
||||
|
||||
public static DateTime get_utctimestamp_as_localtime(string s) {
|
||||
return DateTimeOffset.ParseExact(s, "yyyyMMddHHmmssffffff", null, System.Globalization.DateTimeStyles.AssumeUniversal).ToLocalTime().LocalDateTime;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
using bts.api;
|
||||
using bts.api.orderman;
|
||||
using bts.api.position;
|
||||
using mbts.co2;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace mbts.spark {
|
||||
|
||||
enum trading_session_state {
|
||||
PreOpen = 21,
|
||||
ReadyToTrade = 17,
|
||||
TradingHalt = 2,
|
||||
NotAvailableForTrading = 18,
|
||||
Close = 4,
|
||||
PostClose = 26
|
||||
}
|
||||
|
||||
class MarketSchedule {
|
||||
|
||||
static readonly double _cme_gen_open_hrs;
|
||||
|
||||
const string cst_id = "Central Standard Time";
|
||||
// the sod time in config says it's stored in UTC, but it really represents 16:30 Central Daylight Time
|
||||
// i.e. 21:30 UTC corresponds to 16:30 Central (4:30 PM), but only during *CDT*
|
||||
// CDT it UTC -5
|
||||
// CST is UTC -6
|
||||
static readonly int sod_hh = (int)config.Configuration.get<bts.config.SODTime>().sod_hh;
|
||||
static readonly int sod_mm = (int)config.Configuration.get<bts.config.SODTime>().sod_mm;
|
||||
static readonly TimeZoneInfo central_tzi = TimeZoneInfo.FindSystemTimeZoneById(cst_id);
|
||||
|
||||
static MarketSchedule() {
|
||||
var cme_schedule = config.Configuration.get<bts.md.CMETradingSchedule>();
|
||||
_cme_gen_open_hrs = cme_schedule?.gen_open_hour ?? 8.5;
|
||||
}
|
||||
// for object initializers
|
||||
public MarketSchedule() { }
|
||||
|
||||
/// <summary>
|
||||
/// Returns the CME general open time of the most recent trading date (i.e today, or last Friday if today is not a weekday).
|
||||
/// </summary>
|
||||
public static DateTime get_cme_gen_open() {
|
||||
var now = DateTime.Now;
|
||||
DateTime last_open = now.Date.AddHours(
|
||||
now.TimeOfDay.TotalHours > _cme_gen_open_hrs ?
|
||||
_cme_gen_open_hrs :
|
||||
_cme_gen_open_hrs - 24);
|
||||
switch (last_open.DayOfWeek) {
|
||||
case DayOfWeek.Saturday: last_open = last_open.AddDays(-1); break;
|
||||
case DayOfWeek.Sunday: last_open = last_open.AddDays(-2); break;
|
||||
}
|
||||
return last_open;
|
||||
}
|
||||
|
||||
// DateTime? used here b/c we can't set a default parameter with DateTime.Now
|
||||
// since it's not a compile-time constant
|
||||
public static DateTime get_start_of_trading_day(DateTime? target_datetime = null, DateTimeKind dtk = DateTimeKind.Local ) {
|
||||
if (target_datetime == null) target_datetime = DateTime.Now;
|
||||
var dst_adjusted_central_sod = new DateTimeOffset(
|
||||
new DateTime(target_datetime.Value.Date.Add(new TimeSpan(sod_hh, sod_mm, 0)).Ticks, DateTimeKind.Unspecified),
|
||||
central_tzi.BaseUtcOffset
|
||||
);
|
||||
|
||||
// if we're not currently past the start of the trading day, then the start must be yesterday
|
||||
var temp = dst_adjusted_central_sod;
|
||||
if (temp >= TimeZoneInfo.ConvertTime(target_datetime.Value, central_tzi)) {
|
||||
temp = dst_adjusted_central_sod.AddDays(-1);
|
||||
}
|
||||
var local_time_zone_sod = TimeZoneInfo.ConvertTime(temp.DateTime, central_tzi, TimeZoneInfo.Local);
|
||||
if (dtk == DateTimeKind.Utc) {
|
||||
return local_time_zone_sod.ToUniversalTime();
|
||||
}
|
||||
return local_time_zone_sod;
|
||||
}
|
||||
|
||||
public static DateTime get_utctimestamp_as_localtime(string s) {
|
||||
return DateTimeOffset.ParseExact(s, "yyyyMMddHHmmssffffff", null, System.Globalization.DateTimeStyles.AssumeUniversal).ToLocalTime().LocalDateTime;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using bts.api.core;
|
||||
using mbts;
|
||||
using mbts.ui.shell;
|
||||
using mbts.utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
@@ -8,14 +10,29 @@ using System.Reactive.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
namespace bts.utils {
|
||||
static class ObjectSnapshot {
|
||||
public static IObservable<TableUpdate<(string name, object current, object original), string>> property_table<T>(T instance, params string[] prop_names) where T : INotifyPropertyChanged =>
|
||||
property_table(instance, Array.ConvertAll(prop_names, Impl<T>.type.GetProperty));
|
||||
interface IPropertyChangedDetector<T> where T : IRaiseNotifyPropertyChanged {
|
||||
IDisposable detect_changes(T target);
|
||||
}
|
||||
|
||||
public static IObservable<TableUpdate<(string name, object current, object original), string>> property_table<T>(T instance, params PropertyInfo[] props) where T : INotifyPropertyChanged =>
|
||||
static class ObjectSnapshot {
|
||||
public static IEnumerable<Func<TSource, TValue>> property_delegates_of_type<TSource, TValue>() where TSource : class where TValue : class {
|
||||
var tval = typeof(TValue);
|
||||
return typeof(TSource).GetProperties(BindingFlags.Public | BindingFlags.Instance)
|
||||
.Where(p => tval.IsAssignableFrom(p.PropertyType))
|
||||
.Select(_prop_getter<TSource, TValue>);
|
||||
}
|
||||
|
||||
public static IObservable<TableUpdate<(string name, object current, object original), string>> property_table<T>(T instance, params string[] prop_names) where T : INotifyPropertyChanged =>
|
||||
_property_table(instance, Array.ConvertAll(prop_names, PropertyInfoStore<T>.type.GetProperty));
|
||||
|
||||
public static IObservable<TableUpdate<(string name, object current, object original), string>> property_table(INotifyPropertyChanged instance, params string[] prop_names) =>
|
||||
_property_table(instance, Array.ConvertAll(prop_names, instance.GetType().GetProperty));
|
||||
|
||||
static IObservable<TableUpdate<(string name, object current, object original), string>> _property_table(INotifyPropertyChanged instance, params PropertyInfo[] props) =>
|
||||
Observable.Create<TableUpdate<(string name, object current, object original), string>>(o => {
|
||||
var cache = props.ToDictionary(p => p.Name, p => new PropNode(p, p.GetValue(instance)));
|
||||
var table = TableUpdate<(string name, object current, object original), string>.mk_empty(t => t.name).with_added(cache.Values.Select(pn => (pn.prop.Name, pn.current, pn.original)));
|
||||
o.OnNext(table);
|
||||
instance.PropertyChanged += on_prop_changed;
|
||||
return Disposable.Create(() => instance.PropertyChanged -= on_prop_changed);
|
||||
void on_prop_changed(object sender, PropertyChangedEventArgs e) {
|
||||
@@ -28,11 +45,20 @@ namespace bts.utils {
|
||||
}
|
||||
});
|
||||
|
||||
public static IPropertyChangedDetector<T> build_detector<T>(params string[] props) where T : IRaiseNotifyPropertyChanged =>
|
||||
new DiscretePropertyChangedDetector<T>(props);
|
||||
|
||||
public static IPropertyChangedDetector<T> build_detector_except<T>(params string[] except_props) where T : IRaiseNotifyPropertyChanged {
|
||||
// convert to a hashset for quicker lookups
|
||||
var exclude = new HashSet<string>(except_props);
|
||||
return new DiscretePropertyChangedDetector<T>(PropertyInfoStore<T>.all_props.Where(p => !exclude.Contains(p.Name)).Select(p => p.Name).ToArray());
|
||||
}
|
||||
|
||||
public static IObservable<bool> dirty_monitor<T>(T instance, params string[] prop_names) where T : INotifyPropertyChanged =>
|
||||
dirty_monitor(instance, Array.ConvertAll(prop_names, Impl<T>.type.GetProperty));
|
||||
dirty_monitor(instance, Array.ConvertAll(prop_names, PropertyInfoStore<T>.type.GetProperty));
|
||||
|
||||
public static IObservable<bool> dirty_monitor<T>(T instance, out IDisposable snapshot, params string[] prop_names) where T : INotifyPropertyChanged =>
|
||||
dirty_monitor(instance, out snapshot, Array.ConvertAll(prop_names, Impl<T>.type.GetProperty));
|
||||
dirty_monitor(instance, out snapshot, Array.ConvertAll(prop_names, PropertyInfoStore<T>.type.GetProperty));
|
||||
|
||||
public static IObservable<bool> dirty_monitor<T>(T instance, out IDisposable snapshot, params PropertyInfo[] props) where T : INotifyPropertyChanged {
|
||||
var prop_map = props.ToDictionary(p => p.Name, p => new PropNode(p, p.GetValue(instance)));
|
||||
@@ -67,7 +93,7 @@ namespace bts.utils {
|
||||
});
|
||||
|
||||
public static IDisposable create<T>(T instance) =>
|
||||
create(instance, Impl<T>.mutable_props);
|
||||
create(instance, PropertyInfoStore<T>.mutable_props);
|
||||
|
||||
public static IDisposable create<T>(T instance, PropertyInfo[] props) {
|
||||
var snapshot = Array.ConvertAll(props, p => p.GetValue(instance));
|
||||
@@ -77,13 +103,63 @@ namespace bts.utils {
|
||||
});
|
||||
}
|
||||
|
||||
static class Impl<T> {
|
||||
static Func<T, TValue> _prop_getter<T, TValue>(PropertyInfo prop) =>
|
||||
(Func<T, TValue>)prop.GetGetMethod().CreateDelegate(typeof(Func<T, TValue>));
|
||||
|
||||
static class PropertyInfoStore<T> {
|
||||
public static readonly Type type = typeof(T);
|
||||
|
||||
public static readonly PropertyInfo[] mutable_props =
|
||||
type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
|
||||
.Where(p => p.GetSetMethod() != null)
|
||||
.ToArray();
|
||||
public static readonly PropertyInfo[]
|
||||
all_props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance),
|
||||
mutable_props;
|
||||
|
||||
static PropertyInfoStore() =>
|
||||
mutable_props = all_props.Where(p => p.GetGetMethod() != null).ToArray();
|
||||
}
|
||||
|
||||
// DelegateStore maintains a cache of property getter/setter delegates,
|
||||
// which perform much better than the corresponding PropertyInfo.GetValue/SetValue.
|
||||
static class DelegateStore<T> {
|
||||
public static readonly Type type = typeof(T);
|
||||
|
||||
static readonly IReadOnlyDictionary<string, (Func<T, object> get, Action<T, object> set)> _property_delegates;
|
||||
|
||||
public static Func<T, object> getter(string prop_name) =>
|
||||
_property_delegates.TryGetValue(prop_name, out var dels) ? dels.get : null;
|
||||
|
||||
public static Action<T, object> setter(string prop_name) =>
|
||||
_property_delegates.TryGetValue(prop_name, out var dels) ? dels.set : null;
|
||||
|
||||
static DelegateStore() {
|
||||
var prop_dels = new Dictionary<string, (Func<T, object> get, Action<T, object> set)>();
|
||||
foreach (var p in PropertyInfoStore<T>.all_props) {
|
||||
Func<T, object> wrapped_getter = null;
|
||||
Action<T, object> wrapped_setter = null;
|
||||
var getter = p.GetGetMethod();
|
||||
var setter = p.GetSetMethod();
|
||||
if (getter == null && setter == null)
|
||||
continue;
|
||||
var del_types = (typeof(Func<,>).MakeGenericType(typeof(T), p.PropertyType), typeof(Action<,>).MakeGenericType(typeof(T), p.PropertyType));
|
||||
var twrapper = typeof(Wrapper<>).MakeGenericType(typeof(T), p.PropertyType);
|
||||
if (getter != null) {
|
||||
wrapped_getter = (Func<T, object>)
|
||||
twrapper.GetMethod(nameof(Wrapper<T>.getter), BindingFlags.Static | BindingFlags.Public)
|
||||
.Invoke(null, new[] { getter.CreateDelegate(del_types.Item1) });
|
||||
}
|
||||
if (setter != null) {
|
||||
wrapped_setter = (Action<T, object>)
|
||||
twrapper.GetMethod(nameof(Wrapper<T>.setter), BindingFlags.Static | BindingFlags.Public)
|
||||
.Invoke(null, new[] { setter.CreateDelegate(del_types.Item2) });
|
||||
}
|
||||
prop_dels[p.Name] = (wrapped_getter, wrapped_setter);
|
||||
}
|
||||
_property_delegates = prop_dels;
|
||||
}
|
||||
|
||||
static class Wrapper<TValue> {
|
||||
public static Func<T, object> getter(Func<T, TValue> getter) => src => getter(src);
|
||||
public static Action<T, object> setter(Action<T, TValue> setter) => (src, val) => setter(src, (TValue)val);
|
||||
}
|
||||
}
|
||||
|
||||
class PropNode {
|
||||
@@ -99,5 +175,30 @@ namespace bts.utils {
|
||||
public object original { get; set; }
|
||||
public object current { get; set; }
|
||||
}
|
||||
|
||||
class DiscretePropertyChangedDetector<T> : IPropertyChangedDetector<T> where T : IRaiseNotifyPropertyChanged {
|
||||
static readonly Converter<string, (string name, Func<T, object> get)> _realize_prop =
|
||||
(prop_name) => (prop_name, DelegateStore<T>.getter(prop_name));
|
||||
|
||||
readonly (string name, Func<T, object> get)[] _target_props;
|
||||
|
||||
public DiscretePropertyChangedDetector()
|
||||
: this(Array.ConvertAll(PropertyInfoStore<T>.all_props, p => p.Name)) { }
|
||||
|
||||
public DiscretePropertyChangedDetector(params string[] props) =>
|
||||
_target_props = Array.ConvertAll(props, _realize_prop);
|
||||
|
||||
public IDisposable detect_changes(T target) {
|
||||
var get_target_val = new Converter<(string name, Func<T, object> get), object>(tup => tup.get(target));
|
||||
var orig_state = Array.ConvertAll(_target_props, get_target_val);
|
||||
return Disposable.Create(() => {
|
||||
var curr_state = Array.ConvertAll(_target_props, get_target_val);
|
||||
for (int i = 0; i < curr_state.Length; i++) {
|
||||
if (!Equals(curr_state[i], orig_state[i]))
|
||||
target.raise_notify_property_changed(_target_props[i].name);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,16 @@ namespace bts {
|
||||
}
|
||||
}
|
||||
|
||||
public static OneOf<A, B> MatchFirst<TSource, A, B>(IEnumerable<TSource> source) {
|
||||
foreach (var v in source) {
|
||||
if (v is A va) {
|
||||
return new OneOf<A, B>(va);
|
||||
} else if (v is B vb)
|
||||
return new OneOf<A, B>(vb);
|
||||
}
|
||||
return new OneOf<A, B>();
|
||||
}
|
||||
|
||||
public static T infer<T, A, B>(object value, Func<A, T> a, Func<B, T> b) =>
|
||||
OneOf<A, B>.infer(value).Match(a, b);
|
||||
}
|
||||
@@ -101,6 +111,13 @@ namespace bts {
|
||||
public OneOf(B b) { _b = b; _tag = 1; }
|
||||
public OneOf(C c) { _c = c; _tag = 2; }
|
||||
|
||||
public static OneOf<A, B, C> infer(object obj) {
|
||||
if (obj is A a) return new OneOf<A, B, C>(a);
|
||||
if (obj is B b) return new OneOf<A, B, C>(b);
|
||||
if (obj is C c) return new OneOf<A, B, C>(c);
|
||||
return new OneOf<A, B, C>();
|
||||
}
|
||||
|
||||
public void Match(Action<A> a, Action<B> b, Action<C> c) {
|
||||
switch (_tag) {
|
||||
case 0: a(_a); break;
|
||||
@@ -118,6 +135,15 @@ namespace bts {
|
||||
}
|
||||
}
|
||||
|
||||
public OneOf<A2, B2, C2> Extend<A2, B2, C2>(Func<A, A2> a, Func<B, B2> b, Func<C, C2> c) {
|
||||
switch (_tag) {
|
||||
case 0: return new OneOf<A2, B2, C2>(a(_a));
|
||||
case 1: return new OneOf<A2, B2, C2>(b(_b));
|
||||
case 2: return new OneOf<A2, B2, C2>(c(_c));
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
||||
public object value {
|
||||
get {
|
||||
switch (_tag) {
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
using System;
|
||||
|
||||
namespace bts.utils {
|
||||
/// <summary>
|
||||
/// An IComparable implementation that supports cross type comparisons of any combination of int, uint, double, and decimal.
|
||||
/// </summary>
|
||||
readonly struct UniversalNumericComparer : IComparable {
|
||||
readonly int? _int;
|
||||
readonly uint? _uint;
|
||||
readonly double? _double;
|
||||
readonly decimal? _decimal;
|
||||
|
||||
public UniversalNumericComparer(int i) {
|
||||
_int = i;
|
||||
_uint = null;
|
||||
_double = null;
|
||||
_decimal = null;
|
||||
}
|
||||
|
||||
public UniversalNumericComparer(double d) {
|
||||
_int = null;
|
||||
_uint = null;
|
||||
_double = d;
|
||||
_decimal = null;
|
||||
}
|
||||
|
||||
public UniversalNumericComparer(uint u) {
|
||||
_int = null;
|
||||
_uint = u;
|
||||
_double = null;
|
||||
_decimal = null;
|
||||
}
|
||||
|
||||
public UniversalNumericComparer(decimal d) {
|
||||
_int = null;
|
||||
_uint = null;
|
||||
_double = null;
|
||||
_decimal = d;
|
||||
}
|
||||
|
||||
public int CompareTo(object obj) {
|
||||
if (obj is int i) {
|
||||
return -(i.CompareTo(this));
|
||||
} else if (obj is uint u) {
|
||||
return -(u.CompareTo(this));
|
||||
} else if (obj is double d)
|
||||
return -(d.CompareTo(this));
|
||||
else if (obj is decimal dec)
|
||||
return -(dec.CompareTo(this));
|
||||
throw new ArgumentException("Obj is not of a supported numeric type (int, uint, double).", nameof(obj));
|
||||
}
|
||||
|
||||
public static implicit operator int(UniversalNumericComparer w) {
|
||||
if (w._int.HasValue)
|
||||
return w._int.Value;
|
||||
else if (w._double.HasValue)
|
||||
return (int)w._double.Value;
|
||||
else if (w._uint.HasValue)
|
||||
return (int)w._uint.Value;
|
||||
else if (w._decimal.HasValue)
|
||||
return (int)w._decimal.Value;
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static implicit operator uint(UniversalNumericComparer w) {
|
||||
if (w._uint.HasValue)
|
||||
return w._uint.Value;
|
||||
else if (w._int.HasValue)
|
||||
return (uint)w._int.Value;
|
||||
else if (w._double.HasValue)
|
||||
return (uint)w._double.Value;
|
||||
else if (w._decimal.HasValue)
|
||||
return (uint)w._decimal.Value;
|
||||
return 0u;
|
||||
}
|
||||
|
||||
public static implicit operator double(UniversalNumericComparer w) {
|
||||
if (w._double.HasValue)
|
||||
return w._double.Value;
|
||||
else if (w._int.HasValue)
|
||||
return w._int.Value;
|
||||
else if (w._uint.HasValue)
|
||||
return w._uint.Value;
|
||||
else if (w._decimal.HasValue)
|
||||
return (double)w._decimal.Value;
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
public static implicit operator decimal(UniversalNumericComparer w) {
|
||||
if (w._double.HasValue)
|
||||
return (decimal)w._double.Value;
|
||||
else if (w._int.HasValue)
|
||||
return w._int.Value;
|
||||
else if (w._uint.HasValue)
|
||||
return w._uint.Value;
|
||||
else if (w._decimal.HasValue)
|
||||
return w._decimal.Value;
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static implicit operator UniversalNumericComparer(int i) => new UniversalNumericComparer(i);
|
||||
public static implicit operator UniversalNumericComparer(uint u) => new UniversalNumericComparer(u);
|
||||
public static implicit operator UniversalNumericComparer(double d) => new UniversalNumericComparer(d);
|
||||
public static implicit operator UniversalNumericComparer(decimal d) => new UniversalNumericComparer(d);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
using bts.utils;
|
||||
using mbts.ui;
|
||||
using mbts.ui.utility.text;
|
||||
using ProtoBuf;
|
||||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using FontFamily = System.Windows.Media.FontFamily;
|
||||
|
||||
namespace bts.proto {
|
||||
[ProtoContract]
|
||||
class FontParamsProto : IEquatable<FontParamsProto> {
|
||||
[ProtoMember(1)]
|
||||
public TypefaceProto typeface { get; set; }
|
||||
[ProtoMember(2)]
|
||||
public double font_size { get; set; }
|
||||
[ProtoMember(3)]
|
||||
public uint font_color { get; set; }
|
||||
|
||||
public string font_family => typeface.font_family;
|
||||
public int font_weight => typeface.font_weight;
|
||||
|
||||
public bool Equals(FontParamsProto other) =>
|
||||
other != null &&
|
||||
typeface == other.typeface &&
|
||||
font_size == other.font_size &&
|
||||
font_color == other.font_color;
|
||||
|
||||
public override bool Equals(object obj) =>
|
||||
Equals(obj as FontParamsProto);
|
||||
|
||||
public override int GetHashCode() =>
|
||||
typeface.GetHashCode();
|
||||
}
|
||||
|
||||
static class FontParamsProtoExtensions {
|
||||
public static (FontFamily family, FontWeight? weight, Color? color, double size) to_wpf_params(this FontParamsProto proto) =>
|
||||
proto.typeface.to_typeface_parts().with(
|
||||
_to_nullable_color(proto.font_color),
|
||||
// zero/negative are invalid font sizes, convert to nan to follow convention
|
||||
proto.font_size <= 0 ? double.NaN : proto.font_size);
|
||||
|
||||
public static (FontFamily family, FontWeight? weight, Color? color, double size) to_wpf_params_safe(this FontParamsProto proto,
|
||||
FontFamily fallback_family = null,
|
||||
FontWeight? fallback_weight = null,
|
||||
Color? fallback_color = null,
|
||||
double fallback_size = double.NaN) =>
|
||||
//
|
||||
proto.typeface.to_typeface_parts_safe(fallback_family, fallback_weight).with(
|
||||
_to_color_safe(proto.font_color, fallback_color ?? Colors.Black),
|
||||
_to_size_safe(proto.font_size));
|
||||
|
||||
static Color? _to_nullable_color(uint proto) =>
|
||||
proto == 0 ? (Color?)null : proto.argb_color();
|
||||
|
||||
static Color _to_color_safe(uint proto, Color fallback_color) =>
|
||||
_to_nullable_color(proto) ?? fallback_color;
|
||||
|
||||
static double _to_size_safe(double size, double fallback_size = double.NaN) =>
|
||||
double.IsNaN(size) ?
|
||||
(double.IsNaN(fallback_size) ? FontSizes.Small : fallback_size) :
|
||||
size;
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
using bts.utils;
|
||||
using mbts.ui;
|
||||
using mbts.utils;
|
||||
using ProtoBuf;
|
||||
using System;
|
||||
using System.Windows.Media;
|
||||
@@ -16,6 +15,7 @@ namespace bts.proto {
|
||||
|
||||
public NullableColorProto(uint value) =>
|
||||
_value = value;
|
||||
|
||||
public Color? value => _value?.rgb_color();
|
||||
public Color? argb => _value?.argb_color();
|
||||
|
||||
@@ -31,4 +31,4 @@ namespace bts.proto {
|
||||
public static bool operator ==(NullableColorProto a, NullableColorProto b) => a.Equals(b);
|
||||
public static bool operator !=(NullableColorProto a, NullableColorProto b) => !a.Equals(b);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
using bts.utils;
|
||||
using mbts.ui.utility.text;
|
||||
using ProtoBuf;
|
||||
using System;
|
||||
using System.Windows;
|
||||
using FontFamily = System.Windows.Media.FontFamily;
|
||||
|
||||
namespace bts.proto {
|
||||
[ProtoContract]
|
||||
readonly struct TypefaceProto : IEquatable<TypefaceProto> {
|
||||
static readonly int _type_hash = typeof(TypefaceProto).GetHashCode();
|
||||
|
||||
[ProtoMember(1)]
|
||||
public readonly string font_family;
|
||||
[ProtoMember(2)]
|
||||
public readonly int font_weight;
|
||||
// TODO (maybe): add property for FontStyle (italic, underline, etc.)
|
||||
|
||||
public TypefaceProto(string font_family, int font_weight) {
|
||||
this.font_family = font_family;
|
||||
this.font_weight = font_weight;
|
||||
}
|
||||
|
||||
public TypefaceProto with(string font_family = null, int? font_weight = null, double font_size = double.NaN) =>
|
||||
new TypefaceProto(
|
||||
font_family ?? this.font_family,
|
||||
font_weight ?? this.font_weight);
|
||||
|
||||
public bool Equals(TypefaceProto other) =>
|
||||
font_family == other.font_family &&
|
||||
font_weight == other.font_weight;
|
||||
|
||||
public override bool Equals(object obj) =>
|
||||
obj is TypefaceProto tfp && Equals(tfp);
|
||||
|
||||
public override int GetHashCode() =>
|
||||
EqualityComparer.auto_phase(
|
||||
font_family?.GetHashCode() ?? 0,
|
||||
font_weight);
|
||||
|
||||
public static bool operator ==(TypefaceProto x, TypefaceProto y) => x.Equals(y);
|
||||
public static bool operator !=(TypefaceProto x, TypefaceProto y) => !x.Equals(y);
|
||||
}
|
||||
|
||||
static class TypefaceProtoExtensions {
|
||||
public static (FontFamily family, FontWeight? weight) to_typeface_parts(this TypefaceProto proto) =>
|
||||
(string.IsNullOrEmpty(proto.font_family) ? null : new FontFamily(proto.font_family),
|
||||
proto.font_weight == 0 ? (FontWeight?)null : FontWeight.FromOpenTypeWeight(proto.font_weight));
|
||||
|
||||
public static (FontFamily family, FontWeight weight) to_typeface_parts_safe(
|
||||
this TypefaceProto proto,
|
||||
FontFamily fallback_family = null,
|
||||
FontWeight? fallback_weight = null) {
|
||||
//
|
||||
var (family, weight) = proto.to_typeface_parts();
|
||||
return (
|
||||
family ?? fallback_family ?? TextUtils.StandardFontFamily,
|
||||
weight ?? fallback_weight ?? FontWeights.Normal);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -61,11 +61,19 @@
|
||||
<SolidColorBrush x:Key="CheckBox.Pressed.BorderBrush" Color="{StaticResource CheckBox.Pressed.BorderColor}"/>
|
||||
|
||||
<!--ComboBox-->
|
||||
<Color x:Key="ComboBoxBackgroundLightColor">#FFE5E5E5</Color>
|
||||
<Color x:Key="ComboBoxBackgroundMediumColor">#FFF0F0F0</Color>
|
||||
|
||||
<Color x:Key="ComboBoxBorderLightColor">#FFACACAC</Color>
|
||||
<Color x:Key="ComboBoxBorderMediumColor">#FF7EB4EA</Color>
|
||||
<Color x:Key="ComboBoxBorderDarkColor">#FF569DE5</Color>
|
||||
<Color x:Key="ComboBoxDisabledBorderColor">#FFD9D9D9</Color>
|
||||
|
||||
<LinearGradientBrush x:Key="ComboBox.Static.Background" EndPoint="0,1" StartPoint="0,0">
|
||||
<GradientStop Color="#FFF0F0F0" Offset="0.0"/>
|
||||
<GradientStop Color="#FFE5E5E5" Offset="1.0"/>
|
||||
<GradientStop Color="{StaticResource ComboBoxBackgroundLightColor}" Offset="0.0"/>
|
||||
<GradientStop Color="{StaticResource ComboBoxBackgroundMediumColor}" Offset="1.0"/>
|
||||
</LinearGradientBrush>
|
||||
<SolidColorBrush x:Key="ComboBox.Static.Border" Color="#FFACACAC"/>
|
||||
<SolidColorBrush x:Key="ComboBox.Static.Border" Color="{StaticResource ComboBoxBorderLightColor}"/>
|
||||
<SolidColorBrush x:Key="ComboBox.Static.Editable.Background" Color="#FFFFFFFF"/>
|
||||
<SolidColorBrush x:Key="ComboBox.Static.Editable.Border" Color="#FFABADB3"/>
|
||||
<SolidColorBrush x:Key="ComboBox.Static.Editable.Button.Background" Color="Transparent"/>
|
||||
@@ -75,7 +83,7 @@
|
||||
<GradientStop Color="#FFECF4FC" Offset="0.0"/>
|
||||
<GradientStop Color="#FFDCECFC" Offset="1.0"/>
|
||||
</LinearGradientBrush>
|
||||
<SolidColorBrush x:Key="ComboBox.MouseOver.Border" Color="#FF7EB4EA"/>
|
||||
<SolidColorBrush x:Key="ComboBox.MouseOver.Border" Color="{StaticResource ComboBoxBorderMediumColor}"/>
|
||||
<SolidColorBrush x:Key="ComboBox.MouseOver.Editable.Background" Color="#FFFFFFFF"/>
|
||||
<SolidColorBrush x:Key="ComboBox.MouseOver.Editable.Border" Color="#FF7EB4EA"/>
|
||||
<LinearGradientBrush x:Key="ComboBox.MouseOver.Editable.Button.Background" EndPoint="0,1" StartPoint="0,0">
|
||||
@@ -88,7 +96,7 @@
|
||||
<GradientStop Color="#FFDAECFC" Offset="0.0"/>
|
||||
<GradientStop Color="#FFC4E0FC" Offset="1.0"/>
|
||||
</LinearGradientBrush>
|
||||
<SolidColorBrush x:Key="ComboBox.Pressed.Border" Color="#FF569DE5"/>
|
||||
<SolidColorBrush x:Key="ComboBox.Pressed.Border" Color="{StaticResource ComboBoxBorderDarkColor}"/>
|
||||
<SolidColorBrush x:Key="ComboBox.Pressed.Editable.Background" Color="#FFFFFFFF"/>
|
||||
<SolidColorBrush x:Key="ComboBox.Pressed.Editable.Border" Color="#FF569DE5"/>
|
||||
<LinearGradientBrush x:Key="ComboBox.Pressed.Editable.Button.Background" EndPoint="0,1" StartPoint="0,0">
|
||||
@@ -97,8 +105,8 @@
|
||||
</LinearGradientBrush>
|
||||
<SolidColorBrush x:Key="ComboBox.Pressed.Editable.Button.Border" Color="#FF569DE5"/>
|
||||
<SolidColorBrush x:Key="ComboBox.Disabled.Glyph" Color="#FFBFBFBF"/>
|
||||
<SolidColorBrush x:Key="ComboBox.Disabled.Background" Color="#FFF0F0F0"/>
|
||||
<SolidColorBrush x:Key="ComboBox.Disabled.Border" Color="#FFD9D9D9"/>
|
||||
<SolidColorBrush x:Key="ComboBox.Disabled.Background" Color="{StaticResource ComboBoxBackgroundMediumColor}"/>
|
||||
<SolidColorBrush x:Key="ComboBox.Disabled.Border" Color="{StaticResource ComboBoxDisabledBorderColor}"/>
|
||||
<SolidColorBrush x:Key="ComboBox.Disabled.Editable.Background" Color="#FFFFFFFF"/>
|
||||
<SolidColorBrush x:Key="ComboBox.Disabled.Editable.Border" Color="#FFBFBFBF"/>
|
||||
<SolidColorBrush x:Key="ComboBox.Disabled.Editable.Button.Background" Color="Transparent"/>
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
using mbts.ui.extensions;
|
||||
using mbts.utils;
|
||||
using System;
|
||||
using System.Reactive.Disposables;
|
||||
using System.Windows;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace mbts.ui.controls.adorners {
|
||||
class MouseTraceAdorner : Adorner {
|
||||
readonly Visual _trace_visual;
|
||||
readonly Size _trace_visual_size;
|
||||
|
||||
MouseTraceAdorner(UIElement adorned_element, Visual trace_visual, Size trace_visual_size)
|
||||
: base(adorned_element) {
|
||||
_trace_visual = trace_visual;
|
||||
_trace_visual_size = trace_visual_size;
|
||||
AddVisualChild(trace_visual);
|
||||
IsHitTestVisible = false;
|
||||
HorizontalAlignment = HorizontalAlignment.Left;
|
||||
VerticalAlignment = VerticalAlignment.Top;
|
||||
}
|
||||
|
||||
protected override int VisualChildrenCount => 1;
|
||||
|
||||
public static IDisposable trace(FrameworkElement target, Visual trace_visual, Size trace_visual_size) {
|
||||
var mta = new MouseTraceAdorner(target, trace_visual, trace_visual_size);
|
||||
move_to(Mouse.GetPosition(target));
|
||||
mta.attach();
|
||||
return new CompositeDisposable {
|
||||
target.SubscribePreviewMouseMove(on_mouse),
|
||||
target.SubscribeMouseEnter(on_mouse),
|
||||
target.SubscribeMouseLeave(on_mouse),
|
||||
mta.detach
|
||||
};
|
||||
void on_mouse(object s, MouseEventArgs e) =>
|
||||
move_to(e.GetPosition(target));
|
||||
|
||||
void move_to(Point pt) =>
|
||||
mta.RenderTransform = new TranslateTransform(pt.X, pt.Y);
|
||||
}
|
||||
|
||||
public static IDisposable trace_drag(FrameworkElement target, Visual trace_visual, Size trace_visual_size) {
|
||||
var mta = new MouseTraceAdorner(target, trace_visual, trace_visual_size);
|
||||
move_to(Mouse.GetPosition(target));
|
||||
mta.attach();
|
||||
return new CompositeDisposable {
|
||||
target.SubscribePreviewDragOver(on_drag),
|
||||
target.SubscribePreviewDragLeave(on_drag),
|
||||
target.SubscribePreviewDragEnter(on_drag),
|
||||
mta.detach
|
||||
};
|
||||
void on_drag(object s, DragEventArgs e) =>
|
||||
move_to(e.GetPosition(target));
|
||||
|
||||
void move_to(Point pt) =>
|
||||
mta.RenderTransform = new TranslateTransform(pt.X, pt.Y);
|
||||
}
|
||||
|
||||
protected override Visual GetVisualChild(int index) =>
|
||||
index == 0 ? _trace_visual : throw new ArgumentOutOfRangeException(nameof(index));
|
||||
|
||||
protected override Size MeasureOverride(Size constraint) => _trace_visual_size;
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,24 @@
|
||||
using bts.utils;
|
||||
using bts;
|
||||
using bts.utils;
|
||||
using mbts.ui.extensions;
|
||||
using mbts.utils;
|
||||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls.Primitives;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace mbts.ui.controls.adorners {
|
||||
internal enum HorizontalPosition { Left, Middle, Right }
|
||||
internal enum VerticalPosition { Top, Middle, Botton }
|
||||
internal enum VerticalPosition { Top, Middle, Bottom }
|
||||
|
||||
class PopupAdorner : Adorner {
|
||||
bool _enable_drag_move;
|
||||
IDisposable _drag_move_hook;
|
||||
IDisposable _drag_move_hook, _auto_close_hook;
|
||||
|
||||
public PopupAdorner(UIElement adornedElement, FrameworkElement popupTarget)
|
||||
: this(adornedElement, popupTarget, null, HorizontalPosition.Middle, VerticalPosition.Botton) { }
|
||||
: this(adornedElement, popupTarget, null, HorizontalPosition.Middle, VerticalPosition.Bottom) { }
|
||||
|
||||
public PopupAdorner(UIElement adornedElement, FrameworkElement popupTarget, FrameworkElement popupElement, HorizontalPosition horizontalPosition, VerticalPosition verticalPosition)
|
||||
: this(adornedElement, popupTarget, popupElement, horizontalPosition, verticalPosition, 0.0, 0.0) { }
|
||||
@@ -68,6 +72,37 @@ namespace mbts.ui.controls.adorners {
|
||||
|
||||
#endregion
|
||||
|
||||
#region StaysOpen
|
||||
public static readonly DependencyProperty StaysOpenProperty =
|
||||
Popup.StaysOpenProperty.AddOwner(typeof(PopupAdorner),
|
||||
new FrameworkPropertyMetadata(
|
||||
true,
|
||||
(d, e) => {
|
||||
var popa = ExceptionAssert.of_type<PopupAdorner>(d, nameof(d));
|
||||
Disposable.dispose(ref popa._auto_close_hook);
|
||||
if (!(bool)e.NewValue) {
|
||||
FocusManager.SetIsFocusScope(popa.AdornedElement, true);
|
||||
popa.AdornedElement.Focus();
|
||||
popa.AdornedElement.PreviewGotKeyboardFocus += on_focus_changed;
|
||||
popa.AdornedElement.GotFocus += on_focus_changed;
|
||||
popa._auto_close_hook = Disposable.Create(() => {
|
||||
popa.AdornedElement.PreviewGotKeyboardFocus -= on_focus_changed;
|
||||
popa.AdornedElement.GotFocus -= on_focus_changed;
|
||||
});
|
||||
// callbacks
|
||||
void on_focus_changed(object sender, RoutedEventArgs _e) {
|
||||
if (!popa.PopupElement.IsKeyboardFocusWithin && FocusManager.GetFocusedElement(popa.PopupElement) == null)
|
||||
popa.SetCurrentValue(IsOpenProperty, false);
|
||||
};
|
||||
}
|
||||
}));
|
||||
|
||||
public bool StaysOpen {
|
||||
get => (bool)GetValue(StaysOpenProperty);
|
||||
set => SetValue(StaysOpenProperty, value);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Opened Event
|
||||
|
||||
public static readonly RoutedEvent OpenedEvent = EventManager.RegisterRoutedEvent("Opened", RoutingStrategy.Direct, typeof(RoutedEventHandler), typeof(PopupAdorner));
|
||||
@@ -129,7 +164,7 @@ namespace mbts.ui.controls.adorners {
|
||||
switch (_verticalPosition) {
|
||||
case VerticalPosition.Top: rltv_y = -el_height; break;
|
||||
case VerticalPosition.Middle: rltv_y = -(0.5 * el_height); break;
|
||||
case VerticalPosition.Botton: rltv_y = 0; break;
|
||||
case VerticalPosition.Bottom: rltv_y = 0; break;
|
||||
default: throw new NotImplementedException();
|
||||
}
|
||||
tl = new Point(tl.X + rltv_x + _horizontalOffset, tl.Y + rltv_y + _verticalOffset);
|
||||
@@ -138,14 +173,14 @@ namespace mbts.ui.controls.adorners {
|
||||
double rltv_x, rltv_y;
|
||||
switch (_horizontalPosition) {
|
||||
case HorizontalPosition.Left: rltv_x = -el_width; break;
|
||||
case HorizontalPosition.Middle: rltv_x = 0.5 * (_popupTarget.ActualWidth - el_width); break;
|
||||
case HorizontalPosition.Right: rltv_x = _popupTarget.ActualWidth + el_width; break;
|
||||
case HorizontalPosition.Middle: rltv_x = 0.5 * _popupTarget.ActualWidth; break;
|
||||
case HorizontalPosition.Right: rltv_x = _popupTarget.ActualWidth; break;
|
||||
default: throw new NotImplementedException();
|
||||
}
|
||||
switch (_verticalPosition) {
|
||||
case VerticalPosition.Top: rltv_y = -el_height; break;
|
||||
case VerticalPosition.Middle: rltv_y = 0.5 * (_popupTarget.ActualHeight - el_height); break;
|
||||
case VerticalPosition.Botton: rltv_y = _popupTarget.ActualHeight + el_height; break;
|
||||
case VerticalPosition.Middle: rltv_y = 0.5 * _popupTarget.ActualHeight; break;
|
||||
case VerticalPosition.Bottom: rltv_y = _popupTarget.ActualHeight; break;
|
||||
default: throw new NotImplementedException();
|
||||
}
|
||||
tl = new Point(tl.X + rltv_x + _horizontalOffset, tl.Y + rltv_y + _verticalOffset);
|
||||
|
||||
@@ -1,56 +1,72 @@
|
||||
<ToggleButton
|
||||
x:Class="mbts.ui.controls.ExpanderToggleButton"
|
||||
x:ClassModifier="internal"
|
||||
<ResourceDictionary
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
BorderBrush="Black"
|
||||
Padding="3,0,0,0"
|
||||
xmlns:c="clr-namespace:mbts.ui.controls"
|
||||
xmlns:cv="clr-namespace:mbts.ui.converters"
|
||||
>
|
||||
<ToggleButton.Resources>
|
||||
<SolidColorBrush x:Key="Path.Static.Brush" Color="#FF333333"/>
|
||||
<SolidColorBrush x:Key="Ellipse.MouseOver.Brush" Color="#FF5593FF"/>
|
||||
<SolidColorBrush x:Key="Ellipse.Pressed.Brush" Color="#FF3C77DD"/>
|
||||
<SolidColorBrush x:Key="Ellipse.Disabled.Brush" Color="Gray"/>
|
||||
</ToggleButton.Resources>
|
||||
<ToggleButton.Template>
|
||||
<ControlTemplate TargetType="ToggleButton">
|
||||
<Grid Background="Transparent">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Ellipse x:Name="ellipse" Width="20" Height="20" Stroke="{TemplateBinding BorderBrush}"/>
|
||||
<Path
|
||||
x:Name="arrow"
|
||||
Data="M1,1.5L4.5,5 8,1.5"
|
||||
HorizontalAlignment="Center"
|
||||
SnapsToDevicePixels="False"
|
||||
Stroke="{StaticResource Path.Static.Brush}"
|
||||
StrokeThickness="2"
|
||||
VerticalAlignment="Center"
|
||||
/>
|
||||
<ContentPresenter
|
||||
Grid.Column="1"
|
||||
ContentSource="{TemplateBinding Content}"
|
||||
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
|
||||
Margin="{TemplateBinding Padding}"
|
||||
/>
|
||||
</Grid>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsChecked" Value="True">
|
||||
<Setter Property="Data" TargetName="arrow" Value="M1,4.5L4.5,1 8,4.5"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Stroke" TargetName="ellipse" Value="{StaticResource Ellipse.MouseOver.Brush}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter Property="Stroke" TargetName="ellipse" Value="{StaticResource Ellipse.Pressed.Brush}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter Property="Foreground" Value="Gray"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</ToggleButton.Template>
|
||||
</ToggleButton>
|
||||
<SolidColorBrush x:Key="Path.Static.Brush" Color="#FF333333"/>
|
||||
<SolidColorBrush x:Key="Ellipse.MouseOver.Brush" Color="#FF5593FF"/>
|
||||
<SolidColorBrush x:Key="Ellipse.Pressed.Brush" Color="#FF3C77DD"/>
|
||||
<SolidColorBrush x:Key="Ellipse.Disabled.Brush" Color="Gray"/>
|
||||
<Style TargetType="c:ExpanderToggleButton" BasedOn="{StaticResource {x:Type ToggleButton}}">
|
||||
<Setter Property="BorderBrush" Value="Black"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="Padding" Value="3,0,0,0"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ToggleButton">
|
||||
<Border>
|
||||
<Grid Background="Transparent">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Ellipse x:Name="ellipse" Width="20" Height="20" Stroke="{TemplateBinding BorderBrush}" StrokeThickness="{Binding BorderThickness, RelativeSource={RelativeSource TemplatedParent}, Converter={x:Static cv:ValueConverters.ThicknessToDouble}}"/>
|
||||
<Path
|
||||
x:Name="arrow"
|
||||
Data="M0,0L3.5,3.5 0,7"
|
||||
HorizontalAlignment="Center"
|
||||
SnapsToDevicePixels="False"
|
||||
Stroke="{StaticResource Path.Static.Brush}"
|
||||
StrokeThickness="2"
|
||||
VerticalAlignment="Center"
|
||||
/>
|
||||
<ContentPresenter
|
||||
Grid.Column="1"
|
||||
ContentSource="{TemplateBinding Content}"
|
||||
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
|
||||
Margin="{TemplateBinding Padding}"
|
||||
/>
|
||||
</Grid>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsChecked" Value="True">
|
||||
<Setter Property="Data" TargetName="arrow" Value="M0,0L3.5,3.5 7,0"/>
|
||||
</Trigger>
|
||||
<MultiDataTrigger>
|
||||
<MultiDataTrigger.Conditions>
|
||||
<Condition Binding="{Binding StrokeThickness, ElementName=ellipse}" Value="0"/>
|
||||
<Condition Binding="{Binding IsMouseOver, RelativeSource={RelativeSource Self}}" Value="True"/>
|
||||
</MultiDataTrigger.Conditions>
|
||||
<Setter TargetName="arrow" Property="Stroke" Value="{StaticResource Ellipse.MouseOver.Brush}"/>
|
||||
</MultiDataTrigger>
|
||||
<MultiDataTrigger>
|
||||
<MultiDataTrigger.Conditions>
|
||||
<Condition Binding="{Binding StrokeThickness, ElementName=ellipse, Converter={x:Static cv:ValueConverters.DoubleGreaterThanZero}}" Value="True"/>
|
||||
<Condition Binding="{Binding IsMouseOver, RelativeSource={RelativeSource Self}}" Value="True"/>
|
||||
</MultiDataTrigger.Conditions>
|
||||
<Setter TargetName="ellipse" Property="Stroke" Value="{StaticResource Ellipse.MouseOver.Brush}"/>
|
||||
</MultiDataTrigger>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter Property="Stroke" TargetName="ellipse" Value="{StaticResource Ellipse.Pressed.Brush}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter Property="Foreground" Value="Gray"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
@@ -1,11 +1,12 @@
|
||||
using System.Windows.Controls.Primitives;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls.Primitives;
|
||||
|
||||
namespace mbts.ui.controls {
|
||||
/// <summary>
|
||||
/// Interaction logic for ExpanderToggleButton.xaml
|
||||
/// </summary>
|
||||
partial class ExpanderToggleButton : ToggleButton {
|
||||
public ExpanderToggleButton() =>
|
||||
InitializeComponent();
|
||||
class ExpanderToggleButton : ToggleButton {
|
||||
static ExpanderToggleButton() =>
|
||||
DefaultStyleKeyProperty.OverrideMetadata(typeof(ExpanderToggleButton), new FrameworkPropertyMetadata(typeof(ExpanderToggleButton)));
|
||||
}
|
||||
}
|
||||
@@ -146,7 +146,7 @@
|
||||
<cp:NamedColorCellEditingTemplateSelector>
|
||||
<cp:NamedColorCellEditingTemplateSelector.EditableTemplate>
|
||||
<DataTemplate>
|
||||
<TextBox Text="{Binding name}" u:TextBoxBehavior.CommitOnEnter="True" u:TextBoxBehavior.SelectAllOnFocus="True" u:TextBoxBehavior.FocusOnLoaded="True"/>
|
||||
<TextBox Text="{Binding name}" u:TextBoxBehavior.CommitOnEnter="True" u:TextBoxBehavior.SelectAllOnFocus="True" u:Behavior.FocusOnLoaded="True"/>
|
||||
</DataTemplate>
|
||||
</cp:NamedColorCellEditingTemplateSelector.EditableTemplate>
|
||||
<cp:NamedColorCellEditingTemplateSelector.ReadOnlyTemplate>
|
||||
|
||||
@@ -3,12 +3,48 @@
|
||||
x:ClassModifier="internal"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:u="clr-namespace:mbts.ui"
|
||||
xmlns:c="clr-namespace:mbts.ui.controls"
|
||||
xmlns:cv="clr-namespace:mbts.ui.converters"
|
||||
xmlns:cp="clr-namespace:mbts.ui.controls.color_picker"
|
||||
Background="Transparent"
|
||||
Header="{Binding SelectedColor, RelativeSource={RelativeSource Self}, Mode=OneWay}"
|
||||
ShowExpanderArrow="False"
|
||||
>
|
||||
<c:PopupControl.Background>
|
||||
<LinearGradientBrush EndPoint="0,1" StartPoint="0,0">
|
||||
<GradientStop Color="{StaticResource ComboBoxBackgroundLightColor}" Offset="0.0"/>
|
||||
<GradientStop Color="{StaticResource ComboBoxBackgroundMediumColor}" Offset="1.0"/>
|
||||
</LinearGradientBrush>
|
||||
</c:PopupControl.Background>
|
||||
<c:PopupControl.BorderBrush>
|
||||
<SolidColorBrush Color="{StaticResource ComboBoxBorderLightColor}"/>
|
||||
</c:PopupControl.BorderBrush>
|
||||
<u:StateManager.Groups>
|
||||
<u:StateGroupsManager AttachToMembers="True">
|
||||
<u:StateDefinitionCollection>
|
||||
<u:StateDefinition>
|
||||
<Binding Path="Group.Target.IsEnabled" RelativeSource="{RelativeSource Self}">
|
||||
<Binding.Converter>
|
||||
<cv:BooleanToObjectConverter ValueForFalse="Disabled" ValueForTrue="Enabled"/>
|
||||
</Binding.Converter>
|
||||
</Binding>
|
||||
</u:StateDefinition>
|
||||
</u:StateDefinitionCollection>
|
||||
</u:StateGroupsManager>
|
||||
</u:StateManager.Groups>
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup Name="EnabledStates">
|
||||
<VisualState Name="Enabled"/>
|
||||
<VisualState Name="Disabled">
|
||||
<Storyboard>
|
||||
<ColorAnimation Storyboard.TargetProperty="(c:PopupControl.Background).(LinearGradientBrush.GradientStops)[0].(GradientStop.Color)" Duration="0" To="{StaticResource ComboBoxBackgroundMediumColor}"/>
|
||||
<ColorAnimation Storyboard.TargetProperty="(c:PopupControl.Background).(LinearGradientBrush.GradientStops)[1].(GradientStop.Color)" Duration="0" To="{StaticResource ComboBoxBackgroundMediumColor}"/>
|
||||
<ColorAnimation Storyboard.TargetProperty="(c:PopupControl.BorderBrush).(SolidColorBrush.Color)" Duration="0" To="{StaticResource ComboBoxDisabledBorderColor}"/>
|
||||
<DoubleAnimation Storyboard.TargetProperty="(c:PopupControl.Opacity)" Duration="0" To="0.5"/>
|
||||
</Storyboard>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
<c:PopupControl.HeaderTemplate>
|
||||
<DataTemplate>
|
||||
<Rectangle Stroke="LightBlue" StrokeThickness="1" MinWidth="25" MinHeight="18" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
|
||||
|
||||
@@ -84,4 +84,16 @@ namespace mbts.ui.controls {
|
||||
}
|
||||
public ColorOpacityProto to_proto() => new ColorOpacityProto(color.rgb_color(), opacity);
|
||||
}
|
||||
|
||||
class NamedColorPickerPopupVM : ColorPickerPopupVM {
|
||||
object _name;
|
||||
|
||||
public object name {
|
||||
get => _name;
|
||||
set => Set(ref _name, value);
|
||||
}
|
||||
|
||||
public override string ToString() =>
|
||||
_name == null ? color.ToHexString() : $"{_name}:{color.ToHexString()}";
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -80,7 +80,7 @@ namespace mbts.ui.controls {
|
||||
new[] {
|
||||
new ColumnDefinition { Width = new GridLength(visual_split.HorizontalSplit.Left, GridUnitType.Star) },
|
||||
new ColumnDefinition { Width = new GridLength(visual_split.HorizontalSplit.Right, GridUnitType.Star) },
|
||||
new ColumnDefinition { Width = GridLength.Auto }
|
||||
new ColumnDefinition { MinWidth = 34, Width = GridLength.Auto }
|
||||
});
|
||||
break;
|
||||
default:
|
||||
|
||||
@@ -4,339 +4,388 @@ using System;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using FontFamily = System.Windows.Media.FontFamily;
|
||||
|
||||
namespace mbts.ui.controls {
|
||||
partial class DIYGrid {
|
||||
public class HyperlinkCell : FrameworkElement, ICommandSource {
|
||||
// these fields each represent properties that are accessed frequently,
|
||||
// they are stored locally to reduce repetative DependencyProperty lookups
|
||||
Thickness _padding;
|
||||
HorizontalAlignment _horizontal_alignment;
|
||||
Brush _background, _foreground;
|
||||
double _glyph_width, _font_size;
|
||||
string _text;
|
||||
GlyphRun _glyph_run;
|
||||
GlyphContext _glyph_context;
|
||||
bool _is_mouse_over_text, _can_execute;
|
||||
Pen _underline_pen;
|
||||
class HyperlinkCell : FrameworkElement, ICommandSource {
|
||||
// these fields each represent properties that are accessed frequently,
|
||||
// they are stored locally to reduce repetative DependencyProperty lookups
|
||||
Thickness _padding => _gcr.padding;
|
||||
Brush _background, _foreground_backing, _border_brush_backing;
|
||||
double _glyph_width => _gcr.glyph_run.AdvanceWidths.Sum();
|
||||
GlyphRun _glyph_run => _gcr.glyph_run;
|
||||
bool _is_mouse_over_text, _can_execute;
|
||||
Pen _underline_pen_backing, _border_stroke_pen_backing;
|
||||
GlyphContentRect _gcr = new GlyphContentRect();
|
||||
|
||||
static HyperlinkCell() {
|
||||
WPFHelper.override_metadata<HorizontalAlignment, HyperlinkCell>(
|
||||
HorizontalAlignmentProperty,
|
||||
FrameworkPropertyMetadataOptions.AffectsArrange |
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(o, v) => {
|
||||
o._horizontal_alignment = v;
|
||||
o._glyph_run = null;
|
||||
}, HorizontalAlignment.Left);
|
||||
WPFHelper.override_metadata<double, HyperlinkCell>(
|
||||
WidthProperty,
|
||||
FrameworkPropertyMetadataOptions.AffectsArrange |
|
||||
FrameworkPropertyMetadataOptions.AffectsMeasure |
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(o, v) => o._glyph_run = null);
|
||||
}
|
||||
|
||||
public HyperlinkCell() {
|
||||
_background = Background;
|
||||
_foreground = Foreground;
|
||||
_horizontal_alignment = HorizontalAlignment;
|
||||
_padding = Padding;
|
||||
_font_size = FontSize;
|
||||
_text = Text;
|
||||
_glyph_context = GlyphContext.create(FontFamily, FontWeight);
|
||||
IsMouseDirectlyOverChanged += (s, e) => {
|
||||
if (_is_mouse_over_text != IsMouseDirectlyOver)
|
||||
_toggle_mouse_over_text();
|
||||
};
|
||||
}
|
||||
|
||||
#region Background
|
||||
public static readonly DependencyProperty BackgroundProperty =
|
||||
WPFHelper.create_dp<Brush, HyperlinkCell>(
|
||||
nameof(Background),
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(o, v) => o._background = v,
|
||||
Brushes.Transparent);
|
||||
|
||||
public Brush Background { get => GetValue(BackgroundProperty) as Brush; set => SetValue(BackgroundProperty, value); }
|
||||
#endregion
|
||||
|
||||
#region Foreground
|
||||
public static readonly DependencyProperty ForegroundProperty =
|
||||
WPFHelper.create_dp<Brush, HyperlinkCell>(
|
||||
nameof(Foreground),
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(p, v) => {
|
||||
p._foreground = v;
|
||||
p._underline_pen = new Pen(v, 1);
|
||||
}, Brushes.Black);
|
||||
|
||||
public Brush Foreground { get => (Brush)GetValue(ForegroundProperty); set => SetValue(ForegroundProperty, value); }
|
||||
#endregion
|
||||
|
||||
#region FontWeight
|
||||
public static readonly DependencyProperty FontWeightProperty =
|
||||
TextElement.FontWeightProperty.AddOwner(
|
||||
typeof(HyperlinkCell),
|
||||
new FrameworkPropertyMetadata(
|
||||
FontWeights.Normal,
|
||||
FrameworkPropertyMetadataOptions.AffectsMeasure |
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
WPFHelper.prop_changed_callback<FontWeight, HyperlinkCell>(
|
||||
(p, v) => {
|
||||
p._glyph_context = p._glyph_context.with(v);
|
||||
p._glyph_run = null;
|
||||
})));
|
||||
|
||||
public FontWeight FontWeight { get => (FontWeight)GetValue(FontWeightProperty); set => SetValue(FontWeightProperty, value); }
|
||||
#endregion
|
||||
|
||||
#region FontFamily
|
||||
public static readonly DependencyProperty FontFamilyProperty =
|
||||
TextElement.FontFamilyProperty.AddOwner(
|
||||
typeof(HyperlinkCell),
|
||||
new FrameworkPropertyMetadata(
|
||||
TextUtils.StandardFontFamily,
|
||||
FrameworkPropertyMetadataOptions.AffectsMeasure |
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
WPFHelper.prop_changed_callback<FontFamily, HyperlinkCell>(
|
||||
(p, v) => {
|
||||
p._glyph_context = p._glyph_context.with(v);
|
||||
p._glyph_run = null;
|
||||
})));
|
||||
|
||||
public FontFamily FontFamily { get => GetValue(FontFamilyProperty) as FontFamily; set => SetValue(FontFamilyProperty, value); }
|
||||
#endregion
|
||||
|
||||
#region FontSize
|
||||
public static readonly DependencyProperty FontSizeProperty =
|
||||
TextElement.FontSizeProperty.AddOwner(
|
||||
typeof(HyperlinkCell),
|
||||
new FrameworkPropertyMetadata(
|
||||
FontSizes.Small,
|
||||
FrameworkPropertyMetadataOptions.AffectsMeasure |
|
||||
FrameworkPropertyMetadataOptions.AffectsRender |
|
||||
FrameworkPropertyMetadataOptions.Inherits,
|
||||
WPFHelper.prop_changed_callback<double, HyperlinkCell>(
|
||||
(p, v) => {
|
||||
p._font_size = v;
|
||||
p._glyph_run = null;
|
||||
})));
|
||||
|
||||
public double FontSize { get => (double)GetValue(FontSizeProperty); set => SetValue(FontSizeProperty, value); }
|
||||
#endregion
|
||||
|
||||
#region Padding
|
||||
public static readonly DependencyProperty PaddingProperty =
|
||||
WPFHelper.create_dp<Thickness, HyperlinkCell>(
|
||||
nameof(Padding),
|
||||
FrameworkPropertyMetadataOptions.AffectsMeasure |
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(p, v) => {
|
||||
p._padding = v;
|
||||
p._glyph_run = null;
|
||||
}, DefaultCellPadding);
|
||||
|
||||
public Thickness Padding { get => (Thickness)GetValue(PaddingProperty); set => SetValue(PaddingProperty, value); }
|
||||
#endregion
|
||||
|
||||
#region Text
|
||||
public static readonly DependencyProperty TextProperty =
|
||||
WPFHelper.create_dp<string, HyperlinkCell>(
|
||||
nameof(Text),
|
||||
FrameworkPropertyMetadataOptions.AffectsMeasure |
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(o, v) => {
|
||||
o._text = v;
|
||||
o._glyph_run = null;
|
||||
}, string.Empty);
|
||||
|
||||
public string Text { get => GetValue(TextProperty) as string; set => SetValue(TextProperty, value); }
|
||||
#endregion
|
||||
|
||||
#region ICommandSource
|
||||
|
||||
#region Command
|
||||
public static readonly DependencyProperty CommandProperty =
|
||||
WPFHelper.create_dp<ICommand, HyperlinkCell>(
|
||||
nameof(Command),
|
||||
(s, old_val, new_val) => {
|
||||
if (old_val != null)
|
||||
CanExecuteChangedEventManager.RemoveHandler(old_val, s.OnCanExecuteChanged);
|
||||
if (new_val != null)
|
||||
CanExecuteChangedEventManager.AddHandler(new_val, s.OnCanExecuteChanged);
|
||||
s.UpdateCanExecute();
|
||||
});
|
||||
|
||||
[Bindable(true), Category("Action")]
|
||||
[Localizability(LocalizationCategory.NeverLocalize)]
|
||||
public ICommand Command { get => (ICommand)GetValue(CommandProperty); set => SetValue(CommandProperty, value); }
|
||||
#endregion
|
||||
|
||||
#region CommandParameter
|
||||
public static readonly DependencyProperty CommandParameterProperty =
|
||||
DependencyProperty.Register(
|
||||
nameof(CommandParameter),
|
||||
typeof(object),
|
||||
typeof(HyperlinkCell));
|
||||
|
||||
[Bindable(true), Category("Action")]
|
||||
[Localizability(LocalizationCategory.NeverLocalize)]
|
||||
public object CommandParameter { get => GetValue(CommandParameterProperty); set => SetValue(CommandParameterProperty, value); }
|
||||
#endregion
|
||||
|
||||
#region CommandTarget
|
||||
public static readonly DependencyProperty CommandTargetProperty =
|
||||
DependencyProperty.Register(
|
||||
nameof(CommandTarget),
|
||||
typeof(IInputElement),
|
||||
typeof(HyperlinkCell));
|
||||
|
||||
[Bindable(true), Category("Action")]
|
||||
public IInputElement CommandTarget { get => (IInputElement)GetValue(CommandTargetProperty); set => SetValue(CommandTargetProperty, value); }
|
||||
#endregion
|
||||
|
||||
protected override bool IsEnabledCore => base.IsEnabledCore && CanExecute;
|
||||
|
||||
bool CanExecute {
|
||||
get => _can_execute;
|
||||
set {
|
||||
if (_can_execute != value) {
|
||||
_can_execute = value;
|
||||
CoerceValue(IsEnabledProperty);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void OnCanExecuteChanged(object sender, EventArgs e) =>
|
||||
UpdateCanExecute();
|
||||
|
||||
void UpdateCanExecute() =>
|
||||
CanExecute = Command == null || CommandHelper.CanExecuteCommandSource(this);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Paraphrased/Simplified from https://referencesource.microsoft.com/#PresentationFramework/src/Framework/System/Windows/Documents/Hyperlink.cs
|
||||
|
||||
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e) {
|
||||
base.OnMouseLeftButtonDown(e);
|
||||
_check_mouse_over_text(e);
|
||||
if (_is_mouse_over_text) {
|
||||
// Hyperlink should take focus when left mouse button is clicked on it
|
||||
// This is consistent with all ButtonBase controls and current Win32 behavior
|
||||
Focus();
|
||||
|
||||
// It is possible that the mouse state could have changed during all of
|
||||
// the call-outs that have happened so far.
|
||||
if (e.ButtonState == MouseButtonState.Pressed) {
|
||||
// Capture the mouse, and make sure we got it.
|
||||
CaptureMouse();
|
||||
}
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e) {
|
||||
base.OnMouseLeftButtonUp(e);
|
||||
_check_mouse_over_text(e);
|
||||
if (IsMouseCaptured)
|
||||
ReleaseMouseCapture();
|
||||
if (_can_execute && _is_mouse_over_text)
|
||||
CommandHelper.ExecuteCommandSource(this);
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
protected override void OnPreviewMouseMove(MouseEventArgs e) => _check_mouse_over_text(e);
|
||||
protected override void OnMouseEnter(MouseEventArgs e) => _check_mouse_over_text(e);
|
||||
protected override void OnMouseLeave(MouseEventArgs e) => _check_mouse_over_text(e);
|
||||
|
||||
protected override Size ArrangeOverride(Size finalSize) {
|
||||
if (_glyph_run == null)
|
||||
_set_glyph(finalSize);
|
||||
return finalSize;
|
||||
}
|
||||
|
||||
protected override Size MeasureOverride(Size availableSize) {
|
||||
if (_glyph_run == null)
|
||||
_set_glyph(availableSize);
|
||||
return new Size(Math.Max(availableSize.Width, _padding.Left + _glyph_width + _padding.Right),
|
||||
Math.Max(availableSize.Height, _padding.Top + _font_size + _padding.Bottom));
|
||||
}
|
||||
|
||||
void _set_glyph(Size size) {
|
||||
_glyph_run = _glyph_context.get_glyph_run(_text, _font_size, size, _horizontal_alignment, VerticalAlignment.Center, _padding);
|
||||
_glyph_width = _glyph_run == null ? 0 : _glyph_run.AdvanceWidths.Sum();
|
||||
}
|
||||
|
||||
protected override void OnRender(DrawingContext drawingContext) {
|
||||
var rs = RenderSize;
|
||||
var rect = new Rect(0, 0, rs.Width, rs.Height);
|
||||
var bg = Background;
|
||||
if (bg != null)
|
||||
drawingContext.DrawRectangle(bg, null, rect);
|
||||
if (_glyph_run != null) {
|
||||
drawingContext.PushClip(new RectangleGeometry(rect));
|
||||
drawingContext.DrawGlyphRun(_foreground, _glyph_run);
|
||||
if (_is_mouse_over_text) {
|
||||
// draw underline
|
||||
Point pt;
|
||||
switch (_horizontal_alignment) {
|
||||
case HorizontalAlignment.Left:
|
||||
pt = new Point(Padding.Left, 0.5 * (ActualHeight + _font_size) + 1);
|
||||
break;
|
||||
case HorizontalAlignment.Right:
|
||||
pt = new Point(ActualWidth - Padding.Right - _glyph_width, 0.5 * (ActualHeight + _font_size) + 1);
|
||||
break;
|
||||
case HorizontalAlignment.Center:
|
||||
default:
|
||||
pt = new Point(0.5 * (ActualWidth - _glyph_width), 0.5 * (ActualHeight + _font_size) + 1);
|
||||
break;
|
||||
}
|
||||
drawingContext.DrawLine(_underline_pen, pt, new Point(pt.X + _glyph_width, pt.Y));
|
||||
|
||||
}
|
||||
drawingContext.Pop();
|
||||
}
|
||||
}
|
||||
|
||||
bool _point_horizontally_over_text(double x) {
|
||||
switch (_horizontal_alignment) {
|
||||
case HorizontalAlignment.Left:
|
||||
return x >= _padding.Left && x <= _padding.Left + _glyph_width;
|
||||
case HorizontalAlignment.Right:
|
||||
return x >= ActualWidth - Padding.Right - _glyph_width && x <= ActualWidth - _padding.Right;
|
||||
case HorizontalAlignment.Center:
|
||||
default:
|
||||
return x >= 0.5 * (ActualWidth - _glyph_width) && x <= 0.5 * (ActualWidth - _glyph_width) + _glyph_width;
|
||||
}
|
||||
}
|
||||
|
||||
void _check_mouse_over_text(MouseEventArgs e) {
|
||||
var pt = e.GetPosition(this);
|
||||
bool over_text =
|
||||
_glyph_run != null &&
|
||||
_point_horizontally_over_text(pt.X) &&
|
||||
pt.Y >= _padding.Top &&
|
||||
pt.Y <= _padding.Top + _font_size;
|
||||
if (_is_mouse_over_text != over_text)
|
||||
_toggle_mouse_over_text();
|
||||
}
|
||||
|
||||
void _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();
|
||||
Brush _foreground {
|
||||
get => _foreground_backing;
|
||||
set {
|
||||
_foreground_backing = value;
|
||||
_underline_pen_backing = null;
|
||||
}
|
||||
}
|
||||
Brush _border_brush {
|
||||
get => _border_brush_backing;
|
||||
set {
|
||||
_border_brush_backing = value;
|
||||
_border_stroke_pen_backing = null;
|
||||
}
|
||||
}
|
||||
Thickness _border_thickness {
|
||||
get => _gcr.border_thickness;
|
||||
set {
|
||||
_gcr.border_thickness = new Thickness(value.Left);
|
||||
_border_stroke_pen_backing = null;
|
||||
}
|
||||
}
|
||||
Pen _underline_pen => _underline_pen_backing ?? (_foreground == null ? null : (_underline_pen_backing = new Pen(_foreground, 1)));
|
||||
Pen _border_stroke_pen =>
|
||||
// border_thickness.Left is used as the uniform border thickness. This can be changed if need be.
|
||||
_border_stroke_pen_backing == null && _border_brush != null && _border_thickness.Left > 0 ?
|
||||
(_border_stroke_pen_backing = _border_stroke_pen_backing = new Pen(_border_brush, _border_thickness.Left)) :
|
||||
_border_stroke_pen_backing;
|
||||
|
||||
static HyperlinkCell() {
|
||||
WPFHelper.override_metadata<HorizontalAlignment, HyperlinkCell>(
|
||||
HorizontalAlignmentProperty,
|
||||
FrameworkPropertyMetadataOptions.AffectsArrange |
|
||||
FrameworkPropertyMetadataOptions.AffectsMeasure |
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(o, v) => o._gcr.horizontal_alignment = v, HorizontalAlignment.Left);
|
||||
WPFHelper.override_metadata<double, HyperlinkCell>(
|
||||
WidthProperty,
|
||||
FrameworkPropertyMetadataOptions.AffectsArrange |
|
||||
FrameworkPropertyMetadataOptions.AffectsMeasure |
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(o, v) => o._gcr.width = v,
|
||||
double.NaN);
|
||||
}
|
||||
|
||||
public HyperlinkCell() {
|
||||
_gcr.min_width = MinWidth;
|
||||
_gcr.width = Width;
|
||||
_gcr.max_width = MaxWidth;
|
||||
_gcr.min_height = MinHeight;
|
||||
_gcr.height = Height;
|
||||
_gcr.max_height = Height;
|
||||
_gcr.padding = _underline_padding(Padding);
|
||||
_gcr.border_thickness = BorderThickness;
|
||||
_gcr.horizontal_alignment = HorizontalAlignment;
|
||||
_gcr.horizontal_content_alignment = HorizontalContentAlignment;
|
||||
_gcr.vertical_alignment = VerticalAlignment;
|
||||
_gcr.vertical_content_alignment = VerticalContentAlignment;
|
||||
_background = Background;
|
||||
_foreground_backing = Foreground;
|
||||
_border_brush = BorderBrush;
|
||||
_gcr.font_size = FontSize;
|
||||
if (Text is string str && !string.IsNullOrEmpty(str))
|
||||
_gcr.text_lines = new[] { str };
|
||||
_gcr.glyph_context = GlyphContext.create(FontFamily, FontWeight);
|
||||
}
|
||||
|
||||
#region BorderThickness
|
||||
public static readonly DependencyProperty BorderThicknessProperty =
|
||||
Control.BorderThicknessProperty.AddOwner(typeof(HyperlinkCell),
|
||||
new FrameworkPropertyMetadata(
|
||||
new Thickness(0),
|
||||
FrameworkPropertyMetadataOptions.AffectsMeasure |
|
||||
FrameworkPropertyMetadataOptions.AffectsArrange,
|
||||
(d, e) => ((HyperlinkCell)d)._gcr.border_thickness = (Thickness)e.NewValue));
|
||||
|
||||
public Thickness BorderThickness {
|
||||
get => (Thickness)GetValue(BorderThicknessProperty);
|
||||
set => SetValue(BorderThicknessProperty, value);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region BorderBrush
|
||||
public static readonly DependencyProperty BorderBrushProperty =
|
||||
Control.BorderBrushProperty.AddOwner(typeof(HyperlinkCell),
|
||||
new FrameworkPropertyMetadata(
|
||||
null,
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(d, e) => ((HyperlinkCell)d)._border_brush = e.NewValue as Brush));
|
||||
|
||||
public Brush BorderBrush {
|
||||
get => (Brush)GetValue(BorderBrushProperty);
|
||||
set => SetValue(BorderBrushProperty, value);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region HorizontalContentAlignment
|
||||
public static readonly DependencyProperty HorizontalContentAlignmentProperty =
|
||||
Control.HorizontalContentAlignmentProperty.AddOwner(typeof(HyperlinkCell),
|
||||
new FrameworkPropertyMetadata(
|
||||
HorizontalAlignment.Center,
|
||||
FrameworkPropertyMetadataOptions.AffectsArrange,
|
||||
(d, e) => ((HyperlinkCell)d)._gcr.horizontal_content_alignment = (HorizontalAlignment)e.NewValue));
|
||||
|
||||
public HorizontalAlignment HorizontalContentAlignment {
|
||||
get => (HorizontalAlignment)GetValue(HorizontalContentAlignmentProperty);
|
||||
set => SetValue(HorizontalContentAlignmentProperty, value);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region VerticalContentAlignment
|
||||
public static readonly DependencyProperty VerticalContentAlignmentProperty = Control.VerticalContentAlignmentProperty.AddOwner(typeof(HyperlinkCell),
|
||||
new FrameworkPropertyMetadata(
|
||||
VerticalAlignment.Center,
|
||||
FrameworkPropertyMetadataOptions.AffectsArrange,
|
||||
(d, e) => ((HyperlinkCell)d)._gcr.vertical_content_alignment = (VerticalAlignment)e.NewValue));
|
||||
|
||||
public VerticalAlignment VerticalContentAlignment {
|
||||
get => (VerticalAlignment)GetValue(VerticalContentAlignmentProperty);
|
||||
set => SetValue(VerticalContentAlignmentProperty, value);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Background
|
||||
public static readonly DependencyProperty BackgroundProperty =
|
||||
WPFHelper.create_dp<Brush, HyperlinkCell>(
|
||||
nameof(Background),
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(o, v) => o._background = v,
|
||||
Brushes.Transparent);
|
||||
|
||||
public Brush Background { get => GetValue(BackgroundProperty) as Brush; set => SetValue(BackgroundProperty, value); }
|
||||
#endregion
|
||||
|
||||
#region Foreground
|
||||
public static readonly DependencyProperty ForegroundProperty =
|
||||
WPFHelper.create_dp<Brush, HyperlinkCell>(
|
||||
nameof(Foreground),
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(p, v) => p._foreground = v,
|
||||
Brushes.Black);
|
||||
|
||||
public Brush Foreground { get => (Brush)GetValue(ForegroundProperty); set => SetValue(ForegroundProperty, value); }
|
||||
#endregion
|
||||
|
||||
#region FontWeight
|
||||
public static readonly DependencyProperty FontWeightProperty =
|
||||
TextElement.FontWeightProperty.AddOwner(
|
||||
typeof(HyperlinkCell),
|
||||
new FrameworkPropertyMetadata(
|
||||
FontWeights.Normal,
|
||||
FrameworkPropertyMetadataOptions.AffectsMeasure |
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
WPFHelper.prop_changed_callback<FontWeight, HyperlinkCell>(
|
||||
(p, v) => p._gcr.glyph_context = p._gcr.glyph_context.with(v))));
|
||||
|
||||
public FontWeight FontWeight { get => (FontWeight)GetValue(FontWeightProperty); set => SetValue(FontWeightProperty, value); }
|
||||
#endregion
|
||||
|
||||
#region FontFamily
|
||||
public static readonly DependencyProperty FontFamilyProperty =
|
||||
TextElement.FontFamilyProperty.AddOwner(
|
||||
typeof(HyperlinkCell),
|
||||
new FrameworkPropertyMetadata(
|
||||
TextUtils.StandardFontFamily,
|
||||
FrameworkPropertyMetadataOptions.AffectsMeasure |
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
WPFHelper.prop_changed_callback<FontFamily, HyperlinkCell>(
|
||||
(p, v) => p._gcr.glyph_context = p._gcr.glyph_context.with(v))));
|
||||
|
||||
public FontFamily FontFamily { get => GetValue(FontFamilyProperty) as FontFamily; set => SetValue(FontFamilyProperty, value); }
|
||||
#endregion
|
||||
|
||||
#region FontSize
|
||||
public static readonly DependencyProperty FontSizeProperty =
|
||||
TextElement.FontSizeProperty.AddOwner(
|
||||
typeof(HyperlinkCell),
|
||||
new FrameworkPropertyMetadata(
|
||||
FontSizes.Small,
|
||||
FrameworkPropertyMetadataOptions.AffectsMeasure |
|
||||
FrameworkPropertyMetadataOptions.AffectsRender |
|
||||
FrameworkPropertyMetadataOptions.Inherits,
|
||||
WPFHelper.prop_changed_callback<double, HyperlinkCell>(
|
||||
(p, v) => p._gcr.font_size = v)));
|
||||
|
||||
public double FontSize { get => (double)GetValue(FontSizeProperty); set => SetValue(FontSizeProperty, value); }
|
||||
#endregion
|
||||
|
||||
#region Padding
|
||||
public static readonly DependencyProperty PaddingProperty =
|
||||
WPFHelper.create_dp<Thickness, HyperlinkCell>(
|
||||
nameof(Padding),
|
||||
FrameworkPropertyMetadataOptions.AffectsMeasure |
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(p, v) => p._gcr.padding = _underline_padding(v), DIYGrid.DefaultCellPadding);
|
||||
|
||||
public Thickness Padding { get => (Thickness)GetValue(PaddingProperty); set => SetValue(PaddingProperty, value); }
|
||||
#endregion
|
||||
|
||||
#region Text
|
||||
public static readonly DependencyProperty TextProperty =
|
||||
WPFHelper.create_dp<string, HyperlinkCell>(
|
||||
nameof(Text),
|
||||
FrameworkPropertyMetadataOptions.AffectsMeasure |
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(p, v) => p._gcr.text_lines = string.IsNullOrEmpty(v) ? null : new[] { v }, string.Empty);
|
||||
|
||||
public string Text { get => GetValue(TextProperty) as string; set => SetValue(TextProperty, value); }
|
||||
#endregion
|
||||
|
||||
#region ICommandSource
|
||||
|
||||
#region Command
|
||||
public static readonly DependencyProperty CommandProperty =
|
||||
WPFHelper.create_dp<ICommand, HyperlinkCell>(
|
||||
nameof(Command),
|
||||
(s, old_val, new_val) => {
|
||||
if (old_val != null)
|
||||
CanExecuteChangedEventManager.RemoveHandler(old_val, s.OnCanExecuteChanged);
|
||||
if (new_val != null)
|
||||
CanExecuteChangedEventManager.AddHandler(new_val, s.OnCanExecuteChanged);
|
||||
s.UpdateCanExecute();
|
||||
});
|
||||
|
||||
[Bindable(true), Category("Action")]
|
||||
[Localizability(LocalizationCategory.NeverLocalize)]
|
||||
public ICommand Command { get => (ICommand)GetValue(CommandProperty); set => SetValue(CommandProperty, value); }
|
||||
#endregion
|
||||
|
||||
#region CommandParameter
|
||||
public static readonly DependencyProperty CommandParameterProperty =
|
||||
DependencyProperty.Register(
|
||||
nameof(CommandParameter),
|
||||
typeof(object),
|
||||
typeof(HyperlinkCell));
|
||||
|
||||
[Bindable(true), Category("Action")]
|
||||
[Localizability(LocalizationCategory.NeverLocalize)]
|
||||
public object CommandParameter { get => GetValue(CommandParameterProperty); set => SetValue(CommandParameterProperty, value); }
|
||||
#endregion
|
||||
|
||||
#region CommandTarget
|
||||
public static readonly DependencyProperty CommandTargetProperty =
|
||||
DependencyProperty.Register(
|
||||
nameof(CommandTarget),
|
||||
typeof(IInputElement),
|
||||
typeof(HyperlinkCell));
|
||||
|
||||
[Bindable(true), Category("Action")]
|
||||
public IInputElement CommandTarget { get => (IInputElement)GetValue(CommandTargetProperty); set => SetValue(CommandTargetProperty, value); }
|
||||
#endregion
|
||||
|
||||
protected override bool IsEnabledCore => base.IsEnabledCore && CanExecute;
|
||||
|
||||
bool CanExecute {
|
||||
get => _can_execute;
|
||||
set {
|
||||
if (_can_execute != value) {
|
||||
_can_execute = value;
|
||||
CoerceValue(IsEnabledProperty);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void OnCanExecuteChanged(object sender, EventArgs e) =>
|
||||
UpdateCanExecute();
|
||||
|
||||
void UpdateCanExecute() =>
|
||||
CanExecute = Command == null || CommandHelper.CanExecuteCommandSource(this);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Paraphrased/Simplified from https://referencesource.microsoft.com/#PresentationFramework/src/Framework/System/Windows/Documents/Hyperlink.cs
|
||||
|
||||
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e) {
|
||||
base.OnMouseLeftButtonDown(e);
|
||||
_check_mouse_over_text(e);
|
||||
if (_is_mouse_over_text) {
|
||||
// Hyperlink should take focus when left mouse button is clicked on it
|
||||
// This is consistent with all ButtonBase controls and current Win32 behavior
|
||||
Focus();
|
||||
|
||||
// It is possible that the mouse state could have changed during all of
|
||||
// the call-outs that have happened so far.
|
||||
if (e.ButtonState == MouseButtonState.Pressed) {
|
||||
// Capture the mouse, and make sure we got it.
|
||||
CaptureMouse();
|
||||
}
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e) {
|
||||
base.OnMouseLeftButtonUp(e);
|
||||
_check_mouse_over_text(e);
|
||||
if (IsMouseCaptured)
|
||||
ReleaseMouseCapture();
|
||||
if (_can_execute && _is_mouse_over_text)
|
||||
CommandHelper.ExecuteCommandSource(this);
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
protected override void OnIsMouseDirectlyOverChanged(DependencyPropertyChangedEventArgs e) {
|
||||
if (_is_mouse_over_text != IsMouseDirectlyOver)
|
||||
_toggle_mouse_over_text();
|
||||
}
|
||||
protected override void OnPreviewMouseMove(MouseEventArgs e) => _check_mouse_over_text(e);
|
||||
protected override void OnMouseEnter(MouseEventArgs e) => _check_mouse_over_text(e);
|
||||
protected override void OnMouseLeave(MouseEventArgs e) => _check_mouse_over_text(e);
|
||||
|
||||
protected override Size ArrangeOverride(Size finalSize) => finalSize;
|
||||
|
||||
protected override Size MeasureOverride(Size availableSize) {
|
||||
_gcr.available_size = availableSize;
|
||||
if (double.IsInfinity(_gcr.layout_size.Height) || double.IsInfinity(_gcr.layout_size.Width)) {
|
||||
_gcr.remeasure();
|
||||
}
|
||||
return _gcr.layout_size;
|
||||
}
|
||||
|
||||
protected override 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) {
|
||||
drawingContext.PushClip(new RectangleGeometry(rect));
|
||||
drawingContext.DrawGlyphRun(_foreground, _glyph_run);
|
||||
if (_is_mouse_over_text) {
|
||||
// draw underline
|
||||
double x = _gcr.glyph_run.BaselineOrigin.X;
|
||||
double y = _gcr.glyph_run.BaselineOrigin.Y + 2;
|
||||
drawingContext.DrawLine(_underline_pen, new Point(x, y), new Point(x + _gcr.glyph_run.AdvanceWidths.Sum(), y));
|
||||
}
|
||||
drawingContext.Pop();
|
||||
}
|
||||
}
|
||||
|
||||
static Thickness _underline_padding(Thickness padding) =>
|
||||
new Thickness(padding.Left, padding.Top, padding.Right, padding.Bottom + 2);
|
||||
|
||||
void _check_mouse_over_text(MouseEventArgs e) {
|
||||
var pt = e.GetPosition(this);
|
||||
var orig = _gcr.content_area;
|
||||
var exp = orig.Expanded(1);
|
||||
if (_is_mouse_over_text != exp.Contains(pt)) {
|
||||
bts.Log.assert(_is_mouse_over_text != _gcr.content_area.Expanded(1).Contains(e.GetPosition(this)));
|
||||
_toggle_mouse_over_text();
|
||||
return;
|
||||
}
|
||||
if (_is_mouse_over_text != _gcr.content_area.Expanded(1).Contains(e.GetPosition(this)))
|
||||
_toggle_mouse_over_text();
|
||||
}
|
||||
|
||||
void _toggle_mouse_over_text() {
|
||||
_is_mouse_over_text = !_is_mouse_over_text;
|
||||
if (_is_mouse_over_text)
|
||||
SetCurrentValue(CursorProperty, Cursors.Hand);
|
||||
else
|
||||
ClearValue(CursorProperty);
|
||||
InvalidateVisual();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,178 +8,263 @@ using System.Windows.Media;
|
||||
using FontFamily = System.Windows.Media.FontFamily;
|
||||
|
||||
namespace mbts.ui.controls {
|
||||
partial class DIYGrid : Control {
|
||||
public class TextCell : FrameworkElement {
|
||||
// these fields each represent properties that are accessed frequently,
|
||||
// they are stored locally to reduce repetative DependencyProperty lookups
|
||||
Thickness _padding;
|
||||
HorizontalAlignment _horizontal_alignment;
|
||||
Brush _background, _foreground;
|
||||
double _glyph_width, _font_size;
|
||||
string _text;
|
||||
GlyphRun _glyph_run;
|
||||
GlyphContext _glyph_context;
|
||||
class TextCell : FrameworkElement {
|
||||
// these fields each represent properties that are accessed frequently,
|
||||
// they are stored locally to reduce repetative DependencyProperty lookups
|
||||
Thickness _padding => _gcr.padding;
|
||||
Brush _background, _foreground, _border_brush_backing;
|
||||
double _glyph_width => _gcr.glyph_run.AdvanceWidths.Sum();
|
||||
GlyphRun _glyph_run => _gcr.glyph_run;
|
||||
Pen _border_stroke_pen_backing;
|
||||
GlyphContentRect _gcr = new GlyphContentRect();
|
||||
|
||||
static TextCell() {
|
||||
WPFHelper.override_metadata<HorizontalAlignment, TextCell>(
|
||||
HorizontalAlignmentProperty,
|
||||
FrameworkPropertyMetadataOptions.AffectsArrange |
|
||||
Brush _border_brush {
|
||||
get => _border_brush_backing;
|
||||
set {
|
||||
_border_brush_backing = value;
|
||||
_border_stroke_pen_backing = null;
|
||||
}
|
||||
}
|
||||
Thickness _border_thickness {
|
||||
get => _gcr.border_thickness;
|
||||
set {
|
||||
_gcr.border_thickness = new Thickness(value.Left);
|
||||
_border_stroke_pen_backing = null;
|
||||
}
|
||||
}
|
||||
Pen _border_stroke_pen =>
|
||||
// border_thickness.Left is used as the uniform border thickness. This can be changed if need be.
|
||||
_border_stroke_pen_backing == null && _border_brush != null && _border_thickness.Left > 0 ?
|
||||
(_border_stroke_pen_backing = _border_stroke_pen_backing = new Pen(_border_brush, _border_thickness.Left)) :
|
||||
_border_stroke_pen_backing;
|
||||
|
||||
static TextCell() {
|
||||
WPFHelper.override_metadata<HorizontalAlignment, TextCell>(
|
||||
HorizontalAlignmentProperty,
|
||||
FrameworkPropertyMetadataOptions.AffectsArrange |
|
||||
FrameworkPropertyMetadataOptions.AffectsMeasure |
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(o, v) => o._gcr.horizontal_alignment = v, HorizontalAlignment.Left);
|
||||
WPFHelper.override_metadata<double, TextCell>(
|
||||
WidthProperty,
|
||||
FrameworkPropertyMetadataOptions.AffectsArrange |
|
||||
FrameworkPropertyMetadataOptions.AffectsMeasure |
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(o, v) => o._gcr.width = v,
|
||||
double.NaN);
|
||||
}
|
||||
|
||||
public TextCell() {
|
||||
_gcr.min_width = MinWidth;
|
||||
_gcr.width = Width;
|
||||
_gcr.max_width = MaxWidth;
|
||||
_gcr.min_height = MinHeight;
|
||||
_gcr.height = Height;
|
||||
_gcr.max_height = Height;
|
||||
_gcr.padding = Padding;
|
||||
_gcr.border_thickness = BorderThickness;
|
||||
_gcr.horizontal_alignment = HorizontalAlignment;
|
||||
_gcr.horizontal_content_alignment = HorizontalContentAlignment;
|
||||
_gcr.vertical_alignment = VerticalAlignment;
|
||||
_gcr.vertical_content_alignment = VerticalContentAlignment;
|
||||
_background = Background;
|
||||
_foreground = Foreground;
|
||||
_border_brush = BorderBrush;
|
||||
_gcr.font_size = FontSize;
|
||||
if (Text is string str && !string.IsNullOrEmpty(str))
|
||||
_gcr.text_lines = new[] { str };
|
||||
_gcr.glyph_context = GlyphContext.create(FontFamily, FontWeight);
|
||||
}
|
||||
|
||||
#region BorderThickness
|
||||
public static readonly DependencyProperty BorderThicknessProperty =
|
||||
Control.BorderThicknessProperty.AddOwner(typeof(TextCell),
|
||||
new FrameworkPropertyMetadata(
|
||||
new Thickness(0),
|
||||
FrameworkPropertyMetadataOptions.AffectsMeasure |
|
||||
FrameworkPropertyMetadataOptions.AffectsArrange,
|
||||
(d, e) => ((TextCell)d)._gcr.border_thickness = (Thickness)e.NewValue));
|
||||
|
||||
public Thickness BorderThickness {
|
||||
get => (Thickness)GetValue(BorderThicknessProperty);
|
||||
set => SetValue(BorderThicknessProperty, value);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region BorderBrush
|
||||
public static readonly DependencyProperty BorderBrushProperty =
|
||||
Control.BorderBrushProperty.AddOwner(typeof(TextCell),
|
||||
new FrameworkPropertyMetadata(
|
||||
null,
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(d, e) => ((TextCell)d)._border_brush = e.NewValue as Brush));
|
||||
|
||||
public Brush BorderBrush {
|
||||
get => (Brush)GetValue(BorderBrushProperty);
|
||||
set => SetValue(BorderBrushProperty, value);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region HorizontalContentAlignment
|
||||
public static readonly DependencyProperty HorizontalContentAlignmentProperty =
|
||||
Control.HorizontalContentAlignmentProperty.AddOwner(typeof(TextCell),
|
||||
new FrameworkPropertyMetadata(
|
||||
HorizontalAlignment.Center,
|
||||
FrameworkPropertyMetadataOptions.AffectsArrange,
|
||||
(d, e) => {
|
||||
if (e.OldValue is HorizontalAlignment oldha) {
|
||||
if (oldha == HorizontalAlignment.Right) {
|
||||
|
||||
}
|
||||
}
|
||||
((TextCell)d)._gcr.horizontal_content_alignment = (HorizontalAlignment)e.NewValue;
|
||||
}));
|
||||
|
||||
public HorizontalAlignment HorizontalContentAlignment {
|
||||
get => (HorizontalAlignment)GetValue(HorizontalContentAlignmentProperty);
|
||||
set => SetValue(HorizontalContentAlignmentProperty, value);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region VerticalContentAlignment
|
||||
public static readonly DependencyProperty VerticalContentAlignmentProperty = Control.VerticalContentAlignmentProperty.AddOwner(typeof(TextCell),
|
||||
new FrameworkPropertyMetadata(
|
||||
VerticalAlignment.Center,
|
||||
FrameworkPropertyMetadataOptions.AffectsArrange,
|
||||
(d, e) => ((TextCell)d)._gcr.vertical_content_alignment = (VerticalAlignment)e.NewValue));
|
||||
|
||||
public VerticalAlignment VerticalContentAlignment {
|
||||
get => (VerticalAlignment)GetValue(VerticalContentAlignmentProperty);
|
||||
set => SetValue(VerticalContentAlignmentProperty, value);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Background
|
||||
public static readonly DependencyProperty BackgroundProperty =
|
||||
WPFHelper.create_dp<Brush, TextCell>(
|
||||
nameof(Background),
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(o, v) => o._background = v,
|
||||
Brushes.Transparent);
|
||||
|
||||
public Brush Background { get => GetValue(BackgroundProperty) as Brush; set => SetValue(BackgroundProperty, value); }
|
||||
#endregion
|
||||
|
||||
#region Foreground
|
||||
public static readonly DependencyProperty ForegroundProperty =
|
||||
WPFHelper.create_dp<Brush, TextCell>(
|
||||
nameof(Foreground),
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(p, v) => p._foreground = v,
|
||||
Brushes.Black);
|
||||
|
||||
public Brush Foreground { get => (Brush)GetValue(ForegroundProperty); set => SetValue(ForegroundProperty, value); }
|
||||
#endregion
|
||||
|
||||
#region FontWeight
|
||||
public static readonly DependencyProperty FontWeightProperty =
|
||||
TextElement.FontWeightProperty.AddOwner(
|
||||
typeof(TextCell),
|
||||
new FrameworkPropertyMetadata(
|
||||
FontWeights.Normal,
|
||||
FrameworkPropertyMetadataOptions.AffectsMeasure |
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(o, v) => {
|
||||
o._horizontal_alignment = v;
|
||||
o._glyph_run = null;
|
||||
},
|
||||
HorizontalAlignment.Left);
|
||||
WPFHelper.override_metadata<double, TextCell>(
|
||||
WidthProperty,
|
||||
FrameworkPropertyMetadataOptions.AffectsArrange |
|
||||
WPFHelper.prop_changed_callback<FontWeight, TextCell>(
|
||||
(p, v) => p._gcr.glyph_context = p._gcr.glyph_context.with(v))));
|
||||
|
||||
public FontWeight FontWeight { get => (FontWeight)GetValue(FontWeightProperty); set => SetValue(FontWeightProperty, value); }
|
||||
#endregion
|
||||
|
||||
#region FontFamily
|
||||
public static readonly DependencyProperty FontFamilyProperty =
|
||||
TextElement.FontFamilyProperty.AddOwner(
|
||||
typeof(TextCell),
|
||||
new FrameworkPropertyMetadata(
|
||||
TextUtils.StandardFontFamily,
|
||||
FrameworkPropertyMetadataOptions.AffectsMeasure |
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(o, v) => o._glyph_run = null,
|
||||
double.NaN);
|
||||
}
|
||||
WPFHelper.prop_changed_callback<FontFamily, TextCell>(
|
||||
(p, v) => p._gcr.glyph_context = p._gcr.glyph_context.with(v))));
|
||||
|
||||
public TextCell() {
|
||||
_background = Background;
|
||||
_foreground = Foreground;
|
||||
_horizontal_alignment = HorizontalAlignment;
|
||||
_padding = Padding;
|
||||
_font_size = FontSize;
|
||||
_text = Text;
|
||||
_glyph_context = GlyphContext.create(FontFamily, FontWeight);
|
||||
}
|
||||
public FontFamily FontFamily { get => GetValue(FontFamilyProperty) as FontFamily; set => SetValue(FontFamilyProperty, value); }
|
||||
#endregion
|
||||
|
||||
#region Background
|
||||
public static readonly DependencyProperty BackgroundProperty =
|
||||
WPFHelper.create_dp<Brush, TextCell>(
|
||||
nameof(Background),
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(p, v) => p._background = v,
|
||||
null);
|
||||
|
||||
public Brush Background { get => (Brush)GetValue(BackgroundProperty); set => SetValue(BackgroundProperty, value); }
|
||||
#endregion
|
||||
|
||||
#region Foreground
|
||||
public static readonly DependencyProperty ForegroundProperty =
|
||||
WPFHelper.create_dp<Brush, TextCell>(
|
||||
nameof(Foreground),
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(p, v) => p._foreground = v,
|
||||
Brushes.Black);
|
||||
|
||||
public Brush Foreground { get => (Brush)GetValue(ForegroundProperty); set => SetValue(ForegroundProperty, value); }
|
||||
#endregion
|
||||
|
||||
#region FontFamily
|
||||
public static readonly DependencyProperty FontFamilyProperty =
|
||||
TextElement.FontFamilyProperty.AddOwner(
|
||||
typeof(TextCell),
|
||||
new FrameworkPropertyMetadata(
|
||||
TextUtils.StandardFontFamily,
|
||||
FrameworkPropertyMetadataOptions.AffectsMeasure |
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
WPFHelper.prop_changed_callback<FontFamily, TextCell>(
|
||||
(p, v) => {
|
||||
p._glyph_context = p._glyph_context.with(v);
|
||||
p._glyph_run = null;
|
||||
})));
|
||||
|
||||
public FontFamily FontFamily { get => GetValue(FontFamilyProperty) as FontFamily; set => SetValue(FontFamilyProperty, value); }
|
||||
#endregion
|
||||
|
||||
#region FontWeight
|
||||
public static readonly DependencyProperty FontWeightProperty =
|
||||
TextElement.FontWeightProperty.AddOwner(
|
||||
typeof(TextCell),
|
||||
new FrameworkPropertyMetadata(
|
||||
FontWeights.Normal,
|
||||
FrameworkPropertyMetadataOptions.AffectsMeasure |
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
WPFHelper.prop_changed_callback<FontWeight, TextCell>(
|
||||
(p, v) => {
|
||||
p._glyph_context = p._glyph_context.with(v);
|
||||
p._glyph_run = null;
|
||||
})));
|
||||
|
||||
public FontWeight FontWeight { get => (FontWeight)GetValue(FontWeightProperty); set => SetValue(FontWeightProperty, value); }
|
||||
#endregion
|
||||
|
||||
#region FontSize
|
||||
public static readonly DependencyProperty FontSizeProperty =
|
||||
TextElement.FontSizeProperty.AddOwner(
|
||||
typeof(TextCell),
|
||||
new FrameworkPropertyMetadata(
|
||||
FontSizes.Small,
|
||||
FrameworkPropertyMetadataOptions.AffectsMeasure |
|
||||
FrameworkPropertyMetadataOptions.AffectsRender |
|
||||
FrameworkPropertyMetadataOptions.Inherits,
|
||||
WPFHelper.prop_changed_callback<double, TextCell>(
|
||||
(p, v) => {
|
||||
p._font_size = v;
|
||||
p._glyph_run = null;
|
||||
})));
|
||||
|
||||
public double FontSize { get => (double)GetValue(FontSizeProperty); set => SetValue(FontSizeProperty, value); }
|
||||
#endregion
|
||||
|
||||
#region Padding
|
||||
public static readonly DependencyProperty PaddingProperty =
|
||||
WPFHelper.create_dp<Thickness, TextCell>(
|
||||
nameof(Padding),
|
||||
#region FontSize
|
||||
public static readonly DependencyProperty FontSizeProperty =
|
||||
TextElement.FontSizeProperty.AddOwner(
|
||||
typeof(TextCell),
|
||||
new FrameworkPropertyMetadata(
|
||||
FontSizes.Small,
|
||||
FrameworkPropertyMetadataOptions.AffectsMeasure |
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(p, v) => {
|
||||
p._padding = v;
|
||||
p._glyph_run = null;
|
||||
}, DefaultCellPadding);
|
||||
FrameworkPropertyMetadataOptions.AffectsRender |
|
||||
FrameworkPropertyMetadataOptions.Inherits,
|
||||
WPFHelper.prop_changed_callback<double, TextCell>(
|
||||
(p, v) => p._gcr.font_size = v)));
|
||||
|
||||
public Thickness Padding { get => (Thickness)GetValue(PaddingProperty); set => SetValue(PaddingProperty, value); }
|
||||
#endregion
|
||||
public double FontSize { get => (double)GetValue(FontSizeProperty); set => SetValue(FontSizeProperty, value); }
|
||||
#endregion
|
||||
|
||||
#region Text
|
||||
public static readonly DependencyProperty TextProperty =
|
||||
WPFHelper.create_dp<string, TextCell>(
|
||||
nameof(Text),
|
||||
FrameworkPropertyMetadataOptions.AffectsMeasure |
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(p, v) => {
|
||||
p._text = v;
|
||||
p._glyph_run = null;
|
||||
});
|
||||
#region Padding
|
||||
public static readonly DependencyProperty PaddingProperty =
|
||||
WPFHelper.create_dp<Thickness, TextCell>(
|
||||
nameof(Padding),
|
||||
FrameworkPropertyMetadataOptions.AffectsMeasure |
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(p, v) => p._gcr.padding = v,
|
||||
DIYGrid.DefaultCellPadding);
|
||||
|
||||
public string Text { get => GetValue(TextProperty) as string; set => SetValue(TextProperty, value); }
|
||||
#endregion
|
||||
public Thickness Padding { get => (Thickness)GetValue(PaddingProperty); set => SetValue(PaddingProperty, value); }
|
||||
#endregion
|
||||
|
||||
protected override Size ArrangeOverride(Size finalSize) {
|
||||
if (_glyph_run == null)
|
||||
_set_glyph(finalSize);
|
||||
return finalSize;
|
||||
#region Text
|
||||
public static readonly DependencyProperty TextProperty =
|
||||
WPFHelper.create_dp<string, TextCell>(
|
||||
nameof(Text),
|
||||
FrameworkPropertyMetadataOptions.AffectsMeasure |
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(p, v) => p._gcr.text_lines = string.IsNullOrEmpty(v) ? null : new[] { v }, string.Empty);
|
||||
|
||||
public string Text { get => GetValue(TextProperty) as string; set => SetValue(TextProperty, value); }
|
||||
#endregion
|
||||
|
||||
protected override Size ArrangeOverride(Size finalSize) => finalSize;
|
||||
|
||||
protected override Size MeasureOverride(Size availableSize) {
|
||||
_gcr.available_size = availableSize;
|
||||
if (double.IsInfinity(_gcr.layout_size.Height) || double.IsInfinity(_gcr.layout_size.Width)) {
|
||||
_gcr.remeasure();
|
||||
}
|
||||
return _gcr.layout_size;
|
||||
}
|
||||
|
||||
protected override Size MeasureOverride(Size availableSize) {
|
||||
if (_glyph_run == null)
|
||||
_set_glyph(availableSize);
|
||||
return new Size(Math.Max(availableSize.Width, _padding.Left + _glyph_width + _padding.Right),
|
||||
Math.Max(availableSize.Height, _padding.Top + _font_size + _padding.Bottom));
|
||||
double bl_x = 0;
|
||||
protected override void OnRender(DrawingContext drawingContext) {
|
||||
var rs = RenderSize;
|
||||
var rect = new Rect(0, 0, rs.Width, rs.Height);
|
||||
var bg = Background;
|
||||
if (bg != null) {
|
||||
drawingContext.DrawRectangle(bg, _border_stroke_pen, rect);
|
||||
}
|
||||
if (_glyph_run != null) {
|
||||
var new_x = _glyph_run.BaselineOrigin.X;
|
||||
if (Math.Abs(bl_x - new_x) > 5) {
|
||||
|
||||
void _set_glyph(Size size) {
|
||||
_glyph_run = _glyph_context.get_glyph_run(_text, _font_size, size, _horizontal_alignment, VerticalAlignment.Center, _padding);
|
||||
_glyph_width = _glyph_run == null ? 0 : _glyph_run.AdvanceWidths.Sum();
|
||||
}
|
||||
if (new_x < bl_x) {
|
||||
|
||||
protected override void OnRender(DrawingContext drawingContext) {
|
||||
var rs = RenderSize;
|
||||
var rect = new Rect(0, 0, rs.Width, rs.Height);
|
||||
if (_background != null)
|
||||
drawingContext.DrawRectangle(_background, null, rect);
|
||||
if (_glyph_run != null) {
|
||||
drawingContext.PushClip(new RectangleGeometry(rect));
|
||||
drawingContext.DrawGlyphRun(_foreground, _glyph_run);
|
||||
drawingContext.Pop();
|
||||
}
|
||||
}
|
||||
if (Text == "-3,375.000") {
|
||||
|
||||
}
|
||||
bl_x = new_x;
|
||||
drawingContext.PushClip(new RectangleGeometry(rect));
|
||||
drawingContext.DrawGlyphRun(_foreground, _glyph_run);
|
||||
drawingContext.Pop();
|
||||
}
|
||||
if (bg != null) {
|
||||
var bgo = DIYGrid.GetBackgroundOverlay(this);
|
||||
if (bgo != null)
|
||||
drawingContext.DrawRectangle(bgo, null, rect);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,11 @@ using mbts.spark;
|
||||
using mbts.spark.filters;
|
||||
using mbts.ui.settings;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media;
|
||||
@@ -52,13 +54,30 @@ namespace mbts.ui.controls {
|
||||
|
||||
public static class MiniOrderBook {
|
||||
public const double
|
||||
Cancel = 37,
|
||||
Cancel = 41,
|
||||
AttachDetach = 45,
|
||||
Update = 45,
|
||||
EditableSize = 40,
|
||||
EditablePrice = 64,
|
||||
Commit = 20;
|
||||
}
|
||||
|
||||
public static class PNL {
|
||||
public const double
|
||||
// 'PNL Account' is a bit wider than normal Account cols, bc it is a 'grouping' column, so it has additional content/icons
|
||||
Account = 100,
|
||||
Exchange = 85,
|
||||
Symbol = 72,
|
||||
Security = 87,
|
||||
Qty = 42,
|
||||
// deafult RawDayQty headers are too big for [normal]Qty width
|
||||
RawDayQty = 51,
|
||||
Price = 64,
|
||||
PriceBig = 92,
|
||||
SecurityType = 64,
|
||||
WorkingQty = 54,
|
||||
HasDayTrades = 74;
|
||||
}
|
||||
}
|
||||
|
||||
public static class ShortColumnWidths {
|
||||
@@ -99,6 +118,7 @@ namespace mbts.ui.controls {
|
||||
Working = "Working",
|
||||
Bid = "Bid",
|
||||
Ask = "Ask",
|
||||
Mid = "Mid",
|
||||
Settlement = "Settlement",
|
||||
DayChange = "Change",
|
||||
SessionLow = "Low",
|
||||
@@ -108,8 +128,8 @@ namespace mbts.ui.controls {
|
||||
Description = "Description",
|
||||
N_Legs = "# Legs",
|
||||
N_Working_Orders = "# Working Orders",
|
||||
StartNetPos = "Start Net Pos",
|
||||
StartPnL = "Start PnL",
|
||||
StartNetPos = "Open Pos",
|
||||
StartPnL = "Open PnL",
|
||||
DayLong = "Day Long",
|
||||
DayShort = "Day Short",
|
||||
RawDayPos = "Raw Day Pos",
|
||||
@@ -129,6 +149,38 @@ namespace mbts.ui.controls {
|
||||
PositionInQueue = "PIQ",
|
||||
RootAlgoType = "Root Algo"
|
||||
;
|
||||
|
||||
public static class PNLHeaders {
|
||||
public const string
|
||||
PNL = "PnL",
|
||||
Long = "Long",
|
||||
Short = "Short",
|
||||
Net = "Pos",
|
||||
AvgPrice = "Avg",
|
||||
HasDayTrades = "Traded Today",
|
||||
SecurityType = "Type";
|
||||
|
||||
public static readonly Part
|
||||
SOD = new Part("Open"),
|
||||
Day = new Part("Day"),
|
||||
RawDay = new Part("Raw Day"),
|
||||
Total = new Part("Total"),
|
||||
Working = new Part("Working");
|
||||
|
||||
public readonly struct Part {
|
||||
public readonly string prefix;
|
||||
|
||||
public Part(string prefix) =>
|
||||
this.prefix = prefix;
|
||||
|
||||
public string PNL => $"{prefix} PNL";
|
||||
public string Net => $"{prefix} {PNLHeaders.Net}";
|
||||
public string Long => $"{prefix} {PNLHeaders.Long}";
|
||||
public string LongAvg => $"{Long} {AvgPrice}";
|
||||
public string Short => $"{prefix} {PNLHeaders.Short}";
|
||||
public string ShortAvg => $"{Short} {AvgPrice}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public interface IKeyedColumn<TColKey> {
|
||||
@@ -171,6 +223,7 @@ namespace mbts.ui.controls {
|
||||
// To include extra data with your column type you are encouraged to derive from DIYGridColumn
|
||||
public abstract class ColumnBase : GridViewColumn, IValueFactory<GridColumnSettings>, INotifyPropertyChanged {
|
||||
//readonly ExternalProperty<FontWeight?> _font_weigh
|
||||
GridColumnSettings _settings;
|
||||
FontWeight _fallback_font_weight;
|
||||
Color _fallback_font_color;
|
||||
Color _fallback_background_color;
|
||||
@@ -180,6 +233,7 @@ namespace mbts.ui.controls {
|
||||
Color? _font_color_override;
|
||||
Color? _background_color_override;
|
||||
object _header_copy;
|
||||
bool _is_visible;
|
||||
|
||||
protected ColumnBase() {
|
||||
var gs = GridSettings.Default;
|
||||
@@ -202,13 +256,23 @@ namespace mbts.ui.controls {
|
||||
|
||||
public event Action<ColumnBase, ListSortDirection?> sort_direction_changed;
|
||||
public event Action font_params_changed;
|
||||
public event Action<bool> is_visible_changed;
|
||||
|
||||
public string id { get; set; }
|
||||
public string sort_member_path { get; set; }
|
||||
public PropertyPath data_member_path { get; set; }
|
||||
public double original_width { get; set; }
|
||||
public bool is_collapsed { get; set; }
|
||||
// TODO: hook up visibility
|
||||
public bool is_visible { get; set; } = true;
|
||||
public bool is_visible {
|
||||
get => _is_visible;
|
||||
set {
|
||||
if (_is_visible != value) {
|
||||
_is_visible = value;
|
||||
is_visible_changed?.Invoke(value);
|
||||
raise_prop_changed();
|
||||
}
|
||||
}
|
||||
}
|
||||
public FontWeight? font_weight_override { get => _font_weight_override; set => _set_font_param(ref _font_weight_override, value); }
|
||||
public Color? font_color_override { get => _font_color_override; set => _set_font_param(ref _font_color_override, value); }
|
||||
public HorizontalAlignment horizontal_alignment { get; set; }
|
||||
@@ -221,6 +285,7 @@ namespace mbts.ui.controls {
|
||||
public Color? background_color_override { get => _background_color_override; set => _set_font_param(ref _background_color_override, value); }
|
||||
public Color? background_color => background_color_override ?? _fallback_background_color;
|
||||
public Brush background_brush => background_color?.to_brush();
|
||||
public GridColumnSettings settings => _settings ?? (_settings = create());
|
||||
// TODO: find a better way to store the header-property in a thread-safe manner - a value for GridSettings.display_name, more specifically.
|
||||
// Since save_state is called from a different thread in IShared/SlowWindows, it is not safe to access the DependencyProperty store from that method.
|
||||
// By updating a local _header_copy field in the Header setter, the DependencyProperty lookup may be bypassed when appropriate (see create method below).
|
||||
@@ -229,6 +294,8 @@ namespace mbts.ui.controls {
|
||||
set => base.Header = (_header_copy = value);
|
||||
}
|
||||
|
||||
/// important: don't access any dependency props in here.
|
||||
/// this may be called from a thread other than that of this dispatcher (i.e., when <see cref="IShellWindow.GetState(System.IO.Stream)"/> is called).
|
||||
public GridColumnSettings create() =>
|
||||
new GridColumnSettings(id, _header_copy?.ToString(), is_visible, is_collapsed, font_weight_override, font_color_override, ActualWidth, horizontal_alignment, horizontal_offset, background_color_override);
|
||||
|
||||
@@ -240,7 +307,8 @@ namespace mbts.ui.controls {
|
||||
public virtual void load_from(GridColumnSettings settings) {
|
||||
if (settings == null)
|
||||
return;
|
||||
using (ReflectionHelper.detect_property_changes(this, _raise_prop_changed, nameof(is_visible), nameof(is_collapsed), nameof(font_weight_override), nameof(font_weight),
|
||||
_settings = settings;
|
||||
using (ReflectionHelper.detect_property_changes(this, raise_prop_changed, nameof(is_visible), nameof(is_collapsed), nameof(font_weight_override), nameof(font_weight),
|
||||
nameof(font_color_override), nameof(font_color), nameof(font_brush), nameof(font_family), nameof(font_size), nameof(horizontal_alignment), nameof(horizontal_offset), nameof(background_color_override), nameof(background_color), nameof(background_brush))) {
|
||||
//
|
||||
id = settings.id;
|
||||
@@ -263,7 +331,7 @@ namespace mbts.ui.controls {
|
||||
|
||||
// 'fallback' font-params (aka 'grid-level' font-params)
|
||||
public void set_fallback_font_params(FontFamily font_family, double font_size, Color font_color, FontWeight font_weight, Color background_color) {
|
||||
using (ReflectionHelper.detect_property_changes(this, _raise_prop_changed, nameof(font_family), nameof(font_size), nameof(font_color), nameof(font_weight), nameof(background_color), nameof(background_brush))) {
|
||||
using (ReflectionHelper.detect_property_changes(this, raise_prop_changed, nameof(font_family), nameof(font_size), nameof(font_color), nameof(font_weight), nameof(background_color), nameof(background_brush))) {
|
||||
_fallback_font_family = font_family;
|
||||
_fallback_font_size = font_size;
|
||||
_fallback_font_color = font_color;
|
||||
@@ -276,8 +344,20 @@ namespace mbts.ui.controls {
|
||||
protected virtual void OnSortDirectionChanged(ListSortDirection? new_value) =>
|
||||
sort_direction_changed?.Invoke(this, new_value);
|
||||
|
||||
void _raise_prop_changed(string prop) =>
|
||||
protected void raise_prop_changed([CallerMemberName]string prop = "") =>
|
||||
OnPropertyChanged(new PropertyChangedEventArgs(prop));
|
||||
|
||||
protected void set<T>(ref T backing, T new_value, [CallerMemberName]string prop = "") =>
|
||||
set(ref backing, new_value, EqualityComparer<T>.Default, prop);
|
||||
|
||||
protected void set<T>(ref T backing, T new_value, IEqualityComparer<T> comparer, [CallerMemberName]string prop = "") {
|
||||
if (!comparer.Equals(backing, new_value)) {
|
||||
backing = new_value;
|
||||
OnPropertyChanged(new PropertyChangedEventArgs(prop));
|
||||
}
|
||||
}
|
||||
public override string ToString() =>
|
||||
id.ToString();
|
||||
}
|
||||
|
||||
internal abstract class ColumnBase<TColKey> : ColumnBase, IKeyedColumn<TColKey>, IGridColumn {
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace mbts.ui.controls {
|
||||
public double width { get { return column.ActualWidth; } }
|
||||
public double right { get { return unclipped_left + width; } }
|
||||
public bool is_partially_obscured { get { return unclipped_left < left; } }
|
||||
public double visible_width => width - left + unclipped_left;
|
||||
public double visible_width => Math.Max(0, width - left + unclipped_left);
|
||||
public override string ToString() {
|
||||
return "{3} {0:0.0},{1:0.0} w{2:0.0}".fmt(left, right, width, column.Header);
|
||||
}
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ namespace mbts.ui.controls {
|
||||
}
|
||||
|
||||
public IDataGridColumnFilter filter { get; }
|
||||
public virtual string filter_template => DataGridFilterKnownTemplates.DiscreteValues;
|
||||
public virtual string filter_template => DataGridFilterKnownTemplates.AlgoType;
|
||||
public BindingBase font_weight_binding { get; set; }
|
||||
|
||||
public override FrameworkElement view_factory() {
|
||||
|
||||
@@ -3,10 +3,13 @@ using bts.utils;
|
||||
using mbts.spark.filters;
|
||||
using mbts.ui.controls.adorners;
|
||||
using mbts.ui.extensions;
|
||||
using mbts.ui.settings;
|
||||
using mbts.ui.utility;
|
||||
using mbts.utils;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Specialized;
|
||||
using System.ComponentModel;
|
||||
@@ -31,9 +34,78 @@ namespace mbts.ui.controls {
|
||||
public DIYGrid.ColumnBase Column { get; }
|
||||
public bool Handled { get; set; }
|
||||
}
|
||||
|
||||
class CachedColumnParam {
|
||||
public string SourcePropertyName { get; set; }
|
||||
public DependencyProperty TargetProperty { get; set; }
|
||||
}
|
||||
|
||||
class GroupItemSelectionPivotEventArgs : RoutedEventArgs {
|
||||
public GroupItemSelectionPivotEventArgs(RoutedEvent routed_event, object source, CollectionViewGroup group_item)
|
||||
: base(routed_event, source) => this.group_item = group_item;
|
||||
|
||||
public CollectionViewGroup group_item { get; }
|
||||
}
|
||||
|
||||
delegate void DIYGridSortingEventHandler(object sender, DIYGridSortingEventArgs e);
|
||||
delegate void GroupItemSelectionPivotEventHandler(object sender, GroupItemSelectionPivotEventArgs e);
|
||||
|
||||
static class DIYGridSortHelper {
|
||||
public static IComparer<TRow> build_comparer<TColumnKey, TRow>(IEnumerable<(TColumnKey key, ListSortDirection direction)> sorting_hierarchy, Func<TColumnKey, IComparer<TRow>> get_column_comparer, params TColumnKey[] primary_key) {
|
||||
// ensure primary key is included for deterministic sort (combination of account/security)
|
||||
for (int i = 0; i < primary_key.Length; i++) {
|
||||
var key = primary_key[i];
|
||||
if (!sorting_hierarchy.Any(tup => tup.key.Equals(key)))
|
||||
sorting_hierarchy = sorting_hierarchy.Append((key, ListSortDirection.Ascending));
|
||||
}
|
||||
return new ComparerBuilder<TRow>(
|
||||
sorting_hierarchy
|
||||
.Select(tup => {
|
||||
var col_comp = get_column_comparer(tup.key);
|
||||
return tup.direction == ListSortDirection.Ascending ? col_comp : col_comp.reverse();
|
||||
}))
|
||||
.to_comparer();
|
||||
}
|
||||
|
||||
public static IEnumerable<(TKey key, ListSortDirection direction)> convert_sorting_criterion<TKey>(IEnumerable<SortingCriteria> sorting_criterion) where TKey : struct {
|
||||
foreach (var sc in sorting_criterion) {
|
||||
if (Enum.TryParse(sc.col_name, false, out TKey key))
|
||||
yield return (key, sc.direction());
|
||||
}
|
||||
}
|
||||
|
||||
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) =>
|
||||
//
|
||||
Observable.Create<ImmutableList<(TKey key, IComparer<TRow> comparer, ListSortDirection direction)>>(o => {
|
||||
var active_comparers = ImmutableList<(TKey key, IComparer<TRow> comp, ListSortDirection dir)>.Empty;
|
||||
grid.Sorting += on_sorting;
|
||||
return Disposable.Create(() => grid.Sorting -= on_sorting);
|
||||
void on_sorting(object sender, DIYGridSortingEventArgs e) {
|
||||
if (mark_handled)
|
||||
e.Handled = true;
|
||||
if (e.Column is DIYGrid.IKeyedColumn<TKey> kc) {
|
||||
var comp = get_comparer(kc.key);
|
||||
if (comp == null)
|
||||
return;
|
||||
var dir = e.Column.SortDirection;
|
||||
var next = dir.HasValue && dir.Value == ListSortDirection.Ascending ? ListSortDirection.Descending : ListSortDirection.Ascending;
|
||||
if (!KeyboardExtensions.IsShiftDown()) {
|
||||
e.Column.SortDirection = next;
|
||||
active_comparers = active_comparers.Clear();
|
||||
active_comparers = active_comparers.Add((kc.key, comp, next));
|
||||
} else {
|
||||
if (active_comparers.TryGetFirst(tup => tup.key.Equals(kc.key), out var match, out int i)) {
|
||||
active_comparers = active_comparers.SetItem(i, (kc.key, comp, next));
|
||||
} else {
|
||||
active_comparers = active_comparers.Add((kc.key, comp, next));
|
||||
}
|
||||
}
|
||||
}
|
||||
o.OnNext(active_comparers);
|
||||
}
|
||||
});
|
||||
|
||||
/// Important: All columns MUST IMPLEMENT <see cref="DIYGrid.IKeyedColumn{TColKey}"/>.
|
||||
public static IReadOnlyList<(TColKey col, ListSortDirection direction)> get_sorting_data<TColKey>(List<(DIYGrid.ColumnBase col, TColKey key, ListSortDirection direction)> active_comparers, TColKey primary_key) {
|
||||
var rv = new List<(TColKey col, ListSortDirection direction)>();
|
||||
@@ -60,6 +132,14 @@ namespace mbts.ui.controls {
|
||||
public static IComparer<T> reverse_if_descending<T>(IComparer<T> comp, ListSortDirection direction) =>
|
||||
direction == ListSortDirection.Descending ? comp.reverse() : comp;
|
||||
|
||||
public static void set_sort_directions<TColumnKey>(IEnumerable<(TColumnKey key, ListSortDirection direction)> sorting_hierarchy, IEnumerable<DIYGrid.ColumnBase> columns) {
|
||||
foreach (var col in columns) {
|
||||
col.SortDirection = col is DIYGrid.IKeyedColumn<TColumnKey> kc && sorting_hierarchy.TryGetFirst(tup => tup.key.Equals(kc.key), out var match) ?
|
||||
match.direction :
|
||||
(ListSortDirection?)null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Important: All columns MUST IMPLEMENT <see cref="DIYGrid.IKeyedColumn{TColKey}"/>.
|
||||
static IReadOnlyList<(TColKey col, ListSortDirection direction)> _on_sorting<TColKey>(DIYGridSortingEventArgs e, ref List<(DIYGrid.ColumnBase col, TColKey key, ListSortDirection direction)> active_comparers, TColKey primary_key, bool clear_current) {
|
||||
if (e.Column is DIYGrid.IKeyedColumn<TColKey> col) {
|
||||
@@ -122,14 +202,18 @@ namespace mbts.ui.controls {
|
||||
}
|
||||
|
||||
class DIYGridRowsPresenter : ListBox {
|
||||
readonly LoggerContext _log = new LoggerContext(Log.Ui, $"{nameof(DIYGridRowsPresenter)} ", null);
|
||||
double _row_height = DIYGrid.DefaultRowHeight;
|
||||
ScrollContentPresenter _content_presenter_backing;
|
||||
DIYGrid _grid;
|
||||
DragRectAdorner _drag_rect_adorner;
|
||||
RowSelectionAdorner _rsa;
|
||||
ScrollViewer _scroll_viewer;
|
||||
Point? _drag_start;
|
||||
double _drag_start_vertical_drift;
|
||||
bool _support_group_items;
|
||||
int _drag_start_index;
|
||||
object _drag_start_item;
|
||||
|
||||
static DIYGridRowsPresenter() =>
|
||||
DefaultStyleKeyProperty.OverrideMetadata(typeof(DIYGridRowsPresenter), new FrameworkPropertyMetadata(typeof(DIYGridRowsPresenter)));
|
||||
@@ -141,6 +225,8 @@ namespace mbts.ui.controls {
|
||||
_grid = this.find_ancestor_of_type<DIYGrid>();
|
||||
_row_height = _grid.RowHeight;
|
||||
_grid.RowHeightChanged += on_row_height_changed;
|
||||
_support_group_items = _grid.SupportGroupItems;
|
||||
_grid.SupportGroupItemsChanged += on_support_group_items_changed;
|
||||
_scroll_viewer = this.descendants_of_type<ScrollViewer>().First();
|
||||
if (SelectionMode != SelectionMode.Single) {
|
||||
_drag_rect_adorner = new DragRectAdorner(this, _scroll_viewer, _row_height);
|
||||
@@ -162,8 +248,14 @@ namespace mbts.ui.controls {
|
||||
}
|
||||
};
|
||||
void on_row_height_changed(double v) => _row_height = v;
|
||||
void on_support_group_items_changed(bool v) => _support_group_items = v;
|
||||
}
|
||||
|
||||
public event GroupItemSelectionPivotEventHandler GroupItemSelectionPivot;
|
||||
|
||||
ScrollContentPresenter _content_presenter => _content_presenter_backing ??
|
||||
(_content_presenter_backing = this.descendants_of_type<ScrollContentPresenter>().FirstOrDefault());
|
||||
|
||||
public new void SetSelectedItems(IEnumerable items) {
|
||||
base.SetSelectedItems(items);
|
||||
}
|
||||
@@ -174,22 +266,37 @@ namespace mbts.ui.controls {
|
||||
|
||||
protected override void OnPreviewMouseLeftButtonDown(MouseButtonEventArgs e) {
|
||||
var pt = e.GetPosition(this);
|
||||
var container = this.ChildAtPoint<ListBoxItem>(pt);
|
||||
if (container != null) {
|
||||
var item = ItemContainerGenerator.ItemFromContainer(container);
|
||||
if (item != null) {
|
||||
if (InputHitTest(pt) is UIElement el) {
|
||||
var sb = new StringBuilderEx("beginning drag selection on ");
|
||||
OneOf.MatchFirst<DependencyObject, ListBoxItem, GroupItem>(el.VisualAncestors().Prepend(el)).Match(init_common, on_group);
|
||||
void on_group(GroupItem g) {
|
||||
if (g.DataContext is CollectionViewGroup gvm && gvm.FlatItems().Select(ItemContainerGenerator.ContainerFromItem).OfType<ListBoxItem>().TryGetFirst(out var lbi)) {
|
||||
// ListBox calls this internally when a ListBoxItem is clicked;
|
||||
// it needs to be called explicitly in this case, since the click target is a GroupItem
|
||||
Mouse.Capture(this, CaptureMode.SubTree);
|
||||
sb.Add($"{nameof(GroupItem)} '{gvm.Name}' with actual item ");
|
||||
init_common(lbi);
|
||||
}
|
||||
}
|
||||
void init_common(ListBoxItem item_container) {
|
||||
_drag_start = pt;
|
||||
_drag_start_index = Items.IndexOf(item);
|
||||
_drag_start_item = ItemContainerGenerator.ItemFromContainer(item_container);
|
||||
_drag_start_index = ItemContainerGenerator.IndexFromContainer(item_container);
|
||||
_drag_start_vertical_drift = _scroll_viewer.VerticalOffset;
|
||||
sb.Add($"'{_drag_start_item}' (selection mode {SelectionMode}).");
|
||||
_log.info(sb.ToString());
|
||||
}
|
||||
}
|
||||
base.OnPreviewMouseLeftButtonDown(e);
|
||||
}
|
||||
|
||||
protected override void OnPreviewMouseUp(MouseButtonEventArgs e) {
|
||||
if (SelectionMode != SelectionMode.Single) {
|
||||
if (_drag_start.HasValue) {
|
||||
if (_drag_start.HasValue) {
|
||||
_log.info("ending drag selection on mouse up.");
|
||||
if (SelectionMode != SelectionMode.Single && _content_presenter != null) {
|
||||
var drag_stop = e.GetPosition(this);
|
||||
drag_stop.X = MathUtils.within_range(0, _content_presenter.RenderSize.Width - 1, drag_stop.X);
|
||||
drag_stop.Y = MathUtils.within_range(0, _content_presenter.RenderSize.Height - 1, drag_stop.Y);
|
||||
double vertical_diff = drag_stop.Y + (_row_height * (_scroll_viewer.VerticalOffset - _drag_start_vertical_drift)) - _drag_start.Value.Y;
|
||||
int start_index, n_items;
|
||||
if (vertical_diff > 0) {
|
||||
@@ -209,14 +316,65 @@ namespace mbts.ui.controls {
|
||||
}
|
||||
}
|
||||
if (n_items > 1) {
|
||||
var selection = Enumerable.Range(start_index, n_items).Select(i => Items[i]).ToList();
|
||||
SetSelectedItems(selection);
|
||||
if (_support_group_items) {
|
||||
if (this.ChildAtPoint(drag_stop, out ListBoxItem drag_stop_container)) {
|
||||
_select_items_between(_drag_start_item, ItemContainerGenerator.ItemFromContainer(drag_stop_container));
|
||||
} else if (this.ChildAtPoint(drag_stop, out GroupItem gi) && gi.DataContext is CollectionViewGroup cvg && cvg.ItemCount > 0) {
|
||||
if ((ItemContainerGenerator.ContainerFromItem(_drag_start_item)?.VisualAncestors<GroupItem>()).empty_if_null().FirstOrDefault() == gi) {
|
||||
_select_items_between(_drag_start_item, cvg.Items.FirstOrDefault());
|
||||
} else {
|
||||
var gisp_args = new GroupItemSelectionPivotEventArgs(PreviewMouseUpEvent, this, cvg);
|
||||
GroupItemSelectionPivot?.Invoke(this, gisp_args);
|
||||
if (gisp_args.Handled) {
|
||||
// don't select the inner group items
|
||||
if (vertical_diff < 0) {
|
||||
_select_items_between(_drag_start_item, cvg.FlatItems().FirstOrDefault());
|
||||
} else {
|
||||
var end = cvg.FlatItems().FirstOrDefault();
|
||||
var selection = Items.Cast<object>()
|
||||
.Between(_drag_start_item, cvg.FlatItems().FirstOrDefault())
|
||||
.Prepend(_drag_start_item);
|
||||
_log.info($"selecting item range (inclusive begin '{_drag_start_item}' to exclusive end {end}).");
|
||||
SetSelectedItems(selection);
|
||||
}
|
||||
} else {
|
||||
/// Execute default behavior (select all the inner items in the GroupItem)
|
||||
/// The way <see cref="_select_items_between(object, object)"/> works, is two items play the role of 'bookends' - every item in between them is selected.
|
||||
/// Since the drag_start item is the first bookend, depending on the drag direction, either the first, or last item in the group will be the other bookend.
|
||||
_select_items_between(_drag_start_item, vertical_diff > 0 ? cvg.Items.Last() : cvg.Items.First());
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
var selection = Enumerable.Range(start_index, n_items).Select(i => Items[i]).ToList();
|
||||
Log.assert(selection.Count > 0);
|
||||
_log.info($"selecting item range (size {selection.Count}) determined by drag distance '{vertical_diff}' x row height '{_row_height}' (inclusive begin '{selection[0]}' to inclusive end '{selection[selection.Count - 1]}').");
|
||||
SetSelectedItems(selection);
|
||||
}
|
||||
}
|
||||
_drag_start = null;
|
||||
}
|
||||
_drag_start = null;
|
||||
_drag_start_item = null;
|
||||
}
|
||||
base.OnPreviewMouseUp(e);
|
||||
}
|
||||
|
||||
void _select_items_between(object inclusive_begin, object inclusive_end) {
|
||||
if (inclusive_begin == null || inclusive_end == null) {
|
||||
// One could argue that an exception should be thrown here, as null is a bogus value for either parameter.
|
||||
// However, clearing the selection seems reasonable enough, and does not risk crashing (assuming nothing particularly nefarious is happening).
|
||||
_log.warn($"clearing the selection because the specified item range cannot be determined (inclusive begin '{inclusive_begin}' to inclusive end '{inclusive_end}').");
|
||||
UnselectAll();
|
||||
} else if (inclusive_begin == inclusive_end) {
|
||||
_log.info($"selecting single item '{inclusive_begin}.'");
|
||||
SetSelectedItems(new[] { inclusive_begin });
|
||||
} else {
|
||||
_log.info($"selecting item range (inclusive begin '{inclusive_begin}' to inclusive end '{inclusive_end}').");
|
||||
SetSelectedItems(Items.Cast<object>()
|
||||
.Between(inclusive_begin, inclusive_end)
|
||||
.BookEnd(inclusive_begin, inclusive_end));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
partial class DIYGrid : Control {
|
||||
@@ -231,6 +389,7 @@ namespace mbts.ui.controls {
|
||||
DefaultCellPadding = new Thickness(4, 2, 4, 2),
|
||||
AnimatableCellPadding = new Thickness(8, 2, 4, 2);
|
||||
public static readonly Visibility DefaultRowsHorizontalScrollBarVisibility = Visibility.Visible;
|
||||
bool _support_group_items;
|
||||
|
||||
#region RoutedCommands
|
||||
public static readonly RoutedCommand SortCommand = new RoutedCommand(), DefaultRowCommand = new RoutedCommand();
|
||||
@@ -310,33 +469,37 @@ namespace mbts.ui.controls {
|
||||
CommandBindings.Add(new CommandBinding(ApplicationCommands.Copy, OnCopy, OnCopyCanExecute));
|
||||
CommandBindings.Add(new CommandBinding(SortCommand, (s, e) => {
|
||||
if (e.Parameter is GridViewColumnHeader col_header && col_header.Column is ColumnBase cb) {
|
||||
Sorting?.Invoke(this, new DIYGridSortingEventArgs(cb));
|
||||
if (!e.Handled) {
|
||||
if (Rows is ICollectionView cv_rows) {
|
||||
using (((INotifyCollectionChanged)cv_rows.SortDescriptions).SuspendHandler(_sort_descriptions_collection_changed_cb))
|
||||
using (cv_rows.DeferRefresh()) {
|
||||
if (cb.sort_member_path is string path) {
|
||||
ListSortDirection? curr = cb.SortDirection;
|
||||
var next = curr.HasValue && curr.Value == ListSortDirection.Ascending ? ListSortDirection.Descending : ListSortDirection.Ascending;
|
||||
if (!KeyboardExtensions.IsShiftDown()) {
|
||||
foreach (var sd in cv_rows.SortDescriptions)
|
||||
if (sort_map.TryGetValue(sd.PropertyName, out ColumnBase match)) {
|
||||
match.SortDirection = null;
|
||||
sort_map.Remove(sd.PropertyName);
|
||||
}
|
||||
cv_rows.SortDescriptions.Clear();
|
||||
cb.SortDirection = next;
|
||||
cv_rows.SortDescriptions.Add(new SortDescription(path, next));
|
||||
} else {
|
||||
var sort_desc = new SortDescription(path, next);
|
||||
int i = cv_rows.SortDescriptions.IndexOf(sort_desc, EqualityComparer.Create<SortDescription, string>(x => x.PropertyName));
|
||||
cb.SortDirection = next;
|
||||
if (i >= 0) {
|
||||
cv_rows.SortDescriptions[i] = sort_desc;
|
||||
} else cv_rows.SortDescriptions.Add(new SortDescription(path, next));
|
||||
}
|
||||
sort_map[path] = cb;
|
||||
var sorting_event = Sorting;
|
||||
if (sorting_event != null) {
|
||||
var sorting_args = new DIYGridSortingEventArgs(cb);
|
||||
sorting_event(this, sorting_args);
|
||||
if (sorting_args.Handled)
|
||||
return;
|
||||
}
|
||||
if (Rows is ICollectionView cv_rows) {
|
||||
using (((INotifyCollectionChanged)cv_rows.SortDescriptions).SuspendHandler(_sort_descriptions_collection_changed_cb))
|
||||
using (cv_rows.DeferRefresh()) {
|
||||
if (cb.sort_member_path is string path) {
|
||||
ListSortDirection? curr = cb.SortDirection;
|
||||
var next = curr.HasValue && curr.Value == ListSortDirection.Ascending ? ListSortDirection.Descending : ListSortDirection.Ascending;
|
||||
if (!KeyboardExtensions.IsShiftDown()) {
|
||||
foreach (var sd in cv_rows.SortDescriptions)
|
||||
if (sort_map.TryGetValue(sd.PropertyName, out ColumnBase match)) {
|
||||
match.SortDirection = null;
|
||||
sort_map.Remove(sd.PropertyName);
|
||||
}
|
||||
cv_rows.SortDescriptions.Clear();
|
||||
cb.SortDirection = next;
|
||||
cv_rows.SortDescriptions.Add(new SortDescription(path, next));
|
||||
} else {
|
||||
var sort_desc = new SortDescription(path, next);
|
||||
int i = cv_rows.SortDescriptions.IndexOf(sort_desc, EqualityComparer.Create<SortDescription, string>(x => x.PropertyName));
|
||||
cb.SortDirection = next;
|
||||
if (i >= 0) {
|
||||
cv_rows.SortDescriptions[i] = sort_desc;
|
||||
} else cv_rows.SortDescriptions.Add(new SortDescription(path, next));
|
||||
}
|
||||
sort_map[path] = cb;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -349,6 +512,7 @@ namespace mbts.ui.controls {
|
||||
}
|
||||
|
||||
public event DIYGridSortingEventHandler Sorting;
|
||||
public event Action<bool> SupportGroupItemsChanged;
|
||||
|
||||
public double InactiveSelectionOpacity {
|
||||
get => _inactive_selection_opacity;
|
||||
@@ -360,9 +524,21 @@ namespace mbts.ui.controls {
|
||||
}
|
||||
}
|
||||
|
||||
public bool SupportGroupItems {
|
||||
get => _support_group_items;
|
||||
set {
|
||||
if (_support_group_items != value) {
|
||||
_support_group_items = value;
|
||||
SupportGroupItemsChanged?.Invoke(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ItemContainerGenerator RowContainerGenerator =>
|
||||
_row_presenter.ItemContainerGenerator;
|
||||
|
||||
public ObservableCollection<GroupStyle> GroupStyle { get; } = new ObservableCollection<GroupStyle>();
|
||||
|
||||
private void OnCopy(object sender, ExecutedRoutedEventArgs e) {
|
||||
// TODO: implement excel-style copy rows/cells
|
||||
}
|
||||
@@ -419,7 +595,7 @@ namespace mbts.ui.controls {
|
||||
}
|
||||
|
||||
#region Column Handling
|
||||
private void invalidate_column_layout(object sender = null, EventArgs args = null) {
|
||||
public void invalidate_column_layout(object sender = null, EventArgs args = null) {
|
||||
var geometry = GetColumnGeometry().ToList();
|
||||
var clc = ColumnLayoutChanged;
|
||||
if (clc != null) {
|
||||
@@ -494,26 +670,70 @@ namespace mbts.ui.controls {
|
||||
_superheader_column_index = SuperHeaders.SelectMany(sh => Enumerable.Repeat(sh, sh.Width)).ToArray();
|
||||
}
|
||||
|
||||
ObservableCollection<ColumnBase> _visible_columns = new ObservableCollection<ColumnBase>();
|
||||
IDisposable _visible_columns_filter_hook;
|
||||
readonly ObservableCollection<ColumnBase> _visible_columns = new ObservableCollection<ColumnBase>();
|
||||
IDisposable _columns_hooks;
|
||||
|
||||
private static void columns_changed(DependencyObject d, DependencyPropertyChangedEventArgs args) {
|
||||
if (d is DIYGrid grid) {
|
||||
grid.on_columns_collection_changed(args.OldValue as ObservableCollection<ColumnBase>, args.NewValue as ObservableCollection<ColumnBase>);
|
||||
grid._reset_columns(args.NewValue as ObservableCollection<ColumnBase>);
|
||||
}
|
||||
}
|
||||
|
||||
void _reset_columns() =>
|
||||
_reset_columns(Columns);
|
||||
|
||||
// Called when the columns COLLECTION has been replaced entirely
|
||||
private void on_columns_collection_changed(ObservableCollection<ColumnBase> oldval, ObservableCollection<ColumnBase> newval) {
|
||||
if (oldval != null) {
|
||||
Disposable.dispose(ref _visible_columns_filter_hook);
|
||||
_visible_columns.CollectionChanged -= on_columns_changed;
|
||||
}
|
||||
void _reset_columns(ObservableCollection<ColumnBase> newval) {
|
||||
Disposable.dispose(ref _columns_hooks);
|
||||
if (newval != null) {
|
||||
_visible_columns_filter_hook = ObservableFilterCollection.connect_never_duplicate(newval, _visible_columns, c => c.is_visible, nameof(ColumnBase.is_visible));
|
||||
_visible_columns.CollectionChanged += on_columns_changed;
|
||||
// When column.is_visible changes, visible_columns will be refreshed asynchronously,
|
||||
// via Dispatcher.InvokeAsync, to drown out noisy batch changes.
|
||||
// The vis_cols_sync_queued flag indicates that such a refresh has already been queued.
|
||||
bool vis_cols_sync_queued = false;
|
||||
// vis_cols_disposed, when true, indicates that the Columns dp has changed.
|
||||
// This flag is checked in the Dispatcher.InvokeAsync callback before resetting visible_columns, to avoid overwriting the latest value.
|
||||
bool vis_cols_disposed = false;
|
||||
var vis_cols_hook = new DisposableContainer();
|
||||
reset_visible_columns();
|
||||
_columns_hooks = Disposable.Create(
|
||||
Disposable.Create(() => vis_cols_disposed = true),
|
||||
ObservableAutoWrapper.connect_item_hooks(newval, newval, c => {
|
||||
c.is_visible_changed += column_is_visible_changed;
|
||||
return Disposable.Create(() => c.is_visible_changed -= column_is_visible_changed);
|
||||
}),
|
||||
vis_cols_hook,
|
||||
_visible_columns.Subscribe(on_columns_changed));
|
||||
if (GetCachedColumnParamsContainer(this) is FrameworkElement params_container && CachedColumnParams is IEnumerable<string> ccp) {
|
||||
var ccp_array = ccp.ToArray();
|
||||
_columns_hooks = Disposable.Create(
|
||||
_columns_hooks,
|
||||
ObservableAutoWrapper.connect_item_hooks(newval, c =>
|
||||
ObjectSnapshot.property_table((INotifyPropertyChanged)c, ccp_array)
|
||||
.Subscribe((bts.api.core.TableUpdate<(string name, object current, object original), string> table) => {
|
||||
foreach (var (name, current, original) in table.added.Concat(table.changed))
|
||||
params_container.Resources[ColumnResourceHelper.col_prop_resource_id(c.id, name)] = current;
|
||||
})
|
||||
));
|
||||
}
|
||||
// local methods
|
||||
void reset_visible_columns() =>
|
||||
vis_cols_hook.replace(ObservableFilterCollection.connect_never_duplicate(newval, _visible_columns, c => c.is_visible, nameof(ColumnBase.is_visible)));
|
||||
|
||||
void column_is_visible_changed(bool is_visible) {
|
||||
if (!vis_cols_sync_queued) {
|
||||
vis_cols_sync_queued = true;
|
||||
Dispatcher.InvokeAsync(() => {
|
||||
if (!vis_cols_disposed) {
|
||||
reset_visible_columns();
|
||||
vis_cols_sync_queued = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
on_columns_changed(_visible_columns, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
|
||||
}
|
||||
|
||||
// Called when items are added or removed from the columns collection
|
||||
private void on_columns_changed(object sender, NotifyCollectionChangedEventArgs e) {
|
||||
if (sender is ObservableCollection<ColumnBase> columns) {
|
||||
@@ -564,6 +784,8 @@ namespace mbts.ui.controls {
|
||||
void _on_col_font_params_changed() => CoerceValue(RowHeightProperty);
|
||||
|
||||
void _try_set_column_filter_props(ColumnBase column) {
|
||||
if (column.HeaderTemplate == null && !(column.Header is Visual))
|
||||
column.HeaderTemplate = TryFindResource("DefaultColumnHeaderContentTemplate") as DataTemplate;
|
||||
if (column is IFilterableColumn fc) {
|
||||
DataGridFilterProperties.SetFilter(column, fc.filter);
|
||||
DataGridFilterProperties.SetFilterTemplate(column, FindResource(fc.filter_template) as DataTemplate);
|
||||
@@ -623,6 +845,14 @@ namespace mbts.ui.controls {
|
||||
if (GetSyncWithColumnGeometrySource(d) is DIYGrid diy_grid || panel.try_find_ancestor_of_type(out diy_grid))
|
||||
ColumnGeometrySyncHelper.reset_panel_child(child, diy_grid, e.NewValue);
|
||||
}
|
||||
if (d is FrameworkElement child_el) {
|
||||
var curr_params = GetObservedCachedColumnParams(d);
|
||||
if (curr_params != null) {
|
||||
// clear observed params with old key, then reset with new key
|
||||
ColumnResourceHelper.reset_cell_resource_references(child_el, e.OldValue, curr_params, null);
|
||||
ColumnResourceHelper.reset_cell_resource_references(child_el, e.NewValue, null, curr_params);
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
public static object GetColumnKey(DependencyObject d) => d.GetValue(ColumnKeyProperty);
|
||||
@@ -666,6 +896,62 @@ namespace mbts.ui.controls {
|
||||
d.SetValue(SyncWithColumnGeometryHookPropertyKey, value);
|
||||
#endregion
|
||||
|
||||
#region CachedColumnParamsContainer
|
||||
// This is the element who's ResourceDictionary will be used to store the observed column params.
|
||||
// By allowing callers to specify this element (as opposed to simply putting them in the DIYGrid's resources),
|
||||
// elements that are not visual children of the DIYGrid can bind to these params as well.
|
||||
// Once assigned, this value should not change during the lifetime of the DIYGrid.
|
||||
// This could create conflicting references among the binding targets,
|
||||
// as resources added to the old CachedColumnParamsContainer would not be removed.
|
||||
public static readonly DependencyProperty CachedColumnParamsContainerProperty =
|
||||
DependencyProperty.RegisterAttached(
|
||||
"CachedColumnParamsContainer",
|
||||
typeof(FrameworkElement),
|
||||
typeof(DIYGrid),
|
||||
new PropertyMetadata((d, e) => (d as DIYGrid)?._reset_columns()));
|
||||
|
||||
public static FrameworkElement GetCachedColumnParamsContainer(DependencyObject d) => d.GetValue(CachedColumnParamsContainerProperty) as FrameworkElement;
|
||||
public static void SetCachedColumnParamsContainer(DependencyObject d, ResourceDictionary value) => d.SetValue(CachedColumnParamsContainerProperty, value);
|
||||
#endregion
|
||||
|
||||
#region CachedColumnParams
|
||||
public static readonly DependencyProperty CachedColumnParamsProperty =
|
||||
WPFHelper.create_dp<IEnumerable<string>, DIYGrid>(
|
||||
nameof(CachedColumnParams),
|
||||
(o, v) => {
|
||||
o?._reset_columns();
|
||||
});
|
||||
|
||||
public IEnumerable<string> CachedColumnParams {
|
||||
get => GetValue(CachedColumnParamsProperty) as IEnumerable<string>;
|
||||
set => SetValue(CachedColumnParamsProperty, value);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region ObservedCachedColumnParams
|
||||
public static readonly DependencyProperty ObservedCachedColumnParamsProperty =
|
||||
DependencyProperty.RegisterAttached(
|
||||
"ObservedCachedColumnParams",
|
||||
typeof(IEnumerable<CachedColumnParam>),
|
||||
typeof(DIYGrid),
|
||||
new PropertyMetadata(ColumnResourceHelper.ObservedCachedParamsChanged));
|
||||
|
||||
public static IEnumerable<CachedColumnParam> GetObservedCachedColumnParams(DependencyObject d) => d.GetValue(ObservedCachedColumnParamsProperty) as IEnumerable<CachedColumnParam>;
|
||||
public static void SetObservedCachedColumnParams(DependencyObject d, IEnumerable<CachedColumnParam> value) => d.SetValue(ObservedCachedColumnParamsProperty, value);
|
||||
#endregion
|
||||
|
||||
#region ColumnSettings
|
||||
public static readonly DependencyProperty ColumnSettingsProperty =
|
||||
DependencyProperty.RegisterAttached("ColumnSettings", typeof(settings.GridColumnSettings), typeof(DIYGrid),
|
||||
new FrameworkPropertyMetadata((d, e) => {
|
||||
if (d is ColumnBase c && e.NewValue is settings.GridColumnSettings s)
|
||||
c.load_from(s);
|
||||
}));
|
||||
|
||||
public static settings.GridColumnSettings GetColumnSettings(DependencyObject d) => d.GetValue(ColumnSettingsProperty) as settings.GridColumnSettings;
|
||||
public static void SetColumnSettings(DependencyObject d, settings.GridColumnSettings value) => d.SetValue(ColumnSettingsProperty, value);
|
||||
#endregion
|
||||
|
||||
public static readonly DependencyProperty BackgroundOverlayProperty =
|
||||
DependencyProperty.RegisterAttached(
|
||||
"BackgroundOverlay",
|
||||
@@ -898,6 +1184,7 @@ namespace mbts.ui.controls {
|
||||
|
||||
#endregion
|
||||
|
||||
IDisposable _group_style_row_presenter_link;
|
||||
// Overrides
|
||||
#region Overrides
|
||||
public override void OnApplyTemplate() {
|
||||
@@ -910,6 +1197,8 @@ namespace mbts.ui.controls {
|
||||
_scroll_horizontal = (ScrollBar)GetTemplateChild("_scroll_horiz");
|
||||
_row_presenter = (ListBox)GetTemplateChild("_row_presenter");
|
||||
Log.assert_d(!_row_presenter.IsLoaded);
|
||||
// (re)wrap group style
|
||||
Disposable.replace(ref _group_style_row_presenter_link, ObservableAutoWrapper.connect_two_way(GroupStyle, _row_presenter.GroupStyle));
|
||||
_try_set_row_style();
|
||||
_header_scroll_viewer = (ScrollViewer)GetTemplateChild("_header_scroll_viewer");
|
||||
Log.assert_d(!_header_scroll_viewer.IsLoaded);
|
||||
@@ -1047,6 +1336,31 @@ namespace mbts.ui.controls {
|
||||
}
|
||||
}
|
||||
|
||||
static class ColumnResourceHelper {
|
||||
public static string col_resource_id(object col_id) =>
|
||||
$"Columns_{col_id}";
|
||||
|
||||
public static string col_prop_resource_id(object col_id, string prop) =>
|
||||
$"{col_resource_id(col_id)}_{prop}";
|
||||
|
||||
public static void ObservedCachedParamsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) =>
|
||||
reset_cell_resource_references(d as FrameworkElement, e.OldValue as IEnumerable<CachedColumnParam>, e.NewValue as IEnumerable<CachedColumnParam>);
|
||||
|
||||
public static void reset_cell_resource_references(FrameworkElement target, IEnumerable<CachedColumnParam> old_params, IEnumerable<CachedColumnParam> new_params) =>
|
||||
reset_cell_resource_references(target, GetColumnKey(target), old_params, new_params);
|
||||
|
||||
public static void reset_cell_resource_references(FrameworkElement target, object col_id, IEnumerable<CachedColumnParam> old_params, IEnumerable<CachedColumnParam> new_params) {
|
||||
if (target == null || col_id == null)
|
||||
return;
|
||||
foreach (var p in old_params.empty_if_null()) {
|
||||
target.ClearValue(p.TargetProperty);
|
||||
}
|
||||
foreach (var p in new_params.empty_if_null()) {
|
||||
target.SetResourceReference(p.TargetProperty, col_prop_resource_id(col_id, p.SourcePropertyName));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DataContextHooks : IDisposable {
|
||||
List<IDisposable> _hooks = new List<IDisposable>(4);
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<ResourceDictionary
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mbts="clr-namespace:mbts.ui"
|
||||
xmlns:ui="clr-namespace:mbts.ui"
|
||||
xmlns:bts="clr-namespace:mbts.ui.controls"
|
||||
xmlns:u="clr-namespace:mbts.ui"
|
||||
xmlns:c="clr-namespace:mbts.ui.controls"
|
||||
xmlns:cv="clr-namespace:mbts.ui.converters"
|
||||
>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary Source="/view/Controls/Popup/PopupControl.xaml"/>
|
||||
@@ -34,17 +34,22 @@
|
||||
</Setter>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="ItemsControl.AlternationIndex" Value="0">
|
||||
<Setter Property="Background" Value="{ui:Brush {mbts:BTSColor Grid.input_cell_background}}"/>
|
||||
<Setter Property="Background" Value="{u:Brush {u:BTSColor Grid.input_cell_background}}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="ItemsControl.AlternationIndex" Value="1">
|
||||
<Setter Property="Background" Value="{ui:Brush {mbts:BTSColor Grid.alternate_row_background}}"/>
|
||||
<Setter Property="Background" Value="{u:Brush {u:BTSColor Grid.alternate_row_background}}"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
<Style TargetType="bts:DIYGridRowsPresenter" BasedOn="{StaticResource {x:Type ListBox}}">
|
||||
<Style TargetType="c:DIYGridRowsPresenter" BasedOn="{StaticResource {x:Type ListBox}}">
|
||||
<Setter Property="IsTextSearchEnabled" Value="False"/>
|
||||
</Style>
|
||||
<Style TargetType="bts:DIYGrid">
|
||||
<DataTemplate x:Key="DefaultColumnHeaderContentTemplate">
|
||||
<Grid>
|
||||
<TextBlock HorizontalAlignment="Center" VerticalAlignment="Center" Text="{Binding}" TextWrapping="WrapWithOverflow" TextAlignment="Center"/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
<Style TargetType="c:DIYGrid">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="BorderBrush" Value="Transparent"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
@@ -55,14 +60,16 @@
|
||||
<Style TargetType="GridViewColumnHeader">
|
||||
<!-- Background - note that we're settings this property on the containing stackpanel in which the GridViewHeaderRowPresenter resides -->
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="Foreground" Value="{mbts:BTSColor header_text}"/>
|
||||
<Setter Property="BorderBrush" Value="{mbts:BTSColor header}"/>
|
||||
<Setter Property="Foreground" Value="{u:BTSColor header_text}"/>
|
||||
<Setter Property="BorderBrush" Value="{u:BTSColor header}"/>
|
||||
<Setter Property="BorderThickness" Value="0,1,1,0"/>
|
||||
<Setter Property="Command" Value="{x:Static bts:DIYGrid.SortCommand}"/>
|
||||
<Setter Property="Command" Value="{x:Static c:DIYGrid.SortCommand}"/>
|
||||
<Setter Property="CommandParameter" Value="{Binding RelativeSource={RelativeSource Self}}"/>
|
||||
<Setter Property="CommandTarget" Value="{Binding RelativeSource={RelativeSource AncestorType=bts:DIYGrid}}"/>
|
||||
<Setter Property="HorizontalContentAlignment" Value="Center"/>
|
||||
<Setter Property="Height" Value="{x:Static bts:DIYGrid.DefaultColumnHeaderHeight}"/>
|
||||
<Setter Property="CommandTarget" Value="{Binding RelativeSource={RelativeSource AncestorType=c:DIYGrid}}"/>
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
|
||||
<Setter Property="VerticalContentAlignment" Value="Stretch"/>
|
||||
<Setter Property="Height" Value="{x:Static c:DIYGrid.DefaultColumnHeaderHeight}"/>
|
||||
<Setter Property="UseLayoutRounding" Value="False"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="{x:Type GridViewColumnHeader}">
|
||||
@@ -93,7 +100,7 @@
|
||||
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"
|
||||
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
|
||||
/>
|
||||
<Border x:Name="OperatorsBorder" Grid.Column="1" Background="{mbts:BTSColor Name=header}">
|
||||
<Border x:Name="OperatorsBorder" Grid.Column="1" Background="{u:BTSColor Name=header}">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
@@ -113,7 +120,7 @@
|
||||
Visibility="Collapsed"
|
||||
Width="8"
|
||||
/>
|
||||
<bts:PopupControl
|
||||
<c:PopupControl
|
||||
x:Name="FilterPopup"
|
||||
Grid.Column="1"
|
||||
Margin="2,0,0,0"
|
||||
@@ -125,44 +132,44 @@
|
||||
ShowExpanderArrow="False"
|
||||
PopupPlacement="Bottom"
|
||||
PlacementTarget="{Binding RelativeSource={RelativeSource AncestorType=GridViewColumnHeader}}"
|
||||
IsOpen="{Binding Path='Column.(bts:DataGridFilterProperties.FilterOpen)', Mode=TwoWay, RelativeSource={RelativeSource AncestorType=GridViewColumnHeader}}"
|
||||
ContentTemplate="{Binding Path='Column.(bts:DataGridFilterProperties.FilterTemplate)', RelativeSource={RelativeSource AncestorType=GridViewColumnHeader}}"
|
||||
Content="{Binding Path='Column.(bts:DataGridFilterProperties.Filter)', RelativeSource={RelativeSource AncestorType=GridViewColumnHeader}}"
|
||||
IsOpen="{Binding Path='Column.(c:DataGridFilterProperties.FilterOpen)', Mode=TwoWay, RelativeSource={RelativeSource AncestorType=GridViewColumnHeader}}"
|
||||
ContentTemplate="{Binding Path='Column.(c:DataGridFilterProperties.FilterTemplate)', RelativeSource={RelativeSource AncestorType=GridViewColumnHeader}}"
|
||||
Content="{Binding Path='Column.(c:DataGridFilterProperties.Filter)', RelativeSource={RelativeSource AncestorType=GridViewColumnHeader}}"
|
||||
>
|
||||
<bts:PopupControl.InputBindings>
|
||||
<c:PopupControl.InputBindings>
|
||||
<KeyBinding
|
||||
Key="Return"
|
||||
CommandParameter="{Binding Path='Column.(bts:DataGridFilterProperties.Filter)', RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=GridViewColumnHeader}}"
|
||||
Command="{x:Static bts:DataGridFilterProperties.CommitCommand}"
|
||||
CommandParameter="{Binding Path='Column.(c:DataGridFilterProperties.Filter)', RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=GridViewColumnHeader}}"
|
||||
Command="{x:Static c:DataGridFilterProperties.CommitCommand}"
|
||||
/>
|
||||
<KeyBinding Key="Esc" Command="{x:Static bts:PopupControl.CloseCommand}"/>
|
||||
</bts:PopupControl.InputBindings>
|
||||
<bts:PopupControl.CommandBindings>
|
||||
<KeyBinding Key="Esc" Command="{x:Static c:PopupControl.CloseCommand}"/>
|
||||
</c:PopupControl.InputBindings>
|
||||
<c:PopupControl.CommandBindings>
|
||||
<CommandBinding
|
||||
Command="{x:Static bts:DataGridFilterProperties.CommitCommand}"
|
||||
Executed="{x:Static bts:DataGridFilterProperties.commit_executed}"
|
||||
CanExecute="{x:Static bts:DataGridFilterProperties.commit_can_execute}"
|
||||
Command="{x:Static c:DataGridFilterProperties.CommitCommand}"
|
||||
Executed="{x:Static c:DataGridFilterProperties.commit_executed}"
|
||||
CanExecute="{x:Static c:DataGridFilterProperties.commit_can_execute}"
|
||||
/>
|
||||
<CommandBinding
|
||||
Command="{x:Static bts:DataGridFilterProperties.ClearCommand}"
|
||||
Executed="{x:Static bts:DataGridFilterProperties.clear_executed}"
|
||||
CanExecute="{x:Static bts:DataGridFilterProperties.clear_can_execute}"
|
||||
Command="{x:Static c:DataGridFilterProperties.ClearCommand}"
|
||||
Executed="{x:Static c:DataGridFilterProperties.clear_executed}"
|
||||
CanExecute="{x:Static c:DataGridFilterProperties.clear_can_execute}"
|
||||
/>
|
||||
</bts:PopupControl.CommandBindings>
|
||||
<bts:PopupControl.Header>
|
||||
</c:PopupControl.CommandBindings>
|
||||
<c:PopupControl.Header>
|
||||
<Grid>
|
||||
<Image Name="FilterPopupImage" Height="9" Width="9" Margin="3">
|
||||
<Image.Style>
|
||||
<Style TargetType="Image">
|
||||
<Setter Property="Source" Value="{x:Static mbts:Icons.Filter}"/>
|
||||
<Setter Property="Source" Value="{x:Static u:Icons.Filter}"/>
|
||||
<Setter Property="Opacity" Value="0.0"/>
|
||||
</Style>
|
||||
</Image.Style>
|
||||
</Image>
|
||||
</Grid>
|
||||
</bts:PopupControl.Header>
|
||||
<bts:PopupControl.Style>
|
||||
<Style TargetType="bts:PopupControl">
|
||||
</c:PopupControl.Header>
|
||||
<c:PopupControl.Style>
|
||||
<Style TargetType="c:PopupControl">
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="BorderBrush" Value="LightGray"/>
|
||||
@@ -173,8 +180,8 @@
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</bts:PopupControl.Style>
|
||||
</bts:PopupControl>
|
||||
</c:PopupControl.Style>
|
||||
</c:PopupControl>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
@@ -216,8 +223,8 @@
|
||||
<Setter Property="Grid.Column" TargetName="SortArrow" Value="0"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter Property="Background" TargetName="HeaderBorder" Value="{mbts:BTSColor Name=header_alternate}"/>
|
||||
<Setter Property="Background" TargetName="OperatorsBorder" Value="{mbts:BTSColor Name=header_alternate}"/>
|
||||
<Setter Property="Background" TargetName="HeaderBorder" Value="{u:BTSColor Name=header_alternate}"/>
|
||||
<Setter Property="Background" TargetName="OperatorsBorder" Value="{u:BTSColor Name=header_alternate}"/>
|
||||
<Setter Property="Visibility" TargetName="PART_HeaderGripper" Value="Hidden"/>
|
||||
<Setter Property="Margin" TargetName="HeaderContent" Value="1,1,0,0"/>
|
||||
</Trigger>
|
||||
@@ -225,21 +232,21 @@
|
||||
<Setter Property="MinHeight" Value="20"/>
|
||||
</Trigger>
|
||||
<!--Filter Active-->
|
||||
<DataTrigger Binding="{Binding Path='Column.(bts:DataGridFilterProperties.FilterActive)', RelativeSource={RelativeSource Self}}" Value="True">
|
||||
<Setter TargetName="HeaderContent" Property="Panel.ZIndex" Value="0"/>
|
||||
<Setter TargetName="FilterPopupImage" Property="Source" Value="{x:Static mbts:Icons.FilterActive}"/>
|
||||
<DataTrigger Binding="{Binding Path='Column.(c:DataGridFilterProperties.FilterActive)', RelativeSource={RelativeSource Self}}" Value="True">
|
||||
<Setter TargetName="HeaderContent" Property="Margin" Value="0,0,11,0"/>
|
||||
<Setter TargetName="FilterPopupImage" Property="Source" Value="{x:Static u:Icons.FilterActive}"/>
|
||||
<Setter TargetName="FilterPopupImage" Property="Opacity" Value="1.0"/>
|
||||
<Setter Property="Grid.Column" TargetName="SortArrow" Value="0"/>
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding Path='Column.(bts:DataGridFilterProperties.FilterActive)', RelativeSource={RelativeSource Self}}" Value="{x:Null}">
|
||||
<Setter TargetName="HeaderContent" Property="Panel.ZIndex" Value="0"/>
|
||||
<Setter TargetName="FilterPopupImage" Property="Source" Value="{x:Static mbts:Icons.FilterError}"/>
|
||||
<DataTrigger Binding="{Binding Path='Column.(c:DataGridFilterProperties.FilterActive)', RelativeSource={RelativeSource Self}}" Value="{x:Null}">
|
||||
<Setter TargetName="HeaderContent" Property="Margin" Value="0,0,11,0"/>
|
||||
<Setter TargetName="FilterPopupImage" Property="Source" Value="{x:Static u:Icons.FilterError}"/>
|
||||
<Setter TargetName="FilterPopupImage" Property="Opacity" Value="1.0"/>
|
||||
<Setter Property="Grid.Column" TargetName="SortArrow" Value="0"/>
|
||||
</DataTrigger>
|
||||
<!--Sort Active-->
|
||||
<DataTrigger Binding="{Binding Column.SortDirection, RelativeSource={RelativeSource Self}}" Value="Ascending">
|
||||
<Setter TargetName="HeaderContent" Property="Panel.ZIndex" Value="0"/>
|
||||
<Setter TargetName="HeaderContent" Property="Margin" Value="0,0,11,0"/>
|
||||
<Setter TargetName="SortArrow" Property="Visibility" Value="Visible"/>
|
||||
<Setter TargetName="SortArrow" Property="RenderTransform">
|
||||
<Setter.Value>
|
||||
@@ -248,7 +255,7 @@
|
||||
</Setter>
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding Column.SortDirection, RelativeSource={RelativeSource Self}}" Value="Descending">
|
||||
<Setter TargetName="HeaderContent" Property="Panel.ZIndex" Value="0"/>
|
||||
<Setter TargetName="HeaderContent" Property="Margin" Value="0,0,11,0"/>
|
||||
<Setter TargetName="SortArrow" Property="Visibility" Value="Visible"/>
|
||||
</DataTrigger>
|
||||
</ControlTemplate.Triggers>
|
||||
@@ -262,10 +269,10 @@
|
||||
<Setter.Value>
|
||||
<Style TargetType="{x:Type ToggleButton}">
|
||||
<Setter Property="Focusable" Value="False"/>
|
||||
<Setter Property="Background" Value="{mbts:BTSColor Name=header}"/>
|
||||
<Setter Property="Foreground" Value="{mbts:BTSColor Name=header_text}"/>
|
||||
<Setter Property="Background" Value="{u:BTSColor Name=header}"/>
|
||||
<Setter Property="Foreground" Value="{u:BTSColor Name=header_text}"/>
|
||||
<Setter Property="HorizontalContentAlignment" Value="Center"/>
|
||||
<Setter Property="BorderBrush" Value="{ui:Brush Color={ui:Color Name=Grid.grid_vertical_group_separator}}"/>
|
||||
<Setter Property="BorderBrush" Value="{u:Brush Color={u:Color Name=Grid.grid_vertical_group_separator}}"/>
|
||||
<Setter Property="BorderThickness" Value="0,0,2,0"/>
|
||||
<Setter Property="Margin" Value="0,0,0,-1"/>
|
||||
<Setter Property="FontWeight" Value="Bold"/>
|
||||
@@ -290,13 +297,37 @@
|
||||
</Setter>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="{x:Type bts:DIYGrid}">
|
||||
<ControlTemplate TargetType="{x:Type c:DIYGrid}">
|
||||
<Border
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
>
|
||||
<Grid>
|
||||
<u:StateManager.VisualState>
|
||||
<Binding Path="FrozenColumnCount" RelativeSource="{RelativeSource TemplatedParent}">
|
||||
<Binding.Converter>
|
||||
<cv:ValueMap Default="Active">
|
||||
<cv:KeyValue Key="{u:Int 0}" Value="Inactive"/>
|
||||
</cv:ValueMap>
|
||||
</Binding.Converter>
|
||||
</Binding>
|
||||
</u:StateManager.VisualState>
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup Name="FrozenColumnHeaderStates">
|
||||
<VisualState Name="Active"/>
|
||||
<VisualState Name="Inactive">
|
||||
<Storyboard>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" Storyboard.TargetName="_header_frozen">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{x:Static Visibility.Collapsed}"/>
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" Storyboard.TargetName="_super_header_frozen">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{x:Static Visibility.Collapsed}"/>
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
</Storyboard>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
<Grid.ColumnDefinitions>
|
||||
<!-- Frozen -->
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
@@ -309,9 +340,9 @@
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
<!-- fill the space above the frozen columns -->
|
||||
<Rectangle Fill="{mbts:BTSColor Name=header}" Height="{Binding Path=ActualHeight, ElementName=_super_headers}" HorizontalAlignment="Stretch" Margin="0,0,-1,-1"/>
|
||||
<Rectangle x:Name="_super_header_frozen" Fill="{u:BTSColor Name=header}" Height="{Binding Path=ActualHeight, ElementName=_super_headers}" HorizontalAlignment="Stretch" Margin="0,0,-1,-1"/>
|
||||
<!-- colums set in code behind -->
|
||||
<GridViewHeaderRowPresenter x:Name="_header_frozen" Grid.Row="1" Grid.Column="0" AllowsColumnReorder="False" VerticalAlignment="Bottom" ColumnHeaderContainerStyle="{TemplateBinding ColumnHeaderStyle}" UseLayoutRounding="True"/>
|
||||
<GridViewHeaderRowPresenter x:Name="_header_frozen" Grid.Row="1" Grid.Column="0" AllowsColumnReorder="False" VerticalAlignment="Bottom" ColumnHeaderContainerStyle="{TemplateBinding ColumnHeaderStyle}"/>
|
||||
<ScrollViewer
|
||||
x:Name="_header_scroll_viewer"
|
||||
Grid.Column="1"
|
||||
@@ -323,18 +354,21 @@
|
||||
>
|
||||
<!-- setting the bg color for this stackpanel is a work-around for the vertical white lines that appear on high-dpi displays between some columns -->
|
||||
<!-- in reality, there is some high dpi rounding thing going on, but that's a battle for another day -->
|
||||
<StackPanel Background="{mbts:BTSColor Name=header}">
|
||||
<StackPanel Background="{u:BTSColor Name=header}">
|
||||
<!-- We don't need super headers over the fixed columns -->
|
||||
<StackPanel Orientation="Horizontal" x:Name="_super_headers"/>
|
||||
<GridViewHeaderRowPresenter x:Name="_header_scroll" AllowsColumnReorder="False" ColumnHeaderContainerStyle="{TemplateBinding ColumnHeaderStyle}" UseLayoutRounding="True"/>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
<bts:DIYGridRowsPresenter
|
||||
<c:DIYGridRowsPresenter
|
||||
x:Name="_row_presenter"
|
||||
Grid.Row="2"
|
||||
Grid.ColumnSpan="2"
|
||||
ScrollViewer.HorizontalScrollBarVisibility="Hidden"
|
||||
VirtualizingPanel.IsVirtualizingWhenGrouping="True"
|
||||
VirtualizingPanel.VirtualizationMode="Recycling"
|
||||
ScrollViewer.CanContentScroll="True"
|
||||
VirtualizingPanel.ScrollUnit="{TemplateBinding VirtualizingPanel.ScrollUnit}"
|
||||
AlternationCount="2"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderThickness="0"
|
||||
@@ -343,17 +377,17 @@
|
||||
SelectedItem="{Binding CurrentRow, RelativeSource={RelativeSource TemplatedParent}}"
|
||||
SelectionMode="{TemplateBinding SelectionMode}"
|
||||
>
|
||||
<bts:DIYGridRowsPresenter.Resources>
|
||||
<c:DIYGridRowsPresenter.Resources>
|
||||
<Style TargetType="ListBoxItem" BasedOn="{StaticResource DIYGridRowStyle}"/>
|
||||
</bts:DIYGridRowsPresenter.Resources>
|
||||
<bts:DIYGridRowsPresenter.Template>
|
||||
<ControlTemplate TargetType="{x:Type bts:DIYGridRowsPresenter}">
|
||||
</c:DIYGridRowsPresenter.Resources>
|
||||
<c:DIYGridRowsPresenter.Template>
|
||||
<ControlTemplate TargetType="{x:Type c:DIYGridRowsPresenter}">
|
||||
<ScrollViewer Focusable="False">
|
||||
<ItemsPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
|
||||
</ScrollViewer>
|
||||
</ControlTemplate>
|
||||
</bts:DIYGridRowsPresenter.Template>
|
||||
</bts:DIYGridRowsPresenter>
|
||||
</c:DIYGridRowsPresenter.Template>
|
||||
</c:DIYGridRowsPresenter>
|
||||
<ScrollBar Grid.Row="3" Grid.Column="1" Orientation="Horizontal" LargeChange="10" x:Name="_scroll_horiz" Visibility="{TemplateBinding RowsHorizontalScrollBarVisibility}"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace mbts.ui.controls
|
||||
{
|
||||
@@ -7,6 +8,34 @@ namespace mbts.ui.controls
|
||||
/* smallest such that 1.0+DBL_EPSILON != 1.0 */
|
||||
private const double EPSILON = 2.2204460492503131e-016;
|
||||
private const double BIG_EPSILON = EPSILON * 10;
|
||||
|
||||
public static double nan_to_zero(double value) =>
|
||||
double.IsNaN(value) ? 0.0 : value;
|
||||
/// <summary>
|
||||
/// Returns <see cref="double.NaN"/> if both <paramref name="v1"/> and <paramref name="v2"/> are nan, otherwise returns the sum of the non-nan values (if only one value is nan, the other value is returned).
|
||||
/// </summary>
|
||||
/// <param name="v1">One of the two values to add.</param>
|
||||
/// <param name="v2">One of the two values to add.</param>
|
||||
/// <returns>The sum of the two values, with special handling for nans.</returns>
|
||||
public static double nan_to_zero_sum(double v1, double v2) =>
|
||||
double.IsNaN(v1) ?
|
||||
(double.IsNaN(v2) ? double.NaN : v2) :
|
||||
(double.IsNaN(v2) ? v1 : v1 + v2);
|
||||
|
||||
public static double nan_to_zero_sum(params double[] values) =>
|
||||
values.Aggregate(0.0, (sum, d) => double.IsNaN(d) ? sum : sum + d);
|
||||
|
||||
/// <summary>
|
||||
/// Returns <see cref="double.NaN"/> if both <paramref name="left"/> and <paramref name="right"/> are nan. Otherwise, if only one is nan, it is substituted for zero, and the difference of the resultant values is returned.
|
||||
/// </summary>
|
||||
/// <param name="left">The left operand (minuend) in the subtraction op.</param>
|
||||
/// <param name="right">The right operand (subtrahend) to be used in the subtraction op.</param>
|
||||
/// <returns>The difference of the two values, with special handling for nans.</returns>
|
||||
public static double nan_to_zero_diff(double left, double right) =>
|
||||
double.IsNaN(left) ?
|
||||
(double.IsNaN(right) ? double.NaN : -right) :
|
||||
(double.IsNaN(right) ? left : left - right);
|
||||
|
||||
internal static bool AreClose(double v1, double v2) {
|
||||
if (v1 == v2) return true;
|
||||
double delta = Math.Abs(v1 - v2);
|
||||
|
||||
@@ -90,11 +90,11 @@ namespace mbts.ui.settings {
|
||||
public bool is_collapsed => _is_collapsed;
|
||||
public FontWeight? font_weight_override => _font_weight_override.value;
|
||||
public Color? font_color_override => _font_color_override.value;
|
||||
public Color? background_color_override => _background_color_override.argb;
|
||||
public double width => _width;
|
||||
public HorizontalAlignment horizontal_alignment => _horizontal_alignment;
|
||||
public double horizontal_offset => _horizontal_offset;
|
||||
public string display_name => _display_name;
|
||||
public Color? background_color_override => _background_color_override.argb;
|
||||
|
||||
public GridColumnSettings with(
|
||||
string display_name = null, bool? is_visible = null, bool? is_collapsed = null, NullableFontWeightProto? font_weight_override = null, NullableColorProto? font_color_override = null,
|
||||
@@ -119,11 +119,11 @@ namespace mbts.ui.settings {
|
||||
is_collapsed == other.is_collapsed &&
|
||||
font_weight_override.Equals(other.font_weight_override) &&
|
||||
font_color_override.Equals(other.font_color_override) &&
|
||||
background_color_override.Equals(other.background_color_override) &&
|
||||
width == other.width &&
|
||||
horizontal_alignment == other.horizontal_alignment &&
|
||||
horizontal_offset == other.horizontal_offset &&
|
||||
display_name == other.display_name &&
|
||||
background_color_override.Equals(other.background_color_override);
|
||||
display_name == other.display_name;
|
||||
|
||||
public override bool Equals(object obj) =>
|
||||
Equals(obj as GridColumnSettings);
|
||||
@@ -136,19 +136,15 @@ namespace mbts.ui.settings {
|
||||
is_collapsed.GetHashCode(),
|
||||
font_weight_override.GetHashCode(),
|
||||
font_color_override.GetHashCode(),
|
||||
background_color_override.GetHashCode(),
|
||||
width.GetHashCode(),
|
||||
horizontal_alignment.GetHashCode(),
|
||||
horizontal_offset.GetHashCode(),
|
||||
display_name.GetHashCode(),
|
||||
background_color_override.GetHashCode()
|
||||
display_name.GetHashCode()
|
||||
);
|
||||
|
||||
public override string ToString() {
|
||||
return $"id: {id} display_name: {display_name}";
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -94,8 +94,8 @@ namespace mbts.spark {
|
||||
can_override_display_name ? display_name : null,
|
||||
is_visible,
|
||||
null,
|
||||
is_font_weight_overridden ? new NullableFontWeightProto(font_weight_override) : default,
|
||||
is_font_color_overridden ? new NullableColorProto(font_color_override.color) : default,
|
||||
is_font_weight_overridden ? new NullableFontWeightProto(font_weight_override) : new NullableFontWeightProto(null),
|
||||
is_font_color_overridden ? new NullableColorProto(font_color_override.color) : new NullableColorProto(null),
|
||||
width,
|
||||
horizontal_alignment,
|
||||
background_color_override: is_background_color_overridden ?
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using bts.collections;
|
||||
using bts.proto;
|
||||
using bts.proto;
|
||||
using bts.utils;
|
||||
using mbts.ui.controls;
|
||||
using mbts.ui.converters;
|
||||
@@ -7,7 +6,6 @@ using ProtoBuf;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Media;
|
||||
@@ -17,7 +15,9 @@ namespace mbts.ui.settings {
|
||||
class GridSettings : IEquatable<GridSettings> {
|
||||
const string _default_font_family = "Segoe UI";
|
||||
const double _default_font_size = 11;
|
||||
const uint _default_font_color = 0;
|
||||
const uint
|
||||
_default_font_color = 0,
|
||||
_default_background = 0x00FFFFFF;
|
||||
const ushort _default_font_weight = 400;
|
||||
const uint _default_background_color = 0x00FFFFFF; // Colors.Transparent
|
||||
const bool _default_enable_background_color = false;
|
||||
@@ -25,7 +25,7 @@ namespace mbts.ui.settings {
|
||||
public static GridSettings Default =>
|
||||
new GridSettings(
|
||||
selection_highlight: new ColorOpacityProto(FallbackValues.selection_highlight.Color, FallbackValues.selection_highlight.Opacity),
|
||||
background_color: _default_background_color, //new ColorOpacityProto(FallbackValues.background_color.Color, FallbackValues.background_color.Opacity));
|
||||
background: _default_background_color, //new ColorOpacityProto(FallbackValues.background_color.Color, FallbackValues.background_color.Opacity));
|
||||
enable_background_color: _default_enable_background_color);
|
||||
|
||||
// corrects any inappropriate values that would render the grid unusable (usually nulls)
|
||||
@@ -46,7 +46,7 @@ namespace mbts.ui.settings {
|
||||
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 uint background_color => _default_background_color;
|
||||
public static uint background_color => _default_background;
|
||||
}
|
||||
|
||||
public static IValueConverter selection_highlight_converter => ValueConverter.create<GridSettings, SolidColorBrush>(gs => gs.selection_highlight.to_brush().frozen());
|
||||
@@ -54,29 +54,37 @@ namespace mbts.ui.settings {
|
||||
public static IValueConverter font_weight_converter => ValueConverter.create<GridSettings, FontWeight>(gs => gs.font_weight);
|
||||
public static IValueConverter font_size_converter => ValueConverter.create<GridSettings, double>(gs => gs.font_size);
|
||||
public static IValueConverter font_color_converter => ValueConverter.create<GridSettings, SolidColorBrush>(gs => new SolidColorBrush(gs.font_color.rgb_color()).frozen());
|
||||
|
||||
// font family
|
||||
[ProtoMember(1)]
|
||||
[DefaultValue(_default_font_family)]
|
||||
readonly string _font_family;
|
||||
// font size
|
||||
[ProtoMember(2)]
|
||||
[DefaultValue(_default_font_size)]
|
||||
readonly double _font_size;
|
||||
// font weight
|
||||
[ProtoMember(3)]
|
||||
[DefaultValue(_default_font_weight)]
|
||||
readonly ushort _font_weight;
|
||||
// font color
|
||||
[ProtoMember(4)]
|
||||
[DefaultValue(_default_font_color)]
|
||||
readonly uint _font_color;
|
||||
// selection highlight
|
||||
[ProtoMember(5)]
|
||||
readonly ColorOpacityProto _selection_highlight;
|
||||
// columns
|
||||
[ProtoMember(6)]
|
||||
List<GridColumnSettings> _columns;
|
||||
// sorting criterion
|
||||
[ProtoMember(7)]
|
||||
List<SortingCriteria> _sorting_criterion;
|
||||
// background
|
||||
[ProtoMember(8)]
|
||||
readonly uint _background_color;
|
||||
[DefaultValue(_default_background)]
|
||||
uint _background;
|
||||
[ProtoMember(9)]
|
||||
[DefaultValue(false)]
|
||||
[DefaultValue(_default_enable_background_color)]
|
||||
readonly bool _enable_background_color;
|
||||
|
||||
public GridSettings() {
|
||||
@@ -84,15 +92,20 @@ namespace mbts.ui.settings {
|
||||
_font_size = _default_font_size;
|
||||
_font_weight = _default_font_weight;
|
||||
_font_color = _default_font_color;
|
||||
|
||||
// new setting in proto defaulted here
|
||||
_background_color = _default_background_color;
|
||||
_background = _default_background;
|
||||
_enable_background_color = _default_enable_background_color;
|
||||
}
|
||||
|
||||
public GridSettings(
|
||||
string font_family = _default_font_family, double font_size = _default_font_size, FontWeight font_weight = default, uint font_color = _default_font_color,
|
||||
ColorOpacityProto? selection_highlight = null, IEnumerable<GridColumnSettings> columns = null, IEnumerable<SortingCriteria> sorting_criterion = null, uint background_color = _default_background_color, bool enable_background_color = _default_enable_background_color) {
|
||||
string font_family = _default_font_family,
|
||||
double font_size = _default_font_size,
|
||||
FontWeight font_weight = default,
|
||||
uint font_color = _default_font_color,
|
||||
ColorOpacityProto? selection_highlight = null,
|
||||
IEnumerable<GridColumnSettings> columns = null,
|
||||
IEnumerable<SortingCriteria> sorting_criterion = null,
|
||||
uint background = _default_background,
|
||||
bool enable_background_color = _default_enable_background_color) {
|
||||
//
|
||||
_font_family = font_family;
|
||||
_font_size = font_size;
|
||||
@@ -101,7 +114,7 @@ namespace mbts.ui.settings {
|
||||
_selection_highlight = selection_highlight ?? new ColorOpacityProto(0, 1);
|
||||
_columns = new List<GridColumnSettings>(columns.empty_if_null());
|
||||
_sorting_criterion = new List<SortingCriteria>(sorting_criterion.empty_if_null());
|
||||
_background_color = background_color;
|
||||
_background = background;
|
||||
_enable_background_color = enable_background_color;
|
||||
}
|
||||
|
||||
@@ -109,18 +122,24 @@ namespace mbts.ui.settings {
|
||||
public double font_size => _font_size;
|
||||
public uint font_color => _font_color;
|
||||
public FontWeight font_weight => FontWeight.FromOpenTypeWeight(_font_weight);
|
||||
public Color background => _background.argb_color();
|
||||
public ColorOpacityProto selection_highlight => _selection_highlight;
|
||||
public uint background_color => _background_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(DIYGrid.DefaultSelectionHighlight.Color, DIYGrid.DefaultSelectionHighlight.Opacity),
|
||||
background_color: _default_background_color);
|
||||
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, bool? enable_background_color = null) =>
|
||||
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) =>
|
||||
new GridSettings(
|
||||
font_family ?? _font_family,
|
||||
DoubleHelper.IsNaNOrInfinityOr(font_size, 0) ? _font_size : font_size,
|
||||
@@ -129,7 +148,7 @@ namespace mbts.ui.settings {
|
||||
_selection_highlight,
|
||||
columns ?? _columns,
|
||||
sorting_criterion ?? _sorting_criterion,
|
||||
_background_color,
|
||||
background ?? _background,
|
||||
enable_background_color ?? _enable_background_color);
|
||||
|
||||
public GridSettings clone() => with();
|
||||
@@ -141,12 +160,12 @@ namespace mbts.ui.settings {
|
||||
font_weight == other.font_weight &&
|
||||
font_color == other.font_color &&
|
||||
selection_highlight == other.selection_highlight &&
|
||||
_background == other._background &&
|
||||
_enable_background_color == other._enable_background_color &&
|
||||
EqualityComparer.safe_sequence_equals(columns, other.columns) &&
|
||||
#pragma warning disable CS0612 // Type or member is obsolete
|
||||
EqualityComparer.safe_sequence_equals(sorting_criterion, other.sorting_criterion) &&
|
||||
EqualityComparer.safe_sequence_equals(sorting_criterion, other.sorting_criterion);
|
||||
#pragma warning restore CS0612 // Type or member is obsolete
|
||||
background_color == other.background_color &&
|
||||
enable_background_color == other.enable_background_color;
|
||||
|
||||
public override bool Equals(object obj) =>
|
||||
Equals(obj as GridSettings);
|
||||
@@ -158,39 +177,11 @@ namespace mbts.ui.settings {
|
||||
font_weight.GetHashCode(),
|
||||
font_color.GetHashCode(),
|
||||
selection_highlight.GetHashCode(),
|
||||
_background.GetHashCode(),
|
||||
_enable_background_color.GetHashCode(),
|
||||
EqualityComparer.safe_aggregate_hash(columns),
|
||||
#pragma warning disable CS0612 // Type or member is obsolete
|
||||
EqualityComparer.safe_aggregate_hash(sorting_criterion),
|
||||
EqualityComparer.safe_aggregate_hash(sorting_criterion));
|
||||
#pragma warning restore CS0612 // Type or member is obsolete
|
||||
background_color.GetHashCode(),
|
||||
enable_background_color.GetHashCode());
|
||||
}
|
||||
|
||||
static class GridSettingsExtensions {
|
||||
public static bool add_missing_columns(this GridSettings grid, IEnumerable<GridColumnSettings> required_columns, out GridSettings result) {
|
||||
var col_lookup = new Lazy<ILookup<string, GridColumnSettings>>(() => required_columns.ToLookup(c => c.id));
|
||||
return add_missing_columns(grid, required_columns.Select(c => c.id), id => col_lookup.Value.TryGetValue(id, out var matches) ? matches.FirstOrDefault() : null, out result);
|
||||
}
|
||||
|
||||
public static bool add_missing_columns(this GridSettings grid, IEnumerable<string> col_ids, Func<string, GridColumnSettings> col_factory, out GridSettings result) {
|
||||
// add the new column (RawDayPos), if absent.
|
||||
var cols_to_add = new List<GridColumnSettings>(0);
|
||||
foreach (var col_id in col_ids) {
|
||||
if (!grid.columns.Any(c => c.id == col_id)) {
|
||||
var default_col = col_factory(col_id);
|
||||
if (default_col != null)
|
||||
cols_to_add.Add(default_col);
|
||||
}
|
||||
}
|
||||
if (cols_to_add.Count > 0) {
|
||||
var col_list = grid.columns.ToList();
|
||||
col_list.AddRange(cols_to_add);
|
||||
grid = grid.with(col_list);
|
||||
result = grid;
|
||||
return true;
|
||||
}
|
||||
result = grid;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,119 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace mbts.ui.settings {
|
||||
static class GridSettingsHelper {
|
||||
public static TColKey? parse_col_id<TColKey>(string id) where TColKey : struct =>
|
||||
Enum.TryParse(id, true, out TColKey key) ? key : (TColKey?)null;
|
||||
|
||||
public static string fmt_col_id<TColKey>(TColKey key) where TColKey : struct =>
|
||||
key.ToString();
|
||||
|
||||
public static void normalize_sorting_criterion<TColKey>(IEnumerable<SortingCriteria> sorting_criterion, IReadOnlyDictionary<string, TColKey> sort_member_to_id) where TColKey : struct =>
|
||||
normalize_sorting_criterion(sorting_criterion, new Func<string, TColKey?>[] {
|
||||
parse_col_id<TColKey>,
|
||||
col_name => sort_member_to_id.TryGetValue(col_name, out var k) ? k : (TColKey?)null
|
||||
});
|
||||
|
||||
public static void normalize_sorting_criterion<TColKey>(IEnumerable<SortingCriteria> sorting_criterion, IEnumerable<Func<string, TColKey?>> conversion_dels) where TColKey : struct {
|
||||
foreach (var sc in sorting_criterion) {
|
||||
sc.col_name = conversion_dels
|
||||
.Select(del => del(sc.col_name))
|
||||
.FirstOrDefault(k => k.HasValue)?.ToString() ?? sc.col_name;
|
||||
}
|
||||
}
|
||||
}
|
||||
using bts.collections;
|
||||
using mbts.ui.controls;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace mbts.ui.settings {
|
||||
static class GridSettingsHelper {
|
||||
public static bool add_missing_columns(this GridSettings grid, IEnumerable<GridColumnSettings> required_columns, out GridSettings result) {
|
||||
var col_lookup = new Lazy<ILookup<string, GridColumnSettings>>(() => required_columns.ToLookup(c => c.id));
|
||||
return add_missing_columns(grid, required_columns.Select(c => c.id), id => col_lookup.Value.TryGetValue(id, out var matches) ? matches.FirstOrDefault() : null, out result);
|
||||
}
|
||||
|
||||
public static bool add_missing_columns(this GridSettings grid, IEnumerable<string> col_ids, Func<string, GridColumnSettings> col_factory, out GridSettings result) {
|
||||
if (add_missing_columns(grid.columns, col_ids, col_factory, out var new_cols)) {
|
||||
result = grid.with(new_cols);
|
||||
return true;
|
||||
}
|
||||
result = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool add_missing_columns(IEnumerable<GridColumnSettings> columns, IEnumerable<GridColumnSettings> required_columns, out IEnumerable<GridColumnSettings> result) {
|
||||
var col_lookup = new Lazy<ILookup<string, GridColumnSettings>>(() => required_columns.ToLookup(c => c.id));
|
||||
return add_missing_columns(columns, required_columns.Select(c => c.id), id => col_lookup.Value.TryGetValue(id, out var matches) ? matches.FirstOrDefault() : null, out result);
|
||||
}
|
||||
|
||||
public static bool add_missing_columns(IEnumerable<GridColumnSettings> columns, IEnumerable<string> col_ids, Func<string, GridColumnSettings> col_factory, out IEnumerable<GridColumnSettings> result) {
|
||||
// add the new column (RawDayPos), if absent.
|
||||
var cols_to_add = new List<GridColumnSettings>(0);
|
||||
foreach (var col_id in col_ids) {
|
||||
if (!columns.Any(c => c.id == col_id)) {
|
||||
var default_col = col_factory(col_id);
|
||||
if (default_col != null)
|
||||
cols_to_add.Add(default_col);
|
||||
}
|
||||
}
|
||||
if (cols_to_add.Count > 0) {
|
||||
result = columns.Concat(cols_to_add);
|
||||
return true;
|
||||
}
|
||||
result = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public static TColKey? parse_col_id<TColKey>(string id) where TColKey : struct =>
|
||||
Enum.TryParse(id, true, out TColKey key) ? key : (TColKey?)null;
|
||||
|
||||
public static TColKey strong_id<TColKey>(this GridColumnSettings column_settings) where TColKey : struct =>
|
||||
parse_col_id<TColKey>(column_settings.id) ?? throw new ArgumentException($"Failed to parse column settings' id '{column_settings.id}' to type '{typeof(TColKey)}.'");
|
||||
|
||||
public static string fmt_col_id<TColKey>(TColKey key) where TColKey : struct =>
|
||||
key.ToString();
|
||||
|
||||
public static void normalize_sorting_criterion<TColKey>(IEnumerable<SortingCriteria> sorting_criterion, IReadOnlyDictionary<string, TColKey> sort_member_to_id) where TColKey : struct =>
|
||||
normalize_sorting_criterion(sorting_criterion, new Func<string, TColKey?>[] {
|
||||
parse_col_id<TColKey>,
|
||||
col_name => sort_member_to_id.TryGetValue(col_name, out var k) ? k : (TColKey?)null
|
||||
});
|
||||
|
||||
public static void normalize_sorting_criterion<TColKey>(IEnumerable<SortingCriteria> sorting_criterion, IEnumerable<Func<string, TColKey?>> conversion_dels) where TColKey : struct {
|
||||
foreach (var sc in sorting_criterion) {
|
||||
sc.col_name = conversion_dels
|
||||
.Select(del => del(sc.col_name))
|
||||
.FirstOrDefault(k => k.HasValue)?.ToString() ?? sc.col_name;
|
||||
}
|
||||
}
|
||||
|
||||
public static void load_column_settings(IEnumerable<DIYGrid.ColumnBase> columns, GridSettings grid_settings) =>
|
||||
load_column_settings(columns, grid_settings.columns, grid_settings.background, new FontFamily(grid_settings.font_family), grid_settings.font_size, grid_settings.font_color.rgb_color(), grid_settings.font_weight);
|
||||
|
||||
public static void load_column_settings(IEnumerable<DIYGrid.ColumnBase> columns, IEnumerable<GridColumnSettings> settings, Color grid_background, FontFamily grid_font_family, double grid_font_size, Color grid_font_color, FontWeight grid_font_weight) =>
|
||||
load_column_settings(columns, settings, grid_background, grid_font_family, grid_font_size, grid_font_color, grid_font_weight, (c, s) => c.load_from(s));
|
||||
|
||||
public static void load_column_settings<TColumn>(
|
||||
IEnumerable<TColumn> columns,
|
||||
IEnumerable<GridColumnSettings> settings,
|
||||
Color grid_background,
|
||||
FontFamily grid_font_family,
|
||||
double grid_font_size,
|
||||
Color grid_font_color,
|
||||
FontWeight grid_font_weight,
|
||||
Action<TColumn, GridColumnSettings> load_impl)
|
||||
where TColumn : DIYGrid.ColumnBase {
|
||||
//
|
||||
var join =
|
||||
from stg in settings
|
||||
join col in columns on stg.id equals col.id
|
||||
select new { col, stg };
|
||||
foreach (var tup in join) {
|
||||
load_impl(tup.col, tup.stg);
|
||||
tup.col.set_fallback_font_params(grid_font_family, grid_font_size, grid_font_color, grid_font_weight, grid_background);
|
||||
}
|
||||
}
|
||||
|
||||
public static bool try_find_column<T>(IEnumerable<GridColumnSettings> columns, T col_key, out GridColumnSettings result) where T : struct {
|
||||
var key_str = col_key.ToString();
|
||||
return columns.TryGetFirst(c => c.id == key_str, out result);
|
||||
}
|
||||
|
||||
public static bool try_find_column<T>(IEnumerable<GridColumnSettings> columns, T col_key, out GridColumnSettings result, out int index) where T : struct {
|
||||
var key_str = col_key.ToString();
|
||||
return columns.TryGetFirst(c => c.id == key_str, out result, out index);
|
||||
}
|
||||
|
||||
public static bool try_replace_column<T>(IList<GridColumnSettings> columns, T col_key, Func<GridColumnSettings, GridColumnSettings> replace) {
|
||||
var key_str = col_key.ToString();
|
||||
for (int i = 0; i < columns.Count; i++) {
|
||||
if (columns[i].id == key_str) {
|
||||
columns[i] = replace(columns[i]);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loaded 100 of 221 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user