mirror of
https://github.com/moonlight-stream/moonlight-qt
synced 2024-11-10 13:44:17 +00:00
Add gamepad navigation support for everything except context menus and dialogs
This commit is contained in:
parent
9fb0bffd61
commit
d706e81cd4
9 changed files with 291 additions and 14 deletions
|
@ -108,7 +108,8 @@ SOURCES += \
|
|||
streaming/streamutils.cpp \
|
||||
backend/autoupdatechecker.cpp \
|
||||
path.cpp \
|
||||
settings/mappingmanager.cpp
|
||||
settings/mappingmanager.cpp \
|
||||
gui/sdlgamepadkeynavigation.cpp
|
||||
|
||||
HEADERS += \
|
||||
utils.h \
|
||||
|
@ -130,7 +131,8 @@ HEADERS += \
|
|||
streaming/streamutils.h \
|
||||
backend/autoupdatechecker.h \
|
||||
path.h \
|
||||
settings/mappingmanager.h
|
||||
settings/mappingmanager.h \
|
||||
gui/sdlgamepadkeynavigation.h
|
||||
|
||||
# Platform-specific renderers and decoders
|
||||
ffmpeg {
|
||||
|
|
|
@ -3,8 +3,8 @@ import QtQuick.Dialogs 1.2
|
|||
import QtQuick.Controls 2.2
|
||||
|
||||
import AppModel 1.0
|
||||
|
||||
import ComputerManager 1.0
|
||||
import SdlGamepadKeyNavigation 1.0
|
||||
|
||||
GridView {
|
||||
property int computerIndex
|
||||
|
@ -36,12 +36,18 @@ GridView {
|
|||
currentIndex = -1
|
||||
}
|
||||
|
||||
SdlGamepadKeyNavigation {
|
||||
id: gamepadKeyNav
|
||||
}
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
appModel.computerLost.connect(computerLost)
|
||||
gamepadKeyNav.enable()
|
||||
}
|
||||
else {
|
||||
appModel.computerLost.disconnect(computerLost)
|
||||
gamepadKeyNav.disable()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -119,20 +125,25 @@ GridView {
|
|||
anchors.fill: parent
|
||||
acceptedButtons: Qt.RightButton
|
||||
onClicked: {
|
||||
// Right click
|
||||
appContextMenu.open()
|
||||
// popup() ensures the menu appears under the mouse cursor
|
||||
appContextMenu.popup()
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onMenuPressed: {
|
||||
// We must use open() here so the menu is positioned on
|
||||
// the ItemDelegate and not where the mouse cursor is
|
||||
appContextMenu.open()
|
||||
}
|
||||
|
||||
Menu {
|
||||
id: appContextMenu
|
||||
MenuItem {
|
||||
NavigableMenuItem {
|
||||
text: model.running ? "Resume Game" : "Launch Game"
|
||||
onTriggered: {
|
||||
appContextMenu.close()
|
||||
launchOrResumeSelectedApp()
|
||||
}
|
||||
height: visible ? implicitHeight : 0
|
||||
}
|
||||
NavigableMenuItem {
|
||||
text: "Quit Game"
|
||||
|
@ -142,7 +153,6 @@ GridView {
|
|||
quitAppDialog.open()
|
||||
}
|
||||
visible: model.running
|
||||
height: visible ? implicitHeight : 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,10 @@ import QtQuick 2.0
|
|||
import QtQuick.Controls 2.2
|
||||
|
||||
MenuItem {
|
||||
// Ensure focus can't be given to an invisible item
|
||||
enabled: visible
|
||||
height: visible ? implicitHeight : 0
|
||||
|
||||
Keys.onReturnPressed: {
|
||||
triggered()
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import ComputerModel 1.0
|
|||
|
||||
import ComputerManager 1.0
|
||||
import StreamingPreferences 1.0
|
||||
import SdlGamepadKeyNavigation 1.0
|
||||
|
||||
GridView {
|
||||
property ComputerModel computerModel : createModel()
|
||||
|
@ -32,6 +33,19 @@ GridView {
|
|||
id: prefs
|
||||
}
|
||||
|
||||
SdlGamepadKeyNavigation {
|
||||
id: gamepadKeyNav
|
||||
}
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
gamepadKeyNav.enable()
|
||||
}
|
||||
else {
|
||||
gamepadKeyNav.disable()
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
// Setup signals on CM
|
||||
ComputerManager.computerAddCompleted.connect(addComplete)
|
||||
|
@ -144,7 +158,6 @@ GridView {
|
|||
text: "Wake PC"
|
||||
onTriggered: computerModel.wakeComputer(index)
|
||||
visible: !model.addPc && !model.online && model.wakeable
|
||||
height: visible ? implicitHeight : 0
|
||||
}
|
||||
NavigableMenuItem {
|
||||
text: "Delete PC"
|
||||
|
@ -185,6 +198,7 @@ GridView {
|
|||
}
|
||||
}
|
||||
} else if (!model.online) {
|
||||
// Using open() here because it may be activated by keyboard
|
||||
pcContextMenu.open()
|
||||
}
|
||||
}
|
||||
|
@ -193,12 +207,20 @@ GridView {
|
|||
anchors.fill: parent
|
||||
acceptedButtons: Qt.RightButton;
|
||||
onClicked: {
|
||||
// right click
|
||||
if (!model.addPc) { // but only for actual PCs, not the add-pc option
|
||||
pcContextMenu.open()
|
||||
if (!model.addPc) {
|
||||
// popup() ensures the menu appears under the mouse cursor
|
||||
pcContextMenu.popup()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onMenuPressed: {
|
||||
if (!model.addPc) {
|
||||
// We must use open() here so the menu is positioned on
|
||||
// the ItemDelegate and not where the mouse cursor is
|
||||
pcContextMenu.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MessageDialog {
|
||||
|
|
|
@ -3,6 +3,7 @@ import QtQuick.Controls 2.2
|
|||
|
||||
import StreamingPreferences 1.0
|
||||
import ComputerManager 1.0
|
||||
import SdlGamepadKeyNavigation 1.0
|
||||
|
||||
ScrollView {
|
||||
id: settingsPage
|
||||
|
@ -12,6 +13,25 @@ ScrollView {
|
|||
id: prefs
|
||||
}
|
||||
|
||||
// The StackView will trigger a visibility change when
|
||||
// we're pushed onto it, causing our onVisibleChanged
|
||||
// routine to run, but only if we start as invisible
|
||||
visible: false
|
||||
|
||||
SdlGamepadKeyNavigation {
|
||||
id: gamepadKeyNav
|
||||
}
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
gamepadKeyNav.setSettingsMode(true)
|
||||
gamepadKeyNav.enable()
|
||||
}
|
||||
else {
|
||||
gamepadKeyNav.disable()
|
||||
}
|
||||
}
|
||||
|
||||
Component.onDestruction: {
|
||||
prefs.save()
|
||||
}
|
||||
|
|
|
@ -43,6 +43,10 @@ ApplicationWindow {
|
|||
stackView.pop()
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onMenuPressed: {
|
||||
settingsButton.clicked()
|
||||
}
|
||||
}
|
||||
|
||||
onVisibilityChanged: {
|
||||
|
|
177
app/gui/sdlgamepadkeynavigation.cpp
Normal file
177
app/gui/sdlgamepadkeynavigation.cpp
Normal file
|
@ -0,0 +1,177 @@
|
|||
#include "sdlgamepadkeynavigation.h"
|
||||
|
||||
#include <QKeyEvent>
|
||||
#include <QGuiApplication>
|
||||
#include <QWindow>
|
||||
|
||||
#include "settings/mappingmanager.h"
|
||||
|
||||
SdlGamepadKeyNavigation::SdlGamepadKeyNavigation()
|
||||
: m_Enabled(false),
|
||||
m_SettingsMode(false)
|
||||
{
|
||||
m_PollingTimer = new QTimer(this);
|
||||
connect(m_PollingTimer, SIGNAL(timeout()), this, SLOT(onPollingTimerFired()));
|
||||
}
|
||||
|
||||
SdlGamepadKeyNavigation::~SdlGamepadKeyNavigation()
|
||||
{
|
||||
disable();
|
||||
}
|
||||
|
||||
void SdlGamepadKeyNavigation::enable()
|
||||
{
|
||||
if (m_Enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We have to initialize and uninitialize this in enable()/disable()
|
||||
// because we need to get out of the way of the Session class. If it
|
||||
// doesn't get to reinitialize the GC subsystem, it won't get initial
|
||||
// arrival events. Additionally, there's a race condition between
|
||||
// our QML objects being destroyed and SDL being deinitialized that
|
||||
// this solves too.
|
||||
if (SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) != 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) failed: %s",
|
||||
SDL_GetError());
|
||||
return;
|
||||
}
|
||||
|
||||
MappingManager mappingManager;
|
||||
mappingManager.applyMappings();
|
||||
|
||||
// Drop all pending gamepad add events. SDL will generate these for us
|
||||
// on first init of the GC subsystem. We can't depend on them due to
|
||||
// overlapping lifetimes of SdlGamepadKeyNavigation instances, so we
|
||||
// will attach ourselves.
|
||||
SDL_FlushEvent(SDL_CONTROLLERDEVICEADDED);
|
||||
|
||||
// Open all currently attached game controllers
|
||||
for (int i = 0; i < SDL_NumJoysticks(); i++) {
|
||||
if (SDL_IsGameController(i)) {
|
||||
SDL_GameController* gc = SDL_GameControllerOpen(i);
|
||||
if (gc != nullptr) {
|
||||
m_Gamepads.append(gc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Poll every 50 ms for a new joystick event
|
||||
m_PollingTimer->start(50);
|
||||
|
||||
m_Enabled = true;
|
||||
}
|
||||
|
||||
void SdlGamepadKeyNavigation::disable()
|
||||
{
|
||||
if (!m_Enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_PollingTimer->stop();
|
||||
|
||||
while (!m_Gamepads.isEmpty()) {
|
||||
SDL_GameControllerClose(m_Gamepads[0]);
|
||||
m_Gamepads.removeAt(0);
|
||||
}
|
||||
|
||||
SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER);
|
||||
|
||||
m_Enabled = false;
|
||||
}
|
||||
|
||||
void SdlGamepadKeyNavigation::onPollingTimerFired()
|
||||
{
|
||||
SDL_Event event;
|
||||
|
||||
while (SDL_PollEvent(&event)) {
|
||||
switch (event.type) {
|
||||
case SDL_CONTROLLERBUTTONDOWN:
|
||||
case SDL_CONTROLLERBUTTONUP:
|
||||
{
|
||||
QEvent::Type type =
|
||||
event.type == SDL_CONTROLLERBUTTONDOWN ?
|
||||
QEvent::Type::KeyPress : QEvent::Type::KeyRelease;
|
||||
|
||||
switch (event.cbutton.button) {
|
||||
case SDL_CONTROLLER_BUTTON_DPAD_UP:
|
||||
if (m_SettingsMode) {
|
||||
// Back-tab
|
||||
sendKey(type, Qt::Key_Tab, Qt::ShiftModifier);
|
||||
}
|
||||
else {
|
||||
sendKey(type, Qt::Key_Up);
|
||||
}
|
||||
break;
|
||||
case SDL_CONTROLLER_BUTTON_DPAD_DOWN:
|
||||
if (m_SettingsMode) {
|
||||
sendKey(type, Qt::Key_Tab);
|
||||
}
|
||||
else {
|
||||
sendKey(type, Qt::Key_Down);
|
||||
}
|
||||
break;
|
||||
case SDL_CONTROLLER_BUTTON_DPAD_LEFT:
|
||||
sendKey(type, Qt::Key_Left);
|
||||
if (m_SettingsMode) {
|
||||
// Some settings controls respond to left/right (like the slider)
|
||||
// and others respond to up/down (like combo boxes). They seem to
|
||||
// be mutually exclusive though so let's just send both.
|
||||
sendKey(type, Qt::Key_Up);
|
||||
}
|
||||
break;
|
||||
case SDL_CONTROLLER_BUTTON_DPAD_RIGHT:
|
||||
sendKey(type, Qt::Key_Right);
|
||||
if (m_SettingsMode) {
|
||||
// Some settings controls respond to left/right (like the slider)
|
||||
// and others respond to up/down (like combo boxes). They seem to
|
||||
// be mutually exclusive though so let's just send both.
|
||||
sendKey(type, Qt::Key_Down);
|
||||
}
|
||||
break;
|
||||
case SDL_CONTROLLER_BUTTON_A:
|
||||
if (m_SettingsMode) {
|
||||
sendKey(type, Qt::Key_Space);
|
||||
}
|
||||
else {
|
||||
sendKey(type, Qt::Key_Return);
|
||||
}
|
||||
break;
|
||||
case SDL_CONTROLLER_BUTTON_B:
|
||||
sendKey(type, Qt::Key_Escape);
|
||||
break;
|
||||
case SDL_CONTROLLER_BUTTON_X:
|
||||
case SDL_CONTROLLER_BUTTON_Y:
|
||||
case SDL_CONTROLLER_BUTTON_START:
|
||||
sendKey(type, Qt::Key_Menu);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SDL_CONTROLLERDEVICEADDED:
|
||||
SDL_GameController* gc = SDL_GameControllerOpen(event.cdevice.which);
|
||||
if (gc != nullptr) {
|
||||
m_Gamepads.append(gc);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SdlGamepadKeyNavigation::sendKey(QEvent::Type type, Qt::Key key, Qt::KeyboardModifiers modifiers)
|
||||
{
|
||||
QGuiApplication* app = static_cast<QGuiApplication*>(QGuiApplication::instance());
|
||||
QWindow* focusWindow = app->focusWindow();
|
||||
if (focusWindow != nullptr) {
|
||||
QKeyEvent keyPressEvent(type, key, modifiers);
|
||||
app->sendEvent(focusWindow, &keyPressEvent);
|
||||
}
|
||||
}
|
||||
|
||||
void SdlGamepadKeyNavigation::setSettingsMode(bool settingsMode)
|
||||
{
|
||||
m_SettingsMode = settingsMode;
|
||||
}
|
34
app/gui/sdlgamepadkeynavigation.h
Normal file
34
app/gui/sdlgamepadkeynavigation.h
Normal file
|
@ -0,0 +1,34 @@
|
|||
#pragma once
|
||||
|
||||
#include <QTimer>
|
||||
#include <QEvent>
|
||||
|
||||
#include <SDL.h>
|
||||
|
||||
class SdlGamepadKeyNavigation : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
SdlGamepadKeyNavigation();
|
||||
|
||||
~SdlGamepadKeyNavigation();
|
||||
|
||||
Q_INVOKABLE void enable();
|
||||
|
||||
Q_INVOKABLE void disable();
|
||||
|
||||
Q_INVOKABLE void setSettingsMode(bool settingsMode);
|
||||
|
||||
private:
|
||||
void sendKey(QEvent::Type type, Qt::Key key, Qt::KeyboardModifiers modifiers = Qt::NoModifier);
|
||||
|
||||
private slots:
|
||||
void onPollingTimerFired();
|
||||
|
||||
private:
|
||||
QTimer* m_PollingTimer;
|
||||
QList<SDL_GameController*> m_Gamepads;
|
||||
bool m_Enabled;
|
||||
bool m_SettingsMode;
|
||||
};
|
|
@ -24,6 +24,7 @@
|
|||
#include "backend/autoupdatechecker.h"
|
||||
#include "streaming/session.h"
|
||||
#include "settings/streamingpreferences.h"
|
||||
#include "gui/sdlgamepadkeynavigation.h"
|
||||
|
||||
#if !defined(QT_DEBUG) && defined(Q_OS_WIN32)
|
||||
// Log to file for release Windows builds
|
||||
|
@ -288,6 +289,7 @@ int main(int argc, char *argv[])
|
|||
qmlRegisterType<ComputerModel>("ComputerModel", 1, 0, "ComputerModel");
|
||||
qmlRegisterType<AppModel>("AppModel", 1, 0, "AppModel");
|
||||
qmlRegisterType<StreamingPreferences>("StreamingPreferences", 1, 0, "StreamingPreferences");
|
||||
qmlRegisterType<SdlGamepadKeyNavigation>("SdlGamepadKeyNavigation", 1, 0, "SdlGamepadKeyNavigation");
|
||||
qmlRegisterUncreatableType<Session>("Session", 1, 0, "Session", "Session cannot be created from QML");
|
||||
qmlRegisterSingletonType<ComputerManager>("ComputerManager", 1, 0,
|
||||
"ComputerManager",
|
||||
|
@ -338,6 +340,10 @@ int main(int argc, char *argv[])
|
|||
SDL_GetError());
|
||||
}
|
||||
|
||||
// Use atexit() to ensure SDL_Quit() is called. This avoids
|
||||
// racing with object destruction where SDL may be used.
|
||||
atexit(SDL_Quit);
|
||||
|
||||
// Avoid the default behavior of changing the timer resolution to 1 ms.
|
||||
// We don't want this all the time that Moonlight is open. We will set
|
||||
// it manually when we start streaming.
|
||||
|
@ -345,7 +351,5 @@ int main(int argc, char *argv[])
|
|||
|
||||
int err = app.exec();
|
||||
|
||||
SDL_Quit();
|
||||
|
||||
return err;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue