mirror of
https://github.com/Rafostar/clapper.git
synced 2025-08-31 08:21:59 +02:00
Watch for "adaptive-bandwidth" changes during adaptive streaming, use these to set "adaptive-start-bitrate" player property, so we do not always start streaming from some constant bitrate value which might not be the best for everyone. Additionally, store last value in GSettings on app exit and also add a command line arg to set this too.
1407 lines
42 KiB
C
1407 lines
42 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 MIN_WINDOW_WIDTH 352
|
|
#define MIN_WINDOW_HEIGHT 198
|
|
|
|
#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 MIN_STEP_DELAY 12000
|
|
|
|
#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;
|
|
guint resize_tick_id;
|
|
|
|
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)
|
|
|
|
typedef struct
|
|
{
|
|
gint dest_width, dest_height;
|
|
gint64 last_tick;
|
|
} ClapperAppWindowResizeData;
|
|
|
|
#if CLAPPER_HAVE_MPRIS
|
|
static guint16 instance_count = 0;
|
|
#endif
|
|
|
|
static inline GQuark
|
|
clapper_app_window_extra_options_get_quark (void)
|
|
{
|
|
return g_quark_from_static_string ("clapper-app-window-extra-options-quark");
|
|
}
|
|
|
|
static void
|
|
clapper_app_window_extra_options_free (ClapperAppWindowExtraOptions *extra_opts)
|
|
{
|
|
GST_TRACE ("Freeing window extra options: %p", extra_opts);
|
|
|
|
g_free (extra_opts->video_filter);
|
|
g_free (extra_opts->audio_filter);
|
|
|
|
g_free (extra_opts->video_sink);
|
|
g_free (extra_opts->audio_sink);
|
|
|
|
g_free (extra_opts);
|
|
}
|
|
|
|
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 void
|
|
_player_adaptive_bandwidth_changed_cb (ClapperPlayer *player,
|
|
GParamSpec *pspec G_GNUC_UNUSED, gpointer *user_data G_GNUC_UNUSED)
|
|
{
|
|
/* Do not take whole bandwidth */
|
|
clapper_player_set_adaptive_start_bitrate (player,
|
|
clapper_player_get_adaptive_bandwidth (player) * 0.8);
|
|
}
|
|
|
|
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
|
|
click_pressed_cb (GtkGestureClick *click, gint n_press,
|
|
gdouble x, gdouble y, ClapperAppWindow *self)
|
|
{
|
|
GdkCursor *cursor;
|
|
const gchar *cursor_name = NULL;
|
|
|
|
if (gtk_gesture_single_get_current_button (
|
|
GTK_GESTURE_SINGLE (click)) != GDK_BUTTON_SECONDARY)
|
|
return;
|
|
|
|
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 gboolean
|
|
_resize_tick (GtkWidget *widget, GdkFrameClock *frame_clock,
|
|
ClapperAppWindowResizeData *resize_data)
|
|
{
|
|
gint64 now = gdk_frame_clock_get_frame_time (frame_clock);
|
|
|
|
if (now - resize_data->last_tick >= MIN_STEP_DELAY) {
|
|
ClapperAppWindow *self = CLAPPER_APP_WINDOW_CAST (widget);
|
|
gint win_width, win_height;
|
|
|
|
GST_LOG_OBJECT (self, "Resize step, last: %" G_GINT64_FORMAT
|
|
", now: %" G_GINT64_FORMAT, resize_data->last_tick, now);
|
|
|
|
gtk_window_get_default_size (GTK_WINDOW (self), &win_width, &win_height);
|
|
|
|
if (win_width != resize_data->dest_width) {
|
|
gint width_diff = ABS (win_width - resize_data->dest_width);
|
|
gint step_size = (width_diff > 180) ? 120 : MAX (width_diff / 4, 1);
|
|
|
|
win_width += (win_width > resize_data->dest_width) ? -step_size : step_size;
|
|
}
|
|
if (win_height != resize_data->dest_height) {
|
|
gint height_diff = ABS (win_height - resize_data->dest_height);
|
|
gint step_size = (height_diff > 180) ? 120 : MAX (height_diff / 4, 1);
|
|
|
|
win_height += (win_height > resize_data->dest_height) ? -step_size : step_size;
|
|
}
|
|
|
|
gtk_window_set_default_size (GTK_WINDOW (self), win_width, win_height);
|
|
|
|
if (win_width == resize_data->dest_width
|
|
&& win_height == resize_data->dest_height) {
|
|
GST_DEBUG_OBJECT (self, "Window resize finish");
|
|
self->resize_tick_id = 0;
|
|
|
|
return G_SOURCE_REMOVE;
|
|
}
|
|
|
|
resize_data->last_tick = now;
|
|
}
|
|
|
|
return G_SOURCE_CONTINUE;
|
|
}
|
|
|
|
static void
|
|
_calculate_win_resize (gint win_w, gint win_h,
|
|
gint vid_w, gint vid_h, gint *dest_w, gint *dest_h)
|
|
{
|
|
gdouble win_aspect = (gdouble) win_w / win_h;
|
|
gdouble vid_aspect = (gdouble) vid_w / vid_h;
|
|
|
|
if (win_aspect < vid_aspect) {
|
|
while (!G_APPROX_VALUE (fmod (win_w, vid_aspect), 0, FLT_EPSILON))
|
|
win_w++;
|
|
|
|
win_h = round ((gdouble) win_w / vid_aspect);
|
|
|
|
if (win_h < MIN_WINDOW_HEIGHT) {
|
|
_calculate_win_resize (G_MAXINT, MIN_WINDOW_HEIGHT, vid_w, vid_h, dest_w, dest_h);
|
|
return;
|
|
}
|
|
} else {
|
|
while (!G_APPROX_VALUE (fmod (win_h * vid_aspect, 1.0), 0, FLT_EPSILON))
|
|
win_h++;
|
|
|
|
win_w = round ((gdouble) win_h * vid_aspect);
|
|
|
|
if (win_w < MIN_WINDOW_WIDTH) {
|
|
_calculate_win_resize (MIN_WINDOW_WIDTH, G_MAXINT, vid_w, vid_h, dest_w, dest_h);
|
|
return;
|
|
}
|
|
}
|
|
|
|
*dest_w = win_w;
|
|
*dest_h = win_h;
|
|
}
|
|
|
|
static void
|
|
_resize_window (ClapperAppWindow *self)
|
|
{
|
|
ClapperPlayer *player;
|
|
ClapperStreamList *vstreams;
|
|
ClapperVideoStream *vstream;
|
|
GdkToplevelState toplevel_state, disallowed;
|
|
|
|
if (self->resize_tick_id != 0)
|
|
return;
|
|
|
|
toplevel_state = gdk_toplevel_get_state (GDK_TOPLEVEL (
|
|
gtk_native_get_surface (GTK_NATIVE (self))));
|
|
disallowed = (GDK_TOPLEVEL_STATE_MINIMIZED
|
|
| GDK_TOPLEVEL_STATE_MAXIMIZED
|
|
| GDK_TOPLEVEL_STATE_FULLSCREEN
|
|
| GDK_TOPLEVEL_STATE_TILED);
|
|
|
|
if ((toplevel_state & disallowed) > 0) {
|
|
GST_DEBUG_OBJECT (self, "Cannot resize window in disallowed state");
|
|
return;
|
|
}
|
|
|
|
player = clapper_app_window_get_player (self);
|
|
vstreams = clapper_player_get_video_streams (player);
|
|
vstream = CLAPPER_VIDEO_STREAM_CAST (
|
|
clapper_stream_list_get_current_stream (vstreams));
|
|
|
|
if (vstream) {
|
|
gint video_width = clapper_video_stream_get_width (vstream);
|
|
gint video_height = clapper_video_stream_get_height (vstream);
|
|
|
|
if (G_LIKELY (video_width > 0 && video_height > 0)) {
|
|
gint win_width, win_height, dest_width, dest_height;
|
|
|
|
gtk_window_get_default_size (GTK_WINDOW (self), &win_width, &win_height);
|
|
|
|
_calculate_win_resize (win_width, win_height,
|
|
video_width, video_height, &dest_width, &dest_height);
|
|
|
|
/* Only begin resize when not already at perfect size */
|
|
if (dest_width != win_width || dest_height != win_height) {
|
|
ClapperAppWindowResizeData *resize_data;
|
|
|
|
resize_data = g_new0 (ClapperAppWindowResizeData, 1);
|
|
resize_data->dest_width = dest_width;
|
|
resize_data->dest_height = dest_height;
|
|
|
|
GST_DEBUG_OBJECT (self, "Window resize start, dest: %ix%i",
|
|
resize_data->dest_width, resize_data->dest_height);
|
|
|
|
self->resize_tick_id = gtk_widget_add_tick_callback (GTK_WIDGET (self),
|
|
(GtkTickCallback) _resize_tick, resize_data, g_free);
|
|
}
|
|
}
|
|
|
|
gst_object_unref (vstream);
|
|
}
|
|
}
|
|
|
|
static void
|
|
_handle_middle_click (ClapperAppWindow *self, GtkGestureClick *click)
|
|
{
|
|
_resize_window (self);
|
|
gtk_gesture_set_state (GTK_GESTURE (click), GTK_EVENT_SEQUENCE_CLAIMED);
|
|
}
|
|
|
|
static void
|
|
_handle_right_click (ClapperAppWindow *self, GtkGestureClick *click)
|
|
{
|
|
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
|
|
click_released_cb (GtkGestureClick *click, gint n_press,
|
|
gdouble x, gdouble y, ClapperAppWindow *self)
|
|
{
|
|
switch (gtk_gesture_single_get_current_button (GTK_GESTURE_SINGLE (click))) {
|
|
case GDK_BUTTON_MIDDLE:
|
|
_handle_middle_click (self, click);
|
|
break;
|
|
case GDK_BUTTON_SECONDARY:
|
|
_handle_right_click (self, click);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
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 & GDK_MODIFIER_MASK) == 0)
|
|
gtk_widget_activate_action (self->video, "video.volume-up", NULL);
|
|
break;
|
|
case GDK_KEY_Down:
|
|
if ((state & GDK_MODIFIER_MASK) == 0)
|
|
gtk_widget_activate_action (self->video, "video.volume-down", NULL);
|
|
break;
|
|
case GDK_KEY_Left:
|
|
if ((state & GDK_MODIFIER_MASK) == 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 & GDK_MODIFIER_MASK) == 0)
|
|
_handle_seek_key_press (self, FALSE);
|
|
break;
|
|
case GDK_KEY_Right:
|
|
if ((state & GDK_MODIFIER_MASK) == 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 & GDK_MODIFIER_MASK) == 0)
|
|
_handle_seek_key_press (self, TRUE);
|
|
break;
|
|
case GDK_KEY_space:
|
|
case GDK_KEY_k:
|
|
if (!self->key_held && (state & GDK_MODIFIER_MASK) == 0)
|
|
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:
|
|
if (!self->key_held && (state & GDK_MODIFIER_MASK) == 0)
|
|
gtk_widget_activate_action (self->video, "video.toggle-mute", NULL);
|
|
break;
|
|
case GDK_KEY_p:
|
|
if (!self->key_held && (state & GDK_MODIFIER_MASK) == 0)
|
|
_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
|
|
unfullscreen (GSimpleAction *action, GVariant *param, gpointer user_data)
|
|
{
|
|
GtkWindow *window = GTK_WINDOW (user_data);
|
|
|
|
if (gtk_window_is_fullscreen (window)) {
|
|
ClapperAppWindow *self = CLAPPER_APP_WINDOW_CAST (window);
|
|
video_toggle_fullscreen_cb (CLAPPER_GTK_VIDEO_CAST (self->video), self);
|
|
}
|
|
}
|
|
|
|
static void
|
|
auto_resize (GSimpleAction *action, GVariant *param, gpointer user_data)
|
|
{
|
|
_resize_window (CLAPPER_APP_WINDOW_CAST (user_data));
|
|
}
|
|
|
|
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));
|
|
}
|
|
|
|
ClapperAppWindowExtraOptions *
|
|
clapper_app_window_get_extra_options (ClapperAppWindow *self)
|
|
{
|
|
return g_object_get_qdata ((GObject *) self,
|
|
clapper_app_window_extra_options_get_quark ());
|
|
}
|
|
|
|
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)
|
|
{
|
|
ClapperAppWindowExtraOptions *extra_opts;
|
|
GtkSettings *settings;
|
|
GtkWidget *dummy_titlebar;
|
|
gint distance = 0;
|
|
|
|
gtk_widget_set_size_request (GTK_WIDGET (self),
|
|
MIN_WINDOW_WIDTH, MIN_WINDOW_HEIGHT);
|
|
|
|
extra_opts = g_new0 (ClapperAppWindowExtraOptions, 1);
|
|
GST_TRACE ("Created window extra options: %p", extra_opts);
|
|
|
|
g_object_set_qdata_full ((GObject *) self,
|
|
clapper_app_window_extra_options_get_quark (),
|
|
extra_opts, (GDestroyNotify) clapper_app_window_extra_options_free);
|
|
|
|
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;
|
|
AdwStyleManager *manager;
|
|
|
|
static const GActionEntry win_entries[] = {
|
|
{ "toggle-fullscreen", toggle_fullscreen, NULL, NULL, NULL },
|
|
{ "unfullscreen", unfullscreen, NULL, NULL, NULL },
|
|
{ "auto-resize", auto_resize, 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
|
|
#if CLAPPER_HAVE_MPRIS
|
|
gchar mpris_name[45];
|
|
g_snprintf (mpris_name, sizeof (mpris_name),
|
|
"org.mpris.MediaPlayer2.Clapper.instance%" G_GUINT16_FORMAT, instance_count++);
|
|
#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 (
|
|
mpris_name, CLAPPER_APP_NAME, 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) */
|
|
|
|
clapper_player_set_autoplay (player, TRUE);
|
|
|
|
/* No need to also call these here, as they only change
|
|
* after application window is contructed */
|
|
g_signal_connect (queue, "notify::current-item",
|
|
G_CALLBACK (_queue_current_item_changed_cb), self);
|
|
g_signal_connect (player, "notify::adaptive-bandwidth",
|
|
G_CALLBACK (_player_adaptive_bandwidth_changed_cb), NULL);
|
|
|
|
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);
|
|
|
|
if (self->resize_tick_id != 0) {
|
|
gtk_widget_remove_tick_callback (GTK_WIDGET (self), self->resize_tick_id);
|
|
self->resize_tick_id = 0;
|
|
}
|
|
|
|
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, click_pressed_cb);
|
|
gtk_widget_class_bind_template_callback (widget_class, 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);
|
|
}
|