migrate pyqt5 to pyside6
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 1.9 MiB |
@@ -9,7 +9,10 @@ description = "Orisys Studio desktop app"
|
||||
requires-python = ">=3.10"
|
||||
authors = [{ name = "Orisys-Tech" }]
|
||||
dependencies = [
|
||||
"PyQt5>=5.15",
|
||||
# PySide6 is the LGPLv3 Qt binding: the licence, not the API, is why the studio is not
|
||||
# on PyQt5 (whose free option is GPLv3-or-commercial). See
|
||||
# docs/architecture/studio-3d-design.md §11.1 and apps/studio/tests/test_binding_purity.py.
|
||||
"PySide6>=6.6",
|
||||
"orisys-app-shared>=0.1.0",
|
||||
"orisys>=0.5",
|
||||
"scipy>=1.10.1",
|
||||
|
||||
@@ -9,11 +9,9 @@ from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
from PyQt5.QtCore import Qt, QTimer
|
||||
from PyQt5.QtGui import QIcon, QImage
|
||||
from PyQt5.QtWidgets import (
|
||||
QAction,
|
||||
QActionGroup,
|
||||
from PySide6.QtCore import Qt, QTimer
|
||||
from PySide6.QtGui import QAction, QActionGroup, QIcon, QImage
|
||||
from PySide6.QtWidgets import (
|
||||
QApplication,
|
||||
QFrame,
|
||||
QHBoxLayout,
|
||||
@@ -240,7 +238,7 @@ class StudioMainWindow(QMainWindow):
|
||||
root.addWidget(self.toolbar)
|
||||
self.status_chip = self.toolbar.status_chip
|
||||
|
||||
self.content_splitter = QSplitter(Qt.Horizontal)
|
||||
self.content_splitter = QSplitter(Qt.Orientation.Horizontal)
|
||||
self.mesh3d_panel = Mesh3dPanel(theme=self._theme)
|
||||
self.arrow_panel = ArrowPanel(theme=self._theme)
|
||||
self.force_plots = ForcePlotsPanel(theme=self._theme)
|
||||
@@ -888,7 +886,7 @@ class StudioMainWindow(QMainWindow):
|
||||
# convention everyone's hands already know.
|
||||
self.act_play = QAction(self)
|
||||
self.act_play.setShortcut("Space")
|
||||
self.act_play.setShortcutContext(Qt.ApplicationShortcut)
|
||||
self.act_play.setShortcutContext(Qt.ShortcutContext.ApplicationShortcut)
|
||||
self.addAction(self.act_play)
|
||||
|
||||
self.view_menu = menu_bar.addMenu("")
|
||||
@@ -1402,7 +1400,7 @@ class StudioMainWindow(QMainWindow):
|
||||
self._persist_prefs()
|
||||
|
||||
def _show_about(self) -> None:
|
||||
AboutDialog(self).exec_()
|
||||
AboutDialog(self).exec()
|
||||
|
||||
def _on_camera_id_changed(self, value: int) -> None:
|
||||
self.prefs["camera_id"] = str(int(value))
|
||||
@@ -1507,7 +1505,7 @@ class StudioMainWindow(QMainWindow):
|
||||
filters_locked=bool(self._running or self.recorder.is_recording),
|
||||
sample_rate_hz=self.display_ctl.device_sample_rate(),
|
||||
)
|
||||
if dialog.exec_() != PlotSettingsDialog.Accepted:
|
||||
if dialog.exec() != PlotSettingsDialog.Accepted:
|
||||
return
|
||||
window_sec, fn_max, shear_max, fn_raw_max, shear_raw_max = dialog.values()
|
||||
self.display_ctl.apply_plot_settings(
|
||||
@@ -1589,10 +1587,10 @@ class StudioMainWindow(QMainWindow):
|
||||
sensor_type=sensor_type,
|
||||
config=config_ref,
|
||||
),
|
||||
QMessageBox.Yes | QMessageBox.No,
|
||||
QMessageBox.No,
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||
QMessageBox.StandardButton.No,
|
||||
)
|
||||
if answer != QMessageBox.Yes:
|
||||
if answer != QMessageBox.StandardButton.Yes:
|
||||
self.recorder.log_event(
|
||||
"calibration_apply_blocked", "; ".join(reasons)
|
||||
)
|
||||
@@ -1602,10 +1600,10 @@ class StudioMainWindow(QMainWindow):
|
||||
self,
|
||||
t("cal.apply_title"),
|
||||
t("cal.apply_confirm", sensor_type=sensor_type, config=config_ref),
|
||||
QMessageBox.Yes | QMessageBox.No,
|
||||
QMessageBox.No,
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||
QMessageBox.StandardButton.No,
|
||||
)
|
||||
if answer != QMessageBox.Yes:
|
||||
if answer != QMessageBox.StandardButton.Yes:
|
||||
self.statusBar().showMessage(t("cal.apply_skipped"))
|
||||
return
|
||||
worker = self._worker_for(sensor_type)
|
||||
@@ -1720,7 +1718,18 @@ class StudioMainWindow(QMainWindow):
|
||||
if getattr(self, "act_run", None) is None:
|
||||
return
|
||||
self.act_run.setEnabled(self.toolbar.btn_run.isEnabled())
|
||||
self.act_stop.setEnabled(self._session_active)
|
||||
# The menu's Stop mirrors the toolbar's stop affordance, not the acquisition
|
||||
# session alone: a replay or a recompute pass is something to stop.
|
||||
#
|
||||
# This is not cosmetic. Qt6 honours ``enabled`` in ``QAction::trigger()`` (Qt5
|
||||
# did not) and never delivers the shortcut of a disabled action, so a Stop that
|
||||
# is disabled while a replay is playing is a keyboard escape hatch that does not
|
||||
# work — the one thing ``_on_stop_shortcut`` exists to guarantee.
|
||||
self.act_stop.setEnabled(
|
||||
self._session_active
|
||||
or self.replay_ctl.is_playing()
|
||||
or self._recompute_running
|
||||
)
|
||||
self.act_preview.setEnabled(self.toolbar.btn_preview.isEnabled())
|
||||
self.act_reset.setEnabled(self.toolbar.btn_reset.isEnabled())
|
||||
|
||||
@@ -1740,10 +1749,16 @@ class StudioMainWindow(QMainWindow):
|
||||
|
||||
In the replay view it is the same escape hatch as the rail's 停止 button: a
|
||||
replay or a recompute pass that will not stop by itself has to be stoppable
|
||||
from the keyboard.
|
||||
from the keyboard. The replay branch accepts the same three states
|
||||
``_sync_shortcut_actions`` enables the action for, so the action can never be
|
||||
enabled with nothing behind it.
|
||||
"""
|
||||
if self.toolbar.run_role() == "playback":
|
||||
if self.replay_ctl.is_playing() or self._session_active:
|
||||
if (
|
||||
self.replay_ctl.is_playing()
|
||||
or self._session_active
|
||||
or self._recompute_running
|
||||
):
|
||||
self.replay_ctl.stop_activity()
|
||||
return
|
||||
if not self._session_active:
|
||||
@@ -2348,7 +2363,7 @@ class StudioMainWindow(QMainWindow):
|
||||
geo = self.prefs.get("window_geometry")
|
||||
if isinstance(geo, str) and geo:
|
||||
try:
|
||||
from PyQt5.QtCore import QByteArray
|
||||
from PySide6.QtCore import QByteArray
|
||||
|
||||
self.restoreGeometry(QByteArray.fromHex(geo.encode("ascii")))
|
||||
except Exception:
|
||||
|
||||
@@ -21,7 +21,7 @@ from __future__ import annotations
|
||||
import logging
|
||||
from typing import Any, Optional, Protocol
|
||||
|
||||
from PyQt5.QtCore import QObject
|
||||
from PySide6.QtCore import QObject
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -6,7 +6,12 @@ import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
# pyqtgraph picks its toolkit from whichever binding is already imported, and in a
|
||||
# development environment that also has PyQt5 installed (``orisys[gui]``) it can pick the
|
||||
# wrong one. The application is PySide6-only, so say so before anything imports pyqtgraph.
|
||||
os.environ.setdefault("PYQTGRAPH_QT_LIB", "PySide6")
|
||||
|
||||
from PySide6.QtWidgets import QApplication # noqa: E402
|
||||
|
||||
from orisys_app_shared import API_VERSION, __version__ as shared_version
|
||||
from orisys_app_shared.compat import check_shared_compat
|
||||
@@ -85,7 +90,7 @@ def main() -> int:
|
||||
language,
|
||||
gl_profile,
|
||||
)
|
||||
code = int(app.exec_())
|
||||
code = int(app.exec())
|
||||
|
||||
# Leave the process immediately.
|
||||
#
|
||||
|
||||
@@ -24,7 +24,7 @@ from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import numpy as np
|
||||
from PyQt5.QtCore import QObject, pyqtSignal
|
||||
from PySide6.QtCore import QObject, Signal
|
||||
|
||||
from orisys_app_shared.i18n import t
|
||||
from orisys_app_shared.record import RecordingReader, VideoOnlySource
|
||||
@@ -47,13 +47,13 @@ class ReplayEngine(QObject):
|
||||
"""Open one recording and answer "what is at t?" for every view."""
|
||||
|
||||
#: The cursor moved; the chart cursor and the 2D panel follow.
|
||||
position_changed = pyqtSignal(float)
|
||||
position_changed = Signal(float)
|
||||
#: The frame under the cursor changed.
|
||||
frames_changed = pyqtSignal()
|
||||
frames_changed = Signal()
|
||||
#: The notes about this recording, or the loaded recording, changed.
|
||||
status_changed = pyqtSignal()
|
||||
status_changed = Signal()
|
||||
#: A recording was opened (True) or the previous one was dropped (False).
|
||||
recording_changed = pyqtSignal(bool)
|
||||
recording_changed = Signal(bool)
|
||||
|
||||
def __init__(self, parent=None) -> None:
|
||||
super().__init__(parent)
|
||||
|
||||
@@ -28,8 +28,8 @@ from pathlib import Path
|
||||
from typing import Any, Optional, Protocol
|
||||
|
||||
import numpy as np
|
||||
from PyQt5.QtCore import QObject, QTimer
|
||||
from PyQt5.QtWidgets import QFileDialog
|
||||
from PySide6.QtCore import QObject, QTimer
|
||||
from PySide6.QtWidgets import QFileDialog
|
||||
|
||||
from orisys_app_shared.acquisition.image_backend import SOURCE_REPLAY, ImageFrame
|
||||
from orisys_app_shared.i18n import t
|
||||
|
||||
@@ -33,7 +33,7 @@ from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, Callable, Optional
|
||||
|
||||
from PyQt5.QtCore import QObject, pyqtSignal
|
||||
from PySide6.QtCore import QObject, Signal
|
||||
|
||||
from orisys_app_shared.paths import config_dir
|
||||
from orisys_app_shared.scene3d.contracts import (
|
||||
@@ -180,8 +180,8 @@ class SceneAssetLoader(QObject):
|
||||
selection the operator has already moved away from.
|
||||
"""
|
||||
|
||||
loaded = pyqtSignal(object, str, int, object) # SceneAssets, description, id, device
|
||||
failed = pyqtSignal(object, int) # Diagnostic, request id
|
||||
loaded = Signal(object, str, int, object) # SceneAssets, description, id, device
|
||||
failed = Signal(object, int) # Diagnostic, request id
|
||||
|
||||
def __init__(self, parent: Optional[QObject] = None) -> None:
|
||||
super().__init__(parent)
|
||||
|
||||
@@ -72,21 +72,21 @@ def configure_surface_format(profile: Profile = "desktop33") -> None:
|
||||
component must not slow down (the acquisition loop feeds the scene through a
|
||||
capacity-one mailbox), and the design document defers anti-aliasing work.
|
||||
"""
|
||||
from PyQt5.QtGui import QSurfaceFormat
|
||||
from PySide6.QtGui import QSurfaceFormat
|
||||
|
||||
fmt = QSurfaceFormat()
|
||||
if profile == "gles30":
|
||||
fmt.setRenderableType(QSurfaceFormat.OpenGLES)
|
||||
fmt.setRenderableType(QSurfaceFormat.RenderableType.OpenGLES)
|
||||
fmt.setVersion(3, 0)
|
||||
elif profile == "desktop33":
|
||||
fmt.setRenderableType(QSurfaceFormat.OpenGL)
|
||||
fmt.setRenderableType(QSurfaceFormat.RenderableType.OpenGL)
|
||||
fmt.setVersion(3, 3)
|
||||
fmt.setProfile(QSurfaceFormat.CoreProfile)
|
||||
fmt.setProfile(QSurfaceFormat.OpenGLContextProfile.CoreProfile)
|
||||
else: # pragma: no cover - a typo must not reach a board silently
|
||||
raise ValueError(f"unknown GL profile: {profile!r}")
|
||||
fmt.setDepthBufferSize(24)
|
||||
fmt.setStencilBufferSize(8)
|
||||
fmt.setSwapBehavior(QSurfaceFormat.DoubleBuffer)
|
||||
fmt.setSwapBehavior(QSurfaceFormat.SwapBehavior.DoubleBuffer)
|
||||
fmt.setSamples(0)
|
||||
QSurfaceFormat.setDefaultFormat(fmt)
|
||||
|
||||
@@ -442,7 +442,10 @@ class GLDispatch:
|
||||
if context is None or not context.isValid():
|
||||
raise error("gl_unsupported", "no current OpenGL context")
|
||||
fmt = context.format()
|
||||
profile = profile_from_renderable_type(int(fmt.renderableType()))
|
||||
# PySide6 enums are not ints: `int(QSurfaceFormat.OpenGLES)` raises TypeError, so read
|
||||
# `.value`. (PyQt5 exposed them as ints. Getting this wrong made every GL probe raise,
|
||||
# the 3D pane report "unsupported", and the GL smoke tests skip instead of running.)
|
||||
profile = profile_from_renderable_type(int(fmt.renderableType().value))
|
||||
try:
|
||||
vendor = _gl_string(GL, GL.GL_VENDOR)
|
||||
renderer = _gl_string(GL, GL.GL_RENDERER)
|
||||
@@ -454,7 +457,7 @@ class GLDispatch:
|
||||
"gl_unsupported",
|
||||
f"the context answered no queries: {type(exc).__name__}: {exc}",
|
||||
) from exc
|
||||
from PyQt5.QtGui import QGuiApplication
|
||||
from PySide6.QtGui import QGuiApplication
|
||||
|
||||
platform_name = ""
|
||||
try:
|
||||
|
||||
@@ -22,9 +22,9 @@ from typing import TYPE_CHECKING, Optional
|
||||
|
||||
import numpy as np
|
||||
|
||||
from PyQt5.QtCore import Qt, pyqtSignal
|
||||
from PyQt5.QtGui import QImage
|
||||
from PyQt5.QtWidgets import (
|
||||
from PySide6.QtCore import Qt, Signal
|
||||
from PySide6.QtGui import QImage
|
||||
from PySide6.QtWidgets import (
|
||||
QCheckBox,
|
||||
QComboBox,
|
||||
QDoubleSpinBox,
|
||||
@@ -87,7 +87,7 @@ class ColorScaleBar(QLabel):
|
||||
self.setObjectName("colorScaleBar")
|
||||
self.setFixedHeight(12)
|
||||
self.setMinimumWidth(120)
|
||||
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
||||
self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
|
||||
self.setVisible(False)
|
||||
self._lut_id = "sequential"
|
||||
|
||||
@@ -99,14 +99,14 @@ class ColorScaleBar(QLabel):
|
||||
self.update()
|
||||
|
||||
def paintEvent(self, event) -> None: # noqa: N802 - Qt naming
|
||||
from PyQt5.QtGui import QPainter
|
||||
from PySide6.QtGui import QPainter
|
||||
|
||||
from orisys_studio.scene3d.gl_backend import lut_rgba
|
||||
|
||||
# (256, 1, 4) RGBA -> one scanline; the bytes object stays alive for the call,
|
||||
# which is what QImage's borrowed buffer needs.
|
||||
rgba = np.ascontiguousarray(lut_rgba(self._lut_id)[:, 0, :]).tobytes()
|
||||
image = QImage(rgba, 256, 1, 256 * 4, QImage.Format_RGBA8888)
|
||||
image = QImage(rgba, 256, 1, 256 * 4, QImage.Format.Format_RGBA8888)
|
||||
painter = QPainter(self)
|
||||
painter.drawImage(self.rect(), image)
|
||||
painter.end()
|
||||
@@ -115,10 +115,10 @@ class ColorScaleBar(QLabel):
|
||||
class ThreeDPanel(QFrame):
|
||||
"""Toggle, presets, reset, housing visibility, gain, colour scale, status."""
|
||||
|
||||
enabledChanged = pyqtSignal(bool)
|
||||
settingsChanged = pyqtSignal(object) # DisplaySettings
|
||||
resetRequested = pyqtSignal()
|
||||
presetRequested = pyqtSignal(str)
|
||||
enabledChanged = Signal(bool)
|
||||
settingsChanged = Signal(object) # DisplaySettings
|
||||
resetRequested = Signal()
|
||||
presetRequested = Signal(str)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -299,13 +299,13 @@ class ThreeDPanel(QFrame):
|
||||
scale_row.setSpacing(6)
|
||||
self.scale_lower_label = QLabel()
|
||||
self.scale_lower_label.setObjectName("panelMeta")
|
||||
self.scale_lower_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
|
||||
self.scale_lower_label.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
|
||||
scale_row.addWidget(self.scale_lower_label)
|
||||
self.scale_bar = ColorScaleBar()
|
||||
scale_row.addWidget(self.scale_bar, 1)
|
||||
self.scale_upper_label = QLabel()
|
||||
self.scale_upper_label.setObjectName("panelMeta")
|
||||
self.scale_upper_label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
|
||||
self.scale_upper_label.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
|
||||
scale_row.addWidget(self.scale_upper_label)
|
||||
root.addLayout(scale_row)
|
||||
|
||||
@@ -327,7 +327,7 @@ class ThreeDPanel(QFrame):
|
||||
self.source_label = ShrinkableLabel()
|
||||
self.source_label.setObjectName("panelMeta")
|
||||
self.source_label.setWordWrap(False)
|
||||
self.source_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
|
||||
self.source_label.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
|
||||
legend_row.addWidget(self.source_label, 2)
|
||||
root.addLayout(legend_row)
|
||||
|
||||
|
||||
@@ -16,8 +16,8 @@ from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtWidgets import (
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog,
|
||||
QDialogButtonBox,
|
||||
QLabel,
|
||||
@@ -60,7 +60,7 @@ class Scene3dSettingsDialog(QDialog):
|
||||
root.addWidget(panel.controls_widget)
|
||||
panel.controls_widget.setVisible(True)
|
||||
|
||||
self.buttons = QDialogButtonBox(QDialogButtonBox.Close)
|
||||
self.buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Close)
|
||||
self.buttons.rejected.connect(self.close)
|
||||
self.buttons.accepted.connect(self.close)
|
||||
root.addWidget(self.buttons)
|
||||
@@ -72,7 +72,7 @@ class Scene3dSettingsDialog(QDialog):
|
||||
self.hint.setText(t("scene3d.settings_hint"))
|
||||
# Qt ships its own translations for a button box, and the application installs
|
||||
# none: without this the dialog ends in an English "Close" among Chinese labels.
|
||||
close_button = self.buttons.button(QDialogButtonBox.Close)
|
||||
close_button = self.buttons.button(QDialogButtonBox.StandardButton.Close)
|
||||
if close_button is not None:
|
||||
close_button.setText(t("dialog.close"))
|
||||
|
||||
@@ -84,7 +84,7 @@ class Scene3dSettingsDialog(QDialog):
|
||||
|
||||
def keyPressEvent(self, event) -> None: # noqa: N802 - Qt naming
|
||||
"""Esc closes the dialog, as it does everywhere else in this application."""
|
||||
if event.key() == Qt.Key_Escape:
|
||||
if event.key() == Qt.Key.Key_Escape:
|
||||
self.hide()
|
||||
return
|
||||
super().keyPressEvent(event)
|
||||
|
||||
@@ -28,8 +28,8 @@ import logging
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from typing import Any, Callable, Optional
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal
|
||||
from PyQt5.QtWidgets import QVBoxLayout, QWidget
|
||||
from PySide6.QtCore import Signal
|
||||
from PySide6.QtWidgets import QVBoxLayout, QWidget
|
||||
|
||||
from orisys_app_shared.scene3d.composer import SceneComposer
|
||||
from orisys_app_shared.scene3d.contracts import (
|
||||
@@ -71,9 +71,9 @@ def gl_available() -> tuple[bool, str]:
|
||||
pane on a machine where PyOpenGL is not installed.
|
||||
"""
|
||||
try:
|
||||
from PyQt5.QtWidgets import QOpenGLWidget # noqa: F401
|
||||
from PySide6.QtOpenGLWidgets import QOpenGLWidget # noqa: F401
|
||||
except Exception as exc: # noqa: BLE001
|
||||
return False, f"PyQt5 OpenGL widgets are unavailable: {exc}"
|
||||
return False, f"PySide6 OpenGL widgets are unavailable: {exc}"
|
||||
try:
|
||||
import OpenGL # noqa: F401
|
||||
except Exception as exc: # noqa: BLE001
|
||||
@@ -84,11 +84,11 @@ def gl_available() -> tuple[bool, str]:
|
||||
class Scene3DView(QWidget):
|
||||
"""One 3D pane: the GL widget on top, its controls underneath."""
|
||||
|
||||
stateChanged = pyqtSignal(object) # SceneState
|
||||
diagnosticRaised = pyqtSignal(object) # Diagnostic
|
||||
renderStatsChanged = pyqtSignal(object) # RenderStats
|
||||
descriptorChanged = pyqtSignal(object) # Optional[FieldDescriptor]
|
||||
enabledChanged = pyqtSignal(bool)
|
||||
stateChanged = Signal(object) # SceneState
|
||||
diagnosticRaised = Signal(object) # Diagnostic
|
||||
renderStatsChanged = Signal(object) # RenderStats
|
||||
descriptorChanged = Signal(object) # Optional[FieldDescriptor]
|
||||
enabledChanged = Signal(bool)
|
||||
|
||||
def __init__(self, parent: Optional[QWidget] = None) -> None:
|
||||
super().__init__(parent)
|
||||
|
||||
@@ -19,9 +19,11 @@ from __future__ import annotations
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
from PyQt5.QtCore import QEvent, Qt, pyqtSignal
|
||||
from PyQt5.QtGui import QMouseEvent, QWheelEvent
|
||||
from PyQt5.QtWidgets import QOpenGLWidget, QWidget
|
||||
from PySide6.QtCore import QEvent, Qt, Signal
|
||||
from PySide6.QtGui import QMouseEvent, QWheelEvent
|
||||
# Qt6 moved QOpenGLWidget out of QtWidgets into its own module; PySide6 follows.
|
||||
from PySide6.QtOpenGLWidgets import QOpenGLWidget
|
||||
from PySide6.QtWidgets import QWidget
|
||||
|
||||
from orisys_app_shared.scene3d.contracts import (
|
||||
Diagnostic,
|
||||
@@ -48,13 +50,13 @@ STATS_INTERVAL_S = 0.5
|
||||
class Sensor3DWidget(QOpenGLWidget):
|
||||
"""Renders one scene. Signals carry state, never per-frame arrays."""
|
||||
|
||||
capabilitiesChanged = pyqtSignal(object) # GLCapabilities
|
||||
diagnosticRaised = pyqtSignal(object) # Diagnostic
|
||||
renderStatsChanged = pyqtSignal(object) # RenderStats, rate-limited
|
||||
capabilitiesChanged = Signal(object) # GLCapabilities
|
||||
diagnosticRaised = Signal(object) # Diagnostic
|
||||
renderStatsChanged = Signal(object) # RenderStats, rate-limited
|
||||
#: What the frame on screen claims to be — quantity, unit, quality. The panel
|
||||
#: labels the colour bar from this and nothing else, so an unmeasured surface
|
||||
#: cannot inherit the previous frame's range.
|
||||
descriptorChanged = pyqtSignal(object) # Optional[FieldDescriptor]
|
||||
descriptorChanged = Signal(object) # Optional[FieldDescriptor]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -259,14 +261,19 @@ class Sensor3DWidget(QOpenGLWidget):
|
||||
When the platform cannot create a GL context (the offscreen plugin, a broken
|
||||
driver, a board without EGL) ``initializeGL`` is simply never called: there is
|
||||
no exception to catch, and the widget would sit there looking like a working
|
||||
view that has no data yet. Qt does answer the question, though — ``context()``
|
||||
stays ``None`` — so the first paint asks it and turns "no context" into the
|
||||
state the rest of the application already knows how to show.
|
||||
view that has no data yet. Qt does answer the question, though, so the first
|
||||
paint asks it and turns "no context" into the state the rest of the application
|
||||
already knows how to show.
|
||||
|
||||
Under Qt5 that answer was ``context() is None``. Qt6 returns an *invalid*
|
||||
context object instead, so the check has to be ``isValid()`` as well — comparing
|
||||
against ``None`` alone silently stops reporting a broken GL stack.
|
||||
"""
|
||||
super().paintEvent(event)
|
||||
if self._unavailable or self._capabilities is not None:
|
||||
return
|
||||
if self.context() is None:
|
||||
context = self.context()
|
||||
if context is None or not context.isValid():
|
||||
self._fail(
|
||||
diagnostic(
|
||||
"gl_unsupported",
|
||||
@@ -305,7 +312,7 @@ class Sensor3DWidget(QOpenGLWidget):
|
||||
|
||||
# --------------------------------------------------------------- interaction
|
||||
def mousePressEvent(self, event: QMouseEvent) -> None: # pragma: no cover
|
||||
self._last_press = (float(event.x()), float(event.y()))
|
||||
self._last_press = (float(event.position().x()), float(event.position().y()))
|
||||
super().mousePressEvent(event)
|
||||
|
||||
def mouseReleaseEvent(self, event: QMouseEvent) -> None: # pragma: no cover
|
||||
@@ -316,17 +323,17 @@ class Sensor3DWidget(QOpenGLWidget):
|
||||
if self._last_press is None:
|
||||
super().mouseMoveEvent(event)
|
||||
return
|
||||
x, y = float(event.x()), float(event.y())
|
||||
x, y = float(event.position().x()), float(event.position().y())
|
||||
delta = (x - self._last_press[0], y - self._last_press[1])
|
||||
self._last_press = (x, y)
|
||||
viewport = (max(self.width(), 1), max(self.height(), 1))
|
||||
buttons = event.buttons()
|
||||
panning = bool(buttons & Qt.MiddleButton) or bool(
|
||||
buttons & Qt.LeftButton and event.modifiers() & Qt.ShiftModifier
|
||||
panning = bool(buttons & Qt.MouseButton.MiddleButton) or bool(
|
||||
buttons & Qt.MouseButton.LeftButton and event.modifiers() & Qt.KeyboardModifier.ShiftModifier
|
||||
)
|
||||
if panning:
|
||||
self._camera.pan(delta, viewport)
|
||||
elif buttons & Qt.LeftButton:
|
||||
elif buttons & Qt.MouseButton.LeftButton:
|
||||
self._camera.orbit(delta, viewport)
|
||||
self.update()
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ import logging
|
||||
import time
|
||||
from typing import Any, Callable, Optional
|
||||
|
||||
from PyQt5.QtCore import QObject, QTimer, pyqtSignal
|
||||
from PySide6.QtCore import QObject, QTimer, Signal
|
||||
|
||||
from orisys_app_shared.i18n import t
|
||||
from orisys_studio import session_metadata
|
||||
@@ -47,11 +47,11 @@ class SessionController(QObject):
|
||||
"""Owns the acquisition session's state and its UI-free mechanics."""
|
||||
|
||||
#: The run timer fired: the window's presenter slot does the painting.
|
||||
tick = pyqtSignal()
|
||||
tick = Signal()
|
||||
#: The phase was (re)applied, not only changed: the status chip repaints from it,
|
||||
#: and an idempotent repaint is cheaper than a second source of truth.
|
||||
phaseChanged = pyqtSignal(str)
|
||||
runningChanged = pyqtSignal(bool)
|
||||
phaseChanged = Signal(str)
|
||||
runningChanged = Signal(bool)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtGui import QPixmap
|
||||
from PyQt5.QtWidgets import QDialog, QHBoxLayout, QLabel, QPushButton, QVBoxLayout
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtGui import QPixmap
|
||||
from PySide6.QtWidgets import QDialog, QHBoxLayout, QLabel, QPushButton, QVBoxLayout
|
||||
|
||||
from orisys_app_shared.i18n import t
|
||||
from orisys_app_shared.paths import logo_topbar_path
|
||||
@@ -24,19 +24,19 @@ class AboutDialog(QDialog):
|
||||
logo = QLabel()
|
||||
pix = QPixmap(str(logo_topbar_path()))
|
||||
if not pix.isNull():
|
||||
logo.setPixmap(pix.scaledToHeight(48, Qt.SmoothTransformation))
|
||||
logo.setPixmap(pix.scaledToHeight(48, Qt.TransformationMode.SmoothTransformation))
|
||||
row.addWidget(logo)
|
||||
row.addStretch(1)
|
||||
root.addLayout(row)
|
||||
|
||||
self.body = QLabel()
|
||||
self.body.setWordWrap(True)
|
||||
self.body.setTextInteractionFlags(Qt.TextSelectableByMouse)
|
||||
self.body.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
|
||||
root.addWidget(self.body)
|
||||
|
||||
close_btn = QPushButton("OK")
|
||||
close_btn.clicked.connect(self.accept)
|
||||
root.addWidget(close_btn, alignment=Qt.AlignRight)
|
||||
root.addWidget(close_btn, alignment=Qt.AlignmentFlag.AlignRight)
|
||||
self.retranslate()
|
||||
|
||||
def retranslate(self) -> None:
|
||||
|
||||
@@ -12,9 +12,9 @@ from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from PyQt5.QtCore import QSize, Qt, pyqtSignal
|
||||
from PyQt5.QtGui import QImage, QPainter, QPixmap, QTransform
|
||||
from PyQt5.QtWidgets import (
|
||||
from PySide6.QtCore import QSize, Qt, Signal
|
||||
from PySide6.QtGui import QImage, QPainter, QPixmap, QTransform
|
||||
from PySide6.QtWidgets import (
|
||||
QComboBox,
|
||||
QFrame,
|
||||
QHBoxLayout,
|
||||
@@ -48,7 +48,7 @@ def bgr_to_qimage(frame) -> QImage:
|
||||
"""
|
||||
rgb = frame[:, :, ::-1].copy()
|
||||
height, width, channels = rgb.shape
|
||||
return QImage(rgb.data, width, height, channels * width, QImage.Format_RGB888).copy()
|
||||
return QImage(rgb.data, width, height, channels * width, QImage.Format.Format_RGB888).copy()
|
||||
|
||||
|
||||
_VIEW_LABELS = {
|
||||
@@ -92,7 +92,7 @@ class AspectImageLabel(QLabel):
|
||||
def __init__(self, parent=None) -> None:
|
||||
super().__init__(parent)
|
||||
self._aspect = DEFAULT_FRAME_ASPECT
|
||||
policy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
||||
policy = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
|
||||
policy.setHeightForWidth(True)
|
||||
self.setSizePolicy(policy)
|
||||
self.setMinimumSize(200, _IMAGE_MIN_HEIGHT)
|
||||
@@ -132,7 +132,7 @@ class ColorBarLabel(QLabel):
|
||||
self.setObjectName("colorBar")
|
||||
self.setFixedHeight(_COLORBAR_HEIGHT)
|
||||
self.setMinimumWidth(_COLORBAR_WIDTH)
|
||||
self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
|
||||
self.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
|
||||
self.setVisible(False)
|
||||
|
||||
def set_theme(self, theme: str) -> None:
|
||||
@@ -145,14 +145,14 @@ class ColorBarLabel(QLabel):
|
||||
# (256, 1, 3) BGR -> one RGB scanline. The bytes object stays alive for
|
||||
# the whole call, which is what QImage's borrowed buffer needs.
|
||||
rgb = flow_colormap(self._theme)[:, 0, ::-1].copy().tobytes()
|
||||
image = QImage(rgb, 256, 1, 256 * 3, QImage.Format_RGB888)
|
||||
image = QImage(rgb, 256, 1, 256 * 3, QImage.Format.Format_RGB888)
|
||||
painter = QPainter(self)
|
||||
painter.drawImage(self.rect(), image)
|
||||
painter.end()
|
||||
|
||||
|
||||
class ArrowPanel(QFrame):
|
||||
view_mode_changed = pyqtSignal(str)
|
||||
view_mode_changed = Signal(str)
|
||||
|
||||
def __init__(self, parent=None, *, theme: str = "dark") -> None:
|
||||
super().__init__(parent)
|
||||
@@ -188,7 +188,7 @@ class ArrowPanel(QFrame):
|
||||
layout.addLayout(header)
|
||||
|
||||
self.image_label = AspectImageLabel()
|
||||
self.image_label.setAlignment(Qt.AlignCenter)
|
||||
self.image_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.image_label.setScaledContents(False)
|
||||
self.image_label.setWordWrap(True)
|
||||
# The image keeps the frame's shape and is centred in whatever height is
|
||||
@@ -515,11 +515,11 @@ class ArrowPanel(QFrame):
|
||||
deg = self._effective_rotation_deg()
|
||||
if deg:
|
||||
# QTransform.rotate is clockwise on screen pixmaps; negate for CCW.
|
||||
pix = pix.transformed(QTransform().rotate(-deg), Qt.FastTransformation)
|
||||
pix = pix.transformed(QTransform().rotate(-deg), Qt.TransformationMode.FastTransformation)
|
||||
scaled = pix.scaled(
|
||||
self.image_label.size(),
|
||||
Qt.KeepAspectRatio,
|
||||
Qt.SmoothTransformation,
|
||||
Qt.AspectRatioMode.KeepAspectRatio,
|
||||
Qt.TransformationMode.SmoothTransformation,
|
||||
)
|
||||
self.image_label.setPixmap(scaled)
|
||||
self.image_label.setText("")
|
||||
|
||||
@@ -14,9 +14,9 @@ from __future__ import annotations
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from PyQt5.QtCore import Qt, pyqtSignal
|
||||
from PyQt5.QtGui import QColor, QPainter, QPixmap
|
||||
from PyQt5.QtWidgets import QFrame, QHBoxLayout, QLabel, QPushButton, QVBoxLayout
|
||||
from PySide6.QtCore import Qt, Signal
|
||||
from PySide6.QtGui import QColor, QPainter, QPixmap
|
||||
from PySide6.QtWidgets import QFrame, QHBoxLayout, QLabel, QPushButton, QVBoxLayout
|
||||
|
||||
from orisys_app_shared.i18n import t
|
||||
from orisys_app_shared.paths import logo_topbar_path
|
||||
@@ -39,13 +39,13 @@ def tinted_pixmap(path: Path | str, tint: str, size: int = LOGO_SIZE) -> QPixmap
|
||||
if source.isNull():
|
||||
return QPixmap()
|
||||
scaled = source.scaled(
|
||||
size, size, Qt.KeepAspectRatio, Qt.SmoothTransformation
|
||||
size, size, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation
|
||||
)
|
||||
out = QPixmap(scaled.size())
|
||||
out.fill(Qt.transparent)
|
||||
out.fill(Qt.GlobalColor.transparent)
|
||||
painter = QPainter(out)
|
||||
painter.drawPixmap(0, 0, scaled)
|
||||
painter.setCompositionMode(QPainter.CompositionMode_SourceIn)
|
||||
painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_SourceIn)
|
||||
painter.fillRect(out.rect(), QColor(tint))
|
||||
painter.end()
|
||||
return out
|
||||
@@ -55,11 +55,11 @@ class BrandBar(QFrame):
|
||||
"""Thin brand rail: lockup and top-level view switcher, nothing else."""
|
||||
|
||||
#: The user asked for the live view.
|
||||
live_requested = pyqtSignal()
|
||||
live_requested = Signal()
|
||||
#: The user asked for the history/replay view.
|
||||
playback_requested = pyqtSignal()
|
||||
playback_requested = Signal()
|
||||
#: The user asked for the calibration view.
|
||||
calibration_requested = pyqtSignal()
|
||||
calibration_requested = Signal()
|
||||
|
||||
def __init__(self, parent=None, *, theme: str = "dark") -> None:
|
||||
super().__init__(parent)
|
||||
@@ -74,7 +74,7 @@ class BrandBar(QFrame):
|
||||
self.logo = QLabel()
|
||||
self.logo.setObjectName("brandLogo")
|
||||
self.logo.setFixedSize(LOGO_SIZE, LOGO_SIZE)
|
||||
self.logo.setAlignment(Qt.AlignCenter)
|
||||
self.logo.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
layout.addWidget(self.logo)
|
||||
|
||||
# The lockup keeps its two labels (the product's tagline stays available
|
||||
@@ -113,7 +113,7 @@ class BrandBar(QFrame):
|
||||
|
||||
self.product = QLabel()
|
||||
self.product.setObjectName("brandProduct")
|
||||
self.product.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
|
||||
self.product.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
|
||||
layout.addWidget(self.product)
|
||||
|
||||
self.set_theme(theme)
|
||||
@@ -123,7 +123,7 @@ class BrandBar(QFrame):
|
||||
def _nav_button(self) -> QPushButton:
|
||||
button = QPushButton()
|
||||
button.setObjectName("navButton")
|
||||
button.setCursor(Qt.PointingHandCursor)
|
||||
button.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
button.setFlat(True)
|
||||
button.setCheckable(True)
|
||||
return button
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
from PyQt5.QtCore import Qt, pyqtSignal
|
||||
from PyQt5.QtGui import QColor, QFont, QFontDatabase
|
||||
from PyQt5.QtWidgets import (
|
||||
from PySide6.QtCore import Qt, Signal
|
||||
from PySide6.QtGui import QColor, QFont, QFontDatabase
|
||||
from PySide6.QtWidgets import (
|
||||
QAbstractItemView,
|
||||
QButtonGroup,
|
||||
QDoubleSpinBox,
|
||||
@@ -60,11 +60,11 @@ def _mono_font() -> str:
|
||||
|
||||
|
||||
class CalPanel(QFrame):
|
||||
status_message = pyqtSignal(str)
|
||||
status_message = Signal(str)
|
||||
#: Coefficients were written to disk. It does **not** mean they were applied.
|
||||
calibration_exported = pyqtSignal(str)
|
||||
calibration_exported = Signal(str)
|
||||
#: The operator asked to push the last export into the running device.
|
||||
apply_requested = pyqtSignal(str)
|
||||
apply_requested = Signal(str)
|
||||
|
||||
def __init__(self, parent=None, *, theme: str = "dark") -> None:
|
||||
super().__init__(parent)
|
||||
@@ -97,9 +97,9 @@ class CalPanel(QFrame):
|
||||
outer.setSpacing(0)
|
||||
self.scroll = QScrollArea()
|
||||
self.scroll.setWidgetResizable(True)
|
||||
self.scroll.setFrameShape(QFrame.NoFrame)
|
||||
self.scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
|
||||
self.scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
|
||||
self.scroll.setFrameShape(QFrame.Shape.NoFrame)
|
||||
self.scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
|
||||
self.scroll.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
|
||||
content = QWidget()
|
||||
self.scroll.setWidget(content)
|
||||
outer.addWidget(self.scroll)
|
||||
@@ -135,7 +135,7 @@ class CalPanel(QFrame):
|
||||
form = QFormLayout()
|
||||
form.setHorizontalSpacing(10)
|
||||
form.setVerticalSpacing(6)
|
||||
form.setLabelAlignment(Qt.AlignRight | Qt.AlignVCenter)
|
||||
form.setLabelAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
|
||||
self._weight_label = QLabel()
|
||||
self._weight_label.setObjectName("fieldLabel")
|
||||
self.weight_spin = QDoubleSpinBox()
|
||||
@@ -182,16 +182,16 @@ class CalPanel(QFrame):
|
||||
# Diagnostic table: the loaded axis plus the two crosstalk axes, a
|
||||
# stability measure and the point's state.
|
||||
self.table = QTableWidget(0, len(DIAGNOSTIC_COLUMNS))
|
||||
self.table.setSelectionBehavior(QAbstractItemView.SelectRows)
|
||||
self.table.setSelectionMode(QAbstractItemView.SingleSelection)
|
||||
self.table.setEditTriggers(QAbstractItemView.NoEditTriggers)
|
||||
self.table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
||||
self.table.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
|
||||
self.table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
|
||||
self.table.setAlternatingRowColors(True)
|
||||
self.table.setShowGrid(False)
|
||||
header = self.table.horizontalHeader()
|
||||
# Every column takes the width its own header and cells need: eliding a
|
||||
# diagnostic column ("FN 均值" -> "FN 均") hides the meaning, so a narrow
|
||||
# panel scrolls instead.
|
||||
header.setSectionResizeMode(QHeaderView.ResizeToContents)
|
||||
header.setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents)
|
||||
header.setMinimumSectionSize(64)
|
||||
self.table.verticalHeader().setVisible(False)
|
||||
self.table.verticalHeader().setDefaultSectionSize(26)
|
||||
@@ -235,7 +235,7 @@ class CalPanel(QFrame):
|
||||
):
|
||||
# The action row must survive a narrow three-panel layout.
|
||||
button.setProperty("compact", "true")
|
||||
button.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
|
||||
button.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed)
|
||||
self.btn_toggle_excluded.clicked.connect(self._on_toggle_excluded)
|
||||
self.btn_toggle_validation.clicked.connect(self._on_toggle_validation)
|
||||
self.btn_delete.clicked.connect(self._on_delete_row)
|
||||
@@ -523,11 +523,11 @@ class CalPanel(QFrame):
|
||||
)
|
||||
for column, text in enumerate(values):
|
||||
item = QTableWidgetItem(text)
|
||||
item.setFlags(item.flags() & ~Qt.ItemIsEditable)
|
||||
item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsEditable)
|
||||
if column in (0, 1, 2, 3):
|
||||
item.setTextAlignment(int(Qt.AlignRight | Qt.AlignVCenter))
|
||||
item.setTextAlignment(int(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter))
|
||||
else:
|
||||
item.setTextAlignment(int(Qt.AlignCenter))
|
||||
item.setTextAlignment(int(Qt.AlignmentFlag.AlignCenter))
|
||||
if point.excluded:
|
||||
item.setForeground(QColor(color(self._theme, "text_muted")))
|
||||
elif point.is_validation:
|
||||
@@ -611,7 +611,7 @@ class CalPanel(QFrame):
|
||||
"""Double click a row to inspect that point's three-axis curve."""
|
||||
dialog = self.point_dialog(self._current_point())
|
||||
if dialog is not None:
|
||||
dialog.exec_()
|
||||
dialog.exec()
|
||||
|
||||
def _on_toggle_excluded(self) -> None:
|
||||
point = self._current_point()
|
||||
|
||||
@@ -7,9 +7,9 @@ the three-panel layout, while a single point can still be examined in full.
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtGui import QFont, QFontDatabase
|
||||
from PyQt5.QtWidgets import (
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtGui import QFont, QFontDatabase
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog,
|
||||
QDialogButtonBox,
|
||||
QFormLayout,
|
||||
@@ -103,7 +103,7 @@ class CalibrationPointDialog(QDialog):
|
||||
root.addWidget(self.plot_widget, 1)
|
||||
self._build_traces()
|
||||
|
||||
buttons = QDialogButtonBox(QDialogButtonBox.Close)
|
||||
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Close)
|
||||
buttons.rejected.connect(self.reject)
|
||||
buttons.accepted.connect(self.accept)
|
||||
root.addWidget(buttons)
|
||||
|
||||
@@ -9,8 +9,8 @@ clips the text and the tooltip remains the way to read it.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from PyQt5.QtCore import QSize
|
||||
from PyQt5.QtWidgets import QLabel, QSizePolicy
|
||||
from PySide6.QtCore import QSize
|
||||
from PySide6.QtWidgets import QLabel, QSizePolicy
|
||||
|
||||
|
||||
class ShrinkableLabel(QLabel):
|
||||
@@ -18,7 +18,7 @@ class ShrinkableLabel(QLabel):
|
||||
|
||||
def __init__(self, text: str = "", parent=None) -> None:
|
||||
super().__init__(text, parent)
|
||||
self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)
|
||||
self.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Preferred)
|
||||
self.setMinimumWidth(0)
|
||||
|
||||
def minimumSizeHint(self) -> QSize: # noqa: N802 - Qt naming
|
||||
|
||||
@@ -5,9 +5,9 @@ from __future__ import annotations
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
from PyQt5.QtCore import Qt, QSize, pyqtSignal
|
||||
from PyQt5.QtGui import QColor, QFont, QFontDatabase, QFontMetrics
|
||||
from PyQt5.QtWidgets import (
|
||||
from PySide6.QtCore import Qt, QSize, Signal
|
||||
from PySide6.QtGui import QColor, QFont, QFontDatabase, QFontMetrics
|
||||
from PySide6.QtWidgets import (
|
||||
QFrame,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
@@ -116,18 +116,18 @@ class _Sync:
|
||||
|
||||
class ForcePlotsPanel(QFrame):
|
||||
#: The visible time window changed while browsing history.
|
||||
window_changed = pyqtSignal(float, float)
|
||||
window_changed = Signal(float, float)
|
||||
#: The shared cursor moved; other views sync their playback position to it.
|
||||
cursor_moved = pyqtSignal(float)
|
||||
cursor_moved = Signal(float)
|
||||
#: "follow" (live) or "browse" (user-controlled window).
|
||||
mode_changed = pyqtSignal(str)
|
||||
mode_changed = Signal(str)
|
||||
#: The unit chip was clicked: switch between newton and raw.
|
||||
unit_toggle_requested = pyqtSignal()
|
||||
unit_toggle_requested = Signal()
|
||||
#: The gear was clicked: open the plot settings (window, range, filters).
|
||||
plot_settings_requested = pyqtSignal()
|
||||
plot_settings_requested = Signal()
|
||||
#: The operator took the timeline over (dragged the cursor, box-zoomed, panned
|
||||
#: or moved the overview marker): playback stops where it is.
|
||||
timeline_taken_over = pyqtSignal()
|
||||
timeline_taken_over = Signal()
|
||||
|
||||
def __init__(self, parent=None, *, theme: str = "dark") -> None:
|
||||
super().__init__(parent)
|
||||
@@ -217,7 +217,7 @@ class ForcePlotsPanel(QFrame):
|
||||
self.unit_badge = QPushButton()
|
||||
self.unit_badge.setObjectName("unitBadge")
|
||||
self.unit_badge.setProperty("compact", "true")
|
||||
self.unit_badge.setCursor(Qt.PointingHandCursor)
|
||||
self.unit_badge.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self.unit_badge.clicked.connect(self.unit_toggle_requested.emit)
|
||||
header.addWidget(self.unit_badge)
|
||||
root.addLayout(header)
|
||||
@@ -329,7 +329,7 @@ class ForcePlotsPanel(QFrame):
|
||||
self.window_label.setObjectName("panelMeta")
|
||||
self.stats_label = QLabel()
|
||||
self.stats_label.setObjectName("panelMeta")
|
||||
self.stats_label.setTextInteractionFlags(Qt.TextSelectableByMouse)
|
||||
self.stats_label.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
|
||||
browse_row.addWidget(self.window_caption)
|
||||
browse_row.addWidget(self.window_label)
|
||||
browse_row.addStretch(1)
|
||||
@@ -348,7 +348,7 @@ class ForcePlotsPanel(QFrame):
|
||||
button.setProperty("iconOnly", "true")
|
||||
button.setIcon(icons.icon(icon_name, self._theme))
|
||||
button.setIconSize(QSize(ICON_SIZE, ICON_SIZE))
|
||||
button.setCursor(Qt.PointingHandCursor)
|
||||
button.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
button._icon_name = icon_name # noqa: SLF001 - read back by set_theme
|
||||
return button
|
||||
|
||||
@@ -389,7 +389,7 @@ class ForcePlotsPanel(QFrame):
|
||||
value_row.setSpacing(5)
|
||||
value_label = QLabel()
|
||||
value_label.setObjectName("valueCard")
|
||||
value_label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
|
||||
value_label.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
|
||||
value_label.setFont(_mono_font(16, bold=True))
|
||||
# Never a fixed height: the assertion that a value is not clipped is
|
||||
# derived from the font it is actually drawn with.
|
||||
|
||||
@@ -14,8 +14,8 @@ from __future__ import annotations
|
||||
|
||||
from functools import lru_cache
|
||||
|
||||
from PyQt5.QtCore import QRectF, Qt
|
||||
from PyQt5.QtGui import QColor, QIcon, QPainter, QPen, QPixmap
|
||||
from PySide6.QtCore import QRectF, Qt
|
||||
from PySide6.QtGui import QColor, QIcon, QPainter, QPen, QPixmap
|
||||
|
||||
from orisys_app_shared.theme import color
|
||||
|
||||
@@ -50,7 +50,7 @@ def _paint_zoom_fit(painter: QPainter, size: int) -> None:
|
||||
def _paint_box_zoom(painter: QPainter, size: int) -> None:
|
||||
"""A dashed frame: drag a region to zoom into it."""
|
||||
pen = painter.pen()
|
||||
pen.setStyle(Qt.DashLine)
|
||||
pen.setStyle(Qt.PenStyle.DashLine)
|
||||
pen.setDashPattern([2.2, 1.6])
|
||||
painter.setPen(pen)
|
||||
painter.drawRect(QRectF(2.0, 2.0, size - 4.0, size - 4.0))
|
||||
@@ -143,19 +143,19 @@ _PAINTERS = {
|
||||
def pixmap(name: str, tint: str, size: int = ICON_SIZE) -> QPixmap:
|
||||
"""One monochrome icon, drawn in ``tint`` (a ``#RRGGBB`` token value)."""
|
||||
canvas = QPixmap(size, size)
|
||||
canvas.fill(Qt.transparent)
|
||||
canvas.fill(Qt.GlobalColor.transparent)
|
||||
paint = _PAINTERS.get(str(name))
|
||||
if paint is None:
|
||||
return canvas
|
||||
painter = QPainter(canvas)
|
||||
painter.setRenderHint(QPainter.Antialiasing, True)
|
||||
painter.setRenderHint(QPainter.RenderHint.Antialiasing, True)
|
||||
pen = QPen()
|
||||
pen.setWidthF(_PEN_WIDTH)
|
||||
pen.setCapStyle(Qt.RoundCap)
|
||||
pen.setJoinStyle(Qt.RoundJoin)
|
||||
pen.setCapStyle(Qt.PenCapStyle.RoundCap)
|
||||
pen.setJoinStyle(Qt.PenJoinStyle.RoundJoin)
|
||||
pen.setColor(QColor(tint))
|
||||
painter.setPen(pen)
|
||||
painter.setBrush(Qt.NoBrush)
|
||||
painter.setBrush(Qt.BrushStyle.NoBrush)
|
||||
try:
|
||||
paint(painter, size)
|
||||
finally:
|
||||
|
||||
@@ -16,8 +16,8 @@ from __future__ import annotations
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Callable, Optional
|
||||
|
||||
from PyQt5.QtCore import QSize, Qt, pyqtSignal
|
||||
from PyQt5.QtWidgets import (
|
||||
from PySide6.QtCore import QSize, Qt, Signal
|
||||
from PySide6.QtWidgets import (
|
||||
QFrame,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
@@ -78,7 +78,7 @@ class Mesh3dPanel(QFrame):
|
||||
|
||||
#: Emitted when the operator turns the pane on. The window wires the packet sink
|
||||
#: from this, so no packet is adapted while the pane is off.
|
||||
enabledChanged = pyqtSignal(bool)
|
||||
enabledChanged = Signal(bool)
|
||||
|
||||
def __init__(
|
||||
self, parent: Optional[QWidget] = None, *, theme: str = "dark"
|
||||
@@ -128,14 +128,14 @@ class Mesh3dPanel(QFrame):
|
||||
self.btn_reset_view = QPushButton()
|
||||
self.btn_reset_view.setProperty("compact", "true")
|
||||
self.btn_reset_view.setProperty("iconOnly", "true")
|
||||
self.btn_reset_view.setCursor(Qt.PointingHandCursor)
|
||||
self.btn_reset_view.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self.btn_reset_view.clicked.connect(self.reset_view)
|
||||
header.addWidget(self.btn_reset_view)
|
||||
|
||||
self.btn_settings = QPushButton()
|
||||
self.btn_settings.setProperty("compact", "true")
|
||||
self.btn_settings.setProperty("iconOnly", "true")
|
||||
self.btn_settings.setCursor(Qt.PointingHandCursor)
|
||||
self.btn_settings.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self.btn_settings.clicked.connect(self.open_settings)
|
||||
header.addWidget(self.btn_settings)
|
||||
root.addLayout(header)
|
||||
@@ -145,7 +145,7 @@ class Mesh3dPanel(QFrame):
|
||||
|
||||
self.message = QLabel()
|
||||
self.message.setObjectName("hintText")
|
||||
self.message.setAlignment(Qt.AlignCenter)
|
||||
self.message.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.message.setWordWrap(True)
|
||||
self._stack.addWidget(self.message)
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ reports as done is what the record will say (see ``force/filters.py``).
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from PyQt5.QtWidgets import (
|
||||
from PySide6.QtWidgets import (
|
||||
QCheckBox,
|
||||
QComboBox,
|
||||
QDialog,
|
||||
@@ -133,7 +133,7 @@ class PlotSettingsDialog(QDialog):
|
||||
self.btn_reset.clicked.connect(self._reset_defaults)
|
||||
buttons_row.addWidget(self.btn_reset)
|
||||
buttons_row.addStretch(1)
|
||||
self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
||||
self.button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
|
||||
self.button_box.accepted.connect(self.accept)
|
||||
self.button_box.rejected.connect(self.reject)
|
||||
buttons_row.addWidget(self.button_box)
|
||||
@@ -384,8 +384,8 @@ class PlotSettingsDialog(QDialog):
|
||||
self.rate_label.setText(t("dialog.filter_rate"))
|
||||
self.rate_spin.setToolTip(t("dialog.filter_rate_tip"))
|
||||
self._refresh_filter_note()
|
||||
ok_btn = self.button_box.button(QDialogButtonBox.Ok)
|
||||
cancel_btn = self.button_box.button(QDialogButtonBox.Cancel)
|
||||
ok_btn = self.button_box.button(QDialogButtonBox.StandardButton.Ok)
|
||||
cancel_btn = self.button_box.button(QDialogButtonBox.StandardButton.Cancel)
|
||||
if ok_btn is not None:
|
||||
ok_btn.setText(t("dialog.ok"))
|
||||
if cancel_btn is not None:
|
||||
|
||||
@@ -14,7 +14,7 @@ timer without touching the writer thread.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from PyQt5.QtWidgets import QLabel
|
||||
from PySide6.QtWidgets import QLabel
|
||||
|
||||
from orisys_app_shared.i18n import t
|
||||
from orisys_app_shared.theme import color
|
||||
|
||||
@@ -17,7 +17,7 @@ from __future__ import annotations
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from PyQt5.QtWidgets import (
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog,
|
||||
QFrame,
|
||||
QHBoxLayout,
|
||||
@@ -111,7 +111,7 @@ class SensorDialog(QDialog):
|
||||
def _section_rule() -> QFrame:
|
||||
rule = QFrame()
|
||||
rule.setObjectName("sectionSeparator")
|
||||
rule.setFrameShape(QFrame.HLine)
|
||||
rule.setFrameShape(QFrame.Shape.HLine)
|
||||
rule.setFixedHeight(1)
|
||||
return rule
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@ own labels and tooltips instead of a bare "配置" combo.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from PyQt5.QtCore import Qt, pyqtSignal
|
||||
from PyQt5.QtWidgets import (
|
||||
from PySide6.QtCore import Qt, Signal
|
||||
from PySide6.QtWidgets import (
|
||||
QComboBox,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
@@ -29,8 +29,8 @@ BADGE_WIDTH = 46
|
||||
|
||||
|
||||
class SensorPickerWidget(QWidget):
|
||||
sensor_type_changed = pyqtSignal(str)
|
||||
config_changed = pyqtSignal(str)
|
||||
sensor_type_changed = Signal(str)
|
||||
config_changed = Signal(str)
|
||||
|
||||
def __init__(self, parent=None) -> None:
|
||||
super().__init__(parent)
|
||||
@@ -42,7 +42,7 @@ class SensorPickerWidget(QWidget):
|
||||
#: attribute: it states which sensor the whole window is configured for.
|
||||
self.thumb_label = QLabel()
|
||||
self.thumb_label.setObjectName("unitBadge")
|
||||
self.thumb_label.setAlignment(Qt.AlignCenter)
|
||||
self.thumb_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
# A minimum, not a fixed width: "FINGER" must not be truncated to "INGE".
|
||||
self.thumb_label.setMinimumWidth(BADGE_WIDTH)
|
||||
self.thumb_label.setProperty("rail", "true")
|
||||
@@ -55,7 +55,7 @@ class SensorPickerWidget(QWidget):
|
||||
self.type_combo = QComboBox()
|
||||
self.type_combo.setMinimumWidth(TYPE_WIDTH)
|
||||
self.type_combo.setProperty("rail", "true")
|
||||
self.type_combo.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
|
||||
self.type_combo.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed)
|
||||
for name in list_sensor_types():
|
||||
self.type_combo.addItem(name, name)
|
||||
self.type_combo.currentIndexChanged.connect(self._on_type_index_changed)
|
||||
@@ -65,7 +65,7 @@ class SensorPickerWidget(QWidget):
|
||||
self.config_combo = QComboBox()
|
||||
self.config_combo.setMinimumWidth(CONFIG_WIDTH)
|
||||
self.config_combo.setProperty("rail", "true")
|
||||
self.config_combo.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
|
||||
self.config_combo.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed)
|
||||
self.config_combo.currentIndexChanged.connect(self._on_config_index_changed)
|
||||
|
||||
layout.addWidget(self.thumb_label)
|
||||
|
||||
@@ -12,8 +12,8 @@ scrolls sideways instead of squeezing the controls into an unreadable row.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from PyQt5.QtCore import Qt, pyqtSignal
|
||||
from PyQt5.QtWidgets import (
|
||||
from PySide6.QtCore import Qt, Signal
|
||||
from PySide6.QtWidgets import (
|
||||
QComboBox,
|
||||
QFrame,
|
||||
QHBoxLayout,
|
||||
@@ -42,9 +42,9 @@ RECORD_MODES = (("video_data", "record.mode_video_data"), ("data_only", "record.
|
||||
def _separator() -> QFrame:
|
||||
line = QFrame()
|
||||
line.setObjectName("sectionSeparator")
|
||||
line.setFrameShape(QFrame.VLine)
|
||||
line.setFrameShape(QFrame.Shape.VLine)
|
||||
line.setFixedWidth(1)
|
||||
line.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding)
|
||||
line.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Expanding)
|
||||
return line
|
||||
|
||||
|
||||
@@ -70,23 +70,23 @@ def _group(*widgets: QWidget) -> QWidget:
|
||||
|
||||
|
||||
class ToolbarWidget(QWidget):
|
||||
search_clicked = pyqtSignal()
|
||||
preview_toggled = pyqtSignal(bool)
|
||||
camera_id_changed = pyqtSignal(int)
|
||||
search_clicked = Signal()
|
||||
preview_toggled = Signal(bool)
|
||||
camera_id_changed = Signal(int)
|
||||
#: The sensor summary button: opens the sensor and calibration dialog.
|
||||
sensor_clicked = pyqtSignal()
|
||||
sensor_clicked = Signal()
|
||||
#: "Back to the camera" while a video file is the run source.
|
||||
use_camera_clicked = pyqtSignal()
|
||||
use_camera_clicked = Signal()
|
||||
#: Replay view: pick the recorded video to play.
|
||||
replay_video_clicked = pyqtSignal()
|
||||
replay_video_clicked = Signal()
|
||||
#: Replay view: re-run the pipeline over that video (the only processed picture).
|
||||
recompute_clicked = pyqtSignal()
|
||||
run_toggled = pyqtSignal(bool)
|
||||
reset_clicked = pyqtSignal()
|
||||
units_changed = pyqtSignal(str)
|
||||
record_toggled = pyqtSignal(bool)
|
||||
recompute_clicked = Signal()
|
||||
run_toggled = Signal(bool)
|
||||
reset_clicked = Signal()
|
||||
units_changed = Signal(str)
|
||||
record_toggled = Signal(bool)
|
||||
#: The settings area was expanded or collapsed by the user.
|
||||
settings_toggled = pyqtSignal(bool)
|
||||
settings_toggled = Signal(bool)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -124,9 +124,9 @@ class ToolbarWidget(QWidget):
|
||||
# narrow window.
|
||||
self._settings_scroll = QScrollArea()
|
||||
self._settings_scroll.setWidgetResizable(True)
|
||||
self._settings_scroll.setFrameShape(QFrame.NoFrame)
|
||||
self._settings_scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
||||
self._settings_scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
|
||||
self._settings_scroll.setFrameShape(QFrame.Shape.NoFrame)
|
||||
self._settings_scroll.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
||||
self._settings_scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
|
||||
self.settings_page = self._build_settings()
|
||||
self._settings_scroll.setWidget(self.settings_page)
|
||||
outer.addWidget(self._settings_scroll)
|
||||
@@ -157,7 +157,7 @@ class ToolbarWidget(QWidget):
|
||||
|
||||
self.status_chip = QLabel()
|
||||
self.status_chip.setObjectName("statusChip")
|
||||
self.status_chip.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred)
|
||||
self.status_chip.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Preferred)
|
||||
self.source_summary = ShrinkableLabel()
|
||||
self.source_summary.setObjectName("panelMeta")
|
||||
self.calibration_label = ShrinkableLabel()
|
||||
|
||||
@@ -24,7 +24,7 @@ import logging
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Optional, Protocol
|
||||
|
||||
from PyQt5.QtCore import QObject
|
||||
from PySide6.QtCore import QObject
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
The tests render real Qt widgets with the offscreen platform plugin, so they need
|
||||
no display and no camera hardware. A single ``QApplication`` is shared by the
|
||||
whole session (Qt forbids creating more than one).
|
||||
|
||||
The suite requires PySide6, the app's only Qt binding. Where it is missing the
|
||||
modules are skipped rather than failed, so a broken environment cannot look like a
|
||||
broken application (the same rule ``apps/autocal`` follows).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -16,11 +20,30 @@ from pathlib import Path
|
||||
import pytest
|
||||
|
||||
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||
# In a development environment that also has PyQt5 installed, pyqtgraph would otherwise
|
||||
# bind to it (it takes whichever toolkit it finds first) and the plots would break.
|
||||
os.environ.setdefault("PYQTGRAPH_QT_LIB", "PySide6")
|
||||
if sys.platform == "win32" and "QT_QPA_FONTDIR" not in os.environ:
|
||||
_fonts = Path(os.environ.get("WINDIR", r"C:\Windows")) / "Fonts"
|
||||
if _fonts.is_dir():
|
||||
os.environ["QT_QPA_FONTDIR"] = str(_fonts)
|
||||
|
||||
|
||||
def _have_pyside6() -> tuple[bool, str]:
|
||||
try:
|
||||
import PySide6
|
||||
except ImportError as exc: # pragma: no cover - depends on the machine
|
||||
return False, str(exc)
|
||||
return True, getattr(PySide6, "__version__", "unknown")
|
||||
|
||||
|
||||
HAVE_PYSIDE6, PYSIDE6_VERSION = _have_pyside6()
|
||||
|
||||
if not HAVE_PYSIDE6: # pragma: no cover - depends on the machine
|
||||
# Mirrors apps/autocal/tests/conftest.py: nothing is collected, so a machine without
|
||||
# the binding reports no tests instead of a wall of import errors.
|
||||
collect_ignore_glob = ["test_*.py"]
|
||||
|
||||
# ``tools/ui_check.py`` owns the palette rules; importing it keeps one source of
|
||||
# truth between the pytest suite and the dependency-free checker.
|
||||
_TOOLS_DIR = Path(__file__).resolve().parents[1] / "tools"
|
||||
@@ -30,7 +53,7 @@ if _TOOLS_DIR.is_dir() and str(_TOOLS_DIR) not in sys.path:
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def qt_app():
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
app = QApplication.instance() or QApplication([])
|
||||
yield app
|
||||
@@ -83,7 +106,7 @@ def visible_descendants(widget):
|
||||
|
||||
def show_window(window, size):
|
||||
"""Show a window at ``size`` and flush pending Qt events."""
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
window.resize(*size)
|
||||
window.show()
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
"""Binding policy for the studio.
|
||||
|
||||
The studio is **PySide6 only** — Qt's LGPLv3 binding, which is why the app was migrated off
|
||||
PyQt5 (GPLv3-or-commercial). AutoCal has the same guard in
|
||||
``apps/autocal/tests/test_binding_purity.py``; this file is the studio's half of it:
|
||||
|
||||
* no module may import PyQt5, PySide2 or PyQt6 — the app's environment has PySide6 and
|
||||
nothing else, so such an import is a hard start-up failure;
|
||||
* every UI and 3D module must name the binding it uses rather than inheriting one;
|
||||
* the PyQt5 signal spellings (``pyqtSignal``, ``pyqtBoundSignal``) do not exist in PySide6;
|
||||
* no module may use an unscoped Qt enumerator (``Qt.AlignLeft``, ``QPainter.Antialiasing``);
|
||||
* importing the application must not drag another binding into ``sys.modules`` — that is
|
||||
what breaks ``pyqtgraph``, which binds to whichever toolkit it finds first.
|
||||
|
||||
Two lessons from the migration are baked in here. The unscoped-enum check asks PySide6
|
||||
instead of using a hand-written list of enum holders: a curated list missed
|
||||
``QPainter.RenderHint.Antialiasing``, ``QPainter.CompositionMode.CompositionMode_SourceIn`` and
|
||||
``QDialogButtonBox.StandardButton.Ok|Cancel|Close``. And the ``sys.modules`` check runs in a **fresh
|
||||
interpreter**, because in-process the answer depends on what some earlier test imported.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import enum
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
APP = Path(__file__).resolve().parents[1] # apps/studio
|
||||
SRC = APP / "src"
|
||||
PROJECT_ROOT = APP.parents[1] # orisys-app
|
||||
|
||||
#: Everything that ships or drives the app.
|
||||
SCAN_ROOTS = (SRC, APP / "tools")
|
||||
|
||||
#: Bindings no module may import.
|
||||
FORBIDDEN_BINDINGS = {"PyQt5", "PyQt6", "PySide2"}
|
||||
|
||||
#: The one binding allowed.
|
||||
ALLOWED_BINDING = "PySide6"
|
||||
|
||||
#: Directories that must name their binding explicitly.
|
||||
#:
|
||||
#: ``ui/`` only: every widget there imports PySide6. ``scene3d/`` is deliberately mixed —
|
||||
#: ``coordinator.py``, ``camera.py``, ``adapters.py``, ``prefs.py`` and ``keys.py`` import no
|
||||
#: Qt binding at all so they can be tested headless, and requiring one would forbid that
|
||||
#: design. The modules there that *do* use Qt already name PySide6.
|
||||
BINDING_DECLARING_DIRS = (SRC / "orisys_studio" / "ui",)
|
||||
|
||||
#: Names that only ever existed in PyQt. Quoted here on purpose — the migration scripts that
|
||||
#: do the rewriting skip this file, because a text-based rule cannot tell a forbidden spelling
|
||||
#: in code from one named in the policy that forbids it.
|
||||
PYQT_ONLY_NAMES = ("pyqtSignal", "pyqtBoundSignal", "pyqtSlot", "pyqtProperty")
|
||||
|
||||
_SKIP_PARTS = ("__pycache__",)
|
||||
|
||||
|
||||
def _python_files(root: Path) -> list[Path]:
|
||||
return sorted(
|
||||
path
|
||||
for path in root.rglob("*.py")
|
||||
if not any(part in path.parts for part in _SKIP_PARTS)
|
||||
)
|
||||
|
||||
|
||||
def _read(path: Path) -> str:
|
||||
# One file in the workspace carries a UTF-8 BOM; `utf-8-sig` keeps `ast` happy either way.
|
||||
return path.read_text(encoding="utf-8-sig", errors="replace")
|
||||
|
||||
|
||||
def _imported_packages(path: Path) -> set[str]:
|
||||
"""Every top-level package name imported by ``path``."""
|
||||
found: set[str] = set()
|
||||
for node in ast.walk(ast.parse(_read(path))):
|
||||
if isinstance(node, ast.Import):
|
||||
found.update(alias.name.split(".")[0] for alias in node.names)
|
||||
elif isinstance(node, ast.ImportFrom) and node.module and node.level == 0:
|
||||
found.add(node.module.split(".")[0])
|
||||
return found
|
||||
|
||||
|
||||
def _qt_namespace() -> dict[str, object]:
|
||||
"""The Qt classes the sources could name, resolved through the binding itself."""
|
||||
from PySide6 import QtCore, QtGui, QtOpenGLWidgets, QtWidgets
|
||||
|
||||
namespace: dict[str, object] = {"Qt": QtCore.Qt}
|
||||
return namespace, (QtCore, QtGui, QtWidgets, QtOpenGLWidgets)
|
||||
|
||||
|
||||
def _unscoped_enums(path: Path, namespace: dict[str, object], modules: tuple) -> list[str]:
|
||||
"""Attribute reads whose value resolves to an enum instance rather than an enum class.
|
||||
|
||||
``Qt.AlignmentFlag.AlignLeft`` is an inner attribute of an enum class, so the outer
|
||||
``Qt.AlignmentFlag`` resolves to a *class* and the inner one is not a plain name — neither
|
||||
is a candidate. ``QMessageBox.information`` resolves to a method. ``Qt.AlignmentFlag.AlignLeft``
|
||||
resolves to an enum instance, which is the spelling this test exists to reject.
|
||||
"""
|
||||
tree = ast.parse(_read(path))
|
||||
called = {
|
||||
id(node.func)
|
||||
for node in ast.walk(tree)
|
||||
if isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute)
|
||||
}
|
||||
inner = {id(node.value) for node in ast.walk(tree) if isinstance(node, ast.Attribute)}
|
||||
found: set[str] = set()
|
||||
for node in ast.walk(tree):
|
||||
if not isinstance(node, ast.Attribute) or not isinstance(node.value, ast.Name):
|
||||
continue
|
||||
if id(node) in called or id(node) in inner:
|
||||
continue
|
||||
holder, member = node.value.id, node.attr
|
||||
target_holder = namespace.get(holder)
|
||||
if target_holder is None:
|
||||
for module in modules:
|
||||
if hasattr(module, holder):
|
||||
target_holder = namespace[holder] = getattr(module, holder)
|
||||
break
|
||||
else:
|
||||
continue
|
||||
value = getattr(target_holder, member, None)
|
||||
if isinstance(value, (enum.Enum, enum.Flag)) and member not in namespace:
|
||||
found.add(f"{holder}.{member}")
|
||||
return sorted(found)
|
||||
|
||||
|
||||
def test_no_studio_module_imports_a_forbidden_binding():
|
||||
offenders = []
|
||||
for root in SCAN_ROOTS:
|
||||
for path in _python_files(root):
|
||||
bad = _imported_packages(path) & FORBIDDEN_BINDINGS
|
||||
if bad:
|
||||
offenders.append(f"{path.relative_to(APP)}: {sorted(bad)}")
|
||||
assert not offenders, (
|
||||
f"the studio is {ALLOWED_BINDING}-only; these modules import another binding: "
|
||||
f"{offenders}"
|
||||
)
|
||||
|
||||
|
||||
def test_every_ui_module_imports_the_allowed_binding():
|
||||
"""A widget module that names no binding would be using one it does not declare."""
|
||||
offenders = []
|
||||
for directory in BINDING_DECLARING_DIRS:
|
||||
for path in _python_files(directory):
|
||||
if path.name == "__init__.py":
|
||||
continue # a re-export barrel, not a widget module
|
||||
if ALLOWED_BINDING not in _imported_packages(path):
|
||||
offenders.append(str(path.relative_to(APP)))
|
||||
assert not offenders, f"these modules do not import {ALLOWED_BINDING}: {offenders}"
|
||||
|
||||
|
||||
def test_no_pyqt_only_spelling_survives():
|
||||
offenders = []
|
||||
for root in SCAN_ROOTS:
|
||||
for path in _python_files(root):
|
||||
text = _read(path)
|
||||
for name in PYQT_ONLY_NAMES:
|
||||
if name in text:
|
||||
offenders.append(f"{path.relative_to(APP)}: {name}")
|
||||
assert not offenders, f"these PyQt-only names do not exist in PySide6: {offenders}"
|
||||
|
||||
|
||||
def test_no_module_uses_an_unscoped_enum():
|
||||
try:
|
||||
import PySide6 # noqa: F401
|
||||
except ImportError: # pragma: no cover - depends on the machine
|
||||
import pytest
|
||||
|
||||
pytest.skip("PySide6 is not installed here, so the enum rule cannot be checked")
|
||||
namespace, modules = _qt_namespace()
|
||||
offenders = {}
|
||||
for root in SCAN_ROOTS:
|
||||
for path in _python_files(root):
|
||||
found = _unscoped_enums(path, namespace, modules)
|
||||
if found:
|
||||
offenders[str(path.relative_to(APP))] = found
|
||||
assert not offenders, (
|
||||
"unscoped Qt enumerators are the spelling PySide6 is removing; use the scoped "
|
||||
f"form: {offenders}"
|
||||
)
|
||||
|
||||
|
||||
def test_importing_the_application_pulls_in_no_other_binding():
|
||||
"""Fresh interpreter: PySide6 only, whatever this test session imported before."""
|
||||
import os
|
||||
|
||||
script = (
|
||||
"import sys;"
|
||||
"import orisys_studio.app_window;"
|
||||
"import orisys_studio.scene3d.view;"
|
||||
"import orisys_studio.ui.force_plots;"
|
||||
"print(','.join(m for m in ('PyQt5', 'PyQt6', 'PySide2') if m in sys.modules))"
|
||||
)
|
||||
env = dict(os.environ)
|
||||
entries = [str(PROJECT_ROOT / "shared" / "src"), str(SRC)]
|
||||
if env.get("PYTHONPATH"):
|
||||
entries.append(env["PYTHONPATH"])
|
||||
env["PYTHONPATH"] = os.pathsep.join(entries)
|
||||
# The app's own start-up contract (``main.py``) and a display-free platform: the point
|
||||
# is which *binding* comes in, not whether a window can be shown.
|
||||
env.setdefault("PYQTGRAPH_QT_LIB", "PySide6")
|
||||
env.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-c", script],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=180,
|
||||
check=False,
|
||||
env=env,
|
||||
)
|
||||
assert result.returncode == 0, result.stderr
|
||||
pulled = [name for name in result.stdout.strip().split(",") if name]
|
||||
assert pulled == [], (
|
||||
f"importing the app pulled in {pulled}; pyqtgraph binds to whichever toolkit it "
|
||||
"finds first, so a second binding in the environment breaks the plots"
|
||||
)
|
||||
|
||||
|
||||
def test_the_app_declares_its_binding_dependency():
|
||||
"""The packaging must state PySide6, not leave it to chance."""
|
||||
pyproject = (APP / "pyproject.toml").read_text(encoding="utf-8")
|
||||
assert ALLOWED_BINDING in pyproject
|
||||
for forbidden in FORBIDDEN_BINDINGS:
|
||||
assert f'"{forbidden}' not in pyproject, forbidden
|
||||
@@ -94,15 +94,16 @@ def _render(widget, qt_app) -> np.ndarray:
|
||||
on a little-endian machine, with a stride that can exceed the width — so the
|
||||
conversion and the stride trim are both load-bearing.
|
||||
"""
|
||||
from PyQt5.QtGui import QImage
|
||||
from PySide6.QtGui import QImage
|
||||
|
||||
widget.repaint()
|
||||
qt_app.processEvents()
|
||||
image = widget.grabFramebuffer()
|
||||
assert not image.isNull(), "Qt returned no framebuffer"
|
||||
image = image.convertToFormat(QImage.Format_RGB888)
|
||||
image = image.convertToFormat(QImage.Format.Format_RGB888)
|
||||
width, height = image.width(), image.height()
|
||||
buffer = image.constBits().asarray(image.byteCount())
|
||||
# PySide6 returns a `memoryview` from constBits(), not a QByteArray: wrap it directly.
|
||||
buffer = np.frombuffer(image.constBits(), dtype=np.uint8)
|
||||
stride = image.bytesPerLine()
|
||||
rows = np.frombuffer(buffer, dtype=np.uint8).reshape(height, stride)
|
||||
return rows[:, : width * 3].reshape(height, width, 3).copy()
|
||||
|
||||
@@ -13,8 +13,8 @@ view menu / the pane's header both reach that dialog.
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from PyQt5.QtCore import pyqtSignal
|
||||
from PyQt5.QtWidgets import QWidget
|
||||
from PySide6.QtCore import Signal
|
||||
from PySide6.QtWidgets import QWidget
|
||||
|
||||
from orisys_app_shared.i18n import t
|
||||
from orisys_studio.ui import icons
|
||||
@@ -117,8 +117,8 @@ def test_the_pane_header_offers_the_two_actions(qt_app):
|
||||
|
||||
# Reset forwards to the view when there is one.
|
||||
class _View(QWidget):
|
||||
diagnosticRaised = pyqtSignal(object)
|
||||
enabledChanged = pyqtSignal(bool)
|
||||
diagnosticRaised = Signal(object)
|
||||
enabledChanged = Signal(bool)
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
@@ -229,7 +229,7 @@ def test_the_diagnostics_switch_governs_what_the_pane_explains(panel):
|
||||
|
||||
def test_the_2d_footer_shows_the_picture_and_hides_the_pipeline(make_window):
|
||||
"""One line under the canvas: what the picture is, not how it was made."""
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
window = make_window()
|
||||
window.show()
|
||||
@@ -262,7 +262,7 @@ def test_the_2d_footer_shows_the_picture_and_hides_the_pipeline(make_window):
|
||||
|
||||
def test_the_view_menu_opens_the_3d_settings(make_window):
|
||||
"""视图 ▸ 三维显示设置… is the way in that does not depend on the pane's header."""
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
window = make_window(sensor_type="finger", show_finger_3d=True)
|
||||
window.show()
|
||||
|
||||
@@ -19,7 +19,7 @@ import pytest
|
||||
@pytest.fixture
|
||||
def restore_surface_format():
|
||||
"""The default surface format is process-wide; put it back afterwards."""
|
||||
from PyQt5.QtGui import QSurfaceFormat
|
||||
from PySide6.QtGui import QSurfaceFormat
|
||||
|
||||
previous = QSurfaceFormat.defaultFormat()
|
||||
try:
|
||||
@@ -49,21 +49,21 @@ def test_the_skeleton_does_not_import_a_gl_binding(qt_app):
|
||||
|
||||
|
||||
def test_configure_surface_format_selects_a_complete_profile(qt_app, restore_surface_format):
|
||||
from PyQt5.QtGui import QSurfaceFormat
|
||||
from PySide6.QtGui import QSurfaceFormat
|
||||
|
||||
from orisys_studio.scene3d.gl_backend import configure_surface_format
|
||||
|
||||
configure_surface_format("desktop33")
|
||||
desktop = QSurfaceFormat.defaultFormat()
|
||||
assert desktop.renderableType() == QSurfaceFormat.OpenGL
|
||||
assert desktop.renderableType() == QSurfaceFormat.RenderableType.OpenGL
|
||||
assert (desktop.majorVersion(), desktop.minorVersion()) == (3, 3)
|
||||
assert desktop.profile() == QSurfaceFormat.CoreProfile
|
||||
assert desktop.profile() == QSurfaceFormat.OpenGLContextProfile.CoreProfile
|
||||
assert desktop.depthBufferSize() == 24
|
||||
assert desktop.samples() == 0, "v1 does not pay for multisampling"
|
||||
|
||||
configure_surface_format("gles30")
|
||||
embedded = QSurfaceFormat.defaultFormat()
|
||||
assert embedded.renderableType() == QSurfaceFormat.OpenGLES
|
||||
assert embedded.renderableType() == QSurfaceFormat.RenderableType.OpenGLES
|
||||
assert (embedded.majorVersion(), embedded.minorVersion()) == (3, 0)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
|
||||
@@ -29,8 +29,8 @@ import numpy as np
|
||||
import pytest
|
||||
|
||||
import ui_check
|
||||
from PyQt5.QtCore import pyqtSignal
|
||||
from PyQt5.QtWidgets import QWidget
|
||||
from PySide6.QtCore import Signal
|
||||
from PySide6.QtWidgets import QWidget
|
||||
from orisys_app_shared.i18n import t
|
||||
from orisys_app_shared.scene3d import synthetic
|
||||
from orisys_app_shared.scene3d.contracts import (
|
||||
@@ -276,8 +276,8 @@ class _RecordingView(QWidget):
|
||||
a real ``QWidget`` because the container puts it in its stack.
|
||||
"""
|
||||
|
||||
diagnosticRaised = pyqtSignal(object)
|
||||
enabledChanged = pyqtSignal(bool)
|
||||
diagnosticRaised = Signal(object)
|
||||
enabledChanged = Signal(bool)
|
||||
|
||||
def __init__(self, parent=None) -> None:
|
||||
super().__init__(parent)
|
||||
@@ -559,7 +559,9 @@ def test_a_package_is_loaded_off_the_gui_thread(qt_app, write_poc):
|
||||
loaded: list = []
|
||||
failed: list = []
|
||||
loader.loaded.connect(lambda assets, description: loaded.append((assets, description)))
|
||||
loader.failed.connect(failed.append)
|
||||
# Same arity caveat as below: this branch should never fire, and under PySide6 a
|
||||
# builtin slot would fail *silently* if it ever did.
|
||||
loader.failed.connect(lambda item, request_id: failed.append(item))
|
||||
try:
|
||||
loader.load(SceneSource(path=path, kind="package", directory=path.parent))
|
||||
assert _pump(qt_app, lambda: bool(loaded or failed)), "the loader never answered"
|
||||
@@ -679,7 +681,10 @@ def test_a_corrupt_package_is_reported_not_raised(qt_app, tmp_path):
|
||||
broken.write_bytes(b"not a zip archive at all")
|
||||
loader = SceneAssetLoader()
|
||||
failed: list = []
|
||||
loader.failed.connect(failed.append)
|
||||
# ``failed`` carries (diagnostic, request id). PySide6 adapts to a *Python* callable's
|
||||
# arity but not to a builtin's, so ``failed.append`` raises TypeError inside the emit,
|
||||
# Qt swallows it, and the failure signal looks like it was never sent.
|
||||
loader.failed.connect(lambda item, request_id: failed.append(item))
|
||||
try:
|
||||
loader.load(SceneSource(path=broken, kind="package", directory=tmp_path))
|
||||
assert _pump(qt_app, lambda: bool(failed)), "a corrupt package produced no answer"
|
||||
|
||||
@@ -559,7 +559,7 @@ def test_force_plots_render_demo_data(make_window):
|
||||
window = _show(make_window(), (1440, 880))
|
||||
for i in range(60):
|
||||
window.force_plots.push(i / 30.0, float(i), -1.0, 1.0, unit="N")
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
QApplication.processEvents()
|
||||
assert window.force_plots.history.sample_count > 0
|
||||
@@ -1009,7 +1009,7 @@ def test_top_area_stays_compact(make_window):
|
||||
assert window.toolbar.record_chip.text() == t("record.state_idle")
|
||||
|
||||
window.toolbar.btn_settings.setChecked(True)
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
QApplication.processEvents()
|
||||
assert window.toolbar.settings_expanded()
|
||||
@@ -1032,7 +1032,7 @@ def test_settings_area_stays_inside_its_container(make_window, size):
|
||||
The settings area is the one part of the top rail that may scroll: its own
|
||||
content is exempt, everything else must fit.
|
||||
"""
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
window = _show(make_window(sensor_type="finger", show_cal_panel=True), size)
|
||||
window._set_settings_expanded(True)
|
||||
@@ -1063,7 +1063,7 @@ def test_idle_run_is_the_only_filled_action(make_window):
|
||||
|
||||
def test_connecting_cancel_stays_filled(make_window):
|
||||
"""Cancel is the way out of a hung device open, so it keeps the weight."""
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
window = _show(make_window(), (1440, 880))
|
||||
stuck = ui_check._StuckWorker()
|
||||
@@ -1197,7 +1197,7 @@ def test_overview_region_is_the_timeline_control(make_window):
|
||||
|
||||
def test_the_charts_start_in_follow_mode(make_window):
|
||||
"""Every live entry point resumes following; only browsing is a gesture."""
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
from orisys_app_shared.i18n import t
|
||||
|
||||
@@ -1229,7 +1229,7 @@ def test_the_charts_start_in_follow_mode(make_window):
|
||||
|
||||
def test_the_charts_zoom_and_pan_in_every_view(make_window):
|
||||
"""Zoom/pan must be reachable from the live view, not only while browsing."""
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
window, stuck = _running_window(make_window)
|
||||
panel = window.force_plots
|
||||
@@ -1306,7 +1306,7 @@ def test_the_arrow_legend_follows_the_view_mode(make_window):
|
||||
|
||||
def test_the_sensor_summary_opens_the_dialog(make_window):
|
||||
"""The rail states the sensor; the pickers live in the dialog it opens."""
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
from orisys_app_shared.i18n import t
|
||||
|
||||
@@ -1344,7 +1344,7 @@ def test_the_sensor_summary_opens_the_dialog(make_window):
|
||||
|
||||
def test_the_source_picker_follows_the_view(make_window):
|
||||
"""Live offers a camera, the replay a recorded video — and no residue."""
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
window, stuck = _running_window(make_window)
|
||||
assert window.toolbar.camera_group.isVisible()
|
||||
@@ -1425,11 +1425,11 @@ def test_the_nav_items_switch_the_main_window_layout(make_window):
|
||||
|
||||
|
||||
def test_the_calibration_view_does_not_clip_its_controls(make_window):
|
||||
from PyQt5.QtWidgets import QHeaderView
|
||||
from PySide6.QtWidgets import QHeaderView
|
||||
|
||||
window = _show(make_window(sensor_type="box", show_cal_panel=True), (1440, 880))
|
||||
window.brand_bar.btn_calibration.click()
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
for _ in range(3):
|
||||
QApplication.processEvents()
|
||||
@@ -1437,7 +1437,7 @@ def test_the_calibration_view_does_not_clip_its_controls(make_window):
|
||||
cal = window.cal_panel
|
||||
assert cal.scroll.isVisible(), "the calibration panel cannot scroll"
|
||||
assert (
|
||||
cal.table.horizontalHeader().sectionResizeMode(0) == QHeaderView.ResizeToContents
|
||||
cal.table.horizontalHeader().sectionResizeMode(0) == QHeaderView.ResizeMode.ResizeToContents
|
||||
), "the diagnostic columns can still be elided"
|
||||
for button in (cal.btn_apply, cal.btn_export, cal.btn_toggle_validation):
|
||||
assert button.height() >= button.fontMetrics().height(), button.text()
|
||||
@@ -1468,7 +1468,7 @@ def test_the_replay_view_is_the_live_view_with_a_video_source(make_window):
|
||||
|
||||
window = _show(make_window(), (1440, 880))
|
||||
window.brand_bar.btn_live.click()
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
QApplication.processEvents()
|
||||
live_sizes = tuple(window.content_splitter.sizes())
|
||||
@@ -1496,7 +1496,7 @@ def test_the_replay_view_is_the_live_view_with_a_video_source(make_window):
|
||||
|
||||
def test_the_replay_view_loads_a_recording_and_scrubs_it(make_window, replay_recording):
|
||||
"""The replay view reads the recording: charts, 2D panel and transport."""
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
from orisys_app_shared.i18n import t
|
||||
|
||||
@@ -1602,7 +1602,7 @@ def test_the_unit_chip_switches_the_charts_and_the_toolbar(make_window):
|
||||
from orisys_app_shared.i18n import t
|
||||
from orisys_studio.ui.force_plots import unit_display
|
||||
|
||||
from PyQt5.QtWidgets import QPushButton
|
||||
from PySide6.QtWidgets import QPushButton
|
||||
|
||||
window = _show(make_window(), (1440, 880))
|
||||
plots = window.force_plots
|
||||
@@ -1734,7 +1734,7 @@ def test_a_replay_keeps_the_2d_pane_from_the_live_camera(
|
||||
While the replay view is up, its frames own the 2D panel; a live packet used
|
||||
to paint over the recording. Leaving the view hands the pane back.
|
||||
"""
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
window, stuck = _running_window(make_window)
|
||||
window.brand_bar.btn_playback.click()
|
||||
@@ -1786,7 +1786,7 @@ def test_the_readout_describes_the_point_the_cursor_is_on(
|
||||
A replay used to state the live acquisition in cards sitting next to a chart
|
||||
of 7.5e5 raw counts, which reads as "the replay never opened".
|
||||
"""
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
from orisys_app_shared.i18n import t
|
||||
|
||||
@@ -1820,7 +1820,7 @@ def test_a_raw_only_recording_is_never_shown_in_newton(make_window, replay_recor
|
||||
for the axes it calibrated. Asking for newton anyway drew three blank charts
|
||||
and a dash in every card, which looks exactly like "the replay did not load".
|
||||
"""
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
from orisys_app_shared.i18n import t
|
||||
|
||||
@@ -1847,7 +1847,7 @@ def test_the_replay_transport_plays_and_pauses(make_window, replay_recording):
|
||||
"""Play/pause like an editor: 1× forward, and a manual gesture takes over."""
|
||||
import time
|
||||
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
window = _show(make_window(), (1440, 880))
|
||||
window.brand_bar.btn_playback.click()
|
||||
@@ -1887,7 +1887,7 @@ def test_the_recompute_mode_reruns_the_pipeline_over_the_video(
|
||||
make_window, replay_recording
|
||||
):
|
||||
"""Off: the recorded CSV. On: the recorded video through the vision chain."""
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
window = _show(make_window(), (1440, 880))
|
||||
window.brand_bar.btn_playback.click()
|
||||
@@ -1929,7 +1929,7 @@ def test_the_replay_view_offers_a_stop_that_always_works(make_window, replay_rec
|
||||
wrong. So the rail's second button becomes an unconditional stop in this view, and
|
||||
it covers both things that can run there: plain playback and a recompute pass.
|
||||
"""
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
from orisys_app_shared.i18n import t
|
||||
|
||||
@@ -1989,7 +1989,7 @@ def test_the_replay_view_offers_a_stop_that_always_works(make_window, replay_rec
|
||||
|
||||
def test_escape_stops_the_replay_too(make_window, replay_recording):
|
||||
"""Esc is the documented way out; in the replay view it stops the replay."""
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
window = _show(make_window(), (1440, 880))
|
||||
window.brand_bar.btn_playback.click()
|
||||
@@ -2007,7 +2007,7 @@ def test_escape_stops_the_replay_too(make_window, replay_recording):
|
||||
|
||||
|
||||
def test_the_live_view_keeps_the_tracker_reset(make_window):
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
window = _show(make_window(), (1440, 880))
|
||||
window.brand_bar.btn_live.click()
|
||||
@@ -2028,7 +2028,7 @@ def test_a_bare_video_plays_even_though_it_is_not_a_recording(make_window, recor
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
from orisys_app_shared.i18n import t
|
||||
|
||||
@@ -2091,7 +2091,7 @@ def test_the_replay_and_the_live_paths_share_one_image_backend(make_window, reco
|
||||
"""
|
||||
import cv2
|
||||
import numpy as np
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
import orisys_studio.app_window as app_module
|
||||
from orisys_app_shared.acquisition.image_backend import (
|
||||
@@ -2186,7 +2186,7 @@ def test_the_replay_entries_exist_only_in_the_replay_view(make_window, record_ro
|
||||
"""
|
||||
import cv2
|
||||
import numpy as np
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
from orisys_app_shared.i18n import t
|
||||
|
||||
@@ -2239,7 +2239,7 @@ def test_a_file_that_is_not_a_video_at_all_is_refused_with_a_reason(
|
||||
make_window, record_root
|
||||
):
|
||||
"""Not a recording *and* not decodable: say what is missing and offer recompute."""
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
from orisys_app_shared.i18n import t
|
||||
|
||||
@@ -2272,7 +2272,7 @@ def test_a_file_that_is_not_a_video_at_all_is_refused_with_a_reason(
|
||||
def test_a_video_inside_a_recording_opens_the_recording_and_plays(
|
||||
make_window, replay_recording
|
||||
):
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
from orisys_app_shared.i18n import t
|
||||
|
||||
@@ -2300,7 +2300,7 @@ def test_a_recording_without_a_video_says_it_cannot_be_played(make_window, recor
|
||||
import csv
|
||||
import json
|
||||
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
from orisys_app_shared.i18n import t
|
||||
|
||||
@@ -2399,7 +2399,7 @@ def test_the_status_bar_is_a_notice_line_not_a_state_line(make_window):
|
||||
"""
|
||||
import time
|
||||
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
import orisys_studio.app_window as app_module
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ def check(condition: bool, message: str) -> None:
|
||||
|
||||
def _inside_scroll_area(widget) -> bool:
|
||||
"""True when ``widget`` lives inside a QScrollArea's content."""
|
||||
from PyQt5.QtWidgets import QScrollArea
|
||||
from PySide6.QtWidgets import QScrollArea
|
||||
|
||||
node = widget
|
||||
while node is not None:
|
||||
@@ -538,7 +538,7 @@ def audit_palette(verbose: bool = False) -> list[str]:
|
||||
|
||||
|
||||
def main() -> int:
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
from orisys_app_shared.force.calibration_io import CalSample
|
||||
from orisys_app_shared.i18n import set_locale, t
|
||||
@@ -1491,7 +1491,7 @@ def main() -> int:
|
||||
window.brand_bar.btn_calibration.click()
|
||||
for _ in range(3):
|
||||
app.processEvents()
|
||||
from PyQt5.QtWidgets import QHeaderView as _QHeaderView
|
||||
from PySide6.QtWidgets import QHeaderView as _QHeaderView
|
||||
|
||||
sizes = window.content_splitter.sizes()
|
||||
check(
|
||||
@@ -2000,7 +2000,7 @@ def main() -> int:
|
||||
"import os, sys\n"
|
||||
"os.environ.setdefault('QT_QPA_PLATFORM', 'offscreen')\n"
|
||||
"os.environ.setdefault('QT_QPA_FONTDIR', r'C:\\Windows\\Fonts')\n"
|
||||
"from PyQt5.QtCore import QTimer\n"
|
||||
"from PySide6.QtCore import QTimer\n"
|
||||
"import orisys_studio.main as studio_main\n"
|
||||
"show = studio_main.StudioMainWindow.show\n"
|
||||
"def show_and_close(self):\n"
|
||||
@@ -2259,7 +2259,7 @@ def main() -> int:
|
||||
|
||||
import orisys_studio.ui.cal_panel as _cal_panel_module
|
||||
import orisys_studio.calibration_service as _cal_service_module
|
||||
from PyQt5.QtWidgets import QMessageBox as _QMessageBox
|
||||
from PySide6.QtWidgets import QMessageBox as _QMessageBox
|
||||
|
||||
from orisys_app_shared.force.calibration_io import (
|
||||
build_force_calibration_v2 as _build_v2b,
|
||||
|
||||
@@ -391,7 +391,7 @@ def main() -> int:
|
||||
|
||||
panels = {part.strip().lower() for part in args.panels.split(",") if part.strip()}
|
||||
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
from orisys_app_shared.i18n import set_locale
|
||||
from orisys_app_shared.theme import build_qss
|
||||
|
||||
Reference in New Issue
Block a user