pyobs-gui 2.0.0.dev3__tar.gz → 2.0.0.dev5__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.0.0.dev5/.gitignore +8 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/PKG-INFO +5 -3
- pyobs_gui-2.0.0.dev5/pyobs_gui/__main__.py +21 -0
- pyobs_gui-2.0.0.dev5/pyobs_gui/accounts.py +150 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/camerawidget.py +121 -41
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/compassmovewidget.py +2 -0
- pyobs_gui-2.0.0.dev5/pyobs_gui/gui.py +173 -0
- pyobs_gui-2.0.0.dev5/pyobs_gui/login.py +64 -0
- pyobs_gui-2.0.0.dev5/pyobs_gui/loginwindow.py +377 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/logmodel.py +11 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/mainwindow.py +89 -22
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/camerawidget.ui +28 -43
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/camerawidget_ui.py +61 -128
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/mainwindow.ui +195 -115
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/mainwindow_ui.py +75 -20
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/statuswidget.py +26 -2
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/telescopewidget.py +19 -2
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/utils.py +15 -2
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/weatherwidget.py +6 -4
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyproject.toml +13 -6
- pyobs_gui-2.0.0.dev5/specs/README.md +20 -0
- pyobs_gui-2.0.0.dev5/spike_standalone/main.py +84 -0
- pyobs_gui-2.0.0.dev5/spike_standalone/pysidedeploy.spec +97 -0
- pyobs_gui-2.0.0.dev5/tests/__init__.py +0 -0
- pyobs_gui-2.0.0.dev5/tests/conftest.py +20 -0
- pyobs_gui-2.0.0.dev5/tests/test_accounts.py +106 -0
- pyobs_gui-2.0.0.dev5/tests/test_loginwindow.py +196 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/uv.lock +369 -269
- pyobs_gui-2.0.0.dev3/pyobs_gui/gui.py +0 -92
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/.claude/skills/verify/SKILL.md +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/.github/dependabot.yml +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/.github/workflows/pypi.yml +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/.github/workflows/pyrefly.yml +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/.github/workflows/ruff.yml +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/.pre-commit-config.yaml +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/.readthedocs.yml +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/CHANGELOG.rst +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/DEVELOPMENT.md +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/DEV_IAcquisition.md +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/DEV_IAutoFocus.md +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/DEV_IAutoGuiding.md +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/DEV_NavbarShortcuts.md +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/DEV_qml_evaluation.md +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/DEV_telescopewidget_layout.md +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/LICENSE +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/README.md +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/bin/mypyuic5 +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/docs/Makefile +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/docs/make.bat +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/docs/requirements.txt +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/docs/source/_static/pyobs.gif +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/docs/source/conf.py +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/docs/source/index.rst +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/mypy.ini +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/__init__.py +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/acquisitionwidget.py +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/autofocuswidget.py +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/autoguidingwidget.py +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/base.py +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/commandinputwidget.py +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/coolingwidget.py +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/datadisplaywidget.py +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/eventswidget.py +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/filterwidget.py +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/fitsheaderswidget.py +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/focuswidget.py +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/modewidget.py +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/modifiedmixin.py +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/modulegui.py +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/__init__.py +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/acquisitionwidget.ui +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/acquisitionwidget_ui.py +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/autofocuswidget.ui +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/autofocuswidget_ui.py +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/autoguidingwidget.ui +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/autoguidingwidget_ui.py +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/compassmoveplugin.py +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/compassmovewidget.py +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/compassmovewidget.ui +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/compassmovewidget_ui.py +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/compile.sh +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/coolingwidget.ui +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/coolingwidget_ui.py +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/datadisplayplugin.py +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/datadisplaywidget.ui +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/datadisplaywidget_ui.py +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/designer.sh +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/eventswidget.ui +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/eventswidget_ui.py +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/filterwidget.ui +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/filterwidget_ui.py +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/fitsheaderswidget.ui +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/fitsheaderswidget_ui.py +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/focuswidget.ui +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/focuswidget_ui.py +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/modewidget.ui +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/modewidget_ui.py +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/resources/arrow-alt-circle-down-solid.svg +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/resources/arrow-alt-circle-left-solid.svg +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/resources/arrow-alt-circle-right-solid.svg +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/resources/arrow-alt-circle-up-solid.svg +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/resources/arrow-up-right-from-square-solid.svg +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/resources/arrows-to-eye-solid.svg +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/resources/chart-line-solid.svg +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/resources/circle-question-solid.svg +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/resources/cloud-sun-solid.svg +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/resources/edit-solid.svg +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/resources/search-solid.svg +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/resources/undo-solid.svg +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/resources.qrc +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/resources_rc.py +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/roofwidget.ui +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/roofwidget_ui.py +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/shellwidget.ui +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/shellwidget_ui.py +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/spectrographwidget.ui +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/spectrographwidget_ui.py +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/telescopewidget.ui +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/telescopewidget_ui.py +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/temperaturesplotwidget.ui +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/temperaturesplotwidget_ui.py +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/temperatureswidget.ui +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/temperatureswidget_ui.py +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/videowidget.ui +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/videowidget_ui.py +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/weatherwidget.ui +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/weatherwidget_ui.py +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/roofwidget.py +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/shellwidget.py +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/spectrographwidget.py +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/temperaturesplotwidget.py +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/temperatureswidget.py +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/videowidget.py +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/watchedlabel.py +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/test/acquisition.yaml +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/test/autofocus.yaml +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/test/camera.yaml +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/test/full.yaml +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/test/guiding.yaml +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/test/mode.yaml +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/test/roof.yaml +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/test/spectrograph.yaml +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/test/telescope.yaml +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/test/telescope_acl.yaml +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/test/telescope_acl_denied.yaml +0 -0
- {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/test/video.yaml +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyobs-gui
|
|
3
|
-
Version: 2.0.0.
|
|
3
|
+
Version: 2.0.0.dev5
|
|
4
4
|
Summary: A remote GUI for pyobs
|
|
5
5
|
Author-email: Tim-Oliver Husser <thusser@uni-goettingen.de>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -9,10 +9,12 @@ Requires-Python: <3.14,>=3.11
|
|
|
9
9
|
Requires-Dist: astroplan>=0.10.1
|
|
10
10
|
Requires-Dist: astropy<8,>=7.0.1
|
|
11
11
|
Requires-Dist: colour<0.2,>=0.1.5
|
|
12
|
+
Requires-Dist: keyring<26,>=25.7.0
|
|
12
13
|
Requires-Dist: matplotlib<4,>=3.10.1
|
|
13
|
-
Requires-Dist: pyobs-core<3,>=2.0.0.
|
|
14
|
+
Requires-Dist: pyobs-core<3,>=2.0.0.dev50
|
|
14
15
|
Requires-Dist: pyside6>=6.10.1
|
|
15
|
-
Requires-Dist: qasync<0.
|
|
16
|
+
Requires-Dist: qasync<0.29,>=0.27.1
|
|
16
17
|
Requires-Dist: qfitswidget>=0.13.1
|
|
17
18
|
Requires-Dist: qtawesome<2,>=1.4.0
|
|
19
|
+
Requires-Dist: secretstorage<4,>=3.5.0
|
|
18
20
|
Requires-Dist: sunpy[all]>=7.0.1
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Standalone entry point for an interactive-login pyobs-gui -- bypasses the config-file-driven
|
|
2
|
+
`pyobs` CLI entirely. This is what a compiled binary (see
|
|
3
|
+
specs/plans/gui-widget-plugins-and-packaging.md) targets: no YAML config, no credentials on disk
|
|
4
|
+
unless the user opts into "store password" in the login window.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from pyobs.application import Application
|
|
10
|
+
|
|
11
|
+
from .gui import GUI
|
|
12
|
+
from .login import login_and_build_gui
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def main() -> None:
|
|
16
|
+
app = Application(module_factory=login_and_build_gui, loop_module_class=GUI)
|
|
17
|
+
app.run()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
if __name__ == "__main__":
|
|
21
|
+
main()
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""Saved XMPP login accounts, persisted via QSettings -- see specs/plans/gui-login-window.md.
|
|
2
|
+
|
|
3
|
+
Each account has a stable internal id (a uuid4 hex string, generated in add_account()) distinct
|
|
4
|
+
from its jid/label -- both are freely editable via update_account(), and the id (not jid/label) is
|
|
5
|
+
what a stored password gets keyed on in the system keychain, so renaming an account never orphans
|
|
6
|
+
its password. Mirrors pyobs-polaris's SavedAccountsModel, which follows the same rule for the same
|
|
7
|
+
reason.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import uuid
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
|
|
15
|
+
from PySide6 import QtCore
|
|
16
|
+
|
|
17
|
+
_ORGANIZATION = "pyobs"
|
|
18
|
+
_APPLICATION = "pyobs-gui"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class Account:
|
|
23
|
+
id: str
|
|
24
|
+
jid: str
|
|
25
|
+
label: str = ""
|
|
26
|
+
host: str = ""
|
|
27
|
+
port: int = 0
|
|
28
|
+
use_tls: bool = True
|
|
29
|
+
insecure_skip_tls: bool = False
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def display_name(self) -> str:
|
|
33
|
+
return self.label if self.label else self.jid
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class SavedAccountsModel:
|
|
37
|
+
"""Saved account list, persisted via QSettings. Password storage is a separate concern --
|
|
38
|
+
see login.py's use of the `keyring` package, keyed by Account.id."""
|
|
39
|
+
|
|
40
|
+
def __init__(self, settings: QtCore.QSettings | None = None) -> None:
|
|
41
|
+
self._settings = settings if settings is not None else QtCore.QSettings(_ORGANIZATION, _APPLICATION)
|
|
42
|
+
|
|
43
|
+
def list_accounts(self) -> list[Account]:
|
|
44
|
+
accounts = []
|
|
45
|
+
size = self._settings.beginReadArray("accounts")
|
|
46
|
+
try:
|
|
47
|
+
for i in range(size):
|
|
48
|
+
self._settings.setArrayIndex(i)
|
|
49
|
+
accounts.append(
|
|
50
|
+
Account(
|
|
51
|
+
id=str(self._settings.value("id", "")),
|
|
52
|
+
jid=str(self._settings.value("jid", "")),
|
|
53
|
+
label=str(self._settings.value("label", "")),
|
|
54
|
+
host=str(self._settings.value("host", "")),
|
|
55
|
+
port=int(self._settings.value("port", 0, type=int)), # pyrefly: ignore [bad-argument-type]
|
|
56
|
+
use_tls=bool(self._settings.value("use_tls", True, type=bool)),
|
|
57
|
+
insecure_skip_tls=bool(self._settings.value("insecure_skip_tls", False, type=bool)),
|
|
58
|
+
)
|
|
59
|
+
)
|
|
60
|
+
finally:
|
|
61
|
+
self._settings.endArray()
|
|
62
|
+
return accounts
|
|
63
|
+
|
|
64
|
+
def account_by_id(self, account_id: str) -> Account | None:
|
|
65
|
+
for account in self.list_accounts():
|
|
66
|
+
if account.id == account_id:
|
|
67
|
+
return account
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
def add_account(
|
|
71
|
+
self,
|
|
72
|
+
jid: str,
|
|
73
|
+
label: str = "",
|
|
74
|
+
host: str = "",
|
|
75
|
+
port: int = 0,
|
|
76
|
+
use_tls: bool = True,
|
|
77
|
+
insecure_skip_tls: bool = False,
|
|
78
|
+
) -> str:
|
|
79
|
+
"""Adds a new account and returns its newly generated id."""
|
|
80
|
+
account = Account(
|
|
81
|
+
id=uuid.uuid4().hex,
|
|
82
|
+
jid=jid,
|
|
83
|
+
label=label,
|
|
84
|
+
host=host,
|
|
85
|
+
port=port,
|
|
86
|
+
use_tls=use_tls,
|
|
87
|
+
insecure_skip_tls=insecure_skip_tls,
|
|
88
|
+
)
|
|
89
|
+
accounts = self.list_accounts()
|
|
90
|
+
accounts.append(account)
|
|
91
|
+
self._write_all(accounts)
|
|
92
|
+
return account.id
|
|
93
|
+
|
|
94
|
+
def update_account(
|
|
95
|
+
self,
|
|
96
|
+
account_id: str,
|
|
97
|
+
jid: str,
|
|
98
|
+
label: str = "",
|
|
99
|
+
host: str = "",
|
|
100
|
+
port: int = 0,
|
|
101
|
+
use_tls: bool = True,
|
|
102
|
+
insecure_skip_tls: bool = False,
|
|
103
|
+
) -> None:
|
|
104
|
+
"""Updates an existing account's fields in place -- the id itself never changes."""
|
|
105
|
+
accounts = self.list_accounts()
|
|
106
|
+
for i, account in enumerate(accounts):
|
|
107
|
+
if account.id == account_id:
|
|
108
|
+
accounts[i] = Account(
|
|
109
|
+
id=account_id,
|
|
110
|
+
jid=jid,
|
|
111
|
+
label=label,
|
|
112
|
+
host=host,
|
|
113
|
+
port=port,
|
|
114
|
+
use_tls=use_tls,
|
|
115
|
+
insecure_skip_tls=insecure_skip_tls,
|
|
116
|
+
)
|
|
117
|
+
break
|
|
118
|
+
self._write_all(accounts)
|
|
119
|
+
|
|
120
|
+
def remove_account(self, account_id: str) -> None:
|
|
121
|
+
accounts = [a for a in self.list_accounts() if a.id != account_id]
|
|
122
|
+
self._write_all(accounts)
|
|
123
|
+
|
|
124
|
+
def _write_all(self, accounts: list[Account]) -> None:
|
|
125
|
+
self._settings.beginWriteArray("accounts")
|
|
126
|
+
try:
|
|
127
|
+
for i, account in enumerate(accounts):
|
|
128
|
+
self._settings.setArrayIndex(i)
|
|
129
|
+
self._settings.setValue("id", account.id)
|
|
130
|
+
self._settings.setValue("jid", account.jid)
|
|
131
|
+
self._settings.setValue("label", account.label)
|
|
132
|
+
self._settings.setValue("host", account.host)
|
|
133
|
+
self._settings.setValue("port", account.port)
|
|
134
|
+
self._settings.setValue("use_tls", account.use_tls)
|
|
135
|
+
self._settings.setValue("insecure_skip_tls", account.insecure_skip_tls)
|
|
136
|
+
finally:
|
|
137
|
+
self._settings.endArray()
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def last_selected_account_id(self) -> str:
|
|
141
|
+
"""Only remembers which account was last used (to preselect it next launch) -- never
|
|
142
|
+
implies anything was saved/connected automatically."""
|
|
143
|
+
return str(self._settings.value("last_selected_account_id", ""))
|
|
144
|
+
|
|
145
|
+
@last_selected_account_id.setter
|
|
146
|
+
def last_selected_account_id(self, value: str) -> None:
|
|
147
|
+
self._settings.setValue("last_selected_account_id", value)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
__all__ = ["Account", "SavedAccountsModel"]
|
|
@@ -17,7 +17,6 @@ from pyobs.interfaces import (
|
|
|
17
17
|
IBinning,
|
|
18
18
|
BinningState,
|
|
19
19
|
IWindow,
|
|
20
|
-
WindowState,
|
|
21
20
|
IFilters,
|
|
22
21
|
ICooling,
|
|
23
22
|
ITemperatures,
|
|
@@ -58,13 +57,6 @@ class CameraWidget(BaseWidget, Ui_CameraWidget):
|
|
|
58
57
|
self.exposure_time_left = 0.0
|
|
59
58
|
self.exposure_progress = 0.0
|
|
60
59
|
|
|
61
|
-
self.spinWindowLeft.init_modified(self.labelWindowLeft).committed.connect(self._window_changed)
|
|
62
|
-
self.spinWindowTop.init_modified(self.labelWindowTop).committed.connect(self._window_changed)
|
|
63
|
-
self.spinWindowWidth.init_modified(self.labelWindowWidth).committed.connect(self._window_changed)
|
|
64
|
-
self.spinWindowHeight.init_modified(self.labelWindowHeight).committed.connect(self._window_changed)
|
|
65
|
-
self.spinGain.init_modified(self.labelGain).committed.connect(self._gain_changed)
|
|
66
|
-
self.spinGainOffset.init_modified(self.labelGain).committed.connect(self._gain_changed)
|
|
67
|
-
|
|
68
60
|
async def open(
|
|
69
61
|
self,
|
|
70
62
|
modules: list[str] | None = None,
|
|
@@ -80,7 +72,7 @@ class CameraWidget(BaseWidget, Ui_CameraWidget):
|
|
|
80
72
|
image_types = sorted([it.name for it in ImageType])
|
|
81
73
|
self.comboImageType.addItems(image_types)
|
|
82
74
|
|
|
83
|
-
# before first update, disable
|
|
75
|
+
# before first update, disable myself
|
|
84
76
|
self.setEnabled(False)
|
|
85
77
|
|
|
86
78
|
# hide groups, if necessary
|
|
@@ -118,10 +110,25 @@ class CameraWidget(BaseWidget, Ui_CameraWidget):
|
|
|
118
110
|
|
|
119
111
|
async def _init(self) -> None:
|
|
120
112
|
# window
|
|
113
|
+
window_caps = await self.comm.get_capabilities(self.module, IWindow)
|
|
114
|
+
if window_caps is not None:
|
|
115
|
+
self.spinWindowLeft.setMaximum(int(window_caps.full_frame_width))
|
|
116
|
+
self.spinWindowTop.setMaximum(int(window_caps.full_frame_height))
|
|
117
|
+
self.spinWindowWidth.setMaximum(int(window_caps.full_frame_width))
|
|
118
|
+
self.spinWindowHeight.setMaximum(int(window_caps.full_frame_height))
|
|
121
119
|
async with self.comm.safe_proxy(self.module, IWindow) as proxy:
|
|
122
120
|
if proxy is not None:
|
|
123
|
-
await
|
|
124
|
-
|
|
121
|
+
state = await proxy.wait_for_state(IWindow)
|
|
122
|
+
if state is not None:
|
|
123
|
+
self.spinWindowLeft.setValue(state.x)
|
|
124
|
+
self.spinWindowTop.setValue(state.y)
|
|
125
|
+
self.spinWindowWidth.setValue(state.width)
|
|
126
|
+
self.spinWindowHeight.setValue(state.height)
|
|
127
|
+
elif window_caps is not None:
|
|
128
|
+
self.spinWindowLeft.setValue(window_caps.full_frame_x)
|
|
129
|
+
self.spinWindowTop.setValue(window_caps.full_frame_y)
|
|
130
|
+
self.spinWindowWidth.setValue(window_caps.full_frame_width)
|
|
131
|
+
self.spinWindowHeight.setValue(window_caps.full_frame_height)
|
|
125
132
|
|
|
126
133
|
# binning
|
|
127
134
|
binning_caps = await self.comm.get_capabilities(self.module, IBinning)
|
|
@@ -129,11 +136,21 @@ class CameraWidget(BaseWidget, Ui_CameraWidget):
|
|
|
129
136
|
binnings = [f"{b.x}x{b.y}" for b in binning_caps.binnings]
|
|
130
137
|
self.comboBinning.clear()
|
|
131
138
|
self.comboBinning.addItems(binnings)
|
|
132
|
-
|
|
139
|
+
async with self.comm.safe_proxy(self.module, IBinning) as proxy:
|
|
140
|
+
if proxy is not None:
|
|
141
|
+
state = await proxy.wait_for_state(IBinning)
|
|
142
|
+
if state is not None:
|
|
143
|
+
self.comboBinning.setCurrentText(f"{state.x}x{state.y}")
|
|
144
|
+
await self.comm.subscribe_state(self.module, IBinning, self._update_binning)
|
|
133
145
|
|
|
134
|
-
# gain
|
|
135
|
-
|
|
136
|
-
|
|
146
|
+
# gain
|
|
147
|
+
async with self.comm.safe_proxy(self.module, IGain) as proxy:
|
|
148
|
+
if proxy is not None:
|
|
149
|
+
state = await proxy.wait_for_state(IGain)
|
|
150
|
+
if state is not None:
|
|
151
|
+
self.spinGain.setValue(state.gain)
|
|
152
|
+
self.spinGainOffset.setValue(state.offset)
|
|
153
|
+
await self.comm.subscribe_state(self.module, IGain, self._update_gain)
|
|
137
154
|
|
|
138
155
|
# image format
|
|
139
156
|
image_format_caps = await self.comm.get_capabilities(self.module, IImageFormat)
|
|
@@ -141,38 +158,52 @@ class CameraWidget(BaseWidget, Ui_CameraWidget):
|
|
|
141
158
|
image_formats = [ImageFormat(f) for f in image_format_caps.image_formats]
|
|
142
159
|
self.comboImageFormat.clear()
|
|
143
160
|
self.comboImageFormat.addItems([f.name for f in image_formats])
|
|
144
|
-
|
|
161
|
+
async with self.comm.safe_proxy(self.module, IImageFormat) as proxy:
|
|
162
|
+
if proxy is not None:
|
|
163
|
+
state = await proxy.wait_for_state(IImageFormat)
|
|
164
|
+
if state is not None:
|
|
165
|
+
self.comboImageFormat.setCurrentText(state.image_format.name)
|
|
166
|
+
await self.comm.subscribe_state(self.module, IImageFormat, self._update_image_format)
|
|
145
167
|
|
|
168
|
+
# image type
|
|
146
169
|
async with self.comm.safe_proxy(self.module, IImageType) as proxy:
|
|
147
170
|
if proxy is not None:
|
|
148
|
-
await
|
|
171
|
+
state = await proxy.wait_for_state(IImageType)
|
|
172
|
+
if state is not None:
|
|
173
|
+
self.comboImageType.setCurrentText(state.image_type.name)
|
|
174
|
+
await self.comm.subscribe_state(self.module, IImageType, self._update_image_type)
|
|
149
175
|
|
|
176
|
+
# exposure (status, progress, time left)
|
|
150
177
|
async with self.comm.safe_proxy(self.module, IExposure) as proxy:
|
|
151
178
|
if proxy is not None:
|
|
152
|
-
await
|
|
153
|
-
|
|
179
|
+
state = await proxy.wait_for_state(IExposure)
|
|
180
|
+
if state is not None:
|
|
181
|
+
self.exposure_status = state.status
|
|
182
|
+
self.exposure_progress = state.progress
|
|
183
|
+
self.exposure_time_left = state.exposure_time_left
|
|
184
|
+
await self.comm.subscribe_state(self.module, IExposure, self._update_exposure)
|
|
185
|
+
|
|
186
|
+
# exposure time
|
|
154
187
|
async with self.comm.safe_proxy(self.module, IExposureTime) as proxy:
|
|
155
188
|
if proxy is not None:
|
|
156
|
-
await
|
|
189
|
+
state = await proxy.wait_for_state(IExposureTime)
|
|
190
|
+
if state is not None:
|
|
191
|
+
self.spinExpTime.setValue(state.exposure_time)
|
|
192
|
+
await self.comm.subscribe_state(self.module, IExposureTime, self._update_exposure_time)
|
|
157
193
|
|
|
194
|
+
# data sequence
|
|
158
195
|
if await self.comm.has_proxy(self.module, IDataSequence):
|
|
159
196
|
await self.comm.subscribe_state(self.module, IDataSequence, self._update_sequence)
|
|
160
197
|
|
|
161
198
|
# update GUI
|
|
162
199
|
self.signal_update_gui.emit()
|
|
163
200
|
|
|
164
|
-
def _update_window(self, state: WindowState):
|
|
165
|
-
self.labelWindowLeft.setText(str(state.x))
|
|
166
|
-
self.labelWindowTop.setText(str(state.y))
|
|
167
|
-
self.labelWindowWidth.setText(str(state.width))
|
|
168
|
-
self.labelWindowHeight.setText(str(state.height))
|
|
169
|
-
|
|
170
201
|
def _update_binning(self, state: BinningState):
|
|
171
202
|
self.comboBinning.setCurrentText(f"{state.x}x{state.y}")
|
|
172
203
|
|
|
173
204
|
def _update_gain(self, state: GainState):
|
|
174
|
-
self.
|
|
175
|
-
self.
|
|
205
|
+
self.spinGain.setValue(state.gain)
|
|
206
|
+
self.spinGainOffset.setValue(state.offset)
|
|
176
207
|
|
|
177
208
|
def _update_image_format(self, state: ImageFormatState):
|
|
178
209
|
self.comboImageFormat.setCurrentText(state.image_format.name)
|
|
@@ -194,14 +225,6 @@ class CameraWidget(BaseWidget, Ui_CameraWidget):
|
|
|
194
225
|
self.exposures_left = state.count_left
|
|
195
226
|
self.update_gui()
|
|
196
227
|
|
|
197
|
-
@qasync.asyncSlot()
|
|
198
|
-
async def _window_changed(self):
|
|
199
|
-
print("ok")
|
|
200
|
-
|
|
201
|
-
@qasync.asyncSlot()
|
|
202
|
-
async def _gain_changed(self):
|
|
203
|
-
print("ok")
|
|
204
|
-
|
|
205
228
|
@qasync.asyncSlot() # type: ignore
|
|
206
229
|
async def set_full_frame(self) -> None:
|
|
207
230
|
caps = await self.comm.get_capabilities(self.module, IWindow)
|
|
@@ -247,18 +270,75 @@ class CameraWidget(BaseWidget, Ui_CameraWidget):
|
|
|
247
270
|
|
|
248
271
|
@qasync.asyncSlot() # type: ignore
|
|
249
272
|
async def expose(self) -> None:
|
|
273
|
+
# set binning
|
|
274
|
+
async with self.comm.safe_proxy(self.module, IBinning) as proxy:
|
|
275
|
+
if proxy is not None:
|
|
276
|
+
binning = int(self.comboBinning.currentText()[0])
|
|
277
|
+
try:
|
|
278
|
+
await proxy.set_binning(binning, binning)
|
|
279
|
+
except Exception:
|
|
280
|
+
log.exception("bla")
|
|
281
|
+
QtWidgets.QMessageBox.information(self, "Error", "Could not set binning.")
|
|
282
|
+
return
|
|
283
|
+
else:
|
|
284
|
+
binning = 1
|
|
285
|
+
|
|
286
|
+
# set window
|
|
287
|
+
async with self.comm.safe_proxy(self.module, IWindow) as proxy:
|
|
288
|
+
if proxy is not None:
|
|
289
|
+
left, top = self.spinWindowLeft.value(), self.spinWindowTop.value()
|
|
290
|
+
width, height = self.spinWindowWidth.value(), self.spinWindowHeight.value()
|
|
291
|
+
try:
|
|
292
|
+
await proxy.set_window(left, top, width * binning, height * binning)
|
|
293
|
+
except Exception:
|
|
294
|
+
QtWidgets.QMessageBox.information(self, "Error", "Could not set window.")
|
|
295
|
+
return
|
|
296
|
+
|
|
297
|
+
# set image format
|
|
298
|
+
async with self.comm.safe_proxy(self.module, IImageFormat) as proxy:
|
|
299
|
+
if proxy is not None:
|
|
300
|
+
image_format = ImageFormat[self.comboImageFormat.currentText()]
|
|
301
|
+
await proxy.set_image_format(image_format)
|
|
302
|
+
|
|
303
|
+
# set exposure time
|
|
304
|
+
async with self.comm.safe_proxy(self.module, IExposureTime) as proxy:
|
|
305
|
+
if proxy is not None:
|
|
306
|
+
# get exp_time
|
|
307
|
+
exp_time = self.spinExpTime.value()
|
|
308
|
+
|
|
309
|
+
# unit
|
|
310
|
+
if self.comboExpTimeUnit.currentText() == "ms":
|
|
311
|
+
exp_time /= 1e3
|
|
312
|
+
elif self.comboExpTimeUnit.currentText() == "µs":
|
|
313
|
+
exp_time /= 1e6
|
|
314
|
+
|
|
315
|
+
# set it
|
|
316
|
+
await proxy.set_exposure_time(exp_time)
|
|
317
|
+
|
|
318
|
+
# set gain and offset
|
|
319
|
+
async with self.comm.safe_proxy(self.module, IGain) as proxy:
|
|
320
|
+
if proxy is not None:
|
|
321
|
+
await proxy.set_gain(self.spinGain.value())
|
|
322
|
+
await proxy.set_offset(self.spinGainOffset.value())
|
|
323
|
+
|
|
324
|
+
# set image type
|
|
325
|
+
image_type = ImageType.OBJECT
|
|
326
|
+
async with self.comm.safe_proxy(self.module, IImageType) as proxy:
|
|
327
|
+
if proxy is not None:
|
|
328
|
+
image_type = ImageType(self.comboImageType.currentText().lower())
|
|
329
|
+
await proxy.set_image_type(image_type)
|
|
330
|
+
|
|
331
|
+
# set initial image count
|
|
332
|
+
self.exposures_left = self.spinCount.value()
|
|
250
333
|
broadcast = self.checkBroadcast.isChecked()
|
|
251
|
-
count = self.spinCount.value()
|
|
252
334
|
|
|
253
|
-
# if the module can grab a counted sequence server-side, let it
|
|
254
|
-
# back via the DataSequenceState subscription
|
|
335
|
+
# if the module can grab a counted sequence server-side, let it
|
|
255
336
|
async with self.comm.safe_proxy(self.module, IDataSequence) as proxy:
|
|
256
337
|
if proxy is not None:
|
|
257
|
-
await proxy.grab_sequence(
|
|
338
|
+
await proxy.grab_sequence(self.exposures_left, broadcast)
|
|
258
339
|
return
|
|
259
340
|
|
|
260
341
|
# fall back to a client-side loop for modules that don't support IDataSequence
|
|
261
|
-
self.exposures_left = count
|
|
262
342
|
while self.exposures_left > 0:
|
|
263
343
|
await self.datadisplay.grab_data(broadcast)
|
|
264
344
|
self.exposures_left -= 1
|
|
@@ -46,7 +46,9 @@ class CompassMoveWidget(BaseWidget, Ui_CompassMoveWidget):
|
|
|
46
46
|
if altaz_state is None:
|
|
47
47
|
return
|
|
48
48
|
altaz = SkyCoord(
|
|
49
|
+
# pyrefly: ignore [missing-attribute]
|
|
49
50
|
alt=altaz_state.alt * u.degree,
|
|
51
|
+
# pyrefly: ignore [missing-attribute]
|
|
50
52
|
az=altaz_state.az * u.degree,
|
|
51
53
|
obstime=Time.now(),
|
|
52
54
|
location=self.observer.location,
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
import sys
|
|
4
|
+
from typing import Any, cast, TYPE_CHECKING
|
|
5
|
+
from PySide6 import QtWidgets # type: ignore
|
|
6
|
+
import qasync # type: ignore
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from .mainwindow import MainWindow
|
|
10
|
+
|
|
11
|
+
from pyobs.interfaces import FitsHeaderEntry, IFitsHeaderBefore
|
|
12
|
+
from pyobs.modules import Module
|
|
13
|
+
|
|
14
|
+
from .utils import QAsyncMessageBox
|
|
15
|
+
|
|
16
|
+
log = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class GUI(Module, IFitsHeaderBefore):
|
|
20
|
+
__module__ = "pyobs_gui"
|
|
21
|
+
|
|
22
|
+
app: QtWidgets.QApplication | None = None
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
show_shell: bool = True,
|
|
27
|
+
show_events: bool = True,
|
|
28
|
+
show_status: bool = True,
|
|
29
|
+
show_modules: list[str] | None = None,
|
|
30
|
+
widgets: list[dict[str, Any]] | None = None,
|
|
31
|
+
sidebar: list[dict[str, Any]] | None = None,
|
|
32
|
+
standalone: bool = False,
|
|
33
|
+
*args: Any,
|
|
34
|
+
**kwargs: Any,
|
|
35
|
+
):
|
|
36
|
+
"""Inits a new GUI.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
show_shell: Whether to show the shell page.
|
|
40
|
+
show_events: Whether to show the events page.
|
|
41
|
+
show_modules: If not empty, show only listed modules.
|
|
42
|
+
widgets: List of custom widgets.
|
|
43
|
+
sidebar: List of custom sidebar widgets.
|
|
44
|
+
standalone: Whether this GUI was built by the interactive login window
|
|
45
|
+
(pyobs_gui.login.login_and_build_gui) rather than a YAML-configured Comm. When
|
|
46
|
+
True, MainWindow's bottom-left button reads "Log out" and reconnects via the
|
|
47
|
+
login window instead of quitting the whole app.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
# init module
|
|
51
|
+
Module.__init__(self, *args, **kwargs)
|
|
52
|
+
self._window: MainWindow | None = None
|
|
53
|
+
self._show_shell = show_shell
|
|
54
|
+
self._show_events = show_events
|
|
55
|
+
self._show_status = show_status
|
|
56
|
+
self._show_modules = show_modules
|
|
57
|
+
self._custom_widgets = widgets
|
|
58
|
+
self._custom_sidebar_widgets = sidebar
|
|
59
|
+
self._standalone = standalone
|
|
60
|
+
self._logging_out = False
|
|
61
|
+
|
|
62
|
+
@staticmethod
|
|
63
|
+
def new_event_loop() -> asyncio.AbstractEventLoop:
|
|
64
|
+
GUI.app = QtWidgets.QApplication(sys.argv)
|
|
65
|
+
# standalone/login mode briefly has zero visible top-level windows while the login
|
|
66
|
+
# dialog closes and the (old or new) MainWindow hasn't opened yet -- Qt's default
|
|
67
|
+
# "quit when the last window closes" would tear down the whole event loop right there,
|
|
68
|
+
# so this app manages its own lifetime exclusively through Module.quit() instead.
|
|
69
|
+
GUI.app.setQuitOnLastWindowClosed(False)
|
|
70
|
+
return cast("asyncio.AbstractEventLoop", qasync.QEventLoop(GUI.app))
|
|
71
|
+
|
|
72
|
+
async def open(self) -> None:
|
|
73
|
+
"""Open module."""
|
|
74
|
+
from .mainwindow import MainWindow
|
|
75
|
+
|
|
76
|
+
await Module.open(self)
|
|
77
|
+
|
|
78
|
+
# create and show window
|
|
79
|
+
self._window = MainWindow(
|
|
80
|
+
show_shell=self._show_shell,
|
|
81
|
+
show_events=self._show_events,
|
|
82
|
+
show_status=self._show_status,
|
|
83
|
+
show_modules=self._show_modules,
|
|
84
|
+
widgets=self._custom_widgets,
|
|
85
|
+
sidebar=self._custom_sidebar_widgets,
|
|
86
|
+
on_logout=self._request_logout if self._standalone else None,
|
|
87
|
+
)
|
|
88
|
+
await self._window.open(
|
|
89
|
+
module=self,
|
|
90
|
+
comm=self.comm,
|
|
91
|
+
vfs=self.vfs,
|
|
92
|
+
observer=self._observer,
|
|
93
|
+
)
|
|
94
|
+
self._window.show()
|
|
95
|
+
|
|
96
|
+
def _request_logout(self) -> None:
|
|
97
|
+
"""Wired to MainWindow's "Log out" button (standalone mode only). Hides the current
|
|
98
|
+
window immediately -- like a real logout, not a "maybe" -- then schedules the async part
|
|
99
|
+
(unregistering its widgets' comm subscriptions, closing the comm, showing the login
|
|
100
|
+
window again) as a background task, since Qt click handlers are synchronous.
|
|
101
|
+
"""
|
|
102
|
+
if self._logging_out:
|
|
103
|
+
return # already logging out -- ignore a repeat click
|
|
104
|
+
self._logging_out = True
|
|
105
|
+
|
|
106
|
+
old_window = self._window
|
|
107
|
+
self._window = None
|
|
108
|
+
if old_window is not None:
|
|
109
|
+
old_window.close_for_logout()
|
|
110
|
+
|
|
111
|
+
asyncio.create_task(self._logout(old_window))
|
|
112
|
+
|
|
113
|
+
async def _logout(self, old_window: "MainWindow | None") -> None:
|
|
114
|
+
"""Closes the current comm, then shows the login window and reconnects with whatever it
|
|
115
|
+
resolves to -- all within the same running Application/event loop, so Ctrl+C and the
|
|
116
|
+
eventual real "Quit" still reach this exact module instance (see
|
|
117
|
+
specs/plans/gui-login-window.md for why a brand new GUI() instance would leave
|
|
118
|
+
Application's own bookkeeping stale instead).
|
|
119
|
+
|
|
120
|
+
By the time this runs, MainWindow._request_logout() has already hidden the old window --
|
|
121
|
+
there's no session left to fall back to, so any failure here (the user closes the login
|
|
122
|
+
window without connecting, or the reconnect itself fails) is reported via an error dialog
|
|
123
|
+
and the app then quits cleanly, rather than limping on with no window and no comm.
|
|
124
|
+
"""
|
|
125
|
+
if old_window is not None:
|
|
126
|
+
# must happen before deleteLater(): unregisters every widget's comm subscriptions
|
|
127
|
+
# first, so a stray in-flight callback can't fire against an already-destroyed label
|
|
128
|
+
await old_window.discard_all_widgets()
|
|
129
|
+
old_window.deleteLater()
|
|
130
|
+
|
|
131
|
+
old_comm = self._comm
|
|
132
|
+
if old_comm is not None:
|
|
133
|
+
await old_comm.close()
|
|
134
|
+
|
|
135
|
+
from .login import show_login_and_connect
|
|
136
|
+
|
|
137
|
+
try:
|
|
138
|
+
new_comm = await show_login_and_connect()
|
|
139
|
+
except asyncio.CancelledError:
|
|
140
|
+
log.info("Login window closed without connecting; nothing left to do but quit.")
|
|
141
|
+
self.quit()
|
|
142
|
+
return
|
|
143
|
+
except Exception as e:
|
|
144
|
+
log.exception("Could not build a new connection.")
|
|
145
|
+
await QAsyncMessageBox.critical(None, "Log out", f"Could not reconnect: {e}\n\nClosing the app.")
|
|
146
|
+
self.quit()
|
|
147
|
+
return
|
|
148
|
+
finally:
|
|
149
|
+
self._logging_out = False
|
|
150
|
+
|
|
151
|
+
self._comm = new_comm
|
|
152
|
+
try:
|
|
153
|
+
await self.startup()
|
|
154
|
+
except Exception as e:
|
|
155
|
+
log.exception("Reconnecting with the new account failed.")
|
|
156
|
+
await QAsyncMessageBox.critical(None, "Log out", f"Could not reconnect: {e}\n\nClosing the app.")
|
|
157
|
+
self.quit()
|
|
158
|
+
|
|
159
|
+
async def get_fits_header_before(
|
|
160
|
+
self, namespaces: list[str] | None = None, **kwargs: Any
|
|
161
|
+
) -> dict[str, FitsHeaderEntry]:
|
|
162
|
+
"""Returns FITS header for the current status of this module.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
namespaces: If given, only return FITS headers for the given namespaces.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
Dictionary containing FITS headers.
|
|
169
|
+
"""
|
|
170
|
+
if self._window is not None:
|
|
171
|
+
return self._window.get_fits_headers(namespaces)
|
|
172
|
+
else:
|
|
173
|
+
return {}
|