diff --git a/apps/kbmorse/ChangeLog b/apps/kbmorse/ChangeLog new file mode 100644 index 000000000..f62348ec8 --- /dev/null +++ b/apps/kbmorse/ChangeLog @@ -0,0 +1 @@ +0.01: New Keyboard! \ No newline at end of file diff --git a/apps/kbmorse/README.md b/apps/kbmorse/README.md new file mode 100644 index 000000000..2d5aa166f --- /dev/null +++ b/apps/kbmorse/README.md @@ -0,0 +1,25 @@ +# Morse Keyboard + +A library that provides the ability to input text by entering morse code. + +![demo](demo.gif) + +## Usage + + +* Press `BTN1` to input a dot, `BTN3` to input a dash, and `BTN2` to accept the +character for your current input. +* Long-press `BTN1` to toggle UPPERCASE for your next character. +* Long-press `BTN2` to finish editing. +* Tap the left side of the screen for backspace. +* Swipe left/right to move the cursor. +* Input three spaces in a row for a newline. + +The top/bottom of the screen show which characters start with your current input, +so basically you just look which side includes the letter you want to type, and +press that button to narrow your selection, until it appears next to `BTN2`. + + +## For Developers + +See the README for `kbswipe`/`kbtouch` for instructions on how to use this in your app. \ No newline at end of file diff --git a/apps/kbmorse/app.png b/apps/kbmorse/app.png new file mode 100644 index 000000000..0abc7e67d Binary files /dev/null and b/apps/kbmorse/app.png differ diff --git a/apps/kbmorse/demo.gif b/apps/kbmorse/demo.gif new file mode 100644 index 000000000..991c8c68d Binary files /dev/null and b/apps/kbmorse/demo.gif differ diff --git a/apps/kbmorse/lib.js b/apps/kbmorse/lib.js new file mode 100644 index 000000000..8bc177a46 --- /dev/null +++ b/apps/kbmorse/lib.js @@ -0,0 +1,247 @@ +exports.input = function(options) { + options = options || {}; + let text = options.text; + if ("string"!= typeof text) text = ""; + let code = "", + cur = text.length, // cursor position + uc = !text.length, // uppercase + spc = 0; // consecutive spaces entered + + const codes = { + // letters + "a": ".-", + "b": "-...", + "c": "-.-.", + "d": "-..", + "e": ".", + // no é + "f": "..-.", + "g": "--.", + "h": "....", + "i": "..", + "j": ".---", + "k": "-.-", + "l": ".-..", + "m": "--", + "n": "-.", + "o": "---", + "p": ".--.", + "q": "--.-", + "r": ".-.", + "s": "...", + "t": "-", + "u": "..-", + "v": "...-", + "w": ".--", + "x": "-..-", + "y": "-.--", + "z": "--..", + //digits + "1": ".----", + "2": "..---", + "3": "...--", + "4": "....-", + "5": ".....", + "6": "-....", + "7": "--...", + "8": "---..", + "9": "----.", + "0": "-----", + // punctuation + ".": ".-.-.-", + ",": "--..--", + ":": "---...", + "?": "..--..", + "!": "-.-.--", + "'": ".----.", + "-": "-....-", + "_": "..--.-", + "/": "-..-.", + "(": "-.--.", + ")": "-.--.-", + "\"": ".-..-.", + "=": "-...-", + "+": ".-.-.", + "*": "-..-", + "@": ".--.-.", + "$": "...-..-", + "&": ".-...", + }, chars = Object.keys(codes); + + function choices(start) { + return chars.filter(char => codes[char].startsWith(start)); + } + function char(code) { + if (code==="") return " "; + for(const char in codes) { + if (codes[char]===code) return char; + } + const c = choices(code); + if (c.length===1) return c[0]; // "-.-.-" is nothing, and only "-.-.--"(!) starts with it + return null; + } + + return new Promise((resolve, reject) => { + + function update() { + let dots = [], dashes = []; + layout.pick.label = (code==="" ? " " : ""); + choices(code).forEach(char => { + const c = codes[char]; + if (c===code) { + layout.pick.label = char; + } + const next = c.substring(code.length, code.length+1); + if (next===".") dots.push(char); + else if (next==="-") dashes.push(char); + }); + if (!code && spc>1) layout.pick.label = atob("ABIYAQAAAAAAAAAABwABwABwABwABwABwOBwOBwOBxwBxwBxwB/////////xwABwABwAAOAAOAAOAA=="); + g.setFont("6x8:2"); + const wrap = t => g.wrapString(t, Bangle.appRect.w-60).join("\n"); + layout.del.label = cur ? atob("AAwIAQ/hAiKkEiKhAg/gAA==") : " "; + layout.code.label = code; + layout.dots.label = wrap(dots.join(" ")); + layout.dashes.label = wrap(dashes.join(" ")); + if (uc) { + layout.pick.label = layout.pick.label.toUpperCase(); + layout.dots.label = layout.dots.label.toUpperCase(); + layout.dashes.label = layout.dashes.label.toUpperCase(); + } + let label = text.slice(0, cur)+"|"+text.slice(cur); + layout.text.label = g.wrapString(label, Bangle.appRect.w-80).join("\n") + .replace("|", atob("AAwQAfPPPAwAwAwAwAwAwAwAwAwAwAwAwPPPPA==")); + layout.update(); + layout.render(); + } + + function add(d) { + code += d; + const l = choices(code).length; + if (l===1) done(); + else if (l<1) { + Bangle.buzz(20); + code = code.slice(0, -1); + } else update(); + } + function del() { + if (code.length) code = code.slice(0, -1); // delete last dot/dash + else if (cur) { // delete char at cursor + text = text.slice(0, cur-1)+text.slice(cur); + cur--; + } else Bangle.buzz(20); // (already) at start of text + spc = 0; + uc = false; + update(); + } + + function done() { + let c = char(code); + if (c!==null) { + if (uc) c = c.toUpperCase(); + uc = false; + text = text.slice(0, cur)+c+text.slice(cur); + cur++; + code = ""; + if (c===" ") spc++; + else spc = 0; + if (spc>=3) { + text = text.slice(0, cur-3)+"\n"+text.slice(cur); + cur -= 2; + uc = true; + spc = 0; + } + update(); + } else { + console.log(`No char for ${code}!`); + Bangle.buzz(20); + } + } + + const Layout = require("Layout"); + let layout = new Layout({ + type: "h", c: [ + { + type: "v", width: Bangle.appRect.w-8, bgCol: g.theme.bg, c: [ + {id: "dots", type: "txt", font: "6x8:2", label: "", fillx: 1, bgCol: g.theme.bg}, + {filly: 1, bgCol: g.theme.bg}, + { + type: "h", fillx: 1, c: [ + {id: "del", type: "txt", font: "6x8", label: " + ({type: "txt", font: "6x8", height: Math.floor(Bangle.appRect.h/3), r: 1, label: l}) + ) + } + ] + }); + g.reset().clear(); + update(); + + if (Bangle.btnWatches) Bangle.btnWatches.forEach(clearWatch); + Bangle.btnWatches = []; + + // BTN1: press for dot, long-press to toggle uppercase + let ucTimeout; + const UC_TIME = 500; + Bangle.btnWatches.push(setWatch(e => { + if (ucTimeout) clearTimeout(ucTimeout); + ucTimeout = null; + if (e.state) { + // pressed: start UpperCase toggle timer + ucTimeout = setTimeout(() => { + ucTimeout = null; + uc = !uc; + update(); + }, UC_TIME); + } else if (e.time-e.lastTime { + if (enterTimeout) clearTimeout(enterTimeout); + enterTimeout = null; + if (e.state) { + // pressed: start UpperCase toggle timer + enterTimeout = setTimeout(() => { + enterTimeout = null; + resolve(text); + }, ENTER_TIME); + } else if (e.time-e.lastTime { + add("-"); + }, BTN3, {repeat: true, edge: "falling"})); + + // Left-hand side: backspace + if (Bangle.touchHandler) Bangle.removeListener("touch", Bangle.touchHandler); + Bangle.touchHandler = side => { + if (side===1) del(); + }; + Bangle.on("touch", Bangle.touchHandler); + + // swipe: move cursor + if (Bangle.swipeHandler) Bangle.removeListener("swipe", Bangle.swipeHandler); + Bangle.swipeHandler = dir => { + cur = Math.max(0, Math.min(text.length, cur+dir)); + update(); + }; + Bangle.on("swipe", Bangle.swipeHandler); + }); +}; \ No newline at end of file diff --git a/apps/kbmorse/metadata.json b/apps/kbmorse/metadata.json new file mode 100644 index 000000000..f9c5354f1 --- /dev/null +++ b/apps/kbmorse/metadata.json @@ -0,0 +1,15 @@ +{ + "id": "kbmorse", + "name": "Morse keyboard", + "version": "0.01", + "description": "A library for text input as morse code", + "icon": "app.png", + "type": "textinput", + "tags": "keyboard", + "supports" : ["BANGLEJS"], + "screenshots": [{"url":"screenshot.png"}], + "readme": "README.md", + "storage": [ + {"name":"textinput","url":"lib.js"} + ] +} diff --git a/apps/kbmorse/screenshot.png b/apps/kbmorse/screenshot.png new file mode 100644 index 000000000..9050a45cd Binary files /dev/null and b/apps/kbmorse/screenshot.png differ