fixed window flashing bug

This commit is contained in:
ykcheungab
2026-09-28 21:17:26 +08:00
parent b8dc5e925b
commit 720bd81031
2 changed files with 194 additions and 1 deletions
@@ -0,0 +1,170 @@
"""The main window must never be hidden while the operator switches sensor type.
The 3D pane is built on first use, so on a first run — the pane starts on a box, which
has no 3D scene — its ``QOpenGLWidget`` is created later, while the window is already on
screen. Creating the first *native* child of a visible window makes Qt re-create that
window, and Qt does that by hiding the window and showing it again: the whole
application disappears for a few hundred milliseconds and whatever popup or dialog was
open is dismissed with it. Switching the sensor to ``finger`` in the sensor dialog is
exactly that moment.
These are the acceptance conditions: the window never hides and never takes a close
event, and the dialog and popup that were open are not silently dismissed.
"""
from __future__ import annotations
from PySide6.QtCore import QObject
from PySide6.QtWidgets import QWidget
from orisys_studio.scene3d.view import gl_available
from orisys_studio.ui.mesh3d_panel import Mesh3dPanel
#: The window's own lifecycle, as the operator experiences it: a hide is the window
#: vanishing, a close is the application going away, and neither may happen here.
WATCHED = ("hideEvent", "closeEvent")
class _LifecycleWatcher(QObject):
"""Record every hide/close the window receives, in order."""
def __init__(self, window) -> None:
super().__init__(window)
self.seen: list[str] = []
for name in WATCHED:
setattr(self, f"_original_{name}", getattr(window, name))
window.hideEvent = self.hideEvent
window.closeEvent = self.closeEvent
def hideEvent(self, event) -> None:
self.seen.append("hideEvent")
self._original_hideEvent(event)
def closeEvent(self, event) -> None:
self.seen.append("closeEvent")
self._original_closeEvent(event)
def _pick(window, sensor_type: str) -> None:
"""Do what the operator does: choose ``sensor_type`` in the rail's sensor dialog."""
combo = window.sensor_picker.type_combo
combo.setCurrentIndex(combo.findData(sensor_type))
def test_reserving_the_native_handle_costs_nothing_at_construction(qt_app, make_window):
"""The handle is reserved, and reserving it does not build the pane.
``winId()`` on the handle forces the native window to exist, which is what makes Qt
reuse this top-level window later instead of re-creating it; and reserving it must
still not import GL, read a scene or create a context. The window platforms give a
native window a minimum size, so the reservation is checked by *where* it is and that
it never shows, not by a pixel size.
"""
window = make_window(sensor_type="box")
try:
panel = window.mesh3d_panel
assert isinstance(panel, Mesh3dPanel)
assert panel.view is None, "the pane was built before anything asked for it"
assert panel.scene_path() is None
handle = panel.findChild(QWidget, "mesh3dNativeHandle")
assert handle is not None, "the pane did not reserve a native handle"
assert handle.internalWinId() != 0, (
"the handle exists but Qt has not made it native, which is the whole point"
)
assert handle.isVisible() is False, (
"the reserved handle is a handle, not something to show"
)
assert handle.x() == 0 and handle.y() == 0, (
"the reserved handle must not take room from the pane"
)
window.resize(1440, 880)
window.show()
qt_app.processEvents()
assert handle.isVisible() is False, "the reserved handle showed up on screen"
assert panel.width() > 1, "the reservation took the pane's own space"
assert panel.view is None, "showing the window built the pane by itself"
finally:
window.close()
def test_switching_to_a_finger_never_hides_the_window_or_dismisses_the_dialog(
qt_app, make_window
):
available, reason = gl_available()
if not available:
import pytest
pytest.skip(f"no 3D widget on this machine: {reason}")
window = make_window(sensor_type="box", source_mode="camera")
window.resize(1440, 880)
window.show()
qt_app.processEvents()
# The first run's distinguishing state: nothing has built the pane yet.
assert window.mesh3d_panel.view is None
window._open_sensor_dialog()
dialog = window.sensor_dialog
assert dialog is not None and dialog.isVisible()
# The popup an operator's click leaves open. Qt dismisses both of these along with
# the window, so both are the thing this guards.
combo = window.sensor_picker.type_combo
combo.showPopup()
qt_app.processEvents()
popup = combo.view()
assert popup is not None and popup.isVisible()
watcher = _LifecycleWatcher(window)
try:
_pick(window, "finger")
qt_app.processEvents()
assert watcher.seen == [], (
"the window was hidden/closed by the sensor switch: " + repr(watcher.seen)
)
assert window.isVisible(), "the window left the screen during the switch"
assert dialog.isVisible(), "the sensor dialog was dismissed by the switch"
assert popup.isVisible(), "the popup was dismissed along with the window"
assert window.mesh3d_panel.view is not None, (
"no 3D pane was built, so the case this guards was never reached"
)
assert window.sensor_picker.sensor_type() == "finger"
finally:
dialog.close()
window.close()
def test_the_switch_is_quiet_on_later_runs_too(qt_app, make_window):
"""The other direction: the pane already exists, and back-and-forth stays quiet.
This is the second-run shape (a saved ``finger`` preference builds the pane at
start-up). A layout change that hid the window would be a bug on any run, not only
the first.
"""
available, reason = gl_available()
if not available:
import pytest
pytest.skip(f"no 3D widget on this machine: {reason}")
window = make_window(sensor_type="finger", source_mode="camera")
window.resize(1440, 880)
window.show()
qt_app.processEvents()
assert window.mesh3d_panel.view is not None, (
"a saved finger preference must build the pane before the window is shown"
)
watcher = _LifecycleWatcher(window)
try:
_pick(window, "box")
qt_app.processEvents()
_pick(window, "finger")
qt_app.processEvents()
assert watcher.seen == [], repr(watcher.seen)
assert window.isVisible()
finally:
window.close()