allow overriding hotkeys / inputs instead of failing

This commit is contained in:
David Griswold 2026-03-07 15:32:23 +03:00 committed by NovaChild
parent 3066887ff4
commit 8bc6299d7b
5 changed files with 195 additions and 49 deletions

View File

@ -75,7 +75,10 @@ ConfigureDialog::ConfigureDialog(QWidget* parent, HotkeyRegistry& registry_, Cor
&ConfigureHotkeys::OnInputKeysChanged);
connect(hotkeys_tab.get(), &ConfigureHotkeys::HotkeysChanged, input_tab.get(),
&ConfigureInput::OnHotkeysChanged);
connect(input_tab.get(), &ConfigureInput::ClearHotkey, hotkeys_tab.get(),
&ConfigureHotkeys::OnClearBinding);
connect(hotkeys_tab.get(), &ConfigureHotkeys::ClearInputBinding, input_tab.get(),
&ConfigureInput::OnClearBinding);
// Synchronise lists upon initialisation
input_tab->EmitInputKeysChanged();
hotkeys_tab->EmitHotkeysChanged();

View File

@ -43,13 +43,19 @@ void ConfigureHotkeys::EmitHotkeysChanged() {
emit HotkeysChanged(GetUsedKeyList());
}
QList<QKeySequence> ConfigureHotkeys::GetUsedKeyList() const {
QList<QKeySequence> list;
QMap<QKeySequence, ConfigureInput::InputBinding> ConfigureHotkeys::GetUsedKeyList() const {
QMap<QKeySequence, ConfigureInput::InputBinding> list;
for (int r = 0; r < model->rowCount(); r++) {
QStandardItem* parent = model->item(r, 0);
for (int r2 = 0; r2 < parent->rowCount(); r2++) {
QStandardItem* keyseq = parent->child(r2, 1);
list << QKeySequence::fromString(keyseq->text(), QKeySequence::NativeText);
auto seq = QKeySequence::fromString(keyseq->text(), QKeySequence::NativeText);
if (seq.count() == 1 &&
seq[0].keyboardModifiers() == Qt::KeyboardModifier::NoModifier) {
auto binding = ConfigureInput::InputBinding(
"Hotkey", parent->child(r2, 0)->text().toStdString(), r2);
list[seq] = binding;
}
}
}
return list;
@ -75,7 +81,8 @@ void ConfigureHotkeys::Populate(const HotkeyRegistry& registry) {
ui->hotkey_list->expandAll();
}
void ConfigureHotkeys::OnInputKeysChanged(QList<QKeySequence> new_key_list) {
void ConfigureHotkeys::OnInputKeysChanged(
QMap<QKeySequence, ConfigureInput::InputBinding> new_key_list) {
input_keys_list = new_key_list;
}
@ -96,7 +103,7 @@ void ConfigureHotkeys::Configure(QModelIndex index) {
if (return_code == QDialog::Rejected || key_sequence.isEmpty()) {
return;
}
const auto [key_sequence_used, used_action] = IsUsedKey(key_sequence);
const auto [key_sequence_used, current_binding] = IsUsedKey(key_sequence);
// Check for turbo/per-game speed conflict. Needed to prevent the user from binding both hotkeys
// to the same action. Which cuases problems resetting the frame limit.to the inititla value.
@ -131,22 +138,43 @@ void ConfigureHotkeys::Configure(QModelIndex index) {
}
if (key_sequence_used && key_sequence != QKeySequence(previous_key.toString())) {
QMessageBox::warning(
this, tr("Conflicting Key Sequence"),
tr("The entered key sequence is already assigned to: %1").arg(used_action));
} else {
model->setData(index, key_sequence.toString(QKeySequence::NativeText));
EmitHotkeysChanged();
auto response =
QMessageBox::question(this, QStringLiteral("Duplicate mapping"),
QString::fromStdString("This will clear the mapping for " +
current_binding.name + ". Proceed?"),
QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);
if (response == QMessageBox::No) {
return;
} else {
if (current_binding.binding_type == "Hotkey") {
model->setData(index.sibling(current_binding.index, hotkey_column),
QStringLiteral(""));
} else {
emit ClearInputBinding(current_binding);
}
}
}
model->setData(index, key_sequence.toString(QKeySequence::NativeText));
EmitHotkeysChanged();
}
std::pair<bool, QString> ConfigureHotkeys::IsUsedKey(QKeySequence key_sequence) const {
void ConfigureHotkeys::OnClearBinding(ConfigureInput::InputBinding hotkey_to_clear) {
const auto index = ui->hotkey_list->currentIndex();
if (hotkey_to_clear.binding_type == "Hotkey") {
model->setData(index.sibling(hotkey_to_clear.index, hotkey_column), QStringLiteral(""));
}
EmitHotkeysChanged();
}
std::pair<bool, ConfigureInput::InputBinding> ConfigureHotkeys::IsUsedKey(
QKeySequence key_sequence) const {
if (key_sequence == QKeySequence::fromString(QStringLiteral(""), QKeySequence::NativeText)) {
return std::make_pair(false, QString());
return std::make_pair(false, ConfigureInput::InputBinding{"", "", -1});
}
if (input_keys_list.contains(key_sequence)) {
return std::make_pair(true, tr("A 3ds button"));
return std::make_pair(true, input_keys_list[key_sequence]);
}
for (int r = 0; r < model->rowCount(); ++r) {
@ -158,12 +186,14 @@ std::pair<bool, QString> ConfigureHotkeys::IsUsedKey(QKeySequence key_sequence)
const auto key_seq = QKeySequence::fromString(key_seq_str, QKeySequence::NativeText);
if (key_sequence == key_seq) {
return std::make_pair(true, parent->child(r2, 0)->text());
return std::make_pair(
true, ConfigureInput::InputBinding{
"Hotkey", parent->child(r2, 0)->text().toStdString(), r2});
}
}
}
return std::make_pair(false, QString());
return std::make_pair(false, ConfigureInput::InputBinding{"", "", -1});
}
void ConfigureHotkeys::ApplyConfiguration(HotkeyRegistry& registry) {
@ -197,6 +227,7 @@ void ConfigureHotkeys::RestoreDefaults() {
->setText(QtConfig::default_hotkeys[r2].shortcut.keyseq);
}
}
EmitHotkeysChanged();
}
void ConfigureHotkeys::ClearAll() {
@ -207,6 +238,7 @@ void ConfigureHotkeys::ClearAll() {
model->item(r, 0)->child(r2, hotkey_column)->setText(QString{});
}
}
EmitHotkeysChanged();
}
void ConfigureHotkeys::PopupContextMenu(const QPoint& menu_location) {
@ -234,12 +266,13 @@ void ConfigureHotkeys::RestoreHotkey(QModelIndex index) {
const auto [key_sequence_used, used_action] = IsUsedKey(default_key_sequence);
if (key_sequence_used && default_key_sequence != QKeySequence(model->data(index).toString())) {
QMessageBox::warning(
this, tr("Conflicting Key Sequence"),
tr("The default key sequence is already assigned to: %1").arg(used_action));
QMessageBox::warning(this, tr("Conflicting Key Sequence"),
tr("The default key sequence is already assigned to: %1")
.arg(QString::fromStdString(used_action.name)));
} else {
model->setData(index, default_key_sequence.toString(QKeySequence::NativeText));
}
EmitHotkeysChanged();
}
void ConfigureHotkeys::RetranslateUI() {

View File

@ -6,6 +6,7 @@
#include <memory>
#include <QWidget>
#include "citra_qt/configuration/configure_input.h"
namespace Ui {
class ConfigureHotkeys;
@ -34,15 +35,16 @@ public:
void Populate(const HotkeyRegistry& registry);
public slots:
void OnInputKeysChanged(QList<QKeySequence> new_key_list);
void OnInputKeysChanged(QMap<QKeySequence, ConfigureInput::InputBinding> new_key_list);
void OnClearBinding(ConfigureInput::InputBinding hotkey_to_clear);
signals:
void HotkeysChanged(QList<QKeySequence> new_key_list);
void HotkeysChanged(QMap<QKeySequence, ConfigureInput::InputBinding> new_key_list);
void ClearInputBinding(ConfigureInput::InputBinding binding);
private:
void Configure(QModelIndex index);
std::pair<bool, QString> IsUsedKey(QKeySequence key_sequence) const;
QList<QKeySequence> GetUsedKeyList() const;
std::pair<bool, ConfigureInput::InputBinding> IsUsedKey(QKeySequence key_sequence) const;
QMap<QKeySequence, ConfigureInput::InputBinding> GetUsedKeyList() const;
void RestoreDefaults();
void ClearAll();
@ -54,7 +56,7 @@ private:
* These can't be bound to any hotkey.
* Synchronised with ConfigureInput via signal-slot.
*/
QList<QKeySequence> input_keys_list;
QMap<QKeySequence, ConfigureInput::InputBinding> input_keys_list;
std::unique_ptr<Ui::ConfigureHotkeys> ui;

View File

@ -174,6 +174,11 @@ ConfigureInput::ConfigureInput(Core::System& _system, QWidget* parent)
ui->buttonHome, ui->buttonPower,
};
button_names = {"A Button", "B Button", "X Button", "Y Button", "D-Pad Up",
"D-Pad Down", "D-Pad Left", "D-Pad Right", "L Button", "R Button",
"Start Button", "Select Button", "Debug Button", "GPIO4 Button", "ZL Button",
"ZR Button", "Home Button", "Power Button"};
analog_map_buttons = {{
{
ui->buttonCircleUp,
@ -198,7 +203,7 @@ ConfigureInput::ConfigureInput(Core::System& _system, QWidget* parent)
nullptr,
},
}};
analog_names = {"Circle Pad", "C Stick"};
analog_map_stick = {ui->buttonCircleAnalog, ui->buttonCStickAnalog};
analog_map_deadzone_and_modifier_slider = {ui->sliderCirclePadDeadzoneAndModifier,
ui->sliderCStickDeadzoneAndModifier};
@ -439,16 +444,72 @@ void ConfigureInput::EmitInputKeysChanged() {
emit InputKeysChanged(GetUsedKeyboardKeys());
}
void ConfigureInput::OnHotkeysChanged(QList<QKeySequence> new_key_list) {
void ConfigureInput::OnHotkeysChanged(
QMap<QKeySequence, ConfigureInput::InputBinding> new_key_list) {
hotkey_list = new_key_list;
}
QList<QKeySequence> ConfigureInput::GetUsedKeyboardKeys() {
QList<QKeySequence> list;
void ConfigureInput::OnClearBinding(ConfigureInput::InputBinding binding) {
ClearBinding(binding);
}
bool sameInput(const Common::ParamPackage& param1, const Common::ParamPackage& param2) {
return param1.Has("engine") && param2.Has("engine") &&
param1.Get("engine", "") == param2.Get("engine", "") &&
param1.Get("guid", "") == param2.Get("guid", "") &&
(param1.Get("code", -1) == param2.Get("code", -2) ||
param1.Get("button", -1) == param2.Get("button", -2) ||
(param1.Get("axis", -1) == param2.Get("axis", -2) &&
param1.Get("direction", "a") == param2.Get("direction", "b")) ||
(param1.Get("axis_x", -1) == param2.Get("axis_x", -2) &&
param1.Get("axis_y", -1) == param2.Get("axis_y", -2)) ||
(param1.Get("hat", -1) == param2.Get("hat", -2) &&
param1.Get("direction", "a") == param2.Get("direction", "b")));
}
ConfigureInput::InputBinding ConfigureInput::GetMapping(const Common::ParamPackage& param) {
if (!param.Has("engine"))
return {"", "", 0};
// check for a button map
for (int button = 0; button < Settings::NativeButton::NumButtons; ++button) {
const auto& button_param = buttons_param[button];
if (sameInput(param, button_param)) {
return {"NativeButton", button_names[button], button};
}
}
// check for an analog map
for (int analog_id = 0; analog_id < Settings::NativeAnalog::NumAnalogs; ++analog_id) {
const auto& analog_param = analogs_param[analog_id];
if (analog_param.Get("engine", "") == "analog_from_button") {
for (int sub_button_id = 0; sub_button_id < ANALOG_SUB_BUTTONS_NUM; ++sub_button_id) {
const Common::ParamPackage sub_button{
analog_param.Get(analog_sub_buttons[sub_button_id], "")};
if (sameInput(param, sub_button)) {
return {"AnalogButton",
analog_names[analog_id] + " " + analog_sub_buttons[sub_button_id],
analog_id, sub_button_id};
}
}
} else if (sameInput(param, analog_param)) {
return {"Analog", analog_names[analog_id], analog_id};
}
}
if (param.Get("engine", "") == "keyboard" &&
hotkey_list.contains(QKeySequence(param.Get("code", 0)))) {
return hotkey_list[QKeySequence(param.Get("code", 0))];
}
return {"", "", 0};
}
QMap<QKeySequence, ConfigureInput::InputBinding> ConfigureInput::GetUsedKeyboardKeys() {
QMap<QKeySequence, ConfigureInput::InputBinding> list;
for (int button = 0; button < Settings::NativeButton::NumButtons; button++) {
const auto& button_param = buttons_param[button];
if (button_param.Get("engine", "") == "keyboard") {
list << QKeySequence(button_param.Get("code", 0));
list[QKeySequence(button_param.Get("code", 0))] =
ConfigureInput::InputBinding{"NativeButton", button_names[button], button};
}
}
@ -458,7 +519,10 @@ QList<QKeySequence> ConfigureInput::GetUsedKeyboardKeys() {
for (int sub_button_id = 0; sub_button_id < ANALOG_SUB_BUTTONS_NUM; ++sub_button_id) {
const Common::ParamPackage sub_button{
analog_param.Get(analog_sub_buttons[sub_button_id], "")};
list << QKeySequence(sub_button.Get("code", 0));
list[QKeySequence(sub_button.Get("code", 0))] = ConfigureInput::InputBinding{
"AnalogButton",
analog_names[analog_id] + " " + analog_sub_buttons[sub_button_id], analog_id,
sub_button_id};
}
}
}
@ -498,6 +562,25 @@ void ConfigureInput::RestoreDefaults() {
Settings::SaveProfile(Settings::values.current_input_profile_index);
}
void ConfigureInput::ClearBinding(InputBinding binding) {
if (binding.binding_type == "NativeButton") {
buttons_param[binding.index].Clear();
button_map[binding.index]->setText(tr("[not set]"));
} else if (binding.binding_type == "AnalogButton") {
const auto analog_id = binding.index;
const auto sub_button_id = binding.sub_index;
analogs_param[analog_id].Erase(analog_sub_buttons[sub_button_id]);
analog_map_buttons[analog_id][sub_button_id]->setText(tr("[not set]"));
} else if (binding.binding_type == "Analog") {
const auto analog_id = binding.index;
analogs_param[analog_id].Clear();
UpdateButtonLabels();
} else if (binding.binding_type == "Hotkey") {
emit ClearHotkey(binding);
}
ApplyConfiguration();
}
void ConfigureInput::ClearAll() {
for (int button_id = 0; button_id < Settings::NativeButton::NumButtons; button_id++) {
if (button_map[button_id] && button_map[button_id]->isEnabled())
@ -628,7 +711,7 @@ void ConfigureInput::HandleClick(QPushButton* button,
poll_timer->start(200); // Check for new inputs every 200ms
}
void ConfigureInput::SetPollingResult(const Common::ParamPackage& params, bool abort) {
void ConfigureInput::StopPolling() {
releaseKeyboard();
releaseMouse();
timeout_timer->stop();
@ -636,7 +719,24 @@ void ConfigureInput::SetPollingResult(const Common::ParamPackage& params, bool a
for (auto& poller : device_pollers) {
poller->Stop();
}
}
void ConfigureInput::SetPollingResult(const Common::ParamPackage& params, bool abort) {
auto const currentBinding = GetMapping(params);
StopPolling();
if (!abort && currentBinding.binding_type != "") {
auto response =
QMessageBox::question(this, QStringLiteral("Duplicate mapping"),
QString::fromStdString("This will clear the mapping for " +
currentBinding.name + ". Proceed?"),
QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);
if (response == QMessageBox::No) {
abort = true;
return;
} else {
ClearBinding(currentBinding);
}
}
if (!abort && input_setter) {
(*input_setter)(params);
}
@ -651,16 +751,9 @@ void ConfigureInput::keyPressEvent(QKeyEvent* event) {
if (event->key() != Qt::Key_Escape && event->key() != previous_key_code) {
if (want_keyboard_keys) {
// Check if key is already bound
if (hotkey_list.contains(QKeySequence(event->key())) ||
GetUsedKeyboardKeys().contains(QKeySequence(event->key()))) {
SetPollingResult({}, true);
QMessageBox::critical(this, tr("Error!"),
tr("You're using a key that's already bound."));
return;
}
SetPollingResult(Common::ParamPackage{InputCommon::GenerateKeyboardParam(event->key())},
false);
auto param = Common::ParamPackage(InputCommon::GenerateKeyboardParam(event->key()));
previous_key_code = 0;
SetPollingResult(param, false);
} else {
// Escape key wasn't pressed and we don't want any keyboard keys, so don't stop
// polling

View File

@ -30,6 +30,12 @@ class ConfigureInput : public QWidget {
Q_OBJECT
public:
struct InputBinding {
std::string binding_type;
std::string name;
int index;
int sub_index = -1; // used for sub-buttons
};
explicit ConfigureInput(Core::System& system, QWidget* parent = nullptr);
~ConfigureInput() override;
@ -44,10 +50,12 @@ public:
/// Save the current input profile index
void ApplyProfile();
public slots:
void OnHotkeysChanged(QList<QKeySequence> new_key_list);
void OnHotkeysChanged(QMap<QKeySequence, ConfigureInput::InputBinding> new_key_list);
void OnClearBinding(ConfigureInput::InputBinding binding);
signals:
void InputKeysChanged(QList<QKeySequence> new_key_list);
void InputKeysChanged(QMap<QKeySequence, ConfigureInput::InputBinding> new_key_list);
void ClearHotkey(ConfigureInput::InputBinding hotkey_to_clear);
private:
Core::System& system;
@ -66,6 +74,8 @@ private:
/// Each button input is represented by a QPushButton.
std::array<QPushButton*, Settings::NativeButton::NumButtons> button_map;
std::array<std::string, Settings::NativeButton::NumButtons> button_names;
std::array<std::string, Settings::NativeAnalog::NumAnalogs> analog_names;
/// A group of five QPushButtons represent one analog input. The buttons each represent up,
/// down, left, right, and modifier, respectively.
@ -89,14 +99,17 @@ private:
* These can't be bound to any input key.
* Synchronised with ConfigureHotkeys via signal-slot.
*/
QList<QKeySequence> hotkey_list;
QMap<QKeySequence, ConfigureInput::InputBinding> hotkey_list;
/// A flag to indicate if keyboard keys are okay when configuring an input. If this is false,
/// keyboard events are ignored.
bool want_keyboard_keys = false;
/// Generates list of all used keys
QList<QKeySequence> GetUsedKeyboardKeys();
/// Generates list of all used keyboard keys for sharing with hotkey code
QMap<QKeySequence, ConfigureInput::InputBinding> GetUsedKeyboardKeys();
InputBinding GetMapping(const Common::ParamPackage& param);
void ClearBinding(InputBinding binding);
void MapFromButton(const Common::ParamPackage& params);
void AutoMap();
@ -117,7 +130,9 @@ private:
/// The key code of the previous state of the key being currently bound.
int previous_key_code;
/// Finish polling and configure input using the input_setter
/// Stop polling
void StopPolling();
/// configure input using the input_setter
void SetPollingResult(const Common::ParamPackage& params, bool abort);
/// Handle key press events.