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,1679 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ # Part of the PsychoPy library
5
+ # Copyright (C) 2002-2018 Jonathan Peirce (C) 2019-2021 Open Science Tools Ltd.
6
+ # Distributed under the terms of the GNU General Public License (GPL).
7
+
8
+ """Dialog classes for the Builder, including ParamCtrls
9
+ """
10
+
11
+ from __future__ import absolute_import, division, print_function
12
+ import sys
13
+
14
+ from builtins import map
15
+ from builtins import str
16
+ from builtins import object
17
+ import os
18
+ import copy
19
+ from collections import OrderedDict
20
+
21
+ import numpy
22
+ import re
23
+ import wx
24
+
25
+ import psychopy.experiment.utils
26
+ from psychopy.experiment import Param
27
+
28
+ from ... import dialogs
29
+ from .. import experiment
30
+ from .. validators import NameValidator, CodeSnippetValidator, WarningManager
31
+ from .dlgsConditions import DlgConditions
32
+ from .dlgsCode import DlgCodeComponentProperties, CodeBox
33
+ from . import paramCtrls
34
+ from psychopy import data, logging
35
+ from psychopy.localization import _translate
36
+ from psychopy.tools import versionchooser as vc
37
+ from ...colorpicker import PsychoColorPicker
38
+ from pathlib import Path
39
+
40
+ from ...themes import ThemeMixin
41
+
42
+ white = wx.Colour(255, 255, 255, 255)
43
+ codeSyntaxOkay = wx.Colour(220, 250, 220, 255) # light green
44
+
45
+ from ..localizedStrings import _localizedDialogs as _localized
46
+
47
+
48
+ class ParamCtrls(object):
49
+
50
+ def __init__(self, dlg, label, param, parent, fieldName,
51
+ browse=False, noCtrls=False, advanced=False, appPrefs=None):
52
+ """Create a set of ctrls for a particular Component Parameter, to be
53
+ used in Component Properties dialogs. These need to be positioned
54
+ by the calling dlg.
55
+
56
+ e.g.::
57
+
58
+ param = experiment.Param(val='boo', valType='str')
59
+ ctrls = ParamCtrls(dlg=self, label=fieldName,param=param)
60
+ self.paramCtrls[fieldName] = ctrls # keep track in the dlg
61
+ sizer.Add(ctrls.nameCtrl, (currRow,0), (1,1),wx.ALIGN_RIGHT )
62
+ sizer.Add(ctrls.valueCtrl, (currRow,1) )
63
+ # these are optional (the parameter might be None)
64
+ if ctrls.typeCtrl:
65
+ sizer.Add(ctrls.typeCtrl, (currRow,2) )
66
+ if ctrls.updateCtrl:
67
+ sizer.Add(ctrls.updateCtrl, (currRow,3))
68
+
69
+ If browse is True then a browseCtrl will be added (you need to
70
+ bind events yourself). If noCtrls is True then no actual wx widgets
71
+ are made, but attribute names are created
72
+
73
+ `fieldName`'s value is always in en_US, and never for display,
74
+ whereas `label` is only for display and can be translated or
75
+ tweaked (e.g., add '$'). Component._localized.keys() are
76
+ `fieldName`s, and .values() are `label`s.
77
+ """
78
+ super(ParamCtrls, self).__init__()
79
+ self.param = param
80
+ self.dlg = dlg
81
+ self.dpi = self.dlg.dpi
82
+ self.valueWidth = self.dpi * 3.5
83
+ # try to find the experiment
84
+ self.exp = None
85
+ tryForExp = self.dlg
86
+
87
+ while self.exp is None:
88
+ if hasattr(tryForExp, 'frame'):
89
+ self.exp = tryForExp.frame.exp
90
+ else:
91
+ try:
92
+ tryForExp = tryForExp.parent # try going up a level
93
+ except Exception:
94
+ tryForExp.parent
95
+
96
+ # param has the fields:
97
+ # val, valType, allowedVals=[],allowedTypes=[],
98
+ # hint="", updates=None, allowedUpdates=None
99
+ # we need the following:
100
+ self.nameCtrl = self.valueCtrl = self.typeCtrl = None
101
+ self.updateCtrl = self.browseCtrl = None
102
+ if noCtrls:
103
+ return # we don't need to do any more
104
+
105
+ if type(param.val) == numpy.ndarray:
106
+ initial = param.val.tolist() # convert numpy arrays to lists
107
+ label = _translate(label)
108
+ self.nameCtrl = wx.StaticText(parent, -1, label, size=wx.DefaultSize)
109
+
110
+ if fieldName == 'Use version':
111
+ # _localVersionsCache is the default (faster) when creating
112
+ # settings. If remote info has become available in the meantime,
113
+ # now populate with that as well
114
+ if vc._remoteVersionsCache:
115
+ options = vc._versionFilter(vc.versionOptions(local=False), wx.__version__)
116
+ versions = vc._versionFilter(vc.availableVersions(local=False), wx.__version__)
117
+ param.allowedVals = (options + [''] + versions)
118
+
119
+ if param.inputType == "single":
120
+ # Create single line string control
121
+ self.valueCtrl = paramCtrls.SingleLineCtrl(parent,
122
+ val=str(param.val), valType=param.valType,
123
+ fieldName=fieldName,size=wx.Size(self.valueWidth, 24))
124
+ elif param.inputType == 'multi':
125
+ # Create multiline string control
126
+ self.valueCtrl = paramCtrls.MultiLineCtrl(parent,
127
+ val=str(param.val), valType=param.valType,
128
+ fieldName=fieldName, size=wx.Size(self.valueWidth, 144))
129
+ # Set focus if field is text of a Textbox or Text component
130
+ if fieldName == 'text':
131
+ self.valueCtrl.SetFocus()
132
+ elif param.inputType == 'spin':
133
+ # Create single line string control
134
+ self.valueCtrl = paramCtrls.SingleLineCtrl(parent,
135
+ val=str(param.val), valType=param.valType,
136
+ fieldName=fieldName,size=wx.Size(self.valueWidth, 24))
137
+ # Will have to disable spinCtrl until we have a dropdown for inputType, sadly
138
+ # self.valueCtrl = paramCtrls.IntCtrl(parent,
139
+ # val=param.val, valType=param.valType,
140
+ # fieldName=fieldName,size=wx.Size(self.valueWidth, 24),
141
+ # limits=param.allowedVals)
142
+ elif param.inputType == 'choice':
143
+ self.valueCtrl = paramCtrls.ChoiceCtrl(parent,
144
+ val=str(param.val), valType=param.valType, choices=param.allowedVals,
145
+ fieldName=fieldName, size=wx.Size(self.valueWidth, 24))
146
+ elif param.inputType == 'multiChoice':
147
+ self.valueCtrl = paramCtrls.MultiChoiceCtrl(parent, valType=param.valType,
148
+ vals=param.val, choices=param.allowedVals, fieldName=fieldName,
149
+ size=wx.Size(self.valueWidth, -1))
150
+ elif param.inputType == 'bool':
151
+ self.valueCtrl = paramCtrls.BoolCtrl(parent,
152
+ name=fieldName,size=wx.Size(self.valueWidth, 24))
153
+ self.valueCtrl.SetValue(bool(param.val))
154
+ elif param.inputType == 'file' or browse:
155
+ self.valueCtrl = paramCtrls.FileCtrl(parent,
156
+ val=str(param.val), valType=param.valType,
157
+ fieldName=fieldName, size=wx.Size(self.valueWidth, 24))
158
+ self.valueCtrl.allowedVals = param.allowedVals
159
+ elif param.inputType == 'fileList':
160
+ self.valueCtrl = paramCtrls.FileListCtrl(parent,
161
+ choices=param.val, valType=param.valType,
162
+ size=wx.Size(self.valueWidth, 100), pathtype="rel")
163
+ elif param.inputType == 'table':
164
+ self.valueCtrl = paramCtrls.TableCtrl(parent, val=param.val, valType=param.valType,
165
+ fieldName=fieldName, size=wx.Size(self.valueWidth, 24))
166
+ elif param.inputType == 'color':
167
+ self.valueCtrl = paramCtrls.ColorCtrl(parent,
168
+ val=param.val, valType=param.valType,
169
+ fieldName=fieldName, size=wx.Size(self.valueWidth, 24))
170
+ elif param.inputType == 'dict':
171
+ self.valueCtrl = paramCtrls.DictCtrl(parent,
172
+ val=self.exp.settings.getInfo(), valType=param.valType,
173
+ fieldName=fieldName)
174
+ else:
175
+ self.valueCtrl = paramCtrls.SingleLineCtrl(parent,
176
+ val=str(param.val), valType=param.valType,
177
+ fieldName=fieldName,size=wx.Size(self.valueWidth, 24))
178
+ logging.warn(f"Parameter {fieldName} has unrecognised inputType \"{param.inputType}\"")
179
+
180
+ # if fieldName == 'Experiment info':
181
+ # # for expInfo convert from a string to the list-of-dicts
182
+ # val = self.expInfoToListWidget(param.val)
183
+ # self.valueCtrl = dialogs.ListWidget(
184
+ # parent, val, order=['Field', 'Default'])
185
+
186
+ try:
187
+ self.valueCtrl.SetToolTip(wx.ToolTip(_translate(param.hint)))
188
+ except AttributeError as e:
189
+ self.valueCtrl.SetToolTipString(_translate(param.hint))
190
+
191
+ if len(param.allowedVals) == 1 or param.readOnly:
192
+ self.valueCtrl.Disable() # visible but can't be changed
193
+
194
+ # add a Validator to the valueCtrl
195
+ if fieldName == "name":
196
+ self.valueCtrl.SetValidator(NameValidator())
197
+ elif param.inputType in ("single", "multi"):
198
+ # only want anything that is valType code, or can be with $
199
+ self.valueCtrl.SetValidator(CodeSnippetValidator(fieldName))
200
+
201
+ # create the type control
202
+ if len(param.allowedTypes):
203
+ # are there any components with non-empty allowedTypes?
204
+ self.typeCtrl = wx.Choice(parent, choices=param.allowedTypes)
205
+ self.typeCtrl._choices = copy.copy(param.allowedTypes)
206
+ index = param.allowedTypes.index(param.valType)
207
+ self.typeCtrl.SetSelection(index)
208
+ if len(param.allowedTypes) == 1:
209
+ self.typeCtrl.Disable() # visible but can't be changed
210
+
211
+ # create update control
212
+ if param.allowedUpdates is not None and len(param.allowedUpdates):
213
+ # updates = display-only version of allowed updates
214
+ updateLabels = [_localized[upd] for upd in param.allowedUpdates]
215
+ # allowedUpdates = extend version of allowed updates that includes
216
+ # "set during:static period"
217
+ allowedUpdates = copy.copy(param.allowedUpdates)
218
+ for routineName, routine in list(self.exp.routines.items()):
219
+ for static in routine.getStatics():
220
+ # Note: replacing following line with
221
+ # "localizedMsg = _translate(msg)",
222
+ # poedit would not able to find this message.
223
+ msg = "set during: "
224
+ localizedMsg = _translate("set during: ")
225
+ fullName = "{}.{}".format(
226
+ routineName, static.params['name'])
227
+ allowedUpdates.append(msg + fullName)
228
+ updateLabels.append(localizedMsg + fullName)
229
+ self.updateCtrl = wx.Choice(parent, choices=updateLabels)
230
+ # stash non-localized choices to allow retrieval by index:
231
+ self.updateCtrl._choices = copy.copy(allowedUpdates)
232
+ # If parameter isn't in list, default to the first choice
233
+ if param.updates not in allowedUpdates:
234
+ param.updates = allowedUpdates[0]
235
+ # get index of the currently set update value, set display:
236
+ index = allowedUpdates.index(param.updates)
237
+ # set by integer index, not string value
238
+ self.updateCtrl.SetSelection(index)
239
+
240
+ if param.allowedUpdates != None and len(param.allowedUpdates) == 1:
241
+ self.updateCtrl.Disable() # visible but can't be changed
242
+
243
+ def _getCtrlValue(self, ctrl):
244
+ """Retrieve the current value form the control (whatever type of ctrl
245
+ it is, e.g. checkbox.GetValue, choice.GetSelection)
246
+ Different types of control have different methods for retrieving
247
+ value. This function checks them all and returns the value or None.
248
+
249
+ .. note::
250
+ Don't use GetStringSelection() here to avoid that translated value
251
+ is returned. Instead, use GetSelection() to get index of selection
252
+ and get untranslated value from _choices attribute.
253
+ """
254
+ if ctrl is None:
255
+ return None
256
+ elif hasattr(ctrl, 'GetText'):
257
+ return ctrl.GetText()
258
+ elif hasattr(ctrl, 'GetValue'): # e.g. TextCtrl
259
+ val = ctrl.GetValue()
260
+ if isinstance(self.valueCtrl, dialogs.ListWidget):
261
+ val = self.expInfoFromListWidget(val)
262
+ return val
263
+ elif hasattr(ctrl, 'GetCheckedStrings'):
264
+ return ctrl.GetCheckedStrings()
265
+ elif hasattr(ctrl, 'GetSelection'): # for wx.Choice
266
+ # _choices is defined during __init__ for all wx.Choice() ctrls
267
+ # NOTE: add untranslated value to _choices if
268
+ # _choices[ctrl.GetSelection()] fails.
269
+ return ctrl._choices[ctrl.GetSelection()]
270
+ elif hasattr(ctrl, 'GetLabel'): # for wx.StaticText
271
+ return ctrl.GetLabel()
272
+ else:
273
+ print("failed to retrieve the value for %s" % ctrl)
274
+ return None
275
+
276
+ def _setCtrlValue(self, ctrl, newVal):
277
+ """Set the current value of the control (whatever type of ctrl it
278
+ is, e.g. checkbox.SetValue, choice.SetSelection)
279
+ Different types of control have different methods for retrieving
280
+ value. This function checks them all and returns the value or None.
281
+
282
+ .. note::
283
+ Don't use SetStringSelection() here to avoid using translated
284
+ value. Instead, get index of the value using _choices attribute
285
+ and use SetSelection() to set the value.
286
+ """
287
+ if ctrl is None:
288
+ return None
289
+ elif hasattr(ctrl, 'SetValue'): # e.g. TextCtrl
290
+ ctrl.SetValue(newVal)
291
+ elif hasattr(ctrl, 'SetSelection'): # for wx.Choice
292
+ # _choices = list of non-localized strings, set during __init__
293
+ # NOTE: add untranslated value to _choices if
294
+ # _choices.index(newVal) fails.
295
+ index = ctrl._choices.index(newVal)
296
+ # set the display to the localized version of the string:
297
+ ctrl.SetSelection(index)
298
+ elif hasattr(ctrl, 'SetLabel'): # for wx.StaticText
299
+ ctrl.SetLabel(newVal)
300
+ else:
301
+ print("failed to retrieve the value for %s" % (ctrl))
302
+
303
+ def getValue(self):
304
+ """Get the current value of the value ctrl
305
+ """
306
+ return self._getCtrlValue(self.valueCtrl)
307
+
308
+ def setValue(self, newVal):
309
+ """Get the current value of the value ctrl
310
+ """
311
+ return self._setCtrlValue(self.valueCtrl, newVal)
312
+
313
+ def getType(self):
314
+ """Get the current value of the type ctrl
315
+ """
316
+ if self.typeCtrl:
317
+ return self._getCtrlValue(self.typeCtrl)
318
+
319
+ def getUpdates(self):
320
+ """Get the current value of the updates ctrl
321
+ """
322
+ if self.updateCtrl:
323
+ return self._getCtrlValue(self.updateCtrl)
324
+
325
+ def setVisible(self, newVal=True):
326
+ if hasattr(self.valueCtrl, "ShowAll"):
327
+ self.valueCtrl.ShowAll(newVal)
328
+ else:
329
+ self.valueCtrl.Show(newVal)
330
+ self.nameCtrl.Show(newVal)
331
+ if self.updateCtrl:
332
+ self.updateCtrl.Show(newVal)
333
+ if self.typeCtrl:
334
+ self.typeCtrl.Show(newVal)
335
+
336
+ def expInfoToListWidget(self, expInfoStr):
337
+ """Takes a string describing a dictionary and turns it into a format
338
+ that the ListWidget can receive.
339
+
340
+ returns: list of dicts of {Field:'', Default:''}
341
+ """
342
+ expInfo = self.exp.settings.getInfo()
343
+
344
+ listOfDicts = []
345
+ for field, default in list(expInfo.items()):
346
+ listOfDicts.append({'Field': field, 'Default': default})
347
+ return listOfDicts
348
+
349
+ def expInfoFromListWidget(self, listOfDicts):
350
+ """Creates a string representation of a dict from a list of
351
+ field / default values.
352
+ """
353
+ expInfo = {}
354
+ for field in listOfDicts:
355
+ expInfo[field['Field']] = field['Default']
356
+ expInfoStr = repr(expInfo)
357
+ return expInfoStr
358
+
359
+ def setChangesCallback(self, callbackFunction):
360
+ """Set a callback to detect any changes in this value (whether it's
361
+ a checkbox event or a text event etc
362
+
363
+ :param callbackFunction: the function to be called when the valueCtrl
364
+ changes value
365
+ :return:
366
+ """
367
+ if isinstance(self.valueCtrl, wx.TextCtrl):
368
+ self.valueCtrl.Bind(wx.EVT_KEY_UP, callbackFunction)
369
+ elif isinstance(self.valueCtrl, CodeBox):
370
+ self.valueCtrl.Bind(wx.stc.EVT_STC_CHANGE, callbackFunction)
371
+ elif isinstance(self.valueCtrl, wx.ComboBox):
372
+ self.valueCtrl.Bind(wx.EVT_COMBOBOX, callbackFunction)
373
+ elif isinstance(self.valueCtrl, wx.Choice):
374
+ self.valueCtrl.Bind(wx.EVT_CHOICE, callbackFunction)
375
+ elif isinstance(self.valueCtrl, wx.CheckListBox):
376
+ self.valueCtrl.Bind(wx.EVT_CHECKLISTBOX, callbackFunction)
377
+ elif isinstance(self.valueCtrl, wx.CheckBox):
378
+ self.valueCtrl.Bind(wx.EVT_CHECKBOX, callbackFunction)
379
+ elif isinstance(self.valueCtrl, (paramCtrls.DictCtrl, paramCtrls.FileListCtrl)):
380
+ pass
381
+ else:
382
+ print("setChangesCallback doesn't know how to handle ctrl {}"
383
+ .format(type(self.valueCtrl)))
384
+
385
+
386
+ class StartStopCtrls(wx.GridBagSizer):
387
+ def __init__(self, parent, params):
388
+ wx.GridBagSizer.__init__(self, 0, 0)
389
+ # Make ctrls
390
+ self.ctrls = {}
391
+ for name, param in params.items():
392
+ if name in ['startVal', 'stopVal']:
393
+ self.ctrls[name] = wx.TextCtrl(parent,
394
+ value=str(param.val), size=wx.Size(-1, 24))
395
+ self.label = wx.StaticText(parent, label=param.label)
396
+ self.Add(self.ctrls[name], (0, 1), border=6, flag=wx.EXPAND | wx.TOP)
397
+ if name in ['startType', 'stopType']:
398
+ localizedChoices = list(map(_translate, param.allowedVals))
399
+ self.ctrls[name] = wx.Choice(parent,
400
+ <<<<<<< HEAD
401
+ choices=param.allowedVals or [param.val],
402
+ =======
403
+ choices=localizedChoices,
404
+ >>>>>>> 8531e762b38a68bca6708110a4caa707d9d960ca
405
+ size=wx.Size(96, 24))
406
+ self.ctrls[name]._choices = copy.copy(param.allowedVals)
407
+ self.ctrls[name].SetSelection(param.allowedVals.index(str(param.val)))
408
+ self.Add(self.ctrls[name], (0, 0), border=6, flag=wx.EXPAND | wx.TOP)
409
+ if name in ['startEstim', 'durationEstim']:
410
+ self.ctrls[name] = wx.TextCtrl(parent,
411
+ value=str(param.val), size=wx.Size(-1, 24))
412
+ self.estimLabel = wx.StaticText(parent,
413
+ label=param.label, size=wx.Size(-1, 24))
414
+ self.estimLabel.SetForegroundColour("grey")
415
+ self.Add(self.estimLabel, (1, 0), border=6, flag=wx.EXPAND | wx.ALL)
416
+ self.Add(self.ctrls[name], (1, 1), border=6, flag=wx.EXPAND | wx.TOP | wx.BOTTOM)
417
+ self.AddGrowableCol(1)
418
+
419
+
420
+ class ParamNotebook(wx.Notebook, ThemeMixin):
421
+ class CategoryPage(wx.Panel, ThemeMixin):
422
+ def __init__(self, parent, dlg, params):
423
+ wx.Panel.__init__(self, parent, size=(600, -1))
424
+ self.parent = parent
425
+ self.dlg = dlg
426
+ self.app = self.dlg.app
427
+ # Setup sizer
428
+ self.sizer = wx.GridBagSizer(0, 0)
429
+ self.SetSizer(self.sizer)
430
+ # Add empty row for spacing
431
+ self.sizer.Add(wx.StaticText(self, label=""), (0, 0))
432
+ # Add controls
433
+ self.ctrls = {}
434
+ self.row = 1
435
+ # Sort params
436
+ sortedParams = OrderedDict(params)
437
+ for name in reversed(self.parent.element.order):
438
+ if name in sortedParams:
439
+ sortedParams.move_to_end(name, last=False)
440
+ # Make name ctrl
441
+ if "name" in sortedParams:
442
+ param = sortedParams.pop("name")
443
+ self.addParam("name", param)
444
+ # Make start controls
445
+ startParams = OrderedDict()
446
+ for name in ['startVal', 'startType', 'startEstim']:
447
+ if name in sortedParams:
448
+ startParams[name] = sortedParams.pop(name)
449
+ if startParams:
450
+ self.addStartStopCtrl(startParams)
451
+ # Make stop controls
452
+ stopParams = OrderedDict()
453
+ for name in ['stopVal', 'stopType', 'durationEstim']:
454
+ if name in sortedParams:
455
+ stopParams[name] = sortedParams.pop(name)
456
+ if stopParams:
457
+ self.addStartStopCtrl(stopParams)
458
+ # Make controls
459
+ for name, param in sortedParams.items():
460
+ self.addParam(name, param)
461
+ # Add growable
462
+ self.sizer.AddGrowableCol(1, 1)
463
+ # Check depends
464
+ self.checkDepends()
465
+
466
+ def addParam(self, name, param):
467
+ # Make ctrl
468
+ self.ctrls[name] = ParamCtrls(self.dlg, param.label, param, self, name)
469
+ # Add value ctrl
470
+ _flag = wx.EXPAND | wx.ALL
471
+ if hasattr(self.ctrls[name].valueCtrl, '_szr'):
472
+ self.sizer.Add(self.ctrls[name].valueCtrl._szr, (self.row, 1), border=6, flag=_flag)
473
+ else:
474
+ self.sizer.Add(self.ctrls[name].valueCtrl, (self.row, 1), border=6, flag=_flag)
475
+ # Add other ctrl stuff
476
+ _flag = wx.LEFT | wx.RIGHT | wx.ALIGN_CENTER_VERTICAL
477
+ self.sizer.Add(self.ctrls[name].nameCtrl, (self.row, 0), (1, 1), border=5, flag=_flag)
478
+ if self.ctrls[name].typeCtrl:
479
+ self.sizer.Add(self.ctrls[name].typeCtrl, (self.row, 2), border=5, flag=_flag)
480
+ if self.ctrls[name].updateCtrl:
481
+ self.sizer.Add(self.ctrls[name].updateCtrl, (self.row, 3), border=5, flag=_flag)
482
+ # Link to depends callback
483
+ self.ctrls[name].setChangesCallback(self.doValidate)
484
+ if name == 'name':
485
+ self.ctrls[name].valueCtrl.SetFocus()
486
+ # Iterate row
487
+ self.row += 1
488
+
489
+ def addStartStopCtrl(self, params):
490
+ # Make controls
491
+ panel = StartStopCtrls(self, params)
492
+ # Add to dict of ctrls
493
+ self.ctrls.update(panel.ctrls)
494
+ # Add label
495
+ _flag = wx.LEFT | wx.RIGHT | wx.ALIGN_CENTER_VERTICAL
496
+ self.sizer.Add(panel.label, (self.row, 0), (1, 1), border=5, flag=_flag)
497
+ # Add ctrls
498
+ _flag = wx.EXPAND | wx.ALL
499
+ self.sizer.Add(panel, (self.row, 1), border=6, flag=_flag)
500
+ # Iterate row
501
+ self.row += 1
502
+
503
+ def checkDepends(self, event=None):
504
+ """Checks the relationships between params that depend on each other
505
+
506
+ Dependencies are a list of dicts like this (as in BaseComponent):
507
+ {"dependsOn": "shape",
508
+ "condition": "=='n vertices",
509
+ "param": "n vertices",
510
+ "true": "Enable", # what to do with param if condition is True
511
+ "false": "Disable", # permitted: hide, show, enable, disable
512
+ }"""
513
+ for thisDep in self.parent.element.depends:
514
+ if not (
515
+ thisDep['param'] in self.ctrls
516
+ and thisDep['dependsOn'] in self.ctrls):
517
+ # If params are on another page, skip
518
+ continue
519
+ dependentCtrls = self.ctrls[thisDep['param']]
520
+ dependencyCtrls = self.ctrls[thisDep['dependsOn']]
521
+ condString = "dependencyCtrls.getValue() {}".format(thisDep['condition'])
522
+ if eval(condString):
523
+ action = thisDep['true']
524
+ else:
525
+ action = thisDep['false']
526
+ if action == "hide":
527
+ dependentCtrls.setVisible(False)
528
+ elif action == "show":
529
+ dependentCtrls.setVisible(True)
530
+ else:
531
+ # if action is "enable" then do ctrl.Enable() etc
532
+ for ctrlName in ['valueCtrl', 'nameCtrl', 'updatesCtrl']:
533
+ # disable/enable all parts of the control
534
+ if hasattr(dependentCtrls, ctrlName):
535
+ evalStr = ("dependentCtrls.{}.{}()"
536
+ .format(ctrlName, action.title()))
537
+ eval(evalStr)
538
+ # Update sizer
539
+ self.sizer.SetEmptyCellSize((0, 0))
540
+ self.sizer.Layout()
541
+ self.Fit()
542
+ self.dlg.Fit()
543
+ self.Refresh()
544
+
545
+ def doValidate(self, event=None):
546
+ self.Validate()
547
+ self.checkDepends(event=event)
548
+ if hasattr(self.dlg, "updateExperiment"):
549
+ self.dlg.updateExperiment()
550
+
551
+ def _applyAppTheme(self, target=None):
552
+ self.SetBackgroundColour("white")
553
+
554
+ def __init__(self, parent, element, experiment):
555
+ wx.Notebook.__init__(self, parent)
556
+ self.parent = parent
557
+ self.exp = experiment
558
+ self.element = element
559
+ self.params = element.params
560
+ # Setup sizer
561
+ self.sizer = wx.BoxSizer(wx.VERTICAL)
562
+ self.SetSizer(self.sizer)
563
+ # Get arrays of params
564
+ paramsByCateg = OrderedDict()
565
+ for name, param in self.params.items():
566
+ # Add categ if not present
567
+ if param.categ not in paramsByCateg:
568
+ paramsByCateg[param.categ] = OrderedDict()
569
+ # Append param to categ
570
+ paramsByCateg[param.categ][name] = param
571
+ # Move high priority categs to the front
572
+ for categ in reversed(['Basic', 'Layout', 'Appearance', 'Formatting', 'Texture']):
573
+ if categ in paramsByCateg:
574
+ paramsByCateg.move_to_end(categ, last=False)
575
+ # Move low priority categs to the end
576
+ for categ in ['Data', 'Custom', 'Hardware', 'Testing']:
577
+ if categ in paramsByCateg:
578
+ paramsByCateg.move_to_end(categ, last=True)
579
+ # Setup pages
580
+ self.paramCtrls = {}
581
+ for categ, params in paramsByCateg.items():
582
+ page = self.CategoryPage(self, self.parent, params)
583
+ self.paramCtrls.update(page.ctrls)
584
+ # Add page to notebook
585
+ self.AddPage(page, _translate(categ))
586
+
587
+ def checkDepends(self, event=None):
588
+ """
589
+ When check depends is called on the whole notebook, check each page
590
+ """
591
+ for i in range(self.GetPageCount()):
592
+ self.GetPage(i).checkDepends(event)
593
+
594
+ def getParams(self):
595
+ """retrieves data from any fields in self.paramCtrls
596
+ (populated during the __init__ function)
597
+
598
+ The new data from the dlg get inserted back into the original params
599
+ used in __init__ and are also returned from this method.
600
+
601
+ .. note::
602
+ Don't use GetStringSelection() here to avoid that translated value
603
+ is returned. Instead, use GetSelection() to get index of selection
604
+ and get untranslated value from _choices attribute.
605
+ """
606
+ # get data from input fields
607
+ for fieldName in self.params:
608
+ param = self.params[fieldName]
609
+ # Get control
610
+ ctrl = self.paramCtrls[fieldName]
611
+ # Get value
612
+ if hasattr(ctrl, "getValue"):
613
+ param.val = ctrl.getValue()
614
+ elif isinstance(ctrl, wx.Choice):
615
+ if hasattr(ctrl, "_choices"):
616
+ param.val = ctrl._choices[ctrl.GetSelection()]
617
+ else:
618
+ # use GetStringSelection()
619
+ # only if this control doesn't has _choices
620
+ param.val = ctrl.GetStringSelection()
621
+ elif hasattr(ctrl, "GetValue"):
622
+ param.val = ctrl.GetValue()
623
+ # Get type
624
+ if hasattr(ctrl, "typeCtrl"):
625
+ if ctrl.typeCtrl:
626
+ param.valType = ctrl.typeCtrl._choices[ctrl.typeCtrl.GetSelection()]
627
+ # Get update type
628
+ if hasattr(ctrl, "updateCtrl"):
629
+ if ctrl.updateCtrl:
630
+ updates = ctrl.updateCtrl._choices[ctrl.updateCtrl.GetSelection()]
631
+ # may also need to update a static
632
+ if param.updates != updates:
633
+ self._updateStaticUpdates(fieldName,
634
+ param.updates, updates)
635
+ param.updates = updates
636
+ return self.params
637
+
638
+ def _updateStaticUpdates(self, fieldName, updates, newUpdates):
639
+ """If the old/new updates ctrl is using a Static component then we
640
+ need to remove/add the component name to the appropriate static
641
+ """
642
+ exp = self.exp
643
+ compName = self.params['name'].val
644
+ if hasattr(updates, 'startswith') and "during:" in updates:
645
+ # remove the part that says 'during'
646
+ updates = updates.split(': ')[1]
647
+ origRoutine, origStatic = updates.split('.')
648
+ _comp = exp.routines[origRoutine].getComponentFromName(origStatic)
649
+ if _comp is not None:
650
+ _comp.remComponentUpdate(origRoutine, compName, fieldName)
651
+ if hasattr(newUpdates, 'startswith') and "during:" in newUpdates:
652
+ # remove the part that says 'during'
653
+ newUpdates = newUpdates.split(': ')[1]
654
+ newRoutine, newStatic = newUpdates.split('.')
655
+ _comp = exp.routines[newRoutine].getComponentFromName(newStatic)
656
+ _comp.addComponentUpdate(newRoutine, compName, fieldName)
657
+
658
+
659
+ class _BaseParamsDlg(wx.Dialog):
660
+ _style = wx.DEFAULT_DIALOG_STYLE | wx.DIALOG_NO_PARENT | wx.TAB_TRAVERSAL
661
+
662
+ def __init__(self, frame, element, experiment,
663
+ suppressTitles=True,
664
+ showAdvanced=False,
665
+ size=wx.DefaultSize,
666
+ style=_style, editing=False,
667
+ timeout=None):
668
+
669
+ # translate title
670
+ if "name" in element.params:
671
+ title = element.params['name'].val + _translate(' Properties')
672
+ elif "expName" in element.params:
673
+ title = element.params['expName'].val + _translate(' Properties')
674
+ else:
675
+ title = "Properties"
676
+ # get help url
677
+ if hasattr(element, 'url'):
678
+ helpUrl = element.url
679
+ else:
680
+ helpUrl = None
681
+
682
+ # use translated title for display
683
+ wx.Dialog.__init__(self, parent=None, id=-1, title=title,
684
+ size=size, style=style)
685
+ self.frame = frame
686
+ self.app = frame.app
687
+ self.dpi = self.app.dpi
688
+ self.helpUrl = helpUrl
689
+ self.params = params = element.params # dict
690
+ self.title = title
691
+ self.timeout = timeout
692
+ if (not editing and
693
+ title != 'Experiment Settings' and
694
+ 'name' in self.params):
695
+ # then we're adding a new component, so provide known-valid name:
696
+ makeValid = self.frame.exp.namespace.makeValid
697
+ self.params['name'].val = makeValid(params['name'].val)
698
+ self.paramCtrls = {}
699
+ CodeSnippetValidator.clsWarnings = {}
700
+ self.suppressTitles = suppressTitles
701
+ self.showAdvanced = showAdvanced
702
+ self.order = element.order
703
+ self.depends = element.depends
704
+ self.data = []
705
+ # max( len(str(self.params[x])) for x in keys )
706
+ self.maxFieldLength = 10
707
+ self.timeParams = ['startType', 'startVal', 'stopType', 'stopVal']
708
+ self.codeFieldNameFromID = {}
709
+ self.codeIDFromFieldName = {}
710
+ # a list of all panels in the ctrl to be traversed by validator
711
+ self.panels = []
712
+ # need font size for STCs:
713
+ if wx.Platform == '__WXMSW__':
714
+ self.faceSize = 10
715
+ elif wx.Platform == '__WXMAC__':
716
+ self.faceSize = 14
717
+ else:
718
+ self.faceSize = 12
719
+
720
+ # create main sizer
721
+ self.mainSizer = wx.BoxSizer(wx.VERTICAL)
722
+
723
+ self.ctrls = ParamNotebook(self, element, experiment)
724
+ self.paramCtrls = self.ctrls.paramCtrls
725
+
726
+ self.mainSizer.Add(self.ctrls, # ctrls is the notebook of params
727
+ proportion=1, flag=wx.EXPAND | wx.ALL, border=5)
728
+
729
+ self.SetSizerAndFit(self.mainSizer)
730
+
731
+ def getParams(self):
732
+ return self.ctrls.getParams()
733
+
734
+ def openMonitorCenter(self, event):
735
+ self.app.openMonitorCenter(event)
736
+ self.paramCtrls['Monitor'].valueCtrl.SetFocus()
737
+ # need to delay until the user closes the monitor center
738
+ # self.paramCtrls['Monitor'].valueCtrl.Clear()
739
+ # if wx.TheClipboard.Open():
740
+ # dataObject = wx.TextDataObject()
741
+ # if wx.TheClipboard.GetData(dataObject):
742
+ # self.paramCtrls['Monitor'].valueCtrl.
743
+ # WriteText(dataObject.GetText())
744
+ # wx.TheClipboard.Close()
745
+
746
+ def launchColorPicker(self, event):
747
+ # bring up a colorPicker
748
+ dlg = PsychoColorPicker(self.frame)
749
+ dlg.ShowModal()
750
+ dlg.Destroy()
751
+
752
+ def onNewTextSize(self, event):
753
+ self.Fit() # for ExpandoTextCtrl this is needed
754
+
755
+ def show(self, testing=False):
756
+ """Adds an OK and cancel button, shows dialogue.
757
+
758
+ This method returns wx.ID_OK (as from ShowModal), but also
759
+ sets self.OK to be True or False
760
+ """
761
+ # add buttons for OK and Cancel
762
+ buttons = wx.BoxSizer(wx.HORIZONTAL)
763
+ # help button if we know the url
764
+ if self.helpUrl != None:
765
+ helpBtn = wx.Button(self, wx.ID_HELP, _translate(" Help "))
766
+ _tip = _translate("Go to online help about this component")
767
+ helpBtn.SetToolTip(wx.ToolTip(_tip))
768
+ helpBtn.Bind(wx.EVT_BUTTON, self.onHelp)
769
+ buttons.Add(helpBtn, 0,
770
+ flag=wx.LEFT | wx.ALL | wx.ALIGN_CENTER_VERTICAL,
771
+ border=3)
772
+ self.OKbtn = wx.Button(self, wx.ID_OK, _translate(" OK "))
773
+ # intercept OK button if a loop dialog, in case file name was edited:
774
+ if type(self) == DlgLoopProperties:
775
+ self.OKbtn.Bind(wx.EVT_BUTTON, self.onOK)
776
+ self.OKbtn.SetDefault()
777
+ CANCEL = wx.Button(self, wx.ID_CANCEL, _translate(" Cancel "))
778
+
779
+ # Add validator stuff
780
+ self.warnings = WarningManager(self)
781
+ self.mainSizer.Add(self.warnings.output, border=3, flag=wx.EXPAND | wx.ALL)
782
+ self.Validate() # disables OKbtn if bad name, syntax error, etc
783
+
784
+ buttons.AddStretchSpacer()
785
+
786
+ # Add Okay and Cancel buttons
787
+ if sys.platform == "win32":
788
+ btns = [self.OKbtn, CANCEL]
789
+ else:
790
+ btns = [CANCEL, self.OKbtn]
791
+ buttons.Add(btns[0], 0,
792
+ wx.ALL | wx.RIGHT | wx.ALIGN_CENTER_VERTICAL,
793
+ border=3)
794
+ buttons.Add(btns[1], 0,
795
+ wx.ALL | wx.RIGHT | wx.ALIGN_CENTER_VERTICAL,
796
+ border=3)
797
+
798
+ # buttons.Realize()
799
+ # add to sizer
800
+ self.mainSizer.Add(buttons, flag=wx.ALL | wx.EXPAND, border=3)
801
+ self.SetSizerAndFit(self.mainSizer)
802
+ self.mainSizer.Layout()
803
+ # move the position to be v near the top of screen and
804
+ # to the right of the left-most edge of builder
805
+ builderPos = self.frame.GetPosition()
806
+ self.SetPosition((builderPos[0] + 200, 20))
807
+
808
+ # self.paramCtrls['name'].valueCtrl.SetFocus()
809
+ # do show and process return
810
+ if self.timeout is not None:
811
+ timeout = wx.CallLater(self.timeout, self.autoTerminate)
812
+ timeout.Start()
813
+ if testing:
814
+ self.Show()
815
+ else:
816
+ retVal = self.ShowModal()
817
+ self.OK = bool(retVal == wx.ID_OK)
818
+ return wx.ID_OK
819
+
820
+ def autoTerminate(self, event=None, retval=1):
821
+ """Terminates the dialog early, for use with a timeout
822
+
823
+ :param event: an optional wx.EVT
824
+ :param retval: an optional int to pass to EndModal()
825
+ :return:
826
+ """
827
+ self.EndModal(retval)
828
+
829
+ def Validate(self, *args, **kwargs):
830
+ """Validate form data and disable OK button if validation fails.
831
+ """
832
+ return self.ctrls.Validate()
833
+
834
+ def onOK(self, event=None):
835
+ """Handler for OK button which should validate dialog contents.
836
+ """
837
+ valid = self.Validate()
838
+ if not valid:
839
+ return
840
+ event.Skip()
841
+
842
+ def onTextEventCode(self, event=None):
843
+ """process text events for code components: change color to grey
844
+ """
845
+ codeBox = event.GetEventObject()
846
+ textBeforeThisKey = codeBox.GetText()
847
+ keyCode = event.GetKeyCode()
848
+ pos = event.GetPosition()
849
+ # ord(10)='\n', ord(13)='\l'
850
+ if keyCode < 256 and keyCode not in [10, 13]:
851
+ # new line is trigger to check syntax
852
+ codeBox.setStatus('changed')
853
+ elif (keyCode in (10, 13) and
854
+ len(textBeforeThisKey) and
855
+ textBeforeThisKey[-1] != ':'):
856
+ # ... but skip the check if end of line is colon ord(58)=':'
857
+ self._setNameColor(self._testCompile(codeBox))
858
+ event.Skip()
859
+
860
+ def _testCompile(self, ctrl, mode='exec'):
861
+ """checks whether code.val is legal python syntax,
862
+ returns error status
863
+
864
+ mode = 'exec' (statement or expr) or 'eval' (expr only)
865
+ """
866
+ if hasattr(ctrl, 'GetText'):
867
+ val = ctrl.GetText()
868
+ elif hasattr(ctrl, 'GetValue'):
869
+ # e.g. TextCtrl
870
+ val = ctrl.GetValue()
871
+ else:
872
+ msg = 'Unknown type of ctrl in _testCompile: %s'
873
+ raise ValueError(msg % (type(ctrl)))
874
+ try:
875
+ compile(val, '', mode)
876
+ syntaxOk = True
877
+ ctrl.setStatus('OK')
878
+ except SyntaxError:
879
+ ctrl.setStatus('error')
880
+ syntaxOk = False
881
+ return syntaxOk
882
+
883
+ def checkCodeSyntax(self, event=None):
884
+ """Checks syntax for whole code component by code box,
885
+ sets box bg-color.
886
+ """
887
+ if hasattr(event, 'GetEventObject'):
888
+ codeBox = event.GetEventObject()
889
+ elif hasattr(event, 'GetText'):
890
+ # we were given the control itself, not an event
891
+ codeBox = event
892
+ else:
893
+ msg = ('checkCodeSyntax received unexpected event object (%s). '
894
+ 'Should be a wx.Event or a CodeBox')
895
+ raise ValueError(msg % type(event))
896
+ text = codeBox.GetText()
897
+ if not text.strip():
898
+ # if basically empty
899
+ codeBox.SetBackgroundColour(white)
900
+ return
901
+ # test syntax:
902
+ goodSyntax = self._testCompile(codeBox)
903
+ # not quite every dialog has a name (e.g. settings)
904
+ # but if so then set its color
905
+ if 'name' in self.paramCtrls:
906
+ self._setNameColor(goodSyntax)
907
+
908
+ def _setNameColor(self, goodSyntax):
909
+ if goodSyntax:
910
+ _nValCtrl = self.paramCtrls['name'].valueCtrl
911
+ _nValCtrl.SetBackgroundColour(codeSyntaxOkay)
912
+ self.nameOKlabel.SetLabel('')
913
+ else:
914
+ self.paramCtrls['name'].valueCtrl.SetBackgroundColour(white)
915
+ self.nameOKlabel.SetLabel('syntax error')
916
+
917
+ def checkCodeWanted(self, event=None):
918
+ """check whether a $ is present (if so, set the display font)
919
+ """
920
+ if hasattr(event, 'GetEventObject'):
921
+ strBox = event.GetEventObject()
922
+ elif hasattr(event, 'GetValue'):
923
+ # we were given the control itself, not an event
924
+ strBox = event
925
+ else:
926
+ raise ValueError('checkCodeWanted received unexpected event'
927
+ ' object (%s).')
928
+ try:
929
+ val = strBox.GetValue()
930
+ stc = False
931
+ except Exception:
932
+ if not hasattr(strBox, 'GetText'):
933
+ # eg, wx.Choice control
934
+ if hasattr(event, 'Skip'):
935
+ event.Skip()
936
+ return
937
+ val = strBox.GetText()
938
+ # might be StyledTextCtrl
939
+ stc = True
940
+
941
+ if hasattr(event, 'Skip'):
942
+ event.Skip()
943
+
944
+ def _checkName(self, event=None, name=None):
945
+ """checks namespace, return error-msg (str), enable (bool)
946
+ """
947
+ if event:
948
+ newName = event.GetString()
949
+ elif name:
950
+ newName = name
951
+ elif hasattr(self, 'paramCtrls'):
952
+ newName = self.paramCtrls['name'].getValue()
953
+ elif hasattr(self, 'globalCtrls'):
954
+ newName = self.globalCtrls['name'].getValue()
955
+ if newName == '':
956
+ return _translate("Missing name"), False
957
+ else:
958
+ namespace = self.frame.exp.namespace
959
+ used = namespace.exists(newName)
960
+ sameOldName = bool(newName == self.params['name'].val)
961
+ if used and not sameOldName:
962
+ msg = _translate(
963
+ "That name is in use (it's a %s). Try another name.")
964
+ return msg % _translate(used), False
965
+ elif not namespace.isValid(newName): # valid as a var name
966
+ msg = _translate("Name must be alpha-numeric or _, no spaces")
967
+ return msg, False
968
+ # warn but allow, chances are good that its actually ok
969
+ elif namespace.isPossiblyDerivable(newName):
970
+ msg = namespace.isPossiblyDerivable(newName)
971
+ return msg, True
972
+ else:
973
+ return "", True
974
+
975
+ def onHelp(self, event=None):
976
+ """Uses self.app.followLink() to self.helpUrl
977
+ """
978
+ self.app.followLink(url=self.helpUrl)
979
+
980
+
981
+ class DlgLoopProperties(_BaseParamsDlg):
982
+ _style = wx.DEFAULT_DIALOG_STYLE | wx.DIALOG_NO_PARENT | wx.RESIZE_BORDER
983
+
984
+ def __init__(self, frame, title="Loop Properties", loop=None,
985
+ helpUrl=None, pos=wx.DefaultPosition, size=wx.DefaultSize,
986
+ style=_style, depends=[], timeout=None):
987
+ # translate title
988
+ localizedTitle = title.replace(' Properties',
989
+ _translate(' Properties'))
990
+
991
+ wx.Dialog.__init__(self, None, wx.ID_ANY, localizedTitle,
992
+ pos, size, style)
993
+ self.helpUrl = helpUrl
994
+ self.frame = frame
995
+ self.exp = frame.exp
996
+ self.app = frame.app
997
+ self.dpi = self.app.dpi
998
+ self.params = {}
999
+ self.timeout = timeout
1000
+ self.panel = wx.Panel(self, -1)
1001
+ self.globalCtrls = {}
1002
+ self.constantsCtrls = {}
1003
+ self.staircaseCtrls = {}
1004
+ self.multiStairCtrls = {}
1005
+ self.currentCtrls = {}
1006
+ self.data = []
1007
+ self.mainSizer = wx.BoxSizer(wx.VERTICAL)
1008
+ self.conditions = None
1009
+ self.conditionsFile = None
1010
+ # create a valid new name; save old name in case we need to revert
1011
+ namespace = frame.exp.namespace
1012
+ defaultName = namespace.makeValid('trials')
1013
+ oldLoopName = defaultName
1014
+ if loop:
1015
+ oldLoopName = loop.params['name'].val
1016
+
1017
+ # create default instances of the diff loop types
1018
+ # for 'random','sequential', 'fullRandom'
1019
+ self.trialHandler = experiment.loops.TrialHandler(
1020
+ exp=self.exp, name=oldLoopName, loopType='random',
1021
+ nReps=5, conditions=[])
1022
+ # for staircases:
1023
+ self.stairHandler = experiment.loops.StairHandler(
1024
+ exp=self.exp, name=oldLoopName, nReps=50, nReversals='',
1025
+ stepSizes='[0.8,0.8,0.4,0.4,0.2]', stepType='log', startVal=0.5)
1026
+ self.multiStairHandler = experiment.loops.MultiStairHandler(
1027
+ exp=self.exp, name=oldLoopName, nReps=50, stairType='simple',
1028
+ switchStairs='random', conditions=[], conditionsFile='')
1029
+
1030
+ # replace defaults with the loop we were given
1031
+ if loop is None:
1032
+ self.currentType = 'random'
1033
+ self.currentHandler = self.trialHandler
1034
+ elif loop.type == 'TrialHandler':
1035
+ self.conditions = loop.params['conditions'].val
1036
+ self.conditionsFile = loop.params['conditionsFile'].val
1037
+ self.trialHandler = self.currentHandler = loop
1038
+ # could be 'random', 'sequential', 'fullRandom'
1039
+ self.currentType = loop.params['loopType'].val
1040
+ elif loop.type == 'StairHandler':
1041
+ self.stairHandler = self.currentHandler = loop
1042
+ self.currentType = 'staircase'
1043
+ elif loop.type == 'MultiStairHandler':
1044
+ self.conditions = loop.params['conditions'].val
1045
+ self.conditionsFile = loop.params['conditionsFile'].val
1046
+ self.multiStairHandler = self.currentHandler = loop
1047
+ self.currentType = 'interleaved staircases'
1048
+ elif loop.type == 'QuestHandler':
1049
+ pass # what to do for quest?
1050
+ self.params['name'] = self.currentHandler.params['name']
1051
+ self.globalPanel = self.makeGlobalCtrls()
1052
+ self.stairPanel = self.makeStaircaseCtrls()
1053
+ # the controls for Method of Constants
1054
+ self.constantsPanel = self.makeConstantsCtrls()
1055
+ self.multiStairPanel = self.makeMultiStairCtrls()
1056
+ self.mainSizer.Add(self.globalPanel, border=12,
1057
+ flag=wx.ALL | wx.EXPAND)
1058
+ self.mainSizer.Add(wx.StaticLine(self), border=6,
1059
+ flag=wx.ALL | wx.EXPAND)
1060
+ self.mainSizer.Add(self.stairPanel, border=12,
1061
+ flag=wx.ALL | wx.EXPAND)
1062
+ self.mainSizer.Add(self.constantsPanel, border=12,
1063
+ flag=wx.ALL | wx.EXPAND)
1064
+ self.mainSizer.Add(self.multiStairPanel, border=12,
1065
+ flag=wx.ALL | wx.EXPAND)
1066
+ self.setCtrls(self.currentType)
1067
+ # create a list of panels in the dialog, for the validator to step
1068
+ # through
1069
+ self.panels = [self.globalPanel, self.stairPanel,
1070
+ self.constantsPanel, self.multiStairPanel]
1071
+
1072
+ self.params = {}
1073
+ self.params.update(self.trialHandler.params)
1074
+ self.params.update(self.stairHandler.params)
1075
+ self.params.update(self.multiStairHandler.params)
1076
+ self.paramCtrls = {}
1077
+ self.paramCtrls.update(self.globalCtrls)
1078
+ self.paramCtrls.update(self.constantsCtrls)
1079
+ self.paramCtrls.update(self.staircaseCtrls)
1080
+ self.paramCtrls.update(self.multiStairCtrls)
1081
+
1082
+ # show dialog and get most of the data
1083
+ self.show()
1084
+ if self.OK:
1085
+ self.params = self.getParams()
1086
+ # convert endPoints from str to list
1087
+ _endP = self.params['endPoints'].val
1088
+ self.params['endPoints'].val = eval("%s" % _endP)
1089
+ # then sort the list so the endpoints are in correct order
1090
+ self.params['endPoints'].val.sort()
1091
+ if loop:
1092
+ # editing an existing loop
1093
+ namespace.remove(oldLoopName)
1094
+ namespace.add(self.params['name'].val)
1095
+ # don't always have a conditionsFile
1096
+ if hasattr(self, 'condNamesInFile'):
1097
+ namespace.add(self.condNamesInFile)
1098
+ if hasattr(self, 'duplCondNames'):
1099
+ namespace.remove(self.duplCondNames)
1100
+ else:
1101
+ if loop is not None:
1102
+ # if we had a loop during init then revert to its old name
1103
+ loop.params['name'].val = oldLoopName
1104
+
1105
+ # make sure we set this back regardless of whether OK
1106
+ # otherwise it will be left as a summary string, not a conditions
1107
+ if 'conditionsFile' in self.currentHandler.params:
1108
+ self.currentHandler.params['conditions'].val = self.conditions
1109
+
1110
+ def Validate(self, *args, **kwargs):
1111
+ for ctrl in self.globalCtrls.values():
1112
+ checker = ctrl.valueCtrl.GetValidator()
1113
+ if checker:
1114
+ checker.Validate(self)
1115
+ return self.warnings.OK
1116
+
1117
+ def makeGlobalCtrls(self):
1118
+ panel = wx.Panel(parent=self)
1119
+ panelSizer = wx.GridBagSizer(0, 0)
1120
+ panel.SetSizer(panelSizer)
1121
+ row = 0
1122
+ for fieldName in ('name', 'loopType', 'isTrials'):
1123
+ try:
1124
+ label = self.currentHandler.params[fieldName].label
1125
+ except Exception:
1126
+ label = fieldName
1127
+ self.globalCtrls[fieldName] = ctrls = ParamCtrls(
1128
+ dlg=self, parent=panel, label=label, fieldName=fieldName,
1129
+ param=self.currentHandler.params[fieldName])
1130
+ panelSizer.Add(ctrls.nameCtrl, [row, 0], border=3,
1131
+ flag=wx.LEFT | wx.RIGHT | wx.ALIGN_CENTER_VERTICAL | wx.EXPAND)
1132
+ if hasattr(ctrls.valueCtrl, '_szr'):
1133
+ panelSizer.Add(ctrls.valueCtrl._szr, [row, 1], border=3,
1134
+ flag=wx.EXPAND | wx.ALL)
1135
+ else:
1136
+ panelSizer.Add(ctrls.valueCtrl, [row, 1], border=3,
1137
+ flag=wx.EXPAND | wx.ALL)
1138
+ row += 1
1139
+ panelSizer.AddGrowableCol(1, 1)
1140
+ self.globalCtrls['name'].valueCtrl.Bind(wx.EVT_TEXT, self.Validate)
1141
+ self.Bind(wx.EVT_CHOICE, self.onTypeChanged,
1142
+ self.globalCtrls['loopType'].valueCtrl)
1143
+ return panel
1144
+
1145
+ def makeConstantsCtrls(self):
1146
+ # a list of controls for the random/sequential versions
1147
+ # that can be hidden or shown
1148
+ handler = self.trialHandler
1149
+ # loop through the params
1150
+ keys = list(handler.params.keys())
1151
+ panel = wx.Panel(parent=self)
1152
+ panel.app=self.app
1153
+ panelSizer = wx.GridBagSizer(0, 0)
1154
+ panel.SetSizer(panelSizer)
1155
+ row = 0
1156
+ # add conditions stuff to the *end*
1157
+ if 'conditionsFile' in keys:
1158
+ keys.remove('conditionsFile')
1159
+ keys.append('conditionsFile')
1160
+ if 'conditions' in keys:
1161
+ keys.remove('conditions')
1162
+ keys.append('conditions')
1163
+ _flag = wx.LEFT | wx.RIGHT | wx.ALIGN_CENTER_VERTICAL | wx.EXPAND
1164
+ # then step through them
1165
+ for fieldName in keys:
1166
+ # try and get alternative "label" for the parameter
1167
+ try:
1168
+ label = self.currentHandler.params[fieldName].label
1169
+ if not label:
1170
+ # it might exist but be empty
1171
+ label = fieldName
1172
+ except Exception:
1173
+ label = fieldName
1174
+ # handle special cases
1175
+ if fieldName == 'endPoints':
1176
+ continue # this was deprecated in v1.62.00
1177
+ if fieldName in self.globalCtrls:
1178
+ # these have already been made and inserted into sizer
1179
+ ctrls = self.globalCtrls[fieldName]
1180
+ elif fieldName == 'conditions':
1181
+ if 'conditions' in handler.params:
1182
+ _cond = handler.params['conditions'].val
1183
+ text, OK = self.getTrialsSummary(_cond)
1184
+ else:
1185
+ text = _translate("No parameters set")
1186
+ OK = True # No condition file is not an error
1187
+ # we'll create our own widgets
1188
+ ctrls = ParamCtrls(dlg=self, parent=panel, label=label,
1189
+ fieldName=fieldName,
1190
+ param=text, noCtrls=True)
1191
+ ctrls.valueCtrl = wx.StaticText(
1192
+ panel, label=text, style=wx.ALIGN_RIGHT)
1193
+ if OK:
1194
+ ctrls.valueCtrl.SetForegroundColour("Black")
1195
+ else:
1196
+ ctrls.valueCtrl.SetForegroundColour("Red")
1197
+ if hasattr(ctrls.valueCtrl, "_szr"):
1198
+ panelSizer.Add(ctrls.valueCtrl._szr, (row, 1),
1199
+ flag=wx.ALIGN_RIGHT)
1200
+ else:
1201
+ panelSizer.Add(ctrls.valueCtrl, (row, 1),
1202
+ flag=wx.ALIGN_RIGHT)
1203
+ row += 1
1204
+ else: # normal text entry field
1205
+ ctrls = ParamCtrls(dlg=self, parent=panel, label=label,
1206
+ fieldName=fieldName,
1207
+ param=handler.params[fieldName])
1208
+ panelSizer.Add(ctrls.nameCtrl, [row, 0], border=3, flag=wx.LEFT | wx.RIGHT | wx.ALIGN_CENTER_VERTICAL)
1209
+ if hasattr(ctrls.valueCtrl, "_szr"):
1210
+ panelSizer.Add(ctrls.valueCtrl._szr, [row, 1], border=3, flag=wx.EXPAND | wx.ALL)
1211
+ else:
1212
+ panelSizer.Add(ctrls.valueCtrl, [row, 1], border=3, flag=wx.EXPAND | wx.ALL)
1213
+ row += 1
1214
+ # Link conditions file browse button to its own special method
1215
+ if fieldName == 'conditionsFile':
1216
+ ctrls.valueCtrl.findBtn.Bind(wx.EVT_BUTTON, self.onBrowseTrialsFile)
1217
+ # store info about the field
1218
+ self.constantsCtrls[fieldName] = ctrls
1219
+ panelSizer.AddGrowableCol(1, 1)
1220
+ return panel
1221
+
1222
+ def makeMultiStairCtrls(self):
1223
+ # a list of controls for the random/sequential versions
1224
+ panel = wx.Panel(parent=self)
1225
+ panel.app = self.app
1226
+ panelSizer = wx.GridBagSizer(0, 0)
1227
+ panel.SetSizer(panelSizer)
1228
+ row = 0
1229
+ # that can be hidden or shown
1230
+ handler = self.multiStairHandler
1231
+ # loop through the params
1232
+ keys = list(handler.params.keys())
1233
+ # add conditions stuff to the *end*
1234
+ # add conditions stuff to the *end*
1235
+ if 'conditionsFile' in keys:
1236
+ keys.remove('conditionsFile')
1237
+ keys.append('conditionsFile')
1238
+ if 'conditions' in keys:
1239
+ keys.remove('conditions')
1240
+ keys.append('conditions')
1241
+ # then step through them
1242
+ for fieldName in keys:
1243
+ # try and get alternative "label" for the parameter
1244
+ try:
1245
+ label = handler.params[fieldName].label
1246
+ if not label: # it might exist but be empty
1247
+ label = fieldName
1248
+ except Exception:
1249
+ label = fieldName
1250
+ # handle special cases
1251
+ if fieldName == 'endPoints':
1252
+ continue # this was deprecated in v1.62.00
1253
+ if fieldName in self.globalCtrls:
1254
+ # these have already been made and inserted into sizer
1255
+ ctrls = self.globalCtrls[fieldName]
1256
+ elif fieldName == 'conditions':
1257
+ if 'conditions' in handler.params:
1258
+ text, OK = self.getTrialsSummary(
1259
+ handler.params['conditions'].val)
1260
+ else:
1261
+ text = _translate(
1262
+ "No parameters set (select a file above)")
1263
+ OK = False
1264
+ # we'll create our own widgets
1265
+ ctrls = ParamCtrls(dlg=self, parent=panel, label=label,
1266
+ fieldName=fieldName,
1267
+ param=text, noCtrls=True)
1268
+ ctrls.valueCtrl = wx.StaticText(panel, label=text,
1269
+ style=wx.ALIGN_CENTER)
1270
+ if OK:
1271
+ ctrls.valueCtrl.SetForegroundColour("Black")
1272
+ else:
1273
+ ctrls.valueCtrl.SetForegroundColour("Red")
1274
+ if hasattr(ctrls.valueCtrl, "_szr"):
1275
+ panelSizer.Add(ctrls.valueCtrl._szr, (row, 0),
1276
+ span=(1, 3), flag=wx.ALIGN_CENTER)
1277
+ else:
1278
+ panelSizer.Add(ctrls.valueCtrl, (row, 0),
1279
+ span=(1, 3), flag=wx.ALIGN_CENTER)
1280
+ row += 1
1281
+ else:
1282
+ # normal text entry field
1283
+ ctrls = ParamCtrls(dlg=self, parent=panel, label=label,
1284
+ fieldName=fieldName,
1285
+ param=handler.params[fieldName])
1286
+ panelSizer.Add(ctrls.nameCtrl, [row, 0], border=3, flag=wx.LEFT | wx.RIGHT | wx.ALIGN_CENTER_VERTICAL)
1287
+ if hasattr(ctrls.valueCtrl, "_szr"):
1288
+ panelSizer.Add(ctrls.valueCtrl._szr, [row, 1], border=3, flag=wx.EXPAND | wx.ALL)
1289
+ else:
1290
+ panelSizer.Add(ctrls.valueCtrl, [row, 1], border=3, flag=wx.EXPAND | wx.ALL)
1291
+ row += 1
1292
+ # Bind file button with its own special method
1293
+ if fieldName == 'conditionsFile':
1294
+ ctrls.valueCtrl.findBtn.Bind(wx.EVT_BUTTON, self.onBrowseTrialsFile)
1295
+ # store info about the field
1296
+ self.multiStairCtrls[fieldName] = ctrls
1297
+ panelSizer.AddGrowableCol(1, 1)
1298
+ return panel
1299
+
1300
+ def makeStaircaseCtrls(self):
1301
+ """Setup the controls for a StairHandler
1302
+ """
1303
+ panel = wx.Panel(parent=self)
1304
+ panelSizer = wx.GridBagSizer(0, 0)
1305
+ panel.SetSizer(panelSizer)
1306
+ row = 0
1307
+ handler = self.stairHandler
1308
+ # loop through the params
1309
+ for fieldName in handler.params:
1310
+ # try and get alternative "label" for the parameter
1311
+ try:
1312
+ label = handler.params[fieldName].label
1313
+ if not label:
1314
+ # it might exist but be empty
1315
+ label = fieldName
1316
+ except Exception:
1317
+ label = fieldName
1318
+ # handle special cases
1319
+ if fieldName == 'endPoints':
1320
+ continue # this was deprecated in v1.62.00
1321
+ if fieldName in self.globalCtrls:
1322
+ # these have already been made and inserted into sizer
1323
+ ctrls = self.globalCtrls[fieldName]
1324
+ else: # normal text entry field
1325
+ ctrls = ParamCtrls(dlg=self, parent=panel, label=label,
1326
+ fieldName=fieldName,
1327
+ param=handler.params[fieldName])
1328
+ panelSizer.Add(ctrls.nameCtrl, [row, 0], border=3, flag=wx.LEFT | wx.RIGHT | wx.ALIGN_CENTER_VERTICAL)
1329
+ if hasattr(ctrls.valueCtrl, "_szr"):
1330
+ panelSizer.Add(ctrls.valueCtrl._szr, [row, 1], border=3, flag=wx.EXPAND | wx.ALL)
1331
+ else:
1332
+ panelSizer.Add(ctrls.valueCtrl, [row, 1], border=3, flag=wx.EXPAND | wx.ALL)
1333
+ row += 1
1334
+ # store info about the field
1335
+ self.staircaseCtrls[fieldName] = ctrls
1336
+ panelSizer.AddGrowableCol(1, 1)
1337
+ return panel
1338
+
1339
+ def getTrialsSummary(self, conditions):
1340
+ if type(conditions) == list and len(conditions) > 0:
1341
+ # get attr names (conditions[0].keys() inserts u'name' and u' is
1342
+ # annoying for novice)
1343
+ paramStr = "["
1344
+ for param in conditions[0]:
1345
+ paramStr += (str(param) + ', ')
1346
+ paramStr = paramStr[:-2] + "]" # remove final comma and add ]
1347
+ # generate summary info
1348
+ msg = _translate('%(nCondition)i conditions, with %(nParam)i '
1349
+ 'parameters\n%(paramStr)s')
1350
+ vals = {'nCondition': len(conditions),
1351
+ 'nParam': len(conditions[0]),
1352
+ 'paramStr': paramStr}
1353
+ return msg % vals, True
1354
+ else:
1355
+ if (self.conditionsFile and
1356
+ not os.path.isfile(self.conditionsFile)):
1357
+ return _translate("No parameters set (conditionsFile not found)"), False
1358
+ # No condition file is not an error
1359
+ return _translate("No parameters set"), True
1360
+
1361
+ def setCtrls(self, ctrlType):
1362
+ # choose the ctrls to show/hide
1363
+ if ctrlType == 'staircase':
1364
+ self.currentHandler = self.stairHandler
1365
+ self.stairPanel.Show()
1366
+ self.constantsPanel.Hide()
1367
+ self.multiStairPanel.Hide()
1368
+ self.currentCtrls = self.staircaseCtrls
1369
+ elif ctrlType == 'interleaved staircases':
1370
+ self.currentHandler = self.multiStairHandler
1371
+ self.stairPanel.Hide()
1372
+ self.constantsPanel.Hide()
1373
+ self.multiStairPanel.Show()
1374
+ self.currentCtrls = self.multiStairCtrls
1375
+ else:
1376
+ self.currentHandler = self.trialHandler
1377
+ self.stairPanel.Hide()
1378
+ self.constantsPanel.Show()
1379
+ self.multiStairPanel.Hide()
1380
+ self.currentCtrls = self.constantsCtrls
1381
+ self.currentType = ctrlType
1382
+ # redo layout
1383
+ self.mainSizer.Layout()
1384
+ self.Fit()
1385
+ self.Refresh()
1386
+
1387
+ @property
1388
+ def type(self):
1389
+ """What type of loop is represented by this dlg"""
1390
+ return self.currentHandler.type
1391
+
1392
+ @type.setter
1393
+ def type(self, value):
1394
+ self.setCtrls(value)
1395
+
1396
+ def onTypeChanged(self, evt=None):
1397
+ newType = evt.GetString()
1398
+ if newType == self.currentType:
1399
+ return
1400
+ self.setCtrls(newType)
1401
+
1402
+ def onBrowseTrialsFile(self, event):
1403
+ self.conditionsFileOrig = self.conditionsFile
1404
+ self.conditionsOrig = self.conditions
1405
+ expFolder, expName = os.path.split(self.frame.filename)
1406
+ dlg = wx.FileDialog(self, message=_translate("Open file ..."),
1407
+ style=wx.FD_OPEN, defaultDir=expFolder)
1408
+ if dlg.ShowModal() == wx.ID_OK:
1409
+ newFullPath = dlg.GetPath()
1410
+ if self.conditionsFile:
1411
+ _path = os.path.join(expFolder, self.conditionsFile)
1412
+ oldFullPath = os.path.abspath(_path)
1413
+ isSameFilePathAndName = bool(newFullPath == oldFullPath)
1414
+ else:
1415
+ isSameFilePathAndName = False
1416
+
1417
+ newPath = str(Path(newFullPath).relative_to(expFolder))
1418
+ self.conditionsFile = newPath
1419
+ needUpdate = False
1420
+ try:
1421
+ _c, _n = data.importConditions(dlg.GetPath(),
1422
+ returnFieldNames=True)
1423
+ self.conditions, self.condNamesInFile = _c, _n
1424
+ needUpdate = True
1425
+ except (ImportError, ValueError) as msg:
1426
+ msg = str(msg)
1427
+ if msg.startswith('Could not open'):
1428
+ msg = _translate('Could not read conditions from:\n')
1429
+ _file = newFullPath.split(os.path.sep)[-1]
1430
+ self.currentCtrls['conditions'].setValue(msg + _file)
1431
+ self.currentCtrls['conditions'].valueCtrl.SetForegroundColour("Red")
1432
+ logging.error(
1433
+ 'Could not open as a conditions file: %s' % newFullPath)
1434
+ else:
1435
+ mo = re.search(r'".+\.[0-9]+"$', msg)
1436
+ if 'cannot contain punctuation or spaces' in msg and mo:
1437
+ # column name is something like "stim.1", which may
1438
+ # be in conditionsFile or generated by pandas when
1439
+ # duplicated column names are found.
1440
+ m2 = 'Parameters (column headers) cannot contain dots' \
1441
+ ' or be duplicated.'
1442
+ else:
1443
+ # other exceptions
1444
+ sep2 = os.linesep * 2
1445
+ m2 = msg.replace(
1446
+ 'Conditions file ','').replace(': ', sep2)
1447
+ # Display error message dialog
1448
+ _title = _translate(
1449
+ 'Configuration error in conditions file')
1450
+ dlgErr = dialogs.MessageDialog(
1451
+ parent=self.frame, message=m2,
1452
+ type='Info', title=_title).ShowModal()
1453
+ msg = _translate('Bad condition name(s) in file:\n')
1454
+ val = msg + newFullPath.split(os.path.sep)[-1]
1455
+ self.currentCtrls['conditions'].setValue(val)
1456
+ self.currentCtrls['conditions'].valueCtrl.SetForegroundColour("Red")
1457
+ msg = 'Rejected bad condition name(s) in file: %s'
1458
+ logging.error(msg % newFullPath)
1459
+ self.conditionsFile = self.conditionsFileOrig
1460
+ self.conditions = self.conditionsOrig
1461
+ return # no update or display changes
1462
+
1463
+ # check for Builder variables
1464
+ builderVariables = []
1465
+ for condName in self.condNamesInFile:
1466
+ if condName in self.exp.namespace.builder:
1467
+ builderVariables.append(condName)
1468
+ if builderVariables:
1469
+ msg = _translate('Builder variable(s) ({}) in file:{}').format(
1470
+ ','.join(builderVariables), newFullPath.split(os.path.sep)[-1])
1471
+ self.currentCtrls['conditions'].setValue(msg)
1472
+ self.currentCtrls['conditions'].valueCtrl.SetForegroundColour("Red")
1473
+ msg = 'Rejected Builder variable(s) ({}) in file:{}'.format(
1474
+ ','.join(builderVariables), newFullPath.split(os.path.sep)[-1])
1475
+ logging.error(msg)
1476
+ self.conditionsFile = self.conditionsFileOrig
1477
+ self.conditions = self.conditionsOrig
1478
+ return # no update or display changes
1479
+
1480
+ duplCondNames = []
1481
+ if len(self.condNamesInFile):
1482
+ for condName in self.condNamesInFile:
1483
+ if self.exp.namespace.exists(condName):
1484
+ duplCondNames.append(condName)
1485
+ # abbrev long strings to better fit in the dialog:
1486
+ duplCondNamesStr = ' '.join(duplCondNames)[:42]
1487
+ if len(duplCondNamesStr) == 42:
1488
+ duplCondNamesStr = duplCondNamesStr[:39] + '...'
1489
+ if len(duplCondNames):
1490
+ if isSameFilePathAndName:
1491
+ msg = ('Assuming reloading file: same filename and '
1492
+ 'duplicate condition names in file: %s')
1493
+ logging.info(msg % self.conditionsFile)
1494
+ else:
1495
+ self.currentCtrls['conditionsFile'].setValue(newPath)
1496
+ val = ('Warning: Condition names conflict with existing'
1497
+ ':\n[' + duplCondNamesStr + ']\nProceed'
1498
+ ' anyway? (= safe if these are in old file)')
1499
+ self.currentCtrls['conditions'].setValue(val)
1500
+ self.currentCtrls['conditions'].valueCtrl.SetForegroundColour("Red")
1501
+ msg = ('Duplicate condition names, different '
1502
+ 'conditions file: %s')
1503
+ logging.warning(msg % duplCondNamesStr)
1504
+ # stash condition names but don't add to namespace yet, user can
1505
+ # still cancel
1506
+ # add after self.show() in __init__:
1507
+ self.duplCondNames = duplCondNames
1508
+
1509
+ if (needUpdate
1510
+ or ('conditionsFile' in list(self.currentCtrls.keys())
1511
+ and not duplCondNames)):
1512
+ self.currentCtrls['conditionsFile'].setValue(newPath)
1513
+ msg, OK = self.getTrialsSummary(self.conditions)
1514
+ self.currentCtrls['conditions'].setValue(msg)
1515
+ if OK:
1516
+ self.currentCtrls['conditions'].valueCtrl.SetForegroundColour("Black")
1517
+ else:
1518
+ self.currentCtrls['conditions'].valueCtrl.SetForegroundColour("Red")
1519
+ self.Layout()
1520
+ self.Fit()
1521
+
1522
+ def getParams(self):
1523
+ """Retrieves data and re-inserts it into the handler and returns
1524
+ those handler params
1525
+ """
1526
+ # get data from input fields
1527
+ for fieldName in self.currentHandler.params:
1528
+ if fieldName == 'endPoints':
1529
+ continue # this was deprecated in v1.62.00
1530
+ param = self.currentHandler.params[fieldName]
1531
+ if fieldName in ['conditionsFile']:
1532
+ param.val = self.conditionsFile
1533
+ # not the value from ctrl - that was abbreviated
1534
+ # see onOK() for partial handling = check for '...'
1535
+ else: # most other fields
1536
+ # the various dlg ctrls for this param
1537
+ ctrls = self.currentCtrls[fieldName]
1538
+ param.val = ctrls.getValue()
1539
+ # from _baseParamsDlg (handles diff control types)
1540
+ if ctrls.typeCtrl:
1541
+ param.valType = ctrls.getType()
1542
+ if ctrls.updateCtrl:
1543
+ param.updates = ctrls.getUpdates()
1544
+ return self.currentHandler.params
1545
+
1546
+ def refreshConditions(self):
1547
+ """user might have manually edited the conditionsFile name,
1548
+ which in turn affects self.conditions and namespace. It's harder
1549
+ to handle changes to long names that have been abbrev()'d, so
1550
+ skip them (names containing '...').
1551
+ """
1552
+ val = self.currentCtrls['conditionsFile'].valueCtrl.GetValue()
1553
+ if val.find('...') == -1 and self.conditionsFile != val:
1554
+ self.conditionsFile = val
1555
+ if self.conditions:
1556
+ self.exp.namespace.remove(list(self.conditions[0].keys()))
1557
+ if os.path.isfile(self.conditionsFile):
1558
+ try:
1559
+ self.conditions = data.importConditions(
1560
+ self.conditionsFile)
1561
+ msg, OK = self.getTrialsSummary(self.conditions)
1562
+ self.currentCtrls['conditions'].setValue(msg)
1563
+ if OK:
1564
+ self.currentCtrls['conditions'].valueCtrl.SetForegroundColour("Black")
1565
+ else:
1566
+ self.currentCtrls['conditions'].valueCtrl.SetForegroundColour("Red")
1567
+ except (ImportError, ValueError) as e:
1568
+ msg1 = _translate(
1569
+ 'Badly formed condition name(s) in file:\n')
1570
+ msg2 = _translate('.\nNeed to be legal as var name; '
1571
+ 'edit file, try again.')
1572
+ val = msg1 + str(e).replace(':', '\n') + msg2
1573
+ self.currentCtrls['conditions'].setValue(val)
1574
+ self.currentCtrls['conditions'].valueCtrl.SetForegroundColour("Red")
1575
+ self.conditions = ''
1576
+ msg3 = 'Reject bad condition name in conditions file: %s'
1577
+ logging.error(msg3 % str(e).split(':')[0])
1578
+ else:
1579
+ self.conditions = None
1580
+ self.currentCtrls['conditions'].setValue(_translate(
1581
+ "No parameters set (conditionsFile not found)"))
1582
+ self.currentCtrls['conditions'].valueCtrl.SetForegroundColour("Red")
1583
+ else:
1584
+ msg = ('DlgLoop: could not determine if a condition'
1585
+ ' filename was edited')
1586
+ logging.debug(msg)
1587
+ # self.currentCtrls['conditions'] could be misleading here
1588
+
1589
+ def onOK(self, event=None):
1590
+ # intercept OK in case user deletes or edits the filename manually
1591
+ if 'conditionsFile' in self.currentCtrls:
1592
+ self.refreshConditions()
1593
+ event.Skip() # do the OK button press
1594
+
1595
+
1596
+ class DlgComponentProperties(_BaseParamsDlg):
1597
+
1598
+ def __init__(self, frame, element, experiment,
1599
+ suppressTitles=True, size=wx.DefaultSize,
1600
+ style=wx.DEFAULT_DIALOG_STYLE | wx.DIALOG_NO_PARENT,
1601
+ editing=False,
1602
+ timeout=None, testing=False, type=None):
1603
+ style = style | wx.RESIZE_BORDER
1604
+ self.type = type or element.type
1605
+ _BaseParamsDlg.__init__(self, frame=frame, element=element, experiment=experiment,
1606
+ size=size,
1607
+ style=style, editing=editing,
1608
+ timeout=timeout)
1609
+ self.frame = frame
1610
+ self.app = frame.app
1611
+ self.dpi = self.app.dpi
1612
+
1613
+ # for all components
1614
+ self.show(testing)
1615
+ if not testing:
1616
+ if self.OK:
1617
+ self.params = self.getParams() # get new vals from dlg
1618
+ self.Destroy()
1619
+
1620
+
1621
+ class DlgExperimentProperties(_BaseParamsDlg):
1622
+
1623
+ def __init__(self, frame, element, experiment,
1624
+ suppressTitles=False, size=wx.DefaultSize,
1625
+ style=wx.DEFAULT_DIALOG_STYLE | wx.DIALOG_NO_PARENT,
1626
+ timeout=None):
1627
+ style = style | wx.RESIZE_BORDER
1628
+ _BaseParamsDlg.__init__(self, frame=frame, element=element, experiment=experiment,
1629
+ size=size,
1630
+ style=style,
1631
+ timeout=timeout)
1632
+ self.frame = frame
1633
+ self.app = frame.app
1634
+ self.dpi = self.app.dpi
1635
+
1636
+ # for input devices:
1637
+ # do this just to set the initial values to be
1638
+ self.paramCtrls['Full-screen window'].setChangesCallback(self.onFullScrChange)
1639
+ self.onFullScrChange(event=None)
1640
+ self.Bind(wx.EVT_CHECKBOX, self.onFullScrChange,
1641
+ self.paramCtrls['Full-screen window'].valueCtrl)
1642
+
1643
+ # for all components
1644
+ self.show()
1645
+ if self.OK:
1646
+ self.params = self.getParams() # get new vals from dlg
1647
+ self.Destroy()
1648
+
1649
+ def onFullScrChange(self, event=None):
1650
+ """full-screen has been checked / unchecked.
1651
+ Show or hide the window size field accordingly
1652
+ """
1653
+ if self.paramCtrls['Full-screen window'].valueCtrl.GetValue():
1654
+ # get screen size for requested display
1655
+ numDisplays = wx.Display.GetCount()
1656
+ try:
1657
+ screenValue = int(
1658
+ self.paramCtrls['Screen'].valueCtrl.GetValue())
1659
+ except ValueError:
1660
+ # param control currently contains no integer value
1661
+ screenValue = 1
1662
+ if screenValue < 1 or screenValue > numDisplays:
1663
+ logging.error("User requested non-existent screen")
1664
+ screenN = 0
1665
+ else:
1666
+ screenN = screenValue - 1
1667
+ size = list(wx.Display(screenN).GetGeometry()[2:])
1668
+ # set vals and disable changes
1669
+ field = 'Window size (pixels)'
1670
+ self.paramCtrls[field].valueCtrl.SetValue(str(size))
1671
+ self.paramCtrls[field].param.val = size
1672
+ self.paramCtrls[field].valueCtrl.Disable()
1673
+ self.paramCtrls[field].nameCtrl.Disable()
1674
+ else:
1675
+ self.paramCtrls['Window size (pixels)'].valueCtrl.Enable()
1676
+ self.paramCtrls['Window size (pixels)'].nameCtrl.Enable()
1677
+ self.mainSizer.Layout()
1678
+ self.Fit()
1679
+ self.Refresh()