/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: 2011-02-18 13:01:03 UTC
  • Revision ID: jelmer@samba.org-20110218130103-fiyk203auk28thpn
Remove some unused imports, fix some formatting.

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