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
|
@@ -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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
559
|
-
self.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
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
|
-
|
|
1320
|
-
|
|
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
|
-
|
|
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
|
|
psychopy/app/preferencesDlg.py
CHANGED
|
@@ -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
|
-
|
|
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
|
psychopy/app/psychopyApp.py
CHANGED
|
@@ -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=
|
|
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:
|
psychopy/app/runner/runner.py
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
358
|
+
if wx.Platform != '__WXGTK__':
|
|
359
|
+
EndBusyCursor()
|
|
355
360
|
|
|
356
361
|
|
|
357
362
|
if __name__ == "__main__":
|