# @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. """.tableview -- TableView widget and it's associated classes. """ import math import logging from contextlib import contextmanager from collections import namedtuple from AppKit import * from Foundation import * from objc import YES, NO, nil from mvc import signals from mvc import errors from mvc.widgets import widgetconst from mvc.widgets.tableselection import SelectionOwnerMixin from mvc.widgets.tablescroll import ScrollbarOwnerMixin from .utils import filename_to_unicode import wrappermap import tablemodel import osxmenus from .base import Widget from .simple import Image from .drawing import DrawingContext, DrawingStyle, Gradient, ImageSurface from .helpers import NotificationForwarder from .layoutmanager import LayoutManager EXPANDER_PADDING = 6 HEADER_HEIGHT = 17 CUSTOM_HEADER_HEIGHT = 25 def iter_range(ns_range): """Iterate over an NSRange object""" return xrange(ns_range.location, ns_range.location + ns_range.length) Rect = namedtuple('Rect', 'x y width height') def NSRectToRect(nsrect): origin, size = nsrect.origin, nsrect.size return Rect(origin.x, origin.y, size.width, size.height) Point = namedtuple('Point', 'x y') def NSPointToPoint(nspoint): return Point(int(nspoint.x), int(nspoint.y)) class HotspotTracker(object): """Contains the info on the currently tracked hotspot. See: https://develop.participatoryculture.org/index.php/WidgetAPITableView """ def __init__(self, tableview, point): self.tableview = tableview self.row = tableview.rowAtPoint_(point) self.column = tableview.columnAtPoint_(point) if self.row == -1 or self.column == -1: self.hit = False return model = tableview.dataSource().model self.iter = model.iter_for_row(tableview, self.row) self.table_column = tableview.tableColumns()[self.column] self.cell = self.table_column.dataCell() self.update_position(point) if isinstance(self.cell, CustomTableCell): self.name = self.calc_hotspot() else: self.name = None self.hit = (self.name is not None) def is_for_context_menu(self): return self.name == '#show-context-menu' def calc_cell_hotspot(self, column, row): if (self.hit and self.column == column and self.row == row): return self.name else: return None def update_position(self, point): cell_frame = self.tableview.frameOfCellAtColumn_row_(self.column, self.row) self.pos = NSPoint(point.x - cell_frame.origin.x, point.y - cell_frame.origin.y) def update_hit(self): old_hit = self.hit self.hit = (self.calc_hotspot() == self.name) if old_hit != self.hit: self.redraw_cell() def set_cell_data(self): model = self.tableview.dataSource().model row = model[self.iter] value_dict = model.get_column_data(row, self.table_column) self.cell.setObjectValue_(value_dict) self.cell.set_wrapper_data() def calc_hotspot(self): self.set_cell_data() cell_frame = self.tableview.frameOfCellAtColumn_row_(self.column, self.row) style = self.cell.make_drawing_style(cell_frame, self.tableview) layout_manager = self.cell.layout_manager layout_manager.reset() return self.cell.wrapper.hotspot_test(style, layout_manager, self.pos.x, self.pos.y, cell_frame.size.width, cell_frame.size.height) def redraw_cell(self): # Check to see if we removed the table in response to a hotspot click. if self.tableview.superview() is not nil: cell_frame = self.tableview.frameOfCellAtColumn_row_(self.column, self.row) self.tableview.setNeedsDisplayInRect_(cell_frame) def _calc_interior_frame(total_frame, tableview): """Calculate the inner cell area for a table cell. We tell cocoa that the intercell spacing is (0, 0) and instead handle the spacing ourselves. This method calculates the area that a cell should render to, given the total spacing. """ return NSMakeRect(total_frame.origin.x + tableview.column_spacing // 2, total_frame.origin.y + tableview.row_spacing // 2, total_frame.size.width - tableview.column_spacing, total_frame.size.height - tableview.row_spacing) class MiroTableCell(NSTextFieldCell): def init(self): return super(MiroTableCell, self).initTextCell_('') def calcHeight_(self, view): font = self.font() return math.ceil(font.ascender() + abs(font.descender()) + font.leading()) def highlightColorWithFrame_inView_(self, frame, view): return nil def setObjectValue_(self, value_dict): if isinstance(value_dict, dict): NSCell.setObjectValue_(self, value_dict['value']) else: # OS X calls setObjectValue_('') on intialization NSCell.setObjectValue_(self, value_dict) def drawInteriorWithFrame_inView_(self, frame, view): return NSTextFieldCell.drawInteriorWithFrame_inView_(self, _calc_interior_frame(frame, view), view) class MiroTableInfoListTextCell(MiroTableCell): def initWithAttrGetter_(self, attr_getter): self = self.init() self.setWraps_(NO) self.attr_getter = attr_getter self._textColor = self.textColor() return self def drawWithFrame_inView_(self, frame, view): # adjust frame based on the cell spacing frame = _calc_interior_frame(frame, view) if (self.isHighlighted() and frame is not None and (view.isDescendantOf_(view.window().firstResponder()) or view.gradientHighlight) and view.window().isMainWindow()): self.setTextColor_(NSColor.whiteColor()) else: self.setTextColor_(self._textColor) return MiroTableCell.drawWithFrame_inView_(self, frame, view) def titleRectForBounds_(self, rect): frame = MiroTableCell.titleRectForBounds_(self, rect) text_size = self.attributedStringValue().size() frame.origin.y = rect.origin.y + (rect.size.height - text_size.height) / 2.0 return frame def drawInteriorWithFrame_inView_(self, frame, view): rect = self.titleRectForBounds_(frame) self.attributedStringValue().drawInRect_(rect) def setObjectValue_(self, value): if isinstance(value, tuple): info, attrs, group_info = value cell_text = self.attr_getter(info) NSCell.setObjectValue_(self, cell_text) else: # Getting set to a something other than a model row, usually this # happens in initialization NSCell.setObjectValue_(self, '') class MiroTableImageCell(NSImageCell): def calcHeight_(self, view): return self.value_dict['image'].size().height def highlightColorWithFrame_inView_(self, frame, view): return nil def setObjectValue_(self, value_dict): NSImageCell.setObjectValue_(self, value_dict['image']) def drawInteriorWithFrame_inView_(self, frame, view): return NSImageCell.drawInteriorWithFrame_inView_(self, _calc_interior_frame(frame, view), view) class MiroCheckboxCell(NSButtonCell): def init(self): self = super(MiroCheckboxCell, self).init() self.setButtonType_(NSSwitchButton) self.setTitle_('') return self def calcHeight_(self, view): return self.cellSize().height def highlightColorWithFrame_inView_(self, frame, view): return nil def setObjectValue_(self, value_dict): if isinstance(value_dict, dict): NSButtonCell.setObjectValue_(self, value_dict['value']) else: # OS X calls setObjectValue_('') on intialization NSCell.setObjectValue_(self, value_dict) def startTrackingAt_inView_(self, point, view): return YES def continueTracking_at_inView_(self, lastPoint, at, view): return YES def stopTracking_at_inView_mouseIsUp_(self, lastPoint, at, tableview, mouseIsUp): if mouseIsUp: column = tableview.columnAtPoint_(at) row = tableview.rowAtPoint_(at) if column != -1 and row != -1: wrapper = wrappermap.wrapper(tableview) column = wrapper.columns[column] itr = wrapper.model.iter_for_row(tableview, row) column.renderer.emit('clicked', itr) return NSButtonCell.stopTracking_at_inView_mouseIsUp_(self, lastPoint, at, tableview, mouseIsUp) def drawInteriorWithFrame_inView_(self, frame, view): return NSButtonCell.drawInteriorWithFrame_inView_(self, _calc_interior_frame(frame, view), view) class CellRendererBase(object): DRAW_BACKGROUND = True def set_index(self, index): self.index = index def get_index(self): return self.index class CellRenderer(CellRendererBase): def __init__(self): self.cell = self.build_cell() self._font_scale_factor = 1.0 self._font_bold = False self.set_align('left') def build_cell(self): return MiroTableCell.alloc().init() def setDataCell_(self, column): column.setDataCell_(self.cell) def set_text_size(self, size): if size == widgetconst.SIZE_NORMAL: self._font_scale_factor = 1.0 elif size == widgetconst.SIZE_SMALL: # make the scale factor such so that the font size is 11.0 self._font_scale_factor = 11.0 / NSFont.systemFontSize() else: raise ValueError("Unknown size: %s" % size) self._set_font() def set_font_scale(self, scale_factor): self._font_scale_factor = scale_factor self._set_font() def set_bold(self, bold): self._font_bold = bold self._set_font() def _set_font(self): size = NSFont.systemFontSize() * self._font_scale_factor if self._font_bold: font = NSFont.boldSystemFontOfSize_(size) else: font = NSFont.systemFontOfSize_(size) self.cell.setFont_(font) def set_color(self, color): color = NSColor.colorWithDeviceRed_green_blue_alpha_(color[0], color[1], color[2], 1.0) self.cell._textColor = color self.cell.setTextColor_(color) def set_align(self, align): if align == 'left': ns_alignment = NSLeftTextAlignment elif align == 'center': ns_alignment = NSCenterTextAlignment elif align == 'right': ns_alignment = NSRightTextAlignment else: raise ValueError("unknown alignment: %s", align) self.cell.setAlignment_(ns_alignment) class ImageCellRenderer(CellRendererBase): def setDataCell_(self, column): column.setDataCell_(MiroTableImageCell.alloc().init()) class CheckboxCellRenderer(CellRendererBase, signals.SignalEmitter): def __init__(self): signals.SignalEmitter.__init__(self, 'clicked') self.size = widgetconst.SIZE_NORMAL def set_control_size(self, size): self.size = size def setDataCell_(self, column): cell = MiroCheckboxCell.alloc().init() if self.size == widgetconst.SIZE_SMALL: cell.setControlSize_(NSSmallControlSize) column.setDataCell_(cell) class CustomTableCell(NSCell): def init(self): self = super(CustomTableCell, self).init() self.layout_manager = LayoutManager() self.hotspot = None self.default_drawing_style = DrawingStyle() return self def highlightColorWithFrame_inView_(self, frame, view): return nil def calcHeight_(self, view): self.layout_manager.reset() self.set_wrapper_data() cell_size = self.wrapper.get_size(self.default_drawing_style, self.layout_manager) return cell_size[1] def make_drawing_style(self, frame, view): text_color = None if (self.isHighlighted() and frame is not None and (view.isDescendantOf_(view.window().firstResponder()) or view.gradientHighlight) and view.window().isMainWindow()): text_color = NSColor.whiteColor() return DrawingStyle(text_color=text_color) def drawInteriorWithFrame_inView_(self, frame, view): NSGraphicsContext.currentContext().saveGraphicsState() if not self.wrapper.IGNORE_PADDING: # adjust frame based on the cell spacing. We also have to adjust # the hover position to account for the new frame original_frame = frame frame = _calc_interior_frame(frame, view) hover_adjustment = (frame.origin.x - original_frame.origin.x, frame.origin.y - original_frame.origin.y) else: hover_adjustment = (0, 0) if self.wrapper.outline_column: pad_left = EXPANDER_PADDING else: pad_left = 0 drawing_rect = NSMakeRect(frame.origin.x + pad_left, frame.origin.y, frame.size.width - pad_left, frame.size.height) context = DrawingContext(view, drawing_rect, drawing_rect) context.style = self.make_drawing_style(frame, view) self.layout_manager.reset() self.set_wrapper_data() column = self.wrapper.get_index() hover_pos = view.get_hover(self.row, column) if hover_pos is not None: hover_pos = [hover_pos[0] - hover_adjustment[0], hover_pos[1] - hover_adjustment[1]] self.wrapper.render(context, self.layout_manager, self.isHighlighted(), self.hotspot, hover_pos) NSGraphicsContext.currentContext().restoreGraphicsState() def setObjectValue_(self, value): self.object_value = value def set_wrapper_data(self): self.wrapper.__dict__.update(self.object_value) class CustomCellRenderer(CellRendererBase): CellClass = CustomTableCell IGNORE_PADDING = False def __init__(self): self.outline_column = False self.index = None def setDataCell_(self, column): # Note that the ownership is the opposite of what happens in widgets. # The NSObject owns it's wrapper widget. This happens for a couple # reasons: # 1) The data cell gets copied a bunch of times, so wrappermap won't # work with it. # 2) The Wrapper should only needs to stay around as long as the # NSCell that it's wrapping is around. Once the column gets removed # from the table, the wrapper can be deleted. nscell = self.CellClass.alloc().init() nscell.wrapper = self column.setDataCell_(nscell) def hotspot_test(self, style, layout, x, y, width, height): return None class InfoListTableCell(CustomTableCell): def set_wrapper_data(self): self.wrapper.info, self.wrapper.attrs, self.wrapper.group_info = \ self.object_value class InfoListRenderer(CustomCellRenderer): CellClass = InfoListTableCell def hotspot_test(self, style, layout, x, y, width, height): return None class InfoListRendererText(CellRenderer): def build_cell(self): cell = MiroTableInfoListTextCell.alloc() return cell.initWithAttrGetter_(self.get_value) def calc_row_height(view, model_row): max_height = 0 model = view.dataSource().model for column in view.tableColumns(): cell = column.dataCell() data = model.get_column_data(model_row, column) cell.setObjectValue_(data) cell_height = cell.calcHeight_(view) max_height = max(max_height, cell_height) if max_height == 0: max_height = 12 return max_height + view.row_spacing class TableViewDelegate(NSObject): def tableView_willDisplayCell_forTableColumn_row_(self, view, cell, column, row): column = view.column_index_map[column] cell.column = column cell.row = row if view.hotspot_tracker: cell.hotspot = view.hotspot_tracker.calc_cell_hotspot(column, row) else: cell.hotspot = None def tableView_didClickTableColumn_(self, tableview, column): wrapper = wrappermap.wrapper(tableview) for column_wrapper in wrapper.columns: if column_wrapper._column is column: column_wrapper.emit('clicked') def tableView_toolTipForCell_rect_tableColumn_row_mouseLocation_(self, tableview, cell, rect, column, row, location): wrapper = wrappermap.wrapper(tableview) iter = tableview.dataSource().model.iter_for_row(tableview, row) for wrapper_column in wrapper.columns: if wrapper_column._column is column: break return (wrapper.get_tooltip(iter, wrapper_column), rect) class VariableHeightTableViewDelegate(TableViewDelegate): def tableView_heightOfRow_(self, table_view, row): model = table_view.dataSource().model iter = model.iter_for_row(table_view, row) if iter is None: return 12 return calc_row_height(table_view, model[iter]) # TableViewCommon is a hack to do a Mixin class. We want the same behaviour # for our table views and our outline views. Normally we would use a Mixin, # but that doesn't work with pyobjc. Instead we define the common code in # TableViewCommon, then copy it into MiroTableView and MiroOutlineView class TableViewCommon(object): def init(self): self = super(self.__class__, self).init() self.hotspot_tracker = None self._tracking_rects = [] self.hover_info = None self.column_index_map = {} self.setFocusRingType_(NSFocusRingTypeNone) self.handled_last_mouse_down = False self.gradientHighlight = False self.tracking_area = None self.group_lines_enabled = False self.group_line_width = 1 self.group_line_color = (0, 0, 0, 1.0) # we handle cell spacing manually self.setIntercellSpacing_(NSSize(0, 0)) self.column_spacing = 3 self.row_spacing = 2 return self def updateTrackingAreas(self): # remove existing tracking area if needed if self.tracking_area: self.removeTrackingArea_(self.tracking_area) # create a new tracking area for the entire view. This allows us to # get mouseMoved events whenever the mouse is inside our view. self.tracking_area = NSTrackingArea.alloc() self.tracking_area.initWithRect_options_owner_userInfo_( self.visibleRect(), NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | NSTrackingActiveInKeyWindow, self, nil) self.addTrackingArea_(self.tracking_area) def addTableColumn_(self, column): index = len(self.tableColumns()) column.set_index(index) self.column_index_map[column._column] = index self.SuperClass.addTableColumn_(self, column._column) def removeTableColumn_(self, column): self.SuperClass.removeTableColumn_(self, column) removed = self.column_index_map.pop(column) for key, index in self.column_index_map.items(): if index > removed: self.column_index_map[key] -= 1 def moveColumn_toColumn_(self, src, dest): # Need to switch the TableColumn objects too columns = wrappermap.wrapper(self).columns columns[src], columns[dest] = columns[dest], columns[src] for index, column in enumerate(columns): column.set_index(index) self.SuperClass.moveColumn_toColumn_(self, src, dest) def highlightSelectionInClipRect_(self, rect): if wrappermap.wrapper(self).draws_selection: if not self.gradientHighlight: return self.SuperClass.highlightSelectionInClipRect_(self, rect) context = NSGraphicsContext.currentContext() focused = self.isDescendantOf_(self.window().firstResponder()) for row in tablemodel.list_from_nsindexset(self.selectedRowIndexes()): self.drawBackgroundGradient(context, focused, row) def setFrameSize_(self, size): if size.height == 0: size.height = 4 self.SuperClass.setFrameSize_(self, size) def drawBackgroundGradient(self, context, focused, row): widget = wrappermap.wrapper(self) window = widget.get_window() if window and window.is_active(): if focused: start_color = (0.588, 0.717, 0.843) end_color = (0.416, 0.568, 0.713) top_line_color = (0.416, 0.569, 0.714, 1.0) bottom_line_color = (0.416, 0.569, 0.714, 1.0) else: start_color = (168 / 255.0, 188 / 255.0, 208 / 255.0) end_color = (129 / 255.0, 152 / 255.0, 176 / 255.0) top_line_color = (129 / 255.0, 152 / 255.0, 175 / 255.0, 1.0) bottom_line_color = (0.416, 0.569, 0.714, 1.0) else: start_color = (0.675, 0.722, 0.765) end_color = (0.592, 0.659, 0.710) top_line_color = (0.596, 0.635, 0.671, 1.0) bottom_line_color = (0.522, 0.576, 0.620, 1.0) rect = self.rectOfRow_(row) top = NSMakeRect(rect.origin.x, rect.origin.y, rect.size.width, 1) context.saveGraphicsState() # draw the top line NSColor.colorWithDeviceRed_green_blue_alpha_(*top_line_color).set() NSRectFill(top) bottom = NSMakeRect(rect.origin.x, rect.origin.y + rect.size.height - 2, rect.size.width, 1) NSColor.colorWithDeviceRed_green_blue_alpha_(*bottom_line_color).set() NSRectFill(bottom) highlight = NSMakeRect(rect.origin.x, rect.origin.y + rect.size.height - 1, rect.size.width, 1) NSColor.colorWithDeviceRed_green_blue_alpha_(0.918, 0.925, 0.941, 1.0).set() NSRectFill(highlight) # draw the gradient rect.origin.y += 1 rect.size.height -= 3 NSRectClip(rect) gradient = Gradient(rect.origin.x, rect.origin.y, rect.origin.x, rect.origin.y + rect.size.height) gradient.set_start_color(start_color) gradient.set_end_color(end_color) gradient.draw() context.restoreGraphicsState() def drawBackgroundInClipRect_(self, clip_rect): # save our graphics state, since we are about to modify the clip path graphics_context = NSGraphicsContext.currentContext() graphics_context.saveGraphicsState() # create a NSBezierPath that contains the rects of the columns with # DRAW_BACKGROUND True. clip_path = NSBezierPath.bezierPath() number_of_columns = len(self.tableColumns()) for col_index in iter_range(self.columnsInRect_(clip_rect)): column = wrappermap.wrapper(self.tableColumns()[col_index]) column_rect = None if column.renderer.DRAW_BACKGROUND: # We should draw the background for this column, add it's rect # to our clip rect. column_rect = self.rectOfColumn_(col_index) clip_path.appendBezierPathWithRect_(column_rect) else: # We shouldn't draw the background for this column. Don't add # anything to the clip rect, but do draw the area before the # first row and after the last row. self.drawBackgroundOutsideContent_clipRect_(col_index, clip_rect) if col_index == number_of_columns - 1: # last column if not column_rect: column_rect = self.rectOfColumn_(col_index) column_right = column_rect.origin.x + column_rect.size.width clip_right = clip_rect.origin.x + clip_rect.size.width if column_right < clip_right: # there's space to the right, so add that to the clip_rect remaining = clip_right - column_right left_rect = NSMakeRect(column_right, clip_rect.origin.y, remaining, clip_rect.size.height) clip_path.appendBezierPathWithRect_(left_rect) # clip to that path clip_path.addClip() # do the default drawing self.SuperClass.drawBackgroundInClipRect_(self, clip_rect) # restore graphics state graphics_context.restoreGraphicsState() def drawBackgroundOutsideContent_clipRect_(self, index, clip_rect): """Draw our background outside the rows with content We call this for cells with DRAW_BACKGROUND set to False. For those, we let the cell draw their own background, but we still need to draw the background before the first cell and after the last cell. """ self.backgroundColor().set() total_rect = NSIntersectionRect(self.rectOfColumn_(index), clip_rect) if self.numberOfRows() == 0: # if no rows are selected, draw the background over everything NSRectFill(total_rect) return # fill the area above the first row first_row_rect = self.rectOfRow_(0) if first_row_rect.origin.y > total_rect.origin.y: height = first_row_rect.origin.y - total_rect.origin.y NSRectFill(NSMakeRect(total_rect.origin.x, total_rect.origin.y, total_rect.size.width, height)) # fill the area below the last row last_row_rect = self.rectOfRow_(self.numberOfRows()-1) if NSMaxY(last_row_rect) < NSMaxY(total_rect): y = NSMaxY(last_row_rect) + 1 height = NSMaxY(total_rect) - NSMaxY(last_row_rect) NSRectFill(NSMakeRect(total_rect.origin.x, y, total_rect.size.width, height)) def drawRow_clipRect_(self, row, clip_rect): self.SuperClass.drawRow_clipRect_(self, row, clip_rect) if self.group_lines_enabled: self.drawGroupLine_(row) def drawGroupLine_(self, row): infolist = wrappermap.wrapper(self).model if (not isinstance(infolist, tablemodel.InfoListModel) or infolist.get_grouping() is None): return info, attrs, group_info = infolist[row] if group_info[0] == group_info[1] - 1: rect = self.rectOfRow_(row) rect.origin.y = NSMaxY(rect) - self.group_line_width rect.size.height = self.group_line_width NSColor.colorWithDeviceRed_green_blue_alpha_( *self.group_line_color).set() NSRectFill(rect) def canDragRowsWithIndexes_atPoint_(self, indexes, point): return YES def draggingSourceOperationMaskForLocal_(self, local): drag_source = wrappermap.wrapper(self).drag_source if drag_source and local: return drag_source.allowed_actions() return NSDragOperationNone def mouseMoved_(self, event): location = self.convertPoint_fromView_(event.locationInWindow(), nil) row = self.rowAtPoint_(location) column = self.columnAtPoint_(location) if (self.hover_info is not None and self.hover_info != (row, column)): # left a cell, redraw it the old one rect = self.frameOfCellAtColumn_row_(self.hover_info[1], self.hover_info[0]) self.setNeedsDisplayInRect_(rect) if row == -1 or column == -1: # corner case: we got a mouseMoved_ event, but the pointer is # outside the view self.hover_pos = self.hover_info = None return # queue a redraw on the cell currently hovered over rect = self.frameOfCellAtColumn_row_(column, row) self.setNeedsDisplayInRect_(rect) # recalculate hover_pos and hover_info self.hover_pos = (location[0] - rect[0][0], location[0] - rect[0][1]) self.hover_info = (row, column) def mouseExited_(self, event): if self.hover_info: # mouse left our window, unset hover and redraw the cell that the # mouse was in rect = self.frameOfCellAtColumn_row_(self.hover_info[1], self.hover_info[0]) self.setNeedsDisplayInRect_(rect) self.hover_pos = self.hover_info = None def get_hover(self, row, column): if self.hover_info == (row, column): return self.hover_pos else: return None def mouseDown_(self, event): if event.modifierFlags() & NSControlKeyMask: self.handleContextMenu_(event) self.handled_last_mouse_down = True return point = self.convertPoint_fromView_(event.locationInWindow(), nil) if event.clickCount() == 2: if self.handled_last_mouse_down: return wrapper = wrappermap.wrapper(self) row = self.rowAtPoint_(point) if (row != -1 and self.point_should_click(point, row)): iter = wrapper.model.iter_for_row(self, row) wrapper.emit('row-activated', iter) return # Like clickCount() == 2 but keep running so we can get to run the # hotspot tracker et al. if event.clickCount() == 1: wrapper = wrappermap.wrapper(self) row = self.rowAtPoint_(point) if (row != -1 and self.point_should_click(point, row)): iter = wrapper.model.iter_for_row(self, row) wrapper.emit('row-clicked', iter) hotspot_tracker = HotspotTracker(self, point) if hotspot_tracker.hit: self.hotspot_tracker = hotspot_tracker self.hotspot_tracker.redraw_cell() self.handled_last_mouse_down = True if hotspot_tracker.is_for_context_menu(): self.popup_context_menu(self.hotspot_tracker.row, event) # once we're out of that call, we know the context menu is # gone hotspot_tracker.redraw_cell() self.hotspot_tracker = None else: self.handled_last_mouse_down = False self.SuperClass.mouseDown_(self, event) def point_should_click(self, point, row): """Should a click on a point result in a row-clicked signal? Subclasses can override if not every point should result in a click. """ return True def rightMouseDown_(self, event): self.handleContextMenu_(event) def handleContextMenu_(self, event): self.window().makeFirstResponder_(self) point = self.convertPoint_fromView_(event.locationInWindow(), nil) column = self.columnAtPoint_(point) row = self.rowAtPoint_(point) if self.group_lines_enabled and column == 0: self.selectAllItemsInGroupForRow_(row) self.popup_context_menu(row, event) def selectAllItemsInGroupForRow_(self, row): wrapper = wrappermap.wrapper(self) infolist = wrapper.model if (not isinstance(infolist, tablemodel.InfoListModel) or infolist.get_grouping() is None): return info, attrs, group_info = infolist[row] select_range = NSMakeRange(row - group_info[0], group_info[1]) index_set = NSIndexSet.indexSetWithIndexesInRange_(select_range) self.selectRowIndexes_byExtendingSelection_(index_set, NO) def popup_context_menu(self, row, event): selection = self.selectedRowIndexes() if row != -1 and not selection.containsIndex_(row): index_set = NSIndexSet.alloc().initWithIndex_(row) self.selectRowIndexes_byExtendingSelection_(index_set, NO) wrapper = wrappermap.wrapper(self) if wrapper.context_menu_callback is not None: menu_items = wrapper.context_menu_callback(wrapper) menu = osxmenus.make_context_menu(menu_items) NSMenu.popUpContextMenu_withEvent_forView_(menu, event, self) def mouseDragged_(self, event): if self.hotspot_tracker is not None: point = self.convertPoint_fromView_(event.locationInWindow(), nil) self.hotspot_tracker.update_position(point) self.hotspot_tracker.update_hit() else: self.SuperClass.mouseDragged_(self, event) def mouseUp_(self, event): if self.hotspot_tracker is not None: point = self.convertPoint_fromView_(event.locationInWindow(), nil) self.hotspot_tracker.update_position(point) self.hotspot_tracker.update_hit() if self.hotspot_tracker.hit: wrappermap.wrapper(self).send_hotspot_clicked() if self.hotspot_tracker: self.hotspot_tracker.redraw_cell() self.hotspot_tracker = None else: self.SuperClass.mouseUp_(self, event) def keyDown_(self, event): mods = osxmenus.translate_event_modifiers(event) if event.charactersIgnoringModifiers() == ' ' and len(mods) == 0: # handle spacebar with no modifiers by sending the row-activated # signal wrapper = wrappermap.wrapper(self) row = self.selectedRow() if row >= 0: iter = wrapper.model.iter_for_row(self, row) wrapper.emit('row-activated', iter) else: self.SuperClass.keyDown_(self, event) class TableColumn(signals.SignalEmitter): def __init__(self, title, renderer, header=None, **attrs): signals.SignalEmitter.__init__(self) self.create_signal('clicked') self._column = MiroTableColumn.alloc().initWithIdentifier_(title) self._column.set_attrs(attrs) wrappermap.add(self._column, self) header_cell = MiroTableHeaderCell.alloc().init() self.custom_header = False if header: header_cell.set_widget(header) self.custom_header = True self._column.setHeaderCell_(header_cell) self._column.headerCell().setStringValue_(title) self._column.setEditable_(NO) self._column.setResizingMask_(NSTableColumnNoResizing) self.renderer = renderer self.sort_order_ascending = True self.sort_indicator_visible = False self.do_horizontal_padding = True self.min_width = self.max_width = None renderer.setDataCell_(self._column) def set_do_horizontal_padding(self, horizontal_padding): self.do_horizontal_padding = horizontal_padding def set_right_aligned(self, right_aligned): if right_aligned: self._column.headerCell().setAlignment_(NSRightTextAlignment) else: self._column.headerCell().setAlignment_(NSLeftTextAlignment) def set_min_width(self, width): self.min_width = width def set_max_width(self, width): self.max_width = width def set_width(self, width): self._column.setWidth_(width) def get_width(self): return self._column.width() def set_resizable(self, resizable): mask = 0 if resizable: mask |= NSTableColumnUserResizingMask self._column.setResizingMask_(mask) def set_sort_indicator_visible(self, visible): self.sort_indicator_visible = visible self._column.tableView().headerView().setNeedsDisplay_(True) def get_sort_indicator_visible(self): return self.sort_indicator_visible def set_sort_order(self, ascending): self.sort_order_ascending = ascending self._column.tableView().headerView().setNeedsDisplay_(True) def get_sort_order_ascending(self): return self.sort_order_ascending def set_index(self, index): self.index = index self.renderer.set_index(index) class MiroTableColumn(NSTableColumn): def set_attrs(self, attrs): self._attrs = attrs def attrs(self): return self._attrs class MiroTableView(NSTableView): SuperClass = NSTableView for name, value in TableViewCommon.__dict__.items(): locals()[name] = value class MiroTableHeaderView(NSTableHeaderView): def initWithFrame_(self, frame): # frame is not used self = super(MiroTableHeaderView, self).initWithFrame_(frame) self.selected = None self.custom_header = False return self def drawRect_(self, rect): wrapper = wrappermap.wrapper(self.tableView()) if self.selected: self.selected.set_selected(False) for column in wrapper.columns: if column.sort_indicator_visible: self.selected = column._column.headerCell() self.selected.set_selected(True) self.selected.set_ascending(column.sort_order_ascending) break NSTableHeaderView.drawRect_(self, rect) if self.custom_header: NSGraphicsContext.currentContext().saveGraphicsState() # Draw the separator between the header and the contents. context = DrawingContext(self, rect, rect) context.set_line_width(1) context.set_color((2 / 255.0, 2 / 255.0, 2 / 255.0)) context.move_to(0, context.height - 0.5) context.rel_line_to(context.width, 0) context.stroke() NSGraphicsContext.currentContext().restoreGraphicsState() class MiroTableHeaderCell(NSTableHeaderCell): def init(self): self = super(MiroTableHeaderCell, self).init() self.layout_manager = LayoutManager() self.button = None return self def set_selected(self, selected): self.button._enabled = selected def set_ascending(self, ascending): self.button._ascending = ascending def set_widget(self, widget): self.button = widget def drawWithFrame_inView_(self, frame, view): if self.button is None: # use the default behavior when set_widget hasn't been called return NSTableHeaderCell.drawWithFrame_inView_(self, frame, view) NSGraphicsContext.currentContext().saveGraphicsState() drawing_rect = NSMakeRect(frame.origin.x, frame.origin.y, frame.size.width, frame.size.height) context = DrawingContext(view, drawing_rect, drawing_rect) context.style = self.make_drawing_style(frame, view) self.layout_manager.reset() columns = wrappermap.wrapper(view.tableView()).columns header_cells = [c._column.headerCell() for c in columns] background_only = not self in header_cells self.button.draw(context, self.layout_manager, background_only) NSGraphicsContext.currentContext().restoreGraphicsState() def make_drawing_style(self, frame, view): text_color = None if (self.isHighlighted() and frame is not None and (view.isDescendantOf_(view.window().firstResponder()) or view.gradientHighlight)): text_color = NSColor.whiteColor() return DrawingStyle(text_color=text_color) class CocoaSelectionOwnerMixin(SelectionOwnerMixin): """Cocoa-specific methods for selection management. This subclass should not define any behavior. Methods that cannot be completed in this widget state should raise WidgetActionError. """ def _set_allow_multiple_select(self, allow): self.tableview.setAllowsMultipleSelection_(allow) def _get_allow_multiple_select(self): return self.tableview.allowsMultipleSelection() def _get_selected_iters(self): selection = self.tableview.selectedRowIndexes() selrows = tablemodel.list_from_nsindexset(selection) return [self.model.iter_for_row(self.tableview, row) for row in selrows] def _get_selected_iter(self): row = self.tableview.selectedRow() if row == -1: return None return self.model.iter_for_row(self.tableview, row) def _get_selected_rows(self): return [self.model[i] for i in self._get_selected_iters()] @property def num_rows_selected(self): return self.tableview.numberOfSelectedRows() def _is_selected(self, iter_): row = self.row_of_iter(iter_) return self.tableview.isRowSelected_(row) def _select(self, iter_): row = self.row_of_iter(iter_) index_set = NSIndexSet.alloc().initWithIndex_(row) self.tableview.selectRowIndexes_byExtendingSelection_(index_set, YES) def _unselect(self, iter_): self.tableview.deselectRow_(self.row_of_iter(iter_)) def _unselect_all(self): self.tableview.deselectAll_(nil) def _iter_to_string(self, iter_): return unicode(self.model.row_of_iter(self.tableview, iter_)) def _iter_from_string(self, row): return self.model.iter_for_row(self.tableview, int(row)) class CocoaScrollbarOwnerMixin(ScrollbarOwnerMixin): """Manages a TableView's scroll position.""" def __init__(self): ScrollbarOwnerMixin.__init__(self, _work_around_17153=True) self.connect('place-in-scroller', self.on_place_in_scroller) self.scroll_position = (0, 0) self.clipview_notifications = None self._position_set = False def _set_scroll_position(self, scroll_to): """Restore a saved scroll position.""" self.scroll_position = scroll_to try: scroller = self._scroller except errors.WidgetNotReadyError: return self._position_set = True clipview = scroller.contentView() if not self.clipview_notifications: self.clipview_notifications = NotificationForwarder.create(clipview) # NOTE: intentional changes are BoundsChanged; bad changes are # FrameChanged clipview.setPostsFrameChangedNotifications_(YES) self.clipview_notifications.connect(self.on_scroll_changed, 'NSViewFrameDidChangeNotification') # NOTE: scrollPoint_ just scrolls the point into view; we want to # scroll the view so that the point becomes the origin size = self.tableview.visibleRect().size size = (size.width, size.height) rect = NSMakeRect(scroll_to[0], scroll_to[1], size[0], size[1]) self.tableview.scrollRectToVisible_(rect) def on_place_in_scroller(self, scroller): # workaround for 17153.1 if not self._position_set: self._set_scroll_position(self.scroll_position) @property def _manually_scrolled(self): """Return whether the view has been scrolled explicitly by the user since the last time it was set automatically. Ignores X coords. """ auto_y = self.scroll_position[1] real_y = self.get_scroll_position()[1] return abs(auto_y - real_y) > 5 def _get_item_area(self, iter_): rect = self.tableview.rectOfRow_(self.row_of_iter(iter_)) return NSRectToRect(rect) def _get_visible_area(self): return NSRectToRect(self._scroller.contentView().documentVisibleRect()) def _get_scroll_position(self): point = self._scroller.contentView().documentVisibleRect().origin return NSPointToPoint(point) def on_scroll_changed(self, notification): # we get this notification when the scroll position has been reset (when # it should not have been); put it back self.set_scroll_position(self.scroll_position) # this notification also serves as the Cocoa equivalent to # on_scroll_range_changed, which tells super when we may be ready to # scroll to an iter self.emit('scroll-range-changed') def set_scroller(self, scroller): """For GTK; Cocoa tableview knows its enclosingScrollView""" @property def _scroller(self): """Return an NSScrollView or raise WidgetNotReadyError""" scroller = self.tableview.enclosingScrollView() if not scroller: raise errors.WidgetNotReadyError('enclosingScrollView') return scroller class SorterPadding(NSView): # Why is this a Mac only widget? Because the wrappermap mechanism requires # us to layout the widgets (so that we may call back to the portable API # hooks of the widget. Since we only set the view component, this fake # widget is never placed so the wrappermap mechanism fails to work. # # So far, this is okay because only the Mac uses custom headers. def init(self): self = super(SorterPadding, self).init() image = Image(resources.path('images/headertoolbar.png')) self.image = ImageSurface(image) return self def isFlipped(self): return YES def drawRect_(self, rect): context = DrawingContext(self, self.bounds(), rect) context.style = DrawingStyle() self.image.draw(context, 0, 0, context.width, context.height) # XXX this color doesn't take into account enable/disabled state # of the sorting widgets. edge = 72.0 / 255 context.set_color((edge, edge, edge)) context.set_line_width(1) context.move_to(0.5, 0) context.rel_line_to(0, context.height) context.stroke() class TableView(CocoaSelectionOwnerMixin, CocoaScrollbarOwnerMixin, Widget): """Displays data as a tabular list. TableView follows the GTK TreeView widget fairly closely. """ CREATES_VIEW = False # Bit of a hack. We create several views. By setting CREATES_VIEW to # False, we get to position the views manually. draws_selection = True def __init__(self, model, custom_headers=False): Widget.__init__(self) CocoaSelectionOwnerMixin.__init__(self) CocoaScrollbarOwnerMixin.__init__(self) self.create_signal('hotspot-clicked') self.create_signal('row-clicked') self.create_signal('row-activated') self.create_signal('reallocate-columns') self.model = model self.columns = [] self.drag_source = self.drag_dest = None self.context_menu_callback = None self.tableview = MiroTableView.alloc().init() self.data_source = tablemodel.MiroTableViewDataSource.alloc() types = (tablemodel.MIRO_DND_ITEM_LOCAL,) self.tableview.registerForDraggedTypes_(types) self.view = self.tableview self.data_source.initWithModel_(self.model) self.tableview.setDataSource_(self.data_source) self.tableview.setVerticalMotionCanBeginDrag_(YES) self.set_columns_draggable(False) self.set_auto_resizes(False) self.row_height_set = False self.set_fixed_height(False) self.auto_resizing = False self.header_view = MiroTableHeaderView.alloc().initWithFrame_( NSMakeRect(0, 0, 0, 0)) self.tableview.setCornerView_(None) self.custom_header = False self.header_height = HEADER_HEIGHT self.set_show_headers(True) self.notifications = NotificationForwarder.create(self.tableview) self.model_signal_ids = [ self.model.connect_weak('row-changed', self.on_row_change), self.model.connect_weak('structure-will-change', self.on_model_structure_change), ] self.iters_to_update = [] self.height_changed = self.reload_needed = False self.old_selection = None self._resizing = False if custom_headers: self._enable_custom_headers() def unset_model(self): for signal_id in self.model_signal_ids: self.model.disconnect(signal_id) self.model = None self.tableview.setDataSource_(None) self.data_source = None def _enable_custom_headers(self): self.custom_header = True self.header_height = CUSTOM_HEADER_HEIGHT self.header_view.custom_header = True self.tableview.setCornerView_(SorterPadding.alloc().init()) def enable_album_view_focus_hack(self): # this only matters on GTK pass def focus(self): if self.tableview.window() is not None: self.tableview.window().makeFirstResponder_(self.tableview) def send_hotspot_clicked(self): tracker = self.tableview.hotspot_tracker self.emit('hotspot-clicked', tracker.name, tracker.iter) def on_row_change(self, model, iter): self.iters_to_update.append(iter) if not self.fixed_height: self.height_changed = True if self.tableview.hotspot_tracker is not None: self.tableview.hotspot_tracker.update_hit() def on_model_structure_change(self, model): self.will_need_reload() self.cancel_hotspot_track() def will_need_reload(self): if not self.reload_needed: self.reload_needed = True self.old_selection = self._get_selected_rows() def cancel_hotspot_track(self): if self.tableview.hotspot_tracker is not None: self.tableview.hotspot_tracker.redraw_cell() self.tableview.hotspot_tracker = None def on_expanded(self, notification): self.invalidate_size_request() item = notification.userInfo()['NSObject'] iter_ = self.model.iter_for_item[item] self.emit('row-expanded', iter_, self.model.get_path(iter_)) def on_collapsed(self, notification): self.invalidate_size_request() item = notification.userInfo()['NSObject'] iter_ = self.model.iter_for_item[item] self.emit('row-collapsed', iter_, self.model.get_path(iter_)) def on_column_resize(self, notification): if self.auto_resizing or self._resizing: return self._resizing = True try: column = notification.userInfo()['NSTableColumn'] label = column.headerCell().stringValue() self.emit('reallocate-columns', {label: column.width()}) finally: self._resizing = False def is_tree(self): return isinstance(self.model, tablemodel.TreeTableModel) def set_row_expanded(self, iter, expanded): """Expand or collapse the specified row. Succeeds or raises WidgetActionError. """ item = iter.value() if expanded: self.tableview.expandItem_(item) else: self.tableview.collapseItem_(item) if self.tableview.isItemExpanded_(item) != expanded: raise errors.WidgetActionError( "cannot expand iter. expandable: %r" % ( self.tableview.isExpandable_(item),)) self.invalidate_size_request() def is_row_expanded(self, iter): return self.tableview.isItemExpanded_(iter.value()) def calc_size_request(self): self.tableview.tile() height = self.tableview.frame().size.height if self._show_headers: height += self.header_height return self.calc_width(), height def viewport_repositioned(self): self._do_layout() def viewport_created(self): wrappermap.add(self.tableview, self) self._do_layout() self._add_views() self.notifications.connect(self.on_selection_changed, 'NSTableViewSelectionDidChangeNotification') self.notifications.connect(self.on_column_resize, 'NSTableViewColumnDidResizeNotification') # scroll has been unset self._position_set = False def remove_viewport(self): if self.viewport is not None: self._remove_views() wrappermap.remove(self.tableview) self.notifications.disconnect() self.viewport = None if self.clipview_notifications: self.clipview_notifications.disconnect() self.clipview_notifications = None def _should_place_header_view(self): return self._show_headers and not self.parent_is_scroller def _add_views(self): self.viewport.view.addSubview_(self.tableview) if self._should_place_header_view(): self.viewport.view.addSubview_(self.header_view) def _remove_views(self): self.tableview.removeFromSuperview() self.header_view.removeFromSuperview() def _do_layout(self): x = self.viewport.placement.origin.x y = self.viewport.placement.origin.y width = self.viewport.get_width() height = self.viewport.get_height() if self._should_place_header_view(): self.header_view.setFrame_(NSMakeRect(x, y, width, self.header_height)) self.tableview.setFrame_(NSMakeRect(x, y + self.header_height, width, height - self.header_height)) else: self.header_view.setFrame_(NSMakeRect(x, y, width, self.header_height)) self.tableview.setFrame_(NSMakeRect(x, y, width, height)) if self.auto_resize: self.auto_resizing = True # ListView sizes itself in do_size_allocated; # this is necessary for tablist and StandardView columns = self.tableview.tableColumns() if len(columns) == 1: columns[0].setWidth_(self.viewport.area().size.width) self.auto_resizing = False self.queue_redraw() def calc_width(self): if self.column_count() == 0: return 0 width = 0 columns = self.tableview.tableColumns() if self.auto_resize: # Table auto-resizes, we can shrink to min-width for each column width = sum(column.minWidth() for column in columns) width += self.tableview.column_spacing * self.column_count() else: # Table doesn't auto-resize, the columns can't get smaller than # their current width width = sum(column.width() for column in columns) return width def start_bulk_change(self): # stop our model from emitting signals, which is slow if we're # adding/removing/changing a bunch of rows. Instead, just reload the # model afterwards. self.will_need_reload() self.cancel_hotspot_track() self.model.freeze_signals() def model_changed(self): if not self.row_height_set and self.fixed_height: self.try_to_set_row_height() self.model.thaw_signals() size_changed = False if self.reload_needed: self.tableview.reloadData() new_selection = self._get_selected_rows() if new_selection != self.old_selection: self.on_selection_changed(self.tableview) self.old_selection = None size_changed = True elif self.iters_to_update: if self.fixed_height or not self.height_changed: # our rows don't change height, just update cell areas if self.is_tree(): for iter in self.iters_to_update: self.tableview.reloadItem_(iter.value()) else: for iter in self.iters_to_update: row = self.row_of_iter(iter) rect = self.tableview.rectOfRow_(row) self.tableview.setNeedsDisplayInRect_(rect) else: # our rows can change height inform Cocoa that their heights # might have changed (this will redraw them) index_set = NSMutableIndexSet.alloc().init() for iter in self.iters_to_update: try: index_set.addIndex_(self.row_of_iter(iter)) except LookupError: # This happens when the iter's parent is unexpanded, # just ignore. pass self.tableview.noteHeightOfRowsWithIndexesChanged_(index_set) size_changed = True else: return if size_changed: self.invalidate_size_request() self.height_changed = self.reload_needed = False self.iters_to_update = [] def width_for_columns(self, width): """If the table is width pixels big, how much width is available for the table's columns. """ # XXX this used to do some calculation with the spacing of each column, # but it doesn't appear like we need it to be that complicated anymore # (see #18273) return width - 2 def set_column_spacing(self, column_spacing): self.tableview.column_spacing = column_spacing def set_row_spacing(self, row_spacing): self.tableview.row_spacing = row_spacing def set_alternate_row_backgrounds(self, setting): self.tableview.setUsesAlternatingRowBackgroundColors_(setting) def set_grid_lines(self, horizontal, vertical): mask = 0 if horizontal: mask |= NSTableViewSolidHorizontalGridLineMask if vertical: mask |= NSTableViewSolidVerticalGridLineMask self.tableview.setGridStyleMask_(mask) def set_gradient_highlight(self, setting): self.tableview.gradientHighlight = setting def set_group_lines_enabled(self, enabled): self.tableview.group_lines_enabled = enabled self.queue_redraw() def set_group_line_style(self, color, width): self.tableview.group_line_color = color + (1.0,) self.tableview.group_line_width = width self.queue_redraw() def get_tooltip(self, iter, column): return None def add_column(self, column): if not self.custom_header == column.custom_header: raise ValueError('Column header does not match type ' 'required by TableView') self.columns.append(column) self.tableview.addTableColumn_(column) self._set_min_max_column_widths(column) # Adding a column means that each row could have a different height. # call noteNumberOfRowsChanged() to have OS X recalculate the heights self.tableview.noteNumberOfRowsChanged() self.invalidate_size_request() self.try_to_set_row_height() def _set_min_max_column_widths(self, column): if column.do_horizontal_padding: spacing = self.tableview.column_spacing else: spacing = 0 if column.min_width > 0: column._column.setMinWidth_(column.min_width + spacing) if column.max_width > 0: column._column.setMaxWidth_(column.max_width + spacing) def column_count(self): return len(self.tableview.tableColumns()) def remove_column(self, index): column = self.columns.pop(index) self.tableview.removeTableColumn_(column._column) self.invalidate_size_request() def get_columns(self): titles = [] columns = self.tableview.tableColumns() for column in columns: titles.append(column.headerCell().stringValue()) return titles def set_background_color(self, (red, green, blue)): color = NSColor.colorWithDeviceRed_green_blue_alpha_(red, green, blue, 1.0) self.tableview.setBackgroundColor_(color) def set_show_headers(self, show): self._show_headers = show if show: self.tableview.setHeaderView_(self.header_view) else: self.tableview.setHeaderView_(None) if self.viewport is not None: self._remove_views() self._do_layout() self._add_views() self.invalidate_size_request() self.queue_redraw() def is_showing_headers(self): return self._show_headers def set_search_column(self, model_index): pass def try_to_set_row_height(self): if len(self.model) > 0: first_iter = self.model.first_iter() height = calc_row_height(self.tableview, self.model[first_iter]) self.tableview.setRowHeight_(height) self.row_height_set = True def set_auto_resizes(self, setting): self.auto_resize = setting def set_columns_draggable(self, dragable): self.tableview.setAllowsColumnReordering_(dragable) def set_fixed_height(self, fixed): if fixed: self.fixed_height = True delegate_class = TableViewDelegate self.row_height_set = False self.try_to_set_row_height() else: self.fixed_height = False delegate_class = VariableHeightTableViewDelegate self.delegate = delegate_class.alloc().init() self.tableview.setDelegate_(self.delegate) self.tableview.reloadData() def row_of_iter(self, iter): return self.model.row_of_iter(self.tableview, iter) def set_context_menu_callback(self, callback): self.context_menu_callback = callback # disable the drag when the cells are constantly updating. Mac OS X # deals badly with this.. def set_volatile(self, volatile): if volatile: self.data_source.setDragSource_(None) self.data_source.setDragDest_(None) else: self.data_source.setDragSource_(self.drag_source) self.data_source.setDragDest_(self.drag_dest) def set_drag_source(self, drag_source): self.drag_source = drag_source self.data_source.setDragSource_(drag_source) def set_drag_dest(self, drag_dest): self.drag_dest = drag_dest if drag_dest is None: self.data_source.setDragDest_(None) else: types = drag_dest.allowed_types() self.data_source.setDragDest_(drag_dest)