[gtk/range-selector] Add a quick demo for a range selector



commit 6e029c1877ca173cb3b99f904fb8eea6dbbc7cd0
Author: Matthias Clasen <mclasen redhat com>
Date:   Sat Jul 18 18:23:57 2020 -0400

    Add a quick demo for a range selector
    
    This is a widget that looks like a GtkScale,
    except that it has two sliders instead of one.
    It lets you select a subrange of a given range.
    
    Only horizontal for now, and reusing the GtkScale
    styling.

 tests/meson.build |   1 +
 tests/testrange.c | 659 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 660 insertions(+)
---
diff --git a/tests/meson.build b/tests/meson.build
index 4618df6b97..2e6e66f44a 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -1,5 +1,6 @@
 gtk_tests = [
   # testname, optional extra sources
+  ['testrange'],
   ['testdropdown'],
   ['rendernode'],
   ['rendernode-create-tests'],
diff --git a/tests/testrange.c b/tests/testrange.c
new file mode 100644
index 0000000000..59e9b6e74a
--- /dev/null
+++ b/tests/testrange.c
@@ -0,0 +1,659 @@
+#include <gtk/gtk.h>
+
+G_DECLARE_FINAL_TYPE (DemoSlider, demo_slider, DEMO, SLIDER, GtkWidget)
+
+struct _DemoSlider
+{
+  GtkWidget parent_instance;
+};
+
+struct _DemoSliderClass
+{
+  GtkWidgetClass parent_class;
+};
+
+G_DECLARE_FINAL_TYPE (DemoHighlight, demo_highlight, DEMO, HIGHLIGHT, GtkWidget)
+
+struct _DemoHighlight
+{
+  GtkWidget parent_instance;
+};
+
+struct _DemoHighlightClass
+{
+  GtkWidgetClass parent_class;
+};
+
+G_DECLARE_FINAL_TYPE (DemoTrough, demo_trough, DEMO, TROUGH, GtkWidget)
+
+struct _DemoTrough
+{
+  GtkWidget parent_instance;
+};
+
+struct _DemoTroughClass
+{
+  GtkWidgetClass parent_class;
+};
+
+G_DECLARE_FINAL_TYPE (DemoWidget, demo_widget, DEMO, WIDGET, GtkWidget)
+
+struct _DemoWidget
+{
+  GtkWidget parent_instance;
+
+  GtkWidget *trough;
+  GtkWidget *highlight;
+  GtkWidget *min_slider;
+  GtkWidget *max_slider;
+
+  GtkWidget *grab_location;
+
+  double range_min;
+  double range_max;
+  double min_value;
+  double max_value;
+
+  gboolean shift;
+};
+
+enum
+{
+  PROP_RANGE_MIN = 1,
+  PROP_RANGE_MAX,
+  PROP_MIN_VALUE,
+  PROP_MAX_VALUE,
+  NUM_PROPERTIES
+};
+
+static GParamSpec *properties[NUM_PROPERTIES] = { NULL };
+
+struct _DemoWidgetClass
+{
+  GtkWidgetClass parent_class;
+};
+
+G_DEFINE_TYPE (DemoSlider, demo_slider, GTK_TYPE_WIDGET)
+
+static void
+demo_slider_init (DemoSlider *slider)
+{
+}
+
+static void
+demo_slider_class_init (DemoSliderClass *class)
+{
+  gtk_widget_class_set_css_name (GTK_WIDGET_CLASS (class), "slider");
+}
+
+static GtkWidget *
+demo_slider_new (void)
+{
+  return g_object_new (demo_slider_get_type (), NULL);
+}
+
+G_DEFINE_TYPE (DemoHighlight, demo_highlight, GTK_TYPE_WIDGET)
+
+static void
+demo_highlight_init (DemoHighlight *highlight)
+{
+}
+
+static void
+demo_highlight_class_init (DemoHighlightClass *class)
+{
+  gtk_widget_class_set_css_name (GTK_WIDGET_CLASS (class), "highlight");
+}
+
+static GtkWidget *
+demo_highlight_new (void)
+{
+  return g_object_new (demo_highlight_get_type (), NULL);
+}
+
+G_DEFINE_TYPE (DemoTrough, demo_trough, GTK_TYPE_WIDGET)
+
+static void
+demo_trough_init (DemoTrough *trough)
+{
+}
+
+static void
+demo_trough_measure (GtkWidget      *widget,
+                     GtkOrientation  orientation,
+                     int             for_size,
+                     int            *minimum,
+                     int            *natural,
+                     int            *minimum_baseline,
+                     int            *natural_baseline)
+{
+  DemoWidget *demo = DEMO_WIDGET (gtk_widget_get_parent (widget));
+  int min1, nat1;
+  int min2, nat2;
+  int min3, nat3;
+
+  gtk_widget_measure (demo->min_slider,
+                      orientation, -1,
+                      &min1, &nat1,
+                      NULL, NULL);
+
+  gtk_widget_measure (demo->max_slider,
+                      orientation, -1,
+                      &min2, &nat2,
+                      NULL, NULL);
+
+  gtk_widget_measure (demo->highlight,
+                      orientation, for_size,
+                      &min3, &nat3,
+                      NULL, NULL);
+
+  if (orientation == GTK_ORIENTATION_HORIZONTAL)
+    {
+      *minimum = MAX (min1 + min2, min3);
+      *natural = MAX (nat1 + nat2, nat3);
+    }
+  else
+    {
+      *minimum = MAX (MAX (min1, min2), min3);
+      *natural = MAX (MAX (nat1, nat2), min3);
+    }
+}
+
+static void
+allocate_slider (DemoWidget *demo,
+                 GtkWidget  *slider,
+                 int         x)
+{
+  int width, height;
+  int trough_width, trough_height;
+  int y;
+
+  gtk_widget_measure (slider,
+                      GTK_ORIENTATION_HORIZONTAL, -1,
+                      &width, NULL,
+                      NULL, NULL);
+  gtk_widget_measure (slider,
+                      GTK_ORIENTATION_VERTICAL, -1,
+                      &height, NULL,
+                      NULL, NULL);
+
+  trough_width = gtk_widget_get_width (demo->trough);
+  trough_height = gtk_widget_get_height (demo->trough);
+
+  y = floor ((trough_height - height) / 2);
+
+  gtk_widget_size_allocate (slider,
+                            &(GtkAllocation) { x, y, width, height},
+                            -1);
+}
+
+static void
+demo_trough_size_allocate (GtkWidget *widget,
+                           int        width,
+                           int        height,
+                           int        baseline)
+{
+  DemoWidget *demo = DEMO_WIDGET (gtk_widget_get_parent (widget));
+  int min, max;
+
+  min = floor (width * (demo->min_value - demo->range_min) / (demo->range_max - demo->range_min));
+  max = floor (width * (demo->max_value - demo->range_min) / (demo->range_max - demo->range_min));
+
+  allocate_slider (demo, demo->min_slider, min);
+  allocate_slider (demo, demo->max_slider, max);
+
+  gtk_widget_size_allocate (demo->highlight,
+                            &(GtkAllocation) { min, 0, max - min, height},
+                            -1);
+}
+
+static void
+demo_trough_class_init (DemoTroughClass *class)
+{
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (class);
+
+  widget_class->measure = demo_trough_measure;
+  widget_class->size_allocate = demo_trough_size_allocate;
+
+  gtk_widget_class_set_css_name (widget_class, "trough");
+}
+
+static GtkWidget *
+demo_trough_new (void)
+{
+  return g_object_new (demo_trough_get_type (), NULL);
+}
+
+G_DEFINE_TYPE (DemoWidget, demo_widget, GTK_TYPE_WIDGET)
+
+static void
+click_gesture_pressed (GtkGestureClick *gesture,
+                       guint            n_press,
+                       double           x,
+                       double           y,
+                       DemoWidget      *demo)
+{
+  guint button;
+  GdkModifierType state;
+
+  demo->grab_location = gtk_widget_pick (GTK_WIDGET (demo), x, y, 0);
+
+  button = gtk_gesture_single_get_current_button (GTK_GESTURE_SINGLE (gesture));
+  state = gtk_event_controller_get_current_event_state (GTK_EVENT_CONTROLLER (gesture));
+
+  demo->shift = (button == GDK_BUTTON_PRIMARY && ((state & GDK_SHIFT_MASK) != 0)) ||
+                 button == GDK_BUTTON_SECONDARY;
+}
+
+static void
+click_gesture_released (GtkGestureClick *gesture,
+                        guint            n_press,
+                        double           x,
+                        double           y,
+                        DemoWidget      *demo)
+{
+  demo->grab_location = NULL;
+}
+
+static void
+drag_gesture_begin (GtkGestureDrag *gesture,
+                    double          offset_x,
+                    double          offset_y,
+                    DemoWidget     *demo)
+{
+  if (demo->grab_location == demo->min_slider ||
+      demo->grab_location == demo->max_slider)
+    gtk_gesture_set_state (GTK_GESTURE (gesture), GTK_EVENT_SEQUENCE_CLAIMED);
+}
+
+static void
+drag_gesture_update (GtkGestureDrag *gesture,
+                     double          offset_x,
+                     double          offset_y,
+                     DemoWidget     *demo)
+{
+  double start_x, start_y;
+  int width;
+  double value;
+  double size;
+
+  gtk_gesture_drag_get_start_point (gesture, &start_x, &start_y);
+
+  width = gtk_widget_get_width (GTK_WIDGET (demo));
+
+  value = ((start_x + offset_x) / width) * (demo->range_max - demo->range_min) + demo->range_min;
+  value = CLAMP (value, demo->range_min, demo->range_max);
+  size = demo->max_value - demo->min_value;
+
+  if (demo->shift)
+    {
+      if (demo->grab_location == demo->min_slider)
+        {
+          demo->max_value = MIN (demo->range_max, value + size);
+          demo->min_value = demo->max_value - size;
+        }
+      else if (demo->grab_location == demo->max_slider)
+        {
+          demo->min_value = MAX (demo->range_min, value - size);
+          demo->max_value = demo->min_value + size;
+        }
+    }
+  else
+    {
+      if (demo->grab_location == demo->min_slider)
+        {
+          demo->min_value = value;
+          demo->max_value = MAX (demo->max_value, value);
+        }
+      else if (demo->grab_location == demo->max_slider)
+        {
+          demo->min_value = MIN (demo->min_value, value);
+          demo->max_value = value;
+        }
+    }
+
+  g_object_notify_by_pspec (G_OBJECT (demo), properties[PROP_MIN_VALUE]);
+  g_object_notify_by_pspec (G_OBJECT (demo), properties[PROP_MAX_VALUE]);
+
+  gtk_widget_queue_allocate (GTK_WIDGET (demo));
+}
+
+static void
+demo_widget_init (DemoWidget *demo)
+{
+  GtkGesture *click_gesture, *drag_gesture;
+
+  demo->range_min = 0;
+  demo->range_max = 0;
+  demo->min_value = 0;
+  demo->max_value = 0;
+
+  demo->trough = demo_trough_new ();
+  gtk_widget_set_parent (demo->trough, GTK_WIDGET (demo));
+  demo->highlight = demo_highlight_new ();
+  gtk_widget_set_parent (demo->highlight, demo->trough);
+  demo->min_slider = demo_slider_new ();
+  gtk_widget_set_parent (demo->min_slider, demo->trough);
+  demo->max_slider = demo_slider_new ();
+  gtk_widget_set_parent (demo->max_slider, demo->trough);
+
+  drag_gesture = gtk_gesture_drag_new ();
+  gtk_gesture_single_set_button (GTK_GESTURE_SINGLE (drag_gesture), 0);
+  g_signal_connect (drag_gesture, "drag-begin", G_CALLBACK (drag_gesture_begin), demo);
+  g_signal_connect (drag_gesture, "drag-update", G_CALLBACK (drag_gesture_update), demo);
+  gtk_widget_add_controller (GTK_WIDGET (demo), GTK_EVENT_CONTROLLER (drag_gesture));
+
+  click_gesture = gtk_gesture_click_new ();
+  gtk_gesture_single_set_button (GTK_GESTURE_SINGLE (click_gesture), 0);
+  g_signal_connect (click_gesture, "pressed", G_CALLBACK (click_gesture_pressed), demo);
+  g_signal_connect (click_gesture, "released", G_CALLBACK (click_gesture_released), demo);
+  gtk_widget_add_controller (GTK_WIDGET (demo), GTK_EVENT_CONTROLLER (click_gesture));
+
+  gtk_gesture_group (click_gesture, drag_gesture);
+}
+
+static void
+demo_widget_measure (GtkWidget      *widget,
+                     GtkOrientation  orientation,
+                     int             for_size,
+                     int            *minimum,
+                     int            *natural,
+                     int            *minimum_baseline,
+                     int            *natural_baseline)
+{
+  DemoWidget *demo = DEMO_WIDGET (widget);
+
+  gtk_widget_measure (demo->trough,
+                      orientation, -1,
+                      minimum, natural,
+                      NULL, NULL);
+}
+
+static void
+demo_widget_size_allocate (GtkWidget *widget,
+                           int        width,
+                           int        height,
+                           int        baseline)
+{
+  DemoWidget *demo = DEMO_WIDGET (widget);
+  int min_height;
+
+  gtk_widget_measure (demo->trough,
+                      GTK_ORIENTATION_VERTICAL, -1,
+                      &min_height, NULL,
+                      NULL, NULL);
+
+  gtk_widget_size_allocate (demo->trough,
+                            &(GtkAllocation) { 0, 0, width, min_height},
+                            -1);
+}
+
+static void
+demo_widget_dispose (GObject *object)
+{
+  DemoWidget *demo = DEMO_WIDGET (object);
+
+  g_clear_pointer (&demo->trough, gtk_widget_unparent);
+
+  G_OBJECT_CLASS (demo_widget_parent_class)->dispose (object);
+}
+
+static void
+demo_widget_set_range (DemoWidget *demo,
+                       double      range_min,
+                       double      range_max)
+{
+  double value;
+
+  g_return_if_fail (range_min <= range_max);
+
+  g_object_freeze_notify (G_OBJECT (demo));
+
+  if (demo->range_min != range_min)
+    {
+      demo->range_min = range_min;
+      g_object_notify_by_pspec (G_OBJECT (demo), properties[PROP_RANGE_MIN]);
+    }
+
+  if (demo->range_max != range_max)
+    {
+      demo->range_max = range_max;
+      g_object_notify_by_pspec (G_OBJECT (demo), properties[PROP_RANGE_MAX]);
+    }
+
+  value = CLAMP (demo->min_value, range_min, range_max);
+  if (demo->min_value != value)
+    {
+      demo->min_value = value;
+      g_object_notify_by_pspec (G_OBJECT (demo), properties[PROP_MIN_VALUE]);
+    }
+
+  value = CLAMP (demo->max_value, range_min, range_max);
+  if (demo->max_value != value)
+    {
+      demo->max_value = value;
+      g_object_notify_by_pspec (G_OBJECT (demo), properties[PROP_MAX_VALUE]);
+    }
+
+  g_object_thaw_notify (G_OBJECT (demo));
+}
+
+static void
+demo_widget_set_values (DemoWidget *demo,
+                        double      min_value,
+                        double      max_value)
+{
+  double value;
+
+  g_return_if_fail (min_value <= max_value);
+
+  g_object_freeze_notify (G_OBJECT (demo));
+
+  value = CLAMP (min_value, demo->range_min, demo->range_max);
+  if (demo->min_value != value)
+    {
+      demo->min_value = value;
+      g_object_notify_by_pspec (G_OBJECT (demo), properties[PROP_MIN_VALUE]);
+    }
+
+  value = CLAMP (max_value, demo->range_min, demo->range_max);
+  if (demo->max_value != value)
+    {
+      demo->max_value = value;
+      g_object_notify_by_pspec (G_OBJECT (demo), properties[PROP_MAX_VALUE]);
+    }
+
+  g_object_thaw_notify (G_OBJECT (demo));
+}
+
+static void
+demo_widget_set_property (GObject      *object,
+                          guint         prop_id,
+                          const GValue *value,
+                          GParamSpec   *pspec)
+{
+  DemoWidget *demo = DEMO_WIDGET (object);
+  double v;
+
+  switch (prop_id)
+    {
+    case PROP_RANGE_MIN:
+      v = g_value_get_double (value);
+      demo_widget_set_range (demo, v, MAX (v, demo->range_max));
+      break;
+
+    case PROP_RANGE_MAX:
+      v = g_value_get_double (value);
+      demo_widget_set_range (demo, MIN (v, demo->range_min), v);
+      break;
+
+    case PROP_MIN_VALUE:
+      v = g_value_get_double (value);
+      demo_widget_set_values (demo, v, MAX (v, demo->max_value));
+      break;
+
+    case PROP_MAX_VALUE:
+      v = g_value_get_double (value);
+      demo_widget_set_values (demo, MIN (v, demo->min_value), v);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+      break;
+    }
+}
+
+static void
+demo_widget_get_property (GObject    *object,
+                          guint       prop_id,
+                          GValue     *value,
+                          GParamSpec *pspec)
+{
+  DemoWidget *demo = DEMO_WIDGET (object);
+
+  switch (prop_id)
+    {
+    case PROP_RANGE_MIN:
+      g_value_set_double (value, demo->range_min);
+      break;
+
+    case PROP_RANGE_MAX:
+      g_value_set_double (value, demo->range_max);
+      break;
+
+    case PROP_MIN_VALUE:
+      g_value_set_double (value, demo->min_value);
+      break;
+
+    case PROP_MAX_VALUE:
+      g_value_set_double (value, demo->max_value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+      break;
+    }
+}
+
+static void
+demo_widget_class_init (DemoWidgetClass *class)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (class);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (class);
+
+  object_class->dispose = demo_widget_dispose;
+  object_class->set_property = demo_widget_set_property;
+  object_class->get_property = demo_widget_get_property;
+
+  widget_class->measure = demo_widget_measure;
+  widget_class->size_allocate = demo_widget_size_allocate;
+
+  properties[PROP_RANGE_MIN] = g_param_spec_double ("range-min",
+                                                    "Range Min",
+                                                    "Range Min",
+                                                    -G_MAXDOUBLE, G_MAXDOUBLE, 0,
+                                                    G_PARAM_READWRITE);
+  properties[PROP_RANGE_MAX] = g_param_spec_double ("range-max",
+                                                    "Range Max",
+                                                    "Range Max",
+                                                    -G_MAXDOUBLE, G_MAXDOUBLE, 0,
+                                                    G_PARAM_READWRITE);
+  properties[PROP_MIN_VALUE] = g_param_spec_double ("min-value",
+                                                    "Min Value",
+                                                    "Min Value",
+                                                    -G_MAXDOUBLE, G_MAXDOUBLE, 0,
+                                                    G_PARAM_READWRITE);
+  properties[PROP_MAX_VALUE] = g_param_spec_double ("max-value",
+                                                    "Max Value",
+                                                    "Max Value",
+                                                    -G_MAXDOUBLE, G_MAXDOUBLE, 0,
+                                                    G_PARAM_READWRITE);
+
+  gtk_widget_class_set_css_name (widget_class, "scale");
+}
+
+static GtkWidget *
+demo_widget_new (void)
+{
+  return g_object_new (demo_widget_get_type (), NULL);
+}
+
+static void
+update_range_label (GObject    *object,
+                    GParamSpec *pspec,
+                    gpointer    data)
+{
+  DemoWidget *demo = DEMO_WIDGET (object);
+
+  if (pspec->name == g_intern_static_string ("range-min") ||
+      pspec->name == g_intern_static_string ("range-max"))
+    {
+      char *text;
+
+      text = g_strdup_printf ("Allowed values: [%.1f, %.1f]\n", demo->range_min, demo->range_max);
+      gtk_label_set_label (GTK_LABEL (data), text);
+      g_free (text);
+    }
+}
+
+static void
+update_values_label (GObject    *object,
+                     GParamSpec *pspec,
+                     gpointer    data)
+{
+  DemoWidget *demo = DEMO_WIDGET (object);
+
+  if (pspec->name == g_intern_static_string ("min-value") ||
+      pspec->name == g_intern_static_string ("max-value"))
+    {
+      char *text;
+
+      text = g_strdup_printf ("Selected range: [%.1f, %.1f]\n", demo->min_value, demo->max_value);
+      gtk_label_set_label (GTK_LABEL (data), text);
+      g_free (text);
+    }
+}
+
+int
+main (int argc, char *argv[])
+{
+  GtkWindow *window;
+  GtkWidget *box;
+  GtkWidget *demo;
+  GtkWidget *label;
+
+  gtk_init ();
+
+  window = GTK_WINDOW (gtk_window_new ());
+  gtk_window_set_title (window, "Pick a range");
+
+  box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 10);
+  gtk_window_set_child (window, box);
+
+  demo = demo_widget_new ();
+  gtk_widget_set_halign (demo, GTK_ALIGN_FILL);
+  gtk_widget_set_valign (demo, GTK_ALIGN_CENTER);
+  gtk_widget_set_hexpand (demo, TRUE);
+
+  gtk_box_append (GTK_BOX (box), demo);
+
+  label = gtk_label_new ("");
+  gtk_label_set_xalign (GTK_LABEL (label), 0);
+  g_signal_connect (demo, "notify", G_CALLBACK (update_range_label), label);
+  gtk_box_append (GTK_BOX (box), label);
+
+  label = gtk_label_new ("");
+  gtk_label_set_xalign (GTK_LABEL (label), 0);
+  g_signal_connect (demo, "notify", G_CALLBACK (update_values_label), label);
+  gtk_box_append (GTK_BOX (box), label);
+
+  demo_widget_set_range (DEMO_WIDGET (demo), 0, 1000);
+  demo_widget_set_values (DEMO_WIDGET (demo), 100, 500);
+
+  gtk_window_present (window);
+
+  while (g_list_model_get_n_items (gtk_window_get_toplevels ()) > 0)
+    g_main_context_iteration (NULL, TRUE);
+
+  return 0;
+}


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