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.
- psychopy/.DS_Store +0 -0
- psychopy/GIT_SHA +1 -1
- psychopy/VERSION +1 -1
- psychopy/__init__.py +10 -1
- psychopy/__init__.py.orig +65 -0
- psychopy/app/{locale/ar_001/.DS_Store → .DS_Store} +0 -0
- psychopy/app/Resources/.DS_Store +0 -0
- psychopy/app/_psychopyApp.py +11 -3
- psychopy/app/appData.spec +1 -1
- psychopy/app/builder/builder.py +1 -1
- psychopy/app/builder/builder.py.orig +3932 -0
- psychopy/app/builder/dialogs/__init__.py.orig +1679 -0
- psychopy/app/builder/dialogs/paramCtrls.py +1 -1
- psychopy/app/builder/dialogs/paramCtrls.py.orig +713 -0
- psychopy/app/colorpicker/__init__.py.orig +411 -0
- psychopy/app/cortex.log +0 -0
- psychopy/app/jobs.py +8 -1
- psychopy/app/locale/ar_001/LC_MESSAGE/messages.po +2452 -1731
- psychopy/app/locale/zh_CN/LC_MESSAGE/zh_CN.mo +0 -0
- psychopy/app/locale/zh_CN/LC_MESSAGE/zh_CN.po +6127 -0
- psychopy/app/locale/zh_CN/LC_MESSAGE/zh_CN_allFlagged.mo +0 -0
- psychopy/app/locale/zh_CN/LC_MESSAGE/zh_CN_allFlagged.po +7366 -0
- psychopy/app/plugin_manager/dialog.py +9 -7
- psychopy/app/ribbon.py +2 -1
- psychopy/app/runner/runner.py +7 -5
- psychopy/clock.py +8 -4
- psychopy/core.py.orig +169 -0
- psychopy/demos/builder/Design Templates/randomisedBlocks/html/index.html +23 -0
- psychopy/demos/builder/Design Templates/randomisedBlocks/html/randomisedBlocks-legacy-browsers.js +423 -0
- psychopy/demos/builder/Design Templates/randomisedBlocks/html/randomisedBlocks.js +427 -0
- psychopy/demos/builder/Design Templates/randomisedBlocks/html/resources/chooseBlock.xlsx +0 -0
- psychopy/demos/builder/Design Templates/randomisedBlocks/html/resources/facesBlock.xlsx +0 -0
- psychopy/demos/builder/Design Templates/randomisedBlocks/html/resources/housesBlock.xlsx +0 -0
- psychopy/demos/builder/Design Templates/randomisedBlocks/html/resources/stims/face01.jpg +0 -0
- psychopy/demos/builder/Design Templates/randomisedBlocks/html/resources/stims/face02.jpg +0 -0
- psychopy/demos/builder/Design Templates/randomisedBlocks/html/resources/stims/face03.jpg +0 -0
- psychopy/demos/builder/Design Templates/randomisedBlocks/html/resources/stims/house01.jpg +0 -0
- psychopy/demos/builder/Design Templates/randomisedBlocks/html/resources/stims/house02.jpg +0 -0
- psychopy/demos/builder/Design Templates/randomisedBlocks/html/resources/stims/house03.jpg +0 -0
- psychopy/demos/builder/Design Templates/randomisedBlocks/randomisedBlocks.py +330 -0
- psychopy/demos/builder/Design Templates/randomisedBlocks/randomisedBlocks_lastrun.py +330 -0
- psychopy/demos/builder/Feature Demos/eyetracking/eyetracking.xml +298 -0
- psychopy/demos/builder/Feature Demos/eyetracking/eyetracking.xsd +120 -0
- psychopy/demos/builder/Tools/.DS_Store +0 -0
- psychopy/demos/builder/Tools/gammaCalibration/.DS_Store +0 -0
- psychopy/demos/builder/Tools/gammaCalibration/data/_gamma_correction_visual_2022-05-18_14h18.29.439.csv +38 -0
- psychopy/demos/builder/Tools/gammaCalibration/data/_gamma_correction_visual_2022-05-18_14h18.29.439.log +3418 -0
- psychopy/demos/builder/Tools/gammaCalibration/data/_gamma_correction_visual_2022-05-18_14h18.29.439.psydat +0 -0
- psychopy/demos/builder/Tools/gammaCalibration/data/x1_gamma_correction_visual_2022-05-17_13h59.42.928.csv +2 -0
- psychopy/demos/builder/Tools/gammaCalibration/data/x1_gamma_correction_visual_2022-05-17_13h59.42.928.log +15 -0
- psychopy/demos/builder/Tools/gammaCalibration/data/x1_gamma_correction_visual_2022-05-17_13h59.42.928.psydat +0 -0
- psychopy/demos/builder/Tools/gammaCalibration/gamma_correction_visual.psyexp +323 -0
- psychopy/demos/builder/Tools/gammaCalibration/gamma_correction_visual.py +562 -0
- psychopy/demos/builder/Tools/gammaCalibration/gamma_correction_visual_lastrun.py +562 -0
- psychopy/demos/builder/Tools/gammaCalibration/questStairs.xlsx +0 -0
- psychopy/demos/builder/Tools/gammaCalibration/readme.md +0 -0
- psychopy/demos/builder/Tools/gammaCalibration/resources/low_contrast.png +0 -0
- psychopy/demos/builder/Tools/gammaCalibration/resources/make_2nd_order_tex.py +59 -0
- psychopy/demos/builder/Tools/gammaCalibration/resources/second_order_tex.png +0 -0
- psychopy/demos/coder/.DS_Store +0 -0
- psychopy/demos/coder/experiment control/info_gamma.pickle +0 -0
- psychopy/demos/coder/iohub/.iohpid +1 -0
- psychopy/demos/coder/iohub/eyetracking/.iohpid +1 -0
- psychopy/demos/coder/iohub/wintab/.DS_Store +0 -0
- psychopy/demos/coder/stimuli/.DS_Store +0 -0
- psychopy/demos/coder/stimuli/radialGratingContracting.py +29 -0
- psychopy/experiment/_experiment.py.orig +1032 -0
- psychopy/experiment/components/.DS_Store +0 -0
- psychopy/experiment/components/_base.py +13 -4
- psychopy/experiment/components/_base.py.orig +823 -0
- psychopy/experiment/components/form/.DS_Store +0 -0
- psychopy/experiment/components/microphone/__init__.py +10 -1
- psychopy/experiment/components/microphone/__init__.py.orig +490 -0
- psychopy/experiment/components/polygon/__init__.py +21 -22
- psychopy/experiment/components/settings/__init__.py +13 -14
- psychopy/experiment/components/settings/__init__.py.orig +1337 -0
- psychopy/experiment/components/textbox/__init__.py.orig +310 -0
- psychopy/experiment/components/webcam/.DS_Store +0 -0
- psychopy/experiment/components/webcam/light/.DS_Store +0 -0
- psychopy/experiment/flow.py +10 -8
- psychopy/experiment/loops.py.orig +829 -0
- psychopy/experiment/params.py +8 -3
- psychopy/experiment/params.py.orig +408 -0
- psychopy/experiment/routine.py.orig +503 -0
- psychopy/experiment/routines/_base.py +15 -6
- psychopy/experiment/routines/counterbalance/__init__.py +1 -0
- psychopy/gui/qtgui.py +14 -7
- psychopy/gui/util.py +10 -14
- psychopy/gui/wxgui.py +10 -4
- psychopy/hardware/.DS_Store +0 -0
- psychopy/hardware/brainproducts.py.orig +680 -0
- psychopy/hardware/iolab.py.orig +238 -0
- psychopy/hardware/manager.py +1 -1
- psychopy/hardware/photodiode.py +59 -27
- psychopy/hardware/serialport.py +51 -0
- psychopy/hardware/speaker.py +4 -4
- psychopy/iohub/datastore/__init__.py.orig +443 -0
- psychopy/iohub/datastore/util.py.orig +692 -0
- psychopy/iohub/devices/mouse/darwin.py.orig +427 -0
- psychopy/iohub/devices/mouse/linux2.py.orig +198 -0
- psychopy/preferences/.DS_Store +0 -0
- psychopy/projects/pavlovia.py +10 -3
- psychopy/projects/pavlovia.py.orig +1295 -0
- psychopy/sound/backend_ptb.py +22 -5
- psychopy/sound/transcribe.py +24 -4
- psychopy/tests/.DS_Store +0 -0
- psychopy/tests/data/.DS_Store +0 -0
- psychopy/tests/data/TestCircle_fill_local.png +0 -0
- psychopy/tests/data/__test.png +0 -0
- psychopy/tests/data/aperture1_normHexbackground_local.png +0 -0
- psychopy/tests/data/aperture1_norm_local.png +0 -0
- psychopy/tests/data/aperture2_normHexbackground_local.png +0 -0
- psychopy/tests/data/beatandrcos_height_local.png +0 -0
- psychopy/tests/data/beatandrcos_normAddBlend_local.png +0 -0
- psychopy/tests/data/beatandrcos_normHexbackground_local.png +0 -0
- psychopy/tests/data/beatandrcos_norm_local.png +0 -0
- psychopy/tests/data/beatandrcos_stencil_local.png +0 -0
- psychopy/tests/data/blend_add_height_local.png +0 -0
- psychopy/tests/data/blend_add_normAddBlend_local.png +0 -0
- psychopy/tests/data/blend_add_normHexbackground_local.png +0 -0
- psychopy/tests/data/blend_add_normNoShade_local.png +0 -0
- psychopy/tests/data/blend_add_norm_local.png +0 -0
- psychopy/tests/data/blend_add_stencil_local.png +0 -0
- psychopy/tests/data/bufferimg_gabor_height_local.png +0 -0
- psychopy/tests/data/bufferimg_gabor_normAddBlend_local.png +0 -0
- psychopy/tests/data/bufferimg_gabor_normHexbackground_local.png +0 -0
- psychopy/tests/data/bufferimg_gabor_normNoShade_local.png +0 -0
- psychopy/tests/data/bufferimg_gabor_norm_local.png +0 -0
- psychopy/tests/data/bufferimg_gabor_stencil_local.png +0 -0
- psychopy/tests/data/circleHex_height_local.png +0 -0
- psychopy/tests/data/circleHex_normAddBlend_local.png +0 -0
- psychopy/tests/data/circleHex_normHexbackground_local.png +0 -0
- psychopy/tests/data/circleHex_normNoShade_local.png +0 -0
- psychopy/tests/data/circleHex_norm_local.png +0 -0
- psychopy/tests/data/circleHex_stencil_local.png +0 -0
- psychopy/tests/data/color_comparison_local.png +0 -0
- psychopy/tests/data/corrFullRandom_local.csv +16 -0
- psychopy/tests/data/corrFullRandom_local.tsv +6 -0
- psychopy/tests/data/correctScript/.DS_Store +0 -0
- psychopy/tests/data/dots_height_local.png +0 -0
- psychopy/tests/data/dots_normAddBlend_local.png +0 -0
- psychopy/tests/data/dots_normHexbackground_local.png +0 -0
- psychopy/tests/data/dots_normNoShade_local.png +0 -0
- psychopy/tests/data/dots_norm_local.png +0 -0
- psychopy/tests/data/dots_stencil_local.png +0 -0
- psychopy/tests/data/elarray1_height_local.png +0 -0
- psychopy/tests/data/elarray1_normAddBlend_local.png +0 -0
- psychopy/tests/data/elarray1_normHexbackground_local.png +0 -0
- psychopy/tests/data/elarray1_norm_local.png +0 -0
- psychopy/tests/data/elarray1_stencil_local.png +0 -0
- psychopy/tests/data/envelopeandrcos_height_local.png +0 -0
- psychopy/tests/data/envelopeandrcos_normAddBlend_local.png +0 -0
- psychopy/tests/data/envelopeandrcos_normHexbackground_local.png +0 -0
- psychopy/tests/data/envelopeandrcos_norm_local.png +0 -0
- psychopy/tests/data/envelopeandrcos_stencil_local.png +0 -0
- psychopy/tests/data/envelopepowerandrcos_height_local.png +0 -0
- psychopy/tests/data/envelopepowerandrcos_normAddBlend_local.png +0 -0
- psychopy/tests/data/envelopepowerandrcos_normHexbackground_local.png +0 -0
- psychopy/tests/data/envelopepowerandrcos_norm_local.png +0 -0
- psychopy/tests/data/envelopepowerandrcos_stencil_local.png +0 -0
- psychopy/tests/data/gabor1_height_local.png +0 -0
- psychopy/tests/data/gabor1_normAddBlend_local.png +0 -0
- psychopy/tests/data/gabor1_normHexbackground_local.png +0 -0
- psychopy/tests/data/gabor1_normNoShade_local.png +0 -0
- psychopy/tests/data/gabor1_norm_local.png +0 -0
- psychopy/tests/data/gabor1_stencil_local.png +0 -0
- psychopy/tests/data/greyscale_normHexbackground_local.png +0 -0
- psychopy/tests/data/imageAndGauss_height_local.png +0 -0
- psychopy/tests/data/imageAndGauss_normAddBlend_local.png +0 -0
- psychopy/tests/data/imageAndGauss_normHexbackground_local.png +0 -0
- psychopy/tests/data/imageAndGauss_normNoShade_local.png +0 -0
- psychopy/tests/data/imageAndGauss_norm_local.png +0 -0
- psychopy/tests/data/imageAndGauss_stencil_local.png +0 -0
- psychopy/tests/data/movFrame1_stencil_local.png +0 -0
- psychopy/tests/data/noiseAndRcos_height_local.png +0 -0
- psychopy/tests/data/noiseAndRcos_normAddBlend_local.png +0 -0
- psychopy/tests/data/noiseAndRcos_normHexbackground_local.png +0 -0
- psychopy/tests/data/noiseAndRcos_normNoShade_local.png +0 -0
- psychopy/tests/data/noiseAndRcos_norm_local.png +0 -0
- psychopy/tests/data/noiseAndRcos_stencil_local.png +0 -0
- psychopy/tests/data/noiseFiltersAndRcos_height_local.png +0 -0
- psychopy/tests/data/noiseFiltersAndRcos_normAddBlend_local.png +0 -0
- psychopy/tests/data/noiseFiltersAndRcos_normHexbackground_local.png +0 -0
- psychopy/tests/data/noiseFiltersAndRcos_normNoShade_local.png +0 -0
- psychopy/tests/data/noiseFiltersAndRcos_norm_local.png +0 -0
- psychopy/tests/data/noiseFiltersAndRcos_stencil_local.png +0 -0
- psychopy/tests/data/numpyImage_height_local.png +0 -0
- psychopy/tests/data/numpyImage_normAddBlend_local.png +0 -0
- psychopy/tests/data/numpyImage_normHexbackground_local.png +0 -0
- psychopy/tests/data/numpyImage_normNoShade_local.png +0 -0
- psychopy/tests/data/numpyImage_norm_local.png +0 -0
- psychopy/tests/data/numpyImage_stencil_local.png +0 -0
- psychopy/tests/data/shape2_1_normAddBlend_local.png +0 -0
- psychopy/tests/data/shape2_1_normHexbackground_local.png +0 -0
- psychopy/tests/data/shape2_1_normNoShade_local.png +0 -0
- psychopy/tests/data/shape2_1_norm_local.png +0 -0
- psychopy/tests/data/shape2_1_stencil_local.png +0 -0
- psychopy/tests/data/testLoopsBlocks.psyexp_local.py +328 -0
- psychopy/tests/data/text1_height_local.png +0 -0
- psychopy/tests/data/text1_normAddBlend_local.png +0 -0
- psychopy/tests/data/text1_normHexbackground_local.png +0 -0
- psychopy/tests/data/text1_norm_local.png +0 -0
- psychopy/tests/data/text1_stencil_local.png +0 -0
- psychopy/tests/data/text2_height.png +0 -0
- psychopy/tests/data/text2_normAddBlend.png +0 -0
- psychopy/tests/data/text2_normHexbackground.png +0 -0
- psychopy/tests/data/text2_stencil.png +0 -0
- psychopy/tests/data/wedge1_height_local.png +0 -0
- psychopy/tests/data/wedge1_normAddBlend_local.png +0 -0
- psychopy/tests/data/wedge1_normHexbackground_local.png +0 -0
- psychopy/tests/data/wedge1_normNoShade_local.png +0 -0
- psychopy/tests/data/wedge1_norm_local.png +0 -0
- psychopy/tests/data/wedge1_stencil_local.png +0 -0
- psychopy/tests/test_app/.DS_Store +0 -0
- psychopy/tests/test_app/test_builder/.DS_Store +0 -0
- psychopy/tests/test_app/test_builder/data/_2021_ 5_03_1206.csv +9 -0
- psychopy/tests/test_app/test_builder/data/_2021_ 5_03_1206.log +177 -0
- psychopy/tests/test_app/test_builder/data/_2021_ 5_03_1206.psydat +0 -0
- psychopy/tests/test_app/test_builder/data/_2021_ 5_03_1206.xlsx +0 -0
- psychopy/tests/test_app/test_builder/data/_2021_ 5_03_1324.csv +9 -0
- psychopy/tests/test_app/test_builder/data/_2021_ 5_03_1324.log +168 -0
- psychopy/tests/test_app/test_builder/data/_2021_ 5_03_1324.psydat +0 -0
- psychopy/tests/test_app/test_builder/data/_2021_ 5_03_1324.xlsx +0 -0
- psychopy/tests/test_data/.DS_Store +0 -0
- psychopy/tests/test_hardware/test_CRS_BitsSharp.py.orig +68 -0
- psychopy/tests/test_tools/test_arraytools.py +112 -0
- psychopy/tests/test_visual/test_image.py.orig +219 -0
- psychopy/tools/arraytools.py +47 -0
- psychopy/tools/versionchooser.py +1 -1
- psychopy/visual/backends/pygletbackend.py +26 -8
- psychopy/visual/basevisual.py.orig +1723 -0
- psychopy/visual/form.py.orig +1181 -0
- psychopy/visual/text.py.orig +752 -0
- psychopy/visual/textbox2/textbox2.py.orig +1315 -0
- psychopy/visual/window.py +13 -5
- psychopy/visual/windowwarp.py.orig +463 -0
- {psychopy-2024.2.1.dist-info → psychopy-2024.2.4.dist-info}/METADATA +9 -9
- {psychopy-2024.2.1.dist-info → psychopy-2024.2.4.dist-info}/RECORD +244 -78
- {psychopy-2024.2.1.dist-info → psychopy-2024.2.4.dist-info}/WHEEL +1 -1
- {psychopy-2024.2.1.dist-info → psychopy-2024.2.4.dist-info}/entry_points.txt +2 -0
- psychopy/app/locale/ar_001/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/cs_CZ/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/da_DK/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/de_DE/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/el_GR/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/en_NZ/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/en_US/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/es_CO/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/es_ES/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/es_US/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/et_EE/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/fa_IR/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/fi_FI/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/fr_FR/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/he_IL/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/hi_IN/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/hu_HU/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/it_IT/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/ja_JP/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/ko_KR/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/ms_MY/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/nl_NL/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/nn_NO/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/pl_PL/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/pt_PT/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/ro_RO/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/ru_RU/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/sv_SE/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/tr_TR/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/zh_CN/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/zh_TW/LC_MESSAGE/messages.mo +0 -0
- psychopy-2024.2.1.dist-info/licenses/AUTHORS.md +0 -138
- /psychopy/{app/locale/ar_001/LC_MESSAGE → demos/builder}/.DS_Store +0 -0
- /psychopy/{app/locale/es_ES/LC_MESSAGE → demos/builder/Experiments}/.DS_Store +0 -0
- /psychopy/{visual → demos/builder/Tools/gammaCalibration/data}/.DS_Store +0 -0
- {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']]
|