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
8 changed files with 673 additions and 58 deletions

2
NEWS
View File

@@ -1,7 +1,7 @@
Overview of Changes in GTK 3.99.0
=================================
* Add GtkEditableLabel, a label that can be edited
* Add GtkEditableLabel
* Add GtkBookmarkList, a list model for bookmarks

View File

@@ -123,32 +123,6 @@ the number of listitems they create such as with gtk_grid_view_set_max_columns()
and developers running into performance problems should definitely study the
tradeoffs of those and experiment with them.
## Choosing the right model {#model-choosing}
GTK offers a wide variety of wrapping models which change or supplement an
existing model (or models) in some way. But when it comes to storing your
actual data, there are only a few ready-made choices available: #GListStore
and #GtkStringList.
GListStore is backed by a balanced tree and has performance characteristics
that are expected for that data structure. It works reasonably well for dataset
sizes in the 1,000,000 range, and can handle insertions and deletions. It uses
a cached iter to make linear access to the items fast.
GtkStringList is not a general store - it can only handle strings. It is
backed by an dynamically allocated array and has performance characteristics
that are expected for that data structure. GtkStringList is a good fit for any
place where you would otherwise use `char*[]` and works best if the dataset
is not very dynamic.
If these models don't fit your use case or scalability requirements, you
should make a custom #GListModel. It is a small interface and not very hard
to implement.
For asymptotic performance comparisons between tree- and array-based
implementations, see this
[article](https://en.wikipedia.org/wiki/Dynamic_array#Performance).
## Displaying trees {#displaying-trees}
While #GtkTreeView provided built-in support for trees, the list widgets, and

View File

@@ -857,7 +857,7 @@ gtk_constant_expression_new_for_value (const GValue *value)
/**
* gtk_constant_expression_get_value:
* @expression: (type GtkConstantExpression): a constant #GtkExpression
* @expression: a constant #GtkExpression
*
* Gets the value that a constant expression evaluates to.
*
@@ -1022,7 +1022,7 @@ gtk_object_expression_new (GObject *object)
/**
* gtk_object_expression_get_object:
* @expression: (type GtkObjectExpression): an object #GtkExpression
* @expression: an object #GtkExpression
*
* Gets the object that the expression evaluates to.
*
@@ -1345,7 +1345,7 @@ gtk_property_expression_new_for_pspec (GtkExpression *expression,
/**
* gtk_property_expression_get_expression:
* @expression: (type GtkPropertyExpression): a property #GtkExpression
* @expression: a property #GtkExpression
*
* Gets the expression specifying the object of
* a property expression.
@@ -1364,7 +1364,7 @@ gtk_property_expression_get_expression (GtkExpression *expression)
/**
* gtk_property_expression_get_pspec:
* @expression: (type GtkPropertyExpression): a property #GtkExpression
* @expression: a property #GtkExpression
*
* Gets the #GParamSpec specifying the property of
* a property expression.

View File

@@ -36,9 +36,6 @@
*
* The objects in the model have a "string" property.
*
* GtkStringList is well-suited for any place where you would
* typically use a `char*[]`, but need a list model.
*
* # GtkStringList as GtkBuildable
*
* The GtkStringList implementation of the GtkBuildable interface

View File

@@ -268,6 +268,7 @@ perform_titlebar_action (GtkWindowHandle *self,
GtkSettings *settings;
gchar *action = NULL;
gboolean retval = TRUE;
GtkActionMuxer *context;
settings = gtk_widget_get_settings (GTK_WIDGET (self));
switch (button)
@@ -286,21 +287,23 @@ perform_titlebar_action (GtkWindowHandle *self,
break;
}
context = _gtk_widget_get_action_muxer (GTK_WIDGET (self), TRUE);
if (action == NULL)
retval = FALSE;
else if (g_str_equal (action, "none"))
retval = FALSE;
/* treat all maximization variants the same */
else if (g_str_has_prefix (action, "toggle-maximize"))
gtk_widget_activate_action (GTK_WIDGET (self),
"window.toggle-maximized",
NULL);
g_action_group_activate_action (G_ACTION_GROUP (context),
"window.toggle-maximized",
NULL);
else if (g_str_equal (action, "lower"))
lower_window (self);
else if (g_str_equal (action, "minimize"))
gtk_widget_activate_action (GTK_WIDGET (self),
"window.minimize",
NULL);
g_action_group_activate_action (G_ACTION_GROUP (context),
"window.minimize",
NULL);
else if (g_str_equal (action, "menu"))
do_popup (self, event);
else

View File

@@ -3065,24 +3065,6 @@ row {
}
}
/********************************************
* Treeview like editable lists with columns *
********************************************/
listview row:not(:selected) cell editablelabel:not(.editing):focus-within {
outline: 2px solid $focus_border_color;
}
listview row:not(:selected) cell editablelabel.editing:focus-within {
outline: 2px solid $selected_bg_color;
}
listview row:not(:selected) cell editablelabel.editing text selection {
color: $selected_fg_color;
background-color: $selected_bg_color;
}
/*********************
* App Notifications *

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;
}