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
@@ -7,1418 +7,11 @@
7
7
  # Copyright (C) 2002-2018 Jonathan Peirce (C) 2019-2024 Open Science Tools Ltd.
8
8
  # Distributed under the terms of the GNU General Public License (GPL).
9
9
 
10
- import sys
11
- import numpy
12
-
13
- from psychopy import core, logging, event
14
- from psychopy.visual.circle import Circle
15
- from psychopy.visual.patch import PatchStim
16
- from psychopy.visual.shape import ShapeStim
17
- from psychopy.visual.text import TextStim
18
- from psychopy.visual.basevisual import MinimalStim
19
- from psychopy.visual.helpers import pointInPolygon, groupFlipVert
20
- from psychopy.tools.attributetools import logAttrib
21
- from psychopy.constants import FINISHED, STARTED, NOT_STARTED
22
-
23
-
24
- class RatingScale(MinimalStim):
25
- """A class for obtaining ratings, e.g., on a 1-to-7 or categorical scale.
26
- This is a lazy-imported class, therefore import using full path
27
- `from psychopy.visual.ratingscale import RatingScale` when inheriting
28
- from it.
29
-
30
- A RatingScale instance is a re-usable visual object having a ``draw()``
31
- method, with customizable appearance and response options. ``draw()``
32
- displays the rating scale, handles the subject's mouse or key responses,
33
- and updates the display. When the subject accepts a selection,
34
- ``.noResponse`` goes ``False`` (i.e., there is a response).
35
-
36
- You can call the ``getRating()`` method anytime to get a rating,
37
- ``getRT()`` to get the decision time, or ``getHistory()`` to obtain
38
- the entire set of (rating, RT) pairs.
39
-
40
- There are five main elements of a rating scale: the `scale`
41
- (text above the line intended to be a reminder of how to use the scale),
42
- the `line` (with tick marks), the `marker` (a moveable visual indicator
43
- on the line), the `labels` (text below the line that label specific
44
- points), and the `accept` button. The appearance and function of
45
- elements can be customized by the experimenter; it is not possible
46
- to orient a rating scale to be vertical. Multiple scales can be
47
- displayed at the same time, and continuous real-time ratings can be
48
- obtained from the history.
49
-
50
- The Builder RatingScale component gives a restricted set of options,
51
- but also allows full control over a RatingScale via the
52
- 'customize_everything' field.
53
-
54
- A RatingScale instance has no idea what else is on the screen.
55
- The experimenter has to draw the item to be rated, and handle `escape`
56
- to break or quit, if desired. The subject can use the mouse or keys to
57
- respond. Direction keys (left, right) will move the marker in the
58
- smallest available increment (e.g., 1/10th of a tick-mark if
59
- precision = 10).
60
-
61
- **Example 1**:
62
-
63
- A basic 7-point scale::
64
-
65
- ratingScale = visual.RatingScale(win)
66
- item = <statement, question, image, movie, ...>
67
- while ratingScale.noResponse:
68
- item.draw()
69
- ratingScale.draw()
70
- win.flip()
71
- rating = ratingScale.getRating()
72
- decisionTime = ratingScale.getRT()
73
- choiceHistory = ratingScale.getHistory()
74
-
75
- **Example 2**:
76
-
77
- For fMRI, sometimes only a keyboard can be used. If your response
78
- box sends keys 1-4, you could specify left, right, and accept keys,
79
- and not need a mouse::
80
-
81
- ratingScale = visual.RatingScale(
82
- win, low=1, high=5, markerStart=4,
83
- leftKeys='1', rightKeys = '2', acceptKeys='4')
84
-
85
- **Example 3**:
86
-
87
- Categorical ratings can be obtained using choices::
88
-
89
- ratingScale = visual.RatingScale(
90
- win, choices=['agree', 'disagree'],
91
- markerStart=0.5, singleClick=True)
92
-
93
- For other examples see Coder Demos -> stimuli -> ratingScale.py.
94
-
95
- :Authors:
96
- - 2010 Jeremy Gray: original code and on-going updates
97
- - 2012 Henrik Singmann: tickMarks, labels, ticksAboveLine
98
- - 2014 Jeremy Gray: multiple API changes (v1.80.00)
99
- """
100
-
101
- def __init__(self,
102
- win,
103
- scale='<default>',
104
- choices=None,
105
- low=1,
106
- high=7,
107
- precision=1,
108
- labels=(),
109
- tickMarks=None,
110
- tickHeight=1.0,
111
- marker='triangle',
112
- markerStart=None,
113
- markerColor=None,
114
- markerExpansion=1,
115
- singleClick=False,
116
- disappear=False,
117
- textSize=1.0,
118
- textColor='LightGray',
119
- textFont='Helvetica Bold',
120
- showValue=True,
121
- showAccept=True,
122
- acceptKeys='return',
123
- acceptPreText='key, click',
124
- acceptText='accept?',
125
- acceptSize=1.0,
126
- leftKeys='left',
127
- rightKeys='right',
128
- respKeys=(),
129
- lineColor='White',
130
- colorSpace='rgb',
131
- skipKeys='tab',
132
- mouseOnly=False,
133
- noMouse=False,
134
- size=1.0,
135
- stretch=1.0,
136
- pos=None,
137
- minTime=0.4,
138
- maxTime=0.0,
139
- flipVert=False,
140
- depth=0,
141
- name=None,
142
- autoLog=True,
143
- **kwargs): # catch obsolete args
144
- """
145
- :Parameters:
146
-
147
- win :
148
- A :class:`~psychopy.visual.Window` object (required).
149
- choices :
150
- A list of items which the subject can choose among.
151
- ``choices`` takes precedence over ``low``, ``high``,
152
- ``precision``, ``scale``, ``labels``, and ``tickMarks``.
153
- low :
154
- Lowest numeric rating (integer), default = 1.
155
- high :
156
- Highest numeric rating (integer), default = 7.
157
- precision :
158
- Portions of a tick to accept as input [1, 10, 60, 100];
159
- default = 1 (a whole tick).
160
- Pressing a key in `leftKeys` or `rightKeys` will move the
161
- marker by one portion of a tick. precision=60 is intended to
162
- support ratings of time-based quantities, with seconds being
163
- fractional minutes (or minutes being fractional hours).
164
- The display uses a colon (min:sec, or hours:min)
165
- to signal this to participants. The value returned by getRating()
166
- will be a proportion of a minute (e.g., 1:30 -> 1.5, or 59 seconds
167
- -> 59/60 = 0.98333). hours:min:sec is not supported.
168
- scale :
169
- Optional reminder message about how to respond or rate an item,
170
- displayed above the line; default =
171
- '<low>=not at all, <high>=extremely'.
172
- To suppress the scale, set ``scale=None``.
173
- labels :
174
- Text to be placed at specific tick marks to indicate their value.
175
- Can be just the ends (if given 2 labels), ends + middle
176
- (if given 3 labels),
177
- or all points (if given the same number of labels as points).
178
- tickMarks :
179
- List of positions at which tick marks should be placed from low
180
- to high.
181
- The default is to space tick marks equally, one per integer value.
182
- tickHeight :
183
- The vertical height of tick marks: 1.0 is the default height
184
- (above line), -1.0 is below the line, and 0.0 suppresses the
185
- display of tickmarks. ``tickHeight`` is purely cosmetic, and can
186
- be fractional, e.g., 1.2.
187
- marker :
188
- The moveable visual indicator of the current selection. The
189
- predefined styles are 'triangle', 'circle', 'glow', 'slider',
190
- and 'hover'. A slider moves smoothly when there are enough
191
- screen positions to move through, e.g., low=0, high=100.
192
- Hovering requires a set of choices, and allows clicking directly
193
- on individual choices; dwell-time is not recorded.
194
- Can also be set to a custom marker stimulus: any object with
195
- a .draw() method and .pos will work, e.g.,
196
- ``visual.TextStim(win, text='[]', units='norm')``.
197
- markerStart :
198
- The location or value to be pre-selected upon initial display,
199
- either numeric or one of the choices. Can be fractional,
200
- e.g., midway between two options.
201
- markerColor :
202
- Color to use for a predefined marker style, e.g., 'DarkRed'.
203
- markerExpansion :
204
- Only affects the `glow` marker: How much to expand or
205
- contract when moving rightward; 0=none, negative shrinks.
206
- singleClick :
207
- Enable a mouse click to both select and accept the rating,
208
- default = ``False``.
209
- A legal key press will also count as a singleClick.
210
- The 'accept' box is visible, but clicking it has no effect.
211
- pos : tuple (x, y)
212
- Position of the rating scale on the screen. The midpoint of
213
- the line will be positioned at ``(x, y)``;
214
- default = ``(0.0, -0.4)`` in norm units
215
- size :
216
- How much to expand or contract the overall rating scale display.
217
- Default size = 1.0. For larger than the default, set
218
- ``size`` > 1; for smaller, set < 1.
219
- stretch:
220
- Like ``size``, but only affects the horizontal direction.
221
- textSize :
222
- The size of text elements, relative to the default size
223
- (i.e., a scaling factor, not points).
224
- textColor :
225
- Color to use for labels and scale text; default = 'LightGray'.
226
- textFont :
227
- Name of the font to use; default = 'Helvetica Bold'.
228
- showValue :
229
- Show the subject their current selection default = ``True``.
230
- Ignored if singleClick is ``True``.
231
- showAccept :
232
- Show the button to click to accept the current value by using
233
- the mouse; default = ``True``.
234
- acceptPreText :
235
- The text to display before any value has been selected.
236
- acceptText :
237
- The text to display in the 'accept' button after a value has
238
- been selected.
239
- acceptSize :
240
- The width of the accept box relative to the default
241
- (e.g., 2 is twice as wide).
242
- acceptKeys :
243
- A list of keys that are used to accept the current response;
244
- default = 'return'.
245
- leftKeys :
246
- A list of keys that each mean "move leftwards";
247
- default = 'left'.
248
- rightKeys :
249
- A list of keys that each mean "move rightwards";
250
- default = 'right'.
251
- respKeys :
252
- A list of keys to use for selecting choices, in the desired order.
253
- The first item will be the left-most choice, the second
254
- item will be the next choice, and so on.
255
- skipKeys :
256
- List of keys the subject can use to skip a response,
257
- default = 'tab'.
258
- To require a response to every item, set ``skipKeys=None``.
259
- lineColor :
260
- The RGB color to use for the scale line, default = 'White'.
261
- mouseOnly :
262
- Require the subject to use the mouse (any keyboard input is
263
- ignored), default = ``False``. Can be used to avoid competing
264
- with other objects for keyboard input.
265
- noMouse:
266
- Require the subject to use keys to respond; disable and
267
- hide the mouse.
268
- `markerStart` will default to the left end.
269
- minTime :
270
- Seconds that must elapse before a response can be accepted,
271
- default = `0.4`.
272
- maxTime :
273
- Seconds after which a response cannot be accepted.
274
- If ``maxTime`` <= ``minTime``, there's no time limit.
275
- Default = `0.0` (no time limit).
276
- disappear :
277
- Whether the rating scale should vanish after a value is accepted.
278
- Can be useful when showing multiple scales.
279
- flipVert :
280
- Whether to mirror-reverse the rating scale in the vertical
281
- direction.
282
- """
283
- # what local vars are defined (these are the init params) for use by
284
- # __repr__
285
- self._initParams = dir()
286
- super(RatingScale, self).__init__(name=name, autoLog=False)
287
-
288
- # warn about obsolete arguments; Jan 2014, for v1.80:
289
- obsoleted = {'showScale', 'ticksAboveLine', 'displaySizeFactor',
290
- 'markerStyle', 'customMarker', 'allowSkip',
291
- 'stretchHoriz', 'escapeKeys', 'textSizeFactor',
292
- 'showScale', 'showAnchors',
293
- 'lowAnchorText', 'highAnchorText'}
294
- obsArgs = set(kwargs.keys()).intersection(obsoleted)
295
- if obsArgs:
296
- msg = ('RatingScale obsolete args: %s; see changelog v1.80.00'
297
- ' for notes on how to migrate')
298
- logging.error(msg % list(obsArgs))
299
- core.quit()
300
- # kwargs will absorb everything, including typos, so warn about bad
301
- # args
302
- unknownArgs = set(kwargs.keys()).difference(obsoleted)
303
- if unknownArgs:
304
- msg = "RatingScale unknown kwargs: %s"
305
- logging.error(msg % list(unknownArgs))
306
- core.quit()
307
-
308
- self.autoLog = False # needs to start off False
309
- self.win = win
310
- self.disappear = disappear
311
-
312
- # internally work in norm units, restore to orig units at the end of
313
- # __init__:
314
- self.savedWinUnits = self.win.units
315
- self.win.setUnits(u'norm', log=False)
316
- self.depth = depth
317
-
318
- # 'hover' style = like hyperlink with hover over choices:
319
- if marker == 'hover':
320
- showAccept = False
321
- singleClick = True
322
- textSize *= 1.5
323
- mouseOnly = True
324
- noMouse = False
325
-
326
- self.colorSpace = colorSpace
327
-
328
- # make things well-behaved if the requested value(s) would be trouble:
329
- self._initFirst(showAccept, mouseOnly, noMouse, singleClick,
330
- acceptKeys, marker, markerStart, low, high, precision,
331
- choices, scale, tickMarks, labels, tickHeight)
332
- self._initMisc(minTime, maxTime)
333
-
334
- # Set scale & position, key-bindings:
335
- self._initPosScale(pos, size, stretch)
336
- self._initKeys(self.acceptKeys, skipKeys,
337
- leftKeys, rightKeys, respKeys)
338
-
339
- # Construct the visual elements:
340
- self._initLine(tickMarkValues=tickMarks,
341
- lineColor=lineColor, marker=marker)
342
- self._initMarker(marker, markerColor, markerExpansion)
343
- self._initTextElements(win, self.scale, textColor, textFont, textSize,
344
- showValue, tickMarks)
345
- self._initAcceptBox(self.showAccept, acceptPreText, acceptText,
346
- acceptSize, self.markerColor, self.textSizeSmall,
347
- textSize, self.textFont)
348
-
349
- # List-ify the visual elements; self.marker is handled separately
350
- self.visualDisplayElements = []
351
- if self.showScale:
352
- self.visualDisplayElements += [self.scaleDescription]
353
- if self.showAccept:
354
- self.visualDisplayElements += [self.acceptBox, self.accept]
355
- if self.labels:
356
- for item in self.labels:
357
- if not item.text == '': # skip any empty placeholders
358
- self.visualDisplayElements.append(item)
359
- if marker != 'hover':
360
- self.visualDisplayElements += [self.line]
361
-
362
- # Mirror (flip) vertically if requested
363
- self.flipVert = False
364
- self.setFlipVert(flipVert)
365
-
366
- # Final touches:
367
- self.origScaleDescription = self.scaleDescription.text
368
- self.reset() # sets .status, among other things
369
- self.win.setUnits(self.savedWinUnits, log=False)
370
-
371
- self.timedOut = False
372
- self.beyondMinTime = False
373
-
374
- # set autoLog (now that params have been initialised)
375
- self.autoLog = autoLog
376
- if autoLog:
377
- logging.exp("Created %s = %s" % (self.name, repr(self)))
378
-
379
- def __repr__(self, complete=False):
380
- return self.__str__(complete=complete) # from MinimalStim
381
-
382
- def _initFirst(self, showAccept, mouseOnly, noMouse, singleClick,
383
- acceptKeys, marker, markerStart, low, high, precision,
384
- choices, scale, tickMarks, labels, tickHeight):
385
- """some sanity checking; various things are set, especially those
386
- that are used later; choices, anchors, markerStart settings are
387
- handled here
388
- """
389
- self.showAccept = bool(showAccept)
390
- self.mouseOnly = bool(mouseOnly)
391
- self.noMouse = bool(noMouse) and not self.mouseOnly # mouseOnly wins
392
- self.singleClick = bool(singleClick)
393
- self.acceptKeys = acceptKeys
394
- self.precision = precision
395
- self.labelTexts = None
396
- self.tickHeight = tickHeight
397
-
398
- if not self.showAccept:
399
- # the accept button is the mouse-based way to accept the current
400
- # response
401
- if len(list(self.acceptKeys)) == 0:
402
- # make sure there is in fact a way to respond using a
403
- # key-press:
404
- self.acceptKeys = ['return']
405
- if self.mouseOnly and not self.singleClick:
406
- # then there's no way to respond, so deny mouseOnly / enable
407
- # using keys:
408
- self.mouseOnly = False
409
- msg = ("RatingScale %s: ignoring mouseOnly (because "
410
- "showAccept and singleClick are False)")
411
- logging.warning(msg % self.name)
412
-
413
- # 'choices' is a list of non-numeric (unordered) alternatives:
414
- if choices and len(list(choices)) < 2:
415
- msg = "RatingScale %s: choices requires 2 or more items"
416
- logging.error(msg % self.name)
417
- if choices and len(list(choices)) >= 2:
418
- low = 0
419
- high = len(list(choices)) - 1
420
- self.precision = 1 # a fractional choice makes no sense
421
- self.choices = choices
422
- self.labelTexts = choices
423
- else:
424
- self.choices = False
425
- if marker == 'hover' and not self.choices:
426
- logging.error("RatingScale: marker='hover' requires "
427
- "a set of choices.")
428
- core.quit()
429
-
430
- # Anchors need to be well-behaved [do after choices]:
431
- try:
432
- self.low = int(low)
433
- except Exception:
434
- self.low = 1
435
- try:
436
- self.high = int(high)
437
- except Exception:
438
- self.high = self.low + 1
439
- if self.high <= self.low:
440
- self.high = self.low + 1
441
- self.precision = 100
442
-
443
- if not self.choices:
444
- diff = self.high - self.low
445
- if labels and len(labels) == 2:
446
- # label the endpoints
447
- first, last = labels[0], labels[-1]
448
- self.labelTexts = [first] + [''] * (diff - 1) + [last]
449
- elif labels and len(labels) == 3 and diff > 1 and (1 + diff) % 2:
450
- # label endpoints and middle tick
451
- placeHolder = [''] * ((diff - 2) // 2)
452
- self.labelTexts = ([labels[0]] + placeHolder +
453
- [labels[1]] + placeHolder +
454
- [labels[2]])
455
- elif labels in [None, False]:
456
- self.labelTexts = []
457
- else:
458
- first, last = str(self.low), str(self.high)
459
- self.labelTexts = [first] + [''] * (diff - 1) + [last]
460
-
461
- self.scale = scale
462
- if tickMarks and not labels is False:
463
- if labels is None:
464
- self.labelTexts = tickMarks
465
- else:
466
- self.labelTexts = labels
467
- if len(self.labelTexts) != len(tickMarks):
468
- msg = "RatingScale %s: len(labels) not equal to len(tickMarks)"
469
- logging.warning(msg % self.name)
470
- self.labelTexts = tickMarks
471
- if self.scale == "<default>":
472
- self.scale = False
473
-
474
- # Marker pre-positioned? [do after anchors]
475
- try:
476
- self.markerStart = float(markerStart)
477
- except Exception:
478
- if (isinstance(markerStart, str) and
479
- type(self.choices) == list and
480
- markerStart in self.choices):
481
- self.markerStart = self.choices.index(markerStart)
482
- self.markerPlacedAt = self.markerStart
483
- self.markerPlaced = True
484
- else:
485
- self.markerStart = None
486
- self.markerPlaced = False
487
- else: # float(markerStart) succeeded
488
- self.markerPlacedAt = self.markerStart
489
- self.markerPlaced = True
490
- # default markerStart = 0 if needed but otherwise unspecified:
491
- if self.noMouse and self.markerStart is None:
492
- self.markerPlacedAt = self.markerStart = 0
493
- self.markerPlaced = True
494
-
495
- def _initMisc(self, minTime, maxTime):
496
- # precision is the fractional parts of a tick mark to be sensitive to,
497
- # in [1,10,100]:
498
- if type(self.precision) != int or self.precision < 10:
499
- self.precision = 1
500
- self.fmtStr = "%.0f" # decimal places, purely for display
501
- elif self.precision == 60:
502
- self.fmtStr = "%d:%s" # minutes:seconds.zfill(2)
503
- elif self.precision < 100:
504
- self.precision = 10
505
- self.fmtStr = "%.1f"
506
- else:
507
- self.precision = 100
508
- self.fmtStr = "%.2f"
509
-
510
- self.clock = core.Clock() # for decision time
511
- try:
512
- self.minTime = float(minTime)
513
- except ValueError:
514
- self.minTime = 1.0
515
- self.minTime = max(self.minTime, 0.)
516
- try:
517
- self.maxTime = float(maxTime)
518
- except ValueError:
519
- self.maxTime = 0.0
520
- self.allowTimeOut = bool(self.minTime < self.maxTime)
521
-
522
- self.myMouse = event.Mouse(
523
- win=self.win, visible=bool(not self.noMouse))
524
- # Mouse-click-able 'accept' button pulsates (cycles its brightness
525
- # over frames):
526
- framesPerCycle = 100
527
- self.pulseColor = [0.6 + 0.22 * numpy.cos(i/15.65)
528
- for i in range(framesPerCycle)]
529
-
530
- def _initPosScale(self, pos, size, stretch, log=True):
531
- """position (x,y) and size (magnification) of the rating scale
532
- """
533
- # Screen position (translation) of the rating scale as a whole:
534
- if pos:
535
- if len(list(pos)) == 2:
536
- offsetHoriz, offsetVert = pos
537
- elif log and self.autoLog:
538
- msg = "RatingScale %s: pos expects a tuple (x,y)"
539
- logging.warning(msg % self.name)
540
- try:
541
- self.offsetHoriz = float(offsetHoriz)
542
- except Exception:
543
- if self.savedWinUnits == 'pix':
544
- self.offsetHoriz = 0
545
- else: # default x in norm units:
546
- self.offsetHoriz = 0.0
547
- try:
548
- self.offsetVert = float(offsetVert)
549
- except Exception:
550
- if self.savedWinUnits == 'pix':
551
- self.offsetVert = int(self.win.size[1]/-5.0)
552
- else: # default y in norm units:
553
- self.offsetVert = -0.4
554
- # pos=(x,y) will consider x,y to be in win units, but want norm
555
- # internally
556
- if self.savedWinUnits == 'pix':
557
- self.offsetHoriz = float(self.offsetHoriz) / self.win.size[0] / 0.5
558
- self.offsetVert = float(self.offsetVert) / self.win.size[1] / 0.5
559
- # just expose; not used elsewhere yet
560
- self.pos = [self.offsetHoriz, self.offsetVert]
561
-
562
- # Scale size (magnification) of the rating scale as a whole:
563
- try:
564
- self.stretch = float(stretch)
565
- except ValueError:
566
- self.stretch = 1.
567
- try:
568
- self.size = float(size) * 0.6
569
- except ValueError:
570
- self.size = 0.6
571
-
572
- def _initKeys(self, acceptKeys, skipKeys, leftKeys, rightKeys, respKeys):
573
- # keys for accepting the currently selected response:
574
- if self.mouseOnly:
575
- self.acceptKeys = [] # no valid keys, so must use mouse
576
- else:
577
- if type(acceptKeys) not in [list, tuple, set]:
578
- acceptKeys = [acceptKeys]
579
- self.acceptKeys = acceptKeys
580
- self.skipKeys = []
581
- if skipKeys and not self.mouseOnly:
582
- if type(skipKeys) not in [list, tuple, set]:
583
- skipKeys = [skipKeys]
584
- self.skipKeys = list(skipKeys)
585
- if type(leftKeys) not in [list, tuple, set]:
586
- leftKeys = [leftKeys]
587
- self.leftKeys = leftKeys
588
- if type(rightKeys) not in [list, tuple, set]:
589
- rightKeys = [rightKeys]
590
- self.rightKeys = rightKeys
591
-
592
- # allow responding via arbitrary keys if given as a param:
593
- nonRespKeys = (self.leftKeys + self.rightKeys + self.acceptKeys +
594
- self.skipKeys)
595
- if respKeys and hasattr(respKeys, '__iter__'):
596
- self.respKeys = respKeys
597
- self.enableRespKeys = True
598
- if set(self.respKeys).intersection(nonRespKeys):
599
- msg = 'RatingScale %s: respKeys may conflict with other keys'
600
- logging.warning(msg % self.name)
601
- else:
602
- # allow resp via numeric keys if the response range is in 0-9
603
- self.respKeys = []
604
- if not self.mouseOnly and self.low > -1 and self.high < 10:
605
- self.respKeys = [str(i)
606
- for i in range(self.low, self.high + 1)]
607
- # but if any digit is used as an action key, that should
608
- # take precedence so disable using numeric keys:
609
- if set(self.respKeys).intersection(nonRespKeys) == set([]):
610
- self.enableRespKeys = True
611
- else:
612
- self.enableRespKeys = False
613
- if self.enableRespKeys:
614
- self.tickFromKeyPress = {}
615
- for i, key in enumerate(self.respKeys):
616
- self.tickFromKeyPress[key] = i + self.low
617
-
618
- # if self.noMouse:
619
- # could check that there are appropriate response keys
620
-
621
- self.allKeys = nonRespKeys + self.respKeys
622
-
623
- def _initLine(self, tickMarkValues=None, lineColor='White', marker=None):
624
- """define a ShapeStim to be a graphical line, with tick marks.
625
-
626
- ### Notes (JRG Aug 2010)
627
- Conceptually, the response line is always -0.5 to +0.5
628
- ("internal" units). This line, of unit length, is scaled and
629
- translated for display. The line is effectively "center justified",
630
- expanding both left and right with scaling, with pos[] specifying
631
- the screen coordinate (in window units, norm or pix) of the
632
- mid-point of the response line. Tick marks are in integer units,
633
- internally 0 to (high-low), with 0 being the left end and (high-low)
634
- being the right end. (Subjects see low to high on the screen.)
635
- Non-numeric (categorical) choices are selected using tick-marks
636
- interpreted as an index, choice[tick]. Tick units get mapped to
637
- "internal" units based on their proportion of the total ticks
638
- (--> 0. to 1.). The unit-length internal line is expanded or
639
- contracted by stretch and size, and then is translated to
640
- position pos (offsetHoriz=pos[0], offsetVert=pos[1]).
641
- pos is the name of the arg, and its values appear in the code as
642
- offsetHoriz and offsetVert only for historical reasons (could be
643
- refactored for clarity).
644
-
645
- Auto-rescaling reduces the number of tick marks shown on the
646
- screen by a factor of 10, just for nicer appearance, without
647
- affecting the internal representation.
648
-
649
- Thus, the horizontal screen position of the i-th tick mark,
650
- where i in [0,n], for n total ticks (n = high-low),
651
- in screen units ('norm') will be:
652
- tick-i == offsetHoriz + (-0.5 + i/n ) * stretch * size
653
- So two special cases are:
654
- tick-0 (left end) == offsetHoriz - 0.5 * stretch * size
655
- tick-n (right end) == offsetHoriz + 0.5 * stretch * size
656
- The vertical screen position is just offsetVert (in screen norm units).
657
- To elaborate: tick-0 is the left-most tick, or "low anchor";
658
- here 0 is internal, the subject sees <low>.
659
- tick-n is the right-most tick, or "high anchor", or
660
- internal-tick-(high-low), and the subject sees <high>.
661
- Intermediate ticks, i, are located proportionally
662
- between -0.5 to + 0.5, based on their proportion
663
- of the total number of ticks, float(i)/n.
664
- The "proportion of total" is used because it's a line of unit length,
665
- i.e., the same length as used to internally represent the
666
- scale (-0.5 to +0.5).
667
- If precision > 1, the user / experimenter is asking for
668
- fractional ticks. These map correctly
669
- onto [0, 1] as well without requiring special handling
670
- (just do ensure float() ).
671
-
672
- Another note: -0.5 to +0.5 looked too big to be the default
673
- size of the rating line in screen norm units,
674
- so I set the internal size = 0.6 to compensate (i.e., making
675
- everything smaller). The user can adjust the scaling around
676
- the default by setting size, stretch, or both.
677
- This means that the user / experimenter can just think of > 1
678
- being expansion (and < 1 == contraction) relative to the default
679
- (internal) scaling, and not worry about the internal scaling.
680
-
681
- ### Notes (HS November 2012)
682
- To allow for labels at the ticks, the positions of the tick marks
683
- are saved in self.tickPositions. If tickMarks, those positions
684
- are used instead of the automatic positions.
685
- """
686
-
687
- self.lineColor = lineColor
688
- # vertical height of each tick, norm units; used for markers too:
689
- self.baseSize = 0.04
690
- # num tick marks to display, can get autorescaled
691
- self.tickMarks = float(self.high - self.low)
692
- self.autoRescaleFactor = 1
693
-
694
- if tickMarkValues:
695
- tickTmp = numpy.asarray(tickMarkValues, dtype=numpy.float32)
696
- tickMarkPositions = (tickTmp - self.low)/self.tickMarks
697
- else:
698
- # visually remap 10 ticks onto 1 tick in some conditions (=
699
- # cosmetic):
700
- if (self.low == 0 and
701
- self.tickMarks > 20 and
702
- int(self.tickMarks) % 10 == 0):
703
- self.autoRescaleFactor = 10
704
- self.tickMarks /= self.autoRescaleFactor
705
- tickMarkPositions = numpy.linspace(0, 1, int(self.tickMarks) + 1)
706
- self.scaledPrecision = float(self.precision * self.autoRescaleFactor)
707
-
708
- # how far a left or right key will move the marker, in tick units:
709
- self.keyIncrement = 1. / self.autoRescaleFactor / self.precision
710
- self.hStretchTotal = self.stretch * self.size
711
-
712
- # ends of the rating line, in norm units:
713
- self.lineLeftEnd = self.offsetHoriz - 0.5 * self.hStretchTotal
714
- self.lineRightEnd = self.offsetHoriz + 0.5 * self.hStretchTotal
715
-
716
- # space around the line within which to accept mouse input:
717
- # not needed if self.noMouse, but not a problem either
718
- pad = 0.06 * self.size
719
- if marker == 'hover':
720
- padText = ((1.0/(3 * (self.high - self.low))) *
721
- (self.lineRightEnd - self.lineLeftEnd))
722
- else:
723
- padText = 0
724
- self.nearLine = [
725
- [self.lineLeftEnd - pad - padText, -2 * pad + self.offsetVert],
726
- [self.lineLeftEnd - pad - padText, 2 * pad + self.offsetVert],
727
- [self.lineRightEnd + pad + padText, 2 * pad + self.offsetVert],
728
- [self.lineRightEnd + pad + padText, -2 * pad + self.offsetVert]]
729
-
730
- # vertices for ShapeStim:
731
- self.tickPositions = [] # list to hold horizontal positions
732
- vertices = [[self.lineLeftEnd, self.offsetVert]] # first vertex
733
- # vertical height of ticks (purely cosmetic):
734
- if self.tickHeight is False:
735
- self.tickHeight = -1. # backwards compatibility for boolean
736
- # numeric -> scale tick height; float(True) == 1.
737
- tickSize = self.baseSize * self.size * float(self.tickHeight)
738
- lineLength = self.lineRightEnd - self.lineLeftEnd
739
- for count, tick in enumerate(tickMarkPositions):
740
- horizTmp = self.lineLeftEnd + lineLength * tick
741
- vertices += [[horizTmp, self.offsetVert + tickSize],
742
- [horizTmp, self.offsetVert]]
743
- if count < len(tickMarkPositions) - 1:
744
- tickRelPos = lineLength * tickMarkPositions[count + 1]
745
- nextHorizTmp = self.lineLeftEnd + tickRelPos
746
- vertices.append([nextHorizTmp, self.offsetVert])
747
- self.tickPositions.append(horizTmp)
748
- vertices += [[self.lineRightEnd, self.offsetVert],
749
- [self.lineLeftEnd, self.offsetVert]]
750
-
751
- # create the line:
752
- self.line = ShapeStim(win=self.win, units='norm', vertices=vertices,
753
- lineWidth=4, lineColor=self.lineColor,
754
- name=self.name + '.line', autoLog=False)
755
-
756
- def _initMarker(self, marker, markerColor, expansion):
757
- """define a visual Stim to be used as the indicator.
758
-
759
- marker can be either a string, or a visual object (custom marker).
760
- """
761
- # preparatory stuff:
762
- self.markerOffsetVert = 0.
763
- if isinstance(marker, str):
764
- self.markerStyle = marker
765
- elif not hasattr(marker, 'draw'):
766
- logging.error("RatingScale: custom marker has no draw() method")
767
- self.markerStyle = 'triangle'
768
- else:
769
- self.markerStyle = 'custom'
770
- if hasattr(marker, 'pos'):
771
- self.markerOffsetVert = marker.pos[1]
772
- else:
773
- logging.error(
774
- "RatingScale: custom marker has no pos attribute")
775
-
776
- self.markerSize = 8. * self.size
777
- if isinstance(markerColor, str):
778
- markerColor = markerColor.replace(' ', '')
779
-
780
- # define or create self.marker:
781
- if self.markerStyle == 'hover':
782
- self.marker = TextStim(win=self.win, text=' ', units='norm',
783
- autoLog=False) # placeholder
784
- self.markerOffsetVert = .02
785
- if not markerColor:
786
- markerColor = 'darkorange'
787
- elif self.markerStyle == 'triangle':
788
- scaledTickSize = self.baseSize * self.size
789
- vert = [[-1 * scaledTickSize * 1.8, scaledTickSize * 3],
790
- [scaledTickSize * 1.8, scaledTickSize * 3], [0, -0.005]]
791
- if markerColor is None:
792
- markerColor = 'DarkBlue'
793
- self.marker = ShapeStim(win=self.win, units='norm', vertices=vert,
794
- lineWidth=0.1, lineColor=markerColor,
795
- fillColor=markerColor,
796
- name=self.name + '.markerTri',
797
- autoLog=False)
798
- elif self.markerStyle == 'slider':
799
- scaledTickSize = self.baseSize * self.size
800
- vert = [[-1 * scaledTickSize * 1.8, scaledTickSize],
801
- [scaledTickSize * 1.8, scaledTickSize],
802
- [scaledTickSize * 1.8, -1 * scaledTickSize],
803
- [-1 * scaledTickSize * 1.8, -1 * scaledTickSize]]
804
- if markerColor is None:
805
- markerColor = 'black'
806
- self.marker = ShapeStim(win=self.win, units='norm', vertices=vert,
807
- lineWidth=0.1, lineColor=markerColor,
808
- fillColor=markerColor,
809
- name=self.name + '.markerSlider',
810
- opacity=0.7, autoLog=False)
811
- elif self.markerStyle == 'glow':
812
- if markerColor is None:
813
- markerColor = 'White'
814
- self.marker = PatchStim(win=self.win, units='norm',
815
- tex=None, mask='gauss',
816
- color=markerColor, opacity=0.85,
817
- autoLog=False,
818
- name=self.name + '.markerGlow')
819
- self.markerBaseSize = self.baseSize * self.markerSize
820
- self.markerOffsetVert = .02
821
- self.markerExpansion = float(expansion) * 0.6
822
- if self.markerExpansion == 0:
823
- self.markerBaseSize *= self.markerSize * 0.7
824
- if self.markerSize > 1.2:
825
- self.markerBaseSize *= .7
826
- self.marker.setSize(self.markerBaseSize/2.0, log=False)
827
- elif self.markerStyle == 'custom':
828
- if markerColor is None:
829
- if hasattr(marker, 'color'):
830
- try:
831
- # marker.color 0 causes problems elsewhere too
832
- if not marker.color:
833
- marker.color = 'DarkBlue'
834
- except ValueError: # testing truth value of list
835
- marker.color = 'DarkBlue'
836
- elif hasattr(marker, 'fillColor'):
837
- marker.color = marker.fillColor
838
- else:
839
- marker.color = 'DarkBlue'
840
- markerColor = marker.color
841
- if not hasattr(marker, 'name') or not marker.name:
842
- marker.name = 'customMarker'
843
- self.marker = marker
844
- else: # 'circle':
845
- if markerColor is None:
846
- markerColor = 'DarkRed'
847
- x, y = self.win.size
848
- windowRatio = y/x
849
- self.markerSizeVert = 3.2 * self.baseSize * self.size
850
- circleSize = [self.markerSizeVert *
851
- windowRatio, self.markerSizeVert]
852
- self.markerOffsetVert = self.markerSizeVert/2.0
853
- self.marker = Circle(self.win, size=circleSize, units='norm',
854
- lineColor=markerColor, fillColor=markerColor,
855
- name=self.name + '.markerCir', autoLog=False)
856
- self.markerBaseSize = self.baseSize
857
- self.markerColor = markerColor
858
- self.markerYpos = self.offsetVert + self.markerOffsetVert
859
- # save initial state, restore on reset
860
- self.markerColorOriginal = markerColor
861
-
862
- def _initTextElements(self, win, scale, textColor,
863
- textFont, textSize, showValue, tickMarks):
864
- """creates TextStim for self.scaleDescription and self.labels
865
- """
866
- # text appearance (size, color, font, visibility):
867
- self.showValue = bool(showValue) # hide if False
868
- self.textColor = textColor # rgb
869
- self.textFont = textFont
870
- self.textSize = 0.2 * textSize * self.size
871
- self.textSizeSmall = self.textSize * 0.6
872
-
873
- # set the description text if not already set by user:
874
- if scale == '<default>':
875
- if self.choices:
876
- scale = ''
877
- else:
878
- msg = u' = not at all . . . extremely = '
879
- scale = str(self.low) + msg + str(self.high)
880
-
881
- # create the TextStim:
882
- self.scaleDescription = TextStim(
883
- win=self.win, height=self.textSizeSmall,
884
- pos=[self.offsetHoriz, 0.22 * self.size + self.offsetVert],
885
- color=self.textColor, wrapWidth=2 * self.hStretchTotal,
886
- font=textFont, autoLog=False)
887
- self.scaleDescription.font = textFont
888
- self.labels = []
889
- if self.labelTexts:
890
- if self.markerStyle == 'hover':
891
- vertPosTmp = self.offsetVert # on the line = clickable labels
892
- else:
893
- vertPosTmp = -2 * self.textSizeSmall * self.size + self.offsetVert
894
- for i, label in enumerate(self.labelTexts):
895
- # need all labels for tick position, i
896
- if label or label is not None: # 'is not None' allows creation of '0' (zero or false) labels
897
- txtStim = TextStim(
898
- win=self.win, text=str(label), font=textFont,
899
- pos=[self.tickPositions[i // self.autoRescaleFactor],
900
- vertPosTmp],
901
- height=self.textSizeSmall, color=self.textColor,
902
- autoLog=False)
903
- self.labels.append(txtStim)
904
- self.origScaleDescription = scale
905
- self.setDescription(scale) # do last
906
-
907
- def _setMarkerColor(self, color):
908
- """Set the fill color or color of the marker"""
909
- try:
910
- self.marker.setFillColor(color, colorSpace=self.colorSpace, log=False)
911
- except AttributeError:
912
- try:
913
- self.marker.setColor(color, colorSpace=self.colorSpace, log=False)
914
- except Exception:
915
- pass
916
-
917
- def setDescription(self, scale=None, log=True):
918
- """Method to set the brief description (scale).
919
-
920
- Useful when using the same RatingScale object to rate several
921
- dimensions. `setDescription(None)` will reset the description
922
- to its initial state. Set to a space character (' ') to make
923
- the description invisible.
924
- """
925
- if scale is None:
926
- scale = self.origScaleDescription
927
- self.scaleDescription.setText(scale)
928
- self.showScale = bool(scale) # not in [None, False, '']
929
- if log and self.autoLog:
930
- logging.exp('RatingScale %s: setDescription="%s"' %
931
- (self.name, self.scaleDescription.text))
932
-
933
- def _initAcceptBox(self, showAccept, acceptPreText, acceptText,
934
- acceptSize, markerColor,
935
- textSizeSmall, textSize, textFont):
936
- """creates a ShapeStim for self.acceptBox (mouse-click-able
937
- 'accept' button) and a TextStim for self.accept (container for
938
- the text shown inside the box)
939
- """
940
- if not showAccept: # no point creating things that won't be used
941
- return
942
-
943
- self.acceptLineColor = [-.2, -.2, -.2]
944
- self.acceptFillColor = [.2, .2, .2]
945
-
946
- if self.labelTexts:
947
- boxVert = [0.3, 0.47]
948
- else:
949
- boxVert = [0.2, 0.37]
950
-
951
- # define self.acceptBox:
952
- sizeFactor = self.size * textSize
953
- leftRightAdjust = 0.04 + 0.2 * max(0.1, acceptSize) * sizeFactor
954
- acceptBoxtop = self.offsetVert - boxVert[0] * sizeFactor
955
- self.acceptBoxtop = acceptBoxtop
956
- acceptBoxbot = self.offsetVert - boxVert[1] * sizeFactor
957
- self.acceptBoxbot = acceptBoxbot
958
- acceptBoxleft = self.offsetHoriz - leftRightAdjust
959
- self.acceptBoxleft = acceptBoxleft
960
- acceptBoxright = self.offsetHoriz + leftRightAdjust
961
- self.acceptBoxright = acceptBoxright
962
-
963
- # define a rectangle with rounded corners; for square corners, set
964
- # delta2 to 0
965
- delta = 0.025 * self.size
966
- delta2 = delta/7
967
- acceptBoxVertices = [
968
- [acceptBoxleft, acceptBoxtop - delta],
969
- [acceptBoxleft + delta2, acceptBoxtop - 3 * delta2],
970
- [acceptBoxleft + 3 * delta2, acceptBoxtop - delta2],
971
- [acceptBoxleft + delta, acceptBoxtop],
972
- [acceptBoxright - delta, acceptBoxtop],
973
- [acceptBoxright - 3 * delta2, acceptBoxtop - delta2],
974
- [acceptBoxright - delta2, acceptBoxtop - 3 * delta2],
975
- [acceptBoxright, acceptBoxtop - delta],
976
- [acceptBoxright, acceptBoxbot + delta],
977
- [acceptBoxright - delta2, acceptBoxbot + 3 * delta2],
978
- [acceptBoxright - 3 * delta2, acceptBoxbot + delta2],
979
- [acceptBoxright - delta, acceptBoxbot],
980
- [acceptBoxleft + delta, acceptBoxbot],
981
- [acceptBoxleft + 3 * delta2, acceptBoxbot + delta2],
982
- [acceptBoxleft + delta2, acceptBoxbot + 3 * delta2],
983
- [acceptBoxleft, acceptBoxbot + delta]]
984
- # interpolation looks bad on linux, as of Aug 2010
985
- interpolate = bool(not sys.platform.startswith('linux'))
986
- self.acceptBox = ShapeStim(
987
- win=self.win, vertices=acceptBoxVertices,
988
- fillColor=self.acceptFillColor, lineColor=self.acceptLineColor,
989
- interpolate=interpolate, autoLog=False)
990
-
991
- # text to display inside accept button before a marker is placed:
992
- if self.low > 0 and self.high < 10 and not self.mouseOnly:
993
- self.keyClick = 'key, click'
994
- else:
995
- self.keyClick = 'click line'
996
- if acceptPreText != 'key, click': # non-default
997
- self.keyClick = str(acceptPreText)
998
- self.acceptText = str(acceptText)
999
-
1000
- # create the TextStim:
1001
- self.accept = TextStim(
1002
- win=self.win, text=self.keyClick, font=self.textFont,
1003
- pos=[self.offsetHoriz, (acceptBoxtop + acceptBoxbot)/2.0],
1004
- italic=True, height=textSizeSmall, color=self.textColor,
1005
- autoLog=False)
1006
- self.accept.font = textFont
1007
-
1008
- self.acceptTextColor = markerColor
1009
- if isinstance(markerColor, str):
1010
- # warning raised if color not specified as a string
1011
- if markerColor in ['White']:
1012
- self.acceptTextColor = 'Black'
1013
-
1014
- def _getMarkerFromPos(self, mouseX):
1015
- """Convert mouseX into units of tick marks, 0 .. high-low.
1016
-
1017
- Will be fractional if precision > 1
1018
- """
1019
- value = min(max(mouseX, self.lineLeftEnd), self.lineRightEnd)
1020
- # map mouseX==0 -> mid-point of tick scale:
1021
- _tickStretch = self.tickMarks/self.hStretchTotal
1022
- adjValue = value - self.offsetHoriz
1023
- markerPos = adjValue * _tickStretch + self.tickMarks/2.0
1024
- # We need float value in getRating(), but round() returns
1025
- # numpy.float64 if argument is numpy.float64 in Python3.
1026
- # So we have to convert return value of round() to float.
1027
- rounded = float(round(markerPos * self.scaledPrecision))
1028
- return rounded/self.scaledPrecision
1029
-
1030
- def _getMarkerFromTick(self, tick):
1031
- """Convert a requested tick value into a position on internal scale.
1032
-
1033
- Accounts for non-zero low end, autoRescale, and precision.
1034
- """
1035
- # ensure its on the line:
1036
- value = max(min(self.high, tick), self.low)
1037
- # set requested precision:
1038
- value = round(value * self.scaledPrecision)//self.scaledPrecision
1039
- return (value - self.low) * self.autoRescaleFactor
1040
-
1041
- def setMarkerPos(self, tick):
1042
- """Method to allow the experimenter to set the marker's position
1043
- on the scale (in units of tick marks). This method can also set
1044
- the index within a list of choices (which start at 0).
1045
- No range checking is done.
1046
-
1047
- Assuming you have defined rs = RatingScale(...), you can specify
1048
- a tick position directly::
1049
-
1050
- rs.setMarkerPos(2)
1051
-
1052
- or do range checking, precision management, and auto-rescaling::
1053
-
1054
- rs.setMarkerPos(rs._getMarkerFromTick(2))
1055
-
1056
- To work from a screen coordinate, such as the X position of a
1057
- mouse click::
1058
-
1059
- rs.setMarkerPos(rs._getMarkerFromPos(mouseX))
1060
-
1061
- """
1062
- self.markerPlacedAt = tick
1063
- self.markerPlaced = True # only needed first time
1064
-
1065
- def setFlipVert(self, newVal=True, log=True):
1066
- """Sets current vertical mirroring to ``newVal``.
1067
- """
1068
- if self.flipVert != newVal:
1069
- self.flipVert = not self.flipVert
1070
- self.markerYpos *= -1
1071
- groupFlipVert([self.nearLine, self.marker] +
1072
- self.visualDisplayElements)
1073
- logAttrib(self, log, 'flipVert')
1074
-
1075
- # autoDraw and setAutoDraw are inherited from basevisual.MinimalStim
1076
-
1077
- def acceptResponse(self, triggeringAction, log=True):
1078
- """Commit and optionally log a response and the action.
1079
- """
1080
- self.noResponse = False
1081
- self.history.append((self.getRating(), self.getRT()))
1082
- if log and self.autoLog:
1083
- vals = (self.name, triggeringAction, str(self.getRating()))
1084
- logging.data('RatingScale %s: (%s) rating=%s' % vals)
1085
-
1086
- def setYPos(self, newPos = None):
1087
- """
1088
- This function can be called by the user to change the Y-positioning of the rating scale.
1089
- X location remains unchanged.
1090
- """
1091
- oldXPos, oldYPos = self.offsetHoriz, self.offsetVert
1092
- if not newPos is None:
1093
- if len(list(newPos)) == 2:
1094
- offsetHoriz, offsetVert = newPos
1095
- self.offsetHoriz = float(offsetHoriz)
1096
- self.offsetVert = float(offsetVert)
1097
- for positions in self.visualDisplayElements: # change location of elements based on position arg
1098
- if not positions.pos is None:
1099
- if 'ShapeStim' in str(type(positions)):
1100
- offsetY = abs(oldYPos - positions.pos[1])
1101
- positions.setPos([positions.pos[0], self.offsetVert + offsetY])
1102
- if '.line' in positions.name:# then change Y location of marker and mouse click box
1103
- self.markerYpos = self.offsetVert
1104
- self.nearLine[0][1],self.nearLine[3][1] = offsetVert-.072, offsetVert-.072
1105
- self.nearLine[1][1], self.nearLine[2][1] = offsetVert +.072, offsetVert + .072
1106
- if 'TextStim' in str(type(positions)):
1107
- offsetY = abs(oldYPos-positions.pos[1])
1108
- positions.setPos([positions.pos[0], self.offsetVert - offsetY])
1109
-
1110
-
1111
- def draw(self, log=True):
1112
- """Update the visual display, check for response (key, mouse, skip).
1113
-
1114
- Sets response flags: `self.noResponse`, `self.timedOut`.
1115
- `draw()` only draws the rating scale, not the item to be rated.
1116
- """
1117
- self.win.setUnits(u'norm', log=False) # get restored
1118
- if self.firstDraw:
1119
- self.firstDraw = False
1120
- self.clock.reset()
1121
- self.status = STARTED
1122
- if self.markerStart:
1123
- # has been converted in index if given as str
1124
- if (self.markerStart % 1 or self.markerStart < 0 or
1125
- self.markerStart > self.high or
1126
- self.choices is False):
1127
- first = self.markerStart
1128
- else:
1129
- # back to str for history
1130
- first = self.choices[int(self.markerStart)]
1131
- else:
1132
- first = None
1133
- self.history = [(first, 0.0)] # this will grow
1134
- self.beyondMinTime = False # has minTime elapsed?
1135
- self.timedOut = False
1136
-
1137
- if not self.beyondMinTime:
1138
- self.beyondMinTime = bool(self.clock.getTime() > self.minTime)
1139
- # beyond maxTime = timed out? max < min means never allow time-out
1140
- if (self.allowTimeOut and
1141
- not self.timedOut and
1142
- self.maxTime < self.clock.getTime()):
1143
- # only do this stuff once
1144
- self.timedOut = True
1145
- self.acceptResponse('timed out: %.3fs' % self.maxTime, log=log)
1146
-
1147
- # 'disappear' == draw nothing if subj is done:
1148
- if self.noResponse == False and self.disappear:
1149
- self.win.setUnits(self.savedWinUnits, log=False)
1150
- return
1151
-
1152
- # draw everything except the marker:
1153
- for visualElement in self.visualDisplayElements:
1154
- visualElement.draw()
1155
-
1156
- # draw a fixed marker if the scale is being drawn after a response:
1157
- if self.noResponse == False:
1158
- # fix the marker position on the line
1159
- if not self.markerPosFixed:
1160
- self._setMarkerColor('DarkGray')
1161
-
1162
- # drop it onto the line
1163
- self.marker.setPos((0, -.012), ('+', '-')[self.flipVert],
1164
- log=False)
1165
- self.markerPosFixed = True # flag to park it there
1166
- self.marker.draw()
1167
- if self.showAccept:
1168
- self.acceptBox.draw() # hides the text
1169
- self.win.setUnits(self.savedWinUnits, log=False)
1170
- return # makes the marker unresponsive
1171
-
1172
- if self.noMouse:
1173
- mouseNearLine = False
1174
- else:
1175
- mouseX, mouseY = self.myMouse.getPos() # norm units
1176
- mouseNearLine = pointInPolygon(mouseX, mouseY, self.nearLine)
1177
-
1178
- # draw a dynamic marker:
1179
- if self.markerPlaced or self.singleClick:
1180
- # update position:
1181
- if self.singleClick and mouseNearLine:
1182
- self.setMarkerPos(self._getMarkerFromPos(mouseX))
1183
- proportion = self.markerPlacedAt/self.tickMarks
1184
- # expansion for 'glow', based on proportion of total line
1185
- if self.markerStyle == 'glow' and self.markerExpansion != 0:
1186
- if self.markerExpansion > 0:
1187
- newSize = 0.1 * self.markerExpansion * proportion
1188
- newOpacity = 0.2 + proportion
1189
- else: # self.markerExpansion < 0:
1190
- newSize = - 0.1 * self.markerExpansion * (1 - proportion)
1191
- newOpacity = 1.2 - proportion
1192
- self.marker.setSize(self.markerBaseSize + newSize, log=False)
1193
- self.marker.setOpacity(min(1, max(0, newOpacity)), log=False)
1194
- # set the marker's screen position based on tick (==
1195
- # markerPlacedAt)
1196
- if self.markerPlacedAt is not False:
1197
- x = self.offsetHoriz + self.hStretchTotal * (-0.5 + proportion)
1198
- self.marker.setPos((x, self.markerYpos), log=False)
1199
- self.marker.draw()
1200
- if self.showAccept and self.markerPlacedBySubject:
1201
- self.frame = (self.frame + 1) % 100
1202
- self.acceptBox.setFillColor(
1203
- self.pulseColor[self.frame], colorSpace=self.colorSpace, log=False)
1204
- self.acceptBox.setLineColor(
1205
- self.pulseColor[self.frame], colorSpace=self.colorSpace, log=False)
1206
- self.accept.setColor(self.acceptTextColor, colorSpace=self.colorSpace, log=False)
1207
- if self.showValue and self.markerPlacedAt is not False:
1208
- if self.choices:
1209
- val = str(self.choices[int(self.markerPlacedAt)])
1210
- elif self.precision == 60:
1211
- valTmp = self.markerPlacedAt + self.low
1212
- minutes = int(valTmp) # also works for hours:minutes
1213
- seconds = int(60. * (valTmp - minutes))
1214
- val = self.fmtStr % (minutes, str(seconds).zfill(2))
1215
- else:
1216
- valTmp = self.markerPlacedAt + self.low
1217
- val = self.fmtStr % (valTmp * self.autoRescaleFactor)
1218
- self.accept.setText(val)
1219
- elif self.markerPlacedAt is not False:
1220
- self.accept.setText(self.acceptText)
1221
-
1222
- # handle key responses:
1223
- if not self.mouseOnly:
1224
- for key in event.getKeys(self.allKeys):
1225
- if key in self.skipKeys:
1226
- self.markerPlacedAt = None
1227
- self.noResponse = False
1228
- self.history.append((None, self.getRT()))
1229
- elif key in self.respKeys and self.enableRespKeys:
1230
- # place the marker at the corresponding tick (from key)
1231
- self.markerPlaced = True
1232
- self.markerPlacedBySubject = True
1233
- resp = self.tickFromKeyPress[key]
1234
- self.markerPlacedAt = self._getMarkerFromTick(resp)
1235
- proportion = self.markerPlacedAt/self.tickMarks
1236
- self.marker.setPos(
1237
- [self.size * (-0.5 + proportion), 0], log=False)
1238
- if self.markerPlaced and self.beyondMinTime:
1239
- # placed by experimenter (as markerStart) or by subject
1240
- if (self.markerPlacedBySubject or
1241
- self.markerStart is None or
1242
- not self.markerStart % self.keyIncrement):
1243
- # inefficient to do every frame...
1244
- leftIncr = rightIncr = self.keyIncrement
1245
- else:
1246
- # markerStart is fractional; arrow keys move to next
1247
- # location
1248
- leftIncr = self.markerStart % self.keyIncrement
1249
- rightIncr = self.keyIncrement - leftIncr
1250
- if key in self.leftKeys:
1251
- self.markerPlacedAt = self.markerPlacedAt - leftIncr
1252
- self.markerPlacedBySubject = True
1253
- elif key in self.rightKeys:
1254
- self.markerPlacedAt = self.markerPlacedAt + rightIncr
1255
- self.markerPlacedBySubject = True
1256
- elif key in self.acceptKeys:
1257
- self.acceptResponse('key response', log=log)
1258
- # off the end?
1259
- self.markerPlacedAt = max(0, self.markerPlacedAt)
1260
- self.markerPlacedAt = min(
1261
- self.tickMarks, self.markerPlacedAt)
1262
-
1263
- if (self.markerPlacedBySubject and self.singleClick
1264
- and self.beyondMinTime):
1265
- self.marker.setPos((0, self.offsetVert), '+', log=False)
1266
- self.acceptResponse('key single-click', log=log)
1267
-
1268
- # handle mouse left-click:
1269
- if not self.noMouse and self.myMouse.getPressed()[0]:
1270
- # mouseX, mouseY = self.myMouse.getPos() # done above
1271
- # if click near the line, place the marker there:
1272
- if mouseNearLine:
1273
- self.markerPlaced = True
1274
- self.markerPlacedBySubject = True
1275
- self.markerPlacedAt = self._getMarkerFromPos(mouseX)
1276
- if self.singleClick and self.beyondMinTime:
1277
- self.acceptResponse('mouse single-click', log=log)
1278
- # if click in accept box and conditions are met, accept the
1279
- # response:
1280
- elif (self.showAccept and
1281
- self.markerPlaced and
1282
- self.beyondMinTime and
1283
- self.acceptBox.contains(mouseX, mouseY)):
1284
- self.acceptResponse('mouse response', log=log)
1285
-
1286
- if self.markerStyle == 'hover' and self.markerPlaced:
1287
- # 'hover' --> noMouse = False during init
1288
- if (mouseNearLine or
1289
- self.markerPlacedAt != self.markerPlacedAtLast):
1290
- if hasattr(self, 'targetWord'):
1291
- self.targetWord.setColor(self.textColor, colorSpace=self.colorSpace, log=False)
1292
- # self.targetWord.setHeight(self.textSizeSmall, log=False)
1293
- # # avoid TextStim memory leak
1294
- self.targetWord = self.labels[int(self.markerPlacedAt)]
1295
- self.targetWord.setColor(self.markerColor, colorSpace=self.colorSpace, log=False)
1296
- # skip size change to reduce mem leakage from pyglet text
1297
- # self.targetWord.setHeight(1.05*self.textSizeSmall,log=False)
1298
- self.markerPlacedAtLast = self.markerPlacedAt
1299
- elif not mouseNearLine and self.wasNearLine:
1300
- self.targetWord.setColor(self.textColor, colorSpace=self.colorSpace, log=False)
1301
- # self.targetWord.setHeight(self.textSizeSmall, log=False)
1302
- self.wasNearLine = mouseNearLine
1303
-
1304
- # decision time = sec from first .draw() to when first 'accept' value:
1305
- if not self.noResponse and self.decisionTime == 0:
1306
- self.decisionTime = self.clock.getTime()
1307
- if log and self.autoLog:
1308
- logging.data('RatingScale %s: rating RT=%.3f' %
1309
- (self.name, self.decisionTime))
1310
- logging.data('RatingScale %s: history=%s' %
1311
- (self.name, self.getHistory()))
1312
- # minimum time is enforced during key and mouse handling
1313
- self.status = FINISHED
1314
- if self.showAccept:
1315
- self.acceptBox.setFillColor(self.acceptFillColor, colorSpace=self.colorSpace, log=False)
1316
- self.acceptBox.setLineColor(self.acceptLineColor, colorSpace=self.colorSpace, log=False)
1317
- else:
1318
- # build up response history if no decision or skip yet:
1319
- tmpRating = self.getRating()
1320
- if (self.history[-1][0] != tmpRating and
1321
- self.markerPlacedBySubject):
1322
- self.history.append((tmpRating, self.getRT())) # tuple
1323
-
1324
- # restore user's units:
1325
- self.win.setUnits(self.savedWinUnits, log=False)
1326
-
1327
- def reset(self, log=True):
1328
- """Restores the rating-scale to its post-creation state.
1329
-
1330
- The history is cleared, and the status is set to NOT_STARTED. Does
1331
- not restore the scale text description (such reset is needed between
1332
- items when rating multiple items)
1333
- """
1334
- # only resets things that are likely to have changed when the
1335
- # ratingScale instance is used by a subject
1336
- # reset label color if using hover
1337
- if self.markerStyle == 'hover':
1338
- for labels in self.labels:
1339
- labels.setColor(self.textColor, colorSpace=self.colorSpace, log=False)
1340
- self.noResponse = True
1341
- # restore in case it turned gray, etc
1342
- self.markerColor = self.markerColorOriginal
1343
- self._setMarkerColor(self.markerColor)
1344
- # placed by subject or markerStart: show on screen
1345
- self.markerPlaced = False
1346
- # placed by subject is actionable: show value, singleClick
1347
- self.markerPlacedBySubject = False
1348
- self.markerPlacedAt = False
1349
- # NB markerStart could be 0; during __init__, its forced to be numeric
1350
- # and valid, or None (not boolean)
1351
- if self.markerStart != None:
1352
- self.markerPlaced = True
1353
- # __init__ assures this is valid:
1354
- self.markerPlacedAt = self.markerStart - self.low
1355
- self.markerPlacedAtLast = -1 # unplaced
1356
- self.wasNearLine = False
1357
- self.firstDraw = True # -> self.clock.reset() at start of draw()
1358
- self.decisionTime = 0
1359
- self.markerPosFixed = False
1360
- self.frame = 0 # a counter used only to 'pulse' the 'accept' box
1361
-
1362
- if self.showAccept:
1363
- self.acceptBox.setFillColor(self.acceptFillColor, colorSpace=self.colorSpace, log=False)
1364
- self.acceptBox.setLineColor(self.acceptLineColor, colorSpace=self.colorSpace, log=False)
1365
- self.accept.setColor('#444444', colorSpace='hex', log=False) # greyed out
1366
- self.accept.setText(self.keyClick, log=False)
1367
- if log and self.autoLog:
1368
- logging.exp('RatingScale %s: reset()' % self.name)
1369
- self.status = NOT_STARTED
1370
- self.history = None
1371
-
1372
- def getRating(self):
1373
- """Returns the final, accepted rating, or the current value.
1374
-
1375
- The rating is None if the subject skipped this item, took longer
1376
- than ``maxTime``, or no rating is
1377
- available yet. Returns the currently indicated rating even if it has
1378
- not been accepted yet (and so might change until accept is pressed).
1379
- The first rating in the list will have the value of
1380
- markerStart (whether None, a numeric value, or a choice value).
1381
- """
1382
- if self.noResponse and self.status == FINISHED:
1383
- return None
1384
- if not type(self.markerPlacedAt) in [float, int]:
1385
- return None # eg, if skipped a response
1386
-
1387
- # set type for the response, based on what was wanted
1388
- val = self.markerPlacedAt * self.autoRescaleFactor
1389
- if self.precision == 1:
1390
- response = int(val) + self.low
1391
- else:
1392
- response = float(val) + self.low
1393
- if self.choices:
1394
- try:
1395
- response = self.choices[response]
1396
- except Exception:
1397
- pass
1398
- # == we have a numeric fractional choice from markerStart and
1399
- # want to save the numeric value as first item in the history
1400
- return response
1401
-
1402
- def getRT(self):
1403
- """Returns the seconds taken to make the rating (or to indicate skip).
1404
-
1405
- Returns None if no rating available, or maxTime if the response
1406
- timed out. Returns the time elapsed so far if no rating has been
1407
- accepted yet (e.g., for continuous usage).
1408
- """
1409
- if self.status != FINISHED:
1410
- return round(self.clock.getTime(), 3)
1411
- if self.noResponse:
1412
- if self.timedOut:
1413
- return round(self.maxTime, 3)
1414
- return None
1415
- return round(self.decisionTime, 3)
1416
-
1417
- def getHistory(self):
1418
- """Return a list of the subject's history as (rating, time) tuples.
1419
-
1420
- The history can be retrieved at any time, allowing for continuous
1421
- ratings to be obtained in real-time. Both numerical and categorical
1422
- choices are stored automatically in the history.
1423
- """
1424
- return self.history
10
+ from psychopy.tools.pkgtools import PluginStub
11
+
12
+ class RatingScale(
13
+ PluginStub,
14
+ plugin="psychopy-legacy",
15
+ doclink="https://psychopy.github.io/psychopy-legacy/coder/visual/RatingScale"
16
+ ):
17
+ pass