#!/usr/bin/env python3 # Copyright (c) 2026 Storm Dragon # Authors: Storm Dragon, Angus Kola # SPDX-License-Identifier: GPL-3.0-or-later """Check Qt menu and button states on a private X11/AT-SPI desktop.""" import os from pathlib import Path import subprocess import sys import tempfile sys.dont_write_bytecode = True from native_x11 import wait_for from native_test_session import run_private_session def fixture(version): if version == '5': from PyQt5 import QtCore, QtWidgets else: from PySide6 import QtCore, QtWidgets class ControlWindow(QtWidgets.QMainWindow): def keyPressEvent(self, event): nonlocal option targets = {QtCore.Qt.Key_F6: option, QtCore.Qt.Key_F7: ordinary, QtCore.Qt.Key_F8: contextOption} if event.key() in targets: target = targets[event.key()] popup = contextMenu if target is contextOption else menu popup.popup(self.mapToGlobal(QtCore.QPoint(20, 50))) popup.setActiveAction(target) elif event.key() == QtCore.Qt.Key_F9: checkbox.setFocus() elif event.key() == QtCore.Qt.Key_F10: toggle.setFocus() elif event.key() == QtCore.Qt.Key_F11: menu.removeAction(option) option.deleteLater() option = menu.addAction('Optional feature') elif event.key() == QtCore.Qt.Key_F12: option.setCheckable(False) else: super().keyPressEvent(event) application = QtWidgets.QApplication([]) window = ControlWindow() window.setWindowTitle('Qt control states probe') content = QtWidgets.QWidget() layout = QtWidgets.QVBoxLayout(content) checkbox = QtWidgets.QCheckBox('Updates') layout.addWidget(checkbox) toggle = QtWidgets.QPushButton('Power') toggle.setCheckable(True) layout.addWidget(toggle) window.setCentralWidget(content) menu = window.menuBar().addMenu('&Actions') option = menu.addAction('Optional feature') option.setCheckable(True) ordinary = menu.addAction('Ordinary action') nestedMenu = menu.addMenu('Nested actions') nestedMenu.addAction('Nested command') contextMenu = QtWidgets.QMenu(window) contextOption = contextMenu.addAction('Context option') contextOption.setCheckable(True) # Shared actions can retain their menu-bar accessibility parent even # while the context menu emits their focus events. contextMenu.addAction(ordinary) contextMenu.addAction('Context command') contextMenu.addAction(option) window.resize(400, 250) window.show() checkbox.setFocus() application.exec() def run_inside(): root = Path(__file__).resolve().parents[1] for version in ('5', '6'): process = subprocess.Popen([sys.executable, __file__, '--fixture', version]) try: def find_window(): result = subprocess.run(['xdotool', 'search', '--onlyvisible', '--name', '^Qt control states probe$'], capture_output=True, text=True) return result.stdout.strip().splitlines()[0] if result.stdout.strip() else None windowId = wait_for(find_window, 'Qt fixture window missing') subprocess.run(['xdotool', 'windowfocus', windowId], check=True) from native_x11 import set_active_window set_active_window(int(windowId)) subprocess.run(['go', 'test', './internal/app', '-run', '^TestNativeQtControlStates$', '-count=1', '-v', '-timeout=45s'], cwd=root, check=True, timeout=60, env=dict(os.environ, CTHULHU_NATIVE_QT_CONTROLS=version, GOCACHE='/tmp/cthulhu-cache')) finally: process.terminate() process.wait(timeout=5) if __name__ == '__main__': if '--fixture' in sys.argv: fixture(sys.argv[-1]) elif '--inside' in sys.argv: run_inside() else: with tempfile.TemporaryDirectory(prefix='cthulhu-qt-controls-') as temporary: environment = dict(os.environ, HOME=temporary, XDG_CONFIG_HOME=temporary, XDG_CACHE_HOME=temporary, XDG_DATA_HOME=temporary, GOMODCACHE=subprocess.check_output(['go', 'env', 'GOMODCACHE'], text=True).strip(), QT_QPA_PLATFORM='xcb', QT_LINUX_ACCESSIBILITY_ALWAYS_ON='1', NO_AT_BRIDGE='0') environment.pop('AT_SPI_BUS_ADDRESS', None) run_private_session(['xvfb-run', '-a', 'dbus-run-session', '--', sys.executable, __file__, '--inside'], env=environment, check=True)