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,TimeLineGuiandCustomParamInteractall derive fromQOpenGLWidget, not the Qt6-removedQGLWidget. 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.txtexposesoption(NATRON_QT6 "use Qt6" OFF). With itON, the build requires Qt 6.3 +OpenGLWidgets, Shiboken6 and PySide6; with itOFFit uses Qt 5.15 + Shiboken2 + PySide2. So a dual-toolkit build is already modeled.Binding scaffolding for PySide6 exists (
PySide6_*_Python.halongsidePySide2_*_Python.h).``QtCompat.h`` already carries ``QT_VERSION_CHECK(6,0,0)`` shims and
QT_NO_CAST_FROM_ASCIIis already defined project-wide, so implicitconst char*→QStringconversions (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:
APIs that exist unchanged in both (most code needs no change).
A shim in ``Global/QtCompat.h`` when the two versions need different spellings — extend the existing pattern there rather than scattering version
#ifblocks through business logic.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.
QRegExp→QRegularExpression(highest-effort item)QRegExpis removed from Qt 6 core (it survives only in theQt5Compatmodule, 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).QRegularExpressionhas different semantics (anchoring,exactMatchhas no direct equivalent — use^…$oranchoredPattern()), so each site needs review, not a blind substitution.QRegularExpressionexists 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.cppandViewerGLPrivate.cpp— but audit each: several of these references are already commented out or partly migrated (CurveWidget.cpp, for example, already usesQGuiApplication::primaryScreen()in live code and only has a deadQDesktopWidgetcomment). For the remaining live uses, replace withQGuiApplication::screens()/QWidget::screen()/screen()->geometry(). These APIs exist in Qt 5.15, so the change can be unconditional.QWidget::setMargin→setContentsMarginsA
QLayout::setMargin(0)call inGui/MessageBox.cpp(inside a non-Qt6#elsebranch today). Deprecated; replace withsetContentsMargins(0,0,0,0), which works on both Qt 5 and Qt 6.- Enum /
QFlagsscoping 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 rawintno longer implicitly convert. AuditQtEnumConvertand any place that stores Qt flags asint(e.g. throughQVariant), and make sure thetypesystem_*.xmlfiles expose the flag types correctly to Shiboken6/PySide6. Regenerate the bindings with the Qt6 toolchain.QVariantAPI (1 site)One
QVariant::Typeuse; Qt 6 deprecatesQVariant::Typein favor ofQMetaType. Localized fix.- Shiboken6 / PySide6 binding regeneration
Switch the binding generation to Shiboken6/PySide6 when
NATRON_QT6is on (CMake already selects the toolchain). Regenerate thenatronengine_*andnatrongui_*wrappers and thedevel/PythonReferencedocs. Validate against the tutorials and PyPlugs. This is where most runtime (as opposed to compile-time) surprises will appear.- Module/include changes
QOpenGLWidgetlives in theOpenGLWidgetsmodule in Qt 6 (CMake already appends it). The qmake build must addQT += openglwidgetsunder a Qt6 condition. AuditQtOpenGLincludes 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-basedforopportunistically.QStylestate flags (11) andQt::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
Land the unconditional API modernizations first (
QRegExp→QRegularExpression,QDesktopWidget→QScreen,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.Bring the qmake build to parity with CMake’s
NATRON_QT6switch so both build systems can select Qt 6.Get a clean Qt 6 compile with
-DNATRON_QT6=ON, fixing residual compile errors withQtCompat.hshims or narrow#if QT_VERSIONguards.Regenerate and fix the PySide6 bindings; resolve #854-class enum/flag issues at runtime.
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.
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.