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
@@ -7,6 +7,7 @@ from wx.lib import scrolledpanel
7
7
  import webbrowser
8
8
  from PIL import Image as pil
9
9
 
10
+ from psychopy import logging
10
11
  from psychopy.tools import pkgtools
11
12
  from psychopy.app.themes import theme, handlers, colors, icons
12
13
  from psychopy.tools import stringtools as st
@@ -22,6 +23,11 @@ import sys
22
23
  import json
23
24
  import glob
24
25
 
26
+ from .packageIndex import (
27
+ loadPackageIndex,
28
+ getPluginPackages,
29
+ isUserPackageInstalled)
30
+
25
31
 
26
32
  class AuthorInfo:
27
33
  """Plugin author information.
@@ -97,7 +103,7 @@ class PluginInfo:
97
103
  pipname, name="",
98
104
  author=None, homepage="", docs="", repo="",
99
105
  keywords=None, version=(None, None),
100
- icon=None, description="", **kwargs):
106
+ icon=None, description="", releases=[], **kwargs):
101
107
  self.pipname = pipname
102
108
  self.name = name
103
109
  self.author = author
@@ -107,13 +113,11 @@ class PluginInfo:
107
113
  self.icon = icon
108
114
  self.description = description
109
115
  self.keywords = keywords or []
110
- self.version = VersionRange(*version)
116
+ self.version = VersionRange(version[0], version[1])
117
+ self.releases = releases
111
118
 
112
119
  self.parent = None # set after
113
120
 
114
- # icon graphic
115
- self._icon = None
116
-
117
121
  def __repr__(self):
118
122
  return (f"<psychopy.plugins.PluginInfo: {self.name} "
119
123
  f"[{self.pipname}] by {self.author}>")
@@ -140,52 +144,7 @@ class PluginInfo:
140
144
 
141
145
  @property
142
146
  def icon(self):
143
- # check if the directory for the plugin cache exists, create it otherwise
144
- appPluginCacheDir = os.path.join(
145
- prefs.paths['userCacheDir'], 'appCache', 'plugins')
146
- try:
147
- os.makedirs(appPluginCacheDir, exist_ok=True)
148
- except OSError as err:
149
- if err.errno != errno.EEXIST:
150
- raise
151
-
152
- if isinstance(self._requestedIcon, str):
153
- if st.is_url(self._requestedIcon):
154
- # get the file name from the URL in the JSON
155
- fname = str(self._requestedIcon).split("/")
156
- if len(fname) > 1:
157
- fname = fname[-1]
158
- else:
159
- pass # not a valid URL, use broken image icon
160
-
161
- # check if the icon is already in the cache, use it if so
162
- if fname in os.listdir(appPluginCacheDir):
163
- self._icon = utils.ImageData(os.path.join(
164
- appPluginCacheDir, fname))
165
- return self._icon
166
-
167
- # if not, download it
168
- if st.is_url(self._requestedIcon):
169
- # download to cache directory
170
- ext = "." + str(self._requestedIcon).split(".")[-1]
171
- if ext in pil.registered_extensions():
172
- content = requests.get(self._requestedIcon).content
173
- writeOut = os.path.join(appPluginCacheDir, fname)
174
- with open(writeOut, 'wb') as f:
175
- f.write(content)
176
- self._icon = utils.ImageData(os.path.join(
177
- appPluginCacheDir, fname))
178
-
179
- elif st.is_file(self._requestedIcon):
180
- self._icon = utils.ImageData(self._requestedIcon)
181
- else:
182
- raise ValueError("Invalid icon URL or file path.")
183
-
184
- return self._icon
185
-
186
- # icon already loaded into memory, just return that
187
- if hasattr(self, "_icon"):
188
- return self._icon
147
+ return self._requestedIcon
189
148
 
190
149
  @icon.setter
191
150
  def icon(self, value):
@@ -226,7 +185,7 @@ class PluginInfo:
226
185
 
227
186
  @property
228
187
  def installed(self):
229
- return pkgtools.isInstalled(self.pipname)
188
+ return isUserPackageInstalled(self.pipname)
230
189
 
231
190
  @property
232
191
  def installedVersion(self):
@@ -238,8 +197,7 @@ class PluginInfo:
238
197
  """
239
198
  if self.installed:
240
199
  try:
241
- ver = plugins.pluginMetadata(self.pipname)["Version"]
242
- return Version(ver)
200
+ return Version(self.version)
243
201
  except:
244
202
  return None
245
203
  else:
@@ -272,17 +230,18 @@ class PluginInfo:
272
230
  List of version strings
273
231
  """
274
232
  # get package info
275
- info = pkgtools.getPypiInfo(self.pipname, silence=True)
276
- # convert all release numbers to Version objects
277
- releases = []
278
- for release in info.get('releases', []):
279
- try:
280
- ver = Version(release)
281
- releases.append(ver)
282
- except:
283
- continue
284
-
285
- return releases
233
+ # info = pkgtools.getPypiInfo(self.pipname, silence=True)
234
+ # # convert all release numbers to Version objects
235
+ # releases = []
236
+ # for release in info.get('releases', []):
237
+ # try:
238
+ # ver = Version(release)
239
+ # releases.append(ver)
240
+ # except:
241
+ # continue
242
+
243
+ # lookup releases
244
+ return self.releases
286
245
 
287
246
 
288
247
  class PluginManagerPanel(wx.Panel, handlers.ThemeMixin):
@@ -333,7 +292,7 @@ class PluginManagerPanel(wx.Panel, handlers.ThemeMixin):
333
292
  self.pluginViewer.theme = self.theme
334
293
 
335
294
 
336
- class PluginBrowserList(scrolledpanel.ScrolledPanel, handlers.ThemeMixin):
295
+ class PluginBrowserList(wx.Panel, handlers.ThemeMixin):
337
296
  class PluginListItem(wx.Window, handlers.ThemeMixin):
338
297
  """
339
298
  Individual item pointing to a plugin
@@ -396,7 +355,7 @@ class PluginBrowserList(scrolledpanel.ScrolledPanel, handlers.ThemeMixin):
396
355
  """
397
356
  Return parent's linked viewer when asked for viewer
398
357
  """
399
- return self.parent.viewer
358
+ return self.parent.Parent.viewer
400
359
 
401
360
  def updateInfo(self):
402
361
  # update install state
@@ -458,7 +417,7 @@ class PluginBrowserList(scrolledpanel.ScrolledPanel, handlers.ThemeMixin):
458
417
  evt.Skip()
459
418
 
460
419
  def onSelect(self, evt=None):
461
- self.parent.setSelection(self)
420
+ self.parent.Parent.setSelection(self)
462
421
 
463
422
  def markInstalled(self, installed=True):
464
423
  """
@@ -471,7 +430,7 @@ class PluginBrowserList(scrolledpanel.ScrolledPanel, handlers.ThemeMixin):
471
430
  """
472
431
  markInstalled(
473
432
  pluginItem=self,
474
- pluginPanel=self.parent.viewer,
433
+ pluginPanel=self.parent.Parent.viewer,
475
434
  installed=installed
476
435
  )
477
436
 
@@ -548,29 +507,39 @@ class PluginBrowserList(scrolledpanel.ScrolledPanel, handlers.ThemeMixin):
548
507
 
549
508
 
550
509
  def __init__(self, parent, stream, viewer=None):
551
- scrolledpanel.ScrolledPanel.__init__(self, parent=parent, style=wx.VSCROLL)
510
+ wx.Panel.__init__(self, parent=parent)
552
511
  self.parent = parent
553
512
  self.viewer = viewer
554
513
  self.stream = stream
555
514
  # Setup sizer
556
515
  self.border = wx.BoxSizer(wx.VERTICAL)
557
516
  self.SetSizer(self.border)
558
- self.sizer = wx.BoxSizer(wx.VERTICAL)
559
- self.border.Add(self.sizer, proportion=1, border=6, flag=wx.ALL | wx.EXPAND)
560
- # Add search box
561
- self.searchCtrl = wx.SearchCtrl(self)
562
- self.sizer.Add(self.searchCtrl, border=9, flag=wx.ALL | wx.EXPAND)
517
+
518
+ self.searchCtrl = wx.SearchCtrl(self, style=wx.TE_PROCESS_ENTER)
563
519
  self.searchCtrl.Bind(wx.EVT_SEARCH, self.search)
520
+ self.searchCtrl.Bind(wx.EVT_TEXT, self.onSearchText)
521
+ self.searchCtrl.Bind(wx.EVT_SEARCH_CANCEL, self.onSearchCancel)
522
+
523
+ self.border.Add(self.searchCtrl, border=9, flag=wx.ALL | wx.EXPAND)
524
+
525
+ self.sizer = wx.BoxSizer(wx.VERTICAL)
526
+ self.scrollArea = scrolledpanel.ScrolledPanel(self, style=wx.VSCROLL)
527
+ self.scrollArea.SetSizer(self.sizer)
528
+ winCol = wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW)
529
+ self.scrollArea.SetBackgroundColour(winCol)
530
+
531
+ self.border.Add(self.scrollArea, proportion=1, border=6, flag=wx.ALL | wx.EXPAND)
532
+
564
533
  # Setup items sizers & labels
565
534
  self.itemSizer = wx.BoxSizer(wx.VERTICAL)
566
535
  self.sizer.Add(self.itemSizer, proportion=1, border=3, flag=wx.ALL | wx.EXPAND)
567
- self.badItemLbl = wx.StaticText(self, label=_translate("Not for PsychoPy {}:").format(__version__))
536
+ self.badItemLbl = wx.StaticText(self.scrollArea, label=_translate("Not for PsychoPy {}:").format(__version__))
568
537
  self.sizer.Add(self.badItemLbl, border=9, flag=wx.ALL | wx.EXPAND)
569
538
  self.badItemSizer = wx.BoxSizer(wx.VERTICAL)
570
539
  self.sizer.Add(self.badItemSizer, border=3, flag=wx.ALL | wx.EXPAND)
571
540
  # ctrl to display when plugins can't be retrieved
572
541
  self.errorCtrl = utils.MarkdownCtrl(
573
- self, value=_translate(
542
+ self.scrollArea, value=_translate(
574
543
  "Could not retrieve plugins. Try restarting the PsychoPy app and make sure you "
575
544
  "are connected to the internet."
576
545
  ),
@@ -585,7 +554,9 @@ class PluginBrowserList(scrolledpanel.ScrolledPanel, handlers.ThemeMixin):
585
554
  )
586
555
  self.uninstallAllBtn.SetBitmapMargins(6, 3)
587
556
  self.uninstallAllBtn.Bind(wx.EVT_BUTTON, self.onUninstallAll)
588
- self.sizer.Add(self.uninstallAllBtn, border=12, flag=wx.ALL | wx.CENTER)
557
+ self.border.Add(self.uninstallAllBtn, border=12, flag=wx.ALL | wx.CENTER)
558
+
559
+ # self.border.Add(self.sizer, proportion=1, border=6, flag=wx.ALL | wx.EXPAND)
589
560
 
590
561
  # Bind deselect
591
562
  self.Bind(wx.EVT_LEFT_DOWN, self.onDeselect)
@@ -606,14 +577,15 @@ class PluginBrowserList(scrolledpanel.ScrolledPanel, handlers.ThemeMixin):
606
577
  # put installed packages at top of list
607
578
  items.sort(key=lambda obj: obj.installed, reverse=True)
608
579
  for item in items:
609
- item.setParent(self)
580
+ item.setParent(self.scrollArea)
610
581
  self.appendItem(item)
611
582
  # if we got no items, display error message
612
583
  if not len(items):
613
584
  self.errorCtrl.Show()
614
585
  # layout
615
586
  self.Layout()
616
- self.SetupScrolling()
587
+ self.scrollArea.Layout()
588
+ self.scrollArea.SetupScrolling()
617
589
 
618
590
  def updateInfo(self):
619
591
  for item in self.items:
@@ -621,6 +593,10 @@ class PluginBrowserList(scrolledpanel.ScrolledPanel, handlers.ThemeMixin):
621
593
 
622
594
  def search(self, evt=None):
623
595
  searchTerm = self.searchCtrl.GetValue().strip()
596
+ if searchTerm:
597
+ # Show cancel button if search term is non-empty
598
+ self.searchCtrl.ShowCancelButton(True)
599
+
624
600
  for item in self.items:
625
601
  # Otherwise show/hide according to search
626
602
  match = any((
@@ -632,7 +608,39 @@ class PluginBrowserList(scrolledpanel.ScrolledPanel, handlers.ThemeMixin):
632
608
  ))
633
609
  item.Show(match)
634
610
 
635
- self.Layout()
611
+ self.scrollArea.Layout()
612
+
613
+ def onSearchText(self, evt=None):
614
+ """On text change in search box. Used to refresh the package list if
615
+ the search term is empty since the box doesn't trigger a search
616
+ when the text is cleared.
617
+ """
618
+ # Get search term
619
+ searchTerm = self.searchCtrl.GetValue()
620
+
621
+ if not searchTerm:
622
+ # If empty, refresh
623
+ for item in self.items:
624
+ item.Show(True)
625
+ item.Update()
626
+ self.scrollArea.Layout()
627
+ self.searchCtrl.ShowCancelButton(False)
628
+ return
629
+
630
+ if evt is not None:
631
+ evt.Skip()
632
+
633
+ def onSearchCancel(self, evt=None):
634
+ """On search cancel button pressed. Used to refresh the package list
635
+ if the search term is empty since the box doesn't trigger a search
636
+ when the text is cleared.
637
+ """
638
+ self.searchCtrl.SetValue("")
639
+ for item in self.items:
640
+ item.Show(True)
641
+ item.Update()
642
+ self.scrollArea.Layout()
643
+ self.searchCtrl.ShowCancelButton(False)
636
644
 
637
645
  def getChanges(self):
638
646
  """
@@ -744,7 +752,7 @@ class PluginBrowserList(scrolledpanel.ScrolledPanel, handlers.ThemeMixin):
744
752
  self.badItemLbl.SetFont(fonts.appTheme['h6'].obj)
745
753
 
746
754
  def appendItem(self, info):
747
- item = self.PluginListItem(self, info)
755
+ item = self.PluginListItem(self.scrollArea, info)
748
756
  self.items.append(item)
749
757
  if __version__ in item.info.version:
750
758
  self.itemSizer.Add(item, border=6, flag=wx.ALL | wx.EXPAND)
@@ -1024,22 +1032,20 @@ class PluginDetailsPanel(wx.Panel, handlers.ThemeMixin):
1024
1032
  self._info = value
1025
1033
  # Set icon
1026
1034
  icon = value.icon
1027
- if icon is None:
1028
- icon = wx.Bitmap()
1029
- if isinstance(icon, pil.Image):
1030
- # Resize to fit ctrl
1031
- icon = icon.resize(size=self.iconSize)
1032
- # Supply an alpha channel
1033
- alpha = icon.tobytes("raw", "A")
1034
- icon = wx.Bitmap.FromBufferAndAlpha(
1035
- width=icon.size[0],
1036
- height=icon.size[1],
1037
- data=icon.tobytes("raw", "RGB"),
1038
- alpha=alpha
1039
- )
1040
- if not isinstance(icon, wx.Bitmap):
1041
- icon = wx.Bitmap(icon)
1042
- self.icon.SetBitmap(icon)
1035
+
1036
+ if icon is not None:
1037
+ # this will be a URL, get the file name from it to lookup in cache
1038
+ iconFileName = os.path.basename(icon)
1039
+
1040
+ appPluginCacheDir = os.path.join(
1041
+ prefs.paths['userCacheDir'], 'appCache', 'plugins')
1042
+
1043
+ # check if the icon is in the cache
1044
+ iconCachePath = os.path.join(appPluginCacheDir, iconFileName)
1045
+ if os.path.exists(iconCachePath):
1046
+ iconBitmap = wx.Bitmap(iconCachePath)
1047
+ self.icon.SetBitmap(iconBitmap)
1048
+
1043
1049
  # Set names
1044
1050
  self.title.SetLabelText(value.name)
1045
1051
  self.pipName.SetLabelText(value.pipname)
@@ -1316,127 +1322,8 @@ def getAllPluginDetails():
1316
1322
  List of plugin details.
1317
1323
 
1318
1324
  """
1319
- # check if the local `plugins.json` file exists and is up to date
1320
- appPluginCacheDir = os.path.join(
1321
- prefs.paths['userCacheDir'], 'appCache', 'plugins')
1322
-
1323
- # create the cache directory if it doesn't exist
1324
- if not os.path.exists(appPluginCacheDir):
1325
- try:
1326
- os.makedirs(appPluginCacheDir)
1327
- except OSError:
1328
- pass
1329
-
1330
- # where the database is expected to be
1331
- pluginDatabaseFile = Path(appPluginCacheDir) / "plugins.json"
1332
-
1333
- def downloadPluginDatabase(srcURL="https://psychopy.org/plugins.json"):
1334
- """Downloads the plugin database from the server and returns the text
1335
- as a string. If the download fails, returns None.
1336
-
1337
- Parameters
1338
- ----------
1339
- srcURL : str
1340
- The URL to download the plugin database from.
1341
-
1342
- Returns
1343
- -------
1344
- list or None
1345
- The plugin database as a list, or None if the download failed.
1346
-
1347
- """
1348
- global redownloadPlugins
1349
- # if plugins already up to date, skip
1350
- if not redownloadPlugins:
1351
- return None
1352
- # download database from website
1353
- try:
1354
- resp = requests.get(srcURL)
1355
- except requests.exceptions.ConnectionError:
1356
- # if connection to website fails, return nothing
1357
- return None
1358
- # if download failed, return nothing
1359
- if resp.status_code == 404:
1360
- return None
1361
- # otherwise get as a string
1362
- value = resp.text
1363
-
1364
- if value is None or value == "":
1365
- return None
1366
-
1367
- # make sure we are using UTF-8 encoding
1368
- value = value.encode('utf-8', 'ignore').decode('utf-8')
1369
-
1370
- # attempt to parse JSON
1371
- try:
1372
- database = json.loads(value)
1373
- except json.decoder.JSONDecodeError:
1374
- # if JSON parse fails, return nothing
1375
- return None
1376
- # if we made it this far, mark plugins as not needing update
1377
- redownloadPlugins = False
1378
-
1379
- return database
1380
-
1381
- def readLocalPluginDatabase(srcFile):
1382
- """Read the local plugin database file (if it exists) and return the
1383
- text as a string. If the file doesn't exist, returns None.
1384
-
1385
- Parameters
1386
- ----------
1387
- srcFile : pathlib.Path
1388
- The expected path to the plugin database file.
1389
-
1390
- Returns
1391
- -------
1392
- list or None
1393
- The plugin database as a list, or None if the file doesn't exist.
1394
-
1395
- """
1396
- # if source file doesn't exist, return nothing
1397
- if not srcFile.is_file():
1398
- return None
1399
- # attempt to parse JSON
1400
- try:
1401
- with srcFile.open("r", encoding="utf-8", errors="ignore") as f:
1402
- return json.load(f)
1403
- except json.decoder.JSONDecodeError:
1404
- # if JSON parse fails, return nothing
1405
- return None
1406
-
1407
- def deletePluginDlgCache():
1408
- """Delete the local plugin database file and cached files related to
1409
- the Plugin dialog.
1410
- """
1411
- if os.path.exists(appPluginCacheDir):
1412
- files = glob.glob(os.path.join(appPluginCacheDir, '*'))
1413
- for f in files:
1414
- os.remove(f)
1415
-
1416
- # get a copy of the plugin database from the server, check if it's newer
1417
- # than the local copy, and if so, replace the local copy
1418
-
1419
- # get remote database
1420
- serverPluginDatabase = downloadPluginDatabase()
1421
- # get local database
1422
- localPluginDatabase = readLocalPluginDatabase(pluginDatabaseFile)
1423
-
1424
- if serverPluginDatabase is not None:
1425
- # if we have a database from the remote, use it
1426
- pluginDatabase = serverPluginDatabase
1427
- # if the file contents has changed, delete cached icons and etc.
1428
- if str(pluginDatabase) != str(localPluginDatabase):
1429
- deletePluginDlgCache()
1430
- # write new contents to file
1431
- with pluginDatabaseFile.open("w", encoding='utf-8') as f:
1432
- json.dump(pluginDatabase, f, indent=True)
1433
-
1434
- elif localPluginDatabase is not None:
1435
- # otherwise use cached
1436
- pluginDatabase = localPluginDatabase
1437
- else:
1438
- # if we have neither, treat as blank list
1439
- pluginDatabase = []
1325
+ loadPackageIndex()
1326
+ pluginDatabase = getPluginPackages(asList=True)
1440
1327
 
1441
1328
  # check if we need to update plugin objects, if not return the cached data
1442
1329
  global _pluginObjects
@@ -1444,32 +1331,25 @@ def getAllPluginDetails():
1444
1331
  if not requiresRefresh:
1445
1332
  return _pluginObjects
1446
1333
 
1334
+ import time
1335
+
1336
+ startT = time.time()
1447
1337
  # Create PluginInfo objects from info list
1448
1338
  objs = []
1449
1339
  for info in pluginDatabase:
1340
+ if info['version'] is None:
1341
+ # if no version info, set to None
1342
+ info['version'] = (None, None)
1343
+ elif isinstance(info['version'], str):
1344
+ # if version is a string, convert to tuple
1345
+ info['version'] = (info['version'], None)
1346
+ elif isinstance(info['version'], (list, tuple)):
1347
+ # if version is a tuple, convert to tuple of tuples
1348
+ info['version'] = (info['version'][0], info['version'][1])
1349
+
1450
1350
  objs.append(PluginInfo(**info))
1451
1351
 
1452
- # Add info objects for local plugins which aren't found online
1453
- localPlugins = plugins.listPlugins(which='all')
1454
- for name in localPlugins:
1455
- # Check whether plugin is accounted for
1456
- if name not in objs:
1457
- # If not, get its metadata
1458
- data = plugins.pluginMetadata(name)
1459
- # Create best representation we can from metadata
1460
- author = AuthorInfo(
1461
- name=data.get('Author', ''),
1462
- email=data.get('Author-email', ''),
1463
- )
1464
- info = PluginInfo(
1465
- pipname=name, name=name,
1466
- author=author,
1467
- homepage=data.get('Home-page', ''),
1468
- keywords=data.get('Keywords', ''),
1469
- description=data.get('Summary', ''),
1470
- )
1471
- # Add to list
1472
- objs.append(info)
1352
+ logging.debug("Plugin info loaded in %.2f seconds" % (time.time() - startT))
1473
1353
 
1474
1354
  _pluginObjects = objs # cache for later
1475
1355
 
@@ -41,10 +41,15 @@ class PrefPropGrid(wx.Panel):
41
41
  # make splitter so panels are resizable
42
42
  self.splitter = wx.SplitterWindow(self)
43
43
  bSizer1.Add(self.splitter, proportion=1, border=6, flag=wx.EXPAND | wx.ALL)
44
+
44
45
  # tabs panel
46
+ # note - linux needs special style handling
47
+ prefPageStyle = \
48
+ wx.LC_LIST if wx.Platform == "__WXMSW__" else wx.LC_SMALL_ICON
45
49
  self.lstPrefPages = wx.ListCtrl(
46
50
  self.splitter, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize,
47
- wx.LC_ALIGN_TOP | wx.LC_LIST | wx.LC_SINGLE_SEL)
51
+ prefPageStyle | wx.LC_ALIGN_TOP | wx.LC_SINGLE_SEL)
52
+
48
53
  # images for tabs panel
49
54
  prefsImageSize = wx.Size(48, 48)
50
55
  self.prefsIndex = 0
@@ -19,9 +19,6 @@ import psychopy.locale_setup # noqa
19
19
 
20
20
 
21
21
  def main():
22
- from psychopy.app import startApp, quitApp
23
- from psychopy.preferences import prefs
24
-
25
22
  # parser to process input arguments
26
23
  argParser = argparse.ArgumentParser(
27
24
  prog="PsychoPyApp", description=(
@@ -80,7 +77,7 @@ depends on the type of the [files]:
80
77
  )
81
78
  # add option to hide splash
82
79
  argParser.add_argument(
83
- "--no-splash", dest="showSplash", action="store_false", default=prefs.app['showSplash'], help=(
80
+ "--no-splash", dest="showSplash", action="store_false", default=None, help=(
84
81
  "Suppresses splash screen"
85
82
  )
86
83
  )
@@ -90,8 +87,23 @@ depends on the type of the [files]:
90
87
  "Launches app with profiling to see what specific processes are taking up resources."
91
88
  )
92
89
  )
90
+ # add option to supply custom user directory
91
+ argParser.add_argument(
92
+ "--user-dir", dest="userDir", action="store", help=(
93
+ "Launches app with a custom location for the prefs folder."
94
+ )
95
+ )
93
96
  # parse args
94
97
  args, startFilesRaw = argParser.parse_known_args(sys.argv)
98
+
99
+ # import app and prefs now that args have been parsed
100
+ from psychopy.preferences import prefs
101
+ from psychopy.app import startApp, quitApp
102
+ # setup prefs
103
+ prefs.loadAll(userDir=args.userDir)
104
+ # insert fallbacks from prefs for unsupplied call args
105
+ if args.showSplash is None:
106
+ args.showSplash = prefs.app['showSplash']
95
107
  # pathify startFiles
96
108
  startFiles = None
97
109
  for thisFile in startFilesRaw:
@@ -924,7 +924,8 @@ class RunnerPanel(wx.Panel, ScriptProcess, handlers.ThemeMixin):
924
924
 
925
925
  def onItemDeselected(self, evt):
926
926
  """Set currentSelection, currentFile, currentExperiment and currentProject to None."""
927
- self.expCtrl.SetItemState(self.currentSelection, 0, wx.LIST_STATE_SELECTED)
927
+ if self.currentSelection < self.expCtrl.ItemCount:
928
+ self.expCtrl.SetItemState(self.currentSelection, 0, wx.LIST_STATE_SELECTED)
928
929
  self.currentSelection = None
929
930
  self.currentFile = None
930
931
  self.currentExperiment = None
@@ -1011,8 +1012,15 @@ class RunnerPanel(wx.Panel, ScriptProcess, handlers.ThemeMixin):
1011
1012
  PsychoPy Experiment object
1012
1013
  """
1013
1014
  fileName = str(self.currentFile)
1015
+ # if file doesn't exist, alert user and ask if they want to remove it from Runner
1014
1016
  if not os.path.exists(fileName):
1015
- raise FileNotFoundError("File not found: {}".format(fileName))
1017
+ dlg = wx.MessageDialog(
1018
+ None,
1019
+ _translate("File {} does not exist in this location. Remove item from Runner?").format(fileName),
1020
+ style=wx.YES | wx.NO
1021
+ )
1022
+ if dlg.ShowModal() == wx.ID_YES:
1023
+ wx.CallAfter(self.removeTask, evt=None)
1016
1024
 
1017
1025
  # If not a Builder file, return
1018
1026
  if not fileName.endswith('.psyexp'):
@@ -16,6 +16,8 @@ __all__ = ['ScriptProcess']
16
16
 
17
17
  import os.path
18
18
  import sys
19
+
20
+ import wx
19
21
  import psychopy.app.jobs as jobs
20
22
  from wx import BeginBusyCursor, EndBusyCursor, MessageDialog, ICON_ERROR, OK
21
23
  from psychopy.app.console import StdStreamDispatcher
@@ -156,7 +158,8 @@ class ScriptProcess:
156
158
  terminateCallback=self._onTerminateCallback
157
159
  )
158
160
 
159
- BeginBusyCursor() # visual feedback
161
+ if wx.Platform != '__WXGTK__':
162
+ BeginBusyCursor() # visual feedback
160
163
 
161
164
  # start the subprocess
162
165
  workingDir, _ = os.path.split(fullPath)
@@ -186,7 +189,8 @@ class ScriptProcess:
186
189
  event.Skip()
187
190
 
188
191
  self.scriptProcess = None # reset
189
- EndBusyCursor()
192
+ if wx.Platform != '__WXGTK__':
193
+ EndBusyCursor()
190
194
  return False
191
195
 
192
196
  self.focusOnExit = focusOnExit
@@ -351,7 +355,8 @@ class ScriptProcess:
351
355
  self.app.runner.panel.ribbon.buttons['pypilot'].Enable()
352
356
  self.app.runner.panel.ribbon.buttons['pystop'].Disable()
353
357
 
354
- EndBusyCursor()
358
+ if wx.Platform != '__WXGTK__':
359
+ EndBusyCursor()
355
360
 
356
361
 
357
362
  if __name__ == "__main__":