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