aboutsummaryrefslogtreecommitdiffstats
path: root/lvc/widgets/osx/layout.py
diff options
context:
space:
mode:
Diffstat (limited to 'lvc/widgets/osx/layout.py')
-rw-r--r--lvc/widgets/osx/layout.py748
1 files changed, 748 insertions, 0 deletions
diff --git a/lvc/widgets/osx/layout.py b/lvc/widgets/osx/layout.py
new file mode 100644
index 0000000..f18a47f
--- /dev/null
+++ b/lvc/widgets/osx/layout.py
@@ -0,0 +1,748 @@
+# @Base: Miro - an RSS based video player application
+# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011
+# Participatory Culture Foundation
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+#
+# In addition, as a special exception, the copyright holders give
+# permission to link the code of portions of this program with the OpenSSL
+# library.
+#
+# You must obey the GNU General Public License in all respects for all of
+# the code used other than OpenSSL. If you modify file(s) with this
+# exception, you may extend this exception to your version of the file(s),
+# but you are not obligated to do so. If you do not wish to do so, delete
+# this exception statement from your version. If you delete this exception
+# statement from all source files in the program, then also delete it here.
+
+""".layout -- Widgets that handle laying out other
+widgets.
+
+We basically follow GTK's packing model. Widgets are packed into vboxes,
+hboxes or other container widgets. The child widgets request a minimum size,
+and the container widgets allocate space for their children. Widgets may get
+more size then they requested in which case they have to deal with it. In
+rare cases, widgets may get less size then they requested in which case they
+should just make sure they don't throw an exception or segfault.
+
+Check out the GTK tutorial for more info.
+"""
+
+import itertools
+
+from AppKit import *
+from Foundation import *
+from objc import YES, NO, nil, signature, loadBundle
+
+import tableview
+import wrappermap
+from .base import Container, Bin, FlippedView
+from lvc.utils import Matrix
+
+# These don't seem to be in pyobjc's AppKit (yet)
+NSScrollerKnobStyleDefault = 0
+NSScrollerKnobStyleDark = 1
+NSScrollerKnobStyleLight = 2
+
+NSScrollerStyleLegacy = 0
+NSScrollerStyleOverlay = 1
+
+def _extra_space_iter(extra_length, count):
+ """Utility function to allocate extra space left over in containers."""
+ if count == 0:
+ return
+ extra_space, leftover = divmod(extra_length, count)
+ while leftover >= 1:
+ yield extra_space + 1
+ leftover -= 1
+ yield extra_space + leftover
+ while True:
+ yield extra_space
+
+class BoxPacking:
+ """Utility class to store how we are packing a single widget."""
+
+ def __init__(self, widget, expand, padding):
+ self.widget = widget
+ self.expand = expand
+ self.padding = padding
+
+class Box(Container):
+ """Base class for HBox and VBox. """
+ CREATES_VIEW = False
+
+ def __init__(self, spacing=0):
+ self.spacing = spacing
+ Container.__init__(self)
+ self.packing_start = []
+ self.packing_end = []
+ self.expand_count = 0
+
+ def packing_both(self):
+ return itertools.chain(self.packing_start, self.packing_end)
+
+ def get_children(self):
+ for packing in self.packing_both():
+ yield packing.widget
+ children = property(get_children)
+
+ # Internally Boxes use a (length, breadth) coordinate system. length and
+ # breadth will be either x or y depending on which way the box is
+ # oriented. The subclasses must provide methods to translate between the
+ # 2 coordinate systems.
+
+ def translate_size(self, size):
+ """Translate a (width, height) tulple to (length, breadth)."""
+ raise NotImplementedError()
+
+ def untranslate_size(self, size):
+ """Reverse the work of translate_size."""
+ raise NotImplementedError()
+
+ def make_child_rect(self, position, length):
+ """Create a rect to position a child with."""
+ raise NotImplementedError()
+
+ def pack_start(self, child, expand=False, padding=0):
+ self.packing_start.append(BoxPacking(child, expand, padding))
+ if expand:
+ self.expand_count += 1
+ self.child_added(child)
+
+ def pack_end(self, child, expand=False, padding=0):
+ self.packing_end.append(BoxPacking(child, expand, padding))
+ if expand:
+ self.expand_count += 1
+ self.child_added(child)
+
+ def _remove_from_packing(self, child):
+ for i in xrange(len(self.packing_start)):
+ if self.packing_start[i].widget is child:
+ return self.packing_start.pop(i)
+ for i in xrange(len(self.packing_end)):
+ if self.packing_end[i].widget is child:
+ return self.packing_end.pop(i)
+ raise LookupError("%s not found" % child)
+
+ def remove(self, child):
+ packing = self._remove_from_packing(child)
+ if packing.expand:
+ self.expand_count -= 1
+ self.child_removed(child)
+
+ def translate_widget_size(self, widget):
+ return self.translate_size(widget.get_size_request())
+
+ def calc_size_request(self):
+ length = breadth = 0
+ for packing in self.packing_both():
+ child_length, child_breadth = \
+ self.translate_widget_size(packing.widget)
+ length += child_length
+ if packing.padding:
+ length += packing.padding * 2 # Need to pad on both sides
+ breadth = max(breadth, child_breadth)
+ spaces = max(0, len(self.packing_start) + len(self.packing_end) - 1)
+ length += spaces * self.spacing
+ return self.untranslate_size((length, breadth))
+
+ def place_children(self):
+ request_length, request_breadth = self.translate_widget_size(self)
+ ps = self.viewport.placement.size
+ total_length, dummy = self.translate_size((ps.width, ps.height))
+ total_extra_space = total_length - request_length
+ extra_space_iter = _extra_space_iter(total_extra_space,
+ self.expand_count)
+ start_end = self._place_packing_list(self.packing_start,
+ extra_space_iter, 0)
+ if self.expand_count == 0 and total_extra_space > 0:
+ # account for empty space after the end of pack_start list and
+ # before the pack_end list.
+ self.draw_empty_space(start_end, total_extra_space)
+ start_end += total_extra_space
+ self._place_packing_list(reversed(self.packing_end), extra_space_iter,
+ start_end)
+
+ def draw_empty_space(self, start, length):
+ empty_rect = self.make_child_rect(start, length)
+ my_view = self.viewport.view
+ opaque_view = my_view.opaqueAncestor()
+ if opaque_view is not None:
+ empty_rect2 = opaque_view.convertRect_fromView_(empty_rect, my_view)
+ opaque_view.setNeedsDisplayInRect_(empty_rect2)
+
+ def _place_packing_list(self, packing_list, extra_space_iter, position):
+ for packing in packing_list:
+ child_length, child_breadth = \
+ self.translate_widget_size(packing.widget)
+ if packing.expand:
+ child_length += extra_space_iter.next()
+ if packing.padding: # space before
+ self.draw_empty_space(position, packing.padding)
+ position += packing.padding
+ child_rect = self.make_child_rect(position, child_length)
+ if packing.padding: # space after
+ self.draw_empty_space(position, packing.padding)
+ position += packing.padding
+ packing.widget.place(child_rect, self.viewport.view)
+ position += child_length
+ if self.spacing > 0:
+ self.draw_empty_space(position, self.spacing)
+ position += self.spacing
+ return position
+
+ def enable(self):
+ Container.enable(self)
+ for mem in self.children:
+ mem.enable()
+
+ def disable(self):
+ Container.disable(self)
+ for mem in self.children:
+ mem.disable()
+
+class VBox(Box):
+ """See https://develop.participatoryculture.org/index.php/WidgetAPI for a description of the API for this class."""
+ def translate_size(self, size):
+ return (size[1], size[0])
+
+ def untranslate_size(self, size):
+ return (size[1], size[0])
+
+ def make_child_rect(self, position, length):
+ placement = self.viewport.placement
+ return NSMakeRect(placement.origin.x, placement.origin.y + position,
+ placement.size.width, length)
+
+class HBox(Box):
+ """See https://develop.participatoryculture.org/index.php/WidgetAPI for a description of the API for this class."""
+ def translate_size(self, size):
+ return (size[0], size[1])
+
+ def untranslate_size(self, size):
+ return (size[0], size[1])
+
+ def make_child_rect(self, position, length):
+ placement = self.viewport.placement
+ return NSMakeRect(placement.origin.x + position, placement.origin.y,
+ length, placement.size.height)
+
+class Alignment(Bin):
+ """See https://develop.participatoryculture.org/index.php/WidgetAPI for a description of the API for this class."""
+ CREATES_VIEW = False
+
+ def __init__(self, xalign=0.0, yalign=0.0, xscale=0.0, yscale=0.0,
+ top_pad=0, bottom_pad=0, left_pad=0, right_pad=0):
+ Bin.__init__(self)
+ self.xalign = xalign
+ self.yalign = yalign
+ self.xscale = xscale
+ self.yscale = yscale
+ self.top_pad = top_pad
+ self.bottom_pad = bottom_pad
+ self.left_pad = left_pad
+ self.right_pad = right_pad
+ if self.child is not None:
+ self.place_children()
+
+ def set(self, xalign=0.0, yalign=0.0, xscale=0.0, yscale=0.0):
+ self.xalign = xalign
+ self.yalign = yalign
+ self.xscale = xscale
+ self.yscale = yscale
+ if self.child is not None:
+ self.place_children()
+
+ def set_padding(self, top_pad=0, bottom_pad=0, left_pad=0, right_pad=0):
+ self.top_pad = top_pad
+ self.bottom_pad = bottom_pad
+ self.left_pad = left_pad
+ self.right_pad = right_pad
+ if self.child is not None and self.viewport is not None:
+ self.place_children()
+
+ def vertical_pad(self):
+ return self.top_pad + self.bottom_pad
+
+ def horizontal_pad(self):
+ return self.left_pad + self.right_pad
+
+ def calc_size_request(self):
+ if self.child:
+ child_width, child_height = self.child.get_size_request()
+ return (child_width + self.horizontal_pad(),
+ child_height + self.vertical_pad())
+ else:
+ return (0, 0)
+
+ def calc_size(self, requested, total, scale):
+ extra_width = max(0, total - requested)
+ return requested + int(round(extra_width * scale))
+
+ def calc_position(self, size, total, align):
+ return int(round((total - size) * align))
+
+ def place_children(self):
+ if self.child is None:
+ return
+
+ total_width = self.viewport.placement.size.width
+ total_height = self.viewport.placement.size.height
+ total_width -= self.horizontal_pad()
+ total_height -= self.vertical_pad()
+ request_width, request_height = self.child.get_size_request()
+
+ child_width = self.calc_size(request_width, total_width, self.xscale)
+ child_height = self.calc_size(request_height, total_height, self.yscale)
+ child_x = self.calc_position(child_width, total_width, self.xalign)
+ child_y = self.calc_position(child_height, total_height, self.yalign)
+ child_x += self.left_pad
+ child_y += self.top_pad
+
+ my_origin = self.viewport.area().origin
+ child_rect = NSMakeRect(my_origin.x + child_x, my_origin.y + child_y, child_width, child_height)
+ self.child.place(child_rect, self.viewport.view)
+ # Make sure the space not taken up by our child is redrawn.
+ self.viewport.queue_redraw()
+
+class DetachedWindowHolder(Alignment):
+ def __init__(self):
+ Alignment.__init__(self, bottom_pad=16, xscale=1.0, yscale=1.0)
+
+class _TablePacking(object):
+ """Utility class to help with packing Table widgets."""
+ def __init__(self, widget, column, row, column_span, row_span):
+ self.widget = widget
+ self.column = column
+ self.row = row
+ self.column_span = column_span
+ self.row_span = row_span
+
+ def column_indexes(self):
+ return range(self.column, self.column + self.column_span)
+
+ def row_indexes(self):
+ return range(self.row, self.row + self.row_span)
+
+class Table(Container):
+ """See https://develop.participatoryculture.org/index.php/WidgetAPI for a description of the API for this class."""
+ CREATES_VIEW = False
+
+ def __init__(self, columns, rows):
+ Container.__init__(self)
+ self._cells = Matrix(columns, rows)
+ self._children = [] # List of _TablePacking objects
+ self._children_sorted = True
+ self.rows = rows
+ self.columns = columns
+ self.row_spacing = self.column_spacing = 0
+
+ def _ensure_children_sorted(self):
+ if not self._children_sorted:
+ def cell_area(table_packing):
+ return table_packing.column_span * table_packing.row_span
+ self._children.sort(key=cell_area)
+ self._children_sorted = True
+
+ def get_children(self):
+ return [cell.widget for cell in self._children]
+ children = property(get_children)
+
+ def calc_size_request(self):
+ self._ensure_children_sorted()
+ self._calc_dimensions()
+ return self.total_width, self.total_height
+
+ def _calc_dimensions(self):
+ self.column_widths = [0] * self.columns
+ self.row_heights = [0] * self.rows
+
+ for tp in self._children:
+ child_width, child_height = tp.widget.get_size_request()
+ # recalc the width of the child's columns
+ self._recalc_dimension(child_width, self.column_widths,
+ tp.column_indexes())
+ # recalc the height of the child's rows
+ self._recalc_dimension(child_height, self.row_heights,
+ tp.row_indexes())
+
+ self.total_width = (self.column_spacing * (self.columns - 1) +
+ sum(self.column_widths))
+ self.total_height = (self.row_spacing * (self.rows - 1) +
+ sum(self.row_heights))
+
+ def _recalc_dimension(self, child_size, size_array, positions):
+ current_size = sum(size_array[p] for p in positions)
+ child_size_needed = child_size - current_size
+ if child_size_needed > 0:
+ iter = _extra_space_iter(child_size_needed, len(positions))
+ for p in positions:
+ size_array[p] += iter.next()
+
+ def place_children(self):
+ # This method depepnds on us calling _calc_dimensions() in
+ # calc_size_request(). Ensure that this happens.
+ if self.cached_size_request is None:
+ self.get_size_request()
+ column_positions = [0]
+ for width in self.column_widths[:-1]:
+ column_positions.append(width + column_positions[-1] + self.column_spacing)
+ row_positions = [0]
+ for height in self.row_heights[:-1]:
+ row_positions.append(height + row_positions[-1] + self.row_spacing)
+
+ my_x= self.viewport.placement.origin.x
+ my_y = self.viewport.placement.origin.y
+ for tp in self._children:
+ x = my_x + column_positions[tp.column]
+ y = my_y + row_positions[tp.row]
+ width = sum(self.column_widths[i] for i in tp.column_indexes())
+ height = sum(self.row_heights[i] for i in tp.row_indexes())
+ rect = NSMakeRect(x, y, width, height)
+ tp.widget.place(rect, self.viewport.view)
+
+ def pack(self, widget, column, row, column_span=1, row_span=1):
+ tp = _TablePacking(widget, column, row, column_span, row_span)
+ for c in tp.column_indexes():
+ for r in tp.row_indexes():
+ if self._cells[c, r]:
+ raise ValueError("Cell %d x %d is already taken" % (c, r))
+ self._cells[column, row] = widget
+ self._children.append(tp)
+ self._children_sorted = False
+ self.child_added(widget)
+
+ def remove(self, child):
+ for i in xrange(len(self._children)):
+ if self._children[i].widget is child:
+ self._children.remove(i)
+ break
+ else:
+ raise ValueError("%s is not a child of this Table" % child)
+ self._cells.remove(child)
+ self.child_removed(widget)
+
+ def set_column_spacing(self, spacing):
+ self.column_spacing = spacing
+ self.invalidate_size_request()
+
+ def set_row_spacing(self, spacing):
+ self.row_spacing = spacing
+ self.invalidate_size_request()
+
+ def enable(self, row=None, column=None):
+ Container.enable(self)
+ if row != None and column != None:
+ if self._cells[column, row]:
+ self._cells[column, row].enable()
+ elif row != None:
+ for mem in self._cells.row(row):
+ if mem: mem.enable()
+ elif column != None:
+ for mem in self._cells.column(column):
+ if mem: mem.enable()
+ else:
+ for mem in self._cells:
+ if mem: mem.enable()
+
+ def disable(self, row=None, column=None):
+ Container.disable(self)
+ if row != None and column != None:
+ if self._cells[column, row]:
+ self._cells[column, row].disable()
+ elif row != None:
+ for mem in self._cells.row(row):
+ if mem: mem.disable()
+ elif column != None:
+ for mem in self._cells.column(column):
+ if mem: mem.disable()
+ else:
+ for mem in self._cells:
+ if mem: mem.disable()
+
+class MiroScrollView(NSScrollView):
+ def tile(self):
+ NSScrollView.tile(self)
+ # tile is called when we need to layout our child view and scrollers.
+ # This probably means that we've either hidden or shown a scrollbar so
+ # call invalidate_size_request to ensure that things get re-layed out
+ # correctly. (#see 13842)
+ wrapper = wrappermap.wrapper(self)
+ if wrapper is not None:
+ wrapper.invalidate_size_request()
+
+class Scroller(Bin):
+ """See https://develop.participatoryculture.org/index.php/WidgetAPI for a description of the API for this class."""
+ def __init__(self, horizontal, vertical):
+ Bin.__init__(self)
+ self.view = MiroScrollView.alloc().init()
+ self.view.setAutohidesScrollers_(YES)
+ self.view.setHasHorizontalScroller_(horizontal)
+ self.view.setHasVerticalScroller_(vertical)
+ self.document_view = FlippedView.alloc().init()
+ self.view.setDocumentView_(self.document_view)
+
+ def prepare_for_dark_content(self):
+ try:
+ self.view.setScrollerKnobStyle_(NSScrollerKnobStyleLight)
+ except AttributeError:
+ # This only works on 10.7 and abvoe
+ pass
+
+ def set_has_borders(self, has_border):
+ self.view.setBorderType_(NSBezelBorder)
+
+ def viewport_repositioned(self):
+ # If the window is resized, this translates to a
+ # viewport_repositioned() event. Instead of calling
+ # place_children() one, which is what our suporclass does, we need
+ # some extra logic here. place the chilren to work out if we need a
+ # scrollbar, then get the new size, then replace the children (which
+ # now takes into account of scrollbar size.)
+ super(Scroller, self).viewport_repositioned()
+ self.cached_size_request = self.calc_size_request()
+ self.place_children()
+
+ def set_background_color(self, color):
+ self.view.setBackgroundColor_(self.make_color(color))
+
+ def add(self, child):
+ child.parent_is_scroller = True
+ Bin.add(self, child)
+
+ def remove(self):
+ child.parent_is_scroller = False
+ Bin.remove(self)
+
+ def children_changed(self):
+ # since our size isn't dependent on our children, don't call
+ # invalidate_size_request() here. Just call place_children() so that
+ # they get positioned correctly in the document view.
+ #
+ # XXX dodgy - why are we laying out the children twice? When the
+ # children change, the scroller could appear/disappear. But you have
+ # no idea if that's going to happen without knowing how big your
+ # children are. So we lay it out, get the size, then, place the
+ # children again. This makes sure that the right side of the children
+ # are redrawn. There's got to be a better way??
+ self.place_children()
+ self.cached_size_request = self.calc_size_request()
+ self.place_children()
+
+ def calc_size_request(self):
+ if self.child:
+ width = height = 0
+ try:
+ legacy = self.view.scrollerStyle() == NSScrollerStyleLegacy
+ except AttributeError:
+ legacy = True
+ if not self.view.hasHorizontalScroller():
+ width = self.child.get_size_request()[0]
+ if not self.view.hasVerticalScroller():
+ height = self.child.get_size_request()[1]
+ # Add a little room for the scrollbars (if necessary)
+ if legacy and self.view.hasHorizontalScroller():
+ height += NSScroller.scrollerWidth()
+ if legacy and self.view.hasVerticalScroller():
+ width += NSScroller.scrollerWidth()
+ return width, height
+ else:
+ return 0, 0
+
+ def place_children(self):
+ if self.child is not None:
+ scroll_view_size = self.view.contentView().frame().size
+ child_width, child_height = self.child.get_size_request()
+ child_width = max(child_width, scroll_view_size.width)
+ child_height = max(child_height, scroll_view_size.height)
+ frame = NSRect(NSPoint(0,0), NSSize(child_width, child_height))
+ if isinstance(self.child, tableview.TableView) and self.child.is_showing_headers():
+ # Hack to allow the content of a table view to scroll, but not
+ # the headers
+ self.child.place(frame, self.document_view)
+ if self.view.documentView() is not self.child.tableview:
+ self.view.setDocumentView_(self.child.tableview)
+ else:
+ self.child.place(frame, self.document_view)
+ self.document_view.setFrame_(frame)
+ self.document_view.setNeedsDisplay_(YES)
+ self.view.setNeedsDisplay_(YES)
+ self.child.emit('place-in-scroller')
+
+class ExpanderView(FlippedView):
+ def init(self):
+ self = super(ExpanderView, self).init()
+ self.label_rect = None
+ self.content_view = None
+ self.button = NSButton.alloc().init()
+ self.button.setState_(NSOffState)
+ self.button.setTitle_("")
+ self.button.setBezelStyle_(NSDisclosureBezelStyle)
+ self.button.setButtonType_(NSPushOnPushOffButton)
+ self.button.sizeToFit()
+ self.addSubview_(self.button)
+ self.button.setTarget_(self)
+ self.button.setAction_('buttonChanged:')
+ self.content_view = FlippedView.alloc().init()
+ return self
+
+ def buttonChanged_(self, button):
+ if button.state() == NSOnState:
+ self.addSubview_(self.content_view)
+ else:
+ self.content_view.removeFromSuperview()
+ if self.window():
+ wrappermap.wrapper(self).invalidate_size_request()
+
+ def mouseDown_(self, event):
+ pass # Just need to respond to the selector so we get mouseUp_
+
+ def mouseUp_(self, event):
+ position = event.locationInWindow()
+ window_label_rect = self.convertRect_toView_(self.label_rect, None)
+ if NSPointInRect(position, window_label_rect):
+ self.button.setNextState()
+ self.buttonChanged_(self.button)
+
+class Expander(Bin):
+ BUTTON_PAD_TOP = 2
+ BUTTON_PAD_LEFT = 4
+ LABEL_SPACING = 4
+
+ def __init__(self, child):
+ Bin.__init__(self)
+ if child:
+ self.add(child)
+ self.label = None
+ self.spacing = 0
+ self.view = ExpanderView.alloc().init()
+ self.button = self.view.button
+ self.button.setFrameOrigin_(NSPoint(self.BUTTON_PAD_LEFT,
+ self.BUTTON_PAD_TOP))
+ self.content_view = self.view.content_view
+
+ def remove_viewport(self):
+ Bin.remove_viewport(self)
+ if self.label is not None:
+ self.label.remove_viewport()
+
+ def set_spacing(self, spacing):
+ self.spacing = spacing
+
+ def set_label(self, widget):
+ if self.label is not None:
+ self.label.remove_viewport()
+ self.label = widget
+ self.children_changed()
+
+ def set_expanded(self, expanded):
+ if expanded:
+ self.button.setState_(NSOnState)
+ else:
+ self.button.setState_(NSOffState)
+ self.view.buttonChanged_(self.button)
+
+ def calc_top_size(self):
+ width = self.button.bounds().size.width
+ height = self.button.bounds().size.height
+ if self.label is not None:
+ label_width, label_height = self.label.get_size_request()
+ width += self.LABEL_SPACING + label_width
+ height = max(height, label_height)
+ width += self.BUTTON_PAD_LEFT
+ height += self.BUTTON_PAD_TOP
+ return width, height
+
+ def calc_size_request(self):
+ width, height = self.calc_top_size()
+ if self.child is not None and self.button.state() == NSOnState:
+ child_width, child_height = self.child.get_size_request()
+ width = max(width, child_width)
+ height += self.spacing + child_height
+ return width, height
+
+ def place_children(self):
+ top_width, top_height = self.calc_top_size()
+ if self.label:
+ label_width, label_height = self.label.get_size_request()
+ button_width = self.button.bounds().size.width
+ label_x = self.BUTTON_PAD_LEFT + button_width + self.LABEL_SPACING
+ label_rect = NSMakeRect(label_x, self.BUTTON_PAD_TOP,
+ label_width, label_height)
+ self.label.place(label_rect, self.viewport.view)
+ self.view.label_rect = label_rect
+ if self.child:
+ size = self.viewport.area().size
+ child_rect = NSMakeRect(0, 0, size.width, size.height -
+ top_height)
+ self.content_view.setFrame_(NSMakeRect(0, top_height, size.width,
+ size.height - top_height))
+ self.child.place(child_rect, self.content_view)
+
+
+class TabViewDelegate(NSObject):
+ def tabView_willSelectTabViewItem_(self, tab_view, tab_view_item):
+ try:
+ wrapper = wrappermap.wrapper(tab_view)
+ except KeyError:
+ pass # The NSTabView hasn't been placed yet, don't worry about it.
+ else:
+ wrapper.place_child_with_item(tab_view_item)
+
+class TabContainer(Container):
+ def __init__(self):
+ Container.__init__(self)
+ self.children = []
+ self.item_to_child = {}
+ self.view = NSTabView.alloc().init()
+ self.view.setAllowsTruncatedLabels_(NO)
+ self.delegate = TabViewDelegate.alloc().init()
+ self.view.setDelegate_(self.delegate)
+
+ def append_tab(self, child_widget, label, image):
+ item = NSTabViewItem.alloc().init()
+ item.setLabel_(label)
+ item.setView_(FlippedView.alloc().init())
+ self.view.addTabViewItem_(item)
+ self.children.append(child_widget)
+ self.child_added(child_widget)
+ self.item_to_child[item] = child_widget
+
+ def select_tab(self, index):
+ self.view.selectTabViewItemAtIndex_(index)
+
+ def place_children(self):
+ self.place_child_with_item(self.view.selectedTabViewItem())
+
+ def place_child_with_item(self, tab_view_item):
+ child = self.item_to_child[tab_view_item]
+ child_view = tab_view_item.view()
+ content_rect =self.view.contentRect()
+ child_view.setFrame_(content_rect)
+ child.place(child_view.bounds(), child_view)
+
+ def calc_size_request(self):
+ tab_size = self.view.minimumSize()
+ # make sure there's enough room for the tabs, plus a little extra
+ # space to make things look good
+ max_width = tab_size.width + 60
+ max_height = 0
+ for child in self.children:
+ width, height = child.get_size_request()
+ max_width = max(width, max_width)
+ max_height = max(height, max_height)
+ max_height += tab_size.height
+
+ return max_width, max_height