pyobs-gui 2.3.0__tar.gz → 2.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/PKG-INFO +2 -2
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/structuredconfigwidget.py +113 -9
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyproject.toml +2 -2
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/specs/2026-08-28-gui-main-vs-sidebar-widgets.md +1 -2
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/specs/2026-09-01-gui-video-widget-split.md +1 -1
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/specs/index.md +16 -12
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/tests/test_structuredconfigwidget.py +126 -1
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/uv.lock +5 -5
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/.claude/skills/verify/SKILL.md +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/.github/dependabot.yml +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/.github/workflows/dependabot-automerge.yml +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/.github/workflows/pypi.yml +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/.github/workflows/pyrefly.yml +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/.github/workflows/ruff.yml +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/.gitignore +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/.pre-commit-config.yaml +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/.readthedocs.yml +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/CHANGELOG.rst +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/LICENSE +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/README.md +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/bin/mypyuic5 +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/docs/Makefile +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/docs/make.bat +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/docs/requirements.txt +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/docs/source/_static/pyobs.gif +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/docs/source/conf.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/docs/source/index.rst +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/mypy.ini +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/__init__.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/__main__.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/_nuitka_astropy_patch.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/accounts.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/acquisitionwidget.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/autofocuswidget.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/autoguidingwidget.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/base.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/camerawidget.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/commandinputwidget.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/compassmovewidget.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/coolingwidget.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/datadisplaywidget.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/eventswidget.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/filterwidget.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/fitsheaderswidget.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/focuswidget.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/gui.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/login.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/loginwindow.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/logmodel.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/mainwindow.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/modewidget.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/modifiedmixin.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/modulegui.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/__init__.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/acquisitionwidget.ui +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/acquisitionwidget_ui.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/autofocuswidget.ui +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/autofocuswidget_ui.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/autoguidingwidget.ui +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/autoguidingwidget_ui.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/camerawidget.ui +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/camerawidget_ui.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/compassmoveplugin.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/compassmovewidget.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/compassmovewidget.ui +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/compassmovewidget_ui.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/compile.sh +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/coolingwidget.ui +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/coolingwidget_ui.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/datadisplayplugin.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/datadisplaywidget.ui +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/datadisplaywidget_ui.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/designer.sh +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/eventswidget.ui +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/eventswidget_ui.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/filterwidget.ui +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/filterwidget_ui.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/fitsheaderswidget.ui +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/fitsheaderswidget_ui.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/focuswidget.ui +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/focuswidget_ui.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/mainwindow.ui +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/mainwindow_ui.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/modewidget.ui +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/modewidget_ui.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/resources/arrow-alt-circle-down-solid.svg +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/resources/arrow-alt-circle-left-solid.svg +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/resources/arrow-alt-circle-right-solid.svg +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/resources/arrow-alt-circle-up-solid.svg +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/resources/arrow-up-right-from-square-solid.svg +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/resources/arrows-to-eye-solid.svg +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/resources/chart-line-solid.svg +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/resources/circle-question-solid.svg +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/resources/cloud-sun-solid.svg +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/resources/edit-solid.svg +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/resources/search-solid.svg +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/resources/undo-solid.svg +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/resources.qrc +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/resources_rc.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/roboticwidget.ui +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/roboticwidget_ui.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/roofwidget.ui +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/roofwidget_ui.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/schedulewidget.ui +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/schedulewidget_ui.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/shellwidget.ui +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/shellwidget_ui.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/spectrographwidget.ui +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/spectrographwidget_ui.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/telescopewidget.ui +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/telescopewidget_ui.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/temperaturesplotwidget.ui +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/temperaturesplotwidget_ui.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/temperatureswidget.ui +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/temperatureswidget_ui.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/videograbwidget.ui +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/videograbwidget_ui.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/videowidget.ui +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/videowidget_ui.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/weatherwidget.ui +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/weatherwidget_ui.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/roboticwidget.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/roofwidget.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/schedulewidget.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/shellwidget.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/spectrographwidget.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/statuswidget.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/telescopewidget.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/temperaturesplotwidget.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/temperatureswidget.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/utils.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/videograbwidget.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/videowidget.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/watchedlabel.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/weatherwidget.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pysidedeploy.spec +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/specs/2026-08-20-gui-remote-call-error-handling.md +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/specs/2026-08-21-gui-widget-startup-responsiveness.md +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/specs/2026-08-28-structuredconfig-widget.md +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/specs/2026-08-31-irobotic-widgets.md +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/spike_standalone/main.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/spike_standalone/pysidedeploy.spec +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/test/acquisition.yaml +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/test/autofocus.yaml +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/test/camera.yaml +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/test/full.yaml +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/test/guiding.yaml +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/test/mode.yaml +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/test/robotic.yaml +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/test/roof.yaml +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/test/spectrograph.yaml +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/test/structuredconfig.yaml +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/test/telescope.yaml +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/test/telescope_acl.yaml +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/test/telescope_acl_denied.yaml +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/test/video.yaml +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/tests/__init__.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/tests/conftest.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/tests/test_accounts.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/tests/test_camerawidget.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/tests/test_loginwindow.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/tests/test_mainwindow_startup.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/tests/test_modewidget.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/tests/test_multiwidget_pages.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/tests/test_roboticwidget.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/tests/test_schedulewidget.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/tests/test_videograbwidget.py +0 -0
- {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/tests/test_videowidget.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: pyobs-gui
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.0
|
|
4
4
|
Summary: A remote GUI for pyobs
|
|
5
5
|
Author-email: Tim-Oliver Husser <thusser@uni-goettingen.de>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -11,7 +11,7 @@ Requires-Dist: astropy<8,>=7.0.1
|
|
|
11
11
|
Requires-Dist: colour<0.2,>=0.1.5
|
|
12
12
|
Requires-Dist: keyring<26,>=25.7.0
|
|
13
13
|
Requires-Dist: matplotlib<4,>=3.10.1
|
|
14
|
-
Requires-Dist: pyobs-core<3,>=2.
|
|
14
|
+
Requires-Dist: pyobs-core<3,>=2.7.1
|
|
15
15
|
Requires-Dist: pyside6>=6.10.1
|
|
16
16
|
Requires-Dist: qasync<0.29,>=0.27.1
|
|
17
17
|
Requires-Dist: qfitswidget>=0.13.1
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import dataclasses
|
|
2
2
|
import logging
|
|
3
|
-
from typing import Any, Callable, Dict, Optional, cast
|
|
3
|
+
from typing import Any, Callable, Dict, List, Optional, cast
|
|
4
4
|
|
|
5
5
|
from PySide6 import QtCore, QtWidgets # type: ignore
|
|
6
6
|
|
|
7
7
|
from pyobs.interfaces import ConfigAppliedState, ConfigFieldSchema, ConfigSchema, IStructuredConfig
|
|
8
8
|
from pyobs.interfaces.IStructuredConfig import ConfigValue
|
|
9
|
+
from pyobs.utils.enums import AccessLevel
|
|
9
10
|
from .base import BaseWidget
|
|
10
11
|
|
|
11
12
|
log = logging.getLogger(__name__)
|
|
@@ -27,6 +28,49 @@ class _FieldEditor:
|
|
|
27
28
|
get_value: Callable[[], ConfigValue]
|
|
28
29
|
set_value: Callable[[ConfigValue], None]
|
|
29
30
|
children: Optional[Dict[str, "_FieldEditor"]] = None
|
|
31
|
+
level: AccessLevel = AccessLevel.BASIC
|
|
32
|
+
# the row's label, so the basic/expert toggle can hide it alongside `display_widget` --
|
|
33
|
+
# addRow(str, widget) auto-creates one, but we build it explicitly at each row-adding call
|
|
34
|
+
# site instead so we have a handle on it (QFormLayout.labelForField exists but is keyed
|
|
35
|
+
# per-layout-instance, awkward to thread through recursion; a direct reference is simpler)
|
|
36
|
+
row_label: Optional[QtWidgets.QLabel] = None
|
|
37
|
+
# set only when the field has a description: a small container with `widget` on top and the
|
|
38
|
+
# description label below it. `widget` itself (get_value/set_value's target) never changes
|
|
39
|
+
# shape, so a description doesn't affect how values round-trip -- only what goes in the row.
|
|
40
|
+
row_widget: Optional[QtWidgets.QWidget] = None
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def display_widget(self) -> QtWidgets.QWidget:
|
|
44
|
+
return self.row_widget if self.row_widget is not None else self.widget
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _collect_by_level(editor: "_FieldEditor", level: AccessLevel) -> List["_FieldEditor"]:
|
|
48
|
+
"""Recursively gather every editor (including nested-object children) tagged at exactly the
|
|
49
|
+
given level, for the basic/expert toggle to show/hide as a group."""
|
|
50
|
+
editors = [editor] if editor.level == level else []
|
|
51
|
+
for child in (editor.children or {}).values():
|
|
52
|
+
editors.extend(_collect_by_level(child, level))
|
|
53
|
+
return editors
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _wrap_with_description(widget: QtWidgets.QWidget, description: Optional[str]) -> Optional[QtWidgets.QWidget]:
|
|
57
|
+
"""Stack `widget` over a dimmed, word-wrapped description label, for the field's row to show
|
|
58
|
+
the description below the input rather than only as e.g. a tooltip. Returns None (meaning:
|
|
59
|
+
just use `widget` directly, no wrapping) when there's no description."""
|
|
60
|
+
if not description:
|
|
61
|
+
return None
|
|
62
|
+
container = QtWidgets.QWidget()
|
|
63
|
+
layout = QtWidgets.QVBoxLayout(container)
|
|
64
|
+
layout.setContentsMargins(0, 0, 0, 0)
|
|
65
|
+
layout.setSpacing(2)
|
|
66
|
+
layout.addWidget(widget)
|
|
67
|
+
description_label = QtWidgets.QLabel(description)
|
|
68
|
+
description_label.setWordWrap(True)
|
|
69
|
+
# palette(mid) is a 3D-effect shade (borders/shadows), too dark to read comfortably as text;
|
|
70
|
+
# placeholder-text is Qt's actual semantic role for secondary-but-legible text
|
|
71
|
+
description_label.setStyleSheet("color: palette(placeholder-text);")
|
|
72
|
+
layout.addWidget(description_label)
|
|
73
|
+
return container
|
|
30
74
|
|
|
31
75
|
|
|
32
76
|
def _nested_get(children: Dict[str, _FieldEditor]) -> Dict[str, ConfigValue]:
|
|
@@ -49,7 +93,13 @@ def _build_editor(name: str, schema: ConfigFieldSchema, on_change: Callable[[],
|
|
|
49
93
|
if schema.default is not None:
|
|
50
94
|
line_edit.setText(str(schema.default))
|
|
51
95
|
line_edit.textChanged.connect(lambda _: on_change())
|
|
52
|
-
return _FieldEditor(
|
|
96
|
+
return _FieldEditor(
|
|
97
|
+
line_edit,
|
|
98
|
+
line_edit.text,
|
|
99
|
+
lambda value: line_edit.setText(str(value)),
|
|
100
|
+
level=schema.level,
|
|
101
|
+
row_widget=_wrap_with_description(line_edit, schema.description),
|
|
102
|
+
)
|
|
53
103
|
|
|
54
104
|
if schema.type == "int":
|
|
55
105
|
spin_box = QtWidgets.QSpinBox()
|
|
@@ -57,7 +107,13 @@ def _build_editor(name: str, schema: ConfigFieldSchema, on_change: Callable[[],
|
|
|
57
107
|
if schema.default is not None:
|
|
58
108
|
spin_box.setValue(int(schema.default))
|
|
59
109
|
spin_box.valueChanged.connect(lambda _: on_change())
|
|
60
|
-
return _FieldEditor(
|
|
110
|
+
return _FieldEditor(
|
|
111
|
+
spin_box,
|
|
112
|
+
spin_box.value,
|
|
113
|
+
lambda value: spin_box.setValue(int(cast("int", value))),
|
|
114
|
+
level=schema.level,
|
|
115
|
+
row_widget=_wrap_with_description(spin_box, schema.description),
|
|
116
|
+
)
|
|
61
117
|
|
|
62
118
|
if schema.type == "float":
|
|
63
119
|
double_spin_box = QtWidgets.QDoubleSpinBox()
|
|
@@ -69,7 +125,11 @@ def _build_editor(name: str, schema: ConfigFieldSchema, on_change: Callable[[],
|
|
|
69
125
|
double_spin_box.setValue(float(schema.default))
|
|
70
126
|
double_spin_box.valueChanged.connect(lambda _: on_change())
|
|
71
127
|
return _FieldEditor(
|
|
72
|
-
double_spin_box,
|
|
128
|
+
double_spin_box,
|
|
129
|
+
double_spin_box.value,
|
|
130
|
+
lambda value: double_spin_box.setValue(float(cast("float", value))),
|
|
131
|
+
level=schema.level,
|
|
132
|
+
row_widget=_wrap_with_description(double_spin_box, schema.description),
|
|
73
133
|
)
|
|
74
134
|
|
|
75
135
|
if schema.type == "bool":
|
|
@@ -77,7 +137,13 @@ def _build_editor(name: str, schema: ConfigFieldSchema, on_change: Callable[[],
|
|
|
77
137
|
if schema.default is not None:
|
|
78
138
|
check_box.setChecked(bool(schema.default))
|
|
79
139
|
check_box.toggled.connect(lambda _: on_change())
|
|
80
|
-
return _FieldEditor(
|
|
140
|
+
return _FieldEditor(
|
|
141
|
+
check_box,
|
|
142
|
+
check_box.isChecked,
|
|
143
|
+
lambda value: check_box.setChecked(bool(value)),
|
|
144
|
+
level=schema.level,
|
|
145
|
+
row_widget=_wrap_with_description(check_box, schema.description),
|
|
146
|
+
)
|
|
81
147
|
|
|
82
148
|
if schema.type == "enum":
|
|
83
149
|
combo_box = QtWidgets.QComboBox()
|
|
@@ -86,7 +152,13 @@ def _build_editor(name: str, schema: ConfigFieldSchema, on_change: Callable[[],
|
|
|
86
152
|
if schema.default is not None and str(schema.default) in options:
|
|
87
153
|
combo_box.setCurrentText(str(schema.default))
|
|
88
154
|
combo_box.currentTextChanged.connect(lambda _: on_change())
|
|
89
|
-
return _FieldEditor(
|
|
155
|
+
return _FieldEditor(
|
|
156
|
+
combo_box,
|
|
157
|
+
combo_box.currentText,
|
|
158
|
+
lambda value: combo_box.setCurrentText(str(value)),
|
|
159
|
+
level=schema.level,
|
|
160
|
+
row_widget=_wrap_with_description(combo_box, schema.description),
|
|
161
|
+
)
|
|
90
162
|
|
|
91
163
|
if schema.type == "object" and schema.nested is not None:
|
|
92
164
|
group_box = QtWidgets.QGroupBox(name)
|
|
@@ -94,14 +166,20 @@ def _build_editor(name: str, schema: ConfigFieldSchema, on_change: Callable[[],
|
|
|
94
166
|
group_box.setLayout(form_layout)
|
|
95
167
|
children: Dict[str, _FieldEditor] = {}
|
|
96
168
|
for child_name, child_schema in schema.nested.items():
|
|
169
|
+
if child_schema.level == AccessLevel.HIDDEN:
|
|
170
|
+
continue
|
|
97
171
|
child_editor = _build_editor(child_name, child_schema, on_change)
|
|
98
|
-
|
|
172
|
+
child_label = QtWidgets.QLabel(child_name)
|
|
173
|
+
form_layout.addRow(child_label, child_editor.display_widget)
|
|
174
|
+
child_editor.row_label = child_label
|
|
99
175
|
children[child_name] = child_editor
|
|
100
176
|
return _FieldEditor(
|
|
101
177
|
group_box,
|
|
102
178
|
lambda: _nested_get(children),
|
|
103
179
|
lambda value: _nested_set(children, value),
|
|
104
180
|
children=children,
|
|
181
|
+
level=schema.level,
|
|
182
|
+
row_widget=_wrap_with_description(group_box, schema.description),
|
|
105
183
|
)
|
|
106
184
|
|
|
107
185
|
# object without a nested schema (pydantic freeform dict), or any other schema type this
|
|
@@ -120,7 +198,11 @@ def _build_editor(name: str, schema: ConfigFieldSchema, on_change: Callable[[],
|
|
|
120
198
|
placeholder.setReadOnly(True)
|
|
121
199
|
placeholder.setEnabled(False)
|
|
122
200
|
placeholder.setToolTip(f"Field type {schema.type!r} has no schema-driven editor for this widget.")
|
|
123
|
-
|
|
201
|
+
# always BASIC regardless of schema.level: this is a loud "unsupported type" flag, meant to
|
|
202
|
+
# stay visible so a developer notices it, never swept into the expert-toggle's hide group
|
|
203
|
+
return _FieldEditor(
|
|
204
|
+
placeholder, _get_raw, _set_raw, row_widget=_wrap_with_description(placeholder, schema.description)
|
|
205
|
+
)
|
|
124
206
|
|
|
125
207
|
|
|
126
208
|
class StructuredConfigWidget(BaseWidget):
|
|
@@ -142,10 +224,20 @@ class StructuredConfigWidget(BaseWidget):
|
|
|
142
224
|
# itself register as a user edit (which would otherwise re-enable Apply/Reset for a
|
|
143
225
|
# change that came from the module, not from the operator)
|
|
144
226
|
self._applying_state = False
|
|
227
|
+
# every EXPERT-level editor across the whole (possibly nested) form, collected once in
|
|
228
|
+
# _build_form; their rows (label + widget) are shown/hidden together by checkBoxAdvanced.
|
|
229
|
+
# No password gate, unlike pyftscontrol's Expert Mode button -- operators here are
|
|
230
|
+
# already authenticated via the comm layer, so it's just a visibility toggle, not access
|
|
231
|
+
# control.
|
|
232
|
+
self._expert_editors: List[_FieldEditor] = []
|
|
145
233
|
|
|
146
234
|
outer_layout = QtWidgets.QVBoxLayout()
|
|
147
235
|
self.setLayout(outer_layout)
|
|
148
236
|
|
|
237
|
+
self.checkBoxAdvanced = QtWidgets.QCheckBox("Show advanced fields")
|
|
238
|
+
self.checkBoxAdvanced.toggled.connect(self._on_advanced_toggled)
|
|
239
|
+
outer_layout.addWidget(self.checkBoxAdvanced)
|
|
240
|
+
|
|
149
241
|
scroll_area = QtWidgets.QScrollArea()
|
|
150
242
|
scroll_area.setWidgetResizable(True)
|
|
151
243
|
self._form_container = QtWidgets.QWidget()
|
|
@@ -186,8 +278,12 @@ class StructuredConfigWidget(BaseWidget):
|
|
|
186
278
|
def _build_form(self, schema: ConfigSchema) -> None:
|
|
187
279
|
children: Dict[str, _FieldEditor] = {}
|
|
188
280
|
for name, field_schema in schema.fields.items():
|
|
281
|
+
if field_schema.level == AccessLevel.HIDDEN:
|
|
282
|
+
continue
|
|
189
283
|
editor = _build_editor(name, field_schema, self._on_editor_changed)
|
|
190
|
-
|
|
284
|
+
label = QtWidgets.QLabel(name)
|
|
285
|
+
self._form_layout.addRow(label, editor.display_widget)
|
|
286
|
+
editor.row_label = label
|
|
191
287
|
children[name] = editor
|
|
192
288
|
self._root = _FieldEditor(
|
|
193
289
|
self._form_container,
|
|
@@ -195,11 +291,19 @@ class StructuredConfigWidget(BaseWidget):
|
|
|
195
291
|
lambda value: _nested_set(children, value),
|
|
196
292
|
children=children,
|
|
197
293
|
)
|
|
294
|
+
self._expert_editors = _collect_by_level(self._root, AccessLevel.EXPERT)
|
|
295
|
+
self._on_advanced_toggled(self.checkBoxAdvanced.isChecked())
|
|
198
296
|
# a state may already have arrived (and been cached) before the form existed to receive
|
|
199
297
|
# it -- apply it now instead of waiting for the next update
|
|
200
298
|
if self._last_applied is not None:
|
|
201
299
|
self.signal_update_gui.emit()
|
|
202
300
|
|
|
301
|
+
def _on_advanced_toggled(self, checked: bool) -> None:
|
|
302
|
+
for editor in self._expert_editors:
|
|
303
|
+
editor.display_widget.setVisible(checked)
|
|
304
|
+
if editor.row_label is not None:
|
|
305
|
+
editor.row_label.setVisible(checked)
|
|
306
|
+
|
|
203
307
|
def _on_state(self, state: ConfigAppliedState) -> None:
|
|
204
308
|
self._last_applied = dict(state.config)
|
|
205
309
|
self.signal_update_gui.emit()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "pyobs-gui"
|
|
3
|
-
version = "2.
|
|
3
|
+
version = "2.4.0"
|
|
4
4
|
description = "A remote GUI for pyobs"
|
|
5
5
|
authors = [{ name = "Tim-Oliver Husser", email = "thusser@uni-goettingen.de" }]
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -12,7 +12,7 @@ dependencies = [
|
|
|
12
12
|
"sunpy[all]>=7.0.1",
|
|
13
13
|
"QtAwesome>=1.4.0,<2",
|
|
14
14
|
"qfitswidget>=0.13.1",
|
|
15
|
-
"pyobs-core>=2.
|
|
15
|
+
"pyobs-core>=2.7.1,<3",
|
|
16
16
|
"pyside6>=6.10.1",
|
|
17
17
|
"astroplan>=0.10.1",
|
|
18
18
|
"astropy>=7.0.1,<8",
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# Plan: pyobs-gui — main widgets vs. sidebar widgets, automatic tab pages for multi-widget modules
|
|
2
2
|
|
|
3
|
-
Status: implemented,
|
|
4
|
-
reaches a release on `main`, per repo convention.
|
|
3
|
+
Status: implemented, closed (PR #157, `b7a14a6`; released `v2.3.0`, `main`). Issue #150 closed.
|
|
5
4
|
Audited: 2026-08-28
|
|
6
5
|
Revised: 2026-09-01 (D1, D2 — see "Revision 2026-09-01" below; adds D6)
|
|
7
6
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# `VideoWidget` split into two independent main widgets
|
|
2
2
|
|
|
3
|
-
Status: implemented, closed.
|
|
3
|
+
Status: implemented, closed (`cf42ab4`; released `v2.3.0`).
|
|
4
4
|
|
|
5
5
|
Dependency landed 2026-09-01: `2026-08-28-gui-main-vs-sidebar-widgets.md` merged to develop as
|
|
6
6
|
PR #157 (`b7a14a6`).
|
|
@@ -13,18 +13,22 @@ ADRs that concern `pyobs-gui` live in `pyobs-core`'s `specs/` tree instead (`spe
|
|
|
13
13
|
- `2026-08-28-structuredconfig-widget.md` — **proposed (issue #154)**. Generic
|
|
14
14
|
`StructuredConfigWidget`: schema-driven, auto-built editable form for `IStructuredConfig`
|
|
15
15
|
modules (from `ConfigSchema` capabilities + `ConfigAppliedState` + `set_config`).
|
|
16
|
-
- `2026-08-28-gui-main-vs-sidebar-widgets.md` — **implemented,
|
|
17
|
-
`
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
Post-merge follow-ups: sidebar-fill dedup + D5 sidebar-failure isolation + 3
|
|
22
|
-
findings (`fccf0e0`), FITS-headers sidebar box alignment (`1e89069`),
|
|
23
|
-
sidebar (`f05bcba`).
|
|
24
|
-
- `2026-09-01-gui-video-widget-split.md` — **implemented, closed
|
|
25
|
-
independent main widgets (`VideoWidget` "Live View" + new
|
|
26
|
-
matched on `IVideo`, rendered as separate tabs — turned out
|
|
27
|
-
mechanism (D6) the draft was written for; the two halves
|
|
16
|
+
- `2026-08-28-gui-main-vs-sidebar-widgets.md` — **implemented, closed (PR #157, `b7a14a6`;
|
|
17
|
+
released `v2.3.0`)**. Issue #150 closed. Explicit main-widget/sidebar-widget distinction,
|
|
18
|
+
automatic tab pages for multi-widget modules (shared sidebar, merge-per-interface custom
|
|
19
|
+
config, standalone parity), `sidebar_preferred` promotion rule, universal sidebar container,
|
|
20
|
+
`paired_sidebar_widget` mechanism (D6, still no consumer — the video-widget split below ended up
|
|
21
|
+
not needing it). Post-merge follow-ups: sidebar-fill dedup + D5 sidebar-failure isolation + 3
|
|
22
|
+
other review findings (`fccf0e0`), FITS-headers sidebar box alignment (`1e89069`),
|
|
23
|
+
vertical-only scrollable sidebar (`f05bcba`).
|
|
24
|
+
- `2026-09-01-gui-video-widget-split.md` — **implemented, closed, released `v2.3.0`**. Split
|
|
25
|
+
`VideoWidget` into two independent main widgets (`VideoWidget` "Live View" + new
|
|
26
|
+
`VideoGrabWidget` "FITS Image"), both matched on `IVideo`, rendered as separate tabs — turned out
|
|
27
|
+
not to need the `paired_sidebar_widget` mechanism (D6) the draft was written for; the two halves
|
|
28
|
+
never shared state to begin with. Same release also carries two unplanned same-day fixes with no
|
|
29
|
+
dedicated doc: `CameraWidget`'s control panel clipping its right edge (`scrollArea` sized off a
|
|
30
|
+
stale `.ui` width hint instead of real content, `c22d7c7`) and `ModeWidget`'s edit button staying
|
|
31
|
+
permanently disabled for an `IMode` module with no `IMotion` (`1439437`).
|
|
28
32
|
- `2026-08-21-gui-widget-startup-responsiveness.md` — **implemented, closed 2026-08-23 (PR #141,
|
|
29
33
|
`123161b`)**. Make module widgets appear and respond immediately at startup: non-blocking
|
|
30
34
|
`_add_client` with a "Loading…" placeholder page (clicks are never dropped), parallel
|
|
@@ -5,7 +5,7 @@ from PySide6 import QtWidgets # type: ignore
|
|
|
5
5
|
|
|
6
6
|
from pyobs.interfaces import ConfigAppliedState, ConfigFieldSchema, ConfigSchema
|
|
7
7
|
from pyobs.utils import exceptions as exc
|
|
8
|
-
from pyobs.utils.enums import Unit
|
|
8
|
+
from pyobs.utils.enums import AccessLevel, Unit
|
|
9
9
|
from pyobs_gui.structuredconfigwidget import StructuredConfigWidget, _build_editor, _nested_get
|
|
10
10
|
|
|
11
11
|
# -- shared fixtures -----------------------------------------------------------------------
|
|
@@ -152,6 +152,131 @@ def test_unsupported_field_type_is_also_a_disabled_placeholder() -> None:
|
|
|
152
152
|
assert editor.get_value() == [1, 2]
|
|
153
153
|
|
|
154
154
|
|
|
155
|
+
# -- _build_editor / _build_form: basic/expert level ----------------------------------------
|
|
156
|
+
#
|
|
157
|
+
# _build_editor itself only *tags* each editor with its level; it doesn't touch visibility --
|
|
158
|
+
# that's StructuredConfigWidget's job (_build_form/_on_advanced_toggled), tested below.
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def test_editor_is_tagged_with_its_schema_level() -> None:
|
|
162
|
+
basic = _build_editor("count", ConfigFieldSchema(type="int", default=3, level=AccessLevel.BASIC), lambda: None)
|
|
163
|
+
expert = _build_editor("count", ConfigFieldSchema(type="int", default=3, level=AccessLevel.EXPERT), lambda: None)
|
|
164
|
+
assert basic.level == AccessLevel.BASIC
|
|
165
|
+
assert expert.level == AccessLevel.EXPERT
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def test_unsupported_type_placeholder_ignores_expert_level_for_toggle_grouping() -> None:
|
|
169
|
+
"""A permanently non-editable placeholder must never be swept into the expert-toggle's
|
|
170
|
+
hide/show group, even if the underlying field is tagged EXPERT -- see the comment in
|
|
171
|
+
_build_editor's fallback branch."""
|
|
172
|
+
editor = _build_editor(
|
|
173
|
+
"items", ConfigFieldSchema(type="list", default=[1, 2], level=AccessLevel.EXPERT), lambda: None
|
|
174
|
+
)
|
|
175
|
+
assert editor.level == AccessLevel.BASIC
|
|
176
|
+
assert editor.widget.isEnabled() is False
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def test_nested_object_skips_hidden_children() -> None:
|
|
180
|
+
schema = ConfigFieldSchema(
|
|
181
|
+
type="object",
|
|
182
|
+
level=AccessLevel.EXPERT,
|
|
183
|
+
nested={
|
|
184
|
+
"az": ConfigFieldSchema(type="float", default=1.0),
|
|
185
|
+
"secret": ConfigFieldSchema(type="float", default=2.0, level=AccessLevel.HIDDEN),
|
|
186
|
+
},
|
|
187
|
+
)
|
|
188
|
+
editor = _build_editor("pointing", schema, lambda: None)
|
|
189
|
+
assert editor.children is not None and set(editor.children) == {"az"}
|
|
190
|
+
assert editor.level == AccessLevel.EXPERT
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _sample_schema_with_levels() -> ConfigSchema:
|
|
194
|
+
return ConfigSchema(
|
|
195
|
+
fields={
|
|
196
|
+
"name": ConfigFieldSchema(type="str", default="foo", level=AccessLevel.BASIC),
|
|
197
|
+
"gain": ConfigFieldSchema(type="int", default=1, level=AccessLevel.EXPERT),
|
|
198
|
+
"secret": ConfigFieldSchema(type="int", default=0, level=AccessLevel.HIDDEN),
|
|
199
|
+
}
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def test_build_form_skips_hidden_fields_entirely() -> None:
|
|
204
|
+
widget = StructuredConfigWidget()
|
|
205
|
+
widget._build_form(_sample_schema_with_levels())
|
|
206
|
+
|
|
207
|
+
assert widget._form_layout.rowCount() == 2 # name + gain, not secret
|
|
208
|
+
assert widget._root is not None and widget._root.children is not None
|
|
209
|
+
assert set(widget._root.children) == {"name", "gain"}
|
|
210
|
+
widget.close()
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def test_expert_field_row_starts_hidden_basic_field_row_does_not() -> None:
|
|
214
|
+
widget = StructuredConfigWidget()
|
|
215
|
+
widget._build_form(_sample_schema_with_levels())
|
|
216
|
+
assert widget._root is not None and widget._root.children is not None
|
|
217
|
+
basic_editor = widget._root.children["name"]
|
|
218
|
+
expert_editor = widget._root.children["gain"]
|
|
219
|
+
|
|
220
|
+
assert basic_editor.widget.isHidden() is False
|
|
221
|
+
assert basic_editor.row_label is not None and basic_editor.row_label.isHidden() is False
|
|
222
|
+
assert expert_editor.widget.isHidden() is True
|
|
223
|
+
assert expert_editor.row_label is not None and expert_editor.row_label.isHidden() is True
|
|
224
|
+
widget.close()
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def test_advanced_checkbox_shows_and_hides_expert_field_row() -> None:
|
|
228
|
+
widget = StructuredConfigWidget()
|
|
229
|
+
widget._build_form(_sample_schema_with_levels())
|
|
230
|
+
assert widget._root is not None and widget._root.children is not None
|
|
231
|
+
expert_editor = widget._root.children["gain"]
|
|
232
|
+
|
|
233
|
+
widget.checkBoxAdvanced.setChecked(True)
|
|
234
|
+
assert expert_editor.widget.isHidden() is False
|
|
235
|
+
assert expert_editor.row_label is not None and expert_editor.row_label.isHidden() is False
|
|
236
|
+
|
|
237
|
+
widget.checkBoxAdvanced.setChecked(False)
|
|
238
|
+
assert expert_editor.widget.isHidden() is True
|
|
239
|
+
assert expert_editor.row_label is not None and expert_editor.row_label.isHidden() is True
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
# -- _build_editor: description rendered below the widget -----------------------------------
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def test_field_without_description_is_not_wrapped() -> None:
|
|
246
|
+
editor = _build_editor("count", ConfigFieldSchema(type="int", default=3), lambda: None)
|
|
247
|
+
assert editor.row_widget is None
|
|
248
|
+
assert editor.display_widget is editor.widget
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def test_field_with_description_wraps_widget_with_label_below() -> None:
|
|
252
|
+
schema = ConfigFieldSchema(type="int", default=3, description="Number of exposures to take")
|
|
253
|
+
editor = _build_editor("count", schema, lambda: None)
|
|
254
|
+
|
|
255
|
+
assert editor.row_widget is not None
|
|
256
|
+
assert editor.display_widget is editor.row_widget
|
|
257
|
+
container_layout = editor.row_widget.layout()
|
|
258
|
+
assert container_layout.count() == 2
|
|
259
|
+
assert container_layout.itemAt(0).widget() is editor.widget
|
|
260
|
+
description_label = container_layout.itemAt(1).widget()
|
|
261
|
+
assert isinstance(description_label, QtWidgets.QLabel)
|
|
262
|
+
assert description_label.text() == "Number of exposures to take"
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def test_build_form_places_description_wrapper_in_the_row() -> None:
|
|
266
|
+
widget = StructuredConfigWidget()
|
|
267
|
+
schema = ConfigSchema(
|
|
268
|
+
fields={"count": ConfigFieldSchema(type="int", default=3, description="Number of exposures to take")}
|
|
269
|
+
)
|
|
270
|
+
widget._build_form(schema)
|
|
271
|
+
|
|
272
|
+
assert widget._root is not None and widget._root.children is not None
|
|
273
|
+
editor = widget._root.children["count"]
|
|
274
|
+
row_field_item = widget._form_layout.itemAt(0, QtWidgets.QFormLayout.ItemRole.FieldRole)
|
|
275
|
+
assert row_field_item.widget() is editor.row_widget
|
|
276
|
+
widget.close()
|
|
277
|
+
widget.close()
|
|
278
|
+
|
|
279
|
+
|
|
155
280
|
# -- StructuredConfigWidget: state population, dirty tracking, apply/reset ------------------
|
|
156
281
|
|
|
157
282
|
|
|
@@ -3256,7 +3256,7 @@ wheels = [
|
|
|
3256
3256
|
|
|
3257
3257
|
[[package]]
|
|
3258
3258
|
name = "pyobs-core"
|
|
3259
|
-
version = "2.
|
|
3259
|
+
version = "2.7.1"
|
|
3260
3260
|
source = { registry = "https://pypi.org/simple" }
|
|
3261
3261
|
dependencies = [
|
|
3262
3262
|
{ name = "aiohttp" },
|
|
@@ -3288,14 +3288,14 @@ dependencies = [
|
|
|
3288
3288
|
{ name = "tenacity" },
|
|
3289
3289
|
{ name = "typing-extensions" },
|
|
3290
3290
|
]
|
|
3291
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
3291
|
+
sdist = { url = "https://files.pythonhosted.org/packages/9d/5e/8d4de011685819cd4b04ee8a5aa35822814e0d6c607f4dfe6917b37e71e2/pyobs_core-2.7.1.tar.gz", hash = "sha256:beb50633cc21b6ea5a99713fe114973eecf7999a17f69578a996f4335fcb1ed7", size = 409990, upload-time = "2026-09-04T09:44:17.492Z" }
|
|
3292
3292
|
wheels = [
|
|
3293
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
3293
|
+
{ url = "https://files.pythonhosted.org/packages/fd/14/037a4e52b30816474fb4360a0741952499820e24af01c09f609311369379/pyobs_core-2.7.1-py3-none-any.whl", hash = "sha256:060de003b319e4f7a14dd80500d13d1d202df1e49364af74dac3e7715ff843cb", size = 639800, upload-time = "2026-09-04T09:44:16.291Z" },
|
|
3294
3294
|
]
|
|
3295
3295
|
|
|
3296
3296
|
[[package]]
|
|
3297
3297
|
name = "pyobs-gui"
|
|
3298
|
-
version = "2.
|
|
3298
|
+
version = "2.4.0"
|
|
3299
3299
|
source = { editable = "." }
|
|
3300
3300
|
dependencies = [
|
|
3301
3301
|
{ name = "astroplan" },
|
|
@@ -3339,7 +3339,7 @@ requires-dist = [
|
|
|
3339
3339
|
{ name = "colour", specifier = ">=0.1.5,<0.2" },
|
|
3340
3340
|
{ name = "keyring", specifier = ">=25.7.0,<26" },
|
|
3341
3341
|
{ name = "matplotlib", specifier = ">=3.10.1,<4" },
|
|
3342
|
-
{ name = "pyobs-core", specifier = ">=2.
|
|
3342
|
+
{ name = "pyobs-core", specifier = ">=2.7.1,<3" },
|
|
3343
3343
|
{ name = "pyside6", specifier = ">=6.10.1" },
|
|
3344
3344
|
{ name = "qasync", specifier = ">=0.27.1,<0.29" },
|
|
3345
3345
|
{ name = "qfitswidget", specifier = ">=0.13.1" },
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/resources/arrow-up-right-from-square-solid.svg
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|