1
#Copyright ReportLab Europe Ltd. 2000-2004
2
#see license.txt for license details
3
#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/widgetbase.py
4
__version__=''' $Id$ '''
7
from reportlab.graphics import shapes
8
from reportlab import rl_config
9
from reportlab.lib import colors
10
from reportlab.lib.validators import *
11
from reportlab.lib.attrmap import *
15
'''Base for property holders'''
20
"""If the _attrMap attribute is not None, this
21
checks all expected attributes are present; no
22
unwanted attributes are present; and (if a
23
checking function is found) checks each
24
attribute has a valid value. Either succeeds
25
or raises an informative exception.
28
if self._attrMap is not None:
29
for key in self.__dict__.keys():
31
msg = "Unexpected attribute %s found in %s" % (key, self)
32
assert self._attrMap.has_key(key), msg
33
for (attr, metavalue) in self._attrMap.items():
34
msg = "Missing attribute %s from %s" % (attr, self)
35
assert hasattr(self, attr), msg
36
value = getattr(self, attr)
37
args = (value, attr, self.__class__.__name__)
38
assert metavalue.validate(value), "Invalid value %s for attribute %s in class %s" % args
40
if rl_config.shapeChecking:
41
"""This adds the ability to check every attribute assignment
42
as it is made. It slows down shapes but is a big help when
43
developing. It does not get defined if rl_config.shapeChecking = 0.
46
def __setattr__(self, name, value):
47
"""By default we verify. This could be off
48
in some parallel base classes."""
49
validateSetattr(self,name,value)
52
def getProperties(self,recur=1):
53
"""Returns a list of all properties which can be edited and
54
which are not marked as private. This may include 'child
55
widgets' or 'primitive shapes'. You are free to override
56
this and provide alternative implementations; the default
57
one simply returns everything without a leading underscore.
60
from reportlab.lib.validators import isValidChild
62
# TODO when we need it, but not before -
63
# expose sequence contents?
66
for name in self.__dict__.keys():
68
component = getattr(self, name)
70
if recur and isValidChild(component):
71
# child object, get its properties too
72
childProps = component.getProperties(recur=recur)
73
for (childKey, childValue) in childProps.items():
74
#key might be something indexed like '[2].fillColor'
75
#or simple like 'fillColor'; in the former case we
76
#don't need a '.' between me and my child.
77
if childKey[0] == '[':
78
props['%s%s' % (name, childKey)] = childValue
80
props['%s.%s' % (name, childKey)] = childValue
82
props[name] = component
87
def setProperties(self, propDict):
88
"""Permits bulk setting of properties. These may include
89
child objects e.g. "chart.legend.width = 200".
91
All assignments will be validated by the object as if they
92
were set individually in python code.
94
All properties of a top-level object are guaranteed to be
95
set before any of the children, which may be helpful to
100
for (name, value) in propDict.items():
101
parts = string.split(name, '.', 1)
103
#simple attribute, set it now
104
setattr(self, name, value)
106
(childName, remains) = parts
108
childPropDicts[childName][remains] = value
110
childPropDicts[childName] = {remains: value}
112
# now assign to children
113
for (childName, childPropDict) in childPropDicts.items():
114
child = getattr(self, childName)
115
child.setProperties(childPropDict)
118
def dumpProperties(self, prefix=""):
119
"""Convenience. Lists them on standard output. You
120
may provide a prefix - mostly helps to generate code
121
samples for documentation.
124
propList = self.getProperties().items()
127
prefix = prefix + '.'
128
for (name, value) in propList:
129
print '%s%s = %s' % (prefix, name, value)
132
class Widget(PropHolder, shapes.UserNode):
133
"""Base for all user-defined widgets. Keep as simple as possible. Does
134
not inherit from Shape so that we can rewrite shapes without breaking
135
widgets and vice versa."""
137
def _setKeywords(self,**kw):
138
for k,v in kw.items():
139
if not self.__dict__.has_key(k):
143
msg = "draw() must be implemented for each Widget!"
144
raise shapes.NotImplementedError, msg
147
msg = "demo() must be implemented for each Widget!"
148
raise shapes.NotImplementedError, msg
150
def provideNode(self):
154
"Return outer boundary as x1,y1,x2,y2. Can be overridden for efficiency"
155
return self.draw().getBounds()
159
class TypedPropertyCollection(PropHolder):
160
"""A container with properties for objects of the same kind.
162
This makes it easy to create lists of objects. You initialize
163
it with a class of what it is to contain, and that is all you
164
can add to it. You can assign properties to the collection
165
as a whole, or to a numeric index within it; if so it creates
166
a new child object to hold that data.
169
wedges = TypedPropertyCollection(WedgeProperties)
170
wedges.strokeWidth = 2 # applies to all
171
wedges.strokeColor = colors.red # applies to all
172
wedges[3].strokeColor = colors.blue # only to one
174
The last line should be taken as a prescription of how to
175
create wedge no. 3 if one is needed; no error is raised if
176
there are only two data points.
179
def __init__(self, exampleClass):
180
#give it same validation rules as what it holds
181
self.__dict__['_value'] = exampleClass()
182
self.__dict__['_children'] = {}
184
def __getitem__(self, index):
186
return self._children[index]
188
Klass = self._value.__class__
189
if _ItemWrapper.has_key(Klass):
190
WKlass = _ItemWrapper[Klass]
193
def __getattr__(self,name):
195
return self.__class__.__bases__[0].__getattr__(self,name)
197
if self._index and self._parent._children.has_key(self._index):
198
if self._parent._children[self._index].__dict__.has_key(name):
199
return getattr(self._parent._children[self._index],name)
200
return getattr(self._parent,name)
201
_ItemWrapper[Klass] = WKlass
205
if type(index) in (type(()),type([])):
208
child._index = tuple(index[:-1])
213
for i in filter(lambda x,K=child.__dict__.keys(): x in K,child._attrMap.keys()):
214
del child.__dict__[i]
216
self._children[index] = child
219
def has_key(self,key):
220
if type(key) in (type(()),type([])): key = tuple(key)
221
return self._children.has_key(key)
223
def __setitem__(self, key, value):
224
msg = "This collection can only hold objects of type %s" % self._value.__class__.__name__
225
assert isinstance(value, self._value.__class__), msg
228
return len(self._children.keys())
230
def getProperties(self,recur=1):
231
# return any children which are defined and whatever
232
# differs from the parent
235
for (key, value) in self._value.getProperties(recur=recur).items():
236
props['%s' % key] = value
238
for idx in self._children.keys():
239
childProps = self._children[idx].getProperties(recur=recur)
240
for (key, value) in childProps.items():
241
if not hasattr(self,key) or getattr(self, key)<>value:
242
newKey = '[%s].%s' % (idx, key)
243
props[newKey] = value
246
def setVector(self,**kw):
247
for name, value in kw.items():
248
for i in xrange(len(value)):
249
setattr(self[i],name,value[i])
251
def __getattr__(self,name):
252
return getattr(self._value,name)
254
def __setattr__(self,name,value):
255
return setattr(self._value,name,value)
258
class StyleProperties(PropHolder):
259
"""A container class for attributes used in charts and legends.
261
Attributes contained can be those for any graphical element
262
(shape?) in the ReportLab graphics package. The idea for this
263
container class is to be useful in combination with legends
264
and/or the individual appearance of data series in charts.
266
A legend could be as simple as a wrapper around a list of style
267
properties, where the 'desc' attribute contains a descriptive
268
string and the rest could be used by the legend e.g. to draw
269
something like a color swatch. The graphical presentation of
270
the legend would be its own business, though.
272
A chart could be inspecting a legend or, more directly, a list
273
of style properties to pick individual attributes that it knows
274
about in order to render a particular row of the data. A bar
275
chart e.g. could simply use 'strokeColor' and 'fillColor' for
276
drawing the bars while a line chart could also use additional
277
ones like strokeWidth.
281
strokeWidth = AttrMapValue(isNumber),
282
strokeLineCap = AttrMapValue(isNumber),
283
strokeLineJoin = AttrMapValue(isNumber),
284
strokeMiterLimit = AttrMapValue(None),
285
strokeDashArray = AttrMapValue(isListOfNumbersOrNone),
286
strokeOpacity = AttrMapValue(isNumber),
287
strokeColor = AttrMapValue(isColorOrNone),
288
fillColor = AttrMapValue(isColorOrNone),
289
desc = AttrMapValue(isString),
292
def __init__(self, **kwargs):
293
"Initialize with attributes if any."
295
for k, v in kwargs.items():
299
def __setattr__(self, name, value):
300
"Verify attribute name and value, before setting it."
301
validateSetattr(self,name,value)
304
class TwoCircles(Widget):
306
self.leftCircle = shapes.Circle(100,100,20, fillColor=colors.red)
307
self.rightCircle = shapes.Circle(300,100,20, fillColor=colors.red)
310
return shapes.Group(self.leftCircle, self.rightCircle)
314
"""This draws a face with two eyes.
316
It exposes a couple of properties
317
to configure itself and hides all other details.
321
x = AttrMapValue(isNumber),
322
y = AttrMapValue(isNumber),
323
size = AttrMapValue(isNumber),
324
skinColor = AttrMapValue(isColorOrNone),
325
eyeColor = AttrMapValue(isColorOrNone),
326
mood = AttrMapValue(OneOf('happy','sad','ok')),
333
self.skinColor = None
334
self.eyeColor = colors.blue
341
s = self.size # abbreviate as we will use this a lot
343
g.transform = [1,0,0,1,self.x, self.y]
346
g.add(shapes.Circle(s * 0.5, s * 0.5, s * 0.5, fillColor=self.skinColor))
349
g.add(shapes.Circle(s * 0.35, s * 0.65, s * 0.1, fillColor=colors.white))
350
g.add(shapes.Circle(s * 0.35, s * 0.65, s * 0.05, fillColor=self.eyeColor))
353
g.add(shapes.Circle(s * 0.65, s * 0.65, s * 0.1, fillColor=colors.white))
354
g.add(shapes.Circle(s * 0.65, s * 0.65, s * 0.05, fillColor=self.eyeColor))
357
g.add(shapes.Polygon(
358
points=[s * 0.5, s * 0.6, s * 0.4, s * 0.3, s * 0.6, s * 0.3],
362
if self.mood == 'happy':
364
elif self.mood == 'sad':
369
g.add(shapes.Polygon(
371
s * 0.3, s * 0.2, #left of mouth
372
s * 0.7, s * 0.2, #right of mouth
373
s * 0.6, s * (0.2 + offset), # the bit going up or down
374
s * 0.4, s * (0.2 + offset) # the bit going up or down
376
fillColor = colors.pink,
377
strokeColor = colors.red,
378
strokeWidth = s * 0.03
384
class TwoFaces(Widget):
386
self.faceOne = Face()
387
self.faceOne.mood = "happy"
388
self.faceTwo = Face()
390
self.faceTwo.mood = "sad"
393
"""Just return a group"""
394
return shapes.Group(self.faceOne, self.faceTwo)
397
"""The default case already looks good enough,
398
no implementation needed here"""
402
"Container to show size of all enclosed objects"
404
_attrMap = AttrMap(BASE=shapes.SolidShape,
405
contents = AttrMapValue(isListOfShapes,desc="Contained drawable elements"),
407
def __init__(self, *elements):
409
self.fillColor = colors.cyan
410
self.strokeColor = colors.magenta
412
for elem in elements:
415
def _addNamedNode(self,name,node):
416
'if name is not None add an attribute pointing to node and add to the attrMap'
418
if name not in self._attrMap.keys():
419
self._attrMap[name] = AttrMapValue(isValidChild)
420
setattr(self, name, node)
422
def add(self, node, name=None):
423
"""Appends non-None child node to the 'contents' attribute. In addition,
424
if a name is provided, it is subsequently accessible by name
426
# propagates properties down
428
assert isValidChild(node), "Can only add Shape or UserNode objects to a Group"
429
self.contents.append(node)
430
self._addNamedNode(name,node)
433
# get bounds of each object
436
for elem in self.contents:
437
b.append(elem.getBounds())
438
return shapes.getRectsBounds(b)
444
(x1, y1, x2, y2) = self.getBounds()
450
fillColor = self.fillColor,
451
strokeColor = self.strokeColor
454
for elem in self.contents:
459
from reportlab.graphics.charts.piecharts import WedgeProperties
460
wedges = TypedPropertyCollection(WedgeProperties)
461
wedges.fillColor = colors.red
462
wedges.setVector(fillColor=(colors.blue,colors.green,colors.white))
463
print len(_ItemWrapper)
465
d = shapes.Drawing(400, 200)
469
renderPDF.drawToFile(d, 'sample_widget.pdf', 'A Sample Widget')
470
print 'saved sample_widget.pdf'
472
d = shapes.Drawing(400, 200)
474
f.skinColor = colors.yellow
476
d.add(f, name='theFace')
477
print 'drawing 1 properties:'
479
renderPDF.drawToFile(d, 'face.pdf', 'A Sample Widget')
480
print 'saved face.pdf'
482
d2 = d.expandUserNodes()
483
renderPDF.drawToFile(d2, 'face_copy.pdf', 'An expanded drawing')
484
print 'saved face_copy.pdf'
485
print 'drawing 2 properties:'
489
if __name__=='__main__':