Skip to content

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:

  • MainWindow itself (for re-painting + status updates)
  • TrackListPanel
  • UnknownTechniquesBanner
  • TempoMapPanel
  • (ProfileEditorPanel reloads its profile list manually rather than via change broadcast, so it isn’t on the listener wire — but it does hold an AppController& in its ctor.)

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:

  1. Per-tab modals: each tab spawns its own export/profile/tempo panel, scoped to that tab’s controller.
  2. 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 through
  • void openProjectInNewTab(const juce::File&) for the file menu
  • void closeActiveTab() with the dirty-state prompt
  • void 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:

PhaseEstimate
AppShell class + OwnedArray<AppController> ownership3 hr
Per-tab panel construction + listener re-wiring4 hr
MainWindow command routing through activeController()2 hr
Recent-projects + DnD spawn-new-tab2 hr
Async close-window iteration3 hr
Modal panel ownership decisions + wiring3 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.