/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 viz/diffwin.py

  • Committer: Jelmer Vernooij
  • Date: 2006-05-19 16:37:13 UTC
  • Revision ID: jelmer@samba.org-20060519163713-be77b31c72cbc7e8
Move visualisation code to a separate directory, preparing for bundling 
the GTK+ plugins for bzr.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#!/usr/bin/python
1
2
# -*- coding: UTF-8 -*-
2
3
"""Difference window.
3
4
 
11
12
 
12
13
from cStringIO import StringIO
13
14
 
14
 
import pygtk
15
 
pygtk.require("2.0")
16
15
import gtk
17
16
import pango
18
 
import os
19
 
import re
20
 
import sys
21
17
 
22
18
try:
23
19
    import gtksourceview
24
20
    have_gtksourceview = True
25
21
except ImportError:
26
22
    have_gtksourceview = False
27
 
try:
28
 
    import gconf
29
 
    have_gconf = True
30
 
except ImportError:
31
 
    have_gconf = False
32
 
 
33
 
from bzrlib import osutils
34
 
from bzrlib.diff import show_diff_trees, internal_diff
35
 
from bzrlib.errors import NoSuchFile
36
 
from bzrlib.trace import warning
37
 
from bzrlib.plugins.gtk.window import Window
38
 
 
39
 
 
40
 
class DiffView(gtk.ScrolledWindow):
41
 
    """This is the soft and chewy filling for a DiffWindow."""
42
 
 
43
 
    def __init__(self):
44
 
        gtk.ScrolledWindow.__init__(self)
 
23
 
 
24
from bzrlib.delta import compare_trees
 
25
from bzrlib.diff import show_diff_trees
 
26
 
 
27
 
 
28
class DiffWindow(gtk.Window):
 
29
    """Diff window.
 
30
 
 
31
    This object represents and manages a single window containing the
 
32
    differences between two revisions on a branch.
 
33
    """
 
34
 
 
35
    def __init__(self, app=None):
 
36
        gtk.Window.__init__(self, gtk.WINDOW_TOPLEVEL)
 
37
        self.set_border_width(0)
 
38
        self.set_title("bzrk diff")
 
39
 
 
40
        self.app = app
 
41
 
 
42
        # Use two thirds of the screen by default
 
43
        screen = self.get_screen()
 
44
        monitor = screen.get_monitor_geometry(0)
 
45
        width = int(monitor.width * 0.66)
 
46
        height = int(monitor.height * 0.66)
 
47
        self.set_default_size(width, height)
45
48
 
46
49
        self.construct()
47
 
        self.rev_tree = None
48
 
        self.parent_tree = None
49
50
 
50
51
    def construct(self):
51
 
        self.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
52
 
        self.set_shadow_type(gtk.SHADOW_IN)
53
 
 
54
 
        if have_gtksourceview:
55
 
            self.buffer = gtksourceview.SourceBuffer()
56
 
            slm = gtksourceview.SourceLanguagesManager()
57
 
            gsl = slm.get_language_from_mime_type("text/x-patch")
58
 
            if have_gconf:
59
 
                self.apply_gedit_colors(gsl)
60
 
            self.apply_colordiff_colors(gsl)
61
 
            self.buffer.set_language(gsl)
62
 
            self.buffer.set_highlight(True)
63
 
 
64
 
            sourceview = gtksourceview.SourceView(self.buffer)
65
 
        else:
66
 
            self.buffer = gtk.TextBuffer()
67
 
            sourceview = gtk.TextView(self.buffer)
68
 
 
69
 
        sourceview.set_editable(False)
70
 
        sourceview.modify_font(pango.FontDescription("Monospace"))
71
 
        self.add(sourceview)
72
 
        sourceview.show()
73
 
 
74
 
    @staticmethod
75
 
    def apply_gedit_colors(lang):
76
 
        """Set style for lang to that specified in gedit configuration.
77
 
 
78
 
        This method needs the gconf module.
79
 
 
80
 
        :param lang: a gtksourceview.SourceLanguage object.
81
 
        """
82
 
        GEDIT_SYNTAX_PATH = '/apps/gedit-2/preferences/syntax_highlighting'
83
 
        GEDIT_LANG_PATH = GEDIT_SYNTAX_PATH + '/' + lang.get_id()
84
 
 
85
 
        client = gconf.client_get_default()
86
 
        client.add_dir(GEDIT_LANG_PATH, gconf.CLIENT_PRELOAD_NONE)
87
 
 
88
 
        for tag in lang.get_tags():
89
 
            tag_id = tag.get_id()
90
 
            gconf_key = GEDIT_LANG_PATH + '/' + tag_id
91
 
            style_string = client.get_string(gconf_key)
92
 
 
93
 
            if style_string is None:
94
 
                continue
95
 
 
96
 
            # function to get a bool from a string that's either '0' or '1'
97
 
            string_bool = lambda x: bool(int(x))
98
 
 
99
 
            # style_string is a string like "2/#FFCCAA/#000000/0/1/0/0"
100
 
            # values are: mask, fg, bg, italic, bold, underline, strike
101
 
            # this packs them into (str_value, attr_name, conv_func) tuples
102
 
            items = zip(style_string.split('/'), ['mask', 'foreground',
103
 
                'background', 'italic', 'bold', 'underline', 'strikethrough' ],
104
 
                [ int, gtk.gdk.color_parse, gtk.gdk.color_parse, string_bool,
105
 
                    string_bool, string_bool, string_bool ]
106
 
            )
107
 
 
108
 
            style = gtksourceview.SourceTagStyle()
109
 
 
110
 
            # XXX The mask attribute controls whether the present values of
111
 
            # foreground and background color should in fact be used. Ideally
112
 
            # (and that's what gedit does), one could set all three attributes,
113
 
            # and let the TagStyle object figure out which colors to use.
114
 
            # However, in the GtkSourceview python bindings, the mask attribute
115
 
            # is read-only, and it's derived instead from the colors being
116
 
            # set or not. This means that we have to sometimes refrain from
117
 
            # setting fg or bg colors, depending on the value of the mask.
118
 
            # This code could go away if mask were writable.
119
 
            mask = int(items[0][0])
120
 
            if not (mask & 1): # GTK_SOURCE_TAG_STYLE_USE_BACKGROUND
121
 
                items[2:3] = []
122
 
            if not (mask & 2): # GTK_SOURCE_TAG_STYLE_USE_FOREGROUND
123
 
                items[1:2] = []
124
 
            items[0:1] = [] # skip the mask unconditionally
125
 
 
126
 
            for value, attr, func in items:
127
 
                try:
128
 
                    value = func(value)
129
 
                except ValueError:
130
 
                    warning('gconf key %s contains an invalid value: %s'
131
 
                            % gconf_key, value)
132
 
                else:
133
 
                    setattr(style, attr, value)
134
 
 
135
 
            lang.set_tag_style(tag_id, style)
136
 
 
137
 
    @staticmethod
138
 
    def apply_colordiff_colors(lang):
139
 
        """Set style colors for lang using the colordiff configuration file.
140
 
 
141
 
        Both ~/.colordiffrc and ~/.colordiffrc.bzr-gtk are read.
142
 
 
143
 
        :param lang: a "Diff" gtksourceview.SourceLanguage object.
144
 
        """
145
 
        colors = {}
146
 
 
147
 
        for f in ('~/.colordiffrc', '~/.colordiffrc.bzr-gtk'):
148
 
            f = os.path.expanduser(f)
149
 
            if os.path.exists(f):
150
 
                try:
151
 
                    f = file(f)
152
 
                except IOError, e:
153
 
                    warning('could not open file %s: %s' % (f, str(e)))
154
 
                else:
155
 
                    colors.update(DiffView.parse_colordiffrc(f))
156
 
                    f.close()
157
 
 
158
 
        if not colors:
159
 
            # ~/.colordiffrc does not exist
160
 
            return
161
 
 
162
 
        mapping = {
163
 
                # map GtkSourceView tags to colordiff names
164
 
                # since GSV is richer, accept new names for extra bits,
165
 
                # defaulting to old names if they're not present
166
 
                'Added@32@line': ['newtext'],
167
 
                'Removed@32@line': ['oldtext'],
168
 
                'Location': ['location', 'diffstuff'],
169
 
                'Diff@32@file': ['file', 'diffstuff'],
170
 
                'Special@32@case': ['specialcase', 'diffstuff'],
171
 
        }
172
 
 
173
 
        for tag in lang.get_tags():
174
 
            tag_id = tag.get_id()
175
 
            keys = mapping.get(tag_id, [])
176
 
            color = None
177
 
 
178
 
            for key in keys:
179
 
                color = colors.get(key, None)
180
 
                if color is not None:
181
 
                    break
182
 
 
183
 
            if color is None:
184
 
                continue
185
 
 
186
 
            style = gtksourceview.SourceTagStyle()
187
 
            try:
188
 
                style.foreground = gtk.gdk.color_parse(color)
189
 
            except ValueError:
190
 
                warning('not a valid color: %s' % color)
191
 
            else:
192
 
                lang.set_tag_style(tag_id, style)
193
 
 
194
 
    @staticmethod
195
 
    def parse_colordiffrc(fileobj):
196
 
        """Parse fileobj as a colordiff configuration file.
197
 
 
198
 
        :return: A dict with the key -> value pairs.
199
 
        """
200
 
        colors = {}
201
 
        for line in fileobj:
202
 
            if re.match(r'^\s*#', line):
203
 
                continue
204
 
            if '=' not in line:
205
 
                continue
206
 
            key, val = line.split('=', 1)
207
 
            colors[key.strip()] = val.strip()
208
 
        return colors
209
 
 
210
 
    def set_trees(self, rev_tree, parent_tree):
211
 
        self.rev_tree = rev_tree
212
 
        self.parent_tree = parent_tree
213
 
#        self._build_delta()
214
 
 
215
 
#    def _build_delta(self):
216
 
#        self.parent_tree.lock_read()
217
 
#        self.rev_tree.lock_read()
218
 
#        try:
219
 
#            self.delta = _iter_changes_to_status(self.parent_tree, self.rev_tree)
220
 
#            self.path_to_status = {}
221
 
#            self.path_to_diff = {}
222
 
#            source_inv = self.parent_tree.inventory
223
 
#            target_inv = self.rev_tree.inventory
224
 
#            for (file_id, real_path, change_type, display_path) in self.delta:
225
 
#                self.path_to_status[real_path] = u'=== %s %s' % (change_type, display_path)
226
 
#                if change_type in ('modified', 'renamed and modified'):
227
 
#                    source_ie = source_inv[file_id]
228
 
#                    target_ie = target_inv[file_id]
229
 
#                    sio = StringIO()
230
 
#                    source_ie.diff(internal_diff, *old path, *old_tree,
231
 
#                                   *new_path, target_ie, self.rev_tree,
232
 
#                                   sio)
233
 
#                    self.path_to_diff[real_path] = 
234
 
#
235
 
#        finally:
236
 
#            self.rev_tree.unlock()
237
 
#            self.parent_tree.unlock()
238
 
 
239
 
    def show_diff(self, specific_files):
240
 
        s = StringIO()
241
 
        show_diff_trees(self.parent_tree, self.rev_tree, s, specific_files,
242
 
                        old_label='', new_label='',
243
 
                        # path_encoding=sys.getdefaultencoding()
244
 
                        # The default is utf-8, but we interpret the file
245
 
                        # contents as getdefaultencoding(), so we should
246
 
                        # probably try to make the paths in the same encoding.
247
 
                        )
248
 
        # str.decode(encoding, 'replace') doesn't do anything. Because if a
249
 
        # character is not valid in 'encoding' there is nothing to replace, the
250
 
        # 'replace' is for 'str.encode()'
251
 
        try:
252
 
            decoded = s.getvalue().decode(sys.getdefaultencoding())
253
 
        except UnicodeDecodeError:
254
 
            try:
255
 
                decoded = s.getvalue().decode('UTF-8')
256
 
            except UnicodeDecodeError:
257
 
                decoded = s.getvalue().decode('iso-8859-1')
258
 
                # This always works, because every byte has a valid
259
 
                # mapping from iso-8859-1 to Unicode
260
 
        # TextBuffer must contain pure UTF-8 data
261
 
        self.buffer.set_text(decoded.encode('UTF-8'))
262
 
 
263
 
 
264
 
class DiffWidget(gtk.HPaned):
265
 
    """Diff widget
266
 
 
267
 
    """
268
 
    def __init__(self):
269
 
        super(DiffWidget, self).__init__()
270
 
 
271
 
        # The file hierarchy: a scrollable treeview
 
52
        """Construct the window contents."""
 
53
        hbox = gtk.HBox(spacing=6)
 
54
        hbox.set_border_width(12)
 
55
        self.add(hbox)
 
56
        hbox.show()
 
57
 
272
58
        scrollwin = gtk.ScrolledWindow()
273
59
        scrollwin.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
274
60
        scrollwin.set_shadow_type(gtk.SHADOW_IN)
275
 
        self.pack1(scrollwin)
 
61
        hbox.pack_start(scrollwin, expand=False, fill=True)
276
62
        scrollwin.show()
277
63
 
278
64
        self.model = gtk.TreeStore(str, str)
290
76
        column.add_attribute(cell, "text", 0)
291
77
        self.treeview.append_column(column)
292
78
 
293
 
        # The diffs of the  selected file: a scrollable source or
294
 
        # text view
295
 
        self.diff_view = DiffView()
296
 
        self.pack2(self.diff_view)
297
 
        self.diff_view.show()
298
 
 
299
 
    def set_diff(self, rev_tree, parent_tree):
 
79
 
 
80
        scrollwin = gtk.ScrolledWindow()
 
81
        scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
 
82
        scrollwin.set_shadow_type(gtk.SHADOW_IN)
 
83
        hbox.pack_start(scrollwin, expand=True, fill=True)
 
84
        scrollwin.show()
 
85
 
 
86
        if have_gtksourceview:
 
87
            self.buffer = gtksourceview.SourceBuffer()
 
88
            slm = gtksourceview.SourceLanguagesManager()
 
89
            gsl = slm.get_language_from_mime_type("text/x-patch")
 
90
            self.buffer.set_language(gsl)
 
91
            self.buffer.set_highlight(True)
 
92
 
 
93
            sourceview = gtksourceview.SourceView(self.buffer)
 
94
        else:
 
95
            self.buffer = gtk.TextBuffer()
 
96
            sourceview = gtk.TextView(self.buffer)
 
97
 
 
98
        sourceview.set_editable(False)
 
99
        sourceview.modify_font(pango.FontDescription("Monospace"))
 
100
        scrollwin.add(sourceview)
 
101
        sourceview.show()
 
102
 
 
103
    def set_diff(self, branch, revid, parentid):
300
104
        """Set the differences showed by this window.
301
105
 
302
106
        Compares the two trees and populates the window with the
303
107
        differences.
304
108
        """
305
 
        self.diff_view.set_trees(rev_tree, parent_tree)
306
 
        self.rev_tree = rev_tree
307
 
        self.parent_tree = parent_tree
 
109
        self.rev_tree = branch.repository.revision_tree(revid)
 
110
        self.parent_tree = branch.repository.revision_tree(parentid)
308
111
 
309
112
        self.model.clear()
310
 
        delta = self.rev_tree.changes_from(self.parent_tree)
 
113
        delta = compare_trees(self.parent_tree, self.rev_tree)
311
114
 
312
115
        self.model.append(None, [ "Complete Diff", "" ])
313
116
 
325
128
            titer = self.model.append(None, [ "Renamed", None ])
326
129
            for oldpath, newpath, id, kind, text_modified, meta_modified \
327
130
                    in delta.renamed:
328
 
                self.model.append(titer, [ oldpath, newpath ])
 
131
                self.model.append(titer, [ oldpath, oldpath ])
329
132
 
330
133
        if len(delta.modified):
331
134
            titer = self.model.append(None, [ "Modified", None ])
333
136
                self.model.append(titer, [ path, path ])
334
137
 
335
138
        self.treeview.expand_all()
336
 
 
337
 
    def set_file(self, file_path):
338
 
        tv_path = None
339
 
        for data in self.model:
340
 
            for child in data.iterchildren():
341
 
                if child[0] == file_path or child[1] == file_path:
342
 
                    tv_path = child.path
343
 
                    break
344
 
        if tv_path is None:
345
 
            raise NoSuchFile(file_path)
346
 
        self.treeview.set_cursor(tv_path)
347
 
        self.treeview.scroll_to_cell(tv_path)
 
139
        self.set_title(revid + " - " + branch.nick + " - bzrk diff")
348
140
 
349
141
    def _treeview_cursor_cb(self, *args):
350
142
        """Callback for when the treeview cursor changes."""
353
145
        if specific_files == [ None ]:
354
146
            return
355
147
        elif specific_files == [ "" ]:
356
 
            specific_files = None
357
 
 
358
 
        self.diff_view.show_diff(specific_files)
359
 
 
360
 
 
361
 
class DiffWindow(Window):
362
 
    """Diff window.
363
 
 
364
 
    This object represents and manages a single window containing the
365
 
    differences between two revisions on a branch.
366
 
    """
367
 
 
368
 
    def __init__(self, parent=None):
369
 
        Window.__init__(self, parent)
370
 
        self.set_border_width(0)
371
 
        self.set_title("bzrk diff")
372
 
 
373
 
        # Use two thirds of the screen by default
374
 
        screen = self.get_screen()
375
 
        monitor = screen.get_monitor_geometry(0)
376
 
        width = int(monitor.width * 0.66)
377
 
        height = int(monitor.height * 0.66)
378
 
        self.set_default_size(width, height)
379
 
 
380
 
        self.construct()
381
 
 
382
 
    def construct(self):
383
 
        """Construct the window contents."""
384
 
        self.diff = DiffWidget()
385
 
        self.add(self.diff)
386
 
        self.diff.show_all()
387
 
 
388
 
    def set_diff(self, description, rev_tree, parent_tree):
389
 
        """Set the differences showed by this window.
390
 
 
391
 
        Compares the two trees and populates the window with the
392
 
        differences.
393
 
        """
394
 
        self.diff.set_diff(rev_tree, parent_tree)
395
 
        self.set_title(description + " - bzrk diff")
396
 
 
397
 
    def set_file(self, file_path):
398
 
        self.diff.set_file(file_path)
399
 
 
400
 
 
401
 
def _iter_changes_to_status(source, target):
402
 
    """Determine the differences between trees.
403
 
 
404
 
    This is a wrapper around _iter_changes which just yields more
405
 
    understandable results.
406
 
 
407
 
    :param source: The source tree (basis tree)
408
 
    :param target: The target tree
409
 
    :return: A list of (file_id, real_path, change_type, display_path)
410
 
    """
411
 
    added = 'added'
412
 
    removed = 'removed'
413
 
    renamed = 'renamed'
414
 
    renamed_and_modified = 'renamed and modified'
415
 
    modified = 'modified'
416
 
    kind_changed = 'kind changed'
417
 
 
418
 
    # TODO: Handle metadata changes
419
 
 
420
 
    status = []
421
 
    target.lock_read()
422
 
    try:
423
 
        source.lock_read()
424
 
        try:
425
 
            for (file_id, paths, changed_content, versioned, parent_ids, names,
426
 
                 kinds, executables) in target._iter_changes(source):
427
 
 
428
 
                # Skip the root entry if it isn't very interesting
429
 
                if parent_ids == (None, None):
430
 
                    continue
431
 
 
432
 
                change_type = None
433
 
                if kinds[0] is None:
434
 
                    source_marker = ''
435
 
                else:
436
 
                    source_marker = osutils.kind_marker(kinds[0])
437
 
                if kinds[1] is None:
438
 
                    assert kinds[0] is not None
439
 
                    marker = osutils.kind_marker(kinds[0])
440
 
                else:
441
 
                    marker = osutils.kind_marker(kinds[1])
442
 
 
443
 
                real_path = paths[1]
444
 
                if real_path is None:
445
 
                    real_path = paths[0]
446
 
                assert real_path is not None
447
 
                display_path = real_path + marker
448
 
 
449
 
                present_source = versioned[0] and kinds[0] is not None
450
 
                present_target = versioned[1] and kinds[1] is not None
451
 
 
452
 
                if present_source != present_target:
453
 
                    if present_target:
454
 
                        change_type = added
455
 
                    else:
456
 
                        assert present_source
457
 
                        change_type = removed
458
 
                elif names[0] != names[1] or parent_ids[0] != parent_ids[1]:
459
 
                    # Renamed
460
 
                    if changed_content or executables[0] != executables[1]:
461
 
                        # and modified
462
 
                        change_type = renamed_and_modified
463
 
                    else:
464
 
                        change_type = renamed
465
 
                    display_path = (paths[0] + source_marker
466
 
                                    + ' => ' + paths[1] + marker)
467
 
                elif kinds[0] != kinds[1]:
468
 
                    change_type = kind_changed
469
 
                    display_path = (paths[0] + source_marker
470
 
                                    + ' => ' + paths[1] + marker)
471
 
                elif changed_content is True or executables[0] != executables[1]:
472
 
                    change_type = modified
473
 
                else:
474
 
                    assert False, "How did we get here?"
475
 
 
476
 
                status.append((file_id, real_path, change_type, display_path))
477
 
        finally:
478
 
            source.unlock()
479
 
    finally:
480
 
        target.unlock()
481
 
 
482
 
    return status
 
148
            specific_files = []
 
149
 
 
150
        s = StringIO()
 
151
        show_diff_trees(self.parent_tree, self.rev_tree, s, specific_files)
 
152
        self.buffer.set_text(s.getvalue())