psychopy 2025.1.0__py3-none-any.whl → 2025.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of psychopy might be problematic. Click here for more details.
- psychopy/VERSION +1 -1
- psychopy/alerts/alertsCatalogue/4810.yaml +19 -0
- psychopy/alerts/alertsCatalogue/alertCategories.yaml +4 -0
- psychopy/alerts/alertsCatalogue/alertmsg.py +15 -1
- psychopy/alerts/alertsCatalogue/generateAlertmsg.py +2 -2
- psychopy/app/Resources/classic/add_many.png +0 -0
- psychopy/app/Resources/classic/add_many@2x.png +0 -0
- psychopy/app/Resources/classic/devices.png +0 -0
- psychopy/app/Resources/classic/devices@2x.png +0 -0
- psychopy/app/Resources/classic/photometer.png +0 -0
- psychopy/app/Resources/classic/photometer@2x.png +0 -0
- psychopy/app/Resources/dark/add_many.png +0 -0
- psychopy/app/Resources/dark/add_many@2x.png +0 -0
- psychopy/app/Resources/dark/devices.png +0 -0
- psychopy/app/Resources/dark/devices@2x.png +0 -0
- psychopy/app/Resources/dark/photometer.png +0 -0
- psychopy/app/Resources/dark/photometer@2x.png +0 -0
- psychopy/app/Resources/light/add_many.png +0 -0
- psychopy/app/Resources/light/add_many@2x.png +0 -0
- psychopy/app/Resources/light/devices.png +0 -0
- psychopy/app/Resources/light/devices@2x.png +0 -0
- psychopy/app/Resources/light/photometer.png +0 -0
- psychopy/app/Resources/light/photometer@2x.png +0 -0
- psychopy/app/_psychopyApp.py +35 -13
- psychopy/app/builder/builder.py +88 -35
- psychopy/app/builder/dialogs/__init__.py +69 -220
- psychopy/app/builder/dialogs/dlgsCode.py +29 -8
- psychopy/app/builder/dialogs/paramCtrls.py +1468 -904
- psychopy/app/builder/validators.py +25 -17
- psychopy/app/coder/coder.py +12 -1
- psychopy/app/coder/repl.py +5 -2
- psychopy/app/colorpicker/__init__.py +1 -1
- psychopy/app/deviceManager/__init__.py +1 -0
- psychopy/app/deviceManager/addDialog.py +218 -0
- psychopy/app/deviceManager/dialog.py +185 -0
- psychopy/app/deviceManager/panel.py +191 -0
- psychopy/app/deviceManager/utils.py +60 -0
- psychopy/app/idle.py +7 -0
- psychopy/app/locale/ar_001/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/ar_001/LC_MESSAGE/messages.po +12695 -10592
- psychopy/app/locale/cs_CZ/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/cs_CZ/LC_MESSAGE/messages.po +10199 -24
- psychopy/app/locale/da_DK/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/da_DK/LC_MESSAGE/messages.po +10199 -24
- psychopy/app/locale/de_DE/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/de_DE/LC_MESSAGE/messages.po +11221 -9712
- psychopy/app/locale/el_GR/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/el_GR/LC_MESSAGE/messages.po +10200 -25
- psychopy/app/locale/en_NZ/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/en_NZ/LC_MESSAGE/messages.po +10200 -25
- psychopy/app/locale/en_US/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/en_US/LC_MESSAGE/messages.po +10195 -18
- psychopy/app/locale/es_CO/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/es_CO/LC_MESSAGE/messages.po +11917 -9101
- psychopy/app/locale/es_ES/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/es_ES/LC_MESSAGE/messages.po +11924 -9103
- psychopy/app/locale/es_US/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/es_US/LC_MESSAGE/messages.po +11917 -9101
- psychopy/app/locale/et_EE/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/et_EE/LC_MESSAGE/messages.po +11084 -9569
- psychopy/app/locale/fa_IR/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/fa_IR/LC_MESSAGE/messages.po +11590 -5806
- psychopy/app/locale/fi_FI/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/fi_FI/LC_MESSAGE/messages.po +10199 -24
- psychopy/app/locale/fr_FR/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/fr_FR/LC_MESSAGE/messages.po +11091 -9577
- psychopy/app/locale/he_IL/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/he_IL/LC_MESSAGE/messages.po +11072 -9549
- psychopy/app/locale/hi_IN/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/hi_IN/LC_MESSAGE/messages.po +11071 -9559
- psychopy/app/locale/hu_HU/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/hu_HU/LC_MESSAGE/messages.po +10200 -25
- psychopy/app/locale/it_IT/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/it_IT/LC_MESSAGE/messages.po +11072 -9560
- psychopy/app/locale/ja_JP/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/ja_JP/LC_MESSAGE/messages.po +1485 -1137
- psychopy/app/locale/ko_KR/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/ko_KR/LC_MESSAGE/messages.po +10199 -24
- psychopy/app/locale/ms_MY/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/ms_MY/LC_MESSAGE/messages.po +11463 -8757
- psychopy/app/locale/nl_NL/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/nl_NL/LC_MESSAGE/messages.po +10200 -25
- psychopy/app/locale/nn_NO/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/nn_NO/LC_MESSAGE/messages.po +10200 -25
- psychopy/app/locale/pl_PL/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/pl_PL/LC_MESSAGE/messages.po +10200 -25
- psychopy/app/locale/pt_PT/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/pt_PT/LC_MESSAGE/messages.po +11288 -9434
- psychopy/app/locale/ro_RO/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/ro_RO/LC_MESSAGE/messages.po +10200 -25
- psychopy/app/locale/ru_RU/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/ru_RU/LC_MESSAGE/messages.po +10199 -24
- psychopy/app/locale/sv_SE/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/sv_SE/LC_MESSAGE/messages.po +11441 -8747
- psychopy/app/locale/tr_TR/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/tr_TR/LC_MESSAGE/messages.po +11069 -9545
- psychopy/app/locale/zh_CN/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/zh_CN/LC_MESSAGE/messages.po +12085 -8268
- psychopy/app/locale/zh_TW/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/zh_TW/LC_MESSAGE/messages.po +11929 -8022
- psychopy/app/plugin_manager/dialog.py +12 -3
- psychopy/app/plugin_manager/packageIndex.py +303 -0
- psychopy/app/plugin_manager/packages.py +203 -63
- psychopy/app/plugin_manager/plugins.py +120 -240
- psychopy/app/preferencesDlg.py +6 -1
- psychopy/app/psychopyApp.py +16 -4
- psychopy/app/runner/runner.py +10 -2
- psychopy/app/runner/scriptProcess.py +8 -3
- psychopy/app/stdout/stdOutRich.py +11 -4
- psychopy/app/themes/icons.py +3 -0
- psychopy/app/utils.py +61 -0
- psychopy/colors.py +10 -5
- psychopy/data/experiment.py +133 -23
- psychopy/data/routine.py +12 -0
- psychopy/data/staircase.py +42 -20
- psychopy/data/trial.py +20 -12
- psychopy/data/utils.py +43 -3
- psychopy/demos/builder/Experiments/dragAndDrop/drag_and_drop.psyexp +22 -5
- psychopy/demos/builder/Experiments/dragAndDrop/stimuli/solutions.xlsx +0 -0
- psychopy/demos/builder/Experiments/stroopVoice/stroopVoice.psyexp +2 -12
- psychopy/demos/builder/Feature Demos/buttonBox/buttonBoxDemo.psyexp +3 -8
- psychopy/demos/builder/Feature Demos/movies/movie.psyexp +220 -0
- psychopy/demos/builder/Feature Demos/movies/readme.md +3 -0
- psychopy/demos/builder/Feature Demos/visualValidator/visualValidator.psyexp +1 -2
- psychopy/demos/builder/Hardware/camera/camera.psyexp +3 -16
- psychopy/demos/builder/Hardware/microphone/microphone.psyexp +3 -16
- psychopy/demos/coder/hardware/hdf5_extract.py +133 -0
- psychopy/event.py +20 -15
- psychopy/experiment/_experiment.py +86 -10
- psychopy/experiment/components/__init__.py +3 -10
- psychopy/experiment/components/_base.py +9 -20
- psychopy/experiment/components/button/__init__.py +1 -1
- psychopy/experiment/components/buttonBox/__init__.py +50 -54
- psychopy/experiment/components/camera/__init__.py +137 -359
- psychopy/experiment/components/keyboard/__init__.py +17 -24
- psychopy/experiment/components/microphone/__init__.py +61 -110
- psychopy/experiment/components/movie/__init__.py +2 -3
- psychopy/experiment/components/serialOut/__init__.py +192 -93
- psychopy/experiment/components/settings/__init__.py +45 -27
- psychopy/experiment/components/sound/__init__.py +82 -73
- psychopy/experiment/components/soundsensor/__init__.py +43 -80
- psychopy/experiment/devices.py +303 -0
- psychopy/experiment/exports.py +20 -18
- psychopy/experiment/flow.py +7 -0
- psychopy/experiment/loops.py +47 -29
- psychopy/experiment/monitor.py +74 -0
- psychopy/experiment/params.py +48 -10
- psychopy/experiment/plugins.py +28 -108
- psychopy/experiment/py2js_transpiler.py +1 -1
- psychopy/experiment/routines/__init__.py +1 -1
- psychopy/experiment/routines/_base.py +59 -24
- psychopy/experiment/routines/audioValidator/__init__.py +19 -155
- psychopy/experiment/routines/visualValidator/__init__.py +25 -25
- psychopy/hardware/__init__.py +20 -57
- psychopy/hardware/button.py +15 -2
- psychopy/hardware/camera/__init__.py +2237 -1394
- psychopy/hardware/joystick/__init__.py +1 -1
- psychopy/hardware/keyboard.py +5 -8
- psychopy/hardware/listener.py +4 -1
- psychopy/hardware/manager.py +75 -35
- psychopy/hardware/microphone.py +53 -7
- psychopy/hardware/monitor.py +144 -0
- psychopy/hardware/photometer/__init__.py +156 -117
- psychopy/hardware/serialdevice.py +16 -2
- psychopy/hardware/soundsensor.py +4 -1
- psychopy/iohub/devices/deviceConfigValidation.py +2 -1
- psychopy/iohub/devices/eyetracker/hw/gazepoint/__init__.py +2 -2
- psychopy/iohub/devices/eyetracker/hw/gazepoint/gp3/__init__.py +1 -0
- psychopy/iohub/devices/eyetracker/hw/gazepoint/gp3/eyetracker.py +10 -0
- psychopy/iohub/devices/keyboard/darwin.py +8 -5
- psychopy/iohub/util/__init__.py +7 -8
- psychopy/localization/generateTranslationTemplate.py +208 -116
- psychopy/localization/messages.pot +4305 -3502
- psychopy/monitors/MonitorCenter.py +174 -74
- psychopy/plugins/__init__.py +6 -4
- psychopy/preferences/devices.py +80 -0
- psychopy/preferences/generateHints.py +2 -1
- psychopy/preferences/preferences.py +35 -11
- psychopy/scripts/psychopy-pkgutil.py +969 -0
- psychopy/scripts/psyexpCompile.py +1 -1
- psychopy/session.py +34 -38
- psychopy/sound/__init__.py +6 -260
- psychopy/sound/audioclip.py +164 -0
- psychopy/sound/backend_ptb.py +8 -0
- psychopy/sound/backend_pygame.py +10 -0
- psychopy/sound/backend_pysound.py +9 -0
- psychopy/sound/backends/__init__.py +0 -0
- psychopy/sound/microphone.py +3 -0
- psychopy/sound/sound.py +58 -0
- psychopy/tests/data/correctScript/python/correctNoiseStimComponent.py +1 -1
- psychopy/tests/data/duplicateHeaders.csv +2 -0
- psychopy/tests/test_app/test_builder/test_BuilderFrame.py +22 -7
- psychopy/tests/test_app/test_builder/test_CompileFromBuilder.py +0 -2
- psychopy/tests/test_data/test_utils.py +5 -1
- psychopy/tests/test_experiment/test_components/test_ButtonBoxComponent.py +22 -2
- psychopy/tests/test_hardware/test_ports.py +0 -12
- psychopy/tests/test_tools/test_stringtools.py +1 -1
- psychopy/tools/attributetools.py +12 -5
- psychopy/tools/fontmanager.py +17 -14
- psychopy/tools/gltools.py +4 -2
- psychopy/tools/movietools.py +43 -2
- psychopy/tools/stringtools.py +33 -8
- psychopy/tools/versionchooser.py +1 -1
- psychopy/validation/audio.py +5 -1
- psychopy/validation/visual.py +5 -1
- psychopy/visual/basevisual.py +8 -7
- psychopy/visual/circle.py +2 -2
- psychopy/visual/helpers.py +3 -1
- psychopy/visual/image.py +29 -109
- psychopy/visual/movies/__init__.py +1800 -313
- psychopy/visual/polygon.py +4 -0
- psychopy/visual/shape.py +2 -2
- psychopy/visual/window.py +35 -12
- psychopy/voicekey/__init__.py +41 -669
- psychopy/voicekey/labjack_vks.py +7 -48
- psychopy/voicekey/parallel_vks.py +7 -42
- psychopy/voicekey/vk_tools.py +114 -263
- {psychopy-2025.1.0.dist-info → psychopy-2025.2.1.dist-info}/METADATA +20 -13
- {psychopy-2025.1.0.dist-info → psychopy-2025.2.1.dist-info}/RECORD +222 -190
- {psychopy-2025.1.0.dist-info → psychopy-2025.2.1.dist-info}/WHEEL +1 -1
- psychopy/visual/movies/players/__init__.py +0 -62
- psychopy/visual/movies/players/ffpyplayer_player.py +0 -1401
- psychopy/voicekey/demo_vks.py +0 -12
- psychopy/voicekey/signal.py +0 -42
- {psychopy-2025.1.0.dist-info → psychopy-2025.2.1.dist-info}/entry_points.txt +0 -0
- {psychopy-2025.1.0.dist-info → psychopy-2025.2.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -15,12 +15,17 @@ import wx.stc
|
|
|
15
15
|
|
|
16
16
|
from psychopy.app.colorpicker import PsychoColorPicker
|
|
17
17
|
from psychopy.app.dialogs import ListWidget
|
|
18
|
+
from psychopy.app.themes import fonts, colors
|
|
18
19
|
from psychopy.colors import Color
|
|
20
|
+
from psychopy.experiment.exports import NameSpace
|
|
21
|
+
from psychopy.experiment.params import Param, toList
|
|
19
22
|
from psychopy.localization import _translate
|
|
20
|
-
from psychopy import data, prefs, experiment
|
|
23
|
+
from psychopy import data, exceptions, logging, prefs, experiment
|
|
21
24
|
import re
|
|
22
25
|
from pathlib import Path
|
|
23
26
|
|
|
27
|
+
from psychopy.tools import stringtools
|
|
28
|
+
|
|
24
29
|
from . import CodeBox
|
|
25
30
|
from ...coder import BaseCodeEditor
|
|
26
31
|
from ...themes import icons, handlers
|
|
@@ -28,231 +33,443 @@ from ... import utils
|
|
|
28
33
|
from ...themes import icons
|
|
29
34
|
|
|
30
35
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
36
|
+
inputTypes = {}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
EVT_PARAM_CHANGED = wx.PyEventBinder(wx.IdManager.ReserveId())
|
|
40
|
+
emptyNamespace = NameSpace(experiment.Experiment())
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ParamValueChangedEvent(wx.CommandEvent):
|
|
44
|
+
def __init__(self, obj, param, trigger=None):
|
|
45
|
+
wx.CommandEvent.__init__(self, EVT_PARAM_CHANGED.typeId)
|
|
46
|
+
# set object
|
|
47
|
+
self.SetEventObject(obj)
|
|
48
|
+
# store param
|
|
49
|
+
self.param = param
|
|
50
|
+
# store triggering event
|
|
51
|
+
self.trigger = trigger
|
|
52
|
+
|
|
53
|
+
def getParam(self):
|
|
54
|
+
return self.param
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class BaseParamCtrl(wx.Panel):
|
|
58
|
+
"""
|
|
59
|
+
Base class for all ParamCtrls, defines the minimum functions needed for a ParamCtrl to work.
|
|
60
|
+
|
|
61
|
+
Attributes
|
|
62
|
+
----------
|
|
63
|
+
inputType : str
|
|
64
|
+
Input type which this ctrl corresponds to
|
|
65
|
+
|
|
66
|
+
Parameters
|
|
67
|
+
----------
|
|
68
|
+
parent : wx.Window
|
|
69
|
+
Parent window for this ctrl
|
|
70
|
+
field : str
|
|
71
|
+
Name of the param which this ctrl represents
|
|
72
|
+
param : psychopy.experiment.Param
|
|
73
|
+
Parameter which this ctrl represents
|
|
74
|
+
element
|
|
75
|
+
Builder element (Component, Routine, Loop, etc.) to which this parameter belongs, if any
|
|
76
|
+
"""
|
|
77
|
+
# what inputType does a Param need to have to get this ctrl?
|
|
78
|
+
inputType = None
|
|
79
|
+
|
|
80
|
+
# additional styles for the ctrl (used by overloaded makeCtrls)
|
|
81
|
+
ctrlStyle = wx.DEFAULT
|
|
82
|
+
|
|
83
|
+
def __init__(self, parent, field, param, element=None, warnings=None):
|
|
84
|
+
# initialise
|
|
85
|
+
wx.Panel.__init__(self, parent)
|
|
86
|
+
# store details
|
|
87
|
+
self.parent = parent
|
|
88
|
+
self.field = field
|
|
89
|
+
self.param = param.copy()
|
|
90
|
+
self.element = element
|
|
91
|
+
self.warnings = warnings
|
|
92
|
+
# setup namespace
|
|
93
|
+
if hasattr(element, "exp"):
|
|
94
|
+
self.namespace = self.element.exp.namespace
|
|
95
|
+
else:
|
|
96
|
+
self.namespace = emptyNamespace
|
|
97
|
+
# setup sizer
|
|
98
|
+
self.sizer = wx.BoxSizer(wx.HORIZONTAL)
|
|
99
|
+
self.SetSizer(self.sizer)
|
|
100
|
+
# call method which subclasses override to make controls
|
|
101
|
+
self.makeCtrls()
|
|
102
|
+
# set tooltip
|
|
103
|
+
self.setTooltip(param.hint)
|
|
104
|
+
|
|
105
|
+
def __init_subclass__(cls):
|
|
106
|
+
# index subclasses of BaseParamCtrl by the inputType they represent
|
|
107
|
+
if cls.inputType is not None and cls.inputType not in inputTypes:
|
|
108
|
+
inputTypes[cls.inputType] = cls
|
|
109
|
+
|
|
110
|
+
def makeCtrls(self):
|
|
34
111
|
"""
|
|
35
|
-
|
|
112
|
+
Makes the actual control object.
|
|
36
113
|
"""
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
114
|
+
raise NotImplementedError(
|
|
115
|
+
"All subclasses of BaseParamCtrl should implement `makeCtrls`"
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
def getValue(self):
|
|
119
|
+
"""
|
|
120
|
+
Returns the value of this ctrl
|
|
121
|
+
"""
|
|
122
|
+
raise NotImplementedError(
|
|
123
|
+
"All subclasses of BaseParamCtrl should implement `getValue`"
|
|
124
|
+
)
|
|
42
125
|
|
|
126
|
+
def setValue(self, value):
|
|
127
|
+
"""
|
|
128
|
+
Returns the value of this ctrl
|
|
129
|
+
"""
|
|
130
|
+
raise NotImplementedError(
|
|
131
|
+
"All subclasses of BaseParamCtrl should implement `setValue`"
|
|
132
|
+
)
|
|
43
133
|
|
|
44
|
-
|
|
45
|
-
def validate(self, evt=None):
|
|
46
|
-
"""Redirect validate calls to global validate method, assigning
|
|
47
|
-
appropriate `valType`.
|
|
134
|
+
def setTooltip(self, text):
|
|
48
135
|
"""
|
|
49
|
-
|
|
136
|
+
Set the tooltip on this control.
|
|
50
137
|
|
|
51
|
-
|
|
52
|
-
|
|
138
|
+
Parameters
|
|
139
|
+
----------
|
|
140
|
+
text : str
|
|
141
|
+
Text to show in tooltip
|
|
142
|
+
"""
|
|
143
|
+
# set tooltip on panel
|
|
144
|
+
self.SetToolTip(wx.ToolTip(text))
|
|
145
|
+
# set on ctrl if possible
|
|
146
|
+
if hasattr(self.ctrl, 'SetToolTip'):
|
|
147
|
+
self.ctrl.SetToolTip(wx.ToolTip(text))
|
|
148
|
+
|
|
149
|
+
def getWarning(self):
|
|
150
|
+
"""
|
|
151
|
+
Get the warning associated with this ctrl, if any
|
|
152
|
+
"""
|
|
153
|
+
if self.warnings is not None:
|
|
154
|
+
return self.warnings.getWarning(self)
|
|
53
155
|
|
|
54
|
-
def
|
|
55
|
-
"""
|
|
56
|
-
|
|
57
|
-
return
|
|
156
|
+
def setWarning(self, warning, allowed=True):
|
|
157
|
+
"""
|
|
158
|
+
Set a warning on the warnings handler attached to this ctrl, if any.
|
|
58
159
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
160
|
+
Parameters
|
|
161
|
+
----------
|
|
162
|
+
warning : str
|
|
163
|
+
Warning to display
|
|
164
|
+
"""
|
|
165
|
+
if self.warnings is not None:
|
|
166
|
+
self.warnings.setWarning(self, warning, allowed=allowed)
|
|
167
|
+
|
|
168
|
+
def clearWarning(self):
|
|
169
|
+
"""
|
|
170
|
+
Remove the warning handler attached to this ctrl, if any.
|
|
171
|
+
"""
|
|
172
|
+
if self.warnings is not None:
|
|
173
|
+
self.warnings.clearWarning(self)
|
|
174
|
+
|
|
175
|
+
@property
|
|
176
|
+
def isValid(self):
|
|
177
|
+
"""
|
|
178
|
+
Returns True or False based on whether the current ctrl has generated any warnings
|
|
179
|
+
"""
|
|
180
|
+
if self.warnings is not None:
|
|
181
|
+
return self.warnings.getWarning(self) is None
|
|
63
182
|
|
|
64
|
-
def
|
|
65
|
-
"""
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
# Name is never code
|
|
71
|
-
valType = "str"
|
|
183
|
+
def validate(self):
|
|
184
|
+
"""
|
|
185
|
+
Update warnings based on the value of this ctrl
|
|
186
|
+
"""
|
|
187
|
+
# always start off with no warning
|
|
188
|
+
self.clearWarning()
|
|
72
189
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
190
|
+
def styleValid(self):
|
|
191
|
+
"""
|
|
192
|
+
Style this ctrl according to whether its value is valid (`.isValid`)
|
|
193
|
+
"""
|
|
194
|
+
# if not implemented, do nothing
|
|
195
|
+
return
|
|
196
|
+
|
|
197
|
+
@property
|
|
198
|
+
def isCode(self):
|
|
199
|
+
"""
|
|
200
|
+
Returns True if the contents of this ctrl should be styled as code.
|
|
201
|
+
"""
|
|
202
|
+
# if needed, figure out from $
|
|
203
|
+
if self.param.valType in ("extendedStr","str", "file", "table", "color"):
|
|
204
|
+
return str(self.getValue()).startswith("$")
|
|
205
|
+
|
|
206
|
+
return True
|
|
207
|
+
|
|
208
|
+
def styleCode(self):
|
|
209
|
+
"""
|
|
210
|
+
Style this ctrl according to whether it contains code (`.isCode`)
|
|
211
|
+
"""
|
|
212
|
+
# if not implemented, do nothing
|
|
213
|
+
return
|
|
214
|
+
|
|
215
|
+
def onChange(self, evt=None):
|
|
216
|
+
"""
|
|
217
|
+
Callback which updates the control and param when the value changes.
|
|
78
218
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
219
|
+
Parameters
|
|
220
|
+
----------
|
|
221
|
+
evt : wx.Event
|
|
222
|
+
Whatever event triggered this function
|
|
223
|
+
"""
|
|
224
|
+
# validate ctrl
|
|
225
|
+
self.validate()
|
|
226
|
+
# style according to whether value is code and valid
|
|
227
|
+
self.styleCode()
|
|
228
|
+
self.styleValid()
|
|
229
|
+
# update
|
|
230
|
+
self.Update()
|
|
231
|
+
self.Refresh()
|
|
232
|
+
# update param value
|
|
233
|
+
self.param.val = self.getValue()
|
|
234
|
+
# show any warnings
|
|
235
|
+
if self.warnings is not None:
|
|
236
|
+
self.warnings.showWarning()
|
|
237
|
+
# process dependent params
|
|
238
|
+
if hasattr(self.parent, "checkDepends"):
|
|
239
|
+
self.parent.checkDepends()
|
|
240
|
+
# emit a custom event
|
|
241
|
+
evt = ParamValueChangedEvent(self, param=self.param, trigger=evt)
|
|
242
|
+
wx.PostEvent(self, evt)
|
|
243
|
+
|
|
244
|
+
def onElementOk(self, evt=None):
|
|
245
|
+
"""
|
|
246
|
+
Method which is called when OK is pressed on the element containing this param, if any.
|
|
247
|
+
"""
|
|
248
|
+
# assume no action
|
|
249
|
+
return
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
class ParamCtrl:
|
|
253
|
+
"""
|
|
254
|
+
Constructor which looks for the appropriate subclass of BaseParamCtrl and initialises that.
|
|
255
|
+
"""
|
|
256
|
+
def __new__(cls, parent, field, param, element=None, warnings=None):
|
|
257
|
+
if param.inputType in inputTypes:
|
|
258
|
+
# if a known type, get associated control
|
|
259
|
+
ctrlCls = inputTypes[param.inputType]
|
|
84
260
|
else:
|
|
85
|
-
# otherwise
|
|
86
|
-
|
|
261
|
+
# otherwise, make a single line text ctrl
|
|
262
|
+
ctrlCls = SingleLineCtrl
|
|
263
|
+
|
|
264
|
+
return ctrlCls(parent, field, param, element, warnings)
|
|
87
265
|
|
|
266
|
+
class SingleLineCtrl(BaseParamCtrl):
|
|
267
|
+
inputType = "single"
|
|
88
268
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
269
|
+
# overload this in subclasses to control style
|
|
270
|
+
ctrlStyle = wx.TE_LEFT
|
|
271
|
+
|
|
272
|
+
def makeCtrls(self):
|
|
273
|
+
# add dollar label
|
|
274
|
+
self.dollarLbl = wx.StaticText(
|
|
275
|
+
self, label="$", style=wx.ALIGN_RIGHT
|
|
276
|
+
)
|
|
277
|
+
self.dollarLbl.SetToolTip(_translate(
|
|
278
|
+
"This parameter will be treated as code - we have already put in the $, so you don't "
|
|
279
|
+
"have to."
|
|
280
|
+
))
|
|
281
|
+
self.sizer.Add(
|
|
282
|
+
self.dollarLbl, border=6, flag=wx.CENTER | wx.RIGHT
|
|
283
|
+
)
|
|
284
|
+
# show/hide dollar according to valType
|
|
285
|
+
self.dollarLbl.Show(
|
|
286
|
+
self.param.valType in ("code", "extendedCode")
|
|
287
|
+
)
|
|
288
|
+
# add value ctrl
|
|
289
|
+
self.ctrl = wx.TextCtrl(
|
|
290
|
+
self, value=str(self.param.val), name=self.field, style=self.ctrlStyle
|
|
291
|
+
)
|
|
292
|
+
self.sizer.Add(
|
|
293
|
+
self.ctrl, proportion=1, flag=wx.EXPAND
|
|
294
|
+
)
|
|
295
|
+
# enforce a minimum height on multiline ctrls
|
|
296
|
+
if self.ctrlStyle | wx.TE_MULTILINE == self.ctrlStyle:
|
|
297
|
+
self.ctrl.SetMinSize((-1, 128))
|
|
298
|
+
# map change event
|
|
299
|
+
self.ctrl.Bind(
|
|
300
|
+
wx.EVT_TEXT, self.onChange
|
|
301
|
+
)
|
|
302
|
+
# also do styling once now
|
|
303
|
+
self.onChange()
|
|
304
|
+
|
|
305
|
+
def getValue(self):
|
|
306
|
+
return self.ctrl.GetValue()
|
|
307
|
+
|
|
308
|
+
def setValue(self, value, silent=False):
|
|
309
|
+
# get insertion point if possible
|
|
310
|
+
pt = self.ctrl.GetInsertionPoint()
|
|
311
|
+
# set value
|
|
312
|
+
if silent:
|
|
313
|
+
self.ctrl.ChangeValue(str(value))
|
|
314
|
+
else:
|
|
315
|
+
self.ctrl.SetValue(str(value))
|
|
316
|
+
# restore insertion point if possible
|
|
317
|
+
try:
|
|
318
|
+
self.ctrl.SetInsertionPoint(pt)
|
|
319
|
+
except:
|
|
320
|
+
pass
|
|
321
|
+
|
|
322
|
+
def validateCode(self):
|
|
323
|
+
# get value without any dollar syntax
|
|
324
|
+
value = experiment.getCodeFromParamStr(
|
|
325
|
+
self.getValue(),
|
|
326
|
+
target="PsychoPy"
|
|
327
|
+
)
|
|
328
|
+
# if blank, there's no code yet to be invalid
|
|
329
|
+
if not value:
|
|
109
330
|
return
|
|
110
|
-
file = dlg.GetPath()
|
|
111
331
|
try:
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
style=wx.FD_OPEN | wx.FD_FILE_MUST_EXIST | wx.FD_MULTIPLE,
|
|
120
|
-
wildcard=_translate(wildcard))
|
|
121
|
-
if dlg.ShowModal() != wx.ID_OK:
|
|
332
|
+
variableDefs = stringtools.getVariableDefs(value)
|
|
333
|
+
variables = stringtools.getVariables(value)
|
|
334
|
+
except (SyntaxError, TypeError) as e:
|
|
335
|
+
# if failed to get variables, add warning and mark invalid
|
|
336
|
+
self.setWarning(_translate(
|
|
337
|
+
"Python syntax error in field `{}`: {}"
|
|
338
|
+
).format(self.param.label, e))
|
|
122
339
|
return
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
340
|
+
# for multiline code, check that any variable defs don't break the namespace
|
|
341
|
+
if self.param.valType == "extendedCode":
|
|
342
|
+
# check that nothing important is being overwritten
|
|
343
|
+
if self.element:
|
|
344
|
+
# iterate through variable defs in code (if any)
|
|
345
|
+
for name in variableDefs:
|
|
346
|
+
# is it overwriting something?
|
|
347
|
+
used = self.namespace.exists(name)
|
|
348
|
+
if used:
|
|
349
|
+
# warn but allow
|
|
350
|
+
self.setWarning(_translate(
|
|
351
|
+
"Setting the variable `{}` will overwrite an existing variable ({})"
|
|
352
|
+
).format(name, used), allowed=True)
|
|
353
|
+
else:
|
|
354
|
+
# check any dynamic parameters
|
|
355
|
+
if self.param.updates == "constant":
|
|
356
|
+
# if references a name, is it one defined before experiment start?
|
|
357
|
+
for name in variables:
|
|
358
|
+
if name not in NameSpace.nonUserBuilder:
|
|
359
|
+
# if not, warn but allow
|
|
360
|
+
self.setWarning(_translate(
|
|
361
|
+
"Looks like your variable '{}' in '{}' should be set to "
|
|
362
|
+
"update."
|
|
363
|
+
).format(name, self.param.label), allowed=True)
|
|
364
|
+
|
|
365
|
+
def validateStr(self):
|
|
366
|
+
# warn for unescaped "
|
|
367
|
+
if re.findall(r"(?<!\\)[\"\']", self.getValue()):
|
|
368
|
+
self.setWarning(_translate(
|
|
369
|
+
"Quotation marks (\" or ') need to be escaped (\\\" or \\')"
|
|
370
|
+
))
|
|
133
371
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
sizer = self
|
|
372
|
+
def validate(self):
|
|
373
|
+
# start off valid
|
|
374
|
+
BaseParamCtrl.validate(self)
|
|
375
|
+
# use different method for code vs string
|
|
376
|
+
if self.isCode:
|
|
377
|
+
return self.validateCode()
|
|
141
378
|
else:
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
379
|
+
return self.validateStr()
|
|
380
|
+
|
|
381
|
+
def styleValid(self):
|
|
382
|
+
# text turns red if invalid
|
|
383
|
+
if self.isValid:
|
|
384
|
+
self.ctrl.SetForegroundColour(
|
|
385
|
+
colors.scheme['black']
|
|
386
|
+
)
|
|
146
387
|
else:
|
|
147
|
-
self.
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
self.
|
|
151
|
-
|
|
152
|
-
def
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
def
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
self.
|
|
388
|
+
self.ctrl.SetForegroundColour(
|
|
389
|
+
colors.scheme['red']
|
|
390
|
+
)
|
|
391
|
+
self.ctrl.Refresh()
|
|
392
|
+
|
|
393
|
+
def styleCode(self):
|
|
394
|
+
# text becomes monospace if code
|
|
395
|
+
if self.isCode:
|
|
396
|
+
self.ctrl.SetFont(
|
|
397
|
+
fonts.CodeFont(bold=True).obj
|
|
398
|
+
)
|
|
399
|
+
else:
|
|
400
|
+
self.ctrl.SetFont(
|
|
401
|
+
fonts.AppFont().obj
|
|
402
|
+
)
|
|
403
|
+
self.ctrl.Refresh()
|
|
404
|
+
|
|
405
|
+
def onChange(self, evt=None):
|
|
406
|
+
# do some sanitization before usual onchange behaviour
|
|
407
|
+
if self.isCode:
|
|
408
|
+
# replace unescaped curly quotes
|
|
409
|
+
if re.findall(r"(?<!\\)[\u201c\u201d]", self.getValue()):
|
|
410
|
+
self.setValue(
|
|
411
|
+
re.sub(r"(?<!\\)[\u201c\u201d]", "\"", self.getValue())
|
|
412
|
+
)
|
|
413
|
+
if re.findall(r"(?<!\\)[\u2018\u2019]", self.getValue()):
|
|
414
|
+
self.setValue(
|
|
415
|
+
re.sub(r"(?<!\\)[\u2018\u2019]", "\'", self.getValue())
|
|
416
|
+
)
|
|
417
|
+
else:
|
|
418
|
+
pass
|
|
419
|
+
|
|
420
|
+
BaseParamCtrl.onChange(self, evt)
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
class NameCtrl(SingleLineCtrl):
|
|
424
|
+
inputType = "name"
|
|
425
|
+
|
|
426
|
+
def styleCode(self):
|
|
427
|
+
# a name is always code, we don't need to remind the user, so style as normal
|
|
428
|
+
self.dollarLbl.Hide()
|
|
429
|
+
self.ctrl.Refresh()
|
|
430
|
+
self.ctrl.Layout()
|
|
431
|
+
|
|
432
|
+
def validate(self):
|
|
433
|
+
# start off valid
|
|
434
|
+
BaseParamCtrl.validate(self)
|
|
435
|
+
# is name a valid name?
|
|
436
|
+
if self.getValue() == "":
|
|
437
|
+
# prompt to enter a name if blank
|
|
438
|
+
self.setWarning(_translate(
|
|
439
|
+
"Please enter a name"
|
|
440
|
+
), allowed=False)
|
|
441
|
+
elif NameSpace.isValid(self.getValue()):
|
|
442
|
+
# if we have an experiment, is the name used already?
|
|
443
|
+
if self.element:
|
|
444
|
+
# if unchanged from original name, it does exist but is valid
|
|
445
|
+
if self.getValue() == self.element.name:
|
|
446
|
+
return
|
|
447
|
+
# otherwise, check against extant names
|
|
448
|
+
exists = self.namespace.exists(self.getValue())
|
|
449
|
+
if exists:
|
|
450
|
+
self.setWarning(_translate(
|
|
451
|
+
"Name is already in use ({})"
|
|
452
|
+
).format(exists), allowed=False)
|
|
453
|
+
else:
|
|
454
|
+
self.setWarning(_translate(
|
|
455
|
+
"Name is not valid"
|
|
456
|
+
), allowed=False)
|
|
188
457
|
|
|
189
|
-
def Show(self, value=True):
|
|
190
|
-
wx.TextCtrl.Show(self, value)
|
|
191
|
-
if hasattr(self, "dollarLbl"):
|
|
192
|
-
self.dollarLbl.Show(value)
|
|
193
|
-
if hasattr(self, "deleteBtn"):
|
|
194
|
-
self.deleteBtn.Show(value)
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
class MultiLineCtrl(SingleLineCtrl, _ValidatorMixin, _HideMixin):
|
|
198
|
-
def __init__(self, parent, valType,
|
|
199
|
-
val="", fieldName="",
|
|
200
|
-
size=wx.Size(-1, 144)):
|
|
201
|
-
SingleLineCtrl.__init__(self, parent, valType,
|
|
202
|
-
val=val, fieldName=fieldName,
|
|
203
|
-
size=size, style=wx.TE_MULTILINE)
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
class CodeCtrl(BaseCodeEditor, handlers.ThemeMixin, _ValidatorMixin):
|
|
207
|
-
def __init__(self, parent, valType,
|
|
208
|
-
val="", fieldName="",
|
|
209
|
-
size=wx.Size(-1, 144)):
|
|
210
|
-
BaseCodeEditor.__init__(self, parent,
|
|
211
|
-
ID=wx.ID_ANY, pos=wx.DefaultPosition, size=size,
|
|
212
|
-
style=0)
|
|
213
|
-
self.valType = valType
|
|
214
|
-
self.SetValue(val)
|
|
215
|
-
self.fieldName = fieldName
|
|
216
|
-
self.params = fieldName
|
|
217
|
-
# Setup lexer to style text
|
|
218
|
-
self.SetLexer(wx.stc.STC_LEX_PYTHON)
|
|
219
|
-
self._applyAppTheme()
|
|
220
|
-
# Hide margin
|
|
221
|
-
self.SetMarginWidth(0, 0)
|
|
222
|
-
# Setup auto indent behaviour as in Code component
|
|
223
|
-
self.Bind(wx.EVT_KEY_DOWN, self.onKey)
|
|
224
458
|
|
|
225
|
-
|
|
226
|
-
|
|
459
|
+
class MultiLineCtrl(SingleLineCtrl):
|
|
460
|
+
inputType = "multi"
|
|
227
461
|
|
|
228
|
-
|
|
229
|
-
self.SetValue(value)
|
|
462
|
+
ctrlStyle = wx.TE_LEFT | wx.TE_MULTILINE
|
|
230
463
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
self.setValue(value)
|
|
241
|
-
|
|
242
|
-
def onKey(self, evt=None):
|
|
243
|
-
CodeBox.OnKeyPressed(self, evt)
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
class InvalidCtrl(SingleLineCtrl, _ValidatorMixin, _HideMixin):
|
|
247
|
-
def __init__(self, parent, valType,
|
|
248
|
-
val="", fieldName="",
|
|
249
|
-
size=wx.Size(-1, 24), style=wx.DEFAULT):
|
|
250
|
-
SingleLineCtrl.__init__(self, parent, valType,
|
|
251
|
-
val=val, fieldName=fieldName,
|
|
252
|
-
size=size, style=style)
|
|
253
|
-
self.Disable()
|
|
254
|
-
# Add delete button
|
|
255
|
-
self.deleteBtn = wx.Button(parent, label="×", size=(24, 24))
|
|
464
|
+
|
|
465
|
+
class InvalidCtrl(SingleLineCtrl):
|
|
466
|
+
inputType = "inv"
|
|
467
|
+
|
|
468
|
+
def makeCtrls(self):
|
|
469
|
+
SingleLineCtrl.makeCtrls(self)
|
|
470
|
+
self.ctrl.Disable()
|
|
471
|
+
# add delete button
|
|
472
|
+
self.deleteBtn = wx.Button(self, label="×", size=(24, 24))
|
|
256
473
|
self.deleteBtn.SetForegroundColour("red")
|
|
257
474
|
self.deleteBtn.Bind(wx.EVT_BUTTON, self.deleteParam)
|
|
258
475
|
self.deleteBtn.SetToolTip(_translate(
|
|
@@ -260,463 +477,396 @@ class InvalidCtrl(SingleLineCtrl, _ValidatorMixin, _HideMixin):
|
|
|
260
477
|
"In the latest version of PsychoPy, it is not used. Click this "
|
|
261
478
|
"button to delete it. WARNING: This may affect how this experiment "
|
|
262
479
|
"works in older versions!"))
|
|
263
|
-
self.
|
|
264
|
-
#
|
|
265
|
-
self.deleteLbl = wx.StaticText(
|
|
480
|
+
self.sizer.Add(self.deleteBtn, border=6, flag=wx.LEFT | wx.RIGHT)
|
|
481
|
+
# add deleted label
|
|
482
|
+
self.deleteLbl = wx.StaticText(self, label=_translate("DELETED"))
|
|
266
483
|
self.deleteLbl.SetForegroundColour("red")
|
|
267
484
|
self.deleteLbl.Hide()
|
|
268
|
-
self.
|
|
269
|
-
#
|
|
270
|
-
self.undoBtn = wx.Button(
|
|
485
|
+
self.sizer.Add(self.deleteLbl, border=6, proportion=1, flag=wx.ALL | wx.ALIGN_CENTER_VERTICAL)
|
|
486
|
+
# add undo delete button
|
|
487
|
+
self.undoBtn = wx.Button(self, label="⟲", size=(24, 24))
|
|
271
488
|
self.undoBtn.SetToolTip(_translate(
|
|
272
489
|
"This parameter will not be deleted until you click Okay. "
|
|
273
490
|
"Click this button to revert the deletion and keep the parameter."))
|
|
274
491
|
self.undoBtn.Hide()
|
|
275
492
|
self.undoBtn.Bind(wx.EVT_BUTTON, self.undoDelete)
|
|
276
|
-
self.
|
|
493
|
+
self.sizer.Add(self.undoBtn, border=6, flag=wx.LEFT | wx.RIGHT)
|
|
277
494
|
|
|
278
|
-
#
|
|
495
|
+
# set deletion flag
|
|
279
496
|
self.forDeletion = False
|
|
280
497
|
|
|
281
498
|
def deleteParam(self, evt=None):
|
|
282
499
|
"""
|
|
283
500
|
When the remove button is pressed, mark this param as for deletion
|
|
284
501
|
"""
|
|
285
|
-
#
|
|
502
|
+
# mark for deletion
|
|
286
503
|
self.forDeletion = True
|
|
287
|
-
#
|
|
288
|
-
self.Hide()
|
|
504
|
+
# hide value ctrl and delete button
|
|
505
|
+
self.ctrl.Hide()
|
|
289
506
|
self.deleteBtn.Hide()
|
|
290
|
-
#
|
|
507
|
+
# show delete label and
|
|
291
508
|
self.undoBtn.Show()
|
|
292
509
|
self.deleteLbl.Show()
|
|
293
510
|
|
|
294
|
-
self.
|
|
511
|
+
self.sizer.Layout()
|
|
295
512
|
|
|
296
513
|
def undoDelete(self, evt=None):
|
|
297
|
-
#
|
|
514
|
+
# mark not for deletion
|
|
298
515
|
self.forDeletion = False
|
|
299
|
-
#
|
|
300
|
-
self.Show()
|
|
516
|
+
# show value ctrl and delete button
|
|
517
|
+
self.ctrl.Show()
|
|
301
518
|
self.deleteBtn.Show()
|
|
302
|
-
#
|
|
519
|
+
# hide delete label and
|
|
303
520
|
self.undoBtn.Hide()
|
|
304
521
|
self.deleteLbl.Hide()
|
|
305
522
|
|
|
306
|
-
self.
|
|
523
|
+
self.sizer.Layout()
|
|
307
524
|
|
|
308
525
|
|
|
309
|
-
class
|
|
310
|
-
|
|
311
|
-
val="", fieldName="",
|
|
312
|
-
size=wx.Size(-1, 24), limits=None):
|
|
313
|
-
wx.SpinCtrl.__init__(self)
|
|
314
|
-
limits = limits or (-100,100)
|
|
315
|
-
self.Create(parent, -1, str(val), name=fieldName, size=size, min=min(limits), max=max(limits))
|
|
316
|
-
self.valType = valType
|
|
317
|
-
self.Bind(wx.EVT_SPINCTRL, self.spin)
|
|
526
|
+
class BoolCtrl(BaseParamCtrl):
|
|
527
|
+
inputType = "bool"
|
|
318
528
|
|
|
319
|
-
def
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
self.
|
|
325
|
-
|
|
529
|
+
def makeCtrls(self):
|
|
530
|
+
# add checkbox
|
|
531
|
+
self.ctrl = wx.CheckBox(self)
|
|
532
|
+
self.ctrl.SetValue(bool(self.param))
|
|
533
|
+
self.sizer.Add(
|
|
534
|
+
self.ctrl, border=6, flag=wx.EXPAND | wx.ALL
|
|
535
|
+
)
|
|
536
|
+
# connect onChange
|
|
537
|
+
self.ctrl.Bind(
|
|
538
|
+
wx.EVT_CHECKBOX, self.onChange
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
def getValue(self):
|
|
542
|
+
return self.ctrl.IsChecked()
|
|
326
543
|
|
|
544
|
+
def setValue(self, value):
|
|
545
|
+
self.ctrl.SetValue(bool(value))
|
|
327
546
|
|
|
328
|
-
BoolCtrl = wx.CheckBox
|
|
329
547
|
|
|
548
|
+
class ChoiceCtrl(BaseParamCtrl):
|
|
549
|
+
inputType = "choice"
|
|
330
550
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
#
|
|
338
|
-
|
|
339
|
-
|
|
551
|
+
def makeCtrls(self):
|
|
552
|
+
# add choice ctrl
|
|
553
|
+
self.ctrl = wx.Choice(self)
|
|
554
|
+
self.sizer.Add(
|
|
555
|
+
self.ctrl, proportion=1, flag=wx.EXPAND | wx.ALL
|
|
556
|
+
)
|
|
557
|
+
# connect onChange
|
|
558
|
+
self.ctrl.Bind(
|
|
559
|
+
wx.EVT_CHOICE, self.onChange
|
|
560
|
+
)
|
|
561
|
+
# set initial choices
|
|
340
562
|
self.populate()
|
|
341
|
-
self.valType = valType
|
|
342
|
-
self.SetStringSelection(val)
|
|
343
563
|
|
|
344
564
|
def populate(self):
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
choices = self.
|
|
565
|
+
# convert values to a list (by executing method of just converting value)
|
|
566
|
+
if callable(self.param.allowedVals):
|
|
567
|
+
choices = [str(val) for val in self.param.allowedVals()]
|
|
348
568
|
else:
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
labels = self.
|
|
355
|
-
elif self._labels:
|
|
356
|
-
# otherwise, treat it as a list
|
|
357
|
-
labels = list(self._labels)
|
|
569
|
+
choices = [str(val) for val in self.param.allowedVals]
|
|
570
|
+
# convert labels to a list (by executing method of just converting value)
|
|
571
|
+
if callable(self.param.allowedLabels):
|
|
572
|
+
labels = self.param.allowedLabels()
|
|
573
|
+
elif self.param.allowedLabels:
|
|
574
|
+
labels = list(self.param.allowedLabels)
|
|
358
575
|
else:
|
|
359
576
|
# if not given any labels, alias values
|
|
360
577
|
labels = choices
|
|
361
|
-
#
|
|
362
|
-
|
|
363
|
-
|
|
578
|
+
# make arrays the same length
|
|
579
|
+
self.choices = []
|
|
580
|
+
self.labels = []
|
|
581
|
+
for i in range(max(len(choices), len(labels))):
|
|
582
|
+
# fill in missing choices with label
|
|
583
|
+
if i < len(choices):
|
|
584
|
+
self.choices.append(choices[i])
|
|
585
|
+
else:
|
|
586
|
+
self.choices.append(labels[i])
|
|
587
|
+
# fill in missing labels with choices
|
|
364
588
|
if i < len(labels):
|
|
365
|
-
|
|
589
|
+
self.labels.append(str(labels[i]))
|
|
366
590
|
else:
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
591
|
+
self.labels.append(str(choices[i]))
|
|
592
|
+
# translate labels
|
|
593
|
+
for i in range(len(self.labels)):
|
|
594
|
+
# An empty string must not be translated
|
|
595
|
+
# because it returns meta information of
|
|
596
|
+
# .mo file (due to specification of gettext)
|
|
597
|
+
if self.labels[i] != '':
|
|
598
|
+
self.labels[i] = _translate(self.labels[i])
|
|
373
599
|
# apply to ctrl
|
|
374
|
-
self.SetItems(
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
600
|
+
self.ctrl.SetItems(self.labels)
|
|
601
|
+
# disable if param is readonly
|
|
602
|
+
self.ctrl.Enable(not self.param.readOnly)
|
|
603
|
+
# apply (or re-apply) selection
|
|
604
|
+
self.setValue(self.param.val)
|
|
605
|
+
|
|
606
|
+
def getValue(self):
|
|
607
|
+
return self.choices[self.ctrl.GetSelection()]
|
|
608
|
+
|
|
609
|
+
def setValue(self, value):
|
|
610
|
+
if str(value) not in self.choices:
|
|
611
|
+
# if not known, add it to possible choices
|
|
612
|
+
self.choices.append(str(value))
|
|
613
|
+
# translate label if the value is not ''
|
|
614
|
+
if str(value) != '':
|
|
615
|
+
self.labels.append(_translate(str(value)))
|
|
384
616
|
else:
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
self.
|
|
390
|
-
|
|
391
|
-
)
|
|
392
|
-
# Don't use wx.Choice.SetStringSelection here because label string is localized.
|
|
393
|
-
wx.Choice.SetSelection(self, self.choices.index(string))
|
|
617
|
+
self.labels.append(str(value))
|
|
618
|
+
self.ctrl.SetItems(self.labels)
|
|
619
|
+
# set
|
|
620
|
+
self.ctrl.SetSelection(
|
|
621
|
+
self.choices.index(str(value))
|
|
622
|
+
)
|
|
394
623
|
|
|
395
|
-
def getValue(self):
|
|
396
|
-
# Don't use wx.Choice.GetStringSelection here because label string is localized.
|
|
397
|
-
return self.choices[self.GetSelection()]
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
class MultiChoiceCtrl(wx.CheckListBox, _ValidatorMixin, _HideMixin):
|
|
401
|
-
def __init__(self, parent, valType,
|
|
402
|
-
vals="", choices=[], fieldName="",
|
|
403
|
-
size=wx.Size(-1, -1)):
|
|
404
|
-
wx.CheckListBox.__init__(self)
|
|
405
|
-
self.Create(parent, id=wx.ID_ANY, size=size, choices=choices, name=fieldName, style=wx.LB_MULTIPLE)
|
|
406
|
-
self.valType = valType
|
|
407
|
-
self._choices = choices
|
|
408
|
-
# Make initial selection
|
|
409
|
-
if isinstance(vals, str):
|
|
410
|
-
# Convert to list if needed
|
|
411
|
-
vals = data.utils.listFromString(vals, excludeEmpties=True)
|
|
412
|
-
self.SetCheckedStrings(vals)
|
|
413
|
-
self.validate()
|
|
414
624
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
strings = [strings]
|
|
418
|
-
for s in strings:
|
|
419
|
-
if s not in self._choices:
|
|
420
|
-
self._choices.append(s)
|
|
421
|
-
self.SetItems(self._choices)
|
|
422
|
-
wx.CheckListBox.SetCheckedStrings(self, strings)
|
|
625
|
+
class MultiChoiceCtrl(ChoiceCtrl):
|
|
626
|
+
inputType = "multiChoice"
|
|
423
627
|
|
|
424
|
-
def
|
|
425
|
-
|
|
628
|
+
def makeCtrls(self):
|
|
629
|
+
self.ctrl = wx.CheckListBox(self, style=wx.LB_MULTIPLE)
|
|
630
|
+
self.sizer.Add(
|
|
631
|
+
self.ctrl, proportion=1, flag=wx.EXPAND | wx.ALL
|
|
632
|
+
)
|
|
633
|
+
# connect onChange
|
|
634
|
+
self.ctrl.Bind(
|
|
635
|
+
wx.EVT_CHECKLISTBOX, self.onChange
|
|
636
|
+
)
|
|
426
637
|
|
|
638
|
+
self.populate()
|
|
639
|
+
|
|
640
|
+
def getValue(self):
|
|
641
|
+
return [
|
|
642
|
+
self.choices[i] for i in self.ctrl.GetCheckedItems()
|
|
643
|
+
]
|
|
644
|
+
|
|
645
|
+
def setValue(self, value):
|
|
646
|
+
# coerce to list
|
|
647
|
+
value = data.utils.listFromString(value)
|
|
648
|
+
# iterate through values
|
|
649
|
+
selected = []
|
|
650
|
+
for val in value:
|
|
651
|
+
# if not known, add it to possible choices
|
|
652
|
+
if val not in self.choices:
|
|
653
|
+
self.choices.append(val)
|
|
654
|
+
self.labels.append(str(val))
|
|
655
|
+
self.ctrl.SetItems(self.labels)
|
|
656
|
+
# add index
|
|
657
|
+
selected.append(
|
|
658
|
+
self.choices.index(val)
|
|
659
|
+
)
|
|
660
|
+
# set
|
|
661
|
+
self.ctrl.SetCheckedItems(selected)
|
|
427
662
|
|
|
428
|
-
class RichChoiceCtrl(wx.Panel, _ValidatorMixin, _HideMixin):
|
|
429
|
-
class RichChoiceItem(wx.Panel):
|
|
430
|
-
def __init__(self, parent, value, label, body="", linkText="", link="", startShown="always", viewToggle=True):
|
|
431
|
-
# Initialise
|
|
432
|
-
wx.Panel.__init__(self, parent, style=wx.BORDER_THEME)
|
|
433
|
-
self.parent = parent
|
|
434
|
-
self.value = value
|
|
435
|
-
self.startShown = startShown
|
|
436
|
-
# Setup sizer
|
|
437
|
-
self.border = wx.BoxSizer()
|
|
438
|
-
self.SetSizer(self.border)
|
|
439
|
-
self.sizer = wx.FlexGridSizer(cols=3)
|
|
440
|
-
self.sizer.AddGrowableCol(idx=1, proportion=1)
|
|
441
|
-
self.border.Add(self.sizer, proportion=1, border=6, flag=wx.ALL | wx.EXPAND)
|
|
442
|
-
# Check
|
|
443
|
-
self.check = wx.CheckBox(self, label=" ")
|
|
444
|
-
self.check.Bind(wx.EVT_CHECKBOX, self.onCheck)
|
|
445
|
-
self.check.Bind(wx.EVT_KEY_UP, self.onToggle)
|
|
446
|
-
self.sizer.Add(self.check, border=3, flag=wx.ALL | wx.ALIGN_CENTER_VERTICAL)
|
|
447
|
-
# Title
|
|
448
|
-
self.title = wx.StaticText(self, label=label)
|
|
449
|
-
self.title.SetFont(self.title.GetFont().Bold())
|
|
450
|
-
self.sizer.Add(self.title, border=3, flag=wx.ALL | wx.EXPAND)
|
|
451
|
-
# Toggle
|
|
452
|
-
self.toggleView = wx.ToggleButton(self, style=wx.BU_EXACTFIT)
|
|
453
|
-
self.toggleView.Bind(wx.EVT_TOGGLEBUTTON, self.onToggleView)
|
|
454
|
-
self.toggleView.Show(viewToggle)
|
|
455
|
-
self.sizer.Add(self.toggleView, border=3, flag=wx.ALL | wx.EXPAND)
|
|
456
|
-
# Body
|
|
457
|
-
self.body = utils.WrappedStaticText(self, label=body)
|
|
458
|
-
self.sizer.AddStretchSpacer(1)
|
|
459
|
-
self.sizer.Add(self.body, border=3, proportion=1, flag=wx.ALL | wx.EXPAND)
|
|
460
|
-
self.sizer.AddStretchSpacer(1)
|
|
461
|
-
# Link
|
|
462
|
-
self.link = utils.HyperLinkCtrl(self, label=linkText, URL=link)
|
|
463
|
-
self.link.SetBackgroundColour(self.GetBackgroundColour())
|
|
464
|
-
self.sizer.AddStretchSpacer(1)
|
|
465
|
-
self.sizer.Add(self.link, border=3, flag=wx.ALL | wx.ALIGN_LEFT)
|
|
466
|
-
self.sizer.AddStretchSpacer(1)
|
|
467
663
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
self.body.SetBackgroundColour("white")
|
|
471
|
-
self.link.SetBackgroundColour("white")
|
|
664
|
+
class FileCtrl(SingleLineCtrl):
|
|
665
|
+
inputType = "file"
|
|
472
666
|
|
|
473
|
-
|
|
667
|
+
dlgWildcard = "All Files (*.*)|*.*"
|
|
668
|
+
icon = "folder"
|
|
669
|
+
dlgStyle = wx.FD_FILE_MUST_EXIST
|
|
474
670
|
|
|
475
|
-
|
|
476
|
-
|
|
671
|
+
def makeCtrls(self):
|
|
672
|
+
SingleLineCtrl.makeCtrls(self)
|
|
673
|
+
# add a file browse button
|
|
674
|
+
self.fileBtn = wx.Button(self, style=wx.BU_EXACTFIT)
|
|
675
|
+
self.fileBtn.SetBitmap(
|
|
676
|
+
icons.ButtonIcon(stem=self.icon, size=16, theme="light").bitmap
|
|
677
|
+
)
|
|
678
|
+
self.fileBtn.SetToolTip(
|
|
679
|
+
_translate("Browse for a file")
|
|
680
|
+
)
|
|
681
|
+
self.fileBtn.Bind(wx.EVT_BUTTON, self.openFileBrowser)
|
|
682
|
+
self.sizer.Add(
|
|
683
|
+
self.fileBtn, border=6, flag=wx.EXPAND | wx.LEFT
|
|
684
|
+
)
|
|
685
|
+
|
|
686
|
+
def styleValid(self):
|
|
687
|
+
# style as normal
|
|
688
|
+
SingleLineCtrl.styleValid(self)
|
|
689
|
+
# if not code, check for a link
|
|
690
|
+
if not self.isCode:
|
|
691
|
+
if stringtools.is_url(self.getValue()):
|
|
692
|
+
self.ctrl.SetForegroundColour(
|
|
693
|
+
colors.scheme['blue']
|
|
694
|
+
)
|
|
695
|
+
self.ctrl.Refresh()
|
|
696
|
+
|
|
697
|
+
@property
|
|
698
|
+
def rootDir(self):
|
|
699
|
+
# if no element, use system root
|
|
700
|
+
if self.element is None or not hasattr(self.element, "exp"):
|
|
701
|
+
return Path()
|
|
702
|
+
# otherwise, get from experiment
|
|
703
|
+
root = Path(self.element.exp.filename)
|
|
704
|
+
# move up a dir if root is a file
|
|
705
|
+
if root.is_file():
|
|
706
|
+
root = root.parent
|
|
707
|
+
|
|
708
|
+
return root
|
|
709
|
+
|
|
710
|
+
def openFileBrowser(self, evt=None):
|
|
711
|
+
# open a file browser dialog
|
|
712
|
+
dlg = wx.FileDialog(
|
|
713
|
+
self,
|
|
714
|
+
message=_translate("Specify file..."),
|
|
715
|
+
defaultDir=str(self.rootDir),
|
|
716
|
+
style=wx.FD_OPEN | self.dlgStyle,
|
|
717
|
+
wildcard=self.dlgWildcard
|
|
718
|
+
)
|
|
719
|
+
if dlg.ShowModal() != wx.ID_OK:
|
|
720
|
+
return
|
|
721
|
+
# get path
|
|
722
|
+
file = dlg.GetPath()
|
|
723
|
+
# relativise
|
|
724
|
+
try:
|
|
725
|
+
filename = Path(file).relative_to(self.rootDir)
|
|
726
|
+
except ValueError:
|
|
727
|
+
filename = Path(file).absolute()
|
|
728
|
+
# set value
|
|
729
|
+
self.setValue(
|
|
730
|
+
str(filename).replace("\\", "/")
|
|
731
|
+
)
|
|
732
|
+
|
|
733
|
+
def validate(self):
|
|
734
|
+
from psychopy.tools.filetools import defaultStim
|
|
735
|
+
# start off valid
|
|
736
|
+
BaseParamCtrl.validate(self)
|
|
737
|
+
# if given as code, use regular code checking
|
|
738
|
+
if self.isCode:
|
|
739
|
+
return SingleLineCtrl.validateCode(self)
|
|
740
|
+
# if given a link, it's valid
|
|
741
|
+
if stringtools.is_url(self.getValue()):
|
|
742
|
+
self.clearWarning()
|
|
743
|
+
return
|
|
744
|
+
# if blank, don't worry about it
|
|
745
|
+
if self.getValue() == "":
|
|
746
|
+
self.clearWarning()
|
|
747
|
+
return
|
|
748
|
+
# if it's a string, convert to file
|
|
749
|
+
try:
|
|
750
|
+
file = Path(self.getValue())
|
|
751
|
+
except:
|
|
752
|
+
# if it can't be a file at all, show warning
|
|
753
|
+
self.setWarning(_translate(
|
|
754
|
+
"Not a valid file path: {}"
|
|
755
|
+
).format(self.getValue()))
|
|
756
|
+
return
|
|
757
|
+
# make path absolute
|
|
758
|
+
if not file.is_absolute():
|
|
759
|
+
file = self.rootDir / file
|
|
760
|
+
# valid only if file exists
|
|
761
|
+
if all((
|
|
762
|
+
not file.is_file(),
|
|
763
|
+
file.name not in defaultStim
|
|
764
|
+
)):
|
|
765
|
+
self.setWarning(_translate(
|
|
766
|
+
"No file named {}"
|
|
767
|
+
).format(self.getValue()))
|
|
768
|
+
|
|
769
|
+
|
|
770
|
+
class SoundCtrl(FileCtrl):
|
|
771
|
+
inputType = "soundFile"
|
|
772
|
+
|
|
773
|
+
def validate(self):
|
|
774
|
+
from psychopy.tools.audiotools import knownNoteNames
|
|
775
|
+
# validate like a normal file
|
|
776
|
+
FileCtrl.validate(self)
|
|
777
|
+
# if given a note, this is fine
|
|
778
|
+
if str(self.getValue()).capitalize() in knownNoteNames:
|
|
779
|
+
self.clearWarning()
|
|
780
|
+
|
|
781
|
+
|
|
782
|
+
class TableCtrl(FileCtrl):
|
|
783
|
+
inputType = "table"
|
|
784
|
+
|
|
785
|
+
validExt = [
|
|
786
|
+
".csv", ".tsv", ".txt", ".xl", ".xlsx", ".xlsm", ".xlsb", ".xlam", ".xltx", ".xltm",
|
|
787
|
+
".xls", ".xlt", ".htm", ".html", ".mht", ".mhtml", ".xml", ".xla", ".xlm", ".odc", ".ods",
|
|
788
|
+
".udl", ".dsn", ".mdb", ".mde", ".accdb", ".accde", ".dbc", ".dbf", ".iqy", ".dqy", ".rqy",
|
|
789
|
+
".oqy", ".cub", ".atom", ".atomsvc", ".prn", ".slk", ".dif"
|
|
790
|
+
]
|
|
791
|
+
dlgWildcard = (
|
|
792
|
+
f"All Table Files({'*'+';*'.join(validExt)})"
|
|
793
|
+
f"|{'*'+';*'.join(validExt)}"
|
|
794
|
+
f"|All Files (*.*)"
|
|
795
|
+
f"|*.*"
|
|
796
|
+
)
|
|
797
|
+
|
|
798
|
+
def makeCtrls(self):
|
|
799
|
+
FileCtrl.makeCtrls(self)
|
|
800
|
+
# Add button to open in Excel
|
|
801
|
+
self.xlBtn = wx.Button(self, style=wx.BU_EXACTFIT)
|
|
802
|
+
self.xlBtn.SetBitmap(
|
|
803
|
+
icons.ButtonIcon(stem="filecsv", size=16, theme="light").bitmap
|
|
804
|
+
)
|
|
805
|
+
self.xlBtn.SetToolTip(
|
|
806
|
+
_translate("Open/create in your default table editor")
|
|
807
|
+
)
|
|
808
|
+
self.xlBtn.Bind(wx.EVT_BUTTON, self.openExcel)
|
|
809
|
+
self.sizer.Add(
|
|
810
|
+
self.xlBtn, border=6, flag=wx.EXPAND | wx.LEFT
|
|
811
|
+
)
|
|
812
|
+
# call initial onChange
|
|
813
|
+
self.onChange()
|
|
814
|
+
|
|
815
|
+
def onChange(self, evt=None):
|
|
816
|
+
FileCtrl.onChange(self, evt)
|
|
817
|
+
# if calling before finished initialising, skip
|
|
818
|
+
if not hasattr(self, "xlBtn"):
|
|
819
|
+
return
|
|
820
|
+
|
|
821
|
+
if not self.getValue().strip():
|
|
822
|
+
# if blank, enable/disable according to presence of template
|
|
823
|
+
self.xlBtn.Enable("template" in self.param.ctrlParams)
|
|
824
|
+
else:
|
|
825
|
+
# otherwise, enable/disable according to validity
|
|
826
|
+
self.xlBtn.Enable(self.isValid)
|
|
477
827
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
828
|
+
def openExcel(self, event):
|
|
829
|
+
"""
|
|
830
|
+
Either open the specified excel sheet, or make a new one from a template
|
|
831
|
+
"""
|
|
832
|
+
file = Path(self.getValue())
|
|
833
|
+
# make path absolute
|
|
834
|
+
if not file.is_absolute():
|
|
835
|
+
file = self.rootDir / file
|
|
836
|
+
# open a template if not a valid file
|
|
837
|
+
if file == self.rootDir or not (file.is_file() or file.suffix not in self.validExt):
|
|
838
|
+
dlg = wx.MessageDialog(self, _translate(
|
|
839
|
+
"Once you have created and saved your table, remember to add it here."
|
|
840
|
+
),
|
|
841
|
+
caption=_translate("Reminder")
|
|
842
|
+
)
|
|
843
|
+
dlg.ShowModal()
|
|
844
|
+
# get template
|
|
845
|
+
if "template" in self.param.ctrlParams:
|
|
846
|
+
file = self.param.ctrlParams['template']
|
|
847
|
+
# if template is specified as a method, call it now to get the value live
|
|
848
|
+
if callable(file):
|
|
849
|
+
file = file()
|
|
850
|
+
# convert to Path
|
|
851
|
+
file = Path(file)
|
|
852
|
+
else:
|
|
853
|
+
# use blank template if none given
|
|
854
|
+
file = Path(experiment.__file__).parent / 'blankTemplate.xltx',
|
|
855
|
+
# Open whatever file is used
|
|
856
|
+
try:
|
|
857
|
+
os.startfile(file)
|
|
858
|
+
except AttributeError:
|
|
859
|
+
opener = "open" if sys.platform == "darwin" else "xdg-open"
|
|
860
|
+
subprocess.call([opener, file])
|
|
495
861
|
|
|
496
|
-
def onCheck(self, evt):
|
|
497
|
-
self.setChecked(evt.IsChecked())
|
|
498
862
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
self.setChecked(not self.check.IsChecked())
|
|
863
|
+
class ConditionsCtrl(TableCtrl):
|
|
864
|
+
inputType = "conditions"
|
|
502
865
|
|
|
503
|
-
def onToggleView(self, evt):
|
|
504
|
-
# If called with a boolean, use it directly, otherwise get bool from event
|
|
505
|
-
if isinstance(evt, bool):
|
|
506
|
-
val = evt
|
|
507
|
-
else:
|
|
508
|
-
val = evt.IsChecked()
|
|
509
|
-
# Update toggle ctrl label
|
|
510
|
-
if val:
|
|
511
|
-
lbl = "⯆"
|
|
512
|
-
else:
|
|
513
|
-
lbl = "⯇"
|
|
514
|
-
self.toggleView.SetLabel(lbl)
|
|
515
|
-
# Show/hide body based on value
|
|
516
|
-
self.body.Show(val)
|
|
517
|
-
self.link.Show(val)
|
|
518
|
-
# Layout
|
|
519
|
-
self.Layout()
|
|
520
|
-
self.parent.parent.Layout() # layout params notebook page
|
|
521
866
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
choices=[], labels=[],
|
|
525
|
-
size=wx.Size(-1, -1),
|
|
526
|
-
viewToggle=True):
|
|
527
|
-
# Initialise
|
|
528
|
-
wx.Panel.__init__(self, parent, size=size)
|
|
529
|
-
self.parent = parent
|
|
530
|
-
self.valType = valType
|
|
531
|
-
self.fieldName = fieldName
|
|
532
|
-
self.multi = False
|
|
533
|
-
self.viewToggle = viewToggle
|
|
534
|
-
# Setup sizer
|
|
535
|
-
self.border = wx.BoxSizer()
|
|
536
|
-
self.SetSizer(self.border)
|
|
537
|
-
self.sizer = wx.BoxSizer(wx.VERTICAL)
|
|
538
|
-
self.border.Add(self.sizer, proportion=1, border=6, flag=wx.ALL | wx.EXPAND)
|
|
539
|
-
self.SetSizer(self.border)
|
|
540
|
-
# Store values
|
|
541
|
-
self.choices = {}
|
|
542
|
-
for i, val in enumerate(choices):
|
|
543
|
-
self.choices[val] = labels[i]
|
|
544
|
-
# Populate
|
|
545
|
-
self.populate()
|
|
546
|
-
# Set value
|
|
547
|
-
self.setValue(vals)
|
|
548
|
-
# Start off showing according to param
|
|
549
|
-
for obj in self.items:
|
|
550
|
-
# Work out if we should start out shown
|
|
551
|
-
if self.viewToggle:
|
|
552
|
-
if obj.startShown == "never":
|
|
553
|
-
startShown = False
|
|
554
|
-
elif obj.startShown == "checked":
|
|
555
|
-
startShown = obj.check.IsChecked()
|
|
556
|
-
elif obj.startShown == "unchecked":
|
|
557
|
-
startShown = not obj.check.IsChecked()
|
|
558
|
-
else:
|
|
559
|
-
startShown = True
|
|
560
|
-
else:
|
|
561
|
-
startShown = True
|
|
562
|
-
# Apply starting view
|
|
563
|
-
obj.toggleView.SetValue(startShown)
|
|
564
|
-
obj.onToggleView(startShown)
|
|
565
|
-
|
|
566
|
-
self.Layout()
|
|
567
|
-
|
|
568
|
-
def populate(self):
|
|
569
|
-
self.items = []
|
|
570
|
-
for val, label in self.choices.items():
|
|
571
|
-
if not isinstance(label, dict):
|
|
572
|
-
# Make sure label is dict
|
|
573
|
-
label = {"label": label}
|
|
574
|
-
# Add item control
|
|
575
|
-
self.addItem(val, label=label)
|
|
576
|
-
self.Layout()
|
|
577
|
-
|
|
578
|
-
def addItem(self, value, label={}):
|
|
579
|
-
# Create item object
|
|
580
|
-
item = self.RichChoiceItem(self, value=value, viewToggle=self.viewToggle, **label)
|
|
581
|
-
self.items.append(item)
|
|
582
|
-
# Add to sizer
|
|
583
|
-
self.sizer.Add(item, border=3, flag=wx.ALL | wx.EXPAND)
|
|
584
|
-
|
|
585
|
-
def getValue(self):
|
|
586
|
-
# Get corresponding value for each checked item
|
|
587
|
-
values = []
|
|
588
|
-
for item in self.items:
|
|
589
|
-
if item.getChecked():
|
|
590
|
-
# If checked, append value
|
|
591
|
-
values.append(item.value)
|
|
592
|
-
# Strip list if not multi
|
|
593
|
-
if not self.multi:
|
|
594
|
-
if len(values):
|
|
595
|
-
values = values[0]
|
|
596
|
-
else:
|
|
597
|
-
values = ""
|
|
598
|
-
|
|
599
|
-
return values
|
|
867
|
+
class SurveyCtrl(SingleLineCtrl):
|
|
868
|
+
inputType = "survey"
|
|
600
869
|
|
|
601
|
-
def setValue(self, value):
|
|
602
|
-
# Make sure value is iterable
|
|
603
|
-
if not isinstance(value, (list, tuple)):
|
|
604
|
-
value = [value]
|
|
605
|
-
# Check/uncheck corresponding items
|
|
606
|
-
for item in self.items:
|
|
607
|
-
state = item.value in value
|
|
608
|
-
item.check.SetValue(state)
|
|
609
|
-
|
|
610
|
-
# Post event
|
|
611
|
-
evt = wx.ListEvent(commandType=wx.EVT_CHOICE.typeId, id=-1)
|
|
612
|
-
evt.SetEventObject(self)
|
|
613
|
-
wx.PostEvent(self, evt)
|
|
614
|
-
|
|
615
|
-
self.Layout()
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
class FileCtrl(wx.TextCtrl, _ValidatorMixin, _HideMixin, _FileMixin):
|
|
619
|
-
def __init__(self, parent, valType,
|
|
620
|
-
val="", fieldName="",
|
|
621
|
-
size=wx.Size(-1, 24)):
|
|
622
|
-
# Create self
|
|
623
|
-
wx.TextCtrl.__init__(self)
|
|
624
|
-
self.Create(parent, -1, val, name=fieldName, size=size)
|
|
625
|
-
self.valType = valType
|
|
626
|
-
# Add sizer
|
|
627
|
-
self._szr = wx.BoxSizer(wx.HORIZONTAL)
|
|
628
|
-
self._szr.Add(self, border=5, proportion=1, flag=wx.EXPAND | wx.RIGHT)
|
|
629
|
-
# Add button to browse for file
|
|
630
|
-
fldr = icons.ButtonIcon(stem="folder", size=16, theme="light").bitmap
|
|
631
|
-
self.findBtn = wx.BitmapButton(parent, -1, bitmap=fldr, style=wx.BU_EXACTFIT)
|
|
632
|
-
self.findBtn.SetToolTip(_translate("Specify file ..."))
|
|
633
|
-
self.findBtn.Bind(wx.EVT_BUTTON, self.findFile)
|
|
634
|
-
self._szr.Add(self.findBtn)
|
|
635
|
-
# Configure validation
|
|
636
|
-
self.Bind(wx.EVT_TEXT, self.validate)
|
|
637
|
-
self.validate()
|
|
638
|
-
|
|
639
|
-
def findFile(self, evt):
|
|
640
|
-
file = self.getFile()
|
|
641
|
-
if file:
|
|
642
|
-
self.setFile(file)
|
|
643
|
-
self.validate(evt)
|
|
644
|
-
|
|
645
|
-
def setFile(self, file):
|
|
646
|
-
# Set text value
|
|
647
|
-
wx.TextCtrl.SetValue(self, file)
|
|
648
|
-
# Post event
|
|
649
|
-
evt = wx.FileDirPickerEvent(wx.EVT_FILEPICKER_CHANGED.typeId, self, -1, file)
|
|
650
|
-
evt.SetEventObject(self)
|
|
651
|
-
wx.PostEvent(self, evt)
|
|
652
|
-
# Post keypress event to trigger onchange
|
|
653
|
-
evt = wx.FileDirPickerEvent(wx.EVT_KEY_UP.typeId, self, -1, file)
|
|
654
|
-
evt.SetEventObject(self)
|
|
655
|
-
wx.PostEvent(self, evt)
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
class FileListCtrl(wx.ListBox, _ValidatorMixin, _HideMixin, _FileMixin):
|
|
659
|
-
def __init__(self, parent, valType,
|
|
660
|
-
choices=[], size=None, pathtype="rel"):
|
|
661
|
-
wx.ListBox.__init__(self)
|
|
662
|
-
self.valType = valType
|
|
663
|
-
parent.Bind(wx.EVT_DROP_FILES, self.addItem)
|
|
664
|
-
self.app = parent.app
|
|
665
|
-
if type(choices) == str:
|
|
666
|
-
choices = data.utils.listFromString(choices)
|
|
667
|
-
self.Create(id=wx.ID_ANY, parent=parent, choices=choices, size=size, style=wx.LB_EXTENDED | wx.LB_HSCROLL)
|
|
668
|
-
self.addCustomBtn = wx.Button(parent, -1, size=(24,24), style=wx.BU_EXACTFIT, label="...")
|
|
669
|
-
self.addCustomBtn.Bind(wx.EVT_BUTTON, self.addCustomItem)
|
|
670
|
-
self.addBtn = wx.Button(parent, -1, size=(24,24), style=wx.BU_EXACTFIT, label="+")
|
|
671
|
-
self.addBtn.Bind(wx.EVT_BUTTON, self.addItem)
|
|
672
|
-
self.subBtn = wx.Button(parent, -1, size=(24,24), style=wx.BU_EXACTFIT, label="-")
|
|
673
|
-
self.subBtn.Bind(wx.EVT_BUTTON, self.removeItem)
|
|
674
|
-
self._szr = wx.BoxSizer(wx.HORIZONTAL)
|
|
675
|
-
self.btns = wx.BoxSizer(wx.VERTICAL)
|
|
676
|
-
self.btns.AddMany((self.addCustomBtn, self.addBtn, self.subBtn))
|
|
677
|
-
self._szr.Add(self, proportion=1, flag=wx.EXPAND)
|
|
678
|
-
self._szr.Add(self.btns)
|
|
679
|
-
|
|
680
|
-
def addItem(self, event):
|
|
681
|
-
# Get files
|
|
682
|
-
if event.GetEventObject() == self.addBtn:
|
|
683
|
-
fileList = self.getFiles()
|
|
684
|
-
else:
|
|
685
|
-
fileList = event.GetFiles()
|
|
686
|
-
for i, filename in enumerate(fileList):
|
|
687
|
-
try:
|
|
688
|
-
fileList[i] = Path(filename).relative_to(self.rootDir)
|
|
689
|
-
except ValueError:
|
|
690
|
-
fileList[i] = Path(filename).absolute()
|
|
691
|
-
# Add files to list
|
|
692
|
-
if fileList:
|
|
693
|
-
self.InsertItems(fileList, 0)
|
|
694
|
-
|
|
695
|
-
def removeItem(self, event):
|
|
696
|
-
i = self.GetSelections()
|
|
697
|
-
if isinstance(i, int):
|
|
698
|
-
i = [i]
|
|
699
|
-
items = [item for index, item in enumerate(self.Items)
|
|
700
|
-
if index not in i]
|
|
701
|
-
self.SetItems(items)
|
|
702
|
-
|
|
703
|
-
def addCustomItem(self, event):
|
|
704
|
-
# Create string dialog
|
|
705
|
-
dlg = wx.TextEntryDialog(parent=self, message=_translate("Add custom item"))
|
|
706
|
-
# Show dialog
|
|
707
|
-
if dlg.ShowModal() != wx.ID_OK:
|
|
708
|
-
return
|
|
709
|
-
# Get string
|
|
710
|
-
stringEntry = dlg.GetValue()
|
|
711
|
-
# Add to list
|
|
712
|
-
if stringEntry:
|
|
713
|
-
self.InsertItems([stringEntry], 0)
|
|
714
|
-
|
|
715
|
-
def GetValue(self):
|
|
716
|
-
return self.Items
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
class SurveyCtrl(wx.TextCtrl, _ValidatorMixin, _HideMixin):
|
|
720
870
|
class SurveyFinderDlg(wx.Dialog, utils.ButtonSizerMixin):
|
|
721
871
|
def __init__(self, parent, session):
|
|
722
872
|
wx.Dialog.__init__(self, parent=parent, size=(-1, 496), style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
|
|
@@ -806,23 +956,15 @@ class SurveyCtrl(wx.TextCtrl, _ValidatorMixin, _HideMixin):
|
|
|
806
956
|
else:
|
|
807
957
|
return ""
|
|
808
958
|
|
|
809
|
-
def
|
|
810
|
-
|
|
811
|
-
size=wx.Size(-1, 24)):
|
|
812
|
-
# Create self
|
|
813
|
-
wx.TextCtrl.__init__(self)
|
|
814
|
-
self.Create(parent, -1, val, name=fieldName, size=size)
|
|
815
|
-
self.valType = valType
|
|
959
|
+
def makeCtrls(self):
|
|
960
|
+
SingleLineCtrl.makeCtrls(self)
|
|
816
961
|
# Add CTRL + click behaviour
|
|
817
|
-
self.Bind(wx.EVT_RIGHT_DOWN, self.onRightClick)
|
|
818
|
-
#
|
|
819
|
-
self.SetHint("e.g. e89cd6eb-296e-4960-af14-103026a59c14")
|
|
820
|
-
# Add sizer
|
|
821
|
-
self._szr = wx.BoxSizer(wx.HORIZONTAL)
|
|
822
|
-
self._szr.Add(self, border=5, proportion=1, flag=wx.EXPAND | wx.RIGHT)
|
|
962
|
+
self.ctrl.Bind(wx.EVT_RIGHT_DOWN, self.onRightClick)
|
|
963
|
+
# add placeholder
|
|
964
|
+
self.ctrl.SetHint("e.g. e89cd6eb-296e-4960-af14-103026a59c14")
|
|
823
965
|
# Add button to browse for survey
|
|
824
966
|
self.findBtn = wx.Button(
|
|
825
|
-
|
|
967
|
+
self, -1,
|
|
826
968
|
label=_translate("Find online..."),
|
|
827
969
|
size=wx.Size(-1, 24)
|
|
828
970
|
)
|
|
@@ -833,10 +975,7 @@ class SurveyCtrl(wx.TextCtrl, _ValidatorMixin, _HideMixin):
|
|
|
833
975
|
"Get survey ID from a list of your surveys on Pavlovia"
|
|
834
976
|
))
|
|
835
977
|
self.findBtn.Bind(wx.EVT_BUTTON, self.findSurvey)
|
|
836
|
-
self.
|
|
837
|
-
# Configure validation
|
|
838
|
-
self.Bind(wx.EVT_TEXT, self.validate)
|
|
839
|
-
self.validate()
|
|
978
|
+
self.sizer.Add(self.findBtn, border=6, flag=wx.LEFT)
|
|
840
979
|
|
|
841
980
|
def onRightClick(self, evt=None):
|
|
842
981
|
menu = wx.Menu()
|
|
@@ -864,9 +1003,7 @@ class SurveyCtrl(wx.TextCtrl, _ValidatorMixin, _HideMixin):
|
|
|
864
1003
|
dlg = self.SurveyFinderDlg(self, session)
|
|
865
1004
|
if dlg.ShowModal() == wx.ID_OK:
|
|
866
1005
|
# If OK, get value
|
|
867
|
-
self.SetValue(dlg.getValue())
|
|
868
|
-
# Validate
|
|
869
|
-
self.validate()
|
|
1006
|
+
self.ctrl.SetValue(dlg.getValue())
|
|
870
1007
|
# Raise event
|
|
871
1008
|
evt = wx.ListEvent(wx.EVT_KEY_UP.typeId)
|
|
872
1009
|
evt.SetEventObject(self)
|
|
@@ -878,7 +1015,7 @@ class SurveyCtrl(wx.TextCtrl, _ValidatorMixin, _HideMixin):
|
|
|
878
1015
|
we only take the survey ID
|
|
879
1016
|
"""
|
|
880
1017
|
# Get value by usual wx method
|
|
881
|
-
value = self.GetValue()
|
|
1018
|
+
value = self.ctrl.GetValue()
|
|
882
1019
|
# Strip pavlovia run url
|
|
883
1020
|
if "run.pavlovia.org/pavlovia/survey/?surveyId=" in value:
|
|
884
1021
|
# Keep only the values after the URL
|
|
@@ -897,298 +1034,46 @@ class SurveyCtrl(wx.TextCtrl, _ValidatorMixin, _HideMixin):
|
|
|
897
1034
|
return value
|
|
898
1035
|
|
|
899
1036
|
|
|
900
|
-
class
|
|
901
|
-
|
|
902
|
-
size=wx.Size(-1, 24)):
|
|
903
|
-
# get val and val type
|
|
904
|
-
val = param.val
|
|
905
|
-
valType = param.valType
|
|
906
|
-
# store param
|
|
907
|
-
self.param = param
|
|
908
|
-
# Create self
|
|
909
|
-
wx.TextCtrl.__init__(self)
|
|
910
|
-
self.Create(parent, -1, val, name=fieldName, size=size)
|
|
911
|
-
self.valType = valType
|
|
912
|
-
# Add sizer
|
|
913
|
-
self._szr = wx.BoxSizer(wx.HORIZONTAL)
|
|
914
|
-
self._szr.Add(self, proportion=1, border=5, flag=wx.EXPAND | wx.RIGHT)
|
|
915
|
-
# Add button to browse for file
|
|
916
|
-
fldr = icons.ButtonIcon(stem="folder", size=16, theme="light").bitmap
|
|
917
|
-
self.findBtn = wx.BitmapButton(parent, -1, bitmap=fldr, style=wx.BU_EXACTFIT)
|
|
918
|
-
self.findBtn.SetToolTip(_translate("Specify file ..."))
|
|
919
|
-
self.findBtn.Bind(wx.EVT_BUTTON, self.findFile)
|
|
920
|
-
self._szr.Add(self.findBtn)
|
|
921
|
-
# Add button to open in Excel
|
|
922
|
-
xl = icons.ButtonIcon(stem="filecsv", size=16, theme="light").bitmap
|
|
923
|
-
self.xlBtn = wx.BitmapButton(parent, -1, bitmap=xl, style=wx.BU_EXACTFIT)
|
|
924
|
-
self.xlBtn.SetToolTip(_translate("Open/create in your default table editor"))
|
|
925
|
-
self.xlBtn.Bind(wx.EVT_BUTTON, self.openExcel)
|
|
926
|
-
self._szr.Add(self.xlBtn)
|
|
927
|
-
# Specify valid extensions
|
|
928
|
-
self.validExt = [".csv",".tsv",".txt",
|
|
929
|
-
".xl",".xlsx",".xlsm",".xlsb",".xlam",".xltx",".xltm",".xls",".xlt",
|
|
930
|
-
".htm",".html",".mht",".mhtml",
|
|
931
|
-
".xml",".xla",".xlm",
|
|
932
|
-
".odc",".ods",
|
|
933
|
-
".udl",".dsn",".mdb",".mde",".accdb",".accde",".dbc",".dbf",
|
|
934
|
-
".iqy",".dqy",".rqy",".oqy",
|
|
935
|
-
".cub",".atom",".atomsvc",
|
|
936
|
-
".prn",".slk",".dif"]
|
|
937
|
-
# Configure validation
|
|
938
|
-
self.Bind(wx.EVT_TEXT, self.validate)
|
|
939
|
-
self.validate()
|
|
940
|
-
|
|
941
|
-
def validate(self, evt=None):
|
|
942
|
-
"""Redirect validate calls to global validate method, assigning appropriate valType"""
|
|
943
|
-
validate(self, "file")
|
|
944
|
-
# if field is blank, enable/diable according to whether there's a template
|
|
945
|
-
if not self.GetValue().strip():
|
|
946
|
-
self.xlBtn.Enable("template" in self.param.ctrlParams)
|
|
947
|
-
# otherwise, enable/disable according to validity
|
|
948
|
-
else:
|
|
949
|
-
self.xlBtn.Enable(self.valid)
|
|
950
|
-
# if value isn't known until runtime, always disable Excel button
|
|
951
|
-
if "$" in self.GetValue():
|
|
952
|
-
self.xlBtn.Disable()
|
|
1037
|
+
class ColorCtrl(SingleLineCtrl):
|
|
1038
|
+
inputType = "color"
|
|
953
1039
|
|
|
954
|
-
def
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
caption=_translate("Reminder"))
|
|
962
|
-
dlg.ShowModal()
|
|
963
|
-
# get template
|
|
964
|
-
if "template" in self.param.ctrlParams:
|
|
965
|
-
file = self.param.ctrlParams['template']
|
|
966
|
-
# if template is specified as a method, call it now to get the value live
|
|
967
|
-
if callable(file):
|
|
968
|
-
file = file()
|
|
969
|
-
# convert to Path
|
|
970
|
-
file = Path(file)
|
|
971
|
-
else:
|
|
972
|
-
# use blank template if none given
|
|
973
|
-
file = Path(experiment.__file__).parent / 'blankTemplate.xltx',
|
|
974
|
-
# Open whatever file is used
|
|
975
|
-
try:
|
|
976
|
-
os.startfile(file)
|
|
977
|
-
except AttributeError:
|
|
978
|
-
opener = "open" if sys.platform == "darwin" else "xdg-open"
|
|
979
|
-
subprocess.call([opener, file])
|
|
980
|
-
|
|
981
|
-
def findFile(self, event):
|
|
982
|
-
_wld = f"All Table Files({'*'+';*'.join(self.validExt)})|{'*'+';*'.join(self.validExt)}|All Files (*.*)|*.*"
|
|
983
|
-
file = self.getFile(msg="Specify table file ...", wildcard=_wld)
|
|
984
|
-
if file:
|
|
985
|
-
FileCtrl.setFile(self, file)
|
|
986
|
-
self.validate(event)
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
class ColorCtrl(wx.TextCtrl, _ValidatorMixin, _HideMixin):
|
|
990
|
-
def __init__(self, parent, valType,
|
|
991
|
-
val="", fieldName="",
|
|
992
|
-
size=wx.Size(-1, 24)):
|
|
993
|
-
# Create self
|
|
994
|
-
wx.TextCtrl.__init__(self)
|
|
995
|
-
self.Create(parent, -1, val, name=fieldName, size=size)
|
|
996
|
-
self.valType = valType
|
|
997
|
-
# Add sizer
|
|
998
|
-
self._szr = wx.BoxSizer(wx.HORIZONTAL)
|
|
999
|
-
if valType == "code":
|
|
1000
|
-
# Add $ for anything to be interpreted verbatim
|
|
1001
|
-
self.dollarLbl = wx.StaticText(parent, -1, "$", size=wx.Size(-1, -1), style=wx.ALIGN_RIGHT)
|
|
1002
|
-
self.dollarLbl.SetToolTip(_translate("This parameter will be treated as code - we have already put in the $, so you don't have to."))
|
|
1003
|
-
self._szr.Add(self.dollarLbl, border=5, flag=wx.ALIGN_CENTER_VERTICAL | wx.RIGHT | wx.LEFT)
|
|
1004
|
-
# Add ctrl to sizer
|
|
1005
|
-
self._szr.Add(self, proportion=1, border=5, flag=wx.EXPAND | wx.RIGHT)
|
|
1006
|
-
# Add button to activate color picker
|
|
1007
|
-
fldr = icons.ButtonIcon(stem="color", size=16, theme="light").bitmap
|
|
1008
|
-
self.pickerBtn = wx.BitmapButton(parent, -1, bitmap=fldr, style=wx.BU_EXACTFIT)
|
|
1040
|
+
def makeCtrls(self):
|
|
1041
|
+
SingleLineCtrl.makeCtrls(self)
|
|
1042
|
+
# add button to activate color picker
|
|
1043
|
+
self.pickerBtn = wx.Button(self, style=wx.BU_EXACTFIT)
|
|
1044
|
+
self.pickerBtn.SetBitmap(
|
|
1045
|
+
icons.ButtonIcon(stem="color", size=16, theme="light").bitmap
|
|
1046
|
+
)
|
|
1009
1047
|
self.pickerBtn.SetToolTip(_translate("Specify color ..."))
|
|
1010
1048
|
self.pickerBtn.Bind(wx.EVT_BUTTON, self.colorPicker)
|
|
1011
|
-
self.
|
|
1012
|
-
# Bind to validation
|
|
1013
|
-
self.Bind(wx.EVT_CHAR, self.validate)
|
|
1014
|
-
self.validate()
|
|
1049
|
+
self.sizer.Add(self.pickerBtn)
|
|
1015
1050
|
|
|
1016
1051
|
def colorPicker(self, evt):
|
|
1052
|
+
# show color picker
|
|
1017
1053
|
dlg = PsychoColorPicker(self, context=self, allowCopy=False) # open a color picker
|
|
1018
|
-
dlg.ShowModal()
|
|
1054
|
+
ret = dlg.ShowModal()
|
|
1055
|
+
if ret == wx.ID_OK:
|
|
1056
|
+
self.setValue(
|
|
1057
|
+
f"$({dlg.getOutputValue()})"
|
|
1058
|
+
)
|
|
1059
|
+
else:
|
|
1060
|
+
pass
|
|
1019
1061
|
dlg.Destroy()
|
|
1020
1062
|
|
|
1021
1063
|
|
|
1022
|
-
def validate(obj, valType):
|
|
1023
|
-
val = str(obj.GetValue())
|
|
1024
|
-
valid = True
|
|
1025
|
-
if val.startswith("$"):
|
|
1026
|
-
# If indicated as code, treat as code
|
|
1027
|
-
valType = "code"
|
|
1028
|
-
# Validate string
|
|
1029
|
-
if valType == "str":
|
|
1030
|
-
if re.findall(r"(?<!\\)\"", val):
|
|
1031
|
-
# If there are unescaped "
|
|
1032
|
-
valid = False
|
|
1033
|
-
if re.findall(r"(?<!\\)\'", val):
|
|
1034
|
-
# If there are unescaped '
|
|
1035
|
-
valid = False
|
|
1036
|
-
# Validate code
|
|
1037
|
-
if valType == "code":
|
|
1038
|
-
# Replace unescaped curly quotes
|
|
1039
|
-
if re.findall(r"(?<!\\)[\u201c\u201d]", val):
|
|
1040
|
-
pt = obj.GetInsertionPoint()
|
|
1041
|
-
obj.SetValue(re.sub(r"(?<!\\)[\u201c\u201d]", "\"", val))
|
|
1042
|
-
obj.SetInsertionPoint(pt)
|
|
1043
|
-
# For now, ignore
|
|
1044
|
-
pass
|
|
1045
|
-
# Validate num
|
|
1046
|
-
if valType in ["num", "int"]:
|
|
1047
|
-
try:
|
|
1048
|
-
# Try to convert value to a float
|
|
1049
|
-
float(val)
|
|
1050
|
-
except ValueError:
|
|
1051
|
-
# If conversion fails, value is invalid
|
|
1052
|
-
valid = False
|
|
1053
|
-
# Validate bool
|
|
1054
|
-
if valType == "bool":
|
|
1055
|
-
if val not in ["True", "False"]:
|
|
1056
|
-
# If value is not True or False, it is invalid
|
|
1057
|
-
valid = False
|
|
1058
|
-
# Validate list
|
|
1059
|
-
if valType == "list":
|
|
1060
|
-
empty = not bool(val) # Is value empty?
|
|
1061
|
-
fullList = re.fullmatch(r"[\(\[].*[\]\)]", val) # Is value full list with parentheses?
|
|
1062
|
-
partList = "," in val and not re.match(r"[\(\[].*[\]\)]", val) # Is value list without parentheses?
|
|
1063
|
-
singleVal = not " " in val or re.match(r"[\"\'].*[\"\']", val) # Is value a single value?
|
|
1064
|
-
if not any([empty, fullList, partList, singleVal]):
|
|
1065
|
-
# If value is not any of valid types, it is invalid
|
|
1066
|
-
valid = False
|
|
1067
|
-
# Validate color
|
|
1068
|
-
if valType == "color":
|
|
1069
|
-
# Strip function calls
|
|
1070
|
-
if re.fullmatch(r"\$?(Advanced)?Color\(.*\)", val):
|
|
1071
|
-
val = re.sub(r"\$?(Advanced)?Color\(", "", val[:-1])
|
|
1072
|
-
try:
|
|
1073
|
-
# Try to create a Color object from value
|
|
1074
|
-
obj.color = Color(val, False)
|
|
1075
|
-
if not obj.color:
|
|
1076
|
-
# If invalid object is created, input is invalid
|
|
1077
|
-
valid = False
|
|
1078
|
-
except:
|
|
1079
|
-
# If object creation fails, input is invalid
|
|
1080
|
-
valid = False
|
|
1081
|
-
if valType == "file":
|
|
1082
|
-
val = Path(str(val))
|
|
1083
|
-
if not val.is_absolute():
|
|
1084
|
-
frame = obj.GetTopLevelParent()
|
|
1085
|
-
if hasattr(frame, "frame"):
|
|
1086
|
-
frame = frame.frame
|
|
1087
|
-
# If not an absolute path, append to current directory
|
|
1088
|
-
val = Path(frame.filename).parent / val
|
|
1089
|
-
if not val.is_file():
|
|
1090
|
-
# Is value a valid filepath?
|
|
1091
|
-
valid = False
|
|
1092
|
-
if hasattr(obj, "validExt"):
|
|
1093
|
-
# If control has specified list of ext, does value end in correct ext?
|
|
1094
|
-
if val.suffix not in obj.validExt:
|
|
1095
|
-
valid = False
|
|
1096
|
-
|
|
1097
|
-
# If additional allowed values are defined, override validation
|
|
1098
|
-
if hasattr(obj, "allowedVals"):
|
|
1099
|
-
if val in obj.allowedVals:
|
|
1100
|
-
valid = True
|
|
1101
|
-
|
|
1102
|
-
# Apply valid status to object
|
|
1103
|
-
obj.valid = valid
|
|
1104
|
-
if hasattr(obj, "showValid"):
|
|
1105
|
-
obj.showValid(valid)
|
|
1106
|
-
|
|
1107
|
-
# Update code font
|
|
1108
|
-
obj.updateCodeFont(valType)
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
class DictCtrl(ListWidget, _ValidatorMixin, _HideMixin):
|
|
1112
|
-
def __init__(self, parent,
|
|
1113
|
-
val={}, labels=(_translate("Field"), _translate("Default")), valType='dict',
|
|
1114
|
-
fieldName=""):
|
|
1115
|
-
# try to convert to a dict if given a string
|
|
1116
|
-
if isinstance(val, str):
|
|
1117
|
-
try:
|
|
1118
|
-
val = ast.literal_eval(val)
|
|
1119
|
-
except:
|
|
1120
|
-
raise ValueError(_translate("Could not interpret parameter value as a dict:\n{}").format(val))
|
|
1121
|
-
# raise error if still not a dict
|
|
1122
|
-
if not isinstance(val, (dict, list)):
|
|
1123
|
-
raise ValueError("DictCtrl must be supplied with either a dict or a list of 1-long dicts, value supplied was {}: {}".format(type(val), val))
|
|
1124
|
-
# Get labels
|
|
1125
|
-
keyLbl, valLbl = labels
|
|
1126
|
-
# If supplied with a dict, convert it to a list of dicts
|
|
1127
|
-
if isinstance(val, dict):
|
|
1128
|
-
newVal = []
|
|
1129
|
-
for key, v in val.items():
|
|
1130
|
-
if hasattr(v, "val"):
|
|
1131
|
-
v = v.val
|
|
1132
|
-
newVal.append({keyLbl: key, valLbl: v})
|
|
1133
|
-
val = newVal
|
|
1134
|
-
# Make sure we have at least 1 value
|
|
1135
|
-
if not len(val):
|
|
1136
|
-
val = [{keyLbl: "", valLbl: ""}]
|
|
1137
|
-
# If any items within the list are not dicts or are dicts longer than 1, throw error
|
|
1138
|
-
if not all(isinstance(v, dict) and len(v) == 2 for v in val):
|
|
1139
|
-
raise ValueError("DictCtrl must be supplied with either a dict or a list of 1-long dicts, value supplied was {}".format(val))
|
|
1140
|
-
# Create ListWidget
|
|
1141
|
-
ListWidget.__init__(self, parent, val, order=labels)
|
|
1142
|
-
|
|
1143
|
-
def SetForegroundColour(self, color):
|
|
1144
|
-
for child in self.Children:
|
|
1145
|
-
if hasattr(child, "SetForegroundColour"):
|
|
1146
|
-
child.SetForegroundColour(color)
|
|
1147
|
-
|
|
1148
|
-
def Enable(self, enable=True):
|
|
1149
|
-
"""
|
|
1150
|
-
Enable or disable all items in the dict ctrl
|
|
1151
|
-
"""
|
|
1152
|
-
# Iterate through all children
|
|
1153
|
-
for cell in self.Children:
|
|
1154
|
-
# Get the actual child rather than the sizer item
|
|
1155
|
-
child = cell.Window
|
|
1156
|
-
# If it can be enabled/disabled, enable/disable it
|
|
1157
|
-
if hasattr(child, "Enable"):
|
|
1158
|
-
child.Enable(enable)
|
|
1159
|
-
|
|
1160
|
-
def Disable(self):
|
|
1161
|
-
"""
|
|
1162
|
-
Disable all items in the dict ctrl
|
|
1163
|
-
"""
|
|
1164
|
-
self.Enable(False)
|
|
1165
|
-
|
|
1166
|
-
def Show(self, show=True):
|
|
1167
|
-
"""
|
|
1168
|
-
Show or hide all items in the dict ctrl
|
|
1169
|
-
"""
|
|
1170
|
-
# Iterate through all children
|
|
1171
|
-
for cell in self.Children:
|
|
1172
|
-
# Get the actual child rather than the sizer item
|
|
1173
|
-
child = cell.Window
|
|
1174
|
-
# If it can be shown/hidden, show/hide it
|
|
1175
|
-
if hasattr(child, "Show"):
|
|
1176
|
-
child.Show(show)
|
|
1177
|
-
|
|
1178
|
-
def Hide(self):
|
|
1179
|
-
"""
|
|
1180
|
-
Hide all items in the dict ctrl
|
|
1181
|
-
"""
|
|
1182
|
-
self.Show(False)
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
1064
|
class FontCtrl(SingleLineCtrl):
|
|
1186
|
-
|
|
1065
|
+
inputType = "font"
|
|
1066
|
+
|
|
1067
|
+
def onElementOk(self, evt=None):
|
|
1187
1068
|
# get a font manager
|
|
1188
1069
|
from psychopy.tools.fontmanager import FontManager, MissingFontError
|
|
1189
1070
|
fm = FontManager()
|
|
1190
1071
|
# check whether the font is installed
|
|
1191
|
-
|
|
1072
|
+
if self.element and hasattr(self.element, "exp") and self.element.exp.filename:
|
|
1073
|
+
currentDir = Path(self.element.exp.filename).parent
|
|
1074
|
+
else:
|
|
1075
|
+
currentDir = Path(".")
|
|
1076
|
+
installed = fm.getFontsMatching(self.getValue(), fallback=False, currentDir=currentDir)
|
|
1192
1077
|
# if not installed, ask the user whether to download from Google Fonts
|
|
1193
1078
|
if not installed:
|
|
1194
1079
|
# create dialog
|
|
@@ -1196,19 +1081,19 @@ class FontCtrl(SingleLineCtrl):
|
|
|
1196
1081
|
self.GetTopLevelParent(),
|
|
1197
1082
|
_translate(
|
|
1198
1083
|
"Font {} is not installed, would you like to download it from Google Fonts?"
|
|
1199
|
-
).format(self.
|
|
1084
|
+
).format(self.getValue()),
|
|
1200
1085
|
style=wx.YES|wx.NO|wx.ICON_QUESTION
|
|
1201
1086
|
)
|
|
1202
1087
|
# download if yes
|
|
1203
1088
|
if dlg.ShowModal() == wx.ID_YES:
|
|
1204
1089
|
try:
|
|
1205
|
-
fm.addGoogleFont(self.
|
|
1090
|
+
fm.addGoogleFont(self.getValue().strip())
|
|
1206
1091
|
except MissingFontError as err:
|
|
1207
1092
|
dlg = wx.MessageDialog(
|
|
1208
1093
|
self.GetTopLevelParent(),
|
|
1209
1094
|
_translate(
|
|
1210
1095
|
"Could not download font {} from Google Fonts, reason: {}"
|
|
1211
|
-
).format(self.
|
|
1096
|
+
).format(self.getValue(), err),
|
|
1212
1097
|
style=wx.OK|wx.ICON_ERROR
|
|
1213
1098
|
)
|
|
1214
1099
|
dlg.ShowModal()
|
|
@@ -1221,3 +1106,682 @@ class FontCtrl(SingleLineCtrl):
|
|
|
1221
1106
|
style=wx.OK|wx.ICON_INFORMATION
|
|
1222
1107
|
)
|
|
1223
1108
|
dlg.ShowModal()
|
|
1109
|
+
|
|
1110
|
+
|
|
1111
|
+
class CodeCtrl(BaseParamCtrl, handlers.ThemeMixin):
|
|
1112
|
+
inputType = "code"
|
|
1113
|
+
|
|
1114
|
+
def makeCtrls(self):
|
|
1115
|
+
self.ctrl = CodeBox(
|
|
1116
|
+
self, wx.ID_ANY, prefs,
|
|
1117
|
+
pos=wx.DefaultPosition, size=(-1, 128), style=wx.DEFAULT
|
|
1118
|
+
)
|
|
1119
|
+
self.sizer.Add(
|
|
1120
|
+
self.ctrl, proportion=1, flag=wx.EXPAND | wx.ALL
|
|
1121
|
+
)
|
|
1122
|
+
# hide margin
|
|
1123
|
+
self.ctrl.SetMarginWidth(0, 0)
|
|
1124
|
+
# set initial value
|
|
1125
|
+
self.setValue(self.param.val)
|
|
1126
|
+
# setup auto indent behaviour as in Code component
|
|
1127
|
+
self.ctrl.Bind(wx.EVT_KEY_DOWN, self.onChange)
|
|
1128
|
+
|
|
1129
|
+
def getValue(self):
|
|
1130
|
+
return self.ctrl.GetText()
|
|
1131
|
+
|
|
1132
|
+
def setValue(self, value):
|
|
1133
|
+
# get insertion point if possible
|
|
1134
|
+
pt = self.ctrl.GetInsertionPoint()
|
|
1135
|
+
# set value
|
|
1136
|
+
self.ctrl.SetText(str(value))
|
|
1137
|
+
# restore insertion point if possible
|
|
1138
|
+
try:
|
|
1139
|
+
self.ctrl.SetInsertionPoint(pt)
|
|
1140
|
+
except:
|
|
1141
|
+
pass
|
|
1142
|
+
|
|
1143
|
+
def onChange(self, evt=None):
|
|
1144
|
+
CodeBox.OnKeyPressed(self.ctrl, evt)
|
|
1145
|
+
BaseParamCtrl.onChange(self, evt)
|
|
1146
|
+
|
|
1147
|
+
def styleValid(self):
|
|
1148
|
+
# red border if error
|
|
1149
|
+
if self.isValid:
|
|
1150
|
+
self.ctrl.SetFoldMarginColour(0, colors.scheme['red'])
|
|
1151
|
+
else:
|
|
1152
|
+
self.ctrl._applyAppTheme()
|
|
1153
|
+
self.ctrl.Refresh()
|
|
1154
|
+
|
|
1155
|
+
def validate(self):
|
|
1156
|
+
BaseParamCtrl.validate(self)
|
|
1157
|
+
return SingleLineCtrl.validateCode(self)
|
|
1158
|
+
|
|
1159
|
+
|
|
1160
|
+
class RichChoiceCtrl(BaseParamCtrl):
|
|
1161
|
+
inputType = "richChoice"
|
|
1162
|
+
|
|
1163
|
+
viewToggle = True
|
|
1164
|
+
multi = False
|
|
1165
|
+
|
|
1166
|
+
class RichChoiceItem(wx.Panel):
|
|
1167
|
+
def __init__(self, parent, value, label, body="", linkText="", link="", startShown="always", viewToggle=True):
|
|
1168
|
+
# Initialise
|
|
1169
|
+
wx.Panel.__init__(self, parent, style=wx.BORDER_THEME)
|
|
1170
|
+
self.parent = parent
|
|
1171
|
+
self.value = value
|
|
1172
|
+
self.startShown = startShown
|
|
1173
|
+
# Setup sizer
|
|
1174
|
+
self.border = wx.BoxSizer()
|
|
1175
|
+
self.SetSizer(self.border)
|
|
1176
|
+
self.sizer = wx.FlexGridSizer(cols=3)
|
|
1177
|
+
self.sizer.AddGrowableCol(idx=1, proportion=1)
|
|
1178
|
+
self.border.Add(self.sizer, proportion=1, border=6, flag=wx.ALL | wx.EXPAND)
|
|
1179
|
+
# Check
|
|
1180
|
+
self.check = wx.CheckBox(self, label=" ")
|
|
1181
|
+
self.check.Bind(wx.EVT_CHECKBOX, self.onCheck)
|
|
1182
|
+
self.check.Bind(wx.EVT_KEY_UP, self.onToggle)
|
|
1183
|
+
self.sizer.Add(self.check, border=3, flag=wx.ALL | wx.ALIGN_CENTER_VERTICAL)
|
|
1184
|
+
# Title
|
|
1185
|
+
self.title = wx.StaticText(self, label=label)
|
|
1186
|
+
self.title.SetFont(self.title.GetFont().Bold())
|
|
1187
|
+
self.sizer.Add(self.title, border=3, flag=wx.ALL | wx.EXPAND)
|
|
1188
|
+
# Toggle
|
|
1189
|
+
self.toggleView = wx.ToggleButton(self, style=wx.BU_EXACTFIT)
|
|
1190
|
+
self.toggleView.Bind(wx.EVT_TOGGLEBUTTON, self.onToggleView)
|
|
1191
|
+
self.toggleView.Show(viewToggle)
|
|
1192
|
+
self.sizer.Add(self.toggleView, border=3, flag=wx.ALL | wx.EXPAND)
|
|
1193
|
+
# Body
|
|
1194
|
+
self.body = utils.WrappedStaticText(self, label=body)
|
|
1195
|
+
self.sizer.AddStretchSpacer(1)
|
|
1196
|
+
self.sizer.Add(self.body, border=3, proportion=1, flag=wx.ALL | wx.EXPAND)
|
|
1197
|
+
self.sizer.AddStretchSpacer(1)
|
|
1198
|
+
# Link
|
|
1199
|
+
self.link = utils.HyperLinkCtrl(self, label=linkText, URL=link)
|
|
1200
|
+
self.link.SetBackgroundColour(self.GetBackgroundColour())
|
|
1201
|
+
self.sizer.AddStretchSpacer(1)
|
|
1202
|
+
self.sizer.Add(self.link, border=3, flag=wx.ALL | wx.ALIGN_LEFT)
|
|
1203
|
+
self.sizer.AddStretchSpacer(1)
|
|
1204
|
+
|
|
1205
|
+
# Style
|
|
1206
|
+
self.SetBackgroundColour("white")
|
|
1207
|
+
self.body.SetBackgroundColour("white")
|
|
1208
|
+
self.link.SetBackgroundColour("white")
|
|
1209
|
+
self.toggleView.SetBackgroundColour("white")
|
|
1210
|
+
|
|
1211
|
+
self.Layout()
|
|
1212
|
+
|
|
1213
|
+
def getChecked(self):
|
|
1214
|
+
return self.check.GetValue()
|
|
1215
|
+
|
|
1216
|
+
def setChecked(self, state):
|
|
1217
|
+
if self.parent.multi:
|
|
1218
|
+
# If multi select is allowed, leave other values unchanged
|
|
1219
|
+
values = self.parent.getValue()
|
|
1220
|
+
if not isinstance(values, (list, tuple)):
|
|
1221
|
+
values = [values]
|
|
1222
|
+
if state:
|
|
1223
|
+
# Add this item to list if checked
|
|
1224
|
+
values.append(self.value)
|
|
1225
|
+
else:
|
|
1226
|
+
# Remove this item from list if unchecked
|
|
1227
|
+
if self.value in values:
|
|
1228
|
+
values.remove(self.value)
|
|
1229
|
+
self.parent.setValue(values)
|
|
1230
|
+
elif state:
|
|
1231
|
+
# If single only, set at parent level so others are unchecked
|
|
1232
|
+
self.parent.setValue(self.value)
|
|
1233
|
+
|
|
1234
|
+
# post event
|
|
1235
|
+
evt = wx.ListEvent(commandType=wx.EVT_CHOICE.typeId, id=-1)
|
|
1236
|
+
evt.SetString(self.value)
|
|
1237
|
+
evt.SetEventObject(self.parent)
|
|
1238
|
+
wx.PostEvent(self.parent, evt)
|
|
1239
|
+
|
|
1240
|
+
def onCheck(self, evt):
|
|
1241
|
+
self.setChecked(evt.IsChecked())
|
|
1242
|
+
|
|
1243
|
+
def onToggle(self, evt):
|
|
1244
|
+
if evt.GetUnicodeKey() in (wx.WXK_SPACE, wx.WXK_NUMPAD_SPACE):
|
|
1245
|
+
self.setChecked(not self.check.IsChecked())
|
|
1246
|
+
|
|
1247
|
+
def onToggleView(self, evt):
|
|
1248
|
+
# If called with a boolean, use it directly, otherwise get bool from event
|
|
1249
|
+
if isinstance(evt, bool):
|
|
1250
|
+
val = evt
|
|
1251
|
+
else:
|
|
1252
|
+
val = evt.IsChecked()
|
|
1253
|
+
# Update toggle ctrl label
|
|
1254
|
+
if val:
|
|
1255
|
+
lbl = "⯆"
|
|
1256
|
+
else:
|
|
1257
|
+
lbl = "⯇"
|
|
1258
|
+
self.toggleView.SetLabel(lbl)
|
|
1259
|
+
# Show/hide body based on value
|
|
1260
|
+
self.body.Show(val)
|
|
1261
|
+
self.link.Show(val)
|
|
1262
|
+
# Layout
|
|
1263
|
+
self.Layout()
|
|
1264
|
+
self.parent.parent.Layout() # layout params notebook page
|
|
1265
|
+
|
|
1266
|
+
def makeCtrls(self):
|
|
1267
|
+
self.ctrl = self
|
|
1268
|
+
# make sizer for options
|
|
1269
|
+
self.optionsSizer = wx.BoxSizer(wx.VERTICAL)
|
|
1270
|
+
self.sizer.Add(
|
|
1271
|
+
self.optionsSizer, proportion=1, flag=wx.EXPAND | wx.ALL
|
|
1272
|
+
)
|
|
1273
|
+
# store values
|
|
1274
|
+
self.choices = {}
|
|
1275
|
+
for i, val in enumerate(self.param.allowedVals):
|
|
1276
|
+
self.choices[val] = self.param.allowedLabels[i]
|
|
1277
|
+
# Populate
|
|
1278
|
+
self.populate()
|
|
1279
|
+
# Set value
|
|
1280
|
+
self.setValue(self.param.val)
|
|
1281
|
+
# Start off showing according to param
|
|
1282
|
+
for obj in self.items:
|
|
1283
|
+
# Work out if we should start out shown
|
|
1284
|
+
if self.viewToggle:
|
|
1285
|
+
if obj.startShown == "never":
|
|
1286
|
+
startShown = False
|
|
1287
|
+
elif obj.startShown == "checked":
|
|
1288
|
+
startShown = obj.check.IsChecked()
|
|
1289
|
+
elif obj.startShown == "unchecked":
|
|
1290
|
+
startShown = not obj.check.IsChecked()
|
|
1291
|
+
else:
|
|
1292
|
+
startShown = True
|
|
1293
|
+
else:
|
|
1294
|
+
startShown = True
|
|
1295
|
+
# Apply starting view
|
|
1296
|
+
obj.toggleView.SetValue(startShown)
|
|
1297
|
+
obj.onToggleView(startShown)
|
|
1298
|
+
# bind onChange
|
|
1299
|
+
self.Bind(wx.EVT_CHOICE, self.onChange)
|
|
1300
|
+
|
|
1301
|
+
self.Layout()
|
|
1302
|
+
|
|
1303
|
+
def populate(self):
|
|
1304
|
+
self.items = []
|
|
1305
|
+
for val, label in self.choices.items():
|
|
1306
|
+
if not isinstance(label, dict):
|
|
1307
|
+
# Make sure label is dict
|
|
1308
|
+
label = {"label": label}
|
|
1309
|
+
# Add item control
|
|
1310
|
+
self.addItem(val, label=label)
|
|
1311
|
+
self.Layout()
|
|
1312
|
+
|
|
1313
|
+
def addItem(self, value, label={}):
|
|
1314
|
+
# Create item object
|
|
1315
|
+
item = self.RichChoiceItem(self, value=value, viewToggle=self.viewToggle, **label)
|
|
1316
|
+
self.items.append(item)
|
|
1317
|
+
# Add to sizer
|
|
1318
|
+
self.optionsSizer.Add(item, border=3, flag=wx.ALL | wx.EXPAND)
|
|
1319
|
+
|
|
1320
|
+
def getValue(self):
|
|
1321
|
+
# Get corresponding value for each checked item
|
|
1322
|
+
values = []
|
|
1323
|
+
for item in self.items:
|
|
1324
|
+
if item.getChecked():
|
|
1325
|
+
# If checked, append value
|
|
1326
|
+
values.append(item.value)
|
|
1327
|
+
# Strip list if not multi
|
|
1328
|
+
if not self.multi:
|
|
1329
|
+
if len(values):
|
|
1330
|
+
values = values[0]
|
|
1331
|
+
else:
|
|
1332
|
+
values = ""
|
|
1333
|
+
|
|
1334
|
+
return values
|
|
1335
|
+
|
|
1336
|
+
def setValue(self, value):
|
|
1337
|
+
# Make sure value is iterable
|
|
1338
|
+
value = data.utils.listFromString(value)
|
|
1339
|
+
# Check/uncheck corresponding items
|
|
1340
|
+
for item in self.items:
|
|
1341
|
+
state = item.value in value
|
|
1342
|
+
item.check.SetValue(state)
|
|
1343
|
+
|
|
1344
|
+
self.Layout()
|
|
1345
|
+
|
|
1346
|
+
|
|
1347
|
+
class FileListCtrl(BaseParamCtrl):
|
|
1348
|
+
inputType = "fileList"
|
|
1349
|
+
|
|
1350
|
+
dlgWildcard = "All Files (*.*)|*.*"
|
|
1351
|
+
dlgStyle = wx.FD_FILE_MUST_EXIST
|
|
1352
|
+
|
|
1353
|
+
class FileListItem(FileCtrl):
|
|
1354
|
+
def makeCtrls(self):
|
|
1355
|
+
FileCtrl.makeCtrls(self)
|
|
1356
|
+
# add a delete button
|
|
1357
|
+
self.deleteBtn = wx.Button(self, style=wx.BU_EXACTFIT)
|
|
1358
|
+
self.deleteBtn.SetBitmap(
|
|
1359
|
+
icons.ButtonIcon("delete", size=16, theme="light").bitmap
|
|
1360
|
+
)
|
|
1361
|
+
self.sizer.Add(
|
|
1362
|
+
self.deleteBtn, border=6, flag=wx.EXPAND | wx.LEFT
|
|
1363
|
+
)
|
|
1364
|
+
self.deleteBtn.Bind(wx.EVT_BUTTON, self.deleteSelf)
|
|
1365
|
+
|
|
1366
|
+
self.Layout()
|
|
1367
|
+
|
|
1368
|
+
def deleteSelf(self, evt=None):
|
|
1369
|
+
# remove from parent sizer and array
|
|
1370
|
+
self.parent.items.pop(
|
|
1371
|
+
self.parent.items.index(self)
|
|
1372
|
+
)
|
|
1373
|
+
self.parent.itemsSizer.Detach(self)
|
|
1374
|
+
# clear any warnings
|
|
1375
|
+
self.clearWarning()
|
|
1376
|
+
# delete
|
|
1377
|
+
self.Destroy()
|
|
1378
|
+
self.parent.Layout()
|
|
1379
|
+
|
|
1380
|
+
def onChange(self, evt=None):
|
|
1381
|
+
FileCtrl.onChange(self, evt)
|
|
1382
|
+
self.parent.onChange(evt)
|
|
1383
|
+
|
|
1384
|
+
def makeCtrls(self):
|
|
1385
|
+
self.ctrl = self
|
|
1386
|
+
# make own sizer vertical
|
|
1387
|
+
self.sizer.SetOrientation(wx.VERTICAL)
|
|
1388
|
+
# array to store items
|
|
1389
|
+
self.items = []
|
|
1390
|
+
# sizer to layout items
|
|
1391
|
+
self.itemsSizer = wx.BoxSizer(wx.VERTICAL)
|
|
1392
|
+
self.sizer.Add(
|
|
1393
|
+
self.itemsSizer, border=6, proportion=1, flag=wx.EXPAND | wx.BOTTOM
|
|
1394
|
+
)
|
|
1395
|
+
# add multiple button
|
|
1396
|
+
self.addManyBtn = wx.Button(self, label=_translate("Add multiple items"))
|
|
1397
|
+
self.addManyBtn.SetBitmap(
|
|
1398
|
+
icons.ButtonIcon("add_many", size=16, theme="light").bitmap
|
|
1399
|
+
)
|
|
1400
|
+
self.sizer.Add(
|
|
1401
|
+
self.addManyBtn, border=6, flag=wx.ALIGN_LEFT | wx.BOTTOM
|
|
1402
|
+
)
|
|
1403
|
+
self.addManyBtn.Bind(wx.EVT_BUTTON, self.addMultiItems)
|
|
1404
|
+
# add button
|
|
1405
|
+
self.addBtn = wx.Button(self, label=_translate("Add item"))
|
|
1406
|
+
self.addBtn.SetBitmap(
|
|
1407
|
+
icons.ButtonIcon("add", size=16, theme="light").bitmap
|
|
1408
|
+
)
|
|
1409
|
+
self.sizer.Add(
|
|
1410
|
+
self.addBtn, border=6, flag=wx.ALIGN_LEFT | wx.BOTTOM
|
|
1411
|
+
)
|
|
1412
|
+
self.addBtn.Bind(wx.EVT_BUTTON, self.addItem)
|
|
1413
|
+
# set initial value
|
|
1414
|
+
self.setValue(self.param.val)
|
|
1415
|
+
|
|
1416
|
+
def layout(self):
|
|
1417
|
+
"""
|
|
1418
|
+
Layout this element, and fit its parent around it.
|
|
1419
|
+
"""
|
|
1420
|
+
self.Layout()
|
|
1421
|
+
self.GetParent().Layout()
|
|
1422
|
+
self.GetTopLevelParent().Layout()
|
|
1423
|
+
self.GetTopLevelParent().Fit()
|
|
1424
|
+
|
|
1425
|
+
def addItem(self, evt=None):
|
|
1426
|
+
"""
|
|
1427
|
+
Add a new item to this ctrl
|
|
1428
|
+
"""
|
|
1429
|
+
# make a file control for a param not attached to anything
|
|
1430
|
+
item = self.FileListItem(
|
|
1431
|
+
parent=self,
|
|
1432
|
+
field=str(len(self.items)),
|
|
1433
|
+
param=Param("", valType="str", inputType="file"),
|
|
1434
|
+
element=self.element,
|
|
1435
|
+
warnings=self.warnings
|
|
1436
|
+
)
|
|
1437
|
+
# append it to items array
|
|
1438
|
+
self.items.append(item)
|
|
1439
|
+
# add it to the items sizer
|
|
1440
|
+
self.itemsSizer.Add(
|
|
1441
|
+
item, border=6, flag=wx.EXPAND | wx.BOTTOM
|
|
1442
|
+
)
|
|
1443
|
+
|
|
1444
|
+
self.layout()
|
|
1445
|
+
|
|
1446
|
+
return item
|
|
1447
|
+
|
|
1448
|
+
def addMultiItems(self, evt=None):
|
|
1449
|
+
"""
|
|
1450
|
+
Add several new items to this ctrl
|
|
1451
|
+
"""
|
|
1452
|
+
items = []
|
|
1453
|
+
# open a file browser dialog
|
|
1454
|
+
dlg = wx.FileDialog(
|
|
1455
|
+
self,
|
|
1456
|
+
message=_translate("Specify file..."),
|
|
1457
|
+
defaultDir=str(self.rootDir),
|
|
1458
|
+
style=wx.FD_OPEN | wx.FD_MULTIPLE | self.dlgStyle,
|
|
1459
|
+
wildcard=self.dlgWildcard,
|
|
1460
|
+
)
|
|
1461
|
+
if dlg.ShowModal() != wx.ID_OK:
|
|
1462
|
+
return
|
|
1463
|
+
# get path
|
|
1464
|
+
for file in dlg.GetPaths():
|
|
1465
|
+
# relativise
|
|
1466
|
+
try:
|
|
1467
|
+
filename = Path(file).relative_to(self.rootDir)
|
|
1468
|
+
except ValueError:
|
|
1469
|
+
filename = Path(file).absolute()
|
|
1470
|
+
# make a file control for a param not attached to anything
|
|
1471
|
+
item = self.FileListItem(
|
|
1472
|
+
parent=self,
|
|
1473
|
+
field=str(len(self.items)),
|
|
1474
|
+
param=Param(str(filename).replace("\\", "/"), valType="str", inputType="file"),
|
|
1475
|
+
element=self.element,
|
|
1476
|
+
warnings=self.warnings
|
|
1477
|
+
)
|
|
1478
|
+
items.append(item)
|
|
1479
|
+
# append it to items array
|
|
1480
|
+
self.items.append(item)
|
|
1481
|
+
# add it to the items sizer
|
|
1482
|
+
self.itemsSizer.Add(
|
|
1483
|
+
item, border=6, flag=wx.EXPAND | wx.BOTTOM
|
|
1484
|
+
)
|
|
1485
|
+
|
|
1486
|
+
self.layout()
|
|
1487
|
+
|
|
1488
|
+
return items
|
|
1489
|
+
|
|
1490
|
+
def clearItems(self):
|
|
1491
|
+
"""
|
|
1492
|
+
Clear all items from this ctrl
|
|
1493
|
+
"""
|
|
1494
|
+
for item in self.items:
|
|
1495
|
+
item.deleteSelf()
|
|
1496
|
+
|
|
1497
|
+
self.layout()
|
|
1498
|
+
|
|
1499
|
+
def getValue(self):
|
|
1500
|
+
return [item.getValue() for item in self.items]
|
|
1501
|
+
|
|
1502
|
+
def setValue(self, value):
|
|
1503
|
+
# unstring value into an actual list
|
|
1504
|
+
value = data.utils.listFromString(value)
|
|
1505
|
+
# clear all items
|
|
1506
|
+
self.clearItems()
|
|
1507
|
+
# make a new item for each value
|
|
1508
|
+
for item in value:
|
|
1509
|
+
ctrl = self.addItem()
|
|
1510
|
+
ctrl.setValue(item)
|
|
1511
|
+
|
|
1512
|
+
@property
|
|
1513
|
+
def isValid(self):
|
|
1514
|
+
# return True if all children are valid
|
|
1515
|
+
return all([
|
|
1516
|
+
item.isValid
|
|
1517
|
+
for item in self.items
|
|
1518
|
+
])
|
|
1519
|
+
|
|
1520
|
+
def validate(self):
|
|
1521
|
+
for item in self.items:
|
|
1522
|
+
item.validate()
|
|
1523
|
+
|
|
1524
|
+
@property
|
|
1525
|
+
def rootDir(self):
|
|
1526
|
+
# if no element, use system root
|
|
1527
|
+
if self.element is None or not hasattr(self.element, "exp"):
|
|
1528
|
+
return Path()
|
|
1529
|
+
# otherwise, get from experiment
|
|
1530
|
+
root = Path(self.element.exp.filename)
|
|
1531
|
+
# move up a dir if root is a file
|
|
1532
|
+
if root.is_file():
|
|
1533
|
+
root = root.parent
|
|
1534
|
+
|
|
1535
|
+
return root
|
|
1536
|
+
|
|
1537
|
+
|
|
1538
|
+
class DictCtrl(BaseParamCtrl):
|
|
1539
|
+
inputType = "dict"
|
|
1540
|
+
|
|
1541
|
+
class DictKey(SingleLineCtrl):
|
|
1542
|
+
def validate(self):
|
|
1543
|
+
"""
|
|
1544
|
+
Dict keys can't key variables
|
|
1545
|
+
"""
|
|
1546
|
+
if self.isCode:
|
|
1547
|
+
self.setWarning(_translate(
|
|
1548
|
+
"Dictionary keys can't be code"
|
|
1549
|
+
), allowed=False)
|
|
1550
|
+
else:
|
|
1551
|
+
SingleLineCtrl.validate(self)
|
|
1552
|
+
|
|
1553
|
+
def onChange(self, evt=None):
|
|
1554
|
+
SingleLineCtrl.onChange(self, evt)
|
|
1555
|
+
self.parent.onChange(evt)
|
|
1556
|
+
|
|
1557
|
+
class DictValue(SingleLineCtrl):
|
|
1558
|
+
def validate(self):
|
|
1559
|
+
# update param label so the error reports the value of keyctrl
|
|
1560
|
+
if hasattr(self, "keyCtrl"):
|
|
1561
|
+
self.param.label = f"{self.parent.param.label}:{self.keyCtrl.getValue()}"
|
|
1562
|
+
|
|
1563
|
+
# validate first as code
|
|
1564
|
+
self.param.valType = "code"
|
|
1565
|
+
self.dollarLbl.Show()
|
|
1566
|
+
self.warnings.clearWarning(self)
|
|
1567
|
+
self.validateCode()
|
|
1568
|
+
# if this failed, try as string
|
|
1569
|
+
if self.warnings.getWarning(self):
|
|
1570
|
+
self.warnings.clearWarning(self)
|
|
1571
|
+
self.param.valType = "str"
|
|
1572
|
+
self.validateStr()
|
|
1573
|
+
|
|
1574
|
+
self.dollarLbl.Show(self.param.valType == "code")
|
|
1575
|
+
|
|
1576
|
+
self.Refresh()
|
|
1577
|
+
self.Layout()
|
|
1578
|
+
|
|
1579
|
+
def onChange(self, evt=None):
|
|
1580
|
+
SingleLineCtrl.onChange(self, evt)
|
|
1581
|
+
self.parent.onChange(evt)
|
|
1582
|
+
|
|
1583
|
+
class DictField:
|
|
1584
|
+
def __init__(self, parent):
|
|
1585
|
+
# store parent
|
|
1586
|
+
self.parent = parent
|
|
1587
|
+
# add ctrl for key
|
|
1588
|
+
self.keyCtrl = DictCtrl.DictKey(
|
|
1589
|
+
parent=parent,
|
|
1590
|
+
field=f"key{len(parent.items)}",
|
|
1591
|
+
param=Param("", valType="str", inputType="single"),
|
|
1592
|
+
element=parent.element,
|
|
1593
|
+
warnings=parent.warnings
|
|
1594
|
+
)
|
|
1595
|
+
# add ctrl for value
|
|
1596
|
+
self.valueCtrl = DictCtrl.DictValue(
|
|
1597
|
+
parent=parent,
|
|
1598
|
+
field=f"value{len(parent.items)}",
|
|
1599
|
+
param=Param("", valType="code", inputType="single"),
|
|
1600
|
+
element=parent.element,
|
|
1601
|
+
warnings=parent.warnings
|
|
1602
|
+
)
|
|
1603
|
+
self.valueCtrl.keyCtrl = self.keyCtrl
|
|
1604
|
+
# add delete button
|
|
1605
|
+
self.deleteBtn = wx.Button(parent, style=wx.BU_EXACTFIT)
|
|
1606
|
+
self.deleteBtn.SetBitmap(
|
|
1607
|
+
icons.ButtonIcon("delete", size=16, theme="light").bitmap
|
|
1608
|
+
)
|
|
1609
|
+
self.deleteBtn.Bind(wx.EVT_BUTTON, self.deleteSelf)
|
|
1610
|
+
|
|
1611
|
+
def deleteSelf(self, evt=None):
|
|
1612
|
+
# remove from parent array
|
|
1613
|
+
self.parent.items.pop(
|
|
1614
|
+
self.parent.items.index(self)
|
|
1615
|
+
)
|
|
1616
|
+
# clear any warnings
|
|
1617
|
+
self.keyCtrl.clearWarning()
|
|
1618
|
+
self.valueCtrl.clearWarning()
|
|
1619
|
+
# remove all windows from parent sizer
|
|
1620
|
+
self.parent.itemsSizer.Detach(self.keyCtrl)
|
|
1621
|
+
self.parent.itemsSizer.Detach(self.valueCtrl)
|
|
1622
|
+
self.parent.itemsSizer.Detach(self.deleteBtn)
|
|
1623
|
+
# delete all windows
|
|
1624
|
+
self.keyCtrl.Destroy()
|
|
1625
|
+
self.valueCtrl.Destroy()
|
|
1626
|
+
self.deleteBtn.Destroy()
|
|
1627
|
+
# layout
|
|
1628
|
+
self.parent.layout()
|
|
1629
|
+
|
|
1630
|
+
def makeCtrls(self):
|
|
1631
|
+
self.ctrl = self
|
|
1632
|
+
# make own sizer vertical
|
|
1633
|
+
self.sizer.SetOrientation(wx.VERTICAL)
|
|
1634
|
+
# array to store items
|
|
1635
|
+
self.items = []
|
|
1636
|
+
# sizer to layout items
|
|
1637
|
+
self.itemsSizer = wx.FlexGridSizer(3, vgap=6, hgap=6)
|
|
1638
|
+
self.itemsSizer.AddGrowableCol(0, proportion=1)
|
|
1639
|
+
self.itemsSizer.AddGrowableCol(1, proportion=1)
|
|
1640
|
+
self.sizer.Add(
|
|
1641
|
+
self.itemsSizer, border=6, proportion=1, flag=wx.EXPAND | wx.BOTTOM
|
|
1642
|
+
)
|
|
1643
|
+
# add button
|
|
1644
|
+
self.addBtn = wx.Button(self, label=_translate("Add item"))
|
|
1645
|
+
self.addBtn.SetBitmap(
|
|
1646
|
+
icons.ButtonIcon("add", size=16, theme="light").bitmap
|
|
1647
|
+
)
|
|
1648
|
+
self.sizer.Add(
|
|
1649
|
+
self.addBtn, border=6, flag=wx.ALIGN_LEFT | wx.BOTTOM
|
|
1650
|
+
)
|
|
1651
|
+
self.addBtn.Bind(wx.EVT_BUTTON, self.addItem)
|
|
1652
|
+
# set initial value
|
|
1653
|
+
self.setValue(self.param.val)
|
|
1654
|
+
|
|
1655
|
+
def layout(self):
|
|
1656
|
+
"""
|
|
1657
|
+
Layout this element, and fit its parent around it.
|
|
1658
|
+
"""
|
|
1659
|
+
self.Layout()
|
|
1660
|
+
self.GetParent().Layout()
|
|
1661
|
+
self.GetTopLevelParent().Layout()
|
|
1662
|
+
self.GetTopLevelParent().Fit()
|
|
1663
|
+
|
|
1664
|
+
def addItem(self, evt=None):
|
|
1665
|
+
# create item
|
|
1666
|
+
item = self.DictField(self)
|
|
1667
|
+
# append to array
|
|
1668
|
+
self.items.append(item)
|
|
1669
|
+
# add to sizer
|
|
1670
|
+
self.itemsSizer.Add(
|
|
1671
|
+
item.keyCtrl, proportion=1, flag=wx.EXPAND
|
|
1672
|
+
)
|
|
1673
|
+
self.itemsSizer.Add(
|
|
1674
|
+
item.valueCtrl, proportion=1, flag=wx.EXPAND
|
|
1675
|
+
)
|
|
1676
|
+
self.itemsSizer.Add(
|
|
1677
|
+
item.deleteBtn, flag=wx.EXPAND
|
|
1678
|
+
)
|
|
1679
|
+
# layout
|
|
1680
|
+
self.layout()
|
|
1681
|
+
|
|
1682
|
+
return item
|
|
1683
|
+
|
|
1684
|
+
def clearItems(self):
|
|
1685
|
+
# delete each item
|
|
1686
|
+
for item in self.items:
|
|
1687
|
+
item.deleteSelf()
|
|
1688
|
+
# layout
|
|
1689
|
+
self.layout()
|
|
1690
|
+
|
|
1691
|
+
def getValue(self):
|
|
1692
|
+
return {
|
|
1693
|
+
item.keyCtrl.getValue(): item.valueCtrl.getValue()
|
|
1694
|
+
for item in self.items
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
def setValue(self, value):
|
|
1698
|
+
# clear items
|
|
1699
|
+
self.clearItems()
|
|
1700
|
+
# make sure value is a dict
|
|
1701
|
+
value = data.utils.dictFromString(value)
|
|
1702
|
+
# iterate through key:val pairs
|
|
1703
|
+
for key, val in value.items():
|
|
1704
|
+
# add an item ctrl for each
|
|
1705
|
+
item = self.addItem()
|
|
1706
|
+
# populate
|
|
1707
|
+
item.keyCtrl.setValue(key)
|
|
1708
|
+
item.valueCtrl.setValue(val)
|
|
1709
|
+
|
|
1710
|
+
def validate(self):
|
|
1711
|
+
# check for duplicate keys
|
|
1712
|
+
used = []
|
|
1713
|
+
for key in self.getValue():
|
|
1714
|
+
if key in used:
|
|
1715
|
+
self.setWarning(_translate(
|
|
1716
|
+
"Duplicate dictionary key: {}"
|
|
1717
|
+
).format(key))
|
|
1718
|
+
return
|
|
1719
|
+
used.append(key)
|
|
1720
|
+
# otherwise validate all children
|
|
1721
|
+
for item in self.items:
|
|
1722
|
+
item.keyCtrl.validate()
|
|
1723
|
+
item.valueCtrl.validate()
|
|
1724
|
+
|
|
1725
|
+
@property
|
|
1726
|
+
def isValid(self):
|
|
1727
|
+
# return true if self has no warnings and children have no warnings
|
|
1728
|
+
return BaseParamCtrl.isValid.fget(self) and all([
|
|
1729
|
+
item.keyCtrl.isValid and item.valueCtrl.isValid
|
|
1730
|
+
for item in self.items
|
|
1731
|
+
])
|
|
1732
|
+
|
|
1733
|
+
|
|
1734
|
+
class DeviceCtrl(ChoiceCtrl):
|
|
1735
|
+
inputType = "device"
|
|
1736
|
+
|
|
1737
|
+
def makeCtrls(self):
|
|
1738
|
+
ChoiceCtrl.makeCtrls(self)
|
|
1739
|
+
# add a button to open device manager
|
|
1740
|
+
self.deviceMgrBtn = wx.Button(self, style=wx.BU_EXACTFIT)
|
|
1741
|
+
self.deviceMgrBtn.Bind(wx.EVT_BUTTON, self.openDeviceManager)
|
|
1742
|
+
self.deviceMgrBtn.SetBitmap(
|
|
1743
|
+
icons.ButtonIcon("devices", size=16, theme="light").bitmap
|
|
1744
|
+
)
|
|
1745
|
+
self.deviceMgrBtn.SetToolTip(_translate(
|
|
1746
|
+
"Open the Device Manager to setup devices"
|
|
1747
|
+
))
|
|
1748
|
+
self.sizer.Add(
|
|
1749
|
+
self.deviceMgrBtn, border=6, flag=wx.EXPAND | wx.LEFT
|
|
1750
|
+
)
|
|
1751
|
+
|
|
1752
|
+
def openDeviceManager(self, evt=None):
|
|
1753
|
+
from psychopy.app.deviceManager import DeviceManagerDlg
|
|
1754
|
+
# create dialog
|
|
1755
|
+
dlg = DeviceManagerDlg(parent=self.GetTopLevelParent())
|
|
1756
|
+
# show it
|
|
1757
|
+
dlg.ShowModal()
|
|
1758
|
+
# repopulate devices
|
|
1759
|
+
self.populate()
|
|
1760
|
+
# also repopulate sibling controls
|
|
1761
|
+
for sibling in self.GetParent().GetChildren():
|
|
1762
|
+
if isinstance(sibling, DeviceCtrl) and sibling is not self:
|
|
1763
|
+
sibling.populate()
|
|
1764
|
+
|
|
1765
|
+
def onElementOk(self, evt=None):
|
|
1766
|
+
# get the device manager
|
|
1767
|
+
from psychopy.preferences import prefs
|
|
1768
|
+
from psychopy.app.deviceManager import AddDeviceDlg
|
|
1769
|
+
# if not setup, ask the user whether they want to set it up
|
|
1770
|
+
if self.getValue() and self.getValue() not in prefs.devices:
|
|
1771
|
+
# create dialog
|
|
1772
|
+
dlg = wx.MessageDialog(
|
|
1773
|
+
self.GetTopLevelParent(),
|
|
1774
|
+
_translate(
|
|
1775
|
+
"No device named `{}` has been setup in the Device Manager, set one up now?"
|
|
1776
|
+
).format(self.getValue()),
|
|
1777
|
+
style=wx.YES|wx.NO|wx.ICON_QUESTION
|
|
1778
|
+
)
|
|
1779
|
+
# open device manager if yes
|
|
1780
|
+
if dlg.ShowModal() == wx.ID_YES:
|
|
1781
|
+
dlg = AddDeviceDlg(self, deviceName=self.getValue())
|
|
1782
|
+
# on OK, add device and refresh list
|
|
1783
|
+
if dlg.ShowModal() == wx.ID_OK:
|
|
1784
|
+
device = dlg.getDevice()
|
|
1785
|
+
prefs.devices[device.name] = device
|
|
1786
|
+
prefs.devices.save()
|
|
1787
|
+
self.populate()
|