pyobs-gui 2.3.1__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.1 → pyobs_gui-2.4.0}/PKG-INFO +1 -1
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/structuredconfigwidget.py +83 -36
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyproject.toml +1 -1
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/tests/test_structuredconfigwidget.py +67 -18
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/uv.lock +1 -1
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/.claude/skills/verify/SKILL.md +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/.github/dependabot.yml +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/.github/workflows/dependabot-automerge.yml +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/.github/workflows/pypi.yml +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/.github/workflows/pyrefly.yml +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/.github/workflows/ruff.yml +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/.gitignore +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/.pre-commit-config.yaml +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/.readthedocs.yml +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/CHANGELOG.rst +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/LICENSE +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/README.md +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/bin/mypyuic5 +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/docs/Makefile +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/docs/make.bat +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/docs/requirements.txt +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/docs/source/_static/pyobs.gif +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/docs/source/conf.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/docs/source/index.rst +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/mypy.ini +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/__init__.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/__main__.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/_nuitka_astropy_patch.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/accounts.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/acquisitionwidget.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/autofocuswidget.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/autoguidingwidget.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/base.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/camerawidget.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/commandinputwidget.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/compassmovewidget.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/coolingwidget.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/datadisplaywidget.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/eventswidget.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/filterwidget.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/fitsheaderswidget.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/focuswidget.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/gui.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/login.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/loginwindow.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/logmodel.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/mainwindow.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/modewidget.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/modifiedmixin.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/modulegui.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/qt/__init__.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/qt/acquisitionwidget.ui +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/qt/acquisitionwidget_ui.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/qt/autofocuswidget.ui +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/qt/autofocuswidget_ui.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/qt/autoguidingwidget.ui +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/qt/autoguidingwidget_ui.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/qt/camerawidget.ui +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/qt/camerawidget_ui.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/qt/compassmoveplugin.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/qt/compassmovewidget.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/qt/compassmovewidget.ui +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/qt/compassmovewidget_ui.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/qt/compile.sh +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/qt/coolingwidget.ui +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/qt/coolingwidget_ui.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/qt/datadisplayplugin.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/qt/datadisplaywidget.ui +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/qt/datadisplaywidget_ui.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/qt/designer.sh +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/qt/eventswidget.ui +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/qt/eventswidget_ui.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/qt/filterwidget.ui +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/qt/filterwidget_ui.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/qt/fitsheaderswidget.ui +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/qt/fitsheaderswidget_ui.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/qt/focuswidget.ui +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/qt/focuswidget_ui.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/qt/mainwindow.ui +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/qt/mainwindow_ui.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/qt/modewidget.ui +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/qt/modewidget_ui.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/qt/resources/arrow-alt-circle-down-solid.svg +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/qt/resources/arrow-alt-circle-left-solid.svg +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/qt/resources/arrow-alt-circle-right-solid.svg +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/qt/resources/arrow-alt-circle-up-solid.svg +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/qt/resources/arrow-up-right-from-square-solid.svg +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/qt/resources/arrows-to-eye-solid.svg +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/qt/resources/chart-line-solid.svg +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/qt/resources/circle-question-solid.svg +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/qt/resources/cloud-sun-solid.svg +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/qt/resources/edit-solid.svg +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/qt/resources/search-solid.svg +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/qt/resources/undo-solid.svg +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/qt/resources.qrc +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/qt/resources_rc.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/qt/roboticwidget.ui +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/qt/roboticwidget_ui.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/qt/roofwidget.ui +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/qt/roofwidget_ui.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/qt/schedulewidget.ui +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/qt/schedulewidget_ui.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/qt/shellwidget.ui +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/qt/shellwidget_ui.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/qt/spectrographwidget.ui +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/qt/spectrographwidget_ui.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/qt/telescopewidget.ui +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/qt/telescopewidget_ui.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/qt/temperaturesplotwidget.ui +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/qt/temperaturesplotwidget_ui.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/qt/temperatureswidget.ui +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/qt/temperatureswidget_ui.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/qt/videograbwidget.ui +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/qt/videograbwidget_ui.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/qt/videowidget.ui +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/qt/videowidget_ui.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/qt/weatherwidget.ui +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/qt/weatherwidget_ui.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/roboticwidget.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/roofwidget.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/schedulewidget.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/shellwidget.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/spectrographwidget.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/statuswidget.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/telescopewidget.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/temperaturesplotwidget.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/temperatureswidget.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/utils.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/videograbwidget.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/videowidget.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/watchedlabel.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pyobs_gui/weatherwidget.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/pysidedeploy.spec +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/specs/2026-08-20-gui-remote-call-error-handling.md +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/specs/2026-08-21-gui-widget-startup-responsiveness.md +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/specs/2026-08-28-gui-main-vs-sidebar-widgets.md +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/specs/2026-08-28-structuredconfig-widget.md +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/specs/2026-08-31-irobotic-widgets.md +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/specs/2026-09-01-gui-video-widget-split.md +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/specs/index.md +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/spike_standalone/main.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/spike_standalone/pysidedeploy.spec +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/test/acquisition.yaml +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/test/autofocus.yaml +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/test/camera.yaml +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/test/full.yaml +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/test/guiding.yaml +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/test/mode.yaml +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/test/robotic.yaml +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/test/roof.yaml +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/test/spectrograph.yaml +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/test/structuredconfig.yaml +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/test/telescope.yaml +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/test/telescope_acl.yaml +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/test/telescope_acl_denied.yaml +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/test/video.yaml +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/tests/__init__.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/tests/conftest.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/tests/test_accounts.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/tests/test_camerawidget.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/tests/test_loginwindow.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/tests/test_mainwindow_startup.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/tests/test_modewidget.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/tests/test_multiwidget_pages.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/tests/test_roboticwidget.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/tests/test_schedulewidget.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/tests/test_videograbwidget.py +0 -0
- {pyobs_gui-2.3.1 → pyobs_gui-2.4.0}/tests/test_videowidget.py +0 -0
|
@@ -29,15 +29,48 @@ class _FieldEditor:
|
|
|
29
29
|
set_value: Callable[[ConfigValue], None]
|
|
30
30
|
children: Optional[Dict[str, "_FieldEditor"]] = None
|
|
31
31
|
level: AccessLevel = AccessLevel.BASIC
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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 []
|
|
38
51
|
for child in (editor.children or {}).values():
|
|
39
|
-
|
|
40
|
-
return
|
|
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
|
|
41
74
|
|
|
42
75
|
|
|
43
76
|
def _nested_get(children: Dict[str, _FieldEditor]) -> Dict[str, ConfigValue]:
|
|
@@ -60,9 +93,13 @@ def _build_editor(name: str, schema: ConfigFieldSchema, on_change: Callable[[],
|
|
|
60
93
|
if schema.default is not None:
|
|
61
94
|
line_edit.setText(str(schema.default))
|
|
62
95
|
line_edit.textChanged.connect(lambda _: on_change())
|
|
63
|
-
|
|
64
|
-
line_edit
|
|
65
|
-
|
|
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
|
+
)
|
|
66
103
|
|
|
67
104
|
if schema.type == "int":
|
|
68
105
|
spin_box = QtWidgets.QSpinBox()
|
|
@@ -70,10 +107,12 @@ def _build_editor(name: str, schema: ConfigFieldSchema, on_change: Callable[[],
|
|
|
70
107
|
if schema.default is not None:
|
|
71
108
|
spin_box.setValue(int(schema.default))
|
|
72
109
|
spin_box.valueChanged.connect(lambda _: on_change())
|
|
73
|
-
if schema.level == AccessLevel.EXPERT:
|
|
74
|
-
spin_box.setEnabled(False)
|
|
75
110
|
return _FieldEditor(
|
|
76
|
-
spin_box,
|
|
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),
|
|
77
116
|
)
|
|
78
117
|
|
|
79
118
|
if schema.type == "float":
|
|
@@ -85,13 +124,12 @@ def _build_editor(name: str, schema: ConfigFieldSchema, on_change: Callable[[],
|
|
|
85
124
|
if schema.default is not None:
|
|
86
125
|
double_spin_box.setValue(float(schema.default))
|
|
87
126
|
double_spin_box.valueChanged.connect(lambda _: on_change())
|
|
88
|
-
if schema.level == AccessLevel.EXPERT:
|
|
89
|
-
double_spin_box.setEnabled(False)
|
|
90
127
|
return _FieldEditor(
|
|
91
128
|
double_spin_box,
|
|
92
129
|
double_spin_box.value,
|
|
93
130
|
lambda value: double_spin_box.setValue(float(cast("float", value))),
|
|
94
131
|
level=schema.level,
|
|
132
|
+
row_widget=_wrap_with_description(double_spin_box, schema.description),
|
|
95
133
|
)
|
|
96
134
|
|
|
97
135
|
if schema.type == "bool":
|
|
@@ -99,10 +137,12 @@ def _build_editor(name: str, schema: ConfigFieldSchema, on_change: Callable[[],
|
|
|
99
137
|
if schema.default is not None:
|
|
100
138
|
check_box.setChecked(bool(schema.default))
|
|
101
139
|
check_box.toggled.connect(lambda _: on_change())
|
|
102
|
-
if schema.level == AccessLevel.EXPERT:
|
|
103
|
-
check_box.setEnabled(False)
|
|
104
140
|
return _FieldEditor(
|
|
105
|
-
check_box,
|
|
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),
|
|
106
146
|
)
|
|
107
147
|
|
|
108
148
|
if schema.type == "enum":
|
|
@@ -112,13 +152,12 @@ def _build_editor(name: str, schema: ConfigFieldSchema, on_change: Callable[[],
|
|
|
112
152
|
if schema.default is not None and str(schema.default) in options:
|
|
113
153
|
combo_box.setCurrentText(str(schema.default))
|
|
114
154
|
combo_box.currentTextChanged.connect(lambda _: on_change())
|
|
115
|
-
if schema.level == AccessLevel.EXPERT:
|
|
116
|
-
combo_box.setEnabled(False)
|
|
117
155
|
return _FieldEditor(
|
|
118
156
|
combo_box,
|
|
119
157
|
combo_box.currentText,
|
|
120
158
|
lambda value: combo_box.setCurrentText(str(value)),
|
|
121
159
|
level=schema.level,
|
|
160
|
+
row_widget=_wrap_with_description(combo_box, schema.description),
|
|
122
161
|
)
|
|
123
162
|
|
|
124
163
|
if schema.type == "object" and schema.nested is not None:
|
|
@@ -130,16 +169,17 @@ def _build_editor(name: str, schema: ConfigFieldSchema, on_change: Callable[[],
|
|
|
130
169
|
if child_schema.level == AccessLevel.HIDDEN:
|
|
131
170
|
continue
|
|
132
171
|
child_editor = _build_editor(child_name, child_schema, on_change)
|
|
133
|
-
|
|
172
|
+
child_label = QtWidgets.QLabel(child_name)
|
|
173
|
+
form_layout.addRow(child_label, child_editor.display_widget)
|
|
174
|
+
child_editor.row_label = child_label
|
|
134
175
|
children[child_name] = child_editor
|
|
135
|
-
if schema.level == AccessLevel.EXPERT:
|
|
136
|
-
group_box.setEnabled(False)
|
|
137
176
|
return _FieldEditor(
|
|
138
177
|
group_box,
|
|
139
178
|
lambda: _nested_get(children),
|
|
140
179
|
lambda value: _nested_set(children, value),
|
|
141
180
|
children=children,
|
|
142
181
|
level=schema.level,
|
|
182
|
+
row_widget=_wrap_with_description(group_box, schema.description),
|
|
143
183
|
)
|
|
144
184
|
|
|
145
185
|
# object without a nested schema (pydantic freeform dict), or any other schema type this
|
|
@@ -158,9 +198,11 @@ def _build_editor(name: str, schema: ConfigFieldSchema, on_change: Callable[[],
|
|
|
158
198
|
placeholder.setReadOnly(True)
|
|
159
199
|
placeholder.setEnabled(False)
|
|
160
200
|
placeholder.setToolTip(f"Field type {schema.type!r} has no schema-driven editor for this widget.")
|
|
161
|
-
# always BASIC regardless of schema.level: this
|
|
162
|
-
#
|
|
163
|
-
return _FieldEditor(
|
|
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
|
+
)
|
|
164
206
|
|
|
165
207
|
|
|
166
208
|
class StructuredConfigWidget(BaseWidget):
|
|
@@ -182,11 +224,12 @@ class StructuredConfigWidget(BaseWidget):
|
|
|
182
224
|
# itself register as a user edit (which would otherwise re-enable Apply/Reset for a
|
|
183
225
|
# change that came from the module, not from the operator)
|
|
184
226
|
self._applying_state = False
|
|
185
|
-
# every EXPERT-level editor
|
|
186
|
-
#
|
|
187
|
-
# pyftscontrol's Expert Mode button -- operators here are
|
|
188
|
-
# comm layer, so it's just a
|
|
189
|
-
|
|
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] = []
|
|
190
233
|
|
|
191
234
|
outer_layout = QtWidgets.QVBoxLayout()
|
|
192
235
|
self.setLayout(outer_layout)
|
|
@@ -238,7 +281,9 @@ class StructuredConfigWidget(BaseWidget):
|
|
|
238
281
|
if field_schema.level == AccessLevel.HIDDEN:
|
|
239
282
|
continue
|
|
240
283
|
editor = _build_editor(name, field_schema, self._on_editor_changed)
|
|
241
|
-
|
|
284
|
+
label = QtWidgets.QLabel(name)
|
|
285
|
+
self._form_layout.addRow(label, editor.display_widget)
|
|
286
|
+
editor.row_label = label
|
|
242
287
|
children[name] = editor
|
|
243
288
|
self._root = _FieldEditor(
|
|
244
289
|
self._form_container,
|
|
@@ -246,7 +291,7 @@ class StructuredConfigWidget(BaseWidget):
|
|
|
246
291
|
lambda value: _nested_set(children, value),
|
|
247
292
|
children=children,
|
|
248
293
|
)
|
|
249
|
-
self.
|
|
294
|
+
self._expert_editors = _collect_by_level(self._root, AccessLevel.EXPERT)
|
|
250
295
|
self._on_advanced_toggled(self.checkBoxAdvanced.isChecked())
|
|
251
296
|
# a state may already have arrived (and been cached) before the form existed to receive
|
|
252
297
|
# it -- apply it now instead of waiting for the next update
|
|
@@ -254,8 +299,10 @@ class StructuredConfigWidget(BaseWidget):
|
|
|
254
299
|
self.signal_update_gui.emit()
|
|
255
300
|
|
|
256
301
|
def _on_advanced_toggled(self, checked: bool) -> None:
|
|
257
|
-
for
|
|
258
|
-
|
|
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)
|
|
259
306
|
|
|
260
307
|
def _on_state(self, state: ConfigAppliedState) -> None:
|
|
261
308
|
self._last_applied = dict(state.config)
|
|
@@ -153,23 +153,21 @@ def test_unsupported_field_type_is_also_a_disabled_placeholder() -> None:
|
|
|
153
153
|
|
|
154
154
|
|
|
155
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.
|
|
156
159
|
|
|
157
160
|
|
|
158
|
-
def
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
assert
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
def test_expert_field_starts_disabled() -> None:
|
|
165
|
-
editor = _build_editor("count", ConfigFieldSchema(type="int", default=3, level=AccessLevel.EXPERT), lambda: None)
|
|
166
|
-
assert editor.widget.isEnabled() is False
|
|
167
|
-
assert editor.level == AccessLevel.EXPERT
|
|
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
|
|
168
166
|
|
|
169
167
|
|
|
170
168
|
def test_unsupported_type_placeholder_ignores_expert_level_for_toggle_grouping() -> None:
|
|
171
169
|
"""A permanently non-editable placeholder must never be swept into the expert-toggle's
|
|
172
|
-
|
|
170
|
+
hide/show group, even if the underlying field is tagged EXPERT -- see the comment in
|
|
173
171
|
_build_editor's fallback branch."""
|
|
174
172
|
editor = _build_editor(
|
|
175
173
|
"items", ConfigFieldSchema(type="list", default=[1, 2], level=AccessLevel.EXPERT), lambda: None
|
|
@@ -178,7 +176,7 @@ def test_unsupported_type_placeholder_ignores_expert_level_for_toggle_grouping()
|
|
|
178
176
|
assert editor.widget.isEnabled() is False
|
|
179
177
|
|
|
180
178
|
|
|
181
|
-
def
|
|
179
|
+
def test_nested_object_skips_hidden_children() -> None:
|
|
182
180
|
schema = ConfigFieldSchema(
|
|
183
181
|
type="object",
|
|
184
182
|
level=AccessLevel.EXPERT,
|
|
@@ -189,7 +187,7 @@ def test_nested_object_skips_hidden_children_and_disables_whole_group_when_exper
|
|
|
189
187
|
)
|
|
190
188
|
editor = _build_editor("pointing", schema, lambda: None)
|
|
191
189
|
assert editor.children is not None and set(editor.children) == {"az"}
|
|
192
|
-
assert editor.
|
|
190
|
+
assert editor.level == AccessLevel.EXPERT
|
|
193
191
|
|
|
194
192
|
|
|
195
193
|
def _sample_schema_with_levels() -> ConfigSchema:
|
|
@@ -212,19 +210,70 @@ def test_build_form_skips_hidden_fields_entirely() -> None:
|
|
|
212
210
|
widget.close()
|
|
213
211
|
|
|
214
212
|
|
|
215
|
-
def
|
|
213
|
+
def test_expert_field_row_starts_hidden_basic_field_row_does_not() -> None:
|
|
216
214
|
widget = StructuredConfigWidget()
|
|
217
215
|
widget._build_form(_sample_schema_with_levels())
|
|
218
216
|
assert widget._root is not None and widget._root.children is not None
|
|
219
|
-
|
|
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()
|
|
220
225
|
|
|
221
|
-
|
|
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"]
|
|
222
232
|
|
|
223
233
|
widget.checkBoxAdvanced.setChecked(True)
|
|
224
|
-
assert
|
|
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
|
|
225
236
|
|
|
226
237
|
widget.checkBoxAdvanced.setChecked(False)
|
|
227
|
-
assert
|
|
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()
|
|
228
277
|
widget.close()
|
|
229
278
|
|
|
230
279
|
|
|
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.1 → 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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|