Skip to content

Refactor: break up the 15k-line MainWindow.cpp monolith (phased, behavior-preserving) #3351

Description

@jensenpat

Summary

src/gui/MainWindow.cpp has grown to 15,369 lines / 685 KB in a single
translation unit
. It is the project's central signal-routing hub, but its size
now imposes real friction:

  • Every change funnels through one file that requires CODEOWNERS review,
    so unrelated work (e.g. external-controller / MIDI customization) competes for
    the same approval surface and the same merge conflicts.
  • Large blast radius per edit — a 685 KB TU means each change recompiles the
    whole file, and reviewers must reason about a 15k-line file to judge a 20-line
    change. This raises the risk of code-quality regressions slipping through.

This issue proposes a phased, low-risk refactor to reduce the code sitting in
MainWindow.cpp and to decouple self-contained subsystems (especially external
controllers) from the monolith. No user-visible behavior change is intended.

Current anatomy (where the mass is)

178 #includes · 190 member variables · 433 method declarations · 604
connect() calls
.

Lines Location What it is
~3,400 constructor (MainWindow.cpp:12214637) 237 connect() calls, 184 lambdas, ~30 ── section ── wiring blocks
971 buildMenuBar (:6531) menu construction + 73 connects
904 wirePanadapter (:10299) per-pan signal wiring
846 buildUI (:7502) widget construction
653 registerShortcutActions (:12271) keyboard shortcut table
441 registerMidiParams (:14428) MIDI controller bindings
320 wireVfoWidget (:11375) per-VFO wiring
227 buildControlDevicesSnapshot (:5569) controller snapshot

The constructor alone is ~22% of the file.

Proposed phases (ranked by impact ÷ risk)

Phase 0 — Extract static free-function helpers (quick win, zero risk)

Lines ~470–734 already hold ~15 file-scope static helpers
(buildNetworkTooltip, buildTnfTooltip, formatTnfFrequency,
memorySpotLabel, buildBandStackIndicatorPixmap, etc.) that have no
MainWindow dependency
— pure formatters. Move them to
MainWindowHelpers.{h,cpp} (or fold into existing util headers). ~250 lines out,
no class involvement. Doubles as a warm-up that proves the multi-file CMake
pattern.

Phase 1 — Split the class across multiple .cpp files (highest impact, near-zero risk)

A single C++ class can be defined across several translation units with no
header change, no API change, and no behavior change
— just move method bodies
into new files added to CMakeLists.txt. The codebase already has the precedent
of wireXxx() helpers (wirePanadapter, wireVfoWidget, wireAetherDspWidget,
wireRadioSetupDialogSignals).

Proposed files, mapped onto the existing ── section ── blocks:

  • MainWindow_Controllers.cpp (do this first — solves the acute pain)
    registerMidiParams (441), buildControlDevicesSnapshot (227), the
    external-controllers constructor block (~:3424), plus HID/shortcut glue.
    Gives controller customization its own file → smaller diffs, isolated review
    surface, less collateral risk in unrelated UI code.
  • MainWindow_Wiring.cpp — turn the constructor's ~30 wiring blocks into
    void MainWindow::wireXxx() helpers; the constructor shrinks to
    wireDiscovery(); wireSmartLink(); wireSpots(); ….
  • MainWindow_Menu.cppbuildMenuBar (971) + registerShortcutActions (653).
  • MainWindow_Swr.cpp — the 14 SWR-sweep methods (~700 lines, :13165:13765).
  • MainWindow_FreeDV.cpp — RADE/FDV/FreeDV cluster (:13766:14223).
  • MainWindow_SHistory.cpp — the SHistory-marker cluster (9 methods).

This is purely mechanical (cut/paste bodies, no logic edits) and trivially
reviewable. It does not reduce total LOC but directly attacks the "everything
needs one approval" bottleneck and speeds incremental builds.

Phase 2 — Promote self-contained subsystems to controller classes (medium effort, real LOC reduction)

The SWR sweep and FreeDV clusters are independent state machines living in
MainWindow only by history. Extracting a SwrSweepController /
FreeDvSessionController that own their own timers and state would pull ~1,000+
lines and their member variables out of the 190-member header. Do this after
Phase 1 proves out, per subsystem.

Phase 3 — Table-drive menu/shortcut builders (optional, later)

buildMenuBar and registerShortcutActions are largely repetitive
addAction/lambda boilerplate. A data table ({id, label, slot}) iterated in a
loop could cut these substantially — but this is genuine logic change with real
regression risk. Lowest priority; only if the boilerplate keeps growing.

Suggested execution order

  1. Phase 0 (warm-up, proves CMake multi-file pattern)
  2. Phase 1 starting with MainWindow_Controllers.cpp (acute pain)
  3. Remainder of Phase 1 wiring split
  4. Phase 2 opportunistically, per subsystem

Constraints / guardrails

  • No behavior change in Phases 0–1 — these are mechanical moves only.
  • Preserve the GUI↔Radio no-feedback-loop wiring exactly (signal-blocker
    guards, m_updatingFromModel).
  • Don't break slice 0 RX flow or the multi-pan / multi-client filtering.
  • One PR per phase/file to keep diffs reviewable.

Acceptance criteria

  • MainWindow.cpp reduced from 15.4k lines toward a target (e.g. < 6k for
    the core constructor + lifecycle TU).
  • External-controller / MIDI code lives in its own translation unit with an
    isolated review surface.
  • No user-visible behavior change; existing tests/build pass on Linux CI.
  • Constructor reduced to an ordered list of wireXxx() calls.

Metadata

Metadata

Assignees

Labels

GUIUser interfaceenhancementImprovement to existing featureexternal devicesFlexControl, MIDI, serial port, USB peripheralsmaintainer-reviewRequires maintainer review before any action is takenrefactorCode cleanup, restructuring, or consolidation — no user-visible behavior change

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions