Commit fb70fb99 authored by Kai Willadsen's avatar Kai Willadsen

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.
parent 5e43b73c
/* XPM */
static char * button_copy0_xpm[] = {
"16 16 2 1",
" c None",
". c #000000",
" ",
" .... ",
" .... ",
" .... ",
" .... ",
" .... ",
" .............. ",
" .............. ",
" .............. ",
" .............. ",
" .... ",
" .... ",
" .... ",
" .... ",
" .... ",
" "};
/* XPM */
static char *button_copy1[] = {
/* columns rows colors chars-per-pixel */
"16 24 2 1",
" c black",
". c None",
/* pixels */
"........ ",
"........ ",
"............ ",
"........... ",
"..... .. ",
"..... . . ",
"..... . .. ",
"..... . ... ",
"..... . .... ",
"..... ...... ",
"..... ........",
" ...",
" ...",
" ...",
"..... ........",
"..... . .... ",
"..... . ... ",
"..... . .. ",
"..... . . ",
"..... .. ",
"........... ",
"............ ",
"........ ",
"........ "
};
/* XPM */
static char *button_copy0[] = {
/* columns rows colors chars-per-pixel */
"16 24 2 1",
" c black",
". c None",
/* pixels */
" ........",
" ........",
" ............",
" ...........",
" .. .....",
" . . .....",
" .. . .....",
" ... . .....",
" .... . .....",
" ...... .....",
"........ .....",
"... ",
"... ",
"... ",
"........ .....",
" .... . .....",
" ... . .....",
" .. . .....",
" . . .....",
" .. .....",
" ...........",
" ............",
" ........",
" ........"
};
......@@ -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):
......
# 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
......@@ -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
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment