/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: Martin Albisetti
  • Date: 2008-03-04 22:13:43 UTC
  • mto: This revision was merged to the branch mainline in revision 439.
  • Revision ID: argentina@gmail.com-20080304221343-ixbdn2uf87z3jnfl
Added custom colored context icons

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.revisionview import RevisionView
 
32
from bzrlib.plugins.gtk.window import Window
 
33
 
 
34
 
 
35
(
 
36
    REVISION_ID_COL,
 
37
    LINE_NUM_COL,
 
38
    COMMITTER_COL,
 
39
    REVNO_COL,
 
40
    HIGHLIGHT_COLOR_COL,
 
41
    TEXT_LINE_COL
 
42
) = range(6)
 
43
 
 
44
 
 
45
class GAnnotateWindow(Window):
 
46
    """Annotate window."""
 
47
 
 
48
    def __init__(self, all=False, plain=False, parent=None):
 
49
        self.all = all
 
50
        self.plain = plain
 
51
        
 
52
        Window.__init__(self, parent)
 
53
        
 
54
        self.set_icon(self.render_icon(gtk.STOCK_FIND, gtk.ICON_SIZE_BUTTON))
 
55
        self.annotate_colormap = AnnotateColorSaturation()
 
56
 
 
57
        self._create()
 
58
        self.revisions = {}
 
59
        self.history = []
 
60
        self._no_back = set()
 
61
 
 
62
    def annotate(self, tree, branch, file_id):
 
63
        self.annotations = []
 
64
        self.branch = branch
 
65
        self.tree = tree
 
66
        self.file_id = file_id
 
67
        self.revisionview.set_file_id(file_id)
 
68
        self.revision_id = getattr(tree, 'get_revision_id', 
 
69
                                   lambda: CURRENT_REVISION)()
 
70
        
 
71
        # [revision id, line number, author, revno, highlight color, line]
 
72
        self.annomodel = gtk.ListStore(gobject.TYPE_STRING,
 
73
                                       gobject.TYPE_STRING,
 
74
                                       gobject.TYPE_STRING,
 
75
                                       gobject.TYPE_STRING,
 
76
                                       gobject.TYPE_STRING,
 
77
                                       gobject.TYPE_STRING)
 
78
        
 
79
        last_seen = None
 
80
        try:
 
81
            branch.lock_read()
 
82
            branch.repository.lock_read()
 
83
            self.dotted = {}
 
84
            revno_map = self.branch.get_revision_id_to_revno_map()
 
85
            for revision_id, revno in revno_map.iteritems():
 
86
                self.dotted[revision_id] = '.'.join(str(num) for num in revno)
 
87
            for line_no, (revision, revno, line)\
 
88
                    in enumerate(self._annotate(tree, file_id)):
 
89
                if revision.revision_id == last_seen and not self.all:
 
90
                    revno = author = ""
 
91
                else:
 
92
                    last_seen = revision.revision_id
 
93
                    author = revision.get_apparent_author()
 
94
 
 
95
                if revision.revision_id not in self.revisions:
 
96
                    self.revisions[revision.revision_id] = revision
 
97
 
 
98
                self.annomodel.append([revision.revision_id,
 
99
                                       line_no + 1,
 
100
                                       author,
 
101
                                       revno,
 
102
                                       None,
 
103
                                       line.rstrip("\r\n")
 
104
                                      ])
 
105
                self.annotations.append(revision)
 
106
 
 
107
            if not self.plain:
 
108
                now = time.time()
 
109
                self.annomodel.foreach(self._highlight_annotation, now)
 
110
        finally:
 
111
            branch.repository.unlock()
 
112
            branch.unlock()
 
113
 
 
114
        self.annoview.set_model(self.annomodel)
 
115
        self.annoview.grab_focus()
 
116
 
 
117
    def jump_to_line(self, lineno):
 
118
        if lineno > len(self.annomodel) or lineno < 1:
 
119
            row = 0
 
120
            # FIXME:should really deal with this in the gui. Perhaps a status
 
121
            # bar?
 
122
            print("gannotate: Line number %d does't exist. Defaulting to "
 
123
                  "line 1." % lineno)
 
124
            return
 
125
        else:
 
126
            row = lineno - 1
 
127
 
 
128
        self.annoview.set_cursor(row)
 
129
        self.annoview.scroll_to_cell(row, use_align=True)
 
130
 
 
131
 
 
132
    def _annotate(self, tree, file_id):
 
133
        current_revision = FakeRevision(CURRENT_REVISION)
 
134
        current_revision.committer = self.branch.get_config().username()
 
135
        current_revision.timestamp = time.time()
 
136
        current_revision.message = '[Not yet committed]'
 
137
        current_revision.parent_ids = tree.get_parent_ids()
 
138
        current_revision.properties['branch-nick'] = self.branch.nick
 
139
        current_revno = '%d?' % (self.branch.revno() + 1)
 
140
        repository = self.branch.repository
 
141
        if self.revision_id == CURRENT_REVISION:
 
142
            revision_id = self.branch.last_revision()
 
143
        else:
 
144
            revision_id = self.revision_id
 
145
        revision_cache = RevisionCache(repository, self.revisions)
 
146
        for origin, text in tree.annotate_iter(file_id):
 
147
            rev_id = origin
 
148
            if rev_id == CURRENT_REVISION:
 
149
                revision = current_revision
 
150
                revno = current_revno
 
151
            else:
 
152
                try:
 
153
                    revision = revision_cache.get_revision(rev_id)
 
154
                    revno = self.dotted.get(rev_id, 'merge')
 
155
                    if len(revno) > 15:
 
156
                        revno = 'merge'
 
157
                except NoSuchRevision:
 
158
                    revision = FakeRevision(rev_id)
 
159
                    revno = "?"
 
160
 
 
161
            yield revision, revno, text
 
162
 
 
163
    def _highlight_annotation(self, model, path, iter, now):
 
164
        revision_id, = model.get(iter, REVISION_ID_COL)
 
165
        revision = self.revisions[revision_id]
 
166
        model.set(iter, HIGHLIGHT_COLOR_COL,
 
167
                  self.annotate_colormap.get_color(revision, now))
 
168
 
 
169
    def _selected_revision(self):
 
170
        (path, col) = self.annoview.get_cursor()
 
171
        if path is None:
 
172
            return None
 
173
        return self.annomodel[path][REVISION_ID_COL]
 
174
 
 
175
    def _activate_selected_revision(self, w):
 
176
        rev_id = self._selected_revision()
 
177
        if rev_id is None:
 
178
            return
 
179
        selected = self.revisions[rev_id]
 
180
        self.revisionview.set_revision(selected)
 
181
        if (len(selected.parent_ids) != 0 and selected.parent_ids[0] not in
 
182
            self._no_back):
 
183
            enable_back = True
 
184
        else:
 
185
            enable_back = False
 
186
        self.back_button.set_sensitive(enable_back)
 
187
 
 
188
    def _create(self):
 
189
        self.revisionview = self._create_log_view()
 
190
        self.annoview = self._create_annotate_view()
 
191
 
 
192
        vbox = gtk.VBox(False)
 
193
        vbox.show()
 
194
 
 
195
        sw = gtk.ScrolledWindow()
 
196
        sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
 
197
        sw.set_shadow_type(gtk.SHADOW_IN)
 
198
        sw.add(self.annoview)
 
199
        self.annoview.gwindow = self
 
200
        sw.show()
 
201
 
 
202
        swbox = gtk.VBox()
 
203
        swbox.pack_start(sw)
 
204
        swbox.show()
 
205
 
 
206
        hbox = gtk.HBox(False, 6)
 
207
        self.back_button = self._create_back_button()
 
208
        hbox.pack_start(self.back_button, expand=False, fill=True)
 
209
        self.forward_button = self._create_forward_button()
 
210
        hbox.pack_start(self.forward_button, expand=False, fill=True)
 
211
        hbox.show()
 
212
        vbox.pack_start(hbox, expand=False, fill=True)
 
213
        
 
214
        self.pane = pane = gtk.VPaned()
 
215
        pane.add1(swbox)
 
216
        pane.add2(self.revisionview)
 
217
        pane.show()
 
218
        vbox.pack_start(pane, expand=True, fill=True)
 
219
 
 
220
        self._search = SearchBox()
 
221
        swbox.pack_start(self._search, expand=False, fill=True)
 
222
        accels = gtk.AccelGroup()
 
223
        accels.connect_group(gtk.keysyms.f, gtk.gdk.CONTROL_MASK,
 
224
                             gtk.ACCEL_LOCKED,
 
225
                             self._search_by_text)
 
226
        accels.connect_group(gtk.keysyms.g, gtk.gdk.CONTROL_MASK,
 
227
                             gtk.ACCEL_LOCKED,
 
228
                             self._search_by_line)
 
229
        self.add_accel_group(accels)
 
230
 
 
231
        self.add(vbox)
 
232
 
 
233
    def _search_by_text(self, accel_group, window, key, modifiers):
 
234
        self._search.show_for('text')
 
235
        self._search.set_target(self.annoview, TEXT_LINE_COL)
 
236
 
 
237
    def _search_by_line(self, accel_group, window, key, modifiers):
 
238
        self._search.show_for('line')
 
239
        self._search.set_target(self.annoview, LINE_NUM_COL)
 
240
 
 
241
    def row_diff(self, tv, path, tvc):
 
242
        row = path[0]
 
243
        revision = self.annotations[row]
 
244
        repository = self.branch.repository
 
245
        if revision.revision_id == CURRENT_REVISION:
 
246
            tree1 = self.tree
 
247
            tree2 = self.tree.basis_tree()
 
248
        else:
 
249
            tree1 = repository.revision_tree(revision.revision_id)
 
250
            if len(revision.parent_ids) > 0:
 
251
                tree2 = repository.revision_tree(revision.parent_ids[0])
 
252
            else:
 
253
                tree2 = repository.revision_tree(NULL_REVISION)
 
254
        from bzrlib.plugins.gtk.diff import DiffWindow
 
255
        window = DiffWindow()
 
256
        window.set_diff("Diff for row %d" % (row+1), tree1, tree2)
 
257
        window.set_file(tree1.id2path(self.file_id))
 
258
        window.show()
 
259
 
 
260
 
 
261
    def _create_annotate_view(self):
 
262
        tv = gtk.TreeView()
 
263
        tv.set_rules_hint(False)
 
264
        tv.connect("cursor-changed", self._activate_selected_revision)
 
265
        tv.show()
 
266
        tv.connect("row-activated", self.row_diff)
 
267
 
 
268
        cell = gtk.CellRendererText()
 
269
        cell.set_property("xalign", 1.0)
 
270
        cell.set_property("ypad", 0)
 
271
        cell.set_property("family", "Monospace")
 
272
        cell.set_property("cell-background-gdk",
 
273
                          tv.get_style().bg[gtk.STATE_NORMAL])
 
274
        col = gtk.TreeViewColumn()
 
275
        col.set_resizable(False)
 
276
        col.pack_start(cell, expand=True)
 
277
        col.add_attribute(cell, "text", LINE_NUM_COL)
 
278
        tv.append_column(col)
 
279
 
 
280
        cell = gtk.CellRendererText()
 
281
        cell.set_property("ypad", 0)
 
282
        cell.set_property("ellipsize", pango.ELLIPSIZE_END)
 
283
        cell.set_property("cell-background-gdk",
 
284
                          self.get_style().bg[gtk.STATE_NORMAL])
 
285
        col = gtk.TreeViewColumn("Committer")
 
286
        col.set_resizable(True)
 
287
        col.pack_start(cell, expand=True)
 
288
        col.add_attribute(cell, "text", COMMITTER_COL)
 
289
        tv.append_column(col)
 
290
 
 
291
        cell = gtk.CellRendererText()
 
292
        cell.set_property("xalign", 1.0)
 
293
        cell.set_property("ypad", 0)
 
294
        cell.set_property("cell-background-gdk",
 
295
                          self.get_style().bg[gtk.STATE_NORMAL])
 
296
        col = gtk.TreeViewColumn("Revno")
 
297
        col.set_resizable(False)
 
298
        col.pack_start(cell, expand=True)
 
299
        col.add_attribute(cell, "markup", REVNO_COL)
 
300
        tv.append_column(col)
 
301
 
 
302
        cell = gtk.CellRendererText()
 
303
        cell.set_property("ypad", 0)
 
304
        cell.set_property("family", "Monospace")
 
305
        col = gtk.TreeViewColumn()
 
306
        col.set_resizable(False)
 
307
        col.pack_start(cell, expand=True)
 
308
#        col.add_attribute(cell, "foreground", HIGHLIGHT_COLOR_COL)
 
309
        col.add_attribute(cell, "background", HIGHLIGHT_COLOR_COL)
 
310
        col.add_attribute(cell, "text", TEXT_LINE_COL)
 
311
        tv.append_column(col)
 
312
 
 
313
        # FIXME: Now that C-f is now used for search by text we
 
314
        # may as well disable the auto search.
 
315
        tv.set_search_column(LINE_NUM_COL)
 
316
 
 
317
        return tv
 
318
 
 
319
    def _create_log_view(self):
 
320
        lv = RevisionView()
 
321
        lv.show()
 
322
        return lv
 
323
 
 
324
    def _create_back_button(self):
 
325
        button = gtk.Button()
 
326
        button.set_use_stock(True)
 
327
        button.set_label("gtk-go-back")
 
328
        button.connect("clicked", lambda w: self.go_back())
 
329
        button.set_relief(gtk.RELIEF_NONE)
 
330
        button.show()
 
331
        return button
 
332
 
 
333
    def _create_forward_button(self):
 
334
        button = gtk.Button()
 
335
        button.set_use_stock(True)
 
336
        button.set_label("gtk-go-forward")
 
337
        button.connect("clicked", lambda w: self.go_forward())
 
338
        button.set_relief(gtk.RELIEF_NONE)
 
339
        button.show()
 
340
        button.set_sensitive(False)
 
341
        return button
 
342
 
 
343
    def go_back(self):
 
344
        last_tree = self.tree
 
345
        rev_id = self._selected_revision()
 
346
        parent_id = self.revisions[rev_id].parent_ids[0]
 
347
        target_tree = self.branch.repository.revision_tree(parent_id)
 
348
        if self._go(target_tree):
 
349
            self.history.append(last_tree)
 
350
            self.forward_button.set_sensitive(True)
 
351
        else:
 
352
            self._no_back.add(parent_id)
 
353
            self.back_button.set_sensitive(False)
 
354
 
 
355
    def go_forward(self):
 
356
        if len(self.history) == 0:
 
357
            return
 
358
        target_tree = self.history.pop()
 
359
        if len(self.history) == 0:
 
360
            self.forward_button.set_sensitive(False)
 
361
        self._go(target_tree)
 
362
 
 
363
    def _go(self, target_tree):
 
364
        rev_id = self._selected_revision()
 
365
        if self.file_id in target_tree:
 
366
            offset = self.get_scroll_offset(target_tree)
 
367
            (row,), col = self.annoview.get_cursor()
 
368
            self.annotate(target_tree, self.branch, self.file_id)
 
369
            new_row = row+offset
 
370
            if new_row < 0:
 
371
                new_row = 0
 
372
            self.annoview.set_cursor(new_row)
 
373
            return True
 
374
        else:
 
375
            return False
 
376
 
 
377
    def get_scroll_offset(self, tree):
 
378
        old = self.tree.get_file(self.file_id)
 
379
        new = tree.get_file(self.file_id)
 
380
        (row,), col = self.annoview.get_cursor()
 
381
        matcher = patiencediff.PatienceSequenceMatcher(None, old.readlines(),
 
382
                                                       new.readlines())
 
383
        for i, j, n in matcher.get_matching_blocks():
 
384
            if i + n >= row:
 
385
                return j - i
 
386
 
 
387
 
 
388
class FakeRevision:
 
389
    """ A fake revision.
 
390
 
 
391
    For when a revision is referenced but not present.
 
392
    """
 
393
 
 
394
    def __init__(self, revision_id, committer='?', nick=None):
 
395
        self.revision_id = revision_id
 
396
        self.parent_ids = []
 
397
        self.committer = committer
 
398
        self.message = "?"
 
399
        self.timestamp = 0.0
 
400
        self.timezone = 0
 
401
        self.properties = {}
 
402
 
 
403
    def get_apparent_author(self):
 
404
        return self.committer
 
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