Source code for enable.label
# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX
# All rights reserved.
#
# This software is provided without warranty under the terms of the BSD
# license included in LICENSE.txt and may be redistributed only under
# the conditions described in the aforementioned license. The license
# is also available online at http://www.enthought.com/licenses/BSD.txt
#
# Thanks for using Enthought open source!
""" Defines the Label class.
"""
# Major library imports
from math import pi
from numpy import asarray
# Enthought library imports
from kiva.api import FILL, STROKE
from enable.trait_defs.kiva_font_trait import KivaFont
from traits.api import Bool, Enum, Float, HasTraits, Int, List, Str, observe
# Local, relative imports
from .colors import black_color_trait, transparent_color_trait
from .component import Component
[docs]class Label(Component):
""" A text label """
# The label text. Carriage returns (\n) are always connverted into
# line breaks.
text = Str
# The angle of rotation of the label. Only multiples of 90 are supported.
rotate_angle = Float(0)
# The color of the label text.
color = black_color_trait
# The background color of the label.
bgcolor = transparent_color_trait
# The width of the label border. If it is 0, then it is not shown.
border_width = Int(0)
# The color of the border.
border_color = black_color_trait
# The font of the label text.
font = KivaFont("modern 10")
# Number of pixels of margin around the label, for both X and Y dimensions.
margin = Int(2)
# Number of pixels of spacing between lines of text.
line_spacing = Int(5)
# The horizontal placement of text within the bounds of the label
hjustify = Enum("left", "center", "right")
# The vertical placement of text within the bounds of the label
vjustify = Enum("bottom", "center", "top")
# By default, labels are not resizable
resizable = ""
# ------------------------------------------------------------------------
# Private traits
# ------------------------------------------------------------------------
_bounding_box = List()
_position_cache_valid = Bool(False)
def __init__(self, text="", **kwtraits):
if "text" not in kwtraits:
kwtraits["text"] = text
HasTraits.__init__(self, **kwtraits)
self._bounding_box = [0, 0]
def _calc_line_positions(self, gc):
if not self._position_cache_valid:
with gc:
gc.set_font(self.font)
# The bottommost line starts at postion (0,0).
x_pos = []
y_pos = []
self._bounding_box = [0, 0]
margin = self.margin
prev_y_pos = margin
prev_y_height = -self.line_spacing
max_width = 0
for line in self.text.split("\n")[::-1]:
if line != "":
(
descent,
leading,
width,
height,
) = gc.get_text_extent(line)
if width > max_width:
max_width = width
new_y_pos = (
prev_y_pos
+ prev_y_height
- descent
+ self.line_spacing
)
else:
# For blank lines, we use the height of the previous
# line, if there is one. The width is 0.
leading = 0
if prev_y_height != -self.line_spacing:
new_y_pos = (
prev_y_pos + prev_y_height + self.line_spacing
)
height = prev_y_height
else:
new_y_pos = prev_y_pos
height = 0
x_pos.append(-leading + margin)
y_pos.append(new_y_pos)
prev_y_pos = new_y_pos
prev_y_height = height
width = max_width + 2 * margin + 2 * self.border_width
height = (
prev_y_pos + prev_y_height + margin + 2 * self.border_width
)
self._bounding_box = [width, height]
if self.hjustify == "left":
x_pos = x_pos[::-1]
else:
x_pos = asarray(x_pos[::-1], dtype=float)
if self.hjustify == "center":
x_pos += (self.width - width) / 2.0
elif self.hjustify == "right":
x_pos += self.width - width
self._line_xpos = x_pos
if self.vjustify == "bottom":
y_pos = y_pos[::-1]
else:
y_pos = asarray(y_pos[::-1], dtype=float)
if self.vjustify == "center":
y_pos += (self.height - height) / 2.0
elif self.vjustify == "top":
y_pos += self.height - height
self._line_ypos = y_pos
self._position_cache_valid = True
[docs] def get_width_height(self, gc):
""" Returns the width and height of the label, in the rotated frame of
reference.
"""
self._calc_line_positions(gc)
width, height = self._bounding_box
return width, height
[docs] def get_bounding_box(self, gc):
""" Returns a rectangular bounding box for the Label as (width,height).
"""
# FIXME: Need to deal with non 90 deg rotations
width, height = self.get_width_height(gc)
if self.rotate_angle in (90.0, 270.0):
return (height, width)
elif self.rotate_angle in (0.0, 180.0):
return (width, height)
else:
raise NotImplementedError
[docs] def get_bounding_poly(self, gc):
"""
Returns a list [(x0,y0), (x1,y1),...] of tuples representing a polygon
that bounds the label.
"""
raise NotImplementedError
def _draw_mainlayer(self, gc, view_bounds=None, mode="normal"):
""" Draws the label.
This method assumes the graphics context has been translated to the
correct position such that the origin is at the lower left-hand corner
of this text label's box.
"""
# For this version we're not supporting rotated text.
# temp modified for only one line
self._calc_line_positions(gc)
with gc:
gc.translate_ctm(*self.position)
gc.set_fill_color(self.color_)
gc.set_stroke_color(self.color_)
gc.set_font(self.font)
if self.font.size <= 8.0:
gc.set_antialias(0)
else:
gc.set_antialias(1)
gc.rotate_ctm(pi / 180.0 * self.rotate_angle)
# margin = self.margin
lines = self.text.split("\n")
gc.translate_ctm(self.border_width, self.border_width)
width, height = self.get_width_height(gc)
for i, line in enumerate(lines):
if line == "":
continue
if self.rotate_angle == 90.0 or self.rotate_angle == 270.0:
x_offset = round(self._line_ypos[i])
# this should really be "... - height/2" but
# that looks wrong
y_offset = round(self._line_xpos[i] - height)
else:
x_offset = round(self._line_xpos[i])
y_offset = round(self._line_ypos[i])
gc.set_text_position(0, 0)
gc.translate_ctm(x_offset, y_offset)
gc.show_text(line)
gc.translate_ctm(-x_offset, -y_offset)
def _font_changed(self):
self._position_cache_valid = False
def _margin_changed(self):
self._position_cache_valid = False
def _text_changed(self):
self._position_cache_valid = False
def _rotate_angle_changed(self):
self._position_cache_valid = False
@observe('bounds.items')
def _update_bounds(self, event):
self._position_cache_valid = False