mirror of
https://github.com/Rafostar/clapper.git
synced 2025-08-29 23:32:04 +02:00
1415 lines
42 KiB
C
1415 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;
|
|
|
|
static guint16 instance_count = 0;
|
|
|
|
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);
|
|
ClapperEnhancerProxyList *proxies = clapper_player_get_enhancer_proxies (player);
|
|
ClapperEnhancerProxy *proxy;
|
|
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
|
|
|
|
gchar mpris_name[45];
|
|
g_snprintf (mpris_name, sizeof (mpris_name),
|
|
"org.mpris.MediaPlayer2.Clapper.instance%" G_GUINT16_FORMAT, instance_count++);
|
|
|
|
self->settings = g_settings_new (CLAPPER_APP_ID);
|
|
self->last_volume = PERCENTAGE_ROUND (g_settings_get_double (self->settings, "volume"));
|
|
|
|
if ((proxy = clapper_enhancer_proxy_list_get_proxy_by_module (proxies, "clapper-mpris"))) {
|
|
clapper_enhancer_proxy_set_locally (proxy,
|
|
"own-name", mpris_name,
|
|
"identity", CLAPPER_APP_NAME,
|
|
"desktop-entry", CLAPPER_APP_ID,
|
|
"queue-controllable", TRUE, NULL);
|
|
gst_object_unref (proxy);
|
|
} else {
|
|
#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);
|
|
}
|