[meld] Rework LinkMap as a separate gtk.DrawingArea subclass



commit 67972e85925f0247f284dbfbe899687adc8ba54c
Author: Kai Willadsen <kai willadsen gmail com>
Date:   Wed Mar 2 07:31:49 2011 +1000

    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

 data/ui/filediff.ui       |   24 +---
 meld/filediff.py          |  209 ++++-----------------------------
 meld/filemerge.py         |   51 --------
 meld/linkmap.py           |  286 +++++++++++++++++++++++++++++++++++++++++++++
 meld/ui/catalog.xml       |    2 +
 meld/ui/gladesupport.py   |    1 +
 meld/util/sourceviewer.py |   11 ++
 7 files changed, 327 insertions(+), 257 deletions(-)
---
diff --git a/data/ui/filediff.ui b/data/ui/filediff.ui
index f39d6fa..9619c4a 100644
--- a/data/ui/filediff.ui
+++ b/data/ui/filediff.ui
@@ -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>
diff --git a/meld/filediff.py b/meld/filediff.py
index 7f17695..a7ab8af 100644
--- a/meld/filediff.py
+++ b/meld/filediff.py
@@ -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])
diff --git a/meld/filemerge.py b/meld/filemerge.py
index aaf7113..4cf27df 100644
--- a/meld/filemerge.py
+++ b/meld/filemerge.py
@@ -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
diff --git a/meld/linkmap.py b/meld/linkmap.py
new file mode 100644
index 0000000..3eda2a6
--- /dev/null
+++ b/meld/linkmap.py
@@ -0,0 +1,286 @@
+### 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
+
diff --git a/meld/ui/catalog.xml b/meld/ui/catalog.xml
index 9f8f254..b932c11 100644
--- a/meld/ui/catalog.xml
+++ b/meld/ui/catalog.xml
@@ -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"/>
diff --git a/meld/ui/gladesupport.py b/meld/ui/gladesupport.py
index 6a14f3d..4afbf6d 100644
--- a/meld/ui/gladesupport.py
+++ b/meld/ui/gladesupport.py
@@ -1,5 +1,6 @@
 
 import historyentry
 import msgarea
+import meld.linkmap
 import meld.diffmap
 import meld.util.sourceviewer
diff --git a/meld/util/sourceviewer.py b/meld/util/sourceviewer.py
index fe7267b..a36c236 100644
--- a/meld/util/sourceviewer.py
+++ b/meld/util/sourceviewer.py
@@ -186,3 +186,14 @@ srcviewer = _get_srcviewer()
 
 class MeldSourceView(srcviewer.GtkTextView):
     __gtype_name__ = "MeldSourceView"
+
+    def get_y_for_line_num(self, line):
+        buf = self.get_buffer()
+        it = buf.get_iter_at_line(line)
+        y, h = self.get_line_yrange(it)
+        if line >= buf.get_line_count():
+            return y + h - 1
+        return y
+
+    def get_line_num_for_y(self, y):
+        return self.get_line_at_y(y)[0].get_line()



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]