summaryrefslogtreecommitdiff
path: root/Source
diff options
context:
space:
mode:
authorJosJuice <josjuice@gmail.com>2026-10-31 16:17:25 +0100
committerGitHub <noreply@github.com>2026-10-31 16:17:25 +0100
commitc4856ead6420cceb48744815d79d6be59febb5ff (patch)
treebc662fe345ab37ebc861b1d013da411d06688729 /Source
parent78a8ab40c3b28a6d477c9c6b6260dc3993cfe491 (diff)
parent9e6659a2d8c890d60be9a3609e4e55d3a75a1216 (diff)
Merge pull request #14878 from vhoxha08/add-global-hotkeys-android
Android: Add global hotkeys
Diffstat (limited to 'Source')
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.kt8
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/Hotkeys.kt11
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/controlleremu/EmulatedController.kt6
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/MenuTag.kt4
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt33
-rw-r--r--Source/Android/app/src/main/res/values/strings.xml1
-rw-r--r--Source/Android/jni/CMakeLists.txt2
-rw-r--r--Source/Android/jni/HotkeyScheduler.cpp355
-rw-r--r--Source/Android/jni/HotkeyScheduler.h17
-rw-r--r--Source/Android/jni/Input/EmulatedController.cpp34
-rw-r--r--Source/Android/jni/MainAndroid.cpp5
-rw-r--r--Source/Core/Core/HotkeyManager.cpp2
-rw-r--r--Source/Core/Core/State.cpp41
-rw-r--r--Source/Core/Core/State.h11
-rw-r--r--Source/Core/DolphinQt/MainWindow.cpp24
-rw-r--r--Source/Core/DolphinQt/MainWindow.h1
16 files changed, 536 insertions, 19 deletions
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.kt
index 2761c4b4e9..0145887b78 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.kt
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.kt
@@ -42,6 +42,7 @@ import org.dolphinemu.dolphinemu.features.infinitybase.ui.FigureSlot
import org.dolphinemu.dolphinemu.features.infinitybase.ui.FigureSlotAdapter
import org.dolphinemu.dolphinemu.features.input.model.ControllerInterface
import org.dolphinemu.dolphinemu.features.input.model.DolphinSensorEventListener
+import org.dolphinemu.dolphinemu.features.input.model.Hotkeys
import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting
import org.dolphinemu.dolphinemu.utils.ContentHandler
import org.dolphinemu.dolphinemu.features.settings.model.IntSetting
@@ -319,6 +320,13 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider {
}
DolphinSensorEventListener.setDeviceRotation(windowManager.defaultDisplay.rotation)
+
+ Hotkeys.setEnabled(true)
+ }
+
+ override fun onPause() {
+ Hotkeys.setEnabled(false)
+ super.onPause()
}
override fun onStop() {
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/Hotkeys.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/Hotkeys.kt
new file mode 100644
index 0000000000..7f0100238b
--- /dev/null
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/Hotkeys.kt
@@ -0,0 +1,11 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.dolphinemu.dolphinemu.features.input.model
+
+object Hotkeys {
+ /**
+ * Stops or resumes the native hotkey scheduler acting on presses.
+ */
+ @JvmStatic
+ external fun setEnabled(enabled: Boolean)
+}
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/controlleremu/EmulatedController.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/controlleremu/EmulatedController.kt
index e0938b975d..00c06cb896 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/controlleremu/EmulatedController.kt
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/controlleremu/EmulatedController.kt
@@ -56,6 +56,12 @@ class EmulatedController private constructor(private val pointer: Long) : Contro
external fun getWiimote(controllerIndex: Int): EmulatedController
@JvmStatic
+ external fun getHotkeys(): EmulatedController
+
+ @JvmStatic
+ external fun getSupportedHotkeyIndices(): IntArray
+
+ @JvmStatic
external fun getWiimoteAttachment(
controllerIndex: Int,
attachmentIndex: Int
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/MenuTag.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/MenuTag.kt
index 74556b07d9..3a6089144f 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/MenuTag.kt
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/MenuTag.kt
@@ -26,6 +26,7 @@ enum class MenuTag {
STATISTICS("statistics"),
ADVANCED_GRAPHICS("advanced_graphics"),
GCPAD_TYPE("gc_pad_type"),
+ HOTKEYS("hotkeys"),
WIIMOTE("wiimote"),
WIIMOTE_EXTENSION("wiimote_extension"),
GCPAD_1("gcpad", 0),
@@ -77,7 +78,8 @@ enum class MenuTag {
val correspondingEmulatedController: EmulatedController
get() = if (isGCPadMenu) EmulatedController.getGcPad(subType) else if (isWiimoteMenu) EmulatedController.getWiimote(
subType
- ) else throw UnsupportedOperationException()
+ ) else if (this == HOTKEYS) EmulatedController.getHotkeys()
+ else throw UnsupportedOperationException()
val isSerialPort1Menu: Boolean
get() = this == CONFIG_SERIALPORT1
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt
index 8a46a698ce..e3b47b3786 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt
@@ -303,6 +303,7 @@ class SettingsFragmentPresenter(
)
MenuTag.GCPAD_TYPE -> addGcPadSettings(sl)
+ MenuTag.HOTKEYS -> addHotkeySettings(sl)
MenuTag.WIIMOTE -> addWiimoteSettings(sl)
MenuTag.ENHANCEMENTS -> addEnhanceSettings(sl)
MenuTag.COLOR_CORRECTION -> addColorCorrectionSettings(sl)
@@ -354,6 +355,11 @@ class SettingsFragmentPresenter(
sl.add(SubmenuSetting(context, R.string.wiimote_settings, MenuTag.WIIMOTE))
}
+ // Hotkeys live in Hotkeys.ini and are global, so there is nothing to edit per game.
+ if (gameId.isNullOrEmpty()) {
+ sl.add(SubmenuSetting(context, R.string.hotkey_settings, MenuTag.HOTKEYS))
+ }
+
sl.add(HeaderSetting(context, R.string.setting_clear_info, 0))
}
@@ -2503,6 +2509,33 @@ class SettingsFragmentPresenter(
)
}
+ /**
+ * Lists a subset of the hotkeys the native HotkeyScheduler dispatches.
+ */
+ private fun addHotkeySettings(sl: ArrayList<SettingsItem>) {
+ val hotkeys = EmulatedController.getHotkeys()
+
+ addControllerMetaSettings(sl, hotkeys)
+
+ // Flattened {group index, control index} pairs. Each control group gets a header the first
+ // time one of its hotkeys shows up.
+ val indices = EmulatedController.getSupportedHotkeyIndices()
+ var currentGroupIndex = -1
+ var i = 0
+ while (i < indices.size) {
+ val groupIndex = indices[i]
+ val group = hotkeys.getGroup(groupIndex)
+
+ if (groupIndex != currentGroupIndex) {
+ sl.add(HeaderSetting(group.getUiName(), ""))
+ currentGroupIndex = groupIndex
+ }
+
+ sl.add(InputMappingControlSetting(group.getControl(indices[i + 1]), hotkeys))
+ i += 2
+ }
+ }
+
private fun addGcPadSubSettings(
sl: ArrayList<SettingsItem>, gcPadNumber: Int, gcPadType: Int
) {
diff --git a/Source/Android/app/src/main/res/values/strings.xml b/Source/Android/app/src/main/res/values/strings.xml
index 69d8631e7e..6abf81e1cc 100644
--- a/Source/Android/app/src/main/res/values/strings.xml
+++ b/Source/Android/app/src/main/res/values/strings.xml
@@ -71,6 +71,7 @@
<string name="graphics_settings">Graphics Settings</string>
<string name="gcpad_settings">GameCube Input</string>
<string name="wiimote_settings">Wii Input</string>
+ <string name="hotkey_settings">Hotkeys</string>
<!-- General Preference Fragment -->
<string name="general_submenu">General</string>
diff --git a/Source/Android/jni/CMakeLists.txt b/Source/Android/jni/CMakeLists.txt
index a3a741a433..a7ee0b6b3a 100644
--- a/Source/Android/jni/CMakeLists.txt
+++ b/Source/Android/jni/CMakeLists.txt
@@ -14,6 +14,8 @@ add_library(main SHARED
GameList/GameFile.h
GameList/GameFileCache.cpp
GpuDriver.cpp
+ HotkeyScheduler.cpp
+ HotkeyScheduler.h
InfinityConfig.cpp
Input/Control.cpp
Input/Control.h
diff --git a/Source/Android/jni/HotkeyScheduler.cpp b/Source/Android/jni/HotkeyScheduler.cpp
new file mode 100644
index 0000000000..e4e44415b6
--- /dev/null
+++ b/Source/Android/jni/HotkeyScheduler.cpp
@@ -0,0 +1,355 @@
+// Copyright 2026 Dolphin Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "jni/HotkeyScheduler.h"
+
+#include <chrono>
+#include <cmath>
+#include <jni.h>
+#include <thread>
+#include <vector>
+
+#include <fmt/format.h>
+
+#include "AudioCommon/AudioCommon.h"
+#include "Common/CommonTypes.h"
+#include "Common/Config/Config.h"
+#include "Common/Event.h"
+#include "Common/Flag.h"
+#include "Common/Thread.h"
+#include "Core/Config/GraphicsSettings.h"
+#include "Core/Config/MainSettings.h"
+#include "Core/Core.h"
+#include "Core/HotkeyManager.h"
+#include "Core/State.h"
+#include "Core/System.h"
+#include "InputCommon/ControlReference/ControlReference.h"
+#include "InputCommon/ControllerEmu/Control/Control.h"
+#include "InputCommon/ControllerEmu/ControlGroup/ControlGroup.h"
+#include "InputCommon/ControllerInterface/ControllerInterface.h"
+#include "VideoCommon/OnScreenDisplay.h"
+#include "VideoCommon/VideoConfig.h"
+
+namespace HotkeyScheduler
+{
+namespace
+{
+constexpr int POLL_INTERVAL_MS = 5;
+
+std::thread s_thread;
+Common::Flag s_stop_requested;
+Common::Flag s_any_hotkey_set;
+Common::Event s_wakeup;
+
+bool IsHotkey(int id, bool held = false)
+{
+ return HotkeyManagerEmu::IsPressed(id, held);
+}
+
+bool IsAnyHotkeySet()
+{
+ InputConfig* const config = HotkeyManagerEmu::GetConfig();
+ if (config == nullptr || config->ControllersNeedToBeCreated())
+ return false;
+
+ auto* const hotkeys = static_cast<HotkeyManager*>(config->GetController(0));
+
+ for (const int hotkey : GetSupportedHotkeys())
+ {
+ const int group = hotkeys->FindGroupByID(hotkey);
+ const int index = hotkeys->GetIndexForGroup(group, hotkey);
+ const auto* const control_group =
+ HotkeyManagerEmu::GetHotkeyGroup(static_cast<HotkeyGroup>(group));
+
+ if (!control_group->controls[index]->control_ref->GetExpression().empty())
+ return true;
+ }
+
+ return false;
+}
+
+bool ShouldPoll()
+{
+ if (!s_any_hotkey_set.IsSet() || !HotkeyManagerEmu::IsEnabled())
+ return false;
+
+ return Core::GetState(Core::System::GetInstance()) != Core::State::Stopping;
+}
+
+// Buttons already held when polling resumes must not count as new presses
+void ConsumePendingPresses()
+{
+ for (const int hotkey : GetSupportedHotkeys())
+ IsHotkey(hotkey);
+}
+
+void ShowVolumeOSD()
+{
+ OSD::AddMessage(
+ fmt::format("Volume: {}", Config::Get(Config::MAIN_AUDIO_MUTED) ?
+ "Muted" :
+ fmt::format("{}%", Config::Get(Config::MAIN_AUDIO_VOLUME))));
+}
+
+void ShowEmulationSpeedOSD()
+{
+ const float emulation_speed = Config::Get(Config::MAIN_EMULATION_SPEED);
+ OSD::AddMessage(emulation_speed <= 0 ?
+ "Speed Limit: Unlimited" :
+ fmt::format("Speed Limit: {}%", std::lround(emulation_speed * 100.f)));
+}
+
+void ShowInternalResolutionOSD(int new_efb_scale)
+{
+ switch (new_efb_scale)
+ {
+ case EFB_SCALE_AUTO_INTEGRAL:
+ OSD::AddMessage("Internal Resolution: Auto (integral)");
+ break;
+ case 1:
+ OSD::AddMessage("Internal Resolution: Native");
+ break;
+ default:
+ OSD::AddMessage(fmt::format("Internal Resolution: {}x", new_efb_scale));
+ break;
+ }
+}
+
+void HandleGeneralHotkeys()
+{
+ // TODO: add support for more general hotkeys
+ if (IsHotkey(HK_SCREENSHOT))
+ Core::SaveScreenShot();
+}
+
+void HandleEmulationSpeedHotkeys()
+{
+ if (IsHotkey(HK_TOGGLE_THROTTLE))
+ Core::SetIsThrottlerTempDisabled(!Core::GetIsThrottlerTempDisabled());
+
+ if (IsHotkey(HK_DECREASE_EMULATION_SPEED))
+ {
+ auto speed = Config::Get(Config::MAIN_EMULATION_SPEED) - 0.1;
+ if (speed > 0)
+ {
+ speed = (speed >= 0.95 && speed <= 1.05) ? 1.0 : speed;
+ Config::SetCurrent(Config::MAIN_EMULATION_SPEED, speed);
+ }
+ ShowEmulationSpeedOSD();
+ }
+
+ if (IsHotkey(HK_INCREASE_EMULATION_SPEED))
+ {
+ auto speed = Config::Get(Config::MAIN_EMULATION_SPEED) + 0.1;
+ speed = (speed >= 0.95 && speed <= 1.05) ? 1.0 : speed;
+ Config::SetCurrent(Config::MAIN_EMULATION_SPEED, speed);
+ ShowEmulationSpeedOSD();
+ }
+}
+
+void HandleVolumeHotkeys(Core::System& system)
+{
+ if (IsHotkey(HK_VOLUME_DOWN))
+ {
+ AudioCommon::DecreaseVolume(system, 3);
+ ShowVolumeOSD();
+ }
+
+ if (IsHotkey(HK_VOLUME_UP))
+ {
+ AudioCommon::IncreaseVolume(system, 3);
+ ShowVolumeOSD();
+ }
+
+ if (IsHotkey(HK_VOLUME_TOGGLE_MUTE))
+ {
+ AudioCommon::ToggleMuteVolume(system);
+ ShowVolumeOSD();
+ }
+}
+
+void HandleGraphicsHotkeys()
+{
+ const auto efb_scale = Config::Get(Config::GFX_EFB_SCALE);
+
+ if (IsHotkey(HK_INCREASE_IR))
+ {
+ Config::SetCurrent(Config::GFX_EFB_SCALE, efb_scale + 1);
+ ShowInternalResolutionOSD(efb_scale + 1);
+ }
+ if (IsHotkey(HK_DECREASE_IR))
+ {
+ if (efb_scale > EFB_SCALE_AUTO_INTEGRAL)
+ {
+ Config::SetCurrent(Config::GFX_EFB_SCALE, efb_scale - 1);
+ ShowInternalResolutionOSD(efb_scale - 1);
+ }
+ }
+}
+
+void HandleStateHotkeys(Core::System& system)
+{
+ for (u32 i = 0; i < State::NUM_STATES; ++i)
+ {
+ if (IsHotkey(HK_LOAD_STATE_SLOT_1 + i))
+ State::Load(system, i + 1);
+
+ if (IsHotkey(HK_SAVE_STATE_SLOT_1 + i))
+ State::Save(system, i + 1);
+
+ if (IsHotkey(HK_LOAD_LAST_STATE_1 + i))
+ State::LoadLastSaved(system, i + 1);
+
+ if (IsHotkey(HK_SELECT_STATE_SLOT_1 + i))
+ State::SelectSlot(i + 1);
+ }
+
+ if (IsHotkey(HK_SAVE_STATE_SLOT_SELECTED))
+ State::SaveSelected(system);
+
+ if (IsHotkey(HK_LOAD_STATE_SLOT_SELECTED))
+ State::LoadSelected(system);
+
+ if (IsHotkey(HK_INCREMENT_SELECTED_STATE_SLOT))
+ State::SelectSlot(State::GetNextSlot());
+
+ if (IsHotkey(HK_DECREMENT_SELECTED_STATE_SLOT))
+ State::SelectSlot(State::GetPreviousSlot());
+
+ if (IsHotkey(HK_SAVE_FIRST_STATE))
+ State::SaveFirstSaved(system);
+
+ if (IsHotkey(HK_UNDO_LOAD_STATE))
+ State::UndoLoadState(system);
+
+ if (IsHotkey(HK_UNDO_SAVE_STATE))
+ State::UndoSaveState(system);
+}
+
+void Run()
+{
+ Common::SetCurrentThreadName("HotkeyScheduler");
+
+ bool was_polling = false;
+
+ while (!s_stop_requested.IsSet())
+ {
+ if (!ShouldPoll())
+ {
+ was_polling = false;
+ s_wakeup.Wait();
+ continue;
+ }
+
+ s_wakeup.WaitFor(std::chrono::milliseconds(POLL_INTERVAL_MS));
+
+ if (s_stop_requested.IsSet())
+ break;
+
+ ControllerInterface::SetCurrentInputChannel(ciface::InputChannel::Host);
+ g_controller_interface.UpdateInput();
+
+ ControlReference::SetInputGate(true);
+
+ // These two cover disjoint sets of groups, see HotkeyManager::GetInput.
+ // Skipping either one leaves half the groups reading as unpressed.
+ HotkeyManagerEmu::GetStatus(false);
+ HotkeyManagerEmu::GetStatus(true);
+
+ if (!was_polling)
+ {
+ ConsumePendingPresses();
+ was_polling = true;
+ continue;
+ }
+
+ Core::System& system = Core::System::GetInstance();
+
+ HandleGeneralHotkeys();
+ HandleEmulationSpeedHotkeys();
+ HandleVolumeHotkeys(system);
+ HandleGraphicsHotkeys();
+ HandleStateHotkeys(system);
+ }
+}
+} // namespace
+
+void Start()
+{
+ if (s_thread.joinable())
+ return;
+
+ State::SetSelectedSlot(1);
+ s_any_hotkey_set.Set(IsAnyHotkeySet());
+ s_stop_requested.Clear();
+ s_wakeup.Reset();
+ s_thread = std::thread(Run);
+}
+
+void Stop()
+{
+ if (!s_thread.joinable())
+ return;
+
+ s_stop_requested.Set();
+ s_wakeup.Set();
+ s_thread.join();
+
+ Core::SetIsThrottlerTempDisabled(false);
+}
+
+void RefreshActiveHotkeys()
+{
+ s_any_hotkey_set.Set(IsAnyHotkeySet());
+ s_wakeup.Set();
+}
+
+const std::vector<int>& GetSupportedHotkeys()
+{
+ static const std::vector<int> supported = [] {
+ std::vector<int> hotkeys{
+ // General - only the one entry, see HandleGeneralHotkeys
+ HK_SCREENSHOT,
+ // Emulation Speed
+ HK_TOGGLE_THROTTLE,
+ HK_DECREASE_EMULATION_SPEED,
+ HK_INCREASE_EMULATION_SPEED,
+ // Volume
+ HK_VOLUME_DOWN,
+ HK_VOLUME_UP,
+ HK_VOLUME_TOGGLE_MUTE,
+ // Internal Resolution
+ HK_INCREASE_IR,
+ HK_DECREASE_IR,
+ };
+
+ hotkeys.insert(hotkeys.end(),
+ {HK_SAVE_STATE_SLOT_SELECTED, HK_LOAD_STATE_SLOT_SELECTED,
+ HK_INCREMENT_SELECTED_STATE_SLOT, HK_DECREMENT_SELECTED_STATE_SLOT,
+ HK_SAVE_FIRST_STATE, HK_UNDO_LOAD_STATE, HK_UNDO_SAVE_STATE});
+
+ for (u32 i = 0; i < State::NUM_STATES; ++i)
+ hotkeys.push_back(HK_SAVE_STATE_SLOT_1 + i);
+ for (u32 i = 0; i < State::NUM_STATES; ++i)
+ hotkeys.push_back(HK_LOAD_STATE_SLOT_1 + i);
+ for (u32 i = 0; i < State::NUM_STATES; ++i)
+ hotkeys.push_back(HK_SELECT_STATE_SLOT_1 + i);
+ for (u32 i = 0; i < State::NUM_STATES; ++i)
+ hotkeys.push_back(HK_LOAD_LAST_STATE_1 + i);
+
+ return hotkeys;
+ }();
+
+ return supported;
+}
+} // namespace HotkeyScheduler
+
+extern "C" {
+
+JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_features_input_model_Hotkeys_setEnabled(
+ JNIEnv*, jclass, jboolean enabled)
+{
+ HotkeyManagerEmu::Enable(enabled == JNI_TRUE);
+ HotkeyScheduler::RefreshActiveHotkeys();
+}
+}
diff --git a/Source/Android/jni/HotkeyScheduler.h b/Source/Android/jni/HotkeyScheduler.h
new file mode 100644
index 0000000000..d98d637991
--- /dev/null
+++ b/Source/Android/jni/HotkeyScheduler.h
@@ -0,0 +1,17 @@
+// Copyright 2026 Dolphin Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <vector>
+
+namespace HotkeyScheduler
+{
+void Start();
+
+void Stop();
+
+void RefreshActiveHotkeys();
+
+const std::vector<int>& GetSupportedHotkeys();
+} // namespace HotkeyScheduler
diff --git a/Source/Android/jni/Input/EmulatedController.cpp b/Source/Android/jni/Input/EmulatedController.cpp
index 495a376f69..341e35d168 100644
--- a/Source/Android/jni/Input/EmulatedController.cpp
+++ b/Source/Android/jni/Input/EmulatedController.cpp
@@ -3,18 +3,22 @@
#include <jni.h>
+#include <vector>
+
#include "Common/FileUtil.h"
#include "Common/IniFile.h"
#include "Core/HW/GCKeyboard.h"
#include "Core/HW/GCPad.h"
#include "Core/HW/Wiimote.h"
#include "Core/HW/WiimoteEmu/WiimoteEmu.h"
+#include "Core/HotkeyManager.h"
#include "InputCommon/ControllerEmu/ControlGroup/Attachments.h"
#include "InputCommon/ControllerEmu/ControllerEmu.h"
#include "InputCommon/ControllerInterface/ControllerInterface.h"
#include "InputCommon/InputConfig.h"
#include "jni/AndroidCommon/AndroidCommon.h"
#include "jni/AndroidCommon/IDCache.h"
+#include "jni/HotkeyScheduler.h"
#include "jni/Input/Control.h"
#include "jni/Input/ControlGroup.h"
#include "jni/Input/ControlReference.h"
@@ -192,6 +196,36 @@ Java_org_dolphinemu_dolphinemu_features_input_model_controlleremu_EmulatedContro
}
JNIEXPORT jobject JNICALL
+Java_org_dolphinemu_dolphinemu_features_input_model_controlleremu_EmulatedController_getHotkeys(
+ JNIEnv* env, jclass)
+{
+ return EmulatedControllerToJava(env, HotkeyManagerEmu::GetConfig()->GetController(0));
+}
+
+JNIEXPORT jintArray JNICALL
+Java_org_dolphinemu_dolphinemu_features_input_model_controlleremu_EmulatedController_getSupportedHotkeyIndices(
+ JNIEnv* env, jclass)
+{
+ auto* const hotkeys =
+ static_cast<HotkeyManager*>(HotkeyManagerEmu::GetConfig()->GetController(0));
+ const std::vector<int>& supported = HotkeyScheduler::GetSupportedHotkeys();
+
+ std::vector<jint> indices;
+ indices.reserve(supported.size() * 2);
+ for (const int hotkey : supported)
+ {
+ const int group = hotkeys->FindGroupByID(hotkey);
+ indices.push_back(group);
+ indices.push_back(hotkeys->GetIndexForGroup(group, hotkey));
+ }
+
+ const jsize size = static_cast<jsize>(indices.size());
+ jintArray result = env->NewIntArray(size);
+ env->SetIntArrayRegion(result, 0, size, indices.data());
+ return result;
+}
+
+JNIEXPORT jobject JNICALL
Java_org_dolphinemu_dolphinemu_features_input_model_controlleremu_EmulatedController_getWiimoteAttachment(
JNIEnv* env, jclass, jint controller_index, jint attachment_index)
{
diff --git a/Source/Android/jni/MainAndroid.cpp b/Source/Android/jni/MainAndroid.cpp
index 4806c9a2ff..1cd20f0818 100644
--- a/Source/Android/jni/MainAndroid.cpp
+++ b/Source/Android/jni/MainAndroid.cpp
@@ -67,6 +67,7 @@
#include "jni/AndroidCommon/AndroidCommon.h"
#include "jni/AndroidCommon/IDCache.h"
+#include "jni/HotkeyScheduler.h"
namespace
{
@@ -584,12 +585,16 @@ static void Run(JNIEnv* env, std::unique_ptr<BootParameters>&& boot, bool riivol
s_need_nonblocking_alert_msg = false;
surface_guard.unlock();
+ HotkeyScheduler::Start();
+
while (Core::IsRunning(Core::System::GetInstance()))
{
s_update_main_frame_event.Wait();
Core::HostDispatchJobs(Core::System::GetInstance());
}
+ HotkeyScheduler::Stop();
+
s_game_metadata_is_valid = false;
Core::Shutdown(Core::System::GetInstance());
diff --git a/Source/Core/Core/HotkeyManager.cpp b/Source/Core/Core/HotkeyManager.cpp
index 915dc0d6d7..4f3e4d8739 100644
--- a/Source/Core/Core/HotkeyManager.cpp
+++ b/Source/Core/Core/HotkeyManager.cpp
@@ -396,6 +396,7 @@ void HotkeyManager::LoadDefaults(const ControllerInterface& ciface)
{
EmulatedController::LoadDefaults(ciface);
+#ifndef ANDROID
auto set_key_expression = [this](int index, const std::string& expression) {
m_keys[FindGroupByID(index)]
->controls[GetIndexForGroup(FindGroupByID(index), index)]
@@ -473,4 +474,5 @@ void HotkeyManager::LoadDefaults(const ControllerInterface& ciface)
set_key_expression(HK_SKYLANDERS_PORTAL, hotkey_string({"Ctrl", "P"}));
set_key_expression(HK_INFINITY_BASE, hotkey_string({"Ctrl", "I"}));
+#endif
}
diff --git a/Source/Core/Core/State.cpp b/Source/Core/Core/State.cpp
index cecda5f4e8..952a23ffaa 100644
--- a/Source/Core/Core/State.cpp
+++ b/Source/Core/Core/State.cpp
@@ -78,6 +78,8 @@ static Common::UniqueBuffer<u8> s_undo_load_buffer;
// Used to estimate buffer size for the next save.
static u32 s_last_state_size = 0;
+static u32 s_selected_slot = 1;
+
// Shared locks are acquired for each state save task.
// Tasks generally transition from: Calling thread -> CPU thread -> Compress/Write thread.
// Holding an "exclusive" lock will:
@@ -913,6 +915,45 @@ void Load(Core::System& system, u32 slot)
LoadAs(system, MakeStateFilename(slot));
}
+u32 GetSelectedSlot()
+{
+ return s_selected_slot;
+}
+
+void SetSelectedSlot(u32 slot)
+{
+ s_selected_slot = std::clamp<u32>(slot, 1, NUM_STATES);
+}
+
+void SelectSlot(u32 slot)
+{
+ SetSelectedSlot(slot);
+
+ Core::DisplayMessage(fmt::format("Selected slot {} - {}", s_selected_slot,
+ GetInfoStringOfSlot(s_selected_slot, false)),
+ 2500);
+}
+
+u32 GetNextSlot()
+{
+ return s_selected_slot >= NUM_STATES ? 1 : s_selected_slot + 1;
+}
+
+u32 GetPreviousSlot()
+{
+ return s_selected_slot <= 1 ? NUM_STATES : s_selected_slot - 1;
+}
+
+void SaveSelected(Core::System& system)
+{
+ Save(system, s_selected_slot);
+}
+
+void LoadSelected(Core::System& system)
+{
+ Load(system, s_selected_slot);
+}
+
void LoadLastSaved(Core::System& system, int i)
{
if (!CheckIfStateLoadIsAllowed(system))
diff --git a/Source/Core/Core/State.h b/Source/Core/Core/State.h
index cd35ea1af0..e1bf3384a1 100644
--- a/Source/Core/Core/State.h
+++ b/Source/Core/Core/State.h
@@ -95,6 +95,17 @@ u64 GetUnixTimeOfSlot(u32 slot);
void Save(Core::System& system, u32 slot);
void Load(Core::System& system, u32 slot);
+u32 GetSelectedSlot();
+void SetSelectedSlot(u32 slot);
+
+void SelectSlot(u32 slot);
+
+u32 GetNextSlot();
+u32 GetPreviousSlot();
+
+void SaveSelected(Core::System& system);
+void LoadSelected(Core::System& system);
+
void SaveAs(Core::System& system, std::string filename);
void LoadAs(Core::System& system, std::string filename);
diff --git a/Source/Core/DolphinQt/MainWindow.cpp b/Source/Core/DolphinQt/MainWindow.cpp
index 688ae6ffcd..7905402013 100644
--- a/Source/Core/DolphinQt/MainWindow.cpp
+++ b/Source/Core/DolphinQt/MainWindow.cpp
@@ -297,8 +297,8 @@ MainWindow::MainWindow(Core::System& system, std::unique_ptr<BootParameters> boo
}
}
- m_state_slot =
- std::clamp(Settings::Instance().GetStateSlot(), 1, static_cast<int>(State::NUM_STATES));
+ State::SetSelectedSlot(
+ std::clamp(Settings::Instance().GetStateSlot(), 1, static_cast<int>(State::NUM_STATES)));
m_render_widget_geometry = settings.value(QStringLiteral("renderwidget/geometry")).toByteArray();
@@ -1531,12 +1531,12 @@ void MainWindow::StateSave()
void MainWindow::StateLoadSlot()
{
- State::Load(m_system, m_state_slot);
+ State::LoadSelected(m_system);
}
void MainWindow::StateSaveSlot()
{
- State::Save(m_system, m_state_slot);
+ State::SaveSelected(m_system);
}
void MainWindow::StateLoadSlotAt(int slot)
@@ -1572,27 +1572,17 @@ void MainWindow::StateSaveOldest()
void MainWindow::SetStateSlot(int slot)
{
Settings::Instance().SetStateSlot(slot);
- m_state_slot = slot;
-
- Core::DisplayMessage(fmt::format("Selected slot {} - {}", m_state_slot,
- State::GetInfoStringOfSlot(m_state_slot, false)),
- 2500);
+ State::SelectSlot(slot);
}
void MainWindow::IncrementSelectedStateSlot()
{
- u32 state_slot = m_state_slot + 1;
- if (state_slot > State::NUM_STATES)
- state_slot = 1;
- m_menu_bar->SetStateSlot(state_slot);
+ m_menu_bar->SetStateSlot(State::GetNextSlot());
}
void MainWindow::DecrementSelectedStateSlot()
{
- u32 state_slot = m_state_slot - 1;
- if (state_slot < 1)
- state_slot = State::NUM_STATES;
- m_menu_bar->SetStateSlot(state_slot);
+ m_menu_bar->SetStateSlot(State::GetPreviousSlot());
}
void MainWindow::PerformOnlineUpdate(const std::string& region)
diff --git a/Source/Core/DolphinQt/MainWindow.h b/Source/Core/DolphinQt/MainWindow.h
index 93490eaad6..c342dc0a65 100644
--- a/Source/Core/DolphinQt/MainWindow.h
+++ b/Source/Core/DolphinQt/MainWindow.h
@@ -250,7 +250,6 @@ private:
bool m_exit_requested = false;
bool m_fullscreen_requested = false;
bool m_is_screensaver_inhibited = false;
- u32 m_state_slot = 1;
std::unique_ptr<BootParameters> m_pending_boot;
SettingsWindow* m_settings_window = nullptr;