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