[gitg] Rework diff selection for improved rubber banding



commit 98e49eb09e2749d0e80b257141ca88edd0ce65dd
Author: Jesse van den Kieboom <jessevdk gnome org>
Date:   Sun Dec 20 17:47:22 2015 +0100

    Rework diff selection for improved rubber banding
    
    This changes the behavior to selection being always additive, unless
    pressing alt (mod1).

 libgitg/Makefile.am                         |    2 +-
 libgitg/gitg-diff-view-file-selectable.vala |  349 ++++++++++++++++++++-------
 libgitg/gitg-platform-support-osx.c         |   74 ++++++
 libgitg/gitg-platform-support.c             |   40 +++
 libgitg/gitg-platform-support.h             |    8 +
 vapi/gitg-platform-support.vapi             |    7 +
 6 files changed, 386 insertions(+), 94 deletions(-)
---
diff --git a/libgitg/Makefile.am b/libgitg/Makefile.am
index 24b6994..385d9b8 100644
--- a/libgitg/Makefile.am
+++ b/libgitg/Makefile.am
@@ -24,7 +24,7 @@ libgitg_libgitg_1_0_la_LIBADD =       \
 if GDK_WINDOWING_QUARTZ
 libgitg_libgitg_1_0_la_LIBADD += -lobjc
 libgitg_libgitg_1_0_la_CFLAGS += -xobjective-c
-libgitg_libgitg_1_0_la_LDFLAGS += -framework Foundation
+libgitg_libgitg_1_0_la_LDFLAGS += -framework Foundation -framework AppKit
 endif
 
 libgitg_libgitg_1_0_la_VALAFLAGS =     \
diff --git a/libgitg/gitg-diff-view-file-selectable.vala b/libgitg/gitg-diff-view-file-selectable.vala
index ec922a3..76b2c50 100644
--- a/libgitg/gitg-diff-view-file-selectable.vala
+++ b/libgitg/gitg-diff-view-file-selectable.vala
@@ -17,14 +17,24 @@
  * along with gitg. If not, see <http://www.gnu.org/licenses/>.
  */
 
+enum Gitg.DiffSelectionMode {
+       NONE,
+       SELECTING,
+       DESELECTING
+}
+
 class Gitg.DiffViewFileSelectable : Object
 {
        private string d_selection_category = "selection";
        private Gtk.TextTag d_selection_tag;
-       private bool d_is_selecting;
-       private bool d_is_deselecting;
+       private DiffSelectionMode d_selection_mode;
        private Gtk.TextMark d_start_selection_mark;
        private Gtk.TextMark d_end_selection_mark;
+       private Gee.HashMap<int, bool> d_originally_selected;
+       private Gdk.Cursor d_subtract_cursor_ptr;
+       private Gdk.Cursor d_subtract_cursor_hand;
+       private Gdk.Cursor d_cursor_ptr;
+       private Gdk.Cursor d_cursor_hand;
 
        public Gtk.SourceView source_view
        {
@@ -46,24 +56,118 @@ class Gitg.DiffViewFileSelectable : Object
                source_view.button_press_event.connect(button_press_event_on_view);
                source_view.motion_notify_event.connect(motion_notify_event_on_view);
                source_view.button_release_event.connect(button_release_event_on_view);
+               source_view.key_press_event.connect(key_press_event_on_view);
+               source_view.key_release_event.connect(key_release_event_on_view);
 
                source_view.get_style_context().add_class("handle-selection");
 
                source_view.realize.connect(() => {
-                       update_cursor(Gdk.CursorType.LEFT_PTR);
+                       update_cursor(cursor_ptr);
                });
 
                source_view.notify["state-flags"].connect(() => {
-                       update_cursor(Gdk.CursorType.LEFT_PTR);
+                       update_cursor(cursor_ptr);
                });
 
                d_selection_tag = source_view.buffer.create_tag("selection");
 
                source_view.style_updated.connect(update_theme);
                update_theme();
+
+               d_originally_selected = new Gee.HashMap<int, bool>();
+       }
+
+       private Gdk.Cursor composite_subtract_cursor(Gdk.CursorType cursor)
+       {
+               int width, height, hot_x, hot_y;
+
+               var surface = PlatformSupport.create_cursor_surface(source_view.get_display(),
+                                                                   cursor,
+                                                                   out hot_x,
+                                                                   out hot_y,
+                                                                   out width,
+                                                                   out height);
+
+               if (surface == null)
+               {
+                       return new Gdk.Cursor.for_display(source_view.get_display(), cursor);
+               }
+
+               var ctx = new Cairo.Context(surface);
+
+               ctx.set_line_width(1);
+
+               const int margin = 2;
+               const int length = 5;
+
+               ctx.set_source_rgb(0, 0, 0);
+               ctx.move_to(width - margin - length + 0.5, margin - (length - 1) / 2 + 0.5);
+               ctx.rel_line_to(length, 0);
+               ctx.stroke();
+
+               ctx.set_source_rgb(1, 1, 1);
+               ctx.move_to(width - margin - length + 0.5, margin - (length - 1) / 2 + 1.5);
+               ctx.rel_line_to(length, 0);
+               ctx.stroke();
+
+               return new Gdk.Cursor.from_surface(source_view.get_display(), surface, hot_x, hot_y);
+       }
+
+       private Gdk.Cursor cursor_ptr
+       {
+               owned get
+               {
+                       if (d_cursor_ptr == null)
+                       {
+                               d_cursor_ptr = new Gdk.Cursor.for_display(source_view.get_display(), 
Gdk.CursorType.LEFT_PTR);
+                       }
+
+                       return d_cursor_ptr;
+               }
+       }
+
+       private Gdk.Cursor cursor_hand
+       {
+               owned get
+               {
+                       if (d_cursor_hand == null)
+                       {
+                               d_cursor_hand = new Gdk.Cursor.for_display(source_view.get_display(), 
Gdk.CursorType.HAND1);
+                       }
+
+                       return d_cursor_hand;
+               }
+       }
+
+       private Gdk.Cursor subtract_cursor_ptr
+       {
+               owned get
+               {
+                       if (d_subtract_cursor_ptr == null)
+                       {
+                               d_subtract_cursor_ptr = composite_subtract_cursor(Gdk.CursorType.LEFT_PTR);
+                       }
+
+                       
+                       return d_subtract_cursor_ptr;
+               }
+       }
+
+       private Gdk.Cursor subtract_cursor_hand
+       {
+               owned get
+               {
+                       if (d_subtract_cursor_hand == null)
+                       {
+                               d_subtract_cursor_hand = composite_subtract_cursor(Gdk.CursorType.HAND1);
+                       }
+
+                       
+                       return d_subtract_cursor_hand;
+               }
        }
 
-       private void update_cursor(Gdk.CursorType type)
+       private void update_cursor(Gdk.Cursor cursor)
        {
                var window = source_view.get_window(Gtk.TextWindowType.TEXT);
 
@@ -72,10 +176,32 @@ class Gitg.DiffViewFileSelectable : Object
                        return;
                }
 
-               var cursor = new Gdk.Cursor.for_display(source_view.get_display(), type);
                window.set_cursor(cursor);
        }
 
+       private void update_cursor_for_state(Gdk.ModifierType state)
+       {
+               Gtk.TextIter iter;
+               
+               var is_hunk = get_iter_from_pointer_position(out iter) && get_line_is_hunk(iter);
+
+               if ((state & Gdk.ModifierType.MOD1_MASK) != 0 || d_selection_mode == 
DiffSelectionMode.DESELECTING)
+               {
+                       if (is_hunk)
+                       {
+                               update_cursor(subtract_cursor_hand);
+                       }
+                       else
+                       {
+                               update_cursor(subtract_cursor_ptr);
+                       }
+               }
+               else
+               {
+                       update_cursor(is_hunk ? cursor_hand : cursor_ptr);
+               }
+       }
+
        private void update_theme()
        {
                var selection_attributes = new Gtk.SourceMarkAttributes();
@@ -98,22 +224,22 @@ class Gitg.DiffViewFileSelectable : Object
 
        private bool get_line_selected(Gtk.TextIter iter)
        {
-               var text_view = source_view as Gtk.TextView;
                Gtk.TextIter start = iter;
 
                start.set_line_offset(0);
-               var buffer = text_view.get_buffer() as Gtk.SourceBuffer;
+
+               var buffer = source_view.buffer as Gtk.SourceBuffer;
 
                return buffer.get_source_marks_at_iter(start, d_selection_category) != null;
        }
 
        private bool get_line_is_diff(Gtk.TextIter iter)
        {
-               var text_view = source_view as Gtk.TextView;
                Gtk.TextIter start = iter;
 
                start.set_line_offset(0);
-               var buffer = text_view.get_buffer() as Gtk.SourceBuffer;
+
+               var buffer = source_view.buffer as Gtk.SourceBuffer;
 
                return (buffer.get_source_marks_at_iter(start, "added") != null) ||
                       (buffer.get_source_marks_at_iter(start, "removed") != null);
@@ -121,19 +247,19 @@ class Gitg.DiffViewFileSelectable : Object
 
        private bool get_line_is_hunk(Gtk.TextIter iter)
        {
-               var text_view = source_view as Gtk.TextView;
                Gtk.TextIter start = iter;
 
                start.set_line_offset(0);
-               var buffer = text_view.get_buffer() as Gtk.SourceBuffer;
+
+               var buffer = source_view.buffer as Gtk.SourceBuffer;
 
                return buffer.get_source_marks_at_iter(start, "header") != null;
        }
 
        private bool get_iter_from_pointer_position(out Gtk.TextIter iter)
        {
-               var text_view = source_view as Gtk.TextView;
-               var win = text_view.get_window(Gtk.TextWindowType.TEXT);
+               var win = source_view.get_window(Gtk.TextWindowType.TEXT);
+
                int x, y, width, height;
 
                // To silence unassigned iter warning
@@ -152,17 +278,16 @@ class Gitg.DiffViewFileSelectable : Object
                }
 
                int win_x, win_y;
-               text_view.window_to_buffer_coords(Gtk.TextWindowType.TEXT, x, y, out win_x, out win_y);
 
-               text_view.get_iter_at_location(out iter, win_x, win_y);
+               source_view.window_to_buffer_coords(Gtk.TextWindowType.TEXT, x, y, out win_x, out win_y);
+               source_view.get_iter_at_location(out iter, win_x, win_y);
 
                return true;
        }
 
-       private void select_range(Gtk.TextIter start, Gtk.TextIter end)
+       private void update_selection_range(Gtk.TextIter start, Gtk.TextIter end, bool select)
        {
-               var text_view = source_view as Gtk.TextView;
-               var buffer = text_view.get_buffer() as Gtk.SourceBuffer;
+               var buffer = source_view.buffer as Gtk.SourceBuffer;
 
                Gtk.TextIter real_start, real_end;
 
@@ -172,40 +297,70 @@ class Gitg.DiffViewFileSelectable : Object
                real_start.order(real_end);
                real_start.set_line_offset(0);
 
-               while (real_start.get_line() <= real_end.get_line())
+               real_end.forward_to_line_end();
+
+               var start_line = real_start.get_line();
+               var end_line = real_end.get_line();
+
+               var current = real_start;
+
+               while (start_line <= end_line)
                {
-                       if (get_line_is_diff(real_start))
+                       if (get_line_is_diff(current))
                        {
-                               buffer.create_source_mark(null, d_selection_category, real_start);
+                               if (!d_originally_selected.has_key(start_line))
+                               {
+                                       d_originally_selected[start_line] = get_line_selected(current);
+                               }
+
+                               if (select)
+                               {
+                                       buffer.create_source_mark(null, d_selection_category, current);
 
-                               var line_end = real_start;
-                               line_end.forward_to_line_end();
+                                       var line_end = current;
+                                       line_end.forward_to_line_end();
 
-                               buffer.apply_tag(d_selection_tag, real_start, line_end);
+                                       buffer.apply_tag(d_selection_tag, current, line_end);
+                               }
                        }
 
-                       if (!real_start.forward_line())
+                       if (!current.forward_line())
                        {
                                break;
                        }
+
+                       start_line++;
+               }
+
+               if (!select)
+               {
+                       buffer.remove_source_marks(real_start, real_end, d_selection_category);
+                       buffer.remove_tag(d_selection_tag, real_start, real_end);
                }
        }
 
-       private void deselect_range(Gtk.TextIter start, Gtk.TextIter end)
+       private void clear_original_selection(Gtk.TextIter start, Gtk.TextIter end, bool include_end)
        {
-               var text_view = source_view as Gtk.TextView;
-               var buffer = text_view.get_buffer() as Gtk.SourceBuffer;
+               var current = start;
+               current.set_line_offset(0);
 
-               Gtk.TextIter real_start, real_end;
+               var end_line = end.get_line();
+               var current_line = current.get_line();
 
-               real_start = start;
-               real_start.set_line_offset(0);
+               if (include_end)
+               {
+                       end_line++;
+               }
 
-               real_end = end;
-               real_end.forward_to_line_end();
+               while (current_line < end_line)
+               {
+                       var originally_selected = d_originally_selected[current_line];
+
+                       update_selection_range(current, current, originally_selected);
 
-               buffer.remove_source_marks(real_start, real_end, d_selection_category);
-               buffer.remove_tag(d_selection_tag, real_start, real_end);
+                       current.forward_line();
+                       current_line++;
+               }
        }
 
        private bool button_press_event_on_view(Gdk.EventButton event)
@@ -216,92 +371,81 @@ class Gitg.DiffViewFileSelectable : Object
                }
 
                Gtk.TextIter iter;
+
                if (!get_iter_from_pointer_position(out iter))
                {
                        return false;
                }
 
-               if (get_line_selected(iter))
+               if ((event.state & Gdk.ModifierType.MOD1_MASK) != 0)
                {
-                       d_is_deselecting = true;
-                       deselect_range(iter, iter);
+                       d_selection_mode = DiffSelectionMode.DESELECTING;
                }
                else
                {
-                       d_is_selecting = true;
-                       select_range(iter, iter);
+                       d_selection_mode = DiffSelectionMode.SELECTING;
                }
 
-               var text_view = source_view as Gtk.TextView;
-               var buffer = text_view.get_buffer();
+               var buffer = source_view.buffer;
 
                d_start_selection_mark = buffer.create_mark(null, iter, false);
                d_end_selection_mark = buffer.create_mark(null, iter, false);
 
-               return false;
+               update_selection(iter);
+               update_cursor_for_state(event.state);
+
+               return true;
        }
 
-       private bool motion_notify_event_on_view(Gdk.EventMotion event)
+       private void update_selection(Gtk.TextIter cursor)
        {
-               Gtk.TextIter iter;
-               if (!get_iter_from_pointer_position(out iter))
-               {
-                       return false;
-               }
-
-               update_cursor(get_line_is_hunk(iter) ? Gdk.CursorType.HAND1 : Gdk.CursorType.LEFT_PTR);
-
-               if (!d_is_selecting && !d_is_deselecting)
-               {
-                       return false;
-               }
-
-               var text_view = source_view as Gtk.TextView;
-               var buffer = text_view.get_buffer();
-
-               Gtk.TextIter start, end, current;
+               var buffer = source_view.buffer;
 
-               current = iter;
+               Gtk.TextIter start, end;
 
                buffer.get_iter_at_mark(out start, d_start_selection_mark);
-               start.order(current);
+               buffer.get_iter_at_mark(out end, d_end_selection_mark);
 
-               if (d_is_selecting)
+               // Clear to original selection
+               if (start.get_line() < end.get_line())
                {
-                       select_range(start, current);
+                       var next = cursor;
+                       next.forward_line();
+
+                       clear_original_selection(next, end, true);
                }
                else
                {
-                       deselect_range(start, current);
+                       clear_original_selection(end, cursor, false);
                }
 
-               buffer.get_iter_at_mark(out end, d_end_selection_mark);
-               if (!end.in_range(start, current))
-               {
-                       start = end;
-                       current = iter;
+               update_selection_range(start, cursor, d_selection_mode == DiffSelectionMode.SELECTING);
+               buffer.move_mark(d_end_selection_mark, cursor);
+       }
 
-                       start.order(current);
+       private bool motion_notify_event_on_view(Gdk.EventMotion event)
+       {
+               Gtk.TextIter iter;
 
-                       if (d_is_selecting)
-                       {
-                               deselect_range(start, current);
-                       }
-                       else
-                       {
-                               select_range(start, current);
-                       }
+               if (!get_iter_from_pointer_position(out iter))
+               {
+                       return false;
                }
 
-               buffer.move_mark(d_end_selection_mark, iter);
+               update_cursor_for_state(event.state);
 
-               return false;
+               if (d_selection_mode == DiffSelectionMode.NONE)
+               {
+                       return false;
+               }
+
+               update_selection(iter);
+               return true;
        }
 
        private void update_has_selection()
        {
-               var text_view = source_view as Gtk.TextView;
-               var buffer = text_view.get_buffer();
+               var buffer = source_view.buffer;
 
                Gtk.TextIter iter;
                buffer.get_start_iter(out iter);
@@ -330,20 +474,39 @@ class Gitg.DiffViewFileSelectable : Object
                        return false;
                }
 
-               d_is_selecting = false;
-               d_is_deselecting = false;
+               d_selection_mode = DiffSelectionMode.NONE;
 
-               var text_view = source_view as Gtk.TextView;
-               var buffer = text_view.get_buffer();
+               var buffer = source_view.buffer;
 
-               buffer.delete_mark(d_start_selection_mark);
-               d_start_selection_mark = null;
+               if (d_start_selection_mark != null)
+               {
+                       buffer.delete_mark(d_start_selection_mark);
+                       d_start_selection_mark = null;
+               }
 
-               buffer.delete_mark(d_end_selection_mark);
-               d_end_selection_mark = null;
+               if (d_end_selection_mark != null)
+               {
+                       buffer.delete_mark(d_end_selection_mark);
+                       d_end_selection_mark = null;
+               }
 
                update_has_selection();
+               d_originally_selected.clear();
 
+               return true;
+       }
+
+       private bool key_press_event_on_view(Gdk.EventKey event)
+       {
+               stdout.printf(@"press: $(event.state)\n");
+               update_cursor_for_state(event.state);
+               return false;
+       }
+
+       private bool key_release_event_on_view(Gdk.EventKey event)
+       {
+               stdout.printf(@"release: $(event.state)\n");
+               update_cursor_for_state(event.state);
                return false;
        }
 }
diff --git a/libgitg/gitg-platform-support-osx.c b/libgitg/gitg-platform-support-osx.c
index 3c3f04d..d105cce 100644
--- a/libgitg/gitg-platform-support-osx.c
+++ b/libgitg/gitg-platform-support-osx.c
@@ -85,4 +85,78 @@ gitg_platform_support_http_get_finish (GAsyncResult  *result,
        return g_task_propagate_pointer (G_TASK (result), error);
 }
 
+cairo_surface_t *
+gitg_platform_support_create_cursor_surface (GdkDisplay    *display,
+                                             GdkCursorType  cursor_type,
+                                             gint          *hot_x,
+                                             gint          *hot_y,
+                                             gint          *width,
+                                             gint          *height)
+{
+       NSCursor *cursor;
+       NSImage *image;
+       NSBitmapImageRep *image_rep;
+       const unsigned char *pixel_data;
+       NSSize size;
+       NSPoint hotspot;
+       gint w, h;
+       cairo_surface_t *surface, *target;
+       cairo_t *ctx;
+
+       switch (cursor_type)
+       {
+       case GDK_HAND1:
+               cursor = [NSCursor pointingHandCursor];
+               break;
+       default:
+               cursor = [NSCursor arrowCursor];
+               break;
+       }
+
+       image = [cursor image];
+
+       image_rep = [[NSBitmapImageRep alloc] initWithData:[image TIFFRepresentation]];
+       pixel_data = [image_rep bitmapData];
+
+       w = [image_rep pixelsWide];
+       h = [image_rep pixelsHigh];
+
+       hotspot = [cursor hotSpot];
+       size = [image size];
+
+       if (hot_x)
+       {
+               *hot_x = (gint)(hotspot.x);
+       }
+
+       if (hot_y)
+       {
+               *hot_y = (gint)(hotspot.y);
+       }
+
+       if (width)
+       {
+               *width = size.width;
+       }
+
+       if (height)
+       {
+               *height = size.height;
+       }
+
+       surface = cairo_image_surface_create_for_data (pixel_data, CAIRO_FORMAT_ARGB32, w, h, [image_rep 
bytesPerRow]);
+       target = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, size.width, size.height);
+
+       ctx = cairo_create (target);
+
+       cairo_scale (ctx, size.width / w, size.height / h);
+       cairo_set_source_surface (ctx, surface, 0, 0);
+       cairo_paint (ctx);
+       cairo_destroy (ctx);
+
+       cairo_surface_destroy (surface);
+
+       return target;
+}
+
 // ex:ts=4 noet
diff --git a/libgitg/gitg-platform-support.c b/libgitg/gitg-platform-support.c
index b2406b3..410e5d6 100644
--- a/libgitg/gitg-platform-support.c
+++ b/libgitg/gitg-platform-support.c
@@ -41,4 +41,44 @@ gitg_platform_support_http_get_finish (GAsyncResult  *result,
        return G_INPUT_STREAM (g_file_read_finish (g_async_result_get_source_object (result), result, error));
 }
 
+cairo_surface_t *
+gitg_platform_support_create_cursor_surface (GdkDisplay    *display,
+                                             GdkCursorType  cursor_type,
+                                             gint          *hot_x,
+                                             gint          *hot_y,
+                                             gint          *width,
+                                             gint          *height)
+{
+       GdkCursor *cursor;
+       cairo_surface_t *surface;
+       gint w = 0, h = 0;
+
+       cursor = gdk_cursor_new_for_display (display, cursor_type);
+       surface = gdk_cursor_get_surface (hot_x, hot_y);
+
+       switch (cairo_surface_get_type (surface))
+       {
+       case CAIRO_SURFACE_TYPE_XLIB:
+               w = cairo_xlib_surface_get_width (surface);
+               h = cairo_xlib_surface_get_height (surface);
+               break;
+       case CAIRO_SURFACE_TYPE_IMAGE:
+               w = cairo_image_surface_get_width (surface);
+               h = cairo_image_surface_get_height (surface);
+               break;
+       }
+
+       if (width)
+       {
+               *width = w;
+       }
+
+       if (height)
+       {
+               *height = h;
+       }
+
+       return surface;
+}
+
 // ex:ts=4 noet
diff --git a/libgitg/gitg-platform-support.h b/libgitg/gitg-platform-support.h
index 6bb4075..ea502bb 100644
--- a/libgitg/gitg-platform-support.h
+++ b/libgitg/gitg-platform-support.h
@@ -22,6 +22,7 @@
 
 #include <gdk/gdk.h>
 #include <gio/gio.h>
+#include <cairo/cairo.h>
 
 gboolean gitg_platform_support_use_native_window_controls (GdkDisplay *display);
 
@@ -33,6 +34,13 @@ void          gitg_platform_support_http_get        (GFile                *file,
 GInputStream *gitg_platform_support_http_get_finish (GAsyncResult         *result,
                                                      GError              **error);
 
+cairo_surface_t *gitg_platform_support_create_cursor_surface (GdkDisplay    *display,
+                                                              GdkCursorType  cursor_type,
+                                                              gint          *hot_x,
+                                                              gint          *hot_y,
+                                                              gint          *width,
+                                                              gint          *height);
+
 #endif /* __GITG_PLATFORM_SUPPORT_H__ */
 
 // ex:ts=4 noet
diff --git a/vapi/gitg-platform-support.vapi b/vapi/gitg-platform-support.vapi
index 7f99103..50b1b99 100644
--- a/vapi/gitg-platform-support.vapi
+++ b/vapi/gitg-platform-support.vapi
@@ -5,5 +5,12 @@ namespace Gitg
        {
                public static bool use_native_window_controls(Gdk.Display? display = null);
                public static async GLib.InputStream http_get(GLib.File url, GLib.Cancellable? cancellable = 
null) throws GLib.IOError;
+
+               public static Cairo.Surface create_cursor_surface(Gdk.Display? display,
+                                                                 Gdk.CursorType cursor_type,
+                                                                 out int hot_x = null,
+                                                                 out int hot_y = null,
+                                                                 out int width = null,
+                                                                 out int height = null);
        }
 }


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