Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions pcsx2-gsrunner/Main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,10 @@ void Host::RequestExitBigPicture()
{
}

void Host::RequestEnterBigPicture()
{
}

void Host::RequestVMShutdown(bool allow_confirm, bool allow_save_state, bool default_save_state)
{
VMManager::SetState(VMState::Stopping);
Expand Down
53 changes: 53 additions & 0 deletions pcsx2-qt/GameList/GameListWidget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
#include <QtCore/QSortFilterProxyModel>
#include <QtCore/QDir>
#include <QtCore/QString>
#include <QtGui/QKeyEvent>
#include <QtGui/QPainter>
#include <QtGui/QPixmap>
#include <QtGui/QPixmapCache>
Expand Down Expand Up @@ -657,6 +658,58 @@ void GameListWidget::refreshGridCovers()
m_model->refreshCovers();
}

void GameListWidget::handleControllerNavigation(int qtKey)
{
// If a modal dialog is active (e.g. the resume-state prompt), redirect navigation there
// instead of the game list. Map directional inputs to Tab/Shift+Tab since that is how
// focus moves between dialog buttons.
if (QApplication::activeModalWidget())
{
QWidget* target = QApplication::focusWidget();
if (!target)
target = QApplication::activeModalWidget();

int nav_key = qtKey;
Qt::KeyboardModifiers mods = Qt::NoModifier;
if (qtKey == Qt::Key_Up || qtKey == Qt::Key_Left)
{
nav_key = Qt::Key_Tab;
mods = Qt::ShiftModifier;
}
else if (qtKey == Qt::Key_Down || qtKey == Qt::Key_Right)
{
nav_key = Qt::Key_Tab;
}

QApplication::postEvent(target, new QKeyEvent(QEvent::KeyPress, nav_key, mods));
QApplication::postEvent(target, new QKeyEvent(QEvent::KeyRelease, nav_key, mods));
return;
}

// Only route to visible game views; ignore if showing the empty-game-directory widget.
if (!isShowingGameList() && !isShowingGameGrid())
return;

QAbstractItemView* view = isShowingGameGrid() ? static_cast<QAbstractItemView*>(m_list_view) : static_cast<QAbstractItemView*>(m_table_view);

// Nothing is visually highlighted yet (currentIndex may exist but not be selected).
// Snap to item #1 and consume this input so the user sees it highlighted first.
if (!view->selectionModel()->hasSelection())
{
const QModelIndex first = view->model()->index(0, 0);
if (!first.isValid())
return;
view->setFocus();
view->setCurrentIndex(first);
view->selectionModel()->select(first, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows);
view->scrollToTop();
return;
}

QApplication::postEvent(view, new QKeyEvent(QEvent::KeyPress, qtKey, Qt::NoModifier));
QApplication::postEvent(view, new QKeyEvent(QEvent::KeyRelease, qtKey, Qt::NoModifier));
}

void GameListWidget::showGameList()
{
if (m_ui.stack->currentIndex() == 0 || m_model->rowCount() == 0)
Expand Down
1 change: 1 addition & 0 deletions pcsx2-qt/GameList/GameListWidget.h
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ public Q_SLOTS:
void gridZoomOut();
void gridIntScale(int int_scale);
void refreshGridCovers();
void handleControllerNavigation(int qtKey);

protected:
void showEvent(QShowEvent* event) override;
Expand Down
1 change: 1 addition & 0 deletions pcsx2-qt/MainWindow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,7 @@ void MainWindow::connectVMThreadSignals(EmuThread* thread)
connect(thread, &EmuThread::onAchievementsHardcoreModeChanged, this, &MainWindow::onAchievementsHardcoreModeChanged);
connect(thread, &EmuThread::onCoverDownloaderOpenRequested, this, &MainWindow::onToolsCoverDownloaderTriggered);
connect(thread, &EmuThread::onCreateMemoryCardOpenRequested, this, &MainWindow::onCreateMemoryCardOpenRequested);
connect(thread, &EmuThread::navigationKeyPressed, m_game_list_widget, &GameListWidget::handleControllerNavigation);

connect(m_ui.actionReset, &QAction::triggered, this, &MainWindow::requestReset);
connect(m_ui.actionPause, &QAction::toggled, thread, &EmuThread::setVMPaused);
Expand Down
71 changes: 69 additions & 2 deletions pcsx2-qt/QtHost.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,67 @@ void EmuThread::run()
createBackgroundControllerPollTimer();
startBackgroundControllerPollTimer();

// Install controller navigation callback so D-pad/analog stick can navigate the game list.
InputManager::SetUINavigationCallback([](GenericInputBinding key, float value) -> bool {
if (VMManager::HasValidVM() || FullscreenUI::IsInitialized())
return false;

static constexpr float NAV_THRESHOLD = 0.5f;
static u8 s_nav_state = 0;

u8 bit = 0;
int qt_key = -1;

switch (key)
{
case GenericInputBinding::DPadUp:
case GenericInputBinding::LeftStickUp:
bit = (1 << 0);
qt_key = Qt::Key_Up;
break;
case GenericInputBinding::DPadDown:
case GenericInputBinding::LeftStickDown:
bit = (1 << 1);
qt_key = Qt::Key_Down;
break;
case GenericInputBinding::DPadLeft:
case GenericInputBinding::LeftStickLeft:
bit = (1 << 2);
qt_key = Qt::Key_Left;
break;
case GenericInputBinding::DPadRight:
case GenericInputBinding::LeftStickRight:
bit = (1 << 3);
qt_key = Qt::Key_Right;
break;
case GenericInputBinding::Cross:
case GenericInputBinding::Start:
bit = (1 << 4);
qt_key = Qt::Key_Return;
break;
default:
return false;
}

const bool pressed = (value >= NAV_THRESHOLD);
const bool was_pressed = (s_nav_state & bit) != 0;

if (pressed && !was_pressed)
{
s_nav_state |= bit;
emit g_emu_thread->navigationKeyPressed(qt_key);
}
else if (!pressed && was_pressed)
{
s_nav_state &= ~bit;
}

// Consume the event so it doesn't reach InvokeEvents/pad bindings.
// If it were forwarded, the pad plugin would buffer the button state
// and the game would see a spurious press on launch.
return true;
});

// Main CPU thread loop.
while (!m_shutdown_flag.load())
{
Expand Down Expand Up @@ -418,6 +479,7 @@ void EmuThread::run()
}

// Teardown in reverse order.
InputManager::RemoveUINavigationCallback();
stopBackgroundControllerPollTimer();
destroyBackgroundControllerPollTimer();
VMManager::Internal::CPUThreadShutdown();
Expand Down Expand Up @@ -1237,6 +1299,11 @@ void Host::RequestExitBigPicture()
g_emu_thread->stopFullscreenUI();
}

void Host::RequestEnterBigPicture()
{
g_emu_thread->startFullscreenUI(false);
}

void Host::RequestVMShutdown(bool allow_confirm, bool allow_save_state, bool default_save_state)
{
if (!VMManager::HasValidVM())
Expand Down Expand Up @@ -2297,8 +2364,8 @@ bool QtHost::ParseCommandLineOptions(const QStringList& args, std::shared_ptr<VM
Console.Warning("Skipping autoboot due to no boot parameters.");
autoboot.reset();
}
if(autoboot && autoboot->start_turbo.value_or(false) && autoboot->start_unlimited.value_or(false))

if (autoboot && autoboot->start_turbo.value_or(false) && autoboot->start_unlimited.value_or(false))
{
Console.Warning("Both turbo and unlimited frame limit modes requested. Using unlimited.");
autoboot->start_turbo.reset();
Expand Down
3 changes: 3 additions & 0 deletions pcsx2-qt/QtHost.h
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,9 @@ public Q_SLOTS:
void onCoverDownloaderOpenRequested();
void onCreateMemoryCardOpenRequested();

/// Emitted when a controller navigation event is received while no VM is running.
void navigationKeyPressed(int qtKey);

/// Called when video capture starts/stops.
void onCaptureStarted(const QString& filename);
void onCaptureStopped();
Expand Down
3 changes: 3 additions & 0 deletions pcsx2/ImGui/FullscreenUI.h
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ namespace Host
/// Requests Big Picture mode to be shut down, returning to the desktop interface.
void RequestExitBigPicture();

/// Requests Big Picture mode to be started from the desktop interface.
void RequestEnterBigPicture();

void OnCoverDownloaderOpenRequested();
void OnCreateMemoryCardOpenRequested();

Expand Down
14 changes: 14 additions & 0 deletions pcsx2/ImGui/ImGuiManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1157,6 +1157,20 @@ bool ImGuiManager::ProcessGenericInputEvent(GenericInputBinding key, InputLayout
ImGuiKey_GamepadL2, // R2
};

// Guide/Xbox button toggles Big Picture Mode, but only when no game is running.
// This is checked before the ImGui context guard so it works from the Qt desktop view too.
if (key == GenericInputBinding::System)
{
if (value > 0.0f && !VMManager::HasValidVM())
{
if (FullscreenUI::IsInitialized())
Host::RequestExitBigPicture();
else
Host::RequestEnterBigPicture();
}
return true;
}

if (!ImGui::GetCurrentContext())
return false;

Expand Down
26 changes: 24 additions & 2 deletions pcsx2/Input/InputManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,9 @@ static std::mutex s_binding_map_write_lock;
static std::mutex m_event_intercept_mutex;
static InputInterceptHook::Callback m_event_intercept_callback;

// UI navigation callback (game list controller support)
static InputManager::UINavigationCallback s_ui_nav_callback;

// Input sources. Keyboard/mouse don't exist here.
static std::array<std::unique_ptr<InputSource>, static_cast<u32>(InputSourceType::Count)> s_input_sources;

Expand Down Expand Up @@ -624,7 +627,7 @@ void InputManager::AddBindings(const std::vector<std::string>& bindings, const I
// INISettingsInterface, can just update directly
si.SetStringList(section, key, new_bindings);
si.Save();
}
}
else
{
// LayeredSettingsInterface, Need to find which layer our binding came from
Expand Down Expand Up @@ -1266,6 +1269,9 @@ bool InputManager::PreprocessEvent(InputBindingKey key, float value, GenericInpu
InputLayout layout = s_input_sources[static_cast<u32>(InputSourceType::SDL)]->GetControllerLayout(key.source_index);
if (ImGuiManager::ProcessGenericInputEvent(generic_key, layout, value) && value != 0.0f)
return true;

if (s_ui_nav_callback && s_ui_nav_callback(generic_key, value))
return true;
}

return false;
Expand Down Expand Up @@ -1516,6 +1522,22 @@ bool InputManager::HasHook()
return (bool)m_event_intercept_callback;
}

void InputManager::SetUINavigationCallback(UINavigationCallback callback)
{
s_ui_nav_callback = std::move(callback);
}

void InputManager::RemoveUINavigationCallback()
{
s_ui_nav_callback = {};
}

void InputManager::InvokeUINavigationEvent(GenericInputBinding key, float value)
{
if (s_ui_nav_callback)
s_ui_nav_callback(key, value);
}

bool InputManager::DoEventHook(InputBindingKey key, float value)
{
std::unique_lock<std::mutex> lock(m_event_intercept_mutex);
Expand Down Expand Up @@ -1558,7 +1580,7 @@ void InputManager::ReloadBindings(SettingsInterface& si, SettingsInterface& bind
for (u32 axis = 0; axis <= static_cast<u32>(InputPointerAxis::Y); axis++)
{
s_pointer_axis_speed[axis] = si.GetFloatValue("Pad", fmt::format("Pointer{}Speed", s_pointer_axis_setting_names[axis]).c_str(), 40.0f) /
ui_ctrl_range * pointer_sensitivity;
ui_ctrl_range * pointer_sensitivity;
s_pointer_axis_dead_zone[axis] = std::min(
si.GetFloatValue("Pad", fmt::format("Pointer{}DeadZone", s_pointer_axis_setting_names[axis]).c_str(), 20.0f) / ui_ctrl_range, 1.0f);
s_pointer_axis_range[axis] = 1.0f - s_pointer_axis_dead_zone[axis];
Expand Down
11 changes: 11 additions & 0 deletions pcsx2/Input/InputManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,17 @@ namespace InputManager
/// Returns true if there is an interception hook present.
bool HasHook();

/// Sets a callback to receive generic input events not consumed by ImGui or pad bindings.
/// Intended for UI navigation (e.g. controller-driven game list navigation) when no VM is running.
/// The callback is invoked on the EmuThread. Return true to consume the event.
using UINavigationCallback = std::function<bool(GenericInputBinding, float)>;
void SetUINavigationCallback(UINavigationCallback callback);
void RemoveUINavigationCallback();

/// Directly invokes the UI navigation callback with the given generic binding and value.
/// Used by input sources to fire directional axis events with proper unipolar magnitudes.
void InvokeUINavigationEvent(GenericInputBinding key, float value);

/// Internal method used by pads to dispatch vibration updates to input sources.
/// Intensity is normalized from 0 to 1.
void SetUSBVibrationIntensity(u32 port, float large_or_single_motor_intensity, float small_motor_intensity);
Expand Down
22 changes: 17 additions & 5 deletions pcsx2/Input/SDLInputSource.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1460,7 +1460,19 @@ bool SDLInputSource::HandleGamepadAxisEvent(const SDL_GamepadAxisEvent* ev)
return false;

const InputBindingKey key(MakeGenericControllerAxisKey(InputSourceType::SDL, it->player_id, ev->axis));
InputManager::InvokeEvents(key, NormalizeS16(ev->value));
const float value = NormalizeS16(ev->value);
InputManager::InvokeEvents(key, value);

// Fire directional generic events so UI navigation (game list) receives proper unipolar
// magnitudes for each direction — the bipolar raw value isn't usable for threshold checks.
if (ev->axis < std::size(s_sdl_generic_binding_axis_mapping))
{
InputManager::InvokeUINavigationEvent(s_sdl_generic_binding_axis_mapping[ev->axis][0],
(value < 0.0f) ? -value : 0.0f);
InputManager::InvokeUINavigationEvent(s_sdl_generic_binding_axis_mapping[ev->axis][1],
(value > 0.0f) ? value : 0.0f);
}

return true;
}

Expand All @@ -1472,8 +1484,8 @@ bool SDLInputSource::HandleGamepadButtonEvent(const SDL_GamepadButtonEvent* ev)

const InputBindingKey key(MakeGenericControllerButtonKey(InputSourceType::SDL, it->player_id, ev->button));
const GenericInputBinding generic_key = (ev->button < std::size(s_sdl_generic_binding_button_mapping)) ?
s_sdl_generic_binding_button_mapping[ev->button] :
GenericInputBinding::Unknown;
s_sdl_generic_binding_button_mapping[ev->button] :
GenericInputBinding::Unknown;
InputManager::InvokeEvents(key, static_cast<float>(ev->down), generic_key);
return true;
}
Expand Down Expand Up @@ -1736,6 +1748,6 @@ bool SDLInputSource::IsControllerSixaxis(const ControllerData& cd)
// This differing layout also isn't mapped correctly in SDL, I think due to how L2/R2 are exposed.
// Also see SetHints regarding reading the pressure sense from DsHidMini's SDF mode.
return type == SDL_GAMEPAD_TYPE_PS3 &&
SDL_GetNumJoystickAxes(cd.joystick) == 16 &&
SDL_GetNumJoystickButtons(cd.joystick) == 11;
SDL_GetNumJoystickAxes(cd.joystick) == 16 &&
SDL_GetNumJoystickButtons(cd.joystick) == 11;
}
4 changes: 4 additions & 0 deletions tests/ctest/core/StubHost.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,10 @@ void Host::RequestExitBigPicture()
{
}

void Host::RequestEnterBigPicture()
{
}

void Host::RequestVMShutdown(bool allow_confirm, bool allow_save_state, bool default_save_state)
{
}
Expand Down
Loading