1
# Miro - an RSS based video player application
2
# Copyright (C) 2005-2010 Participatory Culture Foundation
4
# This program is free software; you can redistribute it and/or modify
5
# it under the terms of the GNU General Public License as published by
6
# the Free Software Foundation; either version 2 of the License, or
7
# (at your option) any later version.
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
# GNU General Public License for more details.
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, write to the Free Software
16
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
18
# In addition, as a special exception, the copyright holders give
19
# permission to link the code of portions of this program with the OpenSSL
22
# You must obey the GNU General Public License in all respects for all of
23
# the code used other than OpenSSL. If you modify file(s) with this
24
# exception, you may extend this exception to your version of the file(s),
25
# but you are not obligated to do so. If you do not wish to do so, delete
26
# this exception statement from your version. If you delete this exception
27
# statement from all source files in the program, then also delete it here.
29
"""``miro.frontends.widgets.cellpack`` -- Code to layout
32
We use the hbox/vbox model to lay things out with a couple changes.
33
The main difference here is that layouts are one-shot. We don't keep
34
state around inside the cell renderers, so we just set up the objects
35
at the start, then use them to calculate info.
39
"""Helper object used to calculate margins.
41
def __init__(self , margin):
44
self.margin_left = margin[3]
45
self.margin_top = margin[0]
46
self.margin_width = margin[1] + margin[3]
47
self.margin_height = margin[0] + margin[2]
49
def inner_rect(self, x, y, width, height):
50
"""Returns the x, y, width, height of the inner
53
return (x + self.margin_left,
55
width - self.margin_width,
56
height - self.margin_height)
58
def outer_size(self, inner_size):
59
"""Returns the width, height of the outer box.
61
return (inner_size[0] + self.margin_width,
62
inner_size[1] + self.margin_height)
64
def point_in_margin(self, x, y, width, height):
65
"""Returns whether a given point is inside of the
68
return ((0 <= x - self.margin_left < width - self.margin_width) and
69
(0 <= y - self.margin_top < height - self.margin_height))
71
class Packing(object):
72
"""Helper object used to layout Boxes.
74
def __init__(self, child, expand):
78
def calc_size(self, translate_func):
79
return translate_func(*self.child.get_size())
81
def draw(self, context, x, y, width, height):
82
self.child.draw(context, x, y, width, height)
84
class WhitespacePacking(object):
85
"""Helper object used to layout Boxes.
87
def __init__(self, size, expand):
91
def calc_size(self, translate_func):
94
def draw(self, context, x, y, width, height):
98
"""Base class packing objects. Packer objects work similarly to widgets,
99
but they only used in custom cell renderers so there's a couple
100
differences. The main difference is that cell renderers don't keep state
101
around. Therefore Packers just get set up, used, then discarded.
102
Also Packers can't receive events directly, so they have a different
103
system to figure out where mouse clicks happened (the Hotspot class).
106
def render_layout(self, context):
107
"""position the child elements then call draw() on them."""
108
self._layout(context, 0, 0, context.width, context.height)
110
def draw(self, context, x, y, width, height):
111
"""Included so that Packer objects have a draw() method that matches
112
ImageSurfaces, TextBoxes, etc.
114
self._layout(context, x, y, width, height)
116
def _find_child_at(self, x, y, width, height):
117
raise NotImplementedError()
120
"""Get the minimum size required to hold the Packer. """
123
except AttributeError:
124
self._size = self._calc_size()
127
def get_current_size(self):
128
"""Get the minimum size required to hold the Packer at this point
130
Call this method if you are going to change the packer after the call,
131
for example if you have more children to pack into a box. get_size()
132
saves caches it's result which is can mess things up.
134
return self._calc_size()
136
def find_hotspot(self, x, y, width, height):
137
"""Find the hotspot at (x, y). width and height are the size of the
138
cell this Packer is rendering.
140
If a hotspot is found, return the tuple (name, x, y, width, height)
141
where name is the name of the hotspot, x, y is the position relative
142
to the top-left of the hotspot area and width, height are the
143
dimensions of the hotspot.
145
If no Hotspot is found return None.
147
child_pos = self._find_child_at(x, y, width, height)
149
child, child_x, child_y, child_width, child_height = child_pos
151
return child.find_hotspot(x - child_x, y - child_y,
152
child_width, child_height)
153
except AttributeError:
154
pass # child is a TextBox, Button or something like that
157
def _layout(self, context, x, y, width, height):
158
"""Layout our children and call ``draw()`` on them.
160
raise NotImplementedError()
162
def _calc_size(self):
163
"""Calculate the size needed to hold the box. The return value gets
164
cached and return in ``get_size()``.
166
raise NotImplementedError()
169
"""Box is the base class for VBox and HBox. Box objects lay out children
170
linearly either left to right or top to bottom.
173
def __init__(self, spacing=0):
174
"""Create a new Box. spacing is the amount of space to place
177
self.spacing = spacing
179
self.children_end = []
180
self.expand_count = 0
182
def pack(self, child, expand=False):
183
"""Add a new child to the box. The child will be placed after all the
184
children packed before with pack_start.
186
:param child: child to pack. It can be anything with a
187
``get_size()`` method, including TextBoxes,
188
ImageSurfarces, Buttons, Boxes and Backgrounds.
189
:param expand: If True, then the child will enlarge if space
190
available is more than the space required.
192
if not (hasattr(child, 'draw') and hasattr(child, 'get_size')):
193
raise TypeError("%s can't be drawn" % child)
194
self.children.append(Packing(child, expand))
196
self.expand_count += 1
198
def pack_end(self, child, expand=False):
199
"""Add a new child to the end box. The child will be placed before
200
all the children packed before with pack_end.
202
:param child: child to pack. It can be anything with a
203
``get_size()`` method, including TextBoxes,
204
ImageSurfarces, Buttons, Boxes and Backgrounds.
205
:param expand: If True, then the child will enlarge if space
206
available is more than the space required.
208
if not (hasattr(child, 'draw') and hasattr(child, 'get_size')):
209
raise TypeError("%s can't be drawn" % child)
210
self.children_end.append(Packing(child, expand))
212
self.expand_count += 1
214
def pack_space(self, size, expand=False):
215
"""Pack whitespace into the box.
217
self.children.append(WhitespacePacking(size, expand))
219
self.expand_count += 1
221
def pack_space_end(self, size, expand=False):
222
"""Pack whitespace into the end of box.
224
self.children_end.append(WhitespacePacking(size, expand))
226
self.expand_count += 1
228
def _calc_size(self):
231
for packing in self.children + self.children_end:
232
child_length, child_breadth = packing.calc_size(self._translate)
233
length += child_length
234
breadth = max(breadth, child_breadth)
235
total_children = len(self.children) + len(self.children_end)
236
length += self.spacing * (total_children - 1)
237
return self._translate(length, breadth)
239
def _extra_space_iter(self, total_extra_space):
240
"""Generate the amount of extra space for children with expand set."""
241
if total_extra_space <= 0:
244
average_extra_space, leftover = \
245
divmod(total_extra_space, self.expand_count)
247
# expand_count doesn't divide equally into total_extra_space,
248
# yield average_extra_space+1 for each extra pixel
249
yield average_extra_space + 1
251
# if there's a fraction of a pixel leftover, add that in
252
yield average_extra_space + leftover
254
# no more leftover space
255
yield average_extra_space
257
def _position_children(self, total_length):
258
my_length, my_breadth = self._translate(*self.get_size())
259
extra_space_iter = self._extra_space_iter(total_length - my_length)
262
for packing in self.children:
263
child_length, child_breadth = packing.calc_size(self._translate)
265
child_length += extra_space_iter.next()
266
yield packing, pos, child_length
267
pos += child_length + self.spacing
270
for packing in self.children_end:
271
child_length, child_breadth = packing.calc_size(self._translate)
273
child_length += extra_space_iter.next()
275
yield packing, pos, child_length
278
def _layout(self, context, x, y, width, height):
279
total_length, total_breadth = self._translate(width, height)
280
pos, offset = self._translate(x, y)
281
position_iter = self._position_children(total_length)
282
for packing, child_pos, child_length in position_iter:
283
x, y = self._translate(pos + child_pos, offset)
284
width, height = self._translate(child_length, total_breadth)
285
packing.draw(context, x, y, width, height)
287
def _find_child_at(self, x, y, width, height):
288
total_length, total_breadth = self._translate(width, height)
289
pos, offset = self._translate(x, y)
290
position_iter = self._position_children(total_length)
291
for packing, child_pos, child_length in position_iter:
292
if child_pos <= pos < child_pos + child_length:
293
x, y = self._translate(child_pos, 0)
294
width, height = self._translate(child_length, total_breadth)
295
return packing.child, x, y, width, height
296
elif child_pos > pos:
300
def _translate(self, x, y):
301
"""Translate (x, y) coordinates into (length, breadth) and
304
raise NotImplementedError()
307
def _translate(self, x, y):
311
def _translate(self, x, y):
315
def __init__(self, row_length=1, col_length=1,
316
row_spacing=0, col_spacing=0):
317
"""Create a new Table.
319
:param row_length: how many rows long this should be
320
:param col_length: how many rows wide this should be
321
:param row_spacing: amount of spacing (in pixels) between rows
322
:param col_spacing: amount of spacing (in pixels) between columns
324
assert min(row_length, col_length) > 0
325
assert isinstance(row_length, int) and isinstance(col_length, int)
326
self.row_length = row_length
327
self.col_length = col_length
328
self.row_spacing = row_spacing
329
self.col_spacing = col_spacing
330
self.table_multiarray = self._generate_table_multiarray()
332
def _generate_table_multiarray(self):
333
table_multiarray = []
336
[None for col in range(self.col_length)]
337
for row in range(self.row_length)]
338
return table_multiarray
340
def pack(self, child, row, column, expand=False):
341
# TODO: flesh out "expand" ability, maybe?
343
# possibly throw a special exception if outside the range.
344
# For now, just allowing an IndexError to be thrown.
345
self.table_multiarray[row][column] = Packing(child, expand)
347
def _get_grid_sizes(self):
348
"""Get the width and eights for both rows and columns
352
for row_count, row in enumerate(self.table_multiarray):
353
row_sizes.setdefault(row_count, 0)
354
for col_count, col_packing in enumerate(row):
355
col_sizes.setdefault(col_count, 0)
357
x, y = col_packing.calc_size(self._translate)
358
if y > row_sizes[row_count]:
359
row_sizes[row_count] = y
360
if x > col_sizes[col_count]:
361
col_sizes[col_count] = x
362
return col_sizes, row_sizes
364
def _find_child_at(self, x, y, width, height):
365
col_sizes, row_sizes = self._get_grid_sizes()
367
for row_count, row in enumerate(self.table_multiarray):
369
for col_count, packing in enumerate(row):
370
child_width, child_height = packing.calc_size(self._translate)
372
if (col_distance <= x < col_distance + child_width
373
and row_distance <= y < row_distance + child_height):
374
return (packing.child,
375
col_distance, row_distance,
376
child_width, child_height)
377
col_distance += col_sizes[col_count] + self.col_spacing
378
row_distance += row_sizes[row_count] + self.row_spacing
380
def _calc_size(self):
381
col_sizes, row_sizes = self._get_grid_sizes()
382
x = sum(col_sizes.values()) + ((self.col_length - 1) * self.col_spacing)
383
y = sum(row_sizes.values()) + ((self.row_length - 1) * self.row_spacing)
386
def _layout(self, context, x, y, width, height):
387
col_sizes, row_sizes = self._get_grid_sizes()
390
for row_count, row in enumerate(self.table_multiarray):
392
for col_count, packing in enumerate(row):
394
child_width, child_height = packing.calc_size(self._translate)
395
packing.child.draw(context,
396
x + col_distance, y + row_distance,
397
child_width, child_height)
398
col_distance += col_sizes[col_count] + self.col_spacing
399
row_distance += row_sizes[row_count] + self.row_spacing
401
def _translate(self, x, y):
405
class Alignment(Packer):
406
"""Positions a child inside a larger space.
408
def __init__(self, child, xscale=1.0, yscale=1.0, xalign=0.0, yalign=0.0,
409
min_width=0, min_height=0):
415
self.min_width = min_width
416
self.min_height = min_height
418
def _calc_size(self):
419
width, height = self.child.get_size()
420
return max(self.min_width, width), max(self.min_height, height)
422
def _calc_child_position(self, width, height):
423
req_width, req_height = self.child.get_size()
424
child_width = req_width + self.xscale * (width-req_width)
425
child_height = req_height + self.yscale * (height-req_height)
426
child_x = round(self.xalign * (width - child_width))
427
child_y = round(self.yalign * (height - child_height))
428
return child_x, child_y, child_width, child_height
430
def _layout(self, context, x, y, width, height):
431
child_x, child_y, child_width, child_height = \
432
self._calc_child_position(width, height)
433
self.child.draw(context, x + child_x, y + child_y, child_width,
436
def _find_child_at(self, x, y, width, height):
437
child_x, child_y, child_width, child_height = \
438
self._calc_child_position(width, height)
439
if ((child_x <= x < child_x + child_width) and
440
(child_y <= y < child_y + child_height)):
441
return self.child, child_x, child_y, child_width, child_height
443
return None # (x, y) is in the empty space around child
445
class DrawingArea(Packer):
446
"""Area that uses custom drawing code.
448
def __init__(self, width, height, callback, *args):
451
self.callback_info = (callback, args)
453
def _calc_size(self):
454
return self.width, self.height
456
def _layout(self, context, x, y, width, height):
457
callback, args = self.callback_info
458
callback(context, x, y, width, height, *args)
460
def _find_child_at(self, x, y, width, height):
463
class Background(Packer):
464
"""Draws a background behind a child element.
466
def __init__(self, child, min_width=0, min_height=0, margin=None):
468
self.min_width = min_width
469
self.min_height = min_height
470
self.margin = Margin(margin)
471
self.callback_info = None
473
def set_callback(self, callback, *args):
474
self.callback_info = (callback, args)
476
def _calc_size(self):
477
width, height = self.child.get_size()
478
width = max(self.min_width, width)
479
height = max(self.min_height, height)
480
return self.margin.outer_size((width, height))
482
def _layout(self, context, x, y, width, height):
483
if self.callback_info:
484
callback, args = self.callback_info
485
callback(context, x, y, width, height, *args)
486
self.child.draw(context, *self.margin.inner_rect(x, y, width, height))
488
def _find_child_at(self, x, y, width, height):
489
if not self.margin.point_in_margin(x, y, width, height):
491
return (self.child,) + self.margin.inner_rect(0, 0, width, height)
493
class Padding(Packer):
494
"""Adds padding to the edges of a packer.
496
def __init__(self, child, top=0, right=0, bottom=0, left=0):
498
self.margin = Margin((top, right, bottom, left))
500
def _calc_size(self):
501
return self.margin.outer_size(self.child.get_size())
503
def _layout(self, context, x, y, width, height):
504
self.child.draw(context, *self.margin.inner_rect(x, y, width, height))
506
def _find_child_at(self, x, y, width, height):
507
if not self.margin.point_in_margin(x, y, width, height):
509
return (self.child,) + self.margin.inner_rect(0, 0, width, height)
511
class TextBoxPacker(Packer):
512
"""Base class for ClippedTextLine and ClippedTextBox.
514
def _layout(self, context, x, y, width, height):
515
self.textbox.draw(context, x, y, width, height)
517
def _find_child_at(self, x, y, width, height):
518
# We could return the TextBox here, but we know it doesn't have a
519
# find_hotspot() method
522
class ClippedTextBox(TextBoxPacker):
523
"""A TextBox that gets clipped if it's larger than it's allocated
526
def __init__(self, textbox, min_width=0, min_height=0):
527
self.textbox = textbox
528
self.min_width = min_width
529
self.min_height = min_height
531
def _calc_size(self):
532
height = max(self.min_height, self.textbox.font.line_height())
533
return self.min_width, height
535
class ClippedTextLine(TextBoxPacker):
536
"""A single line of text that gets clipped if it's larger than the
537
space allocated to it. By default the clipping will happen at character
540
def __init__(self, textbox, min_width=0):
541
self.textbox = textbox
542
self.textbox.set_wrap_style('char')
543
self.min_width = min_width
545
def _calc_size(self):
546
return self.min_width, self.textbox.font.line_height()
548
class TruncatedTextLine(ClippedTextLine):
549
def __init__(self, textbox, min_width=0):
550
ClippedTextLine.__init__(self, textbox, min_width)
551
self.textbox.set_wrap_style('truncated-char')
553
class Hotspot(Packer):
554
"""A Hotspot handles mouse click tracking. It's only purpose is
555
to store a name to return from ``find_hotspot()``. In terms of
556
layout, it simply renders it's child in it's allocated space.
558
def __init__(self, name, child):
562
def _calc_size(self):
563
return self.child.get_size()
565
def _layout(self, context, x, y, width, height):
566
self.child.draw(context, x, y, width, height)
568
def find_hotspot(self, x, y, width, height):
569
return self.name, x, y, width, height
572
"""Packer that stacks other packers on top of each other.
577
def pack(self, packer):
578
self.children.append(packer)
580
def pack_below(self, packer):
581
self.children.insert(0, packer)
583
def _layout(self, context, x, y, width, height):
584
for packer in self.children:
585
packer._layout(context, x, y, width, height)
587
def _calc_size(self):
588
"""Calculate the size needed to hold the box. The return value gets
589
cached and return in get_size().
592
for packer in self.children:
593
child_width, child_height = packer.get_size()
594
width = max(width, child_width)
595
height = max(height, child_height)
598
def _find_child_at(self, x, y, width, height):
599
# Return the topmost packer
601
top = self.children[-1]
605
return top._find_child_at(x, y, width, height)
607
def align_left(packer):
608
"""Align a packer to the left side of it's allocated space."""
609
return Alignment(packer, xalign=0.0, xscale=0.0)
611
def align_right(packer):
612
"""Align a packer to the right side of it's allocated space."""
613
return Alignment(packer, xalign=1.0, xscale=0.0)
615
def align_top(packer):
616
"""Align a packer to the top side of it's allocated space."""
617
return Alignment(packer, yalign=0.0, yscale=0.0)
619
def align_bottom(packer):
620
"""Align a packer to the bottom side of it's allocated space."""
621
return Alignment(packer, yalign=1.0, yscale=0.0)
623
def align_middle(packer):
624
"""Align a packer to the middle of it's allocated space."""
625
return Alignment(packer, yalign=0.5, yscale=0.0)
627
def align_center(packer):
628
"""Align a packer to the center of it's allocated space."""
629
return Alignment(packer, xalign=0.5, xscale=0.0)
631
def pad(packer, top=0, left=0, bottom=0, right=0):
632
"""Add padding to a packer."""
633
return Padding(packer, top, right, bottom, left)