Multi-Document Investigation (Phase 10 F10.4)
Multi-Document Investigation (Phase 10 F10.4)
The Phase 10 plan’s F10.4 was a time-boxed spike: see whether
juce::TabbedComponent could host multiple AppController instances per
window, à la Guitar Pro. Outcome: deferred to wishlist. The spike
isn’t a one-day refactor — it touches every panel that listens to
AppController plus the entire MainWindow command-routing layer. This
doc captures what we found so a future session can pick up where this
left off without re-walking the same surface.
What’s coupled to a single AppController today
Direct ownership
MainWindow owns a single AppController controller_ member
(MainWindow.h:78). Every command target, file-menu handler, drop
target, and panel-construction site references that one instance. 31
direct controller_.<method> call sites in MainWindow.cpp.
ChangeListener fanout
Five panels register themselves as ChangeListener on the controller:
MainWindowitself (for re-painting + status updates)TrackListPanelUnknownTechniquesBannerTempoMapPanel- (
ProfileEditorPanelreloads its profile list manually rather than via change broadcast, so it isn’t on the listener wire — but it does hold anAppController&in its ctor.)
Modal panels that capture the controller
ExportDialog, ProfileEditorPanel, and TempoMapPanel all hold
AppController& references for their lifetime. They’re spawned modally
from MainWindow, so they always reference whichever controller was
“current” at spawn time. In a multi-doc world, two scenarios apply:
- Per-tab modals: each tab spawns its own export/profile/tempo panel, scoped to that tab’s controller.
- Global profile editor, per-tab project editors: ProfileStore is process-wide (the user’s profile library doesn’t change with the active project), so the profile editor should probably remain global. ExportDialog and TempoMapPanel are clearly per-tab.
Picking which modal lives where is the kind of UX decision the spike was supposed to surface but that needs collaborative discussion before implementation.
What multi-document would require
AppShell class (new)
Owns the juce::TabbedComponent, manages an OwnedArray<AppController>
or equivalent, and exposes:
AppController& activeController()for command targets to dispatch throughvoid openProjectInNewTab(const juce::File&)for the file menuvoid closeActiveTab()with the dirty-state promptvoid closeAllTabs()for the window-close flow, iterating each tab
MainWindow shrinks to a thin frame around AppShell plus the menu
bar.
Listener re-targeting
TrackListPanel, UnknownTechniquesBanner, TempoMapPanel are owned
per-tab today (or would need to become so). The simplest model:
duplicate the panel set per tab. Each tab is its own
(AppController + TrackListPanel + UnknownTechniquesBanner) triple.
Modals stay global but route to appShell_->activeController() at
spawn time.
The implementation cost is real but contained: each panel stays
unaware of the multi-doc machinery; AppShell just constructs N
copies.
Command routing
Today’s ApplicationCommandTarget chain in MainWindow calls
controller_.<method> directly. Multi-doc replaces those with
appShell_->activeController().<method>. Mechanical change, ~30 sites.
Recent projects + drag-and-drop
AppPreferences is process-global today — no change needed for the
recent-projects list itself. Selecting a recent file should
spawn a new tab, not re-bind the active tab. Same for DnD in
MainWindow::filesDropped.
Window close
closeButtonPressed today shows a single dirty-state prompt for the
sole controller. Multi-doc walks every tab in close order, prompting
per dirty tab. JUCE’s modal-window pattern handles this with a
recursive callback, but it’s not trivial — the close flow is async, so
“prompt for tab 1, then tab 2, then close” needs proper continuation
plumbing.
Why it’s not a one-day refactor
The plan budgeted one day. Realistic estimate based on the surfaces mapped above:
| Phase | Estimate |
|---|---|
AppShell class + OwnedArray<AppController> ownership | 3 hr |
| Per-tab panel construction + listener re-wiring | 4 hr |
MainWindow command routing through activeController() | 2 hr |
| Recent-projects + DnD spawn-new-tab | 2 hr |
| Async close-window iteration | 3 hr |
| Modal panel ownership decisions + wiring | 3 hr |
Tests (AppShellTests per the plan) | 3 hr |
That’s ~20 hours. Two to three working days, not one. And the modal ownership decision is genuinely a UX call, not a mechanical refactor — which means the spike needs collaborative input partway through, not a single-shot implementation.
Recommendation
Defer to wishlist with this doc as the breadcrumb. The work is feasible — nothing in the existing architecture is structurally hostile to multi-doc — but it’s a focused 2-3 day initiative, not a one-day spike, and it benefits from explicit UX decisions on modal panel scope before code lands.
When picked up, the entry point is AppShell plus the per-tab panel
construction model. Everything else cascades from there.