/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-19 01:56:10 UTC
  • Revision ID: jelmer@samba.org-20070319015610-o3z2oyx138a8h5uv
Implement simple GTK+ progress bars. 

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)
 
194
        vbox.show()
 
195
 
 
196
        sw = gtk.ScrolledWindow()
 
197
        sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
 
198
        sw.set_shadow_type(gtk.SHADOW_IN)
 
199
        sw.add(self.annoview)
 
200
        self.annoview.gwindow = self
 
201
        sw.show()
 
202
 
 
203
        swbox = gtk.VBox()
 
204
        swbox.pack_start(sw)
 
205
        swbox.show()
 
206
 
 
207
        hbox = gtk.HBox(False, 6)
 
208
        self.back_button = self._create_back_button()
 
209
        hbox.pack_start(self.back_button, expand=False, fill=True)
 
210
        self.forward_button = self._create_forward_button()
 
211
        hbox.pack_start(self.forward_button, expand=False, fill=True)
 
212
        hbox.show()
 
213
        vbox.pack_start(hbox, expand=False, fill=True)
 
214
        
 
215
        self.pane = pane = gtk.VPaned()
 
216
        pane.add1(swbox)
 
217
        pane.add2(self.logview)
 
218
        pane.show()
 
219
        vbox.pack_start(pane, expand=True, fill=True)
 
220
 
 
221
        self._search = SearchBox()
 
222
        swbox.pack_start(self._search, expand=False, fill=True)
 
223
        accels = gtk.AccelGroup()
 
224
        accels.connect_group(gtk.keysyms.f, gtk.gdk.CONTROL_MASK,
 
225
                             gtk.ACCEL_LOCKED,
 
226
                             self._search_by_text)
 
227
        accels.connect_group(gtk.keysyms.g, gtk.gdk.CONTROL_MASK,
 
228
                             gtk.ACCEL_LOCKED,
 
229
                             self._search_by_line)
 
230
        self.add_accel_group(accels)
 
231
 
 
232
        self.add(vbox)
 
233
 
 
234
    def _search_by_text(self, accel_group, window, key, modifiers):
 
235
        self._search.show_for('text')
 
236
        self._search.set_target(self.annoview, TEXT_LINE_COL)
 
237
 
 
238
    def _search_by_line(self, accel_group, window, key, modifiers):
 
239
        self._search.show_for('line')
 
240
        self._search.set_target(self.annoview, LINE_NUM_COL)
 
241
 
 
242
    def row_diff(self, tv, path, tvc):
 
243
        row = path[0]
 
244
        revision = self.annotations[row]
 
245
        repository = self.branch.repository
 
246
        if revision.revision_id == CURRENT_REVISION:
 
247
            tree1 = self.tree
 
248
            tree2 = self.tree.basis_tree()
 
249
        else:
 
250
            tree1 = repository.revision_tree(revision.revision_id)
 
251
            if len(revision.parent_ids) > 0:
 
252
                tree2 = repository.revision_tree(revision.parent_ids[0])
 
253
            else:
 
254
                tree2 = repository.revision_tree(NULL_REVISION)
 
255
        from bzrlib.plugins.gtk.diff import DiffWindow
 
256
        window = DiffWindow()
 
257
        window.set_diff("Diff for row %d" % (row+1), tree1, tree2)
 
258
        window.set_file(tree1.id2path(self.file_id))
 
259
        window.show()
 
260
 
 
261
 
 
262
    def _create_annotate_view(self):
 
263
        tv = gtk.TreeView()
 
264
        tv.set_rules_hint(False)
 
265
        tv.connect("cursor-changed", self._activate_selected_revision)
 
266
        tv.show()
 
267
        tv.connect("row-activated", self.row_diff)
 
268
 
 
269
        cell = gtk.CellRendererText()
 
270
        cell.set_property("xalign", 1.0)
 
271
        cell.set_property("ypad", 0)
 
272
        cell.set_property("family", "Monospace")
 
273
        cell.set_property("cell-background-gdk",
 
274
                          tv.get_style().bg[gtk.STATE_NORMAL])
 
275
        col = gtk.TreeViewColumn()
 
276
        col.set_resizable(False)
 
277
        col.pack_start(cell, expand=True)
 
278
        col.add_attribute(cell, "text", LINE_NUM_COL)
 
279
        tv.append_column(col)
 
280
 
 
281
        cell = gtk.CellRendererText()
 
282
        cell.set_property("ypad", 0)
 
283
        cell.set_property("ellipsize", pango.ELLIPSIZE_END)
 
284
        cell.set_property("cell-background-gdk",
 
285
                          self.get_style().bg[gtk.STATE_NORMAL])
 
286
        col = gtk.TreeViewColumn("Committer")
 
287
        col.set_resizable(True)
 
288
        col.pack_start(cell, expand=True)
 
289
        col.add_attribute(cell, "text", COMMITTER_COL)
 
290
        tv.append_column(col)
 
291
 
 
292
        cell = gtk.CellRendererText()
 
293
        cell.set_property("xalign", 1.0)
 
294
        cell.set_property("ypad", 0)
 
295
        cell.set_property("cell-background-gdk",
 
296
                          self.get_style().bg[gtk.STATE_NORMAL])
 
297
        col = gtk.TreeViewColumn("Revno")
 
298
        col.set_resizable(False)
 
299
        col.pack_start(cell, expand=True)
 
300
        col.add_attribute(cell, "markup", REVNO_COL)
 
301
        tv.append_column(col)
 
302
 
 
303
        cell = gtk.CellRendererText()
 
304
        cell.set_property("ypad", 0)
 
305
        cell.set_property("family", "Monospace")
 
306
        col = gtk.TreeViewColumn()
 
307
        col.set_resizable(False)
 
308
        col.pack_start(cell, expand=True)
 
309
#        col.add_attribute(cell, "foreground", HIGHLIGHT_COLOR_COL)
 
310
        col.add_attribute(cell, "background", HIGHLIGHT_COLOR_COL)
 
311
        col.add_attribute(cell, "text", TEXT_LINE_COL)
 
312
        tv.append_column(col)
 
313
 
 
314
        # FIXME: Now that C-f is now used for search by text we
 
315
        # may as well disable the auto search.
 
316
        tv.set_search_column(LINE_NUM_COL)
 
317
 
 
318
        return tv
 
319
 
 
320
    def _create_log_view(self):
 
321
        lv = LogView()
 
322
        lv.show()
 
323
        return lv
 
324
 
 
325
    def _create_back_button(self):
 
326
        button = gtk.Button()
 
327
        button.set_use_stock(True)
 
328
        button.set_label("gtk-go-back")
 
329
        button.connect("clicked", lambda w: self.go_back())
 
330
        button.set_relief(gtk.RELIEF_NONE)
 
331
        button.show()
 
332
        return button
 
333
 
 
334
    def _create_forward_button(self):
 
335
        button = gtk.Button()
 
336
        button.set_use_stock(True)
 
337
        button.set_label("gtk-go-forward")
 
338
        button.connect("clicked", lambda w: self.go_forward())
 
339
        button.set_relief(gtk.RELIEF_NONE)
 
340
        button.show()
 
341
        button.set_sensitive(False)
 
342
        return button
 
343
 
 
344
    def go_back(self):
 
345
        self.history.append(self.tree)
 
346
        self.forward_button.set_sensitive(True)
 
347
        rev_id = self._selected_revision()
 
348
        parent_id = self.revisions[rev_id].parent_ids[0]
 
349
        target_tree = self.branch.repository.revision_tree(parent_id)
 
350
        self._go(target_tree)
 
351
 
 
352
    def go_forward(self):
 
353
        if len(self.history) == 0:
 
354
            return
 
355
        target_tree = self.history.pop()
 
356
        if len(self.history) == 0:
 
357
            self.forward_button.set_sensitive(False)
 
358
        self._go(target_tree)
 
359
 
 
360
    def _go(self, target_tree):
 
361
        rev_id = self._selected_revision()
 
362
        if self.file_id in target_tree:
 
363
            offset = self.get_scroll_offset(target_tree)
 
364
            (row,), col = self.annoview.get_cursor()
 
365
            self.annotate(target_tree, self.branch, self.file_id)
 
366
            new_row = row+offset
 
367
            if new_row < 0:
 
368
                new_row = 0
 
369
            self.annoview.set_cursor(new_row)
 
370
 
 
371
    def get_scroll_offset(self, tree):
 
372
        old = self.tree.get_file(self.file_id)
 
373
        new = tree.get_file(self.file_id)
 
374
        (row,), col = self.annoview.get_cursor()
 
375
        matcher = patiencediff.PatienceSequenceMatcher(None, old.readlines(),
 
376
                                                       new.readlines())
 
377
        for i, j, n in matcher.get_matching_blocks():
 
378
            if i + n >= row:
 
379
                return j - i
 
380
 
 
381
 
 
382
 
 
383
class FakeRevision:
 
384
    """ A fake revision.
 
385
 
 
386
    For when a revision is referenced but not present.
 
387
    """
 
388
 
 
389
    def __init__(self, revision_id, committer='?', nick=None):
 
390
        self.revision_id = revision_id
 
391
        self.parent_ids = []
 
392
        self.committer = committer
 
393
        self.message = "?"
 
394
        self.timestamp = 0.0
 
395
        self.timezone = 0
 
396
        self.properties = {}
 
397
 
 
398
 
 
399
class RevisionCache(object):
 
400
    """A caching revision source"""
 
401
    def __init__(self, real_source, seed_cache=None):
 
402
        self.__real_source = real_source
 
403
        if seed_cache is None:
 
404
            self.__cache = {}
 
405
        else:
 
406
            self.__cache = dict(seed_cache)
 
407
 
 
408
    def get_revision(self, revision_id):
 
409
        if revision_id not in self.__cache:
 
410
            revision = self.__real_source.get_revision(revision_id)
 
411
            self.__cache[revision_id] = revision
 
412
        return self.__cache[revision_id]
 
413
 
 
414
class SearchBox(gtk.HBox):
 
415
    """A button box for searching in text or lines of annotations"""
 
416
    def __init__(self):
 
417
        gtk.HBox.__init__(self, False, 6)
 
418
 
 
419
        # Close button
 
420
        button = gtk.Button()
 
421
        image = gtk.Image()
 
422
        image.set_from_stock('gtk-stop', gtk.ICON_SIZE_BUTTON)
 
423
        button.set_image(image)
 
424
        button.set_relief(gtk.RELIEF_NONE)
 
425
        button.connect("clicked", lambda w: self.hide_all())
 
426
        self.pack_start(button, expand=False, fill=False)
 
427
 
 
428
        # Search entry
 
429
        label = gtk.Label()
 
430
        self._label = label
 
431
        self.pack_start(label, expand=False, fill=False)
 
432
 
 
433
        entry = gtk.Entry()
 
434
        self._entry = entry
 
435
        entry.connect("activate", lambda w, d: self._do_search(d),
 
436
                      'forward')
 
437
        self.pack_start(entry, expand=False, fill=False)
 
438
 
 
439
        # Next/previous buttons
 
440
        button = gtk.Button('_Next')
 
441
        image = gtk.Image()
 
442
        image.set_from_stock('gtk-go-forward', gtk.ICON_SIZE_BUTTON)
 
443
        button.set_image(image)
 
444
        button.connect("clicked", lambda w, d: self._do_search(d),
 
445
                       'forward')
 
446
        self.pack_start(button, expand=False, fill=False)
 
447
 
 
448
        button = gtk.Button('_Previous')
 
449
        image = gtk.Image()
 
450
        image.set_from_stock('gtk-go-back', gtk.ICON_SIZE_BUTTON)
 
451
        button.set_image(image)
 
452
        button.connect("clicked", lambda w, d: self._do_search(d),
 
453
                       'backward')
 
454
        self.pack_start(button, expand=False, fill=False)
 
455
 
 
456
        # Search options
 
457
        check = gtk.CheckButton('Match case')
 
458
        self._match_case = check
 
459
        self.pack_start(check, expand=False, fill=False)
 
460
 
 
461
        check = gtk.CheckButton('Regexp')
 
462
        check.connect("toggled", lambda w: self._set_label())
 
463
        self._regexp = check
 
464
        self.pack_start(check, expand=False, fill=False)
 
465
 
 
466
        self._view = None
 
467
        self._column = None
 
468
        # Note that we stay hidden (we do not call self.show_all())
 
469
 
 
470
 
 
471
    def show_for(self, kind):
 
472
        self._kind = kind
 
473
        self.show_all()
 
474
        self._set_label()
 
475
        # Hide unrelated buttons
 
476
        if kind == 'line':
 
477
            self._match_case.hide()
 
478
            self._regexp.hide()
 
479
        # Be ready
 
480
        self._entry.grab_focus()
 
481
 
 
482
    def _set_label(self):
 
483
        if self._kind == 'line':
 
484
            self._label.set_text('Find Line: ')
 
485
        else:
 
486
            if self._regexp.get_active():
 
487
                self._label.set_text('Find Regexp: ')
 
488
            else:
 
489
                self._label.set_text('Find Text: ')
 
490
 
 
491
    def set_target(self, view,column):
 
492
        self._view = view
 
493
        self._column = column
 
494
 
 
495
    def _match(self, model, iterator, column):
 
496
        matching_case = self._match_case.get_active()
 
497
        string, = model.get(iterator, column)
 
498
        key = self._entry.get_text()
 
499
        if self._regexp.get_active():
 
500
            if matching_case:
 
501
                match = re.compile(key).search(string, 1)
 
502
            else:
 
503
                match = re.compile(key, re.I).search(string, 1)
 
504
        else:
 
505
            if not matching_case:
 
506
                string = string.lower()
 
507
                key = key.lower()
 
508
            match = string.find(key) != -1
 
509
 
 
510
        return match
 
511
 
 
512
    def _iterate_rows_forward(self, model, start):
 
513
        model_size = len(model)
 
514
        current = start + 1
 
515
        while model_size != 0:
 
516
            if current >= model_size: current =  0
 
517
            yield model.get_iter_from_string('%d' % current)
 
518
            if current == start: raise StopIteration
 
519
            current += 1
 
520
 
 
521
    def _iterate_rows_backward(self, model, start):
 
522
        model_size = len(model)
 
523
        current = start - 1
 
524
        while model_size != 0:
 
525
            if current < 0: current = model_size - 1
 
526
            yield model.get_iter_from_string('%d' % current)
 
527
            if current == start: raise StopIteration
 
528
            current -= 1
 
529
 
 
530
    def _do_search(self, direction):
 
531
        if direction == 'forward':
 
532
            iterate = self._iterate_rows_forward
 
533
        else:
 
534
            iterate = self._iterate_rows_backward
 
535
 
 
536
        model, sel = self._view.get_selection().get_selected()
 
537
        if sel is None:
 
538
            start = 0
 
539
        else:
 
540
            path = model.get_string_from_iter(sel)
 
541
            start = int(path)
 
542
 
 
543
        for row in iterate(model, start):
 
544
            if self._match(model, row, self._column):
 
545
                path = model.get_path(row)
 
546
                self._view.set_cursor(path)
 
547
                self._view.scroll_to_cell(path, use_align=True)
 
548
                break