~ubuntu-branches/ubuntu/karmic/quodlibet/karmic

« back to all changes in this revision

Viewing changes to qltk/views.py

  • Committer: Bazaar Package Importer
  • Author(s): Luca Falavigna
  • Date: 2009-01-30 23:55:34 UTC
  • mfrom: (1.1.12 upstream)
  • Revision ID: james.westby@ubuntu.com-20090130235534-l4e72ulw0vqfo17w
Tags: 2.0-1ubuntu1
* Merge from Debian experimental (LP: #276856), remaining Ubuntu changes:
  + debian/patches/40-use-music-profile.patch:
    - Use the "Music and Movies" pipeline per default.
* Refresh the above patch for new upstream release.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright 2005 Joe Wreschnig, Michael Urman
2
 
#
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
6
 
#
7
 
# $Id: views.py 3588 2006-07-06 15:47:06Z mu $
8
 
 
9
 
import gobject
10
 
import gtk
11
 
import pango
12
 
 
13
 
import config
14
 
 
15
 
class TreeViewHints(gtk.Window):
16
 
    """Handle 'hints' for treeviews. This includes expansions of truncated
17
 
    columns, and in the future, tooltips."""
18
 
 
19
 
    __gsignals__ = dict.fromkeys(
20
 
        ['button-press-event', 'button-release-event',
21
 
        'motion-notify-event', 'scroll-event'],
22
 
        'override')
23
 
 
24
 
    def __init__(self):
25
 
        super(TreeViewHints, self).__init__(gtk.WINDOW_POPUP)
26
 
        self.__label = label = gtk.Label()
27
 
        label.set_alignment(0.5, 0.5)
28
 
        self.realize()
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)
33
 
        self.add(label)
34
 
 
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)
42
 
 
43
 
        self.__handlers = {}
44
 
        self.__current_path = self.__current_col = None
45
 
        self.__current_renderer = None
46
 
 
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),
53
 
        ]
54
 
 
55
 
    def disconnect_view(self, view):
56
 
        try:
57
 
            for handler in self.__handlers[view]: view.disconnect(handler)
58
 
            del self.__handlers[view]
59
 
        except KeyError: pass
60
 
 
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)
66
 
 
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
72
 
 
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
77
 
 
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
81
 
 
82
 
        if self.__current_path == path and self.__current_col == col: return
83
 
 
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
89
 
 
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]
93
 
 
94
 
        label = self.__label
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
100
 
        else:
101
 
            if isinstance(markup, int): markup = model[path][markup]
102
 
            label.set_markup(markup)
103
 
            w, h1 = label.get_layout().get_pixel_size()
104
 
 
105
 
        if w + 5 < cellw: return # don't display if it doesn't need expansion
106
 
 
107
 
        x, y, cw, h = list(view.get_cell_area(path, col))
108
 
        self.__dx = x
109
 
        self.__dy = y
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)
117
 
        if x_overflow > 0:
118
 
            self.__dx -= x_overflow
119
 
            x -= 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
124
 
 
125
 
        self.__target = view
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)
133
 
        self.resize(w, h)
134
 
        self.move(x, y)
135
 
        self.show_all()
136
 
 
137
 
    def __check_undisplay(self, ev1, event):
138
 
        if self.__time < event.time + 50: self.__undisplay()
139
 
 
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
145
 
        self.hide()
146
 
 
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
151
 
 
152
 
    def __event(self, event):
153
 
        if event.type != gtk.gdk.SCROLL:
154
 
            event.x += self.__dx
155
 
            event.y += self.__dy 
156
 
 
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()
160
 
 
161
 
        gtk.main_do_event(event)
162
 
        return True
163
 
 
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)
168
 
 
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"""
174
 
    
175
 
    def __init__(self, *args):
176
 
        super(MultiDragTreeView, self).__init__(*args)
177
 
        self.connect_object(
178
 
            'button-press-event', MultiDragTreeView.__button_press, self)
179
 
        self.connect_object(
180
 
            'button-release-event', MultiDragTreeView.__button_release, self)
181
 
        self.connect_object('drag-begin', MultiDragTreeView.__begin, self)
182
 
        self.__pending_event = None
183
 
 
184
 
    def __button_press(self, event):
185
 
        if event.button == 1: return self.__block_selection(event)
186
 
 
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
191
 
        self.grab_focus()
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)
200
 
 
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)
212
 
 
213
 
    def __begin(self, ctx):
214
 
        model, paths = self.get_selection().get_selected_rows()
215
 
        MAX = 3
216
 
        if paths:
217
 
            icons = map(self.create_row_drag_icon, paths[:MAX])
218
 
            height = (
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())
225
 
            count_y = 1
226
 
            for icon in icons:
227
 
                w, h = icon.get_size()
228
 
                final.draw_drawable(gc, icon, 1, 1, 1, count_y, w-2, h-2)
229
 
                count_y += h - 2
230
 
            if len(paths) > MAX:
231
 
                count_y -= 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)
243
 
 
244
 
            final.draw_rectangle(gc, False, 0, 0, width-1, height-1)
245
 
            self.drag_source_set_icon(final.get_colormap(), final)
246
 
        else:
247
 
            gobject.idle_add(ctx.drag_abort, gtk.get_current_event_time())
248
 
            self.drag_source_set_icon_stock(gtk.STOCK_MISSING_IMAGE)
249
 
 
250
 
class RCMTreeView(gtk.TreeView):
251
 
    """Emits popup-menu when a row is right-clicked on."""
252
 
 
253
 
    def __init__(self, *args):
254
 
        super(RCMTreeView, self).__init__(*args)
255
 
        self.connect_object(
256
 
            'button-press-event', RCMTreeView.__button_press, self)
257
 
 
258
 
    def __button_press(self, event):
259
 
        if event.button == 3: return self.__check_popup(event)
260
 
 
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
265
 
        self.grab_focus()
266
 
        selection = self.get_selection()
267
 
        if not selection.path_is_selected(path):
268
 
            self.set_cursor(path, col, 0)
269
 
        else:
270
 
            col.focus_cell(col.get_cell_renderers()[0])
271
 
        self.__position_at_mouse = True
272
 
        self.emit('popup-menu')
273
 
        return True
274
 
 
275
 
    def ensure_popup_selection(self):
276
 
        try:
277
 
            self.__position_at_mouse
278
 
        except AttributeError:
279
 
            path, col = self.get_cursor()
280
 
            if path is None:
281
 
                return False
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)
288
 
            return True
289
 
 
290
 
    def popup_menu(self, menu, button, time):
291
 
        try:
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():
296
 
                return False
297
 
            pos_func = self.__popup_position
298
 
        else:
299
 
            pos_func = None
300
 
 
301
 
        menu.popup(None, None, pos_func, button, time)
302
 
        return True
303
 
 
304
 
    def __popup_position(self, menu):
305
 
        path, col = self.get_cursor()
306
 
        if col is None:
307
 
            col = self.get_column(0)
308
 
 
309
 
        # get a rectangle describing the cell render area (assume 3 px pad)
310
 
        rect = self.get_cell_area(path, col)
311
 
        rect.x += 3
312
 
        rect.width -= 6
313
 
        rect.y += 3
314
 
        rect.height -= 6
315
 
        dx, dy = self.window.get_origin()
316
 
        dy += self.get_bin_window().get_position()[1]
317
 
 
318
 
        # fit menu to screen, aligned per text direction
319
 
        screen_width = gtk.gdk.screen_width()
320
 
        screen_height = gtk.gdk.screen_height()
321
 
        menu.realize()
322
 
        ma = menu.allocation
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)
328
 
        else:
329
 
            menu_x = max(0, rect.x + dx - ma.width + rect.width)
330
 
 
331
 
        return (menu_x, menu_y, True) # x, y, move_within_screen
332
 
 
333
 
class HintedTreeView(gtk.TreeView):
334
 
    """A TreeView that pops up a tooltip when you hover over a cell that
335
 
    contains ellipsized text."""
336
 
 
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)
343
 
 
344
 
class TreeViewColumnButton(gtk.TreeViewColumn):
345
 
    """A TreeViewColumn that forwards its header events:
346
 
        button-press-event and popup-menu"""
347
 
 
348
 
    __gsignals__ = {
349
 
        'button-press-event': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
350
 
                (object,)),
351
 
        'popup-menu':  (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
352
 
    }
353
 
 
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)
358
 
        label.show()
359
 
        label.__realize = label.connect('realize', self.__connect_menu_event)
360
 
 
361
 
    def __connect_menu_event(self, widget):
362
 
        widget.disconnect(widget.__realize)
363
 
        del widget.__realize
364
 
        button = widget.get_ancestor(gtk.Button)
365
 
        if button:
366
 
            button.connect('button-press-event', self.button_press_event)
367
 
            button.connect('popup-menu', self.popup_menu)
368
 
 
369
 
    def button_press_event(self, widget, event):
370
 
        self.emit('button-press-event', event)
371
 
 
372
 
    def popup_menu(self, widget):
373
 
        self.emit('popup-menu')
374
 
        return True
375
 
 
376
 
class RCMHintedTreeView(HintedTreeView, RCMTreeView):
377
 
    """A TreeView that has hints and a context menu."""
378
 
    pass
379
 
 
380
 
class AllTreeView(HintedTreeView, RCMTreeView, MultiDragTreeView):
381
 
    """A TreeView that has hints, a context menu, and multi-selection
382
 
    dragging support."""
383
 
    pass