Compare commits

...
Author SHA1 Message Date
Uri Tauber d9a207f9a9 refactor: unify control settings models 2026-08-09 12:24:05 +03:00
Uri TauberandJulia Nguyen 77b58e431f feat: group Settings -> Controls into submenus
The Controls category had grown to a flat list of a dozen unrelated
entries. Split it into Power Button, Front Buttons and Side Buttons
groups, reached through a new SUBMENU row type that swaps the visible
list in place rather than pushing another activity.

- Front Buttons only exists on devices that have them. On touch devices
  the entries that still apply (long-press behaviour, which also covers
  the touch page-turn zones) stay at the top level.
- Tilt Page Turn and Tilt Direction sit at the top level, and only on
  IMU-capable boards.
- Quick-return from footnotes now appears whenever any shortcut targets
  footnotes, not only the short power press.

Back leaves a group before it leaves Settings, and the header names the
open group. Choosing which list is on screen was duplicated between the
rebuild and the input handler; both now call one method.

Ported from CrossInk.

Co-Authored-By: Julia Nguyen <julia@uxj.io>
2026-08-07 19:09:51 +03:00
Uri TauberandJulia Nguyen 167663cb6e feat: run configurable Controls shortcuts in the reader
Adds a single ShortcutAction space that both persisted shortcut enums
translate into, so each dispatcher switches over one type instead of
repeating a near-identical switch per binding. Global actions (sleep,
refresh, screenshot, file browser, file transfer, tilt toggle) run from
one executor; reader-only ones (footnotes, bookmark, sync, look up
word) are handled by the reader and fall through to it.

Wires it up:

- Long power hold runs longPwrBtn, latched so a held button fires once
  and the release that ends the hold does not also run the short-press
  action. Sleep is the default, matching what a hold has always done.
- Short power release runs shortPwrBtn, no longer only Refresh.
- Long Back runs longPressBackAction; the default keeps the existing
  file-browser navigation untouched.
- Long Confirm runs longPressMenuAction over the whole action set. An
  action that declines (sync with no credentials, a page with no
  footnotes) leaves the release alone, so Confirm still opens the menu.
- detectPageTurn reports whether a turn came from the side buttons, so
  the side and front pairs honour their own long-press settings and
  their own press-vs-release timing.

Font-size change is deliberately absent from the long-press options:
it needs per-reader re-layout plumbing that does not exist yet, and the
rule for this port is to offer no action we cannot execute cleanly.

Ported from CrossInk.

Co-Authored-By: Julia Nguyen <julia@uxj.io>
2026-08-07 19:04:19 +03:00
Uri TauberandJulia Nguyen 2aa0f7be17 feat: apply Controls button mapping at input layer
Routes the new Controls settings through MappedInputManager:

- Next/Next side layout, via a table of side-button bindings that also
  replaces the two parallel switch statements over sideButtonLayout.
- sideButtonOrientationAware, swapping the side pair (and reader menu
  Up/Down along with it) whenever the screen renders non-portrait.
- The ALL_BUTTONS front orientation mode, which mirrors Back/Confirm
  across the front row when the screen renders inverted. The navigation
  pair is deliberately left alone: isNavDirectionSwapped() already flips
  it, so mirroring both would cancel out.
- readerFrontButtons*, a reader-only front mapping, gated on a
  readerMode flag main.cpp keeps in sync with the activity stack. The
  renderer cannot supply this — a portrait reader renders exactly like
  portrait UI.

Button hints now resolve through the same mapping as the button reads,
so a reader override or an inverted mirror moves the labels with the
buttons instead of leaving them behind.

ButtonRemapActivity takes a target so it can edit either mapping.

Ported from CrossInk.

Co-Authored-By: Julia Nguyen <julia@uxj.io>
2026-08-07 18:55:39 +03:00
Uri TauberandJulia Nguyen a1c0bdfa05 feat: add Controls settings schema and migrations
Adds the persisted fields the CrossInk Controls screen is built on, and
migrates the existing CrossPoint settings onto them so no stored value
changes meaning:

- longPwrBtn, sharing the power-button action enum with shortPwrBtn.
  Values 0..4 keep their CrossPoint meanings — notably 4 stays
  Footnotes, where CrossInk's numbering diverges — and the new actions
  are appended, with the menu order restored via enumRawValues.
- frontButtonOrientationAware, a three-state setting migrated from the
  frontButtonFollowOrientation toggle (on -> nav buttons only).
- sideButtonOrientationAware and sideButtonLongPress. The side pair used
  to share longPressButtonBehavior with the front pair, so the old value
  is carried across on load.
- readerFrontButtons*, a reader-only front-button remap, inert until
  readerFrontButtonsEnabled is turned on.
- longPressMenuAction and longPressBackAction on a shared quick-action
  enum, migrated from longPressMenuFunction.
- tiltPageTurnDirection, splitting the axis out of the combined
  tiltPageTurn enum. Legacy "normal" is the non-inverted left/right
  gesture and "inverted" its negation, so the HAL reproduces both.

Only actions the reader can already execute are offered; the enums carry
no option that has no implementation behind it.

Ported from CrossInk.

Co-Authored-By: Julia Nguyen <julia@uxj.io>
2026-08-07 18:50:11 +03:00
Uri TauberandJulia Nguyen 4e8aea95a0 feat: support raw enum values in settings schema
An ENUM setting's option order and its persisted values were the same
number, so reordering or gapping an option list silently reinterpreted
existing settings.json files. Add an optional parallel enumRawValues
array and route every reader/writer of an ENUM — device settings UI,
web settings API, and load-time validation — through three shared
helpers that translate between display index and raw value.

Behaviour is unchanged for every setting that does not populate
enumRawValues: the helpers fall through to the identity mapping.

Ported from CrossInk.

Co-Authored-By: Julia Nguyen <julia@uxj.io>
2026-08-07 18:39:57 +03:00
23 changed files with 1281 additions and 470 deletions

No files matched your search

+22
View File
@@ -430,3 +430,25 @@ STR_FIRMWARE_WRITE_FAILED: "Firmware write failed"
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Do not power off!"
STR_RECOVERY_MODE: "Recovery Mode"
STR_RECOVERY_MODE_HINT: "Place firmware.bin on SD card root and select it"
STR_POWER_BUTTON: "Power Button"
STR_FRONT_BUTTONS: "Front Buttons"
STR_SIDE_BUTTONS: "Side Buttons"
STR_SHORT_PRESS_ACTION: "Short-press Action"
STR_LONG_PRESS_ACTION: "Long-press Action"
STR_LONG_PRESS_BACK_ACTION: "Long-press Back Action"
STR_ORIENTATION_AWARE: "Orientation Aware"
STR_ORIENT_NAV_BUTTONS: "Nav Buttons"
STR_ORIENT_ALL_BUTTONS: "All Buttons"
STR_REMAP_BUTTONS: "Remap Buttons"
STR_REMAP_BUTTONS_READER: "Remap Buttons (Reader)"
STR_READER_BUTTON_OVERRIDE: "Reader Override"
STR_NEXT_NEXT: "Next/Next"
STR_LOOK_UP_WORD: "Look Up Word"
STR_TAKE_SCREENSHOT: "Screenshot"
STR_TOGGLE_TILT_PAGE_TURN: "Toggle Tilt Page Turn"
STR_TILT_DIRECTION: "Tilt Direction"
STR_TILT_LEFT_RIGHT: "Left-Right"
STR_TILT_RIGHT_LEFT: "Right-Left"
STR_TILT_FORWARD_BACK: "Forward-Back"
STR_TILT_BACK_FORWARD: "Back-Forward"
+16 -7
View File
@@ -62,7 +62,8 @@ bool HalTiltSensor::deepSleep() {
return true;
}
void HalTiltSensor::update(const uint8_t mode, const uint8_t orientation, const bool inReader) {
void HalTiltSensor::update(const uint8_t mode, const uint8_t direction, const uint8_t orientation,
const bool inReader) {
if (!_available) {
return;
}
@@ -97,26 +98,34 @@ void HalTiltSensor::update(const uint8_t mode, const uint8_t orientation, const
return;
}
// Map the gyro axis to left/right tilt based on reader orientation.
// Map the gyro axis to the selected gesture based on reader orientation.
// On the X3 PCB: X axis = left/right in portrait, Y axis = left/right in landscape.
// The left/right axis with inverted == false reproduces the pre-split TILT_NORMAL behaviour.
const bool forwardBack = direction == CrossPointTiltPageTurnDirection::TILT_FORWARD_BACK ||
direction == CrossPointTiltPageTurnDirection::TILT_BACK_FORWARD;
const bool inverted = direction == CrossPointTiltPageTurnDirection::TILT_RIGHT_LEFT ||
direction == CrossPointTiltPageTurnDirection::TILT_BACK_FORWARD;
float tiltAxis;
switch (orientation) {
case CrossPointOrientation::PORTRAIT:
tiltAxis = mode == CrossPointTiltPageTurn::TILT_INVERTED ? -gx : gx;
tiltAxis = forwardBack ? gy : gx;
break;
case CrossPointOrientation::INVERTED:
tiltAxis = mode == CrossPointTiltPageTurn::TILT_INVERTED ? gx : -gx;
tiltAxis = forwardBack ? -gy : -gx;
break;
case CrossPointOrientation::LANDSCAPE_CW:
tiltAxis = mode == CrossPointTiltPageTurn::TILT_INVERTED ? gy : -gy;
tiltAxis = forwardBack ? gx : -gy;
break;
case CrossPointOrientation::LANDSCAPE_CCW:
tiltAxis = mode == CrossPointTiltPageTurn::TILT_INVERTED ? -gy : gy;
tiltAxis = forwardBack ? -gx : gy;
break;
default:
tiltAxis = gx;
tiltAxis = forwardBack ? gy : gx;
break;
}
if (inverted) {
tiltAxis = -tiltAxis;
}
if (_inTilt) {
// Wait for device to return to neutral before allowing next trigger
+6 -2
View File
@@ -9,7 +9,11 @@ enum Value : uint8_t { PORTRAIT = 0, LANDSCAPE_CW = 1, INVERTED = 2, LANDSCAPE_C
}
namespace CrossPointTiltPageTurn {
enum Value : uint8_t { TILT_OFF = 0, TILT_NORMAL = 1, TILT_INVERTED = 2 };
enum Value : uint8_t { TILT_OFF = 0, TILT_ON = 1 };
}
namespace CrossPointTiltPageTurnDirection {
enum Value : uint8_t { TILT_LEFT_RIGHT = 0, TILT_RIGHT_LEFT = 1, TILT_FORWARD_BACK = 2, TILT_BACK_FORWARD = 3 };
}
class HalTiltSensor;
@@ -54,7 +58,7 @@ class HalTiltSensor {
bool isAvailable() const { return _available; }
// Poll the accelerometer and update tilt gesture state.
void update(const uint8_t mode, const uint8_t orientation, const bool inReader);
void update(const uint8_t mode, const uint8_t direction, const uint8_t orientation, const bool inReader);
// Returns true once per tilt-forward gesture (next page direction).
// Consumed on read — subsequent calls return false until next gesture.
+95 -2
View File
@@ -4,7 +4,6 @@
#include <Logging.h>
#include <ObfuscationUtils.h>
#include <algorithm>
#include <cstring>
#include <iterator>
#include <string>
@@ -44,6 +43,22 @@ void CrossPointSettings::validateFrontButtonMapping(CrossPointSettings& settings
}
}
void CrossPointSettings::validateReaderFrontButtonMapping(CrossPointSettings& settings) {
const uint8_t mapping[] = {settings.readerFrontButtonBack, settings.readerFrontButtonConfirm,
settings.readerFrontButtonLeft, settings.readerFrontButtonRight};
for (size_t i = 0; i < 4; i++) {
for (size_t j = i + 1; j < 4; j++) {
if (mapping[i] == mapping[j]) {
settings.readerFrontButtonBack = FRONT_HW_BACK;
settings.readerFrontButtonConfirm = FRONT_HW_CONFIRM;
settings.readerFrontButtonLeft = FRONT_HW_LEFT;
settings.readerFrontButtonRight = FRONT_HW_RIGHT;
return;
}
}
}
}
uint8_t CrossPointSettings::sleepTimeoutEnumToMinutes(const uint8_t legacyValue) {
switch (legacyValue) {
case SLEEP_1_MIN:
@@ -87,6 +102,10 @@ void CrossPointSettings::toJson(JsonDocument& doc) const {
doc["frontButtonConfirm"] = frontButtonConfirm;
doc["frontButtonLeft"] = frontButtonLeft;
doc["frontButtonRight"] = frontButtonRight;
doc["readerFrontButtonBack"] = readerFrontButtonBack;
doc["readerFrontButtonConfirm"] = readerFrontButtonConfirm;
doc["readerFrontButtonLeft"] = readerFrontButtonLeft;
doc["readerFrontButtonRight"] = readerFrontButtonRight;
// Font family and size — both use dynamic getter/setters in SettingsList (the
// option lists depend on the SD font registry), so the generic loop skips them.
doc["fontFamily"] = fontFamily;
@@ -159,7 +178,7 @@ bool CrossPointSettings::fromJson(JsonVariantConst doc) {
const uint8_t fieldDefault = s.*(info.valuePtr); // struct-initializer default, read before we overwrite it
uint8_t v = doc[info.key] | fieldDefault;
if (info.type == SettingType::ENUM) {
v = clamp(v, (uint8_t)info.enumValues.size(), fieldDefault);
if (!info.isEnumRawValueAllowed(v)) v = fieldDefault;
} else if (info.type == SettingType::TOGGLE) {
v = clamp(v, (uint8_t)2, fieldDefault);
} else if (info.type == SettingType::VALUE) {
@@ -186,6 +205,80 @@ bool CrossPointSettings::fromJson(JsonVariantConst doc) {
frontButtonRight =
clamp(doc["frontButtonRight"] | (uint8_t)FRONT_HW_RIGHT, FRONT_BUTTON_HARDWARE_COUNT, FRONT_HW_RIGHT);
validateFrontButtonMapping(s);
readerFrontButtonBack =
clamp(doc["readerFrontButtonBack"] | (uint8_t)FRONT_HW_BACK, FRONT_BUTTON_HARDWARE_COUNT, FRONT_HW_BACK);
readerFrontButtonConfirm =
clamp(doc["readerFrontButtonConfirm"] | (uint8_t)FRONT_HW_CONFIRM, FRONT_BUTTON_HARDWARE_COUNT, FRONT_HW_CONFIRM);
readerFrontButtonLeft =
clamp(doc["readerFrontButtonLeft"] | (uint8_t)FRONT_HW_LEFT, FRONT_BUTTON_HARDWARE_COUNT, FRONT_HW_LEFT);
readerFrontButtonRight =
clamp(doc["readerFrontButtonRight"] | (uint8_t)FRONT_HW_RIGHT, FRONT_BUTTON_HARDWARE_COUNT, FRONT_HW_RIGHT);
validateReaderFrontButtonMapping(s);
// --- Controls migrations -------------------------------------------------
// Each runs only when the new key is absent, so a file already written by this
// version is left alone and the migration cannot re-fire against a user's edit.
// Orientation-aware front buttons went from a toggle to a three-state setting.
// The old "on" meant navigation buttons only.
if (doc["frontButtonOrientationAware"].isNull() && !doc["frontButtonFollowOrientation"].isNull()) {
frontButtonOrientationAware = (doc["frontButtonFollowOrientation"] | (uint8_t)0) != 0
? FRONT_ORIENTATION_AWARE_NAV_BUTTONS
: FRONT_ORIENTATION_AWARE_OFF;
needsResave = true;
}
// The side buttons used to share longPressButtonBehavior with the front pair.
// Carry that value across so an upgrade does not change what a side hold does.
if (doc["sideButtonLongPress"].isNull() && !doc["longPressButtonBehavior"].isNull()) {
switch (doc["longPressButtonBehavior"] | (uint8_t)OFF) {
case CHAPTER_SKIP:
sideButtonLongPress = SIDE_LONG_CHAPTER_SKIP;
break;
case ORIENTATION_CHANGE:
sideButtonLongPress = SIDE_LONG_ORIENTATION_CHANGE;
break;
default:
sideButtonLongPress = SIDE_LONG_OFF;
break;
}
needsResave = true;
}
// longPressMenuFunction was a four-entry list with its own numbering.
if (doc["longPressMenuAction"].isNull() && !doc["longPressMenuFunction"].isNull()) {
switch (doc["longPressMenuFunction"] | (uint8_t)LP_MENU_DISABLED) {
case LP_MENU_KOSYNC:
longPressMenuAction = shortcutActionRawValue(ShortcutAction::SyncProgress);
break;
case LP_MENU_BOOKMARK:
longPressMenuAction = shortcutActionRawValue(ShortcutAction::ToggleBookmark);
break;
case LP_MENU_DICTIONARY:
longPressMenuAction = shortcutActionRawValue(ShortcutAction::LookUpWord);
break;
case LP_MENU_DISABLED:
default:
longPressMenuAction = shortcutActionRawValue(ShortcutAction::None);
break;
}
needsResave = true;
}
// tiltPageTurn used to carry both the on/off state and the axis direction.
// Legacy "normal" is exactly the non-inverted left/right gesture.
if (doc["tiltPageTurnDirection"].isNull() && !doc["tiltPageTurn"].isNull()) {
const uint8_t legacyTilt = doc["tiltPageTurn"] | (uint8_t)TILT_OFF;
if (legacyTilt == LEGACY_TILT_INVERTED) {
tiltPageTurn = TILT_ON;
tiltPageTurnDirection = TILT_RIGHT_LEFT;
needsResave = true;
} else if (legacyTilt == LEGACY_TILT_NORMAL) {
tiltPageTurn = TILT_ON;
tiltPageTurnDirection = TILT_LEFT_RIGHT;
needsResave = true;
}
}
// Reader font size — an actual point size since 1.5. Files written by 1.4 and
// earlier hold the old SMALL/MEDIUM/LARGE/EXTRA_LARGE slot in 0..3; no font is
+74 -16
View File
@@ -5,6 +5,8 @@
#include <cstdint>
#include "util/ShortcutAction.h"
class CrossPointSettings : public PersistableStore<CrossPointSettings> {
private:
// Private constructor for singleton
@@ -88,7 +90,33 @@ class CrossPointSettings : public PersistableStore<CrossPointSettings> {
// Side button layout options
// Default: Up = Previous, Down = Next
enum SIDE_BUTTON_LAYOUT { PREV_NEXT = 0, NEXT_PREV = 1, SIDE_BUTTONS_DISABLED = 2, SIDE_BUTTON_LAYOUT_COUNT };
// NEXT_NEXT makes both side buttons turn forward, for one-handed reading.
enum SIDE_BUTTON_LAYOUT {
PREV_NEXT = 0,
NEXT_PREV = 1,
SIDE_BUTTONS_DISABLED = 2,
NEXT_NEXT = 3,
SIDE_BUTTON_LAYOUT_COUNT
};
// How far front-button roles follow the reader orientation. NAV_BUTTONS rotates only the
// page-navigation pair; ALL_BUTTONS also mirrors Back/Confirm when the screen is inverted.
enum FRONT_BUTTON_ORIENTATION_AWARE {
FRONT_ORIENTATION_AWARE_OFF = 0,
FRONT_ORIENTATION_AWARE_NAV_BUTTONS = 1,
FRONT_ORIENTATION_AWARE_ALL_BUTTONS = 2,
FRONT_ORIENTATION_AWARE_COUNT
};
// Side button long-press action in the reader. Kept separate from longPressButtonBehavior so
// the side and front pairs can differ; the values deliberately match it, which is what lets
// the migration copy one into the other.
enum SIDE_LONG_PRESS {
SIDE_LONG_OFF = 0,
SIDE_LONG_CHAPTER_SKIP = 1,
SIDE_LONG_ORIENTATION_CHANGE = 2,
SIDE_LONG_PRESS_COUNT
};
// Font family options (built-in fonts only; SD card fonts use sdFontFamilyName)
enum FONT_FAMILY { NOTOSERIF = 0, NOTOSANS = 1, FONT_FAMILY_COUNT };
@@ -129,13 +157,8 @@ class CrossPointSettings : public PersistableStore<CrossPointSettings> {
REFRESH_FREQUENCY_COUNT
};
// Short power button press actions
enum SHORT_PWRBTN { IGNORE = 0, SLEEP = 1, PAGE_TURN = 2, FORCE_REFRESH = 3, FOOTNOTES = 4, SHORT_PWRBTN_COUNT };
// Long-press Confirm action while reading an EPUB. The setting cycles through these values.
// Persisted in settings.json by index: any new function (e.g. dictionary, bookmark) MUST use a
// value >= 2 and be appended at the END of the enumValues array in SettingsList.h, otherwise the
// stored indices shift and existing saves are silently misinterpreted.
// Legacy long-press Confirm setting, superseded by LONG_PRESS_MENU_ACTION.
// Retained only so fromJson() can migrate longPressMenuFunction into longPressMenuAction.
enum LONG_PRESS_MENU_FUNCTION {
LP_MENU_KOSYNC = 0,
LP_MENU_DISABLED = 1,
@@ -147,7 +170,7 @@ class CrossPointSettings : public PersistableStore<CrossPointSettings> {
// Hide battery percentage
enum HIDE_BATTERY_PERCENTAGE { HIDE_NEVER = 0, HIDE_READER = 1, HIDE_ALWAYS = 2, HIDE_BATTERY_PERCENTAGE_COUNT };
// Page turn button long press behavior
// Front navigation button long press behavior
enum LONG_PRESS_BUTTON_BEHAVIOR {
OFF = 0,
CHAPTER_SKIP = 1,
@@ -161,7 +184,22 @@ class CrossPointSettings : public PersistableStore<CrossPointSettings> {
// Image rendering in EPUB reader
enum IMAGE_RENDERING { IMAGES_DISPLAY = 0, IMAGES_PLACEHOLDER = 1, IMAGES_SUPPRESS = 2, IMAGE_RENDERING_COUNT };
enum TILT_PAGE_TURN { TILT_OFF = 0, TILT_NORMAL = 1, TILT_NVERTED = 2, TILT_PAGE_TURN_COUNT };
// Tilt page turn is an on/off switch since the gesture axis moved into
// tiltPageTurnDirection. TILT_NORMAL/TILT_NVERTED are the legacy combined values,
// kept for the fromJson() migration only.
enum TILT_PAGE_TURN { TILT_OFF = 0, TILT_ON = 1, TILT_PAGE_TURN_COUNT };
static constexpr uint8_t LEGACY_TILT_NORMAL = 1;
static constexpr uint8_t LEGACY_TILT_INVERTED = 2;
// Which flick turns the page forward. The *_RIGHT_LEFT / *_BACK_FORWARD variants
// negate the axis; LEFT_RIGHT reproduces the legacy TILT_NORMAL behaviour exactly.
enum TILT_PAGE_TURN_DIRECTION {
TILT_LEFT_RIGHT = 0,
TILT_RIGHT_LEFT = 1,
TILT_FORWARD_BACK = 2,
TILT_BACK_FORWARD = 3,
TILT_PAGE_TURN_DIRECTION_COUNT
};
enum TOUCH_READER_CONTROLS { TOUCH_READER_OFF = 0, TOUCH_READER_ON = 1, TOUCH_READER_CONTROLS_COUNT };
@@ -200,20 +238,34 @@ class CrossPointSettings : public PersistableStore<CrossPointSettings> {
uint8_t extraParagraphSpacing = 1;
uint8_t textAntiAliasing = 1;
// Short power button click behaviour
uint8_t shortPwrBtn = IGNORE;
uint8_t shortPwrBtn = shortcutActionRawValue(ShortcutAction::None);
// Long power button hold behaviour. Sleep matches what a hold has always done.
uint8_t longPwrBtn = shortcutActionRawValue(ShortcutAction::Sleep);
// EPUB reading orientation settings
// 0 = portrait (default), 1 = landscape clockwise, 2 = inverted, 3 = landscape counter-clockwise
uint8_t orientation = PORTRAIT;
// Button layouts (front layout retained for migration only)
uint8_t frontButtonLayout = BACK_CONFIRM_LEFT_RIGHT;
uint8_t sideButtonLayout = PREV_NEXT;
uint8_t frontButtonFollowOrientation = 0;
// Supersedes frontButtonFollowOrientation, which fromJson() migrates into it.
uint8_t frontButtonOrientationAware = FRONT_ORIENTATION_AWARE_OFF;
// Swap the side buttons in the reader whenever the reader is not portrait.
uint8_t sideButtonOrientationAware = 0;
// Reader long-press action for the side buttons.
uint8_t sideButtonLongPress = SIDE_LONG_OFF;
// 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-only front button remap. While readerFrontButtonsEnabled is 0 the reader uses the
// system mapping above, so the defaults here are inert until the user opts in.
uint8_t readerFrontButtonsEnabled = 0;
uint8_t readerFrontButtonBack = FRONT_HW_BACK;
uint8_t readerFrontButtonConfirm = FRONT_HW_CONFIRM;
uint8_t readerFrontButtonLeft = FRONT_HW_LEFT;
uint8_t readerFrontButtonRight = FRONT_HW_RIGHT;
// Reader font settings
uint8_t fontFamily = NOTOSERIF;
// Point size of the reader font. Only sizes the active family actually ships
@@ -245,9 +297,13 @@ class CrossPointSettings : public PersistableStore<CrossPointSettings> {
uint8_t hideBatteryPercentage = HIDE_NEVER;
// Long-press page turn button behavior
uint8_t longPressButtonBehavior = OFF;
// Long-press Confirm function in EPUB reader (cycles through LONG_PRESS_MENU_FUNCTION values).
// Defaults to Disabled so shortcut-based bookmark toggling remains opt-in.
uint8_t longPressMenuFunction = LP_MENU_DISABLED;
// Long-press Confirm quick action in the reader. Defaults to off so shortcut-based
// bookmark toggling stays opt-in. Migrated from the legacy longPressMenuFunction.
uint8_t longPressMenuAction = shortcutActionRawValue(ShortcutAction::None);
// Long-press Back quick action in the reader. The default reproduces the existing
// behaviour, where holding Back opens the file browser (or home, when
// backShortToFileBrowser has swapped the two).
uint8_t longPressBackAction = shortcutActionRawValue(ShortcutAction::FileBrowser);
// UI Theme
uint8_t uiTheme = LYRA;
// Sunlight fading compensation
@@ -274,6 +330,7 @@ class CrossPointSettings : public PersistableStore<CrossPointSettings> {
uint8_t imageRendering = IMAGES_DISPLAY;
// Tilt-based page turning (X3 only — requires QMI8658 IMU)
uint8_t tiltPageTurn = TILT_OFF;
uint8_t tiltPageTurnDirection = TILT_LEFT_RIGHT;
// Touch screen reader zones/gestures on boards with a touch controller.
uint8_t touchReaderControls = TOUCH_READER_ON;
// Language setting (Language enum index, default 0 = EN)
@@ -292,7 +349,7 @@ class CrossPointSettings : public PersistableStore<CrossPointSettings> {
void* sdFontResolverCtx = nullptr;
uint16_t getPowerButtonDuration() const {
return (shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::SLEEP) ? 10 : 400;
return shortPwrBtn == shortcutActionRawValue(ShortcutAction::Sleep) ? 10 : 400;
}
int getReaderFontId() const;
@@ -347,6 +404,7 @@ class CrossPointSettings : public PersistableStore<CrossPointSettings> {
bool fromJson(JsonVariantConst doc);
static void validateFrontButtonMapping(CrossPointSettings& settings);
static void validateReaderFrontButtonMapping(CrossPointSettings& settings);
static uint8_t sleepTimeoutEnumToMinutes(uint8_t legacyValue);
float getReaderLineCompression() const;
+97 -45
View File
@@ -13,7 +13,7 @@ bool MappedInputManager::isNavDirectionSwapped() const {
// setting. The reader (and its modal menus) render rotated, so navigation/labels flip there; the
// home and settings UI render in portrait, so they never flip even when a rotated reader is configured.
const auto orientation = renderer.getOrientation();
return SETTINGS.frontButtonFollowOrientation &&
return SETTINGS.frontButtonOrientationAware != CrossPointSettings::FRONT_ORIENTATION_AWARE_OFF &&
(orientation == GfxRenderer::PortraitInverted || orientation == GfxRenderer::LandscapeCounterClockwise);
}
@@ -44,61 +44,113 @@ MappedInputManager::Button MappedInputManager::mapScreenDirection(const Button b
return button;
}
const uint8_t orientation =
SETTINGS.frontButtonFollowOrientation ? static_cast<uint8_t>(renderer.getOrientation()) : 0;
const uint8_t orientation = SETTINGS.frontButtonOrientationAware != CrossPointSettings::FRONT_ORIENTATION_AWARE_OFF
? static_cast<uint8_t>(renderer.getOrientation())
: 0;
return directions[orientation][direction];
}
bool MappedInputManager::isSideDirectionSwapped() const {
// Keyed on the live renderer orientation for the same reason as isNavDirectionSwapped(): the
// reader and its modal menus render rotated, portrait UI does not.
return SETTINGS.sideButtonOrientationAware && renderer.getOrientation() != GfxRenderer::Portrait;
}
namespace {
// The four front buttons sit in one row, so turning the device over reverses their order.
uint8_t mirrorFrontButton(const uint8_t hardwareButton) {
return hardwareButton < CrossPointSettings::FRONT_BUTTON_HARDWARE_COUNT
? static_cast<uint8_t>(CrossPointSettings::FRONT_BUTTON_HARDWARE_COUNT - 1 - hardwareButton)
: hardwareButton;
}
uint8_t mirrorSideButton(const uint8_t hardwareButton) {
if (hardwareButton == HalGPIO::BTN_UP) return HalGPIO::BTN_DOWN;
if (hardwareButton == HalGPIO::BTN_DOWN) return HalGPIO::BTN_UP;
return hardwareButton;
}
// Side buttons bound to each SIDE_BUTTON_LAYOUT. NO_BUTTON means that direction has no side
// button; NEXT_NEXT is the one layout that binds two buttons to a single direction.
constexpr uint8_t NO_BUTTON = 0xFF;
struct SideLayout {
uint8_t pageBack;
uint8_t pageForward;
uint8_t pageForwardSecondary;
};
constexpr SideLayout SIDE_LAYOUTS[CrossPointSettings::SIDE_BUTTON_LAYOUT_COUNT] = {
{HalGPIO::BTN_UP, HalGPIO::BTN_DOWN, NO_BUTTON}, // PREV_NEXT
{HalGPIO::BTN_DOWN, HalGPIO::BTN_UP, NO_BUTTON}, // NEXT_PREV
{NO_BUTTON, NO_BUTTON, NO_BUTTON}, // SIDE_BUTTONS_DISABLED
{NO_BUTTON, HalGPIO::BTN_UP, HalGPIO::BTN_DOWN}, // NEXT_NEXT
};
} // namespace
int MappedInputManager::getFrontButtonFor(const Button button) const {
// A reader-only remap only applies inside a reader; everything else keeps the system mapping.
const bool useReaderMapping = readerMode && SETTINGS.readerFrontButtonsEnabled;
// ALL_BUTTONS keeps every front role under the same physical finger when the screen renders
// inverted. Only Back/Confirm are mirrored here — isNavDirectionSwapped() already flips the
// navigation pair, and mirroring those as well would cancel it out.
const bool mirror = SETTINGS.frontButtonOrientationAware == CrossPointSettings::FRONT_ORIENTATION_AWARE_ALL_BUTTONS &&
renderer.getOrientation() == GfxRenderer::PortraitInverted;
switch (button) {
case Button::Back: {
const uint8_t hw = useReaderMapping ? SETTINGS.readerFrontButtonBack : SETTINGS.frontButtonBack;
return mirror ? mirrorFrontButton(hw) : hw;
}
case Button::Confirm: {
const uint8_t hw = useReaderMapping ? SETTINGS.readerFrontButtonConfirm : SETTINGS.frontButtonConfirm;
return mirror ? mirrorFrontButton(hw) : hw;
}
case Button::Left:
return useReaderMapping ? SETTINGS.readerFrontButtonLeft : SETTINGS.frontButtonLeft;
case Button::Right:
return useReaderMapping ? SETTINGS.readerFrontButtonRight : SETTINGS.frontButtonRight;
default:
return -1;
}
}
bool MappedInputManager::mapButton(const Button button, bool (HalGPIO::*fn)(uint8_t) const) const {
const auto sideLayout = SETTINGS.sideButtonLayout;
const uint8_t sideLayout = SETTINGS.sideButtonLayout < CrossPointSettings::SIDE_BUTTON_LAYOUT_COUNT
? SETTINGS.sideButtonLayout
: CrossPointSettings::PREV_NEXT;
const SideLayout side = SIDE_LAYOUTS[sideLayout];
const bool swapSides = isSideDirectionSwapped();
// Swapping the pair is the same as swapping the two roles, and leaves NEXT_NEXT (which binds
// both buttons to the same direction) untouched.
const auto readSide = [&](const uint8_t primary, const uint8_t secondary) {
return (primary != NO_BUTTON && (gpio.*fn)(swapSides ? mirrorSideButton(primary) : primary)) ||
(secondary != NO_BUTTON && (gpio.*fn)(swapSides ? mirrorSideButton(secondary) : secondary));
};
switch (button) {
case Button::Back:
// Logical Back maps to user-configured front button.
return (gpio.*fn)(SETTINGS.frontButtonBack);
case Button::Confirm:
// Logical Confirm maps to user-configured front button.
return (gpio.*fn)(SETTINGS.frontButtonConfirm);
case Button::Left:
// Logical Left maps to user-configured front button.
return (gpio.*fn)(SETTINGS.frontButtonLeft);
case Button::Right:
// Logical Right maps to user-configured front button.
return (gpio.*fn)(SETTINGS.frontButtonRight);
case Button::Right: {
const int hw = getFrontButtonFor(button);
return hw >= 0 && (gpio.*fn)(static_cast<uint8_t>(hw));
}
case Button::Up:
// Side buttons remain fixed for Up/Down.
return (gpio.*fn)(HalGPIO::BTN_UP);
// Reader menus follow the same top/bottom side-button orientation as reader page turns.
return (gpio.*fn)(swapSides ? HalGPIO::BTN_DOWN : HalGPIO::BTN_UP);
case Button::Down:
// Side buttons remain fixed for Up/Down.
return (gpio.*fn)(HalGPIO::BTN_DOWN);
return (gpio.*fn)(swapSides ? HalGPIO::BTN_UP : 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.
switch (sideLayout) {
case CrossPointSettings::PREV_NEXT:
return (gpio.*fn)(HalGPIO::BTN_UP);
case CrossPointSettings::NEXT_PREV:
return (gpio.*fn)(HalGPIO::BTN_DOWN);
case CrossPointSettings::SIDE_BUTTONS_DISABLED:
default:
return false;
}
return readSide(side.pageBack, NO_BUTTON);
case Button::PageForward:
// Reader page navigation uses side buttons and can be swapped via settings.
switch (sideLayout) {
case CrossPointSettings::PREV_NEXT:
return (gpio.*fn)(HalGPIO::BTN_DOWN);
case CrossPointSettings::NEXT_PREV:
return (gpio.*fn)(HalGPIO::BTN_UP);
case CrossPointSettings::SIDE_BUTTONS_DISABLED:
default:
return false;
}
return readSide(side.pageForward, side.pageForwardSecondary);
case Button::NavNext:
// Logical "next item" navigation: side Down + front Right, with the control axis flipped in
// INVERTED / LANDSCAPE_CCW (frontButtonFollowOrientation) so it matches the rotated hint labels.
// INVERTED / LANDSCAPE_CCW (frontButtonOrientationAware) so it matches the rotated hint labels.
return isNavDirectionSwapped() ? (mapButton(Button::Up, fn) || mapButton(Button::Left, fn))
: (mapButton(Button::Down, fn) || mapButton(Button::Right, fn));
case Button::NavPrevious:
@@ -359,19 +411,19 @@ MappedInputManager::Labels MappedInputManager::mapDirectionalLabels(const char*
MappedInputManager::Labels MappedInputManager::mapFrontLabels(const char* back, const char* confirm, const char* left,
const char* right) const {
// 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) {
// Build the label order from the same resolved mapping mapButton() reads, so a reader-only
// remap or an inverted-screen mirror moves the hints along with the buttons.
auto labelForHardware = [&](int hw) -> const char* {
if (hw == getFrontButtonFor(Button::Back)) {
return back;
}
if (hw == SETTINGS.frontButtonConfirm) {
if (hw == getFrontButtonFor(Button::Confirm)) {
return confirm;
}
if (hw == SETTINGS.frontButtonLeft) {
if (hw == getFrontButtonFor(Button::Left)) {
return left;
}
if (hw == SETTINGS.frontButtonRight) {
if (hw == getFrontButtonFor(Button::Right)) {
return right;
}
return "";
+14
View File
@@ -34,6 +34,11 @@ class MappedInputManager {
MappedInputManager(HalGPIO& gpio, const GfxRenderer& renderer) : gpio(gpio), renderer(renderer) {}
// Reader activities can override the front-button mapping and swap the side buttons, neither of
// which is derivable from the renderer (a portrait reader renders exactly like portrait UI).
// main.cpp keeps this in sync with ActivityManager::isReaderActivity().
void setReaderMode(bool enabled) { readerMode = enabled; }
void update() const { gpio.update(); }
bool wasPressed(Button button) const;
bool wasReleased(Button button) const;
@@ -81,8 +86,17 @@ class MappedInputManager {
// so portrait UI (home, settings) never swaps while the reader and its menus do.
[[nodiscard]] bool isNavDirectionSwapped() const;
// True when the reader's side buttons are inverted top-for-bottom, because the user opted into
// orientation-aware side buttons and the screen is currently rendered non-portrait.
[[nodiscard]] bool isSideDirectionSwapped() const;
// Physical front button backing a logical front role, after the reader-only remap and the
// orientation mirror. Only Back/Confirm/Left/Right are front roles; anything else returns -1.
[[nodiscard]] int getFrontButtonFor(Button button) const;
private:
HalGPIO& gpio;
bool readerMode = false;
// Logical-to-physical button mapping depends on what the user is actually looking at: when the
// screen is rendered rotated, the directional buttons must flip to match. The renderer is the only
// authority on the *live* orientation (the reader rotates it and restores portrait on exit), so we
+264
View File
@@ -0,0 +1,264 @@
#pragma once
#include <I18n.h>
#include <cstddef>
#include <cstdint>
#include <functional>
#include <initializer_list>
#include <string>
#include <utility>
#include <vector>
#include "CrossPointSettings.h"
enum class SettingType { TOGGLE, ENUM, ACTION, VALUE, STRING, SUBMENU };
enum class SettingAction {
None,
RemapFrontButtons,
RemapFrontButtonsReader,
ControlsPowerButton,
ControlsFrontButtons,
ControlsSideButtons,
CustomiseStatusBar,
KOReaderSync,
OPDSBrowser,
Network,
ClearCache,
CheckForUpdates,
SdFirmwareUpdate,
Language,
DownloadFonts,
TextSettings,
};
struct SettingEnumOption {
StrId label;
uint8_t rawValue;
};
// Owns the relationship between a displayed option and its persisted value.
// Callers cannot add one without the other, so reordered and gapped enums stay coherent.
class SettingEnumOptions {
public:
SettingEnumOptions() = default;
explicit SettingEnumOptions(const std::initializer_list<StrId> labels) {
options.reserve(labels.size());
uint8_t rawValue = 0;
for (const StrId label : labels) {
add(label, rawValue++);
}
}
explicit SettingEnumOptions(const std::initializer_list<SettingEnumOption> mappedOptions,
const size_t additionalCapacity = 0) {
options.reserve(mappedOptions.size() + additionalCapacity);
for (const auto& option : mappedOptions) {
add(option.label, option.rawValue);
}
}
void reserve(const size_t count) { options.reserve(count); }
void add(const StrId label, const uint8_t rawValue) { options.push_back({label, rawValue}); }
[[nodiscard]] size_t size() const { return options.size(); }
[[nodiscard]] bool empty() const { return options.empty(); }
[[nodiscard]] StrId labelAt(const size_t displayIndex) const {
return displayIndex < options.size() ? options[displayIndex].label : StrId::STR_NONE_OPT;
}
[[nodiscard]] uint8_t displayIndexForRawValue(const uint8_t rawValue) const {
for (size_t i = 0; i < options.size(); i++) {
if (options[i].rawValue == rawValue) return static_cast<uint8_t>(i);
}
return 0;
}
[[nodiscard]] uint8_t rawValueForDisplayIndex(const uint8_t displayIndex) const {
if (options.empty()) return 0;
return displayIndex < options.size() ? options[displayIndex].rawValue : options.front().rawValue;
}
[[nodiscard]] bool containsRawValue(const uint8_t rawValue) const {
for (const auto& option : options) {
if (option.rawValue == rawValue) return true;
}
return false;
}
private:
std::vector<SettingEnumOption> options;
};
struct SettingInfo {
StrId nameId = StrId::STR_NONE_OPT;
SettingType type = SettingType::ACTION;
uint8_t CrossPointSettings::* valuePtr = nullptr;
SettingAction action = SettingAction::None;
struct ValueRange {
uint8_t min;
uint8_t max;
uint8_t step;
};
ValueRange valueRange = {};
const char* key = nullptr;
StrId category = StrId::STR_NONE_OPT;
bool obfuscated = false;
bool inTextSettings = false;
size_t stringOffset = 0;
size_t stringMaxLen = 0;
std::function<uint8_t()> valueGetter;
std::function<void(uint8_t)> valueSetter;
std::function<std::string()> stringGetter;
std::function<void(const std::string&)> stringSetter;
SettingInfo& withObfuscated() {
obfuscated = true;
return *this;
}
SettingInfo& withTextSettings() {
inTextSettings = true;
return *this;
}
void setEnumStringValues(std::vector<std::string> values) { enumStringValues = std::move(values); }
[[nodiscard]] bool hasEnumStringValues() const { return !enumStringValues.empty(); }
[[nodiscard]] const std::vector<std::string>& getEnumStringValues() const { return enumStringValues; }
[[nodiscard]] size_t enumOptionCount() const {
return enumStringValues.empty() ? enumOptions.size() : enumStringValues.size();
}
[[nodiscard]] StrId enumLabelAt(const size_t displayIndex) const { return enumOptions.labelAt(displayIndex); }
[[nodiscard]] uint8_t enumDisplayIndexForRawValue(const uint8_t rawValue) const {
return enumOptions.displayIndexForRawValue(rawValue);
}
[[nodiscard]] uint8_t enumRawValueForDisplayIndex(const uint8_t displayIndex) const {
return enumOptions.rawValueForDisplayIndex(displayIndex);
}
[[nodiscard]] bool isEnumRawValueAllowed(const uint8_t rawValue) const {
return enumOptions.containsRawValue(rawValue);
}
// OptionPopup outlives the selected list entry, so its callback owns a compact copy.
[[nodiscard]] SettingEnumOptions enumOptionsSnapshot() const { return enumOptions; }
static SettingInfo Toggle(StrId nameId, uint8_t CrossPointSettings::* ptr, const char* key = nullptr,
StrId category = StrId::STR_NONE_OPT) {
SettingInfo s;
s.nameId = nameId;
s.type = SettingType::TOGGLE;
s.valuePtr = ptr;
s.key = key;
s.category = category;
return s;
}
static SettingInfo Enum(StrId nameId, uint8_t CrossPointSettings::* ptr, std::initializer_list<StrId> labels,
const char* key = nullptr, StrId category = StrId::STR_NONE_OPT) {
SettingInfo s;
s.nameId = nameId;
s.type = SettingType::ENUM;
s.valuePtr = ptr;
s.enumOptions = SettingEnumOptions(labels);
s.key = key;
s.category = category;
return s;
}
static SettingInfo MappedEnum(StrId nameId, uint8_t CrossPointSettings::* ptr,
std::initializer_list<SettingEnumOption> options, const char* key = nullptr,
StrId category = StrId::STR_NONE_OPT) {
return MappedEnum(nameId, ptr, SettingEnumOptions(options), key, category);
}
static SettingInfo MappedEnum(StrId nameId, uint8_t CrossPointSettings::* ptr, SettingEnumOptions options,
const char* key = nullptr, StrId category = StrId::STR_NONE_OPT) {
SettingInfo s;
s.nameId = nameId;
s.type = SettingType::ENUM;
s.valuePtr = ptr;
s.enumOptions = std::move(options);
s.key = key;
s.category = category;
return s;
}
static SettingInfo Action(StrId nameId, SettingAction action) {
SettingInfo s;
s.nameId = nameId;
s.type = SettingType::ACTION;
s.action = action;
return s;
}
static SettingInfo Submenu(StrId nameId, SettingAction action) {
SettingInfo s;
s.nameId = nameId;
s.type = SettingType::SUBMENU;
s.action = action;
return s;
}
static SettingInfo Value(StrId nameId, uint8_t CrossPointSettings::* ptr, const ValueRange valueRange,
const char* key = nullptr, StrId category = StrId::STR_NONE_OPT) {
SettingInfo s;
s.nameId = nameId;
s.type = SettingType::VALUE;
s.valuePtr = ptr;
s.valueRange = valueRange;
s.key = key;
s.category = category;
return s;
}
static SettingInfo String(StrId nameId, char* ptr, size_t maxLen, const char* key = nullptr,
StrId category = StrId::STR_NONE_OPT) {
SettingInfo s;
s.nameId = nameId;
s.type = SettingType::STRING;
s.stringOffset = (size_t)ptr - (size_t)&SETTINGS;
s.stringMaxLen = maxLen;
s.key = key;
s.category = category;
return s;
}
static SettingInfo DynamicEnum(StrId nameId, std::initializer_list<StrId> labels, std::function<uint8_t()> getter,
std::function<void(uint8_t)> setter, const char* key = nullptr,
StrId category = StrId::STR_NONE_OPT) {
SettingInfo s = Enum(nameId, nullptr, labels, key, category);
s.valueGetter = std::move(getter);
s.valueSetter = std::move(setter);
return s;
}
static SettingInfo DynamicString(StrId nameId, std::function<std::string()> getter,
std::function<void(const std::string&)> setter, const char* key = nullptr,
StrId category = StrId::STR_NONE_OPT) {
SettingInfo s;
s.nameId = nameId;
s.type = SettingType::STRING;
s.stringGetter = std::move(getter);
s.stringSetter = std::move(setter);
s.key = key;
s.category = category;
return s;
}
private:
SettingEnumOptions enumOptions;
std::vector<std::string> enumStringValues;
};
+126 -81
View File
@@ -15,46 +15,38 @@
#include "CrossPointSettings.h"
#include "KOReaderCredentialStore.h"
#include "ReaderFontSizes.h"
#include "activities/settings/SettingsActivity.h"
#include "SettingInfo.h"
#include "util/DictionaryRegistry.h"
#include "util/ShortcutAction.h"
// Build the font family setting dynamically. When registry is non-null, SD card fonts
// are appended after the built-in fonts. Otherwise only built-in fonts are listed.
inline SettingInfo buildFontFamilySetting(const SdCardFontRegistry* registry) {
// Built-in font labels (StrId)
std::vector<StrId> enumValues = {StrId::STR_NOTO_SERIF, StrId::STR_NOTO_SANS};
// Runtime string labels for SD card fonts
std::vector<std::string> enumStringValues;
std::vector<std::string> sdFontLabels;
// Reserve: first CrossPointSettings::BUILTIN_FONT_COUNT entries use StrId, rest use strings
if (registry) {
const auto& families = registry->getFamilies();
enumStringValues.reserve(families.size());
std::transform(families.begin(), families.end(), std::back_inserter(enumStringValues),
sdFontLabels.reserve(families.size());
std::transform(families.begin(), families.end(), std::back_inserter(sdFontLabels),
[](const SdCardFontFamilyInfo& f) { return f.name; });
}
// Capture the SD font count for the lambdas
const int sdFontCount = static_cast<int>(enumStringValues.size());
const int sdFontCount = static_cast<int>(sdFontLabels.size());
// Total option count = built-in + SD card families
// For the combined enumStringValues: we need all entries as strings (built-in names + SD names)
// The render code checks enumStringValues first, then enumValues. So we build enumStringValues
// with all options when SD fonts are present.
// Runtime labels cover both built-in and SD options when SD fonts are present.
std::vector<std::string> allStringValues;
if (sdFontCount > 0) {
allStringValues.push_back(I18N.get(StrId::STR_NOTO_SERIF));
allStringValues.push_back(I18N.get(StrId::STR_NOTO_SANS));
allStringValues.insert(allStringValues.end(), enumStringValues.begin(), enumStringValues.end());
allStringValues.insert(allStringValues.end(), sdFontLabels.begin(), sdFontLabels.end());
}
SettingInfo s;
s.nameId = StrId::STR_FONT_FAMILY;
s.type = SettingType::ENUM;
s.enumValues = std::move(enumValues);
s.enumStringValues = std::move(allStringValues);
s.key = "fontFamily";
s.category = StrId::STR_CAT_READER;
SettingInfo s = SettingInfo::Enum(StrId::STR_FONT_FAMILY, nullptr, {StrId::STR_NOTO_SERIF, StrId::STR_NOTO_SANS},
"fontFamily", StrId::STR_CAT_READER);
s.setEnumStringValues(std::move(allStringValues));
s.inTextSettings = true; // matches the static font-family entry it replaces
// Capture registry families by copy for the lambdas
@@ -114,12 +106,8 @@ inline SettingInfo buildFontSizeSetting(const SdCardFontRegistry* registry) {
labels.push_back(std::to_string(pt) + " pt");
}
SettingInfo s;
s.nameId = StrId::STR_FONT_SIZE;
s.type = SettingType::ENUM;
s.enumStringValues = std::move(labels);
s.key = "fontSize";
s.category = StrId::STR_CAT_READER;
SettingInfo s = SettingInfo::Enum(StrId::STR_FONT_SIZE, nullptr, {}, "fontSize", StrId::STR_CAT_READER);
s.setEnumStringValues(std::move(labels));
s.inTextSettings = true; // matches the static font-size entry it replaces
s.valueGetter = [sizes]() -> uint8_t {
@@ -147,13 +135,13 @@ inline SettingInfo buildDictionarySetting(const std::vector<DictionaryEntry>& di
std::transform(dictionaries.begin(), dictionaries.end(), std::back_inserter(folderNames),
[](const DictionaryEntry& d) { return d.name; });
SettingInfo s;
s.nameId = StrId::STR_DICTIONARY;
s.type = SettingType::ENUM;
s.enumStringValues.reserve(folderNames.size() + 1);
s.enumStringValues.push_back(I18N.get(StrId::STR_NONE_OPT));
s.enumStringValues.insert(s.enumStringValues.end(), folderNames.begin(), folderNames.end());
s.category = StrId::STR_CAT_READER;
std::vector<std::string> labels;
labels.reserve(folderNames.size() + 1);
labels.push_back(I18N.get(StrId::STR_NONE_OPT));
labels.insert(labels.end(), folderNames.begin(), folderNames.end());
SettingInfo s = SettingInfo::Enum(StrId::STR_DICTIONARY, nullptr, {}, nullptr, StrId::STR_CAT_READER);
s.setEnumStringValues(std::move(labels));
s.valueGetter = [folderNames]() -> uint8_t {
for (size_t i = 0; i < folderNames.size(); i++) {
@@ -178,6 +166,49 @@ inline SettingInfo buildDictionarySetting(const std::vector<DictionaryEntry>& di
return s;
}
// Power-button shortcut options, shared by the short- and long-press settings.
//
// The display order is grouped by usefulness rather than by persisted value.
// Only actions the reader can execute today are listed; the enum carries no entry we cannot run.
inline SettingInfo buildPowerButtonActionSetting(StrId nameId, uint8_t CrossPointSettings::* valuePtr,
const char* key) {
SettingEnumOptions options({{StrId::STR_IGNORE, shortcutActionRawValue(ShortcutAction::None)},
{StrId::STR_SLEEP, shortcutActionRawValue(ShortcutAction::Sleep)},
{StrId::STR_PAGE_TURN, shortcutActionRawValue(ShortcutAction::PageTurn)},
{StrId::STR_FORCE_REFRESH, shortcutActionRawValue(ShortcutAction::RefreshScreen)},
{StrId::STR_BOOKMARK_OPTION, shortcutActionRawValue(ShortcutAction::ToggleBookmark)},
{StrId::STR_SYNC_PROGRESS, shortcutActionRawValue(ShortcutAction::SyncProgress)},
{StrId::STR_TAKE_SCREENSHOT, shortcutActionRawValue(ShortcutAction::Screenshot)},
{StrId::STR_FOOTNOTES, shortcutActionRawValue(ShortcutAction::Footnotes)},
{StrId::STR_BROWSE_FILES, shortcutActionRawValue(ShortcutAction::FileBrowser)},
{StrId::STR_FILE_TRANSFER, shortcutActionRawValue(ShortcutAction::FileTransfer)},
{StrId::STR_LOOK_UP_WORD, shortcutActionRawValue(ShortcutAction::LookUpWord)}},
1);
if (halTiltSensor.isAvailable()) {
options.add(StrId::STR_TOGGLE_TILT_PAGE_TURN, shortcutActionRawValue(ShortcutAction::ToggleTiltPageTurn));
}
return SettingInfo::MappedEnum(nameId, valuePtr, std::move(options), key, StrId::STR_CAT_CONTROLS);
}
// Reader long-press quick actions, shared by the Confirm and Back long-press settings.
inline SettingInfo buildLongPressActionSetting(StrId nameId, uint8_t CrossPointSettings::* valuePtr, const char* key) {
SettingEnumOptions options({{StrId::STR_STATE_OFF, shortcutActionRawValue(ShortcutAction::None)},
{StrId::STR_SLEEP, shortcutActionRawValue(ShortcutAction::Sleep)},
{StrId::STR_FORCE_REFRESH, shortcutActionRawValue(ShortcutAction::RefreshScreen)},
{StrId::STR_BOOKMARK_OPTION, shortcutActionRawValue(ShortcutAction::ToggleBookmark)},
{StrId::STR_SYNC_PROGRESS, shortcutActionRawValue(ShortcutAction::SyncProgress)},
{StrId::STR_TAKE_SCREENSHOT, shortcutActionRawValue(ShortcutAction::Screenshot)},
{StrId::STR_FOOTNOTES, shortcutActionRawValue(ShortcutAction::Footnotes)},
{StrId::STR_BROWSE_FILES, shortcutActionRawValue(ShortcutAction::FileBrowser)},
{StrId::STR_FILE_TRANSFER, shortcutActionRawValue(ShortcutAction::FileTransfer)},
{StrId::STR_LOOK_UP_WORD, shortcutActionRawValue(ShortcutAction::LookUpWord)}},
1);
if (halTiltSensor.isAvailable()) {
options.add(StrId::STR_TOGGLE_TILT_PAGE_TURN, shortcutActionRawValue(ShortcutAction::ToggleTiltPageTurn));
}
return SettingInfo::MappedEnum(nameId, valuePtr, std::move(options), key, StrId::STR_CAT_CONTROLS);
}
// Shared settings list used by both the device settings UI and the web settings API.
// Each entry has a key (for JSON API) and category (for grouping).
// ACTION-type entries and entries without a key are device-only.
@@ -191,26 +222,17 @@ inline SettingInfo buildDictionarySetting(const std::vector<DictionaryEntry>& di
inline std::vector<SettingInfo> getSettingsList(const SdCardFontRegistry* registry = nullptr,
const std::vector<DictionaryEntry>* dictionaries = nullptr) {
static const std::vector<SettingInfo> baseList = [] {
// Enum settings are persisted as numeric values. Assign these labels by enum
// value so a reordered menu or enum cannot silently swap their behavior.
std::vector<StrId> sleepScreenValues(CrossPointSettings::SLEEP_SCREEN_MODE_COUNT);
sleepScreenValues[CrossPointSettings::DARK] = StrId::STR_DARK;
sleepScreenValues[CrossPointSettings::LIGHT] = StrId::STR_LIGHT;
sleepScreenValues[CrossPointSettings::CUSTOM] = StrId::STR_CUSTOM;
sleepScreenValues[CrossPointSettings::COVER] = StrId::STR_COVER;
sleepScreenValues[CrossPointSettings::COVER_CUSTOM] = StrId::STR_COVER_CUSTOM;
sleepScreenValues[CrossPointSettings::BLANK] = StrId::STR_NONE_OPT;
sleepScreenValues[CrossPointSettings::QUICK_RESUME] = StrId::STR_QUICK_RESUME;
std::vector<StrId> statusBarClockValues(CrossPointSettings::STATUS_BAR_CLOCK_MODE_COUNT);
statusBarClockValues[CrossPointSettings::STATUS_BAR_CLOCK_HIDE] = StrId::STR_HIDE;
statusBarClockValues[CrossPointSettings::STATUS_BAR_CLOCK_RIGHT] = StrId::STR_DIR_RIGHT;
statusBarClockValues[CrossPointSettings::STATUS_BAR_CLOCK_LEFT] = StrId::STR_DIR_LEFT;
std::vector<SettingInfo> v = {
// --- Display ---
SettingInfo::Enum(StrId::STR_SLEEP_SCREEN, &CrossPointSettings::sleepScreen, std::move(sleepScreenValues),
"sleepScreen", StrId::STR_CAT_DISPLAY),
SettingInfo::MappedEnum(StrId::STR_SLEEP_SCREEN, &CrossPointSettings::sleepScreen,
{{StrId::STR_DARK, CrossPointSettings::DARK},
{StrId::STR_LIGHT, CrossPointSettings::LIGHT},
{StrId::STR_CUSTOM, CrossPointSettings::CUSTOM},
{StrId::STR_COVER, CrossPointSettings::COVER},
{StrId::STR_COVER_CUSTOM, CrossPointSettings::COVER_CUSTOM},
{StrId::STR_NONE_OPT, CrossPointSettings::BLANK},
{StrId::STR_QUICK_RESUME, CrossPointSettings::QUICK_RESUME}},
"sleepScreen", StrId::STR_CAT_DISPLAY),
SettingInfo::Enum(StrId::STR_SLEEP_COVER_MODE, &CrossPointSettings::sleepScreenCoverMode,
{StrId::STR_FIT, StrId::STR_CROP}, "sleepScreenCoverMode", StrId::STR_CAT_DISPLAY),
SettingInfo::Enum(StrId::STR_SLEEP_COVER_FILTER, &CrossPointSettings::sleepScreenCoverFilter,
@@ -278,30 +300,47 @@ inline std::vector<SettingInfo> getSettingsList(const SdCardFontRegistry* regist
SettingInfo::Enum(StrId::STR_IMAGES, &CrossPointSettings::imageRendering,
{StrId::STR_IMAGES_DISPLAY, StrId::STR_IMAGES_PLACEHOLDER, StrId::STR_IMAGES_SUPPRESS},
"imageRendering", StrId::STR_CAT_READER),
// --- Controls ---
SettingInfo::Enum(StrId::STR_SIDE_BTN_LAYOUT, &CrossPointSettings::sideButtonLayout,
{StrId::STR_PREV_NEXT, StrId::STR_NEXT_PREV, StrId::STR_DISABLED}, "sideButtonLayout",
StrId::STR_CAT_CONTROLS),
SettingInfo::Enum(StrId::STR_TOUCH_READER_CONTROLS, &CrossPointSettings::touchReaderControls,
{StrId::STR_STATE_OFF, StrId::STR_STATE_ON}, "touchReaderControls", StrId::STR_CAT_CONTROLS),
SettingInfo::Toggle(StrId::STR_FRONT_BTN_FOLLOW_ORIENTATION, &CrossPointSettings::frontButtonFollowOrientation,
"frontButtonFollowOrientation", StrId::STR_CAT_CONTROLS),
// --- Controls: Power Button ---
buildPowerButtonActionSetting(StrId::STR_SHORT_PRESS_ACTION, &CrossPointSettings::shortPwrBtn, "shortPwrBtn"),
buildPowerButtonActionSetting(StrId::STR_LONG_PRESS_ACTION, &CrossPointSettings::longPwrBtn, "longPwrBtn"),
SettingInfo::Toggle(StrId::STR_PWR_BTN_FOOTNOTE_BACK, &CrossPointSettings::pwrBtnFootnoteBack,
"pwrBtnFootnoteBack", StrId::STR_CAT_CONTROLS),
// --- Controls: Front Buttons ---
SettingInfo::Toggle(StrId::STR_READER_BUTTON_OVERRIDE, &CrossPointSettings::readerFrontButtonsEnabled,
"readerFrontButtonsEnabled", StrId::STR_CAT_CONTROLS),
SettingInfo::Enum(StrId::STR_ORIENTATION_AWARE, &CrossPointSettings::frontButtonOrientationAware,
{StrId::STR_NO, StrId::STR_ORIENT_NAV_BUTTONS, StrId::STR_ORIENT_ALL_BUTTONS},
"frontButtonOrientationAware", StrId::STR_CAT_CONTROLS),
SettingInfo::Enum(StrId::STR_LONG_PRESS_BEHAVIOR, &CrossPointSettings::longPressButtonBehavior,
{StrId::STR_LONG_PRESS_BEHAVIOR_OFF, StrId::STR_LONG_PRESS_BEHAVIOR_SKIP,
StrId::STR_LONG_PRESS_BEHAVIOR_ORIENTATION},
"longPressButtonBehavior", StrId::STR_CAT_CONTROLS),
SettingInfo::Enum(StrId::STR_LONG_PRESS_MENU, &CrossPointSettings::longPressMenuFunction,
{StrId::STR_KOSYNC, StrId::STR_DISABLED, StrId::STR_BOOKMARK_OPTION, StrId::STR_DICTIONARY},
"longPressMenuFunction", StrId::STR_CAT_CONTROLS),
SettingInfo::Enum(
StrId::STR_SHORT_PWR_BTN, &CrossPointSettings::shortPwrBtn,
{StrId::STR_IGNORE, StrId::STR_SLEEP, StrId::STR_PAGE_TURN, StrId::STR_FORCE_REFRESH, StrId::STR_FOOTNOTES},
"shortPwrBtn", StrId::STR_CAT_CONTROLS),
SettingInfo::Toggle(StrId::STR_PWR_BTN_FOOTNOTE_BACK, &CrossPointSettings::pwrBtnFootnoteBack,
"pwrBtnFootnoteBack", StrId::STR_CAT_CONTROLS),
buildLongPressActionSetting(StrId::STR_LONG_PRESS_MENU, &CrossPointSettings::longPressMenuAction,
"longPressMenuAction"),
buildLongPressActionSetting(StrId::STR_LONG_PRESS_BACK_ACTION, &CrossPointSettings::longPressBackAction,
"longPressBackAction"),
SettingInfo::Toggle(StrId::STR_BACK_SHORT_TO_FILE_BROWSER, &CrossPointSettings::backShortToFileBrowser,
"backShortToFileBrowser", StrId::STR_CAT_CONTROLS),
// --- Controls: Side Buttons ---
SettingInfo::MappedEnum(StrId::STR_SIDE_BTN_LAYOUT, &CrossPointSettings::sideButtonLayout,
{{StrId::STR_PREV_NEXT, CrossPointSettings::PREV_NEXT},
{StrId::STR_NEXT_PREV, CrossPointSettings::NEXT_PREV},
{StrId::STR_NEXT_NEXT, CrossPointSettings::NEXT_NEXT},
{StrId::STR_DISABLED, CrossPointSettings::SIDE_BUTTONS_DISABLED}},
"sideButtonLayout", StrId::STR_CAT_CONTROLS),
SettingInfo::Toggle(StrId::STR_ORIENTATION_AWARE, &CrossPointSettings::sideButtonOrientationAware,
"sideButtonOrientationAware", StrId::STR_CAT_CONTROLS),
SettingInfo::Enum(StrId::STR_LONG_PRESS_ACTION, &CrossPointSettings::sideButtonLongPress,
{StrId::STR_LONG_PRESS_BEHAVIOR_OFF, StrId::STR_LONG_PRESS_BEHAVIOR_SKIP,
StrId::STR_LONG_PRESS_BEHAVIOR_ORIENTATION},
"sideButtonLongPress", StrId::STR_CAT_CONTROLS),
// --- Controls: Touch ---
SettingInfo::Enum(StrId::STR_TOUCH_READER_CONTROLS, &CrossPointSettings::touchReaderControls,
{StrId::STR_STATE_OFF, StrId::STR_STATE_ON}, "touchReaderControls", StrId::STR_CAT_CONTROLS),
// --- System ---
SettingInfo::Value(
StrId::STR_TIME_TO_SLEEP, &CrossPointSettings::sleepTimeoutMinutes,
@@ -391,8 +430,11 @@ inline std::vector<SettingInfo> getSettingsList(const SdCardFontRegistry* regist
StrId::STR_CUSTOMISE_STATUS_BAR),
// Clock entries (web settings only; device UI uses ClockOffsetActivity for the offset).
// Range 0..104 = quarter-hour steps from UTC-12:00 to UTC+14:00, biased by 48.
SettingInfo::Enum(StrId::STR_CLOCK, &CrossPointSettings::statusBarClock, std::move(statusBarClockValues),
"statusBarClock", StrId::STR_CUSTOMISE_STATUS_BAR),
SettingInfo::MappedEnum(StrId::STR_CLOCK, &CrossPointSettings::statusBarClock,
{{StrId::STR_HIDE, CrossPointSettings::STATUS_BAR_CLOCK_HIDE},
{StrId::STR_DIR_RIGHT, CrossPointSettings::STATUS_BAR_CLOCK_RIGHT},
{StrId::STR_DIR_LEFT, CrossPointSettings::STATUS_BAR_CLOCK_LEFT}},
"statusBarClock", StrId::STR_CUSTOMISE_STATUS_BAR),
SettingInfo::Value(StrId::STR_CLOCK_UTC_OFFSET, &CrossPointSettings::clockUtcOffsetQ, {0, 104, 1},
"clockUtcOffsetQ", StrId::STR_CUSTOMISE_STATUS_BAR),
SettingInfo::Enum(StrId::STR_CLOCK_FORMAT, &CrossPointSettings::clockFormat,
@@ -403,17 +445,14 @@ inline std::vector<SettingInfo> getSettingsList(const SdCardFontRegistry* regist
SettingInfo::Toggle(StrId::STR_CLOCK_SYNCED, &CrossPointSettings::clockHasBeenSynced, "clockHasBeenSynced",
StrId::STR_CUSTOMISE_STATUS_BAR),
};
// Only show tilt page turn setting when the QMI8658 IMU is present (X3)
// Tilt page turning needs an IMU (X3), so both entries only exist on those boards.
if (halTiltSensor.isAvailable()) {
// Insert after the short power button setting (end of Controls section)
for (auto it = v.begin(); it != v.end(); ++it) {
if (it->nameId == StrId::STR_SHORT_PWR_BTN) {
v.insert(it + 1, SettingInfo::Enum(StrId::STR_TILT_PAGE_TURN, &CrossPointSettings::tiltPageTurn,
{StrId::STR_STATE_OFF, StrId::STR_NORMAL, StrId::STR_INVERTED},
"tiltPageTurn", StrId::STR_CAT_CONTROLS));
break;
}
}
v.push_back(SettingInfo::Toggle(StrId::STR_TILT_PAGE_TURN, &CrossPointSettings::tiltPageTurn, "tiltPageTurn",
StrId::STR_CAT_CONTROLS));
v.push_back(SettingInfo::Enum(StrId::STR_TILT_DIRECTION, &CrossPointSettings::tiltPageTurnDirection,
{StrId::STR_TILT_LEFT_RIGHT, StrId::STR_TILT_RIGHT_LEFT,
StrId::STR_TILT_FORWARD_BACK, StrId::STR_TILT_BACK_FORWARD},
"tiltPageTurnDirection", StrId::STR_CAT_CONTROLS));
}
return v;
}();
@@ -425,10 +464,16 @@ inline std::vector<SettingInfo> getSettingsList(const SdCardFontRegistry* regist
v.end());
}
if (BoardConfig::hasTouch()) {
// Touch devices have no front buttons, so the entries that only remap or bind them are
// meaningless there. longPressButtonBehavior deliberately stays: it also governs a long
// press on the reader's touch page-turn zones.
v.erase(std::remove_if(v.begin(), v.end(),
[](const SettingInfo& s) {
return s.nameId == StrId::STR_FRONT_BTN_FOLLOW_ORIENTATION ||
s.nameId == StrId::STR_SUNLIGHT_FADING_FIX;
return s.nameId == StrId::STR_SUNLIGHT_FADING_FIX ||
s.valuePtr == &CrossPointSettings::frontButtonOrientationAware ||
s.valuePtr == &CrossPointSettings::readerFrontButtonsEnabled ||
s.valuePtr == &CrossPointSettings::longPressMenuAction ||
s.valuePtr == &CrossPointSettings::longPressBackAction;
}),
v.end());
}
+89 -57
View File
@@ -526,40 +526,14 @@ void EpubReaderActivity::loop() {
}
}
// Long-press Confirm runs the user-selected function (SETTINGS.longPressMenuFunction).
if (mappedInput.isPressed(MappedInputManager::Button::Confirm)) {
switch (SETTINGS.longPressMenuFunction) {
case CrossPointSettings::LP_MENU_BOOKMARK:
// Hold ~0.4s drops a bookmark at the current page.
if (mappedInput.getHeldTime() >= ReaderUtils::BOOKMARK_HOLD_MS && !showBookmarkMessage) {
addBookmark();
showBookmarkMessage = true;
ignoreNextConfirmRelease = true; // Prevent accidental menu open after adding bookmark
bookmarkMessageTime = millis();
requestUpdate();
}
break;
case CrossPointSettings::LP_MENU_KOSYNC:
// Hold ~1s launches KOReader sync. If sync can't run (no credentials stored), fall
// through so the normal Confirm-release still opens the reader menu.
if (mappedInput.getHeldTime() >= ReaderUtils::GO_HOME_MS) {
if (launchKOReaderSync()) {
ignoreNextConfirmRelease = true; // sync launched or error shown; suppress menu open
return;
}
}
break;
case CrossPointSettings::LP_MENU_DICTIONARY:
// Hold ~0.4s starts dictionary word selection on the current page.
if (mappedInput.getHeldTime() >= ReaderUtils::BOOKMARK_HOLD_MS && !showDictionaryMessage) {
ignoreNextConfirmRelease = true; // Prevent menu open on the release that follows
openDictionaryWordSelect();
return;
}
break;
case CrossPointSettings::LP_MENU_DISABLED:
default:
break;
// Long-press Confirm runs the user-selected quick action (SETTINGS.longPressMenuAction).
// If the action declines (sync with no credentials stored, no footnotes on the page) the
// Confirm release is left alone, so it still opens the reader menu as usual.
if (mappedInput.isPressed(MappedInputManager::Button::Confirm) && !ignoreNextConfirmRelease &&
mappedInput.getHeldTime() >= ReaderUtils::BOOKMARK_HOLD_MS) {
if (runReaderShortcut(shortcutActionFromRawValue(SETTINGS.longPressMenuAction))) {
ignoreNextConfirmRelease = true;
return;
}
}
@@ -570,6 +544,24 @@ void EpubReaderActivity::loop() {
return;
}
// Long-press Back runs the configured quick action. FILE_BROWSER means "leave it alone" —
// that is the navigation handleBackNavigation() already performs, together with
// backShortToFileBrowser.
if (SETTINGS.longPressBackAction != shortcutActionRawValue(ShortcutAction::FileBrowser)) {
if (mappedInput.isPressed(MappedInputManager::Button::Back) &&
mappedInput.getHeldTime() >= ReaderUtils::GO_BACK_OR_HOME_MS) {
if (!backActionFired) {
backActionFired = true;
runReaderShortcut(shortcutActionFromRawValue(SETTINGS.longPressBackAction));
}
return; // Absorb the rest of the hold so it cannot also navigate.
}
if (mappedInput.wasReleased(MappedInputManager::Button::Back) && backActionFired) {
backActionFired = false;
return;
}
}
if (ReaderUtils::handleBackNavigation(mappedInput, activityManager, epub ? epub->getPath().c_str() : "",
{this, [](void* ctx) { static_cast<EpubReaderActivity*>(ctx)->onGoHome(); }})) {
return;
@@ -577,31 +569,19 @@ void EpubReaderActivity::loop() {
// auto [prevTriggered, nextTriggered] = ReaderUtils::detectPageTurn(mappedInput);
// Handle short power button press for footnotes
if (SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::FOOTNOTES &&
mappedInput.wasReleased(MappedInputManager::Button::Power) &&
// Reader-only power button shortcuts. The globally-available ones are dispatched from the
// main loop; these need an open book, so they are handled here.
if (mappedInput.wasReleased(MappedInputManager::Button::Power) &&
!mappedInput.wasReleased(MappedInputManager::Button::Down)) {
if (footnoteDepth > 0) {
restoreSavedPosition();
} else {
if (currentPageFootnotes.size() == 1) {
navigateToHref(currentPageFootnotes[0].href, true);
} else if (currentPageFootnotes.size() > 1) {
startActivityForResult(
std::make_unique<EpubReaderFootnotesActivity>(renderer, mappedInput, currentPageFootnotes),
[this](const ActivityResult& result) {
if (!result.isCancelled) {
const auto& footnoteResult = std::get<FootnoteResult>(result.data);
navigateToHref(footnoteResult.href, true);
}
requestUpdate();
});
}
const auto shortAction = shortcutActionFromRawValue(SETTINGS.shortPwrBtn);
const bool wasHold = mappedInput.getHeldTime() > SETTINGS.getPowerButtonDuration();
const auto action = wasHold ? shortcutActionFromRawValue(SETTINGS.longPwrBtn) : shortAction;
if (!isShortcutAvailableOutsideReader(action) && runReaderShortcut(action)) {
return;
}
return;
}
auto [prevTriggered, nextTriggered, fromTilt] = ReaderUtils::detectPageTurn(mappedInput);
auto [prevTriggered, nextTriggered, fromTilt, fromSide] = ReaderUtils::detectPageTurn(mappedInput);
prevTriggered = prevTriggered || touch.prev;
nextTriggered = nextTriggered || touch.next;
if (!prevTriggered && !nextTriggered) {
@@ -629,13 +609,16 @@ void EpubReaderActivity::loop() {
const unsigned long heldMs = (touch.prev || touch.next) ? touch.heldMs : mappedInput.getHeldTime();
const bool longPress = !fromTilt && heldMs > ReaderUtils::SKIP_HOLD_MS;
// The side and front pairs carry separate long-press bindings; the values line up, so the
// behaviour below only needs to know which of the two to read.
const uint8_t longPressBehavior = fromSide ? SETTINGS.sideButtonLongPress : SETTINGS.longPressButtonBehavior;
// Don't skip chapter after screenshot
if (gpio.wasReleased(HalGPIO::BTN_POWER) && gpio.wasReleased(HalGPIO::BTN_DOWN)) {
return;
}
if (longPress && SETTINGS.longPressButtonBehavior == SETTINGS.CHAPTER_SKIP) {
if (longPress && longPressBehavior == SETTINGS.CHAPTER_SKIP) {
if (!nextTriggered && section && section->currentPage > 0) {
section->currentPage = 0;
requestUpdate();
@@ -657,7 +640,7 @@ void EpubReaderActivity::loop() {
return;
}
if (longPress && SETTINGS.longPressButtonBehavior == SETTINGS.ORIENTATION_CHANGE) {
if (longPress && longPressBehavior == SETTINGS.ORIENTATION_CHANGE) {
const uint8_t newOrientation =
nextTriggered ? (SETTINGS.orientation - 1 + SETTINGS.ORIENTATION_COUNT) % SETTINGS.ORIENTATION_COUNT
: (SETTINGS.orientation + 1) % SETTINGS.ORIENTATION_COUNT;
@@ -1945,6 +1928,55 @@ void EpubReaderActivity::restoreSavedPosition() {
requestUpdate();
}
void EpubReaderActivity::openOrReturnFromFootnotes() {
// While a footnote is open the shortcut doubles as a way back out, unless the user turned
// that off and wants it to keep opening footnotes instead.
if (footnoteDepth > 0 && SETTINGS.pwrBtnFootnoteBack) {
restoreSavedPosition();
return;
}
if (currentPageFootnotes.size() == 1) {
navigateToHref(currentPageFootnotes[0].href, true);
return;
}
if (currentPageFootnotes.size() > 1) {
startActivityForResult(std::make_unique<EpubReaderFootnotesActivity>(renderer, mappedInput, currentPageFootnotes),
[this](const ActivityResult& result) {
if (!result.isCancelled) {
const auto& footnoteResult = std::get<FootnoteResult>(result.data);
navigateToHref(footnoteResult.href, true);
}
requestUpdate();
});
}
}
bool EpubReaderActivity::runReaderShortcut(const ShortcutAction action) {
switch (action) {
case ShortcutAction::Footnotes:
// Nothing to open and nothing to return from: let the button keep its normal meaning.
if (currentPageFootnotes.empty() && footnoteDepth == 0) return false;
openOrReturnFromFootnotes();
return true;
case ShortcutAction::ToggleBookmark:
addBookmark();
showBookmarkMessage = true;
bookmarkMessageTime = millis();
requestUpdate();
return true;
case ShortcutAction::SyncProgress:
// Returns false when no KOReader credentials are stored, which is a no-op worth
// falling through from rather than swallowing the press.
return launchKOReaderSync();
case ShortcutAction::LookUpWord:
if (showDictionaryMessage) return false;
openDictionaryWordSelect();
return true;
default:
return runGlobalShortcut(action, renderer);
}
}
void EpubReaderActivity::loadCachedBookmarks() {
cachedBookmarks.clear();
if (cachedBookmarks.capacity() < initialBookmarkCacheCapacity) {
@@ -10,6 +10,7 @@
#include "EpubReaderMenuActivity.h"
#include "ProgressMapper.h"
#include "activities/Activity.h"
#include "util/ShortcutAction.h"
class EpubReaderActivity final : public Activity {
std::shared_ptr<Epub> epub;
@@ -192,6 +193,16 @@ class EpubReaderActivity final : public Activity {
// Footnote navigation
void navigateToHref(const std::string& href, bool savePosition = false);
void restoreSavedPosition();
// Opens the footnotes on this page, or returns from one when pwrBtnFootnoteBack is on.
void openOrReturnFromFootnotes();
// Runs a configured shortcut. Reader-only actions are handled here; the rest fall through to
// runGlobalShortcut(). Returns false when the action did nothing, so the caller can let the
// triggering button keep its normal meaning.
bool runReaderShortcut(ShortcutAction action);
// Set while a long Back hold has already run its action, so the release does not navigate.
bool backActionFired = false;
public:
explicit EpubReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::unique_ptr<Epub> epub,
+24 -12
View File
@@ -9,6 +9,7 @@
#include "MappedInputManager.h"
#include "activities/ActivityManager.h"
#include "util/ShortcutAction.h"
namespace ReaderUtils {
@@ -46,26 +47,37 @@ struct PageTurnResult {
bool prev;
bool next;
bool fromTilt;
// True when only the side buttons contributed. The side and front pairs have separate
// long-press settings, so the caller needs to know which one produced this turn.
bool fromSide;
};
inline PageTurnResult detectPageTurn(const MappedInputManager& input) {
const bool usePress = SETTINGS.longPressButtonBehavior == SETTINGS.OFF;
// A pair fires on press only when it has no long-press action to wait for.
const bool sideUsePress = SETTINGS.sideButtonLongPress == CrossPointSettings::SIDE_LONG_OFF;
const bool frontUsePress = SETTINGS.longPressButtonBehavior == CrossPointSettings::OFF;
const bool tiltNext = SETTINGS.tiltPageTurn && halTiltSensor.wasTiltedForward();
const bool tiltPrev = SETTINGS.tiltPageTurn && halTiltSensor.wasTiltedBack();
const bool swapFront = input.isNavDirectionSwapped();
const auto prevButton = swapFront ? MappedInputManager::Button::Right : MappedInputManager::Button::Left;
const auto nextButton = swapFront ? MappedInputManager::Button::Left : MappedInputManager::Button::Right;
const bool prev =
tiltPrev ||
(usePress ? (input.wasPressed(MappedInputManager::Button::PageBack) || input.wasPressed(prevButton))
: (input.wasReleased(MappedInputManager::Button::PageBack) || input.wasReleased(prevButton)));
const bool powerTurn = SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::PAGE_TURN &&
input.wasReleased(MappedInputManager::Button::Power);
const bool next = tiltNext || (usePress ? (input.wasPressed(MappedInputManager::Button::PageForward) || powerTurn ||
input.wasPressed(nextButton))
: (input.wasReleased(MappedInputManager::Button::PageForward) || powerTurn ||
input.wasReleased(nextButton)));
return {prev, next, tiltPrev || tiltNext};
const bool sidePrev = sideUsePress ? input.wasPressed(MappedInputManager::Button::PageBack)
: input.wasReleased(MappedInputManager::Button::PageBack);
const bool sideNext = sideUsePress ? input.wasPressed(MappedInputManager::Button::PageForward)
: input.wasReleased(MappedInputManager::Button::PageForward);
const bool powerReleased = input.wasReleased(MappedInputManager::Button::Power);
const uint8_t pageTurnAction = shortcutActionRawValue(ShortcutAction::PageTurn);
const bool powerTurn =
powerReleased && (SETTINGS.shortPwrBtn == pageTurnAction || SETTINGS.longPwrBtn == pageTurnAction);
const bool frontPrev = frontUsePress ? input.wasPressed(prevButton) : input.wasReleased(prevButton);
const bool frontNext = (frontUsePress ? input.wasPressed(nextButton) : input.wasReleased(nextButton)) || powerTurn;
const bool fromSide = (sidePrev || sideNext) && !(frontPrev || frontNext);
return {tiltPrev || sidePrev || frontPrev, tiltNext || sideNext || frontNext, tiltPrev || tiltNext, fromSide};
}
struct TouchPageTurn {
+1 -1
View File
@@ -66,7 +66,7 @@ void TxtReaderActivity::loop() {
}
const auto touch = ReaderUtils::detectTouchPageTurn(renderer, mappedInput);
auto [prevTriggered, nextTriggered, fromTilt] = ReaderUtils::detectPageTurn(mappedInput);
auto [prevTriggered, nextTriggered, fromTilt, fromSide] = ReaderUtils::detectPageTurn(mappedInput);
prevTriggered = prevTriggered || touch.prev;
nextTriggered = nextTriggered || touch.next;
if (!prevTriggered && !nextTriggered) {
+4 -2
View File
@@ -108,7 +108,7 @@ void XtcReaderActivity::loop() {
return;
}
auto [prevTriggered, nextTriggered, fromTilt] = ReaderUtils::detectPageTurn(mappedInput);
auto [prevTriggered, nextTriggered, fromTilt, fromSide] = ReaderUtils::detectPageTurn(mappedInput);
prevTriggered = prevTriggered || touch.prev;
nextTriggered = nextTriggered || touch.next;
if (!prevTriggered && !nextTriggered) {
@@ -134,7 +134,9 @@ void XtcReaderActivity::loop() {
const unsigned long heldMs = (touch.prev || touch.next) ? touch.heldMs : mappedInput.getHeldTime();
const bool skipPages =
!fromTilt && SETTINGS.longPressButtonBehavior == SETTINGS.CHAPTER_SKIP && heldMs > ReaderUtils::SKIP_HOLD_MS;
!fromTilt &&
(fromSide ? SETTINGS.sideButtonLongPress : SETTINGS.longPressButtonBehavior) == SETTINGS.CHAPTER_SKIP &&
heldMs > ReaderUtils::SKIP_HOLD_MS;
const int skipAmount = skipPages ? 10 : 1;
if (prevTriggered) {
@@ -47,10 +47,11 @@ void ButtonRemapActivity::loop() {
// - 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;
const auto fields = targetFields();
SETTINGS.*fields[0] = CrossPointSettings::FRONT_HW_BACK;
SETTINGS.*fields[1] = CrossPointSettings::FRONT_HW_CONFIRM;
SETTINGS.*fields[2] = CrossPointSettings::FRONT_HW_LEFT;
SETTINGS.*fields[3] = CrossPointSettings::FRONT_HW_RIGHT;
SETTINGS.saveToFile();
finish();
return;
@@ -149,12 +150,21 @@ void ButtonRemapActivity::render(RenderLock&&) {
renderer.displayBuffer();
}
std::array<uint8_t CrossPointSettings::*, 4> ButtonRemapActivity::targetFields() const {
if (target == Target::Reader) {
return {&CrossPointSettings::readerFrontButtonBack, &CrossPointSettings::readerFrontButtonConfirm,
&CrossPointSettings::readerFrontButtonLeft, &CrossPointSettings::readerFrontButtonRight};
}
return {&CrossPointSettings::frontButtonBack, &CrossPointSettings::frontButtonConfirm,
&CrossPointSettings::frontButtonLeft, &CrossPointSettings::frontButtonRight};
}
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];
const auto fields = targetFields();
for (uint8_t i = 0; i < kRoleCount; i++) {
SETTINGS.*fields[i] = tempMapping[i];
}
}
bool ButtonRemapActivity::validateUnassigned(const uint8_t pressedButton) {
+11 -2
View File
@@ -1,14 +1,20 @@
#pragma once
#include <array>
#include <functional>
#include <string>
#include "CrossPointSettings.h"
#include "activities/Activity.h"
class ButtonRemapActivity final : public Activity {
public:
explicit ButtonRemapActivity(GfxRenderer& renderer, MappedInputManager& mappedInput)
: Activity("ButtonRemap", renderer, mappedInput) {}
// Which of the two front-button mappings this run edits: the system-wide one, or the
// reader-only override that shadows it while a book is open.
enum class Target : uint8_t { System, Reader };
explicit ButtonRemapActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, Target target = Target::System)
: Activity("ButtonRemap", renderer, mappedInput), target(target) {}
void onEnter() override;
void onExit() override;
@@ -18,6 +24,9 @@ class ButtonRemapActivity final : public Activity {
private:
// Rendering task state.
Target target;
// The four role fields this run writes, in Back/Confirm/Left/Right order.
std::array<uint8_t CrossPointSettings::*, 4> targetFields() const;
// Index of the logical role currently awaiting input.
uint8_t currentStep = 0;
// Temporary mapping from logical role -> hardware button index.
+206 -60
View File
@@ -26,6 +26,22 @@
#include "activities/util/IntervalSelectionActivity.h"
#include "components/UITheme.h"
#include "fontIds.h"
#include "util/ShortcutAction.h"
namespace {
std::vector<std::string> buildEnumDisplayLabels(const SettingInfo& setting) {
if (setting.hasEnumStringValues()) return setting.getEnumStringValues();
std::vector<std::string> labels;
labels.reserve(setting.enumOptionCount());
for (size_t i = 0; i < setting.enumOptionCount(); i++) {
labels.emplace_back(I18N.get(setting.enumLabelAt(i)));
}
return labels;
}
} // namespace
const StrId SettingsActivity::categoryNames[categoryCount] = {StrId::STR_CAT_DISPLAY, StrId::STR_CAT_READER,
StrId::STR_CAT_CONTROLS, StrId::STR_CAT_SYSTEM};
@@ -55,10 +71,7 @@ void SettingsActivity::rebuildSettingsLists() {
if (setting.inTextSettings) continue;
readerSettings.push_back(setting);
} else if (setting.category == StrId::STR_CAT_CONTROLS) {
if (setting.valuePtr == &CrossPointSettings::pwrBtnFootnoteBack &&
SETTINGS.shortPwrBtn != CrossPointSettings::SHORT_PWRBTN::FOOTNOTES) {
continue;
}
// Grouping and per-setting visibility are decided in buildControlsLists().
controlsSettings.push_back(setting);
} else if (setting.category == StrId::STR_CAT_SYSTEM) {
systemSettings.push_back(setting);
@@ -69,6 +82,12 @@ void SettingsActivity::rebuildSettingsLists() {
if (!BoardConfig::hasTouch()) {
controlsSettings.insert(controlsSettings.begin(),
SettingInfo::Action(StrId::STR_REMAP_FRONT_BUTTONS, SettingAction::RemapFrontButtons));
// The reader override is only worth offering once it is switched on.
if (SETTINGS.readerFrontButtonsEnabled) {
controlsSettings.insert(
controlsSettings.begin() + 1,
SettingInfo::Action(StrId::STR_REMAP_BUTTONS_READER, SettingAction::RemapFrontButtonsReader));
}
}
systemSettings.push_back(SettingInfo::Action(StrId::STR_WIFI_NETWORKS, SettingAction::Network));
systemSettings.push_back(SettingInfo::Action(StrId::STR_KOREADER_SYNC, SettingAction::KOReaderSync));
@@ -86,30 +105,149 @@ void SettingsActivity::rebuildSettingsLists() {
SettingInfo::Action(StrId::STR_MANAGE_FONTS, SettingAction::DownloadFonts));
readerSettings.push_back(SettingInfo::Action(StrId::STR_CUSTOMISE_STATUS_BAR, SettingAction::CustomiseStatusBar));
// Update currentSettings pointer and count for the active category
switch (selectedCategoryIndex) {
case 0:
currentSettings = &displaySettings;
buildControlsLists();
applyCurrentSettingsList();
}
// Splits the flat Controls category into the grouped lists the UI navigates: a parent list of
// submenu rows plus one list per group. Entries are moved out of controlsSettings, so anything
// left there stays at the top level.
void SettingsActivity::buildControlsLists() {
controlsPowerSettings.clear();
controlsFrontButtonSettings.clear();
controlsSideButtonSettings.clear();
const bool hasFrontButtons = !BoardConfig::hasTouch();
// Quick-return from footnotes only means anything while some shortcut can open them.
const uint8_t footnotesAction = shortcutActionRawValue(ShortcutAction::Footnotes);
const bool anyFootnoteShortcut = SETTINGS.shortPwrBtn == footnotesAction || SETTINGS.longPwrBtn == footnotesAction ||
SETTINGS.longPressMenuAction == footnotesAction ||
SETTINGS.longPressBackAction == footnotesAction;
// Quick-return from footnotes is the one Controls entry that hides itself: it is meaningless
// unless a shortcut can open footnotes in the first place.
auto isHidden = [&](const SettingInfo& s) {
return s.valuePtr == &CrossPointSettings::pwrBtnFootnoteBack && !anyFootnoteShortcut;
};
// Returns the group a setting belongs in, or nullptr to leave it at the category's top level.
auto groupFor = [&](const SettingInfo& s) -> std::vector<SettingInfo>* {
if (s.valuePtr == &CrossPointSettings::shortPwrBtn || s.valuePtr == &CrossPointSettings::longPwrBtn ||
s.valuePtr == &CrossPointSettings::pwrBtnFootnoteBack) {
return &controlsPowerSettings;
}
if (s.valuePtr == &CrossPointSettings::sideButtonLayout ||
s.valuePtr == &CrossPointSettings::sideButtonOrientationAware ||
s.valuePtr == &CrossPointSettings::sideButtonLongPress) {
return &controlsSideButtonSettings;
}
// Without front buttons there is no Front Buttons group, so anything that survived the
// device filter in SettingsList (longPressButtonBehavior, which also covers touch
// page-turn zones) stays at the top level.
if (!hasFrontButtons) return nullptr;
if (s.valuePtr == &CrossPointSettings::frontButtonOrientationAware ||
s.valuePtr == &CrossPointSettings::readerFrontButtonsEnabled ||
s.valuePtr == &CrossPointSettings::longPressButtonBehavior ||
s.valuePtr == &CrossPointSettings::longPressMenuAction ||
s.valuePtr == &CrossPointSettings::longPressBackAction ||
s.valuePtr == &CrossPointSettings::backShortToFileBrowser || s.action == SettingAction::RemapFrontButtons ||
s.action == SettingAction::RemapFrontButtonsReader) {
return &controlsFrontButtonSettings;
}
return nullptr;
};
std::vector<SettingInfo> topLevel;
topLevel.reserve(controlsSettings.size());
for (auto& setting : controlsSettings) {
if (isHidden(setting)) continue;
auto* group = groupFor(setting);
if (group) {
group->push_back(std::move(setting));
} else {
topLevel.push_back(std::move(setting));
}
}
controlsSettings.clear();
controlsSettings.reserve(topLevel.size() + 3);
controlsSettings.push_back(SettingInfo::Submenu(StrId::STR_POWER_BUTTON, SettingAction::ControlsPowerButton));
if (hasFrontButtons) {
controlsSettings.push_back(SettingInfo::Submenu(StrId::STR_FRONT_BUTTONS, SettingAction::ControlsFrontButtons));
}
controlsSettings.push_back(SettingInfo::Submenu(StrId::STR_SIDE_BUTTONS, SettingAction::ControlsSideButtons));
for (auto& setting : topLevel) {
controlsSettings.push_back(std::move(setting));
}
}
void SettingsActivity::applyCurrentSettingsList() {
switch (activeSubmenu) {
case SettingAction::ControlsPowerButton:
currentSettings = &controlsPowerSettings;
break;
case 1:
currentSettings = &readerSettings;
case SettingAction::ControlsFrontButtons:
currentSettings = &controlsFrontButtonSettings;
break;
case 2:
currentSettings = &controlsSettings;
case SettingAction::ControlsSideButtons:
currentSettings = &controlsSideButtonSettings;
break;
case 3:
currentSettings = &systemSettings;
default:
switch (selectedCategoryIndex) {
case 0:
currentSettings = &displaySettings;
break;
case 1:
currentSettings = &readerSettings;
break;
case 2:
currentSettings = &controlsSettings;
break;
case 3:
default:
currentSettings = &systemSettings;
break;
}
break;
}
settingsCount = static_cast<int>(currentSettings->size());
}
StrId SettingsActivity::activeSubmenuTitleId() const {
switch (activeSubmenu) {
case SettingAction::ControlsPowerButton:
return StrId::STR_POWER_BUTTON;
case SettingAction::ControlsFrontButtons:
return StrId::STR_FRONT_BUTTONS;
case SettingAction::ControlsSideButtons:
return StrId::STR_SIDE_BUTTONS;
default:
return StrId::STR_NONE_OPT;
}
}
void SettingsActivity::openSubmenu(const SettingAction action) {
activeSubmenu = action;
applyCurrentSettingsList();
selectedSettingIndex = settingsCount > 0 ? 1 : 0;
requestUpdate();
}
void SettingsActivity::closeSubmenu() {
activeSubmenu = SettingAction::None;
applyCurrentSettingsList();
selectedSettingIndex = settingsCount > 0 ? 1 : 0;
requestUpdate();
}
void SettingsActivity::onEnter() {
Activity::onEnter();
// Reset selection to first category
selectedCategoryIndex = 0;
selectedSettingIndex = 0;
activeSubmenu = SettingAction::None;
preserveQuickResumeTimeoutOn =
SETTINGS.quickResumeSleepScreen == CrossPointSettings::QUICK_RESUME_SLEEP_SCREEN::QUICK_RESUME_AFTER_TIMEOUT;
quickResumeTimeoutAutoEnabled = false;
@@ -132,22 +270,10 @@ void SettingsActivity::loop() {
bool hasChangedCategory = false;
// Switching category always drops back to that category's top level.
auto applyCategorySelection = [this] {
switch (selectedCategoryIndex) {
case 0:
currentSettings = &displaySettings;
break;
case 1:
currentSettings = &readerSettings;
break;
case 2:
currentSettings = &controlsSettings;
break;
case 3:
currentSettings = &systemSettings;
break;
}
settingsCount = static_cast<int>(currentSettings->size());
activeSubmenu = SettingAction::None;
applyCurrentSettingsList();
};
// Handle actions with early return
@@ -164,7 +290,9 @@ void SettingsActivity::loop() {
}
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
if (selectedSettingIndex > 0) {
if (activeSubmenu != SettingAction::None) {
closeSubmenu();
} else if (selectedSettingIndex > 0) {
selectedSettingIndex = 0;
requestUpdate();
} else {
@@ -319,12 +447,15 @@ void SettingsActivity::toggleCurrentSetting() {
const bool currentValue = SETTINGS.*(setting.valuePtr);
SETTINGS.*(setting.valuePtr) = !currentValue;
} else if (setting.type == SettingType::ENUM && setting.valuePtr != nullptr) {
const uint8_t currentValue = SETTINGS.*(setting.valuePtr);
if (setting.enumValues.size() > 2) {
const uint8_t optionCount = static_cast<uint8_t>(setting.enumOptionCount());
if (optionCount == 0) return;
const uint8_t currentIndex = setting.enumDisplayIndexForRawValue(SETTINGS.*(setting.valuePtr));
if (optionCount > 2) {
const auto valuePtr = setting.valuePtr;
optionPopup.show(setting.nameId, setting.enumValues.data(), static_cast<int>(setting.enumValues.size()),
currentValue, [this, valuePtr, sleepScreenChanged, quickResumeTimeoutChanged](int idx) {
SETTINGS.*valuePtr = idx;
const auto options = setting.enumOptionsSnapshot();
optionPopup.show(setting.nameId, buildEnumDisplayLabels(setting), currentIndex,
[this, valuePtr, options, sleepScreenChanged, quickResumeTimeoutChanged](int idx) {
SETTINGS.*valuePtr = options.rawValueForDisplayIndex(static_cast<uint8_t>(idx));
syncQuickResumeTimeoutForSleepScreen(sleepScreenChanged, quickResumeTimeoutChanged);
SETTINGS.saveToFile();
rebuildSettingsLists();
@@ -332,11 +463,10 @@ void SettingsActivity::toggleCurrentSetting() {
requestUpdate();
return;
}
SETTINGS.*(setting.valuePtr) = (currentValue + 1) % static_cast<uint8_t>(setting.enumValues.size());
SETTINGS.*(setting.valuePtr) = setting.enumRawValueForDisplayIndex((currentIndex + 1) % optionCount);
} else if (setting.type == SettingType::ENUM && setting.valueGetter && setting.valueSetter) {
const uint8_t totalValues = setting.enumStringValues.empty()
? static_cast<uint8_t>(setting.enumValues.size())
: static_cast<uint8_t>(setting.enumStringValues.size());
const uint8_t totalValues = static_cast<uint8_t>(setting.enumOptionCount());
if (totalValues == 0) return;
const uint8_t cur = setting.valueGetter();
if (totalValues > 2) {
const auto valueSetter = setting.valueSetter;
@@ -346,12 +476,7 @@ void SettingsActivity::toggleCurrentSetting() {
SETTINGS.saveToFile();
rebuildSettingsLists();
};
if (!setting.enumStringValues.empty()) {
optionPopup.show(setting.nameId, setting.enumStringValues, cur, std::move(onSelect));
} else {
optionPopup.show(setting.nameId, setting.enumValues.data(), static_cast<int>(setting.enumValues.size()), cur,
std::move(onSelect));
}
optionPopup.show(setting.nameId, buildEnumDisplayLabels(setting), cur, std::move(onSelect));
requestUpdate();
return;
}
@@ -363,6 +488,9 @@ void SettingsActivity::toggleCurrentSetting() {
} else {
SETTINGS.*(setting.valuePtr) = currentValue + setting.valueRange.step;
}
} else if (setting.type == SettingType::SUBMENU) {
openSubmenu(setting.action);
return;
} else if (setting.type == SettingType::ACTION) {
auto resultHandler = [this](const ActivityResult&) { SETTINGS.saveToFile(); };
@@ -370,6 +498,11 @@ void SettingsActivity::toggleCurrentSetting() {
case SettingAction::RemapFrontButtons:
startActivityForResult(std::make_unique<ButtonRemapActivity>(renderer, mappedInput), resultHandler);
break;
case SettingAction::RemapFrontButtonsReader:
startActivityForResult(
std::make_unique<ButtonRemapActivity>(renderer, mappedInput, ButtonRemapActivity::Target::Reader),
resultHandler);
break;
case SettingAction::CustomiseStatusBar:
startActivityForResult(std::make_unique<StatusBarSettingsActivity>(renderer, mappedInput), resultHandler);
break;
@@ -409,8 +542,11 @@ void SettingsActivity::toggleCurrentSetting() {
case SettingAction::Language:
startActivityForResult(std::make_unique<LanguageSelectActivity>(renderer, mappedInput), resultHandler);
break;
case SettingAction::ControlsPowerButton:
case SettingAction::ControlsFrontButtons:
case SettingAction::ControlsSideButtons:
case SettingAction::None:
// Do nothing
// Submenu rows never reach here — they are handled as SettingType::SUBMENU above.
break;
}
return; // Results will be handled in the result handler, so we can return early here
@@ -472,7 +608,10 @@ void SettingsActivity::render(RenderLock&&) {
const auto& metrics = UITheme::getInstance().getMetrics();
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_SETTINGS_TITLE),
// Inside a group the header names it, so the user can tell where Back will take them.
const StrId submenuTitleId = activeSubmenuTitleId();
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight},
submenuTitleId == StrId::STR_NONE_OPT ? tr(STR_SETTINGS_TITLE) : I18N.get(submenuTitleId),
CROSSPOINT_VERSION);
std::vector<TabInfo> tabs;
@@ -494,18 +633,22 @@ void SettingsActivity::render(RenderLock&&) {
[&settings](int i) {
const auto& setting = settings[i];
std::string valueText = "";
if (setting.type == SettingType::TOGGLE && setting.valuePtr != nullptr) {
if (setting.type == SettingType::SUBMENU) {
valueText = ">";
} else if (setting.type == SettingType::TOGGLE && setting.valuePtr != nullptr) {
const bool value = SETTINGS.*(setting.valuePtr);
valueText = value ? tr(STR_STATE_ON) : tr(STR_STATE_OFF);
} else if (setting.type == SettingType::ENUM && setting.valuePtr != nullptr) {
const uint8_t value = SETTINGS.*(setting.valuePtr);
valueText = I18N.get(setting.enumValues[value]);
const uint8_t index = setting.enumDisplayIndexForRawValue(SETTINGS.*(setting.valuePtr));
if (index < setting.enumOptionCount()) {
valueText = I18N.get(setting.enumLabelAt(index));
}
} else if (setting.type == SettingType::ENUM && setting.valueGetter) {
const uint8_t value = setting.valueGetter();
if (!setting.enumStringValues.empty() && value < setting.enumStringValues.size()) {
valueText = setting.enumStringValues[value];
} else if (value < setting.enumValues.size()) {
valueText = I18N.get(setting.enumValues[value]);
if (setting.hasEnumStringValues() && value < setting.getEnumStringValues().size()) {
valueText = setting.getEnumStringValues()[value];
} else if (value < setting.enumOptionCount()) {
valueText = I18N.get(setting.enumLabelAt(value));
}
} else if (setting.type == SettingType::VALUE && setting.valuePtr != nullptr) {
if (setting.nameId == StrId::STR_TIME_TO_SLEEP) {
@@ -526,12 +669,15 @@ void SettingsActivity::render(RenderLock&&) {
true);
// Draw help text
const auto confirmLabel =
(selectedSettingIndex == 0)
? I18N.get(categoryNames[(selectedCategoryIndex + 1) % categoryCount])
: (selectedSettingIndex > 0 && (*currentSettings)[selectedSettingIndex - 1].nameId == StrId::STR_TIME_TO_SLEEP
? tr(STR_SELECT)
: tr(STR_TOGGLE));
const SettingInfo* selected = (selectedSettingIndex > 0 && selectedSettingIndex <= settingsCount)
? &(*currentSettings)[selectedSettingIndex - 1]
: nullptr;
const auto confirmLabel = (selectedSettingIndex == 0)
? I18N.get(categoryNames[(selectedCategoryIndex + 1) % categoryCount])
: (selected == nullptr ? tr(STR_TOGGLE)
: selected->type == SettingType::SUBMENU ? tr(STR_SELECT)
: selected->nameId == StrId::STR_TIME_TO_SLEEP ? tr(STR_SELECT)
: tr(STR_TOGGLE));
const auto labels = mappedInput.mapLabels(tr(STR_BACK), confirmLabel, tr(STR_DIR_UP), tr(STR_DIR_DOWN));
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
+16 -145
View File
@@ -1,155 +1,11 @@
#pragma once
#include <I18n.h>
#include <functional>
#include <string>
#include <vector>
#include "CrossPointSettings.h"
#include "SettingInfo.h"
#include "activities/Activity.h"
#include "components/OptionPopup.h"
#include "util/ButtonNavigator.h"
enum class SettingType { TOGGLE, ENUM, ACTION, VALUE, STRING };
enum class SettingAction {
None,
RemapFrontButtons,
CustomiseStatusBar,
KOReaderSync,
OPDSBrowser,
Network,
ClearCache,
CheckForUpdates,
SdFirmwareUpdate,
Language,
DownloadFonts,
TextSettings,
};
struct SettingInfo {
StrId nameId;
SettingType type;
uint8_t CrossPointSettings::* valuePtr = nullptr;
std::vector<StrId> enumValues;
std::vector<std::string> enumStringValues; // runtime alternative to StrId enumValues (for SD card fonts etc.)
SettingAction action = SettingAction::None;
struct ValueRange {
uint8_t min;
uint8_t max;
uint8_t step;
};
ValueRange valueRange = {};
const char* key = nullptr; // JSON API key (nullptr for ACTION types)
StrId category = StrId::STR_NONE_OPT; // Category for web UI grouping
bool obfuscated = false; // Save/load via base64 obfuscation (passwords)
bool inTextSettings = false; // Surfaced in the Text Settings screen; hidden from the flat Reader list
// Direct char[] string fields (for settings stored in CrossPointSettings)
size_t stringOffset = 0;
size_t stringMaxLen = 0;
// Dynamic accessors (for settings stored outside CrossPointSettings, e.g. KOReaderCredentialStore)
std::function<uint8_t()> valueGetter;
std::function<void(uint8_t)> valueSetter;
std::function<std::string()> stringGetter;
std::function<void(const std::string&)> stringSetter;
SettingInfo& withObfuscated() {
obfuscated = true;
return *this;
}
SettingInfo& withTextSettings() {
inTextSettings = true;
return *this;
}
static SettingInfo Toggle(StrId nameId, uint8_t CrossPointSettings::* ptr, const char* key = nullptr,
StrId category = StrId::STR_NONE_OPT) {
SettingInfo s;
s.nameId = nameId;
s.type = SettingType::TOGGLE;
s.valuePtr = ptr;
s.key = key;
s.category = category;
return s;
}
static SettingInfo Enum(StrId nameId, uint8_t CrossPointSettings::* ptr, std::vector<StrId> values,
const char* key = nullptr, StrId category = StrId::STR_NONE_OPT) {
SettingInfo s;
s.nameId = nameId;
s.type = SettingType::ENUM;
s.valuePtr = ptr;
s.enumValues = std::move(values);
s.key = key;
s.category = category;
return s;
}
static SettingInfo Action(StrId nameId, SettingAction action) {
SettingInfo s;
s.nameId = nameId;
s.type = SettingType::ACTION;
s.action = action;
return s;
}
static SettingInfo Value(StrId nameId, uint8_t CrossPointSettings::* ptr, const ValueRange valueRange,
const char* key = nullptr, StrId category = StrId::STR_NONE_OPT) {
SettingInfo s;
s.nameId = nameId;
s.type = SettingType::VALUE;
s.valuePtr = ptr;
s.valueRange = valueRange;
s.key = key;
s.category = category;
return s;
}
static SettingInfo String(StrId nameId, char* ptr, size_t maxLen, const char* key = nullptr,
StrId category = StrId::STR_NONE_OPT) {
SettingInfo s;
s.nameId = nameId;
s.type = SettingType::STRING;
s.stringOffset = (size_t)ptr - (size_t)&SETTINGS;
s.stringMaxLen = maxLen;
s.key = key;
s.category = category;
return s;
}
static SettingInfo DynamicEnum(StrId nameId, std::vector<StrId> values, std::function<uint8_t()> getter,
std::function<void(uint8_t)> setter, const char* key = nullptr,
StrId category = StrId::STR_NONE_OPT) {
SettingInfo s;
s.nameId = nameId;
s.type = SettingType::ENUM;
s.enumValues = std::move(values);
s.valueGetter = std::move(getter);
s.valueSetter = std::move(setter);
s.key = key;
s.category = category;
return s;
}
static SettingInfo DynamicString(StrId nameId, std::function<std::string()> getter,
std::function<void(const std::string&)> setter, const char* key = nullptr,
StrId category = StrId::STR_NONE_OPT) {
SettingInfo s;
s.nameId = nameId;
s.type = SettingType::STRING;
s.stringGetter = std::move(getter);
s.stringSetter = std::move(setter);
s.key = key;
s.category = category;
return s;
}
};
class SettingsActivity final : public Activity {
ButtonNavigator buttonNavigator;
@@ -162,8 +18,15 @@ class SettingsActivity final : public Activity {
std::vector<SettingInfo> readerSettings;
std::vector<SettingInfo> controlsSettings;
std::vector<SettingInfo> systemSettings;
// Controls sub-lists, reached from the SUBMENU entries in controlsSettings.
std::vector<SettingInfo> controlsPowerSettings;
std::vector<SettingInfo> controlsFrontButtonSettings;
std::vector<SettingInfo> controlsSideButtonSettings;
const std::vector<SettingInfo>* currentSettings = nullptr;
// None while the category's top-level list is showing.
SettingAction activeSubmenu = SettingAction::None;
bool preserveQuickResumeTimeoutOn = false;
bool quickResumeTimeoutAutoEnabled = false;
@@ -177,6 +40,14 @@ class SettingsActivity final : public Activity {
void openSleepTimeoutPicker();
void rebuildSettingsLists();
void syncQuickResumeTimeoutForSleepScreen(bool sleepScreenChanged, bool quickResumeTimeoutChanged);
// Points currentSettings at the list for the active category and submenu, and refreshes
// settingsCount. The single place that decides which list is on screen.
void applyCurrentSettingsList();
void openSubmenu(SettingAction action);
void closeSubmenu();
// Title for the active submenu, or STR_NONE_OPT at the top level.
StrId activeSubmenuTitleId() const;
void buildControlsLists();
public:
explicit SettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput)
+9
View File
@@ -49,6 +49,15 @@ class OptionPopup {
active = true;
}
void show(StrId titleId, std::vector<std::string>&& options, int currentIndex, std::function<void(int)> onSelect) {
title = I18N.get(titleId);
ownedStrings = std::move(options);
selectedIndex = currentIndex;
onSelectCallback = std::move(onSelect);
layoutValid = false;
active = true;
}
bool handleInput(MappedInputManager& input, const std::function<void()>& requestUpdate) {
if (!active) return false;
+32 -20
View File
@@ -34,6 +34,7 @@
#include "images/LoadingIcon.h"
#include "util/ButtonNavigator.h"
#include "util/ScreenshotUtil.h"
#include "util/ShortcutAction.h"
GfxRenderer renderer(display);
MappedInputManager mappedInputManager(gpio, renderer);
@@ -320,7 +321,7 @@ void setup() {
case HalGPIO::WakeupReason::PowerButton:
LOG_DBG("MAIN", "Verifying power button press duration");
if (!gpio.verifyPowerButtonWakeup(SETTINGS.getPowerButtonDuration(),
SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::SLEEP)) {
SETTINGS.shortPwrBtn == shortcutActionRawValue(ShortcutAction::Sleep))) {
powerManager.startDeepSleep(gpio);
}
break;
@@ -461,9 +462,11 @@ void loop() {
const unsigned long loopStartTime = millis();
static unsigned long lastMemPrint = 0;
gpio.setSharedConfirmPowerShortPressEmitsPower(SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::SLEEP);
gpio.setSharedConfirmPowerShortPressEmitsPower(SETTINGS.shortPwrBtn == shortcutActionRawValue(ShortcutAction::Sleep));
gpio.update();
halTiltSensor.update(SETTINGS.tiltPageTurn, SETTINGS.orientation, activityManager.isReaderActivity());
const bool inReader = activityManager.isReaderActivity();
mappedInputManager.setReaderMode(inReader);
halTiltSensor.update(SETTINGS.tiltPageTurn, SETTINGS.tiltPageTurnDirection, SETTINGS.orientation, inReader);
renderer.setFadingFix(SETTINGS.fadingFix);
@@ -530,24 +533,33 @@ void loop() {
return;
}
if (millis() >= allowSleepAt && gpio.isPressed(HalGPIO::BTN_POWER) &&
gpio.getPowerButtonHeldTime() > SETTINGS.getPowerButtonDuration()) {
// If the screenshot combination is potentially being pressed, don't sleep
if (gpio.isPressed(HalGPIO::BTN_DOWN)) {
return;
// Power button shortcuts: a hold past the configured duration runs longPwrBtn (Sleep by
// default, which is what a hold has always done), a release before that runs shortPwrBtn.
// Only actions that need no open book are dispatched here; the reader picks up the rest,
// which is also why a reader-only binding does nothing outside a reader.
static bool longPowerActionFired = false;
if (gpio.isPressed(HalGPIO::BTN_POWER)) {
if (millis() >= allowSleepAt && !longPowerActionFired &&
gpio.getPowerButtonHeldTime() > SETTINGS.getPowerButtonDuration()) {
// If the screenshot combination is potentially being pressed, don't act
if (gpio.isPressed(HalGPIO::BTN_DOWN)) {
return;
}
// Latched even for reader-only actions, so the release that ends the hold does not also
// fire the short-press action on top.
longPowerActionFired = true;
const auto action = shortcutActionFromRawValue(SETTINGS.longPwrBtn);
if (isShortcutAvailableOutsideReader(action) && runGlobalShortcut(action, renderer)) {
// Sleep never returns here — it calls esp_deep_sleep_start.
return;
}
}
enterDeepSleep();
// This should never be hit as `enterDeepSleep` calls esp_deep_sleep_start
return;
}
// Refresh screen when power button is short-pressed with FORCE_REFRESH setting.
if (SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::FORCE_REFRESH &&
mappedInputManager.wasReleased(MappedInputManager::Button::Power)) {
LOG_DBG("MAIN", "Manual screen refresh triggered");
if (!activityManager.handleForcedRefresh()) {
RenderLock lock;
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
} else if (mappedInputManager.wasReleased(MappedInputManager::Button::Power)) {
const bool consumedByHold = longPowerActionFired;
longPowerActionFired = false;
const auto action = shortcutActionFromRawValue(SETTINGS.shortPwrBtn);
if (!consumedByHold && isShortcutAvailableOutsideReader(action)) {
runGlobalShortcut(action, renderer);
}
}
Loaded 20 of 23 files, more files were not shown because too many files have changed in this diff. Show more