Files
clapper/src/bin/clapper-app/clapper-app-window.c
Rafał Dzięgiel c327734ca6 clapper-app: Do not trigger default scroll handler on GtkRange
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.
2024-04-08 20:37:32 +02:00

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 (&current_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 (&current_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);
}