Qt: Implement controller interface/binding

This commit is contained in:
Connor McLaughlin
2020-02-16 00:14:53 +09:00
parent 6a1206dde7
commit 59cf799491
11 changed files with 720 additions and 3 deletions

View File

@ -43,7 +43,7 @@ add_executable(duckstation-qt
settingsdialog.ui
)
target_link_libraries(duckstation-qt PRIVATE core common imgui glad minizip Qt5::Core Qt5::Gui Qt5::Widgets Qt5::Network)
target_link_libraries(duckstation-qt PRIVATE frontend-common core common imgui glad minizip Qt5::Core Qt5::Gui Qt5::Widgets Qt5::Network)
if(WIN32)
target_sources(duckstation-qt PRIVATE

View File

@ -1,5 +1,6 @@
#include "inputbindingwidgets.h"
#include "core/settings.h"
#include "frontend-common/sdl_controller_interface.h"
#include "qthostinterface.h"
#include "qtutils.h"
#include <QtCore/QTimer>
@ -81,6 +82,49 @@ void InputButtonBindingWidget::onInputListenTimerTimeout()
setText(tr("Push Button... [%1]").arg(m_input_listen_remaining_seconds));
}
void InputButtonBindingWidget::hookControllerInput()
{
m_host_interface->enableBackgroundControllerPolling();
g_sdl_controller_interface.SetHook([this](const SDLControllerInterface::Hook& ei) {
if (ei.type == SDLControllerInterface::Hook::Type::Axis)
{
// TODO: this probably should consider the "last value"
QMetaObject::invokeMethod(this, "bindToControllerAxis", Q_ARG(int, ei.controller_index),
Q_ARG(int, ei.button_or_axis_number), Q_ARG(bool, ei.value > 0));
return SDLControllerInterface::Hook::CallbackResult::StopMonitoring;
}
else if (ei.type == SDLControllerInterface::Hook::Type::Button && ei.value > 0.0f)
{
QMetaObject::invokeMethod(this, "bindToControllerButton", Q_ARG(int, ei.controller_index),
Q_ARG(int, ei.button_or_axis_number));
return SDLControllerInterface::Hook::CallbackResult::StopMonitoring;
}
return SDLControllerInterface::Hook::CallbackResult::ContinueMonitoring;
});
}
void InputButtonBindingWidget::unhookControllerInput()
{
g_sdl_controller_interface.ClearHook();
m_host_interface->disableBackgroundControllerPolling();
}
void InputButtonBindingWidget::bindToControllerAxis(int controller_index, int axis_index, bool positive)
{
m_new_binding_value =
QStringLiteral("Controller%1/%2Axis%3").arg(controller_index).arg(positive ? '+' : '-').arg(axis_index);
setNewBinding();
stopListeningForInput();
}
void InputButtonBindingWidget::bindToControllerButton(int controller_index, int button_index)
{
m_new_binding_value = QStringLiteral("Controller%1/Button%2").arg(controller_index).arg(button_index);
setNewBinding();
stopListeningForInput();
}
void InputButtonBindingWidget::startListeningForInput()
{
m_input_listen_timer = new QTimer(this);
@ -95,6 +139,7 @@ void InputButtonBindingWidget::startListeningForInput()
installEventFilter(this);
grabKeyboard();
grabMouse();
hookControllerInput();
}
void InputButtonBindingWidget::stopListeningForInput()
@ -103,6 +148,7 @@ void InputButtonBindingWidget::stopListeningForInput()
delete m_input_listen_timer;
m_input_listen_timer = nullptr;
unhookControllerInput();
releaseMouse();
releaseKeyboard();
removeEventFilter(this);

View File

@ -20,12 +20,16 @@ protected:
private Q_SLOTS:
void onPressed();
void onInputListenTimerTimeout();
void bindToControllerAxis(int controller_index, int axis_index, bool positive);
void bindToControllerButton(int controller_index, int button_index);
private:
bool isListeningForInput() const { return m_input_listen_timer != nullptr; }
void startListeningForInput();
void stopListeningForInput();
void setNewBinding();
void hookControllerInput();
void unhookControllerInput();
QtHostInterface* m_host_interface;
QString m_setting_name;

View File

@ -8,12 +8,14 @@
#include "core/game_list.h"
#include "core/gpu.h"
#include "core/system.h"
#include "frontend-common/sdl_controller_interface.h"
#include "qtsettingsinterface.h"
#include "qtutils.h"
#include <QtCore/QCoreApplication>
#include <QtCore/QDateTime>
#include <QtCore/QDebug>
#include <QtCore/QEventLoop>
#include <QtCore/QTimer>
#include <QtWidgets/QMenu>
#include <QtWidgets/QMessageBox>
#include <memory>
@ -28,7 +30,6 @@ QtHostInterface::QtHostInterface(QObject* parent)
{
checkSettings();
refreshGameList();
doUpdateInputMap();
createThread();
}
@ -283,6 +284,7 @@ void QtHostInterface::OnSystemCreated()
HostInterface::OnSystemCreated();
wakeThread();
destroyBackgroundControllerPollTimer();
emit emulationStarted();
}
@ -301,6 +303,9 @@ void QtHostInterface::OnSystemDestroyed()
{
HostInterface::OnSystemDestroyed();
if (m_background_controller_polling_enable_count > 0)
createBackgroundControllerPollTimer();
emit emulationStopped();
}
@ -350,7 +355,9 @@ void QtHostInterface::updateInputMap()
void QtHostInterface::doUpdateInputMap()
{
m_keyboard_input_handlers.clear();
g_sdl_controller_interface.ClearControllerBindings();
std::lock_guard<std::mutex> lock(m_qsettings_mutex);
updateControllerInputMap();
updateHotkeyInputMap();
}
@ -493,9 +500,41 @@ void QtHostInterface::addButtonToInputMap(const QString& binding, InputButtonHan
m_keyboard_input_handlers.emplace(key_id.value(), std::move(handler));
}
else if (device.startsWith(QStringLiteral("Controller")))
{
bool controller_index_okay;
const int controller_index = device.mid(10).toInt(&controller_index_okay);
if (!controller_index_okay || controller_index < 0)
{
qWarning() << "Malformed controller binding: " << binding;
return;
}
if (button.startsWith(QStringLiteral("Button")))
{
bool button_index_okay;
const int button_index = button.mid(6).toInt(&button_index_okay);
if (!button_index_okay ||
!g_sdl_controller_interface.BindControllerButton(controller_index, button_index, std::move(handler)))
{
qWarning() << "Failed to bind " << binding;
}
}
else if (button.startsWith(QStringLiteral("+Axis")) || button.startsWith(QStringLiteral("-Axis")))
{
bool axis_index_okay;
const int axis_index = button.mid(5).toInt(&axis_index_okay);
const bool positive = (button[0] == '+');
if (!axis_index_okay || !g_sdl_controller_interface.BindControllerAxisToButton(controller_index, axis_index,
positive, std::move(handler)))
{
qWarning() << "Failed to bind " << binding;
}
}
}
else
{
qWarning() << "Unknown input device: " << device;
qWarning() << "Unknown input device: " << binding;
return;
}
}
@ -659,6 +698,63 @@ void QtHostInterface::saveState(bool global, qint32 slot, bool block_until_done
SaveState(global, slot);
}
void QtHostInterface::enableBackgroundControllerPolling()
{
if (!isOnWorkerThread())
{
QMetaObject::invokeMethod(this, "enableBackgroundControllerPolling", Qt::BlockingQueuedConnection);
return;
}
if (m_background_controller_polling_enable_count++ > 0)
return;
if (!m_system)
{
createBackgroundControllerPollTimer();
// drain the event queue so we don't get events late
g_sdl_controller_interface.PumpSDLEvents();
}
}
void QtHostInterface::disableBackgroundControllerPolling()
{
if (!isOnWorkerThread())
{
QMetaObject::invokeMethod(this, "disableBackgroundControllerPolling");
return;
}
Assert(m_background_controller_polling_enable_count > 0);
if (--m_background_controller_polling_enable_count > 0)
return;
if (!m_system)
destroyBackgroundControllerPollTimer();
}
void QtHostInterface::doBackgroundControllerPoll()
{
g_sdl_controller_interface.PumpSDLEvents();
}
void QtHostInterface::createBackgroundControllerPollTimer()
{
DebugAssert(!m_background_controller_polling_timer);
m_background_controller_polling_timer = new QTimer(this);
m_background_controller_polling_timer->setSingleShot(false);
m_background_controller_polling_timer->setTimerType(Qt::VeryCoarseTimer);
connect(m_background_controller_polling_timer, &QTimer::timeout, this, &QtHostInterface::doBackgroundControllerPoll);
m_background_controller_polling_timer->start(BACKGROUND_CONTROLLER_POLLING_INTERVAL);
}
void QtHostInterface::destroyBackgroundControllerPollTimer()
{
delete m_background_controller_polling_timer;
m_background_controller_polling_timer = nullptr;
}
void QtHostInterface::createThread()
{
m_original_thread = QThread::currentThread();
@ -685,6 +781,12 @@ void QtHostInterface::threadEntryPoint()
{
m_worker_thread_event_loop = new QEventLoop();
// set up controller interface and immediate poll to pick up the controller attached events
g_sdl_controller_interface.Initialize(this, true);
g_sdl_controller_interface.PumpSDLEvents();
doUpdateInputMap();
// TODO: Event which flags the thread as ready
while (!m_shutdown_flag.load())
{
@ -710,10 +812,14 @@ void QtHostInterface::threadEntryPoint()
m_system->Throttle();
m_worker_thread_event_loop->processEvents(QEventLoop::AllEvents);
g_sdl_controller_interface.PumpSDLEvents();
}
m_system.reset();
m_audio_stream.reset();
g_sdl_controller_interface.Shutdown();
delete m_worker_thread_event_loop;
m_worker_thread_event_loop = nullptr;

View File

@ -18,6 +18,7 @@ class ByteStream;
class QEventLoop;
class QMenu;
class QWidget;
class QTimer;
class GameList;
@ -88,11 +89,19 @@ public Q_SLOTS:
void loadState(bool global, qint32 slot);
void saveState(bool global, qint32 slot, bool block_until_done = false);
/// Enables controller polling even without a system active. Must be matched by a call to
/// disableBackgroundControllerPolling.
void enableBackgroundControllerPolling();
/// Disables background controller polling.
void disableBackgroundControllerPolling();
private Q_SLOTS:
void doStopThread();
void doUpdateInputMap();
void doHandleKeyEvent(int key, bool pressed);
void onDisplayWindowResized(int width, int height);
void doBackgroundControllerPoll();
protected:
bool AcquireHostDisplay() override;
@ -107,6 +116,12 @@ protected:
void OnControllerTypeChanged(u32 slot) override;
private:
enum : u32
{
BACKGROUND_CONTROLLER_POLLING_INTERVAL =
100 /// Interval at which the controllers are polled when the system is not active.
};
using InputButtonHandler = std::function<void(bool)>;
class Thread : public QThread
@ -124,6 +139,8 @@ private:
void checkSettings();
void updateQSettingsFromCoreSettings();
void createBackgroundControllerPollTimer();
void destroyBackgroundControllerPollTimer();
void updateControllerInputMap();
void updateHotkeyInputMap();
@ -145,4 +162,7 @@ private:
// input key maps, todo hotkeys
std::map<int, InputButtonHandler> m_keyboard_input_handlers;
QTimer* m_background_controller_polling_timer = nullptr;
u32 m_background_controller_polling_enable_count = 0;
};