From 7bb98d38e09c4e581df2d5993eb9de3d83dc8620 Mon Sep 17 00:00:00 2001 From: Richard de Boer Date: Sun, 19 Jul 2020 21:57:49 +0200 Subject: [PATCH] notify: Update notify API for #527 Also some cleanup: - fall back to "src" if title is missing - move message-splitting into separate function --- apps.json | 2 +- apps/notify/ChangeLog | 1 + apps/notify/README.md | 2 +- apps/notify/notify.js | 144 +++++++++++++++++++++++++++--------------- 4 files changed, 97 insertions(+), 52 deletions(-) diff --git a/apps.json b/apps.json index cb66eef39..092aa1642 100644 --- a/apps.json +++ b/apps.json @@ -80,7 +80,7 @@ "name": "Notifications (default)", "shortName":"Notifications", "icon": "notify.png", - "version":"0.02", + "version":"0.03", "description": "A handler for displaying notifications that displays them in a bar at the top of the screen", "tags": "widget", "type": "notify", diff --git a/apps/notify/ChangeLog b/apps/notify/ChangeLog index 0dccc7930..cb974e12d 100644 --- a/apps/notify/ChangeLog +++ b/apps/notify/ChangeLog @@ -1,2 +1,3 @@ 0.01: New Library! 0.02: Add notification ID option +0.03: Pass `area{x,y,w,h}` to render callback instead of just `y` \ No newline at end of file diff --git a/apps/notify/README.md b/apps/notify/README.md index a17caccea..cef9f2124 100644 --- a/apps/notify/README.md +++ b/apps/notify/README.md @@ -16,7 +16,7 @@ options = { src : string, // optional source name body : string, // optional body text icon : string, // optional icon (image string) - render function(y) {} // function callback to render + render function(area) {} // function callback to render in area{x,y,w,h} }; // eg... show notification require("notify").show({title:"Test", body:"Hello"}); diff --git a/apps/notify/notify.js b/apps/notify/notify.js index 51569d912..d8168e048 100644 --- a/apps/notify/notify.js +++ b/apps/notify/notify.js @@ -1,5 +1,36 @@ -var pos = 0; -var id = null; +let pos = 0; +let id = null; + +/** + * Fit text into area, trying to insert newlines between words + * Appends "..." if more text was present but didn't fit + * + * @param {string} text + * @param {number} rows Maximum number of rows + * @param {number} width Maximum line length, in characters + */ +function fitWords(text,rows,width) { + // We never need more than rows*width characters anyway, split by any whitespace + const words = text.trim().substr(0,rows*width).split(/\s+/); + let row=1,len=0,limit=width; + let result = ""; + for (let word of words) { + // len==0 means first word of row, after that we also add a space + if ((len?len+1:0)+word.length > limit) { + if (row>=rows) { + result += "..."; + break; + } + result += "\n"; + len=0; + row++; + if (row===rows) limit -= 3; // last row needs space for "..." + } + result += (len?" ":"") + word; + len += (len?1:0) + word.length; + } + return result; +} /** options = { @@ -13,77 +44,90 @@ var id = null; render function(y) // function callback to render } */ +/* + The screen is 240x240px, but has a 240x320 buffer, used like this: + 0,0: top-left ... 239,0: top-right + [Normal screen contents: lines 0-239] + 239,0: bottom-left ... 239,239: bottom-right + [Usually off-screen: lines 240-319] + 319,0: last line in buffer ... 319,239: last pixel in buffer + + When moving the display area, the buffer wraps around + + So we draw notifications at the end of the buffer, + then shift the display down to show them without touching regular content. + Apps don't know about this, so can just keep updating the usual display area. + + For example, a size 40 notification: + - Draws in bottom 40 buffer lines (279-319) + - Shifts display down by 40px + Display now shows buffer lines 279-319,0-199 + Apps/widgets keep drawing to buffer line 0-239 like nothing happened + */ exports.show = function(options) { - options = options||{}; - if (options.on===undefined) options.on=true; + options = options || {}; + if (options.on===undefined) options.on = true; id = ("id" in options)?options.id:null; - var h = options.size||80; - var oldMode = Bangle.getLCDMode(); + let size = options.size || 80; + if (size>80) {size = 80} + const oldMode = Bangle.getLCDMode(); // TODO: throw exception if double-buffered? + // TODO: throw exception if size>80? Bangle.setLCDMode("direct"); - var y = 320-h; - var x = 4; - g.setClipRect(0, y, 239, 319); + // drawing area + let x = 0, + y = 320-size, + w = 240, + h = size, + b = y+h-1, r = x+w-1; // bottom,right + g.setClipRect(x,y, r,b); // clear area - g.setColor(0).fillRect(0, y+1, 239, 317); - // border - g.setColor(0x39C7).fillRect(0, 318, 239, 319); - // top bar - var top = 0; - if (options.title) { - g.setColor(0x39C7).fillRect(0, y, 239, y+20); + g.setColor(0).fillRect(x,y, r,b); + // bottom border + g.setColor(0x39C7).fillRect(0,b-1, r,b); + b -= 2;h -= 2; + // title bar + if (options.title || options.src) { + g.setColor(0x39C7).fillRect(x,y, r,y+20); + const title = options.title||options.src; g.setColor(-1).setFontAlign(-1, -1, 0).setFont("6x8", 2); - g.drawString(options.title.trim().substring(0, 13), 25, y+3); - y+=20; - } - if (options.src) { - g.setColor(-1).setFontAlign(1, -1, 0).setFont("6x8", 1); - g.drawString(options.src.substring(0, 10), 215, 322-h); + g.drawString(title.trim().substring(0, 13), x+25,y+3); + if (options.title && options.src) { + g.setFont("6x8", 1); + g.drawString(options.src.substring(0, 10), x+215,y+5); + } + y += 20;h -= 20; } if (options.icon) { - let i = options.icon; + let i = options.icon, iw; g.drawImage(i, x,y+4); - if ("string"==typeof i) x += i.charCodeAt(0); - else x += i[0]; + if ("string"==typeof i) {iw = i.charCodeAt(0)} + else {iw = i[0]} + x += iw;w -= iw; } // body text if (options.body) { - var body = options.body; - const maxChars = Math.floor((300-x)/8); - var limit = maxChars; - let row = 1; - let words = body.trim().replace("\n", " ").split(" "); - body = ""; - for (var i = 0; i < words.length; i++) { - if (body.length + words[i].length + 1 > limit) { - if (row>=5) { - body += "..."; - break; - } - body += "\n " + words[i]; - row++; - limit += maxChars; - if (row==5) limit -= 4; - } else { - body += " " + words[i]; - } - } - g.setColor(-1).setFont("6x8", 1).setFontAlign(-1, -1, 0).drawString(body, x-4, y+4); + const maxRows=Math.floor((h-4)/8), // font=6x8 + maxChars=Math.floor(w/6)-2, + text=fitWords(options.body, maxRows, maxChars); + g.setColor(-1).setFont("6x8", 1).setFontAlign(-1, -1, 0).drawString(text, x+6,y+4); } - if (options.render) options.render(320 - h); + if (options.render) { + options.render({x:x, y:y, w:w, h:h}); + } if (options.on) Bangle.setLCDPower(1); // light up Bangle.setLCDMode(oldMode); // clears cliprect function anim() { pos -= 2; - if (pos < -h) { - pos = -h; + if (pos < -size) { + pos = -size; } Bangle.setLCDOffset(pos); - if (pos > -h) setTimeout(anim, 15); + if (pos > -size) setTimeout(anim, 15); } anim(); Bangle.on("touch", exports.hide);