add x11 clipboard
This commit is contained in:
parent
2af4a29314
commit
57e3634691
477
platform.d
477
platform.d
@ -159,6 +159,16 @@ struct InputEvent
|
||||
i32 rel_y;
|
||||
}
|
||||
|
||||
enum ClipboardMode
|
||||
{
|
||||
Clipboard,
|
||||
Primary,
|
||||
Selection = Primary,
|
||||
Secondary,
|
||||
Max
|
||||
}
|
||||
|
||||
alias CBM = ClipboardMode;
|
||||
|
||||
struct Inputs
|
||||
{
|
||||
@ -166,6 +176,24 @@ struct Inputs
|
||||
Arena arena;
|
||||
}
|
||||
|
||||
pragma(inline) bool
|
||||
Shift(Modifier md)
|
||||
{
|
||||
return cast(bool)(md & (MD.LeftShift | MD.RightShift));
|
||||
}
|
||||
|
||||
pragma(inline) bool
|
||||
Ctrl(Modifier md)
|
||||
{
|
||||
return cast(bool)(md & (MD.LeftCtrl | MD.RightCtrl));
|
||||
}
|
||||
|
||||
pragma(inline) bool
|
||||
Alt(Modifier md)
|
||||
{
|
||||
return cast(bool)(md & (MD.LeftAlt | MD.RightAlt));
|
||||
}
|
||||
|
||||
Inputs*
|
||||
GetEvents(PlatformWindow* window)
|
||||
{
|
||||
@ -347,6 +375,7 @@ import core.sys.posix.dlfcn;
|
||||
import core.sys.posix.sys.mman;
|
||||
import core.sys.linux.sys.inotify;
|
||||
import core.sys.linux.fcntl;
|
||||
import core.sys.posix.sys.time : timespec, timeval, gettimeofday;
|
||||
import core.sys.posix.unistd;
|
||||
import core.sys.posix.pthread : PThread = pthread_t,
|
||||
PThreadCond = pthread_cond_t,
|
||||
@ -358,10 +387,14 @@ import core.sys.posix.pthread : PThread = pthread_t,
|
||||
PThreadCondWait = pthread_cond_wait,
|
||||
PThreadMutexUnlock = pthread_mutex_unlock,
|
||||
PThreadCondSignal = pthread_cond_signal,
|
||||
PThreadExit = pthread_exit;
|
||||
PThreadExit = pthread_exit,
|
||||
PThreadCondTimedWait = pthread_cond_timedwait;
|
||||
|
||||
import core.stdc.string : strlen;
|
||||
|
||||
const u32 X11_CB_TRANSFER_SIZE_DEFAULT = 1048576;
|
||||
const u32 X11_TIMEOUT_DEFAULT = 1500;
|
||||
|
||||
struct SysThread
|
||||
{
|
||||
PThread handle;
|
||||
@ -369,6 +402,40 @@ struct SysThread
|
||||
PThreadMutex mut;
|
||||
}
|
||||
|
||||
struct Selection
|
||||
{
|
||||
bool owned;
|
||||
u8[] data;
|
||||
xcb_atom_t target;
|
||||
xcb_atom_t xmode;
|
||||
}
|
||||
|
||||
enum Atoms
|
||||
{
|
||||
Targets,
|
||||
Multiple,
|
||||
Timestamp,
|
||||
Incr,
|
||||
Clipboard,
|
||||
Utf8String,
|
||||
WMProtocols,
|
||||
DeleteWindow,
|
||||
StateHidden,
|
||||
Max,
|
||||
}
|
||||
|
||||
const char[][] ATOM_STRS = [
|
||||
CastStr!(char)("TARGETS"),
|
||||
CastStr!(char)("MULTIPLE"),
|
||||
CastStr!(char)("TIMESTAMP"),
|
||||
CastStr!(char)("INCR"),
|
||||
CastStr!(char)("CLIPBOARD"),
|
||||
CastStr!(char)("UTF8_STRING"),
|
||||
CastStr!(char)("WM_PROTOCOLS"),
|
||||
CastStr!(char)("WM_DELETE_WINDOW"),
|
||||
CastStr!(char)("_NET_WM_STATE_HIDDEN"),
|
||||
];
|
||||
|
||||
alias PThreadProc = extern (C) void* function(void*);
|
||||
|
||||
SysThread
|
||||
@ -442,12 +509,11 @@ PushMotion(Inputs* inputs, i32 rel_x, i32 rel_y, i32 x, i32 y)
|
||||
|
||||
struct PlatformWindow
|
||||
{
|
||||
Display *display;
|
||||
xcb_connection_t *conn;
|
||||
xcb_screen_t *screen;
|
||||
xcb_atom_t[Atoms.max] atoms;
|
||||
Display* display;
|
||||
xcb_connection_t* conn;
|
||||
xcb_screen_t* screen;
|
||||
xcb_window_t window;
|
||||
xcb_atom_t close_event;
|
||||
xcb_atom_t minimize_event;
|
||||
u16 w;
|
||||
u16 h;
|
||||
i32 mouse_prev_x;
|
||||
@ -461,6 +527,10 @@ struct PlatformWindow
|
||||
u32 input_idx;
|
||||
Inputs[2] inputs;
|
||||
TicketMut input_mutex;
|
||||
|
||||
Mut cb_mut;
|
||||
Selection[CBM.max] selections;
|
||||
version(linux) u32 cb_transfer_size;
|
||||
};
|
||||
|
||||
struct Library
|
||||
@ -496,12 +566,18 @@ CreateWindow(string name, u16 width, u16 height)
|
||||
h: height,
|
||||
input_mutex: CreateTicketMut(),
|
||||
msg_queue: CreateMessageQueue(),
|
||||
cb_mut: CreateMut(),
|
||||
inputs: [
|
||||
{ arena: CreateArena(MB(1)) },
|
||||
{ arena: CreateArena(MB(1)) },
|
||||
],
|
||||
};
|
||||
|
||||
version(linux)
|
||||
{
|
||||
window.cb_transfer_size = X11_CB_TRANSFER_SIZE_DEFAULT;
|
||||
}
|
||||
|
||||
assert(width > 0 && height > 0, "CreateWindow error: width and height must be above 0");
|
||||
|
||||
window.display = XOpenDisplay(null);
|
||||
@ -523,7 +599,8 @@ CreateWindow(string name, u16 width, u16 height)
|
||||
XCB_EVENT_MASK_BUTTON_PRESS |
|
||||
XCB_EVENT_MASK_BUTTON_RELEASE |
|
||||
XCB_EVENT_MASK_POINTER_MOTION |
|
||||
XCB_EVENT_MASK_STRUCTURE_NOTIFY;
|
||||
XCB_EVENT_MASK_STRUCTURE_NOTIFY|
|
||||
XCB_EVENT_MASK_PROPERTY_CHANGE;
|
||||
|
||||
i32[2] val_win = [window.screen.black_pixel, event_mask];
|
||||
i32 val_mask = XCB_CW_BACK_PIXEL | XCB_CW_EVENT_MASK;
|
||||
@ -568,42 +645,36 @@ CreateWindow(string name, u16 width, u16 height)
|
||||
CheckErr(&window, &cookie, error, "xcb_change_property_checked failure");
|
||||
pureFree(error);
|
||||
|
||||
xcb_intern_atom_cookie_t c_proto = xcb_intern_atom(window.conn, 1, 12, "WM_PROTOCOLS");
|
||||
xcb_intern_atom_reply_t *r_proto = xcb_intern_atom_reply(window.conn, c_proto, &error);
|
||||
CheckErr(&window, &cookie, error, "xcb_intern_atom WM_PROTOCOLS failure");
|
||||
pureFree(error);
|
||||
for(u64 i = 0; i < Atoms.max; i += 1)
|
||||
{
|
||||
xcb_intern_atom_cookie_t intern = xcb_intern_atom(window.conn, 1, cast(u16)ATOM_STRS[i].length, ATOM_STRS[i].ptr);
|
||||
xcb_intern_atom_reply_t* reply = xcb_intern_atom_reply(window.conn, intern, &error);
|
||||
CheckErr(&window, &cookie, error, "xcb_intern_atom failure");
|
||||
|
||||
xcb_intern_atom_cookie_t c_close = xcb_intern_atom(window.conn, 0, 16, "WM_DELETE_WINDOW");
|
||||
xcb_intern_atom_reply_t *r_close = xcb_intern_atom_reply(window.conn, c_close, &error);
|
||||
CheckErr(&window, &cookie, error, "xcb_intern_atom WM_DELETE_WINDOW failure");
|
||||
pureFree(error);
|
||||
window.atoms[i] = reply.atom;
|
||||
|
||||
xcb_intern_atom_cookie_t c_minimize = xcb_intern_atom(window.conn, 0, 20, "_NET_WM_STATE_HIDDEN");
|
||||
xcb_intern_atom_reply_t *r_minimize = xcb_intern_atom_reply(window.conn, c_minimize, &error);
|
||||
CheckErr(&window, &cookie, error, "xcb_intern_atom _NET_WM_STATE_HIDDEN failure");
|
||||
pureFree(error);
|
||||
pureFree(reply);
|
||||
}
|
||||
|
||||
window.selections[CBM.Clipboard].xmode = window.atoms[Atoms.Clipboard];
|
||||
window.selections[CBM.Primary].xmode = XCB_ATOM_PRIMARY;
|
||||
window.selections[CBM.Secondary].xmode = XCB_ATOM_SECONDARY;
|
||||
|
||||
cookie = xcb_change_property_checked(
|
||||
window.conn,
|
||||
XCB_PROP_MODE_REPLACE,
|
||||
window.window,
|
||||
r_proto.atom,
|
||||
window.atoms[Atoms.WMProtocols],
|
||||
XCB_ATOM_ATOM,
|
||||
32,
|
||||
1,
|
||||
&r_close.atom
|
||||
&window.atoms[Atoms.DeleteWindow]
|
||||
);
|
||||
error = xcb_request_check(window.conn, cookie);
|
||||
CheckErr(&window, &cookie, error, "xcb_change_property_checked failure");
|
||||
pureFree(error);
|
||||
|
||||
window.close_event = r_close.atom;
|
||||
window.minimize_event = r_minimize.atom;
|
||||
|
||||
pureFree(r_proto);
|
||||
pureFree(r_close);
|
||||
pureFree(r_minimize);
|
||||
|
||||
xcb_map_window(window.conn, window.window);
|
||||
|
||||
i32 stream_result = xcb_flush(window.conn);
|
||||
@ -699,6 +770,333 @@ FlushEvents(PlatformWindow* window)
|
||||
} while (e);
|
||||
}
|
||||
|
||||
bool
|
||||
TransmitSelection(PlatformWindow* w, xcb_selection_request_event_t* ev)
|
||||
{
|
||||
bool result;
|
||||
if(ev.property == XCB_NONE)
|
||||
{
|
||||
ev.property = ev.target;
|
||||
}
|
||||
|
||||
if(ev.target == w.atoms[Atoms.Targets])
|
||||
{
|
||||
xcb_atom_t[3] targets = [
|
||||
w.atoms[Atoms.Timestamp],
|
||||
w.atoms[Atoms.Targets],
|
||||
w.atoms[Atoms.Utf8String]
|
||||
];
|
||||
|
||||
xcb_change_property(
|
||||
w.conn,
|
||||
XCB_PROP_MODE_REPLACE,
|
||||
ev.requestor,
|
||||
ev.property,
|
||||
XCB_ATOM_ATOM,
|
||||
xcb_atom_t.sizeof * 8,
|
||||
targets.length,
|
||||
targets.ptr
|
||||
);
|
||||
|
||||
result = true;
|
||||
}
|
||||
else if(ev.target == w.atoms[Atoms.Timestamp])
|
||||
{
|
||||
xcb_timestamp_t cur = XCB_CURRENT_TIME;
|
||||
xcb_change_property(
|
||||
w.conn,
|
||||
XCB_PROP_MODE_REPLACE,
|
||||
ev.requestor,
|
||||
ev.property,
|
||||
XCB_ATOM_INTEGER,
|
||||
cur.sizeof * 8,
|
||||
1,
|
||||
&cur
|
||||
);
|
||||
|
||||
result = true;
|
||||
}
|
||||
else if(ev.target == w.atoms[Atoms.Utf8String])
|
||||
{
|
||||
Selection* sel = null;
|
||||
if(TryLock(&w.cb_mut))
|
||||
{
|
||||
foreach(i; CBM.min .. CBM.max)
|
||||
{
|
||||
if(w.selections[i].xmode == ev.selection)
|
||||
{
|
||||
sel = &w.selections[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(sel != null && sel.owned && sel.data.length > 0 && sel.target == ev.target)
|
||||
{
|
||||
xcb_change_property(
|
||||
w.conn,
|
||||
XCB_PROP_MODE_REPLACE,
|
||||
ev.requestor,
|
||||
ev.property,
|
||||
ev.target,
|
||||
8,
|
||||
cast(u32)sel.data.length,
|
||||
sel.data.ptr
|
||||
);
|
||||
|
||||
result = true;
|
||||
}
|
||||
|
||||
Unlock(&w.cb_mut);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
bool
|
||||
ClipboardOwns(PlatformWindow* w, ClipboardMode mode)
|
||||
{
|
||||
bool result;
|
||||
|
||||
if(TryLock(&w.cb_mut))
|
||||
{
|
||||
result = w.selections[mode].owned;
|
||||
Unlock(&w.cb_mut);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
u8[]
|
||||
ClipboardText(PlatformWindow* w, ClipboardMode mode)
|
||||
{
|
||||
u8[] buf;
|
||||
|
||||
if(TryLock(&w.cb_mut))
|
||||
{
|
||||
scope(exit) Unlock(&w.cb_mut);
|
||||
|
||||
Selection* sel = &w.selections[mode];
|
||||
|
||||
if(sel.owned)
|
||||
{
|
||||
buf = GetClipboardSelection(w, sel);
|
||||
}
|
||||
else
|
||||
{
|
||||
timeval now;
|
||||
timespec timeout;
|
||||
int pret = 0;
|
||||
|
||||
auto owner = xcb_get_selection_owner_reply(w.conn, xcb_get_selection_owner(w.conn, sel.xmode), null);
|
||||
scope(exit) pureFree(owner);
|
||||
|
||||
if(owner != null && owner.owner != 0)
|
||||
{
|
||||
FreeArray(sel.data);
|
||||
sel.data = [];
|
||||
|
||||
sel.target = w.atoms[Atoms.Utf8String];
|
||||
xcb_convert_selection(w.conn, w.window, sel.xmode, sel.target, sel.xmode, XCB_CURRENT_TIME);
|
||||
xcb_flush(w.conn);
|
||||
|
||||
gettimeofday(&now, null);
|
||||
|
||||
timeout.tv_sec = now.tv_sec + (X11_TIMEOUT_DEFAULT / 1000);
|
||||
timeout.tv_nsec = (now.tv_usec * 1000UL) * ((X11_TIMEOUT_DEFAULT % 1000) * 1000000UL);
|
||||
if(timeout.tv_nsec >= 1000000000UL)
|
||||
{
|
||||
timeout.tv_sec += timeout.tv_nsec / 1000000000UL;
|
||||
timeout.tv_nsec = timeout.tv_nsec % 1000000000UL;
|
||||
}
|
||||
|
||||
while(pret == 0 && sel.data.length == 0)
|
||||
{
|
||||
pret = PThreadCondTimedWait(&w.thread.cond, &w.thread.mut, &timeout);
|
||||
}
|
||||
|
||||
GetClipboardSelection(w, sel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return buf;
|
||||
}
|
||||
|
||||
u8[]
|
||||
ClipboardText(PlatformWindow* w)
|
||||
{
|
||||
return ClipboardText(w, CBM.Clipboard);
|
||||
}
|
||||
|
||||
bool
|
||||
SetClipboard(PlatformWindow* w, u8[] data, ClipboardMode mode)
|
||||
{
|
||||
bool result = true;
|
||||
|
||||
if(data.length > 0 && TryLock(&w.cb_mut))
|
||||
{
|
||||
Selection* sel = &w.selections[mode];
|
||||
if(sel.data.length > 0)
|
||||
{
|
||||
FreeArray(sel.data);
|
||||
sel.data = [];
|
||||
}
|
||||
|
||||
sel.data = Alloc!(u8)(data.length+1);
|
||||
if(sel.data.length > 0)
|
||||
{
|
||||
MemCpy(sel.data.ptr, data.ptr, data.length);
|
||||
|
||||
sel.data[sel.data.length-1] = '\0';
|
||||
sel.owned = true;
|
||||
sel.target = w.atoms[Atoms.Utf8String];
|
||||
|
||||
xcb_set_selection_owner(w.conn, w.window, sel.xmode, XCB_CURRENT_TIME);
|
||||
xcb_flush(w.conn);
|
||||
}
|
||||
else
|
||||
{
|
||||
result = false;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
bool
|
||||
SetClipboard(PlatformWindow* w, u8[] data)
|
||||
{
|
||||
return SetClipboard(w, data, CBM.Clipboard);
|
||||
}
|
||||
|
||||
u8[]
|
||||
GetClipboardSelection(PlatformWindow* w, Selection* sel)
|
||||
{
|
||||
u8[] buf;
|
||||
|
||||
if(sel.data.length > 0 && sel.target == w.atoms[Atoms.Utf8String])
|
||||
{
|
||||
buf = ScratchAllocCopy(sel.data);
|
||||
}
|
||||
|
||||
return buf;
|
||||
}
|
||||
|
||||
void
|
||||
RetrieveSelection(PlatformWindow* w, xcb_selection_notify_event_t* ev)
|
||||
{
|
||||
u8[] buf;
|
||||
u64 buf_size;
|
||||
u64 bytes_after = 1;
|
||||
xcb_get_property_reply_t* reply;
|
||||
xcb_atom_t actual_type;
|
||||
u8 actual_format;
|
||||
|
||||
if(ev.property == XCB_ATOM_PRIMARY || ev.property == XCB_ATOM_SECONDARY || ev.property == w.atoms[Atoms.Clipboard])
|
||||
{
|
||||
while(bytes_after > 0)
|
||||
{
|
||||
scope(exit) pureFree(reply);
|
||||
|
||||
xcb_get_property_cookie_t cookie = xcb_get_property(
|
||||
w.conn,
|
||||
true,
|
||||
w.window,
|
||||
ev.property,
|
||||
XCB_ATOM_ANY,
|
||||
cast(u32)(buf_size/4),
|
||||
cast(u32)(w.cb_transfer_size/4)
|
||||
);
|
||||
|
||||
reply = xcb_get_property_reply(w.conn, cookie, null);
|
||||
|
||||
if(reply == null || (buf_size > 0 && (reply.format != actual_format || reply.type != actual_type)) || reply.format%8 != 0)
|
||||
{
|
||||
Errf("RetrieveSelection failure: Invalid return value from xcb_get_property_reply");
|
||||
break;
|
||||
}
|
||||
|
||||
if(buf_size == 0)
|
||||
{
|
||||
actual_type = reply.type;
|
||||
actual_format = reply.format;
|
||||
}
|
||||
|
||||
int nitems = xcb_get_property_value_length(reply);
|
||||
if(nitems > 0)
|
||||
{
|
||||
if(buf_size%4 != 0)
|
||||
{
|
||||
Errf("RetrieveSelection failure: Data size is not a multiple of 4");
|
||||
break;
|
||||
}
|
||||
|
||||
u64 unit_size = reply.format/8;
|
||||
buf = Alloc!(u8)(unit_size * (buf_size + nitems));
|
||||
|
||||
MemCpy(buf.ptr + buf_size, xcb_get_property_value(reply), nitems * unit_size);
|
||||
buf_size += nitems * unit_size;
|
||||
}
|
||||
|
||||
bytes_after = reply.bytes_after;
|
||||
}
|
||||
}
|
||||
|
||||
if(buf != null && TryLock(&w.cb_mut))
|
||||
{
|
||||
Selection* sel;
|
||||
foreach(i; CBM.min .. CBM.max)
|
||||
{
|
||||
if(w.selections[i].xmode == ev.property)
|
||||
{
|
||||
sel = &w.selections[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(sel != null && sel.target == actual_type)
|
||||
{
|
||||
FreeArray(sel.data);
|
||||
sel.data = buf;
|
||||
buf = [];
|
||||
}
|
||||
else
|
||||
{
|
||||
Errf("RetrieveSelection failure: mismatched selection actual_type: %s", actual_type);
|
||||
}
|
||||
|
||||
Unlock(&w.cb_mut);
|
||||
}
|
||||
else
|
||||
{
|
||||
FreeArray(buf);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
ClearSelection(PlatformWindow* w, xcb_selection_clear_event_t* ev)
|
||||
{
|
||||
if(ev.owner == w.window)
|
||||
{
|
||||
foreach(i; CBM.min .. CBM.max)
|
||||
{
|
||||
Selection* sel = &w.selections[i];
|
||||
if(sel.xmode == ev.selection && TryLock(&w.cb_mut))
|
||||
{
|
||||
FreeArray(sel.data);
|
||||
sel.data = [];
|
||||
sel.owned = false;
|
||||
sel.target = XCB_NONE;
|
||||
|
||||
Unlock(&w.cb_mut);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
HandleEvents(void* window_ptr)
|
||||
{
|
||||
@ -782,7 +1180,7 @@ HandleEvents(void* window_ptr)
|
||||
break;
|
||||
}
|
||||
|
||||
if(msg.data.data32[0] == w.close_event)
|
||||
if(msg.data.data32[0] == w.atoms[Atoms.DeleteWindow])
|
||||
{
|
||||
w.close = true;
|
||||
}
|
||||
@ -878,6 +1276,29 @@ HandleEvents(void* window_ptr)
|
||||
w.h = config_event.height;
|
||||
}
|
||||
} break;
|
||||
case XCB_SELECTION_CLEAR:
|
||||
{
|
||||
ClearSelection(w, cast(xcb_selection_clear_event_t*)e);
|
||||
} break;
|
||||
case XCB_SELECTION_NOTIFY:
|
||||
{
|
||||
RetrieveSelection(w, cast(xcb_selection_notify_event_t*)e);
|
||||
} break;
|
||||
case XCB_SELECTION_REQUEST:
|
||||
{
|
||||
auto req = cast(xcb_selection_request_event_t*)e;
|
||||
xcb_selection_notify_event_t notify = {
|
||||
response_type: XCB_SELECTION_NOTIFY,
|
||||
time: XCB_CURRENT_TIME,
|
||||
requestor: req.requestor,
|
||||
selection: req.selection,
|
||||
target: req.target,
|
||||
property: TransmitSelection(w, req) ? req.property : XCB_NONE,
|
||||
};
|
||||
|
||||
xcb_send_event(w.conn, false, req.requestor, XCB_EVENT_MASK_PROPERTY_CHANGE, cast(char*)¬ify);
|
||||
xcb_flush(w.conn);
|
||||
} break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
17
util.d
17
util.d
@ -6,7 +6,7 @@ import dlib.alloc;
|
||||
import xxhash3;
|
||||
import includes;
|
||||
|
||||
import std.stdio;
|
||||
import std.stdio : write, writeln, writefln, stderr;
|
||||
import std.conv;
|
||||
import std.string;
|
||||
import std.traits;
|
||||
@ -43,6 +43,7 @@ Logf(Args...)(string fmt, Args args)
|
||||
{
|
||||
try
|
||||
{
|
||||
write("[INFO]: ");
|
||||
writefln(fmt, args);
|
||||
}
|
||||
catch (Exception e)
|
||||
@ -51,6 +52,20 @@ Logf(Args...)(string fmt, Args args)
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
Errf(Args...)(string fmt, Args args)
|
||||
{
|
||||
try
|
||||
{
|
||||
stderr.write("[ERROR]: ");
|
||||
stderr.writef(fmt, args, '\n');
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
assert(false, "Incompatible format type");
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
Log(string str)
|
||||
{
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user