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.

Files changed (220) hide show
  1. psychopy/VERSION +1 -1
  2. psychopy/alerts/alertsCatalogue/4810.yaml +19 -0
  3. psychopy/alerts/alertsCatalogue/alertCategories.yaml +4 -0
  4. psychopy/alerts/alertsCatalogue/alertmsg.py +15 -1
  5. psychopy/alerts/alertsCatalogue/generateAlertmsg.py +2 -2
  6. psychopy/app/Resources/classic/add_many.png +0 -0
  7. psychopy/app/Resources/classic/add_many@2x.png +0 -0
  8. psychopy/app/Resources/classic/devices.png +0 -0
  9. psychopy/app/Resources/classic/devices@2x.png +0 -0
  10. psychopy/app/Resources/classic/photometer.png +0 -0
  11. psychopy/app/Resources/classic/photometer@2x.png +0 -0
  12. psychopy/app/Resources/dark/add_many.png +0 -0
  13. psychopy/app/Resources/dark/add_many@2x.png +0 -0
  14. psychopy/app/Resources/dark/devices.png +0 -0
  15. psychopy/app/Resources/dark/devices@2x.png +0 -0
  16. psychopy/app/Resources/dark/photometer.png +0 -0
  17. psychopy/app/Resources/dark/photometer@2x.png +0 -0
  18. psychopy/app/Resources/light/add_many.png +0 -0
  19. psychopy/app/Resources/light/add_many@2x.png +0 -0
  20. psychopy/app/Resources/light/devices.png +0 -0
  21. psychopy/app/Resources/light/devices@2x.png +0 -0
  22. psychopy/app/Resources/light/photometer.png +0 -0
  23. psychopy/app/Resources/light/photometer@2x.png +0 -0
  24. psychopy/app/_psychopyApp.py +35 -13
  25. psychopy/app/builder/builder.py +88 -35
  26. psychopy/app/builder/dialogs/__init__.py +69 -220
  27. psychopy/app/builder/dialogs/dlgsCode.py +29 -8
  28. psychopy/app/builder/dialogs/paramCtrls.py +1468 -904
  29. psychopy/app/builder/validators.py +25 -17
  30. psychopy/app/coder/coder.py +12 -1
  31. psychopy/app/coder/repl.py +5 -2
  32. psychopy/app/colorpicker/__init__.py +1 -1
  33. psychopy/app/deviceManager/__init__.py +1 -0
  34. psychopy/app/deviceManager/addDialog.py +218 -0
  35. psychopy/app/deviceManager/dialog.py +185 -0
  36. psychopy/app/deviceManager/panel.py +191 -0
  37. psychopy/app/deviceManager/utils.py +60 -0
  38. psychopy/app/idle.py +7 -0
  39. psychopy/app/locale/ar_001/LC_MESSAGE/messages.mo +0 -0
  40. psychopy/app/locale/ar_001/LC_MESSAGE/messages.po +12695 -10592
  41. psychopy/app/locale/cs_CZ/LC_MESSAGE/messages.mo +0 -0
  42. psychopy/app/locale/cs_CZ/LC_MESSAGE/messages.po +10199 -24
  43. psychopy/app/locale/da_DK/LC_MESSAGE/messages.mo +0 -0
  44. psychopy/app/locale/da_DK/LC_MESSAGE/messages.po +10199 -24
  45. psychopy/app/locale/de_DE/LC_MESSAGE/messages.mo +0 -0
  46. psychopy/app/locale/de_DE/LC_MESSAGE/messages.po +11221 -9712
  47. psychopy/app/locale/el_GR/LC_MESSAGE/messages.mo +0 -0
  48. psychopy/app/locale/el_GR/LC_MESSAGE/messages.po +10200 -25
  49. psychopy/app/locale/en_NZ/LC_MESSAGE/messages.mo +0 -0
  50. psychopy/app/locale/en_NZ/LC_MESSAGE/messages.po +10200 -25
  51. psychopy/app/locale/en_US/LC_MESSAGE/messages.mo +0 -0
  52. psychopy/app/locale/en_US/LC_MESSAGE/messages.po +10195 -18
  53. psychopy/app/locale/es_CO/LC_MESSAGE/messages.mo +0 -0
  54. psychopy/app/locale/es_CO/LC_MESSAGE/messages.po +11917 -9101
  55. psychopy/app/locale/es_ES/LC_MESSAGE/messages.mo +0 -0
  56. psychopy/app/locale/es_ES/LC_MESSAGE/messages.po +11924 -9103
  57. psychopy/app/locale/es_US/LC_MESSAGE/messages.mo +0 -0
  58. psychopy/app/locale/es_US/LC_MESSAGE/messages.po +11917 -9101
  59. psychopy/app/locale/et_EE/LC_MESSAGE/messages.mo +0 -0
  60. psychopy/app/locale/et_EE/LC_MESSAGE/messages.po +11084 -9569
  61. psychopy/app/locale/fa_IR/LC_MESSAGE/messages.mo +0 -0
  62. psychopy/app/locale/fa_IR/LC_MESSAGE/messages.po +11590 -5806
  63. psychopy/app/locale/fi_FI/LC_MESSAGE/messages.mo +0 -0
  64. psychopy/app/locale/fi_FI/LC_MESSAGE/messages.po +10199 -24
  65. psychopy/app/locale/fr_FR/LC_MESSAGE/messages.mo +0 -0
  66. psychopy/app/locale/fr_FR/LC_MESSAGE/messages.po +11091 -9577
  67. psychopy/app/locale/he_IL/LC_MESSAGE/messages.mo +0 -0
  68. psychopy/app/locale/he_IL/LC_MESSAGE/messages.po +11072 -9549
  69. psychopy/app/locale/hi_IN/LC_MESSAGE/messages.mo +0 -0
  70. psychopy/app/locale/hi_IN/LC_MESSAGE/messages.po +11071 -9559
  71. psychopy/app/locale/hu_HU/LC_MESSAGE/messages.mo +0 -0
  72. psychopy/app/locale/hu_HU/LC_MESSAGE/messages.po +10200 -25
  73. psychopy/app/locale/it_IT/LC_MESSAGE/messages.mo +0 -0
  74. psychopy/app/locale/it_IT/LC_MESSAGE/messages.po +11072 -9560
  75. psychopy/app/locale/ja_JP/LC_MESSAGE/messages.mo +0 -0
  76. psychopy/app/locale/ja_JP/LC_MESSAGE/messages.po +1485 -1137
  77. psychopy/app/locale/ko_KR/LC_MESSAGE/messages.mo +0 -0
  78. psychopy/app/locale/ko_KR/LC_MESSAGE/messages.po +10199 -24
  79. psychopy/app/locale/ms_MY/LC_MESSAGE/messages.mo +0 -0
  80. psychopy/app/locale/ms_MY/LC_MESSAGE/messages.po +11463 -8757
  81. psychopy/app/locale/nl_NL/LC_MESSAGE/messages.mo +0 -0
  82. psychopy/app/locale/nl_NL/LC_MESSAGE/messages.po +10200 -25
  83. psychopy/app/locale/nn_NO/LC_MESSAGE/messages.mo +0 -0
  84. psychopy/app/locale/nn_NO/LC_MESSAGE/messages.po +10200 -25
  85. psychopy/app/locale/pl_PL/LC_MESSAGE/messages.mo +0 -0
  86. psychopy/app/locale/pl_PL/LC_MESSAGE/messages.po +10200 -25
  87. psychopy/app/locale/pt_PT/LC_MESSAGE/messages.mo +0 -0
  88. psychopy/app/locale/pt_PT/LC_MESSAGE/messages.po +11288 -9434
  89. psychopy/app/locale/ro_RO/LC_MESSAGE/messages.mo +0 -0
  90. psychopy/app/locale/ro_RO/LC_MESSAGE/messages.po +10200 -25
  91. psychopy/app/locale/ru_RU/LC_MESSAGE/messages.mo +0 -0
  92. psychopy/app/locale/ru_RU/LC_MESSAGE/messages.po +10199 -24
  93. psychopy/app/locale/sv_SE/LC_MESSAGE/messages.mo +0 -0
  94. psychopy/app/locale/sv_SE/LC_MESSAGE/messages.po +11441 -8747
  95. psychopy/app/locale/tr_TR/LC_MESSAGE/messages.mo +0 -0
  96. psychopy/app/locale/tr_TR/LC_MESSAGE/messages.po +11069 -9545
  97. psychopy/app/locale/zh_CN/LC_MESSAGE/messages.mo +0 -0
  98. psychopy/app/locale/zh_CN/LC_MESSAGE/messages.po +12085 -8268
  99. psychopy/app/locale/zh_TW/LC_MESSAGE/messages.mo +0 -0
  100. psychopy/app/locale/zh_TW/LC_MESSAGE/messages.po +11929 -8022
  101. psychopy/app/plugin_manager/dialog.py +12 -3
  102. psychopy/app/plugin_manager/packageIndex.py +303 -0
  103. psychopy/app/plugin_manager/packages.py +203 -63
  104. psychopy/app/plugin_manager/plugins.py +120 -240
  105. psychopy/app/preferencesDlg.py +6 -1
  106. psychopy/app/psychopyApp.py +16 -4
  107. psychopy/app/runner/runner.py +10 -2
  108. psychopy/app/runner/scriptProcess.py +8 -3
  109. psychopy/app/stdout/stdOutRich.py +11 -4
  110. psychopy/app/themes/icons.py +3 -0
  111. psychopy/app/utils.py +61 -0
  112. psychopy/data/experiment.py +133 -23
  113. psychopy/data/routine.py +12 -0
  114. psychopy/data/staircase.py +42 -20
  115. psychopy/data/trial.py +20 -12
  116. psychopy/data/utils.py +42 -2
  117. psychopy/demos/builder/Experiments/dragAndDrop/drag_and_drop.psyexp +22 -5
  118. psychopy/demos/builder/Experiments/dragAndDrop/stimuli/solutions.xlsx +0 -0
  119. psychopy/demos/builder/Experiments/stroopVoice/stroopVoice.psyexp +2 -12
  120. psychopy/demos/builder/Feature Demos/buttonBox/buttonBoxDemo.psyexp +3 -8
  121. psychopy/demos/builder/Feature Demos/movies/movie.psyexp +220 -0
  122. psychopy/demos/builder/Feature Demos/movies/readme.md +3 -0
  123. psychopy/demos/builder/Feature Demos/visualValidator/visualValidator.psyexp +1 -2
  124. psychopy/demos/builder/Hardware/camera/camera.psyexp +3 -16
  125. psychopy/demos/builder/Hardware/microphone/microphone.psyexp +3 -16
  126. psychopy/demos/coder/hardware/hdf5_extract.py +133 -0
  127. psychopy/event.py +20 -15
  128. psychopy/experiment/_experiment.py +86 -10
  129. psychopy/experiment/components/__init__.py +3 -10
  130. psychopy/experiment/components/_base.py +9 -20
  131. psychopy/experiment/components/button/__init__.py +1 -1
  132. psychopy/experiment/components/buttonBox/__init__.py +50 -54
  133. psychopy/experiment/components/camera/__init__.py +137 -359
  134. psychopy/experiment/components/keyboard/__init__.py +17 -24
  135. psychopy/experiment/components/microphone/__init__.py +61 -110
  136. psychopy/experiment/components/movie/__init__.py +2 -3
  137. psychopy/experiment/components/serialOut/__init__.py +192 -93
  138. psychopy/experiment/components/settings/__init__.py +45 -27
  139. psychopy/experiment/components/sound/__init__.py +82 -73
  140. psychopy/experiment/components/soundsensor/__init__.py +43 -80
  141. psychopy/experiment/devices.py +303 -0
  142. psychopy/experiment/exports.py +20 -18
  143. psychopy/experiment/flow.py +7 -0
  144. psychopy/experiment/loops.py +47 -29
  145. psychopy/experiment/monitor.py +74 -0
  146. psychopy/experiment/params.py +48 -10
  147. psychopy/experiment/plugins.py +28 -108
  148. psychopy/experiment/py2js_transpiler.py +1 -1
  149. psychopy/experiment/routines/__init__.py +1 -1
  150. psychopy/experiment/routines/_base.py +59 -24
  151. psychopy/experiment/routines/audioValidator/__init__.py +19 -155
  152. psychopy/experiment/routines/visualValidator/__init__.py +25 -25
  153. psychopy/hardware/__init__.py +20 -57
  154. psychopy/hardware/button.py +15 -2
  155. psychopy/hardware/camera/__init__.py +2237 -1394
  156. psychopy/hardware/joystick/__init__.py +1 -1
  157. psychopy/hardware/keyboard.py +5 -8
  158. psychopy/hardware/listener.py +4 -1
  159. psychopy/hardware/manager.py +75 -35
  160. psychopy/hardware/microphone.py +52 -6
  161. psychopy/hardware/monitor.py +144 -0
  162. psychopy/hardware/photometer/__init__.py +156 -117
  163. psychopy/hardware/serialdevice.py +16 -2
  164. psychopy/hardware/soundsensor.py +4 -1
  165. psychopy/iohub/devices/deviceConfigValidation.py +2 -1
  166. psychopy/iohub/devices/keyboard/darwin.py +8 -5
  167. psychopy/iohub/util/__init__.py +7 -8
  168. psychopy/localization/generateTranslationTemplate.py +208 -116
  169. psychopy/localization/messages.pot +4305 -3502
  170. psychopy/monitors/MonitorCenter.py +174 -74
  171. psychopy/plugins/__init__.py +6 -4
  172. psychopy/preferences/devices.py +80 -0
  173. psychopy/preferences/generateHints.py +2 -1
  174. psychopy/preferences/preferences.py +35 -11
  175. psychopy/scripts/psychopy-pkgutil.py +969 -0
  176. psychopy/scripts/psyexpCompile.py +1 -1
  177. psychopy/session.py +34 -38
  178. psychopy/sound/__init__.py +6 -260
  179. psychopy/sound/audioclip.py +164 -0
  180. psychopy/sound/backend_ptb.py +8 -0
  181. psychopy/sound/backend_pygame.py +10 -0
  182. psychopy/sound/backend_pysound.py +9 -0
  183. psychopy/sound/backends/__init__.py +0 -0
  184. psychopy/sound/microphone.py +3 -0
  185. psychopy/sound/sound.py +58 -0
  186. psychopy/tests/data/correctScript/python/correctNoiseStimComponent.py +1 -1
  187. psychopy/tests/data/duplicateHeaders.csv +2 -0
  188. psychopy/tests/test_app/test_builder/test_BuilderFrame.py +22 -7
  189. psychopy/tests/test_app/test_builder/test_CompileFromBuilder.py +0 -2
  190. psychopy/tests/test_data/test_utils.py +5 -1
  191. psychopy/tests/test_experiment/test_components/test_ButtonBoxComponent.py +22 -2
  192. psychopy/tests/test_hardware/test_ports.py +0 -12
  193. psychopy/tests/test_tools/test_stringtools.py +1 -1
  194. psychopy/tools/attributetools.py +12 -5
  195. psychopy/tools/fontmanager.py +17 -14
  196. psychopy/tools/movietools.py +43 -2
  197. psychopy/tools/stringtools.py +33 -8
  198. psychopy/tools/versionchooser.py +1 -1
  199. psychopy/validation/audio.py +5 -1
  200. psychopy/validation/visual.py +5 -1
  201. psychopy/visual/basevisual.py +8 -7
  202. psychopy/visual/circle.py +2 -2
  203. psychopy/visual/image.py +29 -109
  204. psychopy/visual/movies/__init__.py +1800 -313
  205. psychopy/visual/polygon.py +4 -0
  206. psychopy/visual/shape.py +2 -2
  207. psychopy/visual/window.py +34 -11
  208. psychopy/voicekey/__init__.py +41 -669
  209. psychopy/voicekey/labjack_vks.py +7 -48
  210. psychopy/voicekey/parallel_vks.py +7 -42
  211. psychopy/voicekey/vk_tools.py +114 -263
  212. {psychopy-2025.1.1.dist-info → psychopy-2025.2.1.dist-info}/METADATA +17 -11
  213. {psychopy-2025.1.1.dist-info → psychopy-2025.2.1.dist-info}/RECORD +216 -184
  214. {psychopy-2025.1.1.dist-info → psychopy-2025.2.1.dist-info}/WHEEL +1 -1
  215. psychopy/visual/movies/players/__init__.py +0 -62
  216. psychopy/visual/movies/players/ffpyplayer_player.py +0 -1401
  217. psychopy/voicekey/demo_vks.py +0 -12
  218. psychopy/voicekey/signal.py +0 -42
  219. {psychopy-2025.1.1.dist-info → psychopy-2025.2.1.dist-info}/entry_points.txt +0 -0
  220. {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
- """`True` if there are no warnings (`bool`)."""
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.getVariables(code))
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
@@ -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 = "PsychoPy Coder (v{})".format(self.app.version)
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
@@ -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.BeginBusyCursor()
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.EndBusyCursor()
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.Close()
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