TwoPy 0.1.4__tar.gz → 0.1.5__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 (137) hide show
  1. {twopy-0.1.4 → twopy-0.1.5}/PKG-INFO +3 -4
  2. {twopy-0.1.4 → twopy-0.1.5}/README.md +2 -3
  3. {twopy-0.1.4 → twopy-0.1.5}/pyproject.toml +1 -1
  4. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/__init__.py +2 -2
  5. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/napari/__init__.py +2 -2
  6. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/napari/controls.py +131 -261
  7. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/napari/launcher.py +9 -13
  8. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/napari/plotting/__init__.py +2 -2
  9. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/napari/plotting/dff_options.py +1 -1
  10. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/napari/plotting/docks/__init__.py +2 -2
  11. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/napari/plotting/docks/api.py +53 -20
  12. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/napari/plotting/docks/options_panel.py +4 -2
  13. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/napari/plotting/docks/response_plot_widget.py +2 -2
  14. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/napari/plotting/options.py +1 -27
  15. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/napari/plotting/processing_options.py +2 -1
  16. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/napari/plotting/response_window_options.py +1 -1
  17. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/napari/plotting/widgets.py +1 -1
  18. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/napari/roi.py +1 -1
  19. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/napari/session.py +4 -27
  20. twopy-0.1.5/src/twopy/napari/sidebar.py +123 -0
  21. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/napari/types.py +3 -6
  22. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/napari/viewer.py +19 -33
  23. {twopy-0.1.4 → twopy-0.1.5}/src/twopy.egg-info/PKG-INFO +3 -4
  24. {twopy-0.1.4 → twopy-0.1.5}/src/twopy.egg-info/SOURCES.txt +1 -1
  25. {twopy-0.1.4 → twopy-0.1.5}/tests/test_napari.py +155 -215
  26. twopy-0.1.4/src/twopy/napari/layout.py +0 -147
  27. {twopy-0.1.4 → twopy-0.1.5}/setup.cfg +0 -0
  28. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/_segments.py +0 -0
  29. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/_version.py +0 -0
  30. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/analysis/__init__.py +0 -0
  31. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/analysis/background_subtraction.py +0 -0
  32. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/analysis/dff.py +0 -0
  33. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/analysis/dff_options.py +0 -0
  34. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/analysis/epoch_mapping.py +0 -0
  35. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/analysis/motion.py +0 -0
  36. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/analysis/persistence.py +0 -0
  37. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/analysis/response_processing/__init__.py +0 -0
  38. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/analysis/response_processing/apply.py +0 -0
  39. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/analysis/response_processing/options.py +0 -0
  40. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/analysis/response_processing/persistence.py +0 -0
  41. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/analysis/response_processing/qc.py +0 -0
  42. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/analysis/response_processing/signals.py +0 -0
  43. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/analysis/response_window_options.py +0 -0
  44. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/analysis/responses.py +0 -0
  45. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/analysis/trials.py +0 -0
  46. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/analysis/workflow.py +0 -0
  47. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/api.py +0 -0
  48. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/config.py +0 -0
  49. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/conversion/__init__.py +0 -0
  50. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/conversion/alignment_crop.py +0 -0
  51. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/conversion/api.py +0 -0
  52. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/conversion/hdf5_writing.py +0 -0
  53. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/conversion/matlab_values.py +0 -0
  54. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/conversion/source_loading.py +0 -0
  55. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/conversion/stimulus_code.py +0 -0
  56. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/conversion/types.py +0 -0
  57. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/converted.py +0 -0
  58. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/database/__init__.py +0 -0
  59. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/database/cache.py +0 -0
  60. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/database/catalog.py +0 -0
  61. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/database/connection.py +0 -0
  62. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/database/generic.py +0 -0
  63. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/database/modeled.py +0 -0
  64. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/database/sql.py +0 -0
  65. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/database/types.py +0 -0
  66. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/frame_ranges.py +0 -0
  67. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/inspection.py +0 -0
  68. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/matlab.py +0 -0
  69. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/napari/__main__.py +0 -0
  70. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/napari/constants.py +0 -0
  71. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/napari/display.py +0 -0
  72. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/napari/display_paths.py +0 -0
  73. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/napari/interactive.py +0 -0
  74. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/napari/loading.py +0 -0
  75. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/napari/movie.py +0 -0
  76. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/napari/paths.py +0 -0
  77. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/napari/plotting/data.py +0 -0
  78. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/napari/plotting/docks/dynamic_options.py +0 -0
  79. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/napari/plotting/docks/paths.py +0 -0
  80. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/napari/plotting/docks/plot_area.py +0 -0
  81. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/napari/plotting/docks/save_actions.py +0 -0
  82. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/napari/plotting/docks/visibility_state.py +0 -0
  83. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/napari/plotting/export.py +0 -0
  84. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/napari/plotting/export_controls.py +0 -0
  85. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/napari/plotting/form_controls.py +0 -0
  86. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/napari/plotting/label_visibility.py +0 -0
  87. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/napari/plotting/panels.py +0 -0
  88. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/napari/protocols.py +0 -0
  89. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/napari/responses.py +0 -0
  90. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/napari/state.py +0 -0
  91. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/napari/text.py +0 -0
  92. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/parity/__init__.py +0 -0
  93. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/parity/dff.py +0 -0
  94. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/parity/psycho5.py +0 -0
  95. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/parity/roi.py +0 -0
  96. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/parity/saved_analysis.py +0 -0
  97. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/photodiode.py +0 -0
  98. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/photodiode_classification.py +0 -0
  99. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/roi.py +0 -0
  100. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/session.py +0 -0
  101. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/spatial.py +0 -0
  102. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/stimulus.py +0 -0
  103. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/synchronization.py +0 -0
  104. {twopy-0.1.4 → twopy-0.1.5}/src/twopy/typing_guards.py +0 -0
  105. {twopy-0.1.4 → twopy-0.1.5}/src/twopy.egg-info/dependency_links.txt +0 -0
  106. {twopy-0.1.4 → twopy-0.1.5}/src/twopy.egg-info/entry_points.txt +0 -0
  107. {twopy-0.1.4 → twopy-0.1.5}/src/twopy.egg-info/requires.txt +0 -0
  108. {twopy-0.1.4 → twopy-0.1.5}/src/twopy.egg-info/top_level.txt +0 -0
  109. {twopy-0.1.4 → twopy-0.1.5}/tests/test_api.py +0 -0
  110. {twopy-0.1.4 → twopy-0.1.5}/tests/test_background_subtraction.py +0 -0
  111. {twopy-0.1.4 → twopy-0.1.5}/tests/test_config.py +0 -0
  112. {twopy-0.1.4 → twopy-0.1.5}/tests/test_conversion.py +0 -0
  113. {twopy-0.1.4 → twopy-0.1.5}/tests/test_converted.py +0 -0
  114. {twopy-0.1.4 → twopy-0.1.5}/tests/test_database.py +0 -0
  115. {twopy-0.1.4 → twopy-0.1.5}/tests/test_dff.py +0 -0
  116. {twopy-0.1.4 → twopy-0.1.5}/tests/test_epoch_mapping.py +0 -0
  117. {twopy-0.1.4 → twopy-0.1.5}/tests/test_frame_ranges.py +0 -0
  118. {twopy-0.1.4 → twopy-0.1.5}/tests/test_import.py +0 -0
  119. {twopy-0.1.4 → twopy-0.1.5}/tests/test_inspection.py +0 -0
  120. {twopy-0.1.4 → twopy-0.1.5}/tests/test_matlab.py +0 -0
  121. {twopy-0.1.4 → twopy-0.1.5}/tests/test_motion.py +0 -0
  122. {twopy-0.1.4 → twopy-0.1.5}/tests/test_packaging.py +0 -0
  123. {twopy-0.1.4 → twopy-0.1.5}/tests/test_parity.py +0 -0
  124. {twopy-0.1.4 → twopy-0.1.5}/tests/test_parity_psycho5.py +0 -0
  125. {twopy-0.1.4 → twopy-0.1.5}/tests/test_persistence.py +0 -0
  126. {twopy-0.1.4 → twopy-0.1.5}/tests/test_photodiode_classification.py +0 -0
  127. {twopy-0.1.4 → twopy-0.1.5}/tests/test_real_fixture.py +0 -0
  128. {twopy-0.1.4 → twopy-0.1.5}/tests/test_response_processing.py +0 -0
  129. {twopy-0.1.4 → twopy-0.1.5}/tests/test_response_window_options.py +0 -0
  130. {twopy-0.1.4 → twopy-0.1.5}/tests/test_responses.py +0 -0
  131. {twopy-0.1.4 → twopy-0.1.5}/tests/test_roi.py +0 -0
  132. {twopy-0.1.4 → twopy-0.1.5}/tests/test_saved_analysis.py +0 -0
  133. {twopy-0.1.4 → twopy-0.1.5}/tests/test_session.py +0 -0
  134. {twopy-0.1.4 → twopy-0.1.5}/tests/test_stimulus.py +0 -0
  135. {twopy-0.1.4 → twopy-0.1.5}/tests/test_synchronization.py +0 -0
  136. {twopy-0.1.4 → twopy-0.1.5}/tests/test_trials.py +0 -0
  137. {twopy-0.1.4 → twopy-0.1.5}/tests/test_workflow.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: twopy
3
- Version: 0.1.4
3
+ Version: 0.1.5
4
4
  Summary: Two-photon imaging analysis tool with a napari interface.
5
5
  Author-email: Gustavo Madeira Santana <fromweb1@gumadeiras.com>
6
6
  License-Expression: MIT
@@ -66,9 +66,8 @@ Basic GUI flow:
66
66
 
67
67
  1. Choose a recording.
68
68
  2. Draw or edit ROIs in the `rois` Labels layer.
69
- 3. Click Save ROIs.
70
- 4. Update response plots from the current ROIs.
71
- 5. Save analysis outputs.
69
+ 3. Update response plots from the current ROIs.
70
+ 4. Click Save ROIs + analysis when the plots look right.
72
71
 
73
72
  When a source recording has not been converted yet, twopy converts it first. Converted data includes the aligned movie, mean image, stimulus tables, photodiode signals, and recording metadata.
74
73
 
@@ -46,9 +46,8 @@ Basic GUI flow:
46
46
 
47
47
  1. Choose a recording.
48
48
  2. Draw or edit ROIs in the `rois` Labels layer.
49
- 3. Click Save ROIs.
50
- 4. Update response plots from the current ROIs.
51
- 5. Save analysis outputs.
49
+ 3. Update response plots from the current ROIs.
50
+ 4. Click Save ROIs + analysis when the plots look right.
52
51
 
53
52
  When a source recording has not been converted yet, twopy converts it first. Converted data includes the aligned movie, mean image, stimulus tables, photodiode signals, and recording metadata.
54
53
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "twopy"
7
- version = "0.1.4"
7
+ version = "0.1.5"
8
8
  description = "Two-photon imaging analysis tool with a napari interface."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.13,<3.14"
@@ -57,8 +57,8 @@ from twopy.api import find_recordings
57
57
  from twopy.conversion import convert_recording_to_twopy, load_source_conversion_inputs
58
58
  from twopy.converted import load_converted_recording, recording_frame_rate_hz
59
59
  from twopy.napari import (
60
- NapariControlDocks,
61
60
  NapariRecordingView,
61
+ NapariSidebarWidgets,
62
62
  add_twopy_magicgui_controls,
63
63
  launch_napari,
64
64
  open_recording_in_napari,
@@ -149,8 +149,8 @@ __all__ = [
149
149
  "make_roi_set",
150
150
  "map_stimulus_epochs_to_frame_windows",
151
151
  "map_stimulus_specific_column",
152
- "NapariControlDocks",
153
152
  "NapariRecordingView",
153
+ "NapariSidebarWidgets",
154
154
  "open_recording_in_napari",
155
155
  "pair_photodiode_events_to_imaging_frames",
156
156
  "PhotodiodeAlignment",
@@ -9,7 +9,7 @@ database, ROI, and analysis modules stay independent so a future napari plugin
9
9
  can wrap these helpers instead of owning scientific logic.
10
10
  """
11
11
 
12
- from twopy.napari.controls import NapariControlDocks, add_twopy_magicgui_controls
12
+ from twopy.napari.controls import NapariSidebarWidgets, add_twopy_magicgui_controls
13
13
  from twopy.napari.launcher import launch_napari, main
14
14
  from twopy.napari.roi import roi_label_image_from_layer, save_napari_label_rois
15
15
  from twopy.napari.types import NapariRecordingView
@@ -17,7 +17,7 @@ from twopy.napari.viewer import open_recording_in_napari
17
17
 
18
18
  __all__ = [
19
19
  "NapariRecordingView",
20
- "NapariControlDocks",
20
+ "NapariSidebarWidgets",
21
21
  "add_twopy_magicgui_controls",
22
22
  "launch_napari",
23
23
  "main",
@@ -1,11 +1,11 @@
1
1
  """Magicgui controls for the twopy napari adapter.
2
2
 
3
3
  Inputs: napari viewer, current ROI Labels layer, and paths chosen by the user.
4
- Outputs: small dock widgets for loading converted recordings and saving ROI
5
- masks.
4
+ Outputs: one tabbed right-sidebar dock for loading converted recordings,
5
+ tracking loaded recordings, and controlling response plots.
6
6
 
7
- The control panel owns GUI callbacks only. Loading recordings and saving ROIs
8
- still go through the same typed helpers used by scripts.
7
+ The controls own GUI callbacks only. Loading recordings still goes through the
8
+ same typed helpers used by scripts.
9
9
  """
10
10
 
11
11
  from collections.abc import Callable
@@ -13,27 +13,18 @@ from dataclasses import dataclass, field
13
13
  from pathlib import Path
14
14
  from typing import Protocol, cast
15
15
 
16
- import numpy as np
17
16
  from magicgui.widgets import FileEdit
17
+ from qtpy.QtCore import Qt
18
+ from qtpy.QtWidgets import QFormLayout, QLabel, QSizePolicy, QVBoxLayout, QWidget
18
19
 
19
20
  from twopy.converted import RecordingData
20
21
  from twopy.napari.constants import DEFAULT_PATH_TEXT
21
- from twopy.napari.layout import (
22
- place_loaded_recordings_dock_after_load,
23
- place_save_rois_dock_before_layer_list,
24
- )
25
22
  from twopy.napari.loading import resolve_or_convert_recording
26
- from twopy.napari.movie import resolve_movie_frame_range
27
23
  from twopy.napari.paths import (
28
24
  PathInput,
29
25
  is_default_path,
30
- resolve_widget_output_path,
31
26
  )
32
27
  from twopy.napari.protocols import NapariViewer
33
- from twopy.napari.roi import (
34
- roi_label_image_from_layer_for_recording,
35
- save_napari_label_rois,
36
- )
37
28
  from twopy.napari.session import (
38
29
  LoadedNapariRecording,
39
30
  LoadedRecordingsPanel,
@@ -41,48 +32,33 @@ from twopy.napari.session import (
41
32
  render_loaded_recordings_panel,
42
33
  select_loaded_recording,
43
34
  unload_loaded_recording,
44
- update_selected_roi_save_file,
35
+ )
36
+ from twopy.napari.sidebar import (
37
+ TWOPY_SIDEBAR_MINIMUM_WIDTH,
38
+ create_twopy_sidebar_widget,
45
39
  )
46
40
  from twopy.napari.state import (
47
41
  read_last_recording_folder,
48
42
  recording_folder_for_state,
49
43
  write_last_recording_folder,
50
44
  )
51
- from twopy.napari.text import counted_noun
52
-
53
- __all__ = ["NapariControlDocks", "add_twopy_magicgui_controls"]
54
45
 
55
-
56
- class _RoiSavePathReceiver(Protocol):
57
- """Small protocol for widgets that mirror the selected ROI save path."""
58
-
59
- def set_roi_save_file(self, roi_save_file: Path | None) -> None:
60
- """Store the ROI HDF5 path for the selected recording.
61
-
62
- Args:
63
- roi_save_file: ROI output path, or ``None`` when no recording is
64
- selected.
65
-
66
- Returns:
67
- None.
68
- """
69
- ...
46
+ __all__ = ["NapariSidebarWidgets", "add_twopy_magicgui_controls"]
70
47
 
71
48
 
72
49
  @dataclass
73
- class NapariControlDocks:
74
- """Dock widgets created for twopy napari controls.
50
+ class NapariSidebarWidgets:
51
+ """Widgets created for the twopy napari right sidebar.
75
52
 
76
53
  Inputs: Qt widgets and napari dock objects.
77
54
  Outputs: grouped references for scripts and tests.
78
55
  """
79
56
 
80
57
  load_widget: object
81
- load_dock_widget: object
82
58
  loaded_recordings_widget: object
83
- loaded_recordings_dock_widget: object
84
- save_rois_widget: object
85
- save_rois_dock_widget: object
59
+ response_options_widget: object | None
60
+ sidebar_widget: object
61
+ sidebar_dock_widget: object
86
62
 
87
63
 
88
64
  @dataclass
@@ -111,6 +87,17 @@ class NapariControlState:
111
87
  replace_selected_on_next_load: bool = False
112
88
 
113
89
 
90
+ class _LoadRecordingGui(Protocol):
91
+ """Small shape of the magicgui recording-load callable.
92
+
93
+ Inputs: magicgui function widget created in ``_make_twopy_load_widget``.
94
+ Outputs: access to the child controls that need compact Qt placement.
95
+ """
96
+
97
+ recording_folder: object
98
+ roi_file_to_load: object
99
+
100
+
114
101
  def add_twopy_magicgui_controls(
115
102
  viewer: NapariViewer,
116
103
  *,
@@ -118,12 +105,13 @@ def add_twopy_magicgui_controls(
118
105
  roi_save_file: Path,
119
106
  recording: RecordingData | None = None,
120
107
  response_plot_widget: object | None = None,
108
+ response_options_widget: object | None = None,
121
109
  mean_image_layer: object | None = None,
122
110
  movie_layer: object | None = None,
123
111
  dock_name: str = "twopy",
124
112
  dock_area: str = "right",
125
- ) -> NapariControlDocks:
126
- """Add the first twopy magicgui control panel to a napari viewer.
113
+ ) -> NapariSidebarWidgets:
114
+ """Add the twopy right sidebar to a napari viewer.
127
115
 
128
116
  Args:
129
117
  viewer: Napari viewer that should receive the dock widget.
@@ -134,6 +122,8 @@ def add_twopy_magicgui_controls(
134
122
  recording: Optional loaded recording used to populate GUI defaults.
135
123
  response_plot_widget: Optional response plotting widget to refresh
136
124
  after a new recording is loaded.
125
+ response_options_widget: Optional response-options tabs to place in the
126
+ same right sidebar as the Load tab.
137
127
  mean_image_layer: Optional mean-image layer for an already loaded
138
128
  startup recording.
139
129
  movie_layer: Optional movie layer for an already loaded startup
@@ -142,11 +132,12 @@ def add_twopy_magicgui_controls(
142
132
  dock_area: Napari dock area.
143
133
 
144
134
  Returns:
145
- Created load and save-ROI widgets plus their napari dock widgets.
135
+ Created load, loaded-recordings, and sidebar widgets plus the napari
136
+ dock widget that contains them.
146
137
 
147
- The panel is intentionally small. It loads converted recordings and saves
148
- the current Labels layer through the same helpers used by scripts, so a
149
- future plugin can wrap this without changing ROI semantics.
138
+ The sidebar keeps twopy's right-side controls in one tabbed dock. The loader
139
+ itself stays small and uses the same helpers as scripts, so a future plugin
140
+ can wrap this without changing recording-load semantics.
150
141
  """
151
142
  state = NapariControlState(
152
143
  viewer=viewer,
@@ -168,69 +159,46 @@ def add_twopy_magicgui_controls(
168
159
  )
169
160
  state.selected_recording_index = 0
170
161
  load_widget = _make_twopy_load_widget(state)
171
- load_dock_widget = viewer.window.add_dock_widget(
172
- load_widget,
173
- name=dock_name,
174
- area=dock_area,
175
- )
176
162
  loaded_recordings_widget = _make_loaded_recordings_widget(state)
177
- loaded_recordings_dock_widget = viewer.window.add_dock_widget(
178
- loaded_recordings_widget,
179
- name="twopy loaded recordings",
180
- area=dock_area,
181
- )
182
- place_loaded_recordings_dock_after_load(
183
- viewer,
184
- load_dock_widget,
185
- loaded_recordings_dock_widget,
163
+ sidebar_widget = create_twopy_sidebar_widget(
164
+ load_widget=load_widget,
165
+ loaded_recordings_widget=loaded_recordings_widget,
166
+ response_options_widget=response_options_widget,
186
167
  )
187
- save_rois_widget = _make_twopy_save_rois_widget(state)
188
- save_rois_dock_widget = viewer.window.add_dock_widget(
189
- save_rois_widget,
190
- name="twopy save ROIs",
191
- area="left",
168
+ sidebar_dock_widget = viewer.window.add_dock_widget(
169
+ sidebar_widget,
170
+ name=dock_name,
171
+ area=dock_area,
192
172
  )
193
- place_save_rois_dock_before_layer_list(viewer, save_rois_dock_widget)
194
- return NapariControlDocks(
173
+ if isinstance(sidebar_dock_widget, QWidget):
174
+ sidebar_dock_widget.setMinimumWidth(TWOPY_SIDEBAR_MINIMUM_WIDTH)
175
+ return NapariSidebarWidgets(
195
176
  load_widget=load_widget,
196
- load_dock_widget=load_dock_widget,
197
177
  loaded_recordings_widget=loaded_recordings_widget,
198
- loaded_recordings_dock_widget=loaded_recordings_dock_widget,
199
- save_rois_widget=save_rois_widget,
200
- save_rois_dock_widget=save_rois_dock_widget,
178
+ response_options_widget=response_options_widget,
179
+ sidebar_widget=sidebar_widget,
180
+ sidebar_dock_widget=sidebar_dock_widget,
201
181
  )
202
182
 
203
183
 
204
184
  def _make_twopy_load_widget(state: NapariControlState) -> object:
205
- """Create the right-side magicgui widget for loading recordings.
185
+ """Create the right-side widget for loading recordings.
206
186
 
207
187
  Args:
208
188
  state: Mutable napari control state.
209
189
 
210
190
  Returns:
211
- magicgui container with recording-load controls.
191
+ Qt widget with compact recording-load controls.
212
192
  """
213
193
  from magicgui import magicgui
214
- from magicgui.widgets import Container, Label
215
-
216
- default_movie_end_frame = _default_movie_end_frame(state.recording)
217
194
 
218
195
  @magicgui(
219
196
  call_button=False,
220
197
  layout="vertical",
221
- movie_frame_range={
222
- "widget_type": "RangeSlider",
223
- "min": 0,
224
- "max": max(default_movie_end_frame, 0),
225
- "step": 1,
226
- "tracking": False,
227
- },
228
198
  )
229
199
  def load_recording(
230
200
  recording_folder: Path = Path(DEFAULT_PATH_TEXT),
231
201
  roi_file_to_load: Path = Path(DEFAULT_PATH_TEXT),
232
- movie_frame_range: tuple[int, int] = (0, default_movie_end_frame),
233
- load_movie: bool = True,
234
202
  ) -> str:
235
203
  """Load a recording into the current napari viewer.
236
204
 
@@ -240,11 +208,6 @@ def _make_twopy_load_widget(state: NapariControlState) -> object:
240
208
  first when twopy output does not exist yet.
241
209
  roi_file_to_load: Optional existing ROI HDF5 file. ``default`` uses
242
210
  ``rois.h5`` in the selected recording folder when it exists.
243
- movie_frame_range: First and last movie frames to load. When the
244
- empty-launch widget still shows ``(0, 0)``, twopy resolves it
245
- to the recording's full frame range after the folder is
246
- selected.
247
- load_movie: Whether to load movie frames.
248
211
 
249
212
  Returns:
250
213
  Human-readable status for the dock widget.
@@ -266,23 +229,6 @@ def _make_twopy_load_widget(state: NapariControlState) -> object:
266
229
  if is_default_path(roi_file_to_load)
267
230
  else _resolve_optional_roi_path(roi_file_to_load)
268
231
  )
269
- loaded_recording = _load_recording_for_defaults(
270
- recording_data_path=paths.recording_data_path,
271
- movie_path=paths.movie_path,
272
- )
273
- movie_range = None
274
- if load_movie:
275
- start_frame, end_frame = _widget_movie_frame_range_for_load(
276
- movie_frame_range,
277
- frame_count=loaded_recording.movie.shape[0],
278
- preserve_current_range=replace_selected,
279
- )
280
- movie_range = resolve_movie_frame_range(
281
- start_frame=start_frame,
282
- end_frame=end_frame,
283
- frame_count=loaded_recording.movie.shape[0],
284
- zero_end_means_last=True,
285
- )
286
232
 
287
233
  # Import here to avoid a top-level cycle: viewer creates controls,
288
234
  # while controls can load recordings into that same viewer.
@@ -294,7 +240,7 @@ def _make_twopy_load_widget(state: NapariControlState) -> object:
294
240
  roi_set=roi_path,
295
241
  roi_save_file=paths.roi_save_file,
296
242
  movie_path=paths.movie_path,
297
- movie_frame_range=movie_range,
243
+ movie_frame_range=(0, None),
298
244
  add_controls=False,
299
245
  )
300
246
  record_loaded_view(
@@ -309,9 +255,6 @@ def _make_twopy_load_widget(state: NapariControlState) -> object:
309
255
  paths.recording_data_path,
310
256
  ),
311
257
  )
312
- frame_end = _default_movie_end_frame(view.recording)
313
- load_recording.movie_frame_range.max = frame_end
314
- load_recording.movie_frame_range.value = movie_range or (0, frame_end)
315
258
  _set_recording_picker_display_path(
316
259
  cast(FileEdit, load_recording.recording_folder),
317
260
  state,
@@ -328,10 +271,10 @@ def _make_twopy_load_widget(state: NapariControlState) -> object:
328
271
  load_recording.recording_folder.mode = "d"
329
272
  load_recording.roi_file_to_load.mode = "r"
330
273
 
331
- load_recording.recording_folder.label = "recording folder or file"
332
- load_recording.roi_file_to_load.label = "ROI file to load"
333
- load_recording.movie_frame_range.label = "movie frames"
334
- load_recording.load_movie.label = "load movie"
274
+ load_recording.recording_folder.label = "Recording"
275
+ load_recording.roi_file_to_load.label = "ROI file"
276
+ cast(FileEdit, load_recording.recording_folder).choose_btn.text = "Browse"
277
+ cast(FileEdit, load_recording.roi_file_to_load).choose_btn.text = "Browse"
335
278
  _configure_recording_folder_picker(
336
279
  cast(FileEdit, load_recording.recording_folder),
337
280
  state,
@@ -354,8 +297,8 @@ def _make_twopy_load_widget(state: NapariControlState) -> object:
354
297
  return
355
298
  load_recording()
356
299
 
357
- def reload_after_option_change(_value: object) -> None:
358
- """Reload the current recording after a non-path option changes.
300
+ def reload_after_roi_change(_value: object) -> None:
301
+ """Reload the current recording after the ROI file changes.
359
302
 
360
303
  Args:
361
304
  _value: Ignored widget signal value.
@@ -363,9 +306,8 @@ def _make_twopy_load_widget(state: NapariControlState) -> object:
363
306
  Returns:
364
307
  None.
365
308
 
366
- Secondary controls such as the movie-frame slider should reuse the
367
- resolved recording path stored in state. The visible picker text may be
368
- shortened for display and is not always a real filesystem path.
309
+ The visible picker text may be shortened for display and is not always a
310
+ real filesystem path, so reloads reuse the stored recording path.
369
311
  """
370
312
  if state.is_loading or not _has_recording_to_load(
371
313
  load_recording.recording_folder.value,
@@ -381,18 +323,75 @@ def _make_twopy_load_widget(state: NapariControlState) -> object:
381
323
  )
382
324
 
383
325
  load_recording.recording_folder.changed.connect(load_after_selection)
384
- load_recording.roi_file_to_load.changed.connect(reload_after_option_change)
385
- load_recording.movie_frame_range.changed.connect(reload_after_option_change)
386
- load_recording.load_movie.changed.connect(reload_after_option_change)
387
-
388
- return Container(
389
- widgets=[
390
- Label(value="Load Recording"),
391
- load_recording,
392
- ],
393
- layout="vertical",
394
- labels=False,
395
- )
326
+ load_recording.roi_file_to_load.changed.connect(reload_after_roi_change)
327
+
328
+ return LoadRecordingPanel(load_recording=load_recording)
329
+
330
+
331
+ class LoadRecordingPanel(QWidget):
332
+ """Compact Qt panel that owns the recording-load controls.
333
+
334
+ Args:
335
+ load_recording: Magicgui callable widget whose child controls and
336
+ callbacks perform recording loading.
337
+
338
+ Outputs:
339
+ QWidget with a tight form layout for the Load tab.
340
+
341
+ The magicgui callable still owns validation and callback wiring. This panel
342
+ only arranges the existing controls with shorter labels so the path fields
343
+ and frame slider have enough horizontal room.
344
+ """
345
+
346
+ load_recording: object
347
+
348
+ def __init__(self, *, load_recording: object) -> None:
349
+ """Create a compact Load Recording panel.
350
+
351
+ Args:
352
+ load_recording: Magicgui callable widget built by
353
+ ``_make_twopy_load_widget``.
354
+
355
+ Returns:
356
+ None.
357
+ """
358
+ super().__init__()
359
+ self.load_recording = load_recording
360
+
361
+ layout = QVBoxLayout()
362
+ layout.setContentsMargins(0, 0, 0, 0)
363
+ layout.setSpacing(8)
364
+ title = QLabel("Load Recording")
365
+ title.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Fixed)
366
+ layout.addWidget(title)
367
+
368
+ recording_gui = cast(_LoadRecordingGui, load_recording)
369
+ form = QFormLayout()
370
+ form.setContentsMargins(0, 0, 0, 0)
371
+ form.setHorizontalSpacing(8)
372
+ form.setVerticalSpacing(8)
373
+ form.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow)
374
+ form.setLabelAlignment(Qt.AlignmentFlag.AlignRight)
375
+ form.addRow("Recording", _control_native(recording_gui.recording_folder))
376
+ form.addRow("ROI file", _control_native(recording_gui.roi_file_to_load))
377
+ layout.addLayout(form)
378
+ self.setLayout(layout)
379
+
380
+
381
+ def _control_native(widget: object) -> QWidget:
382
+ """Return the Qt widget wrapped by a magicgui control.
383
+
384
+ Args:
385
+ widget: Magicgui widget or Qt widget.
386
+
387
+ Returns:
388
+ QWidget that can be inserted into the compact Load form.
389
+ """
390
+ native_widget = getattr(widget, "native", widget)
391
+ if not isinstance(native_widget, QWidget):
392
+ msg = f"Load control expected a QWidget, got {type(native_widget).__name__}."
393
+ raise TypeError(msg)
394
+ return native_widget
396
395
 
397
396
 
398
397
  def _make_loaded_recordings_widget(state: NapariControlState) -> object:
@@ -413,63 +412,6 @@ def _make_loaded_recordings_widget(state: NapariControlState) -> object:
413
412
  return panel
414
413
 
415
414
 
416
- def _make_twopy_save_rois_widget(state: NapariControlState) -> object:
417
- """Create the left-side magicgui widget for saving ROI labels.
418
-
419
- Args:
420
- state: Mutable napari control state.
421
-
422
- Returns:
423
- magicgui widget with one Save ROIs button.
424
- """
425
- from magicgui import magicgui
426
-
427
- @magicgui(call_button="Save ROIs", layout="vertical")
428
- def save_rois(roi_save_file: Path = Path(DEFAULT_PATH_TEXT)) -> str:
429
- """Save current napari ROI labels to twopy ROI HDF5.
430
-
431
- Args:
432
- roi_save_file: Destination ROI HDF5 path. ``default`` uses the
433
- current recording's default ROI output path.
434
-
435
- Returns:
436
- Human-readable status for the dock widget.
437
- """
438
- if state.roi_labels_layer is None:
439
- return "No ROI Labels layer is available."
440
- if state.recording is None:
441
- return "No recording is selected."
442
- try:
443
- label_image = roi_label_image_from_layer_for_recording(
444
- state.roi_labels_layer,
445
- state.recording,
446
- )
447
- except ValueError as error:
448
- return str(error)
449
- if not np.any(label_image > 0):
450
- return "No ROI labels to save."
451
-
452
- resolved_output_path = resolve_widget_output_path(
453
- roi_save_file,
454
- default=state.roi_save_file,
455
- )
456
- roi_set = save_napari_label_rois(label_image, resolved_output_path)
457
- state.roi_save_file = resolved_output_path
458
- update_selected_roi_save_file(state, resolved_output_path)
459
- if hasattr(state.response_plot_widget, "set_roi_save_file"):
460
- cast(_RoiSavePathReceiver, state.response_plot_widget).set_roi_save_file(
461
- resolved_output_path
462
- )
463
- return (
464
- f"Saved {counted_noun(len(roi_set.labels), 'ROI', 'ROIs')} "
465
- f"to {resolved_output_path}"
466
- )
467
-
468
- save_rois.roi_save_file.mode = "w"
469
- save_rois.roi_save_file.label = "ROI save file"
470
- return save_rois
471
-
472
-
473
415
  def _configure_recording_folder_picker(
474
416
  widget: FileEdit,
475
417
  state: NapariControlState,
@@ -706,75 +648,3 @@ def _resolve_optional_roi_path(path: PathInput) -> Path | None:
706
648
  msg = f"ROI file does not exist: {resolved}"
707
649
  raise ValueError(msg)
708
650
  return resolved.resolve()
709
-
710
-
711
- def _default_movie_end_frame(recording: RecordingData | None) -> int:
712
- """Return the default inclusive movie end frame for a loaded recording.
713
-
714
- Args:
715
- recording: Optional loaded recording.
716
-
717
- Returns:
718
- Last available frame index, or ``0`` before a recording is loaded.
719
- """
720
- if recording is None:
721
- return 0
722
- return max(recording.movie.shape[0] - 1, 0)
723
-
724
-
725
- def _widget_movie_frame_range_for_load(
726
- movie_frame_range: tuple[int, int],
727
- *,
728
- frame_count: int,
729
- preserve_current_range: bool,
730
- ) -> tuple[int, int]:
731
- """Choose a GUI movie preview range for the recording being loaded.
732
-
733
- Args:
734
- movie_frame_range: Current range-slider value.
735
- frame_count: Number of movie frames in the recording being loaded.
736
- preserve_current_range: Whether this is a reload of the selected
737
- recording after an option change.
738
-
739
- Returns:
740
- Inclusive preview range constrained to the recording length. New
741
- recordings default to their full movie, while reloads preserve the
742
- visible slider value where possible.
743
-
744
- The frame-range widget keeps its previous value until after a recording
745
- loads. That value is meaningful when reloading the same recording after an
746
- option change, but stale when choosing a different recording.
747
- """
748
- last_frame = max(frame_count - 1, 0)
749
- if not preserve_current_range:
750
- return (0, last_frame)
751
-
752
- start_frame = int(movie_frame_range[0])
753
- end_frame = int(movie_frame_range[1])
754
- if start_frame > last_frame:
755
- start_frame = last_frame
756
- if end_frame > last_frame:
757
- end_frame = last_frame
758
- return (start_frame, end_frame)
759
-
760
-
761
- def _load_recording_for_defaults(
762
- *,
763
- recording_data_path: Path,
764
- movie_path: Path | None,
765
- ) -> RecordingData:
766
- """Load recording metadata before resolving frame defaults.
767
-
768
- Args:
769
- recording_data_path: Converted ``recording_data.h5`` path.
770
- movie_path: Optional explicit converted movie path.
771
-
772
- Returns:
773
- Loaded recording object.
774
-
775
- Loading the small manifest lets the empty-launch widget convert its initial
776
- ``0`` end frame into the actual final frame before adding the movie layer.
777
- """
778
- from twopy.converted import load_converted_recording
779
-
780
- return load_converted_recording(recording_data_path, movie_path=movie_path)