diff --git a/demos/gtk-demo/demo.gresource.xml b/demos/gtk-demo/demo.gresource.xml
index 08b90b677a..39d6ad2db8 100644
--- a/demos/gtk-demo/demo.gresource.xml
+++ b/demos/gtk-demo/demo.gresource.xml
@@ -181,6 +181,11 @@
fontify.h
main.ui
+
+ demo3widget.c
+ demo3widget.h
+ demo3widget.ui
+
shortcuts.ui
shortcuts-builder.ui
@@ -265,6 +270,7 @@
list_store.c
main.c
markup.c
+ menu.c
overlay.c
overlay2.c
paint.c
diff --git a/demos/gtk-demo/demo3widget.c b/demos/gtk-demo/demo3widget.c
new file mode 100644
index 0000000000..3a309927e8
--- /dev/null
+++ b/demos/gtk-demo/demo3widget.c
@@ -0,0 +1,243 @@
+#include
+#include "demo3widget.h"
+
+enum
+{
+ PROP_PAINTABLE = 1,
+ PROP_SCALE
+};
+
+struct _Demo3Widget
+{
+ GtkWidget parent_instance;
+
+ GdkPaintable *paintable;
+ float scale;
+
+ GtkWidget *menu;
+};
+
+struct _Demo3WidgetClass
+{
+ GtkWidgetClass parent_class;
+};
+
+G_DEFINE_TYPE (Demo3Widget, demo3_widget, GTK_TYPE_WIDGET)
+
+static void
+demo3_widget_init (Demo3Widget *self)
+{
+ self->scale = 1.f;
+ gtk_widget_init_template (GTK_WIDGET (self));
+}
+
+static void
+demo3_widget_dispose (GObject *object)
+{
+ Demo3Widget *self = DEMO3_WIDGET (object);
+
+ g_clear_object (&self->paintable);
+ g_clear_pointer (&self->menu, gtk_widget_unparent);
+
+ G_OBJECT_CLASS (demo3_widget_parent_class)->dispose (object);
+}
+
+static void
+demo3_widget_snapshot (GtkWidget *widget,
+ GtkSnapshot *snapshot)
+{
+ Demo3Widget *self = DEMO3_WIDGET (widget);
+ int x, y, width, height;
+ double w, h;
+
+ width = gtk_widget_get_width (widget);
+ height = gtk_widget_get_height (widget);
+
+ w = self->scale * gdk_paintable_get_intrinsic_width (self->paintable);
+ h = self->scale * gdk_paintable_get_intrinsic_height (self->paintable);
+
+ x = MAX (0, (width - ceil (w)) / 2);
+ y = MAX (0, (height - ceil (h)) / 2);
+
+ gtk_snapshot_push_clip (snapshot, &GRAPHENE_RECT_INIT (0, 0, width, height));
+ gtk_snapshot_save (snapshot);
+ gtk_snapshot_translate (snapshot, &GRAPHENE_POINT_INIT (x, y));
+ gdk_paintable_snapshot (self->paintable, snapshot, w, h);
+ gtk_snapshot_restore (snapshot);
+ gtk_snapshot_pop (snapshot);
+}
+
+static void
+demo3_widget_measure (GtkWidget *widget,
+ GtkOrientation orientation,
+ int for_size,
+ int *minimum,
+ int *natural,
+ int *minimum_baseline,
+ int *natural_baseline)
+{
+ Demo3Widget *self = DEMO3_WIDGET (widget);
+ int size;
+
+ if (orientation == GTK_ORIENTATION_HORIZONTAL)
+ size = gdk_paintable_get_intrinsic_width (self->paintable);
+ else
+ size = gdk_paintable_get_intrinsic_height (self->paintable);
+
+ *minimum = *natural = self->scale * size;
+}
+
+static void
+demo3_widget_size_allocate (GtkWidget *widget,
+ int width,
+ int height,
+ int baseline)
+{
+ Demo3Widget *self = DEMO3_WIDGET (widget);
+
+ /* Since we are not using a layout manager (who would do this
+ * for us), we need to allocate a size for our menu by calling
+ * gtk_native_check_resize().
+ */
+ gtk_native_check_resize (GTK_NATIVE (self->menu));
+}
+
+static void
+demo3_widget_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ Demo3Widget *self = DEMO3_WIDGET (object);
+
+ switch (prop_id)
+ {
+ case PROP_PAINTABLE:
+ g_clear_object (&self->paintable);
+ self->paintable = g_value_dup_object (value);
+ gtk_widget_queue_resize (GTK_WIDGET (object));
+ break;
+
+ case PROP_SCALE:
+ self->scale = g_value_get_float (value);
+ gtk_widget_queue_resize (GTK_WIDGET (object));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+demo3_widget_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ Demo3Widget *self = DEMO3_WIDGET (object);
+
+ switch (prop_id)
+ {
+ case PROP_PAINTABLE:
+ g_value_set_object (value, self->paintable);
+ break;
+
+ case PROP_SCALE:
+ g_value_set_float (value, self->scale);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+pressed_cb (GtkGestureClick *gesture,
+ guint n_press,
+ double x,
+ double y,
+ Demo3Widget *self)
+{
+ /* We are placing our menu at the point where
+ * the click happened, before popping it up.
+ */
+ gtk_popover_set_pointing_to (GTK_POPOVER (self->menu),
+ &(const GdkRectangle){ x, y, 1, 1 });
+ gtk_popover_popup (GTK_POPOVER (self->menu));
+}
+
+static void
+zoom_cb (GtkWidget *widget,
+ const char *action_name,
+ GVariant *parameter)
+{
+ Demo3Widget *self = DEMO3_WIDGET (widget);
+ float scale;
+
+ if (g_str_equal (action_name, "zoom.in"))
+ scale = MIN (10, self->scale * M_SQRT2);
+ else if (g_str_equal (action_name, "zoom.out"))
+ scale = MAX (0.01, self->scale / M_SQRT2);
+ else
+ scale = 1.0;
+
+ gtk_widget_action_set_enabled (widget, "zoom.in", scale < 10);
+ gtk_widget_action_set_enabled (widget, "zoom.out", scale > 0.01);
+ gtk_widget_action_set_enabled (widget, "zoom.reset", scale != 1);
+
+ g_object_set (widget, "scale", scale, NULL);
+}
+
+static void
+demo3_widget_class_init (Demo3WidgetClass *class)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (class);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (class);
+
+ object_class->dispose = demo3_widget_dispose;
+ object_class->set_property = demo3_widget_set_property;
+ object_class->get_property = demo3_widget_get_property;
+
+ widget_class->snapshot = demo3_widget_snapshot;
+ widget_class->measure = demo3_widget_measure;
+ widget_class->size_allocate = demo3_widget_size_allocate;
+
+ g_object_class_install_property (object_class, PROP_PAINTABLE,
+ g_param_spec_object ("paintable", "Paintable", "Paintable",
+ GDK_TYPE_PAINTABLE,
+ G_PARAM_READWRITE));
+
+ g_object_class_install_property (object_class, PROP_SCALE,
+ g_param_spec_float ("scale", "Scale", "Scale",
+ 0.0, 10.0, 1.0,
+ G_PARAM_READWRITE));
+
+ /* These are the actions that we are using in the menu */
+ gtk_widget_class_install_action (widget_class, "zoom.in", NULL, zoom_cb);
+ gtk_widget_class_install_action (widget_class, "zoom.out", NULL, zoom_cb);
+ gtk_widget_class_install_action (widget_class, "zoom.reset", NULL, zoom_cb);
+
+ gtk_widget_class_set_template_from_resource (widget_class, "/menu/demo3widget.ui");
+ gtk_widget_class_bind_template_child (widget_class, Demo3Widget, menu);
+ gtk_widget_class_bind_template_callback (widget_class, pressed_cb);
+}
+
+GtkWidget *
+demo3_widget_new (const char *resource)
+{
+ Demo3Widget *self;
+ GdkPixbuf *pixbuf;
+ GdkPaintable *paintable;
+
+ pixbuf = gdk_pixbuf_new_from_resource (resource, NULL);
+ paintable = GDK_PAINTABLE (gdk_texture_new_for_pixbuf (pixbuf));
+
+ self = g_object_new (DEMO3_TYPE_WIDGET, "paintable", paintable, NULL);
+
+ g_object_unref (pixbuf);
+ g_object_unref (paintable);
+
+ return GTK_WIDGET (self);
+}
diff --git a/demos/gtk-demo/demo3widget.h b/demos/gtk-demo/demo3widget.h
new file mode 100644
index 0000000000..5bf86bb855
--- /dev/null
+++ b/demos/gtk-demo/demo3widget.h
@@ -0,0 +1,8 @@
+#pragma once
+
+#include
+
+#define DEMO3_TYPE_WIDGET (demo3_widget_get_type ())
+G_DECLARE_FINAL_TYPE (Demo3Widget, demo3_widget, DEMO3, WIDGET, GtkWidget)
+
+GtkWidget * demo3_widget_new (const char *resource);
diff --git a/demos/gtk-demo/demo3widget.ui b/demos/gtk-demo/demo3widget.ui
new file mode 100644
index 0000000000..71f1f57255
--- /dev/null
+++ b/demos/gtk-demo/demo3widget.ui
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/gtk-demo/menu.c b/demos/gtk-demo/menu.c
new file mode 100644
index 0000000000..cdcfd0ed20
--- /dev/null
+++ b/demos/gtk-demo/menu.c
@@ -0,0 +1,47 @@
+/* Menu
+ * #Keywords: action, zoom
+ *
+ * Demonstrates how to add a context menu to a custom widget
+ * and connect it with widget actions.
+ *
+ * The custom widget we create here is similar to a GtkPicture,
+ * but allows setting a zoom level for the displayed paintable.
+ *
+ * Our context menu has items to change the zoom level.
+ */
+
+#include
+#include "demo3widget.h"
+
+
+GtkWidget *
+do_menu (GtkWidget *do_widget)
+{
+ static GtkWidget *window = NULL;
+
+ if (!window)
+ {
+ GtkWidget *sw;
+ GtkWidget *widget;
+
+ window = gtk_window_new ();
+ gtk_window_set_title (GTK_WINDOW (window), "Menu");
+ 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);
+
+ sw = gtk_scrolled_window_new ();
+ gtk_window_set_child (GTK_WINDOW (window), sw);
+
+ widget = demo3_widget_new ("/transparent/portland-rose.jpg");
+ gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (sw), widget);
+ }
+
+ 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 a60f121896..a32fee88e5 100644
--- a/demos/gtk-demo/meson.build
+++ b/demos/gtk-demo/meson.build
@@ -44,6 +44,7 @@ demos = files([
'links.c',
'listbox.c',
'listbox2.c',
+ 'menu.c',
'flowbox.c',
'list_store.c',
'listview_applauncher.c',
@@ -112,7 +113,8 @@ extra_demo_sources = files(['main.c',
'demo2layout.c',
'singular_value_decomposition.c',
'four_point_transform.c',
- 'demo2widget.c'])
+ 'demo2widget.c',
+ 'demo3widget.c'])
if harfbuzz_dep.found() and pangoft_dep.found()
demos += files('font_features.c')