aboutsummaryrefslogtreecommitdiff
path: root/source/ui
diff options
context:
space:
mode:
authorcirakg <cirakg@cirakg.xyz>2026-10-25 23:51:52 +0100
committercirakg <cirakg@cirakg.xyz>2026-10-25 23:51:52 +0100
commitb8addc87f804789368612bf8e1be31a2bf28efe1 (patch)
treebece4051eb6f009eba05ffdd585420232741a7d7 /source/ui
parent140a6660f63f88d05ba9aae0bf1c4b401bb6574d (diff)
test(ui): second UI scenario, and the fixes it found
A second UI-test build (uitest-extra.jungle) covers the legacy PIN and the forced new PIN, Change and Reset PIN, a queued Require PIN, UPC-A, Code 39, the code error screen, the encoding progress, auto-lock on a barcode, the light theme and the glance without a code; the main flow adds the Steam and Library details and the menu's text screens. Fixed from what it found: Entry IDs tripped the watchdog with 110 entries; auto-lock didn't close a locked barcode; a queued Require PIN showed "Confirm in menu"; locked barcodes lacked the padlock; the PIN keypad's bottom row, the Reset PIN question, text-screen bottoms, the glance title and the light-theme warning colour. Goldens approved on all seven watches; only the Forerunner 70 was re-verified after the version bump. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'source/ui')
-rw-r--r--source/ui/CodeView.mc19
-rw-r--r--source/ui/DebugMenu.mc9
-rw-r--r--source/ui/GlanceView.mc23
-rw-r--r--source/ui/InfoView.mc4
-rw-r--r--source/ui/ListView.mc18
-rw-r--r--source/ui/MainMenu.mc113
-rw-r--r--source/ui/OtpView.mc4
-rw-r--r--source/ui/PinView.mc33
-rw-r--r--source/ui/Ui.mc8
9 files changed, 180 insertions, 51 deletions
diff --git a/source/ui/CodeView.mc b/source/ui/CodeView.mc
index efa23d2..03f1fa7 100644
--- a/source/ui/CodeView.mc
+++ b/source/ui/CodeView.mc
@@ -27,6 +27,9 @@
// ticker timer does the renewing (and redraws the progress while
// encoding), so a code screen never needs more timers.
// * Too big: a message with the size needed.
+// * A locked code closes back to the list when auto-lock fires while it
+// is open (README 7.5), like a locked OTP entry: the ticker keeps
+// running for it and checks the session.
// The payload is decrypted when the screen opens and wiped once encoded.
import Toybox.Attention;
@@ -70,6 +73,9 @@ class CodeView extends WatchUi.View {
_shownLogged = false;
encode();
boostBrightness();
+ if (header.locked) {
+ startTicker();
+ }
}
function onHide() as Void {
@@ -143,8 +149,13 @@ class CodeView extends WatchUi.View {
}
// Progress redraws while a job runs; brightness renewals during the
- // minute. The ticker stops when neither is needed.
+ // minute; auto-lock for a locked code. The ticker stops when none of
+ // them is needed.
function onTick() as Void {
+ if (header.locked && AppState.checkAutoLock()) {
+ WatchUi.popView(WatchUi.SLIDE_RIGHT);
+ return;
+ }
if (_job != null) {
WatchUi.requestUpdate();
}
@@ -157,7 +168,7 @@ class CodeView extends WatchUi.View {
light();
}
}
- if (_job == null && _boostStart < 0) {
+ if (_job == null && _boostStart < 0 && !header.locked) {
stopTicker();
}
}
@@ -266,8 +277,10 @@ class CodeView extends WatchUi.View {
} else if (code instanceof RunsPainter) {
drawMatrix(dc, code, smallFont, smallH);
} else if (job != null) {
+ var percent = job.percent();
dc.drawText(w / 2, h / 2 - smallH / 2, smallFont, (job instanceof RunsPainter ? "Drawing… " : "Encoding… ")
- + job.percent() + "%", Graphics.TEXT_JUSTIFY_CENTER);
+ + percent + "%", Graphics.TEXT_JUSTIFY_CENTER);
+ Diag.data("code_progress", "percent=" + percent); // for UI scripts
} else {
drawMessage(dc, code instanceof Codes.CodeError ? code.message : "…", smallFont, smallH);
}
diff --git a/source/ui/DebugMenu.mc b/source/ui/DebugMenu.mc
index f1ab407..452cb1d 100644
--- a/source/ui/DebugMenu.mc
+++ b/source/ui/DebugMenu.mc
@@ -40,6 +40,8 @@ module DebugMenu {
}
}
+ const BENCH_HEADERS = 20;
+
// The running benchmark (kept so its timer isn't collected).
var bench as BenchRunner? = null;
@@ -74,12 +76,15 @@ module DebugMenu {
} else if (_step == 2) {
_lines.add("PIN derivation: " + Kdf.calibrate(1000, Kdf.ITERATIONS_PER_STEP) + " iterations per second");
} else if (_step == 3) {
+ // At most BENCH_HEADERS: all 110 of the UI test's entries in
+ // one event tripped the watchdog.
var vault = AppState.theVault();
var ids = vault.ids();
- for (var i = 0; i < ids.size(); i++) {
+ var n = ids.size() < BENCH_HEADERS ? ids.size() : BENCH_HEADERS;
+ for (var i = 0; i < n; i++) {
vault.header(ids[i]);
}
- _lines.add("Headers: " + ids.size() + " in " + (System.getTimer() - t) + " ms");
+ _lines.add("Headers: " + n + " in " + (System.getTimer() - t) + " ms");
} else if (_step == 4) {
SelfTest.run();
_lines.add("Self-test: " + (System.getTimer() - t) + " ms");
diff --git a/source/ui/GlanceView.mc b/source/ui/GlanceView.mc
index 4540f2a..6be18f3 100644
--- a/source/ui/GlanceView.mc
+++ b/source/ui/GlanceView.mc
@@ -89,8 +89,15 @@ class OtpGlanceView extends WatchUi.GlanceView {
var h = dc.getHeight();
var top = h / 2 - dc.getFontHeight(Graphics.FONT_GLANCE);
var x = inset(dc, top);
+ // In capitals the name reaches the round screen's edge on the
+ // Forerunner 70 and 965, which cuts the last letter; the narrower
+ // mixed case fits there.
+ var title = "OTP WALLET";
+ if (x + dc.getTextWidthInPixels(title, Graphics.FONT_GLANCE) > dc.getWidth() - edgeCut(top)) {
+ title = "OTP Wallet";
+ }
dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_TRANSPARENT);
- dc.drawText(x, top, Graphics.FONT_GLANCE, "OTP WALLET", Graphics.TEXT_JUSTIFY_LEFT);
+ dc.drawText(x, top, Graphics.FONT_GLANCE, title, Graphics.TEXT_JUSTIFY_LEFT);
dc.setColor(Graphics.COLOR_LT_GRAY, Graphics.COLOR_TRANSPARENT);
dc.drawText(x, h / 2, Graphics.FONT_GLANCE, count == 1 ? "1 entry" : count + " entries",
Graphics.TEXT_JUSTIFY_LEFT);
@@ -103,15 +110,23 @@ class OtpGlanceView extends WatchUi.GlanceView {
// screen's top-left curve; on most round watches this is 0 or a few
// pixels).
private function inset(dc as Graphics.Dc, y as Number) as Number {
+ var left = (System.getDeviceSettings().screenWidth - dc.getWidth()) / 2;
+ var edge = edgeCut(y);
+ return edge > left ? edge - left : 0;
+ }
+
+ // How far a round screen's edge reaches in from each side at height y
+ // (0 on other shapes), with the same assumption: the glance starts at
+ // the top of the screen. The right end of a glance reaches the screen's
+ // edge, so this is also what the curve cuts off there.
+ private function edgeCut(y as Number) as Number {
var screen = System.getDeviceSettings();
if (screen.screenShape != System.SCREEN_SHAPE_ROUND) {
return 0;
}
var r = screen.screenWidth / 2;
var dy = r - y;
- var edge = r - Math.sqrt(r * r - dy * dy).toNumber();
- var left = (screen.screenWidth - dc.getWidth()) / 2;
- return edge > left ? edge - left : 0;
+ return r - Math.sqrt(r * r - dy * dy).toNumber();
}
// The label, the code and a bar for the seconds left in the period.
diff --git a/source/ui/InfoView.mc b/source/ui/InfoView.mc
index a965713..877ca51 100644
--- a/source/ui/InfoView.mc
+++ b/source/ui/InfoView.mc
@@ -65,7 +65,9 @@ class InfoView extends WatchUi.View {
dc.drawText(w / 2, y, Graphics.FONT_SMALL, Ui.fit(dc, _title, Graphics.FONT_SMALL, 2 * Ui.halfWidthAt(dc, y)),
Graphics.TEXT_JUSTIFY_CENTER);
y += dc.getFontHeight(Graphics.FONT_SMALL) + 4;
- var bottom = Ui.isRound() ? h - h / 10 : h - 4;
+ // The same margin on rectangles: the Venu X1's rounded corners cut
+ // off a last line drawn at the very bottom, and the scroll arrow.
+ var bottom = h - h / 10;
var lines = _lines;
if (lines == null) {
// Wrap to the narrowest width the text area has.
diff --git a/source/ui/ListView.mc b/source/ui/ListView.mc
index de73cce..36df798 100644
--- a/source/ui/ListView.mc
+++ b/source/ui/ListView.mc
@@ -3,7 +3,8 @@
//
// Layout (README 13.1, 13.3): a title, then rows of two lines each (name;
// group and account) with the code and a countdown on the right for unlocked
-// OTP entries, a padlock for locked entries and a barcode icon for codes.
+// OTP entries, a padlock for locked entries (codes too: opening them asks
+// for the PIN) and a barcode icon for other codes.
// The focused row is outlined; the list scrolls to keep it visible and
// centres short lists. On round screens each row is as wide as the circle
// allows at its height.
@@ -186,7 +187,8 @@ class ListView extends WatchUi.View {
// "Importing… 40%" while an import runs, "Results ready" when settings
// results haven't been looked at, "Confirm in menu" while phone changes
- // wait for the PIN, else the app name.
+ // wait to be confirmed with the PIN (a waiting "Require PIN" applies at
+ // the next PIN by itself), else the app name.
private function title() as String {
var p = AppState.processor;
var busy = p != null ? p.busyText() : null;
@@ -197,7 +199,7 @@ class ListView extends WatchUi.View {
return "Results ready (menu)";
}
var v = AppState.vault;
- return v != null && ChangeQueue.count(v) > 0 ? "Confirm in menu" : "OTP Wallet";
+ return v != null && ChangeQueue.countToConfirm(v) > 0 ? "Confirm in menu" : "OTP Wallet";
}
private function drawEmpty(dc as Graphics.Dc, top as Number, bottom as Number) as Void {
@@ -283,7 +285,7 @@ class ListView extends WatchUi.View {
var right = x1 - 8;
if (header == null) {
- dc.setColor(Ui.WARN_COLOR, Graphics.COLOR_TRANSPARENT);
+ dc.setColor(Ui.warn(), Graphics.COLOR_TRANSPARENT);
dc.drawText(left, lineY, nameFont, Ui.fit(dc, "Damaged " + id, nameFont, right - left), Graphics.TEXT_JUSTIFY_LEFT);
return true;
}
@@ -297,12 +299,12 @@ class ListView extends WatchUi.View {
var secondRight = right;
var complete = true;
var iconSize = nameH * 2 / 3;
- if (header.kind == Entry.KIND_CODE) {
- Ui.drawBarcode(dc, right - iconSize, lineY + (nameH - iconSize) / 2, iconSize, Ui.dim());
- nameRight -= iconSize + 6;
- } else if (header.locked) {
+ if (header.locked) {
Ui.drawLock(dc, right - iconSize, lineY + (nameH - iconSize) / 2, iconSize, Ui.dim());
nameRight -= iconSize + 6;
+ } else if (header.kind == Entry.KIND_CODE) {
+ Ui.drawBarcode(dc, right - iconSize, lineY + (nameH - iconSize) / 2, iconSize, Ui.dim());
+ nameRight -= iconSize + 6;
} else {
var now = AppState.now();
var text = AppState.cryptoReady ? AppState.theCodes().cached(header.id, now) : null;
diff --git a/source/ui/MainMenu.mc b/source/ui/MainMenu.mc
index 8863c6d..db59f4a 100644
--- a/source/ui/MainMenu.mc
+++ b/source/ui/MainMenu.mc
@@ -18,7 +18,10 @@ module MainMenu {
var menu = new WatchUi.Menu2({:title => "OTP Wallet"});
var waiting = ChangeQueue.count(vault);
if (waiting > 0) {
- menu.addItem(new WatchUi.MenuItem("Phone changes", waiting + " to confirm", :queue, null));
+ // Only "Require PIN" waiting: it applies at the next PIN anyway.
+ var toConfirm = ChangeQueue.countToConfirm(vault);
+ menu.addItem(new WatchUi.MenuItem("Phone changes", toConfirm > 0 ? toConfirm + " to confirm" : "Apply at next PIN",
+ :queue, null));
}
if (vault.session.isUnlocked()) {
menu.addItem(new WatchUi.MenuItem("Lock now", null, :lock, null));
@@ -32,28 +35,29 @@ module MainMenu {
menu.addItem(new WatchUi.MenuItem("Clock status", null, :clock, null));
menu.addItem(new WatchUi.MenuItem("About", null, :about, null));
DebugMenu.addItems(menu);
+ logItems(menu);
return menu;
}
- function entryIdLines() as Array<String> {
- var vault = AppState.theVault();
- var ids = vault.ids();
- var lines = [] as Array<String>;
- for (var i = 0; i < ids.size(); i++) {
- var header = vault.header(ids[i]);
- var name = header == null ? "damaged" : (header.name.length() > 0 ? header.name : "Locked entry");
- lines.add(ids[i] + " " + name);
- }
- if (lines.size() == 0) {
- lines.add("No entries yet.");
- }
- var pending = vault.pendingIds().size();
- if (pending > 0) {
- lines.add(pending + " locked entries wait for your PIN.");
+ // Debug builds: the item labels in order, so UI scripts can find an
+ // item on touch-only watches, whose menus have no focus to move.
+ (:debug)
+ function logItems(menu as WatchUi.Menu2) as Void {
+ var labels = "";
+ for (var i = 0; menu.getItem(i) != null; i++) {
+ labels += (i > 0 ? "|" : "") + (menu.getItem(i) as WatchUi.MenuItem).getLabel();
}
- return lines;
+ Diag.data("menu", "items=" + labels);
+ }
+
+ (:release)
+ function logItems(menu as WatchUi.Menu2) as Void {
}
+ // The Entry IDs screen: "Reading…" while EntryIdsJob reads the
+ // headers on a timer, then the list (kept so the runner isn't collected).
+ var idsScreen as EntryIdsScreen? = null;
+
function clockLines() as Array<String> {
var lines = [] as Array<String>;
var watch = Clock.nowUnix(0);
@@ -118,7 +122,9 @@ class MainMenuDelegate extends WatchUi.Menu2InputDelegate {
WatchUi.switchToView(new PinView(PinView.MODE_UNLOCK, then.method(:run)), new PinDelegate(), WatchUi.SLIDE_LEFT);
}
} else if (id == :reset) {
- WatchUi.pushView(new WatchUi.Confirmation("Erase locked entries and set a new PIN?"), new ResetPinConfirmation(),
+ // Two short lines (the item said "Reset PIN"): the native dialog shows
+ // two lines and cuts longer text mid-word.
+ WatchUi.pushView(new WatchUi.Confirmation("Erase locked\nentries?"), new ResetPinConfirmation(),
WatchUi.SLIDE_LEFT);
} else if (id == :results) {
WatchUi.switchToView(new InfoView("Results", Results.lines(), "results"), new InfoDelegate(),
@@ -127,7 +133,9 @@ class MainMenuDelegate extends WatchUi.Menu2InputDelegate {
} else if (id == :queue) {
WatchUi.switchToView(QueueMenu.build(), new QueueMenuDelegate(), WatchUi.SLIDE_LEFT);
} else if (id == :ids) {
- WatchUi.switchToView(new InfoView("Entry IDs", MainMenu.entryIdLines(), "ids"), new InfoDelegate(), WatchUi.SLIDE_LEFT);
+ var screen = new EntryIdsScreen();
+ MainMenu.idsScreen = screen;
+ screen.start();
} else if (id == :clock) {
WatchUi.switchToView(new InfoView("Clock status", MainMenu.clockLines(), "clock"), new InfoDelegate(), WatchUi.SLIDE_LEFT);
} else if (id == :about) {
@@ -138,6 +146,73 @@ class MainMenuDelegate extends WatchUi.Menu2InputDelegate {
}
}
+// Reads one header per entry, a few per timer step: reading 110 in one
+// event tripped the watchdog. Ends with the lines of the Entry IDs screen.
+class EntryIdsJob extends Jobs.Job {
+
+ static const HEADERS_PER_STEP = 10;
+
+ var lines as Array<String> = [] as Array<String>;
+ private var _ids as Array<String>;
+ private var _at as Number = 0;
+
+ function initialize() {
+ Job.initialize();
+ _ids = AppState.theVault().ids();
+ }
+
+ function step(budget as Number) as Boolean {
+ var vault = AppState.theVault();
+ var stop = _at + HEADERS_PER_STEP * budget;
+ stop = stop < _ids.size() ? stop : _ids.size();
+ for (; _at < stop; _at++) {
+ var id = _ids[_at];
+ var header = vault.header(id);
+ var name = header == null ? "damaged" : (header.name.length() > 0 ? header.name : "Locked entry");
+ lines.add(id + " " + name);
+ }
+ if (_at < _ids.size()) {
+ return false;
+ }
+ if (lines.size() == 0) {
+ lines.add("No entries yet.");
+ }
+ var pending = vault.pendingIds().size();
+ if (pending > 0) {
+ lines.add(pending + " locked entries wait for your PIN.");
+ }
+ return true;
+ }
+}
+
+// Shows "Reading…" (replacing the menu), runs EntryIdsJob, then replaces
+// that screen with the IDs, unless the user left it meanwhile.
+class EntryIdsScreen {
+
+ private var _job as EntryIdsJob = new EntryIdsJob();
+ // The timer driver (kept so it isn't collected).
+ var runner as Jobs.JobRunner? = null;
+ private var _reading as InfoView = new InfoView("Entry IDs", ["Reading…"] as Array<String>, "ids_reading");
+
+ function initialize() {
+ }
+
+ function start() as Void {
+ WatchUi.switchToView(_reading, new InfoDelegate(), WatchUi.SLIDE_LEFT);
+ var r = new Jobs.JobRunner(_job, 1, method(:done));
+ runner = r;
+ r.start();
+ }
+
+ function done() as Void {
+ runner = null;
+ MainMenu.idsScreen = null;
+ if (WatchUi.getCurrentView()[0] == _reading) {
+ WatchUi.switchToView(new InfoView("Entry IDs", _job.lines, "ids"), new InfoDelegate(), WatchUi.SLIDE_IMMEDIATE);
+ }
+ }
+}
+
// After unlocking from the menu's "Change PIN": ask for the new PIN.
class ChangePinAfterUnlock {
diff --git a/source/ui/OtpView.mc b/source/ui/OtpView.mc
index 1c0c0bd..d35e103 100644
--- a/source/ui/OtpView.mc
+++ b/source/ui/OtpView.mc
@@ -131,7 +131,7 @@ class OtpView extends WatchUi.View {
dc.drawText(cx, y, smallFont, Ui.fit(dc, hint, smallFont, 2 * Ui.halfWidthAt(dc, y)), Graphics.TEXT_JUSTIFY_CENTER);
y += smallH;
} else {
- dc.setColor(remaining <= NEXT_CODE_SECONDS ? Ui.WARN_COLOR : Ui.dim(), Graphics.COLOR_TRANSPARENT);
+ dc.setColor(remaining <= NEXT_CODE_SECONDS ? Ui.warn() : Ui.dim(), Graphics.COLOR_TRANSPARENT);
dc.drawText(cx, y, infoFont, remaining + " s", Graphics.TEXT_JUSTIFY_CENTER);
y += dc.getFontHeight(infoFont);
if (remaining <= NEXT_CODE_SECONDS && cached != null) {
@@ -232,7 +232,7 @@ class OtpView extends WatchUi.View {
private function drawTimeLeft(dc as Graphics.Dc, remaining as Number, period as Number) as Void {
var w = dc.getWidth();
var h = dc.getHeight();
- var color = remaining <= NEXT_CODE_SECONDS ? Ui.WARN_COLOR : Ui.accent();
+ var color = remaining <= NEXT_CODE_SECONDS ? Ui.warn() : Ui.accent();
if (Ui.isRound()) {
var r = w / 2 - RING_WIDTH / 2 - 1;
dc.setPenWidth(RING_WIDTH);
diff --git a/source/ui/PinView.mc b/source/ui/PinView.mc
index fa58bfe..47b12fd 100644
--- a/source/ui/PinView.mc
+++ b/source/ui/PinView.mc
@@ -4,7 +4,8 @@
// README 13.1 and 13.4:
// * A PIN has exactly 4 digits (KeyManager.PIN_DIGITS) and is submitted
// as soon as the 4th is entered; there is no OK key.
-// * Touch screens get a keypad (1-9, delete, 0). The START button does
+// * Touch screens get a keypad (1-9, then 0 and delete in the bottom row,
+// delete on the right as on phone keypads). The START button does
// nothing there: a tap can also arrive as START, which used to submit a
// PIN early. Watches without a touch screen get a digit selector:
// UP/DOWN change the digit, START adds it, BACK deletes.
@@ -32,9 +33,11 @@ class PinView extends WatchUi.View {
// Selector positions 0-9 are digits; this one is "OK" (legacy PINs only).
static const SELECT_OK = 10;
- // Keypad keys besides the digits (OK for legacy PINs only).
+ // Keypad keys besides the digits (OK for legacy PINs only), and an
+ // empty slot.
static const KEY_DELETE = -1;
static const KEY_OK = -2;
+ static const KEY_NONE = -3;
// Calibrate the PIN derivation to about this long on this watch.
static const DERIVATION_TARGET_MS = 1000;
static const CALIBRATION_SAMPLE = 200;
@@ -358,7 +361,7 @@ class PinView extends WatchUi.View {
var message = wait > 0 ? "Wrong PINs. Wait " + Ui.minutesSeconds(wait) : _message;
if (message != null) {
- dc.setColor(_messageIsError || wait > 0 ? Ui.WARN_COLOR : Ui.accent(), Graphics.COLOR_TRANSPARENT);
+ dc.setColor(_messageIsError || wait > 0 ? Ui.warn() : Ui.accent(), Graphics.COLOR_TRANSPARENT);
dc.drawText(cx, y, Graphics.FONT_XTINY, Ui.fit(dc, message, Graphics.FONT_XTINY, 2 * Ui.halfWidthAt(dc, y)),
Graphics.TEXT_JUSTIFY_CENTER);
}
@@ -372,6 +375,9 @@ class PinView extends WatchUi.View {
if (!_shownLogged) {
_shownLogged = true;
+ // For UI scripts: the state the dots and the title show (never digits).
+ Diag.data("pin_state", "mode=" + mode + " digits=" + _digits.size() + " repeat=" + (_first != null ? 1 : 0)
+ + " busy=" + (busy ? 1 : 0));
Diag.shown(wait > 0 ? "pin_wait" : (_message != null && _messageIsError ? "pin_error" : "pin"));
}
}
@@ -394,19 +400,21 @@ class PinView extends WatchUi.View {
keyW = keyH * 2;
}
var left = w / 2 - 3 * keyW / 2;
- var labels = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "", "0", "OK"];
- var values = [1, 2, 3, 4, 5, 6, 7, 8, 9, KEY_DELETE, 0, KEY_OK];
+ // A legacy PIN needs OK: delete, 0, OK. Otherwise the 4th digit
+ // submits, and the bottom row is an empty slot, 0 and delete.
+ var values = legacy ? [1, 2, 3, 4, 5, 6, 7, 8, 9, KEY_DELETE, 0, KEY_OK]
+ : [1, 2, 3, 4, 5, 6, 7, 8, 9, KEY_NONE, 0, KEY_DELETE];
var font = Graphics.FONT_MEDIUM;
_keys = [] as Array<Array<Number>>;
var log = "";
for (var i = 0; i < 12; i++) {
- if (values[i] == KEY_OK && !legacy) {
- continue; // no OK key: the 4th digit submits; the slot stays empty
+ if (values[i] == KEY_NONE) {
+ continue;
}
var kx = left + (i % 3) * keyW;
var ky = top + (i / 3) * keyH;
_keys.add([kx, ky, keyW, keyH, values[i] as Number]);
- var label = labels[i] as String;
+ var label = values[i] == KEY_OK ? "OK" : (values[i] as Number).toString();
var isAction = (values[i] as Number) < 0;
dc.setColor(disabled ? (Ui.dark() ? 0x444444 : Graphics.COLOR_LT_GRAY) : (isAction ? Ui.accent() : Ui.fg()),
Graphics.COLOR_TRANSPARENT);
@@ -416,7 +424,7 @@ class PinView extends WatchUi.View {
var f = isAction ? Graphics.FONT_SMALL : font;
dc.drawText(kx + keyW / 2, ky + (keyH - dc.getFontHeight(f)) / 2, f, label, Graphics.TEXT_JUSTIFY_CENTER);
}
- log += (i > 0 ? ";" : "") + (values[i] as Number) + "=" + (kx + keyW / 2) + "," + (ky + keyH / 2);
+ log += (log.length() > 0 ? ";" : "") + (values[i] as Number) + "=" + (kx + keyW / 2) + "," + (ky + keyH / 2);
}
if (!_shownLogged) {
Diag.data("pin_keys", log);
@@ -444,7 +452,10 @@ class PinView extends WatchUi.View {
private function drawSelector(dc as Graphics.Dc, top as Number, bottom as Number, disabled as Boolean) as Void {
var cx = dc.getWidth() / 2;
- var mid = (top + bottom) / 2;
+ // Centred above the hint, so the lower arrow never touches it (it
+ // did on the Forerunner 255S).
+ var hintTop = bottom - dc.getFontHeight(Graphics.FONT_XTINY);
+ var mid = (top + hintTop) / 2;
var font = Graphics.FONT_NUMBER_MEDIUM;
var label = _selector == SELECT_OK ? "OK" : _selector.toString();
if (_selector == SELECT_OK) {
@@ -458,7 +469,7 @@ class PinView extends WatchUi.View {
dc.fillPolygon([[cx - 8, mid + a], [cx + 8, mid + a], [cx, mid + a + 8]] as Array<Graphics.Point2D>);
dc.setColor(Ui.dim(), Graphics.COLOR_TRANSPARENT);
var hint = "START: add";
- dc.drawText(cx, bottom - dc.getFontHeight(Graphics.FONT_XTINY), Graphics.FONT_XTINY,
+ dc.drawText(cx, hintTop, Graphics.FONT_XTINY,
Ui.fit(dc, hint, Graphics.FONT_XTINY, 2 * Ui.halfWidthAt(dc, bottom)), Graphics.TEXT_JUSTIFY_CENTER);
}
}
diff --git a/source/ui/Ui.mc b/source/ui/Ui.mc
index a8d1ec7..bf52520 100644
--- a/source/ui/Ui.mc
+++ b/source/ui/Ui.mc
@@ -15,6 +15,7 @@ module Ui {
const ELLIPSIS = "…";
const WARN_COLOR = 0xFF5555;
+ const LIGHT_WARN_COLOR = 0xCC0000; // 0xFF5555 is too faint on white for small text
const ACCENT_COLOR = 0x00AAAA; // the teal of the launcher icon
const LIGHT_ACCENT_COLOR = 0x007777;
@@ -71,6 +72,11 @@ module Ui {
return dark() ? ACCENT_COLOR : LIGHT_ACCENT_COLOR;
}
+ // Warnings (wrong PIN, the last seconds of a code) in the theme.
+ function warn() as Number {
+ return dark() ? WARN_COLOR : LIGHT_WARN_COLOR;
+ }
+
// Clear the screen in the theme's background.
function clear(dc as Graphics.Dc) as Void {
dc.setColor(fg(), bg());
@@ -142,7 +148,7 @@ module Ui {
if (period <= 0 || remaining <= 0) {
return;
}
- dc.setColor(remaining <= 5 ? WARN_COLOR : accent(), Graphics.COLOR_TRANSPARENT);
+ dc.setColor(remaining <= 5 ? warn() : accent(), Graphics.COLOR_TRANSPARENT);
var pen = r / 2 > 1 ? r / 2 : 1;
dc.setPenWidth(pen);
var sweep = 360 * remaining / period;