Compare commits

...

1 Commits

Author SHA1 Message Date
Matthias Clasen
aa4af4c946 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.
2020-07-18 20:16:05 -04:00
2 changed files with 659 additions and 0 deletions

View File

@@ -1,5 +1,6 @@
gtk_tests = [
# testname, optional extra sources
['testrange'],
['testdropdown'],
['rendernode'],
['rendernode-create-tests'],

658
tests/testrange.c Normal file
View File

@@ -0,0 +1,658 @@
#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 trough_height;
int width, 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_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;
}