psychopy 2024.2.1__py3-none-any.whl → 2024.2.4__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 (276) hide show
  1. psychopy/.DS_Store +0 -0
  2. psychopy/GIT_SHA +1 -1
  3. psychopy/VERSION +1 -1
  4. psychopy/__init__.py +10 -1
  5. psychopy/__init__.py.orig +65 -0
  6. psychopy/app/{locale/ar_001/.DS_Store → .DS_Store} +0 -0
  7. psychopy/app/Resources/.DS_Store +0 -0
  8. psychopy/app/_psychopyApp.py +11 -3
  9. psychopy/app/appData.spec +1 -1
  10. psychopy/app/builder/builder.py +1 -1
  11. psychopy/app/builder/builder.py.orig +3932 -0
  12. psychopy/app/builder/dialogs/__init__.py.orig +1679 -0
  13. psychopy/app/builder/dialogs/paramCtrls.py +1 -1
  14. psychopy/app/builder/dialogs/paramCtrls.py.orig +713 -0
  15. psychopy/app/colorpicker/__init__.py.orig +411 -0
  16. psychopy/app/cortex.log +0 -0
  17. psychopy/app/jobs.py +8 -1
  18. psychopy/app/locale/ar_001/LC_MESSAGE/messages.po +2452 -1731
  19. psychopy/app/locale/zh_CN/LC_MESSAGE/zh_CN.mo +0 -0
  20. psychopy/app/locale/zh_CN/LC_MESSAGE/zh_CN.po +6127 -0
  21. psychopy/app/locale/zh_CN/LC_MESSAGE/zh_CN_allFlagged.mo +0 -0
  22. psychopy/app/locale/zh_CN/LC_MESSAGE/zh_CN_allFlagged.po +7366 -0
  23. psychopy/app/plugin_manager/dialog.py +9 -7
  24. psychopy/app/ribbon.py +2 -1
  25. psychopy/app/runner/runner.py +7 -5
  26. psychopy/clock.py +8 -4
  27. psychopy/core.py.orig +169 -0
  28. psychopy/demos/builder/Design Templates/randomisedBlocks/html/index.html +23 -0
  29. psychopy/demos/builder/Design Templates/randomisedBlocks/html/randomisedBlocks-legacy-browsers.js +423 -0
  30. psychopy/demos/builder/Design Templates/randomisedBlocks/html/randomisedBlocks.js +427 -0
  31. psychopy/demos/builder/Design Templates/randomisedBlocks/html/resources/chooseBlock.xlsx +0 -0
  32. psychopy/demos/builder/Design Templates/randomisedBlocks/html/resources/facesBlock.xlsx +0 -0
  33. psychopy/demos/builder/Design Templates/randomisedBlocks/html/resources/housesBlock.xlsx +0 -0
  34. psychopy/demos/builder/Design Templates/randomisedBlocks/html/resources/stims/face01.jpg +0 -0
  35. psychopy/demos/builder/Design Templates/randomisedBlocks/html/resources/stims/face02.jpg +0 -0
  36. psychopy/demos/builder/Design Templates/randomisedBlocks/html/resources/stims/face03.jpg +0 -0
  37. psychopy/demos/builder/Design Templates/randomisedBlocks/html/resources/stims/house01.jpg +0 -0
  38. psychopy/demos/builder/Design Templates/randomisedBlocks/html/resources/stims/house02.jpg +0 -0
  39. psychopy/demos/builder/Design Templates/randomisedBlocks/html/resources/stims/house03.jpg +0 -0
  40. psychopy/demos/builder/Design Templates/randomisedBlocks/randomisedBlocks.py +330 -0
  41. psychopy/demos/builder/Design Templates/randomisedBlocks/randomisedBlocks_lastrun.py +330 -0
  42. psychopy/demos/builder/Feature Demos/eyetracking/eyetracking.xml +298 -0
  43. psychopy/demos/builder/Feature Demos/eyetracking/eyetracking.xsd +120 -0
  44. psychopy/demos/builder/Tools/.DS_Store +0 -0
  45. psychopy/demos/builder/Tools/gammaCalibration/.DS_Store +0 -0
  46. psychopy/demos/builder/Tools/gammaCalibration/data/_gamma_correction_visual_2022-05-18_14h18.29.439.csv +38 -0
  47. psychopy/demos/builder/Tools/gammaCalibration/data/_gamma_correction_visual_2022-05-18_14h18.29.439.log +3418 -0
  48. psychopy/demos/builder/Tools/gammaCalibration/data/_gamma_correction_visual_2022-05-18_14h18.29.439.psydat +0 -0
  49. psychopy/demos/builder/Tools/gammaCalibration/data/x1_gamma_correction_visual_2022-05-17_13h59.42.928.csv +2 -0
  50. psychopy/demos/builder/Tools/gammaCalibration/data/x1_gamma_correction_visual_2022-05-17_13h59.42.928.log +15 -0
  51. psychopy/demos/builder/Tools/gammaCalibration/data/x1_gamma_correction_visual_2022-05-17_13h59.42.928.psydat +0 -0
  52. psychopy/demos/builder/Tools/gammaCalibration/gamma_correction_visual.psyexp +323 -0
  53. psychopy/demos/builder/Tools/gammaCalibration/gamma_correction_visual.py +562 -0
  54. psychopy/demos/builder/Tools/gammaCalibration/gamma_correction_visual_lastrun.py +562 -0
  55. psychopy/demos/builder/Tools/gammaCalibration/questStairs.xlsx +0 -0
  56. psychopy/demos/builder/Tools/gammaCalibration/readme.md +0 -0
  57. psychopy/demos/builder/Tools/gammaCalibration/resources/low_contrast.png +0 -0
  58. psychopy/demos/builder/Tools/gammaCalibration/resources/make_2nd_order_tex.py +59 -0
  59. psychopy/demos/builder/Tools/gammaCalibration/resources/second_order_tex.png +0 -0
  60. psychopy/demos/coder/.DS_Store +0 -0
  61. psychopy/demos/coder/experiment control/info_gamma.pickle +0 -0
  62. psychopy/demos/coder/iohub/.iohpid +1 -0
  63. psychopy/demos/coder/iohub/eyetracking/.iohpid +1 -0
  64. psychopy/demos/coder/iohub/wintab/.DS_Store +0 -0
  65. psychopy/demos/coder/stimuli/.DS_Store +0 -0
  66. psychopy/demos/coder/stimuli/radialGratingContracting.py +29 -0
  67. psychopy/experiment/_experiment.py.orig +1032 -0
  68. psychopy/experiment/components/.DS_Store +0 -0
  69. psychopy/experiment/components/_base.py +13 -4
  70. psychopy/experiment/components/_base.py.orig +823 -0
  71. psychopy/experiment/components/form/.DS_Store +0 -0
  72. psychopy/experiment/components/microphone/__init__.py +10 -1
  73. psychopy/experiment/components/microphone/__init__.py.orig +490 -0
  74. psychopy/experiment/components/polygon/__init__.py +21 -22
  75. psychopy/experiment/components/settings/__init__.py +13 -14
  76. psychopy/experiment/components/settings/__init__.py.orig +1337 -0
  77. psychopy/experiment/components/textbox/__init__.py.orig +310 -0
  78. psychopy/experiment/components/webcam/.DS_Store +0 -0
  79. psychopy/experiment/components/webcam/light/.DS_Store +0 -0
  80. psychopy/experiment/flow.py +10 -8
  81. psychopy/experiment/loops.py.orig +829 -0
  82. psychopy/experiment/params.py +8 -3
  83. psychopy/experiment/params.py.orig +408 -0
  84. psychopy/experiment/routine.py.orig +503 -0
  85. psychopy/experiment/routines/_base.py +15 -6
  86. psychopy/experiment/routines/counterbalance/__init__.py +1 -0
  87. psychopy/gui/qtgui.py +14 -7
  88. psychopy/gui/util.py +10 -14
  89. psychopy/gui/wxgui.py +10 -4
  90. psychopy/hardware/.DS_Store +0 -0
  91. psychopy/hardware/brainproducts.py.orig +680 -0
  92. psychopy/hardware/iolab.py.orig +238 -0
  93. psychopy/hardware/manager.py +1 -1
  94. psychopy/hardware/photodiode.py +59 -27
  95. psychopy/hardware/serialport.py +51 -0
  96. psychopy/hardware/speaker.py +4 -4
  97. psychopy/iohub/datastore/__init__.py.orig +443 -0
  98. psychopy/iohub/datastore/util.py.orig +692 -0
  99. psychopy/iohub/devices/mouse/darwin.py.orig +427 -0
  100. psychopy/iohub/devices/mouse/linux2.py.orig +198 -0
  101. psychopy/preferences/.DS_Store +0 -0
  102. psychopy/projects/pavlovia.py +10 -3
  103. psychopy/projects/pavlovia.py.orig +1295 -0
  104. psychopy/sound/backend_ptb.py +22 -5
  105. psychopy/sound/transcribe.py +24 -4
  106. psychopy/tests/.DS_Store +0 -0
  107. psychopy/tests/data/.DS_Store +0 -0
  108. psychopy/tests/data/TestCircle_fill_local.png +0 -0
  109. psychopy/tests/data/__test.png +0 -0
  110. psychopy/tests/data/aperture1_normHexbackground_local.png +0 -0
  111. psychopy/tests/data/aperture1_norm_local.png +0 -0
  112. psychopy/tests/data/aperture2_normHexbackground_local.png +0 -0
  113. psychopy/tests/data/beatandrcos_height_local.png +0 -0
  114. psychopy/tests/data/beatandrcos_normAddBlend_local.png +0 -0
  115. psychopy/tests/data/beatandrcos_normHexbackground_local.png +0 -0
  116. psychopy/tests/data/beatandrcos_norm_local.png +0 -0
  117. psychopy/tests/data/beatandrcos_stencil_local.png +0 -0
  118. psychopy/tests/data/blend_add_height_local.png +0 -0
  119. psychopy/tests/data/blend_add_normAddBlend_local.png +0 -0
  120. psychopy/tests/data/blend_add_normHexbackground_local.png +0 -0
  121. psychopy/tests/data/blend_add_normNoShade_local.png +0 -0
  122. psychopy/tests/data/blend_add_norm_local.png +0 -0
  123. psychopy/tests/data/blend_add_stencil_local.png +0 -0
  124. psychopy/tests/data/bufferimg_gabor_height_local.png +0 -0
  125. psychopy/tests/data/bufferimg_gabor_normAddBlend_local.png +0 -0
  126. psychopy/tests/data/bufferimg_gabor_normHexbackground_local.png +0 -0
  127. psychopy/tests/data/bufferimg_gabor_normNoShade_local.png +0 -0
  128. psychopy/tests/data/bufferimg_gabor_norm_local.png +0 -0
  129. psychopy/tests/data/bufferimg_gabor_stencil_local.png +0 -0
  130. psychopy/tests/data/circleHex_height_local.png +0 -0
  131. psychopy/tests/data/circleHex_normAddBlend_local.png +0 -0
  132. psychopy/tests/data/circleHex_normHexbackground_local.png +0 -0
  133. psychopy/tests/data/circleHex_normNoShade_local.png +0 -0
  134. psychopy/tests/data/circleHex_norm_local.png +0 -0
  135. psychopy/tests/data/circleHex_stencil_local.png +0 -0
  136. psychopy/tests/data/color_comparison_local.png +0 -0
  137. psychopy/tests/data/corrFullRandom_local.csv +16 -0
  138. psychopy/tests/data/corrFullRandom_local.tsv +6 -0
  139. psychopy/tests/data/correctScript/.DS_Store +0 -0
  140. psychopy/tests/data/dots_height_local.png +0 -0
  141. psychopy/tests/data/dots_normAddBlend_local.png +0 -0
  142. psychopy/tests/data/dots_normHexbackground_local.png +0 -0
  143. psychopy/tests/data/dots_normNoShade_local.png +0 -0
  144. psychopy/tests/data/dots_norm_local.png +0 -0
  145. psychopy/tests/data/dots_stencil_local.png +0 -0
  146. psychopy/tests/data/elarray1_height_local.png +0 -0
  147. psychopy/tests/data/elarray1_normAddBlend_local.png +0 -0
  148. psychopy/tests/data/elarray1_normHexbackground_local.png +0 -0
  149. psychopy/tests/data/elarray1_norm_local.png +0 -0
  150. psychopy/tests/data/elarray1_stencil_local.png +0 -0
  151. psychopy/tests/data/envelopeandrcos_height_local.png +0 -0
  152. psychopy/tests/data/envelopeandrcos_normAddBlend_local.png +0 -0
  153. psychopy/tests/data/envelopeandrcos_normHexbackground_local.png +0 -0
  154. psychopy/tests/data/envelopeandrcos_norm_local.png +0 -0
  155. psychopy/tests/data/envelopeandrcos_stencil_local.png +0 -0
  156. psychopy/tests/data/envelopepowerandrcos_height_local.png +0 -0
  157. psychopy/tests/data/envelopepowerandrcos_normAddBlend_local.png +0 -0
  158. psychopy/tests/data/envelopepowerandrcos_normHexbackground_local.png +0 -0
  159. psychopy/tests/data/envelopepowerandrcos_norm_local.png +0 -0
  160. psychopy/tests/data/envelopepowerandrcos_stencil_local.png +0 -0
  161. psychopy/tests/data/gabor1_height_local.png +0 -0
  162. psychopy/tests/data/gabor1_normAddBlend_local.png +0 -0
  163. psychopy/tests/data/gabor1_normHexbackground_local.png +0 -0
  164. psychopy/tests/data/gabor1_normNoShade_local.png +0 -0
  165. psychopy/tests/data/gabor1_norm_local.png +0 -0
  166. psychopy/tests/data/gabor1_stencil_local.png +0 -0
  167. psychopy/tests/data/greyscale_normHexbackground_local.png +0 -0
  168. psychopy/tests/data/imageAndGauss_height_local.png +0 -0
  169. psychopy/tests/data/imageAndGauss_normAddBlend_local.png +0 -0
  170. psychopy/tests/data/imageAndGauss_normHexbackground_local.png +0 -0
  171. psychopy/tests/data/imageAndGauss_normNoShade_local.png +0 -0
  172. psychopy/tests/data/imageAndGauss_norm_local.png +0 -0
  173. psychopy/tests/data/imageAndGauss_stencil_local.png +0 -0
  174. psychopy/tests/data/movFrame1_stencil_local.png +0 -0
  175. psychopy/tests/data/noiseAndRcos_height_local.png +0 -0
  176. psychopy/tests/data/noiseAndRcos_normAddBlend_local.png +0 -0
  177. psychopy/tests/data/noiseAndRcos_normHexbackground_local.png +0 -0
  178. psychopy/tests/data/noiseAndRcos_normNoShade_local.png +0 -0
  179. psychopy/tests/data/noiseAndRcos_norm_local.png +0 -0
  180. psychopy/tests/data/noiseAndRcos_stencil_local.png +0 -0
  181. psychopy/tests/data/noiseFiltersAndRcos_height_local.png +0 -0
  182. psychopy/tests/data/noiseFiltersAndRcos_normAddBlend_local.png +0 -0
  183. psychopy/tests/data/noiseFiltersAndRcos_normHexbackground_local.png +0 -0
  184. psychopy/tests/data/noiseFiltersAndRcos_normNoShade_local.png +0 -0
  185. psychopy/tests/data/noiseFiltersAndRcos_norm_local.png +0 -0
  186. psychopy/tests/data/noiseFiltersAndRcos_stencil_local.png +0 -0
  187. psychopy/tests/data/numpyImage_height_local.png +0 -0
  188. psychopy/tests/data/numpyImage_normAddBlend_local.png +0 -0
  189. psychopy/tests/data/numpyImage_normHexbackground_local.png +0 -0
  190. psychopy/tests/data/numpyImage_normNoShade_local.png +0 -0
  191. psychopy/tests/data/numpyImage_norm_local.png +0 -0
  192. psychopy/tests/data/numpyImage_stencil_local.png +0 -0
  193. psychopy/tests/data/shape2_1_normAddBlend_local.png +0 -0
  194. psychopy/tests/data/shape2_1_normHexbackground_local.png +0 -0
  195. psychopy/tests/data/shape2_1_normNoShade_local.png +0 -0
  196. psychopy/tests/data/shape2_1_norm_local.png +0 -0
  197. psychopy/tests/data/shape2_1_stencil_local.png +0 -0
  198. psychopy/tests/data/testLoopsBlocks.psyexp_local.py +328 -0
  199. psychopy/tests/data/text1_height_local.png +0 -0
  200. psychopy/tests/data/text1_normAddBlend_local.png +0 -0
  201. psychopy/tests/data/text1_normHexbackground_local.png +0 -0
  202. psychopy/tests/data/text1_norm_local.png +0 -0
  203. psychopy/tests/data/text1_stencil_local.png +0 -0
  204. psychopy/tests/data/text2_height.png +0 -0
  205. psychopy/tests/data/text2_normAddBlend.png +0 -0
  206. psychopy/tests/data/text2_normHexbackground.png +0 -0
  207. psychopy/tests/data/text2_stencil.png +0 -0
  208. psychopy/tests/data/wedge1_height_local.png +0 -0
  209. psychopy/tests/data/wedge1_normAddBlend_local.png +0 -0
  210. psychopy/tests/data/wedge1_normHexbackground_local.png +0 -0
  211. psychopy/tests/data/wedge1_normNoShade_local.png +0 -0
  212. psychopy/tests/data/wedge1_norm_local.png +0 -0
  213. psychopy/tests/data/wedge1_stencil_local.png +0 -0
  214. psychopy/tests/test_app/.DS_Store +0 -0
  215. psychopy/tests/test_app/test_builder/.DS_Store +0 -0
  216. psychopy/tests/test_app/test_builder/data/_2021_ 5_03_1206.csv +9 -0
  217. psychopy/tests/test_app/test_builder/data/_2021_ 5_03_1206.log +177 -0
  218. psychopy/tests/test_app/test_builder/data/_2021_ 5_03_1206.psydat +0 -0
  219. psychopy/tests/test_app/test_builder/data/_2021_ 5_03_1206.xlsx +0 -0
  220. psychopy/tests/test_app/test_builder/data/_2021_ 5_03_1324.csv +9 -0
  221. psychopy/tests/test_app/test_builder/data/_2021_ 5_03_1324.log +168 -0
  222. psychopy/tests/test_app/test_builder/data/_2021_ 5_03_1324.psydat +0 -0
  223. psychopy/tests/test_app/test_builder/data/_2021_ 5_03_1324.xlsx +0 -0
  224. psychopy/tests/test_data/.DS_Store +0 -0
  225. psychopy/tests/test_hardware/test_CRS_BitsSharp.py.orig +68 -0
  226. psychopy/tests/test_tools/test_arraytools.py +112 -0
  227. psychopy/tests/test_visual/test_image.py.orig +219 -0
  228. psychopy/tools/arraytools.py +47 -0
  229. psychopy/tools/versionchooser.py +1 -1
  230. psychopy/visual/backends/pygletbackend.py +26 -8
  231. psychopy/visual/basevisual.py.orig +1723 -0
  232. psychopy/visual/form.py.orig +1181 -0
  233. psychopy/visual/text.py.orig +752 -0
  234. psychopy/visual/textbox2/textbox2.py.orig +1315 -0
  235. psychopy/visual/window.py +13 -5
  236. psychopy/visual/windowwarp.py.orig +463 -0
  237. {psychopy-2024.2.1.dist-info → psychopy-2024.2.4.dist-info}/METADATA +9 -9
  238. {psychopy-2024.2.1.dist-info → psychopy-2024.2.4.dist-info}/RECORD +244 -78
  239. {psychopy-2024.2.1.dist-info → psychopy-2024.2.4.dist-info}/WHEEL +1 -1
  240. {psychopy-2024.2.1.dist-info → psychopy-2024.2.4.dist-info}/entry_points.txt +2 -0
  241. psychopy/app/locale/ar_001/LC_MESSAGE/messages.mo +0 -0
  242. psychopy/app/locale/cs_CZ/LC_MESSAGE/messages.mo +0 -0
  243. psychopy/app/locale/da_DK/LC_MESSAGE/messages.mo +0 -0
  244. psychopy/app/locale/de_DE/LC_MESSAGE/messages.mo +0 -0
  245. psychopy/app/locale/el_GR/LC_MESSAGE/messages.mo +0 -0
  246. psychopy/app/locale/en_NZ/LC_MESSAGE/messages.mo +0 -0
  247. psychopy/app/locale/en_US/LC_MESSAGE/messages.mo +0 -0
  248. psychopy/app/locale/es_CO/LC_MESSAGE/messages.mo +0 -0
  249. psychopy/app/locale/es_ES/LC_MESSAGE/messages.mo +0 -0
  250. psychopy/app/locale/es_US/LC_MESSAGE/messages.mo +0 -0
  251. psychopy/app/locale/et_EE/LC_MESSAGE/messages.mo +0 -0
  252. psychopy/app/locale/fa_IR/LC_MESSAGE/messages.mo +0 -0
  253. psychopy/app/locale/fi_FI/LC_MESSAGE/messages.mo +0 -0
  254. psychopy/app/locale/fr_FR/LC_MESSAGE/messages.mo +0 -0
  255. psychopy/app/locale/he_IL/LC_MESSAGE/messages.mo +0 -0
  256. psychopy/app/locale/hi_IN/LC_MESSAGE/messages.mo +0 -0
  257. psychopy/app/locale/hu_HU/LC_MESSAGE/messages.mo +0 -0
  258. psychopy/app/locale/it_IT/LC_MESSAGE/messages.mo +0 -0
  259. psychopy/app/locale/ja_JP/LC_MESSAGE/messages.mo +0 -0
  260. psychopy/app/locale/ko_KR/LC_MESSAGE/messages.mo +0 -0
  261. psychopy/app/locale/ms_MY/LC_MESSAGE/messages.mo +0 -0
  262. psychopy/app/locale/nl_NL/LC_MESSAGE/messages.mo +0 -0
  263. psychopy/app/locale/nn_NO/LC_MESSAGE/messages.mo +0 -0
  264. psychopy/app/locale/pl_PL/LC_MESSAGE/messages.mo +0 -0
  265. psychopy/app/locale/pt_PT/LC_MESSAGE/messages.mo +0 -0
  266. psychopy/app/locale/ro_RO/LC_MESSAGE/messages.mo +0 -0
  267. psychopy/app/locale/ru_RU/LC_MESSAGE/messages.mo +0 -0
  268. psychopy/app/locale/sv_SE/LC_MESSAGE/messages.mo +0 -0
  269. psychopy/app/locale/tr_TR/LC_MESSAGE/messages.mo +0 -0
  270. psychopy/app/locale/zh_CN/LC_MESSAGE/messages.mo +0 -0
  271. psychopy/app/locale/zh_TW/LC_MESSAGE/messages.mo +0 -0
  272. psychopy-2024.2.1.dist-info/licenses/AUTHORS.md +0 -138
  273. /psychopy/{app/locale/ar_001/LC_MESSAGE → demos/builder}/.DS_Store +0 -0
  274. /psychopy/{app/locale/es_ES/LC_MESSAGE → demos/builder/Experiments}/.DS_Store +0 -0
  275. /psychopy/{visual → demos/builder/Tools/gammaCalibration/data}/.DS_Store +0 -0
  276. {psychopy-2024.2.1.dist-info → psychopy-2024.2.4.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,3932 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """
5
+ Defines the behavior of Psychopy's Builder view window
6
+ Part of the PsychoPy library
7
+ Copyright (C) 2002-2018 Jonathan Peirce (C) 2019-2022 Open Science Tools Ltd.
8
+ Distributed under the terms of the GNU General Public License (GPL).
9
+ """
10
+
11
+ import os, sys
12
+ import subprocess
13
+ import webbrowser
14
+ from pathlib import Path
15
+ import glob
16
+ import copy
17
+ import traceback
18
+ import codecs
19
+ import numpy
20
+
21
+ from pkg_resources import parse_version
22
+ import wx.stc
23
+ from wx.lib import scrolledpanel
24
+ from wx.lib import platebtn
25
+ from wx.html import HtmlWindow
26
+
27
+ from .validators import WarningManager
28
+ from ..pavlovia_ui import sync
29
+ from ..pavlovia_ui.project import ProjectFrame
30
+ from ..pavlovia_ui.search import SearchFrame
31
+ from ..pavlovia_ui.user import UserFrame
32
+ from ...experiment.components import getAllCategories
33
+ from ...experiment.routines import Routine, BaseStandaloneRoutine
34
+ from ...tools.stringtools import prettyname
35
+
36
+ try:
37
+ import markdown_it as md
38
+ except ImportError:
39
+ md = None
40
+ import wx.lib.agw.aui as aui # some versions of phoenix
41
+ try:
42
+ from wx.adv import PseudoDC
43
+ except ImportError:
44
+ from wx import PseudoDC
45
+
46
+ if parse_version(wx.__version__) < parse_version('4.0.3'):
47
+ wx.NewIdRef = wx.NewId
48
+
49
+ from psychopy.localization import _translate
50
+ from ... import experiment, prefs
51
+ from .. import dialogs
52
+ from ..themes import icons, colors, handlers
53
+ from ..themes.ui import ThemeSwitcher
54
+ from ..ui import BaseAuiFrame
55
+ from psychopy import logging, data
56
+ from psychopy.tools.filetools import mergeFolder
57
+ from .dialogs import (DlgComponentProperties, DlgExperimentProperties,
58
+ DlgCodeComponentProperties, DlgLoopProperties,
59
+ ParamNotebook, DlgNewRoutine)
60
+ from ..utils import (BasePsychopyToolbar, PsychopyPlateBtn, WindowFrozen,
61
+ FileDropTarget, FrameSwitcher, updateDemosMenu,
62
+ ToggleButtonArray, HoverMixin)
63
+
64
+ from psychopy.experiment import getAllStandaloneRoutines
65
+ from psychopy.app import pavlovia_ui
66
+ from psychopy.projects import pavlovia
67
+
68
+ from psychopy.scripts.psyexpCompile import generateScript
69
+
70
+ # _localized separates internal (functional) from displayed strings
71
+ # long form here allows poedit string discovery
72
+ _localized = {
73
+ 'Field': _translate('Field'),
74
+ 'Default': _translate('Default'),
75
+ 'Favorites': _translate('Favorites'),
76
+ 'Stimuli': _translate('Stimuli'),
77
+ 'Responses': _translate('Responses'),
78
+ 'Custom': _translate('Custom'),
79
+ 'I/O': _translate('I/O'),
80
+ 'Add to favorites': _translate('Add to favorites'),
81
+ 'Remove from favorites': _translate('Remove from favorites'),
82
+ # contextMenuLabels
83
+ 'edit': _translate('edit'),
84
+ 'remove': _translate('remove'),
85
+ 'copy': _translate('copy'),
86
+ 'paste above': _translate('paste above'),
87
+ 'paste below': _translate('paste below'),
88
+ 'move to top': _translate('move to top'),
89
+ 'move up': _translate('move up'),
90
+ 'move down': _translate('move down'),
91
+ 'move to bottom': _translate('move to bottom')
92
+ }
93
+
94
+
95
+ # Components which are always hidden
96
+ alwaysHidden = [
97
+ 'SettingsComponent', 'UnknownComponent', 'UnknownRoutine', 'UnknownStandaloneRoutine'
98
+ ]
99
+
100
+
101
+ class TemplateManager(dict):
102
+ mainFolder = Path(prefs.paths['resources']).absolute() / 'routine_templates'
103
+ userFolder = Path(prefs.paths['userPrefsDir']).absolute() / 'routine_templates'
104
+ experimentFiles = {}
105
+
106
+ def __init__(self):
107
+ dict.__init__(self)
108
+ self.updateTemplates()
109
+
110
+ def updateTemplates(self, ):
111
+ """Search and import templates in the standard files"""
112
+ for folder in [TemplateManager.mainFolder, TemplateManager.userFolder]:
113
+ categs = folder.glob("*.psyexp")
114
+ for filePath in categs:
115
+ thisExp = experiment.Experiment()
116
+ thisExp.loadFromXML(filePath)
117
+ categName = filePath.stem
118
+ self[categName]={}
119
+ for routineName in thisExp.routines:
120
+ self[categName][routineName] = copy.copy(thisExp.routines[routineName])
121
+
122
+
123
+ class BuilderFrame(BaseAuiFrame, handlers.ThemeMixin):
124
+ """Defines construction of the Psychopy Builder Frame"""
125
+
126
+ routineTemplates = TemplateManager()
127
+
128
+ def __init__(self, parent, id=-1, title='PsychoPy (Experiment Builder)',
129
+ pos=wx.DefaultPosition, fileName=None, frameData=None,
130
+ style=wx.DEFAULT_FRAME_STYLE, app=None):
131
+
132
+ if (fileName is not None) and (type(fileName) == bytes):
133
+ fileName = fileName.decode(sys.getfilesystemencoding())
134
+
135
+ self.app = app
136
+ self.dpi = self.app.dpi
137
+ # things the user doesn't set like winsize etc:
138
+ self.appData = self.app.prefs.appData['builder']
139
+ # things about the builder that the user can set:
140
+ self.prefs = self.app.prefs.builder
141
+ self.appPrefs = self.app.prefs.app
142
+ self.paths = self.app.prefs.paths
143
+ self.frameType = 'builder'
144
+ self.filename = fileName
145
+ self.htmlPath = None
146
+ self.session = pavlovia.getCurrentSession()
147
+ self.scriptProcess = None
148
+ self.stdoutBuffer = None
149
+ self.readmeFrame = None
150
+ self.generateScript = generateScript
151
+
152
+ # default window title
153
+ self.winTitle = 'PsychoPy Builder (v{})'.format(self.app.version)
154
+
155
+ if fileName in self.appData['frames']:
156
+ self.frameData = self.appData['frames'][fileName]
157
+ else: # work out a new frame size/location
158
+ dispW, dispH = self.app.getPrimaryDisplaySize()
159
+ default = self.appData['defaultFrame']
160
+ default['winW'] = int(dispW * 0.75)
161
+ default['winH'] = int(dispH * 0.75)
162
+ if default['winX'] + default['winW'] > dispW:
163
+ default['winX'] = 5
164
+ if default['winY'] + default['winH'] > dispH:
165
+ default['winY'] = 5
166
+ self.frameData = dict(self.appData['defaultFrame']) # copy
167
+ # increment default for next frame
168
+ default['winX'] += 10
169
+ default['winY'] += 10
170
+
171
+ # we didn't have the key or the win was minimized / invalid
172
+ if self.frameData['winH'] == 0 or self.frameData['winW'] == 0:
173
+ self.frameData['winX'], self.frameData['winY'] = (0, 0)
174
+ if self.frameData['winY'] < 20:
175
+ self.frameData['winY'] = 20
176
+
177
+ BaseAuiFrame.__init__(self, parent=parent, id=id, title=title,
178
+ pos=(int(self.frameData['winX']),
179
+ int(self.frameData['winY'])),
180
+ size=(int(self.frameData['winW']),
181
+ int(self.frameData['winH'])),
182
+ style=style)
183
+
184
+ # detect retina displays (then don't use double-buffering)
185
+ self.isRetina = \
186
+ self.GetContentScaleFactor() != 1 and wx.Platform == '__WXMAC__'
187
+
188
+ # create icon
189
+ if sys.platform != 'darwin':
190
+ # doesn't work on darwin and not necessary: handled by app bundle
191
+ iconFile = os.path.join(self.paths['resources'], 'builder.ico')
192
+ if os.path.isfile(iconFile):
193
+ self.SetIcon(wx.Icon(iconFile, wx.BITMAP_TYPE_ICO))
194
+
195
+ # create our panels
196
+ self.flowPanel = FlowPanel(frame=self)
197
+ self.routinePanel = RoutinesNotebook(self)
198
+ self.componentButtons = ComponentsPanel(self)
199
+ # menus and toolbars
200
+ self.toolbar = BuilderToolbar(frame=self)
201
+ self.SetToolBar(self.toolbar)
202
+ self.toolbar.Realize()
203
+ self.makeMenus()
204
+ self.CreateStatusBar()
205
+ self.SetStatusText("")
206
+
207
+ # setup universal shortcuts
208
+ accelTable = self.app.makeAccelTable()
209
+ self.SetAcceleratorTable(accelTable)
210
+
211
+ # setup a default exp
212
+ if fileName is not None and os.path.isfile(fileName):
213
+ self.fileOpen(filename=fileName, closeCurrent=False)
214
+ else:
215
+ self.lastSavedCopy = None
216
+ # don't try to close before opening
217
+ self.fileNew(closeCurrent=False)
218
+
219
+ self.updateReadme() # check/create frame as needed
220
+
221
+ # control the panes using aui manager
222
+ self._mgr = self.getAuiManager()
223
+
224
+ #self._mgr.SetArtProvider(PsychopyDockArt())
225
+ #self._art = self._mgr.GetArtProvider()
226
+ # Create panels
227
+ self._mgr.AddPane(self.routinePanel,
228
+ aui.AuiPaneInfo().
229
+ Name("Routines").Caption("Routines").CaptionVisible(True).
230
+ Floatable(False).
231
+ Movable(False).
232
+ CloseButton(False).MaximizeButton(True).PaneBorder(False).
233
+ Center()) # 'center panes' expand
234
+ rtPane = self._mgr.GetPane('Routines')
235
+ self._mgr.AddPane(self.componentButtons,
236
+ aui.AuiPaneInfo().
237
+ Name("Components").Caption("Components").CaptionVisible(True).
238
+ Floatable(False).
239
+ RightDockable(True).LeftDockable(True).
240
+ CloseButton(False).PaneBorder(False))
241
+ compPane = self._mgr.GetPane('Components')
242
+ self._mgr.AddPane(self.flowPanel,
243
+ aui.AuiPaneInfo().
244
+ Name("Flow").Caption("Flow").CaptionVisible(True).
245
+ BestSize((8 * self.dpi, 2 * self.dpi)).
246
+ Floatable(False).
247
+ RightDockable(True).LeftDockable(True).
248
+ CloseButton(False).PaneBorder(False))
249
+ flowPane = self._mgr.GetPane('Flow')
250
+ self.layoutPanes()
251
+ rtPane.CaptionVisible(True)
252
+ # tell the manager to 'commit' all the changes just made
253
+ self._mgr.Update()
254
+ # self.SetSizer(self.mainSizer) # not necessary for aui type controls
255
+ if self.frameData['auiPerspective']:
256
+ self._mgr.LoadPerspective(self.frameData['auiPerspective'])
257
+ self.SetMinSize(wx.Size(600, 400)) # min size for the whole window
258
+ self.SetSize(
259
+ (int(self.frameData['winW']), int(self.frameData['winH'])))
260
+ self.SendSizeEvent()
261
+ self._mgr.Update()
262
+
263
+ # self.SetAutoLayout(True)
264
+ self.Bind(wx.EVT_CLOSE, self.closeFrame)
265
+ self.Bind(wx.EVT_SIZE, self.onResize)
266
+
267
+ self.app.trackFrame(self)
268
+ self.SetDropTarget(FileDropTarget(targetFrame=self))
269
+
270
+ self.theme = colors.theme
271
+
272
+ # Synonymise Aui manager for use with theme mixin
273
+ def GetAuiManager(self):
274
+ return self._mgr
275
+
276
+ def makeMenus(self):
277
+ """
278
+ Produces Menus for the Builder Frame
279
+ """
280
+
281
+ # ---Menus---#000000#FFFFFF-------------------------------------------
282
+ menuBar = wx.MenuBar()
283
+ # ---_file---#000000#FFFFFF-------------------------------------------
284
+ self.fileMenu = wx.Menu()
285
+ menuBar.Append(self.fileMenu, _translate('&File'))
286
+
287
+ # create a file history submenu
288
+ self.fileHistoryMaxFiles = 10
289
+ self.fileHistory = wx.FileHistory(maxFiles=self.fileHistoryMaxFiles)
290
+ self.recentFilesMenu = wx.Menu()
291
+ self.fileHistory.UseMenu(self.recentFilesMenu)
292
+ for filename in self.appData['fileHistory']:
293
+ if os.path.exists(filename):
294
+ self.fileHistory.AddFileToHistory(filename)
295
+ self.Bind(wx.EVT_MENU_RANGE, self.OnFileHistory,
296
+ id=wx.ID_FILE1, id2=wx.ID_FILE9)
297
+ keys = self.app.keys
298
+ menu = self.fileMenu
299
+ menu.Append(
300
+ wx.ID_NEW,
301
+ _translate("&New\t%s") % keys['new'])
302
+ menu.Append(
303
+ wx.ID_OPEN,
304
+ _translate("&Open...\t%s") % keys['open'])
305
+ menu.AppendSubMenu(
306
+ self.recentFilesMenu,
307
+ _translate("Open &Recent"))
308
+ menu.Append(
309
+ wx.ID_SAVE,
310
+ _translate("&Save\t%s") % keys['save'],
311
+ _translate("Save current experiment file"))
312
+ menu.Append(
313
+ wx.ID_SAVEAS,
314
+ _translate("Save &as...\t%s") % keys['saveAs'],
315
+ _translate("Save current experiment file as..."))
316
+ exportMenu = menu.Append(
317
+ -1,
318
+ _translate("Export HTML...\t%s") % keys['exportHTML'],
319
+ _translate("Export experiment to html/javascript file"))
320
+ menu.Append(
321
+ wx.ID_CLOSE,
322
+ _translate("&Close file\t%s") % keys['close'],
323
+ _translate("Close current experiment"))
324
+ self.Bind(wx.EVT_MENU, self.app.newBuilderFrame, id=wx.ID_NEW)
325
+ self.Bind(wx.EVT_MENU, self.fileExport, id=exportMenu.GetId())
326
+ self.Bind(wx.EVT_MENU, self.fileSave, id=wx.ID_SAVE)
327
+ menu.Enable(wx.ID_SAVE, False)
328
+ self.Bind(wx.EVT_MENU, self.fileSaveAs, id=wx.ID_SAVEAS)
329
+ self.Bind(wx.EVT_MENU, self.fileOpen, id=wx.ID_OPEN)
330
+ self.Bind(wx.EVT_MENU, self.commandCloseFrame, id=wx.ID_CLOSE)
331
+ self.fileMenu.AppendSeparator()
332
+ item = menu.Append(
333
+ wx.ID_PREFERENCES,
334
+ _translate("&Preferences\t%s") % keys['preferences'])
335
+ self.Bind(wx.EVT_MENU, self.app.showPrefs, item)
336
+ item = menu.Append(
337
+ wx.ID_ANY, _translate("Reset preferences...")
338
+ )
339
+ self.Bind(wx.EVT_MENU, self.resetPrefs, item)
340
+ # item = menu.Append(wx.NewId(), "Plug&ins")
341
+ # self.Bind(wx.EVT_MENU, self.pluginManager, item)
342
+ menu.AppendSeparator()
343
+ msg = _translate("Close PsychoPy Builder")
344
+ item = menu.Append(wx.ID_ANY, msg)
345
+ self.Bind(wx.EVT_MENU, self.closeFrame, id=item.GetId())
346
+ self.fileMenu.AppendSeparator()
347
+ self.fileMenu.Append(wx.ID_EXIT,
348
+ _translate("&Quit\t%s") % keys['quit'],
349
+ _translate("Terminate the program"))
350
+ self.Bind(wx.EVT_MENU, self.quit, id=wx.ID_EXIT)
351
+
352
+ # ------------- edit ------------------------------------
353
+ self.editMenu = wx.Menu()
354
+ menuBar.Append(self.editMenu, _translate('&Edit'))
355
+ menu = self.editMenu
356
+ self._undoLabel = menu.Append(wx.ID_UNDO,
357
+ _translate("Undo\t%s") % keys['undo'],
358
+ _translate("Undo last action"),
359
+ wx.ITEM_NORMAL)
360
+ self.Bind(wx.EVT_MENU, self.undo, id=wx.ID_UNDO)
361
+ self._redoLabel = menu.Append(wx.ID_REDO,
362
+ _translate("Redo\t%s") % keys['redo'],
363
+ _translate("Redo last action"),
364
+ wx.ITEM_NORMAL)
365
+ self.Bind(wx.EVT_MENU, self.redo, id=wx.ID_REDO)
366
+ menu.Append(wx.ID_PASTE, _translate("&Paste\t%s") % keys['paste'])
367
+ self.Bind(wx.EVT_MENU, self.paste, id=wx.ID_PASTE)
368
+
369
+ # ---_view---#000000#FFFFFF-------------------------------------------
370
+ self.viewMenu = wx.Menu()
371
+ menuBar.Append(self.viewMenu, _translate('&View'))
372
+ menu = self.viewMenu
373
+
374
+ # item = menu.Append(wx.ID_ANY,
375
+ # _translate("Open Coder view"),
376
+ # _translate("Open a new Coder view"))
377
+ # self.Bind(wx.EVT_MENU, self.app.showCoder, item)
378
+ #
379
+ # item = menu.Append(wx.ID_ANY,
380
+ # _translate("Open Runner view"),
381
+ # _translate("Open the Runner view"))
382
+ # self.Bind(wx.EVT_MENU, self.app.showRunner, item)
383
+ # menu.AppendSeparator()
384
+
385
+ item = menu.Append(wx.ID_ANY,
386
+ _translate("&Toggle readme\t%s") % self.app.keys[
387
+ 'toggleReadme'],
388
+ _translate("Toggle Readme"))
389
+ self.Bind(wx.EVT_MENU, self.toggleReadme, item)
390
+ item = menu.Append(wx.ID_ANY,
391
+ _translate("&Flow Larger\t%s") % self.app.keys[
392
+ 'largerFlow'],
393
+ _translate("Larger flow items"))
394
+ self.Bind(wx.EVT_MENU, self.flowPanel.increaseSize, item)
395
+ item = menu.Append(wx.ID_ANY,
396
+ _translate("&Flow Smaller\t%s") % self.app.keys[
397
+ 'smallerFlow'],
398
+ _translate("Smaller flow items"))
399
+ self.Bind(wx.EVT_MENU, self.flowPanel.decreaseSize, item)
400
+ item = menu.Append(wx.ID_ANY,
401
+ _translate("&Routine Larger\t%s") % keys[
402
+ 'largerRoutine'],
403
+ _translate("Larger routine items"))
404
+ self.Bind(wx.EVT_MENU, self.routinePanel.increaseSize, item)
405
+ item = menu.Append(wx.ID_ANY,
406
+ _translate("&Routine Smaller\t%s") % keys[
407
+ 'smallerRoutine'],
408
+ _translate("Smaller routine items"))
409
+ self.Bind(wx.EVT_MENU, self.routinePanel.decreaseSize, item)
410
+ menu.AppendSeparator()
411
+ # Add Theme Switcher
412
+ self.themesMenu = ThemeSwitcher(app=self.app)
413
+ menu.AppendSubMenu(self.themesMenu,
414
+ _translate("Themes"))
415
+
416
+ # ---_tools ---#000000#FFFFFF-----------------------------------------
417
+ self.toolsMenu = wx.Menu()
418
+ menuBar.Append(self.toolsMenu, _translate('&Tools'))
419
+ menu = self.toolsMenu
420
+ item = menu.Append(wx.ID_ANY,
421
+ _translate("Monitor Center"),
422
+ _translate("To set information about your monitor"))
423
+ self.Bind(wx.EVT_MENU, self.app.openMonitorCenter, item)
424
+
425
+ item = menu.Append(wx.ID_ANY,
426
+ _translate("Compile\t%s") % keys['compileScript'],
427
+ _translate("Compile the exp to a script"))
428
+ self.Bind(wx.EVT_MENU, self.compileScript, item)
429
+ self.bldrRun = menu.Append(wx.ID_ANY,
430
+ _translate("Run\t%s") % keys['runScript'],
431
+ _translate("Run the current script"))
432
+ self.Bind(wx.EVT_MENU, self.runFile, self.bldrRun, id=self.bldrRun)
433
+ item = menu.Append(wx.ID_ANY,
434
+ _translate("Send to runner\t%s") % keys['runnerScript'],
435
+ _translate("Send current script to runner"))
436
+ self.Bind(wx.EVT_MENU, self.runFile, item)
437
+ menu.AppendSeparator()
438
+ item = menu.Append(wx.ID_ANY,
439
+ _translate("PsychoPy updates..."),
440
+ _translate("Update PsychoPy to the latest, or a "
441
+ "specific, version"))
442
+ self.Bind(wx.EVT_MENU, self.app.openUpdater, item)
443
+ if hasattr(self.app, 'benchmarkWizard'):
444
+ item = menu.Append(wx.ID_ANY,
445
+ _translate("Benchmark wizard"),
446
+ _translate("Check software & hardware, generate "
447
+ "report"))
448
+ self.Bind(wx.EVT_MENU, self.app.benchmarkWizard, item)
449
+
450
+ # ---_experiment---#000000#FFFFFF-------------------------------------
451
+ self.expMenu = wx.Menu()
452
+ menuBar.Append(self.expMenu, _translate('&Experiment'))
453
+ menu = self.expMenu
454
+ item = menu.Append(wx.ID_ANY,
455
+ _translate("&New Routine\t%s") % keys['newRoutine'],
456
+ _translate("Create a new routine (e.g. the trial "
457
+ "definition)"))
458
+ self.Bind(wx.EVT_MENU, self.addRoutine, item)
459
+ item = menu.Append(wx.ID_ANY,
460
+ _translate("&Copy Routine\t%s") % keys[
461
+ 'copyRoutine'],
462
+ _translate("Copy the current routine so it can be "
463
+ "used in another exp"),
464
+ wx.ITEM_NORMAL)
465
+ self.Bind(wx.EVT_MENU, self.onCopyRoutine, item)
466
+ item = menu.Append(wx.ID_ANY,
467
+ _translate("&Paste Routine\t%s") % keys[
468
+ 'pasteRoutine'],
469
+ _translate("Paste the Routine into the current "
470
+ "experiment"),
471
+ wx.ITEM_NORMAL)
472
+ self.Bind(wx.EVT_MENU, self.onPasteRoutine, item)
473
+ item = menu.Append(wx.ID_ANY,
474
+ _translate("&Rename Routine\t%s") % keys[
475
+ 'renameRoutine'],
476
+ _translate("Change the name of this routine"))
477
+ self.Bind(wx.EVT_MENU, self.renameRoutine, item)
478
+ item = menu.Append(wx.ID_ANY,
479
+ _translate("Paste Component\t%s") % keys[
480
+ 'pasteCompon'],
481
+ _translate(
482
+ "Paste the Component at bottom of the current "
483
+ "Routine"),
484
+ wx.ITEM_NORMAL)
485
+ self.Bind(wx.EVT_MENU, self.onPasteCompon, item)
486
+ menu.AppendSeparator()
487
+
488
+ item = menu.Append(wx.ID_ANY,
489
+ _translate("Insert Routine in Flow"),
490
+ _translate(
491
+ "Select one of your routines to be inserted"
492
+ " into the experiment flow"))
493
+ self.Bind(wx.EVT_MENU, self.flowPanel.onInsertRoutine, item)
494
+ item = menu.Append(wx.ID_ANY,
495
+ _translate("Insert Loop in Flow"),
496
+ _translate("Create a new loop in your flow window"))
497
+ self.Bind(wx.EVT_MENU, self.flowPanel.insertLoop, item)
498
+ menu.AppendSeparator()
499
+
500
+ item = menu.Append(wx.ID_ANY,
501
+ _translate("README..."),
502
+ _translate("Add or edit the text shown when your experiment is opened"))
503
+ self.Bind(wx.EVT_MENU, self.editREADME, item)
504
+
505
+ # ---_demos---#000000#FFFFFF------------------------------------------
506
+ # for demos we need a dict where the event ID will correspond to a
507
+ # filename
508
+
509
+ self.demosMenu = wx.Menu()
510
+ # unpack demos option
511
+ menu = self.demosMenu
512
+ item = menu.Append(wx.ID_ANY,
513
+ _translate("&Unpack Demos..."),
514
+ _translate(
515
+ "Unpack demos to a writable location (so that"
516
+ " they can be run)"))
517
+ self.Bind(wx.EVT_MENU, self.demosUnpack, item)
518
+ item = menu.Append(wx.ID_ANY,
519
+ _translate("Browse on Pavlovia"),
520
+ _translate("Get more demos from the online demos "
521
+ "repository on Pavlovia")
522
+ )
523
+ self.Bind(wx.EVT_MENU, self.openPavloviaDemos, item)
524
+ item = menu.Append(wx.ID_ANY,
525
+ _translate("Open demos folder"),
526
+ _translate("Open the local folder where demos are stored")
527
+ )
528
+ self.Bind(wx.EVT_MENU, self.openLocalDemos, item)
529
+ menu.AppendSeparator()
530
+ # add any demos that are found in the prefs['demosUnpacked'] folder
531
+ updateDemosMenu(self, self.demosMenu, self.prefs['unpackedDemosDir'], ext=".psyexp")
532
+ menuBar.Append(self.demosMenu, _translate('&Demos'))
533
+
534
+ # ---_onlineStudies---#000000#FFFFFF-------------------------------------------
535
+ self.pavloviaMenu = pavlovia_ui.menu.PavloviaMenu(parent=self)
536
+ menuBar.Append(self.pavloviaMenu, _translate("Pavlovia.org"))
537
+
538
+ # ---_window---#000000#FFFFFF-----------------------------------------
539
+ self.windowMenu = FrameSwitcher(self)
540
+ menuBar.Append(self.windowMenu,
541
+ _translate("Window"))
542
+
543
+ # ---_help---#000000#FFFFFF-------------------------------------------
544
+ self.helpMenu = wx.Menu()
545
+ menuBar.Append(self.helpMenu, _translate('&Help'))
546
+ menu = self.helpMenu
547
+
548
+ item = menu.Append(wx.ID_ANY,
549
+ _translate("&PsychoPy Homepage"),
550
+ _translate("Go to the PsychoPy homepage"))
551
+ self.Bind(wx.EVT_MENU, self.app.followLink, item)
552
+ self.app.urls[item.GetId()] = self.app.urls['psychopyHome']
553
+ item = menu.Append(wx.ID_ANY,
554
+ _translate("&PsychoPy Builder Help"),
555
+ _translate(
556
+ "Go to the online documentation for PsychoPy"
557
+ " Builder"))
558
+ self.Bind(wx.EVT_MENU, self.app.followLink, item)
559
+ self.app.urls[item.GetId()] = self.app.urls['builderHelp']
560
+
561
+ menu.AppendSeparator()
562
+ item = menu.Append(wx.ID_ANY,
563
+ _translate("&System Info..."),
564
+ _translate("Get system information."))
565
+ self.Bind(wx.EVT_MENU, self.app.showSystemInfo, id=item.GetId())
566
+
567
+ menu.AppendSeparator()
568
+ menu.Append(wx.ID_ABOUT, _translate(
569
+ "&About..."), _translate("About PsychoPy"))
570
+ self.Bind(wx.EVT_MENU, self.app.showAbout, id=wx.ID_ABOUT)
571
+ item = menu.Append(wx.ID_ANY,
572
+ _translate("&News..."),
573
+ _translate("News"))
574
+ self.Bind(wx.EVT_MENU, self.app.showNews, id=item.GetId())
575
+
576
+ self.SetMenuBar(menuBar)
577
+
578
+ def commandCloseFrame(self, event):
579
+ """Defines Builder Frame Closing Event"""
580
+ self.Close()
581
+
582
+ def closeFrame(self, event=None, checkSave=True):
583
+ """Defines Frame closing behavior, such as checking for file
584
+ saving"""
585
+ # close file first (check for save) but no need to update view
586
+ okToClose = self.fileClose(updateViews=False, checkSave=checkSave)
587
+
588
+ if not okToClose:
589
+ if hasattr(event, 'Veto'):
590
+ event.Veto()
591
+ return
592
+ else:
593
+ # as of wx3.0 the AUI manager needs to be uninitialised explicitly
594
+ self._mgr.UnInit()
595
+ # is it the last frame?
596
+ lastFrame = len(self.app.getAllFrames()) == 1
597
+ quitting = self.app.quitting
598
+ if lastFrame and sys.platform != 'darwin' and not quitting:
599
+ self.app.quit(event)
600
+ else:
601
+ self.app.forgetFrame(self)
602
+ self.Destroy() # required
603
+
604
+ # Show Runner if hidden
605
+ if self.app.runner is not None:
606
+ self.app.showRunner()
607
+ self.app.updateWindowMenu()
608
+
609
+ def quit(self, event=None):
610
+ """quit the app
611
+ """
612
+ self.app.quit(event)
613
+
614
+ def onResize(self, event):
615
+ """Called when the frame is resized."""
616
+ self.componentButtons.Refresh()
617
+ self.flowPanel.Refresh()
618
+ event.Skip()
619
+
620
+ @property
621
+ def filename(self):
622
+ """Name of the currently open file"""
623
+ return self._filename
624
+
625
+ @filename.setter
626
+ def filename(self, value):
627
+ self._filename = value
628
+ # Skip if there's no toolbar
629
+ if not hasattr(self, "toolbar"):
630
+ return
631
+ # Enable/disable compile buttons
632
+ if 'compile_py' in self.toolbar.buttons:
633
+ self.toolbar.EnableTool(
634
+ self.toolbar.buttons['compile_py'].GetId(),
635
+ Path(value).is_file()
636
+ )
637
+ if 'compile_js' in self.toolbar.buttons:
638
+ self.toolbar.EnableTool(
639
+ self.toolbar.buttons['compile_js'].GetId(),
640
+ Path(value).is_file()
641
+ )
642
+
643
+ def fileNew(self, event=None, closeCurrent=True):
644
+ """Create a default experiment (maybe an empty one instead)
645
+ """
646
+ # Note: this is NOT the method called by the File>New menu item.
647
+ # That calls app.newBuilderFrame() instead
648
+ if closeCurrent: # if no exp exists then don't try to close it
649
+ if not self.fileClose(updateViews=False):
650
+ # close the existing (and prompt for save if necess)
651
+ return False
652
+ self.filename = 'untitled.psyexp'
653
+ self.exp = experiment.Experiment(prefs=self.app.prefs)
654
+ defaultName = 'trial'
655
+ # create the trial routine as an example
656
+ self.exp.addRoutine(defaultName)
657
+ self.exp.flow.addRoutine(
658
+ self.exp.routines[defaultName], pos=1) # add it to flow
659
+ # add it to user's namespace
660
+ self.exp.namespace.add(defaultName, self.exp.namespace.user)
661
+ routine = self.exp.routines[defaultName]
662
+ ## add an ISI component by default
663
+ # components = self.componentButtons.components
664
+ # Static = components['StaticComponent']
665
+ # ISI = Static(self.exp, parentName=defaultName, name='ISI',
666
+ # startType='time (s)', startVal=0.0,
667
+ # stopType='duration (s)', stopVal=0.5)
668
+ # routine.addComponent(ISI)
669
+ self.resetUndoStack()
670
+ self.setIsModified(False)
671
+ self.updateAllViews()
672
+ self.app.updateWindowMenu()
673
+
674
+ def fileOpen(self, event=None, filename=None, closeCurrent=True):
675
+ """Open a FileDialog, then load the file if possible.
676
+ """
677
+ if filename is None:
678
+ # Set wildcard
679
+ if sys.platform != 'darwin':
680
+ wildcard = _translate("PsychoPy experiments (*.psyexp)|*.psyexp|Any file (*.*)|*.*")
681
+ else:
682
+ wildcard = _translate("PsychoPy experiments (*.psyexp)|*.psyexp|Any file (*.*)|*")
683
+ # get path of current file (empty if current file is '')
684
+ if self.filename:
685
+ initPath = str(Path(self.filename).parent)
686
+ else:
687
+ initPath = ""
688
+ # Open dlg
689
+ dlg = wx.FileDialog(self, message=_translate("Open file ..."),
690
+ defaultDir=initPath,
691
+ style=wx.FD_OPEN,
692
+ wildcard=wildcard)
693
+ if dlg.ShowModal() != wx.ID_OK:
694
+ return 0
695
+ filename = dlg.GetPath()
696
+
697
+ # did user try to open a script in Builder?
698
+ if filename.endswith('.py'):
699
+ self.app.showCoder() # ensures that a coder window exists
700
+ self.app.coder.setCurrentDoc(filename)
701
+ self.app.coder.setFileModified(False)
702
+ return
703
+ with WindowFrozen(ctrl=self):
704
+ # try to pause rendering until all panels updated
705
+ if closeCurrent:
706
+ if not self.fileClose(updateViews=False):
707
+ # close the existing (and prompt for save if necess)
708
+ return False
709
+ self.exp = experiment.Experiment(prefs=self.app.prefs)
710
+ try:
711
+ self.exp.loadFromXML(filename)
712
+ except Exception:
713
+ print(u"Failed to load {}. Please send the following to"
714
+ u" the PsychoPy user list".format(filename))
715
+ traceback.print_exc()
716
+ logging.flush()
717
+ self.resetUndoStack()
718
+ self.setIsModified(False)
719
+ self.filename = filename
720
+ # routinePanel.addRoutinePage() is done in
721
+ # routinePanel.redrawRoutines(), called by self.updateAllViews()
722
+ # update the views
723
+ self.updateAllViews() # if frozen effect will be visible on thaw
724
+ self.updateReadme()
725
+ self.fileHistory.AddFileToHistory(filename)
726
+ self.htmlPath = None # so we won't accidentally save to other html exp
727
+
728
+ if self.app.runner:
729
+ self.app.runner.addTask(fileName=self.filename) # Add to Runner
730
+
731
+ self.project = pavlovia.getProject(filename)
732
+ self.app.updateWindowMenu()
733
+
734
+ def fileSave(self, event=None, filename=None):
735
+ """Save file, revert to SaveAs if the file hasn't yet been saved
736
+ """
737
+ if filename is None:
738
+ filename = self.filename
739
+ if filename.startswith('untitled'):
740
+ if not self.fileSaveAs(filename):
741
+ return False # the user cancelled during saveAs
742
+ else:
743
+ filename = self.exp.saveToXML(filename)
744
+ self.fileHistory.AddFileToHistory(filename)
745
+ self.setIsModified(False)
746
+ # if export on save then we should have an html file to update
747
+ if self._getExportPref('on save') and os.path.split(filename)[0]:
748
+ self.filename = filename
749
+ self.fileExport(htmlPath=self.htmlPath)
750
+ return True
751
+
752
+ def fileSaveAs(self, event=None, filename=None):
753
+ """Defines Save File as Behavior
754
+ """
755
+ shortFilename = self.getShortFilename()
756
+ expName = self.exp.getExpName()
757
+ if (not expName) or (shortFilename == expName):
758
+ usingDefaultName = True
759
+ else:
760
+ usingDefaultName = False
761
+ if filename is None:
762
+ filename = self.filename
763
+ initPath, filename = os.path.split(filename)
764
+
765
+ if sys.platform != 'darwin':
766
+ wildcard = _translate("PsychoPy experiments (*.psyexp)|*.psyexp|Any file (*.*)|*.*")
767
+ else:
768
+ wildcard = _translate("PsychoPy experiments (*.psyexp)|*.psyexp|Any file (*.*)|*")
769
+ returnVal = False
770
+ dlg = wx.FileDialog(
771
+ self, message=_translate("Save file as ..."), defaultDir=initPath,
772
+ defaultFile=filename, style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT,
773
+ wildcard=wildcard)
774
+
775
+ if dlg.ShowModal() == wx.ID_OK:
776
+ newPath = dlg.GetPath()
777
+ # update exp name
778
+ # if user has not manually renamed experiment
779
+ if usingDefaultName:
780
+ newShortName = os.path.splitext(
781
+ os.path.split(newPath)[1])[0]
782
+ self.exp.setExpName(newShortName)
783
+ # actually save
784
+ self.fileSave(event=None, filename=newPath)
785
+ self.filename = newPath
786
+ self.project = pavlovia.getProject(filename)
787
+ returnVal = 1
788
+ dlg.Destroy()
789
+
790
+ self.updateWindowTitle()
791
+ return returnVal
792
+
793
+ def fileExport(self, event=None, htmlPath=None):
794
+ """Exports the script as an HTML file (PsychoJS library)
795
+ """
796
+ # get path if not given one
797
+ expPath, expName = os.path.split(self.filename)
798
+ if htmlPath is None:
799
+ htmlPath = self._getHtmlPath(self.filename)
800
+ if not htmlPath:
801
+ return
802
+
803
+ exportPath = os.path.join(htmlPath, expName.replace('.psyexp', '.js'))
804
+ self.generateScript(experimentPath=exportPath,
805
+ exp=self.exp,
806
+ target="PsychoJS")
807
+ # Open exported files
808
+ self.app.showCoder()
809
+ self.app.coder.fileNew(filepath=exportPath)
810
+ self.app.coder.fileReload(event=None, filename=exportPath)
811
+
812
+ def editREADME(self, event):
813
+ folder = Path(self.filename).parent
814
+ if folder == folder.parent:
815
+ dlg = wx.MessageDialog(
816
+ self,
817
+ _translate("Please save experiment before editing the README file"),
818
+ _translate("No readme file"),
819
+ wx.OK | wx.ICON_WARNING | wx.CENTRE)
820
+ dlg.ShowModal()
821
+ return
822
+ self.updateReadme()
823
+ self.showReadme()
824
+ return
825
+
826
+ def getShortFilename(self):
827
+ """returns the filename without path or extension
828
+ """
829
+ return os.path.splitext(os.path.split(self.filename)[1])[0]
830
+
831
+ # def pluginManager(self, evt=None, value=True):
832
+ # """Show the plugin manager frame."""
833
+ # PluginManagerFrame(self).ShowModal()
834
+
835
+ def updateReadme(self):
836
+ """Check whether there is a readme file in this folder and try to show
837
+ """
838
+ # create the frame if we don't have one yet
839
+ if self.readmeFrame is None:
840
+ self.readmeFrame = ReadmeFrame(parent=self)
841
+ # look for a readme file
842
+ if self.filename and self.filename != 'untitled.psyexp':
843
+ dirname = Path(self.filename).parent
844
+ possibles = list(dirname.glob('readme*'))
845
+ if len(possibles) == 0:
846
+ possibles = list(dirname.glob('Readme*'))
847
+ possibles.extend(dirname.glob('README*'))
848
+ # still haven't found a file so use default name
849
+ if len(possibles) == 0:
850
+ self.readmeFilename = str(dirname / 'readme.md') # use this as our default
851
+ else:
852
+ self.readmeFilename = str(possibles[0]) # take the first one found
853
+ else:
854
+ self.readmeFilename = None
855
+ self.readmeFrame.setFile(self.readmeFilename)
856
+ content = self.readmeFrame.ctrl.ToText()
857
+ if content and self.prefs['alwaysShowReadme']:
858
+ self.showReadme()
859
+
860
+ def showReadme(self, evt=None, value=True):
861
+ """Shows Readme file
862
+ """
863
+ if not self.readmeFrame:
864
+ self.updateReadme()
865
+ if not self.readmeFrame.IsShown():
866
+ self.readmeFrame.Show(value)
867
+
868
+ def toggleReadme(self, evt=None):
869
+ """Toggles visibility of Readme file
870
+ """
871
+ if self.readmeFrame is None:
872
+ self.showReadme()
873
+ else:
874
+ self.readmeFrame.toggleVisible()
875
+
876
+ def OnFileHistory(self, evt=None):
877
+ """get the file based on the menu ID
878
+ """
879
+ fileNum = evt.GetId() - wx.ID_FILE1
880
+ path = self.fileHistory.GetHistoryFile(fileNum)
881
+ self.fileOpen(filename=path)
882
+ # add it back to the history so it will be moved up the list
883
+ self.fileHistory.AddFileToHistory(path)
884
+
885
+ def checkSave(self):
886
+ """Check whether we need to save before quitting
887
+ """
888
+ if hasattr(self, 'isModified') and self.isModified:
889
+ self.Show(True)
890
+ self.Raise()
891
+ self.app.SetTopWindow(self)
892
+ msg = _translate('Experiment %s has changed. Save before '
893
+ 'quitting?') % self.filename
894
+ dlg = dialogs.MessageDialog(self, msg, type='Warning')
895
+ resp = dlg.ShowModal()
896
+ if resp == wx.ID_CANCEL:
897
+ return False # return, don't quit
898
+ elif resp == wx.ID_YES:
899
+ if not self.fileSave():
900
+ return False # user might cancel during save
901
+ elif resp == wx.ID_NO:
902
+ pass # don't save just quit
903
+ return True
904
+
905
+ def fileClose(self, event=None, checkSave=True, updateViews=True):
906
+ """This is typically only called when the user x
907
+ """
908
+ if checkSave:
909
+ ok = self.checkSave()
910
+ if not ok:
911
+ return False # user cancelled
912
+ if self.filename is None:
913
+ frameData = self.appData['defaultFrame']
914
+ else:
915
+ frameData = dict(self.appData['defaultFrame'])
916
+ self.appData['prevFiles'].append(self.filename)
917
+
918
+ # get size and window layout info
919
+ if self.IsIconized():
920
+ self.Iconize(False) # will return to normal mode to get size info
921
+ frameData['state'] = 'normal'
922
+ elif self.IsMaximized():
923
+ # will briefly return to normal mode to get size info
924
+ self.Maximize(False)
925
+ frameData['state'] = 'maxim'
926
+ else:
927
+ frameData['state'] = 'normal'
928
+ frameData['auiPerspective'] = self._mgr.SavePerspective()
929
+ frameData['winW'], frameData['winH'] = self.GetSize()
930
+ frameData['winX'], frameData['winY'] = self.GetPosition()
931
+
932
+ # truncate history to the recent-most last N unique files, where
933
+ # N = self.fileHistoryMaxFiles, as defined in makeMenus()
934
+ for ii in range(self.fileHistory.GetCount()):
935
+ self.appData['fileHistory'].append(
936
+ self.fileHistory.GetHistoryFile(ii))
937
+ # fileClose gets calls multiple times, so remove redundancy
938
+ # while preserving order; end of the list is recent-most:
939
+ tmp = []
940
+ fhMax = self.fileHistoryMaxFiles
941
+ for f in self.appData['fileHistory'][-3 * fhMax:]:
942
+ if f not in tmp:
943
+ tmp.append(f)
944
+ self.appData['fileHistory'] = copy.copy(tmp[-fhMax:])
945
+
946
+ # assign the data to this filename
947
+ self.appData['frames'][self.filename] = frameData
948
+ # save the display data only for those frames in the history:
949
+ tmp2 = {}
950
+ for f in self.appData['frames']:
951
+ if f in self.appData['fileHistory']:
952
+ tmp2[f] = self.appData['frames'][f]
953
+ self.appData['frames'] = copy.copy(tmp2)
954
+
955
+ # close self
956
+ self.routinePanel.removePages()
957
+ self.filename = 'untitled.psyexp'
958
+ # add the current exp as the start point for undo:
959
+ self.resetUndoStack()
960
+ if updateViews:
961
+ self.updateAllViews()
962
+ return 1
963
+
964
+ def updateAllViews(self):
965
+ """Updates Flow Panel, Routine Panel, and Window Title simultaneously
966
+ """
967
+ self.flowPanel.draw()
968
+ self.routinePanel.redrawRoutines()
969
+ self.componentButtons.Refresh()
970
+ self.updateWindowTitle()
971
+
972
+ def layoutPanes(self):
973
+ # Get panes
974
+ flowPane = self._mgr.GetPane('Flow')
975
+ compPane = self._mgr.GetPane('Components')
976
+ rtPane = self._mgr.GetPane('Routines')
977
+ # Arrange panes according to prefs
978
+ if 'FlowBottom' in self.prefs['builderLayout']:
979
+ flowPane.Bottom()
980
+ elif 'FlowTop' in self.prefs['builderLayout']:
981
+ flowPane.Top()
982
+ if 'CompRight' in self.prefs['builderLayout']:
983
+ compPane.Right()
984
+ if 'CompLeft' in self.prefs['builderLayout']:
985
+ compPane.Left()
986
+ rtPane.Center()
987
+ # Commit
988
+ self._mgr.Update()
989
+
990
+ def resetPrefs(self, event):
991
+ """Reset preferences to default"""
992
+ # Present "are you sure" dialog
993
+ dlg = wx.MessageDialog(self, _translate("Are you sure you want to reset your preferences? This cannot be undone."),
994
+ caption="Reset Preferences...", style=wx.ICON_WARNING | wx.CANCEL)
995
+ dlg.SetOKCancelLabels(
996
+ _translate("I'm sure"),
997
+ _translate("Wait, go back!")
998
+ )
999
+ if dlg.ShowModal() == wx.ID_OK:
1000
+ # If okay is pressed, remove prefs file (meaning a new one will be created on next restart)
1001
+ os.remove(prefs.paths['userPrefsFile'])
1002
+ # Show confirmation
1003
+ dlg = wx.MessageDialog(self, _translate("Done! Your preferences have been reset. Changes will be applied when you next open PsychoPy."))
1004
+ dlg.ShowModal()
1005
+ else:
1006
+ pass
1007
+
1008
+ def updateWindowTitle(self, newTitle=None):
1009
+ """Defines behavior to update window Title
1010
+ """
1011
+ if newTitle is None:
1012
+ shortName = os.path.split(self.filename)[-1]
1013
+ self.setTitle(title=self.winTitle, document=shortName)
1014
+
1015
+ def setIsModified(self, newVal=None):
1016
+ """Sets current modified status and updates save icon accordingly.
1017
+
1018
+ This method is called by the methods fileSave, undo, redo,
1019
+ addToUndoStack and it is usually preferably to call those
1020
+ than to call this directly.
1021
+
1022
+ Call with ``newVal=None``, to only update the save icon(s)
1023
+ """
1024
+ if newVal is None:
1025
+ newVal = self.getIsModified()
1026
+ else:
1027
+ self.isModified = newVal
1028
+ if hasattr(self, 'bldrBtnSave'):
1029
+ self.toolbar.EnableTool(self.bldrBtnSave.Id, newVal)
1030
+ self.fileMenu.Enable(wx.ID_SAVE, newVal)
1031
+
1032
+ def getIsModified(self):
1033
+ """Checks if changes were made"""
1034
+ return self.isModified
1035
+
1036
+ def resetUndoStack(self):
1037
+ """Reset the undo stack. do *immediately after* creating a new exp.
1038
+
1039
+ Implicitly calls addToUndoStack() using the current exp as the state
1040
+ """
1041
+ self.currentUndoLevel = 1 # 1 is current, 2 is back one setp...
1042
+ self.currentUndoStack = []
1043
+ self.addToUndoStack()
1044
+ self.updateUndoRedo()
1045
+ self.setIsModified(newVal=False) # update save icon if needed
1046
+
1047
+ def addToUndoStack(self, action="", state=None):
1048
+ """Add the given ``action`` to the currentUndoStack, associated
1049
+ with the @state@. ``state`` should be a copy of the exp
1050
+ from *immediately after* the action was taken.
1051
+ If no ``state`` is given the current state of the experiment is used.
1052
+
1053
+ If we are at end of stack already then simply append the action. If
1054
+ not (user has done an undo) then remove orphan actions and append.
1055
+ """
1056
+ if state is None:
1057
+ state = copy.deepcopy(self.exp)
1058
+ # remove actions from after the current level
1059
+ if self.currentUndoLevel > 1:
1060
+ self.currentUndoStack = self.currentUndoStack[
1061
+ :-(self.currentUndoLevel - 1)]
1062
+ self.currentUndoLevel = 1
1063
+ # append this action
1064
+ self.currentUndoStack.append({'action': action, 'state': state})
1065
+ self.setIsModified(newVal=True) # update save icon if needed
1066
+ self.updateUndoRedo()
1067
+
1068
+ def undo(self, event=None):
1069
+ """Step the exp back one level in the @currentUndoStack@ if possible,
1070
+ and update the windows.
1071
+
1072
+ Returns the final undo level (1=current, >1 for further in past)
1073
+ or -1 if redo failed (probably can't undo)
1074
+ """
1075
+ if self.currentUndoLevel >= len(self.currentUndoStack):
1076
+ return -1 # can't undo
1077
+ self.currentUndoLevel += 1
1078
+ state = self.currentUndoStack[-self.currentUndoLevel]['state']
1079
+ self.exp = copy.deepcopy(state)
1080
+ self.updateAllViews()
1081
+ self.setIsModified(newVal=True) # update save icon if needed
1082
+ self.updateUndoRedo()
1083
+
1084
+ return self.currentUndoLevel
1085
+
1086
+ def redo(self, event=None):
1087
+ """Step the exp up one level in the @currentUndoStack@ if possible,
1088
+ and update the windows.
1089
+
1090
+ Returns the final undo level (0=current, >0 for further in past)
1091
+ or -1 if redo failed (probably can't redo)
1092
+ """
1093
+ if self.currentUndoLevel <= 1:
1094
+ return -1 # can't redo, we're already at latest state
1095
+ self.currentUndoLevel -= 1
1096
+ self.exp = copy.deepcopy(
1097
+ self.currentUndoStack[-self.currentUndoLevel]['state'])
1098
+ self.updateUndoRedo()
1099
+ self.updateAllViews()
1100
+ self.setIsModified(newVal=True) # update save icon if needed
1101
+ return self.currentUndoLevel
1102
+
1103
+ def paste(self, event=None):
1104
+ """This receives paste commands for all child dialog boxes as well
1105
+ """
1106
+ foc = self.FindFocus()
1107
+ if hasattr(foc, 'Paste'):
1108
+ foc.Paste()
1109
+
1110
+ def updateUndoRedo(self):
1111
+ """Defines Undo and Redo commands for the window
1112
+ """
1113
+ undoLevel = self.currentUndoLevel
1114
+ # check undo
1115
+ if undoLevel >= len(self.currentUndoStack):
1116
+ # can't undo if we're at top of undo stack
1117
+ label = _translate("Undo\t%s") % self.app.keys['undo']
1118
+ enable = False
1119
+ else:
1120
+ action = self.currentUndoStack[-undoLevel]['action']
1121
+ txt = _translate("Undo %(action)s\t%(key)s")
1122
+ fmt = {'action': action, 'key': self.app.keys['undo']}
1123
+ label = txt % fmt
1124
+ enable = True
1125
+ self._undoLabel.SetItemLabel(label)
1126
+ if hasattr(self, 'bldrBtnUndo'):
1127
+ self.toolbar.EnableTool(self.bldrBtnUndo.Id, enable)
1128
+ self.editMenu.Enable(wx.ID_UNDO, enable)
1129
+
1130
+ # check redo
1131
+ if undoLevel == 1:
1132
+ label = _translate("Redo\t%s") % self.app.keys['redo']
1133
+ enable = False
1134
+ else:
1135
+ action = self.currentUndoStack[-undoLevel + 1]['action']
1136
+ txt = _translate("Redo %(action)s\t%(key)s")
1137
+ fmt = {'action': action, 'key': self.app.keys['redo']}
1138
+ label = txt % fmt
1139
+ enable = True
1140
+ self._redoLabel.SetItemLabel(label)
1141
+ if hasattr(self, 'bldrBtnRedo'):
1142
+ self.toolbar.EnableTool(self.bldrBtnRedo.Id, enable)
1143
+ self.editMenu.Enable(wx.ID_REDO, enable)
1144
+
1145
+ def demosUnpack(self, event=None):
1146
+ """Get a folder location from the user and unpack demos into it."""
1147
+ # choose a dir to unpack in
1148
+ dlg = wx.DirDialog(parent=self, message=_translate(
1149
+ "Location to unpack demos"))
1150
+ if dlg.ShowModal() == wx.ID_OK:
1151
+ unpackFolder = dlg.GetPath()
1152
+ else:
1153
+ return -1 # user cancelled
1154
+ # ensure it's an empty dir:
1155
+ if os.listdir(unpackFolder) != []:
1156
+ unpackFolder = os.path.join(unpackFolder, 'PsychoPy3 Demos')
1157
+ if not os.path.isdir(unpackFolder):
1158
+ os.mkdir(unpackFolder)
1159
+ mergeFolder(os.path.join(self.paths['demos'], 'builder'),
1160
+ unpackFolder)
1161
+ self.prefs['unpackedDemosDir'] = unpackFolder
1162
+ self.app.prefs.saveUserPrefs()
1163
+ updateDemosMenu(self, self.demosMenu, self.prefs['unpackedDemosDir'], ext=".psyexp")
1164
+
1165
+ def demoLoad(self, event=None):
1166
+ """Defines Demo Loading Event."""
1167
+ fileDir = self.demos[event.GetId()]
1168
+ files = glob.glob(os.path.join(fileDir, '*.psyexp'))
1169
+ if len(files) == 0:
1170
+ print("Found no psyexp files in %s" % fileDir)
1171
+ else:
1172
+ self.fileOpen(event=None, filename=files[0], closeCurrent=True)
1173
+
1174
+ def openLocalDemos(self, event=None):
1175
+ # Choose a command according to OS
1176
+ if sys.platform in ['win32']:
1177
+ comm = "explorer"
1178
+ elif sys.platform in ['darwin']:
1179
+ comm = "open"
1180
+ elif sys.platform in ['linux', 'linux2']:
1181
+ comm = "dolphin"
1182
+ # Use command to open themes folder
1183
+ subprocess.call(f"{comm} {prefs.builder['unpackedDemosDir']}", shell=True)
1184
+
1185
+ def openPavloviaDemos(self, event=None):
1186
+ webbrowser.open("https://pavlovia.org/explore")
1187
+
1188
+ def runFile(self, event=None):
1189
+ """Open Runner for running the psyexp file."""
1190
+ # Check whether file is truly untitled (not just saved as untitled)
1191
+ untitled = os.path.abspath("untitled.psyexp")
1192
+ if not os.path.exists(self.filename) or os.path.abspath(self.filename) == untitled:
1193
+ ok = self.fileSave(self.filename)
1194
+ if not ok:
1195
+ return # save file before compiling script
1196
+
1197
+ if self.getIsModified():
1198
+ ok = self.fileSave(self.filename)
1199
+ if not ok:
1200
+ return # save file before compiling script
1201
+ self.app.showRunner()
1202
+ self.stdoutFrame.addTask(fileName=self.filename)
1203
+ self.app.runner.Raise()
1204
+ if event:
1205
+ if event.Id in [self.bldrBtnRun.Id, self.bldrRun.Id]:
1206
+ self.app.runner.panel.runLocal(event)
1207
+ else:
1208
+ self.app.showRunner()
1209
+
1210
+ def onCopyRoutine(self, event=None):
1211
+ """copy the current routine from self.routinePanel
1212
+ to self.app.copiedRoutine.
1213
+ """
1214
+ r = self.routinePanel.getCurrentRoutine().copy()
1215
+ if r is not None:
1216
+ self.app.copiedRoutine = r
1217
+
1218
+ def onPasteRoutine(self, event=None):
1219
+ """Paste the current routine from self.app.copiedRoutine to a new page
1220
+ in self.routinePanel after prompting for a new name.
1221
+ """
1222
+ if self.app.copiedRoutine is None:
1223
+ return -1
1224
+ origName = self.app.copiedRoutine.name
1225
+ defaultName = self.exp.namespace.makeValid(origName)
1226
+ msg = _translate('New name for copy of "%(copied)s"? [%(default)s]')
1227
+ vals = {'copied': origName, 'default': defaultName}
1228
+ message = msg % vals
1229
+ dlg = wx.TextEntryDialog(self, message=message,
1230
+ caption=_translate('Paste Routine'))
1231
+ if dlg.ShowModal() == wx.ID_OK:
1232
+ routineName = dlg.GetValue()
1233
+ if not routineName:
1234
+ routineName = defaultName
1235
+ newRoutine = self.app.copiedRoutine.copy()
1236
+ self.pasteRoutine(newRoutine, routineName)
1237
+ dlg.Destroy()
1238
+
1239
+ def pasteRoutine(self, newRoutine, routineName):
1240
+ """
1241
+ Paste a copied Routine into the current Experiment. Returns a copy of that Routine
1242
+ """
1243
+ newRoutine.name = self.exp.namespace.makeValid(routineName, prefix="routine")
1244
+ newRoutine.params['name'] = newRoutine.name
1245
+ newRoutine.exp = self.exp
1246
+ self.exp.namespace.add(newRoutine.name)
1247
+ # add to the experiment
1248
+ self.exp.addRoutine(newRoutine.name, newRoutine)
1249
+ for newComp in newRoutine: # routine == list of components
1250
+ newName = self.exp.namespace.makeValid(newComp.params['name'])
1251
+ self.exp.namespace.add(newName)
1252
+ newComp.params['name'].val = newName
1253
+ newComp.exp = self.exp
1254
+ # could do redrawRoutines but would be slower?
1255
+ self.routinePanel.addRoutinePage(newRoutine.name, newRoutine)
1256
+ self.routinePanel.setCurrentRoutine(newRoutine)
1257
+ return newRoutine
1258
+
1259
+ def onPasteCompon(self, event=None):
1260
+ """
1261
+ Paste the copied Component (if there is one) into the current
1262
+ Routine
1263
+ """
1264
+ routinePage = self.routinePanel.getCurrentPage()
1265
+ routinePage.pasteCompon()
1266
+
1267
+ def onURL(self, evt):
1268
+ """decompose the URL of a file and line number"""
1269
+ # "C:\Program Files\wxPython...\samples\hangman\hangman.py"
1270
+ filename = evt.GetString().split('"')[1]
1271
+ lineNumber = int(evt.GetString().split(',')[1][5:])
1272
+ self.app.showCoder()
1273
+ self.app.coder.gotoLine(filename, lineNumber)
1274
+
1275
+ def setExperimentSettings(self, event=None, timeout=None):
1276
+ """Defines ability to save experiment settings
1277
+ """
1278
+ component = self.exp.settings
1279
+ # does this component have a help page?
1280
+ if hasattr(component, 'url'):
1281
+ helpUrl = component.url
1282
+ else:
1283
+ helpUrl = None
1284
+ title = '%s Properties' % self.exp.getExpName()
1285
+ dlg = DlgExperimentProperties(
1286
+ frame=self, element=component, experiment=self.exp, timeout=timeout)
1287
+
1288
+ if dlg.OK:
1289
+ self.addToUndoStack("EDIT experiment settings")
1290
+ self.setIsModified(True)
1291
+
1292
+ def addRoutine(self, event=None):
1293
+ """Defines ability to add routine in the routine panel
1294
+ """
1295
+ self.routinePanel.createNewRoutine()
1296
+
1297
+ def renameRoutine(self, name, event=None):
1298
+ """Defines ability to rename routine in the routine panel
1299
+ """
1300
+ # get notebook details
1301
+ currentRoutine = self.routinePanel.getCurrentPage()
1302
+ currentRoutineIndex = self.routinePanel.GetPageIndex(currentRoutine)
1303
+ routine = self.routinePanel.GetPage(
1304
+ self.routinePanel.GetSelection()).routine
1305
+ oldName = routine.name
1306
+ msg = _translate("What is the new name for the Routine?")
1307
+ dlg = wx.TextEntryDialog(self, message=msg, value=oldName,
1308
+ caption=_translate('Rename'))
1309
+ exp = self.exp
1310
+ if dlg.ShowModal() == wx.ID_OK:
1311
+ name = dlg.GetValue()
1312
+ # silently auto-adjust the name to be valid, and register in the
1313
+ # namespace:
1314
+ name = exp.namespace.makeValid(
1315
+ name, prefix='routine')
1316
+ if oldName in self.exp.routines:
1317
+ # Swap old with new names
1318
+ self.exp.routines[oldName].name = name
1319
+ self.exp.routines[name] = self.exp.routines.pop(oldName)
1320
+ self.exp.namespace.rename(oldName, name)
1321
+ self.routinePanel.renameRoutinePage(currentRoutineIndex, name)
1322
+ self.addToUndoStack("`RENAME Routine `%s`" % oldName)
1323
+ dlg.Destroy()
1324
+ self.flowPanel.draw()
1325
+
1326
+ def compileScript(self, event=None):
1327
+ """Defines compile script button behavior"""
1328
+ fullPath = self.filename.replace('.psyexp', '.py')
1329
+ self.generateScript(experimentPath=fullPath, exp=self.exp)
1330
+ self.app.showCoder() # make sure coder is visible
1331
+ self.app.coder.fileNew(filepath=fullPath)
1332
+ self.app.coder.fileReload(event=None, filename=fullPath)
1333
+
1334
+ @property
1335
+ def stdoutFrame(self):
1336
+ """
1337
+ Gets Experiment Runner stdout.
1338
+ """
1339
+ if not self.app.runner:
1340
+ self.app.runner = self.app.showRunner()
1341
+ return self.app.runner
1342
+
1343
+ def _getHtmlPath(self, filename):
1344
+ expPath = os.path.split(filename)[0]
1345
+ if not os.path.isdir(expPath):
1346
+ retVal = self.fileSave()
1347
+ if retVal:
1348
+ return self._getHtmlPath(self.filename)
1349
+ else:
1350
+ return False
1351
+
1352
+ htmlPath = os.path.join(expPath, self.exp.htmlFolder)
1353
+ return htmlPath
1354
+
1355
+ def _getExportPref(self, pref):
1356
+ """Returns True if pref matches exportHTML preference"""
1357
+ if pref.lower() not in [prefs.lower() for prefs in self.exp.settings.params['exportHTML'].allowedVals]:
1358
+ raise ValueError("'{}' is not an allowed value for {}".format(pref, 'exportHTML'))
1359
+ exportHtml = str(self.exp.settings.params['exportHTML'].val).lower()
1360
+ if exportHtml == pref.lower():
1361
+ return True
1362
+
1363
+ def onPavloviaSync(self, evt=None):
1364
+ self.fileSave(self.filename)
1365
+ if self._getExportPref('on sync'):
1366
+ htmlPath = self._getHtmlPath(self.filename)
1367
+ if htmlPath:
1368
+ self.fileExport(htmlPath=htmlPath)
1369
+ else:
1370
+ return
1371
+
1372
+ self.enablePavloviaButton(['pavloviaSync', 'pavloviaRun'], False)
1373
+ pavlovia_ui.syncProject(parent=self, file=self.filename, project=self.project)
1374
+ self.enablePavloviaButton(['pavloviaSync', 'pavloviaRun'], True)
1375
+
1376
+ def onPavloviaRun(self, evt=None):
1377
+ if self._getExportPref('on save') or self._getExportPref('on sync'):
1378
+ # If export on save/sync, sync now
1379
+ pavlovia_ui.syncProject(parent=self, project=self.project)
1380
+ elif self._getExportPref('manually'):
1381
+ # If set to manual, only sync if needed to create a project to run
1382
+ if self.project is None:
1383
+ pavlovia_ui.syncProject(parent=self, project=self.project)
1384
+
1385
+ if self.project is not None:
1386
+ # Make sure we have a html file to run
1387
+ if not (Path(self.project.localRoot) / 'index.html').is_file():
1388
+ self.fileExport(htmlPath=Path(self.project.localRoot) / 'index.html')
1389
+ # Update project status
1390
+ self.project.pavloviaStatus = 'ACTIVATED'
1391
+ # Run
1392
+ url = "https://pavlovia.org/run/{}".format(self.project['path_with_namespace'])
1393
+ wx.LaunchDefaultBrowser(url)
1394
+
1395
+ def enablePavloviaButton(self, buttons, enable):
1396
+ """
1397
+ Enables or disables Pavlovia buttons.
1398
+
1399
+ Parameters
1400
+ ----------
1401
+ name: string, list
1402
+ Takes single buttons 'pavloviaSync', 'pavloviaRun', 'pavloviaSearch', 'pavloviaUser',
1403
+ or multiple buttons in string 'pavloviaSync, pavloviaRun',
1404
+ or comma separated list of strings ['pavloviaSync', 'pavloviaRun', ...].
1405
+ enable: bool
1406
+ True enables and False disables the button
1407
+ """
1408
+ if isinstance(buttons, str):
1409
+ buttons = buttons.split(',')
1410
+ for button in buttons:
1411
+ self.toolbar.EnableTool(self.btnHandles[button.strip(' ')].GetId(), enable)
1412
+
1413
+ def setPavloviaUser(self, user):
1414
+ # TODO: update user icon on button to user avatar
1415
+ pass
1416
+
1417
+ def gitFeedback(self, val):
1418
+ """
1419
+ Set feedback color for the Pavlovia Sync toolbar button.
1420
+
1421
+ Parameters
1422
+ ----------
1423
+ val: int
1424
+ Status of git sync. 1 for SUCCESS (green), 0 or -1 for FAIL (RED)
1425
+ """
1426
+ feedbackTime = 1500
1427
+ colour = {0: "red", -1: "red", 1: "green"}
1428
+ toolbarSize = 32
1429
+
1430
+ # Store original
1431
+ origBtn = self.btnHandles['pavloviaSync'].NormalBitmap
1432
+ # Create new feedback bitmap
1433
+ feedbackBmp = icons.ButtonIcon(f"{colour[val]}globe.png", size=toolbarSize).bitmap
1434
+
1435
+ # Set feedback button
1436
+ self.btnHandles['pavloviaSync'].SetNormalBitmap(feedbackBmp)
1437
+ self.toolbar.Realize()
1438
+ self.toolbar.Refresh()
1439
+
1440
+ # Reset button to default state after time
1441
+ wx.CallLater(feedbackTime, self.btnHandles['pavloviaSync'].SetNormalBitmap, origBtn)
1442
+ wx.CallLater(feedbackTime + 50, self.toolbar.Realize)
1443
+ wx.CallLater(feedbackTime + 50, self.toolbar.Refresh)
1444
+
1445
+ @property
1446
+ def project(self):
1447
+ """A PavloviaProject object if one is known for this experiment
1448
+ """
1449
+ if hasattr(self, "_project"):
1450
+ return self._project
1451
+ elif self.filename:
1452
+ return pavlovia.getProject(self.filename)
1453
+ else:
1454
+ return None
1455
+
1456
+ @project.setter
1457
+ def project(self, project):
1458
+ self._project = project
1459
+
1460
+
1461
+ class RoutinesNotebook(aui.AuiNotebook, handlers.ThemeMixin):
1462
+ """A notebook that stores one or more routines
1463
+ """
1464
+
1465
+ def __init__(self, frame, id=-1):
1466
+ self.frame = frame
1467
+ self.app = frame.app
1468
+ self.routineMaxSize = 2
1469
+ self.appData = self.app.prefs.appData
1470
+ aui.AuiNotebook.__init__(self, frame, id,
1471
+ agwStyle=aui.AUI_NB_TAB_MOVE | aui.AUI_NB_CLOSE_ON_ACTIVE_TAB)
1472
+ self.Bind(aui.EVT_AUINOTEBOOK_PAGE_CLOSE, self.onClosePane)
1473
+
1474
+ # double buffered better rendering except if retina
1475
+
1476
+ self.SetDoubleBuffered(not self.frame.isRetina)
1477
+
1478
+ # This needs to be done on init, otherwise it gets an outline
1479
+ self.GetAuiManager().SetArtProvider(handlers.PsychopyDockArt())
1480
+
1481
+ if not hasattr(self.frame, 'exp'):
1482
+ return # we haven't yet added an exp
1483
+
1484
+ def getCurrentRoutine(self):
1485
+ routinePage = self.getCurrentPage()
1486
+ if routinePage:
1487
+ return routinePage.routine # no routine page
1488
+ return None
1489
+
1490
+ def setCurrentRoutine(self, routine):
1491
+ for ii in range(self.GetPageCount()):
1492
+ if routine is self.GetPage(ii).routine:
1493
+ self.SetSelection(ii)
1494
+
1495
+ def SetSelection(self, index, force=False):
1496
+ aui.AuiNotebook.SetSelection(self, index, force=force)
1497
+ self.frame.componentButtons.enableComponents(
1498
+ not isinstance(self.GetPage(index).routine, BaseStandaloneRoutine)
1499
+ )
1500
+
1501
+ def getCurrentPage(self):
1502
+ if self.GetSelection() >= 0:
1503
+ return self.GetPage(self.GetSelection())
1504
+ return None
1505
+
1506
+ def addRoutinePage(self, routineName, routine):
1507
+ # Make page
1508
+ routinePage = None
1509
+ if isinstance(routine, Routine):
1510
+ routinePage = RoutineCanvas(notebook=self, routine=routine)
1511
+ elif isinstance(routine, BaseStandaloneRoutine):
1512
+ routinePage = StandaloneRoutineCanvas(parent=self, routine=routine)
1513
+ # Add page
1514
+ if routinePage:
1515
+ self.AddPage(routinePage, routineName)
1516
+
1517
+ def renameRoutinePage(self, index, newName, ):
1518
+ self.SetPageText(index, newName)
1519
+
1520
+ def removePages(self):
1521
+ for ii in range(self.GetPageCount()):
1522
+ currId = self.GetSelection()
1523
+ self.DeletePage(currId)
1524
+
1525
+ def createNewRoutine(self, template=None):
1526
+ msg = _translate("What is the name for the new Routine? "
1527
+ "(e.g. instr, trial, feedback)")
1528
+ dlg = DlgNewRoutine(self)
1529
+ routineName = None
1530
+ if dlg.ShowModal() == wx.ID_OK:
1531
+ routineName = dlg.nameCtrl.GetValue()
1532
+ template = copy.deepcopy(dlg.selectedTemplate)
1533
+ self.frame.pasteRoutine(template, routineName)
1534
+ self.frame.addToUndoStack("NEW Routine `%s`" % routineName)
1535
+ dlg.Destroy()
1536
+ return routineName
1537
+
1538
+ def onClosePane(self, event=None):
1539
+ """Close the pane and remove the routine from the exp.
1540
+ """
1541
+ currentPage = self.GetPage(event.GetSelection())
1542
+ routine = currentPage.routine
1543
+ name = routine.name
1544
+
1545
+ # name is not valid for some reason
1546
+ if name not in self.frame.exp.routines:
1547
+ event.Skip()
1548
+ return
1549
+
1550
+ # check if the user wants a prompt
1551
+ showDlg = self.app.prefs.builder.get('confirmRoutineClose', False)
1552
+ if showDlg:
1553
+ # message to display
1554
+ msg = _translate(
1555
+ "Do you want to remove routine '{}' from the experiment?")
1556
+
1557
+ # dialog asking if the user wants to remove the routine
1558
+ dlg = wx.MessageDialog(
1559
+ self,
1560
+ _translate(msg).format(name),
1561
+ _translate('Remove routine?'),
1562
+ wx.YES_NO | wx.NO_DEFAULT | wx.CENTRE | wx.STAY_ON_TOP)
1563
+
1564
+ # show the dialog and get the response
1565
+ dlgResult = dlg.ShowModal()
1566
+ dlg.Destroy()
1567
+
1568
+ if dlgResult == wx.ID_NO: # if NO, stop the tab from closing
1569
+ event.Veto()
1570
+ return
1571
+
1572
+ # remove names of the routine and its components from namespace
1573
+ _nsp = self.frame.exp.namespace
1574
+ for c in self.frame.exp.routines[name]:
1575
+ _nsp.remove(c.params['name'].val)
1576
+ _nsp.remove(self.frame.exp.routines[name].name)
1577
+ del self.frame.exp.routines[name]
1578
+
1579
+ if routine in self.frame.exp.flow:
1580
+ self.frame.exp.flow.removeComponent(routine)
1581
+ self.frame.flowPanel.draw()
1582
+ self.frame.addToUndoStack("REMOVE Routine `%s`" % (name))
1583
+
1584
+ def increaseSize(self, event=None):
1585
+ self.appData['routineSize'] = min(
1586
+ self.routineMaxSize, self.appData['routineSize'] + 1)
1587
+ with WindowFrozen(self):
1588
+ self.redrawRoutines()
1589
+
1590
+ def decreaseSize(self, event=None):
1591
+ self.appData['routineSize'] = max(0, self.appData['routineSize'] - 1)
1592
+ with WindowFrozen(self):
1593
+ self.redrawRoutines()
1594
+
1595
+ def redrawRoutines(self):
1596
+ """Removes all the routines, adds them back (alphabetical order),
1597
+ sets current back to orig
1598
+ """
1599
+ currPage = self.GetSelection()
1600
+ self.removePages()
1601
+ displayOrder = sorted(self.frame.exp.routines.keys()) # alphabetical
1602
+ for routineName in displayOrder:
1603
+ if isinstance(self.frame.exp.routines[routineName], (Routine, BaseStandaloneRoutine)):
1604
+ self.addRoutinePage(
1605
+ routineName, self.frame.exp.routines[routineName])
1606
+ if currPage > -1:
1607
+ self.SetSelection(currPage)
1608
+
1609
+
1610
+ class RoutineCanvas(wx.ScrolledWindow, handlers.ThemeMixin):
1611
+ """Represents a single routine (used as page in RoutinesNotebook)"""
1612
+
1613
+ def __init__(self, notebook, id=wx.ID_ANY, routine=None):
1614
+ """This window is based heavily on the PseudoDC demo of wxPython
1615
+ """
1616
+ wx.ScrolledWindow.__init__(
1617
+ self, notebook, id, (0, 0), style=wx.BORDER_NONE | wx.VSCROLL)
1618
+
1619
+ self.frame = notebook.frame
1620
+ self.app = self.frame.app
1621
+ self.dpi = self.app.dpi
1622
+ self.lines = []
1623
+ self.maxWidth = self.GetSize().GetWidth()
1624
+ self.maxHeight = 15 * self.dpi
1625
+ self.x = self.y = 0
1626
+ self.curLine = []
1627
+ self.drawing = False
1628
+ self.drawSize = self.app.prefs.appData['routineSize']
1629
+ # auto-rescale based on number of components and window size is jumpy
1630
+ # when switch between routines of diff drawing sizes
1631
+ self.iconSize = (24, 24, 48)[self.drawSize] # only 24, 48 so far
1632
+ self.fontBaseSize = (1100, 1200, 1300)[self.drawSize] # depends on OS?
1633
+ #self.scroller = PsychopyScrollbar(self, wx.VERTICAL)
1634
+ self.SetVirtualSize((self.maxWidth, self.maxHeight))
1635
+ self.SetScrollRate(self.dpi / 4, self.dpi / 4)
1636
+
1637
+ self.routine = routine
1638
+ self.yPositions = None
1639
+ self.yPosTop = (25, 40, 60)[self.drawSize]
1640
+ # the step in Y between each component
1641
+ self.componentStep = (25, 32, 50)[self.drawSize]
1642
+ self.timeXposStart = (150, 150, 200)[self.drawSize]
1643
+ # the left hand edge of the icons:
1644
+ _scale = (1.3, 1.5, 1.5)[self.drawSize]
1645
+ self.iconXpos = self.timeXposStart - self.iconSize * _scale
1646
+ self.timeXposEnd = self.timeXposStart + 400 # onResize() overrides
1647
+
1648
+ # create a PseudoDC to record our drawing
1649
+ self.pdc = PseudoDC()
1650
+ self.pen_cache = {}
1651
+ self.brush_cache = {}
1652
+ # vars for handling mouse clicks
1653
+ self.dragid = -1
1654
+ self.lastpos = (0, 0)
1655
+ # use the ID of the drawn icon to retrieve component name:
1656
+ self.componentFromID = {}
1657
+ self.contextMenuItems = [
1658
+ 'copy', 'paste above', 'paste below', 'edit', 'remove',
1659
+ 'move to top', 'move up', 'move down', 'move to bottom']
1660
+ # labels are only for display, and allow localization
1661
+ self.contextMenuLabels = {k: _localized[k]
1662
+ for k in self.contextMenuItems}
1663
+ self.contextItemFromID = {}
1664
+ self.contextIDFromItem = {}
1665
+ for item in self.contextMenuItems:
1666
+ id = wx.NewIdRef()
1667
+ self.contextItemFromID[id] = item
1668
+ self.contextIDFromItem[item] = id
1669
+
1670
+ self.Bind(wx.EVT_PAINT, self.OnPaint)
1671
+ self.Bind(wx.EVT_ERASE_BACKGROUND, lambda x: None)
1672
+ self.Bind(wx.EVT_MOUSE_EVENTS, self.OnMouse)
1673
+ self.Bind(wx.EVT_MOUSEWHEEL, self.OnScroll)
1674
+ self.Bind(wx.EVT_SIZE, self.onResize)
1675
+ # crashes if drop on OSX:
1676
+ # self.SetDropTarget(FileDropTarget(builder = self.frame))
1677
+
1678
+ def _applyAppTheme(self, target=None):
1679
+ """Synonymise app theme method with redraw method"""
1680
+ return self.redrawRoutine()
1681
+
1682
+ def onResize(self, event):
1683
+ self.sizePix = event.GetSize()
1684
+ self.timeXposStart = (150, 150, 200)[self.drawSize]
1685
+ self.timeXposEnd = self.sizePix[0] - (60, 80, 100)[self.drawSize]
1686
+ self.redrawRoutine() # then redraw visible
1687
+
1688
+ def ConvertEventCoords(self, event):
1689
+ xView, yView = self.GetViewStart()
1690
+ xDelta, yDelta = self.GetScrollPixelsPerUnit()
1691
+ return (event.GetX() + (xView * xDelta),
1692
+ event.GetY() + (yView * yDelta))
1693
+
1694
+ def OffsetRect(self, r):
1695
+ """Offset the rectangle, r, to appear in the given pos in the window
1696
+ """
1697
+ xView, yView = self.GetViewStart()
1698
+ xDelta, yDelta = self.GetScrollPixelsPerUnit()
1699
+ r.OffsetXY(-(xView * xDelta), -(yView * yDelta))
1700
+
1701
+ def OnMouse(self, event):
1702
+ if event.LeftDown():
1703
+ x, y = self.ConvertEventCoords(event)
1704
+ icons = self.pdc.FindObjectsByBBox(x, y)
1705
+ if len(icons):
1706
+ self.editComponentProperties(
1707
+ component=self.componentFromID[icons[0]])
1708
+ elif event.RightDown():
1709
+ x, y = self.ConvertEventCoords(event)
1710
+ icons = self.pdc.FindObjectsByBBox(x, y)
1711
+ menuPos = event.GetPosition()
1712
+ if 'flowTop' in self.app.prefs.builder['builderLayout']:
1713
+ # width of components panel
1714
+ menuPos[0] += self.frame.componentButtons.GetSize()[0]
1715
+ # height of flow panel
1716
+ menuPos[1] += self.frame.flowPanel.GetSize()[1]
1717
+ if len(icons):
1718
+ self._menuComponent = self.componentFromID[icons[0]]
1719
+ self.showContextMenu(self._menuComponent, xy=menuPos)
1720
+ else: # no context
1721
+ self.showContextMenu(None, xy=menuPos)
1722
+
1723
+ elif event.Dragging() or event.LeftUp():
1724
+ if self.dragid != -1:
1725
+ pass
1726
+ if event.LeftUp():
1727
+ pass
1728
+ elif event.Moving():
1729
+ try:
1730
+ x, y = self.ConvertEventCoords(event)
1731
+ id = self.pdc.FindObjectsByBBox(x, y)[0]
1732
+ component = self.componentFromID[id]
1733
+ self.frame.SetStatusText("Component: "+component.params['name'].val)
1734
+ except IndexError:
1735
+ self.frame.SetStatusText("")
1736
+
1737
+ def OnScroll(self, event):
1738
+ xy = self.GetViewStart()
1739
+ multiplier = self.dpi / 1600
1740
+ self.Scroll(xy[0], xy[1] - event.WheelRotation*multiplier)
1741
+
1742
+ def showContextMenu(self, component, xy):
1743
+ """Show a context menu in the routine view.
1744
+ """
1745
+ menu = wx.Menu()
1746
+ if component is not None:
1747
+ for item in self.contextMenuItems:
1748
+ id = self.contextIDFromItem[item]
1749
+ # don't show paste option unless something is copied
1750
+ if item.startswith('paste'):
1751
+ if not self.app.copiedCompon: # skip paste options
1752
+ continue
1753
+ itemLabel = " ".join(
1754
+ (self.contextMenuLabels[item],
1755
+ "({})".format(
1756
+ self.app.copiedCompon.params['name'].val)))
1757
+ elif any([item.startswith(op) for op in ('copy', 'remove', 'edit')]):
1758
+ itemLabel = " ".join(
1759
+ (self.contextMenuLabels[item],
1760
+ "({})".format(component.params['name'].val)))
1761
+ else:
1762
+ itemLabel = self.contextMenuLabels[item]
1763
+
1764
+ menu.Append(id, itemLabel)
1765
+ menu.Bind(wx.EVT_MENU, self.onContextSelect, id=id)
1766
+
1767
+ self.frame.PopupMenu(menu, xy)
1768
+ menu.Destroy() # destroy to avoid mem leak
1769
+ else:
1770
+ # anywhere but a hotspot is clicked, show this menu
1771
+ if self.app.copiedCompon:
1772
+ itemLabel = " ".join(
1773
+ (_translate('paste'),
1774
+ "({})".format(
1775
+ self.app.copiedCompon.params['name'].val)))
1776
+ menu.Append(wx.ID_ANY, itemLabel)
1777
+ menu.Bind(wx.EVT_MENU, self.pasteCompon, id=wx.ID_ANY)
1778
+
1779
+ self.frame.PopupMenu(menu, xy)
1780
+ menu.Destroy()
1781
+
1782
+ def onContextSelect(self, event):
1783
+ """Perform a given action on the component chosen
1784
+ """
1785
+ op = self.contextItemFromID[event.GetId()]
1786
+ component = self._menuComponent
1787
+ r = self.routine
1788
+ if op == 'edit':
1789
+ self.editComponentProperties(component=component)
1790
+ elif op == 'copy':
1791
+ self.copyCompon(component=component)
1792
+ elif op == 'paste above':
1793
+ self.pasteCompon(index=r.index(component))
1794
+ elif op == 'paste below':
1795
+ self.pasteCompon(index=r.index(component) + 1)
1796
+ elif op == 'remove':
1797
+ r.removeComponent(component)
1798
+ self.frame.addToUndoStack(
1799
+ "REMOVE `%s` from Routine" % component.params['name'].val)
1800
+ self.frame.exp.namespace.remove(component.params['name'].val)
1801
+ elif op.startswith('move'):
1802
+ lastLoc = r.index(component)
1803
+ r.remove(component)
1804
+ if op == 'move to top':
1805
+ r.insert(0, component)
1806
+ if op == 'move up':
1807
+ r.insert(lastLoc - 1, component)
1808
+ if op == 'move down':
1809
+ r.insert(lastLoc + 1, component)
1810
+ if op == 'move to bottom':
1811
+ r.append(component)
1812
+ self.frame.addToUndoStack("MOVED `%s`" %
1813
+ component.params['name'].val)
1814
+ self.redrawRoutine()
1815
+ self._menuComponent = None
1816
+
1817
+ def OnPaint(self, event):
1818
+ # Create a buffered paint DC. It will create the real
1819
+ # wx.PaintDC and then blit the bitmap to it when dc is
1820
+ # deleted.
1821
+ dc = wx.GCDC(wx.BufferedPaintDC(self))
1822
+ # we need to clear the dc BEFORE calling PrepareDC
1823
+ bg = wx.Brush(self.GetBackgroundColour())
1824
+ dc.SetBackground(bg)
1825
+ dc.Clear()
1826
+ # use PrepareDC to set position correctly
1827
+ self.PrepareDC(dc)
1828
+ # create a clipping rect from our position and size
1829
+ # and the Update Region
1830
+ xv, yv = self.GetViewStart()
1831
+ dx, dy = self.GetScrollPixelsPerUnit()
1832
+ x, y = (xv * dx, yv * dy)
1833
+ rgn = self.GetUpdateRegion()
1834
+ rgn.Offset(x, y)
1835
+ r = rgn.GetBox()
1836
+ # draw to the dc using the calculated clipping rect
1837
+ self.pdc.DrawToDCClipped(dc, r)
1838
+
1839
+ def redrawRoutine(self):
1840
+ self.pdc.Clear() # clear the screen
1841
+ self.pdc.RemoveAll() # clear all objects (icon buttons)
1842
+
1843
+ self.SetBackgroundColour(colors.app['tab_bg'])
1844
+ # work out where the component names and icons should be from name
1845
+ # lengths
1846
+ self.setFontSize(self.fontBaseSize // self.dpi, self.pdc)
1847
+ longest = 0
1848
+ w = 50
1849
+ for comp in self.routine:
1850
+ name = comp.params['name'].val
1851
+ if len(name) > longest:
1852
+ longest = len(name)
1853
+ w = self.GetFullTextExtent(name)[0]
1854
+ self.timeXpos = w + (50, 50, 90)[self.drawSize]
1855
+
1856
+ # separate components according to whether they are drawn in separate
1857
+ # row
1858
+ rowComponents = []
1859
+ staticCompons = []
1860
+ for n, component in enumerate(self.routine):
1861
+ if component.type == 'Static':
1862
+ staticCompons.append(component)
1863
+ else:
1864
+ rowComponents.append(component)
1865
+
1866
+ # draw static, time grid, normal (row) comp:
1867
+ yPos = self.yPosTop
1868
+ yPosBottom = yPos + len(rowComponents) * self.componentStep
1869
+ # draw any Static Components first (below the grid)
1870
+ for component in staticCompons:
1871
+ bottom = max(yPosBottom, self.GetSize()[1])
1872
+ self.drawStatic(self.pdc, component, yPos, bottom)
1873
+ self.drawTimeGrid(self.pdc, yPos, yPosBottom)
1874
+ # normal components, one per row
1875
+ for component in rowComponents:
1876
+ self.drawComponent(self.pdc, component, yPos)
1877
+ yPos += self.componentStep
1878
+
1879
+ # the 50 allows space for labels below the time axis
1880
+ self.SetVirtualSize((self.maxWidth, yPos + 50))
1881
+ self.Refresh() # refresh the visible window after drawing (OnPaint)
1882
+ #self.scroller.Resize()
1883
+
1884
+ def getMaxTime(self):
1885
+ """Return the max time to be drawn in the window
1886
+ """
1887
+ maxTime, nonSlip = self.routine.getMaxTime()
1888
+ if self.routine.hasOnlyStaticComp():
1889
+ maxTime = int(maxTime) + 1.0
1890
+ return maxTime
1891
+
1892
+ def drawTimeGrid(self, dc, yPosTop, yPosBottom, labelAbove=True):
1893
+ """Draws the grid of lines and labels the time axes
1894
+ """
1895
+ tMax = self.getMaxTime() * 1.1
1896
+ xScale = self.getSecsPerPixel()
1897
+ xSt = self.timeXposStart
1898
+ xEnd = self.timeXposEnd
1899
+
1900
+ # dc.SetId(wx.NewIdRef())
1901
+ dc.SetPen(wx.Pen(colors.app['rt_timegrid']))
1902
+ dc.SetTextForeground(wx.Colour(colors.app['rt_timegrid']))
1903
+ # draw horizontal lines on top and bottom
1904
+ dc.DrawLine(x1=xSt, y1=yPosTop,
1905
+ x2=xEnd, y2=yPosTop)
1906
+ dc.DrawLine(x1=xSt, y1=yPosBottom,
1907
+ x2=xEnd, y2=yPosBottom)
1908
+ # draw vertical time points
1909
+ # gives roughly 1/10 the width, but in rounded to base 10 of
1910
+ # 0.1,1,10...
1911
+ unitSize = 10 ** numpy.ceil(numpy.log10(tMax * 0.8)) / 10.0
1912
+ if tMax / unitSize < 3:
1913
+ # gives units of 2 (0.2,2,20)
1914
+ unitSize = 10 ** numpy.ceil(numpy.log10(tMax * 0.8)) / 50.0
1915
+ elif tMax / unitSize < 6:
1916
+ # gives units of 5 (0.5,5,50)
1917
+ unitSize = 10 ** numpy.ceil(numpy.log10(tMax * 0.8)) / 20.0
1918
+ for lineN in range(int(numpy.floor((tMax / unitSize)))):
1919
+ # vertical line:
1920
+ dc.DrawLine(xSt + lineN * unitSize / xScale, yPosTop - 4,
1921
+ xSt + lineN * unitSize / xScale, yPosBottom + 4)
1922
+ # label above:
1923
+ dc.DrawText('%.2g' % (lineN * unitSize), xSt + lineN *
1924
+ unitSize / xScale - 4, yPosTop - 30)
1925
+ if yPosBottom > 300:
1926
+ # if bottom of grid is far away then draw labels here too
1927
+ dc.DrawText('%.2g' % (lineN * unitSize), xSt + lineN *
1928
+ unitSize / xScale - 4, yPosBottom + 10)
1929
+ # add a label
1930
+ self.setFontSize(self.fontBaseSize // self.dpi, dc)
1931
+ # y is y-half height of text
1932
+ dc.DrawText('t (sec)', xEnd + 5,
1933
+ yPosTop - self.GetFullTextExtent('t')[1] / 2.0)
1934
+ # or draw bottom labels only if scrolling is turned on, virtual size >
1935
+ # available size?
1936
+ if yPosBottom > 300:
1937
+ # if bottom of grid is far away then draw labels there too
1938
+ # y is y-half height of text
1939
+ dc.DrawText('t (sec)', xEnd + 5,
1940
+ yPosBottom - self.GetFullTextExtent('t')[1] / 2.0)
1941
+ dc.SetTextForeground(colors.app['text'])
1942
+
1943
+ def setFontSize(self, size, dc):
1944
+ font = self.GetFont()
1945
+ font.SetPointSize(size)
1946
+ dc.SetFont(font)
1947
+ self.SetFont(font)
1948
+
1949
+ def drawStatic(self, dc, component, yPosTop, yPosBottom):
1950
+ """draw a static (ISI) component box"""
1951
+ # set an id for the region of this component (so it can
1952
+ # act as a button). see if we created this already.
1953
+ id = None
1954
+ for key in self.componentFromID:
1955
+ if self.componentFromID[key] == component:
1956
+ id = key
1957
+ if not id: # then create one and add to the dict
1958
+ id = wx.NewIdRef()
1959
+ self.componentFromID[id] = component
1960
+ dc.SetId(id)
1961
+ # deduce start and stop times if possible
1962
+ startTime, duration, nonSlipSafe = component.getStartAndDuration()
1963
+ # ensure static comps are clickable (even if $code start or duration)
1964
+ unknownTiming = False
1965
+ if startTime is None:
1966
+ startTime = 0
1967
+ unknownTiming = True
1968
+ if duration is None:
1969
+ duration = 0 # minimal extent ensured below
1970
+ unknownTiming = True
1971
+ # calculate rectangle for component
1972
+ xScale = self.getSecsPerPixel()
1973
+
1974
+ if component.params['disabled'].val:
1975
+ dc.SetBrush(wx.Brush(colors.app['rt_static_disabled']))
1976
+ dc.SetPen(wx.Pen(colors.app['rt_static_disabled']))
1977
+
1978
+ else:
1979
+ dc.SetBrush(wx.Brush(colors.app['rt_static']))
1980
+ dc.SetPen(wx.Pen(colors.app['rt_static']))
1981
+
1982
+ xSt = self.timeXposStart + startTime // xScale
1983
+ w = duration // xScale + 1 # +1 b/c border alpha=0 in dc.SetPen
1984
+ w = max(min(w, 10000), 2) # ensure 2..10000 pixels
1985
+ h = yPosBottom - yPosTop
1986
+ # name label, position:
1987
+ name = component.params['name'].val # "ISI"
1988
+ if unknownTiming:
1989
+ # flag it as not literally represented in time, e.g., $code
1990
+ # duration
1991
+ name += ' ???'
1992
+ nameW, nameH = self.GetFullTextExtent(name)[0:2]
1993
+ x = xSt + w // 2
1994
+ staticLabelTop = (0, 50, 60)[self.drawSize]
1995
+ y = staticLabelTop - nameH * 3
1996
+ fullRect = wx.Rect(x - 20, y, nameW, nameH)
1997
+ # draw the rectangle, draw text on top:
1998
+ dc.DrawRectangle(xSt, yPosTop - nameH * 4, w, h + nameH * 5)
1999
+ dc.DrawText(name, x - nameW // 2, y)
2000
+ # update bounds to include time bar
2001
+ fullRect.Union(wx.Rect(xSt, yPosTop, w, h))
2002
+ dc.SetIdBounds(id, fullRect)
2003
+
2004
+ def drawComponent(self, dc, component, yPos):
2005
+ """Draw the timing of one component on the timeline"""
2006
+ # set an id for the region of this component (so it
2007
+ # can act as a button). see if we created this already
2008
+ id = None
2009
+ for key in self.componentFromID:
2010
+ if self.componentFromID[key] == component:
2011
+ id = key
2012
+ if not id: # then create one and add to the dict
2013
+ id = wx.NewIdRef()
2014
+ self.componentFromID[id] = component
2015
+ dc.SetId(id)
2016
+
2017
+ iconYOffset = (6, 6, 0)[self.drawSize]
2018
+ # get default icon and bar color
2019
+ thisIcon = icons.ComponentIcon(component, size=self.iconSize).bitmap
2020
+ thisColor = colors.app['rt_comp']
2021
+ thisStyle = wx.BRUSHSTYLE_SOLID
2022
+
2023
+ # check True/False on ForceEndRoutine
2024
+ if 'forceEndRoutine' in component.params:
2025
+ if component.params['forceEndRoutine'].val:
2026
+ thisColor = colors.app['rt_comp_force']
2027
+ # check True/False on ForceEndRoutineOnPress
2028
+ if 'forceEndRoutineOnPress' in component.params:
2029
+ <<<<<<< HEAD
2030
+ if component.params['forceEndRoutineOnPress'].val:
2031
+ thisColor = colors.app['rt_comp_force']
2032
+ =======
2033
+ if component.params['forceEndRoutineOnPress'].val in ['any click', 'valid click']:
2034
+ thisColor = ThemeMixin.appColors['rt_comp_force']
2035
+ >>>>>>> e32726e790e8485ee63b1419cb6e567f086836b9
2036
+ # check True aliases on EndRoutineOn
2037
+ if 'endRoutineOn' in component.params:
2038
+ if component.params['endRoutineOn'].val in ['look at', 'look away']:
2039
+ thisColor = colors.app['rt_comp_force']
2040
+ # grey bar if comp is disabled
2041
+ if component.params['disabled'].val:
2042
+ thisIcon = thisIcon.ConvertToDisabled()
2043
+ thisColor = colors.app['rt_comp_disabled']
2044
+
2045
+ dc.DrawBitmap(thisIcon, self.iconXpos, yPos + iconYOffset, True)
2046
+ fullRect = wx.Rect(self.iconXpos, yPos,
2047
+ thisIcon.GetWidth(), thisIcon.GetHeight())
2048
+
2049
+ self.setFontSize(self.fontBaseSize // self.dpi, dc)
2050
+
2051
+ name = component.params['name'].val
2052
+ # get size based on text
2053
+ w, h = self.GetFullTextExtent(name)[0:2]
2054
+ if w > self.iconXpos - self.dpi/5:
2055
+ # If width is greater than space available, split word at point calculated by average letter width
2056
+ maxLen = int(
2057
+ (self.iconXpos - self.GetFullTextExtent("...")[0] - self.dpi/5)
2058
+ / (w/len(name))
2059
+ )
2060
+ splitAt = int(maxLen/2)
2061
+ name = name[:splitAt] + "..." + name[-splitAt:]
2062
+ w = self.iconXpos - self.dpi/5
2063
+ # draw text
2064
+ # + x position of icon (left side)
2065
+ # - half width of icon (including whitespace around it)
2066
+ # - FULL width of text
2067
+ # + slight adjustment for whitespace
2068
+ x = self.iconXpos - thisIcon.GetWidth()/2 - w + thisIcon.GetWidth()/3
2069
+ _adjust = (5, 5, -2)[self.drawSize]
2070
+ y = yPos + thisIcon.GetHeight() // 2 - h // 2 + _adjust
2071
+ dc.DrawText(name, x, y)
2072
+ fullRect.Union(wx.Rect(x - 20, y, w, h))
2073
+
2074
+ # deduce start and stop times if possible
2075
+ startTime, duration, nonSlipSafe = component.getStartAndDuration()
2076
+ # draw entries on timeline (if they have some time definition)
2077
+ if duration is not None:
2078
+ # then we can draw a sensible time bar!
2079
+ thisPen = wx.Pen(thisColor, style=wx.TRANSPARENT)
2080
+ thisBrush = wx.Brush(thisColor, style=thisStyle)
2081
+ dc.SetPen(thisPen)
2082
+ dc.SetBrush(thisBrush)
2083
+ # If there's a fixed end time and no start time, start 20px before 0
2084
+ if ('stopType' in component.params) and ('startType' in component.params) and (
2085
+ component.params['stopType'].val in ('time (s)', 'duration (s)')
2086
+ and component.params['startType'].val in ('time (s)')
2087
+ and startTime is None
2088
+ ):
2089
+ startTime = -20 * self.getSecsPerPixel()
2090
+ duration += 20 * self.getSecsPerPixel()
2091
+ # thisBrush.SetStyle(wx.BRUSHSTYLE_BDIAGONAL_HATCH)
2092
+ # dc.SetBrush(thisBrush)
2093
+
2094
+ if startTime is not None:
2095
+ xScale = self.getSecsPerPixel()
2096
+ yOffset = (3.5, 3.5, 0.5)[self.drawSize]
2097
+ h = self.componentStep // (4, 3.25, 2.5)[self.drawSize]
2098
+ xSt = self.timeXposStart + startTime // xScale
2099
+ w = duration // xScale + 1
2100
+ if w > 10000:
2101
+ w = 10000 # limit width to 10000 pixels!
2102
+ if w < 2:
2103
+ w = 2 # make sure at least one pixel shows
2104
+ dc.DrawRectangle(xSt, y + yOffset, w, h)
2105
+ # update bounds to include time bar
2106
+ fullRect.Union(wx.Rect(xSt, y + yOffset, w, h))
2107
+ dc.SetIdBounds(id, fullRect)
2108
+
2109
+ def copyCompon(self, event=None, component=None):
2110
+ """This is easy - just take a copy of the component into memory
2111
+ """
2112
+ self.app.copiedCompon = copy.deepcopy(component)
2113
+
2114
+ def pasteCompon(self, event=None, component=None, index=-1):
2115
+ if not self.app.copiedCompon:
2116
+ return -1 # not possible to paste if nothing copied
2117
+ exp = self.frame.exp
2118
+ origName = self.app.copiedCompon.params['name'].val
2119
+ defaultName = exp.namespace.makeValid(origName)
2120
+ msg = _translate('New name for copy of "%(copied)s"? [%(default)s]')
2121
+ vals = {'copied': origName, 'default': defaultName}
2122
+ message = msg % vals
2123
+ dlg = wx.TextEntryDialog(self, message=message,
2124
+ caption=_translate('Paste Component'))
2125
+ if dlg.ShowModal() == wx.ID_OK:
2126
+ newName = dlg.GetValue()
2127
+ newCompon = copy.deepcopy(self.app.copiedCompon)
2128
+ if not newName:
2129
+ newName = defaultName
2130
+ newName = exp.namespace.makeValid(newName)
2131
+ newCompon.params['name'].val = newName
2132
+ if 'name' in dir(newCompon):
2133
+ newCompon.name = newName
2134
+ self.routine.insertComponent(index, newCompon)
2135
+ self.frame.exp.namespace.user.append(newName)
2136
+ # could do redrawRoutines but would be slower?
2137
+ self.redrawRoutine()
2138
+ self.frame.addToUndoStack("PASTE Component `%s`" % newName)
2139
+ dlg.Destroy()
2140
+
2141
+ def editComponentProperties(self, event=None, component=None):
2142
+ # we got here from a wx.button press (rather than our own drawn icons)
2143
+ if event:
2144
+ componentName = event.EventObject.GetName()
2145
+ component = self.routine.getComponentFromName(componentName)
2146
+ # does this component have a help page?
2147
+ if hasattr(component, 'url'):
2148
+ helpUrl = component.url
2149
+ else:
2150
+ helpUrl = None
2151
+ old_name = component.params['name'].val
2152
+ old_disabled = component.params['disabled'].val
2153
+ # check current timing settings of component (if it changes we
2154
+ # need to update views)
2155
+ initialTimings = component.getStartAndDuration()
2156
+ if 'forceEndRoutine' in component.params \
2157
+ or 'forceEndRoutineOnPress' in component.params:
2158
+ # If component can force end routine, check if it did before
2159
+ initialForce = [component.params[key].val
2160
+ for key in ['forceEndRoutine', 'forceEndRoutineOnPress']
2161
+ if key in component.params]
2162
+ else:
2163
+ initialForce = False
2164
+ # create the dialog
2165
+ if hasattr(component, 'type') and component.type.lower() == 'code':
2166
+ _Dlg = DlgCodeComponentProperties
2167
+ else:
2168
+ _Dlg = DlgComponentProperties
2169
+ dlg = _Dlg(frame=self.frame,
2170
+ element=component,
2171
+ experiment=self.frame.exp, editing=True)
2172
+ if dlg.OK:
2173
+ # Redraw if force end routine has changed
2174
+ if any(key in component.params for key in ['forceEndRoutine', 'forceEndRoutineOnPress', 'endRoutineOn']):
2175
+ newForce = [component.params[key].val
2176
+ for key in ['forceEndRoutine', 'forceEndRoutineOnPress', 'endRoutineOn']
2177
+ if key in component.params]
2178
+ if initialForce != newForce:
2179
+ self.redrawRoutine() # need to refresh timings section
2180
+ self.Refresh() # then redraw visible
2181
+ self.frame.flowPanel.draw()
2182
+ # Redraw if timings have changed
2183
+ if component.getStartAndDuration() != initialTimings:
2184
+ self.redrawRoutine() # need to refresh timings section
2185
+ self.Refresh() # then redraw visible
2186
+ self.frame.flowPanel.draw()
2187
+ # self.frame.flowPanel.Refresh()
2188
+ elif component.params['name'].val != old_name:
2189
+ self.redrawRoutine() # need to refresh name
2190
+ elif component.params['disabled'].val != old_disabled:
2191
+ self.redrawRoutine() # need to refresh color
2192
+ self.frame.exp.namespace.remove(old_name)
2193
+ self.frame.exp.namespace.add(component.params['name'].val)
2194
+ self.frame.addToUndoStack("EDIT `%s`" %
2195
+ component.params['name'].val)
2196
+
2197
+ def getSecsPerPixel(self):
2198
+ pixels = float(self.timeXposEnd - self.timeXposStart)
2199
+ return self.getMaxTime() / pixels
2200
+
2201
+
2202
+ class StandaloneRoutineCanvas(scrolledpanel.ScrolledPanel, handlers.ThemeMixin):
2203
+ def __init__(self, parent, routine=None):
2204
+ # Init super
2205
+ scrolledpanel.ScrolledPanel.__init__(
2206
+ self, parent,
2207
+ style=wx.BORDER_NONE)
2208
+ # Store basics
2209
+ self.frame = parent.frame
2210
+ self.app = self.frame.app
2211
+ self.dpi = self.app.dpi
2212
+ self.routine = routine
2213
+ self.params = routine.params
2214
+ # Setup sizer
2215
+ self.sizer = wx.BoxSizer(wx.VERTICAL)
2216
+ self.SetSizer(self.sizer)
2217
+ # Setup categ notebook
2218
+ self.ctrls = ParamNotebook(self, experiment=self.frame.exp, element=routine)
2219
+ self.paramCtrls = self.ctrls.paramCtrls
2220
+ self.sizer.Add(self.ctrls, border=12, proportion=1, flag=wx.ALIGN_CENTER | wx.ALL)
2221
+ # Make buttons
2222
+ self.btnsSizer = wx.BoxSizer(wx.HORIZONTAL)
2223
+ # Add validator stuff
2224
+ self.warnings = WarningManager(self)
2225
+ self.sizer.Add(self.warnings.output, border=3, flag=wx.EXPAND | wx.ALL)
2226
+ # Add buttons to sizer
2227
+ self.sizer.Add(self.btnsSizer, border=6, proportion=0, flag=wx.ALIGN_RIGHT | wx.ALL)
2228
+ # Style
2229
+ self._applyAppTheme()
2230
+ self.SetupScrolling(scroll_y=True)
2231
+
2232
+ def updateExperiment(self, evt=None):
2233
+ """Update this routine's saved parameters to what is currently entered"""
2234
+ # Get params in correct formats
2235
+ self.routine.params = self.ctrls.getParams()
2236
+ # Duplicate routine list and iterate through to find this one
2237
+ routines = self.frame.exp.routines.copy()
2238
+ for name, routine in routines.items():
2239
+ if routine == self.routine:
2240
+ # Update the routine dict keys to use the current name for this routine
2241
+ self.frame.exp.routines[self.routine.params['name'].val] = self.frame.exp.routines.pop(name)
2242
+ # Redraw the flow panel
2243
+ self.frame.flowPanel.draw()
2244
+ # Rename this page
2245
+ page = self.frame.routinePanel.GetPageIndex(self)
2246
+ self.frame.routinePanel.SetPageText(page, self.routine.params['name'].val)
2247
+ # Update save button
2248
+ self.frame.setIsModified(True)
2249
+
2250
+ def Validate(self, *args, **kwargs):
2251
+ return self.ctrls.Validate()
2252
+
2253
+
2254
+ class ComponentsPanel(scrolledpanel.ScrolledPanel, handlers.ThemeMixin):
2255
+ """Panel containing buttons for each component, sorted by category"""
2256
+
2257
+ class CategoryButton(wx.ToggleButton, handlers.ThemeMixin, HoverMixin):
2258
+ """Button to show/hide a category of components"""
2259
+ def __init__(self, parent, name, cat):
2260
+ if sys.platform == 'darwin':
2261
+ label = name # on macOS the wx.BU_LEFT flag has no effect
2262
+ else:
2263
+ label = " "+name
2264
+ # Initialise button
2265
+ wx.ToggleButton.__init__(self, parent,
2266
+ label=label, size=(-1, 24),
2267
+ style= wx.BORDER_NONE | wx.BU_LEFT)
2268
+ self.parent = parent
2269
+ # Link to category of buttons
2270
+ self.menu = self.parent.catSizers[cat]
2271
+ # # Set own sizer
2272
+ # self.sizer = wx.GridSizer(wx.HORIZONTAL)
2273
+ # self.SetSizer(self.sizer)
2274
+ # # Add icon
2275
+ # self.icon = wx.StaticText(parent=self, label="DOWN")
2276
+ # self.sizer.Add(self.icon, border=5, flag=wx.ALL | wx.ALIGN_RIGHT)
2277
+ # Default states to false
2278
+ self.state = False
2279
+ self.hover = False
2280
+ # Bind toggle function
2281
+ self.Bind(wx.EVT_TOGGLEBUTTON, self.ToggleMenu)
2282
+ # Bind hover functions
2283
+ self.SetupHover()
2284
+
2285
+ def ToggleMenu(self, event):
2286
+ # If triggered manually with a bool, treat that as a substitute for event selection
2287
+ if isinstance(event, bool):
2288
+ state = event
2289
+ else:
2290
+ state = event.GetSelection()
2291
+ self.SetValue(state)
2292
+ if state:
2293
+ # If state is show, then show all non-hidden components
2294
+ for btn in self.menu.GetChildren():
2295
+ btn = btn.GetWindow()
2296
+ if isinstance(btn, ComponentsPanel.ComponentButton):
2297
+ comp = btn.component
2298
+ elif isinstance(btn, ComponentsPanel.RoutineButton):
2299
+ comp = btn.routine
2300
+ else:
2301
+ return
2302
+ # Work out if it should be shown based on filter
2303
+ cond = True
2304
+ if self.parent.filter == 'Any':
2305
+ cond = True
2306
+ elif self.parent.filter == 'Both':
2307
+ cond = 'PsychoJS' in comp.targets and 'PsychoPy' in comp.targets
2308
+ elif self.parent.filter in ['PsychoPy', 'PsychoJS']:
2309
+ cond = self.parent.filter in comp.targets
2310
+ # Always hide if hidden by prefs
2311
+ if comp.__name__ in prefs.builder['hiddenComponents'] + alwaysHidden:
2312
+ cond = False
2313
+ btn.Show(cond)
2314
+ # # Update icon
2315
+ # self.icon.SetLabelText(chr(int("1401", 16)))
2316
+ else:
2317
+ # If state is hide, hide all components
2318
+ self.menu.ShowItems(False)
2319
+ # # Update icon
2320
+ # self.icon.SetLabelText(chr(int("140A", 16)))
2321
+ # Do layout
2322
+ self.parent.Layout()
2323
+ self.parent.SetupScrolling()
2324
+ # Restyle
2325
+ self.OnHover()
2326
+
2327
+ def _applyAppTheme(self):
2328
+ """Apply app theme to this button"""
2329
+ self.OnHover()
2330
+
2331
+ class ComponentButton(wx.Button, handlers.ThemeMixin):
2332
+ """Button to open component parameters dialog"""
2333
+ def __init__(self, parent, name, comp, cat):
2334
+ self.parent = parent
2335
+ self.component = comp
2336
+ self.category = cat
2337
+ # Get a shorter, title case version of component name
2338
+ label = name
2339
+ for redundant in ['component', 'Component', "ButtonBox"]:
2340
+ label = label.replace(redundant, "")
2341
+ label = prettyname(label, wrap=10)
2342
+ # Make button
2343
+ wx.Button.__init__(self, parent, wx.ID_ANY,
2344
+ label=label, name=name,
2345
+ size=(68, 68+12*label.count("\n")),
2346
+ style=wx.NO_BORDER)
2347
+ self.SetToolTip(wx.ToolTip(comp.tooltip or name))
2348
+ # Style
2349
+ self._applyAppTheme()
2350
+ # Bind to functions
2351
+ self.Bind(wx.EVT_BUTTON, self.onClick)
2352
+ self.Bind(wx.EVT_RIGHT_DOWN, self.onRightClick)
2353
+
2354
+ def onClick(self, evt=None, timeout=None):
2355
+ """Called when a component button is clicked on.
2356
+ """
2357
+ routine = self.parent.frame.routinePanel.getCurrentRoutine()
2358
+ if routine is None:
2359
+ if timeout is not None: # just return, we're testing the UI
2360
+ return
2361
+ # Show a message telling the user there is no routine in the
2362
+ # experiment, making adding a component pointless until they do
2363
+ # so.
2364
+ dlg = wx.MessageDialog(
2365
+ self,
2366
+ _translate(
2367
+ "Cannot add component, experiment has no routines."),
2368
+ _translate("Error"),
2369
+ wx.OK | wx.ICON_ERROR | wx.CENTRE)
2370
+ dlg.ShowModal()
2371
+ dlg.Destroy()
2372
+ return
2373
+
2374
+ page = self.parent.frame.routinePanel.getCurrentPage()
2375
+ comp = self.component(
2376
+ parentName=routine.name,
2377
+ exp=self.parent.frame.exp)
2378
+
2379
+ # does this component have a help page?
2380
+ if hasattr(comp, 'url'):
2381
+ helpUrl = comp.url
2382
+ else:
2383
+ helpUrl = None
2384
+ # create component template
2385
+ if comp.type == 'Code':
2386
+ _Dlg = DlgCodeComponentProperties
2387
+ else:
2388
+ _Dlg = DlgComponentProperties
2389
+ dlg = _Dlg(frame=self.parent.frame,
2390
+ element=comp,
2391
+ experiment=self.parent.frame.exp,
2392
+ timeout=timeout)
2393
+
2394
+ if dlg.OK:
2395
+ # Add to the actual routine
2396
+ routine.addComponent(comp)
2397
+ namespace = self.parent.frame.exp.namespace
2398
+ desiredName = comp.params['name'].val
2399
+ name = comp.params['name'].val = namespace.makeValid(desiredName)
2400
+ namespace.add(name)
2401
+ # update the routine's view with the new component too
2402
+ page.redrawRoutine()
2403
+ self.parent.frame.addToUndoStack(
2404
+ "ADD `%s` to `%s`" % (name, routine.name))
2405
+ return True
2406
+
2407
+ def onRightClick(self, evt):
2408
+ """
2409
+ Defines rightclick behavior within builder view's
2410
+ components panel
2411
+ """
2412
+ # Make menu
2413
+ menu = wx.Menu()
2414
+ if self.component.__name__ in self.parent.favorites:
2415
+ # If is in favs
2416
+ msg = "Remove from favorites"
2417
+ fun = self.removeFromFavorites
2418
+ else:
2419
+ # If is not in favs
2420
+ msg = "Add to favorites"
2421
+ fun = self.addToFavorites
2422
+ btn = menu.Append(wx.ID_ANY, _localized[msg])
2423
+ menu.Bind(wx.EVT_MENU, fun, btn)
2424
+ # Show as popup
2425
+ self.PopupMenu(menu, evt.GetPosition())
2426
+ # Destroy to avoid mem leak
2427
+ menu.Destroy()
2428
+
2429
+ def addToFavorites(self, evt):
2430
+ self.parent.addToFavorites(self.component)
2431
+
2432
+ def removeFromFavorites(self, evt):
2433
+ self.parent.removeFromFavorites(self)
2434
+
2435
+ def _applyAppTheme(self):
2436
+ # Set colors
2437
+ self.SetForegroundColour(colors.app['text'])
2438
+ self.SetBackgroundColour(colors.app['panel_bg'])
2439
+ # Set bitmap
2440
+ icon = icons.ComponentIcon(self.component, size=48)
2441
+ if hasattr(self.component, "beta") and self.component.beta:
2442
+ icon = icon.beta
2443
+ else:
2444
+ icon = icon.bitmap
2445
+ self.SetBitmap(icon)
2446
+ self.SetBitmapCurrent(icon)
2447
+ self.SetBitmapPressed(icon)
2448
+ self.SetBitmapFocus(icon)
2449
+ self.SetBitmapPosition(wx.TOP)
2450
+ # Refresh
2451
+ self.Refresh()
2452
+
2453
+ class RoutineButton(wx.Button, handlers.ThemeMixin):
2454
+ """Button to open component parameters dialog"""
2455
+ def __init__(self, parent, name, rt, cat):
2456
+ self.parent = parent
2457
+ self.routine = rt
2458
+ self.category = cat
2459
+ # Get a shorter, title case version of routine name
2460
+ label = name
2461
+ for redundant in ['routine', 'Routine', "ButtonBox"]:
2462
+ label = label.replace(redundant, "")
2463
+ label = prettyname(label, wrap=10)
2464
+ # Make button
2465
+ wx.Button.__init__(self, parent, wx.ID_ANY,
2466
+ label=label, name=name,
2467
+ size=(68, 68+12*label.count("\n")),
2468
+ style=wx.NO_BORDER)
2469
+ self.SetToolTip(wx.ToolTip(rt.tooltip or name))
2470
+ # Style
2471
+ self._applyAppTheme()
2472
+ # Bind to functions
2473
+ self.Bind(wx.EVT_BUTTON, self.onClick)
2474
+ self.Bind(wx.EVT_RIGHT_DOWN, self.onRightClick)
2475
+
2476
+ def onClick(self, evt=None, timeout=None):
2477
+ # Make a routine instance
2478
+ comp = self.routine(exp=self.parent.frame.exp)
2479
+ # Add to the actual routine
2480
+ exp = self.parent.frame.exp
2481
+ namespace = exp.namespace
2482
+ name = comp.params['name'].val = namespace.makeValid(
2483
+ comp.params['name'].val)
2484
+ namespace.add(name)
2485
+ exp.addStandaloneRoutine(name, comp)
2486
+ # update the routine's view with the new routine too
2487
+ self.parent.frame.addToUndoStack(
2488
+ "ADD `%s` to `%s`" % (name, exp.name))
2489
+ # Add a routine page
2490
+ notebook = self.parent.frame.routinePanel
2491
+ notebook.addRoutinePage(name, comp)
2492
+ notebook.setCurrentRoutine(comp)
2493
+
2494
+ def onRightClick(self, evt):
2495
+ """
2496
+ Defines rightclick behavior within builder view's
2497
+ routines panel
2498
+ """
2499
+ return
2500
+
2501
+ def addToFavorites(self, evt):
2502
+ self.parent.addToFavorites(self.routine)
2503
+
2504
+ def removeFromFavorites(self, evt):
2505
+ self.parent.removeFromFavorites(self)
2506
+
2507
+ def _applyAppTheme(self):
2508
+ # Set colors
2509
+ self.SetForegroundColour(colors.app['text'])
2510
+ self.SetBackgroundColour(colors.app['panel_bg'])
2511
+ # Set bitmap
2512
+ icon = icons.ComponentIcon(self.routine, size=48)
2513
+ if hasattr(self.routine, "beta") and self.routine.beta:
2514
+ icon = icon.beta
2515
+ else:
2516
+ icon = icon.bitmap
2517
+ self.SetBitmap(icon)
2518
+ self.SetBitmapCurrent(icon)
2519
+ self.SetBitmapPressed(icon)
2520
+ self.SetBitmapFocus(icon)
2521
+ self.SetBitmapPosition(wx.TOP)
2522
+ # Refresh
2523
+ self.Refresh()
2524
+
2525
+ class FilterDialog(wx.Dialog, handlers.ThemeMixin):
2526
+ def __init__(self, parent, size=(200, 300)):
2527
+ wx.Dialog.__init__(self, parent, size=size)
2528
+ self.parent = parent
2529
+ # Setup sizer
2530
+ self.border = wx.BoxSizer(wx.VERTICAL)
2531
+ self.SetSizer(self.border)
2532
+ self.sizer = wx.BoxSizer(wx.VERTICAL)
2533
+ self.border.Add(self.sizer, border=6, proportion=1, flag=wx.ALL | wx.EXPAND)
2534
+ # Label
2535
+ self.label = wx.StaticText(self, label="Show components which \nwork with...")
2536
+ self.sizer.Add(self.label, border=6, flag=wx.ALL | wx.EXPAND)
2537
+ # Control
2538
+ self.viewCtrl = ToggleButtonArray(self,
2539
+ labels=("PsychoPy (local)", "PsychoJS (online)", "Both", "Any"),
2540
+ values=("PsychoPy", "PsychoJS", "Both", "Any"),
2541
+ multi=False, ori=wx.VERTICAL)
2542
+ self.viewCtrl.Bind(wx.EVT_CHOICE, self.onChange)
2543
+ self.sizer.Add(self.viewCtrl, border=6, flag=wx.ALL | wx.EXPAND)
2544
+ self.viewCtrl.SetValue(prefs.builder['componentFilter'])
2545
+ # OK
2546
+ self.OKbtn = wx.Button(self, id=wx.ID_OK, label=_translate("OK"))
2547
+ self.SetAffirmativeId(wx.ID_OK)
2548
+ self.border.Add(self.OKbtn, border=6, flag=wx.ALL | wx.ALIGN_RIGHT)
2549
+
2550
+ self.Layout()
2551
+ self._applyAppTheme()
2552
+
2553
+ def GetValue(self):
2554
+ return self.viewCtrl.GetValue()
2555
+
2556
+ def onChange(self, evt=None):
2557
+ self.parent.filter = prefs.builder['componentFilter'] = self.GetValue()
2558
+ prefs.saveUserPrefs()
2559
+ self.parent.Refresh()
2560
+
2561
+ def __init__(self, frame, id=-1):
2562
+ """A panel that displays available components.
2563
+ """
2564
+ self.frame = frame
2565
+ self.app = frame.app
2566
+ self.dpi = self.app.dpi
2567
+ self.prefs = self.app.prefs
2568
+ panelWidth = 3 * (68 + 12) + 12 + 12
2569
+ scrolledpanel.ScrolledPanel.__init__(self,
2570
+ frame,
2571
+ id,
2572
+ size=(panelWidth, 10 * self.dpi),
2573
+ style=wx.BORDER_NONE)
2574
+ # Get filter from prefs
2575
+ self.filter = prefs.builder['componentFilter']
2576
+ # Setup sizer
2577
+ self.sizer = wx.BoxSizer(wx.VERTICAL)
2578
+ self.SetSizer(self.sizer)
2579
+ # Add filter button
2580
+ self.filterBtn = wx.Button(self, size=(24, 24), style=wx.BORDER_NONE)
2581
+ self.sizer.Add(self.filterBtn, border=0, flag=wx.ALL | wx.ALIGN_RIGHT)
2582
+ self.filterBtn.Bind(wx.EVT_BUTTON, self.onFilterBtn)
2583
+ # Get components
2584
+ self.components = experiment.getAllComponents(
2585
+ self.app.prefs.builder['componentsFolders'])
2586
+ del self.components['SettingsComponent']
2587
+ self.routines = experiment.getAllStandaloneRoutines()
2588
+ # Get categories
2589
+ self.categories = getAllCategories(
2590
+ self.app.prefs.builder['componentsFolders'])
2591
+ for name, rt in self.routines.items():
2592
+ for cat in rt.categories:
2593
+ if cat not in self.categories:
2594
+ self.categories.append(cat)
2595
+ # Get favorites
2596
+ self.faveThreshold = 20
2597
+ self.faveLevels = self.prefs.appDataCfg['builder']['favComponents']
2598
+ self.favorites = []
2599
+ for comp in self.components:
2600
+ # Add component to favorite levels with a score of 0 if it's not already present
2601
+ if comp not in self.faveLevels:
2602
+ self.faveLevels[comp] = 0
2603
+ # Mark as a favorite if it exceeds a threshold
2604
+ if self.faveLevels[comp] > self.faveThreshold:
2605
+ self.favorites.append(comp)
2606
+ # Fill in gaps in favorites with defaults
2607
+ faveDefaults = ['ImageComponent', 'KeyboardComponent', 'SoundComponent',
2608
+ 'TextComponent', 'MouseComponent', 'SliderComponent']
2609
+ while len(self.favorites) < 6:
2610
+ thisDef = faveDefaults.pop(0)
2611
+ if thisDef not in self.favorites:
2612
+ self.favorites.append(thisDef)
2613
+ # Make a sizer and label for each category
2614
+ self.catSizers = {cat: wx.WrapSizer(orient=wx.HORIZONTAL) for cat in self.categories}
2615
+ self.catLabels = {cat: self.CategoryButton(self, name=_translate(str(cat)), cat=str(cat)) for cat in self.categories}
2616
+ for cat in self.categories:
2617
+ self.sizer.Add(self.catLabels[cat], border=3, flag=wx.BOTTOM | wx.EXPAND)
2618
+ self.sizer.Add(self.catSizers[cat], border=6, flag=wx.ALL | wx.ALIGN_CENTER)
2619
+ # Make a button for each component
2620
+ self.compButtons = []
2621
+ for name, comp in self.components.items():
2622
+ for cat in comp.categories: # make one button for each category
2623
+ self.compButtons.append(
2624
+ self.ComponentButton(self, name, comp, cat)
2625
+ )
2626
+ if name in self.favorites:
2627
+ self.compButtons.append(
2628
+ self.ComponentButton(self, name, comp, "Favorites")
2629
+ )
2630
+ # Add component buttons to category sizers
2631
+ for btn in self.compButtons:
2632
+ self.catSizers[btn.category].Add(btn, border=3, flag=wx.ALL)
2633
+ # Make a button for each routine
2634
+ self.rtButtons = []
2635
+ for name, rt in self.routines.items():
2636
+ for cat in rt.categories: # make one button for each category
2637
+ self.rtButtons.append(
2638
+ self.RoutineButton(self, name, rt, cat)
2639
+ )
2640
+ if name in self.favorites:
2641
+ self.rtButtons.append(
2642
+ self.RoutineButton(self, name, rt, "Favorites")
2643
+ )
2644
+ # Add component buttons to category sizers
2645
+ for btn in self.rtButtons:
2646
+ self.catSizers[btn.category].Add(btn, border=3, flag=wx.ALL)
2647
+ # Show favourites on startup
2648
+ self.catLabels['Favorites'].ToggleMenu(True)
2649
+ # Do sizing
2650
+ self.Fit()
2651
+ # double buffered better rendering except if retina
2652
+ self.SetDoubleBuffered(not self.frame.isRetina)
2653
+
2654
+ def _applyAppTheme(self, target=None):
2655
+ # Style component panel
2656
+ self.SetForegroundColour(colors.app['text'])
2657
+ self.SetBackgroundColour(colors.app['panel_bg'])
2658
+ # Style category labels
2659
+ for lbl in self.catLabels:
2660
+ self.catLabels[lbl].SetForegroundColour(colors.app['text'])
2661
+ # Style filter button
2662
+ self.filterBtn.SetBackgroundColour(colors.app['panel_bg'])
2663
+ icon = icons.ButtonIcon("filter", size=16).bitmap
2664
+ self.filterBtn.SetBitmap(icon)
2665
+ self.filterBtn.SetBitmapCurrent(icon)
2666
+ self.filterBtn.SetBitmapPressed(icon)
2667
+ self.filterBtn.SetBitmapFocus(icon)
2668
+
2669
+ def addToFavorites(self, comp):
2670
+ name = comp.__name__
2671
+ # Mark component as a favorite
2672
+ self.faveLevels[name] = self.faveThreshold + 1
2673
+ self.favorites.append(name)
2674
+ # Add button to favorites menu
2675
+ btn = self.ComponentButton(self, name, comp, "Favorites")
2676
+ self.compButtons.append(btn)
2677
+ self.catSizers['Favorites'].Add(btn, border=3, flag=wx.ALL)
2678
+ # Do sizing
2679
+ self.Layout()
2680
+
2681
+ def removeFromFavorites(self, button):
2682
+ comp = button.component
2683
+ name = comp.__name__
2684
+ # Skip if component isn't in favorites
2685
+ if name not in self.favorites:
2686
+ return
2687
+ # Unmark component as favorite
2688
+ self.faveLevels[name] = 0
2689
+ self.favorites.remove(name)
2690
+ # Remove button from favorites menu
2691
+ button.Hide()
2692
+ # Do sizing
2693
+ self.Layout()
2694
+
2695
+ def enableComponents(self, enable=True):
2696
+ for button in self.compButtons:
2697
+ button.Enable(enable)
2698
+ self.Update()
2699
+
2700
+ def Refresh(self, eraseBackground=True, rect=None):
2701
+ wx.Window.Refresh(self, eraseBackground, rect)
2702
+ # Get view value(s)
2703
+ if prefs.builder['componentFilter'] == "Both":
2704
+ view = ["PsychoPy", "PsychoJS"]
2705
+ elif prefs.builder['componentFilter'] == "Any":
2706
+ view = []
2707
+ else:
2708
+ view = [prefs.builder['componentFilter']]
2709
+ # Toggle all categories so they refresh
2710
+ for btn in self.catLabels.values():
2711
+ btn.ToggleMenu(btn.GetValue())
2712
+ # If every button in a category is hidden, hide the category
2713
+ for cat, btn in self.catLabels.items():
2714
+ empty = True
2715
+ for child in self.catSizers[cat].Children:
2716
+ if isinstance(child.Window, self.ComponentButton):
2717
+ name = child.Window.component.__name__
2718
+ elif isinstance(child.Window, self.RoutineButton):
2719
+ name = child.Window.routine.__name__
2720
+ else:
2721
+ name = ""
2722
+ if name not in prefs.builder['hiddenComponents'] + alwaysHidden:
2723
+ empty = False
2724
+ btn.Show(not empty)
2725
+ # Do sizing
2726
+ self.Layout()
2727
+ self.SetupScrolling()
2728
+
2729
+ def onFilterBtn(self, evt=None):
2730
+ dlg = self.FilterDialog(self)
2731
+ dlg.ShowModal()
2732
+
2733
+
2734
+ class ReadmeFrame(wx.Frame):
2735
+ """Defines construction of the Readme Frame"""
2736
+
2737
+ def __init__(self, parent):
2738
+ """
2739
+ A frame for presenting/loading/saving readme files
2740
+ """
2741
+ self.parent = parent
2742
+ title = "%s readme" % (parent.exp.name)
2743
+ self._fileLastModTime = None
2744
+ pos = wx.Point(parent.Position[0] + 80, parent.Position[1] + 80)
2745
+ _style = wx.DEFAULT_FRAME_STYLE | wx.FRAME_FLOAT_ON_PARENT
2746
+ wx.Frame.__init__(self, parent, title=title,
2747
+ size=(600, 500), pos=pos, style=_style)
2748
+ self.Bind(wx.EVT_CLOSE, self.onClose)
2749
+ self.Hide()
2750
+ # create icon
2751
+ if sys.platform == 'darwin':
2752
+ pass # doesn't work and not necessary - handled by app bundle
2753
+ else:
2754
+ iconFile = os.path.join(parent.paths['resources'], 'coder.ico')
2755
+ if os.path.isfile(iconFile):
2756
+ self.SetIcon(wx.Icon(iconFile, wx.BITMAP_TYPE_ICO))
2757
+ self.makeMenus()
2758
+ self.rawText = ""
2759
+ self.ctrl = HtmlWindow(self, wx.ID_ANY)
2760
+ self.ctrl.Bind(wx.html.EVT_HTML_LINK_CLICKED, self.onUrl)
2761
+ # Style
2762
+ self.ctrl.SetFonts(normal_face="Open Sans", fixed_face="JetBrains Mono", sizes=[8, 10, 12, 14, 16, 18, 20])
2763
+
2764
+ def onUrl(self, evt=None):
2765
+ webbrowser.open(evt.LinkInfo.Href)
2766
+
2767
+ def onClose(self, evt=None):
2768
+ """
2769
+ Defines behavior on close of the Readme Frame
2770
+ """
2771
+ self.parent.readmeFrame = None
2772
+ self.Destroy()
2773
+
2774
+ def makeMenus(self):
2775
+ """Produces menus for the Readme Frame"""
2776
+
2777
+ # ---Menus---#000000#FFFFFF-------------------------------------------
2778
+ menuBar = wx.MenuBar()
2779
+ # ---_file---#000000#FFFFFF-------------------------------------------
2780
+ self.fileMenu = wx.Menu()
2781
+ menuBar.Append(self.fileMenu, _translate('&File'))
2782
+ menu = self.fileMenu
2783
+ keys = self.parent.app.keys
2784
+ menu.Append(wx.ID_EDIT, _translate("Edit"))
2785
+ self.Bind(wx.EVT_MENU, self.fileEdit, id=wx.ID_EDIT)
2786
+ menu.Append(wx.ID_CLOSE,
2787
+ _translate("&Close readme\t%s") % keys['close'])
2788
+ item = self.Bind(wx.EVT_MENU, self.toggleVisible, id=wx.ID_CLOSE)
2789
+ item = menu.Append(-1,
2790
+ _translate("&Toggle readme\t%s") % keys[
2791
+ 'toggleReadme'],
2792
+ _translate("Toggle Readme"))
2793
+ self.Bind(wx.EVT_MENU, self.toggleVisible, item)
2794
+ self.SetMenuBar(menuBar)
2795
+
2796
+ def setFile(self, filename):
2797
+ """Sets the readme file found with current builder experiment"""
2798
+ self.filename = filename
2799
+ self.expName = self.parent.exp.getExpName()
2800
+ # check we can read
2801
+ if filename is None: # check if we can write to the directory
2802
+ return False
2803
+ elif not os.path.exists(filename):
2804
+ with open(filename, "w") as f:
2805
+ f.write("")
2806
+ self.filename = filename
2807
+ return False
2808
+ elif not os.access(filename, os.R_OK):
2809
+ msg = "Found readme file (%s) no read permissions"
2810
+ logging.warning(msg % filename)
2811
+ return False
2812
+ # attempt to open
2813
+ try:
2814
+ f = codecs.open(filename, 'r', 'utf-8-sig')
2815
+ except IOError as err:
2816
+ msg = ("Found readme file for %s and appear to have"
2817
+ " permissions, but can't open")
2818
+ logging.warning(msg % self.expName)
2819
+ logging.warning(err)
2820
+ return False
2821
+ # attempt to read
2822
+ try:
2823
+ readmeText = f.read().replace("\r\n", "\n")
2824
+ except Exception:
2825
+ msg = ("Opened readme file for %s it but failed to read it "
2826
+ "(not text/unicode?)")
2827
+ logging.error(msg % self.expName)
2828
+ return False
2829
+ f.close()
2830
+ self._fileLastModTime = os.path.getmtime(filename)
2831
+ self.rawText = readmeText
2832
+ if md:
2833
+ renderedText = md.MarkdownIt().render(readmeText)
2834
+ else:
2835
+ renderedText = readmeText.replace("\n", "<br>")
2836
+ self.ctrl.SetPage(renderedText)
2837
+ self.SetTitle("%s readme (%s)" % (self.expName, filename))
2838
+
2839
+ def refresh(self, evt=None):
2840
+ if hasattr(self, 'filename'):
2841
+ self.setFile(self.filename)
2842
+
2843
+ def fileEdit(self, evt=None):
2844
+ self.parent.app.showCoder()
2845
+ coder = self.parent.app.coder
2846
+ if not self.filename:
2847
+ self.parent.updateReadme()
2848
+ coder.fileOpen(filename=self.filename)
2849
+ # Close README window
2850
+ self.Close()
2851
+
2852
+ def fileSave(self, evt=None):
2853
+ """Defines save behavior for readme frame"""
2854
+ mtime = os.path.getmtime(self.filename)
2855
+ if self._fileLastModTime and mtime > self._fileLastModTime:
2856
+ logging.warning(
2857
+ 'readme file has been changed by another program?')
2858
+ txt = self.rawText
2859
+ with codecs.open(self.filename, 'w', 'utf-8-sig') as f:
2860
+ f.write(txt)
2861
+
2862
+ def toggleVisible(self, evt=None):
2863
+ """Defines visibility toggle for readme frame"""
2864
+ if self.IsShown():
2865
+ self.Hide()
2866
+ else:
2867
+ self.Show()
2868
+
2869
+
2870
+ class FlowPanel(wx.ScrolledWindow, handlers.ThemeMixin):
2871
+
2872
+ def __init__(self, frame, id=-1):
2873
+ """A panel that shows how the routines will fit together
2874
+ """
2875
+ self.frame = frame
2876
+ self.app = frame.app
2877
+ self.dpi = self.app.dpi
2878
+ wx.ScrolledWindow.__init__(self, frame, id, (0, 0),
2879
+ size=wx.Size(8 * self.dpi, 3 * self.dpi),
2880
+ style=wx.HSCROLL | wx.VSCROLL | wx.BORDER_NONE)
2881
+ self.needUpdate = True
2882
+ self.maxWidth = 50 * self.dpi
2883
+ self.maxHeight = 2 * self.dpi
2884
+ self.mousePos = None
2885
+ # if we're adding a loop or routine then add spots to timeline
2886
+ # self.drawNearestRoutinePoint = True
2887
+ # self.drawNearestLoopPoint = False
2888
+ # lists the x-vals of points to draw, eg loop locations:
2889
+ self.pointsToDraw = []
2890
+ # for flowSize, showLoopInfoInFlow:
2891
+ self.appData = self.app.prefs.appData
2892
+
2893
+ # self.SetAutoLayout(True)
2894
+ self.SetScrollRate(self.dpi / 4, self.dpi / 4)
2895
+
2896
+ # create a PseudoDC to record our drawing
2897
+ self.pdc = PseudoDC()
2898
+ if parse_version(wx.__version__) < parse_version('4.0.0a1'):
2899
+ self.pdc.DrawRoundedRectangle = self.pdc.DrawRoundedRectangleRect
2900
+ self.pen_cache = {}
2901
+ self.brush_cache = {}
2902
+ # vars for handling mouse clicks
2903
+ self.hitradius = 5
2904
+ self.dragid = -1
2905
+ self.entryPointPosList = []
2906
+ self.entryPointIDlist = []
2907
+ self.gapsExcluded = []
2908
+ # mode can also be 'loopPoint1','loopPoint2','routinePoint'
2909
+ self.mode = 'normal'
2910
+ self.insertingRoutine = ""
2911
+
2912
+ # for the context menu use the ID of the drawn icon to retrieve
2913
+ # the component (loop or routine)
2914
+ self.componentFromID = {}
2915
+ self.contextMenuLabels = {
2916
+ 'remove': _translate('remove'),
2917
+ 'rename': _translate('rename')}
2918
+ self.contextMenuItems = ['remove', 'rename']
2919
+ self.contextItemFromID = {}
2920
+ self.contextIDFromItem = {}
2921
+ for item in self.contextMenuItems:
2922
+ id = wx.NewIdRef()
2923
+ self.contextItemFromID[id] = item
2924
+ self.contextIDFromItem[item] = id
2925
+
2926
+ # self.btnInsertRoutine = wx.Button(self,-1,
2927
+ # 'Insert Routine', pos=(10,10))
2928
+ # self.btnInsertLoop = wx.Button(self,-1,'Insert Loop', pos=(10,30))
2929
+ labelRoutine = _translate('Insert Routine ')
2930
+ labelLoop = _translate('Insert Loop ')
2931
+ btnHeight = 50
2932
+ # Create add routine button
2933
+ self.btnInsertRoutine = PsychopyPlateBtn(
2934
+ self, -1, labelRoutine, pos=(10, 10), size=(120, btnHeight),
2935
+ style=platebtn.PB_STYLE_SQUARE
2936
+ )
2937
+ # Create add loop button
2938
+ self.btnInsertLoop = PsychopyPlateBtn(
2939
+ self, -1, labelLoop, pos=(10, btnHeight+20),
2940
+ size=(120, btnHeight),
2941
+ style=platebtn.PB_STYLE_SQUARE
2942
+ ) # spaces give size for CANCEL
2943
+
2944
+ # use self.appData['flowSize'] to index a tuple to get a specific
2945
+ # value, eg: (4,6,8)[self.appData['flowSize']]
2946
+ self.flowMaxSize = 2 # upper limit on increaseSize
2947
+
2948
+ # bind events
2949
+ self.Bind(wx.EVT_MOUSE_EVENTS, self.OnMouse)
2950
+ self.Bind(wx.EVT_BUTTON, self.onInsertRoutine, self.btnInsertRoutine)
2951
+ self.Bind(wx.EVT_BUTTON, self.setLoopPoint1, self.btnInsertLoop)
2952
+ self.Bind(wx.EVT_PAINT, self.OnPaint)
2953
+
2954
+ idClear = wx.NewIdRef()
2955
+ self.Bind(wx.EVT_MENU, self.clearMode, id=idClear)
2956
+ aTable = wx.AcceleratorTable([
2957
+ (wx.ACCEL_NORMAL, wx.WXK_ESCAPE, idClear)
2958
+ ])
2959
+ self.SetAcceleratorTable(aTable)
2960
+
2961
+ # double buffered better rendering except if retina
2962
+ self.SetDoubleBuffered(not self.frame.isRetina)
2963
+
2964
+ def _applyAppTheme(self, target=None):
2965
+ """Apply any changes which have been made to the theme since panel was last loaded"""
2966
+ # Style loop/routine buttons
2967
+ self.btnInsertLoop.SetBackgroundColour(colors.app['frame_bg'])
2968
+ self.btnInsertLoop.SetForegroundColour(colors.app['text'])
2969
+ self.btnInsertLoop.Update()
2970
+ self.btnInsertRoutine.SetBackgroundColour(colors.app['frame_bg'])
2971
+ self.btnInsertRoutine.SetForegroundColour(colors.app['text'])
2972
+ self.btnInsertRoutine.Update()
2973
+ # Set background
2974
+ self.SetBackgroundColour(colors.app['panel_bg'])
2975
+
2976
+ self.draw()
2977
+
2978
+ def clearMode(self, event=None):
2979
+ """If we were in middle of doing something (like inserting routine)
2980
+ then end it, allowing user to cancel
2981
+ """
2982
+ self.mode = 'normal'
2983
+ self.insertingRoutine = None
2984
+ for id in self.entryPointIDlist:
2985
+ self.pdc.RemoveId(id)
2986
+ self.entryPointPosList = []
2987
+ self.entryPointIDlist = []
2988
+ self.gapsExcluded = []
2989
+ self.draw()
2990
+ self.frame.SetStatusText("")
2991
+ self.btnInsertRoutine.SetLabel(_translate('Insert Routine'))
2992
+ self.btnInsertRoutine.Update()
2993
+ self.btnInsertLoop.SetLabel(_translate('Insert Loop'))
2994
+ self.btnInsertRoutine.Update()
2995
+
2996
+ def ConvertEventCoords(self, event):
2997
+ xView, yView = self.GetViewStart()
2998
+ xDelta, yDelta = self.GetScrollPixelsPerUnit()
2999
+ return (event.GetX() + (xView * xDelta),
3000
+ event.GetY() + (yView * yDelta))
3001
+
3002
+ def OffsetRect(self, r):
3003
+ """Offset the rectangle, r, to appear in the given position
3004
+ in the window
3005
+ """
3006
+ xView, yView = self.GetViewStart()
3007
+ xDelta, yDelta = self.GetScrollPixelsPerUnit()
3008
+ r.Offset((-(xView * xDelta), -(yView * yDelta)))
3009
+
3010
+ def onInsertRoutine(self, evt):
3011
+ """For when the insert Routine button is pressed - bring up
3012
+ dialog and present insertion point on flow line.
3013
+ see self.insertRoutine() for further info
3014
+ """
3015
+ if self.mode.startswith('loopPoint'):
3016
+ self.clearMode()
3017
+ elif self.mode == 'routine':
3018
+ # clicked again with label now being "Cancel..."
3019
+ self.clearMode()
3020
+ return
3021
+ self.frame.SetStatusText(_translate(
3022
+ "Select a Routine to insert (Esc to exit)"))
3023
+ menu = wx.Menu()
3024
+ self.routinesFromID = {}
3025
+ id = wx.NewIdRef()
3026
+ menu.Append(id, '(new)')
3027
+ self.routinesFromID[id] = '(new)'
3028
+ menu.Bind(wx.EVT_MENU, self.insertNewRoutine, id=id)
3029
+ flow = self.frame.exp.flow
3030
+ for name, routine in self.frame.exp.routines.items():
3031
+ id = wx.NewIdRef()
3032
+ item = menu.Append(id, name)
3033
+ # Enable / disable each routine's button according to limits
3034
+ if hasattr(routine, "limit"):
3035
+ limitProgress = 0
3036
+ for rt in flow:
3037
+ limitProgress += int(isinstance(rt, type(routine)))
3038
+ item.Enable(limitProgress < routine.limit or routine in flow)
3039
+ self.routinesFromID[id] = name
3040
+ menu.Bind(wx.EVT_MENU, self.onInsertRoutineSelect, id=id)
3041
+ btnPos = self.btnInsertRoutine.GetRect()
3042
+ menuPos = (btnPos[0], btnPos[1] + btnPos[3])
3043
+ self.PopupMenu(menu, menuPos)
3044
+ menu.Bind(wx.EVT_MENU_CLOSE, self.clearMode)
3045
+ menu.Destroy() # destroy to avoid mem leak
3046
+
3047
+ def insertNewRoutine(self, event):
3048
+ """selecting (new) is a short-cut for:
3049
+ make new routine, insert it into the flow
3050
+ """
3051
+ newRoutine = self.frame.routinePanel.createNewRoutine()
3052
+ if newRoutine:
3053
+ self.routinesFromID[event.GetId()] = newRoutine
3054
+ self.onInsertRoutineSelect(event)
3055
+ else:
3056
+ self.clearMode()
3057
+
3058
+ def onInsertRoutineSelect(self, event):
3059
+ """User has selected a routine to be entered so bring up the
3060
+ entrypoint marker and await mouse button press.
3061
+ see self.insertRoutine() for further info
3062
+ """
3063
+ self.mode = 'routine'
3064
+ self.btnInsertRoutine.SetLabel(_translate('CANCEL Insert'))
3065
+ self.frame.SetStatusText(_translate(
3066
+ 'Click where you want to insert the Routine, or CANCEL insert.'))
3067
+ self.insertingRoutine = self.routinesFromID[event.GetId()]
3068
+ x = self.getNearestGapPoint(0)
3069
+ self.drawEntryPoints([x])
3070
+
3071
+ def insertRoutine(self, ii):
3072
+ """Insert a routine into the Flow knowing its name and location
3073
+
3074
+ onInsertRoutine() the button has been pressed so present menu
3075
+ onInsertRoutineSelect() user selected the name so present entry points
3076
+ OnMouse() user has selected a point on the timeline to insert entry
3077
+
3078
+ """
3079
+ rtn = self.frame.exp.routines[self.insertingRoutine]
3080
+ self.frame.exp.flow.addRoutine(rtn, ii)
3081
+ self.frame.addToUndoStack("ADD Routine `%s`" % rtn.name)
3082
+ # reset flow drawing (remove entry point)
3083
+ self.clearMode()
3084
+ # enable/disable add loop button
3085
+ self.btnInsertLoop.Enable(bool(len(self.frame.exp.flow)))
3086
+
3087
+ def setLoopPoint1(self, evt=None):
3088
+ """Someone pushed the insert loop button.
3089
+ Fetch the dialog
3090
+ """
3091
+ if self.mode == 'routine':
3092
+ self.clearMode()
3093
+ # clicked again, label is "Cancel..."
3094
+ elif self.mode.startswith('loopPoint'):
3095
+ self.clearMode()
3096
+ return
3097
+ self.btnInsertLoop.SetLabel(_translate('CANCEL insert'))
3098
+ self.mode = 'loopPoint1'
3099
+ self.frame.SetStatusText(_translate(
3100
+ 'Click where you want the loop to start/end, or CANCEL insert.'))
3101
+ x = self.getNearestGapPoint(0)
3102
+ self.drawEntryPoints([x])
3103
+
3104
+ def setLoopPoint2(self, evt=None):
3105
+ """We have the location of the first point, waiting to get the second
3106
+ """
3107
+ self.mode = 'loopPoint2'
3108
+ self.frame.SetStatusText(_translate(
3109
+ 'Click the other end for the loop'))
3110
+ thisPos = self.entryPointPosList[0]
3111
+ self.gapsExcluded = [thisPos]
3112
+ self.gapsExcluded.extend(self.getGapPointsCrossingStreams(thisPos))
3113
+ # is there more than one available point
3114
+ diff = wx.GetMousePosition()[0] - self.GetScreenPosition()[0]
3115
+ x = self.getNearestGapPoint(diff, exclude=self.gapsExcluded)
3116
+ self.drawEntryPoints([self.entryPointPosList[0], x])
3117
+ nAvailableGaps = len(self.gapMidPoints) - len(self.gapsExcluded)
3118
+ if nAvailableGaps == 1:
3119
+ self.insertLoop() # there's only one place - use it
3120
+
3121
+ def insertLoop(self, evt=None):
3122
+ # bring up listbox to choose the routine to add, and / or a new one
3123
+ loopDlg = DlgLoopProperties(frame=self.frame,
3124
+ helpUrl=self.app.urls['builder.loops'])
3125
+ startII = self.gapMidPoints.index(min(self.entryPointPosList))
3126
+ endII = self.gapMidPoints.index(max(self.entryPointPosList))
3127
+ if loopDlg.OK:
3128
+ handler = loopDlg.currentHandler
3129
+ self.frame.exp.flow.addLoop(handler,
3130
+ startPos=startII, endPos=endII)
3131
+ action = "ADD Loop `%s` to Flow" % handler.params['name'].val
3132
+ self.frame.addToUndoStack(action)
3133
+ self.clearMode()
3134
+ self.draw()
3135
+
3136
+ def increaseSize(self, event=None):
3137
+ if self.appData['flowSize'] == self.flowMaxSize:
3138
+ self.appData['showLoopInfoInFlow'] = True
3139
+ self.appData['flowSize'] = min(
3140
+ self.flowMaxSize, self.appData['flowSize'] + 1)
3141
+ self.clearMode() # redraws
3142
+
3143
+ def decreaseSize(self, event=None):
3144
+ if self.appData['flowSize'] == 0:
3145
+ self.appData['showLoopInfoInFlow'] = False
3146
+ self.appData['flowSize'] = max(0, self.appData['flowSize'] - 1)
3147
+ self.clearMode() # redraws
3148
+
3149
+ def editLoopProperties(self, event=None, loop=None):
3150
+ # add routine points to the timeline
3151
+ self.setDrawPoints('loops')
3152
+ self.draw()
3153
+ if 'conditions' in loop.params:
3154
+ condOrig = loop.params['conditions'].val
3155
+ condFileOrig = loop.params['conditionsFile'].val
3156
+ title = loop.params['name'].val + ' Properties'
3157
+ loopDlg = DlgLoopProperties(frame=self.frame,
3158
+ helpUrl=self.app.urls['builder.loops'],
3159
+ title=title, loop=loop)
3160
+ if loopDlg.OK:
3161
+ prevLoop = loop
3162
+ if loopDlg.params['loopType'].val == 'staircase':
3163
+ loop = loopDlg.stairHandler
3164
+ elif loopDlg.params['loopType'].val == 'interleaved staircases':
3165
+ loop = loopDlg.multiStairHandler
3166
+ else:
3167
+ # ['random','sequential', 'fullRandom', ]
3168
+ loop = loopDlg.trialHandler
3169
+ # if the loop is a whole new class then we can't just update the
3170
+ # params
3171
+ if loop.getType() != prevLoop.getType():
3172
+ # get indices for start and stop points of prev loop
3173
+ flow = self.frame.exp.flow
3174
+ # find the index of the initiator
3175
+ startII = flow.index(prevLoop.initiator)
3176
+ # minus one because initiator will have been deleted
3177
+ endII = flow.index(prevLoop.terminator) - 1
3178
+ # remove old loop completely
3179
+ flow.removeComponent(prevLoop)
3180
+ # finally insert the new loop
3181
+ flow.addLoop(loop, startII, endII)
3182
+ self.frame.addToUndoStack("EDIT Loop `%s`" %
3183
+ (loop.params['name'].val))
3184
+ elif 'conditions' in loop.params:
3185
+ loop.params['conditions'].val = condOrig
3186
+ loop.params['conditionsFile'].val = condFileOrig
3187
+ # remove the points from the timeline
3188
+ self.setDrawPoints(None)
3189
+ self.draw()
3190
+
3191
+ def OnMouse(self, event):
3192
+ x, y = self.ConvertEventCoords(event)
3193
+ handlerTypes = ('StairHandler', 'TrialHandler', 'MultiStairHandler')
3194
+ if self.mode == 'normal':
3195
+ if event.LeftDown():
3196
+ icons = self.pdc.FindObjectsByBBox(x, y)
3197
+ for thisIcon in icons:
3198
+ # might intersect several and only one has a callback
3199
+ if thisIcon in self.componentFromID:
3200
+ comp = self.componentFromID[thisIcon]
3201
+ if comp.getType() in handlerTypes:
3202
+ self.editLoopProperties(loop=comp)
3203
+ if comp.getType() in ['Routine'] + list(getAllStandaloneRoutines()):
3204
+ self.frame.routinePanel.setCurrentRoutine(
3205
+ routine=comp)
3206
+ elif event.RightDown():
3207
+ icons = self.pdc.FindObjectsByBBox(x, y)
3208
+ # todo: clean-up remove `comp`, its unused
3209
+ comp = None
3210
+ for thisIcon in icons:
3211
+ # might intersect several and only one has a callback
3212
+ if thisIcon in self.componentFromID:
3213
+ # loop through comps looking for Routine, or a Loop if
3214
+ # no routine
3215
+ thisComp = self.componentFromID[thisIcon]
3216
+ if thisComp.getType() in handlerTypes:
3217
+ comp = thisComp # unused
3218
+ icon = thisIcon
3219
+ if thisComp.getType() in ['Routine'] + list(getAllStandaloneRoutines()):
3220
+ comp = thisComp
3221
+ icon = thisIcon
3222
+ break # we've found a Routine so stop looking
3223
+ self.frame.routinePanel.setCurrentRoutine(comp)
3224
+ try:
3225
+ self._menuComponentID = icon
3226
+ xy = wx.Point(event.X + self.GetPosition()[0],
3227
+ event.Y + self.GetPosition()[1])
3228
+ self.showContextMenu(self._menuComponentID, xy=xy)
3229
+ except UnboundLocalError:
3230
+ # right click but not on an icon
3231
+ # might as well do something
3232
+ self.Refresh()
3233
+ elif self.mode == 'routine':
3234
+ if event.LeftDown():
3235
+ pt = self.entryPointPosList[0]
3236
+ self.insertRoutine(ii=self.gapMidPoints.index(pt))
3237
+ else: # move spot if needed
3238
+ point = self.getNearestGapPoint(mouseX=x)
3239
+ self.drawEntryPoints([point])
3240
+ elif self.mode == 'loopPoint1':
3241
+ if event.LeftDown():
3242
+ self.setLoopPoint2()
3243
+ else: # move spot if needed
3244
+ point = self.getNearestGapPoint(mouseX=x)
3245
+ self.drawEntryPoints([point])
3246
+ elif self.mode == 'loopPoint2':
3247
+ if event.LeftDown():
3248
+ self.insertLoop()
3249
+ else: # move spot if needed
3250
+ point = self.getNearestGapPoint(mouseX=x,
3251
+ exclude=self.gapsExcluded)
3252
+ self.drawEntryPoints([self.entryPointPosList[0], point])
3253
+
3254
+ def getNearestGapPoint(self, mouseX, exclude=()):
3255
+ """Get gap that is nearest to a particular mouse location
3256
+ """
3257
+ d = 1000000000
3258
+ nearest = None
3259
+ for point in self.gapMidPoints:
3260
+ if point in exclude:
3261
+ continue
3262
+ if (point - mouseX) ** 2 < d:
3263
+ d = (point - mouseX) ** 2
3264
+ nearest = point
3265
+ return nearest
3266
+
3267
+ def getGapPointsCrossingStreams(self, gapPoint):
3268
+ """For a given gap point, identify the gap points that are
3269
+ excluded by crossing a loop line
3270
+ """
3271
+ gapArray = numpy.array(self.gapMidPoints)
3272
+ nestLevels = numpy.array(self.gapNestLevels)
3273
+ thisLevel = nestLevels[gapArray == gapPoint]
3274
+ invalidGaps = (gapArray[nestLevels != thisLevel]).tolist()
3275
+ return invalidGaps
3276
+
3277
+ def showContextMenu(self, component, xy):
3278
+ menu = wx.Menu()
3279
+ # get ID
3280
+ # the ID is also the index to the element in the flow list
3281
+ compID = self._menuComponentID
3282
+ flow = self.frame.exp.flow
3283
+ component = flow[compID]
3284
+ compType = component.getType()
3285
+ if compType == 'Routine':
3286
+ for item in self.contextMenuItems:
3287
+ id = self.contextIDFromItem[item]
3288
+ menu.Append(id, self.contextMenuLabels[item])
3289
+ menu.Bind(wx.EVT_MENU, self.onContextSelect, id=id)
3290
+ self.frame.PopupMenu(menu, xy)
3291
+ # destroy to avoid mem leak:
3292
+ menu.Destroy()
3293
+ else:
3294
+ for item in self.contextMenuItems:
3295
+ if item == 'rename':
3296
+ continue
3297
+ id = self.contextIDFromItem[item]
3298
+ menu.Append(id, self.contextMenuLabels[item])
3299
+ menu.Bind(wx.EVT_MENU, self.onContextSelect, id=id)
3300
+ self.frame.PopupMenu(menu, xy)
3301
+ # destroy to avoid mem leak:
3302
+ menu.Destroy()
3303
+
3304
+ def onContextSelect(self, event):
3305
+ """Perform a given action on the component chosen
3306
+ """
3307
+ # get ID
3308
+ op = self.contextItemFromID[event.GetId()]
3309
+ # the ID is also the index to the element in the flow list
3310
+ compID = self._menuComponentID
3311
+ flow = self.frame.exp.flow
3312
+ component = flow[compID]
3313
+ # if we have a Loop Initiator, remove the whole loop
3314
+ if component.getType() == 'LoopInitiator':
3315
+ component = component.loop
3316
+ if op == 'remove':
3317
+ self.removeComponent(component, compID)
3318
+ self.frame.addToUndoStack(
3319
+ "REMOVE `%s` from Flow" % component.params['name'])
3320
+ if op == 'rename':
3321
+ self.frame.renameRoutine(component)
3322
+
3323
+ def removeComponent(self, component, compID):
3324
+ """Remove either a Routine or a Loop from the Flow
3325
+ """
3326
+ flow = self.frame.exp.flow
3327
+ if component.getType() in ['Routine'] + list(getAllStandaloneRoutines()):
3328
+ # check whether this will cause a collapsed loop
3329
+ # prev and next elements on flow are a loop init/end
3330
+ prevIsLoop = nextIsLoop = False
3331
+ if compID > 0: # there is at least one preceding
3332
+ prevIsLoop = (flow[compID - 1]).getType() == 'LoopInitiator'
3333
+ if len(flow) > (compID + 1): # there is at least one more compon
3334
+ nextIsLoop = (flow[compID + 1]).getType() == 'LoopTerminator'
3335
+ if prevIsLoop and nextIsLoop:
3336
+ # because flow[compID+1] is a terminator
3337
+ loop = flow[compID + 1].loop
3338
+ msg = _translate('The "%s" Loop is about to be deleted as '
3339
+ 'well (by collapsing). OK to proceed?')
3340
+ title = _translate('Impending Loop collapse')
3341
+ warnDlg = dialogs.MessageDialog(
3342
+ parent=self.frame, message=msg % loop.params['name'],
3343
+ type='Warning', title=title)
3344
+ resp = warnDlg.ShowModal()
3345
+ if resp in [wx.ID_CANCEL, wx.ID_NO]:
3346
+ return # abort
3347
+ elif resp == wx.ID_YES:
3348
+ # make recursive calls to this same method until success
3349
+ # remove the loop first
3350
+ self.removeComponent(loop, compID)
3351
+ # because the loop has been removed ID is now one less
3352
+ self.removeComponent(component, compID - 1)
3353
+ return # have done the removal in final successful call
3354
+ # remove name from namespace only if it's a loop;
3355
+ # loops exist only in the flow
3356
+ elif 'conditionsFile' in component.params:
3357
+ conditionsFile = component.params['conditionsFile'].val
3358
+ if conditionsFile and conditionsFile not in ['None', '']:
3359
+ try:
3360
+ trialList, fieldNames = data.importConditions(
3361
+ conditionsFile, returnFieldNames=True)
3362
+ for fname in fieldNames:
3363
+ self.frame.exp.namespace.remove(fname)
3364
+ except Exception:
3365
+ msg = ("Conditions file %s couldn't be found so names not"
3366
+ " removed from namespace")
3367
+ logging.debug(msg % conditionsFile)
3368
+ self.frame.exp.namespace.remove(component.params['name'].val)
3369
+ # perform the actual removal
3370
+ flow.removeComponent(component, id=compID)
3371
+ self.draw()
3372
+ # enable/disable add loop button
3373
+ self.btnInsertLoop.Enable(bool(len(flow)))
3374
+
3375
+ def OnPaint(self, event):
3376
+ # Create a buffered paint DC. It will create the real
3377
+ # wx.PaintDC and then blit the bitmap to it when dc is
3378
+ # deleted.
3379
+ dc = wx.GCDC(wx.BufferedPaintDC(self))
3380
+ # use PrepareDC to set position correctly
3381
+ self.PrepareDC(dc)
3382
+ # we need to clear the dc BEFORE calling PrepareDC
3383
+ bg = wx.Brush(self.GetBackgroundColour())
3384
+ dc.SetBackground(bg)
3385
+ dc.Clear()
3386
+ # create a clipping rect from our position and size
3387
+ # and the Update Region
3388
+ xv, yv = self.GetViewStart()
3389
+ dx, dy = self.GetScrollPixelsPerUnit()
3390
+ x, y = (xv * dx, yv * dy)
3391
+ rgn = self.GetUpdateRegion()
3392
+ rgn.Offset(x, y)
3393
+ r = rgn.GetBox()
3394
+ # draw to the dc using the calculated clipping rect
3395
+ self.pdc.DrawToDCClipped(dc, r)
3396
+
3397
+ def draw(self, evt=None):
3398
+ """This is the main function for drawing the Flow panel.
3399
+ It should be called whenever something changes in the exp.
3400
+
3401
+ This then makes calls to other drawing functions,
3402
+ like drawEntryPoints...
3403
+ """
3404
+ if not hasattr(self.frame, 'exp'):
3405
+ # we haven't yet added an exp
3406
+ return
3407
+ # retrieve the current flow from the experiment
3408
+ expFlow = self.frame.exp.flow
3409
+ pdc = self.pdc
3410
+
3411
+ # use the ID of the drawn icon to retrieve component (loop or routine)
3412
+ self.componentFromID = {}
3413
+
3414
+ pdc.Clear() # clear the screen
3415
+ pdc.RemoveAll() # clear all objects (icon buttons)
3416
+
3417
+ font = self.GetFont()
3418
+
3419
+ # draw the main time line
3420
+ self.linePos = (2.5 * self.dpi, 0.5 * self.dpi) # x,y of start
3421
+ gap = self.dpi / (6, 4, 2)[self.appData['flowSize']]
3422
+ dLoopToBaseLine = (15, 25, 43)[self.appData['flowSize']]
3423
+ dBetweenLoops = (20, 24, 30)[self.appData['flowSize']]
3424
+
3425
+ # guess virtual size; nRoutines wide by nLoops high
3426
+ # make bigger than needed and shrink later
3427
+ nRoutines = len(expFlow)
3428
+ nLoops = 0
3429
+ for entry in expFlow:
3430
+ if entry.getType() == 'LoopInitiator':
3431
+ nLoops += 1
3432
+ sizeX = nRoutines * self.dpi * 2
3433
+ sizeY = nLoops * dBetweenLoops + dLoopToBaseLine * 3
3434
+ self.SetVirtualSize(size=(sizeX, sizeY))
3435
+
3436
+ # step through components in flow, get spacing from text size, etc
3437
+ currX = self.linePos[0]
3438
+ lineId = wx.NewIdRef()
3439
+ pdc.SetPen(wx.Pen(colour=colors.app['fl_flowline_bg']))
3440
+ pdc.DrawLine(x1=self.linePos[0] - gap, y1=self.linePos[1],
3441
+ x2=self.linePos[0], y2=self.linePos[1])
3442
+ # NB the loop is itself the key, value is further info about it
3443
+ self.loops = {}
3444
+ nestLevel = 0
3445
+ maxNestLevel = 0
3446
+ self.gapMidPoints = [currX - gap / 2]
3447
+ self.gapNestLevels = [0]
3448
+ for ii, entry in enumerate(expFlow):
3449
+ if entry.getType() == 'LoopInitiator':
3450
+ # NB the loop is itself the dict key!?
3451
+ self.loops[entry.loop] = {
3452
+ 'init': currX, 'nest': nestLevel, 'id': ii}
3453
+ nestLevel += 1 # start of loop so increment level of nesting
3454
+ maxNestLevel = max(nestLevel, maxNestLevel)
3455
+ elif entry.getType() == 'LoopTerminator':
3456
+ # NB the loop is itself the dict key!
3457
+ self.loops[entry.loop]['term'] = currX
3458
+ nestLevel -= 1 # end of loop so decrement level of nesting
3459
+ elif entry.getType() == 'Routine' or entry.getType() in getAllStandaloneRoutines():
3460
+ # just get currX based on text size, don't draw anything yet:
3461
+ currX = self.drawFlowRoutine(pdc, entry, id=ii,
3462
+ pos=[currX, self.linePos[1] - 10],
3463
+ draw=False)
3464
+ self.gapMidPoints.append(currX + gap / 2)
3465
+ self.gapNestLevels.append(nestLevel)
3466
+ pdc.SetId(lineId)
3467
+ pdc.SetPen(wx.Pen(colour=colors.app['fl_flowline_bg']))
3468
+ pdc.DrawLine(x1=currX, y1=self.linePos[1],
3469
+ x2=currX + gap, y2=self.linePos[1])
3470
+ currX += gap
3471
+ lineRect = wx.Rect(self.linePos[0] - 2, self.linePos[1] - 2,
3472
+ currX - self.linePos[0] + 2, 4)
3473
+ pdc.SetIdBounds(lineId, lineRect)
3474
+
3475
+ # draw the loops first:
3476
+ maxHeight = 0
3477
+ for thisLoop in self.loops:
3478
+ thisInit = self.loops[thisLoop]['init']
3479
+ thisTerm = self.loops[thisLoop]['term']
3480
+ thisNest = maxNestLevel - self.loops[thisLoop]['nest'] - 1
3481
+ thisId = self.loops[thisLoop]['id']
3482
+ height = (self.linePos[1] + dLoopToBaseLine +
3483
+ thisNest * dBetweenLoops)
3484
+ self.drawLoop(pdc, thisLoop, id=thisId,
3485
+ startX=thisInit, endX=thisTerm,
3486
+ base=self.linePos[1], height=height)
3487
+ self.drawLoopStart(pdc, pos=[thisInit, self.linePos[1]])
3488
+ self.drawLoopEnd(pdc, pos=[thisTerm, self.linePos[1]])
3489
+ if height > maxHeight:
3490
+ maxHeight = height
3491
+
3492
+ # draw routines second (over loop lines):
3493
+ currX = self.linePos[0]
3494
+ for ii, entry in enumerate(expFlow):
3495
+ if entry.getType() == 'Routine' or entry.getType() in getAllStandaloneRoutines():
3496
+ currX = self.drawFlowRoutine(pdc, entry, id=ii,
3497
+ pos=[currX, self.linePos[1] - 10])
3498
+ pdc.SetPen(wx.Pen(wx.Pen(colour=colors.app['fl_flowline_bg'])))
3499
+ pdc.DrawLine(x1=currX, y1=self.linePos[1],
3500
+ x2=currX + gap, y2=self.linePos[1])
3501
+ currX += gap
3502
+
3503
+ self.SetVirtualSize(size=(currX + 100, maxHeight + 50))
3504
+
3505
+ self.drawLineStart(pdc, (self.linePos[0] - gap, self.linePos[1]))
3506
+ self.drawLineEnd(pdc, (currX, self.linePos[1]))
3507
+
3508
+ # refresh the visible window after drawing (using OnPaint)
3509
+ self.Refresh()
3510
+
3511
+ def drawEntryPoints(self, posList):
3512
+ ptSize = (3, 4, 5)[self.appData['flowSize']]
3513
+ for n, pos in enumerate(posList):
3514
+ if n >= len(self.entryPointPosList):
3515
+ # draw for first time
3516
+ id = wx.NewIdRef()
3517
+ self.entryPointIDlist.append(id)
3518
+ self.pdc.SetId(id)
3519
+ self.pdc.SetBrush(wx.Brush(colors.app['fl_flowline_bg']))
3520
+ self.pdc.DrawCircle(pos, self.linePos[1], ptSize)
3521
+ r = self.pdc.GetIdBounds(id)
3522
+ self.OffsetRect(r)
3523
+ self.RefreshRect(r, False)
3524
+ elif pos == self.entryPointPosList[n]:
3525
+ pass # nothing to see here, move along please :-)
3526
+ else:
3527
+ # move to new position
3528
+ dx = pos - self.entryPointPosList[n]
3529
+ dy = 0
3530
+ r = self.pdc.GetIdBounds(self.entryPointIDlist[n])
3531
+ self.pdc.TranslateId(self.entryPointIDlist[n], dx, dy)
3532
+ r2 = self.pdc.GetIdBounds(self.entryPointIDlist[n])
3533
+ # combine old and new locations to get redraw area
3534
+ rectToRedraw = r.Union(r2)
3535
+ rectToRedraw.Inflate(4, 4)
3536
+ self.OffsetRect(rectToRedraw)
3537
+ self.RefreshRect(rectToRedraw, False)
3538
+
3539
+ self.entryPointPosList = posList
3540
+ # refresh the visible window after drawing (using OnPaint)
3541
+ self.Refresh()
3542
+
3543
+ def setDrawPoints(self, ptType, startPoint=None):
3544
+ """Set the points of 'routines', 'loops', or None
3545
+ """
3546
+ if ptType == 'routines':
3547
+ self.pointsToDraw = self.gapMidPoints
3548
+ elif ptType == 'loops':
3549
+ self.pointsToDraw = self.gapMidPoints
3550
+ else:
3551
+ self.pointsToDraw = []
3552
+
3553
+ def drawLineStart(self, dc, pos):
3554
+ # draw bar at start of timeline; circle looked bad, offset vertically
3555
+ ptSize = (9, 9, 12)[self.appData['flowSize']]
3556
+ thic = (1, 1, 2)[self.appData['flowSize']]
3557
+ dc.SetBrush(wx.Brush(colors.app['fl_flowline_bg']))
3558
+ dc.SetPen(wx.Pen(colors.app['fl_flowline_bg']))
3559
+ dc.DrawPolygon([[0, -ptSize], [thic, -ptSize],
3560
+ [thic, ptSize], [0, ptSize]], pos[0], pos[1])
3561
+
3562
+ def drawLineEnd(self, dc, pos):
3563
+ # draws arrow at end of timeline
3564
+ # tmpId = wx.NewIdRef()
3565
+ # dc.SetId(tmpId)
3566
+ dc.SetBrush(wx.Brush(colors.app['fl_flowline_bg']))
3567
+ dc.SetPen(wx.Pen(colors.app['fl_flowline_bg']))
3568
+ dc.DrawPolygon([[0, -3], [5, 0], [0, 3]], pos[0], pos[1])
3569
+ # dc.SetIdBounds(tmpId,wx.Rect(pos[0],pos[1]+3,5,6))
3570
+
3571
+ def drawLoopEnd(self, dc, pos, downwards=True):
3572
+ # define the right side of a loop but draw nothing
3573
+ # idea: might want an ID for grabbing and relocating the loop endpoint
3574
+ tmpId = wx.NewIdRef()
3575
+ dc.SetId(tmpId)
3576
+ # dc.SetBrush(wx.Brush(wx.Colour(0,0,0, 250)))
3577
+ # dc.SetPen(wx.Pen(wx.Colour(0,0,0, 255)))
3578
+ size = (3, 4, 5)[self.appData['flowSize']]
3579
+ # if downwards:
3580
+ # dc.DrawPolygon([[size, 0], [0, size], [-size, 0]],
3581
+ # pos[0], pos[1] + 2 * size) # points down
3582
+ # else:
3583
+ # dc.DrawPolygon([[size, size], [0, 0], [-size, size]],
3584
+ # pos[0], pos[1]-3*size) # points up
3585
+ dc.SetIdBounds(tmpId, wx.Rect(
3586
+ pos[0] - size, pos[1] - size, 2 * size, 2 * size))
3587
+ return
3588
+
3589
+ def drawLoopStart(self, dc, pos, downwards=True):
3590
+ # draws direction arrow on left side of a loop
3591
+ tmpId = wx.NewIdRef()
3592
+ dc.SetId(tmpId)
3593
+ dc.SetBrush(wx.Brush(colors.app['fl_flowline_bg']))
3594
+ dc.SetPen(wx.Pen(colors.app['fl_flowline_bg']))
3595
+ size = (3, 4, 5)[self.appData['flowSize']]
3596
+ offset = (3, 2, 0)[self.appData['flowSize']]
3597
+ if downwards:
3598
+ dc.DrawPolygon([[size, size], [0, 0], [-size, size]],
3599
+ pos[0], pos[1] + 3 * size - offset) # points up
3600
+ else:
3601
+ dc.DrawPolygon([[size, 0], [0, size], [-size, 0]],
3602
+ pos[0], pos[1] - 4 * size) # points down
3603
+ dc.SetIdBounds(tmpId, wx.Rect(
3604
+ pos[0] - size, pos[1] - size, 2 * size, 2 * size))
3605
+
3606
+ def drawFlowRoutine(self, dc, routine, id, pos=(0, 0), draw=True):
3607
+ """Draw a box to show a routine on the timeline
3608
+ draw=False is for a dry-run, esp to compute and return size
3609
+ without drawing or setting a pdc ID
3610
+ """
3611
+ name = routine.name
3612
+ if self.appData['flowSize'] == 0 and len(name) > 5:
3613
+ name = ' ' + name[:4] + '..'
3614
+ else:
3615
+ name = ' ' + name + ' '
3616
+ if draw:
3617
+ dc.SetId(id)
3618
+ font = self.GetFont()
3619
+ if sys.platform == 'darwin':
3620
+ fontSizeDelta = (9, 6, 0)[self.appData['flowSize']]
3621
+ font.SetPointSize(1400 / self.dpi - fontSizeDelta)
3622
+ elif sys.platform.startswith('linux'):
3623
+ fontSizeDelta = (6, 4, 0)[self.appData['flowSize']]
3624
+ font.SetPointSize(1400 / self.dpi - fontSizeDelta)
3625
+ else:
3626
+ fontSizeDelta = (8, 4, 0)[self.appData['flowSize']]
3627
+ font.SetPointSize(1000 / self.dpi - fontSizeDelta)
3628
+
3629
+ maxTime, nonSlip = routine.getMaxTime()
3630
+ if hasattr(routine, "disabled") and routine.disabled:
3631
+ rtFill = colors.app['rt_comp_disabled']
3632
+ rtEdge = colors.app['rt_comp_disabled']
3633
+ rtText = colors.app['fl_routine_fg']
3634
+ elif nonSlip:
3635
+ rtFill = colors.app['fl_routine_bg_nonslip']
3636
+ rtEdge = colors.app['fl_routine_bg_nonslip']
3637
+ rtText = colors.app['fl_routine_fg']
3638
+ else:
3639
+ rtFill = colors.app['fl_routine_bg_slip']
3640
+ rtEdge = colors.app['fl_routine_bg_slip']
3641
+ rtText = colors.app['fl_routine_fg']
3642
+
3643
+ # get size based on text
3644
+ self.SetFont(font)
3645
+ if draw:
3646
+ dc.SetFont(font)
3647
+ w, h = self.GetFullTextExtent(name)[0:2]
3648
+ pad = (5, 10, 20)[self.appData['flowSize']]
3649
+ # draw box
3650
+ rect = wx.Rect(pos[0], pos[1] + 2 - self.appData['flowSize'],
3651
+ w + pad, h + pad)
3652
+ endX = pos[0] + w + pad
3653
+ # the edge should match the text
3654
+ if draw:
3655
+ dc.SetPen(wx.Pen(wx.Colour(rtEdge[0], rtEdge[1],
3656
+ rtEdge[2], wx.ALPHA_OPAQUE)))
3657
+ dc.SetBrush(wx.Brush(rtFill))
3658
+ dc.DrawRoundedRectangle(
3659
+ rect, (4, 6, 8)[self.appData['flowSize']])
3660
+ # draw text
3661
+ dc.SetTextForeground(rtText)
3662
+ dc.DrawLabel(name, rect, alignment=wx.ALIGN_CENTRE)
3663
+ if nonSlip and self.appData['flowSize'] != 0:
3664
+ font.SetPointSize(font.GetPointSize() * 0.6)
3665
+ dc.SetFont(font)
3666
+ _align = wx.ALIGN_CENTRE | wx.ALIGN_BOTTOM
3667
+ dc.DrawLabel("(%.2fs)" % maxTime, rect, alignment=_align)
3668
+
3669
+ self.componentFromID[id] = routine
3670
+ # set the area for this component
3671
+ dc.SetIdBounds(id, rect)
3672
+
3673
+ return endX
3674
+
3675
+ def drawLoop(self, dc, loop, id, startX, endX,
3676
+ base, height, downwards=True):
3677
+ if downwards:
3678
+ up = -1
3679
+ else:
3680
+ up = +1
3681
+
3682
+ # draw loop itself, as transparent rect with curved corners
3683
+ tmpId = wx.NewIdRef()
3684
+ dc.SetId(tmpId)
3685
+ # extra distance, in both h and w for curve
3686
+ curve = (6, 11, 15)[self.appData['flowSize']]
3687
+ yy = [base, height + curve * up, height +
3688
+ curve * up / 2, height] # for area
3689
+ dc.SetPen(wx.Pen(colors.app['fl_flowline_bg']))
3690
+ vertOffset = 0 # 1 is interesting too
3691
+ area = wx.Rect(startX, base + vertOffset,
3692
+ endX - startX, max(yy) - min(yy))
3693
+ dc.SetBrush(wx.Brush(wx.Colour(0, 0, 0, 0), style=wx.TRANSPARENT))
3694
+ # draws outline:
3695
+ dc.DrawRoundedRectangle(area, curve)
3696
+ dc.SetIdBounds(tmpId, area)
3697
+
3698
+ flowsize = self.appData['flowSize'] # 0, 1, or 2
3699
+
3700
+ # add a name label, loop info, except at smallest size
3701
+ name = loop.params['name'].val
3702
+ _show = self.appData['showLoopInfoInFlow']
3703
+ if _show and flowsize:
3704
+ _cond = 'conditions' in list(loop.params)
3705
+ if _cond and loop.params['conditions'].val:
3706
+ xnumTrials = 'x' + str(len(loop.params['conditions'].val))
3707
+ else:
3708
+ xnumTrials = ''
3709
+ name += ' (' + str(loop.params['nReps'].val) + xnumTrials
3710
+ abbrev = ['', # for flowsize == 0
3711
+ {'random': 'rand.',
3712
+ 'sequential': 'sequ.',
3713
+ 'fullRandom': 'f-ran.',
3714
+ 'staircase': 'stair.',
3715
+ 'interleaved staircases': "int-str."},
3716
+ {'random': 'random',
3717
+ 'sequential': 'sequential',
3718
+ 'fullRandom': 'fullRandom',
3719
+ 'staircase': 'staircase',
3720
+ 'interleaved staircases': "interl'vd stairs"}]
3721
+ name += ' ' + abbrev[flowsize][loop.params['loopType'].val] + ')'
3722
+ if flowsize == 0:
3723
+ if len(name) > 9:
3724
+ name = ' ' + name[:8] + '..'
3725
+ else:
3726
+ name = ' ' + name[:9]
3727
+ else:
3728
+ name = ' ' + name + ' '
3729
+
3730
+ dc.SetId(id)
3731
+ font = self.GetFont()
3732
+ if sys.platform == 'darwin':
3733
+ basePtSize = (650, 750, 900)[flowsize]
3734
+ elif sys.platform.startswith('linux'):
3735
+ basePtSize = (750, 850, 1000)[flowsize]
3736
+ else:
3737
+ basePtSize = (700, 750, 800)[flowsize]
3738
+ font.SetPointSize(basePtSize / self.dpi)
3739
+ self.SetFont(font)
3740
+ dc.SetFont(font)
3741
+
3742
+ # get size based on text
3743
+ pad = (5, 8, 10)[self.appData['flowSize']]
3744
+ w, h = self.GetFullTextExtent(name)[0:2]
3745
+ x = startX + (endX - startX) / 2 - w / 2 - pad / 2
3746
+ y = (height - h / 2)
3747
+
3748
+ # draw box
3749
+ rect = wx.Rect(x, y, w + pad, h + pad)
3750
+ # the edge should match the text
3751
+ dc.SetPen(wx.Pen(colors.app['fl_flowline_bg']))
3752
+ # try to make the loop fill brighter than the background canvas:
3753
+ dc.SetBrush(wx.Brush(colors.app['fl_flowline_bg']))
3754
+
3755
+ dc.DrawRoundedRectangle(rect, (4, 6, 8)[flowsize])
3756
+ # draw text
3757
+ dc.SetTextForeground(colors.app['fl_flowline_fg'])
3758
+ dc.DrawText(name, x + pad / 2, y + pad / 2)
3759
+
3760
+ self.componentFromID[id] = loop
3761
+ # set the area for this component
3762
+ dc.SetIdBounds(id, rect)
3763
+
3764
+
3765
+ class BuilderToolbar(BasePsychopyToolbar):
3766
+ def makeTools(self):
3767
+ # Clear any existing tools
3768
+ self.ClearTools()
3769
+ self.buttons = {}
3770
+
3771
+ # New
3772
+ self.buttons['filenew'] = self.makeTool(
3773
+ name='filenew',
3774
+ label=_translate('New'),
3775
+ shortcut='new',
3776
+ tooltip=_translate("Create new experiment file"),
3777
+ func=self.frame.app.newBuilderFrame
3778
+ )
3779
+ # Open
3780
+ self.buttons['fileopen'] = self.makeTool(
3781
+ name='fileopen',
3782
+ label=_translate('Open'),
3783
+ shortcut='open',
3784
+ tooltip=_translate("Open an existing experiment file"),
3785
+ func=self.frame.fileOpen)
3786
+ # Save
3787
+ self.buttons['filesave'] = self.makeTool(
3788
+ name='filesave',
3789
+ label=_translate('Save'),
3790
+ shortcut='save',
3791
+ tooltip=_translate("Save current experiment file"),
3792
+ func=self.frame.fileSave)
3793
+ self.frame.bldrBtnSave = self.buttons['filesave']
3794
+ # SaveAs
3795
+ self.buttons['filesaveas'] = self.makeTool(
3796
+ name='filesaveas',
3797
+ label=_translate('Save As...'),
3798
+ shortcut='saveAs',
3799
+ tooltip=_translate("Save current experiment file as..."),
3800
+ func=self.frame.fileSaveAs)
3801
+ # Undo
3802
+ self.buttons['undo'] = self.makeTool(
3803
+ name='undo',
3804
+ label=_translate('Undo'),
3805
+ shortcut='undo',
3806
+ tooltip=_translate("Undo last action"),
3807
+ func=self.frame.undo)
3808
+ self.frame.bldrBtnUndo = self.buttons['undo']
3809
+ # Redo
3810
+ self.buttons['redo'] = self.makeTool(
3811
+ name='redo',
3812
+ label=_translate('Redo'),
3813
+ shortcut='redo',
3814
+ tooltip=_translate("Redo last action"),
3815
+ func=self.frame.redo)
3816
+ self.frame.bldrBtnRedo = self.buttons['redo']
3817
+
3818
+ self.AddSeparator()
3819
+
3820
+ # Monitor Center
3821
+ self.buttons['monitors'] = self.makeTool(
3822
+ name='monitors',
3823
+ label=_translate('Monitor Center'),
3824
+ shortcut='none',
3825
+ tooltip=_translate("Monitor settings and calibration"),
3826
+ func=self.frame.app.openMonitorCenter)
3827
+ # Settings
3828
+ self.buttons['cogwindow'] = self.makeTool(
3829
+ name='cogwindow',
3830
+ label=_translate('Experiment Settings'),
3831
+ shortcut='none',
3832
+ tooltip=_translate("Edit experiment settings"),
3833
+ func=self.frame.setExperimentSettings)
3834
+
3835
+ self.AddSeparator()
3836
+
3837
+ # Compile Py
3838
+ self.buttons['compile_py'] = self.makeTool(
3839
+ name='compile_py',
3840
+ label=_translate('Compile Python Script'),
3841
+ shortcut='compileScript',
3842
+ tooltip=_translate("Compile to Python script"),
3843
+ func=self.frame.compileScript)
3844
+ # Compile JS
3845
+ self.buttons['compile_js'] = self.makeTool(
3846
+ name='compile_js',
3847
+ label=_translate('Compile JS Script'),
3848
+ shortcut='compileScript',
3849
+ tooltip=_translate("Compile to JS script"),
3850
+ func=self.frame.fileExport)
3851
+ # Send to runner
3852
+ self.buttons['runner'] = self.makeTool(
3853
+ name='runner',
3854
+ label=_translate('Runner'),
3855
+ shortcut='runnerScript',
3856
+ tooltip=_translate("Send experiment to Runner"),
3857
+ func=self.frame.runFile)
3858
+ self.frame.bldrBtnRunner = self.buttons['runner']
3859
+ # Run
3860
+ self.buttons['run'] = self.makeTool(
3861
+ name='run',
3862
+ label=_translate('Run'),
3863
+ shortcut='runScript',
3864
+ tooltip=_translate("Run experiment"),
3865
+ func=self.frame.runFile)
3866
+ self.frame.bldrBtnRun = self.buttons['run']
3867
+
3868
+ self.AddSeparator()
3869
+
3870
+ # Pavlovia run
3871
+ self.buttons['pavloviaRun'] = self.makeTool(
3872
+ name='globe_run',
3873
+ label=_translate("Run online"),
3874
+ tooltip=_translate("Run the study online (with pavlovia.org)"),
3875
+ func=self.frame.onPavloviaRun)
3876
+ # Pavlovia sync
3877
+ self.buttons['pavloviaSync'] = self.makeTool(
3878
+ name='globe_greensync',
3879
+ label=_translate("Sync online"),
3880
+ tooltip=_translate("Sync with web project (at pavlovia.org)"),
3881
+ func=self.frame.onPavloviaSync)
3882
+ # Pavlovia search
3883
+ self.buttons['pavloviaSearch'] = self.makeTool(
3884
+ name='globe_magnifier',
3885
+ label=_translate("Search Pavlovia.org"),
3886
+ tooltip=_translate("Find existing studies online (at pavlovia.org)"),
3887
+ func=self.onPavloviaSearch)
3888
+ # Pavlovia user
3889
+ self.buttons['pavloviaUser'] = self.makeTool(
3890
+ name='globe_user',
3891
+ label=_translate("Current Pavlovia user"),
3892
+ tooltip=_translate("Log in/out of Pavlovia.org, view your user profile."),
3893
+ func=self.onPavloviaUser)
3894
+ # Pavlovia user
3895
+ self.buttons['pavloviaProject'] = self.makeTool(
3896
+ name='globe_info',
3897
+ label=_translate("View project"),
3898
+ tooltip=_translate("View details of this project"),
3899
+ func=self.onPavloviaProject)
3900
+
3901
+ # Disable compile buttons until an experiment is present
3902
+ self.EnableTool(self.buttons['compile_py'].GetId(), Path(str(self.frame.filename)).is_file())
3903
+ self.EnableTool(self.buttons['compile_js'].GetId(), Path(str(self.frame.filename)).is_file())
3904
+
3905
+ self.frame.btnHandles = self.buttons
3906
+
3907
+ def onPavloviaSearch(self, evt=None):
3908
+ searchDlg = SearchFrame(
3909
+ app=self.frame.app, parent=self.frame,
3910
+ pos=self.frame.GetPosition())
3911
+ searchDlg.Show()
3912
+
3913
+ def onPavloviaUser(self, evt=None):
3914
+ userDlg = UserFrame(self.frame)
3915
+ userDlg.ShowModal()
3916
+
3917
+ def onPavloviaProject(self, evt=None):
3918
+ if self.frame.project is not None:
3919
+ dlg = ProjectFrame(app=self.frame.app,
3920
+ project=self.frame.project)
3921
+ else:
3922
+ dlg = ProjectFrame(app=self.frame.app)
3923
+ dlg.Show()
3924
+
3925
+
3926
+ def extractText(stream):
3927
+ """Take a byte stream (or any file object of type b?) and return
3928
+
3929
+ :param stream: stream from wx.Process or any byte stream from a file
3930
+ :return: text converted to unicode ready for appending to wx text view
3931
+ """
3932
+ return stream.read().decode('utf-8')