Qt 6 Migration Plan

Natron currently builds against Qt 5 (it was migrated from Qt 4 earlier). Adding Qt 6 support — while keeping Qt 5 working — is a tracked goal (issues #1011 “QT6 Support?”, and the Qt5 tracker #827). This chapter is the concrete migration plan, based on an audit of the current source tree.

Note

Migration status and the occurrence counts below reflect an audit of the RB-2.6 branch in mid-2026. They will change as the port progresses and as code is edited — treat the specific numbers as indicative and re-audit (a quick grep) before relying on them. Move items to “done” as they land.

Groundwork already in place

The port is further along than it looks; the two hardest pieces are already done:

  • The OpenGL viewer already uses ``QOpenGLWidget``. ViewerGL, Histogram, CurveWidget, DopeSheetView, TimeLineGui and CustomParamInteract all derive from QOpenGLWidget, not the Qt6-removed QGLWidget. This is normally the single biggest Qt6 blocker for a GL-heavy app, and it is already handled.

  • The CMake build already has a Qt6 switch. CMakeLists.txt exposes option(NATRON_QT6 "use Qt6" OFF). With it ON, the build requires Qt 6.3 + OpenGLWidgets, Shiboken6 and PySide6; with it OFF it uses Qt 5.15 + Shiboken2 + PySide2. So a dual-toolkit build is already modeled.

  • Binding scaffolding for PySide6 exists (PySide6_*_Python.h alongside PySide2_*_Python.h).

  • ``QtCompat.h`` already carries ``QT_VERSION_CHECK(6,0,0)`` shims and QT_NO_CAST_FROM_ASCII is already defined project-wide, so implicit const char*QString conversions (a common Qt6 trap) are already disallowed.

The strategy: build both from one source

Keep a single code base that compiles under Qt 5 and Qt 6, guarded where necessary by #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0). Prefer, in order:

  1. APIs that exist unchanged in both (most code needs no change).

  2. A shim in ``Global/QtCompat.h`` when the two versions need different spellings — extend the existing pattern there rather than scattering version #if blocks through business logic.

  3. A localized ``#if QT_VERSION`` block only when a shim is impractical.

Do the same for the two build systems: extend the qmake build with a CONFIG+=qt6 (or QT_MAJOR_VERSION-driven) path mirroring what CMake’s NATRON_QT6 already does, so both build systems can target either Qt.

Concrete work items (audited)

The following are the actual Qt6-incompatible usages found in the tree today. Counts are occurrences; see the named files.

QRegExpQRegularExpression (highest-effort item)

QRegExp is removed from Qt 6 core (it survives only in the Qt5Compat module, which is not a long-term option). ~33 uses across 16 files. Engine: FileSystemModel.cpp (3), Markdown.cpp (3), Project.cpp (2), OutputEffectInstance.cpp (2), NodeDocumentation.cpp (1), Node.cpp (1), CLArgs.cpp (1). Gui: ScriptTextEdit.cpp (6) + .h (1), RenderStatsDialog.cpp (3), FileTypeMainWindow_win.cpp (3), NodeGraph45.cpp (2), NodeCreationDialog.cpp (2), PreferencesPanel.cpp (1), ViewerGL.cpp (1), SequenceFileDialog.h (1). QRegularExpression has different semantics (anchoring, exactMatch has no direct equivalent — use ^…$ or anchoredPattern()), so each site needs review, not a blind substitution. QRegularExpression exists in Qt 5.15 too, so most conversions can be made unconditionally and will keep Qt 5 working.

QDesktopWidget / QApplication::desktop()QScreen (audit ~6 files)

Removed in Qt 6. Referenced in GuiApplicationManager10.cpp (active — used for logical DPI), MessageBox.cpp, Histogram.cpp, NodeCreationDialog.cpp, CurveWidget.cpp and ViewerGLPrivate.cpp — but audit each: several of these references are already commented out or partly migrated (CurveWidget.cpp, for example, already uses QGuiApplication::primaryScreen() in live code and only has a dead QDesktopWidget comment). For the remaining live uses, replace with QGuiApplication::screens() / QWidget::screen() / screen()->geometry(). These APIs exist in Qt 5.15, so the change can be unconditional.

QWidget::setMarginsetContentsMargins

A QLayout::setMargin(0) call in Gui/MessageBox.cpp (inside a non-Qt6 #else branch today). Deprecated; replace with setContentsMargins(0,0,0,0), which works on both Qt 5 and Qt 6.

Enum / QFlags scoping and the Python bindings (issue #854)

Qt 6 (and PySide6) are stricter about scoped enums and QFlags. Issue #854 (“Qt.Alignment Flags Unrecognized (As Int)”) is exactly this: alignment flags passed as raw int no longer implicitly convert. Audit QtEnumConvert and any place that stores Qt flags as int (e.g. through QVariant), and make sure the typesystem_*.xml files expose the flag types correctly to Shiboken6/PySide6. Regenerate the bindings with the Qt6 toolchain.

QVariant API (1 site)

One QVariant::Type use; Qt 6 deprecates QVariant::Type in favor of QMetaType. Localized fix.

Shiboken6 / PySide6 binding regeneration

Switch the binding generation to Shiboken6/PySide6 when NATRON_QT6 is on (CMake already selects the toolchain). Regenerate the natronengine_* and natrongui_* wrappers and the devel/PythonReference docs. Validate against the tutorials and PyPlugs. This is where most runtime (as opposed to compile-time) surprises will appear.

Module/include changes

QOpenGLWidget lives in the OpenGLWidgets module in Qt 6 (CMake already appends it). The qmake build must add QT += openglwidgets under a Qt6 condition. Audit QtOpenGL includes generally, since that module was reorganized in Qt 6.

Lower-risk, non-blocking cleanups

foreach / Q_FOREACH (~46 uses) still compile under Qt 6 but are deprecated; migrate to range-based for opportunistically. QStyle state flags (11) and Qt::SkipEmptyParts (already using the scoped form) are fine.

Verified replacement snippets

The replacements below were compiled against both Qt 5.15 and Qt 6 (Qt 5.15.19 and Qt 6.11.1 on macOS, clang++ -std=c++17 -fsyntax-only, exit code 0 on both). They use only APIs present in both versions, so they can be applied unconditionally — no #if QT_VERSION guard is needed, and the Qt 5 build keeps working.

QApplication::desktop()->screenGeometry() → primary QScreen:

// before (Qt 5, removed in Qt 6):
QDesktopWidget* d = app->desktop();
QRect g = d->screenGeometry();
// after (Qt 5.15 and Qt 6):
QRect g = QGuiApplication::primaryScreen()->geometry();

desktop()->availableGeometry() for a widget’s screen → QWidget::screen():

// after: use the screen the widget is actually on (Qt 5.14+ and Qt 6)
QScreen* s = widget->screen();
QRect g = s ? s->availableGeometry()
            : QGuiApplication::primaryScreen()->availableGeometry();

QRegExp exact match (e.g. the frame-range check in CLArgs.cpp):

// before:  QRegExp re("[0-9\\-,]*"); bool ok = re.exactMatch(s);
// after:   anchor the pattern (exactMatch has no direct equivalent)
QRegularExpression re(QString::fromUtf8("\\A[0-9\\-,]*\\z"));
bool ok = re.match(s).hasMatch();

QRegExp capture (e.g. the backup-suffix regex in Project.cpp):

// before:  QRegExp rx("\\.~(\\d+)~$"); rx.lastIndexIn(s); QString n = rx.cap(1);
// after:
QRegularExpression rx(QString::fromUtf8("\\.~(\\d+)~$"));
QRegularExpressionMatch m = rx.match(s);
QString n = m.hasMatch() ? m.captured(1) : QString();

QRegExp wildcard (the QRegExp::WildcardUnix case in NodeCreationDialog.cpp — the one site that needs real thought):

// before:  QRegExp expr(pattern, Qt::CaseInsensitive, QRegExp::WildcardUnix);
// after:
QRegularExpression expr(
    QRegularExpression::wildcardToRegularExpression(pattern),
    QRegularExpression::CaseInsensitiveOption);
bool matched = expr.match(text).hasMatch();

QLayout::setMargin(0) (in MessageBox.cpp) → setContentsMargins:

layout->setContentsMargins(0, 0, 0, 0);

Note

QRegExp offered two matching modes: indexIn() found the pattern anywhere in the string, while exactMatch() required the whole string to match. QRegularExpression::match() is find-anywhere and has no exactMatch equivalent. So where the old code used indexIn, the direct match() is correct; where it used exactMatch, anchor the pattern with \A…\z (or QRegularExpression::anchoredPattern()). Review each of the 16 sites for which mode it relied on.

Suggested sequence

  1. Land the unconditional API modernizations first (QRegExpQRegularExpression, QDesktopWidgetQScreen, setMargin, QVariant::Type). These improve the Qt 5 build too and shrink the eventual Qt6 diff. Verify the Qt 5 build and test suite stay green.

  2. Bring the qmake build to parity with CMake’s NATRON_QT6 switch so both build systems can select Qt 6.

  3. Get a clean Qt 6 compile with -DNATRON_QT6=ON, fixing residual compile errors with QtCompat.h shims or narrow #if QT_VERSION guards.

  4. Regenerate and fix the PySide6 bindings; resolve #854-class enum/flag issues at runtime.

  5. Validate the GUI end-to-end on each OS: file dialog, node graph, viewer, properties panels, curve editor/dope sheet, roto/tracker overlays, and the Python console/panels. Mirror the checklist the Qt5 tracker (#827) used.

  6. Set up CI to build both Qt 5 and Qt 6 so the dual-toolkit build does not regress (ties into the CI modernization work, issue #601).

Keeping Qt 5 working

Qt 5.15 is the floor. Every change above either uses an API present in both 5.15 and 6.x (so no guard is needed) or is guarded by QT_VERSION. Do not drop the Qt 5 path until the Qt 6 build has shipped and been validated on all three platforms — the same discipline that governed the Qt4→Qt5 migration.