pyobs-gui 2.4.0__tar.gz → 2.4.2__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 (173) hide show
  1. pyobs_gui-2.4.2/CLAUDE.md +26 -0
  2. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/PKG-INFO +3 -3
  3. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/gui.py +6 -0
  4. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/mainwindow.py +84 -2
  5. pyobs_gui-2.4.2/pyobs_gui/movestack.py +18 -0
  6. pyobs_gui-2.4.2/pyobs_gui/nowheelfilter.py +56 -0
  7. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/qt/filterwidget.ui +3 -0
  8. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/qt/filterwidget_ui.py +2 -1
  9. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/qt/focuswidget.ui +3 -0
  10. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/qt/focuswidget_ui.py +2 -1
  11. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/qt/mainwindow.ui +17 -3
  12. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/qt/mainwindow_ui.py +12 -5
  13. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/qt/telescopewidget.ui +9 -1
  14. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/qt/telescopewidget_ui.py +6 -4
  15. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/telescopewidget.py +12 -0
  16. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyproject.toml +3 -3
  17. pyobs_gui-2.4.2/specs/2026-09-14-fitswidget-toolbar-overflow.md +183 -0
  18. pyobs_gui-2.4.2/specs/2026-09-14-stacked-widget-scroll-fallback.md +87 -0
  19. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/specs/index.md +17 -3
  20. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/uv.lock +85 -78
  21. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/.claude/skills/verify/SKILL.md +0 -0
  22. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/.github/dependabot.yml +0 -0
  23. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/.github/workflows/dependabot-automerge.yml +0 -0
  24. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/.github/workflows/pypi.yml +0 -0
  25. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/.github/workflows/pyrefly.yml +0 -0
  26. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/.github/workflows/ruff.yml +0 -0
  27. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/.gitignore +0 -0
  28. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/.pre-commit-config.yaml +0 -0
  29. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/.readthedocs.yml +0 -0
  30. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/CHANGELOG.rst +0 -0
  31. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/LICENSE +0 -0
  32. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/README.md +0 -0
  33. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/bin/mypyuic5 +0 -0
  34. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/docs/Makefile +0 -0
  35. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/docs/make.bat +0 -0
  36. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/docs/requirements.txt +0 -0
  37. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/docs/source/_static/pyobs.gif +0 -0
  38. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/docs/source/conf.py +0 -0
  39. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/docs/source/index.rst +0 -0
  40. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/mypy.ini +0 -0
  41. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/__init__.py +0 -0
  42. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/__main__.py +0 -0
  43. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/_nuitka_astropy_patch.py +0 -0
  44. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/accounts.py +0 -0
  45. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/acquisitionwidget.py +0 -0
  46. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/autofocuswidget.py +0 -0
  47. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/autoguidingwidget.py +0 -0
  48. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/base.py +0 -0
  49. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/camerawidget.py +0 -0
  50. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/commandinputwidget.py +0 -0
  51. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/compassmovewidget.py +0 -0
  52. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/coolingwidget.py +0 -0
  53. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/datadisplaywidget.py +0 -0
  54. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/eventswidget.py +0 -0
  55. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/filterwidget.py +0 -0
  56. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/fitsheaderswidget.py +0 -0
  57. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/focuswidget.py +0 -0
  58. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/login.py +0 -0
  59. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/loginwindow.py +0 -0
  60. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/logmodel.py +0 -0
  61. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/modewidget.py +0 -0
  62. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/modifiedmixin.py +0 -0
  63. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/modulegui.py +0 -0
  64. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/qt/__init__.py +0 -0
  65. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/qt/acquisitionwidget.ui +0 -0
  66. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/qt/acquisitionwidget_ui.py +0 -0
  67. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/qt/autofocuswidget.ui +0 -0
  68. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/qt/autofocuswidget_ui.py +0 -0
  69. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/qt/autoguidingwidget.ui +0 -0
  70. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/qt/autoguidingwidget_ui.py +0 -0
  71. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/qt/camerawidget.ui +0 -0
  72. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/qt/camerawidget_ui.py +0 -0
  73. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/qt/compassmoveplugin.py +0 -0
  74. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/qt/compassmovewidget.py +0 -0
  75. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/qt/compassmovewidget.ui +0 -0
  76. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/qt/compassmovewidget_ui.py +0 -0
  77. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/qt/compile.sh +0 -0
  78. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/qt/coolingwidget.ui +0 -0
  79. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/qt/coolingwidget_ui.py +0 -0
  80. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/qt/datadisplayplugin.py +0 -0
  81. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/qt/datadisplaywidget.ui +0 -0
  82. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/qt/datadisplaywidget_ui.py +0 -0
  83. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/qt/designer.sh +0 -0
  84. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/qt/eventswidget.ui +0 -0
  85. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/qt/eventswidget_ui.py +0 -0
  86. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/qt/fitsheaderswidget.ui +0 -0
  87. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/qt/fitsheaderswidget_ui.py +0 -0
  88. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/qt/modewidget.ui +0 -0
  89. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/qt/modewidget_ui.py +0 -0
  90. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/qt/resources/arrow-alt-circle-down-solid.svg +0 -0
  91. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/qt/resources/arrow-alt-circle-left-solid.svg +0 -0
  92. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/qt/resources/arrow-alt-circle-right-solid.svg +0 -0
  93. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/qt/resources/arrow-alt-circle-up-solid.svg +0 -0
  94. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/qt/resources/arrow-up-right-from-square-solid.svg +0 -0
  95. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/qt/resources/arrows-to-eye-solid.svg +0 -0
  96. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/qt/resources/chart-line-solid.svg +0 -0
  97. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/qt/resources/circle-question-solid.svg +0 -0
  98. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/qt/resources/cloud-sun-solid.svg +0 -0
  99. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/qt/resources/edit-solid.svg +0 -0
  100. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/qt/resources/search-solid.svg +0 -0
  101. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/qt/resources/undo-solid.svg +0 -0
  102. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/qt/resources.qrc +0 -0
  103. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/qt/resources_rc.py +0 -0
  104. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/qt/roboticwidget.ui +0 -0
  105. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/qt/roboticwidget_ui.py +0 -0
  106. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/qt/roofwidget.ui +0 -0
  107. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/qt/roofwidget_ui.py +0 -0
  108. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/qt/schedulewidget.ui +0 -0
  109. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/qt/schedulewidget_ui.py +0 -0
  110. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/qt/shellwidget.ui +0 -0
  111. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/qt/shellwidget_ui.py +0 -0
  112. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/qt/spectrographwidget.ui +0 -0
  113. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/qt/spectrographwidget_ui.py +0 -0
  114. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/qt/temperaturesplotwidget.ui +0 -0
  115. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/qt/temperaturesplotwidget_ui.py +0 -0
  116. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/qt/temperatureswidget.ui +0 -0
  117. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/qt/temperatureswidget_ui.py +0 -0
  118. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/qt/videograbwidget.ui +0 -0
  119. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/qt/videograbwidget_ui.py +0 -0
  120. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/qt/videowidget.ui +0 -0
  121. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/qt/videowidget_ui.py +0 -0
  122. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/qt/weatherwidget.ui +0 -0
  123. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/qt/weatherwidget_ui.py +0 -0
  124. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/roboticwidget.py +0 -0
  125. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/roofwidget.py +0 -0
  126. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/schedulewidget.py +0 -0
  127. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/shellwidget.py +0 -0
  128. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/spectrographwidget.py +0 -0
  129. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/statuswidget.py +0 -0
  130. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/structuredconfigwidget.py +0 -0
  131. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/temperaturesplotwidget.py +0 -0
  132. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/temperatureswidget.py +0 -0
  133. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/utils.py +0 -0
  134. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/videograbwidget.py +0 -0
  135. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/videowidget.py +0 -0
  136. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/watchedlabel.py +0 -0
  137. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pyobs_gui/weatherwidget.py +0 -0
  138. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/pysidedeploy.spec +0 -0
  139. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/specs/2026-08-20-gui-remote-call-error-handling.md +0 -0
  140. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/specs/2026-08-21-gui-widget-startup-responsiveness.md +0 -0
  141. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/specs/2026-08-28-gui-main-vs-sidebar-widgets.md +0 -0
  142. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/specs/2026-08-28-structuredconfig-widget.md +0 -0
  143. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/specs/2026-08-31-irobotic-widgets.md +0 -0
  144. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/specs/2026-09-01-gui-video-widget-split.md +0 -0
  145. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/spike_standalone/main.py +0 -0
  146. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/spike_standalone/pysidedeploy.spec +0 -0
  147. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/test/acquisition.yaml +0 -0
  148. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/test/autofocus.yaml +0 -0
  149. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/test/camera.yaml +0 -0
  150. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/test/full.yaml +0 -0
  151. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/test/guiding.yaml +0 -0
  152. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/test/mode.yaml +0 -0
  153. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/test/robotic.yaml +0 -0
  154. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/test/roof.yaml +0 -0
  155. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/test/spectrograph.yaml +0 -0
  156. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/test/structuredconfig.yaml +0 -0
  157. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/test/telescope.yaml +0 -0
  158. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/test/telescope_acl.yaml +0 -0
  159. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/test/telescope_acl_denied.yaml +0 -0
  160. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/test/video.yaml +0 -0
  161. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/tests/__init__.py +0 -0
  162. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/tests/conftest.py +0 -0
  163. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/tests/test_accounts.py +0 -0
  164. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/tests/test_camerawidget.py +0 -0
  165. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/tests/test_loginwindow.py +0 -0
  166. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/tests/test_mainwindow_startup.py +0 -0
  167. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/tests/test_modewidget.py +0 -0
  168. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/tests/test_multiwidget_pages.py +0 -0
  169. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/tests/test_roboticwidget.py +0 -0
  170. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/tests/test_schedulewidget.py +0 -0
  171. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/tests/test_structuredconfigwidget.py +0 -0
  172. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/tests/test_videograbwidget.py +0 -0
  173. {pyobs_gui-2.4.0 → pyobs_gui-2.4.2}/tests/test_videowidget.py +0 -0
@@ -0,0 +1,26 @@
1
+ # CLAUDE.md
2
+
3
+ Entry points for working in this repo.
4
+
5
+ ## What this is
6
+
7
+ `pyobs-gui` is a PySide6/Qt GUI for operating a whole `pyobs` system: one page per connected
8
+ module (camera, telescope, roof, focuser, ...), plus Shell/Events/Status tool pages. Run standalone
9
+ with interactive login (`uv run pyobs-gui`) or via the standard `pyobs` CLI with a config file
10
+ whose top-level class is `pyobs_gui.GUI` (see `docs/source/index.rst`).
11
+
12
+ ## Design history and planning
13
+
14
+ This repo keeps its own implementation plans directly under `specs/` (`YYYY-MM-DD-<slug>.md`,
15
+ listed in `specs/index.md`), following the same conventions as `pyobs-core`'s `specs/` tree. Older
16
+ design docs, plans, and ADRs that concern `pyobs-gui` live in `pyobs-core`'s `specs/` tree instead,
17
+ tagged with a `Repos:` line — see `specs/index.md` for the current cross-repo list and
18
+ `pyobs-core/CLAUDE.md`'s "Cross-repo docs" section for the convention.
19
+
20
+ ## Tooling
21
+
22
+ - Lint: `ruff` (config in `pyproject.toml`; excludes `pyobs_gui/qt/`)
23
+ - Format: `black`
24
+ - Type checking: `pyrefly` (`project-includes = ["pyobs_gui", "tests"]`; a `mypy.ini` also exists
25
+ in the repo root but is not the active type checker — use `pyrefly`)
26
+ - Tests: `pytest` (`asyncio_mode = "strict"`)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pyobs-gui
3
- Version: 2.4.0
3
+ Version: 2.4.2
4
4
  Summary: A remote GUI for pyobs
5
5
  Author-email: Tim-Oliver Husser <thusser@uni-goettingen.de>
6
6
  License-Expression: MIT
@@ -11,10 +11,10 @@ Requires-Dist: astropy<8,>=7.0.1
11
11
  Requires-Dist: colour<0.2,>=0.1.5
12
12
  Requires-Dist: keyring<26,>=25.7.0
13
13
  Requires-Dist: matplotlib<4,>=3.10.1
14
- Requires-Dist: pyobs-core<3,>=2.7.1
14
+ Requires-Dist: pyobs-core<3,>=2.7.2
15
15
  Requires-Dist: pyside6>=6.10.1
16
16
  Requires-Dist: qasync<0.29,>=0.27.1
17
- Requires-Dist: qfitswidget>=0.13.1
17
+ Requires-Dist: qfitswidget>=1.1.3
18
18
  Requires-Dist: qtawesome<2,>=1.4.0
19
19
  Requires-Dist: secretstorage<4,>=3.5.0
20
20
  Requires-Dist: sunpy[all]>=7.0.1
@@ -11,6 +11,7 @@ if TYPE_CHECKING:
11
11
  from pyobs.interfaces import FitsHeaderEntry, IFitsHeaderBefore
12
12
  from pyobs.modules import Module
13
13
 
14
+ from .nowheelfilter import NoWheelWhenUnfocused
14
15
  from .utils import QAsyncMessageBox
15
16
 
16
17
  log = logging.getLogger(__name__)
@@ -20,6 +21,9 @@ class GUI(Module, IFitsHeaderBefore):
20
21
  __module__ = "pyobs_gui"
21
22
 
22
23
  app: QtWidgets.QApplication | None = None
24
+ # kept alive here -- QApplication.installEventFilter() doesn't itself keep a Python reference,
25
+ # and an unparented QObject with nothing else referencing it is fair game for GC
26
+ _wheel_filter: NoWheelWhenUnfocused | None = None
23
27
 
24
28
  def __init__(
25
29
  self,
@@ -67,6 +71,8 @@ class GUI(Module, IFitsHeaderBefore):
67
71
  # "quit when the last window closes" would tear down the whole event loop right there,
68
72
  # so this app manages its own lifetime exclusively through Module.quit() instead.
69
73
  GUI.app.setQuitOnLastWindowClosed(False)
74
+ GUI._wheel_filter = NoWheelWhenUnfocused()
75
+ GUI.app.installEventFilter(GUI._wheel_filter)
70
76
  return cast("asyncio.AbstractEventLoop", qasync.QEventLoop(GUI.app))
71
77
 
72
78
  async def open(self) -> None:
@@ -288,6 +288,9 @@ class ModulePage(BaseWidget):
288
288
  # this page's widgets have actually opened -- see its sidebar-fill steps (c) and (e)
289
289
  self.sidebar_preferred_choices: List[WidgetChoice] = sidebar_preferred or []
290
290
  self.custom_sidebar: List[Dict[str, Any]] = custom_sidebar or []
291
+ # user's manual show/hide choice via sidebar_toggle -- independent of
292
+ # hide_if_empty_sidebar()'s content-driven hiding, see _update_sidebar_visibility()
293
+ self._sidebar_collapsed = False
291
294
 
292
295
  layout = QtWidgets.QHBoxLayout(self)
293
296
  layout.setContentsMargins(0, 0, 0, 0)
@@ -311,12 +314,50 @@ class ModulePage(BaseWidget):
311
314
  self.sidebar_scroll.setWidget(self.widgetSidebar)
312
315
  self.sidebar_scroll.setWidgetResizable(True)
313
316
  self.sidebar_scroll.setFrameShape(QtWidgets.QFrame.Shape.NoFrame)
317
+ # No horizontal scrollbar -- deliberately. Without a real minimum width, that would leave
318
+ # the sidebar's own content as the only thing standing between "fits" and "silently
319
+ # clipped" once something upstream (MainWindow.resizeEvent's forced splitterNav.setSizes())
320
+ # squeezes this pane below its natural size. _update_sidebar_min_width() is what actually
321
+ # keeps it from getting that small in the first place -- see there.
314
322
  self.sidebar_scroll.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
315
323
  self.sidebar_scroll.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarPolicy.ScrollBarAsNeeded)
316
- self.sidebar_scroll.setMaximumWidth(320)
317
324
  self.sidebar_scroll.setVisible(False)
325
+ # _update_sidebar_min_width() (called below and on every add_to_sidebar()) sets both the
326
+ # min and max width from real content -- nothing hardcoded here to get stale
327
+
318
328
  layout.addWidget(self.sidebar_scroll)
319
329
 
330
+ # manual show/hide toggle -- independent of the automatic content-driven hiding in
331
+ # hide_if_empty_sidebar(). Lets someone reclaim the sidebar's width on a narrow window
332
+ # without that width being a permanent, un-opt-outable floor.
333
+ #
334
+ # Deliberately NOT a layout item (a QHBoxLayout cell always spans the full row height, so
335
+ # a small button in its own cell reads as a tall, mostly-empty column -- confirmed against
336
+ # a real screenshot, not just theory). Instead it's a small floating overlay, positioned by
337
+ # _position_sidebar_toggle(), parented directly on `self` and raised above everything else.
338
+ self.sidebar_toggle = QtWidgets.QToolButton(self)
339
+ self.sidebar_toggle.setAutoRaise(True)
340
+ self.sidebar_toggle.clicked.connect(self._toggle_sidebar)
341
+ self.sidebar_toggle.setVisible(False)
342
+ self._update_sidebar_visibility()
343
+
344
+ def resizeEvent(self, event: QtGui.QResizeEvent) -> None:
345
+ super().resizeEvent(event)
346
+ self._position_sidebar_toggle()
347
+
348
+ def _position_sidebar_toggle(self) -> None:
349
+ """Pins the toggle just outside the sidebar's left edge when it's shown (a compact handle
350
+ right at the boundary, not a dedicated column), or to the page's right edge when it's
351
+ collapsed (so there's still something to click to bring it back)."""
352
+ margin = 4
353
+ y = margin
354
+ if self.sidebar_scroll.isVisible():
355
+ x = self.sidebar_scroll.x() - self.sidebar_toggle.width() - margin
356
+ else:
357
+ x = self.width() - self.sidebar_toggle.width() - margin
358
+ self.sidebar_toggle.move(max(0, x), y)
359
+ self.sidebar_toggle.raise_()
360
+
320
361
  def remove_widget(self, widget: BaseWidget) -> None:
321
362
  """Removes one tab (D5: a partially-failed open drops just that widget's tab, keeping
322
363
  the rest of the page). No-op if `widget` is the page's sole (non-tabbed) content --
@@ -329,8 +370,49 @@ class ModulePage(BaseWidget):
329
370
  if idx != -1:
330
371
  self.tab_widget.removeTab(idx)
331
372
 
373
+ async def add_to_sidebar(self, widget: BaseWidget) -> None:
374
+ await super().add_to_sidebar(widget)
375
+ self._update_sidebar_min_width()
376
+
377
+ _SIDEBAR_DEFAULT_MAX_WIDTH = 320
378
+
379
+ def _update_sidebar_min_width(self) -> None:
380
+ """Give sidebar_scroll a real minimum width tracking its content, so the horizontal
381
+ scrollbar being off (see __init__) doesn't mean content silently gets compressed below
382
+ what it needs instead.
383
+
384
+ Previous version capped the minimum at the same 320px the column's maximumWidth uses,
385
+ on the theory that content needing more than that was already going to clip and this
386
+ method wasn't responsible for that case -- wrong in practice: real content (a populated
387
+ FITS-headers table, Temperatures/Cooling widgets) routinely exceeds 320px, and capping
388
+ the *minimum* at the same value as the *maximum* left no room between them, so it clipped
389
+ regardless of how correct the minimum-width mechanism otherwise was. The maximum now
390
+ grows to match real content instead: 320px stays the default ceiling for typical small
391
+ content (letting it size elastically within that range, same as before), but never
392
+ shrinks below what's actually needed."""
393
+ natural = self.widgetSidebar.sizeHint().width()
394
+ self.sidebar_scroll.setMaximumWidth(max(natural, self._SIDEBAR_DEFAULT_MAX_WIDTH))
395
+ self.sidebar_scroll.setMinimumWidth(natural)
396
+
332
397
  def hide_if_empty_sidebar(self) -> None:
333
- self.sidebar_scroll.setVisible(len(self.sidebar_widgets) > 0)
398
+ self._update_sidebar_visibility()
399
+
400
+ def _toggle_sidebar(self) -> None:
401
+ self._sidebar_collapsed = not self._sidebar_collapsed
402
+ self._update_sidebar_visibility()
403
+
404
+ def _update_sidebar_visibility(self) -> None:
405
+ has_content = len(self.sidebar_widgets) > 0
406
+ shown = has_content and not self._sidebar_collapsed
407
+ self.sidebar_toggle.setVisible(has_content)
408
+ self.sidebar_scroll.setVisible(shown)
409
+ # icon points the direction the *next click* moves the sidebar, not the current state:
410
+ # left = "click to bring it in and show it", right = "click to push it out and hide it"
411
+ self.sidebar_toggle.setIcon(qta.icon("fa5s.angle-double-right" if shown else "fa5s.angle-double-left"))
412
+ self.sidebar_toggle.setToolTip("Hide sidebar" if shown else "Show sidebar")
413
+ # sidebar_scroll's geometry only settles after Qt processes this visibility/layout change
414
+ # -- queue the reposition for right after that, rather than reading stale geometry now.
415
+ QtCore.QTimer.singleShot(0, self._position_sidebar_toggle)
334
416
 
335
417
  async def discard(self) -> None:
336
418
  for widget in list(self.widgets):
@@ -0,0 +1,18 @@
1
+ from PySide6 import QtCore, QtWidgets # type: ignore
2
+
3
+
4
+ class MoveStack(QtWidgets.QStackedWidget):
5
+ """QStackedWidget sizes itself to the largest of all its pages by default, so
6
+ TelescopeWidget's coordinate-type stack (RA/Dec, Alt/Az, Orbit Elements, ...) reserves width
7
+ for its widest page even when a narrower one is shown -- and every new coordinate-type page
8
+ added in the future raises that floor further, permanently, for every module using this
9
+ widget. Size to the current page instead; call updateGeometry() after switching pages so the
10
+ new size actually takes effect."""
11
+
12
+ def sizeHint(self) -> QtCore.QSize:
13
+ current = self.currentWidget()
14
+ return current.sizeHint() if current is not None else super().sizeHint()
15
+
16
+ def minimumSizeHint(self) -> QtCore.QSize:
17
+ current = self.currentWidget()
18
+ return current.minimumSizeHint() if current is not None else super().minimumSizeHint()
@@ -0,0 +1,56 @@
1
+ from PySide6 import QtCore, QtWidgets # type: ignore
2
+
3
+
4
+ class NoWheelWhenUnfocused(QtCore.QObject):
5
+ """Application-wide event filter: a spin box/combo box/slider that doesn't have keyboard focus
6
+ ignores mouse wheel events instead of changing its value, and forwards the wheel event to the
7
+ nearest scrollable ancestor instead -- so scrolling a page that happens to pass the cursor over
8
+ one of these controls scrolls the page, not the control underneath the cursor. Confirmed a real
9
+ problem, not a hypothetical one: sending a wheel event to an unfocused QDoubleSpinBox changed
10
+ its value before this filter existed (see
11
+ ../specs/2026-09-14-stacked-widget-scroll-fallback.md, risk #1) -- became a live concern once
12
+ stackedWidget started living inside a QScrollArea, where a normal "scroll the page" gesture can
13
+ easily pass over a spin/combo box.
14
+
15
+ Install once, on QApplication (see gui.py's GUI.new_event_loop()). Focusing the control
16
+ (click or Tab) restores the normal wheel-changes-value behavior, same as before this filter
17
+ existed -- this only changes what happens when the control merely has the mouse over it.
18
+ """
19
+
20
+ _WHEEL_ADJUSTABLE = (QtWidgets.QAbstractSpinBox, QtWidgets.QComboBox, QtWidgets.QAbstractSlider)
21
+
22
+ def __init__(self) -> None:
23
+ super().__init__()
24
+ # re-entrancy guard: sendEvent() below re-enters this same app-wide filter (nested scroll
25
+ # areas are common -- e.g. CameraWidget's own left-panel scrollArea sits between a spinbox
26
+ # and the outer stackedWidgetScroll), and if the forwarded copy also lands on an unfocused
27
+ # adjustable control, that's infinite recursion without this. While forwarding, every event
28
+ # just passes through untouched instead of being re-evaluated.
29
+ self._forwarding = False
30
+
31
+ def eventFilter(self, watched: QtCore.QObject, event: QtCore.QEvent) -> bool:
32
+ if self._forwarding:
33
+ return False
34
+ if (
35
+ event.type() == QtCore.QEvent.Type.Wheel
36
+ and isinstance(watched, self._WHEEL_ADJUSTABLE)
37
+ and not watched.hasFocus()
38
+ ):
39
+ scroll_area = self._nearest_scroll_area(watched)
40
+ if scroll_area is not None:
41
+ self._forwarding = True
42
+ try:
43
+ QtWidgets.QApplication.sendEvent(scroll_area.viewport(), event)
44
+ finally:
45
+ self._forwarding = False
46
+ return True # swallow the original either way -- never let it reach the control itself
47
+ return False
48
+
49
+ @staticmethod
50
+ def _nearest_scroll_area(widget: QtWidgets.QWidget) -> QtWidgets.QAbstractScrollArea | None:
51
+ parent = widget.parentWidget()
52
+ while parent is not None:
53
+ if isinstance(parent, QtWidgets.QAbstractScrollArea):
54
+ return parent
55
+ parent = parent.parentWidget()
56
+ return None
@@ -20,6 +20,9 @@
20
20
  <string>Filter</string>
21
21
  </property>
22
22
  <layout class="QFormLayout" name="formLayout">
23
+ <property name="rowWrapPolicy">
24
+ <enum>QFormLayout::RowWrapPolicy::WrapLongRows</enum>
25
+ </property>
23
26
  <item row="0" column="0">
24
27
  <widget class="QLabel" name="label_9">
25
28
  <property name="text">
@@ -3,7 +3,7 @@
3
3
  ################################################################################
4
4
  ## Form generated from reading UI file 'filterwidget.ui'
5
5
  ##
6
- ## Created by: Qt User Interface Compiler version 6.10.1
6
+ ## Created by: Qt User Interface Compiler version 6.11.2
7
7
  ##
8
8
  ## WARNING! All changes made in this file will be lost when recompiling UI file!
9
9
  ################################################################################
@@ -31,6 +31,7 @@ class Ui_FilterWidget(object):
31
31
  self.groupBox_2.setObjectName(u"groupBox_2")
32
32
  self.formLayout = QFormLayout(self.groupBox_2)
33
33
  self.formLayout.setObjectName(u"formLayout")
34
+ self.formLayout.setRowWrapPolicy(QFormLayout.RowWrapPolicy.WrapLongRows)
34
35
  self.label_9 = QLabel(self.groupBox_2)
35
36
  self.label_9.setObjectName(u"label_9")
36
37
 
@@ -35,6 +35,9 @@
35
35
  <string>Focus</string>
36
36
  </property>
37
37
  <layout class="QFormLayout" name="formLayout">
38
+ <property name="rowWrapPolicy">
39
+ <enum>QFormLayout::RowWrapPolicy::WrapLongRows</enum>
40
+ </property>
38
41
  <item row="0" column="0">
39
42
  <widget class="QLabel" name="label_12">
40
43
  <property name="text">
@@ -3,7 +3,7 @@
3
3
  ################################################################################
4
4
  ## Form generated from reading UI file 'focuswidget.ui'
5
5
  ##
6
- ## Created by: Qt User Interface Compiler version 6.10.1
6
+ ## Created by: Qt User Interface Compiler version 6.11.2
7
7
  ##
8
8
  ## WARNING! All changes made in this file will be lost when recompiling UI file!
9
9
  ################################################################################
@@ -33,6 +33,7 @@ class Ui_FocusWidget(object):
33
33
  self.groupBox_5.setObjectName(u"groupBox_5")
34
34
  self.formLayout = QFormLayout(self.groupBox_5)
35
35
  self.formLayout.setObjectName(u"formLayout")
36
+ self.formLayout.setRowWrapPolicy(QFormLayout.RowWrapPolicy.WrapLongRows)
36
37
  self.label_12 = QLabel(self.groupBox_5)
37
38
  self.label_12.setObjectName(u"label_12")
38
39
 
@@ -369,10 +369,24 @@
369
369
  <property name="orientation">
370
370
  <enum>Qt::Orientation::Horizontal</enum>
371
371
  </property>
372
- <widget class="QStackedWidget" name="stackedWidget">
373
- <property name="currentIndex">
374
- <number>-1</number>
372
+ <widget class="QScrollArea" name="stackedWidgetScroll">
373
+ <property name="widgetResizable">
374
+ <bool>true</bool>
375
+ </property>
376
+ <property name="frameShape">
377
+ <enum>QFrame::Shape::NoFrame</enum>
375
378
  </property>
379
+ <property name="horizontalScrollBarPolicy">
380
+ <enum>Qt::ScrollBarPolicy::ScrollBarAsNeeded</enum>
381
+ </property>
382
+ <property name="verticalScrollBarPolicy">
383
+ <enum>Qt::ScrollBarPolicy::ScrollBarAsNeeded</enum>
384
+ </property>
385
+ <widget class="QStackedWidget" name="stackedWidget">
386
+ <property name="currentIndex">
387
+ <number>-1</number>
388
+ </property>
389
+ </widget>
376
390
  </widget>
377
391
  </widget>
378
392
  <widget class="QSplitter" name="splitterClients">
@@ -17,9 +17,9 @@ from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
17
17
  QPalette, QPixmap, QRadialGradient, QTransform)
18
18
  from PySide6.QtWidgets import (QAbstractItemView, QApplication, QFrame, QHeaderView,
19
19
  QLabel, QListView, QListWidget, QListWidgetItem,
20
- QMainWindow, QPushButton, QSizePolicy, QSpacerItem,
21
- QSplitter, QStackedWidget, QTableView, QToolButton,
22
- QVBoxLayout, QWidget)
20
+ QMainWindow, QPushButton, QScrollArea, QSizePolicy,
21
+ QSpacerItem, QSplitter, QStackedWidget, QTableView,
22
+ QToolButton, QVBoxLayout, QWidget)
23
23
  from . import resources_rc
24
24
 
25
25
  class Ui_MainWindow(object):
@@ -141,9 +141,16 @@ class Ui_MainWindow(object):
141
141
  self.splitterToolBox = QSplitter(self.splitterLog)
142
142
  self.splitterToolBox.setObjectName(u"splitterToolBox")
143
143
  self.splitterToolBox.setOrientation(Qt.Orientation.Horizontal)
144
- self.stackedWidget = QStackedWidget(self.splitterToolBox)
144
+ self.stackedWidgetScroll = QScrollArea(self.splitterToolBox)
145
+ self.stackedWidgetScroll.setObjectName(u"stackedWidgetScroll")
146
+ self.stackedWidgetScroll.setWidgetResizable(True)
147
+ self.stackedWidgetScroll.setFrameShape(QFrame.Shape.NoFrame)
148
+ self.stackedWidgetScroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
149
+ self.stackedWidgetScroll.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
150
+ self.stackedWidget = QStackedWidget()
145
151
  self.stackedWidget.setObjectName(u"stackedWidget")
146
- self.splitterToolBox.addWidget(self.stackedWidget)
152
+ self.stackedWidgetScroll.setWidget(self.stackedWidget)
153
+ self.splitterToolBox.addWidget(self.stackedWidgetScroll)
147
154
  self.splitterLog.addWidget(self.splitterToolBox)
148
155
  self.splitterClients = QSplitter(self.splitterLog)
149
156
  self.splitterClients.setObjectName(u"splitterClients")
@@ -626,7 +626,7 @@
626
626
  </widget>
627
627
  </item>
628
628
  <item>
629
- <widget class="QStackedWidget" name="stackedMove">
629
+ <widget class="MoveStack" name="stackedMove">
630
630
  <property name="currentIndex">
631
631
  <number>1</number>
632
632
  </property>
@@ -634,6 +634,9 @@
634
634
  <layout class="QVBoxLayout" name="verticalLayout_9">
635
635
  <item>
636
636
  <layout class="QFormLayout" name="formLayout">
637
+ <property name="rowWrapPolicy">
638
+ <enum>QFormLayout::RowWrapPolicy::WrapLongRows</enum>
639
+ </property>
637
640
  <item row="1" column="0">
638
641
  <widget class="QLabel" name="label_11">
639
642
  <property name="text">
@@ -1716,6 +1719,11 @@
1716
1719
  <extends>QWidget</extends>
1717
1720
  <header>..compassmovewidget</header>
1718
1721
  </customwidget>
1722
+ <customwidget>
1723
+ <class>MoveStack</class>
1724
+ <extends>QStackedWidget</extends>
1725
+ <header>..movestack</header>
1726
+ </customwidget>
1719
1727
  </customwidgets>
1720
1728
  <tabstops>
1721
1729
  <tabstop>labelStatus</tabstop>
@@ -3,7 +3,7 @@
3
3
  ################################################################################
4
4
  ## Form generated from reading UI file 'telescopewidget.ui'
5
5
  ##
6
- ## Created by: Qt User Interface Compiler version 6.11.1
6
+ ## Created by: Qt User Interface Compiler version 6.11.2
7
7
  ##
8
8
  ## WARNING! All changes made in this file will be lost when recompiling UI file!
9
9
  ################################################################################
@@ -18,10 +18,11 @@ from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
18
18
  from PySide6.QtWidgets import (QApplication, QComboBox, QDoubleSpinBox, QFormLayout,
19
19
  QFrame, QGridLayout, QGroupBox, QHBoxLayout,
20
20
  QLabel, QLineEdit, QPushButton, QSizePolicy,
21
- QSpacerItem, QSpinBox, QStackedWidget, QToolButton,
22
- QVBoxLayout, QWidget)
21
+ QSpacerItem, QSpinBox, QToolButton, QVBoxLayout,
22
+ QWidget)
23
23
 
24
24
  from ..compassmovewidget import CompassMoveWidget
25
+ from ..movestack import MoveStack
25
26
  from . import resources_rc
26
27
 
27
28
  class Ui_TelescopeWidget(object):
@@ -236,7 +237,7 @@ class Ui_TelescopeWidget(object):
236
237
 
237
238
  self.verticalLayout_7.addWidget(self.comboMoveType)
238
239
 
239
- self.stackedMove = QStackedWidget(self.groupBox_5)
240
+ self.stackedMove = MoveStack(self.groupBox_5)
240
241
  self.stackedMove.setObjectName(u"stackedMove")
241
242
  self.pageMoveEquatorial = QWidget()
242
243
  self.pageMoveEquatorial.setObjectName(u"pageMoveEquatorial")
@@ -244,6 +245,7 @@ class Ui_TelescopeWidget(object):
244
245
  self.verticalLayout_9.setObjectName(u"verticalLayout_9")
245
246
  self.formLayout = QFormLayout()
246
247
  self.formLayout.setObjectName(u"formLayout")
248
+ self.formLayout.setRowWrapPolicy(QFormLayout.RowWrapPolicy.WrapLongRows)
247
249
  self.label_11 = QLabel(self.pageMoveEquatorial)
248
250
  self.label_11.setObjectName(u"label_11")
249
251
 
@@ -1,4 +1,5 @@
1
1
  import asyncio
2
+ import warnings
2
3
  from enum import Enum
3
4
  from typing import Any
4
5
 
@@ -39,6 +40,12 @@ from .base import BaseWidget
39
40
 
40
41
  log = logging.getLogger(__name__)
41
42
 
43
+ # astropy 7.2.2 / numpy 2.5.x: Angle.to_string()'s vectorized sexagesimal formatter raises a
44
+ # spurious "invalid value encountered in do_format" RuntimeWarning on essentially any non-zero
45
+ # angle, without affecting the (correct) formatted result. Silence it here rather than at every
46
+ # call site.
47
+ warnings.filterwarnings("ignore", message="invalid value encountered in do_format", category=RuntimeWarning)
48
+
42
49
 
43
50
  class COORDS(Enum):
44
51
  EQUITORIAL = "Equitorial"
@@ -726,4 +733,9 @@ class TelescopeWidget(BaseWidget, Ui_TelescopeWidget):
726
733
  except ValueError:
727
734
  return
728
735
  self.stackedMove.setCurrentWidget(self._MOVE_WIDGETS[coord])
736
+ # MoveStack sizes itself to the current page, not the widest one (see movestack.py) --
737
+ # updateGeometry() re-triggers layout after the current page changed, so the window
738
+ # actually shrinks/grows to match instead of staying pinned at whatever page was
739
+ # previously widest.
740
+ self.stackedMove.updateGeometry()
729
741
  self._DEST_CALC[coord]()
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pyobs-gui"
3
- version = "2.4.0"
3
+ version = "2.4.2"
4
4
  description = "A remote GUI for pyobs"
5
5
  authors = [{ name = "Tim-Oliver Husser", email = "thusser@uni-goettingen.de" }]
6
6
  requires-python = ">=3.11"
@@ -11,8 +11,8 @@ dependencies = [
11
11
  "qasync>=0.27.1,<0.29",
12
12
  "sunpy[all]>=7.0.1",
13
13
  "QtAwesome>=1.4.0,<2",
14
- "qfitswidget>=0.13.1",
15
- "pyobs-core>=2.7.1,<3",
14
+ "qfitswidget>=1.1.3",
15
+ "pyobs-core>=2.7.2,<3",
16
16
  "pyside6>=6.10.1",
17
17
  "astroplan>=0.10.1",
18
18
  "astropy>=7.0.1,<8",