/b-gtk/fix-viz

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/b-gtk/fix-viz

« back to all changes in this revision

Viewing changes to annotate/gannotate.py

  • Committer: Jelmer Vernooij
  • Date: 2007-03-17 23:58:03 UTC
  • mfrom: (170.1.5 bzr-gtk-meld)
  • Revision ID: jelmer@samba.org-20070317235803-fyaw65t8ika939s1
MergeĀ upstream

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2005 Dan Loda <danloda@gmail.com>
 
2
 
 
3
# This program is free software; you can redistribute it and/or modify
 
4
# it under the terms of the GNU General Public License as published by
 
5
# the Free Software Foundation; either version 2 of the License, or
 
6
# (at your option) any later version.
 
7
 
 
8
# This program is distributed in the hope that it will be useful,
 
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
11
# GNU General Public License for more details.
 
12
 
 
13
# You should have received a copy of the GNU General Public License
 
14
# along with this program; if not, write to the Free Software
 
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
16
 
 
17
import time
 
18
 
 
19
import pygtk
 
20
pygtk.require("2.0")
 
21
import gobject
 
22
import gtk
 
23
import pango
 
24
import re
 
25
 
 
26
from bzrlib import patiencediff, tsort
 
27
from bzrlib.errors import NoSuchRevision
 
28
from bzrlib.revision import NULL_REVISION, CURRENT_REVISION
 
29
 
 
30
from colormap import AnnotateColorMap, AnnotateColorSaturation
 
31
from bzrlib.plugins.gtk.logview import LogView
 
32
 
 
33
 
 
34
(
 
35
    REVISION_ID_COL,
 
36
    LINE_NUM_COL,
 
37
    COMMITTER_COL,
 
38
    REVNO_COL,
 
39
    HIGHLIGHT_COLOR_COL,
 
40
    TEXT_LINE_COL
 
41
) = range(6)
 
42
 
 
43
 
 
44
class GAnnotateWindow(gtk.Window):
 
45
    """Annotate window."""
 
46
 
 
47
    def __init__(self, all=False, plain=False):
 
48
        self.all = all
 
49
        self.plain = plain
 
50
        
 
51
        gtk.Window.__init__(self, gtk.WINDOW_TOPLEVEL)
 
52
        
 
53
        self.set_icon(self.render_icon(gtk.STOCK_FIND, gtk.ICON_SIZE_BUTTON))
 
54
        self.annotate_colormap = AnnotateColorSaturation()
 
55
 
 
56
        self._create()
 
57
        self.revisions = {}
 
58
        self.history = []
 
59
 
 
60
    def annotate(self, tree, branch, file_id):
 
61
        self.annotations = []
 
62
        self.branch = branch
 
63
        self.tree = tree
 
64
        self.file_id = file_id
 
65
        self.revision_id = getattr(tree, 'get_revision_id', 
 
66
                                   lambda: CURRENT_REVISION)()
 
67
        
 
68
        # [revision id, line number, committer, revno, highlight color, line]
 
69
        self.annomodel = gtk.ListStore(gobject.TYPE_STRING,
 
70
                                       gobject.TYPE_STRING,
 
71
                                       gobject.TYPE_STRING,
 
72
                                       gobject.TYPE_STRING,
 
73
                                       gobject.TYPE_STRING,
 
74
                                       gobject.TYPE_STRING)
 
75
        
 
76
        last_seen = None
 
77
        try:
 
78
            branch.lock_read()
 
79
            branch.repository.lock_read()
 
80
            for line_no, (revision, revno, line)\
 
81
                    in enumerate(self._annotate(tree, file_id)):
 
82
                if revision.revision_id == last_seen and not self.all:
 
83
                    revno = committer = ""
 
84
                else:
 
85
                    last_seen = revision.revision_id
 
86
                    committer = revision.committer
 
87
 
 
88
                if revision.revision_id not in self.revisions:
 
89
                    self.revisions[revision.revision_id] = revision
 
90
 
 
91
                self.annomodel.append([revision.revision_id,
 
92
                                       line_no + 1,
 
93
                                       committer,
 
94
                                       revno,
 
95
                                       None,
 
96
                                       line.rstrip("\r\n")
 
97
                                      ])
 
98
                self.annotations.append(revision)
 
99
 
 
100
            if not self.plain:
 
101
                now = time.time()
 
102
                self.annomodel.foreach(self._highlight_annotation, now)
 
103
        finally:
 
104
            branch.repository.unlock()
 
105
            branch.unlock()
 
106
 
 
107
        self.annoview.set_model(self.annomodel)
 
108
        self.annoview.grab_focus()
 
109
 
 
110
    def jump_to_line(self, lineno):
 
111
        if lineno > len(self.annomodel) or lineno < 1:
 
112
            row = 0
 
113
            # FIXME:should really deal with this in the gui. Perhaps a status
 
114
            # bar?
 
115
            print("gannotate: Line number %d does't exist. Defaulting to "
 
116
                  "line 1." % lineno)
 
117
            return
 
118
        else:
 
119
            row = lineno - 1
 
120
 
 
121
        self.annoview.set_cursor(row)
 
122
        self.annoview.scroll_to_cell(row, use_align=True)
 
123
 
 
124
    def _dotted_revnos(self, repository, revision_id):
 
125
        """Return a dict of revision_id -> dotted revno
 
126
        
 
127
        :param repository: The repository to get the graph from
 
128
        :param revision_id: The last revision for which this info is needed
 
129
        """
 
130
        graph = repository.get_revision_graph(revision_id)
 
131
        dotted = {}
 
132
        for n, revision_id, d, revno, e in tsort.merge_sort(graph, 
 
133
            revision_id, generate_revno=True):
 
134
            dotted[revision_id] = '.'.join(str(num) for num in revno)
 
135
        return dotted
 
136
 
 
137
    def _annotate(self, tree, file_id):
 
138
        current_revision = FakeRevision(CURRENT_REVISION)
 
139
        current_revision.committer = self.branch.get_config().username()
 
140
        current_revision.timestamp = time.time()
 
141
        current_revision.message = '[Not yet committed]'
 
142
        current_revision.parent_ids = tree.get_parent_ids()
 
143
        current_revision.properties['branch-nick'] = self.branch.nick
 
144
        current_revno = '%d?' % (self.branch.revno() + 1)
 
145
        repository = self.branch.repository
 
146
        if self.revision_id == CURRENT_REVISION:
 
147
            revision_id = self.branch.last_revision()
 
148
        else:
 
149
            revision_id = self.revision_id
 
150
        dotted = self._dotted_revnos(repository, revision_id)
 
151
        revision_cache = RevisionCache(repository, self.revisions)
 
152
        for origin, text in tree.annotate_iter(file_id):
 
153
            rev_id = origin
 
154
            if rev_id == CURRENT_REVISION:
 
155
                revision = current_revision
 
156
                revno = current_revno
 
157
            else:
 
158
                try:
 
159
                    revision = revision_cache.get_revision(rev_id)
 
160
                    revno = dotted.get(rev_id, 'merge')
 
161
                    if len(revno) > 15:
 
162
                        revno = 'merge'
 
163
                except NoSuchRevision:
 
164
                    revision = FakeRevision(rev_id)
 
165
                    revno = "?"
 
166
 
 
167
            yield revision, revno, text
 
168
 
 
169
    def _highlight_annotation(self, model, path, iter, now):
 
170
        revision_id, = model.get(iter, REVISION_ID_COL)
 
171
        revision = self.revisions[revision_id]
 
172
        model.set(iter, HIGHLIGHT_COLOR_COL,
 
173
                  self.annotate_colormap.get_color(revision, now))
 
174
 
 
175
    def _selected_revision(self):
 
176
        (path, col) = self.annoview.get_cursor()
 
177
        if path is None:
 
178
            return None
 
179
        return self.annomodel[path][REVISION_ID_COL]
 
180
 
 
181
    def _activate_selected_revision(self, w):
 
182
        rev_id = self._selected_revision()
 
183
        if rev_id is None:
 
184
            return
 
185
        selected = self.revisions[rev_id]
 
186
        self.logview.set_revision(selected)
 
187
        self.back_button.set_sensitive(len(selected.parent_ids) != 0)
 
188
 
 
189
    def _create(self):
 
190
        self.logview = self._create_log_view()
 
191
        self.annoview = self._create_annotate_view()
 
192
 
 
193
        vbox = gtk.VBox(False, 12)
 
194
        vbox.set_border_width(12)
 
195
        vbox.show()
 
196
 
 
197
        sw = gtk.ScrolledWindow()
 
198
        sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
 
199
        sw.set_shadow_type(gtk.SHADOW_IN)
 
200
        sw.add(self.annoview)
 
201
        self.annoview.gwindow = self
 
202
        sw.show()
 
203
 
 
204
        swbox = gtk.VBox()
 
205
        swbox.pack_start(sw)
 
206
        swbox.show()
 
207
        
 
208
        self.pane = pane = gtk.VPaned()
 
209
        pane.add1(swbox)
 
210
        pane.add2(self.logview)
 
211
        pane.show()
 
212
        vbox.pack_start(pane, expand=True, fill=True)
 
213
 
 
214
        self._search = SearchBox()
 
215
        swbox.pack_start(self._search, expand=False, fill=True)
 
216
        accels = gtk.AccelGroup()
 
217
        accels.connect_group(gtk.keysyms.f, gtk.gdk.CONTROL_MASK,
 
218
                             gtk.ACCEL_LOCKED,
 
219
                             self._search_by_text)
 
220
        accels.connect_group(gtk.keysyms.g, gtk.gdk.CONTROL_MASK,
 
221
                             gtk.ACCEL_LOCKED,
 
222
                             self._search_by_line)
 
223
        self.add_accel_group(accels)
 
224
 
 
225
        hbox = gtk.HBox(False, 6)
 
226
        self.back_button = self._create_back_button()
 
227
        hbox.pack_start(self.back_button, expand=False, fill=True)
 
228
        self.forward_button = self._create_forward_button()
 
229
        hbox.pack_start(self.forward_button, expand=False, fill=True)
 
230
        hbox.pack_end(self._create_button_box(), expand=False, fill=True)
 
231
        hbox.show()
 
232
        vbox.pack_start(hbox, expand=False, fill=True)
 
233
 
 
234
        self.add(vbox)
 
235
 
 
236
    def _search_by_text(self, accel_group, window, key, modifiers):
 
237
        self._search.show_for('text')
 
238
        self._search.set_target(self.annoview, TEXT_LINE_COL)
 
239
 
 
240
    def _search_by_line(self, accel_group, window, key, modifiers):
 
241
        self._search.show_for('line')
 
242
        self._search.set_target(self.annoview, LINE_NUM_COL)
 
243
 
 
244
    def row_diff(self, tv, path, tvc):
 
245
        row = path[0]
 
246
        revision = self.annotations[row]
 
247
        repository = self.branch.repository
 
248
        if revision.revision_id == CURRENT_REVISION:
 
249
            tree1 = self.tree
 
250
            tree2 = self.tree.basis_tree()
 
251
        else:
 
252
            tree1 = repository.revision_tree(revision.revision_id)
 
253
            if len(revision.parent_ids) > 0:
 
254
                tree2 = repository.revision_tree(revision.parent_ids[0])
 
255
            else:
 
256
                tree2 = repository.revision_tree(NULL_REVISION)
 
257
        from bzrlib.plugins.gtk.diff import DiffWindow
 
258
        window = DiffWindow()
 
259
        window.set_diff("Diff for row %d" % (row+1), tree1, tree2)
 
260
        window.set_file(tree1.id2path(self.file_id))
 
261
        window.show()
 
262
 
 
263
 
 
264
    def _create_annotate_view(self):
 
265
        tv = gtk.TreeView()
 
266
        tv.set_rules_hint(False)
 
267
        tv.connect("cursor-changed", self._activate_selected_revision)
 
268
        tv.show()
 
269
        tv.connect("row-activated", self.row_diff)
 
270
 
 
271
        cell = gtk.CellRendererText()
 
272
        cell.set_property("xalign", 1.0)
 
273
        cell.set_property("ypad", 0)
 
274
        cell.set_property("family", "Monospace")
 
275
        cell.set_property("cell-background-gdk",
 
276
                          tv.get_style().bg[gtk.STATE_NORMAL])
 
277
        col = gtk.TreeViewColumn()
 
278
        col.set_resizable(False)
 
279
        col.pack_start(cell, expand=True)
 
280
        col.add_attribute(cell, "text", LINE_NUM_COL)
 
281
        tv.append_column(col)
 
282
 
 
283
        cell = gtk.CellRendererText()
 
284
        cell.set_property("ypad", 0)
 
285
        cell.set_property("ellipsize", pango.ELLIPSIZE_END)
 
286
        cell.set_property("cell-background-gdk",
 
287
                          self.get_style().bg[gtk.STATE_NORMAL])
 
288
        col = gtk.TreeViewColumn("Committer")
 
289
        col.set_resizable(True)
 
290
        col.pack_start(cell, expand=True)
 
291
        col.add_attribute(cell, "text", COMMITTER_COL)
 
292
        tv.append_column(col)
 
293
 
 
294
        cell = gtk.CellRendererText()
 
295
        cell.set_property("xalign", 1.0)
 
296
        cell.set_property("ypad", 0)
 
297
        cell.set_property("cell-background-gdk",
 
298
                          self.get_style().bg[gtk.STATE_NORMAL])
 
299
        col = gtk.TreeViewColumn("Revno")
 
300
        col.set_resizable(False)
 
301
        col.pack_start(cell, expand=True)
 
302
        col.add_attribute(cell, "markup", REVNO_COL)
 
303
        tv.append_column(col)
 
304
 
 
305
        cell = gtk.CellRendererText()
 
306
        cell.set_property("ypad", 0)
 
307
        cell.set_property("family", "Monospace")
 
308
        col = gtk.TreeViewColumn()
 
309
        col.set_resizable(False)
 
310
        col.pack_start(cell, expand=True)
 
311
#        col.add_attribute(cell, "foreground", HIGHLIGHT_COLOR_COL)
 
312
        col.add_attribute(cell, "background", HIGHLIGHT_COLOR_COL)
 
313
        col.add_attribute(cell, "text", TEXT_LINE_COL)
 
314
        tv.append_column(col)
 
315
 
 
316
        # FIXME: Now that C-f is now used for search by text we
 
317
        # may as well disable the auto search.
 
318
        tv.set_search_column(LINE_NUM_COL)
 
319
 
 
320
        return tv
 
321
 
 
322
    def _create_log_view(self):
 
323
        lv = LogView()
 
324
        lv.show()
 
325
        return lv
 
326
 
 
327
    def _create_button_box(self):
 
328
        button = gtk.Button()
 
329
        button.set_use_stock(True)
 
330
        button.set_label("gtk-close")
 
331
        button.connect("clicked", lambda w: self.destroy())
 
332
        button.show()
 
333
        return button
 
334
 
 
335
    def _create_back_button(self):
 
336
        button = gtk.Button()
 
337
        button.set_use_stock(True)
 
338
        button.set_label("gtk-go-back")
 
339
        button.connect("clicked", lambda w: self.go_back())
 
340
        button.show()
 
341
        return button
 
342
 
 
343
    def _create_forward_button(self):
 
344
        button = gtk.Button()
 
345
        button.set_use_stock(True)
 
346
        button.set_label("gtk-go-forward")
 
347
        button.connect("clicked", lambda w: self.go_forward())
 
348
        button.show()
 
349
        button.set_sensitive(False)
 
350
        return button
 
351
 
 
352
    def go_back(self):
 
353
        self.history.append(self.tree)
 
354
        self.forward_button.set_sensitive(True)
 
355
        rev_id = self._selected_revision()
 
356
        parent_id = self.revisions[rev_id].parent_ids[0]
 
357
        target_tree = self.branch.repository.revision_tree(parent_id)
 
358
        self._go(target_tree)
 
359
 
 
360
    def go_forward(self):
 
361
        if len(self.history) == 0:
 
362
            return
 
363
        target_tree = self.history.pop()
 
364
        if len(self.history) == 0:
 
365
            self.forward_button.set_sensitive(False)
 
366
        self._go(target_tree)
 
367
 
 
368
    def _go(self, target_tree):
 
369
        rev_id = self._selected_revision()
 
370
        if self.file_id in target_tree:
 
371
            offset = self.get_scroll_offset(target_tree)
 
372
            (row,), col = self.annoview.get_cursor()
 
373
            self.annotate(target_tree, self.branch, self.file_id)
 
374
            new_row = row+offset
 
375
            if new_row < 0:
 
376
                new_row = 0
 
377
            self.annoview.set_cursor(new_row)
 
378
 
 
379
    def get_scroll_offset(self, tree):
 
380
        old = self.tree.get_file(self.file_id)
 
381
        new = tree.get_file(self.file_id)
 
382
        (row,), col = self.annoview.get_cursor()
 
383
        matcher = patiencediff.PatienceSequenceMatcher(None, old.readlines(),
 
384
                                                       new.readlines())
 
385
        for i, j, n in matcher.get_matching_blocks():
 
386
            if i + n >= row:
 
387
                return j - i
 
388
 
 
389
 
 
390
 
 
391
class FakeRevision:
 
392
    """ A fake revision.
 
393
 
 
394
    For when a revision is referenced but not present.
 
395
    """
 
396
 
 
397
    def __init__(self, revision_id, committer='?', nick=None):
 
398
        self.revision_id = revision_id
 
399
        self.parent_ids = []
 
400
        self.committer = committer
 
401
        self.message = "?"
 
402
        self.timestamp = 0.0
 
403
        self.timezone = 0
 
404
        self.properties = {}
 
405
 
 
406
 
 
407
class RevisionCache(object):
 
408
    """A caching revision source"""
 
409
    def __init__(self, real_source, seed_cache=None):
 
410
        self.__real_source = real_source
 
411
        if seed_cache is None:
 
412
            self.__cache = {}
 
413
        else:
 
414
            self.__cache = dict(seed_cache)
 
415
 
 
416
    def get_revision(self, revision_id):
 
417
        if revision_id not in self.__cache:
 
418
            revision = self.__real_source.get_revision(revision_id)
 
419
            self.__cache[revision_id] = revision
 
420
        return self.__cache[revision_id]
 
421
 
 
422
class SearchBox(gtk.HBox):
 
423
    """A button box for searching in text or lines of annotations"""
 
424
    def __init__(self):
 
425
        gtk.HBox.__init__(self, False, 6)
 
426
 
 
427
        # Close button
 
428
        button = gtk.Button()
 
429
        image = gtk.Image()
 
430
        image.set_from_stock('gtk-stop', gtk.ICON_SIZE_BUTTON)
 
431
        button.set_image(image)
 
432
        button.set_relief(gtk.RELIEF_NONE)
 
433
        button.connect("clicked", lambda w: self.hide_all())
 
434
        self.pack_start(button, expand=False, fill=False)
 
435
 
 
436
        # Search entry
 
437
        label = gtk.Label()
 
438
        self._label = label
 
439
        self.pack_start(label, expand=False, fill=False)
 
440
 
 
441
        entry = gtk.Entry()
 
442
        self._entry = entry
 
443
        entry.connect("activate", lambda w, d: self._do_search(d),
 
444
                      'forward')
 
445
        self.pack_start(entry, expand=False, fill=False)
 
446
 
 
447
        # Next/previous buttons
 
448
        button = gtk.Button('_Next')
 
449
        image = gtk.Image()
 
450
        image.set_from_stock('gtk-go-forward', gtk.ICON_SIZE_BUTTON)
 
451
        button.set_image(image)
 
452
        button.connect("clicked", lambda w, d: self._do_search(d),
 
453
                       'forward')
 
454
        self.pack_start(button, expand=False, fill=False)
 
455
 
 
456
        button = gtk.Button('_Previous')
 
457
        image = gtk.Image()
 
458
        image.set_from_stock('gtk-go-back', gtk.ICON_SIZE_BUTTON)
 
459
        button.set_image(image)
 
460
        button.connect("clicked", lambda w, d: self._do_search(d),
 
461
                       'backward')
 
462
        self.pack_start(button, expand=False, fill=False)
 
463
 
 
464
        # Search options
 
465
        check = gtk.CheckButton('Match case')
 
466
        self._match_case = check
 
467
        self.pack_start(check, expand=False, fill=False)
 
468
 
 
469
        check = gtk.CheckButton('Regexp')
 
470
        check.connect("toggled", lambda w: self._set_label())
 
471
        self._regexp = check
 
472
        self.pack_start(check, expand=False, fill=False)
 
473
 
 
474
        self._view = None
 
475
        self._column = None
 
476
        # Note that we stay hidden (we do not call self.show_all())
 
477
 
 
478
 
 
479
    def show_for(self, kind):
 
480
        self._kind = kind
 
481
        self.show_all()
 
482
        self._set_label()
 
483
        # Hide unrelated buttons
 
484
        if kind == 'line':
 
485
            self._match_case.hide()
 
486
            self._regexp.hide()
 
487
        # Be ready
 
488
        self._entry.grab_focus()
 
489
 
 
490
    def _set_label(self):
 
491
        if self._kind == 'line':
 
492
            self._label.set_text('Find Line: ')
 
493
        else:
 
494
            if self._regexp.get_active():
 
495
                self._label.set_text('Find Regexp: ')
 
496
            else:
 
497
                self._label.set_text('Find Text: ')
 
498
 
 
499
    def set_target(self, view,column):
 
500
        self._view = view
 
501
        self._column = column
 
502
 
 
503
    def _match(self, model, iterator, column):
 
504
        matching_case = self._match_case.get_active()
 
505
        string, = model.get(iterator, column)
 
506
        key = self._entry.get_text()
 
507
        if self._regexp.get_active():
 
508
            if matching_case:
 
509
                match = re.compile(key).search(string, 1)
 
510
            else:
 
511
                match = re.compile(key, re.I).search(string, 1)
 
512
        else:
 
513
            if not matching_case:
 
514
                string = string.lower()
 
515
                key = key.lower()
 
516
            match = string.find(key) != -1
 
517
 
 
518
        return match
 
519
 
 
520
    def _iterate_rows_forward(self, model, start):
 
521
        model_size = len(model)
 
522
        current = start + 1
 
523
        while model_size != 0:
 
524
            if current >= model_size: current =  0
 
525
            yield model.get_iter_from_string('%d' % current)
 
526
            if current == start: raise StopIteration
 
527
            current += 1
 
528
 
 
529
    def _iterate_rows_backward(self, model, start):
 
530
        model_size = len(model)
 
531
        current = start - 1
 
532
        while model_size != 0:
 
533
            if current < 0: current = model_size - 1
 
534
            yield model.get_iter_from_string('%d' % current)
 
535
            if current == start: raise StopIteration
 
536
            current -= 1
 
537
 
 
538
    def _do_search(self, direction):
 
539
        if direction == 'forward':
 
540
            iterate = self._iterate_rows_forward
 
541
        else:
 
542
            iterate = self._iterate_rows_backward
 
543
 
 
544
        model, sel = self._view.get_selection().get_selected()
 
545
        if sel is None:
 
546
            start = 0
 
547
        else:
 
548
            path = model.get_string_from_iter(sel)
 
549
            start = int(path)
 
550
 
 
551
        for row in iterate(model, start):
 
552
            if self._match(model, row, self._column):
 
553
                path = model.get_path(row)
 
554
                self._view.set_cursor(path)
 
555
                self._view.scroll_to_cell(path, use_align=True)
 
556
                break