pcdswidgets 2.2.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 (313) hide show
  1. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/PKG-INFO +1 -1
  2. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/_version.py +3 -3
  3. pcdswidgets-2.3.0/pcdswidgets/common/tools/view_saver/registry.py +140 -0
  4. pcdswidgets-2.3.0/pcdswidgets/common/tools/view_saver/view_saver.py +389 -0
  5. pcdswidgets-2.3.0/pcdswidgets/common/tools/view_saver/view_saver_dialog.py +145 -0
  6. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/generated/path_defs.py +1 -0
  7. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/imaging/common/camera_viewer_stretch.py +1 -0
  8. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/imaging/common/centroid_tracker_full.py +23 -0
  9. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/imaging/common/collapsible_section.py +21 -0
  10. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/imaging/common/colormap_intesity_control_full.py +23 -0
  11. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/imaging/common/epics_roi_full.py +15 -0
  12. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/imaging/common/marker_selection_full.py +24 -0
  13. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/motion/common/motor_tip_tilt_full.py +19 -0
  14. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/screens/vacuum/gauges/GHCPLC_expert.ui +8 -2
  15. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/screens/vacuum/pumps/PIPPLC_detailed.ui +16 -10
  16. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/screens/vacuum/pumps/PIPPLC_expert.ui +11 -5
  17. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/screens/vacuum/pumps/PTMPLC_expert.ui +88 -73
  18. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/screens/vacuum/valves/VFS_detailed.ui +4 -4
  19. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/screens/vacuum/valves/VFS_expert.ui +46 -5
  20. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/screens/vacuum/valves/VGC_detailed.ui +35 -4
  21. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/screens/vacuum/valves/VGC_expert.ui +196 -5
  22. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/tests/imaging/test_colormap_intesity_control_full.py +11 -3
  23. pcdswidgets-2.3.0/pcdswidgets/vacuum/demo/__init__.py +0 -0
  24. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets.egg-info/PKG-INFO +1 -1
  25. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets.egg-info/SOURCES.txt +4 -0
  26. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets.egg-info/entry_points.txt +1 -0
  27. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets.egg-info/scm_file_list.json +4 -0
  28. pcdswidgets-2.3.0/pcdswidgets.egg-info/scm_version.json +8 -0
  29. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pyproject.toml +1 -0
  30. pcdswidgets-2.2.0/pcdswidgets.egg-info/scm_version.json +0 -8
  31. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/.coveragerc +0 -0
  32. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/.git_archival.txt +0 -0
  33. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/.gitattributes +0 -0
  34. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/.github/CODEOWNERS +0 -0
  35. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  36. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/.github/dependabot.yml +0 -0
  37. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/.github/workflows/pixi_test.yml +0 -0
  38. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/.gitignore +0 -0
  39. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/.pre-commit-config.yaml +0 -0
  40. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/.python-version +0 -0
  41. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/AUTHORS.rst +0 -0
  42. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/CONTRIBUTING.rst +0 -0
  43. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/LICENSE.md +0 -0
  44. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/MANIFEST.in +0 -0
  45. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/Makefile +0 -0
  46. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/README.md +0 -0
  47. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/conda-recipe/meta.yaml +0 -0
  48. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/docs/Makefile +0 -0
  49. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/docs/source/_static/content_location.png +0 -0
  50. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/docs/source/_static/icons/.gitignore +0 -0
  51. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/docs/source/addwidget.rst +0 -0
  52. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/docs/source/conf.py +0 -0
  53. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/docs/source/dev.rst +0 -0
  54. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/docs/source/icons.rst +0 -0
  55. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/docs/source/index.rst +0 -0
  56. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/docs/source/mixins.rst +0 -0
  57. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/docs/source/utils.rst +0 -0
  58. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/docs/source/vacuum/gauges.rst +0 -0
  59. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/docs/source/vacuum/others.rst +0 -0
  60. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/docs/source/vacuum/pumps.rst +0 -0
  61. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/docs/source/vacuum/valves.rst +0 -0
  62. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/docs/source/vacuum.rst +0 -0
  63. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/examples/basic_table.json +0 -0
  64. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/examples/basic_table.py +0 -0
  65. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/examples/basic_table.ui +0 -0
  66. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/examples/basic_table_row.ui +0 -0
  67. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/examples/show_symbol.py +0 -0
  68. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/examples/symbols.ui +0 -0
  69. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/examples/valve.db +0 -0
  70. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/examples/valve.ui +0 -0
  71. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/__init__.py +0 -0
  72. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/builder/__init__.py +0 -0
  73. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/builder/build.py +0 -0
  74. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/builder/designer_options.py +0 -0
  75. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/builder/designer_widget.py +0 -0
  76. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/builder/entrypoint_finder.py +0 -0
  77. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/builder/get_icon_options.py +0 -0
  78. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/builder/icon_options.j2 +0 -0
  79. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/builder/icon_options.py +0 -0
  80. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/builder/inits.py +0 -0
  81. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/builder/path_defs.py.j2 +0 -0
  82. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/builder/read_ui.py +0 -0
  83. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/builder/screen_paths.py +0 -0
  84. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/builder/ui_base_widget.j2 +0 -0
  85. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/builder/ui_main_widget.j2 +0 -0
  86. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/builder/valid_glyph_map.json +0 -0
  87. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/common/__init__.py +0 -0
  88. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/common/dock/__init__.py +0 -0
  89. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/common/dock/indicator_grid.py +0 -0
  90. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/common/dock/tab_dock.py +0 -0
  91. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/common/dock/tab_dock_button.py +0 -0
  92. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/common/toolbar/__init__.py +0 -0
  93. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/common/toolbar/yaml_toolbar.py +0 -0
  94. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/common/tools/__init__.py +0 -0
  95. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/common/tools/feature_finder.py +0 -0
  96. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/common/tools/pv_channel.py +0 -0
  97. {pcdswidgets-2.2.0/pcdswidgets/generated → pcdswidgets-2.3.0/pcdswidgets/common/tools/view_saver}/__init__.py +0 -0
  98. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/eps_byteindicator.py +0 -0
  99. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/generated/README.md +0 -0
  100. {pcdswidgets-2.2.0/pcdswidgets/generated/common → pcdswidgets-2.3.0/pcdswidgets/generated}/__init__.py +0 -0
  101. {pcdswidgets-2.2.0/pcdswidgets/generated/common/tools → pcdswidgets-2.3.0/pcdswidgets/generated/common}/__init__.py +0 -0
  102. {pcdswidgets-2.2.0/pcdswidgets/generated/imaging → pcdswidgets-2.3.0/pcdswidgets/generated/common/tools}/__init__.py +0 -0
  103. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/generated/common/tools/feature_finder_base.py +0 -0
  104. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/generated/common/tools/feature_finder_form.py +0 -0
  105. {pcdswidgets-2.2.0/pcdswidgets/generated/imaging/common → pcdswidgets-2.3.0/pcdswidgets/generated/imaging}/__init__.py +0 -0
  106. {pcdswidgets-2.2.0/pcdswidgets/generated/motion → pcdswidgets-2.3.0/pcdswidgets/generated/imaging/common}/__init__.py +0 -0
  107. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/generated/imaging/common/acquisition_control_full_base.py +0 -0
  108. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/generated/imaging/common/acquisition_control_full_form.py +0 -0
  109. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/generated/imaging/common/camera_viewer_stretch_base.py +0 -0
  110. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/generated/imaging/common/camera_viewer_stretch_form.py +0 -0
  111. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/generated/imaging/common/centroid_tracker_full_base.py +0 -0
  112. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/generated/imaging/common/centroid_tracker_full_form.py +0 -0
  113. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/generated/imaging/common/colormap_intesity_control_full_base.py +0 -0
  114. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/generated/imaging/common/colormap_intesity_control_full_form.py +0 -0
  115. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/generated/imaging/common/epics_roi_full_base.py +0 -0
  116. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/generated/imaging/common/epics_roi_full_form.py +0 -0
  117. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/generated/imaging/common/exposure_timing_control_full_base.py +0 -0
  118. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/generated/imaging/common/exposure_timing_control_full_form.py +0 -0
  119. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/generated/imaging/common/marker_selection_full_base.py +0 -0
  120. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/generated/imaging/common/marker_selection_full_form.py +0 -0
  121. {pcdswidgets-2.2.0/pcdswidgets/generated/motion/common → pcdswidgets-2.3.0/pcdswidgets/generated/motion}/__init__.py +0 -0
  122. {pcdswidgets-2.2.0/pcdswidgets/generated/motion/expert → pcdswidgets-2.3.0/pcdswidgets/generated/motion/common}/__init__.py +0 -0
  123. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/generated/motion/common/motor_beckhoff_slits_base.py +0 -0
  124. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/generated/motion/common/motor_beckhoff_slits_form.py +0 -0
  125. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/generated/motion/common/motor_classic_full_base.py +0 -0
  126. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/generated/motion/common/motor_classic_full_form.py +0 -0
  127. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/generated/motion/common/motor_classic_row_base.py +0 -0
  128. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/generated/motion/common/motor_classic_row_form.py +0 -0
  129. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/generated/motion/common/motor_classic_vert_base.py +0 -0
  130. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/generated/motion/common/motor_classic_vert_form.py +0 -0
  131. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/generated/motion/common/motor_state_mover_base.py +0 -0
  132. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/generated/motion/common/motor_state_mover_form.py +0 -0
  133. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/generated/motion/common/motor_tc_classic_row_base.py +0 -0
  134. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/generated/motion/common/motor_tc_classic_row_form.py +0 -0
  135. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/generated/motion/common/motor_tip_tilt_double_base.py +0 -0
  136. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/generated/motion/common/motor_tip_tilt_double_form.py +0 -0
  137. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/generated/motion/common/motor_tip_tilt_full_base.py +0 -0
  138. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/generated/motion/common/motor_tip_tilt_full_form.py +0 -0
  139. {pcdswidgets-2.2.0/pcdswidgets/generated/motion/smaract → pcdswidgets-2.3.0/pcdswidgets/generated/motion/expert}/__init__.py +0 -0
  140. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/generated/motion/expert/motor_expert_screen_beckhoff_base.py +0 -0
  141. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/generated/motion/expert/motor_expert_screen_beckhoff_form.py +0 -0
  142. {pcdswidgets-2.2.0/pcdswidgets/generated/tests → pcdswidgets-2.3.0/pcdswidgets/generated/motion/smaract}/__init__.py +0 -0
  143. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/generated/motion/smaract/smaract_open_loop_classic_row_base.py +0 -0
  144. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/generated/motion/smaract/smaract_open_loop_classic_row_form.py +0 -0
  145. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/generated/motion/smaract/smaract_open_loop_context_double_base.py +0 -0
  146. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/generated/motion/smaract/smaract_open_loop_context_double_form.py +0 -0
  147. {pcdswidgets-2.2.0/pcdswidgets/generated/tests/builder → pcdswidgets-2.3.0/pcdswidgets/generated/tests}/__init__.py +0 -0
  148. {pcdswidgets-2.2.0/pcdswidgets/icons → pcdswidgets-2.3.0/pcdswidgets/generated/tests/builder}/__init__.py +0 -0
  149. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/generated/tests/builder/builder_basic_test_widget_base.py +0 -0
  150. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/generated/tests/builder/builder_basic_test_widget_form.py +0 -0
  151. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/generated/tests/builder/builder_filepath_test_widget_base.py +0 -0
  152. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/generated/tests/builder/builder_filepath_test_widget_form.py +0 -0
  153. {pcdswidgets-2.2.0/pcdswidgets/imaging → pcdswidgets-2.3.0/pcdswidgets/icons}/__init__.py +0 -0
  154. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/icons/feature_finder_icon.png +0 -0
  155. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/icons/glyphs/camera-cog.svg +0 -0
  156. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/icons/glyphs/crosshair.svg +0 -0
  157. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/icons/glyphs/disconnected.svg +0 -0
  158. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/icons/glyphs/download.svg +0 -0
  159. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/icons/glyphs/error.svg +0 -0
  160. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/icons/glyphs/eye-off.svg +0 -0
  161. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/icons/glyphs/eye.svg +0 -0
  162. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/icons/glyphs/move.svg +0 -0
  163. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/icons/glyphs/moving.svg +0 -0
  164. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/icons/glyphs/ok.svg +0 -0
  165. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/icons/glyphs/pen-tool.svg +0 -0
  166. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/icons/glyphs/scissors.svg +0 -0
  167. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/icons/glyphs/thickness.svg +0 -0
  168. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/icons/glyphs/trash.svg +0 -0
  169. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/icons/glyphs/warning.svg +0 -0
  170. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/icons/glyphs.py +0 -0
  171. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/icons/lcls.png +0 -0
  172. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/icons/multi_state_led_icon.png +0 -0
  173. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/icons/smaract_tip_tilt_context_qt_icon.png +0 -0
  174. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/icons/smaract_tip_tilt_qt_icon.png +0 -0
  175. {pcdswidgets-2.2.0/pcdswidgets/imaging/common → pcdswidgets-2.3.0/pcdswidgets/imaging}/__init__.py +0 -0
  176. {pcdswidgets-2.2.0/pcdswidgets/motion → pcdswidgets-2.3.0/pcdswidgets/imaging/common}/__init__.py +0 -0
  177. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/imaging/common/acquisition_control_full.py +0 -0
  178. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/imaging/common/cam_marker.py +0 -0
  179. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/imaging/common/cam_roi.py +0 -0
  180. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/imaging/common/centroid_marker_style_dialog.py +0 -0
  181. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/imaging/common/colorselect_button.py +0 -0
  182. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/imaging/common/exposure_timing_control_full.py +0 -0
  183. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/imaging/common/marker_style_dialog.py +0 -0
  184. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/imaging/common/spinbox.py +0 -0
  185. {pcdswidgets-2.2.0/pcdswidgets/motion/common → pcdswidgets-2.3.0/pcdswidgets/motion}/__init__.py +0 -0
  186. {pcdswidgets-2.2.0/pcdswidgets/motion/expert → pcdswidgets-2.3.0/pcdswidgets/motion/common}/__init__.py +0 -0
  187. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/motion/common/motor_beckhoff_slits.py +0 -0
  188. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/motion/common/motor_classic_full.py +0 -0
  189. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/motion/common/motor_classic_row.py +0 -0
  190. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/motion/common/motor_classic_vert.py +0 -0
  191. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/motion/common/motor_state_mover.py +0 -0
  192. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/motion/common/motor_state_mover_expanded.py +0 -0
  193. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/motion/common/motor_style.py +0 -0
  194. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/motion/common/motor_tc_classic_row.py +0 -0
  195. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/motion/common/motor_tip_tilt.py +0 -0
  196. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/motion/common/motor_tip_tilt_double.py +0 -0
  197. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/motion/common/state_mover_common.py +0 -0
  198. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/motion/common/svg_multi_state_led.py +0 -0
  199. {pcdswidgets-2.2.0/pcdswidgets/motion/smaract → pcdswidgets-2.3.0/pcdswidgets/motion/expert}/__init__.py +0 -0
  200. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/motion/expert/motor_expert_screen_beckhoff.py +0 -0
  201. {pcdswidgets-2.2.0/pcdswidgets/symbols/demo → pcdswidgets-2.3.0/pcdswidgets/motion/smaract}/__init__.py +0 -0
  202. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/motion/smaract/smaract_open_loop_classic_row.py +0 -0
  203. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/motion/smaract/smaract_open_loop_context_double.py +0 -0
  204. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/screens/README.md +0 -0
  205. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/screens/motion/motor_state_mover_expert.py +0 -0
  206. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/screens/vacuum/gauges/GCCPLC_detailed.ui +0 -0
  207. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/screens/vacuum/gauges/GCCPLC_expert.ui +0 -0
  208. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/screens/vacuum/gauges/GFSPLC_detailed.ui +0 -0
  209. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/screens/vacuum/gauges/GFSPLC_expert.ui +0 -0
  210. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/screens/vacuum/gauges/GHCPLC_detailed.ui +0 -0
  211. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/screens/vacuum/gauges/GaugePLC_detailed.ui +0 -0
  212. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/screens/vacuum/gauges/GaugePLC_expert.ui +0 -0
  213. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/screens/vacuum/pumps/Kashiyama_G_detailed.ui +0 -0
  214. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/screens/vacuum/pumps/Kashiyama_G_expert.ui +0 -0
  215. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/screens/vacuum/pumps/PIPCombined_controller.ui +0 -0
  216. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/screens/vacuum/pumps/PIPCombined_detailed.ui +0 -0
  217. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/screens/vacuum/pumps/PIPCombined_expert.ui +0 -0
  218. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/screens/vacuum/pumps/PROPLC_detailed.ui +0 -0
  219. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/screens/vacuum/pumps/PTMPLC_detailed.ui +0 -0
  220. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/screens/vacuum/valves/VCN_VAT590_detailed.ui +0 -0
  221. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/screens/vacuum/valves/VCN_VAT590_expert.ui +0 -0
  222. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/screens/vacuum/valves/VCN_detailed.ui +0 -0
  223. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/screens/vacuum/valves/VCN_expert.ui +0 -0
  224. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/screens/vacuum/valves/VGC_2S_detailed.ui +0 -0
  225. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/screens/vacuum/valves/VGC_2S_expert.ui +0 -0
  226. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/screens/vacuum/valves/VRCDA_detailed.ui +0 -0
  227. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/screens/vacuum/valves/VRCDA_expert.ui +0 -0
  228. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/screens/vacuum/valves/VRCNO_detailed.ui +0 -0
  229. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/screens/vacuum/valves/VRCNO_expert.ui +0 -0
  230. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/screens/vacuum/valves/VRC_detailed.ui +0 -0
  231. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/screens/vacuum/valves/VRC_expert.ui +0 -0
  232. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/screens/vacuum/valves/VVCNO_detailed.ui +0 -0
  233. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/screens/vacuum/valves/VVCNO_expert.ui +0 -0
  234. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/screens/vacuum/valves/VVC_detailed.ui +0 -0
  235. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/screens/vacuum/valves/VVC_expert.ui +0 -0
  236. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/show_screen.py +0 -0
  237. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/stylesheets/masterStyleSheet.qss +0 -0
  238. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/symbols/__init__.py +0 -0
  239. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/symbols/base.py +0 -0
  240. {pcdswidgets-2.2.0/pcdswidgets/tests → pcdswidgets-2.3.0/pcdswidgets/symbols/demo}/__init__.py +0 -0
  241. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/symbols/demo/__main__.py +0 -0
  242. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/symbols/gauges.py +0 -0
  243. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/symbols/others.py +0 -0
  244. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/symbols/pumps.py +0 -0
  245. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/symbols/valves.py +0 -0
  246. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/table.py +0 -0
  247. {pcdswidgets-2.2.0/pcdswidgets/tests/builder → pcdswidgets-2.3.0/pcdswidgets/tests}/__init__.py +0 -0
  248. {pcdswidgets-2.2.0/pcdswidgets/tests/dock → pcdswidgets-2.3.0/pcdswidgets/tests/builder}/__init__.py +0 -0
  249. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/tests/builder/builder_basic_test_widget.py +0 -0
  250. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/tests/builder/builder_filepath_test_widget.py +0 -0
  251. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/tests/builder/subdisplay.ui +0 -0
  252. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/tests/builder/test_builder.py +0 -0
  253. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/tests/builder/test_designer_widget.py +0 -0
  254. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/tests/builder/test_entrypoint_widgets.py +0 -0
  255. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/tests/conftest.py +0 -0
  256. {pcdswidgets-2.2.0/pcdswidgets/tests/imaging → pcdswidgets-2.3.0/pcdswidgets/tests/dock}/__init__.py +0 -0
  257. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/tests/dock/dock1.ui +0 -0
  258. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/tests/dock/dock2.ui +0 -0
  259. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/tests/dock/test_dock.py +0 -0
  260. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/tests/dock/test_dock_button.py +0 -0
  261. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/tests/dock/test_indicator_grid.py +0 -0
  262. {pcdswidgets-2.2.0/pcdswidgets/tests/toolbar → pcdswidgets-2.3.0/pcdswidgets/tests/imaging}/__init__.py +0 -0
  263. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/tests/test_show_screen.py +0 -0
  264. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/tests/test_symbols.py +0 -0
  265. {pcdswidgets-2.2.0/pcdswidgets/tests/vacuum → pcdswidgets-2.3.0/pcdswidgets/tests/toolbar}/__init__.py +0 -0
  266. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/tests/toolbar/test_yaml_layout.py +0 -0
  267. {pcdswidgets-2.2.0/pcdswidgets/vacuum/demo → pcdswidgets-2.3.0/pcdswidgets/tests/vacuum}/__init__.py +0 -0
  268. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/tests/vacuum/test_base.py +0 -0
  269. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/tests/vacuum/test_mixins.py +0 -0
  270. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/tests/vacuum/test_symbols.py +0 -0
  271. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/ui/README.md +0 -0
  272. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/ui/common/tools/feature_finder.ui +0 -0
  273. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/ui/imaging/common/acquisition_control_full.ui +0 -0
  274. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/ui/imaging/common/camera_viewer_stretch.ui +0 -0
  275. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/ui/imaging/common/centroid_tracker_full.ui +0 -0
  276. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/ui/imaging/common/colormap_intesity_control_full.ui +0 -0
  277. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/ui/imaging/common/epics_roi_full.ui +0 -0
  278. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/ui/imaging/common/exposure_timing_control_full.ui +0 -0
  279. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/ui/imaging/common/marker_selection_full.ui +0 -0
  280. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/ui/motion/common/motor_beckhoff_slits.ui +0 -0
  281. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/ui/motion/common/motor_classic_full.ui +0 -0
  282. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/ui/motion/common/motor_classic_row.ui +0 -0
  283. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/ui/motion/common/motor_classic_vert.ui +0 -0
  284. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/ui/motion/common/motor_state_mover.ui +0 -0
  285. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/ui/motion/common/motor_tc_classic_row.ui +0 -0
  286. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/ui/motion/common/motor_tip_tilt_double.ui +0 -0
  287. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/ui/motion/common/motor_tip_tilt_full.ui +0 -0
  288. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/ui/motion/expert/motor_expert_screen_beckhoff.ui +0 -0
  289. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/ui/motion/smaract/smaract_open_loop_classic_row.ui +0 -0
  290. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/ui/motion/smaract/smaract_open_loop_context_double.ui +0 -0
  291. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/ui/tests/builder/builder_basic_test_widget.ui +0 -0
  292. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/ui/tests/builder/builder_filepath_test_widget.ui +0 -0
  293. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/utils.py +0 -0
  294. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/vacuum/__init__.py +0 -0
  295. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/vacuum/base.py +0 -0
  296. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/vacuum/demo/__main__.py +0 -0
  297. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/vacuum/gauges.py +0 -0
  298. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/vacuum/mixins.py +0 -0
  299. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/vacuum/others.py +0 -0
  300. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/vacuum/pumps.py +0 -0
  301. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/vacuum/valves.py +0 -0
  302. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets/version.py +0 -0
  303. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets.egg-info/dependency_links.txt +0 -0
  304. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets.egg-info/requires.txt +0 -0
  305. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pcdswidgets.egg-info/top_level.txt +0 -0
  306. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pixi.lock +0 -0
  307. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pixi.toml +0 -0
  308. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pixi_scripts/env_setup.sh +0 -0
  309. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/pixi_scripts/install.sh +0 -0
  310. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/setup.cfg +0 -0
  311. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/show_icon_options.sh +0 -0
  312. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/try_in_designer.sh +0 -0
  313. {pcdswidgets-2.2.0 → pcdswidgets-2.3.0}/try_in_pydm.sh +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pcdswidgets
3
- Version: 2.2.0
3
+ Version: 2.3.0
4
4
  Summary: LCLS PyDM Widget Library
5
5
  Author: SLAC National Accelerator Laboratory
6
6
  License: Copyright (c) 2026, The Board of Trustees of the Leland Stanford Junior
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '2.2.0'
22
- __version_tuple__ = version_tuple = (2, 2, 0)
21
+ __version__ = version = '2.3.0'
22
+ __version_tuple__ = version_tuple = (2, 3, 0)
23
23
 
24
- __commit_id__ = commit_id = 'g6e1ca0e5e'
24
+ __commit_id__ = commit_id = 'gccf31454c'
@@ -0,0 +1,140 @@
1
+ """Fallback registry of savable widget properties for ViewSaver.
2
+
3
+ This registry is the exception path used for widget classes
4
+ we do not plan to override/subclasswith a ``get_view_saver_properties`` method.
5
+ - i.e. built-in Qt widgets and third-party widgets
6
+ """
7
+
8
+ import json
9
+ import logging
10
+ from operator import attrgetter
11
+ from typing import Any, Callable
12
+
13
+ from qtpy.QtWidgets import QWidget
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ def _identity(value: Any) -> Any:
19
+ """No-op converter, for values QSettings already round-trips faithfully."""
20
+ return value
21
+
22
+
23
+ def _json_default(obj: Any) -> Any:
24
+ """Fallback encoder for values ``json.dumps`` can't handle natively.
25
+
26
+ ``ViewBox.getState()`` embeds numpy arrays; convert to lists.
27
+ """
28
+ if hasattr(obj, "tolist"):
29
+ return obj.tolist()
30
+ raise TypeError(f"Object of type {obj.__class__.__name__} is not JSON serializable")
31
+
32
+
33
+ def _json_dumps(value: Any) -> str:
34
+ """``json.dumps`` that tolerates numpy arrays/scalars via ``_json_default``."""
35
+ return json.dumps(value, default=_json_default)
36
+
37
+
38
+ def _make_getter(widget: QWidget, path: str, save_fn: Callable) -> Callable:
39
+ """wrap the widget property getter to return the encoded value.
40
+
41
+ ``save_fn`` turns value to what QSettings stores (usually string)
42
+ """
43
+ method = attrgetter(path)(widget)
44
+ return lambda: save_fn(method())
45
+
46
+
47
+ def _make_setter(widget: QWidget, path: str, load_fn: Callable) -> Callable:
48
+ """wrap the widget property setter to first decode then apply a value.
49
+
50
+ ``load_fn`` decodes the raw QSettings value -> expected type;
51
+
52
+ Special case: tuples are splatted into positional args for
53
+ multi-argument setters (e.g. ``set_levels(mn, mx)``)
54
+ """
55
+ method = attrgetter(path)(widget)
56
+
57
+ def setter(raw: Any) -> None:
58
+ value = load_fn(raw)
59
+ if isinstance(value, tuple):
60
+ method(*value)
61
+ else:
62
+ method(value)
63
+
64
+ return setter
65
+
66
+
67
+ # dict of properties that should have persistance given a ClassName
68
+ #
69
+ # Format is:
70
+ # Qt class name -> { propKey: (getter, setter, load_fn, save_fn) }
71
+ #
72
+ # ``getter``/``setter`` are a (possibly dotted) attribute path resolved on the
73
+ # widget, each of which must resolve to a bound method.
74
+ #
75
+ # ``load_fn`` decodes the raw value read from QSettings (IniFormat stores
76
+ # everything as a string) back to the type the setter expects.
77
+ #
78
+ # ``save_fn`` encodes the getter's return value into what is written to
79
+ # QSettings, and must yield a string: use ``str`` for scalars and
80
+
81
+
82
+ # Only built-in / third-party classes we do not subclass belong here; every
83
+ # widget we own defines ``get_view_saver_properties`` instead.
84
+ WIDGET_REGISTRY: dict[str, dict[str, tuple[str, str, Callable, Callable]]] = {
85
+ # QT BASE
86
+ "QTabWidget": {"currentIndex": ("currentIndex", "setCurrentIndex", int, str)},
87
+ "QComboBox": {"currentIndex": ("currentIndex", "setCurrentIndex", int, str)},
88
+ "QGroupBox": {"checked": ("isChecked", "setChecked", json.loads, json.dumps)},
89
+ "QCheckBox": {"checked": ("isChecked", "setChecked", json.loads, json.dumps)},
90
+ "QPushButton": {"checked": ("isChecked", "setChecked", json.loads, json.dumps)},
91
+ "QSplitter": {"state": ("saveState", "restoreState", _identity, _identity)},
92
+ # Imaging (PyDM widget, not subclassed by us)
93
+ "PyDMImageView": {
94
+ "view": ("view.vb.getState", "view.vb.setState", json.loads, _json_dumps),
95
+ },
96
+ }
97
+
98
+ # Registered classes that are also containers of other savable widgets.
99
+ CONTAINER_REGISTRY_CLASSES: set[str] = {
100
+ "QSplitter",
101
+ "QTabWidget",
102
+ "QGroupBox",
103
+ }
104
+
105
+
106
+ def is_registry_savable(widget: QWidget) -> bool:
107
+ """True if *widget*'s class has fallback properties in ``WIDGET_REGISTRY``."""
108
+ return type(widget).__name__ in WIDGET_REGISTRY
109
+
110
+
111
+ def is_registry_container(widget: QWidget) -> bool:
112
+ """True if *widget*'s class is a registered container of other savables."""
113
+ return type(widget).__name__ in CONTAINER_REGISTRY_CLASSES
114
+
115
+
116
+ def resolve_registry_props(
117
+ widget: QWidget,
118
+ ) -> dict[str, tuple[Callable, Callable]] | None:
119
+ """Resolve getter/setter callables for each registered property of *widget*.
120
+
121
+ Fallback used only for classes without ``get_view_saver_properties`` (see
122
+ module docstring). Returns ``{propKey: (getter, setter)}`` where the getter
123
+ yields a QSettings-storable value and the setter accepts the raw stored
124
+ value, or ``None`` if the class is not registered.
125
+ """
126
+ class_name = type(widget).__name__
127
+ props = WIDGET_REGISTRY.get(class_name)
128
+ if props is None:
129
+ logger.error(f"No registered properties for {class_name}")
130
+ return None
131
+ resolved_props: dict[str, tuple[Callable, Callable]] = {}
132
+ for prop_name, (getter, setter, load_fn, save_fn) in props.items():
133
+ try:
134
+ resolved_props[prop_name] = (
135
+ _make_getter(widget, getter, save_fn),
136
+ _make_setter(widget, setter, load_fn),
137
+ )
138
+ except AttributeError:
139
+ logger.exception(f"ViewSaver: could not resolve {class_name}.{prop_name}, skipping")
140
+ return resolved_props
@@ -0,0 +1,389 @@
1
+ """ViewSaver widget — persists and restores widget states via QSettings."""
2
+
3
+ import logging
4
+ import os
5
+ import random
6
+ import string
7
+ from pathlib import Path
8
+ from typing import Any, Callable
9
+
10
+ from pydm.utilities import is_qt_designer
11
+ from pydm.utilities.iconfont import IconFont
12
+ from pydm.widgets.base import PyDMPrimitiveWidget
13
+ from pydm.widgets.designer_settings import update_property_for_widget
14
+ from qtpy.QtCore import Property, QEvent, QObject, QRect, QSettings, QSize, Qt, QTimer
15
+ from qtpy.QtGui import QColor, QFont, QIcon, QPainter, QPaintEvent, QPen
16
+ from qtpy.QtWidgets import QAction, QApplication, QFrame, QSizePolicy, QWidget
17
+
18
+ from .registry import (
19
+ is_registry_container,
20
+ is_registry_savable,
21
+ resolve_registry_props,
22
+ )
23
+ from .view_saver_dialog import ViewSaverDialog
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+ POLL_INTERVAL_MS = 60_000
28
+
29
+
30
+ class EditWidgetListExtension:
31
+ """Adds an 'Edit ViewSaver…' action to the Designer right-click menu.
32
+
33
+ Follows the same pattern as ``MacroEditExtension`` in
34
+ ``pcdswidgets.builder.designer_widget``.
35
+ """
36
+
37
+ def __init__(self, widget: "ViewSaver"):
38
+ self.widget = widget
39
+ self._action = QAction("&Edit ViewSaver\u2026", self.widget)
40
+ self._action.triggered.connect(widget.open_dialog)
41
+
42
+ def actions(self) -> list[QAction]:
43
+ return [self._action]
44
+
45
+
46
+ class ViewSaver(QFrame, PyDMPrimitiveWidget):
47
+ """
48
+ An invisible container that auto-saves and restores the view-state of the
49
+ widgets placed inside it, using QSettings.
50
+
51
+ Drop this widget onto a screen and place the widgets you want persisted
52
+ inside it.
53
+
54
+ Any child that exposes a ``get_view_saver_properties`` or
55
+ is registered in ``registry.WIDGET_REGISTRY`` is tracked.
56
+
57
+ Optional ``excludedWidgets`` list can
58
+ opt children out.
59
+
60
+ In QtDesigner the container draws a dashed border and a small corner label
61
+ so it can be located and edited (double-click, or the right-click task
62
+ menu, opens the settings dialog). At runtime it is fully transparent with
63
+ no border or margins, so only its child widgets are visible.
64
+
65
+
66
+ Designer properties
67
+ -------------------
68
+ dirName : str
69
+ Directory where the settings .ini file is stored
70
+ (default ``~/.config/pcds_view_saves``).
71
+ fileName : str
72
+ Name of the .ini file (without extension). Supports PyDM
73
+ ``${MACRO}`` expansion and this is recommended in situations the same UI
74
+ screen is used for different devices
75
+ Auto-generated rand on first load
76
+ excludedWidgets : list[str]
77
+ objectNames of savable child widgets to skip.
78
+ """
79
+
80
+ _qt_designer_ = {
81
+ "group": "ECS Common Tools",
82
+ "is_container": True,
83
+ "extensions": [EditWidgetListExtension],
84
+ }
85
+
86
+ _HINT_TEXT = "ViewSaver \u2022 Double-click to edit"
87
+
88
+ @classmethod
89
+ def get_designer_icon(cls) -> QIcon:
90
+ return IconFont().icon("save")
91
+
92
+ def __init__(self, parent: QWidget | None = None, **kwargs: Any):
93
+ super().__init__(parent=parent, **kwargs)
94
+
95
+ self._dir_name: str = os.path.join("~", ".config", "pcds_view_saves")
96
+ self._file_name: str = ""
97
+ self._excluded: list[str] = []
98
+ # objectName -> {propKey: (getter, setter)} resolved at runtime load
99
+ self._tracked_widgets: dict[str, dict[str, tuple[Callable, Callable]]] = {}
100
+ self._loaded: bool = False
101
+ self._last_snapshot: dict[str, Any] = {}
102
+
103
+ # Invisible passthrough container: no frame, no margins, transparent.
104
+ self.setFrameShape(QFrame.NoFrame)
105
+ self.setContentsMargins(0, 0, 0, 0)
106
+ self.setAttribute(Qt.WA_TranslucentBackground, True)
107
+ self.setAutoFillBackground(False)
108
+
109
+ # Poll timer — started in _initial_load
110
+ self._poll_timer = QTimer(self)
111
+ self._poll_timer.setInterval(POLL_INTERVAL_MS)
112
+ self._poll_timer.timeout.connect(self._save_settings)
113
+
114
+ self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)
115
+
116
+ # Defer setup to the next event-loop cycle.
117
+ # ensures fileName loaded from .ui file and sibling widgets exist
118
+ QTimer.singleShot(0, self._deferred_init)
119
+
120
+ def _deferred_init(self) -> None:
121
+ """Run mode-appropriate setup once the event loop resumes."""
122
+ if is_qt_designer():
123
+ # Stay visible and on top so it's easy to find
124
+ self.raise_()
125
+ self._ensure_default_file_name()
126
+ else:
127
+ self._initial_load()
128
+
129
+ def _ensure_default_file_name(self) -> None:
130
+ """Assign a semi-unique default fileName if none was loaded from the .ui"""
131
+ if self._file_name:
132
+ return
133
+
134
+ suffix = "".join(random.choices(string.ascii_letters + "123456789", k=8))
135
+ self._file_name = f"view_saver_{suffix}"
136
+ # informs designer the property has changed so it will be saved.
137
+ update_property_for_widget(self, "fileName", self._file_name)
138
+
139
+ def _build_settings(self) -> QSettings | None:
140
+ """Build a QSettings object in IniFormat, or None macros are not expanded."""
141
+ name = self._file_name
142
+ if not name or "${" in name:
143
+ logger.error(f"View save path ''{self._file_name}'' not initialized.")
144
+ return None
145
+ dir_path = Path(os.path.expanduser(os.path.expandvars(self._dir_name)))
146
+ ini_path = str(dir_path / f"{name}.ini")
147
+ return QSettings(ini_path, QSettings.IniFormat)
148
+
149
+ def _initial_load(self) -> None:
150
+ """non-designer init that happens after all widgets load"""
151
+ settings = self._build_settings()
152
+ if settings is None:
153
+ return
154
+ self._tracked_widgets = self._resolve_tracked_widgets()
155
+ logger.debug(f"ViewSaver: loading from {settings.fileName()} ({len(self._tracked_widgets)} tracked widgets)")
156
+ restored = 0
157
+ for widget_name, prop_dict in self._tracked_widgets.items():
158
+ # restore any saved settings
159
+ for prop_name, (_getter, setter) in prop_dict.items():
160
+ saved_val = settings.value(f"{widget_name}/{prop_name}")
161
+ if saved_val is None:
162
+ logger.debug(f"No saved value for {widget_name}/{prop_name}")
163
+ continue
164
+ try:
165
+ setter(saved_val)
166
+ restored += 1
167
+ logger.debug(f"Restored {widget_name}/{prop_name} = {saved_val!r}")
168
+ except Exception:
169
+ logger.exception(f"Failed to restore {widget_name}/{prop_name}")
170
+ logger.debug(f"Restored {restored} value(s)")
171
+ self._loaded = True
172
+ self._poll_timer.start()
173
+
174
+ app = QApplication.instance()
175
+ if app is not None:
176
+ # Backstop for quit paths that tear the display down without ever
177
+ # firing closeEvent (e.g. PyDMMainWindow's File > Quit calling
178
+ # app.quit() directly), so the last poll interval isn't lost.
179
+ app.aboutToQuit.connect(self._flush_on_quit)
180
+
181
+ # closeEvent is only delivered to the top-level window, never to this
182
+ # hidden child widget, so watch the container window instead to catch
183
+ # the user closing just this screen while the app keeps running.
184
+ window = self.window()
185
+ if window is not None:
186
+ window.installEventFilter(self)
187
+
188
+ def _flush_on_quit(self) -> None:
189
+ """Write current widget state before the application exits."""
190
+ if not self._loaded:
191
+ return
192
+ logger.debug("aboutToQuit fired, saving view to file.")
193
+ self._save_settings()
194
+
195
+ def _save_settings(self) -> None:
196
+ settings = self._build_settings()
197
+ if settings is None:
198
+ return
199
+ saved = 0
200
+ for widget_name, prop_list in self._tracked_widgets.items():
201
+ if prop_list is None:
202
+ continue
203
+ for prop_name, (getter, _setter) in prop_list.items():
204
+ try:
205
+ val = getter()
206
+ settings.setValue(f"{widget_name}/{prop_name}", val)
207
+ saved += 1
208
+ logger.debug(f"Saved {widget_name}/{prop_name} = {val!r}")
209
+ except Exception:
210
+ logger.exception(f"Failed to save {widget_name}/{prop_name}")
211
+ logger.debug(f"Wrote {saved} value(s) to {settings.fileName()}")
212
+
213
+ # ------------------------------------------------------------------
214
+ # Widget Discovery
215
+ # ------------------------------------------------------------------
216
+
217
+ def _walk_widget_tree(self) -> list[QWidget]:
218
+ """Return every savable descendant.
219
+
220
+ A widget is savable if it defines ``get_view_saver_properties`` or
221
+ if its class is listed in ``WIDGET_REGISTRY``.
222
+
223
+ The child tree is walked manually so it stops as soon as a widget is
224
+ found savable. Non-savable containers and registered/duck-typed
225
+ containers are descended into recursively.
226
+ """
227
+ found: list[QWidget] = []
228
+
229
+ def _walk(widget: QWidget) -> None:
230
+ for child in widget.children():
231
+ if not isinstance(child, QWidget):
232
+ continue
233
+ # check if widget is saveable
234
+ if hasattr(child, "get_view_saver_properties") or is_registry_savable(child):
235
+ found.append(child)
236
+ # A savable container still holds nested savables; keep
237
+ # descending otherwise stop.
238
+ if getattr(child, "VIEW_SAVER_IS_CONTAINER", False) or is_registry_container(child):
239
+ _walk(child)
240
+ else:
241
+ _walk(child)
242
+
243
+ _walk(self)
244
+ return found
245
+
246
+ def _walk_widget_names(self) -> list[str]:
247
+ """Return the objectName of every savable descendant, skipping unnamed ones."""
248
+ return [name for w in self._walk_widget_tree() if (name := w.objectName())]
249
+
250
+ def _resolve_tracked_widgets(self) -> dict[str, dict[str, tuple[Callable, Callable]]]:
251
+ """Resolve savable child widgets into ``{objectName: {prop: (get, set)}}``.
252
+
253
+ Walks the child tree and resolve the getter/setter for each saveable widget's
254
+ properties.
255
+
256
+ Skips any excluded objectNames, and warns for savable widgets that lack an
257
+ objectName since they cannot be keyed in the settings file.
258
+ """
259
+ resolved: dict[str, dict[str, tuple[Callable, Callable]]] = {}
260
+ for widget in self._walk_widget_tree():
261
+ name = widget.objectName()
262
+ if not name:
263
+ logger.warning(
264
+ "ViewSaver: skipping savable %s with no objectName",
265
+ type(widget).__name__,
266
+ )
267
+ continue
268
+ if name in self._excluded:
269
+ continue
270
+ prop_list = self._resolve_widget_props(widget)
271
+ if prop_list:
272
+ resolved[name] = prop_list
273
+ return resolved
274
+
275
+ def _resolve_widget_props(
276
+ self,
277
+ widget: QWidget,
278
+ ) -> dict[str, tuple[Callable, Callable]] | None:
279
+ """Resolve getter/setter callables for each persisted property of *widget*.
280
+
281
+ widgets with ``get_view_saver_properties`` are resolved here; this is preferred.
282
+ built-in widgets are checked in the widget registry (see ``registry.resolve_registry_props``).
283
+
284
+ Returns a mapping ``{propKey: (getter, setter)}`` for the given widget
285
+ instance, or ``None`` if no savable properties could be resolved.
286
+ """
287
+ if hasattr(widget, "get_view_saver_properties"):
288
+ try:
289
+ return widget.get_view_saver_properties()
290
+ except Exception:
291
+ logger.exception(f"ViewSaver: get_view_saver_properties failed for {type(widget).__name__}")
292
+ return None
293
+ return resolve_registry_props(widget)
294
+
295
+ # ------------------------------------------------------------------
296
+ # Events
297
+ # ------------------------------------------------------------------
298
+
299
+ def eventFilter(self, obj: QObject, event: QEvent) -> bool: # noqa: N802
300
+ """Save when the watched container window is closing."""
301
+ if event.type() == QEvent.Close and self._loaded:
302
+ logger.debug("Window close event fired, saving view to file.")
303
+ self._save_settings()
304
+ return super().eventFilter(obj, event)
305
+
306
+ # ------------------------------------------------------------------
307
+ # UI
308
+ # ------------------------------------------------------------------
309
+
310
+ def sizeHint(self) -> QSize: # noqa: N802
311
+ """Default size when first dropped in Designer."""
312
+ return QSize(200, 200)
313
+
314
+ def paintEvent(self, event: QPaintEvent) -> None: # noqa: N802
315
+ """Draw a dashed outline and corner label, but only in Qt Designer.
316
+
317
+ At runtime nothing is painted so the container is fully transparent and
318
+ only its child widgets are visible.
319
+ """
320
+ if not is_qt_designer():
321
+ return
322
+ painter = QPainter(self)
323
+ painter.setRenderHint(QPainter.Antialiasing)
324
+
325
+ # Dashed border to mark the container bounds.
326
+ pen = QPen(QColor(90, 130, 200))
327
+ pen.setStyle(Qt.DashLine)
328
+ painter.setPen(pen)
329
+ painter.setBrush(Qt.NoBrush)
330
+ painter.drawRoundedRect(self.rect().adjusted(1, 1, -2, -2), 4, 4)
331
+
332
+ # Small corner label chip (kept out of the center so it never sits on
333
+ # top of child widgets).
334
+ font = QFont()
335
+ font.setPointSize(7)
336
+ painter.setFont(font)
337
+ metrics = painter.fontMetrics()
338
+ text = self._HINT_TEXT
339
+ chip = QRect(2, 2, metrics.horizontalAdvance(text) + 8, metrics.height() + 2)
340
+ painter.setPen(Qt.NoPen)
341
+ painter.setBrush(QColor(90, 130, 200, 210))
342
+ painter.drawRoundedRect(chip, 3, 3)
343
+ painter.setPen(QColor(255, 255, 255))
344
+ painter.drawText(chip, Qt.AlignCenter, text)
345
+ painter.end()
346
+
347
+ def open_dialog(self) -> None:
348
+ dialog = ViewSaverDialog(
349
+ available_widgets=self._walk_widget_names(),
350
+ excluded_widgets=list(self._excluded),
351
+ dir_name=self._dir_name,
352
+ file_name=self._file_name,
353
+ parent=self,
354
+ )
355
+ if dialog.exec_():
356
+ # set properties based on result
357
+ dir_name, file_name, excluded = dialog.results()
358
+ # informs designer the properties have changed for saves to ui file.
359
+ update_property_for_widget(self, "dirName", dir_name)
360
+ update_property_for_widget(self, "fileName", file_name)
361
+ update_property_for_widget(self, "excludedWidgets", list(excluded))
362
+
363
+ # ------------------------------------------------------------------
364
+ # Properties
365
+ # ------------------------------------------------------------------
366
+
367
+ def _get_dir_name(self) -> str:
368
+ return self._dir_name
369
+
370
+ def _set_dir_name(self, value: str) -> None:
371
+ self._dir_name = value
372
+
373
+ dirName = Property("QString", _get_dir_name, _set_dir_name)
374
+
375
+ def _get_file_name(self) -> str:
376
+ return self._file_name
377
+
378
+ def _set_file_name(self, value: str) -> None:
379
+ self._file_name = value
380
+
381
+ fileName = Property("QString", _get_file_name, _set_file_name)
382
+
383
+ def _get_excluded_widgets(self) -> list[str]:
384
+ return list(self._excluded)
385
+
386
+ def _set_excluded_widgets(self, value: list[str]) -> None:
387
+ self._excluded = list(value)
388
+
389
+ excludedWidgets = Property("QStringList", _get_excluded_widgets, _set_excluded_widgets)
@@ -0,0 +1,145 @@
1
+ """Dialog for editing ViewSaver settings in Qt Designer."""
2
+
3
+ from qtpy.QtCore import Qt
4
+ from qtpy.QtWidgets import (
5
+ QDialog,
6
+ QFileDialog,
7
+ QGroupBox,
8
+ QHBoxLayout,
9
+ QLabel,
10
+ QLineEdit,
11
+ QListWidget,
12
+ QListWidgetItem,
13
+ QPushButton,
14
+ QVBoxLayout,
15
+ QWidget,
16
+ )
17
+
18
+
19
+ class ViewSaverDialog(QDialog):
20
+ """Editor dialog for all ViewSaver settings.
21
+
22
+ Provides fields for the save directory and file name, plus a checklist of
23
+ the savable widgets discovered inside the container. Every discovered
24
+ widget is saved by default; unchecking one adds it to the excluded list so
25
+ it is skipped.
26
+ """
27
+
28
+ def __init__(
29
+ self,
30
+ available_widgets: list[str],
31
+ excluded_widgets: list[str],
32
+ dir_name: str,
33
+ file_name: str,
34
+ parent: QWidget | None = None,
35
+ ):
36
+ super().__init__(parent)
37
+ self.setWindowTitle("Edit ViewSaver Settings")
38
+ self.setMinimumWidth(450)
39
+
40
+ self._available_widgets: list[str] = list(available_widgets)
41
+
42
+ layout = QVBoxLayout(self)
43
+
44
+ # --- File settings group ---
45
+ file_group = QGroupBox("Save Location")
46
+ file_layout = QVBoxLayout(file_group)
47
+
48
+ # Directory
49
+ dir_row = QHBoxLayout()
50
+ dir_label = QLabel("Directory:")
51
+ dir_tooltip = "Folder where the .ini settings file will be stored."
52
+ dir_label.setToolTip(dir_tooltip)
53
+ self._dir_edit = QLineEdit(dir_name)
54
+ self._dir_edit.setToolTip(dir_tooltip)
55
+ dir_browse = QPushButton("Browse\u2026")
56
+ dir_browse.setToolTip("Open a file dialog to pick the save directory.")
57
+ dir_browse.clicked.connect(self._browse_dir)
58
+ dir_row.addWidget(dir_label)
59
+ dir_row.addWidget(self._dir_edit, stretch=1)
60
+ dir_row.addWidget(dir_browse)
61
+ file_layout.addLayout(dir_row)
62
+
63
+ # Filename
64
+ name_row = QHBoxLayout()
65
+ name_label = QLabel("File name:")
66
+ tool_tip = (
67
+ "Name of the .ini file (without extension).\n"
68
+ "Supports PyDM ${MACRO} expansion.\n"
69
+ "Auto-generated if left empty."
70
+ )
71
+ name_label.setToolTip(tool_tip)
72
+ self._name_edit = QLineEdit(file_name)
73
+ self._name_edit.setToolTip(tool_tip)
74
+ name_row.addWidget(name_label)
75
+ name_row.addWidget(self._name_edit, stretch=1)
76
+ file_layout.addLayout(name_row)
77
+
78
+ layout.addWidget(file_group)
79
+
80
+ # --- Saved widgets group ---
81
+ widget_list_group = QGroupBox("Saved Widgets")
82
+ bind_layout = QVBoxLayout(widget_list_group)
83
+ bind_layout.addWidget(QLabel("Widgets found inside this container. Uncheck any you do not want to persist."))
84
+
85
+ self._list = QListWidget()
86
+ excluded = set(excluded_widgets)
87
+ for name in self._available_widgets:
88
+ item = QListWidgetItem(name)
89
+ item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
90
+ item.setCheckState(Qt.Unchecked if name in excluded else Qt.Checked)
91
+ self._list.addItem(item)
92
+ bind_layout.addWidget(self._list)
93
+
94
+ # Check-all / uncheck-all convenience buttons.
95
+ select_row = QHBoxLayout()
96
+ check_all_btn = QPushButton("Save A&ll")
97
+ check_all_btn.setToolTip("Persist every discovered widget.")
98
+ check_all_btn.clicked.connect(lambda: self._set_all(Qt.Checked))
99
+ uncheck_all_btn = QPushButton("Save &None")
100
+ uncheck_all_btn.setToolTip("Exclude every discovered widget.")
101
+ uncheck_all_btn.clicked.connect(lambda: self._set_all(Qt.Unchecked))
102
+ select_row.addStretch()
103
+ select_row.addWidget(check_all_btn)
104
+ select_row.addWidget(uncheck_all_btn)
105
+ bind_layout.addLayout(select_row)
106
+
107
+ layout.addWidget(widget_list_group)
108
+
109
+ # --- OK / Cancel ---
110
+ button_row = QHBoxLayout()
111
+ button_row.addStretch()
112
+ cancel_btn = QPushButton("&Cancel")
113
+ cancel_btn.clicked.connect(self.reject)
114
+ ok_btn = QPushButton("&OK")
115
+ ok_btn.setDefault(True)
116
+ ok_btn.clicked.connect(self.accept)
117
+ button_row.addWidget(cancel_btn)
118
+ button_row.addWidget(ok_btn)
119
+ layout.addLayout(button_row)
120
+
121
+ # ------------------------------------------------------------------
122
+
123
+ def _browse_dir(self) -> None:
124
+ path = QFileDialog.getExistingDirectory(self, "Select Save Directory", self._dir_edit.text())
125
+ if path:
126
+ self._dir_edit.setText(path)
127
+
128
+ def _set_all(self, state: Qt.CheckState) -> None:
129
+ for i in range(self._list.count()):
130
+ self._list.item(i).setCheckState(state)
131
+
132
+ def _excluded_names(self) -> list[str]:
133
+ return [
134
+ self._list.item(i).text()
135
+ for i in range(self._list.count())
136
+ if self._list.item(i).checkState() == Qt.Unchecked
137
+ ]
138
+
139
+ def results(self) -> tuple[str, str, list[str]]:
140
+ """Return ``(dir_name, file_name, excluded_widget_names)``."""
141
+ return (
142
+ self._dir_edit.text(),
143
+ self._name_edit.text(),
144
+ self._excluded_names(),
145
+ )