Support swapping the functionality of the front buttons (#133)

## Summary

**What is the goal of this PR?** 

Adds a setting to swap the front buttons. The default functionality are:
Back/Confirm/Left/Right. When this setting is enabled they become:
Left/Right/Back/Confirm. This makes it more comfortable to use when
holding in your right hand since your thumb can more easily rest on the
next button. The original firmware has a similar setting.

**What changes are included?**

- Add the new setting.
- Create a mapper to dynamically switch the buttons based on the
setting.
- Use mapper on the various activity screens.
- Update the button hints to reflect the swapped buttons.

## Additional Context

Full disclosure: I used Codex CLI to put this PR together, but did
review it to make sure it makes sense.

Also tested on my device:
https://share.cleanshot.com/k76891NY
This commit is contained in:
dangson
2025-12-28 21:59:14 -06:00
committed by GitHub
parent 534504cf7a
commit 140d8749a6
35 changed files with 285 additions and 140 deletions

View File

@@ -72,7 +72,7 @@ void OtaUpdateActivity::onEnter() {
// Launch WiFi selection subactivity
Serial.printf("[%lu] [OTA] Launching WifiSelectionActivity...\n", millis());
enterNewActivity(new WifiSelectionActivity(renderer, inputManager,
enterNewActivity(new WifiSelectionActivity(renderer, mappedInput,
[this](const bool connected) { onWifiSelectionComplete(connected); }));
}
@@ -191,7 +191,7 @@ void OtaUpdateActivity::loop() {
}
if (state == WAITING_CONFIRMATION) {
if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) {
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
Serial.printf("[%lu] [OTA] New update available, starting download...\n", millis());
xSemaphoreTake(renderingMutex, portMAX_DELAY);
state = UPDATE_IN_PROGRESS;
@@ -215,7 +215,7 @@ void OtaUpdateActivity::loop() {
updateRequired = true;
}
if (inputManager.wasPressed(InputManager::BTN_BACK)) {
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
goBack();
}
@@ -223,14 +223,14 @@ void OtaUpdateActivity::loop() {
}
if (state == FAILED) {
if (inputManager.wasPressed(InputManager::BTN_BACK)) {
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
goBack();
}
return;
}
if (state == NO_UPDATE) {
if (inputManager.wasPressed(InputManager::BTN_BACK)) {
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
goBack();
}
return;

View File

@@ -35,8 +35,9 @@ class OtaUpdateActivity : public ActivityWithSubactivity {
void render();
public:
explicit OtaUpdateActivity(GfxRenderer& renderer, InputManager& inputManager, const std::function<void()>& goBack)
: ActivityWithSubactivity("OtaUpdate", renderer, inputManager), goBack(goBack), updater() {}
explicit OtaUpdateActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
const std::function<void()>& goBack)
: ActivityWithSubactivity("OtaUpdate", renderer, mappedInput), goBack(goBack), updater() {}
void onEnter() override;
void onExit() override;
void loop() override;

View File

@@ -9,7 +9,7 @@
// Define the static settings list
namespace {
constexpr int settingsCount = 6;
constexpr int settingsCount = 7;
const SettingInfo settingsList[settingsCount] = {
// Should match with SLEEP_SCREEN_MODE
{"Sleep Screen", SettingType::ENUM, &CrossPointSettings::sleepScreen, {"Dark", "Light", "Custom", "Cover"}},
@@ -20,6 +20,10 @@ const SettingInfo settingsList[settingsCount] = {
SettingType::ENUM,
&CrossPointSettings::orientation,
{"Portrait", "Landscape CW", "Inverted", "Landscape CCW"}},
{"Front Button Layout",
SettingType::ENUM,
&CrossPointSettings::frontButtonLayout,
{"Bck, Cnfrm, Lft, Rght", "Lft, Rght, Bck, Cnfrm"}},
{"Check for updates", SettingType::ACTION, nullptr, {}},
};
} // namespace
@@ -68,24 +72,26 @@ void SettingsActivity::loop() {
}
// Handle actions with early return
if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) {
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
toggleCurrentSetting();
updateRequired = true;
return;
}
if (inputManager.wasPressed(InputManager::BTN_BACK)) {
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
SETTINGS.saveToFile();
onGoHome();
return;
}
// Handle navigation
if (inputManager.wasPressed(InputManager::BTN_UP) || inputManager.wasPressed(InputManager::BTN_LEFT)) {
if (mappedInput.wasPressed(MappedInputManager::Button::Up) ||
mappedInput.wasPressed(MappedInputManager::Button::Left)) {
// Move selection up (with wrap-around)
selectedSettingIndex = (selectedSettingIndex > 0) ? (selectedSettingIndex - 1) : (settingsCount - 1);
updateRequired = true;
} else if (inputManager.wasPressed(InputManager::BTN_DOWN) || inputManager.wasPressed(InputManager::BTN_RIGHT)) {
} else if (mappedInput.wasPressed(MappedInputManager::Button::Down) ||
mappedInput.wasPressed(MappedInputManager::Button::Right)) {
// Move selection down
if (selectedSettingIndex < settingsCount - 1) {
selectedSettingIndex++;
@@ -113,7 +119,7 @@ void SettingsActivity::toggleCurrentSetting() {
if (std::string(setting.name) == "Check for updates") {
xSemaphoreTake(renderingMutex, portMAX_DELAY);
exitActivity();
enterNewActivity(new OtaUpdateActivity(renderer, inputManager, [this] {
enterNewActivity(new OtaUpdateActivity(renderer, mappedInput, [this] {
exitActivity();
updateRequired = true;
}));
@@ -173,10 +179,13 @@ void SettingsActivity::render() const {
}
}
// Draw help text
renderer.drawButtonHints(UI_FONT_ID, "« Save", "Toggle", "", "");
// Draw version text above button hints
renderer.drawText(SMALL_FONT_ID, pageWidth - 20 - renderer.getTextWidth(SMALL_FONT_ID, CROSSPOINT_VERSION),
pageHeight - 30, CROSSPOINT_VERSION);
pageHeight - 60, CROSSPOINT_VERSION);
// Draw help text
const auto labels = mappedInput.mapLabels("« Save", "Toggle", "", "");
renderer.drawButtonHints(UI_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
// Always use standard refresh for settings screen
renderer.displayBuffer();

View File

@@ -34,8 +34,9 @@ class SettingsActivity final : public ActivityWithSubactivity {
void toggleCurrentSetting();
public:
explicit SettingsActivity(GfxRenderer& renderer, InputManager& inputManager, const std::function<void()>& onGoHome)
: ActivityWithSubactivity("Settings", renderer, inputManager), onGoHome(onGoHome) {}
explicit SettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
const std::function<void()>& onGoHome)
: ActivityWithSubactivity("Settings", renderer, mappedInput), onGoHome(onGoHome) {}
void onEnter() override;
void onExit() override;
void loop() override;