Revision control

Copy as Markdown

Other Tools

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
import { NetUtil } from "resource://gre/modules/NetUtil.sys.mjs";
const lazy = {};
XPCOMUtils.defineLazyServiceGetters(lazy, {
imgTools: ["@mozilla.org/image/tools;1", Ci.imgITools],
});
/**
* Get an imgIContainer instance from a canvas element.
*
* @param {HTMLCanvasElement} canvas - The canvas element.
* @param {number} width - The width of the canvas to use.
* @param {number} height - The height of the canvas to use.
* @returns {imgIContainer}
* @protected
*/
export function getCanvasAsImgContainer(canvas, width, height) {
const imageData = canvas.getContext("2d").getImageData(0, 0, width, height);
// Create an imgIEncoder so we can turn the image data into a PNG stream.
const imgEncoder = Cc[
"@mozilla.org/image/encoder;2?type=image/png"
].getService(Ci.imgIEncoder);
imgEncoder.initFromData(
imageData.data,
imageData.data.length,
imageData.width,
imageData.height,
imageData.width * 4,
imgEncoder.INPUT_FORMAT_RGBA,
"", // outputOptions
null // randomizationKey
);
// Now turn the PNG stream into an imgIContainer.
const imgBuffer = NetUtil.readInputStreamToString(
imgEncoder,
imgEncoder.available()
);
const iconImage = lazy.imgTools.decodeImageFromBuffer(
imgBuffer,
imgBuffer.length,
"image/png"
);
// Close the PNG stream.
imgEncoder.close();
// Purge image from cache to force encodeImage() to not be lazy
iconImage.requestDiscard();
// Side effect of encodeImage() is that it decodes original image
lazy.imgTools.encodeImage(iconImage, "image/png");
return iconImage;
}
class SystemTrayBadgeManagerImpl {
/** @protected */
_getCurrentWindow() {
return Services.wm.getMostRecentBrowserWindow();
}
/** @protected */
_getBadgeSize(_window) {
return 256;
}
/** @protected */
get _badgeRatio() {
return 1;
}
/** @protected */
get _badgeArcRatio() {
return 2.2;
}
/** @protected */
_computeFontSize(iconSize, text) {
return iconSize * (1 - 0.15 * text.length);
}
/**
* Create a flat badge
*
* @param {HTMLCanvasElement} canvas - The canvas element to draw the badge.
* @param {string} text - The text to draw in the badge.
* @protected
*/
_createModernBadgeStyle(canvas, text) {
const ctx = canvas.getContext("2d");
const iconSize = canvas.width;
// Draw the background.
ctx.save();
// Solid color first.
ctx.fillStyle = "#b40000";
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 0;
ctx.shadowColor = "rgba(0,0,0,0.7)";
ctx.shadowBlur = iconSize / 2 - iconSize / this._badgeArcRatio;
ctx.beginPath();
ctx.arc(
iconSize / 2,
iconSize / 2,
iconSize / this._badgeArcRatio,
0,
Math.PI * 2,
true
);
ctx.fill();
ctx.clip();
ctx.closePath();
// Use smaller fonts for longer text to try and squeeze it in.
const fontSize = this._computeFontSize(iconSize, text);
ctx.font = `bolder ${fontSize}px Calibri`;
ctx.fillStyle = "#ffffff";
ctx.textAlign = "center";
// TODO: There isn't a textBaseline for accurate vertical centering ('middle' is the
// middle of the 'em block', and digits extend higher than 'm'), and the Mozilla core
// does not currently support computation of ascenders and descenters in measureText().
// So, we just assume that the font is 70% of the 'px' height we requested, then
// compute where the baseline ought to be located.
const approximateHeight = fontSize * 0.7;
ctx.textBaseline = "alphabetic";
ctx.fillText(
text,
iconSize / 2,
iconSize - (iconSize - approximateHeight) / 2
);
ctx.restore();
}
/** @protected */
_postProcess(window, canvas) {
return canvas;
}
/**
* Returns the generated badge
*
* @param {number} unreadCount - Unread message count.
* @returns {imgIContainer | null}
*/
getBadge(unreadCount) {
const window = this._getCurrentWindow();
if (!window) {
return null;
}
// Prevent negative values; sometimes, TB doesn't seem to correctly compute
// the number of unread items and can produce a negative value
unreadCount = Math.max(unreadCount, 0);
let badge = window.document.createElement("canvas");
const iconSize = this._getBadgeSize(window);
badge.width = badge.height = iconSize * this._badgeRatio;
badge.style.width = badge.style.height = `${badge.width}px`;
this._createModernBadgeStyle(
badge,
unreadCount < 100 ? `${unreadCount}` : "99+"
);
badge = this._postProcess(window, badge);
return getCanvasAsImgContainer(badge, iconSize, iconSize);
}
}
class SystemTrayBadgeManagerImplWindows extends SystemTrayBadgeManagerImpl {
get _badgeRatio() {
return 4;
}
get _badgeArcRatio() {
return 2.5;
}
_getBadgeSize(window) {
const smallIconSize = Cc["@mozilla.org/windows-ui-utils;1"].getService(
Ci.nsIWindowsUIUtils
).systemSmallIconSize;
return Math.floor((window.windowUtils.displayDPI / 96) * smallIconSize);
}
_computeFontSize(iconSize, text) {
return iconSize * (0.95 - 0.15 * text.length);
}
/**
* Downsample by 4X with simple averaging.
*
* Drawing at 4X and then downscaling like this gives us better results than
* using either CanvasRenderingContext2D.drawImage() to resize or letting
* the Windows taskbar service handle the resize, both of which seem to just
* give us a simple point resize.
*
* @param {Window} window - The DOM window.
* @param {HTMLCanvasElement} canvas - The input canvas element to resize.
* @returns {HTMLCanvasElement} The resized canvas element.
*/
_postProcess(window, canvas) {
const resizedCanvas = window.document.createElement("canvas");
resizedCanvas.width = resizedCanvas.height =
canvas.width / this._badgeRatio;
resizedCanvas.style.width = resizedCanvas.style.height =
resizedCanvas.width + "px";
const source = canvas
.getContext("2d")
.getImageData(0, 0, canvas.width, canvas.height);
const downsampled = resizedCanvas
.getContext("2d")
.createImageData(resizedCanvas.width, resizedCanvas.height);
for (let y = 0; y < resizedCanvas.height; ++y) {
for (let x = 0; x < resizedCanvas.width; ++x) {
let r = 0,
g = 0,
b = 0,
a = 0;
let index;
for (let i = 0; i < 4; ++i) {
for (let j = 0; j < 4; ++j) {
index = ((y * 4 + i) * source.width + (x * 4 + j)) * 4;
r += source.data[index];
g += source.data[index + 1];
b += source.data[index + 2];
a += source.data[index + 3];
}
}
index = (y * downsampled.width + x) * 4;
downsampled.data[index] = Math.round(r / 16);
downsampled.data[index + 1] = Math.round(g / 16);
downsampled.data[index + 2] = Math.round(b / 16);
downsampled.data[index + 3] = Math.round(a / 16);
}
}
resizedCanvas.getContext("2d").putImageData(downsampled, 0, 0);
return resizedCanvas;
}
}
/**
* @property {imgITools} imgTools "@mozilla.org/image/tools;1" service
*/
export const SystemTrayBadgeManager = new (class SystemTrayBadgeManager {
#controller = null;
getBadge(unreadCount) {
if (this.#controller === null) {
if (AppConstants.platform === "linux") {
this.#controller = new SystemTrayBadgeManagerImpl();
} else if (AppConstants.platform === "win") {
this.#controller = new SystemTrayBadgeManagerImplWindows();
}
}
return this.#controller?.getBadge(unreadCount);
}
})();