/b-gtk/fix-viz

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/b-gtk/fix-viz
10 by Scott James Remnant
Add an extra window type, clicking the little icons next to a parent
1
# -*- coding: UTF-8 -*-
2
"""Difference window.
3
4
This module contains the code to manage the diff window which shows
5
the changes made between two revisions on a branch.
6
"""
7
8
__copyright__ = "Copyright © 2005 Canonical Ltd."
9
__author__    = "Scott James Remnant <scott@ubuntu.com>"
10
11
12
from cStringIO import StringIO
13
252 by Aaron Bentley
Fix test suite
14
import pygtk
15
pygtk.require("2.0")
10 by Scott James Remnant
Add an extra window type, clicking the little icons next to a parent
16
import gtk
17
import pango
232.1.1 by Adeodato Simó
Read ~/.colordiffrc to set colors in the diff window.
18
import os
19
import re
76 by Jelmer Vernooij
Replace non-UTF8 characters rather than generating an exception (fixes #44677).
20
import sys
10 by Scott James Remnant
Add an extra window type, clicking the little icons next to a parent
21
22
try:
23
    import gtksourceview
24
    have_gtksourceview = True
25
except ImportError:
26
    have_gtksourceview = False
232.1.3 by Adeodato Simó
Support setting diff colors from gedit's syntax highlighting config too.
27
try:
28
    import gconf
29
    have_gconf = True
30
except ImportError:
31
    have_gconf = False
10 by Scott James Remnant
Add an extra window type, clicking the little icons next to a parent
32
278.1.29 by John Arbash Meinel
Start testing with Unicode data.
33
from bzrlib import osutils
34
from bzrlib.diff import show_diff_trees, internal_diff
59.2.4 by Aaron Bentley
Teach gdiff to accept a single file argument
35
from bzrlib.errors import NoSuchFile
232.1.1 by Adeodato Simó
Read ~/.colordiffrc to set colors in the diff window.
36
from bzrlib.trace import warning
298.2.1 by Daniel Schierbeck
Refactored the GTK window code, creating a single base window class that handles keyboard events.
37
from bzrlib.plugins.gtk.window import Window
38
10 by Scott James Remnant
Add an extra window type, clicking the little icons next to a parent
39
278.1.12 by John Arbash Meinel
Delay computing the delta, and clean up some of the diff view names.
40
class DiffView(gtk.ScrolledWindow):
278.1.4 by John Arbash Meinel
Just playing around.
41
    """This is the soft and chewy filling for a DiffWindow."""
10 by Scott James Remnant
Add an extra window type, clicking the little icons next to a parent
42
51 by Jelmer Vernooij
Rework some of the parameters to DiffWindow.set_diff() to be
43
    def __init__(self):
278.1.4 by John Arbash Meinel
Just playing around.
44
        gtk.ScrolledWindow.__init__(self)
10 by Scott James Remnant
Add an extra window type, clicking the little icons next to a parent
45
46
        self.construct()
278.1.4 by John Arbash Meinel
Just playing around.
47
        self.rev_tree = None
48
        self.parent_tree = None
10 by Scott James Remnant
Add an extra window type, clicking the little icons next to a parent
49
50
    def construct(self):
278.1.4 by John Arbash Meinel
Just playing around.
51
        self.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
52
        self.set_shadow_type(gtk.SHADOW_IN)
10 by Scott James Remnant
Add an extra window type, clicking the little icons next to a parent
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")
232.1.3 by Adeodato Simó
Support setting diff colors from gedit's syntax highlighting config too.
58
            if have_gconf:
59
                self.apply_gedit_colors(gsl)
232.1.2 by Adeodato Simó
Rename apply_colordiffrc to apply_colordiff_colors, improve docstring.
60
            self.apply_colordiff_colors(gsl)
10 by Scott James Remnant
Add an extra window type, clicking the little icons next to a parent
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"))
278.1.4 by John Arbash Meinel
Just playing around.
71
        self.add(sourceview)
10 by Scott James Remnant
Add an extra window type, clicking the little icons next to a parent
72
        sourceview.show()
73
232.1.1 by Adeodato Simó
Read ~/.colordiffrc to set colors in the diff window.
74
    @staticmethod
232.1.3 by Adeodato Simó
Support setting diff colors from gedit's syntax highlighting config too.
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.
278.1.4 by John Arbash Meinel
Just playing around.
79
232.1.3 by Adeodato Simó
Support setting diff colors from gedit's syntax highlighting config too.
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
232.1.2 by Adeodato Simó
Rename apply_colordiffrc to apply_colordiff_colors, improve docstring.
138
    def apply_colordiff_colors(lang):
139
        """Set style colors for lang using the colordiff configuration file.
232.1.1 by Adeodato Simó
Read ~/.colordiffrc to set colors in the diff window.
140
141
        Both ~/.colordiffrc and ~/.colordiffrc.bzr-gtk are read.
142
232.1.2 by Adeodato Simó
Rename apply_colordiffrc to apply_colordiff_colors, improve docstring.
143
        :param lang: a "Diff" gtksourceview.SourceLanguage object.
232.1.1 by Adeodato Simó
Read ~/.colordiffrc to set colors in the diff window.
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:
278.1.12 by John Arbash Meinel
Delay computing the delta, and clean up some of the diff view names.
155
                    colors.update(DiffView.parse_colordiffrc(f))
232.1.1 by Adeodato Simó
Read ~/.colordiffrc to set colors in the diff window.
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)
232.1.4 by Adeodato Simó
Add a test for parse_colordiffrc, and fix the function to handle empty lines.
193
194
    @staticmethod
195
    def parse_colordiffrc(fileobj):
196
        """Parse fileobj as a colordiff configuration file.
278.1.4 by John Arbash Meinel
Just playing around.
197
232.1.4 by Adeodato Simó
Add a test for parse_colordiffrc, and fix the function to handle empty lines.
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
278.1.4 by John Arbash Meinel
Just playing around.
210
    def set_trees(self, rev_tree, parent_tree):
211
        self.rev_tree = rev_tree
212
        self.parent_tree = parent_tree
278.1.29 by John Arbash Meinel
Start testing with Unicode data.
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()
278.1.4 by John Arbash Meinel
Just playing around.
238
239
    def show_diff(self, specific_files):
240
        s = StringIO()
278.1.18 by John Arbash Meinel
Start checking the diff view is correct.
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
                        )
278.1.29 by John Arbash Meinel
Start testing with Unicode data.
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'))
278.1.4 by John Arbash Meinel
Just playing around.
262
263
423.8.1 by Jelmer Vernooij
Allow using the diff control as a widget
264
class DiffWidget(gtk.HPaned):
265
    """Diff widget
278.1.4 by John Arbash Meinel
Just playing around.
266
267
    """
423.8.1 by Jelmer Vernooij
Allow using the diff control as a widget
268
    def __init__(self):
269
        super(DiffWidget, self).__init__()
278.1.4 by John Arbash Meinel
Just playing around.
270
271
        # The file hierarchy: a scrollable treeview
272
        scrollwin = gtk.ScrolledWindow()
273
        scrollwin.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
274
        scrollwin.set_shadow_type(gtk.SHADOW_IN)
423.8.1 by Jelmer Vernooij
Allow using the diff control as a widget
275
        self.pack1(scrollwin)
278.1.4 by John Arbash Meinel
Just playing around.
276
        scrollwin.show()
277
278
        self.model = gtk.TreeStore(str, str)
279
        self.treeview = gtk.TreeView(self.model)
280
        self.treeview.set_headers_visible(False)
281
        self.treeview.set_search_column(1)
282
        self.treeview.connect("cursor-changed", self._treeview_cursor_cb)
283
        scrollwin.add(self.treeview)
284
        self.treeview.show()
285
286
        cell = gtk.CellRendererText()
287
        cell.set_property("width-chars", 20)
288
        column = gtk.TreeViewColumn()
289
        column.pack_start(cell, expand=True)
290
        column.add_attribute(cell, "text", 0)
291
        self.treeview.append_column(column)
292
293
        # The diffs of the  selected file: a scrollable source or
294
        # text view
278.1.12 by John Arbash Meinel
Delay computing the delta, and clean up some of the diff view names.
295
        self.diff_view = DiffView()
423.8.1 by Jelmer Vernooij
Allow using the diff control as a widget
296
        self.pack2(self.diff_view)
278.1.12 by John Arbash Meinel
Delay computing the delta, and clean up some of the diff view names.
297
        self.diff_view.show()
278.1.4 by John Arbash Meinel
Just playing around.
298
423.8.1 by Jelmer Vernooij
Allow using the diff control as a widget
299
    def set_diff(self, rev_tree, parent_tree):
278.1.4 by John Arbash Meinel
Just playing around.
300
        """Set the differences showed by this window.
301
302
        Compares the two trees and populates the window with the
303
        differences.
304
        """
278.1.12 by John Arbash Meinel
Delay computing the delta, and clean up some of the diff view names.
305
        self.diff_view.set_trees(rev_tree, parent_tree)
278.1.4 by John Arbash Meinel
Just playing around.
306
        self.rev_tree = rev_tree
307
        self.parent_tree = parent_tree
308
309
        self.model.clear()
310
        delta = self.rev_tree.changes_from(self.parent_tree)
311
312
        self.model.append(None, [ "Complete Diff", "" ])
313
314
        if len(delta.added):
315
            titer = self.model.append(None, [ "Added", None ])
316
            for path, id, kind in delta.added:
317
                self.model.append(titer, [ path, path ])
318
319
        if len(delta.removed):
320
            titer = self.model.append(None, [ "Removed", None ])
321
            for path, id, kind in delta.removed:
322
                self.model.append(titer, [ path, path ])
323
324
        if len(delta.renamed):
325
            titer = self.model.append(None, [ "Renamed", None ])
326
            for oldpath, newpath, id, kind, text_modified, meta_modified \
327
                    in delta.renamed:
328
                self.model.append(titer, [ oldpath, newpath ])
329
330
        if len(delta.modified):
331
            titer = self.model.append(None, [ "Modified", None ])
332
            for path, id, kind, text_modified, meta_modified in delta.modified:
333
                self.model.append(titer, [ path, path ])
334
335
        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)
348
349
    def _treeview_cursor_cb(self, *args):
350
        """Callback for when the treeview cursor changes."""
351
        (path, col) = self.treeview.get_cursor()
352
        specific_files = [ self.model[path][1] ]
353
        if specific_files == [ None ]:
354
            return
355
        elif specific_files == [ "" ]:
356
            specific_files = None
357
278.1.12 by John Arbash Meinel
Delay computing the delta, and clean up some of the diff view names.
358
        self.diff_view.show_diff(specific_files)
278.1.29 by John Arbash Meinel
Start testing with Unicode data.
359
360
423.8.1 by Jelmer Vernooij
Allow using the diff control as a widget
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
278.1.29 by John Arbash Meinel
Start testing with Unicode data.
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