psychopy 2025.1.1__py3-none-any.whl → 2025.2.1__py3-none-any.whl

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.

Potentially problematic release.


This version of psychopy might be problematic. Click here for more details.

Files changed (220) hide show
  1. psychopy/VERSION +1 -1
  2. psychopy/alerts/alertsCatalogue/4810.yaml +19 -0
  3. psychopy/alerts/alertsCatalogue/alertCategories.yaml +4 -0
  4. psychopy/alerts/alertsCatalogue/alertmsg.py +15 -1
  5. psychopy/alerts/alertsCatalogue/generateAlertmsg.py +2 -2
  6. psychopy/app/Resources/classic/add_many.png +0 -0
  7. psychopy/app/Resources/classic/add_many@2x.png +0 -0
  8. psychopy/app/Resources/classic/devices.png +0 -0
  9. psychopy/app/Resources/classic/devices@2x.png +0 -0
  10. psychopy/app/Resources/classic/photometer.png +0 -0
  11. psychopy/app/Resources/classic/photometer@2x.png +0 -0
  12. psychopy/app/Resources/dark/add_many.png +0 -0
  13. psychopy/app/Resources/dark/add_many@2x.png +0 -0
  14. psychopy/app/Resources/dark/devices.png +0 -0
  15. psychopy/app/Resources/dark/devices@2x.png +0 -0
  16. psychopy/app/Resources/dark/photometer.png +0 -0
  17. psychopy/app/Resources/dark/photometer@2x.png +0 -0
  18. psychopy/app/Resources/light/add_many.png +0 -0
  19. psychopy/app/Resources/light/add_many@2x.png +0 -0
  20. psychopy/app/Resources/light/devices.png +0 -0
  21. psychopy/app/Resources/light/devices@2x.png +0 -0
  22. psychopy/app/Resources/light/photometer.png +0 -0
  23. psychopy/app/Resources/light/photometer@2x.png +0 -0
  24. psychopy/app/_psychopyApp.py +35 -13
  25. psychopy/app/builder/builder.py +88 -35
  26. psychopy/app/builder/dialogs/__init__.py +69 -220
  27. psychopy/app/builder/dialogs/dlgsCode.py +29 -8
  28. psychopy/app/builder/dialogs/paramCtrls.py +1468 -904
  29. psychopy/app/builder/validators.py +25 -17
  30. psychopy/app/coder/coder.py +12 -1
  31. psychopy/app/coder/repl.py +5 -2
  32. psychopy/app/colorpicker/__init__.py +1 -1
  33. psychopy/app/deviceManager/__init__.py +1 -0
  34. psychopy/app/deviceManager/addDialog.py +218 -0
  35. psychopy/app/deviceManager/dialog.py +185 -0
  36. psychopy/app/deviceManager/panel.py +191 -0
  37. psychopy/app/deviceManager/utils.py +60 -0
  38. psychopy/app/idle.py +7 -0
  39. psychopy/app/locale/ar_001/LC_MESSAGE/messages.mo +0 -0
  40. psychopy/app/locale/ar_001/LC_MESSAGE/messages.po +12695 -10592
  41. psychopy/app/locale/cs_CZ/LC_MESSAGE/messages.mo +0 -0
  42. psychopy/app/locale/cs_CZ/LC_MESSAGE/messages.po +10199 -24
  43. psychopy/app/locale/da_DK/LC_MESSAGE/messages.mo +0 -0
  44. psychopy/app/locale/da_DK/LC_MESSAGE/messages.po +10199 -24
  45. psychopy/app/locale/de_DE/LC_MESSAGE/messages.mo +0 -0
  46. psychopy/app/locale/de_DE/LC_MESSAGE/messages.po +11221 -9712
  47. psychopy/app/locale/el_GR/LC_MESSAGE/messages.mo +0 -0
  48. psychopy/app/locale/el_GR/LC_MESSAGE/messages.po +10200 -25
  49. psychopy/app/locale/en_NZ/LC_MESSAGE/messages.mo +0 -0
  50. psychopy/app/locale/en_NZ/LC_MESSAGE/messages.po +10200 -25
  51. psychopy/app/locale/en_US/LC_MESSAGE/messages.mo +0 -0
  52. psychopy/app/locale/en_US/LC_MESSAGE/messages.po +10195 -18
  53. psychopy/app/locale/es_CO/LC_MESSAGE/messages.mo +0 -0
  54. psychopy/app/locale/es_CO/LC_MESSAGE/messages.po +11917 -9101
  55. psychopy/app/locale/es_ES/LC_MESSAGE/messages.mo +0 -0
  56. psychopy/app/locale/es_ES/LC_MESSAGE/messages.po +11924 -9103
  57. psychopy/app/locale/es_US/LC_MESSAGE/messages.mo +0 -0
  58. psychopy/app/locale/es_US/LC_MESSAGE/messages.po +11917 -9101
  59. psychopy/app/locale/et_EE/LC_MESSAGE/messages.mo +0 -0
  60. psychopy/app/locale/et_EE/LC_MESSAGE/messages.po +11084 -9569
  61. psychopy/app/locale/fa_IR/LC_MESSAGE/messages.mo +0 -0
  62. psychopy/app/locale/fa_IR/LC_MESSAGE/messages.po +11590 -5806
  63. psychopy/app/locale/fi_FI/LC_MESSAGE/messages.mo +0 -0
  64. psychopy/app/locale/fi_FI/LC_MESSAGE/messages.po +10199 -24
  65. psychopy/app/locale/fr_FR/LC_MESSAGE/messages.mo +0 -0
  66. psychopy/app/locale/fr_FR/LC_MESSAGE/messages.po +11091 -9577
  67. psychopy/app/locale/he_IL/LC_MESSAGE/messages.mo +0 -0
  68. psychopy/app/locale/he_IL/LC_MESSAGE/messages.po +11072 -9549
  69. psychopy/app/locale/hi_IN/LC_MESSAGE/messages.mo +0 -0
  70. psychopy/app/locale/hi_IN/LC_MESSAGE/messages.po +11071 -9559
  71. psychopy/app/locale/hu_HU/LC_MESSAGE/messages.mo +0 -0
  72. psychopy/app/locale/hu_HU/LC_MESSAGE/messages.po +10200 -25
  73. psychopy/app/locale/it_IT/LC_MESSAGE/messages.mo +0 -0
  74. psychopy/app/locale/it_IT/LC_MESSAGE/messages.po +11072 -9560
  75. psychopy/app/locale/ja_JP/LC_MESSAGE/messages.mo +0 -0
  76. psychopy/app/locale/ja_JP/LC_MESSAGE/messages.po +1485 -1137
  77. psychopy/app/locale/ko_KR/LC_MESSAGE/messages.mo +0 -0
  78. psychopy/app/locale/ko_KR/LC_MESSAGE/messages.po +10199 -24
  79. psychopy/app/locale/ms_MY/LC_MESSAGE/messages.mo +0 -0
  80. psychopy/app/locale/ms_MY/LC_MESSAGE/messages.po +11463 -8757
  81. psychopy/app/locale/nl_NL/LC_MESSAGE/messages.mo +0 -0
  82. psychopy/app/locale/nl_NL/LC_MESSAGE/messages.po +10200 -25
  83. psychopy/app/locale/nn_NO/LC_MESSAGE/messages.mo +0 -0
  84. psychopy/app/locale/nn_NO/LC_MESSAGE/messages.po +10200 -25
  85. psychopy/app/locale/pl_PL/LC_MESSAGE/messages.mo +0 -0
  86. psychopy/app/locale/pl_PL/LC_MESSAGE/messages.po +10200 -25
  87. psychopy/app/locale/pt_PT/LC_MESSAGE/messages.mo +0 -0
  88. psychopy/app/locale/pt_PT/LC_MESSAGE/messages.po +11288 -9434
  89. psychopy/app/locale/ro_RO/LC_MESSAGE/messages.mo +0 -0
  90. psychopy/app/locale/ro_RO/LC_MESSAGE/messages.po +10200 -25
  91. psychopy/app/locale/ru_RU/LC_MESSAGE/messages.mo +0 -0
  92. psychopy/app/locale/ru_RU/LC_MESSAGE/messages.po +10199 -24
  93. psychopy/app/locale/sv_SE/LC_MESSAGE/messages.mo +0 -0
  94. psychopy/app/locale/sv_SE/LC_MESSAGE/messages.po +11441 -8747
  95. psychopy/app/locale/tr_TR/LC_MESSAGE/messages.mo +0 -0
  96. psychopy/app/locale/tr_TR/LC_MESSAGE/messages.po +11069 -9545
  97. psychopy/app/locale/zh_CN/LC_MESSAGE/messages.mo +0 -0
  98. psychopy/app/locale/zh_CN/LC_MESSAGE/messages.po +12085 -8268
  99. psychopy/app/locale/zh_TW/LC_MESSAGE/messages.mo +0 -0
  100. psychopy/app/locale/zh_TW/LC_MESSAGE/messages.po +11929 -8022
  101. psychopy/app/plugin_manager/dialog.py +12 -3
  102. psychopy/app/plugin_manager/packageIndex.py +303 -0
  103. psychopy/app/plugin_manager/packages.py +203 -63
  104. psychopy/app/plugin_manager/plugins.py +120 -240
  105. psychopy/app/preferencesDlg.py +6 -1
  106. psychopy/app/psychopyApp.py +16 -4
  107. psychopy/app/runner/runner.py +10 -2
  108. psychopy/app/runner/scriptProcess.py +8 -3
  109. psychopy/app/stdout/stdOutRich.py +11 -4
  110. psychopy/app/themes/icons.py +3 -0
  111. psychopy/app/utils.py +61 -0
  112. psychopy/data/experiment.py +133 -23
  113. psychopy/data/routine.py +12 -0
  114. psychopy/data/staircase.py +42 -20
  115. psychopy/data/trial.py +20 -12
  116. psychopy/data/utils.py +42 -2
  117. psychopy/demos/builder/Experiments/dragAndDrop/drag_and_drop.psyexp +22 -5
  118. psychopy/demos/builder/Experiments/dragAndDrop/stimuli/solutions.xlsx +0 -0
  119. psychopy/demos/builder/Experiments/stroopVoice/stroopVoice.psyexp +2 -12
  120. psychopy/demos/builder/Feature Demos/buttonBox/buttonBoxDemo.psyexp +3 -8
  121. psychopy/demos/builder/Feature Demos/movies/movie.psyexp +220 -0
  122. psychopy/demos/builder/Feature Demos/movies/readme.md +3 -0
  123. psychopy/demos/builder/Feature Demos/visualValidator/visualValidator.psyexp +1 -2
  124. psychopy/demos/builder/Hardware/camera/camera.psyexp +3 -16
  125. psychopy/demos/builder/Hardware/microphone/microphone.psyexp +3 -16
  126. psychopy/demos/coder/hardware/hdf5_extract.py +133 -0
  127. psychopy/event.py +20 -15
  128. psychopy/experiment/_experiment.py +86 -10
  129. psychopy/experiment/components/__init__.py +3 -10
  130. psychopy/experiment/components/_base.py +9 -20
  131. psychopy/experiment/components/button/__init__.py +1 -1
  132. psychopy/experiment/components/buttonBox/__init__.py +50 -54
  133. psychopy/experiment/components/camera/__init__.py +137 -359
  134. psychopy/experiment/components/keyboard/__init__.py +17 -24
  135. psychopy/experiment/components/microphone/__init__.py +61 -110
  136. psychopy/experiment/components/movie/__init__.py +2 -3
  137. psychopy/experiment/components/serialOut/__init__.py +192 -93
  138. psychopy/experiment/components/settings/__init__.py +45 -27
  139. psychopy/experiment/components/sound/__init__.py +82 -73
  140. psychopy/experiment/components/soundsensor/__init__.py +43 -80
  141. psychopy/experiment/devices.py +303 -0
  142. psychopy/experiment/exports.py +20 -18
  143. psychopy/experiment/flow.py +7 -0
  144. psychopy/experiment/loops.py +47 -29
  145. psychopy/experiment/monitor.py +74 -0
  146. psychopy/experiment/params.py +48 -10
  147. psychopy/experiment/plugins.py +28 -108
  148. psychopy/experiment/py2js_transpiler.py +1 -1
  149. psychopy/experiment/routines/__init__.py +1 -1
  150. psychopy/experiment/routines/_base.py +59 -24
  151. psychopy/experiment/routines/audioValidator/__init__.py +19 -155
  152. psychopy/experiment/routines/visualValidator/__init__.py +25 -25
  153. psychopy/hardware/__init__.py +20 -57
  154. psychopy/hardware/button.py +15 -2
  155. psychopy/hardware/camera/__init__.py +2237 -1394
  156. psychopy/hardware/joystick/__init__.py +1 -1
  157. psychopy/hardware/keyboard.py +5 -8
  158. psychopy/hardware/listener.py +4 -1
  159. psychopy/hardware/manager.py +75 -35
  160. psychopy/hardware/microphone.py +52 -6
  161. psychopy/hardware/monitor.py +144 -0
  162. psychopy/hardware/photometer/__init__.py +156 -117
  163. psychopy/hardware/serialdevice.py +16 -2
  164. psychopy/hardware/soundsensor.py +4 -1
  165. psychopy/iohub/devices/deviceConfigValidation.py +2 -1
  166. psychopy/iohub/devices/keyboard/darwin.py +8 -5
  167. psychopy/iohub/util/__init__.py +7 -8
  168. psychopy/localization/generateTranslationTemplate.py +208 -116
  169. psychopy/localization/messages.pot +4305 -3502
  170. psychopy/monitors/MonitorCenter.py +174 -74
  171. psychopy/plugins/__init__.py +6 -4
  172. psychopy/preferences/devices.py +80 -0
  173. psychopy/preferences/generateHints.py +2 -1
  174. psychopy/preferences/preferences.py +35 -11
  175. psychopy/scripts/psychopy-pkgutil.py +969 -0
  176. psychopy/scripts/psyexpCompile.py +1 -1
  177. psychopy/session.py +34 -38
  178. psychopy/sound/__init__.py +6 -260
  179. psychopy/sound/audioclip.py +164 -0
  180. psychopy/sound/backend_ptb.py +8 -0
  181. psychopy/sound/backend_pygame.py +10 -0
  182. psychopy/sound/backend_pysound.py +9 -0
  183. psychopy/sound/backends/__init__.py +0 -0
  184. psychopy/sound/microphone.py +3 -0
  185. psychopy/sound/sound.py +58 -0
  186. psychopy/tests/data/correctScript/python/correctNoiseStimComponent.py +1 -1
  187. psychopy/tests/data/duplicateHeaders.csv +2 -0
  188. psychopy/tests/test_app/test_builder/test_BuilderFrame.py +22 -7
  189. psychopy/tests/test_app/test_builder/test_CompileFromBuilder.py +0 -2
  190. psychopy/tests/test_data/test_utils.py +5 -1
  191. psychopy/tests/test_experiment/test_components/test_ButtonBoxComponent.py +22 -2
  192. psychopy/tests/test_hardware/test_ports.py +0 -12
  193. psychopy/tests/test_tools/test_stringtools.py +1 -1
  194. psychopy/tools/attributetools.py +12 -5
  195. psychopy/tools/fontmanager.py +17 -14
  196. psychopy/tools/movietools.py +43 -2
  197. psychopy/tools/stringtools.py +33 -8
  198. psychopy/tools/versionchooser.py +1 -1
  199. psychopy/validation/audio.py +5 -1
  200. psychopy/validation/visual.py +5 -1
  201. psychopy/visual/basevisual.py +8 -7
  202. psychopy/visual/circle.py +2 -2
  203. psychopy/visual/image.py +29 -109
  204. psychopy/visual/movies/__init__.py +1800 -313
  205. psychopy/visual/polygon.py +4 -0
  206. psychopy/visual/shape.py +2 -2
  207. psychopy/visual/window.py +34 -11
  208. psychopy/voicekey/__init__.py +41 -669
  209. psychopy/voicekey/labjack_vks.py +7 -48
  210. psychopy/voicekey/parallel_vks.py +7 -42
  211. psychopy/voicekey/vk_tools.py +114 -263
  212. {psychopy-2025.1.1.dist-info → psychopy-2025.2.1.dist-info}/METADATA +17 -11
  213. {psychopy-2025.1.1.dist-info → psychopy-2025.2.1.dist-info}/RECORD +216 -184
  214. {psychopy-2025.1.1.dist-info → psychopy-2025.2.1.dist-info}/WHEEL +1 -1
  215. psychopy/visual/movies/players/__init__.py +0 -62
  216. psychopy/visual/movies/players/ffpyplayer_player.py +0 -1401
  217. psychopy/voicekey/demo_vks.py +0 -12
  218. psychopy/voicekey/signal.py +0 -42
  219. {psychopy-2025.1.1.dist-info → psychopy-2025.2.1.dist-info}/entry_points.txt +0 -0
  220. {psychopy-2025.1.1.dist-info → psychopy-2025.2.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,133 @@
1
+ """
2
+ This script reads an HDF5 eye-tracking data file, identifies trials based on specified
3
+ start and end markers in the experiment messages, extracts gaze data for each trial,
4
+ and animates gaze movement over time in a 2D space.
5
+
6
+ The user can customize:
7
+ - The path to the HDF5 file
8
+ - The marker text used to indicate the start and end of trials
9
+
10
+ ⚠️ If you get an error saying a package (like h5py, numpy, matplotlib) is missing:
11
+ In PsychoPy, go to:
12
+ Tools > Plugins/Packages Manager > Packages
13
+ Then search for and install the missing package (e.g., "h5py", "matplotlib", "numpy").
14
+ """
15
+
16
+ """
17
+ This script reads an HDF5 eye-tracking data file, identifies trials based on specified
18
+ start and end markers in the experiment messages, extracts gaze data for each trial,
19
+ and animates gaze movement over time in a 2D space.
20
+
21
+ The user can customize:
22
+ - The path to the HDF5 file
23
+ - The marker text used to indicate the start and end of trials
24
+ """
25
+
26
+ # ==== USER INPUTS ====
27
+ HDF5_FILE = '823238_eyetracking_youtube_2025-05-01_10h34.25.036.hdf5' # Path to HDF5 file
28
+ TRIAL_START_MARKER = 'BEGIN_SEQUENCE 3'#'trial_start' # Marker text for start of trial
29
+ TRIAL_END_MARKER = 'DONE_SEQUENCE 3'#'trial_end' # Marker text for end of trial
30
+ # ======================
31
+
32
+ import h5py
33
+ import numpy as np
34
+ import matplotlib.pyplot as plt
35
+ import matplotlib.animation as animation
36
+
37
+ def extract_eye_data_in_trials(hdf5_path, start_marker, end_marker):
38
+ """
39
+ Extract gaze data for each trial based on custom trial start/end markers.
40
+
41
+ Parameters:
42
+ hdf5_path (str): Path to HDF5 file.
43
+ start_marker (str): String identifying trial start in the message events.
44
+ end_marker (str): String identifying trial end in the message events.
45
+
46
+ Returns:
47
+ List of dicts: Each dict contains time, gaze_x, and gaze_y arrays for one trial.
48
+ """
49
+ with h5py.File(hdf5_path, 'r') as f:
50
+ # Dataset paths
51
+ msg_path = 'data_collection/events/experiment/MessageEvent'
52
+ eye_path = 'data_collection/events/eyetracker/MonocularEyeSampleEvent'
53
+
54
+ # Load message times and text
55
+ messages = f[msg_path]
56
+ msg_times = messages['time'][:]
57
+ msg_texts = messages['text'][:].astype(str)
58
+
59
+ # Find trial start/end times using the provided marker strings
60
+ trial_starts = msg_times[np.char.find(msg_texts, start_marker) != -1]
61
+ trial_ends = msg_times[np.char.find(msg_texts, end_marker) != -1]
62
+
63
+ if len(trial_starts) != len(trial_ends):
64
+ raise ValueError("Mismatched number of trial start and end events")
65
+
66
+ # Load eye tracking data
67
+ eye_data = f[eye_path]
68
+ eye_times = eye_data['time'][:]
69
+ gaze_x = eye_data['gaze_x'][:]
70
+ gaze_y = eye_data['gaze_y'][:]
71
+
72
+ # Extract data between trial start and end times
73
+ trials_gaze = []
74
+ for start, end in zip(trial_starts, trial_ends):
75
+ mask = (eye_times >= start) & (eye_times <= end)
76
+ trial_gaze = {
77
+ 'start_time': start,
78
+ 'end_time': end,
79
+ 'time': eye_times[mask],
80
+ 'gaze_x': gaze_x[mask],
81
+ 'gaze_y': gaze_y[mask]
82
+ }
83
+ trials_gaze.append(trial_gaze)
84
+
85
+ return trials_gaze
86
+
87
+ def animate_gaze(trial_data, trial_num, save_to_file=False):
88
+ """
89
+ Animate gaze positions over time for a single trial.
90
+
91
+ Parameters:
92
+ trial_data (dict): Dictionary with 'time', 'gaze_x', and 'gaze_y' keys.
93
+ trial_num (int): Index of the trial (for labeling).
94
+ save_to_file (bool): Whether to save the animation as an .mp4 video file.
95
+ """
96
+ x = trial_data['gaze_x']
97
+ y = trial_data['gaze_y']
98
+ t = trial_data['time']
99
+
100
+ fig, ax = plt.subplots(figsize=(8, 6))
101
+ ax.set_xlim(-1, 1)
102
+ ax.set_ylim(-0.5, 0.5)
103
+ ax.set_title(f"Gaze Animation - Trial {trial_num}")
104
+ ax.set_xlabel("Gaze X")
105
+ ax.set_ylabel("Gaze Y")
106
+
107
+ point, = ax.plot([], [], 'ro', markersize=5) # Red dot to show gaze
108
+
109
+ def init():
110
+ point.set_data([], [])
111
+ return point,
112
+
113
+ def update(frame):
114
+ point.set_data(x[frame], y[frame])
115
+ return point,
116
+
117
+ ani = animation.FuncAnimation(
118
+ fig, update, frames=len(x),
119
+ init_func=init, blit=True, interval=10
120
+ )
121
+
122
+ if save_to_file:
123
+ ani.save(f'gaze_trial_{trial_num}.mp4', fps=60, extra_args=['-vcodec', 'libx264'])
124
+
125
+ plt.show()
126
+
127
+ # ====== MAIN EXECUTION ======
128
+ if __name__ == '__main__':
129
+ result = extract_eye_data_in_trials(HDF5_FILE, TRIAL_START_MARKER, TRIAL_END_MARKER)
130
+ if not result:
131
+ print("No valid trials found.")
132
+ else:
133
+ animate_gaze(result[0], trial_num=1, save_to_file=False)
psychopy/event.py CHANGED
@@ -297,7 +297,7 @@ def _onPygletMouseRelease(x, y, button, modifiers, emulated=False):
297
297
 
298
298
  def _onPygletMouseWheel(x, y, scroll_x, scroll_y):
299
299
  global mouseWheelRel
300
- mouseWheelRel = mouseWheelRel + numpy.array([scroll_x, scroll_y])
300
+ mouseWheelRel += numpy.array([scroll_x, scroll_y])
301
301
  msg = "Mouse: wheel shift=(%i,%i), pos=(%i,%i)"
302
302
  logging.data(msg % (scroll_x, scroll_y, x, y))
303
303
 
@@ -874,23 +874,28 @@ class Mouse:
874
874
 
875
875
  """
876
876
  global mouseButtons, mouseTimes
877
- if usePygame:
878
- return mouse.get_pressed()
879
- else:
877
+
878
+ if self.win is None: # no backend specified
879
+ return None
880
+
881
+ if havePyglet and self.win.winType == 'pyglet':
880
882
  # for each (pyglet) window, dispatch its events before checking
881
883
  # event buffer
882
- if havePyglet:
883
- for win in pyglet.app.windows:
884
- win.dispatch_events() # pump events on pyglet windows
885
-
886
- if haveGLFW:
887
- glfw.poll_events()
884
+ for win in pyglet.app.windows:
885
+ win.dispatch_events() # pump events on pyglet windows
886
+ elif haveGLFW and self.win.winType == 'glfw':
887
+ glfw.poll_events()
888
+ elif havePygame and self.win.winType == 'pygame':
889
+ return mouse.get_pressed()
890
+ else:
891
+ raise RuntimeError(
892
+ "Mouse.getPressed() is only supported for the pyglet, "
893
+ "pygame and glfw backends.")
888
894
 
889
- # else:
890
- if not getTime:
891
- return copy.copy(mouseButtons)
892
- else:
893
- return copy.copy(mouseButtons), copy.copy(mouseTimes)
895
+ if not getTime:
896
+ return copy.copy(mouseButtons)
897
+ else:
898
+ return copy.copy(mouseButtons), copy.copy(mouseTimes)
894
899
 
895
900
  def isPressedIn(self, shape, buttons=(0, 1, 2)):
896
901
  """Returns `True` if the mouse is currently inside the shape and
@@ -117,6 +117,7 @@ class Experiment:
117
117
  Routine. The Flow controls how Routines are organised
118
118
  e.g. the nature of repeats and branching of an experiment.
119
119
  """
120
+
120
121
 
121
122
  def __init__(self, prefs=None):
122
123
  super(Experiment, self).__init__()
@@ -165,6 +166,11 @@ class Experiment:
165
166
  self._expHandler = TrialHandler(exp=self, name='thisExp')
166
167
  self._expHandler.type = 'ExperimentHandler' # true at run-time
167
168
 
169
+ # get a local reference of all Components and Routines (refreshed on loading a new file)
170
+ self.allCompons = getAllComponents(
171
+ self.prefsBuilder['componentsFolders'], fetchIcons=False)
172
+ self.allRoutines = getAllStandaloneRoutines(fetchIcons=False)
173
+
168
174
  def __eq__(self, other):
169
175
  if isinstance(other, Experiment):
170
176
  # if another experiment, compare filenames
@@ -577,6 +583,14 @@ class Experiment:
577
583
  name = paramNode.get('name')
578
584
  valType = paramNode.get('valType')
579
585
  val = paramNode.get('val')
586
+ #
587
+ # get knowwn legacy params for the current Component
588
+ componentLegacyParams = []
589
+ if componentNode is not None:
590
+ if componentNode.tag in self.allCompons:
591
+ componentLegacyParams = self.allCompons[componentNode.tag].legacyParams
592
+ if componentNode.tag in self.allRoutines:
593
+ componentLegacyParams = self.allRoutines[componentNode.tag].legacyParams
580
594
  # many components need web char newline replacement
581
595
  if not name == 'advancedParams':
582
596
  val = val.replace("&#10;", "\n")
@@ -727,6 +741,10 @@ class Experiment:
727
741
  else:
728
742
  if name in params:
729
743
  params[name].val = val
744
+ elif name in legacyParams + componentLegacyParams:
745
+ # don't warn people if we know it's OK (e.g. for params
746
+ # that have been removed
747
+ return recognised
730
748
  else:
731
749
  # we found an unknown parameter (probably from the future)
732
750
  params[name] = Param(
@@ -740,11 +758,7 @@ class Experiment:
740
758
  params[name].allowedTypes = paramNode.get('allowedTypes')
741
759
  if params[name].allowedTypes is None:
742
760
  params[name].allowedTypes = []
743
- if name in legacyParams + ['JS libs', 'OSF Project ID']:
744
- # don't warn people if we know it's OK (e.g. for params
745
- # that have been removed
746
- pass
747
- elif componentNode is not None and componentNode.get("plugin", False) not in (False, "", "None", None):
761
+ if componentNode is not None and componentNode.get("plugin", False) not in (False, "", "None", None):
748
762
  # is param unrecognised because it's from a plugin?
749
763
  params[name].categ = "Plugin"
750
764
  params[name].plugin = componentNode.get("plugin", False)
@@ -757,17 +771,24 @@ class Experiment:
757
771
 
758
772
  # get the value type and update rate
759
773
  if 'valType' in list(paramNode.keys()):
760
- params[name].valType = paramNode.get('valType')
774
+ valType = paramNode.get('valType')
775
+ setValType = True
761
776
  # compatibility checks:
762
777
  if name in ['allowedKeys'] and paramNode.get('valType') == 'str':
763
778
  # these components were changed in v1.70.00
764
- params[name].valType = 'code'
779
+ valType = 'code'
765
780
  elif name == 'Selected rows':
766
781
  # changed in 1.81.00 from 'code' to 'str': allow string or var
767
- params[name].valType = 'str'
782
+ valType = 'str'
768
783
  # conversions based on valType
769
784
  if params[name].valType == 'bool':
770
785
  params[name].val = eval("%s" % params[name].val)
786
+ # "device" valType was introduced in 2025.2.0 and should always override saved valType
787
+ if params[name].valType == "device":
788
+ setValType = False
789
+ # do actual setting
790
+ if setValType:
791
+ params[name].valType = valType
771
792
  if 'updates' in list(paramNode.keys()):
772
793
  params[name].updates = paramNode.get('updates')
773
794
 
@@ -873,9 +894,9 @@ class Experiment:
873
894
  self.setExpName(shortName)
874
895
  # fetch routines
875
896
  routinesNode = root.find('Routines')
876
- allCompons = getAllComponents(
897
+ self.allCompons = allCompons = getAllComponents(
877
898
  self.prefsBuilder['componentsFolders'], fetchIcons=False)
878
- allRoutines = getAllStandaloneRoutines(fetchIcons=False)
899
+ self.allRoutines = allRoutines = getAllStandaloneRoutines(fetchIcons=False)
879
900
  # get each routine node from the list of routines
880
901
  for routineNode in routinesNode:
881
902
  if routineNode.tag == "Routine":
@@ -959,6 +980,9 @@ class Experiment:
959
980
  if paramNode.tag == "Param":
960
981
  for key, val in paramNode.items():
961
982
  name = paramNode.get("name")
983
+ # "device" valType was introduced in 2025.2.0 and should always override saved valType
984
+ if key == "valType" and routine.params[name].valType == "device":
985
+ continue
962
986
  if name in routine.params:
963
987
  setattr(routine.params[name], key, val)
964
988
  # Add routine to experiment
@@ -1134,6 +1158,58 @@ class Experiment:
1134
1158
  def htmlFolder(self):
1135
1159
  return self.settings.params['HTML path'].val
1136
1160
 
1161
+ def getRequiredDeviceNames(self):
1162
+ """
1163
+ Get the device names which need to be defined for this experiment to run, along with a list
1164
+ of possible types for each one.
1165
+
1166
+ Returns
1167
+ -------
1168
+ dict[str: list[str]]
1169
+ Device names and a list of possible types for each one
1170
+ """
1171
+ # dict in which to store usages
1172
+ usages = {}
1173
+
1174
+ def _process(emt):
1175
+ """
1176
+ Process an element (Component or Routine) for device names and append them to the
1177
+ usages dict.
1178
+
1179
+ Parameters
1180
+ ----------
1181
+ emt : Component or Routine
1182
+ Element to process
1183
+ """
1184
+ # iterate through param's inita values
1185
+ for param in getInitVals(emt.params).values():
1186
+ # if it's a device...
1187
+ if param.valType == "device":
1188
+ # get value
1189
+ deviceName = param.val
1190
+ # make sure device name is in usages dict
1191
+ if deviceName not in usages:
1192
+ usages[deviceName] = []
1193
+ # add any new usages
1194
+ for cls in getattr(emt, "deviceClasses", []):
1195
+ if cls not in usages[deviceName]:
1196
+ usages[deviceName].append(cls)
1197
+
1198
+ # iterate through routines
1199
+ for rt in self.routines.values():
1200
+ if isinstance(rt, BaseStandaloneRoutine):
1201
+ # for standalone routines, get device names from params
1202
+ _process(rt)
1203
+ else:
1204
+ # for regular routines, get device names from each component
1205
+ for comp in rt:
1206
+ _process(comp)
1207
+ # process settings
1208
+ _process(self.settings)
1209
+
1210
+ return usages
1211
+
1212
+
1137
1213
  def getComponentFromName(self, name):
1138
1214
  """Searches all the Routines in the Experiment for a matching Comp name
1139
1215
 
@@ -25,7 +25,6 @@ excludeComponents = [
25
25
  'BaseComponent',
26
26
  'BaseVisualComponent',
27
27
  'BaseDeviceComponent',
28
- 'BaseStandaloneRoutine' # templates only
29
28
  ] # this one isn't ready yet
30
29
 
31
30
  # Plugin components are added dynamically at runtime, usually from plugin
@@ -288,14 +287,6 @@ def getInitVals(params, target="PsychoPy"):
288
287
  .format(inits[name]))
289
288
  inits[name].valType = 'code'
290
289
 
291
- if name == "deviceLabel":
292
- if "name" in inits and not params[name]:
293
- # if deviceName exists but is blank, use component name
294
- inits[name].val = inits['name'].val
295
- # make a code version of device name
296
- inits['deviceLabelCode'] = copy.copy(inits[name])
297
- inits['deviceLabelCode'].valType = "code"
298
-
299
290
  if not hasattr(inits[name], 'updates'): # might be settings parameter instead
300
291
  continue
301
292
 
@@ -305,7 +296,7 @@ def getInitVals(params, target="PsychoPy"):
305
296
  inits[name].val = None
306
297
  inits[name].valType = 'extendedStr'
307
298
  else:
308
- inits[name].val = 'None'
299
+ inits[name].val = None
309
300
  inits[name].valType = 'code'
310
301
 
311
302
  # is constant so don't touch the parameter value
@@ -393,6 +384,8 @@ def getInitVals(params, target="PsychoPy"):
393
384
  elif name == 'allowedKeys':
394
385
  inits[name].val = "[]"
395
386
  inits[name].valType = 'code'
387
+ elif name == "deviceLabel":
388
+ inits[name].valType = "device"
396
389
  else:
397
390
  # if not explicitly handled, default to None
398
391
  inits[name].val = "None"
@@ -13,6 +13,7 @@ from xml.etree.ElementTree import Element
13
13
 
14
14
  from psychopy import prefs
15
15
  from psychopy.constants import FOREVER
16
+ from psychopy.experiment.devices import DeviceMixin
16
17
  from ..params import Param
17
18
  from psychopy.experiment.utils import canBeNumeric
18
19
  from psychopy.experiment.utils import CodeGenerationException
@@ -42,6 +43,9 @@ class BaseComponent:
42
43
  validatorClasses = []
43
44
  # hide this Component in Builder view?
44
45
  hidden = False
46
+ # are there any known legacy params for this Component?
47
+ # these will be removed & warnings ignored on experiment load
48
+ legacyParams = []
45
49
 
46
50
  def __init__(self, exp, parentName, name='',
47
51
  startType='time (s)', startVal='',
@@ -67,7 +71,7 @@ class BaseComponent:
67
71
  msg = _translate(
68
72
  "Name of this Component (alphanumeric or _, no spaces)")
69
73
  self.params['name'] = Param(name,
70
- valType='code', inputType="single", categ='Basic',
74
+ valType='code', inputType="name", categ='Basic',
71
75
  hint=msg,
72
76
  label=_translate("Name"))
73
77
 
@@ -1373,12 +1377,10 @@ class BaseComponent:
1373
1377
  return "thisExp"
1374
1378
 
1375
1379
 
1376
- class BaseDeviceComponent(BaseComponent):
1380
+ class BaseDeviceComponent(BaseComponent, DeviceMixin):
1377
1381
  """
1378
1382
  Base class for most components which interface with a hardware device.
1379
1383
  """
1380
- # list of class strings (readable by DeviceManager) which this component's device could be
1381
- deviceClasses = []
1382
1384
 
1383
1385
  def __init__(
1384
1386
  self, exp, parentName,
@@ -1404,22 +1406,9 @@ class BaseDeviceComponent(BaseComponent):
1404
1406
  saveStartStop=saveStartStop, syncScreenRefresh=syncScreenRefresh,
1405
1407
  disabled=disabled
1406
1408
  )
1407
- # require hardware
1408
- self.exp.requirePsychopyLibs(
1409
- ['hardware']
1410
- )
1411
- # --- Device params ---
1412
- self.order += [
1413
- "deviceLabel"
1414
- ]
1415
- # label to refer to device by
1416
- self.params['deviceLabel'] = Param(
1417
- deviceLabel, valType="str", inputType="single", categ="Device",
1418
- label=_translate("Device label"),
1419
- hint=_translate(
1420
- "A label to refer to this Component's associated hardware device by. If using the "
1421
- "same device for multiple components, be sure to use the same label here."
1422
- )
1409
+ # add device stuff
1410
+ self.addDeviceParams(
1411
+ defaultLabel=deviceLabel
1423
1412
  )
1424
1413
 
1425
1414
 
@@ -87,7 +87,7 @@ class ButtonComponent(BaseVisualComponent):
87
87
  label=_translate("Run once per click")
88
88
  )
89
89
  self.params['callback'] = Param(
90
- callback, valType='extendedCode', inputType="multi", allowedTypes=[], categ='Basic',
90
+ callback, valType='extendedCode', inputType="code", allowedTypes=[], categ='Basic',
91
91
  updates='constant',
92
92
  hint=_translate("Code to run when button is clicked"),
93
93
  label=_translate("Callback function"))
@@ -1,10 +1,10 @@
1
1
  from pathlib import Path
2
2
  from psychopy.experiment.components import BaseComponent, BaseDeviceComponent, Param, getInitVals
3
- from psychopy.experiment.plugins import PluginDevicesMixin, DeviceBackend
3
+ from psychopy.experiment.devices import DeviceBackend
4
4
  from psychopy.localization import _translate
5
5
 
6
6
 
7
- class ButtonBoxComponent(BaseDeviceComponent, PluginDevicesMixin):
7
+ class ButtonBoxComponent(BaseDeviceComponent):
8
8
  """
9
9
  Component for getting button presses from a button box device.
10
10
  """
@@ -13,6 +13,11 @@ class ButtonBoxComponent(BaseDeviceComponent, PluginDevicesMixin):
13
13
  iconFile = Path(__file__).parent / 'buttonBox.png'
14
14
  tooltip = _translate('Button Box: Get input from a button box')
15
15
  beta = True
16
+ legacyParams = [
17
+ # old device setup params, no longer needed as this is handled by DeviceManager
18
+ "deviceBackend",
19
+ "kbButtonAliases"
20
+ ]
16
21
 
17
22
  def __init__(
18
23
  self, exp, parentName,
@@ -22,6 +27,7 @@ class ButtonBoxComponent(BaseDeviceComponent, PluginDevicesMixin):
22
27
  stopType='duration (s)', stopVal=1.0,
23
28
  startEstim='', durationEstim='',
24
29
  forceEndRoutine=True,
30
+ discardPrevious=True,
25
31
  # device
26
32
  deviceLabel="",
27
33
  deviceBackend="keyboard",
@@ -69,6 +75,7 @@ class ButtonBoxComponent(BaseDeviceComponent, PluginDevicesMixin):
69
75
  "allowedButtons",
70
76
  "storeCorrect",
71
77
  "correctAns",
78
+ "discardPrevious"
72
79
  ]
73
80
  self.params['registerOn'] = Param(
74
81
  registerOn, valType='code', inputType='choice', categ='Data',
@@ -119,26 +126,15 @@ class ButtonBoxComponent(BaseDeviceComponent, PluginDevicesMixin):
119
126
  "$correctAns to compare to the key press. "
120
127
  ),
121
128
  label=_translate("Correct answer"), direct=False)
122
-
123
- # --- Device params ---
124
- self.order += [
125
- "deviceBackend",
126
- ]
127
-
128
- self.params['deviceBackend'] = Param(
129
- deviceBackend, valType="str", inputType="choice", categ="Device",
130
- allowedVals=self.getBackendKeys,
131
- allowedLabels=self.getBackendLabels,
132
- label=_translate("Device backend"),
129
+ self.params['discardPrevious'] = Param(
130
+ discardPrevious, valType='bool', inputType="bool", categ="Data",
131
+ updates="constant",
133
132
  hint=_translate(
134
- "What kind of button box is it? What package/plugin should be used to talk to it?"
133
+ "Do you want to discard all responses occurring before the onset of this Component?"
135
134
  ),
136
- direct=False
135
+ label=_translate("Discard previous")
137
136
  )
138
137
 
139
- # add params for any backends
140
- self.loadBackends()
141
-
142
138
  def writeInitCode(self, buff):
143
139
  inits = getInitVals(self.params)
144
140
  # code to create object
@@ -180,13 +176,14 @@ class ButtonBoxComponent(BaseDeviceComponent, PluginDevicesMixin):
180
176
  # writes an if statement to determine whether to draw etc
181
177
  indented = self.writeStartTestCode(buff)
182
178
  if indented:
183
- # dispatch and clear messages
184
- code = (
185
- "# clear any messages from before starting\n"
186
- "%(name)s.responses = []\n"
187
- "%(name)s.clearResponses()\n"
188
- )
189
- buff.writeIndentedLines(code % params)
179
+ if self.params['discardPrevious']:
180
+ # dispatch and clear messages
181
+ code = (
182
+ "# clear any messages from before starting\n"
183
+ "%(name)s.responses = []\n"
184
+ "%(name)s.clearResponses()\n"
185
+ )
186
+ buff.writeIndentedLines(code % params)
190
187
  # to get out of the if statement
191
188
  buff.setIndentLevel(-indented, relative=True)
192
189
 
@@ -277,50 +274,49 @@ class ButtonBoxComponent(BaseDeviceComponent, PluginDevicesMixin):
277
274
  "thisExp.addData('%(name)s.corr', %(name)s.corr)\n"
278
275
  )
279
276
  buff.writeIndentedLines(code % params)
280
-
281
277
 
282
- class KeyboardButtonBoxBackend(DeviceBackend):
283
- """
284
- Adds a basic keyboard emulation backend for ButtonBoxComponent, as well as acting as an example
285
- for implementing other ButtonBoxBackends.
286
- """
287
278
 
288
- key = "keyboard"
289
- label = _translate("Keyboard")
290
- component = ButtonBoxComponent
291
- deviceClasses = ['psychopy.hardware.button.KeyboardButtonBox']
279
+ class KeyboardButtonBoxDeviceBackend(DeviceBackend):
280
+ backendLabel = "Keyboard Button Box"
281
+ deviceClass = "psychopy.hardware.button.KeyboardButtonBox"
282
+ icon = "light/buttonBox.png"
283
+
284
+ def __init__(self, profile):
285
+ # init parent class
286
+ DeviceBackend.__init__(self, profile)
292
287
 
293
- def getParams(self: ButtonBoxComponent):
294
288
  # define order
295
- order = [
289
+ self.order += [
296
290
  "kbButtonAliases",
297
291
  ]
298
292
  # define params
299
- params = {}
300
- params['kbButtonAliases'] = Param(
301
- "'q', 'w', 'e'", valType="list", inputType="single", categ="Device",
293
+ self.params['kbButtonAliases'] = Param(
294
+ "'q', 'w', 'e'", valType="list", inputType="single",
302
295
  label=_translate("Buttons"),
303
296
  hint=_translate(
304
297
  "Keys to treat as buttons (in order of what button index you want them to be). "
305
298
  "Must be the same length as the number of buttons."
306
299
  )
307
300
  )
301
+
302
+ def writeDeviceCode(self, buff):
303
+ """
304
+ Code to setup a device with this backend.
308
305
 
309
- return params, order
310
-
311
- def addRequirements(self):
312
- # no requirements needed - so just return
313
- return
314
-
315
- def writeDeviceCode(self: ButtonBoxComponent, buff):
316
- # get inits
317
- inits = getInitVals(self.params)
318
- # make ButtonGroup object
306
+ Parameters
307
+ ----------
308
+ buff : io.StringIO
309
+ Text buffer to write code to.
310
+ """
311
+ # write basic code
312
+ self.writeBaseDeviceCode(buff, close=False)
313
+ # add param and close
319
314
  code = (
320
- "deviceManager.addDevice(\n"
321
- " deviceClass='psychopy.hardware.button.KeyboardButtonBox',\n"
322
- " deviceName=%(deviceLabel)s,\n"
323
315
  " buttons=%(kbButtonAliases)s,\n"
324
316
  ")\n"
325
317
  )
326
- buff.writeOnceIndentedLines(code % inits)
318
+ buff.writeIndentedLines(code % self.params)
319
+
320
+
321
+ # register backend with Component
322
+ ButtonBoxComponent.registerBackend(KeyboardButtonBoxDeviceBackend)