diff options
Diffstat (limited to 'lvc/widgets/osx/tableview.py')
-rw-r--r-- | lvc/widgets/osx/tableview.py | 1629 |
1 files changed, 1629 insertions, 0 deletions
diff --git a/lvc/widgets/osx/tableview.py b/lvc/widgets/osx/tableview.py new file mode 100644 index 0000000..9f490d2 --- /dev/null +++ b/lvc/widgets/osx/tableview.py @@ -0,0 +1,1629 @@ +# @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 lvc import signals +from lvc import errors +from lvc.widgets import widgetconst +from lvc.widgets.tableselection import SelectionOwnerMixin +from lvc.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) |