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

@@ -57,7 +57,7 @@ void CrossPointWebServerActivity::onEnter() {
// Launch network mode selection subactivity
Serial.printf("[%lu] [WEBACT] Launching NetworkModeSelectionActivity...\n", millis());
enterNewActivity(new NetworkModeSelectionActivity(
renderer, inputManager, [this](const NetworkMode mode) { onNetworkModeSelected(mode); },
renderer, mappedInput, [this](const NetworkMode mode) { onNetworkModeSelected(mode); },
[this]() { onGoBack(); } // Cancel goes back to home
));
}
@@ -141,7 +141,7 @@ void CrossPointWebServerActivity::onNetworkModeSelected(const NetworkMode mode)
state = WebServerActivityState::WIFI_SELECTION;
Serial.printf("[%lu] [WEBACT] Launching WifiSelectionActivity...\n", millis());
enterNewActivity(new WifiSelectionActivity(renderer, inputManager,
enterNewActivity(new WifiSelectionActivity(renderer, mappedInput,
[this](const bool connected) { onWifiSelectionComplete(connected); }));
} else {
// AP mode - start access point
@@ -174,7 +174,7 @@ void CrossPointWebServerActivity::onWifiSelectionComplete(const bool connected)
exitActivity();
state = WebServerActivityState::MODE_SELECTION;
enterNewActivity(new NetworkModeSelectionActivity(
renderer, inputManager, [this](const NetworkMode mode) { onNetworkModeSelected(mode); },
renderer, mappedInput, [this](const NetworkMode mode) { onNetworkModeSelected(mode); },
[this]() { onGoBack(); }));
}
}
@@ -305,7 +305,7 @@ void CrossPointWebServerActivity::loop() {
}
// Handle exit on Back button
if (inputManager.wasPressed(InputManager::BTN_BACK)) {
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
onGoBack();
return;
}
@@ -428,5 +428,6 @@ void CrossPointWebServerActivity::renderServerRunning() const {
REGULAR);
}
renderer.drawButtonHints(UI_FONT_ID, "« Exit", "", "", "");
const auto labels = mappedInput.mapLabels("« Exit", "", "", "");
renderer.drawButtonHints(UI_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
}

View File

@@ -63,9 +63,9 @@ class CrossPointWebServerActivity final : public ActivityWithSubactivity {
void stopWebServer();
public:
explicit CrossPointWebServerActivity(GfxRenderer& renderer, InputManager& inputManager,
explicit CrossPointWebServerActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
const std::function<void()>& onGoBack)
: ActivityWithSubactivity("CrossPointWebServer", renderer, inputManager), onGoBack(onGoBack) {}
: ActivityWithSubactivity("CrossPointWebServer", renderer, mappedInput), onGoBack(onGoBack) {}
void onEnter() override;
void onExit() override;
void loop() override;

View File

@@ -51,23 +51,23 @@ void NetworkModeSelectionActivity::onExit() {
void NetworkModeSelectionActivity::loop() {
// Handle back button - cancel
if (inputManager.wasPressed(InputManager::BTN_BACK)) {
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
onCancel();
return;
}
// Handle confirm button - select current option
if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) {
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
const NetworkMode mode = (selectedIndex == 0) ? NetworkMode::JOIN_NETWORK : NetworkMode::CREATE_HOTSPOT;
onModeSelected(mode);
return;
}
// Handle navigation
const bool prevPressed =
inputManager.wasPressed(InputManager::BTN_UP) || inputManager.wasPressed(InputManager::BTN_LEFT);
const bool nextPressed =
inputManager.wasPressed(InputManager::BTN_DOWN) || inputManager.wasPressed(InputManager::BTN_RIGHT);
const bool prevPressed = mappedInput.wasPressed(MappedInputManager::Button::Up) ||
mappedInput.wasPressed(MappedInputManager::Button::Left);
const bool nextPressed = mappedInput.wasPressed(MappedInputManager::Button::Down) ||
mappedInput.wasPressed(MappedInputManager::Button::Right);
if (prevPressed) {
selectedIndex = (selectedIndex + MENU_ITEM_COUNT - 1) % MENU_ITEM_COUNT;
@@ -122,7 +122,8 @@ void NetworkModeSelectionActivity::render() const {
}
// Draw help text at bottom
renderer.drawButtonHints(UI_FONT_ID, "« Back", "Select", "", "");
const auto labels = mappedInput.mapLabels("« Back", "Select", "", "");
renderer.drawButtonHints(UI_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer();
}

View File

@@ -31,10 +31,10 @@ class NetworkModeSelectionActivity final : public Activity {
void render() const;
public:
explicit NetworkModeSelectionActivity(GfxRenderer& renderer, InputManager& inputManager,
explicit NetworkModeSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
const std::function<void(NetworkMode)>& onModeSelected,
const std::function<void()>& onCancel)
: Activity("NetworkModeSelection", renderer, inputManager), onModeSelected(onModeSelected), onCancel(onCancel) {}
: Activity("NetworkModeSelection", renderer, mappedInput), onModeSelected(onModeSelected), onCancel(onCancel) {}
void onEnter() override;
void onExit() override;
void loop() override;

View File

@@ -190,7 +190,7 @@ void WifiSelectionActivity::selectNetwork(const int index) {
// Don't allow screen updates while changing activity
xSemaphoreTake(renderingMutex, portMAX_DELAY);
enterNewActivity(new KeyboardEntryActivity(
renderer, inputManager, "Enter WiFi Password",
renderer, mappedInput, "Enter WiFi Password",
"", // No initial text
50, // Y position
64, // Max password length
@@ -302,17 +302,19 @@ void WifiSelectionActivity::loop() {
// Handle save prompt state
if (state == WifiSelectionState::SAVE_PROMPT) {
if (inputManager.wasPressed(InputManager::BTN_LEFT) || inputManager.wasPressed(InputManager::BTN_UP)) {
if (mappedInput.wasPressed(MappedInputManager::Button::Up) ||
mappedInput.wasPressed(MappedInputManager::Button::Left)) {
if (savePromptSelection > 0) {
savePromptSelection--;
updateRequired = true;
}
} else if (inputManager.wasPressed(InputManager::BTN_RIGHT) || inputManager.wasPressed(InputManager::BTN_DOWN)) {
} else if (mappedInput.wasPressed(MappedInputManager::Button::Down) ||
mappedInput.wasPressed(MappedInputManager::Button::Right)) {
if (savePromptSelection < 1) {
savePromptSelection++;
updateRequired = true;
}
} else if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) {
} else if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
if (savePromptSelection == 0) {
// User chose "Yes" - save the password
xSemaphoreTake(renderingMutex, portMAX_DELAY);
@@ -321,7 +323,7 @@ void WifiSelectionActivity::loop() {
}
// Complete - parent will start web server
onComplete(true);
} else if (inputManager.wasPressed(InputManager::BTN_BACK)) {
} else if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
// Skip saving, complete anyway
onComplete(true);
}
@@ -330,17 +332,19 @@ void WifiSelectionActivity::loop() {
// Handle forget prompt state (connection failed with saved credentials)
if (state == WifiSelectionState::FORGET_PROMPT) {
if (inputManager.wasPressed(InputManager::BTN_LEFT) || inputManager.wasPressed(InputManager::BTN_UP)) {
if (mappedInput.wasPressed(MappedInputManager::Button::Up) ||
mappedInput.wasPressed(MappedInputManager::Button::Left)) {
if (forgetPromptSelection > 0) {
forgetPromptSelection--;
updateRequired = true;
}
} else if (inputManager.wasPressed(InputManager::BTN_RIGHT) || inputManager.wasPressed(InputManager::BTN_DOWN)) {
} else if (mappedInput.wasPressed(MappedInputManager::Button::Down) ||
mappedInput.wasPressed(MappedInputManager::Button::Right)) {
if (forgetPromptSelection < 1) {
forgetPromptSelection++;
updateRequired = true;
}
} else if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) {
} else if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
if (forgetPromptSelection == 0) {
// User chose "Yes" - forget the network
xSemaphoreTake(renderingMutex, portMAX_DELAY);
@@ -356,7 +360,7 @@ void WifiSelectionActivity::loop() {
// Go back to network list
state = WifiSelectionState::NETWORK_LIST;
updateRequired = true;
} else if (inputManager.wasPressed(InputManager::BTN_BACK)) {
} else if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
// Skip forgetting, go back to network list
state = WifiSelectionState::NETWORK_LIST;
updateRequired = true;
@@ -373,7 +377,8 @@ void WifiSelectionActivity::loop() {
// Handle connection failed state
if (state == WifiSelectionState::CONNECTION_FAILED) {
if (inputManager.wasPressed(InputManager::BTN_BACK) || inputManager.wasPressed(InputManager::BTN_CONFIRM)) {
if (mappedInput.wasPressed(MappedInputManager::Button::Back) ||
mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
// If we used saved credentials, offer to forget the network
if (usedSavedPassword) {
state = WifiSelectionState::FORGET_PROMPT;
@@ -390,13 +395,13 @@ void WifiSelectionActivity::loop() {
// Handle network list state
if (state == WifiSelectionState::NETWORK_LIST) {
// Check for Back button to exit (cancel)
if (inputManager.wasPressed(InputManager::BTN_BACK)) {
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
onComplete(false);
return;
}
// Check for Confirm button to select network or rescan
if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) {
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
if (!networks.empty()) {
selectNetwork(selectedNetworkIndex);
} else {
@@ -406,12 +411,14 @@ void WifiSelectionActivity::loop() {
}
// Handle UP/DOWN navigation
if (inputManager.wasPressed(InputManager::BTN_UP) || inputManager.wasPressed(InputManager::BTN_LEFT)) {
if (mappedInput.wasPressed(MappedInputManager::Button::Up) ||
mappedInput.wasPressed(MappedInputManager::Button::Left)) {
if (selectedNetworkIndex > 0) {
selectedNetworkIndex--;
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)) {
if (!networks.empty() && selectedNetworkIndex < static_cast<int>(networks.size()) - 1) {
selectedNetworkIndex++;
updateRequired = true;
@@ -557,7 +564,8 @@ void WifiSelectionActivity::renderNetworkList() const {
// Draw help text
renderer.drawText(SMALL_FONT_ID, 20, pageHeight - 75, "* = Encrypted | + = Saved");
renderer.drawButtonHints(UI_FONT_ID, "« Back", "Connect", "", "");
const auto labels = mappedInput.mapLabels("« Back", "Connect", "", "");
renderer.drawButtonHints(UI_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
}
void WifiSelectionActivity::renderConnecting() const {

View File

@@ -92,9 +92,9 @@ class WifiSelectionActivity final : public ActivityWithSubactivity {
std::string getSignalStrengthIndicator(int32_t rssi) const;
public:
explicit WifiSelectionActivity(GfxRenderer& renderer, InputManager& inputManager,
explicit WifiSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
const std::function<void(bool connected)>& onComplete)
: ActivityWithSubactivity("WifiSelection", renderer, inputManager), onComplete(onComplete) {}
: ActivityWithSubactivity("WifiSelection", renderer, mappedInput), onComplete(onComplete) {}
void onEnter() override;
void onExit() override;
void loop() override;