Implemented working asynchronous mouse tracking, dropped synchronous tracking. Selecting text is now also much faster. Auto scrolling is missing for the moment, though
git-svn-id: file:///srv/svn/repos/haiku/haiku/trunk@18942 a95241bf-73f2-0310-859d-f6bbb57e9c96
This commit is contained in:
+170
-182
@@ -88,20 +88,36 @@ enum {
|
|||||||
} separatorCharacters;
|
} separatorCharacters;
|
||||||
|
|
||||||
|
|
||||||
// _BTextTrackState_ class -----------------------------------------------------
|
|
||||||
class _BTextTrackState_ {
|
class _BTextTrackState_ {
|
||||||
|
|
||||||
// TODO: Implement ?
|
|
||||||
// It's most probably used to keep track of asynchronous mouse
|
|
||||||
// movements
|
|
||||||
public:
|
public:
|
||||||
_BTextTrackState_(bool inSelection)
|
_BTextTrackState_(BMessenger messenger)
|
||||||
: fMoved(false),
|
:
|
||||||
fInSelection(inSelection)
|
clickOffset(0),
|
||||||
{}
|
shiftDown(false),
|
||||||
|
anchor(0),
|
||||||
|
selStart(0),
|
||||||
|
selEnd(0),
|
||||||
|
fRunner(NULL)
|
||||||
|
{
|
||||||
|
BMessage message(_PING_);
|
||||||
|
//fRunner = new (nothrow) BMessageRunner(messenger, &message, 30000);
|
||||||
|
}
|
||||||
|
|
||||||
bool fMoved;
|
~_BTextTrackState_()
|
||||||
bool fInSelection;
|
{
|
||||||
|
delete fRunner;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32 clickOffset;
|
||||||
|
bool shiftDown;
|
||||||
|
BRect selectionRect;
|
||||||
|
|
||||||
|
int32 anchor;
|
||||||
|
int32 selStart;
|
||||||
|
int32 selEnd;
|
||||||
|
|
||||||
|
private:
|
||||||
|
BMessageRunner *fRunner;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -110,9 +126,11 @@ _BWidthBuffer_* BTextView::sWidths = NULL;
|
|||||||
sem_id BTextView::sWidthSem = B_BAD_SEM_ID;
|
sem_id BTextView::sWidthSem = B_BAD_SEM_ID;
|
||||||
int32 BTextView::sWidthAtom = 0;
|
int32 BTextView::sWidthAtom = 0;
|
||||||
|
|
||||||
|
|
||||||
const static rgb_color kBlackColor = { 0, 0, 0, 255 };
|
const static rgb_color kBlackColor = { 0, 0, 0, 255 };
|
||||||
const static rgb_color kBlueInputColor = { 152, 203, 255, 255 };
|
const static rgb_color kBlueInputColor = { 152, 203, 255, 255 };
|
||||||
const static rgb_color kRedInputColor = { 255, 152, 152, 255 };
|
const static rgb_color kRedInputColor = { 255, 152, 152, 255 };
|
||||||
|
|
||||||
|
|
||||||
static property_info
|
static property_info
|
||||||
sPropertyList[] = {
|
sPropertyList[] = {
|
||||||
@@ -296,6 +314,8 @@ BTextView::~BTextView()
|
|||||||
delete fStyles;
|
delete fStyles;
|
||||||
delete fDisallowedChars;
|
delete fDisallowedChars;
|
||||||
delete fUndo;
|
delete fUndo;
|
||||||
|
delete fClickRunner;
|
||||||
|
delete fDragRunner;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -438,7 +458,6 @@ BTextView::Draw(BRect updateRect)
|
|||||||
void
|
void
|
||||||
BTextView::MouseDown(BPoint where)
|
BTextView::MouseDown(BPoint where)
|
||||||
{
|
{
|
||||||
CALLED();
|
|
||||||
// should we even bother?
|
// should we even bother?
|
||||||
if (!fEditable && !fSelectable)
|
if (!fEditable && !fSelectable)
|
||||||
return;
|
return;
|
||||||
@@ -452,158 +471,85 @@ BTextView::MouseDown(BPoint where)
|
|||||||
if (fCaretVisible)
|
if (fCaretVisible)
|
||||||
InvertCaret();
|
InvertCaret();
|
||||||
|
|
||||||
int32 oldOffset = fClickOffset;
|
StopMouseTracking();
|
||||||
fClickOffset = OffsetAt(where);
|
|
||||||
|
|
||||||
int32 modifiers = 0;
|
BMessenger messenger(this);
|
||||||
BMessage *currentMessage = Window()->CurrentMessage();
|
fTrackingMouse = new (nothrow) _BTextTrackState_(messenger);
|
||||||
if (currentMessage != NULL)
|
if (fTrackingMouse == NULL)
|
||||||
currentMessage->FindInt32("modifiers", &modifiers);
|
return;
|
||||||
bool shiftDown = modifiers & B_SHIFT_KEY;
|
|
||||||
|
|
||||||
// should we initiate a drag?
|
int32 modifiers = 0;
|
||||||
if (fSelStart != fSelEnd && !shiftDown) {
|
//uint32 buttons;
|
||||||
BPoint loc;
|
BMessage *currentMessage = Window()->CurrentMessage();
|
||||||
ulong buttons;
|
if (currentMessage != NULL) {
|
||||||
GetMouse(&loc, &buttons);
|
currentMessage->FindInt32("modifiers", &modifiers);
|
||||||
// TODO: here we shouldn't check for B_SECONDARY_MOUSE_BUTTON,
|
//currentMessage->FindInt32("buttons", (int32 *)&buttons);
|
||||||
// since dragging works also with the primary button.
|
|
||||||
if (buttons == B_SECONDARY_MOUSE_BUTTON) {
|
|
||||||
// was the click within the selection range?
|
|
||||||
if (fClickOffset >= fSelStart && fClickOffset <= fSelEnd) {
|
|
||||||
InitiateDrag();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unhighlights the selection if needed
|
fTrackingMouse->clickOffset = OffsetAt(where);
|
||||||
if (fSelStart != fSelEnd)
|
fTrackingMouse->shiftDown = modifiers & B_SHIFT_KEY;
|
||||||
Select(fSelStart, fSelEnd);
|
|
||||||
|
bigtime_t clickTime = system_time();
|
||||||
// get the system-wide click speed
|
|
||||||
bigtime_t clickSpeed = 0;
|
bigtime_t clickSpeed = 0;
|
||||||
get_click_speed(&clickSpeed);
|
get_click_speed(&clickSpeed);
|
||||||
|
bool multipleClick = false;
|
||||||
// TODO: it looks like the input server sends the number of clicks in the
|
if (clickTime - fClickTime < clickSpeed && fClickOffset == fTrackingMouse->clickOffset)
|
||||||
// B_MOUSE_DOWN message (the "clicks" field), so we could use it instead
|
multipleClick = true;
|
||||||
// of doing this mess.
|
|
||||||
|
|
||||||
// is this a double/triple click, or is it a new click?
|
fWhere = where;
|
||||||
if (clickSpeed > (system_time() - fClickTime) &&
|
|
||||||
oldOffset == fClickOffset ) {
|
SetMouseEventMask(B_POINTER_EVENTS | B_KEYBOARD_EVENTS, B_LOCK_WINDOW_FOCUS | B_NO_POINTER_HISTORY);
|
||||||
|
|
||||||
|
if (fSelStart != fSelEnd && !fTrackingMouse->shiftDown && !multipleClick) {
|
||||||
|
BRegion region;
|
||||||
|
GetTextRegion(fSelStart, fSelEnd, ®ion);
|
||||||
|
if (region.Contains(where)) {
|
||||||
|
// Setup things for dragging
|
||||||
|
fTrackingMouse->selectionRect = region.Frame();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (multipleClick) {
|
||||||
if (fClickCount > 1) {
|
if (fClickCount > 1) {
|
||||||
// triple click
|
|
||||||
fClickCount = 0;
|
fClickCount = 0;
|
||||||
fClickTime = 0;
|
fClickTime = 0;
|
||||||
} else {
|
} else {
|
||||||
// double click
|
|
||||||
fClickCount = 2;
|
fClickCount = 2;
|
||||||
fClickTime = system_time();
|
fClickTime = clickTime;
|
||||||
int32 start, end;
|
|
||||||
FindWord(fClickOffset, &start, &end);
|
|
||||||
Select(start, end);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// new click
|
fClickOffset = fTrackingMouse->clickOffset;
|
||||||
fClickCount = 1;
|
fClickCount = 1;
|
||||||
fClickTime = system_time();
|
fClickTime = clickTime;
|
||||||
|
|
||||||
if (!shiftDown) {
|
// Deselect any previously selected text
|
||||||
Select(fClickOffset, fClickOffset);
|
if (!fTrackingMouse->shiftDown)
|
||||||
if (fEditable)
|
Select(fTrackingMouse->clickOffset, fTrackingMouse->clickOffset);
|
||||||
InvertCaret();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// no need to track the mouse if we can't select
|
if (fClickTime == clickTime) {
|
||||||
if (!fSelectable)
|
BMessage message(_PING_);
|
||||||
return;
|
message.AddInt64("clickTime", clickTime);
|
||||||
|
delete fClickRunner;
|
||||||
|
|
||||||
if (Window()->Flags() & B_ASYNCHRONOUS_CONTROLS) {
|
BMessenger messenger(this);
|
||||||
SetMouseEventMask(B_POINTER_EVENTS | B_KEYBOARD_EVENTS,
|
fClickRunner = new BMessageRunner(messenger, &message, clickSpeed, 1);
|
||||||
B_LOCK_WINDOW_FOCUS | B_NO_POINTER_HISTORY);
|
|
||||||
BMessage pingMessage(_PING_);
|
|
||||||
fClickRunner = new BMessageRunner(this, &pingMessage, 100000);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// track the mouse while it's down
|
|
||||||
int32 start = 0;
|
if (!fSelectable) {
|
||||||
int32 end = 0;
|
StopMouseTracking();
|
||||||
int32 anchor = (fClickOffset > fSelStart) ? fSelStart : fSelEnd;
|
return;
|
||||||
BPoint curMouse = where;
|
}
|
||||||
ulong buttons = 0;
|
|
||||||
do {
|
|
||||||
if (fClickOffset > anchor) {
|
|
||||||
start = anchor;
|
|
||||||
end = fClickOffset;
|
|
||||||
} else {
|
|
||||||
start = fClickOffset;
|
|
||||||
end = anchor;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (fClickCount) {
|
|
||||||
case 0:
|
|
||||||
// triple click, select line by line
|
|
||||||
start = (*fLines)[LineAt(start)]->offset;
|
|
||||||
end = (*fLines)[LineAt(end) + 1]->offset;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 2:
|
|
||||||
// double click, select word by word
|
|
||||||
FindWord(fClickOffset, &start, &end);
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
// new click, select char by char
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shiftDown) {
|
int32 offset = fSelStart;
|
||||||
if (fClickOffset > anchor)
|
if (fTrackingMouse->clickOffset > fSelStart)
|
||||||
start = anchor;
|
offset = fSelEnd;
|
||||||
else
|
|
||||||
end = anchor;
|
|
||||||
}
|
|
||||||
|
|
||||||
Select(start, end);
|
fTrackingMouse->anchor = offset;
|
||||||
|
|
||||||
// Should we scroll the view?
|
MouseMoved(where, B_INSIDE_VIEW, NULL);
|
||||||
// TODO: the implementation is incorrect, as I think a BTextView
|
|
||||||
// scrolls regardless of the fact if it is targeted by scroll bars.
|
|
||||||
// Also: auto scrolling can be disabled, I think.
|
|
||||||
if (!Bounds().Contains(curMouse)) {
|
|
||||||
float hDelta = 0;
|
|
||||||
float vDelta = 0;
|
|
||||||
if (ScrollBar(B_HORIZONTAL) != NULL) {
|
|
||||||
if (curMouse.x < Bounds().left)
|
|
||||||
hDelta = curMouse.x - Bounds().left;
|
|
||||||
else {
|
|
||||||
if (curMouse.x > Bounds().right)
|
|
||||||
hDelta = curMouse.x - Bounds().right;
|
|
||||||
}
|
|
||||||
if (hDelta != 0)
|
|
||||||
ScrollBar(B_HORIZONTAL)->SetValue(ScrollBar(B_HORIZONTAL)->Value() + hDelta);
|
|
||||||
}
|
|
||||||
if (ScrollBar(B_VERTICAL) != NULL) {
|
|
||||||
if (curMouse.y < Bounds().top)
|
|
||||||
vDelta = curMouse.y - Bounds().top;
|
|
||||||
else if (curMouse.y > Bounds().bottom)
|
|
||||||
vDelta = curMouse.y - Bounds().bottom;
|
|
||||||
if (vDelta != 0)
|
|
||||||
ScrollBar(B_VERTICAL)->SetValue(ScrollBar(B_VERTICAL)->Value() + vDelta);
|
|
||||||
}
|
|
||||||
if (hDelta != 0 || vDelta != 0)
|
|
||||||
Window()->UpdateIfNeeded();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Zzzz...
|
|
||||||
snooze(30000);
|
|
||||||
|
|
||||||
GetMouse(&curMouse, &buttons);
|
|
||||||
fClickOffset = OffsetAt(curMouse);
|
|
||||||
} while (buttons != 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -616,7 +562,11 @@ BTextView::MouseDown(BPoint where)
|
|||||||
void
|
void
|
||||||
BTextView::MouseUp(BPoint where)
|
BTextView::MouseUp(BPoint where)
|
||||||
{
|
{
|
||||||
|
BView::MouseUp(where);
|
||||||
PerformMouseUp(where);
|
PerformMouseUp(where);
|
||||||
|
|
||||||
|
delete fDragRunner;
|
||||||
|
fDragRunner = NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -946,9 +896,28 @@ BTextView::MessageReceived(BMessage *message)
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Find out what these two do
|
|
||||||
case _PING_:
|
case _PING_:
|
||||||
|
{
|
||||||
|
if (message->HasInt64("clickTime")) {
|
||||||
|
bigtime_t clickTime;
|
||||||
|
message->FindInt64("clickTime", &clickTime);
|
||||||
|
if (clickTime == fClickTime) {
|
||||||
|
if (fSelStart != fSelEnd && fSelectable) {
|
||||||
|
BRegion region;
|
||||||
|
GetTextRegion(fSelStart, fSelEnd, ®ion);
|
||||||
|
if (region.Contains(fWhere))
|
||||||
|
TrackMouse(fWhere, NULL);
|
||||||
|
}
|
||||||
|
delete fClickRunner;
|
||||||
|
fClickRunner = NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case _DISPOSE_DRAG_:
|
case _DISPOSE_DRAG_:
|
||||||
|
if (fEditable)
|
||||||
|
TrackDrag(fWhere);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -971,7 +940,6 @@ BTextView::ResolveSpecifier(BMessage *message, int32 index,
|
|||||||
BMessage *specifier, int32 what,
|
BMessage *specifier, int32 what,
|
||||||
const char *property)
|
const char *property)
|
||||||
{
|
{
|
||||||
CALLED();
|
|
||||||
BPropertyInfo propInfo(sPropertyList);
|
BPropertyInfo propInfo(sPropertyList);
|
||||||
BHandler *target = this;
|
BHandler *target = this;
|
||||||
|
|
||||||
@@ -3797,25 +3765,20 @@ BTextView::StopMouseTracking()
|
|||||||
{
|
{
|
||||||
delete fTrackingMouse;
|
delete fTrackingMouse;
|
||||||
fTrackingMouse = NULL;
|
fTrackingMouse = NULL;
|
||||||
|
|
||||||
delete fClickRunner;
|
|
||||||
fClickRunner = NULL;
|
|
||||||
|
|
||||||
delete fDragRunner;
|
|
||||||
fDragRunner = NULL;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
bool
|
bool
|
||||||
BTextView::PerformMouseUp(BPoint where)
|
BTextView::PerformMouseUp(BPoint where)
|
||||||
{
|
{
|
||||||
if (fClickRunner == NULL)
|
if (fTrackingMouse == NULL)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
if (fTrackingMouse->selectionRect.IsValid() && fTrackingMouse->selectionRect.Contains(where))
|
||||||
|
Select(fTrackingMouse->clickOffset, fTrackingMouse->clickOffset);
|
||||||
|
|
||||||
StopMouseTracking();
|
StopMouseTracking();
|
||||||
|
|
||||||
fClickOffset = OffsetAt(where);
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3823,34 +3786,51 @@ BTextView::PerformMouseUp(BPoint where)
|
|||||||
bool
|
bool
|
||||||
BTextView::PerformMouseMoved(BPoint where, uint32 code)
|
BTextView::PerformMouseMoved(BPoint where, uint32 code)
|
||||||
{
|
{
|
||||||
if (fClickRunner == NULL)
|
fWhere = where;
|
||||||
|
|
||||||
|
if (fTrackingMouse == NULL)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
int32 start = 0, end = 0;
|
if (fTrackingMouse->selectionRect.IsValid() && fTrackingMouse->selectionRect.Contains(where)) {
|
||||||
int32 offset = OffsetAt(where);
|
StopMouseTracking();
|
||||||
|
InitiateDrag();
|
||||||
switch (fClickCount) {
|
return true;
|
||||||
case 1:
|
}
|
||||||
start = min_c(offset, fClickOffset);
|
|
||||||
end = max_c(offset, fClickOffset);
|
int32 oldOffset = fTrackingMouse->anchor;
|
||||||
break;
|
int32 currentOffset = OffsetAt(where);
|
||||||
|
if (oldOffset < currentOffset) {
|
||||||
case 2:
|
fTrackingMouse->selStart = oldOffset;
|
||||||
FindWord(offset, &start, &end);
|
fTrackingMouse->selEnd = currentOffset;
|
||||||
start = min_c(start, fClickOffset);
|
} else {
|
||||||
end = max_c(end, fClickOffset);
|
fTrackingMouse->selStart = currentOffset;
|
||||||
break;
|
fTrackingMouse->selEnd = oldOffset;
|
||||||
|
|
||||||
case 3:
|
|
||||||
//TODO: Multiline selection
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int32 start = fTrackingMouse->selStart;
|
||||||
|
int32 end = fTrackingMouse->selEnd;
|
||||||
|
|
||||||
|
switch (fClickCount) {
|
||||||
|
case 0:
|
||||||
|
// triple click, select line by line
|
||||||
|
start = (*fLines)[LineAt(start)]->offset;
|
||||||
|
end = (*fLines)[LineAt(end) + 1]->offset;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 2:
|
||||||
|
// double click, select word by word
|
||||||
|
FindWord(oldOffset, &start, &end);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
// new click, select char by char
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
Select(start, end);
|
Select(start, end);
|
||||||
|
|
||||||
|
TrackMouse(where, NULL);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3892,16 +3872,16 @@ BTextView::TrackDrag(BPoint where)
|
|||||||
void
|
void
|
||||||
BTextView::InitiateDrag()
|
BTextView::InitiateDrag()
|
||||||
{
|
{
|
||||||
BMessage *message = new BMessage(B_MIME_DATA);
|
BMessage *dragMessage = new BMessage(B_MIME_DATA);
|
||||||
BBitmap *dragBitmap = NULL;
|
BBitmap *dragBitmap = NULL;
|
||||||
BPoint bitmapPoint;
|
BPoint bitmapPoint;
|
||||||
BHandler *dragHandler = NULL;
|
BHandler *dragHandler = NULL;
|
||||||
|
|
||||||
GetDragParameters(message, &dragBitmap, &bitmapPoint, &dragHandler);
|
GetDragParameters(dragMessage, &dragBitmap, &bitmapPoint, &dragHandler);
|
||||||
SetViewCursor(B_CURSOR_SYSTEM_DEFAULT);
|
SetViewCursor(B_CURSOR_SYSTEM_DEFAULT);
|
||||||
|
|
||||||
if (dragBitmap != NULL)
|
if (dragBitmap != NULL)
|
||||||
DragMessage(message, dragBitmap, bitmapPoint, dragHandler);
|
DragMessage(dragMessage, dragBitmap, bitmapPoint, dragHandler);
|
||||||
else {
|
else {
|
||||||
BRegion region;
|
BRegion region;
|
||||||
GetTextRegion(fSelStart, fSelEnd, ®ion);
|
GetTextRegion(fSelStart, fSelEnd, ®ion);
|
||||||
@@ -3910,8 +3890,12 @@ BTextView::InitiateDrag()
|
|||||||
if (!bounds.Contains(dragRect))
|
if (!bounds.Contains(dragRect))
|
||||||
dragRect = bounds & dragRect;
|
dragRect = bounds & dragRect;
|
||||||
|
|
||||||
DragMessage(message, dragRect, dragHandler);
|
DragMessage(dragMessage, dragRect, dragHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BMessenger messenger(this);
|
||||||
|
BMessage message(_DISPOSE_DRAG_);
|
||||||
|
fDragRunner = new (nothrow) BMessageRunner(this, &message, 100000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -3933,6 +3917,10 @@ BTextView::MessageDropped(BMessage *inMessage, BPoint where, BPoint offset)
|
|||||||
internalDrop = true;
|
internalDrop = true;
|
||||||
|
|
||||||
DragCaret(-1);
|
DragCaret(-1);
|
||||||
|
|
||||||
|
delete fDragRunner;
|
||||||
|
fDragRunner = NULL;
|
||||||
|
|
||||||
TrackMouse(where, NULL);
|
TrackMouse(where, NULL);
|
||||||
|
|
||||||
// are we sure we like this message?
|
// are we sure we like this message?
|
||||||
@@ -4205,7 +4193,7 @@ BTextView::CharClassification(int32 offset) const
|
|||||||
int32
|
int32
|
||||||
BTextView::NextInitialByte(int32 offset) const
|
BTextView::NextInitialByte(int32 offset) const
|
||||||
{
|
{
|
||||||
if (ByteAt(offset) == '\0')
|
if (offset >= TextLength())
|
||||||
return offset;
|
return offset;
|
||||||
|
|
||||||
for (++offset; (ByteAt(offset) & 0xC0) == 0x80; ++offset)
|
for (++offset; (ByteAt(offset) & 0xC0) == 0x80; ++offset)
|
||||||
|
|||||||
Reference in New Issue
Block a user