mirror of
https://github.com/moonlight-stream/moonlight-qt.git
synced 2025-07-04 00:36:36 +00:00
Add gamepad navigation support for everything except context menus and dialogs
This commit is contained in:
parent
9fb0bffd61
commit
d706e81cd4
@ -108,7 +108,8 @@ SOURCES += \
|
|||||||
streaming/streamutils.cpp \
|
streaming/streamutils.cpp \
|
||||||
backend/autoupdatechecker.cpp \
|
backend/autoupdatechecker.cpp \
|
||||||
path.cpp \
|
path.cpp \
|
||||||
settings/mappingmanager.cpp
|
settings/mappingmanager.cpp \
|
||||||
|
gui/sdlgamepadkeynavigation.cpp
|
||||||
|
|
||||||
HEADERS += \
|
HEADERS += \
|
||||||
utils.h \
|
utils.h \
|
||||||
@ -130,7 +131,8 @@ HEADERS += \
|
|||||||
streaming/streamutils.h \
|
streaming/streamutils.h \
|
||||||
backend/autoupdatechecker.h \
|
backend/autoupdatechecker.h \
|
||||||
path.h \
|
path.h \
|
||||||
settings/mappingmanager.h
|
settings/mappingmanager.h \
|
||||||
|
gui/sdlgamepadkeynavigation.h
|
||||||
|
|
||||||
# Platform-specific renderers and decoders
|
# Platform-specific renderers and decoders
|
||||||
ffmpeg {
|
ffmpeg {
|
||||||
|
@ -3,8 +3,8 @@ import QtQuick.Dialogs 1.2
|
|||||||
import QtQuick.Controls 2.2
|
import QtQuick.Controls 2.2
|
||||||
|
|
||||||
import AppModel 1.0
|
import AppModel 1.0
|
||||||
|
|
||||||
import ComputerManager 1.0
|
import ComputerManager 1.0
|
||||||
|
import SdlGamepadKeyNavigation 1.0
|
||||||
|
|
||||||
GridView {
|
GridView {
|
||||||
property int computerIndex
|
property int computerIndex
|
||||||
@ -36,12 +36,18 @@ GridView {
|
|||||||
currentIndex = -1
|
currentIndex = -1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SdlGamepadKeyNavigation {
|
||||||
|
id: gamepadKeyNav
|
||||||
|
}
|
||||||
|
|
||||||
onVisibleChanged: {
|
onVisibleChanged: {
|
||||||
if (visible) {
|
if (visible) {
|
||||||
appModel.computerLost.connect(computerLost)
|
appModel.computerLost.connect(computerLost)
|
||||||
|
gamepadKeyNav.enable()
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
appModel.computerLost.disconnect(computerLost)
|
appModel.computerLost.disconnect(computerLost)
|
||||||
|
gamepadKeyNav.disable()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -119,20 +125,25 @@ GridView {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
acceptedButtons: Qt.RightButton
|
acceptedButtons: Qt.RightButton
|
||||||
onClicked: {
|
onClicked: {
|
||||||
// Right click
|
// popup() ensures the menu appears under the mouse cursor
|
||||||
appContextMenu.open()
|
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 {
|
Menu {
|
||||||
id: appContextMenu
|
id: appContextMenu
|
||||||
MenuItem {
|
NavigableMenuItem {
|
||||||
text: model.running ? "Resume Game" : "Launch Game"
|
text: model.running ? "Resume Game" : "Launch Game"
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
appContextMenu.close()
|
appContextMenu.close()
|
||||||
launchOrResumeSelectedApp()
|
launchOrResumeSelectedApp()
|
||||||
}
|
}
|
||||||
height: visible ? implicitHeight : 0
|
|
||||||
}
|
}
|
||||||
NavigableMenuItem {
|
NavigableMenuItem {
|
||||||
text: "Quit Game"
|
text: "Quit Game"
|
||||||
@ -142,7 +153,6 @@ GridView {
|
|||||||
quitAppDialog.open()
|
quitAppDialog.open()
|
||||||
}
|
}
|
||||||
visible: model.running
|
visible: model.running
|
||||||
height: visible ? implicitHeight : 0
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,10 @@ import QtQuick 2.0
|
|||||||
import QtQuick.Controls 2.2
|
import QtQuick.Controls 2.2
|
||||||
|
|
||||||
MenuItem {
|
MenuItem {
|
||||||
|
// Ensure focus can't be given to an invisible item
|
||||||
|
enabled: visible
|
||||||
|
height: visible ? implicitHeight : 0
|
||||||
|
|
||||||
Keys.onReturnPressed: {
|
Keys.onReturnPressed: {
|
||||||
triggered()
|
triggered()
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ import ComputerModel 1.0
|
|||||||
|
|
||||||
import ComputerManager 1.0
|
import ComputerManager 1.0
|
||||||
import StreamingPreferences 1.0
|
import StreamingPreferences 1.0
|
||||||
|
import SdlGamepadKeyNavigation 1.0
|
||||||
|
|
||||||
GridView {
|
GridView {
|
||||||
property ComputerModel computerModel : createModel()
|
property ComputerModel computerModel : createModel()
|
||||||
@ -32,6 +33,19 @@ GridView {
|
|||||||
id: prefs
|
id: prefs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SdlGamepadKeyNavigation {
|
||||||
|
id: gamepadKeyNav
|
||||||
|
}
|
||||||
|
|
||||||
|
onVisibleChanged: {
|
||||||
|
if (visible) {
|
||||||
|
gamepadKeyNav.enable()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
gamepadKeyNav.disable()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
// Setup signals on CM
|
// Setup signals on CM
|
||||||
ComputerManager.computerAddCompleted.connect(addComplete)
|
ComputerManager.computerAddCompleted.connect(addComplete)
|
||||||
@ -144,7 +158,6 @@ GridView {
|
|||||||
text: "Wake PC"
|
text: "Wake PC"
|
||||||
onTriggered: computerModel.wakeComputer(index)
|
onTriggered: computerModel.wakeComputer(index)
|
||||||
visible: !model.addPc && !model.online && model.wakeable
|
visible: !model.addPc && !model.online && model.wakeable
|
||||||
height: visible ? implicitHeight : 0
|
|
||||||
}
|
}
|
||||||
NavigableMenuItem {
|
NavigableMenuItem {
|
||||||
text: "Delete PC"
|
text: "Delete PC"
|
||||||
@ -185,6 +198,7 @@ GridView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (!model.online) {
|
} else if (!model.online) {
|
||||||
|
// Using open() here because it may be activated by keyboard
|
||||||
pcContextMenu.open()
|
pcContextMenu.open()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -193,12 +207,20 @@ GridView {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
acceptedButtons: Qt.RightButton;
|
acceptedButtons: Qt.RightButton;
|
||||||
onClicked: {
|
onClicked: {
|
||||||
// right click
|
if (!model.addPc) {
|
||||||
if (!model.addPc) { // but only for actual PCs, not the add-pc option
|
// popup() ensures the menu appears under the mouse cursor
|
||||||
pcContextMenu.open()
|
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 {
|
MessageDialog {
|
||||||
|
@ -3,6 +3,7 @@ import QtQuick.Controls 2.2
|
|||||||
|
|
||||||
import StreamingPreferences 1.0
|
import StreamingPreferences 1.0
|
||||||
import ComputerManager 1.0
|
import ComputerManager 1.0
|
||||||
|
import SdlGamepadKeyNavigation 1.0
|
||||||
|
|
||||||
ScrollView {
|
ScrollView {
|
||||||
id: settingsPage
|
id: settingsPage
|
||||||
@ -12,6 +13,25 @@ ScrollView {
|
|||||||
id: prefs
|
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: {
|
Component.onDestruction: {
|
||||||
prefs.save()
|
prefs.save()
|
||||||
}
|
}
|
||||||
|
@ -43,6 +43,10 @@ ApplicationWindow {
|
|||||||
stackView.pop()
|
stackView.pop()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Keys.onMenuPressed: {
|
||||||
|
settingsButton.clicked()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onVisibilityChanged: {
|
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 "backend/autoupdatechecker.h"
|
||||||
#include "streaming/session.h"
|
#include "streaming/session.h"
|
||||||
#include "settings/streamingpreferences.h"
|
#include "settings/streamingpreferences.h"
|
||||||
|
#include "gui/sdlgamepadkeynavigation.h"
|
||||||
|
|
||||||
#if !defined(QT_DEBUG) && defined(Q_OS_WIN32)
|
#if !defined(QT_DEBUG) && defined(Q_OS_WIN32)
|
||||||
// Log to file for release Windows builds
|
// Log to file for release Windows builds
|
||||||
@ -288,6 +289,7 @@ int main(int argc, char *argv[])
|
|||||||
qmlRegisterType<ComputerModel>("ComputerModel", 1, 0, "ComputerModel");
|
qmlRegisterType<ComputerModel>("ComputerModel", 1, 0, "ComputerModel");
|
||||||
qmlRegisterType<AppModel>("AppModel", 1, 0, "AppModel");
|
qmlRegisterType<AppModel>("AppModel", 1, 0, "AppModel");
|
||||||
qmlRegisterType<StreamingPreferences>("StreamingPreferences", 1, 0, "StreamingPreferences");
|
qmlRegisterType<StreamingPreferences>("StreamingPreferences", 1, 0, "StreamingPreferences");
|
||||||
|
qmlRegisterType<SdlGamepadKeyNavigation>("SdlGamepadKeyNavigation", 1, 0, "SdlGamepadKeyNavigation");
|
||||||
qmlRegisterUncreatableType<Session>("Session", 1, 0, "Session", "Session cannot be created from QML");
|
qmlRegisterUncreatableType<Session>("Session", 1, 0, "Session", "Session cannot be created from QML");
|
||||||
qmlRegisterSingletonType<ComputerManager>("ComputerManager", 1, 0,
|
qmlRegisterSingletonType<ComputerManager>("ComputerManager", 1, 0,
|
||||||
"ComputerManager",
|
"ComputerManager",
|
||||||
@ -338,6 +340,10 @@ int main(int argc, char *argv[])
|
|||||||
SDL_GetError());
|
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.
|
// 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
|
// We don't want this all the time that Moonlight is open. We will set
|
||||||
// it manually when we start streaming.
|
// it manually when we start streaming.
|
||||||
@ -345,7 +351,5 @@ int main(int argc, char *argv[])
|
|||||||
|
|
||||||
int err = app.exec();
|
int err = app.exec();
|
||||||
|
|
||||||
SDL_Quit();
|
|
||||||
|
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user