/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: 2006-05-19 16:56:46 UTC
  • mfrom: (0.1.25 gannotate)
  • Revision ID: jelmer@samba.org-20060519165646-0d867938fdbc9097
Merge in Dan Loda's gannotate plugin and put it in annotate/

Show diffs side-by-side

added added

removed removed

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