diff options
Diffstat (limited to 'source/settings/CodeForm.mc')
| -rw-r--r-- | source/settings/CodeForm.mc | 214 |
1 files changed, 214 insertions, 0 deletions
diff --git a/source/settings/CodeForm.mc b/source/settings/CodeForm.mc new file mode 100644 index 0000000..89d519d --- /dev/null +++ b/source/settings/CodeForm.mc @@ -0,0 +1,214 @@ +// Author: cirakg & claude ai +// Summary: The Add code form (README 10.2): reads the code* fields, and CodeCheck, the job that proves a code encodes and fits. +// +// read() checks the fields that need no encoding (name, symbology, length). +// Whether the contents are valid for the symbology is decided by the real +// encoder (README 12.1), in CodeCheck: +// * Linear codes encode at once; the encoder's message ("EAN-13: check +// digit should be 7, got 3. Choose 'Fix if wrong' to correct it") becomes +// the status, and its note ("check digit 7 added") a remark. +// * 2D codes run their MatrixJob step by step (the watchdog), then the +// matrix is packed for the encrypted cache in a step of its own, so the +// first opening on the watch is instant. +// * A code that encodes but doesn't fit this screen is still added, with +// the "Too large for this screen" warning (README 12.2). + +import Toybox.Lang; +import Toybox.System; + +module CodeForm { + + const SWITCH = "codeAdd"; + const STATUS = "codeStatus"; + const PAYLOAD = "codePayload"; + const NAME_MAX_BYTES = Entry.MAX_NAME_BYTES; + + const FIELDS = ["codeName", "codeSymbology", "codePayload", "codeEcLevel", "codeCheckDigit", "codeShowText", "codeGroup", + "codeLocked"]; + + // Field values after a reset (resources/settings/properties.xml); + // the contents first. + function defaults() as Dictionary<String, String or Number or Boolean> { + return { + PAYLOAD => "", + "codeName" => "", + "codeSymbology" => Codes.QR, + "codeEcLevel" => 1, + "codeCheckDigit" => Codes.CHECK_ADD_VERIFY, + "codeShowText" => true, + "codeGroup" => "", + "codeLocked" => false + } as Dictionary<String, String or Number or Boolean>; + } + + // The entry described by the form (not yet checked by the encoder). + function read(store as Forms.Store) as Forms.Parsed { + var parsed = new Forms.Parsed("code"); + for (var i = 0; i < FIELDS.size(); i++) { + parsed.content.add(store.get(FIELDS[i] as String)); + } + var e = new Entry(); + e.kind = Entry.KIND_CODE; + e.source = Entry.SOURCE_MANUAL; + e.name = Forms.text(store, "codeName"); + e.group = Forms.text(store, "codeGroup"); + e.locked = Forms.flag(store, "codeLocked"); + if (e.name.length() == 0) { + return parsed.fail("Name: enter a name for the card"); + } + var problem = Forms.checkName("Name", e.name, NAME_MAX_BYTES); + if (problem == null) { + problem = Forms.checkName("Group", e.group, Entry.MAX_GROUP_BYTES); + } + if (problem != null) { + return parsed.fail(problem); + } + var symbology = Forms.number(store, "codeSymbology"); + if (symbology == null || symbology < Codes.QR || symbology > Codes.AZTEC) { + return parsed.fail("Type: choose one of the 12 barcode types"); + } + e.type = symbology; + var ec = Forms.number(store, "codeEcLevel"); + var check = Forms.number(store, "codeCheckDigit"); + if (ec == null || ec < 0 || ec > Codes.OPT_EC_MASK) { + ec = 1; + } + if (check == null || check < Codes.CHECK_ADD_VERIFY || check > Codes.CHECK_OFF) { + check = Codes.CHECK_ADD_VERIFY; + } + e.options = ec | (check << Codes.OPT_CHECK_SHIFT) | (Forms.flag(store, "codeShowText") ? Codes.OPT_SHOW_TEXT : 0); + var payload = contents(Forms.rawText(store, PAYLOAD), symbology); + if (payload instanceof String) { + return parsed.fail(payload); + } + e.payload = payload; + parsed.entry = e; + return parsed; + } + + // The contents as stored (trimmed for linear codes, whose digits are + // often typed with spaces around), or the problem. + function contents(text as String, symbology as Number) as ByteArray or String { + var value = Codes.isLinear(symbology) ? Text.trim(text) : text; + if (Text.trim(value).length() == 0) { + return "Contents: missing"; + } + var bytes = Bytes.fromUtf8(value); + if (bytes.size() > Entry.MAX_PAYLOAD_BYTES) { + return "Contents: too long (" + bytes.size() + " bytes, at most " + Entry.MAX_PAYLOAD_BYTES + ")"; + } + return bytes; + } + + function reset(store as Forms.Store, everything as Boolean) as Void { + store.set(PAYLOAD, ""); + if (everything) { + var d = defaults(); + var keys = d.keys(); + for (var i = 0; i < keys.size(); i++) { + store.set(keys[i], d[keys[i]] as String or Number or Boolean); + } + } + store.set(SWITCH, false); + } +} + +// Encodes a code once to prove it is valid and to check that it fits this +// screen. When step() returns true: error is set, or the code is fine +// (notes may hold remarks) and, for 2D codes, cache holds Matrix.toBytes(). +class CodeCheck extends Jobs.Job { + + // Linear codes draw with 10 quiet modules on each side (CodeView). + static const LINEAR_QUIET = 10; + + var error as String? = null; + var notes as Array<String> = [] as Array<String>; + var cache as ByteArray? = null; + + private var _symbology as Number; + private var _payload as String; + private var _options as Number; + private var _job as Codes.MatrixJob? = null; + private var _matrix as Codes.Matrix? = null; + private var _started as Boolean = false; + private var _w as Number; + private var _h as Number; + private var _round as Boolean; + + function initialize(symbology as Number, payload as String, options as Number) { + Job.initialize(); + _symbology = symbology; + _payload = payload; + _options = options; + var screen = System.getDeviceSettings(); + _w = screen.screenWidth; + _h = screen.screenHeight; + _round = screen.screenShape == System.SCREEN_SHAPE_ROUND; + } + + function step(budget as Number) as Boolean { + if (!_started) { + _started = true; + if (!Codes.isMatrix(_symbology)) { + checkLinear(); + return true; + } + _job = Codes.matrixJob(_symbology, _payload, _options, _w, _h, _round); + _payload = ""; + return false; + } + var matrix = _matrix; + if (matrix != null) { + // Packing gets a step of its own (it runs over every module). + cache = matrix.toBytes(); + _matrix = null; + return true; + } + var job = _job as Codes.MatrixJob; + if (!job.step(budget)) { + return false; + } + _job = null; + var m = job.matrix(); + var err = job.error; + if (err != null || m == null) { + error = withName(err != null ? err : "could not encode"); + return true; + } + var totalW = m.width + 2 * m.quiet; + var totalH = m.drawnHeight() + 2 * m.quiet; + var px = Codes.minModulePx(_symbology); + if (!Codes.fits(totalW, totalH, px, _w, _h, _round)) { + notes.add("Too large for this screen: needs " + px + " px per module, " + totalW * px + " x " + totalH * px + " px"); + } + _matrix = m; + return false; + } + + private function checkLinear() as Void { + var result = Codes.encode(_symbology, _payload, _options); + _payload = ""; + if (result instanceof Codes.CodeError) { + error = withName(result.message); + return; + } + var note = result.note; + if (note != null) { + notes.add(note); + } + // The widest band is a little under the screen width on round + // screens (CodeView measures it exactly when drawing). + var available = _round ? _w * 96 / 100 : _w - 4; + var needed = result.modules.size() + 2 * LINEAR_QUIET; + if (needed > available) { + notes.add("Too large for this screen: needs " + needed + " px, " + available + " px available"); + } + } + + // "too long for QR..." becomes "QR: too long for QR...", but a message + // that already names the symbology is kept as it is. + private function withName(message as String) as String { + var name = Codes.name(_symbology); + return message.find(name) == 0 ? message : name + ": " + message; + } +} |
