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

« back to all changes in this revision

Viewing changes to osx/plat/frontends/widgets/tableview.py

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

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Miro - an RSS based video player application
 
2
# Copyright (C) 2005-2010 Participatory Culture Foundation
 
3
#
 
4
# This program is free software; you can redistribute it and/or modify
 
5
# it under the terms of the GNU General Public License as published by
 
6
# the Free Software Foundation; either version 2 of the License, or
 
7
# (at your option) any later version.
 
8
#
 
9
# This program is distributed in the hope that it will be useful,
 
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
12
# GNU General Public License for more details.
 
13
#
 
14
# You should have received a copy of the GNU General Public License
 
15
# along with this program; if not, write to the Free Software
 
16
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
 
17
#
 
18
# In addition, as a special exception, the copyright holders give
 
19
# permission to link the code of portions of this program with the OpenSSL
 
20
# library.
 
21
#
 
22
# You must obey the GNU General Public License in all respects for all of
 
23
# the code used other than OpenSSL. If you modify file(s) with this
 
24
# exception, you may extend this exception to your version of the file(s),
 
25
# but you are not obligated to do so. If you do not wish to do so, delete
 
26
# this exception statement from your version. If you delete this exception
 
27
# statement from all source files in the program, then also delete it here.
 
28
 
 
29
"""miro.plat.frontends.widgets.tableview -- TableView widget and it's
 
30
associated classes.
 
31
"""
 
32
 
 
33
import math
 
34
 
 
35
from AppKit import *
 
36
from Foundation import *
 
37
from objc import YES, NO, nil
 
38
 
 
39
from miro import signals
 
40
from miro.frontends.widgets import widgetconst
 
41
from miro.plat.frontends.widgets import osxmenus
 
42
from miro.plat.frontends.widgets import wrappermap
 
43
from miro.plat.frontends.widgets import tablemodel
 
44
from miro.plat.frontends.widgets.base import Widget, FlippedView
 
45
from miro.plat.frontends.widgets.drawing import DrawingContext, DrawingStyle, Gradient
 
46
from miro.plat.frontends.widgets.helpers import NotificationForwarder
 
47
from miro.plat.frontends.widgets.layoutmanager import LayoutManager
 
48
 
 
49
 
 
50
# Disclosure button used as a reference in get_left_offset()
 
51
_disclosure_button = NSButton.alloc().init()
 
52
_disclosure_button.setButtonType_(NSOnOffButton)
 
53
_disclosure_button.setBezelStyle_(NSDisclosureBezelStyle)
 
54
_disclosure_button.sizeToFit()
 
55
_disclosure_button_width = _disclosure_button.frame().size.width 
 
56
 
 
57
EXPANDER_PADDING = 6
 
58
HEADER_HEIGHT = 17
 
59
 
 
60
def _pack_row_column(row, column):
 
61
    """Convert a row, column pair into a integer suitable for passing to
 
62
    NSView.addTrackingRect_owner_userData_assumeInside_.
 
63
    """
 
64
    if column > (1 << 16):
 
65
        raise ValueError("column value too big: ", column)
 
66
    return (row << 16) + column
 
67
 
 
68
def _unpack_row_column(value):
 
69
    """Reverse the work of _pack_row_column()."""
 
70
    row = value >> 16
 
71
    column = value & ((1 << 16) - 1)
 
72
    return row, column
 
73
 
 
74
class HotspotTracker(object):
 
75
    """Contains the info on the currently tracked hotspot.  See:
 
76
    https://develop.participatoryculture.org/index.php/WidgetAPITableView
 
77
    """
 
78
    def __init__(self, tableview, point):
 
79
        self.tableview = tableview
 
80
        self.row = tableview.rowAtPoint_(point)
 
81
        self.column = tableview.columnAtPoint_(point)
 
82
        if self.row == -1 or self.column == -1:
 
83
            self.hit = False
 
84
            return
 
85
        model = tableview.dataSource().model
 
86
        self.iter = model.iter_for_row(tableview, self.row)
 
87
        self.table_column = tableview.tableColumns()[self.column]
 
88
        self.cell = self.table_column.dataCell()
 
89
        self.update_position(point)
 
90
        if isinstance(self.cell, CustomTableCell):
 
91
            self.name = self.calc_hotspot()
 
92
        else:
 
93
            self.name = None
 
94
        self.hit = (self.name is not None)
 
95
 
 
96
    def calc_cell_hotspot(self, column, row):
 
97
        if (self.hit and self.column == column and self.row == row):
 
98
            return self.name
 
99
        else:
 
100
            return None
 
101
 
 
102
    def update_position(self, point):
 
103
        cell_frame = self.tableview.frameOfCellAtColumn_row_(self.column,
 
104
                self.row)
 
105
        self.pos = NSPoint(point.x - cell_frame.origin.x, 
 
106
                point.y - cell_frame.origin.y)
 
107
 
 
108
    def update_hit(self):
 
109
        old_hit = self.hit
 
110
        self.hit = (self.calc_hotspot() == self.name)
 
111
        if old_hit != self.hit:
 
112
            self.redraw_cell()
 
113
 
 
114
    def set_cell_data(self):
 
115
        row = self.tableview.dataSource().model[self.iter]
 
116
        value_dict = tablemodel.get_column_data(row, self.table_column)
 
117
        self.cell.setObjectValue_(value_dict)
 
118
 
 
119
    def calc_hotspot(self):
 
120
        self.set_cell_data()
 
121
        cell_frame = self.tableview.frameOfCellAtColumn_row_(self.column,
 
122
                self.row)
 
123
        style = self.cell.make_drawing_style(cell_frame, self.tableview)
 
124
        layout_manager = self.cell.layout_manager
 
125
        layout_manager.reset()
 
126
        return self.cell.wrapper.hotspot_test(style, layout_manager,
 
127
                self.pos.x, self.pos.y, cell_frame.size.width,
 
128
                cell_frame.size.height)
 
129
 
 
130
    def redraw_cell(self):
 
131
        # Check to see if we removed the table in response to a hotspot click.
 
132
        if self.tableview.superview() is not nil:
 
133
            cell_frame = self.tableview.frameOfCellAtColumn_row_(self.column,
 
134
                    self.row)
 
135
            self.tableview.setNeedsDisplayInRect_(cell_frame)
 
136
 
 
137
class MiroTableCell(NSTextFieldCell):
 
138
    def init(self):
 
139
        return super(MiroTableCell, self).initTextCell_('')
 
140
 
 
141
    def calcHeight_(self, view):
 
142
        font = self.font()
 
143
        return math.ceil(font.ascender() + abs(font.descender()) +
 
144
                font.leading())
 
145
 
 
146
    def highlightColorWithFrame_inView_(self, frame, view):
 
147
        return nil
 
148
 
 
149
    def setObjectValue_(self, value_dict):
 
150
        if isinstance(value_dict, dict):
 
151
            NSCell.setObjectValue_(self, value_dict['value'])
 
152
        else:
 
153
            # OS X calls setObjectValue_('') on intialization
 
154
            NSCell.setObjectValue_(self, value_dict)
 
155
 
 
156
class MiroTableImageCell(NSImageCell):
 
157
    def calcHeight_(self, view):
 
158
        return self.value_dict['image'].size().height
 
159
 
 
160
    def highlightColorWithFrame_inView_(self, frame, view):
 
161
        return nil
 
162
 
 
163
    def setObjectValue_(self, value_dict):
 
164
        NSImageCell.setObjectValue_(self, value_dict['image'])
 
165
 
 
166
class MiroCheckboxCell(NSButtonCell):
 
167
    def init(self):
 
168
        self = super(MiroCheckboxCell, self).init()
 
169
        self.setButtonType_(NSSwitchButton)
 
170
        self.setTitle_('')
 
171
        return self
 
172
 
 
173
    def calcHeight_(self, view):
 
174
        return self.cellSize().height
 
175
 
 
176
    def highlightColorWithFrame_inView_(self, frame, view):
 
177
        return nil
 
178
 
 
179
    def setObjectValue_(self, value_dict):
 
180
        if isinstance(value_dict, dict):
 
181
            NSButtonCell.setObjectValue_(self, value_dict['value'])
 
182
        else:
 
183
            # OS X calls setObjectValue_('') on intialization
 
184
            NSCell.setObjectValue_(self, value_dict)
 
185
 
 
186
    def startTrackingAt_inView_(self, point, view):
 
187
        return YES
 
188
 
 
189
    def continueTracking_at_inView_(self, lastPoint, at, view):
 
190
        return YES
 
191
 
 
192
    def stopTracking_at_inView_mouseIsUp_(self, lastPoint, at, tableview, mouseIsUp):
 
193
        if mouseIsUp:
 
194
            column = tableview.columnAtPoint_(at)
 
195
            row = tableview.rowAtPoint_(at)
 
196
            if column != -1 and row != -1:
 
197
                wrapper = wrappermap.wrapper(tableview)
 
198
                column = wrapper.columns[column]
 
199
                itr = wrapper.model.iter_for_row(tableview, row)
 
200
                column.renderer.emit('clicked', itr)
 
201
        return NSButtonCell.stopTracking_at_inView_mouseIsUp_(self, lastPoint,
 
202
                at, tableview, mouseIsUp)
 
203
 
 
204
class CellRenderer(object):
 
205
    def __init__(self):
 
206
        self.cell = MiroTableCell.alloc().init()
 
207
 
 
208
    def setDataCell_(self, column):
 
209
        column.setDataCell_(self.cell)
 
210
 
 
211
    def set_text_size(self, size):
 
212
        if size == widgetconst.SIZE_NORMAL:
 
213
            self.cell.setFont_(NSFont.systemFontOfSize_(NSFont.systemFontSize()))
 
214
        elif size == widgetconst.SIZE_SMALL:
 
215
            self.cell.setFont_(NSFont.systemFontOfSize_(11))
 
216
        else:
 
217
            raise ValueError("Unknown size: %s" % size)
 
218
 
 
219
    def set_bold(self, bold):
 
220
        if bold:
 
221
            font = NSFont.boldSystemFontOfSize_(NSFont.systemFontSize())
 
222
        else:
 
223
            font = NSFont.systemFontOfSize_(NSFont.systemFontSize())
 
224
        self.cell.setFont_(font)
 
225
 
 
226
    def set_color(self, color):
 
227
        color = NSColor.colorWithDeviceRed_green_blue_alpha_(color[0],
 
228
                color[1], color[2], 1.0)
 
229
        self.cell.setTextColor_(color)
 
230
 
 
231
class ImageCellRenderer(object):
 
232
    def setDataCell_(self, column):
 
233
        column.setDataCell_(MiroTableImageCell.alloc().init())
 
234
 
 
235
class CheckboxCellRenderer(signals.SignalEmitter):
 
236
    def __init__(self):
 
237
        signals.SignalEmitter.__init__(self, 'clicked')
 
238
        self.size = widgetconst.SIZE_NORMAL
 
239
 
 
240
    def set_control_size(self, size):
 
241
        self.size = size
 
242
 
 
243
    def setDataCell_(self, column):
 
244
        cell = MiroCheckboxCell.alloc().init()
 
245
        if self.size == widgetconst.SIZE_SMALL:
 
246
            cell.setControlSize_(NSSmallControlSize)
 
247
        column.setDataCell_(cell)
 
248
 
 
249
class CustomTableCell(NSCell):
 
250
    def init(self):
 
251
        self = super(CustomTableCell, self).init()
 
252
        self.layout_manager = LayoutManager()
 
253
        self.hotspot = None
 
254
        return self
 
255
 
 
256
    def highlightColorWithFrame_inView_(self, frame, view):
 
257
        return nil
 
258
 
 
259
    def calcHeight_(self, view):
 
260
        self.layout_manager.reset()
 
261
        style = self.make_drawing_style(None, view)
 
262
        self.set_wrapper_data()
 
263
        cell_size = self.wrapper.get_size(style, self.layout_manager)
 
264
        return cell_size[1]
 
265
 
 
266
    def make_drawing_style(self, frame, view):
 
267
        text_color = None
 
268
        if (self.isHighlighted() and frame is not None and
 
269
                (view.isDescendantOf_(view.window().firstResponder()) or
 
270
                    view.gradientHighlight)):
 
271
            text_color = NSColor.whiteColor()
 
272
        return DrawingStyle(text_color=text_color)
 
273
 
 
274
    def drawInteriorWithFrame_inView_(self, frame, view):
 
275
        NSGraphicsContext.currentContext().saveGraphicsState()
 
276
        if self.wrapper.outline_column:
 
277
            pad_left = EXPANDER_PADDING
 
278
        else:
 
279
            pad_left = 0
 
280
        drawing_rect = NSMakeRect(frame.origin.x + pad_left, frame.origin.y,
 
281
                frame.size.width - pad_left, frame.size.height)
 
282
        context = DrawingContext(view, drawing_rect, drawing_rect)
 
283
        context.style = self.make_drawing_style(frame, view)
 
284
        self.layout_manager.reset()
 
285
        self.set_wrapper_data()
 
286
        self.wrapper.render(context, self.layout_manager, self.isHighlighted(),
 
287
                self.hotspot, view.cell_is_hovered(self.row, self.column))
 
288
        NSGraphicsContext.currentContext().restoreGraphicsState()
 
289
 
 
290
    def setObjectValue_(self, value_dict):
 
291
        self.value_dict = value_dict
 
292
 
 
293
    def set_wrapper_data(self):
 
294
        for name, value in self.value_dict.items():
 
295
            setattr(self.wrapper, name, value)
 
296
 
 
297
class CustomCellRenderer(object):
 
298
    def __init__(self):
 
299
        self.outline_column = False
 
300
 
 
301
    def setDataCell_(self, column):
 
302
        # Note that the ownership is the opposite of what happens in widgets.
 
303
        # The NSObject owns it's wrapper widget.  This happens for a couple
 
304
        # reasons:
 
305
        # 1) The data cell gets copied a bunch of times, so wrappermap won't
 
306
        # work with it.
 
307
        # 2) The Wrapper should only needs to stay around as long as the
 
308
        # NSCell that it's wrapping is around.  Once the column gets removed
 
309
        # from the table, the wrapper can be deleted.
 
310
        nscell = CustomTableCell.alloc().init()
 
311
        nscell.wrapper = self
 
312
        column.setDataCell_(nscell)
 
313
 
 
314
    def hotspot_test(self, style, layout, x, y, width, height):
 
315
        return None
 
316
 
 
317
def calc_row_height(view, model_row):
 
318
    row_height = 0
 
319
    for column in view.tableColumns():
 
320
        cell = column.dataCell()
 
321
        value_dict = tablemodel.get_column_data(model_row, column)
 
322
        cell.setObjectValue_(value_dict)
 
323
        cell_height = cell.calcHeight_(view)
 
324
        row_height = max(row_height, cell_height)
 
325
    if row_height == 0:
 
326
        row_height = 12
 
327
    return row_height
 
328
 
 
329
class TableViewDelegate(NSObject):
 
330
    def tableView_willDisplayCell_forTableColumn_row_(self, view, cell,
 
331
            column, row):
 
332
        column = view.column_index_map[column]
 
333
        cell.column = column
 
334
        cell.row = row
 
335
        if view.hotspot_tracker:
 
336
            cell.hotspot = view.hotspot_tracker.calc_cell_hotspot(column, row)
 
337
        else:
 
338
            cell.hotspot = None
 
339
 
 
340
    def tableView_didClickTableColumn_(self, tableview, column):
 
341
        wrapper = wrappermap.wrapper(tableview)
 
342
        for column_wrapper in wrapper.columns:
 
343
            if column_wrapper._column is column:
 
344
                column_wrapper.emit('clicked')
 
345
 
 
346
    def tableView_toolTipForCell_rect_tableColumn_row_mouseLocation_(self, tableview, cell, rect, column, row, location):
 
347
        wrapper = wrappermap.wrapper(tableview)
 
348
        iter = tableview.dataSource().model.iter_for_row(tableview, row)
 
349
        for wrapper_column in wrapper.columns:
 
350
            if wrapper_column._column is column:
 
351
                break
 
352
        return (wrapper.get_tooltip(iter, wrapper_column), rect)
 
353
 
 
354
class VariableHeightTableViewDelegate(TableViewDelegate):
 
355
    def tableView_heightOfRow_(self, table_view, row):
 
356
        iter = table_view.dataSource().model.iter_for_row(table_view, row)
 
357
        if iter is None:
 
358
            return 12
 
359
        return calc_row_height(table_view, iter.value().values)
 
360
 
 
361
class OutlineViewDelegate(NSObject):
 
362
    def outlineView_willDisplayCell_forTableColumn_item_(self, view, cell,
 
363
            column, item):
 
364
        row = view.rowForItem_(item)
 
365
        column = view.column_index_map[column]
 
366
        cell.column = column
 
367
        cell.row = row
 
368
        if view.hotspot_tracker:
 
369
            cell.hotspot = view.hotspot_tracker.calc_cell_hotspot(column, row)
 
370
        else:
 
371
            cell.hotspot = None
 
372
 
 
373
    def outlineView_didClickTableColumn_(self, tableview, column):
 
374
        wrapper = wrappermap.wrapper(tableview)
 
375
        for column_wrapper in wrapper.columns:
 
376
            if column_wrapper._column is column:
 
377
                column_wrapper.emit('clicked')
 
378
 
 
379
    def outlineView_toolTipForCell_rect_tableColumn_row_mouseLocation_(self, tableview, cell, rect, column, row, location):
 
380
        wrapper = wrappermap.wrapper(tableview)
 
381
        iter = tableview.dataSource().model.iter_for_row(tableview, row)
 
382
        for wrapper_column in wrapper.columns:
 
383
            if wrapper_column._column is column:
 
384
                break
 
385
        return (wrapper.get_tooltip(iter, wrapper_column), rect)
 
386
 
 
387
class VariableHeightOutlineViewDelegate(OutlineViewDelegate):
 
388
    def outlineView_heightOfRowByItem_(self, outline_view, item):
 
389
        return calc_row_height(outline_view, item.values)
 
390
 
 
391
# TableViewCommon is a hack to do a Mixin class.  We want the same behaviour
 
392
# for our table views and our outline views.  Normally we would use a Mixin,
 
393
# but that doesn't work with pyobjc.  Instead we define the common code in
 
394
# TableViewCommon, then copy it into MiroTableView and MiroOutlineView
 
395
 
 
396
class TableViewCommon(object):
 
397
    def init(self):
 
398
        self = super(self.__class__, self).init()
 
399
        self.hotspot_tracker = None
 
400
        self._tracking_rects = []
 
401
        self.hover_info = None
 
402
        self.column_index_map = {}
 
403
        self.setFocusRingType_(NSFocusRingTypeNone)
 
404
        self.handled_last_mouse_down = False
 
405
        self.gradientHighlight = False
 
406
        return self
 
407
 
 
408
    def addTableColumn_(self, column):
 
409
        self.column_index_map[column] = len(self.tableColumns())
 
410
        self.SuperClass.addTableColumn_(self, column)
 
411
 
 
412
    def removeTableColumn(self, column):
 
413
        del self.column_index_map[column]
 
414
        for after_index in xrange(index+1, len(self.tableColumns())):
 
415
            self.column_index_map[column_list[after_index]] -= 1
 
416
        self.SuperClass.removeTableColumn(self, column)
 
417
 
 
418
    def moveColumn_toColumn_(self, src, dest):
 
419
        # Need to switch the TableColumn objects too
 
420
        columns = wrappermap.wrapper(self).columns
 
421
        columns[src], columns[dest] = columns[dest], columns[src]
 
422
        self.SuperClass.moveColumn_toColumn_(self, src, dest)
 
423
 
 
424
    def highlightSelectionInClipRect_(self, rect):
 
425
        if wrappermap.wrapper(self).draws_selection:
 
426
            if not self.gradientHighlight:
 
427
                return self.SuperClass.highlightSelectionInClipRect_(self,
 
428
                        rect)
 
429
            context = NSGraphicsContext.currentContext()
 
430
            focused = self.isDescendantOf_(self.window().firstResponder())
 
431
            for row in tablemodel.list_from_nsindexset(self.selectedRowIndexes()):
 
432
                self.drawBackgroundGradient(context, focused, row)
 
433
    
 
434
    def setFrameSize_(self, size):
 
435
        if size.height == 0:
 
436
            size.height = 4
 
437
        self.SuperClass.setFrameSize_(self, size)
 
438
 
 
439
    def drawBackgroundGradient(self, context, focused, row):
 
440
        if focused:
 
441
            start_color = (0.412, 0.584, 0.792)
 
442
            end_color = (0.153, 0.345, 0.62)
 
443
            line_color = NSColor.colorWithDeviceRed_green_blue_alpha_(
 
444
                    0.322, 0.506, 0.733, 1.0)
 
445
        else:
 
446
            start_color = (0.671, 0.694, 0.776)
 
447
            end_color = (0.447, 0.471, 0.596)
 
448
            line_color = NSColor.colorWithDeviceRed_green_blue_alpha_(
 
449
                    0.514, 0.537, 0.655, 1.0)
 
450
 
 
451
        rect = self.rectOfRow_(row)
 
452
        top = NSMakeRect(rect.origin.x, rect.origin.y, rect.size.width, 1)
 
453
        context.saveGraphicsState()
 
454
        # draw the top line
 
455
        line_color.set()
 
456
        NSRectFill(top)
 
457
        # draw the gradient
 
458
        rect.origin.y += 1
 
459
        rect.size.height -= 1
 
460
        NSRectClip(rect)
 
461
        gradient = Gradient(rect.origin.x, rect.origin.y,
 
462
                rect.origin.x, rect.origin.y + rect.size.height)
 
463
        gradient.set_start_color(start_color)
 
464
        gradient.set_end_color(end_color)
 
465
        gradient.draw()
 
466
        context.restoreGraphicsState()
 
467
 
 
468
    def canDragRowsWithIndexes_atPoint_(self, indexes, point):
 
469
        return YES
 
470
 
 
471
    def draggingSourceOperationMaskForLocal_(self, local):
 
472
        drag_source = wrappermap.wrapper(self).drag_source
 
473
        if drag_source and local:
 
474
            return drag_source.allowed_actions()
 
475
        return NSDragOperationNone
 
476
 
 
477
    def recalcTrackingRects(self):
 
478
        # We aren't using mouse hover for 2.0, so let's skip this.  It just
 
479
        # wastes CPU cycles
 
480
        return
 
481
        if self.hover_info is not None:
 
482
            rect = self.frameOfCellAtColumn_row_(self.hover_info[1],
 
483
                    self.hover_info[0])
 
484
            self.hover_info = None
 
485
            self.setNeedsDisplayInRect_(rect)
 
486
        for tr in self._tracking_rects:
 
487
            self.removeTrackingRect_(tr)
 
488
        visible = self.visibleRect()
 
489
        row_range = self.rowsInRect_(visible)
 
490
        column_range = self.columnsInRect_(visible)
 
491
        self._tracking_rects = []
 
492
        for row in xrange(row_range.location, row_range.location +
 
493
                row_range.length):
 
494
            for column in xrange(column_range.location, column_range.location
 
495
                    + column_range.length):
 
496
                rect = self.frameOfCellAtColumn_row_(column, row)
 
497
                tr = self.addTrackingRect_owner_userData_assumeInside_( rect,
 
498
                        self, _pack_row_column(row, column), False)
 
499
                self._tracking_rects.append(tr)
 
500
 
 
501
    def mouseEntered_(self, event):
 
502
        window = self.window()
 
503
        if window is not nil and window.isMainWindow():
 
504
            row, column = _unpack_row_column(event.userData())
 
505
            self.hover_info = (row, column)
 
506
            rect = self.frameOfCellAtColumn_row_(column, row)
 
507
            self.setNeedsDisplayInRect_(rect)
 
508
 
 
509
    def mouseExited_(self, event):
 
510
        window = self.window()
 
511
        if window is not nil and window.isMainWindow():
 
512
            row, column = _unpack_row_column(event.userData())
 
513
            if self.hover_info == (row, column):
 
514
                self.hover_info = None
 
515
            rect = self.frameOfCellAtColumn_row_(column, row)
 
516
            self.setNeedsDisplayInRect_(rect)
 
517
 
 
518
    def cell_is_hovered(self, row, column):
 
519
        return self.hover_info == (row, column)
 
520
 
 
521
    def mouseDown_(self, event):
 
522
        if event.modifierFlags() & NSControlKeyMask:
 
523
            self.handleContextMenu_(event)
 
524
            self.handled_last_mouse_down = True
 
525
            return
 
526
 
 
527
        point = self.convertPoint_fromView_(event.locationInWindow(), nil)
 
528
 
 
529
        if event.clickCount() == 2:
 
530
            if self.handled_last_mouse_down:
 
531
                return
 
532
            wrapper = wrappermap.wrapper(self)
 
533
            row = self.rowAtPoint_(point)
 
534
            if row != -1:
 
535
                iter = wrapper.model.iter_for_row(self, row)
 
536
                wrapper.emit('row-double-clicked', iter)
 
537
            return
 
538
 
 
539
        hotspot_tracker = HotspotTracker(self, point)
 
540
        if hotspot_tracker.hit:
 
541
            self.hotspot_tracker = hotspot_tracker
 
542
            self.hotspot_tracker.redraw_cell()
 
543
            self.handled_last_mouse_down = True
 
544
        else:
 
545
            self.handled_last_mouse_down = False
 
546
            self.SuperClass.mouseDown_(self, event)
 
547
 
 
548
    def rightMouseDown_(self, event):
 
549
        self.handleContextMenu_(event)
 
550
 
 
551
    def handleContextMenu_(self, event):
 
552
        self.window().makeFirstResponder_(self)
 
553
        point = self.convertPoint_fromView_(event.locationInWindow(), nil)
 
554
        row = self.rowAtPoint_(point)
 
555
        selection = self.selectedRowIndexes()
 
556
        if not selection.containsIndex_(row):
 
557
            index_set = NSIndexSet.alloc().initWithIndex_(row)
 
558
            self.selectRowIndexes_byExtendingSelection_(index_set, NO)
 
559
        wrapper = wrappermap.wrapper(self)
 
560
        if wrapper.context_menu_callback is not None:
 
561
            menu_items = wrapper.context_menu_callback(wrapper)
 
562
            menu = osxmenus.make_context_menu(menu_items)
 
563
            NSMenu.popUpContextMenu_withEvent_forView_(menu, event, self)
 
564
 
 
565
    def mouseDragged_(self, event):
 
566
        if self.hotspot_tracker is not None:
 
567
            point = self.convertPoint_fromView_(event.locationInWindow(), nil)
 
568
            self.hotspot_tracker.update_position(point)
 
569
            self.hotspot_tracker.update_hit()
 
570
        else:
 
571
            self.SuperClass.mouseDragged_(self, event)
 
572
 
 
573
    def mouseUp_(self, event):
 
574
        if self.hotspot_tracker is not None:
 
575
            point = self.convertPoint_fromView_(event.locationInWindow(), nil)
 
576
            self.hotspot_tracker.update_position(point)
 
577
            self.hotspot_tracker.update_hit()
 
578
            if self.hotspot_tracker.hit:
 
579
                wrappermap.wrapper(self).send_hotspot_clicked()
 
580
            self.hotspot_tracker.redraw_cell()
 
581
            self.hotspot_tracker = None
 
582
        else:
 
583
            self.SuperClass.mouseUp_(self, event)
 
584
 
 
585
class TableColumn(signals.SignalEmitter):
 
586
    def __init__(self, title, renderer, **attrs):
 
587
        signals.SignalEmitter.__init__(self)
 
588
        self.create_signal('clicked')
 
589
        self._column = NSTableColumn.alloc().initWithIdentifier_(attrs)
 
590
        self._column.setHeaderCell_(MiroTableHeaderCell.alloc().init())
 
591
        self._column.headerCell().setStringValue_(title)
 
592
        self._column.setEditable_(NO)
 
593
        self._column.setResizingMask_(NSTableColumnNoResizing)
 
594
        self.renderer = renderer
 
595
        self.sort_order_ascending = True
 
596
        self.sort_indicator_visible = False
 
597
        renderer.setDataCell_(self._column)
 
598
 
 
599
    def set_right_aligned(self, right_aligned):
 
600
        if right_aligned:
 
601
            self._column.headerCell().setAlignment_(NSRightTextAlignment)
 
602
        else:
 
603
            self._column.headerCell().setAlignment_(NSLeftTextAlignment)
 
604
 
 
605
    def set_min_width(self, width):
 
606
        self._column.setMinWidth_(width)
 
607
 
 
608
    def set_max_width(self, width):
 
609
        self._column.setMaxWidth_(width)
 
610
 
 
611
    def set_width(self, width):
 
612
        self._column.setWidth_(width)
 
613
 
 
614
    def set_resizable(self, resizable):
 
615
        mask = 0
 
616
        if resizable:
 
617
            mask |= NSTableColumnUserResizingMask
 
618
        self._column.setResizingMask_(mask)
 
619
 
 
620
    def set_sort_indicator_visible(self, visible):
 
621
        self.sort_indicator_visible = visible
 
622
        self._column.tableView().headerView().setNeedsDisplay_(True)
 
623
 
 
624
    def get_sort_indicator_visible(self):
 
625
        return self.sort_indicator_visible
 
626
 
 
627
    def set_sort_order(self, ascending):
 
628
        self.sort_order_ascending = ascending
 
629
        self._column.tableView().headerView().setNeedsDisplay_(True)
 
630
 
 
631
    def get_sort_order_ascending(self):
 
632
        return self.sort_order_ascending
 
633
 
 
634
class MiroTableView(NSTableView):
 
635
    SuperClass = NSTableView
 
636
    for name, value in TableViewCommon.__dict__.items():
 
637
        locals()[name] = value
 
638
 
 
639
class MiroOutlineView(NSOutlineView):
 
640
    SuperClass = NSOutlineView
 
641
    for name, value in TableViewCommon.__dict__.items():
 
642
        locals()[name] = value
 
643
 
 
644
class MiroTableHeaderView(NSTableHeaderView):
 
645
    def drawRect_(self, rect):
 
646
        NSTableHeaderView.drawRect_(self, rect)
 
647
        wrapper = wrappermap.wrapper(self.tableView())
 
648
        # Manually handle sort column drawing
 
649
        for i, column in enumerate(wrapper.columns):
 
650
            if column.sort_indicator_visible:
 
651
                cell = column._column.headerCell()
 
652
                frame = self.headerRectOfColumn_(i)
 
653
                cell.highlight_withFrame_inView_(True, frame, self)
 
654
                cell.drawSortIndicatorWithFrame_inView_ascending_priority_(
 
655
                        frame, self, column.sort_order_ascending, 0)
 
656
 
 
657
class MiroTableHeaderCell(NSTableHeaderCell):
 
658
    def drawInteriorWithFrame_inView_(self, frame, view):
 
659
        # Take into account differences in intercellSpacing() (the default is
 
660
        # 3, but that can change using TableView.set_column_spacing())
 
661
        extra_space = view.tableView().intercellSpacing().width - 3
 
662
        padded_frame = NSMakeRect(frame.origin.x + (extra_space / 2),
 
663
                frame.origin.y, frame.size.width - extra_space,
 
664
                frame.size.height)
 
665
        NSTableHeaderCell.drawInteriorWithFrame_inView_(self, padded_frame, view)
 
666
 
 
667
class TableView(Widget):
 
668
    """Displays data as a tabular list.  TableView follows the GTK TreeView
 
669
    widget fairly closely.
 
670
    """
 
671
 
 
672
    CREATES_VIEW = False
 
673
    # Bit of a hack.  We create several views.  By setting CREATES_VIEW to
 
674
    # False, we get to position the views manually.
 
675
 
 
676
    def __init__(self, model):
 
677
        Widget.__init__(self)
 
678
        self.create_signal('selection-changed')
 
679
        self.create_signal('hotspot-clicked')
 
680
        self.create_signal('row-double-clicked')
 
681
        self.model = model
 
682
        self.columns = []
 
683
        self.context_menu_callback = None
 
684
        if self.is_tree():
 
685
            self.create_signal('row-expanded')
 
686
            self.create_signal('row-collapsed')
 
687
            self.tableview = MiroOutlineView.alloc().init()
 
688
            self.data_source = tablemodel.MiroOutlineViewDataSource.alloc()
 
689
        else:
 
690
            self.tableview = MiroTableView.alloc().init()
 
691
            self.data_source = tablemodel.MiroTableViewDataSource.alloc()
 
692
        self.view = self.tableview
 
693
        self.data_source.initWithModel_(self.model)
 
694
        self.tableview.setDataSource_(self.data_source)
 
695
        self.tableview.setVerticalMotionCanBeginDrag_(YES)
 
696
        self.set_columns_draggable(False)
 
697
        self.set_auto_resizes(False)
 
698
        self.draws_selection = True
 
699
        self.row_height_set = False
 
700
        self.set_fixed_height(False)
 
701
        self.auto_resizing = False
 
702
        self.header_view = MiroTableHeaderView.alloc().initWithFrame_(
 
703
            NSMakeRect(0, 0, 0, HEADER_HEIGHT))
 
704
        self.set_show_headers(True)
 
705
        self.notifications = NotificationForwarder.create(self.tableview)
 
706
        self.model.connect_weak('row-changed', self.on_row_change)
 
707
        self.model.connect_weak('row-added', self.on_row_added)
 
708
        self.model.connect_weak('row-will-be-removed', self.on_row_removed)
 
709
        self.iters_to_update = []
 
710
        self.height_changed = self.selection_removed = self.reload_needed = False
 
711
 
 
712
    def send_hotspot_clicked(self):
 
713
        tracker = self.tableview.hotspot_tracker
 
714
        self.emit('hotspot-clicked', tracker.name, tracker.iter)
 
715
 
 
716
    def set_draws_selection(self, draws_selection):
 
717
        self.draws_selection = draws_selection
 
718
 
 
719
    def get_left_offset(self):
 
720
        offset = self.tableview.intercellSpacing().width / 2
 
721
        # Yup this can be a non-integer, it seems like that's what OS X does,
 
722
        # because either way I round it looks worse than this.
 
723
        if self.is_tree():
 
724
            offset +=  _disclosure_button_width + EXPANDER_PADDING
 
725
        return offset
 
726
 
 
727
    def on_row_change(self, model, iter, old_row):
 
728
        self.iters_to_update.append(iter)
 
729
        if not self.fixed_height:
 
730
            old_height = calc_row_height(self.tableview, old_row)
 
731
            new_height = calc_row_height(self.tableview, self.model[iter])
 
732
            if new_height != old_height:
 
733
                self.height_changed = True
 
734
        if self.tableview.hotspot_tracker is not None:
 
735
            self.tableview.hotspot_tracker.update_hit()
 
736
 
 
737
    def on_row_added(self, model, iter):
 
738
        self.reload_needed = True
 
739
        self.cancel_hotspot_track()
 
740
 
 
741
    def on_row_removed(self, model, iter):
 
742
        self.reload_needed = True
 
743
        if self.tableview.isRowSelected_(self.row_for_iter(iter)):
 
744
            self.tableview.deselectAll_(nil)
 
745
            self.selection_removed = True
 
746
        self.cancel_hotspot_track()
 
747
 
 
748
    def cancel_hotspot_track(self):
 
749
        if self.tableview.hotspot_tracker is not None:
 
750
            self.tableview.hotspot_tracker.redraw_cell()
 
751
            self.tableview.hotspot_tracker = None
 
752
 
 
753
    def on_expanded(self, notification):
 
754
        self.invalidate_size_request()
 
755
        item = notification.userInfo()['NSObject']
 
756
        self.emit('row-expanded', self.model.iter_for_item[item])
 
757
 
 
758
    def on_collapsed(self, notification):
 
759
        self.invalidate_size_request()
 
760
        item = notification.userInfo()['NSObject']
 
761
        self.emit('row-collapsed', self.model.iter_for_item[item])
 
762
 
 
763
    def on_selection_change(self, notification):
 
764
        self.emit('selection-changed')
 
765
 
 
766
    def on_column_resize(self, notification):
 
767
        if not self.auto_resizing:
 
768
            self.invalidate_size_request()
 
769
 
 
770
    def is_tree(self):
 
771
        return isinstance(self.model, tablemodel.TreeTableModel)
 
772
 
 
773
    def set_row_expanded(self, iter, expanded):
 
774
        item = iter.value()
 
775
        if expanded:
 
776
            self.tableview.expandItem_(item)
 
777
        else:
 
778
            self.tableview.collapseItem_(item)
 
779
        self.invalidate_size_request()
 
780
 
 
781
    def is_row_expanded(self, iter):
 
782
        return self.tableview.isItemExpanded_(iter.value())
 
783
 
 
784
    def calc_size_request(self):
 
785
        self.tableview.tile()
 
786
        height = self.tableview.frame().size.height
 
787
        if self._show_headers:
 
788
            height += HEADER_HEIGHT
 
789
        return self.calc_width(), height
 
790
 
 
791
    def viewport_repositioned(self):
 
792
        self._do_layout()
 
793
        self.tableview.recalcTrackingRects()
 
794
 
 
795
    def viewport_created(self):
 
796
        wrappermap.add(self.tableview, self)
 
797
        self._do_layout()
 
798
        self._add_views()
 
799
        if self.is_tree():
 
800
            self.notifications.connect(self.on_expanded,
 
801
                'NSOutlineViewItemDidExpandNotification')
 
802
            self.notifications.connect(self.on_collapsed,
 
803
                'NSOutlineViewItemDidCollapseNotification')
 
804
            self.notifications.connect(self.on_selection_change,
 
805
                    'NSOutlineViewSelectionDidChangeNotification')
 
806
            self.notifications.connect(self.on_column_resize,
 
807
                    'NSOutlineViewColumnDidResizeNotification')
 
808
        else:
 
809
            self.notifications.connect(self.on_selection_change,
 
810
                    'NSTableViewSelectionDidChangeNotification')
 
811
            self.notifications.connect(self.on_column_resize,
 
812
                    'NSTableViewColumnDidResizeNotification')
 
813
        self.tableview.recalcTrackingRects()
 
814
 
 
815
    def remove_viewport(self):
 
816
        if self.viewport is not None:
 
817
            self._remove_views()
 
818
            wrappermap.remove(self.tableview)
 
819
            self.notifications.disconnect()
 
820
            self.viewport = None
 
821
 
 
822
    def viewport_scrolled(self):
 
823
        self.tableview.recalcTrackingRects()
 
824
 
 
825
    def _should_place_header_view(self):
 
826
        return self._show_headers and not self.parent_is_scroller
 
827
 
 
828
    def _add_views(self):
 
829
        self.viewport.view.addSubview_(self.tableview)
 
830
        if self._should_place_header_view():
 
831
            self.viewport.view.addSubview_(self.header_view)
 
832
 
 
833
    def _remove_views(self):
 
834
        self.tableview.removeFromSuperview()
 
835
        self.header_view.removeFromSuperview()
 
836
 
 
837
    def _do_layout(self):
 
838
        x = self.viewport.placement.origin.x
 
839
        y = self.viewport.placement.origin.y
 
840
        width = self.viewport.get_width()
 
841
        height = self.viewport.get_height()
 
842
        if self._should_place_header_view():
 
843
            self.header_view.setFrame_(NSMakeRect(x, y, width, HEADER_HEIGHT))
 
844
            self.tableview.setFrame_(NSMakeRect(x, y + HEADER_HEIGHT, 
 
845
                width, height - HEADER_HEIGHT))
 
846
        else:
 
847
            self.tableview.setFrame_(NSMakeRect(x, y, width, height))
 
848
 
 
849
        if self.auto_resize:
 
850
            self.auto_resizing = True
 
851
            self._autoresize_columns()
 
852
            self.auto_resizing = False
 
853
        self.queue_redraw()
 
854
 
 
855
    def _autoresize_columns(self):
 
856
        # Resize the column so that they take up the width we are allocated,
 
857
        # but keep in mind the min/max width constraints.
 
858
        # The algorithm we use is to add/subtract width evenly between the
 
859
        # columns, but not more than their max/min width.  Repeat the process
 
860
        # until there is no extra space.
 
861
        columns = self.tableview.tableColumns()
 
862
        if len(columns) == 1:
 
863
            # we can special case this easily
 
864
            total_width = self.viewport.area().size.width
 
865
            columns[0].setWidth_(total_width)
 
866
            return
 
867
        column_width = sum(column.width() for column in columns)
 
868
        width_difference = self.viewport.area().size.width - column_width
 
869
        width_difference -= self.tableview.intercellSpacing().width * len(columns)
 
870
        while width_difference != 0:
 
871
            did_something = False
 
872
            columns_left = len(columns)
 
873
            for column in columns:
 
874
                old_width = column.width()
 
875
                ideal_change = round(width_difference / columns_left)
 
876
                ideal_new_width = old_width + ideal_change
 
877
                if width_difference < 0:
 
878
                    column.setWidth_(max(ideal_new_width, column.minWidth()))
 
879
                else:
 
880
                    column.setWidth_(min(ideal_new_width, column.maxWidth()))
 
881
                if column.width() != old_width:
 
882
                    width_difference -= (column.width() - old_width)
 
883
                    did_something = True
 
884
                columns_left -= 1
 
885
            if not did_something:
 
886
                # We couldn't change any widths because they were all at their
 
887
                # max/min sizes.  Bailout
 
888
                break
 
889
 
 
890
    def calc_width(self):
 
891
        if self.column_count() == 0:
 
892
            return 0
 
893
        width = 0
 
894
        columns = self.tableview.tableColumns()
 
895
        if self.auto_resize:
 
896
            # Table auto-resizes, we can shrink to min-width for each column
 
897
            width = sum(column.minWidth() for column in columns)
 
898
        else:
 
899
            # Table doesn't auto-resize, the columns can't get smaller than
 
900
            # their current width
 
901
            width = sum(column.width() for column in columns)
 
902
        width += self.tableview.intercellSpacing().width * self.column_count()
 
903
        return width
 
904
 
 
905
    def model_changed(self):
 
906
        if not self.row_height_set and self.fixed_height:
 
907
            self.try_to_set_row_height()
 
908
        if self.reload_needed:
 
909
            self.tableview.reloadData()
 
910
            self.invalidate_size_request()
 
911
            if self.selection_removed:
 
912
                self.emit('selection-changed')
 
913
            self.tableview.recalcTrackingRects()
 
914
        elif self.iters_to_update:
 
915
            if self.fixed_height or not self.height_changed:
 
916
                # our rows don't change height, just update cell areas
 
917
                if self.is_tree():
 
918
                    for iter in self.iters_to_update:
 
919
                        self.tableview.reloadItem_(iter.value())
 
920
                else:
 
921
                    for iter in self.iters_to_update:
 
922
                        row = self.row_for_iter(iter)
 
923
                        rect = self.tableview.rectOfRow_(row)
 
924
                        self.tableview.setNeedsDisplayInRect_(rect)
 
925
            else:
 
926
                # our rows can change height inform Cocoa that their heights
 
927
                # might have changed (this will redraw them)
 
928
                rows_to_change = [ self.row_for_iter(iter) for iter in \
 
929
                    self.iters_to_update]
 
930
                index_set = NSMutableIndexSet.alloc().init()
 
931
                for iter in self.iters_to_update:
 
932
                    index_set.addIndex_(self.row_for_iter(iter))
 
933
                self.tableview.noteHeightOfRowsWithIndexesChanged_(index_set)
 
934
                self.tableview.recalcTrackingRects()
 
935
        else:
 
936
            return
 
937
        self.height_changed = self.selection_removed = self.reload_needed = False
 
938
        self.iters_to_update = []
 
939
 
 
940
    def width_for_columns(self, width):
 
941
        """If the table is width pixels big, how much width is available for
 
942
        the table's columns.
 
943
        """
 
944
        spacing = self.tableview.intercellSpacing().width * self.column_count()
 
945
        return width - spacing
 
946
 
 
947
    def set_column_spacing(self, column_spacing):
 
948
        spacing = self.tableview.intercellSpacing()
 
949
        spacing.width = column_spacing
 
950
        self.tableview.setIntercellSpacing_(spacing)
 
951
 
 
952
    def set_row_spacing(self, row_spacing):
 
953
        spacing = self.tableview.intercellSpacing()
 
954
        spacing.height = row_spacing
 
955
        self.tableview.setIntercellSpacing_(spacing)
 
956
 
 
957
    def set_alternate_row_backgrounds(self, setting):
 
958
        self.tableview.setUsesAlternatingRowBackgroundColors_(setting)
 
959
 
 
960
    def set_grid_lines(self, horizontal, vertical):
 
961
        mask = 0
 
962
        if horizontal:
 
963
            mask |= NSTableViewSolidHorizontalGridLineMask
 
964
        if vertical:
 
965
            mask |= NSTableViewSolidVerticalGridLineMask
 
966
        self.tableview.setGridStyleMask_(mask)
 
967
 
 
968
    def set_gradient_highlight(self, setting):
 
969
        self.tableview.gradientHighlight = setting
 
970
 
 
971
    def get_tooltip(self, iter, column):
 
972
        return None
 
973
 
 
974
    def add_column(self, column):
 
975
        self.columns.append(column)
 
976
        self.tableview.addTableColumn_(column._column)
 
977
        if self.column_count() == 1 and self.is_tree():
 
978
            self.tableview.setOutlineTableColumn_(column._column)
 
979
            column.renderer.outline_column = True
 
980
        # Adding a column means that each row could have a different height.
 
981
        # call noteNumberOfRowsChanged() to have OS X recalculate the heights
 
982
        self.tableview.noteNumberOfRowsChanged()
 
983
        self.invalidate_size_request()
 
984
 
 
985
    def column_count(self):
 
986
        return len(self.tableview.tableColumns())
 
987
 
 
988
    def remove_column(self, index):
 
989
        columns = self.columns.pop(index)
 
990
        self.tableview.removeTableColumn_(column._column)
 
991
        self.invalidate_size_request()
 
992
 
 
993
    def set_background_color(self, (red, green, blue)):
 
994
        color = NSColor.colorWithDeviceRed_green_blue_alpha_(red, green, blue,
 
995
                1.0)
 
996
        self.tableview.setBackgroundColor_(color)
 
997
 
 
998
    def set_show_headers(self, show):
 
999
        self._show_headers = show
 
1000
        if show:
 
1001
            self.tableview.setHeaderView_(self.header_view)
 
1002
        else:
 
1003
            self.tableview.setHeaderView_(None)
 
1004
        if self.viewport is not None:
 
1005
            self._remove_views()
 
1006
            self._do_layout()
 
1007
            self._add_views()
 
1008
        self.invalidate_size_request()
 
1009
        self.queue_redraw()
 
1010
    
 
1011
    def is_showing_headers(self):
 
1012
        return self._show_headers
 
1013
 
 
1014
    def set_search_column(self, model_index):
 
1015
        pass
 
1016
 
 
1017
    def try_to_set_row_height(self):
 
1018
        if len(self.model) > 0:
 
1019
            first_iter = self.model.first_iter()
 
1020
            height = calc_row_height(self.tableview, self.model[first_iter])
 
1021
            self.tableview.setRowHeight_(height)
 
1022
            self.row_height_set = True
 
1023
 
 
1024
    def set_auto_resizes(self, setting):
 
1025
        self.auto_resize = setting
 
1026
 
 
1027
    def set_columns_draggable(self, dragable):
 
1028
        self.tableview.setAllowsColumnReordering_(dragable)
 
1029
 
 
1030
    def set_fixed_height(self, fixed):
 
1031
        if fixed:
 
1032
            self.fixed_height = True
 
1033
            if self.is_tree():
 
1034
                delegate_class = OutlineViewDelegate
 
1035
            else:
 
1036
                delegate_class = TableViewDelegate
 
1037
            self.row_height_set = False
 
1038
            self.try_to_set_row_height()
 
1039
        else:
 
1040
            self.fixed_height = False
 
1041
            if self.is_tree():
 
1042
                delegate_class = VariableHeightOutlineViewDelegate
 
1043
            else:
 
1044
                delegate_class = VariableHeightTableViewDelegate
 
1045
        self.delegate = delegate_class.alloc().init()
 
1046
        self.tableview.setDelegate_(self.delegate)
 
1047
        self.tableview.reloadData()
 
1048
 
 
1049
    def allow_multiple_select(self, allow):
 
1050
        self.tableview.setAllowsMultipleSelection_(allow)
 
1051
 
 
1052
    def get_selection(self):
 
1053
        selection = self.tableview.selectedRowIndexes()
 
1054
        return [self.model.iter_for_row(self.tableview, row)  \
 
1055
                for row in tablemodel.list_from_nsindexset(selection)]
 
1056
 
 
1057
    def get_selected(self):
 
1058
        if self.tableview.allowsMultipleSelection():
 
1059
            raise ValueError("Table allows multiple selection")
 
1060
        row = self.tableview.selectedRow()
 
1061
        if row == -1:
 
1062
            return None
 
1063
        return self.model.iter_for_row(self.tableview, row)
 
1064
 
 
1065
    def num_rows_selected(self):
 
1066
        return self.tableview.selectedRowIndexes().count()
 
1067
 
 
1068
    def row_for_iter(self, iter):
 
1069
        if self.is_tree():
 
1070
            return self.tableview.rowForItem_(iter.value())
 
1071
        else:
 
1072
            return self.model.get_index_of_row(iter.value())
 
1073
 
 
1074
    def select(self, iter):
 
1075
        index_set = NSIndexSet.alloc().initWithIndex_(self.row_for_iter(iter))
 
1076
        self.tableview.selectRowIndexes_byExtendingSelection_(index_set, YES)
 
1077
 
 
1078
    def unselect(self, iter):
 
1079
        self.tableview.deselectRow_(self.row_for_iter(iter))
 
1080
 
 
1081
    def unselect_all(self):
 
1082
        self.tableview.deselectAll_(nil)
 
1083
 
 
1084
    def set_context_menu_callback(self, callback):
 
1085
        self.context_menu_callback = callback
 
1086
 
 
1087
    def set_drag_source(self, drag_source):
 
1088
        self.drag_source = drag_source
 
1089
        self.data_source.setDragSource_(drag_source)
 
1090
 
 
1091
    def set_drag_dest(self, drag_dest):
 
1092
        self.drag_dest = drag_dest
 
1093
        if drag_dest is None:
 
1094
            self.tableview.unregisterDraggedTypes()
 
1095
            self.data_source.setDragDest_(None)
 
1096
        else:
 
1097
            types = drag_dest.allowed_types()
 
1098
            self.tableview.registerForDraggedTypes_(types)
 
1099
            self.data_source.setDragDest_(drag_dest)