Files
crosspoint-reader/src/activities/settings/ButtonRemapActivity.cpp
T

200 lines
6.7 KiB
C++
Raw Normal View History

2026-02-05 14:37:17 +03:00
#include "ButtonRemapActivity.h"
#include <GfxRenderer.h>
2026-02-16 15:28:42 +02:00
#include <I18n.h>
2026-02-05 14:37:17 +03:00
#include "CrossPointSettings.h"
#include "MappedInputManager.h"
#include "components/UITheme.h"
#include "fontIds.h"
namespace {
// UI steps correspond to logical roles in order: Back, Confirm, Left, Right.
constexpr uint8_t kRoleCount = 4;
// Marker used when a role has not been assigned yet.
constexpr uint8_t kUnassigned = 0xFF;
// Duration to show temporary error text when reassigning a button.
constexpr unsigned long kErrorDisplayMs = 1500;
} // namespace
void ButtonRemapActivity::onEnter() {
Activity::onEnter();
// Start with all roles unassigned to avoid duplicate blocking.
currentStep = 0;
tempMapping[0] = kUnassigned;
tempMapping[1] = kUnassigned;
tempMapping[2] = kUnassigned;
tempMapping[3] = kUnassigned;
errorMessage.clear();
errorUntil = 0;
requestUpdate();
2026-02-05 14:37:17 +03:00
}
void ButtonRemapActivity::onExit() { Activity::onExit(); }
2026-02-05 14:37:17 +03:00
void ButtonRemapActivity::loop() {
// Clear any temporary warning after its timeout.
if (errorUntil > 0 && millis() > errorUntil) {
errorMessage.clear();
errorUntil = 0;
requestUpdate();
return;
}
2026-02-05 14:37:17 +03:00
// Side buttons:
// - Up: reset mapping to defaults and exit.
// - Down: cancel without saving.
if (mappedInput.wasPressed(MappedInputManager::Button::Up)) {
// Persist default mapping immediately so the user can recover quickly.
SETTINGS.frontButtonBack = CrossPointSettings::FRONT_HW_BACK;
SETTINGS.frontButtonConfirm = CrossPointSettings::FRONT_HW_CONFIRM;
SETTINGS.frontButtonLeft = CrossPointSettings::FRONT_HW_LEFT;
SETTINGS.frontButtonRight = CrossPointSettings::FRONT_HW_RIGHT;
SETTINGS.saveToFile();
onBack();
return;
}
if (mappedInput.wasPressed(MappedInputManager::Button::Down)) {
// Exit without changing settings.
onBack();
return;
}
{
// Wait for the UI to refresh before accepting another assignment.
// This avoids rapid double-presses that can advance the step without a visible redraw.
requestUpdateAndWait();
2026-02-05 14:37:17 +03:00
// Wait for a front button press to assign to the current role.
const int pressedButton = mappedInput.getPressedFrontButton();
if (pressedButton < 0) {
return;
2026-02-05 14:37:17 +03:00
}
// Update temporary mapping and advance the remap step.
// Only accept the press if this hardware button isn't already assigned elsewhere.
if (!validateUnassigned(static_cast<uint8_t>(pressedButton))) {
requestUpdate();
return;
}
tempMapping[currentStep] = static_cast<uint8_t>(pressedButton);
currentStep++;
if (currentStep >= kRoleCount) {
// All roles assigned; save to settings and exit.
applyTempMapping();
SETTINGS.saveToFile();
onBack();
return;
2026-02-05 14:37:17 +03:00
}
requestUpdate();
2026-02-05 14:37:17 +03:00
}
}
void ButtonRemapActivity::render(Activity::RenderLock&&) {
2026-02-05 14:37:17 +03:00
const auto labelForHardware = [&](uint8_t hardwareIndex) -> const char* {
for (uint8_t i = 0; i < kRoleCount; i++) {
if (tempMapping[i] == hardwareIndex) {
return getRoleName(i);
}
}
return "-";
};
2026-02-19 17:16:55 +07:00
auto metrics = UITheme::getInstance().getMetrics();
const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight();
2026-02-05 14:37:17 +03:00
2026-02-19 17:16:55 +07:00
renderer.clearScreen();
2026-02-05 14:37:17 +03:00
2026-02-19 17:16:55 +07:00
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_REMAP_FRONT_BUTTONS));
GUI.drawSubHeader(renderer, Rect{0, metrics.topPadding + metrics.headerHeight, pageWidth, metrics.tabBarHeight},
tr(STR_REMAP_PROMPT));
2026-02-05 14:37:17 +03:00
2026-02-19 17:16:55 +07:00
int topOffset = metrics.topPadding + metrics.headerHeight + metrics.tabBarHeight + metrics.verticalSpacing;
int contentHeight = pageHeight - topOffset - metrics.buttonHintsHeight - metrics.verticalSpacing;
GUI.drawList(
renderer, Rect{0, topOffset, pageWidth, contentHeight}, kRoleCount, currentStep,
[&](int index) { return getRoleName(static_cast<uint8_t>(index)); }, nullptr, nullptr,
[&](int index) {
uint8_t assignedButton = tempMapping[static_cast<uint8_t>(index)];
return (assignedButton == kUnassigned) ? tr(STR_UNASSIGNED) : getHardwareName(assignedButton);
},
true);
2026-02-05 14:37:17 +03:00
// Temporary warning banner for duplicates.
if (!errorMessage.empty()) {
2026-02-19 17:16:55 +07:00
GUI.drawHelpText(renderer,
Rect{0, pageHeight - metrics.buttonHintsHeight - metrics.contentSidePadding - 15, pageWidth, 20},
errorMessage.c_str());
2026-02-05 14:37:17 +03:00
}
// Provide side button actions at the bottom of the screen (split across two lines).
2026-02-19 17:16:55 +07:00
GUI.drawHelpText(renderer,
Rect{0, topOffset + 4 * metrics.listRowHeight + 4 * metrics.verticalSpacing, pageWidth, 20},
tr(STR_REMAP_RESET_HINT));
GUI.drawHelpText(renderer,
Rect{0, topOffset + 4 * metrics.listRowHeight + 5 * metrics.verticalSpacing + 20, pageWidth, 20},
tr(STR_REMAP_CANCEL_HINT));
2026-02-05 14:37:17 +03:00
// Live preview of logical labels under front buttons.
// This mirrors the on-device front button order: Back, Confirm, Left, Right.
GUI.drawButtonHints(renderer, labelForHardware(CrossPointSettings::FRONT_HW_BACK),
labelForHardware(CrossPointSettings::FRONT_HW_CONFIRM),
labelForHardware(CrossPointSettings::FRONT_HW_LEFT),
labelForHardware(CrossPointSettings::FRONT_HW_RIGHT));
renderer.displayBuffer();
}
void ButtonRemapActivity::applyTempMapping() {
// Commit temporary mapping into settings (logical role -> hardware).
SETTINGS.frontButtonBack = tempMapping[0];
SETTINGS.frontButtonConfirm = tempMapping[1];
SETTINGS.frontButtonLeft = tempMapping[2];
SETTINGS.frontButtonRight = tempMapping[3];
}
bool ButtonRemapActivity::validateUnassigned(const uint8_t pressedButton) {
// Block reusing a hardware button already assigned to another role.
for (uint8_t i = 0; i < kRoleCount; i++) {
if (tempMapping[i] == pressedButton && i != currentStep) {
2026-02-16 15:28:42 +02:00
errorMessage = tr(STR_ALREADY_ASSIGNED);
2026-02-05 14:37:17 +03:00
errorUntil = millis() + kErrorDisplayMs;
return false;
}
}
return true;
}
const char* ButtonRemapActivity::getRoleName(const uint8_t roleIndex) const {
switch (roleIndex) {
case 0:
2026-02-16 15:28:42 +02:00
return tr(STR_BACK);
2026-02-05 14:37:17 +03:00
case 1:
2026-02-16 15:28:42 +02:00
return tr(STR_CONFIRM);
2026-02-05 14:37:17 +03:00
case 2:
2026-02-16 15:28:42 +02:00
return tr(STR_DIR_LEFT);
2026-02-05 14:37:17 +03:00
case 3:
default:
2026-02-16 15:28:42 +02:00
return tr(STR_DIR_RIGHT);
2026-02-05 14:37:17 +03:00
}
}
const char* ButtonRemapActivity::getHardwareName(const uint8_t buttonIndex) const {
switch (buttonIndex) {
case CrossPointSettings::FRONT_HW_BACK:
2026-02-16 15:28:42 +02:00
return tr(STR_HW_BACK_LABEL);
2026-02-05 14:37:17 +03:00
case CrossPointSettings::FRONT_HW_CONFIRM:
2026-02-16 15:28:42 +02:00
return tr(STR_HW_CONFIRM_LABEL);
2026-02-05 14:37:17 +03:00
case CrossPointSettings::FRONT_HW_LEFT:
2026-02-16 15:28:42 +02:00
return tr(STR_HW_LEFT_LABEL);
2026-02-05 14:37:17 +03:00
case CrossPointSettings::FRONT_HW_RIGHT:
2026-02-16 15:28:42 +02:00
return tr(STR_HW_RIGHT_LABEL);
2026-02-05 14:37:17 +03:00
default:
return "Unknown";
}
}