1
# Miro - an RSS based video player application
2
# Copyright (C) 2005-2010 Participatory Culture Foundation
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.
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.
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
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
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.
29
"""miro.plat.frontends.widgets.tableview -- TableView widget and it's
36
from Foundation import *
37
from objc import YES, NO, nil
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
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
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_.
64
if column > (1 << 16):
65
raise ValueError("column value too big: ", column)
66
return (row << 16) + column
68
def _unpack_row_column(value):
69
"""Reverse the work of _pack_row_column()."""
71
column = value & ((1 << 16) - 1)
74
class HotspotTracker(object):
75
"""Contains the info on the currently tracked hotspot. See:
76
https://develop.participatoryculture.org/index.php/WidgetAPITableView
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:
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()
94
self.hit = (self.name is not None)
96
def calc_cell_hotspot(self, column, row):
97
if (self.hit and self.column == column and self.row == row):
102
def update_position(self, point):
103
cell_frame = self.tableview.frameOfCellAtColumn_row_(self.column,
105
self.pos = NSPoint(point.x - cell_frame.origin.x,
106
point.y - cell_frame.origin.y)
108
def update_hit(self):
110
self.hit = (self.calc_hotspot() == self.name)
111
if old_hit != self.hit:
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)
119
def calc_hotspot(self):
121
cell_frame = self.tableview.frameOfCellAtColumn_row_(self.column,
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)
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,
135
self.tableview.setNeedsDisplayInRect_(cell_frame)
137
class MiroTableCell(NSTextFieldCell):
139
return super(MiroTableCell, self).initTextCell_('')
141
def calcHeight_(self, view):
143
return math.ceil(font.ascender() + abs(font.descender()) +
146
def highlightColorWithFrame_inView_(self, frame, view):
149
def setObjectValue_(self, value_dict):
150
if isinstance(value_dict, dict):
151
NSCell.setObjectValue_(self, value_dict['value'])
153
# OS X calls setObjectValue_('') on intialization
154
NSCell.setObjectValue_(self, value_dict)
156
class MiroTableImageCell(NSImageCell):
157
def calcHeight_(self, view):
158
return self.value_dict['image'].size().height
160
def highlightColorWithFrame_inView_(self, frame, view):
163
def setObjectValue_(self, value_dict):
164
NSImageCell.setObjectValue_(self, value_dict['image'])
166
class MiroCheckboxCell(NSButtonCell):
168
self = super(MiroCheckboxCell, self).init()
169
self.setButtonType_(NSSwitchButton)
173
def calcHeight_(self, view):
174
return self.cellSize().height
176
def highlightColorWithFrame_inView_(self, frame, view):
179
def setObjectValue_(self, value_dict):
180
if isinstance(value_dict, dict):
181
NSButtonCell.setObjectValue_(self, value_dict['value'])
183
# OS X calls setObjectValue_('') on intialization
184
NSCell.setObjectValue_(self, value_dict)
186
def startTrackingAt_inView_(self, point, view):
189
def continueTracking_at_inView_(self, lastPoint, at, view):
192
def stopTracking_at_inView_mouseIsUp_(self, lastPoint, at, tableview, 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)
204
class CellRenderer(object):
206
self.cell = MiroTableCell.alloc().init()
208
def setDataCell_(self, column):
209
column.setDataCell_(self.cell)
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))
217
raise ValueError("Unknown size: %s" % size)
219
def set_bold(self, bold):
221
font = NSFont.boldSystemFontOfSize_(NSFont.systemFontSize())
223
font = NSFont.systemFontOfSize_(NSFont.systemFontSize())
224
self.cell.setFont_(font)
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)
231
class ImageCellRenderer(object):
232
def setDataCell_(self, column):
233
column.setDataCell_(MiroTableImageCell.alloc().init())
235
class CheckboxCellRenderer(signals.SignalEmitter):
237
signals.SignalEmitter.__init__(self, 'clicked')
238
self.size = widgetconst.SIZE_NORMAL
240
def set_control_size(self, size):
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)
249
class CustomTableCell(NSCell):
251
self = super(CustomTableCell, self).init()
252
self.layout_manager = LayoutManager()
256
def highlightColorWithFrame_inView_(self, frame, view):
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)
266
def make_drawing_style(self, frame, view):
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)
274
def drawInteriorWithFrame_inView_(self, frame, view):
275
NSGraphicsContext.currentContext().saveGraphicsState()
276
if self.wrapper.outline_column:
277
pad_left = EXPANDER_PADDING
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()
290
def setObjectValue_(self, value_dict):
291
self.value_dict = value_dict
293
def set_wrapper_data(self):
294
for name, value in self.value_dict.items():
295
setattr(self.wrapper, name, value)
297
class CustomCellRenderer(object):
299
self.outline_column = False
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
305
# 1) The data cell gets copied a bunch of times, so wrappermap won't
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)
314
def hotspot_test(self, style, layout, x, y, width, height):
317
def calc_row_height(view, model_row):
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)
329
class TableViewDelegate(NSObject):
330
def tableView_willDisplayCell_forTableColumn_row_(self, view, cell,
332
column = view.column_index_map[column]
335
if view.hotspot_tracker:
336
cell.hotspot = view.hotspot_tracker.calc_cell_hotspot(column, row)
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')
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:
352
return (wrapper.get_tooltip(iter, wrapper_column), rect)
354
class VariableHeightTableViewDelegate(TableViewDelegate):
355
def tableView_heightOfRow_(self, table_view, row):
356
iter = table_view.dataSource().model.iter_for_row(table_view, row)
359
return calc_row_height(table_view, iter.value().values)
361
class OutlineViewDelegate(NSObject):
362
def outlineView_willDisplayCell_forTableColumn_item_(self, view, cell,
364
row = view.rowForItem_(item)
365
column = view.column_index_map[column]
368
if view.hotspot_tracker:
369
cell.hotspot = view.hotspot_tracker.calc_cell_hotspot(column, row)
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')
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:
385
return (wrapper.get_tooltip(iter, wrapper_column), rect)
387
class VariableHeightOutlineViewDelegate(OutlineViewDelegate):
388
def outlineView_heightOfRowByItem_(self, outline_view, item):
389
return calc_row_height(outline_view, item.values)
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
396
class TableViewCommon(object):
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
408
def addTableColumn_(self, column):
409
self.column_index_map[column] = len(self.tableColumns())
410
self.SuperClass.addTableColumn_(self, column)
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)
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)
424
def highlightSelectionInClipRect_(self, rect):
425
if wrappermap.wrapper(self).draws_selection:
426
if not self.gradientHighlight:
427
return self.SuperClass.highlightSelectionInClipRect_(self,
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)
434
def setFrameSize_(self, size):
437
self.SuperClass.setFrameSize_(self, size)
439
def drawBackgroundGradient(self, context, focused, row):
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)
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)
451
rect = self.rectOfRow_(row)
452
top = NSMakeRect(rect.origin.x, rect.origin.y, rect.size.width, 1)
453
context.saveGraphicsState()
459
rect.size.height -= 1
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)
466
context.restoreGraphicsState()
468
def canDragRowsWithIndexes_atPoint_(self, indexes, point):
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
477
def recalcTrackingRects(self):
478
# We aren't using mouse hover for 2.0, so let's skip this. It just
481
if self.hover_info is not None:
482
rect = self.frameOfCellAtColumn_row_(self.hover_info[1],
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 +
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)
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)
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)
518
def cell_is_hovered(self, row, column):
519
return self.hover_info == (row, column)
521
def mouseDown_(self, event):
522
if event.modifierFlags() & NSControlKeyMask:
523
self.handleContextMenu_(event)
524
self.handled_last_mouse_down = True
527
point = self.convertPoint_fromView_(event.locationInWindow(), nil)
529
if event.clickCount() == 2:
530
if self.handled_last_mouse_down:
532
wrapper = wrappermap.wrapper(self)
533
row = self.rowAtPoint_(point)
535
iter = wrapper.model.iter_for_row(self, row)
536
wrapper.emit('row-double-clicked', iter)
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
545
self.handled_last_mouse_down = False
546
self.SuperClass.mouseDown_(self, event)
548
def rightMouseDown_(self, event):
549
self.handleContextMenu_(event)
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)
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()
571
self.SuperClass.mouseDragged_(self, event)
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
583
self.SuperClass.mouseUp_(self, event)
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)
599
def set_right_aligned(self, right_aligned):
601
self._column.headerCell().setAlignment_(NSRightTextAlignment)
603
self._column.headerCell().setAlignment_(NSLeftTextAlignment)
605
def set_min_width(self, width):
606
self._column.setMinWidth_(width)
608
def set_max_width(self, width):
609
self._column.setMaxWidth_(width)
611
def set_width(self, width):
612
self._column.setWidth_(width)
614
def set_resizable(self, resizable):
617
mask |= NSTableColumnUserResizingMask
618
self._column.setResizingMask_(mask)
620
def set_sort_indicator_visible(self, visible):
621
self.sort_indicator_visible = visible
622
self._column.tableView().headerView().setNeedsDisplay_(True)
624
def get_sort_indicator_visible(self):
625
return self.sort_indicator_visible
627
def set_sort_order(self, ascending):
628
self.sort_order_ascending = ascending
629
self._column.tableView().headerView().setNeedsDisplay_(True)
631
def get_sort_order_ascending(self):
632
return self.sort_order_ascending
634
class MiroTableView(NSTableView):
635
SuperClass = NSTableView
636
for name, value in TableViewCommon.__dict__.items():
637
locals()[name] = value
639
class MiroOutlineView(NSOutlineView):
640
SuperClass = NSOutlineView
641
for name, value in TableViewCommon.__dict__.items():
642
locals()[name] = value
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)
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,
665
NSTableHeaderCell.drawInteriorWithFrame_inView_(self, padded_frame, view)
667
class TableView(Widget):
668
"""Displays data as a tabular list. TableView follows the GTK TreeView
669
widget fairly closely.
673
# Bit of a hack. We create several views. By setting CREATES_VIEW to
674
# False, we get to position the views manually.
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')
683
self.context_menu_callback = None
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()
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
712
def send_hotspot_clicked(self):
713
tracker = self.tableview.hotspot_tracker
714
self.emit('hotspot-clicked', tracker.name, tracker.iter)
716
def set_draws_selection(self, draws_selection):
717
self.draws_selection = draws_selection
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.
724
offset += _disclosure_button_width + EXPANDER_PADDING
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()
737
def on_row_added(self, model, iter):
738
self.reload_needed = True
739
self.cancel_hotspot_track()
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()
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
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])
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])
763
def on_selection_change(self, notification):
764
self.emit('selection-changed')
766
def on_column_resize(self, notification):
767
if not self.auto_resizing:
768
self.invalidate_size_request()
771
return isinstance(self.model, tablemodel.TreeTableModel)
773
def set_row_expanded(self, iter, expanded):
776
self.tableview.expandItem_(item)
778
self.tableview.collapseItem_(item)
779
self.invalidate_size_request()
781
def is_row_expanded(self, iter):
782
return self.tableview.isItemExpanded_(iter.value())
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
791
def viewport_repositioned(self):
793
self.tableview.recalcTrackingRects()
795
def viewport_created(self):
796
wrappermap.add(self.tableview, self)
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')
809
self.notifications.connect(self.on_selection_change,
810
'NSTableViewSelectionDidChangeNotification')
811
self.notifications.connect(self.on_column_resize,
812
'NSTableViewColumnDidResizeNotification')
813
self.tableview.recalcTrackingRects()
815
def remove_viewport(self):
816
if self.viewport is not None:
818
wrappermap.remove(self.tableview)
819
self.notifications.disconnect()
822
def viewport_scrolled(self):
823
self.tableview.recalcTrackingRects()
825
def _should_place_header_view(self):
826
return self._show_headers and not self.parent_is_scroller
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)
833
def _remove_views(self):
834
self.tableview.removeFromSuperview()
835
self.header_view.removeFromSuperview()
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))
847
self.tableview.setFrame_(NSMakeRect(x, y, width, height))
850
self.auto_resizing = True
851
self._autoresize_columns()
852
self.auto_resizing = False
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)
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()))
880
column.setWidth_(min(ideal_new_width, column.maxWidth()))
881
if column.width() != old_width:
882
width_difference -= (column.width() - old_width)
885
if not did_something:
886
# We couldn't change any widths because they were all at their
887
# max/min sizes. Bailout
890
def calc_width(self):
891
if self.column_count() == 0:
894
columns = self.tableview.tableColumns()
896
# Table auto-resizes, we can shrink to min-width for each column
897
width = sum(column.minWidth() for column in columns)
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()
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
918
for iter in self.iters_to_update:
919
self.tableview.reloadItem_(iter.value())
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)
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()
937
self.height_changed = self.selection_removed = self.reload_needed = False
938
self.iters_to_update = []
940
def width_for_columns(self, width):
941
"""If the table is width pixels big, how much width is available for
944
spacing = self.tableview.intercellSpacing().width * self.column_count()
945
return width - spacing
947
def set_column_spacing(self, column_spacing):
948
spacing = self.tableview.intercellSpacing()
949
spacing.width = column_spacing
950
self.tableview.setIntercellSpacing_(spacing)
952
def set_row_spacing(self, row_spacing):
953
spacing = self.tableview.intercellSpacing()
954
spacing.height = row_spacing
955
self.tableview.setIntercellSpacing_(spacing)
957
def set_alternate_row_backgrounds(self, setting):
958
self.tableview.setUsesAlternatingRowBackgroundColors_(setting)
960
def set_grid_lines(self, horizontal, vertical):
963
mask |= NSTableViewSolidHorizontalGridLineMask
965
mask |= NSTableViewSolidVerticalGridLineMask
966
self.tableview.setGridStyleMask_(mask)
968
def set_gradient_highlight(self, setting):
969
self.tableview.gradientHighlight = setting
971
def get_tooltip(self, iter, column):
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()
985
def column_count(self):
986
return len(self.tableview.tableColumns())
988
def remove_column(self, index):
989
columns = self.columns.pop(index)
990
self.tableview.removeTableColumn_(column._column)
991
self.invalidate_size_request()
993
def set_background_color(self, (red, green, blue)):
994
color = NSColor.colorWithDeviceRed_green_blue_alpha_(red, green, blue,
996
self.tableview.setBackgroundColor_(color)
998
def set_show_headers(self, show):
999
self._show_headers = show
1001
self.tableview.setHeaderView_(self.header_view)
1003
self.tableview.setHeaderView_(None)
1004
if self.viewport is not None:
1005
self._remove_views()
1008
self.invalidate_size_request()
1011
def is_showing_headers(self):
1012
return self._show_headers
1014
def set_search_column(self, model_index):
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
1024
def set_auto_resizes(self, setting):
1025
self.auto_resize = setting
1027
def set_columns_draggable(self, dragable):
1028
self.tableview.setAllowsColumnReordering_(dragable)
1030
def set_fixed_height(self, fixed):
1032
self.fixed_height = True
1034
delegate_class = OutlineViewDelegate
1036
delegate_class = TableViewDelegate
1037
self.row_height_set = False
1038
self.try_to_set_row_height()
1040
self.fixed_height = False
1042
delegate_class = VariableHeightOutlineViewDelegate
1044
delegate_class = VariableHeightTableViewDelegate
1045
self.delegate = delegate_class.alloc().init()
1046
self.tableview.setDelegate_(self.delegate)
1047
self.tableview.reloadData()
1049
def allow_multiple_select(self, allow):
1050
self.tableview.setAllowsMultipleSelection_(allow)
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)]
1057
def get_selected(self):
1058
if self.tableview.allowsMultipleSelection():
1059
raise ValueError("Table allows multiple selection")
1060
row = self.tableview.selectedRow()
1063
return self.model.iter_for_row(self.tableview, row)
1065
def num_rows_selected(self):
1066
return self.tableview.selectedRowIndexes().count()
1068
def row_for_iter(self, iter):
1070
return self.tableview.rowForItem_(iter.value())
1072
return self.model.get_index_of_row(iter.value())
1074
def select(self, iter):
1075
index_set = NSIndexSet.alloc().initWithIndex_(self.row_for_iter(iter))
1076
self.tableview.selectRowIndexes_byExtendingSelection_(index_set, YES)
1078
def unselect(self, iter):
1079
self.tableview.deselectRow_(self.row_for_iter(iter))
1081
def unselect_all(self):
1082
self.tableview.deselectAll_(nil)
1084
def set_context_menu_callback(self, callback):
1085
self.context_menu_callback = callback
1087
def set_drag_source(self, drag_source):
1088
self.drag_source = drag_source
1089
self.data_source.setDragSource_(drag_source)
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)
1097
types = drag_dest.allowed_types()
1098
self.tableview.registerForDraggedTypes_(types)
1099
self.data_source.setDragDest_(drag_dest)