pyobs-gui 2.1.0__tar.gz → 2.3.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.
Files changed (171) hide show
  1. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/PKG-INFO +1 -1
  2. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/__init__.py +1 -0
  3. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/acquisitionwidget.py +9 -3
  4. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/autofocuswidget.py +5 -3
  5. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/autoguidingwidget.py +6 -3
  6. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/camerawidget.py +22 -6
  7. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/datadisplaywidget.py +5 -3
  8. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/mainwindow.py +16 -3
  9. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/modewidget.py +9 -2
  10. pyobs_gui-2.3.0/pyobs_gui/qt/videograbwidget.ui +257 -0
  11. pyobs_gui-2.3.0/pyobs_gui/qt/videograbwidget_ui.py +146 -0
  12. pyobs_gui-2.3.0/pyobs_gui/qt/videowidget.ui +102 -0
  13. pyobs_gui-2.3.0/pyobs_gui/qt/videowidget_ui.py +101 -0
  14. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/roofwidget.py +2 -1
  15. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/temperaturesplotwidget.py +8 -3
  16. pyobs_gui-2.3.0/pyobs_gui/videograbwidget.py +143 -0
  17. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/videowidget.py +7 -95
  18. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/weatherwidget.py +4 -3
  19. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyproject.toml +1 -1
  20. pyobs_gui-2.3.0/specs/2026-09-01-gui-video-widget-split.md +90 -0
  21. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/specs/index.md +4 -3
  22. pyobs_gui-2.3.0/tests/test_modewidget.py +72 -0
  23. pyobs_gui-2.3.0/tests/test_videograbwidget.py +38 -0
  24. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/tests/test_videowidget.py +1 -34
  25. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/uv.lock +1 -1
  26. pyobs_gui-2.1.0/pyobs_gui/qt/videowidget.ui +0 -1115
  27. pyobs_gui-2.1.0/pyobs_gui/qt/videowidget_ui.py +0 -354
  28. pyobs_gui-2.1.0/specs/2026-09-01-gui-video-widget-split.md +0 -107
  29. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/.claude/skills/verify/SKILL.md +0 -0
  30. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/.github/dependabot.yml +0 -0
  31. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/.github/workflows/dependabot-automerge.yml +0 -0
  32. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/.github/workflows/pypi.yml +0 -0
  33. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/.github/workflows/pyrefly.yml +0 -0
  34. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/.github/workflows/ruff.yml +0 -0
  35. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/.gitignore +0 -0
  36. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/.pre-commit-config.yaml +0 -0
  37. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/.readthedocs.yml +0 -0
  38. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/CHANGELOG.rst +0 -0
  39. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/LICENSE +0 -0
  40. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/README.md +0 -0
  41. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/bin/mypyuic5 +0 -0
  42. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/docs/Makefile +0 -0
  43. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/docs/make.bat +0 -0
  44. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/docs/requirements.txt +0 -0
  45. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/docs/source/_static/pyobs.gif +0 -0
  46. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/docs/source/conf.py +0 -0
  47. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/docs/source/index.rst +0 -0
  48. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/mypy.ini +0 -0
  49. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/__main__.py +0 -0
  50. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/_nuitka_astropy_patch.py +0 -0
  51. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/accounts.py +0 -0
  52. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/base.py +0 -0
  53. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/commandinputwidget.py +0 -0
  54. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/compassmovewidget.py +0 -0
  55. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/coolingwidget.py +0 -0
  56. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/eventswidget.py +0 -0
  57. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/filterwidget.py +0 -0
  58. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/fitsheaderswidget.py +0 -0
  59. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/focuswidget.py +0 -0
  60. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/gui.py +0 -0
  61. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/login.py +0 -0
  62. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/loginwindow.py +0 -0
  63. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/logmodel.py +0 -0
  64. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/modifiedmixin.py +0 -0
  65. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/modulegui.py +0 -0
  66. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/qt/__init__.py +0 -0
  67. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/qt/acquisitionwidget.ui +0 -0
  68. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/qt/acquisitionwidget_ui.py +0 -0
  69. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/qt/autofocuswidget.ui +0 -0
  70. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/qt/autofocuswidget_ui.py +0 -0
  71. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/qt/autoguidingwidget.ui +0 -0
  72. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/qt/autoguidingwidget_ui.py +0 -0
  73. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/qt/camerawidget.ui +0 -0
  74. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/qt/camerawidget_ui.py +0 -0
  75. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/qt/compassmoveplugin.py +0 -0
  76. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/qt/compassmovewidget.py +0 -0
  77. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/qt/compassmovewidget.ui +0 -0
  78. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/qt/compassmovewidget_ui.py +0 -0
  79. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/qt/compile.sh +0 -0
  80. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/qt/coolingwidget.ui +0 -0
  81. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/qt/coolingwidget_ui.py +0 -0
  82. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/qt/datadisplayplugin.py +0 -0
  83. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/qt/datadisplaywidget.ui +0 -0
  84. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/qt/datadisplaywidget_ui.py +0 -0
  85. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/qt/designer.sh +0 -0
  86. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/qt/eventswidget.ui +0 -0
  87. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/qt/eventswidget_ui.py +0 -0
  88. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/qt/filterwidget.ui +0 -0
  89. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/qt/filterwidget_ui.py +0 -0
  90. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/qt/fitsheaderswidget.ui +0 -0
  91. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/qt/fitsheaderswidget_ui.py +0 -0
  92. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/qt/focuswidget.ui +0 -0
  93. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/qt/focuswidget_ui.py +0 -0
  94. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/qt/mainwindow.ui +0 -0
  95. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/qt/mainwindow_ui.py +0 -0
  96. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/qt/modewidget.ui +0 -0
  97. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/qt/modewidget_ui.py +0 -0
  98. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/qt/resources/arrow-alt-circle-down-solid.svg +0 -0
  99. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/qt/resources/arrow-alt-circle-left-solid.svg +0 -0
  100. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/qt/resources/arrow-alt-circle-right-solid.svg +0 -0
  101. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/qt/resources/arrow-alt-circle-up-solid.svg +0 -0
  102. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/qt/resources/arrow-up-right-from-square-solid.svg +0 -0
  103. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/qt/resources/arrows-to-eye-solid.svg +0 -0
  104. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/qt/resources/chart-line-solid.svg +0 -0
  105. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/qt/resources/circle-question-solid.svg +0 -0
  106. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/qt/resources/cloud-sun-solid.svg +0 -0
  107. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/qt/resources/edit-solid.svg +0 -0
  108. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/qt/resources/search-solid.svg +0 -0
  109. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/qt/resources/undo-solid.svg +0 -0
  110. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/qt/resources.qrc +0 -0
  111. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/qt/resources_rc.py +0 -0
  112. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/qt/roboticwidget.ui +0 -0
  113. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/qt/roboticwidget_ui.py +0 -0
  114. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/qt/roofwidget.ui +0 -0
  115. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/qt/roofwidget_ui.py +0 -0
  116. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/qt/schedulewidget.ui +0 -0
  117. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/qt/schedulewidget_ui.py +0 -0
  118. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/qt/shellwidget.ui +0 -0
  119. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/qt/shellwidget_ui.py +0 -0
  120. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/qt/spectrographwidget.ui +0 -0
  121. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/qt/spectrographwidget_ui.py +0 -0
  122. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/qt/telescopewidget.ui +0 -0
  123. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/qt/telescopewidget_ui.py +0 -0
  124. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/qt/temperaturesplotwidget.ui +0 -0
  125. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/qt/temperaturesplotwidget_ui.py +0 -0
  126. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/qt/temperatureswidget.ui +0 -0
  127. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/qt/temperatureswidget_ui.py +0 -0
  128. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/qt/weatherwidget.ui +0 -0
  129. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/qt/weatherwidget_ui.py +0 -0
  130. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/roboticwidget.py +0 -0
  131. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/schedulewidget.py +0 -0
  132. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/shellwidget.py +0 -0
  133. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/spectrographwidget.py +0 -0
  134. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/statuswidget.py +0 -0
  135. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/structuredconfigwidget.py +0 -0
  136. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/telescopewidget.py +0 -0
  137. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/temperatureswidget.py +0 -0
  138. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/utils.py +0 -0
  139. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pyobs_gui/watchedlabel.py +0 -0
  140. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/pysidedeploy.spec +0 -0
  141. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/specs/2026-08-20-gui-remote-call-error-handling.md +0 -0
  142. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/specs/2026-08-21-gui-widget-startup-responsiveness.md +0 -0
  143. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/specs/2026-08-28-gui-main-vs-sidebar-widgets.md +0 -0
  144. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/specs/2026-08-28-structuredconfig-widget.md +0 -0
  145. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/specs/2026-08-31-irobotic-widgets.md +0 -0
  146. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/spike_standalone/main.py +0 -0
  147. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/spike_standalone/pysidedeploy.spec +0 -0
  148. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/test/acquisition.yaml +0 -0
  149. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/test/autofocus.yaml +0 -0
  150. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/test/camera.yaml +0 -0
  151. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/test/full.yaml +0 -0
  152. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/test/guiding.yaml +0 -0
  153. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/test/mode.yaml +0 -0
  154. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/test/robotic.yaml +0 -0
  155. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/test/roof.yaml +0 -0
  156. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/test/spectrograph.yaml +0 -0
  157. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/test/structuredconfig.yaml +0 -0
  158. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/test/telescope.yaml +0 -0
  159. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/test/telescope_acl.yaml +0 -0
  160. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/test/telescope_acl_denied.yaml +0 -0
  161. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/test/video.yaml +0 -0
  162. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/tests/__init__.py +0 -0
  163. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/tests/conftest.py +0 -0
  164. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/tests/test_accounts.py +0 -0
  165. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/tests/test_camerawidget.py +0 -0
  166. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/tests/test_loginwindow.py +0 -0
  167. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/tests/test_mainwindow_startup.py +0 -0
  168. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/tests/test_multiwidget_pages.py +0 -0
  169. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/tests/test_roboticwidget.py +0 -0
  170. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/tests/test_schedulewidget.py +0 -0
  171. {pyobs_gui-2.1.0 → pyobs_gui-2.3.0}/tests/test_structuredconfigwidget.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pyobs-gui
3
- Version: 2.1.0
3
+ Version: 2.3.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
@@ -17,6 +17,7 @@ from .datadisplaywidget import DataDisplayWidget
17
17
  from .coolingwidget import CoolingWidget
18
18
  from .shellwidget import ShellWidget
19
19
  from .videowidget import VideoWidget
20
+ from .videograbwidget import VideoGrabWidget
20
21
  from .focuswidget import FocusWidget
21
22
  from .telescopewidget import TelescopeWidget
22
23
  from .weatherwidget import WeatherWidget
@@ -5,7 +5,7 @@ from typing import Any
5
5
  from PySide6 import QtCore, QtWidgets # type: ignore
6
6
 
7
7
  os.environ["QT_API"] = "PySide6"
8
- from matplotlib import pyplot as plt
8
+ from matplotlib.figure import Figure
9
9
  from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas
10
10
  from matplotlib.ticker import MaxNLocator
11
11
 
@@ -36,8 +36,14 @@ class AcquisitionWidget(BaseWidget, Ui_AcquisitionWidget):
36
36
  self._running = False
37
37
  self._result: AcquisitionResult | None = None
38
38
 
39
- # add plots: distance vs. attempt, and the 2D offset trajectory
40
- self.figure, (self.ax, self.ax2) = plt.subplots(1, 2)
39
+ # add plots: distance vs. attempt, and the 2D offset trajectory -- built via the plain OO
40
+ # Figure API, not pyplot: pyplot.subplots() does one-time GUI-backend registration (and
41
+ # can pop up a throwaway Qt window) on its first call anywhere in the process,
42
+ # synchronously on whatever thread calls it. In this app that's the shared qasync loop,
43
+ # so the first widget of this kind to open in a session stalls the event loop for
44
+ # several seconds. Figure() sidesteps pyplot entirely.
45
+ self.figure = Figure()
46
+ self.ax, self.ax2 = self.figure.subplots(1, 2)
41
47
  layout = QtWidgets.QVBoxLayout(self.framePlot)
42
48
  self.canvas = FigureCanvas(self.figure)
43
49
  layout.addWidget(self.canvas)
@@ -5,7 +5,7 @@ from typing import Any
5
5
  from PySide6 import QtCore, QtWidgets # type: ignore
6
6
 
7
7
  os.environ["QT_API"] = "PySide6"
8
- from matplotlib import pyplot as plt
8
+ from matplotlib.figure import Figure
9
9
  from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas
10
10
 
11
11
  from pyobs.events import Event, FocusFoundEvent
@@ -28,8 +28,10 @@ class AutoFocusWidget(BaseWidget, Ui_AutoFocusWidget):
28
28
  self._running = False
29
29
  self._last_result: tuple[float, float] | None = None
30
30
 
31
- # add plot
32
- self.figure, self.ax = plt.subplots()
31
+ # add plot -- built via the plain OO Figure API, not pyplot: see comment in
32
+ # acquisitionwidget.py for why (avoids a several-second first-use event-loop stall)
33
+ self.figure = Figure()
34
+ self.ax = self.figure.add_subplot()
33
35
  layout = QtWidgets.QVBoxLayout(self.framePlot)
34
36
  self.canvas = FigureCanvas(self.figure)
35
37
  layout.addWidget(self.canvas)
@@ -7,7 +7,7 @@ from typing import Any
7
7
  from PySide6 import QtCore, QtWidgets # type: ignore
8
8
 
9
9
  os.environ["QT_API"] = "PySide6"
10
- from matplotlib import pyplot as plt
10
+ from matplotlib.figure import Figure
11
11
  from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas
12
12
  from matplotlib.ticker import MaxNLocator
13
13
 
@@ -37,8 +37,11 @@ class AutoGuidingWidget(BaseWidget, Ui_AutoGuidingWidget):
37
37
  self._offset: tuple[float, float] | None = None # last (lon, lat), arcsec
38
38
  self._offset_history: deque[tuple[float, float]] = deque(maxlen=_HISTORY_LENGTH)
39
39
 
40
- # add plots: offset magnitude vs. sample, and the lon/lat scatter
41
- self.figure, (self.ax, self.ax2) = plt.subplots(1, 2)
40
+ # add plots: offset magnitude vs. sample, and the lon/lat scatter -- built via the plain
41
+ # OO Figure API, not pyplot: see comment in acquisitionwidget.py for why (avoids a
42
+ # several-second first-use event-loop stall)
43
+ self.figure = Figure()
44
+ self.ax, self.ax2 = self.figure.subplots(1, 2)
42
45
  layout = QtWidgets.QVBoxLayout(self.framePlot)
43
46
  self.canvas = FigureCanvas(self.figure)
44
47
  layout.addWidget(self.canvas)
@@ -57,6 +57,15 @@ class CameraWidget(BaseWidget, Ui_CameraWidget):
57
57
  BaseWidget.__init__(self, **kwargs)
58
58
  self.setupUi(self) # type: ignore
59
59
 
60
+ # scrollArea's horizontal scrollbar is off (this panel should never need to scroll
61
+ # sideways), but with widgetResizable=True that leaves nothing to widen the scroll area
62
+ # itself to fit its content -- it was clipping the right edge of every group box
63
+ # (values/buttons ran past the visible border) because the .ui's Designer-time width hint
64
+ # (239px) is narrower than the controls' actual required width. Size it from the real
65
+ # content instead of a hardcoded guess; recomputed again at the end of _init() once
66
+ # capability-dependent combo boxes (binning, image format) have their real items.
67
+ self._size_scroll_area_to_content()
68
+
60
69
  # variables
61
70
  self.new_image = False
62
71
  self.image_filename = None
@@ -130,9 +139,16 @@ class CameraWidget(BaseWidget, Ui_CameraWidget):
130
139
  self._init_once("data_sequence", self._init_data_sequence),
131
140
  )
132
141
 
142
+ # capability-dependent combo boxes (binning, image format) may have widened the panel
143
+ # further, now that they're populated
144
+ self._size_scroll_area_to_content()
145
+
133
146
  # update GUI
134
147
  self.signal_update_gui.emit()
135
148
 
149
+ def _size_scroll_area_to_content(self) -> None:
150
+ self.scrollArea.setMinimumWidth(self.scrollAreaWidgetContents.sizeHint().width())
151
+
136
152
  async def _init_window(self) -> None:
137
153
  # window
138
154
  window_caps = await self.comm.get_capabilities(self.module, IWindow)
@@ -167,7 +183,7 @@ class CameraWidget(BaseWidget, Ui_CameraWidget):
167
183
  state = await proxy.wait_for_state(IBinning, timeout=_WAIT_FOR_STATE_TIMEOUT)
168
184
  if state is not None:
169
185
  self.comboBinning.setCurrentText(f"{state.x}x{state.y}")
170
- await self.comm.subscribe_state(self.module, IBinning, self._update_binning)
186
+ await self.comm.subscribe_state(self.module, IBinning, self._update_binning)
171
187
 
172
188
  async def _init_gain(self) -> None:
173
189
  # gain
@@ -177,7 +193,7 @@ class CameraWidget(BaseWidget, Ui_CameraWidget):
177
193
  if state is not None:
178
194
  self.spinGain.setValue(state.gain)
179
195
  self.spinGainOffset.setValue(state.offset)
180
- await self.comm.subscribe_state(self.module, IGain, self._update_gain)
196
+ await self.comm.subscribe_state(self.module, IGain, self._update_gain)
181
197
 
182
198
  async def _init_image_format(self) -> None:
183
199
  # image format
@@ -191,7 +207,7 @@ class CameraWidget(BaseWidget, Ui_CameraWidget):
191
207
  state = await proxy.wait_for_state(IImageFormat, timeout=_WAIT_FOR_STATE_TIMEOUT)
192
208
  if state is not None:
193
209
  self.comboImageFormat.setCurrentText(state.image_format.name)
194
- await self.comm.subscribe_state(self.module, IImageFormat, self._update_image_format)
210
+ await self.comm.subscribe_state(self.module, IImageFormat, self._update_image_format)
195
211
 
196
212
  async def _init_image_type(self) -> None:
197
213
  # image type
@@ -200,7 +216,7 @@ class CameraWidget(BaseWidget, Ui_CameraWidget):
200
216
  state = await proxy.wait_for_state(IImageType, timeout=_WAIT_FOR_STATE_TIMEOUT)
201
217
  if state is not None:
202
218
  self.comboImageType.setCurrentText(state.image_type.name)
203
- await self.comm.subscribe_state(self.module, IImageType, self._update_image_type)
219
+ await self.comm.subscribe_state(self.module, IImageType, self._update_image_type)
204
220
 
205
221
  async def _init_exposure(self) -> None:
206
222
  # exposure (status, progress, time left)
@@ -211,7 +227,7 @@ class CameraWidget(BaseWidget, Ui_CameraWidget):
211
227
  self.exposure_status = state.status
212
228
  self.exposure_progress = state.progress
213
229
  self.exposure_time_left = state.exposure_time_left
214
- await self.comm.subscribe_state(self.module, IExposure, self._update_exposure)
230
+ await self.comm.subscribe_state(self.module, IExposure, self._update_exposure)
215
231
 
216
232
  async def _init_exposure_time(self) -> None:
217
233
  # exposure time
@@ -220,7 +236,7 @@ class CameraWidget(BaseWidget, Ui_CameraWidget):
220
236
  state = await proxy.wait_for_state(IExposureTime, timeout=_WAIT_FOR_STATE_TIMEOUT)
221
237
  if state is not None:
222
238
  self.spinExpTime.setValue(state.exposure_time)
223
- await self.comm.subscribe_state(self.module, IExposureTime, self._update_exposure_time)
239
+ await self.comm.subscribe_state(self.module, IExposureTime, self._update_exposure_time)
224
240
 
225
241
  async def _init_data_sequence(self) -> None:
226
242
  # data sequence
@@ -5,9 +5,9 @@ import os
5
5
  from typing import Any, TYPE_CHECKING
6
6
  import numpy as np
7
7
  from PySide6 import QtWidgets, QtCore # type: ignore
8
- from matplotlib import pyplot as plt
9
8
 
10
9
  os.environ["QT_API"] = "PySide6"
10
+ from matplotlib.figure import Figure
11
11
  from matplotlib.backends.backend_qtagg import NavigationToolbar2QT # pyrefly: ignore [missing-module-attribute]
12
12
  from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas
13
13
  from qfitswidget import QFitsWidget # type: ignore
@@ -19,7 +19,6 @@ from .base import BaseWidget
19
19
  from .qt.datadisplaywidget_ui import Ui_DataDisplayWidget
20
20
 
21
21
  if TYPE_CHECKING:
22
- from matplotlib.figure import Figure
23
22
  from matplotlib.axes import Axes
24
23
  from astropy.io import fits
25
24
 
@@ -66,7 +65,10 @@ class DataDisplayWidget(BaseWidget, Ui_DataDisplayWidget):
66
65
  self.imageLayout = QtWidgets.QVBoxLayout(self.tabImage)
67
66
  if await self.comm.has_proxy(self.module, ISpectrograph):
68
67
  self.is_spectrograph = True
69
- self.figure, self.ax = plt.subplots()
68
+ # built via the plain OO Figure API, not pyplot: see comment in acquisitionwidget.py
69
+ # for why (avoids a several-second first-use event-loop stall)
70
+ self.figure = Figure()
71
+ self.ax = self.figure.add_subplot()
70
72
  self.canvas = FigureCanvas(self.figure)
71
73
  self.plotTools = NavigationToolbar2QT(self.canvas, self.tabImage)
72
74
  self.imageLayout.addWidget(self.plotTools)
@@ -53,6 +53,7 @@ from .telescopewidget import TelescopeWidget
53
53
  from .focuswidget import FocusWidget
54
54
  from .temperatureswidget import TemperaturesWidget
55
55
  from .weatherwidget import WeatherWidget
56
+ from .videograbwidget import VideoGrabWidget
56
57
  from .videowidget import VideoWidget
57
58
  from .qt.mainwindow_ui import Ui_MainWindow
58
59
  from .logmodel import LogModel, LogModelProxy
@@ -83,8 +84,11 @@ class MainWidgetEntry:
83
84
  # cross-interface demotion: only promoted into `main` when nothing else matched (D1)
84
85
  sidebar_preferred: bool = False
85
86
  # same-interface pairing: always rendered together whenever this entry survives into `main`
86
- # (D6); no MAIN_WIDGETS entry sets this yet -- first consumer is the VideoWidget split,
87
- # tracked separately in specs/2026-09-01-gui-video-widget-split.md
87
+ # (D6); no MAIN_WIDGETS entry sets this yet. specs/2026-09-01-gui-video-widget-split.md's
88
+ # original plan was going to be the first consumer, but VideoWidget ended up split into two
89
+ # independent same-interface entries (see the two IVideo rows below) instead -- each tab is
90
+ # fully self-contained, with no state to share across a widget boundary, so this mechanism
91
+ # wasn't needed after all.
88
92
  paired_sidebar_widget: Optional[Type[BaseWidget]] = None
89
93
 
90
94
 
@@ -105,7 +109,16 @@ MAIN_WIDGETS: List[MainWidgetEntry] = [
105
109
  MainWidgetEntry(IAcquisition, AcquisitionWidget, "Acquisition", "mdi.target"),
106
110
  MainWidgetEntry(IAutoGuiding, AutoGuidingWidget, "Auto guiding", "mdi.crosshairs-gps"),
107
111
  MainWidgetEntry(IWeather, WeatherWidget, "Weather", "fa5s.cloud-sun"),
108
- MainWidgetEntry(IVideo, VideoWidget, "Video", "fa5s.video", sidebar=((None, FitsHeadersWidget),)),
112
+ # two entries sharing one interface: collect_main_widgets() matches every entry whose
113
+ # interface the module implements (D2), so both render as separate tabs for any IVideo
114
+ # module -- live view and the FITS-grab controls, fully independent widgets/state
115
+ # (previously one VideoWidget combining both; see
116
+ # specs/2026-09-01-gui-video-widget-split.md). Note: a custom `widgets:` config entry with
117
+ # `interface: IVideo` can only ever replace the first of these two matches (mainwindow.py's
118
+ # replace-in-place lookup keys on interface name, not registry position) -- a known, unused
119
+ # edge case, not a blocker for this pair.
120
+ MainWidgetEntry(IVideo, VideoWidget, "Live View", "fa5s.video"),
121
+ MainWidgetEntry(IVideo, VideoGrabWidget, "FITS Image", "fa5s.image", sidebar=((None, FitsHeadersWidget),)),
109
122
  MainWidgetEntry(ISpectrograph, SpectrographWidget, "Spectrograph", "ei.graph"),
110
123
  MainWidgetEntry(IFilters, FilterWidget, "Filter wheel", "mdi.air-filter", sidebar_preferred=True),
111
124
  MainWidgetEntry(ITemperatures, TemperaturesWidget, "Temperatures", "mdi.thermometer", sidebar_preferred=True),
@@ -24,6 +24,11 @@ class ModeWidget(BaseWidget, Ui_ModeWidget):
24
24
  self._mode_options: dict[str, list[str]] = {}
25
25
  self._modes: dict[str, str] = {}
26
26
  self._motion_status = MotionStatus.UNKNOWN
27
+ # whether self.module implements IMotion at all -- an IMode module doesn't have to be a
28
+ # motion device (e.g. a plain camera's fiber-hole/video-path selector), and gating the
29
+ # edit button on motion status for one that isn't left it permanently disabled: the
30
+ # IMotion state subscription below never fires, so _motion_status never leaves UNKNOWN
31
+ self._has_motion = False
27
32
  self._mode_widgets: dict[str, tuple[QtWidgets.QLineEdit, QtWidgets.QToolButton]] = {}
28
33
 
29
34
  # connect signals
@@ -42,7 +47,9 @@ class ModeWidget(BaseWidget, Ui_ModeWidget):
42
47
  await self.register_event(ModeChangedEvent, self._on_mode_changed)
43
48
 
44
49
  async def _init(self) -> None:
45
- await self.comm.subscribe_state(self.module, IMotion, self._on_motion_state)
50
+ self._has_motion = await self.comm.has_proxy(self.module, IMotion)
51
+ if self._has_motion:
52
+ await self.comm.subscribe_state(self.module, IMotion, self._on_motion_state)
46
53
 
47
54
  # permitted methods (ACLs)
48
55
  await self._fetch_permitted_methods()
@@ -80,7 +87,7 @@ class ModeWidget(BaseWidget, Ui_ModeWidget):
80
87
  def update_gui(self) -> None:
81
88
  self.setEnabled(True)
82
89
  self.textStatus.setText(self._motion_status.name)
83
- initialized = self._motion_status in [
90
+ initialized = not self._has_motion or self._motion_status in [
84
91
  MotionStatus.SLEWING,
85
92
  MotionStatus.TRACKING,
86
93
  MotionStatus.IDLE,
@@ -0,0 +1,257 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <ui version="4.0">
3
+ <class>VideoGrabWidget</class>
4
+ <widget class="QWidget" name="VideoGrabWidget">
5
+ <property name="geometry">
6
+ <rect>
7
+ <x>0</x>
8
+ <y>0</y>
9
+ <width>618</width>
10
+ <height>530</height>
11
+ </rect>
12
+ </property>
13
+ <property name="windowTitle">
14
+ <string>Form</string>
15
+ </property>
16
+ <layout class="QHBoxLayout" name="horizontalLayout_3" stretch="0,0">
17
+ <item>
18
+ <widget class="QFrame" name="frame_2">
19
+ <property name="frameShape">
20
+ <enum>QFrame::Shape::StyledPanel</enum>
21
+ </property>
22
+ <property name="frameShadow">
23
+ <enum>QFrame::Shadow::Raised</enum>
24
+ </property>
25
+ <layout class="QVBoxLayout" name="verticalLayout">
26
+ <item>
27
+ <widget class="QGroupBox" name="groupExposure_2">
28
+ <property name="title">
29
+ <string>Exposure</string>
30
+ </property>
31
+ <layout class="QGridLayout" name="gridLayout_5">
32
+ <property name="spacing">
33
+ <number>0</number>
34
+ </property>
35
+ <item row="0" column="0">
36
+ <widget class="QLabel" name="labelImageType">
37
+ <property name="text">
38
+ <string>Type:</string>
39
+ </property>
40
+ </widget>
41
+ </item>
42
+ <item row="1" column="0">
43
+ <widget class="QLabel" name="label_9">
44
+ <property name="text">
45
+ <string>Count:</string>
46
+ </property>
47
+ </widget>
48
+ </item>
49
+ <item row="2" column="1">
50
+ <widget class="QCheckBox" name="checkBroadcast">
51
+ <property name="text">
52
+ <string>Broadcast</string>
53
+ </property>
54
+ <property name="checked">
55
+ <bool>true</bool>
56
+ </property>
57
+ </widget>
58
+ </item>
59
+ <item row="3" column="0" colspan="2">
60
+ <widget class="QPushButton" name="buttonGrabImage">
61
+ <property name="palette">
62
+ <palette>
63
+ <active>
64
+ <colorrole role="Button">
65
+ <brush brushstyle="SolidPattern">
66
+ <color alpha="255">
67
+ <red>0</red>
68
+ <green>85</green>
69
+ <blue>0</blue>
70
+ </color>
71
+ </brush>
72
+ </colorrole>
73
+ <colorrole role="ButtonText">
74
+ <brush brushstyle="SolidPattern">
75
+ <color alpha="255">
76
+ <red>255</red>
77
+ <green>255</green>
78
+ <blue>255</blue>
79
+ </color>
80
+ </brush>
81
+ </colorrole>
82
+ </active>
83
+ <inactive>
84
+ <colorrole role="Button">
85
+ <brush brushstyle="SolidPattern">
86
+ <color alpha="255">
87
+ <red>0</red>
88
+ <green>85</green>
89
+ <blue>0</blue>
90
+ </color>
91
+ </brush>
92
+ </colorrole>
93
+ <colorrole role="ButtonText">
94
+ <brush brushstyle="SolidPattern">
95
+ <color alpha="255">
96
+ <red>255</red>
97
+ <green>255</green>
98
+ <blue>255</blue>
99
+ </color>
100
+ </brush>
101
+ </colorrole>
102
+ </inactive>
103
+ <disabled>
104
+ <colorrole role="Button">
105
+ <brush brushstyle="SolidPattern">
106
+ <color alpha="255">
107
+ <red>0</red>
108
+ <green>85</green>
109
+ <blue>0</blue>
110
+ </color>
111
+ </brush>
112
+ </colorrole>
113
+ <colorrole role="ButtonText">
114
+ <brush brushstyle="SolidPattern">
115
+ <color alpha="255">
116
+ <red>0</red>
117
+ <green>42</green>
118
+ <blue>0</blue>
119
+ </color>
120
+ </brush>
121
+ </colorrole>
122
+ </disabled>
123
+ </palette>
124
+ </property>
125
+ <property name="text">
126
+ <string>Grab image</string>
127
+ </property>
128
+ </widget>
129
+ </item>
130
+ <item row="1" column="1">
131
+ <widget class="QSpinBox" name="spinCount">
132
+ <property name="minimum">
133
+ <number>1</number>
134
+ </property>
135
+ <property name="maximum">
136
+ <number>9999</number>
137
+ </property>
138
+ </widget>
139
+ </item>
140
+ <item row="0" column="1">
141
+ <widget class="QComboBox" name="comboImageType"/>
142
+ </item>
143
+ <item row="4" column="0" colspan="2">
144
+ <widget class="QPushButton" name="buttonAbort">
145
+ <property name="palette">
146
+ <palette>
147
+ <active>
148
+ <colorrole role="Button">
149
+ <brush brushstyle="SolidPattern">
150
+ <color alpha="255">
151
+ <red>170</red>
152
+ <green>0</green>
153
+ <blue>0</blue>
154
+ </color>
155
+ </brush>
156
+ </colorrole>
157
+ <colorrole role="ButtonText">
158
+ <brush brushstyle="SolidPattern">
159
+ <color alpha="255">
160
+ <red>0</red>
161
+ <green>0</green>
162
+ <blue>0</blue>
163
+ </color>
164
+ </brush>
165
+ </colorrole>
166
+ </active>
167
+ <inactive>
168
+ <colorrole role="Button">
169
+ <brush brushstyle="SolidPattern">
170
+ <color alpha="255">
171
+ <red>170</red>
172
+ <green>0</green>
173
+ <blue>0</blue>
174
+ </color>
175
+ </brush>
176
+ </colorrole>
177
+ <colorrole role="ButtonText">
178
+ <brush brushstyle="SolidPattern">
179
+ <color alpha="255">
180
+ <red>0</red>
181
+ <green>0</green>
182
+ <blue>0</blue>
183
+ </color>
184
+ </brush>
185
+ </colorrole>
186
+ </inactive>
187
+ <disabled>
188
+ <colorrole role="Button">
189
+ <brush brushstyle="SolidPattern">
190
+ <color alpha="255">
191
+ <red>170</red>
192
+ <green>0</green>
193
+ <blue>0</blue>
194
+ </color>
195
+ </brush>
196
+ </colorrole>
197
+ <colorrole role="ButtonText">
198
+ <brush brushstyle="SolidPattern">
199
+ <color alpha="255">
200
+ <red>85</red>
201
+ <green>0</green>
202
+ <blue>0</blue>
203
+ </color>
204
+ </brush>
205
+ </colorrole>
206
+ </disabled>
207
+ </palette>
208
+ </property>
209
+ <property name="text">
210
+ <string>Abort sequence</string>
211
+ </property>
212
+ </widget>
213
+ </item>
214
+ </layout>
215
+ </widget>
216
+ </item>
217
+ <item>
218
+ <spacer name="verticalSpacer_2">
219
+ <property name="orientation">
220
+ <enum>Qt::Orientation::Vertical</enum>
221
+ </property>
222
+ <property name="sizeHint" stdset="0">
223
+ <size>
224
+ <width>20</width>
225
+ <height>40</height>
226
+ </size>
227
+ </property>
228
+ </spacer>
229
+ </item>
230
+ <item>
231
+ <widget class="QLabel" name="labelExposuresLeft">
232
+ <property name="text">
233
+ <string>IDLE</string>
234
+ </property>
235
+ <property name="alignment">
236
+ <set>Qt::AlignmentFlag::AlignCenter</set>
237
+ </property>
238
+ </widget>
239
+ </item>
240
+ </layout>
241
+ </widget>
242
+ </item>
243
+ <item>
244
+ <widget class="DataDisplayWidget" name="datadisplay" native="true"/>
245
+ </item>
246
+ </layout>
247
+ </widget>
248
+ <customwidgets>
249
+ <customwidget>
250
+ <class>DataDisplayWidget</class>
251
+ <extends>QWidget</extends>
252
+ <header>..datadisplaywidget</header>
253
+ </customwidget>
254
+ </customwidgets>
255
+ <resources/>
256
+ <connections/>
257
+ </ui>