7
7
__copyright__ = "Copyright 2005 Canonical Ltd."
8
__author__ = "Scott James Remnant <scott@ubuntu.com>"
8
__author__ = "Scott James Remnant <scott@ubuntu.com>"
11
11
from cStringIO import StringIO
21
from xml.etree.ElementTree import Element, SubElement, tostring
23
from elementtree.ElementTree import Element, SubElement, tostring
16
from gi.repository import Gtk
17
from gi.repository import Pango
19
from gi.repository import GtkSource
20
27
have_gtksourceview = True
21
28
except ImportError:
22
29
have_gtksourceview = False
24
36
from bzrlib import (
26
37
merge as _mod_merge,
31
from bzrlib.diff import show_diff_trees
43
from bzrlib.diff import show_diff_trees, internal_diff
44
from bzrlib.errors import NoSuchFile
32
45
from bzrlib.patches import parse_patches
33
from bzrlib.plugins.gtk.dialog import (
38
from bzrlib.plugins.gtk.i18n import _i18n
46
from bzrlib.trace import warning
47
from bzrlib.plugins.gtk import _i18n
39
48
from bzrlib.plugins.gtk.window import Window
49
from dialog import error_dialog, info_dialog, warning_dialog
42
52
def fallback_guess_language(slm, content_type):
55
class DiffFileView(Gtk.ScrolledWindow):
65
class DiffFileView(gtk.ScrolledWindow):
56
66
"""Window for displaying diffs from a diff file"""
60
68
def __init__(self):
61
super(DiffFileView, self).__init__()
69
gtk.ScrolledWindow.__init__(self)
65
73
def construct(self):
66
self.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
67
self.set_shadow_type(Gtk.ShadowType.IN)
74
self.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
75
self.set_shadow_type(gtk.SHADOW_IN)
69
77
if have_gtksourceview:
70
self.buffer = GtkSource.Buffer()
71
lang_manager = GtkSource.LanguageManager.get_default()
72
language = lang_manager.guess_language(None, "text/x-patch")
73
self.buffer.set_language(language)
78
self.buffer = gtksourceview2.Buffer()
79
slm = gtksourceview2.LanguageManager()
80
guess_language = getattr(gtksourceview2.LanguageManager,
81
"guess_language", fallback_guess_language)
82
gsl = guess_language(slm, content_type="text/x-patch")
84
self.apply_gedit_colors(self.buffer)
85
self.apply_colordiff_colors(self.buffer)
86
self.buffer.set_language(gsl)
74
87
self.buffer.set_highlight_syntax(True)
75
self.sourceview = GtkSource.View(buffer=self.buffer)
89
self.sourceview = gtksourceview2.View(self.buffer)
77
self.buffer = Gtk.TextBuffer()
78
self.sourceview = Gtk.TextView(self.buffer)
91
self.buffer = gtk.TextBuffer()
92
self.sourceview = gtk.TextView(self.buffer)
80
94
self.sourceview.set_editable(False)
81
self.sourceview.override_font(Pango.FontDescription("Monospace"))
95
self.sourceview.modify_font(pango.FontDescription("Monospace"))
82
96
self.add(self.sourceview)
84
self.sourceview.show()
97
self.sourceview.show()
100
def apply_gedit_colors(buf):
101
"""Set style to that specified in gedit configuration.
103
This method needs the gconf module.
105
:param buf: a gtksourceview2.Buffer object.
107
GEDIT_SCHEME_PATH = '/apps/gedit-2/preferences/editor/colors/scheme'
109
client = gconf.client_get_default()
110
style_scheme_name = client.get_string(GEDIT_SCHEME_PATH)
111
if style_scheme_name is not None:
112
style_scheme = gtksourceview2.StyleSchemeManager().get_scheme(style_scheme_name)
114
buf.set_style_scheme(style_scheme)
117
def apply_colordiff_colors(klass, buf):
118
"""Set style colors for lang using the colordiff configuration file.
120
Both ~/.colordiffrc and ~/.colordiffrc.bzr-gtk are read.
122
:param buf: a "Diff" gtksourceview2.Buffer object.
124
scheme_manager = gtksourceview2.StyleSchemeManager()
125
style_scheme = scheme_manager.get_scheme('colordiff')
127
# if style scheme not found, we'll generate it from colordiffrc
128
# TODO: reload if colordiffrc has changed.
129
if style_scheme is None:
132
for f in ('~/.colordiffrc', '~/.colordiffrc.bzr-gtk'):
133
f = os.path.expanduser(f)
134
if os.path.exists(f):
138
warning('could not open file %s: %s' % (f, str(e)))
140
colors.update(klass.parse_colordiffrc(f))
144
# ~/.colordiffrc does not exist
148
# map GtkSourceView2 scheme styles to colordiff names
149
# since GSV is richer, accept new names for extra bits,
150
# defaulting to old names if they're not present
151
'diff:added-line': ['newtext'],
152
'diff:removed-line': ['oldtext'],
153
'diff:location': ['location', 'diffstuff'],
154
'diff:file': ['file', 'diffstuff'],
155
'diff:special-case': ['specialcase', 'diffstuff'],
158
converted_colors = {}
159
for name, values in mapping.items():
162
color = colors.get(value, None)
163
if color is not None:
167
converted_colors[name] = color
169
# some xml magic to produce needed style scheme description
170
e_style_scheme = Element('style-scheme')
171
e_style_scheme.set('id', 'colordiff')
172
e_style_scheme.set('_name', 'ColorDiff')
173
e_style_scheme.set('version', '1.0')
174
for name, color in converted_colors.items():
175
style = SubElement(e_style_scheme, 'style')
176
style.set('name', name)
177
style.set('foreground', '#%s' % color)
179
scheme_xml = tostring(e_style_scheme, 'UTF-8')
180
if not os.path.exists(os.path.expanduser('~/.local/share/gtksourceview-2.0/styles')):
181
os.makedirs(os.path.expanduser('~/.local/share/gtksourceview-2.0/styles'))
182
file(os.path.expanduser('~/.local/share/gtksourceview-2.0/styles/colordiff.xml'), 'w').write(scheme_xml)
184
scheme_manager.force_rescan()
185
style_scheme = scheme_manager.get_scheme('colordiff')
187
buf.set_style_scheme(style_scheme)
190
def parse_colordiffrc(fileobj):
191
"""Parse fileobj as a colordiff configuration file.
193
:return: A dict with the key -> value pairs.
197
if re.match(r'^\s*#', line):
201
key, val = line.split('=', 1)
202
colors[key.strip()] = val.strip()
86
205
def set_trees(self, rev_tree, parent_tree):
87
206
self.rev_tree = rev_tree
92
211
# self.parent_tree.lock_read()
93
212
# self.rev_tree.lock_read()
95
# self.delta = iter_changes_to_status(
96
# self.parent_tree, self.rev_tree)
214
# self.delta = iter_changes_to_status(self.parent_tree, self.rev_tree)
97
215
# self.path_to_status = {}
98
216
# self.path_to_diff = {}
99
217
# source_inv = self.parent_tree.inventory
100
218
# target_inv = self.rev_tree.inventory
101
219
# for (file_id, real_path, change_type, display_path) in self.delta:
102
# self.path_to_status[real_path] = u'=== %s %s' % (
103
# change_type, display_path)
220
# self.path_to_status[real_path] = u'=== %s %s' % (change_type, display_path)
104
221
# if change_type in ('modified', 'renamed and modified'):
105
222
# source_ie = source_inv[file_id]
106
223
# target_ie = target_inv[file_id]
158
275
self.buffer.set_text(decoded.encode('UTF-8'))
161
class DiffWidget(Gtk.HPaned):
278
class DiffWidget(gtk.HPaned):
168
282
def __init__(self):
169
283
super(DiffWidget, self).__init__()
171
285
# The file hierarchy: a scrollable treeview
172
scrollwin = Gtk.ScrolledWindow()
173
scrollwin.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
174
scrollwin.set_shadow_type(Gtk.ShadowType.IN)
286
scrollwin = gtk.ScrolledWindow()
287
scrollwin.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
288
scrollwin.set_shadow_type(gtk.SHADOW_IN)
175
289
self.pack1(scrollwin)
176
if self.SHOW_WIDGETS:
179
self.model = Gtk.TreeStore(str, str)
180
self.treeview = Gtk.TreeView(model=self.model)
292
self.model = gtk.TreeStore(str, str)
293
self.treeview = gtk.TreeView(self.model)
181
294
self.treeview.set_headers_visible(False)
182
295
self.treeview.set_search_column(1)
183
296
self.treeview.connect("cursor-changed", self._treeview_cursor_cb)
184
297
scrollwin.add(self.treeview)
185
if self.SHOW_WIDGETS:
188
cell = Gtk.CellRendererText()
300
cell = gtk.CellRendererText()
189
301
cell.set_property("width-chars", 20)
190
column = Gtk.TreeViewColumn()
191
column.pack_start(cell, True)
302
column = gtk.TreeViewColumn()
303
column.pack_start(cell, expand=True)
192
304
column.add_attribute(cell, "text", 0)
193
305
self.treeview.append_column(column)
231
341
self.model.clear()
232
342
delta = self.rev_tree.changes_from(self.parent_tree)
234
self.model.append(None, ["Complete Diff", ""])
344
self.model.append(None, [ "Complete Diff", "" ])
236
346
if len(delta.added):
237
titer = self.model.append(None, ["Added", None])
347
titer = self.model.append(None, [ "Added", None ])
238
348
for path, id, kind in delta.added:
239
self.model.append(titer, [path, path])
349
self.model.append(titer, [ path, path ])
241
351
if len(delta.removed):
242
titer = self.model.append(None, ["Removed", None])
352
titer = self.model.append(None, [ "Removed", None ])
243
353
for path, id, kind in delta.removed:
244
self.model.append(titer, [path, path])
354
self.model.append(titer, [ path, path ])
246
356
if len(delta.renamed):
247
titer = self.model.append(None, ["Renamed", None])
357
titer = self.model.append(None, [ "Renamed", None ])
248
358
for oldpath, newpath, id, kind, text_modified, meta_modified \
249
359
in delta.renamed:
250
self.model.append(titer, [oldpath, newpath])
360
self.model.append(titer, [ oldpath, newpath ])
252
362
if len(delta.modified):
253
titer = self.model.append(None, ["Modified", None])
363
titer = self.model.append(None, [ "Modified", None ])
254
364
for path, id, kind, text_modified, meta_modified in delta.modified:
255
self.model.append(titer, [path, path])
365
self.model.append(titer, [ path, path ])
257
367
self.treeview.expand_all()
258
368
self.diff_view.show_diff(None)
266
376
tv_path = child.path
268
378
if tv_path is None:
269
raise errors.NoSuchFile(file_path)
270
self.treeview.set_cursor(tv_path, None, False)
379
raise NoSuchFile(file_path)
380
self.treeview.set_cursor(tv_path)
271
381
self.treeview.scroll_to_cell(tv_path)
273
383
def _treeview_cursor_cb(self, *args):
274
384
"""Callback for when the treeview cursor changes."""
275
385
(path, col) = self.treeview.get_cursor()
278
specific_files = [self.model[path][1]]
279
if specific_files == [None]:
281
elif specific_files == [""]:
386
specific_files = [ self.model[path][1] ]
387
if specific_files == [ None ]:
389
elif specific_files == [ "" ]:
282
390
specific_files = None
284
392
self.diff_view.show_diff(specific_files)
286
394
def _on_wraplines_toggled(self, widget=None, wrap=False):
287
395
"""Callback for when the wrap lines checkbutton is toggled"""
288
396
if wrap or widget.get_active():
289
self.diff_view.sourceview.set_wrap_mode(Gtk.WrapMode.WORD)
397
self.diff_view.sourceview.set_wrap_mode(gtk.WRAP_WORD)
291
self.diff_view.sourceview.set_wrap_mode(Gtk.WrapMode.NONE)
399
self.diff_view.sourceview.set_wrap_mode(gtk.WRAP_NONE)
294
401
class DiffWindow(Window):
316
421
def construct(self, operations):
317
422
"""Construct the window contents."""
318
self.vbox = Gtk.VBox()
423
self.vbox = gtk.VBox()
319
424
self.add(self.vbox)
320
if self.SHOW_WIDGETS:
322
426
self.diff = DiffWidget()
323
427
self.vbox.pack_end(self.diff, True, True, 0)
324
if self.SHOW_WIDGETS:
326
429
# Build after DiffWidget to connect signals
327
430
menubar = self._get_menu_bar()
328
431
self.vbox.pack_start(menubar, False, False, 0)
329
432
hbox = self._get_button_bar(operations)
330
433
if hbox is not None:
331
434
self.vbox.pack_start(hbox, False, True, 0)
333
437
def _get_menu_bar(self):
334
menubar = Gtk.MenuBar()
438
menubar = gtk.MenuBar()
336
mb_view = Gtk.MenuItem.new_with_mnemonic(_i18n("_View"))
337
mb_view_menu = Gtk.Menu()
338
mb_view_wrapsource = Gtk.CheckMenuItem.new_with_mnemonic(
339
_i18n("Wrap _Long Lines"))
440
mb_view = gtk.MenuItem(_i18n("_View"))
441
mb_view_menu = gtk.Menu()
442
mb_view_wrapsource = gtk.CheckMenuItem(_i18n("Wrap _Long Lines"))
340
443
mb_view_wrapsource.connect('activate', self.diff._on_wraplines_toggled)
444
mb_view_wrapsource.show()
341
445
mb_view_menu.append(mb_view_wrapsource)
342
447
mb_view.set_submenu(mb_view_menu)
343
449
menubar.append(mb_view)
344
if self.SHOW_WIDGETS:
348
453
def _get_button_bar(self, operations):
349
454
"""Return a button bar to use.
353
458
if operations is None:
355
hbox = Gtk.HButtonBox()
356
hbox.set_layout(Gtk.ButtonBoxStyle.START)
460
hbox = gtk.HButtonBox()
461
hbox.set_layout(gtk.BUTTONBOX_START)
357
462
for title, method in operations:
358
merge_button = Gtk.Button(title)
359
if self.SHOW_WIDGETS:
361
merge_button.set_relief(Gtk.ReliefStyle.NONE)
463
merge_button = gtk.Button(title)
465
merge_button.set_relief(gtk.RELIEF_NONE)
362
466
merge_button.connect("clicked", method)
363
hbox.pack_start(merge_button, False, True, 0)
364
if self.SHOW_WIDGETS:
467
hbox.pack_start(merge_button, expand=False, fill=True)
368
471
def _get_merge_target(self):
369
d = Gtk.FileChooserDialog('Merge branch', self,
370
Gtk.FileChooserAction.SELECT_FOLDER,
371
buttons=(Gtk.STOCK_OK, Gtk.ResponseType.OK,
373
Gtk.ResponseType.CANCEL,))
472
d = gtk.FileChooserDialog('Merge branch', self,
473
gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER,
474
buttons=(gtk.STOCK_OK, gtk.RESPONSE_OK,
476
gtk.RESPONSE_CANCEL,))
376
if result != Gtk.ResponseType.OK:
479
if result != gtk.RESPONSE_OK:
377
480
raise SelectCancelled()
378
481
return d.get_current_folder_uri()
393
496
error_dialog('Error', str(e))
395
498
def _get_save_path(self, basename):
396
d = Gtk.FileChooserDialog('Save As', self,
397
Gtk.FileChooserAction.SAVE,
398
buttons=(Gtk.STOCK_OK, Gtk.ResponseType.OK,
400
Gtk.ResponseType.CANCEL,))
499
d = gtk.FileChooserDialog('Save As', self,
500
gtk.FILE_CHOOSER_ACTION_SAVE,
501
buttons=(gtk.STOCK_OK, gtk.RESPONSE_OK,
503
gtk.RESPONSE_CANCEL,))
401
504
d.set_current_name(basename)
404
if result != Gtk.ResponseType.OK:
507
if result != gtk.RESPONSE_OK:
405
508
raise SelectCancelled()
406
509
return urlutils.local_path_from_url(d.get_uri())