move controller connect to popup dialog
This commit is contained in:
@@ -7,7 +7,7 @@ controller is reachable.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtCore import Qt, Signal
|
||||
from PySide6.QtGui import QPixmap
|
||||
from PySide6.QtWidgets import QFrame, QHBoxLayout, QLabel, QWidget
|
||||
|
||||
@@ -27,6 +27,11 @@ _TONE_TOKENS = {
|
||||
class BrandBar(QFrame):
|
||||
"""The identity strip, with a state chip that reports the connection."""
|
||||
|
||||
#: The connection chip was clicked. It is the most prominent statement of the
|
||||
#: connection state, so it is also the obvious way to go and change it — the
|
||||
#: controls themselves live in the device-setup dialog.
|
||||
connectionChipClicked = Signal()
|
||||
|
||||
def __init__(self, theme: str = "dark", parent: QWidget | None = None) -> None:
|
||||
super().__init__(parent)
|
||||
self.setObjectName("brandBar")
|
||||
@@ -67,10 +72,21 @@ class BrandBar(QFrame):
|
||||
|
||||
self.state_chip = QLabel(self)
|
||||
self.state_chip.setObjectName("stateChip")
|
||||
self.state_chip.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
layout.addWidget(self.state_chip)
|
||||
|
||||
self.retranslate()
|
||||
|
||||
def mouseReleaseEvent(self, event) -> None: # noqa: N802 - Qt naming
|
||||
"""A click anywhere on the chip opens the device setup.
|
||||
|
||||
On the bar rather than on the label: a QLabel only receives a mouse event
|
||||
where its own text is, so clicking the chip's padding would do nothing.
|
||||
"""
|
||||
if event.button() == Qt.MouseButton.LeftButton and self.state_chip.underMouse():
|
||||
self.connectionChipClicked.emit()
|
||||
super().mouseReleaseEvent(event)
|
||||
|
||||
# ----------------------------------------------------------------- content
|
||||
|
||||
def retranslate(self) -> None:
|
||||
@@ -78,6 +94,7 @@ class BrandBar(QFrame):
|
||||
self.name.setText(t("brand.name"))
|
||||
self.detail.setText(t("brand.detail"))
|
||||
self.product.setText(t("app.title"))
|
||||
self.state_chip.setToolTip(t("setup.chip_hint", name=t("connection.title")))
|
||||
self.set_connected(self._connected)
|
||||
|
||||
def set_theme(self, theme: str) -> None:
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
"""The device setup: connection and bus initialisation, behind one button.
|
||||
|
||||
Both groups are touched once per machine start-up — reach the controller, download
|
||||
the bus program, watch the bus come up — and then not again for the rest of the
|
||||
session. In the left rail they were spending ~460 px of its height on that, above
|
||||
the axis readout that is read all day, which is what made the rail scroll.
|
||||
|
||||
They live in a **modeless dialog** rather than a modal one on purpose: connecting and
|
||||
downloading are asynchronous (the poller runs the state machine, the status strip
|
||||
reports it), and an operator watching a bus scan go wrong wants to see the strip and
|
||||
the axis readout while it happens. Modeless also means the dialog can be opened from
|
||||
several places — the strip's button, its state chips, the File menu — without any of
|
||||
them having to block.
|
||||
|
||||
The dialog owns the two panels; the window keeps a reference to each
|
||||
(``window.connection_panel`` / ``window.bus_panel``) because it is the window that
|
||||
wires them to the controller, and because the tools and checks reach for them there.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog,
|
||||
QDialogButtonBox,
|
||||
QLabel,
|
||||
QTabWidget,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from orisys_app_shared.i18n import t
|
||||
|
||||
from ..motion import ConnectionSpec
|
||||
from .bus_panel import BusPanel
|
||||
from .connection_panel import ConnectionPanel
|
||||
|
||||
#: Tab identity -> locale key, in the order they are shown. The order is the work:
|
||||
#: reach the machine, then bring its bus up.
|
||||
TABS: tuple[tuple[str, str], ...] = (
|
||||
("connection", "setup.tab_connection"),
|
||||
("bus", "setup.tab_bus"),
|
||||
)
|
||||
|
||||
|
||||
class DeviceSetupDialog(QDialog):
|
||||
"""Connection and bus initialisation, the once-per-start controls."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
spec: ConnectionSpec,
|
||||
bas_path: str = "",
|
||||
parent: QWidget | None = None,
|
||||
) -> None:
|
||||
super().__init__(parent)
|
||||
self.setObjectName("deviceSetup")
|
||||
# Modeless: see the module docstring. ``show()`` raises it when already open.
|
||||
self.setModal(False)
|
||||
self.setSizeGripEnabled(True)
|
||||
|
||||
column = QVBoxLayout(self)
|
||||
column.setContentsMargins(10, 10, 10, 10)
|
||||
column.setSpacing(8)
|
||||
|
||||
self.hint_label = QLabel(self)
|
||||
self.hint_label.setObjectName("hintLabel")
|
||||
self.hint_label.setWordWrap(True)
|
||||
column.addWidget(self.hint_label)
|
||||
|
||||
self.tabs = QTabWidget(self)
|
||||
self.tabs.setObjectName("setupTabs")
|
||||
self.connection_panel = ConnectionPanel(spec, self.tabs)
|
||||
self.bus_panel = BusPanel(bas_path, self.tabs)
|
||||
for key, locale_key in TABS:
|
||||
self.tabs.addTab(self.panel(key), "")
|
||||
column.addWidget(self.tabs, 1)
|
||||
|
||||
self.buttons = QDialogButtonBox(self)
|
||||
self.close_button = self.buttons.addButton(
|
||||
"", QDialogButtonBox.ButtonRole.RejectRole
|
||||
)
|
||||
self.close_button.clicked.connect(self.close)
|
||||
column.addWidget(self.buttons)
|
||||
|
||||
self.retranslate()
|
||||
# Sized to the taller tab (bus initialisation: the file row, four readouts and
|
||||
# two buttons), so the dialog opens without a band of empty panel under the
|
||||
# controls. It is resizable, and the size is remembered.
|
||||
self.resize(520, 470)
|
||||
|
||||
# ----------------------------------------------------------------- content
|
||||
|
||||
def panel(self, key: str) -> QWidget:
|
||||
"""One of the two panels, by tab key."""
|
||||
return self.connection_panel if key == "connection" else self.bus_panel
|
||||
|
||||
def open_for(self, tab: str | None = None) -> None:
|
||||
"""Show the dialog, raising it if it is already up, and pick a tab.
|
||||
|
||||
Raising rather than ignoring the second request: the button and the chips all
|
||||
lead here, and a click that appears to do nothing reads as a broken button.
|
||||
"""
|
||||
key = tab if tab in {name for name, _ in TABS} else None
|
||||
if key is not None:
|
||||
self.select_tab(key)
|
||||
self.show()
|
||||
self.raise_()
|
||||
self.activateWindow()
|
||||
panel = self.panel(key) if key is not None else None
|
||||
if panel is not None:
|
||||
panel.setFocus()
|
||||
|
||||
def select_tab(self, key: str) -> None:
|
||||
for index, (name, _locale_key) in enumerate(TABS):
|
||||
if name == key:
|
||||
self.tabs.setCurrentIndex(index)
|
||||
return
|
||||
|
||||
def current_tab(self) -> str:
|
||||
index = max(0, self.tabs.currentIndex())
|
||||
return TABS[index][0] if index < len(TABS) else TABS[0][0]
|
||||
|
||||
# ------------------------------------------------------------- persistence
|
||||
|
||||
def settings(self) -> dict:
|
||||
"""What is worth remembering between sessions: size and last tab.
|
||||
|
||||
Not the connection state: that belongs to the controller, and a dialog that
|
||||
claimed to remember being connected would be lying after a restart.
|
||||
"""
|
||||
size = self.size()
|
||||
return {"size": [int(size.width()), int(size.height())], "tab": self.current_tab()}
|
||||
|
||||
def apply_settings(self, stored: object) -> None:
|
||||
if not isinstance(stored, dict):
|
||||
return
|
||||
size = stored.get("size")
|
||||
if isinstance(size, list) and len(size) == 2:
|
||||
try:
|
||||
width, height = (int(value) for value in size)
|
||||
except (TypeError, ValueError):
|
||||
return
|
||||
if width > 0 and height > 0:
|
||||
self.resize(width, height)
|
||||
tab = stored.get("tab")
|
||||
if isinstance(tab, str):
|
||||
self.select_tab(tab)
|
||||
|
||||
# ------------------------------------------------------------- translations
|
||||
|
||||
def retranslate(self) -> None:
|
||||
self.setWindowTitle(t("setup.title"))
|
||||
self.hint_label.setText(t("setup.hint"))
|
||||
for index, (_key, locale_key) in enumerate(TABS):
|
||||
self.tabs.setTabText(index, t(locale_key))
|
||||
self.close_button.setText(t("setup.close"))
|
||||
self.connection_panel.retranslate()
|
||||
self.bus_panel.retranslate()
|
||||
@@ -20,7 +20,7 @@ from __future__ import annotations
|
||||
|
||||
import time
|
||||
|
||||
from PySide6.QtCore import Signal
|
||||
from PySide6.QtCore import Qt, Signal
|
||||
from PySide6.QtWidgets import (
|
||||
QFrame,
|
||||
QHBoxLayout,
|
||||
@@ -66,7 +66,17 @@ _FAILED_INTERVALS = 10.0
|
||||
|
||||
|
||||
class _Chip(QLabel):
|
||||
"""A small toned label: the strip is read at a glance, not studied."""
|
||||
"""A small toned label: the strip is read at a glance, not studied.
|
||||
|
||||
Clickable, because the state it shows is also the way to change it: the
|
||||
connection, bus and init controls live in the device-setup dialog, so a chip that
|
||||
reports "not connected" is the natural place to click. Its *text* stays clean
|
||||
(no decoration) so the strip still reads as a status row; the pointing-hand
|
||||
cursor and the tooltip carry the affordance, and the labelled button beside them
|
||||
is the primary way in.
|
||||
"""
|
||||
|
||||
clicked = Signal()
|
||||
|
||||
def __init__(self, theme: str, parent: QWidget | None = None) -> None:
|
||||
super().__init__(parent)
|
||||
@@ -75,6 +85,18 @@ class _Chip(QLabel):
|
||||
self._tone = "muted"
|
||||
self.set_tone("muted")
|
||||
|
||||
def mouseReleaseEvent(self, event) -> None: # noqa: N802 - Qt naming
|
||||
if event.button() == Qt.MouseButton.LeftButton and self.isEnabled():
|
||||
self.clicked.emit()
|
||||
super().mouseReleaseEvent(event)
|
||||
|
||||
def set_clickable(self, clickable: bool, name: str = "") -> None:
|
||||
"""Make the chip open something, or go back to being a plain readout."""
|
||||
self.setCursor(
|
||||
Qt.CursorShape.PointingHandCursor if clickable else Qt.CursorShape.ArrowCursor
|
||||
)
|
||||
self.setToolTip(t("setup.chip_hint", name=name) if clickable else "")
|
||||
|
||||
def set_theme(self, theme: str) -> None:
|
||||
self._theme = theme
|
||||
self.set_tone(self._tone)
|
||||
@@ -93,6 +115,9 @@ class StatusStrip(QFrame):
|
||||
|
||||
pauseRequested = Signal()
|
||||
stopAllRequested = Signal()
|
||||
#: Open the device setup, optionally on a named tab (``connection`` / ``bus``).
|
||||
setupRequested = Signal()
|
||||
chipSetupRequested = Signal(str)
|
||||
|
||||
def __init__(self, theme: str = "dark", parent: QWidget | None = None) -> None:
|
||||
super().__init__(parent)
|
||||
@@ -133,6 +158,24 @@ class StatusStrip(QFrame):
|
||||
self.action_label.setObjectName("stripAction")
|
||||
layout.addWidget(self.action_label)
|
||||
|
||||
# The three state chips double as the way into the device setup: each opens
|
||||
# the tab that explains it. Wired here rather than by the window so the pairing
|
||||
# lives with the chips it belongs to.
|
||||
for chip, tab in (
|
||||
(self.chip_connection, "connection"),
|
||||
(self.chip_bus, "bus"),
|
||||
(self.chip_init, "bus"),
|
||||
):
|
||||
chip.set_clickable(True)
|
||||
chip.clicked.connect(lambda tab=tab: self.chipSetupRequested.emit(tab))
|
||||
|
||||
# Before the run-screen buttons, because it is the one control here that is
|
||||
# about the machine rather than about the job.
|
||||
self.setup_button = QPushButton(self)
|
||||
self.setup_button.setProperty("rail", True)
|
||||
self.setup_button.clicked.connect(self.setupRequested)
|
||||
layout.addWidget(self.setup_button)
|
||||
|
||||
self.pause_button = QPushButton(self)
|
||||
self.pause_button.setProperty("rail", True)
|
||||
self.pause_button.setCheckable(True)
|
||||
@@ -268,8 +311,15 @@ class StatusStrip(QFrame):
|
||||
|
||||
def retranslate(self) -> None:
|
||||
self.title.setText(t("status.motion"))
|
||||
self.setup_button.setText(t("setup.open"))
|
||||
self._refresh_pause_text()
|
||||
self.stop_button.setText(t("axes.stop_all"))
|
||||
for chip, name in (
|
||||
(self.chip_connection, t("connection.title")),
|
||||
(self.chip_bus, t("bus.title")),
|
||||
(self.chip_init, t("bus.title")),
|
||||
):
|
||||
chip.set_clickable(True, name)
|
||||
self.set_snapshot(self._snapshot)
|
||||
self.refresh_health()
|
||||
|
||||
|
||||
@@ -56,8 +56,8 @@ from .axis_panel import AxisPanel
|
||||
from .axis_readout import AxisReadoutPanel
|
||||
from .brand_bar import BrandBar
|
||||
from .bus_data_panel import BusDataPanel
|
||||
from .bus_panel import BusPanel, default_bas_dir
|
||||
from .connection_panel import ConnectionPanel
|
||||
from .bus_panel import default_bas_dir
|
||||
from .device_setup import DeviceSetupDialog
|
||||
from .sensor_panel import FingerPanel
|
||||
from .sequence_panel import SequencePanel
|
||||
from .status_strip import RunBar, StatusStrip
|
||||
@@ -73,7 +73,11 @@ logger = logging.getLogger(__name__)
|
||||
#: gives the workspace that 240 px back at every window size, which is the
|
||||
#: difference between a workable sequence page and a cramped one at the minimum
|
||||
#: window width.
|
||||
MAIN_SPLITTER_SIZES = (330, 1560)
|
||||
#:
|
||||
#: Narrower than it was (330): the rail now holds the axis readout and the collapsed
|
||||
#: bus-data group only — connection and bus initialisation moved into the device
|
||||
#: setup dialog — so the workspace gets the difference.
|
||||
MAIN_SPLITTER_SIZES = (300, 1590)
|
||||
|
||||
|
||||
def _triple(value: object, length: int) -> tuple[float, ...]:
|
||||
@@ -138,6 +142,9 @@ class AutoCalMainWindow(QMainWindow):
|
||||
self._build_poller()
|
||||
|
||||
self._restore_geometry()
|
||||
# Its size and last tab, from the previous session. Applied here rather than
|
||||
# in ``_build_ui`` because it is stored state, like the window geometry.
|
||||
self.device_setup.apply_settings(self.prefs.get("device_setup"))
|
||||
self.retranslate()
|
||||
|
||||
# Load the remembered bundle, so the finger debugger comes up working.
|
||||
@@ -174,31 +181,39 @@ class AutoCalMainWindow(QMainWindow):
|
||||
layout.addWidget(body, stretch=1)
|
||||
|
||||
# ----------------------------------------------------------------- rail
|
||||
# One rail, not two. It holds setup, the axis readout and the collapsed
|
||||
# bus data, and it scrolls: at 900 px high those three groups do not fit,
|
||||
# and a rail that clips its own controls is worse than one that scrolls.
|
||||
# One rail, not two, and it holds only what is read while working: the axis
|
||||
# readout and the collapsed bus-data monitor. Connection and bus
|
||||
# initialisation moved into the device-setup dialog — they are touched once
|
||||
# per machine start-up and were pushing the readout below the fold. The rail
|
||||
# still scrolls, because a rail that clips its own controls is worse than one
|
||||
# that scrolls.
|
||||
self.rail_content = QWidget()
|
||||
left = QVBoxLayout(self.rail_content)
|
||||
left.setContentsMargins(0, 0, 0, 0)
|
||||
left.setSpacing(8)
|
||||
|
||||
connection = self.prefs.get("connection") or {}
|
||||
self.connection_panel = ConnectionPanel(
|
||||
ConnectionSpec(
|
||||
mode=str(connection.get("mode") or "eth"),
|
||||
ip=str(connection.get("ip") or ""),
|
||||
com_id=int(connection.get("com_id") or 0),
|
||||
card_num=int(connection.get("card_num") or 0),
|
||||
local_string=str(connection.get("local_string") or "0"),
|
||||
),
|
||||
self.rail_content,
|
||||
self.spec = ConnectionSpec(
|
||||
mode=str(connection.get("mode") or "eth"),
|
||||
ip=str(connection.get("ip") or ""),
|
||||
com_id=int(connection.get("com_id") or 0),
|
||||
card_num=int(connection.get("card_num") or 0),
|
||||
local_string=str(connection.get("local_string") or "0"),
|
||||
)
|
||||
self.bus_panel = BusPanel(str(self.prefs.get("bas_path") or ""), self.rail_content)
|
||||
# Connection and bus initialisation live in the device-setup dialog: they are
|
||||
# touched once per machine start-up, and in the rail they were pushing the
|
||||
# axis readout below the fold. The dialog is built here and shown on request,
|
||||
# so the two panels always exist — the window wires them to the controller,
|
||||
# and the checks and tools reach for them here.
|
||||
self.device_setup = DeviceSetupDialog(
|
||||
self.spec, str(self.prefs.get("bas_path") or ""), self
|
||||
)
|
||||
self.connection_panel = self.device_setup.connection_panel
|
||||
self.bus_panel = self.device_setup.bus_panel
|
||||
|
||||
self.axis_readout = AxisReadoutPanel(
|
||||
MotionService().axis_count, self._theme, self.rail_content
|
||||
)
|
||||
left.addWidget(self.connection_panel)
|
||||
left.addWidget(self.bus_panel)
|
||||
left.addWidget(self.axis_readout)
|
||||
|
||||
# Bus data (SDO/RTEX) is commissioning-only, so it is collapsed by
|
||||
@@ -228,7 +243,7 @@ class AutoCalMainWindow(QMainWindow):
|
||||
self.left_rail.setWidgetResizable(True)
|
||||
self.left_rail.setFrameShape(QScrollArea.Shape.NoFrame)
|
||||
self.left_rail.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
||||
self.left_rail.setMinimumWidth(300)
|
||||
self.left_rail.setMinimumWidth(280)
|
||||
|
||||
# ------------------------------------------------------------ workspace
|
||||
params = {
|
||||
@@ -303,6 +318,13 @@ class AutoCalMainWindow(QMainWindow):
|
||||
self.bus_data_panel.rtexWriteRequested.connect(self._on_rtex_write)
|
||||
self.status_strip.stopAllRequested.connect(self._on_stop_all)
|
||||
self.status_strip.pauseRequested.connect(self._on_pause_polling)
|
||||
# The three ways into the device setup: the strip's button, the state chips
|
||||
# (each opening the tab that explains it), and the brand bar's connection
|
||||
# chip. All of them land on one method, so there is one definition of "show
|
||||
# the setup".
|
||||
self.status_strip.setupRequested.connect(self._show_device_setup)
|
||||
self.status_strip.chipSetupRequested.connect(self._show_device_setup)
|
||||
self.brand_bar.connectionChipClicked.connect(self._show_device_setup_connection)
|
||||
# The readout rail's own batch buttons, connected once here. They were
|
||||
# connected from inside the finger screen's stack-changed handler, which
|
||||
# meant every edit on that screen added another copy.
|
||||
@@ -318,6 +340,16 @@ class AutoCalMainWindow(QMainWindow):
|
||||
self.finger_panel.pickRequested.connect(self._on_pick_point)
|
||||
self.finger_panel.stackChanged.connect(self._on_finger_stack_changed)
|
||||
|
||||
# ----------------------------------------------------------- device setup
|
||||
|
||||
def _show_device_setup(self, tab: str | None = None) -> None:
|
||||
"""Open the connection/bus dialog, optionally on one of its tabs."""
|
||||
self.device_setup.open_for(tab)
|
||||
|
||||
def _show_device_setup_connection(self) -> None:
|
||||
"""The brand bar's chip sends no argument; open on the connection tab."""
|
||||
self.device_setup.open_for("connection")
|
||||
|
||||
# ---------------------------------------------------------- finger debug
|
||||
|
||||
def load_bundle(self, path: str, *, flat_size: tuple[int, int] = DEFAULT_FLAT_SIZE):
|
||||
@@ -621,6 +653,8 @@ class AutoCalMainWindow(QMainWindow):
|
||||
menu_bar = self.menuBar()
|
||||
|
||||
self.menu_file = menu_bar.addMenu("")
|
||||
self.act_device_setup = QAction("", self)
|
||||
self.act_device_setup.triggered.connect(self._show_device_setup_connection)
|
||||
self.act_download_bas = QAction("", self)
|
||||
self.act_download_bas.triggered.connect(self._on_download_bas)
|
||||
self.act_reveal_config = QAction("", self)
|
||||
@@ -629,6 +663,8 @@ class AutoCalMainWindow(QMainWindow):
|
||||
self.act_reveal_outputs.triggered.connect(self._on_reveal_outputs)
|
||||
self.act_quit = QAction("", self)
|
||||
self.act_quit.triggered.connect(self.close)
|
||||
self.menu_file.addAction(self.act_device_setup)
|
||||
self.menu_file.addSeparator()
|
||||
for action in (
|
||||
self.act_download_bas,
|
||||
self.act_reveal_config,
|
||||
@@ -1039,8 +1075,9 @@ class AutoCalMainWindow(QMainWindow):
|
||||
self.brand_bar.retranslate()
|
||||
self.status_strip.retranslate()
|
||||
self.run_bar.retranslate()
|
||||
self.connection_panel.retranslate()
|
||||
self.bus_panel.retranslate()
|
||||
# One call into the dialog, which retranslates its own title, tabs and both
|
||||
# panels: they are no longer part of the window's rail.
|
||||
self.device_setup.retranslate()
|
||||
self.bus_data_group.setTitle(t("busdata.group"))
|
||||
self.bus_data_panel.retranslate()
|
||||
self.axis_panel.retranslate()
|
||||
@@ -1050,6 +1087,7 @@ class AutoCalMainWindow(QMainWindow):
|
||||
self.workspace.retranslate()
|
||||
|
||||
self.menu_file.setTitle(t("menu.file"))
|
||||
self.act_device_setup.setText(t("menu.device_setup"))
|
||||
self.act_download_bas.setText(t("menu.open_bas"))
|
||||
self.act_reveal_config.setText(t("menu.reveal_config"))
|
||||
self.act_reveal_outputs.setText(t("menu.outputs"))
|
||||
@@ -1113,6 +1151,10 @@ class AutoCalMainWindow(QMainWindow):
|
||||
]
|
||||
self.prefs["main_splitter_sizes"] = [int(v) for v in self.main_splitter.sizes()]
|
||||
self.prefs["bas_path"] = self.bus_panel.bas_path
|
||||
# The setup dialog's size and last tab, but not its content: the connection
|
||||
# state belongs to the controller, and remembering "connected" across a
|
||||
# restart would be a claim the app cannot keep.
|
||||
self.prefs["device_setup"] = self.device_setup.settings()
|
||||
self.prefs["units_per_mm"] = self.axis_panel._units_per_mm
|
||||
self.prefs["axis_params"] = {
|
||||
str(axis): _params_to_dict(params)
|
||||
@@ -1136,7 +1178,10 @@ class AutoCalMainWindow(QMainWindow):
|
||||
self.poller.shutdown()
|
||||
except Exception: # noqa: BLE001 - closing must always finish
|
||||
logger.exception("Shutdown reported a problem")
|
||||
# The dialog is a separate top-level window: closing the main window must not
|
||||
# leave it behind, and its settings have to be read before it is destroyed.
|
||||
self._persist_prefs()
|
||||
self.device_setup.close()
|
||||
event.accept()
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user