232 lines
7.2 KiB
C++
232 lines
7.2 KiB
C++
#include "ClockOffsetActivity.h"
|
|
|
|
#include <GfxRenderer.h>
|
|
#include <HalClock.h>
|
|
#include <I18n.h>
|
|
|
|
#include <cstdio>
|
|
|
|
#include "CrossPointSettings.h"
|
|
#include "MappedInputManager.h"
|
|
#include "components/UITheme.h"
|
|
#include "fontIds.h"
|
|
|
|
namespace {
|
|
constexpr uint8_t MAX_POS_HOURS = 14;
|
|
constexpr uint8_t MAX_NEG_HOURS = 12;
|
|
constexpr uint8_t MINUTE_STEPS = 4; // 0, 15, 30, 45
|
|
constexpr uint8_t MINUTES_PER_QUARTER = 15;
|
|
constexpr uint8_t BIAS_QUARTER_HOURS = 48; // 0 stored = UTC-12, 48 stored = UTC+0
|
|
|
|
// Convert a (sign, hours, quarter) triple into the biased storage value.
|
|
// Returns a value in [0, 104].
|
|
uint8_t encodeOffset(uint8_t sign, uint8_t hours, uint8_t quarter) {
|
|
int signedQuarter = static_cast<int>(hours) * 4 + static_cast<int>(quarter);
|
|
if (sign == 1) signedQuarter = -signedQuarter;
|
|
int biased = signedQuarter + BIAS_QUARTER_HOURS;
|
|
if (biased < 0) biased = 0;
|
|
if (biased > 104) biased = 104;
|
|
return static_cast<uint8_t>(biased);
|
|
}
|
|
|
|
// Decompose the biased storage value into (sign, hours, quarter).
|
|
void decodeOffset(uint8_t biased, uint8_t& sign, uint8_t& hours, uint8_t& quarter) {
|
|
if (biased > 104) biased = BIAS_QUARTER_HOURS;
|
|
int signedQuarter = static_cast<int>(biased) - BIAS_QUARTER_HOURS;
|
|
if (signedQuarter < 0) {
|
|
sign = 1;
|
|
signedQuarter = -signedQuarter;
|
|
} else {
|
|
sign = 0;
|
|
}
|
|
hours = static_cast<uint8_t>(signedQuarter / 4);
|
|
quarter = static_cast<uint8_t>(signedQuarter % 4);
|
|
}
|
|
} // namespace
|
|
|
|
void ClockOffsetActivity::onEnter() {
|
|
Activity::onEnter();
|
|
loadFromSettings();
|
|
activeField = FIELD_HOURS;
|
|
requestUpdate();
|
|
}
|
|
|
|
void ClockOffsetActivity::onExit() {
|
|
saveToSettings();
|
|
Activity::onExit();
|
|
}
|
|
|
|
void ClockOffsetActivity::loadFromSettings() {
|
|
decodeOffset(SETTINGS.clockUtcOffsetQ, sign, hours, minutesQuarter);
|
|
clampForSign();
|
|
}
|
|
|
|
void ClockOffsetActivity::saveToSettings() const {
|
|
const uint8_t encoded = encodeOffset(sign, hours, minutesQuarter);
|
|
if (encoded == SETTINGS.clockUtcOffsetQ) return;
|
|
SETTINGS.clockUtcOffsetQ = encoded;
|
|
SETTINGS.saveToFile();
|
|
}
|
|
|
|
void ClockOffsetActivity::clampForSign() {
|
|
const uint8_t maxHours = (sign == 1) ? MAX_NEG_HOURS : MAX_POS_HOURS;
|
|
if (hours > maxHours) hours = maxHours;
|
|
// At the absolute boundary (-12:00 or +14:00) only :00 is valid.
|
|
if (hours == maxHours && minutesQuarter != 0) {
|
|
minutesQuarter = 0;
|
|
}
|
|
}
|
|
|
|
void ClockOffsetActivity::adjustActiveField(int delta) {
|
|
switch (activeField) {
|
|
case FIELD_SIGN: {
|
|
sign = static_cast<uint8_t>((sign + 1) % 2);
|
|
clampForSign();
|
|
break;
|
|
}
|
|
case FIELD_HOURS: {
|
|
const uint8_t maxHours = (sign == 1) ? MAX_NEG_HOURS : MAX_POS_HOURS;
|
|
const int next = (static_cast<int>(hours) + delta + (maxHours + 1)) % (maxHours + 1);
|
|
hours = static_cast<uint8_t>(next);
|
|
clampForSign();
|
|
break;
|
|
}
|
|
case FIELD_MINUTES: {
|
|
// At the boundary hour, lock minutes to :00.
|
|
const uint8_t maxHours = (sign == 1) ? MAX_NEG_HOURS : MAX_POS_HOURS;
|
|
if (hours == maxHours) {
|
|
minutesQuarter = 0;
|
|
break;
|
|
}
|
|
const int next = (static_cast<int>(minutesQuarter) + delta + MINUTE_STEPS) % MINUTE_STEPS;
|
|
minutesQuarter = static_cast<uint8_t>(next);
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
void ClockOffsetActivity::loop() {
|
|
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
|
finish();
|
|
return;
|
|
}
|
|
|
|
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
|
|
activeField = static_cast<Field>((activeField + 1) % FIELD_COUNT);
|
|
requestUpdate();
|
|
return;
|
|
}
|
|
|
|
buttonNavigator.onNextRelease([this] {
|
|
adjustActiveField(+1);
|
|
requestUpdate();
|
|
});
|
|
buttonNavigator.onPreviousRelease([this] {
|
|
adjustActiveField(-1);
|
|
requestUpdate();
|
|
});
|
|
buttonNavigator.onNextContinuous([this] {
|
|
adjustActiveField(+1);
|
|
requestUpdate();
|
|
});
|
|
buttonNavigator.onPreviousContinuous([this] {
|
|
adjustActiveField(-1);
|
|
requestUpdate();
|
|
});
|
|
}
|
|
|
|
void ClockOffsetActivity::render(RenderLock&&) {
|
|
renderer.clearScreen();
|
|
|
|
const auto& metrics = UITheme::getInstance().getMetrics();
|
|
const auto pageWidth = renderer.getScreenWidth();
|
|
const auto pageHeight = renderer.getScreenHeight();
|
|
|
|
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_CLOCK_UTC_OFFSET));
|
|
|
|
// Build the offset string. Use a generous font and centre it.
|
|
char offsetBuf[16];
|
|
snprintf(offsetBuf, sizeof(offsetBuf), "UTC %c %d:%02d", sign == 1 ? '-' : '+', hours,
|
|
minutesQuarter * MINUTES_PER_QUARTER);
|
|
|
|
const int centreY = pageHeight / 2 - 40;
|
|
renderer.drawCenteredText(UI_12_FONT_ID, centreY, offsetBuf, true, EpdFontFamily::BOLD);
|
|
|
|
// Underline / caret under the active field. Compute positions by measuring substrings of the
|
|
// formatted string so the caret follows the font glyph widths exactly.
|
|
// Field substrings:
|
|
// "UTC " -> prefix
|
|
// "{+/-}" -> sign
|
|
// " "
|
|
// "{hours}" -> hours
|
|
// ":"
|
|
// "{mm}" -> minutes
|
|
auto widthOf = [&](const char* s) { return renderer.getTextWidth(UI_12_FONT_ID, s); };
|
|
const int totalWidth = widthOf(offsetBuf);
|
|
const int leftEdge = (pageWidth - totalWidth) / 2;
|
|
|
|
// Locate each field by reformatting prefixes.
|
|
char prefixSign[16];
|
|
snprintf(prefixSign, sizeof(prefixSign), "UTC ");
|
|
const int signX = leftEdge + widthOf(prefixSign);
|
|
|
|
char prefixHours[16];
|
|
snprintf(prefixHours, sizeof(prefixHours), "UTC %c ", sign == 1 ? '-' : '+');
|
|
const int hoursX = leftEdge + widthOf(prefixHours);
|
|
|
|
char prefixMinutes[16];
|
|
snprintf(prefixMinutes, sizeof(prefixMinutes), "UTC %c %d:", sign == 1 ? '-' : '+', hours);
|
|
const int minutesX = leftEdge + widthOf(prefixMinutes);
|
|
|
|
// Width of each field substring for the caret span.
|
|
const int signW = widthOf(sign == 1 ? "-" : "+");
|
|
char hoursStr[8];
|
|
snprintf(hoursStr, sizeof(hoursStr), "%d", hours);
|
|
const int hoursW = widthOf(hoursStr);
|
|
char minutesStr[8];
|
|
snprintf(minutesStr, sizeof(minutesStr), "%02d", minutesQuarter * MINUTES_PER_QUARTER);
|
|
const int minutesW = widthOf(minutesStr);
|
|
|
|
int caretX = 0;
|
|
int caretW = 0;
|
|
switch (activeField) {
|
|
case FIELD_SIGN:
|
|
caretX = signX;
|
|
caretW = signW;
|
|
break;
|
|
case FIELD_HOURS:
|
|
caretX = hoursX;
|
|
caretW = hoursW;
|
|
break;
|
|
case FIELD_MINUTES:
|
|
caretX = minutesX;
|
|
caretW = minutesW;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
// Caret drawn as a short bar below the active field.
|
|
const int caretY = centreY + 10;
|
|
for (int dy = 0; dy < 2; dy++) {
|
|
renderer.drawLine(caretX, caretY + dy, caretX + caretW, caretY + dy);
|
|
}
|
|
|
|
// Live preview of the resulting wall-clock time, so users can verify against a watch.
|
|
if (halClock.isAvailable()) {
|
|
char timeBuf[9];
|
|
const uint8_t encoded = encodeOffset(sign, hours, minutesQuarter);
|
|
if (halClock.formatTime(timeBuf, sizeof(timeBuf), encoded, SETTINGS.clockFormat == 1)) {
|
|
char preview[24];
|
|
snprintf(preview, sizeof(preview), "%s %s", tr(STR_CURRENT_TIME), timeBuf);
|
|
renderer.drawCenteredText(UI_10_FONT_ID, centreY + 60, preview);
|
|
}
|
|
}
|
|
|
|
const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_NEXT_FIELD), tr(STR_DIR_UP), tr(STR_DIR_DOWN));
|
|
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
|
|
|
renderer.displayBuffer();
|
|
}
|