mirror of
https://github.com/Rafostar/clapper.git
synced 2025-08-29 23:32:04 +02:00
In addition to GtkScrolledWindow, when also hovering over GtkRange subclassing widgets like GtkScale, do not trigger accidentally default app window scroll handler. We want the hovered upon widget and only that widget to handle scroll events in such case.
1154 lines
34 KiB
C
1154 lines
34 KiB
C
/* Clapper Application
|
|
* Copyright (C) 2024 Rafał Dzięgiel <rafostar.github@gmail.com>
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program 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 General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include "config.h"
|
|
|
|
#include <math.h>
|
|
#include <glib/gi18n.h>
|
|
#include <gdk/gdk.h>
|
|
#include <adwaita.h>
|
|
#include <clapper/clapper.h>
|
|
#include <clapper-gtk/clapper-gtk.h>
|
|
|
|
#include "clapper-app-window.h"
|
|
#include "clapper-app-file-dialog.h"
|
|
#include "clapper-app-utils.h"
|
|
|
|
#define DEFAULT_WINDOW_WIDTH 1024
|
|
#define DEFAULT_WINDOW_HEIGHT 576
|
|
|
|
#define N_PROGRESSION_MODES 5
|
|
|
|
#define CLAPPER_APP_SEEK_UNIT_SECOND 0
|
|
#define CLAPPER_APP_SEEK_UNIT_MINUTE 1
|
|
#define CLAPPER_APP_SEEK_UNIT_PERCENTAGE 2
|
|
|
|
#define PERCENTAGE_ROUND(a) (round ((gdouble) a / 0.01) * 0.01)
|
|
#define AXIS_WINS_OVER(a,b) ((a > 0 && a - 0.3 > b) || (a < 0 && a + 0.3 < b))
|
|
|
|
#define GST_CAT_DEFAULT clapper_app_window_debug
|
|
GST_DEBUG_CATEGORY_STATIC (GST_CAT_DEFAULT);
|
|
|
|
struct _ClapperAppWindow
|
|
{
|
|
GtkApplicationWindow parent;
|
|
|
|
GtkWidget *video;
|
|
ClapperGtkBillboard *billboard;
|
|
ClapperGtkSimpleControls *simple_controls;
|
|
|
|
GtkDropTarget *drop_target;
|
|
GtkCssProvider *provider;
|
|
|
|
ClapperMediaItem *current_item;
|
|
|
|
GSettings *settings;
|
|
|
|
guint seek_timeout;
|
|
|
|
gboolean key_held;
|
|
gboolean scrolling;
|
|
gboolean seeking;
|
|
|
|
gboolean was_playing;
|
|
gdouble pending_position;
|
|
gdouble current_duration;
|
|
|
|
gdouble last_volume;
|
|
};
|
|
|
|
#define parent_class clapper_app_window_parent_class
|
|
G_DEFINE_TYPE (ClapperAppWindow, clapper_app_window, GTK_TYPE_APPLICATION_WINDOW)
|
|
|
|
static void
|
|
_media_item_title_changed_cb (ClapperMediaItem *item,
|
|
GParamSpec *pspec G_GNUC_UNUSED, ClapperAppWindow *self)
|
|
{
|
|
gchar *title;
|
|
|
|
if ((title = clapper_media_item_get_title (item))) {
|
|
gtk_window_set_title (GTK_WINDOW (self), title);
|
|
g_free (title);
|
|
} else {
|
|
gtk_window_set_title (GTK_WINDOW (self), CLAPPER_APP_NAME);
|
|
}
|
|
}
|
|
|
|
static void
|
|
_queue_current_item_changed_cb (ClapperQueue *queue,
|
|
GParamSpec *pspec G_GNUC_UNUSED, ClapperAppWindow *self)
|
|
{
|
|
ClapperMediaItem *current_item = clapper_queue_get_current_item (queue);
|
|
|
|
/* 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_OBJECT (self, "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);
|
|
_media_item_title_changed_cb (self->current_item, NULL, self);
|
|
} else {
|
|
gtk_window_set_title (GTK_WINDOW (self), CLAPPER_APP_NAME);
|
|
}
|
|
|
|
gst_clear_object (¤t_item);
|
|
}
|
|
|
|
static gboolean
|
|
_get_seek_method_mapping (GValue *value,
|
|
GVariant *variant, gpointer user_data G_GNUC_UNUSED)
|
|
{
|
|
ClapperPlayerSeekMethod seek_method;
|
|
|
|
seek_method = (ClapperPlayerSeekMethod) g_variant_get_int32 (variant);
|
|
g_value_set_enum (value, seek_method);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static GtkWidget *
|
|
_pick_pointer_widget (ClapperAppWindow *self)
|
|
{
|
|
GdkSurface *surface = gtk_native_get_surface (GTK_NATIVE (self));
|
|
GdkDisplay *display = gtk_widget_get_display (GTK_WIDGET (self));
|
|
GdkSeat *seat = gdk_display_get_default_seat (display);
|
|
GtkWidget *widget = NULL;
|
|
|
|
if (G_LIKELY (seat != NULL)) {
|
|
GdkDevice *device = gdk_seat_get_pointer (seat);
|
|
gdouble px = 0, py = 0, native_x = 0, native_y = 0;
|
|
|
|
if (G_LIKELY (device != NULL))
|
|
gdk_surface_get_device_position (surface, device, &px, &py, NULL);
|
|
|
|
gtk_native_get_surface_transform (GTK_NATIVE (self), &native_x, &native_y);
|
|
|
|
widget = gtk_widget_pick (GTK_WIDGET (self),
|
|
px - native_x, py - native_y, GTK_PICK_DEFAULT);
|
|
}
|
|
|
|
return widget;
|
|
}
|
|
|
|
static void
|
|
_player_volume_changed_cb (ClapperPlayer *player,
|
|
GParamSpec *pspec G_GNUC_UNUSED, ClapperAppWindow *self)
|
|
{
|
|
gdouble volume = PERCENTAGE_ROUND (clapper_player_get_volume (player));
|
|
|
|
/* Only notify when volume changes at least 1%. Remembering last volume
|
|
* also prevents us from showing volume when it is restored on startup. */
|
|
if (volume != self->last_volume) {
|
|
clapper_gtk_billboard_announce_volume (self->billboard);
|
|
self->last_volume = volume;
|
|
}
|
|
}
|
|
|
|
static void
|
|
_player_speed_changed_cb (ClapperPlayer *player G_GNUC_UNUSED,
|
|
GParamSpec *pspec G_GNUC_UNUSED, ClapperAppWindow *self)
|
|
{
|
|
clapper_gtk_billboard_announce_speed (self->billboard);
|
|
}
|
|
|
|
static void
|
|
video_toggle_fullscreen_cb (ClapperGtkVideo *video, ClapperAppWindow *self)
|
|
{
|
|
GtkWindow *window = GTK_WINDOW (self);
|
|
|
|
g_object_set (window, "fullscreened", !gtk_window_is_fullscreen (window), NULL);
|
|
}
|
|
|
|
static void
|
|
video_map_cb (GtkWidget *widget, ClapperAppWindow *self)
|
|
{
|
|
ClapperPlayer *player;
|
|
gdouble speed;
|
|
|
|
GST_TRACE_OBJECT (self, "Video map");
|
|
|
|
player = clapper_gtk_video_get_player (CLAPPER_GTK_VIDEO_CAST (self->video));
|
|
|
|
g_signal_connect (player, "notify::volume",
|
|
G_CALLBACK (_player_volume_changed_cb), self);
|
|
g_signal_connect (player, "notify::speed",
|
|
G_CALLBACK (_player_speed_changed_cb), self);
|
|
|
|
speed = clapper_player_get_speed (player);
|
|
|
|
/* If we are starting with non-1x speed, notify user about it */
|
|
if (!G_APPROX_VALUE (speed, 1.0, FLT_EPSILON))
|
|
clapper_gtk_billboard_announce_speed (self->billboard);
|
|
}
|
|
|
|
static void
|
|
video_unmap_cb (GtkWidget *widget, ClapperAppWindow *self)
|
|
{
|
|
ClapperPlayer *player;
|
|
|
|
GST_TRACE_OBJECT (self, "Video unmap");
|
|
|
|
player = clapper_gtk_video_get_player (CLAPPER_GTK_VIDEO_CAST (self->video));
|
|
|
|
g_signal_handlers_disconnect_by_func (player, _player_volume_changed_cb, self);
|
|
g_signal_handlers_disconnect_by_func (player, _player_speed_changed_cb, self);
|
|
}
|
|
|
|
static void
|
|
_open_subtitles_cb (ClapperGtkExtraMenuButton *button G_GNUC_UNUSED,
|
|
ClapperMediaItem *item, ClapperAppWindow *self)
|
|
{
|
|
GtkApplication *gtk_app = gtk_window_get_application (GTK_WINDOW (self));
|
|
clapper_app_file_dialog_open_subtitles (gtk_app, item);
|
|
}
|
|
|
|
static void
|
|
right_click_pressed_cb (GtkGestureClick *click, gint n_press,
|
|
gdouble x, gdouble y, ClapperAppWindow *self)
|
|
{
|
|
GdkCursor *cursor;
|
|
const gchar *cursor_name = NULL;
|
|
|
|
GST_LOG_OBJECT (self, "Right click pressed");
|
|
|
|
if ((cursor = gtk_widget_get_cursor (self->video)))
|
|
cursor_name = gdk_cursor_get_name (cursor);
|
|
|
|
/* Restore cursor if faded on video */
|
|
if (g_strcmp0 (cursor_name, "none") == 0) {
|
|
GdkCursor *new_cursor = gdk_cursor_new_from_name ("default", NULL);
|
|
|
|
gtk_widget_set_cursor (self->video, new_cursor);
|
|
g_object_unref (new_cursor);
|
|
}
|
|
}
|
|
|
|
static void
|
|
right_click_released_cb (GtkGestureClick *click, gint n_press,
|
|
gdouble x, gdouble y, ClapperAppWindow *self)
|
|
{
|
|
GdkSurface *surface;
|
|
GdkEventSequence *sequence;
|
|
GdkEvent *event;
|
|
|
|
GST_LOG_OBJECT (self, "Right click released");
|
|
|
|
surface = gtk_native_get_surface (GTK_NATIVE (self));
|
|
sequence = gtk_gesture_single_get_current_sequence (GTK_GESTURE_SINGLE (click));
|
|
event = gtk_gesture_get_last_event (GTK_GESTURE (click), sequence);
|
|
|
|
if (G_UNLIKELY (event == NULL))
|
|
return;
|
|
|
|
if (!gdk_toplevel_show_window_menu (GDK_TOPLEVEL (surface), event))
|
|
GST_FIXME_OBJECT (self, "Implement fallback context menu");
|
|
|
|
gtk_gesture_set_state (GTK_GESTURE (click), GTK_EVENT_SEQUENCE_CLAIMED);
|
|
}
|
|
|
|
static void
|
|
drag_begin_cb (GtkGestureDrag *drag,
|
|
gdouble start_x, gdouble start_y, ClapperAppWindow *self)
|
|
{
|
|
GtkWidget *widget, *pickup;
|
|
|
|
widget = gtk_event_controller_get_widget (GTK_EVENT_CONTROLLER (drag));
|
|
pickup = gtk_widget_pick (widget, start_x, start_y, GTK_PICK_DEFAULT);
|
|
|
|
/* We do not want to cause drag on list view as it has
|
|
* a GtkDragSource controller which acts on delay */
|
|
if (GTK_IS_LIST_VIEW (pickup) || gtk_widget_get_ancestor (pickup, GTK_TYPE_LIST_VIEW)) {
|
|
gtk_gesture_set_state (GTK_GESTURE (drag), GTK_EVENT_SEQUENCE_DENIED);
|
|
gtk_event_controller_reset (GTK_EVENT_CONTROLLER (drag));
|
|
|
|
GST_DEBUG_OBJECT (self, "Window drag denied");
|
|
}
|
|
}
|
|
|
|
static void
|
|
drag_update_cb (GtkGestureDrag *drag,
|
|
gdouble offset_x, gdouble offset_y, ClapperAppWindow *self)
|
|
{
|
|
GtkSettings *settings = gtk_widget_get_settings (GTK_WIDGET (self));
|
|
gint drag_threshold = 8; // initially set to default
|
|
|
|
g_object_get (settings, "gtk-dnd-drag-threshold", &drag_threshold, NULL);
|
|
|
|
if (ABS (offset_x) > drag_threshold || ABS (offset_y) > drag_threshold) {
|
|
GdkSurface *surface = gtk_native_get_surface (GTK_NATIVE (self));
|
|
gdouble start_x = 0, start_y = 0, native_x = 0, native_y = 0;
|
|
|
|
gtk_gesture_set_state (GTK_GESTURE (drag), GTK_EVENT_SEQUENCE_CLAIMED);
|
|
gtk_gesture_drag_get_start_point (drag, &start_x, &start_y);
|
|
|
|
gtk_native_get_surface_transform (GTK_NATIVE (self), &native_x, &native_y);
|
|
|
|
gdk_toplevel_begin_move (GDK_TOPLEVEL (surface),
|
|
gtk_gesture_get_device (GTK_GESTURE (drag)),
|
|
GDK_BUTTON_PRIMARY,
|
|
start_x + native_x,
|
|
start_y + native_y,
|
|
gtk_event_controller_get_current_event_time (GTK_EVENT_CONTROLLER (drag)));
|
|
|
|
gtk_event_controller_reset (GTK_EVENT_CONTROLLER (drag));
|
|
}
|
|
}
|
|
|
|
static inline void
|
|
_alter_volume (ClapperAppWindow *self, gdouble dy)
|
|
{
|
|
ClapperPlayer *player = clapper_gtk_video_get_player (CLAPPER_GTK_VIDEO_CAST (self->video));
|
|
gdouble volume = clapper_player_get_volume (player);
|
|
|
|
/* We do not want for volume to change too suddenly */
|
|
if (dy > 2.0)
|
|
dy = 2.0;
|
|
else if (dy < -2.0)
|
|
dy = -2.0;
|
|
|
|
volume -= dy * 0.02;
|
|
|
|
/* Prevent going out of range and make it easier to set exactly 100% */
|
|
if (volume > 2.0)
|
|
volume = 2.0;
|
|
else if (volume < 0.0)
|
|
volume = 0.0;
|
|
|
|
clapper_player_set_volume (player, PERCENTAGE_ROUND (volume));
|
|
}
|
|
|
|
static inline void
|
|
_alter_speed (ClapperAppWindow *self, gdouble dx)
|
|
{
|
|
ClapperPlayer *player = clapper_gtk_video_get_player (CLAPPER_GTK_VIDEO_CAST (self->video));
|
|
gdouble speed = clapper_player_get_speed (player);
|
|
|
|
speed -= dx * 0.02;
|
|
|
|
/* Prevent going out of range and make it easier to set exactly 1.0x */
|
|
if (speed > 2.0)
|
|
speed = 2.0;
|
|
else if (speed < 0.05)
|
|
speed = 0.05;
|
|
|
|
clapper_player_set_speed (player, PERCENTAGE_ROUND (speed));
|
|
}
|
|
|
|
static gboolean
|
|
_begin_seek_operation (ClapperAppWindow *self)
|
|
{
|
|
ClapperPlayer *player;
|
|
ClapperQueue *queue;
|
|
ClapperMediaItem *current_item;
|
|
|
|
if (self->seeking)
|
|
return FALSE;
|
|
|
|
player = clapper_gtk_video_get_player (
|
|
CLAPPER_GTK_VIDEO_CAST (self->video));
|
|
queue = clapper_player_get_queue (player);
|
|
current_item = clapper_queue_get_current_item (queue);
|
|
|
|
self->current_duration = (current_item != NULL)
|
|
? clapper_media_item_get_duration (current_item)
|
|
: 0;
|
|
|
|
gst_clear_object (¤t_item);
|
|
|
|
/* Live content or not a video */
|
|
if (self->current_duration == 0)
|
|
return FALSE;
|
|
|
|
if ((self->was_playing = (
|
|
clapper_player_get_state (player) == CLAPPER_PLAYER_STATE_PLAYING)))
|
|
clapper_player_pause (player);
|
|
|
|
self->pending_position = clapper_player_get_position (player);
|
|
self->seeking = TRUE;
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static void
|
|
_end_seek_operation (ClapperAppWindow *self)
|
|
{
|
|
if (self->seeking && self->current_duration > 0) {
|
|
ClapperPlayer *player = clapper_gtk_video_get_player (
|
|
CLAPPER_GTK_VIDEO_CAST (self->video));
|
|
|
|
clapper_player_seek_custom (player, self->pending_position,
|
|
g_settings_get_int (self->settings, "seek-method"));
|
|
|
|
if (self->was_playing)
|
|
clapper_player_play (player);
|
|
}
|
|
|
|
/* Reset */
|
|
self->was_playing = FALSE;
|
|
self->pending_position = 0;
|
|
self->current_duration = 0;
|
|
|
|
self->seeking = FALSE;
|
|
}
|
|
|
|
static void
|
|
_announce_current_seek_position (ClapperAppWindow *self, gboolean forward)
|
|
{
|
|
gchar *position_str = g_strdup_printf (
|
|
"%" CLAPPER_TIME_FORMAT " / %" CLAPPER_TIME_FORMAT,
|
|
CLAPPER_TIME_ARGS (self->pending_position),
|
|
CLAPPER_TIME_ARGS (self->current_duration));
|
|
|
|
clapper_gtk_billboard_post_message (self->billboard,
|
|
(forward) ? "media-seek-forward-symbolic" : "media-seek-backward-symbolic",
|
|
position_str);
|
|
|
|
g_free (position_str);
|
|
}
|
|
|
|
static inline void
|
|
_alter_position (ClapperAppWindow *self, gdouble dx)
|
|
{
|
|
gboolean forward;
|
|
|
|
/* This can only work on devices that
|
|
* can detect scrolling begin and end */
|
|
if (!self->scrolling
|
|
|| (!self->seeking && !_begin_seek_operation (self)))
|
|
return;
|
|
|
|
forward = (dx > 0);
|
|
self->pending_position += dx;
|
|
|
|
if (!forward) {
|
|
if (self->pending_position < 0)
|
|
self->pending_position = 0;
|
|
} else {
|
|
if (self->pending_position > self->current_duration)
|
|
self->pending_position = self->current_duration;
|
|
}
|
|
|
|
_announce_current_seek_position (self, forward);
|
|
}
|
|
|
|
static void
|
|
scroll_begin_cb (GtkEventControllerScroll *scroll, ClapperAppWindow *self)
|
|
{
|
|
GST_LOG_OBJECT (self, "Scroll begin");
|
|
|
|
/* Assume that if device can begin, it can also end */
|
|
self->scrolling = TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
scroll_cb (GtkEventControllerScroll *scroll,
|
|
gdouble dx, gdouble dy, ClapperAppWindow *self)
|
|
{
|
|
GtkWidget *pickup;
|
|
GdkDevice *device;
|
|
gboolean handled;
|
|
|
|
pickup = _pick_pointer_widget (self);
|
|
|
|
/* We do not want to accidentally allow this controller to handle
|
|
* scrolls when hovering over widgets that also handle scroll */
|
|
while (pickup && !CLAPPER_GTK_IS_VIDEO (pickup)) {
|
|
if (GTK_IS_SCROLLED_WINDOW (pickup) || GTK_IS_RANGE (pickup))
|
|
return FALSE;
|
|
|
|
pickup = gtk_widget_get_parent (pickup);
|
|
}
|
|
|
|
device = gtk_event_controller_get_current_event_device (GTK_EVENT_CONTROLLER (scroll));
|
|
|
|
switch (gdk_device_get_source (device)) {
|
|
case GDK_SOURCE_TOUCHPAD:
|
|
case GDK_SOURCE_TOUCHSCREEN:
|
|
dx *= 0.4;
|
|
dy *= 0.4;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if ((handled = AXIS_WINS_OVER (dy, dx)))
|
|
_alter_volume (self, dy);
|
|
else if ((handled = AXIS_WINS_OVER (dx, dy)))
|
|
_alter_position (self, dx);
|
|
|
|
return handled;
|
|
}
|
|
|
|
static void
|
|
scroll_end_cb (GtkEventControllerScroll *scroll, ClapperAppWindow *self)
|
|
{
|
|
GST_LOG_OBJECT (self, "Scroll end");
|
|
|
|
self->scrolling = FALSE;
|
|
|
|
if (self->seeking)
|
|
_end_seek_operation (self);
|
|
}
|
|
|
|
static void
|
|
_handle_seek_key_press (ClapperAppWindow *self, gboolean forward)
|
|
{
|
|
gint unit;
|
|
gdouble offset;
|
|
|
|
if (!self->seeking && !_begin_seek_operation (self))
|
|
return;
|
|
|
|
offset = (gdouble) g_settings_get_int (self->settings, "seek-value");
|
|
unit = g_settings_get_int (self->settings, "seek-unit");
|
|
|
|
switch (unit) {
|
|
case CLAPPER_APP_SEEK_UNIT_SECOND:
|
|
break;
|
|
case CLAPPER_APP_SEEK_UNIT_MINUTE:
|
|
offset *= 60;
|
|
break;
|
|
case CLAPPER_APP_SEEK_UNIT_PERCENTAGE:
|
|
offset = (offset / 100.) * self->current_duration;
|
|
break;
|
|
default:
|
|
g_assert_not_reached ();
|
|
break;
|
|
}
|
|
|
|
forward ^= (gtk_widget_get_default_direction () == GTK_TEXT_DIR_RTL);
|
|
|
|
if (forward)
|
|
self->pending_position += offset;
|
|
else
|
|
self->pending_position -= offset;
|
|
|
|
if (!forward) {
|
|
if (self->pending_position < 0)
|
|
self->pending_position = 0;
|
|
} else {
|
|
if (self->pending_position > self->current_duration)
|
|
self->pending_position = self->current_duration;
|
|
}
|
|
|
|
_announce_current_seek_position (self, forward);
|
|
}
|
|
|
|
static void
|
|
_handle_chapter_key_press (ClapperAppWindow *self, gboolean forward)
|
|
{
|
|
ClapperPlayer *player = clapper_gtk_video_get_player (
|
|
CLAPPER_GTK_VIDEO_CAST (self->video));
|
|
ClapperQueue *queue = clapper_player_get_queue (player);
|
|
ClapperMediaItem *current_item = clapper_queue_get_current_item (queue);
|
|
ClapperTimeline *timeline;
|
|
ClapperMarker *dest_marker = NULL;
|
|
gdouble position;
|
|
guint i;
|
|
gboolean is_rtl;
|
|
|
|
if (!current_item)
|
|
return;
|
|
|
|
timeline = clapper_media_item_get_timeline (current_item);
|
|
i = clapper_timeline_get_n_markers (timeline);
|
|
|
|
/* No markers to iterate */
|
|
if (i == 0) {
|
|
gst_object_unref (current_item);
|
|
return;
|
|
}
|
|
|
|
is_rtl = (gtk_widget_get_default_direction () == GTK_TEXT_DIR_RTL);
|
|
forward ^= is_rtl;
|
|
position = clapper_player_get_position (player);
|
|
|
|
/* When going backwards give small tolerance, so we can
|
|
* still go to previous one even when directly at/after marker */
|
|
if (!forward)
|
|
position -= 1.5;
|
|
|
|
while (i--) {
|
|
ClapperMarker *marker = clapper_timeline_get_marker (timeline, i);
|
|
ClapperMarkerType marker_type = clapper_marker_get_marker_type (marker);
|
|
gdouble start;
|
|
gboolean found = FALSE;
|
|
|
|
/* Ignore custom markers */
|
|
if (marker_type >= CLAPPER_MARKER_TYPE_CUSTOM_1) {
|
|
gst_object_unref (marker);
|
|
continue;
|
|
}
|
|
|
|
start = clapper_marker_get_start (marker);
|
|
found = (start <= position);
|
|
|
|
if (found) {
|
|
if (!forward)
|
|
dest_marker = marker;
|
|
else
|
|
gst_object_unref (marker);
|
|
|
|
break;
|
|
}
|
|
|
|
if (forward)
|
|
gst_object_replace ((GstObject **) &dest_marker, GST_OBJECT_CAST (marker));
|
|
|
|
gst_object_unref (marker);
|
|
}
|
|
|
|
if (dest_marker) {
|
|
const gchar *title;
|
|
gdouble start, duration;
|
|
gchar *text;
|
|
|
|
title = clapper_marker_get_title (dest_marker);
|
|
start = clapper_marker_get_start (dest_marker);
|
|
duration = clapper_media_item_get_duration (current_item);
|
|
|
|
/* XXX: When RTL with mixed numbers and text, we have to
|
|
* switch positions of start <-> duration ourselves */
|
|
text = g_strdup_printf (
|
|
"%s\n%" CLAPPER_TIME_FORMAT " / %" CLAPPER_TIME_FORMAT, title,
|
|
CLAPPER_TIME_ARGS ((!is_rtl) ? start : duration),
|
|
CLAPPER_TIME_ARGS ((!is_rtl) ? duration : start));
|
|
|
|
clapper_gtk_billboard_post_message (self->billboard,
|
|
"user-bookmarks-symbolic", text);
|
|
clapper_player_seek (player, start);
|
|
|
|
g_free (text);
|
|
gst_object_unref (dest_marker);
|
|
}
|
|
|
|
gst_object_unref (current_item);
|
|
}
|
|
|
|
static void
|
|
_handle_item_key_press (ClapperAppWindow *self, gboolean forward)
|
|
{
|
|
ClapperPlayer *player = clapper_gtk_video_get_player (
|
|
CLAPPER_GTK_VIDEO_CAST (self->video));
|
|
ClapperQueue *queue = clapper_player_get_queue (player);
|
|
guint prev_index, index;
|
|
|
|
forward ^= (gtk_widget_get_default_direction () == GTK_TEXT_DIR_RTL);
|
|
|
|
prev_index = clapper_queue_get_current_index (queue);
|
|
gtk_widget_activate_action (self->video,
|
|
(forward) ? "video.next-item" : "video.previous-item", NULL);
|
|
index = clapper_queue_get_current_index (queue);
|
|
|
|
/* Notify only when changed */
|
|
if (prev_index != index) {
|
|
clapper_gtk_billboard_post_message (self->billboard,
|
|
"applications-multimedia-symbolic",
|
|
gtk_window_get_title (GTK_WINDOW (self)));
|
|
}
|
|
}
|
|
|
|
static void
|
|
_handle_speed_key_press (ClapperAppWindow *self, gboolean forward)
|
|
{
|
|
forward ^= (gtk_widget_get_default_direction () == GTK_TEXT_DIR_RTL);
|
|
|
|
gtk_widget_activate_action (self->video,
|
|
(forward) ? "video.speed-up" : "video.speed-down", NULL);
|
|
}
|
|
|
|
static inline void
|
|
_handle_progression_key_press (ClapperAppWindow *self)
|
|
{
|
|
ClapperPlayer *player = clapper_gtk_video_get_player (
|
|
CLAPPER_GTK_VIDEO_CAST (self->video));
|
|
ClapperQueue *queue = clapper_player_get_queue (player);
|
|
ClapperQueueProgressionMode mode;
|
|
const gchar *icon = NULL, *label = NULL;
|
|
|
|
mode = ((clapper_queue_get_progression_mode (queue) + 1) % N_PROGRESSION_MODES);
|
|
|
|
clapper_app_utils_parse_progression (mode, &icon, &label);
|
|
clapper_queue_set_progression_mode (queue, mode);
|
|
|
|
clapper_gtk_billboard_post_message (self->billboard, icon, label);
|
|
}
|
|
|
|
static gboolean
|
|
key_pressed_cb (GtkEventControllerKey *controller, guint keyval,
|
|
guint keycode, GdkModifierType state, ClapperAppWindow *self)
|
|
{
|
|
switch (keyval) {
|
|
case GDK_KEY_Up:
|
|
if (state == 0)
|
|
gtk_widget_activate_action (self->video, "video.volume-up", NULL);
|
|
break;
|
|
case GDK_KEY_Down:
|
|
if (state == 0)
|
|
gtk_widget_activate_action (self->video, "video.volume-down", NULL);
|
|
break;
|
|
case GDK_KEY_Left:
|
|
if (state == 0) {
|
|
_handle_seek_key_press (self, FALSE);
|
|
} else if (!self->key_held && (state & GDK_SHIFT_MASK) == GDK_SHIFT_MASK) {
|
|
_handle_chapter_key_press (self, FALSE);
|
|
} else if ((state & GDK_CONTROL_MASK) == GDK_CONTROL_MASK) {
|
|
_handle_item_key_press (self, FALSE);
|
|
}
|
|
break;
|
|
case GDK_KEY_j:
|
|
if (state == 0)
|
|
_handle_seek_key_press (self, FALSE);
|
|
break;
|
|
case GDK_KEY_Right:
|
|
if (state == 0) {
|
|
_handle_seek_key_press (self, TRUE);
|
|
} else if (!self->key_held && (state & GDK_SHIFT_MASK) == GDK_SHIFT_MASK) {
|
|
_handle_chapter_key_press (self, TRUE);
|
|
} else if ((state & GDK_CONTROL_MASK) == GDK_CONTROL_MASK) {
|
|
_handle_item_key_press (self, TRUE);
|
|
}
|
|
break;
|
|
case GDK_KEY_l:
|
|
if (state == 0)
|
|
_handle_seek_key_press (self, TRUE);
|
|
break;
|
|
case GDK_KEY_space:
|
|
case GDK_KEY_k:
|
|
if (!self->key_held) // Disable constant toggling when key held
|
|
gtk_widget_activate_action (self->video, "video.toggle-play", NULL);
|
|
break;
|
|
case GDK_KEY_less:
|
|
if (!self->key_held) // Needs seek (action is slow)
|
|
_handle_speed_key_press (self, FALSE);
|
|
break;
|
|
case GDK_KEY_greater:
|
|
if (!self->key_held) // Needs seek (action is slow)
|
|
_handle_speed_key_press (self, TRUE);
|
|
break;
|
|
case GDK_KEY_m:
|
|
gtk_widget_activate_action (self->video, "video.toggle-mute", NULL);
|
|
break;
|
|
case GDK_KEY_p:
|
|
if (!self->key_held)
|
|
_handle_progression_key_press (self);
|
|
break;
|
|
default:
|
|
return FALSE;
|
|
}
|
|
|
|
self->key_held = TRUE;
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static void
|
|
key_released_cb (GtkEventControllerKey *controller, guint keyval,
|
|
guint keycode, GdkModifierType state, ClapperAppWindow *self)
|
|
{
|
|
switch (keyval) {
|
|
case GDK_KEY_Left:
|
|
case GDK_KEY_j:
|
|
case GDK_KEY_Right:
|
|
case GDK_KEY_l:
|
|
_end_seek_operation (self);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
self->key_held = FALSE;
|
|
}
|
|
|
|
static void
|
|
_seek_delay_cb (ClapperAppWindow *self)
|
|
{
|
|
GST_LOG_OBJECT (self, "Delayed seek handler reached");
|
|
self->seek_timeout = 0;
|
|
|
|
if (self->seeking)
|
|
_end_seek_operation (self);
|
|
}
|
|
|
|
static void
|
|
video_seek_request_cb (ClapperGtkVideo *video, gboolean forward, ClapperAppWindow *self)
|
|
{
|
|
g_clear_handle_id (&self->seek_timeout, g_source_remove);
|
|
|
|
_handle_seek_key_press (self, forward);
|
|
|
|
self->seek_timeout = g_timeout_add_once (500, (GSourceOnceFunc) _seek_delay_cb, self);
|
|
}
|
|
|
|
static void
|
|
drop_value_notify_cb (GtkDropTarget *drop_target,
|
|
GParamSpec *pspec G_GNUC_UNUSED, ClapperAppWindow *self)
|
|
{
|
|
GtkWidget *stack;
|
|
const GValue *value = gtk_drop_target_get_value (drop_target);
|
|
|
|
if (!value) {
|
|
clapper_gtk_billboard_unpin_pinned_message (self->billboard);
|
|
return;
|
|
}
|
|
|
|
if (!clapper_app_utils_value_for_item_is_valid (value)) {
|
|
gtk_drop_target_reject (drop_target);
|
|
return;
|
|
}
|
|
|
|
stack = gtk_window_get_child (GTK_WINDOW (self));
|
|
|
|
/* Do not pin message when still in initial state */
|
|
if (gtk_stack_get_visible_child (GTK_STACK (stack)) == self->video) {
|
|
clapper_gtk_billboard_pin_message (self->billboard,
|
|
"insert-object-symbolic",
|
|
_("Drop on title bar to play now or anywhere else to enqueue."));
|
|
}
|
|
}
|
|
|
|
static gboolean
|
|
drop_cb (GtkDropTarget *drop_target, const GValue *value,
|
|
gdouble x, gdouble y, ClapperAppWindow *self)
|
|
{
|
|
GFile **files = NULL;
|
|
gint n_files = 0;
|
|
gboolean success = FALSE;
|
|
|
|
if (clapper_app_utils_files_from_value (value, &files, &n_files)) {
|
|
ClapperPlayer *player = clapper_app_window_get_player (self);
|
|
ClapperQueue *queue = clapper_player_get_queue (player);
|
|
gint i;
|
|
|
|
clapper_app_window_ensure_no_initial_state (self);
|
|
|
|
for (i = 0; i < n_files; ++i) {
|
|
ClapperMediaItem *item = clapper_media_item_new_from_file (files[i]);
|
|
|
|
clapper_queue_add_item (queue, item);
|
|
gst_object_unref (item);
|
|
}
|
|
|
|
clapper_app_utils_files_free (files);
|
|
success = TRUE;
|
|
}
|
|
|
|
return success;
|
|
}
|
|
|
|
static void
|
|
toggle_fullscreen (GSimpleAction *action, GVariant *param, gpointer user_data)
|
|
{
|
|
ClapperAppWindow *self = CLAPPER_APP_WINDOW_CAST (user_data);
|
|
|
|
video_toggle_fullscreen_cb (CLAPPER_GTK_VIDEO_CAST (self->video), self);
|
|
}
|
|
|
|
static void
|
|
show_help_overlay (GSimpleAction *action, GVariant *param, gpointer user_data)
|
|
{
|
|
ClapperAppWindow *self = CLAPPER_APP_WINDOW_CAST (user_data);
|
|
GtkBuilder *builder;
|
|
GtkWidget *help_overlay;
|
|
|
|
builder = gtk_builder_new_from_resource (
|
|
CLAPPER_APP_RESOURCE_PREFIX "/ui/clapper-app-help-overlay.ui");
|
|
help_overlay = GTK_WIDGET (gtk_builder_get_object (builder, "help_overlay"));
|
|
|
|
gtk_window_set_transient_for (GTK_WINDOW (help_overlay), GTK_WINDOW (self));
|
|
gtk_window_present (GTK_WINDOW (help_overlay));
|
|
|
|
g_object_unref (builder);
|
|
}
|
|
|
|
GtkWidget *
|
|
clapper_app_window_new (GtkApplication *application)
|
|
{
|
|
return g_object_new (CLAPPER_APP_TYPE_WINDOW,
|
|
"application", application,
|
|
NULL);
|
|
}
|
|
|
|
GtkWidget *
|
|
clapper_app_window_get_video (ClapperAppWindow *self)
|
|
{
|
|
return self->video;
|
|
}
|
|
|
|
ClapperPlayer *
|
|
clapper_app_window_get_player (ClapperAppWindow *self)
|
|
{
|
|
return clapper_gtk_video_get_player (CLAPPER_GTK_VIDEO_CAST (self->video));
|
|
}
|
|
|
|
void
|
|
clapper_app_window_ensure_no_initial_state (ClapperAppWindow *self)
|
|
{
|
|
GtkWidget *stack = gtk_window_get_child (GTK_WINDOW (self));
|
|
const gchar *child_name = gtk_stack_get_visible_child_name (GTK_STACK (stack));
|
|
|
|
if (g_strcmp0 (child_name, "initial_state") == 0)
|
|
gtk_stack_set_visible_child (GTK_STACK (stack), self->video);
|
|
}
|
|
|
|
static gboolean
|
|
clapper_app_window_close_request (GtkWindow *window)
|
|
{
|
|
ClapperAppWindow *self = CLAPPER_APP_WINDOW_CAST (window);
|
|
/* FIXME: Have GSettings again to store these values
|
|
GSettings *settings = g_settings_new (CLAPPER_APP_ID);
|
|
gint width = DEFAULT_WINDOW_WIDTH, height = DEFAULT_WINDOW_HEIGHT;
|
|
*/
|
|
GST_DEBUG_OBJECT (self, "Close request");
|
|
/*
|
|
gtk_window_get_default_size (window, &width, &height);
|
|
|
|
g_settings_set (settings, "window-size", "(ii)", width, height);
|
|
g_settings_set_boolean (settings, "maximized", gtk_window_is_maximized (window));
|
|
g_settings_set_boolean (settings, "fullscreen", gtk_window_is_fullscreen (window));
|
|
|
|
g_object_unref (settings);
|
|
*/
|
|
|
|
return GTK_WINDOW_CLASS (parent_class)->close_request (window);
|
|
}
|
|
|
|
static void
|
|
clapper_app_window_realize (GtkWidget *widget)
|
|
{
|
|
ClapperAppWindow *self = CLAPPER_APP_WINDOW_CAST (widget);
|
|
|
|
GST_TRACE_OBJECT (self, "Realize");
|
|
|
|
GTK_WIDGET_CLASS (parent_class)->realize (widget);
|
|
|
|
gtk_style_context_add_provider_for_display (gtk_widget_get_display (widget),
|
|
(GtkStyleProvider *) self->provider, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
|
|
}
|
|
|
|
static void
|
|
clapper_app_window_unrealize (GtkWidget *widget)
|
|
{
|
|
ClapperAppWindow *self = CLAPPER_APP_WINDOW_CAST (widget);
|
|
|
|
GST_TRACE_OBJECT (self, "Unrealize");
|
|
|
|
gtk_style_context_remove_provider_for_display (gtk_widget_get_display (widget),
|
|
(GtkStyleProvider *) self->provider);
|
|
|
|
GTK_WIDGET_CLASS (parent_class)->unrealize (widget);
|
|
}
|
|
|
|
static void
|
|
clapper_app_window_init (ClapperAppWindow *self)
|
|
{
|
|
GtkSettings *settings;
|
|
GtkWidget *dummy_titlebar;
|
|
gint distance = 0;
|
|
|
|
gtk_widget_init_template (GTK_WIDGET (self));
|
|
|
|
/* Make double tap easier to perform */
|
|
settings = gtk_widget_get_settings (self->video);
|
|
g_object_get (settings, "gtk-double-click-distance", &distance, NULL);
|
|
g_object_set (settings, "gtk-double-click-distance", MAX (distance, 32), NULL);
|
|
|
|
dummy_titlebar = g_object_new (GTK_TYPE_BOX,
|
|
"can_focus", FALSE,
|
|
"focusable", FALSE,
|
|
"visible", FALSE,
|
|
NULL);
|
|
gtk_window_set_titlebar (GTK_WINDOW (self), dummy_titlebar);
|
|
gtk_window_set_title (GTK_WINDOW (self), CLAPPER_APP_NAME);
|
|
|
|
/* Prevent GTK from redrawing background for each frame */
|
|
gtk_widget_remove_css_class (GTK_WIDGET (self), "background");
|
|
|
|
gtk_drop_target_set_gtypes (self->drop_target,
|
|
(GType[3]) { GDK_TYPE_FILE_LIST, G_TYPE_FILE, G_TYPE_STRING }, 3);
|
|
}
|
|
|
|
static void
|
|
clapper_app_window_constructed (GObject *object)
|
|
{
|
|
ClapperAppWindow *self = CLAPPER_APP_WINDOW_CAST (object);
|
|
ClapperPlayer *player = clapper_app_window_get_player (self);
|
|
ClapperQueue *queue = clapper_player_get_queue (player);
|
|
ClapperGtkExtraMenuButton *button;
|
|
GstElement *element;
|
|
AdwStyleManager *manager;
|
|
|
|
static const GActionEntry win_entries[] = {
|
|
{ "toggle-fullscreen", toggle_fullscreen, NULL, NULL, NULL },
|
|
{ "show-help-overlay", show_help_overlay, NULL, NULL, NULL },
|
|
};
|
|
|
|
#if (CLAPPER_HAVE_MPRIS || CLAPPER_HAVE_SERVER || CLAPPER_HAVE_DISCOVERER)
|
|
ClapperFeature *feature = NULL;
|
|
#endif
|
|
|
|
self->settings = g_settings_new (CLAPPER_APP_ID);
|
|
self->last_volume = PERCENTAGE_ROUND (g_settings_get_double (self->settings, "volume"));
|
|
|
|
#if CLAPPER_HAVE_MPRIS
|
|
feature = CLAPPER_FEATURE (clapper_mpris_new (
|
|
"org.mpris.MediaPlayer2.Clapper",
|
|
"Clapper", CLAPPER_APP_ID));
|
|
clapper_mpris_set_queue_controllable (CLAPPER_MPRIS (feature), TRUE);
|
|
clapper_player_add_feature (player, feature);
|
|
gst_object_unref (feature);
|
|
#endif
|
|
|
|
#if CLAPPER_HAVE_SERVER
|
|
feature = CLAPPER_FEATURE (clapper_server_new ());
|
|
clapper_server_set_queue_controllable (CLAPPER_SERVER (feature), TRUE);
|
|
g_settings_bind (self->settings, "server-enabled",
|
|
feature, "enabled", G_SETTINGS_BIND_GET);
|
|
clapper_player_add_feature (player, feature);
|
|
gst_object_unref (feature);
|
|
#endif
|
|
|
|
#if CLAPPER_HAVE_DISCOVERER
|
|
feature = CLAPPER_FEATURE (clapper_discoverer_new ());
|
|
clapper_player_add_feature (player, feature);
|
|
gst_object_unref (feature);
|
|
#endif
|
|
|
|
/* FIXME: Allow setting sink/filter elements from prefs window
|
|
* (this should include parsing bin descriptions) */
|
|
element = gst_element_factory_make ("scaletempo", NULL);
|
|
if (G_LIKELY (element != NULL))
|
|
clapper_player_set_audio_filter (player, element);
|
|
|
|
clapper_player_set_autoplay (player, TRUE);
|
|
|
|
/* No need to also call this here, as item is selected
|
|
* after application window is contructed */
|
|
g_signal_connect (queue, "notify::current-item",
|
|
G_CALLBACK (_queue_current_item_changed_cb), self);
|
|
|
|
g_settings_bind (self->settings, "audio-offset",
|
|
player, "audio-offset", G_SETTINGS_BIND_GET);
|
|
g_settings_bind (self->settings, "subtitle-offset",
|
|
player, "subtitle-offset", G_SETTINGS_BIND_GET);
|
|
g_settings_bind (self->settings, "subtitle-font-desc",
|
|
player, "subtitle-font-desc", G_SETTINGS_BIND_GET);
|
|
|
|
button = clapper_gtk_simple_controls_get_extra_menu_button (
|
|
self->simple_controls);
|
|
|
|
g_settings_bind_with_mapping (self->settings, "seek-method",
|
|
self->simple_controls, "seek-method", G_SETTINGS_BIND_GET,
|
|
(GSettingsBindGetMapping) _get_seek_method_mapping,
|
|
NULL, NULL, NULL);
|
|
g_signal_connect (button, "open-subtitles",
|
|
G_CALLBACK (_open_subtitles_cb), self);
|
|
clapper_gtk_extra_menu_button_set_can_open_subtitles (button, TRUE);
|
|
|
|
manager = adw_style_manager_get_default ();
|
|
adw_style_manager_set_color_scheme (manager, ADW_COLOR_SCHEME_FORCE_DARK);
|
|
|
|
self->provider = gtk_css_provider_new ();
|
|
gtk_css_provider_load_from_resource (self->provider,
|
|
CLAPPER_APP_RESOURCE_PREFIX "/css/styles.css");
|
|
|
|
g_action_map_add_action_entries (G_ACTION_MAP (self),
|
|
win_entries, G_N_ELEMENTS (win_entries), self);
|
|
|
|
G_OBJECT_CLASS (parent_class)->constructed (object);
|
|
}
|
|
|
|
static void
|
|
clapper_app_window_dispose (GObject *object)
|
|
{
|
|
ClapperAppWindow *self = CLAPPER_APP_WINDOW_CAST (object);
|
|
|
|
g_clear_handle_id (&self->seek_timeout, g_source_remove);
|
|
|
|
gtk_widget_dispose_template (GTK_WIDGET (object), CLAPPER_APP_TYPE_WINDOW);
|
|
|
|
gst_clear_object (&self->current_item);
|
|
|
|
G_OBJECT_CLASS (parent_class)->dispose (object);
|
|
}
|
|
|
|
static void
|
|
clapper_app_window_finalize (GObject *object)
|
|
{
|
|
ClapperAppWindow *self = CLAPPER_APP_WINDOW_CAST (object);
|
|
|
|
GST_TRACE_OBJECT (self, "Finalize");
|
|
|
|
g_object_unref (self->settings);
|
|
g_object_unref (self->provider);
|
|
|
|
G_OBJECT_CLASS (parent_class)->finalize (object);
|
|
}
|
|
|
|
static void
|
|
clapper_app_window_class_init (ClapperAppWindowClass *klass)
|
|
{
|
|
GObjectClass *gobject_class = (GObjectClass *) klass;
|
|
GtkWidgetClass *widget_class = (GtkWidgetClass *) klass;
|
|
GtkWindowClass *window_class = (GtkWindowClass *) klass;
|
|
|
|
GST_DEBUG_CATEGORY_INIT (GST_CAT_DEFAULT, "clapperappwindow", 0,
|
|
"Clapper App Window");
|
|
|
|
gobject_class->constructed = clapper_app_window_constructed;
|
|
gobject_class->dispose = clapper_app_window_dispose;
|
|
gobject_class->finalize = clapper_app_window_finalize;
|
|
|
|
widget_class->realize = clapper_app_window_realize;
|
|
widget_class->unrealize = clapper_app_window_unrealize;
|
|
|
|
window_class->close_request = clapper_app_window_close_request;
|
|
|
|
gtk_widget_class_set_template_from_resource (widget_class,
|
|
CLAPPER_APP_RESOURCE_PREFIX "/ui/clapper-app-window.ui");
|
|
|
|
gtk_widget_class_bind_template_child (widget_class, ClapperAppWindow, video);
|
|
gtk_widget_class_bind_template_child (widget_class, ClapperAppWindow, billboard);
|
|
gtk_widget_class_bind_template_child (widget_class, ClapperAppWindow, simple_controls);
|
|
gtk_widget_class_bind_template_child (widget_class, ClapperAppWindow, drop_target);
|
|
|
|
gtk_widget_class_bind_template_callback (widget_class, video_toggle_fullscreen_cb);
|
|
gtk_widget_class_bind_template_callback (widget_class, video_seek_request_cb);
|
|
gtk_widget_class_bind_template_callback (widget_class, video_map_cb);
|
|
gtk_widget_class_bind_template_callback (widget_class, video_unmap_cb);
|
|
gtk_widget_class_bind_template_callback (widget_class, scroll_begin_cb);
|
|
gtk_widget_class_bind_template_callback (widget_class, scroll_cb);
|
|
gtk_widget_class_bind_template_callback (widget_class, scroll_end_cb);
|
|
gtk_widget_class_bind_template_callback (widget_class, key_pressed_cb);
|
|
gtk_widget_class_bind_template_callback (widget_class, key_released_cb);
|
|
|
|
gtk_widget_class_bind_template_callback (widget_class, right_click_pressed_cb);
|
|
gtk_widget_class_bind_template_callback (widget_class, right_click_released_cb);
|
|
gtk_widget_class_bind_template_callback (widget_class, drag_begin_cb);
|
|
gtk_widget_class_bind_template_callback (widget_class, drag_update_cb);
|
|
|
|
gtk_widget_class_bind_template_callback (widget_class, drop_value_notify_cb);
|
|
gtk_widget_class_bind_template_callback (widget_class, drop_cb);
|
|
}
|