psychopy 2025.1.1__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/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 +42 -2
- 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 +52 -6
- 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/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/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/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 +34 -11
- 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.1.dist-info → psychopy-2025.2.1.dist-info}/METADATA +17 -11
- {psychopy-2025.1.1.dist-info → psychopy-2025.2.1.dist-info}/RECORD +216 -184
- {psychopy-2025.1.1.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.1.dist-info → psychopy-2025.2.1.dist-info}/entry_points.txt +0 -0
- {psychopy-2025.1.1.dist-info → psychopy-2025.2.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -50,19 +50,17 @@ class ValidatorWarning:
|
|
|
50
50
|
Symbolic constant representing the type of warning. Values can be one of
|
|
51
51
|
`VALIDATOR_WARNING_NONE`, `VALIDATOR_WARNING_NAME`,
|
|
52
52
|
`VALIDATOR_WARNING_SYNTAX` or `VALIDATOR_WARNING_FONT_MISSING`.
|
|
53
|
+
allowed : bool
|
|
54
|
+
If False, this warning should prevent the user from proceeding
|
|
53
55
|
|
|
54
56
|
"""
|
|
55
|
-
__slots__ = [
|
|
56
|
-
'_parent',
|
|
57
|
-
'_control',
|
|
58
|
-
'_msg',
|
|
59
|
-
'_kind']
|
|
60
57
|
|
|
61
|
-
def __init__(self, parent, control, msg="", kind=VALIDATOR_WARNING_NONE):
|
|
58
|
+
def __init__(self, parent, control, msg="", kind=VALIDATOR_WARNING_NONE, allowed=True):
|
|
62
59
|
self.parent = parent
|
|
63
60
|
self.control = control
|
|
64
61
|
self.msg = msg
|
|
65
62
|
self.kind = kind
|
|
63
|
+
self.allowed = allowed or self.kind in [VALIDATOR_WARNING_FONT_MISSING]
|
|
66
64
|
|
|
67
65
|
@property
|
|
68
66
|
def parent(self):
|
|
@@ -118,12 +116,6 @@ class ValidatorWarning:
|
|
|
118
116
|
"""`True` if this is a namespace warning (`bool`)."""
|
|
119
117
|
return self._kind == VALIDATOR_WARNING_NAME
|
|
120
118
|
|
|
121
|
-
@property
|
|
122
|
-
def allowed(self):
|
|
123
|
-
"""`True` if this is a non-critical message which doesn't disable the OK button"""
|
|
124
|
-
return self.kind in [VALIDATOR_WARNING_FONT_MISSING]
|
|
125
|
-
|
|
126
|
-
|
|
127
119
|
class WarningManager:
|
|
128
120
|
"""Manager for warnings produced by validators associated with controls
|
|
129
121
|
within the component properties dialog. Assumes that the `parent` dialog
|
|
@@ -159,11 +151,26 @@ class WarningManager:
|
|
|
159
151
|
|
|
160
152
|
@property
|
|
161
153
|
def OK(self):
|
|
162
|
-
"""
|
|
154
|
+
"""
|
|
155
|
+
Return
|
|
156
|
+
======
|
|
157
|
+
bool
|
|
158
|
+
True if there are no messages which aren't `.allowed`
|
|
159
|
+
"""
|
|
163
160
|
if len(self._warnings) == 0:
|
|
164
161
|
return True
|
|
165
162
|
else:
|
|
166
163
|
return all(warning.allowed for warning in self._warnings.values())
|
|
164
|
+
|
|
165
|
+
@property
|
|
166
|
+
def isEmtpy(self):
|
|
167
|
+
"""
|
|
168
|
+
Returns
|
|
169
|
+
-------
|
|
170
|
+
bool
|
|
171
|
+
True if there are no warnings at all, even allowed ones.
|
|
172
|
+
"""
|
|
173
|
+
return len(self._warnings) == 0
|
|
167
174
|
|
|
168
175
|
@property
|
|
169
176
|
def parent(self):
|
|
@@ -200,7 +207,7 @@ class WarningManager:
|
|
|
200
207
|
_, warnings = self._warnings.items()
|
|
201
208
|
return [warning.control for warning in warnings]
|
|
202
209
|
|
|
203
|
-
def setWarning(self, control, msg='', kind=VALIDATOR_WARNING_NONE):
|
|
210
|
+
def setWarning(self, control, msg='', kind=VALIDATOR_WARNING_NONE, allowed=True):
|
|
204
211
|
"""Set a warning for a control. A control can only have one active
|
|
205
212
|
warning associate with it at any given time.
|
|
206
213
|
|
|
@@ -213,10 +220,11 @@ class WarningManager:
|
|
|
213
220
|
kind : int
|
|
214
221
|
Symbolic constant representing the type of warning (e.g.,
|
|
215
222
|
`VALIDATOR_WARN_SYNTAX`).
|
|
216
|
-
|
|
223
|
+
allowed : bool
|
|
224
|
+
If False, prevent the user from continuing
|
|
217
225
|
"""
|
|
218
226
|
self._warnings[id(control)] = ValidatorWarning(
|
|
219
|
-
self.parent, control, msg, kind)
|
|
227
|
+
self.parent, control, msg, kind, allowed)
|
|
220
228
|
|
|
221
229
|
def getWarning(self, control):
|
|
222
230
|
"""Get an active warning associated with the control.
|
|
@@ -500,7 +508,7 @@ class CodeSnippetValidator(BaseValidator):
|
|
|
500
508
|
# get var names from val, check against namespace:
|
|
501
509
|
code = experiment.getCodeFromParamStr(val, target="PsychoPy")
|
|
502
510
|
try:
|
|
503
|
-
names = list(stringtools.
|
|
511
|
+
names = list(stringtools.getVariableDefs(code))
|
|
504
512
|
parent.warnings.clearWarning(control)
|
|
505
513
|
except (SyntaxError, TypeError) as e:
|
|
506
514
|
# empty '' compiles to a syntax error, ignore
|
psychopy/app/coder/coder.py
CHANGED
|
@@ -1101,7 +1101,7 @@ class CoderFrame(BaseAuiFrame, handlers.ThemeMixin):
|
|
|
1101
1101
|
self.showingReloadDialog = False
|
|
1102
1102
|
|
|
1103
1103
|
# default window title string
|
|
1104
|
-
self.winTitle =
|
|
1104
|
+
self.winTitle = title
|
|
1105
1105
|
|
|
1106
1106
|
# we didn't have the key or the win was minimized/invalid
|
|
1107
1107
|
if self.appData['winH'] == 0 or self.appData['winW'] == 0:
|
|
@@ -2856,6 +2856,17 @@ class CoderFrame(BaseAuiFrame, handlers.ThemeMixin):
|
|
|
2856
2856
|
# UnitTestFrame.Show()
|
|
2857
2857
|
|
|
2858
2858
|
def openPluginManager(self, evt=None):
|
|
2859
|
+
import psychopy.app.plugin_manager.packageIndex as packageIndex
|
|
2860
|
+
if packageIndex.isIndexing():
|
|
2861
|
+
msg = _translate("The package index is currently being updated. "
|
|
2862
|
+
"Please try again later.")
|
|
2863
|
+
wx.MessageBox(
|
|
2864
|
+
msg,
|
|
2865
|
+
_translate("Package indexing in progress"),
|
|
2866
|
+
style=wx.OK | wx.ICON_INFORMATION
|
|
2867
|
+
)
|
|
2868
|
+
return
|
|
2869
|
+
|
|
2859
2870
|
dlg = psychopy.app.plugin_manager.dialog.EnvironmentManagerDlg(self)
|
|
2860
2871
|
dlg.Show()
|
|
2861
2872
|
# Do post-close checks
|
psychopy/app/coder/repl.py
CHANGED
|
@@ -349,7 +349,9 @@ class PythonREPLCtrl(wx.Panel, handlers.ThemeMixin):
|
|
|
349
349
|
"Starting Python interpreter session, please wait ...\n")
|
|
350
350
|
|
|
351
351
|
# setup the sub-process
|
|
352
|
-
wx.
|
|
352
|
+
if wx.Platform != '__WXGTK__':
|
|
353
|
+
wx.BeginBusyCursor()
|
|
354
|
+
|
|
353
355
|
self._process = wx.Process(self)
|
|
354
356
|
self._process.Redirect()
|
|
355
357
|
|
|
@@ -373,7 +375,8 @@ class PythonREPLCtrl(wx.Panel, handlers.ThemeMixin):
|
|
|
373
375
|
self._lastTextPos = self.txtTerm.GetLastPosition()
|
|
374
376
|
self.toolbar.update()
|
|
375
377
|
|
|
376
|
-
wx.
|
|
378
|
+
if wx.Platform != '__WXGTK__':
|
|
379
|
+
wx.EndBusyCursor()
|
|
377
380
|
|
|
378
381
|
def interrupt(self, evt=None):
|
|
379
382
|
"""Send a keyboard interrupt signal to the interpreter.
|
|
@@ -566,7 +566,7 @@ class PsychoColorPicker(ColorPickerDialog):
|
|
|
566
566
|
self.context.GetCurrentPos(), "(" + self.getOutputValue() + ")")
|
|
567
567
|
|
|
568
568
|
self._saveState() # retain state
|
|
569
|
-
self.
|
|
569
|
+
self.EndModal(wx.ID_OK)
|
|
570
570
|
|
|
571
571
|
def OnCopy(self, event):
|
|
572
572
|
"""Event to copy the color to the clipboard as a value.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .dialog import DeviceManagerDlg, AddDeviceDlg
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
from psychopy.app.deviceManager.utils import DeviceImageList
|
|
2
|
+
from psychopy.app.builder.dialogs.paramCtrls import EVT_PARAM_CHANGED, ParamCtrl
|
|
3
|
+
from psychopy.app.builder.validators import WarningManager
|
|
4
|
+
from psychopy.app.themes import fonts, icons
|
|
5
|
+
from psychopy.experiment.devices import DeviceBackend
|
|
6
|
+
from psychopy.experiment.params import Param
|
|
7
|
+
from psychopy.hardware.manager import DeviceManager
|
|
8
|
+
from psychopy import logging
|
|
9
|
+
from psychopy.localization import _translate
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
import wx
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AddDeviceDlg(wx.Dialog):
|
|
16
|
+
|
|
17
|
+
availableDevices = None
|
|
18
|
+
|
|
19
|
+
def __init__(self, parent, deviceName=""):
|
|
20
|
+
wx.Dialog.__init__(
|
|
21
|
+
self, parent, title="Add device",
|
|
22
|
+
size=(540, 540),
|
|
23
|
+
style=wx.RESIZE_BORDER | wx.CAPTION | wx.CLOSE_BOX
|
|
24
|
+
)
|
|
25
|
+
# attributes to store selection
|
|
26
|
+
self.selectedCls = None
|
|
27
|
+
self.selectedProfile = None
|
|
28
|
+
# setup warnings
|
|
29
|
+
self.warnings = WarningManager(self)
|
|
30
|
+
# setup sizers
|
|
31
|
+
self.border = wx.BoxSizer(wx.VERTICAL)
|
|
32
|
+
self.SetSizer(self.border)
|
|
33
|
+
self.sizer = wx.BoxSizer(wx.VERTICAL)
|
|
34
|
+
self.border.Add(
|
|
35
|
+
self.sizer, proportion=1, border=12, flag=wx.EXPAND | wx.ALL
|
|
36
|
+
)
|
|
37
|
+
# name ctrl
|
|
38
|
+
self.nameLbl = wx.StaticText(self, label=_translate("Device name"))
|
|
39
|
+
self.sizer.Add(
|
|
40
|
+
self.nameLbl, border=6, flag=wx.EXPAND | wx.TOP
|
|
41
|
+
)
|
|
42
|
+
self.name = Param(
|
|
43
|
+
deviceName, valType="str", inputType="name",
|
|
44
|
+
label=_translate("Device label"),
|
|
45
|
+
hint=_translate(
|
|
46
|
+
"A name to refer to this device by in Device Manager."
|
|
47
|
+
)
|
|
48
|
+
)
|
|
49
|
+
self.nameCtrl = ParamCtrl(
|
|
50
|
+
self,
|
|
51
|
+
field="name",
|
|
52
|
+
param=self.name,
|
|
53
|
+
element=None,
|
|
54
|
+
warnings=self.warnings
|
|
55
|
+
)
|
|
56
|
+
# bump up the font size
|
|
57
|
+
self.nameCtrl.ctrl.SetFont(fonts.AppFont(
|
|
58
|
+
pointSize=int(fonts.AppFont.pointSize*1.5),
|
|
59
|
+
bold=True
|
|
60
|
+
).obj)
|
|
61
|
+
self.sizer.Add(
|
|
62
|
+
self.nameCtrl, border=6, flag=wx.EXPAND | wx.BOTTOM
|
|
63
|
+
)
|
|
64
|
+
self.nameCtrl.Bind(EVT_PARAM_CHANGED, self.validate)
|
|
65
|
+
|
|
66
|
+
# devices ctrl
|
|
67
|
+
self.devicesLbl = wx.StaticText(self, label=_translate("Available devices"))
|
|
68
|
+
self.sizer.Add(
|
|
69
|
+
self.devicesLbl, border=6, flag=wx.EXPAND | wx.TOP
|
|
70
|
+
)
|
|
71
|
+
self.devicesCtrl = wx.TreeCtrl(
|
|
72
|
+
self,
|
|
73
|
+
style=wx.TR_HIDE_ROOT | wx.TR_HAS_BUTTONS | wx.TR_NO_LINES
|
|
74
|
+
)
|
|
75
|
+
self.imageList = DeviceImageList(width=24, height=24)
|
|
76
|
+
self.devicesCtrl.SetImageList(self.imageList)
|
|
77
|
+
self.devicesCtrl.SetIndent(6)
|
|
78
|
+
self.sizer.Add(
|
|
79
|
+
self.devicesCtrl, proportion=1, border=6, flag=wx.EXPAND | wx.BOTTOM
|
|
80
|
+
)
|
|
81
|
+
self.devicesCtrl.Bind(wx.EVT_TREE_SEL_CHANGED, self.onSelectItem)
|
|
82
|
+
self.devicesLoadingLbl = wx.StaticText(
|
|
83
|
+
self,
|
|
84
|
+
label=_translate("Scanning...")
|
|
85
|
+
)
|
|
86
|
+
self.sizer.Add(
|
|
87
|
+
self.devicesLoadingLbl, border=6, flag=wx.EXPAND | wx.ALL
|
|
88
|
+
)
|
|
89
|
+
# warnings panel
|
|
90
|
+
self.sizer.Add(
|
|
91
|
+
self.warnings.output, border=6, flag=wx.EXPAND | wx.TOP
|
|
92
|
+
)
|
|
93
|
+
# add ctrls
|
|
94
|
+
self.ctrls = self.CreateStdDialogButtonSizer(
|
|
95
|
+
flags=wx.OK | wx.CANCEL
|
|
96
|
+
)
|
|
97
|
+
self.border.Add(
|
|
98
|
+
self.ctrls, border=12, flag=wx.EXPAND | wx.LEFT | wx.RIGHT | wx.BOTTOM
|
|
99
|
+
)
|
|
100
|
+
# get handle of OK button
|
|
101
|
+
for item in self.ctrls.GetChildren():
|
|
102
|
+
if item.Window is not None and item.Window.GetId() == wx.ID_OK:
|
|
103
|
+
self.okBtn = item.Window
|
|
104
|
+
self.Layout()
|
|
105
|
+
# queue populate command
|
|
106
|
+
self.Bind(wx.EVT_IDLE, self.populateAsync)
|
|
107
|
+
# start off with focus on name field
|
|
108
|
+
self.nameCtrl.SetFocus()
|
|
109
|
+
|
|
110
|
+
def validate(self, evt=None):
|
|
111
|
+
self.okBtn.Enable(
|
|
112
|
+
self.warnings.OK
|
|
113
|
+
and self.selectedCls is not None
|
|
114
|
+
and self.selectedProfile is not None
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
def populate(self):
|
|
118
|
+
"""
|
|
119
|
+
Populate the devices tree control from DeviceManager
|
|
120
|
+
"""
|
|
121
|
+
# start off with "loading devices" message
|
|
122
|
+
self.devicesLoadingLbl.Show()
|
|
123
|
+
self.devicesCtrl.Hide()
|
|
124
|
+
self.Layout()
|
|
125
|
+
# get array of available devices by backend
|
|
126
|
+
if AddDeviceDlg.availableDevices is None:
|
|
127
|
+
AddDeviceDlg.availableDevices = {}
|
|
128
|
+
for backend in DeviceBackend.getAllBackends():
|
|
129
|
+
try:
|
|
130
|
+
AddDeviceDlg.availableDevices[backend] = DeviceManager.getAvailableDevices(backend.deviceClass)
|
|
131
|
+
except Exception as err:
|
|
132
|
+
logging.warn("Failed to scan for {backend.deviceClass} devices, reason: {err}")
|
|
133
|
+
# clear ctrl
|
|
134
|
+
self.devicesCtrl.DeleteAllItems()
|
|
135
|
+
self.branchClasses = {}
|
|
136
|
+
# add a root
|
|
137
|
+
root = self.devicesCtrl.AddRoot("Available devices")
|
|
138
|
+
# iterate through classes...
|
|
139
|
+
for cls, profiles in self.availableDevices.items():
|
|
140
|
+
# don't add label if there's no profiles
|
|
141
|
+
if len(profiles) == 0:
|
|
142
|
+
continue
|
|
143
|
+
# add a child for each class
|
|
144
|
+
branch = self.devicesCtrl.AppendItem(
|
|
145
|
+
root,
|
|
146
|
+
cls.backendLabel or cls.__name__,
|
|
147
|
+
image=self.imageList.getIcon(cls) or -1
|
|
148
|
+
)
|
|
149
|
+
self.devicesCtrl.SetItemBold(branch)
|
|
150
|
+
# store ref to branch class
|
|
151
|
+
self.branchClasses[branch] = cls
|
|
152
|
+
# iterate through profiles...
|
|
153
|
+
for profile in profiles:
|
|
154
|
+
self.devicesCtrl.AppendItem(branch, profile.get("deviceName", "unnamed"))
|
|
155
|
+
# expand and show
|
|
156
|
+
self.devicesCtrl.ExpandAll()
|
|
157
|
+
self.devicesLoadingLbl.Hide()
|
|
158
|
+
self.devicesCtrl.Show()
|
|
159
|
+
self.Layout()
|
|
160
|
+
|
|
161
|
+
def populateAsync(self, evt):
|
|
162
|
+
"""
|
|
163
|
+
Call `.populate` from an asynchronous event handler, the unbind it.
|
|
164
|
+
|
|
165
|
+
Parameters
|
|
166
|
+
----------
|
|
167
|
+
evt : wx.IdleEvent
|
|
168
|
+
wx event triggering this call
|
|
169
|
+
"""
|
|
170
|
+
# populate
|
|
171
|
+
self.populate()
|
|
172
|
+
# unbind
|
|
173
|
+
if evt.EventType == wx.EVT_IDLE.typeId:
|
|
174
|
+
self.Unbind(wx.EVT_IDLE)
|
|
175
|
+
|
|
176
|
+
def getDevice(self):
|
|
177
|
+
"""
|
|
178
|
+
Get the Device object from the choice made in this ctrl.
|
|
179
|
+
|
|
180
|
+
Returns
|
|
181
|
+
-------
|
|
182
|
+
psychopy.experiment.devices.DeviceBackend
|
|
183
|
+
Backend object for the chosen device
|
|
184
|
+
"""
|
|
185
|
+
# create device object
|
|
186
|
+
device = self.selectedCls(self.selectedProfile)
|
|
187
|
+
# store name
|
|
188
|
+
device.params['deviceLabel'].val = self.nameCtrl.getValue()
|
|
189
|
+
|
|
190
|
+
return device
|
|
191
|
+
|
|
192
|
+
def onSelectItem(self, evt):
|
|
193
|
+
evt.Skip()
|
|
194
|
+
# this event is triggered on deletion due to a bug in wx.TreeCtrl, so catch it
|
|
195
|
+
if not self.devicesCtrl:
|
|
196
|
+
return
|
|
197
|
+
# get id of selected profile and its parent
|
|
198
|
+
item = self.devicesCtrl.GetSelection()
|
|
199
|
+
branch = self.devicesCtrl.GetItemParent(item)
|
|
200
|
+
# update profile
|
|
201
|
+
if branch != self.devicesCtrl.GetRootItem():
|
|
202
|
+
# get class and device name
|
|
203
|
+
cls = self.branchClasses[branch]
|
|
204
|
+
name = self.devicesCtrl.GetItemText(item)
|
|
205
|
+
# find profile with matching name
|
|
206
|
+
profile = None
|
|
207
|
+
for thisProfile in self.availableDevices[cls]:
|
|
208
|
+
if thisProfile.get("deviceName", "unnamed") == name:
|
|
209
|
+
profile = thisProfile
|
|
210
|
+
break
|
|
211
|
+
else:
|
|
212
|
+
# if parent is the root node, selection isn't a profile
|
|
213
|
+
cls = profile = None
|
|
214
|
+
# store selected values
|
|
215
|
+
self.selectedCls = cls
|
|
216
|
+
self.selectedProfile = profile
|
|
217
|
+
# enable OK based on selection
|
|
218
|
+
self.validate()
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
from psychopy.app.deviceManager.addDialog import AddDeviceDlg
|
|
2
|
+
from psychopy.app.deviceManager.panel import DevicePanel
|
|
3
|
+
from psychopy.app.deviceManager.utils import DeviceImageList
|
|
4
|
+
from psychopy.preferences import prefs
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
import wx
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DeviceManagerDlg(wx.Dialog):
|
|
11
|
+
"""
|
|
12
|
+
GUI for managing named devices, allows user to map device names specified in an experiment to
|
|
13
|
+
physical devices on this machine.
|
|
14
|
+
"""
|
|
15
|
+
def __init__(self, parent):
|
|
16
|
+
wx.Dialog.__init__(
|
|
17
|
+
self, parent, title="Device manager",
|
|
18
|
+
size=(720, 540),
|
|
19
|
+
style=wx.RESIZE_BORDER | wx.CAPTION | wx.CLOSE_BOX
|
|
20
|
+
)
|
|
21
|
+
self.SetMinSize((540, 256))
|
|
22
|
+
self.devices = prefs.devices.copy()
|
|
23
|
+
# setup sizers
|
|
24
|
+
self.border = wx.BoxSizer(wx.VERTICAL)
|
|
25
|
+
self.SetSizer(self.border)
|
|
26
|
+
self.sizer = wx.BoxSizer(wx.VERTICAL)
|
|
27
|
+
self.border.Add(
|
|
28
|
+
self.sizer, border=12, proportion=1, flag=wx.EXPAND | wx.ALL
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# profiles notebook
|
|
32
|
+
self.profilesNotebook = wx.Listbook(self, style=wx.LB_LEFT)
|
|
33
|
+
self.sizer.Add(
|
|
34
|
+
self.profilesNotebook, border=0, proportion=1, flag=wx.EXPAND | wx.ALL
|
|
35
|
+
)
|
|
36
|
+
self.pages = {}
|
|
37
|
+
# resize the list ctrl
|
|
38
|
+
self.profilesListCtrl = self.profilesNotebook.GetListView()
|
|
39
|
+
self.profilesListCtrl.SetWindowStyleFlag(wx.LC_LIST)
|
|
40
|
+
|
|
41
|
+
if wx.Platform == "__WXMSW__":
|
|
42
|
+
self.profilesListCtrl.SetColumnWidth(-1, 128)
|
|
43
|
+
|
|
44
|
+
self.profilesListCtrl.SetMinSize((128, 128))
|
|
45
|
+
self.profilesListCtrl.Refresh()
|
|
46
|
+
# apply cached devices image list
|
|
47
|
+
self.imageList = DeviceImageList(width=24, height=24)
|
|
48
|
+
self.profilesListCtrl.SetImageList(self.imageList, which=wx.IMAGE_LIST_SMALL)
|
|
49
|
+
# self.profilesListCtrl.SetWindowStyle(wx.LC_ICON)
|
|
50
|
+
# get list ctrl sizer so we can add ctrls
|
|
51
|
+
self.profilesListCtrl.sizer = self.profilesListCtrl.GetSizer()
|
|
52
|
+
if self.profilesListCtrl.sizer is None:
|
|
53
|
+
# on windows, ListCtrl doesn't have a sizer, so make one
|
|
54
|
+
self.profilesListCtrl.sizer = wx.BoxSizer(wx.VERTICAL)
|
|
55
|
+
self.profilesListCtrl.sizer.AddStretchSpacer(1)
|
|
56
|
+
self.profilesListCtrl.SetSizer(self.profilesListCtrl.sizer)
|
|
57
|
+
# add device button
|
|
58
|
+
self.addDeviceBtn = wx.Button(
|
|
59
|
+
self.profilesListCtrl, label="Add device"
|
|
60
|
+
)
|
|
61
|
+
self.addDeviceBtn.Bind(wx.EVT_BUTTON, self.onAddDeviceBtn)
|
|
62
|
+
self.profilesListCtrl.sizer.Add(
|
|
63
|
+
self.addDeviceBtn, border=6, flag=wx.EXPAND | wx.ALL
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
self.populate()
|
|
67
|
+
|
|
68
|
+
# add ctrls
|
|
69
|
+
self.ctrls = self.CreateStdDialogButtonSizer(
|
|
70
|
+
flags=wx.OK | wx.CANCEL
|
|
71
|
+
)
|
|
72
|
+
self.Bind(wx.EVT_BUTTON, self.onOK, id=wx.ID_OK)
|
|
73
|
+
self.border.Add(self.ctrls, border=12, flag=wx.EXPAND | wx.ALL)
|
|
74
|
+
# get handle of OK button
|
|
75
|
+
for item in self.ctrls.GetChildren():
|
|
76
|
+
if item.Window is not None and item.Window.GetId() == wx.ID_OK:
|
|
77
|
+
self.okBtn = item.Window
|
|
78
|
+
|
|
79
|
+
self.Layout()
|
|
80
|
+
|
|
81
|
+
def populate(self):
|
|
82
|
+
"""
|
|
83
|
+
Populate the device names ctrl from saved devices.
|
|
84
|
+
"""
|
|
85
|
+
# add pages
|
|
86
|
+
for name, device in self.devices.items():
|
|
87
|
+
if name not in self.pages:
|
|
88
|
+
# create page
|
|
89
|
+
self.pages[name] = DevicePanel(
|
|
90
|
+
parent=self.profilesNotebook,
|
|
91
|
+
dlg=self,
|
|
92
|
+
device=device
|
|
93
|
+
)
|
|
94
|
+
# add page
|
|
95
|
+
self.profilesNotebook.AddPage(
|
|
96
|
+
text=name, page=self.pages[name], imageId=self.imageList.getIcon(device)
|
|
97
|
+
)
|
|
98
|
+
# add/remove a placeholder depending on whether there's no pages
|
|
99
|
+
if not len(self.pages):
|
|
100
|
+
self.pages[None] = wx.Panel(self.profilesNotebook)
|
|
101
|
+
self.profilesNotebook.AddPage(text="", page=self.pages[None])
|
|
102
|
+
elif None in self.pages:
|
|
103
|
+
self.profilesNotebook.RemovePage(
|
|
104
|
+
self.profilesNotebook.FindPage(self.pages[None])
|
|
105
|
+
)
|
|
106
|
+
del self.pages[None]
|
|
107
|
+
|
|
108
|
+
def renameDevice(self, oldname, newname):
|
|
109
|
+
# set name param
|
|
110
|
+
self.devices[oldname].name = newname
|
|
111
|
+
# rename tab
|
|
112
|
+
self.profilesNotebook.SetPageText(
|
|
113
|
+
self.profilesNotebook.FindPage(self.pages[oldname]),
|
|
114
|
+
newname
|
|
115
|
+
)
|
|
116
|
+
# relocate in devices array
|
|
117
|
+
self.devices[newname] = self.devices.pop(oldname)
|
|
118
|
+
# relocate in pages array
|
|
119
|
+
self.pages[newname] = self.pages.pop(oldname)
|
|
120
|
+
# validate ok button
|
|
121
|
+
self.validate()
|
|
122
|
+
|
|
123
|
+
def onNameSelected(self, evt=None):
|
|
124
|
+
# get name
|
|
125
|
+
name = self.getCurrentName()
|
|
126
|
+
# disable whole panel if nothing is selected
|
|
127
|
+
self.devicePnl.Enable(name is not None)
|
|
128
|
+
# if mapped, show mapping
|
|
129
|
+
if name in self.pages:
|
|
130
|
+
self.profilesNotebook.ChangeSelection(
|
|
131
|
+
self.profilesNotebook.FindPage(self.pages[name])
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
self.Layout()
|
|
135
|
+
self.Refresh()
|
|
136
|
+
|
|
137
|
+
def onAddDeviceBtn(self, evt=None):
|
|
138
|
+
dlg = AddDeviceDlg(self)
|
|
139
|
+
|
|
140
|
+
if dlg.ShowModal() == wx.ID_OK:
|
|
141
|
+
# get selected device
|
|
142
|
+
device = dlg.getDevice()
|
|
143
|
+
# create Device object
|
|
144
|
+
self.devices[device.name] = device
|
|
145
|
+
|
|
146
|
+
self.populate()
|
|
147
|
+
|
|
148
|
+
def validate(self):
|
|
149
|
+
# enable/disable OK button if every page is okay
|
|
150
|
+
self.okBtn.Enable(all([
|
|
151
|
+
self.profilesNotebook.GetPage(i).warnings.OK
|
|
152
|
+
for i in range(self.profilesNotebook.GetPageCount())
|
|
153
|
+
]))
|
|
154
|
+
|
|
155
|
+
def onOK(self, evt):
|
|
156
|
+
# run on OK methods from all params
|
|
157
|
+
for i in range(self.profilesNotebook.GetPageCount()):
|
|
158
|
+
page = self.profilesNotebook.GetPage(i)
|
|
159
|
+
if hasattr(page, "onElementOk"):
|
|
160
|
+
page.onElementOk(evt)
|
|
161
|
+
# save config
|
|
162
|
+
self.devices.save()
|
|
163
|
+
# reload in prefs so changes are applied this session
|
|
164
|
+
prefs.devices.reload()
|
|
165
|
+
|
|
166
|
+
evt.Skip()
|
|
167
|
+
|
|
168
|
+
def getCurrentName(self):
|
|
169
|
+
"""
|
|
170
|
+
Get the currently selected name.
|
|
171
|
+
|
|
172
|
+
Returns
|
|
173
|
+
-------
|
|
174
|
+
str
|
|
175
|
+
Current name
|
|
176
|
+
"""
|
|
177
|
+
# get index of selection
|
|
178
|
+
i = self.namesCtrl.GetSelection()
|
|
179
|
+
# return None if none found
|
|
180
|
+
if i == wx.NOT_FOUND:
|
|
181
|
+
return None
|
|
182
|
+
# get name
|
|
183
|
+
name = self.namesCtrl.GetString(i)
|
|
184
|
+
|
|
185
|
+
return name
|