1
""" Defines the Label class.
4
from __future__ import with_statement
6
# Major library imports
7
from math import cos, sin, pi
8
from numpy import array, dot
10
# Enthought library imports
11
from enthought.enable.api import black_color_trait, transparent_color_trait
12
from enthought.kiva.traits.kiva_font_trait import KivaFont
13
from enthought.traits.api import Any, Bool, Enum, Float, HasTraits, Int, \
14
List, Str, on_trait_change
17
class Label(HasTraits):
18
""" A label used by overlays.
20
Label is not a Component; it's just an object encapsulating text settings
21
and appearance attributes. It can be used by components that need text
22
labels to store state, perform layout, and render the text.
25
# The anchor point is the position on the label that is placed at the
26
# label's position. The label is also rotated relative to this point.
27
# "Left" refers to the left edge of the text's bounding box (including
28
# margin), while "center" refers to the horizontal and vertical center
29
# of the bounding box.
30
# TODO: Implement this and test thoroughly
31
#anchor = Enum("left", "right", "top", "bottom", "center",
32
# "top left", "top right", "bottom left", "bottom right")
34
# The label text. Carriage returns (\n) are always connverted into
38
# The angle of rotation of the label.
39
rotate_angle = Float(0)
41
# The color of the label text.
42
color = black_color_trait
44
# The background color of the label.
45
bgcolor = transparent_color_trait
47
# The width of the label border. If it is 0, then it is not shown.
50
# The color of the border.
51
border_color = black_color_trait
53
# Whether or not the border is visible
54
border_visible = Bool(True)
56
# The font of the label text.
57
font = KivaFont("modern 10")
59
# Number of pixels of margin around the label, for both X and Y dimensions.
62
# Number of pixels of spacing between lines of text.
66
#------------------------------------------------------------------------
68
#------------------------------------------------------------------------
70
_bounding_box = List()
71
_position_cache_valid = Bool(False)
76
def __init__(self, **traits):
77
HasTraits.__init__(self, **traits)
78
self._bounding_box = [0,0]
81
def _calc_line_positions(self, gc):
82
if not self._position_cache_valid:
84
gc.set_font(self.font)
85
# The bottommost line starts at postion (0,0).
88
self._bounding_box = [0,0]
91
prev_y_height = -self.line_spacing
93
for line in self.text.split("\n")[::-1]:
95
(width, height, descent, leading) = gc.get_full_text_extent(line)
96
ascent = height - abs(descent)
99
new_y_pos = prev_y_pos + prev_y_height + self.line_spacing
101
# For blank lines, we use the height of the previous line, if there
102
# is one. The width is 0.
104
if prev_y_height != -self.line_spacing:
105
new_y_pos = prev_y_pos + prev_y_height + self.line_spacing
106
ascent = prev_y_height
108
new_y_pos = prev_y_pos
110
x_pos.append(-leading + margin)
111
y_pos.append(new_y_pos)
112
prev_y_pos = new_y_pos
113
prev_y_height = ascent
115
self._line_xpos = x_pos[::-1]
116
self._line_ypos = y_pos[::-1]
117
border_width = self.border_width if self.border_visible else 0
118
self._bounding_box[0] = max_width + 2*margin + 2*border_width
119
self._bounding_box[1] = prev_y_pos + prev_y_height + margin + 2*border_width
120
self._position_cache_valid = True
123
def get_width_height(self, gc):
124
""" Returns the width and height of the label, in the rotated frame of
127
self._calc_line_positions(gc)
128
width, height = self._bounding_box
131
def get_bounding_box(self, gc):
132
""" Returns a rectangular bounding box for the Label as (width,height).
134
width, height = self.get_width_height(gc)
135
if self.rotate_angle in (90.0, 270.0):
136
return (height, width)
137
elif self.rotate_angle in (0.0, 180.0):
138
return (width, height)
140
angle = self.rotate_angle
141
return (abs(width*cos(angle))+abs(height*sin(angle)),
142
abs(height*sin(angle))+abs(width*cos(angle)))
144
def get_bounding_poly(self, gc):
146
Returns a list [(x0,y0), (x1,y1),...] of tuples representing a polygon
147
that bounds the label.
149
width, height = self.get_width_height(gc)
150
offset = array(self.get_bounding_box(gc))/2.
151
# unrotated points relative to centre
153
array([[-width/2.], [-height/2.]]),
154
array([[-width/2.], [height/2.]]),
155
array([[width/2.], [height/2.]]),
156
array([[width/2.], [-height/2.]]),
157
array([[-width/2.], [-height/2.]]),
159
# rotate about centre, and offset to bounding box coords
160
points = [dot(self.get_rotation_matrix(), point).transpose()[0]+offset
161
for point in base_points]
164
def get_rotation_matrix(self):
165
return array([[cos(self.rotate_angle), -sin(self.rotate_angle)],
166
[sin(self.rotate_angle), cos(self.rotate_angle)]])
171
This method assumes the graphics context has been translated to the
172
correct position such that the origin is at the lower left-hand corner
173
of this text label's box.
175
# For this version we're not supporting rotated text.
176
self._calc_line_positions(gc)
179
bb_width, bb_height = self.get_bounding_box(gc)
181
# Rotate label about center of bounding box
182
width, height = self._bounding_box
183
gc.translate_ctm(bb_width/2.0, bb_height/2.0)
184
gc.rotate_ctm(pi/180.0*self.rotate_angle)
185
gc.translate_ctm(-width/2.0, -height/2.0)
187
# Draw border and fill background
188
if self.bgcolor != "transparent":
189
gc.set_fill_color(self.bgcolor_)
190
gc.rect(0, 0, width, height)
192
if self.border_visible and self.border_width > 0:
193
gc.set_stroke_color(self.border_color_)
194
gc.set_line_width(self.border_width)
195
border_offset = (self.border_width-1)/2.0
196
gc.rect(border_offset, border_offset, width-2*border_offset, height-2*border_offset)
199
gc.set_fill_color(self.color_)
200
gc.set_stroke_color(self.color_)
201
gc.set_font(self.font)
202
if self.font.size<=8.0:
207
lines = self.text.split("\n")
208
if self.border_visible:
209
gc.translate_ctm(self.border_width, self.border_width)
210
width, height = self.get_width_height(gc)
212
for i, line in enumerate(lines):
215
x_offset = round(self._line_xpos[i])
216
y_offset = round(self._line_ypos[i])
217
gc.set_text_position(x_offset, y_offset)
222
@on_trait_change("font,margin,text,rotate_angle")
223
def _invalidate_position_cache(self):
224
self._position_cache_valid = False