PageRenderTime 148ms CodeModel.GetById 33ms app.highlight 106ms RepoModel.GetById 1ms app.codeStats 1ms

/tortoisehg/hgtk/datamine.py

https://bitbucket.org/tortoisehg/hgtk/
Python | 875 lines | 797 code | 49 blank | 29 comment | 36 complexity | 9606a7bf6062f21b157913d5480b3d4f MD5 | raw file
  1# datamine.py - Data Mining dialog for TortoiseHg
  2#
  3# Copyright 2008 Steve Borho <steve@borho.org>
  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 gtk
  9import gobject
 10import os
 11import pango
 12import Queue
 13import threading
 14import re
 15
 16from mercurial import util, error
 17
 18from tortoisehg.util.i18n import _
 19from tortoisehg.util import hglib, thread2
 20
 21from tortoisehg.util.colormap import AnnotateColorMap
 22from tortoisehg.util.colormap import AnnotateColorSaturation
 23from tortoisehg.hgtk.logview.treeview import TreeView as LogTreeView
 24from tortoisehg.hgtk.logview import treemodel as LogTreeModelModule
 25from tortoisehg.hgtk import gtklib, gdialog, changeset, statusbar, csinfo
 26
 27# Column indexes for grep
 28GCOL_REVID = 0
 29GCOL_LINE  = 1 # matched line
 30GCOL_DESC  = 2 # utf-8, escaped summary
 31GCOL_PATH  = 3
 32
 33# Column indexes for annotation
 34ACOL_REVID = 0
 35ACOL_LINE  = 1 # file line
 36ACOL_DESC  = 2 # utf-8, escaped summary
 37ACOL_PATH  = 3
 38ACOL_COLOR = 4
 39ACOL_USER  = 5
 40ACOL_LNUM  = 6 # line number
 41
 42class DataMineDialog(gdialog.GWindow):
 43
 44    def get_title(self):
 45        return _('%s - datamine') % self.get_reponame()
 46
 47    def get_icon(self):
 48        return 'menurepobrowse.ico'
 49
 50    def parse_opts(self):
 51        pass
 52
 53    def get_tbbuttons(self):
 54        self.stop_button = self.make_toolbutton(gtk.STOCK_STOP, _('Stop'),
 55                self.stop_current_search,
 56                tip=_('Stop operation on current tab'))
 57        return [
 58            self.make_toolbutton(gtk.STOCK_FIND, _('New Search'),
 59                self.search_clicked,
 60                tip=_('Open new search tab')),
 61            self.stop_button
 62            ]
 63
 64    def prepare_display(self):
 65        root = self.repo.root
 66        cf = []
 67        for f in self.pats:
 68            try:
 69                if os.path.isfile(f):
 70                    cf.append(util.canonpath(root, self.cwd, f))
 71                elif os.path.isdir(f):
 72                    for fn in os.listdir(f):
 73                        fname = os.path.join(f, fn)
 74                        if not os.path.isfile(fname):
 75                            continue
 76                        cf.append(util.canonpath(root, self.cwd, fname))
 77            except util.Abort:
 78                pass
 79        for f in cf:
 80            self.add_annotate_page(f, '.')
 81        if not self.notebook.get_n_pages():
 82            self.add_search_page()
 83        os.chdir(root)
 84
 85    def save_settings(self):
 86        settings = gdialog.GWindow.save_settings(self)
 87        return settings
 88
 89    def load_settings(self, settings):
 90        gdialog.GWindow.load_settings(self, settings)
 91        self.tabwidth = hglib.gettabwidth(self.repo.ui)
 92
 93    def get_body(self):
 94        """ Initialize the Dialog. """
 95        self.grep_cmenu = self.grep_context_menu()
 96        self.changedesc = {}
 97        self.newpagecount = 1
 98        self.currev = None
 99        vbox = gtk.VBox()
100        notebook = gtk.Notebook()
101        notebook.set_tab_pos(gtk.POS_TOP)
102        notebook.set_scrollable(True)
103        notebook.popup_enable()
104        notebook.show()
105        self.notebook = notebook
106        vbox.pack_start(self.notebook, True, True, 2)
107
108        self.stop_button.set_sensitive(False)
109
110        accelgroup = gtk.AccelGroup()
111        self.add_accel_group(accelgroup)
112        mod = gtklib.get_thg_modifier()
113        key, modifier = gtk.accelerator_parse(mod+'w')
114        notebook.add_accelerator('thg-close', accelgroup, key,
115                        modifier, gtk.ACCEL_VISIBLE)
116        notebook.connect('thg-close', self.close_notebook)
117        key, modifier = gtk.accelerator_parse(mod+'n')
118        notebook.add_accelerator('thg-new', accelgroup, key,
119                        modifier, gtk.ACCEL_VISIBLE)
120        notebook.connect('thg-new', self.new_notebook)
121
122        # status bar
123        hbox = gtk.HBox()
124        style = csinfo.labelstyle(contents=('%(shortuser)s@%(revnum)s '
125                       '%(dateage)s', ' "%(summary)s"',), selectable=True)
126        self.cslabel = csinfo.create(self.repo, style=style)
127        hbox.pack_start(self.cslabel, False, False, 4)
128        self.stbar = statusbar.StatusBar()
129        hbox.pack_start(self.stbar)
130        vbox.pack_start(hbox, False, False)
131
132        return vbox
133
134    def _destroying(self, gtkobj):
135        self.stop_all_searches()
136        gdialog.GWindow._destroying(self, gtkobj)
137
138    def ann_header_context_menu(self, treeview):
139        m = gtklib.MenuBuilder()
140        m.append(_('Filename'), self.toggle_annatate_columns,
141                 ascheck=True, args=[treeview, 2])
142        m.append(_('User'), self.toggle_annatate_columns,
143                 ascheck=True, args=[treeview, 3])
144        menu = m.build()
145        menu.show_all()
146        return menu
147
148    def grep_context_menu(self):
149        m = gtklib.MenuBuilder()
150        m.append(_('Di_splay Change'), self.cmenu_display,
151                 'menushowchanged.ico')
152        m.append(_('_Annotate File'), self.cmenu_annotate, 'menublame.ico')
153        m.append(_('_File History'), self.cmenu_file_log, 'menulog.ico')
154        m.append(_('_View File at Revision'), self.cmenu_view, gtk.STOCK_EDIT)
155        menu = m.build()
156        menu.show_all()
157        return menu
158
159    def annotate_context_menu(self, objs):
160        m = gtklib.MenuBuilder()
161        m.append(_('_Zoom to Change'), self.cmenu_zoom, gtk.STOCK_ZOOM_IN,
162                 args=[objs])
163        m.append(_('Di_splay Change'), self.cmenu_display,
164                 'menushowchanged.ico')
165        (frame, treeview, filepath, graphview) = objs
166        path = graphview.get_path_at_revid(int(self.currev))
167        filepath = graphview.get_wfile_at_path(path)
168        ctx = self.repo[self.currev]
169        fctx = ctx.filectx(filepath)
170        parents = fctx.parents()
171        if len(parents) > 0:
172            if len(parents) == 1:
173                m.append(_('_Annotate Parent'), self.cmenu_annotate_1st_parent,
174                         'menublame.ico', args=[objs])
175            else:
176                m.append(_('_Annotate First Parent'), self.cmenu_annotate_1st_parent,
177                         'menublame.ico', args=[objs])
178                m.append(_('Annotate Second Parent'), self.cmenu_annotate_2nd_parent,
179                         'menublame.ico', args=[objs])
180        m.append(_('_View File at Revision'), self.cmenu_view, gtk.STOCK_EDIT)
181        m.append(_('_File History'), self.cmenu_file_log, 'menulog.ico')
182        m.append(_('_Diff to Local'), self.cmenu_local_diff)
183        menu = m.build()
184        menu.show_all()
185        return menu
186
187    def cmenu_zoom(self, menuitem, objs):
188        (frame, treeview, path, graphview) = objs
189        graphview.scroll_to_revision(int(self.currev))
190        graphview.set_revision_id(int(self.currev))
191
192    def cmenu_display(self, menuitem):
193        statopts = {'rev' : [self.currev] }
194        dlg = changeset.ChangeSet(self.ui, self.repo, self.cwd, [], statopts)
195        dlg.display()
196
197    def cmenu_view(self, menuitem):
198        self._node2 = self.currev
199        self._view_files([self.curpath], False)
200
201    def cmenu_annotate(self, menuitem):
202        self.add_annotate_page(self.curpath, self.currev)
203
204    def cmenu_annotate_1st_parent(self, menuitem, objs):
205        self.annotate_parent(objs, 0)
206
207    def cmenu_annotate_2nd_parent(self, menuitem, objs):
208        self.annotate_parent(objs, 1)
209
210    def annotate_parent(self, objs, parent_idx):
211        (frame, treeview, filepath, graphview) = objs
212        path = graphview.get_path_at_revid(int(self.currev))
213        filepath = graphview.get_wfile_at_path(path)
214        ctx = self.repo[self.currev]
215        fctx = ctx.filectx(filepath)
216        parents = fctx.parents()
217        parent_fctx = parents[parent_idx]
218        parent_revid = parent_fctx.changectx().rev()
219        filepath = parent_fctx.path()
220        # annotate file of parent rev
221        self.trigger_annotate(parent_revid, filepath, objs)
222        graphview.scroll_to_revision(int(parent_revid))
223        graphview.set_revision_id(int(parent_revid))
224
225    def cmenu_local_diff(self, menuitem):
226        opts = {'rev':[str(self.currev)], 'bundle':None}
227        self._do_diff([self.curpath], opts)
228
229    def cmenu_file_log(self, menuitem):
230        from tortoisehg.hgtk import history
231        dlg = history.run(self.ui, filehist=self.curpath)
232        dlg.display()
233
234    def grep_button_release(self, widget, event):
235        if event.button == 3 and not (event.state & (gtk.gdk.SHIFT_MASK |
236            gtk.gdk.CONTROL_MASK)):
237            self.grep_popup_menu(widget, event.button, event.time)
238        return False
239
240    def grep_popup_menu(self, treeview, button=0, time=0):
241        self.grep_cmenu.popup(None, None, None, button, time)
242        return True
243
244    def grep_thgdiff(self, treeview):
245        if self.currev:
246            self._do_diff([], {'change' : self.currev})
247
248    def grep_row_act(self, tree, path, column):
249        'Default action is the first entry in the context menu'
250        self.grep_cmenu.get_children()[0].activate()
251        return True
252
253    def get_rev_desc(self, rev):
254        if rev in self.changedesc:
255            return self.changedesc[rev]
256        ctx = self.repo[rev]
257        author = hglib.toutf(hglib.username(ctx.user()))
258        date = hglib.toutf(hglib.displaytime(ctx.date()))
259        text = hglib.tounicode(ctx.description()).replace(u'\0', '')
260        lines = text.splitlines()
261        summary = hglib.toutf(lines and lines[0] or '')
262        desc = gtklib.markup_escape_text('%s@%s %s "%s"' % \
263                                         (author, rev, date, summary))
264        self.changedesc[rev] = (desc, author)
265        return self.changedesc[rev]
266
267    def search_clicked(self, button, data):
268        self.add_search_page()
269
270    def create_tab_close_button(self):
271        button = gtk.Button()
272        iconBox = gtk.HBox(False, 0)
273        image = gtk.Image()
274        image.set_from_stock(gtk.STOCK_CLOSE, gtk.ICON_SIZE_MENU)
275        gtk.Button.set_relief(button, gtk.RELIEF_NONE)
276        settings = gtk.Widget.get_settings(button)
277        (w,h) = gtk.icon_size_lookup_for_settings(settings, gtk.ICON_SIZE_MENU)
278        gtk.Widget.set_size_request(button, w + 4, h + 4)
279        image.show()
280        iconBox.pack_start(image, True, False, 0)
281        button.add(iconBox)
282        iconBox.show()
283        return button
284
285    def close_notebook(self, notebook):
286        if notebook.get_n_pages() <= 1:
287            gtklib.thgclose(self)
288        else:
289            self.close_current_page()
290
291    def new_notebook(self, notebook):
292        self.add_search_page()
293
294    def add_search_page(self):
295        frame = gtk.Frame()
296        frame.set_border_width(10)
297        vbox = gtk.VBox()
298
299        search_hbox = gtk.HBox()
300        regexp = gtk.Entry()
301        includes = gtk.Entry()
302        if self.cwd.startswith(self.repo.root):
303            try:
304                relpath = util.canonpath(self.repo.root, self.cwd, '.')
305                includes.set_text(relpath)
306            except util.Abort:
307                # Some paths inside root are invalid (.hg/*)
308                pass
309        excludes = gtk.Entry()
310        search = gtk.Button(_('Search'))
311        search_hbox.pack_start(gtk.Label(_('Regexp:')), False, False, 4)
312        search_hbox.pack_start(regexp, True, True, 4)
313        search_hbox.pack_start(gtk.Label(_('Includes:')), False, False, 4)
314        search_hbox.pack_start(includes, True, True, 4)
315        search_hbox.pack_start(gtk.Label(_('Excludes:')), False, False, 4)
316        search_hbox.pack_start(excludes, True, True, 4)
317        search_hbox.pack_start(search, False, False, 4)
318        self.tooltips.set_tip(search, _('Start this search'))
319        self.tooltips.set_tip(regexp, _('Regular expression search pattern'))
320        self.tooltips.set_tip(includes, _('Comma separated list of'
321                ' inclusion patterns.  By default, the entire repository'
322                ' is searched.'))
323        self.tooltips.set_tip(excludes, _('Comma separated list of'
324                ' exclusion patterns.  Exclusion patterns are applied'
325                ' after inclusion patterns.'))
326        vbox.pack_start(search_hbox, False, False, 4)
327
328        hbox = gtk.HBox()
329        follow = gtk.CheckButton(_('Follow copies and renames'))
330        ignorecase = gtk.CheckButton(_('Ignore case'))
331        linenum = gtk.CheckButton(_('Show line numbers'))
332        showall = gtk.CheckButton(_('Show all matching revisions'))
333        hbox.pack_start(follow, False, False, 4)
334        hbox.pack_start(ignorecase, False, False, 4)
335        hbox.pack_start(linenum, False, False, 4)
336        hbox.pack_start(showall, False, False, 4)
337        vbox.pack_start(hbox, False, False, 4)
338
339        treeview = gtk.TreeView()
340        treeview.get_selection().set_mode(gtk.SELECTION_SINGLE)
341        treeview.set_rules_hint(True)
342        treeview.set_property('fixed-height-mode', True)
343        treeview.connect("cursor-changed", self.grep_selection_changed)
344        treeview.connect('button-release-event', self.grep_button_release)
345        treeview.connect('popup-menu', self.grep_popup_menu)
346        treeview.connect('row-activated', self.grep_row_act)
347
348        accelgroup = gtk.AccelGroup()
349        self.add_accel_group(accelgroup)
350        mod = gtklib.get_thg_modifier()
351        key, modifier = gtk.accelerator_parse(mod+'d')
352        treeview.add_accelerator('thg-diff', accelgroup, key,
353                        modifier, gtk.ACCEL_VISIBLE)
354        treeview.connect('thg-diff', self.grep_thgdiff)
355
356        results = gtk.ListStore(str, # revision id
357                                str, # matched line (utf-8)
358                                str, # description (utf-8, escaped)
359                                str) # file path (utf-8)
360        treeview.set_model(results)
361        treeview.set_search_equal_func(self.search_in_grep)
362        for title, width, ttype, col, emode in (
363                (_('Rev'), 10, 'text', GCOL_REVID, pango.ELLIPSIZE_NONE),
364                (_('File'), 25, 'text', GCOL_PATH, pango.ELLIPSIZE_START),
365                (_('Matches'), 80, 'markup', GCOL_LINE, pango.ELLIPSIZE_END)):
366            cell = gtk.CellRendererText()
367            cell.set_property('width-chars', width)
368            cell.set_property('ellipsize', emode)
369            cell.set_property('family', 'Monospace')
370            column = gtk.TreeViewColumn(title)
371            column.set_resizable(True)
372            column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
373            column.set_fixed_width(cell.get_size(treeview)[2])
374            column.pack_start(cell, expand=True)
375            column.add_attribute(cell, ttype, col)
376            treeview.append_column(column)
377        if hasattr(treeview, 'set_tooltip_column'):
378            treeview.set_tooltip_column(GCOL_DESC)
379        scroller = gtk.ScrolledWindow()
380        scroller.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
381        scroller.add(treeview)
382        vbox.pack_start(scroller, True, True)
383        frame.add(vbox)
384        frame.show_all()
385
386        hbox = gtk.HBox()
387        lbl = gtk.Label(_('Search %d') % self.newpagecount)
388        close = self.create_tab_close_button()
389        close.connect('clicked', self.close_page, frame)
390        hbox.pack_start(lbl, True, True, 2)
391        hbox.pack_start(close, False, False)
392        hbox.show_all()
393        num = self.notebook.append_page(frame, hbox)
394
395        self.newpagecount += 1
396        objs = (treeview.get_model(), frame, regexp, follow, ignorecase,
397                excludes, includes, linenum, showall, search_hbox)
398        # Clicking 'search' or hitting Enter in any text entry triggers search
399        search.connect('clicked', self.trigger_search, objs)
400        regexp.connect('activate', self.trigger_search, objs)
401        includes.connect('activate', self.trigger_search, objs)
402        excludes.connect('activate', self.trigger_search, objs)
403        # Includes/excludes must disable following copies
404        objs = (includes, excludes, follow)
405        includes.connect('changed', self.update_following_possible, objs)
406        excludes.connect('changed', self.update_following_possible, objs)
407        self.update_following_possible(includes, objs)
408
409        if hasattr(self.notebook, 'set_tab_reorderable'):
410            self.notebook.set_tab_reorderable(frame, True)
411        self.notebook.set_current_page(num)
412        regexp.grab_focus()
413
414    def search_in_grep(self, model, column, key, iter):
415        """Searches all fields shown in the tree when the user hits crtr+f,
416        not just the ones that are set via tree.set_search_column.
417        Case insensitive
418        """
419        key = key.lower()
420        for col in (GCOL_PATH, GCOL_LINE):
421            if key in model.get_value(iter, col).lower():
422                return False
423        return True
424
425    def trigger_search(self, button, objs):
426        (model, frame, regexp, follow, ignorecase,
427                excludes, includes, linenum, showall, search_hbox) = objs
428        retext = regexp.get_text()
429        if not retext:
430            gdialog.Prompt(_('No regular expression given'),
431                   _('You must provide a search expression'), self).run()
432            regexp.grab_focus()
433            return
434        try:
435            re.compile(retext)
436        except re.error, e:
437            gdialog.Prompt(_('Invalid regular expression'), 
438                    _('Error: %s') % str(e), self).run()
439            regexp.grab_focus()
440            return
441
442        q = Queue.Queue()
443        args = [q, 'grep']
444        if follow.get_active():     args.append('--follow')
445        if ignorecase.get_active(): args.append('--ignore-case')
446        if linenum.get_active():    args.append('--line-number')
447        if showall.get_active():    args.append('--all')
448        incs = [x.strip() for x in includes.get_text().split(',')]
449        excs = [x.strip() for x in excludes.get_text().split(',')]
450        for i in incs:
451            if i: args.extend(['-I', i])
452        for x in excs:
453            if x: args.extend(['-X', x])
454        args.append(retext)
455
456        def threadfunc(q, *args):
457            try:
458                hglib.hgcmd_toq(q, True, args)
459            except (util.Abort, error.LookupError), e:
460                self.stbar.set_text(_('Abort: %s') % str(e))
461
462        thread = thread2.Thread(target=threadfunc, args=args)
463        thread.start()
464        frame._mythread = thread
465        self.stop_button.set_sensitive(True)
466
467        model.clear()
468        search_hbox.set_sensitive(False)
469        self.stbar.begin(msg='hg ' + ' '.join(args[2:]))
470
471        hbox = gtk.HBox()
472        lbl = gtk.Label(_('Search "%s"') % retext.split()[0])
473        close = self.create_tab_close_button()
474        close.connect('clicked', self.close_page, frame)
475        hbox.pack_start(lbl, True, True, 2)
476        hbox.pack_start(close, False, False)
477        hbox.show_all()
478        self.notebook.set_tab_label(frame, hbox)
479
480        gobject.timeout_add(50, self.grep_wait, thread, q, model,
481                search_hbox, regexp, frame)
482
483    def grep_wait(self, thread, q, model, search_hbox, regexp, frame):
484        """
485        Handle all the messages currently in the queue (if any).
486        """
487        text = ''
488        while q.qsize():
489            data, label = q.get(0)
490            data = gtklib.markup_escape_text(hglib.toutf(data))
491            if label == 'grep.match':
492                text += '<span foreground="red"><b>%s</b></span>' % data
493            else:
494                text += data
495        
496        for line in text.splitlines():
497            try:
498                (path, revid, text) = line.split(':', 2)
499                desc, user = self.get_rev_desc(long(revid))
500            except ValueError:
501                continue
502            if self.tabwidth:
503                text = text.expandtabs(self.tabwidth)
504            model.append((revid, text, desc, hglib.toutf(path)))
505        if thread.isAlive():
506            return True
507        else:
508            if threading.activeCount() == 1:
509                self.stop_button.set_sensitive(False)
510            frame._mythread = None
511            search_hbox.set_sensitive(True)
512            regexp.grab_focus()
513            self.stbar.end()
514            return False
515
516    def grep_selection_changed(self, treeview):
517        """
518        Callback for when the user selects grep output.
519        """
520        (path, focus) = treeview.get_cursor()
521        model = treeview.get_model()
522        if path is not None and model is not None:
523            iter = model.get_iter(path)
524            self.currev = model[iter][GCOL_REVID]
525            self.curpath = hglib.fromutf(model[iter][GCOL_PATH])
526            self.cslabel.update(model[iter][GCOL_REVID])
527
528    def close_current_page(self):
529        num = self.notebook.get_current_page()
530        if num != -1 and self.notebook.get_n_pages():
531            self.notebook.remove_page(num)
532
533    def stop_current_search(self, button, widget):
534        num = self.notebook.get_current_page()
535        frame = self.notebook.get_nth_page(num)
536        self.stop_search(frame)
537
538    def stop_all_searches(self):
539        for num in xrange(self.notebook.get_n_pages()):
540            frame = self.notebook.get_nth_page(num)
541            self.stop_search(frame)
542
543    def stop_search(self, frame):
544        if getattr(frame, '_mythread', None):
545            if frame._mythread.isAlive():
546                try:
547                    frame._mythread.terminate()
548                    frame._mythread.join()
549                except (threading.ThreadError, ValueError):
550                    pass
551            frame._mythread = None
552
553    def close_page(self, button, widget):
554        '''Close page button has been pressed'''
555        num = self.notebook.page_num(widget)
556        if num != -1:
557            self.notebook.remove_page(num)
558            if self.notebook.get_n_pages() < 1:
559                self.newpagecount = 1
560                self.add_search_page()
561
562    def add_header_context_menu(self, col, menu):
563        lb = gtk.Label(col.get_title())
564        lb.show()
565        col.set_widget(lb)
566        wgt = lb.get_parent()
567        while wgt:
568            if type(wgt) == gtk.Button:
569                wgt.connect("button-press-event",
570                        self.tree_header_button_press, menu)
571                break
572            wgt = wgt.get_parent()
573
574    def tree_header_button_press(self, widget, event, menu):
575        if event.button == 3:
576            menu.popup(None, None, None, event.button, event.time)
577            return True
578        return False
579
580    def update_following_possible(self, widget, objs):
581        (includes, excludes, follow) = objs
582        allow = not includes.get_text() and not excludes.get_text()
583        if not allow:
584            follow.set_active(False)
585        follow.set_sensitive(allow)
586
587    def add_annotate_page(self, path, revid):
588        '''
589        Add new annotation page to notebook.  Start scan of
590        file 'path' revision history, start annotate of supplied
591        revision 'revid'.
592        '''
593        if revid == '.':
594            ctx = self.repo.parents()[0]
595            try:
596                fctx = ctx.filectx(path)
597            except error.LookupError:
598                gdialog.Prompt(_('File is unrevisioned'),
599                       _('Unable to annotate ') + path, self).run()
600                return
601            rev = fctx.filelog().linkrev(fctx.filerev())
602            revid = str(rev)
603        else:
604            rev = long(revid)
605
606        frame = gtk.Frame()
607        frame.set_border_width(10)
608        vbox = gtk.VBox()
609
610        graphopts = { 'date': None, 'no_merges':False, 'only_merges':False,
611                'keyword':[], 'branch':None, 'pats':[], 'revrange':[],
612                'revlist':[], 'noheads':False, 'orig-tip':len(self.repo),
613                'branch-view':False, 'rev':[], 'npreviews':0 }
614        graphopts['filehist'] = path
615
616        # File log revision graph
617        graphview = LogTreeView(self.repo, 5000)
618        graphview.set_property('rev-column-visible', True)
619        graphview.set_property('msg-column-visible', True)
620        graphview.set_property('user-column-visible', True)
621        graphview.set_property('age-column-visible', True)
622        graphview.set_columns(['graph', 'rev', 'msg', 'user', 'age'])
623        graphview.connect('revisions-loaded', self.revisions_loaded, rev)
624        graphview.refresh(True, [path], graphopts)
625
626        # Annotation text tree view
627        treeview = gtk.TreeView()
628        treeview.get_selection().set_mode(gtk.SELECTION_SINGLE)
629        treeview.set_property('fixed-height-mode', True)
630        treeview.set_border_width(0)
631
632        accelgroup = gtk.AccelGroup()
633        self.add_accel_group(accelgroup)
634        mod = gtklib.get_thg_modifier()
635        key, modifier = gtk.accelerator_parse(mod+'d')
636        treeview.add_accelerator('thg-diff', accelgroup, key,
637                        modifier, gtk.ACCEL_VISIBLE)
638        treeview.connect('thg-diff', self.annotate_thgdiff)
639
640        results = gtk.ListStore(str, # revision id
641                                str, # file line (utf-8)
642                                str, # description (utf-8, escaped)
643                                str, # file path (utf-8)
644                                str, # color
645                                str, # author (utf-8)
646                                str) # line number
647        treeview.set_model(results)
648        treeview.set_search_equal_func(self.search_in_file)
649
650        context_menu = self.ann_header_context_menu(treeview)
651        for title, width, col, emode, visible in (
652                (_('Line'), 8, ACOL_LNUM, pango.ELLIPSIZE_NONE, True),
653                (_('Rev'), 10, ACOL_REVID, pango.ELLIPSIZE_NONE, True),
654                (_('File'), 15, ACOL_PATH, pango.ELLIPSIZE_START, False),
655                (_('User'), 15, ACOL_USER, pango.ELLIPSIZE_END, False),
656                (_('Source'), 80, ACOL_LINE, pango.ELLIPSIZE_END, True)):
657            cell = gtk.CellRendererText()
658            cell.set_property('width-chars', width)
659            cell.set_property('ellipsize', emode)
660            cell.set_property('family', 'Monospace')
661            column = gtk.TreeViewColumn(title)
662            column.set_resizable(True)
663            column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
664            column.set_fixed_width(cell.get_size(treeview)[2])
665            column.pack_start(cell, expand=True)
666            column.add_attribute(cell, 'text', col)
667            column.add_attribute(cell, 'background', ACOL_COLOR)
668            column.set_visible(visible)
669            treeview.append_column(column)
670            self.add_header_context_menu(column, context_menu)
671        treeview.set_headers_clickable(True)
672        if hasattr(treeview, 'set_tooltip_column'):
673            treeview.set_tooltip_column(ACOL_DESC)
674        results.path = path
675        results.rev = revid
676        scroller = gtk.ScrolledWindow()
677        scroller.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
678        scroller.add(treeview)
679
680        vpaned = gtk.VPaned()
681        vpaned.pack1(graphview, True, True)
682        vpaned.pack2(scroller, True, True)
683        vbox.pack_start(vpaned, True, True)
684        frame.add(vbox)
685        frame.show_all()
686
687        hbox = gtk.HBox()
688        lbl = gtk.Label(hglib.toutf(os.path.basename(path) + '@' + revid))
689        close = self.create_tab_close_button()
690        close.connect('clicked', self.close_page, frame)
691        hbox.pack_start(lbl, True, True, 2)
692        hbox.pack_start(close, False, False)
693        hbox.show_all()
694        num = self.notebook.append_page_menu(frame,
695                hbox, gtk.Label(hglib.toutf(path + '@' + revid)))
696
697        if hasattr(self.notebook, 'set_tab_reorderable'):
698            self.notebook.set_tab_reorderable(frame, True)
699        self.notebook.set_current_page(num)
700
701        graphview.connect('revision-selected', self.log_selection_changed, path)
702
703        objs = (frame, treeview, path, graphview)
704        graphview.treeview.connect('row-activated', self.log_activate, objs)
705        graphview.treeview.connect('button-release-event',
706                self.ann_button_release, objs)
707        graphview.treeview.connect('popup-menu', self.ann_popup_menu, objs)
708
709        treeview.connect("cursor-changed", self.ann_selection_changed)
710        treeview.connect('button-release-event', self.ann_button_release, objs)
711        treeview.connect('popup-menu', self.ann_popup_menu, objs)
712        treeview.connect('row-activated', self.ann_row_act, objs)
713
714        self.stbar.begin(msg=_('Loading history...'))
715
716    def search_in_file(self, model, column, key, iter):
717        """Searches all fields shown in the tree when the user hits crtr+f,
718        not just the ones that are set via tree.set_search_column.
719        Case insensitive
720        """
721        key = key.lower()
722        for col in (ACOL_USER, ACOL_LINE):
723            if key in model.get_value(iter, col).lower():
724                return False
725        return True
726
727    def annotate_thgdiff(self, treeview):
728        self._do_diff([], {'change' : self.currev})
729
730    def toggle_annatate_columns(self, button, treeview, col):
731        b = button.get_active()
732        treeview.get_column(col).set_visible(b)
733
734    def log_selection_changed(self, graphview, path):
735        treeview = graphview.treeview
736        (model, paths) = treeview.get_selection().get_selected_rows()
737        if not paths:
738            return
739        revid = graphview.get_revid_at_path(paths[0])
740        self.currev = str(revid)
741        wfile = graphview.get_wfile_at_path(paths[0])
742        if wfile:
743            self.curpath = wfile
744
745    def log_activate(self, treeview, path, column, objs):
746        (frame, treeview, file, graphview) = objs
747        rev = graphview.get_revid_at_path(path)
748        wfile = graphview.get_wfile_at_path(path)
749        self.trigger_annotate(rev, wfile, objs)
750
751    def revisions_loaded(self, graphview, rev):
752        self.stbar.end()
753        graphview.set_revision_id(rev)
754        treeview = graphview.treeview
755        path, column = treeview.get_cursor()
756        # It's possible that the requested change was not found in the
757        # file's filelog history.  In that case, no row will be
758        # selected.
759        if path != None and column != None:
760            treeview.row_activated(path, column)
761
762    def trigger_annotate(self, rev, path, objs):
763        '''
764        User has selected a file revision to annotate.  Trigger a
765        background thread to perform the annotation.  Disable the select
766        button until this operation is complete.
767        '''
768        def threadfunc(q, *args):
769            try:
770                hglib.hgcmd_toq(q, False, args)
771            except (util.Abort, error.LookupError), e:
772                self.stbar.set_text(_('Abort: %s') % str(e))
773
774        (frame, treeview, origpath, graphview) = objs
775        q = Queue.Queue()
776        # Use short -f here because it's meaning has changed, it used
777        # to be --follow but now it means --file.  We want either.
778        # Replace -f with --file when support for hg-1.4 is dropped
779        args = [q, 'annotate', '-f', '--number', '--rev', str(rev),
780                'path:'+path]
781        thread = thread2.Thread(target=threadfunc, args=args)
782        thread.start()
783        frame._mythread = thread
784        self.stop_button.set_sensitive(True)
785
786        # date of selected revision
787        ctx = self.repo[long(rev)]
788        curdate = ctx.date()[0]
789        colormap = AnnotateColorSaturation()
790
791        model, rows = treeview.get_selection().get_selected_rows()
792        model.clear()
793        self.stbar.begin(msg=hglib.toutf('hg ' + ' '.join(args[2:])))
794
795        hbox = gtk.HBox()
796        lbl = gtk.Label(hglib.toutf(os.path.basename(path) + '@' + str(rev)))
797        close = self.create_tab_close_button()
798        close.connect('clicked', self.close_page, frame)
799        hbox.pack_start(lbl, True, True, 2)
800        hbox.pack_start(close, False, False)
801        hbox.show_all()
802        self.notebook.set_tab_label(frame, hbox)
803
804        gobject.timeout_add(50, self.annotate_wait, thread, q, treeview,
805                curdate, colormap, frame, rows)
806
807    def annotate_wait(self, thread, q, tview, curdate, colormap, frame, rows):
808        """
809        Handle all the messages currently in the queue (if any).
810        """
811        model = tview.get_model()
812        while q.qsize():
813            line = q.get(0).rstrip('\r\n')
814            try:
815                (revpath, text) = line.split(':', 1)
816                revid, path = revpath.lstrip().split(' ', 1)
817                rowrev = long(revid)
818            except ValueError:
819                continue
820            desc, user = self.get_rev_desc(rowrev)
821            ctx = self.repo[rowrev]
822            color = colormap.get_color(ctx, curdate)
823            if self.tabwidth:
824                text = text.expandtabs(self.tabwidth)
825            model.append((revid, hglib.toutf(text[:512]), desc,
826                    hglib.toutf(path.strip()), color, user, len(model)+1))
827        if thread.isAlive():
828            return True
829        else:
830            if threading.activeCount() == 1:
831                self.stop_button.set_sensitive(False)
832            if rows:
833                tview.get_selection().select_path(rows[0])
834                tview.scroll_to_cell(rows[0], use_align=True, row_align=0.5)
835                tview.grab_focus()
836            frame._mythread = None
837            self.stbar.end()
838            return False
839
840    def ann_selection_changed(self, treeview):
841        """
842        User selected line of annotate output, describe revision
843        responsible for this line in the status bar
844        """
845        (path, focus) = treeview.get_cursor()
846        model = treeview.get_model()
847        if path is not None and model is not None:
848            anniter = model.get_iter(path)
849            self.currev = model[anniter][ACOL_REVID]
850            self.path = model.path
851            self.cslabel.update(model[anniter][ACOL_REVID])
852
853    def ann_button_release(self, widget, event, objs):
854        if event.button == 3 and not (event.state & (gtk.gdk.SHIFT_MASK |
855            gtk.gdk.CONTROL_MASK)):
856            self.ann_popup_menu(widget, event.button, event.time, objs)
857        return False
858
859    def ann_popup_menu(self, treeview, button, time, objs):
860        ann_cmenu = self.annotate_context_menu(objs)
861        ann_cmenu.popup(None, None, None, button, time)
862        return True
863
864    def ann_row_act(self, tree, path, column, objs):
865        ann_cmenu = self.annotate_context_menu(objs)
866        ann_cmenu.get_children()[0].activate()
867
868def run(ui, *pats, **opts):
869    cmdoptions = {
870        'follow':False, 'follow-first':False, 'copies':False, 'keyword':[],
871        'limit':0, 'rev':[], 'removed':False, 'no_merges':False, 'date':None,
872        'only_merges':None, 'prune':[], 'git':False, 'verbose':False,
873        'include':[], 'exclude':[]
874    }
875    return DataMineDialog(ui, None, None, pats, cmdoptions)