Compare commits
71
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
61a4c0ae3e | ||
|
|
31a28727f7 | ||
|
|
11033d68cd | ||
|
|
e228db7ffa | ||
|
|
b249aa019b | ||
|
|
b8cd9651f6 | ||
|
|
7349c33c7c | ||
|
|
f9f1c31037 | ||
|
|
d507ea6d16 | ||
|
|
7bdd60e744 | ||
|
|
9f2c701ff9 | ||
|
|
fb185a590f | ||
|
|
bfa0046d2d | ||
|
|
0160c2f33b | ||
|
|
1d01c0af35 | ||
|
|
6ce35597f7 | ||
|
|
cab86fa922 | ||
|
|
26e58d2e5c | ||
|
|
dc86b62d5f | ||
|
|
1518655cb7 | ||
|
|
b7fe59e04a | ||
|
|
489892a8e7 | ||
|
|
ac811be188 | ||
|
|
c6ac0d75a3 | ||
|
|
f199a3795e | ||
|
|
639fe9f6f5 | ||
|
|
37cf61bfc7 | ||
|
|
cb477552eb | ||
|
|
d4853c777d | ||
|
|
c58e892b0a | ||
|
|
d780777c7c | ||
|
|
c531fb4ade | ||
|
|
86695a8719 | ||
|
|
d6789e8e7a | ||
|
|
e68b626ec4 | ||
|
|
915e0d38c3 | ||
|
|
553cdcb80f | ||
|
|
bb70e67f36 | ||
|
|
577d2fa742 | ||
|
|
b32c836f34 | ||
|
|
eb3c64a5be | ||
|
|
57f607ef6e | ||
|
|
097a048ba8 | ||
|
|
6b37c87fcc | ||
|
|
32b9c0db10 | ||
|
|
df6b065e43 | ||
|
|
981b429a4e | ||
|
|
d87de14dd4 | ||
|
|
7d2ccc7ae1 | ||
|
|
2db21b6927 | ||
|
|
6b52d225ba | ||
|
|
6c5d084e20 | ||
|
|
dacf2034a1 | ||
|
|
60593cadc1 | ||
|
|
05d7d9357d | ||
|
|
5267e59f61 | ||
|
|
a2dc1ebbf7 | ||
|
|
fcff996025 | ||
|
|
084e99325d | ||
|
|
ae771f6927 | ||
|
|
20e10f49e4 | ||
|
|
3224e0bce7 | ||
|
|
623d6e3136 | ||
|
|
682195720e | ||
|
|
5c190ebc94 | ||
|
|
74ac34bb88 | ||
|
|
66cb884bee | ||
|
|
fcf573b32d | ||
|
|
03788f04d6 | ||
|
|
9f6f51938d | ||
|
|
7a0e9dc098 |
No files matched your search
@@ -40,9 +40,11 @@ namespace mbts.ui {
|
||||
EditDocumentAltGeometryKey = nameof(EditDocumentAltGeometryKey),
|
||||
EditDocumentGeometryKey = nameof(EditDocumentGeometryKey),
|
||||
GearGeometryKey = nameof(GearGeometryKey),
|
||||
KeyGeometryKey = nameof(KeyGeometryKey),
|
||||
LetterAKey = nameof(LetterAKey),
|
||||
LetterDKey = nameof(LetterDKey),
|
||||
OrdersGeometryKey = nameof(OrdersGeometryKey),
|
||||
PauseGeometryKey = nameof(PauseGeometryKey),
|
||||
PaymentGeometryKey = nameof(PaymentGeometryKey),
|
||||
PencilGeometryKey = nameof(PencilGeometryKey),
|
||||
PlayGeometryKey = nameof(PlayGeometryKey),
|
||||
@@ -169,9 +171,12 @@ namespace mbts.ui {
|
||||
public static ImageSource FilterError { get { return load_resource_and_freeze("FilterError"); } }
|
||||
public static ImageSource Gear => create_icon(GearGeometryKey);
|
||||
public static ImageSource Import => load_resource_and_freeze("Import");
|
||||
public static ImageSource Key => create_icon(KeyGeometryKey);
|
||||
public static ImageSource Knight { get { return load_resource_and_freeze("Knight"); } }
|
||||
public static ImageSource Pause => create_icon(PauseGeometryKey);
|
||||
public static ImageSource Payment => create_icon(PaymentGeometryKey);
|
||||
public static ImageSource Pencil => create_icon(PencilGeometryKey);
|
||||
public static ImageSource Play => create_icon(PlayGeometryKey);
|
||||
public static ImageSource Prohibited => create_icon(ProhibitedGeometryKey);
|
||||
public static ImageSource RubberStamp => create_icon(RubberStampGeometryKey);
|
||||
public static ImageSource RedoCircular => create_icon(UndoCircularGeometryKey, Brushes.Black, null, new ScaleTransform(-1, 1));
|
||||
|
||||
@@ -274,10 +274,18 @@
|
||||
M16.000015,9.6000061C12.500012,9.6000061 9.5999844,12.5 9.5999844,16 9.5999844,19.5 12.500012,22.399994 16.000015,22.399994 19.500019,22.399994 22.400015,19.5 22.400015,16 22.400015,12.5 19.500019,9.6000061 16.000015,9.6000061z M14.199995,0L17.800005,0C18.199999,0,18.599993,0.29998779,18.599993,0.79998779L18.599993,4.7999878C19.900013,5.1000061,21.000019,5.6000061,22.099997,6.2999878L25.000023,3.3999939C25.099999,3.2999878 25.300011,3.1999817 25.500025,3.1999817 25.700007,3.1999817 25.900019,3.2999878 26.000025,3.3999939L28.600033,6C28.900021,6.2999878,28.900021,6.7999878,28.600033,7.1000061L25.700007,10C26.400019,11.100006,26.900019,12.299988,27.200007,13.5L31.20001,13.5C31.600037,13.5,31.999998,13.799988,31.999998,14.299988L31.999998,17.899994C31.999998,18.299988,31.700012,18.699982,31.20001,18.699982L27.200007,18.699982C26.900019,20,26.400019,21.100006,25.700007,22.199982L28.600033,25.100006C28.700009,25.199982 28.799984,25.399994 28.799984,25.600006 28.799984,25.799988 28.700009,26 28.600033,26.100006L26.000025,28.600006C25.900019,28.699982 25.700007,28.799988 25.500025,28.799988 25.300011,28.799988 25.099999,28.699982 25.000023,28.600006L22.099997,25.699982C21.000019,26.399994,19.800007,26.899994,18.599993,27.199982L18.599993,31.199982C18.599993,31.600006,18.300005,32,17.800005,32L14.199995,32C13.8,32,13.400006,31.699982,13.400006,31.199982L13.400006,27.199982C12.099987,26.899994,11.000011,26.399994,9.9000027,25.699982L7.0000064,28.600006C6.9000003,28.699982 6.6999881,28.799988 6.5000059,28.799988 6.2999937,28.799988 6.099981,28.699982 6.0000054,28.600006L3.3999967,26C3.0999784,25.699982,3.0999784,25.199982,3.3999967,24.899994L6.2999937,22C5.5999805,20.899994,5.0999801,19.699982,4.7999923,18.5L0.79998828,18.5C0.39999409,18.5,-1.933513E-07,18.199982,0,17.699982L0,14.100006C-1.933513E-07,13.699982,0.2999879,13.299988,0.79998828,13.299988L4.7999923,13.299988C5.0999801,12,5.5999805,10.899994,6.2999937,9.7999878L3.3999967,7C3.2999908,6.8999939 3.1999845,6.6999817 3.1999845,6.5 3.1999845,6.2999878 3.2999908,6.1000061 3.3999967,6L6.0000054,3.3999939C6.099981,3.2999878 6.2999937,3.1999817 6.5000059,3.1999817 6.6999881,3.1999817 6.9000003,3.2999878 7.0000064,3.3999939L9.9000027,6.2999878C11.000011,5.6000061,12.199993,5.1000061,13.400006,4.7999878L13.400006,0.79998779C13.400006,0.29998779,13.8,0,14.199995,0z
|
||||
</sys:String>
|
||||
|
||||
<sys:String x:Key="{x:Static mbts:Icons.KeyGeometryKey}">
|
||||
M22.399981,2.4000006C22.099982,2.4000006 21.899982,2.5 21.599982,2.6999998 21.199983,3.1000004 21.199983,3.8000002 21.599982,4.2000003L26.199979,8.9000001C26.599978,9.3000002 27.299977,9.3000002 27.699977,8.9000001 28.099977,8.5000002 28.099977,7.8000002 27.699977,7.4000006L23.099981,2.6999998C22.899981,2.5,22.699981,2.4000006,22.399981,2.4000006z M21.499982,0C22.299982,0,23.199981,0.30000019,23.79998,0.90000057L29.299976,6.4000001C30.599975,7.7000003,30.599975,9.7000003,29.299976,11L22.599981,17.7C21.299983,19,19.299984,19,17.999985,17.7L17.299985,17 12.699989,21.6 10.399991,20.900001 10.899991,23.400001 8.4999928,22.8 8.9999924,25.5 6.2999945,25.1 6.7999945,27.6 4.2999964,27 4.7999954,29.6 2.4999971,32 0,26.6 13.499989,13.1 16.999986,16.6 12.699989,12.3C11.399991,11,11.399991,9.0000002,12.699989,7.7000003L19.399984,1C19.899983,0.30000019,20.699983,0,21.499982,0z
|
||||
</sys:String>
|
||||
|
||||
<sys:String x:Key="{x:Static mbts:Icons.OrdersGeometryKey}">
|
||||
M50.14375,33.389126C50.416863,33.394688,50.653496,33.587773,50.705998,33.864617L50.015995,45.114388C49.810295,46.291462,49.130394,45.097489,49.130394,45.097489L47.65659,42.640538C42.747776,49.635197 34.633152,54.221001 25.433924,54.221001 14.884193,54.221001 5.7437552,48.204125 1.2449617,39.419205 0.80087067,38.552021 0.21358696,37.705738 0.052086769,36.736958 -0.49735315,33.490925 3.4507786,32.901036 4.8049124,34.432404 5.3674143,35.067692 6.034036,36.16407 6.5079177,37.026051 10.106778,43.799416 17.2321,48.413819 25.433924,48.413819 32.656544,48.413819 39.026364,44.826695 42.903977,39.351506L40.229367,38.854115C40.229367,38.854115,38.677464,38.647118,39.742466,37.996033L50.024895,33.398526C50.064972,33.391365,50.104733,33.388333,50.14375,33.389126z M23.030291,9.0310004L27.889918,9.0310004 27.889918,13.028299C30.539786,13.205498,32.324932,13.958096,33.464161,14.528294L33.964176,14.775793 32.417435,20.038878 31.706417,19.68068C30.579987,19.11958 28.839241,18.244583 26.149073,18.244583 24.006916,18.244583 22.723183,19.122181 22.723183,20.590876 22.723183,21.894272 23.943314,22.804369 27.551308,24.187165 32.706444,26.124659 34.911,28.702853 34.911,32.817341 34.911,36.892929 32.090527,40.05172 27.673611,40.989117L27.673611,45.540001 22.767485,45.540001 22.767485,41.330414C20.252919,41.155815,17.695551,40.424118,16.101811,39.404721L15.683999,39.137619 17.221739,33.731438 17.969059,34.180634C19.088988,34.851235 21.402748,35.97873 24.340325,35.97873 26.734886,35.97873 28.276728,34.921635 28.276728,33.291336 28.276728,31.986641 27.547409,30.853846 23.986016,29.57255 20.083815,28.144253 16.095511,26.04406 16.095511,21.239174 16.095511,17.280985 18.737379,14.291395 23.030291,13.329098z M25.268765,0C35.819447,2.7347414E-07 44.961449,6.0155495 49.458798,14.804573 49.902695,15.669173 50.490094,16.516972 50.651287,17.48427 51.198281,20.734265 47.255726,21.318962 45.901341,19.789065 45.336346,19.153567 44.669552,18.051968 44.19566,17.19517 40.5957,10.421881 33.473275,5.8072398 25.268765,5.8072393 18.047545,5.8072398 11.678013,9.3918336 7.8003562,14.867073L10.474627,15.367072C10.474627,15.367072,12.026909,15.574172,10.962921,16.223872L0.67933281,20.826762C0.36037083,20.880062,0.057029126,20.669165,0.00099972012,20.354063L0.69093342,9.1118043C0.89552094,7.9296158,1.5738537,9.124924,1.5738539,9.124924L3.0452772,11.583379C7.9566038,4.584462,16.073666,2.7347414E-07,25.268765,0z
|
||||
</sys:String>
|
||||
|
||||
<sys:String x:Key="{x:Static mbts:Icons.PauseGeometryKey}">
|
||||
M17.94,0L29.150001,0 29.150001,32 17.94,32z M0,0L11.210002,0 11.210002,32 0,32z
|
||||
</sys:String>
|
||||
|
||||
<sys:String x:Key="{x:Static mbts:Icons.PaymentGeometryKey}">
|
||||
M28.000001,6.0000026L28.000001,15 6.0010006,15 6.0010006,17.000003 30.001,17.000003 30.001,6.0000026z M14.231,2.9540056L14.979999,2.9540056 14.979999,3.987005C15.704998,4.009005,16.200997,4.178005,16.550998,4.3600049L16.248998,5.0620046C15.994998,4.9340047 15.499998,4.7010047 14.725999,4.7010047 13.796,4.7010047 13.445001,5.1900046 13.445001,5.6160046 13.445001,6.1710043 13.893,6.4470041 14.943999,6.8310037 16.187998,7.2780037 16.816998,7.8320036 16.816998,8.7800031 16.816998,9.6210027 16.151998,10.409003 14.907999,10.591002L14.907999,11.698002 14.146,11.698002 14.146,10.622003C13.445001,10.611002,12.720001,10.420002,12.297002,10.154003L12.587001,9.4400029C13.022001,9.696003 13.651,9.8980026 14.328,9.8980026 15.185999,9.8980026 15.765998,9.4620028 15.765998,8.8540034 15.765998,8.2680035 15.293999,7.9060035 14.4,7.5870035 13.167001,7.1610038 12.406001,6.6710041 12.406001,5.7440041 12.406001,4.8600047 13.119,4.189005 14.231,4.0290052z M2,2.0000002L2,13 26.000001,13 26.000001,2.0000002z M0,0L28.000001,0 28.000001,4.0000031 32.001,4.0000031 32.001,19.000003 4.0010006,19.000003 4.0010006,15 0,15z
|
||||
</sys:String>
|
||||
|
||||
+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; }
|
||||
|
||||
@@ -15,7 +15,6 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Reactive.Subjects;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using LadderSettings = mbts.spark.LadderSettings;
|
||||
@@ -30,13 +29,14 @@ namespace bts.shell {
|
||||
readonly MBOService _mbo;
|
||||
readonly UserSettingsHub _settings_hub;
|
||||
readonly OrdersModel _orders_model;
|
||||
readonly IShellWindowThreadIDProvider _thread_id_provider;
|
||||
IReadOnlyDictionary<uint, SecurityMD> _securities = new Dictionary<uint, SecurityMD>();
|
||||
|
||||
public LadderWindowFactory(
|
||||
IShellModule owner,
|
||||
IBus bus,
|
||||
IWindowManager wm,
|
||||
UserSettingsHub settings_hub,
|
||||
IShellModule owner,
|
||||
IBus bus,
|
||||
IWindowManager wm,
|
||||
UserSettingsHub settings_hub,
|
||||
OrdersModel orders_model,
|
||||
MBOService mbo)
|
||||
|
||||
@@ -46,6 +46,7 @@ namespace bts.shell {
|
||||
_settings_hub = settings_hub;
|
||||
_orders_model = orders_model;
|
||||
_ladder_conf = new LadderConfig(Configuration.get<bts.config.LadderKnobs>(), Configuration.get<bts.config.MultiThreadedWindows>().ladder);
|
||||
_thread_id_provider = ShellWindowThreadIDFactory.create("Ladder", _ladder_conf.ladders_on_threads);
|
||||
_mbo = mbo;
|
||||
_dispose = Disposable.Create(
|
||||
bus.subscribe(SparkTags.SecuritiesTag).Subscribe(s => _securities = s),
|
||||
@@ -88,10 +89,8 @@ namespace bts.shell {
|
||||
return new LadderSaveState { security_key = vm.security.Id, settings = s, settings_id = id };
|
||||
}
|
||||
|
||||
int _ladder_count = 0;
|
||||
IShellWindow create_ladder_window(SecurityMD md, LadderSettings last_value, uint settings_id = 0) {
|
||||
IDisposable active_settings_dialog = null;
|
||||
var thread_id = Interlocked.Increment(ref _ladder_count) % Environment.ProcessorCount;
|
||||
// on_shell_window_vm should behave like an event (do not set to null)
|
||||
Action<ShellWindowVM> on_shell_window_vm = _ => { };
|
||||
ShellWindowVM shell_window_vm = null;
|
||||
@@ -104,7 +103,8 @@ namespace bts.shell {
|
||||
us => ImmutableDictionary<uint, SettingsProfile<LadderSettings>>.Empty.AddRange(us.ladder_profiles.ToDictionary(
|
||||
lp => lp.id,
|
||||
lp => new SettingsProfile<LadderSettings>(lp.id, lp.name, LadderSettings.sanitize(lp.value), lp.security_criteria))),
|
||||
(us, lps) => us.with(ladder_profiles: lps.Values.Select(lp => new LadderSettingsProfile(lp.id, lp.name, lp.value, lp.security_criteria)).ToArray()));
|
||||
(us, lps) => us.with(ladder_profiles: lps.Values.Select(lp => new LadderSettingsProfile(lp.id, lp.name, lp.value, lp.security_criteria)).ToArray()),
|
||||
us => us.next_ladder_profile_id);
|
||||
last_value?.sanitize();
|
||||
/// When settings are adjusted outside of the settings dialog, the vm should raise the <see cref="LadderVM.settings_invalidated"/> event.
|
||||
/// In response to that event, 'needs_resync' is set to true. This causes 'last_value' to be refreshed when the next caller requests the latest settings
|
||||
@@ -263,12 +263,11 @@ namespace bts.shell {
|
||||
Log.Ui.warn($"LadderSettingsDialog error {action_desc}: {error}.");
|
||||
}
|
||||
});
|
||||
string thread = _ladder_conf.ladders_on_threads ? $"Ladder{thread_id:00}" : "";
|
||||
return shell_window = ShellWindow.create(
|
||||
module, md.security.ToString(), _window_type, view,
|
||||
(v, s) => save(_get_state(((LadderView)v).vm, settings_id), s),
|
||||
icon: Icons.Ladder,
|
||||
thread_id: thread,
|
||||
thread_id: _thread_id_provider.next_id(),
|
||||
vm => {
|
||||
shell_window_vm = vm;
|
||||
title_source.connect();
|
||||
@@ -313,4 +312,4 @@ namespace bts.shell {
|
||||
[ProtoMember(3)]
|
||||
public uint settings_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -136,7 +136,7 @@ namespace mbts.spark {
|
||||
foreach (var c in _observable_components)
|
||||
c.PropertyChanged -= on_prop_changed;
|
||||
}
|
||||
AccountTuple? account_factory(KeyValuePair<uint, string> kv) => new AccountTuple(kv.Key, kv.Value);
|
||||
AccountTuple? account_factory(uint key, string name) => new AccountTuple(key, name);
|
||||
AlgoDefinitionVM algo_factory(AlgoData data) => AlgoDefinitionVM.create(_model, data);
|
||||
}
|
||||
|
||||
|
||||
@@ -79,8 +79,8 @@ namespace bts.shell.manual_fills {
|
||||
|
||||
public ManualFillsModel(OrdersModel model)
|
||||
: this(model, model.on_manual_fill_tuples,
|
||||
model.factory.subscribe_accounts().Select(d => MappedDictionary.create(d, kv => new AccountTuple(kv.Key, kv.Value))).Table(u => u.id),
|
||||
model.factory.subscribe_users().Select(d => MappedDictionary.create(d, kv => new UserTuple(kv.Key, kv.Value))).Table(a => a.id)) { }
|
||||
model.factory.subscribe_accounts().Select(d => MappedDictionary.create(d, (k, v) => new AccountTuple(k, v))).Table(u => u.id),
|
||||
model.factory.subscribe_users().Select(d => MappedDictionary.create(d, (k, v) => new UserTuple(k, v))).Table(a => a.id)) { }
|
||||
|
||||
public ManualFillsModel(OrdersModel model, IObservable<TableUpdate<ManualFillTuple, int>> manual_fills, IObservable<TableUpdate<AccountTuple, uint>> accounts, IObservable<TableUpdate<UserTuple, uint>> users) {
|
||||
_model = model;
|
||||
|
||||
@@ -73,7 +73,7 @@ namespace bts.shell.manual_fills {
|
||||
// The security desc lookup could be better optimized, but the speed at which fills are imported is probably not a paramount concern.
|
||||
public static IEnumerable<ParsedManualFill> parse_rows(IEnumerable<string> rows, IReadOnlyDictionary<uint, SecurityImmutable> security_map, IReadOnlyDictionary<uint, string> account_map, IReadOnlyDictionary<uint, string> user_map) =>
|
||||
parse_rows(rows,
|
||||
reverse_lookup(MappedDictionary.create(security_map, kv => kv.Value.ToString())),
|
||||
reverse_lookup(MappedDictionary.create(security_map, s => s.ToString())),
|
||||
reverse_lookup(account_map),
|
||||
reverse_lookup(user_map));
|
||||
|
||||
|
||||
@@ -157,7 +157,7 @@ namespace bts.shell.manual_fills {
|
||||
var lines = File.ReadAllLines(file_name);
|
||||
var parsed_fills = ManualFillsParser.parse_rows(
|
||||
lines,
|
||||
MappedDictionary.create(security_map, kv => kv.Value.security),
|
||||
MappedDictionary.create(security_map, smd => smd.security),
|
||||
accounts.ToDictionary(a => a.Value.id, a => a.Value.name),
|
||||
users.ToDictionary(u => u.Value.id, u => u.Value.name)
|
||||
).ToList();
|
||||
|
||||
@@ -327,6 +327,18 @@ namespace mbts.spark {
|
||||
|
||||
public event Action filter_changed;
|
||||
|
||||
public static IEnumerable<ColumnBase> editable_columns {
|
||||
get {
|
||||
yield return new AttachDetachColumn(typeof(MiniOrderBookView));
|
||||
yield return new CommitColumn();
|
||||
yield return new EditablePriceColumn();
|
||||
yield return new EditableQtyColumn();
|
||||
yield return new UpdateColumn(typeof(MiniOrderBookView));
|
||||
yield return new CancelColumn(nameof(OrderBookRowVM.cancel_command));
|
||||
yield return new CheckColumn(nameof(OrderBookRowVM.is_selected));
|
||||
}
|
||||
}
|
||||
|
||||
public SecurityColumn security_column { get; }
|
||||
public SimpleTextColumn<string[]> symbol_column { get; }
|
||||
public SimpleTextColumn<TimeFilterTuple> time_created_column { get; }
|
||||
@@ -731,8 +743,11 @@ namespace mbts.spark {
|
||||
Width = 30;
|
||||
_checked_binding = new Binding(checked_property);
|
||||
|
||||
var header_cb = new CheckBox();
|
||||
header_cb.ToolTip = "Select / Unselect All";
|
||||
var header_cb = new CheckBox {
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
ToolTip = "Select / Unselect All"
|
||||
};
|
||||
header_cb.SetBinding(CheckBox.IsCheckedProperty, new Binding(nameof(OrderBookVM.all_rows_selected)));
|
||||
this.Header = header_cb;
|
||||
}
|
||||
@@ -1164,6 +1179,7 @@ namespace mbts.spark {
|
||||
|
||||
public override FrameworkElement view_factory() {
|
||||
var price_up_down = new PriceEntryCommitControl();
|
||||
price_up_down.SetBinding(PriceEntryCommitControl.RealPriceProperty, new Binding(nameof(OrderBookRowVM.price)));
|
||||
price_up_down.SetBinding(PriceEntryCommitControl.SourceValueProperty, nameof(OrderBookRowVM.prospective_price));
|
||||
price_up_down.SetBinding(PriceEntryCommitControl.SourceContractSpecProperty, nameof(OrderBookRowVM.contract_spec));
|
||||
price_up_down.SetBinding(PriceEntryCommitControl.OnCommitProperty, nameof(OrderBookRowVM.modify_order_quantity_andor_price));
|
||||
|
||||
@@ -69,10 +69,15 @@ namespace mbts.spark {
|
||||
_detachable_selected_orders = new HashSet<uint>();
|
||||
FilterData _cached_filter_data;
|
||||
|
||||
public OrderBookVM(
|
||||
IBus bus, INotificationService notification_service, OrdersModel model, IWindowSettingsManager<OrderBookGridSettings> settings_manager, bool rows_are_initially_expanded = false)
|
||||
: this(bus, notification_service, model, OrdersModel.piq_enabled ? model.on_order_tuples_with_root_and_piq : model.on_order_tuples_with_root, settings_manager, rows_are_initially_expanded) { }
|
||||
|
||||
public OrderBookVM(
|
||||
IBus bus,
|
||||
INotificationService notification_service,
|
||||
OrdersModel model,
|
||||
IObservable<TableUpdate<OrderSecurityRootPair, uint>> orders_source,
|
||||
IWindowSettingsManager<OrderBookGridSettings> settings_manager,
|
||||
bool rows_are_initially_expanded = false) {
|
||||
|
||||
@@ -157,7 +162,6 @@ namespace mbts.spark {
|
||||
begin_attaching += on_begin_attaching;
|
||||
end_attaching += on_end_attaching;
|
||||
// switch between orders and/or orders w/ piq
|
||||
var orders_source = OrdersModel.piq_enabled ? model.on_order_tuples_with_root_and_piq : model.on_order_tuples_with_root;
|
||||
concat_disposable(
|
||||
connect_order_mod_vm(),
|
||||
_can_attach_selected_orders.observe_current().Subscribe(send_attached_orders_async_cmd, begin_attach_cmd),
|
||||
@@ -761,7 +765,7 @@ namespace mbts.spark {
|
||||
_size_param = new AlgoParamVM<uint?>("size", "Size", owner._selected_params_mon.size) {
|
||||
permissions = _size_permissions & _param_permissions
|
||||
};
|
||||
var price_param = new AlgoParamVM<double>("price", "Price", owner._selected_params_mon.price, spec?.tick_equality_comparer() ?? MathUtils.epsilon_comparer) {
|
||||
var price_param = new AlgoParamVM<double>("price", "Price", owner._selected_params_mon.price, spec?.tick_equality_comparer() ?? MathUtils.epsilon_eq_comparer) {
|
||||
permissions = _price_permissions & _param_permissions
|
||||
};
|
||||
_price_param = price_param;
|
||||
|
||||
@@ -54,7 +54,7 @@ namespace bts.shell.orderbook {
|
||||
pvm.selected_predicate_type = _corresponding_predicate_type(EnumHelper.parse_or_default<Columns>(col_id));
|
||||
return pvm;
|
||||
},
|
||||
(gen, anims) => new OrderBookGridSettings(gen, anims.Select(a => new OrderRecordTuplePredicateAnimations(a.predicate_data, a.cell_animations, a.row_animations, a.sound_notifications))) { algo_tree_display_mode = _algo_tree_display_mode },
|
||||
(gen, anims) => new OrderBookGridSettings(gen, anims.empty_if_null().Select(a => new OrderRecordTuplePredicateAnimations(a.predicate_data, a.cell_animations, a.row_animations, a.sound_notifications))) { algo_tree_display_mode = _algo_tree_display_mode },
|
||||
p => p.create_predicate(notification_service.data_cache),
|
||||
p => p.create_notification_predicate(notification_service.data_cache),
|
||||
notification_service == null ? new Func<IPredicate<OrderSecurity>, SoundNotification, IDisposable>((p, sn) => Disposable.empty) : notification_service.link_to_order,
|
||||
|
||||
@@ -1,168 +1,168 @@
|
||||
using bts.api;
|
||||
using bts.api.orderman;
|
||||
using bts.spark;
|
||||
using mbts.persist;
|
||||
using mbts.spark;
|
||||
using mbts.spark.filters;
|
||||
using mbts.ui;
|
||||
using mbts.ui.settings;
|
||||
using mbts.ui.spark;
|
||||
using ProtoBuf;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using static mbts.spark.PAndLView;
|
||||
using static mbts.ui.controls.DIYGrid;
|
||||
|
||||
namespace bts.shell {
|
||||
internal class PAndLWindowFactory : ShellWindowFactory<PAndLWindowFactory.PAndLStartupData> {
|
||||
readonly IBus _bus;
|
||||
readonly OrdersModel _orders_model;
|
||||
readonly UserSettingsHub _settings_hub;
|
||||
|
||||
public PAndLWindowFactory(IShellModule owner, IBus bus, UserSettingsHub settings_hub, OrdersModel orders_model) : base(owner, "SparkPAndL") {
|
||||
_bus = bus;
|
||||
_orders_model = orders_model;
|
||||
_settings_hub = settings_hub;
|
||||
_dispose = bus.subscribe(SparkTags.ShowPAndLTag).OnDispatcher().Subscribe(show);
|
||||
}
|
||||
|
||||
public static string col_id(Columns col) => col.ToString();
|
||||
|
||||
public override Task<IShellWindow> load(PAndLStartupData state) {
|
||||
return Task.FromResult(_create(state));
|
||||
}
|
||||
|
||||
async void show(PAndLStartupData startup_data) {
|
||||
var w = await load(startup_data);
|
||||
_bus.post(SparkTags.ShowWindow, w);
|
||||
}
|
||||
|
||||
IShellWindow _create(PAndLStartupData state) {
|
||||
var settings_manager = new WindowSettingsManager<GridSettings>(_settings_hub,
|
||||
state.grid,
|
||||
get: us => us.pnl_grid,
|
||||
set: (us, gs) => us.with(provided_pnl_gridsettings: gs));
|
||||
return new PAndLWindow(_orders_model, this, _bus, state, SparkCommands.ShowPAndL.icon, settings_manager);
|
||||
}
|
||||
|
||||
[ProtoContract]
|
||||
public class PAndLStartupData {
|
||||
|
||||
/// <summary>
|
||||
/// Parameterless constructor for protobuf
|
||||
/// </summary>
|
||||
public PAndLStartupData() { }
|
||||
|
||||
public PAndLStartupData(List<SortingCriteria> sorting_criteria, FilterStates filters, pnl_calc_type calc_type, GridSettings grid = default) {
|
||||
_sort_criteria = sorting_criteria;
|
||||
this.filters = filters;
|
||||
this.calc_type = calc_type;
|
||||
this.grid = grid;
|
||||
}
|
||||
|
||||
public static GridSettings DefaultGridSettings => GridSettings.Default.with(DefaultColumns.all, DefaultColumns.default_sorting_criterion);
|
||||
|
||||
public IReadOnlyList<SortingCriteria> sorting_criterion => _sort_criteria ?? (_sort_criteria = DefaultColumns.default_sorting_criterion.ToList());
|
||||
public static PAndLStartupData Default() =>
|
||||
new PAndLStartupData(null, new FilterStates(), pnl_calc_type.midpoint);
|
||||
|
||||
|
||||
[ProtoMember(5)]
|
||||
List<SortingCriteria> _sort_criteria;
|
||||
[ProtoMember(10)]
|
||||
public FilterStates filters;
|
||||
[ProtoMember(15)]
|
||||
public pnl_calc_type calc_type;
|
||||
//[ProtoMember(20)]
|
||||
// ProtoMember 20 cannot be used further
|
||||
[ProtoMember(25)]
|
||||
public GridSettings grid;
|
||||
|
||||
static class DefaultColumns {
|
||||
|
||||
public static GridColumnSettings account => new GridColumnSettings(col_id(Columns.Account), DefaultColumnHeaders.Account, width: DefaultColumnWidths.Account);
|
||||
public static GridColumnSettings exchange => new GridColumnSettings(col_id(Columns.Exchange), DefaultColumnHeaders.Exchange, false, width: DefaultColumnWidths.Exchange);
|
||||
public static GridColumnSettings symbol => new GridColumnSettings(col_id(Columns.Symbol), DefaultColumnHeaders.Symbol, width: DefaultColumnWidths.Symbol);
|
||||
public static GridColumnSettings security => new GridColumnSettings(col_id(Columns.Security), DefaultColumnHeaders.Security, width: DefaultColumnWidths.Security);
|
||||
public static GridColumnSettings security_type => new GridColumnSettings(col_id(Columns.SecurityType), DefaultColumnHeaders.SecurityType, width: DefaultColumnWidths.SecurityType);
|
||||
public static GridColumnSettings start_net_position => new GridColumnSettings(col_id(Columns.StartNetPos), DefaultColumnHeaders.StartNetPos, width: DefaultColumnWidths.Start_Net_Pos, horizontal_alignment: System.Windows.HorizontalAlignment.Right);
|
||||
public static GridColumnSettings start_pnl => new GridColumnSettings(col_id(Columns.StartPNL), DefaultColumnHeaders.StartPnL, false, width: DefaultColumnWidths.PnL_Generic, horizontal_alignment: System.Windows.HorizontalAlignment.Right);
|
||||
public static GridColumnSettings day_long_pnl => new GridColumnSettings(col_id(Columns.DayLong), DefaultColumnHeaders.DayLong, width: DefaultColumnWidths.PnL_Generic, horizontal_alignment: System.Windows.HorizontalAlignment.Right);
|
||||
public static GridColumnSettings day_short_pnl => new GridColumnSettings(col_id(Columns.DayShort), DefaultColumnHeaders.DayShort, width: DefaultColumnWidths.PnL_Generic, horizontal_alignment: System.Windows.HorizontalAlignment.Right);
|
||||
public static GridColumnSettings day_pnl => new GridColumnSettings(col_id(Columns.DayPNL), DefaultColumnHeaders.DayPnL, width: DefaultColumnWidths.PnL_Generic, horizontal_alignment: System.Windows.HorizontalAlignment.Right);
|
||||
public static GridColumnSettings total_net_pos => new GridColumnSettings(col_id(Columns.TotalNetPos), DefaultColumnHeaders.TotalNetPos, false, width: DefaultColumnWidths.Total_Net_Pos, horizontal_alignment: System.Windows.HorizontalAlignment.Right);
|
||||
public static GridColumnSettings total_pnl => new GridColumnSettings(col_id(Columns.TotalPNL), DefaultColumnHeaders.TotalPnL, false, width: DefaultColumnWidths.PnL_Generic, horizontal_alignment: System.Windows.HorizontalAlignment.Right);
|
||||
public static GridColumnSettings pnl_price => new GridColumnSettings(col_id(Columns.PNLPrice), DefaultColumnHeaders.PnLPrice, width: DefaultColumnWidths.PnL_Generic, horizontal_alignment: System.Windows.HorizontalAlignment.Right);
|
||||
|
||||
public static IEnumerable<SortingCriteria> default_sorting_criterion {
|
||||
get {
|
||||
yield return new SortingCriteria(account.id, true);
|
||||
yield return new SortingCriteria(exchange.id, true);
|
||||
yield return new SortingCriteria(symbol.id, true);
|
||||
yield return new SortingCriteria(security.id, true);
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<GridColumnSettings> all {
|
||||
get {
|
||||
yield return account;
|
||||
yield return exchange;
|
||||
yield return symbol;
|
||||
yield return security;
|
||||
yield return security_type;
|
||||
yield return start_net_position;
|
||||
yield return start_pnl;
|
||||
yield return day_long_pnl;
|
||||
yield return day_short_pnl;
|
||||
yield return day_pnl;
|
||||
yield return total_net_pos;
|
||||
yield return total_pnl;
|
||||
yield return pnl_price;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
[ProtoContract]
|
||||
public class FilterStates {
|
||||
[ProtoMember(1)]
|
||||
public string account_regex;
|
||||
[ProtoMember(2)]
|
||||
public string exchange_regex;
|
||||
[ProtoMember(3)]
|
||||
public string symbol_regex;
|
||||
[ProtoMember(4)]
|
||||
public string security_regex;
|
||||
[ProtoMember(5)]
|
||||
public IntMinMaxState start_net_pos;
|
||||
[ProtoMember(6)]
|
||||
public DoubleMinMaxState start_pnl;
|
||||
[ProtoMember(7)]
|
||||
public UIntMinMaxState day_long_pos;
|
||||
[ProtoMember(8)]
|
||||
public UIntMinMaxState day_short_pos;
|
||||
[ProtoMember(9)]
|
||||
public DoubleMinMaxState day_pnl;
|
||||
[ProtoMember(10)]
|
||||
public IntMinMaxState total_net_pos;
|
||||
[ProtoMember(11)]
|
||||
public DoubleMinMaxState total_pnl;
|
||||
[ProtoMember(12)]
|
||||
public DoubleMinMaxState pnl_price;
|
||||
|
||||
// [ProtoMember(13)]
|
||||
// ProtoMember 13 cannot be used further
|
||||
[ProtoMember(14)]
|
||||
public SecurityType[] security_types;
|
||||
[ProtoMember(15)]
|
||||
public bool[] has_day_trades;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
using bts.api;
|
||||
using bts.api.orderman;
|
||||
using bts.spark;
|
||||
using mbts.persist;
|
||||
using mbts.spark;
|
||||
using mbts.spark.filters;
|
||||
using mbts.ui;
|
||||
using mbts.ui.settings;
|
||||
using mbts.ui.spark;
|
||||
using ProtoBuf;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using static mbts.spark.PAndLView;
|
||||
using static mbts.ui.controls.DIYGrid;
|
||||
|
||||
namespace bts.shell {
|
||||
internal class PAndLWindowFactory : ShellWindowFactory<PAndLWindowFactory.PAndLStartupData> {
|
||||
readonly IBus _bus;
|
||||
readonly OrdersModel _orders_model;
|
||||
readonly UserSettingsHub _settings_hub;
|
||||
|
||||
public PAndLWindowFactory(IShellModule owner, IBus bus, UserSettingsHub settings_hub, OrdersModel orders_model) : base(owner, "SparkPAndL") {
|
||||
_bus = bus;
|
||||
_orders_model = orders_model;
|
||||
_settings_hub = settings_hub;
|
||||
_dispose = bus.subscribe(SparkTags.ShowPAndLTag).OnDispatcher().Subscribe(show);
|
||||
}
|
||||
|
||||
public static string col_id(Columns col) => col.ToString();
|
||||
|
||||
public override Task<IShellWindow> load(PAndLStartupData state) {
|
||||
return Task.FromResult(_create(state));
|
||||
}
|
||||
|
||||
async void show(PAndLStartupData startup_data) {
|
||||
var w = await load(startup_data);
|
||||
_bus.post(SparkTags.ShowWindow, w);
|
||||
}
|
||||
|
||||
IShellWindow _create(PAndLStartupData state) {
|
||||
var settings_manager = new WindowSettingsManager<GridSettings>(_settings_hub,
|
||||
state.grid,
|
||||
get: us => us.pnl_grid,
|
||||
set: (us, gs) => us.with(provided_pnl_gridsettings: gs));
|
||||
return new PAndLWindow(_orders_model, this, _bus, state, SparkCommands.ShowPAndL.icon, settings_manager);
|
||||
}
|
||||
|
||||
[ProtoContract]
|
||||
public class PAndLStartupData {
|
||||
|
||||
/// <summary>
|
||||
/// Parameterless constructor for protobuf
|
||||
/// </summary>
|
||||
public PAndLStartupData() { }
|
||||
|
||||
public PAndLStartupData(List<SortingCriteria> sorting_criteria, FilterStates filters, pnl_calc_type calc_type, GridSettings grid = default) {
|
||||
_sort_criteria = sorting_criteria;
|
||||
this.filters = filters;
|
||||
this.calc_type = calc_type;
|
||||
this.grid = grid;
|
||||
}
|
||||
|
||||
public static GridSettings DefaultGridSettings => GridSettings.Default.with(DefaultColumns.all, DefaultColumns.default_sorting_criterion);
|
||||
|
||||
public IReadOnlyList<SortingCriteria> sorting_criterion => _sort_criteria ?? (_sort_criteria = DefaultColumns.default_sorting_criterion.ToList());
|
||||
public static PAndLStartupData Default() =>
|
||||
new PAndLStartupData(null, new FilterStates(), pnl_calc_type.midpoint);
|
||||
|
||||
|
||||
[ProtoMember(5)]
|
||||
List<SortingCriteria> _sort_criteria;
|
||||
[ProtoMember(10)]
|
||||
public FilterStates filters;
|
||||
[ProtoMember(15)]
|
||||
public pnl_calc_type calc_type;
|
||||
//[ProtoMember(20)]
|
||||
// ProtoMember 20 cannot be used further
|
||||
[ProtoMember(25)]
|
||||
public GridSettings grid;
|
||||
|
||||
static class DefaultColumns {
|
||||
|
||||
public static GridColumnSettings account => new GridColumnSettings(col_id(Columns.Account), DefaultColumnHeaders.Account, width: DefaultColumnWidths.Account);
|
||||
public static GridColumnSettings exchange => new GridColumnSettings(col_id(Columns.Exchange), DefaultColumnHeaders.Exchange, false, width: DefaultColumnWidths.Exchange);
|
||||
public static GridColumnSettings symbol => new GridColumnSettings(col_id(Columns.Symbol), DefaultColumnHeaders.Symbol, width: DefaultColumnWidths.Symbol);
|
||||
public static GridColumnSettings security => new GridColumnSettings(col_id(Columns.Security), DefaultColumnHeaders.Security, width: DefaultColumnWidths.Security);
|
||||
public static GridColumnSettings security_type => new GridColumnSettings(col_id(Columns.SecurityType), DefaultColumnHeaders.SecurityType, width: DefaultColumnWidths.SecurityType);
|
||||
public static GridColumnSettings start_net_position => new GridColumnSettings(col_id(Columns.StartNetPos), DefaultColumnHeaders.StartNetPos, width: DefaultColumnWidths.Start_Net_Pos, horizontal_alignment: System.Windows.HorizontalAlignment.Right);
|
||||
public static GridColumnSettings start_pnl => new GridColumnSettings(col_id(Columns.StartPNL), DefaultColumnHeaders.StartPnL, false, width: DefaultColumnWidths.PnL_Generic, horizontal_alignment: System.Windows.HorizontalAlignment.Right);
|
||||
public static GridColumnSettings day_long_pnl => new GridColumnSettings(col_id(Columns.DayLong), DefaultColumnHeaders.DayLong, width: DefaultColumnWidths.PnL_Generic, horizontal_alignment: System.Windows.HorizontalAlignment.Right);
|
||||
public static GridColumnSettings day_short_pnl => new GridColumnSettings(col_id(Columns.DayShort), DefaultColumnHeaders.DayShort, width: DefaultColumnWidths.PnL_Generic, horizontal_alignment: System.Windows.HorizontalAlignment.Right);
|
||||
public static GridColumnSettings day_pnl => new GridColumnSettings(col_id(Columns.DayPNL), DefaultColumnHeaders.DayPnL, width: DefaultColumnWidths.PnL_Generic, horizontal_alignment: System.Windows.HorizontalAlignment.Right);
|
||||
public static GridColumnSettings total_net_pos => new GridColumnSettings(col_id(Columns.TotalNetPos), DefaultColumnHeaders.TotalNetPos, false, width: DefaultColumnWidths.Total_Net_Pos, horizontal_alignment: System.Windows.HorizontalAlignment.Right);
|
||||
public static GridColumnSettings total_pnl => new GridColumnSettings(col_id(Columns.TotalPNL), DefaultColumnHeaders.TotalPnL, false, width: DefaultColumnWidths.PnL_Generic, horizontal_alignment: System.Windows.HorizontalAlignment.Right);
|
||||
public static GridColumnSettings pnl_price => new GridColumnSettings(col_id(Columns.PNLPrice), DefaultColumnHeaders.PnLPrice, width: DefaultColumnWidths.PnL_Generic, horizontal_alignment: System.Windows.HorizontalAlignment.Right);
|
||||
|
||||
public static IEnumerable<SortingCriteria> default_sorting_criterion {
|
||||
get {
|
||||
yield return new SortingCriteria(account.id, true);
|
||||
yield return new SortingCriteria(exchange.id, true);
|
||||
yield return new SortingCriteria(symbol.id, true);
|
||||
yield return new SortingCriteria(security.id, true);
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<GridColumnSettings> all {
|
||||
get {
|
||||
yield return account;
|
||||
yield return exchange;
|
||||
yield return symbol;
|
||||
yield return security;
|
||||
yield return security_type;
|
||||
yield return start_net_position;
|
||||
yield return start_pnl;
|
||||
yield return day_long_pnl;
|
||||
yield return day_short_pnl;
|
||||
yield return day_pnl;
|
||||
yield return total_net_pos;
|
||||
yield return total_pnl;
|
||||
yield return pnl_price;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
[ProtoContract]
|
||||
public class FilterStates {
|
||||
[ProtoMember(1)]
|
||||
public string account_regex;
|
||||
[ProtoMember(2)]
|
||||
public string exchange_regex;
|
||||
[ProtoMember(3)]
|
||||
public string symbol_regex;
|
||||
[ProtoMember(4)]
|
||||
public string security_regex;
|
||||
[ProtoMember(5)]
|
||||
public IntMinMaxState start_net_pos;
|
||||
[ProtoMember(6)]
|
||||
public DoubleMinMaxState start_pnl;
|
||||
[ProtoMember(7)]
|
||||
public UIntMinMaxState day_long_pos;
|
||||
[ProtoMember(8)]
|
||||
public UIntMinMaxState day_short_pos;
|
||||
[ProtoMember(9)]
|
||||
public DoubleMinMaxState day_pnl;
|
||||
[ProtoMember(10)]
|
||||
public IntMinMaxState total_net_pos;
|
||||
[ProtoMember(11)]
|
||||
public DoubleMinMaxState total_pnl;
|
||||
[ProtoMember(12)]
|
||||
public DoubleMinMaxState pnl_price;
|
||||
|
||||
// [ProtoMember(13)]
|
||||
// ProtoMember 13 cannot be used further
|
||||
[ProtoMember(14)]
|
||||
public SecurityType[] security_types;
|
||||
[ProtoMember(15)]
|
||||
public bool[] has_day_trades;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,541 @@
|
||||
using bts.api;
|
||||
using bts.api.orderman;
|
||||
using bts.api.position;
|
||||
using bts.utils;
|
||||
using mbts;
|
||||
using mbts.spark;
|
||||
using mbts.spark.utilities;
|
||||
using mbts.ui.controls;
|
||||
using mbts.utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
|
||||
namespace bts.shell.pnl {
|
||||
public enum PNLCalculationType {
|
||||
None,
|
||||
Bid,
|
||||
Ask,
|
||||
Opposite,
|
||||
Same,
|
||||
Waterfall,
|
||||
Last,
|
||||
Mid,
|
||||
Settle
|
||||
}
|
||||
|
||||
readonly struct PositionPriceSize : IEquatable<PositionPriceSize> {
|
||||
public static readonly PositionPriceSize empty = new PositionPriceSize(0, 0, 0, 0);
|
||||
public readonly double long_pos, short_pos, long_avg, short_avg;
|
||||
public double net_pos => long_pos - short_pos;
|
||||
public bool has_position => long_pos > 0 || short_pos > 0;
|
||||
|
||||
public PositionPriceSize(double long_pos, double long_avg, double short_pos, double short_avg) {
|
||||
this.long_pos = long_pos;
|
||||
this.long_avg = long_avg;
|
||||
this.short_pos = short_pos;
|
||||
this.short_avg = short_avg;
|
||||
}
|
||||
|
||||
public bool Equals(PositionPriceSize other) =>
|
||||
MathUtils.epsilon_equals(long_pos, other.long_pos) &&
|
||||
MathUtils.epsilon_equals(long_avg, other.long_avg) &&
|
||||
MathUtils.epsilon_equals(short_pos, other.short_pos) &&
|
||||
MathUtils.epsilon_equals(short_avg, other.short_avg);
|
||||
|
||||
public override bool Equals(object obj) =>
|
||||
obj is PositionPriceSize other && Equals(other);
|
||||
|
||||
public override int GetHashCode() =>
|
||||
EqualityComparer.aggregate_hash(
|
||||
long_pos.GetHashCode(),
|
||||
long_avg.GetHashCode(),
|
||||
short_pos.GetHashCode(),
|
||||
short_avg.GetHashCode());
|
||||
|
||||
public static bool operator ==(PositionPriceSize a, PositionPriceSize b) => a.Equals(b);
|
||||
public static bool operator !=(PositionPriceSize a, PositionPriceSize b) => !a.Equals(b);
|
||||
}
|
||||
|
||||
readonly struct PNLRowKey : IEquatable<PNLRowKey>, IComparable<PNLRowKey>, IComparable {
|
||||
public static readonly PNLRowKey empty = new PNLRowKey();
|
||||
static readonly IComparer<PNLRowKey> _comparer_inst = new ComparerBuilder<PNLRowKey>(
|
||||
new[] {
|
||||
Comparer<uint>.Default.wrap<uint, PNLRowKey>(r => r.account ?? 0),
|
||||
Comparer<uint>.Default.wrap<uint, PNLRowKey>(r => r.security ?? 0)
|
||||
}).to_comparer();
|
||||
|
||||
public readonly Venue? exchange;
|
||||
public readonly Security.ContractOneofCase? security_type;
|
||||
public readonly string symbol;
|
||||
public readonly PNLKey data_key;
|
||||
public uint? security => data_key.security_id;
|
||||
public uint? account => data_key.account_id;
|
||||
|
||||
public PNLRowKey(
|
||||
Venue? exchange = null,
|
||||
Security.ContractOneofCase? security_type = null,
|
||||
string symbol = null,
|
||||
uint? security = null,
|
||||
uint? account = null) {
|
||||
//
|
||||
this.exchange = exchange;
|
||||
this.security_type = security_type;
|
||||
this.symbol = symbol;
|
||||
data_key = new PNLKey(account.Value, security.Value);
|
||||
}
|
||||
|
||||
int IComparable.CompareTo(object obj) =>
|
||||
obj is PNLRowKey other ? CompareTo(other) : 1;
|
||||
|
||||
public int CompareTo(PNLRowKey other) =>
|
||||
_comparer_inst.Compare(this, other);
|
||||
|
||||
public bool Equals(PNLRowKey other) =>
|
||||
exchange == other.exchange &&
|
||||
security_type == other.security_type &&
|
||||
symbol == other.symbol &&
|
||||
security == other.security &&
|
||||
account == other.account;
|
||||
|
||||
public override bool Equals(object obj) =>
|
||||
obj is PNLRowKey other && Equals(other);
|
||||
|
||||
public override int GetHashCode() =>
|
||||
EqualityComparer.aggregate_hash(
|
||||
exchange?.GetHashCode() ?? 0,
|
||||
security_type?.GetHashCode() ?? 0,
|
||||
symbol?.GetHashCode() ?? 0,
|
||||
security?.GetHashCode() ?? 0,
|
||||
account?.GetHashCode() ?? 0);
|
||||
|
||||
public static bool operator ==(PNLRowKey a, PNLRowKey b) => a.Equals(b);
|
||||
public static bool operator !=(PNLRowKey a, PNLRowKey b) => !a.Equals(b);
|
||||
}
|
||||
|
||||
readonly struct WorkingOrderData {
|
||||
public readonly WorkingOrderSideData long_data, short_data;
|
||||
|
||||
WorkingOrderData(WorkingOrderSideData long_data, WorkingOrderSideData short_data) {
|
||||
this.long_data = long_data;
|
||||
this.short_data = short_data;
|
||||
}
|
||||
|
||||
public static WorkingOrderData Empty =>
|
||||
new WorkingOrderData(WorkingOrderSideData.EmptyLong, WorkingOrderSideData.EmptyShort);
|
||||
|
||||
public double top_bid =>
|
||||
long_data.data.Count > 0 ?
|
||||
long_data.data.First().Key :
|
||||
double.NaN;
|
||||
|
||||
public double top_ask =>
|
||||
short_data.data.Count > 0 ?
|
||||
short_data.data.First().Key :
|
||||
double.NaN;
|
||||
|
||||
public PositionPriceSize summary =>
|
||||
new PositionPriceSize(long_data.working_total, long_data.avg_price, short_data.working_total, short_data.avg_price);
|
||||
|
||||
public static WorkingOrderData Create(IEnumerable<OrderRecordImmutable> orders) =>
|
||||
orders.Aggregate(Empty, (seed, order) => seed.Add(order));
|
||||
|
||||
public WorkingOrderData Add(OrderRecordImmutable order) {
|
||||
if (order.working_quantity() < 0)
|
||||
return new WorkingOrderData(long_data, short_data);
|
||||
switch (order.side()) {
|
||||
case Side.Buy: return new WorkingOrderData(long_data.Add(order), short_data);
|
||||
case Side.Sell: return new WorkingOrderData(long_data, short_data.Add(order));
|
||||
default: throw ExceptionBuilder.Argument.unexpected_case(order.side(), "order.side");
|
||||
}
|
||||
}
|
||||
|
||||
public WorkingOrderData Remove(OrderRecordImmutable order) {
|
||||
if (order.working_quantity() < 0)
|
||||
return new WorkingOrderData(long_data, short_data);
|
||||
switch (order.side()) {
|
||||
case Side.Buy: return new WorkingOrderData(long_data.Remove(order.Key, order.price()), short_data);
|
||||
case Side.Sell: return new WorkingOrderData(long_data, short_data.Remove(order.Key, order.price()));
|
||||
default: throw ExceptionBuilder.Argument.unexpected_case(order.side(), "order.side");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
readonly struct WorkingOrderSideData {
|
||||
public readonly double avg_price;
|
||||
public readonly ImmutableSortedDictionary<double, ImmutableDictionary<uint, OrderRecordImmutable>> data;
|
||||
public readonly int working_total;
|
||||
|
||||
WorkingOrderSideData(double avg_price, int working_total, ImmutableSortedDictionary<double, ImmutableDictionary<uint, OrderRecordImmutable>> data) {
|
||||
this.avg_price = avg_price;
|
||||
this.working_total = working_total;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public static WorkingOrderSideData EmptyLong => new WorkingOrderSideData(double.NaN, 0, ImmutableSortedDictionary.Create<double, ImmutableDictionary<uint, OrderRecordImmutable>>(new ReverseComparer<double>(OrderDataHelper.PriceComparer)));
|
||||
public static WorkingOrderSideData EmptyShort => new WorkingOrderSideData(double.NaN, 0, ImmutableSortedDictionary.Create<double, ImmutableDictionary<uint, OrderRecordImmutable>>(OrderDataHelper.PriceComparer));
|
||||
|
||||
public double top_price =>
|
||||
data.Count > 0 ?
|
||||
data.First().Key :
|
||||
double.NaN;
|
||||
|
||||
public int top_volume =>
|
||||
data.Count > 0 ?
|
||||
data.Values.First().Values.Sum(o => o.working_quantity()) :
|
||||
0;
|
||||
|
||||
public WorkingOrderSideData Add(OrderRecordImmutable order_record) {
|
||||
uint order = order_record.Key;
|
||||
int working_qty = order_record.working_quantity();
|
||||
double price = order_record.price();
|
||||
if (working_qty <= 0)
|
||||
throw new ArgumentException(nameof(working_qty));
|
||||
if (working_total == 0) {
|
||||
return new WorkingOrderSideData(price, working_qty, data.Add(price, ImmutableDictionary<uint, OrderRecordImmutable>.Empty.Add(order, order_record)));
|
||||
} else return new WorkingOrderSideData(
|
||||
avg_price + ((price - avg_price) * ((double)working_qty / (working_total + working_qty))),
|
||||
working_total + working_qty,
|
||||
data.SetItem(price, data.TryGetValue(price, out var curr) ?
|
||||
curr.Add(order, order_record) :
|
||||
ImmutableDictionary<uint, OrderRecordImmutable>.Empty.Add(order, order_record)));
|
||||
}
|
||||
|
||||
public WorkingOrderSideData Remove(uint order, double price) {
|
||||
var tmp_data = data;
|
||||
if (!tmp_data.TryGetValue(price, out var wdata))
|
||||
throw new KeyNotFoundException(nameof(price));
|
||||
if (!wdata.TryGetValue(order, out var worder))
|
||||
throw new KeyNotFoundException(nameof(order));
|
||||
var wqty = worder.working_quantity();
|
||||
wdata = wdata.Remove(order);
|
||||
if (wdata.Count == 0)
|
||||
tmp_data = tmp_data.Remove(price);
|
||||
if (tmp_data.Count == 0)
|
||||
return new WorkingOrderSideData(double.NaN, 0, ImmutableSortedDictionary.Create<double, ImmutableDictionary<uint, OrderRecordImmutable>>(data.KeyComparer));
|
||||
return new WorkingOrderSideData(
|
||||
avg_price - ((price - avg_price) * (wqty / (working_total - wqty))),
|
||||
working_total - wqty,
|
||||
tmp_data);
|
||||
}
|
||||
|
||||
public override string ToString() {
|
||||
if (data.Count > 0) {
|
||||
var best_order = data.Values.First().Values.First();
|
||||
return OrderUtils.order_collection_summary(best_order.price(), avg_price, best_order.working_quantity(), working_total, SecurityDataHelper.Defaults.ContractSpecificationImmutable);
|
||||
}
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
readonly struct PNLRowData : IEquatable<PNLRowData> {
|
||||
public static readonly Func<PNLRowData, PNLKey> key_fn = d => d.key;
|
||||
public static IComparer<PNLRowData> key_comparer = Comparer<PNLKey>.Default.wrap<PNLKey, PNLRowData>(d => d.key);
|
||||
|
||||
static readonly WaterfallPriceFlags[] _waterfall_preferences = new[] {
|
||||
WaterfallPriceFlags.LastInsideSpread,
|
||||
WaterfallPriceFlags.Mid,
|
||||
WaterfallPriceFlags.LastGTEBid,
|
||||
WaterfallPriceFlags.Bid,
|
||||
WaterfallPriceFlags.LastLTEAsk,
|
||||
WaterfallPriceFlags.Ask,
|
||||
WaterfallPriceFlags.Last,
|
||||
WaterfallPriceFlags.Settle
|
||||
};
|
||||
|
||||
public readonly PNLKey key;
|
||||
public readonly SecurityImmutable? security;
|
||||
public readonly string account_name;
|
||||
public readonly PNLCalculationType calc_type;
|
||||
public readonly PositionPriceSize day, sod;
|
||||
public readonly WorkingOrderData working;
|
||||
|
||||
public readonly double
|
||||
cpp,
|
||||
bid_price,
|
||||
ask_price,
|
||||
last_price,
|
||||
settlement_price;
|
||||
|
||||
public PNLRowData(
|
||||
PNLKey key,
|
||||
double cpp = 0,
|
||||
double bid_price = 0,
|
||||
double ask_price = 0,
|
||||
double last_price = 0,
|
||||
double settlement_price = 0,
|
||||
PositionPriceSize? sod = null,
|
||||
PositionPriceSize? day = null,
|
||||
WorkingOrderData? working = null,
|
||||
SecurityImmutable? security = null,
|
||||
string account_name = null,
|
||||
PNLCalculationType calc_type = PNLCalculationType.Mid) {
|
||||
//
|
||||
this.key = key;
|
||||
this.cpp = cpp;
|
||||
this.bid_price = bid_price;
|
||||
this.ask_price = ask_price;
|
||||
this.last_price = last_price;
|
||||
this.settlement_price = settlement_price;
|
||||
this.sod = sod ?? PositionPriceSize.empty;
|
||||
this.day = day ?? PositionPriceSize.empty;
|
||||
this.working = working ?? WorkingOrderData.Empty;
|
||||
this.security = security;
|
||||
this.account_name = account_name;
|
||||
this.calc_type = calc_type;
|
||||
}
|
||||
|
||||
[Flags]
|
||||
enum WaterfallPriceFlags {
|
||||
None = 0,
|
||||
Bid = 1,
|
||||
Ask = 1 << 1,
|
||||
Last = 1 << 2,
|
||||
Settle = 1 << 3,
|
||||
Mid = Bid | Ask,
|
||||
LastGTEBid = Bid | Last | 1 << 4,
|
||||
LastLTEAsk = Ask | Last | 1 << 5,
|
||||
LastInsideSpread = LastGTEBid | LastLTEAsk,
|
||||
BidGTESettle = Bid | Settle | 1 << 6,
|
||||
AskLTESettle = Ask | Settle | 1 << 7
|
||||
}
|
||||
|
||||
/// Market/Calc
|
||||
public double mid_price => (bid_price + ask_price) / 2;
|
||||
public double pnl_price {
|
||||
get {
|
||||
if (!day.has_position && !sod.has_position)
|
||||
return double.NaN;
|
||||
switch (calc_type) {
|
||||
case PNLCalculationType.Bid: return bid_price;
|
||||
case PNLCalculationType.Ask: return ask_price;
|
||||
case PNLCalculationType.Mid: return mid_price;
|
||||
case PNLCalculationType.Last: return last_price;
|
||||
// waited averages must be used when the pnl price varies, depending on the side.
|
||||
case PNLCalculationType.Same: return MathUtils.weighted_average(bid_price, day_long_pos, ask_price, day_short_pos);
|
||||
case PNLCalculationType.Opposite: return MathUtils.weighted_average(ask_price, day_long_pos, bid_price, day_short_pos);
|
||||
case PNLCalculationType.Settle: return settlement_price;
|
||||
case PNLCalculationType.Waterfall:
|
||||
default: return _waterfall_price();
|
||||
}
|
||||
}
|
||||
}
|
||||
public double long_pnl_price {
|
||||
get {
|
||||
switch (calc_type) {
|
||||
case PNLCalculationType.Same: return bid_price;
|
||||
case PNLCalculationType.Opposite: return ask_price;
|
||||
default: return pnl_price;
|
||||
}
|
||||
}
|
||||
}
|
||||
public double short_pnl_price {
|
||||
get {
|
||||
switch (calc_type) {
|
||||
case PNLCalculationType.Same: return ask_price;
|
||||
case PNLCalculationType.Opposite: return bid_price;
|
||||
default: return pnl_price;
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Sod
|
||||
public double sod_pnl => cpp *
|
||||
DoubleUtil.nan_to_zero_sum(
|
||||
sod_long_avg_price_delta_raw * sod_long_pos,
|
||||
sod_short_avg_price_delta_raw * sod_short_pos
|
||||
);
|
||||
// long
|
||||
public double sod_long_pos => sod.long_pos;
|
||||
public double sod_long_avg_price => cpp * sod.long_avg;
|
||||
public double sod_long_avg_price_raw => sod.long_avg;
|
||||
public double sod_long_avg_price_delta => cpp * sod_long_avg_price_delta_raw;
|
||||
public double sod_long_avg_price_delta_raw => sod.long_pos == 0 ? double.NaN : long_pnl_price - sod.long_avg;
|
||||
// short
|
||||
public double sod_short_pos => sod.short_pos;
|
||||
public double sod_short_avg_price => cpp * sod.short_avg;
|
||||
public double sod_short_avg_price_raw => sod.short_avg;
|
||||
public double sod_short_avg_price_delta => cpp * sod_short_avg_price_delta_raw;
|
||||
public double sod_short_avg_price_delta_raw => sod.short_pos == 0 ? double.NaN : sod.short_avg - short_pnl_price;
|
||||
// net
|
||||
public double sod_net_pos => sod.net_pos;
|
||||
public double sod_net_avg_price_delta => cpp * sod_net_avg_price_delta_raw;
|
||||
public double sod_net_avg_price_delta_raw =>
|
||||
MathUtils.weighted_average(
|
||||
sod_long_avg_price_delta_raw, sod.long_pos,
|
||||
sod_short_avg_price_delta_raw, sod.short_pos);
|
||||
|
||||
/// Day
|
||||
public double day_pnl => cpp *
|
||||
DoubleUtil.nan_to_zero_sum(
|
||||
day_long_avg_price_delta_raw * day_long_pos,
|
||||
day_short_avg_price_delta_raw * day_short_pos
|
||||
);
|
||||
// long
|
||||
public double day_long_pos => day.long_pos;
|
||||
public double day_long_avg_price => cpp * day.long_avg;
|
||||
public double day_long_avg_price_raw => day.long_avg;
|
||||
public double day_long_avg_price_delta => cpp * day_long_avg_price_delta_raw;
|
||||
public double day_long_avg_price_delta_raw => day.long_pos == 0 ? double.NaN : long_pnl_price - day.long_avg;
|
||||
// short
|
||||
public double day_short_pos => day.short_pos;
|
||||
public double day_short_avg_price => cpp * day.short_avg;
|
||||
public double day_short_avg_price_raw => day.short_avg;
|
||||
public double day_short_avg_price_delta => cpp * day_short_avg_price_delta_raw;
|
||||
public double day_short_avg_price_delta_raw => day.short_pos == 0 ? double.NaN : day.short_avg - short_pnl_price;
|
||||
public bool has_day_trades => day.long_pos > 0 || day.short_pos > 0;
|
||||
// net
|
||||
public double day_net_pos => day.net_pos;
|
||||
public double day_net_avg_price_delta => cpp * day_net_avg_price_delta_raw;
|
||||
public double day_net_avg_price_delta_raw =>
|
||||
MathUtils.weighted_average(
|
||||
day_long_avg_price_delta_raw, day.long_pos,
|
||||
day_short_avg_price_delta_raw, day.short_pos);
|
||||
|
||||
/// Total
|
||||
public double total_pnl => DoubleUtil.nan_to_zero_sum(sod_pnl, day_pnl);
|
||||
// long
|
||||
public double total_long_pos => DoubleUtil.nan_to_zero_sum(sod.long_pos, day.long_pos);
|
||||
public double total_long_avg_price => cpp * total_long_avg_price_raw;
|
||||
public double total_long_avg_price_raw => DoubleUtil.nan_to_zero_sum(sod.long_avg, day.long_avg);
|
||||
public double total_long_avg_price_delta => cpp * total_long_avg_price_delta_raw;
|
||||
public double total_long_avg_price_delta_raw =>
|
||||
MathUtils.weighted_average(
|
||||
sod_long_avg_price_delta_raw, sod.long_pos,
|
||||
day_long_avg_price_delta_raw, day.long_pos);
|
||||
// short
|
||||
public double total_short_pos => DoubleUtil.nan_to_zero_sum(sod.short_pos, day.short_pos);
|
||||
public double total_short_avg_price => cpp * total_short_avg_price_raw;
|
||||
public double total_short_avg_price_raw => DoubleUtil.nan_to_zero_sum(sod.short_avg, day.short_avg);
|
||||
public double total_short_avg_price_delta => cpp * total_short_avg_price_delta_raw;
|
||||
public double total_short_avg_price_delta_raw =>
|
||||
MathUtils.weighted_average(
|
||||
sod_short_avg_price_delta_raw, sod.short_pos,
|
||||
day_short_avg_price_delta_raw, day.short_pos);
|
||||
// net
|
||||
public double total_net_pos => DoubleUtil.nan_to_zero_sum(sod.long_pos, -sod.short_pos, day.long_pos, -day.short_pos);
|
||||
public double total_net_avg_price_delta => cpp * total_net_avg_price_delta_raw;
|
||||
public double total_net_avg_price_delta_raw =>
|
||||
MathUtils.weighted_average(
|
||||
total_long_avg_price_delta_raw, total_long_pos,
|
||||
total_short_avg_price_delta_raw, total_short_pos);
|
||||
|
||||
#region Working
|
||||
// long
|
||||
public double working_long_pos => working.long_data.working_total;
|
||||
public double working_long_avg_price => cpp * working_long_avg_price_raw;
|
||||
public double working_long_top_price => cpp * working_long_top_price_raw;
|
||||
public double working_long_top_price_raw => working.top_bid;
|
||||
public double working_long_avg_price_raw => working.long_data.avg_price;
|
||||
public double working_long_avg_price_delta => cpp * working_long_avg_price_delta_raw;
|
||||
public double working_long_avg_price_delta_raw => working_long_pos == 0 ? double.NaN : long_pnl_price - working_long_avg_price_raw;
|
||||
// short
|
||||
public double working_short_pos => working.short_data.working_total;
|
||||
public double working_short_avg_price => cpp * working_short_avg_price_raw;
|
||||
public double working_short_top_price => cpp * working_short_top_price_raw;
|
||||
public double working_short_top_price_raw => working.top_ask;
|
||||
public double working_short_avg_price_raw => working.short_data.avg_price;
|
||||
public double working_short_avg_price_delta => cpp * working_short_avg_price_delta_raw;
|
||||
public double working_short_avg_price_delta_raw => working_short_pos == 0 ? double.NaN : working_short_avg_price_raw - short_pnl_price;
|
||||
// net
|
||||
public double working_net_pos => working_long_pos - working_short_pos;
|
||||
public double working_net_avg_price_delta => cpp * working_net_avg_price_delta_raw;
|
||||
public double working_net_avg_price_delta_raw =>
|
||||
MathUtils.weighted_average(
|
||||
working_long_avg_price_delta_raw, working_long_pos,
|
||||
working_short_avg_price_delta_raw, working_short_pos);
|
||||
#endregion
|
||||
|
||||
WaterfallPriceFlags _waterfall_flags() {
|
||||
var flags = WaterfallPriceFlags.None;
|
||||
if (!double.IsNaN(bid_price)) flags |= WaterfallPriceFlags.Bid;
|
||||
if (!double.IsNaN(ask_price)) flags |= WaterfallPriceFlags.Ask;
|
||||
if (!double.IsNaN(last_price)) flags |= WaterfallPriceFlags.Last;
|
||||
if (last_price >= bid_price) flags |= WaterfallPriceFlags.LastGTEBid;
|
||||
if (last_price <= ask_price) flags |= WaterfallPriceFlags.LastLTEAsk;
|
||||
if (!double.IsNaN(settlement_price)) flags |= WaterfallPriceFlags.Settle;
|
||||
if (bid_price >= settlement_price) flags |= WaterfallPriceFlags.BidGTESettle;
|
||||
if (ask_price <= settlement_price) flags |= WaterfallPriceFlags.AskLTESettle;
|
||||
return flags;
|
||||
}
|
||||
|
||||
double _waterfall_price() {
|
||||
var flags = _waterfall_flags();
|
||||
// return price for the best match
|
||||
for (int i = 0; i < _waterfall_preferences.Length; i++) {
|
||||
if ((_waterfall_preferences[i] & flags) == _waterfall_preferences[i])
|
||||
return _waterfall_price(_waterfall_preferences[i]);
|
||||
}
|
||||
return double.NaN;
|
||||
}
|
||||
|
||||
double _waterfall_price(WaterfallPriceFlags ltp) {
|
||||
switch (ltp) {
|
||||
case WaterfallPriceFlags.Last:
|
||||
case WaterfallPriceFlags.LastInsideSpread:
|
||||
case WaterfallPriceFlags.LastGTEBid:
|
||||
case WaterfallPriceFlags.LastLTEAsk: return last_price;
|
||||
case WaterfallPriceFlags.Mid: return mid_price;
|
||||
case WaterfallPriceFlags.Bid:
|
||||
case WaterfallPriceFlags.BidGTESettle: return bid_price;
|
||||
case WaterfallPriceFlags.Ask:
|
||||
case WaterfallPriceFlags.AskLTESettle: return ask_price;
|
||||
case WaterfallPriceFlags.Settle: return settlement_price;
|
||||
default: return double.NaN;
|
||||
}
|
||||
}
|
||||
|
||||
public bool Equals(PNLRowData other) =>
|
||||
key == other.key &&
|
||||
cpp == other.cpp &&
|
||||
security.HasValue == other.security.HasValue &&
|
||||
account_name == other.account_name &&
|
||||
sod == other.sod &&
|
||||
day == other.day &&
|
||||
working.summary == other.working.summary;
|
||||
|
||||
public override bool Equals(object obj) =>
|
||||
obj is PNLRowData other && Equals(other);
|
||||
|
||||
// assuming callers will not combine different values with the same key,
|
||||
// the other components shouldn't be included in the hash calculation.
|
||||
public override int GetHashCode() =>
|
||||
key.GetHashCode();
|
||||
|
||||
public static bool operator ==(PNLRowData a, PNLRowData b) => a.Equals(b);
|
||||
public static bool operator !=(PNLRowData a, PNLRowData b) => !a.Equals(b);
|
||||
}
|
||||
|
||||
static class PNLRowDataFactory {
|
||||
public static PNLRowData create((SecurityImmutable? sec, PNLUnitImmutable value) t) =>
|
||||
new PNLRowData(new PNLKey(t.value.Pos.AccountId, t.value.Pos.SecurityId),
|
||||
t.value.CashValue,
|
||||
t.value.BidPrice,
|
||||
t.value.AskPrice,
|
||||
t.value.LastPrice,
|
||||
t.value.SettlementPrice,
|
||||
new PositionPriceSize(t.value.Pos.SodLongPos, t.value.Pos.SodLongAvgPrice, t.value.Pos.SodShortPos, t.value.Pos.SodShortAvgPrice),
|
||||
new PositionPriceSize(t.value.Pos.DayLongPos, t.value.Pos.DayLongAvgPrice, t.value.Pos.DayShortPos, t.value.Pos.DayShortAvgPrice),
|
||||
WorkingOrderData.Empty,
|
||||
t.sec,
|
||||
t.value.Pos.Account);
|
||||
}
|
||||
|
||||
static class PNLDataExtensions {
|
||||
// Note: PNLRowData is passed by ref because it is fairly large struct, and since it is a readonly struct, we know it cannot be modified (so might as well avoid excessive copying).
|
||||
public static PNLRowData with(this PNLRowData pnl_data, SecurityImmutable? security = null, string account_name = null, WorkingOrderData? working = null, PNLCalculationType? calc_type = null) =>
|
||||
new PNLRowData(
|
||||
pnl_data.key,
|
||||
pnl_data.cpp,
|
||||
pnl_data.bid_price,
|
||||
pnl_data.ask_price,
|
||||
pnl_data.last_price,
|
||||
pnl_data.settlement_price,
|
||||
pnl_data.sod,
|
||||
pnl_data.day,
|
||||
working ?? pnl_data.working,
|
||||
security ?? pnl_data.security,
|
||||
account_name ?? pnl_data.account_name,
|
||||
calc_type ?? pnl_data.calc_type);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using bts.utils;
|
||||
using System;
|
||||
|
||||
namespace bts.shell.pnl {
|
||||
readonly struct PNLKey : IEquatable<PNLKey>, IComparable<PNLKey>, IComparable {
|
||||
public readonly uint account_id, security_id;
|
||||
|
||||
public PNLKey(uint account_id, uint security_id) {
|
||||
this.account_id = account_id;
|
||||
this.security_id = security_id;
|
||||
}
|
||||
|
||||
public int CompareTo(PNLKey other) {
|
||||
int c = account_id.CompareTo(other.account_id);
|
||||
if (c == 0)
|
||||
c = security_id.CompareTo(other.security_id);
|
||||
return c;
|
||||
}
|
||||
|
||||
int IComparable.CompareTo(object obj) =>
|
||||
obj is PNLKey other ? CompareTo(other) : 1;
|
||||
|
||||
public bool Equals(PNLKey other) =>
|
||||
security_id == other.security_id &&
|
||||
account_id == other.account_id;
|
||||
|
||||
public override bool Equals(object obj) =>
|
||||
obj is PNLKey other && Equals(other);
|
||||
|
||||
public override int GetHashCode() =>
|
||||
(security_id.GetHashCode() % MathUtils.Primes.P_65449) |
|
||||
((account_id.GetHashCode() % MathUtils.Primes.P_65449) << 16);
|
||||
|
||||
public override string ToString() =>
|
||||
$"Account: {account_id}, Security: {security_id}";
|
||||
|
||||
public static bool operator ==(PNLKey a, PNLKey b) => a.Equals(b);
|
||||
public static bool operator !=(PNLKey a, PNLKey b) => !a.Equals(b);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
using bts.utils;
|
||||
using System;
|
||||
|
||||
namespace bts.shell.pnl {
|
||||
readonly struct PNLNormalizedData : IEquatable<PNLNormalizedData> {
|
||||
public static readonly PNLNormalizedData zero =
|
||||
new PNLNormalizedData(0, 0, 0, 0, 0, 0);
|
||||
|
||||
public static PNLNormalizedData create_nanless(
|
||||
decimal day_pnl,
|
||||
decimal day_long_pos,
|
||||
decimal day_short_pos,
|
||||
decimal sod_pnl,
|
||||
decimal sod_long_pos,
|
||||
decimal sod_short_pos,
|
||||
decimal working_long_pos,
|
||||
decimal working_short_pos) =>
|
||||
new PNLNormalizedData(
|
||||
day_pnl,
|
||||
day_long_pos,
|
||||
day_short_pos,
|
||||
sod_pnl,
|
||||
sod_long_pos,
|
||||
sod_short_pos,
|
||||
working_long_pos,
|
||||
working_short_pos);
|
||||
|
||||
public PNLNormalizedData(
|
||||
decimal day_pnl,
|
||||
decimal day_long_pos,
|
||||
decimal day_short_pos,
|
||||
decimal sod_pnl,
|
||||
decimal sod_long_pos,
|
||||
decimal sod_short_pos,
|
||||
decimal working_long_pos = 0,
|
||||
decimal working_short_pos = 0) {
|
||||
//
|
||||
this.day_pnl = day_pnl;
|
||||
this.day_long_pos = day_long_pos;
|
||||
this.day_short_pos = day_short_pos;
|
||||
this.sod_pnl = sod_pnl;
|
||||
this.sod_long_pos = sod_long_pos;
|
||||
this.sod_short_pos = sod_short_pos;
|
||||
this.working_long_pos = working_long_pos;
|
||||
this.working_short_pos = working_short_pos;
|
||||
}
|
||||
|
||||
// sod
|
||||
public decimal sod_pnl { get; }
|
||||
public decimal sod_long_pos { get; }
|
||||
public decimal sod_short_pos { get; }
|
||||
public decimal sod_net_pos => sod_long_pos - sod_short_pos;
|
||||
// day
|
||||
public decimal day_pnl { get; }
|
||||
public decimal day_long_pos { get; }
|
||||
public decimal day_short_pos { get; }
|
||||
public decimal day_net_pos => day_long_pos - day_short_pos;
|
||||
// total
|
||||
public decimal total_pnl => sod_pnl + day_pnl;
|
||||
public decimal total_net_pos => total_long_pos - total_short_pos;
|
||||
public decimal total_long_pos => sod_long_pos + day_long_pos;
|
||||
public decimal total_short_pos => sod_short_pos + day_short_pos;
|
||||
// working
|
||||
public decimal working_long_pos { get; }
|
||||
public decimal working_net_pos => working_long_pos - working_short_pos;
|
||||
public decimal working_short_pos { get; }
|
||||
|
||||
public bool Equals(PNLNormalizedData other) =>
|
||||
day_pnl == other.day_pnl &&
|
||||
day_long_pos == other.day_long_pos &&
|
||||
day_short_pos == other.day_short_pos &&
|
||||
sod_pnl == other.sod_pnl &&
|
||||
sod_long_pos == other.sod_long_pos &&
|
||||
sod_short_pos == other.sod_short_pos &&
|
||||
working_long_pos == other.working_long_pos &&
|
||||
working_short_pos == other.working_short_pos;
|
||||
|
||||
public override bool Equals(object obj) =>
|
||||
obj is PNLNormalizedData other && Equals(other);
|
||||
|
||||
public override int GetHashCode() =>
|
||||
EqualityComparer.aggregate_hash(
|
||||
day_pnl.GetHashCode(),
|
||||
sod_pnl.GetHashCode());
|
||||
|
||||
public override string ToString() =>
|
||||
string.Join(",", new[] {
|
||||
$"SOD PNL : {sod_pnl}",
|
||||
$"SOD Long : {sod_pnl}",
|
||||
$"SOD Short: {sod_pnl}",
|
||||
$"Day PNL : {day_pnl}",
|
||||
$"Day Long : {day_pnl}",
|
||||
$"Day Short: {day_pnl}",
|
||||
$"Work Long : {working_long_pos}",
|
||||
$"Work Short: {working_short_pos}",
|
||||
});
|
||||
|
||||
public static bool operator ==(PNLNormalizedData a, PNLNormalizedData b) => a.Equals(b);
|
||||
public static bool operator !=(PNLNormalizedData a, PNLNormalizedData b) => !a.Equals(b);
|
||||
|
||||
public static PNLNormalizedData operator +(PNLNormalizedData a, PNLNormalizedData b) =>
|
||||
new PNLNormalizedData(
|
||||
a.day_pnl + b.day_pnl,
|
||||
a.day_long_pos + b.day_long_pos,
|
||||
a.day_short_pos + b.day_short_pos,
|
||||
a.sod_pnl + b.sod_pnl,
|
||||
a.sod_long_pos + b.sod_long_pos,
|
||||
a.sod_short_pos + b.sod_short_pos,
|
||||
a.working_long_pos + b.working_long_pos,
|
||||
a.working_short_pos + b.working_short_pos);
|
||||
|
||||
public static PNLNormalizedData operator -(PNLNormalizedData a, PNLNormalizedData b) =>
|
||||
new PNLNormalizedData(
|
||||
a.day_pnl - b.day_pnl,
|
||||
a.day_long_pos - b.day_long_pos,
|
||||
a.day_short_pos - b.day_short_pos,
|
||||
a.sod_pnl - b.sod_pnl,
|
||||
a.sod_long_pos - b.sod_long_pos,
|
||||
a.sod_short_pos - b.sod_short_pos,
|
||||
a.working_long_pos - b.working_long_pos,
|
||||
a.working_short_pos - b.working_short_pos);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
using bts.api;
|
||||
using bts.collections;
|
||||
using bts.shell.pnl;
|
||||
using bts.spark;
|
||||
using bts.utils;
|
||||
using mbts;
|
||||
using mbts.config;
|
||||
using mbts.persist;
|
||||
using mbts.spark;
|
||||
using mbts.ui;
|
||||
using mbts.ui.controls;
|
||||
using mbts.ui.settings;
|
||||
using mbts.ui.spark;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Collections.Specialized;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Reactive.Subjects;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace bts.shell {
|
||||
class PNLWindowFactory : ShellWindowFactory<PNLSaveState> {
|
||||
const string _window_type = "PNL", _window_title = "PNL";
|
||||
readonly IBus _bus;
|
||||
readonly OrdersModel _orders_model;
|
||||
readonly UserSettingsHub _settings_hub;
|
||||
readonly PNLConfig _pnl_conf;
|
||||
readonly IShellWindowThreadIDProvider _thread_id_provider;
|
||||
|
||||
public PNLWindowFactory(IShellModule owner, IBus bus, UserSettingsHub settings_hub, OrdersModel orders_model) : base(owner, _window_type) {
|
||||
_bus = bus;
|
||||
_orders_model = orders_model;
|
||||
_settings_hub = settings_hub;
|
||||
_pnl_conf = new PNLConfig(Configuration.get<bts.config.PNLOptions>());
|
||||
_thread_id_provider = ShellWindowThreadIDFactory.create(_window_type, Configuration.get<bts.config.MultiThreadedWindows>().pnl);
|
||||
_dispose = bus.subscribe(SparkTags.ShowPNLTag).OnDispatcher().Subscribe(_show);
|
||||
}
|
||||
|
||||
public override Task<IShellWindow> load(PNLSaveState state) =>
|
||||
Task.FromResult(_create(state.settings));
|
||||
|
||||
void _show(PNLSettings settings) =>
|
||||
_bus.post(SparkTags.ShowWindow, _create(settings));
|
||||
|
||||
IShellWindow _create(PNLSettings settings, uint settings_id = 0) {
|
||||
if (settings != null) {
|
||||
var def_grid = PNLSettings.DefaultGridSettings;
|
||||
// add missing columns (i.e., if new cols were added after this workspace was saved)
|
||||
if (settings.grid.columns.Count != def_grid.columns.Count) {
|
||||
settings.grid.columns.difference_hard(def_grid.columns, out var added, out var removed, EqualityComparer.Create<GridColumnSettings, string>(c => c.id));
|
||||
var new_cols = settings.grid.columns.ToList();
|
||||
for (int i = 0; i < added.Count; i++) {
|
||||
var col = added[i];
|
||||
new_cols.Add(col);
|
||||
}
|
||||
settings.grid = settings.grid.with(columns: new_cols);
|
||||
}
|
||||
}
|
||||
IDisposable active_settings_dialog = null;
|
||||
// Need to keep a local copy of the columns, since they are defined in xaml (not the viewmodel), and get_save_state may be called on a non-dispatcher thread.
|
||||
// If that happens, we would be unable to access _grid.Columns, and thus unable to get the current column settings.
|
||||
// Use an ImmutableList to avoid any threading issues when responding to CollectionChanged events on the dispatcher thread.
|
||||
ImmutableList<DIYGrid.ColumnBase> cached_columns = ImmutableList<DIYGrid.ColumnBase>.Empty;
|
||||
return SparkWindow<PNLSaveState>.create(_window_type, _window_title, _bus, this, Icons.Accounting, _thread_id_provider.next_id(), () => {
|
||||
var settings_subject = new Subject<PNLSettings>();
|
||||
IEnumerable<SortingCriteria> last_sorting_criterion = null;
|
||||
var vm = new PNLVM(_bus, _orders_model, settings ?? PNLSettings.Default);
|
||||
var content = new PNLView {
|
||||
DataContext = vm
|
||||
};
|
||||
cached_columns = ImmutableList<DIYGrid.ColumnBase>.Empty.AddRange(content._grid.Columns);
|
||||
var profile_service = new SettingsProfileService<PNLSettings>(_settings_hub,
|
||||
us => ImmutableDictionary<uint, SettingsProfile<PNLSettings>>.Empty.AddRange(us.pnl_profiles.ToDictionary(
|
||||
psp => psp.id,
|
||||
psp => new SettingsProfile<PNLSettings>(psp.id, psp.name, psp.value, psp.security_criteria))),
|
||||
(us, psps) => us.with(pnl_profiles: psps.Values.Select(psp => new PNLSettingsProfile(psp.id, psp.name, psp.value, psp.security_criteria)).ToArray()),
|
||||
us => us.next_pnl_profile_id);
|
||||
|
||||
Func<PNLSaveState> get_save_state = () => {
|
||||
if (last_sorting_criterion == null)
|
||||
last_sorting_criterion = PNLSettings.Default.sorting_criterion;
|
||||
var cols = cached_columns.ToArray(c => c.create());
|
||||
var filters = vm.get_filters_states();
|
||||
var calc_type = vm.calculation_type;
|
||||
PNLSettings state = settings;
|
||||
//var state = settings_manager.instance_settings;
|
||||
//if (state == null) {
|
||||
// don't use defaults if the user manipulated the window state outside of the settings dialog (e.g., resized column(s) via drag, or changed sort/filter)
|
||||
//var defaults = settings_manager.default_settings;
|
||||
//if (calc_type == defaults.calculation_type &&
|
||||
// defaults.sorting_criterion.SequenceEqual(last_sorting_criterion) &&
|
||||
// defaults.grid.columns.SequenceEqual(cols) && defaults.filters.safe_equals(filters)) {
|
||||
// // the user's current state still matches the defaults (i.e., no outside manipulation was performed); return null so this window will use the latest default settings when restored
|
||||
// return new PNLSaveState { settings_id = settings_id, settings = null };
|
||||
//}
|
||||
//state = defaults;
|
||||
//}
|
||||
// assign latest values, as they may have be modified outside of the settings dialog (in which case the settings manager would be out of date)
|
||||
state = state.with(
|
||||
last_sorting_criterion,
|
||||
vm.anchors,
|
||||
filters,
|
||||
state.grid.with(columns: cols),
|
||||
calc_type,
|
||||
vm.header_row_display);
|
||||
if (settings_id != 0) {
|
||||
if (!_settings_hub.current.ladder_profiles.TryGetFirst(lp => lp.id == settings_id, out var result)) {
|
||||
// profile was deleted
|
||||
settings_id = 0;
|
||||
} else {
|
||||
if (!state.Equals(result.value)) {
|
||||
// ladder's settings diverged from the profile (this happens when the user changes settings outside of the settings dialog, such as column widths, left panel width, etc.)
|
||||
settings_id = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
return new PNLSaveState { settings_id = settings_id, settings = state };
|
||||
};
|
||||
IEnumerable<SparkCommand> commands = new[] {
|
||||
new SparkCommandCommand("Collapse All", Icons.RightArrow, new DelegateCommand(() => vm.set_all_expanded(false))),
|
||||
new SparkCommandCommand("Expand All", Icons.DownArrow, new DelegateCommand(() => vm.set_all_expanded(true))),
|
||||
new SparkCommandCommand("Play", Icons.Play, new DelegateCommand(() => vm.is_paused = false)) {
|
||||
show_in_context_menu = _pnl_conf.show_pause_resume,
|
||||
show_in_toolbar = _pnl_conf.show_pause_resume
|
||||
},
|
||||
new SparkCommandCommand("Pause", Icons.Pause, new DelegateCommand(() => vm.is_paused = true)) {
|
||||
show_in_context_menu = _pnl_conf.show_pause_resume,
|
||||
show_in_toolbar = _pnl_conf.show_pause_resume
|
||||
},
|
||||
SparkCommands.window_settings_command(new DialogCommand(open_settings_dialog))
|
||||
};
|
||||
bool initialized = false;
|
||||
var settings_subscription = _settings_hub.subscribe_default_settings().Subscribe(on_user_settings);
|
||||
vm.OnDisposed += disposing => {
|
||||
Disposable.dispose(ref settings_subscription);
|
||||
Disposable.dispose(ref active_settings_dialog);
|
||||
};
|
||||
void on_user_settings(UserSettings us) {
|
||||
if (initialized) {
|
||||
if (settings_id == 0) {
|
||||
// not on a profile, don't respond to default settings changes
|
||||
return;
|
||||
} else if (us.pnl_profiles.TryGetFirst(p => p.id == settings_id, out var profile)) {
|
||||
if (!settings.Equals(get_save_state().settings)) {
|
||||
settings_id = 0;
|
||||
} else {
|
||||
settings_subject.OnNext(settings = profile.value);
|
||||
}
|
||||
} else {
|
||||
// profile was deleted; keep the current settings and switch to the 'Local' profile.
|
||||
settings_id = 0;
|
||||
}
|
||||
} else if (_settings_hub.initialized) {
|
||||
initialized = true;
|
||||
if (settings_id != 0) {
|
||||
if (us.pnl_profiles.TryGetFirst(p => p.id == settings_id, out var profile)) {
|
||||
settings = profile.value;
|
||||
} else {
|
||||
settings_id = 0;
|
||||
}
|
||||
}
|
||||
if (settings_id == 0 && settings == null) {
|
||||
settings = _settings_hub.current.pnl;
|
||||
}
|
||||
//vm.load_from(settings);
|
||||
settings_subject.OnNext(settings);
|
||||
}
|
||||
}
|
||||
IDisposable dispose = Disposable.Create(
|
||||
settings_subject.OnDispatcher().Subscribe(s => {
|
||||
vm.load_from(s);
|
||||
var col_setting_map = s.grid.columns.ToDictionary(c => c.id);
|
||||
var fallback_family = new FontFamily(s.grid.font_family);
|
||||
// group
|
||||
var group_col_setting_map = s.group_columns.ToDictionary(c => c.id);
|
||||
var group_appearance = s.group_cell_appearance.to_appearance();
|
||||
// header
|
||||
var header_col_setting_map = s.header_columns.ToDictionary(c => c.id);
|
||||
var header_appearance = s.header_cell_appearance.to_appearance();
|
||||
//
|
||||
foreach (var c in content._grid.Columns.OfType<PNLColumn>()) {
|
||||
if (col_setting_map.TryGetValue(c.id, out var cs)) {
|
||||
c.load_from(cs);
|
||||
}
|
||||
if (group_col_setting_map.TryGetValue(c.id, out var gcs)) {
|
||||
c.load_group_params(
|
||||
group_appearance.font_size,
|
||||
gcs.font_weight_override ?? group_appearance.font_weight,
|
||||
gcs.font_color_override ?? group_appearance.foreground,
|
||||
group_appearance.font_family);
|
||||
}
|
||||
if (header_col_setting_map.TryGetValue(c.id, out var hcs)) {
|
||||
c.load_header_params(
|
||||
header_appearance.font_size,
|
||||
hcs.font_weight_override ?? header_appearance.font_weight,
|
||||
hcs.font_color_override ?? header_appearance.foreground,
|
||||
header_appearance.font_family);
|
||||
}
|
||||
}
|
||||
vm.rows_view.set_sort_descriptions(s.sorting_criterion, content._grid.Columns);
|
||||
CollectionHelper.sync_with_never_duplicate(content._grid.Columns, s.grid.columns, cs => {
|
||||
throw new InvalidOperationException();
|
||||
}, c => c.id.ToLower(), cs => cs.id.ToLower());
|
||||
}),
|
||||
vm.rows_view
|
||||
.subscribe_sorting_criterion(sm => content._grid.Columns.TryGetFirst(dc => dc.sort_member_path == sm, out var match) ? match.id : null)
|
||||
.Subscribe(sc => last_sorting_criterion = sc),
|
||||
content._grid.Columns.Subscribe((s, e) => {
|
||||
switch (e.Action) {
|
||||
case NotifyCollectionChangedAction.Add: add_new(); break;
|
||||
case NotifyCollectionChangedAction.Remove: remove_old(); break;
|
||||
case NotifyCollectionChangedAction.Replace:
|
||||
case NotifyCollectionChangedAction.Move:
|
||||
remove_old();
|
||||
add_new();
|
||||
break;
|
||||
case NotifyCollectionChangedAction.Reset:
|
||||
cached_columns = ImmutableList<DIYGrid.ColumnBase>.Empty.AddRange(content._grid.Columns);
|
||||
break;
|
||||
}
|
||||
void add_new() =>
|
||||
cached_columns = e.NewStartingIndex >= 0 ?
|
||||
cached_columns.InsertRange(e.NewStartingIndex, e.NewItems.OfType<DIYGrid.ColumnBase>()) :
|
||||
cached_columns.AddRange(e.NewItems.OfType<DIYGrid.ColumnBase>());
|
||||
|
||||
void remove_old() =>
|
||||
cached_columns = e.OldStartingIndex >= 0 ?
|
||||
cached_columns.RemoveRange(e.OldStartingIndex, e.OldItems.Count) :
|
||||
cached_columns.RemoveRange(e.OldItems.OfType<DIYGrid.ColumnBase>());
|
||||
}),
|
||||
Disposable.wrap_collection(commands.OfType<IDisposable>()),
|
||||
vm);
|
||||
settings_subject.OnNext(settings ?? PNLSettings.Default ?? _settings_hub.current.pnl);
|
||||
return (content, commands, get_save_state, dispose);
|
||||
ShellSettingsDialog open_settings_dialog() {
|
||||
// dialog vm
|
||||
var dialog_vm = new ProfiledShellSettingsDialogVM<PNLSettings, PNLSettingsVM, PNLSettingsProfile>(
|
||||
profile_service,
|
||||
us => us.pnl ?? PNLSettings.Default,
|
||||
s => new PNLSettingsVM(s),
|
||||
_ => _,
|
||||
get_save_state().settings ?? _settings_hub.current.pnl,
|
||||
UserSettings.FileExtensions.LADDER,
|
||||
settings_id);
|
||||
dialog_vm.applied += apply;
|
||||
// dialog
|
||||
var dialog = new ShellSettingsDialog { DataContext = dialog_vm };
|
||||
dialog_vm.RequestClose += on_request_close;
|
||||
dialog.Closed += on_closed;
|
||||
Disposable.replace(ref active_settings_dialog, Disposable.Create(destroy_all));
|
||||
dialog.move_to_mouse(-500, -500);
|
||||
dialog.Show();
|
||||
return dialog;
|
||||
// local methods
|
||||
void apply(SettingsProfile<PNLSettings> profile) {
|
||||
settings_id = profile.id;
|
||||
settings = profile.value;
|
||||
//settings_subject.OnNext(profile.value);
|
||||
vm.load_from(profile.value);
|
||||
}
|
||||
// idempotent
|
||||
void destroy_all() {
|
||||
destroy_window();
|
||||
destroy_vm();
|
||||
}
|
||||
// idempotent
|
||||
void destroy_window() {
|
||||
if (dialog == null) return;
|
||||
try {
|
||||
var temp = dialog;
|
||||
dialog = null;
|
||||
temp.Closed -= on_closed;
|
||||
temp.Close();
|
||||
} catch (Exception ex) {
|
||||
on_error("closing window", ex);
|
||||
}
|
||||
}
|
||||
// idempotent
|
||||
void destroy_vm() {
|
||||
if (dialog_vm == null) return;
|
||||
try {
|
||||
var temp = dialog_vm;
|
||||
dialog_vm = null;
|
||||
temp.applied -= apply;
|
||||
temp.RequestClose -= on_request_close;
|
||||
temp.Dispose();
|
||||
} catch (Exception ex) {
|
||||
on_error("disposing vm", ex);
|
||||
}
|
||||
}
|
||||
void on_closed(object sender, EventArgs e) => destroy_all();
|
||||
void on_request_close(object sender, EventArgs e) => destroy_all();
|
||||
void on_error(string action_desc, Exception error) =>
|
||||
Log.Ui.warn($"LadderSettingsDialog error {action_desc}: {error}.");
|
||||
}
|
||||
},
|
||||
min_width_offset: SystemParameters.HorizontalScrollBarButtonWidth + 8,
|
||||
min_height_offset: 60);
|
||||
}
|
||||
|
||||
readonly struct PNLConfig {
|
||||
public readonly bool
|
||||
enable_animations,
|
||||
show_pause_resume;
|
||||
|
||||
public PNLConfig(bts.config.PNLOptions options) {
|
||||
enable_animations = options.enable_animations;
|
||||
show_pause_resume = options.show_pause_resume;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
using bts.utils;
|
||||
using mbts.expression;
|
||||
using mbts.ui;
|
||||
using ProtoBuf;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using static bts.shell.pnl.PNLView;
|
||||
|
||||
namespace bts.shell.pnl {
|
||||
interface IPNLAnimationTrigger<T> {
|
||||
IReadOnlyDictionary<Columns, PNLCellAppearance> trigger(T value);
|
||||
}
|
||||
|
||||
readonly struct PNLAnimationTrigger<T> : IPNLAnimationTrigger<T> {
|
||||
public readonly Func<T, bool> predicate;
|
||||
public readonly IReadOnlyDictionary<Columns, PNLCellAppearance> animations;
|
||||
|
||||
public PNLAnimationTrigger(Func<T, bool> predicate, IReadOnlyDictionary<Columns, PNLCellAppearance> animations) {
|
||||
this.predicate = predicate;
|
||||
this.animations = animations;
|
||||
}
|
||||
|
||||
public IReadOnlyDictionary<Columns, PNLCellAppearance> trigger(T value) =>
|
||||
predicate(value) ? animations : DictionaryHelper.empty<Columns, PNLCellAppearance>();
|
||||
}
|
||||
|
||||
[ProtoContract]
|
||||
class PNLAnimationTriggerData : IEquatable<PNLAnimationTriggerData> {
|
||||
[ProtoMember(1)]
|
||||
public string expression { get; set; }
|
||||
[ProtoMember(2)]
|
||||
public PNLAnimation[] animations { get; set; }
|
||||
|
||||
public IPNLAnimationTrigger<T> to_trigger<T>() =>
|
||||
new PNLAnimationTrigger<T>(
|
||||
CoreExpressionEvaluator<T>.try_create(expression, out var eval) ?
|
||||
eval.apply : new Func<T, bool>(_ => true),
|
||||
animations.ToDictionary(a => a.column, a => a.to_appearance()));
|
||||
|
||||
public bool Equals(PNLAnimationTriggerData other) =>
|
||||
other != null &&
|
||||
expression == other.expression &&
|
||||
EqualityComparer.safe_sequence_equals(animations, other.animations);
|
||||
|
||||
public override bool Equals(object obj) =>
|
||||
Equals(obj as PNLAnimationTriggerData);
|
||||
|
||||
public override int GetHashCode() =>
|
||||
73451;
|
||||
}
|
||||
|
||||
[ProtoContract]
|
||||
class PNLAnimation : IEquatable<PNLAnimation> {
|
||||
[ProtoMember(1)]
|
||||
public Columns column { get; set; }
|
||||
[ProtoMember(2)]
|
||||
public int? font_weight { get; set; }
|
||||
[ProtoMember(3)]
|
||||
public uint? foreground { get; set; }
|
||||
[ProtoMember(4)]
|
||||
public uint? background { get; set; }
|
||||
|
||||
public PNLCellAppearance to_appearance() =>
|
||||
new PNLCellAppearance(
|
||||
foreground?.argb_color(),
|
||||
background?.argb_color(),
|
||||
font_weight.HasValue ? FontWeight.FromOpenTypeWeight(font_weight.Value) : (FontWeight?)null);
|
||||
|
||||
public bool Equals(PNLAnimation other) =>
|
||||
other != null &&
|
||||
column == other.column &&
|
||||
font_weight == other.font_weight &&
|
||||
foreground == other.foreground &&
|
||||
background == other.background;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
using bts.proto;
|
||||
using mbts;
|
||||
using mbts.ui;
|
||||
using mbts.ui.utility.text;
|
||||
using ProtoBuf;
|
||||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using FontFamily = System.Windows.Media.FontFamily;
|
||||
|
||||
namespace bts.shell.pnl {
|
||||
readonly struct PNLCellAppearance : IEquatable<PNLCellAppearance> {
|
||||
public readonly FontWeight? font_weight;
|
||||
public readonly Color? foreground, background;
|
||||
public readonly double font_size;
|
||||
public readonly FontFamily font_family;
|
||||
|
||||
public PNLCellAppearance(Color? foreground = null, Color? background = null, FontWeight? font_weight = null, FontFamily font_family = null, double font_size = double.NaN) {
|
||||
this.foreground = foreground;
|
||||
this.background = background;
|
||||
this.font_weight = font_weight;
|
||||
this.font_family = font_family;
|
||||
this.font_size = font_size;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uses the specified appearance to replace null/absent values.
|
||||
/// </summary>
|
||||
public PNLCellAppearance merge(PNLCellAppearance other) =>
|
||||
new PNLCellAppearance(
|
||||
foreground ?? other.foreground,
|
||||
background ?? other.background,
|
||||
font_weight ?? other.font_weight,
|
||||
font_family ?? other.font_family,
|
||||
double.IsNaN(font_size) || font_size == 0 ? other.font_size : font_size);
|
||||
|
||||
public bool Equals(PNLCellAppearance other) =>
|
||||
font_weight == other.font_weight &&
|
||||
foreground == other.foreground &&
|
||||
background == other.background &&
|
||||
font_size.Equals(other.font_size) &&
|
||||
Equals(font_family, other.font_family);
|
||||
}
|
||||
|
||||
[ProtoContract]
|
||||
class PNLCellAppearanceProto : IEquatable<PNLCellAppearanceProto> {
|
||||
[ProtoMember(1)]
|
||||
public FontParamsProto font_params { get; set; }
|
||||
[ProtoMember(2)]
|
||||
public uint? foreground { get; set; }
|
||||
[ProtoMember(3)]
|
||||
public uint? background { get; set; }
|
||||
|
||||
public static PNLCellAppearanceProto from_appearance(PNLCellAppearance cell_appearance) =>
|
||||
new PNLCellAppearanceProto {
|
||||
font_params = new FontParamsProto {
|
||||
font_family = cell_appearance.font_family?.name(),
|
||||
font_color = cell_appearance.foreground?.rgb_color() ?? 0,
|
||||
font_size = cell_appearance.font_size,
|
||||
font_weight = (cell_appearance.font_weight ?? FontWeights.Normal).ToOpenTypeWeight()
|
||||
},
|
||||
background = cell_appearance.background?.rgb_color()
|
||||
};
|
||||
|
||||
public PNLCellAppearance to_appearance() {
|
||||
var font = font_params?.to_wpf_params();
|
||||
return new PNLCellAppearance(
|
||||
foreground?.rgb_color(),
|
||||
background?.argb_color(),
|
||||
font?.weight,
|
||||
font?.family,
|
||||
font?.size ?? double.NaN);
|
||||
}
|
||||
|
||||
public bool Equals(PNLCellAppearanceProto other) =>
|
||||
other != null &&
|
||||
foreground == other.foreground &&
|
||||
background == other.background &&
|
||||
font_params.safe_equals(other.font_params);
|
||||
|
||||
public override bool Equals(object obj) =>
|
||||
Equals(obj as PNLCellAppearanceProto);
|
||||
|
||||
public override int GetHashCode() =>
|
||||
17437;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,328 @@
|
||||
using bts.collections;
|
||||
using bts.proto;
|
||||
using bts.utils;
|
||||
using mbts;
|
||||
using mbts.spark;
|
||||
using mbts.ui;
|
||||
using mbts.ui.controls;
|
||||
using mbts.ui.settings;
|
||||
using mbts.ui.shell;
|
||||
using mbts.ui.utility;
|
||||
using mbts.ui.utility.text;
|
||||
using mbts.utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using FontFamily = System.Windows.Media.FontFamily;
|
||||
|
||||
namespace bts.shell.pnl {
|
||||
class PNLCellAppearanceParamsVM : DisposableViewModelBase, IValueFactoryResultChanged<PNLCellAppearance> {
|
||||
static readonly IReadOnlyList<Func<PNLCellAppearanceParamsVM, INotifyPropertyChanged>> _observable_component_getters =
|
||||
ObjectSnapshot.property_delegates_of_type<PNLCellAppearanceParamsVM, INotifyPropertyChanged>()
|
||||
.ToArray();
|
||||
|
||||
readonly DeferrableActionWrapper _update;
|
||||
FontWeight _font_weight;
|
||||
FontFamily _font_family;
|
||||
double _font_size = double.NaN;
|
||||
|
||||
public PNLCellAppearanceParamsVM() {
|
||||
_update = new DeferrableActionWrapper(() => result_changed?.Invoke());
|
||||
_dispose = ObjectSnapshot.connect_property_changed(_observable_component_getters.Select(g => g(this)).Append(this), (s, e) => _update.execute());
|
||||
}
|
||||
|
||||
public event Action result_changed;
|
||||
|
||||
public ColorPickerPopupVM background { get; } = new ColorPickerPopupVM { bind_opacity_to_alpha = true, opacity = 1 };
|
||||
public ColorPickerPopupVM font_color { get; } = new ColorPickerPopupVM();
|
||||
public FontWeight font_weight { get => _font_weight; set => Set(ref _font_weight, value); }
|
||||
public FontFamily font_family { get => _font_family; set => Set(ref _font_family, value); }
|
||||
public double font_size { get => _font_size; set => Set(ref _font_size, value); }
|
||||
|
||||
public PNLCellAppearance create() =>
|
||||
new PNLCellAppearance(font_color.color, background.color, font_weight, font_family, font_size);
|
||||
|
||||
public void load_from(PNLCellAppearance appearance) {
|
||||
using (_update.defer_execution()) {
|
||||
if (appearance.background.HasValue)
|
||||
background.color = appearance.background.Value;
|
||||
if (appearance.font_family != null)
|
||||
font_family = appearance.font_family;
|
||||
if (appearance.font_weight.HasValue)
|
||||
font_weight = appearance.font_weight.Value;
|
||||
if (appearance.font_size > 0)
|
||||
font_size = appearance.font_size;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PNLColumnParamsVM : DisposableViewModelBase, IResultChanged {
|
||||
static readonly IReadOnlyList<Func<PNLColumnParamsVM, INotifyPropertyChanged>> _observable_component_getters =
|
||||
ObjectSnapshot.property_delegates_of_type<PNLColumnParamsVM, INotifyPropertyChanged>(p => !typeof(IResultChanged).IsAssignableFrom(p.PropertyType))
|
||||
.ToArray();
|
||||
|
||||
static readonly IReadOnlyList<Func<PNLColumnParamsVM, IResultChanged>> _result_changed_component_getters =
|
||||
ObjectSnapshot.property_delegates_of_type<PNLColumnParamsVM, IResultChanged>()
|
||||
.ToArray();
|
||||
|
||||
readonly DeferrableActionWrapper _update;
|
||||
bool
|
||||
_is_background_overridden,
|
||||
_is_font_color_overridden,
|
||||
_is_font_weight_overridden,
|
||||
_is_font_family_overridden,
|
||||
_is_font_size_overridden;
|
||||
|
||||
FontWeight _font_weight_override;
|
||||
FontFamily _font_family_override;
|
||||
double _font_size_override = double.NaN;
|
||||
|
||||
public PNLColumnParamsVM() {
|
||||
_update = new DeferrableActionWrapper(() => result_changed?.Invoke());
|
||||
_dispose = Disposable.Create(
|
||||
ObjectSnapshot.connect_property_changed(_observable_component_getters.Select(g => g(this)).Append(this), (s, e) => _update.execute()),
|
||||
ObjectSnapshot.connect_result_changed(_result_changed_component_getters.Select(g => g(this)), _update.execute));
|
||||
}
|
||||
|
||||
public event Action result_changed;
|
||||
// background
|
||||
public bool is_background_overridden { get => _is_background_overridden; set => Set(ref _is_background_overridden, value); }
|
||||
public ColorPickerPopupVM background_override { get; } = new ColorPickerPopupVM {
|
||||
bind_opacity_to_alpha = true,
|
||||
opacity = 1
|
||||
};
|
||||
// font color
|
||||
public bool is_font_color_overridden { get => _is_font_color_overridden; set => Set(ref _is_font_color_overridden, value); }
|
||||
public ColorPickerPopupVM font_color_override { get; } = new ColorPickerPopupVM();
|
||||
// font weight
|
||||
public bool is_font_weight_overridden { get => _is_font_weight_overridden; set => Set(ref _is_font_weight_overridden, value); }
|
||||
public FontWeight font_weight_override { get => _font_weight_override; set => Set(ref _font_weight_override, value); }
|
||||
// font family
|
||||
public bool is_font_family_overriden { get => _is_font_family_overridden; set => Set(ref _is_font_family_overridden, value); }
|
||||
public FontFamily font_family_override { get => _font_family_override; set => Set(ref _font_family_override, value); }
|
||||
// font size
|
||||
public bool is_font_size_overriden { get => _is_font_size_overridden; set => Set(ref _is_font_size_overridden, value); }
|
||||
public double font_size_override { get => _font_size_override; set => Set(ref _font_size_override, value); }
|
||||
|
||||
public GridColumnSettings apply_to(GridColumnSettings target) =>
|
||||
target.with(
|
||||
font_weight_override: new NullableFontWeightProto(is_font_weight_overridden ? font_weight_override : (FontWeight?)null),
|
||||
font_color_override: new NullableColorProto(is_font_color_overridden ? font_color_override.color : (Color?)null),
|
||||
background_override: new NullableColorProto(is_background_overridden ? background_override.color : (Color?)null));
|
||||
|
||||
public void load(GridColumnSettings settings) {
|
||||
if (settings != null) {
|
||||
using (_update.defer_execution())
|
||||
load(settings.font_weight_override, settings.font_color_override, settings.background_override);
|
||||
}
|
||||
}
|
||||
|
||||
public void load(FontParamsProto font_params) {
|
||||
using (_update.defer_execution()) {
|
||||
if (font_params == null)
|
||||
return;
|
||||
// font weight
|
||||
is_font_weight_overridden = font_params.font_weight > 0;
|
||||
if (is_font_weight_overridden)
|
||||
font_weight_override = FontWeight.FromOpenTypeWeight(font_params.font_weight);
|
||||
// font color
|
||||
is_font_color_overridden = font_params.font_color > 0;
|
||||
if (is_font_color_overridden)
|
||||
font_color_override.color = font_params.font_color.rgb_color();
|
||||
// font family
|
||||
if (!string.IsNullOrEmpty(font_params.font_family)) {
|
||||
font_family_override = new FontFamily(font_params.font_family);
|
||||
}
|
||||
if (!double.IsNaN(font_params.font_size) && font_params.font_size > 0) {
|
||||
font_size_override = font_params.font_size;
|
||||
} else {
|
||||
font_size_override = double.NaN;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void load(PNLAnimation animation) {
|
||||
if (animation != null) {
|
||||
using (_update.defer_execution()) {
|
||||
load(
|
||||
animation.font_weight.HasValue ? FontWeight.FromOpenTypeWeight(animation.font_weight.Value) : (FontWeight?)null,
|
||||
animation.foreground?.argb_color(),
|
||||
animation.background?.argb_color());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void load(FontWeight? font_weight, Color? font_color, Color? background) {
|
||||
using (_update.defer_execution()) {
|
||||
// font weight
|
||||
is_font_weight_overridden = font_weight.HasValue;
|
||||
if (is_font_weight_overridden)
|
||||
font_weight_override = font_weight.Value;
|
||||
// font color
|
||||
is_font_color_overridden = font_color.HasValue;
|
||||
if (is_font_color_overridden)
|
||||
font_color_override.color = font_color.Value;
|
||||
// background
|
||||
is_background_overridden = background.HasValue;
|
||||
if (background.HasValue)
|
||||
background_override.color = background.Value;
|
||||
}
|
||||
}
|
||||
|
||||
public PNLAnimation to_animation(PNLView.Columns key) =>
|
||||
new PNLAnimation {
|
||||
column = key,
|
||||
foreground = is_font_color_overridden ? font_color_override.color.rgb_color() : (uint?)null,
|
||||
background = is_background_overridden ? background_override.color.rgb_color() : (uint?)null,
|
||||
font_weight = is_font_weight_overridden ? font_weight_override.ToOpenTypeWeight() : (int?)null
|
||||
};
|
||||
|
||||
public PNLCellAppearanceProto to_appearance_proto() =>
|
||||
new PNLCellAppearanceProto {
|
||||
background = is_background_overridden ? background_override.color.rgb_color() : (uint?)null,
|
||||
foreground = is_font_color_overridden ? font_color_override.color.rgb_color() : (uint?)null,
|
||||
font_params = to_font_params()
|
||||
};
|
||||
|
||||
public FontParamsProto to_font_params() =>
|
||||
new FontParamsProto {
|
||||
font_color = is_font_color_overridden ? font_color_override.color.rgb_color() : 0,
|
||||
font_weight = is_font_weight_overridden ? font_weight_override.ToOpenTypeWeight() : 0,
|
||||
font_family = font_family_override?.name(),
|
||||
font_size = font_size_override
|
||||
};
|
||||
}
|
||||
|
||||
class PNLColumnVM : DisposableViewModelBase, ISimpleColumnVM {
|
||||
static readonly Func<PNLColumnVM, Action, IDisposable> _observe_changes_del = ObjectSnapshot.auto_change_observer_factory<PNLColumnVM>();
|
||||
|
||||
readonly SimpleGridSettingsVM _owner;
|
||||
readonly PNLView.Columns _pnl_id;
|
||||
readonly DeferrableActionWrapper _update;
|
||||
object _selected_item;
|
||||
string _display_name;
|
||||
bool _is_visible;
|
||||
double _width;
|
||||
HorizontalAlignment _horizontal_alignment;
|
||||
|
||||
public PNLColumnVM(SimpleGridSettingsVM owner, PNLView.Columns id) {
|
||||
_owner = owner;
|
||||
_pnl_id = id;
|
||||
_update = new DeferrableActionWrapper(() => result_changed?.Invoke());
|
||||
_dispose = Disposable.Create(
|
||||
_observe_changes_del(this, _update.execute),
|
||||
ObservableAutoWrapper.connect_item_hooks(rule_columns, c => ObjectSnapshot.connect_property_changed(c, on_prop_changed)));
|
||||
|
||||
void on_prop_changed(object sender, PropertyChangedEventArgs e) =>
|
||||
_update.execute();
|
||||
}
|
||||
|
||||
public event Action result_changed;
|
||||
|
||||
public PNLColumnParamsVM default_column_vm { get; } = new PNLColumnParamsVM();
|
||||
public PNLColumnParamsVM group_column_vm { get; } = new PNLColumnParamsVM();
|
||||
public PNLColumnParamsVM header_column_vm { get; } = new PNLColumnParamsVM();
|
||||
//
|
||||
public ObservableCollection<PNLRuleColumnVM> rule_columns { get; } = new ObservableCollection<PNLRuleColumnVM>();
|
||||
|
||||
public object selected_item {
|
||||
get => _selected_item;
|
||||
set {
|
||||
if (value != null)
|
||||
Set(ref _selected_item, value);
|
||||
}
|
||||
}
|
||||
|
||||
public string id { get => _pnl_id.ToString(); set => Log.assert(value == _pnl_id.ToString()); }
|
||||
public string display_name { get => _display_name; set => Set(ref _display_name, value); }
|
||||
public double width { get => _width; set => Set(ref _width, value); }
|
||||
public bool is_visible { get => _is_visible; set => Set(ref _is_visible, value); }
|
||||
public bool is_font_color_overridden {
|
||||
get => default_column_vm.is_font_color_overridden;
|
||||
set => default_column_vm.is_font_color_overridden = value;
|
||||
}
|
||||
public HorizontalAlignment horizontal_alignment { get => _horizontal_alignment; set => Set(ref _horizontal_alignment, value); }
|
||||
|
||||
public ColorPickerPopupVM font_color_override => default_column_vm.font_color_override;
|
||||
|
||||
public bool is_font_weight_overridden { get => default_column_vm.is_font_weight_overridden; set => default_column_vm.is_font_weight_overridden = value; }
|
||||
public FontWeight font_weight_override { get => default_column_vm.font_weight_override; set => default_column_vm.font_weight_override = value; }
|
||||
|
||||
IEnumerable<INotifyPropertyChanged> _observable_components =>
|
||||
_observable_component_getters.Select(g => g(this)).Append(this);
|
||||
|
||||
public void apply_flags(GridColumnSettingsFeatureFlags flags) { }
|
||||
|
||||
public GridColumnSettings create() {
|
||||
var result = new GridColumnSettings(id: id, display_name: display_name, is_visible: is_visible, horizontal_alignment: horizontal_alignment, width: width);
|
||||
result = default_column_vm.apply_to(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
public void load_from(GridColumnSettings value) {
|
||||
using (_update.defer_execution()) {
|
||||
_assert_id(value.id);
|
||||
default_column_vm.load(value);
|
||||
display_name = value.display_name;
|
||||
width = value.width;
|
||||
is_visible = value.is_visible;
|
||||
horizontal_alignment = value.horizontal_alignment;
|
||||
}
|
||||
}
|
||||
|
||||
public void load_from(GridColumnSettings value, IReadOnlyDictionary<string, PNLAnimation> triggers, GridColumnSettings group, IReadOnlyDictionary<string, PNLAnimation> group_triggers, IReadOnlyDictionary<string, PNLAnimation> header_triggers, GridColumnSettings header) {
|
||||
using (_update.defer_execution()) {
|
||||
_assert_id(value.id);
|
||||
load_from(value);
|
||||
if (group != null)
|
||||
group_column_vm.load(group);
|
||||
if (header != null)
|
||||
header_column_vm.load(header);
|
||||
rule_columns.SetItems(triggers.Select(kv => {
|
||||
var rule_col = new PNLRuleColumnVM(kv.Key);
|
||||
rule_col.load_from(kv.Value, group_triggers.GetValueOrDefault(kv.Key), header_triggers.GetValueOrDefault(kv.Key));
|
||||
return rule_col;
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<(string expression, PNLAnimation row, PNLAnimation group, PNLAnimation header)> get_triggers() {
|
||||
foreach (var col in rule_columns)
|
||||
yield return (col.rule_text, col.row_params.to_animation(_pnl_id), col.group_params.to_animation(_pnl_id), col.header_params.to_animation(_pnl_id));
|
||||
}
|
||||
|
||||
void _assert_id(string id) =>
|
||||
Log.assert(id == _pnl_id.ToString(), $"PNLColumn id string '{id}' does not match the expected {typeof(PNLView.Columns)} key '{_pnl_id}.'");
|
||||
|
||||
public override string ToString() => "Default";
|
||||
}
|
||||
|
||||
class PNLRuleColumnVM : ViewModelBaseWithValidation, IEquatable<PNLRuleColumnVM> {
|
||||
public PNLRuleColumnVM(string rule_text) =>
|
||||
this.rule_text = rule_text;
|
||||
|
||||
public string rule_text { get; }
|
||||
|
||||
public PNLColumnParamsVM row_params { get; } = new PNLColumnParamsVM();
|
||||
public PNLColumnParamsVM group_params { get; } = new PNLColumnParamsVM();
|
||||
public PNLColumnParamsVM header_params { get; } = new PNLColumnParamsVM();
|
||||
|
||||
public void load_from(PNLAnimation row, PNLAnimation group, PNLAnimation header) {
|
||||
row_params.load(row);
|
||||
group_params.load(group);
|
||||
header_params.load(header);
|
||||
}
|
||||
|
||||
public bool Equals(PNLRuleColumnVM other) =>
|
||||
other != null && rule_text == other.rule_text;
|
||||
|
||||
public override bool Equals(object obj) => Equals(obj as PNLRuleColumnVM);
|
||||
public override int GetHashCode() => rule_text.GetHashCode();
|
||||
public override string ToString() => rule_text;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
using bts.api;
|
||||
using mbts;
|
||||
using mbts.spark;
|
||||
using mbts.spark.filters;
|
||||
using ProtoBuf;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace bts.shell.pnl {
|
||||
[ProtoContract]
|
||||
class FilterStates : IEquatable<FilterStates> {
|
||||
// account
|
||||
[ProtoMember(1)]
|
||||
uint[] _account;
|
||||
public uint[] account {
|
||||
get => _account ?? Array.Empty<uint>();
|
||||
set => _account = value;
|
||||
}
|
||||
// exchange
|
||||
[ProtoMember(2)]
|
||||
Venue[] _exchange;
|
||||
public Venue[] exchange {
|
||||
get => _exchange ?? Array.Empty<Venue>();
|
||||
set => _exchange = value;
|
||||
}
|
||||
// symbol
|
||||
[ProtoMember(3)]
|
||||
string[] _symbol;
|
||||
public string[] symbol {
|
||||
get => _symbol ?? Array.Empty<string>();
|
||||
set => _symbol = value;
|
||||
}
|
||||
// security
|
||||
[ProtoMember(4)]
|
||||
public string security { get; set; }
|
||||
// security type
|
||||
[ProtoMember(5)]
|
||||
Security.ContractOneofCase[] _security_type;
|
||||
public Security.ContractOneofCase[] security_type {
|
||||
get => _security_type ?? Array.Empty<Security.ContractOneofCase>();
|
||||
set => _security_type = value;
|
||||
}
|
||||
|
||||
/// sod
|
||||
[ProtoMember(6)]
|
||||
public DoubleMinMaxState sod_pnl { get; set; }
|
||||
// sod long
|
||||
[ProtoMember(7)]
|
||||
public DoubleMinMaxState sod_long_pos { get; set; }
|
||||
[ProtoMember(8)]
|
||||
public DoubleMinMaxState sod_long_avg_price { get; set; }
|
||||
[ProtoMember(9)]
|
||||
public DoubleMinMaxState sod_long_avg_price_delta { get; set; }
|
||||
// sod short
|
||||
[ProtoMember(10)]
|
||||
public DoubleMinMaxState sod_short_pos { get; set; }
|
||||
[ProtoMember(11)]
|
||||
public DoubleMinMaxState sod_short_avg_price { get; set; }
|
||||
[ProtoMember(12)]
|
||||
public DoubleMinMaxState sod_short_avg_price_delta { get; set; }
|
||||
// sod net
|
||||
[ProtoMember(13)]
|
||||
public DoubleMinMaxState sod_net_pos { get; set; }
|
||||
[ProtoMember(14)]
|
||||
public DoubleMinMaxState sod_net_avg_price_delta { get; set; }
|
||||
|
||||
/// day
|
||||
[ProtoMember(15)]
|
||||
public DoubleMinMaxState day_pnl { get; set; }
|
||||
// day long
|
||||
[ProtoMember(16)]
|
||||
public DoubleMinMaxState day_long_pos { get; set; }
|
||||
[ProtoMember(17)]
|
||||
public DoubleMinMaxState day_long_avg_price { get; set; }
|
||||
[ProtoMember(18)]
|
||||
public DoubleMinMaxState day_long_avg_price_delta { get; set; }
|
||||
// day short
|
||||
[ProtoMember(19)]
|
||||
public DoubleMinMaxState day_short_pos { get; set; }
|
||||
[ProtoMember(20)]
|
||||
public DoubleMinMaxState day_short_avg_price { get; set; }
|
||||
[ProtoMember(21)]
|
||||
public DoubleMinMaxState day_short_avg_price_delta { get; set; }
|
||||
// day net
|
||||
[ProtoMember(22)]
|
||||
public DoubleMinMaxState day_net_pos { get; set; }
|
||||
[ProtoMember(23)]
|
||||
public DoubleMinMaxState day_net_avg_price_delta { get; set; }
|
||||
|
||||
/// total
|
||||
[ProtoMember(24)]
|
||||
public DoubleMinMaxState total_pnl { get; set; }
|
||||
// total long
|
||||
[ProtoMember(25)]
|
||||
public DoubleMinMaxState total_long_pos { get; set; }
|
||||
[ProtoMember(26)]
|
||||
public DoubleMinMaxState total_long_avg_price { get; set; }
|
||||
[ProtoMember(27)]
|
||||
public DoubleMinMaxState total_long_avg_price_delta { get; set; }
|
||||
// total short
|
||||
[ProtoMember(28)]
|
||||
public DoubleMinMaxState total_short_pos { get; set; }
|
||||
[ProtoMember(29)]
|
||||
public DoubleMinMaxState total_short_avg_price { get; set; }
|
||||
[ProtoMember(30)]
|
||||
public DoubleMinMaxState total_short_avg_price_delta { get; set; }
|
||||
// total net
|
||||
[ProtoMember(31)]
|
||||
public DoubleMinMaxState total_net_pos { get; set; }
|
||||
[ProtoMember(32)]
|
||||
public DoubleMinMaxState total_net_avg_price_delta { get; set; }
|
||||
|
||||
/// market
|
||||
[ProtoMember(33)]
|
||||
public DoubleMinMaxState bid_price { get; set; }
|
||||
[ProtoMember(34)]
|
||||
public DoubleMinMaxState mid_price { get; set; }
|
||||
[ProtoMember(35)]
|
||||
public DoubleMinMaxState ask_price { get; set; }
|
||||
[ProtoMember(36)]
|
||||
public DoubleMinMaxState last_price { get; set; }
|
||||
[ProtoMember(37)]
|
||||
public DoubleMinMaxState settlement_price { get; set; }
|
||||
[ProtoMember(38)]
|
||||
public DoubleMinMaxState pnl_price { get; set; }
|
||||
|
||||
/// working (long)
|
||||
[ProtoMember(39)]
|
||||
public DoubleMinMaxState working_long_pos { get; set; }
|
||||
[ProtoMember(40)]
|
||||
public DoubleMinMaxState working_long_avg_price { get; set; }
|
||||
[ProtoMember(41)]
|
||||
public DoubleMinMaxState working_long_avg_price_delta { get; set; }
|
||||
[ProtoMember(42)]
|
||||
// working short
|
||||
public DoubleMinMaxState working_short_pos { get; set; }
|
||||
[ProtoMember(43)]
|
||||
public DoubleMinMaxState working_short_avg_price { get; set; }
|
||||
[ProtoMember(44)]
|
||||
public DoubleMinMaxState working_short_avg_price_delta { get; set; }
|
||||
// working net
|
||||
[ProtoMember(45)]
|
||||
public DoubleMinMaxState working_net_pos { get; set; }
|
||||
[ProtoMember(46)]
|
||||
public DoubleMinMaxState working_net_avg_price_delta { get; set; }
|
||||
|
||||
// has day trades
|
||||
[ProtoMember(47)]
|
||||
bool[] _has_day_trades { get; set; }
|
||||
public bool[] has_day_trades {
|
||||
get => _has_day_trades ?? Array.Empty<bool>();
|
||||
set => _has_day_trades = value;
|
||||
}
|
||||
// cpp
|
||||
[ProtoMember(48)]
|
||||
public DoubleMinMaxState cpp { get; set; }
|
||||
|
||||
public override bool Equals(object obj) =>
|
||||
Equals(obj as FilterStates);
|
||||
|
||||
public override int GetHashCode() => -113;
|
||||
|
||||
public bool Equals(FilterStates other) =>
|
||||
other != null &&
|
||||
account.SequenceEqual(other.account) &&
|
||||
symbol.SequenceEqual(other.symbol) &&
|
||||
exchange.SequenceEqual(other.exchange) &&
|
||||
security == other.security &&
|
||||
security_type.SequenceEqual(other.security_type) &&
|
||||
/// sod
|
||||
sod_pnl.safe_equals(other.sod_pnl) &&
|
||||
sod_long_pos.safe_equals(other.sod_long_pos) &&
|
||||
sod_long_avg_price.safe_equals(other.sod_long_avg_price) &&
|
||||
sod_long_avg_price_delta.safe_equals(other.sod_long_avg_price_delta) &&
|
||||
sod_short_pos.safe_equals(other.sod_short_pos) &&
|
||||
sod_short_avg_price.safe_equals(other.sod_short_avg_price) &&
|
||||
sod_short_avg_price_delta.safe_equals(other.sod_short_avg_price_delta) &&
|
||||
sod_net_pos.safe_equals(other.sod_net_pos) &&
|
||||
sod_net_avg_price_delta.safe_equals(other.sod_net_avg_price_delta) &&
|
||||
/// day
|
||||
day_pnl.safe_equals(other.day_pnl) &&
|
||||
day_long_pos.safe_equals(other.day_long_pos) &&
|
||||
day_long_avg_price.safe_equals(other.day_long_avg_price) &&
|
||||
day_long_avg_price_delta.safe_equals(other.day_long_avg_price_delta) &&
|
||||
day_short_pos.safe_equals(other.day_short_pos) &&
|
||||
day_short_avg_price.safe_equals(other.day_short_avg_price) &&
|
||||
day_short_avg_price_delta.safe_equals(other.day_short_avg_price_delta) &&
|
||||
day_net_pos.safe_equals(other.day_net_pos) &&
|
||||
day_net_avg_price_delta.safe_equals(other.day_net_avg_price_delta) &&
|
||||
/// market
|
||||
bid_price.safe_equals(other.bid_price) &&
|
||||
ask_price.safe_equals(other.ask_price) &&
|
||||
mid_price.safe_equals(other.mid_price) &&
|
||||
last_price.safe_equals(other.last_price) &&
|
||||
settlement_price.safe_equals(other.settlement_price) &&
|
||||
pnl_price.safe_equals(other.pnl_price) &&
|
||||
/// total
|
||||
total_pnl.safe_equals(other.total_pnl) &&
|
||||
total_long_pos.safe_equals(other.total_long_pos) &&
|
||||
total_long_avg_price.safe_equals(other.total_long_avg_price) &&
|
||||
total_long_avg_price_delta.safe_equals(other.total_long_avg_price_delta) &&
|
||||
total_short_pos.safe_equals(other.total_short_pos) &&
|
||||
total_short_avg_price.safe_equals(other.total_short_avg_price) &&
|
||||
total_short_avg_price_delta.safe_equals(other.total_short_avg_price_delta) &&
|
||||
total_net_pos.safe_equals(other.total_net_pos) &&
|
||||
total_net_avg_price_delta.safe_equals(other.total_net_avg_price_delta) &&
|
||||
has_day_trades.SequenceEqual(other.has_day_trades) &&
|
||||
cpp.safe_equals(other.cpp);
|
||||
}
|
||||
|
||||
static class PNLFilterStatesExtensions {
|
||||
public static Func<PNLRowData, bool> build_predicate(this FilterStates fs) {
|
||||
var bd = new ColumnPredicateBuilder<PNLRowData>();
|
||||
bd.add_discrete_values(fs.account, d => d.key.account_id);
|
||||
bd.add_discrete_values(fs.exchange, d => d.security.exchange_or_default());
|
||||
bd.add_discrete_values(fs.symbol, d => d.security.symbol_or_default());
|
||||
bd.add_regex(fs.security, d => d.security.ToString());
|
||||
bd.add_discrete_values(fs.security_type, d => d.security.HasValue ? d.security.Value.ContractCase : Security.ContractOneofCase.None);
|
||||
/// sod
|
||||
bd.add_numeric(fs.sod_pnl, d => d.sod_pnl);
|
||||
// sod long
|
||||
bd.add_numeric(fs.sod_long_pos, d => d.sod_long_pos);
|
||||
bd.add_numeric(fs.sod_long_avg_price, d => d.sod_long_avg_price_raw);
|
||||
bd.add_numeric(fs.sod_long_avg_price_delta, d => d.sod_long_avg_price_delta_raw);
|
||||
// sod short
|
||||
bd.add_numeric(fs.sod_short_pos, d => d.sod_short_pos);
|
||||
bd.add_numeric(fs.sod_short_avg_price, d => d.sod_short_avg_price_raw);
|
||||
bd.add_numeric(fs.sod_short_avg_price_delta, d => d.sod_short_avg_price_delta_raw);
|
||||
// sod net
|
||||
bd.add_numeric(fs.sod_net_pos, d => d.sod_net_pos);
|
||||
bd.add_numeric(fs.sod_net_avg_price_delta, d => d.sod_net_avg_price_delta_raw);
|
||||
/// day
|
||||
bd.add_numeric(fs.day_pnl, d => d.day_pnl);
|
||||
// day long
|
||||
bd.add_numeric(fs.day_long_pos, d => d.day_long_pos);
|
||||
bd.add_numeric(fs.day_long_avg_price, d => d.day_long_avg_price_raw);
|
||||
bd.add_numeric(fs.day_long_avg_price_delta, d => d.day_long_avg_price_delta_raw);
|
||||
// day short
|
||||
bd.add_numeric(fs.day_short_pos, d => d.day_short_pos);
|
||||
bd.add_numeric(fs.day_short_avg_price, d => d.day_short_avg_price_raw);
|
||||
bd.add_numeric(fs.day_short_avg_price_delta, d => d.day_short_avg_price_delta_raw);
|
||||
// day net
|
||||
bd.add_numeric(fs.day_net_pos, d => d.day_net_pos);
|
||||
bd.add_numeric(fs.day_net_avg_price_delta, d => d.day_net_avg_price_delta_raw);
|
||||
/// total
|
||||
bd.add_numeric(fs.total_pnl, d => d.total_pnl);
|
||||
// total long
|
||||
bd.add_numeric(fs.total_long_pos, d => d.total_long_pos);
|
||||
bd.add_numeric(fs.total_long_avg_price, d => d.total_long_avg_price_raw);
|
||||
bd.add_numeric(fs.total_long_avg_price_delta, d => d.total_long_avg_price_delta_raw);
|
||||
// total short
|
||||
bd.add_numeric(fs.total_short_pos, d => d.total_short_pos);
|
||||
bd.add_numeric(fs.total_short_avg_price, d => d.total_short_avg_price_raw);
|
||||
bd.add_numeric(fs.total_short_avg_price_delta, d => d.total_short_avg_price_delta_raw);
|
||||
// total net
|
||||
bd.add_numeric(fs.total_net_pos, d => d.total_net_pos);
|
||||
bd.add_numeric(fs.total_net_avg_price_delta, d => d.total_net_avg_price_delta_raw);
|
||||
/// market
|
||||
bd.add_numeric(fs.bid_price, d => d.bid_price);
|
||||
bd.add_numeric(fs.ask_price, d => d.ask_price);
|
||||
bd.add_numeric(fs.mid_price, d => d.mid_price);
|
||||
bd.add_numeric(fs.last_price, d => d.last_price);
|
||||
bd.add_numeric(fs.settlement_price, d => d.settlement_price);
|
||||
bd.add_numeric(fs.pnl_price, d => d.pnl_price);
|
||||
/// working (long)
|
||||
bd.add_numeric(fs.working_long_pos, d => d.working_long_pos);
|
||||
bd.add_numeric(fs.working_long_avg_price, d => d.working_long_avg_price_raw);
|
||||
bd.add_numeric(fs.working_long_avg_price_delta, d => d.working_long_avg_price_delta_raw);
|
||||
// working short
|
||||
bd.add_numeric(fs.working_short_pos, d => d.working_short_pos);
|
||||
bd.add_numeric(fs.working_short_avg_price, d => d.working_short_avg_price_raw);
|
||||
bd.add_numeric(fs.working_short_avg_price_delta, d => d.working_short_avg_price_delta_raw);
|
||||
// working net
|
||||
bd.add_numeric(fs.working_net_pos, d => d.working_net_pos);
|
||||
bd.add_numeric(fs.working_net_avg_price_delta, d => d.working_net_avg_price_delta_raw);
|
||||
// misc
|
||||
bd.add_discrete_values(fs.has_day_trades, d => d.has_day_trades);
|
||||
bd.add_numeric(fs.cpp, d => d.cpp);
|
||||
return bd.to_predicate();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,484 @@
|
||||
using bts.proto;
|
||||
using mbts;
|
||||
using mbts.ui;
|
||||
using mbts.ui.settings;
|
||||
using ProtoBuf;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using static bts.shell.pnl.PNLView;
|
||||
using static mbts.ui.controls.DIYGrid;
|
||||
using Headers = mbts.ui.controls.DIYGrid.DefaultColumnHeaders.PNLHeaders;
|
||||
using Widths = mbts.ui.controls.DIYGrid.DefaultColumnWidths.PNL;
|
||||
|
||||
namespace bts.shell.pnl {
|
||||
[ProtoContract]
|
||||
class PNLSaveState : IEquatable<PNLSaveState> {
|
||||
[ProtoMember(1)]
|
||||
public uint settings_id { get; set; }
|
||||
[ProtoMember(2)]
|
||||
public PNLSettings settings { get; set; }
|
||||
|
||||
public bool Equals(PNLSaveState other) =>
|
||||
other != null &&
|
||||
settings_id == other.settings_id &&
|
||||
settings.safe_equals(other.settings);
|
||||
}
|
||||
|
||||
[ProtoContract]
|
||||
class PNLSettings : IEquatable<PNLSettings> {
|
||||
const double _default_group_font_size = double.NaN;
|
||||
// bold
|
||||
const int _default_group_font_weight = 700;
|
||||
|
||||
public static readonly IReadOnlyList<PNLHeaderRowDisplay> header_row_display_options =
|
||||
new[] {
|
||||
PNLHeaderRowDisplay.Total,
|
||||
PNLHeaderRowDisplay.SelectedTotal,
|
||||
PNLHeaderRowDisplay.Total | PNLHeaderRowDisplay.SelectedTotal
|
||||
};
|
||||
|
||||
public static readonly IReadOnlyList<Columns> anchorable_columns =
|
||||
new[] {
|
||||
Columns.Account,
|
||||
Columns.Exchange,
|
||||
Columns.Symbol,
|
||||
Columns.Security
|
||||
};
|
||||
|
||||
[ProtoMember(1)]
|
||||
SortingCriteria[] _sorting_criterion;
|
||||
[ProtoMember(5)]
|
||||
Columns[] _anchors;
|
||||
[ProtoMember(7)]
|
||||
PNLCellAppearanceProto _group_cell_appearance;
|
||||
[ProtoMember(8)]
|
||||
PNLCellAppearanceProto _header_cell_appearance;
|
||||
// don't store in proto yet (this type may change)
|
||||
[ProtoMember(9)]
|
||||
PNLAnimationTriggerData[] _animations;
|
||||
[ProtoMember(10)]
|
||||
PNLAnimationTriggerData[] _group_header_animations;
|
||||
[ProtoMember(11)]
|
||||
GridColumnSettings[] _group_columns;
|
||||
[ProtoMember(12)]
|
||||
GridColumnSettings[] _header_columns;
|
||||
[ProtoMember(13)]
|
||||
PNLAnimationTriggerData[] _header_row_animations;
|
||||
|
||||
PNLCellAppearanceProto _grid_cell_appearance_backing;
|
||||
|
||||
public static PNLSettings Default =>
|
||||
new PNLSettings {
|
||||
_sorting_criterion = new[] {
|
||||
new SortingCriteria(nameof(Columns.Account), true),
|
||||
new SortingCriteria(nameof(Columns.Security), true)
|
||||
},
|
||||
grid = DefaultGridSettings,
|
||||
calculation_type = PNLCalculationType.Waterfall,
|
||||
header_row_display = PNLHeaderRowDisplay.Total | PNLHeaderRowDisplay.SelectedTotal,
|
||||
_anchors = new[] { Columns.Account, Columns.Symbol },
|
||||
_group_cell_appearance = new PNLCellAppearanceProto {
|
||||
foreground = Colors.Black.rgb_color(),
|
||||
font_params = new FontParamsProto {
|
||||
font_family = GridSettings.Default.font_family,
|
||||
font_weight = FontWeights.Bold.ToOpenTypeWeight(),
|
||||
font_size = 11
|
||||
},
|
||||
background = Colors.Transparent.rgb_color()
|
||||
},
|
||||
_header_cell_appearance = new PNLCellAppearanceProto {
|
||||
foreground = Colors.Black.rgb_color(),
|
||||
font_params = new FontParamsProto {
|
||||
font_family = GridSettings.Default.font_family,
|
||||
font_weight = FontWeights.Bold.ToOpenTypeWeight(),
|
||||
font_size = 12
|
||||
},
|
||||
background = Colors.Transparent.rgb_color()
|
||||
}
|
||||
};
|
||||
|
||||
public static GridSettings DefaultGridSettings => GridSettings.Default.with(DefaultColumns.all, DefaultColumns.default_sorting_criterion);
|
||||
|
||||
public static fills.FillsSettings DefaultAuxiliaryFills {
|
||||
get {
|
||||
var grid = fills.FillsSettings.MiniGridSettings;
|
||||
if (grid.columns.TryGetFirst(c => c.id == nameof(mbts.ui.spark.FillsView.Columns.Side), out var side_col, out int side_col_pos)) {
|
||||
var cols = grid.columns.ToList();
|
||||
cols[side_col_pos] = side_col.with(is_visible: false);
|
||||
grid = grid.with(cols);
|
||||
}
|
||||
var rv = fills.FillsSettings.Default();
|
||||
rv.grid = grid;
|
||||
return rv;
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<PNLAnimationTriggerData> default_animations {
|
||||
get {
|
||||
yield return _trigger($"{nameof(PNLRowData.sod_pnl)} > 0", _animation(foreground: BTSColors.PNL.profit_color, font_weight: FontWeights.Bold, column: Columns.SODPNL));
|
||||
yield return _trigger($"{nameof(PNLRowData.sod_pnl)} < 0", _animation(foreground: BTSColors.PNL.loss_color, font_weight: FontWeights.Bold, column: Columns.SODPNL));
|
||||
yield return _trigger($"{nameof(PNLRowData.sod_net_pos)} > 0", _animation(foreground: BTSColors.MarketData.bid_color, column: Columns.SODNetPos));
|
||||
yield return _trigger($"{nameof(PNLRowData.sod_net_pos)} < 0", _animation(foreground: BTSColors.MarketData.ask_color, column: Columns.SODNetPos));
|
||||
// day
|
||||
yield return _trigger($"{nameof(PNLRowData.day_pnl)} > 0", _animation(foreground: BTSColors.PNL.profit_color, font_weight: FontWeights.Bold, column: Columns.DayPNL));
|
||||
yield return _trigger($"{nameof(PNLRowData.day_pnl)} < 0", _animation(foreground: BTSColors.PNL.loss_color, font_weight: FontWeights.Bold, column: Columns.DayPNL));
|
||||
yield return _trigger($"{nameof(PNLRowData.day_net_pos)} > 0", _animation(foreground: BTSColors.MarketData.bid_color, column: Columns.DayNetPos));
|
||||
yield return _trigger($"{nameof(PNLRowData.day_net_pos)} < 0", _animation(foreground: BTSColors.MarketData.ask_color, column: Columns.DayNetPos));
|
||||
// total
|
||||
yield return _trigger($"{nameof(PNLRowData.total_pnl)} > 0", _animation(foreground: BTSColors.PNL.profit_color, font_weight: FontWeights.Bold, column: Columns.TotalPNL));
|
||||
yield return _trigger($"{nameof(PNLRowData.total_pnl)} < 0", _animation(foreground: BTSColors.PNL.loss_color, font_weight: FontWeights.Bold, column: Columns.TotalPNL));
|
||||
yield return _trigger($"{nameof(PNLRowData.total_net_pos)} > 0", _animation(foreground: BTSColors.MarketData.bid_color, column: Columns.TotalNetPos));
|
||||
yield return _trigger($"{nameof(PNLRowData.total_net_pos)} < 0", _animation(foreground: BTSColors.MarketData.ask_color, column: Columns.TotalNetPos));
|
||||
// working
|
||||
yield return _trigger($"{nameof(PNLRowData.working_net_pos)} > 0", _animation(foreground: BTSColors.MarketData.bid_color, column: Columns.WorkingNetPos));
|
||||
yield return _trigger($"{nameof(PNLRowData.working_net_pos)} < 0", _animation(foreground: BTSColors.MarketData.ask_color, column: Columns.WorkingNetPos));
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<PNLAnimationTriggerData> default_group_header_animations {
|
||||
get {
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.sod_pnl)} > 0", _animation(background: BTSColors.PNL.profit_color.with(OpacityLevels.Light), column: Columns.SODPNL));
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.sod_pnl)} < 0", _animation(background: BTSColors.PNL.loss_color.with(OpacityLevels.Light), column: Columns.SODPNL));
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.sod_net_pos)} > 0", _animation(background: BTSColors.MarketData.bid_color.with(OpacityLevels.Medium), column: Columns.SODNetPos));
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.sod_net_pos)} < 0", _animation(background: BTSColors.MarketData.ask_color.with(OpacityLevels.Medium), column: Columns.SODNetPos));
|
||||
// day
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.day_pnl)} > 0", _animation(background: BTSColors.PNL.profit_color.with(OpacityLevels.Light), column: Columns.DayPNL));
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.day_pnl)} < 0", _animation(background: BTSColors.PNL.loss_color.with(OpacityLevels.Light), column: Columns.DayPNL));
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.day_net_pos)} > 0", _animation(background: BTSColors.MarketData.bid_color.with(OpacityLevels.Medium), column: Columns.DayNetPos));
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.day_net_pos)} < 0", _animation(background: BTSColors.MarketData.ask_color.with(OpacityLevels.Medium), column: Columns.DayNetPos));
|
||||
// total
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.total_pnl)} > 0", _animation(background: BTSColors.PNL.profit_color.with(OpacityLevels.Light), column: Columns.TotalPNL));
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.total_pnl)} < 0", _animation(background: BTSColors.PNL.loss_color.with(OpacityLevels.Light), column: Columns.TotalPNL));
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.total_net_pos)} > 0", _animation(background: BTSColors.MarketData.bid_color.with(OpacityLevels.Medium), column: Columns.TotalNetPos));
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.total_net_pos)} < 0", _animation(background: BTSColors.MarketData.ask_color.with(OpacityLevels.Medium), column: Columns.TotalNetPos));
|
||||
// working
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.working_net_pos)} > 0", _animation(background: BTSColors.MarketData.bid_color.with(OpacityLevels.Medium), column: Columns.WorkingNetPos));
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.working_net_pos)} < 0", _animation(background: BTSColors.MarketData.ask_color.with(OpacityLevels.Medium), column: Columns.WorkingNetPos));
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<PNLAnimationTriggerData> default_header_row_animations {
|
||||
get {
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.sod_pnl)} > 0", _animation(background: BTSColors.PNL.profit_color.with(OpacityLevels.Light), column: Columns.SODPNL));
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.sod_pnl)} < 0", _animation(background: BTSColors.PNL.loss_color.with(OpacityLevels.Light), column: Columns.SODPNL));
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.sod_net_pos)} > 0", _animation(background: BTSColors.MarketData.bid_color.with(OpacityLevels.Medium), column: Columns.SODNetPos));
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.sod_net_pos)} < 0", _animation(background: BTSColors.MarketData.ask_color.with(OpacityLevels.Medium), column: Columns.SODNetPos));
|
||||
// day
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.day_pnl)} > 0", _animation(background: BTSColors.PNL.profit_color.with(OpacityLevels.Light), column: Columns.DayPNL));
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.day_pnl)} < 0", _animation(background: BTSColors.PNL.loss_color.with(OpacityLevels.Light), column: Columns.DayPNL));
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.day_net_pos)} > 0", _animation(background: BTSColors.MarketData.bid_color.with(OpacityLevels.Medium), column: Columns.DayNetPos));
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.day_net_pos)} < 0", _animation(background: BTSColors.MarketData.ask_color.with(OpacityLevels.Medium), column: Columns.DayNetPos));
|
||||
// total
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.total_pnl)} > 0", _animation(background: BTSColors.PNL.profit_color.with(OpacityLevels.Light), column: Columns.TotalPNL));
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.total_pnl)} < 0", _animation(background: BTSColors.PNL.loss_color.with(OpacityLevels.Light), column: Columns.TotalPNL));
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.total_net_pos)} > 0", _animation(background: BTSColors.MarketData.bid_color.with(OpacityLevels.Medium), column: Columns.TotalNetPos));
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.total_net_pos)} < 0", _animation(background: BTSColors.MarketData.ask_color.with(OpacityLevels.Medium), column: Columns.TotalNetPos));
|
||||
// working
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.working_net_pos)} > 0", _animation(background: BTSColors.MarketData.bid_color.with(OpacityLevels.Medium), column: Columns.WorkingNetPos));
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.working_net_pos)} < 0", _animation(background: BTSColors.MarketData.ask_color.with(OpacityLevels.Medium), column: Columns.WorkingNetPos));
|
||||
}
|
||||
}
|
||||
|
||||
[ProtoMember(2)]
|
||||
public FilterStates filters { get; set; }
|
||||
[ProtoMember(3)]
|
||||
public PNLCalculationType calculation_type { get; set; }
|
||||
[ProtoMember(4)]
|
||||
public GridSettings grid { get; set; }
|
||||
[ProtoMember(6)]
|
||||
public PNLHeaderRowDisplay header_row_display { get; set; }
|
||||
|
||||
public PNLCellAppearance grid_defaults =>
|
||||
new PNLCellAppearance(
|
||||
font_family: new FontFamily(grid.font_family),
|
||||
font_size: grid.font_size,
|
||||
font_weight: grid.font_weight,
|
||||
background: grid.background,
|
||||
foreground: grid.font_color.rgb_color());
|
||||
|
||||
public PNLCellAppearanceProto group_cell_appearance {
|
||||
get => _group_cell_appearance ?? _grid_cell_appearance;
|
||||
set => _group_cell_appearance = value;
|
||||
}
|
||||
public PNLCellAppearanceProto header_cell_appearance {
|
||||
get => _header_cell_appearance ?? _grid_cell_appearance;
|
||||
set => _header_cell_appearance = value;
|
||||
}
|
||||
|
||||
public IReadOnlyList<SortingCriteria> sorting_criterion => _sorting_criterion ?? (_sorting_criterion = DefaultColumns.default_sorting_criterion.ToArray());
|
||||
public IReadOnlyList<Columns> anchors => _anchors ?? (_anchors = Array.Empty<Columns>());
|
||||
public IReadOnlyList<PNLAnimationTriggerData> animations => _animations ?? (_animations = default_animations.ToArray());
|
||||
public IReadOnlyList<PNLAnimationTriggerData> group_header_animations => _group_header_animations ?? (_group_header_animations = default_group_header_animations.ToArray());
|
||||
public IReadOnlyList<PNLAnimationTriggerData> header_row_animations => _header_row_animations ?? (_header_row_animations = default_header_row_animations.ToArray());
|
||||
public IReadOnlyList<GridColumnSettings> group_columns => _group_columns ?? (_group_columns = DefaultColumns.group_columns.ToArray());
|
||||
public IReadOnlyList<GridColumnSettings> header_columns => _header_columns ?? (_header_columns = DefaultColumns.header_columns.ToArray());
|
||||
|
||||
PNLCellAppearanceProto _grid_cell_appearance => _grid_cell_appearance_backing ?? (
|
||||
_grid_cell_appearance_backing = new PNLCellAppearanceProto {
|
||||
background = grid.background.rgb_color(),
|
||||
foreground = grid.font_color,
|
||||
font_params = new FontParamsProto {
|
||||
//font_color = grid.font_color,
|
||||
font_family = grid.font_family,
|
||||
font_weight = grid.font_weight.ToOpenTypeWeight(),
|
||||
font_size = grid.font_size
|
||||
}
|
||||
});
|
||||
|
||||
public PNLSettings with(
|
||||
IEnumerable<SortingCriteria> sorting_criterion = null,
|
||||
IEnumerable<Columns> anchors = null,
|
||||
FilterStates filters = null,
|
||||
GridSettings grid = null,
|
||||
PNLCalculationType? calculation_type = null,
|
||||
PNLHeaderRowDisplay? header_row_display = null,
|
||||
IEnumerable<GridColumnSettings> group_columns = null,
|
||||
IEnumerable<PNLAnimationTriggerData> animations = null,
|
||||
IEnumerable<PNLAnimationTriggerData> group_header_animations = null,
|
||||
IEnumerable<PNLAnimationTriggerData> header_row_animations = null,
|
||||
IEnumerable<GridColumnSettings> header_columns = null) =>
|
||||
new PNLSettings {
|
||||
_sorting_criterion = (sorting_criterion ?? _sorting_criterion.empty_if_null()).ToArray(),
|
||||
_anchors = (anchors ?? _anchors.empty_if_null()).ToArray(),
|
||||
filters = filters ?? this.filters,
|
||||
grid = grid ?? this.grid,
|
||||
calculation_type = calculation_type ?? this.calculation_type,
|
||||
header_row_display = header_row_display ?? this.header_row_display,
|
||||
_group_columns = group_columns?.ToArray() ?? _group_columns,
|
||||
_header_columns = header_columns?.ToArray() ?? _header_columns,
|
||||
_animations = animations?.ToArray() ?? _animations,
|
||||
_group_header_animations = group_header_animations?.ToArray() ?? _group_header_animations,
|
||||
_header_row_animations = header_row_animations?.ToArray() ?? _header_row_animations,
|
||||
_group_cell_appearance = _group_cell_appearance,
|
||||
_header_cell_appearance = _header_cell_appearance
|
||||
};
|
||||
|
||||
public bool Equals_orig(PNLSettings other) =>
|
||||
other != null &&
|
||||
calculation_type == other.calculation_type &&
|
||||
filters.safe_equals(other.filters) &&
|
||||
header_row_display == other.header_row_display &&
|
||||
header_cell_appearance.safe_equals(other.header_cell_appearance) &&
|
||||
group_cell_appearance.safe_equals(other.group_cell_appearance) &&
|
||||
anchors.SequenceEqual(other.anchors) &&
|
||||
group_columns.SequenceEqual(other.group_columns) &&
|
||||
animations.SequenceEqual(other.animations) &&
|
||||
group_header_animations.SequenceEqual(other.group_header_animations) &&
|
||||
grid.safe_equals(other.grid);
|
||||
|
||||
public bool Equals(PNLSettings other) {
|
||||
if (other == null)
|
||||
return false;
|
||||
if (calculation_type != other.calculation_type)
|
||||
return false;
|
||||
if (!filters.safe_equals(other.filters))
|
||||
return false;
|
||||
if (!header_cell_appearance.safe_equals(other.header_cell_appearance))
|
||||
return false;
|
||||
if (!group_cell_appearance.safe_equals(other.group_cell_appearance))
|
||||
return false;
|
||||
if (!group_columns.SequenceEqual(other.group_columns))
|
||||
return false;
|
||||
if (!anchors.SequenceEqual(other.anchors))
|
||||
return false;
|
||||
if (!animations.SequenceEqual(other.animations))
|
||||
return false;
|
||||
if (!Equals_orig(other))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
public override bool Equals(object obj) =>
|
||||
Equals(obj as PNLSettings);
|
||||
|
||||
public override int GetHashCode() => 583;
|
||||
|
||||
static PNLAnimationTriggerData _trigger(string expression, params PNLAnimation[] animations) =>
|
||||
new PNLAnimationTriggerData {
|
||||
expression = expression,
|
||||
animations = animations
|
||||
};
|
||||
|
||||
static PNLAnimation _animation(
|
||||
Columns column,
|
||||
Color? foreground = null,
|
||||
Color? background = null,
|
||||
FontWeight? font_weight = null) =>
|
||||
new PNLAnimation {
|
||||
column = column,
|
||||
foreground = foreground?.rgb_color(),
|
||||
background = background?.rgb_color(),
|
||||
font_weight = font_weight?.ToOpenTypeWeight()
|
||||
};
|
||||
|
||||
static class DefaultColumns {
|
||||
public static GridColumnSettings account => new GridColumnSettings(nameof(Columns.Account), DefaultColumnHeaders.Account, width: Widths.Account);
|
||||
public static GridColumnSettings exchange => new GridColumnSettings(nameof(Columns.Exchange), DefaultColumnHeaders.Exchange, false, width: Widths.Exchange);
|
||||
public static GridColumnSettings symbol => new GridColumnSettings(nameof(Columns.Symbol), DefaultColumnHeaders.Symbol, width: Widths.Symbol);
|
||||
public static GridColumnSettings security => new GridColumnSettings(nameof(Columns.Security), DefaultColumnHeaders.Security, width: Widths.Security);
|
||||
public static GridColumnSettings security_type => new GridColumnSettings(nameof(Columns.SecurityType), Headers.SecurityType, width: Widths.SecurityType);
|
||||
// sod
|
||||
public static GridColumnSettings sod_pnl => new GridColumnSettings(nameof(Columns.SODPNL), Headers.SOD.PNL, false, width: Widths.PriceBig, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings sod_long_pos => new GridColumnSettings(nameof(Columns.SODLongPos), Headers.SOD.Long, false, width: Widths.Qty);
|
||||
public static GridColumnSettings sod_long_avg_price => new GridColumnSettings(nameof(Columns.SODLongAvgPrice), Headers.SOD.LongAvg, false, width: Widths.Price, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings sod_long_avg_price_delta => new GridColumnSettings(nameof(Columns.SODLongAvgPriceDelta), Headers.SOD.LongAvgDelta, false, width: Widths.Price, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings sod_short_pos => new GridColumnSettings(nameof(Columns.SODShortPos), Headers.SOD.Short, false, width: Widths.Qty);
|
||||
public static GridColumnSettings sod_short_avg_price => new GridColumnSettings(nameof(Columns.SODShortAvgPrice), Headers.SOD.ShortAvg, false, width: Widths.Price, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings sod_short_avg_price_delta => new GridColumnSettings(nameof(Columns.SODShortAvgPriceDelta), Headers.SOD.ShortAvgDelta, false, width: Widths.Price, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings sod_net_pos => new GridColumnSettings(nameof(Columns.SODNetPos), Headers.SOD.Net, false, width: Widths.Qty);
|
||||
public static GridColumnSettings sod_net_avg_price_delta => new GridColumnSettings(nameof(Columns.SODNetAvgPriceDelta), Headers.SOD.NetAvgDelta, false, width: Widths.Price, horizontal_alignment: HorizontalAlignment.Right);
|
||||
// day
|
||||
public static GridColumnSettings day_pnl => new GridColumnSettings(nameof(Columns.DayPNL), Headers.Day.PNL, width: Widths.PriceBig, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings day_long_pos => new GridColumnSettings(nameof(Columns.DayLongPos), Headers.Day.Long, false, width: Widths.Qty);
|
||||
public static GridColumnSettings day_long_avg_price => new GridColumnSettings(nameof(Columns.DayLongAvgPrice), Headers.Day.LongAvg, false, width: Widths.Price, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings day_long_avg_price_delta => new GridColumnSettings(nameof(Columns.DayLongAvgPriceDelta), Headers.Day.LongAvgDelta, false, width: Widths.Price, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings day_short_pos => new GridColumnSettings(nameof(Columns.DayShortPos), Headers.Day.Short, false, width: Widths.Qty);
|
||||
public static GridColumnSettings day_short_avg_price => new GridColumnSettings(nameof(Columns.DayShortAvgPrice), Headers.Day.ShortAvg, false, width: Widths.Price, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings day_short_avg_price_delta => new GridColumnSettings(nameof(Columns.DayShortAvgPriceDelta), Headers.Day.ShortAvgDelta, false, width: Widths.Price, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings day_net_pos => new GridColumnSettings(nameof(Columns.DayNetPos), Headers.Day.Net, width: Widths.Qty);
|
||||
public static GridColumnSettings day_net_avg_price_delta => new GridColumnSettings(nameof(Columns.DayNetAvgPriceDelta), Headers.Day.NetAvgDelta, false, width: Widths.Price, horizontal_alignment: HorizontalAlignment.Right);
|
||||
// working
|
||||
public static GridColumnSettings working_long_pos => new GridColumnSettings(nameof(Columns.WorkingLongPos), Headers.Working.Long, false, width: Widths.WorkingQty, font_color_override: BTSColors.MarketData.bid);
|
||||
public static GridColumnSettings working_long_avg_price => new GridColumnSettings(nameof(Columns.WorkingLongAvgPrice), Headers.Working.LongAvg, false, width: Widths.Qty, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings working_long_avg_price_delta => new GridColumnSettings(nameof(Columns.WorkingLongAvgPriceDelta), Headers.Working.LongAvgDelta, false, width: Widths.Qty, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings working_short_pos => new GridColumnSettings(nameof(Columns.WorkingShortPos), Headers.Working.Short, false, width: Widths.WorkingQty, font_color_override: BTSColors.MarketData.ask);
|
||||
public static GridColumnSettings working_short_avg_price => new GridColumnSettings(nameof(Columns.WorkingShortAvgPrice), Headers.Working.ShortAvg, false, width: Widths.Qty, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings working_short_avg_price_delta => new GridColumnSettings(nameof(Columns.WorkingShortAvgPriceDelta), Headers.Working.ShortAvgDelta, false, width: Widths.Qty, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings working_net_pos => new GridColumnSettings(nameof(Columns.WorkingNetPos), Headers.Working.Net, width: Widths.WorkingQty);
|
||||
public static GridColumnSettings working_net_avg_price_delta => new GridColumnSettings(nameof(Columns.WorkingNetAvgPriceDelta), Headers.Working.NetAvgDelta, false, width: Widths.Qty, horizontal_alignment: HorizontalAlignment.Right);
|
||||
// total
|
||||
public static GridColumnSettings total_pnl => new GridColumnSettings(nameof(Columns.TotalPNL), Headers.Total.PNL, false, width: Widths.PriceBig, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings total_long_pos => new GridColumnSettings(nameof(Columns.TotalLongPos), Headers.Total.Long, false, width: Widths.Qty);
|
||||
public static GridColumnSettings total_long_avg_price => new GridColumnSettings(nameof(Columns.TotalLongAvgPrice), Headers.Total.LongAvg, false, width: Widths.Price, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings total_long_avg_price_delta => new GridColumnSettings(nameof(Columns.TotalLongAvgPriceDelta), Headers.Total.LongAvgDelta, false, width: Widths.Price, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings total_short_pos => new GridColumnSettings(nameof(Columns.TotalShortPos), Headers.Total.Short, false, width: Widths.Qty);
|
||||
public static GridColumnSettings total_short_avg_price => new GridColumnSettings(nameof(Columns.TotalShortAvgPrice), Headers.Total.ShortAvg, false, width: Widths.Price, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings total_short_avg_price_delta => new GridColumnSettings(nameof(Columns.TotalShortAvgPriceDelta), Headers.Total.ShortAvgDelta, false, width: Widths.Price, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings total_net_pos => new GridColumnSettings(nameof(Columns.TotalNetPos), Headers.Total.Net, false, width: Widths.Qty);
|
||||
public static GridColumnSettings total_net_avg_price_delta => new GridColumnSettings(nameof(Columns.TotalNetAvgPriceDelta), Headers.Total.NetAvgDelta, false, width: Widths.Price, horizontal_alignment: HorizontalAlignment.Right);
|
||||
// market
|
||||
public static GridColumnSettings bid_price => new GridColumnSettings(nameof(Columns.BidPrice), DefaultColumnHeaders.Bid, false, width: DefaultColumnWidths.Price, font_color_override: BTSColors.MarketData.bid, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings ask_price => new GridColumnSettings(nameof(Columns.AskPrice), DefaultColumnHeaders.Ask, false, width: DefaultColumnWidths.Price, font_color_override: BTSColors.MarketData.ask, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings mid_price => new GridColumnSettings(nameof(Columns.MidPrice), DefaultColumnHeaders.Mid, false, width: DefaultColumnWidths.Price, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings last_price => new GridColumnSettings(nameof(Columns.LastPrice), DefaultColumnHeaders.Last, false, width: DefaultColumnWidths.Price, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings settlement_price => new GridColumnSettings(nameof(Columns.SettlementPrice), DefaultColumnHeaders.Settlement, false, width: DefaultColumnWidths.Settlement, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings pnl_price => new GridColumnSettings(nameof(Columns.PNLPrice), DefaultColumnHeaders.PnLPrice, width: DefaultColumnWidths.Price, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings cpp => new GridColumnSettings(nameof(Columns.CPP), "CPP", false, width: 50, horizontal_alignment: HorizontalAlignment.Right);
|
||||
// misc
|
||||
public static GridColumnSettings has_day_trades => new GridColumnSettings(nameof(Columns.HasDayTrades), Headers.HasDayTrades, false, width: Widths.HasDayTrades);
|
||||
|
||||
public static IEnumerable<SortingCriteria> default_sorting_criterion {
|
||||
get {
|
||||
yield return new SortingCriteria(account.id, true);
|
||||
yield return new SortingCriteria(security.id, true);
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<GridColumnSettings> all {
|
||||
get {
|
||||
yield return account;
|
||||
yield return exchange;
|
||||
yield return symbol;
|
||||
yield return security;
|
||||
yield return security_type;
|
||||
|
||||
yield return total_pnl; // hidden
|
||||
yield return sod_pnl;
|
||||
yield return day_pnl;
|
||||
/// total
|
||||
yield return total_net_pos;
|
||||
yield return total_net_avg_price_delta; // hidden
|
||||
// total long
|
||||
yield return total_long_pos; // hidden
|
||||
yield return total_long_avg_price; // hidden
|
||||
yield return total_long_avg_price_delta; // hidden
|
||||
// total short
|
||||
yield return total_short_pos; // hidden
|
||||
yield return total_short_avg_price; // hidden
|
||||
yield return total_short_avg_price_delta; // hidden
|
||||
/// sod
|
||||
yield return sod_net_pos;
|
||||
yield return sod_net_avg_price_delta; // hidden
|
||||
// sod long
|
||||
yield return sod_long_pos; // hidden
|
||||
yield return sod_long_avg_price; // hidden
|
||||
yield return sod_long_avg_price_delta; // hidden
|
||||
// sod short
|
||||
yield return sod_short_pos; // hidden
|
||||
yield return sod_short_avg_price; // hidden
|
||||
yield return sod_short_avg_price_delta; // hidden
|
||||
/// day
|
||||
yield return day_net_pos;
|
||||
yield return day_net_avg_price_delta; // hidden
|
||||
// day long
|
||||
yield return day_long_pos; // hidden
|
||||
yield return day_long_avg_price; // hidden
|
||||
yield return day_long_avg_price_delta; // hidden
|
||||
// day short
|
||||
yield return day_short_pos; // hidden
|
||||
yield return day_short_avg_price; // hidden
|
||||
yield return day_short_avg_price_delta; // hidden
|
||||
|
||||
yield return has_day_trades;
|
||||
/// working
|
||||
// working net
|
||||
yield return working_net_pos;
|
||||
yield return working_net_avg_price_delta;
|
||||
// working long
|
||||
yield return working_long_pos;
|
||||
yield return working_long_avg_price;
|
||||
yield return working_long_avg_price_delta;
|
||||
// working short
|
||||
yield return working_short_pos;
|
||||
yield return working_short_avg_price;
|
||||
yield return working_short_avg_price_delta;
|
||||
/// market
|
||||
yield return bid_price;
|
||||
yield return ask_price;
|
||||
yield return mid_price;
|
||||
yield return last_price;
|
||||
yield return settlement_price;
|
||||
yield return pnl_price;
|
||||
yield return cpp;
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<GridColumnSettings> group_columns {
|
||||
get {
|
||||
var rv = all.Select(cs => cs.with(font_weight_override: new NullableFontWeightProto(FontWeights.Bold))).ToList();
|
||||
GridSettingsHelper.try_replace_column(rv, Columns.SODPNL, _override_color);
|
||||
GridSettingsHelper.try_replace_column(rv, Columns.DayPNL, _override_color);
|
||||
return rv;
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<GridColumnSettings> header_columns {
|
||||
get {
|
||||
var rv = all.Select(cs => cs.with(font_weight_override: new NullableFontWeightProto(FontWeights.Bold))).ToList();
|
||||
GridSettingsHelper.try_replace_column(rv, Columns.SODPNL, _override_color);
|
||||
GridSettingsHelper.try_replace_column(rv, Columns.DayPNL, _override_color);
|
||||
return rv;
|
||||
}
|
||||
}
|
||||
|
||||
static GridColumnSettings _override_color(GridColumnSettings col_settings) =>
|
||||
_override_color(col_settings, 0);
|
||||
|
||||
static GridColumnSettings _override_color(GridColumnSettings col_settings, uint color) =>
|
||||
col_settings.with(font_color_override: new NullableColorProto(color));
|
||||
}
|
||||
|
||||
static class OpacityLevels {
|
||||
public const byte Light = 0x55, Medium = 0x6A;
|
||||
}
|
||||
}
|
||||
|
||||
static class PNLSettingsExtensions {
|
||||
public static IReadOnlyList<IPNLAnimationTrigger<PNLRowData>> triggers(this PNLSettings settings) => settings.animations.Select(a => a.to_trigger<PNLRowData>()).ToList();
|
||||
public static IReadOnlyList<IPNLAnimationTrigger<PNLNormalizedData>> group_triggers(this PNLSettings settings) => settings.group_header_animations.Select(a => a.to_trigger<PNLNormalizedData>()).ToList();
|
||||
public static IReadOnlyList<IPNLAnimationTrigger<PNLNormalizedData>> header_triggers(this PNLSettings settings) => settings.header_row_animations.Select(a => a.to_trigger<PNLNormalizedData>()).ToList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
using bts.collections;
|
||||
using bts.utils;
|
||||
using mbts;
|
||||
using mbts.collections;
|
||||
using mbts.expression;
|
||||
using mbts.spark;
|
||||
using mbts.ui;
|
||||
using mbts.ui.settings;
|
||||
using mbts.ui.shell;
|
||||
using mbts.ui.spark;
|
||||
using mbts.ui.utility;
|
||||
using mbts.utils;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Specialized;
|
||||
using System.ComponentModel;
|
||||
using System.Dynamic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using ColumnKeys = bts.shell.pnl.PNLView.Columns;
|
||||
|
||||
namespace bts.shell.pnl {
|
||||
class PNLSettingsVM : DisposableViewModelBase, IWindowSettingsVM<PNLSettings>, IValueFactory<PNLSettings>, IObservable<PNLSettings>, ITemplateSelectorKey {
|
||||
readonly IReadOnlyDictionary<ColumnKeys, AnchorItemVM> _anchor_cache;
|
||||
readonly DeferrableActionWrapper
|
||||
_sync_anchors,
|
||||
_update;
|
||||
|
||||
PNLCalculationType _calculation_type;
|
||||
PNLHeaderRowDisplay _header_row_display;
|
||||
PNLSettings _last_loaded;
|
||||
|
||||
public PNLSettingsVM(PNLSettings settings) {
|
||||
// init anchor cache, deferrable resync
|
||||
IDictionary<string, object> raw_anchor_cache = new ExpandoObject();
|
||||
raw_anchor_cache.AddRange(PNLSettings.anchorable_columns
|
||||
.Select(k => new KeyValuePair<string, object>(k.ToString(), new AnchorItemVM(this, k))));
|
||||
_anchor_cache =
|
||||
MappedDictionary.create(
|
||||
new ReadOnlyDictionary<string, object>(anchor_vm = raw_anchor_cache),
|
||||
ConvertUtils.CastDelegate<object, AnchorItemVM>.value,
|
||||
EnumHelper.parse<ColumnKeys>,
|
||||
k => k.ToString());
|
||||
_sync_anchors = new DeferrableActionWrapper(() => {
|
||||
for (int i = 0; i < anchor_items.Count; i++) {
|
||||
anchor_items[i].anchor_index = i + 1;
|
||||
anchor_items[i].refresh();
|
||||
}
|
||||
});
|
||||
_update = new DeferrableActionWrapper(() => result_changed?.Invoke());
|
||||
core_settings_vm = new SimpleGridSettingsVM(column_factory: col_settings => {
|
||||
var col_vm = new PNLColumnVM(core_settings_vm, EnumHelper.parse<ColumnKeys>(col_settings.id));
|
||||
col_vm.load_from(col_settings);
|
||||
return col_vm;
|
||||
}) {
|
||||
can_edit_background = true
|
||||
};
|
||||
core_settings_vm.selection_highlight.bind_opacity_to_alpha = true;
|
||||
core_settings_vm.result_changed += _update.execute;
|
||||
_dispose = Disposable.Create(
|
||||
anchor_items.Subscribe((s, e) => {
|
||||
_update.execute();
|
||||
_sync_anchors.execute();
|
||||
}),
|
||||
ObjectSnapshot.connect_property_changed(_observable_components, (s, e) => _update.execute()),
|
||||
Disposable.Create(() => core_settings_vm.result_changed -= _update.execute));
|
||||
load(settings);
|
||||
}
|
||||
|
||||
public event Action result_changed;
|
||||
|
||||
public dynamic anchor_vm { get; }
|
||||
public ObservableCollection<AnchorItemVM> anchor_items { get; } = new ObservableCollection<AnchorItemVM>();
|
||||
public SimpleGridSettingsVM core_settings_vm { get; }
|
||||
public PNLCellAppearanceParamsVM group_row_defaults { get; } = new PNLCellAppearanceParamsVM();
|
||||
public PNLCellAppearanceParamsVM header_row_defaults { get; } = new PNLCellAppearanceParamsVM();
|
||||
public PNLCalculationType calculation_type { get => _calculation_type; set => Set(ref _calculation_type, value); }
|
||||
public IEnumerable<PNLCalculationType> calculation_types { get; } = EnumHelper.get_values_except(PNLCalculationType.None);
|
||||
public PNLHeaderRowDisplay header_row_display { get => _header_row_display; set => Set(ref _header_row_display, value); }
|
||||
public IEnumerable<PNLHeaderRowDisplay> header_row_display_options => PNLSettings.header_row_display_options;
|
||||
|
||||
IEnumerable<INotifyPropertyChanged> _observable_components {
|
||||
get {
|
||||
yield return core_settings_vm;
|
||||
yield return group_row_defaults;
|
||||
yield return header_row_defaults;
|
||||
yield return group_row_defaults.background;
|
||||
yield return header_row_defaults.background;
|
||||
yield return this;
|
||||
}
|
||||
}
|
||||
string ITemplateSelectorKey.key => GridSettingsTemplateKeys.PNL;
|
||||
|
||||
public PNLSettings get_result() {
|
||||
var s = _last_loaded == null ? new PNLSettings() : SparkUtils.clone_proto(_last_loaded);
|
||||
s.calculation_type = _calculation_type;
|
||||
s.grid = core_settings_vm.create();
|
||||
var trigger_anims = core_settings_vm.columns
|
||||
.OfType<PNLColumnVM>()
|
||||
.SelectMany(c => c.get_triggers())
|
||||
.GroupBy(tup => tup.expression)
|
||||
.Where(g => CoreExpressionEvaluator<PNLRowData>.validate(g.Key))
|
||||
.Select(g => (
|
||||
expression: g.Key,
|
||||
animations: g.ToArray()))
|
||||
.ToList();
|
||||
|
||||
var normalized_anims = trigger_anims
|
||||
.Where(tup => CoreExpressionEvaluator<PNLNormalizedData>.validate(tup.expression))
|
||||
.ToList();
|
||||
|
||||
s = s.with(anchors: anchor_items.Select(vm => vm.key),
|
||||
group_columns: core_settings_vm.columns.OfType<PNLColumnVM>().Select(c => {
|
||||
var col_settings = c.create();
|
||||
col_settings = c.group_column_vm.apply_to(col_settings);
|
||||
return col_settings;
|
||||
}),
|
||||
header_columns: core_settings_vm.columns.OfType<PNLColumnVM>().Select(c => {
|
||||
var col_settings = c.create();
|
||||
col_settings = c.header_column_vm.apply_to(col_settings);
|
||||
return col_settings;
|
||||
}),
|
||||
animations: trigger_anims.Select(
|
||||
ta => new PNLAnimationTriggerData {
|
||||
expression = ta.expression,
|
||||
animations = Array.ConvertAll(ta.animations, t => t.row)
|
||||
}),
|
||||
group_header_animations: normalized_anims
|
||||
.Select(
|
||||
ta => new PNLAnimationTriggerData {
|
||||
expression = ta.expression,
|
||||
animations = Array.ConvertAll(ta.animations, t => t.group)
|
||||
}),
|
||||
header_row_animations: normalized_anims.Select(
|
||||
ta => new PNLAnimationTriggerData {
|
||||
expression = ta.expression,
|
||||
animations = Array.ConvertAll(ta.animations, t => t.header)
|
||||
}));
|
||||
s.header_row_display = header_row_display;
|
||||
s.header_cell_appearance = PNLCellAppearanceProto.from_appearance(header_row_defaults.create());
|
||||
s.group_cell_appearance = PNLCellAppearanceProto.from_appearance(group_row_defaults.create());
|
||||
return s;
|
||||
}
|
||||
|
||||
public void load(PNLSettings value) {
|
||||
using (_update.defer_execution()) {
|
||||
_last_loaded = value;
|
||||
header_row_defaults.load_from(value.header_cell_appearance.to_appearance());
|
||||
group_row_defaults.load_from(value.group_cell_appearance.to_appearance());
|
||||
var column_map = _map_column_data(value.grid.columns, value.animations);
|
||||
var group_column_map = _map_column_data(value.group_columns, value.group_header_animations);
|
||||
var header_column_map = _map_column_data(value.header_columns, value.header_row_animations);
|
||||
|
||||
core_settings_vm.load_from(value.grid);
|
||||
|
||||
for (int i = 0; i < core_settings_vm.columns.Count; i++) {
|
||||
var col_vm = core_settings_vm.columns[i] as PNLColumnVM ?? throw new InvalidOperationException($"In {nameof(PNLSettingsVM)}, all columns should be of type {typeof(PNLColumnVM)}.");
|
||||
var key = EnumHelper.parse<ColumnKeys>(col_vm.id);
|
||||
var (col_settings, trigger_animations) = column_map[key];
|
||||
var group_data = group_column_map.GetNullable(key);
|
||||
var header_data = header_column_map.GetNullable(key);
|
||||
col_vm.load_from(col_settings, trigger_animations, group_data?.settings, group_data?.triggers, header_data?.triggers, header_data?.settings);
|
||||
}
|
||||
using (_sync_anchors.defer_execution())
|
||||
anchor_items.WrapSelector(a => a.key, k => _anchor_cache[k]).sync_with(value.anchors.Intersect(PNLSettings.anchorable_columns));
|
||||
header_row_display = value.header_row_display;
|
||||
calculation_type = value.calculation_type;
|
||||
}
|
||||
//var result = get_result();
|
||||
//if (!value.Equals(result)) {
|
||||
// compare_vals(result, value, typeof(PNLSettings));
|
||||
//}
|
||||
//void compare_vals(object a, object b, Type t) {
|
||||
// if (a == null || b == null) {
|
||||
// return;
|
||||
// }
|
||||
// var props = t.GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
|
||||
// foreach (var p in props) {
|
||||
// var av = p.GetValue(a);
|
||||
// var bv = p.GetValue(b);
|
||||
// if (av is IEnumerable avals) {
|
||||
// if (bv is IEnumerable bvals) {
|
||||
// var gen_t = ReflectionHelper.GetGenericTypeArgs(p.PropertyType, typeof(IEnumerable<>));
|
||||
// if (gen_t != null) {
|
||||
// var aen = avals.GetEnumerator();
|
||||
// var ben = bvals.GetEnumerator();
|
||||
// while (aen.MoveNext()) {
|
||||
// if (!ben.MoveNext()) {
|
||||
// goto execute;
|
||||
// }
|
||||
// compare_vals(aen.Current, ben.Current, gen_t.First());
|
||||
// }
|
||||
// goto finish;
|
||||
// execute:
|
||||
// continue;
|
||||
// finish:
|
||||
// if (ben.MoveNext()) {
|
||||
|
||||
// }
|
||||
// continue;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// if (!Equals(av, bv)) {
|
||||
// if (!Equals(av, bv)) {
|
||||
// compare_vals(av, bv, p.PropertyType);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
}
|
||||
|
||||
PNLSettings IValueFactory<PNLSettings>.create() => get_result();
|
||||
void IValueFactory<PNLSettings>.load_from(PNLSettings value) => load(value);
|
||||
IDisposable IObservable<PNLSettings>.Subscribe(IObserver<PNLSettings> observer) {
|
||||
result_changed += notify;
|
||||
return Disposable.Create(() => result_changed -= notify);
|
||||
void notify() => observer.OnNext(get_result());
|
||||
}
|
||||
|
||||
static Dictionary<ColumnKeys, (GridColumnSettings settings, Dictionary<string, PNLAnimation> triggers)> _map_column_data(IEnumerable<GridColumnSettings> columns, IEnumerable<PNLAnimationTriggerData> triggers) {
|
||||
var map = columns.ToDictionary(
|
||||
c => c.strong_id<ColumnKeys>(),
|
||||
c => (settings: c, triggers: new Dictionary<string, PNLAnimation>()));
|
||||
foreach (var trig in triggers) {
|
||||
foreach (var anim in trig.animations) {
|
||||
if (map.TryGetValue(anim.column, out var col_data))
|
||||
col_data.triggers[trig.expression] = anim;
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
void _append_or_remove_anchor(ColumnKeys key, bool include) {
|
||||
if (!_anchor_cache.TryGetValue(key, out var vm))
|
||||
throw new KeyNotFoundException($"Column key '{key}' is not anchorable (expected one of: {string.Join(", ", PNLSettings.anchorable_columns)}).");
|
||||
if (vm.include == include)
|
||||
return;
|
||||
if (include) {
|
||||
if (anchor_items.Any(a => a.key == key))
|
||||
throw new InvalidOperationException($"{nameof(anchor_items)} already contains an element with key '{key}.'");
|
||||
anchor_items.Add(vm);
|
||||
vm.anchor_index = anchor_items.Count;
|
||||
} else {
|
||||
if (!anchor_items.Remove(vm))
|
||||
throw new InvalidOperationException($"{nameof(anchor_items)} does not contain an element with key '{key}.'");
|
||||
vm.anchor_index = null;
|
||||
}
|
||||
_sync_anchors.execute();
|
||||
}
|
||||
|
||||
public class AnchorItemVM : ViewModelBase, IEquatable<AnchorItemVM> {
|
||||
readonly PNLSettingsVM _owner;
|
||||
int? _anchor_index;
|
||||
|
||||
public AnchorItemVM(PNLSettingsVM owner, ColumnKeys key) {
|
||||
_owner = owner;
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
public ColumnKeys key { get; }
|
||||
public bool include { get => anchor_index.HasValue; set => _owner._append_or_remove_anchor(key, value); }
|
||||
public int? anchor_index { get => _anchor_index; set => Set(ref _anchor_index, value); }
|
||||
|
||||
public void refresh() =>
|
||||
RaisePropertyChanged(nameof(include), nameof(anchor_index));
|
||||
|
||||
public bool Equals(AnchorItemVM other) =>
|
||||
other != null && key == other.key;
|
||||
|
||||
public override bool Equals(object obj) =>
|
||||
Equals(obj as AnchorItemVM);
|
||||
|
||||
public override int GetHashCode() =>
|
||||
key.GetHashCode();
|
||||
|
||||
public override string ToString() =>
|
||||
EnumHelper.get_display_name(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using ColumnKeys = bts.shell.pnl.PNLView.Columns;
|
||||
|
||||
namespace bts.shell.pnl {
|
||||
class PNLCellEmbeddedAnimationModel<TRow> {
|
||||
IReadOnlyList<IPNLAnimationTrigger<TRow>> _triggers;
|
||||
ImmutableDictionary<ColumnKeys, PNLCellAppearance> _defaults;
|
||||
TRow _row;
|
||||
|
||||
public PNLCellEmbeddedAnimationModel(ImmutableDictionary<ColumnKeys, PNLCellAppearance> defaults, IReadOnlyList<IPNLAnimationTrigger<TRow>> triggers) {
|
||||
_defaults = defaults;
|
||||
_triggers = triggers;
|
||||
}
|
||||
|
||||
public IReadOnlyDictionary<ColumnKeys, PNLCellAppearance> cell_appearances { get; private set; }
|
||||
|
||||
public void on_settings(ImmutableDictionary<ColumnKeys, PNLCellAppearance> defaults, IReadOnlyList<IPNLAnimationTrigger<TRow>> triggers) {
|
||||
_defaults = defaults;
|
||||
_triggers = triggers;
|
||||
on_row(_row);
|
||||
}
|
||||
|
||||
public void on_row(TRow data) {
|
||||
_row = data;
|
||||
var bd = _defaults.ToBuilder();
|
||||
foreach (var t in _triggers) {
|
||||
foreach (var kv in t.trigger(data)) {
|
||||
if (!bd.TryGetValue(kv.Key, out var app)) {
|
||||
app = _defaults[kv.Key];
|
||||
}
|
||||
app = kv.Value.merge(app);
|
||||
bd[kv.Key] = app;
|
||||
}
|
||||
}
|
||||
cell_appearances = bd.ToImmutable();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
using mbts.spark;
|
||||
using mbts.spark.utilities;
|
||||
using mbts.ui;
|
||||
using mbts.ui.spark;
|
||||
using mbts.utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace bts.shell.pnl {
|
||||
class PNLCellIndexBuilder<TRow> {
|
||||
public readonly DIYGridAnimationModel<PNLView.Columns, TRow> animation_model = new DIYGridAnimationModel<PNLView.Columns, TRow>(TimeSpan.FromSeconds(0.3));
|
||||
|
||||
static readonly Func<WorkingOrderSideData, string> _fmt_working_order_data =
|
||||
d => OrderUtils.order_collection_summary(d.top_price, d.top_volume, d.working_total);
|
||||
|
||||
readonly Dictionary<PNLView.Columns, IPNLCellVM<TRow>> _cell_index = new Dictionary<PNLView.Columns, IPNLCellVM<TRow>>();
|
||||
|
||||
public PNLCellVM<TRow, T> add<T>(PNLView.Columns key, Func<TRow, T> get, Func<T, string> fmt = null) =>
|
||||
add(key, get, fmt, EqualityComparer<T>.Default, Comparer<T>.Default);
|
||||
|
||||
public PNLCellVM<TRow, T> add_pnl<T>(PNLView.Columns key, Func<TRow, T> get) where T : struct, IComparable<T>, IFormattable =>
|
||||
add_delta(key, BTSColors.PNL.profit_color, BTSColors.PNL.loss_color, get, FormattingDelegates.Price<T>.Value);
|
||||
|
||||
public PNLCellVM<TRow, decimal> add_pnl_decimal(PNLView.Columns key, Func<TRow, decimal> get) =>
|
||||
add_delta<decimal>(key, BTSColors.PNL.profit_color, BTSColors.PNL.loss_color, OrderDataHelper.PriceComparerDecimal, get, FormattingDelegates.Price<decimal>.Value);
|
||||
|
||||
public PNLCellVM<TRow, double> add_pnl_double(PNLView.Columns key, Func<TRow, double> get) =>
|
||||
add_delta(key, BTSColors.PNL.profit_color, BTSColors.PNL.loss_color, OrderDataHelper.PriceComparer, get, FormattingDelegates.Price<double>.Value);
|
||||
|
||||
public PNLCellVM<TRow, double> add_net_price(PNLView.Columns key, Func<TRow, double> get) =>
|
||||
add_delta(key, BTSColors.MarketData.bid, BTSColors.MarketData.ask, OrderDataHelper.PriceComparer, get, FormattingDelegates.Price<double>.Value);
|
||||
|
||||
public PNLCellVM<TRow, T> add_net_pos<T>(PNLView.Columns key, Func<TRow, T> get, Func<T, string> fmt = null) where T : struct, IComparable<T> =>
|
||||
add_delta(key, BTSColors.MarketData.bid, BTSColors.MarketData.ask, get, fmt);
|
||||
|
||||
public PNLCellVM<TRow, double> add_long_price(PNLView.Columns key, Func<TRow, double> get) =>
|
||||
add_delta(key, BTSColors.MarketData.bid, BTSColors.MarketData.bid, OrderDataHelper.PriceComparer, get, FormattingDelegates.Price<double>.Value);
|
||||
|
||||
public PNLCellVM<TRow, T> add_long_pos<T>(PNLView.Columns key, Func<TRow, T> get, Func<T, string> fmt = null) where T : struct, IComparable<T> =>
|
||||
add_delta(key, BTSColors.MarketData.bid, BTSColors.MarketData.ask, get, fmt);
|
||||
|
||||
public PNLCellVM<TRow, double> add_short_price(PNLView.Columns key, Func<TRow, double> get) =>
|
||||
add_delta(key, BTSColors.MarketData.ask, BTSColors.MarketData.ask, OrderDataHelper.PriceComparer, get, FormattingDelegates.Price<double>.Value);
|
||||
|
||||
public PNLCellVM<TRow, T> add_short_pos<T>(PNLView.Columns key, Func<TRow, T> get, Func<T, string> fmt = null) where T : struct, IComparable<T> =>
|
||||
add_delta(key, BTSColors.MarketData.ask, BTSColors.MarketData.bid, get, fmt);
|
||||
|
||||
public PNLCellVM<TRow, T> add_price<T>(PNLView.Columns key, Func<TRow, T> get) where T : struct, IComparable<T>, IFormattable =>
|
||||
add_delta(key, BTSColors.PNL.profit_color, BTSColors.PNL.loss_color, get, FormattingDelegates.Price<T>.Value);
|
||||
|
||||
public PNLCellVM<TRow, double> add_price_double(PNLView.Columns key, Func<TRow, double> get) =>
|
||||
add_delta(key, BTSColors.PNL.profit_color, BTSColors.PNL.loss_color, OrderDataHelper.PriceComparer, get, FormattingDelegates.Price<double>.Value);
|
||||
|
||||
public PNLCellVM<TRow, WorkingOrderSideData> add_working_long(PNLView.Columns key, Func<TRow, WorkingOrderSideData> get, Func<TRow, double> get_market) =>
|
||||
_add_working(key, BTSColors.MarketData.bid, BTSColors.MarketData.ask, get, get_market, OrderDataHelper.PriceComparer);
|
||||
|
||||
public PNLCellVM<TRow, WorkingOrderSideData> add_working_short(PNLView.Columns key, Func<TRow, WorkingOrderSideData> get, Func<TRow, double> get_market) =>
|
||||
_add_working(key, BTSColors.MarketData.ask, BTSColors.MarketData.bid, get, get_market, new ReverseComparer<double>(OrderDataHelper.PriceComparer));
|
||||
|
||||
PNLCellVM<TRow, WorkingOrderSideData> _add_working(PNLView.Columns key, Color increase, Color decrease, Func<TRow, WorkingOrderSideData> get, Func<TRow, double> get_market, IComparer<double> price_comparer) {
|
||||
var delta_trig = DIYGridTriggerAnimations.delta_trigger<int, PNLView.Columns>(key, increase, decrease)
|
||||
.wrap<int, TRow, PNLView.Columns>(r => get(r).working_total);
|
||||
var top_of_book_trig = new TopOfBookTrigger<int, double>(key, increase,
|
||||
r => get(r).working_total,
|
||||
r => get(r).top_price,
|
||||
get_market,
|
||||
price_comparer);
|
||||
var zipped_trig = delta_trig.zip_indefinite_frame(r => {
|
||||
var frames = top_of_book_trig.on_data(r);
|
||||
return frames.TryGetFirst(out var f) ? f.animation : null;
|
||||
});
|
||||
var cell = add(key, get, _fmt_working_order_data);
|
||||
animation_model.add_trigger(delta_trig);
|
||||
animation_model.add_trigger(top_of_book_trig);
|
||||
return cell;
|
||||
}
|
||||
|
||||
public PNLCellVM<TRow, T> add_delta<T>(PNLView.Columns key, Color increase, Color decrease, Func<TRow, T> get, Func<T, string> fmt = null) where T : struct, IComparable<T> =>
|
||||
add_delta(key, increase, decrease, Comparer<T>.Default, get, fmt);
|
||||
|
||||
public PNLCellVM<TRow, T> add_delta<T>(PNLView.Columns key, Color increase, Color decrease, IComparer<T> comparer, Func<TRow, T> get, Func<T, string> fmt = null) where T : struct {
|
||||
var cell = add(key, get, fmt, EqualityComparer<T>.Default, Comparer<T>.Default);
|
||||
animation_model.add_trigger(
|
||||
DIYGridTriggerAnimations.delta_trigger(key, increase, decrease, comparer)
|
||||
.wrap(get));
|
||||
return cell;
|
||||
}
|
||||
|
||||
public PNLCellVM<TRow, T> add<T>(PNLView.Columns key, Func<TRow, T> get, Func<T, string> fmt, IEqualityComparer<T> eq_comparer, IComparer<T> comparer) {
|
||||
var cell = new PNLCellVM<TRow, T>(get, fmt, eq_comparer, comparer);
|
||||
_cell_index[key] = cell;
|
||||
return cell;
|
||||
}
|
||||
|
||||
public void on_row(TRow row) {
|
||||
foreach (var cell in _cell_index.Values)
|
||||
cell.on_row(row);
|
||||
animation_model.on_row(row);
|
||||
}
|
||||
|
||||
public void set_appearances(IReadOnlyDictionary<PNLView.Columns, PNLCellAppearance> appearances) {
|
||||
foreach (var kv in appearances) {
|
||||
if (_cell_index.TryGetValue(kv.Key, out var c))
|
||||
c.apply_appearance(kv.Value);
|
||||
}
|
||||
}
|
||||
|
||||
class TopOfBookTrigger<TPosition, TPrice> : ITriggerAnimation<TRow, PNLView.Columns> where TPosition : struct, IComparable<TPosition> where TPrice : struct, IComparable<TPrice> {
|
||||
readonly PNLView.Columns _column;
|
||||
readonly Func<TRow, TPosition> _get_pos;
|
||||
readonly Func<TRow, TPrice> _get_price, _get_market;
|
||||
readonly IComparer<TPrice> _comparer;
|
||||
readonly Color _border_color;
|
||||
|
||||
public TopOfBookTrigger(
|
||||
PNLView.Columns column,
|
||||
Color border_color,
|
||||
Func<TRow, TPosition> get_pos,
|
||||
Func<TRow, TPrice> get_price,
|
||||
Func<TRow, TPrice> get_market,
|
||||
IComparer<TPrice> comparer) {
|
||||
_column = column;
|
||||
_border_color = border_color;
|
||||
_get_pos = get_pos;
|
||||
_get_price = get_price;
|
||||
_get_market = get_market;
|
||||
_comparer = comparer;
|
||||
}
|
||||
|
||||
public IEnumerable<AnimationFrame<PNLView.Columns>> on_data(TRow value) {
|
||||
var pos = _get_pos(value);
|
||||
if (pos.CompareTo(default) > 0) {
|
||||
var price = _get_price(value);
|
||||
var market = _get_market(value);
|
||||
if (_comparer.Compare(price, market) >= 0)
|
||||
yield return new AnimationFrame<PNLView.Columns>(
|
||||
TimeSpan.Zero,
|
||||
ImmutableDictionary<SymbolKey<PNLView.Columns>, Animation>.Empty.Add(
|
||||
new SymbolKey<PNLView.Columns>(_column, mbts.ui.spark.CellAnimation.Type.Border),
|
||||
new Animation(mbts.ui.spark.CellAnimation.Type.Border, _border_color)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
using mbts.ui;
|
||||
using mbts.ui.shell;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace bts.shell.pnl {
|
||||
interface IPNLCellVM {
|
||||
object value { get; }
|
||||
FontWeight font_weight { get; set; }
|
||||
Color foreground { get; set; }
|
||||
Color background { get; set; }
|
||||
Color border { get; set; }
|
||||
}
|
||||
|
||||
interface IPNLCellVM<TRow> : IPNLCellVM {
|
||||
void on_row(TRow row);
|
||||
}
|
||||
|
||||
class PNLCellVM<TRow, TValue> : ViewModelBase, IObserver<PNLAnimation>, IPNLCellVM<TRow>, IComparable<PNLCellVM<TRow, TValue>>, IComparable {
|
||||
readonly Func<TValue, string> _fmt;
|
||||
readonly Func<TRow, TValue> _get;
|
||||
readonly IEqualityComparer<TValue> _eq_comparer;
|
||||
readonly IComparer<TValue> _comparer;
|
||||
Color
|
||||
_foreground = Colors.Black,
|
||||
_background = Colors.Transparent,
|
||||
_border = Colors.Transparent;
|
||||
string _text;
|
||||
TValue _value;
|
||||
|
||||
public PNLCellVM(Func<TRow, TValue> get)
|
||||
: this(get, null, EqualityComparer<TValue>.Default, Comparer<TValue>.Default) { }
|
||||
|
||||
public PNLCellVM(Func<TRow, TValue> get, Func<TValue, string> fmt)
|
||||
: this(get, fmt, EqualityComparer<TValue>.Default, Comparer<TValue>.Default) { }
|
||||
|
||||
public PNLCellVM(Func<TRow, TValue> get, Func<TValue, string> fmt, IEqualityComparer<TValue> eq_comparer, IComparer<TValue> comparer) {
|
||||
_get = get;
|
||||
_fmt = fmt;
|
||||
_eq_comparer = eq_comparer;
|
||||
_comparer = comparer;
|
||||
}
|
||||
|
||||
object IPNLCellVM.value => value;
|
||||
public TValue value {
|
||||
get => _value;
|
||||
set {
|
||||
if (Set(ref _value, value)) {
|
||||
_text = null;
|
||||
RaisePropertyChanged(nameof(text));
|
||||
}
|
||||
}
|
||||
}
|
||||
public string text {
|
||||
get => _text ?? (_text = _fmt?.Invoke(value) ?? value?.ToString());
|
||||
set => Set(ref _text, value);
|
||||
}
|
||||
|
||||
public Color foreground {
|
||||
get => _foreground;
|
||||
set => Set(ref _foreground, value);
|
||||
}
|
||||
|
||||
public Color background {
|
||||
get => _background;
|
||||
set => Set(ref _background, value);
|
||||
}
|
||||
|
||||
public Color border {
|
||||
get => _border;
|
||||
set => Set(ref _border, value);
|
||||
}
|
||||
|
||||
FontWeight _font_weight;
|
||||
public FontWeight font_weight {
|
||||
get => _font_weight;
|
||||
set => Set(ref _font_weight, value);
|
||||
}
|
||||
|
||||
public void on_row(TRow row) =>
|
||||
value = _get(row);
|
||||
|
||||
public void OnNext(PNLAnimation animation) {
|
||||
if (animation.foreground.HasValue)
|
||||
foreground = animation.foreground.Value.argb_color();
|
||||
if (animation.background.HasValue)
|
||||
background = animation.background.Value.argb_color();
|
||||
if (animation.font_weight.HasValue)
|
||||
font_weight = FontWeight.FromOpenTypeWeight(animation.font_weight.Value);
|
||||
}
|
||||
public void OnError(Exception error) { }
|
||||
public void OnCompleted() { }
|
||||
|
||||
public override string ToString() =>
|
||||
text;
|
||||
|
||||
int IComparable<PNLCellVM<TRow, TValue>>.CompareTo(PNLCellVM<TRow, TValue> other) =>
|
||||
other == null ? throw new ArgumentNullException(nameof(other)) : _comparer.Compare(value, other.value);
|
||||
|
||||
int IComparable.CompareTo(object obj) =>
|
||||
obj is PNLCellVM<TRow, TValue> other ?
|
||||
_comparer.Compare(value, other.value) :
|
||||
throw new ArgumentException(nameof(obj), $"The specified object is not of type {typeof(PNLCellVM<TRow, TValue>)} (actually {obj?.GetType()}).");
|
||||
}
|
||||
|
||||
static class PNLCellVMExtensions {
|
||||
public static void apply_appearance(this IPNLCellVM cell, PNLCellAppearance animation) {
|
||||
if (animation.foreground.HasValue)
|
||||
cell.foreground = animation.foreground.Value;
|
||||
if (animation.background.HasValue)
|
||||
cell.background = animation.background.Value;
|
||||
if (animation.font_weight.HasValue)
|
||||
cell.font_weight = animation.font_weight.Value;
|
||||
// TODO: font family, size
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
using bts.utils;
|
||||
using mbts.ui.shell;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.ComponentModel;
|
||||
using ColumnKeys = bts.shell.pnl.PNLView.Columns;
|
||||
|
||||
namespace bts.shell.pnl {
|
||||
class PNLNormalizedDataVM : IRaiseNotifyPropertyChanged, INotifyPropertyChanged {
|
||||
static readonly IPropertyChangedDetector<PNLNormalizedDataVM> _change_detector =
|
||||
ObjectSnapshot.build_detector_except<PNLNormalizedDataVM>(nameof(data));
|
||||
|
||||
PNLNormalizedData _data;
|
||||
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
public event Action data_changed;
|
||||
|
||||
public PNLNormalizedData data {
|
||||
get => _data;
|
||||
set {
|
||||
if (_data != value) {
|
||||
using (_change_detector.detect_changes(this))
|
||||
_data = value;
|
||||
data_changed?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// total aggregate pnl
|
||||
public decimal day_pnl => _data.day_pnl;
|
||||
public decimal sod_pnl => _data.sod_pnl;
|
||||
public decimal total_pnl => _data.day_pnl + _data.sod_pnl;
|
||||
// day pos
|
||||
public decimal day_long_pos => _data.day_long_pos;
|
||||
public decimal day_short_pos => _data.day_short_pos;
|
||||
public decimal day_net_pos => _data.day_long_pos - _data.day_short_pos;
|
||||
// sod pos
|
||||
public decimal sod_long_pos => _data.sod_long_pos;
|
||||
public decimal sod_short_pos => _data.sod_short_pos;
|
||||
public decimal sod_net_pos => _data.sod_long_pos - _data.sod_short_pos;
|
||||
// total pos
|
||||
public decimal total_net_pos => _data.total_net_pos;
|
||||
public decimal total_long_pos => _data.day_long_pos + _data.sod_long_pos;
|
||||
public decimal total_short_pos => _data.day_short_pos + _data.sod_short_pos;
|
||||
// working
|
||||
public decimal working_long_pos => _data.working_long_pos;
|
||||
public decimal working_short_pos => _data.working_short_pos;
|
||||
public decimal working_net_pos => working_long_pos - working_short_pos;
|
||||
|
||||
void IRaiseNotifyPropertyChanged.raise_notify_property_changed(string prop) =>
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(prop));
|
||||
}
|
||||
|
||||
class PNLHeaderRowVM : IRaiseNotifyPropertyChanged, INotifyPropertyChanged {
|
||||
static readonly IPropertyChangedDetector<PNLHeaderRowVM> _change_detector =
|
||||
ObjectSnapshot.build_detector_except<PNLHeaderRowVM>(nameof(data));
|
||||
|
||||
readonly PNLCellIndexBuilder<PNLNormalizedData> _cell_map = new PNLCellIndexBuilder<PNLNormalizedData>();
|
||||
readonly PNLCellEmbeddedAnimationModel<PNLNormalizedData> _animator;
|
||||
ImmutableDictionary<ColumnKeys, PNLCellAppearance> _default_appearances;
|
||||
IReadOnlyList<IPNLAnimationTrigger<PNLNormalizedData>> _triggers;
|
||||
PNLNormalizedData _data = PNLNormalizedData.zero;
|
||||
|
||||
public PNLHeaderRowVM(ImmutableDictionary<ColumnKeys, PNLCellAppearance> default_appearances, IReadOnlyList<IPNLAnimationTrigger<PNLNormalizedData>> triggers) {
|
||||
_default_appearances = default_appearances;
|
||||
_triggers = triggers;
|
||||
_animator = new PNLCellEmbeddedAnimationModel<PNLNormalizedData>(default_appearances, triggers);
|
||||
_cell_map.set_appearances(default_appearances);
|
||||
// sod
|
||||
sod_pnl = _cell_map.add_pnl_decimal(ColumnKeys.SODPNL, d => d.sod_pnl);
|
||||
sod_net_pos = _cell_map.add_net_pos(ColumnKeys.SODNetPos, d => d.sod_net_pos);
|
||||
sod_long_pos = _cell_map.add_long_pos(ColumnKeys.SODLongPos, d => d.sod_long_pos);
|
||||
sod_short_pos = _cell_map.add_short_pos(ColumnKeys.SODShortPos, d => d.sod_short_pos);
|
||||
// day
|
||||
day_pnl = _cell_map.add_pnl(ColumnKeys.DayPNL, d => d.day_pnl);
|
||||
day_net_pos = _cell_map.add_net_pos(ColumnKeys.DayNetPos, d => d.day_net_pos);
|
||||
day_long_pos = _cell_map.add_long_pos(ColumnKeys.DayLongPos, d => d.day_long_pos);
|
||||
day_short_pos = _cell_map.add_short_pos(ColumnKeys.DayShortPos, d => d.day_short_pos);
|
||||
// working
|
||||
working_net_pos = _cell_map.add_net_pos(ColumnKeys.WorkingNetPos, d => d.working_net_pos);
|
||||
working_long_pos = _cell_map.add_long_pos(ColumnKeys.WorkingLongPos, d => d.working_long_pos);
|
||||
working_short_pos = _cell_map.add_short_pos(ColumnKeys.WorkingShortPos, r => r.working_short_pos);
|
||||
// total
|
||||
total_pnl = _cell_map.add_pnl_decimal(ColumnKeys.TotalPNL, d => d.total_pnl);
|
||||
total_net_pos = _cell_map.add_net_pos(ColumnKeys.TotalNetPos, d => d.total_net_pos);
|
||||
total_long_pos = _cell_map.add_long_pos(ColumnKeys.TotalLongPos, d => d.total_long_pos);
|
||||
total_short_pos = _cell_map.add_short_pos(ColumnKeys.TotalShortPos, d => d.total_short_pos);
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
public event Action data_changed;
|
||||
|
||||
public PNLNormalizedData data {
|
||||
get => _data;
|
||||
set {
|
||||
if (value != _data) {
|
||||
_data = value;
|
||||
_cell_map.on_row(value);
|
||||
_animator.on_row(value);
|
||||
_cell_map.set_appearances(_animator.cell_appearances);
|
||||
data_changed?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// day
|
||||
public PNLCellVM<PNLNormalizedData, decimal> day_pnl { get; }
|
||||
public PNLCellVM<PNLNormalizedData, decimal> day_long_pos { get; }
|
||||
public PNLCellVM<PNLNormalizedData, decimal> day_short_pos { get; }
|
||||
public PNLCellVM<PNLNormalizedData, decimal> day_net_pos { get; }
|
||||
// sod
|
||||
public PNLCellVM<PNLNormalizedData, decimal> sod_pnl { get; }
|
||||
public PNLCellVM<PNLNormalizedData, decimal> sod_long_pos { get; }
|
||||
public PNLCellVM<PNLNormalizedData, decimal> sod_short_pos { get; }
|
||||
public PNLCellVM<PNLNormalizedData, decimal> sod_net_pos { get; }
|
||||
// total
|
||||
public PNLCellVM<PNLNormalizedData, decimal> total_pnl { get; }
|
||||
public PNLCellVM<PNLNormalizedData, decimal> total_net_pos { get; }
|
||||
public PNLCellVM<PNLNormalizedData, decimal> total_long_pos { get; }
|
||||
public PNLCellVM<PNLNormalizedData, decimal> total_short_pos { get; }
|
||||
// working
|
||||
public PNLCellVM<PNLNormalizedData, decimal> working_long_pos { get; }
|
||||
public PNLCellVM<PNLNormalizedData, decimal> working_short_pos { get; }
|
||||
public PNLCellVM<PNLNormalizedData, decimal> working_net_pos { get; }
|
||||
|
||||
public void on_settings(ImmutableDictionary<ColumnKeys, PNLCellAppearance> default_appearances, IReadOnlyList<IPNLAnimationTrigger<PNLNormalizedData>> triggers) {
|
||||
_default_appearances = default_appearances;
|
||||
_triggers = triggers;
|
||||
_animator.on_settings(_default_appearances, triggers);
|
||||
_cell_map.set_appearances(_animator.cell_appearances);
|
||||
}
|
||||
|
||||
void IRaiseNotifyPropertyChanged.raise_notify_property_changed(string prop) =>
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(prop));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
using bts.api;
|
||||
using bts.utils;
|
||||
using mbts;
|
||||
using mbts.spark;
|
||||
using mbts.ui;
|
||||
using mbts.ui.controls;
|
||||
using mbts.ui.shell;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using ColumnKeys = bts.shell.pnl.PNLView.Columns;
|
||||
|
||||
namespace bts.shell.pnl {
|
||||
class PNLRowVM : DisposableViewModelBase, IEquatable<PNLRowVM> {
|
||||
public static readonly IComparer<PNLRowVM> key_comparer = Comparer<PNLKey>.Default.wrap<PNLKey, PNLRowVM>(r => r.key);
|
||||
static readonly Func<bool, string> _fmt_has_day_trades = hdt => hdt ? "Yes" : "No";
|
||||
//
|
||||
readonly PNLCellIndexBuilder<PNLRowData> _cell_map = new PNLCellIndexBuilder<PNLRowData>();
|
||||
readonly PNLCellEmbeddedAnimationModel<PNLRowData> _animator;
|
||||
bool _refresh_normalized_data = true, _is_selected;
|
||||
PNLNormalizedData _normalized_data;
|
||||
|
||||
public PNLRowVM(PNLRowData data, ImmutableDictionary<ColumnKeys, PNLCellAppearance> default_appearances, IReadOnlyList<IPNLAnimationTrigger<PNLRowData>> triggers) {
|
||||
this.data = data;
|
||||
_animator = new PNLCellEmbeddedAnimationModel<PNLRowData>(default_appearances, triggers);
|
||||
_animator.on_row(data);
|
||||
pnl_price = _cell_map.add_price_double(ColumnKeys.PNLPrice, d => d.pnl_price);
|
||||
account_name_cell = _cell_map.add(ColumnKeys.Account, d => d.account_name);
|
||||
security_cell = _cell_map.add(ColumnKeys.Security, d => d.security, null, EqualityComparer<SecurityImmutable?>.Default, Comparer<SecurityImmutable>.Default.to_nullable());
|
||||
security_type_cell = _cell_map.add(ColumnKeys.SecurityType, d => d.security?.ContractCase ?? Security.ContractOneofCase.None);
|
||||
exchange_cell = _cell_map.add(ColumnKeys.Exchange, d => d.security?.Exchange ?? Venue.Unset);
|
||||
symbol_cell = _cell_map.add(ColumnKeys.Symbol, d => d.security?.Symbol ?? string.Empty);
|
||||
// sod
|
||||
sod_pnl = _cell_map.add_pnl_double(ColumnKeys.SODPNL, d => d.sod_pnl);
|
||||
sod_net_pos = _cell_map.add_net_pos(ColumnKeys.SODNetPos, d => d.sod_net_pos);
|
||||
sod_net_avg_price_delta = _cell_map.add_net_price(ColumnKeys.SODNetAvgPriceDelta, d => d.sod_net_avg_price_delta_raw);
|
||||
sod_long_pos = _cell_map.add_long_pos(ColumnKeys.SODLongPos, d => d.sod_long_pos);
|
||||
sod_long_avg_price = _cell_map.add_long_price(ColumnKeys.SODLongAvgPrice, d => d.sod_long_avg_price_raw);
|
||||
sod_long_avg_price_delta = _cell_map.add_long_price(ColumnKeys.SODLongAvgPriceDelta, d => d.sod_long_avg_price_delta_raw);
|
||||
sod_short_pos = _cell_map.add_short_pos(ColumnKeys.SODShortPos, d => d.sod_short_pos);
|
||||
sod_short_avg_price = _cell_map.add_short_price(ColumnKeys.SODShortAvgPrice, d => d.sod_short_avg_price_raw);
|
||||
sod_short_avg_price_delta = _cell_map.add_short_price(ColumnKeys.SODShortAvgPriceDelta, d => d.sod_short_avg_price_delta_raw);
|
||||
// day
|
||||
day_pnl = _cell_map.add_pnl_double(ColumnKeys.DayPNL, d => d.day_pnl);
|
||||
day_net_pos = _cell_map.add_net_pos(ColumnKeys.DayNetPos, d => d.day_net_pos);
|
||||
day_net_avg_price_delta = _cell_map.add_net_price(ColumnKeys.DayNetAvgPriceDelta, d => d.day_net_avg_price_delta_raw);
|
||||
day_long_pos = _cell_map.add_long_pos(ColumnKeys.DayLongPos, d => d.day_long_pos);
|
||||
day_long_avg_price = _cell_map.add_long_price(ColumnKeys.DayLongAvgPrice, d => d.day_long_avg_price_raw);
|
||||
day_long_avg_price_delta = _cell_map.add_long_price(ColumnKeys.DayLongAvgPriceDelta, d => d.day_long_avg_price_delta_raw);
|
||||
day_short_pos = _cell_map.add_short_pos(ColumnKeys.DayShortPos, d => d.day_short_pos);
|
||||
day_short_avg_price = _cell_map.add_short_price(ColumnKeys.DayShortAvgPrice, d => d.day_short_avg_price_raw);
|
||||
day_short_avg_price_delta = _cell_map.add_short_price(ColumnKeys.DayShortAvgPriceDelta, d => d.day_short_avg_price_delta_raw);
|
||||
// working
|
||||
working_net_pos = _cell_map.add_net_pos(ColumnKeys.WorkingNetPos, d => d.working_net_pos);
|
||||
working_net_avg_price_delta = _cell_map.add_net_price(ColumnKeys.WorkingNetAvgPriceDelta, d => d.working_net_avg_price_delta_raw);
|
||||
working_long_pos = _cell_map.add_working_long(ColumnKeys.WorkingLongPos, d => d.working.long_data, d => d.bid_price);
|
||||
working_long_avg_price = _cell_map.add_long_price(ColumnKeys.WorkingLongAvgPrice, d => d.working_long_avg_price_raw);
|
||||
working_long_avg_price_delta = _cell_map.add_long_price(ColumnKeys.WorkingLongAvgPriceDelta, d => d.working_long_avg_price_delta_raw);
|
||||
working_short_pos = _cell_map.add_working_short(ColumnKeys.WorkingShortPos, r => r.working.short_data, r => r.ask_price);
|
||||
working_short_avg_price = _cell_map.add_short_price(ColumnKeys.WorkingShortAvgPrice, d => d.working_short_avg_price_raw);
|
||||
working_short_avg_price_delta = _cell_map.add_short_price(ColumnKeys.WorkingShortAvgPriceDelta, d => d.working_short_avg_price_delta_raw);
|
||||
// total
|
||||
total_pnl = _cell_map.add_pnl_double(ColumnKeys.TotalPNL, d => d.total_pnl);
|
||||
total_net_pos = _cell_map.add_net_pos(ColumnKeys.TotalNetPos, d => d.total_net_pos);
|
||||
total_net_avg_price_delta = _cell_map.add_net_price(ColumnKeys.TotalNetAvgPriceDelta, d => d.total_net_avg_price_delta_raw);
|
||||
total_long_pos = _cell_map.add_long_pos(ColumnKeys.TotalLongPos, d => d.total_long_pos);
|
||||
total_long_avg_price = _cell_map.add_long_price(ColumnKeys.TotalLongAvgPrice, d => d.total_long_avg_price_raw);
|
||||
total_long_avg_price_delta = _cell_map.add_long_price(ColumnKeys.TotalLongAvgPriceDelta, d => d.total_long_avg_price_delta_raw);
|
||||
total_short_pos = _cell_map.add_short_pos(ColumnKeys.TotalShortPos, d => d.total_short_pos);
|
||||
total_short_avg_price = _cell_map.add_short_price(ColumnKeys.TotalShortAvgPrice, d => d.total_short_avg_price_raw);
|
||||
total_short_avg_price_delta = _cell_map.add_short_price(ColumnKeys.TotalShortAvgPriceDelta, d => d.total_short_avg_price_delta_raw);
|
||||
// market
|
||||
bid_price = _cell_map.add_price_double(ColumnKeys.BidPrice, d => d.bid_price);
|
||||
ask_price = _cell_map.add_price_double(ColumnKeys.AskPrice, d => d.ask_price);
|
||||
mid_price = _cell_map.add_price_double(ColumnKeys.MidPrice, d => d.mid_price);
|
||||
last_price = _cell_map.add_price_double(ColumnKeys.LastPrice, d => d.last_price);
|
||||
settlement_price = _cell_map.add_price_double(ColumnKeys.SettlementPrice, d => d.settlement_price);
|
||||
// misc
|
||||
has_day_trades = _cell_map.add(ColumnKeys.HasDayTrades, d => d.has_day_trades, _fmt_has_day_trades);
|
||||
cpp = _cell_map.add(ColumnKeys.CPP, d => d.cpp);
|
||||
|
||||
_cell_map.on_row(data);
|
||||
_cell_map.set_appearances(_animator.cell_appearances);
|
||||
_dispose = _cell_map.animation_model.OnDispatcher().Subscribe(new_animations => {
|
||||
animations = new_animations.Select(kv => new mbts.ui.spark.CellAnimation(kv.Key.column, kv.Key.type, kv.Value.color.to_brush())).ToList();
|
||||
RaisePropertyChanged(nameof(animations));
|
||||
});
|
||||
}
|
||||
|
||||
// events
|
||||
public event Action<PNLNormalizedData> updated;
|
||||
public event Action<bool> is_selected_changed;
|
||||
|
||||
// data
|
||||
public PNLKey key => data.key;
|
||||
public PNLRowData data { get; private set; }
|
||||
public PNLNormalizedData normalized_data {
|
||||
get {
|
||||
if (_refresh_normalized_data) {
|
||||
_refresh_normalized_data = false;
|
||||
var nd = new PNLNormalizedData(
|
||||
(decimal)DoubleUtil.nan_to_zero(day_pnl.value),
|
||||
(decimal)DoubleUtil.nan_to_zero(data.day.long_pos),
|
||||
(decimal)DoubleUtil.nan_to_zero(data.day.short_pos),
|
||||
(decimal)DoubleUtil.nan_to_zero(sod_pnl.value),
|
||||
(decimal)DoubleUtil.nan_to_zero(data.sod.long_pos),
|
||||
(decimal)DoubleUtil.nan_to_zero(data.sod.short_pos),
|
||||
(decimal)DoubleUtil.nan_to_zero(data.working_long_pos),
|
||||
(decimal)DoubleUtil.nan_to_zero(data.working_short_pos));
|
||||
if (_normalized_data != nd) {
|
||||
_normalized_data = nd;
|
||||
updated?.Invoke(nd);
|
||||
}
|
||||
}
|
||||
return _normalized_data;
|
||||
}
|
||||
}
|
||||
// security
|
||||
public string account_name => data.account_name;
|
||||
public SecurityImmutable? security => data.security;
|
||||
public Security.ContractOneofCase security_type => security?.ContractCase ?? Security.ContractOneofCase.None;
|
||||
public Venue exchange => security?.Exchange ?? Venue.Unset;
|
||||
public string symbol => security.symbol_or_default();
|
||||
// cells
|
||||
public PNLCellVM<PNLRowData, string> account_name_cell { get; }
|
||||
public PNLCellVM<PNLRowData, SecurityImmutable?> security_cell { get; }
|
||||
public PNLCellVM<PNLRowData, Security.ContractOneofCase> security_type_cell { get; }
|
||||
public PNLCellVM<PNLRowData, Venue> exchange_cell { get; }
|
||||
public PNLCellVM<PNLRowData, string> symbol_cell { get; }
|
||||
// sod
|
||||
public PNLCellVM<PNLRowData, double> sod_pnl { get; }
|
||||
public PNLCellVM<PNLRowData, double> sod_net_pos { get; }
|
||||
public PNLCellVM<PNLRowData, double> sod_net_avg_price_delta { get; }
|
||||
public PNLCellVM<PNLRowData, double> sod_long_pos { get; }
|
||||
public PNLCellVM<PNLRowData, double> sod_long_avg_price { get; }
|
||||
public PNLCellVM<PNLRowData, double> sod_long_avg_price_delta { get; }
|
||||
public PNLCellVM<PNLRowData, double> sod_short_pos { get; }
|
||||
public PNLCellVM<PNLRowData, double> sod_short_avg_price { get; }
|
||||
public PNLCellVM<PNLRowData, double> sod_short_avg_price_delta { get; }
|
||||
// (market)
|
||||
public PNLCellVM<PNLRowData, double> bid_price { get; }
|
||||
public PNLCellVM<PNLRowData, double> mid_price { get; }
|
||||
public PNLCellVM<PNLRowData, double> ask_price { get; }
|
||||
public PNLCellVM<PNLRowData, double> last_price { get; }
|
||||
public PNLCellVM<PNLRowData, double> settlement_price { get; }
|
||||
// pnl price
|
||||
public PNLCellVM<PNLRowData, double> pnl_price { get; }
|
||||
// day
|
||||
public PNLCellVM<PNLRowData, double> day_pnl { get; }
|
||||
public PNLCellVM<PNLRowData, double> day_net_pos { get; }
|
||||
public PNLCellVM<PNLRowData, double> day_net_avg_price_delta { get; }
|
||||
public PNLCellVM<PNLRowData, double> day_long_pos { get; }
|
||||
public PNLCellVM<PNLRowData, double> day_long_avg_price { get; }
|
||||
public PNLCellVM<PNLRowData, double> day_long_avg_price_delta { get; }
|
||||
public PNLCellVM<PNLRowData, double> day_short_pos { get; }
|
||||
public PNLCellVM<PNLRowData, double> day_short_avg_price { get; }
|
||||
public PNLCellVM<PNLRowData, double> day_short_avg_price_delta { get; }
|
||||
// working
|
||||
public PNLCellVM<PNLRowData, double> working_net_pos { get; }
|
||||
public PNLCellVM<PNLRowData, double> working_net_avg_price_delta { get; }
|
||||
public PNLCellVM<PNLRowData, WorkingOrderSideData> working_long_pos { get; }
|
||||
public PNLCellVM<PNLRowData, double> working_long_avg_price { get; }
|
||||
public PNLCellVM<PNLRowData, double> working_long_avg_price_delta { get; }
|
||||
public PNLCellVM<PNLRowData, WorkingOrderSideData> working_short_pos { get; }
|
||||
public PNLCellVM<PNLRowData, double> working_short_avg_price { get; }
|
||||
public PNLCellVM<PNLRowData, double> working_short_avg_price_delta { get; }
|
||||
// total
|
||||
public PNLCellVM<PNLRowData, double> total_pnl { get; }
|
||||
public PNLCellVM<PNLRowData, double> total_net_pos { get; }
|
||||
public PNLCellVM<PNLRowData, double> total_net_avg_price_delta { get; }
|
||||
public PNLCellVM<PNLRowData, double> total_long_pos { get; }
|
||||
public PNLCellVM<PNLRowData, double> total_long_avg_price { get; }
|
||||
public PNLCellVM<PNLRowData, double> total_long_avg_price_delta { get; }
|
||||
public PNLCellVM<PNLRowData, double> total_short_pos { get; }
|
||||
public PNLCellVM<PNLRowData, double> total_short_avg_price { get; }
|
||||
public PNLCellVM<PNLRowData, double> total_short_avg_price_delta { get; }
|
||||
|
||||
public PNLCellVM<PNLRowData, bool> has_day_trades { get; }
|
||||
public PNLCellVM<PNLRowData, double> cpp { get; }
|
||||
|
||||
public IList<mbts.ui.spark.CellAnimation> animations { get; private set; }
|
||||
public bool is_selected {
|
||||
get => _is_selected;
|
||||
set {
|
||||
if (Set(ref _is_selected, value))
|
||||
is_selected_changed?.Invoke(value);
|
||||
}
|
||||
}
|
||||
|
||||
public void update(PNLRowData data) {
|
||||
_refresh_normalized_data = true;
|
||||
this.data = data;
|
||||
_cell_map.on_row(data);
|
||||
_animator.on_row(data);
|
||||
_cell_map.set_appearances(_animator.cell_appearances);
|
||||
}
|
||||
|
||||
public void on_settings(ImmutableDictionary<ColumnKeys, PNLCellAppearance> defaults, IReadOnlyList<IPNLAnimationTrigger<PNLRowData>> triggers) {
|
||||
_animator.on_settings(defaults, triggers);
|
||||
_cell_map.set_appearances(_animator.cell_appearances);
|
||||
}
|
||||
|
||||
|
||||
public bool Equals(PNLRowVM other) =>
|
||||
other != null && key == other.key;
|
||||
|
||||
public override int GetHashCode() =>
|
||||
key.GetHashCode();
|
||||
|
||||
public override string ToString() =>
|
||||
$"account: {account_name} exchange: {exchange} symbol: {symbol} security: {security} buy_quantity: {day_long_pos} sell_quantity: {day_short_pos} pnl: {total_pnl}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,674 @@
|
||||
using bts.api;
|
||||
using bts.api.core;
|
||||
using bts.api.orderman;
|
||||
using bts.collections;
|
||||
using bts.utils;
|
||||
using mbts;
|
||||
using mbts.collections;
|
||||
using mbts.persist;
|
||||
using mbts.spark;
|
||||
using mbts.spark.filters;
|
||||
using mbts.ui;
|
||||
using mbts.ui.controls;
|
||||
using mbts.ui.settings;
|
||||
using mbts.ui.shell;
|
||||
using mbts.ui.utility;
|
||||
using mbts.utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using ColumnKeys = bts.shell.pnl.PNLView.Columns;
|
||||
using SecurityType = bts.api.Security.ContractOneofCase;
|
||||
|
||||
namespace bts.shell.pnl {
|
||||
class PNLVM : DisposableViewModelBase, IHandleRowSelection {
|
||||
static readonly IReadOnlyList<Func<PNLVM, IDataGridColumnFilter<PNLRowData>>> _filter_getters =
|
||||
ObjectSnapshot.property_delegates_of_type<PNLVM, IDataGridColumnFilter<PNLRowData>>().ToReadOnlyList();
|
||||
static readonly TimeSpan _auxiliary_grid_update_throttle = TimeSpan.FromSeconds(0.2);
|
||||
|
||||
readonly IBus _bus;
|
||||
readonly OrdersModel _orders_model;
|
||||
readonly IObservableCollectionAddRemoveOnly<PNLRowVM> _selected_rows = new ObservableSortedCollection<PNLRowVM>(PNLRowVM.key_comparer);
|
||||
readonly IList<PNLRowData> _rows_data_wrapper;
|
||||
readonly IList<PNLKey> _rows_key_wrapper;
|
||||
readonly DeferrableActionWrapper _refresh_filter;
|
||||
//
|
||||
readonly Property<PNLCalculationType> _calculation_type;
|
||||
PNLHeaderRowDisplay _header_row_display = PNLHeaderRowDisplay.Total;
|
||||
PNLRowCollectionView _rows_view;
|
||||
Brush _selection_highlight;
|
||||
bool _is_paused;
|
||||
// default appearances
|
||||
ImmutableDictionary<ColumnKeys, PNLCellAppearance>
|
||||
_default_appearances = ImmutableDictionary<ColumnKeys, PNLCellAppearance>.Empty,
|
||||
_default_group_appearances = ImmutableDictionary<ColumnKeys, PNLCellAppearance>.Empty,
|
||||
_default_header_appearances = ImmutableDictionary<ColumnKeys, PNLCellAppearance>.Empty;
|
||||
// triggers
|
||||
IReadOnlyList<IPNLAnimationTrigger<PNLRowData>>
|
||||
_triggers = Array.Empty<IPNLAnimationTrigger<PNLRowData>>();
|
||||
IReadOnlyList<IPNLAnimationTrigger<PNLNormalizedData>>
|
||||
_group_triggers = Array.Empty<IPNLAnimationTrigger<PNLNormalizedData>>(),
|
||||
_header_triggers = Array.Empty<IPNLAnimationTrigger<PNLNormalizedData>>();
|
||||
// pause/resume
|
||||
(TableUpdate<PNLRowData, PNLKey> filtered, TableUpdate<PNLRowData, PNLKey> raw)? _resume;
|
||||
|
||||
public PNLVM(IBus bus, OrdersModel orders_model, PNLSettings settings)
|
||||
: this(bus, orders_model, settings, orders_model.security_account_pnl) { }
|
||||
|
||||
PNLVM(IBus bus, OrdersModel orders_model, PNLSettings settings, IObservable<TableUpdate<PNLRowData, PNLKey>> pnl_source) {
|
||||
_bus = bus;
|
||||
_orders_model = orders_model;
|
||||
_rows_data_wrapper = rows.WrapSelector(r => r.data, d => new PNLRowVM(d, _default_appearances, _triggers));
|
||||
_rows_key_wrapper = rows.WrapSelector(r => r.key, null);
|
||||
_calculation_type = backing(nameof(calculation_type), PNLCalculationType.Waterfall);
|
||||
anchors.AddRange(settings.anchors);
|
||||
// commands
|
||||
set_anchor_cmd = new DelegateCommand<ColumnKeys>(key => anchors.SetItems(new[] { key }));
|
||||
append_anchor_cmd = new DelegateCommand<ColumnKeys>(key => {
|
||||
if (!anchors.Remove(key))
|
||||
anchors.Add(key);
|
||||
});
|
||||
// filters
|
||||
account_filter = DiscreteValuesFilterVM.Common.account<PNLRowData>(orders_model, r => new AccountTuple(r.key.account_id, r.account_name), nameof(PNLRowVM.account_name));
|
||||
_refresh_filter = new DeferrableActionWrapper(() => _filter_changed?.Invoke(get_filters_states().build_predicate()));
|
||||
var filter_src = Observable.Create<Func<PNLRowData, bool>>(o => {
|
||||
o.OnNext(get_filters_states().build_predicate());
|
||||
_filter_changed += o.OnNext;
|
||||
return Disposable.Create(() => _filter_changed -= o.OnNext);
|
||||
});
|
||||
foreach (var f in _all_filters)
|
||||
f.filter_changed += _refresh_filter.execute;
|
||||
// totals/selected headers
|
||||
total_aggregate = new PNLHeaderRowVM(_default_header_appearances, _header_triggers);
|
||||
selected_aggregate = new PNLHeaderRowVM(_default_header_appearances, _header_triggers);
|
||||
// load/subscribe to data
|
||||
load_from(settings);
|
||||
_rows_view = new PNLRowCollectionView(rows, _default_group_appearances, _group_triggers);
|
||||
_dispose = Disposable.Create(
|
||||
pnl_source.Operate(
|
||||
_calculation_type.observe_current(),
|
||||
(data, calc) => data.with(calc_type: calc)
|
||||
).FilterWithRaw(filter_src, PNLRowData.key_fn, null)
|
||||
.OnDispatcher()
|
||||
.Subscribe(_update),
|
||||
Disposable.Create(() => {
|
||||
foreach (var f in _all_filters)
|
||||
f.filter_changed -= _refresh_filter.execute;
|
||||
rows.DisposeAll();
|
||||
_rows_view.Groups.OfType<IDisposable>().DisposeAll();
|
||||
}));
|
||||
}
|
||||
|
||||
event Action<SelectionChangedEventArgs> _selection_changed;
|
||||
event Action<TableUpdate<PNLRowData, PNLKey>> _updated;
|
||||
event Action<Func<PNLRowData, bool>> _filter_changed;
|
||||
|
||||
// rows/selected rows
|
||||
public ObservableCollection<PNLRowVM> rows { get; } = new ObservableCollection<PNLRowVM>();
|
||||
public ICollectionView rows_view => _rows_view;
|
||||
public int filtered_row_count => rows.Count;
|
||||
public int total_row_count { get; private set; }
|
||||
public IReadOnlyObservableCollection<PNLRowVM> selected_rows => _selected_rows;
|
||||
// display/grouping
|
||||
public PNLHeaderRowDisplay header_row_display { get => _header_row_display; set => Set(ref _header_row_display, value); }
|
||||
public ObservableCollection<ColumnKeys> anchors { get; } = new ObservableCollection<ColumnKeys>();
|
||||
// calc
|
||||
public PNLCalculationType calculation_type { get => _calculation_type.Value; set => _calculation_type.set(value); }
|
||||
public PNLHeaderRowVM total_aggregate { get; }
|
||||
public PNLHeaderRowVM selected_aggregate { get; }
|
||||
public bool is_paused {
|
||||
get => _is_paused;
|
||||
set {
|
||||
if (Set(ref _is_paused, value) && !value && _resume.HasValue) {
|
||||
var curr_state = ImmutableDictionary<PNLKey, PNLRowData>.Empty.AddRange(rows.Select(r => new KeyValuePair<PNLKey, PNLRowData>(r.key, r.data)));
|
||||
var reset = TableUpdate<PNLRowData, PNLKey>.update(
|
||||
new TableUpdate<PNLRowData, PNLKey>(
|
||||
curr_state,
|
||||
curr_state.Values,
|
||||
Enumerable.Empty<Change<PNLRowData>>(),
|
||||
Enumerable.Empty<PNLRowData>(),
|
||||
PNLRowData.key_fn),
|
||||
_resume.Value.filtered.state,
|
||||
EqualityComparer<PNLRowData>.Default);
|
||||
_update((reset, _resume.Value.raw));
|
||||
_resume = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Brush selection_highlight { get => _selection_highlight; set => Set(ref _selection_highlight, value); }
|
||||
|
||||
// commands
|
||||
public ICommand set_anchor_cmd { get; }
|
||||
public ICommand append_anchor_cmd { get; }
|
||||
public ICommand show_working_orders_cmd { get; }
|
||||
|
||||
public void set_all_expanded(bool is_expanded) =>
|
||||
_rows_view.set_all_expanded(is_expanded);
|
||||
|
||||
#region Filters
|
||||
/// security
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, uint[]> account_filter { get; }
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, Venue[]> exchange_filter { get; } = DiscreteValuesFilterVM.Common.exchange<PNLRowData>(r => r.security.exchange_or_default(), nameof(PNLRowVM.exchange));
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, string[]> symbol_filter { get; } = DiscreteValuesFilterVM.Common.symbol<PNLRowData>(r => r.security.symbol_or_default(), nameof(PNLRowVM.symbol));
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, string> security_filter { get; } = new RegexFilterVM<PNLRowData>(r => r.security?.ToString(), nameof(PNLRowData.security));
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, SecurityType[]> security_type_filter { get; } = DiscreteValuesFilterVM.Create<PNLRowData, SecurityType>(
|
||||
EnumHelper.get_values_except(SecurityType.Option, SecurityType.Synthetic).ToArray(),
|
||||
r => r.security?.ContractCase ?? SecurityType.None,
|
||||
nameof(PNLRowData.security));
|
||||
/// sod
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> sod_pnl_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.sod_pnl);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> sod_long_pos_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.sod.long_pos);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> sod_long_avg_price_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.sod.long_avg);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> sod_long_avg_price_delta_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.sod_long_avg_price_delta);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> sod_short_pos_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.sod.short_pos);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> sod_short_avg_price_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.sod.short_avg);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> sod_short_avg_price_delta_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.sod_short_avg_price_delta);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> sod_net_pos_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.sod.net_pos);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> sod_net_avg_price_delta_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.sod_net_avg_price_delta);
|
||||
/// day
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> day_pnl_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.day_pnl);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> day_long_pos_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.day.long_pos);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> day_long_avg_price_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.day_long_avg_price_raw);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> day_long_avg_price_delta_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.day_long_avg_price_delta);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> day_short_pos_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.day.short_pos);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> day_short_avg_price_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.day_short_avg_price_raw);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> day_short_avg_price_delta_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.day_short_avg_price_delta_raw);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> day_net_pos_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.day.net_pos);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> day_net_avg_price_delta_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.day_net_avg_price_delta_raw);
|
||||
/// working (long)
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> working_long_pos_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.working_long_pos);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> working_long_avg_price_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.working_long_avg_price_raw);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> working_long_avg_price_delta_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.working_long_avg_price_delta_raw);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> working_short_pos_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.working_short_pos);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> working_short_avg_price_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.working_short_avg_price_raw);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> working_short_avg_price_delta_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.working_short_avg_price_delta_raw);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> working_net_pos_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.working_net_pos);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> working_net_avg_price_delta_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.working_net_avg_price_delta_raw);
|
||||
/// total
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> total_pnl_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.total_pnl);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> total_long_pos_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.total_long_pos);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> total_long_avg_price_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.total_long_avg_price);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> total_long_avg_price_delta_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.total_long_avg_price_delta);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> total_short_pos_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.total_short_pos);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> total_short_avg_price_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.total_short_avg_price);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> total_short_avg_price_delta_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.total_short_avg_price_delta);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> total_net_pos_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.total_net_pos);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> total_net_avg_price_delta_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.total_net_avg_price_delta);
|
||||
/// market
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> bid_price_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.bid_price);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> ask_price_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.ask_price);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> mid_price_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.mid_price);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> last_price_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.last_price);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> settlement_price_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.settlement_price);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> pnl_price_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.pnl_price);
|
||||
/// misc
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, bool[]> has_day_trades_filter { get; } = DiscreteValuesFilterVM.Create<PNLRowData, bool>(new[] { true, false }, r => r.has_day_trades);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> cpp_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.cpp);
|
||||
|
||||
IEnumerable<IDataGridColumnFilter<PNLRowData>> _all_filters =>
|
||||
_filter_getters.Select(get => get(this));
|
||||
|
||||
public FilterStates get_filters_states() => new FilterStates {
|
||||
account = account_filter.get_state(),
|
||||
exchange = exchange_filter.get_state(),
|
||||
symbol = symbol_filter.get_state(),
|
||||
security = security_filter.get_state(),
|
||||
security_type = security_type_filter.get_state(),
|
||||
/// sod
|
||||
sod_pnl = sod_pnl_filter.get_state(),
|
||||
sod_long_pos = sod_long_pos_filter.get_state(),
|
||||
sod_long_avg_price = sod_long_avg_price_filter.get_state(),
|
||||
sod_long_avg_price_delta = sod_long_avg_price_delta_filter.get_state(),
|
||||
sod_short_pos = sod_short_pos_filter.get_state(),
|
||||
sod_short_avg_price = sod_short_avg_price_filter.get_state(),
|
||||
sod_short_avg_price_delta = sod_short_avg_price_delta_filter.get_state(),
|
||||
sod_net_pos = sod_net_pos_filter.get_state(),
|
||||
sod_net_avg_price_delta = sod_net_avg_price_delta_filter.get_state(),
|
||||
/// day
|
||||
day_pnl = day_pnl_filter.get_state(),
|
||||
day_long_pos = day_long_pos_filter.get_state(),
|
||||
day_long_avg_price = day_long_avg_price_filter.get_state(),
|
||||
day_long_avg_price_delta = day_long_avg_price_delta_filter.get_state(),
|
||||
day_short_pos = day_short_pos_filter.get_state(),
|
||||
day_short_avg_price = day_short_avg_price_filter.get_state(),
|
||||
day_short_avg_price_delta = day_short_avg_price_delta_filter.get_state(),
|
||||
day_net_pos = day_net_pos_filter.get_state(),
|
||||
day_net_avg_price_delta = day_net_avg_price_delta_filter.get_state(),
|
||||
/// total
|
||||
total_pnl = total_pnl_filter.get_state(),
|
||||
total_long_pos = total_long_pos_filter.get_state(),
|
||||
total_long_avg_price = total_long_avg_price_filter.get_state(),
|
||||
total_long_avg_price_delta = total_long_avg_price_delta_filter.get_state(),
|
||||
total_short_pos = total_short_pos_filter.get_state(),
|
||||
total_short_avg_price = total_short_avg_price_filter.get_state(),
|
||||
total_short_avg_price_delta = total_short_avg_price_delta_filter.get_state(),
|
||||
total_net_pos = total_net_pos_filter.get_state(),
|
||||
total_net_avg_price_delta = total_net_avg_price_delta_filter.get_state(),
|
||||
/// working
|
||||
working_long_pos = working_long_pos_filter.get_state(),
|
||||
working_long_avg_price = working_long_avg_price_filter.get_state(),
|
||||
working_long_avg_price_delta = working_long_avg_price_delta_filter.get_state(),
|
||||
working_short_pos = working_short_pos_filter.get_state(),
|
||||
working_short_avg_price = working_short_avg_price_filter.get_state(),
|
||||
working_short_avg_price_delta = working_short_avg_price_delta_filter.get_state(),
|
||||
working_net_pos = working_net_pos_filter.get_state(),
|
||||
working_net_avg_price_delta = working_net_avg_price_delta_filter.get_state(),
|
||||
/// market
|
||||
bid_price = bid_price_filter.get_state(),
|
||||
ask_price = ask_price_filter.get_state(),
|
||||
mid_price = mid_price_filter.get_state(),
|
||||
last_price = last_price_filter.get_state(),
|
||||
settlement_price = settlement_price_filter.get_state(),
|
||||
pnl_price = pnl_price_filter.get_state(),
|
||||
/// misc
|
||||
has_day_trades = has_day_trades_filter.get_state()
|
||||
};
|
||||
#endregion
|
||||
|
||||
public void load_from(PNLSettings settings) {
|
||||
selection_highlight = settings.grid.selection_highlight.to_brush();
|
||||
calculation_type = settings.calculation_type;
|
||||
if (settings.filters != null) {
|
||||
using (_refresh_filter.defer_execution()) {
|
||||
var f = settings.filters;
|
||||
/// security
|
||||
account_filter.load_state(f.account);
|
||||
exchange_filter.load_state(f.exchange);
|
||||
symbol_filter.load_state(f.symbol);
|
||||
security_filter.load_state(f.security);
|
||||
security_type_filter.load_state(f.security_type);
|
||||
/// sod
|
||||
sod_pnl_filter.load_state(f.sod_pnl);
|
||||
sod_long_pos_filter.load_state(f.sod_long_pos);
|
||||
sod_long_avg_price_filter.load_state(f.sod_long_avg_price);
|
||||
sod_long_avg_price_delta_filter.load_state(f.sod_long_avg_price_delta);
|
||||
sod_short_pos_filter.load_state(f.sod_short_pos);
|
||||
sod_short_avg_price_filter.load_state(f.sod_short_avg_price);
|
||||
sod_short_avg_price_delta_filter.load_state(f.sod_short_avg_price_delta);
|
||||
sod_net_pos_filter.load_state(f.sod_net_pos);
|
||||
sod_net_avg_price_delta_filter.load_state(f.sod_net_avg_price_delta);
|
||||
/// day
|
||||
day_pnl_filter.load_state(f.day_pnl);
|
||||
day_long_pos_filter.load_state(f.day_long_pos);
|
||||
day_long_avg_price_filter.load_state(f.day_long_avg_price);
|
||||
day_long_avg_price_delta_filter.load_state(f.day_long_avg_price_delta);
|
||||
day_short_pos_filter.load_state(f.day_short_pos);
|
||||
day_short_avg_price_filter.load_state(f.day_short_avg_price);
|
||||
day_short_avg_price_delta_filter.load_state(f.day_short_avg_price_delta);
|
||||
day_net_pos_filter.load_state(f.day_net_pos);
|
||||
day_net_avg_price_delta_filter.load_state(f.day_net_avg_price_delta);
|
||||
/// total
|
||||
total_pnl_filter.load_state(f.total_pnl);
|
||||
total_long_pos_filter.load_state(f.total_long_pos);
|
||||
total_long_avg_price_filter.load_state(f.total_long_avg_price);
|
||||
total_long_avg_price_delta_filter.load_state(f.total_long_avg_price_delta);
|
||||
total_short_pos_filter.load_state(f.total_short_pos);
|
||||
total_short_avg_price_filter.load_state(f.total_short_avg_price);
|
||||
total_short_avg_price_delta_filter.load_state(f.total_short_avg_price_delta);
|
||||
total_net_pos_filter.load_state(f.total_net_pos);
|
||||
/// working
|
||||
working_long_pos_filter.load_state(f.working_long_pos);
|
||||
working_long_avg_price_filter.load_state(f.working_long_avg_price);
|
||||
working_long_avg_price_delta_filter.load_state(f.working_long_avg_price_delta);
|
||||
working_short_pos_filter.load_state(f.working_short_pos);
|
||||
working_short_avg_price_filter.load_state(f.working_short_avg_price);
|
||||
working_short_avg_price_delta_filter.load_state(f.working_short_avg_price_delta);
|
||||
working_net_pos_filter.load_state(f.working_net_pos);
|
||||
working_net_avg_price_delta_filter.load_state(f.working_net_avg_price_delta);
|
||||
/// market
|
||||
bid_price_filter.load_state(f.bid_price);
|
||||
mid_price_filter.load_state(f.mid_price);
|
||||
ask_price_filter.load_state(f.ask_price);
|
||||
last_price_filter.load_state(f.last_price);
|
||||
settlement_price_filter.load_state(f.settlement_price);
|
||||
pnl_price_filter.load_state(f.pnl_price);
|
||||
/// misc
|
||||
has_day_trades_filter.load_state(f.has_day_trades);
|
||||
cpp_filter.load_state(f.cpp);
|
||||
}
|
||||
}
|
||||
anchors.sync_with(settings.anchors);
|
||||
header_row_display = settings.header_row_display;
|
||||
//
|
||||
map_appearances(ref _default_appearances, settings.grid.columns, settings.grid_defaults);
|
||||
map_appearances(ref _default_group_appearances, settings.group_columns, settings.group_cell_appearance.to_appearance().merge(settings.grid_defaults));
|
||||
map_appearances(ref _default_header_appearances, settings.header_columns, settings.header_cell_appearance.to_appearance().merge(settings.grid_defaults));
|
||||
_triggers = settings.triggers();
|
||||
_group_triggers = settings.group_triggers();
|
||||
_header_triggers = settings.header_triggers();
|
||||
foreach (var r in rows)
|
||||
r.on_settings(_default_appearances, _triggers);
|
||||
total_aggregate.on_settings(_default_header_appearances, _header_triggers);
|
||||
selected_aggregate.on_settings(_default_header_appearances, _header_triggers);
|
||||
void map_appearances(ref ImmutableDictionary<ColumnKeys, PNLCellAppearance> from, IEnumerable<GridColumnSettings> col_settings, PNLCellAppearance fallback_appearance) {
|
||||
var dabd = from.ToBuilder();
|
||||
foreach (var k in EnumHelper.get_values<ColumnKeys>()) {
|
||||
if (GridSettingsHelper.try_find_column(col_settings, k, out var match))
|
||||
dabd[k] = new PNLCellAppearance(match.font_color_override, match.background_override, match.font_weight_override).merge(fallback_appearance);
|
||||
}
|
||||
from = dabd.ToImmutable();
|
||||
}
|
||||
if (_rows_view == null)
|
||||
_rows_view = new PNLRowCollectionView(rows, _default_group_appearances, _group_triggers);
|
||||
else _rows_view.on_settings(_default_group_appearances, _group_triggers);
|
||||
}
|
||||
|
||||
#region FillsVM Factory
|
||||
public FillsVM mk_fills_vm(PNLRowVM row, ColumnKeys key) {
|
||||
var state = PNLSettings.DefaultAuxiliaryFills;
|
||||
_append_key_filters(state.filters, key);
|
||||
_append_row_filters(state.filters, row);
|
||||
state.grid = _hide_cols(state.grid, anchors);
|
||||
var src = _orders_model.on_fill_tuples.Filter(ft => ft.fill.SecurityKey == row.key.security_id && ft.account_id == row.key.account_id);
|
||||
return new FillsVM(_bus, _orders_model, Observable.Return(state), src, new WindowSettingsManager<mbts.ui.settings.GridSettings>.DisconnectedImpl(state.grid));
|
||||
}
|
||||
|
||||
public FillsVM mk_fills_vm(PNLRowCollectionViewGroup group, ColumnKeys key) {
|
||||
var state = PNLSettings.DefaultAuxiliaryFills;
|
||||
_append_key_filters(state.filters, key);
|
||||
_append_group_filters(state.filters, group, anchors);
|
||||
// hide all columns with implicit data (i.e., the anchors)
|
||||
state.grid = _hide_cols(state.grid, anchors.WrapSubRange(0, group.level));
|
||||
return new FillsVM(_bus, _orders_model, Observable.Return(state), new WindowSettingsManager<mbts.ui.settings.GridSettings>.DisconnectedImpl(state.grid));
|
||||
}
|
||||
|
||||
public FillsVM mk_fills_vm(PNLHeaderRowDisplay header, ColumnKeys key) {
|
||||
var state = fills.FillsSettings.Default();
|
||||
state.grid = orderbook.OrderBookStartupData.DefaultFillsGrid.general;
|
||||
state.filters = new fills.FillsFilterStates();
|
||||
state.grid = _hide_cols(state.grid, anchors);
|
||||
_append_key_filters(state.filters, key);
|
||||
var src = _orders_model.on_fill_tuples;
|
||||
switch (header) {
|
||||
case PNLHeaderRowDisplay.SelectedTotal: {
|
||||
var filter_src = System.Reactive.Linq.ObservableExtensions.FromEventThrottle<SelectionChangedEventArgs, Func<FillTuple, bool>>(
|
||||
cb => _selection_changed += cb,
|
||||
cb => _selection_changed -= cb,
|
||||
_ => predicate_factory(_selected_rows.Select(r => r.key)),
|
||||
_auxiliary_grid_update_throttle)
|
||||
.StartWith(() => predicate_factory(_selected_rows.Select(r => r.key)));
|
||||
src = src.Filter(filter_src, FillTuple.key_fn);
|
||||
}
|
||||
break;
|
||||
case PNLHeaderRowDisplay.Total: {
|
||||
var filter_src = System.Reactive.Linq.ObservableExtensions.FromEventThrottle<TableUpdate<PNLRowData, PNLKey>, Func<FillTuple, bool>>(
|
||||
cb => _updated += cb,
|
||||
cb => _updated -= cb,
|
||||
t => predicate_factory(t.state.Keys),
|
||||
_auxiliary_grid_update_throttle)
|
||||
.StartWith(() => predicate_factory(rows.Select(r => r.key)));
|
||||
src = src.Filter(filter_src, FillTuple.key_fn);
|
||||
}
|
||||
break;
|
||||
}
|
||||
return new FillsVM(_bus, _orders_model, Observable.Return(state), src, new WindowSettingsManager<mbts.ui.settings.GridSettings>.DisconnectedImpl(state.grid));
|
||||
//
|
||||
Func<FillTuple, bool> predicate_factory(IEnumerable<PNLKey> keys) {
|
||||
var key_set = new HashSet<PNLKey>(keys);
|
||||
return new Func<FillTuple, bool>(ft => key_set.Contains(new PNLKey(ft.account_id, ft.fill.SecurityKey)));
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region OrderBookVM Factory
|
||||
public OrderBookVM mk_orderbook_vm(PNLHeaderRowDisplay header, ColumnKeys key) {
|
||||
var state = new orderbook.OrderBookStartupData(
|
||||
new orderbook.FilterStates(),
|
||||
orderbook.OrderBookStartupData.default_mini_grid,
|
||||
use_defaults: false);
|
||||
_append_key_filters(state.filters, key);
|
||||
state.orderbook_grid.general = _hide_cols(state.orderbook_grid.general, anchors);
|
||||
var src = _orders_model.on_order_tuples_with_root_and_piq;
|
||||
switch (header) {
|
||||
case PNLHeaderRowDisplay.SelectedTotal: {
|
||||
var filter_src = System.Reactive.Linq.ObservableExtensions.FromEventThrottle<SelectionChangedEventArgs, Func<OrderSecurityRootPair, bool>>(
|
||||
cb => _selection_changed += cb,
|
||||
cb => _selection_changed -= cb,
|
||||
_ => predicate_factory(_selected_rows.Select(r => r.key)),
|
||||
_auxiliary_grid_update_throttle);
|
||||
src = src.Filter(filter_src, OrderSecurityHelper.root_pair_key_fn);
|
||||
}
|
||||
break;
|
||||
case PNLHeaderRowDisplay.Total: {
|
||||
var filter_src = System.Reactive.Linq.ObservableExtensions.FromEventThrottle<TableUpdate<PNLRowData, PNLKey>, Func<OrderSecurityRootPair, bool>>(
|
||||
cb => _updated += cb,
|
||||
cb => _updated -= cb,
|
||||
t => predicate_factory(t.state.Keys),
|
||||
_auxiliary_grid_update_throttle)
|
||||
.StartWith(() => predicate_factory(rows.Select(r => r.key)));
|
||||
src = src.Filter(filter_src, OrderSecurityHelper.root_pair_key_fn);
|
||||
}
|
||||
break;
|
||||
}
|
||||
var settings_man = new WindowSettingsManager<orderbook.OrderBookGridSettings>.DisconnectedImpl(state.orderbook_grid);
|
||||
var obvm = new OrderBookVM(_bus, null, _orders_model, src, settings_man, true);
|
||||
obvm.columns.InsertRange(0, OrderBookVM.OrderBookColumnCollection.editable_columns);
|
||||
return obvm;
|
||||
Func<OrderSecurityRootPair, bool> predicate_factory(IEnumerable<PNLKey> keys) {
|
||||
var key_set = new HashSet<PNLKey>(keys);
|
||||
return new Func<OrderSecurityRootPair, bool>(o => key_set.Contains(new PNLKey(o.order.order.account_id(), o.order.order.security_key())));
|
||||
}
|
||||
}
|
||||
|
||||
public OrderBookVM mk_orderbook_vm(PNLRowCollectionViewGroup group, ColumnKeys key) {
|
||||
var filters = new orderbook.FilterStates();
|
||||
_append_group_filters(filters, group, anchors);
|
||||
return _mk_orderbook_vm_common(filters, anchors.WrapSubRange(0, group.level));
|
||||
}
|
||||
|
||||
public OrderBookVM mk_orderbook_vm(PNLRowVM row, ColumnKeys key) {
|
||||
var filters = new orderbook.FilterStates();
|
||||
_append_key_filters(filters, key);
|
||||
_append_row_filters(filters, row);
|
||||
return _mk_orderbook_vm_common(filters, anchors.Concat(new[] { ColumnKeys.Account, ColumnKeys.Symbol, ColumnKeys.Security }).Distinct());
|
||||
}
|
||||
|
||||
OrderBookVM _mk_orderbook_vm_common(orderbook.FilterStates filters, IEnumerable<ColumnKeys> hide_counterparts) {
|
||||
var state = new orderbook.OrderBookStartupData(filters, orderbook.OrderBookStartupData.default_mini_grid, use_defaults: false);
|
||||
state.orderbook_grid.general = _hide_cols(state.orderbook_grid.general, hide_counterparts);
|
||||
var settings_man = new WindowSettingsManager<orderbook.OrderBookGridSettings>.DisconnectedImpl(state.orderbook_grid);
|
||||
var obvm = new OrderBookVM(_bus, null, _orders_model, settings_man, true);
|
||||
foreach (var c in OrderBookVM.OrderBookColumnCollection.editable_columns)
|
||||
obvm.columns.Insert(0, c);
|
||||
obvm.load_from(state);
|
||||
return obvm;
|
||||
}
|
||||
#endregion
|
||||
|
||||
static GridSettings _hide_cols(GridSettings grid, IEnumerable<ColumnKeys> columns) =>
|
||||
grid.with(_hide_cols(grid.columns, columns));
|
||||
|
||||
static IEnumerable<GridColumnSettings> _hide_cols(IEnumerable<GridColumnSettings> cols, IEnumerable<ColumnKeys> columns) {
|
||||
// hide columns who's data is implied by the grouping
|
||||
var col_hash = new HashSet<string>(columns.Select(k => k.ToString()));
|
||||
foreach (var c in cols)
|
||||
yield return col_hash.Contains(c.id) ? c.with(is_visible: false) : c;
|
||||
}
|
||||
|
||||
#region Fills Popup Utils
|
||||
static void _append_key_filters(fills.FillsFilterStates f, ColumnKeys key) {
|
||||
switch (key) {
|
||||
case ColumnKeys.SODNetPos:
|
||||
case ColumnKeys.SODLongPos:
|
||||
case ColumnKeys.SODShortPos:
|
||||
f.fill_kind = new[] { Fill.Types.FillKind.Sod };
|
||||
break;
|
||||
case ColumnKeys.DayNetPos:
|
||||
case ColumnKeys.DayLongPos:
|
||||
case ColumnKeys.DayShortPos:
|
||||
f.fill_kind = new[] { Fill.Types.FillKind.Standard, Fill.Types.FillKind.Leg };
|
||||
break;
|
||||
}
|
||||
switch (key) {
|
||||
case ColumnKeys.SODLongPos:
|
||||
case ColumnKeys.DayLongPos:
|
||||
case ColumnKeys.TotalLongPos:
|
||||
f.side = new[] { Side.Buy };
|
||||
break;
|
||||
case ColumnKeys.SODShortPos:
|
||||
case ColumnKeys.DayShortPos:
|
||||
case ColumnKeys.TotalShortPos:
|
||||
f.side = new[] { Side.Sell };
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void _append_group_filters(fills.FillsFilterStates f, PNLRowCollectionViewGroup group, IReadOnlyList<ColumnKeys> anchors) {
|
||||
if (group != null) {
|
||||
int anchor_pos = group.level - 1;
|
||||
if (anchor_pos < 0)
|
||||
throw new InvalidOperationException("Group level is less than 1.");
|
||||
if (anchor_pos >= anchors.Count)
|
||||
throw new InvalidOperationException("Group level is greater than the number of anchors.");
|
||||
_append_fills_filter(f, group, anchors[anchor_pos]);
|
||||
group = group.parent;
|
||||
while (anchor_pos-- > 0 && group != null) {
|
||||
_append_fills_filter(f, group, anchors[anchor_pos]);
|
||||
group = group.parent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void _append_fills_filter(fills.FillsFilterStates filter, PNLRowCollectionViewGroup group, ColumnKeys key) {
|
||||
switch (key) {
|
||||
case ColumnKeys.Account: filter.account = new[] { group.Name.ToString() }; break;
|
||||
case ColumnKeys.Symbol: filter.symbol = new[] { group.Name.ToString() }; break;
|
||||
case ColumnKeys.Exchange: filter.exchange = new[] { (Venue)group.Name }; break;
|
||||
case ColumnKeys.Security:
|
||||
if (group.Name is SecurityImmutable sec)
|
||||
filter.security = new SecurityCriteria(sec);
|
||||
break;
|
||||
default: throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
static void _append_row_filters(fills.FillsFilterStates f, PNLRowVM row) {
|
||||
f.account = new[] { row.account_name };
|
||||
f.security = SecurityCriteria.from_keys(new[] { row.key.security_id });
|
||||
}
|
||||
|
||||
static void _append_key_filters(orderbook.FilterStates f, ColumnKeys key) {
|
||||
switch (key) {
|
||||
case ColumnKeys.WorkingLongPos:
|
||||
case ColumnKeys.DayLongPos:
|
||||
case ColumnKeys.TotalLongPos:
|
||||
f.sides = new[] { Side.Buy };
|
||||
break;
|
||||
case ColumnKeys.WorkingShortPos:
|
||||
case ColumnKeys.DayShortPos:
|
||||
case ColumnKeys.TotalShortPos:
|
||||
f.sides = new[] { Side.Sell };
|
||||
break;
|
||||
}
|
||||
switch (key) {
|
||||
case ColumnKeys.WorkingLongPos:
|
||||
case ColumnKeys.WorkingShortPos:
|
||||
case ColumnKeys.WorkingNetPos:
|
||||
f.statuses = new[] { OrderStatus.PendingWorking, OrderStatus.Working };
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void _append_group_filters(orderbook.FilterStates f, PNLRowCollectionViewGroup group, IReadOnlyList<ColumnKeys> anchors) {
|
||||
if (group != null) {
|
||||
int anchor_pos = group.level - 1;
|
||||
if (anchor_pos < 0)
|
||||
throw new InvalidOperationException("Group level is less than 1.");
|
||||
if (anchor_pos >= anchors.Count)
|
||||
throw new InvalidOperationException("Group level is greater than the number of anchors.");
|
||||
_append_orderbook_filter(f, group, anchors[anchor_pos]);
|
||||
group = group.parent;
|
||||
while (anchor_pos-- > 0 && group != null) {
|
||||
_append_orderbook_filter(f, group, anchors[anchor_pos]);
|
||||
group = group.parent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void _append_row_filters(orderbook.FilterStates f, PNLRowVM row) {
|
||||
f.accounts = new[] { row.account_name };
|
||||
f.security_criteria = SecurityCriteria.from_keys(new[] { row.key.security_id });
|
||||
}
|
||||
|
||||
static void _append_orderbook_filter(orderbook.FilterStates filter, PNLRowCollectionViewGroup g, ColumnKeys key) {
|
||||
switch (key) {
|
||||
case ColumnKeys.Account: filter.accounts = new[] { g.Name.ToString() }; break;
|
||||
case ColumnKeys.Symbol: filter.symbols = new[] { g.Name.ToString() }; break;
|
||||
case ColumnKeys.Exchange: filter.exchanges = new[] { (Venue)g.Name }; break;
|
||||
case ColumnKeys.Security:
|
||||
if (g.Name is SecurityImmutable sec)
|
||||
filter.security_criteria = new SecurityCriteria(sec);
|
||||
break;
|
||||
default: throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
void _update((TableUpdate<PNLRowData, PNLKey> filtered, TableUpdate<PNLRowData, PNLKey> raw) tup) {
|
||||
if (_is_paused) {
|
||||
_resume = tup;
|
||||
return;
|
||||
}
|
||||
var (filtered, raw) = tup;
|
||||
int old_total_row_count = total_row_count;
|
||||
int old_filtered_row_count = filtered_row_count;
|
||||
var total_pnl = total_aggregate.data;
|
||||
// don't adjust selected_pnl incrementally - it may be possible for the selection to change throughout the update, making incremental calculations tricky
|
||||
foreach (var v in filtered.removed) {
|
||||
int i = CollectionHelper.binary_search(_rows_data_wrapper, v, PNLRowData.key_comparer);
|
||||
Log.assert(i >= 0, $"{typeof(PNLVM)} received an update with a removed item that was not found via binary search.");
|
||||
if (i >= 0) {
|
||||
if (rows[i].security_type == SecurityType.Future)
|
||||
total_pnl -= rows[i].normalized_data;
|
||||
rows.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
foreach (var v in filtered.added) {
|
||||
var r = new PNLRowVM(v.with(calc_type: calculation_type), _default_appearances, _triggers);
|
||||
rows.InsertSorted(r, PNLRowVM.key_comparer);
|
||||
if (r.security_type == SecurityType.Future)
|
||||
total_pnl += r.normalized_data;
|
||||
}
|
||||
foreach (var d in filtered.changed) {
|
||||
int i = CollectionHelper.binary_search(_rows_data_wrapper, d, PNLRowData.key_comparer);
|
||||
if (i >= 0) {
|
||||
var r = rows[i];
|
||||
if (r.security_type == SecurityType.Future)
|
||||
total_pnl -= r.normalized_data;
|
||||
r.update(d);
|
||||
if (r.security_type == SecurityType.Future)
|
||||
total_pnl += r.normalized_data;
|
||||
}
|
||||
}
|
||||
if (_selected_rows.Count > 0) {
|
||||
selected_aggregate.data = _selected_rows
|
||||
.Where(r => r.security_type == SecurityType.Future)
|
||||
.Aggregate(PNLNormalizedData.zero, (d, r) => d + r.normalized_data);
|
||||
}
|
||||
//
|
||||
total_aggregate.data = total_pnl;
|
||||
total_row_count = tup.raw.state.Count;
|
||||
if (total_row_count != old_total_row_count)
|
||||
RaisePropertyChanged(nameof(total_row_count));
|
||||
if (filtered_row_count != old_filtered_row_count)
|
||||
RaisePropertyChanged(nameof(filtered_row_count));
|
||||
_updated?.Invoke(filtered);
|
||||
}
|
||||
|
||||
void IHandleRowSelection.row_selection_changed(SelectionChangedEventArgs e) {
|
||||
RowSelectionHelper.apply_changes(_selected_rows, e);
|
||||
// adding/subtracting pnl incrementally can lose precision over time, due to rounding errors.
|
||||
// do the full calculation on each selection change, as long as it doesn't hurt perf (which seems unlikely).
|
||||
selected_aggregate.data = _selected_rows.Aggregate(PNLNormalizedData.zero, (d, r) => d + r.normalized_data);
|
||||
_selection_changed?.Invoke(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using bts.collections;
|
||||
using mbts.ui.utility;
|
||||
using System.Collections.Generic;
|
||||
using System.Dynamic;
|
||||
|
||||
namespace bts.shell.pnl {
|
||||
class AnchorIndex {
|
||||
readonly IDictionary<PNLView.Columns, bool> _wrapped_anchors;
|
||||
|
||||
public AnchorIndex() {
|
||||
IDictionary<string, object> anchors_dict = anchors = new ExpandoObject();
|
||||
_wrapped_anchors = MappedDictionary.create_mutable(
|
||||
anchors_dict,
|
||||
EnumHelper.parse<PNLView.Columns>,
|
||||
key => key.ToString(),
|
||||
obj => (bool)obj,
|
||||
_ => _);
|
||||
// initialize the dictionary with every key (the keys should never change)
|
||||
foreach (var k in _keys)
|
||||
_wrapped_anchors[k] = false;
|
||||
}
|
||||
|
||||
public dynamic anchors { get; }
|
||||
|
||||
IReadOnlyList<PNLView.Columns> _keys => EnumHelper.get_values<PNLView.Columns>();
|
||||
|
||||
public void reset(IEnumerable<PNLView.Columns> anchor_list) {
|
||||
var anchor_set = new HashSet<PNLView.Columns>(anchor_list.empty_if_null());
|
||||
foreach (var key in _keys)
|
||||
_wrapped_anchors[key] = anchor_set.Contains(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
namespace bts.shell.pnl {
|
||||
readonly struct PNLCell {
|
||||
public readonly object item;
|
||||
public readonly PNLView.Columns column;
|
||||
|
||||
public PNLCell(object item, PNLView.Columns column) {
|
||||
this.item = item;
|
||||
this.column = column;
|
||||
}
|
||||
|
||||
public override string ToString() =>
|
||||
$"Item: {item}, Column: {column}";
|
||||
}
|
||||
|
||||
readonly struct PNLRowCell {
|
||||
public readonly PNLRowVM row;
|
||||
public readonly PNLView.Columns column;
|
||||
|
||||
public PNLRowCell(PNLRowVM row, PNLView.Columns column) {
|
||||
this.row = row;
|
||||
this.column = column;
|
||||
}
|
||||
}
|
||||
|
||||
readonly struct PNLGroupCell {
|
||||
public readonly PNLRowCollectionViewGroup group;
|
||||
public readonly PNLView.Columns column;
|
||||
|
||||
public PNLGroupCell(PNLRowCollectionViewGroup group, PNLView.Columns column) {
|
||||
this.group = group;
|
||||
this.column = column;
|
||||
}
|
||||
}
|
||||
|
||||
readonly struct PNLHeaderRowCell {
|
||||
public readonly PNLHeaderRowDisplay header_row;
|
||||
public readonly PNLView.Columns column;
|
||||
|
||||
public PNLHeaderRowCell(PNLHeaderRowDisplay header_row, PNLView.Columns column) {
|
||||
this.header_row = header_row;
|
||||
this.column = column;
|
||||
}
|
||||
}
|
||||
|
||||
static class PNLCellExtensions {
|
||||
public static OneOf<PNLRowCell, PNLGroupCell, PNLHeaderRowCell> oneof(this PNLCell cell) =>
|
||||
OneOf<PNLRowVM, PNLRowCollectionViewGroup, PNLHeaderRowDisplay>.infer(cell.item).Extend(
|
||||
r => new PNLRowCell(r, cell.column),
|
||||
g => new PNLGroupCell(g, cell.column),
|
||||
h => new PNLHeaderRowCell(h, cell.column));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
using bts.utils;
|
||||
using mbts.ui.controls;
|
||||
using mbts.ui.shell;
|
||||
using mbts.ui.utility.text;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using FontFamily = System.Windows.Media.FontFamily;
|
||||
|
||||
namespace bts.shell.pnl {
|
||||
class PNLColumn : DIYGrid.ColumnBase<PNLView.Columns>, IRaiseNotifyPropertyChanged {
|
||||
static readonly IPropertyChangedDetector<PNLColumn>
|
||||
_group_settings_change_detector = ObjectSnapshot.build_detector<PNLColumn>(
|
||||
nameof(group_font_weight_override),
|
||||
nameof(group_font_weight),
|
||||
nameof(group_font_color_override),
|
||||
nameof(group_font_color),
|
||||
nameof(group_font_family),
|
||||
nameof(group_font_size)),
|
||||
_header_settings_change_detector = ObjectSnapshot.build_detector<PNLColumn>(
|
||||
nameof(header_font_weight_override),
|
||||
nameof(header_font_weight),
|
||||
nameof(header_font_color_override),
|
||||
nameof(header_font_color),
|
||||
nameof(header_font_family),
|
||||
nameof(header_font_size));
|
||||
|
||||
double _group_font_size = 11, _header_font_size = 12;
|
||||
int? _anchor_index;
|
||||
FontFamily
|
||||
_group_font_family = TextUtils.StandardFontFamily,
|
||||
_header_font_family = TextUtils.StandardFontFamily;
|
||||
|
||||
public PNLColumn() {
|
||||
// default to be hidden, so columns are not rendered until their settings are loaded,
|
||||
// in which case, they will only be rendered if 'settings.is_visible' is true.
|
||||
is_visible = false;
|
||||
}
|
||||
|
||||
public int? anchor_index {
|
||||
get => _anchor_index;
|
||||
set => set(ref _anchor_index, value);
|
||||
}
|
||||
// group
|
||||
public FontWeight? group_font_weight_override { get; private set; }
|
||||
public Color? group_font_color_override { get; private set; }
|
||||
public FontFamily group_font_family => _group_font_family ?? font_family;
|
||||
public double group_font_size => double.IsNaN(_group_font_size) || _group_font_size == 0 ? font_size : _group_font_size;
|
||||
public FontWeight group_font_weight => group_font_weight_override ?? font_weight;
|
||||
public Color group_font_color => group_font_color_override ?? font_color;
|
||||
|
||||
public FontWeight? header_font_weight_override { get; private set; }
|
||||
public Color? header_font_color_override { get; private set; }
|
||||
public FontFamily header_font_family => _header_font_family ?? font_family;
|
||||
public double header_font_size => double.IsNaN(_header_font_size) || _header_font_size == 0 ? font_size : _header_font_size;
|
||||
public FontWeight header_font_weight => header_font_weight_override ?? font_weight;
|
||||
public Color header_font_color => header_font_color_override ?? font_color;
|
||||
|
||||
public void load_group_params(
|
||||
double group_font_size = double.NaN, FontWeight? group_font_weight = null, Color? group_font_color = null, FontFamily group_font_family = null) {
|
||||
using (_group_settings_change_detector.detect_changes(this)) {
|
||||
_group_font_size = group_font_size;
|
||||
_group_font_family = group_font_family;
|
||||
group_font_weight_override = group_font_weight;
|
||||
group_font_color_override = group_font_color;
|
||||
}
|
||||
}
|
||||
|
||||
public void load_header_params(
|
||||
double header_font_size = double.NaN, FontWeight? header_font_weight = null, Color? header_font_color = null, FontFamily header_font_family = null) {
|
||||
using (_header_settings_change_detector.detect_changes(this)) {
|
||||
_header_font_size = header_font_size;
|
||||
_header_font_family = header_font_family;
|
||||
header_font_weight_override = header_font_weight;
|
||||
header_font_color_override = header_font_color;
|
||||
}
|
||||
}
|
||||
|
||||
void IRaiseNotifyPropertyChanged.raise_notify_property_changed(string prop) =>
|
||||
raise_prop_changed(prop);
|
||||
|
||||
protected override void on_key_initialized(PNLView.Columns key) =>
|
||||
id = key.ToString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,458 @@
|
||||
using bts.collections;
|
||||
using bts.utils;
|
||||
using mbts;
|
||||
using mbts.ui;
|
||||
using mbts.ui.shell;
|
||||
using mbts.ui.utility;
|
||||
using mbts.utils;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Specialized;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Windows.Data;
|
||||
using CellAnimation = mbts.ui.spark.CellAnimation;
|
||||
using ColumnKeys = bts.shell.pnl.PNLView.Columns;
|
||||
|
||||
namespace bts.shell.pnl {
|
||||
class PNLRowCollectionView : ListCollectionView {
|
||||
ReadOnlyObservableCollection<object> _base_groups, _groups_wrapper;
|
||||
ObservableAutoWrapper<object, object> _groups;
|
||||
// appearance/triggers
|
||||
ImmutableDictionary<ColumnKeys, PNLCellAppearance> _defaults_appearances;
|
||||
IReadOnlyList<IPNLAnimationTrigger<PNLNormalizedData>> _triggers;
|
||||
|
||||
public PNLRowCollectionView(IList list, ImmutableDictionary<ColumnKeys, PNLCellAppearance> default_appearances, IReadOnlyList<IPNLAnimationTrigger<PNLNormalizedData>> triggers) : base(list) {
|
||||
_defaults_appearances = default_appearances;
|
||||
_triggers = triggers;
|
||||
}
|
||||
|
||||
public override bool CanFilter => true;
|
||||
public override bool CanGroup => true;
|
||||
|
||||
public override ReadOnlyObservableCollection<object> Groups {
|
||||
get {
|
||||
if (_groups_wrapper == null) {
|
||||
_base_groups = base.Groups;
|
||||
if (_base_groups == null)
|
||||
return null;
|
||||
_groups = new ObservableAutoWrapper<object, object>(_base_groups, obj => new PNLRowCollectionViewGroup((CollectionViewGroup)obj, this, _defaults_appearances, _triggers));
|
||||
_groups_wrapper = new ReadOnlyObservableCollection<object>(_groups);
|
||||
}
|
||||
return _groups_wrapper;
|
||||
}
|
||||
}
|
||||
|
||||
public void on_settings(ImmutableDictionary<ColumnKeys, PNLCellAppearance> default_appearances, IReadOnlyList<IPNLAnimationTrigger<PNLNormalizedData>> triggers) {
|
||||
_defaults_appearances = default_appearances;
|
||||
_triggers = triggers;
|
||||
foreach (var g in _groups_wrapper.OfTypeSafe<PNLRowCollectionViewGroup>())
|
||||
g.on_settings(default_appearances, triggers);
|
||||
}
|
||||
|
||||
public void set_all_expanded(bool is_expanded) {
|
||||
foreach (var g in Groups.OfTypeSafe<PNLRowCollectionViewGroup>())
|
||||
g.set_all_expanded(is_expanded);
|
||||
}
|
||||
}
|
||||
|
||||
class PNLRowCollectionViewGroup : CollectionViewGroup, IRaiseNotifyPropertyChanged, IEquatable<PNLRowCollectionViewGroup> {
|
||||
readonly PNLRowCollectionView _root;
|
||||
readonly CollectionViewGroup _from;
|
||||
readonly Dictionary<object, InnerGroupSubscription> _inner_groups = new Dictionary<object, InnerGroupSubscription>();
|
||||
readonly Dictionary<PNLKey, IDisposable> _rows = new Dictionary<PNLKey, IDisposable>();
|
||||
readonly VersionContainer<bool?> _is_all_selected;
|
||||
readonly DeferrableActionWrapper _refresh_is_all_selected;
|
||||
readonly HashSet<PNLRowVM> _selected_rows = new HashSet<PNLRowVM>();
|
||||
readonly PNLCellIndexBuilder<PNLNormalizedData> _cell_map = new PNLCellIndexBuilder<PNLNormalizedData>();
|
||||
readonly PNLCellEmbeddedAnimationModel<PNLNormalizedData> _animator;
|
||||
ImmutableDictionary<ColumnKeys, PNLCellAppearance> _defaults_appearances;
|
||||
IReadOnlyList<IPNLAnimationTrigger<PNLNormalizedData>> _triggers;
|
||||
IDisposable _dispose;
|
||||
|
||||
bool _is_expanded = true;
|
||||
int _selected_item_count_backing;
|
||||
|
||||
public PNLRowCollectionViewGroup(CollectionViewGroup from, PNLRowCollectionView root, ImmutableDictionary<ColumnKeys, PNLCellAppearance> default_appearances, IReadOnlyList<IPNLAnimationTrigger<PNLNormalizedData>> triggers) : base(from.Name) {
|
||||
_root = root;
|
||||
_from = from;
|
||||
_defaults_appearances = default_appearances;
|
||||
_triggers = triggers;
|
||||
_animator = new PNLCellEmbeddedAnimationModel<PNLNormalizedData>(default_appearances, triggers);
|
||||
_animator.on_row(normalized_data.data);
|
||||
normalized_data.data_changed += () => {
|
||||
_cell_map.on_row(normalized_data.data);
|
||||
_animator.on_row(normalized_data.data);
|
||||
_cell_map.set_appearances(_animator.cell_appearances);
|
||||
};
|
||||
//sod_net_pos
|
||||
_is_all_selected = new VersionContainer<bool?>(() => {
|
||||
if (_selected_item_count == 0)
|
||||
return false;
|
||||
if (_selected_item_count == ProtectedItemCount)
|
||||
return true;
|
||||
return null;
|
||||
});
|
||||
bool? last_published_all_selected = _is_all_selected.value;
|
||||
_refresh_is_all_selected = new DeferrableActionWrapper(() => {
|
||||
if (_is_all_selected.value != last_published_all_selected) {
|
||||
last_published_all_selected = is_all_selected;
|
||||
OnPropertyChanged(new PropertyChangedEventArgs(nameof(is_all_selected)));
|
||||
}
|
||||
});
|
||||
// sod
|
||||
sod_pnl = _cell_map.add_pnl(ColumnKeys.SODPNL, d => d.sod_pnl);
|
||||
sod_net_pos = _cell_map.add_net_pos(ColumnKeys.SODNetPos, d => d.sod_net_pos);
|
||||
sod_long_pos = _cell_map.add_long_pos(ColumnKeys.SODLongPos, d => d.sod_long_pos);
|
||||
sod_short_pos = _cell_map.add_short_pos(ColumnKeys.SODShortPos, d => d.sod_short_pos);
|
||||
// day
|
||||
day_pnl = _cell_map.add_pnl(ColumnKeys.DayPNL, d => d.day_pnl);
|
||||
day_net_pos = _cell_map.add_net_pos(ColumnKeys.DayNetPos, d => d.day_net_pos);
|
||||
day_long_pos = _cell_map.add_long_pos(ColumnKeys.DayLongPos, d => d.day_long_pos);
|
||||
day_short_pos = _cell_map.add_short_pos(ColumnKeys.DayShortPos, d => d.day_short_pos);
|
||||
// total
|
||||
total_pnl = _cell_map.add(ColumnKeys.TotalPNL, d => d.total_pnl, FormattingDelegates.Price<decimal>.Value);
|
||||
total_net_pos = _cell_map.add(ColumnKeys.TotalNetPos, d => d.total_net_pos);
|
||||
total_long_pos = _cell_map.add(ColumnKeys.TotalLongPos, d => d.total_long_pos);
|
||||
total_short_pos = _cell_map.add(ColumnKeys.TotalShortPos, d => d.total_short_pos);
|
||||
// working
|
||||
working_net_pos = _cell_map.add(ColumnKeys.WorkingNetPos, d => d.working_net_pos);
|
||||
working_long_pos = _cell_map.add(ColumnKeys.WorkingLongPos, d => d.working_long_pos);
|
||||
working_short_pos = _cell_map.add(ColumnKeys.WorkingShortPos, d => d.working_short_pos);
|
||||
|
||||
_cell_map.on_row(normalized_data.data);
|
||||
_cell_map.set_appearances(_animator.cell_appearances);
|
||||
((INotifyCollectionChanged)_from.Items).CollectionChanged += _from_items_CollectionChanged;
|
||||
_from_items_CollectionChanged(_from.Items, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, _from.Items, 0));
|
||||
|
||||
_dispose = _cell_map.animation_model.OnDispatcher().Subscribe(new_animations => {
|
||||
//stopwatch.Start();
|
||||
animations = new_animations.Select(kv => new CellAnimation(kv.Key.column, kv.Key.type, kv.Value.color.to_brush())).ToList();
|
||||
OnPropertyChanged(new PropertyChangedEventArgs(nameof(animations)));
|
||||
});
|
||||
}
|
||||
|
||||
public IList<CellAnimation> animations { get; private set; }
|
||||
|
||||
public event Action ProtectedItemCountChanged, selected_item_count_changed;
|
||||
|
||||
public PNLNormalizedDataVM normalized_data { get; } = new PNLNormalizedDataVM();
|
||||
|
||||
// sod
|
||||
public PNLCellVM<PNLNormalizedData, decimal> sod_pnl { get; }
|
||||
public PNLCellVM<PNLNormalizedData, decimal> sod_net_pos { get; }
|
||||
public PNLCellVM<PNLNormalizedData, decimal> sod_long_pos { get; }
|
||||
public PNLCellVM<PNLNormalizedData, decimal> sod_short_pos { get; }
|
||||
// day
|
||||
public PNLCellVM<PNLNormalizedData, decimal> day_pnl { get; }
|
||||
public PNLCellVM<PNLNormalizedData, decimal> day_net_pos { get; }
|
||||
public PNLCellVM<PNLNormalizedData, decimal> day_long_pos { get; }
|
||||
public PNLCellVM<PNLNormalizedData, decimal> day_short_pos { get; }
|
||||
// total
|
||||
public PNLCellVM<PNLNormalizedData, decimal> total_pnl { get; }
|
||||
public PNLCellVM<PNLNormalizedData, decimal> total_net_pos { get; }
|
||||
public PNLCellVM<PNLNormalizedData, decimal> total_long_pos { get; }
|
||||
public PNLCellVM<PNLNormalizedData, decimal> total_short_pos { get; }
|
||||
// working
|
||||
public PNLCellVM<PNLNormalizedData, decimal> working_net_pos { get; }
|
||||
public PNLCellVM<PNLNormalizedData, decimal> working_long_pos { get; }
|
||||
public PNLCellVM<PNLNormalizedData, decimal> working_short_pos { get; }
|
||||
//
|
||||
public PNLRowCollectionViewGroup parent { get; private set; }
|
||||
public int level => parent == null ? 1 : parent.level + 1;
|
||||
public override bool IsBottomLevel => _from.IsBottomLevel;
|
||||
|
||||
public IEnumerable<PNLRowCollectionViewGroup> ancestors {
|
||||
get {
|
||||
if (parent != null) {
|
||||
var p = parent;
|
||||
do {
|
||||
yield return p;
|
||||
p = p.parent;
|
||||
} while (p != null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public IReadOnlyList<object> GroupNamePath {
|
||||
get {
|
||||
var path = new List<object> { Name };
|
||||
foreach (var a in ancestors)
|
||||
path.Add(a.Name);
|
||||
return path.WrapReverseRO();
|
||||
}
|
||||
}
|
||||
|
||||
public bool is_expanded { get => _is_expanded; set => _set(ref _is_expanded, value); }
|
||||
public bool? is_all_selected {
|
||||
get => _is_all_selected.value;
|
||||
set {
|
||||
// The setter should not be called while is_all_selected deferred.
|
||||
// However, it is good to be defensive, so let's add a conditional check anyways:
|
||||
// if 'is_all_selected' is deferred, the value must be blindly propagated,
|
||||
// bc the equality comparison is erroneous, as the current state is unknown.
|
||||
if (value.HasValue && value != is_all_selected) {
|
||||
using (_refresh_is_all_selected.defer_execution()) {
|
||||
foreach (var i in Items) {
|
||||
if (i is PNLRowCollectionViewGroup g)
|
||||
g.is_all_selected = value;
|
||||
else if (i is PNLRowVM r)
|
||||
r.is_selected = value.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
new int ProtectedItemCount {
|
||||
get => base.ProtectedItemCount;
|
||||
set {
|
||||
if (base.ProtectedItemCount != value) {
|
||||
base.ProtectedItemCount = value;
|
||||
_is_all_selected.increment_version();
|
||||
ProtectedItemCountChanged?.Invoke();
|
||||
_refresh_is_all_selected.execute();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int _selected_item_count {
|
||||
get => _selected_item_count_backing;
|
||||
set {
|
||||
Log.assert(value >= 0, $"{nameof(_selected_item_count)} cannot be negative.");
|
||||
_selected_item_count_backing = value;
|
||||
_is_all_selected.increment_version();
|
||||
_refresh_is_all_selected.execute();
|
||||
selected_item_count_changed?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
public void on_settings(ImmutableDictionary<ColumnKeys, PNLCellAppearance> default_appearances, IReadOnlyList<IPNLAnimationTrigger<PNLNormalizedData>> triggers) {
|
||||
_defaults_appearances = default_appearances;
|
||||
_triggers = triggers;
|
||||
if (Name.Equals("GC")) {
|
||||
|
||||
}
|
||||
_animator.on_settings(default_appearances, triggers);
|
||||
_cell_map.set_appearances(_animator.cell_appearances);
|
||||
foreach (var g in ProtectedItems.OfTypeSafe<PNLRowCollectionViewGroup>())
|
||||
g.on_settings(default_appearances, triggers);
|
||||
}
|
||||
|
||||
public void set_all_expanded(bool is_expanded) {
|
||||
this.is_expanded = is_expanded;
|
||||
foreach (var g in Items.OfTypeSafe<PNLRowCollectionViewGroup>())
|
||||
g.set_all_expanded(is_expanded);
|
||||
}
|
||||
|
||||
public bool Equals(PNLRowCollectionViewGroup other) =>
|
||||
other != null &&
|
||||
level == other.level &&
|
||||
Equals(Name, other.Name);
|
||||
|
||||
void _select_all() {
|
||||
foreach (var i in Items) {
|
||||
if (i is PNLRowCollectionViewGroup cvg) {
|
||||
cvg._select_all();
|
||||
} else if (i is PNLRowVM row) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IDisposable _detect_is_all_selected_changed() {
|
||||
bool? was_all_selected = is_all_selected;
|
||||
return Disposable.Create(() => {
|
||||
if (is_all_selected != was_all_selected)
|
||||
OnPropertyChanged(new PropertyChangedEventArgs(nameof(is_all_selected)));
|
||||
});
|
||||
}
|
||||
|
||||
void _add_item(int index, object item) {
|
||||
Log.assert(index >= 0, "Index is less than 0.");
|
||||
if (item is CollectionViewGroup group) {
|
||||
var inner_group = new PNLRowCollectionViewGroup(group, _root, _defaults_appearances, _triggers);
|
||||
_inner_groups.Add(group.Name, new InnerGroupSubscription(this, inner_group));
|
||||
ProtectedItems.Insert(index, inner_group);
|
||||
} else {
|
||||
var row = (PNLRowVM)item;
|
||||
ProtectedItems.Insert(index, item);
|
||||
_rows.Add(row.key, _connect_row(row));
|
||||
}
|
||||
}
|
||||
|
||||
void _remove_item(int index, object item) {
|
||||
if (item is PNLRowCollectionViewGroup inner_group) {
|
||||
if (!_inner_groups.TryPopValue(inner_group.Name, out var ig_sub))
|
||||
throw new InvalidOperationException($"Inner groups cache does not have an entry for the specified name '{inner_group.Name}' (entire tree structure may be compromised).");
|
||||
ig_sub.Dispose();
|
||||
Disposable.dispose(ref inner_group._dispose);
|
||||
} else {
|
||||
var row = (PNLRowVM)item;
|
||||
if (!_rows.TryPopValue(row.key, out var row_sub))
|
||||
throw new InvalidOperationException($"Inner rows cache does not have an entry for the specified irowstem key '{row.key}' (entire tree structure may be compromised).");
|
||||
row_sub.Dispose();
|
||||
}
|
||||
ProtectedItems.RemoveAt(index);
|
||||
}
|
||||
|
||||
void _from_items_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) {
|
||||
using (_refresh_is_all_selected.defer_execution()) {
|
||||
switch (e.Action) {
|
||||
case NotifyCollectionChangedAction.Add:
|
||||
for (int i = 0; i < e.NewItems.Count; i++)
|
||||
_add_item(e.NewStartingIndex + i, e.NewItems[i]);
|
||||
break;
|
||||
case NotifyCollectionChangedAction.Remove:
|
||||
for (int i = 0; i < e.OldItems.Count; i++)
|
||||
_remove_item(e.OldStartingIndex, ProtectedItems[e.OldStartingIndex]);
|
||||
break;
|
||||
case NotifyCollectionChangedAction.Replace:
|
||||
for (int i = 0; i < e.OldItems.Count; i++)
|
||||
_remove_item(e.OldStartingIndex, ProtectedItems[e.OldStartingIndex]);
|
||||
for (int i = 0; i < e.NewItems.Count; i++)
|
||||
_add_item(e.NewStartingIndex + i, e.NewItems[i]);
|
||||
break;
|
||||
case NotifyCollectionChangedAction.Reset:
|
||||
while (ProtectedItems.Count > 0)
|
||||
_remove_item(0, ProtectedItems[0]);
|
||||
for (int i = 0; i < _from.Items.Count; i++)
|
||||
_add_item(i, _from.Items[i]);
|
||||
break;
|
||||
}
|
||||
_is_all_selected.increment_version();
|
||||
_refresh_is_all_selected.execute();
|
||||
}
|
||||
}
|
||||
|
||||
IDisposable _connect_row(PNLRowVM row) {
|
||||
using (_detect_is_all_selected_changed()) {
|
||||
if (Name.Equals("GC")) {
|
||||
|
||||
}
|
||||
var last_data = PNLNormalizedData.zero;
|
||||
on_updated(row.normalized_data);
|
||||
row.updated += on_updated;
|
||||
row.is_selected_changed += on_is_selected_changed;
|
||||
if (row.is_selected) {
|
||||
Log.assert(_selected_rows.Add(row), $"Selected Rows out of sync ({Name}): hash set already contained selected row that was just added to group ({row}).");
|
||||
_selected_item_count++;
|
||||
}
|
||||
ProtectedItemCount++;
|
||||
return Disposable.Create(() => {
|
||||
row.updated -= on_updated;
|
||||
row.is_selected_changed -= on_is_selected_changed;
|
||||
using (_refresh_is_all_selected.defer_execution()) {
|
||||
if (row.is_selected) {
|
||||
Log.assert(_selected_rows.Remove(row), $"Selected Rows out of sync ({Name}): hash set did not contain selected row that was just removed from group ({row}).");
|
||||
_selected_item_count--;
|
||||
}
|
||||
var newval = normalized_data.data - row.normalized_data;
|
||||
normalized_data.data = newval;
|
||||
ProtectedItemCount--;
|
||||
}
|
||||
});
|
||||
void on_updated(PNLNormalizedData new_data) {
|
||||
if (Name.Equals("GC")) {
|
||||
|
||||
}
|
||||
var less_last = normalized_data.data - last_data;
|
||||
var plus_new = less_last + new_data;
|
||||
normalized_data.data += new_data - last_data;
|
||||
if (plus_new != normalized_data.data) {
|
||||
|
||||
}
|
||||
Log.assert(plus_new == normalized_data.data);
|
||||
last_data = new_data;
|
||||
}
|
||||
void on_is_selected_changed(bool is_selected) {
|
||||
if (is_selected) {
|
||||
Log.assert(_selected_rows.Add(row), $"Selected Rows out of sync ({Name}): hash set already contained row that was just selected ({row}).");
|
||||
_selected_item_count++;
|
||||
} else {
|
||||
Log.assert(_selected_rows.Remove(row), $"Selected Rows out of sync ({Name}): hash set did not contain row that was just unselected ({row}).");
|
||||
_selected_item_count--;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _set<T>(ref T backing, T new_value, [CallerMemberName]string prop = "") =>
|
||||
_set(ref backing, new_value, EqualityComparer<T>.Default, prop);
|
||||
|
||||
void _set<T>(ref T backing, T new_value, IEqualityComparer<T> comparer, [CallerMemberName]string prop = "") {
|
||||
if (!comparer.Equals(backing, new_value)) {
|
||||
backing = new_value;
|
||||
OnPropertyChanged(new PropertyChangedEventArgs(prop));
|
||||
}
|
||||
}
|
||||
|
||||
void IRaiseNotifyPropertyChanged.raise_notify_property_changed(string prop) =>
|
||||
OnPropertyChanged(new PropertyChangedEventArgs(prop));
|
||||
|
||||
class InnerGroupSubscription : IDisposable {
|
||||
public readonly PNLRowCollectionViewGroup _inner_group;
|
||||
readonly PNLRowCollectionViewGroup _parent;
|
||||
PNLNormalizedData _last_data = PNLNormalizedData.zero;
|
||||
int _last_count, _last_selected_item_count;
|
||||
|
||||
public InnerGroupSubscription(PNLRowCollectionViewGroup parent, PNLRowCollectionViewGroup inner_group) {
|
||||
_parent = parent;
|
||||
_inner_group = inner_group;
|
||||
_inner_group.parent = parent;
|
||||
// init item count
|
||||
_on_item_count_changed();
|
||||
// listen to item count changes
|
||||
_inner_group.ProtectedItemCountChanged += _on_item_count_changed;
|
||||
// init selected item count
|
||||
_on_selected_item_count_changed();
|
||||
// listen to selected item count changes
|
||||
_inner_group.selected_item_count_changed += _on_selected_item_count_changed;
|
||||
// init data
|
||||
_on_normalized_data_changed();
|
||||
// listen to data changes
|
||||
_inner_group.normalized_data.data_changed += _on_normalized_data_changed;
|
||||
_parent._selected_item_count += _inner_group._selected_item_count;
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
_inner_group.ProtectedItemCountChanged -= _on_item_count_changed;
|
||||
_inner_group.selected_item_count_changed -= _on_selected_item_count_changed;
|
||||
_inner_group.normalized_data.data_changed -= _on_normalized_data_changed;
|
||||
_inner_group.parent = null;
|
||||
// make sure the state is valid (if a bad state goes by undetected here, nefarious/difficult-to-track bugs could arise)
|
||||
if (_last_count != _inner_group.ProtectedItemCount) {
|
||||
Log.Ui.warn("Cached inner count is not equal to the inner group's ItemCount (entire tree structure may be compromised).");
|
||||
}
|
||||
if (_last_data != _inner_group.normalized_data.data) {
|
||||
Log.Ui.warn($"Cached normalized data is not equal to the inner group's normalized data (aggregated pnl data may be compromised).");
|
||||
}
|
||||
_parent.ProtectedItemCount -= _last_count;
|
||||
_parent.normalized_data.data -= _last_data;
|
||||
_parent._selected_item_count -= _last_selected_item_count;
|
||||
}
|
||||
|
||||
void _on_item_count_changed() {
|
||||
// add the delta
|
||||
_parent.ProtectedItemCount += (_inner_group.ProtectedItemCount - _last_count);
|
||||
// reset the local cache
|
||||
_last_count = _inner_group.ProtectedItemCount;
|
||||
}
|
||||
|
||||
void _on_selected_item_count_changed() {
|
||||
_parent._selected_item_count += (_inner_group._selected_item_count - _last_selected_item_count);
|
||||
_last_selected_item_count = _inner_group._selected_item_count;
|
||||
}
|
||||
|
||||
void _on_normalized_data_changed() {
|
||||
var new_data = _inner_group.normalized_data.data;
|
||||
_parent.normalized_data.data += new_data - _last_data;
|
||||
_last_data = new_data;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,525 @@
|
||||
using bts.collections;
|
||||
using mbts;
|
||||
using mbts.collections;
|
||||
using mbts.spark;
|
||||
using mbts.ui;
|
||||
using mbts.ui.controls;
|
||||
using mbts.ui.controls.adorners;
|
||||
using mbts.ui.converters;
|
||||
using mbts.ui.extensions;
|
||||
using mbts.ui.spark;
|
||||
using mbts.ui.utility;
|
||||
using mbts.utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace bts.shell.pnl {
|
||||
enum PNLHeaderRowDisplay {
|
||||
None,
|
||||
Total,
|
||||
SelectedTotal
|
||||
}
|
||||
|
||||
partial class PNLView : UserControl {
|
||||
public enum Columns {
|
||||
Account = 1,
|
||||
Exchange,
|
||||
Symbol,
|
||||
Security,
|
||||
SecurityType,
|
||||
// SOD
|
||||
SODPNL,
|
||||
SODLongPos,
|
||||
SODLongAvgPrice,
|
||||
SODLongAvgPriceDelta,
|
||||
SODShortPos,
|
||||
SODShortAvgPrice,
|
||||
SODShortAvgPriceDelta,
|
||||
SODNetPos,
|
||||
SODNetAvgPriceDelta,
|
||||
// Day
|
||||
DayPNL,
|
||||
DayLongPos,
|
||||
DayLongAvgPrice,
|
||||
DayLongAvgPriceDelta,
|
||||
DayShortAvgPriceDelta,
|
||||
DayShortAvgPrice,
|
||||
DayShortPos,
|
||||
DayCalcType,
|
||||
DayNetPos,
|
||||
DayNetAvgPriceDelta,
|
||||
// Working
|
||||
WorkingLongPos,
|
||||
WorkingLongAvgPrice,
|
||||
WorkingLongAvgPriceDelta,
|
||||
WorkingShortPos,
|
||||
WorkingShortAvgPrice,
|
||||
WorkingShortAvgPriceDelta,
|
||||
WorkingNetPos,
|
||||
WorkingNetAvgPriceDelta,
|
||||
// Total
|
||||
TotalPNL,
|
||||
TotalLongPos,
|
||||
TotalLongAvgPrice,
|
||||
TotalLongAvgPriceDelta,
|
||||
TotalShortPos,
|
||||
TotalShortAvgPrice,
|
||||
TotalShortAvgPriceDelta,
|
||||
TotalNetPos,
|
||||
TotalNetAvgPriceDelta,
|
||||
// Market
|
||||
BidPrice,
|
||||
MidPrice,
|
||||
AskPrice,
|
||||
LastPrice,
|
||||
SettlementPrice,
|
||||
PNLPrice,
|
||||
// Misc
|
||||
HasDayTrades,
|
||||
CPP
|
||||
}
|
||||
|
||||
public static readonly IValueConverter
|
||||
IncrementalGroupLevelConverter = ValueConverter.create<int, int>(level => level + 1),
|
||||
GroupLevelMarginConverter = ValueConverter.create<int, Thickness>(level => new Thickness(24 * level, 0, 0, 0)),
|
||||
ExpandedVisualStateConverter = ValueConverter.create<bool, string>(is_expanded => is_expanded ? "Expanded" : "Collapsed");
|
||||
|
||||
public static readonly IMultiValueConverter
|
||||
TotalHeaderRowDisplayTextConverter = ValueConverter.create<int, int, string>((filtered, total) => $"Total ({(filtered == total ? total.ToString() : $"{filtered}/{total}")})"),
|
||||
CellParamsConverter = ValueConverter.create<object, Columns, PNLCell>((item, col) => new PNLCell(item, col));
|
||||
|
||||
public static readonly RoutedCommand CellClickCommand = new RoutedCommand();
|
||||
|
||||
const double _auxiliary_popup_height = 300;
|
||||
static readonly string _node_path_separator = $" {SpecialCharacters.Dot} ";
|
||||
|
||||
IDisposable _anchors_hook, _animations_hook;
|
||||
int _anchor_version;
|
||||
|
||||
public PNLView() {
|
||||
SetCurrentValue(AnchorIndexProperty, new AnchorIndex());
|
||||
InitializeComponent();
|
||||
SetBinding(AnchorsProperty, new Binding(nameof(PNLVM.anchors)));
|
||||
_grid.ConnectOnLoaded(() => {
|
||||
if (_grid.try_find_descendent_of_type(out DIYGridRowsPresenter presenter)) {
|
||||
presenter.GroupItemSelectionPivot += on_group_item_selection_pivot;
|
||||
return Disposable.Create(() => presenter.GroupItemSelectionPivot -= on_group_item_selection_pivot);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
void on_group_item_selection_pivot(object sender, GroupItemSelectionPivotEventArgs e) =>
|
||||
e.Handled = e.group_item is PNLRowCollectionViewGroup pnl_group && pnl_group.is_expanded;
|
||||
}
|
||||
|
||||
public new PNLVM DataContext {
|
||||
get => base.DataContext as PNLVM;
|
||||
set => base.DataContext = value;
|
||||
}
|
||||
|
||||
public bool EnableAnimations {
|
||||
get => _animations_hook != null;
|
||||
set {
|
||||
if (EnableAnimations != value) {
|
||||
if (_animations_hook == null) {
|
||||
_animations_hook = AdornerHelper.attach_continuously_on_loaded(
|
||||
() => new GridAnimation.DIYGridAnimationAdorner<Columns>(_grid, _grid.descendants_of_type<DIYGridRowsPresenter>().First()),
|
||||
_grid);
|
||||
} else {
|
||||
_animations_hook.Dispose();
|
||||
_animations_hook = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#region AnchorIndex
|
||||
public static readonly DependencyProperty AnchorIndexProperty =
|
||||
WPFHelper.create_dp<AnchorIndex, PNLView>(
|
||||
nameof(AnchorIndex),
|
||||
(o, v) => v?.reset(o?.Anchors));
|
||||
|
||||
public AnchorIndex AnchorIndex {
|
||||
get => GetValue(AnchorIndexProperty) as AnchorIndex;
|
||||
set => SetValue(AnchorIndexProperty, value);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region GroupLevel
|
||||
public static readonly DependencyProperty GroupLevelProperty =
|
||||
DependencyProperty.RegisterAttached(
|
||||
"GroupLevel",
|
||||
typeof(int),
|
||||
typeof(PNLView),
|
||||
new PropertyMetadata(0, (d, e) => {
|
||||
if (d is GroupItem group && e.NewValue is int level)
|
||||
group.SetBinding(DIYGrid.ColumnKeyProperty, new Binding($"{nameof(Anchors)}[{level}]") { RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor, typeof(PNLView), 1) });
|
||||
}));
|
||||
|
||||
public static int GetGroupLevel(DependencyObject d) => (int)d.GetValue(GroupLevelProperty);
|
||||
public static void SetGroupLevel(DependencyObject d, int value) => d.SetValue(GroupLevelProperty, value);
|
||||
|
||||
void _grid_loaded(object sender, RoutedEventArgs e) {
|
||||
// this is a hack to make room for the 'totals row' between the column headers and the regular rows.
|
||||
if (_grid.try_find_descendent_of_type(out DIYGridRowsPresenter rp)) {
|
||||
rp.SetBinding(MarginProperty, new Binding(nameof(PNLVM.header_row_display)) {
|
||||
Converter = ValueConverter.create<PNLHeaderRowDisplay, Thickness>(hrd => {
|
||||
double top = 0;
|
||||
if ((hrd & PNLHeaderRowDisplay.Total) != 0)
|
||||
top += 24;
|
||||
if ((hrd & PNLHeaderRowDisplay.SelectedTotal) != 0)
|
||||
top += 24;
|
||||
return new Thickness(0, top, 0, 0);
|
||||
}),
|
||||
Source = DataContext
|
||||
});
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Anchors
|
||||
public static readonly DependencyProperty AnchorsProperty =
|
||||
WPFHelper.create_dp<IEnumerable<Columns>, PNLView>(
|
||||
nameof(Anchors),
|
||||
(o, v) => {
|
||||
var cols = o?._grid?.Columns;
|
||||
if (cols == null)
|
||||
return;
|
||||
if (!(o._grid.Rows is ICollectionView rows_view))
|
||||
return;
|
||||
if (!(v is INotifyCollectionChanged anchor_notifier && v is IReadOnlyList<Columns> anchor_keys))
|
||||
return;
|
||||
int version = ++o._anchor_version;
|
||||
bool busy = false;
|
||||
var pnl_cols = cols.CastObservable<DIYGrid.ColumnBase, PNLColumn>();
|
||||
Disposable.replace(ref o._anchors_hook, Disposable.Create(
|
||||
anchor_notifier.Subscribe((_, __) => {
|
||||
if (busy)
|
||||
return;
|
||||
// invoke async to avoid noisy consecutive resets due to batch changes
|
||||
busy = true;
|
||||
o.Dispatcher.InvokeAsync(() => {
|
||||
sync_anchor_cols();
|
||||
busy = false;
|
||||
});
|
||||
})));
|
||||
o.AnchorIndex?.reset(anchor_keys);
|
||||
sync_anchor_cols();
|
||||
void sync_anchor_cols() {
|
||||
// since reset is invoked asynchronously, need to check we are still the latest version.
|
||||
if (o._anchor_version != version)
|
||||
return;
|
||||
o.AnchorIndex?.reset(anchor_keys);
|
||||
// move all the anchor columns to the left, ordered by anchor position (grouping hierarchy).
|
||||
for (int i = 0; i < anchor_keys.Count; i++) {
|
||||
if (pnl_cols.TryGetFirst(c => c.key == anchor_keys[i], out var match, out int curr_pos)) {
|
||||
match.is_visible = true;
|
||||
if (curr_pos != i) {
|
||||
cols.Move(curr_pos, i);
|
||||
}
|
||||
match.anchor_index = i + 1;
|
||||
}
|
||||
}
|
||||
for (int i = anchor_keys.Count; i < pnl_cols.Count; i++) {
|
||||
pnl_cols[i].anchor_index = null;
|
||||
}
|
||||
using (rows_view.DeferRefresh()) {
|
||||
rows_view.GroupDescriptions.Clear();
|
||||
var anchor_cols = from key in anchor_keys
|
||||
join col in pnl_cols on key equals col.key
|
||||
select new PNLGroupDescription(col);
|
||||
rows_view.GroupDescriptions.AddRange(anchor_cols);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
public IEnumerable<Columns> Anchors {
|
||||
get => GetValue(AnchorsProperty) as IEnumerable<Columns>;
|
||||
set => SetValue(AnchorsProperty, value);
|
||||
}
|
||||
#endregion
|
||||
|
||||
void _cell_click(PNLCell cell) {
|
||||
Log.Ui.info($"PNLCell Click: {cell}.");
|
||||
try {
|
||||
switch (cell.column) {
|
||||
case Columns.SODNetPos:
|
||||
case Columns.SODLongPos:
|
||||
case Columns.SODShortPos:
|
||||
case Columns.DayNetPos:
|
||||
case Columns.DayLongPos:
|
||||
case Columns.DayShortPos:
|
||||
case Columns.TotalNetPos:
|
||||
case Columns.TotalLongPos:
|
||||
case Columns.TotalShortPos: {
|
||||
var oneof = cell.oneof();
|
||||
if (oneof == null)
|
||||
return;
|
||||
var (vm, context, title, fg) = cell.oneof().Match(_row_fills, _group_fills, _header_fills);
|
||||
_show_fills_grid(vm, context, title, fg);
|
||||
}
|
||||
break;
|
||||
case Columns.WorkingNetPos:
|
||||
case Columns.WorkingLongPos:
|
||||
case Columns.WorkingShortPos: {
|
||||
var (vm, context, title, fg) = cell.oneof().Match(_orderbook_params, _orderbook_params, _orderbook_params);
|
||||
_show_orderbook(vm, context, title, fg);
|
||||
}
|
||||
break;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Log.Ui.warn($"{nameof(PNLView)} error on cell click: {ex}.");
|
||||
}
|
||||
}
|
||||
|
||||
#region Fills
|
||||
(FillsVM vm, string context, string title, Color fg) _row_fills(PNLRowCell cell) {
|
||||
var (title_desc, fg) = _fills_title(cell.column);
|
||||
return (DataContext.mk_fills_vm(cell.row, cell.column), $"{cell.row.account_name} {cell.row.security}", title_desc, fg);
|
||||
}
|
||||
|
||||
(FillsVM vm, string context, string title, Color fg) _group_fills(PNLGroupCell cell) {
|
||||
var (title_desc, fg) = _fills_title(cell.column);
|
||||
return (DataContext.mk_fills_vm(cell.group, cell.column), string.Join(_node_path_separator, cell.group.GroupNamePath), title_desc, fg);
|
||||
}
|
||||
|
||||
(FillsVM vm, string context, string title, Color fg) _header_fills(PNLHeaderRowCell cell) {
|
||||
var (title_desc, fg) = _fills_title(cell.column);
|
||||
return (DataContext.mk_fills_vm(cell.header_row, cell.column), EnumHelper.get_display_name(cell.header_row), title_desc, fg);
|
||||
}
|
||||
|
||||
|
||||
static (string desc, Color fg) _fills_title(Columns key) {
|
||||
const uint net_fg = 0x333333u;
|
||||
switch (key) {
|
||||
case Columns.SODLongPos: return ("Open Buy Fills", BTSColors.MarketData.bid);
|
||||
case Columns.SODShortPos: return ("Open Sell Fills", BTSColors.MarketData.ask);
|
||||
case Columns.SODNetPos: return ("Open Fills", net_fg.rgb_color());
|
||||
case Columns.DayLongPos: return ("Day Buy Fills", BTSColors.MarketData.bid);
|
||||
case Columns.DayShortPos: return ("Day Sell Fills", BTSColors.MarketData.ask);
|
||||
case Columns.DayNetPos: return ("Day Fills", net_fg.rgb_color());
|
||||
case Columns.TotalLongPos: return ("All Buy Fills", BTSColors.MarketData.bid);
|
||||
case Columns.TotalShortPos: return ("All Sell Fills", BTSColors.MarketData.ask);
|
||||
case Columns.TotalNetPos: return ("All Fills", net_fg.rgb_color());
|
||||
default: throw ExceptionBuilder.Argument.unexpected_case(key, nameof(key));
|
||||
}
|
||||
}
|
||||
|
||||
async void _show_fills_grid(FillsVM fills_vm, string context_desc, string title_desc, Color fg) {
|
||||
var fills_view = new FillsView { DataContext = fills_vm, MaxHeight = _auxiliary_popup_height };
|
||||
await Task.Delay(100); // Wait for obvm to load rows collection before showing UI to avoid a flash
|
||||
fills_view.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
|
||||
// banner
|
||||
var group_header = new TextBlock {
|
||||
Margin = new Thickness(4, 0, 0, 0),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
FontWeight = FontWeights.Bold,
|
||||
Text = context_desc
|
||||
};
|
||||
var title = new TextBlock {
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
FontWeight = FontWeights.Bold,
|
||||
Foreground = fg.to_brush(),
|
||||
Text = title_desc
|
||||
};
|
||||
var keep_open_checkbox = new CheckBox {
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Content = "Keep Open",
|
||||
Focusable = false
|
||||
};
|
||||
var close_bn = new Button {
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
Margin = new Thickness(8, 4, 4, 4),
|
||||
Command = ApplicationCommands.Close,
|
||||
Style = TryFindResource("RoundCloseButton") as Style
|
||||
};
|
||||
var banner_panel_rt = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Right };
|
||||
banner_panel_rt.Children.Add(keep_open_checkbox);
|
||||
banner_panel_rt.Children.Add(close_bn);
|
||||
// grid panel
|
||||
var panel = new Grid();
|
||||
panel.Children.Add(group_header);
|
||||
panel.Children.Add(title);
|
||||
panel.Children.Add(banner_panel_rt);
|
||||
panel.Children.Add(fills_view);
|
||||
panel.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
|
||||
panel.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });
|
||||
Grid.SetRow(fills_view, 1);
|
||||
// popup adorner
|
||||
var popup_adorner = new PopupAdorner(
|
||||
_grid, _grid,
|
||||
new Border {
|
||||
Child = panel,
|
||||
Background = BTSColors.common_background_brush,
|
||||
BorderBrush = Brushes.Black,
|
||||
BorderThickness = new Thickness(1),
|
||||
HorizontalAlignment = HorizontalAlignment.Left,
|
||||
VerticalAlignment = VerticalAlignment.Top
|
||||
},
|
||||
HorizontalPosition.Middle,
|
||||
VerticalPosition.Bottom,
|
||||
0,
|
||||
0) {
|
||||
OpenRelativeToMouse = true,
|
||||
EnableDragMove = true,
|
||||
ClipToBounds = true
|
||||
};
|
||||
close_bn.CommandTarget = popup_adorner.PopupElement;
|
||||
popup_adorner.SetBinding(PopupAdorner.StaysOpenProperty, new Binding(nameof(CheckBox.IsChecked)) { Source = keep_open_checkbox });
|
||||
popup_adorner.PopupElement.InputBindings.Add(new KeyBinding(ApplicationCommands.Close, Key.Escape, ModifierKeys.None));
|
||||
popup_adorner.PopupElement.CommandBindings.Add(new CommandBinding(ApplicationCommands.Close, (s, e) => {
|
||||
Log.assert(popup_adorner != null);
|
||||
popup_adorner.detach();
|
||||
popup_adorner = null;
|
||||
}));
|
||||
//
|
||||
popup_adorner.attach();
|
||||
popup_adorner.IsOpen = true;
|
||||
FocusManager.SetIsFocusScope(popup_adorner.PopupElement, true);
|
||||
fills_view.Focus();
|
||||
popup_adorner.AdornedElement.Focus();
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region OrderBook
|
||||
(OrderBookVM vm, string context, string title, Color fg) _orderbook_params(PNLRowCell cell) {
|
||||
var (title_desc, fg) = _orderbook_title(cell.column);
|
||||
return (DataContext.mk_orderbook_vm(cell.row, cell.column), $"{cell.row.account_name} {cell.row.security}", title_desc, fg);
|
||||
}
|
||||
|
||||
(OrderBookVM vm, string context, string title, Color fg) _orderbook_params(PNLGroupCell cell) {
|
||||
var (title_desc, fg) = _orderbook_title(cell.column);
|
||||
return (DataContext.mk_orderbook_vm(cell.group, cell.column), string.Join(_node_path_separator, cell.group.GroupNamePath), title_desc, fg);
|
||||
}
|
||||
|
||||
(OrderBookVM vm, string context, string title, Color fg) _orderbook_params(PNLHeaderRowCell cell) {
|
||||
var (title_desc, fg) = _orderbook_title(cell.column);
|
||||
return (DataContext.mk_orderbook_vm(cell.header_row, cell.column), EnumHelper.get_display_name(cell.header_row), title_desc, fg);
|
||||
}
|
||||
|
||||
static (string desc, Color fg) _orderbook_title(Columns key) {
|
||||
switch (key) {
|
||||
case Columns.WorkingLongPos: return ("Working Buy Orders", BTSColors.MarketData.bid);
|
||||
case Columns.WorkingShortPos: return ("Working Sell Orders", BTSColors.MarketData.ask);
|
||||
case Columns.WorkingNetPos: return ("Working Orders", 0x333333u.rgb_color());
|
||||
default: throw ExceptionBuilder.Argument.unexpected_case(key, nameof(key));
|
||||
}
|
||||
}
|
||||
|
||||
async void _show_orderbook(OrderBookVM obvm, string context_desc, string title_desc, Color fg) {
|
||||
var mini_ob = new MiniOrderBookView(obvm) { MaxHeight = _auxiliary_popup_height };
|
||||
await Task.Delay(100); // Wait for obvm to load rows collection before showing UI to avoid a flash
|
||||
mini_ob.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
|
||||
// banner
|
||||
var group_header = new TextBlock {
|
||||
Margin = new Thickness(4, 0, 0, 0),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
FontWeight = FontWeights.Bold,
|
||||
Text = context_desc
|
||||
};
|
||||
var title = new TextBlock {
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
FontWeight = FontWeights.Bold,
|
||||
Foreground = fg.to_brush(),
|
||||
Text = title_desc
|
||||
};
|
||||
var keep_open_checkbox = new CheckBox {
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Content = "Keep Open",
|
||||
Focusable = false
|
||||
};
|
||||
var close_bn = new Button {
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
Margin = new Thickness(8, 4, 4, 4),
|
||||
Command = ApplicationCommands.Close,
|
||||
//CommandTarget = mini_ob,
|
||||
Style = TryFindResource("RoundCloseButton") as Style
|
||||
};
|
||||
var banner_panel_rt = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Right };
|
||||
banner_panel_rt.Children.Add(keep_open_checkbox);
|
||||
banner_panel_rt.Children.Add(close_bn);
|
||||
// grid panel
|
||||
var panel = new Grid();
|
||||
panel.Children.Add(group_header);
|
||||
panel.Children.Add(title);
|
||||
panel.Children.Add(banner_panel_rt);
|
||||
panel.Children.Add(mini_ob);
|
||||
panel.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
|
||||
panel.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });
|
||||
Grid.SetRow(mini_ob, 1);
|
||||
// popup adorner
|
||||
var popup_adorner = new PopupAdorner(
|
||||
_grid, _grid,
|
||||
new Border {
|
||||
Child = panel,
|
||||
Background = BTSColors.common_background_brush,
|
||||
BorderBrush = Brushes.Black,
|
||||
BorderThickness = new Thickness(1),
|
||||
HorizontalAlignment = HorizontalAlignment.Left,
|
||||
VerticalAlignment = VerticalAlignment.Top
|
||||
},
|
||||
HorizontalPosition.Middle,
|
||||
VerticalPosition.Bottom,
|
||||
0,
|
||||
0) {
|
||||
OpenRelativeToMouse = true,
|
||||
EnableDragMove = true,
|
||||
ClipToBounds = true
|
||||
};
|
||||
close_bn.CommandTarget = popup_adorner.PopupElement;
|
||||
popup_adorner.SetBinding(PopupAdorner.StaysOpenProperty, new Binding(nameof(CheckBox.IsChecked)) { Source = keep_open_checkbox });
|
||||
popup_adorner.PopupElement.InputBindings.Add(new KeyBinding(ApplicationCommands.Close, Key.Escape, ModifierKeys.None));
|
||||
popup_adorner.PopupElement.CommandBindings.Add(new CommandBinding(ApplicationCommands.Close, (s, e) => {
|
||||
Log.assert(popup_adorner != null);
|
||||
popup_adorner.detach();
|
||||
popup_adorner = null;
|
||||
}));
|
||||
//
|
||||
popup_adorner.attach();
|
||||
popup_adorner.IsOpen = true;
|
||||
FocusManager.SetIsFocusScope(popup_adorner.PopupElement, true);
|
||||
mini_ob.Focus();
|
||||
popup_adorner.AdornedElement.Focus();
|
||||
}
|
||||
#endregion
|
||||
|
||||
void _cell_click_executed(object sender, ExecutedRoutedEventArgs e) {
|
||||
if (e.Parameter is PNLCell cell) {
|
||||
_cell_click(cell);
|
||||
} else Log.Ui.warn($"{nameof(PNLView)} '{nameof(_cell_click_executed)}' parameter should be a {typeof(PNLCell)} (actually '{e.Parameter}').");
|
||||
}
|
||||
|
||||
void _on_group_item_mouse_down(object sender, MouseButtonEventArgs e) {
|
||||
if (!e.Handled && sender is FrameworkElement el && el.DataContext is PNLRowCollectionViewGroup group) {
|
||||
group.is_all_selected = !group.is_all_selected.GetValueOrDefault();
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
class PNLGroupDescription : PropertyGroupDescription, IEquatable<PNLGroupDescription> {
|
||||
public PNLGroupDescription(PNLColumn column) : base(column.data_member_path.Path) =>
|
||||
key = column.key;
|
||||
|
||||
public Columns key { get; }
|
||||
|
||||
public bool Equals(PNLGroupDescription other) =>
|
||||
other != null && key == other.key;
|
||||
|
||||
public override int GetHashCode() =>
|
||||
key.GetHashCode();
|
||||
|
||||
public override bool Equals(object obj) =>
|
||||
Equals(obj as PNLGroupDescription);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,8 @@ namespace bts.shell {
|
||||
LadderWindowFactory _lwf;
|
||||
OrderBookWindowFactory _obwf;
|
||||
FillsWindowFactory _fwf;
|
||||
PAndLWindowFactory _plwf;
|
||||
PAndLWindowFactory _old_plwf;
|
||||
PNLWindowFactory _plwf;
|
||||
AlgoStudioWindowFactory _aswf;
|
||||
ManualFillsWindowFactory _mfwf;
|
||||
|
||||
@@ -36,7 +37,11 @@ namespace bts.shell {
|
||||
_lwf = new LadderWindowFactory(this, bus, wm, settings_hub, orders_model, mbo);
|
||||
_obwf = new OrderBookWindowFactory(this, bus, notification_service, notification_service_data_cache, orders_model, settings_hub);
|
||||
_fwf = new FillsWindowFactory(this, bus, settings_hub, orders_model);
|
||||
_plwf = new PAndLWindowFactory(this, bus, settings_hub, orders_model);
|
||||
var pnl_conf = mbts.config.Configuration.get<bts.config.PNLOptions>();
|
||||
if (pnl_conf.show_new_pnl)
|
||||
_plwf = new PNLWindowFactory(this, bus, settings_hub, orders_model);
|
||||
if (pnl_conf.show_old_pnl)
|
||||
_old_plwf = new PAndLWindowFactory(this, bus, settings_hub, orders_model);
|
||||
_aswf = new AlgoStudioWindowFactory(this, bus, orders_model, error_reporter);
|
||||
_mfwf = new ManualFillsWindowFactory(this, bus, orders_model);
|
||||
}
|
||||
@@ -50,6 +55,7 @@ namespace bts.shell {
|
||||
public IEnumerable<SparkCommand> actions => new [] {
|
||||
SparkCommands.ShowOrders.complete(() => bts.shell.orderbook.OrderBookStartupData.Default),
|
||||
SparkCommands.ShowFills(null).complete(() => new FillsSettings()),
|
||||
SparkCommands.ShowPNL.complete(() => null),
|
||||
SparkCommands.ShowPAndL.complete(() => new PAndLWindowFactory.PAndLStartupData()),
|
||||
SparkCommands.PickLadder,
|
||||
SparkCommands.PickOrderTicket,
|
||||
@@ -61,7 +67,10 @@ namespace bts.shell {
|
||||
yield return _lwf;
|
||||
yield return _obwf;
|
||||
yield return _fwf;
|
||||
yield return _plwf;
|
||||
if (_plwf != null)
|
||||
yield return _plwf;
|
||||
if (_old_plwf != null)
|
||||
yield return _old_plwf;
|
||||
yield return _aswf;
|
||||
yield return _mfwf;
|
||||
}
|
||||
|
||||
@@ -33,6 +33,11 @@ namespace mbts.spark {
|
||||
}
|
||||
}
|
||||
|
||||
public uint account_id =>
|
||||
fill.FillKind != FillKind.Sod && order.HasValue ?
|
||||
order.Value.account_id() :
|
||||
fill.InternalAccountId;
|
||||
|
||||
public bool Equals(FillTuple other) => other != null
|
||||
&& other.order.HasValue == order.HasValue
|
||||
&& other.security.HasValue == security.HasValue
|
||||
|
||||
@@ -304,6 +304,29 @@ namespace mbts.spark.utilities {
|
||||
|
||||
public static OneofDescriptor oneof_descriptor => _oneof_desc;
|
||||
|
||||
const double _price_error_margin = 0.0000001;
|
||||
const decimal _price_error_margin_decimal = 0.0000001m;
|
||||
|
||||
public static readonly IComparer<double> PriceComparer = Comparer<double>.Create((a, b) => {
|
||||
if (double.IsNaN(a)) {
|
||||
return double.IsNaN(b) ? 0 : -1;
|
||||
} else if (double.IsNaN(b)) {
|
||||
return 1;
|
||||
} else {
|
||||
double diff = a - b;
|
||||
if (diff <= _price_error_margin && diff >= -_price_error_margin)
|
||||
return 0;
|
||||
return diff > 0 ? 1 : -1;
|
||||
}
|
||||
});
|
||||
|
||||
public static readonly IComparer<decimal> PriceComparerDecimal = Comparer<decimal>.Create((a, b) => {
|
||||
decimal diff = a - b;
|
||||
if (diff <= _price_error_margin_decimal && diff >= -_price_error_margin_decimal)
|
||||
return 0;
|
||||
return diff > 0 ? 1 : -1;
|
||||
});
|
||||
|
||||
const string _primary_child_field = "child";
|
||||
// algo-type-case maps
|
||||
// Note: obv this will break if we change the name of the 'CustomOneofCase' field
|
||||
|
||||
@@ -268,6 +268,9 @@ namespace mbts.spark {
|
||||
public static Color badge_color(AlgoDefinitionMetadata? algo_meta, OrderMetadata order_meta) =>
|
||||
algo_meta?.badge_color ?? order_meta.Color.rgb_color();
|
||||
|
||||
public static string order_collection_summary(double best_price, int best_price_volume, int total_volume) =>
|
||||
order_collection_summary(best_price, best_price_volume, total_volume, SecurityDataHelper.Defaults.ContractSpecificationImmutable);
|
||||
|
||||
public static string order_collection_summary(double best_price, int best_price_volume, int total_volume, ContractSpecificationImmutable spec) {
|
||||
if (total_volume == 0)
|
||||
return string.Empty;
|
||||
@@ -276,6 +279,14 @@ namespace mbts.spark {
|
||||
$"{best_price_volume} ({total_volume}) @ {spec.format(best_price)}";
|
||||
}
|
||||
|
||||
public static string order_collection_summary(double best_price, double avg_price, int best_price_volume, int total_volume, ContractSpecificationImmutable spec) {
|
||||
if (total_volume == 0)
|
||||
return string.Empty;
|
||||
return total_volume == best_price_volume ?
|
||||
$"{best_price_volume} @ {spec.format(best_price)}" :
|
||||
$"{best_price_volume} @ {spec.format(best_price)} ({total_volume} @ {spec.format(avg_price)})";
|
||||
}
|
||||
|
||||
public static string past_tense(this Side side) {
|
||||
switch (side) {
|
||||
case Side.Buy: return "Bought";
|
||||
|
||||
@@ -5,6 +5,7 @@ using bts.api.orderman;
|
||||
using bts.api.position;
|
||||
using bts.collections;
|
||||
using bts.md;
|
||||
using bts.shell.pnl;
|
||||
using bts.spark;
|
||||
using bts.utils;
|
||||
using mbts.config;
|
||||
@@ -68,13 +69,21 @@ namespace mbts.spark {
|
||||
.Cache();
|
||||
|
||||
on_positions = _position_service.subscribei_position_data().Naggle(interval, scheduler).PermissionFilter(admin_model.on_admin_data).join(sstore, pos => pos.SecurityId).Table().Cache();
|
||||
working_orders =
|
||||
on_pnl = _position_service.subscribei_pnl_data()
|
||||
.Naggle(interval, scheduler)
|
||||
.join(sstore, p => p.Pos.SecurityId)
|
||||
.TableChecked(EqualityComparer<(SecurityImmutable? sec, PNLUnitImmutable data)>.Default)
|
||||
.Where(t => t.has_changes())
|
||||
.Cache();
|
||||
on_account_pnl = _position_service.subscribei_pnl_account_data().Naggle(interval, scheduler).Table().Cache();
|
||||
var working_orders = on_orders.Filter(tup => tup.order.is_active());
|
||||
this.working_orders =
|
||||
on_orders.Scan(
|
||||
ImmutableDictionary<uint,OrderSecurity>.Empty,
|
||||
ImmutableDictionary<uint, OrderSecurity>.Empty,
|
||||
(workers, table) => _update_working_orders(workers, table))
|
||||
.Replay(1).RefCount();
|
||||
|
||||
on_order_securities =
|
||||
on_order_securities =
|
||||
on_orders
|
||||
.as_iincremental()
|
||||
.filter(t => t.sec.HasValue)
|
||||
@@ -132,6 +141,38 @@ namespace mbts.spark {
|
||||
OrderSecurityRootPair.from_order_security,
|
||||
OrderSecurityHelper.root_pair_key_fn
|
||||
).Cache();
|
||||
// working account orders
|
||||
var working_account_order_groups = working_orders.GroupTable(t => t.order.Key, t => new PNLKey(t.order.account_id(), t.order.security_key()));
|
||||
// security account pnl
|
||||
security_account_pnl = on_pnl
|
||||
#if DEBUG
|
||||
.AssertIncremental(p => new PNLKey(p.Item2.Pos.AccountId, p.Item2.Pos.SecurityId))
|
||||
#endif
|
||||
.SelectKeyValue(PNLRowDataFactory.create, PNLRowData.key_fn)
|
||||
.FullJoin(
|
||||
working_account_order_groups,
|
||||
PNLRowData.key_fn,
|
||||
acc_orders => acc_orders.group_key,
|
||||
(pnl_row_data, acc_orders, pnl_key) => pnl_row_data.with(working: WorkingOrderData.Create(acc_orders.table.state.Values.Select(t => t.order))),
|
||||
(pnl_row_data, pnl_id) => pnl_row_data,
|
||||
(acc_orders, pnl_id) => {
|
||||
string account;
|
||||
SecurityImmutable? security;
|
||||
if (acc_orders.table.state.Values.TryGetFirst(out var tup)) {
|
||||
security = tup.sec;
|
||||
account = tup.order.account();
|
||||
} else {
|
||||
security = null;
|
||||
account = null;
|
||||
}
|
||||
return new PNLRowData(pnl_id,
|
||||
security: security,
|
||||
account_name: account,
|
||||
working: WorkingOrderData.Create(acc_orders.table.state.Values.Select(t => t.order)));
|
||||
},
|
||||
PNLRowData.key_fn)
|
||||
.Where(t => t.has_changes())
|
||||
.Cache();
|
||||
// fills
|
||||
var on_fills =
|
||||
_service.subscribei_fills()
|
||||
@@ -154,7 +195,7 @@ namespace mbts.spark {
|
||||
.join(sstore, mf => mf.SecurityKey)
|
||||
.Table().Cache();
|
||||
var manual_fill_tuples_account_join = on_manual_fills_securities.LeftJoin(
|
||||
admin_model.on_accounts.Select(d => MappedDictionary.create(d, kv => new AccountTuple(kv.Key, kv.Value))).Table(a => a.id),
|
||||
admin_model.on_accounts.Select(d => MappedDictionary.create(d, (k, v) => new AccountTuple(k, v))).Table(a => a.id),
|
||||
mfs => mfs.Item2.RecordId,
|
||||
AccountTuple.key_fn,
|
||||
mfs => mfs.Item2.AccountId,
|
||||
@@ -162,7 +203,7 @@ namespace mbts.spark {
|
||||
(mfs, mf_id, a_id) => new ManualFillTuple(mfs.Item2, mfs.Item1, null, null),
|
||||
ManualFillTuple.key_fn);
|
||||
on_manual_fill_tuples = manual_fill_tuples_account_join.LeftJoin(
|
||||
admin_model.on_users.Select(d => MappedDictionary.create(d, kv => new UserTuple(kv.Key, kv.Value))).Table(a => a.id),
|
||||
admin_model.on_users.Select(d => MappedDictionary.create(d, (k, v) => new UserTuple(k, v))).Table(a => a.id),
|
||||
ManualFillTuple.key_fn,
|
||||
UserTuple.key_fn,
|
||||
mft => mft.manual_fill.UserId,
|
||||
@@ -212,6 +253,9 @@ namespace mbts.spark {
|
||||
public IObservable<IIncrementalUpdate<SecurityImmutable, uint>> on_order_securities { get; }
|
||||
public IObservable<TableUpdate<FillTuple, string>> on_fill_tuples { get; }
|
||||
public IObservable<IReadOnlyDictionary<uint,OrderSecurity>> working_orders { get; }
|
||||
public IObservable<TableUpdate<(SecurityImmutable?, PNLUnitImmutable), int>> on_pnl { get; }
|
||||
public IObservable<TableUpdate<PNLRowData, PNLKey>> security_account_pnl { get; }
|
||||
public IObservable<TableUpdate<AccountPNLImmutable, uint>> on_account_pnl { get; }
|
||||
public IObservable<TableUpdate<(SecurityImmutable?, PositionUnitImmutable), int>> on_positions { get; }
|
||||
public IObservable<TableUpdate<(AlgoData algo, AlgoDefinitionMetadata? meta), AlgoKey>> studio_algos_with_meta { get; }
|
||||
public IObservable<TableUpdate<(SecurityImmutable?, ManualFillImmutable), int>> on_manual_fills_securities { get; }
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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,12 +8,9 @@ using mbts.ui;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using static mbts.spark.SparkBusData;
|
||||
|
||||
namespace mbts.spark
|
||||
{
|
||||
static class SparkTags
|
||||
{
|
||||
namespace mbts.spark {
|
||||
static class SparkTags {
|
||||
#region Core Data
|
||||
public static ITag<IReadOnlyDictionary<uint, SecurityMD>> SecuritiesTag { get; } = new SparkTag<IReadOnlyDictionary<uint, SecurityMD>>("Securities");
|
||||
public static ITag<IReadOnlyDictionary<uint, ExecutionStrategyImmutable>> SyntheticstrategiesTag { get; } = new SparkTag<IReadOnlyDictionary<uint, ExecutionStrategyImmutable>>("SyntheticSecuritiesTag");
|
||||
@@ -64,6 +61,7 @@ namespace mbts.spark
|
||||
public static ITag<SecurityCriteria> ShowMarketViewTag { get; } = new SparkTag<SecurityCriteria>("Show MarketView");
|
||||
public static ITag<bts.shell.orderbook.OrderBookStartupData> ShowOrderBookTag { get; } = new SparkTag<bts.shell.orderbook.OrderBookStartupData>(nameof(ShowOrderBookTag));
|
||||
public static ITag<PAndLWindowFactory.PAndLStartupData> ShowPAndLTag { get; } = new SparkTag<PAndLWindowFactory.PAndLStartupData>(nameof(ShowPAndLTag));
|
||||
public static ITag<bts.shell.pnl.PNLSettings> ShowPNLTag { get; } = new SparkTag<bts.shell.pnl.PNLSettings>(nameof(ShowPNLTag));
|
||||
public static ITag<(SecurityImmutable security, ExecutionStrategyImmutable strategy)> ShowSyntheticCloneTag { get; } = new SparkTag<(SecurityImmutable security, ExecutionStrategyImmutable strategy)>(nameof(ShowSyntheticCloneTag));
|
||||
public static ITag<SecurityCriteria> ShowSyntheticCreateTag { get; } = new SparkTag<SecurityCriteria>(nameof(ShowSyntheticCreateTag));
|
||||
public static ITag<SecurityImmutable> ShowSyntheticStrategyTag { get; } = new SparkTag<SecurityImmutable>(nameof(ShowSyntheticStrategyTag));
|
||||
@@ -89,4 +87,4 @@ namespace mbts.spark
|
||||
public override string ToString() => Name;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,12 @@ namespace mbts.ui.utility {
|
||||
public static IReadOnlyObservableCollection<T> create_sorted_wrapper<T>(ObservableCollection<T> source, IComparer<T> comparer) =>
|
||||
new SortedWrapper<T>(source, source, comparer);
|
||||
|
||||
public static IReadOnlyObservableCollection<TResult> CastObservable<T, TResult>(this ObservableCollection<T> source) =>
|
||||
new SelectorWrapper<T, TResult>(source, source, ConvertUtils.CastDelegate<T, TResult>.value);
|
||||
|
||||
public static IReadOnlyObservableCollection<TResult> SelectObservable<T, TResult>(this ObservableCollection<T> source, Func<T, TResult> selector) =>
|
||||
new SelectorWrapper<T, TResult>(source, source, selector);
|
||||
|
||||
public static IDisposable connect_sorted<T>(ObservableCollection<T> source, ObservableCollection<T> target, IComparer<T> comparer) {
|
||||
target.sync_with(source.Ordered(comparer));
|
||||
return source.Subscribe(on_source_collection_changed);
|
||||
@@ -87,7 +93,9 @@ namespace mbts.ui.utility {
|
||||
connect<ObservableCollection<T>, T, TResult>(source, target, create, null);
|
||||
|
||||
public static IDisposable connect<TCollection, T, TResult>(TCollection source, IList<TResult> target, Func<T, TResult> create, Action<TResult> on_removed = null)
|
||||
where TCollection : IReadOnlyList<T>, INotifyCollectionChanged {
|
||||
where TCollection : IReadOnlyList<T>, INotifyCollectionChanged => connect(source, source, target, create, on_removed);
|
||||
|
||||
public static IDisposable connect<T, TResult>(IReadOnlyList<T> source_list, INotifyCollectionChanged source_notifier, IList<TResult> target, Func<T, TResult> create, Action<TResult> on_removed = null) {
|
||||
Action reset_cb;
|
||||
Action<IList, int> removed_cb,
|
||||
added_cb = (new_items, i) => {
|
||||
@@ -100,7 +108,7 @@ namespace mbts.ui.utility {
|
||||
}
|
||||
};
|
||||
if (on_removed == null) {
|
||||
reset_cb = () => target.SetItems(source.Select(create));
|
||||
reset_cb = () => target.SetItems(source_list.Select(create));
|
||||
removed_cb = (old_items, i) => {
|
||||
if (i < 0) {
|
||||
throw new InvalidOperationException("Source collection invoked a CollectionChanged 'Removed' event without a valid 'OldStartingIndex.'");
|
||||
@@ -113,7 +121,7 @@ namespace mbts.ui.utility {
|
||||
reset_cb = () => {
|
||||
foreach (var v in target)
|
||||
on_removed(v);
|
||||
target.SetItems(source.Select(create));
|
||||
target.SetItems(source_list.Select(create));
|
||||
};
|
||||
removed_cb = (old_items, i) => {
|
||||
if (i < 0) {
|
||||
@@ -134,8 +142,8 @@ namespace mbts.ui.utility {
|
||||
added_cb(new_items, new_i);
|
||||
},
|
||||
on_reset: reset_cb);
|
||||
collection_changed_cb(source, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, (IList)source));
|
||||
return source.Subscribe(collection_changed_cb);
|
||||
reset_cb();
|
||||
return source_notifier.Subscribe(collection_changed_cb);
|
||||
}
|
||||
|
||||
public static IDisposable connect<T>(IObservable<ObservableCollection<T>> source, ObservableCollection<T> target) {
|
||||
@@ -209,11 +217,21 @@ namespace mbts.ui.utility {
|
||||
return notifier.Subscribe((s, e) => CollectionHelper.process_new_items(collection, action, e));
|
||||
}
|
||||
|
||||
public static IDisposable connect_item_hooks<T>(IReadOnlyObservableCollection<T> source, Func<T, IDisposable> get_item_hook) {
|
||||
var item_hooks = source.Select(get_item_hook).ToList();
|
||||
public static IDisposable connect_item_hooks<T>(IReadOnlyObservableCollection<T> source, Func<T, IDisposable> get_item_hook) =>
|
||||
connect_item_hooks(source, source, get_item_hook);
|
||||
|
||||
public static IDisposable connect_item_hooks<T>(IReadOnlyList<T> source_list, INotifyCollectionChanged source_notifier, Func<T, IDisposable> get_item_hook) {
|
||||
var item_hooks = source_list.Select(get_item_hook).ToList();
|
||||
return new CompositeDisposable(
|
||||
Disposable.wrap_collection(item_hooks),
|
||||
source.Subscribe((s, e) => CollectionHelper.reflect_change(item_hooks, source, get_item_hook, d => d.Dispose(), e)));
|
||||
source_notifier.Subscribe((s, e) => CollectionHelper.reflect_change(item_hooks, source_list, get_item_hook, d => d.Dispose(), e)));
|
||||
}
|
||||
|
||||
public static IDisposable connect_item_hooks<T>(IReadOnlyList<T> source_list, INotifyCollectionChanged source_notifier, Func<T, int, IDisposable> get_item_hook) {
|
||||
var item_hooks = source_list.Select(get_item_hook).ToList();
|
||||
return new CompositeDisposable(
|
||||
Disposable.wrap_collection(item_hooks),
|
||||
source_notifier.Subscribe((s, e) => CollectionHelper.reflect_change(item_hooks, source_list, get_item_hook, d => d.Dispose(), e)));
|
||||
}
|
||||
|
||||
public static IDisposable connect_item_hooks<T>(ObservableCollection<T> source, Func<T, IDisposable> get_item_hook, bool clear_source_on_dispose = false) {
|
||||
@@ -352,6 +370,46 @@ namespace mbts.ui.utility {
|
||||
public IEnumerator<T> GetEnumerator() => _source.GetEnumerator();
|
||||
IEnumerator IEnumerable.GetEnumerator() => _source.GetEnumerator();
|
||||
}
|
||||
|
||||
class SelectorWrapper<T, TResult> : IReadOnlyObservableCollection<TResult> {
|
||||
readonly IReadOnlyList<T> _source_list;
|
||||
readonly Func<T, TResult> _selector;
|
||||
|
||||
public SelectorWrapper(IReadOnlyList<T> source_list, INotifyCollectionChanged source_notifier, Func<T, TResult> selector) {
|
||||
_source_list = source_list;
|
||||
_selector = selector;
|
||||
source_notifier.CollectionChanged += on_collection_changed;
|
||||
void on_collection_changed(object sender, NotifyCollectionChangedEventArgs e) {
|
||||
var cc = CollectionChanged;
|
||||
if (cc == null)
|
||||
return;
|
||||
NotifyCollectionChangedEventArgs re;
|
||||
switch (e.Action) {
|
||||
case NotifyCollectionChangedAction.Add: re = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, e.NewItems.WrapSelectorHidden(from_obj), e.NewStartingIndex); break;
|
||||
case NotifyCollectionChangedAction.Remove: re = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, e.OldItems.WrapSelectorHidden(from_obj), e.OldStartingIndex); break;
|
||||
case NotifyCollectionChangedAction.Move: re = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Move, e.NewItems.WrapSelectorHidden(from_obj), e.NewStartingIndex, e.OldStartingIndex); break;
|
||||
case NotifyCollectionChangedAction.Replace: re = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, e.NewItems.WrapSelectorHidden(from_obj), e.OldItems.WrapSelectorHidden(from_obj), e.NewStartingIndex); break;
|
||||
case NotifyCollectionChangedAction.Reset: re = e; break;
|
||||
default: throw ExceptionBuilder.Argument.unexpected_case(e.Action, nameof(e));
|
||||
}
|
||||
cc(this, re);
|
||||
}
|
||||
TResult from_obj(object obj) =>
|
||||
selector((T)obj);
|
||||
}
|
||||
|
||||
public TResult this[int index] => _selector(_source_list[index]);
|
||||
|
||||
public int Count => _source_list.Count;
|
||||
|
||||
public event NotifyCollectionChangedEventHandler CollectionChanged;
|
||||
|
||||
public IEnumerator<TResult> GetEnumerator() {
|
||||
foreach (var v in _source_list)
|
||||
yield return _selector(v);
|
||||
}
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
}
|
||||
}
|
||||
|
||||
public class ObservableAutoWrapper<View, Model> : ObservableCollection<View>
|
||||
|
||||
@@ -1,15 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace mbts.expression
|
||||
{
|
||||
namespace mbts.expression {
|
||||
// Implements the language used in bts_core/Config
|
||||
public class CoreExpressionEvaluator<T> : IExpressionEvaluator<T>
|
||||
{
|
||||
public class CoreExpressionEvaluator<T> : IExpressionEvaluator<T> {
|
||||
private Func<T, bool> _apply;
|
||||
|
||||
public CoreExpressionEvaluator(string expression) {
|
||||
@@ -29,10 +22,35 @@ namespace mbts.expression
|
||||
}
|
||||
}
|
||||
|
||||
CoreExpressionEvaluator(Func<T, bool> apply, string expression, string parsed_expression) {
|
||||
_apply = apply;
|
||||
Expression = expression;
|
||||
ParsedExpression = parsed_expression;
|
||||
}
|
||||
|
||||
public static bool try_create(string expression, out CoreExpressionEvaluator<T> result) {
|
||||
try {
|
||||
var parameter = System.Linq.Expressions.Expression.Parameter(typeof(T));
|
||||
var parser = new CoreExpressionParser();
|
||||
if (parser.try_parse(expression, out var ast)) {
|
||||
var body = ast.create_expression(parameter);
|
||||
var parsed = body.ToString();
|
||||
var lambda = System.Linq.Expressions.Expression.Lambda<Func<T, bool>>(body, parameter);
|
||||
result = new CoreExpressionEvaluator<T>(lambda.Compile(), expression, parsed);
|
||||
return true;
|
||||
}
|
||||
} catch { }
|
||||
result = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool validate(string expression) =>
|
||||
try_create(expression, out var _);
|
||||
|
||||
public string Expression { get; private set; }
|
||||
public string ParsedExpression { get; private set; }
|
||||
public bool apply(T arg) {
|
||||
return _apply(arg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,17 +4,28 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace mbts.expression
|
||||
{
|
||||
namespace mbts.expression {
|
||||
// This is a re-implementation of the bts_core Expression parser in c#.
|
||||
// It compiles down to LINQ expression trees which means the code is JITed at runtime for very fast execution
|
||||
class CoreExpressionParser
|
||||
{
|
||||
class CoreExpressionParser {
|
||||
// A cached stringbuilder
|
||||
StringBuilder result = new StringBuilder();
|
||||
|
||||
internal bool try_parse(string str, out Token result) {
|
||||
if (string.IsNullOrWhiteSpace(str)) {
|
||||
result = null;
|
||||
} else {
|
||||
var l = lex(str);
|
||||
try {
|
||||
result = tree(l, false);
|
||||
} catch {
|
||||
result = null;
|
||||
}
|
||||
}
|
||||
return result != null;
|
||||
}
|
||||
|
||||
internal Token parse(string str) {
|
||||
if (string.IsNullOrWhiteSpace(str)) return null;
|
||||
var l = lex(str);
|
||||
@@ -158,22 +169,22 @@ namespace mbts.expression
|
||||
}
|
||||
return new Number(result.ToString());
|
||||
}
|
||||
#endregion
|
||||
#endregion
|
||||
|
||||
#region Recurively convert the Token list to an expression tree
|
||||
|
||||
private Token tree(IEnumerable<Token> tokens) {
|
||||
private Token tree(IEnumerable<Token> tokens, bool assert_valid = true) {
|
||||
var t = tokens.ToList();
|
||||
|
||||
// Handle groups by recursion
|
||||
for (int x = 0; x < t.Count; ++x) {
|
||||
if (t[x] is GroupToken) {
|
||||
t[x] = tree(((GroupToken)t[x]).Items);
|
||||
t[x] = tree(((GroupToken)t[x]).Items, assert_valid);
|
||||
}
|
||||
}
|
||||
|
||||
// This has to be a separate pass for "True == !False"
|
||||
for (int x = t.Count-1; x >= 0; --x) {
|
||||
for (int x = t.Count - 1; x >= 0; --x) {
|
||||
if (t[x] is NotOperator) {
|
||||
NotOperator no = (NotOperator)t[x];
|
||||
no.Child = t[x + 1];
|
||||
@@ -187,7 +198,11 @@ namespace mbts.expression
|
||||
if (t[x] is CompareOperator) {
|
||||
CompareOperator b = (CompareOperator)t[x];
|
||||
if (b.Left != null) {
|
||||
Log.assert(b.Right != null);
|
||||
if (b.Right == null) {
|
||||
if (assert_valid) {
|
||||
Log.assert(false);
|
||||
} else throw new InvalidOperationException("CompareOperator right side operand is null.");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
b.Left = t[x - 1];
|
||||
@@ -213,8 +228,11 @@ namespace mbts.expression
|
||||
x--;
|
||||
}
|
||||
}
|
||||
|
||||
Log.assert(t.Count == 1);
|
||||
if (t.Count != 1) {
|
||||
if (assert_valid) {
|
||||
Log.assert(false);
|
||||
} else throw new InvalidOperationException("Expression tree was not reduced to a single token.");
|
||||
}
|
||||
return t.First();
|
||||
}
|
||||
#endregion
|
||||
@@ -289,7 +307,8 @@ namespace mbts.expression
|
||||
typeof(UInt32),
|
||||
typeof(Int64),
|
||||
typeof(UInt64),
|
||||
typeof(float)
|
||||
typeof(float),
|
||||
typeof(decimal)
|
||||
};
|
||||
|
||||
if (numeric_types.Contains(expr.Type)) {
|
||||
|
||||
@@ -19,13 +19,16 @@ namespace mbts.ui {
|
||||
|
||||
[DisplayInfo(name = "Default Header")]
|
||||
public static BTSColor header { get { return BTSCoreColors.base_blue; } }
|
||||
public static Color header_color => header;
|
||||
public static Brush header_brush { get { return _header_brush; } }
|
||||
public static BTSColor header_text { get { return BTSCoreColors.white; } }
|
||||
public static Color header_text_color => header_text;
|
||||
public static BTSColor header_error_text { get; } = new BTSColor(0xFE, 0x6A, 0x6E);
|
||||
public static Brush header_error_text_brush => _header_error_text_brush;
|
||||
public static BTSColor header_border { get { return new BTSColor(0x63,0x8A,0xCC); } }
|
||||
public static Brush header_text_brush { get { return _header_text_brush; } }
|
||||
public static BTSColor header_alternate { get { return BTSCoreColors.blue_grey; } }
|
||||
public static Color header_alternate_color => header_alternate;
|
||||
public static BTSColor disabled_text { get { return BTSCoreColors.gray_light; } }
|
||||
|
||||
public static BTSColor pinkalicious { get { return new BTSColor(0xff, 0x33, 0x99); } }
|
||||
@@ -98,6 +101,8 @@ namespace mbts.ui {
|
||||
public static Brush semi_transparent_bid_brush => _semi_transparent_bid_brush;
|
||||
public static Brush semi_transparent_ask_brush => _semi_transparent_ask_brush;
|
||||
|
||||
public static Color bid_color => bid;
|
||||
public static Color ask_color => ask;
|
||||
|
||||
[DisplayInfo(name = "Settlement")]
|
||||
public static BTSColor settlement_background { get { return Grid.default_column_background; } }
|
||||
@@ -139,19 +144,10 @@ namespace mbts.ui {
|
||||
|
||||
}
|
||||
|
||||
public static class RiskReport
|
||||
{
|
||||
// Base Colors
|
||||
[DisplayInfo(name = "Risk Report")]
|
||||
public static BTSColor default_background { get { return BTSCoreColors.blue_light_dark; } }
|
||||
public static BTSColor default_text { get { return BTSCoreColors.black; } }
|
||||
|
||||
[DisplayInfo(name = "Risk PnL")]
|
||||
public static BTSColor pnl_background { get { return Grid.default_column_background; } }
|
||||
|
||||
// Alerts
|
||||
public static BTSColor pnl_loss_text { get { return BTSCoreColors.red; } }
|
||||
public static BTSColor pnl_profit_text { get { return text_default; } }
|
||||
public static class PNL {
|
||||
public static Color anchor_color => Colors.Gold;
|
||||
public static Color profit_color => Colors.Green;
|
||||
public static Color loss_color => BTSColors.red;
|
||||
}
|
||||
|
||||
public static class Orders {
|
||||
|
||||
@@ -81,6 +81,10 @@ namespace mbts {
|
||||
added = newval.Except(current, comp);
|
||||
removed = current.Except(newval, comp);
|
||||
}
|
||||
public static void difference_hard<T>(this IEnumerable<T> current, IEnumerable<T> newval, out IList<T> added, out IList<T> removed, IEqualityComparer<T> comp) {
|
||||
added = newval.Except(current, comp).ToList();
|
||||
removed = current.Except(newval, comp).ToList();
|
||||
}
|
||||
public static int indexOf<T>(this IEnumerable me, T value) {
|
||||
IEnumerator enumerator = me.GetEnumerator();
|
||||
int i = 0;
|
||||
|
||||
@@ -23,11 +23,17 @@ namespace mbts.persist {
|
||||
readonly UserSettingsHub _hub;
|
||||
readonly Func<UserSettings, ImmutableDictionary<uint, SettingsProfile<T>>> _get_profiles;
|
||||
readonly Func<UserSettings, IReadOnlyDictionary<uint, SettingsProfile<T>>, UserSettings> _set_profiles;
|
||||
readonly Func<UserSettings, uint> _get_next_id;
|
||||
|
||||
public SettingsProfileService(UserSettingsHub hub, Func<UserSettings, ImmutableDictionary<uint, SettingsProfile<T>>> get_profiles, Func<UserSettings, IReadOnlyDictionary<uint, SettingsProfile<T>>, UserSettings> set_profiles) {
|
||||
public SettingsProfileService(
|
||||
UserSettingsHub hub,
|
||||
Func<UserSettings, ImmutableDictionary<uint, SettingsProfile<T>>> get_profiles,
|
||||
Func<UserSettings, IReadOnlyDictionary<uint, SettingsProfile<T>>, UserSettings> set_profiles,
|
||||
Func<UserSettings, uint> get_next_id) {
|
||||
_hub = hub;
|
||||
_get_profiles = get_profiles;
|
||||
_set_profiles = set_profiles;
|
||||
_get_next_id = get_next_id;
|
||||
}
|
||||
|
||||
// Note: this operation is atomic, but does not protect against overwrites.
|
||||
@@ -37,7 +43,7 @@ namespace mbts.persist {
|
||||
var curr = _hub.current;
|
||||
var mdata = _get_profiles(curr);
|
||||
if (value.id == 0)
|
||||
value = value.with(curr.next_ladder_profile_id);
|
||||
value = value.with(_get_next_id(curr));
|
||||
_hub.save_default(_set_profiles(curr, mdata.SetItem(value.id, value))).ignore();
|
||||
return value.id;
|
||||
}
|
||||
|
||||
@@ -1,79 +1,117 @@
|
||||
using ProtoBuf;
|
||||
using System;
|
||||
|
||||
namespace mbts.spark {
|
||||
interface ISettingsProfile<T> {
|
||||
uint id { get; }
|
||||
string name { get; }
|
||||
T value { get; }
|
||||
SecurityCriteria security_criteria { get; }
|
||||
}
|
||||
|
||||
struct SettingsProfile<T> : IEquatable<SettingsProfile<T>> where T : class, IEquatable<T> {
|
||||
public static readonly Func<SettingsProfile<T>, uint> key_fn = ns => ns.id;
|
||||
|
||||
public readonly uint id;
|
||||
public readonly string name;
|
||||
public readonly T value;
|
||||
public readonly SecurityCriteria security_criteria;
|
||||
|
||||
public SettingsProfile(uint id, string name, T value, SecurityCriteria security_criteria = null) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.value = value;
|
||||
this.security_criteria = security_criteria;
|
||||
}
|
||||
|
||||
public SettingsProfile<T> with(uint? id = null, string name = null, T value = null, SecurityCriteria security_criteria = null) =>
|
||||
new SettingsProfile<T>(id ?? this.id, name ?? this.name, value ?? this.value, security_criteria ?? this.security_criteria);
|
||||
|
||||
public bool Equals(SettingsProfile<T> other) =>
|
||||
id == other.id &&
|
||||
name == other.name &&
|
||||
value.safe_equals(other.value) &&
|
||||
security_criteria.safe_equals(other.security_criteria);
|
||||
|
||||
public override bool Equals(object obj) =>
|
||||
obj is SettingsProfile<T> other && Equals(other);
|
||||
|
||||
public override int GetHashCode() =>
|
||||
id.GetHashCode();
|
||||
}
|
||||
|
||||
[ProtoContract]
|
||||
class LadderSettingsProfile : ISettingsProfile<LadderSettings>, IEquatable<LadderSettingsProfile> {
|
||||
public LadderSettingsProfile() { }
|
||||
|
||||
public LadderSettingsProfile(uint id, string name, LadderSettings value, SecurityCriteria security_criteria = null) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.value = value;
|
||||
this.security_criteria = security_criteria;
|
||||
}
|
||||
|
||||
[ProtoMember(1)]
|
||||
public uint id { get; }
|
||||
|
||||
[ProtoMember(2)]
|
||||
public string name { get; }
|
||||
|
||||
[ProtoMember(3)]
|
||||
public LadderSettings value { get; }
|
||||
|
||||
[ProtoMember(4)]
|
||||
public SecurityCriteria security_criteria { get; }
|
||||
|
||||
public LadderSettingsProfile with(uint? id = null, LadderSettings value = null) =>
|
||||
new LadderSettingsProfile(id ?? this.id, name, value ?? this.value, security_criteria);
|
||||
|
||||
public bool Equals(LadderSettingsProfile other) =>
|
||||
other != null &&
|
||||
id == other.id &&
|
||||
name == other.name &&
|
||||
value.safe_equals(other.value) &&
|
||||
security_criteria.safe_equals(other.security_criteria);
|
||||
|
||||
public override bool Equals(object obj) => Equals(obj as LadderSettingsProfile);
|
||||
public override int GetHashCode() => id.GetHashCode();
|
||||
}
|
||||
using bts.shell.pnl;
|
||||
using ProtoBuf;
|
||||
using System;
|
||||
|
||||
namespace mbts.spark {
|
||||
interface ISettingsProfile<T> {
|
||||
uint id { get; }
|
||||
string name { get; }
|
||||
T value { get; }
|
||||
SecurityCriteria security_criteria { get; }
|
||||
}
|
||||
|
||||
struct SettingsProfile<T> : IEquatable<SettingsProfile<T>> where T : class, IEquatable<T> {
|
||||
public static readonly Func<SettingsProfile<T>, uint> key_fn = ns => ns.id;
|
||||
|
||||
public readonly uint id;
|
||||
public readonly string name;
|
||||
public readonly T value;
|
||||
public readonly SecurityCriteria security_criteria;
|
||||
|
||||
public SettingsProfile(uint id, string name, T value, SecurityCriteria security_criteria = null) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.value = value;
|
||||
this.security_criteria = security_criteria;
|
||||
}
|
||||
|
||||
public SettingsProfile<T> with(uint? id = null, string name = null, T value = null, SecurityCriteria security_criteria = null) =>
|
||||
new SettingsProfile<T>(id ?? this.id, name ?? this.name, value ?? this.value, security_criteria ?? this.security_criteria);
|
||||
|
||||
public bool Equals(SettingsProfile<T> other) =>
|
||||
id == other.id &&
|
||||
name == other.name &&
|
||||
value.safe_equals(other.value) &&
|
||||
security_criteria.safe_equals(other.security_criteria);
|
||||
|
||||
public override bool Equals(object obj) =>
|
||||
obj is SettingsProfile<T> other && Equals(other);
|
||||
|
||||
public override int GetHashCode() =>
|
||||
id.GetHashCode();
|
||||
}
|
||||
|
||||
[ProtoContract]
|
||||
class LadderSettingsProfile : ISettingsProfile<LadderSettings>, IEquatable<LadderSettingsProfile> {
|
||||
public LadderSettingsProfile() { }
|
||||
|
||||
public LadderSettingsProfile(uint id, string name, LadderSettings value, SecurityCriteria security_criteria = null) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.value = value;
|
||||
this.security_criteria = security_criteria;
|
||||
}
|
||||
|
||||
[ProtoMember(1)]
|
||||
public uint id { get; }
|
||||
|
||||
[ProtoMember(2)]
|
||||
public string name { get; }
|
||||
|
||||
[ProtoMember(3)]
|
||||
public LadderSettings value { get; }
|
||||
|
||||
[ProtoMember(4)]
|
||||
public SecurityCriteria security_criteria { get; }
|
||||
|
||||
public LadderSettingsProfile with(uint? id = null, LadderSettings value = null) =>
|
||||
new LadderSettingsProfile(id ?? this.id, name, value ?? this.value, security_criteria);
|
||||
|
||||
public bool Equals(LadderSettingsProfile other) =>
|
||||
other != null &&
|
||||
id == other.id &&
|
||||
name == other.name &&
|
||||
value.safe_equals(other.value) &&
|
||||
security_criteria.safe_equals(other.security_criteria);
|
||||
|
||||
public override bool Equals(object obj) => Equals(obj as LadderSettingsProfile);
|
||||
public override int GetHashCode() => id.GetHashCode();
|
||||
}
|
||||
|
||||
[ProtoContract]
|
||||
class PNLSettingsProfile : ISettingsProfile<PNLSettings>, IEquatable<PNLSettingsProfile> {
|
||||
public PNLSettingsProfile() { }
|
||||
|
||||
public PNLSettingsProfile(uint id, string name, PNLSettings value, SecurityCriteria security_criteria = null) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.value = value;
|
||||
this.security_criteria = security_criteria;
|
||||
}
|
||||
|
||||
[ProtoMember(1)]
|
||||
public uint id { get; }
|
||||
|
||||
[ProtoMember(2)]
|
||||
public string name { get; }
|
||||
|
||||
[ProtoMember(3)]
|
||||
public PNLSettings value { get; }
|
||||
|
||||
[ProtoMember(4)]
|
||||
public SecurityCriteria security_criteria { get; }
|
||||
|
||||
public PNLSettingsProfile with(uint? id = null, PNLSettings value = null) =>
|
||||
new PNLSettingsProfile(id ?? this.id, name, value ?? this.value, security_criteria);
|
||||
|
||||
public bool Equals(PNLSettingsProfile other) =>
|
||||
other != null &&
|
||||
id == other.id &&
|
||||
name == other.name &&
|
||||
value.safe_equals(other.value) &&
|
||||
security_criteria.safe_equals(other.security_criteria);
|
||||
|
||||
public override bool Equals(object obj) => Equals(obj as PNLSettingsProfile);
|
||||
public override int GetHashCode() => id.GetHashCode();
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using Google.Protobuf.WellKnownTypes;
|
||||
using bts.shell.pnl;
|
||||
using Google.Protobuf.WellKnownTypes;
|
||||
using mbts.ui.settings;
|
||||
using ProtoBuf;
|
||||
using System;
|
||||
@@ -40,6 +41,8 @@ namespace mbts.spark {
|
||||
/// Anytime a profile is added, it shall use <see cref="next_ladder_profile_id"/>.
|
||||
[ProtoMember(12)]
|
||||
uint _last_ladder_profile_id;
|
||||
[ProtoMember(14)]
|
||||
uint _last_pnl_profile_id;
|
||||
// removing NotificationSettings these from proto-serialization, since they are disabled by default for now.
|
||||
// This way, when they are enabled, there won't be any backwards compatability issues to worry about, as no clients will have persisted them yet.
|
||||
NotificationSettings _notification;
|
||||
@@ -56,11 +59,19 @@ namespace mbts.spark {
|
||||
GridSettings _pnl_grid;
|
||||
[ProtoMember(10)]
|
||||
bts.shell.orderbook.OrderBookGridSettings _order_book_grid;
|
||||
[ProtoMember(13)]
|
||||
PNLSettings _pnl;
|
||||
[ProtoMember(15)]
|
||||
readonly PNLSettingsProfile[] _pnl_profiles;
|
||||
|
||||
public UserSettings() { }
|
||||
|
||||
public UserSettings(LadderSettingsProfile[] ladder_profiles) =>
|
||||
public UserSettings(
|
||||
LadderSettingsProfile[] ladder_profiles,
|
||||
PNLSettingsProfile[] pnl_profiles) {
|
||||
_ladder_profiles = ladder_profiles;
|
||||
_pnl_profiles = pnl_profiles;
|
||||
}
|
||||
|
||||
public LadderSettings ladder => _ladder ?? (_ladder = LadderSettings.Default);
|
||||
public IReadOnlyList<LadderSettingsProfile> ladder_profiles => _ladder_profiles ?? Array.Empty<LadderSettingsProfile>();
|
||||
@@ -71,6 +82,9 @@ namespace mbts.spark {
|
||||
public GridSettings fills_grid => _fills_grid ?? (_fills_grid = bts.shell.fills.FillsSettings.DefaultGridSettings);
|
||||
public GridSettings synthetic_management_grid => _synthetic_management_grid ?? (_synthetic_management_grid = bts.shell.SyntheticManagementWindowFactory.StartupData.DefaultGridSettings);
|
||||
public GridSettings pnl_grid => _pnl_grid ?? (_pnl_grid = bts.shell.PAndLWindowFactory.PAndLStartupData.DefaultGridSettings);
|
||||
public PNLSettings pnl => _pnl ?? (_pnl = PNLSettings.Default);
|
||||
public uint next_pnl_profile_id => _last_pnl_profile_id + 1;
|
||||
public IReadOnlyList<PNLSettingsProfile> pnl_profiles => _pnl_profiles ?? Array.Empty<PNLSettingsProfile>();
|
||||
public bts.shell.orderbook.OrderBookGridSettings order_book_grid => _order_book_grid ?? (_order_book_grid = bts.shell.orderbook.OrderBookStartupData.Default.grid_settings_wrapper());
|
||||
|
||||
public UserSettings with(LadderSettings ladder = null,
|
||||
@@ -81,25 +95,32 @@ namespace mbts.spark {
|
||||
GridSettings provided_fills_gridsettings = null,
|
||||
GridSettings provided_synthetic_management_gridsettings = null,
|
||||
GridSettings provided_pnl_gridsettings = null,
|
||||
PNLSettings pnl = null,
|
||||
PNLSettingsProfile[] pnl_profiles = null,
|
||||
bts.shell.orderbook.OrderBookGridSettings order_book_grid = null) {
|
||||
var us = new UserSettings(ladder_profiles ?? _ladder_profiles) {
|
||||
var us = new UserSettings(
|
||||
ladder_profiles ?? _ladder_profiles,
|
||||
pnl_profiles ?? _pnl_profiles) {
|
||||
_ladder = ladder ?? _ladder,
|
||||
_notification = notification ?? _notification,
|
||||
_market_view_grid = provided_market_view_gridsettings ?? _market_view_grid,
|
||||
_time_and_sales_grid = time_and_sales ?? _time_and_sales_grid,
|
||||
_fills_grid = provided_fills_gridsettings ?? _fills_grid,
|
||||
_synthetic_management_grid = provided_synthetic_management_gridsettings ?? _synthetic_management_grid,
|
||||
_pnl_grid = provided_pnl_gridsettings ?? _pnl_grid,
|
||||
_pnl_grid = provided_pnl_gridsettings ?? _pnl_grid,
|
||||
_pnl = pnl ?? _pnl,
|
||||
_order_book_grid = order_book_grid ?? _order_book_grid,
|
||||
};
|
||||
us._last_ladder_profile_id = Math.Max(_last_ladder_profile_id, us.ladder_profiles.Select(p => p.id).MaxOrDefault());
|
||||
us._last_pnl_profile_id = Math.Max(_last_pnl_profile_id, us.pnl_profiles.Select(p => p.id).MaxOrDefault());
|
||||
return us;
|
||||
}
|
||||
|
||||
public static class FileExtensions {
|
||||
public const string
|
||||
GRID = "grid",
|
||||
LADDER = "ladder",
|
||||
GRID = "grid";
|
||||
PNL = "pnl";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
|
||||
namespace mbts.utils {
|
||||
delegate bool TryGetValue<T>(out T value);
|
||||
@@ -11,4 +12,18 @@ namespace mbts.utils {
|
||||
XOr = (a, b) => a ? !b : b,
|
||||
None = (a, b) => !a && !b;
|
||||
}
|
||||
|
||||
static class FormattingDelegates {
|
||||
public static class Price {
|
||||
public static readonly Func<double, string> Value =
|
||||
p => double.IsNaN(p) ? "" : p.ToString("#,0.00");
|
||||
}
|
||||
|
||||
public static class Price<T> where T : struct, IFormattable {
|
||||
public static readonly Func<T, string> Value =
|
||||
typeof(T) == typeof(double) ?
|
||||
(Func<T, string>)(object)Price.Value :
|
||||
p => p.ToString("#,0.00", CultureInfo.CurrentCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,13 @@ namespace mbts.spark {
|
||||
void load_from(T value);
|
||||
}
|
||||
|
||||
interface IResultChanged {
|
||||
event Action result_changed;
|
||||
}
|
||||
|
||||
interface IValueFactoryResultChanged<T> : IValueFactory<T>, IResultChanged {
|
||||
}
|
||||
|
||||
interface IMutableValueWrapperVM<T> : INotifyPropertyChanged {
|
||||
T value { get; set; }
|
||||
}
|
||||
|
||||
+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,6 +1,10 @@
|
||||
using bts.api.core;
|
||||
using mbts;
|
||||
using mbts.spark;
|
||||
using mbts.ui.shell;
|
||||
using mbts.utils;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
@@ -8,14 +12,90 @@ using System.Reactive.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
namespace bts.utils {
|
||||
static class ObjectSnapshot {
|
||||
public static IObservable<TableUpdate<(string name, object current, object original), string>> property_table<T>(T instance, params string[] prop_names) where T : INotifyPropertyChanged =>
|
||||
property_table(instance, Array.ConvertAll(prop_names, Impl<T>.type.GetProperty));
|
||||
interface IPropertyChangedDetector<T> where T : IRaiseNotifyPropertyChanged {
|
||||
IDisposable detect_changes(T target);
|
||||
}
|
||||
|
||||
public static IObservable<TableUpdate<(string name, object current, object original), string>> property_table<T>(T instance, params PropertyInfo[] props) where T : INotifyPropertyChanged =>
|
||||
static class ObjectSnapshot {
|
||||
public static IEnumerable<Func<TSource, TValue>> property_delegates_of_type<TSource, TValue>() where TSource : class where TValue : class {
|
||||
var tval = typeof(TValue);
|
||||
return typeof(TSource).GetProperties(BindingFlags.Public | BindingFlags.Instance)
|
||||
.Where(p => tval.IsAssignableFrom(p.PropertyType))
|
||||
.Select(_prop_getter<TSource, TValue>);
|
||||
}
|
||||
|
||||
public static IEnumerable<Func<TSource, TValue>> property_delegates_of_type<TSource, TValue>(params string[] except_props) where TSource : class where TValue : class {
|
||||
var except = new HashSet<string>(except_props);
|
||||
return property_delegates_of_type<TSource, TValue>(p => !except.Contains(p.Name));
|
||||
}
|
||||
|
||||
public static IEnumerable<Func<TSource, TValue>> property_delegates_of_type<TSource, TValue>(Func<PropertyInfo, bool> prop_filter) where TSource : class where TValue : class {
|
||||
if (prop_filter == null)
|
||||
return property_delegates_of_type<TSource, TValue>();
|
||||
var tval = typeof(TValue);
|
||||
return typeof(TSource).GetProperties(BindingFlags.Public | BindingFlags.Instance)
|
||||
.Where(p => tval.IsAssignableFrom(p.PropertyType) && prop_filter(p))
|
||||
.Select(_prop_getter<TSource, TValue>);
|
||||
}
|
||||
|
||||
public static IDisposable connect_property_changed(INotifyPropertyChanged component, PropertyChangedEventHandler callback) {
|
||||
component.PropertyChanged += callback;
|
||||
return Disposable.Create(() => component.PropertyChanged -= callback);
|
||||
}
|
||||
|
||||
public static IDisposable connect_property_changed(IEnumerable<INotifyPropertyChanged> components, PropertyChangedEventHandler callback) {
|
||||
foreach (var c in components)
|
||||
c.PropertyChanged += callback;
|
||||
return Disposable.Create(() => {
|
||||
foreach (var c in components)
|
||||
c.PropertyChanged -= callback;
|
||||
});
|
||||
}
|
||||
|
||||
public static IDisposable connect_result_changed(IResultChanged source, Action callback) {
|
||||
source.result_changed += callback;
|
||||
return Disposable.Create(() => source.result_changed -= callback);
|
||||
}
|
||||
|
||||
public static IDisposable connect_result_changed(IEnumerable<IResultChanged> components, Action callback) {
|
||||
foreach (var c in components)
|
||||
c.result_changed += callback;
|
||||
return Disposable.Create(() => {
|
||||
foreach (var c in components)
|
||||
c.result_changed -= callback;
|
||||
});
|
||||
}
|
||||
|
||||
public static Func<TSource, Action, IDisposable> auto_change_observer_factory<TSource>(params string[] except_props) where TSource : class {
|
||||
var except = new HashSet<string>(except_props);
|
||||
var del = SourceChangeObserver<TSource>.observe_changes_del;
|
||||
return impl;
|
||||
IDisposable impl(TSource source, Action callback) {
|
||||
var hook = del(source, callback);
|
||||
if (source is IResultChanged rc) {
|
||||
rc.result_changed += callback;
|
||||
hook = Disposable.Create(hook, connect_result_changed(rc, callback));
|
||||
} else if (source is INotifyPropertyChanged npc) {
|
||||
npc.PropertyChanged += on_prop_changed;
|
||||
hook = Disposable.Create(hook, connect_property_changed(npc, on_prop_changed));
|
||||
void on_prop_changed(object sender, PropertyChangedEventArgs e) =>
|
||||
callback();
|
||||
}
|
||||
return hook;
|
||||
}
|
||||
}
|
||||
|
||||
public static IObservable<TableUpdate<(string name, object current, object original), string>> property_table<T>(T instance, params string[] prop_names) where T : INotifyPropertyChanged =>
|
||||
_property_table(instance, Array.ConvertAll(prop_names, PropertyInfoStore<T>.type.GetProperty));
|
||||
|
||||
public static IObservable<TableUpdate<(string name, object current, object original), string>> property_table(INotifyPropertyChanged instance, params string[] prop_names) =>
|
||||
_property_table(instance, Array.ConvertAll(prop_names, instance.GetType().GetProperty));
|
||||
|
||||
static IObservable<TableUpdate<(string name, object current, object original), string>> _property_table(INotifyPropertyChanged instance, params PropertyInfo[] props) =>
|
||||
Observable.Create<TableUpdate<(string name, object current, object original), string>>(o => {
|
||||
var cache = props.ToDictionary(p => p.Name, p => new PropNode(p, p.GetValue(instance)));
|
||||
var table = TableUpdate<(string name, object current, object original), string>.mk_empty(t => t.name).with_added(cache.Values.Select(pn => (pn.prop.Name, pn.current, pn.original)));
|
||||
o.OnNext(table);
|
||||
instance.PropertyChanged += on_prop_changed;
|
||||
return Disposable.Create(() => instance.PropertyChanged -= on_prop_changed);
|
||||
void on_prop_changed(object sender, PropertyChangedEventArgs e) {
|
||||
@@ -28,11 +108,20 @@ namespace bts.utils {
|
||||
}
|
||||
});
|
||||
|
||||
public static IPropertyChangedDetector<T> build_detector<T>(params string[] props) where T : IRaiseNotifyPropertyChanged =>
|
||||
new DiscretePropertyChangedDetector<T>(props);
|
||||
|
||||
public static IPropertyChangedDetector<T> build_detector_except<T>(params string[] except_props) where T : IRaiseNotifyPropertyChanged {
|
||||
// convert to a hashset for quicker lookups
|
||||
var exclude = new HashSet<string>(except_props);
|
||||
return new DiscretePropertyChangedDetector<T>(PropertyInfoStore<T>.all_props.Where(p => !exclude.Contains(p.Name)).Select(p => p.Name).ToArray());
|
||||
}
|
||||
|
||||
public static IObservable<bool> dirty_monitor<T>(T instance, params string[] prop_names) where T : INotifyPropertyChanged =>
|
||||
dirty_monitor(instance, Array.ConvertAll(prop_names, Impl<T>.type.GetProperty));
|
||||
dirty_monitor(instance, Array.ConvertAll(prop_names, PropertyInfoStore<T>.type.GetProperty));
|
||||
|
||||
public static IObservable<bool> dirty_monitor<T>(T instance, out IDisposable snapshot, params string[] prop_names) where T : INotifyPropertyChanged =>
|
||||
dirty_monitor(instance, out snapshot, Array.ConvertAll(prop_names, Impl<T>.type.GetProperty));
|
||||
dirty_monitor(instance, out snapshot, Array.ConvertAll(prop_names, PropertyInfoStore<T>.type.GetProperty));
|
||||
|
||||
public static IObservable<bool> dirty_monitor<T>(T instance, out IDisposable snapshot, params PropertyInfo[] props) where T : INotifyPropertyChanged {
|
||||
var prop_map = props.ToDictionary(p => p.Name, p => new PropNode(p, p.GetValue(instance)));
|
||||
@@ -67,7 +156,7 @@ namespace bts.utils {
|
||||
});
|
||||
|
||||
public static IDisposable create<T>(T instance) =>
|
||||
create(instance, Impl<T>.mutable_props);
|
||||
create(instance, PropertyInfoStore<T>.mutable_props);
|
||||
|
||||
public static IDisposable create<T>(T instance, PropertyInfo[] props) {
|
||||
var snapshot = Array.ConvertAll(props, p => p.GetValue(instance));
|
||||
@@ -77,13 +166,88 @@ namespace bts.utils {
|
||||
});
|
||||
}
|
||||
|
||||
static class Impl<T> {
|
||||
static Func<T, TValue> _prop_getter<T, TValue>(PropertyInfo prop) =>
|
||||
(Func<T, TValue>)prop.GetGetMethod().CreateDelegate(typeof(Func<T, TValue>));
|
||||
|
||||
static class PropertyInfoStore<T> {
|
||||
public static readonly Type type = typeof(T);
|
||||
|
||||
public static readonly PropertyInfo[] mutable_props =
|
||||
type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
|
||||
.Where(p => p.GetSetMethod() != null)
|
||||
.ToArray();
|
||||
public static readonly PropertyInfo[]
|
||||
all_props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance),
|
||||
mutable_props;
|
||||
|
||||
static PropertyInfoStore() =>
|
||||
mutable_props = all_props.Where(p => p.GetGetMethod() != null).ToArray();
|
||||
}
|
||||
|
||||
// DelegateStore maintains a cache of property getter/setter delegates,
|
||||
// which perform much better than the corresponding PropertyInfo.GetValue/SetValue.
|
||||
static class DelegateStore<T> {
|
||||
public static readonly Type type = typeof(T);
|
||||
|
||||
static readonly IReadOnlyDictionary<string, (Func<T, object> get, Action<T, object> set)> _property_delegates;
|
||||
|
||||
public static Func<T, object> getter(string prop_name) =>
|
||||
_property_delegates.TryGetValue(prop_name, out var dels) ? dels.get : null;
|
||||
|
||||
public static Action<T, object> setter(string prop_name) =>
|
||||
_property_delegates.TryGetValue(prop_name, out var dels) ? dels.set : null;
|
||||
|
||||
static DelegateStore() {
|
||||
var prop_dels = new Dictionary<string, (Func<T, object> get, Action<T, object> set)>();
|
||||
foreach (var p in PropertyInfoStore<T>.all_props) {
|
||||
Func<T, object> wrapped_getter = null;
|
||||
Action<T, object> wrapped_setter = null;
|
||||
var getter = p.GetGetMethod();
|
||||
var setter = p.GetSetMethod();
|
||||
if (getter == null && setter == null)
|
||||
continue;
|
||||
var del_types = (typeof(Func<,>).MakeGenericType(typeof(T), p.PropertyType), typeof(Action<,>).MakeGenericType(typeof(T), p.PropertyType));
|
||||
var twrapper = typeof(Wrapper<>).MakeGenericType(typeof(T), p.PropertyType);
|
||||
if (getter != null) {
|
||||
wrapped_getter = (Func<T, object>)
|
||||
twrapper.GetMethod(nameof(Wrapper<T>.getter), BindingFlags.Static | BindingFlags.Public)
|
||||
.Invoke(null, new[] { getter.CreateDelegate(del_types.Item1) });
|
||||
}
|
||||
if (setter != null) {
|
||||
wrapped_setter = (Action<T, object>)
|
||||
twrapper.GetMethod(nameof(Wrapper<T>.setter), BindingFlags.Static | BindingFlags.Public)
|
||||
.Invoke(null, new[] { setter.CreateDelegate(del_types.Item2) });
|
||||
}
|
||||
prop_dels[p.Name] = (wrapped_getter, wrapped_setter);
|
||||
}
|
||||
_property_delegates = prop_dels;
|
||||
}
|
||||
|
||||
static class Wrapper<TValue> {
|
||||
public static Func<T, object> getter(Func<T, TValue> getter) => src => getter(src);
|
||||
public static Action<T, object> setter(Action<T, TValue> setter) => (src, val) => setter(src, (TValue)val);
|
||||
}
|
||||
}
|
||||
|
||||
static class SourceChangeObserver<TSource> where TSource : class {
|
||||
public static readonly Func<TSource, Action, IDisposable> observe_changes_del;
|
||||
|
||||
static readonly IReadOnlyList<Func<TSource, INotifyPropertyChanged>> _observable_component_getters =
|
||||
property_delegates_of_type<TSource, INotifyPropertyChanged>(p => typeof(IResultChanged).IsAssignableFrom(p.PropertyType))
|
||||
.ToArray();
|
||||
|
||||
static readonly IReadOnlyList<Func<TSource, IResultChanged>> _result_changed_component_getters =
|
||||
property_delegates_of_type<TSource, IResultChanged>()
|
||||
.ToArray();
|
||||
|
||||
// initialize in constructor to avoid dependency issues (wrt to other static fields)
|
||||
static SourceChangeObserver() =>
|
||||
observe_changes_del = build_observe_changes_del(null);
|
||||
|
||||
public static Func<TSource, Action, IDisposable> build_observe_changes_del(Func<PropertyInfo, bool> prop_filter) {
|
||||
var npc_getters = property_delegates_of_type<TSource, INotifyPropertyChanged>(prop_filter);
|
||||
var result_getters = property_delegates_of_type<TSource, IResultChanged>(prop_filter);
|
||||
return (source, callback) =>
|
||||
Disposable.Create(
|
||||
connect_property_changed(npc_getters.Select(g => g(source)), (s, e) => callback()),
|
||||
connect_result_changed(result_getters.Select(g => g(source)), callback));
|
||||
}
|
||||
}
|
||||
|
||||
class PropNode {
|
||||
@@ -99,5 +263,30 @@ namespace bts.utils {
|
||||
public object original { get; set; }
|
||||
public object current { get; set; }
|
||||
}
|
||||
|
||||
class DiscretePropertyChangedDetector<T> : IPropertyChangedDetector<T> where T : IRaiseNotifyPropertyChanged {
|
||||
static readonly Converter<string, (string name, Func<T, object> get)> _realize_prop =
|
||||
(prop_name) => (prop_name, DelegateStore<T>.getter(prop_name));
|
||||
|
||||
readonly (string name, Func<T, object> get)[] _target_props;
|
||||
|
||||
public DiscretePropertyChangedDetector()
|
||||
: this(Array.ConvertAll(PropertyInfoStore<T>.all_props, p => p.Name)) { }
|
||||
|
||||
public DiscretePropertyChangedDetector(params string[] props) =>
|
||||
_target_props = Array.ConvertAll(props, _realize_prop);
|
||||
|
||||
public IDisposable detect_changes(T target) {
|
||||
var get_target_val = new Converter<(string name, Func<T, object> get), object>(tup => tup.get(target));
|
||||
var orig_state = Array.ConvertAll(_target_props, get_target_val);
|
||||
return Disposable.Create(() => {
|
||||
var curr_state = Array.ConvertAll(_target_props, get_target_val);
|
||||
for (int i = 0; i < curr_state.Length; i++) {
|
||||
if (!Equals(curr_state[i], orig_state[i]))
|
||||
target.raise_notify_property_changed(_target_props[i].name);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,16 @@ namespace bts {
|
||||
}
|
||||
}
|
||||
|
||||
public static OneOf<A, B> MatchFirst<TSource, A, B>(IEnumerable<TSource> source) {
|
||||
foreach (var v in source) {
|
||||
if (v is A va) {
|
||||
return new OneOf<A, B>(va);
|
||||
} else if (v is B vb)
|
||||
return new OneOf<A, B>(vb);
|
||||
}
|
||||
return new OneOf<A, B>();
|
||||
}
|
||||
|
||||
public static T infer<T, A, B>(object value, Func<A, T> a, Func<B, T> b) =>
|
||||
OneOf<A, B>.infer(value).Match(a, b);
|
||||
}
|
||||
@@ -101,6 +111,13 @@ namespace bts {
|
||||
public OneOf(B b) { _b = b; _tag = 1; }
|
||||
public OneOf(C c) { _c = c; _tag = 2; }
|
||||
|
||||
public static OneOf<A, B, C> infer(object obj) {
|
||||
if (obj is A a) return new OneOf<A, B, C>(a);
|
||||
if (obj is B b) return new OneOf<A, B, C>(b);
|
||||
if (obj is C c) return new OneOf<A, B, C>(c);
|
||||
return new OneOf<A, B, C>();
|
||||
}
|
||||
|
||||
public void Match(Action<A> a, Action<B> b, Action<C> c) {
|
||||
switch (_tag) {
|
||||
case 0: a(_a); break;
|
||||
@@ -118,6 +135,15 @@ namespace bts {
|
||||
}
|
||||
}
|
||||
|
||||
public OneOf<A2, B2, C2> Extend<A2, B2, C2>(Func<A, A2> a, Func<B, B2> b, Func<C, C2> c) {
|
||||
switch (_tag) {
|
||||
case 0: return new OneOf<A2, B2, C2>(a(_a));
|
||||
case 1: return new OneOf<A2, B2, C2>(b(_b));
|
||||
case 2: return new OneOf<A2, B2, C2>(c(_c));
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
||||
public object value {
|
||||
get {
|
||||
switch (_tag) {
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
using System;
|
||||
|
||||
namespace bts.utils {
|
||||
/// <summary>
|
||||
/// An IComparable implementation that supports cross type comparisons of any combination of int, uint, double, and decimal.
|
||||
/// </summary>
|
||||
readonly struct UniversalNumericComparer : IComparable {
|
||||
readonly int? _int;
|
||||
readonly uint? _uint;
|
||||
readonly double? _double;
|
||||
readonly decimal? _decimal;
|
||||
|
||||
public UniversalNumericComparer(int i) {
|
||||
_int = i;
|
||||
_uint = null;
|
||||
_double = null;
|
||||
_decimal = null;
|
||||
}
|
||||
|
||||
public UniversalNumericComparer(double d) {
|
||||
_int = null;
|
||||
_uint = null;
|
||||
_double = d;
|
||||
_decimal = null;
|
||||
}
|
||||
|
||||
public UniversalNumericComparer(uint u) {
|
||||
_int = null;
|
||||
_uint = u;
|
||||
_double = null;
|
||||
_decimal = null;
|
||||
}
|
||||
|
||||
public UniversalNumericComparer(decimal d) {
|
||||
_int = null;
|
||||
_uint = null;
|
||||
_double = null;
|
||||
_decimal = d;
|
||||
}
|
||||
|
||||
public int CompareTo(object obj) {
|
||||
if (obj is int i) {
|
||||
return -(i.CompareTo(this));
|
||||
} else if (obj is uint u) {
|
||||
return -(u.CompareTo(this));
|
||||
} else if (obj is double d)
|
||||
return -(d.CompareTo(this));
|
||||
else if (obj is decimal dec)
|
||||
return -(dec.CompareTo(this));
|
||||
throw new ArgumentException("Obj is not of a supported numeric type (int, uint, double).", nameof(obj));
|
||||
}
|
||||
|
||||
public static implicit operator int(UniversalNumericComparer w) {
|
||||
if (w._int.HasValue)
|
||||
return w._int.Value;
|
||||
else if (w._double.HasValue)
|
||||
return (int)w._double.Value;
|
||||
else if (w._uint.HasValue)
|
||||
return (int)w._uint.Value;
|
||||
else if (w._decimal.HasValue)
|
||||
return (int)w._decimal.Value;
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static implicit operator uint(UniversalNumericComparer w) {
|
||||
if (w._uint.HasValue)
|
||||
return w._uint.Value;
|
||||
else if (w._int.HasValue)
|
||||
return (uint)w._int.Value;
|
||||
else if (w._double.HasValue)
|
||||
return (uint)w._double.Value;
|
||||
else if (w._decimal.HasValue)
|
||||
return (uint)w._decimal.Value;
|
||||
return 0u;
|
||||
}
|
||||
|
||||
public static implicit operator double(UniversalNumericComparer w) {
|
||||
if (w._double.HasValue)
|
||||
return w._double.Value;
|
||||
else if (w._int.HasValue)
|
||||
return w._int.Value;
|
||||
else if (w._uint.HasValue)
|
||||
return w._uint.Value;
|
||||
else if (w._decimal.HasValue)
|
||||
return (double)w._decimal.Value;
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
public static implicit operator decimal(UniversalNumericComparer w) {
|
||||
if (w._double.HasValue)
|
||||
return (decimal)w._double.Value;
|
||||
else if (w._int.HasValue)
|
||||
return w._int.Value;
|
||||
else if (w._uint.HasValue)
|
||||
return w._uint.Value;
|
||||
else if (w._decimal.HasValue)
|
||||
return w._decimal.Value;
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static implicit operator UniversalNumericComparer(int i) => new UniversalNumericComparer(i);
|
||||
public static implicit operator UniversalNumericComparer(uint u) => new UniversalNumericComparer(u);
|
||||
public static implicit operator UniversalNumericComparer(double d) => new UniversalNumericComparer(d);
|
||||
public static implicit operator UniversalNumericComparer(decimal d) => new UniversalNumericComparer(d);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using mbts.ui;
|
||||
using mbts.ui.utility.text;
|
||||
using ProtoBuf;
|
||||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using FontFamily = System.Windows.Media.FontFamily;
|
||||
|
||||
namespace bts.proto {
|
||||
[ProtoContract]
|
||||
class FontParamsProto : IEquatable<FontParamsProto> {
|
||||
[ProtoMember(1)]
|
||||
public string font_family { get; set; }
|
||||
[ProtoMember(2)]
|
||||
public int font_weight { get; set; }
|
||||
[ProtoMember(3)]
|
||||
public uint font_color { get; set; }
|
||||
[ProtoMember(4)]
|
||||
public double font_size { get; set; }
|
||||
|
||||
public (FontFamily family, FontWeight? weight, Color? color, double size) to_wpf_params() =>
|
||||
(string.IsNullOrEmpty(font_family) ? null : new FontFamily(font_family),
|
||||
font_weight == 0 ? (FontWeight?)null : FontWeight.FromOpenTypeWeight(font_weight),
|
||||
font_color == 0 ? (Color?)null : font_color.argb_color(),
|
||||
// zero is an invalid font size, convert to nan to follow convention
|
||||
font_size == 0 ? double.NaN : font_size);
|
||||
|
||||
public (FontFamily family, FontWeight weight, Color color, double size) to_wpf_params_safe(
|
||||
FontFamily fallback_family = null,
|
||||
FontWeight? fallback_weight = null,
|
||||
Color? fallback_color = null,
|
||||
double fallback_size = double.NaN) {
|
||||
//
|
||||
var (family, weight, color, size) = to_wpf_params();
|
||||
return (
|
||||
family ?? fallback_family ?? TextUtils.StandardFontFamily,
|
||||
weight ?? fallback_weight ?? FontWeights.Normal,
|
||||
color ?? fallback_color ?? Colors.Black,
|
||||
double.IsNaN(font_size) ? double.IsNaN(fallback_size) ? FontSizes.Small : fallback_size : font_size);
|
||||
}
|
||||
|
||||
public bool Equals(FontParamsProto other) =>
|
||||
other != null &&
|
||||
font_family == other.font_family &&
|
||||
font_weight == other.font_weight &&
|
||||
font_color == other.font_color &&
|
||||
font_size.Equals(other.font_size);
|
||||
|
||||
public override bool Equals(object obj) =>
|
||||
Equals(obj as FontParamsProto);
|
||||
|
||||
public override int GetHashCode() =>
|
||||
872259;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
using bts.utils;
|
||||
using mbts.ui;
|
||||
using mbts.utils;
|
||||
using ProtoBuf;
|
||||
using System;
|
||||
using System.Windows.Media;
|
||||
@@ -17,13 +16,8 @@ namespace bts.proto {
|
||||
public NullableColorProto(uint value) =>
|
||||
_value = value;
|
||||
|
||||
public Color? value {
|
||||
get {
|
||||
if (_value.HasValue)
|
||||
return _value.Value.rgb_color();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
public Color? value => _value?.rgb_color();
|
||||
public Color? argb => _value?.argb_color();
|
||||
|
||||
public bool Equals(NullableColorProto other) =>
|
||||
EqualityComparer.nullable_equals(_value, other._value);
|
||||
|
||||
@@ -61,11 +61,19 @@
|
||||
<SolidColorBrush x:Key="CheckBox.Pressed.BorderBrush" Color="{StaticResource CheckBox.Pressed.BorderColor}"/>
|
||||
|
||||
<!--ComboBox-->
|
||||
<Color x:Key="ComboBoxBackgroundLightColor">#FFE5E5E5</Color>
|
||||
<Color x:Key="ComboBoxBackgroundMediumColor">#FFF0F0F0</Color>
|
||||
|
||||
<Color x:Key="ComboBoxBorderLightColor">#FFACACAC</Color>
|
||||
<Color x:Key="ComboBoxBorderMediumColor">#FF7EB4EA</Color>
|
||||
<Color x:Key="ComboBoxBorderDarkColor">#FF569DE5</Color>
|
||||
<Color x:Key="ComboBoxDisabledBorderColor">#FFD9D9D9</Color>
|
||||
|
||||
<LinearGradientBrush x:Key="ComboBox.Static.Background" EndPoint="0,1" StartPoint="0,0">
|
||||
<GradientStop Color="#FFF0F0F0" Offset="0.0"/>
|
||||
<GradientStop Color="#FFE5E5E5" Offset="1.0"/>
|
||||
<GradientStop Color="{StaticResource ComboBoxBackgroundLightColor}" Offset="0.0"/>
|
||||
<GradientStop Color="{StaticResource ComboBoxBackgroundMediumColor}" Offset="1.0"/>
|
||||
</LinearGradientBrush>
|
||||
<SolidColorBrush x:Key="ComboBox.Static.Border" Color="#FFACACAC"/>
|
||||
<SolidColorBrush x:Key="ComboBox.Static.Border" Color="{StaticResource ComboBoxBorderLightColor}"/>
|
||||
<SolidColorBrush x:Key="ComboBox.Static.Editable.Background" Color="#FFFFFFFF"/>
|
||||
<SolidColorBrush x:Key="ComboBox.Static.Editable.Border" Color="#FFABADB3"/>
|
||||
<SolidColorBrush x:Key="ComboBox.Static.Editable.Button.Background" Color="Transparent"/>
|
||||
@@ -75,7 +83,7 @@
|
||||
<GradientStop Color="#FFECF4FC" Offset="0.0"/>
|
||||
<GradientStop Color="#FFDCECFC" Offset="1.0"/>
|
||||
</LinearGradientBrush>
|
||||
<SolidColorBrush x:Key="ComboBox.MouseOver.Border" Color="#FF7EB4EA"/>
|
||||
<SolidColorBrush x:Key="ComboBox.MouseOver.Border" Color="{StaticResource ComboBoxBorderMediumColor}"/>
|
||||
<SolidColorBrush x:Key="ComboBox.MouseOver.Editable.Background" Color="#FFFFFFFF"/>
|
||||
<SolidColorBrush x:Key="ComboBox.MouseOver.Editable.Border" Color="#FF7EB4EA"/>
|
||||
<LinearGradientBrush x:Key="ComboBox.MouseOver.Editable.Button.Background" EndPoint="0,1" StartPoint="0,0">
|
||||
@@ -88,7 +96,7 @@
|
||||
<GradientStop Color="#FFDAECFC" Offset="0.0"/>
|
||||
<GradientStop Color="#FFC4E0FC" Offset="1.0"/>
|
||||
</LinearGradientBrush>
|
||||
<SolidColorBrush x:Key="ComboBox.Pressed.Border" Color="#FF569DE5"/>
|
||||
<SolidColorBrush x:Key="ComboBox.Pressed.Border" Color="{StaticResource ComboBoxBorderDarkColor}"/>
|
||||
<SolidColorBrush x:Key="ComboBox.Pressed.Editable.Background" Color="#FFFFFFFF"/>
|
||||
<SolidColorBrush x:Key="ComboBox.Pressed.Editable.Border" Color="#FF569DE5"/>
|
||||
<LinearGradientBrush x:Key="ComboBox.Pressed.Editable.Button.Background" EndPoint="0,1" StartPoint="0,0">
|
||||
@@ -97,8 +105,8 @@
|
||||
</LinearGradientBrush>
|
||||
<SolidColorBrush x:Key="ComboBox.Pressed.Editable.Button.Border" Color="#FF569DE5"/>
|
||||
<SolidColorBrush x:Key="ComboBox.Disabled.Glyph" Color="#FFBFBFBF"/>
|
||||
<SolidColorBrush x:Key="ComboBox.Disabled.Background" Color="#FFF0F0F0"/>
|
||||
<SolidColorBrush x:Key="ComboBox.Disabled.Border" Color="#FFD9D9D9"/>
|
||||
<SolidColorBrush x:Key="ComboBox.Disabled.Background" Color="{StaticResource ComboBoxBackgroundMediumColor}"/>
|
||||
<SolidColorBrush x:Key="ComboBox.Disabled.Border" Color="{StaticResource ComboBoxDisabledBorderColor}"/>
|
||||
<SolidColorBrush x:Key="ComboBox.Disabled.Editable.Background" Color="#FFFFFFFF"/>
|
||||
<SolidColorBrush x:Key="ComboBox.Disabled.Editable.Border" Color="#FFBFBFBF"/>
|
||||
<SolidColorBrush x:Key="ComboBox.Disabled.Editable.Button.Background" Color="Transparent"/>
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
using mbts.ui.extensions;
|
||||
using mbts.utils;
|
||||
using System;
|
||||
using System.Reactive.Disposables;
|
||||
using System.Windows;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace mbts.ui.controls.adorners {
|
||||
class MouseTraceAdorner : Adorner {
|
||||
readonly Visual _trace_visual;
|
||||
readonly Size _trace_visual_size;
|
||||
|
||||
MouseTraceAdorner(UIElement adorned_element, Visual trace_visual, Size trace_visual_size)
|
||||
: base(adorned_element) {
|
||||
_trace_visual = trace_visual;
|
||||
_trace_visual_size = trace_visual_size;
|
||||
AddVisualChild(trace_visual);
|
||||
IsHitTestVisible = false;
|
||||
HorizontalAlignment = HorizontalAlignment.Left;
|
||||
VerticalAlignment = VerticalAlignment.Top;
|
||||
}
|
||||
|
||||
protected override int VisualChildrenCount => 1;
|
||||
|
||||
public static IDisposable trace(FrameworkElement target, Visual trace_visual, Size trace_visual_size) {
|
||||
var mta = new MouseTraceAdorner(target, trace_visual, trace_visual_size);
|
||||
move_to(Mouse.GetPosition(target));
|
||||
mta.attach();
|
||||
return new CompositeDisposable {
|
||||
target.SubscribePreviewMouseMove(on_mouse),
|
||||
target.SubscribeMouseEnter(on_mouse),
|
||||
target.SubscribeMouseLeave(on_mouse),
|
||||
mta.detach
|
||||
};
|
||||
void on_mouse(object s, MouseEventArgs e) =>
|
||||
move_to(e.GetPosition(target));
|
||||
|
||||
void move_to(Point pt) =>
|
||||
mta.RenderTransform = new TranslateTransform(pt.X, pt.Y);
|
||||
}
|
||||
|
||||
public static IDisposable trace_drag(FrameworkElement target, Visual trace_visual, Size trace_visual_size) {
|
||||
var mta = new MouseTraceAdorner(target, trace_visual, trace_visual_size);
|
||||
move_to(Mouse.GetPosition(target));
|
||||
mta.attach();
|
||||
return new CompositeDisposable {
|
||||
target.SubscribePreviewDragOver(on_drag),
|
||||
target.SubscribePreviewDragLeave(on_drag),
|
||||
target.SubscribePreviewDragEnter(on_drag),
|
||||
mta.detach
|
||||
};
|
||||
void on_drag(object s, DragEventArgs e) =>
|
||||
move_to(e.GetPosition(target));
|
||||
|
||||
void move_to(Point pt) =>
|
||||
mta.RenderTransform = new TranslateTransform(pt.X, pt.Y);
|
||||
}
|
||||
|
||||
protected override Visual GetVisualChild(int index) =>
|
||||
index == 0 ? _trace_visual : throw new ArgumentOutOfRangeException(nameof(index));
|
||||
|
||||
protected override Size MeasureOverride(Size constraint) => _trace_visual_size;
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,24 @@
|
||||
using bts.utils;
|
||||
using bts;
|
||||
using bts.utils;
|
||||
using mbts.ui.extensions;
|
||||
using mbts.utils;
|
||||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls.Primitives;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace mbts.ui.controls.adorners {
|
||||
internal enum HorizontalPosition { Left, Middle, Right }
|
||||
internal enum VerticalPosition { Top, Middle, Botton }
|
||||
internal enum VerticalPosition { Top, Middle, Bottom }
|
||||
|
||||
class PopupAdorner : Adorner {
|
||||
bool _enable_drag_move;
|
||||
IDisposable _drag_move_hook;
|
||||
IDisposable _drag_move_hook, _auto_close_hook;
|
||||
|
||||
public PopupAdorner(UIElement adornedElement, FrameworkElement popupTarget)
|
||||
: this(adornedElement, popupTarget, null, HorizontalPosition.Middle, VerticalPosition.Botton) { }
|
||||
: this(adornedElement, popupTarget, null, HorizontalPosition.Middle, VerticalPosition.Bottom) { }
|
||||
|
||||
public PopupAdorner(UIElement adornedElement, FrameworkElement popupTarget, FrameworkElement popupElement, HorizontalPosition horizontalPosition, VerticalPosition verticalPosition)
|
||||
: this(adornedElement, popupTarget, popupElement, horizontalPosition, verticalPosition, 0.0, 0.0) { }
|
||||
@@ -68,6 +72,37 @@ namespace mbts.ui.controls.adorners {
|
||||
|
||||
#endregion
|
||||
|
||||
#region StaysOpen
|
||||
public static readonly DependencyProperty StaysOpenProperty =
|
||||
Popup.StaysOpenProperty.AddOwner(typeof(PopupAdorner),
|
||||
new FrameworkPropertyMetadata(
|
||||
true,
|
||||
(d, e) => {
|
||||
var popa = ExceptionAssert.of_type<PopupAdorner>(d, nameof(d));
|
||||
Disposable.dispose(ref popa._auto_close_hook);
|
||||
if (!(bool)e.NewValue) {
|
||||
FocusManager.SetIsFocusScope(popa.AdornedElement, true);
|
||||
popa.AdornedElement.Focus();
|
||||
popa.AdornedElement.PreviewGotKeyboardFocus += on_focus_changed;
|
||||
popa.AdornedElement.GotFocus += on_focus_changed;
|
||||
popa._auto_close_hook = Disposable.Create(() => {
|
||||
popa.AdornedElement.PreviewGotKeyboardFocus -= on_focus_changed;
|
||||
popa.AdornedElement.GotFocus -= on_focus_changed;
|
||||
});
|
||||
// callbacks
|
||||
void on_focus_changed(object sender, RoutedEventArgs _e) {
|
||||
if (!popa.PopupElement.IsKeyboardFocusWithin && FocusManager.GetFocusedElement(popa.PopupElement) == null)
|
||||
popa.SetCurrentValue(IsOpenProperty, false);
|
||||
};
|
||||
}
|
||||
}));
|
||||
|
||||
public bool StaysOpen {
|
||||
get => (bool)GetValue(StaysOpenProperty);
|
||||
set => SetValue(StaysOpenProperty, value);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Opened Event
|
||||
|
||||
public static readonly RoutedEvent OpenedEvent = EventManager.RegisterRoutedEvent("Opened", RoutingStrategy.Direct, typeof(RoutedEventHandler), typeof(PopupAdorner));
|
||||
@@ -129,7 +164,7 @@ namespace mbts.ui.controls.adorners {
|
||||
switch (_verticalPosition) {
|
||||
case VerticalPosition.Top: rltv_y = -el_height; break;
|
||||
case VerticalPosition.Middle: rltv_y = -(0.5 * el_height); break;
|
||||
case VerticalPosition.Botton: rltv_y = 0; break;
|
||||
case VerticalPosition.Bottom: rltv_y = 0; break;
|
||||
default: throw new NotImplementedException();
|
||||
}
|
||||
tl = new Point(tl.X + rltv_x + _horizontalOffset, tl.Y + rltv_y + _verticalOffset);
|
||||
@@ -145,7 +180,7 @@ namespace mbts.ui.controls.adorners {
|
||||
switch (_verticalPosition) {
|
||||
case VerticalPosition.Top: rltv_y = -el_height; break;
|
||||
case VerticalPosition.Middle: rltv_y = 0.5 * (_popupTarget.ActualHeight - el_height); break;
|
||||
case VerticalPosition.Botton: rltv_y = _popupTarget.ActualHeight + el_height; break;
|
||||
case VerticalPosition.Bottom: rltv_y = _popupTarget.ActualHeight + el_height; break;
|
||||
default: throw new NotImplementedException();
|
||||
}
|
||||
tl = new Point(tl.X + rltv_x + _horizontalOffset, tl.Y + rltv_y + _verticalOffset);
|
||||
|
||||
@@ -1,56 +1,72 @@
|
||||
<ToggleButton
|
||||
x:Class="mbts.ui.controls.ExpanderToggleButton"
|
||||
x:ClassModifier="internal"
|
||||
<ResourceDictionary
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
BorderBrush="Black"
|
||||
Padding="3,0,0,0"
|
||||
xmlns:c="clr-namespace:mbts.ui.controls"
|
||||
xmlns:cv="clr-namespace:mbts.ui.converters"
|
||||
>
|
||||
<ToggleButton.Resources>
|
||||
<SolidColorBrush x:Key="Path.Static.Brush" Color="#FF333333"/>
|
||||
<SolidColorBrush x:Key="Ellipse.MouseOver.Brush" Color="#FF5593FF"/>
|
||||
<SolidColorBrush x:Key="Ellipse.Pressed.Brush" Color="#FF3C77DD"/>
|
||||
<SolidColorBrush x:Key="Ellipse.Disabled.Brush" Color="Gray"/>
|
||||
</ToggleButton.Resources>
|
||||
<ToggleButton.Template>
|
||||
<ControlTemplate TargetType="ToggleButton">
|
||||
<Grid Background="Transparent">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Ellipse x:Name="ellipse" Width="20" Height="20" Stroke="{TemplateBinding BorderBrush}"/>
|
||||
<Path
|
||||
x:Name="arrow"
|
||||
Data="M1,1.5L4.5,5 8,1.5"
|
||||
HorizontalAlignment="Center"
|
||||
SnapsToDevicePixels="False"
|
||||
Stroke="{StaticResource Path.Static.Brush}"
|
||||
StrokeThickness="2"
|
||||
VerticalAlignment="Center"
|
||||
/>
|
||||
<ContentPresenter
|
||||
Grid.Column="1"
|
||||
ContentSource="{TemplateBinding Content}"
|
||||
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
|
||||
Margin="{TemplateBinding Padding}"
|
||||
/>
|
||||
</Grid>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsChecked" Value="True">
|
||||
<Setter Property="Data" TargetName="arrow" Value="M1,4.5L4.5,1 8,4.5"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Stroke" TargetName="ellipse" Value="{StaticResource Ellipse.MouseOver.Brush}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter Property="Stroke" TargetName="ellipse" Value="{StaticResource Ellipse.Pressed.Brush}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter Property="Foreground" Value="Gray"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</ToggleButton.Template>
|
||||
</ToggleButton>
|
||||
<SolidColorBrush x:Key="Path.Static.Brush" Color="#FF333333"/>
|
||||
<SolidColorBrush x:Key="Ellipse.MouseOver.Brush" Color="#FF5593FF"/>
|
||||
<SolidColorBrush x:Key="Ellipse.Pressed.Brush" Color="#FF3C77DD"/>
|
||||
<SolidColorBrush x:Key="Ellipse.Disabled.Brush" Color="Gray"/>
|
||||
<Style TargetType="c:ExpanderToggleButton" BasedOn="{StaticResource {x:Type ToggleButton}}">
|
||||
<Setter Property="BorderBrush" Value="Black"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="Padding" Value="3,0,0,0"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ToggleButton">
|
||||
<Border>
|
||||
<Grid Background="Transparent">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Ellipse x:Name="ellipse" Width="20" Height="20" Stroke="{TemplateBinding BorderBrush}" StrokeThickness="{Binding BorderThickness, RelativeSource={RelativeSource TemplatedParent}, Converter={x:Static cv:ValueConverters.ThicknessToDouble}}"/>
|
||||
<Path
|
||||
x:Name="arrow"
|
||||
Data="M0,0L3.5,3.5 0,7"
|
||||
HorizontalAlignment="Center"
|
||||
SnapsToDevicePixels="False"
|
||||
Stroke="{StaticResource Path.Static.Brush}"
|
||||
StrokeThickness="2"
|
||||
VerticalAlignment="Center"
|
||||
/>
|
||||
<ContentPresenter
|
||||
Grid.Column="1"
|
||||
ContentSource="{TemplateBinding Content}"
|
||||
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
|
||||
Margin="{TemplateBinding Padding}"
|
||||
/>
|
||||
</Grid>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsChecked" Value="True">
|
||||
<Setter Property="Data" TargetName="arrow" Value="M0,0L3.5,3.5 7,0"/>
|
||||
</Trigger>
|
||||
<MultiDataTrigger>
|
||||
<MultiDataTrigger.Conditions>
|
||||
<Condition Binding="{Binding StrokeThickness, ElementName=ellipse}" Value="0"/>
|
||||
<Condition Binding="{Binding IsMouseOver, RelativeSource={RelativeSource Self}}" Value="True"/>
|
||||
</MultiDataTrigger.Conditions>
|
||||
<Setter TargetName="arrow" Property="Stroke" Value="{StaticResource Ellipse.MouseOver.Brush}"/>
|
||||
</MultiDataTrigger>
|
||||
<MultiDataTrigger>
|
||||
<MultiDataTrigger.Conditions>
|
||||
<Condition Binding="{Binding StrokeThickness, ElementName=ellipse, Converter={x:Static cv:ValueConverters.DoubleGreaterThanZero}}" Value="True"/>
|
||||
<Condition Binding="{Binding IsMouseOver, RelativeSource={RelativeSource Self}}" Value="True"/>
|
||||
</MultiDataTrigger.Conditions>
|
||||
<Setter TargetName="ellipse" Property="Stroke" Value="{StaticResource Ellipse.MouseOver.Brush}"/>
|
||||
</MultiDataTrigger>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter Property="Stroke" TargetName="ellipse" Value="{StaticResource Ellipse.Pressed.Brush}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter Property="Foreground" Value="Gray"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
@@ -1,11 +1,12 @@
|
||||
using System.Windows.Controls.Primitives;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls.Primitives;
|
||||
|
||||
namespace mbts.ui.controls {
|
||||
/// <summary>
|
||||
/// Interaction logic for ExpanderToggleButton.xaml
|
||||
/// </summary>
|
||||
partial class ExpanderToggleButton : ToggleButton {
|
||||
public ExpanderToggleButton() =>
|
||||
InitializeComponent();
|
||||
class ExpanderToggleButton : ToggleButton {
|
||||
static ExpanderToggleButton() =>
|
||||
DefaultStyleKeyProperty.OverrideMetadata(typeof(ExpanderToggleButton), new FrameworkPropertyMetadata(typeof(ExpanderToggleButton)));
|
||||
}
|
||||
}
|
||||
@@ -3,12 +3,48 @@
|
||||
x:ClassModifier="internal"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:u="clr-namespace:mbts.ui"
|
||||
xmlns:c="clr-namespace:mbts.ui.controls"
|
||||
xmlns:cv="clr-namespace:mbts.ui.converters"
|
||||
xmlns:cp="clr-namespace:mbts.ui.controls.color_picker"
|
||||
Background="Transparent"
|
||||
Header="{Binding SelectedColor, RelativeSource={RelativeSource Self}, Mode=OneWay}"
|
||||
ShowExpanderArrow="False"
|
||||
>
|
||||
<c:PopupControl.Background>
|
||||
<LinearGradientBrush EndPoint="0,1" StartPoint="0,0">
|
||||
<GradientStop Color="{StaticResource ComboBoxBackgroundLightColor}" Offset="0.0"/>
|
||||
<GradientStop Color="{StaticResource ComboBoxBackgroundMediumColor}" Offset="1.0"/>
|
||||
</LinearGradientBrush>
|
||||
</c:PopupControl.Background>
|
||||
<c:PopupControl.BorderBrush>
|
||||
<SolidColorBrush Color="{StaticResource ComboBoxBorderLightColor}"/>
|
||||
</c:PopupControl.BorderBrush>
|
||||
<u:StateManager.Groups>
|
||||
<u:StateGroupsManager AttachToMembers="True">
|
||||
<u:StateDefinitionCollection>
|
||||
<u:StateDefinition>
|
||||
<Binding Path="Group.Target.IsEnabled" RelativeSource="{RelativeSource Self}">
|
||||
<Binding.Converter>
|
||||
<cv:BooleanToObjectConverter ValueForFalse="Disabled" ValueForTrue="Enabled"/>
|
||||
</Binding.Converter>
|
||||
</Binding>
|
||||
</u:StateDefinition>
|
||||
</u:StateDefinitionCollection>
|
||||
</u:StateGroupsManager>
|
||||
</u:StateManager.Groups>
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup Name="EnabledStates">
|
||||
<VisualState Name="Enabled"/>
|
||||
<VisualState Name="Disabled">
|
||||
<Storyboard>
|
||||
<ColorAnimation Storyboard.TargetProperty="(c:PopupControl.Background).(LinearGradientBrush.GradientStops)[0].(GradientStop.Color)" Duration="0" To="{StaticResource ComboBoxBackgroundMediumColor}"/>
|
||||
<ColorAnimation Storyboard.TargetProperty="(c:PopupControl.Background).(LinearGradientBrush.GradientStops)[1].(GradientStop.Color)" Duration="0" To="{StaticResource ComboBoxBackgroundMediumColor}"/>
|
||||
<ColorAnimation Storyboard.TargetProperty="(c:PopupControl.BorderBrush).(SolidColorBrush.Color)" Duration="0" To="{StaticResource ComboBoxDisabledBorderColor}"/>
|
||||
<DoubleAnimation Storyboard.TargetProperty="(c:PopupControl.Opacity)" Duration="0" To="0.5"/>
|
||||
</Storyboard>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
<c:PopupControl.HeaderTemplate>
|
||||
<DataTemplate>
|
||||
<Rectangle Stroke="LightBlue" StrokeThickness="1" MinWidth="25" MinHeight="18" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
using bts.proto;
|
||||
using bts.utils;
|
||||
using mbts.spark;
|
||||
using mbts.ui.controls.color_picker;
|
||||
using mbts.ui.shell;
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
@@ -38,16 +40,42 @@ namespace mbts.ui.controls {
|
||||
class ColorPickerPopupVM : ViewModelBase, IValueFactory<SolidColorBrush> {
|
||||
readonly Property<Color> _color;
|
||||
readonly Property<double> _opacity;
|
||||
ObservableCollection<ColorPaletteVM> _palettes;
|
||||
bool _bind_opacity_to_alpha;
|
||||
IDisposable _opacity_alpha_binding;
|
||||
|
||||
public ColorPickerPopupVM() {
|
||||
_color = backing(nameof(color), Colors.Black);
|
||||
_opacity = backing(nameof(opacity), 1.0);
|
||||
palettes = new ObservableCollection<ColorPaletteVM>(ColorPalette.all_presets().Select(p => new ColorPaletteVM(p)));
|
||||
}
|
||||
|
||||
public Color color { get => _color.Value; set => _color.set(value); }
|
||||
public double opacity { get => _opacity.Value; set => _opacity.set(value); }
|
||||
public ObservableCollection<ColorPaletteVM> palettes { get; }
|
||||
public ObservableCollection<ColorPaletteVM> palettes => _palettes ?? (_palettes = new ObservableCollection<ColorPaletteVM>(ColorPalette.all_presets().Select(p => new ColorPaletteVM(p))));
|
||||
public bool bind_opacity_to_alpha {
|
||||
get => _bind_opacity_to_alpha;
|
||||
set {
|
||||
if (_bind_opacity_to_alpha != value) {
|
||||
Disposable.dispose(ref _opacity_alpha_binding);
|
||||
_bind_opacity_to_alpha = value;
|
||||
if (value) {
|
||||
_opacity.Changed += opacity_changed;
|
||||
_color.Changed += color_changed;
|
||||
color_changed(default, color);
|
||||
void opacity_changed(double oldval, double newval) {
|
||||
_color.Changed -= color_changed;
|
||||
_color.set(color.with((byte)(MathUtils.within_range(0, 1, newval) * 255.0)));
|
||||
_color.Changed += color_changed;
|
||||
}
|
||||
void color_changed(Color oldval, Color newval) {
|
||||
_opacity.Changed -= opacity_changed;
|
||||
_opacity.set(newval.A / 255.0);
|
||||
_opacity.Changed += opacity_changed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public SolidColorBrush create() => new SolidColorBrush(color) { Opacity = opacity }.frozen();
|
||||
public void load_from(SolidColorBrush brush) {
|
||||
|
||||
+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 = 30, Width = GridLength.Auto }
|
||||
});
|
||||
break;
|
||||
default:
|
||||
|
||||
@@ -4,344 +4,393 @@ using System;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using FontFamily = System.Windows.Media.FontFamily;
|
||||
|
||||
namespace mbts.ui.controls {
|
||||
partial class DIYGrid {
|
||||
public class HyperlinkCell : FrameworkElement, ICommandSource {
|
||||
// these fields each represent properties that are accessed frequently,
|
||||
// they are stored locally to reduce repetative DependencyProperty lookups
|
||||
Thickness _padding;
|
||||
HorizontalAlignment _horizontal_alignment;
|
||||
Brush _background, _foreground;
|
||||
double _glyph_width, _font_size;
|
||||
string _text;
|
||||
GlyphRun _glyph_run;
|
||||
GlyphContext _glyph_context;
|
||||
bool _is_mouse_over_text, _can_execute;
|
||||
Pen _underline_pen;
|
||||
class HyperlinkCell : FrameworkElement, ICommandSource {
|
||||
// these fields each represent properties that are accessed frequently,
|
||||
// they are stored locally to reduce repetative DependencyProperty lookups
|
||||
Thickness _padding => _gcr.padding;
|
||||
Brush _background, _foreground_backing, _border_brush_backing;
|
||||
double _glyph_width => _gcr.glyph_run.AdvanceWidths.Sum();
|
||||
GlyphRun _glyph_run => _gcr.glyph_run;
|
||||
bool _is_mouse_over_text, _can_execute;
|
||||
Pen _underline_pen_backing, _border_stroke_pen_backing;
|
||||
GlyphContentRect _gcr = new GlyphContentRect();
|
||||
|
||||
static HyperlinkCell() {
|
||||
WPFHelper.override_metadata<HorizontalAlignment, HyperlinkCell>(
|
||||
HorizontalAlignmentProperty,
|
||||
FrameworkPropertyMetadataOptions.AffectsArrange |
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(o, v) => {
|
||||
o._horizontal_alignment = v;
|
||||
o._glyph_run = null;
|
||||
}, HorizontalAlignment.Left);
|
||||
WPFHelper.override_metadata<double, HyperlinkCell>(
|
||||
WidthProperty,
|
||||
FrameworkPropertyMetadataOptions.AffectsArrange |
|
||||
FrameworkPropertyMetadataOptions.AffectsMeasure |
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(o, v) => o._glyph_run = null);
|
||||
Brush _foreground {
|
||||
get => _foreground_backing;
|
||||
set {
|
||||
_foreground_backing = value;
|
||||
_underline_pen_backing = null;
|
||||
}
|
||||
|
||||
public HyperlinkCell() {
|
||||
_background = Background;
|
||||
_foreground = Foreground;
|
||||
_horizontal_alignment = HorizontalAlignment;
|
||||
_padding = Padding;
|
||||
_font_size = FontSize;
|
||||
_text = Text;
|
||||
_glyph_context = GlyphContext.create(FontFamily, FontWeight);
|
||||
IsMouseDirectlyOverChanged += (s, e) => {
|
||||
if (_is_mouse_over_text != IsMouseDirectlyOver)
|
||||
_toggle_mouse_over_text();
|
||||
};
|
||||
}
|
||||
Brush _border_brush {
|
||||
get => _border_brush_backing;
|
||||
set {
|
||||
_border_brush_backing = value;
|
||||
_border_stroke_pen_backing = null;
|
||||
}
|
||||
}
|
||||
Thickness _border_thickness {
|
||||
get => _gcr.border_thickness;
|
||||
set {
|
||||
_gcr.border_thickness = new Thickness(value.Left);
|
||||
_border_stroke_pen_backing = null;
|
||||
}
|
||||
}
|
||||
Pen _underline_pen => _underline_pen_backing ?? (_foreground == null ? null : (_underline_pen_backing = new Pen(_foreground, 1)));
|
||||
Pen _border_stroke_pen =>
|
||||
// border_thickness.Left is used as the uniform border thickness. This can be changed if need be.
|
||||
_border_stroke_pen_backing == null && _border_brush != null && _border_thickness.Left > 0 ?
|
||||
(_border_stroke_pen_backing = _border_stroke_pen_backing = new Pen(_border_brush, _border_thickness.Left)) :
|
||||
_border_stroke_pen_backing;
|
||||
|
||||
#region Background
|
||||
public static readonly DependencyProperty BackgroundProperty =
|
||||
WPFHelper.create_dp<Brush, HyperlinkCell>(
|
||||
nameof(Background),
|
||||
static HyperlinkCell() {
|
||||
WPFHelper.override_metadata<HorizontalAlignment, HyperlinkCell>(
|
||||
HorizontalAlignmentProperty,
|
||||
FrameworkPropertyMetadataOptions.AffectsArrange |
|
||||
FrameworkPropertyMetadataOptions.AffectsMeasure |
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(o, v) => o._gcr.horizontal_alignment = v, HorizontalAlignment.Left);
|
||||
WPFHelper.override_metadata<double, HyperlinkCell>(
|
||||
WidthProperty,
|
||||
FrameworkPropertyMetadataOptions.AffectsArrange |
|
||||
FrameworkPropertyMetadataOptions.AffectsMeasure |
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(o, v) => o._gcr.width = v,
|
||||
double.NaN);
|
||||
}
|
||||
|
||||
public HyperlinkCell() {
|
||||
_gcr.min_width = MinWidth;
|
||||
_gcr.width = Width;
|
||||
_gcr.max_width = MaxWidth;
|
||||
_gcr.min_height = MinHeight;
|
||||
_gcr.height = Height;
|
||||
_gcr.max_height = Height;
|
||||
_gcr.padding = _underline_padding(Padding);
|
||||
_gcr.border_thickness = BorderThickness;
|
||||
_gcr.horizontal_alignment = HorizontalAlignment;
|
||||
_gcr.horizontal_content_alignment = HorizontalContentAlignment;
|
||||
_gcr.vertical_alignment = VerticalAlignment;
|
||||
_gcr.vertical_content_alignment = VerticalContentAlignment;
|
||||
_background = Background;
|
||||
_foreground_backing = Foreground;
|
||||
_border_brush = BorderBrush;
|
||||
_gcr.font_size = FontSize;
|
||||
if (Text is string str && !string.IsNullOrEmpty(str))
|
||||
_gcr.text_lines = new[] { str };
|
||||
_gcr.glyph_context = GlyphContext.create(FontFamily, FontWeight);
|
||||
}
|
||||
|
||||
#region BorderThickness
|
||||
public static readonly DependencyProperty BorderThicknessProperty =
|
||||
Control.BorderThicknessProperty.AddOwner(typeof(HyperlinkCell),
|
||||
new FrameworkPropertyMetadata(
|
||||
new Thickness(0),
|
||||
FrameworkPropertyMetadataOptions.AffectsMeasure |
|
||||
FrameworkPropertyMetadataOptions.AffectsArrange,
|
||||
(d, e) => ((HyperlinkCell)d)._gcr.border_thickness = (Thickness)e.NewValue));
|
||||
|
||||
public Thickness BorderThickness {
|
||||
get => (Thickness)GetValue(BorderThicknessProperty);
|
||||
set => SetValue(BorderThicknessProperty, value);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region BorderBrush
|
||||
public static readonly DependencyProperty BorderBrushProperty =
|
||||
Control.BorderBrushProperty.AddOwner(typeof(HyperlinkCell),
|
||||
new FrameworkPropertyMetadata(
|
||||
null,
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(o, v) => o._background = v,
|
||||
Brushes.Transparent);
|
||||
(d, e) => ((HyperlinkCell)d)._border_brush = e.NewValue as Brush));
|
||||
|
||||
public Brush Background { get => GetValue(BackgroundProperty) as Brush; set => SetValue(BackgroundProperty, value); }
|
||||
#endregion
|
||||
public Brush BorderBrush {
|
||||
get => (Brush)GetValue(BorderBrushProperty);
|
||||
set => SetValue(BorderBrushProperty, value);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Foreground
|
||||
public static readonly DependencyProperty ForegroundProperty =
|
||||
WPFHelper.create_dp<Brush, HyperlinkCell>(
|
||||
nameof(Foreground),
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(p, v) => {
|
||||
p._foreground = v;
|
||||
p._underline_pen = new Pen(v, 1);
|
||||
}, Brushes.Black);
|
||||
#region HorizontalContentAlignment
|
||||
public static readonly DependencyProperty HorizontalContentAlignmentProperty =
|
||||
Control.HorizontalContentAlignmentProperty.AddOwner(typeof(HyperlinkCell),
|
||||
new FrameworkPropertyMetadata(
|
||||
HorizontalAlignment.Center,
|
||||
FrameworkPropertyMetadataOptions.AffectsArrange,
|
||||
(d, e) => ((HyperlinkCell)d)._gcr.horizontal_content_alignment = (HorizontalAlignment)e.NewValue));
|
||||
|
||||
public Brush Foreground { get => (Brush)GetValue(ForegroundProperty); set => SetValue(ForegroundProperty, value); }
|
||||
#endregion
|
||||
public HorizontalAlignment HorizontalContentAlignment {
|
||||
get => (HorizontalAlignment)GetValue(HorizontalContentAlignmentProperty);
|
||||
set => SetValue(HorizontalContentAlignmentProperty, value);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region FontWeight
|
||||
public static readonly DependencyProperty FontWeightProperty =
|
||||
TextElement.FontWeightProperty.AddOwner(
|
||||
typeof(HyperlinkCell),
|
||||
new FrameworkPropertyMetadata(
|
||||
FontWeights.Normal,
|
||||
FrameworkPropertyMetadataOptions.AffectsMeasure |
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
WPFHelper.prop_changed_callback<FontWeight, HyperlinkCell>(
|
||||
(p, v) => {
|
||||
p._glyph_context = p._glyph_context.with(v);
|
||||
p._glyph_run = null;
|
||||
})));
|
||||
#region VerticalContentAlignment
|
||||
public static readonly DependencyProperty VerticalContentAlignmentProperty = Control.VerticalContentAlignmentProperty.AddOwner(typeof(HyperlinkCell),
|
||||
new FrameworkPropertyMetadata(
|
||||
VerticalAlignment.Center,
|
||||
FrameworkPropertyMetadataOptions.AffectsArrange,
|
||||
(d, e) => ((HyperlinkCell)d)._gcr.vertical_content_alignment = (VerticalAlignment)e.NewValue));
|
||||
|
||||
public FontWeight FontWeight { get => (FontWeight)GetValue(FontWeightProperty); set => SetValue(FontWeightProperty, value); }
|
||||
#endregion
|
||||
public VerticalAlignment VerticalContentAlignment {
|
||||
get => (VerticalAlignment)GetValue(VerticalContentAlignmentProperty);
|
||||
set => SetValue(VerticalContentAlignmentProperty, value);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region FontFamily
|
||||
public static readonly DependencyProperty FontFamilyProperty =
|
||||
TextElement.FontFamilyProperty.AddOwner(
|
||||
typeof(HyperlinkCell),
|
||||
new FrameworkPropertyMetadata(
|
||||
TextUtils.StandardFontFamily,
|
||||
FrameworkPropertyMetadataOptions.AffectsMeasure |
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
WPFHelper.prop_changed_callback<FontFamily, HyperlinkCell>(
|
||||
(p, v) => {
|
||||
p._glyph_context = p._glyph_context.with(v);
|
||||
p._glyph_run = null;
|
||||
})));
|
||||
#region Background
|
||||
public static readonly DependencyProperty BackgroundProperty =
|
||||
WPFHelper.create_dp<Brush, HyperlinkCell>(
|
||||
nameof(Background),
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(o, v) => o._background = v,
|
||||
Brushes.Transparent);
|
||||
|
||||
public FontFamily FontFamily { get => GetValue(FontFamilyProperty) as FontFamily; set => SetValue(FontFamilyProperty, value); }
|
||||
#endregion
|
||||
public Brush Background { get => GetValue(BackgroundProperty) as Brush; set => SetValue(BackgroundProperty, value); }
|
||||
#endregion
|
||||
|
||||
#region FontSize
|
||||
public static readonly DependencyProperty FontSizeProperty =
|
||||
TextElement.FontSizeProperty.AddOwner(
|
||||
typeof(HyperlinkCell),
|
||||
new FrameworkPropertyMetadata(
|
||||
FontSizes.Small,
|
||||
FrameworkPropertyMetadataOptions.AffectsMeasure |
|
||||
FrameworkPropertyMetadataOptions.AffectsRender |
|
||||
FrameworkPropertyMetadataOptions.Inherits,
|
||||
WPFHelper.prop_changed_callback<double, HyperlinkCell>(
|
||||
(p, v) => {
|
||||
p._font_size = v;
|
||||
p._glyph_run = null;
|
||||
})));
|
||||
#region Foreground
|
||||
public static readonly DependencyProperty ForegroundProperty =
|
||||
WPFHelper.create_dp<Brush, HyperlinkCell>(
|
||||
nameof(Foreground),
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(p, v) => p._foreground = v,
|
||||
Brushes.Black);
|
||||
|
||||
public double FontSize { get => (double)GetValue(FontSizeProperty); set => SetValue(FontSizeProperty, value); }
|
||||
#endregion
|
||||
public Brush Foreground { get => (Brush)GetValue(ForegroundProperty); set => SetValue(ForegroundProperty, value); }
|
||||
#endregion
|
||||
|
||||
#region Padding
|
||||
public static readonly DependencyProperty PaddingProperty =
|
||||
WPFHelper.create_dp<Thickness, HyperlinkCell>(
|
||||
nameof(Padding),
|
||||
#region FontWeight
|
||||
public static readonly DependencyProperty FontWeightProperty =
|
||||
TextElement.FontWeightProperty.AddOwner(
|
||||
typeof(HyperlinkCell),
|
||||
new FrameworkPropertyMetadata(
|
||||
FontWeights.Normal,
|
||||
FrameworkPropertyMetadataOptions.AffectsMeasure |
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(p, v) => {
|
||||
p._padding = v;
|
||||
p._glyph_run = null;
|
||||
}, DefaultCellPadding);
|
||||
WPFHelper.prop_changed_callback<FontWeight, HyperlinkCell>(
|
||||
(p, v) => p._gcr.glyph_context = p._gcr.glyph_context.with(v))));
|
||||
|
||||
public Thickness Padding { get => (Thickness)GetValue(PaddingProperty); set => SetValue(PaddingProperty, value); }
|
||||
#endregion
|
||||
public FontWeight FontWeight { get => (FontWeight)GetValue(FontWeightProperty); set => SetValue(FontWeightProperty, value); }
|
||||
#endregion
|
||||
|
||||
#region Text
|
||||
public static readonly DependencyProperty TextProperty =
|
||||
WPFHelper.create_dp<string, HyperlinkCell>(
|
||||
nameof(Text),
|
||||
#region FontFamily
|
||||
public static readonly DependencyProperty FontFamilyProperty =
|
||||
TextElement.FontFamilyProperty.AddOwner(
|
||||
typeof(HyperlinkCell),
|
||||
new FrameworkPropertyMetadata(
|
||||
TextUtils.StandardFontFamily,
|
||||
FrameworkPropertyMetadataOptions.AffectsMeasure |
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(o, v) => {
|
||||
o._text = v;
|
||||
o._glyph_run = null;
|
||||
}, string.Empty);
|
||||
WPFHelper.prop_changed_callback<FontFamily, HyperlinkCell>(
|
||||
(p, v) => p._gcr.glyph_context = p._gcr.glyph_context.with(v))));
|
||||
|
||||
public string Text { get => GetValue(TextProperty) as string; set => SetValue(TextProperty, value); }
|
||||
#endregion
|
||||
|
||||
#region ICommandSource
|
||||
public FontFamily FontFamily { get => GetValue(FontFamilyProperty) as FontFamily; set => SetValue(FontFamilyProperty, value); }
|
||||
#endregion
|
||||
|
||||
#region Command
|
||||
public static readonly DependencyProperty CommandProperty =
|
||||
WPFHelper.create_dp<ICommand, HyperlinkCell>(
|
||||
nameof(Command),
|
||||
(s, old_val, new_val) => {
|
||||
if (old_val != null)
|
||||
CanExecuteChangedEventManager.RemoveHandler(old_val, s.OnCanExecuteChanged);
|
||||
if (new_val != null)
|
||||
CanExecuteChangedEventManager.AddHandler(new_val, s.OnCanExecuteChanged);
|
||||
s.UpdateCanExecute();
|
||||
});
|
||||
#region FontSize
|
||||
public static readonly DependencyProperty FontSizeProperty =
|
||||
TextElement.FontSizeProperty.AddOwner(
|
||||
typeof(HyperlinkCell),
|
||||
new FrameworkPropertyMetadata(
|
||||
FontSizes.Small,
|
||||
FrameworkPropertyMetadataOptions.AffectsMeasure |
|
||||
FrameworkPropertyMetadataOptions.AffectsRender |
|
||||
FrameworkPropertyMetadataOptions.Inherits,
|
||||
WPFHelper.prop_changed_callback<double, HyperlinkCell>(
|
||||
(p, v) => p._gcr.font_size = v)));
|
||||
|
||||
[Bindable(true), Category("Action")]
|
||||
[Localizability(LocalizationCategory.NeverLocalize)]
|
||||
public ICommand Command { get => (ICommand)GetValue(CommandProperty); set => SetValue(CommandProperty, value); }
|
||||
#endregion
|
||||
public double FontSize { get => (double)GetValue(FontSizeProperty); set => SetValue(FontSizeProperty, value); }
|
||||
#endregion
|
||||
|
||||
#region CommandParameter
|
||||
public static readonly DependencyProperty CommandParameterProperty =
|
||||
DependencyProperty.Register(
|
||||
nameof(CommandParameter),
|
||||
typeof(object),
|
||||
typeof(HyperlinkCell));
|
||||
#region Padding
|
||||
public static readonly DependencyProperty PaddingProperty =
|
||||
WPFHelper.create_dp<Thickness, HyperlinkCell>(
|
||||
nameof(Padding),
|
||||
FrameworkPropertyMetadataOptions.AffectsMeasure |
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(p, v) => p._gcr.padding = _underline_padding(v), DIYGrid.DefaultCellPadding);
|
||||
|
||||
[Bindable(true), Category("Action")]
|
||||
[Localizability(LocalizationCategory.NeverLocalize)]
|
||||
public object CommandParameter { get => GetValue(CommandParameterProperty); set => SetValue(CommandParameterProperty, value); }
|
||||
#endregion
|
||||
public Thickness Padding { get => (Thickness)GetValue(PaddingProperty); set => SetValue(PaddingProperty, value); }
|
||||
#endregion
|
||||
|
||||
#region CommandTarget
|
||||
public static readonly DependencyProperty CommandTargetProperty =
|
||||
DependencyProperty.Register(
|
||||
nameof(CommandTarget),
|
||||
typeof(IInputElement),
|
||||
typeof(HyperlinkCell));
|
||||
#region Text
|
||||
public static readonly DependencyProperty TextProperty =
|
||||
WPFHelper.create_dp<string, HyperlinkCell>(
|
||||
nameof(Text),
|
||||
FrameworkPropertyMetadataOptions.AffectsMeasure |
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(p, v) => p._gcr.text_lines = string.IsNullOrEmpty(v) ? null : new[] { v }, string.Empty);
|
||||
|
||||
[Bindable(true), Category("Action")]
|
||||
public IInputElement CommandTarget { get => (IInputElement)GetValue(CommandTargetProperty); set => SetValue(CommandTargetProperty, value); }
|
||||
#endregion
|
||||
public string Text { get => GetValue(TextProperty) as string; set => SetValue(TextProperty, value); }
|
||||
#endregion
|
||||
|
||||
protected override bool IsEnabledCore => base.IsEnabledCore && CanExecute;
|
||||
#region ICommandSource
|
||||
|
||||
bool CanExecute {
|
||||
get => _can_execute;
|
||||
set {
|
||||
if (_can_execute != value) {
|
||||
_can_execute = value;
|
||||
CoerceValue(IsEnabledProperty);
|
||||
}
|
||||
#region Command
|
||||
public static readonly DependencyProperty CommandProperty =
|
||||
WPFHelper.create_dp<ICommand, HyperlinkCell>(
|
||||
nameof(Command),
|
||||
(s, old_val, new_val) => {
|
||||
if (old_val != null)
|
||||
CanExecuteChangedEventManager.RemoveHandler(old_val, s.OnCanExecuteChanged);
|
||||
if (new_val != null)
|
||||
CanExecuteChangedEventManager.AddHandler(new_val, s.OnCanExecuteChanged);
|
||||
s.UpdateCanExecute();
|
||||
});
|
||||
|
||||
[Bindable(true), Category("Action")]
|
||||
[Localizability(LocalizationCategory.NeverLocalize)]
|
||||
public ICommand Command { get => (ICommand)GetValue(CommandProperty); set => SetValue(CommandProperty, value); }
|
||||
#endregion
|
||||
|
||||
#region CommandParameter
|
||||
public static readonly DependencyProperty CommandParameterProperty =
|
||||
DependencyProperty.Register(
|
||||
nameof(CommandParameter),
|
||||
typeof(object),
|
||||
typeof(HyperlinkCell));
|
||||
|
||||
[Bindable(true), Category("Action")]
|
||||
[Localizability(LocalizationCategory.NeverLocalize)]
|
||||
public object CommandParameter { get => GetValue(CommandParameterProperty); set => SetValue(CommandParameterProperty, value); }
|
||||
#endregion
|
||||
|
||||
#region CommandTarget
|
||||
public static readonly DependencyProperty CommandTargetProperty =
|
||||
DependencyProperty.Register(
|
||||
nameof(CommandTarget),
|
||||
typeof(IInputElement),
|
||||
typeof(HyperlinkCell));
|
||||
|
||||
[Bindable(true), Category("Action")]
|
||||
public IInputElement CommandTarget { get => (IInputElement)GetValue(CommandTargetProperty); set => SetValue(CommandTargetProperty, value); }
|
||||
#endregion
|
||||
|
||||
protected override bool IsEnabledCore => base.IsEnabledCore && CanExecute;
|
||||
|
||||
bool CanExecute {
|
||||
get => _can_execute;
|
||||
set {
|
||||
if (_can_execute != value) {
|
||||
_can_execute = value;
|
||||
CoerceValue(IsEnabledProperty);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void OnCanExecuteChanged(object sender, EventArgs e) =>
|
||||
UpdateCanExecute();
|
||||
void OnCanExecuteChanged(object sender, EventArgs e) =>
|
||||
UpdateCanExecute();
|
||||
|
||||
void UpdateCanExecute() =>
|
||||
CanExecute = Command == null || CommandHelper.CanExecuteCommandSource(this);
|
||||
void UpdateCanExecute() =>
|
||||
CanExecute = Command == null || CommandHelper.CanExecuteCommandSource(this);
|
||||
|
||||
#endregion
|
||||
#endregion
|
||||
|
||||
#region Paraphrased/Simplified from https://referencesource.microsoft.com/#PresentationFramework/src/Framework/System/Windows/Documents/Hyperlink.cs
|
||||
#region Paraphrased/Simplified from https://referencesource.microsoft.com/#PresentationFramework/src/Framework/System/Windows/Documents/Hyperlink.cs
|
||||
|
||||
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e) {
|
||||
base.OnMouseLeftButtonDown(e);
|
||||
_check_mouse_over_text(e);
|
||||
if (_is_mouse_over_text) {
|
||||
// Hyperlink should take focus when left mouse button is clicked on it
|
||||
// This is consistent with all ButtonBase controls and current Win32 behavior
|
||||
Focus();
|
||||
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e) {
|
||||
base.OnMouseLeftButtonDown(e);
|
||||
_check_mouse_over_text(e);
|
||||
if (_is_mouse_over_text) {
|
||||
// Hyperlink should take focus when left mouse button is clicked on it
|
||||
// This is consistent with all ButtonBase controls and current Win32 behavior
|
||||
Focus();
|
||||
|
||||
// It is possible that the mouse state could have changed during all of
|
||||
// the call-outs that have happened so far.
|
||||
if (e.ButtonState == MouseButtonState.Pressed) {
|
||||
// Capture the mouse, and make sure we got it.
|
||||
CaptureMouse();
|
||||
}
|
||||
e.Handled = true;
|
||||
// It is possible that the mouse state could have changed during all of
|
||||
// the call-outs that have happened so far.
|
||||
if (e.ButtonState == MouseButtonState.Pressed) {
|
||||
// Capture the mouse, and make sure we got it.
|
||||
CaptureMouse();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e) {
|
||||
base.OnMouseLeftButtonUp(e);
|
||||
_check_mouse_over_text(e);
|
||||
if (IsMouseCaptured)
|
||||
ReleaseMouseCapture();
|
||||
if (_can_execute && _is_mouse_over_text)
|
||||
CommandHelper.ExecuteCommandSource(this);
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e) {
|
||||
base.OnMouseLeftButtonUp(e);
|
||||
_check_mouse_over_text(e);
|
||||
if (IsMouseCaptured)
|
||||
ReleaseMouseCapture();
|
||||
if (_can_execute && _is_mouse_over_text)
|
||||
CommandHelper.ExecuteCommandSource(this);
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
protected override void OnPreviewMouseMove(MouseEventArgs e) => _check_mouse_over_text(e);
|
||||
protected override void OnMouseEnter(MouseEventArgs e) => _check_mouse_over_text(e);
|
||||
protected override void OnMouseLeave(MouseEventArgs e) => _check_mouse_over_text(e);
|
||||
#endregion
|
||||
|
||||
protected override Size ArrangeOverride(Size finalSize) {
|
||||
if (_glyph_run == null)
|
||||
_set_glyph(finalSize);
|
||||
return finalSize;
|
||||
protected override void OnIsMouseDirectlyOverChanged(DependencyPropertyChangedEventArgs e) {
|
||||
if (_is_mouse_over_text != IsMouseDirectlyOver)
|
||||
_toggle_mouse_over_text();
|
||||
}
|
||||
protected override void OnPreviewMouseMove(MouseEventArgs e) => _check_mouse_over_text(e);
|
||||
protected override void OnMouseEnter(MouseEventArgs e) => _check_mouse_over_text(e);
|
||||
protected override void OnMouseLeave(MouseEventArgs e) => _check_mouse_over_text(e);
|
||||
|
||||
protected override Size ArrangeOverride(Size finalSize) => finalSize;
|
||||
|
||||
protected override Size MeasureOverride(Size availableSize) {
|
||||
_gcr.available_size = availableSize;
|
||||
if (double.IsInfinity(_gcr.layout_size.Height) || double.IsInfinity(_gcr.layout_size.Width)) {
|
||||
_gcr.remeasure();
|
||||
}
|
||||
return _gcr.layout_size;
|
||||
}
|
||||
|
||||
protected override Size MeasureOverride(Size availableSize) {
|
||||
if (_glyph_run == null)
|
||||
_set_glyph(availableSize);
|
||||
return new Size(Math.Max(availableSize.Width, _padding.Left + _glyph_width + _padding.Right),
|
||||
Math.Max(availableSize.Height, _padding.Top + _font_size + _padding.Bottom));
|
||||
protected override void OnRender(DrawingContext drawingContext) {
|
||||
var rs = RenderSize;
|
||||
var rect = new Rect(0, 0, rs.Width, rs.Height);
|
||||
var bg = Background;
|
||||
if (bg != null) {
|
||||
drawingContext.DrawRectangle(bg, _border_stroke_pen, rect);
|
||||
}
|
||||
|
||||
void _set_glyph(Size size) {
|
||||
_glyph_run = _glyph_context.get_glyph_run(_text, _font_size, size, _horizontal_alignment, VerticalAlignment.Center, _padding);
|
||||
_glyph_width = _glyph_run == null ? 0 : _glyph_run.AdvanceWidths.Sum();
|
||||
}
|
||||
|
||||
protected override void OnRender(DrawingContext drawingContext) {
|
||||
var rs = RenderSize;
|
||||
var rect = new Rect(0, 0, rs.Width, rs.Height);
|
||||
var bg = Background;
|
||||
if (bg != null)
|
||||
drawingContext.DrawRectangle(bg, null, rect);
|
||||
if (_glyph_run != null) {
|
||||
drawingContext.PushClip(new RectangleGeometry(rect));
|
||||
drawingContext.DrawGlyphRun(_foreground, _glyph_run);
|
||||
if (_is_mouse_over_text) {
|
||||
// draw underline
|
||||
Point pt;
|
||||
switch (_horizontal_alignment) {
|
||||
case HorizontalAlignment.Left:
|
||||
pt = new Point(Padding.Left, 0.5 * (ActualHeight + _font_size) + 1);
|
||||
break;
|
||||
case HorizontalAlignment.Right:
|
||||
pt = new Point(ActualWidth - Padding.Right - _glyph_width, 0.5 * (ActualHeight + _font_size) + 1);
|
||||
break;
|
||||
case HorizontalAlignment.Center:
|
||||
default:
|
||||
pt = new Point(0.5 * (ActualWidth - _glyph_width), 0.5 * (ActualHeight + _font_size) + 1);
|
||||
break;
|
||||
}
|
||||
drawingContext.DrawLine(_underline_pen, pt, new Point(pt.X + _glyph_width, pt.Y));
|
||||
|
||||
}
|
||||
drawingContext.Pop();
|
||||
}
|
||||
if (bg != null) {
|
||||
var bgo = GetBackgroundOverlay(this);
|
||||
if (bgo != null)
|
||||
drawingContext.DrawRectangle(bgo, null, rect);
|
||||
if (_glyph_run != null) {
|
||||
drawingContext.PushClip(new RectangleGeometry(rect));
|
||||
drawingContext.DrawGlyphRun(_foreground, _glyph_run);
|
||||
if (_is_mouse_over_text) {
|
||||
// draw underline
|
||||
double x = _gcr.glyph_run.BaselineOrigin.X;
|
||||
double y = _gcr.glyph_run.BaselineOrigin.Y + 2;
|
||||
drawingContext.DrawLine(_underline_pen, new Point(x, y), new Point(x + _gcr.glyph_run.AdvanceWidths.Sum(), y));
|
||||
}
|
||||
drawingContext.Pop();
|
||||
}
|
||||
if (bg != null) {
|
||||
var bgo = DIYGrid.GetBackgroundOverlay(this);
|
||||
if (bgo != null)
|
||||
drawingContext.DrawRectangle(bgo, null, rect);
|
||||
}
|
||||
}
|
||||
|
||||
bool _point_horizontally_over_text(double x) {
|
||||
switch (_horizontal_alignment) {
|
||||
case HorizontalAlignment.Left:
|
||||
return x >= _padding.Left && x <= _padding.Left + _glyph_width;
|
||||
case HorizontalAlignment.Right:
|
||||
return x >= ActualWidth - Padding.Right - _glyph_width && x <= ActualWidth - _padding.Right;
|
||||
case HorizontalAlignment.Center:
|
||||
default:
|
||||
return x >= 0.5 * (ActualWidth - _glyph_width) && x <= 0.5 * (ActualWidth - _glyph_width) + _glyph_width;
|
||||
}
|
||||
}
|
||||
static Thickness _underline_padding(Thickness padding) =>
|
||||
new Thickness(padding.Left, padding.Top, padding.Right, padding.Bottom + 2);
|
||||
|
||||
void _check_mouse_over_text(MouseEventArgs e) {
|
||||
var pt = e.GetPosition(this);
|
||||
bool over_text =
|
||||
_glyph_run != null &&
|
||||
_point_horizontally_over_text(pt.X) &&
|
||||
pt.Y >= _padding.Top &&
|
||||
pt.Y <= _padding.Top + _font_size;
|
||||
if (_is_mouse_over_text != over_text)
|
||||
_toggle_mouse_over_text();
|
||||
void _check_mouse_over_text(MouseEventArgs e) {
|
||||
var pt = e.GetPosition(this);
|
||||
var orig = _gcr.content_area;
|
||||
var exp = orig.Expanded(1);
|
||||
if (_is_mouse_over_text != exp.Contains(pt)) {
|
||||
bts.Log.assert(_is_mouse_over_text != _gcr.content_area.Expanded(1).Contains(e.GetPosition(this)));
|
||||
_toggle_mouse_over_text();
|
||||
return;
|
||||
}
|
||||
if (_is_mouse_over_text != _gcr.content_area.Expanded(1).Contains(e.GetPosition(this)))
|
||||
_toggle_mouse_over_text();
|
||||
}
|
||||
|
||||
void _toggle_mouse_over_text() {
|
||||
_is_mouse_over_text = !_is_mouse_over_text;
|
||||
if (_is_mouse_over_text)
|
||||
SetCurrentValue(CursorProperty, Cursors.Hand);
|
||||
else
|
||||
ClearValue(CursorProperty);
|
||||
InvalidateVisual();
|
||||
}
|
||||
void _toggle_mouse_over_text() {
|
||||
_is_mouse_over_text = !_is_mouse_over_text;
|
||||
if (_is_mouse_over_text)
|
||||
SetCurrentValue(CursorProperty, Cursors.Hand);
|
||||
else
|
||||
ClearValue(CursorProperty);
|
||||
InvalidateVisual();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,178 +8,263 @@ using System.Windows.Media;
|
||||
using FontFamily = System.Windows.Media.FontFamily;
|
||||
|
||||
namespace mbts.ui.controls {
|
||||
partial class DIYGrid : Control {
|
||||
public class TextCell : FrameworkElement {
|
||||
// these fields each represent properties that are accessed frequently,
|
||||
// they are stored locally to reduce repetative DependencyProperty lookups
|
||||
Thickness _padding;
|
||||
HorizontalAlignment _horizontal_alignment;
|
||||
Brush _background, _foreground;
|
||||
double _glyph_width, _font_size;
|
||||
string _text;
|
||||
GlyphRun _glyph_run;
|
||||
GlyphContext _glyph_context;
|
||||
class TextCell : FrameworkElement {
|
||||
// these fields each represent properties that are accessed frequently,
|
||||
// they are stored locally to reduce repetative DependencyProperty lookups
|
||||
Thickness _padding => _gcr.padding;
|
||||
Brush _background, _foreground, _border_brush_backing;
|
||||
double _glyph_width => _gcr.glyph_run.AdvanceWidths.Sum();
|
||||
GlyphRun _glyph_run => _gcr.glyph_run;
|
||||
Pen _border_stroke_pen_backing;
|
||||
GlyphContentRect _gcr = new GlyphContentRect();
|
||||
|
||||
static TextCell() {
|
||||
WPFHelper.override_metadata<HorizontalAlignment, TextCell>(
|
||||
HorizontalAlignmentProperty,
|
||||
FrameworkPropertyMetadataOptions.AffectsArrange |
|
||||
Brush _border_brush {
|
||||
get => _border_brush_backing;
|
||||
set {
|
||||
_border_brush_backing = value;
|
||||
_border_stroke_pen_backing = null;
|
||||
}
|
||||
}
|
||||
Thickness _border_thickness {
|
||||
get => _gcr.border_thickness;
|
||||
set {
|
||||
_gcr.border_thickness = new Thickness(value.Left);
|
||||
_border_stroke_pen_backing = null;
|
||||
}
|
||||
}
|
||||
Pen _border_stroke_pen =>
|
||||
// border_thickness.Left is used as the uniform border thickness. This can be changed if need be.
|
||||
_border_stroke_pen_backing == null && _border_brush != null && _border_thickness.Left > 0 ?
|
||||
(_border_stroke_pen_backing = _border_stroke_pen_backing = new Pen(_border_brush, _border_thickness.Left)) :
|
||||
_border_stroke_pen_backing;
|
||||
|
||||
static TextCell() {
|
||||
WPFHelper.override_metadata<HorizontalAlignment, TextCell>(
|
||||
HorizontalAlignmentProperty,
|
||||
FrameworkPropertyMetadataOptions.AffectsArrange |
|
||||
FrameworkPropertyMetadataOptions.AffectsMeasure |
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(o, v) => o._gcr.horizontal_alignment = v, HorizontalAlignment.Left);
|
||||
WPFHelper.override_metadata<double, TextCell>(
|
||||
WidthProperty,
|
||||
FrameworkPropertyMetadataOptions.AffectsArrange |
|
||||
FrameworkPropertyMetadataOptions.AffectsMeasure |
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(o, v) => o._gcr.width = v,
|
||||
double.NaN);
|
||||
}
|
||||
|
||||
public TextCell() {
|
||||
_gcr.min_width = MinWidth;
|
||||
_gcr.width = Width;
|
||||
_gcr.max_width = MaxWidth;
|
||||
_gcr.min_height = MinHeight;
|
||||
_gcr.height = Height;
|
||||
_gcr.max_height = Height;
|
||||
_gcr.padding = Padding;
|
||||
_gcr.border_thickness = BorderThickness;
|
||||
_gcr.horizontal_alignment = HorizontalAlignment;
|
||||
_gcr.horizontal_content_alignment = HorizontalContentAlignment;
|
||||
_gcr.vertical_alignment = VerticalAlignment;
|
||||
_gcr.vertical_content_alignment = VerticalContentAlignment;
|
||||
_background = Background;
|
||||
_foreground = Foreground;
|
||||
_border_brush = BorderBrush;
|
||||
_gcr.font_size = FontSize;
|
||||
if (Text is string str && !string.IsNullOrEmpty(str))
|
||||
_gcr.text_lines = new[] { str };
|
||||
_gcr.glyph_context = GlyphContext.create(FontFamily, FontWeight);
|
||||
}
|
||||
|
||||
#region BorderThickness
|
||||
public static readonly DependencyProperty BorderThicknessProperty =
|
||||
Control.BorderThicknessProperty.AddOwner(typeof(TextCell),
|
||||
new FrameworkPropertyMetadata(
|
||||
new Thickness(0),
|
||||
FrameworkPropertyMetadataOptions.AffectsMeasure |
|
||||
FrameworkPropertyMetadataOptions.AffectsArrange,
|
||||
(d, e) => ((TextCell)d)._gcr.border_thickness = (Thickness)e.NewValue));
|
||||
|
||||
public Thickness BorderThickness {
|
||||
get => (Thickness)GetValue(BorderThicknessProperty);
|
||||
set => SetValue(BorderThicknessProperty, value);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region BorderBrush
|
||||
public static readonly DependencyProperty BorderBrushProperty =
|
||||
Control.BorderBrushProperty.AddOwner(typeof(TextCell),
|
||||
new FrameworkPropertyMetadata(
|
||||
null,
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(d, e) => ((TextCell)d)._border_brush = e.NewValue as Brush));
|
||||
|
||||
public Brush BorderBrush {
|
||||
get => (Brush)GetValue(BorderBrushProperty);
|
||||
set => SetValue(BorderBrushProperty, value);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region HorizontalContentAlignment
|
||||
public static readonly DependencyProperty HorizontalContentAlignmentProperty =
|
||||
Control.HorizontalContentAlignmentProperty.AddOwner(typeof(TextCell),
|
||||
new FrameworkPropertyMetadata(
|
||||
HorizontalAlignment.Center,
|
||||
FrameworkPropertyMetadataOptions.AffectsArrange,
|
||||
(d, e) => {
|
||||
if (e.OldValue is HorizontalAlignment oldha) {
|
||||
if (oldha == HorizontalAlignment.Right) {
|
||||
|
||||
}
|
||||
}
|
||||
((TextCell)d)._gcr.horizontal_content_alignment = (HorizontalAlignment)e.NewValue;
|
||||
}));
|
||||
|
||||
public HorizontalAlignment HorizontalContentAlignment {
|
||||
get => (HorizontalAlignment)GetValue(HorizontalContentAlignmentProperty);
|
||||
set => SetValue(HorizontalContentAlignmentProperty, value);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region VerticalContentAlignment
|
||||
public static readonly DependencyProperty VerticalContentAlignmentProperty = Control.VerticalContentAlignmentProperty.AddOwner(typeof(TextCell),
|
||||
new FrameworkPropertyMetadata(
|
||||
VerticalAlignment.Center,
|
||||
FrameworkPropertyMetadataOptions.AffectsArrange,
|
||||
(d, e) => ((TextCell)d)._gcr.vertical_content_alignment = (VerticalAlignment)e.NewValue));
|
||||
|
||||
public VerticalAlignment VerticalContentAlignment {
|
||||
get => (VerticalAlignment)GetValue(VerticalContentAlignmentProperty);
|
||||
set => SetValue(VerticalContentAlignmentProperty, value);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Background
|
||||
public static readonly DependencyProperty BackgroundProperty =
|
||||
WPFHelper.create_dp<Brush, TextCell>(
|
||||
nameof(Background),
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(o, v) => o._background = v,
|
||||
Brushes.Transparent);
|
||||
|
||||
public Brush Background { get => GetValue(BackgroundProperty) as Brush; set => SetValue(BackgroundProperty, value); }
|
||||
#endregion
|
||||
|
||||
#region Foreground
|
||||
public static readonly DependencyProperty ForegroundProperty =
|
||||
WPFHelper.create_dp<Brush, TextCell>(
|
||||
nameof(Foreground),
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(p, v) => p._foreground = v,
|
||||
Brushes.Black);
|
||||
|
||||
public Brush Foreground { get => (Brush)GetValue(ForegroundProperty); set => SetValue(ForegroundProperty, value); }
|
||||
#endregion
|
||||
|
||||
#region FontWeight
|
||||
public static readonly DependencyProperty FontWeightProperty =
|
||||
TextElement.FontWeightProperty.AddOwner(
|
||||
typeof(TextCell),
|
||||
new FrameworkPropertyMetadata(
|
||||
FontWeights.Normal,
|
||||
FrameworkPropertyMetadataOptions.AffectsMeasure |
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(o, v) => {
|
||||
o._horizontal_alignment = v;
|
||||
o._glyph_run = null;
|
||||
},
|
||||
HorizontalAlignment.Left);
|
||||
WPFHelper.override_metadata<double, TextCell>(
|
||||
WidthProperty,
|
||||
FrameworkPropertyMetadataOptions.AffectsArrange |
|
||||
WPFHelper.prop_changed_callback<FontWeight, TextCell>(
|
||||
(p, v) => p._gcr.glyph_context = p._gcr.glyph_context.with(v))));
|
||||
|
||||
public FontWeight FontWeight { get => (FontWeight)GetValue(FontWeightProperty); set => SetValue(FontWeightProperty, value); }
|
||||
#endregion
|
||||
|
||||
#region FontFamily
|
||||
public static readonly DependencyProperty FontFamilyProperty =
|
||||
TextElement.FontFamilyProperty.AddOwner(
|
||||
typeof(TextCell),
|
||||
new FrameworkPropertyMetadata(
|
||||
TextUtils.StandardFontFamily,
|
||||
FrameworkPropertyMetadataOptions.AffectsMeasure |
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(o, v) => o._glyph_run = null,
|
||||
double.NaN);
|
||||
}
|
||||
WPFHelper.prop_changed_callback<FontFamily, TextCell>(
|
||||
(p, v) => p._gcr.glyph_context = p._gcr.glyph_context.with(v))));
|
||||
|
||||
public TextCell() {
|
||||
_background = Background;
|
||||
_foreground = Foreground;
|
||||
_horizontal_alignment = HorizontalAlignment;
|
||||
_padding = Padding;
|
||||
_font_size = FontSize;
|
||||
_text = Text;
|
||||
_glyph_context = GlyphContext.create(FontFamily, FontWeight);
|
||||
}
|
||||
public FontFamily FontFamily { get => GetValue(FontFamilyProperty) as FontFamily; set => SetValue(FontFamilyProperty, value); }
|
||||
#endregion
|
||||
|
||||
#region Background
|
||||
public static readonly DependencyProperty BackgroundProperty =
|
||||
WPFHelper.create_dp<Brush, TextCell>(
|
||||
nameof(Background),
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(p, v) => p._background = v,
|
||||
null);
|
||||
|
||||
public Brush Background { get => (Brush)GetValue(BackgroundProperty); set => SetValue(BackgroundProperty, value); }
|
||||
#endregion
|
||||
|
||||
#region Foreground
|
||||
public static readonly DependencyProperty ForegroundProperty =
|
||||
WPFHelper.create_dp<Brush, TextCell>(
|
||||
nameof(Foreground),
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(p, v) => p._foreground = v,
|
||||
Brushes.Black);
|
||||
|
||||
public Brush Foreground { get => (Brush)GetValue(ForegroundProperty); set => SetValue(ForegroundProperty, value); }
|
||||
#endregion
|
||||
|
||||
#region FontFamily
|
||||
public static readonly DependencyProperty FontFamilyProperty =
|
||||
TextElement.FontFamilyProperty.AddOwner(
|
||||
typeof(TextCell),
|
||||
new FrameworkPropertyMetadata(
|
||||
TextUtils.StandardFontFamily,
|
||||
FrameworkPropertyMetadataOptions.AffectsMeasure |
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
WPFHelper.prop_changed_callback<FontFamily, TextCell>(
|
||||
(p, v) => {
|
||||
p._glyph_context = p._glyph_context.with(v);
|
||||
p._glyph_run = null;
|
||||
})));
|
||||
|
||||
public FontFamily FontFamily { get => GetValue(FontFamilyProperty) as FontFamily; set => SetValue(FontFamilyProperty, value); }
|
||||
#endregion
|
||||
|
||||
#region FontWeight
|
||||
public static readonly DependencyProperty FontWeightProperty =
|
||||
TextElement.FontWeightProperty.AddOwner(
|
||||
typeof(TextCell),
|
||||
new FrameworkPropertyMetadata(
|
||||
FontWeights.Normal,
|
||||
FrameworkPropertyMetadataOptions.AffectsMeasure |
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
WPFHelper.prop_changed_callback<FontWeight, TextCell>(
|
||||
(p, v) => {
|
||||
p._glyph_context = p._glyph_context.with(v);
|
||||
p._glyph_run = null;
|
||||
})));
|
||||
|
||||
public FontWeight FontWeight { get => (FontWeight)GetValue(FontWeightProperty); set => SetValue(FontWeightProperty, value); }
|
||||
#endregion
|
||||
|
||||
#region FontSize
|
||||
public static readonly DependencyProperty FontSizeProperty =
|
||||
TextElement.FontSizeProperty.AddOwner(
|
||||
typeof(TextCell),
|
||||
new FrameworkPropertyMetadata(
|
||||
FontSizes.Small,
|
||||
FrameworkPropertyMetadataOptions.AffectsMeasure |
|
||||
FrameworkPropertyMetadataOptions.AffectsRender |
|
||||
FrameworkPropertyMetadataOptions.Inherits,
|
||||
WPFHelper.prop_changed_callback<double, TextCell>(
|
||||
(p, v) => {
|
||||
p._font_size = v;
|
||||
p._glyph_run = null;
|
||||
})));
|
||||
|
||||
public double FontSize { get => (double)GetValue(FontSizeProperty); set => SetValue(FontSizeProperty, value); }
|
||||
#endregion
|
||||
|
||||
#region Padding
|
||||
public static readonly DependencyProperty PaddingProperty =
|
||||
WPFHelper.create_dp<Thickness, TextCell>(
|
||||
nameof(Padding),
|
||||
#region FontSize
|
||||
public static readonly DependencyProperty FontSizeProperty =
|
||||
TextElement.FontSizeProperty.AddOwner(
|
||||
typeof(TextCell),
|
||||
new FrameworkPropertyMetadata(
|
||||
FontSizes.Small,
|
||||
FrameworkPropertyMetadataOptions.AffectsMeasure |
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(p, v) => {
|
||||
p._padding = v;
|
||||
p._glyph_run = null;
|
||||
}, DefaultCellPadding);
|
||||
FrameworkPropertyMetadataOptions.AffectsRender |
|
||||
FrameworkPropertyMetadataOptions.Inherits,
|
||||
WPFHelper.prop_changed_callback<double, TextCell>(
|
||||
(p, v) => p._gcr.font_size = v)));
|
||||
|
||||
public Thickness Padding { get => (Thickness)GetValue(PaddingProperty); set => SetValue(PaddingProperty, value); }
|
||||
#endregion
|
||||
public double FontSize { get => (double)GetValue(FontSizeProperty); set => SetValue(FontSizeProperty, value); }
|
||||
#endregion
|
||||
|
||||
#region Text
|
||||
public static readonly DependencyProperty TextProperty =
|
||||
WPFHelper.create_dp<string, TextCell>(
|
||||
nameof(Text),
|
||||
FrameworkPropertyMetadataOptions.AffectsMeasure |
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(p, v) => {
|
||||
p._text = v;
|
||||
p._glyph_run = null;
|
||||
});
|
||||
#region Padding
|
||||
public static readonly DependencyProperty PaddingProperty =
|
||||
WPFHelper.create_dp<Thickness, TextCell>(
|
||||
nameof(Padding),
|
||||
FrameworkPropertyMetadataOptions.AffectsMeasure |
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(p, v) => p._gcr.padding = v,
|
||||
DIYGrid.DefaultCellPadding);
|
||||
|
||||
public string Text { get => GetValue(TextProperty) as string; set => SetValue(TextProperty, value); }
|
||||
#endregion
|
||||
public Thickness Padding { get => (Thickness)GetValue(PaddingProperty); set => SetValue(PaddingProperty, value); }
|
||||
#endregion
|
||||
|
||||
protected override Size ArrangeOverride(Size finalSize) {
|
||||
if (_glyph_run == null)
|
||||
_set_glyph(finalSize);
|
||||
return finalSize;
|
||||
#region Text
|
||||
public static readonly DependencyProperty TextProperty =
|
||||
WPFHelper.create_dp<string, TextCell>(
|
||||
nameof(Text),
|
||||
FrameworkPropertyMetadataOptions.AffectsMeasure |
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(p, v) => p._gcr.text_lines = string.IsNullOrEmpty(v) ? null : new[] { v }, string.Empty);
|
||||
|
||||
public string Text { get => GetValue(TextProperty) as string; set => SetValue(TextProperty, value); }
|
||||
#endregion
|
||||
|
||||
protected override Size ArrangeOverride(Size finalSize) => finalSize;
|
||||
|
||||
protected override Size MeasureOverride(Size availableSize) {
|
||||
_gcr.available_size = availableSize;
|
||||
if (double.IsInfinity(_gcr.layout_size.Height) || double.IsInfinity(_gcr.layout_size.Width)) {
|
||||
_gcr.remeasure();
|
||||
}
|
||||
return _gcr.layout_size;
|
||||
}
|
||||
|
||||
protected override Size MeasureOverride(Size availableSize) {
|
||||
if (_glyph_run == null)
|
||||
_set_glyph(availableSize);
|
||||
return new Size(Math.Max(availableSize.Width, _padding.Left + _glyph_width + _padding.Right),
|
||||
Math.Max(availableSize.Height, _padding.Top + _font_size + _padding.Bottom));
|
||||
double bl_x = 0;
|
||||
protected override void OnRender(DrawingContext drawingContext) {
|
||||
var rs = RenderSize;
|
||||
var rect = new Rect(0, 0, rs.Width, rs.Height);
|
||||
var bg = Background;
|
||||
if (bg != null) {
|
||||
drawingContext.DrawRectangle(bg, _border_stroke_pen, rect);
|
||||
}
|
||||
if (_glyph_run != null) {
|
||||
var new_x = _glyph_run.BaselineOrigin.X;
|
||||
if (Math.Abs(bl_x - new_x) > 5) {
|
||||
|
||||
void _set_glyph(Size size) {
|
||||
_glyph_run = _glyph_context.get_glyph_run(_text, _font_size, size, _horizontal_alignment, VerticalAlignment.Center, _padding);
|
||||
_glyph_width = _glyph_run == null ? 0 : _glyph_run.AdvanceWidths.Sum();
|
||||
}
|
||||
if (new_x < bl_x) {
|
||||
|
||||
protected override void OnRender(DrawingContext drawingContext) {
|
||||
var rs = RenderSize;
|
||||
var rect = new Rect(0, 0, rs.Width, rs.Height);
|
||||
if (_background != null)
|
||||
drawingContext.DrawRectangle(_background, null, rect);
|
||||
if (_glyph_run != null) {
|
||||
drawingContext.PushClip(new RectangleGeometry(rect));
|
||||
drawingContext.DrawGlyphRun(_foreground, _glyph_run);
|
||||
drawingContext.Pop();
|
||||
}
|
||||
}
|
||||
if (Text == "-3,375.000") {
|
||||
|
||||
}
|
||||
bl_x = new_x;
|
||||
drawingContext.PushClip(new RectangleGeometry(rect));
|
||||
drawingContext.DrawGlyphRun(_foreground, _glyph_run);
|
||||
drawingContext.Pop();
|
||||
}
|
||||
if (bg != null) {
|
||||
var bgo = DIYGrid.GetBackgroundOverlay(this);
|
||||
if (bgo != null)
|
||||
drawingContext.DrawRectangle(bgo, null, rect);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
using mbts.spark;
|
||||
using bts;
|
||||
using mbts.spark;
|
||||
using mbts.spark.filters;
|
||||
using mbts.ui.settings;
|
||||
using mbts.ui.shell;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace mbts.ui.controls {
|
||||
namespace mbts.ui.controls {
|
||||
// Column types are nested inside the DIYGrid
|
||||
partial class DIYGrid {
|
||||
public static class DefaultColumnWidths {
|
||||
@@ -53,13 +54,28 @@ namespace mbts.ui.controls {
|
||||
|
||||
public static class MiniOrderBook {
|
||||
public const double
|
||||
Cancel = 37,
|
||||
Cancel = 41,
|
||||
AttachDetach = 45,
|
||||
Update = 45,
|
||||
EditableSize = 40,
|
||||
EditablePrice = 64,
|
||||
Commit = 20;
|
||||
}
|
||||
|
||||
public static class PNL {
|
||||
public const double
|
||||
// 'PNL Account' is a bit wider than normal Account cols, bc it is a 'grouping' column, so it has additional content/icons
|
||||
Account = 100,
|
||||
Exchange = 85,
|
||||
Symbol = 72,
|
||||
Security = 87,
|
||||
Qty = 42,
|
||||
Price = 64,
|
||||
PriceBig = 92,
|
||||
SecurityType = 64,
|
||||
WorkingQty = 54,
|
||||
HasDayTrades = 74;
|
||||
}
|
||||
}
|
||||
|
||||
public static class ShortColumnWidths {
|
||||
@@ -100,6 +116,7 @@ namespace mbts.ui.controls {
|
||||
Working = "Working",
|
||||
Bid = "Bid",
|
||||
Ask = "Ask",
|
||||
Mid = "Mid",
|
||||
Settlement = "Settlement",
|
||||
DayChange = "Change",
|
||||
SessionLow = "Low",
|
||||
@@ -109,8 +126,8 @@ namespace mbts.ui.controls {
|
||||
Description = "Description",
|
||||
N_Legs = "# Legs",
|
||||
N_Working_Orders = "# Working Orders",
|
||||
StartNetPos = "Start Net Pos",
|
||||
StartPnL = "Start PnL",
|
||||
StartNetPos = "Open Pos",
|
||||
StartPnL = "Open PnL",
|
||||
DayLong = "Day Long",
|
||||
DayShort = "Day Short",
|
||||
DayPnL = "Day PnL",
|
||||
@@ -129,6 +146,41 @@ namespace mbts.ui.controls {
|
||||
PositionInQueue = "PIQ",
|
||||
RootAlgoType = "Root Algo"
|
||||
;
|
||||
|
||||
public static class PNLHeaders {
|
||||
public const string
|
||||
PNL = "PnL",
|
||||
Long = "Long",
|
||||
Short = "Short",
|
||||
Net = "Pos",
|
||||
AvgPrice = "Avg",
|
||||
AvgPriceDelta = "Avg Delta",
|
||||
HasDayTrades = "Traded Today",
|
||||
SecurityType = "Type";
|
||||
|
||||
public static readonly Part
|
||||
SOD = new Part("Open"),
|
||||
Day = new Part("Day"),
|
||||
Total = new Part("Total"),
|
||||
Working = new Part("Working");
|
||||
|
||||
public readonly struct Part {
|
||||
public readonly string prefix;
|
||||
|
||||
public Part(string prefix) =>
|
||||
this.prefix = prefix;
|
||||
|
||||
public string PNL => $"{prefix} PNL";
|
||||
public string Net => $"{prefix} {PNLHeaders.Net}";
|
||||
public string NetAvgDelta => $"{prefix} Net {AvgPriceDelta}";
|
||||
public string Long => $"{prefix} {PNLHeaders.Long}";
|
||||
public string LongAvg => $"{Long} {AvgPrice}";
|
||||
public string LongAvgDelta => $"{Long} {AvgPriceDelta}";
|
||||
public string Short => $"{prefix} {PNLHeaders.Short}";
|
||||
public string ShortAvg => $"{Short} {AvgPrice}";
|
||||
public string ShortAvgDelta => $"{Short} {AvgPriceDelta}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public interface IKeyedColumn<TColKey> {
|
||||
@@ -171,6 +223,7 @@ namespace mbts.ui.controls {
|
||||
// To include extra data with your column type you are encouraged to derive from DIYGridColumn
|
||||
public abstract class ColumnBase : GridViewColumn, IValueFactory<GridColumnSettings>, INotifyPropertyChanged {
|
||||
//readonly ExternalProperty<FontWeight?> _font_weigh
|
||||
GridColumnSettings _settings;
|
||||
FontWeight _fallback_font_weight;
|
||||
Color _fallback_font_color;
|
||||
double _fallback_font_size;
|
||||
@@ -178,6 +231,7 @@ namespace mbts.ui.controls {
|
||||
FontWeight? _font_weight_override;
|
||||
Color? _font_color_override;
|
||||
object _header_copy;
|
||||
bool _is_visible;
|
||||
|
||||
protected ColumnBase() {
|
||||
var gs = GridSettings.Default;
|
||||
@@ -199,13 +253,23 @@ namespace mbts.ui.controls {
|
||||
|
||||
public event Action<ColumnBase, ListSortDirection?> sort_direction_changed;
|
||||
public event Action font_params_changed;
|
||||
public event Action<bool> is_visible_changed;
|
||||
|
||||
public string id { get; set; }
|
||||
public string sort_member_path { get; set; }
|
||||
public PropertyPath data_member_path { get; set; }
|
||||
public double original_width { get; set; }
|
||||
public bool is_collapsed { get; set; }
|
||||
// TODO: hook up visibility
|
||||
public bool is_visible { get; set; } = true;
|
||||
public bool is_visible {
|
||||
get => _is_visible;
|
||||
set {
|
||||
if (_is_visible != value) {
|
||||
_is_visible = value;
|
||||
is_visible_changed?.Invoke(value);
|
||||
raise_prop_changed();
|
||||
}
|
||||
}
|
||||
}
|
||||
public FontWeight? font_weight_override { get => _font_weight_override; set => _set_font_param(ref _font_weight_override, value); }
|
||||
public Color? font_color_override { get => _font_color_override; set => _set_font_param(ref _font_color_override, value); }
|
||||
public HorizontalAlignment horizontal_alignment { get; set; }
|
||||
@@ -215,6 +279,7 @@ namespace mbts.ui.controls {
|
||||
public FontWeight font_weight => font_weight_override ?? _fallback_font_weight;
|
||||
public Color font_color => font_color_override ?? _fallback_font_color;
|
||||
public Brush font_brush => new SolidColorBrush(font_color);
|
||||
public GridColumnSettings settings => _settings ?? (_settings = create());
|
||||
// TODO: find a better way to store the header-property in a thread-safe manner - a value for GridSettings.display_name, more specifically.
|
||||
// Since save_state is called from a different thread in IShared/SlowWindows, it is not safe to access the DependencyProperty store from that method.
|
||||
// By updating a local _header_copy field in the Header setter, the DependencyProperty lookup may be bypassed when appropriate (see create method below).
|
||||
@@ -223,6 +288,8 @@ namespace mbts.ui.controls {
|
||||
set => base.Header = (_header_copy = value);
|
||||
}
|
||||
|
||||
/// important: don't access any dependency props in here.
|
||||
/// this may be called from a thread other than that of this dispatcher (i.e., when <see cref="IShellWindow.GetState(System.IO.Stream)"/> is called).
|
||||
public GridColumnSettings create() =>
|
||||
new GridColumnSettings(id, _header_copy?.ToString(), is_visible, is_collapsed, font_weight_override, font_color_override, ActualWidth, horizontal_alignment, horizontal_offset);
|
||||
|
||||
@@ -234,7 +301,8 @@ namespace mbts.ui.controls {
|
||||
public virtual void load_from(GridColumnSettings settings) {
|
||||
if (settings == null)
|
||||
return;
|
||||
using (ReflectionHelper.detect_property_changes(this, _raise_prop_changed, nameof(is_visible), nameof(is_collapsed), nameof(font_weight_override), nameof(font_weight),
|
||||
_settings = settings;
|
||||
using (ReflectionHelper.detect_property_changes(this, raise_prop_changed, nameof(is_visible), nameof(is_collapsed), nameof(font_weight_override), nameof(font_weight),
|
||||
nameof(font_color_override), nameof(font_color), nameof(font_brush), nameof(font_family), nameof(font_size), nameof(horizontal_alignment), nameof(horizontal_offset))) {
|
||||
//
|
||||
id = settings.id;
|
||||
@@ -252,11 +320,12 @@ namespace mbts.ui.controls {
|
||||
Width = original_width;
|
||||
}
|
||||
//
|
||||
raise_prop_changed(nameof(ColumnBase.settings));
|
||||
}
|
||||
|
||||
// 'fallback' font-params (aka 'grid-level' font-params)
|
||||
public void set_fallback_font_params(FontFamily font_family, double font_size, Color font_color, FontWeight font_weight) {
|
||||
using (ReflectionHelper.detect_property_changes(this, _raise_prop_changed, nameof(font_family), nameof(font_size), nameof(font_color), nameof(font_weight))) {
|
||||
using (ReflectionHelper.detect_property_changes(this, raise_prop_changed, nameof(font_family), nameof(font_size), nameof(font_color), nameof(font_weight))) {
|
||||
_fallback_font_family = font_family;
|
||||
_fallback_font_size = font_size;
|
||||
_fallback_font_color = font_color;
|
||||
@@ -268,14 +337,42 @@ namespace mbts.ui.controls {
|
||||
protected virtual void OnSortDirectionChanged(ListSortDirection? new_value) =>
|
||||
sort_direction_changed?.Invoke(this, new_value);
|
||||
|
||||
void _raise_prop_changed(string prop) =>
|
||||
protected void raise_prop_changed([CallerMemberName]string prop = "") =>
|
||||
OnPropertyChanged(new PropertyChangedEventArgs(prop));
|
||||
|
||||
protected void set<T>(ref T backing, T new_value, [CallerMemberName]string prop = "") =>
|
||||
set(ref backing, new_value, EqualityComparer<T>.Default, prop);
|
||||
|
||||
protected void set<T>(ref T backing, T new_value, IEqualityComparer<T> comparer, [CallerMemberName]string prop = "") {
|
||||
if (!comparer.Equals(backing, new_value)) {
|
||||
backing = new_value;
|
||||
OnPropertyChanged(new PropertyChangedEventArgs(prop));
|
||||
}
|
||||
}
|
||||
public override string ToString() =>
|
||||
id.ToString();
|
||||
}
|
||||
|
||||
internal abstract class ColumnBase<TColKey> : ColumnBase, IKeyedColumn<TColKey>, IGridColumn {
|
||||
TColKey _key;
|
||||
bool _key_initialized;
|
||||
// need a default constructor to support declaration in xaml
|
||||
protected ColumnBase() { }
|
||||
protected ColumnBase(TColKey key) => this.key = key;
|
||||
|
||||
public TColKey key { get; }
|
||||
public TColKey key {
|
||||
get => _key;
|
||||
set {
|
||||
if (Equals(_key, value))
|
||||
return;
|
||||
Log.assert(!_key_initialized, "Column key already initialized.");
|
||||
_key_initialized = true;
|
||||
_key = value;
|
||||
on_key_initialized(value);
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void on_key_initialized(TColKey key) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ namespace mbts.ui.controls {
|
||||
public double width { get { return column.ActualWidth; } }
|
||||
public double right { get { return unclipped_left + width; } }
|
||||
public bool is_partially_obscured { get { return unclipped_left < left; } }
|
||||
public double visible_width => width - left + unclipped_left;
|
||||
public double visible_width => Math.Max(0, width - left + unclipped_left);
|
||||
public override string ToString() {
|
||||
return "{3} {0:0.0},{1:0.0} w{2:0.0}".fmt(left, right, width, column.Header);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
using bts;
|
||||
using bts.utils;
|
||||
using mbts.spark.filters;
|
||||
using mbts.ui.controls.adorners;
|
||||
using mbts.ui.extensions;
|
||||
using mbts.ui.settings;
|
||||
using mbts.ui.utility;
|
||||
using bts.utils;
|
||||
using mbts.utils;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
@@ -12,14 +14,13 @@ using System.Collections.Specialized;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Controls.Primitives;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using mbts.utils;
|
||||
using System.Reactive.Linq;
|
||||
|
||||
namespace mbts.ui.controls {
|
||||
interface IHandleGridSorting {
|
||||
@@ -32,7 +33,21 @@ namespace mbts.ui.controls {
|
||||
public DIYGrid.ColumnBase Column { get; }
|
||||
public bool Handled { get; set; }
|
||||
}
|
||||
|
||||
class CachedColumnParam {
|
||||
public string SourcePropertyName { get; set; }
|
||||
public DependencyProperty TargetProperty { get; set; }
|
||||
}
|
||||
|
||||
class GroupItemSelectionPivotEventArgs : RoutedEventArgs {
|
||||
public GroupItemSelectionPivotEventArgs(RoutedEvent routed_event, object source, CollectionViewGroup group_item)
|
||||
: base(routed_event, source) => this.group_item = group_item;
|
||||
|
||||
public CollectionViewGroup group_item { get; }
|
||||
}
|
||||
|
||||
delegate void DIYGridSortingEventHandler(object sender, DIYGridSortingEventArgs e);
|
||||
delegate void GroupItemSelectionPivotEventHandler(object sender, GroupItemSelectionPivotEventArgs e);
|
||||
|
||||
static class DIYGridSortHelper {
|
||||
/// Important: All columns MUST IMPLEMENT <see cref="DIYGrid.IKeyedColumn{TColKey}"/>.
|
||||
@@ -123,13 +138,17 @@ namespace mbts.ui.controls {
|
||||
}
|
||||
|
||||
class DIYGridRowsPresenter : ListBox {
|
||||
readonly LoggerContext _log = new LoggerContext(Log.Ui, $"{nameof(DIYGridRowsPresenter)} ", null);
|
||||
double _row_height = DIYGrid.DefaultRowHeight;
|
||||
ScrollContentPresenter _content_presenter_backing;
|
||||
DIYGrid _grid;
|
||||
DragRectAdorner _drag_rect_adorner;
|
||||
ScrollViewer _scroll_viewer;
|
||||
Point? _drag_start;
|
||||
double _drag_start_vertical_drift;
|
||||
bool _support_group_items;
|
||||
int _drag_start_index;
|
||||
object _drag_start_item;
|
||||
|
||||
static DIYGridRowsPresenter() =>
|
||||
DefaultStyleKeyProperty.OverrideMetadata(typeof(DIYGridRowsPresenter), new FrameworkPropertyMetadata(typeof(DIYGridRowsPresenter)));
|
||||
@@ -141,6 +160,8 @@ namespace mbts.ui.controls {
|
||||
_grid = this.find_ancestor_of_type<DIYGrid>();
|
||||
_row_height = _grid.RowHeight;
|
||||
_grid.RowHeightChanged += on_row_height_changed;
|
||||
_support_group_items = _grid.SupportGroupItems;
|
||||
_grid.SupportGroupItemsChanged += on_support_group_items_changed;
|
||||
_scroll_viewer = this.descendants_of_type<ScrollViewer>().First();
|
||||
if (SelectionMode != SelectionMode.Single) {
|
||||
_drag_rect_adorner = new DragRectAdorner(this, _scroll_viewer, _row_height);
|
||||
@@ -156,8 +177,14 @@ namespace mbts.ui.controls {
|
||||
}
|
||||
};
|
||||
void on_row_height_changed(double v) => _row_height = v;
|
||||
void on_support_group_items_changed(bool v) => _support_group_items = v;
|
||||
}
|
||||
|
||||
public event GroupItemSelectionPivotEventHandler GroupItemSelectionPivot;
|
||||
|
||||
ScrollContentPresenter _content_presenter => _content_presenter_backing ??
|
||||
(_content_presenter_backing = this.descendants_of_type<ScrollContentPresenter>().FirstOrDefault());
|
||||
|
||||
public new void SetSelectedItems(IEnumerable items) {
|
||||
base.SetSelectedItems(items);
|
||||
}
|
||||
@@ -168,22 +195,37 @@ namespace mbts.ui.controls {
|
||||
|
||||
protected override void OnPreviewMouseLeftButtonDown(MouseButtonEventArgs e) {
|
||||
var pt = e.GetPosition(this);
|
||||
var container = this.ChildAtPoint<ListBoxItem>(pt);
|
||||
if (container != null) {
|
||||
var item = ItemContainerGenerator.ItemFromContainer(container);
|
||||
if (item != null) {
|
||||
if (InputHitTest(pt) is UIElement el) {
|
||||
var sb = new StringBuilderEx("beginning drag selection on ");
|
||||
OneOf.MatchFirst<DependencyObject, ListBoxItem, GroupItem>(el.VisualAncestors().Prepend(el)).Match(init_common, on_group);
|
||||
void on_group(GroupItem g) {
|
||||
if (g.DataContext is CollectionViewGroup gvm && gvm.FlatItems().Select(ItemContainerGenerator.ContainerFromItem).OfType<ListBoxItem>().TryGetFirst(out var lbi)) {
|
||||
// ListBox calls this internally when a ListBoxItem is clicked;
|
||||
// it needs to be called explicitly in this case, since the click target is a GroupItem
|
||||
Mouse.Capture(this, CaptureMode.SubTree);
|
||||
sb.Add($"{nameof(GroupItem)} '{gvm.Name}' with actual item ");
|
||||
init_common(lbi);
|
||||
}
|
||||
}
|
||||
void init_common(ListBoxItem item_container) {
|
||||
_drag_start = pt;
|
||||
_drag_start_index = Items.IndexOf(item);
|
||||
_drag_start_item = ItemContainerGenerator.ItemFromContainer(item_container);
|
||||
_drag_start_index = ItemContainerGenerator.IndexFromContainer(item_container);
|
||||
_drag_start_vertical_drift = _scroll_viewer.VerticalOffset;
|
||||
sb.Add($"'{_drag_start_item}' (selection mode {SelectionMode}).");
|
||||
_log.info(sb.ToString());
|
||||
}
|
||||
}
|
||||
base.OnPreviewMouseLeftButtonDown(e);
|
||||
}
|
||||
|
||||
protected override void OnPreviewMouseUp(MouseButtonEventArgs e) {
|
||||
if (SelectionMode != SelectionMode.Single) {
|
||||
if (_drag_start.HasValue) {
|
||||
if (_drag_start.HasValue) {
|
||||
_log.info("ending drag selection on mouse up.");
|
||||
if (SelectionMode != SelectionMode.Single && _content_presenter != null) {
|
||||
var drag_stop = e.GetPosition(this);
|
||||
drag_stop.X = MathUtils.within_range(0, _content_presenter.RenderSize.Width - 1, drag_stop.X);
|
||||
drag_stop.Y = MathUtils.within_range(0, _content_presenter.RenderSize.Height - 1, drag_stop.Y);
|
||||
double vertical_diff = drag_stop.Y + (_row_height * (_scroll_viewer.VerticalOffset - _drag_start_vertical_drift)) - _drag_start.Value.Y;
|
||||
int start_index, n_items;
|
||||
if (vertical_diff > 0) {
|
||||
@@ -203,14 +245,65 @@ namespace mbts.ui.controls {
|
||||
}
|
||||
}
|
||||
if (n_items > 1) {
|
||||
var selection = Enumerable.Range(start_index, n_items).Select(i => Items[i]).ToList();
|
||||
SetSelectedItems(selection);
|
||||
if (_support_group_items) {
|
||||
if (this.ChildAtPoint(drag_stop, out ListBoxItem drag_stop_container)) {
|
||||
_select_items_between(_drag_start_item, ItemContainerGenerator.ItemFromContainer(drag_stop_container));
|
||||
} else if (this.ChildAtPoint(drag_stop, out GroupItem gi) && gi.DataContext is CollectionViewGroup cvg && cvg.ItemCount > 0) {
|
||||
if ((ItemContainerGenerator.ContainerFromItem(_drag_start_item)?.VisualAncestors<GroupItem>()).empty_if_null().FirstOrDefault() == gi) {
|
||||
_select_items_between(_drag_start_item, cvg.Items.FirstOrDefault());
|
||||
} else {
|
||||
var gisp_args = new GroupItemSelectionPivotEventArgs(PreviewMouseUpEvent, this, cvg);
|
||||
GroupItemSelectionPivot?.Invoke(this, gisp_args);
|
||||
if (gisp_args.Handled) {
|
||||
// don't select the inner group items
|
||||
if (vertical_diff < 0) {
|
||||
_select_items_between(_drag_start_item, cvg.FlatItems().FirstOrDefault());
|
||||
} else {
|
||||
var end = cvg.FlatItems().FirstOrDefault();
|
||||
var selection = Items.Cast<object>()
|
||||
.Between(_drag_start_item, cvg.FlatItems().FirstOrDefault())
|
||||
.Prepend(_drag_start_item);
|
||||
_log.info($"selecting item range (inclusive begin '{_drag_start_item}' to exclusive end {end}).");
|
||||
SetSelectedItems(selection);
|
||||
}
|
||||
} else {
|
||||
/// Execute default behavior (select all the inner items in the GroupItem)
|
||||
/// The way <see cref="_select_items_between(object, object)"/> works, is two items play the role of 'bookends' - every item in between them is selected.
|
||||
/// Since the drag_start item is the first bookend, depending on the drag direction, either the first, or last item in the group will be the other bookend.
|
||||
_select_items_between(_drag_start_item, vertical_diff > 0 ? cvg.Items.Last() : cvg.Items.First());
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
var selection = Enumerable.Range(start_index, n_items).Select(i => Items[i]).ToList();
|
||||
Log.assert(selection.Count > 0);
|
||||
_log.info($"selecting item range (size {selection.Count}) determined by drag distance '{vertical_diff}' x row height '{_row_height}' (inclusive begin '{selection[0]}' to inclusive end '{selection[selection.Count - 1]}').");
|
||||
SetSelectedItems(selection);
|
||||
}
|
||||
}
|
||||
_drag_start = null;
|
||||
}
|
||||
_drag_start = null;
|
||||
_drag_start_item = null;
|
||||
}
|
||||
base.OnPreviewMouseUp(e);
|
||||
}
|
||||
|
||||
void _select_items_between(object inclusive_begin, object inclusive_end) {
|
||||
if (inclusive_begin == null || inclusive_end == null) {
|
||||
// One could argue that an exception should be thrown here, as null is a bogus value for either parameter.
|
||||
// However, clearing the selection seems reasonable enough, and does not risk crashing (assuming nothing particularly nefarious is happening).
|
||||
_log.warn($"clearing the selection because the specified item range cannot be determined (inclusive begin '{inclusive_begin}' to inclusive end '{inclusive_end}').");
|
||||
UnselectAll();
|
||||
} else if (inclusive_begin == inclusive_end) {
|
||||
_log.info($"selecting single item '{inclusive_begin}.'");
|
||||
SetSelectedItems(new[] { inclusive_begin });
|
||||
} else {
|
||||
_log.info($"selecting item range (inclusive begin '{inclusive_begin}' to inclusive end '{inclusive_end}').");
|
||||
SetSelectedItems(Items.Cast<object>()
|
||||
.Between(inclusive_begin, inclusive_end)
|
||||
.BookEnd(inclusive_begin, inclusive_end));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
partial class DIYGrid : Control {
|
||||
@@ -225,6 +318,7 @@ namespace mbts.ui.controls {
|
||||
DefaultCellPadding = new Thickness(4, 2, 4, 2),
|
||||
AnimatableCellPadding = new Thickness(8, 2, 4, 2);
|
||||
public static readonly Visibility DefaultRowsHorizontalScrollBarVisibility = Visibility.Visible;
|
||||
bool _support_group_items;
|
||||
|
||||
#region RoutedCommands
|
||||
public static readonly RoutedCommand SortCommand = new RoutedCommand(), DefaultRowCommand = new RoutedCommand();
|
||||
@@ -337,6 +431,7 @@ namespace mbts.ui.controls {
|
||||
}
|
||||
|
||||
public event DIYGridSortingEventHandler Sorting;
|
||||
public event Action<bool> SupportGroupItemsChanged;
|
||||
|
||||
public double InactiveSelectionOpacity {
|
||||
get => _inactive_selection_opacity;
|
||||
@@ -348,9 +443,21 @@ namespace mbts.ui.controls {
|
||||
}
|
||||
}
|
||||
|
||||
public bool SupportGroupItems {
|
||||
get => _support_group_items;
|
||||
set {
|
||||
if (_support_group_items != value) {
|
||||
_support_group_items = value;
|
||||
SupportGroupItemsChanged?.Invoke(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ItemContainerGenerator RowContainerGenerator =>
|
||||
_row_presenter.ItemContainerGenerator;
|
||||
|
||||
public ObservableCollection<GroupStyle> GroupStyle { get; } = new ObservableCollection<GroupStyle>();
|
||||
|
||||
private void OnCopy(object sender, ExecutedRoutedEventArgs e) {
|
||||
// TODO: implement excel-style copy rows/cells
|
||||
}
|
||||
@@ -407,7 +514,7 @@ namespace mbts.ui.controls {
|
||||
}
|
||||
|
||||
#region Column Handling
|
||||
private void invalidate_column_layout(object sender = null, EventArgs args = null) {
|
||||
public void invalidate_column_layout(object sender = null, EventArgs args = null) {
|
||||
var geometry = GetColumnGeometry().ToList();
|
||||
var clc = ColumnLayoutChanged;
|
||||
if (clc != null) {
|
||||
@@ -482,26 +589,70 @@ namespace mbts.ui.controls {
|
||||
_superheader_column_index = SuperHeaders.SelectMany(sh => Enumerable.Repeat(sh, sh.Width)).ToArray();
|
||||
}
|
||||
|
||||
ObservableCollection<ColumnBase> _visible_columns = new ObservableCollection<ColumnBase>();
|
||||
IDisposable _visible_columns_filter_hook;
|
||||
readonly ObservableCollection<ColumnBase> _visible_columns = new ObservableCollection<ColumnBase>();
|
||||
IDisposable _columns_hooks;
|
||||
|
||||
private static void columns_changed(DependencyObject d, DependencyPropertyChangedEventArgs args) {
|
||||
if (d is DIYGrid grid) {
|
||||
grid.on_columns_collection_changed(args.OldValue as ObservableCollection<ColumnBase>, args.NewValue as ObservableCollection<ColumnBase>);
|
||||
grid._reset_columns(args.NewValue as ObservableCollection<ColumnBase>);
|
||||
}
|
||||
}
|
||||
|
||||
void _reset_columns() =>
|
||||
_reset_columns(Columns);
|
||||
|
||||
// Called when the columns COLLECTION has been replaced entirely
|
||||
private void on_columns_collection_changed(ObservableCollection<ColumnBase> oldval, ObservableCollection<ColumnBase> newval) {
|
||||
if (oldval != null) {
|
||||
Disposable.dispose(ref _visible_columns_filter_hook);
|
||||
_visible_columns.CollectionChanged -= on_columns_changed;
|
||||
}
|
||||
void _reset_columns(ObservableCollection<ColumnBase> newval) {
|
||||
Disposable.dispose(ref _columns_hooks);
|
||||
if (newval != null) {
|
||||
_visible_columns_filter_hook = ObservableFilterCollection.connect_never_duplicate(newval, _visible_columns, c => c.is_visible, nameof(ColumnBase.is_visible));
|
||||
_visible_columns.CollectionChanged += on_columns_changed;
|
||||
// When column.is_visible changes, visible_columns will be refreshed asynchronously,
|
||||
// via Dispatcher.InvokeAsync, to drown out noisy batch changes.
|
||||
// The vis_cols_sync_queued flag indicates that such a refresh has already been queued.
|
||||
bool vis_cols_sync_queued = false;
|
||||
// vis_cols_disposed, when true, indicates that the Columns dp has changed.
|
||||
// This flag is checked in the Dispatcher.InvokeAsync callback before resetting visible_columns, to avoid overwriting the latest value.
|
||||
bool vis_cols_disposed = false;
|
||||
var vis_cols_hook = new DisposableContainer();
|
||||
reset_visible_columns();
|
||||
_columns_hooks = Disposable.Create(
|
||||
Disposable.Create(() => vis_cols_disposed = true),
|
||||
ObservableAutoWrapper.connect_item_hooks(newval, newval, c => {
|
||||
c.is_visible_changed += column_is_visible_changed;
|
||||
return Disposable.Create(() => c.is_visible_changed -= column_is_visible_changed);
|
||||
}),
|
||||
vis_cols_hook,
|
||||
_visible_columns.Subscribe(on_columns_changed));
|
||||
if (GetCachedColumnParamsContainer(this) is FrameworkElement params_container && CachedColumnParams is IEnumerable<string> ccp) {
|
||||
var ccp_array = ccp.ToArray();
|
||||
_columns_hooks = Disposable.Create(
|
||||
_columns_hooks,
|
||||
ObservableAutoWrapper.connect_item_hooks(newval, c =>
|
||||
ObjectSnapshot.property_table((INotifyPropertyChanged)c, ccp_array)
|
||||
.Subscribe((bts.api.core.TableUpdate<(string name, object current, object original), string> table) => {
|
||||
foreach (var (name, current, original) in table.added.Concat(table.changed))
|
||||
params_container.Resources[ColumnResourceHelper.col_prop_resource_id(c.id, name)] = current;
|
||||
})
|
||||
));
|
||||
}
|
||||
// local methods
|
||||
void reset_visible_columns() =>
|
||||
vis_cols_hook.replace(ObservableFilterCollection.connect_never_duplicate(newval, _visible_columns, c => c.is_visible, nameof(ColumnBase.is_visible)));
|
||||
|
||||
void column_is_visible_changed(bool is_visible) {
|
||||
if (!vis_cols_sync_queued) {
|
||||
vis_cols_sync_queued = true;
|
||||
Dispatcher.InvokeAsync(() => {
|
||||
if (!vis_cols_disposed) {
|
||||
reset_visible_columns();
|
||||
vis_cols_sync_queued = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
on_columns_changed(_visible_columns, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
|
||||
}
|
||||
|
||||
// Called when items are added or removed from the columns collection
|
||||
private void on_columns_changed(object sender, NotifyCollectionChangedEventArgs e) {
|
||||
if (sender is ObservableCollection<ColumnBase> columns) {
|
||||
@@ -552,6 +703,8 @@ namespace mbts.ui.controls {
|
||||
void _on_col_font_params_changed() => CoerceValue(RowHeightProperty);
|
||||
|
||||
void _try_set_column_filter_props(ColumnBase column) {
|
||||
if (column.HeaderTemplate == null && !(column.Header is Visual))
|
||||
column.HeaderTemplate = TryFindResource("DefaultColumnHeaderContentTemplate") as DataTemplate;
|
||||
if (column is IFilterableColumn fc) {
|
||||
DataGridFilterProperties.SetFilter(column, fc.filter);
|
||||
DataGridFilterProperties.SetFilterTemplate(column, FindResource(fc.filter_template) as DataTemplate);
|
||||
@@ -562,6 +715,11 @@ namespace mbts.ui.controls {
|
||||
|
||||
public static readonly DependencyProperty HeaderTotalSizeHackProperty = DependencyProperty.Register("HeaderTotalSizeHack", typeof(double), typeof(DIYGrid), new FrameworkPropertyMetadata(0.0, dp_invalidate_column_layout));
|
||||
|
||||
public double HeaderTotalSizeHack {
|
||||
get => (double)GetValue(HeaderTotalSizeHackProperty);
|
||||
set => SetValue(HeaderTotalSizeHackProperty, value);
|
||||
}
|
||||
|
||||
private static void dp_invalidate_column_layout(DependencyObject d, DependencyPropertyChangedEventArgs unused) {
|
||||
var grid = d as DIYGrid;
|
||||
if (grid != null) {
|
||||
@@ -594,6 +752,125 @@ namespace mbts.ui.controls {
|
||||
// Public Properties
|
||||
#region Public Properties
|
||||
|
||||
#region ColumnKey
|
||||
public static readonly DependencyProperty ColumnKeyProperty =
|
||||
DependencyProperty.RegisterAttached("ColumnKey",
|
||||
typeof(object),
|
||||
typeof(DIYGrid),
|
||||
new PropertyMetadata((d, e) => {
|
||||
if (!(d is UIElement child))
|
||||
return;
|
||||
if (VisualTreeHelper.GetParent(child) is Grid panel) {
|
||||
if (GetSyncWithColumnGeometrySource(d) is DIYGrid diy_grid || panel.try_find_ancestor_of_type(out diy_grid))
|
||||
ColumnGeometrySyncHelper.reset_panel_child(child, diy_grid, e.NewValue);
|
||||
}
|
||||
if (d is FrameworkElement child_el) {
|
||||
var curr_params = GetObservedCachedColumnParams(d);
|
||||
if (curr_params != null) {
|
||||
// clear observed params with old key, then reset with new key
|
||||
ColumnResourceHelper.reset_cell_resource_references(child_el, e.OldValue, curr_params, null);
|
||||
ColumnResourceHelper.reset_cell_resource_references(child_el, e.NewValue, null, curr_params);
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
public static object GetColumnKey(DependencyObject d) => d.GetValue(ColumnKeyProperty);
|
||||
public static void SetColumnKey(DependencyObject d, object value) => d.SetValue(ColumnKeyProperty, value);
|
||||
#endregion
|
||||
|
||||
#region SyncWithColumnGeometrySource
|
||||
public static readonly DependencyProperty SyncWithColumnGeometrySourceProperty =
|
||||
DependencyProperty.RegisterAttached(
|
||||
"SyncWithColumnGeometrySource",
|
||||
typeof(DIYGrid),
|
||||
typeof(DIYGrid),
|
||||
new PropertyMetadata(ColumnGeometrySyncHelper.reset_sync));
|
||||
|
||||
public static DIYGrid GetSyncWithColumnGeometrySource(DependencyObject d) => d.GetValue(SyncWithColumnGeometrySourceProperty) as DIYGrid;
|
||||
public static void SetSyncWithColumnGeometrySource(DependencyObject d, DIYGrid value) => d.SetValue(SyncWithColumnGeometrySourceProperty, value);
|
||||
#endregion
|
||||
|
||||
#region SyncWithColumnGeometry
|
||||
public static readonly DependencyProperty SyncWithColumnGeometryProperty =
|
||||
DependencyProperty.RegisterAttached(
|
||||
"SyncWithColumnGeometry",
|
||||
typeof(bool),
|
||||
typeof(DIYGrid),
|
||||
new PropertyMetadata(false, ColumnGeometrySyncHelper.reset_sync));
|
||||
|
||||
public static bool GetSyncWithColumnGeometry(DependencyObject d) => (bool)d.GetValue(SyncWithColumnGeometryProperty);
|
||||
public static void SetSyncWithColumnGeometry(DependencyObject d, bool value) => d.SetValue(SyncWithColumnGeometryProperty, value);
|
||||
|
||||
static readonly DependencyPropertyKey SyncWithColumnGeometryHookPropertyKey =
|
||||
DependencyProperty.RegisterAttachedReadOnly(
|
||||
"SyncWithColumnGeometryHook",
|
||||
typeof(IDisposable),
|
||||
typeof(DIYGrid),
|
||||
new PropertyMetadata(FrameworkElementLifeDependencyHelper.DisposeOld));
|
||||
|
||||
internal static IDisposable GetSyncWithColumnGeometryHook(DependencyObject d) =>
|
||||
d.GetValue(SyncWithColumnGeometryHookPropertyKey.DependencyProperty) as IDisposable;
|
||||
|
||||
static void SetSyncWithColumnGeometryHook(DependencyObject d, IDisposable value) =>
|
||||
d.SetValue(SyncWithColumnGeometryHookPropertyKey, value);
|
||||
#endregion
|
||||
|
||||
#region CachedColumnParamsContainer
|
||||
// This is the element who's ResourceDictionary will be used to store the observed column params.
|
||||
// By allowing callers to specify this element (as opposed to simply putting them in the DIYGrid's resources),
|
||||
// elements that are not visual children of the DIYGrid can bind to these params as well.
|
||||
// Once assigned, this value should not change during the lifetime of the DIYGrid.
|
||||
// This could create conflicting references among the binding targets,
|
||||
// as resources added to the old CachedColumnParamsContainer would not be removed.
|
||||
public static readonly DependencyProperty CachedColumnParamsContainerProperty =
|
||||
DependencyProperty.RegisterAttached(
|
||||
"CachedColumnParamsContainer",
|
||||
typeof(FrameworkElement),
|
||||
typeof(DIYGrid),
|
||||
new PropertyMetadata((d, e) => (d as DIYGrid)?._reset_columns()));
|
||||
|
||||
public static FrameworkElement GetCachedColumnParamsContainer(DependencyObject d) => d.GetValue(CachedColumnParamsContainerProperty) as FrameworkElement;
|
||||
public static void SetCachedColumnParamsContainer(DependencyObject d, ResourceDictionary value) => d.SetValue(CachedColumnParamsContainerProperty, value);
|
||||
#endregion
|
||||
|
||||
#region CachedColumnParams
|
||||
public static readonly DependencyProperty CachedColumnParamsProperty =
|
||||
WPFHelper.create_dp<IEnumerable<string>, DIYGrid>(
|
||||
nameof(CachedColumnParams),
|
||||
(o, v) => {
|
||||
o?._reset_columns();
|
||||
});
|
||||
|
||||
public IEnumerable<string> CachedColumnParams {
|
||||
get => GetValue(CachedColumnParamsProperty) as IEnumerable<string>;
|
||||
set => SetValue(CachedColumnParamsProperty, value);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region ObservedCachedColumnParams
|
||||
public static readonly DependencyProperty ObservedCachedColumnParamsProperty =
|
||||
DependencyProperty.RegisterAttached(
|
||||
"ObservedCachedColumnParams",
|
||||
typeof(IEnumerable<CachedColumnParam>),
|
||||
typeof(DIYGrid),
|
||||
new PropertyMetadata(ColumnResourceHelper.ObservedCachedParamsChanged));
|
||||
|
||||
public static IEnumerable<CachedColumnParam> GetObservedCachedColumnParams(DependencyObject d) => d.GetValue(ObservedCachedColumnParamsProperty) as IEnumerable<CachedColumnParam>;
|
||||
public static void SetObservedCachedColumnParams(DependencyObject d, IEnumerable<CachedColumnParam> value) => d.SetValue(ObservedCachedColumnParamsProperty, value);
|
||||
#endregion
|
||||
|
||||
#region ColumnSettings
|
||||
public static readonly DependencyProperty ColumnSettingsProperty =
|
||||
DependencyProperty.RegisterAttached("ColumnSettings", typeof(settings.GridColumnSettings), typeof(DIYGrid),
|
||||
new FrameworkPropertyMetadata((d, e) => {
|
||||
if (d is ColumnBase c && e.NewValue is settings.GridColumnSettings s)
|
||||
c.load_from(s);
|
||||
}));
|
||||
|
||||
public static settings.GridColumnSettings GetColumnSettings(DependencyObject d) => d.GetValue(ColumnSettingsProperty) as settings.GridColumnSettings;
|
||||
public static void SetColumnSettings(DependencyObject d, settings.GridColumnSettings value) => d.SetValue(ColumnSettingsProperty, value);
|
||||
#endregion
|
||||
|
||||
public static readonly DependencyProperty BackgroundOverlayProperty =
|
||||
DependencyProperty.RegisterAttached(
|
||||
"BackgroundOverlay",
|
||||
@@ -783,7 +1060,7 @@ namespace mbts.ui.controls {
|
||||
/// Compute the geometry of all visible (or partially visible columns) columns
|
||||
/// This effectively replaces the entire Measure/Arrange pass of the grid rows (and enables column virtualization)
|
||||
/// Mouse events will be hit tested against this as well
|
||||
internal IEnumerable<ColumnGeometry> GetColumnGeometry() {
|
||||
internal IEnumerable<ColumnGeometry> GetColumnGeometry(bool include_obscured_cols = false) {
|
||||
if (_header_scroll_viewer != null && ActualWidth > 0) {
|
||||
double frozen_offset = 0;
|
||||
int vis_frozen_col_count = _get_visible_frozen_column_count();
|
||||
@@ -805,7 +1082,7 @@ namespace mbts.ui.controls {
|
||||
double left = Math.Max(offset, frozen_offset);
|
||||
double right = offset + col.ActualWidth;
|
||||
// If right < frozen_offset column is completely obscured
|
||||
if (right > frozen_offset && left <= ActualWidth) {
|
||||
if (include_obscured_cols || (right > frozen_offset && left <= ActualWidth)) {
|
||||
// fully visible
|
||||
yield return new ColumnGeometry() {
|
||||
column = col,
|
||||
@@ -826,6 +1103,7 @@ namespace mbts.ui.controls {
|
||||
|
||||
#endregion
|
||||
|
||||
IDisposable _group_style_row_presenter_link;
|
||||
// Overrides
|
||||
#region Overrides
|
||||
public override void OnApplyTemplate() {
|
||||
@@ -838,6 +1116,8 @@ namespace mbts.ui.controls {
|
||||
_scroll_horizontal = (ScrollBar)GetTemplateChild("_scroll_horiz");
|
||||
_row_presenter = (ListBox)GetTemplateChild("_row_presenter");
|
||||
Log.assert_d(!_row_presenter.IsLoaded);
|
||||
// (re)wrap group style
|
||||
Disposable.replace(ref _group_style_row_presenter_link, ObservableAutoWrapper.connect_two_way(GroupStyle, _row_presenter.GroupStyle));
|
||||
_try_set_row_style();
|
||||
_header_scroll_viewer = (ScrollViewer)GetTemplateChild("_header_scroll_viewer");
|
||||
Log.assert_d(!_header_scroll_viewer.IsLoaded);
|
||||
@@ -883,6 +1163,123 @@ namespace mbts.ui.controls {
|
||||
}
|
||||
#endregion
|
||||
|
||||
static class ColumnGeometrySyncHelper {
|
||||
public static void reset_panel_child(UIElement child, DIYGrid diy_grid) =>
|
||||
reset_panel_child(child, diy_grid, GetColumnKey(child));
|
||||
|
||||
public static void reset_panel_child(UIElement child, DIYGrid diy_grid, object col_key) {
|
||||
if (col_key == null)
|
||||
return;
|
||||
var key_str = col_key.ToString();
|
||||
if (diy_grid._visible_columns.TryGetFirst(c => string.Equals(c.id, key_str, StringComparison.CurrentCultureIgnoreCase), out var target_col, out int target_pos)) {
|
||||
// set clip to null in case this child was hidden before (see 'else' block).
|
||||
child.Clip = null;
|
||||
Grid.SetColumn(child, target_pos);
|
||||
} else {
|
||||
// hide element by setting clip to 0x0 rect, rather than visibility to collapsed,
|
||||
// to avoid incidentally breaking visibility bindings, triggers, animations, etc.
|
||||
child.Clip = new RectangleGeometry(new Rect(0, 0, 0, 0));
|
||||
}
|
||||
}
|
||||
|
||||
public static void reset_sync(DependencyObject d, DependencyPropertyChangedEventArgs e) {
|
||||
if (!GetSyncWithColumnGeometry(d))
|
||||
SetSyncWithColumnGeometryHook(d, null);
|
||||
if (!(d is Grid g))
|
||||
return;
|
||||
var hook = new DisposableContainer();
|
||||
if (g.IsLoaded)
|
||||
hook.replace(impl());
|
||||
SetSyncWithColumnGeometryHook(d, Disposable.Create(g.SubscribeLoaded(on_loaded), hook));
|
||||
//
|
||||
void on_loaded(object sender, RoutedEventArgs loaded_e) {
|
||||
if (GetSyncWithColumnGeometry(d))
|
||||
hook.replace(impl());
|
||||
}
|
||||
IDisposable impl() {
|
||||
// Avoid resetting children on every collection change, as that could be very noisy and hurt perf.
|
||||
// In the visible columns CollectionChanged handler, call reset_children inside a Dispatcher.InvokeAsync request
|
||||
// to effectively wait for potential batch-changes to finish.
|
||||
bool resetting_children = false;
|
||||
if (!(GetSyncWithColumnGeometrySource(d) is DIYGrid diy_grid) && !g.try_find_ancestor_of_type(out diy_grid))
|
||||
return null;
|
||||
g.SetBinding(Grid.WidthProperty, new Binding(nameof(HeaderTotalSizeHack)) { Source = diy_grid });
|
||||
reset_children();
|
||||
on_layout_changed(diy_grid.GetColumnGeometry(true).ToList());
|
||||
diy_grid.ColumnLayoutChanged += on_layout_changed;
|
||||
diy_grid._visible_columns.CollectionChanged += on_vis_columns_changed;
|
||||
return Disposable.Create(() => {
|
||||
diy_grid.ColumnLayoutChanged -= on_layout_changed;
|
||||
diy_grid._visible_columns.CollectionChanged -= on_vis_columns_changed;
|
||||
});
|
||||
//
|
||||
void on_vis_columns_changed(object sender, NotifyCollectionChangedEventArgs vce) {
|
||||
if (!resetting_children) {
|
||||
// avoid resetting children on every collection change, as that could be very noisy and hurt perf.
|
||||
resetting_children = true;
|
||||
diy_grid.Dispatcher.InvokeAsync(() => {
|
||||
on_layout_changed(diy_grid.GetColumnGeometry().ToList());
|
||||
reset_children();
|
||||
resetting_children = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
void on_layout_changed(List<ColumnGeometry> layout) {
|
||||
// TODO: figure out how to make the ColumnLayoutChanged event lazy, or somehow avoid making 2 lists of columns
|
||||
// (the layout param does not include obscured cols, so we have to create a new list of ColumnGeoemtry to satisfy our needs).
|
||||
layout = (from col in diy_grid._visible_columns
|
||||
join pos in diy_grid.GetColumnGeometry(true)
|
||||
on col.id equals pos.column.id
|
||||
select pos).ToList();
|
||||
|
||||
int n_removed = g.ColumnDefinitions.Count - layout.Count;
|
||||
if (n_removed > 0)
|
||||
g.ColumnDefinitions.RemoveRange(g.ColumnDefinitions.Count - n_removed, n_removed);
|
||||
int i = 0;
|
||||
for (; i < g.ColumnDefinitions.Count; i++) {
|
||||
var col_pos = layout[i];
|
||||
var col = g.ColumnDefinitions[i];
|
||||
if (col.Width.GridUnitType == GridUnitType.Pixel && col.Width.Value == col_pos.visible_width)
|
||||
continue;
|
||||
col.Width = new GridLength(col_pos.visible_width, GridUnitType.Pixel);
|
||||
}
|
||||
for (; i < layout.Count; i++) {
|
||||
g.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(layout[i].visible_width, GridUnitType.Pixel) });
|
||||
}
|
||||
}
|
||||
void reset_children() {
|
||||
foreach (UIElement child in g.Children)
|
||||
reset_panel_child(child, diy_grid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static class ColumnResourceHelper {
|
||||
public static string col_resource_id(object col_id) =>
|
||||
$"Columns_{col_id}";
|
||||
|
||||
public static string col_prop_resource_id(object col_id, string prop) =>
|
||||
$"{col_resource_id(col_id)}_{prop}";
|
||||
|
||||
public static void ObservedCachedParamsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) =>
|
||||
reset_cell_resource_references(d as FrameworkElement, e.OldValue as IEnumerable<CachedColumnParam>, e.NewValue as IEnumerable<CachedColumnParam>);
|
||||
|
||||
public static void reset_cell_resource_references(FrameworkElement target, IEnumerable<CachedColumnParam> old_params, IEnumerable<CachedColumnParam> new_params) =>
|
||||
reset_cell_resource_references(target, GetColumnKey(target), old_params, new_params);
|
||||
|
||||
public static void reset_cell_resource_references(FrameworkElement target, object col_id, IEnumerable<CachedColumnParam> old_params, IEnumerable<CachedColumnParam> new_params) {
|
||||
if (target == null || col_id == null)
|
||||
return;
|
||||
foreach (var p in old_params.empty_if_null()) {
|
||||
target.ClearValue(p.TargetProperty);
|
||||
}
|
||||
foreach (var p in new_params.empty_if_null()) {
|
||||
target.SetResourceReference(p.TargetProperty, col_prop_resource_id(col_id, p.SourcePropertyName));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DataContextHooks : IDisposable {
|
||||
List<IDisposable> _hooks = new List<IDisposable>(4);
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<ResourceDictionary
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mbts="clr-namespace:mbts.ui"
|
||||
xmlns:ui="clr-namespace:mbts.ui"
|
||||
xmlns:bts="clr-namespace:mbts.ui.controls"
|
||||
xmlns:u="clr-namespace:mbts.ui"
|
||||
xmlns:c="clr-namespace:mbts.ui.controls"
|
||||
xmlns:cv="clr-namespace:mbts.ui.converters"
|
||||
>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary Source="/view/Controls/Popup/PopupControl.xaml"/>
|
||||
@@ -35,14 +35,14 @@
|
||||
<Condition Property="Selector.IsSelectionActive" Value="False"/>
|
||||
<Condition Property="IsSelected" Value="True"/>
|
||||
</MultiTrigger.Conditions>
|
||||
<Setter Property="Background" TargetName="Bd" Value="{DynamicResource {x:Static bts:DIYGrid.InactiveSelectionHighlightKey}}"/>
|
||||
<Setter Property="Background" TargetName="Bd" Value="{DynamicResource {x:Static c:DIYGrid.InactiveSelectionHighlightKey}}"/>
|
||||
</MultiTrigger>
|
||||
<MultiTrigger>
|
||||
<MultiTrigger.Conditions>
|
||||
<Condition Property="Selector.IsSelectionActive" Value="True"/>
|
||||
<Condition Property="IsSelected" Value="True"/>
|
||||
</MultiTrigger.Conditions>
|
||||
<Setter Property="Background" TargetName="Bd" Value="{Binding SelectionHighlight, RelativeSource={RelativeSource AncestorType=bts:DIYGrid}}"/>
|
||||
<Setter Property="Background" TargetName="Bd" Value="{Binding SelectionHighlight, RelativeSource={RelativeSource AncestorType=c:DIYGrid}}"/>
|
||||
</MultiTrigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
@@ -50,17 +50,22 @@
|
||||
</Setter>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="ItemsControl.AlternationIndex" Value="0">
|
||||
<Setter Property="Background" Value="{ui:Brush {mbts:BTSColor Grid.input_cell_background}}"/>
|
||||
<Setter Property="Background" Value="{u:Brush {u:BTSColor Grid.input_cell_background}}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="ItemsControl.AlternationIndex" Value="1">
|
||||
<Setter Property="Background" Value="{ui:Brush {mbts:BTSColor Grid.alternate_row_background}}"/>
|
||||
<Setter Property="Background" Value="{u:Brush {u:BTSColor Grid.alternate_row_background}}"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
<Style TargetType="bts:DIYGridRowsPresenter" BasedOn="{StaticResource {x:Type ListBox}}">
|
||||
<Style TargetType="c:DIYGridRowsPresenter" BasedOn="{StaticResource {x:Type ListBox}}">
|
||||
<Setter Property="IsTextSearchEnabled" Value="False"/>
|
||||
</Style>
|
||||
<Style TargetType="bts:DIYGrid">
|
||||
<DataTemplate x:Key="DefaultColumnHeaderContentTemplate">
|
||||
<Grid>
|
||||
<TextBlock HorizontalAlignment="Center" VerticalAlignment="Center" Text="{Binding}" TextWrapping="WrapWithOverflow" TextAlignment="Center"/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
<Style TargetType="c:DIYGrid">
|
||||
<Setter Property="Focusable" Value="False"/>
|
||||
<Setter Property="SelectionMode" Value="Extended"/>
|
||||
<Setter Property="ColumnHeaderStyle">
|
||||
@@ -68,14 +73,15 @@
|
||||
<Style TargetType="GridViewColumnHeader">
|
||||
<!-- Background - note that we're settings this property on the containing stackpanel in which the GridViewHeaderRowPresenter resides -->
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="Foreground" Value="{mbts:BTSColor header_text}"/>
|
||||
<Setter Property="BorderBrush" Value="{mbts:BTSColor header}"/>
|
||||
<Setter Property="Foreground" Value="{u:BTSColor header_text}"/>
|
||||
<Setter Property="BorderBrush" Value="{u:BTSColor header}"/>
|
||||
<Setter Property="BorderThickness" Value="0,1,1,0"/>
|
||||
<Setter Property="Command" Value="{x:Static bts:DIYGrid.SortCommand}"/>
|
||||
<Setter Property="Command" Value="{x:Static c:DIYGrid.SortCommand}"/>
|
||||
<Setter Property="CommandParameter" Value="{Binding RelativeSource={RelativeSource Self}}"/>
|
||||
<Setter Property="CommandTarget" Value="{Binding RelativeSource={RelativeSource AncestorType=bts:DIYGrid}}"/>
|
||||
<Setter Property="HorizontalContentAlignment" Value="Center"/>
|
||||
<Setter Property="Height" Value="{x:Static bts:DIYGrid.DefaultColumnHeaderHeight}"/>
|
||||
<Setter Property="CommandTarget" Value="{Binding RelativeSource={RelativeSource AncestorType=c:DIYGrid}}"/>
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
|
||||
<Setter Property="VerticalContentAlignment" Value="Stretch"/>
|
||||
<Setter Property="Height" Value="{x:Static c:DIYGrid.DefaultColumnHeaderHeight}"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="{x:Type GridViewColumnHeader}">
|
||||
@@ -106,7 +112,7 @@
|
||||
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"
|
||||
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
|
||||
/>
|
||||
<Border x:Name="OperatorsBorder" Grid.Column="1" Background="{mbts:BTSColor Name=header}">
|
||||
<Border x:Name="OperatorsBorder" Grid.Column="1" Background="{u:BTSColor Name=header}">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
@@ -126,7 +132,7 @@
|
||||
Visibility="Collapsed"
|
||||
Width="8"
|
||||
/>
|
||||
<bts:PopupControl
|
||||
<c:PopupControl
|
||||
x:Name="FilterPopup"
|
||||
Grid.Column="1"
|
||||
Margin="2,0,0,0"
|
||||
@@ -138,44 +144,44 @@
|
||||
ShowExpanderArrow="False"
|
||||
PopupPlacement="Bottom"
|
||||
PlacementTarget="{Binding RelativeSource={RelativeSource AncestorType=GridViewColumnHeader}}"
|
||||
IsOpen="{Binding Path='Column.(bts:DataGridFilterProperties.FilterOpen)', Mode=TwoWay, RelativeSource={RelativeSource AncestorType=GridViewColumnHeader}}"
|
||||
ContentTemplate="{Binding Path='Column.(bts:DataGridFilterProperties.FilterTemplate)', RelativeSource={RelativeSource AncestorType=GridViewColumnHeader}}"
|
||||
Content="{Binding Path='Column.(bts:DataGridFilterProperties.Filter)', RelativeSource={RelativeSource AncestorType=GridViewColumnHeader}}"
|
||||
IsOpen="{Binding Path='Column.(c:DataGridFilterProperties.FilterOpen)', Mode=TwoWay, RelativeSource={RelativeSource AncestorType=GridViewColumnHeader}}"
|
||||
ContentTemplate="{Binding Path='Column.(c:DataGridFilterProperties.FilterTemplate)', RelativeSource={RelativeSource AncestorType=GridViewColumnHeader}}"
|
||||
Content="{Binding Path='Column.(c:DataGridFilterProperties.Filter)', RelativeSource={RelativeSource AncestorType=GridViewColumnHeader}}"
|
||||
>
|
||||
<bts:PopupControl.InputBindings>
|
||||
<c:PopupControl.InputBindings>
|
||||
<KeyBinding
|
||||
Key="Return"
|
||||
CommandParameter="{Binding Path='Column.(bts:DataGridFilterProperties.Filter)', RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=GridViewColumnHeader}}"
|
||||
Command="{x:Static bts:DataGridFilterProperties.CommitCommand}"
|
||||
CommandParameter="{Binding Path='Column.(c:DataGridFilterProperties.Filter)', RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=GridViewColumnHeader}}"
|
||||
Command="{x:Static c:DataGridFilterProperties.CommitCommand}"
|
||||
/>
|
||||
<KeyBinding Key="Esc" Command="{x:Static bts:PopupControl.CloseCommand}"/>
|
||||
</bts:PopupControl.InputBindings>
|
||||
<bts:PopupControl.CommandBindings>
|
||||
<KeyBinding Key="Esc" Command="{x:Static c:PopupControl.CloseCommand}"/>
|
||||
</c:PopupControl.InputBindings>
|
||||
<c:PopupControl.CommandBindings>
|
||||
<CommandBinding
|
||||
Command="{x:Static bts:DataGridFilterProperties.CommitCommand}"
|
||||
Executed="{x:Static bts:DataGridFilterProperties.commit_executed}"
|
||||
CanExecute="{x:Static bts:DataGridFilterProperties.commit_can_execute}"
|
||||
Command="{x:Static c:DataGridFilterProperties.CommitCommand}"
|
||||
Executed="{x:Static c:DataGridFilterProperties.commit_executed}"
|
||||
CanExecute="{x:Static c:DataGridFilterProperties.commit_can_execute}"
|
||||
/>
|
||||
<CommandBinding
|
||||
Command="{x:Static bts:DataGridFilterProperties.ClearCommand}"
|
||||
Executed="{x:Static bts:DataGridFilterProperties.clear_executed}"
|
||||
CanExecute="{x:Static bts:DataGridFilterProperties.clear_can_execute}"
|
||||
Command="{x:Static c:DataGridFilterProperties.ClearCommand}"
|
||||
Executed="{x:Static c:DataGridFilterProperties.clear_executed}"
|
||||
CanExecute="{x:Static c:DataGridFilterProperties.clear_can_execute}"
|
||||
/>
|
||||
</bts:PopupControl.CommandBindings>
|
||||
<bts:PopupControl.Header>
|
||||
</c:PopupControl.CommandBindings>
|
||||
<c:PopupControl.Header>
|
||||
<Grid>
|
||||
<Image Name="FilterPopupImage" Height="9" Width="9" Margin="3">
|
||||
<Image.Style>
|
||||
<Style TargetType="Image">
|
||||
<Setter Property="Source" Value="{x:Static mbts:Icons.Filter}"/>
|
||||
<Setter Property="Source" Value="{x:Static u:Icons.Filter}"/>
|
||||
<Setter Property="Opacity" Value="0.0"/>
|
||||
</Style>
|
||||
</Image.Style>
|
||||
</Image>
|
||||
</Grid>
|
||||
</bts:PopupControl.Header>
|
||||
<bts:PopupControl.Style>
|
||||
<Style TargetType="bts:PopupControl">
|
||||
</c:PopupControl.Header>
|
||||
<c:PopupControl.Style>
|
||||
<Style TargetType="c:PopupControl">
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="BorderBrush" Value="LightGray"/>
|
||||
@@ -186,8 +192,8 @@
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</bts:PopupControl.Style>
|
||||
</bts:PopupControl>
|
||||
</c:PopupControl.Style>
|
||||
</c:PopupControl>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
@@ -229,8 +235,8 @@
|
||||
<Setter Property="Grid.Column" TargetName="SortArrow" Value="0"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter Property="Background" TargetName="HeaderBorder" Value="{mbts:BTSColor Name=header_alternate}"/>
|
||||
<Setter Property="Background" TargetName="OperatorsBorder" Value="{mbts:BTSColor Name=header_alternate}"/>
|
||||
<Setter Property="Background" TargetName="HeaderBorder" Value="{u:BTSColor Name=header_alternate}"/>
|
||||
<Setter Property="Background" TargetName="OperatorsBorder" Value="{u:BTSColor Name=header_alternate}"/>
|
||||
<Setter Property="Visibility" TargetName="PART_HeaderGripper" Value="Hidden"/>
|
||||
<Setter Property="Margin" TargetName="HeaderContent" Value="1,1,0,0"/>
|
||||
</Trigger>
|
||||
@@ -238,21 +244,21 @@
|
||||
<Setter Property="MinHeight" Value="20"/>
|
||||
</Trigger>
|
||||
<!--Filter Active-->
|
||||
<DataTrigger Binding="{Binding Path='Column.(bts:DataGridFilterProperties.FilterActive)', RelativeSource={RelativeSource Self}}" Value="True">
|
||||
<Setter TargetName="HeaderContent" Property="Panel.ZIndex" Value="0"/>
|
||||
<Setter TargetName="FilterPopupImage" Property="Source" Value="{x:Static mbts:Icons.FilterActive}"/>
|
||||
<DataTrigger Binding="{Binding Path='Column.(c:DataGridFilterProperties.FilterActive)', RelativeSource={RelativeSource Self}}" Value="True">
|
||||
<Setter TargetName="HeaderContent" Property="Margin" Value="0,0,11,0"/>
|
||||
<Setter TargetName="FilterPopupImage" Property="Source" Value="{x:Static u:Icons.FilterActive}"/>
|
||||
<Setter TargetName="FilterPopupImage" Property="Opacity" Value="1.0"/>
|
||||
<Setter Property="Grid.Column" TargetName="SortArrow" Value="0"/>
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding Path='Column.(bts:DataGridFilterProperties.FilterActive)', RelativeSource={RelativeSource Self}}" Value="{x:Null}">
|
||||
<Setter TargetName="HeaderContent" Property="Panel.ZIndex" Value="0"/>
|
||||
<Setter TargetName="FilterPopupImage" Property="Source" Value="{x:Static mbts:Icons.FilterError}"/>
|
||||
<DataTrigger Binding="{Binding Path='Column.(c:DataGridFilterProperties.FilterActive)', RelativeSource={RelativeSource Self}}" Value="{x:Null}">
|
||||
<Setter TargetName="HeaderContent" Property="Margin" Value="0,0,11,0"/>
|
||||
<Setter TargetName="FilterPopupImage" Property="Source" Value="{x:Static u:Icons.FilterError}"/>
|
||||
<Setter TargetName="FilterPopupImage" Property="Opacity" Value="1.0"/>
|
||||
<Setter Property="Grid.Column" TargetName="SortArrow" Value="0"/>
|
||||
</DataTrigger>
|
||||
<!--Sort Active-->
|
||||
<DataTrigger Binding="{Binding Column.SortDirection, RelativeSource={RelativeSource Self}}" Value="Ascending">
|
||||
<Setter TargetName="HeaderContent" Property="Panel.ZIndex" Value="0"/>
|
||||
<Setter TargetName="HeaderContent" Property="Margin" Value="0,0,11,0"/>
|
||||
<Setter TargetName="SortArrow" Property="Visibility" Value="Visible"/>
|
||||
<Setter TargetName="SortArrow" Property="RenderTransform">
|
||||
<Setter.Value>
|
||||
@@ -261,7 +267,7 @@
|
||||
</Setter>
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding Column.SortDirection, RelativeSource={RelativeSource Self}}" Value="Descending">
|
||||
<Setter TargetName="HeaderContent" Property="Panel.ZIndex" Value="0"/>
|
||||
<Setter TargetName="HeaderContent" Property="Margin" Value="0,0,11,0"/>
|
||||
<Setter TargetName="SortArrow" Property="Visibility" Value="Visible"/>
|
||||
</DataTrigger>
|
||||
</ControlTemplate.Triggers>
|
||||
@@ -275,10 +281,10 @@
|
||||
<Setter.Value>
|
||||
<Style TargetType="{x:Type ToggleButton}">
|
||||
<Setter Property="Focusable" Value="False"/>
|
||||
<Setter Property="Background" Value="{mbts:BTSColor Name=header}"/>
|
||||
<Setter Property="Foreground" Value="{mbts:BTSColor Name=header_text}"/>
|
||||
<Setter Property="Background" Value="{u:BTSColor Name=header}"/>
|
||||
<Setter Property="Foreground" Value="{u:BTSColor Name=header_text}"/>
|
||||
<Setter Property="HorizontalContentAlignment" Value="Center"/>
|
||||
<Setter Property="BorderBrush" Value="{ui:Brush Color={ui:Color Name=Grid.grid_vertical_group_separator}}"/>
|
||||
<Setter Property="BorderBrush" Value="{u:Brush Color={u:Color Name=Grid.grid_vertical_group_separator}}"/>
|
||||
<Setter Property="BorderThickness" Value="0,0,2,0"/>
|
||||
<Setter Property="Margin" Value="0,0,0,-1"/>
|
||||
<Setter Property="FontWeight" Value="Bold"/>
|
||||
@@ -303,8 +309,32 @@
|
||||
</Setter>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="{x:Type bts:DIYGrid}">
|
||||
<ControlTemplate TargetType="{x:Type c:DIYGrid}">
|
||||
<Grid>
|
||||
<u:StateManager.VisualState>
|
||||
<Binding Path="FrozenColumnCount" RelativeSource="{RelativeSource TemplatedParent}">
|
||||
<Binding.Converter>
|
||||
<cv:ValueMap Default="Active">
|
||||
<cv:KeyValue Key="{u:Int 0}" Value="Inactive"/>
|
||||
</cv:ValueMap>
|
||||
</Binding.Converter>
|
||||
</Binding>
|
||||
</u:StateManager.VisualState>
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup Name="FrozenColumnHeaderStates">
|
||||
<VisualState Name="Active"/>
|
||||
<VisualState Name="Inactive">
|
||||
<Storyboard>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" Storyboard.TargetName="_header_frozen">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{x:Static Visibility.Collapsed}"/>
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" Storyboard.TargetName="_super_header_frozen">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{x:Static Visibility.Collapsed}"/>
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
</Storyboard>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
<Grid.ColumnDefinitions>
|
||||
<!-- Frozen -->
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
@@ -317,9 +347,9 @@
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
<!-- fill the space above the frozen columns -->
|
||||
<Rectangle Fill="{mbts:BTSColor Name=header}" Height="{Binding Path=ActualHeight, ElementName=_super_headers}" HorizontalAlignment="Stretch" Margin="0,0,-1,-1"/>
|
||||
<Rectangle x:Name="_super_header_frozen" Fill="{u:BTSColor Name=header}" Height="{Binding Path=ActualHeight, ElementName=_super_headers}" HorizontalAlignment="Stretch" Margin="0,0,-1,-1"/>
|
||||
<!-- colums set in code behind -->
|
||||
<GridViewHeaderRowPresenter x:Name="_header_frozen" Grid.Row="1" Grid.Column="0" AllowsColumnReorder="False" VerticalAlignment="Bottom" ColumnHeaderContainerStyle="{TemplateBinding ColumnHeaderStyle}" UseLayoutRounding="True"/>
|
||||
<GridViewHeaderRowPresenter x:Name="_header_frozen" Grid.Row="1" Grid.Column="0" AllowsColumnReorder="False" VerticalAlignment="Bottom" ColumnHeaderContainerStyle="{TemplateBinding ColumnHeaderStyle}"/>
|
||||
<ScrollViewer
|
||||
x:Name="_header_scroll_viewer"
|
||||
Grid.Column="1"
|
||||
@@ -331,18 +361,21 @@
|
||||
>
|
||||
<!-- setting the bg color for this stackpanel is a work-around for the vertical white lines that appear on high-dpi displays between some columns -->
|
||||
<!-- in reality, there is some high dpi rounding thing going on, but that's a battle for another day -->
|
||||
<StackPanel Background="{mbts:BTSColor Name=header}">
|
||||
<StackPanel Background="{u:BTSColor Name=header}">
|
||||
<!-- We don't need super headers over the fixed columns -->
|
||||
<StackPanel Orientation="Horizontal" x:Name="_super_headers"/>
|
||||
<GridViewHeaderRowPresenter x:Name="_header_scroll" AllowsColumnReorder="False" ColumnHeaderContainerStyle="{TemplateBinding ColumnHeaderStyle}" UseLayoutRounding="True"/>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
<bts:DIYGridRowsPresenter
|
||||
<c:DIYGridRowsPresenter
|
||||
x:Name="_row_presenter"
|
||||
Grid.Row="2"
|
||||
Grid.ColumnSpan="2"
|
||||
ScrollViewer.HorizontalScrollBarVisibility="Hidden"
|
||||
VirtualizingPanel.IsVirtualizingWhenGrouping="True"
|
||||
VirtualizingPanel.VirtualizationMode="Recycling"
|
||||
ScrollViewer.CanContentScroll="True"
|
||||
VirtualizingPanel.ScrollUnit="{TemplateBinding VirtualizingPanel.ScrollUnit}"
|
||||
AlternationCount="2"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderThickness="0"
|
||||
@@ -351,21 +384,21 @@
|
||||
SelectedItem="{Binding CurrentRow, RelativeSource={RelativeSource TemplatedParent}}"
|
||||
SelectionMode="{TemplateBinding SelectionMode}"
|
||||
>
|
||||
<bts:DIYGridRowsPresenter.Resources>
|
||||
<c:DIYGridRowsPresenter.Resources>
|
||||
<Style TargetType="ListBoxItem" BasedOn="{StaticResource DIYGridRowStyle}"/>
|
||||
</bts:DIYGridRowsPresenter.Resources>
|
||||
<bts:DIYGridRowsPresenter.Template>
|
||||
<ControlTemplate TargetType="{x:Type bts:DIYGridRowsPresenter}">
|
||||
</c:DIYGridRowsPresenter.Resources>
|
||||
<c:DIYGridRowsPresenter.Template>
|
||||
<ControlTemplate TargetType="{x:Type c:DIYGridRowsPresenter}">
|
||||
<ScrollViewer Focusable="False">
|
||||
<ItemsPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
|
||||
</ScrollViewer>
|
||||
</ControlTemplate>
|
||||
</bts:DIYGridRowsPresenter.Template>
|
||||
</bts:DIYGridRowsPresenter>
|
||||
</c:DIYGridRowsPresenter.Template>
|
||||
</c:DIYGridRowsPresenter>
|
||||
<ScrollBar Grid.Row="3" Grid.Column="1" Orientation="Horizontal" LargeChange="10" x:Name="_scroll_horiz" Visibility="{TemplateBinding RowsHorizontalScrollBarVisibility}"/>
|
||||
</Grid>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
</ResourceDictionary>
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace mbts.ui.controls
|
||||
{
|
||||
@@ -7,6 +8,34 @@ namespace mbts.ui.controls
|
||||
/* smallest such that 1.0+DBL_EPSILON != 1.0 */
|
||||
private const double EPSILON = 2.2204460492503131e-016;
|
||||
private const double BIG_EPSILON = EPSILON * 10;
|
||||
|
||||
public static double nan_to_zero(double value) =>
|
||||
double.IsNaN(value) ? 0.0 : value;
|
||||
/// <summary>
|
||||
/// Returns <see cref="double.NaN"/> if both <paramref name="v1"/> and <paramref name="v2"/> are nan, otherwise returns the sum of the non-nan values (if only one value is nan, the other value is returned).
|
||||
/// </summary>
|
||||
/// <param name="v1">One of the two values to add.</param>
|
||||
/// <param name="v2">One of the two values to add.</param>
|
||||
/// <returns>The sum of the two values, with special handling for nans.</returns>
|
||||
public static double nan_to_zero_sum(double v1, double v2) =>
|
||||
double.IsNaN(v1) ?
|
||||
(double.IsNaN(v2) ? double.NaN : v2) :
|
||||
(double.IsNaN(v2) ? v1 : v1 + v2);
|
||||
|
||||
public static double nan_to_zero_sum(params double[] values) =>
|
||||
values.Aggregate(0.0, (sum, d) => double.IsNaN(d) ? sum : sum + d);
|
||||
|
||||
/// <summary>
|
||||
/// Returns <see cref="double.NaN"/> if both <paramref name="left"/> and <paramref name="right"/> are nan. Otherwise, if only one is nan, it is substituted for zero, and the difference of the resultant values is returned.
|
||||
/// </summary>
|
||||
/// <param name="left">The left operand (minuend) in the subtraction op.</param>
|
||||
/// <param name="right">The right operand (subtrahend) to be used in the subtraction op.</param>
|
||||
/// <returns>The difference of the two values, with special handling for nans.</returns>
|
||||
public static double nan_to_zero_diff(double left, double right) =>
|
||||
double.IsNaN(left) ?
|
||||
(double.IsNaN(right) ? double.NaN : -right) :
|
||||
(double.IsNaN(right) ? left : left - right);
|
||||
|
||||
internal static bool AreClose(double v1, double v2) {
|
||||
if (v1 == v2) return true;
|
||||
double delta = Math.Abs(v1 - v2);
|
||||
|
||||
@@ -60,6 +60,9 @@ namespace mbts.ui.settings {
|
||||
[DefaultValue(_default_horizontal_offset)]
|
||||
readonly double _horizontal_offset;
|
||||
|
||||
[ProtoMember(10)]
|
||||
readonly NullableColorProto _background_override;
|
||||
|
||||
public GridColumnSettings() {
|
||||
_is_visible = _default_is_visible;
|
||||
_is_collapsed = _default_is_collapsed;
|
||||
@@ -69,7 +72,7 @@ namespace mbts.ui.settings {
|
||||
}
|
||||
|
||||
public GridColumnSettings(string id, string display_name = null, bool is_visible = _default_is_visible, bool is_collapsed = _default_is_collapsed, FontWeight? font_weight_override = null, Color? font_color_override = null,
|
||||
double width = _default_width, HorizontalAlignment horizontal_alignment = _default_horizontal_alignment, double horizontal_offset = _default_horizontal_offset) {
|
||||
double width = _default_width, HorizontalAlignment horizontal_alignment = _default_horizontal_alignment, double horizontal_offset = _default_horizontal_offset, Color? background_override = null) {
|
||||
_id = id;
|
||||
_display_name = display_name;
|
||||
_is_visible = is_visible;
|
||||
@@ -79,6 +82,7 @@ namespace mbts.ui.settings {
|
||||
_width = width;
|
||||
_horizontal_alignment = horizontal_alignment;
|
||||
_horizontal_offset = horizontal_offset;
|
||||
_background_override = new NullableColorProto(background_override);
|
||||
}
|
||||
|
||||
public string id => _id;
|
||||
@@ -86,6 +90,7 @@ namespace mbts.ui.settings {
|
||||
public bool is_collapsed => _is_collapsed;
|
||||
public FontWeight? font_weight_override => _font_weight_override.value;
|
||||
public Color? font_color_override => _font_color_override.value;
|
||||
public Color? background_override => _background_override.argb;
|
||||
public double width => _width;
|
||||
public HorizontalAlignment horizontal_alignment => _horizontal_alignment;
|
||||
public double horizontal_offset => _horizontal_offset;
|
||||
@@ -93,7 +98,7 @@ namespace mbts.ui.settings {
|
||||
|
||||
public GridColumnSettings with(
|
||||
string display_name = null, bool? is_visible = null, bool? is_collapsed = null, NullableFontWeightProto? font_weight_override = null, NullableColorProto? font_color_override = null,
|
||||
double? width = null, HorizontalAlignment? horizontal_alignment = null, double? horizontal_offset = null) =>
|
||||
double? width = null, HorizontalAlignment? horizontal_alignment = null, double? horizontal_offset = null, NullableColorProto? background_override = null) =>
|
||||
//
|
||||
new GridColumnSettings(_id,
|
||||
display_name ?? this.display_name,
|
||||
@@ -103,7 +108,8 @@ namespace mbts.ui.settings {
|
||||
(font_color_override ?? _font_color_override).value,
|
||||
width ?? this.width,
|
||||
horizontal_alignment ?? this.horizontal_alignment,
|
||||
horizontal_offset ?? this.horizontal_offset);
|
||||
horizontal_offset ?? this.horizontal_offset,
|
||||
(background_override ?? _background_override).argb);
|
||||
|
||||
public bool Equals(GridColumnSettings other) =>
|
||||
other != null &&
|
||||
@@ -138,9 +144,5 @@ namespace mbts.ui.settings {
|
||||
public override string ToString() {
|
||||
return $"id: {id} display_name: {display_name}";
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -63,6 +63,11 @@ namespace mbts.spark {
|
||||
_dispose = Disposable.Create(() => predicate_animations.OfType<IDisposable>().DisposeAll());
|
||||
}
|
||||
|
||||
// TODO: hook up/impl
|
||||
// - currently, this is only used in the orderbook settings dialog
|
||||
// - will need to hook up if/when profiles are added to orderbook (or any grid settings dialog that uses this column type)
|
||||
event Action ISimpleColumnVM.result_changed { add { } remove { } }
|
||||
|
||||
public string id { get => _id; private set => Set(ref _id, value); }
|
||||
public double width { get => _width; set => Set(ref _width, value); }
|
||||
public HorizontalAlignment horizontal_alignment { get => _horizontal_alignment; set => Set(ref _horizontal_alignment, value); }
|
||||
@@ -86,8 +91,8 @@ namespace mbts.spark {
|
||||
can_override_display_name ? display_name : null,
|
||||
is_visible,
|
||||
null,
|
||||
is_font_weight_overridden ? new NullableFontWeightProto(font_weight_override) : default,
|
||||
is_font_color_overridden ? new NullableColorProto(font_color_override.color) : default,
|
||||
is_font_weight_overridden ? new NullableFontWeightProto(font_weight_override) : new NullableFontWeightProto(null),
|
||||
is_font_color_overridden ? new NullableColorProto(font_color_override.color) : new NullableColorProto(null),
|
||||
width,
|
||||
horizontal_alignment);
|
||||
var animations_lookup = predicate_animations.ToFlatLookup(pa => pa.predicate.create_assigned(owner._predicate_data_factory), pa => pa.animations.Select(a => a.create()));
|
||||
|
||||
@@ -15,7 +15,9 @@ namespace mbts.ui.settings {
|
||||
class GridSettings : IEquatable<GridSettings> {
|
||||
const string _default_font_family = "Segoe UI";
|
||||
const double _default_font_size = 11;
|
||||
const uint _default_font_color = 0;
|
||||
const uint
|
||||
_default_font_color = 0,
|
||||
_default_background = 0;
|
||||
const ushort _default_font_weight = 400;
|
||||
|
||||
public static GridSettings Default =>
|
||||
@@ -46,36 +48,53 @@ namespace mbts.ui.settings {
|
||||
public static IValueConverter font_weight_converter => ValueConverter.create<GridSettings, FontWeight>(gs => gs.font_weight);
|
||||
public static IValueConverter font_size_converter => ValueConverter.create<GridSettings, double>(gs => gs.font_size);
|
||||
public static IValueConverter font_color_converter => ValueConverter.create<GridSettings, SolidColorBrush>(gs => new SolidColorBrush(gs.font_color.rgb_color()).frozen());
|
||||
|
||||
// font family
|
||||
[ProtoMember(1)]
|
||||
[DefaultValue(_default_font_family)]
|
||||
readonly string _font_family;
|
||||
// font size
|
||||
[ProtoMember(2)]
|
||||
[DefaultValue(_default_font_size)]
|
||||
readonly double _font_size;
|
||||
// font weight
|
||||
[ProtoMember(3)]
|
||||
[DefaultValue(_default_font_weight)]
|
||||
readonly ushort _font_weight;
|
||||
// font color
|
||||
[ProtoMember(4)]
|
||||
[DefaultValue(_default_font_color)]
|
||||
readonly uint _font_color;
|
||||
// selection highlight
|
||||
[ProtoMember(5)]
|
||||
readonly ColorOpacityProto _selection_highlight;
|
||||
// columns
|
||||
[ProtoMember(6)]
|
||||
List<GridColumnSettings> _columns;
|
||||
// sorting criterion
|
||||
[ProtoMember(7)]
|
||||
List<SortingCriteria> _sorting_criterion;
|
||||
// background
|
||||
[ProtoMember(8)]
|
||||
[DefaultValue(_default_background)]
|
||||
uint _background;
|
||||
|
||||
public GridSettings() {
|
||||
_font_family = _default_font_family;
|
||||
_font_size = _default_font_size;
|
||||
_font_weight = _default_font_weight;
|
||||
_font_color = _default_font_color;
|
||||
_background = _default_background;
|
||||
}
|
||||
|
||||
public GridSettings(
|
||||
string font_family = _default_font_family, double font_size = _default_font_size, FontWeight font_weight = default, uint font_color = _default_font_color,
|
||||
ColorOpacityProto? selection_highlight = null, IEnumerable<GridColumnSettings> columns = null, IEnumerable<SortingCriteria> sorting_criterion = null) {
|
||||
string font_family = _default_font_family,
|
||||
double font_size = _default_font_size,
|
||||
FontWeight font_weight = default,
|
||||
uint font_color = _default_font_color,
|
||||
ColorOpacityProto? selection_highlight = null,
|
||||
IEnumerable<GridColumnSettings> columns = null,
|
||||
IEnumerable<SortingCriteria> sorting_criterion = null,
|
||||
uint background = _default_background) {
|
||||
//
|
||||
_font_family = font_family;
|
||||
_font_size = font_size;
|
||||
@@ -84,12 +103,14 @@ namespace mbts.ui.settings {
|
||||
_selection_highlight = selection_highlight ?? new ColorOpacityProto(0, 1);
|
||||
_columns = new List<GridColumnSettings>(columns.empty_if_null());
|
||||
_sorting_criterion = new List<SortingCriteria>(sorting_criterion.empty_if_null());
|
||||
_background = background;
|
||||
}
|
||||
|
||||
public string font_family => _font_family;
|
||||
public double font_size => _font_size;
|
||||
public uint font_color => _font_color;
|
||||
public FontWeight font_weight => FontWeight.FromOpenTypeWeight(_font_weight);
|
||||
public Color background => _background.argb_color();
|
||||
public ColorOpacityProto selection_highlight => _selection_highlight;
|
||||
public IReadOnlyList<GridColumnSettings> columns => _columns ?? (_columns = new List<GridColumnSettings>(0));
|
||||
[Obsolete] // TODO: remove sorting_criterion from GridSettings entirely. This should be part of IShellWindow's StartupData, since it is an instance-specific setting, and prob not part of the shared defaults
|
||||
@@ -98,7 +119,13 @@ namespace mbts.ui.settings {
|
||||
public static GridSettings get_default() =>
|
||||
new GridSettings(_default_font_family, _default_font_size, FontWeights.Normal, _default_font_color, new ColorOpacityProto(Colors.MediumSlateBlue, 0.4));
|
||||
|
||||
public GridSettings with(IEnumerable<GridColumnSettings> columns = null, IEnumerable<SortingCriteria> sorting_criterion = null, string font_family = null, double font_size = double.NaN) =>
|
||||
public GridSettings with(
|
||||
IEnumerable<GridColumnSettings> columns = null,
|
||||
IEnumerable<SortingCriteria> sorting_criterion = null,
|
||||
string font_family = null,
|
||||
double font_size = double.NaN,
|
||||
uint? background = null) =>
|
||||
//
|
||||
new GridSettings(
|
||||
font_family ?? _font_family,
|
||||
DoubleHelper.IsNaNOrInfinityOr(font_size, 0) ? _font_size : font_size,
|
||||
@@ -106,7 +133,8 @@ namespace mbts.ui.settings {
|
||||
_font_color,
|
||||
_selection_highlight,
|
||||
columns ?? _columns,
|
||||
sorting_criterion ?? _sorting_criterion);
|
||||
sorting_criterion ?? _sorting_criterion,
|
||||
background ?? _background);
|
||||
|
||||
public GridSettings clone() => with();
|
||||
|
||||
@@ -117,6 +145,7 @@ namespace mbts.ui.settings {
|
||||
font_weight == other.font_weight &&
|
||||
font_color == other.font_color &&
|
||||
selection_highlight == other.selection_highlight &&
|
||||
_background == other._background &&
|
||||
EqualityComparer.safe_sequence_equals(columns, other.columns) &&
|
||||
#pragma warning disable CS0612 // Type or member is obsolete
|
||||
EqualityComparer.safe_sequence_equals(sorting_criterion, other.sorting_criterion);
|
||||
@@ -132,6 +161,7 @@ namespace mbts.ui.settings {
|
||||
font_weight.GetHashCode(),
|
||||
font_color.GetHashCode(),
|
||||
selection_highlight.GetHashCode(),
|
||||
_background.GetHashCode(),
|
||||
EqualityComparer.safe_aggregate_hash(columns),
|
||||
#pragma warning disable CS0612 // Type or member is obsolete
|
||||
EqualityComparer.safe_aggregate_hash(sorting_criterion));
|
||||
|
||||
@@ -1,27 +1,80 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace mbts.ui.settings {
|
||||
static class GridSettingsHelper {
|
||||
public static TColKey? parse_col_id<TColKey>(string id) where TColKey : struct =>
|
||||
Enum.TryParse(id, true, out TColKey key) ? key : (TColKey?)null;
|
||||
|
||||
public static string fmt_col_id<TColKey>(TColKey key) where TColKey : struct =>
|
||||
key.ToString();
|
||||
|
||||
public static void normalize_sorting_criterion<TColKey>(IEnumerable<SortingCriteria> sorting_criterion, IReadOnlyDictionary<string, TColKey> sort_member_to_id) where TColKey : struct =>
|
||||
normalize_sorting_criterion(sorting_criterion, new Func<string, TColKey?>[] {
|
||||
parse_col_id<TColKey>,
|
||||
col_name => sort_member_to_id.TryGetValue(col_name, out var k) ? k : (TColKey?)null
|
||||
});
|
||||
|
||||
public static void normalize_sorting_criterion<TColKey>(IEnumerable<SortingCriteria> sorting_criterion, IEnumerable<Func<string, TColKey?>> conversion_dels) where TColKey : struct {
|
||||
foreach (var sc in sorting_criterion) {
|
||||
sc.col_name = conversion_dels
|
||||
.Select(del => del(sc.col_name))
|
||||
.FirstOrDefault(k => k.HasValue)?.ToString() ?? sc.col_name;
|
||||
}
|
||||
}
|
||||
}
|
||||
using mbts.ui.controls;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace mbts.ui.settings {
|
||||
static class GridSettingsHelper {
|
||||
public static TColKey? parse_col_id<TColKey>(string id) where TColKey : struct =>
|
||||
Enum.TryParse(id, true, out TColKey key) ? key : (TColKey?)null;
|
||||
|
||||
public static TColKey strong_id<TColKey>(this GridColumnSettings column_settings) where TColKey : struct =>
|
||||
parse_col_id<TColKey>(column_settings.id) ?? throw new ArgumentException($"Failed to parse column settings' id '{column_settings.id}' to type '{typeof(TColKey)}.'");
|
||||
|
||||
public static string fmt_col_id<TColKey>(TColKey key) where TColKey : struct =>
|
||||
key.ToString();
|
||||
|
||||
public static void normalize_sorting_criterion<TColKey>(IEnumerable<SortingCriteria> sorting_criterion, IReadOnlyDictionary<string, TColKey> sort_member_to_id) where TColKey : struct =>
|
||||
normalize_sorting_criterion(sorting_criterion, new Func<string, TColKey?>[] {
|
||||
parse_col_id<TColKey>,
|
||||
col_name => sort_member_to_id.TryGetValue(col_name, out var k) ? k : (TColKey?)null
|
||||
});
|
||||
|
||||
public static void normalize_sorting_criterion<TColKey>(IEnumerable<SortingCriteria> sorting_criterion, IEnumerable<Func<string, TColKey?>> conversion_dels) where TColKey : struct {
|
||||
foreach (var sc in sorting_criterion) {
|
||||
sc.col_name = conversion_dels
|
||||
.Select(del => del(sc.col_name))
|
||||
.FirstOrDefault(k => k.HasValue)?.ToString() ?? sc.col_name;
|
||||
}
|
||||
}
|
||||
|
||||
public static void load_column_settings(IEnumerable<DIYGrid.ColumnBase> columns, GridSettings grid_settings) =>
|
||||
load_column_settings(columns, grid_settings.columns, new FontFamily(grid_settings.font_family), grid_settings.font_size, grid_settings.font_color.rgb_color(), grid_settings.font_weight);
|
||||
|
||||
public static void load_column_settings(IEnumerable<DIYGrid.ColumnBase> columns, IEnumerable<GridColumnSettings> settings, FontFamily grid_font_family, double grid_font_size, Color grid_font_color, FontWeight grid_font_weight) =>
|
||||
load_column_settings(columns, settings, grid_font_family, grid_font_size, grid_font_color, grid_font_weight, (c, s) => c.load_from(s));
|
||||
|
||||
public static void load_column_settings<TColumn>(
|
||||
IEnumerable<TColumn> columns,
|
||||
IEnumerable<GridColumnSettings> settings,
|
||||
FontFamily grid_font_family,
|
||||
double grid_font_size,
|
||||
Color grid_font_color,
|
||||
FontWeight grid_font_weight,
|
||||
Action<TColumn, GridColumnSettings> load_impl)
|
||||
where TColumn : DIYGrid.ColumnBase {
|
||||
//
|
||||
var join =
|
||||
from stg in settings
|
||||
join col in columns on stg.id equals col.id
|
||||
select new { col, stg };
|
||||
foreach (var tup in join) {
|
||||
load_impl(tup.col, tup.stg);
|
||||
tup.col.set_fallback_font_params(grid_font_family, grid_font_size, grid_font_color, grid_font_weight);
|
||||
}
|
||||
}
|
||||
|
||||
public static bool try_find_column<T>(IEnumerable<GridColumnSettings> columns, T col_key, out GridColumnSettings result) where T : struct {
|
||||
var key_str = col_key.ToString();
|
||||
return columns.TryGetFirst(c => c.id == key_str, out result);
|
||||
}
|
||||
|
||||
public static bool try_find_column<T>(IEnumerable<GridColumnSettings> columns, T col_key, out GridColumnSettings result, out int index) where T : struct {
|
||||
var key_str = col_key.ToString();
|
||||
return columns.TryGetFirst(c => c.id == key_str, out result, out index);
|
||||
}
|
||||
|
||||
public static bool try_replace_column<T>(IList<GridColumnSettings> columns, T col_key, Func<GridColumnSettings, GridColumnSettings> replace) {
|
||||
var key_str = col_key.ToString();
|
||||
for (int i = 0; i < columns.Count; i++) {
|
||||
if (columns[i].id == key_str) {
|
||||
columns[i] = replace(columns[i]);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,8 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:proto="clr-namespace:bts.proto"
|
||||
xmlns:m="clr-namespace:mbts"
|
||||
xmlns:sp="clr-namespace:mbts.spark"
|
||||
xmlns:pnl="clr-namespace:bts.shell.pnl"
|
||||
xmlns:u="clr-namespace:mbts.ui"
|
||||
xmlns:c="clr-namespace:mbts.ui.controls"
|
||||
xmlns:cv="clr-namespace:mbts.ui.converters"
|
||||
@@ -314,6 +316,357 @@
|
||||
/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate x:Key="DefaultCellContentTemplate">
|
||||
<Grid>
|
||||
<u:StateManager.Groups>
|
||||
<u:StateGroupsManager AttachToMembers="True">
|
||||
<u:StateDefinitionCollection>
|
||||
<u:StateDefinition State="{Binding Path='Group.Target.DataContext.can_override_display_name', RelativeSource={RelativeSource Self}, Converter={x:Static usp:GridSettingsView.DisplayNameVisualStateConverter}}"/>
|
||||
</u:StateDefinitionCollection>
|
||||
</u:StateGroupsManager>
|
||||
</u:StateManager.Groups>
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup Name="{x:Static GroupName, MemberType={u:Nested usp:GridSettingsView.VisualStateNames.DisplayName}}">
|
||||
<VisualState Name="{x:Static Active, MemberType={u:Nested usp:GridSettingsView.VisualStateNames.DisplayName}}"/>
|
||||
<VisualState Name="{x:Static Inactive, MemberType={u:Nested usp:GridSettingsView.VisualStateNames.DisplayName}}">
|
||||
<Storyboard>
|
||||
<BooleanAnimationUsingKeyFrames Storyboard.TargetName="display_name_text_box" Storyboard.TargetProperty="(UIElement.IsEnabled)">
|
||||
<DiscreteBooleanKeyFrame KeyTime="0" Value="False"/>
|
||||
</BooleanAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="display_name_text_box" Storyboard.TargetProperty="(FrameworkElement.ToolTip)">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="(N/A)"/>
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
</Storyboard>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="{Binding Source={StaticResource VerticalPush}}"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="{Binding Source={StaticResource VerticalPush}}"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="{Binding Source={StaticResource VerticalPush}}"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="{Binding Source={StaticResource VerticalPush}}"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="{Binding Source={StaticResource HorizontalSplit}}" />
|
||||
<ColumnDefinition/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock>Header</TextBlock>
|
||||
<TextBox x:Name="display_name_text_box" Grid.Column="2" ToolTipService.ShowOnDisabled="True" Text="{Binding display_name}"/>
|
||||
<TextBlock Grid.Row="2">Width</TextBlock>
|
||||
<c:TickUpDownDouble Grid.Row="2" Grid.Column="2" Value="{Binding width}" Format="0.##"/>
|
||||
<TextBlock Grid.Row="4">Horizontal Alignment</TextBlock>
|
||||
<ComboBox Grid.Row="4" Grid.Column="2" ItemsSource="{u:EnumBindingSource Type=HorizontalAlignment}" SelectedItem="{Binding horizontal_alignment}"/>
|
||||
<CheckBox Grid.Row="6" VerticalAlignment="Center" IsEnabled="{Binding can_override_font_weight}" IsChecked="{Binding is_font_weight_overridden}">Override Font Weight</CheckBox>
|
||||
<ComboBox
|
||||
Grid.Row="6"
|
||||
Grid.Column="2"
|
||||
Visibility="{Binding is_font_weight_overridden, Converter={StaticResource BooleanToVisibilityConverter}}"
|
||||
ItemsSource="{Binding DataContext.font_family, Converter={StaticResource FontFamilyWeightsConverter}, RelativeSource={RelativeSource AncestorType=usp:GridSettingsView}}"
|
||||
SelectedItem="{Binding font_weight_override}"
|
||||
/>
|
||||
<ComboBox Grid.Row="6" Grid.Column="2" Visibility="{Binding is_font_weight_overridden, Converter={StaticResource FalseVisibilityConverter}}" Style="{StaticResource DisabledPlaceholderComboBox}"/>
|
||||
<CheckBox Grid.Row="8" VerticalAlignment="Top" IsEnabled="{Binding can_override_font_color}" IsChecked="{Binding is_font_color_overridden}">Override Font Color</CheckBox>
|
||||
<c:ColorPickerPopup
|
||||
Grid.Row="8"
|
||||
Grid.Column="2"
|
||||
DataContext="{Binding font_color_override}"
|
||||
VerticalAlignment="Top"
|
||||
Visibility="{Binding DataContext.current_column.is_font_color_overridden, Converter={StaticResource BooleanToVisibilityConverter}, RelativeSource={RelativeSource AncestorType=usp:GridSettingsView}}"
|
||||
/>
|
||||
<ComboBox
|
||||
Grid.Row="8"
|
||||
Grid.Column="2"
|
||||
VerticalAlignment="Top"
|
||||
Visibility="{Binding is_font_color_overridden, Converter={StaticResource FalseVisibilityConverter}}"
|
||||
Style="{StaticResource DisabledPlaceholderComboBox}"
|
||||
/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate DataType="{x:Type pnl:PNLColumnParamsVM}">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="{Binding Source={StaticResource VerticalPush}}"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="{Binding Source={StaticResource VerticalPush}}"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="name_col"/>
|
||||
<ColumnDefinition Width="{Binding Source={StaticResource HorizontalSplit}}"/>
|
||||
<ColumnDefinition/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<CheckBox Grid.Row="0" VerticalAlignment="Center" IsEnabled="{Binding can_override_font_weight}" IsChecked="{Binding is_font_weight_overridden}">Override Font Weight</CheckBox>
|
||||
<ComboBox
|
||||
Grid.Row="0"
|
||||
Grid.Column="2"
|
||||
Visibility="{Binding is_font_weight_overridden, Converter={StaticResource BooleanToVisibilityConverter}}"
|
||||
ItemsSource="{Binding DataContext.font_family, RelativeSource={RelativeSource AncestorType=usp:GridSettingsView}, Converter={StaticResource FontFamilyWeightsConverter}}"
|
||||
SelectedItem="{Binding font_weight_override}"
|
||||
/>
|
||||
<ComboBox Grid.Row="0" Grid.Column="2" Visibility="{Binding is_font_weight_overridden, Converter={StaticResource FalseVisibilityConverter}}" Style="{StaticResource DisabledPlaceholderComboBox}"/>
|
||||
<CheckBox Grid.Row="2" VerticalAlignment="Top" IsChecked="{Binding is_font_color_overridden}">Override Font Color</CheckBox>
|
||||
<c:ColorPickerPopup
|
||||
Grid.Row="2"
|
||||
Grid.Column="2"
|
||||
SelectedColor="{Binding font_color_override.color}"
|
||||
Palettes="{Binding font_color_override.palettes}"
|
||||
Visibility="{Binding is_font_color_overridden, Converter={StaticResource BooleanToVisibilityConverter}}"
|
||||
/>
|
||||
<ComboBox
|
||||
Grid.Row="2"
|
||||
Grid.Column="2"
|
||||
Visibility="{Binding is_font_color_overridden, Converter={StaticResource FalseVisibilityConverter}}"
|
||||
Style="{StaticResource DisabledPlaceholderComboBox}"
|
||||
/>
|
||||
<CheckBox Grid.Row="4" VerticalAlignment="Top" IsChecked="{Binding is_background_overridden}">Override Background</CheckBox>
|
||||
<c:ColorPickerPopup
|
||||
Grid.Row="4"
|
||||
Grid.Column="2"
|
||||
SelectedColor="{Binding background_override.color}"
|
||||
Palettes="{Binding background_override.palettes}"
|
||||
IsEnabled="{Binding is_background_overridden}"
|
||||
/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate x:Key="PNLColumnTemplate" DataType="{x:Type pnl:PNLColumnVM}">
|
||||
<StackPanel Grid.IsSharedSizeScope="True">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="{Binding Source={StaticResource VerticalPush}}"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="{Binding Source={StaticResource VerticalPush}}"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="name_col"/>
|
||||
<ColumnDefinition Width="{Binding Source={StaticResource HorizontalSplit}}"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock>Header</TextBlock>
|
||||
<TextBox x:Name="display_name_text_box" Grid.Column="2" ToolTipService.ShowOnDisabled="True" Text="{Binding display_name}"/>
|
||||
<TextBlock Grid.Row="2">Width</TextBlock>
|
||||
<c:TickUpDownDouble Grid.Row="2" Grid.Column="2" Value="{Binding width}" Format="0.##"/>
|
||||
<TextBlock Grid.Row="4">Horizontal Alignment</TextBlock>
|
||||
<ComboBox Grid.Row="4" Grid.Column="2" ItemsSource="{u:EnumBindingSource Type=HorizontalAlignment}" SelectedItem="{Binding horizontal_alignment}"/>
|
||||
</Grid>
|
||||
<GroupBox Margin="0,8,0,0" Header="Rows" Content="{Binding default_column_vm}"/>
|
||||
<GroupBox Margin="0,8,0,0" Header="Group Header Rows" Content="{Binding group_column_vm}"/>
|
||||
<GroupBox Margin="0,8,0,0" Header="Total/Selected Rows" Content="{Binding header_column_vm}"/>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate DataType="{x:Type pnl:PNLRuleColumnVM}">
|
||||
<StackPanel Grid.IsSharedSizeScope="True">
|
||||
<GroupBox Header="Rows" DataContext="{Binding row_params}">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="{Binding Source={StaticResource VerticalPush}}"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="{Binding Source={StaticResource VerticalPush}}"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="name_col"/>
|
||||
<ColumnDefinition Width="{Binding Source={StaticResource HorizontalSplit}}" />
|
||||
<ColumnDefinition/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<CheckBox Grid.Row="0" VerticalAlignment="Center" IsEnabled="{Binding can_override_font_weight}" IsChecked="{Binding is_font_weight_overridden}">Override Font Weight</CheckBox>
|
||||
<ComboBox
|
||||
Grid.Row="0"
|
||||
Grid.Column="2"
|
||||
Visibility="{Binding is_font_weight_overridden, Converter={StaticResource BooleanToVisibilityConverter}}"
|
||||
ItemsSource="{Binding DataContext.font_family, RelativeSource={RelativeSource AncestorType=usp:GridSettingsView}, Converter={StaticResource FontFamilyWeightsConverter}}"
|
||||
SelectedItem="{Binding font_weight_override}"
|
||||
/>
|
||||
<!--Group-->
|
||||
<ComboBox Grid.Row="0" Grid.Column="2" Visibility="{Binding is_font_weight_overridden, Converter={StaticResource FalseVisibilityConverter}}" Style="{StaticResource DisabledPlaceholderComboBox}"/>
|
||||
<CheckBox Grid.Row="2" VerticalAlignment="Top" IsChecked="{Binding is_font_color_overridden}">Override Font Color</CheckBox>
|
||||
<c:ColorPickerPopup
|
||||
Grid.Row="2"
|
||||
Grid.Column="2"
|
||||
VerticalAlignment="Top"
|
||||
SelectedColor="{Binding font_color_override.color}"
|
||||
Palettes="{Binding font_color_override.palettes}"
|
||||
Visibility="{Binding is_font_color_overridden, Converter={StaticResource BooleanToVisibilityConverter}}"
|
||||
/>
|
||||
<ComboBox
|
||||
Grid.Row="2"
|
||||
Grid.Column="2"
|
||||
VerticalAlignment="Top"
|
||||
Visibility="{Binding is_font_color_overridden, Converter={StaticResource FalseVisibilityConverter}}"
|
||||
Style="{StaticResource DisabledPlaceholderComboBox}"
|
||||
/>
|
||||
<CheckBox Grid.Row="4" VerticalAlignment="Top" IsChecked="{Binding is_background_overridden}">Override Background</CheckBox>
|
||||
<c:ColorPickerPopup
|
||||
Grid.Row="4"
|
||||
Grid.Column="2"
|
||||
VerticalAlignment="Top"
|
||||
SelectedColor="{Binding background_override.color}"
|
||||
Palettes="{Binding background_override.palettes}"
|
||||
Visibility="{Binding is_background_overridden, Converter={StaticResource BooleanToVisibilityConverter}}"
|
||||
/>
|
||||
</Grid>
|
||||
</GroupBox>
|
||||
<GroupBox Header="Group Header Rows" DataContext="{Binding group_params}">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="{Binding Source={StaticResource VerticalPush}}"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="{Binding Source={StaticResource VerticalPush}}"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="name_col"/>
|
||||
<ColumnDefinition Width="{Binding Source={StaticResource HorizontalSplit}}" />
|
||||
<ColumnDefinition/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<CheckBox Grid.Row="0" VerticalAlignment="Center" IsEnabled="{Binding can_override_font_weight}" IsChecked="{Binding is_font_weight_overridden}">Override Font Weight</CheckBox>
|
||||
<ComboBox
|
||||
Grid.Row="0"
|
||||
Grid.Column="2"
|
||||
Visibility="{Binding is_font_weight_overridden, Converter={StaticResource BooleanToVisibilityConverter}}"
|
||||
ItemsSource="{Binding DataContext.font_family, RelativeSource={RelativeSource AncestorType=usp:GridSettingsView}, Converter={StaticResource FontFamilyWeightsConverter}}"
|
||||
SelectedItem="{Binding font_weight_override}"
|
||||
/>
|
||||
<!--Group-->
|
||||
<ComboBox Grid.Row="0" Grid.Column="2" Visibility="{Binding is_font_weight_overridden, Converter={StaticResource FalseVisibilityConverter}}" Style="{StaticResource DisabledPlaceholderComboBox}"/>
|
||||
<CheckBox Grid.Row="2" VerticalAlignment="Top" IsChecked="{Binding is_font_color_overridden}">Override Font Color</CheckBox>
|
||||
<c:ColorPickerPopup
|
||||
Grid.Row="2"
|
||||
Grid.Column="2"
|
||||
VerticalAlignment="Top"
|
||||
SelectedColor="{Binding font_color_override.color}"
|
||||
Palettes="{Binding font_color_override.palettes}"
|
||||
Visibility="{Binding is_font_color_overridden, Converter={StaticResource BooleanToVisibilityConverter}}"
|
||||
/>
|
||||
<ComboBox
|
||||
Grid.Row="2"
|
||||
Grid.Column="2"
|
||||
VerticalAlignment="Top"
|
||||
Visibility="{Binding is_font_color_overridden, Converter={StaticResource FalseVisibilityConverter}}"
|
||||
Style="{StaticResource DisabledPlaceholderComboBox}"
|
||||
/>
|
||||
<CheckBox Grid.Row="4" VerticalAlignment="Top" IsChecked="{Binding is_background_overridden}">Override Background</CheckBox>
|
||||
<c:ColorPickerPopup
|
||||
Grid.Row="4"
|
||||
Grid.Column="2"
|
||||
VerticalAlignment="Top"
|
||||
SelectedColor="{Binding background_override.color}"
|
||||
Palettes="{Binding background_override.palettes}"
|
||||
Visibility="{Binding is_background_overridden, Converter={StaticResource BooleanToVisibilityConverter}}"
|
||||
/>
|
||||
</Grid>
|
||||
</GroupBox>
|
||||
<GroupBox Header="Total/Selected Rows" DataContext="{Binding header_params}">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="{Binding Source={StaticResource VerticalPush}}"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="{Binding Source={StaticResource VerticalPush}}"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="name_col"/>
|
||||
<ColumnDefinition Width="{Binding Source={StaticResource HorizontalSplit}}" />
|
||||
<ColumnDefinition/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<CheckBox Grid.Row="0" VerticalAlignment="Center" IsEnabled="{Binding can_override_font_weight}" IsChecked="{Binding is_font_weight_overridden}">Override Font Weight</CheckBox>
|
||||
<ComboBox
|
||||
Grid.Row="0"
|
||||
Grid.Column="2"
|
||||
Visibility="{Binding is_font_weight_overridden, Converter={StaticResource BooleanToVisibilityConverter}}"
|
||||
ItemsSource="{Binding DataContext.font_family, RelativeSource={RelativeSource AncestorType=usp:GridSettingsView}, Converter={StaticResource FontFamilyWeightsConverter}}"
|
||||
SelectedItem="{Binding font_weight_override}"
|
||||
/>
|
||||
<!--Group-->
|
||||
<ComboBox Grid.Row="0" Grid.Column="2" Visibility="{Binding is_font_weight_overridden, Converter={StaticResource FalseVisibilityConverter}}" Style="{StaticResource DisabledPlaceholderComboBox}"/>
|
||||
<CheckBox Grid.Row="2" VerticalAlignment="Top" IsChecked="{Binding is_font_color_overridden}">Override Font Color</CheckBox>
|
||||
<c:ColorPickerPopup
|
||||
Grid.Row="2"
|
||||
Grid.Column="2"
|
||||
VerticalAlignment="Top"
|
||||
SelectedColor="{Binding font_color_override.color}"
|
||||
Palettes="{Binding font_color_override.palettes}"
|
||||
Visibility="{Binding is_font_color_overridden, Converter={StaticResource BooleanToVisibilityConverter}}"
|
||||
/>
|
||||
<ComboBox
|
||||
Grid.Row="2"
|
||||
Grid.Column="2"
|
||||
VerticalAlignment="Top"
|
||||
Visibility="{Binding is_font_color_overridden, Converter={StaticResource FalseVisibilityConverter}}"
|
||||
Style="{StaticResource DisabledPlaceholderComboBox}"
|
||||
/>
|
||||
<CheckBox Grid.Row="4" VerticalAlignment="Top" IsChecked="{Binding is_background_overridden}">Override Background</CheckBox>
|
||||
<c:ColorPickerPopup
|
||||
Grid.Row="4"
|
||||
Grid.Column="2"
|
||||
VerticalAlignment="Top"
|
||||
SelectedColor="{Binding background_override.color}"
|
||||
Palettes="{Binding background_override.palettes}"
|
||||
Visibility="{Binding is_background_overridden, Converter={StaticResource BooleanToVisibilityConverter}}"
|
||||
/>
|
||||
<ComboBox
|
||||
Grid.Row="2"
|
||||
Grid.Column="2"
|
||||
VerticalAlignment="Top"
|
||||
Visibility="{Binding is_background_overridden, Converter={StaticResource FalseVisibilityConverter}}"
|
||||
Style="{StaticResource DisabledPlaceholderComboBox}"
|
||||
/>
|
||||
</Grid>
|
||||
</GroupBox>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate x:Key="PNLCellContentTemplate" DataType="{x:Type pnl:PNLColumnVM}">
|
||||
<!--The 'Delay' addresses a side effect (it's a wpf bug) when changing the SelectedItem in TabControls,
|
||||
where null values are propagated to binding targets of the previous SelectedItem -->
|
||||
<TabControl x:Name="column_tabs" Padding="8" SelectedItem="{Binding selected_item, Delay=100}">
|
||||
<TabControl.ItemContainerStyle>
|
||||
<Style TargetType="TabItem">
|
||||
<Setter Property="HeaderTemplateSelector">
|
||||
<Setter.Value>
|
||||
<u:DataTypeTemplateSelector>
|
||||
<DataTemplate DataType="{x:Type pnl:PNLRuleColumnVM}">
|
||||
<TextBlock Text="{Binding rule_text, Converter={x:Static ut:DisplayNameHelper.ExpressionSnakeToTitleCaseConverter}}"/>
|
||||
</DataTemplate>
|
||||
<u:DataTypeTemplateSelector.Default>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding}"/>
|
||||
</DataTemplate>
|
||||
</u:DataTypeTemplateSelector.Default>
|
||||
</u:DataTypeTemplateSelector>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</TabControl.ItemContainerStyle>
|
||||
<TabControl.ItemsSource>
|
||||
<CompositeCollection>
|
||||
<CollectionContainer>
|
||||
<CollectionContainer.Collection>
|
||||
<x:Array Type="{x:Type TabItem}">
|
||||
<TabItem Header="Default" Content="{Binding}" ContentTemplate="{StaticResource PNLColumnTemplate}"/>
|
||||
</x:Array>
|
||||
</CollectionContainer.Collection>
|
||||
</CollectionContainer>
|
||||
<CollectionContainer Collection="{Binding DataContext.rule_columns, Source={u:DirectReference column_tabs}}"/>
|
||||
</CompositeCollection>
|
||||
</TabControl.ItemsSource>
|
||||
</TabControl>
|
||||
</DataTemplate>
|
||||
</ResourceDictionary>
|
||||
</UserControl.Resources>
|
||||
<Grid Margin="{StaticResource FullPadding}">
|
||||
@@ -554,7 +907,7 @@
|
||||
<ColumnDefinition Width="{Binding Source={StaticResource HorizontalGap}}" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<c:DragDropListBox Grid.RowSpan="3" MinWidth="200" ItemsSource="{Binding columns}" SelectedItem="{Binding current_column}">
|
||||
<c:DragDropListBox Grid.RowSpan="3" MinWidth="200" MaxHeight="500" ItemsSource="{Binding columns}" SelectedItem="{Binding current_column}">
|
||||
<c:DragDropListBox.ItemContainerStyle>
|
||||
<Style TargetType="c:DraggableListBoxItem">
|
||||
<Setter Property="Cursor" Value="Arrow"/>
|
||||
@@ -590,10 +943,10 @@
|
||||
<ColumnDefinition Width="{Binding Source={StaticResource HorizontalPush}}"/>
|
||||
<ColumnDefinition/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<CheckBox x:Name="visibility_toggle" ToolTipService.ShowOnDisabled="True" IsChecked="{Binding is_visible}"/>
|
||||
<StackPanel Grid.Column="2" Orientation="Horizontal">
|
||||
<TextBlock Text="{Binding id, Converter={x:Static ut:DisplayNameHelper.CamelCaseSpacesConverter}}"/>
|
||||
<TextBlock Margin="2,0,0,0" FontStyle="Italic" Text="{Binding display_name, Mode=OneWay, StringFormat='{} ({0})'}">
|
||||
<CheckBox x:Name="visibility_toggle" ToolTipService.ShowOnDisabled="True" IsChecked="{Binding is_visible}" Focusable="False"/>
|
||||
<StackPanel Grid.Column="2" Orientation="Horizontal" Focusable="False">
|
||||
<TextBlock Text="{Binding id, Converter={x:Static ut:DisplayNameHelper.CamelCaseSpacesConverter}}" Focusable="False"/>
|
||||
<TextBlock Margin="2,0,0,0" FontStyle="Italic" Text="{Binding display_name, Mode=OneWay, StringFormat='{} ({0})'}" Focusable="False">
|
||||
<TextBlock.Visibility>
|
||||
<MultiBinding Converter="{x:Static usp:GridSettingsView.ColumnDisplayNameVisibilityConverter}">
|
||||
<Binding Path="id" Converter="{x:Static ut:DisplayNameHelper.CamelCaseSpacesConverter}"/>
|
||||
@@ -606,73 +959,10 @@
|
||||
</DataTemplate>
|
||||
</c:DragDropListBox.ItemTemplate>
|
||||
</c:DragDropListBox>
|
||||
<GroupBox Grid.Column="2" MinWidth="280">
|
||||
<GroupBox Grid.Column="2" MinWidth="280" Content="{Binding current_column}" ContentTemplate="{Binding CellContentTemplate, RelativeSource={RelativeSource AncestorType=usp:GridSettingsView}}">
|
||||
<GroupBox.Header>
|
||||
<TextBlock Text="{Binding current_column.display_name, StringFormat='{}\'{0}\' Cell Appearance'}"/>
|
||||
</GroupBox.Header>
|
||||
<Grid x:Name="column_content_root">
|
||||
<u:StateManager.Groups>
|
||||
<u:StateGroupsManager>
|
||||
<u:StateDefinitionCollection>
|
||||
<u:StateDefinition State="{Binding DataContext.current_column.can_override_display_name, Source={u:DirectReference column_content_root}, Converter={x:Static usp:GridSettingsView.DisplayNameVisualStateConverter}}"/>
|
||||
</u:StateDefinitionCollection>
|
||||
</u:StateGroupsManager>
|
||||
</u:StateManager.Groups>
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup Name="{x:Static GroupName, MemberType={u:Nested usp:GridSettingsView.VisualStateNames.DisplayName}}">
|
||||
<VisualState Name="{x:Static Active, MemberType={u:Nested usp:GridSettingsView.VisualStateNames.DisplayName}}"/>
|
||||
<VisualState Name="{x:Static Inactive, MemberType={u:Nested usp:GridSettingsView.VisualStateNames.DisplayName}}">
|
||||
<Storyboard>
|
||||
<BooleanAnimationUsingKeyFrames Storyboard.TargetName="display_name_text_box" Storyboard.TargetProperty="(UIElement.IsEnabled)">
|
||||
<DiscreteBooleanKeyFrame KeyTime="0" Value="False"/>
|
||||
</BooleanAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="display_name_text_box" Storyboard.TargetProperty="(FrameworkElement.ToolTip)">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="(N/A)"/>
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
</Storyboard>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="{Binding Source={StaticResource VerticalPush}}"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="{Binding Source={StaticResource VerticalPush}}"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="{Binding Source={StaticResource VerticalPush}}"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="{Binding Source={StaticResource VerticalPush}}"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="{Binding Source={StaticResource HorizontalSplit}}" />
|
||||
<ColumnDefinition/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock>Header</TextBlock>
|
||||
<TextBox x:Name="display_name_text_box" Grid.Column="2" ToolTipService.ShowOnDisabled="True" Text="{Binding current_column.display_name}"/>
|
||||
<TextBlock Grid.Row="2">Width</TextBlock>
|
||||
<c:TickUpDownDouble Grid.Row="2" Grid.Column="2" Value="{Binding current_column.width}"/>
|
||||
<TextBlock Grid.Row="4">Horizontal Alignment</TextBlock>
|
||||
<ComboBox Grid.Row="4" Grid.Column="2" ItemsSource="{u:EnumBindingSource Type=HorizontalAlignment}" SelectedItem="{Binding current_column.horizontal_alignment}"/>
|
||||
<CheckBox Grid.Row="6" VerticalAlignment="Center" IsEnabled="{Binding current_column.can_override_font_weight}" IsChecked="{Binding current_column.is_font_weight_overridden}">Override Font Weight</CheckBox>
|
||||
<ComboBox
|
||||
Grid.Row="6"
|
||||
Grid.Column="2"
|
||||
Visibility="{Binding current_column.is_font_weight_overridden, Converter={StaticResource BooleanToVisibilityConverter}}"
|
||||
ItemsSource="{Binding font_family, Converter={StaticResource FontFamilyWeightsConverter}}"
|
||||
SelectedItem="{Binding current_column.font_weight_override}"
|
||||
/>
|
||||
<ComboBox Grid.Row="6" Grid.Column="2" Style="{StaticResource DisabledPlaceholderComboBox}" Visibility="{Binding current_column.is_font_weight_overridden, Converter={StaticResource FalseVisibilityConverter}}"/>
|
||||
<CheckBox Grid.Row="8" VerticalAlignment="Center" IsEnabled="{Binding current_column.can_override_font_color}" IsChecked="{Binding current_column.is_font_color_overridden}">Override Font Color</CheckBox>
|
||||
<c:ColorPickerPopup
|
||||
Grid.Row="8"
|
||||
Grid.Column="2"
|
||||
DataContext="{Binding current_column.font_color_override}"
|
||||
Visibility="{Binding DataContext.current_column.is_font_color_overridden, Converter={StaticResource BooleanToVisibilityConverter}, RelativeSource={RelativeSource AncestorType=usp:GridSettingsView}}"
|
||||
/>
|
||||
<ComboBox Grid.Row="8" Grid.Column="2" Style="{StaticResource DisabledPlaceholderComboBox}" Visibility="{Binding current_column.is_font_color_overridden, Converter={StaticResource FalseVisibilityConverter}}"/>
|
||||
</Grid>
|
||||
</GroupBox>
|
||||
<!--TODO: Make this visible when cell animations are ready-->
|
||||
<GroupBox Grid.Row="2" Grid.Column="2" VerticalAlignment="Top" Visibility="Collapsed">
|
||||
|
||||
@@ -38,6 +38,16 @@ namespace mbts.ui.spark {
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region CellContentTemplate
|
||||
public static readonly DependencyProperty CellContentTemplateProperty =
|
||||
WPFHelper.create_dp<DataTemplate, GridSettingsView>(nameof(CellContentTemplate));
|
||||
|
||||
public DataTemplate CellContentTemplate {
|
||||
get => GetValue(CellContentTemplateProperty) as DataTemplate;
|
||||
set => SetValue(CellContentTemplateProperty, value);
|
||||
}
|
||||
#endregion
|
||||
|
||||
public static IMultiValueConverter ColumnDisplayNameVisibilityConverter =>
|
||||
ValueConverter.create<string, string, Visibility>((id, header) => id == header ? Visibility.Collapsed : Visibility.Visible);
|
||||
|
||||
|
||||
@@ -1,15 +1,25 @@
|
||||
using bts.proto;
|
||||
using bts.utils;
|
||||
using mbts.ui.controls;
|
||||
using mbts.ui.settings;
|
||||
using mbts.ui.shell;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace mbts.spark {
|
||||
partial class SimpleGridSettingsVM {
|
||||
public class SimpleColumnVM : ViewModelBase, ISimpleColumnVM {
|
||||
public class SimpleColumnVM : DisposableViewModelBase, ISimpleColumnVM {
|
||||
static readonly IReadOnlyList<Func<SimpleColumnVM, INotifyPropertyChanged>> _observable_component_getters =
|
||||
ObjectSnapshot.property_delegates_of_type<SimpleColumnVM, INotifyPropertyChanged>()
|
||||
.ToArray();
|
||||
|
||||
public readonly SimpleGridSettingsVM owner;
|
||||
|
||||
readonly DeferrableActionWrapper _update;
|
||||
string
|
||||
_id = string.Empty,
|
||||
_display_name = string.Empty;
|
||||
@@ -25,8 +35,13 @@ namespace mbts.spark {
|
||||
FontWeight _font_weight_override;
|
||||
GridColumnSettings _settings;
|
||||
|
||||
public SimpleColumnVM(SimpleGridSettingsVM owner) =>
|
||||
public SimpleColumnVM(SimpleGridSettingsVM owner) {
|
||||
this.owner = owner;
|
||||
_update = new DeferrableActionWrapper(() => result_changed?.Invoke());
|
||||
_dispose = ObjectSnapshot.connect_property_changed(_observable_components, (s, e) => _update.execute());
|
||||
}
|
||||
|
||||
public event Action result_changed;
|
||||
|
||||
public string id { get => _id; private set => Set(ref _id, value); }
|
||||
public double width { get => _width; set => Set(ref _width, value); }
|
||||
@@ -44,13 +59,15 @@ namespace mbts.spark {
|
||||
public bool is_font_weight_overridden { get => _is_font_weight_overridden; set => Set(ref _is_font_weight_overridden, value); }
|
||||
public FontWeight font_weight_override { get => _font_weight_override; set => Set(ref _font_weight_override, value); }
|
||||
|
||||
IEnumerable<INotifyPropertyChanged> _observable_components => _observable_component_getters.Select(g => g(this)).Append(this);
|
||||
|
||||
public GridColumnSettings create() =>
|
||||
_settings.with(
|
||||
can_override_display_name ? display_name : null,
|
||||
is_visible,
|
||||
null,
|
||||
is_font_weight_overridden ? new NullableFontWeightProto(font_weight_override) : (NullableFontWeightProto?)null,
|
||||
is_font_color_overridden ? new NullableColorProto(font_color_override.color) : (NullableColorProto?)null,
|
||||
is_font_weight_overridden ? new NullableFontWeightProto(font_weight_override) : new NullableFontWeightProto(null),
|
||||
is_font_color_overridden ? new NullableColorProto(font_color_override.color) : new NullableColorProto(null),
|
||||
width,
|
||||
horizontal_alignment);
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ namespace mbts.spark {
|
||||
bool is_font_weight_overridden { get; set; }
|
||||
FontWeight font_weight_override { get; set; }
|
||||
void apply_flags(GridColumnSettingsFeatureFlags flags);
|
||||
event Action result_changed;
|
||||
}
|
||||
|
||||
partial class SimpleGridSettingsVM : DisposableViewModelBase, IValueFactory<GridSettings>, IWindowSettingsVM<GridSettings>, ITemplateSelectorKey {
|
||||
@@ -39,9 +40,13 @@ namespace mbts.spark {
|
||||
readonly Property<bool> _show_predicate_animations, _show_cell_predicate_animations;
|
||||
readonly DeferrableActionWrapper _update;
|
||||
readonly IReadOnlyDictionary<string, GridColumnSettingsFeatureFlags> _column_flags;
|
||||
readonly Func<GridColumnSettings, ISimpleColumnVM> _column_factory;
|
||||
bool _can_edit_background;
|
||||
ColorPickerPopupVM _background;
|
||||
|
||||
public SimpleGridSettingsVM(IReadOnlyDictionary<string, GridColumnSettingsFeatureFlags> column_flags = null) {
|
||||
public SimpleGridSettingsVM(IReadOnlyDictionary<string, GridColumnSettingsFeatureFlags> column_flags = null, Func<GridColumnSettings, ISimpleColumnVM> column_factory = null) {
|
||||
_column_flags = column_flags ?? new Dictionary<string, GridColumnSettingsFeatureFlags>();
|
||||
_column_factory = column_factory ?? _default_column_factory;
|
||||
_font_family = backing(nameof(font_family), TextUtils.StandardFontFamily);
|
||||
_font_size = backing(nameof(font_size), FontSizes.Small);
|
||||
_font_weight = backing(nameof(font_weight), FontWeights.Normal);
|
||||
@@ -55,10 +60,11 @@ namespace mbts.spark {
|
||||
c.PropertyChanged += on_prop_changed;
|
||||
_dispose = new CompositeDisposable(
|
||||
ObservableAutoWrapper.connect_item_hooks(columns, c => {
|
||||
if (c is INotifyPropertyChanged npc) {
|
||||
npc.PropertyChanged += on_prop_changed;
|
||||
return Disposable.Create(() => npc.PropertyChanged -= on_prop_changed);
|
||||
} else return Disposable.empty;
|
||||
c.result_changed += _update.execute;
|
||||
var hook = Disposable.Create(() => c.result_changed -= _update.execute);
|
||||
if (c is IDisposable d)
|
||||
hook = Disposable.Create(hook, d);
|
||||
return hook;
|
||||
})) {
|
||||
() => {
|
||||
foreach (var c in _observable_components)
|
||||
@@ -79,8 +85,10 @@ namespace mbts.spark {
|
||||
public FontWeight font_weight { get => _font_weight.Value; set => _font_weight.set(value); }
|
||||
public ColorPickerPopupVM font_color { get; } = new ColorPickerPopupVM();
|
||||
public ColorPickerPopupVM selection_highlight { get; } = new ColorPickerPopupVM();
|
||||
public ColorPickerPopupVM background => _background ?? (_background = new ColorPickerPopupVM { bind_opacity_to_alpha = true, opacity = 1 });
|
||||
public double font_size { get => _font_size.Value; set => _font_size.set(value); }
|
||||
public bool enable_sound_triggers => false;
|
||||
public bool can_edit_background { get => _can_edit_background; set => Set(ref _can_edit_background, value); }
|
||||
public bool show_predicate_animations { get => _show_predicate_animations.Value; set => _show_predicate_animations.set(value); }
|
||||
public bool show_cell_predicate_animations { get => _show_cell_predicate_animations.Value; set => _show_cell_predicate_animations.set(value); }
|
||||
public IReadOnlyList<DialogSkirtItem> dialog_skirt_items => new[] {
|
||||
@@ -102,8 +110,18 @@ namespace mbts.spark {
|
||||
}
|
||||
}
|
||||
|
||||
public GridSettings create() =>
|
||||
new GridSettings(font_family.name(), font_size, font_weight, font_color.color.rgb_color(), selection_highlight.to_proto(), columns.Select(c => c.create()));
|
||||
public GridSettings create() {
|
||||
var rv = new GridSettings(
|
||||
font_family.name(),
|
||||
font_size,
|
||||
font_weight,
|
||||
font_color.color.rgb_color(),
|
||||
selection_highlight.to_proto(),
|
||||
columns.Select(c => c.create()));
|
||||
if (can_edit_background)
|
||||
rv = rv.with(background: background.color.rgb_color());
|
||||
return rv;
|
||||
}
|
||||
|
||||
public void load_from(GridSettings value) {
|
||||
if (value == null)
|
||||
@@ -115,22 +133,26 @@ namespace mbts.spark {
|
||||
selection_highlight.load_from(value.selection_highlight.to_brush());
|
||||
_font_size.set(value.font_size);
|
||||
var curr_col_id = current_column?.id;
|
||||
columns.SetItems(value.columns.Select(col => {
|
||||
var vm = new SimpleColumnVM(this);
|
||||
vm.load_from(col);
|
||||
// reapply our favorite column flags
|
||||
// this comes in handy when performing Load From Factory - the editor will contain
|
||||
// a consistent view of what can be overridden and what cannot
|
||||
if (_column_flags.TryGetValue(vm.id, out GridColumnSettingsFeatureFlags flags))
|
||||
vm.apply_flags(flags);
|
||||
return vm;
|
||||
}));
|
||||
columns.SetItems(value.columns.Select(_column_factory));
|
||||
if (curr_col_id == null || !columns.TryGetFirst(c => c.id == curr_col_id, out ISimpleColumnVM curr_column))
|
||||
curr_column = columns.FirstOrDefault();
|
||||
_current_column.set(curr_column);
|
||||
if (can_edit_background)
|
||||
background.color = value.background;
|
||||
}
|
||||
}
|
||||
|
||||
ISimpleColumnVM _default_column_factory(GridColumnSettings col_settings) {
|
||||
var vm = new SimpleColumnVM(this);
|
||||
vm.load_from(col_settings);
|
||||
// reapply our favorite column flags
|
||||
// this comes in handy when performing Load From Factory - the editor will contain
|
||||
// a consistent view of what can be overridden and what cannot
|
||||
if (_column_flags.TryGetValue(vm.id, out GridColumnSettingsFeatureFlags flags))
|
||||
vm.apply_flags(flags);
|
||||
return vm;
|
||||
}
|
||||
|
||||
void IWindowSettingsVM<GridSettings>.load(GridSettings value) => load_from(value);
|
||||
GridSettings IWindowSettingsVM<GridSettings>.get_result() => create();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
using mbts.ui.spark;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace bts.shell.pnl {
|
||||
readonly struct AnimationFrame<TColumn> where TColumn : struct {
|
||||
public readonly TimeSpan timestamp;
|
||||
public readonly ImmutableDictionary<SymbolKey<TColumn>, Animation> animation;
|
||||
|
||||
public AnimationFrame(TimeSpan timestamp) {
|
||||
this.timestamp = timestamp;
|
||||
animation = ImmutableDictionary<SymbolKey<TColumn>, Animation>.Empty;
|
||||
}
|
||||
|
||||
public AnimationFrame(TimeSpan timestamp, ImmutableDictionary<SymbolKey<TColumn>, Animation> animation) {
|
||||
this.timestamp = timestamp;
|
||||
this.animation = animation;
|
||||
}
|
||||
}
|
||||
|
||||
static class AnimationFrameFactory {
|
||||
public static IEnumerable<AnimationFrame<TColumn>> flash<TColumn>(TColumn column, params Animation[] animation) where TColumn : struct =>
|
||||
flash(column, TimeSpan.FromSeconds(2), animation);
|
||||
|
||||
public static IEnumerable<AnimationFrame<TColumn>> flash<TColumn>(TColumn column, TimeSpan duration, params Animation[] animation) where TColumn : struct {
|
||||
var anims = ImmutableDictionary<SymbolKey<TColumn>, Animation>.Empty;
|
||||
for (int i = 0; i < animation.Length; i++)
|
||||
anims = anims.Add(new SymbolKey<TColumn>(column, animation[i].type), animation[i]);
|
||||
yield return new AnimationFrame<TColumn>(TimeSpan.Zero, anims);
|
||||
yield return new AnimationFrame<TColumn>(duration);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
using mbts.ui.spark;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Reactive.Subjects;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Threading;
|
||||
|
||||
namespace bts.shell.pnl {
|
||||
class DIYGridAnimationModel<TColumn, TRow> : IObservable<ImmutableDictionary<SymbolKey<TColumn>, Animation>> where TColumn : struct {
|
||||
readonly Dictionary<int, ITriggerAnimation<TRow, TColumn>> _triggers = new Dictionary<int, ITriggerAnimation<TRow, TColumn>>();
|
||||
readonly Subject<ImmutableDictionary<SymbolKey<TColumn>, Animation>> _subject = new Subject<ImmutableDictionary<SymbolKey<TColumn>, Animation>>();
|
||||
readonly TimeSpan? _delay;
|
||||
TRow _pending_update;
|
||||
bool _is_busy, _is_dirty;
|
||||
ImmutableDictionary<int, int> _versions = ImmutableDictionary<int, int>.Empty;
|
||||
ImmutableDictionary<SymbolKey<TColumn>, Animation> _full_state = ImmutableDictionary<SymbolKey<TColumn>, Animation>.Empty;
|
||||
// Since animations may begin before the previous one is finished, there may be symbol use conflicts (i.e., animation units, like arrow, border, etc.).
|
||||
// The Symbol Access map below indicates which symbols are in use, and which trigger is using them (value).
|
||||
// When the a particular trigger begins a new animation timeline (in response to quick data changes), before the previous one completed,
|
||||
// the symbols being used by the previous animation are removed from the overall state.
|
||||
ImmutableDictionary<SymbolKey<TColumn>, int> _symbol_access = ImmutableDictionary<SymbolKey<TColumn>, int>.Empty;
|
||||
|
||||
public DIYGridAnimationModel() { }
|
||||
|
||||
public DIYGridAnimationModel(TimeSpan delay) {
|
||||
_delay = delay;
|
||||
}
|
||||
|
||||
int _trigger_id = 0;
|
||||
public void add_trigger(ITriggerAnimation<TRow, TColumn> trigger) =>
|
||||
_triggers.Add(++_trigger_id, trigger);
|
||||
|
||||
public void add_trigger(int id, ITriggerAnimation<TRow, TColumn> trigger) =>
|
||||
_triggers.Add(id, trigger);
|
||||
|
||||
public async void on_row(TRow r) {
|
||||
if (_delay.HasValue) {
|
||||
if (_is_busy) {
|
||||
_is_dirty = true;
|
||||
_pending_update = r;
|
||||
} else {
|
||||
_is_busy = true;
|
||||
_pending_update = r;
|
||||
do {
|
||||
_is_dirty = false;
|
||||
int thread = System.Threading.Thread.CurrentThread.ManagedThreadId;
|
||||
await Task.Delay(_delay.Value);
|
||||
int thread_after = System.Threading.Thread.CurrentThread.ManagedThreadId;
|
||||
Log.assert(thread == thread_after, "Delayed execution returned on a different thread.");
|
||||
on_row(_pending_update, _triggers);
|
||||
} while (_is_dirty);
|
||||
_is_busy = false;
|
||||
}
|
||||
} else {
|
||||
on_row(r, _triggers);
|
||||
}
|
||||
}
|
||||
|
||||
public void on_row(TRow r, IEnumerable<KeyValuePair<int, ITriggerAnimation<TRow, TColumn>>> triggers) {
|
||||
var timeline = ImmutableSortedDictionary<TimeSpan, ImmutableDictionary<int, ImmutableDictionary<SymbolKey<TColumn>, Animation>>>.Empty;
|
||||
foreach (var kv in triggers) {
|
||||
var frames = kv.Value.on_data(r).ToList();
|
||||
if (frames.Count > 0) {
|
||||
// clear previous animation-state for this trigger
|
||||
foreach (var sym_kv in _symbol_access) {
|
||||
if (sym_kv.Value == kv.Key) {
|
||||
_symbol_access = _symbol_access.Remove(sym_kv.Key);
|
||||
_full_state = _full_state.Remove(sym_kv.Key);
|
||||
}
|
||||
}
|
||||
foreach (var f in frames) {
|
||||
if (timeline.TryGetValue(f.timestamp, out var tanims)) {
|
||||
tanims = tanims.Add(kv.Key, f.animation);
|
||||
timeline = timeline.SetItem(f.timestamp, tanims);
|
||||
} else {
|
||||
timeline = timeline.Add(f.timestamp, ImmutableDictionary<int, ImmutableDictionary<SymbolKey<TColumn>, Animation>>.Empty.Add(kv.Key, f.animation));
|
||||
}
|
||||
}
|
||||
_versions = _versions.SetItem(kv.Key, _versions.TryGetValue(kv.Key, out int vers) ? vers + 1 : 1);
|
||||
}
|
||||
}
|
||||
if (timeline.Count > 0)
|
||||
_animate(timeline);
|
||||
}
|
||||
|
||||
void _animate(ImmutableSortedDictionary<TimeSpan, ImmutableDictionary<int, ImmutableDictionary<SymbolKey<TColumn>, Animation>>> timeline) {
|
||||
var active = ImmutableDictionary<SymbolKey<TColumn>, int>.Empty;
|
||||
var snapshot = _versions;
|
||||
Log.assert(timeline.First().Key == TimeSpan.Zero);
|
||||
// ObserveOnDispatcher() is critical here, since all the animation timelines share access to a finite set of symbols (n per cell).
|
||||
// Unless this demonstrably hurts performance, it is probably not worth the trouble of implementing concurrent symbol sharing.
|
||||
var begin = timeline.GetEnumerator();
|
||||
DispatcherObservable.ObserveOnDispatcher(
|
||||
Observable.Generate(begin,
|
||||
en => begin.MoveNext() && begin.Current.Key < TimeSpan.MaxValue,
|
||||
en => en,
|
||||
en => begin.Current.Value,
|
||||
en => begin.Current.Key),
|
||||
DispatcherPriority.ApplicationIdle
|
||||
).Subscribe(on_animation);
|
||||
|
||||
void on_animation(ImmutableDictionary<int, ImmutableDictionary<SymbolKey<TColumn>, Animation>> changes) {
|
||||
var abd = active.ToBuilder();
|
||||
foreach (var kv in active) {
|
||||
if (changes.TryGetValue(kv.Value, out var new_trig_state)) {
|
||||
if (!new_trig_state.ContainsKey(kv.Key)) {
|
||||
abd.Remove(kv.Key);
|
||||
_symbol_access = _symbol_access.Remove(kv.Key);
|
||||
_full_state = _full_state.Remove(kv.Key);
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach (var kv in changes) {
|
||||
if (_versions.TryGetValue(kv.Key, out int vers) && vers == snapshot[kv.Key]) {
|
||||
foreach (var akv in kv.Value) {
|
||||
_symbol_access = _symbol_access.SetItem(akv.Key, kv.Key);
|
||||
_full_state = _full_state.SetItem(akv.Key, akv.Value);
|
||||
abd[akv.Key] = kv.Key;
|
||||
}
|
||||
}
|
||||
}
|
||||
active = abd.ToImmutable();
|
||||
_subject.OnNext(_full_state);
|
||||
}
|
||||
}
|
||||
|
||||
public IDisposable Subscribe(IObserver<ImmutableDictionary<SymbolKey<TColumn>, Animation>> observer) =>
|
||||
_subject.Subscribe(observer);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
using mbts.ui.spark;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Windows.Media;
|
||||
using CellAnimationType = mbts.ui.spark.CellAnimation.Type;
|
||||
|
||||
namespace bts.shell.pnl {
|
||||
interface ITriggerAnimation<T, TColumn> where TColumn : struct {
|
||||
IEnumerable<AnimationFrame<TColumn>> on_data(T value);
|
||||
}
|
||||
|
||||
static class DIYGridTriggerAnimations {
|
||||
static readonly TimeSpan _default_flash_duration = TimeSpan.FromSeconds(2);
|
||||
|
||||
public static ITriggerAnimation<T, TColumn> create<T, TColumn>(Func<T, IEnumerable<AnimationFrame<TColumn>>> animate) where TColumn : struct =>
|
||||
new AnonymousAnimator<T, TColumn>(animate);
|
||||
|
||||
public static ITriggerAnimation<T, TColumn> delta_trigger<T, TColumn>(TColumn column, Color increase, Color decrease) where T : struct, IComparable<T> where TColumn : struct =>
|
||||
new DeltaTimeline<T, TColumn>(column, increase, decrease, _default_flash_duration);
|
||||
|
||||
public static ITriggerAnimation<T, TColumn> delta_trigger<T, TColumn>(TColumn column, Color increase, Color decrease, IComparer<T> comparer) where T : struct where TColumn : struct =>
|
||||
new DeltaTimeline<T, TColumn>(column, increase, decrease, _default_flash_duration, comparer);
|
||||
|
||||
public static ITriggerAnimation<T, TColumn> delta_trigger<T, TColumn>(TColumn column, Color increase, Color decrease, TimeSpan flash_duration) where T : struct, IComparable<T> where TColumn : struct =>
|
||||
new DeltaTimeline<T, TColumn>(column, increase, decrease, flash_duration);
|
||||
|
||||
public static ITriggerAnimation<T, TColumn> append<T, TColumn>(this ITriggerAnimation<T, TColumn> first, ITriggerAnimation<T, TColumn> second) where TColumn : struct =>
|
||||
new AppendTrigger<T, TColumn>(first, second);
|
||||
|
||||
public static ITriggerAnimation<T, TColumn> zip_indefinite_frame<T, TColumn>(this ITriggerAnimation<T, TColumn> trigger, Func<T, ImmutableDictionary<SymbolKey<TColumn>, Animation>> get_indefinite) where TColumn : struct =>
|
||||
new IndefiniteFrameZipper<T, TColumn>(trigger, get_indefinite);
|
||||
|
||||
public static ITriggerAnimation<T, TColumn> where<T, TColumn>(this ITriggerAnimation<T, TColumn> trigger, Func<T, bool> predicate) where T : struct, IComparable<T> where TColumn : struct =>
|
||||
new WhereTrigger<T, TColumn>(trigger, predicate);
|
||||
|
||||
class DeltaTimeline<T, TColumn> : ITriggerAnimation<T, TColumn> where T : struct where TColumn : struct {
|
||||
readonly TColumn _column;
|
||||
readonly Color _increase_color, _decrease_color;
|
||||
readonly TimeSpan _flash_duration;
|
||||
readonly IComparer<T> _comparer;
|
||||
T _last_value;
|
||||
|
||||
public DeltaTimeline(TColumn column, Color increase, Color decrease, TimeSpan flash_duration)
|
||||
: this(column, increase, decrease, flash_duration, Comparer<T>.Default) { }
|
||||
|
||||
public DeltaTimeline(TColumn column, Color increase, Color decrease, TimeSpan flash_duration, IComparer<T> comparer) {
|
||||
_column = column;
|
||||
_increase_color = increase;
|
||||
_decrease_color = decrease;
|
||||
_flash_duration = flash_duration;
|
||||
_comparer = comparer;
|
||||
}
|
||||
|
||||
public IEnumerable<AnimationFrame<TColumn>> on_data(T value) {
|
||||
int delta = _comparer.Compare(value, _last_value);
|
||||
var temp = _last_value;
|
||||
_last_value = value;
|
||||
if (delta > 0) {
|
||||
return AnimationFrameFactory.flash(_column,
|
||||
new Animation(CellAnimationType.UpArrow, _increase_color));
|
||||
} else if (delta < 0) {
|
||||
return AnimationFrameFactory.flash(_column,
|
||||
new Animation(CellAnimationType.DownArrow, _decrease_color));
|
||||
} else {
|
||||
return Enumerable.Empty<AnimationFrame<TColumn>>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class WhereTrigger<T, TColumn> : ITriggerAnimation<T, TColumn> where T : struct, IComparable<T> where TColumn : struct {
|
||||
readonly ITriggerAnimation<T, TColumn> _trigger;
|
||||
readonly Func<T, bool> _predicate;
|
||||
|
||||
public WhereTrigger(ITriggerAnimation<T, TColumn> trigger, Func<T, bool> predicate) {
|
||||
_trigger = trigger;
|
||||
_predicate = predicate;
|
||||
}
|
||||
|
||||
public IEnumerable<AnimationFrame<TColumn>> on_data(T value) =>
|
||||
_predicate(value) ? _trigger.on_data(value) : Enumerable.Empty<AnimationFrame<TColumn>>();
|
||||
}
|
||||
|
||||
class AppendTrigger<T, TColumn> : ITriggerAnimation<T, TColumn> where TColumn : struct {
|
||||
readonly ITriggerAnimation<T, TColumn> _first, _second;
|
||||
|
||||
public AppendTrigger(ITriggerAnimation<T, TColumn> first, ITriggerAnimation<T, TColumn> second) {
|
||||
_first = first;
|
||||
_second = second;
|
||||
}
|
||||
|
||||
public IEnumerable<AnimationFrame<TColumn>> on_data(T value) {
|
||||
//_predicate(value) ? _first.on_data(value) : Enumerable.Empty<(ImmutableDictionary<SymbolKey<TColumn>, Animation> animation, TimeSpan timestamp)>();
|
||||
var first_anims = _first.on_data(value);
|
||||
var second_anims = _second.on_data(value);
|
||||
|
||||
var first_en = _first.on_data(value).GetEnumerator();
|
||||
var second_en = _second.on_data(value).GetEnumerator();
|
||||
while (first_en.MoveNext()) {
|
||||
if (second_en.MoveNext()) {
|
||||
while (first_en.Current.timestamp < second_en.Current.timestamp) {
|
||||
yield return first_en.Current;
|
||||
if (!first_en.MoveNext())
|
||||
goto finish_with_second;
|
||||
}
|
||||
while (second_en.Current.timestamp < first_en.Current.timestamp) {
|
||||
yield return second_en.Current;
|
||||
if (!second_en.MoveNext())
|
||||
goto finish_with_first;
|
||||
}
|
||||
if (first_en.Current.timestamp == second_en.Current.timestamp) {
|
||||
var combined_anims = first_en.Current.animation;
|
||||
foreach (var kv in second_en.Current.animation) {
|
||||
if (!combined_anims.ContainsKey(kv.Key))
|
||||
combined_anims = combined_anims.Add(kv.Key, kv.Value);
|
||||
}
|
||||
yield return new AnimationFrame<TColumn>(first_en.Current.timestamp, combined_anims);
|
||||
}
|
||||
} else {
|
||||
goto finish_with_first;
|
||||
}
|
||||
}
|
||||
if (second_en.MoveNext())
|
||||
goto finish_with_second;
|
||||
yield break;
|
||||
finish_with_first:
|
||||
do yield return first_en.Current;
|
||||
while (first_en.MoveNext());
|
||||
yield break;
|
||||
finish_with_second:
|
||||
do yield return second_en.Current;
|
||||
while (second_en.MoveNext());
|
||||
yield break;
|
||||
}
|
||||
}
|
||||
|
||||
class AnonymousAnimator<T, TColumn> : ITriggerAnimation<T, TColumn> where TColumn : struct {
|
||||
readonly Func<T, IEnumerable<AnimationFrame<TColumn>>> _on_data;
|
||||
|
||||
public AnonymousAnimator(Func<T, IEnumerable<AnimationFrame<TColumn>>> on_data) =>
|
||||
_on_data = on_data;
|
||||
|
||||
public IEnumerable<AnimationFrame<TColumn>> on_data(T value) =>
|
||||
_on_data(value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When c
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <typeparam name="TColumn"></typeparam>
|
||||
class IndefiniteFrameZipper<T, TColumn> : ITriggerAnimation<T, TColumn> where TColumn : struct {
|
||||
readonly ITriggerAnimation<T, TColumn> _trigger;
|
||||
readonly Func<T, ImmutableDictionary<SymbolKey<TColumn>, Animation>> _get_indefinite;
|
||||
bool _active;
|
||||
|
||||
public IndefiniteFrameZipper(ITriggerAnimation<T, TColumn> trigger, Func<T, ImmutableDictionary<SymbolKey<TColumn>, Animation>> get_indefinite) {
|
||||
_get_indefinite = get_indefinite;
|
||||
_trigger = trigger;
|
||||
}
|
||||
|
||||
public IEnumerable<AnimationFrame<TColumn>> on_data(T value) {
|
||||
var rv = _trigger.on_data(value);
|
||||
var indef = _get_indefinite(value);
|
||||
if (indef != null && indef.Count > 0) {
|
||||
_active = true;
|
||||
if (rv == null || !rv.Any()) {
|
||||
return new[] { new AnimationFrame<TColumn>(TimeSpan.Zero, indef) };
|
||||
} else {
|
||||
return rv.Select(f => new AnimationFrame<TColumn>(f.timestamp, f.animation.SetItems(indef))).ToList();
|
||||
}
|
||||
}
|
||||
if (_active) {
|
||||
_active = false;
|
||||
if (rv == null || !rv.Any()) {
|
||||
return new[] { new AnimationFrame<TColumn>(TimeSpan.Zero, ImmutableDictionary<SymbolKey<TColumn>, Animation>.Empty) };
|
||||
} else {
|
||||
return rv;
|
||||
}
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static class DIYGridAnimationExtensions {
|
||||
public static ITriggerAnimation<TOuter, TColumn> wrap<T, TOuter, TColumn>(this ITriggerAnimation<T, TColumn> timeline, Func<TOuter, T> selector) where TColumn : struct =>
|
||||
new SelectorWrapper<T, TOuter, TColumn>(timeline, selector);
|
||||
|
||||
public static ITriggerAnimation<T, TColumn> aggregate<T, TAccum, TColumn>(this ITriggerAnimation<TAccum, TColumn> timeline, Func<TAccum, T, TAccum> accumulate, TAccum seed = default) where TColumn : struct =>
|
||||
new AggregatorWrapper<T, TAccum, TColumn>(timeline, accumulate, seed);
|
||||
|
||||
public static ITriggerAnimation<T, TColumn> delta<T, TColumn>(this ITriggerAnimation<T, TColumn> animator, Func<T, T, T> get_delta) where TColumn : struct =>
|
||||
new DeltaWrapper<T, TColumn>(animator, get_delta);
|
||||
|
||||
class ComparisonAnimation<T, TColumn> : ITriggerAnimation<T, TColumn> where T : struct, IComparable<T> where TColumn : struct {
|
||||
readonly TColumn _column;
|
||||
readonly Animation[] _animations;
|
||||
readonly T[] _sorted_values;
|
||||
IComparer<T> _comparer;
|
||||
|
||||
public ComparisonAnimation(TColumn column, params (T value, Animation animation)[] sorted_animation_buckets)
|
||||
: this(column, Comparer<T>.Default, sorted_animation_buckets) { }
|
||||
|
||||
public ComparisonAnimation(TColumn column, IComparer<T> comparer, params (T value, Animation animation)[] sorted_animation_buckets) {
|
||||
_column = column;
|
||||
_comparer = comparer;
|
||||
_animations = Array.ConvertAll(sorted_animation_buckets, a => a.animation);
|
||||
_sorted_values = Array.ConvertAll(sorted_animation_buckets, a => a.value);
|
||||
}
|
||||
|
||||
public IEnumerable<AnimationFrame<TColumn>> on_data(T value) {
|
||||
int pos = Array.BinarySearch(_sorted_values, value);
|
||||
if (pos < 0)
|
||||
pos = ~pos;
|
||||
var anim = pos == _animations.Length ? _animations[_animations.Length - 1] : _animations[pos];
|
||||
yield return new AnimationFrame<TColumn>(
|
||||
TimeSpan.Zero,
|
||||
ImmutableDictionary<SymbolKey<TColumn>, Animation>.Empty.Add(new SymbolKey<TColumn>(_column, anim.type), anim));
|
||||
}
|
||||
}
|
||||
|
||||
class SelectorWrapper<T, TOuter, TColumn> : ITriggerAnimation<TOuter, TColumn> where TColumn : struct {
|
||||
readonly ITriggerAnimation<T, TColumn> _timeline;
|
||||
readonly Func<TOuter, T> _selector;
|
||||
|
||||
public SelectorWrapper(ITriggerAnimation<T, TColumn> timeline, Func<TOuter, T> selector) {
|
||||
_timeline = timeline;
|
||||
_selector = selector;
|
||||
}
|
||||
|
||||
public IEnumerable<AnimationFrame<TColumn>> on_data(TOuter value) =>
|
||||
_timeline.on_data(_selector(value));
|
||||
}
|
||||
|
||||
class AggregatorWrapper<T, TAccum, TColumn> : ITriggerAnimation<T, TColumn> where TColumn : struct {
|
||||
readonly ITriggerAnimation<TAccum, TColumn> _timeline;
|
||||
readonly Func<TAccum, T, TAccum> _accumulate;
|
||||
TAccum _seed;
|
||||
|
||||
public AggregatorWrapper(ITriggerAnimation<TAccum, TColumn> timeline, Func<TAccum, T, TAccum> accumulate, TAccum seed = default) {
|
||||
_timeline = timeline;
|
||||
_accumulate = accumulate;
|
||||
_seed = seed;
|
||||
}
|
||||
|
||||
public IEnumerable<AnimationFrame<TColumn>> on_data(T value) =>
|
||||
_timeline.on_data(_seed = _accumulate(_seed, value));
|
||||
}
|
||||
|
||||
class DeltaWrapper<T, TColumn> : ITriggerAnimation<T, TColumn> where TColumn : struct {
|
||||
readonly Func<T, T, T> _get_delta;
|
||||
readonly ITriggerAnimation<T, TColumn> _animator;
|
||||
T _last_value;
|
||||
|
||||
public DeltaWrapper(ITriggerAnimation<T, TColumn> animator, Func<T, T, T> get_delta, T default_value = default) {
|
||||
_animator = animator;
|
||||
_get_delta = get_delta;
|
||||
_last_value = default_value;
|
||||
}
|
||||
|
||||
public IEnumerable<AnimationFrame<TColumn>> on_data(T value) {
|
||||
T delta = _get_delta(_last_value, value);
|
||||
_last_value = value;
|
||||
return _animator.on_data(delta);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using bts.utils;
|
||||
using System;
|
||||
using CellAnimationType = mbts.ui.spark.CellAnimation.Type;
|
||||
|
||||
namespace bts.shell.pnl {
|
||||
readonly struct SymbolKey<T> : IEquatable<SymbolKey<T>> where T : struct {
|
||||
public readonly T column;
|
||||
public readonly CellAnimationType type;
|
||||
|
||||
public SymbolKey(T column, CellAnimationType type) {
|
||||
this.column = column;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public bool Equals(SymbolKey<T> other) =>
|
||||
column.Equals(other.column) &&
|
||||
type == other.type;
|
||||
|
||||
public override bool Equals(object obj) =>
|
||||
obj is SymbolKey<T> other && Equals(other);
|
||||
|
||||
public override int GetHashCode() =>
|
||||
(column.GetHashCode() % MathUtils.Primes.P_65449) |
|
||||
((type.GetHashCode() % MathUtils.Primes.P_65449) << 16);
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,10 @@
|
||||
using bts.collections;
|
||||
using mbts.ui.utility;
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
|
||||
namespace mbts.spark.filters {
|
||||
static class DataGridFilterHelper {
|
||||
public static IDisposable hook_filters<TRow>(ObservableFilterCollection<TRow> rows, Action filter_action, params IDataGridColumnFilter<TRow>[] filters) =>
|
||||
_hook_filters(filter_action, filters);
|
||||
|
||||
public static IDisposable hook_filters<TRow>(ObservableFilterCollection<TRow> rows, params IDataGridColumnFilter<TRow>[] filters) {
|
||||
return _hook_filters(set_filter, filters);
|
||||
void set_filter() {
|
||||
using (rows.defer_refresh()) {
|
||||
var active_filters = _get_active_filters(filters);
|
||||
rows.live_filter_properties.SetItems(active_filters.Select(f => f.prop_name));
|
||||
rows.filter = r => active_filters.All(f => f.filter(r));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static IDisposable hook_filters<TRow>(ICollectionView rows, params IDataGridColumnFilter<TRow>[] filters) {
|
||||
var icvls = (ICollectionViewLiveShaping)rows;
|
||||
icvls.IsLiveFiltering = true;
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using bts.api.core;
|
||||
using bts;
|
||||
using bts.api;
|
||||
using bts.api.core;
|
||||
using bts.collections;
|
||||
using bts.utils;
|
||||
using mbts.collections;
|
||||
@@ -339,5 +341,47 @@ namespace mbts.spark.filters {
|
||||
_selected_items.sync_with(_selected_items_snapshot.Intersect(_items));
|
||||
}
|
||||
}
|
||||
|
||||
public static class Common {
|
||||
// Account
|
||||
public static IDataGridColumnPersistentFilter<TRow, uint[]> account<TRow>(OrdersModel model, Func<TRow, AccountTuple> get_row_account, string prop_name) =>
|
||||
account(model.admin_model.on_account_tuples, get_row_account, prop_name);
|
||||
|
||||
public static IDataGridColumnPersistentFilter<TRow, uint[]> account<TRow>(IObservable<IEnumerable<AccountTuple>> source, Func<TRow, AccountTuple> get_row_account, string prop_name) =>
|
||||
Create(
|
||||
source,
|
||||
get_row_account,
|
||||
vals => vals.Select(v => v.id).ToArray(),
|
||||
vals => {
|
||||
var id_set = new HashSet<uint>(vals.empty_if_null());
|
||||
return source.Select(acc => acc.Where(a => id_set.Contains(a.id)));
|
||||
},
|
||||
prop_name);
|
||||
|
||||
public static IDataGridColumnPersistentFilter<TRow, uint[]> account<TRow>(IObservable<TableUpdate<AccountTuple, uint>> source, Func<TRow, AccountTuple> get_row_account, string prop_name) =>
|
||||
Create(
|
||||
source.Select(t => t.state.Values),
|
||||
get_row_account,
|
||||
vals => vals.Select(v => v.id).ToArray(),
|
||||
vals => {
|
||||
var id_set = new HashSet<uint>(vals.empty_if_null());
|
||||
return source.Filter(acc => id_set.Contains(acc.id)).Select(t => t.state.Values);
|
||||
},
|
||||
prop_name);
|
||||
// Exchange
|
||||
public static IDataGridColumnPersistentFilter<TRow, Venue[]> exchange<TRow>(Func<TRow, Venue> get_row_exchange, string prop_name) =>
|
||||
Create(EnumHelper.get_values_except(Venue.Unset).ToList(), get_row_exchange, prop_name);
|
||||
// Symbol
|
||||
public static IDataGridColumnPersistentFilter<TRow, string[]> symbol<TRow>(Func<TRow, string> get_row_symbol, string prop_name) =>
|
||||
Create(co2.UserSymbol.get_all().Select(u => u.symbol).Distinct().ToList(), get_row_symbol, prop_name);
|
||||
// Security Type
|
||||
public static IDataGridColumnPersistentFilter<TRow, Security.ContractOneofCase[]> security_type<TRow>(Func<TRow, Security.ContractOneofCase> get_row_security_type, string prop_name) =>
|
||||
Create(EnumHelper.get_values_except(
|
||||
Security.ContractOneofCase.None,
|
||||
Security.ContractOneofCase.Option,
|
||||
Security.ContractOneofCase.Synthetic).ToList(),
|
||||
get_row_security_type,
|
||||
prop_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using ProtoBuf;
|
||||
using bts.utils;
|
||||
using ProtoBuf;
|
||||
using System;
|
||||
|
||||
namespace mbts.spark.filters {
|
||||
@@ -8,7 +9,7 @@ namespace mbts.spark.filters {
|
||||
}
|
||||
|
||||
[ProtoContract]
|
||||
class IntStatus : INullableStructProto<int> {
|
||||
class IntStatus : INullableStructProto<int>, IEquatable<IntStatus> {
|
||||
[ProtoMember(1)]
|
||||
bool _active;
|
||||
[ProtoMember(2)]
|
||||
@@ -19,10 +20,23 @@ namespace mbts.spark.filters {
|
||||
get => _active ? new int?(_value) : new int?();
|
||||
set { if (_active = value.HasValue) _value = value.Value; }
|
||||
}
|
||||
|
||||
public bool Equals(IntStatus other) =>
|
||||
other != null &&
|
||||
_active == other._active &&
|
||||
_value == other._value;
|
||||
|
||||
public override bool Equals(object obj) =>
|
||||
Equals(obj as IntStatus);
|
||||
|
||||
public override int GetHashCode() =>
|
||||
EqualityComparer.aggregate_hash(
|
||||
_active.GetHashCode(),
|
||||
_value.GetHashCode());
|
||||
}
|
||||
|
||||
[ProtoContract]
|
||||
class IntMinMaxState : IMinMaxState<int> {
|
||||
class IntMinMaxState : IMinMaxState<int>, IEquatable<IntMinMaxState> {
|
||||
[ProtoMember(1)]
|
||||
readonly IntStatus _minimum;
|
||||
[ProtoMember(2)]
|
||||
@@ -35,5 +49,19 @@ namespace mbts.spark.filters {
|
||||
|
||||
public INullableStructProto<int> minimum => _minimum;
|
||||
public INullableStructProto<int> maximum => _maximum;
|
||||
|
||||
public bool Equals(IntMinMaxState other) =>
|
||||
other != null &&
|
||||
_minimum.safe_equals(other._minimum) &&
|
||||
_maximum.safe_equals(other._maximum);
|
||||
|
||||
public override bool Equals(object obj) =>
|
||||
Equals(obj as IntMinMaxState);
|
||||
|
||||
public override int GetHashCode() =>
|
||||
|
||||
EqualityComparer.aggregate_hash(
|
||||
_minimum?.GetHashCode() ?? 0,
|
||||
_maximum?.GetHashCode() ?? 0);
|
||||
}
|
||||
}
|
||||
@@ -13,11 +13,13 @@
|
||||
<Border BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
Background="{TemplateBinding Background}">
|
||||
<ItemsPresenter />
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
|
||||
<ItemsPresenter/>
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
</AdornerDecorator>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
</ResourceDictionary>
|
||||
@@ -30,7 +30,7 @@
|
||||
<HierarchicalDataTemplate DataType="{x:Type local:LaunchItem}">
|
||||
<Button Style="{DynamicResource StartMenuButton}" Content="{Binding name}" Command="{Binding command}" CommandParameter="{Binding args}" MinWidth="100" Padding="4" />
|
||||
</HierarchicalDataTemplate>
|
||||
<HierarchicalDataTemplate DataType="{x:Type local:GroupItem}" ItemsSource="{Binding Value}" >
|
||||
<HierarchicalDataTemplate DataType="{x:Type local:HierarchicalGroupItem}" ItemsSource="{Binding Value}" >
|
||||
<TextBlock Text="{Binding Key}" Margin="4" />
|
||||
</HierarchicalDataTemplate>
|
||||
</TreeView.Resources>
|
||||
|
||||
@@ -35,10 +35,10 @@ namespace mbts.ui.controls {
|
||||
private Dictionary<string, ObservableCollection<object>> _data;
|
||||
public Dictionary<string, ObservableCollection<object>> Data { get { return _data; } set { _data = value; RaisePropertyChanged("InternalData"); } }
|
||||
// A hack to provide a non-generic type for template binding
|
||||
public IEnumerable<GroupItem> InternalData {
|
||||
public IEnumerable<HierarchicalGroupItem> InternalData {
|
||||
get {
|
||||
if (_data == null) return null;
|
||||
return _data.Select(k => new GroupItem() { Key = k.Key, Value = k.Value }); }
|
||||
return _data.Select(k => new HierarchicalGroupItem() { Key = k.Key, Value = k.Value }); }
|
||||
}
|
||||
string _btext = string.Empty;
|
||||
public string ButtonText { get { return _btext; } set { _btext = value; RaisePropertyChanged("ButtonText"); RaisePropertyChanged("ButtonVisibility"); } }
|
||||
@@ -46,7 +46,7 @@ namespace mbts.ui.controls {
|
||||
public ICommand Command { get; set; }
|
||||
}
|
||||
|
||||
public class GroupItem
|
||||
public class HierarchicalGroupItem
|
||||
{
|
||||
public string Key { get; set; }
|
||||
public object Value { get; set; }
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
Background="{TemplateBinding Background}"
|
||||
Width="{TemplateBinding Width}"
|
||||
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
|
||||
VerticalAlignment="{TemplateBinding VerticalAlignment}"
|
||||
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
|
||||
@@ -20,6 +20,19 @@ namespace mbts.ui.controls {
|
||||
/// Interaction logic for PriceEntryCommitControl.xaml
|
||||
/// </summary>
|
||||
public partial class PriceEntryCommitControl : UserControl {
|
||||
public static readonly DependencyProperty RealPriceProperty =
|
||||
WPFHelper.create_dp<double, PriceEntryCommitControl>(
|
||||
nameof(RealPrice),
|
||||
(o, v) => {
|
||||
if (!double.IsNaN(v) && !o.IsKeyboardFocusWithin)
|
||||
o.SetCurrentValue(SourceValueProperty, v);
|
||||
},
|
||||
double.NaN);
|
||||
|
||||
public double RealPrice {
|
||||
get => (double)GetValue(RealPriceProperty);
|
||||
set => SetValue(RealPriceProperty, value);
|
||||
}
|
||||
|
||||
|
||||
public static readonly DependencyProperty SourceValueProperty =
|
||||
|
||||
@@ -153,7 +153,7 @@
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="SparkToolBar" TargetType="ToolBar">
|
||||
<Style x:Key="SparkToolBarBase" TargetType="ToolBar">
|
||||
<Setter Property="ToolBarTray.IsLocked" Value="True"/>
|
||||
<Setter Property="Background" Value="LightGray"/>
|
||||
<Setter Property="BorderBrush" Value="DarkGray"/>
|
||||
@@ -162,7 +162,6 @@
|
||||
<Setter Property="SnapsToDevicePixels" Value="True"/>
|
||||
<Setter Property="OverridesDefaultStyle" Value="True"/>
|
||||
<Setter Property="UseLayoutRounding" Value="True"/>
|
||||
<Setter Property="ItemContainerStyle" Value="{StaticResource SparkToolBarItem}"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="{x:Type ToolBar}">
|
||||
@@ -223,4 +222,8 @@
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="SparkToolBar" TargetType="ToolBar" BasedOn="{StaticResource SparkToolBarBase}">
|
||||
<Setter Property="ItemContainerStyle" Value="{StaticResource SparkToolBarItem}"/>
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
@@ -21,10 +21,12 @@ namespace mbts.ui.converters {
|
||||
static class ValueConverters {
|
||||
public static readonly IValueConverter
|
||||
DoubleTextConverter = new DoubleTextConverter(),
|
||||
DoubleGreaterThanZero = new DoubleGreaterThanConverter { Value = 0 },
|
||||
IntTextConverter = new IntTextConverter(),
|
||||
UIntTextConverter = new UIntTextConverter(),
|
||||
QuantityMeasureTextConverter = ValueConverter.create<QuantityMeasureImmutable?, string>(fmt),
|
||||
SimpleString = new AnonymousValueConverter(obj => obj?.ToString());
|
||||
SimpleString = new AnonymousValueConverter(obj => obj?.ToString()),
|
||||
ThicknessToDouble = new ThicknessToDoubleConverter();
|
||||
|
||||
public static readonly IMultiValueConverter
|
||||
AreEqualConverter = new AreEqualConverter();
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Windows;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace mbts.ui.converters {
|
||||
[ValueConversion(typeof(double), typeof(Thickness))]
|
||||
class DoubleToThicknessConverter : IValueConverter {
|
||||
public double LeftMultiplier { get; set; }
|
||||
public double TopMultiplier { get; set; }
|
||||
@@ -15,4 +16,14 @@ namespace mbts.ui.converters {
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new NotImplementedException();
|
||||
}
|
||||
|
||||
[ValueConversion(typeof(Thickness), typeof(double))]
|
||||
class ThicknessToDoubleConverter : IValueConverter {
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
|
||||
if (value is Thickness th)
|
||||
return th.Left;
|
||||
return 0.0;
|
||||
}
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace mbts.ui.converters {
|
||||
[ValueConversion(typeof(double), typeof(bool))]
|
||||
class DoubleGreaterThanConverter : IValueConverter {
|
||||
public double Value { get; set; }
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
|
||||
if (value is double d)
|
||||
return d > Value;
|
||||
return new ValidationResult(false, "Not an double.");
|
||||
}
|
||||
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) =>
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
+11
-7
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using bts.utils;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Media;
|
||||
@@ -26,6 +27,7 @@ namespace mbts.ui.converters {
|
||||
public static SignedDataStore<T> with_positive<T>(this SignedDataStore<T> store, T positive) => new SignedDataStore<T>(positive, store.neutral, store.negative, store.error);
|
||||
public static SignedDataStore<T> with_neutral<T>(this SignedDataStore<T> store, T neutral) => new SignedDataStore<T>(store.positive, neutral, store.negative, store.error);
|
||||
public static SignedDataStore<T> with_negative<T>(this SignedDataStore<T> store, T negative) => new SignedDataStore<T>(store.positive, store.neutral, negative, store.error);
|
||||
public static SignedDataStore<T> with_error<T>(this SignedDataStore<T> store, T error) => new SignedDataStore<T>(store.positive, store.neutral, store.negative, error);
|
||||
}
|
||||
|
||||
class NumericSignBrushConverter : IValueConverter {
|
||||
@@ -35,6 +37,7 @@ namespace mbts.ui.converters {
|
||||
public Color PositiveColor { get => _brushes.positive.Color; set => _brushes = _brushes.with_positive(new SolidColorBrush(value) { Opacity = _opacity }.frozen()); }
|
||||
public Color NeutralColor { get => _brushes.neutral.Color; set => _brushes = _brushes.with_neutral(new SolidColorBrush(value) { Opacity = _opacity }.frozen()); }
|
||||
public Color NegativeColor { get => _brushes.negative.Color; set => _brushes = _brushes.with_negative(new SolidColorBrush(value) { Opacity = _opacity }.frozen()); }
|
||||
public Color ErrorColor { get => _brushes.error.Color; set => _brushes = _brushes.with_error(new SolidColorBrush(value) { Opacity = _opacity }.frozen()); }
|
||||
|
||||
double _opacity = 1.0;
|
||||
public double Opacity {
|
||||
@@ -42,19 +45,20 @@ namespace mbts.ui.converters {
|
||||
set {
|
||||
_opacity = value;
|
||||
_brushes = new SignedDataStore<SolidColorBrush>(
|
||||
_brushes.positive.with(value).frozen(),
|
||||
_brushes.neutral.with(value).frozen(),
|
||||
_brushes.negative.with(value).frozen(),
|
||||
_brushes.error.with(value).frozen());
|
||||
_brushes.positive.Clone().with(value).frozen(),
|
||||
_brushes.neutral.Clone().with(value).frozen(),
|
||||
_brushes.negative.Clone().with(value).frozen(),
|
||||
_brushes.error.Clone().with(value).frozen());
|
||||
}
|
||||
}
|
||||
public IComparable Zero { get; set; } = 0.0;
|
||||
public IComparable Zero { get; set; } = new UniversalNumericComparer(0.0);
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
|
||||
var comparable = value as IComparable;
|
||||
if (comparable == null || comparable is double && double.IsNaN((double)value))
|
||||
return _brushes.error;
|
||||
int comp = comparable.CompareTo(Zero);
|
||||
/// [reverse]compare with <see cref="Zero"/> bc it is the <see cref="UniversalNumericComparer"/>, and supports comparisons with most numeric types.
|
||||
int comp = -Zero.CompareTo(comparable);
|
||||
return comp == 0 ? _brushes.neutral : comp > 0 ? _brushes.positive : _brushes.negative;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using bts;
|
||||
using bts.utils;
|
||||
using mbts.ui.extensions;
|
||||
using mbts.ui.shell;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
@@ -33,6 +35,39 @@ namespace mbts.ui {
|
||||
|
||||
private static DragDropDocumentPreview _dddp;
|
||||
|
||||
public static IObservable<Point> subscribe_drag_start(FrameworkElement element) =>
|
||||
Observable.Create<Point>(o => {
|
||||
Point? mouse_down = null, drag_start = null;
|
||||
element.MouseDown += on_mouse_down;
|
||||
element.MouseMove += on_mouse_move;
|
||||
element.MouseUp += on_mouse_up;
|
||||
return Disposable.Create(() => {
|
||||
element.MouseDown -= on_mouse_down;
|
||||
element.MouseMove -= on_mouse_move;
|
||||
element.MouseUp -= on_mouse_up;
|
||||
});
|
||||
// callbacks
|
||||
void on_mouse_up(object sender, MouseButtonEventArgs e) {
|
||||
mouse_down = null;
|
||||
drag_start = null;
|
||||
}
|
||||
void on_mouse_move(object sender, MouseEventArgs e) {
|
||||
if (mouse_down.HasValue) {
|
||||
var curr_pt = e.GetPosition(element);
|
||||
if (is_sufficient_delta(mouse_down.Value, curr_pt)) {
|
||||
e.Handled = true;
|
||||
drag_start = curr_pt;
|
||||
mouse_down = null;
|
||||
o.OnNext(curr_pt);
|
||||
}
|
||||
}
|
||||
}
|
||||
void on_mouse_down(object sender, MouseButtonEventArgs e) {
|
||||
drag_start = null;
|
||||
mouse_down = e.GetPosition(element);
|
||||
}
|
||||
});
|
||||
|
||||
public static IObservable<Point?> subscribe_drag_coordinates(FrameworkElement element) =>
|
||||
Observable.Create<Point?>(o => {
|
||||
Point? mouse_down = null, drag_start = null;
|
||||
@@ -70,6 +105,57 @@ namespace mbts.ui {
|
||||
}
|
||||
});
|
||||
|
||||
public static IDisposable connect_horizontal_insertion_adorner(ItemsControl container, FrameworkElement root = null) {
|
||||
HorizontalInsertionAdorner insertion_adorner = null;
|
||||
UIElement target = null;
|
||||
TargetSide target_side = TargetSide.LEFT;
|
||||
if (root == null)
|
||||
root = container;
|
||||
return Disposable.Create(
|
||||
root.SubscribePreviewDragEnter(on_drag_enter),
|
||||
root.SubscribePreviewDragLeave((s, e) => remove_adorner()),
|
||||
root.SubscribePreviewDragOver(on_drag_over),
|
||||
root.SubscribePreviewDrop((s, e) => remove_adorner()),
|
||||
Disposable.Create(remove_adorner));
|
||||
void on_drag_over(object sender, DragEventArgs e) {
|
||||
_determine_drop_target(e);
|
||||
_update_insertion_adorner();
|
||||
e.Effects = DragDropEffects.Move;
|
||||
e.Handled = true;
|
||||
}
|
||||
void on_drag_enter(object sender, DragEventArgs e) {
|
||||
_determine_drop_target(e);
|
||||
_update_insertion_adorner();
|
||||
e.Effects = e.AllowedEffects;
|
||||
}
|
||||
// Draw an indicator where the drop will insert the item
|
||||
void _update_insertion_adorner() {
|
||||
if (insertion_adorner == null) {
|
||||
insertion_adorner = new HorizontalInsertionAdorner(target, target_side);
|
||||
} else insertion_adorner.Side = target_side;
|
||||
}
|
||||
void _determine_drop_target(DragEventArgs e) {
|
||||
target = null;
|
||||
var pt = e.GetPosition(container);
|
||||
pt.X = MathUtils.within_range(0, container.ActualWidth - 2, pt.X);
|
||||
pt.Y = MathUtils.within_range(0, container.ActualHeight - 2, pt.Y);
|
||||
//if (e.OriginalSource is DependencyObject) {
|
||||
if (container.InputHitTest(pt) is DependencyObject hit) {
|
||||
target = container.ContainerFromElement(hit) as UIElement;
|
||||
if (target != null) {
|
||||
Point target_position = e.GetPosition(target);
|
||||
target_side = target_position.X < (target.RenderSize.Width / 2) ? TargetSide.LEFT : TargetSide.RIGHT;
|
||||
}
|
||||
}
|
||||
}
|
||||
void remove_adorner() {
|
||||
if (insertion_adorner != null) {
|
||||
insertion_adorner.Detach();
|
||||
insertion_adorner = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Conditional("DEBUG")]
|
||||
private static void log(string message, params object[] args) {
|
||||
Console.WriteLine(message, args);
|
||||
|
||||
@@ -117,5 +117,26 @@ namespace mbts.ui.extensions {
|
||||
tcs.TrySetResult(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static IDisposable ConnectOnLoaded(this FrameworkElement element, Func<IDisposable> connect, bool disconnect_on_unloaded = true) {
|
||||
IDisposable connection = null;
|
||||
element.Loaded += on_loaded;
|
||||
if (disconnect_on_unloaded)
|
||||
element.Unloaded += on_unloaded;
|
||||
return Disposable.Create(dispose);
|
||||
|
||||
void on_loaded(object sender, RoutedEventArgs e) =>
|
||||
Disposable.replace(ref connection, connect());
|
||||
|
||||
void on_unloaded(object sender, RoutedEventArgs e) =>
|
||||
Disposable.dispose(ref connection);
|
||||
|
||||
void dispose() {
|
||||
Disposable.dispose(ref connection);
|
||||
element.Loaded -= on_loaded;
|
||||
if (disconnect_on_unloaded)
|
||||
element.Unloaded -= on_unloaded;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loaded 100 of 138 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user