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,713 @@
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-2022 Open Science Tools Ltd.
6
+ # Distributed under the terms of the GNU General Public License (GPL).
7
+
8
+ import os
9
+ import subprocess
10
+ import sys
11
+
12
+ import wx
13
+
14
+ from psychopy.app.colorpicker import PsychoColorPicker
15
+ from psychopy.app.dialogs import ListWidget
16
+ from psychopy.colors import Color
17
+ from psychopy.localization import _translate
18
+ from psychopy import data, prefs, experiment
19
+ import re
20
+ from pathlib import Path
21
+
22
+ from ..localizedStrings import _localizedDialogs as _localized
23
+ from ...themes import icons
24
+
25
+
26
+ class _ValidatorMixin:
27
+ def validate(self, evt=None):
28
+ """Redirect validate calls to global validate method, assigning
29
+ appropriate `valType`.
30
+ """
31
+ validate(self, self.valType)
32
+
33
+ if evt is not None:
34
+ evt.Skip()
35
+
36
+ def showValid(self, valid):
37
+ """Style input box according to valid"""
38
+ if not hasattr(self, "SetForegroundColour"):
39
+ return
40
+
41
+ if valid:
42
+ self.SetForegroundColour(wx.Colour(0, 0, 0))
43
+ else:
44
+ self.SetForegroundColour(wx.Colour(1, 0, 0))
45
+
46
+ def updateCodeFont(self, valType):
47
+ """Style input box according to code wanted"""
48
+ if not hasattr(self, "SetFont"):
49
+ # Skip if font not applicable to object type
50
+ return
51
+ if self.GetName() == "name":
52
+ # Name is never code
53
+ valType = "str"
54
+
55
+ fontNormal = self.GetTopLevelParent().app._mainFont
56
+ if valType == "code" or hasattr(self, "dollarLbl"):
57
+ # Set font
58
+ fontCode = self.GetTopLevelParent().app._codeFont
59
+ fontCodeBold = fontCode.Bold()
60
+ if fontCodeBold.IsOk():
61
+ self.SetFont(fontCodeBold)
62
+ else:
63
+ # use normal font if the bold version is invalid on the system
64
+ self.SetFont(fontCode)
65
+ else:
66
+ self.SetFont(fontNormal)
67
+
68
+
69
+ class _FileMixin:
70
+ @property
71
+ def rootDir(self):
72
+ if not hasattr(self, "_rootDir"):
73
+ # Store location of root directory if not defined
74
+ self._rootDir = Path(self.GetTopLevelParent().frame.exp.filename)
75
+ if self._rootDir.is_file():
76
+ # Move up a dir if root is a file
77
+ self._rootDir = self._rootDir.parent
78
+ # Return stored rootDir
79
+ return self._rootDir
80
+ @rootDir.setter
81
+ def rootDir(self, value):
82
+ self._rootDir = value
83
+
84
+ def getFile(self, msg="Specify file ...", wildcard="All Files (*.*)|*.*"):
85
+ dlg = wx.FileDialog(self, message=_translate(msg), defaultDir=str(self.rootDir),
86
+ style=wx.FD_OPEN | wx.FD_FILE_MUST_EXIST,
87
+ wildcard=_translate(wildcard))
88
+ if dlg.ShowModal() != wx.ID_OK:
89
+ return
90
+ file = dlg.GetPath()
91
+ try:
92
+ filename = Path(file).relative_to(self.rootDir)
93
+ except ValueError:
94
+ filename = Path(file).absolute()
95
+ return str(filename).replace("\\", "/")
96
+
97
+ def getFiles(self, msg="Specify file or files...", wildcard="All Files (*.*)|*.*"):
98
+ dlg = wx.FileDialog(self, message=_translate(msg), defaultDir=str(self.rootDir),
99
+ style=wx.FD_OPEN | wx.FD_FILE_MUST_EXIST | wx.FD_MULTIPLE,
100
+ wildcard=_translate(wildcard))
101
+ if dlg.ShowModal() != wx.ID_OK:
102
+ return
103
+ inList = dlg.GetPaths()
104
+ outList = []
105
+ for file in inList:
106
+ try:
107
+ filename = Path(file).relative_to(self.rootDir)
108
+ except ValueError:
109
+ filename = Path(file).absolute()
110
+ outList.append(str(filename).replace("\\", "/"))
111
+ return outList
112
+
113
+
114
+ class _HideMixin:
115
+ def ShowAll(self, visible):
116
+ # Get sizer, if present
117
+ if hasattr(self, "_szr"):
118
+ sizer = self._szr
119
+ elif isinstance(self, DictCtrl):
120
+ sizer = self
121
+ else:
122
+ sizer = self.GetSizer()
123
+ # If there is a sizer, recursively hide children
124
+ if sizer is not None:
125
+ self.tunnelShow(sizer, visible)
126
+ else:
127
+ self.Show(visible)
128
+
129
+ def HideAll(self):
130
+ self.Show(False)
131
+
132
+ def tunnelShow(self, sizer, visible):
133
+ if sizer is not None:
134
+ # Show/hide everything in the sizer
135
+ for child in sizer.Children:
136
+ if child.Window is not None:
137
+ child.Window.Show(visible)
138
+ if child.Sizer is not None:
139
+ # If child is a sizer, recur
140
+ self.tunnelShow(child.Sizer, visible)
141
+
142
+
143
+ class SingleLineCtrl(wx.TextCtrl, _ValidatorMixin, _HideMixin):
144
+ def __init__(self, parent, valType,
145
+ val="", fieldName="",
146
+ size=wx.Size(-1, 24), style=wx.DEFAULT):
147
+ # Create self
148
+ wx.TextCtrl.__init__(self)
149
+ self.Create(parent, -1, val, name=fieldName, size=size, style=style)
150
+ self.valType = valType
151
+
152
+ # On MacOS, we need to disable smart quotes
153
+ if sys.platform == 'darwin':
154
+ self.OSXDisableAllSmartSubstitutions()
155
+
156
+ # Add sizer
157
+ self._szr = wx.BoxSizer(wx.HORIZONTAL)
158
+ if not valType == "str" and not fieldName == "name":
159
+ # Add $ for anything to be interpreted verbatim
160
+ self.dollarLbl = wx.StaticText(parent, -1, "$", size=wx.Size(-1, -1), style=wx.ALIGN_RIGHT)
161
+ self.dollarLbl.SetToolTip(_translate("This parameter will be treated as code - we have already put in the $, so you don't have to."))
162
+ self._szr.Add(self.dollarLbl, border=5, flag=wx.ALIGN_CENTER_VERTICAL | wx.RIGHT | wx.LEFT)
163
+ # Add self to sizer
164
+ self._szr.Add(self, proportion=1, border=5, flag=wx.EXPAND)
165
+ # Bind to validation
166
+ self.Bind(wx.EVT_CHAR, self.validate)
167
+ self.validate()
168
+
169
+ def Show(self, value=True):
170
+ wx.TextCtrl.Show(self, value)
171
+ if hasattr(self, "dollarLbl"):
172
+ self.dollarLbl.Show(value)
173
+ if hasattr(self, "deleteBtn"):
174
+ self.deleteBtn.Show(value)
175
+
176
+
177
+ class MultiLineCtrl(SingleLineCtrl, _ValidatorMixin, _HideMixin):
178
+ def __init__(self, parent, valType,
179
+ val="", fieldName="",
180
+ size=wx.Size(-1, 144)):
181
+ SingleLineCtrl.__init__(self, parent, valType,
182
+ val=val, fieldName=fieldName,
183
+ size=size, style=wx.TE_MULTILINE)
184
+
185
+
186
+ class InvalidCtrl(SingleLineCtrl, _ValidatorMixin, _HideMixin):
187
+ def __init__(self, parent, valType,
188
+ val="", fieldName="",
189
+ size=wx.Size(-1, 24), style=wx.DEFAULT):
190
+ SingleLineCtrl.__init__(self, parent, valType,
191
+ val=val, fieldName=fieldName,
192
+ size=size, style=style)
193
+ self.Disable()
194
+ # Add delete button
195
+ self.deleteBtn = wx.Button(parent, label="×", size=(24, 24))
196
+ self.deleteBtn.SetForegroundColour("red")
197
+ self.deleteBtn.Bind(wx.EVT_BUTTON, self.deleteParam)
198
+ self.deleteBtn.SetToolTip(_translate(
199
+ "This parameter has come from an older version of PsychoPy. "
200
+ "In the latest version of PsychoPy, it is not used. Click this "
201
+ "button to delete it. WARNING: This may affect how this experiment "
202
+ "works in older versions!"))
203
+ self._szr.Add(self.deleteBtn, border=6, flag=wx.LEFT | wx.RIGHT)
204
+ # Add deleted label
205
+ self.deleteLbl = wx.StaticText(parent, label=_translate("DELETED"))
206
+ self.deleteLbl.SetForegroundColour("red")
207
+ self.deleteLbl.Hide()
208
+ self._szr.Add(self.deleteLbl, border=6, proportion=1, flag=wx.ALL | wx.ALIGN_CENTER_VERTICAL)
209
+ # Add undo delete button
210
+ self.undoBtn = wx.Button(parent, label="⟲", size=(24, 24))
211
+ self.undoBtn.SetToolTip(_translate(
212
+ "This parameter will not be deleted until you click Okay. "
213
+ "Click this button to revert the deletion and keep the parameter."))
214
+ self.undoBtn.Hide()
215
+ self.undoBtn.Bind(wx.EVT_BUTTON, self.undoDelete)
216
+ self._szr.Add(self.undoBtn, border=6, flag=wx.LEFT | wx.RIGHT)
217
+
218
+ # Set deletion flag
219
+ self.forDeletion = False
220
+
221
+ def deleteParam(self, evt=None):
222
+ """
223
+ When the remove button is pressed, mark this param as for deletion
224
+ """
225
+ # Mark for deletion
226
+ self.forDeletion = True
227
+ # Hide value ctrl and delete button
228
+ self.Hide()
229
+ self.deleteBtn.Hide()
230
+ # Show delete label and
231
+ self.undoBtn.Show()
232
+ self.deleteLbl.Show()
233
+
234
+ self._szr.Layout()
235
+
236
+ def undoDelete(self, evt=None):
237
+ # Mark not for deletion
238
+ self.forDeletion = False
239
+ # Show value ctrl and delete button
240
+ self.Show()
241
+ self.deleteBtn.Show()
242
+ # Hide delete label and
243
+ self.undoBtn.Hide()
244
+ self.deleteLbl.Hide()
245
+
246
+ self._szr.Layout()
247
+
248
+
249
+ class IntCtrl(wx.SpinCtrl, _ValidatorMixin, _HideMixin):
250
+ def __init__(self, parent, valType,
251
+ val="", fieldName="",
252
+ size=wx.Size(-1, 24), limits=None):
253
+ wx.SpinCtrl.__init__(self)
254
+ limits = limits or (-100,100)
255
+ self.Create(parent, -1, str(val), name=fieldName, size=size, min=min(limits), max=max(limits))
256
+ self.valType = valType
257
+ self.Bind(wx.EVT_SPINCTRL, self.spin)
258
+
259
+ def spin(self, evt):
260
+ """Redirect validate calls to global validate method, assigning appropriate valType"""
261
+ if evt.EventType == wx.EVT_SPIN_UP.evtType[0]:
262
+ self.SetValue(str(int(self.GetValue())+1))
263
+ elif evt.EventType == wx.EVT_SPIN_DOWN.evtType[0]:
264
+ self.SetValue(str(int(self.GetValue()) - 1))
265
+ validate(self, "int")
266
+
267
+
268
+ BoolCtrl = wx.CheckBox
269
+
270
+
271
+ class ChoiceCtrl(wx.Choice, _ValidatorMixin, _HideMixin):
272
+ def __init__(self, parent, valType,
273
+ val="", choices=[], labels=[], fieldName="",
274
+ size=wx.Size(-1, 24)):
275
+ <<<<<<< HEAD
276
+ # If not given any labels, alias values
277
+ if not labels:
278
+ labels = choices
279
+ # Map labels to values
280
+ self._choices = {}
281
+ for i, value in enumerate(choices):
282
+ if i < len(labels):
283
+ self._choices[labels[i]] = value
284
+ else:
285
+ self._choices[value] = value
286
+ =======
287
+ # translate and add labels to the dropdown
288
+ choiceLabels = []
289
+ for i, value in enumerate(choices):
290
+ if i < len(labels):
291
+ label = labels[i]
292
+ else:
293
+ label = value
294
+ try:
295
+ choiceLabels.append(_localized[label])
296
+ except:
297
+ choiceLabels.append(label)
298
+
299
+ >>>>>>> e32726e790e8485ee63b1419cb6e567f086836b9
300
+ # Create choice ctrl from labels
301
+ wx.Choice.__init__(self)
302
+ self.Create(parent, -1, size=size, choices=list(self._choices), name=fieldName)
303
+ self.valType = valType
304
+ self.SetStringSelection(val)
305
+
306
+ def SetStringSelection(self, string):
307
+ if string not in self._choices:
308
+ <<<<<<< HEAD
309
+ self._choices[string] = string
310
+ self.SetItems(list(self._choices))
311
+ wx.Choice.SetStringSelection(self, string)
312
+
313
+ def GetValue(self):
314
+ lbl = self.GetStringSelection()
315
+ return self._choices[lbl]
316
+ =======
317
+ self._choices.append(string)
318
+ self.SetItems(self._choices)
319
+ # Don't use wx.Choice.SetStringSelection here
320
+ # because label string is localized.
321
+ wx.Choice.SetSelection(self, self._choices.index(string))
322
+
323
+ def GetValue(self):
324
+ # Don't use wx.Choice.GetStringSelection here
325
+ # because label string is localized.
326
+ return self._choices[self.GetSelection()]
327
+
328
+ >>>>>>> e32726e790e8485ee63b1419cb6e567f086836b9
329
+
330
+
331
+ class MultiChoiceCtrl(wx.CheckListBox, _ValidatorMixin, _HideMixin):
332
+ def __init__(self, parent, valType,
333
+ vals="", choices=[], fieldName="",
334
+ size=wx.Size(-1, -1)):
335
+ wx.CheckListBox.__init__(self)
336
+ self.Create(parent, id=wx.ID_ANY, size=size, choices=choices, name=fieldName, style=wx.LB_MULTIPLE)
337
+ self.valType = valType
338
+ self._choices = choices
339
+ # Make initial selection
340
+ if isinstance(vals, str):
341
+ # Convert to list if needed
342
+ vals = data.utils.listFromString(vals, excludeEmpties=True)
343
+ self.SetCheckedStrings(vals)
344
+ self.validate()
345
+
346
+ def SetCheckedStrings(self, strings):
347
+ if not isinstance(strings, (list, tuple)):
348
+ strings = [strings]
349
+ for s in strings:
350
+ if s not in self._choices:
351
+ self._choices.append(s)
352
+ self.SetItems(self._choices)
353
+ wx.CheckListBox.SetCheckedStrings(self, strings)
354
+
355
+ def GetValue(self, evt=None):
356
+ return self.GetCheckedStrings()
357
+
358
+
359
+ class FileCtrl(wx.TextCtrl, _ValidatorMixin, _HideMixin, _FileMixin):
360
+ def __init__(self, parent, valType,
361
+ val="", fieldName="",
362
+ size=wx.Size(-1, 24)):
363
+ # Create self
364
+ wx.TextCtrl.__init__(self)
365
+ self.Create(parent, -1, val, name=fieldName, size=size)
366
+ self.valType = valType
367
+ # Add sizer
368
+ self._szr = wx.BoxSizer(wx.HORIZONTAL)
369
+ self._szr.Add(self, border=5, proportion=1, flag=wx.EXPAND | wx.RIGHT)
370
+ # Add button to browse for file
371
+ fldr = icons.ButtonIcon(stem="folder", size=16).bitmap
372
+ self.findBtn = wx.BitmapButton(parent, -1, size=wx.Size(24, 24), bitmap=fldr)
373
+ self.findBtn.SetToolTip(_translate("Specify file ..."))
374
+ self.findBtn.Bind(wx.EVT_BUTTON, self.findFile)
375
+ self._szr.Add(self.findBtn)
376
+ # Configure validation
377
+ self.Bind(wx.EVT_TEXT, self.validate)
378
+ self.validate()
379
+
380
+ def findFile(self, evt):
381
+ file = self.getFile()
382
+ if file:
383
+ self.SetValue(file)
384
+ self.validate(evt)
385
+
386
+
387
+ class FileListCtrl(wx.ListBox, _ValidatorMixin, _HideMixin, _FileMixin):
388
+ def __init__(self, parent, valType,
389
+ choices=[], size=None, pathtype="rel"):
390
+ wx.ListBox.__init__(self)
391
+ self.valType = valType
392
+ parent.Bind(wx.EVT_DROP_FILES, self.addItem)
393
+ self.app = parent.app
394
+ if type(choices) == str:
395
+ choices = data.utils.listFromString(choices)
396
+ self.Create(id=wx.ID_ANY, parent=parent, choices=choices, size=size, style=wx.LB_EXTENDED | wx.LB_HSCROLL)
397
+ self.addCustomBtn = wx.Button(parent, -1, size=(24,24), style=wx.BU_EXACTFIT, label="...")
398
+ self.addCustomBtn.Bind(wx.EVT_BUTTON, self.addCustomItem)
399
+ self.addBtn = wx.Button(parent, -1, size=(24,24), style=wx.BU_EXACTFIT, label="+")
400
+ self.addBtn.Bind(wx.EVT_BUTTON, self.addItem)
401
+ self.subBtn = wx.Button(parent, -1, size=(24,24), style=wx.BU_EXACTFIT, label="-")
402
+ self.subBtn.Bind(wx.EVT_BUTTON, self.removeItem)
403
+ self._szr = wx.BoxSizer(wx.HORIZONTAL)
404
+ self.btns = wx.BoxSizer(wx.VERTICAL)
405
+ self.btns.AddMany((self.addCustomBtn, self.addBtn, self.subBtn))
406
+ self._szr.Add(self, proportion=1, flag=wx.EXPAND)
407
+ self._szr.Add(self.btns)
408
+
409
+ def addItem(self, event):
410
+ # Get files
411
+ if event.GetEventObject() == self.addBtn:
412
+ fileList = self.getFiles()
413
+ else:
414
+ fileList = event.GetFiles()
415
+ for i, filename in enumerate(fileList):
416
+ try:
417
+ fileList[i] = Path(filename).relative_to(self.rootDir)
418
+ except ValueError:
419
+ fileList[i] = Path(filename).absolute()
420
+ # Add files to list
421
+ if fileList:
422
+ self.InsertItems(fileList, 0)
423
+
424
+ def removeItem(self, event):
425
+ i = self.GetSelections()
426
+ if isinstance(i, int):
427
+ i = [i]
428
+ items = [item for index, item in enumerate(self.Items)
429
+ if index not in i]
430
+ self.SetItems(items)
431
+
432
+ def addCustomItem(self, event):
433
+ # Create string dialog
434
+ dlg = wx.TextEntryDialog(parent=self, message=_translate("Add custom item"))
435
+ # Show dialog
436
+ if dlg.ShowModal() != wx.ID_OK:
437
+ return
438
+ # Get string
439
+ stringEntry = dlg.GetValue()
440
+ # Add to list
441
+ if stringEntry:
442
+ self.InsertItems([stringEntry], 0)
443
+
444
+ def GetValue(self):
445
+ return self.Items
446
+
447
+
448
+ class TableCtrl(wx.TextCtrl, _ValidatorMixin, _HideMixin, _FileMixin):
449
+ def __init__(self, parent, valType,
450
+ val="", fieldName="",
451
+ size=wx.Size(-1, 24)):
452
+ # Create self
453
+ wx.TextCtrl.__init__(self)
454
+ self.Create(parent, -1, val, name=fieldName, size=size)
455
+ self.valType = valType
456
+ # Add sizer
457
+ self._szr = wx.BoxSizer(wx.HORIZONTAL)
458
+ self._szr.Add(self, proportion=1, border=5, flag=wx.EXPAND | wx.RIGHT)
459
+ # Add button to browse for file
460
+ fldr = icons.ButtonIcon(stem="folder", size=16).bitmap
461
+ self.findBtn = wx.BitmapButton(parent, -1, size=wx.Size(24,24), bitmap=fldr)
462
+ self.findBtn.SetToolTip(_translate("Specify file ..."))
463
+ self.findBtn.Bind(wx.EVT_BUTTON, self.findFile)
464
+ self._szr.Add(self.findBtn)
465
+ # Add button to open in Excel
466
+ xl = icons.ButtonIcon(stem="filecsv", size=16).bitmap
467
+ self.xlBtn = wx.BitmapButton(parent, -1, size=wx.Size(24,24), bitmap=xl)
468
+ self.xlBtn.SetToolTip(_translate("Open/create in your default table editor"))
469
+ self.xlBtn.Bind(wx.EVT_BUTTON, self.openExcel)
470
+ self._szr.Add(self.xlBtn)
471
+ # Link to Excel templates for certain contexts
472
+ cmpRoot = Path(experiment.components.__file__).parent
473
+ expRoot = Path(cmpRoot).parent
474
+ self.templates = {
475
+ 'Form': Path(cmpRoot) / "form" / "formItems.xltx",
476
+ 'TrialHandler': Path(expRoot) / "loopTemplate.xltx",
477
+ 'StairHandler': Path(expRoot) / "loopTemplate.xltx",
478
+ 'MultiStairHandler': Path(expRoot) / "loopTemplate.xltx",
479
+ 'QuestHandler': Path(expRoot) / "loopTemplate.xltx",
480
+ 'None': Path(expRoot) / 'blankTemplate.xltx',
481
+ }
482
+ # Specify valid extensions
483
+ self.validExt = [".csv",".tsv",".txt",
484
+ ".xl",".xlsx",".xlsm",".xlsb",".xlam",".xltx",".xltm",".xls",".xlt",
485
+ ".htm",".html",".mht",".mhtml",
486
+ ".xml",".xla",".xlm",
487
+ ".odc",".ods",
488
+ ".udl",".dsn",".mdb",".mde",".accdb",".accde",".dbc",".dbf",
489
+ ".iqy",".dqy",".rqy",".oqy",
490
+ ".cub",".atom",".atomsvc",
491
+ ".prn",".slk",".dif"]
492
+ # Configure validation
493
+ self.Bind(wx.EVT_TEXT, self.validate)
494
+ self.validate()
495
+
496
+ def validate(self, evt=None):
497
+ """Redirect validate calls to global validate method, assigning appropriate valType"""
498
+ validate(self, "file")
499
+ # Enable Excel button if valid
500
+ self.xlBtn.Enable(self.valid)
501
+ # Is component type available?
502
+ if self.GetValue() in [None, ""] + self.validExt and hasattr(self.GetTopLevelParent(), 'type'):
503
+ # Does this component have a default template?
504
+ if self.GetTopLevelParent().type in self.templates:
505
+ self.xlBtn.Enable(True)
506
+
507
+ def openExcel(self, event):
508
+ """Either open the specified excel sheet, or make a new one from a template"""
509
+ file = self.rootDir / self.GetValue()
510
+ if not (file.is_file() and file.suffix in self.validExt): # If not a valid file
511
+ dlg = wx.MessageDialog(self, _translate(
512
+ "Once you have created and saved your table,"
513
+ "please remember to add it to {name}").format(name=_translate(self.Name)),
514
+ caption=_translate("Reminder"))
515
+ dlg.ShowModal()
516
+ if hasattr(self.GetTopLevelParent(), 'type'):
517
+ if self.GetTopLevelParent().type in self.templates:
518
+ file = self.templates[self.GetTopLevelParent().type] # Open type specific template
519
+ else:
520
+ file = self.templates['None'] # Open blank template
521
+ # Open whatever file is used
522
+ try:
523
+ os.startfile(file)
524
+ except AttributeError:
525
+ opener = "open" if sys.platform == "darwin" else "xdg-open"
526
+ subprocess.call([opener, file])
527
+
528
+ def findFile(self, event):
529
+ _wld = f"All Table Files({'*'+';*'.join(self.validExt)})|{'*'+';*'.join(self.validExt)}|All Files (*.*)|*.*"
530
+ file = self.getFile(msg="Specify table file ...", wildcard=_wld)
531
+ if file:
532
+ self.SetValue(file)
533
+ self.validate(event)
534
+
535
+
536
+ class ColorCtrl(wx.TextCtrl, _ValidatorMixin, _HideMixin):
537
+ def __init__(self, parent, valType,
538
+ val="", fieldName="",
539
+ size=wx.Size(-1, 24)):
540
+ # Create self
541
+ wx.TextCtrl.__init__(self)
542
+ self.Create(parent, -1, val, name=fieldName, size=size)
543
+ self.valType = valType
544
+ # Add sizer
545
+ self._szr = wx.BoxSizer(wx.HORIZONTAL)
546
+ if valType == "code":
547
+ # Add $ for anything to be interpreted verbatim
548
+ self.dollarLbl = wx.StaticText(parent, -1, "$", size=wx.Size(-1, -1), style=wx.ALIGN_RIGHT)
549
+ self.dollarLbl.SetToolTip(_translate("This parameter will be treated as code - we have already put in the $, so you don't have to."))
550
+ self._szr.Add(self.dollarLbl, border=5, flag=wx.ALIGN_CENTER_VERTICAL | wx.RIGHT | wx.LEFT)
551
+ # Add ctrl to sizer
552
+ self._szr.Add(self, proportion=1, border=5, flag=wx.EXPAND | wx.RIGHT)
553
+ # Add button to activate color picker
554
+ fldr = icons.ButtonIcon(stem="color", size=16).bitmap
555
+ self.pickerBtn = wx.BitmapButton(parent, -1, size=wx.Size(24,24), bitmap=fldr)
556
+ self.pickerBtn.SetToolTip(_translate("Specify color ..."))
557
+ self.pickerBtn.Bind(wx.EVT_BUTTON, self.colorPicker)
558
+ self._szr.Add(self.pickerBtn)
559
+ # Bind to validation
560
+ self.Bind(wx.EVT_CHAR, self.validate)
561
+ self.validate()
562
+
563
+ def colorPicker(self, evt):
564
+ dlg = PsychoColorPicker(self, context=self, allowCopy=False) # open a color picker
565
+ dlg.ShowModal()
566
+ dlg.Destroy()
567
+
568
+
569
+ def validate(obj, valType):
570
+ val = str(obj.GetValue())
571
+ valid = True
572
+ if val.startswith("$"):
573
+ # If indicated as code, treat as code
574
+ valType = "code"
575
+ # Validate string
576
+ if valType == "str":
577
+ if re.findall(r"(?<!\\)\"", val):
578
+ # If there are unescaped "
579
+ valid = False
580
+ if re.findall(r"(?<!\\)\'", val):
581
+ # If there are unescaped '
582
+ valid = False
583
+ # Validate code
584
+ if valType == "code":
585
+ # Replace unescaped curly quotes
586
+ if re.findall(r"(?<!\\)[\u201c\u201d]", val):
587
+ pt = obj.GetInsertionPoint()
588
+ obj.SetValue(re.sub(r"(?<!\\)[\u201c\u201d]", "\"", val))
589
+ obj.SetInsertionPoint(pt)
590
+ # For now, ignore
591
+ pass
592
+ # Validate num
593
+ if valType in ["num", "int"]:
594
+ try:
595
+ # Try to convert value to a float
596
+ float(val)
597
+ except ValueError:
598
+ # If conversion fails, value is invalid
599
+ valid = False
600
+ # Validate bool
601
+ if valType == "bool":
602
+ if val not in ["True", "False"]:
603
+ # If value is not True or False, it is invalid
604
+ valid = False
605
+ # Validate list
606
+ if valType == "list":
607
+ empty = not bool(val) # Is value empty?
608
+ fullList = re.fullmatch(r"[\(\[].*[\]\)]", val) # Is value full list with parentheses?
609
+ partList = "," in val and not re.match(r"[\(\[].*[\]\)]", val) # Is value list without parentheses?
610
+ singleVal = not " " in val or re.match(r"[\"\'].*[\"\']", val) # Is value a single value?
611
+ if not any([empty, fullList, partList, singleVal]):
612
+ # If value is not any of valid types, it is invalid
613
+ valid = False
614
+ # Validate color
615
+ if valType == "color":
616
+ # Strip function calls
617
+ if re.fullmatch(r"\$?(Advanced)?Color\(.*\)", val):
618
+ val = re.sub(r"\$?(Advanced)?Color\(", "", val[:-1])
619
+ try:
620
+ # Try to create a Color object from value
621
+ obj.color = Color(val, False)
622
+ if not obj.color:
623
+ # If invalid object is created, input is invalid
624
+ valid = False
625
+ except:
626
+ # If object creation fails, input is invalid
627
+ valid = False
628
+ if valType == "file":
629
+ val = Path(str(val))
630
+ if not val.is_absolute():
631
+ frame = obj.GetTopLevelParent().frame
632
+ # If not an absolute path, append to current directory
633
+ val = Path(frame.filename).parent / val
634
+ if not val.is_file():
635
+ # Is value a valid filepath?
636
+ valid = False
637
+ if hasattr(obj, "validExt"):
638
+ # If control has specified list of ext, does value end in correct ext?
639
+ if val.suffix not in obj.validExt:
640
+ valid = False
641
+
642
+ # If additional allowed values are defined, override validation
643
+ if hasattr(obj, "allowedVals"):
644
+ if val in obj.allowedVals:
645
+ valid = True
646
+
647
+ # Apply valid status to object
648
+ obj.valid = valid
649
+ if hasattr(obj, "showValid"):
650
+ obj.showValid(valid)
651
+
652
+ # Update code font
653
+ obj.updateCodeFont(valType)
654
+
655
+
656
+ class DictCtrl(ListWidget, _ValidatorMixin, _HideMixin):
657
+ def __init__(self, parent,
658
+ val={}, valType='dict',
659
+ fieldName=""):
660
+ if not isinstance(val, (dict, list)):
661
+ raise ValueError("DictCtrl must be supplied with either a dict or a list of 1-long dicts, value supplied was {}".format(val))
662
+ # If supplied with a dict, convert it to a list of dicts
663
+ if isinstance(val, dict):
664
+ newVal = []
665
+ for key, v in val.items():
666
+ newVal.append({'Field': key, 'Default': v.val})
667
+ val = newVal
668
+ # If any items within the list are not dicts or are dicts longer than 1, throw error
669
+ if not all(isinstance(v, dict) and len(v) == 2 for v in val):
670
+ raise ValueError("DictCtrl must be supplied with either a dict or a list of 1-long dicts, value supplied was {}".format(val))
671
+ # Create ListWidget
672
+ ListWidget.__init__(self, parent, val, order=['Field', 'Default'])
673
+
674
+ def SetForegroundColour(self, color):
675
+ for child in self.Children:
676
+ if hasattr(child, "SetForegroundColour"):
677
+ child.SetForegroundColour(color)
678
+
679
+ def Enable(self, enable=True):
680
+ """
681
+ Enable or disable all items in the dict ctrl
682
+ """
683
+ # Iterate through all children
684
+ for cell in self.Children:
685
+ # Get the actual child rather than the sizer item
686
+ child = cell.Window
687
+ # If it can be enabled/disabled, enable/disable it
688
+ if hasattr(child, "Enable"):
689
+ child.Enable(enable)
690
+
691
+ def Disable(self):
692
+ """
693
+ Disable all items in the dict ctrl
694
+ """
695
+ self.Enable(False)
696
+
697
+ def Show(self, show=True):
698
+ """
699
+ Show or hide all items in the dict ctrl
700
+ """
701
+ # Iterate through all children
702
+ for cell in self.Children:
703
+ # Get the actual child rather than the sizer item
704
+ child = cell.Window
705
+ # If it can be shown/hidden, show/hide it
706
+ if hasattr(child, "Show"):
707
+ child.Show(show)
708
+
709
+ def Hide(self):
710
+ """
711
+ Hide all items in the dict ctrl
712
+ """
713
+ self.Show(False)