/* 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 can_scrub; gboolean scrubbing; 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 (gtk_widget_get_default_direction () == GTK_TEXT_DIR_RTL) x = max_pointing_val + min_pointing_val - x; 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 scale_scroll_begin_cb (GtkEventControllerScroll *scroll, ClapperGtkSeekBar *self) { self->can_scrub = TRUE; } static gboolean scale_scroll_cb (GtkEventControllerScroll *scroll, gdouble dx, gdouble dy, ClapperGtkSeekBar *self) { if (self->can_scrub && !self->scrubbing) { GST_DEBUG_OBJECT (self, "Scrubbing start"); self->scrubbing = TRUE; gtk_widget_add_css_class (self->scale, "dragging"); return TRUE; } return FALSE; } static void scale_scroll_end_cb (GtkEventControllerScroll *scroll, ClapperGtkSeekBar *self) { if (self->scrubbing) { GST_DEBUG_OBJECT (self, "Scrubbing end"); gtk_widget_remove_css_class (self->scale, "dragging"); self->scrubbing = FALSE; } self->can_scrub = FALSE; } 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; if (gtk_widget_get_default_direction () == GTK_TEXT_DIR_RTL) pointing_val = upper - pointing_val; 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, scale_scroll_begin_cb); gtk_widget_class_bind_template_callback (widget_class, scale_scroll_cb); gtk_widget_class_bind_template_callback (widget_class, scale_scroll_end_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"); }