psychopy 2024.1.3__py3-none-any.whl → 2024.2.0__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 (331) hide show
  1. psychopy/.DS_Store +0 -0
  2. psychopy/CHANGELOG.txt +206 -0
  3. psychopy/GIT_SHA +1 -0
  4. psychopy/VERSION +1 -0
  5. psychopy/__init__.py +77 -15
  6. psychopy/app/Resources/classic/plugin16.png +0 -0
  7. psychopy/app/Resources/classic/plugin16@2x.png +0 -0
  8. psychopy/app/Resources/dark/plugin16.png +0 -0
  9. psychopy/app/Resources/dark/plugin16@2x.png +0 -0
  10. psychopy/app/Resources/light/plugin16.png +0 -0
  11. psychopy/app/Resources/light/plugin16@2x.png +0 -0
  12. psychopy/app/__init__.py +76 -2
  13. psychopy/app/_psychopyApp.py +126 -101
  14. psychopy/app/builder/builder.py +14 -10
  15. psychopy/app/builder/dialogs/__init__.py +8 -8
  16. psychopy/app/builder/dialogs/dlgsConditions.py +12 -13
  17. psychopy/app/builder/dialogs/paramCtrls.py +24 -57
  18. psychopy/app/builder/localizedStrings.py +11 -9
  19. psychopy/app/builder/validators.py +2 -2
  20. psychopy/app/coder/codeEditorBase.py +8 -8
  21. psychopy/app/coder/coder.py +4 -4
  22. psychopy/app/connections/sendusage.py +2 -2
  23. psychopy/app/connections/updates.py +9 -9
  24. psychopy/app/dialogs.py +34 -2
  25. psychopy/app/idle.py +31 -0
  26. psychopy/app/jobs.py +21 -3
  27. psychopy/app/linuxconfig/__init__.py +9 -0
  28. psychopy/app/locale/ar_001/LC_MESSAGE/messages.mo +0 -0
  29. psychopy/app/locale/ar_001/LC_MESSAGE/messages.po +4602 -2540
  30. psychopy/app/locale/es_CO/LC_MESSAGE/messages.mo +0 -0
  31. psychopy/app/locale/es_CO/LC_MESSAGE/messages.po +56 -54
  32. psychopy/app/locale/es_ES/LC_MESSAGE/messages.po +53 -43
  33. psychopy/app/locale/es_US/LC_MESSAGE/messages.mo +0 -0
  34. psychopy/app/locale/es_US/LC_MESSAGE/messages.po +56 -54
  35. psychopy/app/locale/ja_JP/LC_MESSAGE/messages.mo +0 -0
  36. psychopy/app/locale/ja_JP/LC_MESSAGE/messages.po +1258 -1176
  37. psychopy/app/locale/pt_PT/LC_MESSAGE/messages.po +9415 -5
  38. psychopy/app/pavlovia_ui/_base.py +33 -3
  39. psychopy/app/pavlovia_ui/search.py +0 -1
  40. psychopy/app/plugin_manager/dialog.py +104 -51
  41. psychopy/app/plugin_manager/packages.py +5 -0
  42. psychopy/app/plugin_manager/plugins.py +152 -67
  43. psychopy/app/preferencesDlg.py +8 -8
  44. psychopy/app/psychopyApp.py +11 -5
  45. psychopy/app/ribbon.py +124 -14
  46. psychopy/app/runner/runner.py +6 -1
  47. psychopy/app/stdout/stdOutRich.py +27 -11
  48. psychopy/app/themes/icons.py +52 -2
  49. psychopy/assets/__init__.py +0 -0
  50. psychopy/assets/click.png +0 -0
  51. psychopy/assets/clicknext.png +0 -0
  52. psychopy/assets/next.png +0 -0
  53. psychopy/assets/psychopy.ico +0 -0
  54. psychopy/assets/psychopy.png +0 -0
  55. psychopy/assets/templates/__init__.py +0 -0
  56. psychopy/assets/touch.png +0 -0
  57. psychopy/assets/touchnext.png +0 -0
  58. psychopy/assets/window.ico +0 -0
  59. psychopy/changes/2023.1.0.md +9 -0
  60. psychopy/changes/2024.1.0.md +16 -0
  61. psychopy/changes/__init__.py +0 -0
  62. psychopy/clock.py +2 -2
  63. psychopy/colors.py +2 -1
  64. psychopy/compatibility.py +53 -1
  65. psychopy/contrib/.DS_Store +0 -0
  66. psychopy/contrib/configobj/__init__.py +10 -8
  67. psychopy/data/__init__.py +3 -2
  68. psychopy/data/base.py +5 -5
  69. psychopy/data/experiment.py +130 -4
  70. psychopy/data/routine.py +56 -0
  71. psychopy/data/staircase.py +2 -2
  72. psychopy/data/trial.py +559 -97
  73. psychopy/data/utils.py +56 -21
  74. psychopy/demos/.DS_Store +0 -0
  75. psychopy/demos/builder/.DS_Store +0 -0
  76. psychopy/demos/builder/Design Templates/.DS_Store +0 -0
  77. psychopy/demos/builder/Experiments/.DS_Store +0 -0
  78. psychopy/demos/builder/Feature Demos/.DS_Store +0 -0
  79. psychopy/demos/builder/Feature Demos/buttonBox/buttonBoxDemo.psyexp +375 -0
  80. psychopy/demos/builder/Feature Demos/buttonBox/readme.md +5 -0
  81. psychopy/demos/builder/Feature Demos/pilotMode/pilotMode.psyexp +433 -0
  82. psychopy/demos/builder/Feature Demos/pilotMode/readme.md +7 -0
  83. psychopy/demos/builder/Feature Demos/progress/progressBar.psyexp +4 -4
  84. psychopy/demos/builder/Hardware/.DS_Store +0 -0
  85. psychopy/demos/builder/Helper Tools/.DS_Store +0 -0
  86. psychopy/demos/coder/.DS_Store +0 -0
  87. psychopy/demos/coder/hardware/testSoundLatency.py +2 -2
  88. psychopy/demos/coder/iohub/.DS_Store +0 -0
  89. psychopy/demos/coder/misc/hdf5_2_csv +33 -0
  90. psychopy/event.py +30 -29
  91. psychopy/experiment/.DS_Store +0 -0
  92. psychopy/experiment/_experiment.py +6 -6
  93. psychopy/experiment/components/.DS_Store +0 -0
  94. psychopy/experiment/components/__init__.py +6 -3
  95. psychopy/experiment/components/_base.py +286 -131
  96. psychopy/experiment/components/aperture/.DS_Store +0 -0
  97. psychopy/experiment/components/brush/.DS_Store +0 -0
  98. psychopy/experiment/components/button/.DS_Store +0 -0
  99. psychopy/experiment/components/button/__init__.py +5 -1
  100. psychopy/experiment/components/buttonBox/.DS_Store +0 -0
  101. psychopy/experiment/components/buttonBox/__init__.py +21 -12
  102. psychopy/experiment/components/camera/.DS_Store +0 -0
  103. psychopy/experiment/components/code/.DS_Store +0 -0
  104. psychopy/experiment/components/dots/.DS_Store +0 -0
  105. psychopy/experiment/components/eyetracker_record/.DS_Store +0 -0
  106. psychopy/experiment/components/eyetracker_record/__init__.py +92 -30
  107. psychopy/experiment/components/form/.DS_Store +0 -0
  108. psychopy/experiment/components/form/__init__.py +6 -2
  109. psychopy/experiment/components/grating/.DS_Store +0 -0
  110. psychopy/experiment/components/grating/__init__.py +14 -3
  111. psychopy/experiment/components/image/.DS_Store +0 -0
  112. psychopy/experiment/components/image/__init__.py +14 -3
  113. psychopy/experiment/components/joyButtons/.DS_Store +0 -0
  114. psychopy/experiment/components/joystick/.DS_Store +0 -0
  115. psychopy/experiment/components/keyboard/.DS_Store +0 -0
  116. psychopy/experiment/components/keyboard/__init__.py +22 -10
  117. psychopy/experiment/components/microphone/.DS_Store +0 -0
  118. psychopy/experiment/components/microphone/__init__.py +59 -39
  119. psychopy/experiment/components/mouse/.DS_Store +0 -0
  120. psychopy/experiment/components/mouse/__init__.py +44 -29
  121. psychopy/experiment/components/movie/.DS_Store +0 -0
  122. psychopy/experiment/components/movie/__init__.py +1 -1
  123. psychopy/experiment/components/panorama/.DS_Store +0 -0
  124. psychopy/experiment/components/parallelOut/.DS_Store +0 -0
  125. psychopy/experiment/components/patch/.DS_Store +0 -0
  126. psychopy/experiment/components/polygon/.DS_Store +0 -0
  127. psychopy/experiment/components/polygon/__init__.py +26 -6
  128. psychopy/experiment/components/progress/.DS_Store +0 -0
  129. psychopy/experiment/components/progress/__init__.py +1 -1
  130. psychopy/experiment/components/ratingScale/.DS_Store +0 -0
  131. psychopy/experiment/components/resourceManager/.DS_Store +0 -0
  132. psychopy/experiment/components/roi/.DS_Store +0 -0
  133. psychopy/experiment/components/roi/__init__.py +5 -0
  134. psychopy/experiment/components/routineSettings/.DS_Store +0 -0
  135. psychopy/experiment/components/routineSettings/__init__.py +57 -10
  136. psychopy/experiment/components/serialOut/.DS_Store +0 -0
  137. psychopy/experiment/components/settings/.DS_Store +0 -0
  138. psychopy/experiment/components/settings/__init__.py +117 -42
  139. psychopy/experiment/components/slider/.DS_Store +0 -0
  140. psychopy/experiment/components/sound/.DS_Store +0 -0
  141. psychopy/experiment/components/sound/__init__.py +54 -19
  142. psychopy/experiment/components/static/.DS_Store +0 -0
  143. psychopy/experiment/components/static/__init__.py +1 -1
  144. psychopy/experiment/components/text/.DS_Store +0 -0
  145. psychopy/experiment/components/text/__init__.py +28 -3
  146. psychopy/experiment/components/textbox/.DS_Store +0 -0
  147. psychopy/experiment/components/textbox/__init__.py +12 -2
  148. psychopy/experiment/components/unknown/.DS_Store +0 -0
  149. psychopy/experiment/components/unknown/__init__.py +1 -2
  150. psychopy/experiment/components/unknownPlugin/.DS_Store +0 -0
  151. psychopy/experiment/components/unknownPlugin/__init__.py +2 -2
  152. psychopy/experiment/components/variable/.DS_Store +0 -0
  153. psychopy/experiment/flow.py +11 -4
  154. psychopy/experiment/loops.py +85 -37
  155. psychopy/experiment/params.py +74 -32
  156. psychopy/experiment/py2js_transpiler.py +8 -1
  157. psychopy/experiment/routines/.DS_Store +0 -0
  158. psychopy/experiment/routines/_base.py +102 -22
  159. psychopy/experiment/routines/counterbalance/.DS_Store +0 -0
  160. psychopy/experiment/routines/counterbalance/__init__.py +5 -1
  161. psychopy/experiment/routines/eyetracker_calibrate/.DS_Store +0 -0
  162. psychopy/experiment/routines/eyetracker_validate/.DS_Store +0 -0
  163. psychopy/experiment/routines/pavlovia_survey/.DS_Store +0 -0
  164. psychopy/experiment/routines/photodiodeValidator/.DS_Store +0 -0
  165. psychopy/experiment/routines/photodiodeValidator/__init__.py +7 -6
  166. psychopy/experiment/routines/unknown/.DS_Store +0 -0
  167. psychopy/gui/wxgui.py +4 -4
  168. psychopy/hardware/.DS_Store +0 -0
  169. psychopy/hardware/__init__.py +1 -1
  170. psychopy/hardware/base.py +12 -0
  171. psychopy/hardware/camera/__init__.py +1 -15
  172. psychopy/hardware/cedrus.py +10 -11
  173. psychopy/hardware/crs/colorcal.py +13 -22
  174. psychopy/hardware/crs/optical.py +10 -20
  175. psychopy/hardware/emulator.py +17 -14
  176. psychopy/hardware/eyetracker.py +42 -118
  177. psychopy/hardware/gammasci.py +4 -15
  178. psychopy/hardware/keyboard.py +102 -11
  179. psychopy/hardware/listener.py +3 -0
  180. psychopy/hardware/microphone.py +148 -18
  181. psychopy/hardware/minolta.py +8 -15
  182. psychopy/hardware/photodiode.py +191 -16
  183. psychopy/hardware/photometer/__init__.py +11 -19
  184. psychopy/hardware/pr.py +8 -15
  185. psychopy/hardware/speaker.py +39 -4
  186. psychopy/info.py +0 -71
  187. psychopy/iohub/.DS_Store +0 -0
  188. psychopy/iohub/__init__.py +1 -1
  189. psychopy/iohub/client/__init__.py +30 -20
  190. psychopy/iohub/client/keyboard.py +24 -24
  191. psychopy/iohub/datastore/__init__.py +2 -2
  192. psychopy/iohub/datastore/util.py +2 -2
  193. psychopy/iohub/default_config.yaml +1 -1
  194. psychopy/iohub/devices/.DS_Store +0 -0
  195. psychopy/iohub/devices/__init__.py +112 -25
  196. psychopy/iohub/devices/deviceConfigValidation.py +2 -1
  197. psychopy/iohub/devices/experiment/default_experiment.yaml +12 -1
  198. psychopy/iohub/devices/experiment/supported_config_settings.yaml +5 -1
  199. psychopy/iohub/devices/eyetracker/.DS_Store +0 -0
  200. psychopy/iohub/devices/eyetracker/__init__.py +46 -0
  201. psychopy/iohub/devices/eyetracker/calibration/procedure.py +2 -2
  202. psychopy/iohub/devices/eyetracker/hw/gazepoint/__init__.py +14 -2
  203. psychopy/iohub/devices/eyetracker/hw/mouse/eyetracker.py +3 -4
  204. psychopy/iohub/server.py +2 -2
  205. psychopy/iohub/start_iohub_process.py +3 -0
  206. psychopy/iohub/util/__init__.py +62 -70
  207. psychopy/layout.py +5 -5
  208. psychopy/logging.py +8 -1
  209. psychopy/microphone.py +10 -37
  210. psychopy/platform_specific/__init__.py +0 -2
  211. psychopy/platform_specific/darwin.py +1 -3
  212. psychopy/platform_specific/linux.py +31 -33
  213. psychopy/platform_specific/win32.py +38 -13
  214. psychopy/plugins/__init__.py +148 -116
  215. psychopy/plugins/util.py +39 -0
  216. psychopy/preferences/Darwin.spec +4 -2
  217. psychopy/preferences/FreeBSD.spec +4 -2
  218. psychopy/preferences/Linux.spec +4 -2
  219. psychopy/preferences/Windows.spec +4 -2
  220. psychopy/preferences/baseNoArch.spec +4 -2
  221. psychopy/preferences/preferences.py +47 -24
  222. psychopy/projects/pavlovia.py +47 -4
  223. psychopy/scripts/psyexpCompile.py +0 -4
  224. psychopy/session.py +153 -21
  225. psychopy/sound/__init__.py +31 -21
  226. psychopy/sound/_base.py +20 -3
  227. psychopy/sound/audioclip.py +320 -33
  228. psychopy/sound/backend_ptb.py +47 -58
  229. psychopy/sound/backend_pygame.py +1 -1
  230. psychopy/sound/backend_pysound.py +6 -15
  231. psychopy/sound/transcribe.py +53 -0
  232. psychopy/tests/.DS_Store +0 -0
  233. psychopy/tests/data/.DS_Store +0 -0
  234. psychopy/tests/data/TestUnknownPluginComponent_load_resave.psyexp +135 -0
  235. psychopy/tests/data/Test_textbox/test_ori_0_bottom right.png +0 -0
  236. psychopy/tests/data/Test_textbox/test_ori_0_center.png +0 -0
  237. psychopy/tests/data/Test_textbox/test_ori_0_top left.png +0 -0
  238. psychopy/tests/data/Test_textbox/test_ori_120_bottom right.png +0 -0
  239. psychopy/tests/data/Test_textbox/test_ori_120_center.png +0 -0
  240. psychopy/tests/data/Test_textbox/test_ori_120_top left.png +0 -0
  241. psychopy/tests/data/Test_textbox/test_ori_180_bottom right.png +0 -0
  242. psychopy/tests/data/Test_textbox/test_ori_180_center.png +0 -0
  243. psychopy/tests/data/Test_textbox/test_ori_180_top left.png +0 -0
  244. psychopy/tests/data/Test_textbox/test_ori_240_bottom right.png +0 -0
  245. psychopy/tests/data/Test_textbox/test_ori_240_center.png +0 -0
  246. psychopy/tests/data/Test_textbox/test_ori_240_top left.png +0 -0
  247. psychopy/tests/data/correctScript/.DS_Store +0 -0
  248. psychopy/tests/data/test_components/testClearKeyboard/testClearKeyboard.psyexp +200 -0
  249. psychopy/tests/data/test_session/.DS_Store +0 -0
  250. psychopy/tests/data/test_session/root/testFutureTrials/testFutureTrials.psyexp +155 -0
  251. psychopy/tests/data/test_session/root/testTrialNav/trialNav.psyexp +158 -0
  252. psychopy/tests/test_app/.DS_Store +0 -0
  253. psychopy/tests/test_app/conftest.py +2 -2
  254. psychopy/tests/test_app/test_speed.py +4 -1
  255. psychopy/tests/test_data/test_TrialHandler2.py +146 -1
  256. psychopy/tests/test_experiment/.DS_Store +0 -0
  257. psychopy/tests/test_experiment/needs_wx/genComponsTemplate.py +3 -3
  258. psychopy/tests/test_experiment/needs_wx/test_components.py +2 -2
  259. psychopy/tests/test_experiment/test_components/test_KeyboardComponent.py +28 -0
  260. psychopy/tests/test_experiment/test_components/test_UnknownPluginComponent.py +27 -0
  261. psychopy/tests/test_experiment/test_components/test_base_components.py +58 -0
  262. psychopy/tests/test_experiment/test_py2js.py +1 -1
  263. psychopy/tests/test_hardware/test_keyboard.py +184 -16
  264. psychopy/tests/test_hardware/test_ports.py +1 -11
  265. psychopy/tests/test_liaison/test_Liaison.py +47 -0
  266. psychopy/tests/test_misc/test_core.py +5 -0
  267. psychopy/tests/test_session/test_Session.py +5 -1
  268. psychopy/tests/test_tools/test_versionchooser.py +39 -8
  269. psychopy/tests/test_visual/test_all_stimuli.py +0 -97
  270. psychopy/tests/test_visual/test_image.py +6 -5
  271. psychopy/tests/test_visual/test_textbox.py +36 -0
  272. psychopy/tests/utils.py +4 -0
  273. psychopy/tools/filetools.py +1 -1
  274. psychopy/tools/pkgtools.py +160 -137
  275. psychopy/tools/versionchooser.py +10 -10
  276. psychopy/tools/wizard.py +3 -3
  277. psychopy/visual/.DS_Store +0 -0
  278. psychopy/visual/backends/pygletbackend.py +24 -13
  279. psychopy/visual/basevisual.py +5 -11
  280. psychopy/visual/button.py +2 -14
  281. psychopy/visual/helpers.py +5 -5
  282. psychopy/visual/line.py +1 -2
  283. psychopy/visual/movie2.py +7 -816
  284. psychopy/visual/movie3.py +7 -589
  285. psychopy/visual/movies/__init__.py +8 -11
  286. psychopy/visual/movies/frame.py +5 -2
  287. psychopy/visual/movies/players/ffpyplayer_player.py +5 -2
  288. psychopy/visual/noise.py +8 -7
  289. psychopy/visual/patch.py +7 -16
  290. psychopy/visual/progress.py +1 -1
  291. psychopy/visual/radial.py +9 -7
  292. psychopy/visual/ratingscale.py +8 -1415
  293. psychopy/visual/secondorder.py +10 -9
  294. psychopy/visual/shape.py +7 -2
  295. psychopy/visual/text.py +1 -1
  296. psychopy/visual/textbox2/textbox2.py +28 -5
  297. psychopy/web.py +5 -2
  298. {psychopy-2024.1.3.dist-info → psychopy-2024.2.0.dist-info}/METADATA +8 -13
  299. {psychopy-2024.1.3.dist-info → psychopy-2024.2.0.dist-info}/RECORD +313 -219
  300. {psychopy-2024.1.3.dist-info → psychopy-2024.2.0.dist-info}/WHEEL +1 -1
  301. psychopy/app/Resources/click.png +0 -0
  302. psychopy/app/Resources/next.png +0 -0
  303. psychopy/experiment/components/patch/__init__.py +0 -121
  304. psychopy/experiment/components/patch/classic/patch.png +0 -0
  305. psychopy/experiment/components/patch/dark/patch.png +0 -0
  306. psychopy/experiment/components/patch/dark/patch@2x.png +0 -0
  307. psychopy/experiment/components/patch/light/patch.png +0 -0
  308. psychopy/experiment/components/patch/light/patch@2x.png +0 -0
  309. psychopy/experiment/components/ratingScale/__init__.py +0 -337
  310. psychopy/experiment/components/ratingScale/classic/ratingscale.png +0 -0
  311. psychopy/experiment/components/ratingScale/classic/ratingscale@2x.png +0 -0
  312. psychopy/experiment/components/ratingScale/dark/ratingScale@2x.png +0 -0
  313. psychopy/experiment/components/ratingScale/dark/ratingscale.png +0 -0
  314. psychopy/experiment/components/ratingScale/light/ratingScale@2x.png +0 -0
  315. psychopy/experiment/components/ratingScale/light/ratingscale.png +0 -0
  316. psychopy/platform_specific/posix.py +0 -16
  317. psychopy/tests/test_sound/test_microphone.py +0 -217
  318. psychopy/tests/test_visual/test_ratingScale.py +0 -299
  319. /psychopy/{app/Resources → assets}/Psychopy Window Favicon@16w.png +0 -0
  320. /psychopy/{app/Resources → assets}/Psychopy Window Favicon@32w.png +0 -0
  321. /psychopy/{app/Resources → assets}/USB-C.png +0 -0
  322. /psychopy/{app/Resources → assets}/USB.png +0 -0
  323. /psychopy/{app/Resources → assets}/creditCard.png +0 -0
  324. /psychopy/{app/Resources → assets}/default.mp3 +0 -0
  325. /psychopy/{app/Resources → assets}/default.mp4 +0 -0
  326. /psychopy/{app/Resources → assets}/default.png +0 -0
  327. /psychopy/{app/Resources → assets/templates}/instruct1.png +0 -0
  328. /psychopy/{app/Resources → assets/templates}/instruct2.png +0 -0
  329. {psychopy-2024.1.3.dist-info → psychopy-2024.2.0.dist-info}/entry_points.txt +0 -0
  330. {psychopy-2024.1.3.dist-info → psychopy-2024.2.0.dist-info}/licenses/AUTHORS.md +0 -0
  331. {psychopy-2024.1.3.dist-info → psychopy-2024.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -17,6 +17,7 @@ The code that writes out a *_lastrun.py experiment file is (in order):
17
17
  """
18
18
 
19
19
  from copy import deepcopy
20
+ from pathlib import Path
20
21
  from xml.etree.ElementTree import Element
21
22
 
22
23
  from psychopy.experiment import getInitVals
@@ -111,7 +112,11 @@ class TrialHandler(_BaseLoopHandler):
111
112
  hint=_translate("Name of a file specifying the parameters for "
112
113
  "each condition (.csv, .xlsx, or .pkl). Browse "
113
114
  "to select a file. Right-click to preview file "
114
- "contents, or create a new file."))
115
+ "contents, or create a new file."),
116
+ ctrlParams={
117
+ 'template': Path(__file__).parent / "loopTemplate.xltx"
118
+ }
119
+ )
115
120
  self.params['endPoints'] = Param(
116
121
  list(endPoints), valType='num', inputType="single", updates=None, allowedUpdates=None,
117
122
  label=_translate('End points'),
@@ -153,34 +158,45 @@ class TrialHandler(_BaseLoopHandler):
153
158
  inits = getInitVals(self.params)
154
159
  # import conditions from file?
155
160
  if self.params['conditionsFile'].val in ['None', None, 'none', '']:
156
- condsStr = "[None]"
161
+ inits['trialList'] = (
162
+ "[None]"
163
+ )
157
164
  elif self.params['Selected rows'].val in ['None', None, 'none', '']:
158
165
  # just a conditions file with no sub-selection
159
- _con = "data.importConditions(%s)"
160
- condsStr = _con % self.params['conditionsFile']
166
+ inits['trialList'] = (
167
+ "data.importConditions(%(conditionsFile)s)"
168
+ ) % inits
161
169
  else:
162
170
  # a subset of a conditions file
163
- condsStr = ("data.importConditions(%(conditionsFile)s, selection="
164
- "%(Selected rows)s)") % self.params
171
+ inits['trialList'] = (
172
+ "data.importConditions(\n"
173
+ " %(conditionsFile)s, \n"
174
+ " selection=%(Selected rows)s\n"
175
+ ")\n"
176
+ ) % inits
165
177
  # also a 'thisName' for use in "for thisTrial in trials:"
166
178
  makeLoopIndex = self.exp.namespace.makeLoopIndex
167
- self.thisName = makeLoopIndex(self.params['name'].val)
179
+ self.thisName = inits['loopIndex'] = makeLoopIndex(self.params['name'].val)
168
180
  # write the code
169
- code = ("\n# set up handler to look after randomisation of conditions etc\n"
170
- "%(name)s = data.TrialHandler(nReps=%(nReps)s, method=%(loopType)s, \n"
171
- " extraInfo=expInfo, originPath=-1,\n")
181
+ code = (
182
+ "\n"
183
+ "# set up handler to look after randomisation of conditions etc\n"
184
+ "%(name)s = data.TrialHandler2(\n"
185
+ " name='%(name)s',\n"
186
+ " nReps=%(nReps)s, \n"
187
+ " method=%(loopType)s, \n"
188
+ " extraInfo=expInfo, \n"
189
+ " originPath=-1, \n"
190
+ " trialList=%(trialList)s, \n"
191
+ " seed=%(random seed)s, \n"
192
+ ")\n"
193
+ )
172
194
  buff.writeIndentedLines(code % inits)
173
- # the next line needs to be kept separate to preserve potential string formatting
174
- # by the user in condStr (i.e. it shouldn't be a formatted string itself
175
- code = " trialList=" + condsStr + ",\n" # conditions go here
176
- buff.writeIndented(code)
177
- code = " seed=%(random seed)s, name='%(name)s')\n"
195
+ code = (
196
+ "thisExp.addLoop(%(name)s) # add the loop to the experiment\n"
197
+ "%(loopIndex)s = %(name)s.trialList[0] # so we can initialise stimuli with some values\n"
198
+ )
178
199
  buff.writeIndentedLines(code % inits)
179
-
180
- code = ("thisExp.addLoop(%(name)s) # add the loop to the experiment\n" +
181
- self.thisName + " = %(name)s.trialList[0] " +
182
- "# so we can initialise stimuli with some values\n")
183
- buff.writeIndentedLines(code % self.params)
184
200
  # unclutter the namespace
185
201
  if not self.exp.prefsBuilder['unclutteredNamespace']:
186
202
  code = ("# abbreviate parameter names if possible (e.g. rgb = %(name)s.rgb)\n"
@@ -189,6 +205,14 @@ class TrialHandler(_BaseLoopHandler):
189
205
  " globals()[paramName] = %(name)s[paramName]\n")
190
206
  buff.writeIndentedLines(code % {'name': self.thisName})
191
207
 
208
+ # send data to Liaison before loop starts
209
+ if self.params['isTrials'].val:
210
+ buff.writeIndentedLines(
211
+ "if thisSession is not None:\n"
212
+ " # if running in a Session with a Liaison client, send data up to now\n"
213
+ " thisSession.sendExperimentData()\n"
214
+ )
215
+
192
216
  # then run the trials loop
193
217
  code = "\nfor %s in %s:\n"
194
218
  buff.writeIndentedLines(code % (self.thisName, self.params['name']))
@@ -200,18 +224,13 @@ class TrialHandler(_BaseLoopHandler):
200
224
  )
201
225
  buff.writeIndentedLines(code % self.params)
202
226
 
203
- # handle pausing
204
- code = (
205
- "# pause experiment here if requested\n"
206
- "if thisExp.status == PAUSED:\n"
207
- " pauseExperiment(\n"
208
- " thisExp=thisExp, \n"
209
- " win=win, \n"
210
- " timers=[routineTimer], \n"
211
- " playbackComponents=[]\n"
212
- ")\n"
213
- )
214
- buff.writeIndentedLines(code)
227
+ # send data to Liaison at start of each iteration
228
+ if self.params['isTrials'].val:
229
+ buff.writeIndentedLines(
230
+ "if thisSession is not None:\n"
231
+ " # if running in a Session with a Liaison client, send data up to now\n"
232
+ " thisSession.sendExperimentData()\n"
233
+ )
215
234
  # unclutter the namespace
216
235
  if not self.exp.prefsBuilder['unclutteredNamespace']:
217
236
  code = ("# abbreviate parameter names if possible (e.g. rgb = %(name)s.rgb)\n"
@@ -331,9 +350,6 @@ class TrialHandler(_BaseLoopHandler):
331
350
  buff.writeIndentedLines(
332
351
  "thisExp.nextEntry()\n"
333
352
  "\n"
334
- "if thisSession is not None:\n"
335
- " # if running in a Session with a Liaison client, send data up to now\n"
336
- " thisSession.sendExperimentData()\n"
337
353
  )
338
354
  # end of the loop. dedent
339
355
  buff.setIndentLevel(-1, relative=True)
@@ -341,7 +357,13 @@ class TrialHandler(_BaseLoopHandler):
341
357
  % (self.params['nReps'], self.params['name']))
342
358
  buff.writeIndented("\n")
343
359
  # save data
344
- if self.params['isTrials'].val == True:
360
+ if self.params['isTrials'].val:
361
+ # send final data to Liaison
362
+ buff.writeIndentedLines(
363
+ "if thisSession is not None:\n"
364
+ " # if running in a Session with a Liaison client, send data up to now\n"
365
+ " thisSession.sendExperimentData()\n"
366
+ )
345
367
  # a string to show all the available variables (if the conditions
346
368
  # isn't just None or [None])
347
369
  saveExcel = self.exp.settings.params['Save excel file'].val
@@ -615,11 +637,37 @@ class MultiStairHandler(_BaseLoopHandler):
615
637
  label=_translate('Conditions'),
616
638
  hint=_translate("A list of dictionaries describing the "
617
639
  "differences between each staircase"))
640
+
641
+ def getTemplate():
642
+ """
643
+ Method to get the template for this loop's chosen stair type. This is specified as a
644
+ method rather than a simple value as the control needs to update its target according
645
+ to the current value of stairType.
646
+
647
+ Returns
648
+ -------
649
+ pathlib.Path
650
+ Path to the appropriate template file
651
+ """
652
+ # root folder
653
+ root = Path(__file__).parent
654
+ # get file path according to stairType param
655
+ if self.params['stairType'] == "QUEST":
656
+ return root / "questTemplate.xltx"
657
+ elif self.params['stairType'] == "questplus":
658
+ return root / "questPlusTemplate.xltx"
659
+ else:
660
+ return root / "staircaseTemplate.xltx"
661
+
618
662
  self.params['conditionsFile'] = Param(
619
663
  conditionsFile, valType='file', inputType='table', updates=None, allowedUpdates=None,
620
664
  label=_translate('Conditions'),
621
665
  hint=_translate("An xlsx or csv file specifying the parameters "
622
- "for each condition"))
666
+ "for each condition"),
667
+ ctrlParams={
668
+ 'template': getTemplate
669
+ }
670
+ )
623
671
  self.params['isTrials'] = Param(
624
672
  isTrials, valType='bool', inputType='bool', updates=None, allowedUpdates=None,
625
673
  label=_translate("Is trials"),
@@ -116,38 +116,73 @@ class Param():
116
116
  def __init__(self, val, valType, inputType=None, allowedVals=None, allowedTypes=None,
117
117
  hint="", label="", updates=None, allowedUpdates=None,
118
118
  allowedLabels=None, direct=True,
119
- canBePath=True,
119
+ canBePath=True, ctrlParams=None,
120
120
  categ="Basic"):
121
121
  """
122
- @param val: the value for this parameter
123
- @type val: any
124
- @param valType: the type of this parameter ('num', 'str', 'code')
125
- @type valType: string
126
- @param allowedVals: possible vals for this param
127
- (e.g. units param can only be 'norm','pix',...)
128
- @type allowedVals: any
129
- @param allowedTypes: if other types are allowed then this is
130
- the possible types this parameter can have
131
- (e.g. rgb can be 'red' or [1,0,1])
132
- @type allowedTypes: list
133
- @param hint: describe this parameter for the user
134
- @type hint: string
135
- @param updates: how often does this parameter update
136
- ('experiment', 'routine', 'set every frame')
137
- @type updates: string
138
- @param allowedUpdates: conceivable updates for this param
139
- [None, 'routine', 'set every frame']
140
- @type allowedUpdates: list
141
- @param categ: category for this parameter
142
- will populate tabs in Component Dlg
143
- @type allowedUpdates: string
144
- @param canBePath: is it possible for this parameter to be
145
- a path? If so, writing as str will check for pathlike
146
- characters and sanitise if needed.
147
- @type canBePath: bool
148
- @param direct: purely used in the test suite, marks whether this
149
- param's value is expected to appear in the script
150
- @type direct: bool
122
+
123
+ Parameters
124
+ ----------
125
+ val : any
126
+ The value for this parameter
127
+ valType : str
128
+ The type of this parameter, one of:
129
+ - str: A string, will be compiled with " around it
130
+ - extendedStr: A long string, will be compiled with " around it and linebreaks will
131
+ be preserved
132
+ - code: Some code, will be compiled verbatim or translated to JS (no ")
133
+ - extendedCode: A block of code, will be compiled verbatim or translated to JS and
134
+ linebreaks will be preserved
135
+ - file: A file path, will be compiled like str but will replace unescaped \ with /
136
+ - list: A list of values, will be compiled like code but if there's no [] or () then
137
+ these are added
138
+ Note that, if value begins with a $, it will always be treated as code regardless of
139
+ valType
140
+ inputType : str
141
+ The type of control to make for this parameter in Builder, one of:
142
+ - single: A single-line text control
143
+ - multi: A multi-line text control
144
+ - color: A single-line text control with a button to open the color picker
145
+ - survey: A single-line text control with a button to open Pavlovia surveys list
146
+ - file: A single-line text control with a button to open a file browser
147
+ - fileList: Several file controls with buttons to add/remove
148
+ - table: A file control with an additional button to open in Excel
149
+ - choice: A single-choice control (dropdown)
150
+ - multiChoice: A multi-choice control (tickboxes)
151
+ - richChoice: A single-choice control (dropdown) with rich text for each option
152
+ - bool: A single checkbox control
153
+ - dict: Several key:value pair controls with buttons to add/remove fields
154
+ allowedVals : list[str]
155
+ Possible vals for this param (e.g. units param can only be 'norm','pix',...),
156
+ these are used in the compiled code
157
+ allowedLabels : list[str] or None
158
+ Labels corresponding to each value in allowedVals, these are displayed in Builder but
159
+ not used in the compiled code. Leave as None to simply copy allowedVals.
160
+ hint : str
161
+ Tooltip to display when param is hovered over
162
+ label : str
163
+ Label to display next to param
164
+ updates : str
165
+ How often does this parameter update, usually one of:
166
+ - constant: Value is set just the once
167
+ - set every repeat: Value is set at the start of each Routine
168
+ - set every frame: Value is set each frame
169
+ allowedUpdates : list[str]
170
+ List of values to show in the choice control for updates.
171
+ direct : bool
172
+ Are we expecting the value of this param to directly appear in the compiled code?
173
+ Mostly used by the test suite to check that params which should be used are used.
174
+ canBePath : bool
175
+ Is it possible for this parameter to be a path? Setting to False will disable
176
+ filepath sanitization (e.g. for textbox you may not want to replace \ with /)
177
+ ctrlParams : dict
178
+ Extra information to pass to the control, such as the Excel template file to use in a
179
+ `table` control.
180
+ categ : str
181
+ Category (tab) under which this param appears in Builder.
182
+
183
+ Deprecated params
184
+ -----------------
185
+ allowedTypes
151
186
  """
152
187
  super(Param, self).__init__()
153
188
  self.label = label
@@ -165,6 +200,7 @@ class Param():
165
200
  self.codeWanted = False
166
201
  self.canBePath = canBePath
167
202
  self.direct = direct
203
+ self.ctrlParams = ctrlParams or {}
168
204
  self.plugin = None
169
205
  if inputType:
170
206
  self.inputType = inputType
@@ -311,8 +347,11 @@ class Param():
311
347
  return False
312
348
  # if not a clear alias, use bool method of value
313
349
  return bool(self.val)
314
-
315
- def __deepcopy__(self, memo):
350
+
351
+ def copy(self):
352
+ """
353
+ Create a copy of this Param object
354
+ """
316
355
  return Param(
317
356
  val=self.val,
318
357
  valType=self.valType,
@@ -329,6 +368,9 @@ class Param():
329
368
  categ=self.categ
330
369
  )
331
370
 
371
+ def __deepcopy__(self, memo):
372
+ return self.copy()
373
+
332
374
  @property
333
375
  def _xml(self):
334
376
  # Make root element
@@ -337,7 +337,14 @@ class pythonTransformer(ast.NodeTransformer):
337
337
  # The default first argument for pop is -1 (remove the last item).
338
338
  elif func.attr == 'pop':
339
339
  func.attr = 'splice'
340
- args = args if args else [ast.Constant(value=-1, kind=None)]
340
+ # if no args, construct an index that's `<name>.length-1`
341
+ if not args:
342
+ args = ast.BinOp(
343
+ ast.Attribute(value=func.value, attr="length"),
344
+ ast.Sub(),
345
+ ast.Constant(1, kind="int")
346
+ )
347
+ # add 1 as a second argument so the last item is deleted
341
348
  args = [args, [ast.Constant(value=1, kind=None)]]
342
349
 
343
350
  return ast.Call(
Binary file
@@ -618,6 +618,23 @@ class Routine(list):
618
618
  # create the frame loop for this routine
619
619
  code = ('\n# --- Prepare to start Routine "%s" ---\n')
620
620
  buff.writeIndentedLines(code % (self.name))
621
+ # get list of components which have an in-experiment object
622
+ comps = [
623
+ c.name for c in self
624
+ if 'startType' in c.params and c.type != 'Variable'
625
+ ]
626
+ compStr = ", ".join(comps)
627
+ # create object
628
+ code = (
629
+ "# create an object to store info about Routine %(name)s\n"
630
+ "%(name)s = data.Routine(\n"
631
+ " name='%(name)s',\n"
632
+ " components=[{}],\n"
633
+ ")\n"
634
+ "%(name)s.status = NOT_STARTED\n"
635
+ ).format(compStr)
636
+ buff.writeIndentedLines(code % self.params)
637
+
621
638
  code = (
622
639
  'continueRoutine = True\n'
623
640
  )
@@ -626,21 +643,29 @@ class Routine(list):
626
643
  # can we use non-slip timing?
627
644
  maxTime, useNonSlip = self.getMaxTime()
628
645
 
646
+ # this is the beginning of the routine, before the loop starts
629
647
  code = "# update component parameters for each repeat\n"
630
648
  buff.writeIndentedLines(code)
631
- # This is the beginning of the routine, before the loop starts
632
649
  for event in self:
650
+ # don't write Routine Settings just yet...
651
+ if event is self.settings:
652
+ continue
653
+ # write the other Components'
633
654
  event.writeRoutineStartCode(buff)
634
655
  event.writeRoutineStartValidationCode(buff)
656
+ # write the Routine Settings code last
657
+ self.settings.writeRoutineStartCode(buff)
658
+ self.settings.writeRoutineStartValidationCode(buff)
635
659
 
636
660
  code = '# keep track of which components have finished\n'
637
661
  buff.writeIndentedLines(code)
638
- # Get list of components, but leave out Variable components, which may not support attributes
639
- compStr = ', '.join([c.params['name'].val for c in self
640
- if 'startType' in c.params and c.type != 'Variable'])
641
- buff.writeIndented('%sComponents = [%s]\n' % (self.name, compStr))
662
+ # legacy code to support old `...Components` variable
663
+ code = (
664
+ "%(name)sComponents = %(name)s.components"
665
+ )
666
+ buff.writeIndentedLines(code % self.params)
642
667
 
643
- code = ("for thisComponent in {name}Components:\n"
668
+ code = ("for thisComponent in {name}.components:\n"
644
669
  " thisComponent.tStart = None\n"
645
670
  " thisComponent.tStop = None\n"
646
671
  " thisComponent.tStartRefresh = None\n"
@@ -655,12 +680,23 @@ class Routine(list):
655
680
  '\n# --- Run Routine "{name}" ---\n')
656
681
  buff.writeIndentedLines(code.format(name=self.name,
657
682
  clockName=self._clockName))
683
+ # check for the trials loop ending this Routine
684
+ if len(self.exp.flow._loopList):
685
+ loop = self.exp.flow._loopList[-1]
686
+ code = (
687
+ "# if trial has changed, end Routine now\n"
688
+ "if isinstance({name}, data.TrialHandler2) and {thisName}.thisN != {"
689
+ "name}.thisTrial.thisN:\n"
690
+ " continueRoutine = False\n"
691
+ ).format(name=loop.name, thisName=loop.thisName)
692
+ buff.writeIndentedLines(code)
693
+
658
694
  # initial value for forceRoutineEnded (needs to happen now as Code components will have executed
659
695
  # their Begin Routine code)
660
696
  code = (
661
- 'routineForceEnded = not continueRoutine\n'
697
+ '%(name)s.forceEnded = routineForceEnded = not continueRoutine\n'
662
698
  )
663
- buff.writeIndentedLines(code)
699
+ buff.writeIndentedLines(code % self.params)
664
700
 
665
701
  if useNonSlip:
666
702
  code = f'while continueRoutine and routineTimer.getTime() < {maxTime}:\n'
@@ -706,21 +742,43 @@ class Routine(list):
706
742
  )
707
743
  buff.writeIndentedLines(code)
708
744
 
745
+ # handle pausing
746
+ playbackComponents = [
747
+ comp.name for comp in self
748
+ if type(comp).__name__ in ("MovieComponent", "SoundComponent")
749
+ ]
750
+ playbackComponentsStr = ", ".join(playbackComponents)
751
+ code = (
752
+ "# pause experiment here if requested\n"
753
+ "if thisExp.status == PAUSED:\n"
754
+ " pauseExperiment(\n"
755
+ " thisExp=thisExp, \n"
756
+ " win=win, \n"
757
+ " timers=[routineTimer], \n"
758
+ " playbackComponents=[{playbackComponentsStr}]\n"
759
+ " )\n"
760
+ " # skip the frame we paused on\n"
761
+ " continue"
762
+ )
763
+ code = code.format(playbackComponentsStr=playbackComponentsStr)
764
+ buff.writeIndentedLines(code)
765
+
709
766
  # are we done yet?
710
767
  code = (
711
- '\n# check if all components have finished\n'
768
+ '\n'
769
+ '# check if all components have finished\n'
712
770
  'if not continueRoutine: # a component has requested a '
713
771
  'forced-end of Routine\n'
714
- ' routineForceEnded = True\n'
772
+ ' %(name)s.forceEnded = routineForceEnded = True\n'
715
773
  ' break\n'
716
774
  'continueRoutine = False # will revert to True if at least '
717
775
  'one component still running\n'
718
- 'for thisComponent in %sComponents:\n'
776
+ 'for thisComponent in %(name)s.components:\n'
719
777
  ' if hasattr(thisComponent, "status") and '
720
778
  'thisComponent.status != FINISHED:\n'
721
779
  ' continueRoutine = True\n'
722
780
  ' break # at least one component has not yet finished\n')
723
- buff.writeIndentedLines(code % self.name)
781
+ buff.writeIndentedLines(code % self.params)
724
782
 
725
783
  # update screen
726
784
  code = ('\n# refresh the screen\n'
@@ -733,23 +791,25 @@ class Routine(list):
733
791
  buff.setIndentLevel(-1, True)
734
792
 
735
793
  # write the code for each component for the end of the routine
736
- code = ('\n# --- Ending Routine "%s" ---\n'
737
- 'for thisComponent in %sComponents:\n'
794
+ code = ('\n# --- Ending Routine "%(name)s" ---\n'
795
+ 'for thisComponent in %(name)s.components:\n'
738
796
  ' if hasattr(thisComponent, "setAutoDraw"):\n'
739
797
  ' thisComponent.setAutoDraw(False)\n')
740
- buff.writeIndentedLines(code % (self.name, self.name))
798
+ buff.writeIndentedLines(code % self.params)
741
799
  for event in self:
742
800
  event.writeRoutineEndCode(buff)
743
801
 
744
802
  if useNonSlip:
745
803
  code = (
746
804
  "# using non-slip timing so subtract the expected duration of this Routine (unless ended on request)\n"
747
- "if routineForceEnded:\n"
805
+ "if %(name)s.maxDurationReached:\n"
806
+ " routineTimer.addTime(-%(name)s.maxDuration)\n"
807
+ "elif %(name)s.forceEnded:\n"
748
808
  " routineTimer.reset()\n"
749
809
  "else:\n"
750
- " routineTimer.addTime(-%f)\n"
751
- )
752
- buff.writeIndentedLines(code % (maxTime))
810
+ " routineTimer.addTime(-{:f})\n"
811
+ ).format(maxTime)
812
+ buff.writeIndentedLines(code % self.params)
753
813
 
754
814
  def writeRoutineBeginCodeJS(self, buff, modular):
755
815
 
@@ -773,13 +833,21 @@ class Routine(list):
773
833
  maxTime, useNonSlip = self.getMaxTime()
774
834
  if useNonSlip:
775
835
  buff.writeIndented('routineTimer.add(%f);\n' % (maxTime))
836
+ # keep track of whether max duration is reached
837
+ code = (
838
+ "%(name)sMaxDurationReached = false;\n"
839
+ )
840
+ buff.writeIndentedLines(code % self.params)
776
841
 
777
842
  code = "// update component parameters for each repeat\n"
778
843
  buff.writeIndentedLines(code)
779
844
  # This is the beginning of the routine, before the loop starts
780
845
  for thisCompon in self:
846
+ if thisCompon is self.settings:
847
+ continue
781
848
  if "PsychoJS" in thisCompon.targets:
782
849
  thisCompon.writeRoutineStartCodeJS(buff)
850
+ self.settings.writeRoutineStartCodeJS(buff)
783
851
 
784
852
  code = ("// keep track of which components have finished\n"
785
853
  "%(name)sComponents = [];\n" % self.params)
@@ -937,7 +1005,16 @@ class Routine(list):
937
1005
  compon.writeRoutineEndCodeJS(buff)
938
1006
 
939
1007
  # reset routineTimer at the *very end* of all non-nonSlip routines
940
- if not useNonSlip:
1008
+ if useNonSlip:
1009
+ code = (
1010
+ "if (%(name)sMaxDurationReached) {{\n"
1011
+ " routineTimer.add(%(name)sMaxDuration);\n"
1012
+ "}} else {{\n"
1013
+ " routineTimer.add(-{:f});\n"
1014
+ "}}\n"
1015
+ ).format(maxTime)
1016
+ buff.writeIndented(code % self.params)
1017
+ else:
941
1018
  code = ('// the Routine "%s" was not non-slip safe, so reset '
942
1019
  'the non-slip timer\n'
943
1020
  'routineTimer.reset();\n\n')
@@ -1002,7 +1079,7 @@ class Routine(list):
1002
1079
  if duration == FOREVER:
1003
1080
  # only the *start* of an unlimited event should contribute
1004
1081
  # to maxTime
1005
- duration = 1 # plus some minimal duration so it's visible
1082
+ duration = 0 # plus some minimal duration so it's visible
1006
1083
  # now see if we have a end t value that beats the previous max
1007
1084
  try:
1008
1085
  # will fail if either value is not defined:
@@ -1014,8 +1091,11 @@ class Routine(list):
1014
1091
  rtDur, numericStop = self.settings.getDuration()
1015
1092
  if rtDur != FOREVER:
1016
1093
  maxTime = rtDur
1094
+ # if nonslip is actively requested, force it
1095
+ if self.settings.params['forceNonSlip'] and maxTime not in (0, FOREVER):
1096
+ nonSlipSafe = True
1017
1097
  # if there are no components, default to 10s
1018
- if maxTime == 0:
1098
+ if maxTime in (0, None):
1019
1099
  maxTime = 10
1020
1100
  nonSlipSafe = False
1021
1101
  return maxTime, nonSlipSafe
@@ -91,7 +91,11 @@ class CounterbalanceRoutine(BaseStandaloneRoutine):
91
91
  hint=_translate(
92
92
  "Name of a file specifying the parameters for each group (.csv, .xlsx, or .pkl). Browse to select "
93
93
  "a file. Right-click to preview file contents, or create a new file."
94
- ))
94
+ ),
95
+ ctrlParams={
96
+ 'template': Path(__file__).parent / "counterbalanceItems.xltx"
97
+ }
98
+ )
95
99
 
96
100
  self.params['conditionsVariable'] = Param(
97
101
  conditionsVariable, valType='code', inputType="single", categ="Basic",
@@ -17,9 +17,9 @@ class PhotodiodeValidatorRoutine(BaseValidatorRoutine, PluginDevicesMixin):
17
17
 
18
18
  categories = ['Validation']
19
19
  iconFile = Path(__file__).parent / 'photodiode_validator.png'
20
- tooltip = _translate('')
20
+ tooltip = _translate('Photodiode validator')
21
21
  deviceClasses = []
22
- version = "2024.2.0"
22
+ version = "2025.1.0"
23
23
 
24
24
  def __init__(
25
25
  self,
@@ -169,8 +169,9 @@ class PhotodiodeValidatorRoutine(BaseValidatorRoutine, PluginDevicesMixin):
169
169
  channel, valType="code", inputType="single", categ="Device",
170
170
  label=_translate("Photodiode channel"),
171
171
  hint=_translate(
172
- "If relevant, a channel number attached to the photodiode, to distinguish it from other photodiodes on "
173
- "the same port."
172
+ "If relevant, a channel number attached to the photodiode, to distinguish it "
173
+ "from other photodiodes on the same port. Leave blank to use the first photodiode "
174
+ "which can detect the Window."
174
175
  )
175
176
  )
176
177
 
@@ -266,14 +267,14 @@ class PhotodiodeValidatorRoutine(BaseValidatorRoutine, PluginDevicesMixin):
266
267
  " report=%(report)s,\n"
267
268
  ")\n"
268
269
  )
269
- buff.writeIndentedLines(code % self.params)
270
+ buff.writeIndentedLines(code % inits)
270
271
  # connect stimuli
271
272
  for stim in self.findConnectedStimuli():
272
273
  code = (
273
274
  "# connect {stim} to %(name)s\n"
274
275
  "%(name)s.connectStimulus({stim})\n"
275
276
  ).format(stim=stim.params['name'])
276
- buff.writeIndentedLines(code % self.params)
277
+ buff.writeIndentedLines(code % inits)
277
278
 
278
279
  def writeRoutineStartValidationCode(self, buff, stim):
279
280
  """