[gtk/prop-list: 75/83] listbase: Add rubberband selection



commit 979e5314841416244f9ec01d74cd775fc98163a6
Author: Matthias Clasen <mclasen redhat com>
Date:   Sat Dec 21 19:07:11 2019 -0500

    listbase: Add rubberband selection
    
    Implement the typical rubberband selection, including
    autoscroll. This is only useful with multiselection,
    and not very compatible with single-click-activate.
    Therefore, it is not enabled by default, and needs
    to be turned on with the enable-rubber-band property.

 gtk/gtklistbase.c        | 395 ++++++++++++++++++++++++++++++++++++++++++++++-
 gtk/gtklistbaseprivate.h |   3 +
 gtk/gtklistitemwidget.c  |  10 +-
 3 files changed, 406 insertions(+), 2 deletions(-)
---
diff --git a/gtk/gtklistbase.c b/gtk/gtklistbase.c
index fde6ff498b..81154594f6 100644
--- a/gtk/gtklistbase.c
+++ b/gtk/gtklistbase.c
@@ -29,6 +29,12 @@
 #include "gtkscrollable.h"
 #include "gtksingleselection.h"
 #include "gtktypebuiltins.h"
+#include "gtkgesturedrag.h"
+#include "gtkwidgetprivate.h"
+#include "gtkcssnodeprivate.h"
+#include "gtkstylecontextprivate.h"
+#include "gtksnapshot.h"
+#include "gtkmultiselection.h"
 
 typedef struct _GtkListBasePrivate GtkListBasePrivate;
 
@@ -51,6 +57,22 @@ struct _GtkListBasePrivate
   GtkListItemTracker *selected;
   /* the item that has input focus */
   GtkListItemTracker *focus;
+
+  gboolean enable_rubberband;
+  gboolean doing_rubberband;
+  double rb_x1;
+  double rb_y1;
+  double rb_x2;
+  double rb_y2;
+  GtkGesture *drag_gesture;
+  GtkCssNode *rubberband_node;
+  GtkSelectionModel *old_selection;
+  gboolean modify;
+  gboolean extend;
+
+  guint autoscroll_id;
+  double autoscroll_delta_x;
+  double autoscroll_delta_y;
 };
 
 enum
@@ -61,6 +83,7 @@ enum
   PROP_ORIENTATION,
   PROP_VADJUSTMENT,
   PROP_VSCROLL_POLICY,
+  PROP_ENABLE_RUBBERBAND,
 
   N_PROPS
 };
@@ -586,6 +609,10 @@ gtk_list_base_get_property (GObject    *object,
       g_value_set_enum (value, priv->scroll_policy[GTK_ORIENTATION_VERTICAL]);
       break;
 
+    case PROP_ENABLE_RUBBERBAND:
+      g_value_set_boolean (value, priv->enable_rubberband);
+      break;
+
     default:
       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
       break;
@@ -677,6 +704,10 @@ gtk_list_base_set_property (GObject      *object,
       gtk_list_base_set_scroll_policy (self, GTK_ORIENTATION_VERTICAL, g_value_get_enum (value));
       break;
 
+    case PROP_ENABLE_RUBBERBAND:
+      gtk_list_base_set_enable_rubberband (self, g_value_get_boolean (value));
+      break;
+
     default:
       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
       break;
@@ -1059,6 +1090,20 @@ gtk_list_base_add_custom_move_binding (GtkBindingSet      *binding_set,
                                   NULL, NULL);
 }
 
+static void gtk_list_base_snapshot_rubberband (GtkListBase *self,
+                                               GtkSnapshot *snapshot);
+
+static void
+gtk_list_base_snapshot (GtkWidget   *widget,
+                        GtkSnapshot *snapshot)
+{
+  GtkListBase *self = GTK_LIST_BASE (widget);
+
+  GTK_WIDGET_CLASS (gtk_list_base_parent_class)->snapshot (widget, snapshot);
+
+  gtk_list_base_snapshot_rubberband (self, snapshot);
+}
+
 static void
 gtk_list_base_class_init (GtkListBaseClass *klass)
 {
@@ -1068,6 +1113,7 @@ gtk_list_base_class_init (GtkListBaseClass *klass)
   gpointer iface;
 
   widget_class->focus = gtk_list_base_focus;
+  widget_class->snapshot = gtk_list_base_snapshot;
 
   gobject_class->dispose = gtk_list_base_dispose;
   gobject_class->get_property = gtk_list_base_get_property;
@@ -1102,6 +1148,13 @@ gtk_list_base_class_init (GtkListBaseClass *klass)
                        GTK_ORIENTATION_VERTICAL,
                        G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_EXPLICIT_NOTIFY);
 
+  properties[PROP_ENABLE_RUBBERBAND] =
+    g_param_spec_boolean ("enable-rubberband",
+                          P_("Enable rubberband"),
+                          P_("Enable selection by dragging"),
+                          FALSE,
+                          G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_EXPLICIT_NOTIFY);
+
   g_object_class_install_properties (gobject_class, N_PROPS, properties);
 
   /**
@@ -1187,6 +1240,347 @@ gtk_list_base_class_init (GtkListBaseClass *klass)
   gtk_binding_entry_add_action (binding_set, GDK_KEY_backslash, GDK_CONTROL_MASK, "list.unselect-all", NULL);
 }
 
+static void gtk_list_base_update_rubberband_selection (GtkListBase *self);
+
+static gboolean
+autoscroll_cb (GtkWidget     *widget,
+               GdkFrameClock *frame_clock,
+               gpointer       data)
+{
+  GtkListBase *self = data;
+  GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
+  double value;
+
+  value = gtk_adjustment_get_value (priv->adjustment[GTK_ORIENTATION_HORIZONTAL]);
+  gtk_adjustment_set_value (priv->adjustment[GTK_ORIENTATION_HORIZONTAL], value + priv->autoscroll_delta_x);
+  value = gtk_adjustment_get_value (priv->adjustment[GTK_ORIENTATION_VERTICAL]);
+  gtk_adjustment_set_value (priv->adjustment[GTK_ORIENTATION_VERTICAL], value + priv->autoscroll_delta_y);
+
+  if (priv->doing_rubberband)
+    {
+      priv->rb_x2 += priv->autoscroll_delta_x;
+      priv->rb_y2 += priv->autoscroll_delta_y;
+      gtk_list_base_update_rubberband_selection (self);
+    }
+
+  gtk_widget_queue_draw (GTK_WIDGET (self));
+
+  return G_SOURCE_CONTINUE;
+}
+
+static void
+add_autoscroll (GtkListBase *self,
+                double       delta_x,
+                double       delta_y)
+{
+  GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
+
+  priv->autoscroll_delta_x = delta_x;
+  priv->autoscroll_delta_y = delta_y;
+
+  if (priv->autoscroll_id == 0)
+    priv->autoscroll_id = gtk_widget_add_tick_callback (GTK_WIDGET (self), autoscroll_cb, self, NULL);
+}
+
+static void
+remove_autoscroll (GtkListBase *self)
+{
+  GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
+
+  if (priv->autoscroll_id != 0)
+    {
+      gtk_widget_remove_tick_callback (GTK_WIDGET (self), priv->autoscroll_id);
+      priv->autoscroll_id = 0;
+    }
+}
+
+static void
+gtk_list_base_start_rubberband (GtkListBase *self,
+                                double       x,
+                                double       y,
+                                gboolean     modify,
+                                gboolean     extend)
+{
+  GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
+  GtkCssNode *widget_node;
+  double value_x, value_y;
+  GtkSelectionModel *selection;
+
+  if (priv->doing_rubberband)
+    return;
+
+  value_x = gtk_adjustment_get_value (priv->adjustment[GTK_ORIENTATION_HORIZONTAL]);
+  value_y = gtk_adjustment_get_value (priv->adjustment[GTK_ORIENTATION_VERTICAL]);
+
+  priv->rb_x1 = priv->rb_x2 = x + value_x;
+  priv->rb_y1 = priv->rb_y2 = y + value_y;
+
+  priv->modify = modify;
+  priv->extend = extend;
+
+  widget_node = gtk_widget_get_css_node (GTK_WIDGET (self));
+  priv->rubberband_node = gtk_css_node_new ();
+  gtk_css_node_set_name (priv->rubberband_node, I_("rubberband"));
+  gtk_css_node_set_parent (priv->rubberband_node, widget_node);
+  gtk_css_node_set_state (priv->rubberband_node, gtk_css_node_get_state (widget_node));
+  g_object_unref (priv->rubberband_node);
+
+  selection = gtk_list_item_manager_get_model (priv->item_manager);
+
+  if (modify)
+    priv->old_selection = GTK_SELECTION_MODEL (gtk_multi_selection_copy (selection));
+
+  priv->doing_rubberband = TRUE;
+}
+
+static void
+gtk_list_base_stop_rubberband (GtkListBase *self)
+{
+  GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
+
+  if (!priv->doing_rubberband)
+    return;
+
+  priv->doing_rubberband = FALSE;
+  gtk_css_node_set_parent (priv->rubberband_node, NULL);
+  priv->rubberband_node = NULL;
+
+  g_clear_object (&priv->old_selection);
+
+  remove_autoscroll (self);
+
+  gtk_widget_queue_draw (GTK_WIDGET (self));
+}
+
+#define SCROLL_EDGE_SIZE 15
+
+static void
+gtk_list_base_update_rubberband (GtkListBase *self,
+                                 double       x,
+                                 double       y)
+{
+  GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
+  double value_x, value_y, page_size, upper;
+  double delta_x, delta_y;
+
+  if (!priv->doing_rubberband)
+    return;
+
+  value_x = gtk_adjustment_get_value (priv->adjustment[GTK_ORIENTATION_HORIZONTAL]);
+  value_y = gtk_adjustment_get_value (priv->adjustment[GTK_ORIENTATION_VERTICAL]);
+
+  priv->rb_x2 = x + value_x;
+  priv->rb_y2 = y + value_y;
+
+  gtk_list_base_update_rubberband_selection (self);
+
+  page_size = gtk_adjustment_get_page_size (priv->adjustment[GTK_ORIENTATION_HORIZONTAL]);
+  upper = gtk_adjustment_get_upper (priv->adjustment[GTK_ORIENTATION_HORIZONTAL]);
+
+  if (x < SCROLL_EDGE_SIZE && value_x > 0)
+    delta_x = - (SCROLL_EDGE_SIZE - x)/3.0;
+  else if (page_size - x < SCROLL_EDGE_SIZE && value_x + page_size < upper)
+    delta_x = (SCROLL_EDGE_SIZE - (page_size - x))/3.0;
+  else
+    delta_x = 0;
+
+  page_size = gtk_adjustment_get_page_size (priv->adjustment[GTK_ORIENTATION_VERTICAL]);
+  upper = gtk_adjustment_get_upper (priv->adjustment[GTK_ORIENTATION_VERTICAL]);
+
+  if (y < SCROLL_EDGE_SIZE && value_y > 0)
+    delta_y = - (SCROLL_EDGE_SIZE - y)/3.0;
+  else if (page_size - y < SCROLL_EDGE_SIZE && value_y + page_size < upper)
+    delta_y = (SCROLL_EDGE_SIZE - (page_size - y))/3.0;
+  else
+    delta_y = 0;
+
+  if (delta_x != 0 || delta_y != 0)
+    add_autoscroll (self, delta_x, delta_y);
+  else
+    remove_autoscroll (self);
+
+  gtk_widget_queue_draw (GTK_WIDGET (self));
+}
+
+static void
+gtk_list_base_update_rubberband_selection (GtkListBase *self)
+{
+  GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
+  GdkRectangle rect;
+  GdkRectangle alloc;
+  double value_x, value_y;
+  GtkSelectionModel *model;
+  GtkListItemManagerItem *item;
+
+  value_x = gtk_adjustment_get_value (priv->adjustment[GTK_ORIENTATION_HORIZONTAL]);
+  value_y = gtk_adjustment_get_value (priv->adjustment[GTK_ORIENTATION_VERTICAL]);
+
+  rect.x = MIN (priv->rb_x1, priv->rb_x2) - value_x;
+  rect.y = MIN (priv->rb_y1, priv->rb_y2) - value_y;
+  rect.width = ABS (priv->rb_x1 - priv->rb_x2) + 1;
+  rect.height = ABS (priv->rb_y1 - priv->rb_y2) + 1;
+
+  model = gtk_list_item_manager_get_model (priv->item_manager);
+
+  for (item = gtk_list_item_manager_get_first (priv->item_manager);
+       item != NULL;
+       item = gtk_rb_tree_node_get_next (item))
+    {
+      guint pos;
+      gboolean was_selected, selected;
+
+      if (!item->widget)
+        continue;
+
+      pos = gtk_list_item_manager_get_item_position (priv->item_manager, item);
+
+      gtk_widget_get_allocation (item->widget, &alloc);
+
+      selected = gdk_rectangle_intersect (&rect, &alloc, &alloc);
+
+      if (priv->modify)
+        {
+          was_selected = gtk_selection_model_is_selected (priv->old_selection, pos);
+          selected = selected ^ was_selected;
+        }
+
+      if (selected)
+        gtk_selection_model_select_item (model, pos, FALSE);
+      else if (!priv->extend)
+        gtk_selection_model_unselect_item (model, pos);
+    }
+}
+
+static void
+gtk_list_base_snapshot_rubberband (GtkListBase *self,
+                                   GtkSnapshot *snapshot)
+{
+  GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
+  GtkStyleContext *context;
+  GdkRectangle rect;
+  double value_x, value_y;
+
+  if (!priv->doing_rubberband)
+    return;
+
+  value_x = gtk_adjustment_get_value (priv->adjustment[GTK_ORIENTATION_HORIZONTAL]);
+  value_y = gtk_adjustment_get_value (priv->adjustment[GTK_ORIENTATION_VERTICAL]);
+
+  rect.x = MIN (priv->rb_x1, priv->rb_x2) - value_x;
+  rect.y = MIN (priv->rb_y1, priv->rb_y2) - value_y;
+  rect.width = ABS (priv->rb_x1 - priv->rb_x2) + 1;
+  rect.height = ABS (priv->rb_y1 - priv->rb_y2) + 1;
+
+  context = gtk_widget_get_style_context (GTK_WIDGET (self));
+
+  gtk_style_context_save_to_node (context, priv->rubberband_node);
+
+  gtk_snapshot_render_background (snapshot, context, rect.x, rect.y, rect.width, rect.height);
+  gtk_snapshot_render_frame (snapshot, context, rect.x, rect.y, rect.width, rect.height);
+
+  gtk_style_context_restore (context);
+
+}
+
+static void
+get_selection_modifiers (GtkGesture *gesture,
+                         gboolean   *modify,
+                         gboolean   *extend)
+{
+  GtkWidget *widget;
+  GdkEventSequence *sequence;
+  const GdkEvent *event;
+  GdkModifierType state;
+  GdkModifierType mask;
+
+  *modify = FALSE;
+  *extend = FALSE;
+
+  widget = gtk_event_controller_get_widget (GTK_EVENT_CONTROLLER (gesture));
+
+  sequence = gtk_gesture_get_last_updated_sequence (gesture);
+  event = gtk_gesture_get_last_event (gesture, sequence);
+  if (gdk_event_get_state (event, &state))
+    {
+      mask = gtk_widget_get_modifier_mask (widget, GDK_MODIFIER_INTENT_MODIFY_SELECTION);
+      if ((state & mask) == mask)
+        *modify = TRUE;
+      mask = gtk_widget_get_modifier_mask (widget, GDK_MODIFIER_INTENT_EXTEND_SELECTION);
+      if ((state & mask) == mask)
+        *extend = TRUE;
+    }
+}
+
+static void
+gtk_list_base_drag_begin (GtkGestureDrag *gesture,
+                          double          start_x,
+                          double          start_y,
+                          GtkListBase    *self)
+{
+  gboolean modify;
+  gboolean extend;
+
+  get_selection_modifiers (GTK_GESTURE (gesture), &modify, &extend);
+  gtk_list_base_start_rubberband (self, start_x, start_y, modify, extend);
+}
+
+static void
+gtk_list_base_drag_update (GtkGestureDrag *gesture,
+                           double          offset_x,
+                           double          offset_y,
+                           GtkListBase    *self)
+{
+  double start_x, start_y;
+  gtk_gesture_drag_get_start_point (gesture, &start_x, &start_y);
+  gtk_list_base_update_rubberband (self, start_x + offset_x, start_y + offset_y);
+}
+
+static void
+gtk_list_base_drag_end (GtkGestureDrag *gesture,
+                        double          offset_x,
+                        double          offset_y,
+                        GtkListBase    *self)
+{
+  gtk_list_base_drag_update (gesture, offset_x, offset_y, self);
+  gtk_list_base_stop_rubberband (self);
+}
+
+void
+gtk_list_base_set_enable_rubberband (GtkListBase *self,
+                                     gboolean     enable)
+{
+  GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
+
+  if (priv->enable_rubberband == enable)
+    return;
+
+  priv->enable_rubberband = enable;
+
+  if (enable)
+    {
+      priv->drag_gesture = gtk_gesture_drag_new ();
+      g_signal_connect (priv->drag_gesture, "drag-begin", G_CALLBACK (gtk_list_base_drag_begin), self);
+      g_signal_connect (priv->drag_gesture, "drag-update", G_CALLBACK (gtk_list_base_drag_update), self);
+      g_signal_connect (priv->drag_gesture, "drag-end", G_CALLBACK (gtk_list_base_drag_end), self);
+      gtk_widget_add_controller (GTK_WIDGET (self), GTK_EVENT_CONTROLLER (priv->drag_gesture));
+    }
+  else
+    {
+      gtk_widget_remove_controller (GTK_WIDGET (self), GTK_EVENT_CONTROLLER (priv->drag_gesture));
+      priv->drag_gesture = NULL;
+    }
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ENABLE_RUBBERBAND]);
+}
+
+gboolean
+gtk_list_base_get_enable_rubberband (GtkListBase *self)
+{
+  GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
+
+  return priv->enable_rubberband;
+}
+
 static void
 gtk_list_base_init_real (GtkListBase      *self,
                          GtkListBaseClass *g_class)
@@ -1537,4 +1931,3 @@ gtk_list_base_set_model (GtkListBase *self,
 
   return TRUE;
 }
-
diff --git a/gtk/gtklistbaseprivate.h b/gtk/gtklistbaseprivate.h
index 06757985d3..73d20151c8 100644
--- a/gtk/gtklistbaseprivate.h
+++ b/gtk/gtklistbaseprivate.h
@@ -99,5 +99,8 @@ gboolean               gtk_list_base_grab_focus_on_item         (GtkListBase
                                                                  gboolean                select,
                                                                  gboolean                modify,
                                                                  gboolean                extend);
+void                   gtk_list_base_set_enable_rubberband      (GtkListBase            *self,
+                                                                 gboolean                enable);
+gboolean               gtk_list_base_get_enable_rubberband      (GtkListBase            *self);
 
 #endif /* __GTK_LIST_BASE_PRIVATE_H__ */
diff --git a/gtk/gtklistitemwidget.c b/gtk/gtklistitemwidget.c
index a35e132d14..b4fd1db89c 100644
--- a/gtk/gtklistitemwidget.c
+++ b/gtk/gtklistitemwidget.c
@@ -30,6 +30,7 @@
 #include "gtkintl.h"
 #include "gtklistitemfactoryprivate.h"
 #include "gtklistitemprivate.h"
+#include "gtklistbaseprivate.h"
 #include "gtkmain.h"
 #include "gtkselectionmodel.h"
 #include "gtkwidget.h"
@@ -313,6 +314,8 @@ gtk_list_item_widget_click_gesture_pressed (GtkGestureClick   *gesture,
 {
   GtkListItemWidgetPrivate *priv = gtk_list_item_widget_get_instance_private (self);
   GtkWidget *widget = GTK_WIDGET (self);
+  GtkWidget * parent = gtk_widget_get_parent (widget);
+  gboolean rubberband;
 
   if (priv->list_item && !priv->list_item->selectable && !priv->list_item->activatable)
     {
@@ -320,7 +323,12 @@ gtk_list_item_widget_click_gesture_pressed (GtkGestureClick   *gesture,
       return;
     }
 
-  if (!priv->list_item || priv->list_item->selectable)
+  if (GTK_IS_LIST_BASE (parent))
+    rubberband = gtk_list_base_get_enable_rubberband (GTK_LIST_BASE (parent));
+  else
+    rubberband = FALSE;
+
+  if (!rubberband && (!priv->list_item || priv->list_item->selectable))
     {
       GdkModifierType state;
       GdkModifierType mask;


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