1
# Copyright 2005 Joe Wreschnig, Michael Urman
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License version 2 as
5
# published by the Free Software Foundation
7
# $Id: views.py 3588 2006-07-06 15:47:06Z mu $
15
class TreeViewHints(gtk.Window):
16
"""Handle 'hints' for treeviews. This includes expansions of truncated
17
columns, and in the future, tooltips."""
19
__gsignals__ = dict.fromkeys(
20
['button-press-event', 'button-release-event',
21
'motion-notify-event', 'scroll-event'],
25
super(TreeViewHints, self).__init__(gtk.WINDOW_POPUP)
26
self.__label = label = gtk.Label()
27
label.set_alignment(0.5, 0.5)
29
self.add_events(gtk.gdk.BUTTON_MOTION_MASK | gtk.gdk.BUTTON_PRESS_MASK |
30
gtk.gdk.BUTTON_RELEASE_MASK | gtk.gdk.KEY_PRESS_MASK |
31
gtk.gdk.KEY_RELEASE_MASK | gtk.gdk.ENTER_NOTIFY_MASK |
32
gtk.gdk.LEAVE_NOTIFY_MASK | gtk.gdk.SCROLL_MASK)
35
self.set_app_paintable(True)
36
self.set_resizable(False)
37
self.set_name("gtk-tooltips")
38
self.set_border_width(1)
39
self.connect('expose-event', self.__expose)
40
self.connect('enter-notify-event', self.__enter)
41
self.connect('leave-notify-event', self.__check_undisplay)
44
self.__current_path = self.__current_col = None
45
self.__current_renderer = None
47
def connect_view(self, view):
48
self.__handlers[view] = [
49
view.connect('motion-notify-event', self.__motion),
50
view.connect('scroll-event', self.__undisplay),
51
view.connect('key-press-event', self.__undisplay),
52
view.connect('destroy', self.disconnect_view),
55
def disconnect_view(self, view):
57
for handler in self.__handlers[view]: view.disconnect(handler)
58
del self.__handlers[view]
61
def __expose(self, widget, event):
62
w, h = self.get_size_request()
63
self.style.paint_flat_box(self.window,
64
gtk.STATE_NORMAL, gtk.SHADOW_OUT,
65
None, self, "tooltip", 0, 0, w, h)
67
def __enter(self, widget, event):
68
# on entry, kill the hiding timeout
69
try: gobject.source_remove(self.__timeout_id)
70
except AttributeError: pass
71
else: del self.__timeout_id
73
def __motion(self, view, event):
74
# trigger over row area, not column headers
75
if event.window is not view.get_bin_window(): return
76
if event.get_state() & gtk.gdk.MODIFIER_MASK: return
78
x, y = map(int, [event.x, event.y])
79
try: path, col, cellx, celly = view.get_path_at_pos(x, y)
80
except TypeError: return # no hints where no rows exist
82
if self.__current_path == path and self.__current_col == col: return
84
# need to handle more renderers later...
85
try: renderer, = col.get_cell_renderers()
86
except ValueError: return
87
if not isinstance(renderer, gtk.CellRendererText): return
88
if renderer.get_property('ellipsize') == pango.ELLIPSIZE_NONE: return
90
model = view.get_model()
91
col.cell_set_cell_data(model, model.get_iter(path), False, False)
92
cellw = col.cell_get_position(renderer)[1]
95
label.set_ellipsize(pango.ELLIPSIZE_NONE)
96
label.set_text(renderer.get_property('text'))
97
w, h0 = label.get_layout().get_pixel_size()
98
try: markup = renderer.markup
99
except AttributeError: pass
101
if isinstance(markup, int): markup = model[path][markup]
102
label.set_markup(markup)
103
w, h1 = label.get_layout().get_pixel_size()
105
if w + 5 < cellw: return # don't display if it doesn't need expansion
107
x, y, cw, h = list(view.get_cell_area(path, col))
110
y += view.get_bin_window().get_position()[1]
111
ox, oy = view.window.get_origin()
112
x += ox; y += oy; w += 5
113
if gtk.gtk_version >= (2,8,0): w += 1 # width changed in 2.8?
114
screen_width = gtk.gdk.screen_width()
115
x_overflow = min([x, x + w - screen_width])
116
label.set_ellipsize(pango.ELLIPSIZE_NONE)
118
self.__dx -= x_overflow
120
w = min([w, screen_width])
121
label.set_ellipsize(pango.ELLIPSIZE_END)
122
if not((x<=int(event.x_root) < x+w) and (y <= int(event.y_root) < y+h)):
123
return # reject if cursor isn't above hint
126
self.__current_renderer = renderer
127
self.__edit_id = renderer.connect('editing-started', self.__undisplay)
128
self.__current_path = path
129
self.__current_col = col
130
self.__time = event.time
131
self.__timeout(id=gobject.timeout_add(100, self.__undisplay))
132
self.set_size_request(w, h)
137
def __check_undisplay(self, ev1, event):
138
if self.__time < event.time + 50: self.__undisplay()
140
def __undisplay(self, *args):
141
if self.__current_renderer and self.__edit_id:
142
self.__current_renderer.disconnect(self.__edit_id)
143
self.__current_renderer = self.__edit_id = None
144
self.__current_path = self.__current_col = None
147
def __timeout(self, ev=None, event=None, id=None):
148
try: gobject.source_remove(self.__timeout_id)
149
except AttributeError: pass
150
if id is not None: self.__timeout_id = id
152
def __event(self, event):
153
if event.type != gtk.gdk.SCROLL:
157
# modifying event.window is a necessary evil, made okay because
158
# nobody else should tie to any TreeViewHints events ever.
159
event.window = self.__target.get_bin_window()
161
gtk.main_do_event(event)
164
def do_button_press_event(self, event): return self.__event(event)
165
def do_button_release_event(self, event): return self.__event(event)
166
def do_motion_notify_event(self, event): return self.__event(event)
167
def do_scroll_event(self, event): return self.__event(event)
169
class MultiDragTreeView(gtk.TreeView):
170
"""TreeView with multirow drag support:
171
* Selections don't change until button-release-event...
172
* Unless they're a Shift/Ctrl modification, then they happen immediately
173
* Drag icons include 3 rows/2 plus a "and more" count"""
175
def __init__(self, *args):
176
super(MultiDragTreeView, self).__init__(*args)
178
'button-press-event', MultiDragTreeView.__button_press, self)
180
'button-release-event', MultiDragTreeView.__button_release, self)
181
self.connect_object('drag-begin', MultiDragTreeView.__begin, self)
182
self.__pending_event = None
184
def __button_press(self, event):
185
if event.button == 1: return self.__block_selection(event)
187
def __block_selection(self, event):
188
x, y = map(int, [event.x, event.y])
189
try: path, col, cellx, celly = self.get_path_at_pos(x, y)
190
except TypeError: return True
192
selection = self.get_selection()
193
if ((selection.path_is_selected(path)
194
and not (event.state & (gtk.gdk.CONTROL_MASK|gtk.gdk.SHIFT_MASK)))):
195
self.__pending_event = [x, y]
196
selection.set_select_function(lambda *args: False)
197
elif event.type == gtk.gdk.BUTTON_PRESS:
198
self.__pending_event = None
199
selection.set_select_function(lambda *args: True)
201
def __button_release(self, event):
202
if self.__pending_event:
203
selection = self.get_selection()
204
selection.set_select_function(lambda *args: True)
205
oldevent = self.__pending_event
206
self.__pending_event = None
207
if oldevent != [event.x, event.y]: return True
208
x, y = map(int, [event.x, event.y])
209
try: path, col, cellx, celly = self.get_path_at_pos(x, y)
210
except TypeError: return True
211
self.set_cursor(path, col, 0)
213
def __begin(self, ctx):
214
model, paths = self.get_selection().get_selected_rows()
217
icons = map(self.create_row_drag_icon, paths[:MAX])
219
sum(map(lambda s: s.get_size()[1], icons))-2*len(icons))+2
220
width = max(map(lambda s: s.get_size()[0], icons))
221
final = gtk.gdk.Pixmap(icons[0], width, height)
222
gc = gtk.gdk.GC(final)
223
gc.copy(self.style.fg_gc[gtk.STATE_NORMAL])
224
gc.set_colormap(self.window.get_colormap())
227
w, h = icon.get_size()
228
final.draw_drawable(gc, icon, 1, 1, 1, count_y, w-2, h-2)
232
bgc = gtk.gdk.GC(final)
233
bgc.copy(self.style.base_gc[gtk.STATE_NORMAL])
234
final.draw_rectangle(bgc, True, 1, count_y, w-2, h-2)
235
more = _("and %d more...") % (len(paths) - MAX + 1)
236
layout = self.create_pango_layout(more)
237
attrs = pango.AttrList()
238
attrs.insert(pango.AttrStyle(pango.STYLE_ITALIC, 0, len(more)))
239
layout.set_attributes(attrs)
240
layout.set_width(pango.SCALE * (w - 2))
241
lw, lh = layout.get_pixel_size()
242
final.draw_layout(gc, (w-lw)//2, count_y + (h-lh)//2, layout)
244
final.draw_rectangle(gc, False, 0, 0, width-1, height-1)
245
self.drag_source_set_icon(final.get_colormap(), final)
247
gobject.idle_add(ctx.drag_abort, gtk.get_current_event_time())
248
self.drag_source_set_icon_stock(gtk.STOCK_MISSING_IMAGE)
250
class RCMTreeView(gtk.TreeView):
251
"""Emits popup-menu when a row is right-clicked on."""
253
def __init__(self, *args):
254
super(RCMTreeView, self).__init__(*args)
256
'button-press-event', RCMTreeView.__button_press, self)
258
def __button_press(self, event):
259
if event.button == 3: return self.__check_popup(event)
261
def __check_popup(self, event):
262
x, y = map(int, [event.x, event.y])
263
try: path, col, cellx, celly = self.get_path_at_pos(x, y)
264
except TypeError: return True
266
selection = self.get_selection()
267
if not selection.path_is_selected(path):
268
self.set_cursor(path, col, 0)
270
col.focus_cell(col.get_cell_renderers()[0])
271
self.__position_at_mouse = True
272
self.emit('popup-menu')
275
def ensure_popup_selection(self):
277
self.__position_at_mouse
278
except AttributeError:
279
path, col = self.get_cursor()
282
self.scroll_to_cell(path, col)
283
# ensure current cursor path is selected, just like right-click
284
selection = self.get_selection()
285
if not selection.path_is_selected(path):
286
selection.unselect_all()
287
selection.select_path(path)
290
def popup_menu(self, menu, button, time):
292
del self.__position_at_mouse
293
except AttributeError:
294
# suppress menu if the cursor isn't on a real path
295
if not self.ensure_popup_selection():
297
pos_func = self.__popup_position
301
menu.popup(None, None, pos_func, button, time)
304
def __popup_position(self, menu):
305
path, col = self.get_cursor()
307
col = self.get_column(0)
309
# get a rectangle describing the cell render area (assume 3 px pad)
310
rect = self.get_cell_area(path, col)
315
dx, dy = self.window.get_origin()
316
dy += self.get_bin_window().get_position()[1]
318
# fit menu to screen, aligned per text direction
319
screen_width = gtk.gdk.screen_width()
320
screen_height = gtk.gdk.screen_height()
323
menu_y = rect.y + rect.height + dy
324
if menu_y + ma.height > screen_height and rect.y + dy - ma.height > 0:
325
menu_y = rect.y + dy - ma.height
326
if gtk.widget_get_default_direction() == gtk.TEXT_DIR_LTR:
327
menu_x = min(rect.x + dx, screen_width - ma.width)
329
menu_x = max(0, rect.x + dx - ma.width + rect.width)
331
return (menu_x, menu_y, True) # x, y, move_within_screen
333
class HintedTreeView(gtk.TreeView):
334
"""A TreeView that pops up a tooltip when you hover over a cell that
335
contains ellipsized text."""
337
def __init__(self, *args):
338
super(HintedTreeView, self).__init__(*args)
339
if not config.state('disable_hints'):
340
try: tvh = HintedTreeView.hints
341
except AttributeError: tvh = HintedTreeView.hints = TreeViewHints()
342
tvh.connect_view(self)
344
class TreeViewColumnButton(gtk.TreeViewColumn):
345
"""A TreeViewColumn that forwards its header events:
346
button-press-event and popup-menu"""
349
'button-press-event': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
351
'popup-menu': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
354
def __init__(self, title="", *args, **kw):
355
super(TreeViewColumnButton, self).__init__(title, *args, **kw)
356
label = gtk.Label(title)
357
self.set_widget(label)
359
label.__realize = label.connect('realize', self.__connect_menu_event)
361
def __connect_menu_event(self, widget):
362
widget.disconnect(widget.__realize)
364
button = widget.get_ancestor(gtk.Button)
366
button.connect('button-press-event', self.button_press_event)
367
button.connect('popup-menu', self.popup_menu)
369
def button_press_event(self, widget, event):
370
self.emit('button-press-event', event)
372
def popup_menu(self, widget):
373
self.emit('popup-menu')
376
class RCMHintedTreeView(HintedTreeView, RCMTreeView):
377
"""A TreeView that has hints and a context menu."""
380
class AllTreeView(HintedTreeView, RCMTreeView, MultiDragTreeView):
381
"""A TreeView that has hints, a context menu, and multi-selection