configuration dialog for controller hotkeys

This commit is contained in:
David Griswold 2026-02-06 21:12:27 +03:00
parent f354fd8c27
commit 43ead51b50
8 changed files with 173 additions and 13 deletions

View File

@ -189,6 +189,8 @@ add_library(citra_qt STATIC EXCLUDE_FROM_ALL
util/graphics_device_info.h
util/sequence_dialog/sequence_dialog.cpp
util/sequence_dialog/sequence_dialog.h
util/sequence_dialog/controller_sequence_dialog.cpp
util/sequence_dialog/controller_sequence_dialog.h
util/spinbox.cpp
util/spinbox.h
util/util.cpp

View File

@ -8,7 +8,7 @@
#include "citra_qt/configuration/config.h"
#include "citra_qt/configuration/configure_hotkeys_controller.h"
#include "citra_qt/hotkeys.h"
#include "citra_qt/util/sequence_dialog/sequence_dialog.h"
#include "citra_qt/util/sequence_dialog/controller_sequence_dialog.h"
#include "ui_configure_hotkeys_controller.h"
constexpr int name_column = 0;
@ -24,7 +24,7 @@ ConfigureControllerHotkeys::ConfigureControllerHotkeys(QWidget* parent)
model->setColumnCount(2);
model->setHorizontalHeaderLabels({tr("Action"), tr("Controller Hotkey")});
// TODO: re-enable and get profiles workin
ui->horizontalLayout_5->setEnabled(false);
ui->profileGroup->setEnabled(false);
connect(ui->hotkey_list, &QTreeView::doubleClicked, this,
&ConfigureControllerHotkeys::Configure);
connect(ui->hotkey_list, &QTreeView::customContextMenuRequested, this,
@ -60,7 +60,27 @@ void ConfigureControllerHotkeys::Populate(const HotkeyRegistry& registry) {
ui->hotkey_list->expandAll();
}
void ConfigureControllerHotkeys::Configure(QModelIndex index) {}
void ConfigureControllerHotkeys::Configure(QModelIndex index) {
if (!index.parent().isValid()) {
return;
}
// Swap to the hotkey column
index = index.sibling(index.row(), hotkey_column);
QModelIndex readableIndex = index.sibling(index.row(), readable_hotkey_column);
const auto previous_key = model->data(index);
ControllerSequenceDialog hotkey_dialog{this};
const int return_code = hotkey_dialog.exec();
const auto key_sequence = hotkey_dialog.GetSequence();
if (return_code == QDialog::Rejected || key_sequence.isEmpty()) {
return;
}
model->setData(index, key_sequence);
model->setData(readableIndex, CleanSequence(key_sequence));
}
void ConfigureControllerHotkeys::ApplyConfiguration(HotkeyRegistry& registry) {
for (int key_id = 0; key_id < model->rowCount(); key_id++) {

View File

@ -30,12 +30,12 @@ public:
* @param registry The HotkeyRegistry whose data is used to populate the list.
*/
void Populate(const HotkeyRegistry& registry);
static QString CleanSequence(QString controller_keyseq);
private:
void Configure(QModelIndex index);
void ClearAll();
void PopupContextMenu(const QPoint& menu_location);
QString CleanSequence(QString controller_keyseq);
std::unique_ptr<Ui::ConfigureControllerHotkeys> ui;

View File

@ -15,6 +15,7 @@
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QGroupBox" name="profileGroup">
<layout class="QHBoxLayout" name="horizontalLayout_5">
<item>
<widget class="QLabel" name="label_7">
@ -61,6 +62,7 @@
</widget>
</item>
</layout>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">

View File

@ -407,7 +407,8 @@ ConfigureInput::ConfigureInput(Core::System& _system, QWidget* parent)
Common::ParamPackage params;
for (auto& poller : device_pollers) {
params = poller->GetNextInput();
if (params.Has("engine")) {
// skip button downs and only process button ups to maintain former behavior
if (params.Has("engine") && !params.Has("down")) {
SetPollingResult(params, false);
return;
}

View File

@ -0,0 +1,96 @@
// Copyright 2026 Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include <iostream>
#include <QDialogButtonBox>
#include <QLabel>
#include <QTimer>
#include <QVBoxLayout>
#include "common/param_package.h"
#include "configuration/configure_hotkeys_controller.h"
#include "controller_sequence_dialog.h"
#include "util/sequence_dialog/controller_sequence_dialog.h"
ControllerSequenceDialog::ControllerSequenceDialog(QWidget* parent)
: QDialog(parent), poll_timer(std::make_unique<QTimer>()) {
setWindowTitle(tr("Press then release one or two controller buttons"));
auto* const buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
buttons->setCenterButtons(true);
textBox = new QLabel(QStringLiteral("Waiting..."), this);
auto* const layout = new QVBoxLayout(this);
layout->addWidget(textBox);
layout->addWidget(buttons);
connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
LaunchPollers(); // Fixed: added semicolon
}
ControllerSequenceDialog::~ControllerSequenceDialog() = default;
QString ControllerSequenceDialog::GetSequence() {
return key_sequence;
}
void ControllerSequenceDialog::closeEvent(QCloseEvent*) {
reject();
}
bool ControllerSequenceDialog::focusNextPrevChild(bool next) {
return false;
}
void ControllerSequenceDialog::LaunchPollers() {
device_pollers = InputCommon::Polling::GetPollers(InputCommon::Polling::DeviceType::Button);
for (auto& poller : device_pollers) {
poller->Start();
}
connect(poll_timer.get(), &QTimer::timeout, this, [this, downCount = 0]() mutable {
Common::ParamPackage params;
for (auto& poller : device_pollers) {
params = poller->GetNextInput();
if (params.Has("engine") && params.Has("button")) { // for now, no analog inputs
if (params.Has("down")) {
std::cerr << "button " + params.Get("button", "") + " down" << std::endl;
downCount++;
if (downCount > 2) {
// ignore third and fourth and fifth buttons
} else if (downCount == 1) {
key_sequence = QStringLiteral("");
params1 = params;
params2 = Common::ParamPackage();
textBox->setText(ConfigureControllerHotkeys::CleanSequence(
QString::fromStdString(params1.Serialize())) +
QStringLiteral("..."));
} else if (downCount == 2) {
// this is a second button while the first one is held down,
// go ahead and set the key_sequence as the chord and clear params
params2 = params;
key_sequence = QString::fromStdString(params1.Serialize() + "||" +
params2.Serialize());
textBox->setText(ConfigureControllerHotkeys::CleanSequence(key_sequence));
}
} else { // button release
downCount--;
std::cerr << "button " + params.Get("button", "") + " up" << std::endl;
if (downCount == 0) {
// all released, clear all saved params
params1 = Common::ParamPackage();
params2 = Common::ParamPackage();
}
if (key_sequence.isEmpty()) {
key_sequence = QString::fromStdString(params.Serialize());
textBox->setText(ConfigureControllerHotkeys::CleanSequence(key_sequence));
}
}
}
}
});
poll_timer->start(100);
}

View File

@ -0,0 +1,31 @@
// Copyright 2018 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#pragma once
#include <memory>
#include <QDialog>
#include <QLabel>
#include "common/param_package.h"
#include "input_common/main.h"
class ControllerSequenceDialog : public QDialog {
Q_OBJECT
public:
explicit ControllerSequenceDialog(QWidget* parent = nullptr);
~ControllerSequenceDialog();
QString GetSequence();
void closeEvent(QCloseEvent*) override;
private:
void LaunchPollers();
QLabel* textBox;
QString key_sequence;
Common::ParamPackage params1, params2;
bool focusNextPrevChild(bool next) override;
std::vector<std::unique_ptr<InputCommon::Polling::DevicePoller>> device_pollers;
std::unique_ptr<QTimer> poll_timer;
};

View File

@ -894,12 +894,12 @@ SDLState::~SDLState() {
}
}
Common::ParamPackage SDLEventToButtonParamPackage(SDLState& state, const SDL_Event& event) {
Common::ParamPackage SDLEventToButtonParamPackage(SDLState& state, const SDL_Event& event,
const bool down = false) {
Common::ParamPackage params({{"engine", "sdl"}});
auto joystick = state.GetSDLJoystickBySDLID(event.jhat.which);
switch (event.type) {
case SDL_JOYAXISMOTION: {
auto joystick = state.GetSDLJoystickBySDLID(event.jaxis.which);
params.Set("port", joystick->GetPort());
params.Set("guid", joystick->GetGUID());
params.Set("axis", event.jaxis.axis);
@ -912,15 +912,14 @@ Common::ParamPackage SDLEventToButtonParamPackage(SDLState& state, const SDL_Eve
}
break;
}
case SDL_JOYBUTTONUP: {
auto joystick = state.GetSDLJoystickBySDLID(event.jbutton.which);
case SDL_JOYBUTTONUP:
case SDL_JOYBUTTONDOWN: {
params.Set("port", joystick->GetPort());
params.Set("guid", joystick->GetGUID());
params.Set("button", event.jbutton.button);
break;
}
case SDL_JOYHATMOTION: {
auto joystick = state.GetSDLJoystickBySDLID(event.jhat.which);
params.Set("port", joystick->GetPort());
params.Set("guid", joystick->GetGUID());
params.Set("hat", event.jhat.hat);
@ -943,6 +942,8 @@ Common::ParamPackage SDLEventToButtonParamPackage(SDLState& state, const SDL_Eve
break;
}
}
if (down)
params.Set("down", 1);
return params;
}
@ -971,6 +972,7 @@ public:
Common::ParamPackage GetNextInput() override {
SDL_Event event;
bool down = false;
while (state.event_queue.Pop(event)) {
switch (event.type) {
case SDL_JOYAXISMOTION:
@ -978,7 +980,11 @@ public:
!axis_memory[event.jaxis.which].count(event.jaxis.axis)) {
axis_memory[event.jaxis.which][event.jaxis.axis] = event.jaxis.value;
axis_event_count[event.jaxis.which][event.jaxis.axis] = 1;
break;
if (IsAxisAtPole(event.jaxis.value)) {
down = true;
} else {
break;
}
} else {
axis_event_count[event.jaxis.which][event.jaxis.axis]++;
// The joystick and axis exist in our map if we take this branch, so no checks
@ -1010,9 +1016,11 @@ public:
axis_event_count.clear();
}
}
case SDL_JOYBUTTONDOWN:
down = true;
case SDL_JOYBUTTONUP:
case SDL_JOYHATMOTION:
return SDLEventToButtonParamPackage(state, event);
return SDLEventToButtonParamPackage(state, event, down);
}
}
return {};