[meld] Move chunk actions from Linkmap to GtkSource.Gutter



commit fb70fb9909fc504e7730bdd126c4843718e0b1b0
Author: Kai Willadsen <kai willadsen gmail com>
Date:   Wed Oct 9 06:10:12 2013 +1000

    Move chunk actions from Linkmap to GtkSource.Gutter
    
    Drawing and handling these 'buttons' on the Linkmap has always been
    awkward, and it's a lot of complexity in a single widget. By breaking
    the actions out into a GutterRenderer, this should be more pleasant.

 data/icons/button_copy.xpm  |   21 +++++
 data/icons/button_copy0.xpm |   32 -------
 data/icons/button_copy1.xpm |   32 -------
 meld/filediff.py            |   20 ++++
 meld/gutterrendererchunk.py |  177 +++++++++++++++++++++++++++++++++++++
 meld/linkmap.py             |  204 -------------------------------------------
 6 files changed, 218 insertions(+), 268 deletions(-)
---
diff --git a/data/icons/button_copy.xpm b/data/icons/button_copy.xpm
new file mode 100644
index 0000000..8654e6c
--- /dev/null
+++ b/data/icons/button_copy.xpm
@@ -0,0 +1,21 @@
+/* XPM */
+static char * button_copy0_xpm[] = {
+"16 16 2 1",
+"      c None",
+".     c #000000",
+"                ",
+"      ....      ",
+"      ....      ",
+"      ....      ",
+"      ....      ",
+"      ....      ",
+" .............. ",
+" .............. ",
+" .............. ",
+" .............. ",
+"      ....      ",
+"      ....      ",
+"      ....      ",
+"      ....      ",
+"      ....      ",
+"                "};
diff --git a/meld/filediff.py b/meld/filediff.py
index d6cc163..da2511d 100644
--- a/meld/filediff.py
+++ b/meld/filediff.py
@@ -324,6 +324,26 @@ class FileDiff(melddoc.MeldDoc, gnomeglade.Component):
         cursor_label.show()
         self.status_info_labels = [overwrite_label, cursor_label]
 
+        # Prototype implementation
+
+        from gutterrendererchunk import GutterRendererChunkAction
+
+        for pane, t in enumerate(self.textview):
+            # FIXME: set_num_panes will break this good
+            if pane == 0 or (pane == 1 and self.num_panes == 3):
+                window = Gtk.TextWindowType.RIGHT
+                views = [self.textview[pane], self.textview[pane + 1]]
+                renderer = GutterRendererChunkAction(pane, pane + 1, views, self, self.linediffer)
+                gutter = t.get_gutter(window)
+                gutter.insert(renderer, 10)
+            if pane in (1, 2):
+                window = Gtk.TextWindowType.LEFT
+                views = [self.textview[pane], self.textview[pane - 1]]
+                renderer = GutterRendererChunkAction(pane, pane - 1, views, self, self.linediffer)
+                gutter = t.get_gutter(window)
+                gutter.insert(renderer, 10)
+
+
     def get_keymask(self):
         return self._keymask
     def set_keymask(self, value):
diff --git a/meld/gutterrendererchunk.py b/meld/gutterrendererchunk.py
new file mode 100644
index 0000000..1e988c0
--- /dev/null
+++ b/meld/gutterrendererchunk.py
@@ -0,0 +1,177 @@
+# Copyright (C) 2013 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., 51
+# Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+
+from gettext import gettext as _
+
+from gi.repository import Gtk
+from gi.repository import GtkSource
+
+
+# FIXME: This is obviously beyond horrible
+line_height = 16
+icon_theme = Gtk.IconTheme.get_default()
+load = lambda x: icon_theme.load_icon(x, line_height, 0)
+pixbuf_apply0 = load("button_apply0")
+pixbuf_apply1 = load("button_apply1")
+pixbuf_delete = load("button_delete")
+pixbuf_copy = load("button_copy")
+
+# FIXME: import order issues
+MODE_REPLACE, MODE_DELETE, MODE_INSERT = 0, 1, 2
+ACTION_MAP = {
+    'LTR': {
+        MODE_REPLACE: pixbuf_apply0,
+        MODE_DELETE: pixbuf_delete,
+        MODE_INSERT: pixbuf_copy,
+    },
+    'RTL': {
+        MODE_REPLACE: pixbuf_apply1,
+        MODE_DELETE: pixbuf_delete,
+        MODE_INSERT: pixbuf_copy,
+    }
+}
+
+
+class GutterRendererChunkAction(GtkSource.GutterRendererPixbuf):
+    __gtype_name__ = "GutterRendererChunkAction"
+
+    def __init__(self, from_pane, to_pane, views, filediff, linediffer):
+        super(GutterRendererChunkAction, self).__init__()
+        self.from_pane = from_pane
+        self.to_pane = to_pane
+        # FIXME: Views are needed only for editable checking; connect to this
+        # in Filediff instead?
+        self.views = views
+        # FIXME: Don't pass in the linediffer; pass a generator like elsewhere
+        self.linediffer = linediffer
+        self.mode = MODE_REPLACE
+        self.set_size(line_height)
+        direction = 'LTR' if from_pane < to_pane else 'RTL'
+        self.action_map = ACTION_MAP[direction]
+        self.filediff = filediff
+        self.filediff.connect("action-mode-changed",
+                              self.on_container_mode_changed)
+
+    def do_activate(self, start, area, event):
+        line = start.get_line()
+        chunk_index = self.linediffer.locate_chunk(self.from_pane, line)[0]
+        if chunk_index is None:
+            return
+
+        # FIXME: This is all chunks, not just those shared with to_pane
+        chunk = self.linediffer.get_chunk(chunk_index, self.from_pane)
+        if chunk[1] != line:
+            return
+
+        action = self._classify_change_actions(chunk)
+        if action == MODE_DELETE:
+            self.filediff.delete_chunk(self.from_pane, chunk)
+        elif action == MODE_INSERT:
+            copy_menu = self._make_copy_menu(chunk)
+            # TODO: Need a custom GtkMenuPositionFunc to position this next to
+            # the clicked gutter, not where the cursor is
+            copy_menu.popup(None, None, None, None, 0, event.time)
+        else:
+            self.filediff.replace_chunk(self.from_pane, self.to_pane, chunk)
+
+    def _make_copy_menu(self, chunk):
+        copy_menu = Gtk.Menu()
+        copy_up = Gtk.MenuItem.new_with_mnemonic(_("Copy _up"))
+        copy_down = Gtk.MenuItem.new_with_mnemonic(_("Copy _down"))
+        copy_menu.append(copy_up)
+        copy_menu.append(copy_down)
+        copy_menu.show_all()
+
+        # FIXME: This is horrible
+        widget = self.filediff.widget
+        copy_menu.attach_to_widget(widget, None)
+
+        def copy_chunk(widget, chunk, copy_up):
+            self.filediff.copy_chunk(self.from_pane, self.to_pane, chunk,
+                                     copy_up)
+
+        copy_up.connect('activate', copy_chunk, chunk, True)
+        copy_down.connect('activate', copy_chunk, chunk, False)
+        return copy_menu
+
+    def do_query_activatable(self, start, area, event):
+        line = start.get_line()
+        chunk_index = self.linediffer.locate_chunk(self.from_pane, line)[0]
+        if chunk_index is not None:
+            # FIXME: This is all chunks, not just those shared with to_pane
+            chunk = self.linediffer.get_chunk(chunk_index, self.from_pane)
+            if chunk[1] == line:
+                return True
+        return False
+
+    def do_query_data(self, start, end, state):
+        line = start.get_line()
+        chunk_index = self.linediffer.locate_chunk(self.from_pane, line)[0]
+
+        pixbuf = None
+        if chunk_index is not None:
+            chunk = self.linediffer.get_chunk(chunk_index, self.from_pane)
+            # FIXME: This is all chunks, not just those shared with to_pane
+            if chunk[1] == line:
+                action = self._classify_change_actions(chunk)
+                pixbuf = self.action_map.get(action)
+        if pixbuf:
+            self.set_pixbuf(pixbuf)
+        else:
+            self.props.pixbuf = None
+
+    def on_container_mode_changed(self, container, mode):
+        self.mode = mode
+        self.queue_draw()
+
+    def _classify_change_actions(self, change):
+        """Classify possible actions for the given change
+
+        Returns the action that can be performed given the content and
+        context of the change.
+        """
+        editable, other_editable = [v.get_editable() for v in self.views]
+
+        if not editable and not other_editable:
+            return 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"
+
+        action = None
+        if change_type == "delete":
+            if (editable and (self.mode == MODE_DELETE or not other_editable)):
+                action = MODE_DELETE
+            elif other_editable:
+                action = MODE_REPLACE
+        elif change_type == "replace":
+            if not editable:
+                if self.mode in (MODE_INSERT, MODE_REPLACE):
+                    action = self.mode
+            elif not other_editable:
+                action = MODE_DELETE
+            else:
+                action = self.mode
+
+        return action
diff --git a/meld/linkmap.py b/meld/linkmap.py
index 6368a57..1f705e1 100644
--- a/meld/linkmap.py
+++ b/meld/linkmap.py
@@ -19,22 +19,14 @@
 
 import math
 
-from gi.repository import Gdk
 from gi.repository import Gtk
 
-from . import diffutil
-
-
-# FIXME: import order issues
-MODE_REPLACE, MODE_DELETE, MODE_INSERT = 0, 1, 2
-
 
 class LinkMap(Gtk.DrawingArea):
 
     __gtype_name__ = "LinkMap"
 
     def __init__(self):
-        self.mode = MODE_REPLACE
         self._setup = False
 
     def associate(self, filediff, left_view, right_view):
@@ -47,109 +39,13 @@ class LinkMap(Gtk.DrawingArea):
         self.set_color_scheme((filediff.fill_colors, filediff.line_colors))
 
         self.line_height = filediff.pixels_per_line
-        icon_theme = Gtk.IconTheme.get_default()
-        load = lambda x: icon_theme.load_icon(x, self.line_height, 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, self.line_height * 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)
         self._setup = True
 
     def set_color_scheme(self, color_map):
         self.fill_colors, self.line_colors = color_map
         self.queue_draw()
 
-    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
-        allocation = self.get_allocation()
-        pixbuf_width = self.button_width
-        self.queue_draw_area(0, 0, pixbuf_width, allocation.height)
-        self.queue_draw_area(allocation.width - pixbuf_width, 0,
-                             pixbuf_width, allocation.height)
-
-    def paint_pixbuf_at(self, context, pixbuf, x, y):
-        context.translate(x, y)
-        Gdk.cairo_set_source_pixbuf(context, 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 or not right_editable) and \
-               left_editable:
-                left_act = MODE_DELETE
-        elif change_type == "insert":
-            right_act = MODE_REPLACE
-            if (self.mode == MODE_DELETE or not left_editable) 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 do_draw(self, context):
         if not self._setup:
             return
@@ -182,7 +78,6 @@ class LinkMap(Gtk.DrawingArea):
             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]]
 
-            culled = False
             # If either endpoint is completely off-screen, we cull for clarity
             if (t0 < 0 and t1 < 0) or (t0 > height and t1 > height):
                 if f0 == f1:
@@ -192,7 +87,6 @@ class LinkMap(Gtk.DrawingArea):
                 context.rel_line_to(0, f1 - f0 - radius * 2)
                 context.arc(x_steps[0], f1 - 0.5 - radius, radius, 0, q_rad)
                 context.close_path()
-                culled = True
             elif (f0 < 0 and f1 < 0) or (f0 > height and f1 > height):
                 if t0 == t1:
                     continue
@@ -203,7 +97,6 @@ class LinkMap(Gtk.DrawingArea):
                 context.arc_negative(x_steps[3], t1 - 0.5 - radius, radius,
                                      q_rad * 2, q_rad)
                 context.close_path()
-                culled = True
             else:
                 context.move_to(x_steps[0], f0 - 0.5)
                 context.curve_to(x_steps[1], f0 - 0.5,
@@ -228,18 +121,6 @@ class LinkMap(Gtk.DrawingArea):
             context.set_source_rgba(*self.line_colors[c[0]])
             context.stroke()
 
-            if culled:
-                continue
-
-            x = wtotal - self.button_width
-            left_act, right_act = self._classify_change_actions(c)
-            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)
-
         # allow for scrollbar at end of textview
         mid = int(0.5 * self.views[0].get_allocation().height) + 0.5
         context.set_source_rgba(0., 0., 0., 0.5)
@@ -249,88 +130,3 @@ class LinkMap(Gtk.DrawingArea):
 
     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]
-
-        allocation = self.get_allocation()
-        vis_offset = [t.get_visible_rect().y for t in self.views]
-        height = allocation.height
-
-        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))
-
-        # FIXME: As above, with relative offset gone we'd better be at the same
-        # y position as our textview.
-        view_offset_line = lambda v, l: (self.views[v].get_y_for_line_num(l) -
-                                         vis_offset[v])
-        for c in self.filediff.linediffer.pair_changes(src, dst, bounds):
-            f0, f1 = [view_offset_line(src_idx, l) for l in c[1:3]]
-            t0, t1 = [view_offset_line(dst_idx, l) for l in c[3:5]]
-
-            f0 = view_offset_line(src_idx, c[1])
-
-            if f0 < event.y < f0 + pix_height:
-                if (t0 < 0 and t1 < 0) or (t0 > height and t1 > height) or \
-                   (f0 < 0 and f1 < 0) or (f0 > height and f1 > height):
-                    break
-
-                # _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 = Gdk.Rectangle()
-                    rect.x = x
-                    rect.y = f0
-                    rect.width = pix_width
-                    rect.height = 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.get_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


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