[gtk/wip/otte/listview: 136/138] gtk-demo: Add a Clocks demo



commit 4f1ed597b2b6816a850df89c8adf2a3c1d9f0650
Author: Benjamin Otte <otte redhat com>
Date:   Sun Nov 24 08:07:33 2019 +0100

    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.

 demos/gtk-demo/demo.gresource.xml |   1 +
 demos/gtk-demo/listview_clocks.c  | 488 ++++++++++++++++++++++++++++++++++++++
 demos/gtk-demo/meson.build        |   3 +-
 3 files changed, 491 insertions(+), 1 deletion(-)
---
diff --git a/demos/gtk-demo/demo.gresource.xml b/demos/gtk-demo/demo.gresource.xml
index 8a0bedf729..2b9efbd0ad 100644
--- a/demos/gtk-demo/demo.gresource.xml
+++ b/demos/gtk-demo/demo.gresource.xml
@@ -211,6 +211,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..b463839051
--- /dev/null
+++ b/demos/gtk-demo/listview_clocks.c
@@ -0,0 +1,488 @@
+/* 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_container_add (GTK_CONTAINER (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_container_add (GTK_CONTAINER (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_container_add (GTK_CONTAINER (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_TOPLEVEL);
+      gtk_window_set_display (GTK_WINDOW (window),
+                              gtk_widget_get_display (do_widget));
+      g_signal_connect (window, "destroy",
+                        G_CALLBACK (gtk_widget_destroyed), &window);
+
+      /* List widgets go into a scrolled window. Always. */
+      sw = gtk_scrolled_window_new (NULL, NULL);
+      gtk_container_add (GTK_CONTAINER (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_container_add (GTK_CONTAINER (sw), gridview);
+      g_object_unref (selection);
+      g_object_unref (model);
+    }
+
+  if (!gtk_widget_get_visible (window))
+    gtk_widget_show (window);
+  else
+    gtk_widget_destroy (window);
+
+  return window;
+}
diff --git a/demos/gtk-demo/meson.build b/demos/gtk-demo/meson.build
index 4b1cc3ec73..826d146436 100644
--- a/demos/gtk-demo/meson.build
+++ b/demos/gtk-demo/meson.build
@@ -45,6 +45,7 @@ demos = files([
   'flowbox.c',
   'list_store.c',
   'listview_applauncher.c',
+  'listview_clocks.c',
   'listview_filebrowser.c',
   'listview_minesweeper.c',
   'listview_settings.c',
@@ -103,7 +104,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


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