gtk2/demos/gtk-demo/listview_clocks.c
Matthias Clasen 8d79a32c50 list widgets: Use selection models in the api
Change the apis in GtkListView, GtkColumnView and
GtkGridView to be explicitly about GtkSelectionModel,
to make it obvious that the widgets handle selection.

Update all users.
2020-08-31 17:15:05 -04:00

502 lines
16 KiB
C

/* 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 transforms again: Instead of computing where the angle
* points to, we just rotate everything and then draw the hand as if it
* 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 clocks
* 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 drawn.
*/
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 property,
* 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 *_tz)
{
GtkClock *result;
result = g_object_new (GTK_TYPE_CLOCK,
"location", location,
"timezone", _tz,
NULL);
g_clear_pointer (&_tz, 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 ("Xalapa", g_time_zone_new ("America/Mexico_City"));
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", location_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", picture);
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", time_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;
GtkSelectionModel *model;
/* This is the normal window setup code every demo does */
window = gtk_window_new ();
gtk_window_set_title (GTK_WINDOW (window), "Clocks");
gtk_window_set_default_size (GTK_WINDOW (window), 600, 400);
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 ();
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);
model = GTK_SELECTION_MODEL (gtk_no_selection_new (create_clocks_model ()));
gridview = gtk_grid_view_new_with_factory (model, factory);
gtk_scrollable_set_hscroll_policy (GTK_SCROLLABLE (gridview), GTK_SCROLL_NATURAL);
gtk_scrollable_set_vscroll_policy (GTK_SCROLLABLE (gridview), GTK_SCROLL_NATURAL);
gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (sw), gridview);
}
if (!gtk_widget_get_visible (window))
gtk_widget_show (window);
else
gtk_window_destroy (GTK_WINDOW (window));
return window;
}