/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: Scott James Remnant
  • Date: 2005-10-17 01:07:49 UTC
  • Revision ID: scott@netsplit.com-20051017010749-15fa95fc2cf09289
Commit the first version of bzrk.

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