diff --git a/apps/promenu/ChangeLog b/apps/promenu/ChangeLog index e9ab57880..e06007c98 100644 --- a/apps/promenu/ChangeLog +++ b/apps/promenu/ChangeLog @@ -1,3 +1,7 @@ 0.01: New App! 0.02: Add Bangle.js 2 Support 0.03: Minor code improvements +0.04: Fix display/overwriting of boolean menu items. Handle back + functionality, both via `.back` and `"< Back"` items, displaying an + entry and the `setUI` back widget. Fix `setUI`'s back overwrite. Add + support for scroll. diff --git a/apps/promenu/README.md b/apps/promenu/README.md index 8b1378917..585e72cce 100644 --- a/apps/promenu/README.md +++ b/apps/promenu/README.md @@ -1 +1,3 @@ +# Description +A menu replacement controlled by dragging on the screen to change the focused menu entry. Single tap to activate the selected entry. Promenu can display inline boolean and number adjustments - scrolling with a number entry selected will increase/decrese the number. Swipe left on the menu to go back. diff --git a/apps/promenu/bootb2.js b/apps/promenu/bootb2.js index b02803a7a..87d6e4a54 100644 --- a/apps/promenu/bootb2.js +++ b/apps/promenu/bootb2.js @@ -1,142 +1,175 @@ -E.showMenu = function(items) { - function RectRnd(x1,y1,x2,y2,r) { - let pp = []; - pp.push.apply(pp,g.quadraticBezier([x2-r,y1, x2,y1,x2,y1+r])); - pp.push.apply(pp,g.quadraticBezier([x2,y2-r,x2,y2,x2-r,y2])); - pp.push.apply(pp,g.quadraticBezier([x1+r,y2,x1,y2,x1,y2-r])); - pp.push.apply(pp,g.quadraticBezier([x1,y1+r,x1,y1,x1+r,y1])); - return pp; - } - function fillRectRnd(x1,y1,x2,y2,r,c) { - g.setColor(c); - g.fillPoly(RectRnd(x1,y1,x2,y2,r),1); - g.setColor(255,255,255); - } - function drawRectRnd(x1,y1,x2,y2,r,c) { - g.setColor(c); - g.drawPoly(RectRnd(x1,y1,x2,y2,r),1); - g.setColor(255,255,255); - } - g.reset().clearRect(Bangle.appRect); // clear if no menu supplied - Bangle.setLCDPower(1); // ensure screen is on - if (!items) { - Bangle.setUI(); - return; - } - var menuItems = Object.keys(items); - var options = items[""]; - if (options) menuItems.splice(menuItems.indexOf(""),1); - if (!(options instanceof Object)) options = {}; - options.fontHeight = options.fontHeight||25; - if (options.selected === undefined) - options.selected = 0; - var ar = Bangle.appRect; - var x = ar.x; - var x2 = ar.x2; - var y = ar.y; - var y2 = ar.y2 - 12; // padding at end for arrow - if (options.title) - y += 22; - var loc = require("locale"); - var l = { - lastIdx : 0, - draw : function(rowmin,rowmax) { - var rows = 0|Math.min((y2-y) / options.fontHeight,menuItems.length); - var idx = E.clip(options.selected-( rows>>1),0,menuItems.length-rows); - if (idx!=l.lastIdx) rowmin=undefined; // redraw all if we scrolled - l.lastIdx = idx; - var iy = y; - g.reset().setFontAlign(0,-1,0).setFont('12x20'); - if (options.predraw) options.predraw(g); - if (rowmin===undefined && options.title) - g.drawString(options.title,(x+x2)/2,y-21).drawLine(x,y-2,x2,y-2). - setColor(g.theme.fg).setBgColor(g.theme.bg); - iy += 4; - if (rowmin!==undefined) { - if (idxrowmax) { - rows = 1+rowmax-rowmin; - } - } - while (rows--) { - var name = menuItems[idx]; - var item = items[name]; - var hl = (idx==options.selected && !l.selectEdit); - if(g.theme.dark){ - fillRectRnd(x,iy,x2,iy+options.fontHeight-3,7,hl ? g.theme.bgH : g.theme.bg+40); - }else{ - fillRectRnd(x,iy,x2,iy+options.fontHeight-3,7,hl ? g.theme.bgH : g.theme.bg-20); - } - g.setColor(hl ? g.theme.fgH : g.theme.fg); - g.setFontAlign(-1,-1); - var v = item.value; - v = loc.translate(""+v); - if(loc.translate(name).length >= 17-v.length && "object" == typeof item){ - if (item.format) v=item.format(v); - g.drawString(loc.translate(name).substring(0, 12-v.length)+"...",x+3.7,iy+2.7); - }else{ - if(loc.translate(name).length >= 15){ - g.drawString(loc.translate(name).substring(0, 15)+"...",x+3.7,iy+2.7); - }else{ - g.drawString(loc.translate(name),x+3.7,iy+2.7); - } - } - if ("object" == typeof item) { - var xo = x2; - var v = item.value; - if (item.format) v=item.format(v); - v = loc.translate(""+v); - if (l.selectEdit && idx==options.selected) { - xo -= 24 + 1; - g.setColor(g.theme.fgH).drawImage("\x0c\x05\x81\x00 \x07\x00\xF9\xF0\x0E\x00@",xo,iy+(options.fontHeight-10)/2,{scale:2}); - } - g.setFontAlign(1,-1); - g.drawString(v,xo-2,iy+1); - } - g.setColor(g.theme.fg); - iy += options.fontHeight; - idx++; - } - g.setFontAlign(-1,-1); - g.setColor((idxitem.max) item.value = item.wrap ? item.min : item.max; - if (item.onchange) item.onchange(item.value); - l.draw(options.selected,options.selected); - } else { - var lastSelected=options.selected; - options.selected = (dir+options.selected+menuItems.length)%menuItems.length; - l.draw(Math.min(lastSelected,options.selected), Math.max(lastSelected,options.selected)); - } +E.showMenu = function (items) { + var RectRnd = function (x1, y1, x2, y2, r) { + var pp = []; + pp.push.apply(pp, g.quadraticBezier([x2 - r, y1, x2, y1, x2, y1 + r])); + pp.push.apply(pp, g.quadraticBezier([x2, y2 - r, x2, y2, x2 - r, y2])); + pp.push.apply(pp, g.quadraticBezier([x1 + r, y2, x1, y2, x1, y2 - r])); + pp.push.apply(pp, g.quadraticBezier([x1, y1 + r, x1, y1, x1 + r, y1])); + return pp; + }; + var fillRectRnd = function (x1, y1, x2, y2, r, c) { + g.setColor(c); + g.fillPoly(RectRnd(x1, y1, x2, y2, r)); + g.setColor(255, 255, 255); + }; + var menuItems = Object.keys(items); + var options = items[""] || {}; + if (!(options instanceof Object)) + options = {}; + if (options) + menuItems.splice(menuItems.indexOf(""), 1); + var fontHeight = options.fontHeight || 25; + var selected = options.scroll || options.selected || 0; + var ar = Bangle.appRect; + g.reset().clearRect(ar); + var x = ar.x; + var x2 = ar.x2; + var y = ar.y; + var y2 = ar.y2 - 12; + if (options.title) + y += 22; + var lastIdx = 0; + var selectEdit = undefined; + var l = { + draw: function (rowmin, rowmax) { + var rows = 0 | Math.min((y2 - y) / fontHeight, menuItems.length); + var idx = E.clip(selected - (rows >> 1), 0, menuItems.length - rows); + if (idx != lastIdx) + rowmin = undefined; + lastIdx = idx; + var iy = y; + g.reset().setFontAlign(0, -1, 0).setFont12x20(); + if (options.predraw) + options.predraw(g); + if (rowmin === undefined && options.title) + g.drawString(options.title, (x + x2) / 2, y - 21).drawLine(x, y - 2, x2, y - 2). + setColor(g.theme.fg).setBgColor(g.theme.bg); + iy += 4; + if (rowmin !== undefined) { + if (idx < rowmin) { + iy += fontHeight * (rowmin - idx); + idx = rowmin; + } + if (rowmax && idx + rows > rowmax) { + rows = 1 + rowmax - rowmin; + } + } + while (rows--) { + var name = menuItems[idx]; + var item = items[name]; + var hl = (idx === selected && !selectEdit); + if (g.theme.dark) { + fillRectRnd(x, iy, x2, iy + fontHeight - 3, 7, hl ? g.theme.bgH : g.theme.bg + 40); + } + else { + fillRectRnd(x, iy, x2, iy + fontHeight - 3, 7, hl ? g.theme.bgH : g.theme.bg - 20); + } + g.setColor(hl ? g.theme.fgH : g.theme.fg); + g.setFontAlign(-1, -1); + var v = void 0; + if (typeof item === "object") { + v = "format" in item + ? item.format(item.value) + : item.value; + if (typeof v !== "string") + v = "".concat(v); + } + else { + v = ""; + } + { + if (name.length >= 17 - v.length && typeof item === "object") { + g.drawString(name.substring(0, 12 - v.length) + "...", x + 3.7, iy + 2.7); + } + else if (name.length >= 15) { + g.drawString(name.substring(0, 15) + "...", x + 3.7, iy + 2.7); + } + else { + g.drawString(name, x + 3.7, iy + 2.7); + } + var xo = x2; + if (selectEdit && idx === selected) { + xo -= 24 + 1; + g.setColor(g.theme.fgH) + .drawImage("\x0c\x05\x81\x00 \x07\x00\xF9\xF0\x0E\x00@", xo, iy + (fontHeight - 10) / 2, { scale: 2 }); + } + g.setFontAlign(1, -1); + g.drawString(v, xo - 2, iy + 1); + } + g.setColor(g.theme.fg); + iy += fontHeight; + idx++; + } + g.setFontAlign(-1, -1); + g.setColor((idx < menuItems.length) ? g.theme.fg : g.theme.bg).fillPoly([72, 166, 104, 166, 88, 174]); + g.flip(); + }, + select: function () { + var item = items[menuItems[selected]]; + if (typeof item === "function") { + item(); + } + else if (typeof item === "object") { + if (typeof item.value === "number") { + selectEdit = selectEdit ? undefined : item; + } + else { + if (typeof item.value === "boolean") + item.value = !item.value; + if (item.onchange) + item.onchange(item.value); + } + l.draw(); + } + }, + move: function (dir) { + var item = selectEdit; + if (typeof item === "object" && typeof item.value === "number") { + item.value += (-dir || 1) * (item.step || 1); + if (item.min && item.value < item.min) + item.value = item.wrap ? item.max : item.min; + if ("max" in item && item.value > item.max) + item.value = item.wrap ? item.min : item.max; + if (item.onchange) + item.onchange(item.value); + l.draw(selected, selected); + } + else { + var lastSelected = selected; + selected = (selected + dir + menuItems.length) % menuItems.length; + l.draw(Math.min(lastSelected, selected), Math.max(lastSelected, selected)); + } + }, + }; + l.draw(); + var back = options.back; + if (!back) { + var backItem = items["< Back"]; + if (typeof backItem === "function") + back = backItem; + else if (backItem && "back" in backItem) + back = backItem.back; } - }; - l.draw(); - Bangle.setUI("updown",dir => { - if (dir) l.move(dir); - else l.select(); - }); - return l; + var onSwipe; + if (typeof back === "function") { + var back_1 = back; + onSwipe = function (lr) { + if (lr < 0) + back_1(); + }; + Bangle.on('swipe', onSwipe); + } + Bangle.setUI({ + mode: "updown", + back: back, + remove: function () { + Bangle.removeListener("swipe", onSwipe); + }, + }, function (dir) { + if (dir) + l.move(dir); + else + l.select(); + }); + return l; }; diff --git a/apps/promenu/bootb2.ts b/apps/promenu/bootb2.ts new file mode 100644 index 000000000..c3ec89bea --- /dev/null +++ b/apps/promenu/bootb2.ts @@ -0,0 +1,196 @@ +type ActualMenuItem = Exclude; + +(E.showMenu as any) = (items: Menu): MenuInstance => { + const RectRnd = (x1: number, y1: number, x2: number, y2: number, r: number) => { + const pp = []; + pp.push(...g.quadraticBezier([x2 - r, y1, x2, y1, x2, y1 + r])); + pp.push(...g.quadraticBezier([x2, y2 - r, x2, y2, x2 - r, y2])); + pp.push(...g.quadraticBezier([x1 + r, y2, x1, y2, x1, y2 - r])); + pp.push(...g.quadraticBezier([x1, y1 + r, x1, y1, x1 + r, y1])); + return pp; + }; + const fillRectRnd = (x1: number, y1: number, x2: number, y2: number, r: number, c: ColorResolvable) => { + g.setColor(c); + g.fillPoly(RectRnd(x1, y1, x2, y2, r)); + g.setColor(255, 255, 255); + }; + const menuItems = Object.keys(items); + let options = items[""] || {}; + if (!(options instanceof Object)) options = {}; + + if (options) + menuItems.splice(menuItems.indexOf(""), 1); + + const fontHeight = options.fontHeight||25; + + let selected = options.scroll || options.selected || 0; + + const ar = Bangle.appRect; + g.reset().clearRect(ar); + + const x = ar.x; + const x2 = ar.x2; + let y = ar.y; + const y2 = ar.y2 - 12; // padding at end for arrow + if (options.title) + y += 22; + + let lastIdx = 0; + let selectEdit: undefined | ActualMenuItem = undefined; + + const l = { + draw: (rowmin?: number, rowmax?: number) => { + let rows = 0|Math.min((y2 - y) / fontHeight, menuItems.length); + let idx = E.clip(selected - (rows>>1), 0, menuItems.length - rows); + + if (idx != lastIdx) rowmin=undefined; // redraw all if we scrolled + lastIdx = idx; + let iy = y; + g.reset().setFontAlign(0, -1, 0).setFont12x20(); + if (options.predraw) options.predraw(g); + if (rowmin === undefined && options.title) + g.drawString(options.title, (x + x2) / 2, y - 21).drawLine(x, y - 2, x2, y - 2). + setColor(g.theme.fg).setBgColor(g.theme.bg); + iy += 4; + if (rowmin !== undefined) { + if (idx < rowmin) { + iy += fontHeight * (rowmin - idx); + idx = rowmin; + } + if (rowmax && idx + rows > rowmax) { + rows = 1 + rowmax - rowmin; + } + } + while (rows--) { + const name = menuItems[idx]; + const item = items[name]! as ActualMenuItem; + + const hl = (idx === selected && !selectEdit); + if(g.theme.dark){ + fillRectRnd(x, iy, x2, iy + fontHeight - 3, 7, hl ? g.theme.bgH : g.theme.bg + 40); + }else{ + fillRectRnd(x, iy, x2, iy + fontHeight - 3, 7, hl ? g.theme.bgH : g.theme.bg - 20); + } + + g.setColor(hl ? g.theme.fgH : g.theme.fg); + g.setFontAlign( - 1, -1); + + let v; + if (typeof item === "object") { + v = "format" in item + ? (item.format as any)(item.value) // format(), value: T + : item.value; + if (typeof v !== "string") v = `${v}`; + } else { + v = ""; + } + + /*???*/{ + if(name.length >= 17 - v.length && typeof item === "object"){ + g.drawString(name.substring(0, 12 - v.length) + "...", x + 3.7, iy + 2.7); + }else if(name.length >= 15){ + g.drawString(name.substring(0, 15) + "...", x + 3.7, iy + 2.7); + }else{ + g.drawString(name, x + 3.7, iy + 2.7); + } + + let xo = x2; + if (selectEdit && idx === selected) { + xo -= 24 + 1; + g.setColor(g.theme.fgH) + .drawImage( + "\x0c\x05\x81\x00 \x07\x00\xF9\xF0\x0E\x00@", + xo, + iy + (fontHeight - 10) / 2, + {scale:2}, + ); + } + g.setFontAlign(1, -1); + g.drawString(v, xo - 2, iy + 1); + } + + g.setColor(g.theme.fg); + iy += fontHeight; + idx++; + } + g.setFontAlign( - 1, -1); + g.setColor((idx < menuItems.length)?g.theme.fg:g.theme.bg).fillPoly([72, 166, 104, 166, 88, 174]); + g.flip(); + }, + select: () => { + const item = items[menuItems[selected]] as ActualMenuItem; + + if (typeof item === "function") { + item(); + } else if (typeof item === "object") { + if (typeof item.value === "number") { + selectEdit = selectEdit ? undefined : item; + } else { + if (typeof item.value === "boolean") + item.value = !item.value; + + if (item.onchange) + item.onchange(item.value as boolean); + } + l.draw(); + } + }, + move: (dir: number) => { + const item = selectEdit; + + if (typeof item === "object" && typeof item.value === "number") { + item.value += (-dir||1) * (item.step||1); + + if (item.min && item.value < item.min) + item.value = item.wrap ? item.max as number : item.min; + + if ("max" in item && item.value > item.max) + item.value = item.wrap ? item.min as number : item.max; + + if (item.onchange) + item.onchange(item.value); + + l.draw(selected, selected); + + } else { + const lastSelected = selected; + selected = (selected + dir + /*keep +ve*/menuItems.length) % menuItems.length; + l.draw(Math.min(lastSelected, selected), Math.max(lastSelected, selected)); + } + }, + }; + + l.draw(); + + let back = options.back; + if (!back) { + const backItem = items["< Back"]; + if (typeof backItem === "function") + back = backItem; + else if (backItem && "back" in backItem) + back = backItem.back; + } + let onSwipe: SwipeCallback | undefined; + if (typeof back === "function") { + const back_ = back; + onSwipe = (lr/*, ud*/) => { + if (lr < 0) back_(); + }; + Bangle.on('swipe', onSwipe); + } + + Bangle.setUI({ + mode: "updown", + back, + remove: () => { + Bangle.removeListener("swipe", onSwipe); + }, + } as SetUIArg<"updown">, + dir => { + if (dir) l.move(dir); + else l.select(); + } + ); + + return l; +}; diff --git a/apps/promenu/metadata.json b/apps/promenu/metadata.json index 5e7508207..7da6e32df 100644 --- a/apps/promenu/metadata.json +++ b/apps/promenu/metadata.json @@ -1,7 +1,7 @@ { "id": "promenu", "name": "Pro Menu", - "version": "0.03", + "version": "0.04", "description": "Replace the built in menu function. Supports Bangle.js 1 and Bangle.js 2.", "icon": "icon.png", "type": "bootloader",