diff --git a/apps/messagesoverlay/ChangeLog b/apps/messagesoverlay/ChangeLog index 47aa51107..1775d4b68 100644 --- a/apps/messagesoverlay/ChangeLog +++ b/apps/messagesoverlay/ChangeLog @@ -5,3 +5,8 @@ 0.05: Fix the overlay keeping the LCD on 0.06: Better low memory handling Fix first message beeing displayed again on unlock +0.07: Adds settings + Automatic discard of oldest messages + Indicator for multiple messages in queue + Some optimization in the rendering code + Track handler changes done by background code diff --git a/apps/messagesoverlay/README.md b/apps/messagesoverlay/README.md index 8ce1cc64d..6e9192423 100644 --- a/apps/messagesoverlay/README.md +++ b/apps/messagesoverlay/README.md @@ -2,23 +2,26 @@ This app handles the display of messages and message notifications as an overlay pop up. -It is a GUI replacement for the `messages` apps. +It is a GUI replacement for the `messagesgui` app. Messages are ephemeral and not stored on the Bangle. ## Usage -Close app by tapping the X and scroll by swiping. The border of the pop up changes color if the Bangle is locked. The color depends on your currently active theme. +Close app by tapping the X and scroll by swiping. The title background of the pop up changes color if the Bangle is locked. The color depends on your currently active theme. -## Firmware hint -Current stable firmware draws incorrect colors for emojis. Nightly firmware builds correct this. +## Theme support + +Using the system theme needs more RAM since it uses a 16 bit color buffer for normal message display. Selecting the "low RAM" theme reduces that to a 4 bit buffer. +16 bit buffer with a small message takes ~4K RAM blocks while 4 bit buffer only needs about 1.5K. ## Low memory mode -If free memory is below 2000 blocks, the overlay automatically only uses 1 bit depth. Default uses roundabout 1300 blocks, while low memory mode uses about 600. +If the overlay estimates that showing the next message would get under the configured minimum free memory limit it automatically only tries to use 1 bit depth. Low memory mode uses about 0.8K blocks plus memory needed for messages. If dropping to 1 bit depth is not enough the oldest messages are discarded to keep the overlay working. ## Creator [halemmerich](https://github.com/halemmerich) + Forked from messages_light by Rarder44 diff --git a/apps/messagesoverlay/default.json b/apps/messagesoverlay/default.json new file mode 100644 index 000000000..e18b5b892 --- /dev/null +++ b/apps/messagesoverlay/default.json @@ -0,0 +1,6 @@ +{ + "autoclear": 30, + "border": 10, + "minfreemem": 2, + "systemTheme": true +} \ No newline at end of file diff --git a/apps/messagesoverlay/lib.js b/apps/messagesoverlay/lib.js index feb9ff379..bf6fa7bc4 100644 --- a/apps/messagesoverlay/lib.js +++ b/apps/messagesoverlay/lib.js @@ -1,49 +1,69 @@ -const MIN_FREE_MEM = 1000; -const LOW_MEM = 2000; -const ovrx = 10; -const ovry = 10; +let lockListener; +let ovr; +let clearingTimeout; + +// Converts a espruino version to a semantiv versioning object +const toSemantic = function (v){ + return { + major: v.substring(0,v.indexOf("v")), + minor: v.substring(v.indexOf("v") + 1, v.includes(".") ? v.indexOf(".") : v.length), + patch: v.includes(".") ? v.substring(v.indexOf(".") + 1, v.length) : 0 + }; +}; + +const isNewer = function(espruinoVersion, baseVersion){ + const s = toSemantic(espruinoVersion); + const b = toSemantic(baseVersion); + + return s.major >= b.major && + s.minor >= b.major && + s.patch > b.patch; +}; + +let needsWorkaround; + +let settings = Object.assign( + require('Storage').readJSON("messagesoverlay.default.json", true) || {}, + require('Storage').readJSON("messagesoverlay.json", true) || {} +); + +settings = Object.assign({ + fontSmall:"6x8", + fontMedium:"6x15", + fontBig: "12x20" +}, settings); + +const ovrx = settings.border; +const ovry = ovrx; const ovrw = g.getWidth()-2*ovrx; const ovrh = g.getHeight()-2*ovry; -let _g = g; -let lockListener; -let quiet; +let LOG=()=>{}; +//LOG = function() { print.apply(null, arguments);}; -let LOG = function() { - //print.apply(null, arguments); -}; - -let isQuiet = function(){ - if (quiet == undefined) quiet = (require('Storage').readJSON('setting.json', 1) || {}).quiet; - return quiet; -}; - -let settings = { - fontSmall:"6x8", - fontMedium:"Vector:14", - fontBig:"Vector:20", - fontLarge:"Vector:30", +const isQuiet = function(){ + return (require('Storage').readJSON('setting.json', 1) || {}).quiet; }; let eventQueue = []; let callInProgress = false; +let buzzing = false; -let show = function(ovr){ - let img = ovr; - LOG("show", img.getBPP()); +const show = function(){ + let img = ovr.asImage(); + LOG("show", img.bpp); if (ovr.getBPP() == 1) { - img = ovr.asImage(); - img.palette = new Uint16Array([_g.theme.fg,_g.theme.bg]); + img.palette = new Uint16Array([g.theme.fg,g.theme.bg]); } Bangle.setLCDOverlay(img, ovrx, ovry); }; -let manageEvent = function(ovr, event) { +const manageEvent = function(event) { event.new = true; LOG("manageEvent"); if (event.id == "call") { - showCall(ovr, event); + showCall(event); return; } switch (event.t) { @@ -51,7 +71,7 @@ let manageEvent = function(ovr, event) { eventQueue.unshift(event); if (!callInProgress) - showMessage(ovr, event); + showMessage(event); break; case "modify": { @@ -66,23 +86,23 @@ let manageEvent = function(ovr, event) { eventQueue.unshift(event); if (!callInProgress) - showMessage(ovr, event); + showMessage(event); break; } case "remove": if (eventQueue.length == 0 && !callInProgress) - next(ovr); + next(); if (!callInProgress && eventQueue[0] !== undefined && eventQueue[0].id == event.id) - next(ovr); - else + next(); + else eventQueue = []; break; } }; -let roundedRect = function(ovr, x,y,w,h,filled){ +const roundedRect = function(x,y,w,h,filled){ var poly = [ x,y+4, x+4,y, @@ -94,115 +114,153 @@ let roundedRect = function(ovr, x,y,w,h,filled){ x,y+h-5, x,y+4 ]; + if (filled){ + let c = ovr.getColor(); + ovr.setColor(ovr.getBgColor()); + ovr.fillPoly(poly,true); + ovr.setColor(c); + } ovr.drawPoly(poly,true); - if (filled) ovr.fillPoly(poly,true); }; -let drawScreen = function(ovr, title, titleFont, src, iconcolor, icon){ - ovr.setBgColor(ovr.theme.bg2); - ovr.clearRect(2,2,ovr.getWidth()-3,37); +const DIVIDER = 38; + +const drawScreen = function(title, src, iconcolor, icon){ + setColors(false); + + drawBorder(); + + setColors(true); + ovr.clearRect(2,2,ovr.getWidth()-3, DIVIDER - 1); - ovr.setColor(ovr.theme.fg2); ovr.setFont(settings.fontSmall); ovr.setFontAlign(0,-1); - let textCenter = (ovr.getWidth()+35-26)/2; + const textCenter = (ovr.getWidth()+34-24)/2-1; - if (src) { - let shortened = src; - while (ovr.stringWidth(shortened) > ovr.getWidth()-80) shortened = shortened.substring(0,shortened.length-2); - if (shortened.length != src.length) shortened += "..."; - ovr.drawString(shortened, textCenter, 2); + const w = ovr.getWidth() - 35 - 26; + + if (title) + drawTitle(title, textCenter, w, 8, DIVIDER - 8, 0); + + if (src) + drawSource(src, textCenter, w, 2, -1); + + if (ovr.getBPP() > 1) { + let old = ovr.getBgColor(); + ovr.setBgColor("#888"); + roundedRect(4, 5, 30, 30,true); + ovr.setBgColor(old); + old = ovr.getColor(); + ovr.setColor(iconcolor); + ovr.drawImage(icon,7,8); + ovr.setColor(old); + } else { + roundedRect(4, 5, 30, 30,true); + ovr.drawImage(icon,7,8); } + roundedRect(ovr.getWidth()-26,5,22,30,true); ovr.setFontAlign(0,0); - ovr.setFont(titleFont); - if (title) ovr.drawString(title, textCenter, 38/2 + 5); - - ovr.setColor(ovr.theme.fg2); - - ovr.setFont(settings.fontMedium); - roundedRect(ovr, ovr.getWidth()-26,5,22,30,false); ovr.setFont("Vector:16"); - ovr.drawString("X",ovr.getWidth()-14,21); - - ovr.setColor("#888"); - roundedRect(ovr, 5,5,30,30,true); - ovr.setColor(ovr.getBPP() != 1 ? iconcolor : ovr.theme.bg2); - ovr.drawImage(icon,8,8); + ovr.drawString("X",ovr.getWidth()-14,20); }; -let showMessage = function(ovr, msg) { - LOG("showMessage"); - ovr.setBgColor(ovr.theme.bg); +const drawSource = function(src, center, w, y, align) { + ovr.setFont(settings.fontSmall); + while (ovr.stringWidth(src) > w) src = src.substring(0,src.length-2); + if (src.length != src.length) src += "..."; + ovr.setFontAlign(0,align); + ovr.drawString(src, center, y); +}; - if (typeof msg.CanscrollDown === "undefined") - msg.CanscrollDown = false; - if (typeof msg.CanscrollUp === "undefined") - msg.CanscrollUp = false; +const drawTitle = function(title, center, w, y, h) { + let size = 30; - // Normal text message display - let title = msg.title, - titleFont = settings.fontLarge, - lines; - if (title) { - let w = ovr.getWidth() - 35 - 26; - if (ovr.setFont(titleFont).stringWidth(title) > w) - titleFont = settings.fontMedium; - if (ovr.setFont(titleFont).stringWidth(title) > w) { - lines = ovr.wrapString(title, w); - title = (lines.length > 2) ? lines.slice(0, 2).join("\n") + "..." : lines.join("\n"); + while (ovr.setFont("Vector:" + size).stringWidth(title) > w){ + size -= 2; + if (size < 14){ + ovr.setFont(settings.fontMedium); + break; } } - drawScreen(ovr, title, titleFont, msg.src || /*LANG*/ "Message", require("messageicons").getColor(msg), require("messageicons").getImage(msg)); + let dh; + let a; + if (ovr.stringWidth(title) > w) { + let ws = ovr.wrapString(title, w); + if (ws.length >= 2 && ovr.stringWidth(ws[1]) > w - 8){ + ws[1] = ws[1].substring(0, ws[1].length - 2); + ws[1] += "..."; + } + title = ws.slice(0, 2).join("\n"); + + a = -1; + dh = y + 2; + } else { + a = 0; + dh = y + h/2; + } + ovr.setFontAlign(0, a); + ovr.drawString(title, center, dh); +}; + +const setColors = function(lockRelevant) { + if (lockRelevant && !Bangle.isLocked()){ + ovr.setColor(ovr.theme.fg2); + ovr.setBgColor(ovr.theme.bg2); + } else { + ovr.setColor(ovr.theme.fg); + ovr.setBgColor(ovr.theme.bg); + } +}; + +const showMessage = function(msg) { + LOG("showMessage"); + + ovr.setClipRect(0,0,ovr.getWidth(),ovr.getHeight()); + + drawScreen(msg.title, msg.src || /*LANG*/ "Message", require("messageicons").getColor(msg), require("messageicons").getImage(msg)); + + + if (!Bangle.isLocked()){ + ovr.setColor(ovr.theme.fg); + ovr.setBgColor(ovr.theme.bg); + } + + drawMessage(msg); if (!isQuiet() && msg.new) { msg.new = false; - Bangle.buzz(); + if (!buzzing){ + buzzing = true; + Bangle.buzz().then(()=>{setTimeout(()=>{buzzing = false;},2000);}); + } + Bangle.setLCDPower(1); } - - drawMessage(ovr, msg); }; -let drawBorder = function(img) { +const drawBorder = function() { LOG("drawBorder", isQuiet()); - if (img) ovr=img; - if (Bangle.isLocked()) - ovr.setColor(ovr.theme.fgH); - else - ovr.setColor(ovr.theme.fg); ovr.drawRect(0,0,ovr.getWidth()-1,ovr.getHeight()-1); ovr.drawRect(1,1,ovr.getWidth()-2,ovr.getHeight()-2); - show(ovr); + ovr.drawRect(2,DIVIDER,ovr.getWidth()-2,DIVIDER+1); + show(); }; -let showCall = function(ovr, msg) { +const showCall = function(msg) { LOG("showCall"); LOG(msg); if (msg.t == "remove") { LOG("hide call screen"); - next(ovr); //dont shift + next(); //dont shift return; } callInProgress = true; - let title = msg.title, - titleFont = settings.fontLarge, - lines; - if (title) { - let w = ovr.getWidth() - 35 -26; - if (ovr.setFont(titleFont).stringWidth(title) > w) - titleFont = settings.fontMedium; - if (ovr.setFont(titleFont).stringWidth(title) > w) { - lines = ovr.wrapString(title, w); - title = (lines.length > 2) ? lines.slice(0, 2).join("\n") + "..." : lines.join("\n"); - } - } - - drawScreen(ovr, title, titleFont, msg.src || /*LANG*/ "Message", require("messageicons").getColor(msg), require("messageicons").getImage(msg)); + drawScreen(msg.title, msg.src || /*LANG*/ "Message", require("messageicons").getColor(msg), require("messageicons").getImage(msg)); stopCallBuzz(); if (!isQuiet()) { @@ -216,10 +274,10 @@ let showCall = function(ovr, msg) { Bangle.buzz(500); } } - drawMessage(ovr, msg); + drawMessage(msg); }; -let next = function(ovr) { +const next = function() { LOG("next"); stopCallBuzz(); @@ -230,203 +288,379 @@ let next = function(ovr) { if (eventQueue.length == 0) { LOG("no element in queue - closing"); cleanup(); - return; + return false; } - showMessage(ovr, eventQueue[0]); + showMessage(eventQueue[0]); + return true; }; let callBuzzTimer = null; -let stopCallBuzz = function() { +const stopCallBuzz = function() { if (callBuzzTimer) { clearInterval(callBuzzTimer); callBuzzTimer = undefined; } }; -let drawTriangleUp = function(ovr) { - ovr.reset(); - ovr.fillPoly([ovr.getWidth()-9, 46,ovr.getWidth()-14, 56,ovr.getWidth()-4, 56]); +const drawTriangleUp = function() { + ovr.fillPoly([ovr.getWidth()-10, 46,ovr.getWidth()-15, 56,ovr.getWidth()-5, 56]); }; -let drawTriangleDown = function(ovr) { - ovr.reset(); - ovr.fillPoly([ovr.getWidth()-9, ovr.getHeight()-6, ovr.getWidth()-14, ovr.getHeight()-16, ovr.getWidth()-4, ovr.getHeight()-16]); +const drawTriangleDown = function() { + ovr.fillPoly([ovr.getWidth()-10, ovr.getHeight()-6, ovr.getWidth()-15, ovr.getHeight()-16, ovr.getWidth()-5, ovr.getHeight()-16]); }; -let linesScroll = 6; -let scrollUp = function(ovr) { - msg = eventQueue[0]; +const scrollUp = function() { + const msg = eventQueue[0]; LOG("up", msg); - if (typeof msg.FirstLine === "undefined") - msg.FirstLine = 0; - if (typeof msg.CanscrollUp === "undefined") - msg.CanscrollUp = false; if (!msg.CanscrollUp) return; - msg.FirstLine = msg.FirstLine > 0 ? msg.FirstLine - linesScroll : 0; - - drawMessage(ovr, msg); + msg.FirstLine = msg.FirstLine > 0 ? msg.FirstLine - 1 : 0; + drawMessage(msg); }; -let scrollDown = function(ovr) { - msg = eventQueue[0]; +const scrollDown = function() { + const msg = eventQueue[0]; LOG("down", msg); - if (typeof msg.FirstLine === "undefined") - msg.FirstLine = 0; - if (typeof msg.CanscrollDown === "undefined") - msg.CanscrollDown = false; if (!msg.CanscrollDown) return; - msg.FirstLine = msg.FirstLine + linesScroll; - drawMessage(ovr, msg); + msg.FirstLine = msg.FirstLine + 1; + drawMessage(msg); }; -let drawMessage = function(ovr, msg) { - let MyWrapString = function(str, maxWidth) { +const drawMessage = function(msg) { + setColors(false); + const getStringHeight = function(str){ + "jit"; + const metrics = ovr.stringMetrics(str); + if (needsWorkaround === undefined) + needsWorkaround = isNewer("2v21.13", process.version); + if (needsWorkaround && metrics.maxImageHeight > 16) + metrics.maxImageHeight = metrics.height; + return Math.max(metrics.height, metrics.maxImageHeight); + }; + + const wrapString = function(str, maxWidth) { str = str.replace("\r\n", "\n").replace("\r", "\n"); return ovr.wrapString(str, maxWidth); }; + const wrappedStringHeight = function(strArray){ + let r = 0; + strArray.forEach((line, i) => { + r += getStringHeight(line); + }); + return r; + }; - if (typeof msg.FirstLine === "undefined") msg.FirstLine = 0; + if (msg.FirstLine === undefined) msg.FirstLine = 0; - let bodyFont = typeof msg.bodyFont === "undefined" ? settings.fontMedium : msg.bodyFont; - let Padding = 3; - if (typeof msg.lines === "undefined") { + const padding = eventQueue.length > 1 ? (eventQueue.length > 3 ? 7 : 5) : 3; + + const yText = DIVIDER+2; + let yLine = yText + 4; + + ovr.setClipRect(2, yText, ovr.getWidth() - 3, ovr.getHeight() - 3); + + const maxTextHeight = ovr.getHeight() - yLine - padding + 2; + + if (!msg.lines) { + let bodyFont = settings.fontBig; ovr.setFont(bodyFont); - msg.lines = MyWrapString(msg.body, ovr.getWidth() - (Padding * 2)); - if (msg.lines.length <= 2) { - bodyFont = ovr.getFonts().includes("Vector") ? "Vector:20" : "6x8:3"; + msg.lines = wrapString(msg.body, ovr.getWidth() - 4 - padding); + + if (wrappedStringHeight(msg.lines) > maxTextHeight) { + bodyFont = settings.fontMedium; ovr.setFont(bodyFont); - msg.lines = MyWrapString(msg.body, ovr.getWidth() - (Padding * 2)); - msg.bodyFont = bodyFont; + msg.lines = wrapString(msg.body, ovr.getWidth() - 4 - padding); } + msg.bodyFont = bodyFont; + msg.lineHeights = []; + msg.lines.forEach((line, i) => { + msg.lineHeights[i] = getStringHeight(line); + }); } - let NumLines = 7; + LOG("Prepared message", msg); - let linesToPrint = (msg.lines.length > NumLines) ? msg.lines.slice(msg.FirstLine, msg.FirstLine + NumLines) : msg.lines; + ovr.setFont(msg.bodyFont); + ovr.clearRect(2, yText, ovr.getWidth()-3, ovr.getHeight()-3); - let yText = 40; + let xText = 4; - ovr.setBgColor(ovr.theme.bg); - ovr.setColor(ovr.theme.fg); - ovr.clearRect(2, yText, ovrw-3, ovrh-3); - let xText = Padding; - yText += Padding; - ovr.setFont(bodyFont); - let HText = ovr.getFontHeight(); - - yText = ((ovrh - yText) / 2) - (linesToPrint.length * HText / 2) + yText; - - if (linesToPrint.length <= 3) { + if (msg.bodyFont == settings.fontBig) { ovr.setFontAlign(0, -1); - xText = ovr.getWidth() / 2; - } else + xText = Math.round(ovr.getWidth() / 2 - (padding - 3) / 2) + 1; + yLine = (ovr.getHeight() + yLine) / 2 - (wrappedStringHeight(msg.lines) / 2); + ovr.drawString(msg.lines.join("\n"), xText, yLine); + } else { ovr.setFontAlign(-1, -1); + } + let currentLine = msg.FirstLine; - linesToPrint.forEach((line, i) => { - ovr.drawString(line, xText, yText + HText * i); - }); + let drawnHeight = 0; + + while(drawnHeight < maxTextHeight && msg.lines.length > currentLine) { + const lineHeight = msg.lineHeights[currentLine]; + ovr.drawString(msg.lines[currentLine], xText, yLine + drawnHeight); + drawnHeight += lineHeight; + currentLine++; + } + + if (eventQueue.length > 1){ + ovr.drawLine(ovr.getWidth()-4,ovr.getHeight()/2,ovr.getWidth()-4,ovr.getHeight()-4); + ovr.drawLine(ovr.getWidth()/2,ovr.getHeight()-4,ovr.getWidth()-4,ovr.getHeight()-4); + } + if (eventQueue.length > 3){ + ovr.drawLine(ovr.getWidth()-6,ovr.getHeight()*0.6,ovr.getWidth()-6,ovr.getHeight()-6); + ovr.drawLine(ovr.getWidth()*0.6,ovr.getHeight()-6,ovr.getWidth()-6,ovr.getHeight()-6); + } if (msg.FirstLine != 0) { msg.CanscrollUp = true; - drawTriangleUp(ovr); + drawTriangleUp(); } else msg.CanscrollUp = false; - if (msg.FirstLine + linesToPrint.length < msg.lines.length) { + if (currentLine < msg.lines.length) { msg.CanscrollDown = true; - drawTriangleDown(ovr); + drawTriangleDown(); } else msg.CanscrollDown = false; - show(ovr); - if (!isQuiet()) Bangle.setLCDPower(1); + + show(); }; -let getSwipeHandler = function(ovr){ - return (lr, ud) => { - if (ud == 1) { - scrollUp(ovr); - } else if (ud == -1){ - scrollDown(ovr); +const getDragHandler = function(){ + return (e) => { + if (e.dy > 0) { + scrollUp(); + } else if (e.dy < 0){ + scrollDown(); } }; }; -let getTouchHandler = function(ovr){ +const getTouchHandler = function(){ return (_, xy) => { - if (xy.y < ovry + 40){ - next(ovr); + if (xy.y < ovry + DIVIDER){ + next(); } }; }; -let restoreHandler = function(event){ - LOG("Restore", event, backup[event]); - Bangle.removeAllListeners(event); - Bangle["#on" + event]=backup[event]; - backup[event] = undefined; +const EVENTS=["touch", "drag", "swipe"]; + +let hasBackup = false; + +const origOn = Bangle.on; +const backupOn = function(event, handler){ + if (EVENTS.includes(event)){ + if (!backup[event]) + backup[event] = []; + backup[event].push(handler); + } + else origOn.call(Bangle, event, handler); }; -let backupHandler = function(event){ - if (backupDone) return; // do not backup, overlay is already up - backup[event] = Bangle["#on" + event]; - LOG("Backed up", backup[event]); - Bangle.removeAllListeners(event); +const origClearWatch = clearWatch; +const backupClearWatch = function(w) { + if (w) + backup.watches.filter((e)=>e.index != w); + else + backup.watches = []; }; -let cleanup = function(){ +const origSetWatch = setWatch; +const backupSetWatch = function(){ + if (!backup.watches) + backup.watches = []; + LOG("backup for watch", arguments); + let i = backup.watches.map((e)=>e.index).sort().pop() + 1; + backup.watches.push({index:i, args:arguments}); + return i; +}; + +const origRemove = Bangle.removeListener; +const backupRemove = function(event, handler){ + if (EVENTS.includes(event) && backup[event]){ + LOG("backup for " + event + ": " + backup[event]); + backup[event] = backup[event].filter(e=>e!==handler); + } + else origRemove.call(Bangle, event, handler); +}; + +const origRemoveAll = Bangle.removeAllListeners; +const backupRemoveAll = function(event){ + if (backup[event]) + backup[event] = undefined; + origRemoveAll.call(Bangle); +}; + +const restoreHandlers = function(){ + if (!hasBackup){ + LOG("No backup available"); + return; + } + + for (const event of EVENTS){ + LOG("Restore", backup[event]); + origRemoveAll.call(Bangle, event); + if (backup[event] && backup[event].length == 1) + backup[event] = backup[event][0]; + Bangle["#on" + event]=backup[event]; + backup[event] = undefined; + } + + if (backup.watches){ + toRemove = []; + + origClearWatch.call(global); + + for(let i = 0; i < backup.watches.length; i++){ + let w = backup.watches[i]; + LOG("Restoring watch", w); + if (w) { + origSetWatch.apply(global, w); + } else { + toRemove.push(i+1); + origSetWatch.call(global, ()=>{}, BTN); + } + } + + LOG("Remove watches", toRemove, global["\xff"].watches); + for(let c of toRemove){ + origClearWatch.call(global, c); + } + } + + global.setWatch = origSetWatch; + global.clearWatch = origClearWatch; + Bangle.on = origOn; + Bangle.removeListener = origRemove; + Bangle.removeAllListeners = origRemoveAll; + + hasBackup = false; +}; + +const backupHandlers = function(){ + if (hasBackup){ + LOG("Backup already exists"); + return false; // do not backup, overlay is already up + } + + for (const event of EVENTS){ + backup[event] = Bangle["#on" + event]; + if (typeof backup[event] == "function") + backup[event] = [ backup[event] ]; + LOG("Backed up", backup[event], event); + Bangle.removeAllListeners(event); + } + + backup.watches = []; + + for (let i = 1; i < global["\xff"].watches.length; i++){ + let w = global["\xff"].watches[i]; + LOG("Transform watch", w); + if (w) { + w = [ + w.callback, + w.pin, + w + ]; + delete w[2].callback; + delete w[2].pin; + w[2].debounce = Math.round(w[2].debounce / 1048.576); + } else { + w = null; + } + LOG("Transformed to", w); + backup.watches.push(w); + } + + LOG("Backed up watches", backup.watches); + clearWatch(); + + global.setWatch = backupSetWatch; + global.clearWatch = backupClearWatch; + Bangle.on = backupOn; + Bangle.removeListener = backupRemove; + Bangle.removeAllListeners = backupRemoveAll; + + hasBackup = true; + + return true; +}; + +const cleanup = function(){ if (lockListener) { Bangle.removeListener("lock", lockListener); lockListener = undefined; } - restoreHandler("touch"); - restoreHandler("swipe"); - restoreHandler("drag"); + restoreHandlers(); Bangle.setLCDOverlay(); - backupDone = false; ovr = undefined; quiet = undefined; }; -let backup = {}; +const backup = {}; -let backupDone = false; - -let main = function(ovr, event) { - LOG("Main", event, settings); +const main = function(event) { + LOG("Main", event.t); + const didBackup = backupHandlers(); if (!lockListener) { - lockListener = function (){ - drawBorder(); + lockListener = function (e){ + updateClearingTimeout(); + showMessage(eventQueue[0]); }; - Bangle.on('lock', lockListener); + LOG("Add overlay lock handlers"); + origOn.call(Bangle, 'lock', lockListener); } - backupHandler("touch"); - backupHandler("swipe"); - backupHandler("drag"); - if (!backupDone){ - Bangle.on('touch', getTouchHandler(ovr)); - Bangle.on('swipe', getSwipeHandler(ovr)); + + if (didBackup){ + LOG("Add overlay UI handlers"); + origOn.call(Bangle, 'touch', getTouchHandler(ovr)); + origOn.call(Bangle, 'drag', getDragHandler(ovr)); } - backupDone=true; if (event !== undefined){ - drawBorder(ovr); - manageEvent(ovr, event); + manageEvent(event); } else { LOG("No event given"); cleanup(); } }; -let ovr; +const updateClearingTimeout = ()=>{ + LOG("updateClearingTimeout"); + if (settings.autoclear <= 0) + return; + LOG("Remove clearing timeout", clearingTimeout); + if (clearingTimeout) clearTimeout(clearingTimeout); + if (Bangle.isLocked()){ + LOG("Set new clearing timeout"); + clearingTimeout = setTimeout(()=>{ + LOG("setNewTimeout"); + const event = eventQueue.pop(); + if (event) + showMessage(event); + if (eventQueue.length > 0){ + LOG("still got elements"); + updateClearingTimeout(); + } else { + cleanup(); + } + }, settings.autoclear * 1000); + } else { + clearingTimeout = undefined; + } +}; exports.message = function(type, event) { LOG("Got message", type, event); @@ -434,13 +668,40 @@ exports.message = function(type, event) { if(!(type=="text" || type == "call")) return; if(type=="text" && event.id == "nav") return; if(event.handled) return; + if(event.messagesoverlayignore) return; - bpp = 4; - if (process.memory().free < LOW_MEM) + let free = process.memory().free; + bpp = settings.systemTheme ? 16 : 4; + + let estimatedMemUse = bpp == 16 ? 4096 : (bpp == 4 ? 1536 : 768); + // reduce estimation if ovr already exists and uses memory; + if (ovr) + estimatedMemUse -= ovr.getBPP() == 16 ? 4096 : (ovr.getBPP() == 4 ? 1536 : 768); + + if (process.memory().free - estimatedMemUse < settings.minfreemem * 1024) { + // we are going to be under our minfreemem setting if we proceed bpp = 1; + if (ovr && ovr.getBPP() > 1){ + // can reduce memory by going 1 bit + let saves = ovr.getBPP() == 16 ? 4096 - 768 : 768; + estimatedMemUse -= saves; + LOG("Go to 1 bit, saving", saves); + } else { + estimatedMemUse = 768; + } + } - while (process.memory().free < MIN_FREE_MEM && eventQueue.length > 0){ - let dropped = eventQueue.pop(); + + if (E.getSizeOf){ + let e = E.getSizeOf(eventQueue); + estimatedMemUse += e; + LOG("EventQueue has", e, "blocks"); + } + + LOG("Free ", free, "estimated use", estimatedMemUse, "for", bpp, "BPP"); + + while (process.memory().free - estimatedMemUse < settings.minfreemem * 1024 && eventQueue.length > 0){ + const dropped = eventQueue.pop(); print("Dropped message because of memory constraints", dropped); } @@ -448,19 +709,37 @@ exports.message = function(type, event) { ovr = Graphics.createArrayBuffer(ovrw, ovrh, bpp, { msb: true }); - } else { - ovr.clear(); + if(E.getSizeOf) + LOG("New overlay uses", E.getSizeOf(ovr), "blocks"); } - g = ovr; + ovr.reset(); - if (bpp == 4) - ovr.theme = g.theme; - else - ovr.theme = { fg:0, bg:1, fg2:1, bg2:0, fgH:1, bgH:0 }; + if (bpp > 1){ + if (settings.systemTheme){ + ovr.theme = g.theme; + } else { + ovr.theme = { + fg: g.theme.dark ? 15: 0, + bg: g.theme.dark ? 0: 15, + fg2: g.theme.dark ? 15: 0, + bg2: g.theme.dark ? 9 : 8, + fgH: g.theme.dark ? 15 : 0, + bgH: g.theme.dark ? 9: 8, + }; + } + } + else { + if (g.theme.dark) + ovr.theme = { fg:1, bg:0, fg2:0, bg2:1, fgH:0, bgH:1 }; + else + ovr.theme = { fg:0, bg:1, fg2:1, bg2:0, fgH:1, bgH:0 }; + } + + main(event); + + updateClearingTimeout(); - main(ovr, event); - if (!isQuiet()) Bangle.setLCDPower(1); event.handled = true; - g = _g; + g.flip(); }; diff --git a/apps/messagesoverlay/metadata.json b/apps/messagesoverlay/metadata.json index c16a41f5c..8487aabf1 100644 --- a/apps/messagesoverlay/metadata.json +++ b/apps/messagesoverlay/metadata.json @@ -1,7 +1,7 @@ { "id": "messagesoverlay", "name": "Messages Overlay", - "version": "0.06", + "version": "0.07", "description": "An overlay based implementation of a messages UI (display notifications from iOS and Gadgetbridge/Android)", "icon": "app.png", "type": "bootloader", @@ -11,7 +11,10 @@ "readme": "README.md", "storage": [ {"name":"messagesoverlay","url":"lib.js"}, - {"name":"messagesoverlay.0.boot.js","url":"boot.js"} + {"name":"messagesoverlay.0.boot.js","url":"boot.js"}, + {"name":"messagesoverlay.settings.js","url":"settings.js"}, + {"name":"messagesoverlay.default.json","url":"default.json"} ], + "data": [{"name":"bthrm.json"}], "screenshots": [{"url":"screen_call.png"} ,{"url":"screen_message.png"} ] } diff --git a/apps/messagesoverlay/settings.js b/apps/messagesoverlay/settings.js new file mode 100644 index 000000000..cd76bf115 --- /dev/null +++ b/apps/messagesoverlay/settings.js @@ -0,0 +1,66 @@ +(function(back) { + function writeSettings(key, value) { + var s = require('Storage').readJSON(FILE, true) || {}; + s[key] = value; + require('Storage').writeJSON(FILE, s); + readSettings(); + } + + function readSettings(){ + settings = Object.assign( + require('Storage').readJSON("messagesoverlay.default.json", true) || {}, + require('Storage').readJSON(FILE, true) || {} + ); + } + + var FILE="messagesoverlay.json"; + var settings; + readSettings(); + + function buildMainMenu(){ + var mainmenu = { + '' : { title: "Messages Overlay"}, + '< Back': back, + 'Border': { + value: settings.border, + min: 0, + max: Math.floor(g.getWidth()/2-50), + step: 1, + format: v=>v + "px", + onchange: v => { + writeSettings("border",v); + } + }, + 'Autoclear after': { + value: settings.autoclear, + min: 0, + max: 3600, + step: 10, + format: v=>v>0?v+"s":"Off", + onchange: v => { + writeSettings("autoclear",v); + } + }, + 'Theme': { + value: settings.systemTheme, + format: v=>v?"System":"low RAM", + onchange: v => { + writeSettings("systemTheme",v); + } + }, + 'Min. free RAM': { + value: settings.minfreemem, + min: 0, + max: process.memory().total/1000, + step: 1, + format: v=>v + "k free", + onchange: v => { + writeSettings("minfreemem",v); + } + } + }; + return mainmenu; + } + + E.showMenu(buildMainMenu()); +});