initial commit
Signed-off-by: Peter Siegmund <mars3142@noreply.mars3142.dev>
This commit is contained in:
570
libs/wxWidgets-3.3.1/tests/controls/textentrytest.cpp
Normal file
570
libs/wxWidgets-3.3.1/tests/controls/textentrytest.cpp
Normal file
@@ -0,0 +1,570 @@
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Name: tests/controls/textentrytest.cpp
|
||||
// Purpose: TestEntryTestCase implementation
|
||||
// Author: Vadim Zeitlin
|
||||
// Created: 2008-09-19 (extracted from textctrltest.cpp)
|
||||
// Copyright: (c) 2007, 2008 Vadim Zeitlin <vadim@wxwidgets.org>
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include "testprec.h"
|
||||
|
||||
#ifndef WX_PRECOMP
|
||||
#include "wx/app.h"
|
||||
#include "wx/dialog.h"
|
||||
#include "wx/event.h"
|
||||
#include "wx/sizer.h"
|
||||
#include "wx/textctrl.h"
|
||||
#include "wx/textentry.h"
|
||||
#include "wx/timer.h"
|
||||
#include "wx/window.h"
|
||||
#endif // WX_PRECOMP
|
||||
|
||||
#include "textentrytest.h"
|
||||
#include "testableframe.h"
|
||||
|
||||
#include "wx/uiaction.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
void TextEntryTestCase::SetValue()
|
||||
{
|
||||
wxTextEntry * const entry = GetTestEntry();
|
||||
|
||||
CPPUNIT_ASSERT( entry->IsEmpty() );
|
||||
|
||||
entry->SetValue("foo");
|
||||
CPPUNIT_ASSERT_EQUAL( "foo", entry->GetValue() );
|
||||
|
||||
entry->SetValue("");
|
||||
CPPUNIT_ASSERT( entry->IsEmpty() );
|
||||
|
||||
entry->SetValue("hi");
|
||||
CPPUNIT_ASSERT_EQUAL( "hi", entry->GetValue() );
|
||||
|
||||
entry->SetValue("bye");
|
||||
CPPUNIT_ASSERT_EQUAL( "bye", entry->GetValue() );
|
||||
}
|
||||
|
||||
void TextEntryTestCase::TextChangeEvents()
|
||||
{
|
||||
EventCounter updated(GetTestWindow(), wxEVT_TEXT);
|
||||
|
||||
wxTextEntry * const entry = GetTestEntry();
|
||||
|
||||
// notice that SetValue() generates an event even if the text didn't change
|
||||
entry->SetValue("");
|
||||
CPPUNIT_ASSERT_EQUAL( 1, updated.GetCount() );
|
||||
updated.Clear();
|
||||
|
||||
entry->SetValue("foo");
|
||||
CPPUNIT_ASSERT_EQUAL( 1, updated.GetCount() );
|
||||
updated.Clear();
|
||||
|
||||
entry->SetValue("foo");
|
||||
CPPUNIT_ASSERT_EQUAL( 1, updated.GetCount() );
|
||||
updated.Clear();
|
||||
|
||||
entry->SetValue("");
|
||||
CPPUNIT_ASSERT_EQUAL( 1, updated.GetCount() );
|
||||
updated.Clear();
|
||||
|
||||
entry->ChangeValue("bar");
|
||||
CPPUNIT_ASSERT_EQUAL( 0, updated.GetCount() );
|
||||
|
||||
entry->AppendText("bar");
|
||||
CPPUNIT_ASSERT_EQUAL( 1, updated.GetCount() );
|
||||
updated.Clear();
|
||||
|
||||
entry->Replace(3, 6, "baz");
|
||||
CPPUNIT_ASSERT_EQUAL( 1, updated.GetCount() );
|
||||
updated.Clear();
|
||||
|
||||
entry->Remove(0, 3);
|
||||
CPPUNIT_ASSERT_EQUAL( 1, updated.GetCount() );
|
||||
updated.Clear();
|
||||
|
||||
entry->WriteText("foo");
|
||||
CPPUNIT_ASSERT_EQUAL( 1, updated.GetCount() );
|
||||
updated.Clear();
|
||||
|
||||
entry->Clear();
|
||||
CPPUNIT_ASSERT_EQUAL( 1, updated.GetCount() );
|
||||
updated.Clear();
|
||||
|
||||
entry->ChangeValue("");
|
||||
CPPUNIT_ASSERT_EQUAL( 0, updated.GetCount() );
|
||||
updated.Clear();
|
||||
|
||||
entry->ChangeValue("non-empty");
|
||||
CPPUNIT_ASSERT_EQUAL( 0, updated.GetCount() );
|
||||
updated.Clear();
|
||||
|
||||
entry->ChangeValue("");
|
||||
CPPUNIT_ASSERT_EQUAL( 0, updated.GetCount() );
|
||||
updated.Clear();
|
||||
}
|
||||
|
||||
void TextEntryTestCase::CheckStringSelection(const char *sel)
|
||||
{
|
||||
CPPUNIT_ASSERT_EQUAL( sel, GetTestEntry()->GetStringSelection() );
|
||||
}
|
||||
|
||||
void TextEntryTestCase::AssertSelection(int from, int to, const char *sel)
|
||||
{
|
||||
wxTextEntry * const entry = GetTestEntry();
|
||||
|
||||
CPPUNIT_ASSERT( entry->HasSelection() );
|
||||
|
||||
long fromReal,
|
||||
toReal;
|
||||
entry->GetSelection(&fromReal, &toReal);
|
||||
CPPUNIT_ASSERT_EQUAL( from, fromReal );
|
||||
CPPUNIT_ASSERT_EQUAL( to, toReal );
|
||||
|
||||
CPPUNIT_ASSERT_EQUAL( from, entry->GetInsertionPoint() );
|
||||
|
||||
CheckStringSelection(sel);
|
||||
}
|
||||
|
||||
void TextEntryTestCase::Selection()
|
||||
{
|
||||
wxTextEntry * const entry = GetTestEntry();
|
||||
|
||||
entry->SetValue("0123456789");
|
||||
|
||||
entry->SetSelection(2, 4);
|
||||
AssertSelection(2, 4, "23"); // not "234"!
|
||||
|
||||
entry->SetSelection(3, -1);
|
||||
AssertSelection(3, 10, "3456789");
|
||||
|
||||
entry->SelectAll();
|
||||
AssertSelection(0, 10, "0123456789");
|
||||
|
||||
entry->SetSelection(0, 0);
|
||||
CPPUNIT_ASSERT( !entry->HasSelection() );
|
||||
}
|
||||
|
||||
void TextEntryTestCase::InsertionPoint()
|
||||
{
|
||||
wxTextEntry * const entry = GetTestEntry();
|
||||
|
||||
CPPUNIT_ASSERT_EQUAL( 0, entry->GetLastPosition() );
|
||||
CPPUNIT_ASSERT_EQUAL( 0, entry->GetInsertionPoint() );
|
||||
|
||||
entry->SetValue("0"); // should put the insertion point in front
|
||||
CPPUNIT_ASSERT_EQUAL( 1, entry->GetLastPosition() );
|
||||
CPPUNIT_ASSERT_EQUAL( 0, entry->GetInsertionPoint() );
|
||||
|
||||
entry->AppendText("12"); // should update the insertion point position
|
||||
CPPUNIT_ASSERT_EQUAL( 3, entry->GetLastPosition() );
|
||||
CPPUNIT_ASSERT_EQUAL( 3, entry->GetInsertionPoint() );
|
||||
|
||||
entry->SetInsertionPoint(1);
|
||||
CPPUNIT_ASSERT_EQUAL( 3, entry->GetLastPosition() );
|
||||
CPPUNIT_ASSERT_EQUAL( 1, entry->GetInsertionPoint() );
|
||||
|
||||
entry->SetValue("012"); // shouldn't change the position if no real change
|
||||
CPPUNIT_ASSERT_EQUAL( 1, entry->GetInsertionPoint() );
|
||||
|
||||
entry->ChangeValue("012"); // same as for SetValue()
|
||||
CPPUNIT_ASSERT_EQUAL( 1, entry->GetInsertionPoint() );
|
||||
|
||||
entry->SetInsertionPointEnd();
|
||||
CPPUNIT_ASSERT_EQUAL( 3, entry->GetInsertionPoint() );
|
||||
|
||||
entry->SetInsertionPoint(0);
|
||||
entry->WriteText("-"); // should move it after the written text
|
||||
CPPUNIT_ASSERT_EQUAL( 4, entry->GetLastPosition() );
|
||||
CPPUNIT_ASSERT_EQUAL( 1, entry->GetInsertionPoint() );
|
||||
|
||||
entry->SetValue("something different"); // should still reset the caret
|
||||
CPPUNIT_ASSERT_EQUAL( 0, entry->GetInsertionPoint() );
|
||||
}
|
||||
|
||||
void TextEntryTestCase::Replace()
|
||||
{
|
||||
wxTextEntry * const entry = GetTestEntry();
|
||||
|
||||
entry->SetValue("Hello replace!"
|
||||
"0123456789012");
|
||||
entry->SetInsertionPoint(0);
|
||||
|
||||
entry->Replace(6, 13, "changed");
|
||||
|
||||
CPPUNIT_ASSERT_EQUAL("Hello changed!"
|
||||
"0123456789012",
|
||||
entry->GetValue());
|
||||
CPPUNIT_ASSERT_EQUAL(13, entry->GetInsertionPoint());
|
||||
|
||||
entry->Replace(13, -1, "");
|
||||
CPPUNIT_ASSERT_EQUAL("Hello changed", entry->GetValue());
|
||||
CPPUNIT_ASSERT_EQUAL(13, entry->GetInsertionPoint());
|
||||
|
||||
entry->Replace(0, 6, "Un");
|
||||
CPPUNIT_ASSERT_EQUAL("Unchanged", entry->GetValue());
|
||||
CPPUNIT_ASSERT_EQUAL(2, entry->GetInsertionPoint());
|
||||
}
|
||||
|
||||
void TextEntryTestCase::WriteText()
|
||||
{
|
||||
wxTextEntry * const entry = GetTestEntry();
|
||||
|
||||
entry->SetValue("foo");
|
||||
entry->SetInsertionPoint(3);
|
||||
entry->WriteText("bar");
|
||||
CPPUNIT_ASSERT_EQUAL( "foobar", entry->GetValue() );
|
||||
|
||||
entry->SetValue("foo");
|
||||
entry->SetInsertionPoint(0);
|
||||
entry->WriteText("bar");
|
||||
CPPUNIT_ASSERT_EQUAL( "barfoo", entry->GetValue() );
|
||||
|
||||
entry->SetValue("abxxxhi");
|
||||
entry->SetSelection(2, 5);
|
||||
entry->WriteText("cdefg");
|
||||
CPPUNIT_ASSERT_EQUAL( "abcdefghi", entry->GetValue() );
|
||||
CPPUNIT_ASSERT_EQUAL( 7, entry->GetInsertionPoint() );
|
||||
CPPUNIT_ASSERT_EQUAL( false, entry->HasSelection() );
|
||||
}
|
||||
|
||||
#if wxUSE_UIACTIONSIMULATOR
|
||||
|
||||
class TextEventHandler
|
||||
{
|
||||
public:
|
||||
explicit TextEventHandler(wxWindow* win)
|
||||
: m_win(win)
|
||||
{
|
||||
m_win->Bind(wxEVT_TEXT, &TextEventHandler::OnText, this);
|
||||
}
|
||||
|
||||
~TextEventHandler()
|
||||
{
|
||||
m_win->Unbind(wxEVT_TEXT, &TextEventHandler::OnText, this);
|
||||
}
|
||||
|
||||
const wxString& GetLastString() const
|
||||
{
|
||||
return m_string;
|
||||
}
|
||||
|
||||
private:
|
||||
void OnText(wxCommandEvent& event)
|
||||
{
|
||||
m_string = event.GetString();
|
||||
}
|
||||
|
||||
wxWindow* const m_win;
|
||||
|
||||
wxString m_string;
|
||||
};
|
||||
|
||||
void TextEntryTestCase::Editable()
|
||||
{
|
||||
wxTextEntry * const entry = GetTestEntry();
|
||||
wxWindow * const window = GetTestWindow();
|
||||
|
||||
EventCounter updated(window, wxEVT_TEXT);
|
||||
|
||||
window->SetFocus();
|
||||
wxYield();
|
||||
|
||||
#ifdef __WXGTK__
|
||||
// For some reason, wxBitmapComboBox doesn't appear on the screen without
|
||||
// this (due to wxTLW size hacks perhaps?). It would be nice to avoid doing
|
||||
// this, but without this hack the test often (although not always) fails.
|
||||
wxMilliSleep(50);
|
||||
#endif // __WGTK__
|
||||
|
||||
// Check that we get the expected number of events.
|
||||
wxUIActionSimulator sim;
|
||||
sim.Text("abcdef");
|
||||
wxYield();
|
||||
|
||||
CPPUNIT_ASSERT_EQUAL("abcdef", entry->GetValue());
|
||||
CPPUNIT_ASSERT_EQUAL(6, updated.GetCount());
|
||||
|
||||
wxYield();
|
||||
|
||||
// And that the event carries the right value.
|
||||
TextEventHandler handler(window);
|
||||
|
||||
sim.Text("g");
|
||||
wxYield();
|
||||
|
||||
CPPUNIT_ASSERT_EQUAL("abcdefg", handler.GetLastString());
|
||||
|
||||
// ... even if we generate the event programmatically and whether it uses
|
||||
// the same value as the control has right now
|
||||
entry->SetValue("abcdefg");
|
||||
CPPUNIT_ASSERT_EQUAL("abcdefg", handler.GetLastString());
|
||||
|
||||
// ... or not
|
||||
entry->SetValue("abcdef");
|
||||
CPPUNIT_ASSERT_EQUAL("abcdef", handler.GetLastString());
|
||||
|
||||
// Check that making the control not editable does indeed prevent it from
|
||||
// being edited.
|
||||
updated.Clear();
|
||||
|
||||
entry->SetEditable(false);
|
||||
sim.Text("gh");
|
||||
wxYield();
|
||||
|
||||
CPPUNIT_ASSERT_EQUAL("abcdef", entry->GetValue());
|
||||
CPPUNIT_ASSERT_EQUAL(0, updated.GetCount());
|
||||
}
|
||||
|
||||
#endif // wxUSE_UIACTIONSIMULATOR
|
||||
|
||||
void TextEntryTestCase::Hint()
|
||||
{
|
||||
GetTestEntry()->SetHint("This is a hint");
|
||||
CPPUNIT_ASSERT_EQUAL("", GetTestEntry()->GetValue());
|
||||
}
|
||||
|
||||
void TextEntryTestCase::CopyPaste()
|
||||
{
|
||||
#ifndef __WXOSX__
|
||||
wxTextEntry * const entry = GetTestEntry();
|
||||
|
||||
entry->AppendText("sometext");
|
||||
entry->SelectAll();
|
||||
|
||||
if(entry->CanCopy() && entry->CanPaste())
|
||||
{
|
||||
entry->Copy();
|
||||
entry->Clear();
|
||||
CPPUNIT_ASSERT(entry->IsEmpty());
|
||||
|
||||
wxYield();
|
||||
|
||||
entry->Paste();
|
||||
CPPUNIT_ASSERT_EQUAL("sometext", entry->GetValue());
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void TextEntryTestCase::UndoRedo()
|
||||
{
|
||||
wxTextEntry * const entry = GetTestEntry();
|
||||
|
||||
entry->AppendText("sometext");
|
||||
|
||||
if(entry->CanUndo())
|
||||
{
|
||||
entry->Undo();
|
||||
CPPUNIT_ASSERT(entry->IsEmpty());
|
||||
|
||||
if(entry->CanRedo())
|
||||
{
|
||||
entry->Redo();
|
||||
CPPUNIT_ASSERT_EQUAL("sometext", entry->GetValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if wxUSE_UIACTIONSIMULATOR
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
enum ProcessEnter
|
||||
{
|
||||
ProcessEnter_No,
|
||||
ProcessEnter_ButSkip,
|
||||
ProcessEnter_WithoutSkipping
|
||||
};
|
||||
|
||||
class TestDialog : public wxDialog
|
||||
{
|
||||
public:
|
||||
explicit TestDialog(const TextLikeControlCreator& controlCreator,
|
||||
ProcessEnter processEnter)
|
||||
: wxDialog(wxTheApp->GetTopWindow(), wxID_ANY, "Test dialog"),
|
||||
m_control
|
||||
(
|
||||
controlCreator.Create
|
||||
(
|
||||
this,
|
||||
processEnter == ProcessEnter_No ? 0 : wxTE_PROCESS_ENTER
|
||||
)
|
||||
),
|
||||
m_processEnter(processEnter),
|
||||
m_gotEnter(false)
|
||||
{
|
||||
wxSizer* const sizer = new wxBoxSizer(wxVERTICAL);
|
||||
sizer->Add(m_control, wxSizerFlags().Expand());
|
||||
sizer->Add(CreateStdDialogButtonSizer(wxOK));
|
||||
SetSizerAndFit(sizer);
|
||||
|
||||
CallAfter(&TestDialog::SimulateEnter);
|
||||
|
||||
m_timer.Bind(wxEVT_TIMER, &TestDialog::OnTimeOut, this);
|
||||
m_timer.StartOnce(2000);
|
||||
}
|
||||
|
||||
bool GotEnter() const { return m_gotEnter; }
|
||||
|
||||
private:
|
||||
void OnTextEnter(wxCommandEvent& e)
|
||||
{
|
||||
m_gotEnter = true;
|
||||
|
||||
switch ( m_processEnter )
|
||||
{
|
||||
case ProcessEnter_No:
|
||||
FAIL("Shouldn't be getting wxEVT_TEXT_ENTER at all");
|
||||
break;
|
||||
|
||||
case ProcessEnter_ButSkip:
|
||||
e.Skip();
|
||||
break;
|
||||
|
||||
case ProcessEnter_WithoutSkipping:
|
||||
// Close the dialog with a different exit code than what
|
||||
// pressing the OK button would have generated.
|
||||
EndModal(wxID_APPLY);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void OnText(wxCommandEvent& WXUNUSED(e))
|
||||
{
|
||||
// This should only happen for the multiline text controls.
|
||||
switch ( m_processEnter )
|
||||
{
|
||||
case ProcessEnter_No:
|
||||
case ProcessEnter_ButSkip:
|
||||
// We consider that the text succeeded, but in a different way,
|
||||
// so use a different ID to be able to distinguish between this
|
||||
// scenario and Enter activating the default button.
|
||||
EndModal(wxID_CLOSE);
|
||||
break;
|
||||
|
||||
case ProcessEnter_WithoutSkipping:
|
||||
FAIL("Shouldn't be getting wxEVT_TEXT if handled");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void OnTimeOut(wxTimerEvent&)
|
||||
{
|
||||
EndModal(wxID_CANCEL);
|
||||
}
|
||||
|
||||
void SimulateEnter()
|
||||
{
|
||||
wxUIActionSimulator sim;
|
||||
|
||||
// Calling SetFocus() is somehow not enough to give the focus to this
|
||||
// window when running this test with wxGTK, apparently because the
|
||||
// dialog itself needs to be raised to the front first, so simulate a
|
||||
// click doing this.
|
||||
sim.MouseMove(m_control->GetScreenPosition() + wxPoint(5, 5));
|
||||
wxYield();
|
||||
sim.MouseClick();
|
||||
wxYield();
|
||||
|
||||
// Note that clicking it is still not enough to give it focus with
|
||||
// wxGTK either, so we still need to call SetFocus() nevertheless: but
|
||||
// now it works.
|
||||
m_control->SetFocus();
|
||||
|
||||
sim.Char(WXK_RETURN);
|
||||
}
|
||||
|
||||
wxControl* const m_control;
|
||||
const ProcessEnter m_processEnter;
|
||||
wxTimer m_timer;
|
||||
bool m_gotEnter;
|
||||
|
||||
wxDECLARE_EVENT_TABLE();
|
||||
};
|
||||
|
||||
// Note that we must use event table macros here instead of Bind() because
|
||||
// binding wxEVT_TEXT_ENTER handler for a control without wxTE_PROCESS_ENTER
|
||||
// style would fail with an assertion failure, due to wx helpfully complaining
|
||||
// about it.
|
||||
wxBEGIN_EVENT_TABLE(TestDialog, wxDialog)
|
||||
EVT_TEXT(wxID_ANY, TestDialog::OnText)
|
||||
EVT_TEXT_ENTER(wxID_ANY, TestDialog::OnTextEnter)
|
||||
wxEND_EVENT_TABLE()
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
void TestProcessEnter(const TextLikeControlCreator& controlCreator)
|
||||
{
|
||||
if ( !EnableUITests() )
|
||||
{
|
||||
WARN("Skipping wxTE_PROCESS_ENTER tests: wxUIActionSimulator use disabled");
|
||||
return;
|
||||
}
|
||||
|
||||
SECTION("Without wxTE_PROCESS_ENTER")
|
||||
{
|
||||
TestDialog dlg(controlCreator, ProcessEnter_No);
|
||||
REQUIRE( dlg.ShowModal() == wxID_OK );
|
||||
CHECK( !dlg.GotEnter() );
|
||||
}
|
||||
|
||||
SECTION("With wxTE_PROCESS_ENTER but skipping")
|
||||
{
|
||||
TestDialog dlgProcessEnter(controlCreator, ProcessEnter_ButSkip);
|
||||
REQUIRE( dlgProcessEnter.ShowModal() == wxID_OK );
|
||||
CHECK( dlgProcessEnter.GotEnter() );
|
||||
}
|
||||
|
||||
SECTION("With wxTE_PROCESS_ENTER without skipping")
|
||||
{
|
||||
TestDialog dlgProcessEnter(controlCreator, ProcessEnter_WithoutSkipping);
|
||||
REQUIRE( dlgProcessEnter.ShowModal() == wxID_APPLY );
|
||||
CHECK( dlgProcessEnter.GotEnter() );
|
||||
}
|
||||
|
||||
SECTION("Without wxTE_PROCESS_ENTER but with wxTE_MULTILINE")
|
||||
{
|
||||
std::unique_ptr<TextLikeControlCreator>
|
||||
multiLineCreator(controlCreator.CloneAsMultiLine());
|
||||
if ( !multiLineCreator )
|
||||
return;
|
||||
|
||||
TestDialog dlg(*multiLineCreator, ProcessEnter_No);
|
||||
REQUIRE( dlg.ShowModal() == wxID_CLOSE );
|
||||
CHECK( !dlg.GotEnter() );
|
||||
}
|
||||
|
||||
SECTION("With wxTE_PROCESS_ENTER and wxTE_MULTILINE but skipping")
|
||||
{
|
||||
std::unique_ptr<TextLikeControlCreator>
|
||||
multiLineCreator(controlCreator.CloneAsMultiLine());
|
||||
if ( !multiLineCreator )
|
||||
return;
|
||||
|
||||
TestDialog dlg(*multiLineCreator, ProcessEnter_ButSkip);
|
||||
REQUIRE( dlg.ShowModal() == wxID_CLOSE );
|
||||
CHECK( dlg.GotEnter() );
|
||||
}
|
||||
|
||||
SECTION("With wxTE_PROCESS_ENTER and wxTE_MULTILINE without skipping")
|
||||
{
|
||||
std::unique_ptr<TextLikeControlCreator>
|
||||
multiLineCreator(controlCreator.CloneAsMultiLine());
|
||||
if ( !multiLineCreator )
|
||||
return;
|
||||
|
||||
TestDialog dlg(*multiLineCreator, ProcessEnter_WithoutSkipping);
|
||||
REQUIRE( dlg.ShowModal() == wxID_APPLY );
|
||||
CHECK( dlg.GotEnter() );
|
||||
}
|
||||
}
|
||||
|
||||
#else // !wxUSE_UIACTIONSIMULATOR
|
||||
|
||||
void TestProcessEnter(const TextLikeControlCreator& WXUNUSED(controlCreator))
|
||||
{
|
||||
WARN("Skipping wxTE_PROCESS_ENTER tests: wxUIActionSimulator not available");
|
||||
}
|
||||
|
||||
#endif // wxUSE_UIACTIONSIMULATOR/!wxUSE_UIACTIONSIMULATOR
|
||||
Reference in New Issue
Block a user