#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Created on Thu Mar 21 18:38:35 2013
@author: Sol
"""
import os
import inspect
import numbers
from weakref import proxy
import time
import numpy as np
from psychopy import core, misc, colors, logging
import psychopy.tools.colorspacetools as colortools
import psychopy.tools.arraytools as arraytools
import pyglet
pyglet.options['debug_gl'] = False
from pyglet.gl import (glCallList, glFinish, glGenLists, glNewList, glViewport,
glMatrixMode, glLoadIdentity, glDisable, glEnable, glColorMaterial,
glBlendFunc, glTranslatef, glColor4f, glRectf, glLineWidth, glBegin,
GL_LINES, glVertex2d, glEndList, glClearColor, gluOrtho2D, glOrtho,
glDeleteLists, GL_COMPILE, GL_PROJECTION, GL_MODELVIEW, glEnd,
GL_DEPTH_TEST, GL_BLEND, GL_COLOR_MATERIAL, GL_FRONT_AND_BACK,
GL_AMBIENT_AND_DIFFUSE, GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA,
glIsEnabled, GL_LINE_SMOOTH, GLint, GLfloat, glGetIntegerv,
GL_LINE_WIDTH, glGetFloatv, GL_ALIASED_LINE_WIDTH_RANGE,
GL_SMOOTH_LINE_WIDTH_RANGE, GL_SMOOTH_LINE_WIDTH_GRANULARITY,
GL_POLYGON_SMOOTH)
from .fontmanager import FontManager
from .textgrid import TextGrid
def getTime():
return core.getTime()
def is_sequence(arg):
return ( # not hasattr(arg, "strip") and
hasattr(arg, "__getitem__") or
hasattr(arg, "__iter__"))
_system_font_manager = None
def getFontManager(mono_only=True):
global _system_font_manager
if _system_font_manager is None:
_system_font_manager = FontManager(mono_only)
return _system_font_manager
# check that font manager is working
fm = getFontManager()
font_names = fm.getFontFamilyNames()
if len(font_names) == 0:
logging.warn("TextBox Font Manager Found No Fonts.")
def getGLInfo():
gl_info = dict()
gl_info['GL_LINE_SMOOTH'] = glIsEnabled(GL_LINE_SMOOTH)
lwidth = GLint()
glGetIntegerv(GL_LINE_WIDTH, lwidth)
gl_info['GL_LINE_WIDTH'] = lwidth
awrange = (GLfloat * 2)(0.0, 0.0)
glGetFloatv(GL_ALIASED_LINE_WIDTH_RANGE, awrange)
gl_info['GL_ALIASED_LINE_WIDTH_RANGE'] = awrange[0], awrange[1]
swrange = (GLfloat * 2)(0.0, 0.0)
glGetFloatv(GL_SMOOTH_LINE_WIDTH_RANGE, swrange)
gl_info['GL_SMOOTH_LINE_WIDTH_RANGE'] = swrange[0], swrange[1]
swg = GLfloat()
glGetFloatv(GL_SMOOTH_LINE_WIDTH_GRANULARITY, swg)
gl_info['GL_SMOOTH_LINE_WIDTH_GRANULARITY'] = swg
return gl_info
[docs]class TextBox:
"""
Similar to the visual.TextStim component, TextBox can be used to display
text within a psychopy window. TextBox and TextStim each have different
strengths and weaknesses. You should select the most appropriate text
component type based on how it will be used within the experiment.
NOTE: As of PsychoPy 1.79, TextBox should be considered experimental.
The two TextBox demo scripts provided have been tested on
all PsychoPy supported OS's and run without exceptions. However there are
very likely bugs in the existing TextBox code and the TextBox API will
be further enhanced and improved (i.e. changed) over the next couple months.
**TextBox Features**
* Text character placement is very well defined, useful when the exact
positioning of each letter needs to be known.
* The text string that is displayed can be changed ( setText() ) and
drawn ( win.draw() ) **very** quickly. See the TextBox vs. TextStim
comparison table for details.
* Built-in font manager; providing easy access to the font family names
and styles that are available on the computer being used.
* TextBox is a composite stimulus type, with the following graphical
elements, many of which can be changed to control many aspects of how
the TextBox is displayed.:
- TextBox Border / Outline
- TextBox Fill Area
- Text Grid Cell Lines
- Text Glyphs
* When using 'rgb' or 'rgb255' color spaces, colors can be specified as
a list/tuple of 3 elements (red, green, blue), or with four elements
(reg, green, blue, alpha) which allows different elements of the
TextBox to use different opacity settings if desired. For colors that
include the alpha channel value, it will be applied instead of the
opacity setting of the TextBox, effectively overriding the stimulus
defined opacity for that part of the textbox graphics. Colors that
do not include an alpha channel use the opacity setting as normal.
* Text Line Spacing can be controlled.
**Textbox Limitations**
* Only Monospace Fonts are supported.
* TextBox component is not a completely **standard** psychopy visual
stim and has the following functional difference:
- TextBox attributes are never accessed directly; get* and set*
methods are always used (this will be changed to use class
properties in the future).
- Setting an attribute of a TextBox only supports value replacement,
( textbox.setFontColor([1.0,1.0,1.0]) ) and does not support
specifying operators.
* Some key word arguments supported by other stimulus types in general,
or by TextStim itself, are not supported by TextBox. See the TextBox
class definition for the arguments that are supported.
* When a new font, style, and size are used it takes about 1 second to
load and process the font. This is a one time delay for a given
font name, style, and size. After first being loaded,
the same font style can be used or re-applied to multiple TextBox
components with no significant delay.
* Auto logging or auto drawing is not currently supported.
TextStim and TextBox Comparison:
============================ ============= ===========
Feature TextBox TextStim
============================ ============= ===========
Change text + redraw time^ 1.513 msec 28.537 msec
No change + redraw time^ 0.240 msec 0.931 msec
Initial Creation time^ 0.927 msec 0.194 msec
MonoSpace Font Support Yes Yes
Non MonoSpace Font Support No Yes
Adjustable Line Spacing Yes No
Precise Text Pos. Info Yes No
Auto logging Support No Yes
Rotation Support No Yes
Word Wrapping Support Yes Yes
============================ ============= ===========
^ Times are in msec.usec format. Tested using the textstim_vs_textbox.py
demo script provided with the PsychoPy distribution. Results are
dependent on text length, video card, and OS. Displayed results are
based on 120 character string with an average of 24 words. Test computer
used Windows 7 64 bit, PsychoPy 1.79, with a i7 3.4 Ghz CPU, 8 GB RAM,
and NVIDIA 480 GTX 2GB graphics card.
Example::
from psychopy import visual
win=visual.Window(...)
# A Textbox stim that will look similar to a TextStim component
textstimlike=visual.TextBox(
window=win,
text="This textbox looks most like a textstim.",
font_size=18,
font_color=[-1,-1,1],
color_space='rgb',
size=(1.8,.1),
pos=(0.0,.5),
units='norm')
# A Textbox stim that uses more of the supported graphical features
#
textboxloaded=visual.TextBox(
window=win
text='TextBox showing all supported graphical elements',
font_size=32,
font_color=[1,1,1],
border_color=[-1,-1,1], # draw a blue border around stim
border_stroke_width=4, # border width of 4 pix.
background_color=[-1,-1,-1], # fill the stim background
grid_color=[1,-1,-1,0.5], # draw a red line around each
# possible letter area,
# 50% transparent
grid_stroke_width=1, # with a width of 1 pix
textgrid_shape=[20,2], # specify area of text box
# by the number of cols x
# number of rows of text to support
# instead of by a screen
# units width x height.
pos=(0.0,-.5),
# If the text string length < num rows * num cols in
# textgrid_shape, how should text be justified?
#
grid_horz_justification='center',
grid_vert_justification='center')
textstimlike.draw()
textboxloaded.draw()
win.flip()
"""
_textbox_instances = {}
_gl_info = None
def __init__(self,
window=None, # PsychoPy Window instance
text='Default Test Text.', # Initial text to be displayed.
font_name=None, # Family name of Font
bold=False, # Bold and italics are used to
italic=False, # determine style of font
font_size=32, # Pt size to use for font.
font_color=(0, 0, 0, 1), # Color to draw the text with.
dpi=72, # DPI used to create font bitmaps
line_spacing=0, # Amount of extra spacing to add between
line_spacing_units='pix', # lines of text.
background_color=None, # Color to use to fill the entire area
# on the screen TextBox is using.
border_color=None, # TextBox border color to use.
border_stroke_width=1, # Stroke width of TextBox boarder (in pix)
size=None, # (width,height) desired for the TextBox
# stim to use. Specify using the unit
# type the textBox is using.
textgrid_shape=None, # (cols,rows) of characters to use when
# creating the textgrid layout.
# rows*cols = maximum number of chars
# that can be displayed. If textgrid_shape
# is not None, then the TextBox size
# must be at least large enough to hold
# the number of specified cols and rows.
# If the size specified is less than
# what is needed, the size will be increased
# automatically.
pos=(0.0, 0.0), # (x,y) screen position for the TextBox
# stim. Specify using the unit
# type the textBox is using.
align_horz='center', # Determines how TextBox x pos is
# should be interpreted to.
# 'left', 'center', 'right' are valid options.
align_vert='center', # Determines how TextBox y pos is
# should be interpreted to.
# 'left', 'center', 'right' are valid options.
units='norm', # Coordinate unit type to use for position
# and size related attributes. Valid
# options are 'pix', 'cm', 'deg', 'norm'
# Only pix is currently working though.
grid_color=None, # Color to draw the TextBox text grid
# lines with.
grid_stroke_width=1, # Line thickness (in pix) to use when
# displaying text grid lines.
color_space='rgb', # PsychoPy color space to use for any
# color attributes of TextBox.
opacity=1.0, # Opacity (transparency) to use for
# TextBox graphics, assuming alpha
# channel was not specified in the color
# attribute.
grid_horz_justification='left', # 'left', 'center', 'right'
grid_vert_justification='top', # 'top', 'bottom', 'center'
autoLog=True, # Log each time stim is updated.
interpolate=False,
name=None
):
self._window = proxy(window)
self._font_name = font_name
if self.getWindow().useRetina:
self._font_size = font_size*2
else:
self._font_size = font_size
self._dpi = dpi
self._bold = bold
self._italic = italic
self._text = text
self._label = name
self._line_spacing = line_spacing
self._line_spacing_units = line_spacing_units
self._border_color = border_color
self._background_color = background_color
self._border_stroke_width = border_stroke_width
self._grid_horz_justification = grid_horz_justification
self._grid_vert_justification = grid_vert_justification
self._align_horz = align_horz
self._align_vert = align_vert
self._size = size
self._position = pos
self._textgrid_shape = textgrid_shape
self._interpolate = interpolate
self._draw_start_dlist = None
self._draw_end_dlist = None
self._draw_te_background_dlist = None
if TextBox._gl_info is None:
TextBox._gl_info = getGLInfo()
aliased_wrange = TextBox._gl_info['GL_ALIASED_LINE_WIDTH_RANGE']
antia_wrange = TextBox._gl_info['GL_SMOOTH_LINE_WIDTH_RANGE']
if grid_stroke_width and grid_color:
if interpolate:
if grid_stroke_width < antia_wrange[0]:
self._grid_stroke_width = antia_wrange[0]
if grid_stroke_width > antia_wrange[1]:
self._grid_stroke_width = antia_wrange[1]
else:
if grid_stroke_width < aliased_wrange[0]:
self._grid_stroke_width = aliased_wrange[0]
if grid_stroke_width > aliased_wrange[1]:
self._grid_stroke_width = aliased_wrange[1]
if border_stroke_width and border_color:
if interpolate:
if border_stroke_width < antia_wrange[0]:
self._border_stroke_width = antia_wrange[0]
if border_stroke_width > antia_wrange[1]:
self._border_stroke_width = antia_wrange[1]
else:
if border_stroke_width < aliased_wrange[0]:
self._border_stroke_width = aliased_wrange[0]
if border_stroke_width > aliased_wrange[1]:
self._border_stroke_width = aliased_wrange[1]
self._units = units
if self._units is None:
self._units = self._window.units
self._opacity = opacity
if opacity is None:
self._opacity = 1.0
elif float(opacity) and float(opacity) >= 0 and float(opacity) <= 1.0:
self._opacity = float(opacity)
else:
raise ValueError(
"Text Box: opacity must be a number between 0.0 and 1.0, or None (which == 1.0). %s is not valid" % (str(opacity)))
self._color_space = color_space
if self._color_space is None:
self._color_space = self._window.colorSpace
# TODO: Implement support for autoLog
self._auto_log = autoLog
self._current_glfont = None
self._text_grid = None
if self._label is None:
self._label = 'TextBox_%s' % (str(int(time.time())))
fm = getFontManager()
if self._font_name is None:
self._font_name = fm.getFontFamilyStyles()[0][0]
gl_font = fm.getGLFont(
self._font_name, self._font_size, self._bold, self._italic, self._dpi)
self._current_glfont = gl_font
if size is None and textgrid_shape is None:
print('WARNING (TextBox) - No `size` or `textgrid_shape` given. Defaulting to displaying all text in a single row.')
textgrid_shape = (len(text), 1)
self._text_grid = TextGrid(self, line_color=grid_color,
line_width=grid_stroke_width, font_color=list(
font_color),
shape=textgrid_shape,
grid_horz_justification=grid_horz_justification,
grid_vert_justification=grid_vert_justification)
self._text_grid.setCurrentFontDisplayLists(
gl_font.charcode2displaylist)
self._text = self._text.replace('\r\n', '\n')
self._text_grid._createParsedTextDocument(self._text)
self._textbox_instances[self.getLabel()] = proxy(self)
[docs] def getWindow(self):
"""
Returns the psychopy window that the textBox is associated with.
"""
return self._window
[docs] def getText(self):
"""
Return the text to display.
"""
return self._text
[docs] def setText(self, text_source):
"""
Set the text to be displayed within the Textbox.
Note that once a TextBox has been created, the number of character
rows and columns is static. To change the size of a TextBox,
a new TextBox stim must be created to replace the current Textbox stim.
Therefore ensure that the textbox is large enough to display
the largest length string to be presented in the TextBox. Characters
that do not fit within the TextBox will not be displayed.
Color value must be valid for the color space being used by the TextBox.
"""
if not self._text:
raise ValueError(
"TextBox.setText only accepts strings with a length > 0")
self._text = text_source.replace('\r\n', '\n')
return self._text_grid._setText(self._text)
[docs] def getDisplayedText(self):
"""
Return the text that fits within the TextBox and therefore is actually
seen. This is equal to::
text_length=len(self.getText())
cols,rows=self.getTextGridShape()
displayed_text=self.getText()[0:min(text_length,rows*cols]
"""
return self._getTextWrappedDoc().getDisplayedText()
[docs] def getTextGridCellPlacement(self):
"""Returns a 3D numpy array containing position information for each
text grid cell in the TextBox. The array has the shape (`num_cols`,
`num_rows`, `cell_bounds`), where num_cols is the number of `textgrid`
columns in the TextBox. `num_rows` is the number of `textgrid` rows in
the `TextBox`. `cell_bounds` is a 4 element array containing the (x pos,
y pos, width, height) data for the given cell. Position fields are for
the top left hand corner of the cell box. Column and Row indices start
at 0.
To get the shape of the textgrid in terms of columns and rows, use::
cell_pos_array=textbox.getTextGridCellPlacement()
col_row_count=cell_pos_array.shape[:2]
To access the position, width, and height for textgrid cell at
column 0 and row 0 (so the top left cell in the textgrid)::
cell00=cell_pos_array[0,0,:]
For the cell at col 3, row 1 (so 4th cell on second row)::
cell41=cell_pos_array[4,1,:]
"""
col_lines = self._text_grid._col_lines
row_lines = self._text_grid._row_lines
cellinfo = np.zeros(
(len(col_lines[:-1]), len(row_lines[:-1]), 4), dtype=np.float32)
tb_tl = self._getTopLeftPixPos()
tg_tl = self._text_grid._position
starting_x, starting_y = tb_tl[0] + tg_tl[0], tb_tl[1] - tg_tl[1]
for i, x in enumerate(col_lines[:-1]):
for j, y in enumerate(row_lines[:-1]):
px, py = starting_x + x, starting_y + y
w, h = col_lines[i + 1] - px, -(row_lines[j + 1] - py)
cellinfo[i, j, 0] = px
cellinfo[i, j, 1] = py
cellinfo[i, j, 2] = w
cellinfo[i, j, 3] = h
for i, x in enumerate(col_lines[:-1]):
for j, y in enumerate(row_lines[:-1]):
x, y = self._pix2units(
(float(cellinfo[i, j, 0]), float(cellinfo[i, j, 1])))
w, h = self._pix2units(
(float(cellinfo[i, j, 2]), float(cellinfo[i, j, 3])), False)
cellinfo[i, j, 0] = x
cellinfo[i, j, 1] = y
cellinfo[i, j, 2] = w
cellinfo[i, j, 3] = h
return cellinfo
[docs] def getTextGridCellForCharIndex(self, char_index):
return self._getTextWrappedDoc().getTextGridCellForCharIndex(char_index)
[docs] def getGlyphPositionForTextIndex(self, char_index):
"""For the provided char_index, which is the index of one character in
the current text being displayed by the TextBox ( getDisplayedText() ),
return the bounding box position, width, and height for the associated
glyph drawn to the screen. This factors in the glyphs position within
the textgrid cell it is being drawn in, so the returned bounding box is
for the actual glyph itself, not the textgrid cell. For textgrid cell
placement information, see the getTextGridCellPlacement() method.
The glyph position for the given text index is returned as a tuple
(x,y,width,height), where x,y is the top left hand corner of the
bounding box.
Special Cases:
* If the index provided is out of bounds for the currently displayed
text, None is returned.
* For u' ' (space) characters, the full textgrid cell bounding box
is returned.
* For u'\n' ( new line ) characters,the textgrid cell bounding box
is returned, but with the box width set to 0.
"""
if char_index < 0 or char_index >= len(self.getDisplayedText()):
raise IndexError(
"The provided index of %d is out of range for the currently displayed text." % (char_index))
# Get the Glyph info for the char in question:
gl_font = getFontManager().getGLFont(self._font_name, self._font_size,
self._bold, self._italic, self._dpi)
glyph_data = gl_font.charcode2glyph.get(ord(self._text[char_index]))
ox, oy = glyph_data['offset'][
0], gl_font.max_ascender - glyph_data['offset'][1]
gw, gh = glyph_data['size']
# get the col,row for the xhar index provided
col_row = self._getTextWrappedDoc().getTextGridCellForCharIndex(char_index)
if col_row is None:
return None
cline = self._getTextWrappedDoc().getParsedLine(col_row[1])
c = col_row[0] + cline._trans_left
r = col_row[1] + cline._trans_top
x, y, width, height = self.getTextGridCellPlacement()[c, r, :]
ox, oy = self._pix2units((ox, oy), False)
gw, gh = self._pix2units((gw, gh), False)
return x + ox, y - oy, gw, gh
def _getTextWrappedDoc(self):
return self._text_grid._text_document
[docs] def getPosition(self):
"""Return the x,y position of the textbox, in getUnitType() coord space.
"""
return self._position
[docs] def setPosition(self, pos):
"""
Set the (x,y) position of the TextBox on the Monitor. The position must
be given using the unit coord type used by the stim.
The TextBox position is interpreted differently depending on the
Horizontal and Vertical Alignment settings of the stim. See
getHorzAlignment() and getVertAlignment() for more information.
For example,
if the TextBox alignment is specified as left, top, then the position
specifies the top left hand corner of where the stim will be drawn.
An alignment of bottom,right indicates that the position value will
define where the bottom right corner of the TextBox will be drawn.
A horz., vert. alignment of center, center will place the center of
the TextBox at pos.
"""
if pos[0] != self._position[0] or pos[1] != self._position[1]:
self._position = pos[0], pos[1]
self._deleteBackgroundDL()
self._deleteStartDL()
[docs] def getUnitType(self):
"""
Returns which of the psychopy coordinate systems are used by the
TextBox. Position and size related attributes mush be specified
relative to the unit type being used. Valid options are:
* pix
* norm
* cm
"""
return self._units
[docs] def getHorzAlign(self):
"""
Return what textbox x position should be interpreted as. Valid options
are 'left', 'center', or 'right' .
"""
return self._align_horz
[docs] def setHorzAlign(self, v):
"""
Specify how the horizontal (x) component of the TextBox position
is to be interpreted. left = x position is the left edge, right =
x position is the right edge x position, and center = the x position
is used to center the stim horizontally.
"""
if v != self._align_horz:
self._align_horz = v
self._deleteBackgroundDL()
self._deleteStartDL()
[docs] def getVertAlign(self):
"""
Return what textbox y position should be interpreted as. Valid options
are 'top', 'center', or 'bottom' .
"""
return self._align_vert
[docs] def setVertAlign(self, v):
"""
Specify how the vertical (y) component of the TextBox position
is to be interpreted. top = y position is the top edge, bottom =
y position is the bottom edge y position, and center = the y position
is used to center the stim vertically.
"""
if v != self._align_vert:
self._align_vert = v
self._deleteBackgroundDL()
self._deleteStartDL()
[docs] def getSize(self):
"""
Return the width,height of the TextBox, using the unit type being
used by the stimulus.
"""
return self._size
[docs] def getFontSize(self):
if self.getWindow().useRetina:
return self._font_size//2
return self._font_size
[docs] def getFontColor(self):
"""
Return the color used when drawing text glyphs.
"""
return self._text_grid._font_color
[docs] def setFontColor(self, c):
"""
Set the color to use when drawing text glyphs within the TextBox.
Color value must be valid for the color space being used by the TextBox.
For 'rgb', 'rgb255', and 'norm' based colors, three or four element lists
are valid. Three element colors use the TextBox getOpacity() value to
determine the alpha channel for the color. Four element colors use the
value of the fourth element to set the alpha value for the color.
"""
if c != self._text_grid._font_color:
self._text_grid._font_color = c
self._text_grid._deleteTextDL()
[docs] def getBorderColor(self):
"""
A border can be drawn around the perimeter of the TextBox. This method
sets the color of that border.
"""
return self._border_color
[docs] def setBorderColor(self, c):
"""
Set the color to use for the border of the textBox. The TextBox border
is a rectangular outline drawn around the edges of the TextBox stim.
Color value must be valid for the color space being used by the TextBox.
A value of None will disable drawing of the border.
"""
if c != self._border_color:
self._border_color = c
self._deleteBackgroundDL()
[docs] def getBackgroundColor(self):
"""
Get the color used to fill the rectangular area of the TextBox stim.
All other graphical elements of the TextBox are drawn on top of the
background.
"""
return self._background_color
[docs] def setBackgroundColor(self, c):
"""
Set the fill color used to fill the rectangular area of the TextBox stim.
Color value must be valid for the color space being used by the TextBox.
A value of None will disable drawing of the TextBox background.
"""
if c != self._background_color:
self._background_color = c
self._deleteBackgroundDL()
[docs] def getTextGridLineColor(self):
"""
Return the color used when drawing the outline of the text grid cells.
Each letter displayed in a TextBox populates one of the text cells
defined by the shape of the TextBox text grid.
Color value must be valid for the color space being used by the TextBox.
A value of None indicates drawing of the textgrid lines is disabled.
"""
return self._text_grid._line_color
[docs] def setTextGridLineColor(self, c):
"""
Set the color used when drawing text grid lines.
These are lines that can be drawn which mark the bounding box for
each character within the TextBox text grid.
Color value must be valid for the color space being used by the TextBox.
Provide a value of None to disable drawing of textgrid lines.
"""
if c != self._text_grid._line_color:
self._text_grid._line_color = c
self._text_grid._deleteGridLinesDL()
[docs] def getColorSpace(self):
"""Returns the psychopy color space used when specifying colors for
the TextBox. Supported values are:
* 'rgb'
* 'rbg255'
* 'norm'
* hex (implicit)
* html name (implicit)
See the Color Space section of the PsychoPy docs for details.
"""
return self._color_space
[docs] def getHorzJust(self):
"""Return how text should laid out horizontally when the number of
columns of each text grid row is greater than the number needed to
display the text for that text row.
"""
return self._text_grid._horz_justification
[docs] def setHorzJust(self, v):
"""
Specify how text within the TextBox should be aligned horizontally.
For example, if a text grid has 10 columns, and the text being displayed
is 6 characters in length, the horizontal justification determines
if the text should be draw starting at the left of the text columns (left),
or should be centered on the columns ('center', in this example
there would be two empty text cells to the left and right of the text.),
or should be drawn such that the last letter of text is drawn in the
last column of the text row ('right').
"""
if v != self._text_grid._horz_justification:
self._text_grid.setHorzJust(v)
self._text_grid._deleteTextDL()
[docs] def getVertJust(self):
"""
Return how text should laid out vertically when the
number of text grid rows is greater than the number
needed to display the current text
"""
return self._text_grid._vert_justification
[docs] def setVertJust(self, v):
"""
Specify how text within the TextBox should be aligned vertically.
For example, if a text grid has 3 rows for text, and the text being
displayed all fits on one row, the vertical justification determines
if the text should be draw on the top row of the text grid (top),
or should be centered on the rows ('center', in this example
there would be one row above and below the row used to draw the text),
or should be drawn on the last row of the text grid, ('bottom').
"""
if v != self._text_grid._vert_justification:
self._text_grid.setVertJust(v)
self._text_grid._deleteTextDL()
[docs] def getBorderWidth(self):
"""
Get the stroke width of the optional TextBox area outline. This is always
given in pixel units.
"""
return self._border_stroke_width
[docs] def setBorderWidth(self, c):
"""
Set the stroke width (in pixels) to use for the border of the TextBox
stim. Border values must be within the range of stroke widths supported
by the OpenGL driver used by the graphics. Setting the
width outside the valid range will result in the stroke width being
clamped to the nearest end of the valid range.
Use the TextBox.getValidStrokeWidths() to access the minimum -
maximum range of valid line widths.
"""
if c != self._border_stroke_width:
if self._interpolate:
lrange = TextBox._gl_info['GL_SMOOTH_LINE_WIDTH_RANGE']
else:
lrange = TextBox._gl_info['GL_ALIASED_LINE_WIDTH_RANGE']
if c < lrange[0]:
c = lrange[0]
elif c > lrange[1]:
c = lrange[1]
self._border_stroke_width = c
self._deleteBackgroundDL()
[docs] def getTextGridLineWidth(self):
"""
Return the stroke width (in pixels) of the optional lines drawn around
the text grid cell areas.
"""
return self._text_grid._line_width
[docs] def setTextGridLineWidth(self, c):
"""
Set the stroke width (in pixels) to use for the text grid character
bounding boxes. Border values must be within the range of stroke
widths supported by the OpenGL driver used by the computer graphics
card. Setting the width outside the valid range will result in the
stroke width being clamped to the nearest end of the valid range.
Use the TextBox.getGLineRanges() to access a dict containing some
OpenGL parameters which provide the minimum, maximum, and resolution
of valid line widths.
"""
if c != self._text_grid._line_width:
if self._interpolate:
lrange = TextBox._gl_info['GL_SMOOTH_LINE_WIDTH_RANGE']
else:
lrange = TextBox._gl_info['GL_ALIASED_LINE_WIDTH_RANGE']
if c < lrange[0]:
c = lrange[0]
elif c > lrange[1]:
c = lrange[1]
self._text_grid._line_width = c
self._text_grid._deleteGridLinesDL()
[docs] def getValidStrokeWidths(self):
"""
Returns the stroke width range supported by the graphics card being
used. If the TextBox is Interpolated, a tuple is returns using
float values, with the following structure:
((min_line_width, max_line_width), line_width_granularity)
If Interpolation is disabled for the TextBox, the returned tuple elements
are int values, with the following structure:
(min_line_width, max_line_width)
"""
if self._interpolate:
return (TextBox._gl_info['GL_SMOOTH_LINE_WIDTH_RANGE'],
TextBox._gl_info['GL_SMOOTH_LINE_WIDTH_GRANULARITY'])
else:
return self._gl_info['GL_ALIASED_LINE_WIDTH_RANGE']
[docs] def getOpacity(self):
"""
Get the default TextBox transparency level used for color related
attributes. 0.0 equals fully transparent, 1.0 equals fully opaque.
"""
return self._opacity
[docs] def setOpacity(self, o):
"""
Sets the TextBox transparency level to use for color related
attributes of the Textbox. 0.0 equals fully transparent, 1.0 equals
fully opaque.
If opacity is set to None, it is assumed to have a default value of 1.0.
When a color is defined with a 4th element in the colors element list,
then this opacity value is ignored and the alpha value provided in the
color itself is used for that TextGrid element instead.
"""
if o != self._opacity and o >= 0.0 and o <= 1.0:
self._text_grid._deleteTextDL()
self._deleteBackgroundDL()
self._text_grid._deleteGridLinesDL()
self._deleteStartDL()
self._deleteEndDL()
self._opacity = o
[docs] def getInterpolated(self):
"""
Returns whether interpolation is enabled for the TextBox
when it is drawn. When True, GL_LINE_SMOOTH and GL_POLYGON_SMOOTH
are enabled within OpenGL; otherwise they are disabled.
"""
return self._interpolate
[docs] def setInterpolated(self, interpolate):
"""
Specify whether interpolation should be enabled for the TextBox
when it is drawn. When interpolate == True, GL_LINE_SMOOTH and
GL_POLYGON_SMOOTH are enabled within OpenGL. When interpolate is set
to False, GL_POLYGON_SMOOTH and GL_LINE_SMOOTH are disabled.
"""
if interpolate != self._interpolate:
self._deleteStartDL()
self._interpolate = interpolate
[docs] def getLabel(self):
"""
Return the label / name assigned to the textbox. This does not impact
how the stimulus looks when drawn, and instead is used for internal
purposes only.
"""
return self._label
[docs] def getName(self):
"""
Same as the GetLabel method.
"""
return self._label
[docs] def getAutoLog(self):
"""
Indicates if changes to textBox attribute values should be logged
automatically by PsychoPy. *Currently not supported by TextBox.*
"""
return self._auto_log
[docs] def setAutoLog(self, v):
"""
Specify if changes to textBox attribute values should be logged
automatically by PsychoPy. True enables auto logging; False disables it.
*Currently not supported by TextBox.*
"""
self._auto_log = v
[docs] def getLineSpacing(self):
"""
Return the additional spacing being applied between rows of text.
The value is in units specified by the textbox getUnits() method.
"""
return self._line_spacing
[docs] def draw(self):
"""
Draws the TextBox to the back buffer of the graphics card. Then call
win.flip() to display the changes drawn. If draw() is not called prior
to a call to win.flip(), the textBox will not be displayed for that
retrace.
"""
self._te_start_gl()
self._te_bakground_dlist()
self._text_grid._text_glyphs_gl()
self._text_grid._textgrid_lines_gl()
self._te_end_gl()
def _te_start_gl(self):
if not self._draw_start_dlist:
dl_index = glGenLists(1)
glNewList(dl_index, GL_COMPILE)
glViewport(0, 0, self._window.size[0],
self._window.size[1])
glMatrixMode(GL_PROJECTION)
glLoadIdentity()
glOrtho(0, self._window.size[0], 0,
self._window.size[1], -1, 1)
glMatrixMode(GL_MODELVIEW)
glLoadIdentity()
self._window.depthTest = False
glEnable(GL_BLEND)
glEnable(GL_COLOR_MATERIAL)
glColorMaterial(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE)
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
if self._interpolate:
glEnable(GL_LINE_SMOOTH)
glEnable(GL_POLYGON_SMOOTH)
else:
glDisable(GL_LINE_SMOOTH)
glDisable(GL_POLYGON_SMOOTH)
t, l = self._getTopLeftPixPos()
glTranslatef(t, l, 0)
glEndList()
self._draw_start_dlist = dl_index
glCallList(self._draw_start_dlist)
def _deleteStartDL(self):
if self._draw_start_dlist:
glDeleteLists(self._draw_start_dlist, 1)
self._draw_start_dlist = None
def _te_bakground_dlist(self):
if not self._draw_te_background_dlist and (self._background_color or self._border_color):
dl_index = glGenLists(1)
glNewList(dl_index, GL_COMPILE)
# draw textbox_background and outline
border_thickness = self._border_stroke_width
size = self._getPixelSize()
if self._border_stroke_width is None:
border_thickness = 0
if self._background_color:
bcolor = self._toRGBA(self._background_color)
glColor4f(*bcolor)
glRectf(0, 0, size[0], -size[1])
if self._border_color:
glLineWidth(border_thickness)
bcolor = self._toRGBA(self._border_color)
glColor4f(*bcolor)
glBegin(GL_LINES)
x1 = 0
y1 = 0
x2, y2 = size
x2, y2 = x2, y2
hbthick = border_thickness // 2
if hbthick < 1:
hbthick = 1
glVertex2d(x1 - border_thickness, y1 + hbthick)
glVertex2d(x2 + border_thickness, y1 + hbthick)
glVertex2d(x2 + hbthick, y1)
glVertex2d(x2 + hbthick, -y2)
glVertex2d(x2 + border_thickness, -y2 - hbthick)
glVertex2d(x1 - border_thickness, -y2 - hbthick)
glVertex2d(x1 - hbthick, -y2)
glVertex2d(x1 - hbthick, y1)
glEnd()
glColor4f(0.0, 0.0, 0.0, 1.0)
glEndList()
self._draw_te_background_dlist = dl_index
if (self._background_color or self._border_color):
glCallList(self._draw_te_background_dlist)
def _deleteBackgroundDL(self):
if self._draw_te_background_dlist:
glDeleteLists(self._draw_te_background_dlist, 1)
self._draw_te_background_dlist = None
def _te_end_gl(self):
if not self._draw_end_dlist:
dl_index = glGenLists(1)
glNewList(dl_index, GL_COMPILE)
rgb = self._window.rgb
rgb = TextBox._toRGBA2(
rgb, 1, self._window.colorSpace, self._window)
glClearColor(rgb[0], rgb[1], rgb[2], 1.0)
glViewport(0, 0, int(self._window.size[0]), int(
self._window.size[1]))
glMatrixMode(GL_PROJECTION) # Reset The Projection Matrix
glLoadIdentity()
gluOrtho2D(-1, 1, -1, 1)
glMatrixMode(GL_MODELVIEW) # Reset The Projection Matrix
glLoadIdentity()
glEndList()
self._draw_end_dlist = dl_index
glCallList(self._draw_end_dlist)
def _deleteEndDL(self):
if self._draw_end_dlist:
glDeleteLists(self._draw_end_dlist, 1)
self._draw_end_dlist = None
@staticmethod
def _toPix(xy, units, window):
if isinstance(xy, numbers.Number):
xy = xy, xy
elif is_sequence(xy):
if len(xy) == 1:
xy = xy[0], xy[0]
else:
xy = xy[:2]
else:
return ValueError("TextBox: coord variables must be array-like or a single number. Invalid: %s" % (str(xy)))
if not isinstance(xy[0], numbers.Number) or not isinstance(xy[1], numbers.Number):
return ValueError("TextBox: coord variables must only contain numbers. Invalid: %s" % (str(xy)))
if units in ('pix', 'pixs'):
return xy
if units in ['deg', 'degs']:
return misc.deg2pix(xy[0], window.monitor), misc.deg2pix(xy[1], window.monitor)
if units in ['cm']:
return misc.cm2pix(xy[0], window.monitor), misc.cm2pix(xy[1], window.monitor)
if units in ['norm']:
# -1.0 to 1.0
if xy[0] <= 1.0 and xy[0] >= -1.0 and xy[1] <= 1.0 and xy[1] >= -1.0:
return xy[0] * window.size[0] / 2.0, xy[1] * window.size[1] / 2.0
return ValueError("TextBox: %s, %s could not be converted to pix units" % (str(xy), str(units)))
def _pix2units(self, xy, is_position=True):
units = self._units
window = self._window
ww, wh = float(window.size[0]), float(window.size[1])
if isinstance(xy, numbers.Number):
xy = xy, xy
elif is_sequence(xy):
if len(xy) == 1:
xy = xy[0], xy[0]
else:
xy = xy[:2]
else:
raise ValueError(
"TextBox: coord variables must be array-like or a single number. Invalid: %s" % (str(xy)))
if not isinstance(xy[0], numbers.Number) or not isinstance(xy[1], numbers.Number):
raise ValueError("TextBox: coord variables must only contain numbers. Invalid: %s, %s, %s" % (
str(xy), str(type(xy[0])), str(type(xy[1]))))
x, y = xy
if is_position:
# convert to psychopy pix, origin is center of monitor.
x, y = int(x - ww / 2), int(y - wh / 2)
if units in ('pix', 'pixs'):
return x, y
if units in ['deg', 'degs']:
return misc.pix2deg(x, window.monitor), misc.deg2pix(y, window.monitor)
if units in ['cm']:
return misc.pix2cm(x, window.monitor), misc.cm2pix(y, window.monitor)
if units in ['norm']:
return x / ww * 2.0, y / wh * 2.0
raise ValueError(
"TextBox: %s, %s could not be converted to pix units" % (str(xy), str(units)))
def _toRGBA(self, color):
return self.__class__._toRGBA2(color, self._opacity, self._color_space, self._window)
@classmethod
def _toRGBA2(cls, color, opacity=None, color_space=None, window=None):
if color is None:
raise ValueError("TextBox: None is not a valid color input")
#if not colors.isValidColor(color):
# raise ValueError(
# "TextBox: %s is not a valid color." % (str(color)))
valid_opacity = opacity >= 0.0 and opacity <= 1.0
if isinstance(color, str):
if color[0] == '#' or color[0:2].lower() == '0x':
rgb255color = colors.hex2rgb255(color)
if rgb255color and valid_opacity:
return rgb255color[0] / 255.0, rgb255color[1] / 255.0, rgb255color[2] / 255.0, opacity
else:
raise ValueError(
"TextBox: %s is not a valid hex color." % (str(color)))
named_color = colors.colors.get(color.lower())
if named_color and valid_opacity:
return (named_color[0] + 1.0) / 2.0, (named_color[1] + 1.0) / 2.0, (named_color[2] + 1.0) / 2.0, opacity
raise ValueError(
"TextBox: String color value could not be translated: %s" % (str(color)))
if isinstance(color, (float, int, int)) or (is_sequence(color) and len(color) == 3):
color = arraytools.val2array(color, length=3)
if color_space == 'dkl' and valid_opacity:
dkl_rgb = None
if window:
dkl_rgb = window.dkl_rgb
rgb = colortools.dkl2rgb(color, dkl_rgb)
return (rgb[0] + 1.0) / 2.0, (rgb[1] + 1.0) / 2.0, (rgb[2] + 1.0) / 2.0, opacity
if color_space == 'lms' and valid_opacity:
lms_rgb = None
if window:
lms_rgb = window.lms_rgb
rgb = colortools.lms2rgb(color, lms_rgb)
return (rgb[0] + 1.0) / 2.0, (rgb[1] + 1.0) / 2.0, (rgb[2] + 1.0) / 2.0, opacity
if color_space == 'hsv' and valid_opacity:
rgb = colortools.hsv2rgb(color)
return (rgb[0] + 1.0) / 2.0, (rgb[1] + 1.0) / 2.0, (rgb[2] + 1.0) / 2.0, opacity
if color_space == 'rgb255' and valid_opacity:
rgb = color
if [cc for cc in color if cc < 0 or cc > 255]:
raise ValueError(
'TextBox: rgb255 colors must contain elements between 0 and 255. Value: ' + str(rgb))
return rgb[0] / 255.0, rgb[1] / 255.0, rgb[2] / 255.0, opacity
if color_space == 'rgb' and valid_opacity:
rgb = color
if [cc for cc in color if cc < -1.0 or cc > 1.0]:
raise ValueError(
'TextBox: rgb colors must contain elements between -1.0 and 1.0. Value: ' + str(rgb))
return (rgb[0] + 1.0) / 2.0, (rgb[1] + 1.0) / 2.0, (rgb[2] + 1.0) / 2.0, opacity
if is_sequence(color) and len(color) == 4:
if color_space == 'rgb255':
if [cc for cc in color if cc < 0 or cc > 255]:
raise ValueError(
'TextBox: rgb255 colors must contain elements between 0 and 255. Value: ' + str(color))
return color[0] / 255.0, color[1] / 255.0, color[2] / 255.0, color[3] / 255.0
if color_space == 'rgb':
if [cc for cc in color if cc < -1.0 or cc > 1.0]:
raise ValueError(
'TextBox: rgb colors must contain elements between -1.0 and 1.0. Value: ' + str(color))
return (color[0] + 1.0) / 2.0, (color[1] + 1.0) / 2.0, (color[2] + 1.0) / 2.0, (color[3] + 1.0) / 2.0
raise ValueError("TextBox: color: %s, opacity: %s, is not a valid color for color space %s." % (
str(color), str(opacity), color_space))
def _reset(self):
self._text_grid.reset()
def _getPixelSize(self):
if self._units == 'norm':
r = self._toPix(
(self._size[0] - 1.0, self._size[1] - 1.0), self._units, self._window)
return int(r[0] + self._window.size[0] / 2), int(r[1] + self._window.size[1] / 2)
return [int(x) for x in self._toPix(self._size, self._units, self._window)]
def _setSize(self, pix_sz):
units = self._units
if units in ('pix', 'pixs'):
self._size = list(pix_sz)
if units in ['deg', 'degs']:
self._size = misc.pix2deg(pix_sz[0], self._window.monitor), misc.pix2deg(
pix_sz[1], self._window.monitor)
if units in ['cm']:
self._size = misc.pix2cm(pix_sz[0], self._window.monitor), misc.pix2cm(
pix_sz[1], self._window.monitor)
if units in ['norm']:
pw, ph = pix_sz
dw, dh = self._window.size
nw = (pw / float(dw)) * 2.0
nh = (ph / float(dh)) * 2.0
self._size = nw, nh
def _getPixelPosition(self):
ppos = self._toPix(self._position, self._units, self._window)
return int(ppos[0]), int(ppos[1])
def _getPixelTextLineSpacing(self):
if self._line_spacing:
max_size = self._current_glfont.max_tile_width, self._current_glfont.max_tile_height
line_spacing_units = self._line_spacing_units
line_spacing_height = self._line_spacing
if line_spacing_units == 'ratio':
# run though _toPix to validate line_spacing value type only
r = self._toPix(line_spacing_height, 'pix', self._window)[0]
return int(max_size[1] * r)
return self._toPix(line_spacing_height, line_spacing_units, self._window)
return 0
def _getTopLeftPixPos(self):
# Create a window position based on the window size, alignment types,
# TextBox size, etc...
win_w, win_h = self._window.size
te_w, te_h = self._getPixelSize()
te_x, te_y = self._getPixelPosition()
# convert te_x,te_y from psychopy pix coord to gl pix coord
te_x, te_y = te_x + win_w // 2, te_y + win_h // 2
# convert from alignment to top left
horz_align, vert_align = self._align_horz, self._align_vert
if horz_align.lower() == u'center':
te_x = te_x - te_w // 2
elif horz_align.lower() == u'right':
te_x = te_x - te_w
if vert_align.lower() == u'center':
te_y = te_y + te_h // 2
if vert_align.lower() == u'bottom':
te_y = te_y + te_h
return te_x, te_y
def __del__(self):
if hasattr(self, '_textbox_instance') and self.getName() in self._textbox_instance:
del self._textbox_instances[self.getName()]
del self._current_glfont
del self._text_grid