Add basic cursor handling (including focusing clients)
This commit is contained in:
parent
2f4c865afc
commit
06debf11db
9 changed files with 222 additions and 24 deletions
|
@ -17,7 +17,8 @@ struct kiwmi_desktop {
|
||||||
struct wlr_data_device_manager *data_device_manager;
|
struct wlr_data_device_manager *data_device_manager;
|
||||||
struct wlr_output_layout *output_layout;
|
struct wlr_output_layout *output_layout;
|
||||||
struct wl_list outputs; // struct kiwmi_output::link
|
struct wl_list outputs; // struct kiwmi_output::link
|
||||||
struct wl_list views; // struct kiwmi_view::link
|
struct kiwmi_view *focused_view;
|
||||||
|
struct wl_list views; // struct kiwmi_view::link
|
||||||
|
|
||||||
struct wl_listener xdg_shell_new_surface;
|
struct wl_listener xdg_shell_new_surface;
|
||||||
struct wl_listener new_output;
|
struct wl_listener new_output;
|
||||||
|
|
|
@ -30,6 +30,8 @@ struct kiwmi_view {
|
||||||
struct wlr_xdg_surface *xdg_surface;
|
struct wlr_xdg_surface *xdg_surface;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct wlr_surface *wlr_surface;
|
||||||
|
|
||||||
struct wl_listener map;
|
struct wl_listener map;
|
||||||
struct wl_listener unmap;
|
struct wl_listener unmap;
|
||||||
struct wl_listener destroy;
|
struct wl_listener destroy;
|
||||||
|
@ -45,14 +47,38 @@ struct kiwmi_view_impl {
|
||||||
struct kiwmi_view *view,
|
struct kiwmi_view *view,
|
||||||
wlr_surface_iterator_func_t iterator,
|
wlr_surface_iterator_func_t iterator,
|
||||||
void *user_data);
|
void *user_data);
|
||||||
|
void (*set_activated)(struct kiwmi_view *view, bool activated);
|
||||||
|
struct wlr_surface *(*surface_at)(
|
||||||
|
struct kiwmi_view *view,
|
||||||
|
double sx,
|
||||||
|
double sy,
|
||||||
|
double *sub_x,
|
||||||
|
double *sub_y);
|
||||||
};
|
};
|
||||||
|
|
||||||
void kiwmi_view_for_each_surface(
|
void view_for_each_surface(
|
||||||
struct kiwmi_view *view,
|
struct kiwmi_view *view,
|
||||||
wlr_surface_iterator_func_t iterator,
|
wlr_surface_iterator_func_t iterator,
|
||||||
void *user_data);
|
void *user_data);
|
||||||
|
void view_set_activated(struct kiwmi_view *view, bool activated);
|
||||||
|
struct wlr_surface *view_surface_at(
|
||||||
|
struct kiwmi_view *view,
|
||||||
|
double sx,
|
||||||
|
double sy,
|
||||||
|
double *sub_x,
|
||||||
|
double *sub_y);
|
||||||
|
|
||||||
void focus_view(struct kiwmi_view *view, struct wlr_surface *surface);
|
void focus_view(struct kiwmi_view *view);
|
||||||
struct kiwmi_view *view_create(struct kiwmi_desktop *desktop, enum kiwmi_view_type type, const struct kiwmi_view_impl *impl);
|
struct kiwmi_view *view_at(
|
||||||
|
struct kiwmi_desktop *desktop,
|
||||||
|
double lx,
|
||||||
|
double ly,
|
||||||
|
struct wlr_surface **surface,
|
||||||
|
double *sx,
|
||||||
|
double *sy);
|
||||||
|
struct kiwmi_view *view_create(
|
||||||
|
struct kiwmi_desktop *desktop,
|
||||||
|
enum kiwmi_view_type type,
|
||||||
|
const struct kiwmi_view_impl *impl);
|
||||||
|
|
||||||
#endif /* KIWMI_DESKTOP_VIEW_H */
|
#endif /* KIWMI_DESKTOP_VIEW_H */
|
||||||
|
|
|
@ -12,12 +12,16 @@
|
||||||
#include <wlr/types/wlr_output_layout.h>
|
#include <wlr/types/wlr_output_layout.h>
|
||||||
|
|
||||||
struct kiwmi_cursor {
|
struct kiwmi_cursor {
|
||||||
|
struct kiwmi_server *server;
|
||||||
struct wlr_cursor *cursor;
|
struct wlr_cursor *cursor;
|
||||||
struct wlr_xcursor_manager *xcursor_manager;
|
struct wlr_xcursor_manager *xcursor_manager;
|
||||||
struct wl_listener cursor_motion;
|
struct wl_listener cursor_motion;
|
||||||
struct wl_listener cursor_motion_absolute;
|
struct wl_listener cursor_motion_absolute;
|
||||||
|
struct wl_listener cursor_button;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct kiwmi_cursor *cursor_create(struct wlr_output_layout *output_layout);
|
struct kiwmi_cursor *cursor_create(
|
||||||
|
struct kiwmi_server *server,
|
||||||
|
struct wlr_output_layout *output_layout);
|
||||||
|
|
||||||
#endif /* KIWMI_INPUT_CURSOR_H */
|
#endif /* KIWMI_INPUT_CURSOR_H */
|
||||||
|
|
|
@ -41,6 +41,8 @@ desktop_init(struct kiwmi_desktop *desktop, struct wlr_renderer *renderer)
|
||||||
&desktop->xdg_shell->events.new_surface,
|
&desktop->xdg_shell->events.new_surface,
|
||||||
&desktop->xdg_shell_new_surface);
|
&desktop->xdg_shell_new_surface);
|
||||||
|
|
||||||
|
desktop->focused_view = NULL;
|
||||||
|
|
||||||
wl_list_init(&desktop->outputs);
|
wl_list_init(&desktop->outputs);
|
||||||
wl_list_init(&desktop->views);
|
wl_list_init(&desktop->views);
|
||||||
|
|
||||||
|
|
|
@ -109,7 +109,7 @@ output_frame_notify(struct wl_listener *listener, void *data)
|
||||||
.when = &now,
|
.when = &now,
|
||||||
};
|
};
|
||||||
|
|
||||||
kiwmi_view_for_each_surface(view, render_surface, &rdata);
|
view_for_each_surface(view, render_surface, &rdata);
|
||||||
}
|
}
|
||||||
|
|
||||||
wlr_output_render_software_cursors(wlr_output, NULL);
|
wlr_output_render_software_cursors(wlr_output, NULL);
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
#include "server.h"
|
#include "server.h"
|
||||||
|
|
||||||
void
|
void
|
||||||
kiwmi_view_for_each_surface(
|
view_for_each_surface(
|
||||||
struct kiwmi_view *view,
|
struct kiwmi_view *view,
|
||||||
wlr_surface_iterator_func_t iterator,
|
wlr_surface_iterator_func_t iterator,
|
||||||
void *user_data)
|
void *user_data)
|
||||||
|
@ -24,7 +24,28 @@ kiwmi_view_for_each_surface(
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
focus_view(struct kiwmi_view *view, struct wlr_surface *surface)
|
view_set_activated(struct kiwmi_view *view, bool activated)
|
||||||
|
{
|
||||||
|
if (view->impl->set_activated) {
|
||||||
|
view->impl->set_activated(view, activated);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct wlr_surface *
|
||||||
|
view_surface_at(
|
||||||
|
struct kiwmi_view *view,
|
||||||
|
double sx,
|
||||||
|
double sy,
|
||||||
|
double *sub_x,
|
||||||
|
double *sub_y)
|
||||||
|
{
|
||||||
|
if (view->impl->surface_at) {
|
||||||
|
return view->impl->surface_at(view, sx, sy, sub_x, sub_y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
focus_view(struct kiwmi_view *view)
|
||||||
{
|
{
|
||||||
if (!view) {
|
if (!view) {
|
||||||
return;
|
return;
|
||||||
|
@ -33,16 +54,13 @@ focus_view(struct kiwmi_view *view, struct wlr_surface *surface)
|
||||||
struct kiwmi_desktop *desktop = view->desktop;
|
struct kiwmi_desktop *desktop = view->desktop;
|
||||||
struct kiwmi_server *server = wl_container_of(desktop, server, desktop);
|
struct kiwmi_server *server = wl_container_of(desktop, server, desktop);
|
||||||
struct wlr_seat *seat = server->input.seat;
|
struct wlr_seat *seat = server->input.seat;
|
||||||
struct wlr_surface *prev_surface = seat->keyboard_state.focused_surface;
|
|
||||||
|
|
||||||
if (prev_surface == surface) {
|
if (view == desktop->focused_view) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prev_surface) {
|
if (desktop->focused_view) {
|
||||||
struct wlr_xdg_surface *previous = wlr_xdg_surface_from_wlr_surface(
|
view_set_activated(desktop->focused_view, false);
|
||||||
seat->keyboard_state.focused_surface);
|
|
||||||
wlr_xdg_toplevel_set_activated(previous, false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct wlr_keyboard *keyboard = wlr_seat_get_keyboard(seat);
|
struct wlr_keyboard *keyboard = wlr_seat_get_keyboard(seat);
|
||||||
|
@ -51,15 +69,61 @@ focus_view(struct kiwmi_view *view, struct wlr_surface *surface)
|
||||||
wl_list_remove(&view->link);
|
wl_list_remove(&view->link);
|
||||||
wl_list_insert(&desktop->views, &view->link);
|
wl_list_insert(&desktop->views, &view->link);
|
||||||
|
|
||||||
wlr_xdg_toplevel_set_activated(view->xdg_surface, true);
|
view_set_activated(view, true);
|
||||||
wlr_seat_keyboard_notify_enter(
|
wlr_seat_keyboard_notify_enter(
|
||||||
seat,
|
seat,
|
||||||
view->xdg_surface->surface,
|
view->wlr_surface,
|
||||||
keyboard->keycodes,
|
keyboard->keycodes,
|
||||||
keyboard->num_keycodes,
|
keyboard->num_keycodes,
|
||||||
&keyboard->modifiers);
|
&keyboard->modifiers);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
surface_at(
|
||||||
|
struct kiwmi_view *view,
|
||||||
|
struct wlr_surface **surface,
|
||||||
|
double lx,
|
||||||
|
double ly,
|
||||||
|
double *sx,
|
||||||
|
double *sy)
|
||||||
|
{
|
||||||
|
double view_sx = lx - view->x;
|
||||||
|
double view_sy = ly - view->y;
|
||||||
|
|
||||||
|
double _sx, _sy;
|
||||||
|
struct wlr_surface *_surface = NULL;
|
||||||
|
_surface = view_surface_at(view, view_sx, view_sy, &_sx, &_sy);
|
||||||
|
|
||||||
|
if (_surface != NULL) {
|
||||||
|
*sx = _sx;
|
||||||
|
*sy = _sy;
|
||||||
|
*surface = _surface;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct kiwmi_view *
|
||||||
|
view_at(
|
||||||
|
struct kiwmi_desktop *desktop,
|
||||||
|
double lx,
|
||||||
|
double ly,
|
||||||
|
struct wlr_surface **surface,
|
||||||
|
double *sx,
|
||||||
|
double *sy)
|
||||||
|
{
|
||||||
|
struct kiwmi_view *view;
|
||||||
|
wl_list_for_each(view, &desktop->views, link)
|
||||||
|
{
|
||||||
|
if (surface_at(view, surface, lx, ly, sx, sy)) {
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
struct kiwmi_view *
|
struct kiwmi_view *
|
||||||
view_create(
|
view_create(
|
||||||
struct kiwmi_desktop *desktop,
|
struct kiwmi_desktop *desktop,
|
||||||
|
|
|
@ -19,7 +19,7 @@ xdg_surface_map_notify(struct wl_listener *listener, void *UNUSED(data))
|
||||||
{
|
{
|
||||||
struct kiwmi_view *view = wl_container_of(listener, view, map);
|
struct kiwmi_view *view = wl_container_of(listener, view, map);
|
||||||
view->mapped = true;
|
view->mapped = true;
|
||||||
focus_view(view, view->xdg_surface->surface);
|
focus_view(view);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
|
@ -32,7 +32,12 @@ xdg_surface_unmap_notify(struct wl_listener *listener, void *UNUSED(data))
|
||||||
static void
|
static void
|
||||||
xdg_surface_destroy_notify(struct wl_listener *listener, void *UNUSED(data))
|
xdg_surface_destroy_notify(struct wl_listener *listener, void *UNUSED(data))
|
||||||
{
|
{
|
||||||
struct kiwmi_view *view = wl_container_of(listener, view, destroy);
|
struct kiwmi_view *view = wl_container_of(listener, view, destroy);
|
||||||
|
struct kiwmi_desktop *desktop = view->desktop;
|
||||||
|
|
||||||
|
if (desktop->focused_view == view) {
|
||||||
|
desktop->focused_view = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
wl_list_remove(&view->link);
|
wl_list_remove(&view->link);
|
||||||
wl_list_remove(&view->map.link);
|
wl_list_remove(&view->map.link);
|
||||||
|
@ -51,8 +56,27 @@ xdg_shell_view_for_each_surface(
|
||||||
wlr_xdg_surface_for_each_surface(view->xdg_surface, iterator, user_data);
|
wlr_xdg_surface_for_each_surface(view->xdg_surface, iterator, user_data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
xdg_shell_view_set_activated(struct kiwmi_view *view, bool activated)
|
||||||
|
{
|
||||||
|
wlr_xdg_toplevel_set_activated(view->xdg_surface, activated);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct wlr_surface *
|
||||||
|
xdg_shell_view_surface_at(
|
||||||
|
struct kiwmi_view *view,
|
||||||
|
double sx,
|
||||||
|
double sy,
|
||||||
|
double *sub_x,
|
||||||
|
double *sub_y)
|
||||||
|
{
|
||||||
|
return wlr_xdg_surface_surface_at(view->xdg_surface, sx, sy, sub_x, sub_y);
|
||||||
|
}
|
||||||
|
|
||||||
static const struct kiwmi_view_impl xdg_shell_view_impl = {
|
static const struct kiwmi_view_impl xdg_shell_view_impl = {
|
||||||
.for_each_surface = xdg_shell_view_for_each_surface,
|
.for_each_surface = xdg_shell_view_for_each_surface,
|
||||||
|
.set_activated = xdg_shell_view_set_activated,
|
||||||
|
.surface_at = xdg_shell_view_surface_at,
|
||||||
};
|
};
|
||||||
|
|
||||||
void
|
void
|
||||||
|
@ -82,6 +106,7 @@ xdg_shell_new_surface_notify(struct wl_listener *listener, void *data)
|
||||||
}
|
}
|
||||||
|
|
||||||
view->xdg_surface = xdg_surface;
|
view->xdg_surface = xdg_surface;
|
||||||
|
view->wlr_surface = xdg_surface->surface;
|
||||||
|
|
||||||
view->map.notify = xdg_surface_map_notify;
|
view->map.notify = xdg_surface_map_notify;
|
||||||
wl_signal_add(&xdg_surface->events.map, &view->map);
|
wl_signal_add(&xdg_surface->events.map, &view->map);
|
||||||
|
|
|
@ -13,20 +13,59 @@
|
||||||
#include <wlr/types/wlr_cursor.h>
|
#include <wlr/types/wlr_cursor.h>
|
||||||
#include <wlr/types/wlr_output_layout.h>
|
#include <wlr/types/wlr_output_layout.h>
|
||||||
#include <wlr/types/wlr_pointer.h>
|
#include <wlr/types/wlr_pointer.h>
|
||||||
|
#include <wlr/types/wlr_seat.h>
|
||||||
#include <wlr/types/wlr_xcursor_manager.h>
|
#include <wlr/types/wlr_xcursor_manager.h>
|
||||||
#include <wlr/util/log.h>
|
#include <wlr/util/log.h>
|
||||||
|
|
||||||
|
#include "desktop/desktop.h"
|
||||||
|
#include "desktop/view.h"
|
||||||
|
#include "server.h"
|
||||||
|
|
||||||
|
static void
|
||||||
|
process_cursor_motion(struct kiwmi_server *server, uint32_t time)
|
||||||
|
{
|
||||||
|
struct kiwmi_desktop *desktop = &server->desktop;
|
||||||
|
struct kiwmi_input *input = &server->input;
|
||||||
|
struct kiwmi_cursor *cursor = input->cursor;
|
||||||
|
struct wlr_seat *seat = input->seat;
|
||||||
|
|
||||||
|
struct wlr_surface *surface = NULL;
|
||||||
|
double sx;
|
||||||
|
double sy;
|
||||||
|
|
||||||
|
struct kiwmi_view *view = view_at(
|
||||||
|
desktop, cursor->cursor->x, cursor->cursor->y, &surface, &sx, &sy);
|
||||||
|
|
||||||
|
if (!view) {
|
||||||
|
wlr_xcursor_manager_set_cursor_image(
|
||||||
|
cursor->xcursor_manager, "left_ptr", cursor->cursor);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (surface) {
|
||||||
|
bool focus_changed = surface != seat->pointer_state.focused_surface;
|
||||||
|
|
||||||
|
wlr_seat_pointer_notify_enter(seat, surface, sx, sy);
|
||||||
|
|
||||||
|
if (!focus_changed) {
|
||||||
|
wlr_seat_pointer_notify_motion(seat, time, sx, sy);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
wlr_seat_pointer_clear_focus(seat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
cursor_motion_notify(struct wl_listener *listener, void *data)
|
cursor_motion_notify(struct wl_listener *listener, void *data)
|
||||||
{
|
{
|
||||||
struct kiwmi_cursor *cursor =
|
struct kiwmi_cursor *cursor =
|
||||||
wl_container_of(listener, cursor, cursor_motion);
|
wl_container_of(listener, cursor, cursor_motion);
|
||||||
|
struct kiwmi_server *server = cursor->server;
|
||||||
struct wlr_event_pointer_motion *event = data;
|
struct wlr_event_pointer_motion *event = data;
|
||||||
|
|
||||||
wlr_xcursor_manager_set_cursor_image(
|
|
||||||
cursor->xcursor_manager, "left_ptr", cursor->cursor);
|
|
||||||
wlr_cursor_move(
|
wlr_cursor_move(
|
||||||
cursor->cursor, event->device, event->delta_x, event->delta_y);
|
cursor->cursor, event->device, event->delta_x, event->delta_y);
|
||||||
|
|
||||||
|
process_cursor_motion(server, event->time_msec);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
|
@ -34,15 +73,47 @@ cursor_motion_absolute_notify(struct wl_listener *listener, void *data)
|
||||||
{
|
{
|
||||||
struct kiwmi_cursor *cursor =
|
struct kiwmi_cursor *cursor =
|
||||||
wl_container_of(listener, cursor, cursor_motion_absolute);
|
wl_container_of(listener, cursor, cursor_motion_absolute);
|
||||||
|
struct kiwmi_server *server = cursor->server;
|
||||||
struct wlr_event_pointer_motion_absolute *event = data;
|
struct wlr_event_pointer_motion_absolute *event = data;
|
||||||
|
|
||||||
wlr_xcursor_manager_set_cursor_image(
|
|
||||||
cursor->xcursor_manager, "left_ptr", cursor->cursor);
|
|
||||||
wlr_cursor_warp_absolute(cursor->cursor, event->device, event->x, event->y);
|
wlr_cursor_warp_absolute(cursor->cursor, event->device, event->x, event->y);
|
||||||
|
|
||||||
|
process_cursor_motion(server, event->time_msec);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
cursor_button_notify(struct wl_listener *listener, void *data)
|
||||||
|
{
|
||||||
|
struct kiwmi_cursor *cursor =
|
||||||
|
wl_container_of(listener, cursor, cursor_button);
|
||||||
|
struct kiwmi_server *server = cursor->server;
|
||||||
|
struct kiwmi_input *input = &server->input;
|
||||||
|
struct wlr_event_pointer_button *event = data;
|
||||||
|
|
||||||
|
wlr_seat_pointer_notify_button(
|
||||||
|
input->seat, event->time_msec, event->button, event->state);
|
||||||
|
|
||||||
|
double sx;
|
||||||
|
double sy;
|
||||||
|
struct wlr_surface *surface;
|
||||||
|
|
||||||
|
struct kiwmi_view *view = view_at(
|
||||||
|
&server->desktop,
|
||||||
|
cursor->cursor->x,
|
||||||
|
cursor->cursor->y,
|
||||||
|
&surface,
|
||||||
|
&sx,
|
||||||
|
&sy);
|
||||||
|
|
||||||
|
if (event->state == WLR_BUTTON_PRESSED) {
|
||||||
|
focus_view(view);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct kiwmi_cursor *
|
struct kiwmi_cursor *
|
||||||
cursor_create(struct wlr_output_layout *output_layout)
|
cursor_create(
|
||||||
|
struct kiwmi_server *server,
|
||||||
|
struct wlr_output_layout *output_layout)
|
||||||
{
|
{
|
||||||
wlr_log(WLR_DEBUG, "Creating cursor");
|
wlr_log(WLR_DEBUG, "Creating cursor");
|
||||||
|
|
||||||
|
@ -52,6 +123,8 @@ cursor_create(struct wlr_output_layout *output_layout)
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cursor->server = server;
|
||||||
|
|
||||||
cursor->cursor = wlr_cursor_create();
|
cursor->cursor = wlr_cursor_create();
|
||||||
if (!cursor->cursor) {
|
if (!cursor->cursor) {
|
||||||
wlr_log(WLR_ERROR, "Failed to create cursor");
|
wlr_log(WLR_ERROR, "Failed to create cursor");
|
||||||
|
@ -71,5 +144,8 @@ cursor_create(struct wlr_output_layout *output_layout)
|
||||||
&cursor->cursor->events.motion_absolute,
|
&cursor->cursor->events.motion_absolute,
|
||||||
&cursor->cursor_motion_absolute);
|
&cursor->cursor_motion_absolute);
|
||||||
|
|
||||||
|
cursor->cursor_button.notify = cursor_button_notify;
|
||||||
|
wl_signal_add(&cursor->cursor->events.button, &cursor->cursor_button);
|
||||||
|
|
||||||
return cursor;
|
return cursor;
|
||||||
}
|
}
|
||||||
|
|
|
@ -71,7 +71,7 @@ input_init(struct kiwmi_input *input)
|
||||||
{
|
{
|
||||||
struct kiwmi_server *server = wl_container_of(input, server, input);
|
struct kiwmi_server *server = wl_container_of(input, server, input);
|
||||||
|
|
||||||
input->cursor = cursor_create(server->desktop.output_layout);
|
input->cursor = cursor_create(server, server->desktop.output_layout);
|
||||||
if (!input->cursor) {
|
if (!input->cursor) {
|
||||||
wlr_log(WLR_ERROR, "Failed to create cursor");
|
wlr_log(WLR_ERROR, "Failed to create cursor");
|
||||||
return false;
|
return false;
|
||||||
|
|
Loading…
Reference in a new issue