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;
|
2026-02-16 11:11:15 +01:00
|
|
|
requestUpdate();
|
2026-02-05 14:37:17 +03:00
|
|
|
}
|
|
|
|
|
|
2026-02-16 11:11:15 +01:00
|
|
|
void ButtonRemapActivity::onExit() { Activity::onExit(); }
|
2026-02-05 14:37:17 +03:00
|
|
|
|
|
|
|
|
void ButtonRemapActivity::loop() {
|
2026-02-16 11:11:15 +01:00
|
|
|
// 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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-16 11:11:15 +01:00
|
|
|
{
|
|
|
|
|
// 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
|
|
|
|
2026-02-16 11:11:15 +01: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
|
|
|
}
|
|
|
|
|
|
2026-02-16 11:11:15 +01: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
|
|
|
}
|
|
|
|
|
|
2026-02-16 11:11:15 +01:00
|
|
|
requestUpdate();
|
2026-02-05 14:37:17 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-16 11:11:15 +01: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";
|
|
|
|
|
}
|
|
|
|
|
}
|