mirror of
https://gitlab.freedesktop.org/gstreamer/gstreamer.git
synced 2025-01-15 11:55:32 +00:00
libs: wayland: add support for XDG-shell protocol
[wl_shell] is officially [deprecated], so provide support for the
XDG-shell protocol should be provided by all desktop-like compositors.
(In case they don't, we can of course fall back to wl_shell).
Note that the XML file is directly provided by the `wayland-protocols`
dependency and generates the protocol marshalling code.
[wl_shell]: https://people.freedesktop.org/~whot/wayland-doxygen/wayland/Client/group__iface__wl__shell.html
[deprecated]: 698dde1958
This commit is contained in:
parent
8e695c8fdb
commit
f84394fa49
8 changed files with 222 additions and 19 deletions
|
@ -466,13 +466,18 @@ fi
|
|||
dnl Check for Wayland
|
||||
USE_WAYLAND=0
|
||||
if test "x$enable_wayland" = "xyes"; then
|
||||
PKG_CHECK_MODULES([WAYLAND], [wayland-client >= $WAYLAND_REQ],
|
||||
PKG_CHECK_MODULES([WAYLAND], [wayland-client >= $WAYLAND_REQ, wayland-protocols >= 1.15],
|
||||
[
|
||||
USE_WAYLAND=1
|
||||
saved_CPPFLAGS="$CPPFLAGS"
|
||||
CPPFLAGS="$CPPFLAGS $WAYLAND_CFLAGS"
|
||||
AC_CHECK_HEADERS([wayland-client.h], [], [USE_WAYLAND=0])
|
||||
CPPFLAGS="$saved_CPPFLAGS"
|
||||
|
||||
AC_CHECK_PROGS(WAYLAND_SCANNER, wayland-scanner, [USE_WAYLAND=0])
|
||||
|
||||
WAYLAND_PROTOCOLS_DATADIR="`$PKG_CONFIG --variable=pkgdatadir wayland-protocols`"
|
||||
AC_SUBST(WAYLAND_PROTOCOLS_DATADIR, $WAYLAND_PROTOCOLS_DATADIR)
|
||||
], [:])
|
||||
fi
|
||||
|
||||
|
|
|
@ -314,9 +314,28 @@ libgstvaapi_egl_source_priv_h = \
|
|||
ogl_compat.h \
|
||||
$(NULL)
|
||||
|
||||
BUILT_SOURCES=
|
||||
CLEANFILES=
|
||||
|
||||
# Generate the necessary files for XDG-shell
|
||||
if USE_WAYLAND
|
||||
xdg_shell_protocol_spec = $(WAYLAND_PROTOCOLS_DATADIR)/stable/xdg-shell/xdg-shell.xml
|
||||
xdg_shell_header = xdg-shell-client-protocol.h
|
||||
xdg_shell_source = xdg-shell-client-protocol.c
|
||||
|
||||
$(xdg_shell_header): $(xdg_shell_protocol_spec)
|
||||
$(AM_V_GEN) $(WAYLAND_SCANNER) client-header < $< > $@
|
||||
$(xdg_shell_source): $(xdg_shell_protocol_spec)
|
||||
$(AM_V_GEN) $(WAYLAND_SCANNER) private-code < $< > $@
|
||||
|
||||
BUILT_SOURCES += $(xdg_shell_header) $(xdg_shell_source)
|
||||
CLEANFILES += $(BUILT_SOURCES)
|
||||
endif
|
||||
|
||||
libgstvaapi_wayland_source_c = \
|
||||
gstvaapidisplay_wayland.c \
|
||||
gstvaapiwindow_wayland.c \
|
||||
$(xdg_shell_source) \
|
||||
$(NULL)
|
||||
|
||||
libgstvaapi_wayland_source_h = \
|
||||
|
@ -327,6 +346,7 @@ libgstvaapi_wayland_source_h = \
|
|||
libgstvaapi_wayland_source_priv_h = \
|
||||
gstvaapicompat.h \
|
||||
gstvaapidisplay_wayland_priv.h \
|
||||
$(xdg_shell_header) \
|
||||
$(NULL)
|
||||
|
||||
libgstvaapi_la_SOURCES = \
|
||||
|
|
|
@ -94,6 +94,17 @@ static const struct wl_output_listener output_listener = {
|
|||
output_handle_mode,
|
||||
};
|
||||
|
||||
static void
|
||||
handle_xdg_wm_base_ping (void *user_data, struct xdg_wm_base *xdg_wm_base,
|
||||
uint32_t serial)
|
||||
{
|
||||
xdg_wm_base_pong (xdg_wm_base, serial);
|
||||
}
|
||||
|
||||
static const struct xdg_wm_base_listener xdg_wm_base_listener = {
|
||||
handle_xdg_wm_base_ping
|
||||
};
|
||||
|
||||
static void
|
||||
registry_handle_global (void *data,
|
||||
struct wl_registry *registry,
|
||||
|
@ -106,7 +117,11 @@ registry_handle_global (void *data,
|
|||
wl_registry_bind (registry, id, &wl_compositor_interface, 1);
|
||||
else if (strcmp (interface, "wl_shell") == 0)
|
||||
priv->wl_shell = wl_registry_bind (registry, id, &wl_shell_interface, 1);
|
||||
else if (strcmp (interface, "wl_output") == 0) {
|
||||
else if (strcmp (interface, "xdg_wm_base") == 0) {
|
||||
priv->xdg_wm_base =
|
||||
wl_registry_bind (registry, id, &xdg_wm_base_interface, 1);
|
||||
xdg_wm_base_add_listener (priv->xdg_wm_base, &xdg_wm_base_listener, priv);
|
||||
} else if (strcmp (interface, "wl_output") == 0) {
|
||||
priv->output = wl_registry_bind (registry, id, &wl_output_interface, 1);
|
||||
wl_output_add_listener (priv->output, &output_listener, priv);
|
||||
}
|
||||
|
@ -142,10 +157,14 @@ gst_vaapi_display_wayland_setup (GstVaapiDisplay * display)
|
|||
return FALSE;
|
||||
}
|
||||
|
||||
if (priv->xdg_wm_base)
|
||||
return TRUE;
|
||||
|
||||
if (!priv->wl_shell) {
|
||||
GST_ERROR ("failed to bind wl_shell interface");
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
|
@ -192,6 +211,7 @@ gst_vaapi_display_wayland_close_display (GstVaapiDisplay * display)
|
|||
|
||||
g_clear_pointer (&priv->output, wl_output_destroy);
|
||||
g_clear_pointer (&priv->wl_shell, wl_shell_destroy);
|
||||
g_clear_pointer (&priv->xdg_wm_base, xdg_wm_base_destroy);
|
||||
g_clear_pointer (&priv->compositor, wl_compositor_destroy);
|
||||
g_clear_pointer (&priv->registry, wl_registry_destroy);
|
||||
|
||||
|
|
|
@ -24,6 +24,8 @@
|
|||
#ifndef GST_VAAPI_DISPLAY_WAYLAND_PRIV_H
|
||||
#define GST_VAAPI_DISPLAY_WAYLAND_PRIV_H
|
||||
|
||||
#include "xdg-shell-client-protocol.h"
|
||||
|
||||
#include <gst/vaapi/gstvaapidisplay_wayland.h>
|
||||
#include "gstvaapidisplay_priv.h"
|
||||
|
||||
|
@ -61,6 +63,7 @@ struct _GstVaapiDisplayWaylandPrivate
|
|||
struct wl_display *wl_display;
|
||||
struct wl_compositor *compositor;
|
||||
struct wl_shell *wl_shell;
|
||||
struct xdg_wm_base *xdg_wm_base;
|
||||
struct wl_output *output;
|
||||
struct wl_registry *registry;
|
||||
guint width;
|
||||
|
|
|
@ -98,6 +98,8 @@ frame_state_free (FrameState * frame)
|
|||
|
||||
struct _GstVaapiWindowWaylandPrivate
|
||||
{
|
||||
struct xdg_surface *xdg_surface;
|
||||
struct xdg_toplevel *xdg_toplevel;
|
||||
struct wl_shell_surface *wl_shell_surface;
|
||||
struct wl_surface *surface;
|
||||
struct wl_region *opaque_region;
|
||||
|
@ -137,10 +139,79 @@ struct _GstVaapiWindowWaylandClass
|
|||
G_DEFINE_TYPE_WITH_PRIVATE (GstVaapiWindowWayland, gst_vaapi_window_wayland,
|
||||
GST_TYPE_VAAPI_WINDOW);
|
||||
|
||||
/* Object signals */
|
||||
enum
|
||||
{
|
||||
SIZE_CHANGED,
|
||||
N_SIGNALS
|
||||
};
|
||||
|
||||
static guint signals[N_SIGNALS];
|
||||
|
||||
static void
|
||||
handle_xdg_toplevel_configure (void *data, struct xdg_toplevel *xdg_toplevel,
|
||||
int32_t width, int32_t height, struct wl_array *states)
|
||||
{
|
||||
GstVaapiWindow *window = GST_VAAPI_WINDOW (data);
|
||||
const uint32_t *state;
|
||||
|
||||
GST_DEBUG ("Got XDG-toplevel::reconfigure, [width x height] = [%d x %d]",
|
||||
width, height);
|
||||
|
||||
wl_array_for_each (state, states) {
|
||||
switch (*state) {
|
||||
case XDG_TOPLEVEL_STATE_FULLSCREEN:
|
||||
case XDG_TOPLEVEL_STATE_MAXIMIZED:
|
||||
case XDG_TOPLEVEL_STATE_RESIZING:
|
||||
case XDG_TOPLEVEL_STATE_ACTIVATED:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (width > 0 && height > 0) {
|
||||
gst_vaapi_window_set_size (window, width, height);
|
||||
g_signal_emit (window, signals[SIZE_CHANGED], 0, width, height);
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
handle_xdg_toplevel_close (void *data, struct xdg_toplevel *xdg_toplevel)
|
||||
{
|
||||
}
|
||||
|
||||
static const struct xdg_toplevel_listener xdg_toplevel_listener = {
|
||||
handle_xdg_toplevel_configure,
|
||||
handle_xdg_toplevel_close,
|
||||
};
|
||||
|
||||
static gboolean
|
||||
gst_vaapi_window_wayland_show (GstVaapiWindow * window)
|
||||
{
|
||||
GST_WARNING ("unimplemented GstVaapiWindowWayland::show()");
|
||||
GstVaapiWindowWaylandPrivate *priv =
|
||||
GST_VAAPI_WINDOW_WAYLAND_GET_PRIVATE (window);
|
||||
|
||||
if (priv->xdg_surface == NULL) {
|
||||
GST_FIXME ("GstVaapiWindowWayland::show() unimplemented for wl_shell");
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
if (priv->xdg_toplevel != NULL) {
|
||||
GST_DEBUG ("XDG toplevel already mapped");
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/* Create a toplevel window out of it */
|
||||
priv->xdg_toplevel = xdg_surface_get_toplevel (priv->xdg_surface);
|
||||
g_return_val_if_fail (priv->xdg_toplevel, FALSE);
|
||||
xdg_toplevel_set_title (priv->xdg_toplevel, "VA-API Wayland window");
|
||||
wl_proxy_set_queue ((struct wl_proxy *) priv->xdg_toplevel,
|
||||
priv->event_queue);
|
||||
|
||||
xdg_toplevel_add_listener (priv->xdg_toplevel, &xdg_toplevel_listener,
|
||||
window);
|
||||
|
||||
/* Commit the xdg_surface state as top-level window */
|
||||
wl_surface_commit (priv->surface);
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
@ -148,7 +219,18 @@ gst_vaapi_window_wayland_show (GstVaapiWindow * window)
|
|||
static gboolean
|
||||
gst_vaapi_window_wayland_hide (GstVaapiWindow * window)
|
||||
{
|
||||
GST_WARNING ("unimplemented GstVaapiWindowWayland::hide()");
|
||||
GstVaapiWindowWaylandPrivate *priv =
|
||||
GST_VAAPI_WINDOW_WAYLAND_GET_PRIVATE (window);
|
||||
|
||||
if (priv->xdg_surface == NULL) {
|
||||
GST_FIXME ("GstVaapiWindowWayland::hide() unimplemented for wl_shell");
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
if (priv->xdg_toplevel != NULL) {
|
||||
g_clear_pointer (&priv->xdg_toplevel, xdg_toplevel_destroy);
|
||||
wl_surface_commit (priv->surface);
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
@ -236,6 +318,17 @@ static const struct wl_shell_surface_listener shell_surface_listener = {
|
|||
handle_popup_done
|
||||
};
|
||||
|
||||
static void
|
||||
handle_xdg_surface_configure (void *data, struct xdg_surface *xdg_surface,
|
||||
uint32_t serial)
|
||||
{
|
||||
xdg_surface_ack_configure (xdg_surface, serial);
|
||||
}
|
||||
|
||||
static const struct xdg_surface_listener xdg_surface_listener = {
|
||||
handle_xdg_surface_configure,
|
||||
};
|
||||
|
||||
static gboolean
|
||||
gst_vaapi_window_wayland_set_fullscreen (GstVaapiWindow * window,
|
||||
gboolean fullscreen)
|
||||
|
@ -248,6 +341,16 @@ gst_vaapi_window_wayland_set_fullscreen (GstVaapiWindow * window,
|
|||
return TRUE;
|
||||
}
|
||||
|
||||
/* XDG-shell */
|
||||
if (priv->xdg_toplevel != NULL) {
|
||||
if (fullscreen)
|
||||
xdg_toplevel_set_fullscreen (priv->xdg_toplevel, NULL);
|
||||
else
|
||||
xdg_toplevel_unset_fullscreen (priv->xdg_toplevel);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/* wl_shell fallback */
|
||||
if (!fullscreen)
|
||||
wl_shell_surface_set_toplevel (priv->wl_shell_surface);
|
||||
else {
|
||||
|
@ -270,7 +373,8 @@ gst_vaapi_window_wayland_create (GstVaapiWindow * window,
|
|||
GST_DEBUG ("create window, size %ux%u", *width, *height);
|
||||
|
||||
g_return_val_if_fail (priv_display->compositor != NULL, FALSE);
|
||||
g_return_val_if_fail (priv_display->wl_shell != NULL, FALSE);
|
||||
g_return_val_if_fail (priv_display->xdg_wm_base || priv_display->wl_shell,
|
||||
FALSE);
|
||||
|
||||
GST_VAAPI_WINDOW_LOCK_DISPLAY (window);
|
||||
priv->event_queue = wl_display_create_queue (priv_display->wl_display);
|
||||
|
@ -285,18 +389,33 @@ gst_vaapi_window_wayland_create (GstVaapiWindow * window,
|
|||
return FALSE;
|
||||
wl_proxy_set_queue ((struct wl_proxy *) priv->surface, priv->event_queue);
|
||||
|
||||
GST_VAAPI_WINDOW_LOCK_DISPLAY (window);
|
||||
priv->wl_shell_surface = wl_shell_get_shell_surface (priv_display->wl_shell,
|
||||
priv->surface);
|
||||
GST_VAAPI_WINDOW_UNLOCK_DISPLAY (window);
|
||||
if (!priv->wl_shell_surface)
|
||||
return FALSE;
|
||||
wl_proxy_set_queue ((struct wl_proxy *) priv->wl_shell_surface,
|
||||
priv->event_queue);
|
||||
/* Prefer XDG-shell over deprecated wl_shell (if available) */
|
||||
if (priv_display->xdg_wm_base) {
|
||||
/* Create the XDG surface. We make the toplevel on VaapiWindow::show() */
|
||||
GST_VAAPI_WINDOW_LOCK_DISPLAY (window);
|
||||
priv->xdg_surface = xdg_wm_base_get_xdg_surface (priv_display->xdg_wm_base,
|
||||
priv->surface);
|
||||
GST_VAAPI_WINDOW_UNLOCK_DISPLAY (window);
|
||||
if (!priv->xdg_surface)
|
||||
return FALSE;
|
||||
wl_proxy_set_queue ((struct wl_proxy *) priv->xdg_surface,
|
||||
priv->event_queue);
|
||||
xdg_surface_add_listener (priv->xdg_surface, &xdg_surface_listener, window);
|
||||
} else {
|
||||
/* Fall back to wl_shell */
|
||||
GST_VAAPI_WINDOW_LOCK_DISPLAY (window);
|
||||
priv->wl_shell_surface = wl_shell_get_shell_surface (priv_display->wl_shell,
|
||||
priv->surface);
|
||||
GST_VAAPI_WINDOW_UNLOCK_DISPLAY (window);
|
||||
if (!priv->wl_shell_surface)
|
||||
return FALSE;
|
||||
wl_proxy_set_queue ((struct wl_proxy *) priv->wl_shell_surface,
|
||||
priv->event_queue);
|
||||
|
||||
wl_shell_surface_add_listener (priv->wl_shell_surface,
|
||||
&shell_surface_listener, priv);
|
||||
wl_shell_surface_set_toplevel (priv->wl_shell_surface);
|
||||
wl_shell_surface_add_listener (priv->wl_shell_surface,
|
||||
&shell_surface_listener, priv);
|
||||
wl_shell_surface_set_toplevel (priv->wl_shell_surface);
|
||||
}
|
||||
|
||||
priv->poll = gst_poll_new (TRUE);
|
||||
gst_poll_fd_init (&priv->pollfd);
|
||||
|
@ -332,6 +451,7 @@ gst_vaapi_window_wayland_finalize (GObject * object)
|
|||
if (priv->event_queue)
|
||||
wl_display_roundtrip_queue (wl_display, priv->event_queue);
|
||||
|
||||
g_clear_pointer (&priv->xdg_surface, xdg_surface_destroy);
|
||||
g_clear_pointer (&priv->wl_shell_surface, wl_shell_surface_destroy);
|
||||
g_clear_pointer (&priv->surface, wl_surface_destroy);
|
||||
g_clear_pointer (&priv->event_queue, wl_event_queue_destroy);
|
||||
|
@ -550,6 +670,10 @@ gst_vaapi_window_wayland_class_init (GstVaapiWindowWaylandClass * klass)
|
|||
window_class->set_fullscreen = gst_vaapi_window_wayland_set_fullscreen;
|
||||
window_class->unblock = gst_vaapi_window_wayland_unblock;
|
||||
window_class->unblock_cancel = gst_vaapi_window_wayland_unblock_cancel;
|
||||
|
||||
signals[SIZE_CHANGED] = g_signal_new ("size-changed",
|
||||
G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL,
|
||||
G_TYPE_NONE, 2, G_TYPE_INT, G_TYPE_INT);
|
||||
}
|
||||
|
||||
static void
|
||||
|
|
|
@ -183,10 +183,24 @@ if USE_EGL
|
|||
endif
|
||||
|
||||
if USE_WAYLAND
|
||||
# The XDG shell interface needs to be generated first
|
||||
wayland_protocols_basedir = wayland_protocols_dep.get_pkgconfig_variable('pkgdatadir')
|
||||
xdg_shell_xml_spec = join_paths(wayland_protocols_basedir, 'stable', 'xdg-shell', 'xdg-shell.xml')
|
||||
xdg_shell_header = custom_target('vaapi-xdg-shell-client-header',
|
||||
command: [ wayland_scanner_bin, 'client-header', '@INPUT@', '@OUTPUT@' ],
|
||||
input: xdg_shell_xml_spec,
|
||||
output: 'xdg-shell-client-protocol.h')
|
||||
xdg_shell_code = custom_target('vaapi-xdg-shell-client-code',
|
||||
command: [ wayland_scanner_bin, 'private-code', '@INPUT@', '@OUTPUT@' ],
|
||||
input: xdg_shell_xml_spec,
|
||||
output: 'xdg-shell-client-protocol.c')
|
||||
|
||||
gstlibvaapi_sources += [
|
||||
'gstvaapidisplay_wayland.c',
|
||||
'gstvaapiwindow_wayland.c',
|
||||
]
|
||||
xdg_shell_header,
|
||||
xdg_shell_code,
|
||||
]
|
||||
gstlibvaapi_headers += [
|
||||
'gstvaapidisplay_wayland.h',
|
||||
'gstvaapiwindow_wayland.h',
|
||||
|
@ -209,7 +223,7 @@ if USE_GLX
|
|||
gstlibvaapi_deps += [libva_x11_dep, x11_dep, gl_dep, libdl_dep]
|
||||
endif
|
||||
if USE_WAYLAND
|
||||
gstlibvaapi_deps += [libva_wayland_dep, wayland_client_dep]
|
||||
gstlibvaapi_deps += [libva_wayland_dep, wayland_client_dep, wayland_protocols_dep]
|
||||
endif
|
||||
if USE_X11
|
||||
gstlibvaapi_deps += [libva_x11_dep, x11_dep, xrandr_dep, xrender_dep]
|
||||
|
|
|
@ -533,6 +533,17 @@ gst_vaapisink_backend_x11 (void)
|
|||
#include <gst/vaapi/gstvaapidisplay_wayland.h>
|
||||
#include <gst/vaapi/gstvaapiwindow_wayland.h>
|
||||
|
||||
static void
|
||||
on_window_wayland_size_changed (GstVaapiWindowWayland * window, gint width,
|
||||
gint height, gpointer user_data)
|
||||
{
|
||||
GstVaapiSink *sink = GST_VAAPISINK (user_data);
|
||||
|
||||
GST_DEBUG ("Wayland window size changed to: %dx%d", width, height);
|
||||
gst_vaapisink_reconfigure_window (sink);
|
||||
gst_vaapisink_show_frame (GST_VIDEO_SINK_CAST (sink), NULL);
|
||||
}
|
||||
|
||||
static gboolean
|
||||
gst_vaapisink_wayland_create_window (GstVaapiSink * sink, guint width,
|
||||
guint height)
|
||||
|
@ -544,6 +555,10 @@ gst_vaapisink_wayland_create_window (GstVaapiSink * sink, guint width,
|
|||
sink->window = gst_vaapi_window_wayland_new (display, width, height);
|
||||
if (!sink->window)
|
||||
return FALSE;
|
||||
|
||||
g_signal_connect_object (sink->window, "size-changed",
|
||||
G_CALLBACK (on_window_wayland_size_changed), sink, 0);
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
|
|
|
@ -65,6 +65,8 @@ gl_dep = dependency('gl', required: false)
|
|||
glesv2_dep = dependency('glesv2', required: false)
|
||||
libdl_dep = cc.find_library('dl', required: false)
|
||||
wayland_client_dep = dependency('wayland-client', version: libwayland_req, required: false)
|
||||
wayland_protocols_dep = dependency('wayland-protocols', version: '>= 1.15', required: false)
|
||||
wayland_scanner_bin = find_program('wayland-scanner', required: false)
|
||||
x11_dep = dependency('x11', required: false)
|
||||
xrandr_dep = dependency('xrandr', required: false)
|
||||
xrender_dep = dependency('xrender', required: false)
|
||||
|
@ -92,7 +94,7 @@ USE_H264_FEI_ENCODER = USE_ENCODERS and cc.has_header('va/va_fei_h264.h', depend
|
|||
USE_DRM = libva_drm_dep.found() and libdrm_dep.found() and libudev_dep.found() and get_option('with_drm') != 'no'
|
||||
USE_EGL = gmodule_dep.found() and egl_dep.found() and GLES_VERSION_MASK != 0 and get_option('with_egl') != 'no'
|
||||
USE_GLX = libva_x11_dep.found() and x11_dep.found() and gl_dep.found() and libdl_dep.found() and get_option('with_glx') != 'no'
|
||||
USE_WAYLAND = libva_wayland_dep.found() and wayland_client_dep.found() and get_option('with_wayland') != 'no'
|
||||
USE_WAYLAND = libva_wayland_dep.found() and wayland_client_dep.found() and wayland_protocols_dep.found() and wayland_scanner_bin.found() and get_option('with_wayland') != 'no'
|
||||
USE_X11 = libva_x11_dep.found() and x11_dep.found() and get_option('with_x11') != 'no'
|
||||
|
||||
driverdir = libva_dep.get_pkgconfig_variable('driverdir')
|
||||
|
|
Loading…
Reference in a new issue