diff options
author | Benjamin Otte <otte@redhat.com> | 2019-11-24 08:07:33 +0100 |
---|---|---|
committer | Matthias Clasen <mclasen@redhat.com> | 2020-05-30 19:26:46 -0400 |
commit | 22e6fa3a64024aef798138db7b6e9a3e0ccd1565 (patch) | |
tree | 612d58821a94440054e31f69ff59d67f1e57d27a /demos | |
parent | c337887e29bc1bd9da374ddd3a00d92a774bf32c (diff) | |
download | gtk+-22e6fa3a64024aef798138db7b6e9a3e0ccd1565.tar.gz |
gtk-demo: Add a Clocks demo
This demo is meant to showcase expressions.
It also needs the fixes in glib 2.64 to work properly.
Diffstat (limited to 'demos')
-rw-r--r-- | demos/gtk-demo/demo.gresource.xml | 1 | ||||
-rw-r--r-- | demos/gtk-demo/listview_clocks.c | 487 | ||||
-rw-r--r-- | demos/gtk-demo/meson.build | 3 |
3 files changed, 490 insertions, 1 deletions
diff --git a/demos/gtk-demo/demo.gresource.xml b/demos/gtk-demo/demo.gresource.xml index bfaca7e81a..5e18b3b195 100644 --- a/demos/gtk-demo/demo.gresource.xml +++ b/demos/gtk-demo/demo.gresource.xml @@ -216,6 +216,7 @@ <file>links.c</file> <file>listbox.c</file> <file>listview_applauncher.c</file> + <file>listview_clocks.c</file> <file>listview_filebrowser.c</file> <file>listview_minesweeper.c</file> <file>listview_settings.c</file> diff --git a/demos/gtk-demo/listview_clocks.c b/demos/gtk-demo/listview_clocks.c new file mode 100644 index 0000000000..3254428d7b --- /dev/null +++ b/demos/gtk-demo/listview_clocks.c @@ -0,0 +1,487 @@ +/* Lists/Clocks + * + * This demo displays the time in different timezones. + * + * The goal is to show how to set up expressions that track changes + * in objects and make them update widgets. + * + * For that, we create a GtkClock object that updates its time every + * second and then use various ways to display that time. + * + * Typically, this will be done using GtkBuilder .ui files with the + * help of the <binding> tag, but this demo shows the code that runs + * behind that. + */ + +#include <gtk/gtk.h> + +#define GTK_TYPE_CLOCK (gtk_clock_get_type ()) +G_DECLARE_FINAL_TYPE (GtkClock, gtk_clock, GTK, CLOCK, GObject) + +/* This is our object. It's just a timezone */ +typedef struct _GtkClock GtkClock; +struct _GtkClock +{ + GObject parent_instance; + + /* We allow this to be NULL for the local timezone */ + GTimeZone *timezone; + /* Name of the location we're displaying time for */ + char *location; +}; + +enum { + PROP_0, + PROP_LOCATION, + PROP_TIME, + PROP_TIMEZONE, + + N_PROPS +}; + +/* This function returns the current time in the clock's timezone. + * Note that this returns a new object every time, so we need to + * remember to unref it after use. */ +static GDateTime * +gtk_clock_get_time (GtkClock *clock) +{ + if (clock->timezone) + return g_date_time_new_now (clock->timezone); + else + return g_date_time_new_now_local (); +} + +/* Here, we implement the functionality required by the GdkPaintable interface. + * This way we have a trivial way to display an analog clock. + * It also allows demonstrating how to directly use objects in the listview + * later by making this object do something interesting. */ +static void +gtk_clock_snapshot (GdkPaintable *paintable, + GdkSnapshot *snapshot, + double width, + double height) +{ + GtkClock *self = GTK_CLOCK (paintable); + GDateTime *time; + GskRoundedRect outline; + +#define BLACK ((GdkRGBA) { 0, 0, 0, 1 }) + + /* save/restore() is necessary so we can undo the transforms we start + * out with. */ + gtk_snapshot_save (snapshot); + + /* First, we move the (0, 0) point to the center of the area so + * we can draw everything relative to it. */ + gtk_snapshot_translate (snapshot, &GRAPHENE_POINT_INIT (width / 2, height / 2)); + /* Next we scale it, so that we can pretend that the clock is + * 100px in size. That way, we don't need to do any complicated + * math later. + * We use MIN() here so that we use the smaller dimension for sizing. + * That way we don't overdraw but keep the aspect ratio. */ + gtk_snapshot_scale (snapshot, MIN (width, height) / 100.0, MIN (width, height) / 100.0); + /* Now we have a circle with diameter 100px (and radius 50px) that + * has its (0, 0) point at the center. + * Let's draw a simple clock into it. */ + + time = gtk_clock_get_time (self); + + /* First, draw a circle. This is a neat little trick to draw a circle + * without requiring Cairo. */ + gsk_rounded_rect_init_from_rect (&outline, &GRAPHENE_RECT_INIT(-50, -50, 100, 100), 50); + gtk_snapshot_append_border (snapshot, + &outline, + (float[4]) { 4, 4, 4, 4 }, + (GdkRGBA [4]) { BLACK, BLACK, BLACK, BLACK }); + + /* Next, draw the hour hand. + * We do this using tranforms again: Instead of computing where the angle points + * to, we just rotate everything and then draw the hand as if if was :00. + * We don't even need to care about am/pm here because rotations just work. */ + gtk_snapshot_save (snapshot); + gtk_snapshot_rotate (snapshot, 30 * g_date_time_get_hour (time) + 0.5 * g_date_time_get_minute (time)); + gsk_rounded_rect_init_from_rect (&outline, &GRAPHENE_RECT_INIT(-2, -23, 4, 25), 2); + gtk_snapshot_push_rounded_clip (snapshot, &outline); + gtk_snapshot_append_color (snapshot, &BLACK, &outline.bounds); + gtk_snapshot_pop (snapshot); + gtk_snapshot_restore (snapshot); + + /* And the same as above for the minute hand. Just make this one longer + * so people can tell the hands apart. */ + gtk_snapshot_save (snapshot); + gtk_snapshot_rotate (snapshot, 6 * g_date_time_get_minute (time)); + gsk_rounded_rect_init_from_rect (&outline, &GRAPHENE_RECT_INIT(-2, -43, 4, 45), 2); + gtk_snapshot_push_rounded_clip (snapshot, &outline); + gtk_snapshot_append_color (snapshot, &BLACK, &outline.bounds); + gtk_snapshot_pop (snapshot); + gtk_snapshot_restore (snapshot); + + /* and finally, the second indicator. */ + gtk_snapshot_save (snapshot); + gtk_snapshot_rotate (snapshot, 6 * g_date_time_get_second (time)); + gsk_rounded_rect_init_from_rect (&outline, &GRAPHENE_RECT_INIT(-2, -43, 4, 10), 2); + gtk_snapshot_push_rounded_clip (snapshot, &outline); + gtk_snapshot_append_color (snapshot, &BLACK, &outline.bounds); + gtk_snapshot_pop (snapshot); + gtk_snapshot_restore (snapshot); + + /* And finally, don't forget to restore the initial save() that we did for + * the initial transformations. */ + gtk_snapshot_restore (snapshot); + + g_date_time_unref (time); +} + +/* Our desired size is 100px. That sounds okay for an analog clock */ +static int +gtk_clock_get_intrinsic_width (GdkPaintable *paintable) +{ + return 100; +} + +static int +gtk_clock_get_intrinsic_height (GdkPaintable *paintable) +{ + return 100; +} + +/* Initialize the paintable interface. This way we turn our clock objects + * into objects that can be drawn. + * There are more functions to this interface to define desired size, + * but this is enough. + */ +static void +gtk_clock_paintable_init (GdkPaintableInterface *iface) +{ + iface->snapshot = gtk_clock_snapshot; + iface->get_intrinsic_width = gtk_clock_get_intrinsic_width; + iface->get_intrinsic_height = gtk_clock_get_intrinsic_height; +} + +/* Finally, we define the type. The important part is adding the paintable + * interface, so GTK knows that this object can indeed be drawm. + */ +G_DEFINE_TYPE_WITH_CODE (GtkClock, gtk_clock, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (GDK_TYPE_PAINTABLE, + gtk_clock_paintable_init)) + +static GParamSpec *properties[N_PROPS] = { NULL, }; + +static void +gtk_clock_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + GtkClock *self = GTK_CLOCK (object); + + switch (property_id) + { + case PROP_LOCATION: + g_value_set_string (value, self->location); + break; + + case PROP_TIME: + g_value_take_boxed (value, gtk_clock_get_time (self)); + break; + + case PROP_TIMEZONE: + g_value_set_boxed (value, self->timezone); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +gtk_clock_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + GtkClock *self = GTK_CLOCK (object); + + switch (property_id) + { + case PROP_LOCATION: + self->location = g_value_dup_string (value); + break; + + case PROP_TIMEZONE: + self->timezone = g_value_dup_boxed (value); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +/* This is the list of all the ticking clocks */ +static GSList *ticking_clocks = NULL; +/* This is the id of the timeout source that is updating all ticking clocks */ +static guint ticking_clock_id = 0; + +/* Every second, this function is called to tell everybody that the + * clocks are ticking. + */ +static gboolean +gtk_clock_tick (gpointer unused) +{ + GSList *l; + + for (l = ticking_clocks; l; l = l->next) + { + GtkClock *clock = l->data; + + /* We will now return a different value for the time porperty, + * so notify about that. + */ + g_object_notify_by_pspec (G_OBJECT (clock), properties[PROP_TIME]); + /* We will also draw the hands of the clock differently. + * So notify about that, too. + */ + gdk_paintable_invalidate_contents (GDK_PAINTABLE (clock)); + } + + return G_SOURCE_CONTINUE; +} + +static void +gtk_clock_stop_ticking (GtkClock *self) +{ + ticking_clocks = g_slist_remove (ticking_clocks, self); + + /* If no clock is remaining, stop running the tick updates */ + if (ticking_clocks == NULL && ticking_clock_id != 0) + g_clear_handle_id (&ticking_clock_id, g_source_remove); +} + +static void +gtk_clock_start_ticking (GtkClock *self) +{ + /* if no clock is ticking yet, start */ + if (ticking_clock_id == 0) + ticking_clock_id = g_timeout_add_seconds (1, gtk_clock_tick, NULL); + + ticking_clocks = g_slist_prepend (ticking_clocks, self); +} + +static void +gtk_clock_finalize (GObject *object) +{ + GtkClock *self = GTK_CLOCK (object); + + gtk_clock_stop_ticking (self); + + g_free (self->location); + g_clear_pointer (&self->timezone, g_time_zone_unref); + + G_OBJECT_CLASS (gtk_clock_parent_class)->finalize (object); +} + +static void +gtk_clock_class_init (GtkClockClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + gobject_class->get_property = gtk_clock_get_property; + gobject_class->set_property = gtk_clock_set_property; + gobject_class->finalize = gtk_clock_finalize; + + properties[PROP_LOCATION] = + g_param_spec_string ("location", NULL, NULL, NULL, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY); + properties[PROP_TIME] = + g_param_spec_boxed ("time", NULL, NULL, G_TYPE_DATE_TIME, G_PARAM_READABLE); + properties[PROP_TIMEZONE] = + g_param_spec_boxed ("timezone", NULL, NULL, G_TYPE_TIME_ZONE, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY); + + g_object_class_install_properties (gobject_class, N_PROPS, properties); +} + +static void +gtk_clock_init (GtkClock *self) +{ + gtk_clock_start_ticking (self); +} + +static GtkClock * +gtk_clock_new (const char *location, + GTimeZone *timezone) +{ + GtkClock *result; + + result = g_object_new (GTK_TYPE_CLOCK, + "location", location, + "timezone", timezone, + NULL); + + g_clear_pointer (&timezone, g_time_zone_unref); + + return result; +} + +static GListModel * +create_clocks_model (void) +{ + GListStore *result; + GtkClock *clock; + + result = g_list_store_new (GTK_TYPE_CLOCK); + + /* local time */ + clock = gtk_clock_new ("local", NULL); + g_list_store_append (result, clock); + g_object_unref (clock); + /* UTC time */ + clock = gtk_clock_new ("UTC", g_time_zone_new_utc ()); + g_list_store_append (result, clock); + g_object_unref (clock); + /* A bunch of timezones with GTK hackers */ + clock = gtk_clock_new ("San Francisco", g_time_zone_new ("America/Los_Angeles")); + g_list_store_append (result, clock); + g_object_unref (clock); + clock = gtk_clock_new ("Boston", g_time_zone_new ("America/New_York")); + g_list_store_append (result, clock); + g_object_unref (clock); + clock = gtk_clock_new ("London", g_time_zone_new ("Europe/London")); + g_list_store_append (result, clock); + g_object_unref (clock); + clock = gtk_clock_new ("Berlin", g_time_zone_new ("Europe/Berlin")); + g_list_store_append (result, clock); + g_object_unref (clock); + clock = gtk_clock_new ("Moscow", g_time_zone_new ("Europe/Moscow")); + g_list_store_append (result, clock); + g_object_unref (clock); + clock = gtk_clock_new ("New Delhi", g_time_zone_new ("Asia/Kolkata")); + g_list_store_append (result, clock); + g_object_unref (clock); + clock = gtk_clock_new ("Shanghai", g_time_zone_new ("Asia/Shanghai")); + g_list_store_append (result, clock); + g_object_unref (clock); + + return G_LIST_MODEL (result); +} + +static char * +convert_time_to_string (GObject *image, + GDateTime *time, + gpointer unused) +{ + return g_date_time_format (time, "%x\n%X"); +} + +/* And this function is the crux for this whole demo. + * It shows how to use expressions to set up bindings. + */ +static void +setup_listitem_cb (GtkListItemFactory *factory, + GtkListItem *list_item) +{ + GtkWidget *box, *picture, *location_label, *time_label; + GtkExpression *clock_expression, *expression; + + box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0); + gtk_list_item_set_child (list_item, box); + + /* First, we create an expression that gets us the clock from the listitem: + * 1. Create an expression that gets the list item. + * 2. Use that expression's "item" property to get the clock + */ + expression = gtk_constant_expression_new (GTK_TYPE_LIST_ITEM, list_item); + clock_expression = gtk_property_expression_new (GTK_TYPE_LIST_ITEM, expression, "item"); + + /* Bind the clock's location to a label. + * This is easy: We just get the "location" property of the clock. + */ + expression = gtk_property_expression_new (GTK_TYPE_CLOCK, + gtk_expression_ref (clock_expression), + "location"); + /* Now create the label and bind the expression to it. */ + location_label = gtk_label_new (NULL); + gtk_expression_bind (expression, location_label, "label"); + gtk_box_append (GTK_BOX (box), location_label); + + + /* Here we bind the item itself to a GdkPicture. + * This is simply done by using the clock expression itself. + */ + expression = gtk_expression_ref (clock_expression); + /* Now create the widget and bind the expression to it. */ + picture = gtk_picture_new (); + gtk_expression_bind (expression, picture, "paintable"); + gtk_box_append (GTK_BOX (box), picture); + + + /* And finally, everything comes together. + * We create a label for displaying the time as text. + * For that, we need to transform the "GDateTime" of the + * time property into a string so that the label can display it. + */ + expression = gtk_property_expression_new (GTK_TYPE_CLOCK, + gtk_expression_ref (clock_expression), + "time"); + expression = gtk_cclosure_expression_new (G_TYPE_STRING, + NULL, + 1, (GtkExpression *[1]) { expression }, + G_CALLBACK (convert_time_to_string), + NULL, NULL); + /* Now create the label and bind the expression to it. */ + time_label = gtk_label_new (NULL); + gtk_expression_bind (expression, time_label, "label"); + gtk_box_append (GTK_BOX (box), time_label); + + gtk_expression_unref (clock_expression); +} + +static GtkWidget *window = NULL; + +GtkWidget * +do_listview_clocks (GtkWidget *do_widget) +{ + if (window == NULL) + { + GtkWidget *gridview, *sw; + GtkListItemFactory *factory; + GListModel *model; + GtkNoSelection *selection; + + /* This is the normal window setup code every demo does */ + window = gtk_window_new (); + gtk_window_set_display (GTK_WINDOW (window), + gtk_widget_get_display (do_widget)); + g_object_add_weak_pointer (G_OBJECT (window), (gpointer *) &window); + + /* List widgets go into a scrolled window. Always. */ + sw = gtk_scrolled_window_new (NULL, NULL); + gtk_window_set_child (GTK_WINDOW (window), sw); + + /* Create the factory that creates the listitems. Because we + * used bindings above during setup, we only need to connect + * to the setup signal. + * The bindings take care of the bind step. + */ + factory = gtk_signal_list_item_factory_new (); + g_signal_connect (factory, "setup", G_CALLBACK (setup_listitem_cb), NULL); + + gridview = gtk_grid_view_new_with_factory (factory); + gtk_scrollable_set_hscroll_policy (GTK_SCROLLABLE (gridview), GTK_SCROLL_NATURAL); + gtk_scrollable_set_vscroll_policy (GTK_SCROLLABLE (gridview), GTK_SCROLL_NATURAL); + + model = create_clocks_model (); + selection = gtk_no_selection_new (model); + gtk_grid_view_set_model (GTK_GRID_VIEW (gridview), G_LIST_MODEL (selection)); + gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (sw), gridview); + g_object_unref (selection); + g_object_unref (model); + } + + if (!gtk_widget_get_visible (window)) + gtk_widget_show (window); + else + gtk_window_destroy (GTK_WINDOW (window)); + + return window; +} diff --git a/demos/gtk-demo/meson.build b/demos/gtk-demo/meson.build index 4b61514e7c..fe116c503d 100644 --- a/demos/gtk-demo/meson.build +++ b/demos/gtk-demo/meson.build @@ -43,6 +43,7 @@ demos = files([ 'flowbox.c', 'list_store.c', 'listview_applauncher.c', + 'listview_clocks.c', 'listview_filebrowser.c', 'listview_minesweeper.c', 'listview_settings.c', @@ -102,7 +103,7 @@ extra_demo_sources = files(['main.c', if harfbuzz_dep.found() and pangoft_dep.found() demos += files('font_features.c') extra_demo_sources += files(['script-names.c', 'language-names.c']) - gtkdemo_deps += [ harfbuzz_dep, ] + gtkdemo_deps += [ harfbuzz_dep, epoxy_dep ] endif if os_unix |