diff --git a/doc/reference/clapper-gtk/clapper-gtk.toml.in b/doc/reference/clapper-gtk/clapper-gtk.toml.in
new file mode 100644
index 00000000..f120b038
--- /dev/null
+++ b/doc/reference/clapper-gtk/clapper-gtk.toml.in
@@ -0,0 +1,63 @@
+[library]
+version = "@CLAPPER_VERSION@"
+browse_url = "https://github.com/Rafostar/clapper/"
+repository_url = "https://github.com/Rafostar/clapper.git"
+website_url = "https://rafostar.github.io/clapper/"
+docs_url = "https://rafostar.github.io/clapper/doc/clapper-gtk/"
+authors = "Rafał Dzięgiel"
+logo_url = "clapper-logo.svg"
+license = "LGPL-2.1-or-later"
+description = "Clapper GTK integration library"
+devhelp = true
+search_index = true
+
+dependencies = ["Clapper@CLAPPER_VERSION_SUFFIX@", "Gtk-4.0"]
+
+ [dependencies."Clapper@CLAPPER_VERSION_SUFFIX@"]
+ name = "Clapper"
+ description = "Clapper playback library"
+ docs_url = "https://rafostar.github.io/clapper/doc/clapper/"
+
+ [dependencies."Gtk-4.0"]
+ name = "Gtk"
+ description = "The GTK toolkit"
+ docs_url = "https://docs.gtk.org/gtk4/"
+
+related = ["GLib-2.0", "GObject-2.0", "Gio-2.0", "Gst-1.0"]
+
+ [related."GLib-2.0"]
+ name = "GLib"
+ description = "A general-purpose, portable utility library"
+ docs_url = "https://docs.gtk.org/glib/"
+
+ [related."GObject-2.0"]
+ name = "GObject"
+ description = "The base type system library"
+ docs_url = "https://docs.gtk.org/gobject/"
+
+ [related."Gio-2.0"]
+ name = "Gio"
+ description = "GObject Interfaces and Objects, Networking, IPC, and I/O"
+ docs_url = "https://docs.gtk.org/gio/"
+
+ [related."Gst-1.0"]
+ name = "Gst"
+ description = "GStreamer core library"
+ docs_url = "https://gstreamer.freedesktop.org/documentation/gstreamer/gi-index.html"
+
+[theme]
+name = "basic"
+show_index_summary = true
+show_class_hierarchy = true
+
+[source-location]
+base_url = "https://github.com/Rafostar/clapper/tree/master/"
+
+[extra]
+# The same order will be used when generating the index
+content_files = [
+]
+content_images = [
+ "images/clapper-logo.svg",
+]
+urlmap_file = "urlmap.js"
diff --git a/doc/reference/clapper-gtk/images/clapper-logo.svg b/doc/reference/clapper-gtk/images/clapper-logo.svg
new file mode 100644
index 00000000..2889584b
--- /dev/null
+++ b/doc/reference/clapper-gtk/images/clapper-logo.svg
@@ -0,0 +1,29 @@
+
+
diff --git a/doc/reference/clapper-gtk/meson.build b/doc/reference/clapper-gtk/meson.build
new file mode 100644
index 00000000..24590ace
--- /dev/null
+++ b/doc/reference/clapper-gtk/meson.build
@@ -0,0 +1,29 @@
+clappergtk_toml = configure_file(
+ input: 'clapper-gtk.toml.in',
+ output: 'clapper-gtk.toml',
+ configuration: doc_version_conf,
+ install: true,
+ install_dir: join_paths(datadir, 'doc', 'clapper-gtk'),
+)
+
+custom_target('clapper-gtk-doc',
+ input: [
+ clappergtk_toml,
+ clappergtk_gir[0],
+ ],
+ output: 'clapper-gtk',
+ command: [
+ gi_docgen,
+ 'generate',
+ gi_docgen_common_args,
+ '--add-include-path=@0@'.format(join_paths(meson.project_build_root(), 'src', 'lib', 'clapper-gtk')),
+ '--config=@INPUT0@',
+ '--output-dir=@OUTPUT@',
+ '--content-dir=@0@'.format(meson.current_build_dir()),
+ '--content-dir=@0@'.format(meson.current_source_dir()),
+ '@INPUT1@',
+ ],
+ build_by_default: true,
+ install: true,
+ install_dir: join_paths(datadir, 'doc'),
+)
diff --git a/doc/reference/clapper-gtk/urlmap.js b/doc/reference/clapper-gtk/urlmap.js
new file mode 100644
index 00000000..17b6f4e5
--- /dev/null
+++ b/doc/reference/clapper-gtk/urlmap.js
@@ -0,0 +1,8 @@
+baseURLs = [
+ ['GLib', 'https://docs.gtk.org/glib/'],
+ ['GObject', 'https://docs.gtk.org/gobject/'],
+ ['Gio', 'https://docs.gtk.org/gio/'],
+ ['Gtk', 'https://docs.gtk.org/gtk4/'],
+ ['Gst', 'https://gstreamer.freedesktop.org/documentation/gstreamer/gi-index.html?'],
+ ['Clapper', 'https://rafostar.github.io/clapper/doc/clapper/'],
+]
diff --git a/doc/reference/meson.build b/doc/reference/meson.build
index ccf2941c..065e3a48 100644
--- a/doc/reference/meson.build
+++ b/doc/reference/meson.build
@@ -13,3 +13,6 @@ endif
if build_clapper
subdir('clapper')
endif
+if build_clappergtk
+ subdir('clapper-gtk')
+endif
diff --git a/meson.build b/meson.build
index e3b02f7c..4ed7103f 100644
--- a/meson.build
+++ b/meson.build
@@ -130,6 +130,7 @@ summary({
}, section: 'Directories')
summary('clapper', build_clapper ? 'Yes' : 'No', section: 'Build')
+summary('clapper-gtk', build_clappergtk ? 'Yes' : 'No', section: 'Build')
summary('gst-plugin', build_gst_plugin ? 'Yes' : 'No', section: 'Build')
summary('introspection', build_gir ? 'Yes' : 'No', section: 'Build')
summary('vapi', build_vapi ? 'Yes' : 'No', section: 'Build')
diff --git a/meson_options.txt b/meson_options.txt
index 61d4e4d4..5cea2a0e 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -4,6 +4,11 @@ option('clapper',
value: 'auto',
description: 'Build Clapper library'
)
+option('clapper-gtk',
+ type: 'feature',
+ value: 'auto',
+ description: 'Build Clapper GTK integration library'
+)
option('gst-plugin',
type: 'feature',
value: 'auto',
diff --git a/src/lib/clapper-gtk/clapper-gtk-billboard.c b/src/lib/clapper-gtk/clapper-gtk-billboard.c
new file mode 100644
index 00000000..cf139416
--- /dev/null
+++ b/src/lib/clapper-gtk/clapper-gtk-billboard.c
@@ -0,0 +1,464 @@
+/* Clapper GTK Integration Library
+ * Copyright (C) 2024 Rafał Dzięgiel
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+/**
+ * ClapperGtkBillboard:
+ *
+ * A layer where various messages can be displayed.
+ *
+ * #ClapperGtkBillboard widget is meant to be overlaid on top of
+ * [class@ClapperGtk.Video] as a normal (non-fading) overlay.
+ *
+ * It is used to display various messages/announcements and later
+ * takes care of fading them on its own.
+ *
+ * If automatic volume/speed change notifications when their values do
+ * change are desired, functions for announcing them can be run in callbacks
+ * to corresponding property notify signals on the [class@Clapper.Player].
+ */
+
+#include "config.h"
+
+#include
+#include
+
+#include "clapper-gtk-billboard.h"
+#include "clapper-gtk-utils-private.h"
+
+#define PERCENTAGE_ROUND(a) (round ((gdouble) a / 0.01) * 0.01)
+#define WORDS_PER_MSECOND 0.004
+
+#define GST_CAT_DEFAULT clapper_gtk_billboard_debug
+GST_DEBUG_CATEGORY_STATIC (GST_CAT_DEFAULT);
+
+struct _ClapperGtkBillboard
+{
+ ClapperGtkContainer parent;
+
+ GtkWidget *side_revealer;
+ GtkWidget *progress_revealer;
+ GtkWidget *progress_box;
+ GtkWidget *top_progress;
+ GtkWidget *bottom_progress;
+ GtkWidget *progress_image;
+ GtkWidget *progress_label;
+
+ GtkWidget *message_revealer;
+ GtkWidget *message_image;
+ GtkWidget *message_label;
+
+ gboolean mute;
+
+ gboolean has_pinned;
+
+ guint side_timeout;
+ guint message_timeout;
+
+ ClapperPlayer *player;
+};
+
+#define parent_class clapper_gtk_billboard_parent_class
+G_DEFINE_TYPE (ClapperGtkBillboard, clapper_gtk_billboard, CLAPPER_GTK_TYPE_CONTAINER)
+
+/* We calculate estimated read time. This allows
+ * translated text to be displayed as long as
+ * necessary without app developer caring. */
+static guint
+_estimate_read_time (const gchar *text)
+{
+ guint i, n_words = 1;
+ guint read_time;
+
+ for (i = 0; text[i] != '\0'; ++i) {
+ if (text[i] == ' ' || text[i] == '\n')
+ n_words++;
+ }
+
+ read_time = MAX (1500, (n_words / WORDS_PER_MSECOND) + 500);
+ GST_DEBUG ("Estimated message read time: %u", read_time);
+
+ return read_time;
+}
+
+static void
+_unreveal_side_delay_cb (ClapperGtkBillboard *self)
+{
+ GST_LOG_OBJECT (self, "Unreveal side handler reached");
+ self->side_timeout = 0;
+
+ gtk_revealer_set_reveal_child (GTK_REVEALER (self->side_revealer), FALSE);
+}
+
+static void
+_unreveal_message_delay_cb (ClapperGtkBillboard *self)
+{
+ GST_LOG_OBJECT (self, "Unreveal message handler reached");
+ self->message_timeout = 0;
+
+ gtk_revealer_set_reveal_child (GTK_REVEALER (self->message_revealer), FALSE);
+}
+
+static void
+_reset_fade_side_timeout (ClapperGtkBillboard *self)
+{
+ GST_TRACE_OBJECT (self, "Fade side timeout reset");
+
+ g_clear_handle_id (&self->side_timeout, g_source_remove);
+ self->side_timeout = g_timeout_add_once (1500,
+ (GSourceOnceFunc) _unreveal_side_delay_cb, self);
+}
+
+static void
+_reset_fade_message_timeout (ClapperGtkBillboard *self)
+{
+ const gchar *text = gtk_label_get_text (GTK_LABEL (self->message_label));
+
+ GST_TRACE_OBJECT (self, "Fade side timeout reset");
+
+ g_clear_handle_id (&self->message_timeout, g_source_remove);
+ self->message_timeout = g_timeout_add_once (
+ _estimate_read_time (text),
+ (GSourceOnceFunc) _unreveal_message_delay_cb, self);
+}
+
+static void
+adapt_cb (ClapperGtkContainer *container, gboolean adapt,
+ ClapperGtkBillboard *self)
+{
+ GST_DEBUG_OBJECT (self, "Adapted: %s", (adapt) ? "yes" : "no");
+
+ gtk_revealer_set_reveal_child (GTK_REVEALER (self->progress_revealer), !adapt);
+}
+
+static void
+revealer_revealed_cb (GtkRevealer *revealer,
+ GParamSpec *pspec G_GNUC_UNUSED, ClapperGtkBillboard *self)
+{
+ if (!gtk_revealer_get_child_revealed (revealer)) {
+ GtkWidget *other_revealer = (GTK_WIDGET (revealer) == self->side_revealer)
+ ? self->message_revealer
+ : self->side_revealer;
+
+ gtk_widget_set_visible (GTK_WIDGET (revealer), FALSE);
+
+ /* We only hide here when nothing is posted on the board,
+ * visiblity is set to TRUE when post is made */
+ if (!gtk_revealer_get_child_revealed (GTK_REVEALER (other_revealer)))
+ gtk_widget_set_visible (GTK_WIDGET (self), FALSE);
+ } else {
+ if ((GTK_WIDGET (revealer) == self->side_revealer))
+ _reset_fade_side_timeout (self);
+ else if (!self->has_pinned)
+ _reset_fade_message_timeout (self);
+ }
+}
+
+static void
+reveal_side (ClapperGtkBillboard *self)
+{
+ g_clear_handle_id (&self->side_timeout, g_source_remove);
+
+ gtk_widget_set_visible (GTK_WIDGET (self), TRUE);
+ gtk_widget_set_visible (self->side_revealer, TRUE);
+ gtk_revealer_set_reveal_child (GTK_REVEALER (self->side_revealer), TRUE);
+
+ if (gtk_revealer_get_child_revealed (GTK_REVEALER (self->side_revealer)))
+ _reset_fade_side_timeout (self);
+}
+
+static void
+_post_message_internal (ClapperGtkBillboard *self,
+ const gchar *icon_name, const gchar *message, gboolean pin)
+{
+ if (self->has_pinned)
+ return;
+
+ self->has_pinned = pin;
+
+ gtk_image_set_from_icon_name (GTK_IMAGE (self->message_image), icon_name);
+ gtk_label_set_label (GTK_LABEL (self->message_label), message);
+
+ g_clear_handle_id (&self->message_timeout, g_source_remove);
+
+ gtk_widget_set_visible (GTK_WIDGET (self), TRUE);
+ gtk_widget_set_visible (self->message_revealer, TRUE);
+ gtk_revealer_set_reveal_child (GTK_REVEALER (self->message_revealer), TRUE);
+
+ if (!self->has_pinned
+ && gtk_revealer_get_child_revealed (GTK_REVEALER (self->message_revealer)))
+ _reset_fade_message_timeout (self);
+}
+
+static void
+_player_mute_changed_cb (ClapperPlayer *player,
+ GParamSpec *pspec G_GNUC_UNUSED, ClapperGtkBillboard *self)
+{
+ self->mute = clapper_player_get_mute (player);
+
+ clapper_gtk_billboard_announce_volume (self);
+}
+
+/**
+ * clapper_gtk_billboard_new:
+ *
+ * Creates a new #ClapperGtkBillboard instance.
+ *
+ * Returns: a new billboard #GtkWidget.
+ */
+GtkWidget *
+clapper_gtk_billboard_new (void)
+{
+ return g_object_new (CLAPPER_GTK_TYPE_BILLBOARD, NULL);
+}
+
+/**
+ * clapper_gtk_billboard_post_message:
+ * @billboard: a #ClapperGtkBillboard
+ * @icon_name: an icon name
+ * @message: a message text
+ *
+ * Posts a temporary message on the @billboard.
+ *
+ * Duration how long a message will stay is automatically
+ * calculated based on amount of text.
+ */
+void
+clapper_gtk_billboard_post_message (ClapperGtkBillboard *self,
+ const gchar *icon_name, const gchar *message)
+{
+ _post_message_internal (self, icon_name, message, FALSE);
+}
+
+/**
+ * clapper_gtk_billboard_pin_message:
+ * @billboard: a #ClapperGtkBillboard
+ * @icon_name: an icon name
+ * @message: a message text
+ *
+ * Pins a permanent message on the @billboard.
+ *
+ * The message will stay on the @billboard until a
+ * [method@ClapperGtk.Billboard.unpin_pinned_message] is called.
+ */
+void
+clapper_gtk_billboard_pin_message (ClapperGtkBillboard *self,
+ const gchar *icon_name, const gchar *message)
+{
+ _post_message_internal (self, icon_name, message, TRUE);
+}
+
+/**
+ * clapper_gtk_billboard_unpin_pinned_message:
+ * @billboard: a #ClapperGtkBillboard
+ *
+ * Unpins previously pinned message on the @billboard.
+ *
+ * If no message was pinned this function will do nothing,
+ * so it is safe to call when unsure.
+ */
+void
+clapper_gtk_billboard_unpin_pinned_message (ClapperGtkBillboard *self)
+{
+ if (!self->has_pinned)
+ return;
+
+ _unreveal_message_delay_cb (self);
+ self->has_pinned = FALSE;
+}
+
+/**
+ * clapper_gtk_billboard_announce_volume:
+ * @billboard: a #ClapperGtkBillboard
+ *
+ * Temporarily displays current volume level on the
+ * side of @billboard.
+ *
+ * Use this if you want to present current volume level to the user.
+ * Note that @billboard also automatically announces volume changes.
+ */
+void
+clapper_gtk_billboard_announce_volume (ClapperGtkBillboard *self)
+{
+ gdouble volume = PERCENTAGE_ROUND (clapper_player_get_volume (self->player));
+ gchar *percent_str;
+ gboolean has_overamp;
+
+ /* Revert popup_speed changes */
+ gtk_progress_bar_set_inverted (GTK_PROGRESS_BAR (self->bottom_progress), TRUE);
+
+ has_overamp = gtk_widget_has_css_class (self->progress_box, "overamp");
+ percent_str = g_strdup_printf ("%.0lf%%", volume * 100);
+
+ if (volume <= 1.0) {
+ gtk_progress_bar_set_fraction (GTK_PROGRESS_BAR (self->top_progress), 0.0);
+ gtk_progress_bar_set_fraction (GTK_PROGRESS_BAR (self->bottom_progress), volume);
+
+ if (has_overamp)
+ gtk_widget_remove_css_class (self->progress_box, "overamp");
+ } else {
+ gtk_progress_bar_set_fraction (GTK_PROGRESS_BAR (self->top_progress), volume - 1.0);
+ gtk_progress_bar_set_fraction (GTK_PROGRESS_BAR (self->bottom_progress), 1.0);
+
+ if (!has_overamp)
+ gtk_widget_add_css_class (self->progress_box, "overamp");
+ }
+
+ gtk_image_set_from_icon_name (GTK_IMAGE (self->progress_image),
+ clapper_gtk_get_icon_name_for_volume ((!self->mute) ? volume : 0));
+ gtk_label_set_label (GTK_LABEL (self->progress_label), percent_str);
+
+ g_free (percent_str);
+
+ reveal_side (self);
+}
+
+/**
+ * clapper_gtk_billboard_announce_speed:
+ * @billboard: a #ClapperGtkBillboard
+ *
+ * Temporarily displays current speed value on the
+ * side of @billboard.
+ *
+ * Use this if you want to present current speed value to the user.
+ * Note that @billboard also automatically announces speed changes.
+ */
+void
+clapper_gtk_billboard_announce_speed (ClapperGtkBillboard *self)
+{
+ gdouble speed = PERCENTAGE_ROUND (clapper_player_get_speed (self->player));
+ gchar *speed_str;
+
+ /* Revert popup_volume changes */
+ if (gtk_widget_has_css_class (self->progress_box, "overamp"))
+ gtk_widget_remove_css_class (self->progress_box, "overamp");
+
+ gtk_progress_bar_set_inverted (GTK_PROGRESS_BAR (self->bottom_progress), FALSE);
+
+ speed_str = g_strdup_printf ("%.2lfx", speed);
+
+ if (speed <= 1.0) {
+ gtk_progress_bar_set_fraction (GTK_PROGRESS_BAR (self->top_progress), 0.0);
+ gtk_progress_bar_set_fraction (GTK_PROGRESS_BAR (self->bottom_progress), 1.0 - speed);
+ } else {
+ gtk_progress_bar_set_fraction (GTK_PROGRESS_BAR (self->top_progress), speed - 1.0);
+ gtk_progress_bar_set_fraction (GTK_PROGRESS_BAR (self->bottom_progress), 0.0);
+ }
+
+ gtk_image_set_from_icon_name (GTK_IMAGE (self->progress_image),
+ clapper_gtk_get_icon_name_for_speed (speed));
+ gtk_label_set_label (GTK_LABEL (self->progress_label), speed_str);
+
+ g_free (speed_str);
+
+ reveal_side (self);
+}
+
+static void
+clapper_gtk_billboard_root (GtkWidget *widget)
+{
+ ClapperGtkBillboard *self = CLAPPER_GTK_BILLBOARD_CAST (widget);
+
+ GTK_WIDGET_CLASS (parent_class)->root (widget);
+
+ if ((self->player = clapper_gtk_get_player_from_ancestor (widget))) {
+ g_signal_connect (self->player, "notify::mute",
+ G_CALLBACK (_player_mute_changed_cb), self);
+ self->mute = clapper_player_get_mute (self->player);
+ }
+}
+
+static void
+clapper_gtk_billboard_unroot (GtkWidget *widget)
+{
+ ClapperGtkBillboard *self = CLAPPER_GTK_BILLBOARD_CAST (widget);
+
+ if (self->player) {
+ g_signal_handlers_disconnect_by_func (self->player, _player_mute_changed_cb, self);
+
+ self->player = NULL;
+ }
+
+ /* Reset in case of rooted again not within video widget */
+ self->mute = FALSE;
+
+ GTK_WIDGET_CLASS (parent_class)->unroot (widget);
+}
+
+static void
+clapper_gtk_billboard_init (ClapperGtkBillboard *self)
+{
+ gtk_widget_init_template (GTK_WIDGET (self));
+}
+
+static void
+clapper_gtk_billboard_dispose (GObject *object)
+{
+ gtk_widget_dispose_template (GTK_WIDGET (object), CLAPPER_GTK_TYPE_BILLBOARD);
+
+ G_OBJECT_CLASS (parent_class)->dispose (object);
+}
+
+static void
+clapper_gtk_billboard_finalize (GObject *object)
+{
+ ClapperGtkBillboard *self = CLAPPER_GTK_BILLBOARD_CAST (object);
+
+ g_clear_handle_id (&self->side_timeout, g_source_remove);
+ g_clear_handle_id (&self->message_timeout, g_source_remove);
+
+ G_OBJECT_CLASS (parent_class)->finalize (object);
+}
+
+static void
+clapper_gtk_billboard_class_init (ClapperGtkBillboardClass *klass)
+{
+ GObjectClass *gobject_class = (GObjectClass *) klass;
+ GtkWidgetClass *widget_class = (GtkWidgetClass *) klass;
+
+ GST_DEBUG_CATEGORY_INIT (GST_CAT_DEFAULT, "clappergtkbillboard", 0,
+ "Clapper GTK Billboard");
+
+ gobject_class->dispose = clapper_gtk_billboard_dispose;
+ gobject_class->finalize = clapper_gtk_billboard_finalize;
+
+ /* Using root/unroot since initially invisible (unrealized)
+ * and we want for signals to stay connected as long as parented */
+ widget_class->root = clapper_gtk_billboard_root;
+ widget_class->unroot = clapper_gtk_billboard_unroot;
+
+ gtk_widget_class_set_template_from_resource (widget_class,
+ CLAPPER_GTK_RESOURCE_PREFIX "/ui/clapper-gtk-billboard.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, ClapperGtkBillboard, side_revealer);
+ gtk_widget_class_bind_template_child (widget_class, ClapperGtkBillboard, progress_box);
+ gtk_widget_class_bind_template_child (widget_class, ClapperGtkBillboard, progress_revealer);
+ gtk_widget_class_bind_template_child (widget_class, ClapperGtkBillboard, top_progress);
+ gtk_widget_class_bind_template_child (widget_class, ClapperGtkBillboard, bottom_progress);
+ gtk_widget_class_bind_template_child (widget_class, ClapperGtkBillboard, progress_image);
+ gtk_widget_class_bind_template_child (widget_class, ClapperGtkBillboard, progress_label);
+
+ gtk_widget_class_bind_template_child (widget_class, ClapperGtkBillboard, message_revealer);
+ gtk_widget_class_bind_template_child (widget_class, ClapperGtkBillboard, message_image);
+ gtk_widget_class_bind_template_child (widget_class, ClapperGtkBillboard, message_label);
+
+ gtk_widget_class_bind_template_callback (widget_class, adapt_cb);
+ gtk_widget_class_bind_template_callback (widget_class, revealer_revealed_cb);
+
+ gtk_widget_class_set_css_name (widget_class, "clapper-gtk-billboard");
+}
diff --git a/src/lib/clapper-gtk/clapper-gtk-billboard.h b/src/lib/clapper-gtk/clapper-gtk-billboard.h
new file mode 100644
index 00000000..b9f6f067
--- /dev/null
+++ b/src/lib/clapper-gtk/clapper-gtk-billboard.h
@@ -0,0 +1,51 @@
+/* Clapper GTK Integration Library
+ * Copyright (C) 2024 Rafał Dzięgiel
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+#pragma once
+
+#if !defined(__CLAPPER_GTK_INSIDE__) && !defined(CLAPPER_GTK_COMPILATION)
+#error "Only can be included directly."
+#endif
+
+#include
+#include
+#include
+
+#include
+
+G_BEGIN_DECLS
+
+#define CLAPPER_GTK_TYPE_BILLBOARD (clapper_gtk_billboard_get_type())
+#define CLAPPER_GTK_BILLBOARD_CAST(obj) ((ClapperGtkBillboard *)(obj))
+
+G_DECLARE_FINAL_TYPE (ClapperGtkBillboard, clapper_gtk_billboard, CLAPPER_GTK, BILLBOARD, ClapperGtkContainer)
+
+GtkWidget * clapper_gtk_billboard_new (void);
+
+void clapper_gtk_billboard_post_message (ClapperGtkBillboard *billboard, const gchar *icon_name, const gchar *message);
+
+void clapper_gtk_billboard_pin_message (ClapperGtkBillboard *billboard, const gchar *icon_name, const gchar *message);
+
+void clapper_gtk_billboard_unpin_pinned_message (ClapperGtkBillboard *billboard);
+
+void clapper_gtk_billboard_announce_volume (ClapperGtkBillboard *billboard);
+
+void clapper_gtk_billboard_announce_speed (ClapperGtkBillboard *billboard);
+
+G_END_DECLS
diff --git a/src/lib/clapper-gtk/clapper-gtk-buffering-animation-private.h b/src/lib/clapper-gtk/clapper-gtk-buffering-animation-private.h
new file mode 100644
index 00000000..6946470a
--- /dev/null
+++ b/src/lib/clapper-gtk/clapper-gtk-buffering-animation-private.h
@@ -0,0 +1,41 @@
+/* Clapper GTK Integration Library
+ * Copyright (C) 2024 Rafał Dzięgiel
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+#pragma once
+
+#include
+#include
+#include
+
+#include "clapper-gtk-container.h"
+
+G_BEGIN_DECLS
+
+#define CLAPPER_GTK_TYPE_BUFFERING_ANIMATION (clapper_gtk_buffering_animation_get_type())
+#define CLAPPER_GTK_BUFFERING_ANIMATION_CAST(obj) ((ClapperGtkBufferingAnimation *)(obj))
+
+G_DECLARE_FINAL_TYPE (ClapperGtkBufferingAnimation, clapper_gtk_buffering_animation, CLAPPER_GTK, BUFFERING_ANIMATION, ClapperGtkContainer)
+
+G_GNUC_INTERNAL
+void clapper_gtk_buffering_animation_start (ClapperGtkBufferingAnimation *animation);
+
+G_GNUC_INTERNAL
+void clapper_gtk_buffering_animation_stop (ClapperGtkBufferingAnimation *animation);
+
+G_END_DECLS
diff --git a/src/lib/clapper-gtk/clapper-gtk-buffering-animation.c b/src/lib/clapper-gtk/clapper-gtk-buffering-animation.c
new file mode 100644
index 00000000..f2d501ff
--- /dev/null
+++ b/src/lib/clapper-gtk/clapper-gtk-buffering-animation.c
@@ -0,0 +1,140 @@
+/* Clapper GTK Integration Library
+ * Copyright (C) 2024 Rafał Dzięgiel
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+#include
+
+#include "clapper-gtk-buffering-animation-private.h"
+#include "clapper-gtk-buffering-paintable-private.h"
+
+#define MIN_STEP_DELAY 30000
+
+#define GST_CAT_DEFAULT clapper_gtk_buffering_animation_debug
+GST_DEBUG_CATEGORY_STATIC (GST_CAT_DEFAULT);
+
+struct _ClapperGtkBufferingAnimation
+{
+ ClapperGtkContainer parent;
+
+ ClapperGtkBufferingPaintable *buffering_paintable;
+
+ guint tick_id;
+ gint64 last_tick;
+};
+
+#define parent_class clapper_gtk_buffering_animation_parent_class
+G_DEFINE_TYPE (ClapperGtkBufferingAnimation, clapper_gtk_buffering_animation, CLAPPER_GTK_TYPE_CONTAINER)
+
+static gboolean
+_animation_tick (GtkWidget *picture, GdkFrameClock *frame_clock, ClapperGtkBufferingAnimation *self)
+{
+ gint64 now = gdk_frame_clock_get_frame_time (frame_clock);
+
+ /* We do not want for this animation to move too fast */
+ if (now - self->last_tick >= MIN_STEP_DELAY) {
+ GST_LOG_OBJECT (self, "Animation step, last: %" G_GINT64_FORMAT
+ ", now: %" G_GINT64_FORMAT, self->last_tick, now);
+ clapper_gtk_buffering_paintable_step (self->buffering_paintable);
+ self->last_tick = now;
+ }
+
+ return G_SOURCE_CONTINUE;
+}
+
+void
+clapper_gtk_buffering_animation_start (ClapperGtkBufferingAnimation *self)
+{
+ GtkWidget *picture;
+
+ if (self->tick_id != 0)
+ return;
+
+ GST_DEBUG_OBJECT (self, "Animation start");
+
+ picture = clapper_gtk_container_get_child (CLAPPER_GTK_CONTAINER (self));
+ self->tick_id = gtk_widget_add_tick_callback (picture,
+ (GtkTickCallback) _animation_tick, self, NULL);
+}
+
+void
+clapper_gtk_buffering_animation_stop (ClapperGtkBufferingAnimation *self)
+{
+ GtkWidget *picture;
+
+ if (self->tick_id == 0)
+ return;
+
+ GST_DEBUG_OBJECT (self, "Animation stop");
+
+ picture = clapper_gtk_container_get_child (CLAPPER_GTK_CONTAINER (self));
+ gtk_widget_remove_tick_callback (picture, self->tick_id);
+
+ self->tick_id = 0;
+ self->last_tick = 0;
+ clapper_gtk_buffering_paintable_reset (self->buffering_paintable);
+}
+
+static void
+clapper_gtk_buffering_animation_init (ClapperGtkBufferingAnimation *self)
+{
+ GtkWidget *picture = gtk_picture_new ();
+ self->buffering_paintable = clapper_gtk_buffering_paintable_new ();
+
+ gtk_picture_set_paintable (GTK_PICTURE (picture),
+ GDK_PAINTABLE (self->buffering_paintable));
+
+ clapper_gtk_container_set_child (CLAPPER_GTK_CONTAINER (self), picture);
+}
+
+static void
+clapper_gtk_buffering_animation_unmap (GtkWidget *widget)
+{
+ ClapperGtkBufferingAnimation *self = CLAPPER_GTK_BUFFERING_ANIMATION_CAST (widget);
+
+ clapper_gtk_buffering_animation_stop (self);
+
+ GTK_WIDGET_CLASS (parent_class)->unmap (widget);
+}
+
+static void
+clapper_gtk_buffering_animation_finalize (GObject *object)
+{
+ ClapperGtkBufferingAnimation *self = CLAPPER_GTK_BUFFERING_ANIMATION_CAST (object);
+
+ GST_TRACE_OBJECT (self, "Finalize");
+
+ g_object_unref (self->buffering_paintable);
+
+ G_OBJECT_CLASS (parent_class)->finalize (object);
+}
+
+static void
+clapper_gtk_buffering_animation_class_init (ClapperGtkBufferingAnimationClass *klass)
+{
+ GObjectClass *gobject_class = (GObjectClass *) klass;
+ GtkWidgetClass *widget_class = (GtkWidgetClass *) klass;
+
+ GST_DEBUG_CATEGORY_INIT (GST_CAT_DEFAULT, "clappergtkbufferinganimation", 0,
+ "Clapper GTK Buffering Animation");
+
+ gobject_class->finalize = clapper_gtk_buffering_animation_finalize;
+
+ widget_class->unmap = clapper_gtk_buffering_animation_unmap;
+
+ gtk_widget_class_set_css_name (widget_class, "clapper-gtk-buffering-animation");
+}
diff --git a/src/lib/clapper-gtk/clapper-gtk-buffering-paintable-private.h b/src/lib/clapper-gtk/clapper-gtk-buffering-paintable-private.h
new file mode 100644
index 00000000..ac7d2067
--- /dev/null
+++ b/src/lib/clapper-gtk/clapper-gtk-buffering-paintable-private.h
@@ -0,0 +1,42 @@
+/* Clapper GTK Integration Library
+ * Copyright (C) 2024 Rafał Dzięgiel
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+#pragma once
+
+#include
+#include
+#include
+
+G_BEGIN_DECLS
+
+#define CLAPPER_GTK_TYPE_BUFFERING_PAINTABLE (clapper_gtk_buffering_paintable_get_type())
+#define CLAPPER_GTK_BUFFERING_PAINTABLE_CAST(obj) ((ClapperGtkBufferingPaintable *)(obj))
+
+G_DECLARE_FINAL_TYPE (ClapperGtkBufferingPaintable, clapper_gtk_buffering_paintable, CLAPPER_GTK, BUFFERING_PAINTABLE, GObject)
+
+G_GNUC_INTERNAL
+ClapperGtkBufferingPaintable * clapper_gtk_buffering_paintable_new (void);
+
+G_GNUC_INTERNAL
+void clapper_gtk_buffering_paintable_step (ClapperGtkBufferingPaintable *buffering_paintable);
+
+G_GNUC_INTERNAL
+void clapper_gtk_buffering_paintable_reset (ClapperGtkBufferingPaintable *buffering_paintable);
+
+G_END_DECLS
diff --git a/src/lib/clapper-gtk/clapper-gtk-buffering-paintable.c b/src/lib/clapper-gtk/clapper-gtk-buffering-paintable.c
new file mode 100644
index 00000000..0a8f4f54
--- /dev/null
+++ b/src/lib/clapper-gtk/clapper-gtk-buffering-paintable.c
@@ -0,0 +1,177 @@
+/* Clapper GTK Integration Library
+ * Copyright (C) 2024 Rafał Dzięgiel
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+#include "clapper-gtk-buffering-paintable-private.h"
+
+#define CIRCLE_MAX_SIZE 48
+#define CIRCLE_SPACING 10
+#define CIRCLE_OUTLINE 2
+#define INTRINSIC_SIZE 184 // 3 * CIRCLE_MAX_SIZE + 4 * CIRCLE_SPACING
+
+#define BLACK ((GdkRGBA) { 0, 0, 0, 1 })
+#define WHITE ((GdkRGBA) { 1, 1, 1, 1 })
+
+struct _ClapperGtkBufferingPaintable
+{
+ GObject parent;
+
+ gfloat sizes[3]; // current size of each circle
+ gboolean reverses[3]; // grow/shrink direction
+ gboolean initialized[3]; // big enough to start growing the next one
+};
+
+static GdkPaintableFlags
+clapper_gtk_buffering_paintable_get_flags (GdkPaintable *paintable)
+{
+ return GDK_PAINTABLE_STATIC_SIZE;
+}
+
+static gint
+clapper_gtk_buffering_paintable_get_intrinsic_size (GdkPaintable *paintable)
+{
+ return INTRINSIC_SIZE;
+}
+
+static void
+_draw_scaled_circle (GdkSnapshot *snapshot, gfloat scale)
+{
+ GskRoundedRect outline;
+ gfloat half_size = ((gfloat) CIRCLE_MAX_SIZE / 2) * scale;
+ gfloat inside_size = ((gfloat) (CIRCLE_MAX_SIZE - 2 * CIRCLE_OUTLINE) / 2) * scale;
+
+ /* Draw white inner circle */
+ gsk_rounded_rect_init_from_rect (&outline,
+ &GRAPHENE_RECT_INIT (-inside_size, -inside_size, 2 * inside_size, 2 * inside_size), inside_size);
+ gtk_snapshot_append_border (snapshot, &outline,
+ (gfloat[4]) { inside_size, inside_size, inside_size, inside_size },
+ (GdkRGBA[4]) { WHITE, WHITE, WHITE, WHITE });
+
+ /* Draw black circle border */
+ gsk_rounded_rect_init_from_rect (&outline,
+ &GRAPHENE_RECT_INIT (-half_size, -half_size, 2 * half_size, 2 * half_size), half_size);
+ gtk_snapshot_append_border (snapshot, &outline,
+ (gfloat[4]) { CIRCLE_OUTLINE, CIRCLE_OUTLINE, CIRCLE_OUTLINE, CIRCLE_OUTLINE },
+ (GdkRGBA[4]) { BLACK, BLACK, BLACK, BLACK });
+}
+
+static void
+clapper_gtk_buffering_paintable_snapshot (GdkPaintable *paintable,
+ GdkSnapshot *snapshot, gdouble width, gdouble height)
+{
+ ClapperGtkBufferingPaintable *self = CLAPPER_GTK_BUFFERING_PAINTABLE_CAST (paintable);
+ guint i;
+
+ gtk_snapshot_save (snapshot);
+
+ gtk_snapshot_translate (snapshot, &GRAPHENE_POINT_INIT (0, height / 2));
+ gtk_snapshot_scale (snapshot, MIN (width, height) / INTRINSIC_SIZE, MIN (width, height) / INTRINSIC_SIZE);
+
+ for (i = 0; i < 3; ++i) {
+ gtk_snapshot_translate (snapshot, &GRAPHENE_POINT_INIT (CIRCLE_SPACING + CIRCLE_MAX_SIZE / 2, 0));
+ _draw_scaled_circle (snapshot, self->sizes[i]);
+ gtk_snapshot_translate (snapshot, &GRAPHENE_POINT_INIT (CIRCLE_MAX_SIZE / 2, 0));
+ }
+
+ gtk_snapshot_restore (snapshot);
+}
+
+static GdkPaintable *
+clapper_gtk_buffering_paintable_get_current_image (GdkPaintable *paintable)
+{
+ ClapperGtkBufferingPaintable *self = CLAPPER_GTK_BUFFERING_PAINTABLE_CAST (paintable);
+ ClapperGtkBufferingPaintable *copy = clapper_gtk_buffering_paintable_new ();
+ guint i;
+
+ /* Only current sizes are needed for static image */
+ for (i = 0; i < 3; ++i)
+ copy->sizes[i] = self->sizes[i];
+
+ return GDK_PAINTABLE (copy);
+}
+
+static void
+_paintable_iface_init (GdkPaintableInterface *iface)
+{
+ iface->get_flags = clapper_gtk_buffering_paintable_get_flags;
+ iface->get_intrinsic_width = clapper_gtk_buffering_paintable_get_intrinsic_size;
+ iface->get_intrinsic_height = clapper_gtk_buffering_paintable_get_intrinsic_size;
+ iface->snapshot = clapper_gtk_buffering_paintable_snapshot;
+ iface->get_current_image = clapper_gtk_buffering_paintable_get_current_image;
+}
+
+#define parent_class clapper_gtk_buffering_paintable_parent_class
+G_DEFINE_TYPE_WITH_CODE (ClapperGtkBufferingPaintable, clapper_gtk_buffering_paintable, G_TYPE_OBJECT,
+ G_IMPLEMENT_INTERFACE (GDK_TYPE_PAINTABLE, _paintable_iface_init))
+
+ClapperGtkBufferingPaintable *
+clapper_gtk_buffering_paintable_new (void)
+{
+ return g_object_new (CLAPPER_GTK_TYPE_BUFFERING_PAINTABLE, NULL);
+}
+
+void
+clapper_gtk_buffering_paintable_step (ClapperGtkBufferingPaintable *self)
+{
+ guint i;
+
+ for (i = 0; i < 3; ++i) {
+ /* If previous circle is not big enough
+ * do not start animating the next one */
+ if (i > 0 && !self->initialized[i - 1])
+ break;
+
+ if (!self->initialized[i] && self->sizes[i] >= 0.3)
+ self->initialized[i] = TRUE;
+
+ self->sizes[i] += (self->reverses[i]) ? -0.04 : 0.04;
+ if (self->sizes[i] > 1.0) {
+ self->sizes[i] = 1.0;
+ self->reverses[i] = TRUE;
+ } else if (self->sizes[i] < 0.0) {
+ self->sizes[i] = 0.0;
+ self->reverses[i] = FALSE;
+ }
+ }
+
+ gdk_paintable_invalidate_contents ((GdkPaintable *) self);
+}
+
+void
+clapper_gtk_buffering_paintable_reset (ClapperGtkBufferingPaintable *self)
+{
+ guint i;
+
+ for (i = 0; i < 3; ++i) {
+ self->sizes[i] = 0;
+ self->reverses[i] = FALSE;
+ self->initialized[i] = FALSE;
+ }
+
+ gdk_paintable_invalidate_contents ((GdkPaintable *) self);
+}
+
+static void
+clapper_gtk_buffering_paintable_init (ClapperGtkBufferingPaintable *self)
+{
+}
+
+static void
+clapper_gtk_buffering_paintable_class_init (ClapperGtkBufferingPaintableClass *klass)
+{
+}
diff --git a/src/lib/clapper-gtk/clapper-gtk-container-private.h b/src/lib/clapper-gtk/clapper-gtk-container-private.h
new file mode 100644
index 00000000..7a6a1e17
--- /dev/null
+++ b/src/lib/clapper-gtk/clapper-gtk-container-private.h
@@ -0,0 +1,28 @@
+/* Clapper GTK Integration Library
+ * Copyright (C) 2024 Rafał Dzięgiel
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+#pragma once
+
+#include "clapper-gtk-container.h"
+
+G_BEGIN_DECLS
+
+void clapper_gtk_container_emit_adapt (ClapperGtkContainer *container, gboolean adapt);
+
+G_END_DECLS
diff --git a/src/lib/clapper-gtk/clapper-gtk-container.c b/src/lib/clapper-gtk/clapper-gtk-container.c
new file mode 100644
index 00000000..3115f60e
--- /dev/null
+++ b/src/lib/clapper-gtk/clapper-gtk-container.c
@@ -0,0 +1,413 @@
+/* Clapper GTK Integration Library
+ * Copyright (C) 2024 Rafał Dzięgiel
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+/**
+ * ClapperGtkContainer:
+ *
+ * A simple container widget that holds just one child.
+ *
+ * It is designed to work well with OSD overlay, adding some useful functionalities
+ * to it, such as width and height that widget should target. This helps with
+ * implementing simple adaptive widgets by observing its own width and signalling
+ * when adaptive threshold is reached.
+ *
+ * You can use this when you need to create a widget that is adaptive or should have
+ * a limited maximal width/height.
+ *
+ * If you need to have more then single widget as child, place a widget that
+ * can hold multiple children such as [class@Gtk.Box] as a single conatiner child
+ * and then your widgets into that child.
+ */
+
+#include "clapper-gtk-container.h"
+#include "clapper-gtk-container-private.h"
+#include "clapper-gtk-limited-layout-private.h"
+
+#define parent_class clapper_gtk_container_parent_class
+G_DEFINE_TYPE (ClapperGtkContainer, clapper_gtk_container, GTK_TYPE_WIDGET)
+
+enum
+{
+ PROP_0,
+ PROP_WIDTH_TARGET,
+ PROP_HEIGHT_TARGET,
+ PROP_ADAPTIVE_WIDTH,
+ PROP_ADAPTIVE_HEIGHT,
+ PROP_LAST
+};
+
+enum
+{
+ SIGNAL_ADAPT,
+ SIGNAL_LAST
+};
+
+static GParamSpec *param_specs[PROP_LAST] = { NULL, };
+static guint signals[SIGNAL_LAST] = { 0, };
+
+static inline void
+_unparent_child (ClapperGtkContainer *self)
+{
+ GtkWidget *child;
+
+ if ((child = gtk_widget_get_first_child (GTK_WIDGET (self))))
+ gtk_widget_unparent (child);
+}
+
+/**
+ * clapper_gtk_container_new:
+ *
+ * Creates a new #ClapperGtkContainer instance.
+ *
+ * Returns: a new container #GtkWidget.
+ */
+GtkWidget *
+clapper_gtk_container_new (void)
+{
+ return g_object_new (CLAPPER_GTK_TYPE_CONTAINER, NULL);
+}
+
+/**
+ * clapper_gtk_container_set_child:
+ * @container: a #ClapperGtkContainer
+ *
+ * Set a child #GtkWidget of @container.
+ */
+void
+clapper_gtk_container_set_child (ClapperGtkContainer *self, GtkWidget *child)
+{
+ g_return_if_fail (CLAPPER_GTK_IS_CONTAINER (self));
+ g_return_if_fail (GTK_IS_WIDGET (child));
+
+ _unparent_child (self);
+ gtk_widget_set_parent (child, GTK_WIDGET (self));
+}
+
+/**
+ * clapper_gtk_container_get_child:
+ * @container: a #ClapperGtkContainer
+ *
+ * Get a child #GtkWidget of @container.
+ *
+ * Returns: (transfer none) (nullable): #GtkWidget set as child.
+ */
+GtkWidget *
+clapper_gtk_container_get_child (ClapperGtkContainer *self)
+{
+ g_return_val_if_fail (CLAPPER_GTK_IS_CONTAINER (self), NULL);
+
+ return gtk_widget_get_first_child (GTK_WIDGET (self));
+}
+
+/**
+ * clapper_gtk_container_set_width_target:
+ * @container: a #ClapperGtkContainer
+ * @width: width to target -1 to restore default behavior
+ *
+ * Set a width that @container should target. When set container
+ * will not stretch beyond set @width while still expanding into
+ * possible boundaries trying to reach its target.
+ */
+void
+clapper_gtk_container_set_width_target (ClapperGtkContainer *self, gint width)
+{
+ GtkLayoutManager *layout;
+
+ g_return_if_fail (CLAPPER_GTK_IS_CONTAINER (self));
+
+ layout = gtk_widget_get_layout_manager (GTK_WIDGET (self));
+ clapper_gtk_limited_layout_set_max_width (CLAPPER_GTK_LIMITED_LAYOUT_CAST (layout), width);
+}
+
+/**
+ * clapper_gtk_container_get_width_target:
+ * @container: a #ClapperGtkContainer
+ *
+ * Get a @container width target.
+ *
+ * Returns: width target set by user or -1 when none.
+ */
+gint
+clapper_gtk_container_get_width_target (ClapperGtkContainer *self)
+{
+ GtkLayoutManager *layout;
+
+ g_return_val_if_fail (CLAPPER_GTK_IS_CONTAINER (self), -1);
+
+ layout = gtk_widget_get_layout_manager (GTK_WIDGET (self));
+ return clapper_gtk_limited_layout_get_max_width (CLAPPER_GTK_LIMITED_LAYOUT_CAST (layout));
+}
+
+/**
+ * clapper_gtk_container_set_height_target:
+ * @container: a #ClapperGtkContainer
+ * @height: height to target or -1 to restore default behavior
+ *
+ * Same as clapper_gtk_container_set_width_target() but for widget height.
+ */
+void
+clapper_gtk_container_set_height_target (ClapperGtkContainer *self, gint height)
+{
+ GtkLayoutManager *layout;
+
+ g_return_if_fail (CLAPPER_GTK_IS_CONTAINER (self));
+
+ layout = gtk_widget_get_layout_manager (GTK_WIDGET (self));
+ clapper_gtk_limited_layout_set_max_height (CLAPPER_GTK_LIMITED_LAYOUT_CAST (layout), height);
+}
+
+/**
+ * clapper_gtk_container_get_height_target:
+ * @container: a #ClapperGtkContainer
+ *
+ * Get a @container height target.
+ *
+ * Returns: height target set by user or -1 when none.
+ */
+gint
+clapper_gtk_container_get_height_target (ClapperGtkContainer *self)
+{
+ GtkLayoutManager *layout;
+
+ g_return_val_if_fail (CLAPPER_GTK_IS_CONTAINER (self), -1);
+
+ layout = gtk_widget_get_layout_manager (GTK_WIDGET (self));
+ return clapper_gtk_limited_layout_get_max_height (CLAPPER_GTK_LIMITED_LAYOUT_CAST (layout));
+}
+
+/**
+ * clapper_gtk_container_set_adaptive_width:
+ * @container: a #ClapperGtkContainer
+ * @width: a threshold on which adapt signal should be triggered or -1 to disable.
+ *
+ * Set an adaptive width threshold. When widget is resized to value or lower,
+ * an [signal@ClapperGtk.Container::adapt] signal will be emitted with %TRUE to
+ * notify implementation about mobile adaptation request, otherwise %FALSE when
+ * both threshold values are exceeded.
+ */
+void
+clapper_gtk_container_set_adaptive_width (ClapperGtkContainer *self, gint width)
+{
+ GtkLayoutManager *layout;
+
+ g_return_if_fail (CLAPPER_GTK_IS_CONTAINER (self));
+
+ layout = gtk_widget_get_layout_manager (GTK_WIDGET (self));
+ clapper_gtk_limited_layout_set_adaptive_width (
+ CLAPPER_GTK_LIMITED_LAYOUT_CAST (layout), width);
+}
+
+/**
+ * clapper_gtk_container_get_adaptive_width:
+ * @container: a #ClapperGtkContainer
+ *
+ * Get a @container adaptive width threshold.
+ *
+ * Returns: adaptive width set by user or -1 when none.
+ */
+gint
+clapper_gtk_container_get_adaptive_width (ClapperGtkContainer *self)
+{
+ GtkLayoutManager *layout;
+
+ g_return_val_if_fail (CLAPPER_GTK_IS_CONTAINER (self), -1);
+
+ layout = gtk_widget_get_layout_manager (GTK_WIDGET (self));
+ return clapper_gtk_limited_layout_get_adaptive_width (CLAPPER_GTK_LIMITED_LAYOUT_CAST (layout));
+}
+
+/**
+ * clapper_gtk_container_set_adaptive_height:
+ * @container: a #ClapperGtkContainer
+ * @height: a threshold on which adapt signal should be triggered or -1 to disable.
+ *
+ * Set an adaptive height threshold. When widget is resized to value or lower,
+ * an [signal@ClapperGtk.Container::adapt] signal will be emitted with %TRUE to
+ * notify implementation about mobile adaptation request, otherwise %FALSE when
+ * both threshold values are exceeded.
+ */
+void
+clapper_gtk_container_set_adaptive_height (ClapperGtkContainer *self, gint height)
+{
+ GtkLayoutManager *layout;
+
+ g_return_if_fail (CLAPPER_GTK_IS_CONTAINER (self));
+
+ layout = gtk_widget_get_layout_manager (GTK_WIDGET (self));
+ clapper_gtk_limited_layout_set_adaptive_height (
+ CLAPPER_GTK_LIMITED_LAYOUT_CAST (layout), height);
+}
+
+/**
+ * clapper_gtk_container_get_adaptive_height:
+ * @container: a #ClapperGtkContainer
+ *
+ * Get a @container adaptive height threshold.
+ *
+ * Returns: adaptive height set by user or -1 when none.
+ */
+gint
+clapper_gtk_container_get_adaptive_height (ClapperGtkContainer *self)
+{
+ GtkLayoutManager *layout;
+
+ g_return_val_if_fail (CLAPPER_GTK_IS_CONTAINER (self), -1);
+
+ layout = gtk_widget_get_layout_manager (GTK_WIDGET (self));
+ return clapper_gtk_limited_layout_get_adaptive_height (CLAPPER_GTK_LIMITED_LAYOUT_CAST (layout));
+}
+
+void
+clapper_gtk_container_emit_adapt (ClapperGtkContainer *self, gboolean adapt)
+{
+ if (g_signal_handler_find (self, G_SIGNAL_MATCH_ID,
+ signals[SIGNAL_ADAPT], 0, NULL, NULL, NULL) != 0) {
+ g_signal_emit (self, signals[SIGNAL_ADAPT], 0, adapt);
+ }
+}
+
+static void
+clapper_gtk_container_init (ClapperGtkContainer *self)
+{
+}
+
+static void
+clapper_gtk_container_dispose (GObject *object)
+{
+ ClapperGtkContainer *self = CLAPPER_GTK_CONTAINER_CAST (object);
+
+ _unparent_child (self);
+
+ G_OBJECT_CLASS (parent_class)->dispose (object);
+}
+
+static void
+clapper_gtk_container_get_property (GObject *object, guint prop_id,
+ GValue *value, GParamSpec *pspec)
+{
+ ClapperGtkContainer *self = CLAPPER_GTK_CONTAINER_CAST (object);
+
+ switch (prop_id) {
+ case PROP_WIDTH_TARGET:
+ g_value_set_int (value, clapper_gtk_container_get_width_target (self));
+ break;
+ case PROP_HEIGHT_TARGET:
+ g_value_set_int (value, clapper_gtk_container_get_height_target (self));
+ break;
+ case PROP_ADAPTIVE_WIDTH:
+ g_value_set_int (value, clapper_gtk_container_get_adaptive_width (self));
+ break;
+ case PROP_ADAPTIVE_HEIGHT:
+ g_value_set_int (value, clapper_gtk_container_get_adaptive_height (self));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+clapper_gtk_container_set_property (GObject *object, guint prop_id,
+ const GValue *value, GParamSpec *pspec)
+{
+ ClapperGtkContainer *self = CLAPPER_GTK_CONTAINER_CAST (object);
+
+ switch (prop_id) {
+ case PROP_WIDTH_TARGET:
+ clapper_gtk_container_set_width_target (self, g_value_get_int (value));
+ break;
+ case PROP_HEIGHT_TARGET:
+ clapper_gtk_container_set_height_target (self, g_value_get_int (value));
+ break;
+ case PROP_ADAPTIVE_WIDTH:
+ clapper_gtk_container_set_adaptive_width (self, g_value_get_int (value));
+ break;
+ case PROP_ADAPTIVE_HEIGHT:
+ clapper_gtk_container_set_adaptive_height (self, g_value_get_int (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+clapper_gtk_container_class_init (ClapperGtkContainerClass *klass)
+{
+ GObjectClass *gobject_class = (GObjectClass *) klass;
+ GtkWidgetClass *widget_class = (GtkWidgetClass *) klass;
+
+ gobject_class->get_property = clapper_gtk_container_get_property;
+ gobject_class->set_property = clapper_gtk_container_set_property;
+ gobject_class->dispose = clapper_gtk_container_dispose;
+
+ /**
+ * ClapperGtkContainer:width-target:
+ *
+ * Width that container should target.
+ */
+ param_specs[PROP_WIDTH_TARGET] = g_param_spec_int ("width-target",
+ NULL, NULL, -1, G_MAXINT, -1,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * ClapperGtkContainer:height-target:
+ *
+ * Height that container should target.
+ */
+ param_specs[PROP_HEIGHT_TARGET] = g_param_spec_int ("height-target",
+ NULL, NULL, -1, G_MAXINT, -1,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * ClapperGtkContainer:adaptive-width:
+ *
+ * Adaptive width threshold that triggers [signal@ClapperGtk.Container::adapt] signal.
+ */
+ param_specs[PROP_ADAPTIVE_WIDTH] = g_param_spec_int ("adaptive-width",
+ NULL, NULL, -1, G_MAXINT, -1,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * ClapperGtkContainer:adaptive-height:
+ *
+ * Adaptive height threshold that triggers [signal@ClapperGtk.Container::adapt] signal.
+ */
+ param_specs[PROP_ADAPTIVE_HEIGHT] = g_param_spec_int ("adaptive-height",
+ NULL, NULL, -1, G_MAXINT, -1,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * ClapperGtkContainer::adapt:
+ * @container: a #ClapperGtkContainer
+ * @adapt: %TRUE if narrowness reached adaptive threshold, %FALSE otherwise
+ *
+ * A helper signal for implementing mobile/narrow adaptive
+ * behavior on descendants.
+ */
+ signals[SIGNAL_ADAPT] = g_signal_new ("adapt",
+ G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST | G_SIGNAL_NO_RECURSE | G_SIGNAL_NO_HOOKS,
+ 0, NULL, NULL, NULL, G_TYPE_NONE, 1, G_TYPE_BOOLEAN);
+
+ g_object_class_install_properties (gobject_class, PROP_LAST, param_specs);
+
+ gtk_widget_class_set_layout_manager_type (widget_class, CLAPPER_GTK_TYPE_LIMITED_LAYOUT);
+ gtk_widget_class_set_accessible_role (widget_class, GTK_ACCESSIBLE_ROLE_GENERIC);
+ gtk_widget_class_set_css_name (widget_class, "clapper-gtk-container");
+}
diff --git a/src/lib/clapper-gtk/clapper-gtk-container.h b/src/lib/clapper-gtk/clapper-gtk-container.h
new file mode 100644
index 00000000..dbc54002
--- /dev/null
+++ b/src/lib/clapper-gtk/clapper-gtk-container.h
@@ -0,0 +1,67 @@
+/* Clapper GTK Integration Library
+ * Copyright (C) 2024 Rafał Dzięgiel
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+#pragma once
+
+#if !defined(__CLAPPER_GTK_INSIDE__) && !defined(CLAPPER_GTK_COMPILATION)
+#error "Only can be included directly."
+#endif
+
+#include
+#include
+#include
+
+G_BEGIN_DECLS
+
+#define CLAPPER_GTK_TYPE_CONTAINER (clapper_gtk_container_get_type())
+#define CLAPPER_GTK_CONTAINER_CAST(obj) ((ClapperGtkContainer *)(obj))
+
+G_DECLARE_DERIVABLE_TYPE (ClapperGtkContainer, clapper_gtk_container, CLAPPER_GTK, CONTAINER, GtkWidget)
+
+struct _ClapperGtkContainerClass
+{
+ GtkWidgetClass parent_class;
+
+ /*< private >*/
+ gpointer padding[4];
+};
+
+GtkWidget * clapper_gtk_container_new (void);
+
+void clapper_gtk_container_set_child (ClapperGtkContainer *container, GtkWidget *child);
+
+GtkWidget * clapper_gtk_container_get_child (ClapperGtkContainer *container);
+
+void clapper_gtk_container_set_width_target (ClapperGtkContainer *container, gint width);
+
+gint clapper_gtk_container_get_width_target (ClapperGtkContainer *container);
+
+void clapper_gtk_container_set_height_target (ClapperGtkContainer *container, gint height);
+
+gint clapper_gtk_container_get_height_target (ClapperGtkContainer *container);
+
+void clapper_gtk_container_set_adaptive_width (ClapperGtkContainer *container, gint width);
+
+gint clapper_gtk_container_get_adaptive_width (ClapperGtkContainer *container);
+
+void clapper_gtk_container_set_adaptive_height (ClapperGtkContainer *container, gint height);
+
+gint clapper_gtk_container_get_adaptive_height (ClapperGtkContainer *container);
+
+G_END_DECLS
diff --git a/src/lib/clapper-gtk/clapper-gtk-enums.h b/src/lib/clapper-gtk/clapper-gtk-enums.h
new file mode 100644
index 00000000..f761c363
--- /dev/null
+++ b/src/lib/clapper-gtk/clapper-gtk-enums.h
@@ -0,0 +1,50 @@
+/* Clapper GTK Integration Library
+ * Copyright (C) 2024 Rafał Dzięgiel
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+#pragma once
+
+#if !defined(__CLAPPER_GTK_INSIDE__) && !defined(CLAPPER_GTK_COMPILATION)
+#error "Only can be included directly."
+#endif
+
+#include
+#include
+
+G_BEGIN_DECLS
+
+/**
+ * ClapperGtkVideoActionMask:
+ * @CLAPPER_GTK_VIDEO_ACTION_NONE: No action
+ * @CLAPPER_GTK_VIDEO_ACTION_REVEAL_OVERLAYS: Reveal fading overlays
+ * @CLAPPER_GTK_VIDEO_ACTION_TOGGLE_PLAY: Toggle playback (triggered by single click/tap)
+ * @CLAPPER_GTK_VIDEO_ACTION_TOGGLE_FULLSCREEN: Toggle fullscreen (triggered by double click/tap)
+ * @CLAPPER_GTK_VIDEO_ACTION_SEEK_REQUEST: Seek request (triggered by double tap on screen side)
+ * @CLAPPER_GTK_VIDEO_ACTION_ANY: All of the above
+ */
+typedef enum
+{
+ CLAPPER_GTK_VIDEO_ACTION_NONE = 0,
+ CLAPPER_GTK_VIDEO_ACTION_REVEAL_OVERLAYS = 1 << 0,
+ CLAPPER_GTK_VIDEO_ACTION_TOGGLE_PLAY = 1 << 1,
+ CLAPPER_GTK_VIDEO_ACTION_TOGGLE_FULLSCREEN = 1 << 2,
+ CLAPPER_GTK_VIDEO_ACTION_SEEK_REQUEST = 1 << 3,
+ CLAPPER_GTK_VIDEO_ACTION_ANY = 0x3FFFFFF
+} ClapperGtkVideoActionMask;
+
+G_END_DECLS
diff --git a/src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c b/src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c
new file mode 100644
index 00000000..69723806
--- /dev/null
+++ b/src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c
@@ -0,0 +1,801 @@
+/* Clapper GTK Integration Library
+ * Copyright (C) 2024 Rafał Dzięgiel
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+/**
+ * ClapperGtkExtraMenuButton:
+ *
+ * A menu button with extra options.
+ */
+
+#include "config.h"
+
+#include
+#include
+#include
+
+#include "clapper-gtk-extra-menu-button.h"
+#include "clapper-gtk-utils-private.h"
+
+#define PERCENTAGE_ROUND(a) (round ((gdouble) a / 0.01) * 0.01)
+
+#define DEFAULT_CAN_OPEN_SUBTITLES FALSE
+
+#define GST_CAT_DEFAULT clapper_gtk_extra_menu_button_debug
+GST_DEBUG_CATEGORY_STATIC (GST_CAT_DEFAULT);
+
+struct _ClapperGtkExtraMenuButton
+{
+ GtkWidget parent;
+
+ GtkWidget *menu_button;
+
+ GtkWidget *volume_box;
+ GtkWidget *volume_button;
+ GtkWidget *volume_spin;
+
+ GtkWidget *speed_box;
+ GtkWidget *speed_button;
+ GtkWidget *speed_spin;
+
+ GtkWidget *top_separator;
+
+ GtkWidget *video_list_view;
+ GtkScrolledWindow *video_sw;
+
+ GtkWidget *audio_list_view;
+ GtkScrolledWindow *audio_sw;
+
+ GtkWidget *subtitle_list_view;
+ GtkScrolledWindow *subtitle_sw;
+
+ ClapperPlayer *player;
+ ClapperMediaItem *current_item;
+
+ GSimpleActionGroup *action_group;
+
+ gboolean mute;
+
+ GBinding *volume_binding;
+ GBinding *speed_binding;
+
+ GBinding *video_binding;
+ GBinding *audio_binding;
+ GBinding *subtitle_binding;
+
+ gboolean can_open_subtitles;
+};
+
+#define parent_class clapper_gtk_extra_menu_button_parent_class
+G_DEFINE_TYPE (ClapperGtkExtraMenuButton, clapper_gtk_extra_menu_button, GTK_TYPE_WIDGET)
+
+enum
+{
+ PROP_0,
+ PROP_VOLUME_VISIBLE,
+ PROP_SPEED_VISIBLE,
+ PROP_CAN_OPEN_SUBTITLES,
+ PROP_LAST
+};
+
+enum
+{
+ SIGNAL_OPEN_SUBTITLES,
+ SIGNAL_LAST
+};
+
+static GParamSpec *param_specs[PROP_LAST] = { NULL, };
+static guint signals[SIGNAL_LAST] = { 0, };
+
+static void
+_set_action_enabled (ClapperGtkExtraMenuButton *self, const gchar *name, gboolean enabled)
+{
+ GAction *action = g_action_map_lookup_action (G_ACTION_MAP (self->action_group), name);
+ gboolean was_enabled = g_action_get_enabled (action);
+
+ if (was_enabled == enabled)
+ return;
+
+ g_simple_action_set_enabled (G_SIMPLE_ACTION (action), enabled);
+}
+
+static gboolean
+_double_transform_func (GBinding *binding, const GValue *from_value,
+ GValue *to_value, gpointer user_data G_GNUC_UNUSED)
+{
+ gdouble val_dbl = g_value_get_double (from_value);
+
+ g_value_set_double (to_value, PERCENTAGE_ROUND (val_dbl));
+ return TRUE;
+}
+
+static gint
+volume_spin_input_cb (GtkSpinButton *spin_button, gdouble *value, ClapperGtkExtraMenuButton *self)
+{
+ const gchar *text = gtk_editable_get_text (GTK_EDITABLE (spin_button));
+ gchar *sign = NULL;
+ gdouble volume = g_ascii_strtod (text, &sign);
+
+ if (volume < 0 || volume > 200
+ || (sign && sign[0] != '\0' && sign[0] != '%'))
+ return GTK_INPUT_ERROR;
+
+ volume /= 100.0;
+
+ if (volume > 0.99 && volume < 1.01)
+ volume = 1.0;
+
+ *value = volume;
+
+ return TRUE;
+}
+
+static gboolean
+volume_spin_output_cb (GtkSpinButton *spin_button, ClapperGtkExtraMenuButton *self)
+{
+ GtkAdjustment *adjustment = gtk_spin_button_get_adjustment (spin_button);
+ gdouble volume = gtk_adjustment_get_value (adjustment);
+ gchar *volume_str;
+
+ volume_str = g_strdup_printf ("%.0lf%%", volume * 100);
+ gtk_editable_set_text (GTK_EDITABLE (spin_button), volume_str);
+ g_free (volume_str);
+
+ return TRUE;
+}
+
+static void
+volume_spin_changed_cb (GtkSpinButton *spin_button, ClapperGtkExtraMenuButton *self)
+{
+ GtkAdjustment *adjustment = gtk_spin_button_get_adjustment (spin_button);
+ gdouble volume = gtk_adjustment_get_value (adjustment);
+
+ gtk_button_set_icon_name (GTK_BUTTON (self->volume_button),
+ clapper_gtk_get_icon_name_for_volume ((!self->mute) ? volume : 0));
+}
+
+static gint
+speed_spin_input_cb (GtkSpinButton *spin_button, gdouble *value, ClapperGtkExtraMenuButton *self)
+{
+ const gchar *text = gtk_editable_get_text (GTK_EDITABLE (spin_button));
+ gchar *sign = NULL;
+ gdouble speed = g_ascii_strtod (text, &sign);
+
+ if (speed < 0.05 || speed > 2.0
+ || (sign && sign[0] != '\0' && sign[0] != 'x'))
+ return GTK_INPUT_ERROR;
+
+ if (speed > 0.99 && speed < 1.01)
+ speed = 1.0;
+
+ *value = speed;
+
+ return TRUE;
+}
+
+static gboolean
+speed_spin_output_cb (GtkSpinButton *spin_button, ClapperGtkExtraMenuButton *self)
+{
+ GtkAdjustment *adjustment = gtk_spin_button_get_adjustment (spin_button);
+ gdouble speed = gtk_adjustment_get_value (adjustment);
+ gchar *speed_str;
+
+ speed_str = g_strdup_printf ("%.2lfx", speed);
+ gtk_editable_set_text (GTK_EDITABLE (spin_button), speed_str);
+ g_free (speed_str);
+
+ return TRUE;
+}
+
+static void
+speed_spin_changed_cb (GtkSpinButton *spin_button, ClapperGtkExtraMenuButton *self)
+{
+ GtkAdjustment *adjustment = gtk_spin_button_get_adjustment (spin_button);
+ gdouble speed = gtk_adjustment_get_value (adjustment);
+
+ gtk_button_set_icon_name (GTK_BUTTON (self->speed_button),
+ clapper_gtk_get_icon_name_for_speed (speed));
+}
+
+static void
+visible_submenu_changed_cb (GtkPopoverMenu *popover_menu,
+ GParamSpec *pspec G_GNUC_UNUSED, ClapperGtkExtraMenuButton *self)
+{
+ gchar *name = NULL;
+ gboolean in_video, in_audio, in_subtitles;
+
+ g_object_get (popover_menu, "visible-submenu", &name, NULL);
+
+ /* TODO: Check if we have to compare translated strings here */
+ GST_DEBUG ("Visible submenu changed to: \"%s\"", name);
+
+ in_video = (g_strcmp0 (name, _("Video")) == 0);
+ in_audio = (g_strcmp0 (name, _("Audio")) == 0);
+ in_subtitles = (g_strcmp0 (name, _("Subtitles")) == 0);
+
+ /* XXX: This works around the issue where popover does not adapt its
+ * width when navigating submenus making spin buttons unnecesary centered */
+ gtk_scrolled_window_set_propagate_natural_width (self->video_sw, in_video);
+ gtk_scrolled_window_set_propagate_natural_width (self->audio_sw, in_audio);
+ gtk_scrolled_window_set_propagate_natural_width (self->subtitle_sw, in_subtitles);
+
+ g_free (name);
+}
+
+static void
+_subtitles_enabled_changed_cb (ClapperPlayer *player,
+ GParamSpec *pspec G_GNUC_UNUSED, ClapperGtkExtraMenuButton *self)
+{
+ GAction *action = g_action_map_lookup_action (
+ G_ACTION_MAP (self->action_group), "subtitle-stream-enabled");
+ GVariant *variant = g_action_get_state (action);
+ gboolean was_enabled, enabled;
+
+ was_enabled = g_variant_get_boolean (variant);
+ enabled = clapper_player_get_subtitles_enabled (player);
+
+ g_variant_unref (variant);
+
+ if (was_enabled == enabled)
+ return;
+
+ variant = g_variant_ref_sink (g_variant_new_boolean (enabled));
+
+ g_simple_action_set_state (G_SIMPLE_ACTION (action), variant);
+ g_variant_unref (variant);
+}
+
+static void
+_player_mute_changed_cb (ClapperPlayer *player,
+ GParamSpec *pspec G_GNUC_UNUSED, ClapperGtkExtraMenuButton *self)
+{
+ self->mute = clapper_player_get_mute (player);
+
+ volume_spin_changed_cb (GTK_SPIN_BUTTON (self->volume_spin), self);
+}
+
+static void
+_queue_current_item_changed_cb (ClapperQueue *queue,
+ GParamSpec *pspec G_GNUC_UNUSED, ClapperGtkExtraMenuButton *self)
+{
+ ClapperMediaItem *current_item = clapper_queue_get_current_item (queue);
+
+ /* NOTE: This is also called after popover "map" signal */
+
+ if (gst_object_replace ((GstObject **) &self->current_item, GST_OBJECT_CAST (current_item))) {
+ _set_action_enabled (self, "open-subtitle-stream",
+ (self->can_open_subtitles && self->current_item != NULL));
+ }
+
+ gst_clear_object (¤t_item);
+}
+
+static void
+change_subtitle_stream_enabled (GSimpleAction *action, GVariant *value, gpointer user_data)
+{
+ ClapperGtkExtraMenuButton *self = CLAPPER_GTK_EXTRA_MENU_BUTTON_CAST (user_data);
+ gboolean enable = g_variant_get_boolean (value);
+
+ if (G_LIKELY (self->player != NULL))
+ clapper_player_set_subtitles_enabled (self->player, enable);
+
+ g_simple_action_set_state (action, value);
+}
+
+static void
+open_subtitle_stream (GSimpleAction *action, GVariant *param, gpointer user_data)
+{
+ ClapperGtkExtraMenuButton *self = CLAPPER_GTK_EXTRA_MENU_BUTTON_CAST (user_data);
+
+ /* We should not be here otherwise */
+ if (G_LIKELY (self->can_open_subtitles && self->current_item != NULL))
+ g_signal_emit (self, signals[SIGNAL_OPEN_SUBTITLES], 0, self->current_item);
+}
+
+static void
+_determine_top_separator_visibility (ClapperGtkExtraMenuButton *self)
+{
+ gboolean visible = gtk_widget_get_visible (self->volume_box)
+ || gtk_widget_get_visible (self->speed_box);
+
+ gtk_widget_set_visible (self->top_separator, visible);
+}
+
+/**
+ * clapper_gtk_extra_menu_button_new:
+ *
+ * Creates a new #ClapperGtkExtraMenuButton instance.
+ *
+ * Returns: a new extra menu button #GtkWidget.
+ */
+GtkWidget *
+clapper_gtk_extra_menu_button_new (void)
+{
+ return g_object_new (CLAPPER_GTK_TYPE_EXTRA_MENU_BUTTON, NULL);
+}
+
+/**
+ * clapper_gtk_extra_menu_button_set_volume_visible:
+ * @button: a #ClapperGtkExtraMenuButton
+ * @visible: whether visible
+ *
+ * Set whether volume control inside popover should be visible.
+ */
+void
+clapper_gtk_extra_menu_button_set_volume_visible (ClapperGtkExtraMenuButton *self, gboolean visible)
+{
+ g_return_if_fail (CLAPPER_GTK_IS_EXTRA_MENU_BUTTON (self));
+
+ if (gtk_widget_get_visible (self->volume_box) != visible) {
+ gtk_widget_set_visible (self->volume_box, visible);
+ _determine_top_separator_visibility (self);
+
+ g_object_notify_by_pspec (G_OBJECT (self), param_specs[PROP_VOLUME_VISIBLE]);
+ }
+}
+
+/**
+ * clapper_gtk_extra_menu_button_get_volume_visible:
+ * @button: a #ClapperGtkExtraMenuButton
+ *
+ * Get whether volume control inside popover is visible.
+ *
+ * Returns: TRUE if volume control is visible, %FALSE otherwise.
+ */
+gboolean
+clapper_gtk_extra_menu_button_get_volume_visible (ClapperGtkExtraMenuButton *self)
+{
+ g_return_val_if_fail (CLAPPER_GTK_IS_EXTRA_MENU_BUTTON (self), FALSE);
+
+ return gtk_widget_get_visible (self->volume_box);
+}
+
+/**
+ * clapper_gtk_extra_menu_button_set_speed_visible:
+ * @button: a #ClapperGtkExtraMenuButton
+ * @visible: whether visible
+ *
+ * Set whether speed control inside popover should be visible.
+ */
+void
+clapper_gtk_extra_menu_button_set_speed_visible (ClapperGtkExtraMenuButton *self, gboolean visible)
+{
+ g_return_if_fail (CLAPPER_GTK_IS_EXTRA_MENU_BUTTON (self));
+
+ if (gtk_widget_get_visible (self->speed_box) != visible) {
+ gtk_widget_set_visible (self->speed_box, visible);
+ _determine_top_separator_visibility (self);
+
+ g_object_notify_by_pspec (G_OBJECT (self), param_specs[PROP_SPEED_VISIBLE]);
+ }
+}
+
+/**
+ * clapper_gtk_extra_menu_button_get_speed_visible:
+ * @button: a #ClapperGtkExtraMenuButton
+ *
+ * Get whether speed control inside popover is visible.
+ *
+ * Returns: %TRUE if speed control is visible, %FALSE otherwise.
+ */
+gboolean
+clapper_gtk_extra_menu_button_get_speed_visible (ClapperGtkExtraMenuButton *self)
+{
+ g_return_val_if_fail (CLAPPER_GTK_IS_EXTRA_MENU_BUTTON (self), FALSE);
+
+ return gtk_widget_get_visible (self->speed_box);
+}
+
+/**
+ * clapper_gtk_extra_menu_button_set_can_open_subtitles:
+ * @button: a #ClapperGtkExtraMenuButton
+ * @allowed: whether opening subtitles should be allowed
+ *
+ * Set whether an option to open external subtitle stream should be allowed.
+ *
+ * Note that this [class@Gtk.Widget] can only add subtitles to currently playing
+ * [class@Clapper.MediaItem]. When no media is selected, option to open subtitles
+ * will not be shown regardless how this option is set.
+ */
+void
+clapper_gtk_extra_menu_button_set_can_open_subtitles (ClapperGtkExtraMenuButton *self, gboolean allowed)
+{
+ gboolean changed;
+
+ g_return_if_fail (CLAPPER_GTK_IS_EXTRA_MENU_BUTTON (self));
+
+ if ((changed = self->can_open_subtitles != allowed)) {
+ self->can_open_subtitles = allowed;
+
+ _set_action_enabled (self, "open-subtitle-stream",
+ (self->can_open_subtitles && self->current_item != NULL));
+
+ g_object_notify_by_pspec (G_OBJECT (self), param_specs[PROP_CAN_OPEN_SUBTITLES]);
+ }
+}
+
+/**
+ * clapper_gtk_extra_menu_button_get_can_open_subtitles:
+ * @button: a #ClapperGtkExtraMenuButton
+ *
+ * Get whether an option to open external subtitle stream inside popover is visible.
+ *
+ * Returns: %TRUE if open subtitles is visible, %FALSE otherwise.
+ */
+gboolean
+clapper_gtk_extra_menu_button_get_can_open_subtitles (ClapperGtkExtraMenuButton *self)
+{
+ g_return_val_if_fail (CLAPPER_GTK_IS_EXTRA_MENU_BUTTON (self), FALSE);
+
+ return self->can_open_subtitles;
+}
+
+static void
+clapper_gtk_extra_menu_button_init (ClapperGtkExtraMenuButton *self)
+{
+ static GActionEntry action_entries[] = {
+ { "subtitle-stream-enabled", NULL, NULL, "true", change_subtitle_stream_enabled },
+ { "open-subtitle-stream", open_subtitle_stream, NULL, NULL, NULL },
+ };
+
+ gtk_widget_init_template (GTK_WIDGET (self));
+
+ self->action_group = g_simple_action_group_new ();
+
+ g_object_bind_property (self, "css-classes", self->menu_button,
+ "css-classes", G_BINDING_DEFAULT);
+
+ g_action_map_add_action_entries (G_ACTION_MAP (self->action_group),
+ action_entries, G_N_ELEMENTS (action_entries), self);
+ gtk_widget_insert_action_group (GTK_WIDGET (self),
+ "clappergtk", G_ACTION_GROUP (self->action_group));
+
+ /* Set default values */
+ self->can_open_subtitles = DEFAULT_CAN_OPEN_SUBTITLES;
+ _set_action_enabled (self, "open-subtitle-stream", self->can_open_subtitles);
+}
+
+static void
+clapper_gtk_extra_menu_button_compute_expand (GtkWidget *widget,
+ gboolean *hexpand_p, gboolean *vexpand_p)
+{
+ GtkWidget *child;
+ gboolean hexpand = FALSE;
+ gboolean vexpand = FALSE;
+
+ if ((child = gtk_widget_get_first_child (widget))) {
+ hexpand = gtk_widget_compute_expand (child, GTK_ORIENTATION_HORIZONTAL);
+ vexpand = gtk_widget_compute_expand (child, GTK_ORIENTATION_VERTICAL);
+ }
+
+ *hexpand_p = hexpand;
+ *vexpand_p = vexpand;
+}
+
+static void
+clapper_gtk_extra_menu_button_realize (GtkWidget *widget)
+{
+ ClapperGtkExtraMenuButton *self = CLAPPER_GTK_EXTRA_MENU_BUTTON_CAST (widget);
+
+ GST_TRACE_OBJECT (self, "Realize");
+
+ if ((self->player = clapper_gtk_get_player_from_ancestor (GTK_WIDGET (self)))) {
+ ClapperStreamList *stream_list;
+ GtkSingleSelection *selection;
+
+ g_signal_connect (self->player, "notify::mute",
+ G_CALLBACK (_player_mute_changed_cb), self);
+ self->mute = clapper_player_get_mute (self->player);
+
+ stream_list = clapper_player_get_video_streams (self->player);
+ selection = gtk_single_selection_new (gst_object_ref (stream_list));
+ gtk_single_selection_set_autoselect (selection, FALSE);
+
+ self->video_binding = g_object_bind_property (stream_list, "current-index",
+ selection, "selected", G_BINDING_SYNC_CREATE);
+
+ gtk_list_view_set_model (GTK_LIST_VIEW (self->video_list_view),
+ GTK_SELECTION_MODEL (selection));
+ g_object_unref (selection);
+
+ stream_list = clapper_player_get_audio_streams (self->player);
+ selection = gtk_single_selection_new (gst_object_ref (stream_list));
+ gtk_single_selection_set_autoselect (selection, FALSE);
+
+ self->audio_binding = g_object_bind_property (stream_list, "current-index",
+ selection, "selected", G_BINDING_SYNC_CREATE);
+
+ gtk_list_view_set_model (GTK_LIST_VIEW (self->audio_list_view),
+ GTK_SELECTION_MODEL (selection));
+ g_object_unref (selection);
+
+ stream_list = clapper_player_get_subtitle_streams (self->player);
+ selection = gtk_single_selection_new (gst_object_ref (stream_list));
+ gtk_single_selection_set_autoselect (selection, FALSE);
+
+ self->subtitle_binding = g_object_bind_property (stream_list, "current-index",
+ selection, "selected", G_BINDING_SYNC_CREATE);
+
+ gtk_list_view_set_model (GTK_LIST_VIEW (self->subtitle_list_view),
+ GTK_SELECTION_MODEL (selection));
+ g_object_unref (selection);
+ }
+
+ GTK_WIDGET_CLASS (parent_class)->realize (widget);
+}
+
+static void
+clapper_gtk_extra_menu_button_unrealize (GtkWidget *widget)
+{
+ ClapperGtkExtraMenuButton *self = CLAPPER_GTK_EXTRA_MENU_BUTTON_CAST (widget);
+
+ GST_TRACE_OBJECT (self, "Unrealize");
+
+ g_clear_pointer (&self->video_binding, g_binding_unbind);
+ g_clear_pointer (&self->audio_binding, g_binding_unbind);
+ g_clear_pointer (&self->subtitle_binding, g_binding_unbind);
+
+ gtk_list_view_set_model (GTK_LIST_VIEW (self->video_list_view), NULL);
+ gtk_list_view_set_model (GTK_LIST_VIEW (self->audio_list_view), NULL);
+ gtk_list_view_set_model (GTK_LIST_VIEW (self->subtitle_list_view), NULL);
+
+ if (self->player) {
+ g_signal_handlers_disconnect_by_func (self->player, _player_mute_changed_cb, self);
+
+ self->player = NULL;
+ }
+
+ GTK_WIDGET_CLASS (parent_class)->unrealize (widget);
+}
+
+static void
+popover_map_cb (GtkWidget *widget, ClapperGtkExtraMenuButton *self)
+{
+ ClapperQueue *queue;
+
+ GST_TRACE_OBJECT (self, "Popover map");
+
+ if (G_UNLIKELY (self->player == NULL))
+ return;
+
+ queue = clapper_player_get_queue (self->player);
+
+ self->volume_binding = g_object_bind_property_full (self->player, "volume",
+ self->volume_spin, "value",
+ G_BINDING_BIDIRECTIONAL | G_BINDING_SYNC_CREATE,
+ (GBindingTransformFunc) _double_transform_func,
+ (GBindingTransformFunc) _double_transform_func,
+ NULL, NULL);
+ self->speed_binding = g_object_bind_property_full (self->player, "speed",
+ self->speed_spin, "value",
+ G_BINDING_BIDIRECTIONAL | G_BINDING_SYNC_CREATE,
+ (GBindingTransformFunc) _double_transform_func,
+ (GBindingTransformFunc) _double_transform_func,
+ NULL, NULL);
+
+ g_signal_connect (self->player, "notify::subtitles-enabled",
+ G_CALLBACK (_subtitles_enabled_changed_cb), self);
+ _subtitles_enabled_changed_cb (self->player, NULL, self);
+
+ g_signal_connect (queue, "notify::current-item",
+ G_CALLBACK (_queue_current_item_changed_cb), self);
+ _queue_current_item_changed_cb (queue, NULL, self);
+}
+
+static void
+popover_unmap_cb (GtkWidget *widget, ClapperGtkExtraMenuButton *self)
+{
+ ClapperQueue *queue;
+
+ GST_TRACE_OBJECT (self, "Popover unmap");
+
+ if (G_UNLIKELY (self->player == NULL))
+ return;
+
+ queue = clapper_player_get_queue (self->player);
+
+ g_clear_pointer (&self->volume_binding, g_binding_unbind);
+ g_clear_pointer (&self->speed_binding, g_binding_unbind);
+
+ g_signal_handlers_disconnect_by_func (self->player, _subtitles_enabled_changed_cb, self);
+ g_signal_handlers_disconnect_by_func (queue, _queue_current_item_changed_cb, self);
+}
+
+static void
+clapper_gtk_extra_menu_button_dispose (GObject *object)
+{
+ ClapperGtkExtraMenuButton *self = CLAPPER_GTK_EXTRA_MENU_BUTTON_CAST (object);
+
+ gtk_widget_dispose_template (GTK_WIDGET (object), CLAPPER_GTK_TYPE_EXTRA_MENU_BUTTON);
+
+ g_clear_pointer (&self->menu_button, gtk_widget_unparent);
+
+ G_OBJECT_CLASS (parent_class)->dispose (object);
+}
+
+static void
+clapper_gtk_extra_menu_button_finalize (GObject *object)
+{
+ ClapperGtkExtraMenuButton *self = CLAPPER_GTK_EXTRA_MENU_BUTTON_CAST (object);
+
+ GST_TRACE_OBJECT (self, "Finalize");
+
+ gst_clear_object (&self->current_item);
+ g_object_unref (self->action_group);
+
+ G_OBJECT_CLASS (parent_class)->finalize (object);
+}
+
+static void
+clapper_gtk_extra_menu_button_get_property (GObject *object, guint prop_id,
+ GValue *value, GParamSpec *pspec)
+{
+ ClapperGtkExtraMenuButton *self = CLAPPER_GTK_EXTRA_MENU_BUTTON_CAST (object);
+
+ switch (prop_id) {
+ case PROP_VOLUME_VISIBLE:
+ g_value_set_boolean (value, clapper_gtk_extra_menu_button_get_volume_visible (self));
+ break;
+ case PROP_SPEED_VISIBLE:
+ g_value_set_boolean (value, clapper_gtk_extra_menu_button_get_speed_visible (self));
+ break;
+ case PROP_CAN_OPEN_SUBTITLES:
+ g_value_set_boolean (value, clapper_gtk_extra_menu_button_get_can_open_subtitles (self));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+clapper_gtk_extra_menu_button_set_property (GObject *object, guint prop_id,
+ const GValue *value, GParamSpec *pspec)
+{
+ ClapperGtkExtraMenuButton *self = CLAPPER_GTK_EXTRA_MENU_BUTTON_CAST (object);
+
+ switch (prop_id) {
+ case PROP_VOLUME_VISIBLE:
+ clapper_gtk_extra_menu_button_set_volume_visible (self, g_value_get_boolean (value));
+ break;
+ case PROP_SPEED_VISIBLE:
+ clapper_gtk_extra_menu_button_set_speed_visible (self, g_value_get_boolean (value));
+ break;
+ case PROP_CAN_OPEN_SUBTITLES:
+ clapper_gtk_extra_menu_button_set_can_open_subtitles (self, g_value_get_boolean (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+clapper_gtk_extra_menu_button_class_init (ClapperGtkExtraMenuButtonClass *klass)
+{
+ GObjectClass *gobject_class = (GObjectClass *) klass;
+ GtkWidgetClass *widget_class = (GtkWidgetClass *) klass;
+
+ GST_DEBUG_CATEGORY_INIT (GST_CAT_DEFAULT, "clappergtkextramenubutton", 0,
+ "Clapper GTK Extra Menu Button");
+ clapper_gtk_init_translations ();
+
+ gobject_class->get_property = clapper_gtk_extra_menu_button_get_property;
+ gobject_class->set_property = clapper_gtk_extra_menu_button_set_property;
+ gobject_class->dispose = clapper_gtk_extra_menu_button_dispose;
+ gobject_class->finalize = clapper_gtk_extra_menu_button_finalize;
+
+ widget_class->compute_expand = clapper_gtk_extra_menu_button_compute_expand;
+ widget_class->realize = clapper_gtk_extra_menu_button_realize;
+ widget_class->unrealize = clapper_gtk_extra_menu_button_unrealize;
+
+ /**
+ * ClapperGtkExtraMenuButton:volume-visible:
+ *
+ * Visibility of volume control inside popover.
+ */
+ param_specs[PROP_VOLUME_VISIBLE] = g_param_spec_boolean ("volume-visible",
+ NULL, NULL, TRUE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * ClapperGtkExtraMenuButton:speed-visible:
+ *
+ * Visibility of speed control inside popover.
+ */
+ param_specs[PROP_SPEED_VISIBLE] = g_param_spec_boolean ("speed-visible",
+ NULL, NULL, TRUE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * ClapperGtkExtraMenuButton:can-open-subtitles:
+ *
+ * Visibility of open subtitles option inside popover.
+ */
+ param_specs[PROP_CAN_OPEN_SUBTITLES] = g_param_spec_boolean ("can-open-subtitles",
+ NULL, NULL, FALSE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * ClapperGtkExtraMenuButton::open-subtitles:
+ * @button: a #ClapperGtkExtraMenuButton
+ * @item: a #ClapperMediaItem
+ *
+ * A signal that user wants to open subtitles file.
+ *
+ * Implementation should add a way for user to select subtitles to open
+ * such as by e.g. using [class@Gtk.FileDialog] and then add them to the
+ * @item using [method@Clapper.MediaItem.set_suburi] method.
+ *
+ * This signal will pass the [class@Clapper.MediaItem] that was current when
+ * user clicked the open button and subtitles should be added to this @item.
+ * This avoids situations where another item starts playing before user selects
+ * subtitles file to be opened. When using asynchronous operations to open file,
+ * implementation should [method@GObject.Object.ref] the item to ensure that it
+ * stays valid until finish.
+ *
+ * Note that this signal will not be emitted if open button is not visible by
+ * setting [method@ClapperGtk.ExtraMenuButton.set_can_open_subtitles] to %TRUE,
+ * so you do not have to implement handler for it otherwise.
+ */
+ signals[SIGNAL_OPEN_SUBTITLES] = g_signal_new ("open-subtitles",
+ G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST | G_SIGNAL_NO_RECURSE | G_SIGNAL_NO_HOOKS,
+ 0, NULL, NULL, NULL, G_TYPE_NONE, 1, CLAPPER_TYPE_MEDIA_ITEM);
+
+ g_object_class_install_properties (gobject_class, PROP_LAST, param_specs);
+
+ gtk_widget_class_set_template_from_resource (widget_class,
+ CLAPPER_GTK_RESOURCE_PREFIX "/ui/clapper-gtk-extra-menu-button.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, ClapperGtkExtraMenuButton, menu_button);
+
+ gtk_widget_class_bind_template_child (widget_class, ClapperGtkExtraMenuButton, volume_box);
+ gtk_widget_class_bind_template_child (widget_class, ClapperGtkExtraMenuButton, volume_button);
+ gtk_widget_class_bind_template_child (widget_class, ClapperGtkExtraMenuButton, volume_spin);
+
+ gtk_widget_class_bind_template_child (widget_class, ClapperGtkExtraMenuButton, speed_box);
+ gtk_widget_class_bind_template_child (widget_class, ClapperGtkExtraMenuButton, speed_button);
+ gtk_widget_class_bind_template_child (widget_class, ClapperGtkExtraMenuButton, speed_spin);
+
+ gtk_widget_class_bind_template_child (widget_class, ClapperGtkExtraMenuButton, top_separator);
+
+ gtk_widget_class_bind_template_child (widget_class, ClapperGtkExtraMenuButton, video_list_view);
+ gtk_widget_class_bind_template_child (widget_class, ClapperGtkExtraMenuButton, video_sw);
+
+ gtk_widget_class_bind_template_child (widget_class, ClapperGtkExtraMenuButton, audio_list_view);
+ gtk_widget_class_bind_template_child (widget_class, ClapperGtkExtraMenuButton, audio_sw);
+
+ gtk_widget_class_bind_template_child (widget_class, ClapperGtkExtraMenuButton, subtitle_list_view);
+ gtk_widget_class_bind_template_child (widget_class, ClapperGtkExtraMenuButton, subtitle_sw);
+
+ gtk_widget_class_bind_template_callback (widget_class, volume_spin_input_cb);
+ gtk_widget_class_bind_template_callback (widget_class, volume_spin_output_cb);
+ gtk_widget_class_bind_template_callback (widget_class, volume_spin_changed_cb);
+
+ gtk_widget_class_bind_template_callback (widget_class, speed_spin_input_cb);
+ gtk_widget_class_bind_template_callback (widget_class, speed_spin_output_cb);
+ gtk_widget_class_bind_template_callback (widget_class, speed_spin_changed_cb);
+
+ gtk_widget_class_bind_template_callback (widget_class, popover_map_cb);
+ gtk_widget_class_bind_template_callback (widget_class, popover_unmap_cb);
+ gtk_widget_class_bind_template_callback (widget_class, visible_submenu_changed_cb);
+
+ gtk_widget_class_set_layout_manager_type (widget_class, GTK_TYPE_BIN_LAYOUT);
+ gtk_widget_class_set_accessible_role (widget_class, GTK_ACCESSIBLE_ROLE_BUTTON);
+ gtk_widget_class_set_css_name (widget_class, "clapper-gtk-extra-menu-button");
+}
diff --git a/src/lib/clapper-gtk/clapper-gtk-extra-menu-button.h b/src/lib/clapper-gtk/clapper-gtk-extra-menu-button.h
new file mode 100644
index 00000000..19a5310a
--- /dev/null
+++ b/src/lib/clapper-gtk/clapper-gtk-extra-menu-button.h
@@ -0,0 +1,51 @@
+/* Clapper GTK Integration Library
+ * Copyright (C) 2024 Rafał Dzięgiel
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+#pragma once
+
+#if !defined(__CLAPPER_GTK_INSIDE__) && !defined(CLAPPER_GTK_COMPILATION)
+#error "Only can be included directly."
+#endif
+
+#include
+#include
+#include
+
+G_BEGIN_DECLS
+
+#define CLAPPER_GTK_TYPE_EXTRA_MENU_BUTTON (clapper_gtk_extra_menu_button_get_type())
+#define CLAPPER_GTK_EXTRA_MENU_BUTTON_CAST(obj) ((ClapperGtkExtraMenuButton *)(obj))
+
+G_DECLARE_FINAL_TYPE (ClapperGtkExtraMenuButton, clapper_gtk_extra_menu_button, CLAPPER_GTK, EXTRA_MENU_BUTTON, GtkWidget)
+
+GtkWidget * clapper_gtk_extra_menu_button_new (void);
+
+void clapper_gtk_extra_menu_button_set_volume_visible (ClapperGtkExtraMenuButton *button, gboolean visible);
+
+gboolean clapper_gtk_extra_menu_button_get_volume_visible (ClapperGtkExtraMenuButton *button);
+
+void clapper_gtk_extra_menu_button_set_speed_visible (ClapperGtkExtraMenuButton *button, gboolean visible);
+
+gboolean clapper_gtk_extra_menu_button_get_speed_visible (ClapperGtkExtraMenuButton *button);
+
+void clapper_gtk_extra_menu_button_set_can_open_subtitles (ClapperGtkExtraMenuButton *button, gboolean allowed);
+
+gboolean clapper_gtk_extra_menu_button_get_can_open_subtitles (ClapperGtkExtraMenuButton *button);
+
+G_END_DECLS
diff --git a/src/lib/clapper-gtk/clapper-gtk-lead-container.c b/src/lib/clapper-gtk/clapper-gtk-lead-container.c
new file mode 100644
index 00000000..50d8e07e
--- /dev/null
+++ b/src/lib/clapper-gtk/clapper-gtk-lead-container.c
@@ -0,0 +1,235 @@
+/* Clapper GTK Integration Library
+ * Copyright (C) 2024 Rafał Dzięgiel
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+/**
+ * ClapperGtkLeadContainer:
+ *
+ * A #ClapperGtkContainer that can take priority in user interactions with the #ClapperGtkVideo.
+ *
+ * #ClapperGtkLeadContainer is a special type of [class@ClapperGtk.Container] that can
+ * lead in interaction events. When "leading", it is assumed that user interactions
+ * over it which would normally trigger actions can be blocked/ignored when set in mask
+ * of actions that this widget should block.
+ *
+ * This kind of container is useful when creating some statically visible overlays
+ * covering top of [class@ClapperGtk.Video] that you want to take priority instead of
+ * triggering default actions such as toggle play on click or revealing fading overlays.
+ *
+ * For more info how container widget works see [class@ClapperGtk.Container] documentation.
+ */
+
+#include "clapper-gtk-lead-container.h"
+
+#define DEFAULT_LEADING TRUE
+#define DEFAULT_BLOCKED_ACTIONS CLAPPER_GTK_VIDEO_ACTION_NONE
+
+typedef struct _ClapperGtkLeadContainerPrivate ClapperGtkLeadContainerPrivate;
+
+struct _ClapperGtkLeadContainerPrivate
+{
+ gboolean leading;
+ ClapperGtkVideoActionMask blocked_actions;
+};
+
+#define parent_class clapper_gtk_lead_container_parent_class
+G_DEFINE_TYPE_WITH_PRIVATE (ClapperGtkLeadContainer, clapper_gtk_lead_container, CLAPPER_GTK_TYPE_CONTAINER)
+
+enum
+{
+ PROP_0,
+ PROP_LEADING,
+ PROP_BLOCKED_ACTIONS,
+ PROP_LAST
+};
+
+static GParamSpec *param_specs[PROP_LAST] = { NULL, };
+
+/**
+ * clapper_gtk_lead_container_new:
+ *
+ * Creates a new #ClapperGtkLeadContainer instance.
+ *
+ * Returns: a new lead container #GtkWidget.
+ */
+GtkWidget *
+clapper_gtk_lead_container_new (void)
+{
+ return g_object_new (CLAPPER_GTK_TYPE_LEAD_CONTAINER, NULL);
+}
+
+/**
+ * clapper_gtk_lead_container_set_leading:
+ * @lead_container: a #ClapperGtkLeadContainer
+ * @leading: enable leadership
+ *
+ * Set if @lead_container leadership should be enabled.
+ *
+ * When enabled, interactions with @lead_container will not trigger
+ * their default behavior, instead container and its contents will take priority.
+ */
+void
+clapper_gtk_lead_container_set_leading (ClapperGtkLeadContainer *self, gboolean leading)
+{
+ ClapperGtkLeadContainerPrivate *priv;
+
+ g_return_if_fail (CLAPPER_GTK_IS_LEAD_CONTAINER (self));
+
+ priv = clapper_gtk_lead_container_get_instance_private (self);
+
+ priv->leading = leading;
+}
+
+/**
+ * clapper_gtk_lead_container_get_leading:
+ * @lead_container: a #ClapperGtkLeadContainer
+ *
+ * Get a whenever @lead_container has leadership set.
+ *
+ * Returns: %TRUE if container is leading, %FALSE otherwise.
+ */
+gboolean
+clapper_gtk_lead_container_get_leading (ClapperGtkLeadContainer *self)
+{
+ ClapperGtkLeadContainerPrivate *priv;
+
+ g_return_val_if_fail (CLAPPER_GTK_IS_LEAD_CONTAINER (self), FALSE);
+
+ priv = clapper_gtk_lead_container_get_instance_private (self);
+
+ return priv->leading;
+}
+
+/**
+ * clapper_gtk_lead_container_set_blocked_actions:
+ * @lead_container: a #ClapperGtkLeadContainer
+ * @actions: a #ClapperGtkVideoActionMask of actions to block
+ *
+ * Set @actions that #ClapperGtkVideo should skip when #GdkEvent which
+ * would normally trigger them happens inside @lead_container.
+ */
+void
+clapper_gtk_lead_container_set_blocked_actions (ClapperGtkLeadContainer *self, ClapperGtkVideoActionMask actions)
+{
+ ClapperGtkLeadContainerPrivate *priv;
+
+ g_return_if_fail (CLAPPER_GTK_IS_LEAD_CONTAINER (self));
+
+ priv = clapper_gtk_lead_container_get_instance_private (self);
+
+ priv->blocked_actions = actions;
+}
+
+/**
+ * clapper_gtk_lead_container_get_blocked_actions:
+ * @lead_container: a #ClapperGtkLeadContainer
+ *
+ * Get @actions that were set for this @lead_container to block.
+ *
+ * Returns: a mask of actions that container blocks from being triggered on video.
+ */
+ClapperGtkVideoActionMask
+clapper_gtk_lead_container_get_blocked_actions (ClapperGtkLeadContainer *self)
+{
+ ClapperGtkLeadContainerPrivate *priv;
+
+ g_return_val_if_fail (CLAPPER_GTK_IS_LEAD_CONTAINER (self), 0);
+
+ priv = clapper_gtk_lead_container_get_instance_private (self);
+
+ return priv->blocked_actions;
+}
+
+static void
+clapper_gtk_lead_container_init (ClapperGtkLeadContainer *self)
+{
+ ClapperGtkLeadContainerPrivate *priv = clapper_gtk_lead_container_get_instance_private (self);
+
+ priv->leading = DEFAULT_LEADING;
+ priv->blocked_actions = DEFAULT_BLOCKED_ACTIONS;
+}
+
+static void
+clapper_gtk_lead_container_get_property (GObject *object, guint prop_id,
+ GValue *value, GParamSpec *pspec)
+{
+ ClapperGtkLeadContainer *self = CLAPPER_GTK_LEAD_CONTAINER_CAST (object);
+
+ switch (prop_id) {
+ case PROP_LEADING:
+ g_value_set_boolean (value, clapper_gtk_lead_container_get_leading (self));
+ break;
+ case PROP_BLOCKED_ACTIONS:
+ g_value_set_flags (value, clapper_gtk_lead_container_get_blocked_actions (self));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+clapper_gtk_lead_container_set_property (GObject *object, guint prop_id,
+ const GValue *value, GParamSpec *pspec)
+{
+ ClapperGtkLeadContainer *self = CLAPPER_GTK_LEAD_CONTAINER_CAST (object);
+
+ switch (prop_id) {
+ case PROP_LEADING:
+ clapper_gtk_lead_container_set_leading (self, g_value_get_boolean (value));
+ break;
+ case PROP_BLOCKED_ACTIONS:
+ clapper_gtk_lead_container_set_blocked_actions (self, g_value_get_flags (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+clapper_gtk_lead_container_class_init (ClapperGtkLeadContainerClass *klass)
+{
+ GObjectClass *gobject_class = (GObjectClass *) klass;
+ GtkWidgetClass *widget_class = (GtkWidgetClass *) klass;
+
+ gobject_class->get_property = clapper_gtk_lead_container_get_property;
+ gobject_class->set_property = clapper_gtk_lead_container_set_property;
+
+ /**
+ * ClapperGtkLeadContainer:leading:
+ *
+ * Width that container should target.
+ */
+ param_specs[PROP_LEADING] = g_param_spec_boolean ("leading",
+ NULL, NULL, DEFAULT_LEADING,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * ClapperGtkLeadContainer:blocked-actions:
+ *
+ * Mask of actions that container blocks from being triggered on video.
+ */
+ param_specs[PROP_BLOCKED_ACTIONS] = g_param_spec_flags ("blocked-actions",
+ NULL, NULL, CLAPPER_GTK_TYPE_VIDEO_ACTION_MASK, DEFAULT_BLOCKED_ACTIONS,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+ g_object_class_install_properties (gobject_class, PROP_LAST, param_specs);
+
+ gtk_widget_class_set_css_name (widget_class, "clapper-gtk-lead-container");
+}
diff --git a/src/lib/clapper-gtk/clapper-gtk-lead-container.h b/src/lib/clapper-gtk/clapper-gtk-lead-container.h
new file mode 100644
index 00000000..8d5be7ce
--- /dev/null
+++ b/src/lib/clapper-gtk/clapper-gtk-lead-container.h
@@ -0,0 +1,58 @@
+/* Clapper GTK Integration Library
+ * Copyright (C) 2024 Rafał Dzięgiel
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+#pragma once
+
+#if !defined(__CLAPPER_GTK_INSIDE__) && !defined(CLAPPER_GTK_COMPILATION)
+#error "Only can be included directly."
+#endif
+
+#include
+#include
+#include
+
+#include
+#include
+
+G_BEGIN_DECLS
+
+#define CLAPPER_GTK_TYPE_LEAD_CONTAINER (clapper_gtk_lead_container_get_type())
+#define CLAPPER_GTK_LEAD_CONTAINER_CAST(obj) ((ClapperGtkLeadContainer *)(obj))
+
+G_DECLARE_DERIVABLE_TYPE (ClapperGtkLeadContainer, clapper_gtk_lead_container, CLAPPER_GTK, LEAD_CONTAINER, ClapperGtkContainer)
+
+struct _ClapperGtkLeadContainerClass
+{
+ ClapperGtkContainerClass parent_class;
+
+ /*< private >*/
+ gpointer padding[4];
+};
+
+GtkWidget * clapper_gtk_lead_container_new (void);
+
+void clapper_gtk_lead_container_set_leading (ClapperGtkLeadContainer *lead_container, gboolean leading);
+
+gboolean clapper_gtk_lead_container_get_leading (ClapperGtkLeadContainer *lead_container);
+
+void clapper_gtk_lead_container_set_blocked_actions (ClapperGtkLeadContainer *lead_container, ClapperGtkVideoActionMask actions);
+
+ClapperGtkVideoActionMask clapper_gtk_lead_container_get_blocked_actions (ClapperGtkLeadContainer *lead_container);
+
+G_END_DECLS
diff --git a/src/lib/clapper-gtk/clapper-gtk-limited-layout-private.h b/src/lib/clapper-gtk/clapper-gtk-limited-layout-private.h
new file mode 100644
index 00000000..2ce50b58
--- /dev/null
+++ b/src/lib/clapper-gtk/clapper-gtk-limited-layout-private.h
@@ -0,0 +1,57 @@
+/* Clapper GTK Integration Library
+ * Copyright (C) 2024 Rafał Dzięgiel
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+#pragma once
+
+#include
+#include
+#include
+
+G_BEGIN_DECLS
+
+#define CLAPPER_GTK_TYPE_LIMITED_LAYOUT (clapper_gtk_limited_layout_get_type())
+#define CLAPPER_GTK_LIMITED_LAYOUT_CAST(obj) ((ClapperGtkLimitedLayout *)(obj))
+
+G_DECLARE_FINAL_TYPE (ClapperGtkLimitedLayout, clapper_gtk_limited_layout, CLAPPER_GTK, LIMITED_LAYOUT, GtkLayoutManager)
+
+G_GNUC_INTERNAL
+void clapper_gtk_limited_layout_set_max_width (ClapperGtkLimitedLayout *layout, gint max_width);
+
+G_GNUC_INTERNAL
+gint clapper_gtk_limited_layout_get_max_width (ClapperGtkLimitedLayout *layout);
+
+G_GNUC_INTERNAL
+void clapper_gtk_limited_layout_set_max_height (ClapperGtkLimitedLayout *layout, gint max_height);
+
+G_GNUC_INTERNAL
+gint clapper_gtk_limited_layout_get_max_height (ClapperGtkLimitedLayout *layout);
+
+G_GNUC_INTERNAL
+void clapper_gtk_limited_layout_set_adaptive_width (ClapperGtkLimitedLayout *layout, gint width);
+
+G_GNUC_INTERNAL
+gint clapper_gtk_limited_layout_get_adaptive_width (ClapperGtkLimitedLayout *layout);
+
+G_GNUC_INTERNAL
+void clapper_gtk_limited_layout_set_adaptive_height (ClapperGtkLimitedLayout *layout, gint height);
+
+G_GNUC_INTERNAL
+gint clapper_gtk_limited_layout_get_adaptive_height (ClapperGtkLimitedLayout *layout);
+
+G_END_DECLS
diff --git a/src/lib/clapper-gtk/clapper-gtk-limited-layout.c b/src/lib/clapper-gtk/clapper-gtk-limited-layout.c
new file mode 100644
index 00000000..44fdc2f8
--- /dev/null
+++ b/src/lib/clapper-gtk/clapper-gtk-limited-layout.c
@@ -0,0 +1,159 @@
+/* Clapper GTK Integration Library
+ * Copyright (C) 2024 Rafał Dzięgiel
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+#include
+
+#include "clapper-gtk-limited-layout-private.h"
+#include "clapper-gtk-container-private.h"
+
+#define GST_CAT_DEFAULT clapper_gtk_limited_layout_debug
+GST_DEBUG_CATEGORY_STATIC (GST_CAT_DEFAULT);
+
+struct _ClapperGtkLimitedLayout
+{
+ GtkLayoutManager parent;
+
+ gint max_width;
+ gint max_height;
+
+ gint adaptive_width;
+ gint adaptive_height;
+
+ gboolean adapt;
+};
+
+#define parent_class clapper_gtk_limited_layout_parent_class
+G_DEFINE_TYPE (ClapperGtkLimitedLayout, clapper_gtk_limited_layout, GTK_TYPE_LAYOUT_MANAGER)
+
+void
+clapper_gtk_limited_layout_set_max_width (ClapperGtkLimitedLayout *self, gint max_width)
+{
+ self->max_width = max_width;
+}
+
+gint
+clapper_gtk_limited_layout_get_max_width (ClapperGtkLimitedLayout *self)
+{
+ return self->max_width;
+}
+
+void
+clapper_gtk_limited_layout_set_max_height (ClapperGtkLimitedLayout *self, gint max_height)
+{
+ self->max_height = max_height;
+}
+
+gint
+clapper_gtk_limited_layout_get_max_height (ClapperGtkLimitedLayout *self)
+{
+ return self->max_height;
+}
+
+void
+clapper_gtk_limited_layout_set_adaptive_width (ClapperGtkLimitedLayout *self, gint width)
+{
+ self->adaptive_width = width;
+}
+
+gint
+clapper_gtk_limited_layout_get_adaptive_width (ClapperGtkLimitedLayout *self)
+{
+ return self->adaptive_width;
+}
+
+void
+clapper_gtk_limited_layout_set_adaptive_height (ClapperGtkLimitedLayout *self, gint height)
+{
+ self->adaptive_height = height;
+}
+
+gint
+clapper_gtk_limited_layout_get_adaptive_height (ClapperGtkLimitedLayout *self)
+{
+ return self->adaptive_height;
+}
+
+static void
+clapper_gtk_limited_layout_measure (GtkLayoutManager *layout_manager,
+ GtkWidget *widget, GtkOrientation orientation, gint for_size,
+ gint *minimum, gint *natural, gint *minimum_baseline, gint *natural_baseline)
+{
+ ClapperGtkLimitedLayout *self = CLAPPER_GTK_LIMITED_LAYOUT_CAST (layout_manager);
+ GtkWidget *child = gtk_widget_get_first_child (widget);
+
+ if (child && gtk_widget_should_layout (child)) {
+ gint child_min = 0, child_nat = 0;
+ gint child_min_baseline = -1, child_nat_baseline = -1;
+
+ gtk_widget_measure (child, orientation, for_size,
+ &child_min, &child_nat,
+ &child_min_baseline, &child_nat_baseline);
+
+ if (orientation == GTK_ORIENTATION_HORIZONTAL)
+ *natural = (self->max_width < 0) ? MAX (*natural, child_nat) : self->max_width;
+ else if (orientation == GTK_ORIENTATION_VERTICAL)
+ *natural = (self->max_height < 0) ? MAX (*natural, child_nat) : self->max_height;
+
+ *minimum = MAX (*minimum, child_min);
+
+ if (child_min_baseline > -1)
+ *minimum_baseline = MAX (*minimum_baseline, child_min_baseline);
+ if (child_nat_baseline > -1)
+ *natural_baseline = MAX (*natural_baseline, child_nat_baseline);
+ }
+}
+
+static void
+clapper_gtk_limited_layout_allocate (GtkLayoutManager *layout_manager,
+ GtkWidget *widget, gint width, gint height, gint baseline)
+{
+ ClapperGtkLimitedLayout *self = CLAPPER_GTK_LIMITED_LAYOUT_CAST (layout_manager);
+ GtkWidget *child = gtk_widget_get_first_child (widget);
+ gboolean adapt = (width <= self->adaptive_width || height <= self->adaptive_height);
+
+ if (child && gtk_widget_should_layout (child))
+ gtk_widget_allocate (child, width, height, baseline, NULL);
+
+ if (G_UNLIKELY (self->adapt != adapt)) {
+ self->adapt = adapt;
+ clapper_gtk_container_emit_adapt (CLAPPER_GTK_CONTAINER_CAST (widget), adapt);
+ }
+}
+
+static void
+clapper_gtk_limited_layout_init (ClapperGtkLimitedLayout *self)
+{
+ self->max_width = -1;
+ self->max_height = -1;
+
+ self->adaptive_width = -1;
+ self->adaptive_height = -1;
+}
+
+static void
+clapper_gtk_limited_layout_class_init (ClapperGtkLimitedLayoutClass *klass)
+{
+ GtkLayoutManagerClass *layout_manager_class = (GtkLayoutManagerClass *) klass;
+
+ GST_DEBUG_CATEGORY_INIT (GST_CAT_DEFAULT, "clappergtklimitedlayout", 0,
+ "Clapper GTK Limited Layout");
+
+ layout_manager_class->measure = clapper_gtk_limited_layout_measure;
+ layout_manager_class->allocate = clapper_gtk_limited_layout_allocate;
+}
diff --git a/src/lib/clapper-gtk/clapper-gtk-next-item-button.c b/src/lib/clapper-gtk/clapper-gtk-next-item-button.c
new file mode 100644
index 00000000..79cafc92
--- /dev/null
+++ b/src/lib/clapper-gtk/clapper-gtk-next-item-button.c
@@ -0,0 +1,132 @@
+/* Clapper GTK Integration Library
+ * Copyright (C) 2024 Rafał Dzięgiel
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+/**
+ * ClapperGtkNextItemButton:
+ *
+ * A #GtkButton for selecting next queue item.
+ */
+
+#include
+
+#include "clapper-gtk-next-item-button.h"
+#include "clapper-gtk-utils.h"
+
+#define GST_CAT_DEFAULT clapper_gtk_next_item_button_debug
+GST_DEBUG_CATEGORY_STATIC (GST_CAT_DEFAULT);
+
+struct _ClapperGtkNextItemButton
+{
+ GtkButton parent;
+
+ GBinding *n_items_binding;
+ GBinding *current_index_binding;
+};
+
+#define parent_class clapper_gtk_next_item_button_parent_class
+G_DEFINE_TYPE (ClapperGtkNextItemButton, clapper_gtk_next_item_button, GTK_TYPE_BUTTON)
+
+static gboolean
+_transform_sensitive_func (GBinding *binding, const GValue *from_value,
+ GValue *to_value, ClapperGtkNextItemButton *self)
+{
+ ClapperQueue *queue = CLAPPER_QUEUE_CAST (g_binding_dup_source (binding));
+ guint current_index;
+ gboolean can_skip;
+
+ if (G_UNLIKELY (queue == NULL))
+ return FALSE;
+
+ current_index = clapper_queue_get_current_index (queue);
+
+ can_skip = (current_index != CLAPPER_QUEUE_INVALID_POSITION
+ && current_index < clapper_queue_get_n_items (queue) - 1);
+ gst_object_unref (queue);
+
+ g_value_set_boolean (to_value, can_skip);
+ GST_DEBUG_OBJECT (self, "Set sensitive: %s", (can_skip) ? "yes" : "no");
+
+ return TRUE;
+}
+
+/**
+ * clapper_gtk_next_item_button_new:
+ *
+ * Creates a new #ClapperGtkNextItemButton to play next #ClapperMediaItem.
+ *
+ * Returns: a new next item button #GtkWidget.
+ */
+GtkWidget *
+clapper_gtk_next_item_button_new (void)
+{
+ return g_object_new (CLAPPER_GTK_TYPE_NEXT_ITEM_BUTTON, NULL);
+}
+
+static void
+clapper_gtk_next_item_button_init (ClapperGtkNextItemButton *self)
+{
+ gtk_widget_set_sensitive (GTK_WIDGET (self), FALSE);
+ gtk_button_set_icon_name (GTK_BUTTON (self), "media-skip-forward-symbolic");
+ gtk_actionable_set_action_name (GTK_ACTIONABLE (self), "video.next-item");
+}
+
+static void
+clapper_gtk_next_item_button_map (GtkWidget *widget)
+{
+ ClapperGtkNextItemButton *self = CLAPPER_GTK_NEXT_ITEM_BUTTON_CAST (widget);
+ ClapperPlayer *player;
+
+ if ((player = clapper_gtk_get_player_from_ancestor (widget))) {
+ ClapperQueue *queue = clapper_player_get_queue (player);
+
+ self->n_items_binding = g_object_bind_property_full (queue, "n-items",
+ self, "sensitive", G_BINDING_DEFAULT, // Since we sync below, no need to do it twice
+ (GBindingTransformFunc) _transform_sensitive_func,
+ NULL, self, NULL);
+ self->current_index_binding = g_object_bind_property_full (queue, "current-index",
+ self, "sensitive", G_BINDING_SYNC_CREATE,
+ (GBindingTransformFunc) _transform_sensitive_func,
+ NULL, self, NULL);
+ }
+
+ GTK_WIDGET_CLASS (parent_class)->map (widget);
+}
+
+static void
+clapper_gtk_next_item_button_unmap (GtkWidget *widget)
+{
+ ClapperGtkNextItemButton *self = CLAPPER_GTK_NEXT_ITEM_BUTTON_CAST (widget);
+
+ g_clear_pointer (&self->n_items_binding, g_binding_unbind);
+ g_clear_pointer (&self->current_index_binding, g_binding_unbind);
+
+ GTK_WIDGET_CLASS (parent_class)->unmap (widget);
+}
+
+static void
+clapper_gtk_next_item_button_class_init (ClapperGtkNextItemButtonClass *klass)
+{
+ GtkWidgetClass *widget_class = (GtkWidgetClass *) klass;
+
+ GST_DEBUG_CATEGORY_INIT (GST_CAT_DEFAULT, "clappergtknextitembutton", 0,
+ "Clapper GTK Next Item Button");
+
+ widget_class->map = clapper_gtk_next_item_button_map;
+ widget_class->unmap = clapper_gtk_next_item_button_unmap;
+}
diff --git a/src/lib/clapper-gtk/clapper-gtk-next-item-button.h b/src/lib/clapper-gtk/clapper-gtk-next-item-button.h
new file mode 100644
index 00000000..a792c5ed
--- /dev/null
+++ b/src/lib/clapper-gtk/clapper-gtk-next-item-button.h
@@ -0,0 +1,39 @@
+/* Clapper GTK Integration Library
+ * Copyright (C) 2024 Rafał Dzięgiel
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+#pragma once
+
+#if !defined(__CLAPPER_GTK_INSIDE__) && !defined(CLAPPER_GTK_COMPILATION)
+#error "Only can be included directly."
+#endif
+
+#include
+#include
+#include
+
+G_BEGIN_DECLS
+
+#define CLAPPER_GTK_TYPE_NEXT_ITEM_BUTTON (clapper_gtk_next_item_button_get_type())
+#define CLAPPER_GTK_NEXT_ITEM_BUTTON_CAST(obj) ((ClapperGtkNextItemButton *)(obj))
+
+G_DECLARE_FINAL_TYPE (ClapperGtkNextItemButton, clapper_gtk_next_item_button, CLAPPER_GTK, NEXT_ITEM_BUTTON, GtkButton)
+
+GtkWidget * clapper_gtk_next_item_button_new (void);
+
+G_END_DECLS
diff --git a/src/lib/clapper-gtk/clapper-gtk-previous-item-button.c b/src/lib/clapper-gtk/clapper-gtk-previous-item-button.c
new file mode 100644
index 00000000..f214ebb8
--- /dev/null
+++ b/src/lib/clapper-gtk/clapper-gtk-previous-item-button.c
@@ -0,0 +1,132 @@
+/* Clapper GTK Integration Library
+ * Copyright (C) 2024 Rafał Dzięgiel
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+/**
+ * ClapperGtkPreviousItemButton:
+ *
+ * A #GtkButton for selecting previous queue item.
+ */
+
+#include
+
+#include "clapper-gtk-previous-item-button.h"
+#include "clapper-gtk-utils.h"
+
+#define GST_CAT_DEFAULT clapper_gtk_previous_item_button_debug
+GST_DEBUG_CATEGORY_STATIC (GST_CAT_DEFAULT);
+
+struct _ClapperGtkPreviousItemButton
+{
+ GtkButton parent;
+
+ GBinding *n_items_binding;
+ GBinding *current_index_binding;
+};
+
+#define parent_class clapper_gtk_previous_item_button_parent_class
+G_DEFINE_TYPE (ClapperGtkPreviousItemButton, clapper_gtk_previous_item_button, GTK_TYPE_BUTTON)
+
+static gboolean
+_transform_sensitive_func (GBinding *binding, const GValue *from_value,
+ GValue *to_value, ClapperGtkPreviousItemButton *self)
+{
+ ClapperQueue *queue = CLAPPER_QUEUE_CAST (g_binding_dup_source (binding));
+ guint current_index;
+ gboolean can_skip;
+
+ if (G_UNLIKELY (queue == NULL))
+ return FALSE;
+
+ current_index = clapper_queue_get_current_index (queue);
+
+ can_skip = (current_index != CLAPPER_QUEUE_INVALID_POSITION
+ && current_index > 0);
+ gst_object_unref (queue);
+
+ g_value_set_boolean (to_value, can_skip);
+ GST_DEBUG_OBJECT (self, "Set sensitive: %s", (can_skip) ? "yes" : "no");
+
+ return TRUE;
+}
+
+/**
+ * clapper_gtk_previous_item_button_new:
+ *
+ * Creates a new #ClapperGtkPreviousItemButton to play previous #ClapperMediaItem.
+ *
+ * Returns: a new previous item button #GtkWidget.
+ */
+GtkWidget *
+clapper_gtk_previous_item_button_new (void)
+{
+ return g_object_new (CLAPPER_GTK_TYPE_PREVIOUS_ITEM_BUTTON, NULL);
+}
+
+static void
+clapper_gtk_previous_item_button_init (ClapperGtkPreviousItemButton *self)
+{
+ gtk_widget_set_sensitive (GTK_WIDGET (self), FALSE);
+ gtk_button_set_icon_name (GTK_BUTTON (self), "media-skip-backward-symbolic");
+ gtk_actionable_set_action_name (GTK_ACTIONABLE (self), "video.previous-item");
+}
+
+static void
+clapper_gtk_previous_item_button_map (GtkWidget *widget)
+{
+ ClapperGtkPreviousItemButton *self = CLAPPER_GTK_PREVIOUS_ITEM_BUTTON_CAST (widget);
+ ClapperPlayer *player;
+
+ if ((player = clapper_gtk_get_player_from_ancestor (widget))) {
+ ClapperQueue *queue = clapper_player_get_queue (player);
+
+ self->n_items_binding = g_object_bind_property_full (queue, "n-items",
+ self, "sensitive", G_BINDING_DEFAULT, // Since we sync below, no need to do it twice
+ (GBindingTransformFunc) _transform_sensitive_func,
+ NULL, self, NULL);
+ self->current_index_binding = g_object_bind_property_full (queue, "current-index",
+ self, "sensitive", G_BINDING_SYNC_CREATE,
+ (GBindingTransformFunc) _transform_sensitive_func,
+ NULL, self, NULL);
+ }
+
+ GTK_WIDGET_CLASS (parent_class)->map (widget);
+}
+
+static void
+clapper_gtk_previous_item_button_unmap (GtkWidget *widget)
+{
+ ClapperGtkPreviousItemButton *self = CLAPPER_GTK_PREVIOUS_ITEM_BUTTON_CAST (widget);
+
+ g_clear_pointer (&self->n_items_binding, g_binding_unbind);
+ g_clear_pointer (&self->current_index_binding, g_binding_unbind);
+
+ GTK_WIDGET_CLASS (parent_class)->unmap (widget);
+}
+
+static void
+clapper_gtk_previous_item_button_class_init (ClapperGtkPreviousItemButtonClass *klass)
+{
+ GtkWidgetClass *widget_class = (GtkWidgetClass *) klass;
+
+ GST_DEBUG_CATEGORY_INIT (GST_CAT_DEFAULT, "clappergtkpreviousitembutton", 0,
+ "Clapper GTK Previous Item Button");
+
+ widget_class->map = clapper_gtk_previous_item_button_map;
+ widget_class->unmap = clapper_gtk_previous_item_button_unmap;
+}
diff --git a/src/lib/clapper-gtk/clapper-gtk-previous-item-button.h b/src/lib/clapper-gtk/clapper-gtk-previous-item-button.h
new file mode 100644
index 00000000..e6a4838b
--- /dev/null
+++ b/src/lib/clapper-gtk/clapper-gtk-previous-item-button.h
@@ -0,0 +1,39 @@
+/* Clapper GTK Integration Library
+ * Copyright (C) 2024 Rafał Dzięgiel
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+#pragma once
+
+#if !defined(__CLAPPER_GTK_INSIDE__) && !defined(CLAPPER_GTK_COMPILATION)
+#error "Only can be included directly."
+#endif
+
+#include
+#include
+#include
+
+G_BEGIN_DECLS
+
+#define CLAPPER_GTK_TYPE_PREVIOUS_ITEM_BUTTON (clapper_gtk_previous_item_button_get_type())
+#define CLAPPER_GTK_PREVIOUS_ITEM_BUTTON_CAST(obj) ((ClapperGtkPreviousItemButton *)(obj))
+
+G_DECLARE_FINAL_TYPE (ClapperGtkPreviousItemButton, clapper_gtk_previous_item_button, CLAPPER_GTK, PREVIOUS_ITEM_BUTTON, GtkButton)
+
+GtkWidget * clapper_gtk_previous_item_button_new (void);
+
+G_END_DECLS
diff --git a/src/lib/clapper-gtk/clapper-gtk-seek-bar.c b/src/lib/clapper-gtk/clapper-gtk-seek-bar.c
new file mode 100644
index 00000000..f157379b
--- /dev/null
+++ b/src/lib/clapper-gtk/clapper-gtk-seek-bar.c
@@ -0,0 +1,783 @@
+/* Clapper GTK Integration Library
+ * Copyright (C) 2024 Rafał Dzięgiel
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+/**
+ * ClapperGtkSeekBar:
+ *
+ * A bar for seeking and displaying playback position.
+ */
+
+#include "config.h"
+
+#include
+
+#include "clapper-gtk-seek-bar.h"
+#include "clapper-gtk-container.h"
+#include "clapper-gtk-utils.h"
+
+#define DEFAULT_REVEAL_LABELS TRUE
+#define DEFAULT_SEEK_METHOD CLAPPER_PLAYER_SEEK_METHOD_NORMAL
+
+#define GST_CAT_DEFAULT clapper_gtk_seek_bar_debug
+GST_DEBUG_CATEGORY_STATIC (GST_CAT_DEFAULT);
+
+struct _ClapperGtkSeekBar
+{
+ GtkWidget parent;
+
+ GtkWidget *position_revealer;
+ GtkWidget *position_label;
+
+ GtkWidget *scale;
+
+ GtkPopover *popover;
+ GtkLabel *popover_label;
+
+ GtkWidget *duration_revealer;
+ GtkWidget *duration_label;
+
+ gboolean has_hours;
+ gboolean has_markers;
+
+ gboolean dragging;
+ guint position_uint;
+
+ gulong position_signal_id;
+
+ gboolean reveal_labels;
+ ClapperPlayerSeekMethod seek_method;
+
+ ClapperPlayer *player;
+ ClapperMediaItem *current_item;
+
+ /* Cache */
+ gdouble curr_marker_start;
+ gdouble next_marker_start;
+};
+
+static void
+clapper_gtk_seek_bar_add_child (GtkBuildable *buildable,
+ GtkBuilder *builder, GObject *child, const char *type)
+{
+ if (GTK_IS_WIDGET (child)) {
+ gtk_widget_insert_before (GTK_WIDGET (child), GTK_WIDGET (buildable), NULL);
+ } else {
+ GtkBuildableIface *parent_iface = g_type_interface_peek_parent (GTK_BUILDABLE_GET_IFACE (buildable));
+ parent_iface->add_child (buildable, builder, child, type);
+ }
+}
+
+static void
+_buildable_iface_init (GtkBuildableIface *iface)
+{
+ iface->add_child = clapper_gtk_seek_bar_add_child;
+}
+
+#define parent_class clapper_gtk_seek_bar_parent_class
+G_DEFINE_TYPE_WITH_CODE (ClapperGtkSeekBar, clapper_gtk_seek_bar, GTK_TYPE_WIDGET,
+ G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE, _buildable_iface_init))
+
+enum
+{
+ PROP_0,
+ PROP_REVEAL_LABELS,
+ PROP_SEEK_METHOD,
+ PROP_LAST
+};
+
+static GParamSpec *param_specs[PROP_LAST] = { NULL, };
+
+static inline gboolean
+_prepare_popover (ClapperGtkSeekBar *self, gdouble x,
+ gdouble pointing_val, gdouble upper)
+{
+ /* Avoid iterating through markers if within last marker range
+ * (currently set title label remains the same) */
+ gboolean found_title = (pointing_val >= self->curr_marker_start
+ && pointing_val < self->next_marker_start);
+
+ if (!found_title) {
+ ClapperTimeline *timeline = clapper_media_item_get_timeline (self->current_item);
+ guint i = clapper_timeline_get_n_markers (timeline);
+
+ GST_DEBUG ("Searching for marker at: %lf", pointing_val);
+
+ /* We start from the end of scale */
+ self->next_marker_start = upper;
+
+ while (i--) {
+ ClapperMarker *marker = clapper_timeline_get_marker (timeline, i);
+ self->curr_marker_start = clapper_marker_get_start (marker);
+
+ if (self->curr_marker_start <= pointing_val) {
+ const gchar *title = clapper_marker_get_title (marker);
+
+ GST_DEBUG ("Found marker, range: (%lf-%lf), title: \"%s\"",
+ self->curr_marker_start, self->next_marker_start,
+ GST_STR_NULL (title));
+
+ /* XXX: It does string comparison internally, so its more efficient
+ * for us and we do not have to compare strings here too */
+ gtk_label_set_label (self->popover_label, title);
+ found_title = (title != NULL);
+ }
+
+ gst_object_unref (marker);
+
+ if (found_title)
+ break;
+
+ self->next_marker_start = self->curr_marker_start;
+ }
+ }
+
+ gtk_popover_set_pointing_to (self->popover,
+ &(const GdkRectangle){ x, 0, 1, 1 });
+
+ return found_title;
+}
+
+static inline gboolean
+_compute_scale_coords (ClapperGtkSeekBar *self,
+ gdouble *min_pointing_val, gdouble *max_pointing_val)
+{
+ graphene_rect_t slider_bounds;
+
+ if (!gtk_widget_compute_bounds (GTK_WIDGET (self), self->scale, &slider_bounds))
+ return FALSE;
+
+ /* XXX: Number "2" is the correction for range protruding rounded sides
+ * compared to how marks above/below it are positioned */
+ *min_pointing_val = -slider_bounds.origin.x + 2;
+ *max_pointing_val = slider_bounds.size.width + slider_bounds.origin.x - 2;
+
+ return TRUE;
+}
+
+static void
+_player_position_changed_cb (ClapperPlayer *player,
+ GParamSpec *pspec G_GNUC_UNUSED, ClapperGtkSeekBar *self)
+{
+ GtkAdjustment *adjustment;
+ gdouble position;
+
+ if (self->dragging)
+ return;
+
+ position = clapper_player_get_position (player);
+
+ if (ABS (self->position_uint - position) < 1)
+ return;
+
+ GST_LOG_OBJECT (self, "Position changed: %lf", position);
+
+ self->position_uint = (guint) position;
+
+ adjustment = gtk_range_get_adjustment (GTK_RANGE (self->scale));
+ gtk_adjustment_set_value (adjustment, position);
+}
+
+static void
+_player_state_changed_cb (ClapperPlayer *player,
+ GParamSpec *pspec G_GNUC_UNUSED, ClapperGtkSeekBar *self)
+{
+ switch (clapper_player_get_state (player)) {
+ case CLAPPER_PLAYER_STATE_PAUSED:
+ /* Force refresh, so scale always reaches end after playback */
+ self->position_uint = G_MAXUINT;
+ _player_position_changed_cb (player, NULL, self);
+ break;
+ default:
+ break;
+ }
+}
+
+static void
+_player_seek_done_cb (ClapperPlayer *player, ClapperGtkSeekBar *self)
+{
+ GST_DEBUG ("Seek done");
+
+ if (self->position_signal_id == 0) {
+ self->position_signal_id = g_signal_connect (self->player,
+ "notify::position", G_CALLBACK (_player_position_changed_cb), self);
+ }
+ _player_position_changed_cb (player, NULL, self);
+}
+
+static void
+scale_value_changed_cb (GtkRange *range, ClapperGtkSeekBar *self)
+{
+ gdouble value = gtk_range_get_value (range);
+ gchar *position_str = g_strdup_printf ("%" CLAPPER_TIME_FORMAT, CLAPPER_TIME_ARGS (value));
+
+ gtk_label_set_label (GTK_LABEL (self->position_label),
+ (self->has_hours) ? position_str : position_str + 3);
+ g_free (position_str);
+
+ if (self->dragging && self->has_markers) {
+ gdouble min_pointing_val, max_pointing_val;
+ gdouble x, upper, scaling;
+
+ if (!_compute_scale_coords (self, &min_pointing_val, &max_pointing_val)) {
+ gtk_popover_popdown (self->popover);
+ return;
+ }
+
+ upper = gtk_adjustment_get_upper (
+ gtk_range_get_adjustment (GTK_RANGE (self->scale)));
+ scaling = (upper / (max_pointing_val - min_pointing_val));
+
+ x = min_pointing_val + (value / scaling);
+
+ if (_prepare_popover (self, x, value, upper))
+ gtk_popover_popup (self->popover);
+ else
+ gtk_popover_popdown (self->popover);
+ }
+}
+
+static void
+scale_css_classes_changed_cb (GtkWidget *widget,
+ GParamSpec *pspec G_GNUC_UNUSED, ClapperGtkSeekBar *self)
+{
+ const gboolean dragging = gtk_widget_has_css_class (widget, "dragging");
+ gdouble value;
+
+ if (self->dragging == dragging)
+ return;
+
+ if ((self->dragging = dragging)) {
+ GST_DEBUG_OBJECT (self, "Scale drag started");
+ return;
+ }
+
+ value = gtk_range_get_value (GTK_RANGE (widget));
+ GST_DEBUG_OBJECT (self, "Scale dropped at: %lf", value);
+
+ if (G_UNLIKELY (self->player == NULL))
+ return;
+
+ if (self->position_signal_id != 0) {
+ g_signal_handler_disconnect (self->player, self->position_signal_id);
+ self->position_signal_id = 0;
+ }
+
+ /* We should be ALWAYS doing normal seeks if dropped at marker position */
+ if (self->has_markers
+ && G_APPROX_VALUE (self->curr_marker_start, value, FLT_EPSILON)) {
+ GST_DEBUG ("Seeking to marker");
+ clapper_player_seek (self->player, value);
+ } else {
+ clapper_player_seek_custom (self->player, value, self->seek_method);
+ }
+}
+
+static void
+motion_cb (GtkEventControllerMotion *motion,
+ gdouble x, gdouble y, ClapperGtkSeekBar *self)
+{
+ gdouble min_pointing_val, max_pointing_val, pointing_val;
+ gdouble upper, scaling;
+
+ /* If no markers, popover should never popup,
+ * so we do not try to pop it down here */
+ if (!self->has_markers)
+ return;
+
+ if (!_compute_scale_coords (self, &min_pointing_val, &max_pointing_val)
+ || (x < min_pointing_val || x > max_pointing_val)) {
+ gtk_popover_popdown (self->popover);
+ return;
+ }
+
+ upper = gtk_adjustment_get_upper (
+ gtk_range_get_adjustment (GTK_RANGE (self->scale)));
+ scaling = (upper / (max_pointing_val - min_pointing_val));
+
+ pointing_val = (x - min_pointing_val) * scaling;
+ GST_LOG ("Cursor pointing to: %lf", pointing_val);
+
+ if (_prepare_popover (self, x, pointing_val, upper))
+ gtk_popover_popup (self->popover);
+ else
+ gtk_popover_popdown (self->popover);
+}
+
+static void
+motion_leave_cb (GtkEventControllerMotion *motion, ClapperGtkSeekBar *self)
+{
+ gtk_popover_popdown (self->popover);
+}
+
+static void
+touch_released_cb (GtkGestureClick *click, gint n_press,
+ gdouble x, gdouble y, ClapperGtkSeekBar *self)
+{
+ gtk_popover_popdown (self->popover);
+}
+
+static void
+_update_duration_label (ClapperGtkSeekBar *self, gdouble duration)
+{
+ GtkAdjustment *adjustment = gtk_range_get_adjustment (GTK_RANGE (self->scale));
+ gchar *duration_str = g_strdup_printf ("%" CLAPPER_TIME_FORMAT, CLAPPER_TIME_ARGS (duration));
+ gboolean has_hours = (duration >= 3600);
+
+ GST_LOG_OBJECT (self, "Duration changed: %lf", duration);
+
+ /* Refresh position label when changing text length */
+ if (has_hours != self->has_hours) {
+ self->has_hours = has_hours;
+ scale_value_changed_cb (GTK_RANGE (self->scale), self);
+ }
+
+ gtk_label_set_label (GTK_LABEL (self->duration_label),
+ (self->has_hours) ? duration_str : duration_str + 3);
+ g_free (duration_str);
+
+ gtk_adjustment_set_upper (adjustment, duration);
+}
+
+static void
+_update_scale_marks (ClapperGtkSeekBar *self, ClapperTimeline *timeline)
+{
+ GtkAdjustment *adjustment;
+ guint i, n_markers = clapper_timeline_get_n_markers (timeline);
+
+ GST_DEBUG_OBJECT (self, "Placing %u markers on scale", n_markers);
+
+ gtk_scale_clear_marks (GTK_SCALE (self->scale));
+
+ self->curr_marker_start = -1;
+ self->next_marker_start = -1;
+ self->has_markers = FALSE;
+
+ if (n_markers == 0) {
+ gtk_popover_popdown (self->popover);
+ return;
+ }
+
+ adjustment = gtk_range_get_adjustment (GTK_RANGE (self->scale));
+
+ /* Avoid placing marks when duration is zero. Otherwise we may
+ * end up with a single mark at zero until another refresh. */
+ if (gtk_adjustment_get_upper (adjustment) <= 0)
+ return;
+
+ for (i = 0; i < n_markers; ++i) {
+ ClapperMarker *marker = clapper_timeline_get_marker (timeline, i);
+ gdouble start = clapper_marker_get_start (marker);
+
+ gtk_scale_add_mark (GTK_SCALE (self->scale), start, GTK_POS_TOP, NULL);
+ gtk_scale_add_mark (GTK_SCALE (self->scale), start, GTK_POS_BOTTOM, NULL);
+
+ gst_object_unref (marker);
+ }
+
+ self->has_markers = TRUE;
+}
+
+static void
+_current_item_duration_changed_cb (ClapperMediaItem *current_item,
+ GParamSpec *pspec G_GNUC_UNUSED, ClapperGtkSeekBar *self)
+{
+ /* GtkScale ignores markers placed post its adjustment upper range.
+ * We need to place them again on scale AFTER duration changes. */
+ _update_duration_label (self, clapper_media_item_get_duration (current_item));
+ _update_scale_marks (self, clapper_media_item_get_timeline (current_item));
+}
+
+static void
+_timeline_markers_changed_cb (GListModel *list_model, guint position,
+ guint removed, guint added, ClapperGtkSeekBar *self)
+{
+ _update_scale_marks (self, CLAPPER_TIMELINE (list_model));
+}
+
+static void
+_queue_current_item_changed_cb (ClapperQueue *queue,
+ GParamSpec *pspec G_GNUC_UNUSED, ClapperGtkSeekBar *self)
+{
+ ClapperMediaItem *current_item = clapper_queue_get_current_item (queue);
+
+ /* Disconnect signals from old item */
+ if (self->current_item) {
+ ClapperTimeline *timeline = clapper_media_item_get_timeline (self->current_item);
+
+ g_signal_handlers_disconnect_by_func (self->current_item,
+ _current_item_duration_changed_cb, self);
+ g_signal_handlers_disconnect_by_func (timeline,
+ _timeline_markers_changed_cb, self);
+ }
+
+ gst_object_replace ((GstObject **) &self->current_item, GST_OBJECT_CAST (current_item));
+ gst_clear_object (¤t_item);
+
+ /* Reconnect signals to new item */
+ if (self->current_item) {
+ ClapperTimeline *timeline = clapper_media_item_get_timeline (self->current_item);
+
+ g_signal_connect (self->current_item, "notify::duration",
+ G_CALLBACK (_current_item_duration_changed_cb), self);
+ g_signal_connect (timeline, "items-changed",
+ G_CALLBACK (_timeline_markers_changed_cb), self);
+
+ _update_duration_label (self, clapper_media_item_get_duration (self->current_item));
+ _update_scale_marks (self, timeline);
+ } else {
+ gtk_scale_clear_marks (GTK_SCALE (self->scale));
+ _update_duration_label (self, 0);
+ }
+}
+
+/**
+ * clapper_gtk_seek_bar_new:
+ *
+ * Creates a new #ClapperGtkSeekBar instance.
+ *
+ * Returns: a new seek bar #GtkWidget.
+ */
+GtkWidget *
+clapper_gtk_seek_bar_new (void)
+{
+ return g_object_new (CLAPPER_GTK_TYPE_SEEK_BAR, NULL);
+}
+
+/**
+ * clapper_gtk_seek_bar_set_reveal_labels:
+ * @seek_bar: a #ClapperGtkSeekBar
+ * @reveal: whether to reveal labels
+ *
+ * Set whether the position and duration labels should be revealed.
+ */
+void
+clapper_gtk_seek_bar_set_reveal_labels (ClapperGtkSeekBar *self, gboolean reveal)
+{
+ g_return_if_fail (CLAPPER_GTK_IS_SEEK_BAR (self));
+
+ if (self->reveal_labels != reveal) {
+ self->reveal_labels = reveal;
+ gtk_revealer_set_reveal_child (GTK_REVEALER (self->position_revealer), reveal);
+ g_object_notify_by_pspec (G_OBJECT (self), param_specs[PROP_REVEAL_LABELS]);
+ }
+}
+
+/**
+ * clapper_gtk_seek_bar_get_reveal_labels:
+ * @seek_bar: a #ClapperGtkSeekBar
+ *
+ * Get whether the position and duration labels are going to be revealed.
+ *
+ * Returns: TRUE if the labels are going to be revealed, %FALSE otherwise.
+ */
+gboolean
+clapper_gtk_seek_bar_get_reveal_labels (ClapperGtkSeekBar *self)
+{
+ g_return_val_if_fail (CLAPPER_GTK_IS_SEEK_BAR (self), FALSE);
+
+ return self->reveal_labels;
+}
+
+/**
+ * clapper_gtk_seek_bar_set_seek_method:
+ * @seek_bar: a #ClapperGtkSeekBar
+ * @method: a #ClapperPlayerSeekMethod
+ *
+ * Set [enum@Clapper.PlayerSeekMethod] to use when seeking with seek bar.
+ */
+void
+clapper_gtk_seek_bar_set_seek_method (ClapperGtkSeekBar *self, ClapperPlayerSeekMethod method)
+{
+ g_return_if_fail (CLAPPER_GTK_IS_SEEK_BAR (self));
+
+ if (self->seek_method != method) {
+ self->seek_method = method;
+ GST_DEBUG_OBJECT (self, "Set seek method to: %i", self->seek_method);
+ g_object_notify_by_pspec (G_OBJECT (self), param_specs[PROP_SEEK_METHOD]);
+ }
+}
+
+/**
+ * clapper_gtk_seek_bar_get_seek_method:
+ * @seek_bar: a #ClapperGtkSeekBar
+ *
+ * Get [enum@Clapper.PlayerSeekMethod] used when seeking with seek bar.
+ *
+ * Returns: #ClapperPlayerSeekMethod used for seeking.
+ */
+ClapperPlayerSeekMethod
+clapper_gtk_seek_bar_get_seek_method (ClapperGtkSeekBar *self)
+{
+ g_return_val_if_fail (CLAPPER_GTK_IS_SEEK_BAR (self), DEFAULT_SEEK_METHOD);
+
+ return self->seek_method;
+}
+
+static void
+clapper_gtk_seek_bar_init (ClapperGtkSeekBar *self)
+{
+ gtk_widget_init_template (GTK_WIDGET (self));
+
+ self->reveal_labels = DEFAULT_REVEAL_LABELS;
+ self->seek_method = DEFAULT_SEEK_METHOD;
+
+ self->curr_marker_start = -1;
+ self->next_marker_start = -1;
+
+ gtk_revealer_set_reveal_child (GTK_REVEALER (self->position_revealer), self->reveal_labels);
+
+ /* Correction for calculated popover position when marks are drawn */
+ gtk_popover_set_offset (self->popover, 0, -2);
+}
+
+static void
+clapper_gtk_seek_bar_compute_expand (GtkWidget *widget,
+ gboolean *hexpand_p, gboolean *vexpand_p)
+{
+ GtkWidget *w;
+ gboolean hexpand = FALSE;
+ gboolean vexpand = FALSE;
+
+ for (w = gtk_widget_get_first_child (widget); w != NULL; w = gtk_widget_get_next_sibling (w)) {
+ hexpand = (hexpand || gtk_widget_compute_expand (w, GTK_ORIENTATION_HORIZONTAL));
+ vexpand = (vexpand || gtk_widget_compute_expand (w, GTK_ORIENTATION_VERTICAL));
+ }
+
+ *hexpand_p = hexpand;
+ *vexpand_p = vexpand;
+}
+
+static void
+clapper_gtk_seek_bar_size_allocate (GtkWidget *widget,
+ gint width, gint height, gint baseline)
+{
+ ClapperGtkSeekBar *self = CLAPPER_GTK_SEEK_BAR_CAST (widget);
+
+ gtk_popover_present (self->popover);
+
+ GTK_WIDGET_CLASS (parent_class)->size_allocate (widget, width, height, baseline);
+}
+
+static void
+clapper_gtk_seek_bar_realize (GtkWidget *widget)
+{
+ ClapperGtkSeekBar *self = CLAPPER_GTK_SEEK_BAR_CAST (widget);
+
+ if ((self->player = clapper_gtk_get_player_from_ancestor (widget))) {
+ ClapperQueue *queue = clapper_player_get_queue (self->player);
+
+ g_signal_connect (queue, "notify::current-item",
+ G_CALLBACK (_queue_current_item_changed_cb), self);
+ _queue_current_item_changed_cb (queue, NULL, self);
+ }
+
+ GTK_WIDGET_CLASS (parent_class)->realize (widget);
+}
+
+static void
+clapper_gtk_seek_bar_unrealize (GtkWidget *widget)
+{
+ ClapperGtkSeekBar *self = CLAPPER_GTK_SEEK_BAR_CAST (widget);
+
+ if (self->player) {
+ ClapperQueue *queue = clapper_player_get_queue (self->player);
+
+ if (self->position_signal_id != 0) {
+ g_signal_handler_disconnect (self->player, self->position_signal_id);
+ self->position_signal_id = 0;
+ }
+ g_signal_handlers_disconnect_by_func (queue, _queue_current_item_changed_cb, self);
+
+ self->player = NULL;
+ }
+
+ GTK_WIDGET_CLASS (parent_class)->unrealize (widget);
+}
+
+static void
+clapper_gtk_seek_bar_map (GtkWidget *widget)
+{
+ ClapperGtkSeekBar *self = CLAPPER_GTK_SEEK_BAR_CAST (widget);
+
+ if (self->player) {
+ if (self->position_signal_id == 0) {
+ self->position_signal_id = g_signal_connect (self->player,
+ "notify::position", G_CALLBACK (_player_position_changed_cb), self);
+ }
+ g_signal_connect (self->player, "notify::state",
+ G_CALLBACK (_player_state_changed_cb), self);
+ g_signal_connect (self->player, "seek-done",
+ G_CALLBACK (_player_seek_done_cb), self);
+
+ _player_position_changed_cb (self->player, NULL, self);
+ }
+
+ GTK_WIDGET_CLASS (parent_class)->map (widget);
+}
+
+static void
+clapper_gtk_seek_bar_unmap (GtkWidget *widget)
+{
+ ClapperGtkSeekBar *self = CLAPPER_GTK_SEEK_BAR_CAST (widget);
+
+ if (self->player) {
+ if (self->position_signal_id != 0) {
+ g_signal_handler_disconnect (self->player, self->position_signal_id);
+ self->position_signal_id = 0;
+ }
+ g_signal_handlers_disconnect_by_func (self->player, _player_state_changed_cb, self);
+ g_signal_handlers_disconnect_by_func (self->player, _player_seek_done_cb, self);
+ }
+
+ GTK_WIDGET_CLASS (parent_class)->unmap (widget);
+}
+
+static void
+_popover_unparent (GtkPopover *popover)
+{
+ gtk_widget_unparent (GTK_WIDGET (popover));
+}
+
+static void
+clapper_gtk_seek_bar_dispose (GObject *object)
+{
+ ClapperGtkSeekBar *self = CLAPPER_GTK_SEEK_BAR_CAST (object);
+
+ gtk_widget_dispose_template (GTK_WIDGET (object), CLAPPER_GTK_TYPE_SEEK_BAR);
+
+ g_clear_pointer (&self->position_revealer, gtk_widget_unparent);
+ g_clear_pointer (&self->scale, gtk_widget_unparent);
+ g_clear_pointer (&self->popover, _popover_unparent);
+ g_clear_pointer (&self->duration_revealer, gtk_widget_unparent);
+
+ G_OBJECT_CLASS (parent_class)->dispose (object);
+}
+
+static void
+clapper_gtk_seek_bar_finalize (GObject *object)
+{
+ ClapperGtkSeekBar *self = CLAPPER_GTK_SEEK_BAR_CAST (object);
+
+ GST_TRACE_OBJECT (self, "Finalize");
+
+ gst_clear_object (&self->current_item);
+
+ G_OBJECT_CLASS (parent_class)->finalize (object);
+}
+
+static void
+clapper_gtk_seek_bar_get_property (GObject *object, guint prop_id,
+ GValue *value, GParamSpec *pspec)
+{
+ ClapperGtkSeekBar *self = CLAPPER_GTK_SEEK_BAR_CAST (object);
+
+ switch (prop_id) {
+ case PROP_REVEAL_LABELS:
+ g_value_set_boolean (value, clapper_gtk_seek_bar_get_reveal_labels (self));
+ break;
+ case PROP_SEEK_METHOD:
+ g_value_set_enum (value, clapper_gtk_seek_bar_get_seek_method (self));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+clapper_gtk_seek_bar_set_property (GObject *object, guint prop_id,
+ const GValue *value, GParamSpec *pspec)
+{
+ ClapperGtkSeekBar *self = CLAPPER_GTK_SEEK_BAR_CAST (object);
+
+ switch (prop_id) {
+ case PROP_REVEAL_LABELS:
+ clapper_gtk_seek_bar_set_reveal_labels (self, g_value_get_boolean (value));
+ break;
+ case PROP_SEEK_METHOD:
+ clapper_gtk_seek_bar_set_seek_method (self, g_value_get_enum (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+clapper_gtk_seek_bar_class_init (ClapperGtkSeekBarClass *klass)
+{
+ GObjectClass *gobject_class = (GObjectClass *) klass;
+ GtkWidgetClass *widget_class = (GtkWidgetClass *) klass;
+
+ GST_DEBUG_CATEGORY_INIT (GST_CAT_DEFAULT, "clappergtkseekbar", 0,
+ "Clapper GTK Seek Bar");
+
+ gobject_class->get_property = clapper_gtk_seek_bar_get_property;
+ gobject_class->set_property = clapper_gtk_seek_bar_set_property;
+ gobject_class->dispose = clapper_gtk_seek_bar_dispose;
+ gobject_class->finalize = clapper_gtk_seek_bar_finalize;
+
+ widget_class->compute_expand = clapper_gtk_seek_bar_compute_expand;
+ widget_class->size_allocate = clapper_gtk_seek_bar_size_allocate;
+ widget_class->realize = clapper_gtk_seek_bar_realize;
+ widget_class->unrealize = clapper_gtk_seek_bar_unrealize;
+ widget_class->map = clapper_gtk_seek_bar_map;
+ widget_class->unmap = clapper_gtk_seek_bar_unmap;
+
+ /**
+ * ClapperGtkSeekBar:reveal-labels:
+ *
+ * Reveal state of the position and duration labels.
+ */
+ param_specs[PROP_REVEAL_LABELS] = g_param_spec_boolean ("reveal-labels",
+ NULL, NULL, DEFAULT_REVEAL_LABELS,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * ClapperGtkSeekBar:seek-method:
+ *
+ * Method used for seeking.
+ */
+ param_specs[PROP_SEEK_METHOD] = g_param_spec_enum ("seek-method",
+ NULL, NULL, CLAPPER_TYPE_PLAYER_SEEK_METHOD, DEFAULT_SEEK_METHOD,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+ g_object_class_install_properties (gobject_class, PROP_LAST, param_specs);
+
+ gtk_widget_class_set_template_from_resource (widget_class,
+ CLAPPER_GTK_RESOURCE_PREFIX "/ui/clapper-gtk-seek-bar.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, ClapperGtkSeekBar, position_revealer);
+ gtk_widget_class_bind_template_child (widget_class, ClapperGtkSeekBar, position_label);
+ gtk_widget_class_bind_template_child (widget_class, ClapperGtkSeekBar, scale);
+ gtk_widget_class_bind_template_child (widget_class, ClapperGtkSeekBar, popover);
+ gtk_widget_class_bind_template_child (widget_class, ClapperGtkSeekBar, popover_label);
+ gtk_widget_class_bind_template_child (widget_class, ClapperGtkSeekBar, duration_revealer);
+ gtk_widget_class_bind_template_child (widget_class, ClapperGtkSeekBar, duration_label);
+
+ gtk_widget_class_bind_template_callback (widget_class, scale_value_changed_cb);
+ gtk_widget_class_bind_template_callback (widget_class, scale_css_classes_changed_cb);
+ gtk_widget_class_bind_template_callback (widget_class, motion_cb);
+ gtk_widget_class_bind_template_callback (widget_class, motion_leave_cb);
+ gtk_widget_class_bind_template_callback (widget_class, touch_released_cb);
+
+ gtk_widget_class_set_layout_manager_type (widget_class, GTK_TYPE_BOX_LAYOUT);
+ gtk_widget_class_set_accessible_role (widget_class, GTK_ACCESSIBLE_ROLE_GENERIC);
+ gtk_widget_class_set_css_name (widget_class, "clapper-gtk-seek-bar");
+}
diff --git a/src/lib/clapper-gtk/clapper-gtk-seek-bar.h b/src/lib/clapper-gtk/clapper-gtk-seek-bar.h
new file mode 100644
index 00000000..b2f93215
--- /dev/null
+++ b/src/lib/clapper-gtk/clapper-gtk-seek-bar.h
@@ -0,0 +1,48 @@
+/* Clapper GTK Integration Library
+ * Copyright (C) 2024 Rafał Dzięgiel
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+#pragma once
+
+#if !defined(__CLAPPER_GTK_INSIDE__) && !defined(CLAPPER_GTK_COMPILATION)
+#error "Only can be included directly."
+#endif
+
+#include
+#include
+#include
+#include
+
+G_BEGIN_DECLS
+
+#define CLAPPER_GTK_TYPE_SEEK_BAR (clapper_gtk_seek_bar_get_type())
+#define CLAPPER_GTK_SEEK_BAR_CAST(obj) ((ClapperGtkSeekBar *)(obj))
+
+G_DECLARE_FINAL_TYPE (ClapperGtkSeekBar, clapper_gtk_seek_bar, CLAPPER_GTK, SEEK_BAR, GtkWidget)
+
+GtkWidget * clapper_gtk_seek_bar_new (void);
+
+void clapper_gtk_seek_bar_set_reveal_labels (ClapperGtkSeekBar *seek_bar, gboolean reveal);
+
+gboolean clapper_gtk_seek_bar_get_reveal_labels (ClapperGtkSeekBar *seek_bar);
+
+void clapper_gtk_seek_bar_set_seek_method (ClapperGtkSeekBar *seek_bar, ClapperPlayerSeekMethod method);
+
+ClapperPlayerSeekMethod clapper_gtk_seek_bar_get_seek_method (ClapperGtkSeekBar *seek_bar);
+
+G_END_DECLS
diff --git a/src/lib/clapper-gtk/clapper-gtk-simple-controls.c b/src/lib/clapper-gtk/clapper-gtk-simple-controls.c
new file mode 100644
index 00000000..38a9dcc2
--- /dev/null
+++ b/src/lib/clapper-gtk/clapper-gtk-simple-controls.c
@@ -0,0 +1,344 @@
+/* Clapper GTK Integration Library
+ * Copyright (C) 2024 Rafał Dzięgiel
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+/**
+ * ClapperGtkSimpleControls:
+ *
+ * A minimalistic playback controls panel widget.
+ *
+ * #ClapperGtkSimpleControls is a simple, ready to be used playback controls widget.
+ * It is meant to be placed as an overlay (either fading or not) of [class@ClapperGtk.Video]
+ * as-is, providing minimal yet universal playback controls for your app.
+ *
+ * If you need a further customized controls, please use individual widgets this
+ * widget consists of to build your own controls implementation instead.
+ */
+
+#include "config.h"
+
+#include
+
+#include "clapper-gtk-simple-controls.h"
+#include "clapper-gtk-seek-bar.h"
+
+#define DEFAULT_FULLSCREENABLE TRUE
+#define DEFAULT_SEEK_METHOD CLAPPER_PLAYER_SEEK_METHOD_NORMAL
+
+#define IS_REVEALED(widget) (gtk_revealer_get_child_revealed ((GtkRevealer *) (widget)))
+#define IS_REVEAL(widget) (gtk_revealer_get_reveal_child ((GtkRevealer *) (widget)))
+#define SET_REVEAL(widget,reveal) (gtk_revealer_set_reveal_child ((GtkRevealer *) (widget), reveal))
+
+#define GST_CAT_DEFAULT clapper_gtk_simple_controls_debug
+GST_DEBUG_CATEGORY_STATIC (GST_CAT_DEFAULT);
+
+struct _ClapperGtkSimpleControls
+{
+ ClapperGtkContainer parent;
+
+ GtkWidget *seek_bar;
+ GtkWidget *extra_menu_button;
+ GtkWidget *fullscreen_top_revealer;
+ GtkWidget *fullscreen_bottom_revealer;
+ GtkWidget *controls_slide_revealer;
+
+ gboolean fullscreenable;
+ ClapperPlayerSeekMethod seek_method;
+
+ gboolean adapt;
+};
+
+#define parent_class clapper_gtk_simple_controls_parent_class
+G_DEFINE_TYPE (ClapperGtkSimpleControls, clapper_gtk_simple_controls, CLAPPER_GTK_TYPE_CONTAINER)
+
+enum
+{
+ PROP_0,
+ PROP_FULLSCREENABLE,
+ PROP_SEEK_METHOD,
+ PROP_EXTRA_MENU_BUTTON,
+ PROP_LAST
+};
+
+static GParamSpec *param_specs[PROP_LAST] = { NULL, };
+
+static void
+initial_adapt_cb (ClapperGtkContainer *container, gboolean adapt,
+ ClapperGtkSimpleControls *self)
+{
+ GST_DEBUG_OBJECT (self, "Initially adapted: %s", (adapt) ? "yes" : "no");
+
+ clapper_gtk_seek_bar_set_reveal_labels (CLAPPER_GTK_SEEK_BAR (self->seek_bar), !adapt);
+}
+
+static void
+full_adapt_cb (ClapperGtkContainer *container, gboolean adapt,
+ ClapperGtkSimpleControls *self)
+{
+ self->adapt = adapt;
+
+ GST_DEBUG_OBJECT (self, "Width adapted: %s", (self->adapt) ? "yes" : "no");
+
+ /* Take different action, depending on transition step we are currently at */
+ if (self->adapt) {
+ if (IS_REVEAL (self->fullscreen_bottom_revealer))
+ SET_REVEAL (self->fullscreen_bottom_revealer, FALSE);
+ else if (IS_REVEAL (self->controls_slide_revealer))
+ SET_REVEAL (self->controls_slide_revealer, FALSE);
+ else
+ SET_REVEAL (self->fullscreen_top_revealer, TRUE);
+ } else {
+ if (IS_REVEAL (self->fullscreen_top_revealer))
+ SET_REVEAL (self->fullscreen_top_revealer, FALSE);
+ else if (!IS_REVEAL (self->controls_slide_revealer))
+ SET_REVEAL (self->controls_slide_revealer, TRUE);
+ else
+ SET_REVEAL (self->fullscreen_bottom_revealer, TRUE);
+ }
+}
+
+static void
+controls_revealed_cb (GtkRevealer *revealer,
+ GParamSpec *pspec G_GNUC_UNUSED, ClapperGtkSimpleControls *self)
+{
+ gboolean revealed = IS_REVEALED (revealer);
+
+ GST_DEBUG_OBJECT (self, "Slide revealed: %s", (revealed) ? "yes" : "no");
+
+ /* We should be hidden when adapted, otherwise go back */
+ if (G_UNLIKELY (revealed == self->adapt))
+ gtk_revealer_set_reveal_child (revealer, !revealed);
+}
+
+/**
+ * clapper_gtk_simple_controls_new:
+ *
+ * Creates a new #ClapperGtkSimpleControls instance.
+ *
+ * Returns: a new simple controls #GtkWidget.
+ */
+GtkWidget *
+clapper_gtk_simple_controls_new (void)
+{
+ return g_object_new (CLAPPER_GTK_TYPE_SIMPLE_CONTROLS, NULL);
+}
+
+/**
+ * clapper_gtk_simple_controls_set_fullscreenable:
+ * @controls: a #ClapperGtkSimpleControls
+ * @fullscreenable: whether show button for toggling fullscreen state
+ *
+ * Set whether [class@ClapperGtk.ToggleFullscreenButton] button in the @controls
+ * should be visible.
+ *
+ * You might want to consider setting this to %FALSE, if your application
+ * does not implement [signal@ClapperGtk.Video::toggle-fullscreen] signal.
+ */
+void
+clapper_gtk_simple_controls_set_fullscreenable (ClapperGtkSimpleControls *self, gboolean fullscreenable)
+{
+ g_return_if_fail (CLAPPER_GTK_IS_SIMPLE_CONTROLS (self));
+
+ if (self->fullscreenable != fullscreenable) {
+ self->fullscreenable = fullscreenable;
+ g_object_notify_by_pspec (G_OBJECT (self), param_specs[PROP_FULLSCREENABLE]);
+ }
+}
+
+/**
+ * clapper_gtk_simple_controls_get_fullscreenable:
+ * @controls: a #ClapperGtkSimpleControls
+ *
+ * Get whether [class@ClapperGtk.ToggleFullscreenButton] button in the @controls
+ * is set to be visible.
+ */
+gboolean
+clapper_gtk_simple_controls_get_fullscreenable (ClapperGtkSimpleControls *self)
+{
+ g_return_val_if_fail (CLAPPER_GTK_IS_SIMPLE_CONTROLS (self), FALSE);
+
+ return self->fullscreenable;
+}
+
+/**
+ * clapper_gtk_simple_controls_set_seek_method:
+ * @controls: a #ClapperGtkSimpleControls
+ * @method: a #ClapperPlayerSeekMethod
+ *
+ * Set [enum@Clapper.PlayerSeekMethod] to use when seeking with progress bar.
+ */
+void
+clapper_gtk_simple_controls_set_seek_method (ClapperGtkSimpleControls *self,
+ ClapperPlayerSeekMethod method)
+{
+ g_return_if_fail (CLAPPER_GTK_IS_SIMPLE_CONTROLS (self));
+
+ clapper_gtk_seek_bar_set_seek_method (CLAPPER_GTK_SEEK_BAR (self->seek_bar), method);
+}
+
+/**
+ * clapper_gtk_simple_controls_get_seek_method:
+ * @controls: a #ClapperGtkSimpleControls
+ *
+ * Get [enum@Clapper.PlayerSeekMethod] used when seeking with progress bar.
+ *
+ * Returns: #ClapperPlayerSeekMethod used for seeking.
+ */
+ClapperPlayerSeekMethod
+clapper_gtk_simple_controls_get_seek_method (ClapperGtkSimpleControls *self)
+{
+ g_return_val_if_fail (CLAPPER_GTK_IS_SIMPLE_CONTROLS (self), DEFAULT_SEEK_METHOD);
+
+ return clapper_gtk_seek_bar_get_seek_method (CLAPPER_GTK_SEEK_BAR (self->seek_bar));
+}
+
+/**
+ * clapper_gtk_simple_controls_get_extra_menu_button:
+ * @controls: a #ClapperGtkSimpleControls
+ *
+ * Get [class@ClapperGtk.ExtraMenuButton] that resides within @controls.
+ *
+ * Returns: (transfer none): #ClapperGtkExtraMenuButton within simple controls panel.
+ */
+ClapperGtkExtraMenuButton *
+clapper_gtk_simple_controls_get_extra_menu_button (ClapperGtkSimpleControls *self)
+{
+ g_return_val_if_fail (CLAPPER_GTK_IS_SIMPLE_CONTROLS (self), NULL);
+
+ return CLAPPER_GTK_EXTRA_MENU_BUTTON (self->extra_menu_button);
+}
+
+static void
+clapper_gtk_simple_controls_init (ClapperGtkSimpleControls *self)
+{
+ self->fullscreenable = DEFAULT_FULLSCREENABLE;
+ self->seek_method = DEFAULT_SEEK_METHOD;
+
+ gtk_widget_init_template (GTK_WIDGET (self));
+
+ /* Set our defaults to children */
+ clapper_gtk_seek_bar_set_seek_method (
+ CLAPPER_GTK_SEEK_BAR_CAST (self->seek_bar), self->seek_method);
+}
+
+static void
+clapper_gtk_simple_controls_dispose (GObject *object)
+{
+ gtk_widget_dispose_template (GTK_WIDGET (object), CLAPPER_GTK_TYPE_SIMPLE_CONTROLS);
+
+ G_OBJECT_CLASS (parent_class)->dispose (object);
+}
+
+static void
+clapper_gtk_simple_controls_get_property (GObject *object, guint prop_id,
+ GValue *value, GParamSpec *pspec)
+{
+ ClapperGtkSimpleControls *self = CLAPPER_GTK_SIMPLE_CONTROLS_CAST (object);
+
+ switch (prop_id) {
+ case PROP_FULLSCREENABLE:
+ g_value_set_boolean (value, clapper_gtk_simple_controls_get_fullscreenable (self));
+ break;
+ case PROP_SEEK_METHOD:
+ g_value_set_enum (value, clapper_gtk_simple_controls_get_seek_method (self));
+ break;
+ case PROP_EXTRA_MENU_BUTTON:
+ g_value_set_object (value, clapper_gtk_simple_controls_get_extra_menu_button (self));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+clapper_gtk_simple_controls_set_property (GObject *object, guint prop_id,
+ const GValue *value, GParamSpec *pspec)
+{
+ ClapperGtkSimpleControls *self = CLAPPER_GTK_SIMPLE_CONTROLS_CAST (object);
+
+ switch (prop_id) {
+ case PROP_FULLSCREENABLE:
+ clapper_gtk_simple_controls_set_fullscreenable (self, g_value_get_boolean (value));
+ break;
+ case PROP_SEEK_METHOD:
+ clapper_gtk_simple_controls_set_seek_method (self, g_value_get_enum (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+clapper_gtk_simple_controls_class_init (ClapperGtkSimpleControlsClass *klass)
+{
+ GObjectClass *gobject_class = (GObjectClass *) klass;
+ GtkWidgetClass *widget_class = (GtkWidgetClass *) klass;
+
+ GST_DEBUG_CATEGORY_INIT (GST_CAT_DEFAULT, "clappergtksimplecontrols", 0,
+ "Clapper GTK Simple Controls");
+
+ gobject_class->get_property = clapper_gtk_simple_controls_get_property;
+ gobject_class->set_property = clapper_gtk_simple_controls_set_property;
+ gobject_class->dispose = clapper_gtk_simple_controls_dispose;
+
+ /**
+ * ClapperGtkSimpleControls:fullscreenable:
+ *
+ * Whether toggle fullscreen button should be visible.
+ */
+ param_specs[PROP_FULLSCREENABLE] = g_param_spec_boolean ("fullscreenable",
+ NULL, NULL, DEFAULT_FULLSCREENABLE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * ClapperGtkSimpleControls:seek-method:
+ *
+ * Method used for seeking.
+ */
+ param_specs[PROP_SEEK_METHOD] = g_param_spec_enum ("seek-method",
+ NULL, NULL, CLAPPER_TYPE_PLAYER_SEEK_METHOD, DEFAULT_SEEK_METHOD,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * ClapperGtkSimpleControls:extra-menu-button:
+ *
+ * Access to extra menu button within controls.
+ */
+ param_specs[PROP_EXTRA_MENU_BUTTON] = g_param_spec_object ("extra-menu-button",
+ NULL, NULL, CLAPPER_GTK_TYPE_EXTRA_MENU_BUTTON,
+ G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+ g_object_class_install_properties (gobject_class, PROP_LAST, param_specs);
+
+ gtk_widget_class_set_template_from_resource (widget_class,
+ CLAPPER_GTK_RESOURCE_PREFIX "/ui/clapper-gtk-simple-controls.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, ClapperGtkSimpleControls, seek_bar);
+ gtk_widget_class_bind_template_child (widget_class, ClapperGtkSimpleControls, extra_menu_button);
+ gtk_widget_class_bind_template_child (widget_class, ClapperGtkSimpleControls, fullscreen_top_revealer);
+ gtk_widget_class_bind_template_child (widget_class, ClapperGtkSimpleControls, fullscreen_bottom_revealer);
+ gtk_widget_class_bind_template_child (widget_class, ClapperGtkSimpleControls, controls_slide_revealer);
+
+ gtk_widget_class_bind_template_callback (widget_class, initial_adapt_cb);
+ gtk_widget_class_bind_template_callback (widget_class, full_adapt_cb);
+ gtk_widget_class_bind_template_callback (widget_class, controls_revealed_cb);
+
+ gtk_widget_class_set_css_name (widget_class, "clapper-gtk-simple-controls");
+}
diff --git a/src/lib/clapper-gtk/clapper-gtk-simple-controls.h b/src/lib/clapper-gtk/clapper-gtk-simple-controls.h
new file mode 100644
index 00000000..cd1fcd4a
--- /dev/null
+++ b/src/lib/clapper-gtk/clapper-gtk-simple-controls.h
@@ -0,0 +1,51 @@
+/* Clapper GTK Integration Library
+ * Copyright (C) 2024 Rafał Dzięgiel
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+#pragma once
+
+#if !defined(__CLAPPER_GTK_INSIDE__) && !defined(CLAPPER_GTK_COMPILATION)
+#error "Only can be included directly."
+#endif
+
+#include
+#include
+#include
+#include
+#include
+
+G_BEGIN_DECLS
+
+#define CLAPPER_GTK_TYPE_SIMPLE_CONTROLS (clapper_gtk_simple_controls_get_type())
+#define CLAPPER_GTK_SIMPLE_CONTROLS_CAST(obj) ((ClapperGtkSimpleControls *)(obj))
+
+G_DECLARE_FINAL_TYPE (ClapperGtkSimpleControls, clapper_gtk_simple_controls, CLAPPER_GTK, SIMPLE_CONTROLS, ClapperGtkContainer)
+
+GtkWidget * clapper_gtk_simple_controls_new (void);
+
+void clapper_gtk_simple_controls_set_fullscreenable (ClapperGtkSimpleControls *controls, gboolean fullscreenable);
+
+gboolean clapper_gtk_simple_controls_get_fullscreenable (ClapperGtkSimpleControls *controls);
+
+void clapper_gtk_simple_controls_set_seek_method (ClapperGtkSimpleControls *controls, ClapperPlayerSeekMethod method);
+
+ClapperPlayerSeekMethod clapper_gtk_simple_controls_get_seek_method (ClapperGtkSimpleControls *controls);
+
+ClapperGtkExtraMenuButton * clapper_gtk_simple_controls_get_extra_menu_button (ClapperGtkSimpleControls *controls);
+
+G_END_DECLS
diff --git a/src/lib/clapper-gtk/clapper-gtk-status-private.h b/src/lib/clapper-gtk/clapper-gtk-status-private.h
new file mode 100644
index 00000000..00b94bfa
--- /dev/null
+++ b/src/lib/clapper-gtk/clapper-gtk-status-private.h
@@ -0,0 +1,44 @@
+/* Clapper GTK Integration Library
+ * Copyright (C) 2024 Rafał Dzięgiel
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+#pragma once
+
+#include
+#include
+#include
+
+#include "clapper-gtk-container.h"
+
+G_BEGIN_DECLS
+
+#define CLAPPER_GTK_TYPE_STATUS (clapper_gtk_status_get_type())
+#define CLAPPER_GTK_STATUS_CAST(obj) ((ClapperGtkStatus *)(obj))
+
+G_DECLARE_FINAL_TYPE (ClapperGtkStatus, clapper_gtk_status, CLAPPER_GTK, STATUS, ClapperGtkContainer)
+
+G_GNUC_INTERNAL
+void clapper_gtk_status_set_error (ClapperGtkStatus *status, const GError *error);
+
+G_GNUC_INTERNAL
+void clapper_gtk_status_set_missing_plugin (ClapperGtkStatus *status, const gchar *name);
+
+G_GNUC_INTERNAL
+void clapper_gtk_status_clear (ClapperGtkStatus *status);
+
+G_END_DECLS
diff --git a/src/lib/clapper-gtk/clapper-gtk-status.c b/src/lib/clapper-gtk/clapper-gtk-status.c
new file mode 100644
index 00000000..0076f926
--- /dev/null
+++ b/src/lib/clapper-gtk/clapper-gtk-status.c
@@ -0,0 +1,137 @@
+/* Clapper GTK Integration Library
+ * Copyright (C) 2024 Rafał Dzięgiel
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+#include "config.h"
+
+#include
+
+#include "clapper-gtk-status-private.h"
+
+#define NORMAL_SPACING 16
+#define ADAPT_SPACING 8
+
+#define GST_CAT_DEFAULT clapper_gtk_status_debug
+GST_DEBUG_CATEGORY_STATIC (GST_CAT_DEFAULT);
+
+struct _ClapperGtkStatus
+{
+ ClapperGtkContainer parent;
+
+ GtkWidget *status_box;
+ GtkWidget *image;
+ GtkWidget *title_label;
+ GtkWidget *description_label;
+};
+
+#define parent_class clapper_gtk_status_parent_class
+G_DEFINE_TYPE (ClapperGtkStatus, clapper_gtk_status, CLAPPER_GTK_TYPE_CONTAINER)
+
+static void
+adapt_cb (ClapperGtkContainer *container, gboolean adapt,
+ ClapperGtkStatus *self)
+{
+ GST_DEBUG_OBJECT (self, "Adapted: %s", (adapt) ? "yes" : "no");
+
+ gtk_box_set_spacing (GTK_BOX (self->status_box), (adapt) ? ADAPT_SPACING : NORMAL_SPACING);
+
+ if (adapt) {
+ gtk_widget_add_css_class (GTK_WIDGET (self), "adapted");
+ gtk_widget_add_css_class (GTK_WIDGET (self->title_label), "title-2");
+ } else {
+ gtk_widget_remove_css_class (GTK_WIDGET (self), "adapted");
+ gtk_widget_remove_css_class (GTK_WIDGET (self->title_label), "title-2");
+ }
+}
+
+static void
+_set_status (ClapperGtkStatus *self, const gchar *icon_name,
+ const gchar *title, const gchar *description)
+{
+ gtk_image_set_from_icon_name (GTK_IMAGE (self->image), icon_name);
+ gtk_label_set_label (GTK_LABEL (self->title_label), title);
+ gtk_label_set_label (GTK_LABEL (self->description_label), description);
+
+ gtk_widget_set_visible (GTK_WIDGET (self), TRUE);
+}
+
+void
+clapper_gtk_status_set_error (ClapperGtkStatus *self, const GError *error)
+{
+ GST_DEBUG_OBJECT (self, "Status set to \"error\"");
+ _set_status (self, "dialog-warning-symbolic", "Unplayable Content", error->message);
+}
+
+void
+clapper_gtk_status_set_missing_plugin (ClapperGtkStatus *self, const gchar *name)
+{
+ gchar *description;
+
+ GST_DEBUG_OBJECT (self, "Status set to \"missing-plugin\"");
+ description = g_strdup_printf ("Your GStreamer installation is missing a plugin: %s", name);
+ _set_status (self, "dialog-information-symbolic", "Missing Plugin", description);
+
+ g_free (description);
+}
+
+void
+clapper_gtk_status_clear (ClapperGtkStatus *self)
+{
+ GST_DEBUG_OBJECT (self, "Status cleared");
+ gtk_widget_set_visible (GTK_WIDGET (self), FALSE);
+}
+
+static void
+clapper_gtk_status_init (ClapperGtkStatus *self)
+{
+ gtk_widget_init_template (GTK_WIDGET (self));
+
+ gtk_box_set_spacing (GTK_BOX (self->status_box), NORMAL_SPACING);
+}
+
+static void
+clapper_gtk_status_dispose (GObject *object)
+{
+ gtk_widget_dispose_template (GTK_WIDGET (object), CLAPPER_GTK_TYPE_STATUS);
+
+ G_OBJECT_CLASS (parent_class)->dispose (object);
+}
+
+static void
+clapper_gtk_status_class_init (ClapperGtkStatusClass *klass)
+{
+ GObjectClass *gobject_class = (GObjectClass *) klass;
+ GtkWidgetClass *widget_class = (GtkWidgetClass *) klass;
+
+ GST_DEBUG_CATEGORY_INIT (GST_CAT_DEFAULT, "clappergtkstatus", 0,
+ "Clapper GTK Status");
+
+ gobject_class->dispose = clapper_gtk_status_dispose;
+
+ gtk_widget_class_set_template_from_resource (widget_class,
+ CLAPPER_GTK_RESOURCE_PREFIX "/ui/clapper-gtk-status.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, ClapperGtkStatus, status_box);
+ gtk_widget_class_bind_template_child (widget_class, ClapperGtkStatus, image);
+ gtk_widget_class_bind_template_child (widget_class, ClapperGtkStatus, title_label);
+ gtk_widget_class_bind_template_child (widget_class, ClapperGtkStatus, description_label);
+
+ gtk_widget_class_bind_template_callback (widget_class, adapt_cb);
+
+ gtk_widget_class_set_css_name (widget_class, "clapper-gtk-status");
+}
diff --git a/src/lib/clapper-gtk/clapper-gtk-stream-check-button-private.h b/src/lib/clapper-gtk/clapper-gtk-stream-check-button-private.h
new file mode 100644
index 00000000..b3f51084
--- /dev/null
+++ b/src/lib/clapper-gtk/clapper-gtk-stream-check-button-private.h
@@ -0,0 +1,33 @@
+/* Clapper GTK Integration Library
+ * Copyright (C) 2024 Rafał Dzięgiel
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+#pragma once
+
+#include
+#include
+#include
+
+G_BEGIN_DECLS
+
+#define CLAPPER_GTK_TYPE_STREAM_CHECK_BUTTON (clapper_gtk_stream_check_button_get_type())
+#define CLAPPER_GTK_STREAM_CHECK_BUTTON_CAST(obj) ((ClapperGtkStreamCheckButton *)(obj))
+
+G_DECLARE_FINAL_TYPE (ClapperGtkStreamCheckButton, clapper_gtk_stream_check_button, CLAPPER_GTK, STREAM_CHECK_BUTTON, GtkCheckButton)
+
+G_END_DECLS
diff --git a/src/lib/clapper-gtk/clapper-gtk-stream-check-button.c b/src/lib/clapper-gtk/clapper-gtk-stream-check-button.c
new file mode 100644
index 00000000..ad5be786
--- /dev/null
+++ b/src/lib/clapper-gtk/clapper-gtk-stream-check-button.c
@@ -0,0 +1,284 @@
+/* Clapper GTK Integration Library
+ * Copyright (C) 2024 Rafał Dzięgiel
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+#include "config.h"
+
+#include
+#include
+
+#include "clapper-gtk-stream-check-button-private.h"
+#include "clapper-gtk-utils-private.h"
+
+#define MAX_SIGNALS 4
+
+struct _ClapperGtkStreamCheckButton
+{
+ GtkCheckButton parent;
+
+ ClapperStream *stream;
+
+ GtkWidget *fallback_check_button;
+
+ gulong signal_ids[MAX_SIGNALS];
+ gboolean grouped;
+};
+
+#define parent_class clapper_gtk_stream_check_button_parent_class
+G_DEFINE_TYPE (ClapperGtkStreamCheckButton, clapper_gtk_stream_check_button, GTK_TYPE_CHECK_BUTTON)
+
+enum
+{
+ PROP_0,
+ PROP_STREAM,
+ PROP_LAST
+};
+
+static GParamSpec *param_specs[PROP_LAST] = { NULL, };
+
+static inline gchar *
+_get_video_stream_label (ClapperVideoStream *vstream)
+{
+ gchar *label, *codec = clapper_video_stream_get_codec (vstream);
+ gint height = clapper_video_stream_get_height (vstream);
+ gdouble fps = clapper_video_stream_get_fps (vstream);
+
+ if (codec) {
+ label = g_strdup_printf ("%ip@%.3f [%s]", height, fps, codec);
+ g_free (codec);
+ } else {
+ label = g_strdup_printf ("%ip@%.3f", height, fps);
+ }
+
+ return label;
+}
+
+static inline gchar *
+_get_audio_stream_label (ClapperAudioStream *astream)
+{
+ gchar *label, *title, *codec = clapper_audio_stream_get_codec (astream);
+ gint channels = clapper_audio_stream_get_channels (astream);
+
+ if (!(title = clapper_stream_get_title (CLAPPER_STREAM_CAST (astream))))
+ title = clapper_audio_stream_get_lang_name (astream);
+
+ if (codec) {
+ label = g_strdup_printf ("%s [%s, %i %s]",
+ (title) ? title : _("Undetermined"), codec, channels, _("Channels"));
+ g_free (title);
+ g_free (codec);
+ } else {
+ label = title;
+ }
+
+ return label;
+}
+
+static inline gchar *
+_get_subtitle_stream_label (ClapperSubtitleStream *sstream)
+{
+ gchar *title;
+
+ if (!(title = clapper_stream_get_title (CLAPPER_STREAM_CAST (sstream))))
+ title = clapper_subtitle_stream_get_lang_name (sstream);
+
+ return title;
+}
+
+static void
+_refresh_label_cb (ClapperStream *stream,
+ GParamSpec *pspec G_GNUC_UNUSED, ClapperGtkStreamCheckButton *self)
+{
+ gchar *label = NULL;
+
+ if (stream) {
+ switch (clapper_stream_get_stream_type (stream)) {
+ case CLAPPER_STREAM_TYPE_VIDEO:
+ label = _get_video_stream_label (CLAPPER_VIDEO_STREAM (stream));
+ break;
+ case CLAPPER_STREAM_TYPE_AUDIO:
+ label = _get_audio_stream_label (CLAPPER_AUDIO_STREAM (stream));
+ break;
+ case CLAPPER_STREAM_TYPE_SUBTITLE:
+ label = _get_subtitle_stream_label (CLAPPER_SUBTITLE_STREAM (stream));
+ break;
+ default:
+ break;
+ }
+ }
+
+ gtk_check_button_set_label (GTK_CHECK_BUTTON (self),
+ (label != NULL) ? label : _("Undetermined"));
+ g_free (label);
+}
+
+static inline void
+_disconnect_current_signals (ClapperGtkStreamCheckButton *self)
+{
+ guint i;
+
+ for (i = 0; i < MAX_SIGNALS; ++i) {
+ /* No more signals connected */
+ if (self->signal_ids[i] == 0)
+ break;
+
+ g_signal_handler_disconnect (self->stream, self->signal_ids[i]);
+ self->signal_ids[i] = 0;
+ }
+}
+
+static inline void
+clapper_gtk_stream_check_button_set_stream (ClapperGtkStreamCheckButton *self, ClapperStream *stream)
+{
+ guint i = 0;
+
+ _disconnect_current_signals (self);
+ gst_object_replace ((GstObject **) &self->stream, GST_OBJECT_CAST (stream));
+
+ if (!self->stream)
+ return;
+
+ switch (clapper_stream_get_stream_type (stream)) {
+ case CLAPPER_STREAM_TYPE_VIDEO:
+ self->signal_ids[i++] = g_signal_connect (self->stream, "notify::codec",
+ G_CALLBACK (_refresh_label_cb), self);
+ self->signal_ids[i++] = g_signal_connect (self->stream, "notify::height",
+ G_CALLBACK (_refresh_label_cb), self);
+ self->signal_ids[i++] = g_signal_connect (self->stream, "notify::fps",
+ G_CALLBACK (_refresh_label_cb), self);
+ break;
+ case CLAPPER_STREAM_TYPE_AUDIO:
+ self->signal_ids[i++] = g_signal_connect (self->stream, "notify::codec",
+ G_CALLBACK (_refresh_label_cb), self);
+ self->signal_ids[i++] = g_signal_connect (self->stream, "notify::channels",
+ G_CALLBACK (_refresh_label_cb), self);
+ G_GNUC_FALLTHROUGH;
+ case CLAPPER_STREAM_TYPE_SUBTITLE:
+ self->signal_ids[i++] = g_signal_connect (self->stream, "notify::title",
+ G_CALLBACK (_refresh_label_cb), self);
+ self->signal_ids[i++] = g_signal_connect (self->stream, "notify::lang-name",
+ G_CALLBACK (_refresh_label_cb), self);
+ break;
+ default:
+ break;
+ }
+
+ _refresh_label_cb (self->stream, NULL, self);
+}
+
+static void
+clapper_gtk_stream_check_button_realize (GtkWidget *widget)
+{
+ ClapperGtkStreamCheckButton *self = CLAPPER_GTK_STREAM_CHECK_BUTTON_CAST (widget);
+ GtkWidget *other_widget;
+
+ GST_TRACE_OBJECT (self, "Realize");
+
+ /* Set same group as previous check button in the same list view */
+ if (!self->grouped) {
+ if ((other_widget = gtk_widget_get_parent (widget))
+ && (other_widget = gtk_widget_get_prev_sibling (other_widget))
+ && (other_widget = gtk_widget_get_first_child (other_widget))
+ && CLAPPER_GTK_IS_STREAM_CHECK_BUTTON (other_widget)) {
+ gtk_check_button_set_group (GTK_CHECK_BUTTON (self), GTK_CHECK_BUTTON (other_widget));
+ } else {
+ if (!self->fallback_check_button)
+ self->fallback_check_button = g_object_ref_sink (gtk_check_button_new ());
+
+ gtk_check_button_set_group (GTK_CHECK_BUTTON (self), GTK_CHECK_BUTTON (self->fallback_check_button));
+ }
+ self->grouped = TRUE;
+ }
+
+ GTK_WIDGET_CLASS (parent_class)->realize (widget);
+}
+
+static void
+clapper_gtk_stream_check_button_toggled (GtkCheckButton *check_button)
+{
+ ClapperGtkStreamCheckButton *self = CLAPPER_GTK_STREAM_CHECK_BUTTON_CAST (check_button);
+
+ if (gtk_check_button_get_active (check_button) && self->stream) {
+ ClapperStreamList *stream_list = CLAPPER_STREAM_LIST (gst_object_get_parent (GST_OBJECT (self->stream)));
+
+ if (G_LIKELY (stream_list != NULL)) {
+ GST_INFO_OBJECT (self, "Toggled: %" GST_PTR_FORMAT, self->stream);
+ clapper_stream_list_select_stream (stream_list, self->stream);
+ gst_object_unref (stream_list);
+ }
+ }
+}
+
+static void
+clapper_gtk_stream_check_button_init (ClapperGtkStreamCheckButton *self)
+{
+}
+
+static void
+clapper_gtk_stream_check_button_finalize (GObject *object)
+{
+ ClapperGtkStreamCheckButton *self = CLAPPER_GTK_STREAM_CHECK_BUTTON_CAST (object);
+
+ GST_TRACE_OBJECT (self, "Finalize");
+
+ _disconnect_current_signals (self);
+ gst_clear_object (&self->stream);
+
+ g_clear_object (&self->fallback_check_button);
+
+ G_OBJECT_CLASS (parent_class)->finalize (object);
+}
+
+static void
+clapper_gtk_stream_check_button_set_property (GObject *object, guint prop_id,
+ const GValue *value, GParamSpec *pspec)
+{
+ ClapperGtkStreamCheckButton *self = CLAPPER_GTK_STREAM_CHECK_BUTTON_CAST (object);
+
+ switch (prop_id) {
+ case PROP_STREAM:
+ clapper_gtk_stream_check_button_set_stream (self, g_value_get_object (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+clapper_gtk_stream_check_button_class_init (ClapperGtkStreamCheckButtonClass *klass)
+{
+ GObjectClass *gobject_class = (GObjectClass *) klass;
+ GtkWidgetClass *widget_class = (GtkWidgetClass *) klass;
+ GtkCheckButtonClass *check_button_class = (GtkCheckButtonClass *) klass;
+
+ clapper_gtk_init_translations ();
+
+ gobject_class->set_property = clapper_gtk_stream_check_button_set_property;
+ gobject_class->finalize = clapper_gtk_stream_check_button_finalize;
+
+ widget_class->realize = clapper_gtk_stream_check_button_realize;
+
+ check_button_class->toggled = clapper_gtk_stream_check_button_toggled;
+
+ param_specs[PROP_STREAM] = g_param_spec_object ("stream",
+ NULL, NULL, CLAPPER_TYPE_STREAM,
+ G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS);
+
+ g_object_class_install_properties (gobject_class, PROP_LAST, param_specs);
+}
diff --git a/src/lib/clapper-gtk/clapper-gtk-title-header.c b/src/lib/clapper-gtk/clapper-gtk-title-header.c
new file mode 100644
index 00000000..06d36187
--- /dev/null
+++ b/src/lib/clapper-gtk/clapper-gtk-title-header.c
@@ -0,0 +1,228 @@
+/* Clapper GTK Integration Library
+ * Copyright (C) 2024 Rafał Dzięgiel
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+/**
+ * ClapperGtkTitleHeader:
+ *
+ * A header panel widget that displays current media title.
+ *
+ * #ClapperGtkTitleHeader is a simple, ready to be used header widget that
+ * displays current media title. It can be placed as-is as a [class@ClapperGtk.Video]
+ * overlay (either fading or not).
+ *
+ * If you need a further customized header, you can use [class@ClapperGtk.TitleLabel]
+ * which is used by this widget to build your own implementation instead.
+ */
+
+#include "config.h"
+
+#include
+
+#include "clapper-gtk-title-header.h"
+#include "clapper-gtk-title-label.h"
+
+#define DEFAULT_FALLBACK_TO_URI FALSE
+
+#define GST_CAT_DEFAULT clapper_gtk_title_header_debug
+GST_DEBUG_CATEGORY_STATIC (GST_CAT_DEFAULT);
+
+struct _ClapperGtkTitleHeader
+{
+ ClapperGtkLeadContainer parent;
+
+ ClapperGtkTitleLabel *label;
+};
+
+#define parent_class clapper_gtk_title_header_parent_class
+G_DEFINE_TYPE (ClapperGtkTitleHeader, clapper_gtk_title_header, CLAPPER_GTK_TYPE_LEAD_CONTAINER)
+
+enum
+{
+ PROP_0,
+ PROP_CURRENT_TITLE,
+ PROP_FALLBACK_TO_URI,
+ PROP_LAST
+};
+
+static GParamSpec *param_specs[PROP_LAST] = { NULL, };
+
+static void
+_label_current_title_changed_cb (ClapperGtkTitleLabel *label G_GNUC_UNUSED,
+ GParamSpec *pspec G_GNUC_UNUSED, ClapperGtkTitleHeader *self)
+{
+ /* Forward current title changed notify from internal label */
+ g_object_notify_by_pspec (G_OBJECT (self), param_specs[PROP_CURRENT_TITLE]);
+}
+
+/**
+ * clapper_gtk_title_header_new:
+ *
+ * Creates a new #ClapperGtkTitleHeader instance.
+ *
+ * Returns: a new title header #GtkWidget.
+ */
+GtkWidget *
+clapper_gtk_title_header_new (void)
+{
+ return g_object_new (CLAPPER_GTK_TYPE_TITLE_HEADER, NULL);
+}
+
+/**
+ * clapper_gtk_title_header_get_current_title:
+ * @header: a #ClapperGtkTitleHeader
+ *
+ * Get currently displayed title by @header.
+ *
+ * Returns: (transfer none): text of title label.
+ */
+const gchar *
+clapper_gtk_title_header_get_current_title (ClapperGtkTitleHeader *self)
+{
+ g_return_val_if_fail (CLAPPER_GTK_IS_TITLE_HEADER (self), NULL);
+
+ return clapper_gtk_title_label_get_current_title (self->label);
+}
+
+/**
+ * clapper_gtk_title_header_set_fallback_to_uri:
+ * @header: a #ClapperGtkTitleHeader
+ * @enabled: whether enabled
+ *
+ * Set whether a [property@Clapper.MediaItem:uri] property should
+ * be displayed as a header text when no other title could be determined.
+ */
+void
+clapper_gtk_title_header_set_fallback_to_uri (ClapperGtkTitleHeader *self, gboolean enabled)
+{
+ g_return_if_fail (CLAPPER_GTK_IS_TITLE_HEADER (self));
+
+ clapper_gtk_title_label_set_fallback_to_uri (self->label, enabled);
+}
+
+/**
+ * clapper_gtk_title_header_get_fallback_to_uri:
+ * @header: a #ClapperGtkTitleHeader
+ *
+ * Get whether a [property@Clapper.MediaItem:uri] property is going
+ * be displayed as a header text when no other title could be determined.
+ */
+gboolean
+clapper_gtk_title_header_get_fallback_to_uri (ClapperGtkTitleHeader *self)
+{
+ g_return_val_if_fail (CLAPPER_GTK_IS_TITLE_HEADER (self), FALSE);
+
+ return clapper_gtk_title_label_get_fallback_to_uri (self->label);
+}
+
+static void
+clapper_gtk_title_header_init (ClapperGtkTitleHeader *self)
+{
+ gtk_widget_init_template (GTK_WIDGET (self));
+
+ clapper_gtk_title_label_set_fallback_to_uri (self->label, DEFAULT_FALLBACK_TO_URI);
+
+ g_object_bind_property (self->label, "fallback-to-uri",
+ self, "fallback-to-uri", G_BINDING_DEFAULT);
+ g_signal_connect (self->label, "notify::current-title",
+ G_CALLBACK (_label_current_title_changed_cb), self);
+}
+
+static void
+clapper_gtk_title_header_dispose (GObject *object)
+{
+ gtk_widget_dispose_template (GTK_WIDGET (object), CLAPPER_GTK_TYPE_TITLE_HEADER);
+
+ G_OBJECT_CLASS (parent_class)->dispose (object);
+}
+
+static void
+clapper_gtk_title_header_get_property (GObject *object, guint prop_id,
+ GValue *value, GParamSpec *pspec)
+{
+ ClapperGtkTitleHeader *self = CLAPPER_GTK_TITLE_HEADER_CAST (object);
+
+ switch (prop_id) {
+ case PROP_CURRENT_TITLE:
+ g_value_set_string (value, clapper_gtk_title_header_get_current_title (self));
+ break;
+ case PROP_FALLBACK_TO_URI:
+ g_value_set_boolean (value, clapper_gtk_title_header_get_fallback_to_uri (self));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+clapper_gtk_title_header_set_property (GObject *object, guint prop_id,
+ const GValue *value, GParamSpec *pspec)
+{
+ ClapperGtkTitleHeader *self = CLAPPER_GTK_TITLE_HEADER_CAST (object);
+
+ switch (prop_id) {
+ case PROP_FALLBACK_TO_URI:
+ clapper_gtk_title_header_set_fallback_to_uri (self, g_value_get_boolean (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+clapper_gtk_title_header_class_init (ClapperGtkTitleHeaderClass *klass)
+{
+ GObjectClass *gobject_class = (GObjectClass *) klass;
+ GtkWidgetClass *widget_class = (GtkWidgetClass *) klass;
+
+ GST_DEBUG_CATEGORY_INIT (GST_CAT_DEFAULT, "clappergtktitleheader", 0,
+ "Clapper GTK Title Header");
+
+ gobject_class->get_property = clapper_gtk_title_header_get_property;
+ gobject_class->set_property = clapper_gtk_title_header_set_property;
+ gobject_class->dispose = clapper_gtk_title_header_dispose;
+
+ /**
+ * ClapperGtkTitleHeader:current-title:
+ *
+ * Currently displayed title.
+ */
+ param_specs[PROP_CURRENT_TITLE] = g_param_spec_string ("current-title",
+ NULL, NULL, NULL,
+ G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * ClapperGtkTitleHeader:fallback-to-uri:
+ *
+ * When title cannot be determined, show URI instead.
+ */
+ param_specs[PROP_FALLBACK_TO_URI] = g_param_spec_boolean ("fallback-to-uri",
+ NULL, NULL, DEFAULT_FALLBACK_TO_URI,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+ g_object_class_install_properties (gobject_class, PROP_LAST, param_specs);
+
+ gtk_widget_class_set_template_from_resource (widget_class,
+ CLAPPER_GTK_RESOURCE_PREFIX "/ui/clapper-gtk-title-header.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, ClapperGtkTitleHeader, label);
+
+ gtk_widget_class_set_css_name (widget_class, "clapper-gtk-title-header");
+}
diff --git a/src/lib/clapper-gtk/clapper-gtk-title-header.h b/src/lib/clapper-gtk/clapper-gtk-title-header.h
new file mode 100644
index 00000000..2f918867
--- /dev/null
+++ b/src/lib/clapper-gtk/clapper-gtk-title-header.h
@@ -0,0 +1,46 @@
+/* Clapper GTK Integration Library
+ * Copyright (C) 2024 Rafał Dzięgiel
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+#pragma once
+
+#if !defined(__CLAPPER_GTK_INSIDE__) && !defined(CLAPPER_GTK_COMPILATION)
+#error "Only can be included directly."
+#endif
+
+#include
+#include
+#include
+#include
+
+G_BEGIN_DECLS
+
+#define CLAPPER_GTK_TYPE_TITLE_HEADER (clapper_gtk_title_header_get_type())
+#define CLAPPER_GTK_TITLE_HEADER_CAST(obj) ((ClapperGtkTitleHeader *)(obj))
+
+G_DECLARE_FINAL_TYPE (ClapperGtkTitleHeader, clapper_gtk_title_header, CLAPPER_GTK, TITLE_HEADER, ClapperGtkLeadContainer)
+
+GtkWidget * clapper_gtk_title_header_new (void);
+
+const gchar * clapper_gtk_title_header_get_current_title (ClapperGtkTitleHeader *header);
+
+void clapper_gtk_title_header_set_fallback_to_uri (ClapperGtkTitleHeader *header, gboolean enabled);
+
+gboolean clapper_gtk_title_header_get_fallback_to_uri (ClapperGtkTitleHeader *header);
+
+G_END_DECLS
diff --git a/src/lib/clapper-gtk/clapper-gtk-title-label.c b/src/lib/clapper-gtk/clapper-gtk-title-label.c
new file mode 100644
index 00000000..87ca4f60
--- /dev/null
+++ b/src/lib/clapper-gtk/clapper-gtk-title-label.c
@@ -0,0 +1,488 @@
+/* Clapper GTK Integration Library
+ * Copyright (C) 2024 Rafał Dzięgiel
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+/**
+ * ClapperGtkTitleLabel:
+ *
+ * A label showing an up to date title of media item.
+ *
+ * By default #ClapperGtkTitleLabel will automatically show title
+ * of [property@Clapper.Queue:current-item] when placed within
+ * [class@ClapperGtk.Video] widget hierarchy.
+ *
+ * Setting [property@ClapperGtk.TitleLabel:media-item] property will
+ * make it show title of that particular [class@Clapper.MediaItem]
+ * instead. Providing an item to read title from also allows using
+ * this [class@Gtk.Widget] outside of [class@ClapperGtk.Video].
+ */
+
+#include "config.h"
+
+#include
+#include
+
+#include "clapper-gtk-title-label.h"
+#include "clapper-gtk-utils-private.h"
+
+#define DEFAULT_FALLBACK_TO_URI FALSE
+
+#define GST_CAT_DEFAULT clapper_gtk_title_label_debug
+GST_DEBUG_CATEGORY_STATIC (GST_CAT_DEFAULT);
+
+struct _ClapperGtkTitleLabel
+{
+ GtkWidget parent;
+
+ GtkLabel *label;
+
+ ClapperMediaItem *current_item;
+ ClapperMediaItem *custom_item;
+ gboolean fallback_to_uri;
+
+ ClapperPlayer *player;
+};
+
+#define parent_class clapper_gtk_title_label_parent_class
+G_DEFINE_TYPE (ClapperGtkTitleLabel, clapper_gtk_title_label, GTK_TYPE_WIDGET)
+
+enum
+{
+ PROP_0,
+ PROP_MEDIA_ITEM,
+ PROP_CURRENT_TITLE,
+ PROP_FALLBACK_TO_URI,
+ PROP_LAST
+};
+
+static GParamSpec *param_specs[PROP_LAST] = { NULL, };
+
+static void
+_label_changed_cb (GtkLabel *label G_GNUC_UNUSED,
+ GParamSpec *pspec G_GNUC_UNUSED, ClapperGtkTitleLabel *self)
+{
+ g_object_notify_by_pspec (G_OBJECT (self), param_specs[PROP_CURRENT_TITLE]);
+}
+
+static void
+_refresh_title (ClapperGtkTitleLabel *self)
+{
+ ClapperMediaItem *item;
+ gchar *title;
+
+ item = (self->custom_item) ? self->custom_item : self->current_item;
+
+ if (!item) {
+ gtk_label_set_label (self->label, _("No media"));
+ return;
+ }
+
+ title = clapper_media_item_get_title (item);
+
+ if (title) {
+ gtk_label_set_label (self->label, title);
+ g_free (title);
+ } else if (self->fallback_to_uri) {
+ gtk_label_set_label (self->label, clapper_media_item_get_uri (item));
+ } else {
+ gtk_label_set_label (self->label, _("Unknown title"));
+ }
+}
+
+static void
+_media_item_title_changed_cb (ClapperMediaItem *item G_GNUC_UNUSED,
+ GParamSpec *pspec G_GNUC_UNUSED, ClapperGtkTitleLabel *self)
+{
+ _refresh_title (self);
+}
+
+static void
+_set_current_item (ClapperGtkTitleLabel *self, ClapperMediaItem *current_item)
+{
+ /* Disconnect signal from old item */
+ if (self->current_item) {
+ g_signal_handlers_disconnect_by_func (self->current_item,
+ _media_item_title_changed_cb, self);
+ }
+
+ gst_object_replace ((GstObject **) &self->current_item, GST_OBJECT_CAST (current_item));
+ GST_DEBUG ("Current item changed to: %" GST_PTR_FORMAT, self->current_item);
+
+ /* Reconnect signal to new item */
+ if (self->current_item) {
+ g_signal_connect (self->current_item, "notify::title",
+ G_CALLBACK (_media_item_title_changed_cb), self);
+ }
+}
+
+static void
+_queue_current_item_changed_cb (ClapperQueue *queue,
+ GParamSpec *pspec G_GNUC_UNUSED, ClapperGtkTitleLabel *self)
+{
+ ClapperMediaItem *current_item = clapper_queue_get_current_item (queue);
+
+ _set_current_item (self, current_item);
+ _refresh_title (self);
+
+ gst_clear_object (¤t_item);
+}
+
+static void
+_bind_current_item (ClapperGtkTitleLabel *self)
+{
+ ClapperQueue *queue = clapper_player_get_queue (self->player);
+ ClapperMediaItem *current_item;
+
+ GST_DEBUG ("Binding current item");
+
+ g_signal_connect (queue, "notify::current-item",
+ G_CALLBACK (_queue_current_item_changed_cb), self);
+
+ current_item = clapper_queue_get_current_item (queue);
+ _set_current_item (self, current_item);
+ gst_clear_object (¤t_item);
+}
+
+static void
+_unbind_current_item (ClapperGtkTitleLabel *self)
+{
+ ClapperQueue *queue = clapper_player_get_queue (self->player);
+
+ GST_DEBUG ("Unbinding current item");
+
+ g_signal_handlers_disconnect_by_func (queue,
+ _queue_current_item_changed_cb, self);
+ _set_current_item (self, NULL);
+}
+
+/**
+ * clapper_gtk_title_label_new:
+ *
+ * Creates a new #ClapperGtkTitleLabel instance.
+ *
+ * Returns: a new title label #GtkWidget.
+ */
+GtkWidget *
+clapper_gtk_title_label_new (void)
+{
+ return g_object_new (CLAPPER_GTK_TYPE_TITLE_LABEL, NULL);
+}
+
+/**
+ * clapper_gtk_title_label_set_media_item:
+ * @label: a #ClapperGtkTitleLabel
+ * @item: (nullable): a #ClapperMediaItem
+ *
+ * Set a media item to display title of as label. When set to %NULL,
+ * @label will use default behavior (showing title of current queue item).
+ */
+void
+clapper_gtk_title_label_set_media_item (ClapperGtkTitleLabel *self, ClapperMediaItem *item)
+{
+ g_return_if_fail (CLAPPER_GTK_IS_TITLE_LABEL (self));
+ g_return_if_fail (item == NULL || CLAPPER_IS_MEDIA_ITEM (item));
+
+ if (self->custom_item == item)
+ return;
+
+ if (self->player) {
+ _unbind_current_item (self);
+ self->player = NULL;
+ }
+ if (self->custom_item) {
+ g_signal_handlers_disconnect_by_func (self->custom_item,
+ _media_item_title_changed_cb, self);
+ }
+
+ gst_object_replace ((GstObject **) &self->custom_item, GST_OBJECT_CAST (item));
+
+ GST_DEBUG ("Set media item: %" GST_PTR_FORMAT, self->custom_item);
+ g_object_notify_by_pspec (G_OBJECT (self), param_specs[PROP_MEDIA_ITEM]);
+
+ if (self->custom_item) {
+ g_signal_connect (self->custom_item, "notify::title",
+ G_CALLBACK (_media_item_title_changed_cb), self);
+ } else if ((self->player = clapper_gtk_get_player_from_ancestor (GTK_WIDGET (self)))) {
+ _bind_current_item (self);
+ }
+
+ _refresh_title (self);
+}
+
+/**
+ * clapper_gtk_title_label_get_media_item:
+ * @label: a #ClapperGtkTitleLabel
+ *
+ * Get currently set media item to display title of.
+ *
+ * Returns: (transfer none) (nullable): currently set media item.
+ */
+ClapperMediaItem *
+clapper_gtk_title_label_get_media_item (ClapperGtkTitleLabel *self)
+{
+ g_return_val_if_fail (CLAPPER_GTK_IS_TITLE_LABEL (self), NULL);
+
+ return self->custom_item;
+}
+
+/**
+ * clapper_gtk_title_label_get_current_title:
+ * @label: a #ClapperGtkTitleLabel
+ *
+ * Get currently displayed title by @label.
+ *
+ * Returns: (transfer none): text of title label.
+ */
+const gchar *
+clapper_gtk_title_label_get_current_title (ClapperGtkTitleLabel *self)
+{
+ g_return_val_if_fail (CLAPPER_GTK_IS_TITLE_LABEL (self), NULL);
+
+ return gtk_label_get_label (self->label);
+}
+
+/**
+ * clapper_gtk_title_label_set_fallback_to_uri:
+ * @label: a #ClapperGtkTitleLabel
+ * @enabled: whether enabled
+ *
+ * Set whether a [property@Clapper.MediaItem:uri] property should
+ * be displayed as a label text when no other title could be determined.
+ */
+void
+clapper_gtk_title_label_set_fallback_to_uri (ClapperGtkTitleLabel *self, gboolean enabled)
+{
+ g_return_if_fail (CLAPPER_GTK_IS_TITLE_LABEL (self));
+
+ if (self->fallback_to_uri != enabled) {
+ self->fallback_to_uri = enabled;
+ g_object_notify_by_pspec (G_OBJECT (self), param_specs[PROP_FALLBACK_TO_URI]);
+
+ _refresh_title (self);
+ }
+}
+
+/**
+ * clapper_gtk_title_label_get_fallback_to_uri:
+ * @label: a #ClapperGtkTitleLabel
+ *
+ * Get whether a [property@Clapper.MediaItem:uri] property is going
+ * be displayed as a label text when no other title could be determined.
+ */
+gboolean
+clapper_gtk_title_label_get_fallback_to_uri (ClapperGtkTitleLabel *self)
+{
+ g_return_val_if_fail (CLAPPER_GTK_IS_TITLE_LABEL (self), FALSE);
+
+ return self->fallback_to_uri;
+}
+
+static void
+clapper_gtk_title_label_init (ClapperGtkTitleLabel *self)
+{
+ self->label = GTK_LABEL (gtk_label_new (NULL));
+ gtk_label_set_single_line_mode (self->label, TRUE);
+ gtk_label_set_ellipsize (self->label, PANGO_ELLIPSIZE_END);
+ gtk_widget_set_can_target (GTK_WIDGET (self->label), FALSE);
+ gtk_widget_set_parent (GTK_WIDGET (self->label), GTK_WIDGET (self));
+
+ self->fallback_to_uri = DEFAULT_FALLBACK_TO_URI;
+
+ /* Apply CSS styles to internal label */
+ g_object_bind_property (self, "css-classes",
+ self->label, "css-classes", G_BINDING_DEFAULT);
+}
+
+static void
+clapper_gtk_title_label_constructed (GObject *object)
+{
+ ClapperGtkTitleLabel *self = CLAPPER_GTK_TITLE_LABEL_CAST (object);
+
+ /* Ensure label if no custom item set yet */
+ if (!self->custom_item)
+ _refresh_title (self);
+
+ /* This avoids us from comparing label changes as GTK will do this
+ * for us and emit this signal only when label text actually changes. */
+ g_signal_connect (self->label, "notify::label",
+ G_CALLBACK (_label_changed_cb), self);
+
+ G_OBJECT_CLASS (parent_class)->constructed (object);
+}
+
+static void
+clapper_gtk_title_label_compute_expand (GtkWidget *widget,
+ gboolean *hexpand_p, gboolean *vexpand_p)
+{
+ ClapperGtkTitleLabel *self = CLAPPER_GTK_TITLE_LABEL_CAST (widget);
+
+ *hexpand_p = gtk_widget_compute_expand ((GtkWidget *) self->label, GTK_ORIENTATION_HORIZONTAL);
+ *vexpand_p = gtk_widget_compute_expand ((GtkWidget *) self->label, GTK_ORIENTATION_VERTICAL);
+}
+
+static void
+clapper_gtk_title_label_root (GtkWidget *widget)
+{
+ ClapperGtkTitleLabel *self = CLAPPER_GTK_TITLE_LABEL_CAST (widget);
+
+ GTK_WIDGET_CLASS (parent_class)->root (widget);
+
+ if (!self->custom_item
+ && (self->player = clapper_gtk_get_player_from_ancestor (widget))) {
+ GST_LOG ("Label placed without media item set");
+ _bind_current_item (self);
+ _refresh_title (self);
+ }
+}
+
+static void
+clapper_gtk_title_label_unroot (GtkWidget *widget)
+{
+ ClapperGtkTitleLabel *self = CLAPPER_GTK_TITLE_LABEL_CAST (widget);
+
+ if (self->player) {
+ _unbind_current_item (self);
+ self->player = NULL;
+ }
+
+ GTK_WIDGET_CLASS (parent_class)->unroot (widget);
+}
+
+static void
+_label_unparent (GtkLabel *label)
+{
+ gtk_widget_unparent (GTK_WIDGET (label));
+}
+
+static void
+clapper_gtk_title_label_dispose (GObject *object)
+{
+ ClapperGtkTitleLabel *self = CLAPPER_GTK_TITLE_LABEL_CAST (object);
+
+ if (self->custom_item) {
+ g_signal_handlers_disconnect_by_func (self->custom_item,
+ _media_item_title_changed_cb, self);
+ }
+ if (self->label) {
+ g_signal_handlers_disconnect_by_func (self->label,
+ _label_changed_cb, self);
+ }
+
+ gst_clear_object (&self->current_item);
+ gst_clear_object (&self->custom_item);
+ g_clear_pointer (&self->label, _label_unparent);
+
+ G_OBJECT_CLASS (parent_class)->dispose (object);
+}
+
+static void
+clapper_gtk_title_label_get_property (GObject *object, guint prop_id,
+ GValue *value, GParamSpec *pspec)
+{
+ ClapperGtkTitleLabel *self = CLAPPER_GTK_TITLE_LABEL_CAST (object);
+
+ switch (prop_id) {
+ case PROP_MEDIA_ITEM:
+ g_value_set_object (value, clapper_gtk_title_label_get_media_item (self));
+ break;
+ case PROP_CURRENT_TITLE:
+ g_value_set_string (value, clapper_gtk_title_label_get_current_title (self));
+ break;
+ case PROP_FALLBACK_TO_URI:
+ g_value_set_boolean (value, clapper_gtk_title_label_get_fallback_to_uri (self));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+clapper_gtk_title_label_set_property (GObject *object, guint prop_id,
+ const GValue *value, GParamSpec *pspec)
+{
+ ClapperGtkTitleLabel *self = CLAPPER_GTK_TITLE_LABEL_CAST (object);
+
+ switch (prop_id) {
+ case PROP_MEDIA_ITEM:
+ clapper_gtk_title_label_set_media_item (self, g_value_get_object (value));
+ break;
+ case PROP_FALLBACK_TO_URI:
+ clapper_gtk_title_label_set_fallback_to_uri (self, g_value_get_boolean (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+clapper_gtk_title_label_class_init (ClapperGtkTitleLabelClass *klass)
+{
+ GObjectClass *gobject_class = (GObjectClass *) klass;
+ GtkWidgetClass *widget_class = (GtkWidgetClass *) klass;
+
+ GST_DEBUG_CATEGORY_INIT (GST_CAT_DEFAULT, "clappergtktitlelabel", 0,
+ "Clapper GTK Title Label");
+ clapper_gtk_init_translations ();
+
+ gobject_class->constructed = clapper_gtk_title_label_constructed;
+ gobject_class->get_property = clapper_gtk_title_label_get_property;
+ gobject_class->set_property = clapper_gtk_title_label_set_property;
+ gobject_class->dispose = clapper_gtk_title_label_dispose;
+
+ widget_class->compute_expand = clapper_gtk_title_label_compute_expand;
+
+ /* Using root/unroot so label "current-title" is immediately
+ * updated and can be accessed before label was made visible */
+ widget_class->root = clapper_gtk_title_label_root;
+ widget_class->unroot = clapper_gtk_title_label_unroot;
+
+ /**
+ * ClapperGtkTitleLabel:media-item:
+ *
+ * Currently set media item to display title of.
+ */
+ param_specs[PROP_MEDIA_ITEM] = g_param_spec_object ("media-item",
+ NULL, NULL, CLAPPER_TYPE_MEDIA_ITEM,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * ClapperGtkTitleLabel:current-title:
+ *
+ * Currently displayed title.
+ */
+ param_specs[PROP_CURRENT_TITLE] = g_param_spec_string ("current-title",
+ NULL, NULL, NULL,
+ G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * ClapperGtkTitleLabel:fallback-to-uri:
+ *
+ * When title cannot be determined, show URI instead.
+ */
+ param_specs[PROP_FALLBACK_TO_URI] = g_param_spec_boolean ("fallback-to-uri",
+ NULL, NULL, DEFAULT_FALLBACK_TO_URI,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+ g_object_class_install_properties (gobject_class, PROP_LAST, param_specs);
+
+ gtk_widget_class_set_layout_manager_type (widget_class, GTK_TYPE_BIN_LAYOUT);
+ gtk_widget_class_set_accessible_role (widget_class, GTK_ACCESSIBLE_ROLE_GENERIC);
+ gtk_widget_class_set_css_name (widget_class, "clapper-gtk-title-label");
+}
diff --git a/src/lib/clapper-gtk/clapper-gtk-title-label.h b/src/lib/clapper-gtk/clapper-gtk-title-label.h
new file mode 100644
index 00000000..d5593a77
--- /dev/null
+++ b/src/lib/clapper-gtk/clapper-gtk-title-label.h
@@ -0,0 +1,50 @@
+/* Clapper GTK Integration Library
+ * Copyright (C) 2024 Rafał Dzięgiel
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+#pragma once
+
+#if !defined(__CLAPPER_GTK_INSIDE__) && !defined(CLAPPER_GTK_COMPILATION)
+#error "Only can be included directly."
+#endif
+
+#include
+#include
+#include
+#include
+
+G_BEGIN_DECLS
+
+#define CLAPPER_GTK_TYPE_TITLE_LABEL (clapper_gtk_title_label_get_type())
+#define CLAPPER_GTK_TITLE_LABEL_CAST(obj) ((ClapperGtkTitleLabel *)(obj))
+
+G_DECLARE_FINAL_TYPE (ClapperGtkTitleLabel, clapper_gtk_title_label, CLAPPER_GTK, TITLE_LABEL, GtkWidget)
+
+GtkWidget * clapper_gtk_title_label_new (void);
+
+void clapper_gtk_title_label_set_media_item (ClapperGtkTitleLabel *label, ClapperMediaItem *item);
+
+ClapperMediaItem * clapper_gtk_title_label_get_media_item (ClapperGtkTitleLabel *label);
+
+const gchar * clapper_gtk_title_label_get_current_title (ClapperGtkTitleLabel *label);
+
+void clapper_gtk_title_label_set_fallback_to_uri (ClapperGtkTitleLabel *label, gboolean enabled);
+
+gboolean clapper_gtk_title_label_get_fallback_to_uri (ClapperGtkTitleLabel *label);
+
+G_END_DECLS
diff --git a/src/lib/clapper-gtk/clapper-gtk-toggle-fullscreen-button.c b/src/lib/clapper-gtk/clapper-gtk-toggle-fullscreen-button.c
new file mode 100644
index 00000000..90cefc77
--- /dev/null
+++ b/src/lib/clapper-gtk/clapper-gtk-toggle-fullscreen-button.c
@@ -0,0 +1,154 @@
+/* Clapper GTK Integration Library
+ * Copyright (C) 2024 Rafał Dzięgiel
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+/**
+ * ClapperGtkToggleFullscreenButton:
+ *
+ * A #GtkButton for toggling fullscreen state.
+ */
+
+#include
+
+#include "clapper-gtk-toggle-fullscreen-button.h"
+#include "clapper-gtk-video.h"
+
+#define ENTER_FULLSCREEN_ICON_NAME "view-fullscreen-symbolic"
+#define LEAVE_FULLSCREEN_ICON_NAME "view-restore-symbolic"
+
+#define GST_CAT_DEFAULT clapper_gtk_toggle_fullscreen_button_debug
+GST_DEBUG_CATEGORY_STATIC (GST_CAT_DEFAULT);
+
+struct _ClapperGtkToggleFullscreenButton
+{
+ GtkButton parent;
+
+ gboolean is_fullscreen;
+};
+
+#define parent_class clapper_gtk_toggle_fullscreen_button_parent_class
+G_DEFINE_TYPE (ClapperGtkToggleFullscreenButton, clapper_gtk_toggle_fullscreen_button, GTK_TYPE_BUTTON)
+
+static void
+_toplevel_state_changed_cb (GdkToplevel *toplevel,
+ GParamSpec *pspec G_GNUC_UNUSED, ClapperGtkToggleFullscreenButton *self)
+{
+ GdkToplevelState state = gdk_toplevel_get_state (toplevel);
+ gboolean is_fullscreen = (state & GDK_TOPLEVEL_STATE_FULLSCREEN);
+
+ if (self->is_fullscreen == is_fullscreen)
+ return;
+
+ self->is_fullscreen = is_fullscreen;
+
+ GST_DEBUG_OBJECT (self, "Toplevel state changed, fullscreen: %s",
+ (self->is_fullscreen) ? "yes" : "no");
+
+ gtk_button_set_icon_name (GTK_BUTTON (self),
+ (!self->is_fullscreen) ? ENTER_FULLSCREEN_ICON_NAME : LEAVE_FULLSCREEN_ICON_NAME);
+}
+
+/**
+ * clapper_gtk_toggle_fullscreen_button_new:
+ *
+ * Creates a new #ClapperGtkToggleFullscreenButton instance.
+ *
+ * Returns: a new toggle fullscreen button #GtkWidget.
+ */
+GtkWidget *
+clapper_gtk_toggle_fullscreen_button_new (void)
+{
+ return g_object_new (CLAPPER_GTK_TYPE_TOGGLE_FULLSCREEN_BUTTON, NULL);
+}
+
+static void
+clapper_gtk_toggle_fullscreen_button_init (ClapperGtkToggleFullscreenButton *self)
+{
+ gtk_button_set_icon_name (GTK_BUTTON (self), ENTER_FULLSCREEN_ICON_NAME);
+}
+
+static void
+clapper_gtk_toggle_fullscreen_button_map (GtkWidget *widget)
+{
+ ClapperGtkToggleFullscreenButton *self = CLAPPER_GTK_TOGGLE_FULLSCREEN_BUTTON_CAST (widget);
+ GtkRoot *root;
+ GdkSurface *surface;
+
+ GST_TRACE_OBJECT (self, "Map");
+
+ root = gtk_widget_get_root (widget);
+ surface = gtk_native_get_surface (GTK_NATIVE (root));
+
+ if (G_LIKELY (GDK_IS_TOPLEVEL (surface))) {
+ GdkToplevel *toplevel = GDK_TOPLEVEL (surface);
+
+ g_signal_connect (toplevel, "notify::state",
+ G_CALLBACK (_toplevel_state_changed_cb), self);
+ _toplevel_state_changed_cb (toplevel, NULL, self);
+ }
+
+ GTK_WIDGET_CLASS (parent_class)->map (widget);
+}
+
+static void
+clapper_gtk_toggle_fullscreen_button_unmap (GtkWidget *widget)
+{
+ ClapperGtkToggleFullscreenButton *self = CLAPPER_GTK_TOGGLE_FULLSCREEN_BUTTON_CAST (widget);
+ GtkRoot *root;
+ GdkSurface *surface;
+
+ GST_TRACE_OBJECT (self, "Unmap");
+
+ root = gtk_widget_get_root (widget);
+ surface = gtk_native_get_surface (GTK_NATIVE (root));
+
+ if (G_LIKELY (GDK_IS_TOPLEVEL (surface))) {
+ GdkToplevel *toplevel = GDK_TOPLEVEL (surface);
+
+ g_signal_handlers_disconnect_by_func (toplevel,
+ _toplevel_state_changed_cb, self);
+ }
+
+ GTK_WIDGET_CLASS (parent_class)->unmap (widget);
+}
+
+static void
+clapper_gtk_toggle_fullscreen_button_clicked (GtkButton* button)
+{
+ GtkWidget *video;
+
+ GST_DEBUG_OBJECT (button, "Clicked");
+
+ if ((video = gtk_widget_get_ancestor (GTK_WIDGET (button), CLAPPER_GTK_TYPE_VIDEO)))
+ g_signal_emit_by_name (video, "toggle-fullscreen");
+}
+
+static void
+clapper_gtk_toggle_fullscreen_button_class_init (ClapperGtkToggleFullscreenButtonClass *klass)
+{
+ GtkWidgetClass *widget_class = (GtkWidgetClass *) klass;
+ GtkButtonClass *button_class = (GtkButtonClass *) klass;
+
+ GST_DEBUG_CATEGORY_INIT (GST_CAT_DEFAULT, "clappergtktogglefullscreenbutton", 0,
+ "Clapper GTK Toggle Fullscreen Button");
+
+ widget_class->map = clapper_gtk_toggle_fullscreen_button_map;
+ widget_class->unmap = clapper_gtk_toggle_fullscreen_button_unmap;
+
+ button_class->clicked = clapper_gtk_toggle_fullscreen_button_clicked;
+}
diff --git a/src/lib/clapper-gtk/clapper-gtk-toggle-fullscreen-button.h b/src/lib/clapper-gtk/clapper-gtk-toggle-fullscreen-button.h
new file mode 100644
index 00000000..d3f51083
--- /dev/null
+++ b/src/lib/clapper-gtk/clapper-gtk-toggle-fullscreen-button.h
@@ -0,0 +1,39 @@
+/* Clapper GTK Integration Library
+ * Copyright (C) 2024 Rafał Dzięgiel
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+#pragma once
+
+#if !defined(__CLAPPER_GTK_INSIDE__) && !defined(CLAPPER_GTK_COMPILATION)
+#error "Only can be included directly."
+#endif
+
+#include
+#include
+#include
+
+G_BEGIN_DECLS
+
+#define CLAPPER_GTK_TYPE_TOGGLE_FULLSCREEN_BUTTON (clapper_gtk_toggle_fullscreen_button_get_type())
+#define CLAPPER_GTK_TOGGLE_FULLSCREEN_BUTTON_CAST(obj) ((ClapperGtkToggleFullscreenButton *)(obj))
+
+G_DECLARE_FINAL_TYPE (ClapperGtkToggleFullscreenButton, clapper_gtk_toggle_fullscreen_button, CLAPPER_GTK, TOGGLE_FULLSCREEN_BUTTON, GtkButton)
+
+GtkWidget * clapper_gtk_toggle_fullscreen_button_new (void);
+
+G_END_DECLS
diff --git a/src/lib/clapper-gtk/clapper-gtk-toggle-play-button.c b/src/lib/clapper-gtk/clapper-gtk-toggle-play-button.c
new file mode 100644
index 00000000..376244d7
--- /dev/null
+++ b/src/lib/clapper-gtk/clapper-gtk-toggle-play-button.c
@@ -0,0 +1,126 @@
+/* Clapper GTK Integration Library
+ * Copyright (C) 2024 Rafał Dzięgiel
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+/**
+ * ClapperGtkTogglePlayButton:
+ *
+ * A #GtkButton for toggling play/pause of playback.
+ */
+
+#include
+
+#include "clapper-gtk-toggle-play-button.h"
+#include "clapper-gtk-utils.h"
+
+#define PLAY_ICON_NAME "media-playback-start-symbolic"
+#define PAUSE_ICON_NAME "media-playback-pause-symbolic"
+
+#define GST_CAT_DEFAULT clapper_gtk_toggle_play_button_debug
+GST_DEBUG_CATEGORY_STATIC (GST_CAT_DEFAULT);
+
+struct _ClapperGtkTogglePlayButton
+{
+ GtkButton parent;
+
+ GBinding *state_binding;
+};
+
+#define parent_class clapper_gtk_toggle_play_button_parent_class
+G_DEFINE_TYPE (ClapperGtkTogglePlayButton, clapper_gtk_toggle_play_button, GTK_TYPE_BUTTON)
+
+static gboolean
+_transform_state_func (GBinding *binding, const GValue *from_value,
+ GValue *to_value, ClapperGtkTogglePlayButton *self)
+{
+ ClapperPlayerState state = g_value_get_enum (from_value);
+
+ GST_DEBUG_OBJECT (self, "Reflecting player state change, now: %i", state);
+
+ switch (state) {
+ case CLAPPER_PLAYER_STATE_STOPPED:
+ case CLAPPER_PLAYER_STATE_PAUSED:
+ g_value_set_string (to_value, PLAY_ICON_NAME);
+ break;
+ case CLAPPER_PLAYER_STATE_PLAYING:
+ g_value_set_string (to_value, PAUSE_ICON_NAME);
+ break;
+ default:
+ return FALSE; // no change
+ }
+
+ return TRUE;
+}
+
+/**
+ * clapper_gtk_toggle_play_button_new:
+ *
+ * Creates a new #ClapperGtkTogglePlayButton instance.
+ *
+ * Returns: a new toggle play button #GtkWidget.
+ */
+GtkWidget *
+clapper_gtk_toggle_play_button_new (void)
+{
+ return g_object_new (CLAPPER_GTK_TYPE_TOGGLE_PLAY_BUTTON, NULL);
+}
+
+static void
+clapper_gtk_toggle_play_button_init (ClapperGtkTogglePlayButton *self)
+{
+ gtk_button_set_icon_name (GTK_BUTTON (self), PLAY_ICON_NAME);
+ gtk_actionable_set_action_name (GTK_ACTIONABLE (self), "video.toggle-play");
+}
+
+static void
+clapper_gtk_toggle_play_button_map (GtkWidget *widget)
+{
+ ClapperGtkTogglePlayButton *self = CLAPPER_GTK_TOGGLE_PLAY_BUTTON_CAST (widget);
+ ClapperPlayer *player;
+
+ if ((player = clapper_gtk_get_player_from_ancestor (widget))) {
+ self->state_binding = g_object_bind_property_full (player, "state",
+ self, "icon-name", G_BINDING_SYNC_CREATE,
+ (GBindingTransformFunc) _transform_state_func,
+ NULL, self, NULL);
+ }
+
+ GTK_WIDGET_CLASS (parent_class)->map (widget);
+}
+
+static void
+clapper_gtk_toggle_play_button_unmap (GtkWidget *widget)
+{
+ ClapperGtkTogglePlayButton *self = CLAPPER_GTK_TOGGLE_PLAY_BUTTON_CAST (widget);
+
+ g_clear_pointer (&self->state_binding, g_binding_unbind);
+
+ GTK_WIDGET_CLASS (parent_class)->unmap (widget);
+}
+
+static void
+clapper_gtk_toggle_play_button_class_init (ClapperGtkTogglePlayButtonClass *klass)
+{
+ GtkWidgetClass *widget_class = (GtkWidgetClass *) klass;
+
+ GST_DEBUG_CATEGORY_INIT (GST_CAT_DEFAULT, "clappergtktoggleplaybutton", 0,
+ "Clapper GTK Toggle Play Button");
+
+ widget_class->map = clapper_gtk_toggle_play_button_map;
+ widget_class->unmap = clapper_gtk_toggle_play_button_unmap;
+}
diff --git a/src/lib/clapper-gtk/clapper-gtk-toggle-play-button.h b/src/lib/clapper-gtk/clapper-gtk-toggle-play-button.h
new file mode 100644
index 00000000..1eec02e0
--- /dev/null
+++ b/src/lib/clapper-gtk/clapper-gtk-toggle-play-button.h
@@ -0,0 +1,39 @@
+/* Clapper GTK Integration Library
+ * Copyright (C) 2024 Rafał Dzięgiel
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+#pragma once
+
+#if !defined(__CLAPPER_GTK_INSIDE__) && !defined(CLAPPER_GTK_COMPILATION)
+#error "Only can be included directly."
+#endif
+
+#include
+#include
+#include
+
+G_BEGIN_DECLS
+
+#define CLAPPER_GTK_TYPE_TOGGLE_PLAY_BUTTON (clapper_gtk_toggle_play_button_get_type())
+#define CLAPPER_GTK_TOGGLE_PLAY_BUTTON_CAST(obj) ((ClapperGtkTogglePlayButton *)(obj))
+
+G_DECLARE_FINAL_TYPE (ClapperGtkTogglePlayButton, clapper_gtk_toggle_play_button, CLAPPER_GTK, TOGGLE_PLAY_BUTTON, GtkButton)
+
+GtkWidget * clapper_gtk_toggle_play_button_new (void);
+
+G_END_DECLS
diff --git a/src/lib/clapper-gtk/clapper-gtk-utils-private.h b/src/lib/clapper-gtk/clapper-gtk-utils-private.h
new file mode 100644
index 00000000..ca4be335
--- /dev/null
+++ b/src/lib/clapper-gtk/clapper-gtk-utils-private.h
@@ -0,0 +1,35 @@
+/* Clapper GTK Integration Library
+ * Copyright (C) 2024 Rafał Dzięgiel
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+#pragma once
+
+#include "clapper-gtk-utils.h"
+
+G_BEGIN_DECLS
+
+G_GNUC_INTERNAL
+void clapper_gtk_init_translations (void);
+
+G_GNUC_INTERNAL
+const gchar * clapper_gtk_get_icon_name_for_volume (gfloat volume);
+
+G_GNUC_INTERNAL
+const gchar * clapper_gtk_get_icon_name_for_speed (gfloat speed);
+
+G_END_DECLS
diff --git a/src/lib/clapper-gtk/clapper-gtk-utils.c b/src/lib/clapper-gtk/clapper-gtk-utils.c
new file mode 100644
index 00000000..bc0f3364
--- /dev/null
+++ b/src/lib/clapper-gtk/clapper-gtk-utils.c
@@ -0,0 +1,96 @@
+/* Clapper GTK Integration Library
+ * Copyright (C) 2024 Rafał Dzięgiel
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+#include "config.h"
+
+#include
+
+#include "clapper-gtk-utils-private.h"
+#include "clapper-gtk-video.h"
+
+static gboolean initialized = FALSE;
+
+/**
+ * clapper_gtk_get_player_from_ancestor:
+ * @widget: a #GtkWidget
+ *
+ * Get [class@Clapper.Player] used by [class@ClapperGtk.Video] ancestor of @widget.
+ *
+ * This utility is a convenience wrapper for calling [method@Gtk.Widget.get_ancestor]
+ * of type `CLAPPER_GTK_TYPE_VIDEO` and [method@ClapperGtk.Video.get_player] with
+ * additional %NULL checking and type casting.
+ *
+ * This is meant to be used mainly for custom widget development as an easy access to the
+ * underlying parent [class@Clapper.Player] object. If you want to get the player from
+ * [class@ClapperGtk.Video] widget itself, use [method@ClapperGtk.Video.get_player] instead.
+ *
+ * Rememeber that this function will return %NULL when widget does not have
+ * a [class@ClapperGtk.Video] ancestor in widget hierarchy (widget is not yet placed).
+ *
+ * Returns: (transfer none) (nullable): a #ClapperPlayer from ancestor of a @widget.
+ */
+ClapperPlayer *
+clapper_gtk_get_player_from_ancestor (GtkWidget *widget)
+{
+ GtkWidget *parent;
+ ClapperPlayer *player = NULL;
+
+ g_return_val_if_fail (GTK_IS_WIDGET (widget), NULL);
+
+ if ((parent = gtk_widget_get_ancestor (widget, CLAPPER_GTK_TYPE_VIDEO)))
+ player = clapper_gtk_video_get_player (CLAPPER_GTK_VIDEO_CAST (parent));
+
+ return player;
+}
+
+void
+clapper_gtk_init_translations (void)
+{
+ if (initialized)
+ return;
+
+ bindtextdomain (GETTEXT_PACKAGE, LOCALEDIR);
+ bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");
+
+ initialized = TRUE;
+}
+
+const gchar *
+clapper_gtk_get_icon_name_for_volume (gfloat volume)
+{
+ return (volume <= 0.0f)
+ ? "audio-volume-muted-symbolic"
+ : (volume <= 0.3f)
+ ? "audio-volume-low-symbolic"
+ : (volume <= 0.7f)
+ ? "audio-volume-medium-symbolic"
+ : (volume <= 1.0f)
+ ? "audio-volume-high-symbolic"
+ : "audio-volume-overamplified-symbolic";
+}
+
+const gchar *
+clapper_gtk_get_icon_name_for_speed (gfloat speed)
+{
+ return (speed < 1.0f)
+ ? "power-profile-power-saver-symbolic"
+ : (speed == 1.0f)
+ ? "power-profile-balanced-symbolic"
+ : "power-profile-performance-symbolic";
+}
diff --git a/src/lib/clapper-gtk/clapper-gtk-utils.h b/src/lib/clapper-gtk/clapper-gtk-utils.h
new file mode 100644
index 00000000..be5d54de
--- /dev/null
+++ b/src/lib/clapper-gtk/clapper-gtk-utils.h
@@ -0,0 +1,34 @@
+/* Clapper GTK Integration Library
+ * Copyright (C) 2024 Rafał Dzięgiel
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+#pragma once
+
+#if !defined(__CLAPPER_GTK_INSIDE__) && !defined(CLAPPER_GTK_COMPILATION)
+#error "Only can be included directly."
+#endif
+
+#include
+#include
+#include
+
+G_BEGIN_DECLS
+
+ClapperPlayer * clapper_gtk_get_player_from_ancestor (GtkWidget *widget);
+
+G_END_DECLS
diff --git a/src/lib/clapper-gtk/clapper-gtk-version.h.in b/src/lib/clapper-gtk/clapper-gtk-version.h.in
new file mode 100644
index 00000000..17145751
--- /dev/null
+++ b/src/lib/clapper-gtk/clapper-gtk-version.h.in
@@ -0,0 +1,76 @@
+/* Clapper GTK Integration Library
+ * Copyright (C) 2024 Rafał Dzięgiel
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+#pragma once
+
+#if !defined(__CLAPPER_GTK_INSIDE__) && !defined(CLAPPER_GTK_COMPILATION)
+#error "Only can be included directly."
+#endif
+
+/**
+ * CLAPPER_GTK_MAJOR_VERSION:
+ *
+ * ClapperGtk major version component
+ */
+#define CLAPPER_GTK_MAJOR_VERSION (@CLAPPER_GTK_MAJOR_VERSION@)
+
+/**
+ * CLAPPER_GTK_MINOR_VERSION:
+ *
+ * ClapperGtk minor version component
+ */
+#define CLAPPER_GTK_MINOR_VERSION (@CLAPPER_GTK_MINOR_VERSION@)
+
+/**
+ * CLAPPER_GTK_MICRO_VERSION:
+ *
+ * ClapperGtk micro version component
+ */
+#define CLAPPER_GTK_MICRO_VERSION (@CLAPPER_GTK_MICRO_VERSION@)
+
+/**
+ * CLAPPER_GTK_VERSION:
+ *
+ * ClapperGtk version
+ */
+#define CLAPPER_GTK_VERSION (@CLAPPER_GTK_VERSION@)
+
+/**
+ * CLAPPER_GTK_VERSION_S:
+ *
+ * ClapperGtk version, encoded as a string
+ */
+#define CLAPPER_GTK_VERSION_S "@CLAPPER_GTK_VERSION@"
+
+#define CLAPPER_GTK_ENCODE_VERSION(major,minor,micro) \
+ ((major) << 24 | (minor) << 16 | (micro) << 8)
+
+/**
+ * CLAPPER_GTK_VERSION_HEX:
+ *
+ * ClapperGtk version, encoded as an hexadecimal number, useful for integer comparisons.
+ */
+#define CLAPPER_GTK_VERSION_HEX \
+ (CLAPPER_GTK_ENCODE_VERSION (CLAPPER_GTK_MAJOR_VERSION, CLAPPER_GTK_MINOR_VERSION, CLAPPER_GTK_MICRO_VERSION))
+
+#define CLAPPER_GTK_CHECK_VERSION(major, minor, micro) \
+ (CLAPPER_GTK_MAJOR_VERSION > (major) || \
+ (CLAPPER_GTK_MAJOR_VERSION == (major) && CLAPPER_GTK_MINOR_VERSION > (minor)) || \
+ (CLAPPER_GTK_MAJOR_VERSION == (major) && CLAPPER_GTK_MINOR_VERSION == (minor) && \
+ CLAPPER_GTK_MICRO_VERSION >= (micro)))
diff --git a/src/lib/clapper-gtk/clapper-gtk-video.c b/src/lib/clapper-gtk/clapper-gtk-video.c
new file mode 100644
index 00000000..f65ca461
--- /dev/null
+++ b/src/lib/clapper-gtk/clapper-gtk-video.c
@@ -0,0 +1,1601 @@
+/* Clapper GTK Integration Library
+ * Copyright (C) 2024 Rafał Dzięgiel
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+/**
+ * ClapperGtkVideo:
+ *
+ * A ready to be used GTK video widget implementing Clapper API.
+ *
+ * #ClapperGtkVideo is the main widget exposed by `ClapperGtk` API. It both displays
+ * videos played by [class@Clapper.Player] (exposed as its property) and manages
+ * revealing and fading of any additional widgets overlaid on top of it.
+ *
+ * Other widgets provided by `ClapperGtk` library, once placed anywhere on video
+ * (including nesting within another widget like [class@Gtk.Box]) will automatically
+ * control #ClapperGtkVideo they were overlaid on top of. This allows to freely create
+ * custom playback control panels best suited for specific application. Additionally,
+ * pre-made widgets such as [class@ClapperGtk.SimpleControls] are also available.
+ *
+ * # Basic usage
+ *
+ * A typical use case is to embed video widget as part of your app where video playback
+ * is needed. Get the [class@Clapper.Player] belonging to the video widget and start adding
+ * new [class@Clapper.MediaItem] items to the [class@Clapper.Queue] for playback.
+ * For more information please refer to the Clapper playback library documentation.
+ *
+ * #ClapperGtkVideo can automatically take care of revealing and later fading overlaid
+ * content when interacting with the video. To do this, simply add your widgets with
+ * [method@ClapperGtk.Video.add_fading_overlay]. If you want to display some static content
+ * on top of video (or take care of visibility within overlaid widget itself) you can add
+ * it to the video as a normal overlay with [method@ClapperGtk.Video.add_overlay].
+ *
+ * # Actions
+ *
+ * #ClapperGtkVideo defines a set of built-in actions:
+ *
+ * ```yaml
+ * - "video.toggle-play": toggle play/pause
+ * - "video.play": start/resume playback
+ * - "video.pause": pause playback
+ * - "video.stop": stop playback
+ * - "video.seek": seek to position (variant "d")
+ * - "video.seek-custom": seek to position using seek method (variant "(di)")
+ * - "video.toggle-mute": toggle mute state
+ * - "video.set-mute": set mute state (variant "b")
+ * - "video.volume-up": increase volume by 2%
+ * - "video.volume-down": decrease volume by 2%
+ * - "video.set-volume": set volume to specified value (variant "d")
+ * - "video.speed-up": increase speed (from 0.05x - 2x range to nearest quarter)
+ * - "video.speed-down": decrease speed (from 0.05x - 2x range to nearest quarter)
+ * - "video.set-speed": set speed to specified value (variant "d")
+ * - "video.previous-item": select previous item in queue
+ * - "video.next-item": select next item in queue
+ * - "video.select-item": select item at specified index in queue (variant "u")
+ * ```
+ *
+ * # ClapperGtkVideo as GtkBuildable
+ *
+ * #ClapperGtkVideo implementation of the [iface@Gtk.Buildable] interface supports
+ * placing children as either normal overlay by specifying `overlay` or a fading
+ * one by specifying `fading-overlay` as the `type` attribute of a `` element.
+ * Position of overlaid content is determined by `valign/halign` properties.
+ *
+ * ```xml
+ *
+ * ```
+ */
+
+#include "config.h"
+
+#include
+
+#include "clapper-gtk-enums.h"
+#include "clapper-gtk-video.h"
+#include "clapper-gtk-lead-container.h"
+#include "clapper-gtk-status-private.h"
+#include "clapper-gtk-buffering-animation-private.h"
+
+#define PERCENTAGE_ROUND(a) (round ((gdouble) a / 0.01) * 0.01)
+
+#define DEFAULT_FADE_DELAY 3000
+#define DEFAULT_TOUCH_FADE_DELAY 5000
+#define DEFAULT_AUTO_INHIBIT FALSE
+
+#define MIN_MOTION_DELAY 100000
+
+#define GST_CAT_DEFAULT clapper_gtk_video_debug
+GST_DEBUG_CATEGORY_STATIC (GST_CAT_DEFAULT);
+
+struct _ClapperGtkVideo
+{
+ GtkWidget parent;
+
+ GtkWidget *overlay;
+ GtkWidget *status;
+ GtkWidget *buffering_animation;
+
+ GtkGesture *touch_gesture;
+ GtkGesture *click_gesture;
+
+ /* Props */
+ ClapperPlayer *player;
+ guint fade_delay;
+ guint touch_fade_delay;
+ gboolean auto_inhibit;
+
+ GPtrArray *overlays;
+ GPtrArray *fading_overlays;
+
+ gboolean buffering;
+ gboolean showing_status;
+
+ gulong notify_revealed_id;
+ guint fade_timeout;
+ gboolean reveal, revealed;
+
+ guint inhibit_cookie;
+
+ /* Current pointer coords and type */
+ gdouble x, y;
+ gboolean is_touch;
+ gboolean touching;
+ gint64 last_motion_time;
+ gboolean pending_toggle_play;
+};
+
+static void
+clapper_gtk_video_add_child (GtkBuildable *buildable,
+ GtkBuilder *builder, GObject *child, const char *type)
+{
+ if (GTK_IS_WIDGET (child)) {
+ if (g_strcmp0 (type, "overlay") == 0)
+ clapper_gtk_video_add_overlay (CLAPPER_GTK_VIDEO (buildable), GTK_WIDGET (child));
+ else if (g_strcmp0 (type, "fading-overlay") == 0)
+ clapper_gtk_video_add_fading_overlay (CLAPPER_GTK_VIDEO (buildable), GTK_WIDGET (child));
+ else
+ GTK_BUILDER_WARN_INVALID_CHILD_TYPE (buildable, type);
+ } else {
+ GtkBuildableIface *parent_iface = g_type_interface_peek_parent (GTK_BUILDABLE_GET_IFACE (buildable));
+ parent_iface->add_child (buildable, builder, child, type);
+ }
+}
+
+static void
+_buildable_iface_init (GtkBuildableIface *iface)
+{
+ iface->add_child = clapper_gtk_video_add_child;
+}
+
+#define parent_class clapper_gtk_video_parent_class
+G_DEFINE_TYPE_WITH_CODE (ClapperGtkVideo, clapper_gtk_video, GTK_TYPE_WIDGET,
+ G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE, _buildable_iface_init))
+
+enum
+{
+ PROP_0,
+ PROP_PLAYER,
+ PROP_FADE_DELAY,
+ PROP_TOUCH_FADE_DELAY,
+ PROP_AUTO_INHIBIT,
+ PROP_INHIBITED,
+ PROP_LAST
+};
+
+enum
+{
+ SIGNAL_TOGGLE_FULLSCREEN,
+ SIGNAL_SEEK_REQUEST,
+ SIGNAL_LAST
+};
+
+static gboolean provider_added = FALSE;
+static GParamSpec *param_specs[PROP_LAST] = { NULL, };
+static guint signals[SIGNAL_LAST] = { 0, };
+
+static void
+toggle_play_action_cb (GtkWidget *widget, const gchar *action_name, GVariant *parameter)
+{
+ ClapperGtkVideo *self = CLAPPER_GTK_VIDEO_CAST (widget);
+ ClapperPlayer *player = clapper_gtk_video_get_player (self);
+
+ switch (clapper_player_get_state (player)) {
+ case CLAPPER_PLAYER_STATE_PLAYING:
+ clapper_player_pause (player);
+ break;
+ case CLAPPER_PLAYER_STATE_STOPPED:
+ case CLAPPER_PLAYER_STATE_PAUSED:
+ clapper_player_play (player);
+ break;
+ default:
+ break;
+ }
+}
+
+static void
+play_action_cb (GtkWidget *widget, const gchar *action_name, GVariant *parameter)
+{
+ ClapperGtkVideo *self = CLAPPER_GTK_VIDEO_CAST (widget);
+ ClapperPlayer *player = clapper_gtk_video_get_player (self);
+
+ clapper_player_play (player);
+}
+
+static void
+pause_action_cb (GtkWidget *widget, const gchar *action_name, GVariant *parameter)
+{
+ ClapperGtkVideo *self = CLAPPER_GTK_VIDEO_CAST (widget);
+ ClapperPlayer *player = clapper_gtk_video_get_player (self);
+
+ clapper_player_pause (player);
+}
+
+static void
+stop_action_cb (GtkWidget *widget, const gchar *action_name, GVariant *parameter)
+{
+ ClapperGtkVideo *self = CLAPPER_GTK_VIDEO_CAST (widget);
+ ClapperPlayer *player = clapper_gtk_video_get_player (self);
+
+ clapper_player_stop (player);
+}
+
+static void
+seek_action_cb (GtkWidget *widget, const gchar *action_name, GVariant *parameter)
+{
+ ClapperGtkVideo *self = CLAPPER_GTK_VIDEO_CAST (widget);
+ ClapperPlayer *player = clapper_gtk_video_get_player (self);
+ gdouble position = g_variant_get_double (parameter);
+
+ clapper_player_seek (player, position);
+}
+
+static void
+seek_custom_action_cb (GtkWidget *widget, const gchar *action_name, GVariant *parameter)
+{
+ ClapperGtkVideo *self = CLAPPER_GTK_VIDEO_CAST (widget);
+ ClapperPlayer *player = clapper_gtk_video_get_player (self);
+ ClapperPlayerSeekMethod method = CLAPPER_PLAYER_SEEK_METHOD_NORMAL;
+ gdouble position = 0;
+
+ g_variant_get (parameter, "(di)", &position, &method);
+ clapper_player_seek_custom (player, position, method);
+}
+
+static void
+toggle_mute_action_cb (GtkWidget *widget, const gchar *action_name, GVariant *parameter)
+{
+ ClapperGtkVideo *self = CLAPPER_GTK_VIDEO_CAST (widget);
+ ClapperPlayer *player = clapper_gtk_video_get_player (self);
+
+ clapper_player_set_mute (player, !clapper_player_get_mute (player));
+}
+
+static void
+set_mute_action_cb (GtkWidget *widget, const gchar *action_name, GVariant *parameter)
+{
+ ClapperGtkVideo *self = CLAPPER_GTK_VIDEO_CAST (widget);
+ ClapperPlayer *player = clapper_gtk_video_get_player (self);
+ gboolean mute = g_variant_get_boolean (parameter);
+
+ clapper_player_set_mute (player, mute);
+}
+
+static void
+volume_up_action_cb (GtkWidget *widget, const gchar *action_name, GVariant *parameter)
+{
+ ClapperGtkVideo *self = CLAPPER_GTK_VIDEO_CAST (widget);
+ ClapperPlayer *player = clapper_gtk_video_get_player (self);
+ gdouble volume = (clapper_player_get_volume (player) + 0.02);
+
+ if (volume > 2.0)
+ volume = 2.0;
+
+ clapper_player_set_volume (player, PERCENTAGE_ROUND (volume));
+}
+
+static void
+volume_down_action_cb (GtkWidget *widget, const gchar *action_name, GVariant *parameter)
+{
+ ClapperGtkVideo *self = CLAPPER_GTK_VIDEO_CAST (widget);
+ ClapperPlayer *player = clapper_gtk_video_get_player (self);
+ gdouble volume = (clapper_player_get_volume (player) - 0.02);
+
+ if (volume < 0)
+ volume = 0;
+
+ clapper_player_set_volume (player, PERCENTAGE_ROUND (volume));
+}
+
+static void
+set_volume_action_cb (GtkWidget *widget, const gchar *action_name, GVariant *parameter)
+{
+ ClapperGtkVideo *self = CLAPPER_GTK_VIDEO_CAST (widget);
+ ClapperPlayer *player = clapper_gtk_video_get_player (self);
+ gdouble volume = g_variant_get_double (parameter);
+
+ clapper_player_set_volume (player, volume);
+}
+
+static void
+speed_up_action_cb (GtkWidget *widget, const gchar *action_name, GVariant *parameter)
+{
+ ClapperGtkVideo *self = CLAPPER_GTK_VIDEO_CAST (widget);
+ ClapperPlayer *player = clapper_gtk_video_get_player (self);
+ gdouble dest, speed = clapper_player_get_speed (player);
+
+ if (speed >= 2.0)
+ return;
+
+ dest = 0.25;
+ while (speed >= dest)
+ dest += 0.25;
+
+ if (dest > 2.0)
+ dest = 2.0;
+
+ clapper_player_set_speed (player, dest);
+}
+
+static void
+speed_down_action_cb (GtkWidget *widget, const gchar *action_name, GVariant *parameter)
+{
+ ClapperGtkVideo *self = CLAPPER_GTK_VIDEO_CAST (widget);
+ ClapperPlayer *player = clapper_gtk_video_get_player (self);
+ gdouble dest, speed = clapper_player_get_speed (player);
+
+ if (speed <= 0.05)
+ return;
+
+ dest = 2.0;
+ while (speed <= dest)
+ dest -= 0.25;
+
+ if (dest < 0.05)
+ dest = 0.05;
+
+ clapper_player_set_speed (player, dest);
+}
+
+static void
+set_speed_action_cb (GtkWidget *widget, const gchar *action_name, GVariant *parameter)
+{
+ ClapperGtkVideo *self = CLAPPER_GTK_VIDEO_CAST (widget);
+ ClapperPlayer *player = clapper_gtk_video_get_player (self);
+ gdouble speed = g_variant_get_double (parameter);
+
+ clapper_player_set_speed (player, speed);
+}
+
+static void
+previous_item_action_cb (GtkWidget *widget, const gchar *action_name, GVariant *parameter)
+{
+ ClapperGtkVideo *self = CLAPPER_GTK_VIDEO_CAST (widget);
+ ClapperPlayer *player = clapper_gtk_video_get_player (self);
+
+ clapper_queue_select_previous_item (clapper_player_get_queue (player));
+}
+
+static void
+next_item_action_cb (GtkWidget *widget, const gchar *action_name, GVariant *parameter)
+{
+ ClapperGtkVideo *self = CLAPPER_GTK_VIDEO_CAST (widget);
+ ClapperPlayer *player = clapper_gtk_video_get_player (self);
+
+ clapper_queue_select_next_item (clapper_player_get_queue (player));
+}
+
+static void
+select_item_action_cb (GtkWidget *widget, const gchar *action_name, GVariant *parameter)
+{
+ ClapperGtkVideo *self = CLAPPER_GTK_VIDEO_CAST (widget);
+ ClapperPlayer *player = clapper_gtk_video_get_player (self);
+ guint index = g_variant_get_uint32 (parameter);
+
+ clapper_queue_select_index (clapper_player_get_queue (player), index);
+}
+
+static void
+_set_reveal_fading_overlays (ClapperGtkVideo *self, gboolean reveal)
+{
+ GdkCursor *cursor = gdk_cursor_new_from_name ((reveal) ? "default" : "none", NULL);
+ guint i;
+
+ self->reveal = reveal;
+ GST_LOG_OBJECT (self, "%s requested", (self->reveal) ? "Reveal" : "Fade");
+
+ gtk_widget_set_cursor (GTK_WIDGET (self), cursor);
+ g_object_unref (cursor);
+
+ for (i = 0; i < self->fading_overlays->len; ++i) {
+ GtkRevealer *revealer = (GtkRevealer *) g_ptr_array_index (self->fading_overlays, i);
+
+ if (reveal)
+ gtk_widget_set_visible ((GtkWidget *) revealer, TRUE);
+
+ gtk_revealer_set_reveal_child (revealer, reveal);
+ }
+}
+
+static inline gboolean
+_is_on_leading_overlay (ClapperGtkVideo *self, ClapperGtkVideoActionMask blocked_action)
+{
+ GtkWidget *video = (GtkWidget *) self;
+ GtkWidget *tmp_widget = gtk_widget_pick (video, self->x, self->y, GTK_PICK_DEFAULT);
+ gboolean is_leading = FALSE;
+
+ GST_LOG_OBJECT (self, "Checking if is on leading overlay...");
+
+ while (tmp_widget && tmp_widget != video) {
+ if (CLAPPER_GTK_IS_LEAD_CONTAINER (tmp_widget)) {
+ ClapperGtkLeadContainer *lead_container = CLAPPER_GTK_LEAD_CONTAINER_CAST (tmp_widget);
+
+ if (clapper_gtk_lead_container_get_leading (lead_container)
+ && (clapper_gtk_lead_container_get_blocked_actions (lead_container) & blocked_action)) {
+ is_leading = TRUE;
+ break;
+ }
+ }
+ tmp_widget = gtk_widget_get_parent (tmp_widget);
+ }
+
+ GST_LOG_OBJECT (self, "Is on leading overlay: %s", (is_leading) ? "yes" : "no");
+
+ return is_leading;
+}
+
+static inline gboolean
+_determine_can_fade (ClapperGtkVideo *self)
+{
+ GtkWidget *video = (GtkWidget *) self;
+ GtkRoot *root;
+ GtkNative *native, *child_native;
+ GtkWidget *focus_child;
+ gboolean in_fading_overlay = FALSE;
+
+ GST_LOG_OBJECT (self, "Checking if overlays can fade...");
+
+ if (self->is_touch) {
+ if (self->touching) {
+ GST_LOG_OBJECT (self, "Cannot fade while interacting with touchscreen");
+ return FALSE;
+ }
+ } else if (self->x > 0 && self->y > 0) {
+ GtkWidget *tmp_widget = gtk_widget_pick (video, self->x, self->y, GTK_PICK_DEFAULT);
+ guint i;
+
+ if (!tmp_widget) {
+ GST_LOG_OBJECT (self, "Can fade, since no widget under pointer");
+ return TRUE;
+ }
+
+ for (i = 0; i < self->fading_overlays->len; ++i) {
+ GtkWidget *revealer = (GtkWidget *) g_ptr_array_index (self->fading_overlays, i);
+
+ if (tmp_widget == revealer || gtk_widget_is_ancestor (tmp_widget, revealer)) {
+ in_fading_overlay = TRUE;
+ break;
+ }
+ }
+
+ if (!in_fading_overlay) {
+ GST_LOG_OBJECT (self, "Can fade, since pointer not within fading overlay");
+ return TRUE;
+ }
+
+ while (tmp_widget && tmp_widget != video) {
+ GtkStateFlags state_flags = gtk_widget_get_state_flags (tmp_widget);
+
+ if (GTK_IS_ACTIONABLE (tmp_widget)
+ && (state_flags & (GTK_STATE_FLAG_PRELIGHT | GTK_STATE_FLAG_ACTIVE))) {
+ GST_LOG_OBJECT (self, "Cannot fade while on activatable widget");
+ return FALSE;
+ }
+ if ((state_flags & GTK_STATE_FLAG_DROP_ACTIVE)) {
+ GST_LOG_OBJECT (self, "Cannot fade on drop-active widget");
+ return FALSE;
+ }
+ if (GTK_IS_ACCESSIBLE (tmp_widget) && gtk_widget_get_can_target (tmp_widget)) {
+ GtkAccessibleRole role = gtk_accessible_get_accessible_role ((GtkAccessible *) tmp_widget);
+
+ switch (role) {
+ case GTK_ACCESSIBLE_ROLE_LIST:
+ GST_LOG_OBJECT (self, "Cannot fade while browsing list");
+ return FALSE;
+ case GTK_ACCESSIBLE_ROLE_SLIDER:
+ case GTK_ACCESSIBLE_ROLE_SCROLLBAR:
+ GST_LOG_OBJECT (self, "Cannot fade while on slider/scrollbar");
+ return FALSE;
+ default:
+ break;
+ }
+ }
+
+ tmp_widget = gtk_widget_get_parent (tmp_widget);
+ };
+ }
+
+ root = gtk_widget_get_root (video);
+
+ if (G_UNLIKELY (root == NULL))
+ return FALSE;
+
+ focus_child = gtk_root_get_focus (root);
+
+ if (!focus_child
+ || !gtk_widget_has_focus (focus_child)
+ || !gtk_widget_is_ancestor (focus_child, video)) {
+ GST_LOG_OBJECT (self, "Can fade, since no focused child in video");
+ return TRUE;
+ }
+
+ native = gtk_widget_get_native (video);
+ child_native = gtk_widget_get_native (focus_child);
+
+ if (native != child_native) {
+ GST_LOG_OBJECT (self, "Cannot fade while another surface is open");
+ return FALSE;
+ }
+
+ GST_LOG_OBJECT (self, "Can fade");
+ return TRUE;
+}
+
+static void
+_fade_overlay_delay_cb (ClapperGtkVideo *self)
+{
+ GST_LOG_OBJECT (self, "Fade handler reached");
+ self->fade_timeout = 0;
+
+ if (self->reveal) {
+ gboolean can_fade = _determine_can_fade (self);
+
+ GST_DEBUG_OBJECT (self, "Can fade overlays: %s", (can_fade) ? "yes" : "no");
+
+ if (can_fade)
+ _set_reveal_fading_overlays (self, FALSE);
+ }
+}
+
+static void
+_reset_fade_timeout (ClapperGtkVideo *self)
+{
+ GST_TRACE_OBJECT (self, "Fade timeout reset");
+
+ g_clear_handle_id (&self->fade_timeout, g_source_remove);
+ self->fade_timeout = g_timeout_add_once (
+ (self->is_touch) ? self->touch_fade_delay : self->fade_delay,
+ (GSourceOnceFunc) _fade_overlay_delay_cb, self);
+}
+
+static void
+_window_is_active_cb (GtkWindow *window,
+ GParamSpec *pspec G_GNUC_UNUSED, ClapperGtkVideo *self)
+{
+ gboolean active = gtk_window_is_active (window);
+
+ GST_DEBUG_OBJECT (self, "Window is now %sactive",
+ (active) ? "" : "in");
+
+ if (!active) {
+ /* Needs to set when drag starts during touch,
+ * we do not get touch release then */
+ self->touching = FALSE;
+
+ /* Ensure our overlays will fade eventually */
+ if (self->revealed && !self->fade_timeout)
+ _reset_fade_timeout (self);
+ }
+}
+
+static void
+_handle_motion (ClapperGtkVideo *self, GtkEventController *controller, gdouble x, gdouble y)
+{
+ gint64 now;
+
+ /* Start with points comparison as its faster,
+ * otherwise we will check if threshold exceeded */
+ if (self->x == x && self->y == y)
+ return;
+
+ now = g_get_monotonic_time ();
+
+ /* We do not want to reset timeout too often
+ * (especially on high refresh rate screens). */
+ if (now - self->last_motion_time >= MIN_MOTION_DELAY) {
+ GdkDevice *device = gtk_event_controller_get_current_event_device (controller);
+ gboolean is_threshold = (ABS (self->x - x) > 1 || ABS (self->y - y) > 1);
+
+ self->x = x;
+ self->y = y;
+ self->is_touch = (device && gdk_device_get_source (device) == GDK_SOURCE_TOUCHSCREEN);
+
+ if (is_threshold) {
+ if (!self->reveal && !_is_on_leading_overlay (self, CLAPPER_GTK_VIDEO_ACTION_REVEAL_OVERLAYS))
+ _set_reveal_fading_overlays (self, TRUE);
+
+ /* Extend time until fade */
+ if (self->revealed)
+ _reset_fade_timeout (self);
+ }
+
+ self->last_motion_time = now;
+ }
+}
+
+static void
+_handle_motion_leave (ClapperGtkVideo *self)
+{
+ GST_LOG_OBJECT (self, "Motion leave");
+
+ /* On leave we only reset coords to let overlays fade,
+ * device is not expected to change here */
+ self->x = -1;
+ self->y = -1;
+
+ /* Ensure our overlays will fade eventually */
+ if (self->revealed && !self->fade_timeout)
+ _reset_fade_timeout (self);
+}
+
+static void
+motion_enter_cb (GtkEventControllerMotion *motion,
+ gdouble x, gdouble y, ClapperGtkVideo *self)
+{
+ GdkDevice *device = gtk_event_controller_get_current_event_device (GTK_EVENT_CONTROLLER (motion));
+
+ /* XXX: We do not update x/y coords here in order to not mislead us
+ * that we are not on non-fading overlay when another surface is open */
+
+ self->is_touch = (device && gdk_device_get_source (device) == GDK_SOURCE_TOUCHSCREEN);
+
+ /* Tap to reveal is handled elsewhere */
+ if (self->is_touch)
+ return;
+
+ if (!self->reveal && !_is_on_leading_overlay (self, CLAPPER_GTK_VIDEO_ACTION_REVEAL_OVERLAYS))
+ _set_reveal_fading_overlays (self, TRUE);
+
+ /* Extend time until fade */
+ if (self->revealed)
+ _reset_fade_timeout (self);
+}
+
+static void
+motion_cb (GtkEventControllerMotion *motion,
+ gdouble x, gdouble y, ClapperGtkVideo *self)
+{
+ _handle_motion (self, GTK_EVENT_CONTROLLER (motion), x, y);
+}
+
+static void
+motion_leave_cb (GtkEventControllerMotion *motion, ClapperGtkVideo *self)
+{
+ _handle_motion_leave (self);
+}
+
+static void
+drop_motion_cb (GtkDropControllerMotion *drop_motion,
+ gdouble x, gdouble y, ClapperGtkVideo *self)
+{
+ /* We do not actually support D&D here, just want to track
+ * drop motion events from it and reveal overlays as one
+ * or more widgets overlaid may support current drop */
+
+ _handle_motion (self, GTK_EVENT_CONTROLLER (drop_motion), x, y);
+}
+
+static void
+drop_motion_leave_cb (GtkDropControllerMotion *drop_motion, ClapperGtkVideo *self)
+{
+ _handle_motion_leave (self);
+}
+
+static void
+left_click_pressed_cb (GtkGestureClick *click, gint n_press,
+ gdouble x, gdouble y, ClapperGtkVideo *self)
+{
+ GdkDevice *device;
+
+ GST_LOG_OBJECT (self, "Left click pressed");
+
+ /* Need to always clear click timeout,
+ * so we will not pause after double click */
+ self->pending_toggle_play = FALSE;
+
+ device = gtk_gesture_get_device (GTK_GESTURE (click));
+
+ self->x = x;
+ self->y = y;
+ self->is_touch = (device && gdk_device_get_source (device) == GDK_SOURCE_TOUCHSCREEN);
+}
+
+static gboolean
+_touch_in_lr_area (ClapperGtkVideo *self, gboolean *forward)
+{
+ gint video_w = gtk_widget_get_width (GTK_WIDGET (self));
+ gdouble area_w = (video_w / 4.);
+ gboolean in_area;
+
+ if ((in_area = (self->x <= area_w))) {
+ if (forward)
+ *forward = FALSE;
+ } else if ((in_area = (self->x >= video_w - area_w))) {
+ if (forward)
+ *forward = TRUE;
+ }
+
+ if (in_area && forward)
+ *forward ^= (gtk_widget_get_default_direction () == GTK_TEXT_DIR_RTL);
+
+ GST_LOG_OBJECT (self, "Touch in area: %s (x: %.2lf, video_w: %i, area_w: %.0lf)",
+ (in_area) ? "yes" : "no", self->x, video_w, area_w);
+
+ return in_area;
+}
+
+static inline void
+_handle_single_click (ClapperGtkVideo *self, GtkGestureClick *click)
+{
+ GdkDevice *device = gtk_gesture_get_device (GTK_GESTURE (click));
+
+ /* FIXME: Try GstNavigation first and do below logic only when not handled
+ * by upstream elements (maybe use sequence claiming for that?) */
+
+ switch (gdk_device_get_source (device)) {
+ case GDK_SOURCE_TOUCHSCREEN:
+ /* First tap should only reveal overlays if fading/faded */
+ if (!self->reveal && !_is_on_leading_overlay (self, CLAPPER_GTK_VIDEO_ACTION_REVEAL_OVERLAYS)) {
+ _set_reveal_fading_overlays (self, TRUE);
+ gtk_gesture_set_state (GTK_GESTURE (click), GTK_EVENT_SEQUENCE_CLAIMED);
+ break;
+ }
+ G_GNUC_FALLTHROUGH;
+ default:
+ if (!_is_on_leading_overlay (self, CLAPPER_GTK_VIDEO_ACTION_TOGGLE_PLAY)) {
+ self->pending_toggle_play = TRUE;
+ gtk_gesture_set_state (GTK_GESTURE (click), GTK_EVENT_SEQUENCE_CLAIMED);
+ }
+ break;
+ }
+}
+
+static inline void
+_handle_double_click (ClapperGtkVideo *self, GtkGestureClick *click)
+{
+ gboolean handled = FALSE;
+
+ if (self->is_touch) {
+ gboolean forward = FALSE;
+
+ if (_touch_in_lr_area (self, &forward)
+ && !_is_on_leading_overlay (self, CLAPPER_GTK_VIDEO_ACTION_SEEK_REQUEST)
+ && g_signal_handler_find (self, G_SIGNAL_MATCH_ID,
+ signals[SIGNAL_SEEK_REQUEST], 0, NULL, NULL, NULL) != 0) {
+ g_signal_emit (self, signals[SIGNAL_SEEK_REQUEST], 0, forward);
+ handled = TRUE;
+ }
+ }
+
+ if (!handled) {
+ if ((handled = !_is_on_leading_overlay (self, CLAPPER_GTK_VIDEO_ACTION_TOGGLE_FULLSCREEN)))
+ g_signal_emit (self, signals[SIGNAL_TOGGLE_FULLSCREEN], 0);
+ }
+
+ if (handled)
+ gtk_gesture_set_state (GTK_GESTURE (click), GTK_EVENT_SEQUENCE_CLAIMED);
+}
+
+static inline void
+_handle_nth_click (ClapperGtkVideo *self, GtkGestureClick *click)
+{
+ gboolean forward = FALSE;
+
+ if (_touch_in_lr_area (self, &forward)
+ && !_is_on_leading_overlay (self, CLAPPER_GTK_VIDEO_ACTION_SEEK_REQUEST)) {
+ g_signal_emit (self, signals[SIGNAL_SEEK_REQUEST], 0, forward);
+ gtk_gesture_set_state (GTK_GESTURE (click), GTK_EVENT_SEQUENCE_CLAIMED);
+ }
+}
+
+static void
+left_click_released_cb (GtkGestureClick *click, gint n_press,
+ gdouble x, gdouble y, ClapperGtkVideo *self)
+{
+ GST_LOG_OBJECT (self, "Left click released");
+
+ if (self->x < 0 || self->y < 0) {
+ GST_LOG_OBJECT (self, "Ignoring click release outside of video");
+ return;
+ }
+
+ self->x = x;
+ self->y = y;
+
+ switch (n_press) {
+ case 1:
+ _handle_single_click (self, click);
+ break;
+ case 2:
+ _handle_double_click (self, click);
+ break;
+ default:
+ _handle_nth_click (self, click);
+ break;
+ }
+
+ /* Keep fading overlays revealed while clicking/tapping on video */
+ if (self->revealed)
+ _reset_fade_timeout (self);
+}
+
+static void
+left_click_stopped_cb (GtkGestureClick *click, ClapperGtkVideo *self)
+{
+ GST_LOG_OBJECT (self, "Left click stopped");
+
+ if (self->pending_toggle_play) {
+ toggle_play_action_cb (GTK_WIDGET (self), NULL, NULL);
+ self->pending_toggle_play = FALSE;
+ }
+}
+
+static void
+touch_pressed_cb (GtkGestureClick *click, gint n_press,
+ gdouble x, gdouble y, ClapperGtkVideo *self)
+{
+ GST_LOG_OBJECT (self, "Touch pressed");
+
+ self->is_touch = TRUE;
+ self->touching = TRUE;
+
+ if (self->revealed)
+ _reset_fade_timeout (self);
+}
+
+static void
+touch_released_cb (GtkGestureClick *click, gint n_press,
+ gdouble x, gdouble y, ClapperGtkVideo *self)
+{
+ GST_LOG_OBJECT (self, "Touch released");
+
+ self->touching = FALSE;
+
+ /* Ensure our overlays will fade eventually */
+ if (self->revealed)
+ _reset_fade_timeout (self);
+}
+
+static void
+_ensure_css_provider (void)
+{
+ GdkDisplay *display;
+
+ if (provider_added)
+ return;
+
+ display = gdk_display_get_default ();
+
+ if (G_LIKELY (display != NULL)) {
+ GtkCssProvider *provider = gtk_css_provider_new ();
+ gtk_css_provider_load_from_resource (provider,
+ CLAPPER_GTK_RESOURCE_PREFIX "/css/styles.css");
+
+ gtk_style_context_add_provider_for_display (display,
+ (GtkStyleProvider *) provider, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION - 1);
+ g_object_unref (provider);
+
+ provider_added = TRUE;
+ }
+}
+
+static inline void
+_set_inhibit_session (ClapperGtkVideo *self, gboolean inhibit)
+{
+ GtkRoot *root;
+ GApplication *app;
+ gboolean inhibited = (self->inhibit_cookie != 0);
+
+ if (inhibited == inhibit)
+ return;
+
+ GST_DEBUG_OBJECT (self, "Trying to %sinhibit session...", (inhibit) ? "" : "un");
+
+ root = gtk_widget_get_root (GTK_WIDGET (self));
+
+ if (!root && !GTK_IS_WINDOW (root)) {
+ GST_WARNING_OBJECT (self, "Cannot %sinhibit session "
+ "without root window", (inhibit) ? "" : "un");
+ return;
+ }
+
+ /* NOTE: Not using application from window prop,
+ * as it goes away early when unrooting */
+ app = g_application_get_default ();
+
+ if (!app && !GTK_IS_APPLICATION (app)) {
+ GST_WARNING_OBJECT (self, "Cannot %sinhibit session "
+ "without window application set", (inhibit) ? "" : "un");
+ return;
+ }
+
+ if (inhibited) {
+ gtk_application_uninhibit (GTK_APPLICATION (app), self->inhibit_cookie);
+ self->inhibit_cookie = 0;
+ }
+ if (inhibit) {
+ self->inhibit_cookie = gtk_application_inhibit (GTK_APPLICATION (app),
+ GTK_WINDOW (root), GTK_APPLICATION_INHIBIT_IDLE,
+ "Video is playing");
+ }
+
+ GST_DEBUG_OBJECT (self, "Session %sinhibited", (inhibit) ? "" : "un");
+ g_object_notify_by_pspec (G_OBJECT (self), param_specs[PROP_INHIBITED]);
+}
+
+static inline void
+_set_buffering_animation_enabled (ClapperGtkVideo *self, gboolean enabled)
+{
+ ClapperGtkBufferingAnimation *animation;
+
+ if (self->buffering == enabled)
+ return;
+
+ animation = CLAPPER_GTK_BUFFERING_ANIMATION_CAST (self->buffering_animation);
+ gtk_widget_set_visible (self->buffering_animation, enabled);
+
+ if (enabled)
+ clapper_gtk_buffering_animation_start (animation);
+ else
+ clapper_gtk_buffering_animation_stop (animation);
+
+ self->buffering = enabled;
+}
+
+static void
+_player_state_changed_cb (ClapperPlayer *player,
+ GParamSpec *pspec G_GNUC_UNUSED, ClapperGtkVideo *self)
+{
+ ClapperPlayerState state = clapper_player_get_state (player);
+
+ if (self->auto_inhibit)
+ _set_inhibit_session (self, state == CLAPPER_PLAYER_STATE_PLAYING);
+
+ _set_buffering_animation_enabled (self, state == CLAPPER_PLAYER_STATE_BUFFERING);
+}
+
+static void
+_video_sink_changed_cb (ClapperPlayer *player,
+ GParamSpec *pspec G_GNUC_UNUSED, ClapperGtkVideo *self)
+{
+ GstElement *vsink = clapper_player_get_video_sink (player);
+ GtkWidget *child = NULL;
+
+ GST_DEBUG_OBJECT (self, "Video sink changed");
+
+ if (vsink) {
+ GParamSpec *pspec;
+
+ if ((pspec = g_object_class_find_property (G_OBJECT_GET_CLASS (vsink), "widget"))
+ && pspec->value_type == GTK_TYPE_WIDGET) {
+ g_object_get (vsink, "widget", &child, NULL);
+ GST_DEBUG_OBJECT (self, "Video sink provides a widget");
+ } else if ((pspec = g_object_class_find_property (G_OBJECT_GET_CLASS (vsink), "paintable"))
+ && pspec->value_type == GDK_TYPE_PAINTABLE) {
+ GdkPaintable *paintable = NULL;
+
+ g_object_get (vsink, "paintable", &paintable, NULL);
+ GST_DEBUG_OBJECT (self, "Video sink provides a paintable");
+
+ child = gtk_picture_new ();
+ gtk_picture_set_paintable (GTK_PICTURE (child), paintable);
+
+ g_object_unref (paintable);
+ }
+
+ gst_object_unref (vsink);
+ }
+
+ if (!child) {
+ /* FIXME: Create some default widget to show */
+ child = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);
+
+ GST_DEBUG_OBJECT (self, "No widget from video sink, using placeholder");
+ }
+
+ gtk_overlay_set_child (GTK_OVERLAY (self->overlay), child);
+ GST_DEBUG_OBJECT (self, "Set new video widget");
+}
+
+static void
+_player_error_cb (ClapperPlayer *player, GError *error,
+ const gchar *debug_info, ClapperGtkVideo *self)
+{
+ /* FIXME: Handle authentication error (pop dialog to set credentials and retry) */
+
+ /* Buffering will not finish anymore if we were in middle of it */
+ _set_buffering_animation_enabled (self, FALSE);
+
+ if (!self->showing_status) {
+ clapper_gtk_status_set_error (CLAPPER_GTK_STATUS_CAST (self->status), error);
+ self->showing_status = TRUE;
+ }
+}
+
+static void
+_player_missing_plugin_cb (ClapperPlayer *player, const gchar *name,
+ const gchar *installer_detail, ClapperGtkVideo *self)
+{
+ /* XXX: Playbin2 seems to not emit state change here,
+ * so manually stop buffering animation just in case */
+ _set_buffering_animation_enabled (self, FALSE);
+
+ /* XXX: Some content can still be played partially (e.g. without audio),
+ * but it should be better to stop and notify user that something is missing */
+ clapper_player_stop (player);
+
+ /* We might get "missing-plugin" followed by "error" signal. This boolean prevents
+ * immediately overwriting status and lets user deal with problems in order. */
+ if (!self->showing_status) {
+ clapper_gtk_status_set_missing_plugin (CLAPPER_GTK_STATUS_CAST (self->status), name);
+ self->showing_status = TRUE;
+ }
+}
+
+static void
+_queue_current_item_changed_cb (ClapperQueue *queue,
+ GParamSpec *pspec G_GNUC_UNUSED, ClapperGtkVideo *self)
+{
+ clapper_gtk_status_clear (CLAPPER_GTK_STATUS_CAST (self->status));
+ self->showing_status = FALSE;
+}
+
+static void
+_fading_overlay_revealed_cb (GtkRevealer *revealer,
+ GParamSpec *pspec G_GNUC_UNUSED, ClapperGtkVideo *self)
+{
+ self->revealed = gtk_revealer_get_child_revealed (revealer);
+
+ /* Start fade timeout once fully revealed */
+ if (self->revealed)
+ _reset_fade_timeout (self);
+}
+
+/**
+ * clapper_gtk_video_new:
+ *
+ * Creates a new #ClapperGtkVideo instance.
+ *
+ * Returns: a new video #GtkWidget.
+ */
+GtkWidget *
+clapper_gtk_video_new (void)
+{
+ return g_object_new (CLAPPER_GTK_TYPE_VIDEO, NULL);
+}
+
+/**
+ * clapper_gtk_video_add_overlay:
+ * @video: a #ClapperGtkVideo
+ * @widget: a #GtkWidget
+ *
+ * Add another #GtkWidget to be overlaid on top of video.
+ *
+ * The position at which @widget is placed is determined from
+ * [property@Gtk.Widget:halign] and [property@Gtk.Widget:valign] properties.
+ *
+ * This function will overlay @widget as-is meaning that widget is responsible
+ * for managing its own visablity if needed. If you want to add a #GtkWidget
+ * that will reveal and fade itself automatically when interacting with @video
+ * (e.g. controls panel) you can use clapper_gtk_video_add_fading_overlay()
+ * function for convenience.
+ */
+void
+clapper_gtk_video_add_overlay (ClapperGtkVideo *self, GtkWidget *widget)
+{
+ g_return_if_fail (CLAPPER_GTK_IS_VIDEO (self));
+ g_return_if_fail (GTK_IS_WIDGET (widget));
+
+ g_ptr_array_add (self->overlays, widget);
+ gtk_overlay_add_overlay (GTK_OVERLAY (self->overlay), widget);
+}
+
+/**
+ * clapper_gtk_video_add_fading_overlay:
+ * @video: a #ClapperGtkVideo
+ * @widget: a #GtkWidget
+ *
+ * Similiar as clapper_gtk_video_add_overlay() but will also automatically
+ * add fading functionality to overlaid #GtkWidget for convenience. This will
+ * make widget reveal itself when interacting with @video and fade otherwise.
+ * Useful when placing widgets such as playback controls panels.
+ */
+void
+clapper_gtk_video_add_fading_overlay (ClapperGtkVideo *self, GtkWidget *widget)
+{
+ GtkWidget *revealer;
+
+ g_return_if_fail (CLAPPER_GTK_IS_VIDEO (self));
+ g_return_if_fail (GTK_IS_WIDGET (widget));
+
+ revealer = gtk_revealer_new ();
+
+ g_object_bind_property (revealer, "child-revealed", revealer, "visible", G_BINDING_DEFAULT);
+
+ g_object_bind_property (widget, "halign", revealer, "halign", G_BINDING_SYNC_CREATE);
+ g_object_bind_property (widget, "valign", revealer, "valign", G_BINDING_SYNC_CREATE);
+
+ /* Since we reveal/fade all at once, one signal connection is enough */
+ if (self->notify_revealed_id == 0) {
+ self->notify_revealed_id = g_signal_connect (revealer, "notify::child-revealed",
+ G_CALLBACK (_fading_overlay_revealed_cb), self);
+ }
+
+ gtk_widget_set_visible (revealer, self->reveal);
+ gtk_revealer_set_reveal_child (GTK_REVEALER (revealer), self->reveal);
+ gtk_revealer_set_transition_type (GTK_REVEALER (revealer), GTK_REVEALER_TRANSITION_TYPE_CROSSFADE);
+ gtk_revealer_set_transition_duration (GTK_REVEALER (revealer), 800);
+ gtk_revealer_set_child (GTK_REVEALER (revealer), widget);
+
+ g_ptr_array_add (self->fading_overlays, revealer);
+ gtk_overlay_add_overlay (GTK_OVERLAY (self->overlay), revealer);
+}
+
+/**
+ * clapper_gtk_video_get_player:
+ * @video: a #ClapperGtkVideo
+ *
+ * Get #ClapperPlayer used by this #ClapperGtkVideo instance.
+ *
+ * Returns: (transfer none): a #ClapperPlayer used by video.
+ */
+ClapperPlayer *
+clapper_gtk_video_get_player (ClapperGtkVideo *self)
+{
+ g_return_val_if_fail (CLAPPER_GTK_IS_VIDEO (self), NULL);
+
+ return self->player;
+}
+
+/**
+ * clapper_gtk_video_set_fade_delay:
+ * @video: a #ClapperGtkVideo
+ * @delay: a fade delay
+ *
+ * Set time in milliseconds after which fading overlays should fade.
+ */
+void
+clapper_gtk_video_set_fade_delay (ClapperGtkVideo *self, guint delay)
+{
+ g_return_if_fail (CLAPPER_GTK_IS_VIDEO (self));
+ g_return_if_fail (delay >= 1000);
+
+ self->fade_delay = delay;
+ g_object_notify_by_pspec (G_OBJECT (self), param_specs[PROP_FADE_DELAY]);
+}
+
+/**
+ * clapper_gtk_video_get_fade_delay:
+ * @video: a #ClapperGtkVideo
+ *
+ * Get time in milliseconds after which fading overlays should fade.
+ *
+ * Returns: currently set fade delay.
+ */
+guint
+clapper_gtk_video_get_fade_delay (ClapperGtkVideo *self)
+{
+ g_return_val_if_fail (CLAPPER_GTK_IS_VIDEO (self), 0);
+
+ return self->fade_delay;
+}
+
+/**
+ * clapper_gtk_video_set_touch_fade_delay:
+ * @video: a #ClapperGtkVideo
+ * @delay: a touch fade delay
+ *
+ * Set time in milliseconds after which fading overlays should fade
+ * when using touchscreen.
+ *
+ * It is often useful to set this higher then normal fade delay property,
+ * as in case of touch events user do not have a moving pointer that would
+ * extend fade timeout, so he can have more time to decide what to press next.
+ */
+void
+clapper_gtk_video_set_touch_fade_delay (ClapperGtkVideo *self, guint delay)
+{
+ g_return_if_fail (CLAPPER_GTK_IS_VIDEO (self));
+ g_return_if_fail (delay >= 1);
+
+ self->touch_fade_delay = delay;
+ g_object_notify_by_pspec (G_OBJECT (self), param_specs[PROP_TOUCH_FADE_DELAY]);
+}
+
+/**
+ * clapper_gtk_video_get_touch_fade_delay:
+ * @video: a #ClapperGtkVideo
+ *
+ * Get time in milliseconds after which fading overlays should fade
+ * when revealed using touch device.
+ *
+ * Returns: currently set touch fade delay.
+ */
+guint
+clapper_gtk_video_get_touch_fade_delay (ClapperGtkVideo *self)
+{
+ g_return_val_if_fail (CLAPPER_GTK_IS_VIDEO (self), 0);
+
+ return self->touch_fade_delay;
+}
+
+/**
+ * clapper_gtk_video_set_auto_inhibit:
+ * @video: a #ClapperGtkVideo
+ * @inhibit: whether to enable automatic session inhibit
+ *
+ * Set whether video should try to automatically inhibit session
+ * from idling (and possibly screen going black) when video is playing.
+ */
+void
+clapper_gtk_video_set_auto_inhibit (ClapperGtkVideo *self, gboolean inhibit)
+{
+ g_return_if_fail (CLAPPER_GTK_IS_VIDEO (self));
+
+ if (self->auto_inhibit != inhibit) {
+ self->auto_inhibit = inhibit;
+
+ /* Uninhibit if we were auto inhibited earlier */
+ if (!self->auto_inhibit)
+ _set_inhibit_session (self, FALSE);
+
+ g_object_notify_by_pspec (G_OBJECT (self), param_specs[PROP_AUTO_INHIBIT]);
+ }
+}
+
+/**
+ * clapper_gtk_video_get_auto_inhibit:
+ * @video: a #ClapperGtkVideo
+ *
+ * Get whether automatic session inhibit is enabled.
+ *
+ * Returns: %TRUE if enabled, %FALSE otherwise.
+ */
+gboolean
+clapper_gtk_video_get_auto_inhibit (ClapperGtkVideo *self)
+{
+ g_return_val_if_fail (CLAPPER_GTK_IS_VIDEO (self), FALSE);
+
+ return self->auto_inhibit;
+}
+
+/**
+ * clapper_gtk_video_get_inhibited:
+ * @video: a #ClapperGtkVideo
+ *
+ * Get whether session is currently inhibited by
+ * [property@ClapperGtk.Video:auto-inhibit].
+ *
+ * Returns: %TRUE if inhibited, %FALSE otherwise.
+ */
+gboolean
+clapper_gtk_video_get_inhibited (ClapperGtkVideo *self)
+{
+ g_return_val_if_fail (CLAPPER_GTK_IS_VIDEO (self), FALSE);
+
+ return (self->inhibit_cookie != 0);
+}
+
+static void
+clapper_gtk_video_root (GtkWidget *widget)
+{
+ ClapperGtkVideo *self = CLAPPER_GTK_VIDEO_CAST (widget);
+ GtkRoot *root;
+
+ _ensure_css_provider ();
+
+ GTK_WIDGET_CLASS (parent_class)->root (widget);
+
+ root = gtk_widget_get_root (widget);
+
+ if (root && GTK_IS_WINDOW (root)) {
+ GtkWindow *window = GTK_WINDOW (root);
+
+ g_signal_connect (window, "notify::is-active",
+ G_CALLBACK (_window_is_active_cb), self);
+ _window_is_active_cb (window, NULL, self);
+ }
+
+ if (self->auto_inhibit) {
+ ClapperPlayerState state = clapper_player_get_state (self->player);
+ _set_inhibit_session (self, state == CLAPPER_PLAYER_STATE_PLAYING);
+ }
+}
+
+static void
+clapper_gtk_video_unroot (GtkWidget *widget)
+{
+ ClapperGtkVideo *self = CLAPPER_GTK_VIDEO_CAST (widget);
+ GtkRoot *root = gtk_widget_get_root (widget);
+
+ if (root && GTK_IS_WINDOW (root)) {
+ g_signal_handlers_disconnect_by_func (GTK_WINDOW (root),
+ _window_is_active_cb, self);
+ }
+
+ _set_inhibit_session (self, FALSE);
+
+ GTK_WIDGET_CLASS (parent_class)->unroot (widget);
+}
+
+static void
+clapper_gtk_video_init (ClapperGtkVideo *self)
+{
+ self->overlay = gtk_overlay_new ();
+ gtk_widget_set_overflow (self->overlay, GTK_OVERFLOW_HIDDEN);
+ gtk_widget_set_parent (self->overlay, GTK_WIDGET (self));
+
+ self->overlays = g_ptr_array_new ();
+ self->fading_overlays = g_ptr_array_new ();
+
+ self->fade_delay = DEFAULT_FADE_DELAY;
+ self->touch_fade_delay = DEFAULT_TOUCH_FADE_DELAY;
+ self->auto_inhibit = DEFAULT_AUTO_INHIBIT;
+
+ gtk_widget_init_template (GTK_WIDGET (self));
+
+ gtk_gesture_group (self->touch_gesture, self->click_gesture);
+}
+
+static void
+clapper_gtk_video_constructed (GObject *object)
+{
+ ClapperGtkVideo *self = CLAPPER_GTK_VIDEO_CAST (object);
+ GstElement *vsink = gst_element_factory_make ("clappersink", NULL);
+ ClapperQueue *queue;
+
+ self->player = clapper_player_new ();
+ queue = clapper_player_get_queue (self->player);
+
+ g_signal_connect (self->player, "notify::state",
+ G_CALLBACK (_player_state_changed_cb), self);
+ g_signal_connect (self->player, "notify::video-sink",
+ G_CALLBACK (_video_sink_changed_cb), self);
+
+ clapper_player_set_video_sink (self->player, vsink);
+
+ g_signal_connect (self->player, "error",
+ G_CALLBACK (_player_error_cb), self);
+ g_signal_connect (self->player, "missing-plugin",
+ G_CALLBACK (_player_missing_plugin_cb), self);
+
+ g_signal_connect (queue, "notify::current-item",
+ G_CALLBACK (_queue_current_item_changed_cb), self);
+
+ G_OBJECT_CLASS (parent_class)->constructed (object);
+}
+
+static void
+clapper_gtk_video_dispose (GObject *object)
+{
+ ClapperGtkVideo *self = CLAPPER_GTK_VIDEO_CAST (object);
+
+ if (self->notify_revealed_id != 0) {
+ GtkRevealer *revealer = GTK_REVEALER (g_ptr_array_index (self->fading_overlays, 0));
+
+ g_signal_handler_disconnect (revealer, self->notify_revealed_id);
+ self->notify_revealed_id = 0;
+ }
+
+ g_clear_handle_id (&self->fade_timeout, g_source_remove);
+
+ gtk_widget_dispose_template (GTK_WIDGET (object), CLAPPER_GTK_TYPE_VIDEO);
+
+ g_clear_pointer (&self->overlay, gtk_widget_unparent);
+ gst_clear_object (&self->player);
+
+ G_OBJECT_CLASS (parent_class)->dispose (object);
+}
+
+static void
+clapper_gtk_video_finalize (GObject *object)
+{
+ ClapperGtkVideo *self = CLAPPER_GTK_VIDEO_CAST (object);
+
+ g_ptr_array_unref (self->overlays);
+ g_ptr_array_unref (self->fading_overlays);
+
+ G_OBJECT_CLASS (parent_class)->finalize (object);
+}
+
+static void
+clapper_gtk_video_get_property (GObject *object, guint prop_id,
+ GValue *value, GParamSpec *pspec)
+{
+ ClapperGtkVideo *self = CLAPPER_GTK_VIDEO_CAST (object);
+
+ switch (prop_id) {
+ case PROP_PLAYER:
+ g_value_set_object (value, clapper_gtk_video_get_player (self));
+ break;
+ case PROP_FADE_DELAY:
+ g_value_set_uint (value, clapper_gtk_video_get_fade_delay (self));
+ break;
+ case PROP_TOUCH_FADE_DELAY:
+ g_value_set_uint (value, clapper_gtk_video_get_touch_fade_delay (self));
+ break;
+ case PROP_AUTO_INHIBIT:
+ g_value_set_boolean (value, clapper_gtk_video_get_auto_inhibit (self));
+ break;
+ case PROP_INHIBITED:
+ g_value_set_boolean (value, clapper_gtk_video_get_inhibited (self));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+clapper_gtk_video_set_property (GObject *object, guint prop_id,
+ const GValue *value, GParamSpec *pspec)
+{
+ ClapperGtkVideo *self = CLAPPER_GTK_VIDEO_CAST (object);
+
+ switch (prop_id) {
+ case PROP_FADE_DELAY:
+ clapper_gtk_video_set_fade_delay (self, g_value_get_uint (value));
+ break;
+ case PROP_TOUCH_FADE_DELAY:
+ clapper_gtk_video_set_touch_fade_delay (self, g_value_get_uint (value));
+ break;
+ case PROP_AUTO_INHIBIT:
+ clapper_gtk_video_set_auto_inhibit (self, g_value_get_boolean (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+clapper_gtk_video_class_init (ClapperGtkVideoClass *klass)
+{
+ GObjectClass *gobject_class = (GObjectClass *) klass;
+ GtkWidgetClass *widget_class = (GtkWidgetClass *) klass;
+
+ GST_DEBUG_CATEGORY_INIT (GST_CAT_DEFAULT, "clappergtkvideo", GST_DEBUG_FG_MAGENTA,
+ "Clapper GTK Video");
+
+ widget_class->root = clapper_gtk_video_root;
+ widget_class->unroot = clapper_gtk_video_unroot;
+
+ gobject_class->constructed = clapper_gtk_video_constructed;
+ gobject_class->get_property = clapper_gtk_video_get_property;
+ gobject_class->set_property = clapper_gtk_video_set_property;
+ gobject_class->dispose = clapper_gtk_video_dispose;
+ gobject_class->finalize = clapper_gtk_video_finalize;
+
+ /**
+ * ClapperGtkVideo:player:
+ *
+ * A #ClapperPlayer used by video.
+ */
+ param_specs[PROP_PLAYER] = g_param_spec_object ("player",
+ NULL, NULL, CLAPPER_TYPE_PLAYER,
+ G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * ClapperGtkVideo:fade-delay:
+ *
+ * A delay in milliseconds before trying to fade all fading overlays.
+ */
+ param_specs[PROP_FADE_DELAY] = g_param_spec_uint ("fade-delay",
+ NULL, NULL, 1, G_MAXUINT, DEFAULT_FADE_DELAY,
+ G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * ClapperGtkVideo:touch-fade-delay:
+ *
+ * A delay in milliseconds before trying to fade all fading overlays
+ * after revealed using touchscreen.
+ */
+ param_specs[PROP_TOUCH_FADE_DELAY] = g_param_spec_uint ("touch-fade-delay",
+ NULL, NULL, 1, G_MAXUINT, DEFAULT_TOUCH_FADE_DELAY,
+ G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * ClapperGtkVideo:auto-inhibit:
+ *
+ * Try to automatically inhibit session when video is playing.
+ */
+ param_specs[PROP_AUTO_INHIBIT] = g_param_spec_boolean ("auto-inhibit",
+ NULL, NULL, DEFAULT_AUTO_INHIBIT,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * ClapperGtkVideo:inhibited:
+ *
+ * Get whether session is currently inhibited by the video.
+ */
+ param_specs[PROP_INHIBITED] = g_param_spec_boolean ("inhibited",
+ NULL, NULL, FALSE,
+ G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * ClapperGtkVideo::toggle-fullscreen:
+ * @video: a #ClapperGtkVideo
+ *
+ * A signal that user requested a change in fullscreen state of the video.
+ *
+ * Note that when going fullscreen from this signal, user will expect
+ * for only video to be fullscreened and not the whole app window.
+ * It is up to implementation to decide how to handle that.
+ */
+ signals[SIGNAL_TOGGLE_FULLSCREEN] = g_signal_new ("toggle-fullscreen",
+ G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST | G_SIGNAL_NO_RECURSE | G_SIGNAL_NO_HOOKS,
+ 0, NULL, NULL, NULL, G_TYPE_NONE, 0);
+
+ /**
+ * ClapperGtkVideo::seek-request:
+ * @video: a #ClapperGtkVideo
+ * @forward: %TRUE if seek should be forward, %FALSE if backward
+ *
+ * A helper signal for implementing common seeking by double tap
+ * on screen side for touchscreen devices.
+ *
+ * Note that @forward already takes into account RTL direction,
+ * so implementation does not have to check.
+ */
+ signals[SIGNAL_SEEK_REQUEST] = g_signal_new ("seek-request",
+ G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST | G_SIGNAL_NO_RECURSE | G_SIGNAL_NO_HOOKS,
+ 0, NULL, NULL, NULL, G_TYPE_NONE, 1, G_TYPE_BOOLEAN);
+
+ g_object_class_install_properties (gobject_class, PROP_LAST, param_specs);
+
+ gtk_widget_class_install_action (widget_class, "video.toggle-play", NULL, toggle_play_action_cb);
+ gtk_widget_class_install_action (widget_class, "video.play", NULL, play_action_cb);
+ gtk_widget_class_install_action (widget_class, "video.pause", NULL, pause_action_cb);
+ gtk_widget_class_install_action (widget_class, "video.stop", NULL, stop_action_cb);
+ gtk_widget_class_install_action (widget_class, "video.seek", "d", seek_action_cb);
+ gtk_widget_class_install_action (widget_class, "video.seek-custom", "(di)", seek_custom_action_cb);
+ gtk_widget_class_install_action (widget_class, "video.toggle-mute", NULL, toggle_mute_action_cb);
+ gtk_widget_class_install_action (widget_class, "video.set-mute", "b", set_mute_action_cb);
+ gtk_widget_class_install_action (widget_class, "video.volume-up", NULL, volume_up_action_cb);
+ gtk_widget_class_install_action (widget_class, "video.volume-down", NULL, volume_down_action_cb);
+ gtk_widget_class_install_action (widget_class, "video.set-volume", "d", set_volume_action_cb);
+ gtk_widget_class_install_action (widget_class, "video.speed-up", NULL, speed_up_action_cb);
+ gtk_widget_class_install_action (widget_class, "video.speed-down", NULL, speed_down_action_cb);
+ gtk_widget_class_install_action (widget_class, "video.set-speed", "d", set_speed_action_cb);
+ gtk_widget_class_install_action (widget_class, "video.previous-item", NULL, previous_item_action_cb);
+ gtk_widget_class_install_action (widget_class, "video.next-item", NULL, next_item_action_cb);
+ gtk_widget_class_install_action (widget_class, "video.select-item", "u", select_item_action_cb);
+
+ gtk_widget_class_set_template_from_resource (widget_class,
+ CLAPPER_GTK_RESOURCE_PREFIX "/ui/clapper-gtk-video.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, ClapperGtkVideo, status);
+ gtk_widget_class_bind_template_child (widget_class, ClapperGtkVideo, buffering_animation);
+ gtk_widget_class_bind_template_child (widget_class, ClapperGtkVideo, touch_gesture);
+ gtk_widget_class_bind_template_child (widget_class, ClapperGtkVideo, click_gesture);
+
+ gtk_widget_class_bind_template_callback (widget_class, left_click_pressed_cb);
+ gtk_widget_class_bind_template_callback (widget_class, left_click_released_cb);
+ gtk_widget_class_bind_template_callback (widget_class, left_click_stopped_cb);
+ gtk_widget_class_bind_template_callback (widget_class, touch_pressed_cb);
+ gtk_widget_class_bind_template_callback (widget_class, touch_released_cb);
+ gtk_widget_class_bind_template_callback (widget_class, motion_enter_cb);
+ gtk_widget_class_bind_template_callback (widget_class, motion_cb);
+ gtk_widget_class_bind_template_callback (widget_class, motion_leave_cb);
+ gtk_widget_class_bind_template_callback (widget_class, drop_motion_cb);
+ gtk_widget_class_bind_template_callback (widget_class, drop_motion_leave_cb);
+
+ gtk_widget_class_set_layout_manager_type (widget_class, GTK_TYPE_BIN_LAYOUT);
+ gtk_widget_class_set_accessible_role (widget_class, GTK_ACCESSIBLE_ROLE_GENERIC);
+ gtk_widget_class_set_css_name (widget_class, "clapper-gtk-video");
+}
diff --git a/src/lib/clapper-gtk/clapper-gtk-video.h b/src/lib/clapper-gtk/clapper-gtk-video.h
new file mode 100644
index 00000000..d86a6f5c
--- /dev/null
+++ b/src/lib/clapper-gtk/clapper-gtk-video.h
@@ -0,0 +1,60 @@
+/* Clapper GTK Integration Library
+ * Copyright (C) 2024 Rafał Dzięgiel
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+#pragma once
+
+#if !defined(__CLAPPER_GTK_INSIDE__) && !defined(CLAPPER_GTK_COMPILATION)
+#error "Only can be included directly."
+#endif
+
+#include
+#include
+#include
+#include
+
+G_BEGIN_DECLS
+
+#define CLAPPER_GTK_TYPE_VIDEO (clapper_gtk_video_get_type())
+#define CLAPPER_GTK_VIDEO_CAST(obj) ((ClapperGtkVideo *)(obj))
+
+G_DECLARE_FINAL_TYPE (ClapperGtkVideo, clapper_gtk_video, CLAPPER_GTK, VIDEO, GtkWidget)
+
+GtkWidget * clapper_gtk_video_new (void);
+
+void clapper_gtk_video_add_overlay (ClapperGtkVideo *video, GtkWidget *widget);
+
+void clapper_gtk_video_add_fading_overlay (ClapperGtkVideo *video, GtkWidget *widget);
+
+ClapperPlayer * clapper_gtk_video_get_player (ClapperGtkVideo *video);
+
+void clapper_gtk_video_set_fade_delay (ClapperGtkVideo *video, guint delay);
+
+guint clapper_gtk_video_get_fade_delay (ClapperGtkVideo *video);
+
+void clapper_gtk_video_set_touch_fade_delay (ClapperGtkVideo *video, guint delay);
+
+guint clapper_gtk_video_get_touch_fade_delay (ClapperGtkVideo *video);
+
+void clapper_gtk_video_set_auto_inhibit (ClapperGtkVideo *video, gboolean inhibit);
+
+gboolean clapper_gtk_video_get_auto_inhibit (ClapperGtkVideo *video);
+
+gboolean clapper_gtk_video_get_inhibited (ClapperGtkVideo *video);
+
+G_END_DECLS
diff --git a/src/lib/clapper-gtk/clapper-gtk.gresources.xml b/src/lib/clapper-gtk/clapper-gtk.gresources.xml
new file mode 100644
index 00000000..c25bb2f6
--- /dev/null
+++ b/src/lib/clapper-gtk/clapper-gtk.gresources.xml
@@ -0,0 +1,14 @@
+
+
+
+ ui/clapper-gtk-billboard.ui
+ ui/clapper-gtk-extra-menu-button.ui
+ ui/clapper-gtk-seek-bar.ui
+ ui/clapper-gtk-simple-controls.ui
+ ui/clapper-gtk-status.ui
+ ui/clapper-gtk-stream-list-item.ui
+ ui/clapper-gtk-title-header.ui
+ ui/clapper-gtk-video.ui
+ css/styles.css
+
+
diff --git a/src/lib/clapper-gtk/clapper-gtk.h b/src/lib/clapper-gtk/clapper-gtk.h
new file mode 100644
index 00000000..f34608ed
--- /dev/null
+++ b/src/lib/clapper-gtk/clapper-gtk.h
@@ -0,0 +1,42 @@
+/* Clapper GTK Integration Library
+ * Copyright (C) 2024 Rafał Dzięgiel
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+#pragma once
+
+#define __CLAPPER_GTK_INSIDE__
+
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#undef __CLAPPER_GTK_INSIDE__
diff --git a/src/lib/clapper-gtk/css/styles.css b/src/lib/clapper-gtk/css/styles.css
new file mode 100644
index 00000000..9110496d
--- /dev/null
+++ b/src/lib/clapper-gtk/css/styles.css
@@ -0,0 +1,153 @@
+/* Adwaita OSD background color is unacceptable:
+ * https://gitlab.gnome.org/GNOME/libadwaita/-/issues/454 */
+clapper-gtk-video menubutton.osd,
+clapper-gtk-video button.osd,
+clapper-gtk-video box.osd,
+clapper-gtk-video clapper-gtk-title-header.osd,
+clapper-gtk-video .osd popover contents,
+clapper-gtk-video .osd popover arrow {
+ background-color: rgba(38,38,38,0.8);
+}
+clapper-gtk-video button.osd:hover,
+clapper-gtk-video button.osd:checked {
+ background-color: rgba(63,63,63,0.8);
+}
+clapper-gtk-video button.osd:active {
+ background-color: rgba(82,82,82,0.8);
+}
+clapper-gtk-video box.osd listview.osd {
+ background-color: transparent;
+}
+
+clapper-gtk-status box {
+ padding-left: 6px;
+ padding-right: 6px;
+}
+clapper-gtk-status box image {
+ -gtk-icon-size: 128px;
+ -gtk-icon-filter: opacity(0.6);
+}
+clapper-gtk-status.adapted box image {
+ -gtk-icon-size: 96px;
+}
+
+clapper-gtk-billboard .sidebox {
+ padding: 12px;
+ border-radius: 9999px;
+}
+clapper-gtk-billboard .sidebox:dir(ltr) {
+ margin-left: 6px;
+}
+clapper-gtk-billboard .sidebox:dir(rtl) {
+ margin-right: 6px;
+}
+clapper-gtk-billboard .sidebox .progressbox {
+ margin-bottom: 6px;
+}
+clapper-gtk-billboard .sidebox .progressbox progressbar {
+ min-height: 90px;
+}
+clapper-gtk-billboard .sidebox .progressbox.overamp progress {
+ background: #c01c28;
+}
+clapper-gtk-billboard .sidebox .progresslabel:dir(ltr) {
+ margin-left: 6px;
+}
+clapper-gtk-billboard .sidebox .progresslabel:dir(rtl) {
+ margin-right: 6px;
+}
+clapper-gtk-billboard .messagebox {
+ margin: 6px;
+ padding: 12px;
+ min-width: 92px;
+}
+clapper-gtk-billboard .messagebox image {
+ -gtk-icon-size: 48px;
+}
+
+menubutton.circular,
+dropdown.circular button.toggle {
+ border-radius: 9999px;
+}
+.rounded {
+ border-radius: 19px;
+}
+
+clapper-gtk-title-header {
+ margin: 6px;
+ min-height: 38px;
+}
+clapper-gtk-title-header label {
+ margin-left: 12px;
+ margin-right: 12px;
+}
+
+clapper-gtk-simple-controls .centerbox {
+ margin-bottom: 2px;
+}
+clapper-gtk-simple-controls .mainbox {
+ margin-left: 6px;
+ margin-right: 6px;
+ margin-bottom: 4px;
+}
+clapper-gtk-simple-controls .mainbox button {
+ margin: 2px;
+}
+clapper-gtk-simple-controls .mainbox scale {
+ margin-top: 2px;
+ margin-bottom: 2px;
+}
+clapper-gtk-simple-controls .mainbox popover button {
+ margin: 0px;
+}
+clapper-gtk-simple-controls .fullscreenbutton {
+ margin-bottom: 6px;
+}
+clapper-gtk-simple-controls .fullscreenbutton:dir(ltr) {
+ margin-right: 6px;
+}
+clapper-gtk-simple-controls .fullscreenbutton:dir(rtl) {
+ margin-left: 6px;
+}
+
+clapper-gtk-seek-bar scale trough highlight {
+ min-height: 6px;
+}
+clapper-gtk-seek-bar label {
+ margin-left: 2px;
+ margin-right: 2px;
+}
+
+clapper-gtk-extra-menu-button popover .spinsidebutton {
+ min-width: 28px;
+ min-height: 28px;
+}
+clapper-gtk-extra-menu-button popover .spinsidebutton:dir(ltr) {
+ margin-right: 2px;
+}
+clapper-gtk-extra-menu-button popover .spinsidebutton:dir(rtl) {
+ margin-left: 2px;
+}
+clapper-gtk-extra-menu-button popover spinbutton {
+ border-radius: 9999px;
+}
+clapper-gtk-extra-menu-button popover spinbutton button.up:dir(ltr) {
+ border-radius: 0px 9999px 9999px 0px;
+}
+clapper-gtk-extra-menu-button popover spinbutton button.up:dir(rtl) {
+ border-radius: 9999px 0px 0px 9999px;
+}
+clapper-gtk-extra-menu-button popover spinbutton button.up:dir(ltr) {
+ padding-right: 6px;
+}
+clapper-gtk-extra-menu-button popover spinbutton button.up:dir(rtl) {
+ padding-left: 6px;
+}
+clapper-gtk-menu-scrolled-window listview row {
+ padding-left: 6px;
+ padding-right: 6px;
+}
+clapper-gtk-menu-scrolled-window listview row checkbutton label {
+ margin-left: 2px;
+ margin-right: 2px;
+}
diff --git a/src/lib/clapper-gtk/meson.build b/src/lib/clapper-gtk/meson.build
new file mode 100644
index 00000000..8c989a73
--- /dev/null
+++ b/src/lib/clapper-gtk/meson.build
@@ -0,0 +1,221 @@
+clappergtk_dep = dependency('', required: false)
+clappergtk_option = get_option('clapper-gtk')
+clappergtk_api_name = meson.project_name() + '-gtk' + clapper_version_suffix
+clappergtk_resource_prefix = '/com/github/rafostar/Clapper/clapper-gtk'
+build_clappergtk = false
+
+if clappergtk_option.disabled()
+ subdir_done()
+endif
+
+clappergtk_deps = [
+ clapper_dep,
+ gst_dep,
+ gst_clapper_sink_dep,
+ gtk4_dep,
+ glib_dep,
+ gobject_dep,
+ libm,
+]
+
+foreach dep : clappergtk_deps
+ if not dep.found()
+ if clappergtk_option.enabled()
+ error('clapper-gtk option was enabled, but required dependencies were not found')
+ endif
+ subdir_done()
+ endif
+endforeach
+
+subdir('po')
+
+config_h = configuration_data()
+config_h.set_quoted('GETTEXT_PACKAGE', meson.project_name() + '-app')
+config_h.set_quoted('LOCALEDIR', join_paths (prefix, localedir))
+config_h.set_quoted('CLAPPER_GTK_RESOURCE_PREFIX', clappergtk_resource_prefix)
+
+configure_file(
+ output: 'config.h',
+ configuration: config_h,
+)
+
+version_conf = configuration_data()
+
+version_conf.set(
+ 'CLAPPER_GTK_VERSION',
+ meson.project_version(),
+)
+version_conf.set(
+ 'CLAPPER_GTK_MAJOR_VERSION',
+ version_array[0].to_int(),
+)
+version_conf.set(
+ 'CLAPPER_GTK_MINOR_VERSION',
+ version_array[1].to_int(),
+)
+version_conf.set(
+ 'CLAPPER_GTK_MICRO_VERSION',
+ version_array[2].to_int(),
+)
+
+clappergtk_version_header = configure_file(
+ input: 'clapper-gtk-version.h.in',
+ output: 'clapper-gtk-version.h',
+ configuration: version_conf,
+)
+clappergtk_resources = gnome.compile_resources(
+ 'clapper-gtk-resources',
+ 'clapper-gtk.gresources.xml',
+ c_name: 'clapper_gtk',
+)
+
+# Include the generated headers
+clappergtk_conf_inc = [
+ include_directories('.'),
+ include_directories('..'),
+]
+
+clappergtk_headers = [
+ 'clapper-gtk.h',
+ 'clapper-gtk-enums.h',
+ 'clapper-gtk-billboard.h',
+ 'clapper-gtk-container.h',
+ 'clapper-gtk-extra-menu-button.h',
+ 'clapper-gtk-lead-container.h',
+ 'clapper-gtk-next-item-button.h',
+ 'clapper-gtk-previous-item-button.h',
+ 'clapper-gtk-seek-bar.h',
+ 'clapper-gtk-simple-controls.h',
+ 'clapper-gtk-title-header.h',
+ 'clapper-gtk-title-label.h',
+ 'clapper-gtk-toggle-fullscreen-button.h',
+ 'clapper-gtk-toggle-play-button.h',
+ 'clapper-gtk-utils.h',
+ 'clapper-gtk-video.h',
+ clappergtk_version_header,
+]
+clappergtk_sources = [
+ 'clapper-gtk-billboard.c',
+ 'clapper-gtk-buffering-animation.c',
+ 'clapper-gtk-buffering-paintable.c',
+ 'clapper-gtk-container.c',
+ 'clapper-gtk-extra-menu-button.c',
+ 'clapper-gtk-lead-container.c',
+ 'clapper-gtk-limited-layout.c',
+ 'clapper-gtk-next-item-button.c',
+ 'clapper-gtk-previous-item-button.c',
+ 'clapper-gtk-seek-bar.c',
+ 'clapper-gtk-simple-controls.c',
+ 'clapper-gtk-status.c',
+ 'clapper-gtk-stream-check-button.c',
+ 'clapper-gtk-title-header.c',
+ 'clapper-gtk-title-label.c',
+ 'clapper-gtk-toggle-fullscreen-button.c',
+ 'clapper-gtk-toggle-play-button.c',
+ 'clapper-gtk-utils.c',
+ 'clapper-gtk-video.c',
+ clappergtk_resources,
+]
+clappergtk_c_args = [
+ '-DG_LOG_DOMAIN="ClapperGtk"',
+ '-DCLAPPER_GTK_COMPILATION',
+ '-DGST_USE_UNSTABLE_API',
+]
+
+clappergtk_headers_dir = join_paths(includedir, clapper_api_name, 'clapper-gtk')
+
+clappergtk_enums = gnome.mkenums_simple(
+ 'clapper-gtk-enum-types',
+ sources: clappergtk_headers,
+ identifier_prefix: 'ClapperGtk',
+ symbol_prefix: 'clapper_gtk',
+ install_header: true,
+ install_dir: clappergtk_headers_dir,
+)
+
+clappergtk_lib = library(
+ clappergtk_api_name,
+ clappergtk_sources + clappergtk_enums,
+ dependencies: clappergtk_deps,
+ include_directories: clappergtk_conf_inc,
+ c_args: clappergtk_c_args,
+ version: clapper_version,
+ install: true,
+)
+install_headers(clappergtk_headers,
+ install_dir: clappergtk_headers_dir,
+)
+build_clappergtk = true
+
+if build_gir
+ clappergtk_gir = gnome.generate_gir(clappergtk_lib,
+ sources: [
+ clappergtk_sources,
+ clappergtk_headers,
+ clappergtk_enums,
+ ],
+ extra_args: [
+ gir_init_section,
+ '--quiet',
+ '--warn-all',
+ '-DCLAPPER_GTK_COMPILATION',
+ '-DGST_USE_UNSTABLE_API',
+ ],
+ nsversion: version_array[0] + '.0',
+ namespace: 'ClapperGtk',
+ identifier_prefix: 'ClapperGtk',
+ symbol_prefix: 'clapper_gtk',
+ export_packages: clappergtk_api_name,
+ install: true,
+ includes: [
+ clapper_gir[0],
+ 'Gst-1.0',
+ 'Gtk-4.0',
+ 'GLib-2.0',
+ 'GObject-2.0',
+ ],
+ header: join_paths(meson.project_name() + '-gtk', 'clapper-gtk.h'),
+ )
+endif
+
+if build_vapi
+ if not build_gir
+ if get_option('vapi').enabled()
+ error('Cannot build "vapi" without "introspection"')
+ endif
+ else
+ gnome.generate_vapi(clappergtk_api_name,
+ sources: clappergtk_gir[0],
+ packages: clapper_pkg_reqs + [
+ clapper_vapi,
+ 'gtk4',
+ ],
+ install: true,
+ )
+ endif
+endif
+
+pkgconfig.generate(clappergtk_lib,
+ subdirs: [clappergtk_api_name],
+ filebase: clappergtk_api_name,
+ name: meson.project_name() + '-gtk',
+ version: meson.project_version(),
+ description: 'Clapper GTK integration library',
+ requires: [
+ clapper_lib,
+ 'gstreamer-1.0',
+ 'gtk4',
+ 'glib-2.0',
+ 'gobject-2.0',
+ ],
+)
+
+clappergtk_dep = declare_dependency(
+ link_with: clappergtk_lib,
+ include_directories: clappergtk_conf_inc,
+ dependencies: clappergtk_deps,
+ sources: [
+ clappergtk_version_header,
+ clappergtk_enums[1],
+ ],
+)
diff --git a/src/lib/clapper-gtk/po/LINGUAS b/src/lib/clapper-gtk/po/LINGUAS
new file mode 100644
index 00000000..ad6fe0cf
--- /dev/null
+++ b/src/lib/clapper-gtk/po/LINGUAS
@@ -0,0 +1 @@
+ar ast ca cs de es eu fa fi fr he hr hu it ja lt nl pl pt pt_BR ru sk sv tr zh_CN
diff --git a/src/lib/clapper-gtk/po/POTFILES b/src/lib/clapper-gtk/po/POTFILES
new file mode 100644
index 00000000..cc4ca53f
--- /dev/null
+++ b/src/lib/clapper-gtk/po/POTFILES
@@ -0,0 +1,5 @@
+src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui
+
+src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c
+src/lib/clapper-gtk/clapper-gtk-stream-check-button.c
+src/lib/clapper-gtk/clapper-gtk-title-label.c
diff --git a/src/lib/clapper-gtk/po/ar.po b/src/lib/clapper-gtk/po/ar.po
new file mode 100644
index 00000000..1801b808
--- /dev/null
+++ b/src/lib/clapper-gtk/po/ar.po
@@ -0,0 +1,65 @@
+# Arabic translations for clapper-gtk package.
+# Copyright (C) 2024 THE clapper-gtk'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the clapper-gtk package.
+# Automatically generated, 2024.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: clapper-gtk\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-04-05 20:50+0200\n"
+"PO-Revision-Date: 2024-03-07 20:49+0100\n"
+"Last-Translator: Automatically generated\n"
+"Language-Team: none\n"
+"Language: ar\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:9
+#: src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c:227
+msgid "Video"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:17
+#: src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c:228
+msgid "Audio"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:25
+#: src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c:229
+msgid "Subtitles"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:28
+msgid "Enabled"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:32
+msgid "Open…"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:75
+msgid "Mute"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:119
+msgid "Reset"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-stream-check-button.c:82
+#: src/lib/clapper-gtk/clapper-gtk-stream-check-button.c:126
+msgid "Undetermined"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-stream-check-button.c:82
+msgid "Channels"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-title-label.c:91
+msgid "No media"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-title-label.c:103
+msgid "Unknown title"
+msgstr ""
diff --git a/src/lib/clapper-gtk/po/ast.po b/src/lib/clapper-gtk/po/ast.po
new file mode 100644
index 00000000..5f7329d1
--- /dev/null
+++ b/src/lib/clapper-gtk/po/ast.po
@@ -0,0 +1,65 @@
+# Asturian translations for clapper-gtk package.
+# Copyright (C) 2024 THE clapper-gtk'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the clapper-gtk package.
+# Automatically generated, 2024.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: clapper-gtk\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-04-05 20:50+0200\n"
+"PO-Revision-Date: 2024-03-07 20:49+0100\n"
+"Last-Translator: Automatically generated\n"
+"Language-Team: none\n"
+"Language: ast\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:9
+#: src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c:227
+msgid "Video"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:17
+#: src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c:228
+msgid "Audio"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:25
+#: src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c:229
+msgid "Subtitles"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:28
+msgid "Enabled"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:32
+msgid "Open…"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:75
+msgid "Mute"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:119
+msgid "Reset"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-stream-check-button.c:82
+#: src/lib/clapper-gtk/clapper-gtk-stream-check-button.c:126
+msgid "Undetermined"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-stream-check-button.c:82
+msgid "Channels"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-title-label.c:91
+msgid "No media"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-title-label.c:103
+msgid "Unknown title"
+msgstr ""
diff --git a/src/lib/clapper-gtk/po/ca.po b/src/lib/clapper-gtk/po/ca.po
new file mode 100644
index 00000000..6ddaa142
--- /dev/null
+++ b/src/lib/clapper-gtk/po/ca.po
@@ -0,0 +1,66 @@
+# Catalan translations for clapper-gtk package.
+# Copyright (C) 2024 THE clapper-gtk'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the clapper-gtk package.
+# Automatically generated, 2024.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: clapper-gtk\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-04-05 20:50+0200\n"
+"PO-Revision-Date: 2024-03-07 20:49+0100\n"
+"Last-Translator: Automatically generated\n"
+"Language-Team: none\n"
+"Language: ca\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:9
+#: src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c:227
+msgid "Video"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:17
+#: src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c:228
+msgid "Audio"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:25
+#: src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c:229
+msgid "Subtitles"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:28
+msgid "Enabled"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:32
+msgid "Open…"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:75
+msgid "Mute"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:119
+msgid "Reset"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-stream-check-button.c:82
+#: src/lib/clapper-gtk/clapper-gtk-stream-check-button.c:126
+msgid "Undetermined"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-stream-check-button.c:82
+msgid "Channels"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-title-label.c:91
+msgid "No media"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-title-label.c:103
+msgid "Unknown title"
+msgstr ""
diff --git a/src/lib/clapper-gtk/po/clapper-gtk.pot b/src/lib/clapper-gtk/po/clapper-gtk.pot
new file mode 100644
index 00000000..ab4eee1d
--- /dev/null
+++ b/src/lib/clapper-gtk/po/clapper-gtk.pot
@@ -0,0 +1,66 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the clapper-gtk package.
+# FIRST AUTHOR , YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: clapper-gtk\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-04-05 20:50+0200\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME \n"
+"Language-Team: LANGUAGE \n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:9
+#: src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c:227
+msgid "Video"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:17
+#: src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c:228
+msgid "Audio"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:25
+#: src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c:229
+msgid "Subtitles"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:28
+msgid "Enabled"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:32
+msgid "Open…"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:75
+msgid "Mute"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:119
+msgid "Reset"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-stream-check-button.c:82
+#: src/lib/clapper-gtk/clapper-gtk-stream-check-button.c:126
+msgid "Undetermined"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-stream-check-button.c:82
+msgid "Channels"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-title-label.c:91
+msgid "No media"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-title-label.c:103
+msgid "Unknown title"
+msgstr ""
diff --git a/src/lib/clapper-gtk/po/cs.po b/src/lib/clapper-gtk/po/cs.po
new file mode 100644
index 00000000..94e0ddaf
--- /dev/null
+++ b/src/lib/clapper-gtk/po/cs.po
@@ -0,0 +1,66 @@
+# Czech translations for clapper-gtk package.
+# Copyright (C) 2024 THE clapper-gtk'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the clapper-gtk package.
+# Automatically generated, 2024.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: clapper-gtk\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-04-05 20:50+0200\n"
+"PO-Revision-Date: 2024-03-07 20:49+0100\n"
+"Last-Translator: Automatically generated\n"
+"Language-Team: none\n"
+"Language: cs\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n"
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:9
+#: src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c:227
+msgid "Video"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:17
+#: src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c:228
+msgid "Audio"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:25
+#: src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c:229
+msgid "Subtitles"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:28
+msgid "Enabled"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:32
+msgid "Open…"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:75
+msgid "Mute"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:119
+msgid "Reset"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-stream-check-button.c:82
+#: src/lib/clapper-gtk/clapper-gtk-stream-check-button.c:126
+msgid "Undetermined"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-stream-check-button.c:82
+msgid "Channels"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-title-label.c:91
+msgid "No media"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-title-label.c:103
+msgid "Unknown title"
+msgstr ""
diff --git a/src/lib/clapper-gtk/po/de.po b/src/lib/clapper-gtk/po/de.po
new file mode 100644
index 00000000..808a44b6
--- /dev/null
+++ b/src/lib/clapper-gtk/po/de.po
@@ -0,0 +1,66 @@
+# German translations for clapper-gtk package.
+# Copyright (C) 2024 THE clapper-gtk'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the clapper-gtk package.
+# Automatically generated, 2024.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: clapper-gtk\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-04-05 20:50+0200\n"
+"PO-Revision-Date: 2024-03-07 20:49+0100\n"
+"Last-Translator: Automatically generated\n"
+"Language-Team: none\n"
+"Language: de\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:9
+#: src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c:227
+msgid "Video"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:17
+#: src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c:228
+msgid "Audio"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:25
+#: src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c:229
+msgid "Subtitles"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:28
+msgid "Enabled"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:32
+msgid "Open…"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:75
+msgid "Mute"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:119
+msgid "Reset"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-stream-check-button.c:82
+#: src/lib/clapper-gtk/clapper-gtk-stream-check-button.c:126
+msgid "Undetermined"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-stream-check-button.c:82
+msgid "Channels"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-title-label.c:91
+msgid "No media"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-title-label.c:103
+msgid "Unknown title"
+msgstr ""
diff --git a/src/lib/clapper-gtk/po/es.po b/src/lib/clapper-gtk/po/es.po
new file mode 100644
index 00000000..7ea062c7
--- /dev/null
+++ b/src/lib/clapper-gtk/po/es.po
@@ -0,0 +1,66 @@
+# Spanish translations for clapper-gtk package.
+# Copyright (C) 2024 THE clapper-gtk'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the clapper-gtk package.
+# Automatically generated, 2024.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: clapper-gtk\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-04-05 20:50+0200\n"
+"PO-Revision-Date: 2024-03-07 20:49+0100\n"
+"Last-Translator: Automatically generated\n"
+"Language-Team: none\n"
+"Language: es\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:9
+#: src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c:227
+msgid "Video"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:17
+#: src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c:228
+msgid "Audio"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:25
+#: src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c:229
+msgid "Subtitles"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:28
+msgid "Enabled"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:32
+msgid "Open…"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:75
+msgid "Mute"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:119
+msgid "Reset"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-stream-check-button.c:82
+#: src/lib/clapper-gtk/clapper-gtk-stream-check-button.c:126
+msgid "Undetermined"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-stream-check-button.c:82
+msgid "Channels"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-title-label.c:91
+msgid "No media"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-title-label.c:103
+msgid "Unknown title"
+msgstr ""
diff --git a/src/lib/clapper-gtk/po/eu.po b/src/lib/clapper-gtk/po/eu.po
new file mode 100644
index 00000000..475d2662
--- /dev/null
+++ b/src/lib/clapper-gtk/po/eu.po
@@ -0,0 +1,65 @@
+# Basque translations for clapper-gtk package.
+# Copyright (C) 2024 THE clapper-gtk'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the clapper-gtk package.
+# Automatically generated, 2024.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: clapper-gtk\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-04-05 20:50+0200\n"
+"PO-Revision-Date: 2024-03-07 20:49+0100\n"
+"Last-Translator: Automatically generated\n"
+"Language-Team: none\n"
+"Language: eu\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:9
+#: src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c:227
+msgid "Video"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:17
+#: src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c:228
+msgid "Audio"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:25
+#: src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c:229
+msgid "Subtitles"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:28
+msgid "Enabled"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:32
+msgid "Open…"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:75
+msgid "Mute"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:119
+msgid "Reset"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-stream-check-button.c:82
+#: src/lib/clapper-gtk/clapper-gtk-stream-check-button.c:126
+msgid "Undetermined"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-stream-check-button.c:82
+msgid "Channels"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-title-label.c:91
+msgid "No media"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-title-label.c:103
+msgid "Unknown title"
+msgstr ""
diff --git a/src/lib/clapper-gtk/po/fa.po b/src/lib/clapper-gtk/po/fa.po
new file mode 100644
index 00000000..984605d7
--- /dev/null
+++ b/src/lib/clapper-gtk/po/fa.po
@@ -0,0 +1,65 @@
+# Persian translations for clapper-gtk package.
+# Copyright (C) 2024 THE clapper-gtk'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the clapper-gtk package.
+# Automatically generated, 2024.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: clapper-gtk\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-04-05 20:50+0200\n"
+"PO-Revision-Date: 2024-03-07 20:49+0100\n"
+"Last-Translator: Automatically generated\n"
+"Language-Team: none\n"
+"Language: fa\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:9
+#: src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c:227
+msgid "Video"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:17
+#: src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c:228
+msgid "Audio"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:25
+#: src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c:229
+msgid "Subtitles"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:28
+msgid "Enabled"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:32
+msgid "Open…"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:75
+msgid "Mute"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:119
+msgid "Reset"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-stream-check-button.c:82
+#: src/lib/clapper-gtk/clapper-gtk-stream-check-button.c:126
+msgid "Undetermined"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-stream-check-button.c:82
+msgid "Channels"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-title-label.c:91
+msgid "No media"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-title-label.c:103
+msgid "Unknown title"
+msgstr ""
diff --git a/src/lib/clapper-gtk/po/fi.po b/src/lib/clapper-gtk/po/fi.po
new file mode 100644
index 00000000..c012bfad
--- /dev/null
+++ b/src/lib/clapper-gtk/po/fi.po
@@ -0,0 +1,66 @@
+# Finnish translations for clapper-gtk package.
+# Copyright (C) 2024 THE clapper-gtk'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the clapper-gtk package.
+# Automatically generated, 2024.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: clapper-gtk\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-04-05 20:50+0200\n"
+"PO-Revision-Date: 2024-03-07 20:49+0100\n"
+"Last-Translator: Automatically generated\n"
+"Language-Team: none\n"
+"Language: fi\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:9
+#: src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c:227
+msgid "Video"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:17
+#: src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c:228
+msgid "Audio"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:25
+#: src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c:229
+msgid "Subtitles"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:28
+msgid "Enabled"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:32
+msgid "Open…"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:75
+msgid "Mute"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:119
+msgid "Reset"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-stream-check-button.c:82
+#: src/lib/clapper-gtk/clapper-gtk-stream-check-button.c:126
+msgid "Undetermined"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-stream-check-button.c:82
+msgid "Channels"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-title-label.c:91
+msgid "No media"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-title-label.c:103
+msgid "Unknown title"
+msgstr ""
diff --git a/src/lib/clapper-gtk/po/fr.po b/src/lib/clapper-gtk/po/fr.po
new file mode 100644
index 00000000..da3b6737
--- /dev/null
+++ b/src/lib/clapper-gtk/po/fr.po
@@ -0,0 +1,66 @@
+# French translations for clapper-gtk package.
+# Copyright (C) 2024 THE clapper-gtk'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the clapper-gtk package.
+# Automatically generated, 2024.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: clapper-gtk\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-04-05 20:50+0200\n"
+"PO-Revision-Date: 2024-03-07 20:49+0100\n"
+"Last-Translator: Automatically generated\n"
+"Language-Team: none\n"
+"Language: fr\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:9
+#: src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c:227
+msgid "Video"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:17
+#: src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c:228
+msgid "Audio"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:25
+#: src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c:229
+msgid "Subtitles"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:28
+msgid "Enabled"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:32
+msgid "Open…"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:75
+msgid "Mute"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:119
+msgid "Reset"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-stream-check-button.c:82
+#: src/lib/clapper-gtk/clapper-gtk-stream-check-button.c:126
+msgid "Undetermined"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-stream-check-button.c:82
+msgid "Channels"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-title-label.c:91
+msgid "No media"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-title-label.c:103
+msgid "Unknown title"
+msgstr ""
diff --git a/src/lib/clapper-gtk/po/he.po b/src/lib/clapper-gtk/po/he.po
new file mode 100644
index 00000000..a56c6620
--- /dev/null
+++ b/src/lib/clapper-gtk/po/he.po
@@ -0,0 +1,66 @@
+# Hebrew translations for clapper-gtk package.
+# Copyright (C) 2024 THE clapper-gtk'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the clapper-gtk package.
+# Automatically generated, 2024.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: clapper-gtk\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-04-05 20:50+0200\n"
+"PO-Revision-Date: 2024-03-07 20:49+0100\n"
+"Last-Translator: Automatically generated\n"
+"Language-Team: none\n"
+"Language: he\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:9
+#: src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c:227
+msgid "Video"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:17
+#: src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c:228
+msgid "Audio"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:25
+#: src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c:229
+msgid "Subtitles"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:28
+msgid "Enabled"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:32
+msgid "Open…"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:75
+msgid "Mute"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:119
+msgid "Reset"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-stream-check-button.c:82
+#: src/lib/clapper-gtk/clapper-gtk-stream-check-button.c:126
+msgid "Undetermined"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-stream-check-button.c:82
+msgid "Channels"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-title-label.c:91
+msgid "No media"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-title-label.c:103
+msgid "Unknown title"
+msgstr ""
diff --git a/src/lib/clapper-gtk/po/hr.po b/src/lib/clapper-gtk/po/hr.po
new file mode 100644
index 00000000..c6eea09d
--- /dev/null
+++ b/src/lib/clapper-gtk/po/hr.po
@@ -0,0 +1,67 @@
+# Croatian translations for clapper-gtk package.
+# Copyright (C) 2024 THE clapper-gtk'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the clapper-gtk package.
+# Automatically generated, 2024.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: clapper-gtk\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-04-05 20:50+0200\n"
+"PO-Revision-Date: 2024-03-07 20:49+0100\n"
+"Last-Translator: Automatically generated\n"
+"Language-Team: none\n"
+"Language: hr\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
+"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:9
+#: src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c:227
+msgid "Video"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:17
+#: src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c:228
+msgid "Audio"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:25
+#: src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c:229
+msgid "Subtitles"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:28
+msgid "Enabled"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:32
+msgid "Open…"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:75
+msgid "Mute"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:119
+msgid "Reset"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-stream-check-button.c:82
+#: src/lib/clapper-gtk/clapper-gtk-stream-check-button.c:126
+msgid "Undetermined"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-stream-check-button.c:82
+msgid "Channels"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-title-label.c:91
+msgid "No media"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-title-label.c:103
+msgid "Unknown title"
+msgstr ""
diff --git a/src/lib/clapper-gtk/po/hu.po b/src/lib/clapper-gtk/po/hu.po
new file mode 100644
index 00000000..e1c002dd
--- /dev/null
+++ b/src/lib/clapper-gtk/po/hu.po
@@ -0,0 +1,66 @@
+# Hungarian translations for clapper-gtk package.
+# Copyright (C) 2024 THE clapper-gtk'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the clapper-gtk package.
+# Automatically generated, 2024.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: clapper-gtk\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-04-05 20:50+0200\n"
+"PO-Revision-Date: 2024-03-07 20:49+0100\n"
+"Last-Translator: Automatically generated\n"
+"Language-Team: none\n"
+"Language: hu\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:9
+#: src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c:227
+msgid "Video"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:17
+#: src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c:228
+msgid "Audio"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:25
+#: src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c:229
+msgid "Subtitles"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:28
+msgid "Enabled"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:32
+msgid "Open…"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:75
+msgid "Mute"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:119
+msgid "Reset"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-stream-check-button.c:82
+#: src/lib/clapper-gtk/clapper-gtk-stream-check-button.c:126
+msgid "Undetermined"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-stream-check-button.c:82
+msgid "Channels"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-title-label.c:91
+msgid "No media"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-title-label.c:103
+msgid "Unknown title"
+msgstr ""
diff --git a/src/lib/clapper-gtk/po/it.po b/src/lib/clapper-gtk/po/it.po
new file mode 100644
index 00000000..4157f052
--- /dev/null
+++ b/src/lib/clapper-gtk/po/it.po
@@ -0,0 +1,66 @@
+# Italian translations for clapper-gtk package.
+# Copyright (C) 2024 THE clapper-gtk'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the clapper-gtk package.
+# Automatically generated, 2024.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: clapper-gtk\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-04-05 20:50+0200\n"
+"PO-Revision-Date: 2024-03-07 20:49+0100\n"
+"Last-Translator: Automatically generated\n"
+"Language-Team: none\n"
+"Language: it\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:9
+#: src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c:227
+msgid "Video"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:17
+#: src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c:228
+msgid "Audio"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:25
+#: src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c:229
+msgid "Subtitles"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:28
+msgid "Enabled"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:32
+msgid "Open…"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:75
+msgid "Mute"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:119
+msgid "Reset"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-stream-check-button.c:82
+#: src/lib/clapper-gtk/clapper-gtk-stream-check-button.c:126
+msgid "Undetermined"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-stream-check-button.c:82
+msgid "Channels"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-title-label.c:91
+msgid "No media"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-title-label.c:103
+msgid "Unknown title"
+msgstr ""
diff --git a/src/lib/clapper-gtk/po/ja.po b/src/lib/clapper-gtk/po/ja.po
new file mode 100644
index 00000000..046a7f28
--- /dev/null
+++ b/src/lib/clapper-gtk/po/ja.po
@@ -0,0 +1,66 @@
+# Japanese translations for clapper-gtk package.
+# Copyright (C) 2024 THE clapper-gtk'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the clapper-gtk package.
+# Automatically generated, 2024.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: clapper-gtk\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-04-05 20:50+0200\n"
+"PO-Revision-Date: 2024-03-07 20:49+0100\n"
+"Last-Translator: Automatically generated\n"
+"Language-Team: none\n"
+"Language: ja\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:9
+#: src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c:227
+msgid "Video"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:17
+#: src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c:228
+msgid "Audio"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:25
+#: src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c:229
+msgid "Subtitles"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:28
+msgid "Enabled"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:32
+msgid "Open…"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:75
+msgid "Mute"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:119
+msgid "Reset"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-stream-check-button.c:82
+#: src/lib/clapper-gtk/clapper-gtk-stream-check-button.c:126
+msgid "Undetermined"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-stream-check-button.c:82
+msgid "Channels"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-title-label.c:91
+msgid "No media"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-title-label.c:103
+msgid "Unknown title"
+msgstr ""
diff --git a/src/lib/clapper-gtk/po/lt.po b/src/lib/clapper-gtk/po/lt.po
new file mode 100644
index 00000000..174ccb5f
--- /dev/null
+++ b/src/lib/clapper-gtk/po/lt.po
@@ -0,0 +1,67 @@
+# Lithuanian translations for clapper-gtk package.
+# Copyright (C) 2024 THE clapper-gtk'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the clapper-gtk package.
+# Automatically generated, 2024.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: clapper-gtk\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-04-05 20:50+0200\n"
+"PO-Revision-Date: 2024-03-07 20:49+0100\n"
+"Last-Translator: Automatically generated\n"
+"Language-Team: none\n"
+"Language: lt\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
+"(n%100<10 || n%100>=20) ? 1 : 2);\n"
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:9
+#: src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c:227
+msgid "Video"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:17
+#: src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c:228
+msgid "Audio"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:25
+#: src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c:229
+msgid "Subtitles"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:28
+msgid "Enabled"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:32
+msgid "Open…"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:75
+msgid "Mute"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:119
+msgid "Reset"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-stream-check-button.c:82
+#: src/lib/clapper-gtk/clapper-gtk-stream-check-button.c:126
+msgid "Undetermined"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-stream-check-button.c:82
+msgid "Channels"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-title-label.c:91
+msgid "No media"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-title-label.c:103
+msgid "Unknown title"
+msgstr ""
diff --git a/src/lib/clapper-gtk/po/meson.build b/src/lib/clapper-gtk/po/meson.build
new file mode 100644
index 00000000..8072d4d9
--- /dev/null
+++ b/src/lib/clapper-gtk/po/meson.build
@@ -0,0 +1 @@
+i18n.gettext(meson.project_name() + '-gtk', preset: 'glib')
diff --git a/src/lib/clapper-gtk/po/nl.po b/src/lib/clapper-gtk/po/nl.po
new file mode 100644
index 00000000..ca71250d
--- /dev/null
+++ b/src/lib/clapper-gtk/po/nl.po
@@ -0,0 +1,66 @@
+# Dutch translations for clapper-gtk package.
+# Copyright (C) 2024 THE clapper-gtk'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the clapper-gtk package.
+# Automatically generated, 2024.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: clapper-gtk\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-04-05 20:50+0200\n"
+"PO-Revision-Date: 2024-03-07 20:49+0100\n"
+"Last-Translator: Automatically generated\n"
+"Language-Team: none\n"
+"Language: nl\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:9
+#: src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c:227
+msgid "Video"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:17
+#: src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c:228
+msgid "Audio"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:25
+#: src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c:229
+msgid "Subtitles"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:28
+msgid "Enabled"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:32
+msgid "Open…"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:75
+msgid "Mute"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:119
+msgid "Reset"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-stream-check-button.c:82
+#: src/lib/clapper-gtk/clapper-gtk-stream-check-button.c:126
+msgid "Undetermined"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-stream-check-button.c:82
+msgid "Channels"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-title-label.c:91
+msgid "No media"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-title-label.c:103
+msgid "Unknown title"
+msgstr ""
diff --git a/src/lib/clapper-gtk/po/pl.po b/src/lib/clapper-gtk/po/pl.po
new file mode 100644
index 00000000..c7b56599
--- /dev/null
+++ b/src/lib/clapper-gtk/po/pl.po
@@ -0,0 +1,67 @@
+# Polish translations for clapper-gtk package.
+# Copyright (C) 2024 THE clapper-gtk'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the clapper-gtk package.
+# Automatically generated, 2024.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: clapper-gtk\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-04-05 20:50+0200\n"
+"PO-Revision-Date: 2024-03-07 20:49+0100\n"
+"Last-Translator: Automatically generated\n"
+"Language-Team: none\n"
+"Language: pl\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 "
+"|| n%100>=20) ? 1 : 2);\n"
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:9
+#: src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c:227
+msgid "Video"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:17
+#: src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c:228
+msgid "Audio"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:25
+#: src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c:229
+msgid "Subtitles"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:28
+msgid "Enabled"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:32
+msgid "Open…"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:75
+msgid "Mute"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:119
+msgid "Reset"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-stream-check-button.c:82
+#: src/lib/clapper-gtk/clapper-gtk-stream-check-button.c:126
+msgid "Undetermined"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-stream-check-button.c:82
+msgid "Channels"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-title-label.c:91
+msgid "No media"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-title-label.c:103
+msgid "Unknown title"
+msgstr ""
diff --git a/src/lib/clapper-gtk/po/pt.po b/src/lib/clapper-gtk/po/pt.po
new file mode 100644
index 00000000..f5e0cc2e
--- /dev/null
+++ b/src/lib/clapper-gtk/po/pt.po
@@ -0,0 +1,66 @@
+# Portuguese translations for clapper-gtk package.
+# Copyright (C) 2024 THE clapper-gtk'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the clapper-gtk package.
+# Automatically generated, 2024.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: clapper-gtk\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-04-05 20:50+0200\n"
+"PO-Revision-Date: 2024-03-07 20:49+0100\n"
+"Last-Translator: Automatically generated\n"
+"Language-Team: none\n"
+"Language: pt\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:9
+#: src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c:227
+msgid "Video"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:17
+#: src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c:228
+msgid "Audio"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:25
+#: src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c:229
+msgid "Subtitles"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:28
+msgid "Enabled"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:32
+msgid "Open…"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:75
+msgid "Mute"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:119
+msgid "Reset"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-stream-check-button.c:82
+#: src/lib/clapper-gtk/clapper-gtk-stream-check-button.c:126
+msgid "Undetermined"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-stream-check-button.c:82
+msgid "Channels"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-title-label.c:91
+msgid "No media"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-title-label.c:103
+msgid "Unknown title"
+msgstr ""
diff --git a/src/lib/clapper-gtk/po/pt_BR.po b/src/lib/clapper-gtk/po/pt_BR.po
new file mode 100644
index 00000000..50508028
--- /dev/null
+++ b/src/lib/clapper-gtk/po/pt_BR.po
@@ -0,0 +1,67 @@
+# Portuguese translations for clapper-gtk package
+# Traduções em português brasileiro para o pacote clapper-gtk.
+# Copyright (C) 2024 THE clapper-gtk'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the clapper-gtk package.
+# Automatically generated, 2024.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: clapper-gtk\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-04-05 20:50+0200\n"
+"PO-Revision-Date: 2024-03-07 20:49+0100\n"
+"Last-Translator: Automatically generated\n"
+"Language-Team: none\n"
+"Language: pt_BR\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:9
+#: src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c:227
+msgid "Video"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:17
+#: src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c:228
+msgid "Audio"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:25
+#: src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c:229
+msgid "Subtitles"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:28
+msgid "Enabled"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:32
+msgid "Open…"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:75
+msgid "Mute"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:119
+msgid "Reset"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-stream-check-button.c:82
+#: src/lib/clapper-gtk/clapper-gtk-stream-check-button.c:126
+msgid "Undetermined"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-stream-check-button.c:82
+msgid "Channels"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-title-label.c:91
+msgid "No media"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-title-label.c:103
+msgid "Unknown title"
+msgstr ""
diff --git a/src/lib/clapper-gtk/po/ru.po b/src/lib/clapper-gtk/po/ru.po
new file mode 100644
index 00000000..89a3405d
--- /dev/null
+++ b/src/lib/clapper-gtk/po/ru.po
@@ -0,0 +1,67 @@
+# Russian translations for clapper-gtk package.
+# Copyright (C) 2024 THE clapper-gtk'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the clapper-gtk package.
+# Automatically generated, 2024.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: clapper-gtk\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-04-05 20:50+0200\n"
+"PO-Revision-Date: 2024-03-07 20:49+0100\n"
+"Last-Translator: Automatically generated\n"
+"Language-Team: none\n"
+"Language: ru\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
+"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:9
+#: src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c:227
+msgid "Video"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:17
+#: src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c:228
+msgid "Audio"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:25
+#: src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c:229
+msgid "Subtitles"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:28
+msgid "Enabled"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:32
+msgid "Open…"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:75
+msgid "Mute"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:119
+msgid "Reset"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-stream-check-button.c:82
+#: src/lib/clapper-gtk/clapper-gtk-stream-check-button.c:126
+msgid "Undetermined"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-stream-check-button.c:82
+msgid "Channels"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-title-label.c:91
+msgid "No media"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-title-label.c:103
+msgid "Unknown title"
+msgstr ""
diff --git a/src/lib/clapper-gtk/po/sk.po b/src/lib/clapper-gtk/po/sk.po
new file mode 100644
index 00000000..0d48f82a
--- /dev/null
+++ b/src/lib/clapper-gtk/po/sk.po
@@ -0,0 +1,66 @@
+# Slovak translations for clapper-gtk package.
+# Copyright (C) 2024 THE clapper-gtk'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the clapper-gtk package.
+# Automatically generated, 2024.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: clapper-gtk\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-04-05 20:50+0200\n"
+"PO-Revision-Date: 2024-03-07 20:49+0100\n"
+"Last-Translator: Automatically generated\n"
+"Language-Team: none\n"
+"Language: sk\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n"
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:9
+#: src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c:227
+msgid "Video"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:17
+#: src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c:228
+msgid "Audio"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:25
+#: src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c:229
+msgid "Subtitles"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:28
+msgid "Enabled"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:32
+msgid "Open…"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:75
+msgid "Mute"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:119
+msgid "Reset"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-stream-check-button.c:82
+#: src/lib/clapper-gtk/clapper-gtk-stream-check-button.c:126
+msgid "Undetermined"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-stream-check-button.c:82
+msgid "Channels"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-title-label.c:91
+msgid "No media"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-title-label.c:103
+msgid "Unknown title"
+msgstr ""
diff --git a/src/lib/clapper-gtk/po/sv.po b/src/lib/clapper-gtk/po/sv.po
new file mode 100644
index 00000000..f53d620c
--- /dev/null
+++ b/src/lib/clapper-gtk/po/sv.po
@@ -0,0 +1,66 @@
+# Swedish translations for clapper-gtk package.
+# Copyright (C) 2024 THE clapper-gtk'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the clapper-gtk package.
+# Automatically generated, 2024.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: clapper-gtk\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-04-05 20:50+0200\n"
+"PO-Revision-Date: 2024-03-07 20:49+0100\n"
+"Last-Translator: Automatically generated\n"
+"Language-Team: none\n"
+"Language: sv\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:9
+#: src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c:227
+msgid "Video"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:17
+#: src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c:228
+msgid "Audio"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:25
+#: src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c:229
+msgid "Subtitles"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:28
+msgid "Enabled"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:32
+msgid "Open…"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:75
+msgid "Mute"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:119
+msgid "Reset"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-stream-check-button.c:82
+#: src/lib/clapper-gtk/clapper-gtk-stream-check-button.c:126
+msgid "Undetermined"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-stream-check-button.c:82
+msgid "Channels"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-title-label.c:91
+msgid "No media"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-title-label.c:103
+msgid "Unknown title"
+msgstr ""
diff --git a/src/lib/clapper-gtk/po/tr.po b/src/lib/clapper-gtk/po/tr.po
new file mode 100644
index 00000000..143ea047
--- /dev/null
+++ b/src/lib/clapper-gtk/po/tr.po
@@ -0,0 +1,66 @@
+# Turkish translations for clapper-gtk package.
+# Copyright (C) 2024 THE clapper-gtk'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the clapper-gtk package.
+# Automatically generated, 2024.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: clapper-gtk\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-04-05 20:50+0200\n"
+"PO-Revision-Date: 2024-03-07 20:49+0100\n"
+"Last-Translator: Automatically generated\n"
+"Language-Team: none\n"
+"Language: tr\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:9
+#: src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c:227
+msgid "Video"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:17
+#: src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c:228
+msgid "Audio"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:25
+#: src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c:229
+msgid "Subtitles"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:28
+msgid "Enabled"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:32
+msgid "Open…"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:75
+msgid "Mute"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:119
+msgid "Reset"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-stream-check-button.c:82
+#: src/lib/clapper-gtk/clapper-gtk-stream-check-button.c:126
+msgid "Undetermined"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-stream-check-button.c:82
+msgid "Channels"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-title-label.c:91
+msgid "No media"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-title-label.c:103
+msgid "Unknown title"
+msgstr ""
diff --git a/src/lib/clapper-gtk/po/zh_CN.po b/src/lib/clapper-gtk/po/zh_CN.po
new file mode 100644
index 00000000..e5b6567f
--- /dev/null
+++ b/src/lib/clapper-gtk/po/zh_CN.po
@@ -0,0 +1,66 @@
+# Chinese translations for clapper-gtk package
+# clapper-gtk Èí¼þ°üµÄ¼òÌåÖÐÎÄ·Òë.
+# Copyright (C) 2024 THE clapper-gtk'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the clapper-gtk package.
+# Automatically generated, 2024.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: clapper-gtk\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-04-05 20:50+0200\n"
+"PO-Revision-Date: 2024-03-07 20:49+0100\n"
+"Last-Translator: Automatically generated\n"
+"Language-Team: none\n"
+"Language: zh_CN\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:9
+#: src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c:227
+msgid "Video"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:17
+#: src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c:228
+msgid "Audio"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:25
+#: src/lib/clapper-gtk/clapper-gtk-extra-menu-button.c:229
+msgid "Subtitles"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:28
+msgid "Enabled"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:32
+msgid "Open…"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:75
+msgid "Mute"
+msgstr ""
+
+#: src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui:119
+msgid "Reset"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-stream-check-button.c:82
+#: src/lib/clapper-gtk/clapper-gtk-stream-check-button.c:126
+msgid "Undetermined"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-stream-check-button.c:82
+msgid "Channels"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-title-label.c:91
+msgid "No media"
+msgstr ""
+
+#: src/lib/clapper-gtk/clapper-gtk-title-label.c:103
+msgid "Unknown title"
+msgstr ""
diff --git a/src/lib/clapper-gtk/ui/clapper-gtk-billboard.ui b/src/lib/clapper-gtk/ui/clapper-gtk-billboard.ui
new file mode 100644
index 00000000..e1156608
--- /dev/null
+++ b/src/lib/clapper-gtk/ui/clapper-gtk-billboard.ui
@@ -0,0 +1,161 @@
+
+
+
+ fill
+ center
+ 422
+ 420
+ false
+
+
+
+ horizontal
+ fill
+ center
+ true
+
+
+ start
+ center
+ crossfade
+ 500
+ false
+
+
+
+ toggle-play
+
+
+ vertical
+ start
+
+
+
+ fill
+ end
+ slide-up
+ 200
+ true
+
+
+ vertical
+ center
+ fill
+ 4
+
+
+
+ vertical
+ center
+ fill
+ true
+ true
+
+
+
+
+ vertical
+ center
+ fill
+ true
+ true
+
+
+
+
+
+
+
+
+ horizontal
+ start
+ center
+
+
+ center
+ center
+
+
+
+
+ start
+ fill
+ slide-right
+
+ false
+
+
+ center
+ 100%
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ center
+ center
+ crossfade
+ 500
+
+
+
+ 280
+
+
+ vertical
+ center
+ center
+ 8
+
+
+
+ center
+ center
+
+
+
+
+ center
+ center
+ center
+ true
+ word-char
+ word
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui b/src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui
new file mode 100644
index 00000000..92604fc2
--- /dev/null
+++ b/src/lib/clapper-gtk/ui/clapper-gtk-extra-menu-button.ui
@@ -0,0 +1,254 @@
+
+
+
+
+
diff --git a/src/lib/clapper-gtk/ui/clapper-gtk-seek-bar.ui b/src/lib/clapper-gtk/ui/clapper-gtk-seek-bar.ui
new file mode 100644
index 00000000..264ecb49
--- /dev/null
+++ b/src/lib/clapper-gtk/ui/clapper-gtk-seek-bar.ui
@@ -0,0 +1,83 @@
+
+
+
+ true
+
+
+ end
+ slide-left
+ 800
+
+
+ center
+ 00:00
+
+
+
+
+
+
+
+ horizontal
+ fill
+ center
+ true
+ false
+ false
+ position_adjustment
+
+
+
+
+
+
+ top
+ false
+
+
+ center
+
+
+
+
+
+
+ start
+ slide-right
+ 800
+
+
+
+ center
+ 00:00
+
+
+
+
+
+
+
+
+
+
+
+
+
+ capture
+ true
+
+
+
+
+
+ 0
+ 0
+ 0
+ 0
+ 8
+
+
diff --git a/src/lib/clapper-gtk/ui/clapper-gtk-simple-controls.ui b/src/lib/clapper-gtk/ui/clapper-gtk-simple-controls.ui
new file mode 100644
index 00000000..62057441
--- /dev/null
+++ b/src/lib/clapper-gtk/ui/clapper-gtk-simple-controls.ui
@@ -0,0 +1,137 @@
+
+
+
+ center
+ end
+ 560
+
+
+
+ 400
+
+
+
+ horizontal
+ center
+ end
+
+
+
+ 944
+
+
+ vertical
+ fill
+ center
+
+
+ end
+ center
+ crossfade
+ 200
+ false
+ false
+
+
+ end
+ center
+
+
+
+
+
+
+
+
+ toggle-play|seek-request
+
+
+ horizontal
+ fill
+ center
+
+
+
+ center
+ center
+
+
+
+
+
+ fill
+ center
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ end
+ end
+ slide-left
+ 400
+ true
+
+
+
+
+ center
+ end
+ crossfade
+ 200
+ true
+
+
+ center
+ end
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/lib/clapper-gtk/ui/clapper-gtk-status.ui b/src/lib/clapper-gtk/ui/clapper-gtk-status.ui
new file mode 100644
index 00000000..e33dd04c
--- /dev/null
+++ b/src/lib/clapper-gtk/ui/clapper-gtk-status.ui
@@ -0,0 +1,45 @@
+
+
+
+ 562
+ 402
+ 560
+ 400
+
+
+
+ vertical
+ center
+ center
+
+
+ center
+ center
+
+
+
+
+ center
+ center
+ center
+ true
+
+
+
+
+
+ center
+ center
+ center
+ true
+
+
+
+
+
+
+
diff --git a/src/lib/clapper-gtk/ui/clapper-gtk-stream-list-item.ui b/src/lib/clapper-gtk/ui/clapper-gtk-stream-list-item.ui
new file mode 100644
index 00000000..5b453790
--- /dev/null
+++ b/src/lib/clapper-gtk/ui/clapper-gtk-stream-list-item.ui
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+ GtkListItem
+
+
+ GtkListItem
+
+
+
+
+
diff --git a/src/lib/clapper-gtk/ui/clapper-gtk-title-header.ui b/src/lib/clapper-gtk/ui/clapper-gtk-title-header.ui
new file mode 100644
index 00000000..fae5d27b
--- /dev/null
+++ b/src/lib/clapper-gtk/ui/clapper-gtk-title-header.ui
@@ -0,0 +1,22 @@
+
+
+
+
diff --git a/src/lib/clapper-gtk/ui/clapper-gtk-video.ui b/src/lib/clapper-gtk/ui/clapper-gtk-video.ui
new file mode 100644
index 00000000..8ee8db7d
--- /dev/null
+++ b/src/lib/clapper-gtk/ui/clapper-gtk-video.ui
@@ -0,0 +1,48 @@
+
+
+
+
+
+ center
+ center
+ false
+
+
+
+
+ center
+ center
+ false
+
+
+
+
+ capture
+ true
+
+
+
+
+
+
+ 1
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/lib/meson.build b/src/lib/meson.build
index 95ce0967..70569fb3 100644
--- a/src/lib/meson.build
+++ b/src/lib/meson.build
@@ -12,3 +12,4 @@ gir_init_section = '--add-init-section=extern void gst_init(gint*,gchar**);' + \
subdir('gst')
subdir('clapper')
+subdir('clapper-gtk')