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