mirror of
https://github.com/swift-project/pilotclient.git
synced 2026-04-07 11:05:33 +08:00
refs #453 Refactor CInputManger and low level input handlers
This commit is contained in:
committed by
Mathew Sutcliffe
parent
63c7c6be0d
commit
199a1e5fcb
@@ -28,11 +28,6 @@ namespace BlackCore
|
||||
// this->m_dBusInterface = new CGenericDBusInterface(serviceName, IContextApplication::ObjectPath(), IContextApplication::InterfaceName(), connection, this);
|
||||
this->relaySignals(serviceName, connection);
|
||||
|
||||
// Enable event forwarding from GUI process to core
|
||||
CInputManager *inputManager = CInputManager::getInstance();
|
||||
connect(inputManager, &CInputManager::hotkeyFuncEvent, this, &CContextApplicationProxy::processHotkeyFuncEvent);
|
||||
inputManager->setEventForwarding(true);
|
||||
|
||||
connect(this, &IContextApplication::messageLogged, this, [](const CStatusMessage & message, const CIdentifier & origin)
|
||||
{
|
||||
if (!origin.isFromSameProcess())
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
using namespace BlackMisc;
|
||||
using namespace BlackMisc::Aviation;
|
||||
using namespace BlackMisc::Audio;
|
||||
using namespace BlackMisc::Hardware;
|
||||
using namespace BlackMisc::Input;
|
||||
using namespace BlackSound;
|
||||
|
||||
namespace BlackCore
|
||||
@@ -42,10 +42,6 @@ namespace BlackCore
|
||||
// own aircraft may or may not be available
|
||||
const CCallsign ownCallsign = (this->getIContextOwnAircraft()) ? getIContextOwnAircraft()->getOwnAircraft().getCallsign() : CCallsign();
|
||||
|
||||
// Register PTT hotkey function
|
||||
m_inputManager = CInputManager::getInstance();
|
||||
m_handlePtt = m_inputManager->registerHotkeyFunc(CHotkeyFunction::Ptt(), this, &CContextAudio::ps_setVoiceTransmission);
|
||||
|
||||
m_channel1 = m_voice->createVoiceChannel();
|
||||
m_channel1->setOwnAircraftCallsign(ownCallsign);
|
||||
connect(m_channel1.data(), &IVoiceChannel::connectionStatusChanged, this, &CContextAudio::ps_connectionStatusChanged);
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
#include "voice_channel.h"
|
||||
#include "audio_device.h"
|
||||
#include "audio_mixer.h"
|
||||
#include "input_manager.h"
|
||||
#include "blackinput/keyboard.h"
|
||||
#include "blackmisc/audio/voiceroomlist.h"
|
||||
|
||||
@@ -154,8 +153,6 @@ namespace BlackCore
|
||||
|
||||
QSharedPointer<IVoiceChannel> getVoiceChannelBy(const BlackMisc::Audio::CVoiceRoom &voiceRoom);
|
||||
|
||||
CInputManager *m_inputManager = nullptr;
|
||||
CInputManager::RegistrationHandle m_handlePtt;
|
||||
|
||||
std::unique_ptr<IVoice> m_voice; //!< underlying voice lib
|
||||
std::unique_ptr<IAudioMixer> m_audioMixer;
|
||||
|
||||
@@ -4,107 +4,152 @@
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
#include "input_manager.h"
|
||||
#include "blackmisc/input/keyboardkeylist.h"
|
||||
|
||||
using namespace BlackInput;
|
||||
using namespace BlackMisc;
|
||||
using namespace BlackMisc::Hardware;
|
||||
using namespace BlackMisc::Input;
|
||||
|
||||
namespace BlackCore
|
||||
{
|
||||
CInputManager *CInputManager::m_instance = nullptr;
|
||||
|
||||
CInputManager::CInputManager(QObject *parent) :
|
||||
QObject(parent),
|
||||
m_keyboard(IKeyboard::getInstance()),
|
||||
m_joystick(IJoystick::getInstance())
|
||||
m_keyboard(std::move(IKeyboard::create(this))),
|
||||
m_joystick(std::move(IJoystick::create(this)))
|
||||
{
|
||||
connect(m_keyboard, &IKeyboard::keyUp, this, &CInputManager::ps_processKeyboardKeyUp);
|
||||
connect(m_keyboard, &IKeyboard::keyDown, this, &CInputManager::ps_processKeyboardKeyDown);
|
||||
connect(m_joystick, &IJoystick::buttonUp, this, &CInputManager::ps_processJoystickButtonUp);
|
||||
connect(m_joystick, &IJoystick::buttonDown, this, &CInputManager::ps_processJoystickButtonDown);
|
||||
connect(m_keyboard.get(), &IKeyboard::keyCombinationChanged, this, &CInputManager::ps_processKeyCombinationChanged);
|
||||
connect(m_joystick.get(), &IJoystick::buttonCombinationChanged, this, &CInputManager::ps_processButtonCombinationChanged);
|
||||
}
|
||||
|
||||
CInputManager *CInputManager::getInstance()
|
||||
CInputManager *CInputManager::instance()
|
||||
{
|
||||
if (!m_instance)
|
||||
static CInputManager instance;
|
||||
return &instance;
|
||||
}
|
||||
|
||||
void CInputManager::registerAction(const QString &action)
|
||||
{
|
||||
if (!m_availableActions.contains(action))
|
||||
{
|
||||
m_instance = new CInputManager();
|
||||
m_availableActions.push_back(action);
|
||||
emit hotkeyActionRegistered( { action } );
|
||||
}
|
||||
return m_instance;
|
||||
}
|
||||
|
||||
void CInputManager::changeHotkeySettings(Settings::CSettingKeyboardHotkeyList hotkeys)
|
||||
void CInputManager::registerRemoteActions(const QStringList &actions)
|
||||
{
|
||||
CKeyboardKeyList keyList;
|
||||
for (Settings::CSettingKeyboardHotkey settingHotkey : hotkeys)
|
||||
for (const auto &action : actions)
|
||||
{
|
||||
CKeyboardKey key = settingHotkey.getKey();
|
||||
if (key.isEmpty()) continue;
|
||||
|
||||
m_hashKeyboardKeyFunctions.insert(key, settingHotkey.getFunction());
|
||||
keyList.push_back(key);
|
||||
if (!m_availableActions.contains(action))
|
||||
{
|
||||
m_availableActions.push_back(action);
|
||||
emit hotkeyActionRegistered( { action } );
|
||||
}
|
||||
}
|
||||
m_keyboard->setKeysToMonitor(keyList);
|
||||
}
|
||||
|
||||
void CInputManager::ps_processKeyboardKeyDown(const CKeyboardKey &key)
|
||||
void CInputManager::unbind(int index)
|
||||
{
|
||||
if (!m_hashKeyboardKeyFunctions.contains(key)) return;
|
||||
|
||||
// Get configured hotkey function
|
||||
CHotkeyFunction hotkeyFunc = m_hashKeyboardKeyFunctions.value(key);
|
||||
callFunctionsBy(hotkeyFunc, true);
|
||||
auto info = std::find_if (m_boundActions.begin(), m_boundActions.end(), [index] (const BindInfo &info) { return info.m_index == index; });
|
||||
if (info != m_boundActions.end())
|
||||
{
|
||||
m_boundActions.erase(info);
|
||||
}
|
||||
}
|
||||
|
||||
void CInputManager::ps_processKeyboardKeyUp(const CKeyboardKey &key)
|
||||
void CInputManager::ps_changeHotkeySettings()
|
||||
{
|
||||
if (!m_hashKeyboardKeyFunctions.contains(key)) return;
|
||||
m_configuredActions.clear();
|
||||
for (CActionHotkey actionHotkey : m_actionHotkeys.get())
|
||||
{
|
||||
CHotkeyCombination combination = actionHotkey.getCombination();
|
||||
if (combination.isEmpty()) continue;
|
||||
|
||||
// Get configured hotkey function
|
||||
CHotkeyFunction hotkeyFunc = m_hashKeyboardKeyFunctions.value(key);
|
||||
callFunctionsBy(hotkeyFunc, false);
|
||||
m_configuredActions.insert(combination, actionHotkey.getAction());
|
||||
}
|
||||
}
|
||||
|
||||
void CInputManager::ps_processJoystickButtonDown(const CJoystickButton &button)
|
||||
void CInputManager::ps_processKeyCombinationChanged(const CHotkeyCombination &combination)
|
||||
{
|
||||
qDebug() << "Pressed Button" << button.getButtonIndex();
|
||||
if (!m_hashJoystickKeyFunctions.contains(button)) return;
|
||||
|
||||
// Get configured hotkey function
|
||||
CHotkeyFunction hotkeyFunc = m_hashJoystickKeyFunctions.value(button);
|
||||
callFunctionsBy(hotkeyFunc, true);
|
||||
// Merge in the joystick part
|
||||
CHotkeyCombination copy(combination);
|
||||
copy.setJoystickButtons(m_lastCombination.getJoystickButtons());
|
||||
processCombination(copy);
|
||||
}
|
||||
|
||||
void CInputManager::ps_processJoystickButtonUp(const CJoystickButton &button)
|
||||
void CInputManager::ps_processButtonCombinationChanged(const CHotkeyCombination &combination)
|
||||
{
|
||||
qDebug() << "Released Button" << button.getButtonIndex();
|
||||
if (!m_hashJoystickKeyFunctions.contains(button)) return;
|
||||
|
||||
// Get configured hotkey function
|
||||
CHotkeyFunction hotkeyFunc = m_hashJoystickKeyFunctions.value(button);
|
||||
callFunctionsBy(hotkeyFunc, false);
|
||||
// Merge in the keyboard keys
|
||||
CHotkeyCombination copy(combination);
|
||||
copy.setKeyboardKeys(m_lastCombination.getKeyboardKeys());
|
||||
processCombination(copy);
|
||||
}
|
||||
|
||||
void CInputManager::callFunctionsBy(const CHotkeyFunction &hotkeyFunction, bool isKeyDown)
|
||||
void CInputManager::startCapture()
|
||||
{
|
||||
BlackMisc::Event::CEventHotkeyFunction hotkeyEvent(hotkeyFunction, isKeyDown);
|
||||
if(m_eventForwardingEnabled) emit hotkeyFuncEvent(hotkeyEvent);
|
||||
|
||||
if (!m_hashRegisteredFunctions.contains(hotkeyFunction)) return;
|
||||
auto func = m_hashRegisteredFunctions.value(hotkeyFunction);
|
||||
func(isKeyDown);
|
||||
m_captureActive = true;
|
||||
m_capturedCombination = {};
|
||||
}
|
||||
|
||||
CInputManager::RegistrationHandle CInputManager::registerHotkeyFuncImpl(const BlackMisc::CHotkeyFunction &hotkeyFunction,
|
||||
QObject *receiver,
|
||||
std::function<void (bool)> function)
|
||||
void CInputManager::callFunctionsBy(const QString &action, bool isKeyDown)
|
||||
{
|
||||
m_hashRegisteredFunctions.insert(hotkeyFunction, function);
|
||||
if (action.isEmpty()) { return; }
|
||||
if(m_actionRelayingEnabled) emit remoteActionFromLocal(action, isKeyDown);
|
||||
|
||||
RegistrationHandle handle;
|
||||
handle.function = function;
|
||||
handle.hotkeyFunction = hotkeyFunction;
|
||||
handle.m_receiver = receiver;
|
||||
return handle;
|
||||
for (const auto &boundAction : m_boundActions)
|
||||
{
|
||||
if (boundAction.m_action == action)
|
||||
{
|
||||
boundAction.m_function(isKeyDown);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CInputManager::triggerKey(const CHotkeyCombination &combination, bool isPressed)
|
||||
{
|
||||
Q_UNUSED(isPressed)
|
||||
QString previousAction = m_configuredActions.value(m_lastCombination);
|
||||
QString action = m_configuredActions.value(combination);
|
||||
callFunctionsBy(previousAction, false);
|
||||
callFunctionsBy(action, true);
|
||||
m_lastCombination = combination;
|
||||
}
|
||||
|
||||
int CInputManager::bindImpl(const QString &action, QObject *receiver, std::function<void (bool)> function)
|
||||
{
|
||||
static int index = 0;
|
||||
Q_ASSERT(index < INT_MAX);
|
||||
BindInfo info;
|
||||
info.m_index = index;
|
||||
++index;
|
||||
info.m_function = function;
|
||||
info.m_action = action;
|
||||
info.m_receiver = receiver;
|
||||
m_boundActions.push_back(info);
|
||||
return info.m_index;
|
||||
}
|
||||
|
||||
void CInputManager::processCombination(const CHotkeyCombination &combination)
|
||||
{
|
||||
if (m_captureActive)
|
||||
{
|
||||
if (combination.size() < m_capturedCombination.size())
|
||||
{
|
||||
emit combinationSelectionFinished(m_capturedCombination);
|
||||
m_captureActive = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
emit combinationSelectionChanged(combination);
|
||||
m_capturedCombination = combination;
|
||||
}
|
||||
}
|
||||
|
||||
QString previousAction = m_configuredActions.value(m_lastCombination);
|
||||
QString action = m_configuredActions.value(combination);
|
||||
m_lastCombination = combination;
|
||||
callFunctionsBy(previousAction, false);
|
||||
callFunctionsBy(action, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,15 +7,13 @@
|
||||
#define BLACKCORE_INPUTMANAGER_H
|
||||
|
||||
#include "blackcoreexport.h"
|
||||
#include "blackcore/settings/application.h"
|
||||
#include "blackinput/keyboard.h"
|
||||
#include "blackinput/joystick.h"
|
||||
#include "blackmisc/hardware/keyboardkeylist.h"
|
||||
#include "blackmisc/hardware/joystickbutton.h"
|
||||
#include "blackmisc/hotkeyfunction.h"
|
||||
#include "blackmisc/setkeyboardhotkeylist.h"
|
||||
#include "blackmisc/eveventhotkeyfunction.h"
|
||||
#include "blackmisc/input/hotkeycombination.h"
|
||||
#include <QObject>
|
||||
#include <QHash>
|
||||
#include <QVector>
|
||||
#include <type_traits>
|
||||
#include <functional>
|
||||
|
||||
@@ -29,62 +27,67 @@ namespace BlackCore
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
//! Register new action
|
||||
void registerAction(const QString &action);
|
||||
|
||||
//! \brief Handle to a registered hotkey function
|
||||
struct RegistrationHandle
|
||||
{
|
||||
//! \brief Constructor
|
||||
RegistrationHandle() {}
|
||||
|
||||
BlackMisc::CHotkeyFunction hotkeyFunction; //!< Registered hotkey function
|
||||
QPointer<QObject> m_receiver; //!< Registered receiver
|
||||
std::function<void(bool)> function; //!< Registered function
|
||||
};
|
||||
//! Register remote actions
|
||||
void registerRemoteActions(const QStringList &actions);
|
||||
|
||||
//! Register a new hotkey function
|
||||
RegistrationHandle registerHotkeyFunc(const BlackMisc::CHotkeyFunction &hotkeyFunction, QObject *receiver, const QByteArray &slotName)
|
||||
{
|
||||
auto function = [=](bool isKeyDown){ QMetaObject::invokeMethod(receiver, slotName, Q_ARG(bool, isKeyDown)); };
|
||||
return registerHotkeyFuncImpl(hotkeyFunction, receiver, function);
|
||||
}
|
||||
|
||||
//! Register a new hotkey function
|
||||
template <class RecvObj>
|
||||
RegistrationHandle registerHotkeyFunc(const BlackMisc::CHotkeyFunction &hotkeyFunction, RecvObj *receiver, void (RecvObj:: *slotPointer)(bool))
|
||||
template <typename RecvObj>
|
||||
int bind(const QString &action, RecvObj *receiver, void (RecvObj:: *slotPointer)(bool))
|
||||
{
|
||||
using namespace std::placeholders;
|
||||
auto function = std::bind(slotPointer, receiver, _1);
|
||||
return registerHotkeyFuncImpl(hotkeyFunction, receiver, function);
|
||||
return bindImpl(action, receiver, function);
|
||||
}
|
||||
|
||||
//! Register a new hotkey function
|
||||
template <class Func>
|
||||
RegistrationHandle registerHotkeyFunc(const BlackMisc::CHotkeyFunction &hotkeyFunction, QObject *receiver, Func functionObject)
|
||||
template <typename Func>
|
||||
int bind(const QString &action, QObject *receiver, Func functionObject)
|
||||
{
|
||||
return registerHotkeyFuncImpl(hotkeyFunction, receiver, functionObject);
|
||||
return bindImpl(action, receiver, functionObject);
|
||||
}
|
||||
|
||||
//! Unbind a slot
|
||||
void unbind(int index);
|
||||
|
||||
//!
|
||||
//! Select a key combination as hotkey. This method returns immediatly.
|
||||
//! Listen for signals combinationSelectionChanged and combinationSelectionFinished
|
||||
//! to retrieve the user input.
|
||||
void startCapture();
|
||||
|
||||
//! Deletes all registered hotkeys. Be careful with this method!
|
||||
void resetAllHotkeyFuncs() { m_hashRegisteredFunctions.clear(); }
|
||||
void resetAllActions() { m_configuredActions.clear(); }
|
||||
|
||||
//! Get all available and known actions
|
||||
QStringList allAvailableActions() const { return m_availableActions; }
|
||||
|
||||
//! Enable event forwarding to core
|
||||
void setEventForwarding(bool enabled) { m_eventForwardingEnabled = enabled; }
|
||||
|
||||
//! Creates a native keyboard handler object
|
||||
static CInputManager *getInstance();
|
||||
|
||||
public slots:
|
||||
|
||||
//! Change hotkey settings
|
||||
void changeHotkeySettings(BlackMisc::Settings::CSettingKeyboardHotkeyList hotkeys);
|
||||
void setForwarding(bool enabled) { m_actionRelayingEnabled = enabled; }
|
||||
|
||||
//! Call functions by hotkeyfunction
|
||||
void callFunctionsBy(const BlackMisc::CHotkeyFunction &hotkeyFunction, bool isKeyDown);
|
||||
void callFunctionsBy(const QString &action, bool isKeyDown);
|
||||
|
||||
//! Triggers a key event manually and calls the registered functions.
|
||||
void triggerKey(const BlackMisc::Input::CHotkeyCombination &combination, bool isPressed);
|
||||
|
||||
//! Creates a native keyboard handler object
|
||||
static CInputManager *instance();
|
||||
|
||||
signals:
|
||||
|
||||
//! Event hotkeyfunction occured
|
||||
void hotkeyFuncEvent(const BlackMisc::Event::CEventHotkeyFunction &event);
|
||||
void remoteActionFromLocal(const QString &action, bool argument);
|
||||
|
||||
//! Selected combination has changed
|
||||
void combinationSelectionChanged(const BlackMisc::Input::CHotkeyCombination &combination);
|
||||
|
||||
//! Combination selection has finished
|
||||
void combinationSelectionFinished(const BlackMisc::Input::CHotkeyCombination &combination);
|
||||
|
||||
//! New hotkey action is registered
|
||||
void hotkeyActionRegistered(const QStringList &actions);
|
||||
|
||||
protected:
|
||||
//! Constructor
|
||||
@@ -92,28 +95,43 @@ namespace BlackCore
|
||||
|
||||
private slots:
|
||||
|
||||
void ps_processKeyboardKeyDown(const BlackMisc::Hardware::CKeyboardKey &);
|
||||
void ps_processKeyCombinationChanged(const BlackMisc::Input::CHotkeyCombination &combination);
|
||||
|
||||
void ps_processKeyboardKeyUp(const BlackMisc::Hardware::CKeyboardKey &);
|
||||
void ps_processButtonCombinationChanged(const BlackMisc::Input::CHotkeyCombination &combination);
|
||||
|
||||
void ps_processJoystickButtonDown(const BlackMisc::Hardware::CJoystickButton &button);
|
||||
|
||||
void ps_processJoystickButtonUp(const BlackMisc::Hardware::CJoystickButton &button);
|
||||
//! Change hotkey settings
|
||||
void ps_changeHotkeySettings();
|
||||
|
||||
private:
|
||||
//! Handle to a bound action
|
||||
struct BindInfo
|
||||
{
|
||||
// Using unique int intex for identification because std::function does not have a operator==
|
||||
int m_index = 0;
|
||||
QString m_action;
|
||||
QPointer<QObject> m_receiver;
|
||||
std::function<void(bool)> m_function;
|
||||
};
|
||||
|
||||
RegistrationHandle registerHotkeyFuncImpl(const BlackMisc::CHotkeyFunction &hotkeyFunction, QObject *receiver, std::function<void(bool)> function);
|
||||
int bindImpl(const QString &action, QObject *receiver, std::function<void(bool)> function);
|
||||
|
||||
void processCombination(const BlackMisc::Input::CHotkeyCombination &combination);
|
||||
|
||||
static CInputManager *m_instance;
|
||||
|
||||
BlackInput::IKeyboard *m_keyboard = nullptr;
|
||||
BlackInput::IJoystick *m_joystick = nullptr;
|
||||
std::unique_ptr<BlackInput::IKeyboard> m_keyboard;
|
||||
std::unique_ptr<BlackInput::IJoystick> m_joystick;
|
||||
|
||||
QHash<BlackMisc::CHotkeyFunction, std::function<void(bool)> > m_hashRegisteredFunctions;
|
||||
QHash<BlackMisc::Hardware::CKeyboardKey, BlackMisc::CHotkeyFunction> m_hashKeyboardKeyFunctions;
|
||||
QHash<BlackMisc::Hardware::CJoystickButton, BlackMisc::CHotkeyFunction> m_hashJoystickKeyFunctions;
|
||||
QStringList m_availableActions;
|
||||
QHash<BlackMisc::Input::CHotkeyCombination, QString> m_configuredActions;
|
||||
QVector<BindInfo> m_boundActions;
|
||||
|
||||
bool m_eventForwardingEnabled = false;
|
||||
bool m_actionRelayingEnabled = false;
|
||||
bool m_captureActive = false;
|
||||
BlackMisc::Input::CHotkeyCombination m_lastCombination;
|
||||
BlackMisc::Input::CHotkeyCombination m_capturedCombination;
|
||||
|
||||
BlackCore::CSetting<BlackCore::Settings::Application::ActionHotkeys> m_actionHotkeys { this, &CInputManager::ps_changeHotkeySettings };
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
46
src/blackcore/settings/application.h
Normal file
46
src/blackcore/settings/application.h
Normal file
@@ -0,0 +1,46 @@
|
||||
/* Copyright (C) 2015
|
||||
* swift project Community / Contributors
|
||||
*
|
||||
* This file is part of swift project. It is subject to the license terms in the LICENSE file found in the top-level
|
||||
* directory of this distribution and at http://www.swift-project.org/license.html. No part of swift project,
|
||||
* including this file, may be copied, modified, propagated, or distributed except according to the terms
|
||||
* contained in the LICENSE file.
|
||||
*/
|
||||
|
||||
//! \file
|
||||
|
||||
#ifndef BLACKCORE_SETTINGS_APPLICATION_H
|
||||
#define BLACKCORE_SETTINGS_APPLICATION_H
|
||||
|
||||
#include "blackcore/settingscache.h"
|
||||
#include "blackmisc/input/actionhotkeylist.h"
|
||||
|
||||
namespace BlackCore
|
||||
{
|
||||
namespace Settings
|
||||
{
|
||||
namespace Application
|
||||
{
|
||||
//! User configured hotkeys
|
||||
struct ActionHotkeys : public CSettingTrait<BlackMisc::Input::CActionHotkeyList>
|
||||
{
|
||||
//! \copydoc BlackCore::CSetting::key
|
||||
static const char *key() { return "application/actionhotkeys"; }
|
||||
|
||||
//! \copydoc BlackCore::CSetting::isValid
|
||||
static bool isValid(const BlackMisc::Input::CActionHotkeyList &value)
|
||||
{
|
||||
for (const auto &actionHotkey : value)
|
||||
{
|
||||
if (actionHotkey.getApplicableMachine().getMachineName().isEmpty() ||
|
||||
actionHotkey.getAction().isEmpty() ||
|
||||
actionHotkey.getCombination().isEmpty()) { return false; }
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user