[meld] Move chunk actions from Linkmap to GtkSource.Gutter
- From: Kai Willadsen <kaiw src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [meld] Move chunk actions from Linkmap to GtkSource.Gutter
- Date: Mon, 14 Oct 2013 21:11:11 +0000 (UTC)
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]