1
# Copyright (C) 2005 Dan Loda <danloda@gmail.com>
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.
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.
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
19
from gi.repository import GObject
20
from gi.repository import Gdk
21
from gi.repository import Gtk
22
from gi.repository import Pango
25
from bzrlib import patiencediff
26
from bzrlib.errors import NoSuchRevision
27
from bzrlib.revision import NULL_REVISION, CURRENT_REVISION
29
from bzrlib.plugins.gtk.annotate.colormap import AnnotateColorSaturation
30
from bzrlib.plugins.gtk.i18n import _i18n
31
from bzrlib.plugins.gtk.revisionview import RevisionView
32
from bzrlib.plugins.gtk.window import Window
45
class GAnnotateWindow(Window):
46
"""Annotate window."""
48
def __init__(self, all=False, plain=False, parent=None, branch=None):
53
super(GAnnotateWindow, self).__init__(parent=parent)
56
self.render_icon_pixbuf(Gtk.STOCK_FIND, Gtk.IconSize.BUTTON))
57
self.annotate_colormap = AnnotateColorSaturation()
64
def annotate(self, tree, branch, file_id):
68
self.file_id = file_id
69
self.revisionview.set_file_id(file_id)
70
self.revision_id = getattr(tree, 'get_revision_id',
71
lambda: CURRENT_REVISION)()
73
# [revision id, line number, author, revno, highlight color, line]
74
self.annomodel = Gtk.ListStore(GObject.TYPE_STRING,
84
branch.repository.lock_read()
86
revno_map = self.branch.get_revision_id_to_revno_map()
87
for revision_id, revno in revno_map.iteritems():
88
self.dotted[revision_id] = '.'.join(str(num) for num in revno)
89
for line_no, (revision, revno, line)\
90
in enumerate(self._annotate(tree, file_id)):
91
if revision.revision_id == last_seen and not self.all:
94
last_seen = revision.revision_id
95
author = ", ".join(revision.get_apparent_authors())
97
if revision.revision_id not in self.revisions:
98
self.revisions[revision.revision_id] = revision
100
self.annomodel.append([revision.revision_id,
107
self.annotations.append(revision)
111
self.annomodel.foreach(self._highlight_annotation, now)
113
branch.repository.unlock()
116
self.annoview.set_model(self.annomodel)
117
self.annoview.grab_focus()
118
my_revno = self.dotted.get(self.revision_id, 'current')
119
title = '%s (%s) - gannotate' % (self.tree.id2path(file_id), my_revno)
120
self.set_title(title)
122
def jump_to_line(self, lineno):
123
if lineno > len(self.annomodel) or lineno < 1:
125
# FIXME:should really deal with this in the gui. Perhaps a status
127
print("gannotate: Line number %d does't exist. Defaulting to "
133
tree_path = Gtk.TreePath(path=row)
134
self.annoview.set_cursor(tree_path, None, False)
135
self.annoview.scroll_to_cell(tree_path, use_align=True)
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._get_nick(local=True)
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()
149
revision_id = self.revision_id
150
revision_cache = RevisionCache(repository, self.revisions)
151
for origin, text in tree.annotate_iter(file_id):
153
if rev_id == CURRENT_REVISION:
154
revision = current_revision
155
revno = current_revno
158
revision = revision_cache.get_revision(rev_id)
159
revno = self.dotted.get(rev_id, 'merge')
162
except NoSuchRevision:
163
revision = FakeRevision(rev_id)
166
yield revision, revno, text
168
def _highlight_annotation(self, model, path, iter, now):
169
revision_id, = model.get(iter, REVISION_ID_COL)
170
revision = self.revisions[revision_id]
171
# XXX sinzui 2011-08-12: What does get_color return?
172
color = self.annotate_colormap.get_color(revision, now)
173
model.set_value(iter, HIGHLIGHT_COLOR_COL, color)
175
def _selected_revision(self):
176
(path, col) = self.annoview.get_cursor()
179
return self.annomodel[path][REVISION_ID_COL]
181
def _activate_selected_revision(self, w):
182
rev_id = self._selected_revision()
183
if not rev_id or rev_id == NULL_REVISION:
185
selected = self.revisions[rev_id]
186
self.revisionview.set_revision(selected)
187
if (len(selected.parent_ids) != 0 and selected.parent_ids[0] not in
192
self.back_button.set_sensitive(enable_back)
195
self.revisionview = self._create_log_view()
196
self.annoview = self._create_annotate_view()
198
vbox = Gtk.VBox(homogeneous=False, spacing=0)
201
sw = Gtk.ScrolledWindow()
202
sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
203
sw.set_shadow_type(Gtk.ShadowType.IN)
204
sw.add(self.annoview)
205
self.annoview.gwindow = self
209
swbox.pack_start(sw, True, True, 0)
212
hbox = Gtk.HBox(homogeneous=False, spacing=6)
213
self.back_button = self._create_back_button()
214
hbox.pack_start(self.back_button, False, True, 0)
215
self.forward_button = self._create_forward_button()
216
hbox.pack_start(self.forward_button, False, True, 0)
217
self.find_button = self._create_find_button()
218
hbox.pack_start(self.find_button, False, True, 0)
219
self.goto_button = self._create_goto_button()
220
hbox.pack_start(self.goto_button, False, True, 0)
222
vbox.pack_start(hbox, False, True, 0)
224
self.pane = pane = Gtk.Paned.new(Gtk.Orientation.VERTICAL)
226
pane.add2(self.revisionview)
228
vbox.pack_start(pane, True, True, 0)
230
self._search = SearchBox()
231
swbox.pack_start(self._search, False, True, 0)
232
accels = Gtk.AccelGroup()
233
accels.connect(Gdk.KEY_f, Gdk.ModifierType.CONTROL_MASK,
234
Gtk.AccelFlags.LOCKED,
235
self._search_by_text)
236
accels.connect(Gdk.KEY_g, Gdk.ModifierType.CONTROL_MASK,
237
Gtk.AccelFlags.LOCKED,
238
self._search_by_line)
239
self.add_accel_group(accels)
243
def _search_by_text(self, *ignored): # (accel_group, window, key, modifiers):
244
self._search.show_for('text')
245
self._search.set_target(self.annoview, TEXT_LINE_COL)
247
def _search_by_line(self, *ignored): # accel_group, window, key, modifiers):
248
self._search.show_for('line')
249
self._search.set_target(self.annoview, LINE_NUM_COL)
251
def line_diff(self, tv, path, tvc):
252
row = path.get_indices()[0]
253
revision = self.annotations[row]
254
repository = self.branch.repository
255
if revision.revision_id == CURRENT_REVISION:
257
tree2 = self.tree.basis_tree()
259
tree1 = repository.revision_tree(revision.revision_id)
260
if len(revision.parent_ids) > 0:
261
tree2 = repository.revision_tree(revision.parent_ids[0])
263
tree2 = repository.revision_tree(NULL_REVISION)
264
from bzrlib.plugins.gtk.diff import DiffWindow
265
window = DiffWindow(self)
266
window.set_diff("Diff for line %d" % (row+1), tree1, tree2)
267
window.set_file(tree1.id2path(self.file_id))
271
def _create_annotate_view(self):
273
tv.set_rules_hint(False)
274
tv.connect("cursor-changed", self._activate_selected_revision)
276
tv.connect("row-activated", self.line_diff)
278
cell = Gtk.CellRendererText()
279
cell.set_property("xalign", 1.0)
280
cell.set_property("ypad", 0)
281
cell.set_property("family", "Monospace")
283
"cell-background-rgba",
284
tv.get_style_context().get_background_color(Gtk.StateType.NORMAL))
285
col = Gtk.TreeViewColumn()
286
col.set_resizable(False)
287
col.pack_start(cell, True)
288
col.add_attribute(cell, "text", LINE_NUM_COL)
289
tv.append_column(col)
291
style_context = self.get_style_context()
293
cell = Gtk.CellRendererText()
294
cell.set_property("ypad", 0)
295
cell.set_property("ellipsize", Pango.EllipsizeMode.END)
297
"cell-background-rgba",
298
style_context.get_background_color(Gtk.StateType.NORMAL))
299
col = Gtk.TreeViewColumn("Committer")
300
col.set_resizable(True)
301
col.pack_start(cell, True)
302
col.add_attribute(cell, "text", COMMITTER_COL)
303
tv.append_column(col)
305
cell = Gtk.CellRendererText()
306
cell.set_property("xalign", 1.0)
307
cell.set_property("ypad", 0)
309
"cell-background-rgba",
310
style_context.get_background_color(Gtk.StateType.NORMAL))
311
col = Gtk.TreeViewColumn("Revno")
312
col.set_resizable(False)
313
col.pack_start(cell, True)
314
col.add_attribute(cell, "markup", REVNO_COL)
315
tv.append_column(col)
317
cell = Gtk.CellRendererText()
318
cell.set_property("ypad", 0)
319
cell.set_property("family", "Monospace")
320
col = Gtk.TreeViewColumn()
321
col.set_resizable(False)
322
col.pack_start(cell, True)
323
# col.add_attribute(cell, "foreground", HIGHLIGHT_COLOR_COL)
324
col.add_attribute(cell, "background", HIGHLIGHT_COLOR_COL)
325
col.add_attribute(cell, "text", TEXT_LINE_COL)
326
tv.append_column(col)
328
# interactive substring search
329
def search_equal_func(model, column, key, iter):
330
return model.get_value(iter, TEXT_LINE_COL).lower().find(key.lower()) == -1
332
tv.set_enable_search(True)
333
tv.set_search_equal_func(search_equal_func, None)
337
def _create_log_view(self):
338
lv = RevisionView(self._branch)
342
def _create_back_button(self):
343
button = Gtk.Button()
344
button.set_use_stock(True)
345
button.set_label("gtk-go-back")
346
button.connect("clicked", lambda w: self.go_back())
347
button.set_relief(Gtk.ReliefStyle.NONE)
351
def _create_forward_button(self):
352
button = Gtk.Button()
353
button.set_use_stock(True)
354
button.set_label("gtk-go-forward")
355
button.connect("clicked", lambda w: self.go_forward())
356
button.set_relief(Gtk.ReliefStyle.NONE)
358
button.set_sensitive(False)
361
def _create_find_button(self):
362
button = Gtk.Button()
363
button.set_use_stock(True)
364
button.set_label("gtk-find")
365
button.set_tooltip_text("Search for text (Ctrl+F)")
366
button.connect("clicked", self._search_by_text)
367
button.set_relief(Gtk.ReliefStyle.NONE)
369
button.set_sensitive(True)
372
def _create_goto_button(self):
373
button = Gtk.Button()
374
button.set_label("Goto Line")
375
button.set_tooltip_text("Scroll to a line by entering its number (Ctrl+G)")
376
button.connect("clicked", self._search_by_line)
377
button.set_relief(Gtk.ReliefStyle.NONE)
379
button.set_sensitive(True)
383
last_tree = self.tree
384
rev_id = self._selected_revision()
385
parent_id = self.revisions[rev_id].parent_ids[0]
386
target_tree = self.branch.repository.revision_tree(parent_id)
387
if self._go(target_tree):
388
self.history.append(last_tree)
389
self.forward_button.set_sensitive(True)
391
self._no_back.add(parent_id)
392
self.back_button.set_sensitive(False)
394
def go_forward(self):
395
if len(self.history) == 0:
397
target_tree = self.history.pop()
398
if len(self.history) == 0:
399
self.forward_button.set_sensitive(False)
400
self._go(target_tree)
402
def _go(self, target_tree):
403
rev_id = self._selected_revision()
404
if self.file_id in target_tree:
405
offset = self.get_scroll_offset(target_tree)
406
path, col = self.annoview.get_cursor()
407
(row,) = path.get_indices()
408
self.annotate(target_tree, self.branch, self.file_id)
412
new_path = Gtk.TreePath(path=new_row)
413
self.annoview.set_cursor(new_path, None, False)
418
def get_scroll_offset(self, tree):
419
old = self.tree.get_file(self.file_id)
420
new = tree.get_file(self.file_id)
421
path, col = self.annoview.get_cursor()
422
(row,) = path.get_indices()
423
matcher = patiencediff.PatienceSequenceMatcher(None, old.readlines(),
425
for i, j, n in matcher.get_matching_blocks():
430
class FakeRevision(object):
433
For when a revision is referenced but not present.
436
def __init__(self, revision_id, committer='?', nick=None):
437
self.revision_id = revision_id
439
self.committer = committer
445
def get_apparent_authors(self):
446
return [self.committer]
449
class RevisionCache(object):
450
"""A caching revision source"""
452
def __init__(self, real_source, seed_cache=None):
453
self.__real_source = real_source
454
if seed_cache is None:
457
self.__cache = dict(seed_cache)
459
def get_revision(self, revision_id):
460
if revision_id not in self.__cache:
461
revision = self.__real_source.get_revision(revision_id)
462
self.__cache[revision_id] = revision
463
return self.__cache[revision_id]
466
class SearchBox(Gtk.HBox):
467
"""A button box for searching in text or lines of annotations"""
469
super(SearchBox, self).__init__(homogeneous=False, spacing=6)
472
button = Gtk.Button()
474
image.set_from_stock('gtk-stop', Gtk.IconSize.BUTTON)
475
button.set_image(image)
476
button.set_relief(Gtk.ReliefStyle.NONE)
477
button.connect("clicked", lambda w: self.hide())
478
self.pack_start(button, False, False, 0)
483
self.pack_start(label, False, False, 0)
487
entry.connect("activate", lambda w, d: self._do_search(d),
489
self.pack_start(entry, False, False, 0)
491
# Next/previous buttons
492
button = Gtk.Button(_i18n('_Next'), use_underline=True)
494
image.set_from_stock('gtk-go-forward', Gtk.IconSize.BUTTON)
495
button.set_image(image)
496
button.connect("clicked", lambda w, d: self._do_search(d),
498
self.pack_start(button, False, False, 0)
500
button = Gtk.Button(_i18n('_Previous'), use_underline=True)
502
image.set_from_stock('gtk-go-back', Gtk.IconSize.BUTTON)
503
button.set_image(image)
504
button.connect("clicked", lambda w, d: self._do_search(d),
506
self.pack_start(button, False, False, 0)
509
check = Gtk.CheckButton('Match case')
510
self._match_case = check
511
self.pack_start(check, False, False, 0)
513
check = Gtk.CheckButton('Regexp')
514
check.connect("toggled", lambda w: self._set_label())
516
self.pack_start(check, False, False, 0)
520
# Note that we stay hidden (we do not call self.show_all())
523
def show_for(self, kind):
527
# Hide unrelated buttons
529
self._match_case.hide()
532
self._entry.grab_focus()
534
def _set_label(self):
535
if self._kind == 'line':
536
self._label.set_text('Find Line: ')
538
if self._regexp.get_active():
539
self._label.set_text('Find Regexp: ')
541
self._label.set_text('Find Text: ')
543
def set_target(self, view,column):
545
self._column = column
547
def _match(self, model, iterator, column):
548
matching_case = self._match_case.get_active()
549
cell_value, = model.get(iterator, column)
550
key = self._entry.get_text()
551
if column == LINE_NUM_COL:
552
# FIXME: For goto-line there are faster algorithms than searching
553
# every line til we find the right one! -- mbp 2011-01-27
554
return key.strip() == str(cell_value)
555
elif self._regexp.get_active():
557
match = re.compile(key).search(cell_value, 1)
559
match = re.compile(key, re.I).search(cell_value, 1)
561
if not matching_case:
562
cell_value = cell_value.lower()
564
match = cell_value.find(key) != -1
568
def _iterate_rows_forward(self, model, start):
569
model_size = len(model)
571
while model_size != 0:
572
if current >= model_size: current = 0
573
yield model.get_iter_from_string('%d' % current)
574
if current == start: raise StopIteration
577
def _iterate_rows_backward(self, model, start):
578
model_size = len(model)
580
while model_size != 0:
581
if current < 0: current = model_size - 1
582
yield model.get_iter_from_string('%d' % current)
583
if current == start: raise StopIteration
586
def _do_search(self, direction):
587
if direction == 'forward':
588
iterate = self._iterate_rows_forward
590
iterate = self._iterate_rows_backward
592
model, sel = self._view.get_selection().get_selected()
596
path = model.get_string_from_iter(sel)
599
for row in iterate(model, start):
600
if self._match(model, row, self._column):
601
path = model.get_path(row)
602
self._view.set_cursor(path, None, False)
603
self._view.scroll_to_cell(path, use_align=True)