PageRenderTime 72ms CodeModel.GetById 15ms app.highlight 48ms RepoModel.GetById 2ms app.codeStats 0ms

/tortoisehg/hgtk/logview/treeview.py

https://bitbucket.org/tortoisehg/hgtk/
Python | 606 lines | 520 code | 59 blank | 27 comment | 74 complexity | 25e306ed1291bdd736e240e4e590ab7e MD5 | raw file
  1# treeview.py - changelog viewer implementation
  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
  8''' Mercurial revision DAG visualization library
  9
 10  Implements a gtk.TreeModel which visualizes a Mercurial repository
 11  revision history.
 12
 13Portions of this code stolen mercilessly from bzr-gtk visualization
 14dialog.  Other portions stolen from graphlog extension.
 15'''
 16
 17import gtk
 18import gobject
 19import pango
 20import os
 21import time
 22
 23from tortoisehg.util.i18n import _
 24from tortoisehg.util import hglib
 25
 26from tortoisehg.hgtk.logview import treemodel
 27from tortoisehg.hgtk.logview.graphcell import CellRendererGraph
 28from tortoisehg.hgtk.logview.revgraph import *
 29
 30COLS = 'graph rev id revhex branch changes msg user date utc age tag svn'
 31
 32class TreeView(gtk.ScrolledWindow):
 33
 34    __gproperties__ = {
 35        'repo': (gobject.TYPE_PYOBJECT,
 36                   'Repository',
 37                   'The Mercurial repository being visualized',
 38                   gobject.PARAM_CONSTRUCT_ONLY | gobject.PARAM_WRITABLE),
 39
 40        'limit': (gobject.TYPE_PYOBJECT,
 41                   'Revision Display Limit',
 42                   'The maximum number of revisions to display',
 43                   gobject.PARAM_READWRITE),
 44
 45        'original-tip-revision': (gobject.TYPE_PYOBJECT,
 46                   'Tip revision when application opened',
 47                   'Revisions above this number will be drawn green',
 48                   gobject.PARAM_READWRITE),
 49
 50        'msg-column-visible': (gobject.TYPE_BOOLEAN,
 51                                 'Summary',
 52                                 'Show summary column',
 53                                 True,
 54                                 gobject.PARAM_READWRITE),
 55        'user-column-visible': (gobject.TYPE_BOOLEAN,
 56                                 'User',
 57                                 'Show user column',
 58                                 True,
 59                                 gobject.PARAM_READWRITE),
 60        'date-column-visible': (gobject.TYPE_BOOLEAN,
 61                                 'Date',
 62                                 'Show date column',
 63                                 False,
 64                                 gobject.PARAM_READWRITE),
 65        'utc-column-visible': (gobject.TYPE_BOOLEAN,
 66                                 'UTC',
 67                                 'Show UTC/GMT date column',
 68                                 False,
 69                                 gobject.PARAM_READWRITE),
 70        'age-column-visible': (gobject.TYPE_BOOLEAN,
 71                                 'Age',
 72                                 'Show age column',
 73                                 False,
 74                                 gobject.PARAM_READWRITE),
 75        'rev-column-visible': (gobject.TYPE_BOOLEAN,
 76                                 'Rev',
 77                                 'Show revision number column',
 78                                 False,
 79                                 gobject.PARAM_READWRITE),
 80        'id-column-visible': (gobject.TYPE_BOOLEAN,
 81                                 'ID',
 82                                 'Show revision ID column',
 83                                 False,
 84                                 gobject.PARAM_READWRITE),
 85        'revhex-column-visible': (gobject.TYPE_BOOLEAN,
 86                                 'Rev/ID',
 87                                 'Show revision number/ID column',
 88                                 False,
 89                                 gobject.PARAM_READWRITE),
 90        'branch-column-visible': (gobject.TYPE_BOOLEAN,
 91                                 'Branch',
 92                                 'Show branch',
 93                                 False,
 94                                 gobject.PARAM_READWRITE),
 95        'changes-column-visible': (gobject.TYPE_BOOLEAN,
 96                                 'Changes',
 97                                 'Show changes column',
 98                                 False,
 99                                 gobject.PARAM_READWRITE),
100        'tag-column-visible': (gobject.TYPE_BOOLEAN,
101                                 'Tags',
102                                 'Show tag column',
103                                 False,
104                                 gobject.PARAM_READWRITE),
105        'svn-column-visible': (gobject.TYPE_BOOLEAN,
106                                 'Subversion',
107                                 'Show Subversion column',
108                                 False,
109                                 gobject.PARAM_READWRITE),
110        'branch-color': (gobject.TYPE_BOOLEAN,
111                                 'Branch color',
112                                 'Color by branch',
113                                 False,
114                                 gobject.PARAM_READWRITE)
115    }
116
117    __gsignals__ = {
118        'revisions-loaded': (gobject.SIGNAL_RUN_FIRST, 
119                             gobject.TYPE_NONE,
120                             ()),
121        'batch-loaded': (gobject.SIGNAL_RUN_FIRST, 
122                         gobject.TYPE_NONE,
123                         ()),
124        'revision-selected': (gobject.SIGNAL_RUN_FIRST,
125                              gobject.TYPE_NONE,
126                              ())
127    }
128
129    def __init__(self, repo, limit=500, stbar=None):
130        """Create a new TreeView.
131
132        :param repo:  Repository object to show
133        """
134        gtk.ScrolledWindow.__init__(self)
135
136        self.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
137        self.set_shadow_type(gtk.SHADOW_IN)
138
139        self.batchsize = limit
140        self.repo = repo
141        self.currevid = None
142        self.stbar = stbar
143        self.grapher = None
144        self.graphdata = []
145        self.index = {}
146        self.opts = { 'outgoing':[], 'orig-tip':None, 'npreviews':0,
147                      'branch-color':False, 'show-graph':True }
148        self.construct_treeview()
149
150    def set_repo(self, repo, stbar=None):
151        self.repo = repo
152        self.stbar = stbar
153
154    def search_in_tree(self, model, column, key, iter, data):
155        """Searches all fields shown in the tree when the user hits crtr+f,
156        not just the ones that are set via tree.set_search_column.
157        Case insensitive
158        """
159        key = key.lower()
160        row = model[iter]
161        if row[treemodel.HEXID].startswith(key):
162            return False
163        for col in (treemodel.REVID, treemodel.COMMITER, treemodel.MESSAGE):
164            if key in str(row[col]).lower():
165                return False
166        return True
167
168    def create_log_generator(self, graphcol, pats, opts):
169        if self.repo is None:
170            self.grapher = None
171            return
172            
173        only_branch = opts.get('branch', None)
174
175        if opts.get('filehist') is not None:
176            self.grapher = filelog_grapher(self.repo, opts['filehist'])
177        elif graphcol:
178            end = 0
179            if only_branch is not None:
180                b = self.repo.branchtags()
181                if only_branch in b:
182                    node = b[only_branch]
183                    start = self.repo.changelog.rev(node)
184                else:
185                    start = len(self.repo.changelog) - 1
186            elif opts.get('revrange'):
187                if len(opts['revrange']) >= 2:
188                    start, end = opts['revrange']
189                else:
190                    start = opts['revrange'][0]
191                    end = start
192            else:
193                start = len(self.repo.changelog) - 1
194            noheads = opts.get('noheads', False)
195            if opts.get('branch-view', False):
196                self.grapher = branch_grapher(self.repo, start, end, 
197                    only_branch, self.opts.get('branch-color'))
198            else:
199                self.grapher = revision_grapher(self.repo, start, end,
200                        only_branch, noheads, self.opts.get('branch-color'))
201        elif opts.get('revlist', None):
202            graphcol = True
203            self.grapher = revision_grapher(self.repo, only_revs=opts['revlist'])
204        else:
205            self.grapher = filtered_log_generator(self.repo, pats, opts)
206        self.opts['show-graph'] = graphcol
207        self.graphdata = []
208        self.index = {}
209        self.max_cols = 1
210        self.model = None
211        self.limit = self.batchsize
212
213    def populate(self, revid=None):
214        'Fill the treeview with contents'
215        stopped = False
216        if self.repo is None:
217            stopped = True
218            return False
219
220        if os.name == "nt":
221            timer = time.clock
222        else:
223            timer = time.time
224        startsec = timer()
225        try:
226            while (not self.limit) or len(self.graphdata) < self.limit:
227                (rev, node, lines, wfile) = self.grapher.next()
228                self.max_cols = max(self.max_cols, len(lines))
229                self.index[rev] = len(self.graphdata)
230                self.graphdata.append( (rev, node, lines, wfile) )
231                if self.model:
232                    rowref = self.model.get_iter(len(self.graphdata)-1)
233                    path = self.model.get_path(rowref) 
234                    self.model.row_inserted(path, rowref) 
235                cursec = timer()
236                if cursec < startsec or cursec > startsec + 0.1:
237                    break
238        except StopIteration:
239            stopped = True
240
241        if stopped:
242            pass
243        elif self.limit is None:
244            return True
245        elif len(self.graphdata) < self.limit:
246            return True
247
248        if not len(self.graphdata):
249            self.treeview.set_model(None)
250            if self.stbar is not None:
251                self.stbar.end()
252            self.emit('revisions-loaded')
253            return False
254
255        self.graph_cell.columns_len = self.max_cols
256        width = self.graph_cell.get_size(self.treeview)[2]
257        if width > 500:
258            width = 500
259        gcol = self.tvcolumns['graph']
260        gcol.set_fixed_width(width)
261        gcol.set_visible(self.opts.get('show-graph'))
262
263        if not self.model:
264            model = treemodel.TreeModel(self.repo, self.graphdata, self.opts)
265            self.treeview.set_model(model)
266            self.model = model
267
268        self.emit('batch-loaded')
269        if stopped:
270            self.emit('revisions-loaded')
271        if revid is not None:
272            self.set_revision_id(revid)
273        if self.stbar is not None:
274            self.stbar.end()
275            revision_text = _('%(count)d of %(total)d Revisions') % {
276                    'count': len(self.model),
277                    'total': len(self.repo) }
278            self.stbar.set_text(revision_text, name='rev')
279        return False
280
281    def do_get_property(self, property):
282        pn = property.name
283        cv = '-column-visible'
284        if pn.endswith(cv):
285            colname = pn[:-len(cv)]
286            return self.tvcolumns[colname].get_visible()
287        elif pn == 'branch-color':
288            return self.opts.get('branch-color')
289        elif pn == 'repo':
290            return self.repo
291        elif pn == 'limit':
292            return self.limit
293        else:
294            raise AttributeError, 'unknown property %s' % pn
295
296    def do_set_property(self, property, value):
297        pn = property.name
298        cv = '-column-visible'
299        if pn.endswith(cv):
300            colname = pn[:-len(cv)]
301            self.tvcolumns[colname].set_visible(value)
302        elif pn == 'branch-color':
303            self.opts['branch-color'] = value
304        elif pn == 'repo':
305            self.repo = value
306        elif pn == 'limit':
307            self.batchsize = value
308        else:
309            raise AttributeError, 'unknown property %s' % pn
310
311    def get_revid_at_path(self, path):
312        return self.model[path][treemodel.REVID]
313
314    def get_path_at_revid(self, revid):
315        if revid in self.index:
316            row_index = self.index[revid]
317            iter = self.model.get_iter(row_index)
318            path = self.model.get_path(iter)
319            return path
320        else:
321            return None
322
323    def get_wfile_at_path(self, path):
324        if self.model:
325            return self.model[path][treemodel.WFILE]
326        else:
327            return None
328
329    def next_revision_batch(self, size):
330        if not self.grapher:
331            self.emit('revisions-loaded')
332            return
333        self.batchsize = size
334        self.limit += self.batchsize
335        if self.stbar is not None:
336            self.stbar.begin()
337        gobject.idle_add(self.populate)
338
339    def load_all_revisions(self):
340        if not self.grapher:
341            self.emit('revisions-loaded')
342            return
343        self.limit = None
344        if self.stbar is not None:
345            self.stbar.begin()
346        gobject.idle_add(self.populate)
347
348    def scroll_to_revision(self, revid):
349        if revid in self.index:
350            row = self.index[revid]
351            self.treeview.scroll_to_cell(row, use_align=True, row_align=0.5)
352
353    def set_revision_id(self, revid, load=False):
354        """Change the currently selected revision.
355
356        :param revid: Revision id of revision to display.
357        """
358        if revid in self.index:
359            row = self.index[revid]
360            self.treeview.set_cursor(row)
361            self.treeview.grab_focus()
362        elif load:
363            handlers = [None, None]
364
365            def loaded(dummy):
366                if revid in self.index:
367                    hload = handlers[0]
368                    if hload is not None:
369                        self.disconnect(hload)
370                        handlers[0] = None
371                    self.set_revision_id(revid)
372                    self.scroll_to_revision(revid)
373                else:
374                    self.next_revision_batch(self.batchsize)
375            def stopped(dummy):
376                hload, hstop = handlers
377                if hload is not None:
378                    self.disconnect(hload)
379                    handlers[0] = None
380                if hstop is not None:
381                    self.disconnect(hstop)
382                    handlers[1] = None
383                self.stbar.set_text(_('Changeset not found in current view'))
384
385            try:
386                ctx = self.repo[revid]
387                if ctx.rev() == -1:
388                    self.stbar.set_text(_('Null changeset is not viewable'))
389                    return
390            except Exception, e:
391                self.stbar.set_text(str(e))
392                return
393            handlers[0] = self.connect('batch-loaded', loaded)
394            handlers[1] = self.connect('revisions-loaded', stopped)
395            self.next_revision_batch(self.batchsize)
396
397    def refresh(self, graphcol, pats, opts):
398        self.opts.update(opts)
399        if self.repo is not None:
400            hglib.invalidaterepo(self.repo)
401            if len(self.repo) > 0:
402                self.create_log_generator(graphcol, pats, opts)
403                if self.stbar is not None:
404                    self.stbar.begin()
405                gobject.idle_add(self.populate, self.currevid)
406            else:
407                self.treeview.set_model(None)
408                self.stbar.set_text(_('Repository is empty'))
409
410    def construct_treeview(self):
411        self.treeview = gtk.TreeView()
412        self.treeview.set_rules_hint(True)
413        self.treeview.set_reorderable(False)
414        self.treeview.set_enable_search(True)
415        self.treeview.set_search_equal_func(self.search_in_tree, None)
416
417        self.treeview.get_selection().set_mode(gtk.SELECTION_SINGLE)
418        self.treeview.connect("cursor-changed", self._on_selection_changed)
419        self.treeview.set_property('fixed-height-mode', True)
420        self.treeview.show()
421        self.add(self.treeview)
422
423        self.tvcolumns = {}
424
425        self.graph_cell = CellRendererGraph()
426        col = self.tvcolumns['graph'] = gtk.TreeViewColumn(_('Graph'))
427        col.set_resizable(True)
428        col.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
429        col.pack_start(self.graph_cell, expand=False)
430        col.add_attribute(self.graph_cell,
431                "node", treemodel.GRAPHNODE)
432        col.add_attribute(self.graph_cell,
433                "in-lines", treemodel.LAST_LINES)
434        col.add_attribute(self.graph_cell,
435                "out-lines", treemodel.LINES)
436
437        cell = gtk.CellRendererText()
438        cell.set_property("width-chars", 8)
439        cell.set_property("ellipsize", pango.ELLIPSIZE_END)
440        cell.set_property("xalign", 1.0)
441        col = self.tvcolumns['rev'] = gtk.TreeViewColumn(_('Rev'))
442        col.set_visible(False)
443        col.set_resizable(True)
444        col.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
445        col.set_fixed_width(cell.get_size(self.treeview)[2])
446        col.pack_start(cell, expand=True)
447        col.add_attribute(cell, "text", treemodel.REVID)
448        col.add_attribute(cell, "foreground", treemodel.FGCOLOR)
449
450        cell = gtk.CellRendererText()
451        cell.set_property("width-chars", 15)
452        cell.set_property("ellipsize", pango.ELLIPSIZE_END)
453        cell.set_property("family", "Monospace")
454        col = self.tvcolumns['id'] = gtk.TreeViewColumn(_('ID'))
455        col.set_visible(False)
456        col.set_resizable(True)
457        col.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
458        col.set_fixed_width(cell.get_size(self.treeview)[2])
459        col.pack_start(cell, expand=True)
460        col.add_attribute(cell, "text", treemodel.HEXID)
461        col.add_attribute(cell, "foreground", treemodel.FGCOLOR)
462
463        cell = gtk.CellRendererText()
464        cell.set_property("width-chars", 22)
465        cell.set_property("ellipsize", pango.ELLIPSIZE_END)
466        col = self.tvcolumns['revhex'] = gtk.TreeViewColumn(_('Rev/ID'))
467        col.set_visible(False)
468        col.set_resizable(True)
469        col.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
470        col.set_fixed_width(cell.get_size(self.treeview)[2])
471        col.pack_start(cell, expand=True)
472        col.add_attribute(cell, "foreground", treemodel.FGCOLOR)
473        col.add_attribute(cell, "markup", treemodel.REVHEX)
474
475        cell = gtk.CellRendererText()
476        cell.set_property("width-chars", 15)
477        cell.set_property("ellipsize", pango.ELLIPSIZE_END)
478        col = self.tvcolumns['branch'] = gtk.TreeViewColumn(_('Branch'))
479        col.set_visible(False)
480        col.set_resizable(True)
481        col.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
482        col.set_fixed_width(cell.get_size(self.treeview)[2])
483        col.pack_start(cell, expand=True)
484        col.add_attribute(cell, "text", treemodel.BRANCH)
485        col.add_attribute(cell, "foreground", treemodel.FGCOLOR)
486
487        cell = gtk.CellRendererText()
488        cell.set_property("width-chars", 10)
489        cell.set_property("ellipsize", pango.ELLIPSIZE_END)
490        col = self.tvcolumns['changes'] = gtk.TreeViewColumn(_('Changes'))
491        col.set_visible(False)
492        col.set_resizable(True)
493        col.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
494        col.set_fixed_width(cell.get_size(self.treeview)[2])
495        col.pack_start(cell, expand=True)
496        col.add_attribute(cell, "markup", treemodel.CHANGES)
497
498        cell = gtk.CellRendererText()
499        cell.set_property("width-chars", 80)
500        cell.set_property("ellipsize", pango.ELLIPSIZE_END)
501        col = self.tvcolumns['msg'] = gtk.TreeViewColumn(_('Summary'))
502        col.set_resizable(True)
503        col.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
504        col.set_fixed_width(cell.get_size(self.treeview)[2])
505        col.pack_end(cell, expand=True)
506        col.add_attribute(cell, "foreground", treemodel.FGCOLOR)
507        col.add_attribute(cell, "markup", treemodel.MESSAGE)
508
509        cell = gtk.CellRendererText()
510        cell.set_property("width-chars", 20)
511        cell.set_property("ellipsize", pango.ELLIPSIZE_END)
512        col = self.tvcolumns['user'] = gtk.TreeViewColumn(_('User'))
513        col.set_resizable(True)
514        col.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
515        col.set_fixed_width(cell.get_size(self.treeview)[2])
516        col.pack_start(cell, expand=True)
517        col.add_attribute(cell, "text", treemodel.COMMITER)
518        col.add_attribute(cell, "foreground", treemodel.FGCOLOR)
519
520        cell = gtk.CellRendererText()
521        cell.set_property("width-chars", 20)
522        cell.set_property("ellipsize", pango.ELLIPSIZE_END)
523        col = self.tvcolumns['date'] = gtk.TreeViewColumn(_('Local Date'))
524        col.set_visible(False)
525        col.set_resizable(True)
526        col.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
527        col.set_fixed_width(cell.get_size(self.treeview)[2])
528        col.pack_start(cell, expand=True)
529        col.add_attribute(cell, "text", treemodel.LOCALTIME)
530        col.add_attribute(cell, "foreground", treemodel.FGCOLOR)
531
532        cell = gtk.CellRendererText()
533        cell.set_property("width-chars", 20)
534        cell.set_property("ellipsize", pango.ELLIPSIZE_END)
535        col = self.tvcolumns['utc'] = gtk.TreeViewColumn(_('Universal Date'))
536        col.set_visible(False)
537        col.set_resizable(True)
538        col.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
539        col.set_fixed_width(cell.get_size(self.treeview)[2])
540        col.pack_start(cell, expand=True)
541        col.add_attribute(cell, "text", treemodel.UTC)
542        col.add_attribute(cell, "foreground", treemodel.FGCOLOR)
543
544        cell = gtk.CellRendererText()
545        cell.set_property("width-chars", 16)
546        cell.set_property("ellipsize", pango.ELLIPSIZE_END)
547        col = self.tvcolumns['age'] = gtk.TreeViewColumn(_('Age'))
548        col.set_visible(True)
549        col.set_resizable(True)
550        col.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
551        col.set_fixed_width(cell.get_size(self.treeview)[2])
552        col.pack_start(cell, expand=True)
553        col.add_attribute(cell, "text", treemodel.AGE)
554        col.add_attribute(cell, "foreground", treemodel.FGCOLOR)
555
556        cell = gtk.CellRendererText()
557        cell.set_property("width-chars", 10)
558        cell.set_property("ellipsize", pango.ELLIPSIZE_END)
559        col = self.tvcolumns['tag']  = gtk.TreeViewColumn(_('Tags'))
560        col.set_visible(False)
561        col.set_resizable(True)
562        col.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
563        col.set_fixed_width(cell.get_size(self.treeview)[2])
564        col.pack_start(cell, expand=True)
565        col.add_attribute(cell, "text", treemodel.TAGS)
566        col.add_attribute(cell, "foreground", treemodel.FGCOLOR)
567
568        cell = gtk.CellRendererText()
569        cell.set_property("width-chars", 10)
570        cell.set_property("ellipsize", pango.ELLIPSIZE_END)
571        col = self.tvcolumns['svn']  = gtk.TreeViewColumn(_('Subversion'))
572        col.set_visible(False)
573        col.set_resizable(True)
574        col.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
575        col.set_fixed_width(cell.get_size(self.treeview)[2])
576        col.pack_start(cell, expand=True)
577        col.add_attribute(cell, "text", treemodel.SVNREV)
578        col.add_attribute(cell, "foreground", treemodel.FGCOLOR)
579
580        self.columns = COLS.split()
581
582    def set_columns(self, columns):
583        if ' '.join(columns) != ' '.join(self.columns):
584            cols = self.treeview.get_columns()
585            for cn in self.columns:
586                c = self.tvcolumns[cn]
587                if c in cols:
588                    self.treeview.remove_column(c)
589            for cn in columns:
590                try:
591                    c = self.tvcolumns[cn]
592                    self.treeview.append_column(c)
593                except KeyError:
594                    continue
595            self.columns = columns
596
597    def get_columns(self):
598        return self.columns
599
600    def _on_selection_changed(self, treeview):
601        """callback for when the treeview changes."""
602        (path, focus) = treeview.get_cursor()
603        if path and self.model:
604            self.currevid = self.model[path][treemodel.REVID]
605            self.emit('revision-selected')
606