diff --git a/README.md b/README.md index 8e186cf79..6d0a981ac 100644 --- a/README.md +++ b/README.md @@ -172,12 +172,13 @@ The widget example is available in [`apps/_example_widget`](apps/_example_widget Widgets are just small bits of code that run whenever an app that supports them calls `Bangle.loadWidgets()`. If they want to display something in the 24px high -widget bars at the top and bottom of the screen they can add themselves to -the global `WIDGETS` array with: +widget bar at the top of the screen they can add themselves to the global +`WIDGETS` array with: ``` WIDGETS["mywidget"]={ - area:"tl", // tl (top left), tr (top right), bl (bottom left), br (bottom right) + area:"tl", // tl (top left), tr (top right) + sortorder:0, // (Optional) determines order of widgets in the same corner width: 24, // how wide is the widget? You can change this and call Bangle.drawWidgets() to re-layout draw:draw // called to draw the widget }; @@ -461,16 +462,13 @@ The screen is parted in a widget and app area for lcd mode `direct`(default). | areas | as rectangle or point | | :-:| :-: | | Widget | (0,0,239,23) | -| Widget bottom bar (optional) | (0,216,239,239) | -| Apps | (0,24,239,239) (see below) | +| Apps | (0,24,239,239) | | BTN1 | (230, 55) | | BTN2 | (230, 140) | | BTN3 | (230, 210) | | BTN4 | (0,0,119, 239)| | BTN5 | (120,0,239,239) | -- If there are widgets at the bottom of the screen, apps should actually keep the bottom 24px free, so should keep to the area (0,24,239,215) - - Use `g.setFontAlign(0, 0, 3)` to draw rotated string to BTN1-BTN3 with `g.drawString()`. - For BTN4-5 the touch area is named diff --git a/apps.json b/apps.json index 2e11e37f6..fe37683f3 100644 --- a/apps.json +++ b/apps.json @@ -5682,5 +5682,23 @@ {"name":"crowclk.app.js","url":"crow_clock.js"}, {"name":"crowclk.img","url":"crow_clock-icon.js","evaluate":true} ] + }, + { + "id": "wid_edit", + "version": "0.01", + "name": "Widget Editor", + "icon": "icon.png", + "description": "Customize widget locations", + "supports": ["BANGLEJS", "BANGLEJS2"], + "readme": "README.md", + "type": "bootloader", + "tags": "widget,tool", + "storage": [ + {"name":"wid_edit.boot.js","url":"boot.js"}, + {"name":"wid_edit.settings.js","url":"settings.js"} + ], + "data": [ + {"name":"wid_edit.json"} + ] } ] diff --git a/apps/wid_edit/ChangeLog b/apps/wid_edit/ChangeLog new file mode 100644 index 000000000..2fa857bd8 --- /dev/null +++ b/apps/wid_edit/ChangeLog @@ -0,0 +1 @@ +0.01: new Widget Editor! \ No newline at end of file diff --git a/apps/wid_edit/README.md b/apps/wid_edit/README.md new file mode 100644 index 000000000..e5003280c --- /dev/null +++ b/apps/wid_edit/README.md @@ -0,0 +1,16 @@ +# Widget Editor + +This adds a setting menu which allows you to change the location of widgets. + +## Settings + +There is no app icon in the launcher; you can find the settings under +`Apps`->`Widget Editor`. + +For every widget, you have these options: +* **Side**: On which side to draw the widget. +* **Sort Order**: Changes the order if several widgets use the same side. + +## Creator + +Richard de Boer diff --git a/apps/wid_edit/boot.js b/apps/wid_edit/boot.js new file mode 100644 index 000000000..872965c97 --- /dev/null +++ b/apps/wid_edit/boot.js @@ -0,0 +1,24 @@ +Bangle.loadWidgets = function() { + global.WIDGETS={}; + require("Storage").list(/\.wid\.js$/) + .forEach(w=>{ + try { eval(require("Storage").read(w)); } + catch (e) { print(w, e); } + }); + const s = require("Storage").readJSON("wid_edit.json", 1) || {}, + c = s.custom || {}; + for (const w in c){ + if (!(w in WIDGETS)) continue; // widget no longer exists + // store defaults of customized values in _WIDGETS + global._WIDGETS=global._WIDGETS||{}; + _WIDGETS[w] = {}; + Object.keys(c[w]).forEach(k => _WIDGETS[w][k] = WIDGETS[w][k]); + Object.assign(WIDGETS[w], c[w]); + } + const W = WIDGETS; + WIDGETS = {}; + Object.keys(W) + .sort() + .sort((a, b) => (0|W[b].sortorder)-(0|W[a].sortorder)) + .forEach(k => WIDGETS[k] = W[k]); +} \ No newline at end of file diff --git a/apps/wid_edit/icon.png b/apps/wid_edit/icon.png new file mode 100644 index 000000000..1d072c381 Binary files /dev/null and b/apps/wid_edit/icon.png differ diff --git a/apps/wid_edit/settings.js b/apps/wid_edit/settings.js new file mode 100644 index 000000000..0969ed533 --- /dev/null +++ b/apps/wid_edit/settings.js @@ -0,0 +1,190 @@ +/** + * @param {function} back Use back() to return to settings menu + */ +(function(back) { + const names = {}; + const settings = require("Storage").readJSON("wid_edit.json", 1) || {}; + if (!('custom' in settings)) settings.custom = {}; + global._WIDGETS = global._WIDGETS || {}; + + let cleanup = false; + for (const id in settings.custom) { + if (!(id in WIDGETS)) { + // widget which no longer exists + cleanup = true; + delete settings.custom[id]; + } + } + if (cleanup) { + if (!Object.keys(settings.custom).length) delete settings.custom; + require("Storage").writeJSON("wid_edit.json", settings); + } + + /** + * Sort & redraw all widgets + */ + function redrawWidgets() { + let W = WIDGETS; + global.WIDGETS = {}; + Object.keys(W) + .sort() + .sort((a, b) => (0|W[b].sortorder)-(0|W[a].sortorder)) + .forEach(k => {WIDGETS[k] = W[k]}); + Bangle.drawWidgets(); + } + + /** + * Try to find app name for widget + * @param {string} widget WIDGETS key + * @return {string} widget name + */ + function name(widget) { + if (!(widget in names)) { + let infoFile = widget+".info"; + // widget names don't always correspond to appid :-( + // so we try both with and without 'wid'-prefix + if (!require("Storage").list(new RegExp(`^${infoFile}$`)).length) { + infoFile = (widget.substr(0, 3)==="wid") ? infoFile.substr(3) : ("wid"+infoFile); + } + names[widget] = (require("Storage").readJSON(infoFile, 1) || {}).name || widget; + } + return names[widget]; + } + + function edit(id) { + let WIDGET = WIDGETS[id], + def = {area: WIDGET.area, sortorder: WIDGET.sortorder|0}; // default values + Object.assign(def, _WIDGETS[id]||{}); // defaults were saved in _WIDGETS + + settings.custom = settings.custom||{}; + let saved = settings.custom[id] || {}, + area = saved.area || def.area, + sortorder = ("sortorder" in saved) ? saved.sortorder : def.sortorder; + + /** + * Draw highlighted widget + */ + function highlight() { + if (WIDGET.width > 0) { + // draw widget, then draw a highlighted border on top + WIDGET.draw(); + g.setColor(g.theme.fgH) + .drawRect(WIDGET.x, WIDGET.y, WIDGET.x+WIDGET.width-1, WIDGET.y+23); + } else { + // hidden widget: fake a width and provide our own draw() + const draw = WIDGET.draw, width = WIDGET.width; + WIDGET.width = 24; + WIDGET.draw = function() { + g.setColor(g.theme.bgH).setColor(g.theme.fgH) + .clearRect(this.x, this.y, this.x+23, this.y+23) + .drawRect(this.x, this.y, this.x+23, this.y+23) + .drawLine(this.x, this.y, this.x+23, this.y+23) + .drawLine(this.x, this.y+23, this.x+23, this.y); + }; + // re-layout+draw all widgets with our placeholder in between + redrawWidgets(); + // and restore original values + WIDGET.draw = draw; + WIDGET.width = width; + } + } + highlight(); + + /** + * Save widget and redraw with new settings + */ + function save() { + // we only save non-default values + saved = {}; + if ((area!==def.area) || (sortorder!==def.sortorder)) { + if (area!==def.area) saved.area = area; + if (sortorder!==def.sortorder) saved.sortorder = sortorder; + settings.custom = settings.custom || {}; + settings.custom[id] = saved; + } else if (settings.custom) { + delete settings.custom[id] + } + if (!Object.keys(settings.custom).length) delete settings.custom; + require("Storage").writeJSON("wid_edit.json", settings); + Object.assign(WIDGET, def, saved); + if (WIDGET.sortorder === undefined) delete WIDGET.sortorder; // default can be undefined, but don't put that in the widget + // if we assigned custom values, store defaults in _WIDGETS + let _W = {}; + if (saved.area) _W.area = def.area; + if ('sortorder' in saved) _W.sortorder = def.sortorder; + if (Object.keys(_W).length) _WIDGETS[id] = _W; + else delete _WIDGETS[id]; + + // drawWidgets won't clear e.g. bottom bar if we just disabled the last bottom widget + redrawWidgets(); + + highlight(); + m.draw(); + } + + const menu = { + "": {"title": name(id)}, + /*LANG*/"< Back": () => { + redrawWidgets(); + mainMenu(); + }, + /*LANG*/"Side": { + value: (area === 'tl'), + format: tl => tl ? /*LANG*/"Left" : /*LANG*/"Right", + onchange: tl => { + area = tl ? "tl" : "tr"; + save(); + } + }, + /*LANG*/"Sort Order": { + value: sortorder, + onchange: o => { + sortorder = o; + save(); + } + }, + /*LANG*/"Reset": () => { + area = def.area; + sortorder = def.sortorder; + save(); + mainMenu(); // changing multiple values made the rest of the menu wrong, so take the easy out + } + } + + let m = E.showMenu(menu); + } + + + function mainMenu() { + let menu = { + "": {"title": /*LANG*/"Widgets"}, + }; + menu[/*LANG*/"< Back"] = ()=>{ + if (!Object.keys(_WIDGETS).length) delete _WIDGETS; // no defaults to remember + back(); + }; + Object.keys(WIDGETS).forEach(id=>{ + // mark customized widgets with asterisk + menu[name(id)+((id in _WIDGETS) ? " *" : "")] = () => edit(id); + }); + if (Object.keys(_WIDGETS).length) { // only show reset if there is anything to reset + menu[/*LANG*/"Reset All"] = () => { + E.showPrompt(/*LANG*/"Reset all widgets?").then(confirm => { + if (confirm) { + delete settings.custom; + require("Storage").writeJSON("wid_edit.json", settings); + for(let id in _WIDGETS) { + Object.assign(WIDGETS[id], _WIDGETS[id]) // restore defaults + } + global._WIDGETS = {}; + redrawWidgets(); + } + mainMenu(); // reload with reset widgets + }) + } + } + + E.showMenu(menu); + } + mainMenu(); +});