diff --git a/Quantum.Editor.Avalonia/App.axaml b/Quantum.Editor.Avalonia/App.axaml
index 1de3cff..1c8e617 100644
--- a/Quantum.Editor.Avalonia/App.axaml
+++ b/Quantum.Editor.Avalonia/App.axaml
@@ -2,8 +2,10 @@
x:Class="Quantum.Editor.Avalonia.App"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:dockTheme="using:Dock.Avalonia.Themes.Fluent"
RequestedThemeVariant="Dark">
+
diff --git a/Quantum.Editor.Avalonia/MainWindow.axaml b/Quantum.Editor.Avalonia/MainWindow.axaml
index 45377e7..48e1cbe 100644
--- a/Quantum.Editor.Avalonia/MainWindow.axaml
+++ b/Quantum.Editor.Avalonia/MainWindow.axaml
@@ -2,7 +2,7 @@
x:Class="Quantum.Editor.Avalonia.MainWindow"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
- xmlns:controls="using:Quantum.Editor.Avalonia.Controls"
+ xmlns:dock="using:Dock.Avalonia.Controls"
Title="Quantum CoasterWorks Editor"
Width="1480"
Height="920"
@@ -42,7 +42,7 @@
-
+
+
+
@@ -106,50 +114,15 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
(StringComparer.Ordinal)
+ {
+ [WorkspacePaneIds.Route] = RoutePane,
+ [WorkspacePaneIds.Viewport] = ViewportPane,
+ [WorkspacePaneIds.Inspector] = InspectorPane,
+ [WorkspacePaneIds.MathPlots] = MathPlotsPane,
+ [WorkspacePaneIds.Diagnostics] = DiagnosticsPane
+ });
+ DockHost.Factory = docking.Factory;
+ DockHost.Layout = docking.Layout;
+
this.workspace.WorkspaceChanged += OnWorkspaceChanged;
this.workspace.StationCursorChanged += OnStationCursorChanged;
this.workspace.SectionHighlightChanged += OnSectionHighlightChanged;
@@ -57,6 +85,8 @@ public MainWindow(
public WorkspaceProfileManager WorkspaceProfiles => workspaceProfiles;
+ public EditorDockingAdapter Docking => docking;
+
private static EditorWorkspace CreateWorkspace()
{
var result = new EditorWorkspace();
@@ -64,6 +94,17 @@ private static EditorWorkspace CreateWorkspace()
return result;
}
+ private void WirePaneInteractions()
+ {
+ RoutePane.NodeSelected += OnGraphNodeSelected;
+ RoutePane.SectionPointerChanged += OnSectionPointerChanged;
+ ViewportPane.SectionPointerChanged += OnSectionPointerChanged;
+ ViewportPane.SampleSelected += OnViewportSampleSelected;
+ MathPlotsPane.SectionPointerChanged += OnSectionPointerChanged;
+ MathPlotsPane.StationChanged += OnEngineeringPlotStationChanged;
+ MathPlotsPane.StationSelected += OnEngineeringPlotStationSelected;
+ }
+
private void OnWorkspaceChanged(object? sender, EventArgs eventArgs)
{
UpdateWorkspaceView();
@@ -77,38 +118,21 @@ private void OnCurrentProfileChanged(object? sender, EventArgs eventArgs)
private void ApplyWorkspaceProfile(WorkspaceProfile profile)
{
- bool showRoute = profile.IsPaneVisibleByDefault(WorkspacePaneIds.Route);
- bool showViewport = profile.IsPaneVisibleByDefault(WorkspacePaneIds.Viewport);
- bool showInspector = profile.IsPaneVisibleByDefault(WorkspacePaneIds.Inspector);
- bool showMathPlots = profile.IsPaneVisibleByDefault(WorkspacePaneIds.MathPlots);
- bool showDiagnostics = profile.IsPaneVisibleByDefault(WorkspacePaneIds.Diagnostics);
- bool showBottomPanel = showMathPlots || showDiagnostics;
-
- RoutePane.IsVisible = showRoute;
- ViewportPane.IsVisible = showViewport;
- InspectorPane.IsVisible = showInspector;
- RouteSplitter.IsVisible = showRoute && showViewport;
- InspectorSplitter.IsVisible = showViewport && showInspector;
- MathPlotsTab.IsVisible = showMathPlots;
- DiagnosticsTab.IsVisible = showDiagnostics;
- BottomPanelRegion.IsVisible = showBottomPanel;
- BottomPanelSplitter.IsVisible = showBottomPanel;
- if (showBottomPanel &&
- BottomWorkspaceTabs.SelectedItem is Control selectedTab &&
- !selectedTab.IsVisible)
- {
- BottomWorkspaceTabs.SelectedItem = showMathPlots ? MathPlotsTab : DiagnosticsTab;
- }
-
- ContentPaneGrid.ColumnDefinitions[0].Width = new GridLength(showRoute ? 320 : 0);
- ContentPaneGrid.ColumnDefinitions[1].Width = new GridLength(showRoute && showViewport ? 5 : 0);
- ContentPaneGrid.ColumnDefinitions[2].Width = showViewport
- ? new GridLength(1, GridUnitType.Star)
- : new GridLength(0);
- ContentPaneGrid.ColumnDefinitions[3].Width = new GridLength(showViewport && showInspector ? 5 : 0);
- ContentPaneGrid.ColumnDefinitions[4].Width = new GridLength(showInspector ? 340 : 0);
- WorkbenchGrid.RowDefinitions[3].Height = new GridLength(showBottomPanel ? 5 : 0);
- WorkbenchGrid.RowDefinitions[4].Height = new GridLength(showBottomPanel ? 300 : 0);
+ docking.SetPaneVisible(
+ WorkspacePaneIds.Route,
+ profile.IsPaneVisibleByDefault(WorkspacePaneIds.Route));
+ docking.SetPaneVisible(
+ WorkspacePaneIds.Viewport,
+ profile.IsPaneVisibleByDefault(WorkspacePaneIds.Viewport));
+ docking.SetPaneVisible(
+ WorkspacePaneIds.Inspector,
+ profile.IsPaneVisibleByDefault(WorkspacePaneIds.Inspector));
+ docking.SetPaneVisible(
+ WorkspacePaneIds.MathPlots,
+ profile.IsPaneVisibleByDefault(WorkspacePaneIds.MathPlots));
+ docking.SetPaneVisible(
+ WorkspacePaneIds.Diagnostics,
+ profile.IsPaneVisibleByDefault(WorkspacePaneIds.Diagnostics));
bool showFileCommands = profile.HasCommandGroup(WorkspaceCommandGroupIds.File);
bool showEditCommands = profile.HasCommandGroup(WorkspaceCommandGroupIds.Edit);
@@ -361,7 +385,15 @@ private void OnMenuShowFramesClick(object? sender, RoutedEventArgs eventArgs)
private void OnShowEngineeringPlotsClick(object? sender, RoutedEventArgs eventArgs)
{
- BottomWorkspaceTabs.SelectedIndex = 0;
+ docking.ShowPane(WorkspacePaneIds.MathPlots);
+ }
+
+ private void OnShowPaneClick(object? sender, RoutedEventArgs eventArgs)
+ {
+ if (sender is MenuItem { Tag: string paneId })
+ {
+ docking.ShowPane(paneId);
+ }
}
private void OnEngineeringPlotStationChanged(
diff --git a/Quantum.Editor.Avalonia/Quantum.Editor.Avalonia.csproj b/Quantum.Editor.Avalonia/Quantum.Editor.Avalonia.csproj
index 011611c..d42e81c 100644
--- a/Quantum.Editor.Avalonia/Quantum.Editor.Avalonia.csproj
+++ b/Quantum.Editor.Avalonia/Quantum.Editor.Avalonia.csproj
@@ -12,6 +12,9 @@
+
+
+
diff --git a/Quantum.Editor.Avalonia/Services/Docking/DockPaneRegistration.cs b/Quantum.Editor.Avalonia/Services/Docking/DockPaneRegistration.cs
new file mode 100644
index 0000000..5d08543
--- /dev/null
+++ b/Quantum.Editor.Avalonia/Services/Docking/DockPaneRegistration.cs
@@ -0,0 +1,30 @@
+namespace Quantum.Editor.Avalonia.Services.Docking;
+
+///
+/// Frontend-only metadata for one dockable editor pane.
+///
+public sealed class DockPaneRegistration
+{
+ public DockPaneRegistration(string id, string title, bool canClose = true)
+ {
+ if (string.IsNullOrWhiteSpace(id))
+ {
+ throw new ArgumentException("A dock pane identifier cannot be empty.", nameof(id));
+ }
+
+ if (string.IsNullOrWhiteSpace(title))
+ {
+ throw new ArgumentException("A dock pane title cannot be empty.", nameof(title));
+ }
+
+ Id = id.Trim();
+ Title = title.Trim();
+ CanClose = canClose;
+ }
+
+ public string Id { get; }
+
+ public string Title { get; }
+
+ public bool CanClose { get; }
+}
diff --git a/Quantum.Editor.Avalonia/Services/Docking/DockPaneRegistry.cs b/Quantum.Editor.Avalonia/Services/Docking/DockPaneRegistry.cs
new file mode 100644
index 0000000..1b479bd
--- /dev/null
+++ b/Quantum.Editor.Avalonia/Services/Docking/DockPaneRegistry.cs
@@ -0,0 +1,57 @@
+using Quantum.Editor.Avalonia.Services.Workspaces;
+
+namespace Quantum.Editor.Avalonia.Services.Docking;
+
+///
+/// Registers the panes understood by the Avalonia docking composition root.
+///
+public sealed class DockPaneRegistry
+{
+ private readonly Dictionary panesById =
+ new(StringComparer.Ordinal);
+ private readonly List panes = new();
+
+ public IReadOnlyList Panes => panes.AsReadOnly();
+
+ public static DockPaneRegistry CreateDefaultTrack()
+ {
+ var result = new DockPaneRegistry();
+ result.Register(new DockPaneRegistration(WorkspacePaneIds.Route, "Route"));
+ result.Register(new DockPaneRegistration(
+ WorkspacePaneIds.Viewport,
+ "Viewport",
+ canClose: false));
+ result.Register(new DockPaneRegistration(WorkspacePaneIds.Inspector, "Inspector"));
+ result.Register(new DockPaneRegistration(WorkspacePaneIds.MathPlots, "Math Plots"));
+ result.Register(new DockPaneRegistration(WorkspacePaneIds.Diagnostics, "Diagnostics"));
+ return result;
+ }
+
+ public void Register(DockPaneRegistration pane)
+ {
+ ArgumentNullException.ThrowIfNull(pane);
+ if (panesById.ContainsKey(pane.Id))
+ {
+ throw new InvalidOperationException($"Dock pane '{pane.Id}' is already registered.");
+ }
+
+ panesById.Add(pane.Id, pane);
+ panes.Add(pane);
+ }
+
+ public bool Contains(string paneId) =>
+ !string.IsNullOrWhiteSpace(paneId) && panesById.ContainsKey(paneId);
+
+ public bool TryGet(string paneId, out DockPaneRegistration pane) =>
+ panesById.TryGetValue(paneId, out pane!);
+
+ public DockPaneRegistration Get(string paneId)
+ {
+ if (!panesById.TryGetValue(paneId, out DockPaneRegistration? pane))
+ {
+ throw new KeyNotFoundException($"Dock pane '{paneId}' is not registered.");
+ }
+
+ return pane;
+ }
+}
diff --git a/Quantum.Editor.Avalonia/Services/Docking/DockingLayoutIds.cs b/Quantum.Editor.Avalonia/Services/Docking/DockingLayoutIds.cs
new file mode 100644
index 0000000..ce250a6
--- /dev/null
+++ b/Quantum.Editor.Avalonia/Services/Docking/DockingLayoutIds.cs
@@ -0,0 +1,15 @@
+namespace Quantum.Editor.Avalonia.Services.Docking;
+
+///
+/// Stable identifiers for the frontend-only default docking graph.
+///
+public static class DockingLayoutIds
+{
+ public const string Root = "track-root";
+ public const string Main = "track-main";
+ public const string Top = "track-top";
+ public const string RouteHost = "track-route-host";
+ public const string ViewportHost = "track-viewport-host";
+ public const string InspectorHost = "track-inspector-host";
+ public const string BottomHost = "track-bottom-host";
+}
diff --git a/Quantum.Editor.Avalonia/Services/Docking/EditorDockFactory.cs b/Quantum.Editor.Avalonia/Services/Docking/EditorDockFactory.cs
new file mode 100644
index 0000000..8a240c6
--- /dev/null
+++ b/Quantum.Editor.Avalonia/Services/Docking/EditorDockFactory.cs
@@ -0,0 +1,148 @@
+using Dock.Avalonia.Controls;
+using Dock.Model.Controls;
+using Dock.Model.Core;
+using Dock.Model.Mvvm;
+using Dock.Model.Mvvm.Controls;
+using Quantum.Editor.Avalonia.Services.Workspaces;
+
+namespace Quantum.Editor.Avalonia.Services.Docking;
+
+///
+/// Dock.Avalonia model factory for the current Track workspace composition.
+///
+internal sealed class EditorDockFactory : Factory
+{
+ private readonly DockPaneRegistry registry;
+ private readonly IReadOnlyDictionary paneContexts;
+ private readonly Dictionary panes = new(StringComparer.Ordinal);
+
+ public EditorDockFactory(
+ DockPaneRegistry registry,
+ IReadOnlyDictionary paneContexts)
+ {
+ this.registry = registry ?? throw new ArgumentNullException(nameof(registry));
+ this.paneContexts = paneContexts ?? throw new ArgumentNullException(nameof(paneContexts));
+ HideToolsOnClose = true;
+ }
+
+ public IDockable GetPane(string paneId) => panes[paneId];
+
+ public override IRootDock CreateLayout()
+ {
+ panes.Clear();
+
+ IDockable route = CreatePane(WorkspacePaneIds.Route);
+ IDockable viewport = CreatePane(WorkspacePaneIds.Viewport);
+ IDockable inspector = CreatePane(WorkspacePaneIds.Inspector);
+ IDockable mathPlots = CreatePane(WorkspacePaneIds.MathPlots);
+ IDockable diagnostics = CreatePane(WorkspacePaneIds.Diagnostics);
+
+ var routeHost = new ToolDock
+ {
+ Id = DockingLayoutIds.RouteHost,
+ Title = "Route",
+ Alignment = Alignment.Left,
+ Proportion = 0.22,
+ ActiveDockable = route,
+ VisibleDockables = CreateList(route)
+ };
+ var viewportHost = new DocumentDock
+ {
+ Id = DockingLayoutIds.ViewportHost,
+ Title = "Viewport",
+ IsCollapsable = false,
+ CanCreateDocument = false,
+ EnableWindowDrag = false,
+ Proportion = 0.55,
+ ActiveDockable = viewport,
+ VisibleDockables = CreateList(viewport)
+ };
+ var inspectorHost = new ToolDock
+ {
+ Id = DockingLayoutIds.InspectorHost,
+ Title = "Inspector",
+ Alignment = Alignment.Right,
+ Proportion = 0.23,
+ ActiveDockable = inspector,
+ VisibleDockables = CreateList(inspector)
+ };
+ var top = new ProportionalDock
+ {
+ Id = DockingLayoutIds.Top,
+ Title = "Track workbench",
+ Orientation = Orientation.Horizontal,
+ Proportion = 0.64,
+ ActiveDockable = viewportHost,
+ VisibleDockables = CreateList(
+ routeHost,
+ new ProportionalDockSplitter(),
+ viewportHost,
+ new ProportionalDockSplitter(),
+ inspectorHost)
+ };
+ var bottomHost = new ToolDock
+ {
+ Id = DockingLayoutIds.BottomHost,
+ Title = "Engineering",
+ Alignment = Alignment.Bottom,
+ Proportion = 0.36,
+ ActiveDockable = mathPlots,
+ VisibleDockables = CreateList(mathPlots, diagnostics)
+ };
+ var main = new ProportionalDock
+ {
+ Id = DockingLayoutIds.Main,
+ Title = "Track workspace",
+ Orientation = Orientation.Vertical,
+ ActiveDockable = top,
+ VisibleDockables = CreateList(
+ top,
+ new ProportionalDockSplitter(),
+ bottomHost)
+ };
+
+ var root = (RootDock)CreateRootDock();
+ root.Id = DockingLayoutIds.Root;
+ root.Title = "Track workspace";
+ root.IsCollapsable = false;
+ root.DefaultDockable = main;
+ root.ActiveDockable = main;
+ root.VisibleDockables = CreateList(main);
+ return root;
+ }
+
+ public override void InitLayout(IDockable layout)
+ {
+ ContextLocator = registry.Panes.ToDictionary(
+ pane => pane.Id,
+ pane => new Func