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,1315 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ # -----------------------------------------------------------------------------
4
+ #
5
+ # FreeType high-level python API - Copyright 2011-2015 Nicolas P. Rougier
6
+ # Distributed under the terms of the new BSD license.
7
+ #
8
+ # -----------------------------------------------------------------------------
9
+ r"""
10
+ TextBox2 provides a combination of features from TextStim and TextBox and then
11
+ some more added:
12
+
13
+ - fast like TextBox (TextStim is pyglet-based and slow)
14
+ - provides for fonts that aren't monospaced (unlike TextBox)
15
+ - adds additional options to use <b>bold<\b> and <i>italic<\i> tags in text
16
+
17
+ """
18
+ import numpy as np
19
+ from pyglet import gl
20
+
21
+ from ..basevisual import BaseVisualStim, ColorMixin, ContainerMixin, WindowMixin
22
+ from psychopy.tools.attributetools import attributeSetter, setAttribute
23
+ from psychopy.tools.arraytools import val2array
24
+ from psychopy.tools.monitorunittools import convertToPix
25
+ from .fontmanager import FontManager, GLFont
26
+ from .. import shaders
27
+ from ..rect import Rect
28
+ from ... import core, alerts, layout
29
+
30
+ from psychopy.tools.linebreak import get_breakable_points, break_units
31
+
32
+ allFonts = FontManager()
33
+
34
+ # compile global shader programs later (when we're certain a GL context exists)
35
+ rgbShader = None
36
+ alphaShader = None
37
+ showWhiteSpace = False
38
+
39
+ NONE=0
40
+ ITALIC=1
41
+ BOLD=2
42
+
43
+ codes = {'BOLD_START': u'\uE100',
44
+ 'BOLD_END': u'\uE101',
45
+ 'ITAL_START': u'\uE102',
46
+ 'ITAL_END': u'\uE103'}
47
+
48
+ wordBreaks = " -\n" # what about ",."?
49
+
50
+
51
+ END_OF_THIS_LINE = 983349843
52
+
53
+ # Setting debug to True will make the sub-elements on TextBox2 to be outlined in red, making it easier to determine their position
54
+ debug = False
55
+
56
+ # If text is ". " we don't want to start next line with single space?
57
+
58
+ class TextBox2(BaseVisualStim, ContainerMixin, ColorMixin):
59
+ def __init__(self, win, text, font,
60
+ pos=(0, 0), units=None, letterHeight=None,
61
+ size=None,
62
+ color=(1.0, 1.0, 1.0), colorSpace='rgb',
63
+ fillColor=None, fillColorSpace=None,
64
+ borderWidth=2, borderColor=None, borderColorSpace=None,
65
+ contrast=1,
66
+ opacity=None,
67
+ bold=False,
68
+ italic=False,
69
+ lineSpacing=None,
70
+ padding=None, # gap between box and text
71
+ anchor='center',
72
+ alignment='left',
73
+ flipHoriz=False,
74
+ flipVert=False,
75
+ editable=False,
76
+ lineBreaking='default',
77
+ name='',
78
+ autoLog=None,
79
+ onTextCallback=None):
80
+ """
81
+
82
+ Parameters
83
+ ----------
84
+ win
85
+ text
86
+ font
87
+ pos
88
+ units
89
+ letterHeight
90
+ size : Specifying None gets the default size for this type of unit.
91
+ Specifying [None, None] gets a TextBox that's expandable in both
92
+ dimensions. Specifying [0.75, None] gets a textbox that expands in the
93
+ length but fixed at 0.75 units in the width
94
+ color
95
+ colorSpace
96
+ contrast
97
+ opacity
98
+ bold
99
+ italic
100
+ lineSpacing
101
+ padding
102
+ anchor
103
+ alignment
104
+ fillColor
105
+ borderWidth
106
+ borderColor
107
+ flipHoriz
108
+ flipVert
109
+ editable
110
+ lineBreaking: Specifying 'default', text will be broken at a set of
111
+ characters defined in the module. Specifying 'uax14', text will be
112
+ broken in accordance with UAX#14 (Unicode Line Breaking Algorithm).
113
+ name
114
+ autoLog
115
+ """
116
+
117
+ BaseVisualStim.__init__(self, win, units=units, name=name)
118
+ self.win = win
119
+ self.colorSpace = colorSpace
120
+ ColorMixin.foreColor.fset(self, color) # Have to call the superclass directly on init as text has not been set
121
+ self.onTextCallback = onTextCallback
122
+
123
+ # Box around the whole textbox - drawn
124
+ self.box = Rect(
125
+ win,
126
+ units=self.units, pos=(0, 0), size=(0, 0), # set later by self.size and self.pos
127
+ colorSpace=colorSpace, lineColor=borderColor, fillColor=fillColor,
128
+ lineWidth=borderWidth,
129
+ opacity=self.opacity,
130
+ autoLog=False,
131
+ )
132
+ # Box around just the content area, excluding padding - not drawn
133
+ self.contentBox = Rect(
134
+ win,
135
+ units=self.units, pos=(0, 0), size=(0, 0), # set later by self.size and self.pos
136
+ colorSpace=colorSpace, lineColor='red', fillColor=None,
137
+ lineWidth=1, opacity=int(debug),
138
+ autoLog=False
139
+ )
140
+ # Box around current content, wrapped tight - not drawn
141
+ self.boundingBox = Rect(
142
+ win,
143
+ units='pix', pos=(0, 0), size=(0, 0), # set later by self.size and self.pos
144
+ colorSpace=colorSpace, lineColor='blue', fillColor=None,
145
+ lineWidth=1, opacity=int(debug),
146
+ autoLog=False
147
+ )
148
+ # Sizing params
149
+ self.letterHeight = letterHeight
150
+ self.padding = padding
151
+ self.size = size
152
+ self.pos = pos
153
+
154
+ # self._pixLetterHeight helps get font size right but not final layout
155
+ if 'deg' in self.units: # treat deg, degFlat or degFlatPos the same
156
+ scaleUnits = 'deg' # scale units are just for font resolution
157
+ else:
158
+ scaleUnits = self.units
159
+ self._pixelScaling = self.letterHeightPix / self.letterHeight
160
+ self.bold = bold
161
+ self.italic = italic
162
+ self.glFont = None # will be set by the self.font attribute setter
163
+ self.font = font
164
+ if lineSpacing is not None:
165
+ self.lineSpacing = lineSpacing
166
+ # If font not found, default to Open Sans Regular and raise alert
167
+ if not self.glFont:
168
+ alerts.alert(4325, self, {
169
+ 'font': font,
170
+ 'weight': 'bold' if self.bold is True else 'regular' if self.bold is False else self.bold,
171
+ 'style': 'italic' if self.italic else '',
172
+ 'name': self.name})
173
+ self.bold = False
174
+ self.italic = False
175
+ self.font = "Open Sans"
176
+
177
+ # once font is set up we can set the shader (depends on rgb/a of font)
178
+ if self.glFont.atlas.format == 'rgb':
179
+ global rgbShader
180
+ self.shader = rgbShader = shaders.Shader(
181
+ shaders.vertSimple, shaders.fragTextBox2)
182
+ else:
183
+ global alphaShader
184
+ self.shader = alphaShader = shaders.Shader(
185
+ shaders.vertSimple, shaders.fragTextBox2alpha)
186
+ self._needVertexUpdate = False # this will be set True during layout
187
+
188
+ # standard stimulus params
189
+ self.pos = pos
190
+ self.ori = 0.0
191
+ self.depth = 0.0
192
+ # used at render time
193
+ self._lines = None # np.array the line numbers for each char
194
+ self._colors = None
195
+ self._styles = None
196
+ self.flipHoriz = flipHoriz
197
+ self.flipVert = flipVert
198
+ # params about positioning (after layout has occurred)
199
+ self.anchor = anchor # 'center', 'top_left', 'bottom-center'...
200
+ self.alignment = alignment
201
+
202
+ # box border and fill
203
+ self.borderWidth = borderWidth
204
+ self.borderColor = borderColor
205
+ self.fillColor = fillColor
206
+ self.contrast = contrast
207
+ self.opacity = opacity
208
+
209
+ # set linebraking option
210
+ if lineBreaking not in ('default', 'uax14'):
211
+ raise ValueError("Unknown lineBreaking option ({}) is"
212
+ "specified.".format(lineBreaking))
213
+ self._lineBreaking = lineBreaking
214
+ # then layout the text (setting text triggers _layout())
215
+ self._text = ''
216
+ self.text = self.startText = text if text is not None else ""
217
+
218
+ # caret
219
+ self.editable = editable
220
+ self.caret = Caret(self, color=self.color, width=2)
221
+
222
+
223
+ self.autoLog = autoLog
224
+
225
+ @property
226
+ def editable(self):
227
+ return self._editable
228
+
229
+ @editable.setter
230
+ def editable(self, editable):
231
+ self._editable = editable
232
+ if editable is False:
233
+ if self.win:
234
+ self.win.removeEditable(self)
235
+ if editable is True:
236
+ if self.win:
237
+ self.win.addEditable(self)
238
+
239
+ @property
240
+ def pallette(self):
241
+ self._pallette = {
242
+ False: {
243
+ 'lineColor': self._borderColor,
244
+ 'lineWidth': self.borderWidth,
245
+ 'fillColor': self._fillColor
246
+ },
247
+ True: {
248
+ 'lineColor': self._borderColor-0.1,
249
+ 'lineWidth': self.borderWidth+1,
250
+ 'fillColor': self._fillColor+0.1
251
+ }
252
+ }
253
+ return self._pallette[self.hasFocus]
254
+
255
+ @pallette.setter
256
+ def pallette(self, value):
257
+ self._pallette = {
258
+ False: value,
259
+ True: value
260
+ }
261
+
262
+ @property
263
+ def foreColor(self):
264
+ return ColorMixin.foreColor.fget(self)
265
+ @foreColor.setter
266
+ def foreColor(self, value):
267
+ ColorMixin.foreColor.fset(self, value)
268
+ self._layout()
269
+ if hasattr(self, "foreColor") and hasattr(self, 'caret'):
270
+ self.caret.color = self._foreColor
271
+
272
+ @attributeSetter
273
+ def font(self, fontName, italic=False, bold=False):
274
+ if isinstance(fontName, GLFont):
275
+ self.glFont = fontName
276
+ self.__dict__['font'] = fontName.name
277
+ else:
278
+ self.__dict__['font'] = fontName
279
+ self.glFont = allFonts.getFont(
280
+ fontName,
281
+ size=self.letterHeightPix,
282
+ bold=self.bold, italic=self.italic)
283
+
284
+ @property
285
+ def units(self):
286
+ return WindowMixin.units.fget(self)
287
+
288
+ @units.setter
289
+ def units(self, value):
290
+ WindowMixin.units.fset(self, value)
291
+ if hasattr(self, "box"):
292
+ self.box.units = value
293
+ if hasattr(self, "contentBox"):
294
+ self.contentBox.units = value
295
+ if hasattr(self, "caret"):
296
+ self.caret.units = value
297
+
298
+ @property
299
+ def size(self):
300
+ return WindowMixin.size.fget(self)
301
+
302
+ @size.setter
303
+ def size(self, value):
304
+ WindowMixin.size.fset(self, value)
305
+ if hasattr(self, "box"):
306
+ self.box.size = self._size
307
+ if hasattr(self, "contentBox"):
308
+ self.contentBox.size = self._size - self._padding * 2
309
+ # Refresh pos
310
+ self.pos = self.pos
311
+
312
+ @property
313
+ def pos(self):
314
+ """The position of the center of the TextBox in the stimulus
315
+ :ref:`units <units>`
316
+
317
+ `value` should be an :ref:`x,y-pair <attrib-xy>`.
318
+ :ref:`Operations <attrib-operations>` are also supported.
319
+
320
+ Example::
321
+
322
+ stim.pos = (0.5, 0) # Set slightly to the right of center
323
+ stim.pos += (0.5, -1) # Increment pos rightwards and upwards.
324
+ Is now (1.0, -1.0)
325
+ stim.pos *= 0.2 # Move stim towards the center.
326
+ Is now (0.2, -0.2)
327
+
328
+ Tip: If you need the position of stim in pixels, you can obtain
329
+ it like this:
330
+
331
+ myTextbox._pos.pix
332
+ """
333
+ return WindowMixin.pos.fget(self)
334
+
335
+ @pos.setter
336
+ def pos(self, value):
337
+ WindowMixin.pos.fset(self, value)
338
+ if hasattr(self, "box"):
339
+ self.box.size = self._pos
340
+ if hasattr(self, "contentBox"):
341
+ # Content box should be anchored center relative to box, but its pos needs to be relative to box's vertices, not its pos
342
+ self.contentBox.pos = self.pos + self.size * self.box._vertices.anchorAdjust
343
+ self.contentBox._needVertexUpdate = True
344
+
345
+ # Set caret pos again so it recalculates its vertices
346
+ if hasattr(self, "caret"):
347
+ self.caret.index = self.caret.index
348
+
349
+ self._needVertexUpdate = True
350
+
351
+ @property
352
+ def vertices(self):
353
+ return WindowMixin.vertices.fget(self)
354
+
355
+ @vertices.setter
356
+ def vertices(self, value):
357
+ # If None, use defaut
358
+ if value is None:
359
+ value = [
360
+ [0.5, -0.5],
361
+ [-0.5, -0.5],
362
+ [-0.5, 0.5],
363
+ [0.5, 0.5],
364
+ ]
365
+ # Create Vertices object
366
+ self._vertices = layout.Vertices(value, obj=self.contentBox, flip=self.flip)
367
+
368
+ @property
369
+ def padding(self):
370
+ if hasattr(self, "_padding"):
371
+ return getattr(self._padding, self.units)
372
+
373
+ @padding.setter
374
+ def padding(self, value):
375
+ # Substitute None for a default value
376
+ if value is None:
377
+ value = self.letterHeight / 2
378
+ # Create a Size object to handle padding
379
+ self._padding = layout.Size(value, self.units, self.win)
380
+ # Update size of bounding box
381
+ if hasattr(self, "contentBox") and hasattr(self, "_size"):
382
+ self.contentBox.size = self._size - self._padding * 2
383
+
384
+ @property
385
+ def letterHeight(self):
386
+ if hasattr(self, "_letterHeight"):
387
+ return getattr(self._letterHeight, self.units)[1]
388
+
389
+ @letterHeight.setter
390
+ def letterHeight(self, value):
391
+ if isinstance(value, layout.Vector):
392
+ # If given a Vector, use it directly
393
+ self._letterHeight = value
394
+ elif isinstance(value, (int, float)):
395
+ # If given an integer, convert it to a 2D Vector with width 0
396
+ self._letterHeight = layout.Size([0, value], units=self.units, win=self.win)
397
+ elif value is None:
398
+ # If None, use default (20px)
399
+ self._letterHeight = layout.Size([0, 20], units='pix', win=self.win)
400
+ elif isinstance(value, (list, tuple, np.ndarray)):
401
+ # If given an array, convert it to a Vector
402
+ self._letterHeight = layout.Size(value, units=self.units, win=self.win)
403
+
404
+ @property
405
+ def letterHeightPix(self):
406
+ """
407
+ Convenience function to get self._letterHeight.pix and be guaranteed a return that is a single integer
408
+ """
409
+ return self._letterHeight.pix[1]
410
+
411
+ @property
412
+ def lineSpacing(self):
413
+ return self.glFont.lineSpacing
414
+
415
+ @lineSpacing.setter
416
+ def lineSpacing(self, value):
417
+ self.glFont.lineSpacing = value
418
+ self._needVertexUpdate = True
419
+
420
+ @property
421
+ def fontMGR(self):
422
+ return allFonts
423
+
424
+ @fontMGR.setter
425
+ def fontMGR(self, mgr):
426
+ global allFonts
427
+ if isinstance(mgr, FontManager):
428
+ allFonts = mgr
429
+ else:
430
+ raise TypeError(f"Could not set font manager for TextBox2 object `{self.name}`, must be supplied with a FontManager object")
431
+
432
+ @property
433
+ def anchor(self):
434
+ return self.box.anchor
435
+
436
+ @anchor.setter
437
+ def anchor(self, anchor):
438
+ # Box should use this anchor
439
+ self.box.anchor = anchor
440
+ # Set pos again to update sub-element vertices
441
+ self.pos = self.pos
442
+
443
+ @property
444
+ def alignment(self):
445
+ if hasattr(self, "_alignX") and hasattr(self, "_alignY"):
446
+ return (self._alignX, self._alignY)
447
+ else:
448
+ return ("top", "left")
449
+
450
+ @alignment.setter
451
+ def alignment(self, alignment):
452
+ # look for unambiguous terms first (top, bottom, left, right)
453
+ self._alignY = None
454
+ self._alignX = None
455
+ if 'top' in alignment:
456
+ self._alignY = 'top'
457
+ elif 'bottom' in alignment:
458
+ self._alignY = 'bottom'
459
+ if 'right' in alignment:
460
+ self._alignX = 'right'
461
+ elif 'left' in alignment:
462
+ self._alignX = 'left'
463
+ # then 'center' can apply to either axis that isn't already set
464
+ if self._alignX is None:
465
+ self._alignX = 'center'
466
+ if self._alignY is None:
467
+ self._alignY = 'center'
468
+
469
+ self._needVertexUpdate = True
470
+ if hasattr(self, "_text"):
471
+ # If text has been set, layout
472
+ self._layout()
473
+
474
+ @property
475
+ def text(self):
476
+ lastFormatter = NONE
477
+ formatted_text = ''
478
+ styles = self._styles
479
+ for i, c in enumerate(self._text):
480
+ if styles[i] == ITALIC and lastFormatter != styles[i]:
481
+ formatted_text+='<i>%s'%(c)
482
+ elif styles[i] == BOLD and lastFormatter != styles[i]:
483
+ formatted_text+='<b>%s'%(c)
484
+
485
+ elif styles[i] != ITALIC and lastFormatter == ITALIC:
486
+ formatted_text+='</i>%s'%(c)
487
+ elif styles[i] != BOLD and lastFormatter == BOLD:
488
+ formatted_text+='</b>%s'%(c)
489
+ else:
490
+ formatted_text+=c
491
+ lastFormatter = styles[i]
492
+ return formatted_text
493
+
494
+ @text.setter
495
+ def text(self, text):
496
+ # Convert to string
497
+ text = str(text)
498
+ # Substitute HTML tags
499
+ text = text.replace('<i>', codes['ITAL_START'])
500
+ text = text.replace('</i>', codes['ITAL_END'])
501
+ text = text.replace('<b>', codes['BOLD_START'])
502
+ text = text.replace('</b>', codes['BOLD_END'])
503
+ visible_text = ''.join([c for c in text if c not in codes.values()])
504
+ self._styles = [0,]*len(visible_text)
505
+ self._text = visible_text
506
+
507
+ current_style=0
508
+ ci = 0
509
+ for c in text:
510
+ if c == codes['ITAL_START']:
511
+ current_style += ITALIC
512
+ elif c == codes['BOLD_START']:
513
+ current_style += BOLD
514
+ elif c == codes['BOLD_END']:
515
+ current_style -= BOLD
516
+ elif c == codes['ITAL_END']:
517
+ current_style -= ITALIC
518
+ else:
519
+ self._styles[ci]=current_style
520
+ ci+=1
521
+
522
+ self._layout()
523
+
524
+ def addCharAtCaret(self, char):
525
+ txt = self._text
526
+ txt = txt[:self.caret.index] + char + txt[self.caret.index:]
527
+ cstyle = NONE
528
+ if len(self._styles) and self.caret.index <= len(self._styles):
529
+ cstyle = self._styles[self.caret.index-1]
530
+ self._styles.insert(self.caret.index, cstyle)
531
+ self.caret.index += 1
532
+ self._text = txt
533
+ self._layout()
534
+
535
+ def deleteCaretLeft(self):
536
+ if self.caret.index > 0:
537
+ txt = self._text
538
+ ci = self.caret.index
539
+ txt = txt[:ci-1] + txt[ci:]
540
+ self._styles = self._styles[:ci-1]+self._styles[ci:]
541
+ self.caret.index -= 1
542
+ self._text = txt
543
+ self._layout()
544
+
545
+ def deleteCaretRight(self):
546
+ ci = self.caret.index
547
+ if ci < len(self._text):
548
+ txt = self._text
549
+ txt = txt[:ci] + txt[ci+1:]
550
+ self._styles = self._styles[:ci]+self._styles[ci+1:]
551
+ self._text = txt
552
+ self._layout()
553
+
554
+ def _layout(self):
555
+ """Layout the text, calculating the vertex locations
556
+ """
557
+
558
+ rgb = self._foreColor.render('rgba1')
559
+ font = self.glFont
560
+
561
+ # the vertices are initially pix (natural for freetype)
562
+ # then we convert them to the requested units for self._vertices
563
+ # then they are converted back during rendering using standard BaseStim
564
+ visible_text = self._text
565
+ vertices = np.zeros((len(visible_text) * 4, 2), dtype=np.float32)
566
+ self._charIndices = np.zeros((len(visible_text)), dtype=int)
567
+ self._colors = np.zeros((len(visible_text) * 4, 4), dtype=np.double)
568
+ self._texcoords = np.zeros((len(visible_text) * 4, 2), dtype=np.double)
569
+ self._glIndices = np.zeros((len(visible_text) * 4), dtype=int)
570
+
571
+ # the following are used internally for layout
572
+ self._lineNs = np.zeros(len(visible_text), dtype=int)
573
+ _lineBottoms = []
574
+ self._lineLenChars = [] #
575
+ _lineWidths = [] # width in stim units of each line
576
+
577
+ lineMax = self.contentBox._size.pix[0]
578
+ current = [0, 0 - font.ascender]
579
+ fakeItalic = 0.0
580
+ fakeBold = 0.0
581
+ # for some reason glyphs too wide when using alpha channel only
582
+ if font.atlas.format == 'alpha':
583
+ alphaCorrection = 1 / 3.0
584
+ else:
585
+ alphaCorrection = 1
586
+
587
+ if self._lineBreaking == 'default':
588
+
589
+ wordLen = 0
590
+ charsThisLine = 0
591
+ wordsThisLine = 0
592
+ lineN = 0
593
+
594
+ for i, charcode in enumerate(self._text):
595
+ printable = True # unless we decide otherwise
596
+ # handle formatting codes
597
+ if self._styles[i] == NONE:
598
+ fakeItalic = 0.0
599
+ fakeBold = 0.0
600
+ elif self._styles[i] == ITALIC:
601
+ fakeItalic = 0.1 * font.size
602
+ elif self._styles[i] == BOLD:
603
+ fakeBold = 0.3 * font.size
604
+
605
+ # handle newline
606
+ if charcode == '\n':
607
+ printable = False
608
+
609
+ # handle printable characters
610
+ if printable:
611
+ glyph = font[charcode]
612
+ if showWhiteSpace and charcode == " ":
613
+ glyph = font[u"·"]
614
+ elif charcode == " ":
615
+ # glyph size of space is smaller than actual size, so use size of dot instead
616
+ glyph.size = font[u"·"].size
617
+ # Get top and bottom coords
618
+ yTop = current[1] + glyph.offset[1]
619
+ yBot = yTop - glyph.size[1]
620
+ # Get x mid point
621
+ xMid = current[0] + glyph.offset[0] + glyph.size[0] * alphaCorrection / 2 + fakeBold / 2
622
+ # Get left and right corners from midpoint
623
+ xBotL = xMid - glyph.size[0] * alphaCorrection / 2 - fakeItalic - fakeBold / 2
624
+ xBotR = xMid + glyph.size[0] * alphaCorrection / 2 - fakeItalic + fakeBold / 2
625
+ xTopL = xMid - glyph.size[0] * alphaCorrection / 2 - fakeBold / 2
626
+ xTopR = xMid + glyph.size[0] * alphaCorrection / 2 + fakeBold / 2
627
+
628
+ u0 = glyph.texcoords[0]
629
+ v0 = glyph.texcoords[1]
630
+ u1 = glyph.texcoords[2]
631
+ v1 = glyph.texcoords[3]
632
+ else:
633
+ glyph = font[u"·"]
634
+ x = current[0] + glyph.offset[0]
635
+ yTop = current[1] + glyph.offset[1]
636
+ yBot = yTop - glyph.size[1]
637
+ xBotL = x
638
+ xTopL = x
639
+ xBotR = x
640
+ xTopR = x
641
+ u0 = glyph.texcoords[0]
642
+ v0 = glyph.texcoords[1]
643
+ u1 = glyph.texcoords[2]
644
+ v1 = glyph.texcoords[3]
645
+
646
+ theseVertices = [[xTopL, yTop], [xBotL, yBot],
647
+ [xBotR, yBot], [xTopR, yTop]]
648
+ texcoords = [[u0, v0], [u0, v1],
649
+ [u1, v1], [u1, v0]]
650
+
651
+ vertices[i * 4:i * 4 + 4] = theseVertices
652
+ self._texcoords[i * 4:i * 4 + 4] = texcoords
653
+ self._colors[i*4 : i*4+4, :4] = rgb
654
+ self._lineNs[i] = lineN
655
+ current[0] = current[0] + glyph.advance[0] + fakeBold / 2
656
+ current[1] = current[1] + glyph.advance[1]
657
+
658
+ # are we wrapping the line?
659
+ if charcode == "\n":
660
+ lineWPix = current[0]
661
+ current[0] = 0
662
+ current[1] -= font.height
663
+ lineN += 1
664
+ charsThisLine += 1
665
+ self._lineLenChars.append(charsThisLine)
666
+ _lineWidths.append(lineWPix)
667
+ charsThisLine = 0
668
+ wordsThisLine = 0
669
+ elif charcode in wordBreaks:
670
+ wordLen = 0
671
+ charsThisLine += 1
672
+ wordsThisLine += 1
673
+ elif printable:
674
+ wordLen += 1
675
+ charsThisLine += 1
676
+
677
+ # end line with auto-wrap on space
678
+ if current[0] >= lineMax and wordLen > 0 and wordsThisLine > 1:
679
+ # move the current word to next line
680
+ lineBreakPt = vertices[(i - wordLen + 1) * 4, 0]
681
+ wordWidth = current[0] - lineBreakPt
682
+ # shift all chars of the word left by wordStartX
683
+ vertices[(i - wordLen + 1) * 4: (i + 1) * 4, 0] -= lineBreakPt
684
+ vertices[(i - wordLen + 1) * 4: (i + 1) * 4, 1] -= font.height
685
+ # update line values
686
+ self._lineNs[i - wordLen + 1: i + 1] += 1
687
+ self._lineLenChars.append(charsThisLine - wordLen)
688
+ _lineWidths.append(lineBreakPt)
689
+ lineN += 1
690
+ # and set current to correct location
691
+ current[0] = wordWidth
692
+ current[1] -= font.height
693
+ charsThisLine = wordLen
694
+ wordsThisLine = 1
695
+
696
+ # have we stored the top/bottom of this line yet
697
+ if lineN + 1 > len(_lineBottoms):
698
+ _lineBottoms.append(current[1])
699
+
700
+ # add length of this (unfinished) line
701
+ _lineWidths.append(current[0])
702
+ self._lineLenChars.append(charsThisLine)
703
+
704
+ elif self._lineBreaking == 'uax14':
705
+
706
+ # get a list of line-breakable points according to UAX#14
707
+ breakable_points = list(get_breakable_points(self._text))
708
+ text_seg = list(break_units(self._text, breakable_points))
709
+ styles_seg = list(break_units(self._styles, breakable_points))
710
+
711
+ lineN = 0
712
+ charwidth_list = []
713
+ segwidth_list = []
714
+ y_advance_list = []
715
+ vertices_list = []
716
+ texcoords_list = []
717
+
718
+ # calculate width of each segments
719
+ for this_seg in range(len(text_seg)):
720
+
721
+ thisSegWidth = 0 # width of this segment
722
+
723
+ for i, charcode in enumerate(text_seg[this_seg]):
724
+ printable = True # unless we decide otherwise
725
+ # handle formatting codes
726
+ if styles_seg[this_seg][i] == NONE:
727
+ fakeItalic = 0.0
728
+ fakeBold = 0.0
729
+ elif styles_seg[this_seg][i] == ITALIC:
730
+ fakeItalic = 0.1 * font.size
731
+ elif styles_seg[this_seg][i] == ITALIC:
732
+ fakeBold = 0.3 * font.size
733
+
734
+ # handle newline
735
+ if charcode == '\n':
736
+ printable = False
737
+
738
+ # handle printable characters
739
+ if printable:
740
+ if showWhiteSpace and charcode == " ":
741
+ glyph = font[u"·"]
742
+ else:
743
+ glyph = font[charcode]
744
+ xBotL = glyph.offset[0] - fakeItalic - fakeBold / 2
745
+ xTopL = glyph.offset[0] - fakeBold / 2
746
+ yTop = glyph.offset[1]
747
+ xBotR = xBotL + glyph.size[0] * alphaCorrection + fakeBold
748
+ xTopR = xTopL + glyph.size[0] * alphaCorrection + fakeBold
749
+ yBot = yTop - glyph.size[1]
750
+ u0 = glyph.texcoords[0]
751
+ v0 = glyph.texcoords[1]
752
+ u1 = glyph.texcoords[2]
753
+ v1 = glyph.texcoords[3]
754
+ else:
755
+ glyph = font[u"·"]
756
+ x = glyph.offset[0]
757
+ yTop = glyph.offset[1]
758
+ yBot = yTop - glyph.size[1]
759
+ xBotL = x
760
+ xTopL = x
761
+ xBotR = x
762
+ xTopR = x
763
+ u0 = glyph.texcoords[0]
764
+ v0 = glyph.texcoords[1]
765
+ u1 = glyph.texcoords[2]
766
+ v1 = glyph.texcoords[3]
767
+
768
+ # calculate width and update segment width
769
+ w = glyph.advance[0] + fakeBold / 2
770
+ thisSegWidth += w
771
+
772
+ # keep vertices, texcoords, width and y_advance of this character
773
+ vertices_list.append([[xTopL, yTop], [xBotL, yBot],
774
+ [xBotR, yBot], [xTopR, yTop]])
775
+ texcoords_list.append([[u0, v0], [u0, v1],
776
+ [u1, v1], [u1, v0]])
777
+ charwidth_list.append(w)
778
+ y_advance_list.append(glyph.advance[1])
779
+
780
+ # append width of this segment to the list
781
+ segwidth_list.append(thisSegWidth)
782
+
783
+ # concatenate segments to build line
784
+ lines = []
785
+ while text_seg:
786
+ line_width = 0
787
+ for i in range(len(text_seg)):
788
+ # if this segment is \n, break line here.
789
+ if text_seg[i][-1] == '\n':
790
+ i+=1 # increment index to include \n to current line
791
+ break
792
+ # concatenate next segment
793
+ line_width += segwidth_list[i]
794
+ # break if line_width is greater than lineMax
795
+ if lineMax < line_width:
796
+ break
797
+ else:
798
+ # if for sentence finished without break, all segments
799
+ # should be concatenated.
800
+ i = len(text_seg)
801
+ p = max(1, i)
802
+ # concatenate segments and remove from segment list
803
+ lines.append("".join(text_seg[:p]))
804
+ del text_seg[:p], segwidth_list[:p] #, avoid[:p]
805
+
806
+ # build lines
807
+ i = 0 # index of the current character
808
+ if lines:
809
+ for line in lines:
810
+ for c in line:
811
+ theseVertices = vertices_list[i]
812
+ #update vertices
813
+ for j in range(4):
814
+ theseVertices[j][0] += current[0]
815
+ theseVertices[j][1] += current[1]
816
+ texcoords = texcoords_list[i]
817
+
818
+ vertices[i * 4:i * 4 + 4] = theseVertices
819
+ self._texcoords[i * 4:i * 4 + 4] = texcoords
820
+ self._colors[i*4 : i*4+4, :4] = rgb
821
+ self._lineNs[i] = lineN
822
+
823
+ current[0] = current[0] + charwidth_list[i]
824
+ current[1] = current[1] + y_advance_list[i]
825
+
826
+ # have we stored the top/bottom of this line yet
827
+ if lineN + 1 > len(_lineBottoms):
828
+ _lineBottoms.append(current[1])
829
+
830
+ # next chacactor
831
+ i += 1
832
+
833
+ # prepare for next line
834
+ current[0] = 0
835
+ current[1] -= font.height
836
+
837
+ lineBreakPt = vertices[(i-1) * 4, 0]
838
+ self._lineLenChars.append(len(line))
839
+ _lineWidths.append(lineBreakPt)
840
+
841
+ # need not increase lineN when the last line doesn't end with '\n'
842
+ if lineN < len(lines)-1 or line[-1] == '\n' :
843
+ lineN += 1
844
+ else:
845
+ raise ValueError("Unknown lineBreaking option ({}) is"
846
+ "specified.".format(self._lineBreaking))
847
+
848
+ # Apply vertical alignment
849
+ if self.alignment[1] in ("bottom", "center"):
850
+ # Get bottom of last line (or starting line, if there are none)
851
+ if len(_lineBottoms):
852
+ lastLine = min(_lineBottoms)
853
+ else:
854
+ lastLine = current[1]
855
+ if self.alignment[1] == "bottom":
856
+ # Work out how much we need to adjust by for the bottom base line to sit at the bottom of the content box
857
+ adjustY = lastLine + self.contentBox._size.pix[1]
858
+ if self.alignment[1] == "center":
859
+ # Work out how much we need to adjust by for the line midpoint (mean of ascender and descender) to sit in the middle of the content box
860
+ adjustY = (lastLine + font.descender + self.contentBox._size.pix[1]) / 2
861
+ # Adjust vertices and line bottoms
862
+ vertices[:, 1] = vertices[:, 1] - adjustY
863
+ _lineBottoms -= adjustY
864
+
865
+ # Apply horizontal alignment
866
+ if self.alignment[0] in ("right", "center"):
867
+ if self.alignment[0] == "right":
868
+ # Calculate adjust value per line
869
+ lineAdjustX = self.contentBox._size.pix[0] - np.array(_lineWidths)
870
+ if self.alignment[0] == "center":
871
+ # Calculate adjust value per line
872
+ lineAdjustX = (self.contentBox._size.pix[0] - np.array(_lineWidths)) / 2
873
+ # Get adjust value per vertex
874
+ adjustX = lineAdjustX[np.repeat(self._lineNs, 4)]
875
+ # Adjust vertices
876
+ vertices[:, 0] = vertices[:, 0] + adjustX
877
+
878
+ # Convert the vertices to be relative to content box and set
879
+ self.vertices = vertices / self.contentBox._size.pix + (-0.5, 0.5)
880
+ if len(_lineBottoms):
881
+ self._lineBottoms = max(self.contentBox._vertices.pix[:, 1]) + np.array(_lineBottoms)
882
+ self._lineWidths = min(self.contentBox._vertices.pix[:, 0]) + np.array(_lineWidths)
883
+ else:
884
+ self._lineBottoms = np.array(_lineBottoms)
885
+ self._lineWidths = np.array(_lineWidths)
886
+
887
+ # if we had to add more glyphs to make possible then
888
+ if self.glFont._dirty:
889
+ self.glFont.upload()
890
+ self.glFont._dirty = False
891
+ self._needVertexUpdate = True
892
+
893
+ def draw(self):
894
+ """Draw the text to the back buffer"""
895
+ # Border width
896
+ self.box.setLineWidth(self.pallette['lineWidth']) # Use 1 as base if border width is none
897
+ #self.borderWidth = self.box.lineWidth
898
+ # Border colour
899
+ self.box.setLineColor(self.pallette['lineColor'], colorSpace='rgb')
900
+ #self.borderColor = self.box.lineColor
901
+ # Background
902
+ self.box.setFillColor(self.pallette['fillColor'], colorSpace='rgb')
903
+ #self.fillColor = self.box.fillColor
904
+
905
+ if self._needVertexUpdate:
906
+ #print("Updating vertices...")
907
+ self._updateVertices()
908
+ if self.fillColor is not None or self.borderColor is not None:
909
+ self.box.draw()
910
+
911
+ # Draw sub-elements if in debug mode
912
+ if debug:
913
+ self.contentBox.draw()
914
+ self.boundingBox.draw()
915
+
916
+ # self.boundingBox.draw() # could draw for debug purposes
917
+ gl.glPushMatrix()
918
+ self.win.setScale('pix')
919
+
920
+ gl.glActiveTexture(gl.GL_TEXTURE0)
921
+ gl.glBindTexture(gl.GL_TEXTURE_2D, self.glFont.textureID)
922
+ gl.glEnable(gl.GL_TEXTURE_2D)
923
+ gl.glDisable(gl.GL_DEPTH_TEST)
924
+
925
+ gl.glEnableClientState(gl.GL_VERTEX_ARRAY)
926
+ gl.glEnableClientState(gl.GL_COLOR_ARRAY)
927
+ gl.glEnableClientState(gl.GL_TEXTURE_COORD_ARRAY)
928
+ gl.glEnableClientState(gl.GL_VERTEX_ARRAY)
929
+
930
+ gl.glVertexPointer(2, gl.GL_DOUBLE, 0, self.verticesPix.ctypes)
931
+ gl.glColorPointer(4, gl.GL_DOUBLE, 0, self._colors.ctypes)
932
+ gl.glTexCoordPointer(2, gl.GL_DOUBLE, 0, self._texcoords.ctypes)
933
+
934
+ self.shader.bind()
935
+ self.shader.setInt('texture', 0)
936
+ self.shader.setFloat('pixel', [1.0 / 512, 1.0 / 512])
937
+ nVerts = len(self._text)*4
938
+
939
+ gl.glDrawArrays(gl.GL_QUADS, 0, nVerts)
940
+ self.shader.unbind()
941
+
942
+ # removed the colors and font texture
943
+ gl.glDisableClientState(gl.GL_COLOR_ARRAY)
944
+ gl.glDisableClientState(gl.GL_TEXTURE_COORD_ARRAY)
945
+ gl.glDisableVertexAttribArray(1)
946
+ gl.glDisableClientState(gl.GL_VERTEX_ARRAY)
947
+
948
+ gl.glActiveTexture(gl.GL_TEXTURE0)
949
+ gl.glBindTexture(gl.GL_TEXTURE_2D, 0)
950
+ gl.glDisable(gl.GL_TEXTURE_2D)
951
+
952
+ if self.hasFocus: # draw caret line
953
+ self.caret.draw()
954
+
955
+ gl.glPopMatrix()
956
+
957
+ def reset(self):
958
+ # Reset contents
959
+ self.text = self.startText
960
+
961
+ def clear(self):
962
+ # Clear contents
963
+ self.text = ""
964
+
965
+
966
+ def contains(self, x, y=None, units=None, tight=False):
967
+ """Returns True if a point x,y is inside the stimulus' border.
968
+
969
+ Can accept variety of input options:
970
+ + two separate args, x and y
971
+ + one arg (list, tuple or array) containing two vals (x,y)
972
+ + an object with a getPos() method that returns x,y, such
973
+ as a :class:`~psychopy.event.Mouse`.
974
+
975
+ Returns `True` if the point is within the area defined either by its
976
+ `border` attribute (if one defined), or its `vertices` attribute if
977
+ there is no .border. This method handles
978
+ complex shapes, including concavities and self-crossings.
979
+
980
+ Note that, if your stimulus uses a mask (such as a Gaussian) then
981
+ this is not accounted for by the `contains` method; the extent of the
982
+ stimulus is determined purely by the size, position (pos), and
983
+ orientation (ori) settings (and by the vertices for shape stimuli).
984
+
985
+ See Coder demos: shapeContains.py
986
+ See Coder demos: shapeContains.py
987
+ """
988
+ if tight:
989
+ return self.boundingBox.contains(x, y, units)
990
+ else:
991
+ return self.box.contains(x, y, units)
992
+
993
+ def overlaps(self, polygon, tight=False):
994
+ """Returns `True` if this stimulus intersects another one.
995
+
996
+ If `polygon` is another stimulus instance, then the vertices
997
+ and location of that stimulus will be used as the polygon.
998
+ Overlap detection is typically very good, but it
999
+ can fail with very pointy shapes in a crossed-swords configuration.
1000
+
1001
+ Note that, if your stimulus uses a mask (such as a Gaussian blob)
1002
+ then this is not accounted for by the `overlaps` method; the extent
1003
+ of the stimulus is determined purely by the size, pos, and
1004
+ orientation settings (and by the vertices for shape stimuli).
1005
+
1006
+ Parameters
1007
+
1008
+ See coder demo, shapeContains.py
1009
+ """
1010
+ if tight:
1011
+ return self.boundingBox.overlaps(polygon)
1012
+ else:
1013
+ return self.box.overlaps(polygon)
1014
+
1015
+ def _updateVertices(self):
1016
+ """Sets Stim.verticesPix and ._borderPix from pos, size, ori,
1017
+ flipVert, flipHoriz
1018
+ """
1019
+ # check whether stimulus needs flipping in either direction
1020
+ flip = np.array([1, 1])
1021
+ if hasattr(self, 'flipHoriz') and self.flipHoriz:
1022
+ flip[0] = -1 # True=(-1), False->(+1)
1023
+ if hasattr(self, 'flipVert') and self.flipVert:
1024
+ flip[1] = -1 # True=(-1), False->(+1)
1025
+
1026
+ self.__dict__['verticesPix'] = self._vertices.pix
1027
+
1028
+ # tight bounding box
1029
+ if hasattr(self._vertices, self.units) and self.vertices.shape[0] >= 1:
1030
+ verts = self._vertices.pix
1031
+ L = verts[:, 0].min()
1032
+ R = verts[:, 0].max()
1033
+ B = verts[:, 1].min()
1034
+ T = verts[:, 1].max()
1035
+ tightW = R-L
1036
+ Xmid = (R+L)/2
1037
+ tightH = T-B
1038
+ Ymid = (T+B)/2
1039
+ # for the tight box anchor offset is included in vertex calcs
1040
+ self.boundingBox.size = tightW, tightH
1041
+ self.boundingBox.pos = self.pos + (Xmid, Ymid)
1042
+ <<<<<<< HEAD
1043
+ else:
1044
+ self.boundingBox.size = 0, 0
1045
+ self.boundingBox.pos = self.pos
1046
+ # box (larger than bounding box) needs anchor offest adding
1047
+ self.box.pos = self.pos
1048
+ =======
1049
+ # box (larger than bounding box) needs anchor offset adding
1050
+ self.box.pos = self.pos + (boxOffsetX, boxOffsetY)
1051
+ >>>>>>> release
1052
+ self.box.size = self.size # this might have changed from _requested
1053
+
1054
+ self._needVertexUpdate = False
1055
+
1056
+ def _onText(self, chr):
1057
+ """Called by the window when characters are received"""
1058
+ if chr == '\t':
1059
+ self.win.nextEditable()
1060
+ return
1061
+ if chr == '\r': # make it newline not Carriage Return
1062
+ chr = '\n'
1063
+ self.addCharAtCaret(chr)
1064
+ if self.onTextCallback:
1065
+ self.onTextCallback()
1066
+
1067
+ def _onCursorKeys(self, key):
1068
+ """Called by the window when cursor/del/backspace... are received"""
1069
+ if key == 'MOTION_UP':
1070
+ self.caret.row -= 1
1071
+ elif key == 'MOTION_DOWN':
1072
+ self.caret.row += 1
1073
+ elif key == 'MOTION_RIGHT':
1074
+ self.caret.char += 1
1075
+ elif key == 'MOTION_LEFT':
1076
+ self.caret.char -= 1
1077
+ elif key == 'MOTION_BACKSPACE':
1078
+ self.deleteCaretLeft()
1079
+ elif key == 'MOTION_DELETE':
1080
+ self.deleteCaretRight()
1081
+ elif key == 'MOTION_NEXT_WORD':
1082
+ pass
1083
+ elif key == 'MOTION_PREVIOUS_WORD':
1084
+ pass
1085
+ elif key == 'MOTION_BEGINNING_OF_LINE':
1086
+ self.caret.char = 0
1087
+ elif key == 'MOTION_END_OF_LINE':
1088
+ self.caret.char = END_OF_THIS_LINE
1089
+ elif key == 'MOTION_NEXT_PAGE':
1090
+ pass
1091
+ elif key == 'MOTION_PREVIOUS_PAGE':
1092
+ pass
1093
+ elif key == 'MOTION_BEGINNING_OF_FILE':
1094
+ pass
1095
+ elif key == 'MOTION_END_OF_FILE':
1096
+ pass
1097
+ else:
1098
+ print("Received unhandled cursor motion type: ", key)
1099
+
1100
+ @property
1101
+ def hasFocus(self):
1102
+ if self.win and self.win.currentEditable == self:
1103
+ return True
1104
+ return False
1105
+
1106
+ @hasFocus.setter
1107
+ def hasFocus(self, focus):
1108
+ if focus is False and self.hasFocus:
1109
+ # If focus is being set to False, tell window to
1110
+ # give focus to next editable.
1111
+ if self.win:
1112
+ self.win.nextEditable()
1113
+ elif focus is True and self.hasFocus is False:
1114
+ # If focus is being set True, set textbox instance to be
1115
+ # window.currentEditable.
1116
+ if self.win:
1117
+ self.win.currentEditable=self
1118
+ return False
1119
+
1120
+ def getText(self):
1121
+ """Returns the current text in the box, including formatting tokens."""
1122
+ return self.text
1123
+
1124
+ @property
1125
+ def visibleText(self):
1126
+ """Returns the current visible text in the box"""
1127
+ return self._text
1128
+
1129
+ def getVisibleText(self):
1130
+ """Returns the current visible text in the box"""
1131
+ return self.visibleText
1132
+
1133
+ def setText(self, text=None, log=None):
1134
+ """Usually you can use 'stim.attribute = value' syntax instead,
1135
+ but use this method if you need to suppress the log message.
1136
+ """
1137
+ setAttribute(self, 'text', text, log)
1138
+
1139
+ def setHeight(self, height, log=None):
1140
+ """Usually you can use 'stim.attribute = value' syntax instead,
1141
+ but use this method if you need to suppress the log message. """
1142
+ setAttribute(self, 'height', height, log)
1143
+
1144
+ def setFont(self, font, log=None):
1145
+ """Usually you can use 'stim.attribute = value' syntax instead,
1146
+ but use this method if you need to suppress the log message.
1147
+ """
1148
+ setAttribute(self, 'font', font, log)
1149
+
1150
+
1151
+ class Caret(ColorMixin):
1152
+ """
1153
+ Class to handle the caret (cursor) within a textbox. Do **not** call without a textbox.
1154
+ Parameters
1155
+ ----------
1156
+ textbox : psychopy.visual.TextBox2
1157
+ Textbox which caret corresponds to
1158
+ visible : bool
1159
+ Whether the caret is visible
1160
+ row : int
1161
+ Textbox row which caret is on
1162
+ char : int
1163
+ Text character within row which caret is on
1164
+ index : int
1165
+ Index of character which caret is on
1166
+ vertices : list, tuple
1167
+ Coordinates of each corner of caret
1168
+ width : int, float
1169
+ Width of caret line
1170
+ color : list, tuple, str
1171
+ Caret colour
1172
+ """
1173
+
1174
+ def __init__(self, textbox, color, width, colorSpace='rgb'):
1175
+ self.textbox = textbox
1176
+ self.index = len(textbox._text) # start off at the end
1177
+ self.autoLog = False
1178
+ self.width = width
1179
+ self.units = textbox.units
1180
+ self.colorSpace = colorSpace
1181
+ self.color = color
1182
+
1183
+ def draw(self):
1184
+ if not self.visible:
1185
+ return
1186
+ if core.getTime() % 1 > 0.6: # Flash every other second
1187
+ return
1188
+ gl.glLineWidth(self.width)
1189
+ gl.glColor4f(
1190
+ *self._foreColor.rgba1
1191
+ )
1192
+ gl.glBegin(gl.GL_LINES)
1193
+ gl.glVertex2f(self.vertices[0, 0], self.vertices[0, 1])
1194
+ gl.glVertex2f(self.vertices[1, 0], self.vertices[1, 1])
1195
+ gl.glEnd()
1196
+
1197
+ @property
1198
+ def visible(self):
1199
+ return self.textbox.hasFocus
1200
+
1201
+ @property
1202
+ def row(self):
1203
+ """What row is caret on?"""
1204
+ # Check that index is with range of all character indices
1205
+ if len(self.textbox._lineNs) == 0: # no chars at all
1206
+ return 0
1207
+ elif self.index > len(self.textbox._lineNs):
1208
+ self.index = len(self.textbox._lineNs)
1209
+ # Get line of index
1210
+ if self.index >= len(self.textbox._lineNs):
1211
+ return self.textbox._lineNs[-1]
1212
+ else:
1213
+ return self.textbox._lineNs[self.index]
1214
+
1215
+ @row.setter
1216
+ def row(self, value):
1217
+ """Use line to index conversion to set index according to row value"""
1218
+ # Figure out how many characters into previous row the cursor was
1219
+ charsIn = self.char
1220
+ nRows = len(self.textbox._lineLenChars)
1221
+ # If new row is more than total number of rows, move to end of last row
1222
+ if value >= nRows:
1223
+ value = nRows
1224
+ charsIn = self.textbox._lineLenChars[-1]
1225
+ # If new row is less than 0, move to beginning of first row
1226
+ elif value < 0:
1227
+ value = 0
1228
+ charsIn = 0
1229
+ elif value == nRows-1 and charsIn > self.textbox._lineLenChars[value]:
1230
+ # last row last char
1231
+ charsIn = self.textbox._lineLenChars[value]
1232
+ elif charsIn > self.textbox._lineLenChars[value]-1:
1233
+ # end of a middle row (account for the newline)
1234
+ charsIn = self.textbox._lineLenChars[value]-1
1235
+ # Set new index in new row
1236
+ self.index = sum(self.textbox._lineLenChars[:value]) + charsIn
1237
+
1238
+ @property
1239
+ def char(self):
1240
+ """What character within current line is caret on?"""
1241
+ # Check that index is with range of all character indices
1242
+ self.index = min(self.index, len(self.textbox._lineNs))
1243
+ self.index = max(self.index, 0)
1244
+ # Get first index of line, subtract from index to get char
1245
+ return self.index - sum(self.textbox._lineLenChars[:self.row])
1246
+ @char.setter
1247
+ def char(self, value):
1248
+ """Set character within row"""
1249
+ # If setting char to less than 0, move to last char on previous line
1250
+ row = self.row
1251
+ if value < 0:
1252
+ if row == 0:
1253
+ value = 0
1254
+ else:
1255
+ row -= 1
1256
+ value = self.textbox._lineLenChars[row]-1 # end of that row
1257
+ elif row >= len(self.textbox._lineLenChars)-1 and \
1258
+ value >= self.textbox._lineLenChars[-1]:
1259
+ # this is the last row
1260
+ row = len(self.textbox._lineLenChars)-1
1261
+ value = self.textbox._lineLenChars[-1]
1262
+ elif value == END_OF_THIS_LINE:
1263
+ value = self.textbox._lineLenChars[row]-1
1264
+ elif value >= self.textbox._lineLenChars[row]:
1265
+ # end of this row (not the last) so go to next
1266
+ row += 1
1267
+ value = 0
1268
+ # then calculate index
1269
+ if row: # if not on first row
1270
+ self.index = sum(self.textbox._lineLenChars[:row]) + value
1271
+ else:
1272
+ self.index = value
1273
+
1274
+ @property
1275
+ def vertices(self):
1276
+ textbox = self.textbox
1277
+ # check we have a caret index
1278
+ if self.index is None or self.index > len(textbox._text):
1279
+ self.index = len(textbox._text)
1280
+ if self.index < 0:
1281
+ self.index = 0
1282
+ # Get vertices of caret based on characters and index
1283
+ ii = self.index
1284
+ if textbox.vertices.shape[0] == 0:
1285
+ # If there are no chars, put caret at start position (determined by alignment)
1286
+ if textbox.alignment[1] == "bottom":
1287
+ bottom = min(textbox.contentBox._vertices.pix[:, 1])
1288
+ elif textbox.alignment[1] == "center":
1289
+ bottom = (min(textbox.contentBox._vertices.pix[:, 1]) + max(textbox.contentBox._vertices.pix[:, 1]) - textbox.glFont.ascender - textbox.glFont.descender) / 2
1290
+ else:
1291
+ bottom = max(textbox.contentBox._vertices.pix[:, 1]) - textbox.glFont.ascender
1292
+ if textbox.alignment[0] == "right":
1293
+ x = max(textbox.contentBox._vertices.pix[:, 0])
1294
+ elif textbox.alignment[0] == "center":
1295
+ x = (min(textbox.contentBox._vertices.pix[:, 0]) + max(textbox.contentBox._vertices.pix[:, 0])) / 2
1296
+ else:
1297
+ x = min(textbox.contentBox._vertices.pix[:, 0])
1298
+ else:
1299
+ # Otherwise, get caret position from character vertices
1300
+ if self.index >= len(textbox._lineNs):
1301
+ # If the caret is after the last char, position it to the right
1302
+ chrVerts = textbox._vertices.pix[range((ii-1) * 4, (ii-1) * 4 + 4)]
1303
+ x = chrVerts[2, 0] # x-coord of left edge (of final char)
1304
+ else:
1305
+ # Otherwise, position it to the left
1306
+ chrVerts = textbox._vertices.pix[range(ii * 4, ii * 4 + 4)]
1307
+ x = chrVerts[1, 0] # x-coord of right edge
1308
+ # Get top of this line
1309
+ bottom = textbox._lineBottoms[self.row]
1310
+ # Top will always be line bottom + font height
1311
+ top = bottom + self.textbox.glFont.size
1312
+ return np.array([
1313
+ [x, bottom],
1314
+ [x, top]
1315
+ ])