psychopy 2024.2.1__py3-none-any.whl → 2024.2.4__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 (276) hide show
  1. psychopy/.DS_Store +0 -0
  2. psychopy/GIT_SHA +1 -1
  3. psychopy/VERSION +1 -1
  4. psychopy/__init__.py +10 -1
  5. psychopy/__init__.py.orig +65 -0
  6. psychopy/app/{locale/ar_001/.DS_Store → .DS_Store} +0 -0
  7. psychopy/app/Resources/.DS_Store +0 -0
  8. psychopy/app/_psychopyApp.py +11 -3
  9. psychopy/app/appData.spec +1 -1
  10. psychopy/app/builder/builder.py +1 -1
  11. psychopy/app/builder/builder.py.orig +3932 -0
  12. psychopy/app/builder/dialogs/__init__.py.orig +1679 -0
  13. psychopy/app/builder/dialogs/paramCtrls.py +1 -1
  14. psychopy/app/builder/dialogs/paramCtrls.py.orig +713 -0
  15. psychopy/app/colorpicker/__init__.py.orig +411 -0
  16. psychopy/app/cortex.log +0 -0
  17. psychopy/app/jobs.py +8 -1
  18. psychopy/app/locale/ar_001/LC_MESSAGE/messages.po +2452 -1731
  19. psychopy/app/locale/zh_CN/LC_MESSAGE/zh_CN.mo +0 -0
  20. psychopy/app/locale/zh_CN/LC_MESSAGE/zh_CN.po +6127 -0
  21. psychopy/app/locale/zh_CN/LC_MESSAGE/zh_CN_allFlagged.mo +0 -0
  22. psychopy/app/locale/zh_CN/LC_MESSAGE/zh_CN_allFlagged.po +7366 -0
  23. psychopy/app/plugin_manager/dialog.py +9 -7
  24. psychopy/app/ribbon.py +2 -1
  25. psychopy/app/runner/runner.py +7 -5
  26. psychopy/clock.py +8 -4
  27. psychopy/core.py.orig +169 -0
  28. psychopy/demos/builder/Design Templates/randomisedBlocks/html/index.html +23 -0
  29. psychopy/demos/builder/Design Templates/randomisedBlocks/html/randomisedBlocks-legacy-browsers.js +423 -0
  30. psychopy/demos/builder/Design Templates/randomisedBlocks/html/randomisedBlocks.js +427 -0
  31. psychopy/demos/builder/Design Templates/randomisedBlocks/html/resources/chooseBlock.xlsx +0 -0
  32. psychopy/demos/builder/Design Templates/randomisedBlocks/html/resources/facesBlock.xlsx +0 -0
  33. psychopy/demos/builder/Design Templates/randomisedBlocks/html/resources/housesBlock.xlsx +0 -0
  34. psychopy/demos/builder/Design Templates/randomisedBlocks/html/resources/stims/face01.jpg +0 -0
  35. psychopy/demos/builder/Design Templates/randomisedBlocks/html/resources/stims/face02.jpg +0 -0
  36. psychopy/demos/builder/Design Templates/randomisedBlocks/html/resources/stims/face03.jpg +0 -0
  37. psychopy/demos/builder/Design Templates/randomisedBlocks/html/resources/stims/house01.jpg +0 -0
  38. psychopy/demos/builder/Design Templates/randomisedBlocks/html/resources/stims/house02.jpg +0 -0
  39. psychopy/demos/builder/Design Templates/randomisedBlocks/html/resources/stims/house03.jpg +0 -0
  40. psychopy/demos/builder/Design Templates/randomisedBlocks/randomisedBlocks.py +330 -0
  41. psychopy/demos/builder/Design Templates/randomisedBlocks/randomisedBlocks_lastrun.py +330 -0
  42. psychopy/demos/builder/Feature Demos/eyetracking/eyetracking.xml +298 -0
  43. psychopy/demos/builder/Feature Demos/eyetracking/eyetracking.xsd +120 -0
  44. psychopy/demos/builder/Tools/.DS_Store +0 -0
  45. psychopy/demos/builder/Tools/gammaCalibration/.DS_Store +0 -0
  46. psychopy/demos/builder/Tools/gammaCalibration/data/_gamma_correction_visual_2022-05-18_14h18.29.439.csv +38 -0
  47. psychopy/demos/builder/Tools/gammaCalibration/data/_gamma_correction_visual_2022-05-18_14h18.29.439.log +3418 -0
  48. psychopy/demos/builder/Tools/gammaCalibration/data/_gamma_correction_visual_2022-05-18_14h18.29.439.psydat +0 -0
  49. psychopy/demos/builder/Tools/gammaCalibration/data/x1_gamma_correction_visual_2022-05-17_13h59.42.928.csv +2 -0
  50. psychopy/demos/builder/Tools/gammaCalibration/data/x1_gamma_correction_visual_2022-05-17_13h59.42.928.log +15 -0
  51. psychopy/demos/builder/Tools/gammaCalibration/data/x1_gamma_correction_visual_2022-05-17_13h59.42.928.psydat +0 -0
  52. psychopy/demos/builder/Tools/gammaCalibration/gamma_correction_visual.psyexp +323 -0
  53. psychopy/demos/builder/Tools/gammaCalibration/gamma_correction_visual.py +562 -0
  54. psychopy/demos/builder/Tools/gammaCalibration/gamma_correction_visual_lastrun.py +562 -0
  55. psychopy/demos/builder/Tools/gammaCalibration/questStairs.xlsx +0 -0
  56. psychopy/demos/builder/Tools/gammaCalibration/readme.md +0 -0
  57. psychopy/demos/builder/Tools/gammaCalibration/resources/low_contrast.png +0 -0
  58. psychopy/demos/builder/Tools/gammaCalibration/resources/make_2nd_order_tex.py +59 -0
  59. psychopy/demos/builder/Tools/gammaCalibration/resources/second_order_tex.png +0 -0
  60. psychopy/demos/coder/.DS_Store +0 -0
  61. psychopy/demos/coder/experiment control/info_gamma.pickle +0 -0
  62. psychopy/demos/coder/iohub/.iohpid +1 -0
  63. psychopy/demos/coder/iohub/eyetracking/.iohpid +1 -0
  64. psychopy/demos/coder/iohub/wintab/.DS_Store +0 -0
  65. psychopy/demos/coder/stimuli/.DS_Store +0 -0
  66. psychopy/demos/coder/stimuli/radialGratingContracting.py +29 -0
  67. psychopy/experiment/_experiment.py.orig +1032 -0
  68. psychopy/experiment/components/.DS_Store +0 -0
  69. psychopy/experiment/components/_base.py +13 -4
  70. psychopy/experiment/components/_base.py.orig +823 -0
  71. psychopy/experiment/components/form/.DS_Store +0 -0
  72. psychopy/experiment/components/microphone/__init__.py +10 -1
  73. psychopy/experiment/components/microphone/__init__.py.orig +490 -0
  74. psychopy/experiment/components/polygon/__init__.py +21 -22
  75. psychopy/experiment/components/settings/__init__.py +13 -14
  76. psychopy/experiment/components/settings/__init__.py.orig +1337 -0
  77. psychopy/experiment/components/textbox/__init__.py.orig +310 -0
  78. psychopy/experiment/components/webcam/.DS_Store +0 -0
  79. psychopy/experiment/components/webcam/light/.DS_Store +0 -0
  80. psychopy/experiment/flow.py +10 -8
  81. psychopy/experiment/loops.py.orig +829 -0
  82. psychopy/experiment/params.py +8 -3
  83. psychopy/experiment/params.py.orig +408 -0
  84. psychopy/experiment/routine.py.orig +503 -0
  85. psychopy/experiment/routines/_base.py +15 -6
  86. psychopy/experiment/routines/counterbalance/__init__.py +1 -0
  87. psychopy/gui/qtgui.py +14 -7
  88. psychopy/gui/util.py +10 -14
  89. psychopy/gui/wxgui.py +10 -4
  90. psychopy/hardware/.DS_Store +0 -0
  91. psychopy/hardware/brainproducts.py.orig +680 -0
  92. psychopy/hardware/iolab.py.orig +238 -0
  93. psychopy/hardware/manager.py +1 -1
  94. psychopy/hardware/photodiode.py +59 -27
  95. psychopy/hardware/serialport.py +51 -0
  96. psychopy/hardware/speaker.py +4 -4
  97. psychopy/iohub/datastore/__init__.py.orig +443 -0
  98. psychopy/iohub/datastore/util.py.orig +692 -0
  99. psychopy/iohub/devices/mouse/darwin.py.orig +427 -0
  100. psychopy/iohub/devices/mouse/linux2.py.orig +198 -0
  101. psychopy/preferences/.DS_Store +0 -0
  102. psychopy/projects/pavlovia.py +10 -3
  103. psychopy/projects/pavlovia.py.orig +1295 -0
  104. psychopy/sound/backend_ptb.py +22 -5
  105. psychopy/sound/transcribe.py +24 -4
  106. psychopy/tests/.DS_Store +0 -0
  107. psychopy/tests/data/.DS_Store +0 -0
  108. psychopy/tests/data/TestCircle_fill_local.png +0 -0
  109. psychopy/tests/data/__test.png +0 -0
  110. psychopy/tests/data/aperture1_normHexbackground_local.png +0 -0
  111. psychopy/tests/data/aperture1_norm_local.png +0 -0
  112. psychopy/tests/data/aperture2_normHexbackground_local.png +0 -0
  113. psychopy/tests/data/beatandrcos_height_local.png +0 -0
  114. psychopy/tests/data/beatandrcos_normAddBlend_local.png +0 -0
  115. psychopy/tests/data/beatandrcos_normHexbackground_local.png +0 -0
  116. psychopy/tests/data/beatandrcos_norm_local.png +0 -0
  117. psychopy/tests/data/beatandrcos_stencil_local.png +0 -0
  118. psychopy/tests/data/blend_add_height_local.png +0 -0
  119. psychopy/tests/data/blend_add_normAddBlend_local.png +0 -0
  120. psychopy/tests/data/blend_add_normHexbackground_local.png +0 -0
  121. psychopy/tests/data/blend_add_normNoShade_local.png +0 -0
  122. psychopy/tests/data/blend_add_norm_local.png +0 -0
  123. psychopy/tests/data/blend_add_stencil_local.png +0 -0
  124. psychopy/tests/data/bufferimg_gabor_height_local.png +0 -0
  125. psychopy/tests/data/bufferimg_gabor_normAddBlend_local.png +0 -0
  126. psychopy/tests/data/bufferimg_gabor_normHexbackground_local.png +0 -0
  127. psychopy/tests/data/bufferimg_gabor_normNoShade_local.png +0 -0
  128. psychopy/tests/data/bufferimg_gabor_norm_local.png +0 -0
  129. psychopy/tests/data/bufferimg_gabor_stencil_local.png +0 -0
  130. psychopy/tests/data/circleHex_height_local.png +0 -0
  131. psychopy/tests/data/circleHex_normAddBlend_local.png +0 -0
  132. psychopy/tests/data/circleHex_normHexbackground_local.png +0 -0
  133. psychopy/tests/data/circleHex_normNoShade_local.png +0 -0
  134. psychopy/tests/data/circleHex_norm_local.png +0 -0
  135. psychopy/tests/data/circleHex_stencil_local.png +0 -0
  136. psychopy/tests/data/color_comparison_local.png +0 -0
  137. psychopy/tests/data/corrFullRandom_local.csv +16 -0
  138. psychopy/tests/data/corrFullRandom_local.tsv +6 -0
  139. psychopy/tests/data/correctScript/.DS_Store +0 -0
  140. psychopy/tests/data/dots_height_local.png +0 -0
  141. psychopy/tests/data/dots_normAddBlend_local.png +0 -0
  142. psychopy/tests/data/dots_normHexbackground_local.png +0 -0
  143. psychopy/tests/data/dots_normNoShade_local.png +0 -0
  144. psychopy/tests/data/dots_norm_local.png +0 -0
  145. psychopy/tests/data/dots_stencil_local.png +0 -0
  146. psychopy/tests/data/elarray1_height_local.png +0 -0
  147. psychopy/tests/data/elarray1_normAddBlend_local.png +0 -0
  148. psychopy/tests/data/elarray1_normHexbackground_local.png +0 -0
  149. psychopy/tests/data/elarray1_norm_local.png +0 -0
  150. psychopy/tests/data/elarray1_stencil_local.png +0 -0
  151. psychopy/tests/data/envelopeandrcos_height_local.png +0 -0
  152. psychopy/tests/data/envelopeandrcos_normAddBlend_local.png +0 -0
  153. psychopy/tests/data/envelopeandrcos_normHexbackground_local.png +0 -0
  154. psychopy/tests/data/envelopeandrcos_norm_local.png +0 -0
  155. psychopy/tests/data/envelopeandrcos_stencil_local.png +0 -0
  156. psychopy/tests/data/envelopepowerandrcos_height_local.png +0 -0
  157. psychopy/tests/data/envelopepowerandrcos_normAddBlend_local.png +0 -0
  158. psychopy/tests/data/envelopepowerandrcos_normHexbackground_local.png +0 -0
  159. psychopy/tests/data/envelopepowerandrcos_norm_local.png +0 -0
  160. psychopy/tests/data/envelopepowerandrcos_stencil_local.png +0 -0
  161. psychopy/tests/data/gabor1_height_local.png +0 -0
  162. psychopy/tests/data/gabor1_normAddBlend_local.png +0 -0
  163. psychopy/tests/data/gabor1_normHexbackground_local.png +0 -0
  164. psychopy/tests/data/gabor1_normNoShade_local.png +0 -0
  165. psychopy/tests/data/gabor1_norm_local.png +0 -0
  166. psychopy/tests/data/gabor1_stencil_local.png +0 -0
  167. psychopy/tests/data/greyscale_normHexbackground_local.png +0 -0
  168. psychopy/tests/data/imageAndGauss_height_local.png +0 -0
  169. psychopy/tests/data/imageAndGauss_normAddBlend_local.png +0 -0
  170. psychopy/tests/data/imageAndGauss_normHexbackground_local.png +0 -0
  171. psychopy/tests/data/imageAndGauss_normNoShade_local.png +0 -0
  172. psychopy/tests/data/imageAndGauss_norm_local.png +0 -0
  173. psychopy/tests/data/imageAndGauss_stencil_local.png +0 -0
  174. psychopy/tests/data/movFrame1_stencil_local.png +0 -0
  175. psychopy/tests/data/noiseAndRcos_height_local.png +0 -0
  176. psychopy/tests/data/noiseAndRcos_normAddBlend_local.png +0 -0
  177. psychopy/tests/data/noiseAndRcos_normHexbackground_local.png +0 -0
  178. psychopy/tests/data/noiseAndRcos_normNoShade_local.png +0 -0
  179. psychopy/tests/data/noiseAndRcos_norm_local.png +0 -0
  180. psychopy/tests/data/noiseAndRcos_stencil_local.png +0 -0
  181. psychopy/tests/data/noiseFiltersAndRcos_height_local.png +0 -0
  182. psychopy/tests/data/noiseFiltersAndRcos_normAddBlend_local.png +0 -0
  183. psychopy/tests/data/noiseFiltersAndRcos_normHexbackground_local.png +0 -0
  184. psychopy/tests/data/noiseFiltersAndRcos_normNoShade_local.png +0 -0
  185. psychopy/tests/data/noiseFiltersAndRcos_norm_local.png +0 -0
  186. psychopy/tests/data/noiseFiltersAndRcos_stencil_local.png +0 -0
  187. psychopy/tests/data/numpyImage_height_local.png +0 -0
  188. psychopy/tests/data/numpyImage_normAddBlend_local.png +0 -0
  189. psychopy/tests/data/numpyImage_normHexbackground_local.png +0 -0
  190. psychopy/tests/data/numpyImage_normNoShade_local.png +0 -0
  191. psychopy/tests/data/numpyImage_norm_local.png +0 -0
  192. psychopy/tests/data/numpyImage_stencil_local.png +0 -0
  193. psychopy/tests/data/shape2_1_normAddBlend_local.png +0 -0
  194. psychopy/tests/data/shape2_1_normHexbackground_local.png +0 -0
  195. psychopy/tests/data/shape2_1_normNoShade_local.png +0 -0
  196. psychopy/tests/data/shape2_1_norm_local.png +0 -0
  197. psychopy/tests/data/shape2_1_stencil_local.png +0 -0
  198. psychopy/tests/data/testLoopsBlocks.psyexp_local.py +328 -0
  199. psychopy/tests/data/text1_height_local.png +0 -0
  200. psychopy/tests/data/text1_normAddBlend_local.png +0 -0
  201. psychopy/tests/data/text1_normHexbackground_local.png +0 -0
  202. psychopy/tests/data/text1_norm_local.png +0 -0
  203. psychopy/tests/data/text1_stencil_local.png +0 -0
  204. psychopy/tests/data/text2_height.png +0 -0
  205. psychopy/tests/data/text2_normAddBlend.png +0 -0
  206. psychopy/tests/data/text2_normHexbackground.png +0 -0
  207. psychopy/tests/data/text2_stencil.png +0 -0
  208. psychopy/tests/data/wedge1_height_local.png +0 -0
  209. psychopy/tests/data/wedge1_normAddBlend_local.png +0 -0
  210. psychopy/tests/data/wedge1_normHexbackground_local.png +0 -0
  211. psychopy/tests/data/wedge1_normNoShade_local.png +0 -0
  212. psychopy/tests/data/wedge1_norm_local.png +0 -0
  213. psychopy/tests/data/wedge1_stencil_local.png +0 -0
  214. psychopy/tests/test_app/.DS_Store +0 -0
  215. psychopy/tests/test_app/test_builder/.DS_Store +0 -0
  216. psychopy/tests/test_app/test_builder/data/_2021_ 5_03_1206.csv +9 -0
  217. psychopy/tests/test_app/test_builder/data/_2021_ 5_03_1206.log +177 -0
  218. psychopy/tests/test_app/test_builder/data/_2021_ 5_03_1206.psydat +0 -0
  219. psychopy/tests/test_app/test_builder/data/_2021_ 5_03_1206.xlsx +0 -0
  220. psychopy/tests/test_app/test_builder/data/_2021_ 5_03_1324.csv +9 -0
  221. psychopy/tests/test_app/test_builder/data/_2021_ 5_03_1324.log +168 -0
  222. psychopy/tests/test_app/test_builder/data/_2021_ 5_03_1324.psydat +0 -0
  223. psychopy/tests/test_app/test_builder/data/_2021_ 5_03_1324.xlsx +0 -0
  224. psychopy/tests/test_data/.DS_Store +0 -0
  225. psychopy/tests/test_hardware/test_CRS_BitsSharp.py.orig +68 -0
  226. psychopy/tests/test_tools/test_arraytools.py +112 -0
  227. psychopy/tests/test_visual/test_image.py.orig +219 -0
  228. psychopy/tools/arraytools.py +47 -0
  229. psychopy/tools/versionchooser.py +1 -1
  230. psychopy/visual/backends/pygletbackend.py +26 -8
  231. psychopy/visual/basevisual.py.orig +1723 -0
  232. psychopy/visual/form.py.orig +1181 -0
  233. psychopy/visual/text.py.orig +752 -0
  234. psychopy/visual/textbox2/textbox2.py.orig +1315 -0
  235. psychopy/visual/window.py +13 -5
  236. psychopy/visual/windowwarp.py.orig +463 -0
  237. {psychopy-2024.2.1.dist-info → psychopy-2024.2.4.dist-info}/METADATA +9 -9
  238. {psychopy-2024.2.1.dist-info → psychopy-2024.2.4.dist-info}/RECORD +244 -78
  239. {psychopy-2024.2.1.dist-info → psychopy-2024.2.4.dist-info}/WHEEL +1 -1
  240. {psychopy-2024.2.1.dist-info → psychopy-2024.2.4.dist-info}/entry_points.txt +2 -0
  241. psychopy/app/locale/ar_001/LC_MESSAGE/messages.mo +0 -0
  242. psychopy/app/locale/cs_CZ/LC_MESSAGE/messages.mo +0 -0
  243. psychopy/app/locale/da_DK/LC_MESSAGE/messages.mo +0 -0
  244. psychopy/app/locale/de_DE/LC_MESSAGE/messages.mo +0 -0
  245. psychopy/app/locale/el_GR/LC_MESSAGE/messages.mo +0 -0
  246. psychopy/app/locale/en_NZ/LC_MESSAGE/messages.mo +0 -0
  247. psychopy/app/locale/en_US/LC_MESSAGE/messages.mo +0 -0
  248. psychopy/app/locale/es_CO/LC_MESSAGE/messages.mo +0 -0
  249. psychopy/app/locale/es_ES/LC_MESSAGE/messages.mo +0 -0
  250. psychopy/app/locale/es_US/LC_MESSAGE/messages.mo +0 -0
  251. psychopy/app/locale/et_EE/LC_MESSAGE/messages.mo +0 -0
  252. psychopy/app/locale/fa_IR/LC_MESSAGE/messages.mo +0 -0
  253. psychopy/app/locale/fi_FI/LC_MESSAGE/messages.mo +0 -0
  254. psychopy/app/locale/fr_FR/LC_MESSAGE/messages.mo +0 -0
  255. psychopy/app/locale/he_IL/LC_MESSAGE/messages.mo +0 -0
  256. psychopy/app/locale/hi_IN/LC_MESSAGE/messages.mo +0 -0
  257. psychopy/app/locale/hu_HU/LC_MESSAGE/messages.mo +0 -0
  258. psychopy/app/locale/it_IT/LC_MESSAGE/messages.mo +0 -0
  259. psychopy/app/locale/ja_JP/LC_MESSAGE/messages.mo +0 -0
  260. psychopy/app/locale/ko_KR/LC_MESSAGE/messages.mo +0 -0
  261. psychopy/app/locale/ms_MY/LC_MESSAGE/messages.mo +0 -0
  262. psychopy/app/locale/nl_NL/LC_MESSAGE/messages.mo +0 -0
  263. psychopy/app/locale/nn_NO/LC_MESSAGE/messages.mo +0 -0
  264. psychopy/app/locale/pl_PL/LC_MESSAGE/messages.mo +0 -0
  265. psychopy/app/locale/pt_PT/LC_MESSAGE/messages.mo +0 -0
  266. psychopy/app/locale/ro_RO/LC_MESSAGE/messages.mo +0 -0
  267. psychopy/app/locale/ru_RU/LC_MESSAGE/messages.mo +0 -0
  268. psychopy/app/locale/sv_SE/LC_MESSAGE/messages.mo +0 -0
  269. psychopy/app/locale/tr_TR/LC_MESSAGE/messages.mo +0 -0
  270. psychopy/app/locale/zh_CN/LC_MESSAGE/messages.mo +0 -0
  271. psychopy/app/locale/zh_TW/LC_MESSAGE/messages.mo +0 -0
  272. psychopy-2024.2.1.dist-info/licenses/AUTHORS.md +0 -138
  273. /psychopy/{app/locale/ar_001/LC_MESSAGE → demos/builder}/.DS_Store +0 -0
  274. /psychopy/{app/locale/es_ES/LC_MESSAGE → demos/builder/Experiments}/.DS_Store +0 -0
  275. /psychopy/{visual → demos/builder/Tools/gammaCalibration/data}/.DS_Store +0 -0
  276. {psychopy-2024.2.1.dist-info → psychopy-2024.2.4.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,1181 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+
5
+ # Part of the PsychoPy library
6
+ # Copyright (C) 2002-2018 Jonathan Peirce (C) 2019-2021 Open Science Tools Ltd.
7
+ # Distributed under the terms of the GNU General Public License (GPL).
8
+ import copy
9
+ import psychopy
10
+ from .text import TextStim
11
+ from .rect import Rect
12
+ from psychopy.data.utils import importConditions, listFromString
13
+ from psychopy.visual.basevisual import (BaseVisualStim,
14
+ ContainerMixin,
15
+ ColorMixin)
16
+ from psychopy import logging, layout
17
+ from random import shuffle
18
+ from pathlib import Path
19
+
20
+ __author__ = 'Jon Peirce, David Bridges, Anthony Haffey'
21
+
22
+ from ..colors import Color
23
+
24
+ _REQUIRED = -12349872349873 # an unlikely int
25
+
26
+ # a dict of known fields with their default vals
27
+ _knownFields = {
28
+ 'index': None, # optional field to index into the rows
29
+ 'itemText': _REQUIRED, # (question used until 2020.2)
30
+ 'itemColor': None,
31
+ 'itemWidth': 1, # fraction of the form
32
+ 'type': _REQUIRED, # type of response box (see below)
33
+ 'options': ('Yes', 'No'), # for choice box
34
+ 'ticks': None,#(1, 2, 3, 4, 5, 6, 7),
35
+ 'tickLabels': None,
36
+ 'font': None,
37
+ # for rating/slider
38
+ 'responseWidth': 1, # fraction of the form
39
+ 'responseColor': None,
40
+ 'markerColor': None,
41
+ 'layout': 'horiz', # can be vert or horiz
42
+ }
43
+ _doNotSave = [
44
+ 'itemCtrl', 'responseCtrl', # these genuinely can't be save
45
+ 'itemColor', 'itemWidth', 'options', 'ticks', 'tickLabels', # not useful?
46
+ 'responseWidth', 'responseColor', 'layout',
47
+ ]
48
+ _knownRespTypes = {
49
+ 'heading', 'description', # no responses
50
+ 'rating', 'slider', # slider is continuous
51
+ 'free text',
52
+ 'choice', 'radio' # synonyms (radio was used until v2020.2)
53
+ }
54
+ _synonyms = {
55
+ 'itemText': 'questionText',
56
+ 'choice': 'radio',
57
+ 'free text': 'textBox'
58
+ }
59
+
60
+ # Setting debug to True will set the sub-elements on Form to be outlined in red, making it easier to determine their position
61
+ debug = False
62
+
63
+
64
+ class Form(BaseVisualStim, ContainerMixin, ColorMixin):
65
+ """A class to add Forms to a `psychopy.visual.Window`
66
+
67
+ The Form allows Psychopy to be used as a questionnaire tool, where
68
+ participants can be presented with a series of questions requiring responses.
69
+ Form items, defined as questions and response pairs, are presented
70
+ simultaneously onscreen with a scrollable viewing window.
71
+
72
+ Example
73
+ -------
74
+ survey = Form(win, items=[{}], size=(1.0, 0.7), pos=(0.0, 0.0))
75
+
76
+ Parameters
77
+ ----------
78
+ win : psychopy.visual.Window
79
+ The window object to present the form.
80
+ items : List of dicts or csv or xlsx file
81
+ a list of dicts or csv file should have the following key, value pairs / column headers:
82
+ "index": The item index as a number
83
+ "itemText": item question string,
84
+ "itemWidth": fraction of the form width 0:1
85
+ "type": type of rating e.g., 'radio', 'rating', 'slider'
86
+ "responseWidth": fraction of the form width 0:1,
87
+ "options": list of tick labels for options,
88
+ "layout": Response object layout e.g., 'horiz' or 'vert'
89
+ textHeight : float
90
+ Text height.
91
+ size : tuple, list
92
+ Size of form on screen.
93
+ pos : tuple, list
94
+ Position of form on screen.
95
+ itemPadding : float
96
+ Space or padding between form items.
97
+ units : str
98
+ units for stimuli - Currently, Form class only operates with 'height' units.
99
+ randomize : bool
100
+ Randomize order of Form elements
101
+ """
102
+
103
+ knownStyles = {
104
+ 'light': {
105
+ 'fillColor': [0.89, 0.89, 0.89],
106
+ 'borderColor': None,
107
+ 'itemColor': 'black',
108
+ 'responseColor': 'black',
109
+ 'markerColor': [0.89, -0.35, -0.28],
110
+ 'font': "Open Sans",
111
+ },
112
+ 'dark': {
113
+ 'fillColor': [-0.19, -0.19, -0.14],
114
+ 'borderColor': None,
115
+ 'itemColor': 'white',
116
+ 'responseColor': 'white',
117
+ 'markerColor': [0.89, -0.35, -0.28],
118
+ 'font': "Open Sans",
119
+ },
120
+ }
121
+
122
+ def __init__(self,
123
+ win,
124
+ name='default',
125
+ colorSpace='rgb',
126
+ fillColor=None,
127
+ borderColor=None,
128
+ itemColor='white',
129
+ responseColor='white',
130
+ markerColor='red',
131
+ items=None,
132
+ font=None,
133
+ textHeight=.02,
134
+ size=(.5, .5),
135
+ pos=(0, 0),
136
+ style=None,
137
+ itemPadding=0.05,
138
+ units='height',
139
+ randomize=False,
140
+ autoLog=True,
141
+ # legacy
142
+ color=None,
143
+ foreColor=None
144
+ ):
145
+
146
+ super(Form, self).__init__(win, units, autoLog=False)
147
+ self.win = win
148
+ self.autoLog = autoLog
149
+ self.name = name
150
+ self.randomize = randomize
151
+ self.items = self.importItems(items)
152
+ self.size = size
153
+ self._pos = pos
154
+ self.itemPadding = itemPadding
155
+ self.scrollSpeed = self.setScrollSpeed(self.items, 4)
156
+ self.units = units
157
+ self.depth = 0
158
+
159
+ # Appearance
160
+ self.colorSpace = colorSpace
161
+ self.fillColor = fillColor
162
+ self.borderColor = borderColor
163
+ self.itemColor = itemColor
164
+ self.responseColor = responseColor
165
+ self.markerColor = markerColor
166
+ if color:
167
+ self.foreColor = color
168
+ if foreColor:
169
+ self.foreColor = color
170
+
171
+ self.font = font or "Open Sans"
172
+
173
+ self.textHeight = textHeight
174
+ self._baseYpositions = []
175
+ self.leftEdge = None
176
+ self.rightEdge = None
177
+ self.topEdge = None
178
+ self._currentVirtualY = 0 # Y position in the virtual sheet
179
+ self._vheight = 0 # Height of the virtual sheet
180
+ self._decorations = []
181
+ self._externalDecorations = []
182
+ # Check units - only works with height units for now
183
+ if self.win.units != 'height':
184
+ logging.warning(
185
+ "Form currently only formats correctly using height units. "
186
+ "Please change the units in Experiment Settings to 'height'")
187
+
188
+ self._complete = False
189
+
190
+ # Create layout of form
191
+ self._createItemCtrls()
192
+
193
+ self.style = style
194
+
195
+ if self.autoLog:
196
+ logging.exp("Created {} = {}".format(self.name, repr(self)))
197
+
198
+ def __repr__(self, complete=False):
199
+ return self.__str__(complete=complete) # from MinimalStim
200
+
201
+ def importItems(self, items):
202
+ """Import items from csv or excel sheet and convert to list of dicts.
203
+ Will also accept a list of dicts.
204
+
205
+ Note, for csv and excel files, 'options' must contain comma separated values,
206
+ e.g., one, two, three. No parenthesis, or quotation marks required.
207
+
208
+ Parameters
209
+ ----------
210
+ items : Excel or CSV file, list of dicts
211
+ Items used to populate the Form
212
+
213
+ Returns
214
+ -------
215
+ List of dicts
216
+ A list of dicts, where each list entry is a dict containing all fields for a single Form item
217
+ """
218
+ def _checkSynonyms(items, fieldNames):
219
+ """Checks for updated names for fields (i.e. synonyms)"""
220
+
221
+ replacedFields = set()
222
+ for field in _synonyms:
223
+ synonym = _synonyms[field]
224
+ for item in items:
225
+ if synonym in item:
226
+ # convert to new name
227
+ item[field] = item[synonym]
228
+ del item[synonym]
229
+ replacedFields.add(field)
230
+ for field in replacedFields:
231
+ fieldNames.append(field)
232
+ fieldNames.remove(_synonyms[field])
233
+ logging.warning("Form {} included field no longer used {}. "
234
+ "Replacing with new name '{}'"
235
+ .format(self.name, _synonyms[field], field))
236
+
237
+ def _checkRequiredFields(fieldNames):
238
+ """Checks for required headings (do this after checking synonyms)"""
239
+ for hdr in _knownFields:
240
+ # is it required and/or present?
241
+ if _knownFields[hdr] == _REQUIRED and hdr not in fieldNames:
242
+ raise ValueError("Missing header ({}) in Form ({}). "
243
+ "Headers found were: {}"
244
+ .format(hdr, self.name, fieldNames))
245
+
246
+
247
+ def _checkTypes(types, itemText):
248
+ """A nested function for testing the number of options given
249
+
250
+ Raises ValueError if n Options not > 1
251
+ """
252
+ itemDiff = set([types]) - set(_knownRespTypes)
253
+
254
+ for incorrItemType in itemDiff:
255
+ if incorrItemType == _REQUIRED:
256
+ if self._itemsFile:
257
+ itemsFileStr = ("in items file '{}'"
258
+ .format(self._itemsFile))
259
+ else:
260
+ itemsFileStr = ""
261
+ msg = ("Item {}{} is missing a required "
262
+ "value for its response type. Permitted types are "
263
+ "{}.".format(itemText, itemsFileStr,
264
+ _knownRespTypes))
265
+ if self.autoLog:
266
+ logging.error(msg)
267
+ raise ValueError(msg)
268
+
269
+ def _addDefaultItems(items):
270
+ """
271
+ Adds default items when missing. Works in-place.
272
+
273
+ Parameters
274
+ ----------
275
+ items : List of dicts
276
+ headers : List of column headers for each item
277
+ """
278
+ def isPresent(d, field):
279
+ # check if the field is there and not empty on this row
280
+ return (field in d and d[field] not in [None, ''])
281
+
282
+ missingHeaders = []
283
+ defaultValues = _knownFields
284
+ for index, item in enumerate(items):
285
+ defaultValues['index'] = index
286
+ for header in defaultValues:
287
+ # if header is missing of val is None or ''
288
+ if not isPresent(item, header):
289
+ oldHeader = header.replace('item', 'question')
290
+ if isPresent(item, oldHeader):
291
+ item[header] = item[oldHeader]
292
+ logging.warning(
293
+ "{} is a deprecated heading for Forms. "
294
+ "Use {} instead"
295
+ .format(oldHeader, header)
296
+ )
297
+ continue
298
+ # Default to colour scheme if specified
299
+ if defaultValues[header] in ['fg', 'bg', 'em']:
300
+ item[header] = self.color
301
+ else:
302
+ item[header] = defaultValues[header]
303
+ missingHeaders.append(header)
304
+
305
+ msg = "Using default values for the following headers: {}".format(
306
+ missingHeaders)
307
+ if self.autoLog:
308
+ logging.info(msg)
309
+
310
+ if self.autoLog:
311
+ logging.info("Importing items...")
312
+
313
+ if not isinstance(items, list):
314
+ # items is a conditions file
315
+ self._itemsFile = Path(items)
316
+ items, fieldNames = importConditions(items, returnFieldNames=True)
317
+ else: # we already have a list so lets find the fieldnames
318
+ fieldNames = set()
319
+ for item in items:
320
+ fieldNames = fieldNames.union(item)
321
+ fieldNames = list(fieldNames) # convert to list at the end
322
+ self._itemsFile = None
323
+
324
+ _checkSynonyms(items, fieldNames)
325
+ _checkRequiredFields(fieldNames)
326
+ # Add default values if entries missing
327
+ _addDefaultItems(items)
328
+
329
+ # Convert options to list of strings
330
+ for idx, item in enumerate(items):
331
+ if item['ticks']:
332
+ item['ticks'] = listFromString(item['ticks'])
333
+ if 'tickLabels' in item and item['tickLabels']:
334
+ item['tickLabels'] = listFromString(item['tickLabels'])
335
+ if 'options' in item and item['options']:
336
+ item['options'] = listFromString(item['options'])
337
+
338
+ # Check types
339
+ [_checkTypes(item['type'], item['itemText']) for item in items]
340
+ # Check N options > 1
341
+ # Randomise items if requested
342
+ if self.randomize:
343
+ shuffle(items)
344
+ return items
345
+
346
+ def setScrollSpeed(self, items, multiplier=2):
347
+ """Set scroll speed of Form. Higher multiplier gives smoother, but
348
+ slower scroll.
349
+
350
+ Parameters
351
+ ----------
352
+ items : list of dicts
353
+ Items used to populate the form
354
+ multiplier : int (default=2)
355
+ Number used to calculate scroll speed
356
+
357
+ Returns
358
+ -------
359
+ int
360
+ Scroll speed, calculated using N items by multiplier
361
+ """
362
+ return len(items) * multiplier
363
+
364
+ def _getItemRenderedWidth(self, size):
365
+ """Returns text width for item text based on itemWidth and Form width.
366
+
367
+ Parameters
368
+ ----------
369
+ size : float, int
370
+ The question width
371
+
372
+ Returns
373
+ -------
374
+ float
375
+ Wrap width for question text
376
+ """
377
+ return size * self.size[0] - (self.itemPadding * 2)
378
+
379
+ def _setQuestion(self, item):
380
+ """Creates TextStim object containing question
381
+
382
+ Parameters
383
+ ----------
384
+ item : dict
385
+ The dict entry for a single item
386
+
387
+ Returns
388
+ -------
389
+ psychopy.visual.text.TextStim
390
+ The textstim object with the question string
391
+ questionHeight
392
+ The height of the question bounding box as type float
393
+ questionWidth
394
+ The width of the question bounding box as type float
395
+ """
396
+ if self.autoLog:
397
+ logging.exp(
398
+ u"Question text: {}".format(item['itemText']))
399
+
400
+ if item['type'] == 'heading':
401
+ letterScale = 1.5
402
+ bold = True
403
+ else:
404
+ letterScale = 1.0
405
+ bold = False
406
+ w = self._getItemRenderedWidth(item['itemWidth'])
407
+ question = psychopy.visual.TextBox2(
408
+ self.win,
409
+ text=item['itemText'],
410
+ units=self.units,
411
+ letterHeight=self.textHeight * letterScale,
412
+ anchor='top-left',
413
+ alignment='center-left',
414
+ pos=(self.leftEdge+self.itemPadding, 0), # y pos irrelevant
415
+ size=[w, 0.1], # expand height with text
416
+ autoLog=False,
417
+ colorSpace=self.colorSpace,
418
+ color=item['itemColor'] or self.itemColor,
419
+ fillColor=None,
420
+ padding=0, # handle this by padding between items
421
+ borderWidth=1,
422
+ borderColor='red' if debug else None, # add borderColor to help debug
423
+ editable=False,
424
+ bold=bold,
425
+ font=item['font'] or self.font)
426
+ # Resize textbox to be at least as tall as the text
427
+ question._updateVertices()
428
+ textHeight = getattr(question.boundingBox._size, question.units)[1]
429
+ if textHeight > question.size[1]:
430
+ question.size[1] = textHeight + question.padding[1] * 2
431
+ question._layout()
432
+
433
+ questionHeight = question.size[1]
434
+ questionWidth = question.size[0]
435
+ # store virtual pos to combine with scroll bar for actual pos
436
+ question._baseY = self._currentVirtualY
437
+
438
+ # Add question objects to Form element dict
439
+ item['itemCtrl'] = question
440
+
441
+ return question, questionHeight, questionWidth
442
+
443
+ def _setResponse(self, item):
444
+ """Makes calls to methods which make Slider or TextBox response objects
445
+ for Form
446
+
447
+ Parameters
448
+ ----------
449
+ item : dict
450
+ The dict entry for a single item
451
+ question : TextStim
452
+ The question text object
453
+
454
+ Returns
455
+ -------
456
+ psychopy.visual.slider.Slider
457
+ The Slider object for response
458
+ psychopy.visual.TextBox
459
+ The TextBox object for response
460
+ respHeight
461
+ The height of the response object as type float
462
+ """
463
+ if self.autoLog:
464
+ logging.info(
465
+ "Adding response to Form type: {}, layout: {}, options: {}"
466
+ .format(item['type'], item['layout'], item['options']))
467
+
468
+ if item['type'].lower() == 'free text':
469
+ respCtrl, respHeight = self._makeTextBox(item)
470
+ elif item['type'].lower() in ['heading', 'description']:
471
+ respCtrl, respHeight = None, 0
472
+ elif item['type'].lower() in ['rating', 'slider', 'choice', 'radio']:
473
+ respCtrl, respHeight = self._makeSlider(item)
474
+
475
+ item['responseCtrl'] = respCtrl
476
+ return respCtrl, float(respHeight)
477
+
478
+ def _makeSlider(self, item):
479
+ """Creates Slider object for Form class
480
+
481
+ Parameters
482
+ ----------
483
+ item : dict
484
+ The dict entry for a single item
485
+ pos : tuple
486
+ position of response object
487
+
488
+ Returns
489
+ -------
490
+ psychopy.visual.slider.Slider
491
+ The Slider object for response
492
+ respHeight
493
+ The height of the response object as type float
494
+ """
495
+ # Slider dict
496
+
497
+ kind = item['type'].lower()
498
+
499
+ # what are the ticks for the scale/slider?
500
+ if item['type'].lower() in ['radio', 'choice']:
501
+ if item['ticks']:
502
+ ticks = item['ticks']
503
+ else:
504
+ ticks = None
505
+ tickLabels = item['tickLabels'] or item['options'] or item['ticks']
506
+ granularity = 1
507
+ style = 'radio'
508
+ else:
509
+ if item['ticks']:
510
+ ticks = item['ticks']
511
+ elif item['options']:
512
+ ticks = range(0, len(item['options']))
513
+
514
+ else:
515
+ raise ValueError("We don't appear to have either options or "
516
+ "ticks for item '{}' of {}."
517
+ .format(item['itemText'], self.name))
518
+ # how to label those ticks
519
+ if item['tickLabels']:
520
+ tickLabels = [str(i).strip() for i in item['tickLabels']]
521
+ elif 'options' in item and item['options']:
522
+ tickLabels = [str(i).strip() for i in item['options']]
523
+ else:
524
+ tickLabels = None
525
+ # style/granularity
526
+ if kind == 'slider' and 'granularity' in item:
527
+ if item['granularity']:
528
+ granularity = item['granularity']
529
+ else:
530
+ granularity = 0
531
+ elif kind == 'slider' and 'granularity' not in item:
532
+ granularity = 0
533
+ else:
534
+ granularity = 1
535
+ style = kind
536
+
537
+ # Make invisible guide rect to help with laying out slider
538
+ w = (item['responseWidth'] - self.itemPadding * 2) * (self.size[0] - self.scrollbarWidth) * 0.8
539
+ if item['layout'] == 'horiz':
540
+ h = self.textHeight * 2 + 0.03
541
+ elif item['layout'] == 'vert':
542
+ h = self.textHeight * 1.1 * len(item['options'])
543
+ x = self.rightEdge - self.itemPadding - self.scrollbarWidth - w * 0.1
544
+ guide = Rect(
545
+ self.win,
546
+ size=(w, h),
547
+ pos=(x, 0),
548
+ anchor="top-right",
549
+ lineColor="red",
550
+ fillColor=None,
551
+ units=self.units,
552
+ autoLog=False
553
+ )
554
+ # Get slider pos and size
555
+ if item['layout'] == 'horiz':
556
+ x = guide.pos[0] - guide.size[0] / 2
557
+ w = guide.size[0]
558
+ h = 0.03
559
+ wrap = None # Slider defaults are fine for horizontal
560
+ elif item['layout'] == 'vert':
561
+ # for vertical take into account the nOptions
562
+ x = guide.pos[0] - guide.size[0]
563
+ w = 0.03
564
+ h = guide.size[1]
565
+ wrap = guide.size[0] / 2 - 0.03
566
+ item['options'].reverse()
567
+
568
+ # Create Slider
569
+ resp = psychopy.visual.Slider(
570
+ self.win,
571
+ pos=(x, 0), # NB y pos is irrelevant here - handled later
572
+ size=(w, h),
573
+ ticks=ticks,
574
+ labels=tickLabels,
575
+ units=self.units,
576
+ labelHeight=self.textHeight,
577
+ labelWrapWidth=wrap,
578
+ granularity=granularity,
579
+ flip=True,
580
+ style=style,
581
+ autoLog=False,
582
+ font=item['font'] or self.font,
583
+ color=item['responseColor'] or self.responseColor,
584
+ fillColor=item['markerColor'] or self.markerColor,
585
+ borderColor=item['responseColor'] or self.responseColor,
586
+ colorSpace=self.colorSpace)
587
+ resp.guide = guide
588
+
589
+ # store virtual pos to combine with scroll bar for actual pos
590
+ resp._baseY = self._currentVirtualY - guide.size[1] / 2 - self.itemPadding
591
+
592
+ return resp, guide.size[1]
593
+
594
+ def _getItemHeight(self, item, ctrl=None):
595
+ """Returns the full height of the item to be inserted in the form"""
596
+ if type(ctrl) == psychopy.visual.TextBox2:
597
+ return ctrl.size[1]
598
+ if type(ctrl) == psychopy.visual.Slider:
599
+ # Set radio button layout
600
+ if item['layout'] == 'horiz':
601
+ return 0.03 + ctrl.labelHeight*3
602
+ elif item['layout'] == 'vert':
603
+ # for vertical take into account the nOptions
604
+ return ctrl.labelHeight*len(item['options'])
605
+
606
+ def _makeTextBox(self, item):
607
+ """Creates TextBox object for Form class
608
+
609
+ NOTE: The TextBox 2 in work in progress, and has not been added to Form class yet.
610
+ Parameters
611
+ ----------
612
+ item : dict
613
+ The dict entry for a single item
614
+ pos : tuple
615
+ position of response object
616
+
617
+ Returns
618
+ -------
619
+ psychopy.visual.TextBox
620
+ The TextBox object for response
621
+ respHeight
622
+ The height of the response object as type float
623
+ """
624
+ w = (item['responseWidth'] - self.itemPadding * 2) * (self.size[0] - self.scrollbarWidth)
625
+ x = self.rightEdge - self.itemPadding - self.scrollbarWidth
626
+ resp = psychopy.visual.TextBox2(
627
+ self.win,
628
+ text='',
629
+ pos=(x, 0), # y pos irrelevant now (handled by scrollbar)
630
+ size=(w, 0.1),
631
+ letterHeight=self.textHeight,
632
+ units=self.units,
633
+ anchor='top-right',
634
+ color=item['responseColor'] or self.responseColor,
635
+ colorSpace=self.colorSpace,
636
+ font=item['font'] or self.font,
637
+ editable=True,
638
+ borderColor=item['responseColor'] or self.responseColor,
639
+ borderWidth=2,
640
+ fillColor=None,
641
+ onTextCallback=self._layoutY,
642
+ )
643
+ if debug:
644
+ resp.borderColor = "red"
645
+ # Resize textbox to be at least as tall as the text
646
+ resp._updateVertices()
647
+ textHeight = getattr(resp.boundingBox._size, resp.units)[1]
648
+ if textHeight > resp.size[1]:
649
+ resp.size[1] = textHeight + resp.padding[1] * 2
650
+ resp._layout()
651
+
652
+ respHeight = resp.size[1]
653
+ # store virtual pos to combine with scroll bar for actual pos
654
+ resp._baseY = self._currentVirtualY
655
+
656
+ return resp, respHeight
657
+
658
+ def _setScrollBar(self):
659
+ """Creates Slider object for scrollbar
660
+
661
+ Returns
662
+ -------
663
+ psychopy.visual.slider.Slider
664
+ The Slider object for scroll bar
665
+ """
666
+ scroll = psychopy.visual.Slider(win=self.win,
667
+ size=(self.scrollbarWidth, self.size[1] / 1.2), # Adjust size to account for scrollbar overflow
668
+ ticks=[0, 1],
669
+ style='scrollbar',
670
+ borderColor=self.responseColor,
671
+ fillColor=self.markerColor,
672
+ pos=(self.rightEdge - self.scrollbarWidth / 2, self.pos[1]),
673
+ autoLog=False)
674
+ return scroll
675
+
676
+ def _setBorder(self):
677
+ """Creates border using Rect
678
+
679
+ Returns
680
+ -------
681
+ psychopy.visual.Rect
682
+ The border for the survey
683
+ """
684
+ return psychopy.visual.Rect(win=self.win,
685
+ units=self.units,
686
+ pos=self.pos,
687
+ width=self.size[0],
688
+ height=self.size[1],
689
+ colorSpace=self.colorSpace,
690
+ fillColor=self.fillColor,
691
+ lineColor=self.borderColor,
692
+ opacity=None,
693
+ autoLog=False)
694
+
695
+ def _setAperture(self):
696
+ """Blocks text beyond border using Aperture
697
+
698
+ Returns
699
+ -------
700
+ psychopy.visual.Aperture
701
+ The aperture setting viewable area for forms
702
+ """
703
+ aperture = psychopy.visual.Aperture(win=self.win,
704
+ name='aperture',
705
+ units=self.units,
706
+ shape='square',
707
+ size=self.size,
708
+ pos=self.pos,
709
+ autoLog=False)
710
+ aperture.disable() # Disable on creation. Only enable on draw.
711
+ return aperture
712
+
713
+ def _getScrollOffset(self):
714
+ """Calculate offset position of items in relation to markerPos. Offset is a proportion of
715
+ `vheight - height`, meaning the max offset (when scrollbar.markerPos is 1) is enough
716
+ to take the bottom element to the bottom of the border.
717
+
718
+ Returns
719
+ -------
720
+ float
721
+ Offset position of items proportionate to scroll bar
722
+ """
723
+ offset = max(self._vheight - self.size[1], 0) * (1 - self.scrollbar.markerPos) * -1
724
+ return offset
725
+
726
+ def _createItemCtrls(self):
727
+ """Define layout of form"""
728
+ # Define boundaries of form
729
+ if self.autoLog:
730
+ logging.info("Setting layout of Form: {}.".format(self.name))
731
+
732
+ self.leftEdge = self.pos[0] - self.size[0] / 2.0
733
+ self.rightEdge = self.pos[0] + self.size[0] / 2.0
734
+
735
+ # For each question, create textstim and rating scale
736
+ for item in self.items:
737
+ # set up the question object
738
+ self._setQuestion(item)
739
+ # set up the response object
740
+ self._setResponse(item)
741
+
742
+
743
+ # position a slider on right-hand edge
744
+ self.scrollbar = self._setScrollBar()
745
+ self.scrollbar.markerPos = 1 # Set scrollbar to start position
746
+ self.border = self._setBorder()
747
+ self.aperture = self._setAperture()
748
+ # then layout the Y positions
749
+ self._layoutY()
750
+
751
+ if self.autoLog:
752
+ logging.info("Layout set for Form: {}.".format(self.name))
753
+
754
+ def _layoutY(self):
755
+ """This needs to be done when editable textboxes change their size
756
+ because everything below them needs to move too"""
757
+
758
+ self.topEdge = self.pos[1] + self.size[1] / 2.0
759
+
760
+ self._currentVirtualY = self.topEdge - self.itemPadding
761
+ # For each question, create textstim and rating scale
762
+ for item in self.items:
763
+ question = item['itemCtrl']
764
+ response = item['responseCtrl']
765
+
766
+ # update item baseY
767
+ question._baseY = self._currentVirtualY
768
+ # and get height to update current Y
769
+ questionHeight = self._getItemHeight(item=item, ctrl=question)
770
+
771
+ # go on to next line if together they're too wide
772
+ oneLine = (item['itemWidth']+item['responseWidth'] <= 1
773
+ or not response)
774
+ if not oneLine:
775
+ # response on next line
776
+ self._currentVirtualY -= questionHeight + self.itemPadding / 4
777
+
778
+ # update response baseY
779
+ if not response:
780
+ self._currentVirtualY -= questionHeight + self.itemPadding
781
+ continue
782
+ # get height to update current Y
783
+ respHeight = self._getItemHeight(item=item, ctrl=response)
784
+
785
+ # update item baseY
786
+ # slider needs to align by middle
787
+ if type(response) == psychopy.visual.Slider:
788
+ response._baseY = self._currentVirtualY - max(questionHeight, respHeight)/2
789
+ else: # hopefully we have an object that can anchor at top?
790
+ response._baseY = self._currentVirtualY
791
+
792
+ # go on to next line if together they're too wide
793
+ if oneLine:
794
+ # response on same line - work out which is bigger
795
+ self._currentVirtualY -= (
796
+ max(questionHeight, respHeight) + self.itemPadding
797
+ )
798
+ else:
799
+ # response on next line
800
+ self._currentVirtualY -= respHeight + self.itemPadding * 5/4
801
+
802
+ <<<<<<< HEAD
803
+ self._setDecorations() # choose whether show/hide scrollbar
804
+ =======
805
+ # Calculate virtual height as distance from top edge to bottom of last element
806
+ self._vheight = abs(self.topEdge - self._currentVirtualY)
807
+
808
+ self._setDecorations() # choose whether show/hide scroolbar
809
+ >>>>>>> 42d3d4fb2734301c9ae5f8a95868a9a5c2c4e7a5
810
+
811
+ def _setDecorations(self):
812
+ """Sets Form decorations i.e., Border and scrollbar"""
813
+ # add scrollbar if it's needed
814
+ self._decorations = [self.border]
815
+ if self._vheight > self.size[1]:
816
+ self._decorations.append(self.scrollbar)
817
+
818
+ def _inRange(self, item):
819
+ """Check whether item position falls within border area
820
+
821
+ Parameters
822
+ ----------
823
+ item : TextStim, Slider object
824
+ TextStim or Slider item from survey
825
+
826
+ Returns
827
+ -------
828
+ bool
829
+ Returns True if item position falls within border area
830
+ """
831
+ upperRange = self.size[1]
832
+ lowerRange = -self.size[1]
833
+ return (item.pos[1] < upperRange and item.pos[1] > lowerRange)
834
+
835
+ def _drawDecorations(self):
836
+ """Draw decorations on form."""
837
+ [decoration.draw() for decoration in self._decorations]
838
+
839
+ def _drawExternalDecorations(self):
840
+ """Draw decorations outside the aperture"""
841
+ [decoration.draw() for decoration in self._externalDecorations]
842
+
843
+ def _drawCtrls(self):
844
+ """Draw elements on form within border range.
845
+
846
+ Parameters
847
+ ----------
848
+ items : List
849
+ List of TextStim or Slider item from survey
850
+ """
851
+ for idx, item in enumerate(self.items):
852
+ for element in [item['itemCtrl'], item['responseCtrl']]:
853
+ if element is None: # e.g. because this has no resp obj
854
+ continue
855
+
856
+ element.pos = (element.pos[0],
857
+ element._baseY - self._getScrollOffset())
858
+ if self._inRange(element):
859
+ element.draw()
860
+ if debug and hasattr(element, "guide"):
861
+ # If debugging, draw position guide too
862
+ element.guide.pos = (element.guide.pos[0], element._baseY - self._getScrollOffset() + element.guide.size[1] / 2)
863
+ element.guide.draw()
864
+
865
+ def setAutoDraw(self, value, log=None):
866
+ """Sets autoDraw for Form and any responseCtrl contained within
867
+ """
868
+ for i in self.items:
869
+ if i['responseCtrl']:
870
+ i['responseCtrl'].__dict__['autoDraw'] = value
871
+ self.win.addEditable(i['responseCtrl'])
872
+ BaseVisualStim.setAutoDraw(self, value, log)
873
+
874
+ def draw(self):
875
+ """Draw all form elements"""
876
+ # Check mouse wheel
877
+ self.scrollbar.markerPos += self.scrollbar.mouse.getWheelRel()[
878
+ 1] / self.scrollSpeed
879
+ # draw the box and scrollbar
880
+ self._drawExternalDecorations()
881
+ # enable aperture
882
+ self.aperture.enable()
883
+ # draw the box and scrollbar
884
+ self._drawDecorations()
885
+ # Draw question and response objects
886
+ self._drawCtrls()
887
+ # disable aperture
888
+ self.aperture.disable()
889
+
890
+ def getData(self):
891
+ """Extracts form questions, response ratings and response times from
892
+ Form items
893
+
894
+ Returns
895
+ -------
896
+ list
897
+ A copy of the data as a list of dicts
898
+ """
899
+ nIncomplete = 0
900
+ nIncompleteRequired = 0
901
+ for thisItem in self.items:
902
+ if 'responseCtrl' not in thisItem or not thisItem['responseCtrl']:
903
+ continue # maybe a heading or similar
904
+ responseCtrl = thisItem['responseCtrl']
905
+ # get response if available
906
+ if hasattr(responseCtrl, 'getRating'):
907
+ thisItem['response'] = responseCtrl.getRating()
908
+ else:
909
+ thisItem['response'] = responseCtrl.text
910
+ if thisItem['response'] in [None, '']:
911
+ # todo : handle required items here (e.g. ending with * ?)
912
+ nIncomplete += 1
913
+ # get RT if available
914
+ if hasattr(responseCtrl, 'getRT'):
915
+ thisItem['rt'] = responseCtrl.getRT()
916
+ else:
917
+ thisItem['rt'] = None
918
+ self._complete = (nIncomplete == 0)
919
+ return copy.copy(self.items) # don't want users changing orig
920
+
921
+ def addDataToExp(self, exp, itemsAs='rows'):
922
+ """Gets the current Form data and inserts into an
923
+ :class:`~psychopy.experiment.ExperimentHandler` object either as rows
924
+ or as columns
925
+
926
+ Parameters
927
+ ----------
928
+ exp : :class:`~psychopy.experiment.ExperimentHandler`
929
+ itemsAs: 'rows','cols' (or 'columns')
930
+
931
+ Returns
932
+ -------
933
+
934
+ """
935
+ data = self.getData() # will be a copy of data (we can trash it)
936
+ asCols = itemsAs.lower() in ['cols', 'columns']
937
+ # iterate over items and fields within each item
938
+ # iterate all items and all fields before calling nextEntry
939
+ for ii, thisItem in enumerate(data): # data is a list of dicts
940
+ for fieldName in thisItem:
941
+ if fieldName in _doNotSave:
942
+ continue
943
+ if asCols: # for columns format, we need index for item
944
+ columnName = "{}[{}].{}".format(self.name, ii, fieldName)
945
+ else:
946
+ columnName = "{}.{}".format(self.name, fieldName)
947
+ exp.addData(columnName, thisItem[fieldName])
948
+ # finished field
949
+ if not asCols: # for rows format we add a newline each item
950
+ exp.nextEntry()
951
+ # finished item
952
+ # finished form
953
+ if asCols: # for cols format we add a newline each item
954
+ exp.nextEntry()
955
+
956
+ def formComplete(self):
957
+ """Deprecated in version 2020.2. Please use the Form.complete property
958
+ """
959
+ return self.complete
960
+
961
+ @property
962
+ def pos(self):
963
+ if hasattr(self, '_pos'):
964
+ return self._pos
965
+
966
+ @pos.setter
967
+ def pos(self, value):
968
+ self._pos = value
969
+ if hasattr(self, 'aperture'):
970
+ self.aperture.pos = value
971
+ if hasattr(self, 'border'):
972
+ self.border.pos = value
973
+ self.leftEdge = self.pos[0] - self.size[0] / 2.0
974
+ self.rightEdge = self.pos[0] + self.size[0] / 2.0
975
+ # Set horizontal position of elements
976
+ for item in self.items:
977
+ for element in [item['itemCtrl'], item['responseCtrl']]:
978
+ if element is None: # e.g. because this has no resp obj
979
+ continue
980
+ element.pos = [value[0], element.pos[1]]
981
+ element._baseY = value[1]
982
+ if hasattr(element, 'anchor'):
983
+ element.anchor = 'top-center'
984
+ # Calculate new position for everything on the y axis
985
+ self.scrollbar.pos = (self.rightEdge - .008, self.pos[1])
986
+ self._layoutY()
987
+
988
+ @property
989
+ def scrollbarWidth(self):
990
+ """
991
+ Width of the scrollbar for this Form, in the spatial units of this Form. Can also be set as a
992
+ `layout.Vector` object.
993
+ """
994
+ if not hasattr(self, "_scrollbarWidth"):
995
+ # Default to 15px
996
+ self._scrollbarWidth = layout.Vector(15, 'pix', self.win)
997
+ return getattr(self._scrollbarWidth, self.units)[0]
998
+
999
+ @scrollbarWidth.setter
1000
+ def scrollbarWidth(self, value):
1001
+ self._scrollbarWidth = layout.Vector(value, self.units, self.win)
1002
+ self.scrollbar.width[0] = self.scrollbarWidth
1003
+
1004
+ @property
1005
+ def opacity(self):
1006
+ return BaseVisualStim.opacity.fget(self)
1007
+
1008
+ @opacity.setter
1009
+ def opacity(self, value):
1010
+ BaseVisualStim.opacity.fset(self, value)
1011
+ self.fillColor = self._fillColor
1012
+ self.borderColor = self._borderColor
1013
+ if hasattr(self, "_foreColor"):
1014
+ self._foreColor.alpha = value
1015
+ if hasattr(self, "_itemColor"):
1016
+ self._itemColor.alpha = value
1017
+ if hasattr(self, "_responseColor"):
1018
+ self._responseColor.alpha = value
1019
+ if hasattr(self, "_markerColor"):
1020
+ self._markerColor.alpha = value
1021
+
1022
+ @property
1023
+ def complete(self):
1024
+ """A read-only property to determine if the current form is complete"""
1025
+ self.getData()
1026
+ return self._complete
1027
+
1028
+ @property
1029
+ def foreColor(self):
1030
+ """
1031
+ Sets both `itemColor` and `responseColor` to the same value
1032
+ """
1033
+ return ColorMixin.foreColor.fget(self)
1034
+
1035
+ @foreColor.setter
1036
+ def foreColor(self, value):
1037
+ ColorMixin.foreColor.fset(self, value)
1038
+ self.itemColor = value
1039
+ self.responseColor = value
1040
+
1041
+ @property
1042
+ def fillColor(self):
1043
+ """
1044
+ Color of the form's background
1045
+ """
1046
+ return ColorMixin.fillColor.fget(self)
1047
+
1048
+ @fillColor.setter
1049
+ def fillColor(self, value):
1050
+ ColorMixin.fillColor.fset(self, value)
1051
+ if hasattr(self, "border"):
1052
+ self.border.fillColor = value
1053
+
1054
+ @property
1055
+ def borderColor(self):
1056
+ """
1057
+ Color of the line around the form
1058
+ """
1059
+ return ColorMixin.borderColor.fget(self)
1060
+
1061
+ @borderColor.setter
1062
+ def borderColor(self, value):
1063
+ ColorMixin.borderColor.fset(self, value)
1064
+ if hasattr(self, "border"):
1065
+ self.border.borderColor = value
1066
+
1067
+ @property
1068
+ def itemColor(self):
1069
+ """
1070
+ Color of the text on form items
1071
+ """
1072
+ return getattr(self._itemColor, self.colorSpace)
1073
+
1074
+ @itemColor.setter
1075
+ def itemColor(self, value):
1076
+ self._itemColor = Color(value, self.colorSpace)
1077
+ # Set text color on each item
1078
+ for item in self.items:
1079
+ if 'itemCtrl' in item:
1080
+ if isinstance(item['itemCtrl'], psychopy.visual.TextBox2):
1081
+ item['itemCtrl'].foreColor = self._itemColor
1082
+
1083
+ @property
1084
+ def responseColor(self):
1085
+ """
1086
+ Color of the lines and text on form responses
1087
+ """
1088
+ if hasattr(self, "_responseColor"):
1089
+ return getattr(self._responseColor, self.colorSpace)
1090
+
1091
+ @responseColor.setter
1092
+ def responseColor(self, value):
1093
+ self._responseColor = Color(value, self.colorSpace)
1094
+ # Set line color on scrollbar
1095
+ if hasattr(self, "scrollbar"):
1096
+ self.scrollbar.borderColor = self._responseColor
1097
+ # Set line and label color on each item
1098
+ for item in self.items:
1099
+ if 'responseCtrl' in item:
1100
+ if isinstance(item['responseCtrl'], psychopy.visual.Slider) or isinstance(item['responseCtrl'], psychopy.visual.TextBox2):
1101
+ item['responseCtrl'].borderColor = self._responseColor
1102
+ item['responseCtrl'].foreColor = self._responseColor
1103
+
1104
+ @property
1105
+ def markerColor(self):
1106
+ """
1107
+ Color of the marker on any sliders in this form
1108
+ """
1109
+ if hasattr(self, "_markerColor"):
1110
+ return getattr(self._markerColor, self.colorSpace)
1111
+
1112
+ @markerColor.setter
1113
+ def markerColor(self, value):
1114
+ self._markerColor = Color(value, self.colorSpace)
1115
+ # Set marker color on scrollbar
1116
+ if hasattr(self, "scrollbar"):
1117
+ self.scrollbar.fillColor = self._markerColor
1118
+ # Set marker color on each item
1119
+ for item in self.items:
1120
+ if 'responseCtrl' in item:
1121
+ if isinstance(item['responseCtrl'], psychopy.visual.Slider):
1122
+ item['responseCtrl'].fillColor = self._markerColor
1123
+
1124
+ @property
1125
+ def style(self):
1126
+ if hasattr(self, "_style"):
1127
+ return self._style
1128
+
1129
+ @style.setter
1130
+ def style(self, style):
1131
+ """Sets some predefined styles or use these to create your own.
1132
+
1133
+ If you fancy creating and including your own styles that would be great!
1134
+
1135
+ Parameters
1136
+ ----------
1137
+ style: string
1138
+
1139
+ Known styles currently include:
1140
+
1141
+ 'light': black text on a light background
1142
+ 'dark': white text on a dark background
1143
+
1144
+ """
1145
+ self._style = style
1146
+ # If style is custom, skip the rest
1147
+ if style in ['custom...', 'None', None]:
1148
+ return
1149
+ # If style is a string of a known style, use that
1150
+ if style in self.knownStyles:
1151
+ style = self.knownStyles[style]
1152
+ # By here, style should be a dict
1153
+ if not isinstance(style, dict):
1154
+ return
1155
+ # Apply each key in the style dict as an attr
1156
+ for key, val in style.items():
1157
+ if hasattr(self, key):
1158
+ setattr(self, key, val)
1159
+
1160
+ @property
1161
+ def values(self):
1162
+ # Iterate through each control and append its value to a dict
1163
+ out = {}
1164
+ for item in self.getData():
1165
+ out.update(
1166
+ {item['index']: item['response']}
1167
+ )
1168
+ return out
1169
+
1170
+ @values.setter
1171
+ def values(self, values):
1172
+ for item in self.items:
1173
+ if item['index'] in values:
1174
+ ctrl = item['responseCtrl']
1175
+ # set response if available
1176
+ if hasattr(ctrl, "rating"):
1177
+ ctrl.rating = values[item['index']]
1178
+ elif hasattr(ctrl, "value"):
1179
+ ctrl.value = values[item['index']]
1180
+ else:
1181
+ ctrl.text = values[item['index']]