~ubuntu-branches/ubuntu/natty/miro/natty

« back to all changes in this revision

Viewing changes to platform/osx/plat/frontends/widgets/layout.py

  • Committer: Bazaar Package Importer
  • Author(s): Bryce Harrington
  • Date: 2011-01-22 02:46:33 UTC
  • mfrom: (1.4.10 upstream) (1.7.5 experimental)
  • Revision ID: james.westby@ubuntu.com-20110122024633-kjme8u93y2il5nmf
Tags: 3.5.1-1ubuntu1
* Merge from debian.  Remaining ubuntu changes:
  - Use python 2.7 instead of python 2.6
  - Relax dependency on python-dbus to >= 0.83.0

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Miro - an RSS based video player application
2
 
# Copyright (C) 2005-2010 Participatory Culture Foundation
3
 
#
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.
8
 
#
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.
13
 
#
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
17
 
#
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
20
 
# library.
21
 
#
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.
28
 
 
29
 
"""miro.plat.frontends.widgets.layout -- Widgets that handle laying out other
30
 
widgets.
31
 
 
32
 
We basically follow GTK's packing model.  Widgets are packed into vboxes,
33
 
hboxes or other container widgets.  The child widgets request a minimum size,
34
 
and the container widgets allocate space for their children.  Widgets may get
35
 
more size then they requested in which case they have to deal with it.  In
36
 
rare cases, widgets may get less size then they requested in which case they
37
 
should just make sure they don't throw an exception or segfault.
38
 
 
39
 
Check out the GTK tutorial for more info.
40
 
"""
41
 
 
42
 
import itertools
43
 
 
44
 
from AppKit import *
45
 
from Foundation import *
46
 
from objc import YES, NO, nil, signature, loadBundle
47
 
import WebKit
48
 
 
49
 
from miro.plat.frontends.widgets import tableview
50
 
from miro.plat.frontends.widgets import wrappermap
51
 
from miro.plat.frontends.widgets import viewport
52
 
from miro.plat.frontends.widgets.base import Container, Bin, FlippedView
53
 
from miro.plat.frontends.widgets.helpers import NotificationForwarder
54
 
from miro.util import Matrix
55
 
 
56
 
rbSplitViewBundlePath = '%s/RBSplitView.framework' % NSBundle.mainBundle().privateFrameworksPath()
57
 
loadBundle('RBSplitView', globals(), bundle_path=rbSplitViewBundlePath)
58
 
 
59
 
def _extra_space_iter(extra_length, count):
60
 
    """Utility function to allocate extra space left over in containers."""
61
 
    if count == 0:
62
 
        return
63
 
    extra_space, leftover = divmod(extra_length, count)
64
 
    while leftover >= 1:
65
 
        yield extra_space + 1
66
 
        leftover -= 1
67
 
    yield extra_space + leftover
68
 
    while True:
69
 
        yield extra_space
70
 
 
71
 
class BoxPacking:
72
 
    """Utility class to store how we are packing a single widget."""
73
 
 
74
 
    def __init__(self, widget, expand, padding):
75
 
        self.widget = widget
76
 
        self.expand = expand
77
 
        self.padding = padding
78
 
 
79
 
class Box(Container):
80
 
    """Base class for HBox and VBox.  """
81
 
    CREATES_VIEW = False
82
 
 
83
 
    def __init__(self, spacing=0):
84
 
        self.spacing = spacing
85
 
        Container.__init__(self)
86
 
        self.packing_start = []
87
 
        self.packing_end = []
88
 
        self.expand_count = 0
89
 
 
90
 
    def packing_both(self):
91
 
        return itertools.chain(self.packing_start, self.packing_end)
92
 
 
93
 
    def get_children(self):
94
 
        for packing in self.packing_both():
95
 
            yield packing.widget
96
 
    children = property(get_children)
97
 
 
98
 
    # Internally Boxes use a (length, breadth) coordinate system.  length and
99
 
    # breadth will be either x or y depending on which way the box is
100
 
    # oriented.  The subclasses must provide methods to translate between the
101
 
    # 2 coordinate systems.
102
 
 
103
 
    def translate_size(self, size):
104
 
        """Translate a (width, height) tulple to (length, breadth)."""
105
 
        raise NotImplementedError()
106
 
 
107
 
    def untranslate_size(self, size):
108
 
        """Reverse the work of translate_size."""
109
 
        raise NotImplementedError()
110
 
 
111
 
    def make_child_rect(self, position, length):
112
 
        """Create a rect to position a child with."""
113
 
        raise NotImplementedError()
114
 
 
115
 
    def pack_start(self, child, expand=False, padding=0):
116
 
        self.packing_start.append(BoxPacking(child, expand, padding))
117
 
        if expand:
118
 
            self.expand_count += 1
119
 
        self.child_added(child)
120
 
 
121
 
    def pack_end(self, child, expand=False, padding=0):
122
 
        self.packing_end.append(BoxPacking(child, expand, padding))
123
 
        if expand:
124
 
            self.expand_count += 1
125
 
        self.child_added(child)
126
 
 
127
 
    def _remove_from_packing(self, child):
128
 
        for i in xrange(len(self.packing_start)):
129
 
            if self.packing_start[i].widget is child:
130
 
                return self.packing_start.pop(i)
131
 
        for i in xrange(len(self.packing_end)):
132
 
            if self.packing_end[i].widget is child:
133
 
                return self.packing_end.pop(i)
134
 
        raise LookupError("%s not found" % child)
135
 
 
136
 
    def remove(self, child):
137
 
        packing = self._remove_from_packing(child)
138
 
        if packing.expand:
139
 
            self.expand_count -= 1
140
 
        self.child_removed(child)
141
 
 
142
 
    def translate_widget_size(self, widget):
143
 
        return self.translate_size(widget.get_size_request())
144
 
 
145
 
    def calc_size_request(self):
146
 
        length = breadth = 0
147
 
        for packing in self.packing_both():
148
 
            child_length, child_breadth = \
149
 
                    self.translate_widget_size(packing.widget)
150
 
            length += child_length
151
 
            if packing.padding:
152
 
                length += packing.padding * 2 # Need to pad on both sides
153
 
            breadth = max(breadth, child_breadth)
154
 
        spaces = max(0, len(self.packing_start) + len(self.packing_end) - 1)
155
 
        length += spaces * self.spacing
156
 
        return self.untranslate_size((length, breadth))
157
 
 
158
 
    def place_children(self):
159
 
        request_length, request_breadth = self.translate_widget_size(self)
160
 
        ps = self.viewport.placement.size
161
 
        total_length, dummy = self.translate_size((ps.width, ps.height))
162
 
        total_extra_space = total_length - request_length
163
 
        extra_space_iter = _extra_space_iter(total_extra_space,
164
 
                self.expand_count)
165
 
        start_end = self._place_packing_list(self.packing_start, 
166
 
                extra_space_iter, 0)
167
 
        if self.expand_count == 0 and total_extra_space > 0:
168
 
            # account for empty space after the end of pack_start list and
169
 
            # before the pack_end list.
170
 
            self.draw_empty_space(start_end, total_extra_space)
171
 
            start_end += total_extra_space
172
 
        self._place_packing_list(reversed(self.packing_end), extra_space_iter, 
173
 
                start_end)
174
 
 
175
 
    def draw_empty_space(self, start, length):
176
 
        empty_rect = self.make_child_rect(start, length)
177
 
        my_view = self.viewport.view
178
 
        opaque_view = my_view.opaqueAncestor()
179
 
        if opaque_view is not None:
180
 
            empty_rect2 = opaque_view.convertRect_fromView_(empty_rect, my_view)
181
 
            opaque_view.setNeedsDisplayInRect_(empty_rect2)
182
 
 
183
 
    def _place_packing_list(self, packing_list, extra_space_iter, position):
184
 
        for packing in packing_list:
185
 
            child_length, child_breadth = \
186
 
                    self.translate_widget_size(packing.widget)
187
 
            if packing.expand:
188
 
                child_length += extra_space_iter.next()
189
 
            if packing.padding: # space before
190
 
                self.draw_empty_space(position, packing.padding)
191
 
                position += packing.padding
192
 
            child_rect = self.make_child_rect(position, child_length)
193
 
            if packing.padding: # space after
194
 
                self.draw_empty_space(position, packing.padding)
195
 
                position += packing.padding
196
 
            packing.widget.place(child_rect, self.viewport.view)
197
 
            position += child_length
198
 
            if self.spacing > 0:
199
 
                self.draw_empty_space(position, self.spacing)
200
 
                position += self.spacing
201
 
        return position
202
 
 
203
 
    def enable(self):
204
 
        Container.enable(self)
205
 
        for mem in self.children:
206
 
            mem.enable()
207
 
 
208
 
    def disable(self):
209
 
        Container.disable(self)
210
 
        for mem in self.children:
211
 
            mem.disable()
212
 
 
213
 
class VBox(Box):
214
 
    """See https://develop.participatoryculture.org/trac/democracy/wiki/WidgetAPI for a description of the API for this class."""
215
 
    def translate_size(self, size):
216
 
        return (size[1], size[0])
217
 
 
218
 
    def untranslate_size(self, size):
219
 
        return (size[1], size[0])
220
 
 
221
 
    def make_child_rect(self, position, length):
222
 
        placement = self.viewport.placement
223
 
        return NSMakeRect(placement.origin.x, placement.origin.y + position,
224
 
                placement.size.width, length)
225
 
 
226
 
class HBox(Box):
227
 
    """See https://develop.participatoryculture.org/trac/democracy/wiki/WidgetAPI for a description of the API for this class."""
228
 
    def translate_size(self, size):
229
 
        return (size[0], size[1])
230
 
 
231
 
    def untranslate_size(self, size):
232
 
        return (size[0], size[1])
233
 
 
234
 
    def make_child_rect(self, position, length):
235
 
        placement = self.viewport.placement
236
 
        return NSMakeRect(placement.origin.x + position, placement.origin.y,
237
 
                length, placement.size.height)
238
 
 
239
 
class Alignment(Bin):
240
 
    """See https://develop.participatoryculture.org/trac/democracy/wiki/WidgetAPI for a description of the API for this class."""
241
 
    CREATES_VIEW = False
242
 
 
243
 
    def __init__(self, xalign=0.0, yalign=0.0, xscale=0.0, yscale=0.0,
244
 
            top_pad=0, bottom_pad=0, left_pad=0, right_pad=0):
245
 
        Bin.__init__(self)
246
 
        self.xalign = xalign
247
 
        self.yalign = yalign
248
 
        self.xscale = xscale
249
 
        self.yscale = yscale
250
 
        self.top_pad = top_pad
251
 
        self.bottom_pad = bottom_pad
252
 
        self.left_pad = left_pad
253
 
        self.right_pad = right_pad
254
 
        if self.child is not None:
255
 
            self.place_children()
256
 
 
257
 
    def set(self, xalign=0.0, yalign=0.0, xscale=0.0, yscale=0.0):
258
 
        self.xalign = xalign
259
 
        self.yalign = yalign
260
 
        self.xscale = xscale
261
 
        self.yscale = yscale
262
 
        if self.child is not None:
263
 
            self.place_children()
264
 
 
265
 
    def set_padding(self, top_pad=0, bottom_pad=0, left_pad=0, right_pad=0):
266
 
        self.top_pad = top_pad
267
 
        self.bottom_pad = bottom_pad
268
 
        self.left_pad = left_pad
269
 
        self.right_pad = right_pad
270
 
        if self.child is not None:
271
 
            self.place_children()
272
 
 
273
 
    def vertical_pad(self):
274
 
        return self.top_pad + self.bottom_pad
275
 
 
276
 
    def horizontal_pad(self):
277
 
        return self.left_pad + self.right_pad
278
 
 
279
 
    def calc_size_request(self):
280
 
        if self.child:
281
 
            child_width, child_height = self.child.get_size_request()
282
 
            return (child_width + self.horizontal_pad(),
283
 
                    child_height + self.vertical_pad())
284
 
        else:
285
 
            return (0, 0)
286
 
 
287
 
    def calc_size(self, requested, total, scale):
288
 
        extra_width = max(0, total - requested)
289
 
        return requested + int(round(extra_width * scale))
290
 
 
291
 
    def calc_position(self, size, total, align):
292
 
        return int(round((total - size) * align))
293
 
 
294
 
    def place_children(self):
295
 
        if self.child is None:
296
 
            return
297
 
 
298
 
        total_width = self.viewport.placement.size.width
299
 
        total_height = self.viewport.placement.size.height
300
 
        total_width -= self.horizontal_pad()
301
 
        total_height -= self.vertical_pad()
302
 
        request_width, request_height = self.child.get_size_request()
303
 
 
304
 
        child_width = self.calc_size(request_width, total_width, self.xscale)
305
 
        child_height = self.calc_size(request_height, total_height, self.yscale)
306
 
        child_x = self.calc_position(child_width, total_width, self.xalign)
307
 
        child_y = self.calc_position(child_height, total_height, self.yalign)
308
 
        child_x += self.left_pad
309
 
        child_y += self.top_pad
310
 
        
311
 
        my_origin = self.viewport.area().origin
312
 
        child_rect = NSMakeRect(my_origin.x + child_x, my_origin.y + child_y,  child_width, child_height)
313
 
        self.child.place(child_rect, self.viewport.view)
314
 
        # Make sure the space not taken up by our child is redrawn.
315
 
        self.viewport.queue_redraw()
316
 
 
317
 
class DetachedWindowHolder(Alignment):
318
 
    def __init__(self):
319
 
        Alignment.__init__(self, bottom_pad=16, xscale=1.0, yscale=1.0)
320
 
 
321
 
class SplitterDelegate(NSObject):
322
 
 
323
 
    def initWithSplitter_(self, splitter):
324
 
        self = super(SplitterDelegate, self).init()
325
 
        self.splitter = splitter
326
 
        self.normalColor = NSColor.colorWithDeviceWhite_alpha_(64.0/255.0, 1.0)
327
 
        self.disabledColor = NSColor.colorWithDeviceWhite_alpha_(135.0/255.0, 1.0)
328
 
        return self
329
 
 
330
 
    @signature("{_NSRect={_NSPoint=ff}{_NSSize=ff}}@:@{_NSRect={_NSPoint=ff}{_NSSize=ff}}i")
331
 
    def splitView_cursorRect_forDivider_(self, sender, rect, divider):
332
 
        if divider == 0:
333
 
            rect.origin.x -= 3
334
 
            rect.size.width += 6
335
 
        return rect
336
 
 
337
 
    @signature("i@:@{_NSPoint=ff}@")
338
 
    def splitView_dividerForPoint_inSubview_(self, sender, point, subview):
339
 
        left_width = self.splitter.left_view.bounds().size.width
340
 
        if (subview.identifier() == 'left' and point.x >= left_width-3) or (subview.identifier() == 'right' and point.x-left_width <= 3):
341
 
            return 0
342
 
        return NSNotFound
343
 
 
344
 
    @signature("v@:@@{_NSRect={_NSPoint=ff}{_NSSize=ff}}{_NSRect={_NSPoint=ff}{_NSSize=ff}}")
345
 
    def splitView_changedFrameOfSubview_from_to_(self, sender, subview, before, after):
346
 
        if subview.identifier() == 'left':
347
 
            self.splitter.place_left_children()
348
 
        else:
349
 
            self.splitter.place_right_children()
350
 
 
351
 
    @signature("v@:@ff")
352
 
    def splitView_wasResizedFrom_to_(self, sender, before, after):
353
 
        sender.adjustSubviewsExcepting_(self.splitter.left_view);
354
 
 
355
 
    @signature("{_NSRect={_NSPoint=ff}{_NSSize=ff}}@:@{_NSRect={_NSPoint=ff}{_NSSize=ff}}@@{_NSRect={_NSPoint=ff}{_NSSize=ff}}")
356
 
    def splitView_willDrawDividerInRect_betweenView_andView_withProposedRect_(self, sender, dividerRect, leading, trailing, imageRect):
357
 
        if self.splitter.view.window().isMainWindow():
358
 
            self.normalColor.set()
359
 
        else:
360
 
            self.disabledColor.set()
361
 
        NSRectFill(dividerRect)
362
 
        return NSZeroRect
363
 
 
364
 
    @signature("i@:@i@@i")
365
 
    def splitView_shouldResizeWindowForDivider_betweenView_andView_willGrow_(self, sender, divider, leading, trailing, grow):
366
 
        return (NSApp().currentEvent().modifierFlags() & NSAlternateKeyMask != 0)
367
 
 
368
 
class MiroSplitSubview(RBSplitSubview):
369
 
    def isFlipped(self):
370
 
        return YES
371
 
    
372
 
class Splitter(Container):
373
 
    """See https://develop.participatoryculture.org/trac/democracy/wiki/WidgetAPI for a description of the API for this class."""
374
 
    def __init__(self):
375
 
        Container.__init__(self)
376
 
 
377
 
        self.view = RBSplitView.alloc().initWithFrame_(NSRect((0,0), (800,600)))
378
 
        self.view.setVertical_(YES)
379
 
 
380
 
        self.delegate = SplitterDelegate.alloc().initWithSplitter_(self)
381
 
        self.view.setDelegate_(self.delegate)
382
 
 
383
 
        self.left = None
384
 
        self.left_view = MiroSplitSubview.alloc().init()
385
 
        self.left_view.setIdentifier_('left')
386
 
        self.view.addSubview_atPosition_(self.left_view, 0)
387
 
 
388
 
        self.right = None
389
 
        self.right_view = MiroSplitSubview.alloc().init()
390
 
        self.right_view.setIdentifier_('right')
391
 
        self.view.addSubview_atPosition_(self.right_view, 1)
392
 
        
393
 
        divider = NSImage.alloc().initWithSize_(NSSize(1.0, 1.0))
394
 
        divider.lockFocus()
395
 
        NSColor.clearColor().set()
396
 
        NSRectFill(NSRect((0.0, 0.0), (1.0, 1.0)))
397
 
        divider.unlockFocus()
398
 
        divider.setFlipped_(YES)
399
 
        self.view.setDivider_(divider)
400
 
 
401
 
    def get_children(self):
402
 
        children = []
403
 
        if self.left:
404
 
            children.append(self.left)
405
 
        if self.right:
406
 
            children.append(self.right)
407
 
        return children
408
 
 
409
 
    def calc_size_request(self):
410
 
        width = 1
411
 
        height = 0
412
 
        for child in self.get_children():
413
 
            child_width, child_height = child.get_size_request()
414
 
            width += child_width
415
 
            height = max(height, child_height)
416
 
        return width, height
417
 
 
418
 
    def place_left_children(self):
419
 
        if self.left is not None:
420
 
            self.left.place(self.left_view.bounds(), self.left_view)
421
 
 
422
 
    def place_right_children(self):
423
 
        if self.right is not None:
424
 
            self.right.place(self.right_view.bounds(), self.right_view)
425
 
 
426
 
    def place_children(self):
427
 
        self.place_left_children()
428
 
        self.place_right_children()
429
 
 
430
 
    def set_left(self, widget):
431
 
        """Set the left child widget."""
432
 
        old_left = self.left
433
 
        self.left = widget
434
 
        self.set_min_left_width()
435
 
        self.child_changed(old_left, self.left)
436
 
 
437
 
    def set_right(self, widget):
438
 
        """Set the right child widget.  """
439
 
        old_right = self.right
440
 
        self.right = widget
441
 
        self.set_min_right_width()
442
 
        self.child_changed(old_right, self.right)
443
 
 
444
 
    def set_left_width(self, width):
445
 
        if width == 0:
446
 
            self.left_view.setHidden_(YES)
447
 
        else:
448
 
            self.left_view.setHidden_(NO)
449
 
            self.left_view.setDimension_(width)
450
 
            self.place_children()
451
 
 
452
 
    def get_left_width(self):
453
 
        return self.left_view.frame().size.width
454
 
 
455
 
    def set_right_width(self, width):
456
 
        self.right_view.setDimension_(width)
457
 
        self.place_children()
458
 
 
459
 
    def set_min_left_width(self):
460
 
        min_width, _ = self.left.get_size_request()
461
 
        self.left_view.setMinDimension_andMaxDimension_(min_width, 600)
462
 
 
463
 
    def set_min_right_width(self):
464
 
        min_width, _ = self.right.get_size_request()
465
 
        self.right_view.setMinDimension_andMaxDimension_(min_width, 4000)
466
 
 
467
 
    def remove_left(self):
468
 
        """Remove the left child widget."""
469
 
        old_left = self.left
470
 
        self.left = None
471
 
        self.child_removed(old_left)
472
 
 
473
 
    def remove_right(self):
474
 
        """Remove the right child widget."""
475
 
        old_right = self.right
476
 
        self.right = None
477
 
        self.child_removed(old_right)
478
 
 
479
 
class _TablePacking(object):
480
 
    """Utility class to help with packing Table widgets."""
481
 
    def __init__(self, widget, column, row, column_span, row_span):
482
 
        self.widget = widget
483
 
        self.column = column
484
 
        self.row = row
485
 
        self.column_span = column_span
486
 
        self.row_span = row_span
487
 
 
488
 
    def column_indexes(self):
489
 
        return range(self.column, self.column + self.column_span)
490
 
 
491
 
    def row_indexes(self):
492
 
        return range(self.row, self.row + self.row_span)
493
 
 
494
 
class Table(Container):
495
 
    """See https://develop.participatoryculture.org/trac/democracy/wiki/WidgetAPI for a description of the API for this class."""
496
 
    CREATES_VIEW = False
497
 
 
498
 
    def __init__(self, columns, rows):
499
 
        Container.__init__(self)
500
 
        self._cells = Matrix(columns, rows)
501
 
        self._children = [] # List of _TablePacking objects
502
 
        self._children_sorted = True
503
 
        self.rows = rows
504
 
        self.columns = columns
505
 
        self.row_spacing = self.column_spacing = 0
506
 
 
507
 
    def _ensure_children_sorted(self):
508
 
        if not self._children_sorted:
509
 
            def cell_area(table_packing):
510
 
                return table_packing.column_span * table_packing.row_span
511
 
            self._children.sort(key=cell_area)
512
 
            self._children_sorted = True
513
 
 
514
 
    def get_children(self):
515
 
        return [cell.widget for cell in self._children]
516
 
    children = property(get_children)
517
 
 
518
 
    def calc_size_request(self):
519
 
        self._ensure_children_sorted()
520
 
        self._calc_dimensions()
521
 
        return self.total_width, self.total_height
522
 
 
523
 
    def _calc_dimensions(self):
524
 
        self.column_widths = [0] * self.columns
525
 
        self.row_heights = [0] * self.rows
526
 
 
527
 
        for tp in self._children:
528
 
            child_width, child_height = tp.widget.get_size_request()
529
 
            # recalc the width of the child's columns
530
 
            self._recalc_dimension(child_width, self.column_widths,
531
 
                    tp.column_indexes())
532
 
            # recalc the height of the child's rows
533
 
            self._recalc_dimension(child_height, self.row_heights,
534
 
                    tp.row_indexes())
535
 
 
536
 
        self.total_width = (self.column_spacing * (self.columns - 1) +
537
 
                sum(self.column_widths))
538
 
        self.total_height = (self.row_spacing * (self.rows - 1) +
539
 
                sum(self.row_heights))
540
 
 
541
 
    def _recalc_dimension(self, child_size, size_array, positions):
542
 
        current_size = sum(size_array[p] for p in positions)
543
 
        child_size_needed = child_size - current_size
544
 
        if child_size_needed > 0:
545
 
            iter = _extra_space_iter(child_size_needed, len(positions))
546
 
            for p in positions:
547
 
                size_array[p] += iter.next()
548
 
 
549
 
    def place_children(self):
550
 
        column_positions = [0]
551
 
        for width in self.column_widths[:-1]:
552
 
            column_positions.append(width + column_positions[-1])
553
 
        row_positions = [0]
554
 
        for height in self.row_heights[:-1]:
555
 
            row_positions.append(height + row_positions[-1])
556
 
 
557
 
        my_x= self.viewport.placement.origin.x
558
 
        my_y = self.viewport.placement.origin.y
559
 
        for tp in self._children:
560
 
            x = my_x + column_positions[tp.column]
561
 
            y = my_y + row_positions[tp.row]
562
 
            width = sum(self.column_widths[i] for i in tp.column_indexes())
563
 
            height = sum(self.row_heights[i] for i in tp.row_indexes())
564
 
            rect = NSMakeRect(x, y, width, height)
565
 
            tp.widget.place(rect, self.viewport.view)
566
 
 
567
 
    def pack(self, widget, column, row, column_span=1, row_span=1):
568
 
        tp = _TablePacking(widget, column, row, column_span, row_span)
569
 
        for c in tp.column_indexes():
570
 
            for r in tp.row_indexes():
571
 
                if self._cells[c, r]:
572
 
                    raise ValueError("Cell %d x %d is already taken" % (c, r))
573
 
        self._cells[column, row] = widget
574
 
        self._children.append(tp)
575
 
        self._children_sorted = False
576
 
        self.child_added(widget)
577
 
 
578
 
    def remove(self, child):
579
 
        for i in xrange(len(self._children)):
580
 
            if self._children[i].widget is child:
581
 
                self._children.remove(i)
582
 
                break
583
 
        else:
584
 
            raise ValueError("%s is not a child of this Table" % child)
585
 
        self._cells.remove(child)
586
 
        self.child_removed(widget)
587
 
 
588
 
    def set_column_spacing(self, spacing):
589
 
        self.column_spacing = spacing
590
 
        self.invalidate_size_request()
591
 
 
592
 
    def set_row_spacing(self, spacing):
593
 
        self.row_spacing = spacing
594
 
        self.invalidate_size_request()
595
 
 
596
 
    def enable(self, row=None, column=None):
597
 
        Container.enable(self)
598
 
        if row != None and column != None:
599
 
            if self._cells[column, row]:
600
 
                self._cells[column, row].enable()
601
 
        elif row != None:
602
 
            for mem in self._cells.row(row):
603
 
                if mem: mem.enable()
604
 
        elif column != None:
605
 
            for mem in self._cells.column(column):
606
 
                if mem: mem.enable()
607
 
        else:
608
 
            for mem in self._cells:
609
 
                if mem: mem.enable()
610
 
 
611
 
    def disable(self, row=None, column=None):
612
 
        Container.disable(self)
613
 
        if row != None and column != None:
614
 
            if self._cells[column, row]: 
615
 
                self._cells[column, row].disable()
616
 
        elif row != None:
617
 
            for mem in self._cells.row(row):
618
 
                if mem: mem.disable()
619
 
        elif column != None:
620
 
            for mem in self._cells.column(column):
621
 
                if mem: mem.disable()
622
 
        else:
623
 
            for mem in self._cells:
624
 
                if mem: mem.disable()
625
 
 
626
 
class Scroller(Bin):
627
 
    """See https://develop.participatoryculture.org/trac/democracy/wiki/WidgetAPI for a description of the API for this class."""
628
 
    def __init__(self, horizontal, vertical):
629
 
        Bin.__init__(self)
630
 
        self.view = NSScrollView.alloc().init()
631
 
        self.view.setAutohidesScrollers_(YES)
632
 
        self.view.setHasHorizontalScroller_(horizontal)
633
 
        self.view.setHasVerticalScroller_(vertical)
634
 
        self.document_view = FlippedView.alloc().init()
635
 
        self.view.setDocumentView_(self.document_view)
636
 
        self.view.setAutohidesScrollers_(True)
637
 
 
638
 
    def set_has_borders(self, has_border):
639
 
        self.view.setBorderType_(NSBezelBorder)
640
 
 
641
 
    def set_background_color(self, color):
642
 
        self.view.setBackgroundColor_(self.make_color(color))
643
 
 
644
 
    def add(self, child):
645
 
        child.parent_is_scroller = True
646
 
        Bin.add(self, child)
647
 
 
648
 
    def remove(self):
649
 
        child.parent_is_scroller = False
650
 
        Bin.remove(self)
651
 
 
652
 
    def calc_size_request(self):
653
 
        if self.child:
654
 
            width = height = 0
655
 
            if not self.view.hasHorizontalScroller():
656
 
                width = self.child.get_size_request()[0]
657
 
            if not self.view.hasVerticalScroller():
658
 
                height = self.child.get_size_request()[1]
659
 
            # Add a little room for the scrollbars
660
 
            if self.view.hasHorizontalScroller():
661
 
                height += 10
662
 
            if self.view.hasVerticalScroller():
663
 
                width += 10
664
 
            return width, height
665
 
        else:
666
 
            return 0, 0
667
 
 
668
 
    def place_children(self):
669
 
        if self.child is not None:
670
 
            child_width, child_height = self.child.get_size_request()
671
 
            child_width = max(child_width, self.view.contentView().frame().size.width)
672
 
            frame = NSRect(NSPoint(0,0), NSSize(child_width, child_height))
673
 
            if isinstance(self.child, tableview.TableView) and self.child.is_showing_headers():
674
 
                # Hack to allow the content of a table view to scroll, but not
675
 
                # the headers
676
 
                self.child.place(frame, self.document_view)
677
 
                self.view.setDocumentView_(self.child.tableview)
678
 
            else:
679
 
                self.child.place(frame, self.document_view)
680
 
            self.document_view.setFrame_(frame)
681
 
            self.document_view.setNeedsDisplay_(YES)
682
 
        self.view.setNeedsDisplay_(YES)
683
 
 
684
 
class ExpanderView(FlippedView):
685
 
    def init(self):
686
 
        self = super(ExpanderView, self).init()
687
 
        self.label_rect = None
688
 
        self.content_view = None
689
 
        self.button = NSButton.alloc().init()
690
 
        self.button.setState_(NSOffState)
691
 
        self.button.setTitle_("")
692
 
        self.button.setBezelStyle_(NSDisclosureBezelStyle)
693
 
        self.button.setButtonType_(NSPushOnPushOffButton)
694
 
        self.button.sizeToFit()
695
 
        self.addSubview_(self.button)
696
 
        self.button.setTarget_(self)
697
 
        self.button.setAction_('buttonChanged:')
698
 
        self.content_view = FlippedView.alloc().init()
699
 
        return self
700
 
 
701
 
    def buttonChanged_(self, button):
702
 
        if button.state() == NSOnState:
703
 
            self.addSubview_(self.content_view)
704
 
        else:
705
 
            self.content_view.removeFromSuperview()
706
 
        if self.window():
707
 
            wrappermap.wrapper(self).invalidate_size_request()
708
 
 
709
 
    def mouseDown_(self, event):
710
 
        pass  # Just need to respond to the selector so we get mouseUp_
711
 
 
712
 
    def mouseUp_(self, event):
713
 
        position = event.locationInWindow()
714
 
        window_label_rect = self.convertRect_toView_(self.label_rect, None)
715
 
        if NSPointInRect(position, window_label_rect):
716
 
            self.button.setNextState()
717
 
            self.buttonChanged_(self.button)
718
 
 
719
 
class Expander(Bin):
720
 
    BUTTON_PAD_TOP = 2
721
 
    BUTTON_PAD_LEFT = 4
722
 
    LABEL_SPACING = 4
723
 
 
724
 
    def __init__(self, child):
725
 
        Bin.__init__(self)
726
 
        if child:
727
 
            self.add(child)
728
 
        self.label = None
729
 
        self.spacing = 0
730
 
        self.view = ExpanderView.alloc().init()
731
 
        self.button = self.view.button
732
 
        self.button.setFrameOrigin_(NSPoint(self.BUTTON_PAD_LEFT,
733
 
            self.BUTTON_PAD_TOP))
734
 
        self.content_view = self.view.content_view
735
 
 
736
 
    def remove_viewport(self):
737
 
        Bin.remove_viewport(self)
738
 
        if self.label is not None:
739
 
            self.label.remove_viewport()
740
 
 
741
 
    def set_spacing(self, spacing):
742
 
        self.spacing = spacing
743
 
 
744
 
    def set_label(self, widget):
745
 
        if self.label is not None:
746
 
            self.label.remove_viewport()
747
 
        self.label = widget
748
 
        self.children_changed()
749
 
 
750
 
    def set_expanded(self, expanded):
751
 
        if expanded:
752
 
            self.button.setState_(NSOnState)
753
 
        else:
754
 
            self.button.setState_(NSOffState)
755
 
        self.view.buttonChanged_(self.button)
756
 
 
757
 
    def calc_top_size(self):
758
 
        width = self.button.bounds().size.width
759
 
        height = self.button.bounds().size.height
760
 
        if self.label is not None:
761
 
            label_width, label_height = self.label.get_size_request()
762
 
            width += self.LABEL_SPACING + label_width
763
 
            height = max(height, label_height)
764
 
        width += self.BUTTON_PAD_LEFT
765
 
        height += self.BUTTON_PAD_TOP
766
 
        return width, height
767
 
 
768
 
    def calc_size_request(self):
769
 
        width, height = self.calc_top_size()
770
 
        if self.child is not None and self.button.state() == NSOnState:
771
 
            child_width, child_height = self.child.get_size_request()
772
 
            width = max(width, child_width)
773
 
            height += self.spacing + child_height
774
 
        return width, height
775
 
 
776
 
    def place_children(self):
777
 
        top_width, top_height = self.calc_top_size()
778
 
        if self.label:
779
 
            label_width, label_height = self.label.get_size_request()
780
 
            button_width = self.button.bounds().size.width
781
 
            label_x = self.BUTTON_PAD_LEFT + button_width + self.LABEL_SPACING
782
 
            label_rect = NSMakeRect(label_x, self.BUTTON_PAD_TOP, 
783
 
                    label_width, label_height)
784
 
            self.label.place(label_rect, self.viewport.view)
785
 
            self.view.label_rect = label_rect
786
 
        if self.child:
787
 
            size = self.viewport.area().size
788
 
            child_rect = NSMakeRect(0, 0, size.width, size.height -
789
 
                    top_height)
790
 
            self.content_view.setFrame_(NSMakeRect(0, top_height, size.width,
791
 
                size.height - top_height))
792
 
            self.child.place(child_rect, self.content_view)
793
 
 
794
 
 
795
 
class TabViewDelegate(NSObject):
796
 
    def tabView_willSelectTabViewItem_(self, tab_view, tab_view_item):
797
 
        try:
798
 
            wrapper = wrappermap.wrapper(tab_view)
799
 
        except KeyError:
800
 
            pass # The NSTabView hasn't been placed yet, don't worry about it.
801
 
        else:
802
 
            wrapper.place_child_with_item(tab_view_item)
803
 
 
804
 
class TabContainer(Container):
805
 
    def __init__(self):
806
 
        Container.__init__(self)
807
 
        self.children = []
808
 
        self.item_to_child = {}
809
 
        self.view = NSTabView.alloc().init()
810
 
        self.view.setAllowsTruncatedLabels_(NO)
811
 
        self.delegate = TabViewDelegate.alloc().init()
812
 
        self.view.setDelegate_(self.delegate)
813
 
 
814
 
    def append_tab(self, child_widget, label, image):
815
 
        item = NSTabViewItem.alloc().init()
816
 
        item.setLabel_(label)
817
 
        item.setView_(FlippedView.alloc().init())
818
 
        self.view.addTabViewItem_(item)
819
 
        self.children.append(child_widget)
820
 
        self.child_added(child_widget)
821
 
        self.item_to_child[item] = child_widget
822
 
 
823
 
    def select_tab(self, index):
824
 
        self.view.selectTabViewItemAtIndex_(index)
825
 
 
826
 
    def place_children(self):
827
 
        self.place_child_with_item(self.view.selectedTabViewItem())
828
 
 
829
 
    def place_child_with_item(self, tab_view_item):
830
 
        child = self.item_to_child[tab_view_item]
831
 
        child_view = tab_view_item.view()
832
 
        content_rect =self.view.contentRect()
833
 
        child_view.setFrame_(content_rect)
834
 
        child.place(child_view.bounds(), child_view)
835
 
 
836
 
    def calc_size_request(self):
837
 
        tab_size = self.view.minimumSize()
838
 
        # make sure there's enough room for the tabs, plus a little extra
839
 
        # space to make things look good
840
 
        max_width = tab_size.width + 60
841
 
        max_height = 0
842
 
        for child in self.children:
843
 
            width, height = child.get_size_request()
844
 
            max_width = max(width, max_width)
845
 
            max_height = max(height, max_height)
846
 
        max_height += tab_size.height
847
 
 
848
 
        return max_width, max_height