mirror of
https://github.com/crosspoint-reader/crosspoint-reader.git
synced 2026-08-13 14:21:57 -07:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d9a207f9a9 | ||
|
|
77b58e431f | ||
|
|
167663cb6e | ||
|
|
2aa0f7be17 | ||
|
|
a1c0bdfa05 | ||
|
|
4e8aea95a0 |
No files matched your search
@@ -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"
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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
@@ -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 "";
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
Reference in new issue
Block a user