зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1270742 - Add support for default_icon in chrome.pageAction. r=kmag
MozReview-Commit-ID: D9uR0JUXJwx --HG-- extra : transplant_source : %D2%EEUA%D1%C7Yp%88E%851%200n%90%D7%8F%0C%F4
This commit is contained in:
Родитель
c91928a953
Коммит
48b0f752e6
|
@ -5,9 +5,9 @@
|
|||
"AllWindowEvents": true,
|
||||
"currentWindow": true,
|
||||
"EventEmitter": true,
|
||||
"IconDetails": true,
|
||||
"makeWidgetId": true,
|
||||
"pageActionFor": true,
|
||||
"IconDetails": true,
|
||||
"PanelPopup": true,
|
||||
"TabContext": true,
|
||||
"ViewPopup": true,
|
||||
|
|
|
@ -6,6 +6,7 @@ Cu.import("resource://gre/modules/Task.jsm");
|
|||
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
|
||||
var {
|
||||
EventManager,
|
||||
IconDetails,
|
||||
} = ExtensionUtils;
|
||||
|
||||
// WeakMap[Extension -> PageAction]
|
||||
|
|
|
@ -8,138 +8,21 @@ XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
|
|||
"resource://gre/modules/PrivateBrowsingUtils.jsm");
|
||||
|
||||
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
|
||||
Cu.import("resource://gre/modules/AddonManager.jsm");
|
||||
Cu.import("resource://gre/modules/AppConstants.jsm");
|
||||
|
||||
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
|
||||
const INTEGER = /^[1-9]\d*$/;
|
||||
|
||||
// Minimum time between two resizes.
|
||||
const RESIZE_TIMEOUT = 100;
|
||||
|
||||
var {
|
||||
EventManager,
|
||||
instanceOf,
|
||||
} = ExtensionUtils;
|
||||
|
||||
// This file provides some useful code for the |tabs| and |windows|
|
||||
// modules. All of the code is installed on |global|, which is a scope
|
||||
// shared among the different ext-*.js scripts.
|
||||
|
||||
|
||||
// Manages icon details for toolbar buttons in the |pageAction| and
|
||||
// |browserAction| APIs.
|
||||
global.IconDetails = {
|
||||
// Normalizes the various acceptable input formats into an object
|
||||
// with icon size as key and icon URL as value.
|
||||
//
|
||||
// If a context is specified (function is called from an extension):
|
||||
// Throws an error if an invalid icon size was provided or the
|
||||
// extension is not allowed to load the specified resources.
|
||||
//
|
||||
// If no context is specified, instead of throwing an error, this
|
||||
// function simply logs a warning message.
|
||||
normalize(details, extension, context = null) {
|
||||
let result = {};
|
||||
|
||||
try {
|
||||
if (details.imageData) {
|
||||
let imageData = details.imageData;
|
||||
|
||||
// The global might actually be from Schema.jsm, which
|
||||
// normalizes most of our arguments. In that case it won't have
|
||||
// an ImageData property. But Schema.jsm doesn't normalize
|
||||
// actual ImageData objects, so they will come from a global
|
||||
// with the right property.
|
||||
if (instanceOf(imageData, "ImageData")) {
|
||||
imageData = {"19": imageData};
|
||||
}
|
||||
|
||||
for (let size of Object.keys(imageData)) {
|
||||
if (!INTEGER.test(size)) {
|
||||
throw new Error(`Invalid icon size ${size}, must be an integer`);
|
||||
}
|
||||
result[size] = this.convertImageDataToPNG(imageData[size], context);
|
||||
}
|
||||
}
|
||||
|
||||
if (details.path) {
|
||||
let path = details.path;
|
||||
if (typeof path != "object") {
|
||||
path = {"19": path};
|
||||
}
|
||||
|
||||
let baseURI = context ? context.uri : extension.baseURI;
|
||||
|
||||
for (let size of Object.keys(path)) {
|
||||
if (!INTEGER.test(size)) {
|
||||
throw new Error(`Invalid icon size ${size}, must be an integer`);
|
||||
}
|
||||
|
||||
let url = baseURI.resolve(path[size]);
|
||||
|
||||
// The Chrome documentation specifies these parameters as
|
||||
// relative paths. We currently accept absolute URLs as well,
|
||||
// which means we need to check that the extension is allowed
|
||||
// to load them. This will throw an error if it's not allowed.
|
||||
Services.scriptSecurityManager.checkLoadURIStrWithPrincipal(
|
||||
extension.principal, url,
|
||||
Services.scriptSecurityManager.DISALLOW_SCRIPT);
|
||||
|
||||
result[size] = url;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Function is called from extension code, delegate error.
|
||||
if (context) {
|
||||
throw e;
|
||||
}
|
||||
// If there's no context, it's because we're handling this
|
||||
// as a manifest directive. Log a warning rather than
|
||||
// raising an error.
|
||||
extension.manifestError(`Invalid icon data: ${e}`);
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
// Returns the appropriate icon URL for the given icons object and the
|
||||
// screen resolution of the given window.
|
||||
getURL(icons, window, extension, size = 16) {
|
||||
const DEFAULT = "chrome://browser/content/extension.svg";
|
||||
|
||||
size *= window.devicePixelRatio;
|
||||
|
||||
let bestSize = null;
|
||||
if (icons[size]) {
|
||||
bestSize = size;
|
||||
} else if (icons[2 * size]) {
|
||||
bestSize = 2 * size;
|
||||
} else {
|
||||
let sizes = Object.keys(icons)
|
||||
.map(key => parseInt(key, 10))
|
||||
.sort((a, b) => a - b);
|
||||
|
||||
bestSize = sizes.find(candidate => candidate > size) || sizes.pop();
|
||||
}
|
||||
|
||||
if (bestSize) {
|
||||
return {size: bestSize, icon: icons[bestSize]};
|
||||
}
|
||||
|
||||
return {size, icon: DEFAULT};
|
||||
},
|
||||
|
||||
convertImageDataToPNG(imageData, context) {
|
||||
let document = context.contentWindow.document;
|
||||
let canvas = document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
|
||||
canvas.width = imageData.width;
|
||||
canvas.height = imageData.height;
|
||||
canvas.getContext("2d").putImageData(imageData, 0, 0);
|
||||
|
||||
return canvas.toDataURL("image/png");
|
||||
},
|
||||
};
|
||||
|
||||
global.makeWidgetId = id => {
|
||||
id = id.toLowerCase();
|
||||
// FIXME: This allows for collisions.
|
||||
|
|
|
@ -55,6 +55,7 @@
|
|||
{
|
||||
"name": "show",
|
||||
"type": "function",
|
||||
"async": true,
|
||||
"description": "Shows the page action. The page action is shown whenever the tab is selected.",
|
||||
"parameters": [
|
||||
{"type": "integer", "name": "tabId", "minimum": 0, "description": "The id of the tab for which you want to modify the page action."}
|
||||
|
@ -63,6 +64,7 @@
|
|||
{
|
||||
"name": "hide",
|
||||
"type": "function",
|
||||
"async": true,
|
||||
"description": "Hides the page action.",
|
||||
"parameters": [
|
||||
{"type": "integer", "name": "tabId", "minimum": 0, "description": "The id of the tab for which you want to modify the page action."}
|
||||
|
|
|
@ -15,6 +15,7 @@ XPCOMUtils.defineLazyModuleGetter(this, "PageActions",
|
|||
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
|
||||
|
||||
var {
|
||||
IconDetails,
|
||||
SingletonEventManager,
|
||||
} = ExtensionUtils;
|
||||
|
||||
|
@ -24,13 +25,13 @@ var pageActionMap = new WeakMap();
|
|||
function PageAction(options, extension) {
|
||||
this.id = null;
|
||||
|
||||
let DEFAULT_ICON = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACQAAAAkCAYAAADhAJiYAAAC4klEQVRYhdWXLWzbQBSADQtDAwsHC1tUhUxqfL67lk2tdn+OJg0ODU0rLByqgqINBY6tmlbn7LMTJ5FaFVVBk1G0oUGjG2jT2Y7jxmmcbU/6iJ+f36fz+e5sGP9riCGm9hB37RG+scd4Yo/wsDXCZyIE2xuXsce4bY+wXkAsQtzYmExrfFgvkJkRbkzo1ehoxx5iXcgI/9iYUGt8WH9MqDXEcmNChmEYrRCf2SHWeYgQx3x0tLNRIeKQLTtEFyJEep4NTuhk8BC+yMrwEE3+iozo42d8gK7FAOkMsRiiN8QhW2ttSK5QTfRRV4QoymVeJMvPvDp7gCZigD613MN6yRFA3SWarow9QB9LCfG+NeF9qCtjAKOSQjCqVKhfVsiHEQ+grgx/lRGqUihAc1uL8EFD+KCRO+GrF4J61phcoRoPoEzkYhZYpykh5sMb7kOdIeY+jHKur4QI4Feh4AFX1nVeLxrAvQchGsBz5ls6wa2QdwcvIcE2863bTH79KOvsz/uUYJsp+J0pSzNlDckVqqVGUAF+n6uS7txcOl6wot4JVy70ufDLy4pWLUQVPE81pRI0mGe9oxLMHSeohHvMs/STUNaUK6vDPCvOyxMFDx4achehRDJmHnydnkPww5OFfLxrGIZBFDyYl4LpMzlTQFIP6AQx86w2UeYBccFpJrcKv5L9eGDtUAU6RIELqsB74uynjy/UBRF1gS5BTFxwQT1wTiXoUg9MH7m/3NZRRoi5IJytUbMgzv4Wc832+oQkiKgEehmyMkkpKsFkQV11QsRJL5rJYBLItQgRaUZEmnoZXsomz3vGiWw+I9KMF9SVFOqZEemZekli1jN3U/UOqhHHvC6oWWGElhfSpGdOk6+O9prdwvtLj5BjRsQxdRnot+Zeifpy/2/0stktKTRNLmbk0mwXyl8253fyojj+8rxOHNAhjjm5n0/5OOCGOKBzkrMO0Z75lvSAzKlrF32Z/3z8BqLAn+yMV7VhAAAAAElFTkSuQmCC";
|
||||
this.extension = extension;
|
||||
this.icons = IconDetails.normalize({path: options.default_icon}, extension);
|
||||
|
||||
this.popupUrl = options.default_popup;
|
||||
|
||||
this.options = {
|
||||
title: options.default_title || extension.name,
|
||||
icon: DEFAULT_ICON,
|
||||
id: extension.id,
|
||||
clickCallback: () => {
|
||||
if (this.popupUrl) {
|
||||
|
@ -45,18 +46,40 @@ function PageAction(options, extension) {
|
|||
},
|
||||
};
|
||||
|
||||
this.shouldShow = false;
|
||||
|
||||
EventEmitter.decorate(this);
|
||||
}
|
||||
|
||||
PageAction.prototype = {
|
||||
show(tabId) {
|
||||
// TODO: Only show the PageAction for the tab with the provided tabId.
|
||||
if (!this.id) {
|
||||
show(tabId, context) {
|
||||
if (this.id) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
if (this.options.icon) {
|
||||
this.id = PageActions.add(this.options);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
this.shouldShow = true;
|
||||
|
||||
let imageURL = IconDetails.getURL(this.icons, context.contentWindow, this.extension);
|
||||
let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
|
||||
return IconDetails.convertImageURLToDataURL(imageURL, context, browserWindow).then(dataURI => {
|
||||
if (this.shouldShow) {
|
||||
this.options.icon = dataURI;
|
||||
this.id = PageActions.add(this.options);
|
||||
}
|
||||
}).catch(() => {
|
||||
return Promise.reject({
|
||||
message: "Failed to load PageAction icon",
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
hide(tabId) {
|
||||
this.shouldShow = false;
|
||||
if (this.id) {
|
||||
PageActions.remove(this.id);
|
||||
this.id = null;
|
||||
|
@ -106,11 +129,14 @@ extensions.registerSchemaAPI("pageAction", (extension, context) => {
|
|||
}).api(),
|
||||
|
||||
show(tabId) {
|
||||
pageActionMap.get(extension).show(tabId);
|
||||
return pageActionMap.get(extension)
|
||||
.show(tabId, context)
|
||||
.then(() => {});
|
||||
},
|
||||
|
||||
hide(tabId) {
|
||||
pageActionMap.get(extension).hide(tabId);
|
||||
return Promise.resolve();
|
||||
},
|
||||
|
||||
setPopup(details) {
|
||||
|
|
|
@ -19,7 +19,6 @@
|
|||
"preprocess": "localize"
|
||||
},
|
||||
"default_icon": {
|
||||
"unsupported": true,
|
||||
"$ref": "IconPath",
|
||||
"optional": true
|
||||
},
|
||||
|
@ -57,6 +56,7 @@
|
|||
"name": "show",
|
||||
"type": "function",
|
||||
"description": "Shows the page action. The page action is shown whenever the tab is selected.",
|
||||
"async": true,
|
||||
"parameters": [
|
||||
{"type": "integer", "name": "tabId", "minimum": 0, "description": "The id of the tab for which you want to modify the page action."}
|
||||
]
|
||||
|
@ -65,6 +65,7 @@
|
|||
"name": "hide",
|
||||
"type": "function",
|
||||
"description": "Hides the page action.",
|
||||
"async": true,
|
||||
"parameters": [
|
||||
{"type": "integer", "name": "tabId", "minimum": 0, "description": "The id of the tab for which you want to modify the page action."}
|
||||
]
|
||||
|
@ -161,6 +162,7 @@
|
|||
{
|
||||
"name": "setPopup",
|
||||
"type": "function",
|
||||
"async": true,
|
||||
"description": "Sets the html document to be opened as a popup when the user clicks on the page action's icon.",
|
||||
"parameters": [
|
||||
{
|
||||
|
@ -180,7 +182,7 @@
|
|||
"name": "getPopup",
|
||||
"type": "function",
|
||||
"description": "Gets the html document set as the popup for this page action.",
|
||||
"async": "callback",
|
||||
"async": true,
|
||||
"parameters": [
|
||||
{
|
||||
"name": "details",
|
||||
|
@ -191,16 +193,6 @@
|
|||
"description": "Specify the tab to get the popup from."
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"name": "callback",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "result",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -13,6 +13,11 @@
|
|||
<script type="text/javascript">
|
||||
"use strict";
|
||||
|
||||
let dataURI = "iVBORw0KGgoAAAANSUhEUgAAACQAAAAkCAYAAADhAJiYAAAC4klEQVRYhdWXLWzbQBSADQtDAwsHC1tUhUxqfL67lk2tdn+OJg0ODU0rLByqgqINBY6tmlbn7LMTJ5FaFVVBk1G0oUGjG2jT2Y7jxmmcbU/6iJ+f36fz+e5sGP9riCGm9hB37RG+scd4Yo/wsDXCZyIE2xuXsce4bY+wXkAsQtzYmExrfFgvkJkRbkzo1ehoxx5iXcgI/9iYUGt8WH9MqDXEcmNChmEYrRCf2SHWeYgQx3x0tLNRIeKQLTtEFyJEep4NTuhk8BC+yMrwEE3+iozo42d8gK7FAOkMsRiiN8QhW2ttSK5QTfRRV4QoymVeJMvPvDp7gCZigD613MN6yRFA3SWarow9QB9LCfG+NeF9qCtjAKOSQjCqVKhfVsiHEQ+grgx/lRGqUihAc1uL8EFD+KCRO+GrF4J61phcoRoPoEzkYhZYpykh5sMb7kOdIeY+jHKur4QI4Feh4AFX1nVeLxrAvQchGsBz5ls6wa2QdwcvIcE2863bTH79KOvsz/uUYJsp+J0pSzNlDckVqqVGUAF+n6uS7txcOl6wot4JVy70ufDLy4pWLUQVPE81pRI0mGe9oxLMHSeohHvMs/STUNaUK6vDPCvOyxMFDx4achehRDJmHnydnkPww5OFfLxrGIZBFDyYl4LpMzlTQFIP6AQx86w2UeYBccFpJrcKv5L9eGDtUAU6RIELqsB74uynjy/UBRF1gS5BTFxwQT1wTiXoUg9MH7m/3NZRRoi5IJytUbMgzv4Wc832+oQkiKgEehmyMkkpKsFkQV11QsRJL5rJYBLItQgRaUZEmnoZXsomz3vGiWw+I9KMF9SVFOqZEemZekli1jN3U/UOqhHHvC6oWWGElhfSpGdOk6+O9prdwvtLj5BjRsQxdRnot+Zeifpy/2/0stktKTRNLmbk0mwXyl8253fyojj+8rxOHNAhjjm5n0/5OOCGOKBzkrMO0Z75lvSAzKlrF32Z/3z8BqLAn+yMV7VhAAAAAElFTkSuQmCC";
|
||||
|
||||
let image = atob(dataURI);
|
||||
const IMAGE_ARRAYBUFFER = Uint8Array.from(image, byte => byte.charCodeAt(0)).buffer;
|
||||
|
||||
function backgroundScript() {
|
||||
browser.test.assertTrue("pageAction" in browser, "Namespace 'pageAction' exists in browser");
|
||||
browser.test.assertTrue("show" in browser.pageAction, "API method 'show' exists in browser.pageAction");
|
||||
|
@ -21,11 +26,13 @@ function backgroundScript() {
|
|||
let tabId = 1;
|
||||
browser.test.onMessage.addListener(msg => {
|
||||
if (msg === "pageAction-show") {
|
||||
browser.pageAction.show(tabId);
|
||||
browser.pageAction.show(tabId).then(() => {
|
||||
browser.test.sendMessage("page-action-shown");
|
||||
});
|
||||
} else if (msg === "pageAction-hide") {
|
||||
browser.pageAction.hide(tabId);
|
||||
browser.pageAction.hide(tabId).then(() => {
|
||||
browser.test.sendMessage("page-action-hidden");
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -44,8 +51,14 @@ add_task(function* test_contentscript() {
|
|||
"name": "PageAction Extension",
|
||||
"page_action": {
|
||||
"default_title": "Page Action",
|
||||
"default_icon": {
|
||||
"18": "extension.png",
|
||||
},
|
||||
},
|
||||
},
|
||||
files: {
|
||||
"extension.png": IMAGE_ARRAYBUFFER,
|
||||
},
|
||||
});
|
||||
|
||||
yield extension.startup();
|
||||
|
|
|
@ -15,6 +15,11 @@
|
|||
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
|
||||
let dataURI = "iVBORw0KGgoAAAANSUhEUgAAACQAAAAkCAYAAADhAJiYAAAC4klEQVRYhdWXLWzbQBSADQtDAwsHC1tUhUxqfL67lk2tdn+OJg0ODU0rLByqgqINBY6tmlbn7LMTJ5FaFVVBk1G0oUGjG2jT2Y7jxmmcbU/6iJ+f36fz+e5sGP9riCGm9hB37RG+scd4Yo/wsDXCZyIE2xuXsce4bY+wXkAsQtzYmExrfFgvkJkRbkzo1ehoxx5iXcgI/9iYUGt8WH9MqDXEcmNChmEYrRCf2SHWeYgQx3x0tLNRIeKQLTtEFyJEep4NTuhk8BC+yMrwEE3+iozo42d8gK7FAOkMsRiiN8QhW2ttSK5QTfRRV4QoymVeJMvPvDp7gCZigD613MN6yRFA3SWarow9QB9LCfG+NeF9qCtjAKOSQjCqVKhfVsiHEQ+grgx/lRGqUihAc1uL8EFD+KCRO+GrF4J61phcoRoPoEzkYhZYpykh5sMb7kOdIeY+jHKur4QI4Feh4AFX1nVeLxrAvQchGsBz5ls6wa2QdwcvIcE2863bTH79KOvsz/uUYJsp+J0pSzNlDckVqqVGUAF+n6uS7txcOl6wot4JVy70ufDLy4pWLUQVPE81pRI0mGe9oxLMHSeohHvMs/STUNaUK6vDPCvOyxMFDx4achehRDJmHnydnkPww5OFfLxrGIZBFDyYl4LpMzlTQFIP6AQx86w2UeYBccFpJrcKv5L9eGDtUAU6RIELqsB74uynjy/UBRF1gS5BTFxwQT1wTiXoUg9MH7m/3NZRRoi5IJytUbMgzv4Wc832+oQkiKgEehmyMkkpKsFkQV11QsRJL5rJYBLItQgRaUZEmnoZXsomz3vGiWw+I9KMF9SVFOqZEemZekli1jN3U/UOqhHHvC6oWWGElhfSpGdOk6+O9prdwvtLj5BjRsQxdRnot+Zeifpy/2/0stktKTRNLmbk0mwXyl8253fyojj+8rxOHNAhjjm5n0/5OOCGOKBzkrMO0Z75lvSAzKlrF32Z/3z8BqLAn+yMV7VhAAAAAElFTkSuQmCC";
|
||||
|
||||
let image = atob(dataURI);
|
||||
const IMAGE_ARRAYBUFFER = Uint8Array.from(image, byte => byte.charCodeAt(0)).buffer;
|
||||
|
||||
add_task(function* test_contentscript() {
|
||||
function backgroundScript() {
|
||||
// TODO: Use the Tabs API to obtain the tab ids for showing pageActions.
|
||||
|
@ -24,11 +29,13 @@ add_task(function* test_contentscript() {
|
|||
browser.test.onMessage.addListener((msg, details) => {
|
||||
if (msg === "page-action-show") {
|
||||
// TODO: switch to using .show(tabId).then(...) once bug 1270742 lands.
|
||||
browser.pageAction.show(tabId);
|
||||
browser.pageAction.show(tabId).then(() => {
|
||||
browser.test.sendMessage("page-action-shown");
|
||||
});
|
||||
} else if (msg == "page-action-set-popup") {
|
||||
browser.pageAction.setPopup({popup: details.name, tabId: tabId});
|
||||
browser.pageAction.setPopup({popup: details.name, tabId: tabId}).then(() => {
|
||||
browser.test.sendMessage("page-action-popup-set");
|
||||
});
|
||||
} else if (msg == "page-action-get-popup") {
|
||||
browser.pageAction.getPopup({tabId: tabId}).then(url => {
|
||||
browser.test.sendMessage("page-action-got-popup", url);
|
||||
|
@ -70,10 +77,14 @@ add_task(function* test_contentscript() {
|
|||
"page_action": {
|
||||
"default_title": "Page Action",
|
||||
"default_popup": "default.html",
|
||||
"default_icon": {
|
||||
"18": "extension.png",
|
||||
},
|
||||
},
|
||||
},
|
||||
files: {
|
||||
"default.html": `<html><head><meta charset="utf-8"><script src="popup.js"></${"script"}></head></html>`,
|
||||
"extension.png": IMAGE_ARRAYBUFFER,
|
||||
"a.html": `<html><head><meta charset="utf-8"><script src="popup.js"></${"script"}></head></html>`,
|
||||
"b.html": `<html><head><meta charset="utf-8"><script src="popup.js"></${"script"}></head></html>`,
|
||||
"popup.js": `(${popupScript})()`,
|
||||
|
|
|
@ -11,9 +11,13 @@ const Cc = Components.classes;
|
|||
const Cu = Components.utils;
|
||||
const Cr = Components.results;
|
||||
|
||||
const INTEGER = /^[1-9]\d*$/;
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
|
||||
"resource://gre/modules/AddonManager.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
|
||||
"resource://gre/modules/AppConstants.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "LanguageDetector",
|
||||
|
@ -391,6 +395,133 @@ class BaseContext {
|
|||
}
|
||||
}
|
||||
|
||||
// Manages icon details for toolbar buttons in the |pageAction| and
|
||||
// |browserAction| APIs.
|
||||
let IconDetails = {
|
||||
// Normalizes the various acceptable input formats into an object
|
||||
// with icon size as key and icon URL as value.
|
||||
//
|
||||
// If a context is specified (function is called from an extension):
|
||||
// Throws an error if an invalid icon size was provided or the
|
||||
// extension is not allowed to load the specified resources.
|
||||
//
|
||||
// If no context is specified, instead of throwing an error, this
|
||||
// function simply logs a warning message.
|
||||
normalize(details, extension, context = null) {
|
||||
let result = {};
|
||||
|
||||
try {
|
||||
if (details.imageData) {
|
||||
let imageData = details.imageData;
|
||||
|
||||
// The global might actually be from Schema.jsm, which
|
||||
// normalizes most of our arguments. In that case it won't have
|
||||
// an ImageData property. But Schema.jsm doesn't normalize
|
||||
// actual ImageData objects, so they will come from a global
|
||||
// with the right property.
|
||||
if (instanceOf(imageData, "ImageData")) {
|
||||
imageData = {"19": imageData};
|
||||
}
|
||||
|
||||
for (let size of Object.keys(imageData)) {
|
||||
if (!INTEGER.test(size)) {
|
||||
throw new Error(`Invalid icon size ${size}, must be an integer`);
|
||||
}
|
||||
result[size] = this.convertImageDataToDataURI(imageData[size], context);
|
||||
}
|
||||
}
|
||||
|
||||
if (details.path) {
|
||||
let path = details.path;
|
||||
if (typeof path != "object") {
|
||||
path = {"19": path};
|
||||
}
|
||||
|
||||
let baseURI = context ? context.uri : extension.baseURI;
|
||||
|
||||
for (let size of Object.keys(path)) {
|
||||
if (!INTEGER.test(size)) {
|
||||
throw new Error(`Invalid icon size ${size}, must be an integer`);
|
||||
}
|
||||
|
||||
let url = baseURI.resolve(path[size]);
|
||||
|
||||
// The Chrome documentation specifies these parameters as
|
||||
// relative paths. We currently accept absolute URLs as well,
|
||||
// which means we need to check that the extension is allowed
|
||||
// to load them. This will throw an error if it's not allowed.
|
||||
Services.scriptSecurityManager.checkLoadURIStrWithPrincipal(
|
||||
extension.principal, url,
|
||||
Services.scriptSecurityManager.DISALLOW_SCRIPT);
|
||||
|
||||
result[size] = url;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Function is called from extension code, delegate error.
|
||||
if (context) {
|
||||
throw e;
|
||||
}
|
||||
// If there's no context, it's because we're handling this
|
||||
// as a manifest directive. Log a warning rather than
|
||||
// raising an error.
|
||||
extension.manifestError(`Invalid icon data: ${e}`);
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
// Returns the appropriate icon URL for the given icons object and the
|
||||
// screen resolution of the given window.
|
||||
getURL(icons, window, extension, size = 18) {
|
||||
const DEFAULT = "chrome://browser/content/extension.svg";
|
||||
|
||||
return AddonManager.getPreferredIconURL({icons: icons}, size, window) || DEFAULT;
|
||||
},
|
||||
|
||||
convertImageURLToDataURL(imageURL, context, browserWindow, size = 18) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let image = new context.contentWindow.Image();
|
||||
image.onload = function() {
|
||||
let canvas = context.contentWindow.document.createElement("canvas");
|
||||
let ctx = canvas.getContext("2d");
|
||||
let dSize = size * browserWindow.devicePixelRatio;
|
||||
|
||||
// Scales the image while maintaing width to height ratio.
|
||||
// If the width and height differ, the image is centered using the
|
||||
// smaller of the two dimensions.
|
||||
let dWidth, dHeight, dx, dy;
|
||||
if (this.width > this.height) {
|
||||
dWidth = dSize;
|
||||
dHeight = image.height * (dSize / image.width);
|
||||
dx = 0;
|
||||
dy = (dSize - dHeight) / 2;
|
||||
} else {
|
||||
dWidth = image.width * (dSize / image.height);
|
||||
dHeight = dSize;
|
||||
dx = (dSize - dWidth) / 2;
|
||||
dy = 0;
|
||||
}
|
||||
|
||||
ctx.drawImage(this, 0, 0, this.width, this.height, dx, dy, dWidth, dHeight);
|
||||
resolve(canvas.toDataURL("image/png"));
|
||||
};
|
||||
image.onerror = reject;
|
||||
image.src = imageURL;
|
||||
});
|
||||
},
|
||||
|
||||
convertImageDataToDataURL(imageData, context) {
|
||||
let document = context.contentWindow.document;
|
||||
let canvas = document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
|
||||
canvas.width = imageData.width;
|
||||
canvas.height = imageData.height;
|
||||
canvas.getContext("2d").putImageData(imageData, 0, 0);
|
||||
|
||||
return canvas.toDataURL("image/png");
|
||||
},
|
||||
};
|
||||
|
||||
function LocaleData(data) {
|
||||
this.defaultLocale = data.defaultLocale;
|
||||
this.selectedLocale = data.selectedLocale;
|
||||
|
@ -1256,6 +1387,7 @@ this.ExtensionUtils = {
|
|||
BaseContext,
|
||||
DefaultWeakMap,
|
||||
EventManager,
|
||||
IconDetails,
|
||||
LocaleData,
|
||||
Messenger,
|
||||
PlatformInfo,
|
||||
|
|
|
@ -946,10 +946,11 @@ class ArrayType extends Type {
|
|||
}
|
||||
|
||||
class FunctionType extends Type {
|
||||
constructor(schema, parameters, isAsync) {
|
||||
constructor(schema, parameters, isAsync, hasAsyncCallback) {
|
||||
super(schema);
|
||||
this.parameters = parameters;
|
||||
this.isAsync = isAsync;
|
||||
this.hasAsyncCallback = hasAsyncCallback;
|
||||
}
|
||||
|
||||
normalize(value, context) {
|
||||
|
@ -1156,6 +1157,7 @@ class FunctionEntry extends CallEntry {
|
|||
this.permissions = permissions;
|
||||
|
||||
this.isAsync = type.isAsync;
|
||||
this.hasAsyncCallback = type.hasAsyncCallback;
|
||||
}
|
||||
|
||||
inject(path, name, dest, context) {
|
||||
|
@ -1172,7 +1174,10 @@ class FunctionEntry extends CallEntry {
|
|||
stub = (...args) => {
|
||||
this.checkDeprecated(context);
|
||||
let actuals = this.checkParameters(args, context);
|
||||
let callback = actuals.pop();
|
||||
let callback = null;
|
||||
if (this.hasAsyncCallback) {
|
||||
callback = actuals.pop();
|
||||
}
|
||||
return context.callAsyncFunction(path, name, actuals, callback);
|
||||
};
|
||||
} else if (!this.returns) {
|
||||
|
@ -1403,7 +1408,7 @@ this.Schemas = {
|
|||
checkTypeProperties();
|
||||
return new BooleanType(type);
|
||||
} else if (type.type == "function") {
|
||||
let isAsync = typeof(type.async) == "string";
|
||||
let isAsync = Boolean(type.async);
|
||||
|
||||
let parameters = null;
|
||||
if ("parameters" in type) {
|
||||
|
@ -1421,9 +1426,10 @@ this.Schemas = {
|
|||
}
|
||||
}
|
||||
|
||||
let hasAsyncCallback = false;
|
||||
if (isAsync) {
|
||||
if (!parameters || !parameters.length || parameters[parameters.length - 1].name != type.async) {
|
||||
throw new Error(`Internal error: "async" property must name the last parameter of the function.`);
|
||||
if (parameters && parameters.length && parameters[parameters.length - 1].name == type.async) {
|
||||
hasAsyncCallback = true;
|
||||
}
|
||||
if (type.returns || type.allowAmbiguousOptionalArguments) {
|
||||
throw new Error(`Internal error: Async functions must not have return values or ambiguous arguments.`);
|
||||
|
@ -1431,7 +1437,7 @@ this.Schemas = {
|
|||
}
|
||||
|
||||
checkTypeProperties("parameters", "async", "returns");
|
||||
return new FunctionType(type, parameters, isAsync);
|
||||
return new FunctionType(type, parameters, isAsync, hasAsyncCallback);
|
||||
} else if (type.type == "any") {
|
||||
// Need to see what minimum and maximum are supposed to do here.
|
||||
checkTypeProperties("minimum", "maximum");
|
||||
|
|
Загрузка…
Ссылка в новой задаче