/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)
45
 
 
46
 
        self.construct()
47
 
        self.rev_tree = None
48
 
        self.parent_tree = None
49
 
 
50
 
    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 DiffWindow(Window):
 
23
 
 
24
from bzrlib.delta import compare_trees
 
25
from bzrlib.diff import show_diff_trees
 
26
 
 
27
 
 
28
class DiffWindow(gtk.Window):
265
29
    """Diff window.
266
30
 
267
31
    This object represents and manages a single window containing the
268
32
    differences between two revisions on a branch.
269
33
    """
270
34
 
271
 
    def __init__(self, parent=None):
272
 
        Window.__init__(self, parent)
 
35
    def __init__(self, app=None):
 
36
        gtk.Window.__init__(self, gtk.WINDOW_TOPLEVEL)
273
37
        self.set_border_width(0)
274
38
        self.set_title("bzrk diff")
275
39
 
 
40
        self.app = app
 
41
 
276
42
        # Use two thirds of the screen by default
277
43
        screen = self.get_screen()
278
44
        monitor = screen.get_monitor_geometry(0)
284
50
 
285
51
    def construct(self):
286
52
        """Construct the window contents."""
287
 
        # The   window  consists  of   a  pane   containing:  the
288
 
        # hierarchical list  of files on  the left, and  the diff
289
 
        # for the currently selected file on the right.
290
 
        pane = gtk.HPaned()
291
 
        self.add(pane)
292
 
        pane.show()
 
53
        hbox = gtk.HBox(spacing=6)
 
54
        hbox.set_border_width(12)
 
55
        self.add(hbox)
 
56
        hbox.show()
293
57
 
294
 
        # The file hierarchy: a scrollable treeview
295
58
        scrollwin = gtk.ScrolledWindow()
296
59
        scrollwin.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
297
60
        scrollwin.set_shadow_type(gtk.SHADOW_IN)
298
 
        pane.pack1(scrollwin)
 
61
        hbox.pack_start(scrollwin, expand=False, fill=True)
299
62
        scrollwin.show()
300
63
 
301
64
        self.model = gtk.TreeStore(str, str)
313
76
        column.add_attribute(cell, "text", 0)
314
77
        self.treeview.append_column(column)
315
78
 
316
 
        # The diffs of the  selected file: a scrollable source or
317
 
        # text view
318
 
        self.diff_view = DiffView()
319
 
        pane.pack2(self.diff_view)
320
 
        self.diff_view.show()
321
 
 
322
 
    def set_diff(self, description, 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):
323
104
        """Set the differences showed by this window.
324
105
 
325
106
        Compares the two trees and populates the window with the
326
107
        differences.
327
108
        """
328
 
        self.diff_view.set_trees(rev_tree, parent_tree)
329
 
        self.rev_tree = rev_tree
330
 
        self.parent_tree = parent_tree
 
109
        self.rev_tree = branch.repository.revision_tree(revid)
 
110
        self.parent_tree = branch.repository.revision_tree(parentid)
331
111
 
332
112
        self.model.clear()
333
 
        delta = self.rev_tree.changes_from(self.parent_tree)
 
113
        delta = compare_trees(self.parent_tree, self.rev_tree)
334
114
 
335
115
        self.model.append(None, [ "Complete Diff", "" ])
336
116
 
348
128
            titer = self.model.append(None, [ "Renamed", None ])
349
129
            for oldpath, newpath, id, kind, text_modified, meta_modified \
350
130
                    in delta.renamed:
351
 
                self.model.append(titer, [ oldpath, newpath ])
 
131
                self.model.append(titer, [ oldpath, oldpath ])
352
132
 
353
133
        if len(delta.modified):
354
134
            titer = self.model.append(None, [ "Modified", None ])
356
136
                self.model.append(titer, [ path, path ])
357
137
 
358
138
        self.treeview.expand_all()
359
 
        self.set_title(description + " - bzrk diff")
360
 
 
361
 
    def set_file(self, file_path):
362
 
        tv_path = None
363
 
        for data in self.model:
364
 
            for child in data.iterchildren():
365
 
                if child[0] == file_path or child[1] == file_path:
366
 
                    tv_path = child.path
367
 
                    break
368
 
        if tv_path is None:
369
 
            raise NoSuchFile(file_path)
370
 
        self.treeview.set_cursor(tv_path)
371
 
        self.treeview.scroll_to_cell(tv_path)
 
139
        self.set_title(revid + " - " + branch.nick + " - bzrk diff")
372
140
 
373
141
    def _treeview_cursor_cb(self, *args):
374
142
        """Callback for when the treeview cursor changes."""
377
145
        if specific_files == [ None ]:
378
146
            return
379
147
        elif specific_files == [ "" ]:
380
 
            specific_files = None
381
 
 
382
 
        self.diff_view.show_diff(specific_files)
383
 
 
384
 
 
385
 
def _iter_changes_to_status(source, target):
386
 
    """Determine the differences between trees.
387
 
 
388
 
    This is a wrapper around _iter_changes which just yields more
389
 
    understandable results.
390
 
 
391
 
    :param source: The source tree (basis tree)
392
 
    :param target: The target tree
393
 
    :return: A list of (file_id, real_path, change_type, display_path)
394
 
    """
395
 
    added = 'added'
396
 
    removed = 'removed'
397
 
    renamed = 'renamed'
398
 
    renamed_and_modified = 'renamed and modified'
399
 
    modified = 'modified'
400
 
    kind_changed = 'kind changed'
401
 
 
402
 
    # TODO: Handle metadata changes
403
 
 
404
 
    status = []
405
 
    target.lock_read()
406
 
    try:
407
 
        source.lock_read()
408
 
        try:
409
 
            for (file_id, paths, changed_content, versioned, parent_ids, names,
410
 
                 kinds, executables) in target._iter_changes(source):
411
 
 
412
 
                # Skip the root entry if it isn't very interesting
413
 
                if parent_ids == (None, None):
414
 
                    continue
415
 
 
416
 
                change_type = None
417
 
                if kinds[0] is None:
418
 
                    source_marker = ''
419
 
                else:
420
 
                    source_marker = osutils.kind_marker(kinds[0])
421
 
                if kinds[1] is None:
422
 
                    assert kinds[0] is not None
423
 
                    marker = osutils.kind_marker(kinds[0])
424
 
                else:
425
 
                    marker = osutils.kind_marker(kinds[1])
426
 
 
427
 
                real_path = paths[1]
428
 
                if real_path is None:
429
 
                    real_path = paths[0]
430
 
                assert real_path is not None
431
 
                display_path = real_path + marker
432
 
 
433
 
                present_source = versioned[0] and kinds[0] is not None
434
 
                present_target = versioned[1] and kinds[1] is not None
435
 
 
436
 
                if present_source != present_target:
437
 
                    if present_target:
438
 
                        change_type = added
439
 
                    else:
440
 
                        assert present_source
441
 
                        change_type = removed
442
 
                elif names[0] != names[1] or parent_ids[0] != parent_ids[1]:
443
 
                    # Renamed
444
 
                    if changed_content or executables[0] != executables[1]:
445
 
                        # and modified
446
 
                        change_type = renamed_and_modified
447
 
                    else:
448
 
                        change_type = renamed
449
 
                    display_path = (paths[0] + source_marker
450
 
                                    + ' => ' + paths[1] + marker)
451
 
                elif kinds[0] != kinds[1]:
452
 
                    change_type = kind_changed
453
 
                    display_path = (paths[0] + source_marker
454
 
                                    + ' => ' + paths[1] + marker)
455
 
                elif changed_content is True or executables[0] != executables[1]:
456
 
                    change_type = modified
457
 
                else:
458
 
                    assert False, "How did we get here?"
459
 
 
460
 
                status.append((file_id, real_path, change_type, display_path))
461
 
        finally:
462
 
            source.unlock()
463
 
    finally:
464
 
        target.unlock()
465
 
 
466
 
    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())