InputManager: Add 'Disable Background Input' option

Ignores controller input when application is not in the foreground.
This commit is contained in:
Stenzek
2026-01-14 01:10:04 +10:00
parent f901d716bc
commit e52b7dde8b
14 changed files with 134 additions and 77 deletions

View File

@@ -2266,9 +2266,10 @@ void FullscreenUI::DrawInterfaceSettingsPage()
bsi, FSUI_ICONVSTR(ICON_FA_WAND_MAGIC_SPARKLES, "Inhibit Screensaver"),
FSUI_VSTR("Prevents the screen saver from activating and the host from sleeping while emulation is running."),
"Main", "InhibitScreensaver", true);
DrawToggleSetting(bsi, FSUI_ICONVSTR(ICON_FA_PAUSE, "Pause On Start"),
FSUI_VSTR("Pauses the emulator when a game is started."), "Main", "StartPaused", false);
DrawToggleSetting(bsi, FSUI_ICONVSTR(ICON_FA_EYE_LOW_VISION, "Pause On Focus Loss"),
DrawToggleSetting(bsi, FSUI_ICONVSTR(ICON_FA_EYE_LOW_VISION, "Disable Background Input"),
FSUI_VSTR("Prevents inputs from being processed when another application is active."), "Main",
"DisableBackgroundInput", false);
DrawToggleSetting(bsi, FSUI_ICONVSTR(ICON_FA_PAUSE, "Pause On Focus Loss"),
FSUI_VSTR("Pauses the emulator when you minimize the window or switch to another "
"application, and unpauses when you switch back."),
"Main", "PauseOnFocusLoss", false);
@@ -5181,6 +5182,9 @@ void FullscreenUI::DrawAdvancedSettingsPage()
FSUI_VSTR("Uses OpenGL ES even when desktop OpenGL is supported. May improve performance on some SBC drivers."),
"GPU", "PreferGLESContext", Settings::DEFAULT_GPU_PREFER_GLES_CONTEXT);
DrawToggleSetting(bsi, FSUI_VSTR("Pause On Start"), FSUI_VSTR("Pauses the emulator when a game is started."), "Main",
"StartPaused", false);
DrawToggleSetting(
bsi, FSUI_VSTR("Load Devices From Save States"),
FSUI_VSTR("When enabled, memory cards and controllers will be overwritten when save states are loaded."), "Main",

View File

@@ -261,6 +261,7 @@ TRANSLATE_NOOP("FullscreenUI", "Determines the size of screenshots created by Du
TRANSLATE_NOOP("FullscreenUI", "Determines whether a prompt will be displayed to confirm closing the game.");
TRANSLATE_NOOP("FullscreenUI", "Determines which algorithm is used to convert interlaced frames to progressive for display on your system.");
TRANSLATE_NOOP("FullscreenUI", "Device Settings");
TRANSLATE_NOOP("FullscreenUI", "Disable Background Input");
TRANSLATE_NOOP("FullscreenUI", "Disable Mailbox Presentation");
TRANSLATE_NOOP("FullscreenUI", "Disable Speedup on MDEC");
TRANSLATE_NOOP("FullscreenUI", "Disable Subdirectory Scanning");
@@ -560,6 +561,7 @@ TRANSLATE_NOOP("FullscreenUI", "Preload Replacement Textures");
TRANSLATE_NOOP("FullscreenUI", "Preserve Projection Precision");
TRANSLATE_NOOP("FullscreenUI", "Press To Toggle");
TRANSLATE_NOOP("FullscreenUI", "Pressure");
TRANSLATE_NOOP("FullscreenUI", "Prevents inputs from being processed when another application is active.");
TRANSLATE_NOOP("FullscreenUI", "Prevents resizing of the window while a game is running.");
TRANSLATE_NOOP("FullscreenUI", "Prevents the emulator from producing any audible sound.");
TRANSLATE_NOOP("FullscreenUI", "Prevents the screen saver from activating and the host from sleeping while emulation is running.");

View File

@@ -217,6 +217,7 @@ void Settings::Load(const SettingsInterface& si, const SettingsInterface& contro
inhibit_screensaver = si.GetBoolValue("Main", "InhibitScreensaver", true);
pause_on_focus_loss = si.GetBoolValue("Main", "PauseOnFocusLoss", false);
pause_on_controller_disconnection = si.GetBoolValue("Main", "PauseOnControllerDisconnection", false);
ignore_background_input = si.GetBoolValue("Main", "IgnoreBackgroundInput", false);
save_state_on_exit = si.GetBoolValue("Main", "SaveStateOnExit", true);
create_save_state_backups = si.GetBoolValue("Main", "CreateSaveStateBackups", DEFAULT_SAVE_STATE_BACKUPS);
confim_power_off = si.GetBoolValue("Main", "ConfirmPowerOff", true);
@@ -631,6 +632,8 @@ void Settings::Save(SettingsInterface& si, bool ignore_base) const
si.SetBoolValue("Main", "EnableDiscordPresence", enable_discord_presence);
}
si.SetBoolValue("Main", "IgnoreBackgroundInput", ignore_background_input);
si.SetBoolValue("Main", "LoadDevicesFromSaveStates", load_devices_from_save_states);
si.SetBoolValue("Main", "DisableAllEnhancements", disable_all_enhancements);
si.SetBoolValue("Main", "RewindEnable", rewind_enable);

View File

@@ -327,6 +327,7 @@ struct Settings : public GPUSettings
bool inhibit_screensaver : 1 = true;
bool pause_on_focus_loss : 1 = false;
bool pause_on_controller_disconnection : 1 = false;
bool ignore_background_input : 1 = false;
bool save_state_on_exit : 1 = true;
bool create_save_state_backups : 1 = DEFAULT_SAVE_STATE_BACKUPS;
bool confim_power_off : 1 = true;

View File

@@ -4856,6 +4856,9 @@ void System::CheckForSettingsChanges(const Settings& old_settings)
}
}
if (g_settings.ignore_background_input != old_settings.ignore_background_input)
InputManager::UpdateInputIgnoreState();
Achievements::UpdateSettings(old_settings);
#ifdef ENABLE_DISCORD_PRESENCE

View File

@@ -259,6 +259,8 @@ void AdvancedSettingsWidget::addTweakOptions()
"ApplyCompatibilitySettings", true);
addBooleanTweakOption(m_dialog, m_ui.tweakOptionTable, tr("Load Devices From Save States"), "Main",
"LoadDevicesFromSaveStates", false);
addBooleanTweakOption(m_dialog, m_ui.tweakOptionTable, tr("Pause On Start"), "Main", "StartPaused",
false);
addChoiceTweakOption(m_dialog, m_ui.tweakOptionTable, tr("Save State Compression"), "Main", "SaveStateCompression",
&Settings::ParseSaveStateCompressionModeName, &Settings::GetSaveStateCompressionModeName,
&Settings::GetSaveStateCompressionModeDisplayName,
@@ -334,6 +336,7 @@ void AdvancedSettingsWidget::onResetToDefaultClicked()
setBooleanTweakOption(m_ui.tweakOptionTable, i++, true); // Apply Game Settings
setBooleanTweakOption(m_ui.tweakOptionTable, i++, true); // Apply Compatibility settings
setBooleanTweakOption(m_ui.tweakOptionTable, i++, false); // Pause On Start
setBooleanTweakOption(m_ui.tweakOptionTable, i++, false); // Load Devices From Save States
setChoiceTweakOption(m_ui.tweakOptionTable, i++,
Settings::DEFAULT_SAVE_STATE_COMPRESSION_MODE); // Save State Compression
@@ -376,6 +379,7 @@ void AdvancedSettingsWidget::onResetToDefaultClicked()
INISettingsInterface* sif = m_dialog->getSettingsInterface();
sif->DeleteValue("Main", "ApplyCompatibilitySettings");
sif->DeleteValue("Main", "LoadDevicesFromSaveStates");
sif->DeleteValue("Main", "PauseOnStart");
sif->DeleteValue("Main", "CompressSaveStates");
sif->DeleteValue("Display", "ActiveStartOffset");
sif->DeleteValue("Display", "ActiveEndOffset");

View File

@@ -67,7 +67,8 @@ InterfaceSettingsWidget::InterfaceSettingsWidget(SettingsWindow* dialog, QWidget
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.pauseOnFocusLoss, "Main", "PauseOnFocusLoss", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.pauseOnControllerDisconnection, "Main",
"PauseOnControllerDisconnection", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.pauseOnStart, "Main", "StartPaused", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.disableBackgroundInput, "Main", "DisableBackgroundInput",
false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.saveStateOnGameClose, "Main", "SaveStateOnExit", true);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.confirmGameClose, "Main", "ConfirmPowerOff", true);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.startFullscreen, "Main", "StartFullscreen", false);
@@ -159,8 +160,8 @@ InterfaceSettingsWidget::InterfaceSettingsWidget(SettingsWindow* dialog, QWidget
dialog->registerWidgetHelp(
m_ui.inhibitScreensaver, tr("Inhibit Screensaver"), tr("Checked"),
tr("Prevents the screen saver from activating and the host from sleeping while emulation is running."));
dialog->registerWidgetHelp(m_ui.pauseOnStart, tr("Pause On Start"), tr("Unchecked"),
tr("Pauses the emulator when a game is started."));
dialog->registerWidgetHelp(m_ui.disableBackgroundInput, tr("Disable Background Input"), tr("Unchecked"),
tr("Prevents inputs from being processed when another application is active."));
dialog->registerWidgetHelp(m_ui.pauseOnFocusLoss, tr("Pause On Focus Loss"), tr("Unchecked"),
tr("Pauses the emulator when you minimize the window or switch to another application, "
"and unpauses when you switch back."));

View File

@@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>652</width>
<height>490</height>
<height>572</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
@@ -50,13 +50,6 @@
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QCheckBox" name="pauseOnStart">
<property name="text">
<string>Pause On Start</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QCheckBox" name="pauseOnFocusLoss">
<property name="text">
@@ -64,13 +57,6 @@
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QCheckBox" name="pauseOnControllerDisconnection">
<property name="text">
<string>Pause On Controller Disconnection</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QCheckBox" name="createSaveStateBackups">
<property name="text">
@@ -85,6 +71,20 @@
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QCheckBox" name="pauseOnControllerDisconnection">
<property name="text">
<string>Pause On Controller Disconnection</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QCheckBox" name="disableBackgroundInput">
<property name="text">
<string>Disable Background Input</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>

View File

@@ -680,36 +680,6 @@ void MainWindow::onMediaCaptureStopped()
m_ui.actionMediaCapture->setChecked(false);
}
void MainWindow::onApplicationStateChanged(Qt::ApplicationState state)
{
if (!s_locals.system_valid)
return;
const bool focus_loss = (state != Qt::ApplicationActive);
if (focus_loss)
{
if (g_settings.pause_on_focus_loss && !m_was_paused_by_focus_loss && !s_locals.system_paused)
{
g_core_thread->setSystemPaused(true);
m_was_paused_by_focus_loss = true;
}
// Clear the state of all keyboard binds.
// That way, if we had a key held down, and lost focus, the bind won't be stuck enabled because we never
// got the key release message, because it happened in another window which "stole" the event.
g_core_thread->clearInputBindStateFromSource(InputManager::MakeHostKeyboardKey(0));
}
else
{
if (m_was_paused_by_focus_loss)
{
if (s_locals.system_paused)
g_core_thread->setSystemPaused(false);
m_was_paused_by_focus_loss = false;
}
}
}
void MainWindow::onStartFileActionTriggered()
{
QString filename = QDir::toNativeSeparators(
@@ -2440,7 +2410,6 @@ void MainWindow::switchToEmulationView()
void MainWindow::connectSignals()
{
connect(qApp, &QGuiApplication::applicationStateChanged, this, &MainWindow::onApplicationStateChanged);
connect(m_ui.toolBar, &QToolBar::customContextMenuRequested, this, &MainWindow::onToolbarContextMenuRequested);
connect(m_ui.toolBar, &QToolBar::topLevelChanged, this, &MainWindow::onToolbarTopLevelChanged);

View File

@@ -253,8 +253,6 @@ private:
Host::AuxiliaryRenderWindowHandle* handle, WindowInfo* wi, Error* error);
void onDestroyAuxiliaryRenderWindow(Host::AuxiliaryRenderWindowHandle handle, QPoint* pos, QSize* size);
void onApplicationStateChanged(Qt::ApplicationState state);
void onToolbarContextMenuRequested(const QPoint& pos);
void onToolbarTopLevelChanged(bool top_level);
@@ -355,7 +353,6 @@ private:
MemoryEditorWindow* m_memory_editor_window = nullptr;
CoverDownloadWindow* m_cover_download_window = nullptr;
bool m_was_paused_by_focus_loss = false;
bool m_relative_mouse_mode = false;
bool m_hide_mouse_cursor = false;

View File

@@ -1291,17 +1291,6 @@ void CoreThread::updatePostProcessingSettings(bool display, bool internal, bool
GPUPresenter::ReloadPostProcessingSettings(display, internal, force_reload);
}
void CoreThread::clearInputBindStateFromSource(InputBindingKey key)
{
if (!isCurrentThread())
{
QMetaObject::invokeMethod(this, &CoreThread::clearInputBindStateFromSource, Qt::QueuedConnection, key);
return;
}
InputManager::ClearBindStateFromSource(key);
}
void CoreThread::reloadTextureReplacements()
{
if (!isCurrentThread())
@@ -1658,6 +1647,38 @@ void CoreThread::saveScreenshot()
System::SaveScreenshot();
}
void CoreThread::applicationStateChanged(Qt::ApplicationState state)
{
const bool background = (state != Qt::ApplicationActive);
InputManager::OnApplicationBackgroundStateChanged(background);
if (!System::IsValid())
return;
if (background)
{
if (g_settings.pause_on_focus_loss && !m_was_paused_by_focus_loss && !System::IsPaused())
{
setSystemPaused(true);
m_was_paused_by_focus_loss = true;
}
// Clear the state of all keyboard binds.
// That way, if we had a key held down, and lost focus, the bind won't be stuck enabled because we never
// got the key release message, because it happened in another window which "stole" the event.
InputManager::ClearBindStateFromSource(InputManager::MakeHostKeyboardKey(0));
}
else
{
if (m_was_paused_by_focus_loss)
{
if (System::IsPaused())
setSystemPaused(false);
m_was_paused_by_focus_loss = false;
}
}
}
void Host::OnAchievementsLoginRequested(Achievements::LoginRequestReason reason)
{
emit g_core_thread->achievementsLoginRequested(reason);
@@ -1964,6 +1985,9 @@ void CoreThread::run()
// TODO: Replace this with QThreads
s_async_task_queue.SetWorkerCount(NUM_ASYNC_WORKER_THREADS);
// connections
connect(qApp, &QGuiApplication::applicationStateChanged, this, &CoreThread::applicationStateChanged);
// enumerate all devices, even those which were added early
m_input_device_list_model->enumerateDevices();

View File

@@ -166,6 +166,7 @@ public:
void dumpVRAM(const QString& path);
void dumpSPURAM(const QString& path);
void saveScreenshot();
void applicationStateChanged(Qt::ApplicationState state);
void redrawDisplayWindow();
void toggleFullscreen();
void setFullscreen(bool fullscreen);
@@ -175,7 +176,6 @@ public:
void applyCheat(const QString& name);
void reloadPostProcessingShaders();
void updatePostProcessingSettings(bool display, bool internal, bool force_reload);
void clearInputBindStateFromSource(InputBindingKey key);
void reloadTextureReplacements();
void captureGPUFrameDump();
void startControllerTest();
@@ -215,6 +215,7 @@ private:
bool m_shutdown_flag = false;
bool m_gpu_thread_run_idle = false;
bool m_is_fullscreen_ui_started = false;
bool m_was_paused_by_focus_loss = false;
float m_last_speed = std::numeric_limits<float>::infinity();
float m_last_game_fps = std::numeric_limits<float>::infinity();

View File

@@ -164,6 +164,7 @@ static void InternalClearEffects();
static void GenerateRelativeMouseEvents();
[[maybe_unused]] static void ReloadDevices();
static bool ShouldMaskBackgroundInput(InputBindingKey key);
static bool DoEventHook(InputBindingKey key, float value);
static bool PreprocessEvent(InputBindingKey key, float value, GenericInputBinding generic_key);
static bool ProcessEvent(InputBindingKey key, float value, bool skip_button_handlers);
@@ -228,6 +229,14 @@ struct ALIGN_TO_CACHE_LINE State
// Input sources. Keyboard/mouse don't exist here.
std::array<std::unique_ptr<InputSource>, static_cast<u32>(InputSourceType::Count)> input_sources;
bool application_in_background = false;
bool ignore_input_events = false;
bool has_pointer_device_bindings = false;
bool relative_mouse_mode = false;
bool relative_mouse_mode_active = false;
bool hide_host_mouse_cursor = false;
bool hide_host_mouse_cusor_active = false;
#ifdef _WIN32
// Device notification handle for Windows.
HCMNOTIFICATION device_notification_handle = nullptr;
@@ -244,11 +253,6 @@ struct ALIGN_TO_CACHE_LINE State
// Window size, used for clamping the mouse position in raw input modes.
std::array<float, 2> window_size = {};
bool has_pointer_device_bindings = false;
bool relative_mouse_mode = false;
bool relative_mouse_mode_active = false;
bool hide_host_mouse_cursor = false;
bool hide_host_mouse_cusor_active = false;
};
} // namespace
@@ -1157,14 +1161,28 @@ bool InputManager::IsAxisHandler(const InputEventHandler& handler)
return std::holds_alternative<InputAxisEventHandler>(handler);
}
bool InputManager::InvokeEvents(InputBindingKey key, float value, GenericInputBinding generic_key)
bool InputManager::ShouldMaskBackgroundInput(InputBindingKey key)
{
// Keyboard events won't get sent to us if we're in the background.
// We want to still update our mouse pointer state.
// Sensors are probably fine, but not used on desktop.
// Everything else should be ignored.
return (key.source_type > InputSourceType::Sensor && s_state.ignore_input_events);
}
void InputManager::InvokeEvents(InputBindingKey key, float value, GenericInputBinding generic_key)
{
if (DoEventHook(key, value))
return true;
return;
// If imgui ate the event, don't fire our handlers.
const bool skip_button_handlers = PreprocessEvent(key, value, generic_key);
return ProcessEvent(key, value, skip_button_handlers);
// Background input test
if (ShouldMaskBackgroundInput(key))
return;
ProcessEvent(key, value, skip_button_handlers);
}
bool InputManager::ProcessEvent(InputBindingKey key, float value, bool skip_button_handlers)
@@ -1534,7 +1552,7 @@ void InputManager::UpdatePointerRelativeDelta(u32 index, InputPointerAxis axis,
s_state.host_pointer_positions[index][static_cast<u8>(axis)] += d;
s_state.pointer_state[index][static_cast<u8>(axis)].delta.fetch_add(static_cast<s32>(d * 65536.0f),
std::memory_order_release);
std::memory_order_acq_rel);
// We need to clamp the position ourselves in relative mode.
if (axis <= InputPointerAxis::Y)
@@ -1621,6 +1639,32 @@ std::pair<float, float> InputManager::GetDisplayWindowSize()
return std::make_pair(s_state.window_size[0], s_state.window_size[1]);
}
void InputManager::OnApplicationBackgroundStateChanged(bool in_background)
{
s_state.application_in_background = in_background;
UpdateInputIgnoreState();
}
void InputManager::UpdateInputIgnoreState()
{
const bool prev_ignore_input_events = s_state.ignore_input_events;
s_state.ignore_input_events = s_state.application_in_background && g_settings.ignore_background_input;
if (s_state.ignore_input_events != prev_ignore_input_events)
{
if (s_state.ignore_input_events)
{
VERBOSE_COLOR_LOG(StrongOrange, "Application in background, ignoring input events");
}
else
{
VERBOSE_COLOR_LOG(StrongGreen, "Application in foreground, processing input events");
// Synchronize button state, it might have changed
SynchronizeBindingHandlerState();
}
}
}
void InputManager::SetDefaultSourceConfig(SettingsInterface& si)
{
si.ClearSection("InputSources");

View File

@@ -310,7 +310,7 @@ void AddVibrationBinding(u32 pad_index, u32 bind_index, const InputBindingKey& b
/// Updates internal state for any binds for this key, and fires callbacks as needed.
/// Returns true if anything was bound to this key, otherwise false.
bool InvokeEvents(InputBindingKey key, float value, GenericInputBinding generic_key = GenericInputBinding::Unknown);
void InvokeEvents(InputBindingKey key, float value, GenericInputBinding generic_key = GenericInputBinding::Unknown);
/// Clears internal state for any binds with a matching source/index.
void ClearBindStateFromSource(InputBindingKey key);
@@ -370,6 +370,10 @@ bool IsUsingRawInput();
void SetDisplayWindowSize(float width, float height);
std::pair<float, float> GetDisplayWindowSize();
/// Called when the application window gains or loses focus.
void OnApplicationBackgroundStateChanged(bool in_background);
void UpdateInputIgnoreState();
/// Restores default configuration.
void SetDefaultSourceConfig(SettingsInterface& si);