#!/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
import sys
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, PointerMixin
)
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 psychopy.tools import gltools as gt
from .. import shaders
from ..rect import Rect
from ... import core, alerts, layout
from psychopy.tools.linebreak import get_breakable_points, break_units
import pyglet
USE_LEGACY_GL = pyglet.version < '2.0'
import pyglet.gl as gl
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, PointerMixin, DraggingMixin, ContainerMixin, ColorMixin):
    def __init__(self, win, text,
                 font="Noto 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,
                 clickable=True):
        """
        Parameters
        ----------
        win : Window
            The window this stimulus is associated with.
        text : str
            The text to display in the TextBox.
        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.clickable = clickable
        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 Noto 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 = "Noto 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 _drawLegacyGL(self):
        """Legacy draw routine for older GL versions.
        """
        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() 
[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
        # Border colour
        self.box.setLineColor(self.palette['lineColor'], colorSpace='rgb')
        # Background
        self.box.setFillColor(self.palette['fillColor'], colorSpace='rgb')
        # 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()
        if self.win.USE_LEGACY_GL:
            self._drawLegacyGL()
        else:
            self._selectWindow(self.win)
            self.win.setScale('pix')
            self.win.setOrthographicView()
            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)
            prog = self.shader.handle
            gt.useProgram(prog)
            gt.setUniformSampler2D(prog, b'uTexture', 0)
            gt.setUniformValue(prog, b'uColor', self._foreColor.render('rgba1'))
            gt.setUniformMatrix(
                prog, 
                b'uModelViewMatrix', 
                self.win._viewMatrix,
                transpose=True)
            gt.setUniformMatrix(
                prog, 
                b'uProjectionMatrix', 
                self.win._projectionMatrix,
                transpose=True)
            gt.drawClientArrays({
                'gl_Vertex': self.verticesPix,
                'gl_Color': self._colors,
                'gl_MultiTexCoord0': self._texcoords}, 
                'GL_QUADS')
        
            gt.useProgram(None)
            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()
        # 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) 
[docs]
    def getCharAtPos(self, pos):
        """Get the character index at a given position.
        
        This can be used to determine what character is under the specified 
        position in the stimulus.
        Parameters
        ----------
        pos : list, tuple
            Position in stimulus units.
        Returns
        -------
        int or None
            Index of character at the given position. Returns None if no 
            character is at the given position or if the position is outside
            the stimulus bounds.
            
        """
        px, py = pos[0] * 2.0, pos[1] * 2.0   # why x2?
        # read verticies in blocks of 4
        for i in range(0, len(self.vertices), 4):
            # get the four corners of the character
            charVertices = self.vertices[i:i + 4]
            
            x0, y0 = charVertices[0]  # top-left
            x1, y1 = charVertices[2]  # bottom-right
            # check if the mouse is within the bounds of the character
            if x0 <= px <= x1 and y0 >= py >= y1:
                toReturn = i // 4
                return toReturn
        return None 
[docs]
    def _onMouse(self):
        """Called by the window when the mouse is inside the stimulus.
        """
        if not self.editable:
            return
        # get button state
        buttons = self.mouse.getPressed()
        leftMbDn, middleMbDn, rightMbDn = buttons
        if leftMbDn:
            # get the character index at the given position
            charIdxAtPointer = self.getCharAtPos(self.mouse.getPos())
            if charIdxAtPointer:
                self.caret.index = charIdxAtPointer 
    @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 _drawLegacyGL(self):
        """Legacy drawing code for older GL versions.
        """
        # 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()
    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 USE_LEGACY_GL:
            self._drawLegacyGL()
            return
        # If no override and conditions are met, or override is True, draw
        prog = self.win._progSignedFrag
        gt.useProgram(prog)
        gt.setLineWidth(self.width)
        gt.setUniformValue(prog, 'uColor', self._foreColor.rgba1)
        gt.setUniformMatrix(
            prog, 
            b'uProjectionMatrix', 
            self.win.projectionMatrix,
            transpose=True)
        gt.setUniformMatrix(
            prog, 
            b'uModelViewMatrix', 
            self.win.viewMatrix,
            transpose=True)
        gt.drawClientArrays({
            'gl_Vertex': self.vertices}, 'lines')
        gt.useProgram(None)
    @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
        )