PageRenderTime 75ms CodeModel.GetById 10ms app.highlight 57ms RepoModel.GetById 2ms app.codeStats 0ms

/tortoisehg/hgtk/cslist.py

https://bitbucket.org/tortoisehg/hgtk/
Python | 875 lines | 771 code | 40 blank | 64 comment | 35 complexity | 71340e9dbed71f7b778f12908cb05ade MD5 | raw file
  1# cslist.py - embeddable changeset/patch list component
  2#
  3# Copyright 2009 Yuki KODAMA <endflow.net@gmail.com>
  4#
  5# This software may be used and distributed according to the terms of the
  6# GNU General Public License version 2, incorporated herein by reference.
  7
  8import os
  9import gtk
 10import gobject
 11
 12from mercurial import hg, ui
 13
 14from tortoisehg.util.i18n import _
 15from tortoisehg.util import hglib, paths
 16
 17from tortoisehg.hgtk import csinfo, gtklib
 18
 19CSL_DND_ITEM     = 1024
 20CSL_DND_URI_LIST = 1025
 21
 22ASYNC_LIMIT = 60
 23
 24class ChangesetList(gtk.Frame):
 25
 26    __gsignals__ = {
 27        'list-updated': (gobject.SIGNAL_RUN_FIRST,
 28                         gobject.TYPE_NONE,
 29                         (object, # number of all items or None
 30                          object, # number of selections or None
 31                          object, # number of showings or None
 32                          bool)), # whether cslist is updating
 33        'files-dropped': (gobject.SIGNAL_RUN_FIRST,
 34                          gobject.TYPE_NONE,
 35                          (object, # list of dropped files
 36                           str)),  # raw string data
 37        'item-activated': (gobject.SIGNAL_RUN_FIRST,
 38                           gobject.TYPE_NONE,
 39                           (str,     # revision number or patch file path
 40                            object)) # reference of csinfo widget
 41    }
 42
 43    def __init__(self):
 44        gtk.Frame.__init__(self)
 45        self.set_shadow_type(gtk.SHADOW_IN)
 46
 47        # member variables
 48        self.curitems = None
 49        self.currepo = None
 50        self.showitems = None
 51        self.chkmap = {}
 52        self.limit = 20
 53        self.curfactory = None
 54
 55        self.timeout_queue = []
 56        self.sel_enable = False
 57        self.dnd_enable = False
 58        self.act_enable = False
 59
 60        # dnd variables
 61        self.itemmap = {}
 62        self.hlsep = None
 63        self.dnd_pb = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, True, 8, 1, 1)
 64        self.scroll_timer = None
 65
 66        # base box
 67        basebox = gtk.VBox()
 68        self.add(basebox)
 69
 70        ## status box
 71        self.statusbox = statusbox = gtk.HBox()
 72        basebox.pack_start(statusbox, False, False)
 73        basebox.pack_start(gtk.HSeparator(), False, False, 2)
 74
 75        # copy form thgstrip.py
 76        def createlabel():
 77            label = gtk.Label()
 78            label.set_alignment(0, 0.5)
 79            label.set_size_request(-1, 24)
 80            label.size_request()
 81            return label
 82
 83        ### status label
 84        self.statuslabel = createlabel()
 85        statusbox.pack_start(self.statuslabel, False, False, 2)
 86
 87        ### show all button
 88        self.allbtn = gtk.Button(_('Show all')) # add later
 89
 90        ### list option
 91        self.compactopt = gtk.CheckButton(_('Use compact view'))
 92        statusbox.pack_end(self.compactopt, False, False, 2)
 93
 94        ## item list
 95        scroll = gtk.ScrolledWindow()
 96        basebox.add(scroll)
 97        self.scroll = scroll
 98        scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
 99        scroll.set_size_request(400, 180)
100        scroll.size_request()
101        self.csbox = gtk.VBox()
102        self.csevent = csevent = gtk.EventBox()
103        csevent.add(self.csbox)
104        csevent.add_events(gtk.gdk.BUTTON_PRESS_MASK |
105                           gtk.gdk.BUTTON_RELEASE_MASK)
106        scroll.add_with_viewport(csevent)
107        scroll.child.set_shadow_type(gtk.SHADOW_NONE)
108        self.csbox.set_border_width(4)
109
110        # signal handlers
111        self.allbtn.connect('clicked', lambda b: self.expand_items())
112        self.compactopt.connect('toggled', lambda b: self.update( \
113                self.curitems, self.currepo, queue=False, keep=True))
114
115        # dnd setup
116        self.dnd_targets = [('thg-dnd', gtk.TARGET_SAME_WIDGET, CSL_DND_ITEM)]
117        targets = self.dnd_targets + [('text/uri-list', 0, CSL_DND_URI_LIST)]
118        csevent.drag_dest_set(gtk.DEST_DEFAULT_MOTION | gtk.DEST_DEFAULT_DROP,
119                              targets, gtk.gdk.ACTION_MOVE)
120        csevent.connect('drag-begin', self.dnd_begin)
121        csevent.connect('drag-end', self.dnd_end)
122        csevent.connect('drag-motion', self.dnd_motion)
123        csevent.connect('drag-leave', self.dnd_leave)
124        csevent.connect('drag-data-received', self.dnd_received)
125        csevent.connect('drag-data-get', self.dnd_get)
126        csevent.connect('button-press-event', self.button_press)
127
128        # csetinfo
129        def data_func(widget, item, ctx):
130            if item in ('item', 'item_l'):
131                if not isinstance(ctx, csinfo.patchctx):
132                    return True # dummy
133                revid = widget.get_data('revid')
134                if not revid:
135                    return widget.target
136                filename = os.path.basename(widget.target)
137                return filename, revid
138            raise csinfo.UnknownItem(item)
139        def label_func(widget, item):
140            if item in ('item', 'item_l'):
141                if not isinstance(widget.ctx, csinfo.patchctx):
142                    return _('Revision:')
143                return _('Patch:')
144            raise csinfo.UnknownItem(item)
145        def markup_func(widget, item, value):
146            if item in ('item', 'item_l'):
147                if not isinstance(widget.ctx, csinfo.patchctx):
148                    if item == 'item':
149                        return widget.get_markup('rev')
150                    return widget.get_markup('revnum')
151                mono = dict(face='monospace', size='9000')
152                if isinstance(value, basestring):
153                    return gtklib.markup(value, **mono)
154                filename = gtklib.markup(value[0])
155                revid = gtklib.markup(value[1], **mono)
156                if item == 'item':
157                    return '%s (%s)' % (filename, revid)
158                return filename
159            raise csinfo.UnknownItem(item)
160        self.custom = csinfo.custom(data=data_func, label=label_func,
161                                    markup=markup_func)
162        self.lstyle = csinfo.labelstyle(
163                             contents=('%(item_l)s:', ' %(branch)s',
164                                       ' %(tags)s', ' %(summary)s'))
165        self.pstyle = csinfo.panelstyle(
166                             contents=('item', 'summary', 'user','dateage',
167                                       'rawbranch', 'tags', 'transplant',
168                                       'p4', 'svn'))
169
170        # prepare to show
171        gtklib.idle_add_single_call(self.after_init)
172
173    ### public functions ###
174
175    def update(self, items=None, repo=None, limit=True, queue=False, **kargs):
176        """
177        Update the item list.
178
179        Public arguments:
180        items: List of revision numbers and/or patch file path.
181               You can pass mixed list.  The order will be respected.
182               If omitted, previous items will be used to show them.
183               Default: None.
184        repo: Repository used to get changeset information.
185              If omitted, previous repo will be used to show them.
186              Default: None.
187        limit: If True, some of items will be shown.  Default: True.
188        queue: If True, the update request will be queued to prevent
189               frequent updatings.  In some cases, this option will help
190               to improve UI response.  Default: False.
191
192        Internal argument:
193        keep: If True, it keeps previous selection states and 'limit' value
194              after refreshing.  Note that if you use 'limit' and this options
195              at the same time, 'limit' value is used against previous value.
196              Default: False.
197
198        return: True if the item list was updated successfully,
199                False if it wasn't updated.
200        """
201        # check parameters
202        if not items or not repo:
203            self.clear()
204            return False
205        elif queue:
206            def timeout(eid, items, repo):
207                if self.timeout_queue and self.timeout_queue[-1] == eid[0]:
208                    self.timeout_queue = []
209                    self.update(items, repo, limit, False)
210                return False # don't repeat
211            eid = [None]
212            eid[0] = gobject.timeout_add(650, timeout, eid, items, repo)
213            self.timeout_queue.append(eid[0])
214            return False
215
216        # determine whether to keep previous 'limit' state
217        if kargs.get('keep', False) and self.has_limit() is False:
218            limit = False
219
220        # initialize variables
221        self.curitems = items
222        self.currepo = repo
223        self.itemmap = {}
224
225        if self.sel_enable and not kargs.get('keep', False):
226            self.chkmap = {}
227            for item in items:
228                self.chkmap[item] = True
229
230        # determine items to show
231        numtotal = len(items)
232        if limit and self.limit < numtotal:
233            toshow, lastitem = items[:self.limit-1], items[-1]
234        else:
235            toshow, lastitem = items, None
236        numshow = len(toshow) + (lastitem and 1 or 0)
237        self.showitems = toshow + (lastitem and [lastitem] or [])
238
239        # prepare to update item list
240        self.curfactory = csinfo.factory(repo, self.custom, withupdate=True)
241
242        def add_sep():
243            sep = self.create_sep()
244            self.csbox.pack_start(sep, False, False)
245
246        # clear item list
247        self.csbox.foreach(lambda c: c.parent.remove(c))
248
249        # update item list
250        def proc():
251            # add csinfo widgets
252            for index, r in enumerate(toshow):
253                self.add_csinfo(r)
254            if lastitem:
255                self.add_snip()
256                self.add_csinfo(lastitem)
257            add_sep()
258            self.csbox.show_all()
259
260            # show/hide separators
261            self.update_seps()
262
263            # update status
264            self.update_status()
265
266        # determine doing it now or later
267        if numshow < ASYNC_LIMIT:
268            proc()
269        else:
270            self.update_status(updating=True)
271            gtklib.idle_add_single_call(proc)
272
273        return True
274
275    def clear(self):
276        """ Clear the item list  """
277        self.csbox.foreach(lambda c: c.parent.remove(c))
278        self.curitems = None
279        self.update_status()
280
281    def get_items(self, sel=False):
282        """
283        Return a list of items or tuples contained 2 values:
284        'item' (String) and 'selection state' (Boolean).
285        If cslist lists no items, it returns an empty list.
286
287        sel: If True, it returns a list of tuples.  Default: False.
288        """
289        items = self.curitems
290        if items:
291            if not sel:
292                return items
293            return [(item, self.chkmap[item]) for item in items]
294        return []
295
296    def get_list_limit(self):
297        """ Return number of items to limit to display """
298        return self.limit
299
300    def set_list_limit(self, limit):
301        """
302        Set number of items to limit to display.
303
304        limit: Integer, must be more than 3.  Default: 20.
305        """
306        if limit < 3:
307            limit = 3
308        self.limit = limit
309
310    def get_dnd_enable(self):
311        """ Return whether drag and drop feature is enabled """
312        return self.dnd_enable
313
314    def set_dnd_enable(self, enable):
315        """
316        Set whether drag and drop feature is enabled.
317
318        enable: Boolean, if True, drag and drop feature will be enabled.
319                Default: False.
320        """
321        self.dnd_enable = enable
322
323    def get_checkbox_enable(self):
324        """ Return whether the selection feature is enabled """
325        return self.sel_enable
326
327    def set_checkbox_enable(self, enable):
328        """
329        Set whether the selection feature is enabled.
330        When it's enabled, checboxes will be placed at the left of
331        csinfo widgets.
332
333        enable: Boolean, if True, the selection feature will be enabled.
334                Default: False.
335        """
336        self.sel_enable = enable
337
338    def get_activatable_enable(self):
339        """ Return whether items are activatable """
340        return self.act_enable
341
342    def set_activatable_enable(self, enable):
343        """
344        Set whether items are activatable.
345        By enabling this, items in the list will be emitted 'item-activated'
346        signal when the user double-clicked on the list.
347
348        enable: Boolean, if True, items will be selectable.  Default: False.
349        """
350        self.act_enable = enable
351
352    def get_compact_view(self):
353        """ Return whether the compact view is enabled """
354        return self.compactopt.get_active()
355
356    def set_compact_view(self, compact):
357        """
358        Set whether the compact view is enabled.
359
360        enable: Boolean, if True, the compact view will be enabled.
361                Default: False.
362        """
363        self.compactopt.set_active(compact)
364
365    def has_limit(self):
366        """
367        Return whether the item list shows all items.
368        If the item list has no items, it will return None.
369        """
370        if self.curitems:
371            num = len(self.curitems)
372            return self.limit < num and len(self.showitems) != num
373        return None
374
375    ### internal functions ###
376
377    def after_init(self):
378        self.statusbox.pack_start(self.allbtn, False, False, 4)
379
380    def update_status(self, updating=False):
381        numshow = numsel = numtotal = all = None
382        if self.curitems is None:
383            button = False
384            text = _('No items to display')
385        else:
386            # prepare data
387            numshow, numtotal = len(self.showitems), len(self.curitems)
388            data = dict(count=numshow, total=numtotal)
389            if self.sel_enable:
390                items = self.get_items(sel=True)
391                numsel = len([item for item, sel in items if sel])
392                data['sel'] = numsel
393            all = data['count'] == data['total']
394            button = not all
395            # generate status text
396            if updating:
397                text = _('Updating...')
398            elif self.sel_enable:
399                if all:
400                    text = _('Selecting %(sel)d of %(total)d, displaying '
401                             'all items') % data
402                else:
403                    text = _('Selecting %(sel)d, displaying %(count)d of '
404                             '%(total)d items') % data
405            else:
406                if all:
407                    text = _('Displaying all items')
408                else:
409                    text = _('Displaying %(count)d of %(total)d items') % data
410        self.statuslabel.set_text(text)
411        self.allbtn.set_property('visible', button)
412        self.emit('list-updated', numtotal, numsel, numshow, updating)
413
414    def setup_dnd(self, restart=False):
415        if not restart and self.scroll_timer is None:
416            self.scroll_timer = gobject.timeout_add(25, self.scroll_timeout)
417
418    def teardown_dnd(self, pause=False):
419        first = self.get_sep(0)
420        if first:
421            first.set_visible(False)
422        last = self.get_sep(-1)
423        if last:
424            last.set_visible(False)
425        if self.hlsep:
426            self.hlsep.drag_unhighlight()
427            self.hlsep = None
428        if not pause and self.scroll_timer:
429            gobject.source_remove(self.scroll_timer)
430            self.scroll_timer = None
431
432    def get_item_pos(self, y, detail=False):
433        pos = None
434        items = self.curitems
435        num = len(items)
436        numshow = len(self.showitems)
437        first = self.itemmap[items[0]]
438        beforesnip = self.itemmap[items[numshow - 2]]
439        snip = self.has_limit() and self.itemmap['snip'] or None
440        last = self.itemmap[items[-1]]
441        def calc_ratio(geom):
442            return (y - geom['y']) / float(geom['height'])
443        if y < first['y']:
444            start, end = -1, 0
445        elif last['bottom'] < y:
446            start, end = num - 1, num
447        elif snip and beforesnip['bottom'] < y and y < last['y']:
448            ratio = calc_ratio(snip)
449            if ratio < 0.5:
450                start, end = numshow - 2, numshow - 1
451            else:
452                start, end = num - 2, num - 1
453        else:
454            # calc item showitems pos (binary search)
455            def mid(start, end):
456                return (start + end) / 2
457            start, end = 0, numshow - 1
458            pos = mid(start, end)
459            while start < end:
460                data = self.itemmap[self.showitems[pos]]
461                if y < data['y']:
462                    end = pos - 1
463                elif data['bottom'] < y:
464                    start = pos + 1
465                else:
466                    break
467                pos = mid(start, end)
468            # translate to curitems pos
469            pos = self.trans_to_cur(pos)
470            # calc detailed pos if need
471            if detail:
472                data = self.itemmap[items[pos]]
473                ratio = calc_ratio(data)
474                if ratio < 0.5:
475                    start, end = pos - 1, pos
476                else:
477                    start, end = pos, pos + 1
478        if detail:
479            return pos, start, end
480        return pos
481
482    def get_sep(self, pos):
483        """
484        pos: Number, the position of separator you need.
485             If -1 or list length, indicates the last separator.
486        """
487        # invalid position/condition
488        if pos < -1 or not self.showitems:
489            return None
490        def get_last():
491            child = self.csbox.get_children()[-1]
492            return isinstance(child, FixedHSeparator) and child or None
493        # last separator
494        if pos == -1:
495            return get_last()
496        # limiting case
497        if self.has_limit():
498            # snip box separator
499            if pos == self.limit - 1:
500                return self.itemmap['snip']['sep']
501            # list length (+ snip box)
502            if pos == self.limit + 1:
503                return get_last()
504            # separators after snip box
505            if self.limit - 1 < pos:
506                return self.itemmap[self.showitems[pos-1]]['sep']
507        # list length
508        elif pos == len(self.showitems):
509            return get_last()
510        # others
511        return self.itemmap[self.showitems[pos]]['sep']
512
513    def get_sep_by_y(self, y):
514        pos, start, end = self.get_item_pos(y, detail=True)
515        if self.has_limit() and self.limit - 1 < end:
516            end -= len(self.curitems) - self.limit - 1
517        return self.get_sep(end)
518
519    def update_seps(self):
520        """ Update visibility of all separators """
521        compact = self.get_compact_view()
522        for item in self.showitems[1:]:
523            sep = self.itemmap[item]['sep']
524            sep.set_visible(not compact)
525        if self.has_limit():
526            self.itemmap['snip']['sep'].set_visible(False)
527            self.itemmap[self.showitems[-1]]['sep'].set_visible(False)
528        self.get_sep(0).set_visible(False)
529        self.get_sep(-1).set_visible(False)
530
531    def expand_items(self):
532        if not self.has_limit():
533            return
534
535        # fix up snipped items
536        rest = self.curitems[self.limit - 1:-1]
537
538        def proc():
539            # insert snipped csinfo
540            for pos, item in enumerate(rest):
541                self.insert_csinfo(item, self.limit + pos)
542            # remove snip
543            self.remove_snip()
544
545            self.showitems = self.curitems[:]
546            self.update_seps()
547            self.update_status()
548
549        # determine doing it now or later
550        if len(rest) < ASYNC_LIMIT:
551            proc()
552        else:
553            self.update_status(updating=True)
554            gtklib.idle_add_single_call(proc)
555
556    def reorder_item(self, pos, insert):
557        """
558        pos: Number, the position of item to move. This must be curitems
559             index, not showitems index.
560        insert: Number, the new position to insert target item.
561                If list length, indicates the end of the list.
562                This must be curitems index, not showitems index.
563        """
564        # reject unneeded reordering
565        if pos == insert or pos + 1 == insert:
566            return
567
568        # reorder target csinfo
569        if self.has_limit() and self.limit - 1 <= pos:
570            item = self.curitems[pos]
571            if insert < self.limit - 1:
572                # move target csinfo to insert pos
573                target = self.itemmap[item]['widget']
574                self.csbox.reorder_child(target, insert)
575
576                # remove csinfo to be snipped
577                item = self.showitems[-2]
578                self.remove_csinfo(item)
579            else:
580                # remove target csinfo
581                self.remove_csinfo(item)
582
583            # insert csinfo the end of the item list
584            item = self.curitems[-2]
585            self.insert_csinfo(item, -1)
586
587        elif self.has_limit() and self.limit - 1 < insert:
588            if self.trans_to_show(insert) < self.limit:
589                # remove target csinfo
590                rm_item = self.curitems[pos]
591            else:
592                # move target csinfo to the end of VBox
593                item = self.curitems[pos]
594                target = self.itemmap[item]['widget']
595                numc = len(self.csbox.get_children())
596                self.csbox.reorder_child(target, numc - 2)
597
598                # remove last csinfo
599                rm_item = self.showitems[-1]
600
601            # remove it
602            self.remove_csinfo(rm_item)
603
604            # insert csinfo before snip box
605            item = self.curitems[self.limit - 1]
606            self.insert_csinfo(item, self.limit - 1)
607        else:
608            info = self.itemmap[self.showitems[pos]]['widget']
609            if insert < pos:
610                self.csbox.reorder_child(info, insert)
611            else:
612                self.csbox.reorder_child(info, insert - 1)
613
614        # reorder curitems
615        item = self.curitems[pos]
616        items = self.curitems[:pos] + self.curitems[pos+1:]
617        if insert < pos:
618            items.insert(insert, item)
619        else:
620            items.insert(insert - 1, item)
621        self.curitems = items
622
623        # reorder showitems
624        if self.has_limit():
625            self.showitems = items[:self.limit-1] + [items[-1]]
626        else:
627            self.showitems = items
628
629        # show/hide separators
630        self.update_seps()
631
632        # just emit 'list-updated' signal
633        self.update_status()
634
635    def trans_to_show(self, index):
636        """ Translate from curitems index to showitems index """
637        numrest = len(self.curitems) - self.limit
638        if self.has_limit() and numrest <= index:
639            return index - numrest
640        return index
641
642    def trans_to_cur(self, index):
643        """ Translate from showitems index to curitems index """
644        if self.has_limit() and self.limit - 1 <= index:
645            return index + len(self.curitems) - self.limit
646        return index
647
648    def create_sep(self):
649        return FixedHSeparator()
650
651    def add_csinfo(self, item):
652        self.insert_csinfo(item, -1)
653
654    def insert_csinfo(self, item, pos):
655        """
656        item: String, revision number or patch file path to display.
657        pos: Number, an index of insertion point.  If -1, indicates
658        the end of the item list.
659        """
660        # create csinfo
661        wrapbox = gtk.VBox()
662        sep = self.create_sep()
663        wrapbox.pack_start(sep, False, False)
664        style = self.get_compact_view() and self.lstyle or self.pstyle
665        if self.dnd_enable:
666            style['selectable'] = False
667        info = self.curfactory(item, style)
668        if self.sel_enable:
669            check = gtk.CheckButton()
670            check.set_active(self.chkmap[item])
671            check.connect('toggled', self.check_toggled, item)
672            align = gtk.Alignment(0.5, 0)
673            align.add(check)
674            hbox = gtk.HBox()
675            hbox.pack_start(align, False, False)
676            hbox.pack_start(info, False, False)
677            info = hbox
678        wrapbox.pack_start(info, False, False)
679        wrapbox.show_all()
680        self.csbox.pack_start(wrapbox, False, False)
681        self.itemmap[item] = {'widget': wrapbox,
682                              'info': info,
683                              'sep': sep}
684
685        # reorder it
686        children = self.csbox.get_children()
687        if 1 < len(children) and isinstance(children[-2], FixedHSeparator):
688            if pos == -1:
689                numc = len(children)
690                pos = numc - 2
691            elif self.has_limit():
692                pos = pos - 1
693            self.csbox.reorder_child(wrapbox, pos)
694
695    def remove_csinfo(self, item):
696        info = self.itemmap[item]['widget']
697        self.csbox.remove(info)
698        del self.itemmap[item]
699
700    def add_snip(self):
701        wrapbox = gtk.VBox()
702        sep = self.create_sep()
703        wrapbox.pack_start(sep, False, False)
704        snipbox = gtk.HBox()
705        wrapbox.pack_start(snipbox, False, False)
706        spacer = gtk.Label()
707        snipbox.pack_start(spacer, False, False)
708        spacer.set_width_chars(24)
709        sniplbl = gtk.Label()
710        snipbox.pack_start(sniplbl, False, False)
711        sniplbl.set_markup('<span size="large" weight="heavy"'
712                           ' font_family="monospace">...</span>')
713        sniplbl.set_angle(90)
714        snipbox.pack_start(gtk.Label())
715        self.csbox.pack_start(wrapbox, False, False, 2)
716        self.itemmap['snip'] = {'widget': wrapbox,
717                                'snip': snipbox,
718                                'sep': sep}
719
720    def remove_snip(self):
721        if not self.has_limit():
722            return
723        snip = self.itemmap['snip']['widget']
724        self.csbox.remove(snip)
725        del self.itemmap['snip']
726
727    ### signal handlers ###
728
729    def check_toggled(self, button, item):
730        self.chkmap[item] = button.get_active()
731        self.update_status()
732
733    def allbtn_clicked(self, button):
734        self.update(self.curitems, self.currepo, limit=False,
735                    queue=False, keep=True)
736
737    ### dnd signal handlers ###
738
739    def dnd_begin(self, widget, context):
740        self.setup_dnd()
741        context.set_icon_pixbuf(self.dnd_pb, 0, 0)
742
743    def dnd_end(self, widget, context):
744        self.teardown_dnd()
745
746    def dnd_motion(self, widget, context, x, y, event_time):
747        if hasattr(self, 'item_drag') and self.item_drag is not None:
748            num = len(self.curitems)
749            if not self.hlsep:
750                self.setup_dnd(restart=True)
751            # highlight separator
752            sep = self.get_sep_by_y(y)
753            first = self.get_sep(0)
754            first.set_visible(first == sep)
755            last = self.get_sep(-1)
756            last.set_visible(last == sep)
757            if self.hlsep != sep:
758                if self.hlsep:
759                    self.hlsep.drag_unhighlight()
760                sep.drag_highlight()
761                self.hlsep = sep
762
763    def dnd_leave(self, widget, context, event_time):
764        self.teardown_dnd(pause=True)
765
766    def dnd_received(self, widget, context, x, y, sel, target_type, *args):
767        if target_type == CSL_DND_ITEM:
768            items = self.curitems
769            pos, start, end = self.get_item_pos(y, detail=True)
770            self.reorder_item(self.item_drag, end)
771        elif target_type == CSL_DND_URI_LIST:
772            paths = gtklib.normalize_dnd_paths(sel.data)
773            if paths:
774                self.emit('files-dropped', paths, sel.data)
775
776    def dnd_get(self, widget, context, sel, target_type, event_time):
777        pos = self.item_drag
778        if target_type == CSL_DND_ITEM and pos is not None:
779            sel.set(sel.target, 8, str(self.curitems[pos]))
780
781    def button_press(self, widget, event):
782        if not self.curitems:
783            return
784        # gather geometry data
785        items = self.showitems
786        if self.has_limit():
787            items.append('snip')
788        for item in items:
789            data = self.itemmap[item]
790            alloc = data['widget'].allocation
791            data.update(y=alloc.y, height=alloc.height)
792            data['bottom'] = alloc.y + alloc.height
793        # get pressed csinfo widget based on pointer position
794        pos = self.get_item_pos(event.y)
795        if pos is None:
796            return
797        # emit activated signal
798        if self.act_enable and event.type == gtk.gdk._2BUTTON_PRESS:
799            item = self.curitems[pos]
800            self.emit('item-activated', item, self.itemmap[item]['widget'])
801        # dnd setup
802        if self.dnd_enable and event.type == gtk.gdk.BUTTON_PRESS \
803                           and 1 < len(self.curitems):
804            # prepare for dnd auto-scrolling
805            W = 20
806            alloc = self.scroll.child.allocation
807            self.areas = {}
808            def add(name, arg):
809                region = gtk.gdk.region_rectangle(arg)
810                self.areas[name] = (region, gtk.gdk.Rectangle(*arg))
811            add('top', (0, 0, alloc.width, W))
812            add('right', (alloc.width - W, 0, W, alloc.height))
813            add('bottom', (0, alloc.height - W, alloc.width, W))
814            add('left', (0, 0, W, alloc.height))
815            add('center', (W, W, alloc.width - 2 * W, alloc.height - 2 * W))
816            # start dnd
817            self.item_drag = pos
818            self.csevent.drag_begin(self.dnd_targets,
819                                    gtk.gdk.ACTION_MOVE, 1, event)
820
821    def scroll_timeout(self):
822        x, y = self.scroll.get_pointer()
823        if not self.areas['center'][0].point_in(x, y):
824            def hscroll(left=False, fast=False):
825                amount = 2
826                if left: amount *= -1
827                if fast: amount *= 3
828                hadj = self.scroll.get_hadjustment()
829                hadj.set_value(hadj.get_value() + amount)
830            def vscroll(up=False, fast=False):
831                amount = 2
832                if up: amount *= -1
833                if fast: amount *= 3
834                vadj = self.scroll.get_vadjustment()
835                vadj.set_value(vadj.get_value() + amount)
836            top, topr = self.areas['top']
837            bottom, bottomr = self.areas['bottom']
838            if y < topr.y:
839                vscroll(up=True, fast=True)
840            elif top.point_in(x, y):
841                vscroll(up=True)
842            elif (bottomr.y + bottomr.height) < y:
843                vscroll(fast=True)
844            elif bottom.point_in(x, y):
845                vscroll()
846            left, leftr = self.areas['left']
847            right, rightr = self.areas['right']
848            if x < leftr.x:
849                hscroll(left=True, fast=True)
850            elif left.point_in(x, y):
851                hscroll(left=True)
852            elif (rightr.x + rightr.width) < x:
853                hscroll(fast=True)
854            elif right.point_in(x, y):
855                hscroll()
856        return True # repeat
857
858class FixedHSeparator(gtk.VBox):
859
860    def __init__(self, visible=True):
861        gtk.VBox.__init__(self)
862        self.set_size_request(-1, 2)
863
864        self.visible = visible
865
866        self.sep = gtk.HSeparator()
867        self.pack_start(self.sep, False, False)
868        self.sep.set_no_show_all(not visible)
869
870    def set_visible(self, visible):
871        if self.visible != visible:
872            self.visible = visible
873            self.sep.set_no_show_all(False)
874            self.sep.set_property('visible', visible)
875            self.sep.set_no_show_all(not visible)