Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7112c40a4a | ||
|
|
27b87febda | ||
|
|
50b76d479b | ||
|
|
5a4b31ccc0 | ||
|
|
64416de10d | ||
|
|
46953cb639 | ||
|
|
7e68a235ee | ||
|
|
70e0979836 |
No files matched your search
@@ -124,4 +124,17 @@ service OrderService {
|
||||
```
|
||||
### C++ Example
|
||||
```cpp
|
||||
#include "lib/api/public/ServiceContext.h"
|
||||
#include "lib/api/services/OrderService.client.pb.h"
|
||||
using namespace bts::api;
|
||||
ServiceContext ctx;
|
||||
auto orders = ctx.locate<orderman::OrderService>();
|
||||
auto unsub = orders.subscribe_fills().subscribe([](const auto& update) {
|
||||
std::cout
|
||||
<< "Added " << update.added().size() << " items, "
|
||||
<< "Removed " << update.removed().size() << " items, "
|
||||
<< "Updated " << update.updated().size() << " items, "
|
||||
<< "Total items " << update.all().size() << std::endl;
|
||||
});
|
||||
std::this_thread::sleep_for(std::chrono::minutes(5));
|
||||
```
|
||||
@@ -1,3 +1,51 @@
|
||||
+++
|
||||
title = "Market Data"
|
||||
+++
|
||||
+++
|
||||
|
||||
|
||||
Use the MarketDataService to subscribe enumerate all available securities.
|
||||
|
||||
```cs
|
||||
using System;
|
||||
using System.Threading;
|
||||
using bts.api;
|
||||
using bts.api.core;
|
||||
|
||||
namespace sample {
|
||||
class Program {
|
||||
static void Main(string[] args) {
|
||||
using var context = new ServiceContext();
|
||||
var md = context.locate<MarketDataService>();
|
||||
using var sub = md.subscribe_securities().Subscribe(update => {
|
||||
foreach (var security in update.added) {
|
||||
Console.WriteLine($"Added security {security.Id}: {security.Symbol}");
|
||||
}
|
||||
});
|
||||
|
||||
Thread.Sleep(TimeSpan.FromMinutes(5));
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```cpp
|
||||
#include "lib/api/public/ServiceContext.h"
|
||||
#include "spark/lib/domain_model/Security.h"
|
||||
#include "lib/api/services/MarketDataService.client.pb.h"
|
||||
#include <thread>
|
||||
|
||||
using namespace bts::api;
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
ServiceContext ctx;
|
||||
auto md_service = ctx.locate<bts::api::MarketDataService>();
|
||||
auto sub = md_service.subscribe_securities().subscribe([](const auto& update) {
|
||||
for (const auto& s : update.added()) {
|
||||
std::cout << "Added security: " << s->id() << ": " << s->symbol() << std::endl;
|
||||
}
|
||||
});
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::minutes(5));
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
@@ -44,6 +44,7 @@ namespace mbts.ui {
|
||||
GearGeometryKey = nameof(GearGeometryKey),
|
||||
HornGeometryKey = nameof(HornGeometryKey),
|
||||
KeyGeometryKey = nameof(KeyGeometryKey),
|
||||
LadderGeometryKey = nameof(LadderGeometryKey),
|
||||
LetterAKey = nameof(LetterAKey),
|
||||
LetterDKey = nameof(LetterDKey),
|
||||
MenuOrientationGeometryKey = nameof(MenuOrientationGeometryKey),
|
||||
|
||||
@@ -217,6 +217,10 @@
|
||||
</sys:String>
|
||||
</x:Array>
|
||||
|
||||
<sys:String x:Key="{x:Static mbts:Icons.LadderGeometryKey}">
|
||||
M -2.7407407 21.999952 V 25 H 11.37037 v -3.000048 z m 0 -4.999952 v 2.999952 H 11.37037 V 17 Z m 0 -5 v 3 H 11.37037 v -3 z m 0 -5 v 3 H 11.37037 V 7 Z m -1 -7 c 0.552 0 1.0810413 0.45297098 1 1 V 5 H 11.37037 V 1 c 0 -0.5529995 0.448 -1 1 -1 0.552 0 1 0.4470005 1 1 0 11.544484 0 19.62158 0 30 0 0.553 -0.448 1 -1 1 -0.552 0 -0.918958 -0.45297 -1 -1 V 27 H -2.7407407 v 4 c 0 0.553 -0.448 1 -1 1 -0.552 0 -1 -0.447 -1 -1 0 -11.54448 0 -19.621581 0 -30 0 -0.5529995 0.448 -1 1 -1 z
|
||||
</sys:String>
|
||||
|
||||
<!--File (the icon is free to use if we include this information in our credit section)
|
||||
Source: https://www.flaticon.com/authors/smashicons
|
||||
Ttile: File free icon
|
||||
|
||||
@@ -60,7 +60,7 @@ namespace bts.shell {
|
||||
public async override Task<IShellWindow> load(LadderSaveState state) {
|
||||
await Wait.until_true(() => _securities.ContainsKey(state.security_key), _ladder_conf.security_timeout);
|
||||
if (_securities.TryGetValue(state.security_key, out SecurityMD security)) {
|
||||
return create_ladder_window(security, state.settings, state.settings_id);
|
||||
return create_ladder_window(security, state.settings, state.settings_id, state.contract_legs_window_family);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -79,7 +79,7 @@ namespace bts.shell {
|
||||
return load(state);
|
||||
}
|
||||
|
||||
LadderSaveState _get_state(LadderVM vm, uint id) {
|
||||
LadderSaveState _get_state(LadderVM vm, uint id, string contract_legs_window_family) {
|
||||
LadderSettings s = vm.get_save_state();
|
||||
if (id != 0) {
|
||||
if (!_settings_hub.current.ladder_profiles.TryGetFirst(lp => lp.id == id, out var result)) {
|
||||
@@ -90,10 +90,15 @@ namespace bts.shell {
|
||||
id = 0;
|
||||
}
|
||||
}
|
||||
return new LadderSaveState { security_key = vm.security.Id, settings = s, settings_id = id };
|
||||
return new LadderSaveState {
|
||||
security_key = vm.security.Id,
|
||||
settings = s,
|
||||
settings_id = id,
|
||||
contract_legs_window_family = contract_legs_window_family
|
||||
};
|
||||
}
|
||||
|
||||
IShellWindow create_ladder_window(SecurityMD md, LadderSettings last_value, uint settings_id = 0) {
|
||||
IShellWindow create_ladder_window(SecurityMD md, LadderSettings last_value, uint settings_id = 0, string contract_legs_window_family = null) {
|
||||
IDisposable active_settings_dialog = null;
|
||||
// on_shell_window_vm should behave like an event (do not set to null)
|
||||
Action<ShellWindowVM> on_shell_window_vm = _ => { };
|
||||
@@ -131,17 +136,54 @@ namespace bts.shell {
|
||||
vm.order_properties_vm.use_algo_param_grid = _ladder_conf.use_algo_param_grid;
|
||||
vm.request_settings_dialog += open_settings_dialog;
|
||||
var settings_subscription = _settings_hub.subscribe_default_settings().Subscribe(on_user_settings);
|
||||
IDisposable contract_legs_window_family_hook = null;
|
||||
vm.OnDisposed += disposing => {
|
||||
Disposable.dispose(ref settings_subscription);
|
||||
Disposable.dispose(ref active_settings_dialog);
|
||||
Disposable.dispose(ref settings_subject);
|
||||
Disposable.dispose(ref contract_legs_window_family_hook);
|
||||
};
|
||||
vm.request_toggle_contract_legs_expanded += toggle_contract_legs_expanded;
|
||||
if (contract_legs_window_family != null)
|
||||
on_contract_legs_expanded(contract_legs_window_family);
|
||||
// view
|
||||
var lv = new LadderView(vm);
|
||||
foreach (var kv in AlgoStudioModelUtils.get_dynamic_resources(vm.model.order_gateway.algo_model.ui_configuration))
|
||||
lv.Resources[kv.Key] = kv.Value;
|
||||
return lv;
|
||||
// local methods
|
||||
void toggle_contract_legs_expanded() {
|
||||
if (shell_window == null)
|
||||
return;
|
||||
Disposable.dispose(ref contract_legs_window_family_hook);
|
||||
if (vm.contract_legs_expanded) {
|
||||
vm.contract_legs_expanded = false;
|
||||
_bus.post(SparkTags.CloseChildwindows, contract_legs_window_family);
|
||||
return;
|
||||
}
|
||||
var fam = ShellWindowFamily.create(shell_window,
|
||||
vm.security.Synthetic.Legs
|
||||
.Select(l => l.Security.Id)
|
||||
.KeyJoin(_securities)
|
||||
.Select(leg_md => create_ladder_window(leg_md, vm.get_save_state())));
|
||||
_bus.post(SparkTags.ShowChildWindows, fam);
|
||||
on_contract_legs_expanded(fam.id);
|
||||
}
|
||||
|
||||
void on_contract_legs_expanded(string win_fam_id) {
|
||||
Disposable.dispose(ref contract_legs_window_family_hook);
|
||||
contract_legs_window_family = win_fam_id ?? throw new ArgumentNullException(nameof(win_fam_id));
|
||||
vm.contract_legs_expanded = true;
|
||||
contract_legs_window_family_hook = _bus.subscribe(SparkTags.ChildWindowsClosed)
|
||||
.Where(id => win_fam_id.Equals(id))
|
||||
.Take(1)
|
||||
.OnDispatcher()
|
||||
.Subscribe(_ => {
|
||||
vm.contract_legs_expanded = false;
|
||||
contract_legs_window_family = null;
|
||||
});
|
||||
}
|
||||
|
||||
void on_settings_invalidated() =>
|
||||
needs_resync = true;
|
||||
|
||||
@@ -267,15 +309,16 @@ namespace bts.shell {
|
||||
Log.Ui.warn($"LadderSettingsDialog error {action_desc}: {error}.");
|
||||
}
|
||||
});
|
||||
return shell_window = ShellWindow.create(
|
||||
shell_window = ShellWindow.create(
|
||||
module, md.security.ToString(), _window_type, view,
|
||||
(v, s) => save(_get_state(((LadderView)v).vm, settings_id), s),
|
||||
(v, s) => save(_get_state(((LadderView)v).vm, settings_id, contract_legs_window_family), s),
|
||||
icon: Icons.Ladder,
|
||||
thread_id: _thread_id_provider.next_id(),
|
||||
vm => {
|
||||
shell_window_vm = vm;
|
||||
title_source.connect();
|
||||
});
|
||||
return shell_window;
|
||||
}
|
||||
|
||||
async void _show_windows(IImmutableSet<uint> securities) {
|
||||
@@ -329,5 +372,7 @@ namespace bts.shell {
|
||||
public mbts.spark.LadderSettings settings;
|
||||
[ProtoMember(3)]
|
||||
public uint settings_id;
|
||||
[ProtoMember(4)]
|
||||
public string contract_legs_window_family;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
using bts.api;
|
||||
using bts.api.orderman;
|
||||
using bts.collections;
|
||||
using bts.shell;
|
||||
using bts.spark;
|
||||
using bts.utils;
|
||||
@@ -43,7 +42,7 @@ namespace mbts.spark {
|
||||
_left_panel_view_port_height,
|
||||
_left_panel_scroll_position,
|
||||
_left_panel_scroll_position_target;
|
||||
bool _show_account_position, _show_day_position, _show_raw_day_position;
|
||||
bool _show_account_position, _show_day_position, _show_raw_day_position, _contract_legs_expanded;
|
||||
ulong _misc_time = 0;
|
||||
ulong _last_trade_time = 0;
|
||||
IEqualityComparer<double> _equality;
|
||||
@@ -154,6 +153,9 @@ namespace mbts.spark {
|
||||
show_book_command = new DelegateCommand(_show_book);
|
||||
show_ticket_command = new DelegateCommand(_show_ticket);
|
||||
settings_command = new DelegateCommand(() => request_settings_dialog?.Invoke());
|
||||
toggle_contract_legs_expanded_cmd = new DelegateCommand(
|
||||
() => request_toggle_contract_legs_expanded?.Invoke(),
|
||||
() => security.ContractCase == Security.ContractOneofCase.Synthetic);
|
||||
|
||||
orders = new ObservableCollection<LadderOrderVM>();
|
||||
|
||||
@@ -257,7 +259,7 @@ namespace mbts.spark {
|
||||
|
||||
public IBus bus => _bus;
|
||||
|
||||
public event Action on_tick, request_settings_dialog, settings_invalidated;
|
||||
public event Action on_tick, request_settings_dialog, settings_invalidated, request_toggle_contract_legs_expanded;
|
||||
|
||||
public MDConsumerHandle md => _model.md;
|
||||
// we return only entries who have piqs and whose piqs are not uint.Max
|
||||
@@ -273,6 +275,9 @@ namespace mbts.spark {
|
||||
|
||||
public ICommand position_click_command { get; }
|
||||
public ICommand status_click_command => SparkCommands.ShowActivityLog.ICommand(_bus);
|
||||
public ICommand toggle_contract_legs_expanded_cmd { get; }
|
||||
public bool can_expand_contract_legs => security.ContractCase == Security.ContractOneofCase.Synthetic;
|
||||
public bool contract_legs_expanded { get => _contract_legs_expanded; set => Set(ref _contract_legs_expanded, value); }
|
||||
|
||||
public async void status_update(Activity a) {
|
||||
if (a.security == _model.security && a.category != ActivityLogCategory.modify) {
|
||||
|
||||
@@ -565,7 +565,65 @@
|
||||
</ScrollViewer>
|
||||
<!-- Ladder Column Headers -->
|
||||
<Border Grid.Column="2" HorizontalAlignment="Stretch">
|
||||
<GridViewHeaderRowPresenter x:Name="_column_headers_presenter" AllowsColumnReorder="False" />
|
||||
<Grid>
|
||||
<GridViewHeaderRowPresenter x:Name="_column_headers_presenter" AllowsColumnReorder="False"/>
|
||||
<Grid HorizontalAlignment="Right" Height="{Binding ActualHeight, ElementName=_column_headers_presenter}">
|
||||
<Grid.Width>
|
||||
<MultiBinding>
|
||||
<MultiBinding.Converter>
|
||||
<converters:DoubleDynamicSubtractionConverter MinimumValue="0"/>
|
||||
</MultiBinding.Converter>
|
||||
<Binding Path="ActualWidth" ElementName="_column_headers_presenter"/>
|
||||
<Binding Path="TotalColumnWidth" RelativeSource="{RelativeSource AncestorType=spark:LadderView}" Mode="OneWay"/>
|
||||
</MultiBinding>
|
||||
</Grid.Width>
|
||||
<Button
|
||||
Grid.Column="1"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Command="{Binding toggle_contract_legs_expanded_cmd}"
|
||||
HorizontalAlignment="Right"
|
||||
Margin="2"
|
||||
Visibility="{Binding can_expand_contract_legs, Converter={StaticResource BooleanToVisibilityConverter}}"
|
||||
>
|
||||
<Button.ToolTip>
|
||||
<Binding Path="contract_legs_expanded">
|
||||
<Binding.Converter>
|
||||
<converters:BooleanToObjectConverter ValueForFalse="Show Legs" ValueForTrue="Hide Legs"/>
|
||||
</Binding.Converter>
|
||||
</Binding>
|
||||
</Button.ToolTip>
|
||||
<Grid>
|
||||
<mbts:StateManager.VisualState>
|
||||
<Binding Path="contract_legs_expanded">
|
||||
<Binding.Converter>
|
||||
<converters:BooleanToObjectConverter ValueForFalse="Collapsed" ValueForTrue="Expanded"/>
|
||||
</Binding.Converter>
|
||||
</Binding>
|
||||
</mbts:StateManager.VisualState>
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup Name="ToggleStates">
|
||||
<VisualState Name="Collapsed"/>
|
||||
<VisualState Name="Expanded">
|
||||
<Storyboard>
|
||||
<DoubleAnimation Storyboard.TargetName="icon" Storyboard.TargetProperty="Opacity" BeginTime="0" Duration="0" To="1.0"/>
|
||||
</Storyboard>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
<Image x:Name="icon" Opacity="0.5">
|
||||
<Image.Source>
|
||||
<DrawingImage>
|
||||
<DrawingImage.Drawing>
|
||||
<GeometryDrawing Brush="White" Geometry="{Binding Source={StaticResource ResourceKey={x:Static mbts:Icons.LadderGeometryKey}}}"/>
|
||||
</DrawingImage.Drawing>
|
||||
</DrawingImage>
|
||||
</Image.Source>
|
||||
</Image>
|
||||
</Grid>
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Border>
|
||||
<!-- LadderGridHost -->
|
||||
<spark:LadderGridHost
|
||||
|
||||
@@ -83,8 +83,16 @@ namespace mbts.spark {
|
||||
internal LadderView(LadderVM vm) {
|
||||
InitializeComponent();
|
||||
_column_headers_presenter.Columns = new GridViewColumnCollection();
|
||||
_ladder.visible_columns_changed += () => _column_headers_presenter.Columns.SetItems(
|
||||
_ladder.visible_column_types.Select(t => ColumnFactory.create(_ladder, t)));
|
||||
_ladder.visible_columns_changed += () => {
|
||||
_column_headers_presenter.Columns.SetItems(
|
||||
_ladder.visible_column_types.Select(t => ColumnFactory.create(_ladder, t)));
|
||||
var mb = new MultiBinding {
|
||||
Converter = new SumConverter()
|
||||
};
|
||||
mb.Bindings.AddRange(_column_headers_presenter.Columns
|
||||
.Select(col => new Binding(nameof(GridViewColumn.ActualWidth)) { Source = col }));
|
||||
SetBinding(TotalColumnWidthProperty, mb);
|
||||
};
|
||||
this.vm = vm;
|
||||
this.ConnectDataContext<LadderVM>(lvm => lvm.bus.subscribe(SparkTags.SecuritiesTag).OnDispatcher().Subscribe(sec_map => Resources[ResourceKeys.Securities] = sec_map));
|
||||
vm.SubscribeDisposed(_connect_left_panel_scroll());
|
||||
@@ -95,6 +103,19 @@ namespace mbts.spark {
|
||||
this.PreviewMouseDown += LadderView_PreviewMouseDown;
|
||||
}
|
||||
|
||||
#region TotalColumnWidth
|
||||
// the expand/collapse legs button (for synthetics) uses this prop
|
||||
// to compute the width between the last column and the edge of the window
|
||||
// so the button can be docked to the top right, without covering the right-most column.
|
||||
public static readonly DependencyProperty TotalColumnWidthProperty =
|
||||
WPFHelper.create_dp<double, LadderView>(nameof(TotalColumnWidth));
|
||||
|
||||
public double TotalColumnWidth {
|
||||
get => (double)GetValue(TotalColumnWidthProperty);
|
||||
set => SetValue(TotalColumnWidthProperty, value);
|
||||
}
|
||||
#endregion
|
||||
|
||||
IDisposable _connect_left_panel_scroll() {
|
||||
// extent height
|
||||
bool busy = false;
|
||||
@@ -252,4 +273,4 @@ namespace mbts.spark {
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,18 @@ namespace mbts.spark {
|
||||
|
||||
public static ITag<IShellWindow> ShowWindow { get; } = new SparkTag<IShellWindow>("Show Window");
|
||||
public static ITag<Task<IShellWindow>> ShowWindowAsync { get; } = new SparkTag<Task<IShellWindow>>("ShowWindowAsync");
|
||||
public static ITag<ShellWindowFamily> ShowChildWindows { get; } = new SparkTag<ShellWindowFamily>(nameof(ShowChildWindows));
|
||||
/// <summary>
|
||||
/// <see cref="CloseChildwindows"/> is a 'request' tag. Once the request is received (presumably by the <see cref="IWindowManager"/>, or a related agent),
|
||||
/// and the child windows are closed, the <see cref="ShellWindowFamily.id"/> is posted under the tag <see cref="ChildWindowsClosed"/>.
|
||||
/// </summary>
|
||||
public static ITag<string> CloseChildwindows { get; } = new SparkTag<string>(nameof(CloseChildwindows));
|
||||
/// <summary>
|
||||
/// When all the child windows in a family are closed (<see cref="ShellWindowFamily.child_windows"/>),
|
||||
/// the <see cref="ShellWindowFamily.id"/> is posted under the tag <see cref="ChildWindowsClosed"/>.
|
||||
/// Note that window families may be closed by request, with the tag <see cref="CloseChildwindows"/>.
|
||||
/// </summary>
|
||||
public static ITag<string> ChildWindowsClosed { get; } = new SparkTag<string>(nameof(ChildWindowsClosed));
|
||||
public static ITag<IShellWindow> ActivateWindow { get; } = new SparkTag<IShellWindow>("Activate Window");
|
||||
public static ITag<Nothing> ShowWorkspaces { get; } = new SparkTag<Nothing>(nameof(ShowWorkspaces));
|
||||
public static ITag<Nothing> ShowSettings { get; } = new SparkTag<Nothing>(nameof(ShowSettings));
|
||||
@@ -92,4 +104,4 @@ namespace mbts.spark {
|
||||
public override string ToString() => Name;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,5 @@
|
||||
using ProtoBuf;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace mbts.ui.shell {
|
||||
public enum DockState {
|
||||
@@ -42,6 +37,22 @@ namespace mbts.ui.shell {
|
||||
|
||||
[ProtoMember(104)]
|
||||
public string custom_title;
|
||||
|
||||
[ProtoMember(105)]
|
||||
public WindowFamilyData? family;
|
||||
}
|
||||
|
||||
[ProtoContract]
|
||||
public readonly struct WindowFamilyData {
|
||||
public WindowFamilyData(string id, bool is_parent) : this() {
|
||||
this.id = id;
|
||||
this.is_parent = is_parent;
|
||||
}
|
||||
|
||||
[ProtoMember(1)]
|
||||
public string id { get; }
|
||||
[ProtoMember(2)]
|
||||
public bool is_parent { get; }
|
||||
}
|
||||
|
||||
[ProtoContract]
|
||||
|
||||
@@ -24,13 +24,15 @@ namespace mbts.ui.converters {
|
||||
|
||||
[ValueConversion(typeof(double[]), typeof(double))]
|
||||
class DoubleDynamicSubtractionConverter : IMultiValueConverter {
|
||||
public double MinimumValue { get; set; } = double.NaN;
|
||||
|
||||
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) {
|
||||
if (values.Length > 0 && values[0] is double difference) {
|
||||
for (int i = 1; i < values.Length; i++) {
|
||||
if (values[i] is double v)
|
||||
difference -= v;
|
||||
}
|
||||
return difference;
|
||||
return double.IsNaN(MinimumValue) || difference > MinimumValue ? difference : MinimumValue;
|
||||
}
|
||||
return new ValidationResult(false, "No doubles.");
|
||||
}
|
||||
|
||||
@@ -115,7 +115,7 @@ namespace mbts.ui.shell
|
||||
}
|
||||
|
||||
// if is_loaded is true, will NOT set width/height
|
||||
public async Task add_document(TabDocument document, int index, bool is_loaded=false) {
|
||||
public async Task add_document(TabDocument document, int index, bool is_loaded = false, bool select_document = true) {
|
||||
Log.assert(document.Parent == null);
|
||||
// Manually copy over the initial size if this is a new Window
|
||||
if (_tabs.Items.Count == 0 && !is_loaded)
|
||||
@@ -126,7 +126,8 @@ namespace mbts.ui.shell
|
||||
_tabs.Items.Insert(Math.Min(index, _tabs.Items.Count), document);
|
||||
document.ViewModel.Closing += TabClosing;
|
||||
document.ViewModel.host_id = host_id;
|
||||
_tabs.SelectedItem = document;
|
||||
if (select_document)
|
||||
_tabs.SelectedItem = document;
|
||||
}
|
||||
|
||||
internal int document_index(IShellWindow shell_window) {
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
using mbts.ui;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace mbts.spark {
|
||||
// Keep this class immutable, as it is designed to be posted to the bus.
|
||||
// It would be unreasonable to expect subscribers to observe changes to this data.
|
||||
class ShellWindowFamily {
|
||||
ShellWindowFamily(string id, IShellWindow parent_window, ImmutableList<IShellWindow> child_windows) {
|
||||
this.id = id;
|
||||
this.parent_window = parent_window;
|
||||
this.child_windows = child_windows;
|
||||
}
|
||||
|
||||
public string id { get; }
|
||||
public IShellWindow parent_window { get; }
|
||||
public ImmutableList<IShellWindow> child_windows { get; }
|
||||
|
||||
public static ShellWindowFamily create(IShellWindow parent_window, IEnumerable<IShellWindow> child_windows) =>
|
||||
new ShellWindowFamily(Guid.NewGuid().ToString(), parent_window, ImmutableList.CreateRange(child_windows));
|
||||
|
||||
public static ShellWindowFamily restore(string id, IShellWindow parent_window = null, IEnumerable<IShellWindow> child_windows = null) =>
|
||||
new ShellWindowFamily(id, parent_window, ImmutableList.CreateRange(child_windows.empty_if_null()));
|
||||
|
||||
public ShellWindowFamily change_parent(IShellWindow parent_window) =>
|
||||
new ShellWindowFamily(id, parent_window, child_windows);
|
||||
|
||||
public ShellWindowFamily add(IShellWindow child_window) =>
|
||||
add_range(new[] { child_window });
|
||||
|
||||
public ShellWindowFamily add_range(IEnumerable<IShellWindow> child_windows) =>
|
||||
new ShellWindowFamily(id, parent_window, this.child_windows.AddRange(child_windows));
|
||||
|
||||
public ShellWindowFamily remove(IShellWindow child_window) =>
|
||||
remove_range(new[] { child_window });
|
||||
|
||||
public ShellWindowFamily remove_range(IEnumerable<IShellWindow> child_windows) =>
|
||||
new ShellWindowFamily(id, parent_window, this.child_windows.RemoveRange(child_windows));
|
||||
}
|
||||
}
|
||||
@@ -64,6 +64,21 @@ namespace mbts.ui.shell
|
||||
Visibility = System.Windows.Visibility.Visible;
|
||||
}
|
||||
|
||||
internal async Task close_async() {
|
||||
FloatingWindow window = Window.GetWindow(this) as FloatingWindow;
|
||||
if (window.SingleChild) {
|
||||
if (_content.CloseCommand.CanExecute(null)) {
|
||||
_content.CloseCommand.Execute(null);
|
||||
}
|
||||
}
|
||||
else {
|
||||
await hide();
|
||||
if (_content.CloseCommand.CanExecute(null)) {
|
||||
_content.CloseCommand.Execute(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal async Task slide_over() {
|
||||
Storyboard sb = Resources["_slide"] as Storyboard;
|
||||
if (sb != null) {
|
||||
@@ -78,19 +93,7 @@ namespace mbts.ui.shell
|
||||
}
|
||||
}
|
||||
|
||||
async void close_click(object sender, RoutedEventArgs e) {
|
||||
FloatingWindow window = Window.GetWindow(this) as FloatingWindow;
|
||||
if (window.SingleChild) {
|
||||
if (_content.CloseCommand.CanExecute(null)) {
|
||||
_content.CloseCommand.Execute(null);
|
||||
}
|
||||
}
|
||||
else {
|
||||
await hide();
|
||||
if (_content.CloseCommand.CanExecute(null)) {
|
||||
_content.CloseCommand.Execute(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
void close_click(object sender, RoutedEventArgs e) =>
|
||||
close_async().ignore();
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,12 @@ namespace mbts.ui.shell {
|
||||
public class WindowManager : IWindowManager, IWindowsInterop, IDisposable {
|
||||
Dictionary<IShellWindow, TabDocument> _documents = new Dictionary<IShellWindow, TabDocument>();
|
||||
Dictionary<string, FloatingWindow> _windows = new Dictionary<string, FloatingWindow>();
|
||||
/// Given a window family, each family member (all child windows, and the parent) should have an entry in <see cref="_reverse_family_map"/>,
|
||||
/// and each entry should have the same <see cref="MutableWindowFamily"/> reference.
|
||||
/// Note: a 'mutable' window family type is used because the reverse mapping is non-linear.
|
||||
/// This way, when a family changes, the change does not need to be propagated to each child window's entry.
|
||||
readonly Dictionary<IShellWindow, MutableWindowFamily> _reverse_family_map = new Dictionary<IShellWindow, MutableWindowFamily>();
|
||||
readonly Dictionary<string, MutableWindowFamily> _family_map = new Dictionary<string, MutableWindowFamily>();
|
||||
readonly IShellThreadFactory _threads;
|
||||
Lazy<ISettingsCollector> _window_settings;
|
||||
readonly ISavedWindowFactory _window_factory;
|
||||
@@ -46,33 +52,54 @@ namespace mbts.ui.shell {
|
||||
Disposable.Create(
|
||||
bus.subscribe(SparkTags.ShowWindow).OnDispatcher().Subscribe(w => add(w).ignore()),
|
||||
bus.subscribe(SparkTags.ShowWindowAsync).OnDispatcher().Subscribe(w => add(w).ignore()),
|
||||
bus.subscribe(SparkTags.ShowChildWindows).OnDispatcher().Subscribe(_show_child_windows),
|
||||
bus.subscribe(SparkTags.CloseChildwindows).OnDispatcher().Subscribe(_close_window_family),
|
||||
bus.subscribe(SparkTags.ActivateWindow).OnDispatcher().Subscribe(MakeVisible)
|
||||
);
|
||||
}
|
||||
|
||||
#region Window creation
|
||||
async Task<TabDocument> _create_document(IShellWindow model) {
|
||||
async Task<TabDocument> _create_document(IShellWindow model, string host_id = null, bool activate = true) {
|
||||
var viewmodel = new ShellWindowVM(model, this, _threads);
|
||||
if (host_id != null) {
|
||||
viewmodel.host_id = host_id;
|
||||
}
|
||||
var content = new TabDocument(this, viewmodel);
|
||||
var state = model.start_position();
|
||||
if (state != null) {
|
||||
viewmodel.host_id = state.host_id ?? "";
|
||||
viewmodel.TitleOverride = state.custom_title;
|
||||
if (state.family.HasValue) {
|
||||
var f = state.family.Value;
|
||||
if (f.id != null) {
|
||||
lock (_lock) {
|
||||
if (!_family_map.TryGetValue(f.id, out var mfam)) {
|
||||
_family_map[f.id] = mfam = new MutableWindowFamily(ShellWindowFamily.restore(f.id));
|
||||
}
|
||||
_reverse_family_map[model] = mfam;
|
||||
if (state.family.Value.is_parent) {
|
||||
mfam.parent_window = model;
|
||||
} else {
|
||||
mfam.add_child(model);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var window = await CreateWindow(content, state);
|
||||
var window = await CreateWindow(content, state, activate);
|
||||
|
||||
window.Show();
|
||||
return content;
|
||||
}
|
||||
|
||||
public async Task<Window> CreateWindow(TabDocument document, WindowPosition position = null) {
|
||||
public async Task<Window> CreateWindow(TabDocument document, WindowPosition position = null, bool activate = true) {
|
||||
var key = document.ViewModel.host_id;
|
||||
FloatingWindow window;
|
||||
if (_windows.TryGetValue(key, out window)) {
|
||||
if (position != null) {
|
||||
await window.add_document(document, position.tab_index, true);
|
||||
await window.add_document(document, position.tab_index, true, activate);
|
||||
} else {
|
||||
await window.add_document(document, -1);
|
||||
await window.add_document(document, -1, select_document: activate);
|
||||
}
|
||||
} else {
|
||||
if (position != null) {
|
||||
@@ -95,12 +122,15 @@ namespace mbts.ui.shell {
|
||||
|
||||
public IReadOnlyList<TabDocument> list_documents() => _documents.Values.ToList();
|
||||
|
||||
public async Task add(IShellWindow window) {
|
||||
public Task add(IShellWindow window) =>
|
||||
add(window, null, true);
|
||||
|
||||
public async Task add(IShellWindow window, string host_id, bool activate) {
|
||||
if (window != null) {
|
||||
if (!Application.Current.Dispatcher.CheckAccess())
|
||||
await Application.Current.Dispatcher.InvokeAsync(() => add(window));
|
||||
Log.Ui.info($"Opening window {window.diagnostic_name()}.");
|
||||
var doc = await _create_document(window);
|
||||
var doc = await _create_document(window, host_id, activate);
|
||||
lock (_lock) {
|
||||
_documents[window] = doc;
|
||||
}
|
||||
@@ -130,6 +160,7 @@ namespace mbts.ui.shell {
|
||||
}
|
||||
}
|
||||
|
||||
// Note: this method should be called from within a lock
|
||||
void _remove(IShellWindow shell_window) {
|
||||
if (_documents.TryGetValue(shell_window, out TabDocument document)) {
|
||||
var key = document.ViewModel.host_id;
|
||||
@@ -138,6 +169,33 @@ namespace mbts.ui.shell {
|
||||
}
|
||||
document.ViewModel?.Dispose();
|
||||
_documents.Remove(shell_window);
|
||||
if (_reverse_family_map.TryGetValue(shell_window, out var mfam)) {
|
||||
if (mfam.parent_window == shell_window) {
|
||||
_close_window_family_impl(mfam.id);
|
||||
} else {
|
||||
_reverse_family_map.Remove(shell_window);
|
||||
mfam.remove(shell_window);
|
||||
if (!mfam.child_windows.Any()) {
|
||||
// all that is left is the parent, without any children,
|
||||
// it can be treated like a regular window
|
||||
_family_map.Remove(mfam.id);
|
||||
// The parent window could be null, if a workspace was just loaded,
|
||||
// more specifically: if a set of child windows were restored, and closed (i.e., if the user managed to click x quick enough),
|
||||
// before the parent window was restored.
|
||||
if (mfam.parent_window != null) {
|
||||
_reverse_family_map.Remove(mfam.parent_window);
|
||||
}
|
||||
_bus.post(SparkTags.ChildWindowsClosed, mfam.id);
|
||||
// The ChildWindowsClosed tag functions as a stateless signal, meaning
|
||||
// new subscribers should not be notified of the most recent closed window family.
|
||||
// Once the signal is sent, the state must be whiped.
|
||||
// A null family id is posted under the ChildWindowsClosed tag to do just that.
|
||||
// TODO: as the bus is not designed for this kind of stateless signal communication,
|
||||
// it would be prudent to come up with a more precise implementation for this behavior.
|
||||
_bus.post(SparkTags.ChildWindowsClosed, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,7 +228,12 @@ namespace mbts.ui.shell {
|
||||
pos.tab_index = window.document_index(shell_window);
|
||||
pos.host_id = window.host_id;
|
||||
pos.custom_title = doc?.ViewModel?.TitleOverride;
|
||||
return pos;
|
||||
lock (_lock) {
|
||||
if (_reverse_family_map.TryGetValue(shell_window, out var mfam)) {
|
||||
pos.family = new WindowFamilyData(mfam.id, mfam.parent_window == shell_window);
|
||||
}
|
||||
return pos;
|
||||
}
|
||||
}
|
||||
Log.Ui.warn("IShellWindow '{0}' does not have a valid position. It may not be displayed".fmt(shell_window.Title));
|
||||
return new WindowPosition();
|
||||
@@ -215,5 +278,107 @@ namespace mbts.ui.shell {
|
||||
public void Dispose() {
|
||||
Disposable.dispose(ref _dispose);
|
||||
}
|
||||
|
||||
// Note: this method uses a lock.
|
||||
/// If the call to this method is already within a lock,
|
||||
/// use <see cref="_close_window_family_impl(string)"/> instead.
|
||||
void _close_window_family(string family_id) {
|
||||
lock (_lock)
|
||||
_close_window_family_impl(family_id);
|
||||
}
|
||||
|
||||
// Note: this method should be called from within a lock
|
||||
void _close_window_family_impl(string family_id) {
|
||||
if (family_id == null)
|
||||
return;
|
||||
if (_family_map.TryGetValue(family_id, out var mfam)) {
|
||||
_reverse_family_map.Remove(mfam.parent_window);
|
||||
foreach (var cw in mfam.child_windows) {
|
||||
if (_documents.TryGetValue(cw, out var tab_doc))
|
||||
tab_doc.close_async().ignore();
|
||||
_reverse_family_map.Remove(cw);
|
||||
}
|
||||
_family_map.Remove(family_id);
|
||||
}
|
||||
}
|
||||
|
||||
async void _show_child_windows(ShellWindowFamily family) {
|
||||
if (family == null || _family_map.ContainsKey(family.id))
|
||||
return;
|
||||
if (_try_get_tab(family.parent_window, out var tab) &&
|
||||
Window.GetWindow(tab) is FloatingWindow win) {
|
||||
var mfam = new MutableWindowFamily(family);
|
||||
var log = new List<string>();
|
||||
lock (_lock) {
|
||||
_reverse_family_map[family.parent_window] = mfam;
|
||||
// remove any child windows that already have a parent
|
||||
foreach (var cw in family.child_windows) {
|
||||
if (_reverse_family_map.TryGetValue(cw, out var old_family)) {
|
||||
// imposter! this child already has a family...someone is misusing the ShowChildWindows tag
|
||||
log.Add($"Unable to add child window to the specified parent window (family id '{family.id}'): child already has a parent (family id {old_family.id}).");
|
||||
mfam.remove(cw);
|
||||
} else {
|
||||
_reverse_family_map[cw] = mfam;
|
||||
}
|
||||
}
|
||||
_family_map[family.id] = mfam;
|
||||
}
|
||||
foreach (var l in log)
|
||||
Log.Ui.warn(l);
|
||||
using (var en = mfam.child_windows.GetEnumerator()) {
|
||||
if (en.MoveNext()) {
|
||||
// use the first child to create a new window adjacent to the parent
|
||||
// on the right, then add the remaining children as tabs.
|
||||
// Note: different startup locations/behavior can be implemented as needed,
|
||||
// this specific implementation addresses the spreader leg ladders issue:
|
||||
// https://bluetradesys.atlassian.net/browse/SP-250
|
||||
await add(en.Current, null, true);
|
||||
var cwin = window_for(en.Current);
|
||||
cwin.Left = win.Right();
|
||||
cwin.Top = win.Top;
|
||||
cwin.ensure_onscreen();
|
||||
if ((cwin.Left - win.Right()) < -1) {
|
||||
// try expanding to the left
|
||||
cwin.Left = win.Left - cwin.ActualWidth;
|
||||
cwin.ensure_onscreen();
|
||||
}
|
||||
while (en.MoveNext()) {
|
||||
add(en.Current, cwin.host_id, false).ignore();
|
||||
}
|
||||
}
|
||||
}
|
||||
// Prevent the bus from holding a reference to the windows in the family
|
||||
_bus.post(SparkTags.ShowChildWindows, null);
|
||||
}
|
||||
}
|
||||
|
||||
bool _try_get_tab(IShellWindow shell_window, out TabDocument result) {
|
||||
lock (_lock) {
|
||||
return _documents.TryGetValue(shell_window, out result);
|
||||
}
|
||||
}
|
||||
|
||||
class MutableWindowFamily {
|
||||
ShellWindowFamily _family;
|
||||
|
||||
public MutableWindowFamily(ShellWindowFamily family) =>
|
||||
_family = family;
|
||||
|
||||
public string id => _family.id;
|
||||
|
||||
public IShellWindow parent_window {
|
||||
get => _family.parent_window;
|
||||
set => _family = _family.change_parent(value);
|
||||
}
|
||||
|
||||
public IEnumerable<IShellWindow> child_windows =>
|
||||
_family.child_windows;
|
||||
|
||||
public void add_child(IShellWindow child) =>
|
||||
_family = _family.add(child);
|
||||
|
||||
public void remove(IShellWindow child) =>
|
||||
_family = _family.remove(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -8,7 +8,7 @@
|
||||
"name": "(gdb) Launch",
|
||||
"type": "cppdbg",
|
||||
"request": "launch",
|
||||
"program": "/sparkbin/sparkd",
|
||||
"program": "${workspaceFolder}/client_sdk_example",
|
||||
"args": ["run"],
|
||||
"stopAtEntry": false,
|
||||
"cwd": "/sparkbin",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "spark/lib/domain_model/Security.h"
|
||||
#include "lib/api/services/AdminService.client.pb.h"
|
||||
#include "lib/api/services/PositionService.client.pb.h"
|
||||
#include "lib/api/services/MarketDataService.client.pb.h"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -30,6 +31,17 @@ int main(int argc, char** argv) {
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
auto md_service = ctx.locate<bts::api::MarketDataService>();
|
||||
auto sub2 =
|
||||
md_service
|
||||
.subscribe_securities()
|
||||
.subscribe([](const auto& update) {
|
||||
for (const auto& s : update.added()) {
|
||||
std::cout << "Added security: " << s->id() << ": " << s->symbol() << std::endl;
|
||||
}
|
||||
});
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::minutes(5));
|
||||
return 0;
|
||||
}
|
||||
@@ -23,6 +23,8 @@ Ports {
|
||||
value {
|
||||
config_port: 9000
|
||||
qbus_port: 9001
|
||||
glbx_sim_fix_port: 9002
|
||||
ice_sim_fix_port: 9003
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user