mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2026-02-05 15:17:37 +03:00
feat: front button remapper (#664)
## Summary
* Custom remapper to create any variant of front button layout.
## Additional Context
* Included migration from previous frontlayout setting
* This will solve:
* https://github.com/crosspoint-reader/crosspoint-reader/issues/654
* https://github.com/crosspoint-reader/crosspoint-reader/issues/652
* https://github.com/crosspoint-reader/crosspoint-reader/issues/620
* https://github.com/crosspoint-reader/crosspoint-reader/issues/468
<img width="860" height="1147" alt="image"
src="https://github.com/user-attachments/assets/457356ed-7a7d-4e1c-8683-e187a1df47c0"
/>
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**< PARTIALLY >**_
This commit is contained in:
parent
bf87a7dc60
commit
c49a819939
@ -22,8 +22,59 @@ void readAndValidate(FsFile& file, uint8_t& member, const uint8_t maxValue) {
|
||||
namespace {
|
||||
constexpr uint8_t SETTINGS_FILE_VERSION = 1;
|
||||
// Increment this when adding new persisted settings fields
|
||||
constexpr uint8_t SETTINGS_COUNT = 24;
|
||||
constexpr uint8_t SETTINGS_COUNT = 28;
|
||||
constexpr char SETTINGS_FILE[] = "/.crosspoint/settings.bin";
|
||||
|
||||
// Validate front button mapping to ensure each hardware button is unique.
|
||||
// If duplicates are detected, reset to the default physical order to prevent invalid mappings.
|
||||
void validateFrontButtonMapping(CrossPointSettings& settings) {
|
||||
// Snapshot the logical->hardware mapping so we can compare for duplicates.
|
||||
const uint8_t mapping[] = {settings.frontButtonBack, settings.frontButtonConfirm, settings.frontButtonLeft,
|
||||
settings.frontButtonRight};
|
||||
for (size_t i = 0; i < 4; i++) {
|
||||
for (size_t j = i + 1; j < 4; j++) {
|
||||
if (mapping[i] == mapping[j]) {
|
||||
// Duplicate detected: restore the default physical order (Back, Confirm, Left, Right).
|
||||
settings.frontButtonBack = CrossPointSettings::FRONT_HW_BACK;
|
||||
settings.frontButtonConfirm = CrossPointSettings::FRONT_HW_CONFIRM;
|
||||
settings.frontButtonLeft = CrossPointSettings::FRONT_HW_LEFT;
|
||||
settings.frontButtonRight = CrossPointSettings::FRONT_HW_RIGHT;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convert legacy front button layout into explicit logical->hardware mapping.
|
||||
void applyLegacyFrontButtonLayout(CrossPointSettings& settings) {
|
||||
switch (static_cast<CrossPointSettings::FRONT_BUTTON_LAYOUT>(settings.frontButtonLayout)) {
|
||||
case CrossPointSettings::LEFT_RIGHT_BACK_CONFIRM:
|
||||
settings.frontButtonBack = CrossPointSettings::FRONT_HW_LEFT;
|
||||
settings.frontButtonConfirm = CrossPointSettings::FRONT_HW_RIGHT;
|
||||
settings.frontButtonLeft = CrossPointSettings::FRONT_HW_BACK;
|
||||
settings.frontButtonRight = CrossPointSettings::FRONT_HW_CONFIRM;
|
||||
break;
|
||||
case CrossPointSettings::LEFT_BACK_CONFIRM_RIGHT:
|
||||
settings.frontButtonBack = CrossPointSettings::FRONT_HW_CONFIRM;
|
||||
settings.frontButtonConfirm = CrossPointSettings::FRONT_HW_LEFT;
|
||||
settings.frontButtonLeft = CrossPointSettings::FRONT_HW_BACK;
|
||||
settings.frontButtonRight = CrossPointSettings::FRONT_HW_RIGHT;
|
||||
break;
|
||||
case CrossPointSettings::BACK_CONFIRM_RIGHT_LEFT:
|
||||
settings.frontButtonBack = CrossPointSettings::FRONT_HW_BACK;
|
||||
settings.frontButtonConfirm = CrossPointSettings::FRONT_HW_CONFIRM;
|
||||
settings.frontButtonLeft = CrossPointSettings::FRONT_HW_RIGHT;
|
||||
settings.frontButtonRight = CrossPointSettings::FRONT_HW_LEFT;
|
||||
break;
|
||||
case CrossPointSettings::BACK_CONFIRM_LEFT_RIGHT:
|
||||
default:
|
||||
settings.frontButtonBack = CrossPointSettings::FRONT_HW_BACK;
|
||||
settings.frontButtonConfirm = CrossPointSettings::FRONT_HW_CONFIRM;
|
||||
settings.frontButtonLeft = CrossPointSettings::FRONT_HW_LEFT;
|
||||
settings.frontButtonRight = CrossPointSettings::FRONT_HW_RIGHT;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool CrossPointSettings::saveToFile() const {
|
||||
@ -42,7 +93,7 @@ bool CrossPointSettings::saveToFile() const {
|
||||
serialization::writePod(outputFile, shortPwrBtn);
|
||||
serialization::writePod(outputFile, statusBar);
|
||||
serialization::writePod(outputFile, orientation);
|
||||
serialization::writePod(outputFile, frontButtonLayout);
|
||||
serialization::writePod(outputFile, frontButtonLayout); // legacy
|
||||
serialization::writePod(outputFile, sideButtonLayout);
|
||||
serialization::writePod(outputFile, fontFamily);
|
||||
serialization::writePod(outputFile, fontSize);
|
||||
@ -60,8 +111,12 @@ bool CrossPointSettings::saveToFile() const {
|
||||
serialization::writeString(outputFile, std::string(opdsUsername));
|
||||
serialization::writeString(outputFile, std::string(opdsPassword));
|
||||
serialization::writePod(outputFile, sleepScreenCoverFilter);
|
||||
// New fields added at end for backward compatibility
|
||||
serialization::writePod(outputFile, uiTheme);
|
||||
serialization::writePod(outputFile, frontButtonBack);
|
||||
serialization::writePod(outputFile, frontButtonConfirm);
|
||||
serialization::writePod(outputFile, frontButtonLeft);
|
||||
serialization::writePod(outputFile, frontButtonRight);
|
||||
// New fields added at end for backward compatibility
|
||||
outputFile.close();
|
||||
|
||||
Serial.printf("[%lu] [CPS] Settings saved to file\n", millis());
|
||||
@ -87,6 +142,8 @@ bool CrossPointSettings::loadFromFile() {
|
||||
|
||||
// load settings that exist (support older files with fewer fields)
|
||||
uint8_t settingsRead = 0;
|
||||
// Track whether remap fields were present in the settings file.
|
||||
bool frontButtonMappingRead = false;
|
||||
do {
|
||||
readAndValidate(inputFile, sleepScreen, SLEEP_SCREEN_MODE_COUNT);
|
||||
if (++settingsRead >= fileSettingsCount) break;
|
||||
@ -98,7 +155,7 @@ bool CrossPointSettings::loadFromFile() {
|
||||
if (++settingsRead >= fileSettingsCount) break;
|
||||
readAndValidate(inputFile, orientation, ORIENTATION_COUNT);
|
||||
if (++settingsRead >= fileSettingsCount) break;
|
||||
readAndValidate(inputFile, frontButtonLayout, FRONT_BUTTON_LAYOUT_COUNT);
|
||||
readAndValidate(inputFile, frontButtonLayout, FRONT_BUTTON_LAYOUT_COUNT); // legacy
|
||||
if (++settingsRead >= fileSettingsCount) break;
|
||||
readAndValidate(inputFile, sideButtonLayout, SIDE_BUTTON_LAYOUT_COUNT);
|
||||
if (++settingsRead >= fileSettingsCount) break;
|
||||
@ -149,11 +206,25 @@ bool CrossPointSettings::loadFromFile() {
|
||||
if (++settingsRead >= fileSettingsCount) break;
|
||||
readAndValidate(inputFile, sleepScreenCoverFilter, SLEEP_SCREEN_COVER_FILTER_COUNT);
|
||||
if (++settingsRead >= fileSettingsCount) break;
|
||||
// New fields added at end for backward compatibility
|
||||
serialization::readPod(inputFile, uiTheme);
|
||||
if (++settingsRead >= fileSettingsCount) break;
|
||||
readAndValidate(inputFile, frontButtonBack, FRONT_BUTTON_HARDWARE_COUNT);
|
||||
if (++settingsRead >= fileSettingsCount) break;
|
||||
readAndValidate(inputFile, frontButtonConfirm, FRONT_BUTTON_HARDWARE_COUNT);
|
||||
if (++settingsRead >= fileSettingsCount) break;
|
||||
readAndValidate(inputFile, frontButtonLeft, FRONT_BUTTON_HARDWARE_COUNT);
|
||||
if (++settingsRead >= fileSettingsCount) break;
|
||||
readAndValidate(inputFile, frontButtonRight, FRONT_BUTTON_HARDWARE_COUNT);
|
||||
frontButtonMappingRead = true;
|
||||
// New fields added at end for backward compatibility
|
||||
} while (false);
|
||||
|
||||
if (frontButtonMappingRead) {
|
||||
validateFrontButtonMapping(*this);
|
||||
} else {
|
||||
applyLegacyFrontButtonLayout(*this);
|
||||
}
|
||||
|
||||
inputFile.close();
|
||||
Serial.printf("[%lu] [CPS] Settings loaded from file\n", millis());
|
||||
return true;
|
||||
|
||||
@ -42,7 +42,7 @@ class CrossPointSettings {
|
||||
ORIENTATION_COUNT
|
||||
};
|
||||
|
||||
// Front button layout options
|
||||
// Front button layout options (legacy)
|
||||
// Default: Back, Confirm, Left, Right
|
||||
// Swapped: Left, Right, Back, Confirm
|
||||
enum FRONT_BUTTON_LAYOUT {
|
||||
@ -53,6 +53,15 @@ class CrossPointSettings {
|
||||
FRONT_BUTTON_LAYOUT_COUNT
|
||||
};
|
||||
|
||||
// Front button hardware identifiers (for remapping)
|
||||
enum FRONT_BUTTON_HARDWARE {
|
||||
FRONT_HW_BACK = 0,
|
||||
FRONT_HW_CONFIRM = 1,
|
||||
FRONT_HW_LEFT = 2,
|
||||
FRONT_HW_RIGHT = 3,
|
||||
FRONT_BUTTON_HARDWARE_COUNT
|
||||
};
|
||||
|
||||
// Side button layout options
|
||||
// Default: Previous, Next
|
||||
// Swapped: Next, Previous
|
||||
@ -116,9 +125,15 @@ class CrossPointSettings {
|
||||
// EPUB reading orientation settings
|
||||
// 0 = portrait (default), 1 = landscape clockwise, 2 = inverted, 3 = landscape counter-clockwise
|
||||
uint8_t orientation = PORTRAIT;
|
||||
// Button layouts
|
||||
// Button layouts (front layout retained for migration only)
|
||||
uint8_t frontButtonLayout = BACK_CONFIRM_LEFT_RIGHT;
|
||||
uint8_t sideButtonLayout = PREV_NEXT;
|
||||
// Front button remap (logical -> hardware)
|
||||
// Used by MappedInputManager to translate logical buttons into physical front buttons.
|
||||
uint8_t frontButtonBack = FRONT_HW_BACK;
|
||||
uint8_t frontButtonConfirm = FRONT_HW_CONFIRM;
|
||||
uint8_t frontButtonLeft = FRONT_HW_LEFT;
|
||||
uint8_t frontButtonRight = FRONT_HW_RIGHT;
|
||||
// Reader font settings
|
||||
uint8_t fontFamily = BOOKERLY;
|
||||
uint8_t fontSize = MEDIUM;
|
||||
|
||||
@ -5,26 +5,11 @@
|
||||
namespace {
|
||||
using ButtonIndex = uint8_t;
|
||||
|
||||
struct FrontLayoutMap {
|
||||
ButtonIndex back;
|
||||
ButtonIndex confirm;
|
||||
ButtonIndex left;
|
||||
ButtonIndex right;
|
||||
};
|
||||
|
||||
struct SideLayoutMap {
|
||||
ButtonIndex pageBack;
|
||||
ButtonIndex pageForward;
|
||||
};
|
||||
|
||||
// Order matches CrossPointSettings::FRONT_BUTTON_LAYOUT.
|
||||
constexpr FrontLayoutMap kFrontLayouts[] = {
|
||||
{HalGPIO::BTN_BACK, HalGPIO::BTN_CONFIRM, HalGPIO::BTN_LEFT, HalGPIO::BTN_RIGHT},
|
||||
{HalGPIO::BTN_LEFT, HalGPIO::BTN_RIGHT, HalGPIO::BTN_BACK, HalGPIO::BTN_CONFIRM},
|
||||
{HalGPIO::BTN_CONFIRM, HalGPIO::BTN_LEFT, HalGPIO::BTN_BACK, HalGPIO::BTN_RIGHT},
|
||||
{HalGPIO::BTN_BACK, HalGPIO::BTN_CONFIRM, HalGPIO::BTN_RIGHT, HalGPIO::BTN_LEFT},
|
||||
};
|
||||
|
||||
// Order matches CrossPointSettings::SIDE_BUTTON_LAYOUT.
|
||||
constexpr SideLayoutMap kSideLayouts[] = {
|
||||
{HalGPIO::BTN_UP, HalGPIO::BTN_DOWN},
|
||||
@ -33,29 +18,36 @@ constexpr SideLayoutMap kSideLayouts[] = {
|
||||
} // namespace
|
||||
|
||||
bool MappedInputManager::mapButton(const Button button, bool (HalGPIO::*fn)(uint8_t) const) const {
|
||||
const auto frontLayout = static_cast<CrossPointSettings::FRONT_BUTTON_LAYOUT>(SETTINGS.frontButtonLayout);
|
||||
const auto sideLayout = static_cast<CrossPointSettings::SIDE_BUTTON_LAYOUT>(SETTINGS.sideButtonLayout);
|
||||
const auto& front = kFrontLayouts[frontLayout];
|
||||
const auto& side = kSideLayouts[sideLayout];
|
||||
|
||||
switch (button) {
|
||||
case Button::Back:
|
||||
return (gpio.*fn)(front.back);
|
||||
// Logical Back maps to user-configured front button.
|
||||
return (gpio.*fn)(SETTINGS.frontButtonBack);
|
||||
case Button::Confirm:
|
||||
return (gpio.*fn)(front.confirm);
|
||||
// Logical Confirm maps to user-configured front button.
|
||||
return (gpio.*fn)(SETTINGS.frontButtonConfirm);
|
||||
case Button::Left:
|
||||
return (gpio.*fn)(front.left);
|
||||
// Logical Left maps to user-configured front button.
|
||||
return (gpio.*fn)(SETTINGS.frontButtonLeft);
|
||||
case Button::Right:
|
||||
return (gpio.*fn)(front.right);
|
||||
// Logical Right maps to user-configured front button.
|
||||
return (gpio.*fn)(SETTINGS.frontButtonRight);
|
||||
case Button::Up:
|
||||
// Side buttons remain fixed for Up/Down.
|
||||
return (gpio.*fn)(HalGPIO::BTN_UP);
|
||||
case Button::Down:
|
||||
// Side buttons remain fixed for Up/Down.
|
||||
return (gpio.*fn)(HalGPIO::BTN_DOWN);
|
||||
case Button::Power:
|
||||
// Power button bypasses remapping.
|
||||
return (gpio.*fn)(HalGPIO::BTN_POWER);
|
||||
case Button::PageBack:
|
||||
// Reader page navigation uses side buttons and can be swapped via settings.
|
||||
return (gpio.*fn)(side.pageBack);
|
||||
case Button::PageForward:
|
||||
// Reader page navigation uses side buttons and can be swapped via settings.
|
||||
return (gpio.*fn)(side.pageForward);
|
||||
}
|
||||
|
||||
@ -76,17 +68,42 @@ unsigned long MappedInputManager::getHeldTime() const { return gpio.getHeldTime(
|
||||
|
||||
MappedInputManager::Labels MappedInputManager::mapLabels(const char* back, const char* confirm, const char* previous,
|
||||
const char* next) const {
|
||||
const auto layout = static_cast<CrossPointSettings::FRONT_BUTTON_LAYOUT>(SETTINGS.frontButtonLayout);
|
||||
// Build the label order based on the configured hardware mapping.
|
||||
auto labelForHardware = [&](uint8_t hw) -> const char* {
|
||||
// Compare against configured logical roles and return the matching label.
|
||||
if (hw == SETTINGS.frontButtonBack) {
|
||||
return back;
|
||||
}
|
||||
if (hw == SETTINGS.frontButtonConfirm) {
|
||||
return confirm;
|
||||
}
|
||||
if (hw == SETTINGS.frontButtonLeft) {
|
||||
return previous;
|
||||
}
|
||||
if (hw == SETTINGS.frontButtonRight) {
|
||||
return next;
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
switch (layout) {
|
||||
case CrossPointSettings::LEFT_RIGHT_BACK_CONFIRM:
|
||||
return {previous, next, back, confirm};
|
||||
case CrossPointSettings::LEFT_BACK_CONFIRM_RIGHT:
|
||||
return {previous, back, confirm, next};
|
||||
case CrossPointSettings::BACK_CONFIRM_RIGHT_LEFT:
|
||||
return {back, confirm, next, previous};
|
||||
case CrossPointSettings::BACK_CONFIRM_LEFT_RIGHT:
|
||||
default:
|
||||
return {back, confirm, previous, next};
|
||||
return {labelForHardware(HalGPIO::BTN_BACK), labelForHardware(HalGPIO::BTN_CONFIRM),
|
||||
labelForHardware(HalGPIO::BTN_LEFT), labelForHardware(HalGPIO::BTN_RIGHT)};
|
||||
}
|
||||
|
||||
int MappedInputManager::getPressedFrontButton() const {
|
||||
// Scan the raw front buttons in hardware order.
|
||||
// This bypasses remapping so the remap activity can capture physical presses.
|
||||
if (gpio.wasPressed(HalGPIO::BTN_BACK)) {
|
||||
return HalGPIO::BTN_BACK;
|
||||
}
|
||||
if (gpio.wasPressed(HalGPIO::BTN_CONFIRM)) {
|
||||
return HalGPIO::BTN_CONFIRM;
|
||||
}
|
||||
if (gpio.wasPressed(HalGPIO::BTN_LEFT)) {
|
||||
return HalGPIO::BTN_LEFT;
|
||||
}
|
||||
if (gpio.wasPressed(HalGPIO::BTN_RIGHT)) {
|
||||
return HalGPIO::BTN_RIGHT;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
@ -22,6 +22,8 @@ class MappedInputManager {
|
||||
bool wasAnyReleased() const;
|
||||
unsigned long getHeldTime() const;
|
||||
Labels mapLabels(const char* back, const char* confirm, const char* previous, const char* next) const;
|
||||
// Returns the raw front button index that was pressed this frame (or -1 if none).
|
||||
int getPressedFrontButton() const;
|
||||
|
||||
private:
|
||||
HalGPIO& gpio;
|
||||
|
||||
227
src/activities/settings/ButtonRemapActivity.cpp
Normal file
227
src/activities/settings/ButtonRemapActivity.cpp
Normal file
@ -0,0 +1,227 @@
|
||||
#include "ButtonRemapActivity.h"
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
|
||||
#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::taskTrampoline(void* param) {
|
||||
auto* self = static_cast<ButtonRemapActivity*>(param);
|
||||
self->displayTaskLoop();
|
||||
}
|
||||
|
||||
void ButtonRemapActivity::onEnter() {
|
||||
Activity::onEnter();
|
||||
|
||||
renderingMutex = xSemaphoreCreateMutex();
|
||||
// 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;
|
||||
updateRequired = true;
|
||||
|
||||
xTaskCreate(&ButtonRemapActivity::taskTrampoline, "ButtonRemapTask", 4096, this, 1, &displayTaskHandle);
|
||||
}
|
||||
|
||||
void ButtonRemapActivity::onExit() {
|
||||
Activity::onExit();
|
||||
|
||||
// Ensure display task is stopped outside of active rendering.
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
if (displayTaskHandle) {
|
||||
vTaskDelete(displayTaskHandle);
|
||||
displayTaskHandle = nullptr;
|
||||
}
|
||||
vSemaphoreDelete(renderingMutex);
|
||||
renderingMutex = nullptr;
|
||||
}
|
||||
|
||||
void ButtonRemapActivity::loop() {
|
||||
// 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.
|
||||
if (updateRequired) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Wait for a front button press to assign to the current role.
|
||||
const int pressedButton = mappedInput.getPressedFrontButton();
|
||||
if (pressedButton < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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))) {
|
||||
updateRequired = true;
|
||||
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;
|
||||
}
|
||||
|
||||
updateRequired = true;
|
||||
}
|
||||
|
||||
[[noreturn]] void ButtonRemapActivity::displayTaskLoop() {
|
||||
while (true) {
|
||||
if (updateRequired) {
|
||||
// Ensure render calls are serialized with UI thread changes.
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
render();
|
||||
updateRequired = false;
|
||||
xSemaphoreGive(renderingMutex);
|
||||
}
|
||||
|
||||
// Clear any temporary warning after its timeout.
|
||||
if (errorUntil > 0 && millis() > errorUntil) {
|
||||
errorMessage.clear();
|
||||
errorUntil = 0;
|
||||
updateRequired = true;
|
||||
}
|
||||
|
||||
vTaskDelay(50 / portTICK_PERIOD_MS);
|
||||
}
|
||||
}
|
||||
|
||||
void ButtonRemapActivity::render() {
|
||||
renderer.clearScreen();
|
||||
|
||||
const auto pageWidth = renderer.getScreenWidth();
|
||||
const auto labelForHardware = [&](uint8_t hardwareIndex) -> const char* {
|
||||
for (uint8_t i = 0; i < kRoleCount; i++) {
|
||||
if (tempMapping[i] == hardwareIndex) {
|
||||
return getRoleName(i);
|
||||
}
|
||||
}
|
||||
return "-";
|
||||
};
|
||||
|
||||
renderer.drawCenteredText(UI_12_FONT_ID, 15, "Remap Front Buttons", true, EpdFontFamily::BOLD);
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, 40, "Press a front button for each role");
|
||||
|
||||
for (uint8_t i = 0; i < kRoleCount; i++) {
|
||||
const int y = 70 + i * 30;
|
||||
const bool isSelected = (i == currentStep);
|
||||
|
||||
// Highlight the role that is currently being assigned.
|
||||
if (isSelected) {
|
||||
renderer.fillRect(0, y - 2, pageWidth - 1, 30);
|
||||
}
|
||||
|
||||
const char* roleName = getRoleName(i);
|
||||
renderer.drawText(UI_10_FONT_ID, 20, y, roleName, !isSelected);
|
||||
|
||||
// Show currently assigned hardware button (or unassigned).
|
||||
const char* assigned = (tempMapping[i] == kUnassigned) ? "Unassigned" : getHardwareName(tempMapping[i]);
|
||||
const auto width = renderer.getTextWidth(UI_10_FONT_ID, assigned);
|
||||
renderer.drawText(UI_10_FONT_ID, pageWidth - 20 - width, y, assigned, !isSelected);
|
||||
}
|
||||
|
||||
// Temporary warning banner for duplicates.
|
||||
if (!errorMessage.empty()) {
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, 210, errorMessage.c_str(), true);
|
||||
}
|
||||
|
||||
// Provide side button actions at the bottom of the screen (split across two lines).
|
||||
renderer.drawCenteredText(SMALL_FONT_ID, 250, "Side button Up: Reset to default layout", true);
|
||||
renderer.drawCenteredText(SMALL_FONT_ID, 280, "Side button Down: Cancel remapping", true);
|
||||
|
||||
// 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) {
|
||||
errorMessage = "Already assigned";
|
||||
errorUntil = millis() + kErrorDisplayMs;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const char* ButtonRemapActivity::getRoleName(const uint8_t roleIndex) const {
|
||||
switch (roleIndex) {
|
||||
case 0:
|
||||
return "Back";
|
||||
case 1:
|
||||
return "Confirm";
|
||||
case 2:
|
||||
return "Left";
|
||||
case 3:
|
||||
default:
|
||||
return "Right";
|
||||
}
|
||||
}
|
||||
|
||||
const char* ButtonRemapActivity::getHardwareName(const uint8_t buttonIndex) const {
|
||||
switch (buttonIndex) {
|
||||
case CrossPointSettings::FRONT_HW_BACK:
|
||||
return "Back (1st button)";
|
||||
case CrossPointSettings::FRONT_HW_CONFIRM:
|
||||
return "Confirm (2nd button)";
|
||||
case CrossPointSettings::FRONT_HW_LEFT:
|
||||
return "Left (3rd button)";
|
||||
case CrossPointSettings::FRONT_HW_RIGHT:
|
||||
return "Right (4th button)";
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
49
src/activities/settings/ButtonRemapActivity.h
Normal file
49
src/activities/settings/ButtonRemapActivity.h
Normal file
@ -0,0 +1,49 @@
|
||||
#pragma once
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/semphr.h>
|
||||
#include <freertos/task.h>
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
#include "activities/Activity.h"
|
||||
|
||||
class ButtonRemapActivity final : public Activity {
|
||||
public:
|
||||
explicit ButtonRemapActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
||||
const std::function<void()>& onBack)
|
||||
: Activity("ButtonRemap", renderer, mappedInput), onBack(onBack) {}
|
||||
|
||||
void onEnter() override;
|
||||
void onExit() override;
|
||||
void loop() override;
|
||||
|
||||
private:
|
||||
// Rendering task state.
|
||||
TaskHandle_t displayTaskHandle = nullptr;
|
||||
SemaphoreHandle_t renderingMutex = nullptr;
|
||||
bool updateRequired = false;
|
||||
|
||||
// Callback used to exit the remap flow back to the settings list.
|
||||
const std::function<void()> onBack;
|
||||
// Index of the logical role currently awaiting input.
|
||||
uint8_t currentStep = 0;
|
||||
// Temporary mapping from logical role -> hardware button index.
|
||||
uint8_t tempMapping[4] = {0xFF, 0xFF, 0xFF, 0xFF};
|
||||
// Error banner timing (used when reassigning duplicate buttons).
|
||||
unsigned long errorUntil = 0;
|
||||
std::string errorMessage;
|
||||
|
||||
// FreeRTOS task helpers.
|
||||
static void taskTrampoline(void* param);
|
||||
[[noreturn]] void displayTaskLoop();
|
||||
void render();
|
||||
|
||||
// Commit temporary mapping to settings.
|
||||
void applyTempMapping();
|
||||
// Returns false if a hardware button is already assigned to a different role.
|
||||
bool validateUnassigned(uint8_t pressedButton);
|
||||
// Labels for UI display.
|
||||
const char* getRoleName(uint8_t roleIndex) const;
|
||||
const char* getHardwareName(uint8_t buttonIndex) const;
|
||||
};
|
||||
@ -3,6 +3,7 @@
|
||||
#include <GfxRenderer.h>
|
||||
#include <HardwareSerial.h>
|
||||
|
||||
#include "ButtonRemapActivity.h"
|
||||
#include "CalibreSettingsActivity.h"
|
||||
#include "ClearCacheActivity.h"
|
||||
#include "CrossPointSettings.h"
|
||||
@ -47,9 +48,8 @@ const SettingInfo readerSettings[readerSettingsCount] = {
|
||||
|
||||
constexpr int controlsSettingsCount = 4;
|
||||
const SettingInfo controlsSettings[controlsSettingsCount] = {
|
||||
SettingInfo::Enum(
|
||||
"Front Button Layout", &CrossPointSettings::frontButtonLayout,
|
||||
{"Bck, Cnfrm, Lft, Rght", "Lft, Rght, Bck, Cnfrm", "Lft, Bck, Cnfrm, Rght", "Bck, Cnfrm, Rght, Lft"}),
|
||||
// Launches the remap wizard for front buttons.
|
||||
SettingInfo::Action("Remap Front Buttons"),
|
||||
SettingInfo::Enum("Side Button Layout (reader)", &CrossPointSettings::sideButtonLayout,
|
||||
{"Prev, Next", "Next, Prev"}),
|
||||
SettingInfo::Toggle("Long-press Chapter Skip", &CrossPointSettings::longPressChapterSkip),
|
||||
@ -201,7 +201,15 @@ void SettingsActivity::toggleCurrentSetting() {
|
||||
SETTINGS.*(setting.valuePtr) = currentValue + setting.valueRange.step;
|
||||
}
|
||||
} else if (setting.type == SettingType::ACTION) {
|
||||
if (strcmp(setting.name, "KOReader Sync") == 0) {
|
||||
if (strcmp(setting.name, "Remap Front Buttons") == 0) {
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
exitActivity();
|
||||
enterNewActivity(new ButtonRemapActivity(renderer, mappedInput, [this] {
|
||||
exitActivity();
|
||||
updateRequired = true;
|
||||
}));
|
||||
xSemaphoreGive(renderingMutex);
|
||||
} else if (strcmp(setting.name, "KOReader Sync") == 0) {
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
exitActivity();
|
||||
enterNewActivity(new KOReaderSettingsActivity(renderer, mappedInput, [this] {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user