psychopy 2024.1.3__py3-none-any.whl → 2024.2.0__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 (331) hide show
  1. psychopy/.DS_Store +0 -0
  2. psychopy/CHANGELOG.txt +206 -0
  3. psychopy/GIT_SHA +1 -0
  4. psychopy/VERSION +1 -0
  5. psychopy/__init__.py +77 -15
  6. psychopy/app/Resources/classic/plugin16.png +0 -0
  7. psychopy/app/Resources/classic/plugin16@2x.png +0 -0
  8. psychopy/app/Resources/dark/plugin16.png +0 -0
  9. psychopy/app/Resources/dark/plugin16@2x.png +0 -0
  10. psychopy/app/Resources/light/plugin16.png +0 -0
  11. psychopy/app/Resources/light/plugin16@2x.png +0 -0
  12. psychopy/app/__init__.py +76 -2
  13. psychopy/app/_psychopyApp.py +126 -101
  14. psychopy/app/builder/builder.py +14 -10
  15. psychopy/app/builder/dialogs/__init__.py +8 -8
  16. psychopy/app/builder/dialogs/dlgsConditions.py +12 -13
  17. psychopy/app/builder/dialogs/paramCtrls.py +24 -57
  18. psychopy/app/builder/localizedStrings.py +11 -9
  19. psychopy/app/builder/validators.py +2 -2
  20. psychopy/app/coder/codeEditorBase.py +8 -8
  21. psychopy/app/coder/coder.py +4 -4
  22. psychopy/app/connections/sendusage.py +2 -2
  23. psychopy/app/connections/updates.py +9 -9
  24. psychopy/app/dialogs.py +34 -2
  25. psychopy/app/idle.py +31 -0
  26. psychopy/app/jobs.py +21 -3
  27. psychopy/app/linuxconfig/__init__.py +9 -0
  28. psychopy/app/locale/ar_001/LC_MESSAGE/messages.mo +0 -0
  29. psychopy/app/locale/ar_001/LC_MESSAGE/messages.po +4602 -2540
  30. psychopy/app/locale/es_CO/LC_MESSAGE/messages.mo +0 -0
  31. psychopy/app/locale/es_CO/LC_MESSAGE/messages.po +56 -54
  32. psychopy/app/locale/es_ES/LC_MESSAGE/messages.po +53 -43
  33. psychopy/app/locale/es_US/LC_MESSAGE/messages.mo +0 -0
  34. psychopy/app/locale/es_US/LC_MESSAGE/messages.po +56 -54
  35. psychopy/app/locale/ja_JP/LC_MESSAGE/messages.mo +0 -0
  36. psychopy/app/locale/ja_JP/LC_MESSAGE/messages.po +1258 -1176
  37. psychopy/app/locale/pt_PT/LC_MESSAGE/messages.po +9415 -5
  38. psychopy/app/pavlovia_ui/_base.py +33 -3
  39. psychopy/app/pavlovia_ui/search.py +0 -1
  40. psychopy/app/plugin_manager/dialog.py +104 -51
  41. psychopy/app/plugin_manager/packages.py +5 -0
  42. psychopy/app/plugin_manager/plugins.py +152 -67
  43. psychopy/app/preferencesDlg.py +8 -8
  44. psychopy/app/psychopyApp.py +11 -5
  45. psychopy/app/ribbon.py +124 -14
  46. psychopy/app/runner/runner.py +6 -1
  47. psychopy/app/stdout/stdOutRich.py +27 -11
  48. psychopy/app/themes/icons.py +52 -2
  49. psychopy/assets/__init__.py +0 -0
  50. psychopy/assets/click.png +0 -0
  51. psychopy/assets/clicknext.png +0 -0
  52. psychopy/assets/next.png +0 -0
  53. psychopy/assets/psychopy.ico +0 -0
  54. psychopy/assets/psychopy.png +0 -0
  55. psychopy/assets/templates/__init__.py +0 -0
  56. psychopy/assets/touch.png +0 -0
  57. psychopy/assets/touchnext.png +0 -0
  58. psychopy/assets/window.ico +0 -0
  59. psychopy/changes/2023.1.0.md +9 -0
  60. psychopy/changes/2024.1.0.md +16 -0
  61. psychopy/changes/__init__.py +0 -0
  62. psychopy/clock.py +2 -2
  63. psychopy/colors.py +2 -1
  64. psychopy/compatibility.py +53 -1
  65. psychopy/contrib/.DS_Store +0 -0
  66. psychopy/contrib/configobj/__init__.py +10 -8
  67. psychopy/data/__init__.py +3 -2
  68. psychopy/data/base.py +5 -5
  69. psychopy/data/experiment.py +130 -4
  70. psychopy/data/routine.py +56 -0
  71. psychopy/data/staircase.py +2 -2
  72. psychopy/data/trial.py +559 -97
  73. psychopy/data/utils.py +56 -21
  74. psychopy/demos/.DS_Store +0 -0
  75. psychopy/demos/builder/.DS_Store +0 -0
  76. psychopy/demos/builder/Design Templates/.DS_Store +0 -0
  77. psychopy/demos/builder/Experiments/.DS_Store +0 -0
  78. psychopy/demos/builder/Feature Demos/.DS_Store +0 -0
  79. psychopy/demos/builder/Feature Demos/buttonBox/buttonBoxDemo.psyexp +375 -0
  80. psychopy/demos/builder/Feature Demos/buttonBox/readme.md +5 -0
  81. psychopy/demos/builder/Feature Demos/pilotMode/pilotMode.psyexp +433 -0
  82. psychopy/demos/builder/Feature Demos/pilotMode/readme.md +7 -0
  83. psychopy/demos/builder/Feature Demos/progress/progressBar.psyexp +4 -4
  84. psychopy/demos/builder/Hardware/.DS_Store +0 -0
  85. psychopy/demos/builder/Helper Tools/.DS_Store +0 -0
  86. psychopy/demos/coder/.DS_Store +0 -0
  87. psychopy/demos/coder/hardware/testSoundLatency.py +2 -2
  88. psychopy/demos/coder/iohub/.DS_Store +0 -0
  89. psychopy/demos/coder/misc/hdf5_2_csv +33 -0
  90. psychopy/event.py +30 -29
  91. psychopy/experiment/.DS_Store +0 -0
  92. psychopy/experiment/_experiment.py +6 -6
  93. psychopy/experiment/components/.DS_Store +0 -0
  94. psychopy/experiment/components/__init__.py +6 -3
  95. psychopy/experiment/components/_base.py +286 -131
  96. psychopy/experiment/components/aperture/.DS_Store +0 -0
  97. psychopy/experiment/components/brush/.DS_Store +0 -0
  98. psychopy/experiment/components/button/.DS_Store +0 -0
  99. psychopy/experiment/components/button/__init__.py +5 -1
  100. psychopy/experiment/components/buttonBox/.DS_Store +0 -0
  101. psychopy/experiment/components/buttonBox/__init__.py +21 -12
  102. psychopy/experiment/components/camera/.DS_Store +0 -0
  103. psychopy/experiment/components/code/.DS_Store +0 -0
  104. psychopy/experiment/components/dots/.DS_Store +0 -0
  105. psychopy/experiment/components/eyetracker_record/.DS_Store +0 -0
  106. psychopy/experiment/components/eyetracker_record/__init__.py +92 -30
  107. psychopy/experiment/components/form/.DS_Store +0 -0
  108. psychopy/experiment/components/form/__init__.py +6 -2
  109. psychopy/experiment/components/grating/.DS_Store +0 -0
  110. psychopy/experiment/components/grating/__init__.py +14 -3
  111. psychopy/experiment/components/image/.DS_Store +0 -0
  112. psychopy/experiment/components/image/__init__.py +14 -3
  113. psychopy/experiment/components/joyButtons/.DS_Store +0 -0
  114. psychopy/experiment/components/joystick/.DS_Store +0 -0
  115. psychopy/experiment/components/keyboard/.DS_Store +0 -0
  116. psychopy/experiment/components/keyboard/__init__.py +22 -10
  117. psychopy/experiment/components/microphone/.DS_Store +0 -0
  118. psychopy/experiment/components/microphone/__init__.py +59 -39
  119. psychopy/experiment/components/mouse/.DS_Store +0 -0
  120. psychopy/experiment/components/mouse/__init__.py +44 -29
  121. psychopy/experiment/components/movie/.DS_Store +0 -0
  122. psychopy/experiment/components/movie/__init__.py +1 -1
  123. psychopy/experiment/components/panorama/.DS_Store +0 -0
  124. psychopy/experiment/components/parallelOut/.DS_Store +0 -0
  125. psychopy/experiment/components/patch/.DS_Store +0 -0
  126. psychopy/experiment/components/polygon/.DS_Store +0 -0
  127. psychopy/experiment/components/polygon/__init__.py +26 -6
  128. psychopy/experiment/components/progress/.DS_Store +0 -0
  129. psychopy/experiment/components/progress/__init__.py +1 -1
  130. psychopy/experiment/components/ratingScale/.DS_Store +0 -0
  131. psychopy/experiment/components/resourceManager/.DS_Store +0 -0
  132. psychopy/experiment/components/roi/.DS_Store +0 -0
  133. psychopy/experiment/components/roi/__init__.py +5 -0
  134. psychopy/experiment/components/routineSettings/.DS_Store +0 -0
  135. psychopy/experiment/components/routineSettings/__init__.py +57 -10
  136. psychopy/experiment/components/serialOut/.DS_Store +0 -0
  137. psychopy/experiment/components/settings/.DS_Store +0 -0
  138. psychopy/experiment/components/settings/__init__.py +117 -42
  139. psychopy/experiment/components/slider/.DS_Store +0 -0
  140. psychopy/experiment/components/sound/.DS_Store +0 -0
  141. psychopy/experiment/components/sound/__init__.py +54 -19
  142. psychopy/experiment/components/static/.DS_Store +0 -0
  143. psychopy/experiment/components/static/__init__.py +1 -1
  144. psychopy/experiment/components/text/.DS_Store +0 -0
  145. psychopy/experiment/components/text/__init__.py +28 -3
  146. psychopy/experiment/components/textbox/.DS_Store +0 -0
  147. psychopy/experiment/components/textbox/__init__.py +12 -2
  148. psychopy/experiment/components/unknown/.DS_Store +0 -0
  149. psychopy/experiment/components/unknown/__init__.py +1 -2
  150. psychopy/experiment/components/unknownPlugin/.DS_Store +0 -0
  151. psychopy/experiment/components/unknownPlugin/__init__.py +2 -2
  152. psychopy/experiment/components/variable/.DS_Store +0 -0
  153. psychopy/experiment/flow.py +11 -4
  154. psychopy/experiment/loops.py +85 -37
  155. psychopy/experiment/params.py +74 -32
  156. psychopy/experiment/py2js_transpiler.py +8 -1
  157. psychopy/experiment/routines/.DS_Store +0 -0
  158. psychopy/experiment/routines/_base.py +102 -22
  159. psychopy/experiment/routines/counterbalance/.DS_Store +0 -0
  160. psychopy/experiment/routines/counterbalance/__init__.py +5 -1
  161. psychopy/experiment/routines/eyetracker_calibrate/.DS_Store +0 -0
  162. psychopy/experiment/routines/eyetracker_validate/.DS_Store +0 -0
  163. psychopy/experiment/routines/pavlovia_survey/.DS_Store +0 -0
  164. psychopy/experiment/routines/photodiodeValidator/.DS_Store +0 -0
  165. psychopy/experiment/routines/photodiodeValidator/__init__.py +7 -6
  166. psychopy/experiment/routines/unknown/.DS_Store +0 -0
  167. psychopy/gui/wxgui.py +4 -4
  168. psychopy/hardware/.DS_Store +0 -0
  169. psychopy/hardware/__init__.py +1 -1
  170. psychopy/hardware/base.py +12 -0
  171. psychopy/hardware/camera/__init__.py +1 -15
  172. psychopy/hardware/cedrus.py +10 -11
  173. psychopy/hardware/crs/colorcal.py +13 -22
  174. psychopy/hardware/crs/optical.py +10 -20
  175. psychopy/hardware/emulator.py +17 -14
  176. psychopy/hardware/eyetracker.py +42 -118
  177. psychopy/hardware/gammasci.py +4 -15
  178. psychopy/hardware/keyboard.py +102 -11
  179. psychopy/hardware/listener.py +3 -0
  180. psychopy/hardware/microphone.py +148 -18
  181. psychopy/hardware/minolta.py +8 -15
  182. psychopy/hardware/photodiode.py +191 -16
  183. psychopy/hardware/photometer/__init__.py +11 -19
  184. psychopy/hardware/pr.py +8 -15
  185. psychopy/hardware/speaker.py +39 -4
  186. psychopy/info.py +0 -71
  187. psychopy/iohub/.DS_Store +0 -0
  188. psychopy/iohub/__init__.py +1 -1
  189. psychopy/iohub/client/__init__.py +30 -20
  190. psychopy/iohub/client/keyboard.py +24 -24
  191. psychopy/iohub/datastore/__init__.py +2 -2
  192. psychopy/iohub/datastore/util.py +2 -2
  193. psychopy/iohub/default_config.yaml +1 -1
  194. psychopy/iohub/devices/.DS_Store +0 -0
  195. psychopy/iohub/devices/__init__.py +112 -25
  196. psychopy/iohub/devices/deviceConfigValidation.py +2 -1
  197. psychopy/iohub/devices/experiment/default_experiment.yaml +12 -1
  198. psychopy/iohub/devices/experiment/supported_config_settings.yaml +5 -1
  199. psychopy/iohub/devices/eyetracker/.DS_Store +0 -0
  200. psychopy/iohub/devices/eyetracker/__init__.py +46 -0
  201. psychopy/iohub/devices/eyetracker/calibration/procedure.py +2 -2
  202. psychopy/iohub/devices/eyetracker/hw/gazepoint/__init__.py +14 -2
  203. psychopy/iohub/devices/eyetracker/hw/mouse/eyetracker.py +3 -4
  204. psychopy/iohub/server.py +2 -2
  205. psychopy/iohub/start_iohub_process.py +3 -0
  206. psychopy/iohub/util/__init__.py +62 -70
  207. psychopy/layout.py +5 -5
  208. psychopy/logging.py +8 -1
  209. psychopy/microphone.py +10 -37
  210. psychopy/platform_specific/__init__.py +0 -2
  211. psychopy/platform_specific/darwin.py +1 -3
  212. psychopy/platform_specific/linux.py +31 -33
  213. psychopy/platform_specific/win32.py +38 -13
  214. psychopy/plugins/__init__.py +148 -116
  215. psychopy/plugins/util.py +39 -0
  216. psychopy/preferences/Darwin.spec +4 -2
  217. psychopy/preferences/FreeBSD.spec +4 -2
  218. psychopy/preferences/Linux.spec +4 -2
  219. psychopy/preferences/Windows.spec +4 -2
  220. psychopy/preferences/baseNoArch.spec +4 -2
  221. psychopy/preferences/preferences.py +47 -24
  222. psychopy/projects/pavlovia.py +47 -4
  223. psychopy/scripts/psyexpCompile.py +0 -4
  224. psychopy/session.py +153 -21
  225. psychopy/sound/__init__.py +31 -21
  226. psychopy/sound/_base.py +20 -3
  227. psychopy/sound/audioclip.py +320 -33
  228. psychopy/sound/backend_ptb.py +47 -58
  229. psychopy/sound/backend_pygame.py +1 -1
  230. psychopy/sound/backend_pysound.py +6 -15
  231. psychopy/sound/transcribe.py +53 -0
  232. psychopy/tests/.DS_Store +0 -0
  233. psychopy/tests/data/.DS_Store +0 -0
  234. psychopy/tests/data/TestUnknownPluginComponent_load_resave.psyexp +135 -0
  235. psychopy/tests/data/Test_textbox/test_ori_0_bottom right.png +0 -0
  236. psychopy/tests/data/Test_textbox/test_ori_0_center.png +0 -0
  237. psychopy/tests/data/Test_textbox/test_ori_0_top left.png +0 -0
  238. psychopy/tests/data/Test_textbox/test_ori_120_bottom right.png +0 -0
  239. psychopy/tests/data/Test_textbox/test_ori_120_center.png +0 -0
  240. psychopy/tests/data/Test_textbox/test_ori_120_top left.png +0 -0
  241. psychopy/tests/data/Test_textbox/test_ori_180_bottom right.png +0 -0
  242. psychopy/tests/data/Test_textbox/test_ori_180_center.png +0 -0
  243. psychopy/tests/data/Test_textbox/test_ori_180_top left.png +0 -0
  244. psychopy/tests/data/Test_textbox/test_ori_240_bottom right.png +0 -0
  245. psychopy/tests/data/Test_textbox/test_ori_240_center.png +0 -0
  246. psychopy/tests/data/Test_textbox/test_ori_240_top left.png +0 -0
  247. psychopy/tests/data/correctScript/.DS_Store +0 -0
  248. psychopy/tests/data/test_components/testClearKeyboard/testClearKeyboard.psyexp +200 -0
  249. psychopy/tests/data/test_session/.DS_Store +0 -0
  250. psychopy/tests/data/test_session/root/testFutureTrials/testFutureTrials.psyexp +155 -0
  251. psychopy/tests/data/test_session/root/testTrialNav/trialNav.psyexp +158 -0
  252. psychopy/tests/test_app/.DS_Store +0 -0
  253. psychopy/tests/test_app/conftest.py +2 -2
  254. psychopy/tests/test_app/test_speed.py +4 -1
  255. psychopy/tests/test_data/test_TrialHandler2.py +146 -1
  256. psychopy/tests/test_experiment/.DS_Store +0 -0
  257. psychopy/tests/test_experiment/needs_wx/genComponsTemplate.py +3 -3
  258. psychopy/tests/test_experiment/needs_wx/test_components.py +2 -2
  259. psychopy/tests/test_experiment/test_components/test_KeyboardComponent.py +28 -0
  260. psychopy/tests/test_experiment/test_components/test_UnknownPluginComponent.py +27 -0
  261. psychopy/tests/test_experiment/test_components/test_base_components.py +58 -0
  262. psychopy/tests/test_experiment/test_py2js.py +1 -1
  263. psychopy/tests/test_hardware/test_keyboard.py +184 -16
  264. psychopy/tests/test_hardware/test_ports.py +1 -11
  265. psychopy/tests/test_liaison/test_Liaison.py +47 -0
  266. psychopy/tests/test_misc/test_core.py +5 -0
  267. psychopy/tests/test_session/test_Session.py +5 -1
  268. psychopy/tests/test_tools/test_versionchooser.py +39 -8
  269. psychopy/tests/test_visual/test_all_stimuli.py +0 -97
  270. psychopy/tests/test_visual/test_image.py +6 -5
  271. psychopy/tests/test_visual/test_textbox.py +36 -0
  272. psychopy/tests/utils.py +4 -0
  273. psychopy/tools/filetools.py +1 -1
  274. psychopy/tools/pkgtools.py +160 -137
  275. psychopy/tools/versionchooser.py +10 -10
  276. psychopy/tools/wizard.py +3 -3
  277. psychopy/visual/.DS_Store +0 -0
  278. psychopy/visual/backends/pygletbackend.py +24 -13
  279. psychopy/visual/basevisual.py +5 -11
  280. psychopy/visual/button.py +2 -14
  281. psychopy/visual/helpers.py +5 -5
  282. psychopy/visual/line.py +1 -2
  283. psychopy/visual/movie2.py +7 -816
  284. psychopy/visual/movie3.py +7 -589
  285. psychopy/visual/movies/__init__.py +8 -11
  286. psychopy/visual/movies/frame.py +5 -2
  287. psychopy/visual/movies/players/ffpyplayer_player.py +5 -2
  288. psychopy/visual/noise.py +8 -7
  289. psychopy/visual/patch.py +7 -16
  290. psychopy/visual/progress.py +1 -1
  291. psychopy/visual/radial.py +9 -7
  292. psychopy/visual/ratingscale.py +8 -1415
  293. psychopy/visual/secondorder.py +10 -9
  294. psychopy/visual/shape.py +7 -2
  295. psychopy/visual/text.py +1 -1
  296. psychopy/visual/textbox2/textbox2.py +28 -5
  297. psychopy/web.py +5 -2
  298. {psychopy-2024.1.3.dist-info → psychopy-2024.2.0.dist-info}/METADATA +8 -13
  299. {psychopy-2024.1.3.dist-info → psychopy-2024.2.0.dist-info}/RECORD +313 -219
  300. {psychopy-2024.1.3.dist-info → psychopy-2024.2.0.dist-info}/WHEEL +1 -1
  301. psychopy/app/Resources/click.png +0 -0
  302. psychopy/app/Resources/next.png +0 -0
  303. psychopy/experiment/components/patch/__init__.py +0 -121
  304. psychopy/experiment/components/patch/classic/patch.png +0 -0
  305. psychopy/experiment/components/patch/dark/patch.png +0 -0
  306. psychopy/experiment/components/patch/dark/patch@2x.png +0 -0
  307. psychopy/experiment/components/patch/light/patch.png +0 -0
  308. psychopy/experiment/components/patch/light/patch@2x.png +0 -0
  309. psychopy/experiment/components/ratingScale/__init__.py +0 -337
  310. psychopy/experiment/components/ratingScale/classic/ratingscale.png +0 -0
  311. psychopy/experiment/components/ratingScale/classic/ratingscale@2x.png +0 -0
  312. psychopy/experiment/components/ratingScale/dark/ratingScale@2x.png +0 -0
  313. psychopy/experiment/components/ratingScale/dark/ratingscale.png +0 -0
  314. psychopy/experiment/components/ratingScale/light/ratingScale@2x.png +0 -0
  315. psychopy/experiment/components/ratingScale/light/ratingscale.png +0 -0
  316. psychopy/platform_specific/posix.py +0 -16
  317. psychopy/tests/test_sound/test_microphone.py +0 -217
  318. psychopy/tests/test_visual/test_ratingScale.py +0 -299
  319. /psychopy/{app/Resources → assets}/Psychopy Window Favicon@16w.png +0 -0
  320. /psychopy/{app/Resources → assets}/Psychopy Window Favicon@32w.png +0 -0
  321. /psychopy/{app/Resources → assets}/USB-C.png +0 -0
  322. /psychopy/{app/Resources → assets}/USB.png +0 -0
  323. /psychopy/{app/Resources → assets}/creditCard.png +0 -0
  324. /psychopy/{app/Resources → assets}/default.mp3 +0 -0
  325. /psychopy/{app/Resources → assets}/default.mp4 +0 -0
  326. /psychopy/{app/Resources → assets}/default.png +0 -0
  327. /psychopy/{app/Resources → assets/templates}/instruct1.png +0 -0
  328. /psychopy/{app/Resources → assets/templates}/instruct2.png +0 -0
  329. {psychopy-2024.1.3.dist-info → psychopy-2024.2.0.dist-info}/entry_points.txt +0 -0
  330. {psychopy-2024.1.3.dist-info → psychopy-2024.2.0.dist-info}/licenses/AUTHORS.md +0 -0
  331. {psychopy-2024.1.3.dist-info → psychopy-2024.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,4 +1,5 @@
1
1
  from pathlib import Path
2
+ import shutil
2
3
 
3
4
  import wx
4
5
  from wx.lib import scrolledpanel
@@ -220,7 +221,7 @@ class PluginInfo:
220
221
  return
221
222
 
222
223
  wx.CallAfter(
223
- self.parent.GetTopLevelParent().uninstallPackage, self.pipname)
224
+ self.parent.GetTopLevelParent().uninstallPlugin, self)
224
225
 
225
226
  @property
226
227
  def installed(self):
@@ -277,6 +278,12 @@ class PluginManagerPanel(wx.Panel, handlers.ThemeMixin):
277
278
  self.Layout()
278
279
  self.splitter.SetSashPosition(1, True)
279
280
  self.theme = theme.app
281
+
282
+ def updateInfo(self):
283
+ # refresh all list items
284
+ self.pluginList.updateInfo()
285
+ # set current plugin again to refresh view
286
+ self.pluginViewer.info = self.pluginViewer.info
280
287
 
281
288
  def _applyAppTheme(self):
282
289
  # Set colors
@@ -318,9 +325,21 @@ class PluginBrowserList(scrolledpanel.ScrolledPanel, handlers.ThemeMixin):
318
325
  # self.activeBtn.Bind(wx.EVT_BUTTON, self.onToggleActivate)
319
326
  # self.btnSizer.Add(self.activeBtn, border=3, flag=wx.ALL | wx.ALIGN_RIGHT)
320
327
  # Add install button
321
- self.installBtn = wx.Button(self)
328
+ self.installBtn = wx.Button(self, label=_translate("Install"))
329
+ self.installBtn.SetBitmap(
330
+ icons.ButtonIcon("download", 16).bitmap
331
+ )
332
+ self.installBtn.SetBitmapMargins(6, 3)
322
333
  self.installBtn.Bind(wx.EVT_BUTTON, self.onInstall)
323
334
  self.btnSizer.Add(self.installBtn, border=3, flag=wx.ALL | wx.ALIGN_RIGHT)
335
+ # # add uninstall button
336
+ # self.uninstallBtn = wx.Button(self, label=_translate("Uninstall"))
337
+ # self.uninstallBtn.SetBitmap(
338
+ # icons.ButtonIcon("delete", 16).bitmap
339
+ # )
340
+ # self.uninstallBtn.SetBitmapMargins(6, 3)
341
+ # self.uninstallBtn.Bind(wx.EVT_BUTTON, self.onUninstall)
342
+ # self.btnSizer.Add(self.uninstallBtn, border=3, flag=wx.ALL | wx.EXPAND)
324
343
 
325
344
  # Map to onclick function
326
345
  self.Bind(wx.EVT_LEFT_DOWN, self.onSelect)
@@ -329,10 +348,8 @@ class PluginBrowserList(scrolledpanel.ScrolledPanel, handlers.ThemeMixin):
329
348
  # Bind navigation
330
349
  self.Bind(wx.EVT_NAVIGATION_KEY, self.onNavigation)
331
350
 
332
- # Handle version mismatch
333
- self.installBtn.Enable(__version__ in self.info.version)
334
-
335
351
  self._applyAppTheme()
352
+ self.markInstalled(self.info.installed)
336
353
 
337
354
  @property
338
355
  def viewer(self):
@@ -341,6 +358,10 @@ class PluginBrowserList(scrolledpanel.ScrolledPanel, handlers.ThemeMixin):
341
358
  """
342
359
  return self.parent.viewer
343
360
 
361
+ def updateInfo(self):
362
+ # update install state
363
+ self.markInstalled(self.info.installed)
364
+
344
365
  def _applyAppTheme(self):
345
366
  # Set label fonts
346
367
  from psychopy.app.themes import fonts
@@ -429,14 +450,40 @@ class PluginBrowserList(scrolledpanel.ScrolledPanel, handlers.ThemeMixin):
429
450
  active=active
430
451
  )
431
452
 
432
- def onInstall(self, evt=None):
433
- # Mark as pending
453
+ def _doInstall(self):
454
+ """Routine to run the installation of a package after the `onInstall`
455
+ event is processed.
456
+ """
457
+ # mark as pending
434
458
  self.markInstalled(None)
435
- # Do install
459
+ # install
436
460
  self.info.install()
437
- # Mark according to install success
461
+ # mark according to install success
462
+ self.markInstalled(self.info.installed)
463
+
464
+ def _doUninstall(self):
465
+ # mark as pending
466
+ self.markInstalled(None)
467
+ # uninstall
468
+ self.info.uninstall()
469
+ # mark according to uninstall success
438
470
  self.markInstalled(self.info.installed)
439
471
 
472
+ def onInstall(self, evt=None):
473
+ """Event called when the install button is clicked.
474
+ """
475
+ wx.CallAfter(self._doInstall) # call after processing button events
476
+ if evt is not None and hasattr(evt, 'Skip'):
477
+ evt.Skip()
478
+
479
+ def onUninstall(self, evt=None):
480
+ """
481
+ Event called when the uninstall button is clicked.
482
+ """
483
+ wx.CallAfter(self._doUninstall) # call after processing button events
484
+ if evt is not None and hasattr(evt, 'Skip'):
485
+ evt.Skip()
486
+
440
487
  def onToggleActivate(self, evt=None):
441
488
  if self.info.active:
442
489
  self.onDeactivate(evt=evt)
@@ -491,6 +538,14 @@ class PluginBrowserList(scrolledpanel.ScrolledPanel, handlers.ThemeMixin):
491
538
  )
492
539
  self.sizer.Add(self.errorCtrl, proportion=1, border=3, flag=wx.ALL | wx.EXPAND)
493
540
  self.errorCtrl.Hide()
541
+ # add button to uninstall all
542
+ self.uninstallAllBtn = wx.Button(self, label=_translate("Uninstall all plugins"))
543
+ self.uninstallAllBtn.SetBitmap(
544
+ icons.ButtonIcon("delete", 16).bitmap
545
+ )
546
+ self.uninstallAllBtn.SetBitmapMargins(6, 3)
547
+ self.uninstallAllBtn.Bind(wx.EVT_BUTTON, self.onUninstallAll)
548
+ self.sizer.Add(self.uninstallAllBtn, border=12, flag=wx.ALL | wx.CENTER)
494
549
 
495
550
  # Bind deselect
496
551
  self.Bind(wx.EVT_LEFT_DOWN, self.onDeselect)
@@ -519,6 +574,10 @@ class PluginBrowserList(scrolledpanel.ScrolledPanel, handlers.ThemeMixin):
519
574
  # layout
520
575
  self.Layout()
521
576
  self.SetupScrolling()
577
+
578
+ def updateInfo(self):
579
+ for item in self.items:
580
+ item.updateInfo()
522
581
 
523
582
  def search(self, evt=None):
524
583
  searchTerm = self.searchCtrl.GetValue().strip()
@@ -571,6 +630,31 @@ class PluginBrowserList(scrolledpanel.ScrolledPanel, handlers.ThemeMixin):
571
630
  def onClick(self, evt=None):
572
631
  self.SetFocusIgnoringChildren()
573
632
  self.viewer.info = None
633
+
634
+ def onUninstallAll(self, evt=None):
635
+ """
636
+ Called when the "Uninstall all" button is clicked
637
+ """
638
+ # warn user that they'll delete the packages folder
639
+ dlg = wx.MessageDialog(
640
+ self,
641
+ message=_translate("This will uninstall all plugins an additional packages you have installed, are you sure you want to continue?"),
642
+ style=wx.ICON_WARNING | wx.YES | wx.NO
643
+ )
644
+ if dlg.ShowModal() != wx.ID_YES:
645
+ # cancel if they didn't explicitly say yes
646
+ return
647
+ # delete the packages folder
648
+ shutil.rmtree(prefs.paths['packages'])
649
+ # print success
650
+ dlg = wx.MessageDialog(
651
+ self,
652
+ message=_translate("All plugins and additional packages have been uninstalled. You will need to restart PsychoPy for this to take effect."),
653
+ style=wx.ICON_INFORMATION | wx.OK
654
+ )
655
+ dlg.ShowModal()
656
+ # close dialog
657
+ self.GetTopLevelParent().Close()
574
658
 
575
659
  def setSelection(self, item):
576
660
  """
@@ -672,14 +756,22 @@ class PluginDetailsPanel(wx.Panel, handlers.ThemeMixin):
672
756
  # Buttons
673
757
  self.buttonSizer = wx.BoxSizer(wx.HORIZONTAL)
674
758
  self.titleSizer.Add(self.buttonSizer, flag=wx.EXPAND)
675
- # Install btn
676
- self.installBtn = wx.Button(self)
759
+ # install btn
760
+ self.installBtn = wx.Button(self, label=_translate("Install"))
761
+ self.installBtn.SetBitmap(
762
+ icons.ButtonIcon("download", 16).bitmap
763
+ )
764
+ self.installBtn.SetBitmapMargins(6, 3)
677
765
  self.installBtn.Bind(wx.EVT_BUTTON, self.onInstall)
678
766
  self.buttonSizer.Add(self.installBtn, border=3, flag=wx.ALL | wx.EXPAND)
679
- # Active btn
680
- # self.activeBtn = wx.Button(self)
681
- # self.activeBtn.Bind(wx.EVT_BUTTON, self.onToggleActivate)
682
- # self.buttonSizer.Add(self.activeBtn, border=3, flag=wx.ALL | wx.EXPAND)
767
+ # uninstall btn
768
+ self.uninstallBtn = wx.Button(self, label=_translate("Uninstall"))
769
+ self.uninstallBtn.SetBitmap(
770
+ icons.ButtonIcon("delete", 16).bitmap
771
+ )
772
+ self.uninstallBtn.SetBitmapMargins(6, 3)
773
+ self.uninstallBtn.Bind(wx.EVT_BUTTON, self.onUninstall)
774
+ self.buttonSizer.Add(self.uninstallBtn, border=3, flag=wx.ALL | wx.EXPAND)
683
775
  # Homepage btn
684
776
  self.homepageBtn = wx.Button(self, label=_translate("Homepage"))
685
777
  self.homepageBtn.Bind(wx.EVT_BUTTON, self.onHomepage)
@@ -779,11 +871,19 @@ class PluginDetailsPanel(wx.Panel, handlers.ThemeMixin):
779
871
  """Routine to run the installation of a package after the `onInstall`
780
872
  event is processed.
781
873
  """
782
- # Mark as pending
874
+ # mark as pending
783
875
  self.markInstalled(None)
784
- # Do install
876
+ # install
785
877
  self.info.install()
786
- # Mark according to install success
878
+ # mark according to install success
879
+ self.markInstalled(self.info.installed)
880
+
881
+ def _doUninstall(self):
882
+ # mark as pending
883
+ self.markInstalled(None)
884
+ # uninstall
885
+ self.info.uninstall()
886
+ # mark according to uninstall success
787
887
  self.markInstalled(self.info.installed)
788
888
 
789
889
  def onInstall(self, evt=None):
@@ -792,7 +892,15 @@ class PluginDetailsPanel(wx.Panel, handlers.ThemeMixin):
792
892
  wx.CallAfter(self._doInstall) # call after processing button events
793
893
  if evt is not None and hasattr(evt, 'Skip'):
794
894
  evt.Skip()
795
-
895
+
896
+ def onUninstall(self, evt=None):
897
+ """
898
+ Event called when the uninstall button is clicked.
899
+ """
900
+ wx.CallAfter(self._doUninstall) # call after processing button events
901
+ if evt is not None and hasattr(evt, 'Skip'):
902
+ evt.Skip()
903
+
796
904
  def _doActivate(self, state=True):
797
905
  """Activate a plugin or package after the `onActivate` event.
798
906
 
@@ -1031,69 +1139,39 @@ def markInstalled(pluginItem, pluginPanel, installed=True):
1031
1139
  installed : bool or None
1032
1140
  True if installed, False if not installed, None if pending/unclear
1033
1141
  """
1034
- def _setAllBitmaps(btn, bmp):
1035
- """
1036
- Set all bitmaps (enabled, disabled, focus, unfocus, etc.) for a button
1037
- """
1038
- btn.SetBitmap(bmp)
1039
- btn.SetBitmapDisabled(bmp)
1040
- btn.SetBitmapPressed(bmp)
1041
- btn.SetBitmapCurrent(bmp)
1042
- btn.SetBitmapMargins(6, 3)
1043
1142
 
1044
- # Update plugin item
1143
+ # update plugin item
1045
1144
  if pluginItem:
1046
1145
  if installed is None:
1047
- # If pending, show elipsis and refresh icon
1048
- pluginItem.installBtn.Show()
1049
- pluginItem.installBtn.SetLabel("...")
1050
- _setAllBitmaps(pluginItem.installBtn, icons.ButtonIcon("view-refresh", 16).bitmap)
1051
- # Hide active button while pending
1052
- # pluginItem.activeBtn.Hide()
1146
+ # if pending, hide both buttons
1147
+ pluginItem.installBtn.Hide()
1053
1148
  elif installed:
1054
- # If installed, hide install button
1149
+ # if installed, hide install button
1055
1150
  pluginItem.installBtn.Hide()
1056
- # Show active button when installed
1057
- # pluginItem.activeBtn.Show()
1058
1151
  else:
1059
- # If not installed, show "Install" and download icon
1152
+ # if not installed, show "Install" and download icon
1060
1153
  pluginItem.installBtn.Show()
1061
- pluginItem.installBtn.SetLabel(_translate("Install"))
1062
- _setAllBitmaps(pluginItem.installBtn, icons.ButtonIcon("download", 16).bitmap)
1063
- # Hide active button when not installed
1064
- # pluginItem.activeBtn.Hide()
1065
- # Refresh buttons
1154
+ # refresh buttons
1066
1155
  pluginItem.Update()
1067
1156
  pluginItem.Layout()
1068
1157
 
1069
- # Update panel (if applicable)
1158
+ # update panel (if applicable)
1070
1159
  if pluginPanel and pluginItem and pluginPanel.info == pluginItem.info:
1071
1160
  if installed is None:
1072
- # If pending, show elipsis and refresh icon
1073
- pluginPanel.installBtn.Show()
1074
- pluginPanel.installBtn.Enable(__version__ in pluginItem.info.version)
1075
- pluginPanel.installBtn.SetLabel("...")
1076
- _setAllBitmaps(pluginPanel.installBtn, icons.ButtonIcon("view-refresh", 16).bitmap)
1077
- # Hide active button while pending
1078
- # pluginPanel.activeBtn.Hide()
1161
+ # if pending, show elipsis and refresh icon
1162
+ pluginPanel.installBtn.Hide()
1163
+ pluginPanel.uninstallBtn.Hide()
1079
1164
  elif installed:
1080
- # If installed, show as installed with tick
1081
- pluginPanel.installBtn.Show()
1082
- pluginPanel.installBtn.Disable()
1083
- pluginPanel.installBtn.SetLabelText(_translate("Installed"))
1084
- _setAllBitmaps(pluginPanel.installBtn, icons.ButtonIcon("greytick", 16).bitmap)
1085
- # Show active button when installed
1086
- # pluginPanel.activeBtn.Show()
1165
+ # if installed, show as installed with tick
1166
+ pluginPanel.installBtn.Hide()
1167
+ pluginPanel.uninstallBtn.Show()
1087
1168
  else:
1088
- # If not installed, show "Install" and download icon
1169
+ # if not installed, show "Install" and download icon
1089
1170
  pluginPanel.installBtn.Show()
1090
- pluginPanel.installBtn.Enable(__version__ in pluginItem.info.version)
1091
- pluginPanel.installBtn.SetLabel(_translate("Install"))
1092
- _setAllBitmaps(pluginPanel.installBtn, icons.ButtonIcon("download", 16).bitmap)
1093
- # Hide active button when not installed
1094
- # pluginPanel.activeBtn.Hide()
1095
- # Refresh buttons
1171
+ pluginPanel.uninstallBtn.Hide()
1172
+ # refresh buttons
1096
1173
  pluginPanel.Update()
1174
+ pluginPanel.Layout()
1097
1175
 
1098
1176
 
1099
1177
  def markActive(pluginItem, pluginPanel, active=True):
@@ -1222,6 +1300,13 @@ def getAllPluginDetails():
1222
1300
  return None
1223
1301
  # otherwise get as a string
1224
1302
  value = resp.text
1303
+
1304
+ if value is None or value == "":
1305
+ return None
1306
+
1307
+ # make sure we are using UTF-8 encoding
1308
+ value = value.encode('utf-8', 'ignore').decode('utf-8')
1309
+
1225
1310
  # attempt to parse JSON
1226
1311
  try:
1227
1312
  database = json.loads(value)
@@ -1253,7 +1338,7 @@ def getAllPluginDetails():
1253
1338
  return None
1254
1339
  # attempt to parse JSON
1255
1340
  try:
1256
- with srcFile.open("r", encoding="utf-8") as f:
1341
+ with srcFile.open("r", encoding="utf-8", errors="ignore") as f:
1257
1342
  return json.load(f)
1258
1343
  except json.decoder.JSONDecodeError:
1259
1344
  # if JSON parse fails, return nothing
@@ -15,7 +15,7 @@ from psychopy.app.themes import icons
15
15
  from . import dialogs
16
16
  from psychopy import localization, prefs
17
17
  from psychopy.localization import _translate
18
- from pkg_resources import parse_version
18
+ from packaging.version import Version
19
19
  from psychopy import sound
20
20
  from psychopy.app.utils import getSystemFonts
21
21
  import collections
@@ -190,7 +190,7 @@ class PrefPropGrid(wx.Panel):
190
190
  self.helpText[name] = helpText
191
191
 
192
192
  def addDirItem(self, section, label=wx.propgrid.PG_LABEL,
193
- name=wx.propgrid.PG_LABEL, value='', helpText=""):
193
+ name=wx.propgrid.PG_LABEL, value='', helpText=""):
194
194
  if section not in self.sections.keys():
195
195
  self.sections[section] = dict()
196
196
 
@@ -430,12 +430,12 @@ class PreferencesDlg(wx.Dialog):
430
430
  # get sound devices for "audioDevice" property
431
431
  try:
432
432
  devnames = sorted(sound.getDevices('output'))
433
- except (ValueError, OSError, ImportError):
433
+ except (ValueError, OSError, ImportError, AttributeError):
434
434
  devnames = []
435
435
 
436
436
  audioConf = self.prefsCfg['hardware']['audioDevice']
437
437
  self.audioDevDefault = audioConf \
438
- if type(audioConf) != list else list(audioConf)
438
+ if type(audioConf) is list else list(audioConf)
439
439
  self.audioDevNames = [
440
440
  dev.replace('\r\n', '') for dev in devnames
441
441
  if dev != self.audioDevDefault]
@@ -479,7 +479,7 @@ class PreferencesDlg(wx.Dialog):
479
479
  hint = hints[-1].lstrip().lstrip('#').lstrip()
480
480
  helpText = _translate(hint)
481
481
 
482
- if type(thisPref) == bool:
482
+ if type(thisPref) is bool:
483
483
  # only True or False - use a checkbox
484
484
  self.proPrefs.addBoolItem(
485
485
  sectionName, pLabel, prefName, thisPref,
@@ -695,7 +695,7 @@ class PreferencesDlg(wx.Dialog):
695
695
  try:
696
696
  # if thisPref is not a null string, do eval() to get a
697
697
  # list.
698
- if thisPref == '' or type(thisPref) == list:
698
+ if thisPref == '' or type(thisPref) is list:
699
699
  newVal = thisPref
700
700
  else:
701
701
  newVal = eval(thisPref)
@@ -713,7 +713,7 @@ class PreferencesDlg(wx.Dialog):
713
713
  title=title)
714
714
  warnDlg.ShowModal()
715
715
  return
716
- if type(newVal) != list:
716
+ if type(newVal) is not list:
717
717
  self.prefsCfg[sectionName][prefName] = [newVal]
718
718
  else:
719
719
  self.prefsCfg[sectionName][prefName] = newVal
@@ -781,7 +781,7 @@ class PreferencesDlg(wx.Dialog):
781
781
 
782
782
  if __name__ == '__main__':
783
783
  from psychopy import preferences
784
- if parse_version(wx.__version__) < parse_version('2.9'):
784
+ if Version(wx.__version__) < Version('2.9'):
785
785
  app = wx.PySimpleApp()
786
786
  else:
787
787
  app = wx.App(False)
@@ -73,17 +73,23 @@ Options:
73
73
  ('| packaged by conda-forge |' in sys.version or
74
74
  '|Anaconda' in sys.version)):
75
75
 
76
- # On macOS with Anaconda, GUI applications need to be run using
76
+ # On macOS with Anaconda, GUI applications used to need to be run using
77
77
  # `pythonw`. Since we have no way to determine whether this is currently
78
78
  # the case, we run this script again -- ensuring we're definitely using
79
79
  # pythonw.
80
80
  import os
81
81
  env = os.environ
82
82
  PYTHONW = env.get('PYTHONW', 'False')
83
-
84
- if PYTHONW != 'True':
83
+ pyw_exe = sys.executable + 'w'
84
+
85
+ # Updated 2024.1.6: as of Python 3, `pythonw` and `python` can be used
86
+ # interchangeably for wxPython applications on macOS with GUI support.
87
+ # The defaults and conda-forge channels no longer install python with a
88
+ # framework build (to do so: `conda install python=3.8 python.app`).
89
+ # Therefore `pythonw` often doesn't exist, and we can just use `python`.
90
+ if PYTHONW != 'True' and os.path.isfile(pyw_exe):
85
91
  from psychopy import core
86
- cmd = [sys.executable + 'w', __file__]
92
+ cmd = [pyw_exe, __file__]
87
93
  if '--no-splash' in sys.argv:
88
94
  cmd.append('--no-splash')
89
95
 
@@ -100,4 +106,4 @@ Options:
100
106
 
101
107
 
102
108
  if __name__ == '__main__':
103
- main()
109
+ main()
psychopy/app/ribbon.py CHANGED
@@ -55,6 +55,52 @@ class FrameRibbon(wx.Panel, handlers.ThemeMixin):
55
55
 
56
56
  return sct
57
57
 
58
+ def addPluginSections(self, group):
59
+ """
60
+ Add any sections to the ribbon which are defined by plugins, targeting the given entry point group.
61
+
62
+ Parameters
63
+ ----------
64
+ group : str
65
+ Entry point group to look for plugin sections in.
66
+
67
+ Returns
68
+ -------
69
+ list[FrameRubbinPluginSection]
70
+ List of section objects which were added
71
+ """
72
+ from importlib import metadata
73
+ # start off with no entry points or sections
74
+ entryPoints = []
75
+ sections = []
76
+ # iterate through all entry point groups
77
+ for thisGroup, eps in metadata.entry_points().items():
78
+ # get entry points for matching group
79
+ if thisGroup == group:
80
+ # add to list of all entry points
81
+ entryPoints += eps
82
+ # iterate through found entry points
83
+ for ep in entryPoints:
84
+ try:
85
+ # load (import) module
86
+ cls = ep.load()
87
+ except:
88
+ # if failed for any reason, skip it
89
+ continue
90
+ # if the target is not a subclass of FrameRibbonPluginSection, discard it
91
+ if not isinstance(cls, type) or not issubclass(cls, FrameRibbonPluginSection):
92
+ continue
93
+ # if it's a section, add it
94
+ sct = cls(parent=self)
95
+ self.sections[sct.name] = sct
96
+ sections.append(sct)
97
+ # add to sizer
98
+ self.sizer.Add(sct, border=0, flag=wx.EXPAND | wx.ALL)
99
+ # add separator
100
+ self.addSeparator()
101
+
102
+ return sections
103
+
58
104
  def addButton(self, section, name, label="", icon=None, tooltip="", callback=None,
59
105
  style=wx.BU_NOTEXT):
60
106
  """
@@ -244,13 +290,14 @@ class FrameRibbonSection(wx.Panel, handlers.ThemeMixin):
244
290
  self.label, flag=wx.EXPAND
245
291
  )
246
292
 
247
-
248
293
  # add space
249
294
  self.border.AddSpacer(6)
250
295
 
251
296
  # dict in which to store buttons
252
297
  self.buttons = {}
253
298
 
299
+ self._applyAppTheme()
300
+
254
301
  def addButton(self, name, label="", icon=None, tooltip="", callback=None, style=wx.BU_NOTEXT):
255
302
  """
256
303
  Add a button to this section.
@@ -362,9 +409,34 @@ class FrameRibbonSection(wx.Panel, handlers.ThemeMixin):
362
409
  return btn
363
410
 
364
411
  def _applyAppTheme(self):
412
+ # set color
365
413
  self.SetBackgroundColour(colors.app['frame_bg'])
366
-
414
+ self.SetForegroundColour(colors.app['text'])
415
+ # set bitmaps again
416
+ self._icon.reload()
367
417
  self.icon.SetBitmap(self._icon.bitmap)
418
+ # refresh
419
+ self.Refresh()
420
+
421
+
422
+ class FrameRibbonPluginSection(FrameRibbonSection):
423
+ """
424
+ Subclass of FrameRibbonSection specifically for adding sections to the ribbon via plugins. To
425
+ add a section, create a subclass of FrameRibbonPluginSection in your plugin and add any buttons
426
+ you want it to have in the `__init__` function. Then give it an entry point in either
427
+ "psychopy.app.builder", "psychopy.app.coder" or "psychopy.app.runner" to tell PsychoPy which
428
+ frame to add it to.
429
+ """
430
+ def __init__(self, parent, name, label=None):
431
+ # if not given a label, use name
432
+ if label is None:
433
+ label = name
434
+ # store name
435
+ self.name = name
436
+ # initialise subclass
437
+ FrameRibbonSection.__init__(
438
+ self, parent, label=label, icon="plugin"
439
+ )
368
440
 
369
441
 
370
442
  class FrameRibbonButton(wx.Button, handlers.ThemeMixin):
@@ -402,11 +474,8 @@ class FrameRibbonButton(wx.Button, handlers.ThemeMixin):
402
474
  tooltip = f"{label}: {tooltip}"
403
475
  self.SetToolTip(tooltip)
404
476
  # set icon
477
+ self._icon = icons.ButtonIcon(icon, size=32)
405
478
  bmpStyle = style & (wx.TOP | wx.BOTTOM | wx.LEFT | wx.RIGHT)
406
- self.SetBitmap(
407
- icons.ButtonIcon(icon, size=32).bitmap,
408
- dir=bmpStyle or wx.TOP
409
- )
410
479
  # if given, bind callback
411
480
  if callback is not None:
412
481
  self.Bind(wx.EVT_BUTTON, callback)
@@ -414,8 +483,20 @@ class FrameRibbonButton(wx.Button, handlers.ThemeMixin):
414
483
  self.Bind(wx.EVT_ENTER_WINDOW, self.onHover)
415
484
  self.Bind(wx.EVT_LEAVE_WINDOW, self.onHover)
416
485
 
486
+ self._applyAppTheme()
487
+
417
488
  def _applyAppTheme(self):
489
+ # set color
418
490
  self.SetBackgroundColour(colors.app['frame_bg'])
491
+ self.SetForegroundColour(colors.app['text'])
492
+ # set bitmaps again
493
+ self._icon.reload()
494
+ self.SetBitmap(self._icon.bitmap)
495
+ self.SetBitmapCurrent(self._icon.bitmap)
496
+ self.SetBitmapPressed(self._icon.bitmap)
497
+ self.SetBitmapFocus(self._icon.bitmap)
498
+ # refresh
499
+ self.Refresh()
419
500
 
420
501
  def onHover(self, evt):
421
502
  if evt.EventType == wx.EVT_ENTER_WINDOW.typeId:
@@ -437,9 +518,7 @@ class FrameRibbonDropdownButton(wx.Panel, handlers.ThemeMixin):
437
518
  self.button = wx.Button(self, label=label, style=wx.BORDER_NONE)
438
519
  self.sizer.Add(self.button, proportion=1, border=0, flag=wx.EXPAND | wx.ALL)
439
520
  # set icon
440
- self.button.SetBitmap(
441
- icons.ButtonIcon(icon, size=32).bitmap
442
- )
521
+ self._icon = icons.ButtonIcon(icon, size=32)
443
522
  # bind button callback
444
523
  if callback is not None:
445
524
  self.button.Bind(wx.EVT_BUTTON, callback)
@@ -457,6 +536,8 @@ class FrameRibbonDropdownButton(wx.Panel, handlers.ThemeMixin):
457
536
  self.drop.Bind(wx.EVT_ENTER_WINDOW, self.onHover)
458
537
  self.drop.Bind(wx.EVT_LEAVE_WINDOW, self.onHover)
459
538
 
539
+ self._applyAppTheme()
540
+
460
541
  def onMenu(self, evt):
461
542
  menu = self.menu
462
543
  # skip if there's no menu
@@ -469,9 +550,18 @@ class FrameRibbonDropdownButton(wx.Panel, handlers.ThemeMixin):
469
550
  self.PopupMenu(menu)
470
551
 
471
552
  def _applyAppTheme(self):
472
- self.SetBackgroundColour(colors.app['frame_bg'])
473
- self.button.SetBackgroundColour(colors.app['frame_bg'])
474
- self.drop.SetBackgroundColour(colors.app['frame_bg'])
553
+ # set color
554
+ for obj in (self, self.button, self.drop):
555
+ obj.SetBackgroundColour(colors.app['frame_bg'])
556
+ obj.SetForegroundColour(colors.app['text'])
557
+ # set bitmaps again
558
+ self._icon.reload()
559
+ self.button.SetBitmap(self._icon.bitmap)
560
+ self.button.SetBitmapCurrent(self._icon.bitmap)
561
+ self.button.SetBitmapPressed(self._icon.bitmap)
562
+ self.button.SetBitmapFocus(self._icon.bitmap)
563
+ # refresh
564
+ self.Refresh()
475
565
 
476
566
  def onHover(self, evt):
477
567
  if evt.EventType == wx.EVT_ENTER_WINDOW.typeId:
@@ -676,6 +766,16 @@ class PavloviaUserCtrl(FrameRibbonDropdownButton):
676
766
  # bind deletion behaviour
677
767
  self.Bind(wx.EVT_WINDOW_DESTROY, self.onDelete)
678
768
 
769
+ self._applyAppTheme()
770
+
771
+ def _applyAppTheme(self):
772
+ # set color
773
+ for obj in (self, self.button, self.drop):
774
+ obj.SetBackgroundColour(colors.app['frame_bg'])
775
+ obj.SetForegroundColour(colors.app['text'])
776
+ # refresh
777
+ self.Refresh()
778
+
679
779
  def onDelete(self, evt=None):
680
780
  i = self.frame.app.pavloviaButtons['user'].index(self)
681
781
  self.frame.app.pavloviaButtons['user'].pop(i)
@@ -803,6 +903,16 @@ class PavloviaProjectCtrl(FrameRibbonDropdownButton):
803
903
  # bind deletion behaviour
804
904
  self.Bind(wx.EVT_WINDOW_DESTROY, self.onDelete)
805
905
 
906
+ self._applyAppTheme()
907
+
908
+ def _applyAppTheme(self):
909
+ # set color
910
+ for obj in (self, self.button, self.drop):
911
+ obj.SetBackgroundColour(colors.app['frame_bg'])
912
+ obj.SetForegroundColour(colors.app['text'])
913
+ # refresh
914
+ self.Refresh()
915
+
806
916
  def onDelete(self, evt=None):
807
917
  i = self.frame.app.pavloviaButtons['project'].index(self)
808
918
  self.frame.app.pavloviaButtons['project'].pop(i)
@@ -845,7 +955,7 @@ class PavloviaProjectCtrl(FrameRibbonDropdownButton):
845
955
  # get project
846
956
  project = self.GetTopLevelParent().project
847
957
 
848
- if project is None:
958
+ if project is None or project['path_with_namespace'] is None:
849
959
  self.button.SetLabel(_translate("No project"))
850
960
  else:
851
961
  self.button.SetLabel(project['path_with_namespace'])
@@ -895,7 +1005,7 @@ class PavloviaProjectCtrl(FrameRibbonDropdownButton):
895
1005
  else:
896
1006
  name = path = ""
897
1007
  # open dlg to create new project
898
- createDlg = sync.CreateDlg(self,
1008
+ createDlg = sync.CreateDlg(self.frame,
899
1009
  user=pavlovia.getCurrentSession().user,
900
1010
  name=name,
901
1011
  path=path)