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.
- psychopy/.DS_Store +0 -0
- psychopy/GIT_SHA +1 -1
- psychopy/VERSION +1 -1
- psychopy/__init__.py +10 -1
- psychopy/__init__.py.orig +65 -0
- psychopy/app/{locale/ar_001/.DS_Store → .DS_Store} +0 -0
- psychopy/app/Resources/.DS_Store +0 -0
- psychopy/app/_psychopyApp.py +11 -3
- psychopy/app/appData.spec +1 -1
- psychopy/app/builder/builder.py +1 -1
- psychopy/app/builder/builder.py.orig +3932 -0
- psychopy/app/builder/dialogs/__init__.py.orig +1679 -0
- psychopy/app/builder/dialogs/paramCtrls.py +1 -1
- psychopy/app/builder/dialogs/paramCtrls.py.orig +713 -0
- psychopy/app/colorpicker/__init__.py.orig +411 -0
- psychopy/app/cortex.log +0 -0
- psychopy/app/jobs.py +8 -1
- psychopy/app/locale/ar_001/LC_MESSAGE/messages.po +2452 -1731
- psychopy/app/locale/zh_CN/LC_MESSAGE/zh_CN.mo +0 -0
- psychopy/app/locale/zh_CN/LC_MESSAGE/zh_CN.po +6127 -0
- psychopy/app/locale/zh_CN/LC_MESSAGE/zh_CN_allFlagged.mo +0 -0
- psychopy/app/locale/zh_CN/LC_MESSAGE/zh_CN_allFlagged.po +7366 -0
- psychopy/app/plugin_manager/dialog.py +9 -7
- psychopy/app/ribbon.py +2 -1
- psychopy/app/runner/runner.py +7 -5
- psychopy/clock.py +8 -4
- psychopy/core.py.orig +169 -0
- psychopy/demos/builder/Design Templates/randomisedBlocks/html/index.html +23 -0
- psychopy/demos/builder/Design Templates/randomisedBlocks/html/randomisedBlocks-legacy-browsers.js +423 -0
- psychopy/demos/builder/Design Templates/randomisedBlocks/html/randomisedBlocks.js +427 -0
- psychopy/demos/builder/Design Templates/randomisedBlocks/html/resources/chooseBlock.xlsx +0 -0
- psychopy/demos/builder/Design Templates/randomisedBlocks/html/resources/facesBlock.xlsx +0 -0
- psychopy/demos/builder/Design Templates/randomisedBlocks/html/resources/housesBlock.xlsx +0 -0
- psychopy/demos/builder/Design Templates/randomisedBlocks/html/resources/stims/face01.jpg +0 -0
- psychopy/demos/builder/Design Templates/randomisedBlocks/html/resources/stims/face02.jpg +0 -0
- psychopy/demos/builder/Design Templates/randomisedBlocks/html/resources/stims/face03.jpg +0 -0
- psychopy/demos/builder/Design Templates/randomisedBlocks/html/resources/stims/house01.jpg +0 -0
- psychopy/demos/builder/Design Templates/randomisedBlocks/html/resources/stims/house02.jpg +0 -0
- psychopy/demos/builder/Design Templates/randomisedBlocks/html/resources/stims/house03.jpg +0 -0
- psychopy/demos/builder/Design Templates/randomisedBlocks/randomisedBlocks.py +330 -0
- psychopy/demos/builder/Design Templates/randomisedBlocks/randomisedBlocks_lastrun.py +330 -0
- psychopy/demos/builder/Feature Demos/eyetracking/eyetracking.xml +298 -0
- psychopy/demos/builder/Feature Demos/eyetracking/eyetracking.xsd +120 -0
- psychopy/demos/builder/Tools/.DS_Store +0 -0
- psychopy/demos/builder/Tools/gammaCalibration/.DS_Store +0 -0
- psychopy/demos/builder/Tools/gammaCalibration/data/_gamma_correction_visual_2022-05-18_14h18.29.439.csv +38 -0
- psychopy/demos/builder/Tools/gammaCalibration/data/_gamma_correction_visual_2022-05-18_14h18.29.439.log +3418 -0
- psychopy/demos/builder/Tools/gammaCalibration/data/_gamma_correction_visual_2022-05-18_14h18.29.439.psydat +0 -0
- psychopy/demos/builder/Tools/gammaCalibration/data/x1_gamma_correction_visual_2022-05-17_13h59.42.928.csv +2 -0
- psychopy/demos/builder/Tools/gammaCalibration/data/x1_gamma_correction_visual_2022-05-17_13h59.42.928.log +15 -0
- psychopy/demos/builder/Tools/gammaCalibration/data/x1_gamma_correction_visual_2022-05-17_13h59.42.928.psydat +0 -0
- psychopy/demos/builder/Tools/gammaCalibration/gamma_correction_visual.psyexp +323 -0
- psychopy/demos/builder/Tools/gammaCalibration/gamma_correction_visual.py +562 -0
- psychopy/demos/builder/Tools/gammaCalibration/gamma_correction_visual_lastrun.py +562 -0
- psychopy/demos/builder/Tools/gammaCalibration/questStairs.xlsx +0 -0
- psychopy/demos/builder/Tools/gammaCalibration/readme.md +0 -0
- psychopy/demos/builder/Tools/gammaCalibration/resources/low_contrast.png +0 -0
- psychopy/demos/builder/Tools/gammaCalibration/resources/make_2nd_order_tex.py +59 -0
- psychopy/demos/builder/Tools/gammaCalibration/resources/second_order_tex.png +0 -0
- psychopy/demos/coder/.DS_Store +0 -0
- psychopy/demos/coder/experiment control/info_gamma.pickle +0 -0
- psychopy/demos/coder/iohub/.iohpid +1 -0
- psychopy/demos/coder/iohub/eyetracking/.iohpid +1 -0
- psychopy/demos/coder/iohub/wintab/.DS_Store +0 -0
- psychopy/demos/coder/stimuli/.DS_Store +0 -0
- psychopy/demos/coder/stimuli/radialGratingContracting.py +29 -0
- psychopy/experiment/_experiment.py.orig +1032 -0
- psychopy/experiment/components/.DS_Store +0 -0
- psychopy/experiment/components/_base.py +13 -4
- psychopy/experiment/components/_base.py.orig +823 -0
- psychopy/experiment/components/form/.DS_Store +0 -0
- psychopy/experiment/components/microphone/__init__.py +10 -1
- psychopy/experiment/components/microphone/__init__.py.orig +490 -0
- psychopy/experiment/components/polygon/__init__.py +21 -22
- psychopy/experiment/components/settings/__init__.py +13 -14
- psychopy/experiment/components/settings/__init__.py.orig +1337 -0
- psychopy/experiment/components/textbox/__init__.py.orig +310 -0
- psychopy/experiment/components/webcam/.DS_Store +0 -0
- psychopy/experiment/components/webcam/light/.DS_Store +0 -0
- psychopy/experiment/flow.py +10 -8
- psychopy/experiment/loops.py.orig +829 -0
- psychopy/experiment/params.py +8 -3
- psychopy/experiment/params.py.orig +408 -0
- psychopy/experiment/routine.py.orig +503 -0
- psychopy/experiment/routines/_base.py +15 -6
- psychopy/experiment/routines/counterbalance/__init__.py +1 -0
- psychopy/gui/qtgui.py +14 -7
- psychopy/gui/util.py +10 -14
- psychopy/gui/wxgui.py +10 -4
- psychopy/hardware/.DS_Store +0 -0
- psychopy/hardware/brainproducts.py.orig +680 -0
- psychopy/hardware/iolab.py.orig +238 -0
- psychopy/hardware/manager.py +1 -1
- psychopy/hardware/photodiode.py +59 -27
- psychopy/hardware/serialport.py +51 -0
- psychopy/hardware/speaker.py +4 -4
- psychopy/iohub/datastore/__init__.py.orig +443 -0
- psychopy/iohub/datastore/util.py.orig +692 -0
- psychopy/iohub/devices/mouse/darwin.py.orig +427 -0
- psychopy/iohub/devices/mouse/linux2.py.orig +198 -0
- psychopy/preferences/.DS_Store +0 -0
- psychopy/projects/pavlovia.py +10 -3
- psychopy/projects/pavlovia.py.orig +1295 -0
- psychopy/sound/backend_ptb.py +22 -5
- psychopy/sound/transcribe.py +24 -4
- psychopy/tests/.DS_Store +0 -0
- psychopy/tests/data/.DS_Store +0 -0
- psychopy/tests/data/TestCircle_fill_local.png +0 -0
- psychopy/tests/data/__test.png +0 -0
- psychopy/tests/data/aperture1_normHexbackground_local.png +0 -0
- psychopy/tests/data/aperture1_norm_local.png +0 -0
- psychopy/tests/data/aperture2_normHexbackground_local.png +0 -0
- psychopy/tests/data/beatandrcos_height_local.png +0 -0
- psychopy/tests/data/beatandrcos_normAddBlend_local.png +0 -0
- psychopy/tests/data/beatandrcos_normHexbackground_local.png +0 -0
- psychopy/tests/data/beatandrcos_norm_local.png +0 -0
- psychopy/tests/data/beatandrcos_stencil_local.png +0 -0
- psychopy/tests/data/blend_add_height_local.png +0 -0
- psychopy/tests/data/blend_add_normAddBlend_local.png +0 -0
- psychopy/tests/data/blend_add_normHexbackground_local.png +0 -0
- psychopy/tests/data/blend_add_normNoShade_local.png +0 -0
- psychopy/tests/data/blend_add_norm_local.png +0 -0
- psychopy/tests/data/blend_add_stencil_local.png +0 -0
- psychopy/tests/data/bufferimg_gabor_height_local.png +0 -0
- psychopy/tests/data/bufferimg_gabor_normAddBlend_local.png +0 -0
- psychopy/tests/data/bufferimg_gabor_normHexbackground_local.png +0 -0
- psychopy/tests/data/bufferimg_gabor_normNoShade_local.png +0 -0
- psychopy/tests/data/bufferimg_gabor_norm_local.png +0 -0
- psychopy/tests/data/bufferimg_gabor_stencil_local.png +0 -0
- psychopy/tests/data/circleHex_height_local.png +0 -0
- psychopy/tests/data/circleHex_normAddBlend_local.png +0 -0
- psychopy/tests/data/circleHex_normHexbackground_local.png +0 -0
- psychopy/tests/data/circleHex_normNoShade_local.png +0 -0
- psychopy/tests/data/circleHex_norm_local.png +0 -0
- psychopy/tests/data/circleHex_stencil_local.png +0 -0
- psychopy/tests/data/color_comparison_local.png +0 -0
- psychopy/tests/data/corrFullRandom_local.csv +16 -0
- psychopy/tests/data/corrFullRandom_local.tsv +6 -0
- psychopy/tests/data/correctScript/.DS_Store +0 -0
- psychopy/tests/data/dots_height_local.png +0 -0
- psychopy/tests/data/dots_normAddBlend_local.png +0 -0
- psychopy/tests/data/dots_normHexbackground_local.png +0 -0
- psychopy/tests/data/dots_normNoShade_local.png +0 -0
- psychopy/tests/data/dots_norm_local.png +0 -0
- psychopy/tests/data/dots_stencil_local.png +0 -0
- psychopy/tests/data/elarray1_height_local.png +0 -0
- psychopy/tests/data/elarray1_normAddBlend_local.png +0 -0
- psychopy/tests/data/elarray1_normHexbackground_local.png +0 -0
- psychopy/tests/data/elarray1_norm_local.png +0 -0
- psychopy/tests/data/elarray1_stencil_local.png +0 -0
- psychopy/tests/data/envelopeandrcos_height_local.png +0 -0
- psychopy/tests/data/envelopeandrcos_normAddBlend_local.png +0 -0
- psychopy/tests/data/envelopeandrcos_normHexbackground_local.png +0 -0
- psychopy/tests/data/envelopeandrcos_norm_local.png +0 -0
- psychopy/tests/data/envelopeandrcos_stencil_local.png +0 -0
- psychopy/tests/data/envelopepowerandrcos_height_local.png +0 -0
- psychopy/tests/data/envelopepowerandrcos_normAddBlend_local.png +0 -0
- psychopy/tests/data/envelopepowerandrcos_normHexbackground_local.png +0 -0
- psychopy/tests/data/envelopepowerandrcos_norm_local.png +0 -0
- psychopy/tests/data/envelopepowerandrcos_stencil_local.png +0 -0
- psychopy/tests/data/gabor1_height_local.png +0 -0
- psychopy/tests/data/gabor1_normAddBlend_local.png +0 -0
- psychopy/tests/data/gabor1_normHexbackground_local.png +0 -0
- psychopy/tests/data/gabor1_normNoShade_local.png +0 -0
- psychopy/tests/data/gabor1_norm_local.png +0 -0
- psychopy/tests/data/gabor1_stencil_local.png +0 -0
- psychopy/tests/data/greyscale_normHexbackground_local.png +0 -0
- psychopy/tests/data/imageAndGauss_height_local.png +0 -0
- psychopy/tests/data/imageAndGauss_normAddBlend_local.png +0 -0
- psychopy/tests/data/imageAndGauss_normHexbackground_local.png +0 -0
- psychopy/tests/data/imageAndGauss_normNoShade_local.png +0 -0
- psychopy/tests/data/imageAndGauss_norm_local.png +0 -0
- psychopy/tests/data/imageAndGauss_stencil_local.png +0 -0
- psychopy/tests/data/movFrame1_stencil_local.png +0 -0
- psychopy/tests/data/noiseAndRcos_height_local.png +0 -0
- psychopy/tests/data/noiseAndRcos_normAddBlend_local.png +0 -0
- psychopy/tests/data/noiseAndRcos_normHexbackground_local.png +0 -0
- psychopy/tests/data/noiseAndRcos_normNoShade_local.png +0 -0
- psychopy/tests/data/noiseAndRcos_norm_local.png +0 -0
- psychopy/tests/data/noiseAndRcos_stencil_local.png +0 -0
- psychopy/tests/data/noiseFiltersAndRcos_height_local.png +0 -0
- psychopy/tests/data/noiseFiltersAndRcos_normAddBlend_local.png +0 -0
- psychopy/tests/data/noiseFiltersAndRcos_normHexbackground_local.png +0 -0
- psychopy/tests/data/noiseFiltersAndRcos_normNoShade_local.png +0 -0
- psychopy/tests/data/noiseFiltersAndRcos_norm_local.png +0 -0
- psychopy/tests/data/noiseFiltersAndRcos_stencil_local.png +0 -0
- psychopy/tests/data/numpyImage_height_local.png +0 -0
- psychopy/tests/data/numpyImage_normAddBlend_local.png +0 -0
- psychopy/tests/data/numpyImage_normHexbackground_local.png +0 -0
- psychopy/tests/data/numpyImage_normNoShade_local.png +0 -0
- psychopy/tests/data/numpyImage_norm_local.png +0 -0
- psychopy/tests/data/numpyImage_stencil_local.png +0 -0
- psychopy/tests/data/shape2_1_normAddBlend_local.png +0 -0
- psychopy/tests/data/shape2_1_normHexbackground_local.png +0 -0
- psychopy/tests/data/shape2_1_normNoShade_local.png +0 -0
- psychopy/tests/data/shape2_1_norm_local.png +0 -0
- psychopy/tests/data/shape2_1_stencil_local.png +0 -0
- psychopy/tests/data/testLoopsBlocks.psyexp_local.py +328 -0
- psychopy/tests/data/text1_height_local.png +0 -0
- psychopy/tests/data/text1_normAddBlend_local.png +0 -0
- psychopy/tests/data/text1_normHexbackground_local.png +0 -0
- psychopy/tests/data/text1_norm_local.png +0 -0
- psychopy/tests/data/text1_stencil_local.png +0 -0
- psychopy/tests/data/text2_height.png +0 -0
- psychopy/tests/data/text2_normAddBlend.png +0 -0
- psychopy/tests/data/text2_normHexbackground.png +0 -0
- psychopy/tests/data/text2_stencil.png +0 -0
- psychopy/tests/data/wedge1_height_local.png +0 -0
- psychopy/tests/data/wedge1_normAddBlend_local.png +0 -0
- psychopy/tests/data/wedge1_normHexbackground_local.png +0 -0
- psychopy/tests/data/wedge1_normNoShade_local.png +0 -0
- psychopy/tests/data/wedge1_norm_local.png +0 -0
- psychopy/tests/data/wedge1_stencil_local.png +0 -0
- psychopy/tests/test_app/.DS_Store +0 -0
- psychopy/tests/test_app/test_builder/.DS_Store +0 -0
- psychopy/tests/test_app/test_builder/data/_2021_ 5_03_1206.csv +9 -0
- psychopy/tests/test_app/test_builder/data/_2021_ 5_03_1206.log +177 -0
- psychopy/tests/test_app/test_builder/data/_2021_ 5_03_1206.psydat +0 -0
- psychopy/tests/test_app/test_builder/data/_2021_ 5_03_1206.xlsx +0 -0
- psychopy/tests/test_app/test_builder/data/_2021_ 5_03_1324.csv +9 -0
- psychopy/tests/test_app/test_builder/data/_2021_ 5_03_1324.log +168 -0
- psychopy/tests/test_app/test_builder/data/_2021_ 5_03_1324.psydat +0 -0
- psychopy/tests/test_app/test_builder/data/_2021_ 5_03_1324.xlsx +0 -0
- psychopy/tests/test_data/.DS_Store +0 -0
- psychopy/tests/test_hardware/test_CRS_BitsSharp.py.orig +68 -0
- psychopy/tests/test_tools/test_arraytools.py +112 -0
- psychopy/tests/test_visual/test_image.py.orig +219 -0
- psychopy/tools/arraytools.py +47 -0
- psychopy/tools/versionchooser.py +1 -1
- psychopy/visual/backends/pygletbackend.py +26 -8
- psychopy/visual/basevisual.py.orig +1723 -0
- psychopy/visual/form.py.orig +1181 -0
- psychopy/visual/text.py.orig +752 -0
- psychopy/visual/textbox2/textbox2.py.orig +1315 -0
- psychopy/visual/window.py +13 -5
- psychopy/visual/windowwarp.py.orig +463 -0
- {psychopy-2024.2.1.dist-info → psychopy-2024.2.4.dist-info}/METADATA +9 -9
- {psychopy-2024.2.1.dist-info → psychopy-2024.2.4.dist-info}/RECORD +244 -78
- {psychopy-2024.2.1.dist-info → psychopy-2024.2.4.dist-info}/WHEEL +1 -1
- {psychopy-2024.2.1.dist-info → psychopy-2024.2.4.dist-info}/entry_points.txt +2 -0
- psychopy/app/locale/ar_001/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/cs_CZ/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/da_DK/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/de_DE/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/el_GR/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/en_NZ/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/en_US/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/es_CO/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/es_ES/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/es_US/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/et_EE/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/fa_IR/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/fi_FI/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/fr_FR/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/he_IL/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/hi_IN/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/hu_HU/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/it_IT/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/ja_JP/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/ko_KR/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/ms_MY/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/nl_NL/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/nn_NO/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/pl_PL/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/pt_PT/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/ro_RO/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/ru_RU/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/sv_SE/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/tr_TR/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/zh_CN/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/zh_TW/LC_MESSAGE/messages.mo +0 -0
- psychopy-2024.2.1.dist-info/licenses/AUTHORS.md +0 -138
- /psychopy/{app/locale/ar_001/LC_MESSAGE → demos/builder}/.DS_Store +0 -0
- /psychopy/{app/locale/es_ES/LC_MESSAGE → demos/builder/Experiments}/.DS_Store +0 -0
- /psychopy/{visual → demos/builder/Tools/gammaCalibration/data}/.DS_Store +0 -0
- {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
|
+
])
|