From 63da707b61ef0b839fd23e138e0690bd31e19ed6 Mon Sep 17 00:00:00 2001 From: Will Stott Date: Sun, 6 Sep 2026 17:23:40 +0100 Subject: [PATCH 1/3] Fix Wayland work-around for COSMIC, and add to the PyPI entry point The work-around from #575 lives only in run.py, the PyInstaller entry point (pyinstaller.spec runs Analysis(['run.py'])), which is not shipped in the wheel. Installs from PyPI enter at cq_editor.cqe_run:main and never ran it -- #266 was reported against the cq-editor console script, so the fix did not reach it. Additionally: skipping the fix-up whenever QT_QPA_PLATFORM is already set reads the variable as user intent, but COSMIC and others export "wayland;xcb" session-wide, so on exactly those systems nothing happened and the viewer still died with BadWindow. Move the logic to cq_editor/qt_platform.py, called from both entry points, and reorder the plugin list rather than replacing it: wayland* entries are demoted but kept, so an explicitly requested plugin is still reachable as a fallback and an offscreen or eglfs choice is untouched. Only an unset variable defaults to xcb. --- cq_editor/cqe_run.py | 4 ++++ cq_editor/qt_platform.py | 28 ++++++++++++++++++++++++++++ cq_editor/widgets/occt_widget.py | 1 + run.py | 8 ++++---- 4 files changed, 37 insertions(+), 4 deletions(-) create mode 100644 cq_editor/qt_platform.py diff --git a/cq_editor/cqe_run.py b/cq_editor/cqe_run.py index 622bd9ef..0f091d47 100644 --- a/cq_editor/cqe_run.py +++ b/cq_editor/cqe_run.py @@ -1,11 +1,15 @@ import os, sys, asyncio +from cq_editor.qt_platform import prefer_non_wayland_platform + if "CASROOT" in os.environ: del os.environ["CASROOT"] if sys.platform == "win32": asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) +prefer_non_wayland_platform() + from cq_editor.__main__ import main if __name__ == "__main__": diff --git a/cq_editor/qt_platform.py b/cq_editor/qt_platform.py new file mode 100644 index 00000000..7cb6a394 --- /dev/null +++ b/cq_editor/qt_platform.py @@ -0,0 +1,28 @@ +"""Override platform env vars before Qt starts up to work around platforms +unsupported by ourselves or OCCT. + +Must not import Qt: this runs before cq_editor.__main__ builds the QApplication. +""" + +import os +import sys + + +def prefer_non_wayland_platform(): + """ + Reorder QT_QPA_PLATFORM so Qt reaches a Wayland plugin only after everything + else has failed to load, defaulting to xcb when completely unset. + + Desktops such as COSMIC export QT_QPA_PLATFORM="wayland;xcb" session-wide, + we want to respect this config as far as possible, without using wayland. + + On wayland occt_widget hands winId() to OCP's Xw_Window, which needs an X11 + window id and gets a Wayland surface id instead, dying with BadWindow. + """ + if sys.platform != "linux": + return + + plugins = (os.environ.get("QT_QPA_PLATFORM") or "xcb").split(";") + os.environ["QT_QPA_PLATFORM"] = ";".join( + sorted(plugins, key=lambda plugin: plugin.startswith("wayland")) + ) diff --git a/cq_editor/widgets/occt_widget.py b/cq_editor/widgets/occt_widget.py index 195a5a51..2929fe8c 100755 --- a/cq_editor/widgets/occt_widget.py +++ b/cq_editor/widgets/occt_widget.py @@ -220,6 +220,7 @@ def _get_window_win(self, wid): def _get_window_linux(self, wid): + # X11 only as OCCT currently doesn't have wayland support from OCP.Xw import Xw_Window return Xw_Window(self.display_connection, int(wid)) diff --git a/run.py b/run.py index fbcd001e..798506f3 100644 --- a/run.py +++ b/run.py @@ -1,10 +1,10 @@ import sys -import os import faulthandler -# OCP's Xw_Window requires an X11 window ID, so we force XCB so winId() is valid under Wayland/XWayland. -if sys.platform == "linux" and "QT_QPA_PLATFORM" not in os.environ: - os.environ["QT_QPA_PLATFORM"] = "xcb" +from cq_editor.qt_platform import prefer_non_wayland_platform + +# Also done in cqe_run, for installs that enter at the console script instead. +prefer_non_wayland_platform() # PyInstaller with console=False sets sys.stdout/stderr to None on Windows if sys.stdout is None: From ea6d4d99204238f63246efb3f0165059f5b7ffa0 Mon Sep 17 00:00:00 2001 From: Will Stott Date: Sun, 20 Sep 2026 19:12:24 +0100 Subject: [PATCH 2/3] De-duplicate wayland workaround into __main__.py --- cq_editor/__main__.py | 3 +++ cq_editor/cqe_run.py | 4 ---- run.py | 5 ----- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/cq_editor/__main__.py b/cq_editor/__main__.py index c56f87c8..3f22de89 100644 --- a/cq_editor/__main__.py +++ b/cq_editor/__main__.py @@ -5,6 +5,9 @@ NAME = "CQ-editor" +from cq_editor.qt_platform import prefer_non_wayland_platform +prefer_non_wayland_platform() + # need to initialize QApp here, otherewise svg icons do not work on windows app = QApplication(sys.argv, applicationName=NAME) diff --git a/cq_editor/cqe_run.py b/cq_editor/cqe_run.py index 0f091d47..622bd9ef 100644 --- a/cq_editor/cqe_run.py +++ b/cq_editor/cqe_run.py @@ -1,15 +1,11 @@ import os, sys, asyncio -from cq_editor.qt_platform import prefer_non_wayland_platform - if "CASROOT" in os.environ: del os.environ["CASROOT"] if sys.platform == "win32": asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) -prefer_non_wayland_platform() - from cq_editor.__main__ import main if __name__ == "__main__": diff --git a/run.py b/run.py index 798506f3..a8bd2baf 100644 --- a/run.py +++ b/run.py @@ -1,11 +1,6 @@ import sys import faulthandler -from cq_editor.qt_platform import prefer_non_wayland_platform - -# Also done in cqe_run, for installs that enter at the console script instead. -prefer_non_wayland_platform() - # PyInstaller with console=False sets sys.stdout/stderr to None on Windows if sys.stdout is None: sys.stdout = open("nul", "w") From 4ad8f6503908a80f452ba5c7f3a3f25b0feed9af Mon Sep 17 00:00:00 2001 From: Will Stott Date: Sun, 20 Sep 2026 19:17:20 +0100 Subject: [PATCH 3/3] Move wayland workaround inline into __main__ Reorder QT_QPA_PLATFORM so Qt reaches a Wayland plugin only after everything else has failed to load, defaulting to xcb when unset. Desktops such as COSMIC export "wayland;xcb" session-wide; we respect that config as far as possible without running on Wayland. On Wayland occt_widget hands winId() to OCP's Xw_Window, which needs an X11 window id and gets a Wayland surface id instead, dying with BadWindow. OCCT does not yet support Wayland. Inline in __main__ (rather than a separate module) as it must run before the QApplication is created, and both cqe_run and `python -m cq_editor` import it. --- cq_editor/__main__.py | 16 ++++++++++++---- cq_editor/qt_platform.py | 28 ---------------------------- 2 files changed, 12 insertions(+), 32 deletions(-) delete mode 100644 cq_editor/qt_platform.py diff --git a/cq_editor/__main__.py b/cq_editor/__main__.py index 3f22de89..217de2e4 100644 --- a/cq_editor/__main__.py +++ b/cq_editor/__main__.py @@ -1,3 +1,4 @@ +import os import sys import argparse @@ -5,10 +6,17 @@ NAME = "CQ-editor" -from cq_editor.qt_platform import prefer_non_wayland_platform -prefer_non_wayland_platform() - -# need to initialize QApp here, otherewise svg icons do not work on windows +# Reorder QT_QPA_PLATFORM so Qt reaches a Wayland plugin only after everything +# else has failed to load, defaulting to xcb. OCCT does not yet support Wayland. +if sys.platform == "linux": + # Desktops such as COSMIC export QT_QPA_PLATFORM="wayland;xcb" session-wide, + # we want to respect this config as far as possible, without using wayland. + plugins = (os.environ.get("QT_QPA_PLATFORM") or "xcb").split(";") + os.environ["QT_QPA_PLATFORM"] = ";".join( + sorted(plugins, key=lambda plugin: plugin.startswith("wayland")) + ) + +# need to initialize QApp here, otherwise svg icons do not work on windows app = QApplication(sys.argv, applicationName=NAME) from .main_window import MainWindow diff --git a/cq_editor/qt_platform.py b/cq_editor/qt_platform.py deleted file mode 100644 index 7cb6a394..00000000 --- a/cq_editor/qt_platform.py +++ /dev/null @@ -1,28 +0,0 @@ -"""Override platform env vars before Qt starts up to work around platforms -unsupported by ourselves or OCCT. - -Must not import Qt: this runs before cq_editor.__main__ builds the QApplication. -""" - -import os -import sys - - -def prefer_non_wayland_platform(): - """ - Reorder QT_QPA_PLATFORM so Qt reaches a Wayland plugin only after everything - else has failed to load, defaulting to xcb when completely unset. - - Desktops such as COSMIC export QT_QPA_PLATFORM="wayland;xcb" session-wide, - we want to respect this config as far as possible, without using wayland. - - On wayland occt_widget hands winId() to OCP's Xw_Window, which needs an X11 - window id and gets a Wayland surface id instead, dying with BadWindow. - """ - if sys.platform != "linux": - return - - plugins = (os.environ.get("QT_QPA_PLATFORM") or "xcb").split(";") - os.environ["QT_QPA_PLATFORM"] = ";".join( - sorted(plugins, key=lambda plugin: plugin.startswith("wayland")) - )