/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: 2012-07-09 15:23:26 UTC
  • mto: This revision was merged to the branch mainline in revision 794.
  • Revision ID: jelmer@samba.org-20120709152326-dzxb8zoz0btull7n
Remove bzr-notify.

Show diffs side-by-side

added added

removed removed

Lines of Context:
16
16
 
17
17
import time
18
18
 
19
 
import pygtk
20
 
pygtk.require("2.0")
21
 
import gobject
22
 
import gtk
23
 
import pango
 
19
from gi.repository import GObject
 
20
from gi.repository import Gdk
 
21
from gi.repository import Gtk
 
22
from gi.repository import Pango
 
23
import re
24
24
 
25
 
from bzrlib import tsort
 
25
from bzrlib import patiencediff
26
26
from bzrlib.errors import NoSuchRevision
27
 
from bzrlib.revision import NULL_REVISION
 
27
from bzrlib.revision import NULL_REVISION, CURRENT_REVISION
28
28
 
29
 
from colormap import AnnotateColorMap, AnnotateColorSaturation
30
 
from logview import LogView
31
 
from spanselector import SpanSelector
 
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
32
33
 
33
34
 
34
35
(
41
42
) = range(6)
42
43
 
43
44
 
44
 
class GAnnotateWindow(gtk.Window):
 
45
class GAnnotateWindow(Window):
45
46
    """Annotate window."""
46
47
 
47
 
    def __init__(self, all=False, plain=False):
 
48
    def __init__(self, all=False, plain=False, parent=None, branch=None):
48
49
        self.all = all
49
50
        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))
 
51
        self._branch = branch
 
52
 
 
53
        super(GAnnotateWindow, self).__init__(parent=parent)
 
54
 
 
55
        self.set_icon(
 
56
            self.render_icon_pixbuf(Gtk.STOCK_FIND, Gtk.IconSize.BUTTON))
54
57
        self.annotate_colormap = AnnotateColorSaturation()
55
58
 
56
59
        self._create()
57
 
 
58
 
        if self.plain:
59
 
            self.span_selector.hide()
60
 
 
61
 
    def annotate(self, branch, file_id, revision_id=None):
62
60
        self.revisions = {}
 
61
        self.history = []
 
62
        self._no_back = set()
 
63
 
 
64
    def annotate(self, tree, branch, file_id):
63
65
        self.annotations = []
64
66
        self.branch = branch
 
67
        self.tree = tree
65
68
        self.file_id = file_id
66
 
        self.revision_id = revision_id
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
 
        
 
69
        self.revisionview.set_file_id(file_id)
 
70
        self.revision_id = getattr(tree, 'get_revision_id', 
 
71
                                   lambda: CURRENT_REVISION)()
 
72
 
 
73
        # [revision id, line number, author, revno, highlight color, line]
 
74
        self.annomodel = Gtk.ListStore(GObject.TYPE_STRING,
 
75
                                       GObject.TYPE_INT,
 
76
                                       GObject.TYPE_STRING,
 
77
                                       GObject.TYPE_STRING,
 
78
                                       GObject.TYPE_STRING,
 
79
                                       GObject.TYPE_STRING)
 
80
 
76
81
        last_seen = None
77
82
        try:
78
83
            branch.lock_read()
79
84
            branch.repository.lock_read()
 
85
            self.dotted = {}
 
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)
80
89
            for line_no, (revision, revno, line)\
81
 
                    in enumerate(self._annotate(branch, file_id, revision_id)):
 
90
                in enumerate(self._annotate(tree, file_id)):
82
91
                if revision.revision_id == last_seen and not self.all:
83
 
                    revno = committer = ""
 
92
                    revno = author = ""
84
93
                else:
85
94
                    last_seen = revision.revision_id
86
 
                    committer = revision.committer
 
95
                    author = ", ".join(revision.get_apparent_authors())
87
96
 
88
97
                if revision.revision_id not in self.revisions:
89
98
                    self.revisions[revision.revision_id] = revision
90
99
 
91
100
                self.annomodel.append([revision.revision_id,
92
101
                                       line_no + 1,
93
 
                                       committer,
 
102
                                       author,
94
103
                                       revno,
95
104
                                       None,
96
105
                                       line.rstrip("\r\n")
97
 
                                      ])
 
106
                                       ])
98
107
                self.annotations.append(revision)
99
108
 
100
109
            if not self.plain:
101
 
                self._set_oldest_newest()
102
 
                # Recall that calling activate_default will emit "span-changed",
103
 
                # so self._span_changed_cb will take care of initial highlighting
104
 
                self.span_selector.activate_default()
 
110
                now = time.time()
 
111
                self.annomodel.foreach(self._highlight_annotation, now)
105
112
        finally:
106
113
            branch.repository.unlock()
107
114
            branch.unlock()
108
115
 
109
116
        self.annoview.set_model(self.annomodel)
110
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)
111
121
 
112
122
    def jump_to_line(self, lineno):
113
123
        if lineno > len(self.annomodel) or lineno < 1:
116
126
            # bar?
117
127
            print("gannotate: Line number %d does't exist. Defaulting to "
118
128
                  "line 1." % lineno)
119
 
            return
 
129
            return
120
130
        else:
121
131
            row = lineno - 1
122
132
 
123
 
        self.annoview.set_cursor(row)
124
 
        self.annoview.scroll_to_cell(row, use_align=True)
125
 
 
126
 
    def _dotted_revnos(self, repository, revision_id):
127
 
        """Return a dict of revision_id -> dotted revno
128
 
        
129
 
        :param repository: The repository to get the graph from
130
 
        :param revision_id: The last revision for which this info is needed
131
 
        """
132
 
        graph = repository.get_revision_graph(revision_id)
133
 
        dotted = {}
134
 
        for n, revision_id, d, revno, e in tsort.merge_sort(graph, 
135
 
            revision_id, generate_revno=True):
136
 
            dotted[revision_id] = '.'.join(str(num) for num in revno)
137
 
        return dotted
138
 
 
139
 
    def _annotate(self, branch, file_id, revision_id):
140
 
        repository = branch.repository
141
 
        if revision_id is None:
142
 
            revision_id = branch.last_revision()
143
 
        dotted = self._dotted_revnos(repository, revision_id)
144
 
        rev_tree = repository.revision_tree(revision_id)
145
 
        revision_id = rev_tree.inventory[file_id].revision
146
 
        weave = repository.weave_store.get_weave(file_id,
147
 
                                                 branch.get_transaction())
148
 
        
149
 
        revision_cache = RevisionCache(repository)
150
 
        for origin, text in weave.annotate_iter(revision_id):
 
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)
 
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._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()
 
148
        else:
 
149
            revision_id = self.revision_id
 
150
        revision_cache = RevisionCache(repository, self.revisions)
 
151
        for origin, text in tree.annotate_iter(file_id):
151
152
            rev_id = origin
152
 
            try:
153
 
                revision = revision_cache.get_revision(rev_id)
154
 
                revno = dotted.get(rev_id, 'merge')
155
 
                if len(revno) > 15:
156
 
                    revno = 'merge'
157
 
            except NoSuchRevision:
158
 
                revision = NoneRevision(rev_id)
159
 
                revno = "?"
 
153
            if rev_id == CURRENT_REVISION:
 
154
                revision = current_revision
 
155
                revno = current_revno
 
156
            else:
 
157
                try:
 
158
                    revision = revision_cache.get_revision(rev_id)
 
159
                    revno = self.dotted.get(rev_id, 'merge')
 
160
                    if len(revno) > 15:
 
161
                        revno = 'merge'
 
162
                except NoSuchRevision:
 
163
                    revision = FakeRevision(rev_id)
 
164
                    revno = "?"
160
165
 
161
166
            yield revision, revno, text
162
167
 
163
 
    def _set_oldest_newest(self):
164
 
        rev_dates = map(lambda i: self.revisions[i].timestamp, self.revisions)
165
 
        if len(rev_dates) == 0:
166
 
            return
167
 
        oldest = min(rev_dates)
168
 
        newest = max(rev_dates)
169
 
 
170
 
        span = self._span_from_seconds(time.time() - oldest)
171
 
        self.span_selector.set_to_oldest_span(span)
172
 
        
173
 
        span = self._span_from_seconds(newest - oldest)
174
 
        self.span_selector.set_newest_to_oldest_span(span)
175
 
 
176
 
    def _span_from_seconds(self, seconds):
177
 
        return (seconds / (24 * 60 * 60))
178
 
    
179
 
    def _span_changed_cb(self, w, span):
180
 
        self.annotate_colormap.set_span(span)
181
 
        now = time.time()
182
 
        self.annomodel.foreach(self._highlight_annotation, now)
183
 
 
184
168
    def _highlight_annotation(self, model, path, iter, now):
185
169
        revision_id, = model.get(iter, REVISION_ID_COL)
186
170
        revision = self.revisions[revision_id]
187
 
        model.set(iter, HIGHLIGHT_COLOR_COL,
188
 
                  self.annotate_colormap.get_color(revision, now))
 
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)
189
174
 
190
 
    def _show_log(self, w):
 
175
    def _selected_revision(self):
191
176
        (path, col) = self.annoview.get_cursor()
192
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 not rev_id or rev_id == NULL_REVISION:
193
184
            return
194
 
        rev_id = self.annomodel[path][REVISION_ID_COL]
195
 
        self.logview.set_revision(self.revisions[rev_id])
 
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
 
188
            self._no_back):
 
189
            enable_back = True
 
190
        else:
 
191
            enable_back = False
 
192
        self.back_button.set_sensitive(enable_back)
196
193
 
197
194
    def _create(self):
198
 
        self.logview = self._create_log_view()
 
195
        self.revisionview = self._create_log_view()
199
196
        self.annoview = self._create_annotate_view()
200
 
        self.span_selector = self._create_span_selector()
201
197
 
202
 
        vbox = gtk.VBox(False, 12)
203
 
        vbox.set_border_width(12)
 
198
        vbox = Gtk.VBox(homogeneous=False, spacing=0)
204
199
        vbox.show()
205
200
 
206
 
        sw = gtk.ScrolledWindow()
207
 
        sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
208
 
        sw.set_shadow_type(gtk.SHADOW_IN)
 
201
        sw = Gtk.ScrolledWindow()
 
202
        sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
 
203
        sw.set_shadow_type(Gtk.ShadowType.IN)
209
204
        sw.add(self.annoview)
210
205
        self.annoview.gwindow = self
211
206
        sw.show()
212
 
        
213
 
        self.pane = pane = gtk.VPaned()
214
 
        pane.add1(sw)
215
 
        pane.add2(self.logview)
 
207
 
 
208
        swbox = Gtk.VBox()
 
209
        swbox.pack_start(sw, True, True, 0)
 
210
        swbox.show()
 
211
 
 
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)
 
221
        hbox.show()
 
222
        vbox.pack_start(hbox, False, True, 0)
 
223
 
 
224
        self.pane = pane = Gtk.Paned.new(Gtk.Orientation.VERTICAL)
 
225
        pane.add1(swbox)
 
226
        pane.add2(self.revisionview)
216
227
        pane.show()
217
 
        vbox.pack_start(pane, expand=True, fill=True)
218
 
        
219
 
        hbox = gtk.HBox(True, 6)
220
 
        hbox.pack_start(self.span_selector, expand=False, fill=True)
221
 
        hbox.pack_start(self._create_button_box(), expand=False, fill=True)
222
 
        hbox.show()
223
 
        vbox.pack_start(hbox, expand=False, fill=True)
 
228
        vbox.pack_start(pane, True, True, 0)
 
229
 
 
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)
224
240
 
225
241
        self.add(vbox)
226
242
 
227
 
    def row_diff(self, tv, path, tvc):
228
 
        row = path[0]
 
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)
 
246
 
 
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)
 
250
 
 
251
    def line_diff(self, tv, path, tvc):
 
252
        row = path.get_indices()[0]
229
253
        revision = self.annotations[row]
230
254
        repository = self.branch.repository
231
 
        tree1 = repository.revision_tree(revision.revision_id)
232
 
        if len(revision.parent_ids) > 0:
233
 
            tree2 = repository.revision_tree(revision.parent_ids[0])
 
255
        if revision.revision_id == CURRENT_REVISION:
 
256
            tree1 = self.tree
 
257
            tree2 = self.tree.basis_tree()
234
258
        else:
235
 
            tree2 = repository.revision_tree(NULL_REVISION)
236
 
        from bzrlib.plugins.gtk.viz.diffwin import DiffWindow
237
 
        window = DiffWindow()
238
 
        window.set_diff("Diff for row %d" % (row+1), tree1, tree2)
 
259
            tree1 = repository.revision_tree(revision.revision_id)
 
260
            if len(revision.parent_ids) > 0:
 
261
                tree2 = repository.revision_tree(revision.parent_ids[0])
 
262
            else:
 
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)
239
267
        window.set_file(tree1.id2path(self.file_id))
240
268
        window.show()
241
269
 
242
270
 
243
271
    def _create_annotate_view(self):
244
 
        tv = gtk.TreeView()
 
272
        tv = Gtk.TreeView()
245
273
        tv.set_rules_hint(False)
246
 
        tv.connect("cursor-changed", self._show_log)
 
274
        tv.connect("cursor-changed", self._activate_selected_revision)
247
275
        tv.show()
248
 
        tv.connect("row-activated", self.row_diff)
 
276
        tv.connect("row-activated", self.line_diff)
249
277
 
250
 
        cell = gtk.CellRendererText()
 
278
        cell = Gtk.CellRendererText()
251
279
        cell.set_property("xalign", 1.0)
252
280
        cell.set_property("ypad", 0)
253
281
        cell.set_property("family", "Monospace")
254
282
        cell.set_property("cell-background-gdk",
255
 
                          tv.get_style().bg[gtk.STATE_NORMAL])
256
 
        col = gtk.TreeViewColumn()
 
283
                          tv.get_style().bg[Gtk.StateType.NORMAL])
 
284
        col = Gtk.TreeViewColumn()
257
285
        col.set_resizable(False)
258
 
        col.pack_start(cell, expand=True)
 
286
        col.pack_start(cell, True)
259
287
        col.add_attribute(cell, "text", LINE_NUM_COL)
260
288
        tv.append_column(col)
261
289
 
262
 
        cell = gtk.CellRendererText()
 
290
        cell = Gtk.CellRendererText()
263
291
        cell.set_property("ypad", 0)
264
 
        cell.set_property("ellipsize", pango.ELLIPSIZE_END)
 
292
        cell.set_property("ellipsize", Pango.EllipsizeMode.END)
265
293
        cell.set_property("cell-background-gdk",
266
 
                          self.get_style().bg[gtk.STATE_NORMAL])
267
 
        col = gtk.TreeViewColumn("Committer")
 
294
                          self.get_style().bg[Gtk.StateType.NORMAL])
 
295
        col = Gtk.TreeViewColumn("Committer")
268
296
        col.set_resizable(True)
269
 
        col.pack_start(cell, expand=True)
 
297
        col.pack_start(cell, True)
270
298
        col.add_attribute(cell, "text", COMMITTER_COL)
271
299
        tv.append_column(col)
272
300
 
273
 
        cell = gtk.CellRendererText()
 
301
        cell = Gtk.CellRendererText()
274
302
        cell.set_property("xalign", 1.0)
275
303
        cell.set_property("ypad", 0)
276
304
        cell.set_property("cell-background-gdk",
277
 
                          self.get_style().bg[gtk.STATE_NORMAL])
278
 
        col = gtk.TreeViewColumn("Revno")
 
305
                          self.get_style().bg[Gtk.StateType.NORMAL])
 
306
        col = Gtk.TreeViewColumn("Revno")
279
307
        col.set_resizable(False)
280
 
        col.pack_start(cell, expand=True)
 
308
        col.pack_start(cell, True)
281
309
        col.add_attribute(cell, "markup", REVNO_COL)
282
310
        tv.append_column(col)
283
311
 
284
 
        cell = gtk.CellRendererText()
 
312
        cell = Gtk.CellRendererText()
285
313
        cell.set_property("ypad", 0)
286
314
        cell.set_property("family", "Monospace")
287
 
        col = gtk.TreeViewColumn()
 
315
        col = Gtk.TreeViewColumn()
288
316
        col.set_resizable(False)
289
 
        col.pack_start(cell, expand=True)
 
317
        col.pack_start(cell, True)
290
318
#        col.add_attribute(cell, "foreground", HIGHLIGHT_COLOR_COL)
291
319
        col.add_attribute(cell, "background", HIGHLIGHT_COLOR_COL)
292
320
        col.add_attribute(cell, "text", TEXT_LINE_COL)
293
321
        tv.append_column(col)
294
322
 
295
 
        tv.set_search_column(LINE_NUM_COL)
296
 
        
 
323
        # interactive substring search
 
324
        def search_equal_func(model, column, key, iter):
 
325
            return model.get_value(iter, TEXT_LINE_COL).lower().find(key.lower()) == -1
 
326
 
 
327
        tv.set_enable_search(True)
 
328
        tv.set_search_equal_func(search_equal_func, None)
 
329
 
297
330
        return tv
298
331
 
299
 
    def _create_span_selector(self):
300
 
        ss = SpanSelector()
301
 
        ss.connect("span-changed", self._span_changed_cb)
302
 
        ss.show()
303
 
 
304
 
        return ss
305
 
 
306
332
    def _create_log_view(self):
307
 
        lv = LogView()
 
333
        lv = RevisionView(self._branch)
308
334
        lv.show()
309
 
 
310
335
        return lv
311
336
 
312
 
    def _create_button_box(self):
313
 
        box = gtk.HButtonBox()
314
 
        box.set_layout(gtk.BUTTONBOX_END)
315
 
        box.show()
316
 
        
317
 
        button = gtk.Button()
318
 
        button.set_use_stock(True)
319
 
        button.set_label("gtk-close")
320
 
        button.connect("clicked", lambda w: self.destroy())
321
 
        button.show()
322
 
        
323
 
        box.pack_start(button, expand=False, fill=False)
324
 
 
325
 
        return box
326
 
 
327
 
 
328
 
class NoneRevision:
 
337
    def _create_back_button(self):
 
338
        button = Gtk.Button()
 
339
        button.set_use_stock(True)
 
340
        button.set_label("gtk-go-back")
 
341
        button.connect("clicked", lambda w: self.go_back())
 
342
        button.set_relief(Gtk.ReliefStyle.NONE)
 
343
        button.show()
 
344
        return button
 
345
 
 
346
    def _create_forward_button(self):
 
347
        button = Gtk.Button()
 
348
        button.set_use_stock(True)
 
349
        button.set_label("gtk-go-forward")
 
350
        button.connect("clicked", lambda w: self.go_forward())
 
351
        button.set_relief(Gtk.ReliefStyle.NONE)
 
352
        button.show()
 
353
        button.set_sensitive(False)
 
354
        return button
 
355
 
 
356
    def _create_find_button(self):
 
357
        button = Gtk.Button()
 
358
        button.set_use_stock(True)
 
359
        button.set_label("gtk-find")
 
360
        button.set_tooltip_text("Search for text (Ctrl+F)")
 
361
        button.connect("clicked", self._search_by_text)
 
362
        button.set_relief(Gtk.ReliefStyle.NONE)
 
363
        button.show()
 
364
        button.set_sensitive(True)
 
365
        return button
 
366
 
 
367
    def _create_goto_button(self):
 
368
        button = Gtk.Button()
 
369
        button.set_label("Goto Line")
 
370
        button.set_tooltip_text("Scroll to a line by entering its number (Ctrl+G)")
 
371
        button.connect("clicked", self._search_by_line)
 
372
        button.set_relief(Gtk.ReliefStyle.NONE)
 
373
        button.show()
 
374
        button.set_sensitive(True)
 
375
        return button
 
376
 
 
377
    def go_back(self):
 
378
        last_tree = self.tree
 
379
        rev_id = self._selected_revision()
 
380
        parent_id = self.revisions[rev_id].parent_ids[0]
 
381
        target_tree = self.branch.repository.revision_tree(parent_id)
 
382
        if self._go(target_tree):
 
383
            self.history.append(last_tree)
 
384
            self.forward_button.set_sensitive(True)
 
385
        else:
 
386
            self._no_back.add(parent_id)
 
387
            self.back_button.set_sensitive(False)
 
388
 
 
389
    def go_forward(self):
 
390
        if len(self.history) == 0:
 
391
            return
 
392
        target_tree = self.history.pop()
 
393
        if len(self.history) == 0:
 
394
            self.forward_button.set_sensitive(False)
 
395
        self._go(target_tree)
 
396
 
 
397
    def _go(self, target_tree):
 
398
        rev_id = self._selected_revision()
 
399
        if self.file_id in target_tree:
 
400
            offset = self.get_scroll_offset(target_tree)
 
401
            path, col = self.annoview.get_cursor()
 
402
            (row,) = path.get_indices()
 
403
            self.annotate(target_tree, self.branch, self.file_id)
 
404
            new_row = row+offset
 
405
            if new_row < 0:
 
406
                new_row = 0
 
407
            new_path = Gtk.TreePath(path=new_row)
 
408
            self.annoview.set_cursor(new_path, None, False)
 
409
            return True
 
410
        else:
 
411
            return False
 
412
 
 
413
    def get_scroll_offset(self, tree):
 
414
        old = self.tree.get_file(self.file_id)
 
415
        new = tree.get_file(self.file_id)
 
416
        path, col = self.annoview.get_cursor()
 
417
        (row,) = path.get_indices()
 
418
        matcher = patiencediff.PatienceSequenceMatcher(None, old.readlines(),
 
419
                                                       new.readlines())
 
420
        for i, j, n in matcher.get_matching_blocks():
 
421
            if i + n >= row:
 
422
                return j - i
 
423
 
 
424
 
 
425
class FakeRevision(object):
329
426
    """ A fake revision.
330
427
 
331
428
    For when a revision is referenced but not present.
332
429
    """
333
430
 
334
 
    def __init__(self, revision_id):
 
431
    def __init__(self, revision_id, committer='?', nick=None):
335
432
        self.revision_id = revision_id
336
433
        self.parent_ids = []
337
 
        self.committer = "?"
 
434
        self.committer = committer
338
435
        self.message = "?"
339
436
        self.timestamp = 0.0
340
437
        self.timezone = 0
 
438
        self.properties = {}
 
439
 
 
440
    def get_apparent_authors(self):
 
441
        return [self.committer]
341
442
 
342
443
 
343
444
class RevisionCache(object):
344
445
    """A caching revision source"""
345
 
    def __init__(self, real_source):
 
446
 
 
447
    def __init__(self, real_source, seed_cache=None):
346
448
        self.__real_source = real_source
347
 
        self.__cache = {}
 
449
        if seed_cache is None:
 
450
            self.__cache = {}
 
451
        else:
 
452
            self.__cache = dict(seed_cache)
348
453
 
349
454
    def get_revision(self, revision_id):
350
455
        if revision_id not in self.__cache:
351
456
            revision = self.__real_source.get_revision(revision_id)
352
457
            self.__cache[revision_id] = revision
353
458
        return self.__cache[revision_id]
 
459
 
 
460
 
 
461
class SearchBox(Gtk.HBox):
 
462
    """A button box for searching in text or lines of annotations"""
 
463
    def __init__(self):
 
464
        super(SearchBox, self).__init__(homogeneous=False, spacing=6)
 
465
 
 
466
        # Close button
 
467
        button = Gtk.Button()
 
468
        image = Gtk.Image()
 
469
        image.set_from_stock('gtk-stop', Gtk.IconSize.BUTTON)
 
470
        button.set_image(image)
 
471
        button.set_relief(Gtk.ReliefStyle.NONE)
 
472
        button.connect("clicked", lambda w: self.hide())
 
473
        self.pack_start(button, False, False, 0)
 
474
 
 
475
        # Search entry
 
476
        label = Gtk.Label()
 
477
        self._label = label
 
478
        self.pack_start(label, False, False, 0)
 
479
 
 
480
        entry = Gtk.Entry()
 
481
        self._entry = entry
 
482
        entry.connect("activate", lambda w, d: self._do_search(d),
 
483
                      'forward')
 
484
        self.pack_start(entry, False, False, 0)
 
485
 
 
486
        # Next/previous buttons
 
487
        button = Gtk.Button(_i18n('_Next'), use_underline=True)
 
488
        image = Gtk.Image()
 
489
        image.set_from_stock('gtk-go-forward', Gtk.IconSize.BUTTON)
 
490
        button.set_image(image)
 
491
        button.connect("clicked", lambda w, d: self._do_search(d),
 
492
                       'forward')
 
493
        self.pack_start(button, False, False, 0)
 
494
 
 
495
        button = Gtk.Button(_i18n('_Previous'), use_underline=True)
 
496
        image = Gtk.Image()
 
497
        image.set_from_stock('gtk-go-back', Gtk.IconSize.BUTTON)
 
498
        button.set_image(image)
 
499
        button.connect("clicked", lambda w, d: self._do_search(d),
 
500
                       'backward')
 
501
        self.pack_start(button, False, False, 0)
 
502
 
 
503
        # Search options
 
504
        check = Gtk.CheckButton('Match case')
 
505
        self._match_case = check
 
506
        self.pack_start(check, False, False, 0)
 
507
 
 
508
        check = Gtk.CheckButton('Regexp')
 
509
        check.connect("toggled", lambda w: self._set_label())
 
510
        self._regexp = check
 
511
        self.pack_start(check, False, False, 0)
 
512
 
 
513
        self._view = None
 
514
        self._column = None
 
515
        # Note that we stay hidden (we do not call self.show_all())
 
516
 
 
517
 
 
518
    def show_for(self, kind):
 
519
        self._kind = kind
 
520
        self.show_all()
 
521
        self._set_label()
 
522
        # Hide unrelated buttons
 
523
        if kind == 'line':
 
524
            self._match_case.hide()
 
525
            self._regexp.hide()
 
526
        # Be ready
 
527
        self._entry.grab_focus()
 
528
 
 
529
    def _set_label(self):
 
530
        if self._kind == 'line':
 
531
            self._label.set_text('Find Line: ')
 
532
        else:
 
533
            if self._regexp.get_active():
 
534
                self._label.set_text('Find Regexp: ')
 
535
            else:
 
536
                self._label.set_text('Find Text: ')
 
537
 
 
538
    def set_target(self, view,column):
 
539
        self._view = view
 
540
        self._column = column
 
541
 
 
542
    def _match(self, model, iterator, column):
 
543
        matching_case = self._match_case.get_active()
 
544
        cell_value, = model.get(iterator, column)
 
545
        key = self._entry.get_text()
 
546
        if column == LINE_NUM_COL:
 
547
            # FIXME: For goto-line there are faster algorithms than searching 
 
548
            # every line til we find the right one! -- mbp 2011-01-27
 
549
            return key.strip() == str(cell_value)
 
550
        elif self._regexp.get_active():
 
551
            if matching_case:
 
552
                match = re.compile(key).search(cell_value, 1)
 
553
            else:
 
554
                match = re.compile(key, re.I).search(cell_value, 1)
 
555
        else:
 
556
            if not matching_case:
 
557
                cell_value = cell_value.lower()
 
558
                key = key.lower()
 
559
            match = cell_value.find(key) != -1
 
560
 
 
561
        return match
 
562
 
 
563
    def _iterate_rows_forward(self, model, start):
 
564
        model_size = len(model)
 
565
        current = start + 1
 
566
        while model_size != 0:
 
567
            if current >= model_size: current =  0
 
568
            yield model.get_iter_from_string('%d' % current)
 
569
            if current == start: raise StopIteration
 
570
            current += 1
 
571
 
 
572
    def _iterate_rows_backward(self, model, start):
 
573
        model_size = len(model)
 
574
        current = start - 1
 
575
        while model_size != 0:
 
576
            if current < 0: current = model_size - 1
 
577
            yield model.get_iter_from_string('%d' % current)
 
578
            if current == start: raise StopIteration
 
579
            current -= 1
 
580
 
 
581
    def _do_search(self, direction):
 
582
        if direction == 'forward':
 
583
            iterate = self._iterate_rows_forward
 
584
        else:
 
585
            iterate = self._iterate_rows_backward
 
586
 
 
587
        model, sel = self._view.get_selection().get_selected()
 
588
        if sel is None:
 
589
            start = 0
 
590
        else:
 
591
            path = model.get_string_from_iter(sel)
 
592
            start = int(path)
 
593
 
 
594
        for row in iterate(model, start):
 
595
            if self._match(model, row, self._column):
 
596
                path = model.get_path(row)
 
597
                self._view.set_cursor(path, None, False)
 
598
                self._view.scroll_to_cell(path, use_align=True)
 
599
                break