mirror of https://github.com/espruino/BangleApps
Merge pull request #3416 from bobrippling/feat/promenu-api
promenu: handle boolean menu entries, back functionality and scroll and use typescriptpull/3420/head
commit
f8aab1d6b7
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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 (idx<rowmin) {
|
||||
iy += options.fontHeight*(rowmin-idx);
|
||||
idx=rowmin;
|
||||
}
|
||||
if (idx+rows>rowmax) {
|
||||
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((idx<menuItems.length)?g.theme.fg:g.theme.bg).fillPoly([72,166,104,166,88,174]);
|
||||
g.flip();
|
||||
},
|
||||
select : function() {
|
||||
var item = items[menuItems[options.selected]];
|
||||
if ("function" == typeof item) item(l);
|
||||
else if ("object" == typeof item) {
|
||||
// if a number, go into 'edit mode'
|
||||
if ("number" == typeof item.value)
|
||||
l.selectEdit = l.selectEdit?undefined:item;
|
||||
else { // else just toggle bools
|
||||
if ("boolean" == typeof item.value) item.value=!item.value;
|
||||
if (item.onchange) item.onchange(item.value);
|
||||
}
|
||||
l.draw();
|
||||
}
|
||||
},
|
||||
move : function(dir) {
|
||||
var item = l.selectEdit;
|
||||
if (item) {
|
||||
item.value -= (dir||1)*(item.step||1);
|
||||
if (item.min!==undefined && item.value<item.min) item.value = item.wrap ? item.max : item.min;
|
||||
if (item.max!==undefined && item.value>item.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;
|
||||
};
|
||||
|
|
|
@ -0,0 +1,196 @@
|
|||
type ActualMenuItem = Exclude<Menu["..."], MenuOptions | undefined>;
|
||||
|
||||
(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) // <T>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;
|
||||
};
|
|
@ -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",
|
||||
|
|
Loading…
Reference in New Issue