Commit 67972e85 authored by Kai Willadsen's avatar Kai Willadsen

Rework LinkMap as a separate gtk.DrawingArea subclass

The LinkMap is the widget that draws correspondence lines between two
panes in our FileDiff views, and provides clickable areas for
performing actions on change blocks. This commit isolates the LinkMap
code into its own class and overrides signals as appropriate, rather
than dealing with all callbacks through FileDiff.

In addition, the new LinkMap widget has improved handling of
non-writable TextViews it is associated with, which should fulfil the
requirements associated with merge mode.

meld/linkmap.py: Add new LinkMap class, with most code moved and
                 adapted from the existing implementation in FileDiff.

meld/filediff.py: Remove LinkMap code and add FileDiff/LinkMap
                  integration bits. We also move to treating the
                  window-wide keymask as a property.

meld/filemerge.py: Remove LinkMap code; the new LinkMap should handle
                   merge mode correctly itself.

data/ui/filediff.ui: Adjust UI file to treat LinkMap as a real widget

meld/util/sourceviewer.py: Add some TextView-specific APIs to our proxy
                           widget for use in LinkMap; these APIs will
                           be used in FileDiff, but are not currently.

meld/ui/catalog.xml: Add Glade support for LinkMap
meld/ui/gladesupport.py: Add Glade support for LinkMap
parent 97dbd555
......@@ -241,18 +241,12 @@
</packing>
</child>
<child>
<object class="GtkDrawingArea" id="linkmap0">
<object class="LinkMap" id="linkmap0">
<property name="width_request">50</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="has_focus">True</property>
<property name="can_focus">False</property>
<property name="has_focus">False</property>
<property name="events">GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK</property>
<signal handler="on_linkmap_expose_event" name="expose_event"/>
<signal handler="on_linkmap_button_press_event" name="button_press_event"/>
<signal handler="on_key_press_event" name="key_press_event"/>
<signal handler="on_linkmap_button_release_event" name="button_release_event"/>
<signal handler="on_key_release_event" name="key_release_event"/>
<signal handler="on_linkmap_scroll_event" name="scroll_event"/>
</object>
<packing>
<property name="left_attach">2</property>
......@@ -274,18 +268,12 @@
</packing>
</child>
<child>
<object class="GtkDrawingArea" id="linkmap1">
<object class="LinkMap" id="linkmap1">
<property name="width_request">50</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="has_focus">True</property>
<property name="can_focus">False</property>
<property name="has_focus">False</property>
<property name="events">GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK</property>
<signal handler="on_linkmap_expose_event" name="expose_event"/>
<signal handler="on_linkmap_button_press_event" name="button_press_event"/>
<signal handler="on_key_press_event" name="key_press_event"/>
<signal handler="on_linkmap_button_release_event" name="button_release_event"/>
<signal handler="on_key_release_event" name="key_release_event"/>
<signal handler="on_linkmap_scroll_event" name="scroll_event"/>
</object>
<packing>
<property name="left_attach">4</property>
......
......@@ -139,6 +139,8 @@ class BufferLines(object):
MASK_SHIFT, MASK_CTRL = 1, 2
MODE_REPLACE, MODE_DELETE, MODE_INSERT = 0, 1, 2
def get_iter_at_line_or_eof(buffer, line):
if line >= buffer.get_line_count():
return buffer.get_end_iter()
......@@ -184,6 +186,7 @@ class FileDiff(melddoc.MeldDoc, gnomeglade.Component):
__gsignals__ = {
'next-conflict-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, (bool, bool)),
'action-mode-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, (int,)),
}
def __init__(self, prefs, num_panes):
......@@ -206,7 +209,7 @@ class FileDiff(melddoc.MeldDoc, gnomeglade.Component):
if self.prefs.show_whitespace:
v.set_draw_spaces(srcviewer.spaces_flag)
srcviewer.set_tab_width(v, self.prefs.tab_size)
self.keymask = 0
self._keymask = 0
self.load_font()
self.deleted_lines_pending = -1
self.textview_overwrite = 0
......@@ -296,7 +299,6 @@ class FileDiff(melddoc.MeldDoc, gnomeglade.Component):
gnomeglade.connect_signal_handlers(self)
self.findbar = findbar.FindBar()
self.filediff.pack_end(self.findbar.widget, False)
self.focus_before_click = None
self.cursor = CursorDetails()
self.connect("current-diff-changed", self.on_current_diff_changed)
for t in self.textview:
......@@ -306,9 +308,21 @@ class FileDiff(melddoc.MeldDoc, gnomeglade.Component):
self.undosequence.connect("checkpointed", self.on_undo_checkpointed)
self.connect("next-conflict-changed", self.on_next_conflict_changed)
def get_keymask(self):
return self._keymask
def set_keymask(self, value):
if value & MASK_SHIFT:
mode = MODE_DELETE
elif value & MASK_CTRL:
mode = MODE_INSERT
else:
mode = MODE_REPLACE
self._keymask = value
self.emit("action-mode-changed", mode)
keymask = property(get_keymask, set_keymask)
def on_focus_change(self):
self.keymask = 0
self._update_linkmap_buttons()
def on_container_switch_in_event(self, ui):
melddoc.MeldDoc.on_container_switch_in_event(self, ui)
......@@ -602,17 +616,6 @@ class FileDiff(melddoc.MeldDoc, gnomeglade.Component):
for i in range(2):
self.linkmap[i].queue_draw()
icon_theme = gtk.icon_theme_get_default()
load = lambda x: icon_theme.load_icon(x, self.pixels_per_line, 0)
self.pixbuf_apply0 = load("button_apply0")
self.pixbuf_apply1 = load("button_apply1")
self.pixbuf_delete = load("button_delete")
# FIXME: this is a somewhat bizarre action to take, but our non-square
# icons really make this kind of handling difficult
load = lambda x: icon_theme.load_icon(x, self.pixels_per_line * 2, 0)
self.pixbuf_copy0 = load("button_copy0")
self.pixbuf_copy1 = load("button_copy1")
def on_preference_changed(self, key, value):
if key == "tab_size":
tabs = pango.TabArray(10, 0)
......@@ -649,18 +652,10 @@ class FileDiff(melddoc.MeldDoc, gnomeglade.Component):
self.linediffer.ignore_blanks = self.prefs.ignore_blank_lines
self.set_files([None] * self.num_panes) # Refresh
def _update_linkmap_buttons(self):
for l in self.linkmap[:self.num_panes - 1]:
a = l.get_allocation()
w = self.pixbuf_copy0.get_width()
l.queue_draw_area(0, 0, w, a[3])
l.queue_draw_area(a[2]-w, 0, w, a[3])
def on_key_press_event(self, object, event):
x = self.keylookup.get(event.keyval, 0)
if self.keymask | x != self.keymask:
self.keymask |= x
self._update_linkmap_buttons()
elif event.keyval == gtk.keysyms.Escape:
self.findbar.hide()
......@@ -668,11 +663,9 @@ class FileDiff(melddoc.MeldDoc, gnomeglade.Component):
x = self.keylookup.get(event.keyval, 0)
if self.keymask & ~x != self.keymask:
self.keymask &= ~x
self._update_linkmap_buttons()
# Ugly workaround for bgo#584342
elif event.keyval == gtk.keysyms.ISO_Prev_Group:
self.keymask = 0
self._update_linkmap_buttons()
def _get_pane_label(self, i):
#TRANSLATORS: this is the name of a new file which has not yet been saved
......@@ -758,17 +751,13 @@ class FileDiff(melddoc.MeldDoc, gnomeglade.Component):
def on_find_activate(self, *args):
self.findbar.start_find( self.textview_focussed )
self.keymask = 0
self.queue_draw()
def on_replace_activate(self, *args):
self.findbar.start_replace( self.textview_focussed )
self.keymask = 0
self.queue_draw()
def on_find_next_activate(self, *args):
self.findbar.start_find_next( self.textview_focussed )
self.keymask = 0
self.queue_draw()
self.findbar.start_find_next(self.textview_focussed)
def on_filediff__key_press_event(self, entry, event):
if event.keyval == gtk.keysyms.Escape:
......@@ -1409,6 +1398,9 @@ class FileDiff(melddoc.MeldDoc, gnomeglade.Component):
scroll = self.scrolledwindow[i].get_vscrollbar()
w.setup(scroll, coords_iter(i), colour_map)
for (w, i) in zip(self.linkmap, (0, self.num_panes - 2)):
w.associate(self, self.textview[i], self.textview[i + 1])
for i in range(self.num_panes):
if self.bufferdata[i].modified:
self.statusimage[i].show()
......@@ -1451,165 +1443,6 @@ class FileDiff(melddoc.MeldDoc, gnomeglade.Component):
buf.place_cursor(buf.get_iter_at_line(c[1]))
self.textview[pane].scroll_to_mark(buf.get_insert(), 0.1)
def paint_pixbuf_at(self, context, pixbuf, x, y):
context.translate(x, y)
context.set_source_pixbuf(pixbuf, 0, 0)
context.paint()
context.identity_matrix()
def _linkmap_draw_icon(self, context, which, change, x, f0, t0):
if self.keymask & MASK_SHIFT:
pix0 = self.pixbuf_delete
pix1 = self.pixbuf_delete
elif self.keymask & MASK_CTRL and \
change[0] not in ('insert', 'delete'):
pix0 = self.pixbuf_copy0
pix1 = self.pixbuf_copy1
else: # self.keymask == 0:
pix0 = self.pixbuf_apply0
pix1 = self.pixbuf_apply1
if change[0] in ("insert", "replace") or (change[0] == "conflict" and
change[3] - change[4] != 0):
self.paint_pixbuf_at(context, pix1, x, t0)
if change[0] in ("delete", "replace") or (change[0] == "conflict" and
change[1] - change[2] != 0):
self.paint_pixbuf_at(context, pix0, 0, f0)
#
# linkmap drawing
#
def on_linkmap_expose_event(self, widget, event):
wtotal, htotal = widget.allocation.width, widget.allocation.height
yoffset = widget.allocation.y
context = widget.window.cairo_create()
context.rectangle(event.area.x, event.area.y, event.area.width, event.area.height)
context.clip()
context.set_line_width(1.0)
which = self.linkmap.index(widget)
pix_start = [t.get_visible_rect().y for t in self.textview]
rel_offset = [t.allocation.y - yoffset for t in self.textview]
def bounds(idx):
return [self._pixel_to_line(idx, pix_start[idx]), self._pixel_to_line(idx, pix_start[idx]+htotal)]
visible = [None] + bounds(which) + bounds(which+1)
# For bezier control points
x_steps = [-0.5, (1. / 3) * wtotal, (2. / 3) * wtotal, wtotal + 0.5]
for c in self.linediffer.pair_changes(which, which + 1, visible[1:5]):
# f and t are short for "from" and "to"
f0, f1 = [self._line_to_pixel(which, l) - pix_start[which] + rel_offset[which] for l in c[1:3]]
t0, t1 = [self._line_to_pixel(which + 1, l) - pix_start[which + 1] + rel_offset[which + 1] for l in c[3:5]]
context.move_to(x_steps[0], f0 - 0.5)
context.curve_to(x_steps[1], f0 - 0.5,
x_steps[2], t0 - 0.5,
x_steps[3], t0 - 0.5)
context.line_to(x_steps[3], t1 - 0.5)
context.curve_to(x_steps[2], t1 - 0.5,
x_steps[1], f1 - 0.5,
x_steps[0], f1 - 0.5)
context.close_path()
context.set_source_rgb(*self.fill_colors[c[0]])
context.fill_preserve()
if self.linediffer.locate_chunk(which, c[1])[0] == self.cursor.chunk:
context.set_source_rgba(1.0, 1.0, 1.0, 0.5)
context.fill_preserve()
context.set_source_rgb(*self.line_colors[c[0]])
context.stroke()
x = wtotal-self.pixbuf_apply0.get_width()
self._linkmap_draw_icon(context, which, c, x, f0, t0)
# allow for scrollbar at end of textview
mid = int(0.5 * self.textview[0].allocation.height) + 0.5
context.set_source_rgba(0., 0., 0., 0.5)
context.move_to(.35 * wtotal, mid)
context.line_to(.65 * wtotal, mid)
context.stroke()
def on_linkmap_scroll_event(self, area, event):
self.next_diff(event.direction)
def _linkmap_process_event(self, event, which, side, htotal, rect_x, pix_width, pix_height):
src = which + side
dst = which + 1 - side
yoffset = self.linkmap[which].allocation.y
rel_offset = self.textview[src].allocation.y - yoffset
adj = self.scrolledwindow[src].get_vadjustment()
for c in self.linediffer.pair_changes(src, dst):
if c[0] == "insert" or (c[0] == "conflict" and c[1] - c[2] == 0):
continue
h = self._line_to_pixel(src, c[1]) - adj.value + rel_offset
if h < 0: # find first visible chunk
continue
elif h > htotal: # we've gone past last visible
break
elif h < event.y and event.y < h + pix_height:
self.mouse_chunk = ((src, dst), (rect_x, h, pix_width, pix_height), c)
break
def on_linkmap_button_press_event(self, area, event):
if event.button == 1:
self.focus_before_click = None
for t in self.textview:
if t.is_focus():
self.focus_before_click = t
break
area.grab_focus()
self.mouse_chunk = None
wtotal, htotal = area.allocation.width, area.allocation.height
pix_width = self.pixbuf_apply0.get_width()
pix_height = self.pixbuf_apply0.get_height()
if self.keymask == MASK_CTRL: # hack
pix_height *= 2
which = self.linkmap.index(area)
# quick reject are we near the gutter?
if event.x < pix_width:
side = 0
rect_x = 0
elif event.x > wtotal - pix_width:
side = 1
rect_x = wtotal - pix_width
else:
return True
self._linkmap_process_event(event, which, side, htotal, rect_x, pix_width, pix_height)
#print self.mouse_chunk
return True
return False
def on_linkmap_button_release_event(self, area, event):
if event.button == 1:
if self.focus_before_click:
self.focus_before_click.grab_focus()
self.focus_before_click = None
if self.mouse_chunk:
(src,dst), rect, chunk = self.mouse_chunk
self.mouse_chunk = None
# check we're still in button
inrect = lambda p, r: (r[0] < p.x < r[0] + r[2]) and (r[1] < p.y < r[1] + r[3])
if inrect(event, rect):
# gtk tries to jump back to where the cursor was unless we move the cursor
self.textview[src].place_cursor_onscreen()
self.textview[dst].place_cursor_onscreen()
if self.keymask & MASK_SHIFT: # delete
self.delete_chunk(src, chunk)
elif self.keymask & MASK_CTRL: # copy up or down
copy_up = event.y - rect[1] < 0.5 * rect[3]
self.copy_chunk(src, dst, chunk, copy_up)
else: # replace
self.replace_chunk(src, dst, chunk)
return True
return False
def copy_chunk(self, src, dst, chunk, copy_up):
b0, b1 = self.textbuffer[src], self.textbuffer[dst]
start = get_iter_at_line_or_eof(b0, chunk[1])
......
......@@ -87,54 +87,3 @@ class FileMerge(filediff.FileDiff):
self.bufferdata[1].modified = 1
self.recompute_label()
yield 1
def _linkmap_draw_icon(self, context, which, change, x, f0, t0):
pix0 = self.pixbuf_delete
if which:
if self.keymask & MASK_CTRL:
pix1 = self.pixbuf_copy1
else:
pix1 = self.pixbuf_apply1
else:
if self.keymask & MASK_CTRL:
pix1 = self.pixbuf_copy0
else:
pix1 = self.pixbuf_apply0
if which:
if change[0] in ("delete"):
self.paint_pixbuf_at(context, pix0, 0, f0)
if change[0] in ("insert", "replace", "conflict"):
self.paint_pixbuf_at(context, pix1, x, t0)
else:
if change[0] in ("insert"):
self.paint_pixbuf_at(context, pix0, x, t0)
if change[0] in ("delete", "replace", "conflict"):
self.paint_pixbuf_at(context, pix1, 0, f0)
def _linkmap_process_event(self, event, which, side, htotal, rect_x, pix_width, pix_height):
origsrc = which + side
src = 2 * which
dst = 1
srcadj = self.scrolledwindow[src].get_vadjustment()
dstadj = self.scrolledwindow[dst].get_vadjustment()
yoffset = self.linkmap[which].allocation.y
dst_offset = self.textview[dst].allocation.y - yoffset
src_offset = self.textview[src].allocation.y - yoffset
for c in self.linediffer.pair_changes(src, dst):
if c[0] == "insert":
if origsrc != 1:
continue
h = self._line_to_pixel(dst, c[3]) - dstadj.value + dst_offset
else:
if origsrc == 1:
continue
h = self._line_to_pixel(src, c[1]) - srcadj.value + src_offset
if h < 0: # find first visible chunk
continue
elif h > htotal: # we've gone past last visible
break
elif h < event.y and event.y < h + pix_height:
self.mouse_chunk = ((src, dst), (rect_x, h, pix_width, pix_height), c)
break
### Copyright (C) 2002-2006 Stephen Kennedy <stevek@gnome.org>
### Copyright (C) 2009-2011 Kai Willadsen <kai.willadsen@gmail.com>
### This program is free software; you can redistribute it and/or modify
### it under the terms of the GNU General Public License as published by
### the Free Software Foundation; either version 2 of the License, or
### (at your option) any later version.
### This program is distributed in the hope that it will be useful,
### but WITHOUT ANY WARRANTY; without even the implied warranty of
### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
### GNU General Public License for more details.
### You should have received a copy of the GNU General Public License
### along with this program; if not, write to the Free Software
### Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
import gtk
import diffutil
# FIXME: import order issues
MODE_REPLACE, MODE_DELETE, MODE_INSERT = 0, 1, 2
class LinkMap(gtk.DrawingArea):
__gtype_name__ = "LinkMap"
__gsignals__ = {
'expose-event': 'override',
'scroll-event': 'override',
'button-press-event': 'override',
'button-release-event': 'override',
}
def __init__(self):
self.mode = MODE_REPLACE
def associate(self, filediff, left_view, right_view):
self.filediff = filediff
self.views = [left_view, right_view]
self.view_indices = [filediff.textview.index(t) for t in self.views]
self.fill_colors = filediff.fill_colors
self.line_colors = filediff.line_colors
pixels_per_line = filediff.pixels_per_line
icon_theme = gtk.icon_theme_get_default()
load = lambda x: icon_theme.load_icon(x, pixels_per_line, 0)
pixbuf_apply0 = load("button_apply0")
pixbuf_apply1 = load("button_apply1")
pixbuf_delete = load("button_delete")
# FIXME: this is a somewhat bizarre action to take, but our non-square
# icons really make this kind of handling difficult
load = lambda x: icon_theme.load_icon(x, pixels_per_line * 2, 0)
pixbuf_copy0 = load("button_copy0")
pixbuf_copy1 = load("button_copy1")
self.action_map_left = {
MODE_REPLACE: pixbuf_apply0,
MODE_DELETE: pixbuf_delete,
MODE_INSERT: pixbuf_copy0,
}
self.action_map_right = {
MODE_REPLACE: pixbuf_apply1,
MODE_DELETE: pixbuf_delete,
MODE_INSERT: pixbuf_copy1,
}
self.button_width = pixbuf_apply0.get_width()
self.button_height = pixbuf_apply0.get_height()
filediff.connect("action-mode-changed", self.on_container_mode_changed)
def on_container_mode_changed(self, container, mode):
# On mode change, set our local copy of the mode, and cancel any mouse
# actions in progress. Otherwise, if someone clicks, then releases
# Shift, then releases the button... what do we do?
self.mode = mode
self.mouse_chunk = None
x, y, width, height = self.allocation
pixbuf_width = self.button_width
self.queue_draw_area(0, 0, pixbuf_width, height)
self.queue_draw_area(width - pixbuf_width, 0, pixbuf_width, height)
def paint_pixbuf_at(self, context, pixbuf, x, y):
context.translate(x, y)
context.set_source_pixbuf(pixbuf, 0, 0)
context.paint()
context.identity_matrix()
def _classify_change_actions(self, change):
"""Classify possible actions for the given change
Returns a tuple containing actions that can be performed given the
content and context of the change. The tuple gives the actions for
the left and right sides of the LinkMap.
"""
left_editable, right_editable = [v.get_editable() for v in self.views]
if not left_editable and not right_editable:
return None, None
# Reclassify conflict changes, since we treat them the same as a
# normal two-way change as far as actions are concerned
change_type = change[0]
if change_type == "conflict":
if change[1] == change[2]:
change_type = "insert"
elif change[3] == change[4]:
change_type = "delete"
else:
change_type = "replace"
left_act, right_act = None, None
if change_type == "delete":
left_act = MODE_REPLACE
if self.mode == MODE_DELETE and left_editable:
left_act = MODE_DELETE
elif change_type == "insert":
right_act = MODE_REPLACE
if self.mode == MODE_DELETE and right_editable:
right_act = MODE_DELETE
elif change_type == "replace":
if not left_editable:
left_act, right_act = MODE_REPLACE, MODE_DELETE
if self.mode == MODE_INSERT:
left_act = MODE_INSERT
elif not right_editable:
left_act, right_act = MODE_DELETE, MODE_REPLACE
if self.mode == MODE_INSERT:
right_act = MODE_INSERT
else:
left_act, right_act = MODE_REPLACE, MODE_REPLACE
if self.mode == MODE_DELETE:
left_act, right_act = MODE_DELETE, MODE_DELETE
elif self.mode == MODE_INSERT:
left_act, right_act = MODE_INSERT, MODE_INSERT
return left_act, right_act
def _linkmap_draw_icon(self, context, change, x, f0, t0):
left_act, right_act = self._classify_change_actions(change)
if left_act is not None:
pix0 = self.action_map_left[left_act]
self.paint_pixbuf_at(context, pix0, 0, f0)
if right_act is not None:
pix1 = self.action_map_right[right_act]
self.paint_pixbuf_at(context, pix1, x, t0)
def do_expose_event(self, event):
context = self.window.cairo_create()
context.rectangle(event.area.x, event.area.y, event.area.width, \
event.area.height)
context.clip()
context.set_line_width(1.0)
pix_start = [t.get_visible_rect().y for t in self.views]
rel_offset = [t.allocation.y - self.allocation.y for t in self.views]
height = self.allocation.height
visible = [self.views[0].get_line_num_for_y(pix_start[0]),
self.views[0].get_line_num_for_y(pix_start[0] + height),
self.views[1].get_line_num_for_y(pix_start[1]),
self.views[1].get_line_num_for_y(pix_start[1] + height)]
wtotal = self.allocation.width
# For bezier control points
x_steps = [-0.5, (1. / 3) * wtotal, (2. / 3) * wtotal, wtotal + 0.5]
left, right = self.view_indices
view_offset_line = lambda v, l: self.views[v].get_y_for_line_num(l) - \
pix_start[v] + rel_offset[v]
for c in self.filediff.linediffer.pair_changes(left, right, visible):
# f and t are short for "from" and "to"
f0, f1 = [view_offset_line(0, l) for l in c[1:3]]
t0, t1 = [view_offset_line(1, l) for l in c[3:5]]
context.move_to(x_steps[0], f0 - 0.5)
context.curve_to(x_steps[1], f0 - 0.5,
x_steps[2], t0 - 0.5,
x_steps[3], t0 - 0.5)
context.line_to(x_steps[3], t1 - 0.5)
context.curve_to(x_steps[2], t1 - 0.5,
x_steps[1], f1 - 0.5,
x_steps[0], f1 - 0.5)
context.close_path()
context.set_source_rgb(*self.fill_colors[c[0]])
context.fill_preserve()
chunk_idx = self.filediff.linediffer.locate_chunk(left, c[1])[0]
if chunk_idx == self.filediff.cursor.chunk:
context.set_source_rgba(1.0, 1.0, 1.0, 0.5)
context.fill_preserve()
context.set_source_rgb(*self.line_colors[c[0]])
context.stroke()
x = wtotal - self.button_width
self._linkmap_draw_icon(context, c, x, f0, t0)
# allow for scrollbar at end of textview
mid = int(0.5 * self.views[0].allocation.height) + 0.5
context.set_source_rgba(0., 0., 0., 0.5)
context.move_to(.35 * wtotal, mid)
context.line_to(.65 * wtotal, mid)
context.stroke()
def do_scroll_event(self, event):
self.filediff.next_diff(event.direction)
def _linkmap_process_event(self, event, side, x, pix_width, pix_height):
src_idx, dst_idx = side, 1 if side == 0 else 0
src, dst = self.view_indices[src_idx], self.view_indices[dst_idx]
yoffset = self.allocation.y
rel_offset = self.views[side].allocation.y - yoffset
vis_offset = self.views[side].get_visible_rect().y
bounds = []
for v in (self.views[src_idx], self.views[dst_idx]):
visible = v.get_visible_rect()
bounds.append(v.get_line_num_for_y(visible.y))
bounds.append(v.get_line_num_for_y(visible.y + visible.height))
for c in self.filediff.linediffer.pair_changes(src, dst, bounds):
h = self.views[src_idx].get_y_for_line_num(c[1]) - \
vis_offset + rel_offset
if h < event.y < h + pix_height:
# _classify_change_actions assumes changes are left->right
action_change = diffutil.reverse_chunk(c) if dst < src else c
actions = self._classify_change_actions(action_change)
if actions[side] is not None:
rect = gtk.gdk.Rectangle(x, h, pix_width, pix_height)
self.mouse_chunk = ((src, dst), rect, c, actions[side])
break
def do_button_press_event(self, event):
if event.button == 1:
self.mouse_chunk = None
pix_width = self.button_width
pix_height = self.button_height
# Hack to deal with our non-square insert-mode icons
if self.mode == MODE_INSERT:
pix_height *= 2
# Quick reject if not in the area used to draw our buttons
right_gutter_x = self.allocation.width - pix_width
if event.x >= pix_width and event.x <= right_gutter_x:
return True
# side = 0 means left side of linkmap, so action from left -> right
side = 0 if event.x < pix_width else 1
x = 0 if event.x < pix_width else right_gutter_x
self._linkmap_process_event(event, side, x, pix_width, pix_height)
return True
return False
def do_button_release_event(self, event):
if event.button == 1:
if self.mouse_chunk:
(src, dst), rect, chunk, action = self.mouse_chunk
self.mouse_chunk = None
# Check that we're still in the same button we started in
if rect.x <= event.x < rect.x + rect.width and \
rect.y <= event.y < rect.y + rect.height:
# Unless we move the cursor, the view scrolls back to
# its old position
self.views[0].place_cursor_onscreen()
self.views[1].place_cursor_onscreen()
if action == MODE_DELETE:
self.filediff.delete_chunk(src, chunk)
elif action == MODE_INSERT:
copy_up = event.y - rect[1] < 0.5 * rect[3]
self.filediff.copy_chunk(src, dst, chunk, copy_up)
else:
self.filediff.replace_chunk(src, dst, chunk)
return True
return False
......@@ -4,6 +4,7 @@
<glade-widget-classes>
<glade-widget-class title="DiffMap" name="DiffMap" generic-name="diffmap"/>
<glade-widget-class title="LinkMap" name="LinkMap" generic-name="linkmap"/>
<glade-widget-class title="HistoryEntry" name="HistoryEntry" generic-name="historyentry"/>
<glade-widget-class title="HistoryFileEntry" name="HistoryFileEntry" generic-name="historyfileentry"/>
<glade-widget-class title="MsgArea" name="MsgArea" generic-name="msgarea"/>
......@@ -12,6 +13,7 @@
<glade-widget-group name="meld" title="Meld">
<glade-widget-class-ref name="DiffMap"/>
<glade-widget-class-ref name="LinkMap"/>
<glade-widget-class-ref name="HistoryEntry"/>
<glade-widget-class-ref name="HistoryFileEntry"/>
<glade-widget-class-ref name="MsgArea"/>
......
import historyentry
import msgarea
import meld.linkmap
import meld.diffmap
import meld.util.sourceviewer
......@@ -186,3 +186,14 @@ srcviewer = _get_srcviewer()
class MeldSourceView(srcviewer.GtkTextView):