/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
#!/usr/bin/python
2
# -*- coding: UTF-8 -*-
3
"""Difference window.
4
5
This module contains the code to manage the diff window which shows
6
the changes made between two revisions on a branch.
7
"""
8
9
__copyright__ = "Copyright © 2005 Canonical Ltd."
10
__author__    = "Scott James Remnant <scott@ubuntu.com>"
11
12
13
from cStringIO import StringIO
14
15
import gtk
16
import pango
232.1.1 by Adeodato Simó
Read ~/.colordiffrc to set colors in the diff window.
17
import os
18
import re
76 by Jelmer Vernooij
Replace non-UTF8 characters rather than generating an exception (fixes #44677).
19
import sys
10 by Scott James Remnant
Add an extra window type, clicking the little icons next to a parent
20
21
try:
22
    import gtksourceview
23
    have_gtksourceview = True
24
except ImportError:
25
    have_gtksourceview = False
232.1.3 by Adeodato Simó
Support setting diff colors from gedit's syntax highlighting config too.
26
try:
27
    import gconf
28
    have_gconf = True
29
except ImportError:
30
    have_gconf = False
10 by Scott James Remnant
Add an extra window type, clicking the little icons next to a parent
31
71 by Szilveszter Farkas (Phanatic)
(phanatic) compare_trees deprecated in 0.9
32
import bzrlib
33
10 by Scott James Remnant
Add an extra window type, clicking the little icons next to a parent
34
from bzrlib.diff import show_diff_trees
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
10 by Scott James Remnant
Add an extra window type, clicking the little icons next to a parent
37
38
39
class DiffWindow(gtk.Window):
40
    """Diff window.
41
42
    This object represents and manages a single window containing the
43
    differences between two revisions on a branch.
44
    """
45
51 by Jelmer Vernooij
Rework some of the parameters to DiffWindow.set_diff() to be
46
    def __init__(self):
10 by Scott James Remnant
Add an extra window type, clicking the little icons next to a parent
47
        gtk.Window.__init__(self, gtk.WINDOW_TOPLEVEL)
48
        self.set_border_width(0)
49
        self.set_title("bzrk diff")
50
51
        # Use two thirds of the screen by default
52
        screen = self.get_screen()
53
        monitor = screen.get_monitor_geometry(0)
54
        width = int(monitor.width * 0.66)
55
        height = int(monitor.height * 0.66)
56
        self.set_default_size(width, height)
57
58
        self.construct()
59
60
    def construct(self):
61
        """Construct the window contents."""
75 by Jelmer Vernooij
Use HPaned rather than HBox so long filenames can be viewed (fixes #56993).
62
        # The   window  consists  of   a  pane   containing:  the
63
        # hierarchical list  of files on  the left, and  the diff
64
        # for the currently selected file on the right.
65
        pane = gtk.HPaned()
66
        self.add(pane)
67
        pane.show()
10 by Scott James Remnant
Add an extra window type, clicking the little icons next to a parent
68
75 by Jelmer Vernooij
Use HPaned rather than HBox so long filenames can be viewed (fixes #56993).
69
        # The file hierarchy: a scrollable treeview
10 by Scott James Remnant
Add an extra window type, clicking the little icons next to a parent
70
        scrollwin = gtk.ScrolledWindow()
71
        scrollwin.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
72
        scrollwin.set_shadow_type(gtk.SHADOW_IN)
75 by Jelmer Vernooij
Use HPaned rather than HBox so long filenames can be viewed (fixes #56993).
73
        pane.pack1(scrollwin)
10 by Scott James Remnant
Add an extra window type, clicking the little icons next to a parent
74
        scrollwin.show()
75
76
        self.model = gtk.TreeStore(str, str)
77
        self.treeview = gtk.TreeView(self.model)
78
        self.treeview.set_headers_visible(False)
79
        self.treeview.set_search_column(1)
80
        self.treeview.connect("cursor-changed", self._treeview_cursor_cb)
81
        scrollwin.add(self.treeview)
82
        self.treeview.show()
83
84
        cell = gtk.CellRendererText()
85
        cell.set_property("width-chars", 20)
86
        column = gtk.TreeViewColumn()
87
        column.pack_start(cell, expand=True)
88
        column.add_attribute(cell, "text", 0)
89
        self.treeview.append_column(column)
90
75 by Jelmer Vernooij
Use HPaned rather than HBox so long filenames can be viewed (fixes #56993).
91
        # The diffs of the  selected file: a scrollable source or
92
        # text view
10 by Scott James Remnant
Add an extra window type, clicking the little icons next to a parent
93
        scrollwin = gtk.ScrolledWindow()
94
        scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
95
        scrollwin.set_shadow_type(gtk.SHADOW_IN)
75 by Jelmer Vernooij
Use HPaned rather than HBox so long filenames can be viewed (fixes #56993).
96
        pane.pack2(scrollwin)
10 by Scott James Remnant
Add an extra window type, clicking the little icons next to a parent
97
        scrollwin.show()
98
99
        if have_gtksourceview:
100
            self.buffer = gtksourceview.SourceBuffer()
101
            slm = gtksourceview.SourceLanguagesManager()
102
            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.
103
            if have_gconf:
104
                self.apply_gedit_colors(gsl)
232.1.2 by Adeodato Simó
Rename apply_colordiffrc to apply_colordiff_colors, improve docstring.
105
            self.apply_colordiff_colors(gsl)
10 by Scott James Remnant
Add an extra window type, clicking the little icons next to a parent
106
            self.buffer.set_language(gsl)
107
            self.buffer.set_highlight(True)
108
109
            sourceview = gtksourceview.SourceView(self.buffer)
110
        else:
111
            self.buffer = gtk.TextBuffer()
112
            sourceview = gtk.TextView(self.buffer)
113
114
        sourceview.set_editable(False)
115
        sourceview.modify_font(pango.FontDescription("Monospace"))
116
        scrollwin.add(sourceview)
117
        sourceview.show()
118
51 by Jelmer Vernooij
Rework some of the parameters to DiffWindow.set_diff() to be
119
    def set_diff(self, description, rev_tree, parent_tree):
10 by Scott James Remnant
Add an extra window type, clicking the little icons next to a parent
120
        """Set the differences showed by this window.
121
122
        Compares the two trees and populates the window with the
123
        differences.
124
        """
51 by Jelmer Vernooij
Rework some of the parameters to DiffWindow.set_diff() to be
125
        self.rev_tree = rev_tree
126
        self.parent_tree = parent_tree
10 by Scott James Remnant
Add an extra window type, clicking the little icons next to a parent
127
128
        self.model.clear()
77 by Jelmer Vernooij
Prepare for 0.10.0
129
        delta = self.rev_tree.changes_from(self.parent_tree)
10 by Scott James Remnant
Add an extra window type, clicking the little icons next to a parent
130
11 by Scott James Remnant
Add a default "Complete Diff" option to the top of the diff window
131
        self.model.append(None, [ "Complete Diff", "" ])
132
10 by Scott James Remnant
Add an extra window type, clicking the little icons next to a parent
133
        if len(delta.added):
134
            titer = self.model.append(None, [ "Added", None ])
135
            for path, id, kind in delta.added:
136
                self.model.append(titer, [ path, path ])
137
138
        if len(delta.removed):
139
            titer = self.model.append(None, [ "Removed", None ])
140
            for path, id, kind in delta.removed:
141
                self.model.append(titer, [ path, path ])
142
143
        if len(delta.renamed):
144
            titer = self.model.append(None, [ "Renamed", None ])
145
            for oldpath, newpath, id, kind, text_modified, meta_modified \
146
                    in delta.renamed:
63 by Aaron Bentley
Accept either side of a rename for DiffWindow.set_file
147
                self.model.append(titer, [ oldpath, newpath ])
10 by Scott James Remnant
Add an extra window type, clicking the little icons next to a parent
148
149
        if len(delta.modified):
150
            titer = self.model.append(None, [ "Modified", None ])
151
            for path, id, kind, text_modified, meta_modified in delta.modified:
152
                self.model.append(titer, [ path, path ])
153
154
        self.treeview.expand_all()
51 by Jelmer Vernooij
Rework some of the parameters to DiffWindow.set_diff() to be
155
        self.set_title(description + " - bzrk diff")
10 by Scott James Remnant
Add an extra window type, clicking the little icons next to a parent
156
59.2.3 by Aaron Bentley
Gannotate-launched diffs now jump to correct file
157
    def set_file(self, file_path):
158
        tv_path = None
159
        for data in self.model:
160
            for child in data.iterchildren():
63 by Aaron Bentley
Accept either side of a rename for DiffWindow.set_file
161
                if child[0] == file_path or child[1] == file_path:
59.2.3 by Aaron Bentley
Gannotate-launched diffs now jump to correct file
162
                    tv_path = child.path
163
                    break
59.2.4 by Aaron Bentley
Teach gdiff to accept a single file argument
164
        if tv_path is None:
165
            raise NoSuchFile(file_path)
59.2.3 by Aaron Bentley
Gannotate-launched diffs now jump to correct file
166
        self.treeview.set_cursor(tv_path)
64 by Aaron Bentley
Scroll to the appropriate cell when file selected
167
        self.treeview.scroll_to_cell(tv_path)
59.2.3 by Aaron Bentley
Gannotate-launched diffs now jump to correct file
168
10 by Scott James Remnant
Add an extra window type, clicking the little icons next to a parent
169
    def _treeview_cursor_cb(self, *args):
170
        """Callback for when the treeview cursor changes."""
171
        (path, col) = self.treeview.get_cursor()
11 by Scott James Remnant
Add a default "Complete Diff" option to the top of the diff window
172
        specific_files = [ self.model[path][1] ]
173
        if specific_files == [ None ]:
10 by Scott James Remnant
Add an extra window type, clicking the little icons next to a parent
174
            return
11 by Scott James Remnant
Add a default "Complete Diff" option to the top of the diff window
175
        elif specific_files == [ "" ]:
176
            specific_files = []
10 by Scott James Remnant
Add an extra window type, clicking the little icons next to a parent
177
178
        s = StringIO()
11 by Scott James Remnant
Add a default "Complete Diff" option to the top of the diff window
179
        show_diff_trees(self.parent_tree, self.rev_tree, s, specific_files)
66.2.10 by Jelmer Vernooij
Merge some fixes from Alexander Belchenko
180
        self.buffer.set_text(s.getvalue().decode(sys.getdefaultencoding(), 'replace'))
232.1.1 by Adeodato Simó
Read ~/.colordiffrc to set colors in the diff window.
181
182
    @staticmethod
232.1.3 by Adeodato Simó
Support setting diff colors from gedit's syntax highlighting config too.
183
    def apply_gedit_colors(lang):
184
        """Set style for lang to that specified in gedit configuration.
185
186
        This method needs the gconf module.
187
        
188
        :param lang: a gtksourceview.SourceLanguage object.
189
        """
190
        GEDIT_SYNTAX_PATH = '/apps/gedit-2/preferences/syntax_highlighting'
191
        GEDIT_LANG_PATH = GEDIT_SYNTAX_PATH + '/' + lang.get_id()
192
193
        client = gconf.client_get_default()
194
        client.add_dir(GEDIT_LANG_PATH, gconf.CLIENT_PRELOAD_NONE)
195
196
        for tag in lang.get_tags():
197
            tag_id = tag.get_id()
198
            gconf_key = GEDIT_LANG_PATH + '/' + tag_id
199
            style_string = client.get_string(gconf_key)
200
201
            if style_string is None:
202
                continue
203
204
            # function to get a bool from a string that's either '0' or '1'
205
            string_bool = lambda x: bool(int(x))
206
207
            # style_string is a string like "2/#FFCCAA/#000000/0/1/0/0"
208
            # values are: mask, fg, bg, italic, bold, underline, strike
209
            # this packs them into (str_value, attr_name, conv_func) tuples
210
            items = zip(style_string.split('/'), ['mask', 'foreground',
211
                'background', 'italic', 'bold', 'underline', 'strikethrough' ],
212
                [ int, gtk.gdk.color_parse, gtk.gdk.color_parse, string_bool,
213
                    string_bool, string_bool, string_bool ]
214
            )
215
216
            style = gtksourceview.SourceTagStyle()
217
218
            # XXX The mask attribute controls whether the present values of
219
            # foreground and background color should in fact be used. Ideally
220
            # (and that's what gedit does), one could set all three attributes,
221
            # and let the TagStyle object figure out which colors to use.
222
            # However, in the GtkSourceview python bindings, the mask attribute
223
            # is read-only, and it's derived instead from the colors being
224
            # set or not. This means that we have to sometimes refrain from
225
            # setting fg or bg colors, depending on the value of the mask.
226
            # This code could go away if mask were writable.
227
            mask = int(items[0][0])
228
            if not (mask & 1): # GTK_SOURCE_TAG_STYLE_USE_BACKGROUND
229
                items[2:3] = []
230
            if not (mask & 2): # GTK_SOURCE_TAG_STYLE_USE_FOREGROUND
231
                items[1:2] = []
232
            items[0:1] = [] # skip the mask unconditionally
233
234
            for value, attr, func in items:
235
                try:
236
                    value = func(value)
237
                except ValueError:
238
                    warning('gconf key %s contains an invalid value: %s'
239
                            % gconf_key, value)
240
                else:
241
                    setattr(style, attr, value)
242
243
            lang.set_tag_style(tag_id, style)
244
245
    @staticmethod
232.1.2 by Adeodato Simó
Rename apply_colordiffrc to apply_colordiff_colors, improve docstring.
246
    def apply_colordiff_colors(lang):
247
        """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.
248
249
        Both ~/.colordiffrc and ~/.colordiffrc.bzr-gtk are read.
250
232.1.2 by Adeodato Simó
Rename apply_colordiffrc to apply_colordiff_colors, improve docstring.
251
        :param lang: a "Diff" gtksourceview.SourceLanguage object.
232.1.1 by Adeodato Simó
Read ~/.colordiffrc to set colors in the diff window.
252
        """
253
        colors = {}
254
255
        for f in ('~/.colordiffrc', '~/.colordiffrc.bzr-gtk'):
256
            f = os.path.expanduser(f)
257
            if os.path.exists(f):
258
                try:
259
                    f = file(f)
260
                except IOError, e:
261
                    warning('could not open file %s: %s' % (f, str(e)))
262
                else:
232.1.4 by Adeodato Simó
Add a test for parse_colordiffrc, and fix the function to handle empty lines.
263
                    colors.update(DiffWindow.parse_colordiffrc(f))
232.1.1 by Adeodato Simó
Read ~/.colordiffrc to set colors in the diff window.
264
                    f.close()
265
266
        if not colors:
267
            # ~/.colordiffrc does not exist
268
            return
269
270
        mapping = {
271
                # map GtkSourceView tags to colordiff names
272
                # since GSV is richer, accept new names for extra bits,
273
                # defaulting to old names if they're not present
274
                'Added@32@line': ['newtext'],
275
                'Removed@32@line': ['oldtext'],
276
                'Location': ['location', 'diffstuff'],
277
                'Diff@32@file': ['file', 'diffstuff'],
278
                'Special@32@case': ['specialcase', 'diffstuff'],
279
        }
280
281
        for tag in lang.get_tags():
282
            tag_id = tag.get_id()
283
            keys = mapping.get(tag_id, [])
284
            color = None
285
286
            for key in keys:
287
                color = colors.get(key, None)
288
                if color is not None:
289
                    break
290
291
            if color is None:
292
                continue
293
294
            style = gtksourceview.SourceTagStyle()
295
            try:
296
                style.foreground = gtk.gdk.color_parse(color)
297
            except ValueError:
298
                warning('not a valid color: %s' % color)
299
            else:
300
                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.
301
302
    @staticmethod
303
    def parse_colordiffrc(fileobj):
304
        """Parse fileobj as a colordiff configuration file.
305
        
306
        :return: A dict with the key -> value pairs.
307
        """
308
        colors = {}
309
        for line in fileobj:
310
            if re.match(r'^\s*#', line):
311
                continue
312
            if '=' not in line:
313
                continue
314
            key, val = line.split('=', 1)
315
            colors[key.strip()] = val.strip()
316
        return colors
317