Source code for psychopy.visual.textbox2.textbox2

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# -----------------------------------------------------------------------------
#
#  FreeType high-level python API - Copyright 2011-2015 Nicolas P. Rougier
#  Distributed under the terms of the new BSD license.
#
# -----------------------------------------------------------------------------
r"""
TextBox2 provides a combination of features from TextStim and TextBox and then
some more added:

    - fast like TextBox (TextStim is pyglet-based and slow)
    - provides for fonts that aren't monospaced (unlike TextBox)
    - adds additional options to use <b>bold<\b>, <i>italic<\i>, <c=#ffffff>color</c> tags in text

"""
from ast import literal_eval

import numpy as np
from arabic_reshaper import ArabicReshaper
from pyglet import gl
from bidi import algorithm as bidi
import re

from ..aperture import Aperture
from ..basevisual import (
    BaseVisualStim, ColorMixin, ContainerMixin, WindowMixin, DraggingMixin
)
from psychopy.tools.attributetools import attributeSetter, setAttribute
from psychopy.tools import mathtools as mt
from psychopy.colors import Color
from psychopy.tools.fontmanager import FontManager, GLFont
from .. import shaders
from ..rect import Rect
from ... import core, alerts, layout

from psychopy.tools.linebreak import get_breakable_points, break_units

allFonts = FontManager()

# compile global shader programs later (when we're certain a GL context exists)
rgbShader = None
alphaShader = None
showWhiteSpace = False

codes = {'BOLD_START': u'\uE100',
         'BOLD_END': u'\uE101',
         'ITAL_START': u'\uE102',
         'ITAL_END': u'\uE103',
         'COLOR_START': u'\uE104',
         'COLOR_END': u'\uE105'}

# Compile regex pattern for color matching once
re_color_pattern = re.compile('<c=[^>]*>')
_colorCache = {}

wordBreaks = " -\n"  # what about ",."?


END_OF_THIS_LINE = 983349843

# Setting debug to True will make the sub-elements on TextBox2 to be outlined in red, making it easier to determine their position
debug = False

# If text is ". " we don't want to start next line with single space?


[docs]class TextBox2(BaseVisualStim, DraggingMixin, ContainerMixin, ColorMixin): def __init__(self, win, text, font="Open Sans", pos=(0, 0), units=None, letterHeight=None, ori=0, size=None, color=(1.0, 1.0, 1.0), colorSpace='rgb', fillColor=None, fillColorSpace=None, borderWidth=2, borderColor=None, borderColorSpace=None, contrast=1, opacity=None, bold=False, italic=False, placeholder="Type here...", lineSpacing=1.0, letterSpacing=None, padding=None, # gap between box and text speechPoint=None, anchor='center', alignment='left', flipHoriz=False, flipVert=False, languageStyle="LTR", editable=False, overflow="visible", lineBreaking='default', draggable=False, name='', autoLog=None, autoDraw=False, depth=0, onTextCallback=None): """ Parameters ---------- win text font pos units letterHeight size : Specifying None gets the default size for this type of unit. Specifying [None, None] gets a TextBox that's expandable in both dimensions. Specifying [0.75, None] gets a textbox that expands in the length but fixed at 0.75 units in the width color colorSpace contrast opacity bold italic lineSpacing padding speechPoint : list, tuple, np.ndarray or None Location of the end of a speech bubble tail on the textbox, in the same units as this textbox. If the point sits within the textbox, the tail will be inverted. Use `None` for no tail. anchor alignment fillColor borderWidth borderColor flipHoriz flipVert editable lineBreaking: Specifying 'default', text will be broken at a set of characters defined in the module. Specifying 'uax14', text will be broken in accordance with UAX#14 (Unicode Line Breaking Algorithm). draggable : bool Can this stimulus be dragged by a mouse click? name autoLog """ BaseVisualStim.__init__(self, win, units=units, name=name) self.depth = depth self.win = win self.colorSpace = colorSpace ColorMixin.foreColor.fset(self, color) # Have to call the superclass directly on init as text has not been set self.onTextCallback = onTextCallback self.draggable = draggable # Box around the whole textbox - drawn self.box = Rect( win, units=self.units, pos=(0, 0), size=(0, 0), # set later by self.size and self.pos colorSpace=colorSpace, lineColor=borderColor, fillColor=fillColor, lineWidth=borderWidth, opacity=self.opacity, autoLog=False, ) # Aperture & scrollbar self.container = None self.scrollbar = None # Box around just the content area, excluding padding - not drawn self.contentBox = Rect( win, units=self.units, pos=(0, 0), size=(0, 0), # set later by self.size and self.pos colorSpace=colorSpace, lineColor='red', fillColor=None, lineWidth=1, opacity=int(debug), autoLog=False ) # Box around current content, wrapped tight - not drawn self.boundingBox = Rect( win, units='pix', pos=(0, 0), size=(0, 0), # set later by self.size and self.pos colorSpace=colorSpace, lineColor='blue', fillColor=None, lineWidth=1, opacity=int(debug), autoLog=False ) # Sizing params self.letterHeight = letterHeight self.padding = padding self.size = size self.pos = pos # self._pixLetterHeight helps get font size right but not final layout if 'deg' in self.units: # treat deg, degFlat or degFlatPos the same scaleUnits = 'deg' # scale units are just for font resolution else: scaleUnits = self.units self._pixelScaling = self.letterHeightPix / self.letterHeight self.bold = bold self.italic = italic if lineSpacing is None: lineSpacing = 1.0 self.lineSpacing = lineSpacing self.glFont = None # will be set by the self.font attribute setter self.font = font self.letterSpacing = letterSpacing # If font not found, default to Open Sans Regular and raise alert if not self.glFont: alerts.alert(4325, self, { 'font': font, 'weight': 'bold' if self.bold is True else 'regular' if self.bold is False else self.bold, 'style': 'italic' if self.italic else '', 'name': self.name}) self.bold = False self.italic = False self.font = "Open Sans" # once font is set up we can set the shader (depends on rgb/a of font) if self.glFont.atlas.format == 'rgb': global rgbShader self.shader = rgbShader = shaders.Shader( shaders.vertSimple, shaders.fragTextBox2) else: global alphaShader self.shader = alphaShader = shaders.Shader( shaders.vertSimple, shaders.fragTextBox2alpha) self._needVertexUpdate = False # this will be set True during layout # standard stimulus params self.pos = pos self.ori = 0.0 # used at render time self._lines = None # np.array the line numbers for each char self._colors = None self._styles = None self.flipHoriz = flipHoriz self.flipVert = flipVert # params about positioning (after layout has occurred) self.anchor = anchor # 'center', 'top_left', 'bottom-center'... self.alignment = alignment # box border and fill self.borderWidth = borderWidth self.borderColor = borderColor self.fillColor = fillColor self.contrast = contrast self.opacity = opacity # set linebraking option if lineBreaking not in ('default', 'uax14'): raise ValueError("Unknown lineBreaking option ({}) is" "specified.".format(lineBreaking)) self._lineBreaking = lineBreaking # then layout the text (setting text triggers _layout()) self.languageStyle = languageStyle self._text = '' self.text = self.startText = text if text is not None else "" # now that we have text, set orientation self.ori = ori # Initialise arabic reshaper arabic_config = {'delete_harakat': False, # if present, retain any diacritics 'shift_harakat_position': False} # shift by 1 to be compatible with the bidi algorithm self.arabicReshaper = ArabicReshaper(configuration=arabic_config) # caret self.editable = editable self.overflow = overflow self.caret = Caret(self, color=self.color, width=2) # tail self.speechPoint = speechPoint # Placeholder text (don't create if this textbox IS the placeholder) if not isinstance(self, PlaceholderText): self._placeholder = PlaceholderText(self, placeholder) self.autoDraw = autoDraw self.autoLog = autoLog def __copy__(self): return TextBox2( self.win, self.text, self.font, pos=self.pos, units=self.units, letterHeight=self.letterHeight, size=self.size, color=self.color, colorSpace=self.colorSpace, fillColor=self.fillColor, borderWidth=self.borderWidth, borderColor=self.borderColor, contrast=self.contrast, opacity=self.opacity, bold=self.bold, italic=self.italic, lineSpacing=self.lineSpacing, padding=self.padding, # gap between box and text anchor=self.anchor, alignment=self.alignment, flipHoriz=self.flipHoriz, flipVert=self.flipVert, editable=self.editable, lineBreaking=self._lineBreaking, name=self.name, autoLog=self.autoLog, onTextCallback=self.onTextCallback ) @property def editable(self): """Determines whether or not the TextBox2 instance can receive typed text""" return self._editable @editable.setter def editable(self, editable): self._editable = editable if editable is False: if self.win: self.win.removeEditable(self) if editable is True: if self.win: self.win.addEditable(self) @property def palette(self): """Describes the current visual properties of the TextBox in a dict""" self._palette = { False: { 'lineColor': self._borderColor, 'lineWidth': self.borderWidth, 'fillColor': self._fillColor }, True: { 'lineColor': self._borderColor-0.1, 'lineWidth': self.borderWidth+1, 'fillColor': self._fillColor+0.1 } } return self._palette[self.hasFocus] @palette.setter def palette(self, value): self._palette = { False: value, True: value } @property def pallette(self): """ Disambiguation for palette. """ return self.palette @pallette.setter def pallette(self, value): self.palette = value @property def foreColor(self): return ColorMixin.foreColor.fget(self) @foreColor.setter def foreColor(self, value): ColorMixin.foreColor.fset(self, value) self._layout() if hasattr(self, "foreColor") and hasattr(self, 'caret'): self.caret.color = self._foreColor @attributeSetter def font(self, fontName): if isinstance(fontName, GLFont): self.glFont = fontName self.__dict__['font'] = fontName.name else: self.__dict__['font'] = fontName self.glFont = allFonts.getFont( fontName, size=self.letterHeightPix, bold=self.bold, italic=self.italic, lineSpacing=self.lineSpacing) @attributeSetter def overflow(self, value): if 'overflow' in self.__dict__ and value == self.__dict__['overflow']: return self.__dict__['overflow'] = value self.container = None self.scrollbar = None if value in ("hidden", "scroll"): # If needed, create Aperture self.container = Aperture( self.win, inverted=False, size=self.contentBox.size, pos=self.contentBox.pos, anchor=self.anchor, shape='square', units=self.units, autoLog=False ) self.container.disable() if value in ("scroll",): # If needed, create Slider from ..slider import Slider # Slider contains textboxes, so only import now self.scrollbar = Slider( self.win, ticks=(-1, 1), labels=None, startValue=1, pos=self.pos + (self.size[0] * 1.05 / 2, 0), size=self.size * (0.05, 1 / 1.2), units=self.units, style='scrollbar', granularity=0, labelColor=None, markerColor=self.color, lineColor=self.fillColor, colorSpace=self.colorSpace, opacity=self.opacity, autoLog=False ) @property def units(self): return WindowMixin.units.fget(self) @units.setter def units(self, value): if hasattr(self, "_placeholder"): self._placeholder.units = value WindowMixin.units.fset(self, value) if hasattr(self, "box"): self.box.units = value if hasattr(self, "contentBox"): self.contentBox.units = value if hasattr(self, "caret"): self.caret.units = value @property def size(self): """The (requested) size of the TextBox (w,h) in whatever units the stimulus is using This determines the outer extent of the area. If the width is set to None then the text will continue extending and not wrap. If the height is set to None then the text will continue to grow downwards as needed. """ return WindowMixin.size.fget(self) @size.setter def size(self, value): if hasattr(self, "_placeholder"): self._placeholder.size = value WindowMixin.size.fset(self, value) if hasattr(self, "box"): self.box.size = self._size if hasattr(self, "contentBox"): self.contentBox.size = self._size - self._padding * 2 # Refresh pos self.pos = self.pos @property def pos(self): """The position of the center of the TextBox in the stimulus :ref:`units <units>` `value` should be an :ref:`x,y-pair <attrib-xy>`. :ref:`Operations <attrib-operations>` are also supported. Example:: stim.pos = (0.5, 0) # Set slightly to the right of center stim.pos += (0.5, -1) # Increment pos rightwards and upwards. Is now (1.0, -1.0) stim.pos *= 0.2 # Move stim towards the center. Is now (0.2, -0.2) Tip: If you need the position of stim in pixels, you can obtain it like this: myTextbox._pos.pix """ return WindowMixin.pos.fget(self) @pos.setter def pos(self, value): WindowMixin.pos.fset(self, value) if hasattr(self, "box"): self.box.size = self._pos if hasattr(self, "contentBox"): # set content box pos with offset for anchor (accounting for orientation) self.contentBox.pos = self.pos + np.dot(self.size * self.box._vertices.anchorAdjust, self._rotationMatrix) self.contentBox._needVertexUpdate = True if hasattr(self, "_placeholder"): self._placeholder.pos = self._pos # Set caret pos again so it recalculates its vertices if hasattr(self, "caret"): self.caret.index = self.caret.index if hasattr(self, "_text"): self._layout() self._needVertexUpdate = True @property def vertices(self): return WindowMixin.vertices.fget(self) @vertices.setter def vertices(self, value): # If None, use defaut if value is None: value = [ [0.5, -0.5], [-0.5, -0.5], [-0.5, 0.5], [0.5, 0.5], ] # Create Vertices object self._vertices = layout.Vertices(value, obj=self.contentBox, flip=self.flip) @attributeSetter def speechPoint(self, value): self.__dict__['speechPoint'] = value # Match box size to own size self.box.size = self.size # No tail if value is None if value is None: self.box.vertices = [ [0.5, -0.5], [-0.5, -0.5], [-0.5, 0.5], [0.5, 0.5], ] return # Normalize point to vertex units _point = layout.Vertices( [[1, 1]], obj=self ) _point.setas([value], self.units) point = _point.base[0] # Square with snap points and tail point verts = [ # Top right -> Bottom right [0.5, 0.5], [0.5, 0.3], [0.5, 0.1], [0.5, -0.1], [0.5, -0.3], # Bottom right -> Bottom left [0.5, -0.5], [0.3, -0.5], [0.1, -0.5], [-0.1, -0.5], [-0.3, -0.5], # Bottom left -> Top left [-0.5, -0.5], [-0.5, -0.3], [-0.5, -0.1], [-0.5, 0.1], [-0.5, 0.3], # Top left -> Top right [-0.5, 0.5], [-0.3, 0.5], [-0.1, 0.5], [0.1, 0.5], [0.3, 0.5], # Tail point ] # Sort clockwise so tail point moves to correct place in vertices order verts = mt.sortClockwise(verts) verts.reverse() # Assign vertices self.box.vertices = verts
[docs] def setSpeechPoint(self, value, log=None): setAttribute(self, 'speechPoint', value, log)
@property def padding(self): if hasattr(self, "_padding"): return getattr(self._padding, self.units) @padding.setter def padding(self, value): # Substitute None for a default value if value is None: value = self.letterHeight / 2 # Create a Size object to handle padding self._padding = layout.Size(value, self.units, self.win) # Update size of bounding box if hasattr(self, "contentBox") and hasattr(self, "_size"): self.contentBox.size = self._size - self._padding * 2 @property def letterHeight(self): if hasattr(self, "_letterHeight"): return getattr(self._letterHeight, self.units)[1] @letterHeight.setter def letterHeight(self, value): # Cascade to placeholder if hasattr(self, "_placeholder"): self._placeholder.letterHeight = value if isinstance(value, layout.Vector): # If given a Vector, use it directly self._letterHeight = value elif isinstance(value, (int, float)): # If given an integer, convert it to a 2D Vector with width 0 self._letterHeight = layout.Size([0, value], units=self.units, win=self.win) elif value is None: # If None, use default (20px) self._letterHeight = layout.Size([0, 20], units='pix', win=self.win) elif isinstance(value, (list, tuple, np.ndarray)): # If given an array, convert it to a Vector self._letterHeight = layout.Size(value, units=self.units, win=self.win)
[docs] def setLetterHeight(self, value, log=None): setAttribute( self, "letterHeight", value=value, log=log )
@property def letterHeightPix(self): """ Convenience function to get self._letterHeight.pix and be guaranteed a return that is a single integer """ return self._letterHeight.pix[1] @attributeSetter def letterSpacing(self, value): """ Distance between letters, relative to the current font's default. Set as None or 1 to use font default unchanged. """ # Default is 1 if value is None: value = 1 # Set self.__dict__['letterSpacing'] = value # If text has been set, layout if hasattr(self, "_text"): self._layout() @property def fontMGR(self): return allFonts @fontMGR.setter def fontMGR(self, mgr): global allFonts if isinstance(mgr, FontManager): allFonts = mgr else: raise TypeError(f"Could not set font manager for TextBox2 object `{self.name}`, must be supplied with a FontManager object") @property def languageStyle(self): """ How is text laid out? Left to right (LTR), right to left (RTL) or using Arabic layout rules? """ if hasattr(self, "_languageStyle"): return self._languageStyle @languageStyle.setter def languageStyle(self, value): self._languageStyle = value if hasattr(self, "_placeholder"): self._placeholder.languageStyle = value # If layout is anything other than LTR, mark that we need to use bidi to lay it out self._needsBidi = value != "LTR" self._needsArabic = value.lower() == "arabic" @property def anchor(self): return self.box.anchor @anchor.setter def anchor(self, anchor): # Box should use this anchor self.box.anchor = anchor # Set pos again to update sub-element vertices self.pos = self.pos @property def alignment(self): if hasattr(self, "_alignX") and hasattr(self, "_alignY"): return (self._alignX, self._alignY) else: return ("top", "left") @alignment.setter def alignment(self, alignment): if hasattr(self, "_placeholder"): self._placeholder.alignment = alignment # look for unambiguous terms first (top, bottom, left, right) self._alignY = None self._alignX = None if 'top' in alignment: self._alignY = 'top' elif 'bottom' in alignment: self._alignY = 'bottom' if 'right' in alignment: self._alignX = 'right' elif 'left' in alignment: self._alignX = 'left' # then 'center' can apply to either axis that isn't already set if self._alignX is None: self._alignX = 'center' if self._alignY is None: self._alignY = 'center' self._needVertexUpdate = True if hasattr(self, "_text"): # If text has been set, layout self._layout() @property def text(self): return self._styles.formatted_text @text.setter def text(self, text): # Convert to string text = str(text) original_text = text # Substitute HTML tags text = text.replace('<i>', codes['ITAL_START']) text = text.replace('</i>', codes['ITAL_END']) text = text.replace('<b>', codes['BOLD_START']) text = text.replace('</b>', codes['BOLD_END']) text = text.replace('</c>', codes['COLOR_END']) # Handle starting color tag colorMatches = re.findall(re_color_pattern, text) # Only execute if color codes are found to save a regex call if len(colorMatches) > 0: text = re.sub(re_color_pattern, codes['COLOR_START'], text) # Interpret colors from tags color_values = [] for match in colorMatches: # Strip C tag matchKey = match.replace("<c=", "").replace(">", "") # Convert to arrays as needed try: matchVal = literal_eval(matchKey) except (ValueError, SyntaxError): # If eval fails, use value as is matchVal = matchKey # Retrieve/cache color if matchKey not in _colorCache: _colorCache[matchKey] = Color(matchVal, self.colorSpace) if not _colorCache[matchKey].valid: raise ValueError(f"Could not interpret color value for `{matchKey}` in textbox.") color_values.append(_colorCache[matchKey].render('rgba1')) visible_text = ''.join([c for c in text if c not in codes.values()]) self._styles = Style(len(visible_text)) self._styles.formatted_text = original_text self._text = visible_text if self._needsArabic and hasattr(self, "arabicReshaper"): self._text = self.arabicReshaper.reshape(self._text) if self._needsBidi: self._text = bidi.get_display(self._text) color_iter = 0 # iterator for color_values list current_color = [()] # keeps track of color style(s) is_bold = False is_italic = False ci = 0 for c in text: if c == codes['ITAL_START']: is_italic = True elif c == codes['BOLD_START']: is_bold = True elif c == codes['COLOR_START']: current_color.append(color_values[color_iter]) color_iter += 1 elif c == codes['ITAL_END']: is_italic = False elif c == codes['BOLD_END']: is_bold = False elif c == codes['COLOR_END']: current_color.pop() else: self._styles.c[ci] = current_color[-1] self._styles.i[ci] = is_italic self._styles.b[ci] = is_bold ci += 1 self._layout()
[docs] def addCharAtCaret(self, char): """Allows a character to be added programmatically at the current caret""" txt = self._text txt = txt[:self.caret.index] + char + txt[self.caret.index:] cstyle = Style(1) if len(self._styles) and self.caret.index <= len(self._styles): cstyle = self._styles[self.caret.index-1] self._styles.insert(self.caret.index, cstyle) self.caret.index += 1 self.text = txt self._layout()
[docs] def deleteCaretLeft(self): """Deletes 1 character to the left of the caret""" if self.caret.index > 0: txt = self._text ci = self.caret.index txt = txt[:ci-1] + txt[ci:] self._styles = self._styles[:ci-1]+self._styles[ci:] self.caret.index -= 1 self.text = txt self._layout()
[docs] def deleteCaretRight(self): """Deletes 1 character to the right of the caret""" ci = self.caret.index if ci < len(self._text): txt = self._text txt = txt[:ci] + txt[ci+1:] self._styles = self._styles[:ci]+self._styles[ci+1:] self.text = txt self._layout()
[docs] def _layout(self): """Layout the text, calculating the vertex locations """ rgb = self._foreColor.render('rgba1') font = self.glFont # the vertices are initially pix (natural for freetype) # then we convert them to the requested units for self._vertices # then they are converted back during rendering using standard BaseStim visible_text = self._text vertices = np.zeros((len(visible_text) * 4, 2), dtype=np.float32) self._charIndices = np.zeros((len(visible_text)), dtype=int) self._colors = np.zeros((len(visible_text) * 4, 4), dtype=np.double) self._texcoords = np.zeros((len(visible_text) * 4, 2), dtype=np.double) self._glIndices = np.zeros((len(visible_text) * 4), dtype=int) self._renderChars = [] # the following are used internally for layout self._lineNs = np.zeros(len(visible_text), dtype=int) _lineBottoms = [] self._lineLenChars = [] # _lineWidths = [] # width in stim units of each line lineMax = self.contentBox._size.pix[0] current = [0, 0 - font.ascender] fakeItalic = 0.0 fakeBold = 0.0 # for some reason glyphs too wide when using alpha channel only if font.atlas.format == 'alpha': alphaCorrection = 1 / 3.0 else: alphaCorrection = 1 if self._lineBreaking == 'default': wordLen = 0 charsThisLine = 0 wordsThisLine = 0 lineN = 0 for i, charcode in enumerate(self._text): printable = True # unless we decide otherwise # handle formatting codes fakeItalic = 0.0 fakeBold = 0.0 if self._styles.i[i]: fakeItalic = 0.1 * font.size if self._styles.b[i]: fakeBold = 0.3 * font.size # handle newline if charcode == '\n': printable = False # handle printable characters if printable: glyph = font[charcode] if showWhiteSpace and charcode == " ": glyph = font[u"·"] elif charcode == " ": # glyph size of space is smaller than actual size, so use size of dot instead glyph.size = font[u"·"].size # Get top and bottom coords yTop = current[1] + glyph.offset[1] yBot = yTop - glyph.size[1] # Get x mid point xMid = current[0] + glyph.offset[0] + glyph.size[0] * alphaCorrection / 2 + fakeBold / 2 # Get left and right corners from midpoint xBotL = xMid - glyph.size[0] * alphaCorrection / 2 - fakeItalic - fakeBold / 2 xBotR = xMid + glyph.size[0] * alphaCorrection / 2 - fakeItalic + fakeBold / 2 xTopL = xMid - glyph.size[0] * alphaCorrection / 2 - fakeBold / 2 xTopR = xMid + glyph.size[0] * alphaCorrection / 2 + fakeBold / 2 u0 = glyph.texcoords[0] v0 = glyph.texcoords[1] u1 = glyph.texcoords[2] v1 = glyph.texcoords[3] else: glyph = font[u"·"] x = current[0] + glyph.offset[0] yTop = current[1] + glyph.offset[1] yBot = yTop - glyph.size[1] xBotL = x xTopL = x xBotR = x xTopR = x u0 = glyph.texcoords[0] v0 = glyph.texcoords[1] u1 = glyph.texcoords[2] v1 = glyph.texcoords[3] theseVertices = [[xTopL, yTop], [xBotL, yBot], [xBotR, yBot], [xTopR, yTop]] texcoords = [[u0, v0], [u0, v1], [u1, v1], [u1, v0]] vertices[i * 4:i * 4 + 4] = theseVertices self._texcoords[i * 4:i * 4 + 4] = texcoords # handle character color rgb_ = self._styles.c[i] if len(rgb_) > 0: self._colors[i*4 : i*4+4, :4] = rgb_ # set custom color else: self._colors[i*4 : i*4+4, :4] = rgb # set default color self._lineNs[i] = lineN current[0] = current[0] + (glyph.advance[0] + fakeBold / 2) * self.letterSpacing current[1] = current[1] + glyph.advance[1] # are we wrapping the line? if charcode == "\n": # check if we have stored the top/bottom of the previous line yet if lineN + 1 > len(_lineBottoms): _lineBottoms.append(current[1]) lineWPix = current[0] current[0] = 0 current[1] -= font.height lineN += 1 charsThisLine += 1 self._lineLenChars.append(charsThisLine) _lineWidths.append(lineWPix) charsThisLine = 0 wordsThisLine = 0 elif charcode in wordBreaks: wordLen = 0 charsThisLine += 1 wordsThisLine += 1 elif printable: wordLen += 1 charsThisLine += 1 # end line with auto-wrap on space if current[0] >= lineMax and wordLen > 0: # move the current word to next line lineBreakPt = vertices[(i - wordLen + 1) * 4, 0] if wordsThisLine <= 1: # if whole line is just 1 word, wrap regardless of presence of wordbreak wordLen = 0 charsThisLine += 1 wordsThisLine += 1 # add hyphen self._renderChars.append({ "i": i, "current": (current[0], current[1]), "glyph": font["-"] }) # store linebreak point lineBreakPt = current[0] wordWidth = current[0] - lineBreakPt # shift all chars of the word left by wordStartX vertices[(i - wordLen + 1) * 4: (i + 1) * 4, 0] -= lineBreakPt vertices[(i - wordLen + 1) * 4: (i + 1) * 4, 1] -= font.height # update line values self._lineNs[i - wordLen + 1: i + 1] += 1 self._lineLenChars.append(charsThisLine - wordLen) _lineWidths.append(lineBreakPt) lineN += 1 # and set current to correct location current[0] = wordWidth current[1] -= font.height charsThisLine = wordLen wordsThisLine = 1 # have we stored the top/bottom of this line yet if lineN + 1 > len(_lineBottoms): _lineBottoms.append(current[1]) # add length of this (unfinished) line _lineWidths.append(current[0]) self._lineLenChars.append(charsThisLine) elif self._lineBreaking == 'uax14': # get a list of line-breakable points according to UAX#14 breakable_points = list(get_breakable_points(self._text)) text_seg = list(break_units(self._text, breakable_points)) styles_seg = list(break_units(self._styles, breakable_points)) lineN = 0 charwidth_list = [] segwidth_list = [] y_advance_list = [] vertices_list = [] texcoords_list = [] # calculate width of each segments for this_seg in range(len(text_seg)): thisSegWidth = 0 # width of this segment for i, charcode in enumerate(text_seg[this_seg]): printable = True # unless we decide otherwise # handle formatting codes fakeItalic = 0.0 fakeBold = 0.0 if self._styles.i[i]: fakeItalic = 0.1 * font.size if self._styles.b[i]: fakeBold = 0.3 * font.size # handle newline if charcode == '\n': printable = False # handle printable characters if printable: if showWhiteSpace and charcode == " ": glyph = font[u"·"] else: glyph = font[charcode] xBotL = glyph.offset[0] - fakeItalic - fakeBold / 2 xTopL = glyph.offset[0] - fakeBold / 2 yTop = glyph.offset[1] xBotR = xBotL + glyph.size[0] * alphaCorrection + fakeBold xTopR = xTopL + glyph.size[0] * alphaCorrection + fakeBold yBot = yTop - glyph.size[1] u0 = glyph.texcoords[0] v0 = glyph.texcoords[1] u1 = glyph.texcoords[2] v1 = glyph.texcoords[3] else: glyph = font[u"·"] x = glyph.offset[0] yTop = glyph.offset[1] yBot = yTop - glyph.size[1] xBotL = x xTopL = x xBotR = x xTopR = x u0 = glyph.texcoords[0] v0 = glyph.texcoords[1] u1 = glyph.texcoords[2] v1 = glyph.texcoords[3] # calculate width and update segment width w = glyph.advance[0] + fakeBold / 2 thisSegWidth += w # keep vertices, texcoords, width and y_advance of this character vertices_list.append([[xTopL, yTop], [xBotL, yBot], [xBotR, yBot], [xTopR, yTop]]) texcoords_list.append([[u0, v0], [u0, v1], [u1, v1], [u1, v0]]) charwidth_list.append(w) y_advance_list.append(glyph.advance[1]) # append width of this segment to the list segwidth_list.append(thisSegWidth) # concatenate segments to build line lines = [] while text_seg: line_width = 0 for i in range(len(text_seg)): # if this segment is \n, break line here. if text_seg[i][-1] == '\n': i+=1 # increment index to include \n to current line break # concatenate next segment line_width += segwidth_list[i] # break if line_width is greater than lineMax if lineMax < line_width: break else: # if for sentence finished without break, all segments # should be concatenated. i = len(text_seg) p = max(1, i) # concatenate segments and remove from segment list lines.append("".join(text_seg[:p])) del text_seg[:p], segwidth_list[:p] #, avoid[:p] # build lines i = 0 # index of the current character if lines: for line in lines: for c in line: theseVertices = vertices_list[i] #update vertices for j in range(4): theseVertices[j][0] += current[0] theseVertices[j][1] += current[1] texcoords = texcoords_list[i] vertices[i * 4:i * 4 + 4] = theseVertices self._texcoords[i * 4:i * 4 + 4] = texcoords # handle character color rgb_ = self._styles.c[i] if len(rgb_) > 0: self._colors[i*4 : i*4+4, :4] = rgb_ # set custom color else: self._colors[i*4 : i*4+4, :4] = rgb # set default color self._lineNs[i] = lineN current[0] = current[0] + charwidth_list[i] current[1] = current[1] + y_advance_list[i] # have we stored the top/bottom of this line yet if lineN + 1 > len(_lineBottoms): _lineBottoms.append(current[1]) # next chacactor i += 1 # prepare for next line current[0] = 0 current[1] -= font.height lineBreakPt = vertices[(i-1) * 4, 0] self._lineLenChars.append(len(line)) _lineWidths.append(lineBreakPt) # need not increase lineN when the last line doesn't end with '\n' if lineN < len(lines)-1 or line[-1] == '\n' : lineN += 1 else: raise ValueError("Unknown lineBreaking option ({}) is" "specified.".format(self._lineBreaking)) # Add render-only characters for rend in self._renderChars: vertices = self._addRenderOnlyChar( i=rend['i'], x=rend['current'][0], y=rend['current'][1], vertices=vertices, glyph=rend['glyph'], alphaCorrection=alphaCorrection ) # Apply vertical alignment if self.alignment[1] in ("bottom", "center"): # Get bottom of last line (or starting line, if there are none) if len(_lineBottoms): lastLine = min(_lineBottoms) else: lastLine = current[1] if self.alignment[1] == "bottom": # Work out how much we need to adjust by for the bottom base line to sit at the bottom of the content box adjustY = lastLine + self.contentBox._size.pix[1] if self.alignment[1] == "center": # 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 adjustY = (lastLine + font.descender + self.contentBox._size.pix[1]) / 2 # Adjust vertices and line bottoms vertices[:, 1] = vertices[:, 1] - adjustY _lineBottoms -= adjustY # Apply horizontal alignment if self.alignment[0] in ("right", "center"): if self.alignment[0] == "right": # Calculate adjust value per line lineAdjustX = self.contentBox._size.pix[0] - np.array(_lineWidths) if self.alignment[0] == "center": # Calculate adjust value per line lineAdjustX = (self.contentBox._size.pix[0] - np.array(_lineWidths)) / 2 # Get adjust value per vertex adjustX = lineAdjustX[np.repeat(self._lineNs, 4)] # Adjust vertices vertices[:, 0] = vertices[:, 0] + adjustX # convert the vertices to be relative to content box and set vertices = vertices / self.contentBox._size.pix + (-0.5, 0.5) # apply orientation self.vertices = (vertices * self.size).dot(self._rotationMatrix) / self.size if len(_lineBottoms): if self.flipVert: self._lineBottoms = min(self.contentBox._vertices.pix[:, 1]) - np.array(_lineBottoms) else: self._lineBottoms = max(self.contentBox._vertices.pix[:, 1]) + np.array(_lineBottoms) self._lineWidths = min(self.contentBox._vertices.pix[:, 0]) + np.array(_lineWidths) else: self._lineBottoms = np.array(_lineBottoms) self._lineWidths = np.array(_lineWidths) # if we had to add more glyphs to make possible then if self.glFont._dirty: self.glFont.upload() self.glFont._dirty = False self._needVertexUpdate = True
@attributeSetter def ori(self, value): # get previous orientaiton lastOri = self.__dict__.get("ori", 0) # set new value BaseVisualStim.ori.func(self, value) # set on all boxes self.box.ori = value self.boundingBox.ori = value self.contentBox.ori = value # trigger layout if value has changed if lastOri != value: self._layout()
[docs] def draw(self): """Draw the text to the back buffer""" # Border width self.box.setLineWidth(self.palette['lineWidth']) # Use 1 as base if border width is none #self.borderWidth = self.box.lineWidth # Border colour self.box.setLineColor(self.palette['lineColor'], colorSpace='rgb') #self.borderColor = self.box.lineColor # Background self.box.setFillColor(self.palette['fillColor'], colorSpace='rgb') #self.fillColor = self.box.fillColor # Inherit win self.box.win = self.win self.contentBox.win = self.win self.boundingBox.win = self.win if self._needVertexUpdate: #print("Updating vertices...") self._updateVertices() if self.fillColor is not None or self.borderColor is not None: self.box.draw() # Draw sub-elements if in debug mode if debug: self.contentBox.draw() self.boundingBox.draw() tightH = self.boundingBox._size.pix[1] areaH = self.contentBox._size.pix[1] if self.overflow in ("scroll",) and tightH > areaH: # Draw scrollbar self.scrollbar.draw() # Scroll if self._alignY == "top": # Top aligned means scroll between 1 and 0, and no adjust for line height adjMulti = (-self.scrollbar.markerPos + 1) / 2 adjAdd = -self.glFont.descender elif self._alignY == "bottom": # Top aligned means scroll between -1 and 0, and adjust for line height adjMulti = (-self.scrollbar.markerPos - 1) / 2 adjAdd = -self.glFont.descender else: # Center aligned means scroll between -0.5 and 0.5, and 50% adjust for line height adjMulti = -self.scrollbar.markerPos / 2 adjAdd = 0 self.contentBox._pos.pix = self._pos.pix + ( 0, (tightH - areaH) * adjMulti + adjAdd ) self._needVertexUpdate = True if self.overflow in ("hidden", "scroll"): # Activate aperture self.container.enable() gl.glPushMatrix() self.win.setScale('pix') gl.glActiveTexture(gl.GL_TEXTURE0) gl.glBindTexture(gl.GL_TEXTURE_2D, self.glFont.textureID) gl.glEnable(gl.GL_TEXTURE_2D) gl.glDisable(gl.GL_DEPTH_TEST) gl.glEnableClientState(gl.GL_VERTEX_ARRAY) gl.glEnableClientState(gl.GL_COLOR_ARRAY) gl.glEnableClientState(gl.GL_TEXTURE_COORD_ARRAY) gl.glEnableClientState(gl.GL_VERTEX_ARRAY) gl.glVertexPointer(2, gl.GL_DOUBLE, 0, self.verticesPix.ctypes) gl.glColorPointer(4, gl.GL_DOUBLE, 0, self._colors.ctypes) gl.glTexCoordPointer(2, gl.GL_DOUBLE, 0, self._texcoords.ctypes) self.shader.bind() self.shader.setInt('texture', 0) self.shader.setFloat('pixel', [1.0 / 512, 1.0 / 512]) nVerts = (len(self._text) + len(self._renderChars)) * 4 gl.glDrawArrays(gl.GL_QUADS, 0, nVerts) self.shader.unbind() # removed the colors and font texture gl.glDisableClientState(gl.GL_COLOR_ARRAY) gl.glDisableClientState(gl.GL_TEXTURE_COORD_ARRAY) gl.glDisableVertexAttribArray(1) gl.glDisableClientState(gl.GL_VERTEX_ARRAY) gl.glActiveTexture(gl.GL_TEXTURE0) gl.glBindTexture(gl.GL_TEXTURE_2D, 0) gl.glDisable(gl.GL_TEXTURE_2D) if self.hasFocus: # draw caret line self.caret.draw() gl.glPopMatrix() # Draw placeholder if blank if self.editable and len(self.text) == 0: self._placeholder.draw() if self.container is not None: self.container.disable()
[docs] def reset(self): """Resets the TextBox2 to hold **whatever it was given on initialisation**""" # Reset contents self.text = self.startText
[docs] def clear(self): """Resets the TextBox2 to a blank string""" # Clear contents self.text = ""
[docs] def contains(self, x, y=None, units=None, tight=False): """Returns True if a point x,y is inside the stimulus' border. Can accept variety of input options: + two separate args, x and y + one arg (list, tuple or array) containing two vals (x,y) + an object with a getPos() method that returns x,y, such as a :class:`~psychopy.event.Mouse`. Returns `True` if the point is within the area defined either by its `border` attribute (if one defined), or its `vertices` attribute if there is no .border. This method handles complex shapes, including concavities and self-crossings. Note that, if your stimulus uses a mask (such as a Gaussian) then this is not accounted for by the `contains` method; the extent of the stimulus is determined purely by the size, position (pos), and orientation (ori) settings (and by the vertices for shape stimuli). See Coder demos: shapeContains.py See Coder demos: shapeContains.py """ if tight: return self.boundingBox.contains(x, y, units) else: return self.box.contains(x, y, units)
[docs] def overlaps(self, polygon, tight=False): """Returns `True` if this stimulus intersects another one. If `polygon` is another stimulus instance, then the vertices and location of that stimulus will be used as the polygon. Overlap detection is typically very good, but it can fail with very pointy shapes in a crossed-swords configuration. Note that, if your stimulus uses a mask (such as a Gaussian blob) then this is not accounted for by the `overlaps` method; the extent of the stimulus is determined purely by the size, pos, and orientation settings (and by the vertices for shape stimuli). Parameters See coder demo, shapeContains.py """ if tight: return self.boundingBox.overlaps(polygon) else: return self.box.overlaps(polygon)
[docs] def _addRenderOnlyChar(self, i, x, y, vertices, glyph, alphaCorrection=1): """ Add a character at index i which is drawn but not actually part of the text """ i4 = i * 4 # Get coordinates of glyph texture self._texcoords = np.vstack([ self._texcoords[:i4], [glyph.texcoords[0], glyph.texcoords[1]], [glyph.texcoords[0], glyph.texcoords[3]], [glyph.texcoords[2], glyph.texcoords[3]], [glyph.texcoords[2], glyph.texcoords[1]], self._texcoords[i4:] ]) # Get coords of box corners top = y + glyph.offset[1] bot = top - glyph.size[1] mid = x + glyph.offset[0] + glyph.size[0] * alphaCorrection / 2 left = mid - glyph.size[0] * alphaCorrection / 2 right = mid + glyph.size[0] * alphaCorrection / 2 vertices = np.vstack([ vertices[:i4], [left, top], [left, bot], [right, bot], [right, top], vertices[i4:] ]) # Make same colour as other text self._colors = np.vstack([ self._colors[:i4], self._foreColor.render('rgba1'), self._foreColor.render('rgba1'), self._foreColor.render('rgba1'), self._foreColor.render('rgba1'), self._colors[i4:] ]) # Extend line numbers array self._lineNs = np.hstack([ self._lineNs[:i], self._lineNs[i-1], self._lineNs[i:] ]) return vertices
[docs] def _updateVertices(self): """Sets Stim.verticesPix and ._borderPix from pos, size, ori, flipVert, flipHoriz """ # check whether stimulus needs flipping in either direction flip = np.array([1, 1]) if hasattr(self, 'flipHoriz') and self.flipHoriz: flip[0] = -1 # True=(-1), False->(+1) if hasattr(self, 'flipVert') and self.flipVert: flip[1] = -1 # True=(-1), False->(+1) self.__dict__['verticesPix'] = self._vertices.pix # tight bounding box if hasattr(self._vertices, self.units) and self.vertices.shape[0] >= 1: verts = self._vertices.pix L = verts[:, 0].min() R = verts[:, 0].max() B = verts[:, 1].min() T = verts[:, 1].max() tightW = R-L Xmid = (R+L)/2 tightH = T-B Ymid = (T+B)/2 # for the tight box anchor offset is included in vertex calcs self.boundingBox.size = tightW, tightH self.boundingBox.pos = self.pos + (Xmid, Ymid) else: self.boundingBox.size = 0, 0 self.boundingBox.pos = self.pos # box (larger than bounding box) needs anchor offest adding self.box.pos = self.pos self.box.size = self.size # this might have changed from _requested self._needVertexUpdate = False
[docs] def _onText(self, chr): """Called by the window when characters are received""" if chr == '\t': self.win.nextEditable() return if chr == '\r': # make it newline not Carriage Return chr = '\n' self.addCharAtCaret(chr) if self.onTextCallback: self.onTextCallback()
[docs] def _onCursorKeys(self, key): """Called by the window when cursor/del/backspace... are received""" if key == 'MOTION_UP': self.caret.row -= 1 elif key == 'MOTION_DOWN': self.caret.row += 1 elif key == 'MOTION_RIGHT': self.caret.char += 1 elif key == 'MOTION_LEFT': self.caret.char -= 1 elif key == 'MOTION_BACKSPACE': self.deleteCaretLeft() elif key == 'MOTION_DELETE': self.deleteCaretRight() elif key == 'MOTION_NEXT_WORD': pass elif key == 'MOTION_PREVIOUS_WORD': pass elif key == 'MOTION_BEGINNING_OF_LINE': self.caret.char = 0 elif key == 'MOTION_END_OF_LINE': self.caret.char = END_OF_THIS_LINE elif key == 'MOTION_NEXT_PAGE': pass elif key == 'MOTION_PREVIOUS_PAGE': pass elif key == 'MOTION_BEGINNING_OF_FILE': pass elif key == 'MOTION_END_OF_FILE': pass else: print("Received unhandled cursor motion type: ", key)
@property def hasFocus(self): if self.win and self.win.currentEditable == self: return True return False @hasFocus.setter def hasFocus(self, focus): if focus is False and self.hasFocus: # If focus is being set to False, tell window to # give focus to next editable. if self.win: self.win.nextEditable() elif focus is True and self.hasFocus is False: # If focus is being set True, set textbox instance to be # window.currentEditable. if self.win: self.win.currentEditable=self return False
[docs] def getText(self): """Returns the current text in the box, including formatting tokens.""" return self.text
@property def visibleText(self): """Returns the current visible text in the box""" return self._text
[docs] def getVisibleText(self): """Returns the current visible text in the box""" return self.visibleText
[docs] def setText(self, text=None, log=None): """Usually you can use 'stim.attribute = value' syntax instead, but use this method if you need to suppress the log message. """ setAttribute(self, 'text', text, log)
[docs] def setHeight(self, height, log=None): """Usually you can use 'stim.attribute = value' syntax instead, but use this method if you need to suppress the log message. """ setAttribute(self, 'height', height, log)
[docs] def setFont(self, font, log=None): """Usually you can use 'stim.attribute = value' syntax instead, but use this method if you need to suppress the log message. """ setAttribute(self, 'font', font, log)
@attributeSetter def placeholder(self, value): """ Text to display when textbox is editable and has no content. """ # Store value self.__dict__['placeholder'] = value # Set placeholder object text if hasattr(self, "_placeholder"): self._placeholder.text = value
[docs] def setPlaceholder(self, value, log=False): """ Set text to display when textbox is editable and has no content. """ self.placeholder = value
class Caret(ColorMixin): """ Class to handle the caret (cursor) within a textbox. Do **not** call without a textbox. Parameters ---------- textbox : psychopy.visual.TextBox2 Textbox which caret corresponds to visible : bool Whether the caret is visible row : int Textbox row which caret is on char : int Text character within row which caret is on index : int Index of character which caret is on vertices : list, tuple Coordinates of each corner of caret width : int, float Width of caret line color : list, tuple, str Caret colour """ def __init__(self, textbox, color, width, colorSpace='rgb'): self.textbox = textbox self.index = len(textbox._text) # start off at the end self.autoLog = False self.width = width self.units = textbox.units self.colorSpace = colorSpace self.color = color def draw(self, override=None): """ Draw the caret Parameters ========== override : bool or None Set to True to always draw the caret, to False to never draw the caret, or leave as None to draw only according to the usual conditions (being visible and within the correct timeframe for the flashing effect) """ if override is None: # If no override, draw only if conditions are met if not self.visible: return # Flash every other second if core.getTime() % 1 > 0.6: return elif not override: # If override is False, never draw return # If no override and conditions are met, or override is True, draw gl.glLineWidth(self.width) gl.glColor4f( *self._foreColor.rgba1 ) gl.glBegin(gl.GL_LINES) gl.glVertex2f(self.vertices[0, 0], self.vertices[0, 1]) gl.glVertex2f(self.vertices[1, 0], self.vertices[1, 1]) gl.glEnd() @property def visible(self): return self.textbox.hasFocus @property def row(self): """What row is caret on?""" # Check that index is with range of all character indices if len(self.textbox._lineNs) == 0: # no chars at all return 0 elif self.index > len(self.textbox._lineNs): self.index = len(self.textbox._lineNs) # Get line of index if self.index >= len(self.textbox._lineNs): if len(self.textbox._lineBottoms) - 1 > self.textbox._lineNs[-1]: return len(self.textbox._lineBottoms) - 1 return self.textbox._lineNs[-1] else: return self.textbox._lineNs[self.index] @row.setter def row(self, value): """Use line to index conversion to set index according to row value""" # Figure out how many characters into previous row the cursor was charsIn = self.char nRows = len(self.textbox._lineLenChars) # If new row is more than total number of rows, move to end of last row if value >= nRows: value = nRows charsIn = self.textbox._lineLenChars[-1] # If new row is less than 0, move to beginning of first row elif value < 0: value = 0 charsIn = 0 elif value == nRows-1 and charsIn > self.textbox._lineLenChars[value]: # last row last char charsIn = self.textbox._lineLenChars[value] elif charsIn > self.textbox._lineLenChars[value]-1: # end of a middle row (account for the newline) charsIn = self.textbox._lineLenChars[value]-1 # Set new index in new row self.index = sum(self.textbox._lineLenChars[:value]) + charsIn @property def char(self): """What character within current line is caret on?""" # Check that index is with range of all character indices self.index = min(self.index, len(self.textbox._lineNs)) self.index = max(self.index, 0) # Get first index of line, subtract from index to get char return self.index - sum(self.textbox._lineLenChars[:self.row]) @char.setter def char(self, value): """Set character within row""" # If setting char to less than 0, move to last char on previous line row = self.row if value < 0: if row == 0: value = 0 else: row -= 1 value = self.textbox._lineLenChars[row]-1 # end of that row elif row >= len(self.textbox._lineLenChars)-1 and \ value >= self.textbox._lineLenChars[-1]: # this is the last row row = len(self.textbox._lineLenChars)-1 value = self.textbox._lineLenChars[-1] elif value == END_OF_THIS_LINE: value = self.textbox._lineLenChars[row]-1 elif value >= self.textbox._lineLenChars[row]: # end of this row (not the last) so go to next row += 1 value = 0 # then calculate index if row: # if not on first row self.index = sum(self.textbox._lineLenChars[:row]) + value else: self.index = value @property def vertices(self): textbox = self.textbox # check we have a caret index if self.index is None or self.index > len(textbox._text): self.index = len(textbox._text) if self.index < 0: self.index = 0 # Get vertices of caret based on characters and index ii = self.index if textbox.vertices.shape[0] == 0: # If there are no chars, put caret at start position (determined by alignment) if textbox.alignment[1] == "bottom": bottom = min(textbox.contentBox._vertices.pix[:, 1]) elif textbox.alignment[1] == "center": bottom = (min(textbox.contentBox._vertices.pix[:, 1]) + max(textbox.contentBox._vertices.pix[:, 1]) - textbox.glFont.ascender - textbox.glFont.descender) / 2 else: bottom = max(textbox.contentBox._vertices.pix[:, 1]) - textbox.glFont.ascender if textbox.alignment[0] == "right": x = max(textbox.contentBox._vertices.pix[:, 0]) elif textbox.alignment[0] == "center": x = (min(textbox.contentBox._vertices.pix[:, 0]) + max(textbox.contentBox._vertices.pix[:, 0])) / 2 else: x = min(textbox.contentBox._vertices.pix[:, 0]) else: # Otherwise, get caret position from character vertices if self.index >= len(textbox._lineNs): if len(textbox._lineBottoms) - 1 > textbox._lineNs[-1]: x = textbox._lineWidths[len(textbox._lineBottoms) - 1] else: # If the caret is after the last char, position it to the right chrVerts = textbox._vertices.pix[range((ii-1) * 4, (ii-1) * 4 + 4)] x = chrVerts[2, 0] # x-coord of left edge (of final char) else: # Otherwise, position it to the left chrVerts = textbox._vertices.pix[range(ii * 4, ii * 4 + 4)] x = chrVerts[1, 0] # x-coord of right edge # Get top of this line bottom = textbox._lineBottoms[self.row] # Top will always be line bottom + font height if self.textbox.flipVert: top = bottom - self.textbox.glFont.size else: top = bottom + self.textbox.glFont.size return np.array([ [x, bottom], [x, top] ]) class Style: # Define a simple Style class for storing information in text(). # Additional features exist to maintain extant edit/caret syntax def __init__(self, text_length, i=None, b=None, c=None): self.len = text_length self.i = i self.b = b self.c = c if i == None: self.i = [False]*text_length if b == None: self.b = [False]*text_length if c == None: self.c = [()]*text_length self.formatted_text = '' def __len__(self): return self.len def __getitem__(self, i): # Return a new Style object with data from current index if isinstance(i, int): s = Style(1, [self.i[i]], [self.b[i]], [self.c[i]]) else: s = Style(len(self.i[i]), self.i[i], self.b[i], self.c[i]) return s def __add__(self, c): s = self.copy() s.insert(len(s), c) return s def copy(self): s = Style(self.len, self.i.copy(), self.b.copy(), self.c.copy()) s.formatted_text = self.formatted_text return s def insert(self, i, style): # in-place, like list if not isinstance(style, Style): raise TypeError('Inserted object must be Style.') self.i[i:i] = style.i self.b[i:i] = style.b self.c[i:i] = style.c self.len += len(style) class PlaceholderText(TextBox2): """ Subclass of TextBox2 used only for presenting placeholder text, should never be called outside of TextBox2's init method. """ def __init__(self, parent, text): # Should only ever be called from a textbox, make sure parent is a textbox assert isinstance(parent, TextBox2), "Parent of PlaceholderText object must be of type visual.TextBox2" # Create textbox sdfs df TextBox2.__init__( self, parent.win, text, font=parent.font, bold=parent.bold, italic=parent.italic, units=parent.contentBox.units, anchor=parent.contentBox.anchor, pos=parent.contentBox.pos, size=parent.contentBox.size, letterHeight=parent.letterHeight, color=parent.color, colorSpace=parent.colorSpace, fillColor=None, borderColor=None, opacity=0.5, lineSpacing=parent.lineSpacing, padding=0, # gap between box and text alignment=parent.alignment, flipHoriz=parent.flipHoriz, flipVert=parent.flipVert, languageStyle=parent.languageStyle, editable=False, overflow=parent.overflow, lineBreaking=parent._lineBreaking, autoLog=False, autoDraw=False )

Back to top