[libgd] gd-main-view: Add rubberband selection



commit 700f73ad9a4e802dcc9eec0a7abc792d8665b013
Author: Alexander Larsson <alexl redhat com>
Date:   Thu Apr 11 22:34:00 2013 +0200

    gd-main-view: Add rubberband selection
    
    We support a rubberband-like drag-selection when in selection
    mode.
    
    https://bugzilla.gnome.org/show_bug.cgi?id=697645

 libgd/gd-main-icon-view.c    | 162 +++++++++++++++++++++++++++++++++++++++++
 libgd/gd-main-list-view.c    |  58 +++++++++++++++
 libgd/gd-main-view-generic.c |  78 ++++++++++++++++++++
 libgd/gd-main-view-generic.h |   6 ++
 libgd/gd-main-view.c         | 166 ++++++++++++++++++++++++++++++++++++++-----
 5 files changed, 454 insertions(+), 16 deletions(-)
---
diff --git a/libgd/gd-main-icon-view.c b/libgd/gd-main-icon-view.c
index 2789c27..d96be6c 100644
--- a/libgd/gd-main-icon-view.c
+++ b/libgd/gd-main-icon-view.c
@@ -128,6 +128,167 @@ gd_main_icon_view_constructed (GObject *obj)
 }
 
 static void
+path_from_line_rects (cairo_t *cr,
+                     GdkRectangle *lines,
+                     int n_lines)
+{
+  int start_line, end_line;
+  GdkRectangle *r;
+  int i;
+
+  /* Join rows vertically by extending to the middle */
+  for (i = 0; i < n_lines - 1; i++)
+    {
+      GdkRectangle *r1 = &lines[i];
+      GdkRectangle *r2 = &lines[i+1];
+      int gap = r2->y - (r1->y + r1->height);
+      int old_y;
+
+      r1->height += gap / 2;
+      old_y = r2->y;
+      r2->y = r1->y + r1->height;
+      r2->height += old_y - r2->y;
+    }
+
+  cairo_new_path (cr);
+  start_line = 0;
+
+  do
+    {
+      for (i = start_line; i < n_lines; i++)
+       {
+         r = &lines[i];
+         if (i == start_line)
+           cairo_move_to (cr, r->x + r->width, r->y);
+         else
+           cairo_line_to (cr, r->x + r->width, r->y);
+         cairo_line_to (cr, r->x + r->width, r->y + r->height);
+
+         if (i < n_lines - 1 &&
+             (r->x + r->width < lines[i+1].x ||
+              r->x > lines[i+1].x + lines[i+1].width))
+           {
+             i++;
+             break;
+           }
+       }
+      end_line = i;
+      for (i = end_line - 1; i >= start_line; i--)
+       {
+         r = &lines[i];
+         cairo_line_to (cr, r->x, r->y + r->height);
+         cairo_line_to (cr, r->x, r->y);
+       }
+      cairo_close_path (cr);
+      start_line = end_line;
+    }
+  while (end_line < n_lines);
+}
+
+static gboolean
+gd_main_icon_view_draw (GtkWidget *widget,
+                       cairo_t   *cr)
+{
+  GdMainIconView *self = GD_MAIN_ICON_VIEW (widget);
+  GtkAllocation allocation;
+  GtkStyleContext *context;
+  GdkRectangle line_rect;
+  GdkRectangle rect;
+  GtkTreePath *path;
+  GArray *lines;
+  GtkTreePath *rubberband_start, *rubberband_end;
+
+  GTK_WIDGET_CLASS (gd_main_icon_view_parent_class)->draw (widget, cr);
+
+  _gd_main_view_generic_get_rubberband_range (GD_MAIN_VIEW_GENERIC (self),
+                                             &rubberband_start, &rubberband_end);
+
+  if (rubberband_start)
+    {
+      cairo_save (cr);
+
+      context = gtk_widget_get_style_context (widget);
+
+      gtk_style_context_save (context);
+      gtk_style_context_add_class (context, GTK_STYLE_CLASS_RUBBERBAND);
+
+      path = gtk_tree_path_copy (rubberband_start);
+
+      line_rect.width = 0;
+      lines = g_array_new (FALSE, FALSE, sizeof (GdkRectangle));
+
+      while (gtk_tree_path_compare (path, rubberband_end) <= 0)
+       {
+         if (gtk_icon_view_get_cell_rect (GTK_ICON_VIEW (widget),
+                                          path,
+                                          NULL, &rect))
+           {
+             if (line_rect.width == 0)
+               line_rect = rect;
+             else
+               {
+                 if (rect.y == line_rect.y)
+                   gdk_rectangle_union (&rect, &line_rect, &line_rect);
+                 else
+                   {
+                     g_array_append_val (lines, line_rect);
+                     line_rect = rect;
+                   }
+               }
+           }
+         gtk_tree_path_next (path);
+       }
+
+      if (line_rect.width != 0)
+       g_array_append_val (lines, line_rect);
+
+      if (lines->len > 0)
+       {
+         GtkStateFlags state;
+         cairo_path_t *path;
+         GtkBorder border;
+         GdkRGBA border_color;
+
+         path_from_line_rects (cr, (GdkRectangle *)lines->data, lines->len);
+
+         /* For some reason we need to copy and reapply the path, or it gets
+            eaten by gtk_render_background() */
+         path = cairo_copy_path (cr);
+
+         cairo_save (cr);
+         cairo_clip (cr);
+         gtk_widget_get_allocation (widget, &allocation);
+         gtk_render_background (context, cr,
+                                0, 0,
+                                allocation.width, allocation.height);
+         cairo_restore (cr);
+
+         cairo_append_path (cr, path);
+         cairo_path_destroy (path);
+
+         state = gtk_widget_get_state_flags (widget);
+         gtk_style_context_get_border_color (context,
+                                             state,
+                                             &border_color);
+         gtk_style_context_get_border (context, state,
+                                       &border);
+
+         cairo_set_line_width (cr, border.left);
+         gdk_cairo_set_source_rgba (cr, &border_color);
+         cairo_stroke (cr);
+       }
+      g_array_free (lines, TRUE);
+
+      gtk_tree_path_free (path);
+
+      gtk_style_context_restore (context);
+      cairo_restore (cr);
+    }
+
+  return FALSE;
+}
+
+static void
 gd_main_icon_view_class_init (GdMainIconViewClass *klass)
 {
   GObjectClass *oclass = G_OBJECT_CLASS (klass);
@@ -135,6 +296,7 @@ gd_main_icon_view_class_init (GdMainIconViewClass *klass)
 
   oclass->constructed = gd_main_icon_view_constructed;
   wclass->drag_data_get = gd_main_icon_view_drag_data_get;
+  wclass->draw = gd_main_icon_view_draw;
 
   gtk_widget_class_install_style_property (wclass,
                                            g_param_spec_int ("check-icon-size",
diff --git a/libgd/gd-main-list-view.c b/libgd/gd-main-list-view.c
index 7a136b5..a1be3e6 100644
--- a/libgd/gd-main-list-view.c
+++ b/libgd/gd-main-list-view.c
@@ -37,6 +37,9 @@ G_DEFINE_TYPE_WITH_CODE (GdMainListView, gd_main_list_view, GTK_TYPE_TREE_VIEW,
                          G_IMPLEMENT_INTERFACE (GD_TYPE_MAIN_VIEW_GENERIC,
                                                 gd_main_view_generic_iface_init))
 
+static gboolean gd_main_list_view_draw (GtkWidget *widget,
+                                       cairo_t   *cr);
+
 static GtkTreePath*
 get_source_row (GdkDragContext *context)
 {
@@ -144,6 +147,7 @@ gd_main_list_view_class_init (GdMainListViewClass *klass)
 
   oclass->constructed = gd_main_list_view_constructed;
   wclass->drag_data_get = gd_main_list_view_drag_data_get;
+  wclass->draw = gd_main_list_view_draw;
 
   g_type_class_add_private (klass, sizeof (GdMainListViewPrivate));
 }
@@ -181,6 +185,60 @@ gd_main_list_view_set_selection_mode (GdMainViewGeneric *mv,
   gtk_tree_view_column_queue_resize (self->priv->tree_col);
 }
 
+static gboolean
+gd_main_list_view_draw (GtkWidget *widget,
+                       cairo_t   *cr)
+{
+  GdMainListView *self = GD_MAIN_LIST_VIEW (widget);
+  GtkStyleContext *context;
+  GdkRectangle lines_rect;
+  GdkRectangle rect;
+  GtkTreePath *path;
+  GtkTreePath *rubberband_start, *rubberband_end;
+
+  GTK_WIDGET_CLASS (gd_main_list_view_parent_class)->draw (widget, cr);
+
+  _gd_main_view_generic_get_rubberband_range (GD_MAIN_VIEW_GENERIC (self),
+                                             &rubberband_start, &rubberband_end);
+
+  if (rubberband_start)
+    {
+      context = gtk_widget_get_style_context (widget);
+
+      gtk_style_context_save (context);
+      gtk_style_context_add_class (context, GTK_STYLE_CLASS_RUBBERBAND);
+
+      path = gtk_tree_path_copy (rubberband_start);
+
+      lines_rect.width = 0;
+
+      while (gtk_tree_path_compare (path, rubberband_end) <= 0)
+       {
+         gtk_tree_view_get_cell_area (GTK_TREE_VIEW (self),
+                                      path, self->priv->tree_col, &rect);
+         if (lines_rect.width == 0)
+           lines_rect = rect;
+         else
+           gdk_rectangle_union (&rect, &lines_rect, &lines_rect);
+
+         gtk_tree_path_next (path);
+       }
+      gtk_tree_path_free (path);
+
+      gtk_render_background (context, cr,
+                            lines_rect.x, lines_rect.y,
+                            lines_rect.width, lines_rect.height);
+      gtk_render_frame (context, cr,
+                            lines_rect.x, lines_rect.y,
+                            lines_rect.width, lines_rect.height);
+
+
+      gtk_style_context_restore (context);
+    }
+
+  return FALSE;
+}
+
 static void
 gd_main_list_view_scroll_to_path (GdMainViewGeneric *mv,
                                   GtkTreePath *path)
diff --git a/libgd/gd-main-view-generic.c b/libgd/gd-main-view-generic.c
index b396ec5..200153c 100644
--- a/libgd/gd-main-view-generic.c
+++ b/libgd/gd-main-view-generic.c
@@ -82,6 +82,84 @@ gd_main_view_generic_set_selection_mode (GdMainViewGeneric *self,
   (* iface->set_selection_mode) (self, selection_mode);
 }
 
+
+typedef struct {
+  GtkTreePath *rubberband_start;
+  GtkTreePath *rubberband_end;
+} RubberbandInfo;
+
+static void
+rubber_band_info_destroy (RubberbandInfo *info)
+{
+  g_clear_pointer (&info->rubberband_start,
+                  gtk_tree_path_free);
+  g_clear_pointer (&info->rubberband_end,
+                  gtk_tree_path_free);
+  g_slice_free (RubberbandInfo, info);
+}
+
+static RubberbandInfo*
+get_rubber_band_info (GdMainViewGeneric *self)
+{
+  RubberbandInfo *info;
+
+  info = g_object_get_data (G_OBJECT (self), "gd-main-view-generic-rubber-band");
+  if (info == NULL)
+    {
+      info = g_slice_new0 (RubberbandInfo);
+      g_object_set_data_full (G_OBJECT (self), "gd-main-view-generic-rubber-band",
+                             info, (GDestroyNotify)rubber_band_info_destroy);
+    }
+
+  return info;
+}
+
+void
+gd_main_view_generic_set_rubberband_range (GdMainViewGeneric *self,
+                                          GtkTreePath *start,
+                                          GtkTreePath *end)
+{
+  RubberbandInfo *info;
+
+  info = get_rubber_band_info (self);
+
+  if (start == NULL || end == NULL)
+    {
+      g_clear_pointer (&info->rubberband_start,
+                      gtk_tree_path_free);
+      g_clear_pointer (&info->rubberband_end,
+                      gtk_tree_path_free);
+    }
+  else
+    {
+      if (gtk_tree_path_compare (start, end) < 0)
+       {
+         info->rubberband_start = gtk_tree_path_copy (start);
+         info->rubberband_end = gtk_tree_path_copy (end);
+       }
+      else
+       {
+         info->rubberband_start = gtk_tree_path_copy (end);
+         info->rubberband_end = gtk_tree_path_copy (start);
+       }
+    }
+
+  gtk_widget_queue_draw (GTK_WIDGET (self));
+}
+
+void
+_gd_main_view_generic_get_rubberband_range (GdMainViewGeneric *self,
+                                           GtkTreePath **start,
+                                           GtkTreePath **end)
+{
+  RubberbandInfo *info;
+
+  info = get_rubber_band_info (self);
+
+  *start = info->rubberband_start;
+  *end = info->rubberband_end;
+}
+
 void
 gd_main_view_generic_scroll_to_path (GdMainViewGeneric *self,
                                      GtkTreePath *path)
diff --git a/libgd/gd-main-view-generic.h b/libgd/gd-main-view-generic.h
index ffaaa6d..4df84ff 100644
--- a/libgd/gd-main-view-generic.h
+++ b/libgd/gd-main-view-generic.h
@@ -98,12 +98,18 @@ GtkTreePath * gd_main_view_generic_get_path_at_pos (GdMainViewGeneric *self,
                                                     gint y);
 void gd_main_view_generic_select_all (GdMainViewGeneric *self);
 void gd_main_view_generic_unselect_all (GdMainViewGeneric *self);
+void gd_main_view_generic_set_rubberband_range (GdMainViewGeneric *self,
+                                               GtkTreePath *start,
+                                               GtkTreePath *end);
 
 /* private */
 void _gd_main_view_generic_dnd_common (GtkTreeModel *model,
                                        gboolean selection_mode,
                                        GtkTreePath *path,
                                        GtkSelectionData *data);
+void _gd_main_view_generic_get_rubberband_range (GdMainViewGeneric *self,
+                                                GtkTreePath **start,
+                                                GtkTreePath **end);
 
 G_END_DECLS
 
diff --git a/libgd/gd-main-view.c b/libgd/gd-main-view.c
index 8f3407a..6a9036b 100644
--- a/libgd/gd-main-view.c
+++ b/libgd/gd-main-view.c
@@ -27,6 +27,7 @@
 
 #define MAIN_VIEW_TYPE_INITIAL -1
 #define MAIN_VIEW_DND_ICON_OFFSET 20
+#define MAIN_VIEW_RUBBERBAND_SELECT_TRIGGER_LENGTH 32
 
 struct _GdMainViewPrivate {
   GdMainViewType current_type;
@@ -35,6 +36,13 @@ struct _GdMainViewPrivate {
   GtkWidget *current_view;
   GtkTreeModel *model;
 
+  gboolean track_motion;
+  gboolean rubberband_select;
+  GtkTreePath *rubberband_select_first_path;
+  GtkTreePath *rubberband_select_last_path;
+  int button_down_x;
+  int button_down_y;
+
   gchar *button_press_item_path;
 
   gchar *last_selected_id;
@@ -77,6 +85,12 @@ gd_main_view_finalize (GObject *obj)
   g_free (self->priv->button_press_item_path);
   g_free (self->priv->last_selected_id);
 
+  if (self->priv->rubberband_select_first_path)
+    gtk_tree_path_free (self->priv->rubberband_select_first_path);
+
+  if (self->priv->rubberband_select_last_path)
+    gtk_tree_path_free (self->priv->rubberband_select_last_path);
+
   G_OBJECT_CLASS (gd_main_view_parent_class)->finalize (obj);
 }
 
@@ -528,10 +542,12 @@ on_button_release_event (GtkWidget *view,
 {
   GdMainView *self = user_data;
   GdMainViewGeneric *generic = get_generic (self);
-  GtkTreePath *path;
+  GtkTreePath *path, *start_path, *end_path, *tmp_path;
+  GtkTreeIter iter;
   gchar *button_release_item_path;
   gboolean selection_mode;
   gboolean res, same_item = FALSE;
+  gboolean is_selected;
 
   /* eat double/triple click events */
   if (event->type != GDK_BUTTON_RELEASE)
@@ -551,6 +567,53 @@ on_button_release_event (GtkWidget *view,
   g_free (self->priv->button_press_item_path);
   self->priv->button_press_item_path = NULL;
 
+  self->priv->track_motion = FALSE;
+  if (self->priv->rubberband_select)
+    {
+      gd_main_view_generic_set_rubberband_range (get_generic (self), NULL, NULL);
+      if (self->priv->rubberband_select_last_path)
+       {
+         if (!self->priv->selection_mode)
+           g_signal_emit (self, signals[SELECTION_MODE_REQUEST], 0);
+
+         start_path = gtk_tree_path_copy (self->priv->rubberband_select_first_path);
+         end_path = gtk_tree_path_copy (self->priv->rubberband_select_last_path);
+         if (gtk_tree_path_compare (start_path, end_path) > 0)
+           {
+             tmp_path = start_path;
+             start_path = end_path;
+             end_path = tmp_path;
+           }
+
+         while (gtk_tree_path_compare (start_path, end_path) <= 0)
+           {
+             if (gtk_tree_model_get_iter (self->priv->model,
+                                          &iter, start_path))
+               {
+                 gtk_tree_model_get (self->priv->model, &iter,
+                                     GD_MAIN_COLUMN_SELECTED, &is_selected,
+                                     -1);
+                 gtk_list_store_set (GTK_LIST_STORE (self->priv->model), &iter,
+                                     GD_MAIN_COLUMN_SELECTED, !is_selected,
+                                     -1);
+               }
+
+             gtk_tree_path_next (start_path);
+           }
+
+         gtk_tree_path_free (start_path);
+         gtk_tree_path_free (end_path);
+       }
+
+      g_clear_pointer (&self->priv->rubberband_select_first_path,
+                      gtk_tree_path_free);
+      g_clear_pointer (&self->priv->rubberband_select_last_path,
+                      gtk_tree_path_free);
+
+      res = TRUE;
+      goto out;
+    }
+
   if (!same_item)
     {
       res = FALSE;
@@ -590,40 +653,109 @@ on_button_press_event (GtkWidget *view,
   GList *selection, *l;
   GtkTreePath *sel_path;
   gboolean found = FALSE;
+  gboolean force_selection;
 
   path = gd_main_view_generic_get_path_at_pos (generic, event->x, event->y);
 
   if (path != NULL)
     self->priv->button_press_item_path = gtk_tree_path_to_string (path);
 
-  if (!self->priv->selection_mode ||
-      path == NULL)
+  force_selection =
+    (event->button == 3) ||
+    ((event->button == 1) && (event->state & GDK_CONTROL_MASK));
+
+  if (!self->priv->selection_mode && !force_selection)
     {
       gtk_tree_path_free (path);
       return FALSE;
     }
 
-  selection = gd_main_view_get_selection (self);
-
-  for (l = selection; l != NULL; l = l->next)
+  if (path && !force_selection)
     {
-      sel_path = l->data;
-      if (gtk_tree_path_compare (path, sel_path) == 0)
-        {
-          found = TRUE;
-          break;
-        }
-    }
+      selection = gd_main_view_get_selection (self);
 
-  if (selection != NULL)
-    g_list_free_full (selection, (GDestroyNotify) gtk_tree_path_free);
+      for (l = selection; l != NULL; l = l->next)
+       {
+         sel_path = l->data;
+         if (gtk_tree_path_compare (path, sel_path) == 0)
+           {
+             found = TRUE;
+             break;
+           }
+       }
+
+      if (selection != NULL)
+       g_list_free_full (selection, (GDestroyNotify) gtk_tree_path_free);
+    }
 
   /* if we did not find the item in the selection, block
    * drag and drop, while in selection mode
    */
-  return !found;
+  if (!found)
+    {
+      self->priv->track_motion = TRUE;
+      self->priv->rubberband_select = FALSE;
+      self->priv->rubberband_select_first_path = NULL;
+      self->priv->rubberband_select_last_path = NULL;
+      self->priv->button_down_x = event->x;
+      self->priv->button_down_y = event->y;
+      return TRUE;
+    }
+  else
+    return FALSE;
 }
 
+static gboolean
+on_motion_event (GtkWidget      *widget,
+                GdkEventMotion *event,
+                gpointer user_data)
+{
+  GdMainView *self = user_data;
+  GtkTreePath *path;
+
+  if (self->priv->track_motion)
+    {
+      if (!self->priv->rubberband_select &&
+         (event->x - self->priv->button_down_x) * (event->x - self->priv->button_down_x) +
+         (event->y - self->priv->button_down_y) * (event->y - self->priv->button_down_y)  >
+         MAIN_VIEW_RUBBERBAND_SELECT_TRIGGER_LENGTH * MAIN_VIEW_RUBBERBAND_SELECT_TRIGGER_LENGTH)
+       {
+         self->priv->rubberband_select = TRUE;
+         if (self->priv->button_press_item_path)
+           {
+             self->priv->rubberband_select_first_path =
+               gtk_tree_path_new_from_string (self->priv->button_press_item_path);
+           }
+       }
+
+      if (self->priv->rubberband_select)
+       {
+         path = gd_main_view_generic_get_path_at_pos (get_generic (self), event->x, event->y);
+         if (path != NULL)
+           {
+             if (self->priv->rubberband_select_first_path == NULL)
+               self->priv->rubberband_select_first_path = gtk_tree_path_copy (path);
+
+             if (self->priv->rubberband_select_last_path == NULL ||
+                 gtk_tree_path_compare (self->priv->rubberband_select_last_path, path) != 0)
+               {
+                 if (self->priv->rubberband_select_last_path)
+                   gtk_tree_path_free (self->priv->rubberband_select_last_path);
+                 self->priv->rubberband_select_last_path = path;
+
+                 gd_main_view_generic_set_rubberband_range (get_generic (self),
+                                                            self->priv->rubberband_select_first_path,
+                                                            self->priv->rubberband_select_last_path);
+               }
+             else
+               gtk_tree_path_free (path);
+           }
+       }
+    }
+  return FALSE;
+}
+
+
 static void
 on_drag_begin (GdMainViewGeneric *generic,
                GdkDragContext *drag_context,
@@ -773,6 +905,8 @@ gd_main_view_rebuild (GdMainView *self)
                     G_CALLBACK (on_button_press_event), self);
   g_signal_connect (self->priv->current_view, "button-release-event",
                     G_CALLBACK (on_button_release_event), self);
+  g_signal_connect (self->priv->current_view, "motion-notify-event",
+                    G_CALLBACK (on_motion_event), self);
   g_signal_connect_after (self->priv->current_view, "drag-begin",
                           G_CALLBACK (on_drag_begin), self);
   g_signal_connect (self->priv->current_view, "view-selection-changed",


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