Merge fx-team to m-c.

This commit is contained in:
Ryan VanderMeulen 2014-02-05 15:31:20 -05:00
commit 8a68d16b9a
64 changed files with 2152 additions and 1312 deletions

View File

@ -1094,12 +1094,18 @@ pref("devtools.toolbox.toolbarSpec", '["splitconsole", "paintflashing toggle","t
pref("devtools.toolbox.sideEnabled", true);
pref("devtools.toolbox.zoomValue", "1");
// Inspector preferences
// Enable the Inspector
pref("devtools.inspector.enabled", true);
// What was the last active sidebar in the inspector
pref("devtools.inspector.activeSidebar", "ruleview");
// Enable the markup preview
pref("devtools.inspector.markupPreview", false);
pref("devtools.inspector.remote", false);
// Expand pseudo-elements by default in the rule-view
pref("devtools.inspector.show_pseudo_elements", true);
// The default size for image preview tooltips in the rule-view/computed-view/markup-view
pref("devtools.inspector.imagePreviewTooltipSize", 300);
// DevTools default color unit
pref("devtools.defaultColorUnit", "hex");

View File

@ -142,18 +142,23 @@ let wrapper = {
// Remember who it was so we can log out next time.
setPreviousAccountNameHash(newAccountEmail);
fxAccounts.setSignedInUser(accountData).then(
() => {
this.injectData("message", { status: "login" });
// until we sort out a better UX, just leave the jelly page in place.
// If the account email is not yet verified, it will tell the user to
// go check their email, but then it will *not* change state after
// the verification completes (the browser will begin syncing, but
// won't notify the user). If the email has already been verified,
// the jelly will say "Welcome! You are successfully signed in as
// EMAIL", but it won't then say "syncing started".
},
(err) => this.injectData("message", { status: "error", error: err })
// A sync-specific hack - we want to ensure sync has been initialized
// before we set the signed-in user.
let xps = Cc["@mozilla.org/weave/service;1"]
.getService(Ci.nsISupports)
.wrappedJSObject;
xps.whenLoaded().then(() => {
return fxAccounts.setSignedInUser(accountData);
}).then(() => {
this.injectData("message", { status: "login" });
// until we sort out a better UX, just leave the jelly page in place.
// If the account email is not yet verified, it will tell the user to
// go check their email, but then it will *not* change state after
// the verification completes (the browser will begin syncing, but
// won't notify the user). If the email has already been verified,
// the jelly will say "Welcome! You are successfully signed in as
// EMAIL", but it won't then say "syncing started".
}, (err) => this.injectData("message", { status: "error", error: err })
);
},

View File

@ -152,8 +152,12 @@
<panelview id="PanelUI-characterEncodingView" flex="1">
<label value="&charsetMenu.label;" class="panel-subview-header"/>
<vbox id="PanelUI-characterEncodingView-customlist"
<vbox id="PanelUI-characterEncodingView-pinned"
class="PanelUI-characterEncodingView-list"/>
<toolbarseparator/>
<vbox id="PanelUI-characterEncodingView-charsets"
class="PanelUI-characterEncodingView-list"/>
<toolbarseparator/>
<vbox>
<label id="PanelUI-characterEncodingView-autodetect-label"/>
<vbox id="PanelUI-characterEncodingView-autodetect"

View File

@ -18,6 +18,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "RecentlyClosedTabsAndWindowsMenuUtils",
"resource:///modules/sessionstore/RecentlyClosedTabsAndWindowsMenuUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ShortcutUtils",
"resource://gre/modules/ShortcutUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "CharsetMenu",
"resource://gre/modules/CharsetMenu.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "CharsetManager",
"@mozilla.org/charset-converter-manager;1",
"nsICharsetConverterManager");
@ -650,131 +652,86 @@ const CustomizableWidgets = [{
window.gBrowser.docShell &&
window.gBrowser.docShell.mayEnableCharacterEncodingMenu);
},
getCharsetList: function(aSection, aDocument) {
let currCharset = aDocument.defaultView.content.document.characterSet;
let list = "";
try {
let pref = "intl.charsetmenu.browser." + aSection;
list = Services.prefs.getComplexValue(pref,
Ci.nsIPrefLocalizedString).data;
} catch (e) {}
list = list.trim();
if (!list)
return [];
list = list.split(",");
let items = [];
for (let charset of list) {
charset = charset.trim();
let notForBrowser = false;
try {
notForBrowser = CharsetManager.getCharsetData(charset,
"notForBrowser");
} catch (e) {}
if (notForBrowser)
continue;
let title = charset;
try {
title = CharsetManager.getCharsetTitle(charset);
} catch (e) {}
items.push({value: charset, name: title, current: charset == currCharset});
}
return items;
},
getAutoDetectors: function(aDocument) {
let detectorEnum = CharsetManager.GetCharsetDetectorList();
let currDetector;
try {
currDetector = Services.prefs.getComplexValue(
"intl.charset.detector", Ci.nsIPrefLocalizedString).data;
} catch (e) {}
if (!currDetector)
currDetector = "off";
currDetector = "chardet." + currDetector;
let items = [];
while (detectorEnum.hasMore()) {
let detector = detectorEnum.getNext();
let title = detector;
try {
title = CharsetManager.getCharsetTitle(detector);
} catch (e) {}
items.push({value: detector, name: title, current: detector == currDetector});
}
items.sort((aItem1, aItem2) => {
return aItem1.name.localeCompare(aItem2.name);
});
return items;
},
populateList: function(aDocument, aContainerId, aSection) {
let containerElem = aDocument.getElementById(aContainerId);
while (containerElem.firstChild) {
containerElem.removeChild(containerElem.firstChild);
}
containerElem.addEventListener("command", this.onCommand, false);
let list = [];
if (aSection == "autodetect") {
list = this.getAutoDetectors(aDocument);
} else if (aSection == "browser") {
let staticList = this.getCharsetList("static", aDocument);
let cacheList = this.getCharsetList("cache", aDocument);
// Combine lists, and de-duplicate.
let checkedIn = new Set();
for (let item of staticList.concat(cacheList)) {
let itemName = item.name.toLowerCase();
if (!checkedIn.has(itemName)) {
list.push(item);
checkedIn.add(itemName);
}
}
}
let list = this.charsetInfo[aSection];
// Update the appearance of the buttons when it's not possible to
// customize encoding.
let disabled = this.maybeDisableMenu(aDocument);
for (let item of list) {
let elem = aDocument.createElementNS(kNSXUL, "toolbarbutton");
elem.setAttribute("label", item.name);
elem.section = aSection;
elem.value = item.value;
if (item.current)
elem.setAttribute("current", "true");
if (disabled)
elem.setAttribute("disabled", "true");
elem.setAttribute("label", item.label);
elem.section = aSection == "detectors" ? "detectors" : "charsets";
elem.value = item.id;
elem.setAttribute("class", "subviewbutton");
containerElem.appendChild(elem);
}
},
updateCurrentCharset: function(aDocument) {
let content = aDocument.defaultView.content;
let currentCharset = content && content.document && content.document.characterSet;
if (currentCharset) {
currentCharset = aDocument.defaultView.FoldCharset(currentCharset);
}
currentCharset = currentCharset ? ("charset." + currentCharset) : "";
let pinnedContainer = aDocument.getElementById("PanelUI-characterEncodingView-pinned");
let charsetContainer = aDocument.getElementById("PanelUI-characterEncodingView-charsets");
let elements = [...(pinnedContainer.childNodes), ...(charsetContainer.childNodes)];
this._updateElements(elements, currentCharset);
},
updateCurrentDetector: function(aDocument) {
let detectorContainer = aDocument.getElementById("PanelUI-characterEncodingView-autodetect");
let detectorEnum = CharsetManager.GetCharsetDetectorList();
let currentDetector;
try {
currentDetector = Services.prefs.getComplexValue(
"intl.charset.detector", Ci.nsIPrefLocalizedString).data;
} catch (e) {}
currentDetector = "chardet." + (currentDetector || "off");
this._updateElements(detectorContainer.childNodes, currentDetector);
},
_updateElements: function(aElements, aCurrentItem) {
if (!aElements.length) {
return;
}
let disabled = this.maybeDisableMenu(aElements[0].ownerDocument);
for (let elem of aElements) {
if (disabled) {
elem.setAttribute("disabled", "true");
} else {
elem.removeAttribute("disabled");
}
if (elem.value.toLowerCase() == aCurrentItem.toLowerCase()) {
elem.setAttribute("current", "true");
} else {
elem.removeAttribute("current");
}
}
},
onViewShowing: function(aEvent) {
let document = aEvent.target.ownerDocument;
let autoDetectLabelId = "PanelUI-characterEncodingView-autodetect-label";
let autoDetectLabel = document.getElementById(autoDetectLabelId);
let label = CharsetBundle.GetStringFromName("charsetMenuAutodet");
autoDetectLabel.setAttribute("value", label);
this.populateList(document,
"PanelUI-characterEncodingView-customlist",
"browser");
this.populateList(document,
"PanelUI-characterEncodingView-autodetect",
"autodetect");
if (!autoDetectLabel.hasAttribute("value")) {
let label = CharsetBundle.GetStringFromName("charsetMenuAutodet");
autoDetectLabel.setAttribute("value", label);
this.populateList(document,
"PanelUI-characterEncodingView-pinned",
"pinnedCharsets");
this.populateList(document,
"PanelUI-characterEncodingView-charsets",
"otherCharsets");
this.populateList(document,
"PanelUI-characterEncodingView-autodetect",
"detectors");
}
this.updateCurrentDetector(document);
this.updateCurrentCharset(document);
},
onCommand: function(aEvent) {
let node = aEvent.target;
@ -782,16 +739,16 @@ const CustomizableWidgets = [{
return;
}
CustomizableUI.hidePanelForNode(node);
let window = node.ownerDocument.defaultView;
let section = node.section;
let value = node.value;
// The behavior as implemented here is directly based off of the
// `MultiplexHandler()` method in browser.js.
if (section == "browser") {
window.BrowserSetForcedCharacterSet(value);
} else if (section == "autodetect") {
if (section != "detectors") {
let charset = value.substring(value.indexOf('charset.') + 'charset.'.length);
window.BrowserSetForcedCharacterSet(charset);
} else {
value = value.replace(/^chardet\./, "");
if (value == "off") {
value = "";
@ -852,6 +809,9 @@ const CustomizableWidgets = [{
}
};
CustomizableUI.addListener(listener);
if (!this.charsetInfo) {
this.charsetInfo = CharsetMenu.getData();
}
}
}, {
id: "email-link-button",

View File

@ -1060,17 +1060,25 @@ Toolbox.prototype = {
let deferred = promise.defer();
if (this._inspector) {
this._selection.destroy();
this._selection = null;
this._walker.release().then(
// Selection is not always available.
if (this._selection) {
this._selection.destroy();
this._selection = null;
}
let walker = this._walker ? this._walker.release() : promise.resolve(null);
walker.then(
() => {
this._inspector.destroy();
this._highlighter.destroy();
if (this._highlighter) {
this._highlighter.destroy();
}
},
(e) => {
console.error("Walker.release() failed: " + e);
this._inspector.destroy();
return this._highlighter.destroy();
return this._highlighter ? this._highlighter.destroy() : promise.resolve(null);
}
).then(() => {
this._inspector = null;

View File

@ -101,6 +101,10 @@ InspectorPanel.prototype = {
return this._target.client.traits.editOuterHTML;
},
get hasUrlToImageDataResolver() {
return this._target.client.traits.urlToImageDataResolver;
},
_deferredOpen: function(defaultSelection) {
let deferred = promise.defer();

View File

@ -14,7 +14,6 @@ const COLLAPSE_ATTRIBUTE_LENGTH = 120;
const COLLAPSE_DATA_URL_REGEX = /^data.+base64/;
const COLLAPSE_DATA_URL_LENGTH = 60;
const CONTAINER_FLASHING_DURATION = 500;
const IMAGE_PREVIEW_MAX_DIM = 400;
const NEW_SELECTION_HIGHLIGHTER_TIMER = 1000;
const {UndoStack} = require("devtools/shared/undo");
@ -1270,16 +1269,17 @@ MarkupContainer.prototype = {
data: def.promise
};
this.node.getImageData(IMAGE_PREVIEW_MAX_DIM).then(data => {
if (data) {
data.data.string().then(str => {
let res = {data: str, size: data.size};
// Resolving the data promise and, to always keep tooltipData.data
// as a promise, create a new one that resolves immediately
def.resolve(res);
this.tooltipData.data = promise.resolve(res);
});
}
let maxDim = Services.prefs.getIntPref("devtools.inspector.imagePreviewTooltipSize");
this.node.getImageData(maxDim).then(data => {
data.data.string().then(str => {
let res = {data: str, size: data.size};
// Resolving the data promise and, to always keep tooltipData.data
// as a promise, create a new one that resolves immediately
def.resolve(res);
this.tooltipData.data = promise.resolve(res);
});
}, () => {
this.tooltipData.data = promise.reject();
});
}
},
@ -1288,11 +1288,9 @@ MarkupContainer.prototype = {
// We need to send again a request to gettooltipData even if one was sent for
// the tooltip, because we want the full-size image
this.node.getImageData().then(data => {
if (data) {
data.data.string().then(str => {
clipboardHelper.copyString(str, this.markup.doc);
});
}
data.data.string().then(str => {
clipboardHelper.copyString(str, this.markup.doc);
});
});
},
@ -1300,6 +1298,8 @@ MarkupContainer.prototype = {
if (this.tooltipData && target === this.tooltipData.target) {
this.tooltipData.data.then(({data, size}) => {
tooltip.setImageContent(data, size);
}, () => {
tooltip.setBrokenImageContent();
});
return true;
}

View File

@ -846,10 +846,10 @@ var Scratchpad = {
// Assemble the best possible stack we can given the properties we have.
let stack;
if (typeof error.stack == "string") {
if (typeof error.stack == "string" && error.stack) {
stack = error.stack;
}
else if (typeof error.fileName == "number") {
else if (typeof error.fileName == "string") {
stack = "@" + error.fileName;
if (typeof error.lineNumber == "number") {
stack += ":" + error.lineNumber;

View File

@ -13,7 +13,7 @@ function test()
openScratchpad(runTests, {"state":{"text":""}});
}, true);
content.location = "data:text/html,<p>test that exceptions our output as " +
content.location = "data:text/html,<p>test that exceptions are output as " +
"comments for 'display' and not sent to the console in Scratchpad";
}
@ -25,6 +25,7 @@ function runTests()
let openComment = "\n/*\n";
let closeComment = "\n*/";
let error = "throw new Error(\"Ouch!\")";
let syntaxError = "(";
let tests = [{
method: "display",
@ -39,6 +40,13 @@ function runTests()
scratchpad.uniqueName + ":1" + closeComment,
label: "error display output",
},
{
method: "display",
code: syntaxError,
result: syntaxError + openComment + "Exception: syntax error\n@" +
scratchpad.uniqueName + ":1" + closeComment,
label: "syntaxError display output",
},
{
method: "run",
code: message,
@ -51,6 +59,13 @@ function runTests()
result: error + openComment + "Exception: Ouch!\n@" +
scratchpad.uniqueName + ":1" + closeComment,
label: "error run output",
},
{
method: "run",
code: syntaxError,
result: syntaxError + openComment + "Exception: syntax error\n@" +
scratchpad.uniqueName + ":1" + closeComment,
label: "syntaxError run output",
}];
runAsyncTests(scratchpad, tests).then(finish);

View File

@ -29,7 +29,6 @@ XPCOMUtils.defineLazyModuleGetter(this, "VariablesViewController",
const GRADIENT_RE = /\b(repeating-)?(linear|radial)-gradient\(((rgb|hsl)a?\(.+?\)|[^\)])+\)/gi;
const BORDERCOLOR_RE = /^border-[-a-z]*color$/ig;
const BORDER_RE = /^border(-(top|bottom|left|right))?$/ig;
const BACKGROUND_IMAGE_RE = /url\([\'\"]?(.*?)[\'\"]?\)/;
const XHTML_NS = "http://www.w3.org/1999/xhtml";
const SPECTRUM_FRAME = "chrome://browser/content/devtools/spectrum-frame.xhtml";
const ESCAPE_KEYCODE = Ci.nsIDOMKeyEvent.DOM_VK_ESCAPE;
@ -569,9 +568,45 @@ Tooltip.prototype = {
},
/**
* Fill the tooltip with an image, displayed over a tiled background useful
* for transparent images. Also adds the image dimension as a label at the
* bottom.
* Uses the provided inspectorFront's getImageDataFromURL method to resolve
* the relative URL on the server-side, in the page context, and then sets the
* tooltip content with the resulting image just like |setImageContent| does.
*
* @return a promise that resolves when the image is shown in the tooltip
*/
setRelativeImageContent: function(imageUrl, inspectorFront, maxDim) {
if (imageUrl.startsWith("data:")) {
// If the imageUrl already is a data-url, save ourselves a round-trip
this.setImageContent(imageUrl, {maxDim: maxDim});
return promise.resolve();
} else if (inspectorFront) {
return inspectorFront.getImageDataFromURL(imageUrl, maxDim).then(res => {
res.size.maxDim = maxDim;
return res.data.string().then(str => {
this.setImageContent(str, res.size);
});
}, () => {
this.setBrokenImageContent();
});
}
return promise.resolve();
},
/**
* Fill the tooltip with a message explaining the the image is missing
*/
setBrokenImageContent: function() {
this.setTextContent({
messages: [l10n.strings.GetStringFromName("previewTooltip.image.brokenImage")]
});
},
/**
* Fill the tooltip with an image and add the image dimension at the bottom.
*
* Only use this for absolute URLs that can be queried from the devtools
* client-side. For relative URLs, use |setRelativeImageContent|.
*
* @param {string} imageUrl
* The url to load the image from
* @param {Object} options
@ -585,57 +620,46 @@ Tooltip.prototype = {
* a number here
*/
setImageContent: function(imageUrl, options={}) {
// Main container
let vbox = this.doc.createElement("vbox");
vbox.setAttribute("align", "center");
if (imageUrl) {
// Main container
let vbox = this.doc.createElement("vbox");
vbox.setAttribute("align", "center");
// Display the image
let image = this.doc.createElement("image");
image.setAttribute("src", imageUrl);
if (options.maxDim) {
image.style.maxWidth = options.maxDim + "px";
image.style.maxHeight = options.maxDim + "px";
}
vbox.appendChild(image);
// Dimension label
let label = this.doc.createElement("label");
label.classList.add("devtools-tooltip-caption");
label.classList.add("theme-comment");
if (options.naturalWidth && options.naturalHeight) {
label.textContent = this._getImageDimensionLabel(options.naturalWidth,
options.naturalHeight);
} else {
// If no dimensions were provided, load the image to get them
label.textContent = l10n.strings.GetStringFromName("previewTooltip.image.brokenImage");
let imgObj = new this.doc.defaultView.Image();
imgObj.src = imageUrl;
imgObj.onload = () => {
imgObj.onload = null;
label.textContent = this._getImageDimensionLabel(imgObj.naturalWidth,
imgObj.naturalHeight);
// Display the image
let image = this.doc.createElement("image");
image.setAttribute("src", imageUrl);
if (options.maxDim) {
image.style.maxWidth = options.maxDim + "px";
image.style.maxHeight = options.maxDim + "px";
}
}
vbox.appendChild(label);
vbox.appendChild(image);
this.content = vbox;
// Dimension label
let label = this.doc.createElement("label");
label.classList.add("devtools-tooltip-caption");
label.classList.add("theme-comment");
if (options.naturalWidth && options.naturalHeight) {
label.textContent = this._getImageDimensionLabel(options.naturalWidth,
options.naturalHeight);
} else {
// If no dimensions were provided, load the image to get them
label.textContent = l10n.strings.GetStringFromName("previewTooltip.image.brokenImage");
let imgObj = new this.doc.defaultView.Image();
imgObj.src = imageUrl;
imgObj.onload = () => {
imgObj.onload = null;
label.textContent = this._getImageDimensionLabel(imgObj.naturalWidth,
imgObj.naturalHeight);
}
}
vbox.appendChild(label);
this.content = vbox;
}
},
_getImageDimensionLabel: (w, h) => w + " x " + h,
/**
* Exactly the same as the `image` function but takes a css background image
* value instead : url(....)
*/
setCssBackgroundImageContent: function(cssBackground, sheetHref, maxDim=400) {
let uri = getBackgroundImageUri(cssBackground, sheetHref);
if (uri) {
this.setImageContent(uri, {
maxDim: maxDim
});
}
},
/**
* Fill the tooltip with a new instance of the spectrum color picker widget
* initialized with the given color, and return a promise that resolves to
@ -941,24 +965,6 @@ function isColorOnly(property, value) {
property.match(BORDERCOLOR_RE);
}
/**
* Internal util, returns the background image uri if any
*/
function getBackgroundImageUri(value, sheetHref) {
let uriMatch = BACKGROUND_IMAGE_RE.exec(value);
let uri = null;
if (uriMatch && uriMatch[1]) {
uri = uriMatch[1];
if (sheetHref) {
let sheetUri = IOService.newURI(sheetHref, null, null);
uri = sheetUri.resolve(uri);
}
}
return uri;
}
/**
* L10N utility class
*/

View File

@ -6,28 +6,25 @@
const {Cc, Ci, Cu} = require("chrome");
let ToolDefinitions = require("main").Tools;
let {CssLogic} = require("devtools/styleinspector/css-logic");
let {ELEMENT_STYLE} = require("devtools/server/actors/styles");
let promise = require("sdk/core/promise");
let {EventEmitter} = require("devtools/shared/event-emitter");
const ToolDefinitions = require("main").Tools;
const {CssLogic} = require("devtools/styleinspector/css-logic");
const {ELEMENT_STYLE} = require("devtools/server/actors/styles");
const promise = require("sdk/core/promise");
const {EventEmitter} = require("devtools/shared/event-emitter");
const {OutputParser} = require("devtools/output-parser");
const {Tooltip} = require("devtools/shared/widgets/Tooltip");
const {PrefObserver, PREF_ORIG_SOURCES} = require("devtools/styleeditor/utils");
const {gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/PluralForm.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/devtools/Templater.jsm");
let {gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
const FILTER_CHANGED_TIMEOUT = 300;
const HTML_NS = "http://www.w3.org/1999/xhtml";
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
/**
* Helper for long-running processes that should yield occasionally to
* the mainloop.
@ -183,7 +180,7 @@ function CssHtmlTree(aStyleInspector, aPageStyle)
// Properties preview tooltip
this.tooltip = new Tooltip(this.styleInspector.inspector.panelDoc);
this.tooltip.startTogglingOnHover(this.propertyContainer,
this._buildTooltipContent.bind(this));
this._onTooltipTargetHover.bind(this));
this._buildContextMenu();
this.createStyleViews();
@ -514,32 +511,39 @@ CssHtmlTree.prototype = {
},
/**
* Verify that target is indeed a css value we want a tooltip on, and if yes
* prepare some content for the tooltip
* Executed by the tooltip when the pointer hovers over an element of the view.
* Used to decide whether the tooltip should be shown or not and to actually
* put content in it.
* Checks if the hovered target is a css value we support tooltips for.
*/
_buildTooltipContent: function(target)
_onTooltipTargetHover: function(target)
{
let inspector = this.styleInspector.inspector;
// Test for image url
if (target.classList.contains("theme-link")) {
if (target.classList.contains("theme-link") && inspector.hasUrlToImageDataResolver) {
let propValue = target.parentNode;
let propName = propValue.parentNode.querySelector(".property-name");
if (propName.textContent === "background-image") {
this.tooltip.setCssBackgroundImageContent(propValue.textContent);
return true;
let maxDim = Services.prefs.getIntPref("devtools.inspector.imagePreviewTooltipSize");
let uri = CssLogic.getBackgroundImageUriFromProperty(propValue.textContent);
return this.tooltip.setRelativeImageContent(uri, inspector.inspector, maxDim);
}
}
// Test for css transform
if (target.classList.contains("property-value")) {
let def = promise.defer();
let propValue = target;
let propName = target.parentNode.querySelector(".property-name");
if (propName.textContent === "transform") {
this.tooltip.setCssTransformContent(propValue.textContent,
this.pageStyle, this.viewedElement).then(def.resolve);
return def.promise;
return this.tooltip.setCssTransformContent(propValue.textContent,
this.pageStyle, this.viewedElement);
}
}
// If the target isn't one that should receive a tooltip, signal it by rejecting
// a promise
return promise.reject();
},
/**

View File

@ -8,13 +8,13 @@
const {Cc, Ci, Cu} = require("chrome");
const promise = require("sdk/core/promise");
const {CssLogic} = require("devtools/styleinspector/css-logic");
const {InplaceEditor, editableField, editableItem} = require("devtools/shared/inplace-editor");
const {ELEMENT_STYLE, PSEUDO_ELEMENTS} = require("devtools/server/actors/styles");
const {gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
const {Tooltip, SwatchColorPickerTooltip} = require("devtools/shared/widgets/Tooltip");
const {OutputParser} = require("devtools/output-parser");
const { PrefObserver, PREF_ORIG_SOURCES } = require("devtools/styleeditor/utils");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
@ -22,8 +22,6 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm");
const HTML_NS = "http://www.w3.org/1999/xhtml";
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
const { PrefObserver, PREF_ORIG_SOURCES } = require("devtools/styleeditor/utils");
/**
* These regular expressions are adapted from firebug's css.js, and are
* used to parse CSSStyleDeclaration's cssText attribute.
@ -116,8 +114,7 @@ function createDummyDocument() {
*
* @constructor
*/
function ElementStyle(aElement, aStore, aPageStyle)
{
function ElementStyle(aElement, aStore, aPageStyle) {
this.element = aElement;
this.store = aStore || {};
this.pageStyle = aPageStyle;
@ -142,11 +139,11 @@ function ElementStyle(aElement, aStore, aPageStyle)
return this.dummyElement;
}).then(null, promiseWarn);
}
// We're exporting _ElementStyle for unit tests.
exports._ElementStyle = ElementStyle;
ElementStyle.prototype = {
// The element we're looking at.
element: null,
@ -154,8 +151,7 @@ ElementStyle.prototype = {
// to figure out how shorthand properties will be parsed.
dummyElement: null,
destroy: function()
{
destroy: function() {
this.dummyElement = null;
this.dummyElementPromise.then(dummyElement => {
if (dummyElement.parentNode) {
@ -169,8 +165,7 @@ ElementStyle.prototype = {
* Called by the Rule object when it has been changed through the
* setProperty* methods.
*/
_changed: function ElementStyle_changed()
{
_changed: function() {
if (this.onChanged) {
this.onChanged();
}
@ -183,8 +178,7 @@ ElementStyle.prototype = {
* Returns a promise that will be resolved when the elementStyle is
* ready.
*/
populate: function ElementStyle_populate()
{
populate: function() {
let populated = this.pageStyle.getApplied(this.element, {
inherited: true,
matchedSelectors: true
@ -224,8 +218,7 @@ ElementStyle.prototype = {
/**
* Put pseudo elements in front of others.
*/
_sortRulesForPseudoElement: function ElementStyle_sortRulesForPseudoElement()
{
_sortRulesForPseudoElement: function() {
this.rules = this.rules.sort((a, b) => {
return (a.pseudoElement || "z") > (b.pseudoElement || "z");
});
@ -240,8 +233,7 @@ ElementStyle.prototype = {
*
* @return {bool} true if we added the rule.
*/
_maybeAddRule: function ElementStyle_maybeAddRule(aOptions)
{
_maybeAddRule: function(aOptions) {
// If we've already included this domRule (for example, when a
// common selector is inherited), ignore it.
if (aOptions.rule &&
@ -284,8 +276,7 @@ ElementStyle.prototype = {
/**
* Calls markOverridden with all supported pseudo elements
*/
markOverriddenAll: function ElementStyle_markOverriddenAll()
{
markOverriddenAll: function() {
this.markOverridden();
for (let pseudo of PSEUDO_ELEMENTS) {
this.markOverridden(pseudo);
@ -299,8 +290,7 @@ ElementStyle.prototype = {
* Which pseudo element to flag as overridden.
* Empty string or undefined will default to no pseudo element.
*/
markOverridden: function ElementStyle_markOverridden(pseudo="")
{
markOverridden: function(pseudo="") {
// Gather all the text properties applied by these rules, ordered
// from more- to less-specific.
let textProps = [];
@ -380,8 +370,7 @@ ElementStyle.prototype = {
* @return {bool} true if the TextProperty's overridden state (or any of its
* computed properties overridden state) changed.
*/
_updatePropertyOverridden: function ElementStyle_updatePropertyOverridden(aProp)
{
_updatePropertyOverridden: function(aProp) {
let overridden = true;
let dirty = false;
for each (let computedProp in aProp.computed) {
@ -410,8 +399,7 @@ ElementStyle.prototype = {
* the rule applies directly to the current element.
* @constructor
*/
function Rule(aElementStyle, aOptions)
{
function Rule(aElementStyle, aOptions) {
this.elementStyle = aElementStyle;
this.domRule = aOptions.rule || null;
this.style = aOptions.rule;
@ -437,8 +425,7 @@ function Rule(aElementStyle, aOptions)
Rule.prototype = {
mediaText: "",
get title()
{
get title() {
if (this._title) {
return this._title;
}
@ -451,8 +438,7 @@ Rule.prototype = {
return this._title;
},
get inheritedSource()
{
get inheritedSource() {
if (this._inheritedSource) {
return this._inheritedSource;
}
@ -468,32 +454,28 @@ Rule.prototype = {
return this._inheritedSource;
},
get selectorText()
{
get selectorText() {
return this.domRule.selectors ? this.domRule.selectors.join(", ") : CssLogic.l10n("rule.sourceElement");
},
/**
* The rule's stylesheet.
*/
get sheet()
{
get sheet() {
return this.domRule ? this.domRule.parentStyleSheet : null;
},
/**
* The rule's line within a stylesheet
*/
get ruleLine()
{
get ruleLine() {
return this.domRule ? this.domRule.line : null;
},
/**
* The rule's column within a stylesheet
*/
get ruleColumn()
{
get ruleColumn() {
return this.domRule ? this.domRule.column : null;
},
@ -504,8 +486,7 @@ Rule.prototype = {
* @return {Promise}
* Promise which resolves with location as a string.
*/
getOriginalSourceString: function Rule_getOriginalSourceString()
{
getOriginalSourceString: function() {
if (this._originalSourceString) {
return promise.resolve(this._originalSourceString);
}
@ -523,8 +504,7 @@ Rule.prototype = {
* @param {object} aOptions
* Creation options. See the Rule constructor for documentation.
*/
matches: function Rule_matches(aOptions)
{
matches: function(aOptions) {
return this.style === aOptions.rule;
},
@ -540,8 +520,7 @@ Rule.prototype = {
* @param {TextProperty} aSiblingProp
* Optional, property next to which the new property will be added.
*/
createProperty: function Rule_createProperty(aName, aValue, aPriority, aSiblingProp)
{
createProperty: function(aName, aValue, aPriority, aSiblingProp) {
let prop = new TextProperty(this, aName, aValue, aPriority);
if (aSiblingProp) {
@ -566,8 +545,7 @@ Rule.prototype = {
* when calling from setPropertyValue & setPropertyName to signify
* that the property should be saved in store.userProperties.
*/
applyProperties: function Rule_applyProperties(aModifications, aName)
{
applyProperties: function(aModifications, aName) {
this.elementStyle.markOverriddenAll();
if (!aModifications) {
@ -652,8 +630,7 @@ Rule.prototype = {
* @param {string} aName
* The new property name (such as "background" or "border-top").
*/
setPropertyName: function Rule_setPropertyName(aProperty, aName)
{
setPropertyName: function(aProperty, aName) {
if (aName === aProperty.name) {
return;
}
@ -673,8 +650,7 @@ Rule.prototype = {
* @param {string} aPriority
* The property's priority (either "important" or an empty string).
*/
setPropertyValue: function Rule_setPropertyValue(aProperty, aValue, aPriority)
{
setPropertyValue: function(aProperty, aValue, aPriority) {
if (aValue === aProperty.value && aPriority === aProperty.priority) {
return;
}
@ -687,8 +663,7 @@ Rule.prototype = {
/**
* Disables or enables given TextProperty.
*/
setPropertyEnabled: function Rule_enableProperty(aProperty, aValue)
{
setPropertyEnabled: function(aProperty, aValue) {
aProperty.enabled = !!aValue;
let modifications = this.style.startModifyingProperties();
if (!aProperty.enabled) {
@ -701,8 +676,7 @@ Rule.prototype = {
* Remove a given TextProperty from the rule and update the rule
* accordingly.
*/
removeProperty: function Rule_removeProperty(aProperty)
{
removeProperty: function(aProperty) {
this.textProps = this.textProps.filter(function(prop) prop != aProperty);
let modifications = this.style.startModifyingProperties();
modifications.removeProperty(aProperty.name);
@ -715,8 +689,7 @@ Rule.prototype = {
* Get the list of TextProperties from the style. Needs
* to parse the style's cssText.
*/
_getTextProperties: function Rule_getTextProperties()
{
_getTextProperties: function() {
let textProps = [];
let store = this.elementStyle.store;
let props = parseCSSText(this.style.cssText);
@ -736,8 +709,7 @@ Rule.prototype = {
/**
* Return the list of disabled properties from the store for this rule.
*/
_getDisabledProperties: function Rule_getDisabledProperties()
{
_getDisabledProperties: function() {
let store = this.elementStyle.store;
// Include properties from the disabled property store, if any.
@ -762,8 +734,7 @@ Rule.prototype = {
* Reread the current state of the rules and rebuild text
* properties as needed.
*/
refresh: function Rule_refresh(aOptions)
{
refresh: function(aOptions) {
this.matchedSelectors = aOptions.matchedSelectors || [];
let newTextProps = this._getTextProperties();
@ -825,7 +796,7 @@ Rule.prototype = {
* @return {bool} true if a property was updated, false if no properties
* were updated.
*/
_updateTextProperty: function Rule__updateTextProperty(aNewProp) {
_updateTextProperty: function(aNewProp) {
let match = { rank: 0, prop: null };
for each (let prop in this.textProps) {
@ -887,8 +858,7 @@ Rule.prototype = {
* The text property that will be left to focus on a sibling.
*
*/
editClosestTextProperty: function Rule__editClosestTextProperty(aTextProperty)
{
editClosestTextProperty: function(aTextProperty) {
let index = this.textProps.indexOf(aTextProperty);
let previous = false;
@ -930,8 +900,7 @@ Rule.prototype = {
* The property's priority (either "important" or an empty string).
*
*/
function TextProperty(aRule, aName, aValue, aPriority)
{
function TextProperty(aRule, aName, aValue, aPriority) {
this.rule = aRule;
this.name = aName;
this.value = aValue;
@ -945,8 +914,7 @@ TextProperty.prototype = {
* Update the editor associated with this text property,
* if any.
*/
updateEditor: function TextProperty_updateEditor()
{
updateEditor: function() {
if (this.editor) {
this.editor.update();
}
@ -955,8 +923,7 @@ TextProperty.prototype = {
/**
* Update the list of computed properties for this text property.
*/
updateComputed: function TextProperty_updateComputed()
{
updateComputed: function() {
if (!this.name) {
return;
}
@ -988,8 +955,7 @@ TextProperty.prototype = {
* @param {TextProperty} aOther
* The other TextProperty instance.
*/
set: function TextProperty_set(aOther)
{
set: function(aOther) {
let changed = false;
for (let item of ["name", "value", "priority", "enabled"]) {
if (this[item] != aOther[item]) {
@ -1003,28 +969,24 @@ TextProperty.prototype = {
}
},
setValue: function TextProperty_setValue(aValue, aPriority)
{
setValue: function(aValue, aPriority) {
this.rule.setPropertyValue(this, aValue, aPriority);
this.updateEditor();
},
setName: function TextProperty_setName(aName)
{
setName: function(aName) {
this.rule.setPropertyName(this, aName);
this.updateEditor();
},
setEnabled: function TextProperty_setEnabled(aValue)
{
setEnabled: function(aValue) {
this.rule.setPropertyEnabled(this, aValue);
this.updateEditor();
},
remove: function TextProperty_remove()
{
remove: function() {
this.rule.removeProperty(this);
},
}
};
@ -1061,8 +1023,7 @@ TextProperty.prototype = {
* The PageStyleFront for communicating with the remote server.
* @constructor
*/
function CssRuleView(aInspector, aDoc, aStore, aPageStyle)
{
function CssRuleView(aInspector, aDoc, aStore, aPageStyle) {
this.inspector = aInspector;
this.doc = aDoc;
this.store = aStore || {};
@ -1097,7 +1058,7 @@ function CssRuleView(aInspector, aDoc, aStore, aPageStyle)
// Create a tooltip for previewing things in the rule view (images for now)
this.previewTooltip = new Tooltip(this.inspector.panelDoc);
this.previewTooltip.startTogglingOnHover(this.element,
this._buildTooltipContent.bind(this));
this._onTooltipTargetHover.bind(this));
// Also create a more complex tooltip for editing colors with the spectrum
// color picker
@ -1149,10 +1110,12 @@ CssRuleView.prototype = {
},
/**
* Verify that target is indeed a css value we want a tooltip on, and if yes
* prepare some content for the tooltip
* Executed by the tooltip when the pointer hovers over an element of the view.
* Used to decide whether the tooltip should be shown or not and to actually
* put content in it.
* Checks if the hovered target is a css value we support tooltips for.
*/
_buildTooltipContent: function(target) {
_onTooltipTargetHover: function(target) {
let property = target.textProperty, def = promise.defer(), hasTooltip = false;
// Test for css transform
@ -1163,19 +1126,26 @@ CssRuleView.prototype = {
}
// Test for image
let isImageHref = target.classList.contains("theme-link") &&
target.parentNode.classList.contains("ruleview-propertyvalue");
if (isImageHref) {
property = target.parentNode.textProperty;
this.previewTooltip.setCssBackgroundImageContent(property.value,
property.rule.domRule.href);
def.resolve();
hasTooltip = true;
if (this.inspector.hasUrlToImageDataResolver) {
let isImageHref = target.classList.contains("theme-link") &&
target.parentNode.classList.contains("ruleview-propertyvalue");
if (isImageHref) {
property = target.parentNode.textProperty;
let maxDim = Services.prefs.getIntPref("devtools.inspector.imagePreviewTooltipSize");
let uri = CssLogic.getBackgroundImageUriFromProperty(property.value,
property.rule.domRule.href);
this.previewTooltip.setRelativeImageContent(uri,
this.inspector.inspector, maxDim).then(def.resolve);
hasTooltip = true;
}
}
if (hasTooltip) {
this.colorPicker.revert();
this.colorPicker.hide();
} else {
def.reject();
}
return def.promise;
@ -1225,8 +1195,7 @@ CssRuleView.prototype = {
/**
* Select all text.
*/
_onSelectAll: function()
{
_onSelectAll: function() {
let win = this.doc.defaultView;
let selection = win.getSelection();
@ -1239,8 +1208,7 @@ CssRuleView.prototype = {
* @param {Event} event
* The event object.
*/
_onCopy: function(event)
{
_onCopy: function(event) {
try {
let target = event.target;
let text;
@ -1279,8 +1247,7 @@ CssRuleView.prototype = {
/**
* Toggle the original sources pref.
*/
_onToggleOrigSources: function()
{
_onToggleOrigSources: function() {
let isEnabled = Services.prefs.getBoolPref(PREF_ORIG_SOURCES);
Services.prefs.setBoolPref(PREF_ORIG_SOURCES, !isEnabled);
},
@ -1305,8 +1272,7 @@ CssRuleView.prototype = {
}
},
_onSourcePrefChanged: function()
{
_onSourcePrefChanged: function() {
if (this.menuitemSources) {
let isEnabled = Services.prefs.getBoolPref(PREF_ORIG_SOURCES);
this.menuitemSources.setAttribute("checked", isEnabled);
@ -1320,8 +1286,7 @@ CssRuleView.prototype = {
}
},
destroy: function CssRuleView_destroy()
{
destroy: function() {
this.clear();
gDummyPromise = null;
@ -1378,8 +1343,7 @@ CssRuleView.prototype = {
* @param {NodeActor} aElement
* The node whose style rules we'll inspect.
*/
highlight: function CssRuleView_highlight(aElement)
{
highlight: function(aElement) {
if (this._viewedElement === aElement) {
return promise.resolve(undefined);
}
@ -1407,8 +1371,7 @@ CssRuleView.prototype = {
/**
* Update the rules for the currently highlighted element.
*/
nodeChanged: function CssRuleView_nodeChanged()
{
nodeChanged: function() {
// Ignore refreshes during editing or when no element is selected.
if (this.isEditing || !this._elementStyle) {
return;
@ -1439,8 +1402,7 @@ CssRuleView.prototype = {
/**
* Show the user that the rule view has no node selected.
*/
_showEmpty: function CssRuleView_showEmpty()
{
_showEmpty: function() {
if (this.doc.getElementById("noResults") > 0) {
return;
}
@ -1454,8 +1416,7 @@ CssRuleView.prototype = {
/**
* Clear the rules.
*/
_clearRules: function CssRuleView_clearRules()
{
_clearRules: function() {
while (this.element.hasChildNodes()) {
this.element.removeChild(this.element.lastChild);
}
@ -1464,8 +1425,7 @@ CssRuleView.prototype = {
/**
* Clear the rule view.
*/
clear: function CssRuleView_clear()
{
clear: function() {
this._clearRules();
this._viewedElement = null;
this._elementStyle = null;
@ -1478,8 +1438,7 @@ CssRuleView.prototype = {
* Called when the user has made changes to the ElementStyle.
* Emits an event that clients can listen to.
*/
_changed: function CssRuleView_changed()
{
_changed: function() {
var evt = this.doc.createEvent("Events");
evt.initEvent("CssRuleViewChanged", true, false);
this.element.dispatchEvent(evt);
@ -1488,8 +1447,7 @@ CssRuleView.prototype = {
/**
* Text for header that shows above rules for this element
*/
get selectedElementLabel ()
{
get selectedElementLabel() {
if (this._selectedElementLabel) {
return this._selectedElementLabel;
}
@ -1500,8 +1458,7 @@ CssRuleView.prototype = {
/**
* Text for header that shows above rules for pseudo elements
*/
get pseudoElementLabel ()
{
get pseudoElementLabel() {
if (this._pseudoElementLabel) {
return this._pseudoElementLabel;
}
@ -1509,8 +1466,7 @@ CssRuleView.prototype = {
return this._pseudoElementLabel;
},
togglePseudoElementVisibility: function(value)
{
togglePseudoElementVisibility: function(value) {
this._showPseudoElements = !!value;
let isOpen = this.showPseudoElements;
@ -1529,8 +1485,7 @@ CssRuleView.prototype = {
}
},
get showPseudoElements ()
{
get showPseudoElements() {
if (this._showPseudoElements === undefined) {
this._showPseudoElements =
Services.prefs.getBoolPref("devtools.inspector.show_pseudo_elements");
@ -1546,8 +1501,7 @@ CssRuleView.prototype = {
/**
* Creates editor UI for each of the rules in _elementStyle.
*/
_createEditors: function CssRuleView_createEditors()
{
_createEditors: function() {
// Run through the current list of rules, attaching
// their editors in order. Create editors if needed.
let lastInheritedSource = "";
@ -1606,8 +1560,7 @@ CssRuleView.prototype = {
}
this.togglePseudoElementVisibility(this.showPseudoElements);
},
}
};
/**
@ -1619,8 +1572,7 @@ CssRuleView.prototype = {
* The Rule object we're editing.
* @constructor
*/
function RuleEditor(aRuleView, aRule)
{
function RuleEditor(aRuleView, aRule) {
this.ruleView = aRuleView;
this.doc = this.ruleView.doc;
this.rule = aRule;
@ -1632,8 +1584,7 @@ function RuleEditor(aRuleView, aRule)
}
RuleEditor.prototype = {
_create: function RuleEditor_create()
{
_create: function() {
this.element = this.doc.createElementNS(HTML_NS, "div");
this.element.className = "ruleview-rule theme-separator";
this.element._ruleEditor = this;
@ -1742,8 +1693,7 @@ RuleEditor.prototype = {
/**
* Update the rule editor with the contents of the rule.
*/
populate: function RuleEditor_populate()
{
populate: function() {
// Clear out existing viewers.
while (this.selectorText.hasChildNodes()) {
this.selectorText.removeChild(this.selectorText.lastChild);
@ -1797,8 +1747,7 @@ RuleEditor.prototype = {
* @return {TextProperty}
* The new property
*/
addProperty: function RuleEditor_addProperty(aName, aValue, aPriority, aSiblingProp)
{
addProperty: function(aName, aValue, aPriority, aSiblingProp) {
let prop = this.rule.createProperty(aName, aValue, aPriority, aSiblingProp);
let index = this.rule.textProps.indexOf(prop);
let editor = new TextPropertyEditor(this, prop);
@ -1829,8 +1778,7 @@ RuleEditor.prototype = {
* @param {TextProperty} aSiblingProp
* Optional, the property next to which all new props should be added.
*/
addProperties: function RuleEditor_addProperties(aProperties, aSiblingProp)
{
addProperties: function(aProperties, aSiblingProp) {
if (!aProperties || !aProperties.length) {
return;
}
@ -1853,8 +1801,7 @@ RuleEditor.prototype = {
* name is given, we'll create a real TextProperty and add it to the
* rule.
*/
newProperty: function RuleEditor_newProperty()
{
newProperty: function() {
// If we're already creating a new property, ignore this.
if (!this.closeBrace.hasAttribute("tabindex")) {
return;
@ -1898,8 +1845,7 @@ RuleEditor.prototype = {
* @param {bool} aCommit
* True if the value should be committed.
*/
_onNewProperty: function RuleEditor__onNewProperty(aValue, aCommit)
{
_onNewProperty: function(aValue, aCommit) {
if (!aValue || !aCommit) {
return;
}
@ -1924,8 +1870,7 @@ RuleEditor.prototype = {
* added, since we want to wait until after the inplace editor `destroy`
* event has been fired to keep consistent UI state.
*/
_newPropertyDestroy: function RuleEditor__newPropertyDestroy()
{
_newPropertyDestroy: function() {
// We're done, make the close brace focusable again.
this.closeBrace.setAttribute("tabindex", "0");
@ -1951,8 +1896,7 @@ RuleEditor.prototype = {
* The text property to edit.
* @constructor
*/
function TextPropertyEditor(aRuleEditor, aProperty)
{
function TextPropertyEditor(aRuleEditor, aProperty) {
this.ruleEditor = aRuleEditor;
this.doc = this.ruleEditor.doc;
this.popup = this.ruleEditor.ruleView.popup;
@ -1991,8 +1935,7 @@ TextPropertyEditor.prototype = {
/**
* Create the property editor's DOM.
*/
_create: function TextPropertyEditor_create()
{
_create: function() {
this.element = this.doc.createElementNS(HTML_NS, "li");
this.element.classList.add("ruleview-property");
@ -2119,8 +2062,7 @@ TextPropertyEditor.prototype = {
* @param {string} relativePath the path to resolve
* @return {string} the resolved path.
*/
resolveURI: function(relativePath)
{
resolveURI: function(relativePath) {
if (this.sheetURI) {
relativePath = this.sheetURI.resolve(relativePath);
}
@ -2131,8 +2073,7 @@ TextPropertyEditor.prototype = {
* Check the property value to find an external resource (if any).
* @return {string} the URI in the property value, or null if there is no match.
*/
getResourceURI: function()
{
getResourceURI: function() {
let val = this.prop.value;
let uriMatch = CSS_RESOURCE_RE.exec(val);
let uri = null;
@ -2147,8 +2088,7 @@ TextPropertyEditor.prototype = {
/**
* Populate the span based on changes to the TextProperty.
*/
update: function TextPropertyEditor_update()
{
update: function() {
if (this.prop.enabled) {
this.enable.style.removeProperty("visibility");
this.enable.setAttribute("checked", "");
@ -2216,8 +2156,7 @@ TextPropertyEditor.prototype = {
this._updateComputed();
},
_onStartEditing: function TextPropertyEditor_onStartEditing()
{
_onStartEditing: function() {
this.element.classList.remove("ruleview-overridden");
this._livePreview(this.prop.value);
},
@ -2225,8 +2164,7 @@ TextPropertyEditor.prototype = {
/**
* Populate the list of computed styles.
*/
_updateComputed: function TextPropertyEditor_updateComputed()
{
_updateComputed: function () {
// Clear out existing viewers.
while (this.computed.hasChildNodes()) {
this.computed.removeChild(this.computed.lastChild);
@ -2284,8 +2222,7 @@ TextPropertyEditor.prototype = {
/**
* Handles clicks on the disabled property.
*/
_onEnableClicked: function TextPropertyEditor_onEnableClicked(aEvent)
{
_onEnableClicked: function(aEvent) {
let checked = this.enable.hasAttribute("checked");
if (checked) {
this.enable.removeAttribute("checked");
@ -2299,8 +2236,7 @@ TextPropertyEditor.prototype = {
/**
* Handles clicks on the computed property expander.
*/
_onExpandClicked: function TextPropertyEditor_onExpandClicked(aEvent)
{
_onExpandClicked: function(aEvent) {
this.computed.classList.toggle("styleinspector-open");
if (this.computed.classList.contains("styleinspector-open")) {
this.expander.setAttribute("open", "true");
@ -2320,8 +2256,7 @@ TextPropertyEditor.prototype = {
* @param {boolean} aCommit
* True if the change should be applied.
*/
_onNameDone: function TextPropertyEditor_onNameDone(aValue, aCommit)
{
_onNameDone: function(aValue, aCommit) {
if (aCommit) {
// Unlike the value editor, if a name is empty the entire property
// should always be removed.
@ -2349,8 +2284,7 @@ TextPropertyEditor.prototype = {
* Remove property from style and the editors from DOM.
* Begin editing next available property.
*/
remove: function TextPropertyEditor_remove()
{
remove: function() {
if (this._swatchSpans && this._swatchSpans.length) {
for (let span of this._swatchSpans) {
this.ruleEditor.ruleView.colorPicker.removeSwatch(span);
@ -2372,8 +2306,7 @@ TextPropertyEditor.prototype = {
* @param {bool} aCommit
* True if the change should be applied.
*/
_onValueDone: function PropertyEditor_onValueDone(aValue, aCommit)
{
_onValueDone: function(aValue, aCommit) {
if (!aCommit) {
// A new property should be removed when escape is pressed.
if (this.removeOnRevert) {
@ -2425,8 +2358,7 @@ TextPropertyEditor.prototype = {
* propertiesToAdd: An array with additional properties, following the
* parseCSSText format of {name,value,priority}
*/
_getValueAndExtraProperties: function PropetyEditor_getValueAndExtraProperties(aValue) {
_getValueAndExtraProperties: function(aValue) {
// The inplace editor will prevent manual typing of multiple properties,
// but we need to deal with the case during a paste event.
// Adding multiple properties inside of value editor sets value with the
@ -2462,8 +2394,7 @@ TextPropertyEditor.prototype = {
};
},
_applyNewValue: function PropetyEditor_applyNewValue(aValue)
{
_applyNewValue: function(aValue) {
let val = parseCSSValue(aValue);
// Any property should be removed if has an empty value.
if (val.value.trim() === "") {
@ -2482,8 +2413,7 @@ TextPropertyEditor.prototype = {
* @param {string} [aValue]
* The value to set the current property to.
*/
_livePreview: function TextPropertyEditor_livePreview(aValue)
{
_livePreview: function(aValue) {
// Since function call is throttled, we need to make sure we are still editing
if (!this.editing) {
return;
@ -2506,8 +2436,7 @@ TextPropertyEditor.prototype = {
*
* @return {bool} true if the property value is valid, false otherwise.
*/
isValid: function TextPropertyEditor_isValid(aValue)
{
isValid: function(aValue) {
let name = this.prop.name;
let value = typeof aValue == "undefined" ? this.prop.value : aValue;
let val = parseCSSValue(value);
@ -2534,8 +2463,7 @@ TextPropertyEditor.prototype = {
* Store of CSSStyleDeclarations mapped to properties that have been changed by
* the user.
*/
function UserProperties()
{
function UserProperties() {
this.map = new Map();
}
@ -2607,7 +2535,7 @@ UserProperties.prototype = {
getKey: function(aStyle) {
return aStyle.href + ":" + aStyle.line;
},
}
};
/**
@ -2624,8 +2552,7 @@ UserProperties.prototype = {
* @param {object} aAttributes
* A set of attributes to set on the node.
*/
function createChild(aParent, aTag, aAttributes)
{
function createChild(aParent, aTag, aAttributes) {
let elt = aParent.ownerDocument.createElementNS(HTML_NS, aTag);
for (let attr in aAttributes) {
if (aAttributes.hasOwnProperty(attr)) {
@ -2642,8 +2569,7 @@ function createChild(aParent, aTag, aAttributes)
return elt;
}
function createMenuItem(aMenu, aAttributes)
{
function createMenuItem(aMenu, aAttributes) {
let item = aMenu.ownerDocument.createElementNS(XUL_NS, "menuitem");
item.setAttribute("label", _strings.GetStringFromName(aAttributes.label));
@ -2655,20 +2581,17 @@ function createMenuItem(aMenu, aAttributes)
return item;
}
function setTimeout()
{
function setTimeout() {
let window = Services.appShell.hiddenDOMWindow;
return window.setTimeout.apply(window, arguments);
}
function clearTimeout()
{
function clearTimeout() {
let window = Services.appShell.hiddenDOMWindow;
return window.clearTimeout.apply(window, arguments);
}
function throttle(func, wait, scope)
{
function throttle(func, wait, scope) {
var timer = null;
return function() {
if(timer) {
@ -2690,8 +2613,7 @@ function throttle(func, wait, scope)
* The value from the text editor.
* @return {object} an object with 'value' and 'priority' properties.
*/
function parseCSSValue(aValue)
{
function parseCSSValue(aValue) {
let pieces = aValue.split("!", 2);
return {
value: pieces[0].trim(),
@ -2709,8 +2631,7 @@ function parseCSSValue(aValue)
* @return {Array} an array of objects with the following signature:
* [{"name": string, "value": string, "priority": string}, ...]
*/
function parseCSSText(aCssText)
{
function parseCSSText(aCssText) {
let lines = aCssText.match(CSS_LINE_RE);
let props = [];
@ -2739,8 +2660,7 @@ function parseCSSText(aCssText)
* Event handler that causes a blur on the target if the input has
* multiple CSS properties as the value.
*/
function blurOnMultipleProperties(e)
{
function blurOnMultipleProperties(e) {
setTimeout(() => {
if (parseCSSText(e.target.value).length) {
e.target.blur();
@ -2751,8 +2671,7 @@ function blurOnMultipleProperties(e)
/**
* Append a text node to an element.
*/
function appendText(aParent, aText)
{
function appendText(aParent, aText) {
aParent.appendChild(aParent.ownerDocument.createTextNode(aText));
}

View File

@ -104,7 +104,7 @@ function testDivRuleView() {
assertTooltipShownOn(ruleView.previewTooltip, uriSpan, () => {
let images = panel.getElementsByTagName("image");
is(images.length, 1, "Tooltip contains an image");
ok(images[0].src === "chrome://global/skin/icons/warning-64.png");
ok(images[0].src.startsWith("data:"), "Tooltip contains a data-uri image as expected");
ruleView.previewTooltip.hide();
@ -147,7 +147,7 @@ function testComputedView() {
assertTooltipShownOn(computedView.tooltip, uriSpan, () => {
let images = panel.getElementsByTagName("image");
is(images.length, 1, "Tooltip contains an image");
ok(images[0].src === "chrome://global/skin/icons/warning-64.png");
ok(images[0].src.startsWith("data:"), "Tooltip contains a data-uri in the computed-view too");
computedView.tooltip.hide();

View File

@ -16,12 +16,28 @@ const URL_CLASS = "url-class";
function test() {
waitForExplicitFinish();
function countAll(fragment) {
return fragment.querySelectorAll("*").length;
}
function countColors(fragment) {
return fragment.querySelectorAll("." + COLOR_CLASS).length;
}
function countUrls(fragment) {
return fragment.querySelectorAll("." + URL_CLASS).length;
}
function getColor(fragment, index) {
return fragment.querySelectorAll("." + COLOR_CLASS)[index||0].textContent;
}
function getUrl(fragment, index) {
return fragment.querySelectorAll("." + URL_CLASS)[index||0].textContent;
}
let testData = [
{
name: "width",
value: "100%",
test: fragment => {
is(fragment.querySelectorAll("*").length, 0);
is(countAll(fragment), 0);
is(fragment.textContent, "100%");
}
},
@ -29,36 +45,36 @@ function test() {
name: "width",
value: "blue",
test: fragment => {
is(fragment.querySelectorAll("*").length, 0);
is(countAll(fragment), 0);
}
},
{
name: "content",
value: "'red url(test.png) repeat top left'",
test: fragment => {
is(fragment.querySelectorAll("*").length, 0);
is(countAll(fragment), 0);
}
},
{
name: "content",
value: "\"blue\"",
test: fragment => {
is(fragment.querySelectorAll("*").length, 0);
is(countAll(fragment), 0);
}
},
{
name: "margin-left",
value: "url(something.jpg)",
test: fragment => {
is(fragment.querySelectorAll("*").length, 0);
is(countAll(fragment), 0);
}
},
{
name: "background-color",
value: "transparent",
test: fragment => {
is(fragment.querySelectorAll("*").length, 1);
is(fragment.querySelectorAll("." + COLOR_CLASS).length, 1);
is(countAll(fragment), 1);
is(countColors(fragment), 1);
is(fragment.textContent, "transparent");
}
},
@ -66,7 +82,7 @@ function test() {
name: "color",
value: "red",
test: fragment => {
is(fragment.querySelectorAll("." + COLOR_CLASS).length, 1);
is(countColors(fragment), 1);
is(fragment.textContent, "red");
}
},
@ -74,7 +90,7 @@ function test() {
name: "color",
value: "#F06",
test: fragment => {
is(fragment.querySelectorAll("." + COLOR_CLASS).length, 1);
is(countColors(fragment), 1);
is(fragment.textContent, "#F06");
}
},
@ -82,8 +98,8 @@ function test() {
name: "border-top-left-color",
value: "rgba(14, 255, 20, .5)",
test: fragment => {
is(fragment.querySelectorAll("*").length, 1);
is(fragment.querySelectorAll("." + COLOR_CLASS).length, 1);
is(countAll(fragment), 1);
is(countColors(fragment), 1);
is(fragment.textContent, "rgba(14, 255, 20, .5)");
}
},
@ -91,16 +107,16 @@ function test() {
name: "border",
value: "80em dotted pink",
test: fragment => {
is(fragment.querySelectorAll("*").length, 1);
is(fragment.querySelectorAll("." + COLOR_CLASS).length, 1);
is(fragment.querySelector("." + COLOR_CLASS).textContent, "pink");
is(countAll(fragment), 1);
is(countColors(fragment), 1);
is(getColor(fragment), "pink");
}
},
{
name: "color",
value: "red !important",
test: fragment => {
is(fragment.querySelectorAll("." + COLOR_CLASS).length, 1);
is(countColors(fragment), 1);
is(fragment.textContent, "red !important");
}
},
@ -108,38 +124,38 @@ function test() {
name: "background",
value: "red url(test.png) repeat top left",
test: fragment => {
is(fragment.querySelectorAll("." + COLOR_CLASS).length, 1);
is(fragment.querySelectorAll("." + URL_CLASS).length, 1);
is(fragment.querySelector("." + COLOR_CLASS).textContent, "red");
is(fragment.querySelector("." + URL_CLASS).textContent, "test.png");
is(fragment.querySelectorAll("*").length, 2);
is(countColors(fragment), 1);
is(countUrls(fragment), 1);
is(getColor(fragment), "red");
is(getUrl(fragment), "test.png");
is(countAll(fragment), 2);
}
},
{
name: "background",
value: "blue url(test.png) repeat top left !important",
test: fragment => {
is(fragment.querySelectorAll("." + COLOR_CLASS).length, 1);
is(fragment.querySelectorAll("." + URL_CLASS).length, 1);
is(fragment.querySelector("." + COLOR_CLASS).textContent, "blue");
is(fragment.querySelector("." + URL_CLASS).textContent, "test.png");
is(fragment.querySelectorAll("*").length, 2);
is(countColors(fragment), 1);
is(countUrls(fragment), 1);
is(getColor(fragment), "blue");
is(getUrl(fragment), "test.png");
is(countAll(fragment), 2);
}
},
{
name: "list-style-image",
value: "url(\"images/arrow.gif\")",
test: fragment => {
is(fragment.querySelectorAll("*").length, 1);
is(fragment.querySelector("." + URL_CLASS).textContent, "images/arrow.gif");
is(countAll(fragment), 1);
is(getUrl(fragment), "images/arrow.gif");
}
},
{
name: "list-style-image",
value: "url(\"images/arrow.gif\")!important",
test: fragment => {
is(fragment.querySelectorAll("*").length, 1);
is(fragment.querySelector("." + URL_CLASS).textContent, "images/arrow.gif");
is(countAll(fragment), 1);
is(getUrl(fragment), "images/arrow.gif");
is(fragment.textContent, "url('images/arrow.gif')!important");
}
},
@ -147,16 +163,16 @@ function test() {
name: "-moz-binding",
value: "url(http://somesite.com/path/to/binding.xml#someid)",
test: fragment => {
is(fragment.querySelectorAll("*").length, 1);
is(fragment.querySelectorAll("." + URL_CLASS).length, 1);
is(fragment.querySelector("." + URL_CLASS).textContent, "http://somesite.com/path/to/binding.xml#someid");
is(countAll(fragment), 1);
is(countUrls(fragment), 1);
is(getUrl(fragment), "http://somesite.com/path/to/binding.xml#someid");
}
},
{
name: "background",
value: "linear-gradient(to right, rgba(183,222,237,1) 0%, rgba(33,180,226,1) 30%, rgba(31,170,217,.5) 44%, #F06 75%, red 100%)",
test: fragment => {
is(fragment.querySelectorAll("*").length, 5);
is(countAll(fragment), 5);
let allSwatches = fragment.querySelectorAll("." + COLOR_CLASS);
is(allSwatches.length, 5);
is(allSwatches[0].textContent, "rgba(183,222,237,1)");
@ -170,12 +186,54 @@ function test() {
name: "background",
value: "-moz-radial-gradient(center 45deg, circle closest-side, orange 0%, red 100%)",
test: fragment => {
is(fragment.querySelectorAll("*").length, 2);
is(countAll(fragment), 2);
let allSwatches = fragment.querySelectorAll("." + COLOR_CLASS);
is(allSwatches.length, 2);
is(allSwatches[0].textContent, "orange");
is(allSwatches[1].textContent, "red");
}
},
{
name: "background",
value: "white url(http://test.com/wow_such_image.png) no-repeat top left",
test: fragment => {
is(countAll(fragment), 2);
is(countUrls(fragment), 1);
is(countColors(fragment), 1);
}
},
{
name: "background",
value: "url(\"http://test.com/wow_such_(oh-noes)image.png?testid=1&color=red#w00t\")",
test: fragment => {
is(countAll(fragment), 1);
is(getUrl(fragment), "http://test.com/wow_such_(oh-noes)image.png?testid=1&color=red#w00t");
}
},
{
name: "background-image",
value: "url(this-is-an-incredible-image.jpeg)",
test: fragment => {
is(countAll(fragment), 1);
is(getUrl(fragment), "this-is-an-incredible-image.jpeg");
}
},
{
name: "background",
value: "red url( \"http://wow.com/cool/../../../you're(doingit)wrong\" ) repeat center",
test: fragment => {
is(countAll(fragment), 2);
is(countColors(fragment), 1);
is(getUrl(fragment), "http://wow.com/cool/../../../you're(doingit)wrong");
}
},
{
name: "background-image",
value: "url(../../../look/at/this/folder/structure/../../red.blue.green.svg )",
test: fragment => {
is(countAll(fragment), 1);
is(getUrl(fragment), "../../../look/at/this/folder/structure/../../red.blue.green.svg");
}
}
];

View File

@ -106,6 +106,8 @@ var BrowserUI = {
window.addEventListener("MozPrecisePointer", this, true);
window.addEventListener("MozImprecisePointer", this, true);
window.addEventListener("AppCommand", this, true);
Services.prefs.addObserver("browser.cache.disk_cache_ssl", this, false);
// Init core UI modules
@ -781,6 +783,9 @@ var BrowserUI = {
case "MozImprecisePointer":
this._onImpreciseInput();
break;
case "AppCommand":
this.handleAppCommandEvent(aEvent);
break;
}
},
@ -1140,6 +1145,48 @@ var BrowserUI = {
}
},
handleAppCommandEvent: function (aEvent) {
switch (aEvent.command) {
case "Back":
this.doCommand("cmd_back");
break;
case "Forward":
this.doCommand("cmd_forward");
break;
case "Reload":
this.doCommand("cmd_reload");
break;
case "Stop":
this.doCommand("cmd_stop");
break;
case "Home":
this.doCommand("cmd_home");
break;
case "New":
this.doCommand("cmd_newTab");
break;
case "Close":
this.doCommand("cmd_closeTab");
break;
case "Find":
FindHelperUI.show();
break;
case "Open":
this.doCommand("cmd_openFile");
break;
case "Save":
this.doCommand("cmd_savePage");
break;
case "Search":
this.doCommand("cmd_openLocation");
break;
default:
return;
}
aEvent.stopPropagation();
aEvent.preventDefault();
},
confirmSanitizeDialog: function () {
let bundle = Services.strings.createBundle("chrome://browser/locale/browser.properties");
let title = bundle.GetStringFromName("clearPrivateData.title2");

View File

@ -323,6 +323,7 @@ this.UITour = {
teardownTour: function(aWindow, aWindowClosing = false) {
aWindow.gBrowser.tabContainer.removeEventListener("TabSelect", this);
aWindow.PanelUI.panel.removeEventListener("popuphiding", this.onAppMenuHiding);
aWindow.removeEventListener("SSWindowClosing", this);
let originTabs = this.originTabs.get(aWindow);
@ -432,7 +433,10 @@ this.UITour = {
}
if (aTargetName == "pinnedTab") {
deferred.resolve({node: this.ensurePinnedTab(aWindow, aSticky)});
deferred.resolve({
targetName: aTargetName,
node: this.ensurePinnedTab(aWindow, aSticky)
});
return deferred.promise;
}
@ -446,6 +450,7 @@ this.UITour = {
aWindow.PanelUI.ensureReady().then(() => {
if (typeof targetQuery == "function") {
deferred.resolve({
targetName: aTargetName,
node: targetQuery(aWindow.document),
widgetName: targetObject.widgetName,
});
@ -453,6 +458,7 @@ this.UITour = {
}
deferred.resolve({
targetName: aTargetName,
node: aWindow.document.querySelector(targetQuery),
widgetName: targetObject.widgetName,
});
@ -573,12 +579,26 @@ this.UITour = {
effect = this.highlightEffects[randomEffect];
}
highlighter.setAttribute("active", effect);
highlighter.parentElement.setAttribute("targetName", aTarget.targetName);
highlighter.parentElement.hidden = false;
let targetRect = aTargetEl.getBoundingClientRect();
let highlightHeight = targetRect.height;
let highlightWidth = targetRect.width;
let minDimension = Math.min(highlightHeight, highlightWidth);
let maxDimension = Math.max(highlightHeight, highlightWidth);
highlighter.style.height = targetRect.height + "px";
highlighter.style.width = targetRect.width + "px";
// If the dimensions are within 40% of eachother, make the highlight a circle with the
// largest dimension as the diameter.
if (maxDimension / minDimension <= 1.4) {
highlightHeight = highlightWidth = maxDimension;
highlighter.style.borderRadius = "100%";
} else {
highlighter.style.borderRadius = "";
}
highlighter.style.height = highlightHeight + "px";
highlighter.style.width = highlightWidth + "px";
// Close a previous highlight so we can relocate the panel.
if (highlighter.parentElement.state == "open") {
@ -591,10 +611,12 @@ this.UITour = {
let paddingTopPx = 0 - parseFloat(containerStyle.paddingTop);
let paddingLeftPx = 0 - parseFloat(containerStyle.paddingLeft);
let highlightStyle = highlightWindow.getComputedStyle(highlighter);
let highlightHeightWithMin = Math.max(highlightHeight, parseFloat(highlightStyle.minHeight));
let highlightWidthWithMin = Math.max(highlightWidth, parseFloat(highlightStyle.minWidth));
let offsetX = paddingTopPx
- (Math.max(0, parseFloat(highlightStyle.minWidth) - targetRect.width) / 2);
- (Math.max(0, highlightWidthWithMin - targetRect.width) / 2);
let offsetY = paddingLeftPx
- (Math.max(0, parseFloat(highlightStyle.minHeight) - targetRect.height) / 2);
- (Math.max(0, highlightHeightWithMin - targetRect.height) / 2);
highlighter.parentElement.openPopup(aTargetEl, "overlap", offsetX, offsetY);
}
@ -665,6 +687,7 @@ this.UITour = {
let tooltipClose = document.getElementById("UITourTooltipClose");
tooltipClose.addEventListener("command", this);
tooltip.setAttribute("targetName", aAnchor.targetName);
tooltip.hidden = false;
let alignment = "bottomcenter topright";
tooltip.openPopup(aAnchorEl, alignment);
@ -709,6 +732,7 @@ this.UITour = {
if (aMenuName == "appMenu") {
aWindow.PanelUI.panel.setAttribute("noautohide", "true");
aWindow.PanelUI.panel.addEventListener("popuphiding", this.onAppMenuHiding);
if (aOpenCallback) {
aWindow.PanelUI.panel.addEventListener("popupshown", onPopupShown);
}
@ -733,6 +757,31 @@ this.UITour = {
}
},
onAppMenuHiding: function(aEvent) {
let win = aEvent.target.ownerDocument.defaultView;
let annotationElements = new Map([
// [annotationElement (panel), method to hide the annotation]
[win.document.getElementById("UITourHighlightContainer"), UITour.hideHighlight.bind(UITour)],
[win.document.getElementById("UITourTooltip"), UITour.hideInfo.bind(UITour)],
]);
annotationElements.forEach((hideMethod, annotationElement) => {
if (annotationElement.state != "closed") {
let targetName = annotationElement.getAttribute("targetName");
UITour.getTarget(win, targetName).then((aTarget) => {
// Since getTarget is async, we need to make sure that the target hasn't
// changed since it may have just moved to somewhere outside of the app menu.
if (annotationElement.getAttribute("targetName") != aTarget.targetName ||
annotationElement.state == "closed" ||
!UITour.targetIsInAppMenu(aTarget)) {
return;
}
hideMethod(win);
}).then(null, Cu.reportError);
}
});
UITour.appMenuOpenForAnnotation.clear();
},
startUrlbarCapture: function(aWindow, aExpectedText, aUrl) {
let urlbar = aWindow.document.getElementById("urlbar");
this.urlbarCapture.set(aWindow, {

View File

@ -10,6 +10,7 @@ support-files =
skip-if = os == "linux" # Intermittent failures, bug 951965
[browser_UITour2.js]
[browser_UITour3.js]
[browser_UITour_panel_close_annotation.js]
[browser_UITour_sync.js]
[browser_taskbar_preview.js]
run-if = os == "win"

View File

@ -9,79 +9,13 @@ let gContentWindow;
Components.utils.import("resource:///modules/UITour.jsm");
function loadTestPage(callback, host = "https://example.com/") {
if (gTestTab)
gBrowser.removeTab(gTestTab);
let url = getRootDirectory(gTestPath) + "uitour.html";
url = url.replace("chrome://mochitests/content/", host);
gTestTab = gBrowser.addTab(url);
gBrowser.selectedTab = gTestTab;
gTestTab.linkedBrowser.addEventListener("load", function onLoad() {
gTestTab.linkedBrowser.removeEventListener("load", onLoad);
gContentWindow = Components.utils.waiveXrays(gTestTab.linkedBrowser.contentDocument.defaultView);
gContentAPI = gContentWindow.Mozilla.UITour;
waitForFocus(callback, gContentWindow);
}, true);
}
function test() {
Services.prefs.setBoolPref("browser.uitour.enabled", true);
let testUri = Services.io.newURI("http://example.com", null, null);
Services.perms.add(testUri, "uitour", Services.perms.ALLOW_ACTION);
waitForExplicitFinish();
registerCleanupFunction(function() {
delete window.UITour;
delete window.gContentWindow;
delete window.gContentAPI;
if (gTestTab)
gBrowser.removeTab(gTestTab);
delete window.gTestTab;
Services.prefs.clearUserPref("browser.uitour.enabled", true);
Services.perms.remove("example.com", "uitour");
});
function done() {
if (gTestTab)
gBrowser.removeTab(gTestTab);
gTestTab = null;
let highlight = document.getElementById("UITourHighlightContainer");
is_element_hidden(highlight, "Highlight should be closed/hidden after UITour tab is closed");
let tooltip = document.getElementById("UITourTooltip");
is_element_hidden(tooltip, "Tooltip should be closed/hidden after UITour tab is closed");
ok(!PanelUI.panel.hasAttribute("noautohide"), "@noautohide on the menu panel should have been cleaned up");
is(UITour.pinnedTabs.get(window), null, "Any pinned tab should be closed after UITour tab is closed");
executeSoon(nextTest);
}
function nextTest() {
if (tests.length == 0) {
finish();
return;
}
let test = tests.shift();
info("Starting " + test.name);
loadTestPage(function() {
test(done);
});
}
nextTest();
UITourTest();
}
let tests = [
function test_untrusted_host(done) {
loadTestPage(function() {
loadUITourTestPage(function() {
let bookmarksMenu = document.getElementById("bookmarks-menu-button");
ise(bookmarksMenu.open, false, "Bookmark menu should initially be closed");
@ -92,7 +26,7 @@ let tests = [
}, "http://mochi.test:8888/");
},
function test_unsecure_host(done) {
loadTestPage(function() {
loadUITourTestPage(function() {
let bookmarksMenu = document.getElementById("bookmarks-menu-button");
ise(bookmarksMenu.open, false, "Bookmark menu should initially be closed");
@ -104,7 +38,7 @@ let tests = [
},
function test_unsecure_host_override(done) {
Services.prefs.setBoolPref("browser.uitour.requireSecure", false);
loadTestPage(function() {
loadUITourTestPage(function() {
let highlight = document.getElementById("UITourHighlight");
is_element_hidden(highlight, "Highlight should initially be hidden");
@ -147,6 +81,27 @@ let tests = [
gContentAPI.showHighlight("urlbar");
waitForElementToBeVisible(highlight, test_highlight_2, "Highlight should be shown after showHighlight()");
},
function test_highlight_circle(done) {
function check_highlight_size() {
let panel = highlight.parentElement;
let anchor = panel.anchorNode;
let anchorRect = anchor.getBoundingClientRect();
info("addons target: width: " + anchorRect.width + " height: " + anchorRect.height);
let maxDimension = Math.round(Math.max(anchorRect.width, anchorRect.height));
let highlightRect = highlight.getBoundingClientRect();
info("highlight: width: " + highlightRect.width + " height: " + highlightRect.height);
is(Math.round(highlightRect.width), maxDimension, "The width of the highlight should be equal to the largest dimension of the target");
is(Math.round(highlightRect.height), maxDimension, "The height of the highlight should be equal to the largest dimension of the target");
is(Math.round(highlightRect.height), Math.round(highlightRect.width), "The height and width of the highlight should be the same to create a circle");
is(highlight.style.borderRadius, "100%", "The border-radius should be 100% to create a circle");
done();
}
let highlight = document.getElementById("UITourHighlight");
is_element_hidden(highlight, "Highlight should initially be hidden");
gContentAPI.showHighlight("addons");
waitForElementToBeVisible(highlight, check_highlight_size, "Highlight should be shown after showHighlight()");
},
function test_highlight_customize_auto_open_close(done) {
let highlight = document.getElementById("UITourHighlight");
gContentAPI.showHighlight("customize");

View File

@ -9,74 +9,8 @@ let gContentWindow;
Components.utils.import("resource:///modules/UITour.jsm");
function loadTestPage(callback, host = "https://example.com/") {
if (gTestTab)
gBrowser.removeTab(gTestTab);
let url = getRootDirectory(gTestPath) + "uitour.html";
url = url.replace("chrome://mochitests/content/", host);
gTestTab = gBrowser.addTab(url);
gBrowser.selectedTab = gTestTab;
gTestTab.linkedBrowser.addEventListener("load", function onLoad() {
gTestTab.linkedBrowser.removeEventListener("load", onLoad);
gContentWindow = Components.utils.waiveXrays(gTestTab.linkedBrowser.contentDocument.defaultView);
gContentAPI = gContentWindow.Mozilla.UITour;
waitForFocus(callback, gContentWindow);
}, true);
}
function test() {
Services.prefs.setBoolPref("browser.uitour.enabled", true);
let testUri = Services.io.newURI("http://example.com", null, null);
Services.perms.add(testUri, "uitour", Services.perms.ALLOW_ACTION);
waitForExplicitFinish();
registerCleanupFunction(function() {
delete window.UITour;
delete window.gContentWindow;
delete window.gContentAPI;
if (gTestTab)
gBrowser.removeTab(gTestTab);
delete window.gTestTab;
Services.prefs.clearUserPref("browser.uitour.enabled", true);
Services.perms.remove("example.com", "uitour");
});
function done() {
if (gTestTab)
gBrowser.removeTab(gTestTab);
gTestTab = null;
let highlight = document.getElementById("UITourHighlightContainer");
is_element_hidden(highlight, "Highlight should be closed/hidden after UITour tab is closed");
let tooltip = document.getElementById("UITourTooltip");
is_element_hidden(tooltip, "Tooltip should be closed/hidden after UITour tab is closed");
ok(!PanelUI.panel.hasAttribute("noautohide"), "@noautohide on the menu panel should have been cleaned up");
is(UITour.pinnedTabs.get(window), null, "Any pinned tab should be closed after UITour tab is closed");
executeSoon(nextTest);
}
function nextTest() {
if (tests.length == 0) {
finish();
return;
}
let test = tests.shift();
info("Starting " + test.name);
loadTestPage(function() {
test(done);
});
}
nextTest();
UITourTest();
}
let tests = [

View File

@ -9,74 +9,8 @@ let gContentWindow;
Components.utils.import("resource:///modules/UITour.jsm");
function loadTestPage(callback, host = "https://example.com/") {
if (gTestTab)
gBrowser.removeTab(gTestTab);
let url = getRootDirectory(gTestPath) + "uitour.html";
url = url.replace("chrome://mochitests/content/", host);
gTestTab = gBrowser.addTab(url);
gBrowser.selectedTab = gTestTab;
gTestTab.linkedBrowser.addEventListener("load", function onLoad() {
gTestTab.linkedBrowser.removeEventListener("load", onLoad);
gContentWindow = Components.utils.waiveXrays(gTestTab.linkedBrowser.contentDocument.defaultView);
gContentAPI = gContentWindow.Mozilla.UITour;
waitForFocus(callback, gContentWindow);
}, true);
}
function test() {
Services.prefs.setBoolPref("browser.uitour.enabled", true);
let testUri = Services.io.newURI("http://example.com", null, null);
Services.perms.add(testUri, "uitour", Services.perms.ALLOW_ACTION);
waitForExplicitFinish();
registerCleanupFunction(function() {
delete window.UITour;
delete window.gContentWindow;
delete window.gContentAPI;
if (gTestTab)
gBrowser.removeTab(gTestTab);
delete window.gTestTab;
Services.prefs.clearUserPref("browser.uitour.enabled", true);
Services.perms.remove("example.com", "uitour");
});
function done() {
if (gTestTab)
gBrowser.removeTab(gTestTab);
gTestTab = null;
let highlight = document.getElementById("UITourHighlightContainer");
is_element_hidden(highlight, "Highlight should be closed/hidden after UITour tab is closed");
let tooltip = document.getElementById("UITourTooltip");
is_element_hidden(tooltip, "Tooltip should be closed/hidden after UITour tab is closed");
ok(!PanelUI.panel.hasAttribute("noautohide"), "@noautohide on the menu panel should have been cleaned up");
is(UITour.pinnedTabs.get(window), null, "Any pinned tab should be closed after UITour tab is closed");
executeSoon(nextTest);
}
function nextTest() {
if (tests.length == 0) {
finish();
return;
}
let test = tests.shift();
info("Starting " + test.name);
loadTestPage(function() {
test(done);
});
}
nextTest();
UITourTest();
}
let tests = [

View File

@ -0,0 +1,117 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests that annotations disappear when their target is hidden.
*/
"use strict";
let gTestTab;
let gContentAPI;
let gContentWindow;
let highlight = document.getElementById("UITourHighlight");
let tooltip = document.getElementById("UITourTooltip");
Components.utils.import("resource:///modules/UITour.jsm");
function test() {
registerCleanupFunction(() => {
// Close the find bar in case it's open in the remaining tab
gBrowser.getFindBar(gBrowser.selectedTab).close();
});
UITourTest();
}
let tests = [
function test_highlight_move_outside_panel(done) {
gContentAPI.showInfo("urlbar", "test title", "test text");
gContentAPI.showHighlight("customize");
waitForElementToBeVisible(highlight, function checkPanelIsOpen() {
isnot(PanelUI.panel.state, "closed", "Panel should have opened");
// Move the highlight outside which should close the app menu.
gContentAPI.showHighlight("appMenu");
waitForPopupAtAnchor(highlight.parentElement, document.getElementById("PanelUI-menu-button"), () => {
isnot(PanelUI.panel.state, "open",
"Panel should have closed after the highlight moved elsewhere.");
is(tooltip.state, "open", "The info panel should have remained open");
done();
}, "Highlight should move to the appMenu button and still be visible");
}, "Highlight should be shown after showHighlight() for fixed panel items");
},
function test_highlight_panel_hideMenu(done) {
gContentAPI.showHighlight("customize");
gContentAPI.showInfo("search", "test title", "test text");
waitForElementToBeVisible(highlight, function checkPanelIsOpen() {
isnot(PanelUI.panel.state, "closed", "Panel should have opened");
// Close the app menu and make sure the highlight also disappeared.
gContentAPI.hideMenu("appMenu");
waitForElementToBeHidden(highlight, function checkPanelIsClosed() {
isnot(PanelUI.panel.state, "open",
"Panel still should have closed");
is(tooltip.state, "open", "The info panel should have remained open");
done();
}, "Highlight should have disappeared when panel closed");
}, "Highlight should be shown after showHighlight() for fixed panel items");
},
function test_highlight_panel_click_find(done) {
gContentAPI.showHighlight("help");
gContentAPI.showInfo("searchProvider", "test title", "test text");
waitForElementToBeVisible(highlight, function checkPanelIsOpen() {
isnot(PanelUI.panel.state, "closed", "Panel should have opened");
// Click the find button which should close the panel.
let findButton = document.getElementById("find-button");
EventUtils.synthesizeMouseAtCenter(findButton, {});
waitForElementToBeHidden(highlight, function checkPanelIsClosed() {
isnot(PanelUI.panel.state, "open",
"Panel should have closed when the find bar opened");
is(tooltip.state, "open", "The info panel should have remained open");
done();
}, "Highlight should have disappeared when panel closed");
}, "Highlight should be shown after showHighlight() for fixed panel items");
},
function test_highlight_info_panel_click_find(done) {
gContentAPI.showHighlight("help");
gContentAPI.showInfo("customize", "customize me!", "awesome!");
waitForElementToBeVisible(highlight, function checkPanelIsOpen() {
isnot(PanelUI.panel.state, "closed", "Panel should have opened");
// Click the find button which should close the panel.
let findButton = document.getElementById("find-button");
EventUtils.synthesizeMouseAtCenter(findButton, {});
waitForElementToBeHidden(highlight, function checkPanelIsClosed() {
isnot(PanelUI.panel.state, "open",
"Panel should have closed when the find bar opened");
waitForElementToBeHidden(tooltip, function checkTooltipIsClosed() {
isnot(tooltip.state, "open", "The info panel should have closed too");
done();
}, "Tooltip should hide with the menu");
}, "Highlight should have disappeared when panel closed");
}, "Highlight should be shown after showHighlight() for fixed panel items");
},
function test_info_move_outside_panel(done) {
gContentAPI.showInfo("addons", "test title", "test text");
gContentAPI.showHighlight("urlbar");
let addonsButton = document.getElementById("add-ons-button");
waitForPopupAtAnchor(tooltip, addonsButton, function checkPanelIsOpen() {
isnot(PanelUI.panel.state, "closed", "Panel should have opened");
// Move the info panel outside which should close the app menu.
gContentAPI.showInfo("appMenu", "Cool menu button", "It's three lines");
waitForPopupAtAnchor(tooltip, document.getElementById("PanelUI-menu-button"), () => {
isnot(PanelUI.panel.state, "open",
"Menu should have closed after the highlight moved elsewhere.");
is(highlight.parentElement.state, "open", "The highlight should have remained visible");
done();
}, "Tooltip should move to the appMenu button and still be visible");
}, "Tooltip should be shown after showInfo() for a panel item");
},
];

View File

@ -9,75 +9,11 @@ let gContentWindow;
Components.utils.import("resource:///modules/UITour.jsm");
function loadTestPage(callback, host = "https://example.com/") {
if (gTestTab)
gBrowser.removeTab(gTestTab);
let url = getRootDirectory(gTestPath) + "uitour.html";
url = url.replace("chrome://mochitests/content/", host);
gTestTab = gBrowser.addTab(url);
gBrowser.selectedTab = gTestTab;
gTestTab.linkedBrowser.addEventListener("load", function onLoad() {
gTestTab.linkedBrowser.removeEventListener("load", onLoad);
gContentWindow = Components.utils.waiveXrays(gTestTab.linkedBrowser.contentDocument.defaultView);
gContentAPI = gContentWindow.Mozilla.UITour;
waitForFocus(callback, gContentWindow);
}, true);
}
function test() {
Services.prefs.setBoolPref("browser.uitour.enabled", true);
let testUri = Services.io.newURI("http://example.com", null, null);
Services.perms.add(testUri, "uitour", Services.perms.ALLOW_ACTION);
waitForExplicitFinish();
registerCleanupFunction(function() {
delete window.UITour;
delete window.gContentWindow;
delete window.gContentAPI;
if (gTestTab)
gBrowser.removeTab(gTestTab);
delete window.gTestTab;
Services.prefs.clearUserPref("browser.uitour.enabled", true);
Services.prefs.clearUserPref("services.sync.username");
Services.perms.remove("example.com", "uitour");
});
function done() {
executeSoon(() => {
if (gTestTab)
gBrowser.removeTab(gTestTab);
gTestTab = null;
let highlight = document.getElementById("UITourHighlightContainer");
is_element_hidden(highlight, "Highlight should be closed/hidden after UITour tab is closed");
let tooltip = document.getElementById("UITourTooltip");
is_element_hidden(tooltip, "Tooltip should be closed/hidden after UITour tab is closed");
is(UITour.pinnedTabs.get(window), null, "Any pinned tab should be closed after UITour tab is closed");
executeSoon(nextTest);
});
}
function nextTest() {
if (tests.length == 0) {
finish();
return;
}
let test = tests.shift();
info("Starting " + test.name);
loadTestPage(function() {
test(done);
});
}
nextTest();
UITourTest();
}
let tests = [

View File

@ -53,11 +53,20 @@ function waitForElementToBeVisible(element, nextTest, msg) {
"Timeout waiting for visibility: " + msg);
}
function waitForElementToBeHidden(element, nextTest, msg) {
waitForCondition(() => is_hidden(element),
() => {
ok(true, msg);
nextTest();
},
"Timeout waiting for invisibility: " + msg);
}
function waitForPopupAtAnchor(popup, anchorNode, nextTest, msg) {
waitForCondition(() => popup.popupBoxObject.anchorNode == anchorNode,
() => {
ok(true, msg);
is_element_visible(popup);
is_element_visible(popup, "Popup should be visible");
nextTest();
},
"Timeout waiting for popup at anchor: " + msg);
@ -67,3 +76,75 @@ function is_element_hidden(element, msg) {
isnot(element, null, "Element should not be null, when checking visibility");
ok(is_hidden(element), msg);
}
function loadUITourTestPage(callback, host = "https://example.com/") {
if (gTestTab)
gBrowser.removeTab(gTestTab);
let url = getRootDirectory(gTestPath) + "uitour.html";
url = url.replace("chrome://mochitests/content/", host);
gTestTab = gBrowser.addTab(url);
gBrowser.selectedTab = gTestTab;
gTestTab.linkedBrowser.addEventListener("load", function onLoad() {
gTestTab.linkedBrowser.removeEventListener("load", onLoad);
gContentWindow = Components.utils.waiveXrays(gTestTab.linkedBrowser.contentDocument.defaultView);
gContentAPI = gContentWindow.Mozilla.UITour;
waitForFocus(callback, gContentWindow);
}, true);
}
function UITourTest() {
Services.prefs.setBoolPref("browser.uitour.enabled", true);
let testUri = Services.io.newURI("http://example.com", null, null);
Services.perms.add(testUri, "uitour", Services.perms.ALLOW_ACTION);
waitForExplicitFinish();
registerCleanupFunction(function() {
delete window.UITour;
delete window.gContentWindow;
delete window.gContentAPI;
if (gTestTab)
gBrowser.removeTab(gTestTab);
delete window.gTestTab;
Services.prefs.clearUserPref("browser.uitour.enabled", true);
Services.perms.remove("example.com", "uitour");
});
function done() {
executeSoon(() => {
if (gTestTab)
gBrowser.removeTab(gTestTab);
gTestTab = null;
let highlight = document.getElementById("UITourHighlightContainer");
is_element_hidden(highlight, "Highlight should be closed/hidden after UITour tab is closed");
let tooltip = document.getElementById("UITourTooltip");
is_element_hidden(tooltip, "Tooltip should be closed/hidden after UITour tab is closed");
ok(!PanelUI.panel.hasAttribute("noautohide"), "@noautohide on the menu panel should have been cleaned up");
is(UITour.pinnedTabs.get(window), null, "Any pinned tab should be closed after UITour tab is closed");
executeSoon(nextTest);
});
}
function nextTest() {
if (tests.length == 0) {
finish();
return;
}
let test = tests.shift();
info("Starting " + test.name);
loadUITourTestPage(function() {
test(done);
});
}
nextTest();
}

View File

@ -1532,10 +1532,6 @@ richlistitem[type~="action"][actiontype="switchtab"] > .ac-url-box > .ac-action-
-moz-binding: url("chrome://browser/content/customizableui/toolbar.xml#toolbar-drag");
}
#TabsToolbar:not(:-moz-lwtheme) > #tabbrowser-tabs > .tabbrowser-tab:not([selected]) {
color: -moz-menubartext;
}
.tabbrowser-tab:focus > .tab-stack > .tab-content > .tab-label {
outline: 1px dotted;
}

View File

@ -2645,9 +2645,8 @@ toolbarbutton.chevron > .toolbarbutton-menu-dropmarker {
border: none;
}
.tabbrowser-tab:not(:-moz-lwtheme) {
color: #333;
text-shadow: @loweredShadow@;
.tabbrowser-tab[selected=true]:not(:-moz-lwtheme) {
color: inherit;
}
.tabbrowser-tab[selected=true]:-moz-lwtheme {
@ -2694,6 +2693,11 @@ toolbarbutton.chevron > .toolbarbutton-menu-dropmarker {
background-repeat: repeat-x;
}
#TabsToolbar:not(:-moz-lwtheme) {
color: #333;
text-shadow: @loweredShadow@;
}
/*
* Draw the bottom border of the tabstrip when core doesn't do it for us:
*/

View File

@ -11,6 +11,19 @@
%include ../browser.inc
#PanelUI-button {
background-image: -moz-linear-gradient(hsla(0,0%,100%,0), hsla(0,0%,100%,.3) 30%, hsla(0,0%,100%,.3) 70%, hsla(0,0%,100%,0)),
-moz-linear-gradient(hsla(210,54%,20%,0), hsla(210,54%,20%,.3) 30%, hsla(210,54%,20%,.3) 70%, hsla(210,54%,20%,0)),
-moz-linear-gradient(hsla(0,0%,100%,0), hsla(0,0%,100%,.3) 30%, hsla(0,0%,100%,.3) 70%, hsla(0,0%,100%,0));
background-size: 1px calc(100% - 1px), 1px calc(100% - 1px), 1px calc(100% - 1px) !important;
background-position: 0px 0px, 1px 0px, 2px 0px;
background-repeat: no-repeat;
}
#PanelUI-menu-button {
margin: 0 7px 0 9px;
}
.panel-subviews {
padding: 4px;
background-color: hsla(0,0%,100%,.97);

View File

@ -119,6 +119,7 @@
width: @tabCurveWidth@;
}
.tabbrowser-tab:not([selected=true]),
.tabbrowser-tab:-moz-lwtheme {
color: inherit;
}

View File

@ -1577,10 +1577,6 @@ toolbarbutton[type="socialmark"] > .toolbarbutton-icon {
-moz-image-region: rect(0, 64px, 16px, 48px) !important;
}
#main-window[tabsintitlebar]:not([inFullscreen]) .tabbrowser-tab:not([selected]):not(:-moz-lwtheme) {
color: CaptionText;
}
/* tabbrowser-tab focus ring */
.tabbrowser-tab:focus > .tab-stack > .tab-content > .tab-label {
outline: 1px dotted;

View File

@ -19,6 +19,8 @@ import org.mozilla.gecko.gfx.ImmutableViewportMetrics;
import org.mozilla.gecko.gfx.LayerMarginsAnimator;
import org.mozilla.gecko.health.BrowserHealthRecorder;
import org.mozilla.gecko.health.BrowserHealthReporter;
import org.mozilla.gecko.health.HealthRecorder;
import org.mozilla.gecko.health.SessionInformation;
import org.mozilla.gecko.home.BrowserSearch;
import org.mozilla.gecko.home.HomePager;
import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
@ -29,6 +31,7 @@ import org.mozilla.gecko.prompts.Prompt;
import org.mozilla.gecko.toolbar.AutocompleteHandler;
import org.mozilla.gecko.toolbar.BrowserToolbar;
import org.mozilla.gecko.util.Clipboard;
import org.mozilla.gecko.util.EventDispatcher;
import org.mozilla.gecko.util.GamepadUtils;
import org.mozilla.gecko.util.HardwareUtils;
import org.mozilla.gecko.util.StringUtils;
@ -43,6 +46,7 @@ import org.json.JSONObject;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
@ -2583,4 +2587,19 @@ abstract public class BrowserApp extends GeckoApp
mShowActionModeEndAnimation = false;
}
}
@Override
protected HealthRecorder createHealthRecorder(final Context context,
final String profilePath,
final EventDispatcher dispatcher,
final String osLocale,
final String appLocale,
final SessionInformation previousSession) {
return new BrowserHealthRecorder(context,
profilePath,
dispatcher,
osLocale,
appLocale,
previousSession);
}
}

View File

@ -16,8 +16,9 @@ import org.mozilla.gecko.prompts.PromptService;
import org.mozilla.gecko.menu.GeckoMenu;
import org.mozilla.gecko.menu.GeckoMenuInflater;
import org.mozilla.gecko.menu.MenuPanel;
import org.mozilla.gecko.health.BrowserHealthRecorder;
import org.mozilla.gecko.health.BrowserHealthRecorder.SessionInformation;
import org.mozilla.gecko.health.HealthRecorder;
import org.mozilla.gecko.health.SessionInformation;
import org.mozilla.gecko.health.StubbedHealthRecorder;
import org.mozilla.gecko.preferences.GeckoPreferences;
import org.mozilla.gecko.updater.UpdateService;
import org.mozilla.gecko.updater.UpdateServiceHelper;
@ -214,7 +215,7 @@ public abstract class GeckoApp
private String mPrivateBrowsingSession;
private volatile BrowserHealthRecorder mHealthRecorder = null;
private volatile HealthRecorder mHealthRecorder = null;
private int mSignalStrenth;
private PhoneStateListener mPhoneStateListener = null;
@ -582,7 +583,7 @@ public abstract class GeckoApp
// know that mHealthRecorder will exist. That doesn't stop us being
// paranoid.
// This method is cheap, so don't spawn a new runnable.
final BrowserHealthRecorder rec = mHealthRecorder;
final HealthRecorder rec = mHealthRecorder;
if (rec != null) {
rec.recordGeckoStartupTime(mGeckoReadyStartupTimer.getElapsed());
}
@ -1303,7 +1304,7 @@ public abstract class GeckoApp
// of the activity itself.
final String profilePath = getProfile().getDir().getAbsolutePath();
final EventDispatcher dispatcher = GeckoAppShell.getEventDispatcher();
Log.i(LOGTAG, "Creating BrowserHealthRecorder.");
Log.i(LOGTAG, "Creating HealthRecorder.");
final String osLocale = Locale.getDefault().toString();
String appLocale = LocaleManager.getAndApplyPersistedLocale();
@ -1313,12 +1314,12 @@ public abstract class GeckoApp
appLocale = osLocale;
}
mHealthRecorder = new BrowserHealthRecorder(GeckoApp.this,
profilePath,
dispatcher,
osLocale,
appLocale,
previousSession);
mHealthRecorder = GeckoApp.this.createHealthRecorder(GeckoApp.this,
profilePath,
dispatcher,
osLocale,
appLocale,
previousSession);
final String uiLocale = appLocale;
ThreadUtils.postToUiThread(new Runnable() {
@ -1599,7 +1600,7 @@ public abstract class GeckoApp
ThreadUtils.getBackgroundHandler().postDelayed(new Runnable() {
@Override
public void run() {
final BrowserHealthRecorder rec = mHealthRecorder;
final HealthRecorder rec = mHealthRecorder;
if (rec != null) {
rec.recordJavaStartupTime(javaDuration);
}
@ -1962,7 +1963,7 @@ public abstract class GeckoApp
ThreadUtils.postToBackgroundThread(new Runnable() {
@Override
public void run() {
// Now construct the new session on BrowserHealthRecorder's behalf. We do this here
// Now construct the new session on HealthRecorder's behalf. We do this here
// so it can benefit from a single near-startup prefs commit.
SessionInformation currentSession = new SessionInformation(now, realTime);
@ -1972,7 +1973,7 @@ public abstract class GeckoApp
currentSession.recordBegin(editor);
editor.commit();
final BrowserHealthRecorder rec = mHealthRecorder;
final HealthRecorder rec = mHealthRecorder;
if (rec != null) {
rec.setCurrentSession(currentSession);
} else {
@ -1995,7 +1996,7 @@ public abstract class GeckoApp
@Override
public void onPause()
{
final BrowserHealthRecorder rec = mHealthRecorder;
final HealthRecorder rec = mHealthRecorder;
final Context context = this;
// In some way it's sad that Android will trigger StrictMode warnings
@ -2114,9 +2115,9 @@ public abstract class GeckoApp
SmsManager.getInstance().shutdown();
}
final BrowserHealthRecorder rec = mHealthRecorder;
final HealthRecorder rec = mHealthRecorder;
mHealthRecorder = null;
if (rec != null) {
if (rec != null && rec.isEnabled()) {
// Closing a BrowserHealthRecorder could incur a write.
ThreadUtils.postToBackgroundThread(new Runnable() {
@Override
@ -2773,7 +2774,7 @@ public abstract class GeckoApp
/**
* Use LocaleManager to change our persisted and current locales,
* and poke BrowserHealthRecorder to tell it of our changed state.
* and poke HealthRecorder to tell it of our changed state.
*/
private void setLocale(final String locale) {
if (locale == null) {
@ -2784,15 +2785,17 @@ public abstract class GeckoApp
return;
}
final BrowserHealthRecorder rec = mHealthRecorder;
if (rec == null) {
return;
}
final boolean startNewSession = true;
final boolean shouldRestart = false;
rec.onAppLocaleChanged(resultant);
rec.onEnvironmentChanged(startNewSession, SESSION_END_LOCALE_CHANGED);
// If the HealthRecorder is not yet initialized (unlikely), the locale change won't
// trigger a session transition and subsequent events will be recorded in an environment
// with the wrong locale.
final HealthRecorder rec = mHealthRecorder;
if (rec != null) {
rec.onAppLocaleChanged(resultant);
rec.onEnvironmentChanged(startNewSession, SESSION_END_LOCALE_CHANGED);
}
if (!shouldRestart) {
ThreadUtils.postToUiThread(new Runnable() {
@ -2827,4 +2830,14 @@ public abstract class GeckoApp
}
});
}
protected HealthRecorder createHealthRecorder(final Context context,
final String profilePath,
final EventDispatcher dispatcher,
final String osLocale,
final String appLocale,
final SessionInformation previousSession) {
// GeckoApp does not need to record any health information - return a stub.
return new StubbedHealthRecorder();
}
}

View File

@ -6,6 +6,7 @@ package org.mozilla.gecko;
import org.mozilla.gecko.db.BrowserContract;
import org.mozilla.gecko.db.BrowserDB;
import org.mozilla.gecko.home.HomeConfigInvalidator;
import org.mozilla.gecko.mozglue.GeckoLoader;
import org.mozilla.gecko.util.Clipboard;
import org.mozilla.gecko.util.HardwareUtils;
@ -68,6 +69,7 @@ public class GeckoApplication extends Application {
GeckoBatteryManager.getInstance().start();
GeckoNetworkManager.getInstance().init(getApplicationContext());
MemoryMonitor.getInstance().init(getApplicationContext());
HomeConfigInvalidator.getInstance().init(getApplicationContext());
mInited = true;
}

View File

@ -62,7 +62,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
*
* Shut it down when you're done being a browser: {@link #close()}.
*/
public class BrowserHealthRecorder implements GeckoEventListener {
public class BrowserHealthRecorder implements HealthRecorder, GeckoEventListener {
private static final String LOG_TAG = "GeckoHealthRec";
private static final String PREF_ACCEPT_LANG = "intl.accept_languages";
private static final String PREF_BLOCKLIST_ENABLED = "extensions.blocklist.enabled";
@ -97,128 +97,10 @@ public class BrowserHealthRecorder implements GeckoEventListener {
private final ProfileInformationCache profileCache;
private final EventDispatcher dispatcher;
public static class SessionInformation {
private static final String LOG_TAG = "GeckoSessInfo";
public static final String PREFS_SESSION_START = "sessionStart";
public final long wallStartTime; // System wall clock.
public final long realStartTime; // Realtime clock.
private final boolean wasOOM;
private final boolean wasStopped;
private volatile long timedGeckoStartup = -1;
private volatile long timedJavaStartup = -1;
// Current sessions don't (right now) care about wasOOM/wasStopped.
// Eventually we might want to lift that logic out of GeckoApp.
public SessionInformation(long wallTime, long realTime) {
this(wallTime, realTime, false, false);
}
// Previous sessions do...
public SessionInformation(long wallTime, long realTime, boolean wasOOM, boolean wasStopped) {
this.wallStartTime = wallTime;
this.realStartTime = realTime;
this.wasOOM = wasOOM;
this.wasStopped = wasStopped;
}
/**
* Initialize a new SessionInformation instance from the supplied prefs object.
*
* This includes retrieving OOM/crash data, as well as timings.
*
* If no wallStartTime was found, that implies that the previous
* session was correctly recorded, and an object with a zero
* wallStartTime is returned.
*/
public static SessionInformation fromSharedPrefs(SharedPreferences prefs) {
boolean wasOOM = prefs.getBoolean(GeckoApp.PREFS_OOM_EXCEPTION, false);
boolean wasStopped = prefs.getBoolean(GeckoApp.PREFS_WAS_STOPPED, true);
long wallStartTime = prefs.getLong(PREFS_SESSION_START, 0L);
long realStartTime = 0L;
Log.d(LOG_TAG, "Building SessionInformation from prefs: " +
wallStartTime + ", " + realStartTime + ", " +
wasStopped + ", " + wasOOM);
return new SessionInformation(wallStartTime, realStartTime, wasOOM, wasStopped);
}
/**
* Initialize a new SessionInformation instance to 'split' the current
* session.
*/
public static SessionInformation forRuntimeTransition() {
final boolean wasOOM = false;
final boolean wasStopped = true;
final long wallStartTime = System.currentTimeMillis();
final long realStartTime = android.os.SystemClock.elapsedRealtime();
Log.v(LOG_TAG, "Recording runtime session transition: " +
wallStartTime + ", " + realStartTime);
return new SessionInformation(wallStartTime, realStartTime, wasOOM, wasStopped);
}
public boolean wasKilled() {
return wasOOM || !wasStopped;
}
/**
* Record the beginning of this session to SharedPreferences by
* recording our start time. If a session was already recorded, it is
* overwritten (there can only be one running session at a time). Does
* not commit the editor.
*/
public void recordBegin(SharedPreferences.Editor editor) {
Log.d(LOG_TAG, "Recording start of session: " + this.wallStartTime);
editor.putLong(PREFS_SESSION_START, this.wallStartTime);
}
/**
* Record the completion of this session to SharedPreferences by
* deleting our start time. Does not commit the editor.
*/
public void recordCompletion(SharedPreferences.Editor editor) {
Log.d(LOG_TAG, "Recording session done: " + this.wallStartTime);
editor.remove(PREFS_SESSION_START);
}
/**
* Return the JSON that we'll put in the DB for this session.
*/
public JSONObject getCompletionJSON(String reason, long realEndTime) throws JSONException {
long durationSecs = (realEndTime - this.realStartTime) / 1000;
JSONObject out = new JSONObject();
out.put("r", reason);
out.put("d", durationSecs);
if (this.timedGeckoStartup > 0) {
out.put("sg", this.timedGeckoStartup);
}
if (this.timedJavaStartup > 0) {
out.put("sj", this.timedJavaStartup);
}
return out;
}
public JSONObject getCrashedJSON() throws JSONException {
JSONObject out = new JSONObject();
// We use ints here instead of booleans, because we're packing
// stuff into JSON, and saving bytes in the DB is a worthwhile
// goal.
out.put("oom", this.wasOOM ? 1 : 0);
out.put("stopped", this.wasStopped ? 1 : 0);
out.put("r", "A");
return out;
}
}
// We track previousSession to avoid order-of-initialization confusion. We
// accept it in the constructor, and process it after init.
private final SessionInformation previousSession;
private volatile SessionInformation session = null;
public SessionInformation getCurrentSession() {
return this.session;
}
public void setCurrentSession(SessionInformation session) {
this.session = session;
@ -228,13 +110,13 @@ public class BrowserHealthRecorder implements GeckoEventListener {
if (this.session == null) {
return;
}
this.session.timedGeckoStartup = duration;
this.session.setTimedGeckoStartup(duration);
}
public void recordJavaStartupTime(long duration) {
if (this.session == null) {
return;
}
this.session.timedJavaStartup = duration;
this.session.setTimedJavaStartup(duration);
}
/**
@ -286,6 +168,10 @@ public class BrowserHealthRecorder implements GeckoEventListener {
}
}
public boolean isEnabled() {
return true;
}
/**
* Shut down database connections, unregister event listeners, and perform
* provider-specific uninitialization.
@ -1017,4 +903,3 @@ public class BrowserHealthRecorder implements GeckoEventListener {
session.recordCompletion(editor);
}
}

View File

@ -0,0 +1,37 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* 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 http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.health;
import android.content.SharedPreferences;
import org.json.JSONObject;
/**
* HealthRecorder is an interface into the Firefox Health Report storage system.
*/
public interface HealthRecorder {
/**
* Returns whether the Health Recorder is actively recording events.
*/
public boolean isEnabled();
public void setCurrentSession(SessionInformation session);
public void checkForOrphanSessions();
public void recordGeckoStartupTime(long duration);
public void recordJavaStartupTime(long duration);
public void recordSearch(final String engineID, final String location);
public void recordSessionEnd(String reason, SharedPreferences.Editor editor);
public void recordSessionEnd(String reason, SharedPreferences.Editor editor, final int environment);
public void onAppLocaleChanged(String to);
public void onAddonChanged(String id, JSONObject json);
public void onAddonUninstalling(String id);
public void onEnvironmentChanged();
public void onEnvironmentChanged(final boolean startNewSession, final String sessionEndReason);
public void close();
}

View File

@ -0,0 +1,137 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* 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 http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.health;
import android.content.SharedPreferences;
import android.util.Log;
import org.mozilla.gecko.GeckoApp;
import org.json.JSONException;
import org.json.JSONObject;
public class SessionInformation {
private static final String LOG_TAG = "GeckoSessInfo";
public static final String PREFS_SESSION_START = "sessionStart";
public final long wallStartTime; // System wall clock.
public final long realStartTime; // Realtime clock.
private final boolean wasOOM;
private final boolean wasStopped;
private volatile long timedGeckoStartup = -1;
private volatile long timedJavaStartup = -1;
// Current sessions don't (right now) care about wasOOM/wasStopped.
// Eventually we might want to lift that logic out of GeckoApp.
public SessionInformation(long wallTime, long realTime) {
this(wallTime, realTime, false, false);
}
// Previous sessions do...
public SessionInformation(long wallTime, long realTime, boolean wasOOM, boolean wasStopped) {
this.wallStartTime = wallTime;
this.realStartTime = realTime;
this.wasOOM = wasOOM;
this.wasStopped = wasStopped;
}
/**
* Initialize a new SessionInformation instance from the supplied prefs object.
*
* This includes retrieving OOM/crash data, as well as timings.
*
* If no wallStartTime was found, that implies that the previous
* session was correctly recorded, and an object with a zero
* wallStartTime is returned.
*/
public static SessionInformation fromSharedPrefs(SharedPreferences prefs) {
boolean wasOOM = prefs.getBoolean(GeckoApp.PREFS_OOM_EXCEPTION, false);
boolean wasStopped = prefs.getBoolean(GeckoApp.PREFS_WAS_STOPPED, true);
long wallStartTime = prefs.getLong(PREFS_SESSION_START, 0L);
long realStartTime = 0L;
Log.d(LOG_TAG, "Building SessionInformation from prefs: " +
wallStartTime + ", " + realStartTime + ", " +
wasStopped + ", " + wasOOM);
return new SessionInformation(wallStartTime, realStartTime, wasOOM, wasStopped);
}
/**
* Initialize a new SessionInformation instance to 'split' the current
* session.
*/
public static SessionInformation forRuntimeTransition() {
final boolean wasOOM = false;
final boolean wasStopped = true;
final long wallStartTime = System.currentTimeMillis();
final long realStartTime = android.os.SystemClock.elapsedRealtime();
Log.v(LOG_TAG, "Recording runtime session transition: " +
wallStartTime + ", " + realStartTime);
return new SessionInformation(wallStartTime, realStartTime, wasOOM, wasStopped);
}
public boolean wasKilled() {
return wasOOM || !wasStopped;
}
/**
* Record the beginning of this session to SharedPreferences by
* recording our start time. If a session was already recorded, it is
* overwritten (there can only be one running session at a time). Does
* not commit the editor.
*/
public void recordBegin(SharedPreferences.Editor editor) {
Log.d(LOG_TAG, "Recording start of session: " + this.wallStartTime);
editor.putLong(PREFS_SESSION_START, this.wallStartTime);
}
/**
* Record the completion of this session to SharedPreferences by
* deleting our start time. Does not commit the editor.
*/
public void recordCompletion(SharedPreferences.Editor editor) {
Log.d(LOG_TAG, "Recording session done: " + this.wallStartTime);
editor.remove(PREFS_SESSION_START);
}
/**
* Return the JSON that we'll put in the DB for this session.
*/
public JSONObject getCompletionJSON(String reason, long realEndTime) throws JSONException {
long durationSecs = (realEndTime - this.realStartTime) / 1000;
JSONObject out = new JSONObject();
out.put("r", reason);
out.put("d", durationSecs);
if (this.timedGeckoStartup > 0) {
out.put("sg", this.timedGeckoStartup);
}
if (this.timedJavaStartup > 0) {
out.put("sj", this.timedJavaStartup);
}
return out;
}
public JSONObject getCrashedJSON() throws JSONException {
JSONObject out = new JSONObject();
// We use ints here instead of booleans, because we're packing
// stuff into JSON, and saving bytes in the DB is a worthwhile
// goal.
out.put("oom", this.wasOOM ? 1 : 0);
out.put("stopped", this.wasStopped ? 1 : 0);
out.put("r", "A");
return out;
}
public void setTimedGeckoStartup(final long duration) {
timedGeckoStartup = duration;
}
public void setTimedJavaStartup(final long duration) {
timedJavaStartup = duration;
}
}

View File

@ -0,0 +1,35 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* 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 http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.health;
import android.content.SharedPreferences;
import org.json.JSONObject;
/**
* StubbedHealthRecorder is an implementation of HealthRecorder that does (you guessed it!)
* nothing.
*/
public class StubbedHealthRecorder implements HealthRecorder {
public boolean isEnabled() { return false; }
public void setCurrentSession(SessionInformation session) { }
public void checkForOrphanSessions() { }
public void recordGeckoStartupTime(long duration) { }
public void recordJavaStartupTime(long duration) { }
public void recordSearch(final String engineID, final String location) { }
public void recordSessionEnd(String reason, SharedPreferences.Editor editor) { }
public void recordSessionEnd(String reason, SharedPreferences.Editor editor, final int environment) { }
public void onAppLocaleChanged(String to) { }
public void onAddonChanged(String id, JSONObject json) { }
public void onAddonUninstalling(String id) { }
public void onEnvironmentChanged() { }
public void onEnvironmentChanged(final boolean startNewSession, final String sessionEndReason) { }
public void close() { }
}

View File

@ -533,9 +533,9 @@ public class BrowserSearch extends HomeFragment
mAdapter.notifyDataSetChanged();
}
// Show suggestions opt-in prompt only if user hasn't been prompted
// and we're not on a private browsing tab.
if (!suggestionsPrompted && mSuggestClient != null) {
// Show suggestions opt-in prompt only if suggestions are not enabled yet,
// user hasn't been prompted and we're not on a private browsing tab.
if (!mSuggestionsEnabled && !suggestionsPrompted && mSuggestClient != null) {
showSuggestionsOptIn();
}
} catch (JSONException e) {

View File

@ -168,7 +168,7 @@ class HomeAdapter extends FragmentStatePagerAdapter {
args.putBoolean(HomePager.CAN_LOAD_ARG, mCanLoadHint);
// Only DynamicPanels need the PanelConfig argument
if (mPanelConfig.getType() == PanelType.DYNAMIC) {
if (mPanelConfig.isDynamic()) {
args.putParcelable(HomePager.PANEL_CONFIG_ARG, mPanelConfig);
}

View File

@ -5,6 +5,8 @@
package org.mozilla.gecko.home;
import org.mozilla.gecko.R;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
@ -104,11 +106,18 @@ public final class HomeConfig {
public enum Flags {
DEFAULT_PANEL,
DISABLED_PANEL
DISABLED_PANEL,
DELETED_PANEL
}
public PanelConfig(JSONObject json) throws JSONException, IllegalArgumentException {
mType = PanelType.fromId(json.getString(JSON_KEY_TYPE));
final String panelType = json.optString(JSON_KEY_TYPE, null);
if (TextUtils.isEmpty(panelType)) {
mType = PanelType.DYNAMIC;
} else {
mType = PanelType.fromId(panelType);
}
mTitle = json.getString(JSON_KEY_TITLE);
mId = json.getString(JSON_KEY_ID);
@ -249,6 +258,10 @@ public final class HomeConfig {
return (mViews != null ? mViews.get(index) : null);
}
public boolean isDynamic() {
return (mType == PanelType.DYNAMIC);
}
public boolean isDefault() {
return mFlags.contains(Flags.DEFAULT_PANEL);
}
@ -273,6 +286,18 @@ public final class HomeConfig {
}
}
public boolean isDeleted() {
return mFlags.contains(Flags.DELETED_PANEL);
}
public void setIsDeleted(boolean isDeleted) {
if (isDeleted) {
mFlags.add(Flags.DELETED_PANEL);
} else {
mFlags.remove(Flags.DELETED_PANEL);
}
}
public JSONObject toJSON() throws JSONException {
final JSONObject json = new JSONObject();
@ -308,6 +333,24 @@ public final class HomeConfig {
return json;
}
@Override
public boolean equals(Object o) {
if (o == null) {
return false;
}
if (this == o) {
return true;
}
if (!(o instanceof PanelConfig)) {
return false;
}
final PanelConfig other = (PanelConfig) o;
return mId.equals(other.mId);
}
@Override
public int describeContents() {
return 0;
@ -536,6 +579,12 @@ public final class HomeConfig {
public void setOnChangeListener(OnChangeListener listener);
}
// UUIDs used to create PanelConfigs for default built-in panels
private static final String TOP_SITES_PANEL_ID = "4becc86b-41eb-429a-a042-88fe8b5a094e";
private static final String BOOKMARKS_PANEL_ID = "7f6d419a-cd6c-4e34-b26f-f68b1b551907";
private static final String READING_LIST_PANEL_ID = "20f4549a-64ad-4c32-93e4-1dcef792733b";
private static final String HISTORY_PANEL_ID = "f134bf20-11f7-4867-ab8b-e8e705d7fbe8";
private final HomeConfigBackend mBackend;
public HomeConfig(HomeConfigBackend backend) {
@ -546,14 +595,56 @@ public final class HomeConfig {
return mBackend.load();
}
public void save(List<PanelConfig> entries) {
mBackend.save(entries);
public void save(List<PanelConfig> panelConfigs) {
for (PanelConfig panelConfig : panelConfigs) {
if (panelConfig.isDeleted()) {
throw new IllegalArgumentException("Should never save a deleted PanelConfig: " + panelConfig.getId());
}
}
mBackend.save(panelConfigs);
}
public void setOnChangeListener(OnChangeListener listener) {
mBackend.setOnChangeListener(listener);
}
public static PanelConfig createBuiltinPanelConfig(Context context, PanelType panelType) {
return createBuiltinPanelConfig(context, panelType, EnumSet.noneOf(PanelConfig.Flags.class));
}
public static PanelConfig createBuiltinPanelConfig(Context context, PanelType panelType, EnumSet<PanelConfig.Flags> flags) {
int titleId = 0;
String id = null;
switch(panelType) {
case TOP_SITES:
titleId = R.string.home_top_sites_title;
id = TOP_SITES_PANEL_ID;
break;
case BOOKMARKS:
titleId = R.string.bookmarks_title;
id = BOOKMARKS_PANEL_ID;
break;
case HISTORY:
titleId = R.string.home_history_title;
id = HISTORY_PANEL_ID;
break;
case READING_LIST:
titleId = R.string.reading_list_title;
id = READING_LIST_PANEL_ID;
break;
case DYNAMIC:
throw new IllegalArgumentException("createBuiltinPanelConfig() is only for built-in panels");
}
return new PanelConfig(panelType, context.getString(titleId), id, flags);
}
public static HomeConfig getDefault(Context context) {
return new HomeConfig(new HomeConfigPrefsBackend(context));
}

View File

@ -0,0 +1,232 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* 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 http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.home;
import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.home.HomeConfig.PanelConfig;
import org.mozilla.gecko.home.HomeConfig.PanelType;
import org.mozilla.gecko.home.PanelManager.PanelInfo;
import org.mozilla.gecko.home.PanelManager.RequestCallback;
import org.mozilla.gecko.util.GeckoEventListener;
import org.mozilla.gecko.util.ThreadUtils;
import static org.mozilla.gecko.home.HomeConfig.createBuiltinPanelConfig;
import android.content.Context;
import android.os.Handler;
import android.util.Log;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentLinkedQueue;
public class HomeConfigInvalidator implements GeckoEventListener {
public static final String LOGTAG = "HomeConfigInvalidator";
private static final HomeConfigInvalidator sInstance = new HomeConfigInvalidator();
private static final int INVALIDATION_DELAY_MSEC = 500;
private static final int PANEL_INFO_TIMEOUT_MSEC = 1000;
private static final String EVENT_HOMEPANELS_INSTALL = "HomePanels:Install";
private static final String EVENT_HOMEPANELS_REMOVE = "HomePanels:Remove";
private static final String JSON_KEY_PANEL = "panel";
private Context mContext;
private HomeConfig mHomeConfig;
private final ConcurrentLinkedQueue<PanelConfig> mPendingChanges = new ConcurrentLinkedQueue<PanelConfig>();
private final Runnable mInvalidationRunnable = new InvalidationRunnable();
public static HomeConfigInvalidator getInstance() {
return sInstance;
}
public void init(Context context) {
mContext = context;
mHomeConfig = HomeConfig.getDefault(context);
GeckoAppShell.getEventDispatcher().registerEventListener(EVENT_HOMEPANELS_INSTALL, this);
GeckoAppShell.getEventDispatcher().registerEventListener(EVENT_HOMEPANELS_REMOVE, this);
}
@Override
public void handleMessage(String event, JSONObject message) {
try {
final JSONObject json = message.getJSONObject(JSON_KEY_PANEL);
final PanelConfig panelConfig = new PanelConfig(json);
if (event.equals(EVENT_HOMEPANELS_INSTALL)) {
Log.d(LOGTAG, EVENT_HOMEPANELS_INSTALL);
handlePanelInstall(panelConfig);
} else if (event.equals(EVENT_HOMEPANELS_REMOVE)) {
Log.d(LOGTAG, EVENT_HOMEPANELS_REMOVE);
handlePanelRemove(panelConfig);
}
} catch (Exception e) {
Log.e(LOGTAG, "Failed to handle event " + event, e);
}
}
/**
* Runs in the gecko thread.
*/
private void handlePanelInstall(PanelConfig panelConfig) {
mPendingChanges.offer(panelConfig);
Log.d(LOGTAG, "handlePanelInstall: " + mPendingChanges.size());
scheduleInvalidation();
}
/**
* Runs in the gecko thread.
*/
private void handlePanelRemove(PanelConfig panelConfig) {
panelConfig.setIsDeleted(true);
mPendingChanges.offer(panelConfig);
Log.d(LOGTAG, "handlePanelRemove: " + mPendingChanges.size());
scheduleInvalidation();
}
/**
* Runs in the gecko or main thread.
*/
private void scheduleInvalidation() {
final Handler handler = ThreadUtils.getBackgroundHandler();
handler.removeCallbacks(mInvalidationRunnable);
handler.postDelayed(mInvalidationRunnable, INVALIDATION_DELAY_MSEC);
Log.d(LOGTAG, "scheduleInvalidation: scheduled new invalidation");
}
/**
* Runs in the background thread.
*/
private List<PanelConfig> executePendingChanges(List<PanelConfig> panelConfigs) {
while (!mPendingChanges.isEmpty()) {
final PanelConfig panelConfig = mPendingChanges.poll();
final String id = panelConfig.getId();
if (panelConfig.isDeleted()) {
if (panelConfigs.remove(panelConfig)) {
Log.d(LOGTAG, "executePendingChanges: removed panel " + id);
}
} else {
final int index = panelConfigs.indexOf(panelConfig);
if (index >= 0) {
panelConfigs.set(index, panelConfig);
Log.d(LOGTAG, "executePendingChanges: replaced position " + index + " with " + id);
} else {
panelConfigs.add(panelConfig);
Log.d(LOGTAG, "executePendingChanges: added panel " + id);
}
}
}
return executeRefresh(panelConfigs);
}
/**
* Runs in the background thread.
*/
private List<PanelConfig> refreshFromPanelInfos(List<PanelConfig> panelConfigs, List<PanelInfo> panelInfos) {
Log.d(LOGTAG, "refreshFromPanelInfos");
final int count = panelConfigs.size();
for (int i = 0; i < count; i++) {
final PanelConfig panelConfig = panelConfigs.get(i);
PanelConfig refreshedPanelConfig = null;
if (panelConfig.isDynamic()) {
for (PanelInfo panelInfo : panelInfos) {
if (panelInfo.getId().equals(panelConfig.getId())) {
refreshedPanelConfig = panelInfo.toPanelConfig();
Log.d(LOGTAG, "refreshFromPanelInfos: refreshing from panel info: " + panelInfo.getId());
break;
}
}
} else {
refreshedPanelConfig = createBuiltinPanelConfig(mContext, panelConfig.getType());
Log.d(LOGTAG, "refreshFromPanelInfos: refreshing built-in panel: " + panelConfig.getId());
}
if (refreshedPanelConfig == null) {
Log.d(LOGTAG, "refreshFromPanelInfos: no refreshed panel, falling back: " + panelConfig.getId());
refreshedPanelConfig = panelConfig;
}
refreshedPanelConfig.setIsDefault(panelConfig.isDefault());
refreshedPanelConfig.setIsDisabled(panelConfig.isDisabled());
Log.d(LOGTAG, "refreshFromPanelInfos: set " + i + " with " + refreshedPanelConfig.getId());
panelConfigs.set(i, refreshedPanelConfig);
}
return panelConfigs;
}
/**
* Runs in the background thread.
*/
private List<PanelConfig> executeRefresh(List<PanelConfig> panelConfigs) {
if (panelConfigs.isEmpty()) {
return panelConfigs;
}
Log.d(LOGTAG, "executeRefresh");
final Set<String> ids = new HashSet<String>();
for (PanelConfig panelConfig : panelConfigs) {
ids.add(panelConfig.getId());
}
final Object panelRequestLock = new Object();
final List<PanelInfo> latestPanelInfos = new ArrayList<PanelInfo>();
final PanelManager pm = new PanelManager();
pm.requestPanelsById(ids, new RequestCallback() {
@Override
public void onComplete(List<PanelInfo> panelInfos) {
synchronized(panelRequestLock) {
latestPanelInfos.addAll(panelInfos);
Log.d(LOGTAG, "executeRefresh: fetched panel infos: " + panelInfos.size());
panelRequestLock.notifyAll();
}
}
});
try {
synchronized(panelRequestLock) {
panelRequestLock.wait(PANEL_INFO_TIMEOUT_MSEC);
Log.d(LOGTAG, "executeRefresh: done fetching panel infos");
return refreshFromPanelInfos(panelConfigs, latestPanelInfos);
}
} catch (InterruptedException e) {
Log.e(LOGTAG, "Failed to fetch panels from gecko", e);
return panelConfigs;
}
}
/**
* Runs in the background thread.
*/
private class InvalidationRunnable implements Runnable {
@Override
public void run() {
mHomeConfig.save(executePendingChanges(mHomeConfig.load()));
}
};
}

View File

@ -13,6 +13,8 @@ import org.mozilla.gecko.home.HomeConfig.PanelType;
import org.mozilla.gecko.util.HardwareUtils;
import org.mozilla.gecko.util.ThreadUtils;
import static org.mozilla.gecko.home.HomeConfig.createBuiltinPanelConfig;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
@ -25,7 +27,6 @@ import android.text.TextUtils;
import android.util.Log;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
@ -34,12 +35,6 @@ class HomeConfigPrefsBackend implements HomeConfigBackend {
private static final String PREFS_KEY = "home_panels";
// UUIDs used to create PanelConfigs for default built-in panels
private static final String TOP_SITES_PANEL_ID = "4becc86b-41eb-429a-a042-88fe8b5a094e";
private static final String BOOKMARKS_PANEL_ID = "7f6d419a-cd6c-4e34-b26f-f68b1b551907";
private static final String READING_LIST_PANEL_ID = "20f4549a-64ad-4c32-93e4-1dcef792733b";
private static final String HISTORY_PANEL_ID = "f134bf20-11f7-4867-ab8b-e8e705d7fbe8";
private final Context mContext;
private PrefsListener mPrefsListener;
private OnChangeListener mChangeListener;
@ -55,26 +50,18 @@ class HomeConfigPrefsBackend implements HomeConfigBackend {
private List<PanelConfig> loadDefaultConfig() {
final ArrayList<PanelConfig> panelConfigs = new ArrayList<PanelConfig>();
panelConfigs.add(new PanelConfig(PanelType.TOP_SITES,
mContext.getString(R.string.home_top_sites_title),
TOP_SITES_PANEL_ID,
EnumSet.of(PanelConfig.Flags.DEFAULT_PANEL)));
panelConfigs.add(createBuiltinPanelConfig(mContext, PanelType.TOP_SITES,
EnumSet.of(PanelConfig.Flags.DEFAULT_PANEL)));
panelConfigs.add(new PanelConfig(PanelType.BOOKMARKS,
mContext.getString(R.string.bookmarks_title),
BOOKMARKS_PANEL_ID));
panelConfigs.add(createBuiltinPanelConfig(mContext, PanelType.BOOKMARKS));
// We disable reader mode support on low memory devices. Hence the
// reading list panel should not show up on such devices.
if (!HardwareUtils.isLowMemoryPlatform()) {
panelConfigs.add(new PanelConfig(PanelType.READING_LIST,
mContext.getString(R.string.reading_list_title),
READING_LIST_PANEL_ID));
panelConfigs.add(createBuiltinPanelConfig(mContext, PanelType.READING_LIST));
}
final PanelConfig historyEntry = new PanelConfig(PanelType.HISTORY,
mContext.getString(R.string.home_history_title),
HISTORY_PANEL_ID);
final PanelConfig historyEntry = createBuiltinPanelConfig(mContext, PanelType.HISTORY);
// On tablets, the history panel is the last.
// On phones, the history panel is the first one.
@ -126,7 +113,7 @@ class HomeConfigPrefsBackend implements HomeConfigBackend {
panelConfigs = loadConfigFromString(jsonString);
}
return Collections.unmodifiableList(panelConfigs);
return panelConfigs;
}
@Override

View File

@ -6,8 +6,11 @@
package org.mozilla.gecko.home;
import org.mozilla.gecko.R;
import org.mozilla.gecko.db.BrowserContract.HomeItems;
import org.mozilla.gecko.home.HomeConfig.ViewConfig;
import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
import org.mozilla.gecko.home.PanelLayout.DatasetBacked;
import org.mozilla.gecko.home.PanelLayout.PanelView;
import android.content.Context;
import android.database.Cursor;
@ -15,18 +18,30 @@ import android.support.v4.widget.CursorAdapter;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.GridView;
public class PanelGridView extends GridView implements DatasetBacked {
import java.util.EnumSet;
public class PanelGridView extends GridView
implements DatasetBacked, PanelView {
private static final String LOGTAG = "GeckoPanelGridView";
private final PanelGridViewAdapter mAdapter;
protected OnUrlOpenListener mUrlOpenListener;
public PanelGridView(Context context, ViewConfig viewConfig) {
super(context, null, R.attr.panelGridViewStyle);
mAdapter = new PanelGridViewAdapter(context);
setAdapter(mAdapter);
setNumColumns(AUTO_FIT);
setOnItemClickListener(new PanelGridItemClickListener());
}
@Override
public void onDetachedFromWindow() {
super.onDetachedFromWindow();
mUrlOpenListener = null;
}
@Override
@ -34,6 +49,11 @@ public class PanelGridView extends GridView implements DatasetBacked {
mAdapter.swapCursor(cursor);
}
@Override
public void setOnUrlOpenListener(OnUrlOpenListener listener) {
mUrlOpenListener = listener;
}
private class PanelGridViewAdapter extends CursorAdapter {
public PanelGridViewAdapter(Context context) {
@ -51,4 +71,19 @@ public class PanelGridView extends GridView implements DatasetBacked {
return new PanelGridItemView(context);
}
}
private class PanelGridItemClickListener implements AdapterView.OnItemClickListener {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Cursor cursor = mAdapter.getCursor();
if (cursor == null || !cursor.moveToPosition(position)) {
throw new IllegalStateException("Couldn't move cursor to position " + position);
}
int urlIndex = cursor.getColumnIndexOrThrow(HomeItems.URL);
final String url = cursor.getString(urlIndex);
mUrlOpenListener.onUrlOpen(url, EnumSet.of(OnUrlOpenListener.Flags.OPEN_WITH_INTENT));
}
}
}

View File

@ -7,6 +7,7 @@ package org.mozilla.gecko.home;
import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.GeckoEvent;
import org.mozilla.gecko.home.HomeConfig.PanelConfig;
import org.mozilla.gecko.util.GeckoEventListener;
import org.mozilla.gecko.util.ThreadUtils;
@ -23,22 +24,38 @@ import android.util.SparseArray;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
public class PanelManager implements GeckoEventListener {
private static final String LOGTAG = "GeckoPanelManager";
public class PanelInfo {
public final String id;
public final String title;
public final String layout;
public final JSONArray views;
private final String mId;
private final String mTitle;
private final JSONObject mJSONData;
public PanelInfo(String id, String title, String layout, JSONArray views) {
this.id = id;
this.title = title;
this.layout = layout;
this.views = views;
public PanelInfo(String id, String title, JSONObject jsonData) {
mId = id;
mTitle = title;
mJSONData = jsonData;
}
public String getId() {
return mId;
}
public String getTitle() {
return mTitle;
}
public PanelConfig toPanelConfig() {
try {
return new PanelConfig(mJSONData);
} catch (Exception e) {
Log.e(LOGTAG, "Failed to convert PanelInfo to PanelConfig", e);
return null;
}
}
}
@ -52,11 +69,14 @@ public class PanelManager implements GeckoEventListener {
private static final SparseArray<RequestCallback> sCallbacks = new SparseArray<RequestCallback>();
/**
* Asynchronously fetches list of available panels from Gecko.
* Asynchronously fetches list of available panels from Gecko
* for the given IDs.
*
* @param ids list of panel ids to be fetched. A null value will fetch all
* available panels.
* @param callback onComplete will be called on the UI thread.
*/
public void requestAvailablePanels(RequestCallback callback) {
public void requestPanelsById(Set<String> ids, RequestCallback callback) {
final int requestId = sRequestId.getAndIncrement();
synchronized(sCallbacks) {
@ -67,7 +87,33 @@ public class PanelManager implements GeckoEventListener {
sCallbacks.put(requestId, callback);
}
GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("HomePanels:Get", Integer.toString(requestId)));
final JSONObject message = new JSONObject();
try {
message.put("requestId", requestId);
if (ids != null && ids.size() > 0) {
JSONArray idsArray = new JSONArray();
for (String id : ids) {
idsArray.put(id);
}
message.put("ids", idsArray);
}
} catch (JSONException e) {
Log.e(LOGTAG, "Failed to build event to request panels by id", e);
return;
}
GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("HomePanels:Get", message.toString()));
}
/**
* Asynchronously fetches list of available panels from Gecko.
*
* @param callback onComplete will be called on the UI thread.
*/
public void requestAvailablePanels(RequestCallback callback) {
requestPanelsById(null, callback);
}
/**
@ -112,9 +158,7 @@ public class PanelManager implements GeckoEventListener {
private PanelInfo getPanelInfoFromJSON(JSONObject jsonPanelInfo) throws JSONException {
final String id = jsonPanelInfo.getString("id");
final String title = jsonPanelInfo.getString("title");
final String layout = jsonPanelInfo.getString("layout");
final JSONArray views = jsonPanelInfo.getJSONArray("views");
return new PanelInfo(id, title, layout, views);
return new PanelInfo(id, title, jsonPanelInfo);
}
}

View File

@ -207,6 +207,9 @@ gbjar.sources += [
'GlobalHistory.java',
'health/BrowserHealthRecorder.java',
'health/BrowserHealthReporter.java',
'health/HealthRecorder.java',
'health/SessionInformation.java',
'health/StubbedHealthRecorder.java',
'home/BookmarkFolderView.java',
'home/BookmarksListAdapter.java',
'home/BookmarksListView.java',
@ -219,6 +222,7 @@ gbjar.sources += [
'home/HomeAdapter.java',
'home/HomeBanner.java',
'home/HomeConfig.java',
'home/HomeConfigInvalidator.java',
'home/HomeConfigLoader.java',
'home/HomeConfigPrefsBackend.java',
'home/HomeContextMenuInfo.java',

View File

@ -24,7 +24,7 @@ public class PanelsPreferenceCategory extends CustomListCategory {
public static final String LOGTAG = "PanelsPrefCategory";
protected HomeConfig mHomeConfig;
protected final List<PanelConfig> mPanelConfigs = new ArrayList<PanelConfig>();
protected List<PanelConfig> mPanelConfigs;
protected UiAsyncTask<Void, Void, List<PanelConfig>> mLoadTask;
protected UiAsyncTask<Void, Void, Void> mSaveTask;
@ -67,17 +67,15 @@ public class PanelsPreferenceCategory extends CustomListCategory {
@Override
public void onPostExecute(List<PanelConfig> panelConfigs) {
displayPanelConfig(panelConfigs);
mPanelConfigs = panelConfigs;
displayPanelConfig();
}
};
mLoadTask.execute();
}
private void displayPanelConfig(List<PanelConfig> panelConfigs) {
for (PanelConfig panelConfig: panelConfigs) {
// Populate our local copy of the panels.
mPanelConfigs.add(panelConfig);
private void displayPanelConfig() {
for (PanelConfig panelConfig : mPanelConfigs) {
// Create and add the pref.
final PanelsPreference pref = new PanelsPreference(getContext(), PanelsPreferenceCategory.this);
pref.setTitle(panelConfig.getTitle());
@ -102,6 +100,10 @@ public class PanelsPreferenceCategory extends CustomListCategory {
* @param panelConfigs Configuration to be saved
*/
private void saveHomeConfig() {
if (mPanelConfigs == null) {
return;
}
final List<PanelConfig> panelConfigs = makeConfigListDeepCopy();
mSaveTask = new UiAsyncTask<Void, Void, Void>(ThreadUtils.getBackgroundHandler()) {
@Override

View File

@ -25,6 +25,7 @@ const XRE_APP_DISTRIBUTION_DIR = "XREAppDist";
const XRE_UPDATE_ROOT_DIR = "UpdRootD";
const ENVVAR_UPDATE_DIR = "UPDATES_DIRECTORY";
const WEBAPPS_DIR = "webappsDir";
const DOWNLOAD_DIR = "DfltDwnld"
const SYSTEM_DIST_PATH = "/system/@ANDROID_PACKAGE_NAME@/distribution";
@ -73,6 +74,9 @@ DirectoryProvider.prototype = {
}
let dm = Cc["@mozilla.org/download-manager;1"].getService(Ci.nsIDownloadManager);
return dm.defaultDownloadsDirectory;
} else if (prop == DOWNLOAD_DIR) {
let dm = Cc["@mozilla.org/download-manager;1"].getService(Ci.nsIDownloadManager);
return dm.defaultDownloadsDirectory;
}
// We are retuning null to show failure instead for throwing an error. The

View File

@ -23,6 +23,7 @@ FilePicker.prototype = {
_filePath: null,
_promptActive: false,
_filterIndex: 0,
_addToRecentDocs: false,
init: function(aParent, aTitle, aMode) {
this._domWin = aParent;
@ -150,11 +151,11 @@ FilePicker.prototype = {
},
get addToRecentDocs() {
throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
return this._addToRecentDocs;
},
set addToRecentDocs(val) {
throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
this._addToRecentDocs = val;
},
get mode() {

View File

@ -166,16 +166,27 @@ let HomePanels = {
// Holds the currrent set of registered panels.
_panels: {},
_handleGet: function(requestId) {
_panelToJSON : function(panel) {
return {
id: panel.id,
title: panel.title,
layout: panel.layout,
views: panel.views
};
},
_handleGet: function(data) {
let requestId = data.requestId;
let ids = data.ids || null;
let panels = [];
for (let id in this._panels) {
let panel = this._panels[id];
panels.push({
id: panel.id,
title: panel.title,
layout: panel.layout,
views: panel.views
});
// Null ids means we want to fetch all available panels
if (ids == null || ids.indexOf(panel.id) >= 0) {
panels.push(this._panelToJSON(panel));
}
}
sendMessageToJava({
@ -211,14 +222,26 @@ let HomePanels = {
}
this._panels[panel.id] = panel;
if (options.autoInstall) {
sendMessageToJava({
type: "HomePanels:Install",
panel: this._panelToJSON(panel)
});
}
},
remove: function(id) {
if (!(id in this._panels)) {
throw "Home.panels: Panel doesn't exist: id = " + id;
}
let panel = this._panels[id];
delete this._panels[id];
sendMessageToJava({
type: "HomePanels:Remove",
id: id
panel: this._panelToJSON(panel)
});
},
@ -242,7 +265,7 @@ this.Home = {
observe: function(subject, topic, data) {
switch(topic) {
case "HomePanels:Get":
HomePanels._handleGet(data);
HomePanels._handleGet(JSON.parse(data));
break;
}
}

View File

@ -9,6 +9,7 @@ const Cu = Components.utils;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/FileUtils.jsm");
Cu.import("resource://gre/modules/Promise.jsm");
Cu.import("resource://services-sync/util.js");
const SYNC_PREFS_BRANCH = "services.sync.";
@ -25,13 +26,25 @@ const SYNC_PREFS_BRANCH = "services.sync.";
*
* If Sync is not configured, no extra Sync code is loaded. If an
* external component (say the UI) needs to interact with Sync, it
* should do something like the following:
* should use the promise-base function whenLoaded() - something like the
* following:
*
* // 1. Grab a handle to the Sync XPCOM service.
* let service = Cc["@mozilla.org/weave/service;1"]
* .getService(Components.interfaces.nsISupports)
* .wrappedJSObject;
*
* // 2. Use the .then method of the promise.
* service.whenLoaded().then(() => {
* // You are free to interact with "Weave." objects.
* return;
* });
*
* And that's it! However, if you really want to avoid promises and do it
* old-school, then
*
* // 1. Get a reference to the service as done in (1) above.
*
* // 2. Check if the service has been initialized.
* if (service.ready) {
* // You are free to interact with "Weave." objects.
@ -65,6 +78,20 @@ WeaveService.prototype = {
Weave.Service;
},
whenLoaded: function() {
if (this.ready) {
return Promise.resolve();
}
let deferred = Promise.defer();
Services.obs.addObserver(function onReady() {
Services.obs.removeObserver(onReady, "weave:service:ready");
deferred.resolve();
}, "weave:service:ready", false);
this.ensureLoaded();
return deferred.promise;
},
get fxAccountsEnabled() {
// work out what identity manager to use. This is stored in a preference;
// if the preference exists, we trust it.

View File

@ -164,6 +164,7 @@ this.BrowserIDManager.prototype = {
},
observe: function (subject, topic, data) {
this._log.debug("observed " + topic);
switch (topic) {
case fxAccountsCommon.ONLOGIN_NOTIFICATION:
this.initializeWithCurrentIdentity(true);
@ -409,15 +410,17 @@ this.BrowserIDManager.prototype = {
// Both Jelly and FxAccounts give us kB as hex
let kBbytes = CommonUtils.hexToBytes(userData.kB);
let headers = {"X-Client-State": this._computeXClientState(kBbytes)};
log.info("Fetching Sync token from: " + tokenServerURI);
log.info("Fetching assertion and token from: " + tokenServerURI);
function getToken(tokenServerURI, assertion) {
log.debug("Getting a token");
let deferred = Promise.defer();
let cb = function (err, token) {
if (err) {
log.info("TokenServerClient.getTokenFromBrowserIDAssertion() failed with: " + err.message);
return deferred.reject(new AuthenticationError(err.message));
} else {
log.debug("Successfully got a sync token");
return deferred.resolve(token);
}
};
@ -427,6 +430,7 @@ this.BrowserIDManager.prototype = {
}
function getAssertion() {
log.debug("Getting an assertion");
let audience = Services.io.newURI(tokenServerURI, null, null).prePath;
return fxAccounts.getAssertion(audience).then(null, err => {
if (err.code === 401) {

View File

@ -1844,26 +1844,39 @@ function appendSectionHeader(aP, aText)
function saveReportsToFile()
{
let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
fp.init(window, "Save Memory Reports", Ci.nsIFilePicker.modeSave);
fp.appendFilter("Zipped JSON files", "*.json.gz");
fp.appendFilters(Ci.nsIFilePicker.filterAll);
fp.filterIndex = 0;
fp.addToRecentDocs = true;
fp.defaultString = "memory-report.json.gz";
let fpFinish = function(file) {
let dumper = Cc["@mozilla.org/memory-info-dumper;1"]
.getService(Ci.nsIMemoryInfoDumper);
let finishDumping = () => {
updateMainAndFooter("Saved reports to " + file.path, HIDE_FOOTER);
}
dumper.dumpMemoryReportsToNamedFile(file.path, finishDumping, null);
}
let fpCallback = function(aResult) {
if (aResult == Ci.nsIFilePicker.returnOK ||
aResult == Ci.nsIFilePicker.returnReplace) {
let dumper = Cc["@mozilla.org/memory-info-dumper;1"]
.getService(Ci.nsIMemoryInfoDumper);
let finishDumping = () => {
updateMainAndFooter("Saved reports to " + fp.file.path, HIDE_FOOTER);
}
dumper.dumpMemoryReportsToNamedFile(fp.file.path, finishDumping, null);
fpFinish(fp.file);
}
};
try {
fp.init(window, "Save Memory Reports", Ci.nsIFilePicker.modeSave);
} catch(ex) {
// This will fail on Android, since there is no Save as file picker there.
// Just save to the default downloads dir if it does.
let file = Services.dirsvc.get("DfltDwnld", Ci.nsIFile);
file.append(fp.defaultString);
fpFinish(file);
return;
}
fp.open(fpCallback);
}

View File

@ -22,11 +22,11 @@
</stack>
<panel id="panel" type="arrow" onpopupshown="checkPanelPosition(this)" onpopuphidden="runNextTest.next()">
<label id="panellabel" value="This is some text." height="65"/>
<label id="panellabel" value="This is some text..." height="65"/>
</panel>
<panel id="bigpanel" type="arrow" onpopupshown="checkBigPanel(this)" onpopuphidden="runNextTest.next()">
<button label="This is some text." height="3000"/>
<button label="This is some text..." height="3000"/>
</panel>
<script type="application/javascript">

View File

@ -20,14 +20,14 @@
// [dammit]: acorn_loose.js
// [walk]: util/walk.js
(function(mod) {
(function(root, mod) {
if (typeof exports == "object" && typeof module == "object") return mod(exports); // CommonJS
if (typeof define == "function" && define.amd) return define(["exports"], mod); // AMD
mod(self.acorn || (self.acorn = {})); // Plain browser env
})(function(exports) {
mod(root.acorn || (root.acorn = {})); // Plain browser env
})(this, function(exports) {
"use strict";
exports.version = "0.1.01";
exports.version = "0.4.1";
// The main exported interface (under `self.acorn` when in the
// browser) is a `parse` function that takes a code string and
@ -77,7 +77,8 @@
// character offsets that denote the start and end of the comment.
// When the `locations` option is on, two more parameters are
// passed, the full `{line, column}` locations of the start and
// end of the comments.
// end of the comments. Note that you are not allowed to call the
// parser from the callback—that will corrupt its internal state.
onComment: null,
// Nodes have their start and end characters offsets recorded in
// `start` and `end` properties (directly on the node, rather than
@ -94,14 +95,17 @@
// toplevel forms of the parsed file to the `Program` (top) node
// of an existing parse tree.
program: null,
// When `location` is on, you can pass this to record the source
// When `locations` is on, you can pass this to record the source
// file in every node's `loc` object.
sourceFile: null
sourceFile: null,
// This value, if given, is stored in every node, whether
// `locations` is on or off.
directSourceFile: null
};
function setOptions(opts) {
options = opts || {};
for (var opt in defaultOptions) if (!options.hasOwnProperty(opt))
for (var opt in defaultOptions) if (!Object.prototype.hasOwnProperty.call(options, opt))
options[opt] = defaultOptions[opt];
sourceFile = options.sourceFile || null;
}
@ -138,6 +142,7 @@
var t = {};
function getToken(forceRegexp) {
lastEnd = tokEnd;
readToken(forceRegexp);
t.start = tokStart; t.end = tokEnd;
t.startLoc = tokStartLoc; t.endLoc = tokEndLoc;
@ -147,14 +152,14 @@
getToken.jumpTo = function(pos, reAllowed) {
tokPos = pos;
if (options.locations) {
tokCurLine = tokLineStart = lineBreak.lastIndex = 0;
tokCurLine = 1;
tokLineStart = lineBreak.lastIndex = 0;
var match;
while ((match = lineBreak.exec(input)) && match.index < pos) {
++tokCurLine;
tokLineStart = match.index + match[0].length;
}
}
var ch = input.charAt(pos - 1);
tokRegexpAllowed = reAllowed;
skipSpace();
};
@ -228,6 +233,10 @@
throw err;
}
// Reused empty array added for node fields that are always empty.
var empty = [];
// ## Token types
// The assignment of fine-grained, information-carrying type objects
@ -313,13 +322,18 @@
// in AssignmentExpression nodes.
var _slash = {binop: 10, beforeExpr: true}, _eq = {isAssign: true, beforeExpr: true};
var _assign = {isAssign: true, beforeExpr: true}, _plusmin = {binop: 9, prefix: true, beforeExpr: true};
var _incdec = {postfix: true, prefix: true, isUpdate: true}, _prefix = {prefix: true, beforeExpr: true};
var _bin1 = {binop: 1, beforeExpr: true}, _bin2 = {binop: 2, beforeExpr: true};
var _bin3 = {binop: 3, beforeExpr: true}, _bin4 = {binop: 4, beforeExpr: true};
var _bin5 = {binop: 5, beforeExpr: true}, _bin6 = {binop: 6, beforeExpr: true};
var _bin7 = {binop: 7, beforeExpr: true}, _bin8 = {binop: 8, beforeExpr: true};
var _bin10 = {binop: 10, beforeExpr: true};
var _assign = {isAssign: true, beforeExpr: true};
var _incDec = {postfix: true, prefix: true, isUpdate: true}, _prefix = {prefix: true, beforeExpr: true};
var _logicalOR = {binop: 1, beforeExpr: true};
var _logicalAND = {binop: 2, beforeExpr: true};
var _bitwiseOR = {binop: 3, beforeExpr: true};
var _bitwiseXOR = {binop: 4, beforeExpr: true};
var _bitwiseAND = {binop: 5, beforeExpr: true};
var _equality = {binop: 6, beforeExpr: true};
var _relational = {binop: 7, beforeExpr: true};
var _bitShift = {binop: 8, beforeExpr: true};
var _plusMin = {binop: 9, prefix: true, beforeExpr: true};
var _multiplyModulo = {binop: 10, beforeExpr: true};
// Provide access to the token types for external users of the
// tokenizer.
@ -328,7 +342,7 @@
parenL: _parenL, parenR: _parenR, comma: _comma, semi: _semi, colon: _colon,
dot: _dot, question: _question, slash: _slash, eq: _eq, name: _name, eof: _eof,
num: _num, regexp: _regexp, string: _string};
for (var kw in keywordTypes) exports.tokTypes[kw] = keywordTypes[kw];
for (var kw in keywordTypes) exports.tokTypes["_" + kw] = keywordTypes[kw];
// This is a trick taken from Esprima. It turns out that, on
// non-Chrome browsers, to check whether a string is in a set, a
@ -405,9 +419,9 @@
// are only applied when a character is found to actually have a
// code point above 128.
var nonASCIIwhitespace = /[\u1680\u180e\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]/;
var nonASCIIwhitespace = /[\u1680\u180e\u2000-\u200a\u202f\u205f\u3000\ufeff]/;
var nonASCIIidentifierStartChars = "\xaa\xb5\xba\xc0-\xd6\xd8-\xf6\xf8-\u02c1\u02c6-\u02d1\u02e0-\u02e4\u02ec\u02ee\u0370-\u0374\u0376\u0377\u037a-\u037d\u0386\u0388-\u038a\u038c\u038e-\u03a1\u03a3-\u03f5\u03f7-\u0481\u048a-\u0527\u0531-\u0556\u0559\u0561-\u0587\u05d0-\u05ea\u05f0-\u05f2\u0620-\u064a\u066e\u066f\u0671-\u06d3\u06d5\u06e5\u06e6\u06ee\u06ef\u06fa-\u06fc\u06ff\u0710\u0712-\u072f\u074d-\u07a5\u07b1\u07ca-\u07ea\u07f4\u07f5\u07fa\u0800-\u0815\u081a\u0824\u0828\u0840-\u0858\u08a0\u08a2-\u08ac\u0904-\u0939\u093d\u0950\u0958-\u0961\u0971-\u0977\u0979-\u097f\u0985-\u098c\u098f\u0990\u0993-\u09a8\u09aa-\u09b0\u09b2\u09b6-\u09b9\u09bd\u09ce\u09dc\u09dd\u09df-\u09e1\u09f0\u09f1\u0a05-\u0a0a\u0a0f\u0a10\u0a13-\u0a28\u0a2a-\u0a30\u0a32\u0a33\u0a35\u0a36\u0a38\u0a39\u0a59-\u0a5c\u0a5e\u0a72-\u0a74\u0a85-\u0a8d\u0a8f-\u0a91\u0a93-\u0aa8\u0aaa-\u0ab0\u0ab2\u0ab3\u0ab5-\u0ab9\u0abd\u0ad0\u0ae0\u0ae1\u0b05-\u0b0c\u0b0f\u0b10\u0b13-\u0b28\u0b2a-\u0b30\u0b32\u0b33\u0b35-\u0b39\u0b3d\u0b5c\u0b5d\u0b5f-\u0b61\u0b71\u0b83\u0b85-\u0b8a\u0b8e-\u0b90\u0b92-\u0b95\u0b99\u0b9a\u0b9c\u0b9e\u0b9f\u0ba3\u0ba4\u0ba8-\u0baa\u0bae-\u0bb9\u0bd0\u0c05-\u0c0c\u0c0e-\u0c10\u0c12-\u0c28\u0c2a-\u0c33\u0c35-\u0c39\u0c3d\u0c58\u0c59\u0c60\u0c61\u0c85-\u0c8c\u0c8e-\u0c90\u0c92-\u0ca8\u0caa-\u0cb3\u0cb5-\u0cb9\u0cbd\u0cde\u0ce0\u0ce1\u0cf1\u0cf2\u0d05-\u0d0c\u0d0e-\u0d10\u0d12-\u0d3a\u0d3d\u0d4e\u0d60\u0d61\u0d7a-\u0d7f\u0d85-\u0d96\u0d9a-\u0db1\u0db3-\u0dbb\u0dbd\u0dc0-\u0dc6\u0e01-\u0e30\u0e32\u0e33\u0e40-\u0e46\u0e81\u0e82\u0e84\u0e87\u0e88\u0e8a\u0e8d\u0e94-\u0e97\u0e99-\u0e9f\u0ea1-\u0ea3\u0ea5\u0ea7\u0eaa\u0eab\u0ead-\u0eb0\u0eb2\u0eb3\u0ebd\u0ec0-\u0ec4\u0ec6\u0edc-\u0edf\u0f00\u0f40-\u0f47\u0f49-\u0f6c\u0f88-\u0f8c\u1000-\u102a\u103f\u1050-\u1055\u105a-\u105d\u1061\u1065\u1066\u106e-\u1070\u1075-\u1081\u108e\u10a0-\u10c5\u10c7\u10cd\u10d0-\u10fa\u10fc-\u1248\u124a-\u124d\u1250-\u1256\u1258\u125a-\u125d\u1260-\u1288\u128a-\u128d\u1290-\u12b0\u12b2-\u12b5\u12b8-\u12be\u12c0\u12c2-\u12c5\u12c8-\u12d6\u12d8-\u1310\u1312-\u1315\u1318-\u135a\u1380-\u138f\u13a0-\u13f4\u1401-\u166c\u166f-\u167f\u1681-\u169a\u16a0-\u16ea\u16ee-\u16f0\u1700-\u170c\u170e-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176c\u176e-\u1770\u1780-\u17b3\u17d7\u17dc\u1820-\u1877\u1880-\u18a8\u18aa\u18b0-\u18f5\u1900-\u191c\u1950-\u196d\u1970-\u1974\u1980-\u19ab\u19c1-\u19c7\u1a00-\u1a16\u1a20-\u1a54\u1aa7\u1b05-\u1b33\u1b45-\u1b4b\u1b83-\u1ba0\u1bae\u1baf\u1bba-\u1be5\u1c00-\u1c23\u1c4d-\u1c4f\u1c5a-\u1c7d\u1ce9-\u1cec\u1cee-\u1cf1\u1cf5\u1cf6\u1d00-\u1dbf\u1e00-\u1f15\u1f18-\u1f1d\u1f20-\u1f45\u1f48-\u1f4d\u1f50-\u1f57\u1f59\u1f5b\u1f5d\u1f5f-\u1f7d\u1f80-\u1fb4\u1fb6-\u1fbc\u1fbe\u1fc2-\u1fc4\u1fc6-\u1fcc\u1fd0-\u1fd3\u1fd6-\u1fdb\u1fe0-\u1fec\u1ff2-\u1ff4\u1ff6-\u1ffc\u2071\u207f\u2090-\u209c\u2102\u2107\u210a-\u2113\u2115\u2119-\u211d\u2124\u2126\u2128\u212a-\u212d\u212f-\u2139\u213c-\u213f\u2145-\u2149\u214e\u2160-\u2188\u2c00-\u2c2e\u2c30-\u2c5e\u2c60-\u2ce4\u2ceb-\u2cee\u2cf2\u2cf3\u2d00-\u2d25\u2d27\u2d2d\u2d30-\u2d67\u2d6f\u2d80-\u2d96\u2da0-\u2da6\u2da8-\u2dae\u2db0-\u2db6\u2db8-\u2dbe\u2dc0-\u2dc6\u2dc8-\u2dce\u2dd0-\u2dd6\u2dd8-\u2dde\u2e2f\u3005-\u3007\u3021-\u3029\u3031-\u3035\u3038-\u303c\u3041-\u3096\u309d-\u309f\u30a1-\u30fa\u30fc-\u30ff\u3105-\u312d\u3131-\u318e\u31a0-\u31ba\u31f0-\u31ff\u3400-\u4db5\u4e00-\u9fcc\ua000-\ua48c\ua4d0-\ua4fd\ua500-\ua60c\ua610-\ua61f\ua62a\ua62b\ua640-\ua66e\ua67f-\ua697\ua6a0-\ua6ef\ua717-\ua71f\ua722-\ua788\ua78b-\ua78e\ua790-\ua793\ua7a0-\ua7aa\ua7f8-\ua801\ua803-\ua805\ua807-\ua80a\ua80c-\ua822\ua840-\ua873\ua882-\ua8b3\ua8f2-\ua8f7\ua8fb\ua90a-\ua925\ua930-\ua946\ua960-\ua97c\ua984-\ua9b2\ua9cf\uaa00-\uaa28\uaa40-\uaa42\uaa44-\uaa4b\uaa60-\uaa76\uaa7a\uaa80-\uaaaf\uaab1\uaab5\uaab6\uaab9-\uaabd\uaac0\uaac2\uaadb-\uaadd\uaae0-\uaaea\uaaf2-\uaaf4\uab01-\uab06\uab09-\uab0e\uab11-\uab16\uab20-\uab26\uab28-\uab2e\uabc0-\uabe2\uac00-\ud7a3\ud7b0-\ud7c6\ud7cb-\ud7fb\uf900-\ufa6d\ufa70-\ufad9\ufb00-\ufb06\ufb13-\ufb17\ufb1d\ufb1f-\ufb28\ufb2a-\ufb36\ufb38-\ufb3c\ufb3e\ufb40\ufb41\ufb43\ufb44\ufb46-\ufbb1\ufbd3-\ufd3d\ufd50-\ufd8f\ufd92-\ufdc7\ufdf0-\ufdfb\ufe70-\ufe74\ufe76-\ufefc\uff21-\uff3a\uff41-\uff5a\uff66-\uffbe\uffc2-\uffc7\uffca-\uffcf\uffd2-\uffd7\uffda-\uffdc";
var nonASCIIidentifierChars = "\u0371-\u0374\u0483-\u0487\u0591-\u05bd\u05bf\u05c1\u05c2\u05c4\u05c5\u05c7\u0610-\u061a\u0620-\u0649\u0672-\u06d3\u06e7-\u06e8\u06fb-\u06fc\u0730-\u074a\u0800-\u0814\u081b-\u0823\u0825-\u0827\u0829-\u082d\u0840-\u0857\u08e4-\u08fe\u0900-\u0903\u093a-\u093c\u093e-\u094f\u0951-\u0957\u0962-\u0963\u0966-\u096f\u0981-\u0983\u09bc\u09be-\u09c4\u09c7\u09c8\u09d7\u09df-\u09e0\u0a01-\u0a03\u0a3c\u0a3e-\u0a42\u0a47\u0a48\u0a4b-\u0a4d\u0a51\u0a66-\u0a71\u0a75\u0a81-\u0a83\u0abc\u0abe-\u0ac5\u0ac7-\u0ac9\u0acb-\u0acd\u0ae2-\u0ae3\u0ae6-\u0aef\u0b01-\u0b03\u0b3c\u0b3e-\u0b44\u0b47\u0b48\u0b4b-\u0b4d\u0b56\u0b57\u0b5f-\u0b60\u0b66-\u0b6f\u0b82\u0bbe-\u0bc2\u0bc6-\u0bc8\u0bca-\u0bcd\u0bd7\u0be6-\u0bef\u0c01-\u0c03\u0c46-\u0c48\u0c4a-\u0c4d\u0c55\u0c56\u0c62-\u0c63\u0c66-\u0c6f\u0c82\u0c83\u0cbc\u0cbe-\u0cc4\u0cc6-\u0cc8\u0cca-\u0ccd\u0cd5\u0cd6\u0ce2-\u0ce3\u0ce6-\u0cef\u0d02\u0d03\u0d46-\u0d48\u0d57\u0d62-\u0d63\u0d66-\u0d6f\u0d82\u0d83\u0dca\u0dcf-\u0dd4\u0dd6\u0dd8-\u0ddf\u0df2\u0df3\u0e34-\u0e3a\u0e40-\u0e45\u0e50-\u0e59\u0eb4-\u0eb9\u0ec8-\u0ecd\u0ed0-\u0ed9\u0f18\u0f19\u0f20-\u0f29\u0f35\u0f37\u0f39\u0f41-\u0f47\u0f71-\u0f84\u0f86-\u0f87\u0f8d-\u0f97\u0f99-\u0fbc\u0fc6\u1000-\u1029\u1040-\u1049\u1067-\u106d\u1071-\u1074\u1082-\u108d\u108f-\u109d\u135d-\u135f\u170e-\u1710\u1720-\u1730\u1740-\u1750\u1772\u1773\u1780-\u17b2\u17dd\u17e0-\u17e9\u180b-\u180d\u1810-\u1819\u1920-\u192b\u1930-\u193b\u1951-\u196d\u19b0-\u19c0\u19c8-\u19c9\u19d0-\u19d9\u1a00-\u1a15\u1a20-\u1a53\u1a60-\u1a7c\u1a7f-\u1a89\u1a90-\u1a99\u1b46-\u1b4b\u1b50-\u1b59\u1b6b-\u1b73\u1bb0-\u1bb9\u1be6-\u1bf3\u1c00-\u1c22\u1c40-\u1c49\u1c5b-\u1c7d\u1cd0-\u1cd2\u1d00-\u1dbe\u1e01-\u1f15\u200c\u200d\u203f\u2040\u2054\u20d0-\u20dc\u20e1\u20e5-\u20f0\u2d81-\u2d96\u2de0-\u2dff\u3021-\u3028\u3099\u309a\ua640-\ua66d\ua674-\ua67d\ua69f\ua6f0-\ua6f1\ua7f8-\ua800\ua806\ua80b\ua823-\ua827\ua880-\ua881\ua8b4-\ua8c4\ua8d0-\ua8d9\ua8f3-\ua8f7\ua900-\ua909\ua926-\ua92d\ua930-\ua945\ua980-\ua983\ua9b3-\ua9c0\uaa00-\uaa27\uaa40-\uaa41\uaa4c-\uaa4d\uaa50-\uaa59\uaa7b\uaae0-\uaae9\uaaf2-\uaaf3\uabc0-\uabe1\uabec\uabed\uabf0-\uabf9\ufb20-\ufb28\ufe00-\ufe0f\ufe20-\ufe26\ufe33\ufe34\ufe4d-\ufe4f\uff10-\uff19\uff3f";
var nonASCIIidentifierChars = "\u0300-\u036f\u0483-\u0487\u0591-\u05bd\u05bf\u05c1\u05c2\u05c4\u05c5\u05c7\u0610-\u061a\u0620-\u0649\u0672-\u06d3\u06e7-\u06e8\u06fb-\u06fc\u0730-\u074a\u0800-\u0814\u081b-\u0823\u0825-\u0827\u0829-\u082d\u0840-\u0857\u08e4-\u08fe\u0900-\u0903\u093a-\u093c\u093e-\u094f\u0951-\u0957\u0962-\u0963\u0966-\u096f\u0981-\u0983\u09bc\u09be-\u09c4\u09c7\u09c8\u09d7\u09df-\u09e0\u0a01-\u0a03\u0a3c\u0a3e-\u0a42\u0a47\u0a48\u0a4b-\u0a4d\u0a51\u0a66-\u0a71\u0a75\u0a81-\u0a83\u0abc\u0abe-\u0ac5\u0ac7-\u0ac9\u0acb-\u0acd\u0ae2-\u0ae3\u0ae6-\u0aef\u0b01-\u0b03\u0b3c\u0b3e-\u0b44\u0b47\u0b48\u0b4b-\u0b4d\u0b56\u0b57\u0b5f-\u0b60\u0b66-\u0b6f\u0b82\u0bbe-\u0bc2\u0bc6-\u0bc8\u0bca-\u0bcd\u0bd7\u0be6-\u0bef\u0c01-\u0c03\u0c46-\u0c48\u0c4a-\u0c4d\u0c55\u0c56\u0c62-\u0c63\u0c66-\u0c6f\u0c82\u0c83\u0cbc\u0cbe-\u0cc4\u0cc6-\u0cc8\u0cca-\u0ccd\u0cd5\u0cd6\u0ce2-\u0ce3\u0ce6-\u0cef\u0d02\u0d03\u0d46-\u0d48\u0d57\u0d62-\u0d63\u0d66-\u0d6f\u0d82\u0d83\u0dca\u0dcf-\u0dd4\u0dd6\u0dd8-\u0ddf\u0df2\u0df3\u0e34-\u0e3a\u0e40-\u0e45\u0e50-\u0e59\u0eb4-\u0eb9\u0ec8-\u0ecd\u0ed0-\u0ed9\u0f18\u0f19\u0f20-\u0f29\u0f35\u0f37\u0f39\u0f41-\u0f47\u0f71-\u0f84\u0f86-\u0f87\u0f8d-\u0f97\u0f99-\u0fbc\u0fc6\u1000-\u1029\u1040-\u1049\u1067-\u106d\u1071-\u1074\u1082-\u108d\u108f-\u109d\u135d-\u135f\u170e-\u1710\u1720-\u1730\u1740-\u1750\u1772\u1773\u1780-\u17b2\u17dd\u17e0-\u17e9\u180b-\u180d\u1810-\u1819\u1920-\u192b\u1930-\u193b\u1951-\u196d\u19b0-\u19c0\u19c8-\u19c9\u19d0-\u19d9\u1a00-\u1a15\u1a20-\u1a53\u1a60-\u1a7c\u1a7f-\u1a89\u1a90-\u1a99\u1b46-\u1b4b\u1b50-\u1b59\u1b6b-\u1b73\u1bb0-\u1bb9\u1be6-\u1bf3\u1c00-\u1c22\u1c40-\u1c49\u1c5b-\u1c7d\u1cd0-\u1cd2\u1d00-\u1dbe\u1e01-\u1f15\u200c\u200d\u203f\u2040\u2054\u20d0-\u20dc\u20e1\u20e5-\u20f0\u2d81-\u2d96\u2de0-\u2dff\u3021-\u3028\u3099\u309a\ua640-\ua66d\ua674-\ua67d\ua69f\ua6f0-\ua6f1\ua7f8-\ua800\ua806\ua80b\ua823-\ua827\ua880-\ua881\ua8b4-\ua8c4\ua8d0-\ua8d9\ua8f3-\ua8f7\ua900-\ua909\ua926-\ua92d\ua930-\ua945\ua980-\ua983\ua9b3-\ua9c0\uaa00-\uaa27\uaa40-\uaa41\uaa4c-\uaa4d\uaa50-\uaa59\uaa7b\uaae0-\uaae9\uaaf2-\uaaf3\uabc0-\uabe1\uabec\uabed\uabf0-\uabf9\ufb20-\ufb28\ufe00-\ufe0f\ufe20-\ufe26\ufe33\ufe34\ufe4d-\ufe4f\uff10-\uff19\uff3f";
var nonASCIIidentifierStart = new RegExp("[" + nonASCIIidentifierStartChars + "]");
var nonASCIIidentifier = new RegExp("[" + nonASCIIidentifierStartChars + nonASCIIidentifierChars + "]");
@ -422,17 +436,17 @@
// Test whether a given character code starts an identifier.
function isIdentifierStart(code) {
var isIdentifierStart = exports.isIdentifierStart = function(code) {
if (code < 65) return code === 36;
if (code < 91) return true;
if (code < 97) return code === 95;
if (code < 123)return true;
return code >= 0xaa && nonASCIIidentifierStart.test(String.fromCharCode(code));
}
};
// Test whether a given character is part of an identifier.
function isIdentifierChar(code) {
var isIdentifierChar = exports.isIdentifierChar = function(code) {
if (code < 48) return code === 36;
if (code < 58) return true;
if (code < 65) return false;
@ -440,7 +454,7 @@
if (code < 97) return code === 95;
if (code < 123)return true;
return code >= 0xaa && nonASCIIidentifier.test(String.fromCharCode(code));
}
};
// ## Tokenizer
@ -496,7 +510,7 @@
var start = tokPos;
var startLoc = options.onComment && options.locations && new line_loc_t;
var ch = input.charCodeAt(tokPos+=2);
while (tokPos < inputLen && ch !== 10 && ch !== 13 && ch !== 8232 && ch !== 8329) {
while (tokPos < inputLen && ch !== 10 && ch !== 13 && ch !== 8232 && ch !== 8233) {
++tokPos;
ch = input.charCodeAt(tokPos);
}
@ -513,30 +527,32 @@
var ch = input.charCodeAt(tokPos);
if (ch === 32) { // ' '
++tokPos;
} else if(ch === 13) {
} else if (ch === 13) {
++tokPos;
var next = input.charCodeAt(tokPos);
if(next === 10) {
if (next === 10) {
++tokPos;
}
if(options.locations) {
if (options.locations) {
++tokCurLine;
tokLineStart = tokPos;
}
} else if (ch === 10) {
} else if (ch === 10 || ch === 8232 || ch === 8233) {
++tokPos;
++tokCurLine;
tokLineStart = tokPos;
} else if(ch < 14 && ch > 8) {
if (options.locations) {
++tokCurLine;
tokLineStart = tokPos;
}
} else if (ch > 8 && ch < 14) {
++tokPos;
} else if (ch === 47) { // '/'
var next = input.charCodeAt(tokPos+1);
var next = input.charCodeAt(tokPos + 1);
if (next === 42) { // '*'
skipBlockComment();
} else if (next === 47) { // '/'
skipLineComment();
} else break;
} else if ((ch < 14 && ch > 8) || ch === 32 || ch === 160) { // ' ', '\xa0'
} else if (ch === 160) { // '\xa0'
++tokPos;
} else if (ch >= 5760 && nonASCIIwhitespace.test(String.fromCharCode(ch))) {
++tokPos;
@ -559,61 +575,79 @@
// `tokRegexpAllowed` trick does not work. See `parseStatement`.
function readToken_dot() {
var next = input.charCodeAt(tokPos+1);
var next = input.charCodeAt(tokPos + 1);
if (next >= 48 && next <= 57) return readNumber(true);
++tokPos;
return finishToken(_dot);
}
function readToken_slash() { // '/'
var next = input.charCodeAt(tokPos+1);
var next = input.charCodeAt(tokPos + 1);
if (tokRegexpAllowed) {++tokPos; return readRegexp();}
if (next === 61) return finishOp(_assign, 2);
return finishOp(_slash, 1);
}
function readToken_mult_modulo() { // '%*'
var next = input.charCodeAt(tokPos+1);
var next = input.charCodeAt(tokPos + 1);
if (next === 61) return finishOp(_assign, 2);
return finishOp(_bin10, 1);
return finishOp(_multiplyModulo, 1);
}
function readToken_pipe_amp(code) { // '|&'
var next = input.charCodeAt(tokPos+1);
if (next === code) return finishOp(code === 124 ? _bin1 : _bin2, 2);
var next = input.charCodeAt(tokPos + 1);
if (next === code) return finishOp(code === 124 ? _logicalOR : _logicalAND, 2);
if (next === 61) return finishOp(_assign, 2);
return finishOp(code === 124 ? _bin3 : _bin5, 1);
return finishOp(code === 124 ? _bitwiseOR : _bitwiseAND, 1);
}
function readToken_caret() { // '^'
var next = input.charCodeAt(tokPos+1);
var next = input.charCodeAt(tokPos + 1);
if (next === 61) return finishOp(_assign, 2);
return finishOp(_bin4, 1);
return finishOp(_bitwiseXOR, 1);
}
function readToken_plus_min(code) { // '+-'
var next = input.charCodeAt(tokPos+1);
if (next === code) return finishOp(_incdec, 2);
var next = input.charCodeAt(tokPos + 1);
if (next === code) {
if (next == 45 && input.charCodeAt(tokPos + 2) == 62 &&
newline.test(input.slice(lastEnd, tokPos))) {
// A `-->` line comment
tokPos += 3;
skipLineComment();
skipSpace();
return readToken();
}
return finishOp(_incDec, 2);
}
if (next === 61) return finishOp(_assign, 2);
return finishOp(_plusmin, 1);
return finishOp(_plusMin, 1);
}
function readToken_lt_gt(code) { // '<>'
var next = input.charCodeAt(tokPos+1);
var next = input.charCodeAt(tokPos + 1);
var size = 1;
if (next === code) {
size = code === 62 && input.charCodeAt(tokPos+2) === 62 ? 3 : 2;
size = code === 62 && input.charCodeAt(tokPos + 2) === 62 ? 3 : 2;
if (input.charCodeAt(tokPos + size) === 61) return finishOp(_assign, size + 1);
return finishOp(_bin8, size);
return finishOp(_bitShift, size);
}
if (next == 33 && code == 60 && input.charCodeAt(tokPos + 2) == 45 &&
input.charCodeAt(tokPos + 3) == 45) {
// `<!--`, an XML-style comment that should be interpreted as a line comment
tokPos += 4;
skipLineComment();
skipSpace();
return readToken();
}
if (next === 61)
size = input.charCodeAt(tokPos+2) === 61 ? 3 : 2;
return finishOp(_bin7, size);
size = input.charCodeAt(tokPos + 2) === 61 ? 3 : 2;
return finishOp(_relational, size);
}
function readToken_eq_excl(code) { // '=!'
var next = input.charCodeAt(tokPos+1);
if (next === 61) return finishOp(_bin6, input.charCodeAt(tokPos+2) === 61 ? 3 : 2);
var next = input.charCodeAt(tokPos + 1);
if (next === 61) return finishOp(_equality, input.charCodeAt(tokPos + 2) === 61 ? 3 : 2);
return finishOp(code === 61 ? _eq : _prefix, 1);
}
@ -638,7 +672,7 @@
// '0x' is a hexadecimal number.
case 48: // '0'
var next = input.charCodeAt(tokPos+1);
var next = input.charCodeAt(tokPos + 1);
if (next === 120 || next === 88) return readHexNumber();
// Anything else beginning with a digit is an integer, octal
// number, or float.
@ -693,7 +727,7 @@
// Identifier or keyword. '\uXXXX' sequences are allowed in
// identifiers, so '\' also dispatches to that.
if (isIdentifierStart(code) || code === 92 /* '\' */) return readWord();
var tok = getTokenFromCode(code);
if (tok === false) {
@ -702,7 +736,7 @@
var ch = String.fromCharCode(code);
if (ch === "\\" || nonASCIIidentifierStart.test(ch)) return readWord();
raise(tokPos, "Unexpected character '" + ch + "'");
}
}
return tok;
}
@ -735,7 +769,13 @@
// here (don't ask).
var mods = readWord1();
if (mods && !/^[gmsiy]*$/.test(mods)) raise(start, "Invalid regexp flag");
return finishToken(_regexp, new RegExp(content, mods));
try {
var value = new RegExp(content, mods);
} catch (e) {
if (e instanceof SyntaxError) raise(start, e.message);
raise(e);
}
return finishToken(_regexp, value);
}
// Read an integer in the given radix. Return null if zero digits
@ -768,7 +808,7 @@
}
// Read an integer, octal integer, or floating-point number.
function readNumber(startsWithDot) {
var start = tokPos, isFloat = false, octal = input.charCodeAt(tokPos) === 48;
if (!startsWithDot && readInt(10) === null) raise(start, "Invalid number");
@ -781,7 +821,7 @@
if (next === 69 || next === 101) { // 'eE'
next = input.charCodeAt(++tokPos);
if (next === 43 || next === 45) ++tokPos; // '+-'
if (readInt(10) === null) raise(start, "Invalid number")
if (readInt(10) === null) raise(start, "Invalid number");
isFloat = true;
}
if (isIdentifierStart(input.charCodeAt(tokPos))) raise(tokPos, "Identifier directly after number");
@ -810,7 +850,7 @@
ch = input.charCodeAt(++tokPos);
var octal = /^[0-7]+/.exec(input.slice(tokPos, tokPos + 3));
if (octal) octal = octal[0];
while (octal && parseInt(octal, 8) > 255) octal = octal.slice(0, octal.length - 1);
while (octal && parseInt(octal, 8) > 255) octal = octal.slice(0, -1);
if (octal === "0") octal = null;
++tokPos;
if (octal) {
@ -837,7 +877,7 @@
}
}
} else {
if (ch === 13 || ch === 10 || ch === 8232 || ch === 8329) raise(tokStart, "Unterminated string constant");
if (ch === 13 || ch === 10 || ch === 8232 || ch === 8233) raise(tokStart, "Unterminated string constant");
out += String.fromCharCode(ch); // '\'
++tokPos;
}
@ -931,7 +971,7 @@
// ### Parser utilities
// Continue to the next token.
function next() {
lastStart = tokStart;
lastEnd = tokEnd;
@ -944,10 +984,12 @@
function setStrict(strct) {
strict = strct;
tokPos = lastEnd;
while (tokPos < tokLineStart) {
tokLineStart = input.lastIndexOf("\n", tokLineStart - 2) + 1;
--tokCurLine;
tokPos = tokStart;
if (options.locations) {
while (tokPos < tokLineStart) {
tokLineStart = input.lastIndexOf("\n", tokLineStart - 2) + 1;
--tokCurLine;
}
}
skipSpace();
readToken();
@ -971,6 +1013,8 @@
var node = new node_t();
if (options.locations)
node.loc = new node_loc_t();
if (options.directSourceFile)
node.sourceFile = options.directSourceFile;
if (options.ranges)
node.range = [tokStart, 0];
return node;
@ -1095,7 +1139,7 @@
// does not help.
function parseStatement() {
if (tokType === _slash)
if (tokType === _slash || tokType === _assign && tokVal == "/=")
readToken(true);
var starttype = tokType, node = startNode();
@ -1159,6 +1203,7 @@
var init = startNode();
next();
parseVar(init, true);
finishNode(init, "VariableDeclaration");
if (init.declarations.length === 1 && eat(_in))
return parseForIn(node, init);
return parseFor(node, init);
@ -1185,7 +1230,7 @@
// In `return` (and `break`/`continue`), the keywords with
// optional arguments, we eagerly look for a semicolon or the
// possibility to insert one.
if (eat(_semi) || canInsertSemicolon()) node.argument = null;
else { node.argument = parseExpression(); semicolon(); }
return finishNode(node, "ReturnStatement");
@ -1200,7 +1245,7 @@
// Statements under must be grouped (by label) in SwitchCase
// nodes. `cur` is used to keep the node that we are currently
// adding statements to.
for (var cur, sawDefault; tokType != _braceR;) {
if (tokType === _case || tokType === _default) {
var isCase = tokType === _case;
@ -1248,6 +1293,7 @@
clause.body = parseBlock();
node.handler = finishNode(clause, "CatchClause");
}
node.guardedHandlers = empty;
node.finalizer = eat(_finally) ? parseBlock() : null;
if (!node.handler && !node.finalizer)
raise(node.start, "Missing catch or finally clause");
@ -1255,9 +1301,9 @@
case _var:
next();
node = parseVar(node);
parseVar(node);
semicolon();
return node;
return finishNode(node, "VariableDeclaration");
case _while:
next();
@ -1327,11 +1373,11 @@
while (!eat(_braceR)) {
var stmt = parseStatement();
node.body.push(stmt);
if (first && isUseStrict(stmt)) {
if (first && allowStrict && isUseStrict(stmt)) {
oldStrict = strict;
setStrict(strict = true);
}
first = false
first = false;
}
if (strict && !oldStrict) setStrict(false);
return finishNode(node, "BlockStatement");
@ -1378,7 +1424,7 @@
node.declarations.push(finishNode(decl, "VariableDeclarator"));
if (!eat(_comma)) break;
}
return finishNode(node, "VariableDeclaration");
return node;
}
// ### Expression parsing
@ -1439,7 +1485,7 @@
// Start the precedence parser.
function parseExprOps(noIn) {
return parseExprOp(parseMaybeUnary(noIn), -1, noIn);
return parseExprOp(parseMaybeUnary(), -1, noIn);
}
// Parse binary operators with the operator precedence parsing
@ -1455,10 +1501,11 @@
var node = startNodeFrom(left);
node.left = left;
node.operator = tokVal;
var op = tokType;
next();
node.right = parseExprOp(parseMaybeUnary(noIn), prec, noIn);
var node = finishNode(node, /&&|\|\|/.test(node.operator) ? "LogicalExpression" : "BinaryExpression");
return parseExprOp(node, minPrec, noIn);
node.right = parseExprOp(parseMaybeUnary(), prec, noIn);
var exprNode = finishNode(node, (op === _logicalOR || op === _logicalAND) ? "LogicalExpression" : "BinaryExpression");
return parseExprOp(exprNode, minPrec, noIn);
}
}
return left;
@ -1466,13 +1513,14 @@
// Parse unary operators, both prefix and postfix.
function parseMaybeUnary(noIn) {
function parseMaybeUnary() {
if (tokType.prefix) {
var node = startNode(), update = tokType.isUpdate;
node.operator = tokVal;
node.prefix = true;
tokRegexpAllowed = true;
next();
node.argument = parseMaybeUnary(noIn);
node.argument = parseMaybeUnary();
if (update) checkLVal(node.argument);
else if (strict && node.operator === "delete" &&
node.argument.type === "Identifier")
@ -1543,7 +1591,7 @@
case _null: case _true: case _false:
var node = startNode();
node.value = tokType.atomValue;
node.raw = tokType.keyword
node.raw = tokType.keyword;
next();
return finishNode(node, "Literal");
@ -1586,14 +1634,14 @@
// New's precedence is slightly tricky. It must allow its argument
// to be a `[]` or dot subscript expression, but not a call — at
// least, not without wrapping it in parentheses. Thus, it uses the
// least, not without wrapping it in parentheses. Thus, it uses the
function parseNew() {
var node = startNode();
next();
node.callee = parseSubscripts(parseExprAtom(), true);
if (eat(_parenL)) node.arguments = parseExprList(_parenR, false);
else node.arguments = [];
else node.arguments = empty;
return finishNode(node, "NewExpression");
}
@ -1712,6 +1760,7 @@
function parseIdent(liberal) {
var node = startNode();
node.name = tokType === _name ? tokVal : (liberal && !options.forbidReserved && tokType.keyword) || unexpected();
tokRegexpAllowed = false;
next();
return finishNode(node, "Identifier");
}

View File

@ -29,11 +29,11 @@
// invasive changes and simplifications without creating a complicated
// tangle.
(function(mod) {
(function(root, mod) {
if (typeof exports == "object" && typeof module == "object") return mod(exports, require("./acorn")); // CommonJS
if (typeof define == "function" && define.amd) return define(["exports", "./acorn"], mod); // AMD
mod(self.acorn || (self.acorn = {}), self.acorn); // Plain browser env
})(function(exports, acorn) {
mod(root.acorn || (root.acorn = {}), root.acorn); // Plain browser env
})(this, function(exports, acorn) {
"use strict";
var tt = acorn.tokTypes;
@ -168,10 +168,6 @@
while (pos < input.length && !isNewline(input.charCodeAt(pos))) ++pos;
return pos;
}
function lineStart(pos) {
while (pos > 0 && !isNewline(input.charCodeAt(pos - 1))) --pos;
return pos;
}
function indentationAfter(pos) {
for (var count = 0;; ++pos) {
var ch = input.charCodeAt(pos);
@ -181,16 +177,16 @@
}
}
function closesBlock(closeTok, indent, line) {
function closes(closeTok, indent, line, blockHeuristic) {
if (token.type === closeTok || token.type === tt.eof) return true;
if (line != curLineStart && curIndent < indent && tokenStartsLine() &&
(nextLineStart >= input.length ||
(!blockHeuristic || nextLineStart >= input.length ||
indentationAfter(nextLineStart) < indent)) return true;
return false;
}
function tokenStartsLine() {
for (var p = token.start - 1; p > curLineStart; --p) {
for (var p = token.start - 1; p >= curLineStart; --p) {
var ch = input.charCodeAt(p);
if (ch !== 9 && ch !== 32) return false;
}
@ -213,7 +209,9 @@
var node = new node_t(token.start);
if (options.locations)
node.loc = new node_loc_t();
return node
if (options.directSourceFile)
node.sourceFile = options.directSourceFile;
return node;
}
function startNodeFrom(other) {
@ -291,60 +289,60 @@
var starttype = token.type, node = startNode();
switch (starttype) {
case tt.break: case tt.continue:
case tt._break: case tt._continue:
next();
var isBreak = starttype === tt.break;
var isBreak = starttype === tt._break;
node.label = token.type === tt.name ? parseIdent() : null;
semicolon();
return finishNode(node, isBreak ? "BreakStatement" : "ContinueStatement");
case tt.debugger:
case tt._debugger:
next();
semicolon();
return finishNode(node, "DebuggerStatement");
case tt.do:
case tt._do:
next();
node.body = parseStatement();
node.test = eat(tt.while) ? parseParenExpression() : dummyIdent();
node.test = eat(tt._while) ? parseParenExpression() : dummyIdent();
semicolon();
return finishNode(node, "DoWhileStatement");
case tt.for:
case tt._for:
next();
pushCx();
expect(tt.parenL);
if (token.type === tt.semi) return parseFor(node, null);
if (token.type === tt.var) {
if (token.type === tt._var) {
var init = startNode();
next();
parseVar(init, true);
if (init.declarations.length === 1 && eat(tt.in))
if (init.declarations.length === 1 && eat(tt._in))
return parseForIn(node, init);
return parseFor(node, init);
}
var init = parseExpression(false, true);
if (eat(tt.in)) {return parseForIn(node, checkLVal(init));}
if (eat(tt._in)) {return parseForIn(node, checkLVal(init));}
return parseFor(node, init);
case tt.function:
case tt._function:
next();
return parseFunction(node, true);
case tt.if:
case tt._if:
next();
node.test = parseParenExpression();
node.consequent = parseStatement();
node.alternate = eat(tt.else) ? parseStatement() : null;
node.alternate = eat(tt._else) ? parseStatement() : null;
return finishNode(node, "IfStatement");
case tt.return:
case tt._return:
next();
if (eat(tt.semi) || canInsertSemicolon()) node.argument = null;
else { node.argument = parseExpression(); semicolon(); }
return finishNode(node, "ReturnStatement");
case tt.switch:
case tt._switch:
var blockIndent = curIndent, line = curLineStart;
next();
node.discriminant = parseParenExpression();
@ -352,9 +350,9 @@
pushCx();
expect(tt.braceL);
for (var cur; !closesBlock(tt.braceR, blockIndent, line);) {
if (token.type === tt.case || token.type === tt.default) {
var isCase = token.type === tt.case;
for (var cur; !closes(tt.braceR, blockIndent, line, true);) {
if (token.type === tt._case || token.type === tt._default) {
var isCase = token.type === tt._case;
if (cur) finishNode(cur, "SwitchCase");
node.cases.push(cur = startNode());
cur.consequent = [];
@ -376,17 +374,17 @@
eat(tt.braceR);
return finishNode(node, "SwitchStatement");
case tt.throw:
case tt._throw:
next();
node.argument = parseExpression();
semicolon();
return finishNode(node, "ThrowStatement");
case tt.try:
case tt._try:
next();
node.block = parseBlock();
node.handler = null;
if (token.type === tt.catch) {
if (token.type === tt._catch) {
var clause = startNode();
next();
expect(tt.parenL);
@ -396,23 +394,23 @@
clause.body = parseBlock();
node.handler = finishNode(clause, "CatchClause");
}
node.finalizer = eat(tt.finally) ? parseBlock() : null;
node.finalizer = eat(tt._finally) ? parseBlock() : null;
if (!node.handler && !node.finalizer) return node.block;
return finishNode(node, "TryStatement");
case tt.var:
case tt._var:
next();
node = parseVar(node);
semicolon();
return node;
case tt.while:
case tt._while:
next();
node.test = parseParenExpression();
node.body = parseStatement();
return finishNode(node, "WhileStatement");
case tt.with:
case tt._with:
next();
node.object = parseParenExpression();
node.body = parseStatement();
@ -426,7 +424,7 @@
return finishNode(node, "EmptyStatement");
default:
var maybeName = token.value, expr = parseExpression();
var expr = parseExpression();
if (isDummy(expr)) {
next();
if (token.type === tt.eof) return finishNode(node, "EmptyStatement");
@ -449,7 +447,7 @@
expect(tt.braceL);
var blockIndent = curIndent, line = curLineStart;
node.body = [];
while (!closesBlock(tt.braceR, blockIndent, line))
while (!closes(tt.braceR, blockIndent, line, true))
node.body.push(parseStatement());
popCx();
eat(tt.braceR);
@ -486,6 +484,11 @@
node.declarations.push(finishNode(decl, "VariableDeclarator"));
if (!eat(tt.comma)) break;
}
if (!node.declarations.length) {
var decl = startNode();
decl.id = dummyIdent();
node.declarations.push(finishNode(decl, "VariableDeclarator"));
}
return finishNode(node, "VariableDeclaration");
}
@ -542,7 +545,7 @@
function parseExprOp(left, minPrec, noIn, indent, line) {
if (curLineStart != line && curIndent < indent && tokenStartsLine()) return left;
var prec = token.type.binop;
if (prec != null && (!noIn || token.type !== tt.in)) {
if (prec != null && (!noIn || token.type !== tt._in)) {
if (prec > minPrec) {
var node = startNodeFrom(left);
node.left = left;
@ -582,8 +585,7 @@
}
function parseExprSubscripts() {
var indent = curIndent, line = curLineStart;
return parseSubscripts(parseExprAtom(), false, curIndent, line);
return parseSubscripts(parseExprAtom(), false, curIndent, curLineStart);
}
function parseSubscripts(base, noCalls, startIndent, line) {
@ -628,7 +630,7 @@
function parseExprAtom() {
switch (token.type) {
case tt.this:
case tt._this:
var node = startNode();
next();
return finishNode(node, "ThisExpression");
@ -641,10 +643,10 @@
next();
return finishNode(node, "Literal");
case tt.null: case tt.true: case tt.false:
case tt._null: case tt._true: case tt._false:
var node = startNode();
node.value = token.type.atomValue;
node.raw = token.type.keyword
node.raw = token.type.keyword;
next();
return finishNode(node, "Literal");
@ -666,12 +668,12 @@
case tt.braceL:
return parseObj();
case tt.function:
case tt._function:
var node = startNode();
next();
return parseFunction(node, false);
case tt.new:
case tt._new:
return parseNew();
default:
@ -698,7 +700,7 @@
pushCx();
next();
var propIndent = curIndent, line = curLineStart;
while (!closesBlock(tt.braceR, propIndent, line)) {
while (!closes(tt.braceR, propIndent, line)) {
var name = parsePropertyName();
if (!name) { if (isDummy(parseExpression(true))) next(); eat(tt.comma); continue; }
var prop = {key: name}, isGetSet = false, kind;
@ -755,12 +757,13 @@
}
function parseExprList(close) {
var indent = curIndent + 1, line = curLineStart, elts = [];
var indent = curIndent, line = curLineStart, elts = [], continuedLine = nextLineStart;
next(); // Opening bracket
while (!closesBlock(close, indent, line)) {
if (curLineStart > continuedLine) continuedLine = curLineStart;
while (!closes(close, indent + (curLineStart <= continuedLine ? 1 : 0), line)) {
var elt = parseExpression(true);
if (isDummy(elt)) {
if (closesBlock(close, indent, line)) break;
if (closes(close, indent, line)) break;
next();
} else {
elts.push(elt);

View File

@ -12,7 +12,6 @@ const HTML_NS = "http://www.w3.org/1999/xhtml";
const MAX_ITERATIONS = 100;
const REGEX_QUOTES = /^".*?"|^".*|^'.*?'|^'.*/;
const REGEX_URL = /^url\(["']?(.+?)(?::(\d+))?["']?\)/;
const REGEX_WHITESPACE = /^\s+/;
const REGEX_FIRST_WORD_OR_CHAR = /^\w+|^./;
const REGEX_CSS_PROPERTY_VALUE = /(^[^;]+)/;
@ -119,6 +118,32 @@ OutputParser.prototype = {
return this._parse(value, options);
},
/**
* Matches the beginning of the provided string to a css background-image url
* and return both the whole url(...) match and the url itself.
* This isn't handled via a regular expression to make sure we can match urls
* that contain parenthesis easily
*/
_matchBackgroundUrl: function(text) {
let startToken = "url(";
if (text.indexOf(startToken) !== 0) {
return null;
}
let uri = text.substring(startToken.length).trim();
let quote = uri.substring(0, 1);
if (quote === "'" || quote === '"') {
uri = uri.substring(1, uri.search(new RegExp(quote + "\\s*\\)")));
} else {
uri = uri.substring(0, uri.indexOf(")"));
quote = "";
}
let end = startToken + quote + uri;
text = text.substring(0, text.indexOf(")", end.length) + 1);
return [text, uri.trim()];
},
/**
* Parse a string.
*
@ -166,11 +191,11 @@ OutputParser.prototype = {
continue;
}
matched = text.match(REGEX_URL);
matched = this._matchBackgroundUrl(text);
if (matched) {
let [match, url] = matched;
text = this._trimMatchFromStart(text, match);
this._appendURL(match, url, options);
continue;
}

View File

@ -65,6 +65,8 @@ const {HighlighterActor} = require("devtools/server/actors/highlighter");
const PSEUDO_CLASSES = [":hover", ":active", ":focus"];
const HIDDEN_CLASS = "__fx-devtools-hide-shortcut__";
const XHTML_NS = "http://www.w3.org/1999/xhtml";
const IMAGE_FETCHING_TIMEOUT = 500;
// The possible completions to a ':' with added score to give certain values
// some preference.
const PSEUDO_SELECTORS = [
@ -288,58 +290,25 @@ var NodeActor = exports.NodeActor = protocol.ActorClass({
/**
* Get the node's image data if any (for canvas and img nodes).
* Returns a LongStringActor with the image or canvas' image data as png
* a data:image/png;base64,.... string
* A null return value means the node isn't an image
* An empty string return value means the node is an image but image data
* could not be retrieved (missing/broken image).
* Returns an imageData object with the actual data being a LongStringActor
* and a size json object.
* The image data is transmitted as a base64 encoded png data-uri.
* The method rejects if the node isn't an image or if the image is missing
*
* Accepts a maxDim request parameter to resize images that are larger. This
* is important as the resizing occurs server-side so that image-data being
* transfered in the longstring back to the client will be that much smaller
*/
getImageData: method(function(maxDim) {
let isImg = this.rawNode.tagName.toLowerCase() === "img";
let isCanvas = this.rawNode.tagName.toLowerCase() === "canvas";
if (!isImg && !isCanvas) {
return null;
}
// Get the image resize ratio if a maxDim was provided
let resizeRatio = 1;
let imgWidth = isImg ? this.rawNode.naturalWidth : this.rawNode.width;
let imgHeight = isImg ? this.rawNode.naturalHeight : this.rawNode.height;
let imgMax = Math.max(imgWidth, imgHeight);
if (maxDim && imgMax > maxDim) {
resizeRatio = maxDim / imgMax;
}
// Create a canvas to copy the rawNode into and get the imageData from
let canvas = this.rawNode.ownerDocument.createElement("canvas");
canvas.width = imgWidth * resizeRatio;
canvas.height = imgHeight * resizeRatio;
let ctx = canvas.getContext("2d");
// Copy the rawNode image or canvas in the new canvas and extract data
let imageData;
// This may fail if the image is missing
// imageToImageData may fail if the node isn't an image
try {
ctx.drawImage(this.rawNode, 0, 0, canvas.width, canvas.height);
imageData = canvas.toDataURL("image/png");
} catch (e) {
imageData = "";
}
return {
data: LongStringActor(this.conn, imageData),
size: {
naturalWidth: imgWidth,
naturalHeight: imgHeight,
width: canvas.width,
height: canvas.height,
resized: resizeRatio !== 1
}
let imageData = imageToImageData(this.rawNode, maxDim);
return promise.resolve({
data: LongStringActor(this.conn, imageData.data),
size: imageData.size
});
} catch(e) {
return promise.reject(new Error("Image not available"));
}
}, {
request: {maxDim: Arg(0, "nullable:number")},
@ -1362,8 +1331,7 @@ var WalkerActor = protocol.ActorClass({
* Helper function for the `children` method: Read forward in the sibling
* list into an array with `count` items, including the current node.
*/
_readForward: function(walker, count)
{
_readForward: function(walker, count) {
let ret = [];
let node = walker.currentNode;
do {
@ -1377,8 +1345,7 @@ var WalkerActor = protocol.ActorClass({
* Helper function for the `children` method: Read backward in the sibling
* list into an array with `count` items, including the current node.
*/
_readBackward: function(walker, count)
{
_readBackward: function(walker, count) {
let ret = [];
let node = walker.currentNode;
do {
@ -2403,7 +2370,7 @@ var WalkerFront = exports.WalkerFront = protocol.FrontClass(WalkerActor, {
// XXX hack during transition to remote inspector: get a proper NodeFront
// for a given local node. Only works locally.
frontForRawNode: function(rawNode){
frontForRawNode: function(rawNode) {
if (!this.isLocal()) {
console.warn("Tried to use frontForRawNode on a remote connection.");
return null;
@ -2550,6 +2517,54 @@ var InspectorActor = protocol.ActorClass({
response: {
highligter: RetVal("highlighter")
}
}),
/**
* Get the node's image data if any (for canvas and img nodes).
* Returns an imageData object with the actual data being a LongStringActor
* and a size json object.
* The image data is transmitted as a base64 encoded png data-uri.
* The method rejects if the node isn't an image or if the image is missing
*
* Accepts a maxDim request parameter to resize images that are larger. This
* is important as the resizing occurs server-side so that image-data being
* transfered in the longstring back to the client will be that much smaller
*/
getImageDataFromURL: method(function(url, maxDim) {
let deferred = promise.defer();
let img = new this.window.Image();
// On load, get the image data and send the response
img.onload = () => {
// imageToImageData throws an error if the image is missing
try {
let imageData = imageToImageData(img, maxDim);
deferred.resolve({
data: LongStringActor(this.conn, imageData.data),
size: imageData.size
});
} catch (e) {
deferred.reject(new Error("Image " + url+ " not available"));
}
}
// If the URL doesn't point to a resource, reject
img.onerror = () => {
deferred.reject(new Error("Image " + url+ " not available"));
}
// If the request hangs for too long, kill it to avoid queuing up other requests
// to the same actor
this.window.setTimeout(() => {
deferred.reject(new Error("Image " + url + " could not be retrieved in time"));
}, IMAGE_FETCHING_TIMEOUT);
img.src = url;
return deferred.promise;
}, {
request: {url: Arg(0), maxDim: Arg(1, "nullable:number")},
response: RetVal("imageData")
})
});
@ -2615,8 +2630,7 @@ function nodeDocument(node) {
*
* See TreeWalker documentation for explanations of the methods.
*/
function DocumentWalker(aNode, aRootWin, aShow, aFilter, aExpandEntityReferences)
{
function DocumentWalker(aNode, aRootWin, aShow, aFilter, aExpandEntityReferences) {
let doc = nodeDocument(aNode);
this.layoutHelpers = new LayoutHelpers(aRootWin);
this.walker = doc.createTreeWalker(doc,
@ -2637,7 +2651,7 @@ DocumentWalker.prototype = {
* the current node, creates a new treewalker for the document we've
* run in to.
*/
_reparentWalker: function DW_reparentWalker(aNewNode) {
_reparentWalker: function(aNewNode) {
if (!aNewNode) {
return null;
}
@ -2649,8 +2663,7 @@ DocumentWalker.prototype = {
return aNewNode;
},
parentNode: function DW_parentNode()
{
parentNode: function() {
let currentNode = this.walker.currentNode;
let parentNode = this.walker.parentNode();
@ -2670,8 +2683,7 @@ DocumentWalker.prototype = {
return parentNode;
},
firstChild: function DW_firstChild()
{
firstChild: function() {
let node = this.walker.currentNode;
if (!node)
return null;
@ -2683,8 +2695,7 @@ DocumentWalker.prototype = {
return this.walker.firstChild();
},
lastChild: function DW_lastChild()
{
lastChild: function() {
let node = this.walker.currentNode;
if (!node)
return null;
@ -2698,13 +2709,12 @@ DocumentWalker.prototype = {
previousSibling: function DW_previousSibling() this.walker.previousSibling(),
nextSibling: function DW_nextSibling() this.walker.nextSibling()
}
};
/**
* A tree walker filter for avoiding empty whitespace text nodes.
*/
function whitespaceTextFilter(aNode)
{
function whitespaceTextFilter(aNode) {
if (aNode.nodeType == Ci.nsIDOMNode.TEXT_NODE &&
!/[^\s]/.exec(aNode.nodeValue)) {
return Ci.nsIDOMNodeFilter.FILTER_SKIP;
@ -2713,6 +2723,60 @@ function whitespaceTextFilter(aNode)
}
}
/**
* Given an image DOMNode, return the image data-uri.
* @param {DOMNode} node The image node
* @param {Number} maxDim Optionally pass a maximum size you want the longest
* side of the image to be resized to before getting the image data.
* @return {Object} An object containing the data-uri and size-related information
* {data: "...", size: {naturalWidth: 400, naturalHeight: 300, resized: true}}
* @throws an error if the node isn't an image or if the image is missing
*/
function imageToImageData(node, maxDim) {
let isImg = node.tagName.toLowerCase() === "img";
let isCanvas = node.tagName.toLowerCase() === "canvas";
if (!isImg && !isCanvas) {
return null;
}
// Get the image resize ratio if a maxDim was provided
let resizeRatio = 1;
let imgWidth = node.naturalWidth || node.width;
let imgHeight = node.naturalHeight || node.height;
let imgMax = Math.max(imgWidth, imgHeight);
if (maxDim && imgMax > maxDim) {
resizeRatio = maxDim / imgMax;
}
// Extract the image data
let imageData;
// The image may already be a data-uri, in which case, save ourselves the
// trouble of converting via the canvas.drawImage.toDataURL method
if (isImg && node.src.startsWith("data:")) {
imageData = node.src;
} else {
// Create a canvas to copy the rawNode into and get the imageData from
let canvas = node.ownerDocument.createElementNS(XHTML_NS, "canvas");
canvas.width = imgWidth * resizeRatio;
canvas.height = imgHeight * resizeRatio;
let ctx = canvas.getContext("2d");
// Copy the rawNode image or canvas in the new canvas and extract data
ctx.drawImage(node, 0, 0, canvas.width, canvas.height);
imageData = canvas.toDataURL("image/png");
}
return {
data: imageData,
size: {
naturalWidth: imgWidth,
naturalHeight: imgHeight,
resized: resizeRatio !== 1
}
}
}
loader.lazyGetter(this, "DOMUtils", function () {
return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
});

View File

@ -179,7 +179,11 @@ RootActor.prototype = {
editOuterHTML: true,
// Wether the server-side highlighter actor exists and can be used to
// remotely highlight nodes (see server/actors/highlighter.js)
highlightable: true
highlightable: true,
// Wether the inspector actor implements the getImageDataFromURL
// method that returns data-uris for image URLs. This is used for image
// tooltips for instance
urlToImageDataResolver: true
}
};
},

View File

@ -46,8 +46,6 @@ addTest(function testLargeImage() {
ok(imageData.size, "Image size info was sent back too");
is(imageData.size.naturalWidth, 5333, "Natural width of the image correct");
is(imageData.size.naturalHeight, 3000, "Natural width of the image correct");
is(imageData.size.width, 100, "Resized image width correct");
is(imageData.size.height, 56, "Resized image height correct");
ok(imageData.size.resized, "Image was resized");
imageData.data.string().then(str => {
@ -69,8 +67,6 @@ addTest(function testLargeCanvas() {
ok(imageData.size, "Image size info was sent back too");
is(imageData.size.naturalWidth, 1000, "Natural width of the image correct");
is(imageData.size.naturalHeight, 2000, "Natural width of the image correct");
is(imageData.size.width, 175, "Resized image width correct");
is(imageData.size.height, 350, "Resized image height correct");
ok(imageData.size.resized, "Image was resized");
imageData.data.string().then(str => {
@ -92,8 +88,6 @@ addTest(function testSmallImage() {
ok(imageData.size, "Image size info was sent back too");
is(imageData.size.naturalWidth, 245, "Natural width of the image correct");
is(imageData.size.naturalHeight, 240, "Natural width of the image correct");
is(imageData.size.width, 245, "Resized image width correct");
is(imageData.size.height, 240, "Resized image height correct");
ok(!imageData.size.resized, "Image was NOT resized");
imageData.data.string().then(str => {

View File

@ -809,6 +809,42 @@ CssLogic.shortSource = function CssLogic_shortSource(aSheet)
return dataUrl ? dataUrl[1] : aSheet.href;
}
/**
* Extract the background image URL (if any) from a property value.
* Used, for example, for the preview tooltip in the rule view and
* computed view.
*
* @param {String} aProperty
* @param {String} aSheetHref
* @return {string} a image URL
*/
CssLogic.getBackgroundImageUriFromProperty = function(aProperty, aSheetHref) {
let startToken = "url(", start = aProperty.indexOf(startToken), end;
if (start === -1) {
return null;
}
aProperty = aProperty.substring(start + startToken.length).trim();
let quote = aProperty.substring(0, 1);
if (quote === "'" || quote === '"') {
end = aProperty.search(new RegExp(quote + "\\s*\\)"));
start = 1;
} else {
end = aProperty.indexOf(")");
start = 0;
}
let uri = aProperty.substring(start, end).trim();
if (aSheetHref) {
let IOService = Cc["@mozilla.org/network/io-service;1"]
.getService(Ci.nsIIOService);
let sheetUri = IOService.newURI(aSheetHref, null, null);
uri = sheetUri.resolve(uri);
}
return uri;
}
/**
* Find the position of [element] in [nodeList].
* @returns an index of the match, or -1 if there is no match

View File

@ -75,7 +75,7 @@ panel[type="arrow"][side="right"] {
}
.panel-arrowcontent {
padding: 10px;
padding: 4px;
color: -moz-FieldText;
background: -moz-field;
background-clip: padding-box;

View File

@ -3695,70 +3695,6 @@ bool nsWindow::DispatchWindowEvent(WidgetGUIEvent* event,
return ConvertStatus(aStatus);
}
bool nsWindow::DispatchCommandEvent(uint32_t aEventCommand)
{
nsCOMPtr<nsIAtom> command;
switch (aEventCommand) {
case APPCOMMAND_BROWSER_BACKWARD:
command = nsGkAtoms::Back;
break;
case APPCOMMAND_BROWSER_FORWARD:
command = nsGkAtoms::Forward;
break;
case APPCOMMAND_BROWSER_REFRESH:
command = nsGkAtoms::Reload;
break;
case APPCOMMAND_BROWSER_STOP:
command = nsGkAtoms::Stop;
break;
case APPCOMMAND_BROWSER_SEARCH:
command = nsGkAtoms::Search;
break;
case APPCOMMAND_BROWSER_FAVORITES:
command = nsGkAtoms::Bookmarks;
break;
case APPCOMMAND_BROWSER_HOME:
command = nsGkAtoms::Home;
break;
case APPCOMMAND_CLOSE:
command = nsGkAtoms::Close;
break;
case APPCOMMAND_FIND:
command = nsGkAtoms::Find;
break;
case APPCOMMAND_HELP:
command = nsGkAtoms::Help;
break;
case APPCOMMAND_NEW:
command = nsGkAtoms::New;
break;
case APPCOMMAND_OPEN:
command = nsGkAtoms::Open;
break;
case APPCOMMAND_PRINT:
command = nsGkAtoms::Print;
break;
case APPCOMMAND_SAVE:
command = nsGkAtoms::Save;
break;
case APPCOMMAND_FORWARD_MAIL:
command = nsGkAtoms::ForwardMail;
break;
case APPCOMMAND_REPLY_TO_MAIL:
command = nsGkAtoms::ReplyToMail;
break;
case APPCOMMAND_SEND_MAIL:
command = nsGkAtoms::SendMail;
break;
default:
return false;
}
WidgetCommandEvent event(true, nsGkAtoms::onAppCommand, command, this);
InitEvent(event);
return DispatchWindowEvent(&event);
}
// Recursively dispatch synchronous paints for nsIWidget
// descendants with invalidated rectangles.
BOOL CALLBACK nsWindow::DispatchStarvedPaints(HWND aWnd, LPARAM aMsg)
@ -5089,69 +5025,8 @@ nsWindow::ProcessMessage(UINT msg, WPARAM& wParam, LPARAM& lParam,
break;
case WM_APPCOMMAND:
{
uint32_t appCommand = GET_APPCOMMAND_LPARAM(lParam);
uint32_t contentCommandMessage = NS_EVENT_NULL;
// XXX After we implement KeyboardEvent.key, we should dispatch the
// key event if (GET_DEVICE_LPARAM(lParam) == FAPPCOMMAND_KEY) is.
switch (appCommand)
{
case APPCOMMAND_BROWSER_BACKWARD:
case APPCOMMAND_BROWSER_FORWARD:
case APPCOMMAND_BROWSER_REFRESH:
case APPCOMMAND_BROWSER_STOP:
case APPCOMMAND_BROWSER_SEARCH:
case APPCOMMAND_BROWSER_FAVORITES:
case APPCOMMAND_BROWSER_HOME:
case APPCOMMAND_CLOSE:
case APPCOMMAND_FIND:
case APPCOMMAND_HELP:
case APPCOMMAND_NEW:
case APPCOMMAND_OPEN:
case APPCOMMAND_PRINT:
case APPCOMMAND_SAVE:
case APPCOMMAND_FORWARD_MAIL:
case APPCOMMAND_REPLY_TO_MAIL:
case APPCOMMAND_SEND_MAIL:
// We shouldn't consume the message always because if we don't handle
// the message, the sender (typically, utility of keyboard or mouse)
// may send other key messages which indicate well known shortcut key.
if (DispatchCommandEvent(appCommand)) {
// tell the driver that we handled the event
*aRetValue = 1;
result = true;
}
break;
// Use content command for following commands:
case APPCOMMAND_COPY:
contentCommandMessage = NS_CONTENT_COMMAND_COPY;
break;
case APPCOMMAND_CUT:
contentCommandMessage = NS_CONTENT_COMMAND_CUT;
break;
case APPCOMMAND_PASTE:
contentCommandMessage = NS_CONTENT_COMMAND_PASTE;
break;
case APPCOMMAND_REDO:
contentCommandMessage = NS_CONTENT_COMMAND_REDO;
break;
case APPCOMMAND_UNDO:
contentCommandMessage = NS_CONTENT_COMMAND_UNDO;
break;
}
if (contentCommandMessage) {
WidgetContentCommandEvent contentCommand(true, contentCommandMessage,
this);
DispatchWindowEvent(&contentCommand);
// tell the driver that we handled the event
*aRetValue = 1;
result = true;
}
// default = false - tell the driver that the event was not handled
}
break;
result = HandleAppCommandMsg(wParam, lParam, aRetValue);
break;
// The WM_ACTIVATE event is fired when a window is raised or lowered,
// and the loword of wParam specifies which. But we don't want to tell

View File

@ -357,7 +357,6 @@ protected:
*/
void DispatchFocusToTopLevelWindow(bool aIsActivate);
bool DispatchStandardEvent(uint32_t aMsg);
bool DispatchCommandEvent(uint32_t aEventCommand);
void RelayMouseEvent(UINT aMsg, WPARAM wParam, LPARAM lParam);
virtual bool ProcessMessage(UINT msg, WPARAM &wParam,
LPARAM &lParam, LRESULT *aRetValue);

View File

@ -6,7 +6,7 @@
#include "nsWindowBase.h"
#include "mozilla/MiscEvents.h"
#include "nsGkAtoms.h"
#include "WinUtils.h"
#include "npapi.h"
@ -194,4 +194,136 @@ nsWindowBase::ClearNativeTouchSequence()
return NS_OK;
}
bool
nsWindowBase::DispatchCommandEvent(uint32_t aEventCommand)
{
nsCOMPtr<nsIAtom> command;
switch (aEventCommand) {
case APPCOMMAND_BROWSER_BACKWARD:
command = nsGkAtoms::Back;
break;
case APPCOMMAND_BROWSER_FORWARD:
command = nsGkAtoms::Forward;
break;
case APPCOMMAND_BROWSER_REFRESH:
command = nsGkAtoms::Reload;
break;
case APPCOMMAND_BROWSER_STOP:
command = nsGkAtoms::Stop;
break;
case APPCOMMAND_BROWSER_SEARCH:
command = nsGkAtoms::Search;
break;
case APPCOMMAND_BROWSER_FAVORITES:
command = nsGkAtoms::Bookmarks;
break;
case APPCOMMAND_BROWSER_HOME:
command = nsGkAtoms::Home;
break;
case APPCOMMAND_CLOSE:
command = nsGkAtoms::Close;
break;
case APPCOMMAND_FIND:
command = nsGkAtoms::Find;
break;
case APPCOMMAND_HELP:
command = nsGkAtoms::Help;
break;
case APPCOMMAND_NEW:
command = nsGkAtoms::New;
break;
case APPCOMMAND_OPEN:
command = nsGkAtoms::Open;
break;
case APPCOMMAND_PRINT:
command = nsGkAtoms::Print;
break;
case APPCOMMAND_SAVE:
command = nsGkAtoms::Save;
break;
case APPCOMMAND_FORWARD_MAIL:
command = nsGkAtoms::ForwardMail;
break;
case APPCOMMAND_REPLY_TO_MAIL:
command = nsGkAtoms::ReplyToMail;
break;
case APPCOMMAND_SEND_MAIL:
command = nsGkAtoms::SendMail;
break;
default:
return false;
}
WidgetCommandEvent event(true, nsGkAtoms::onAppCommand, command, this);
InitEvent(event);
return DispatchWindowEvent(&event);
}
bool
nsWindowBase::HandleAppCommandMsg(WPARAM aWParam,
LPARAM aLParam,
LRESULT *aRetValue)
{
uint32_t appCommand = GET_APPCOMMAND_LPARAM(aLParam);
uint32_t contentCommandMessage = NS_EVENT_NULL;
// XXX After we implement KeyboardEvent.key, we should dispatch the
// key event if (GET_DEVICE_LPARAM(lParam) == FAPPCOMMAND_KEY) is.
switch (appCommand)
{
case APPCOMMAND_BROWSER_BACKWARD:
case APPCOMMAND_BROWSER_FORWARD:
case APPCOMMAND_BROWSER_REFRESH:
case APPCOMMAND_BROWSER_STOP:
case APPCOMMAND_BROWSER_SEARCH:
case APPCOMMAND_BROWSER_FAVORITES:
case APPCOMMAND_BROWSER_HOME:
case APPCOMMAND_CLOSE:
case APPCOMMAND_FIND:
case APPCOMMAND_HELP:
case APPCOMMAND_NEW:
case APPCOMMAND_OPEN:
case APPCOMMAND_PRINT:
case APPCOMMAND_SAVE:
case APPCOMMAND_FORWARD_MAIL:
case APPCOMMAND_REPLY_TO_MAIL:
case APPCOMMAND_SEND_MAIL:
// We shouldn't consume the message always because if we don't handle
// the message, the sender (typically, utility of keyboard or mouse)
// may send other key messages which indicate well known shortcut key.
if (DispatchCommandEvent(appCommand)) {
// tell the driver that we handled the event
*aRetValue = 1;
return true;
}
break;
// Use content command for following commands:
case APPCOMMAND_COPY:
contentCommandMessage = NS_CONTENT_COMMAND_COPY;
break;
case APPCOMMAND_CUT:
contentCommandMessage = NS_CONTENT_COMMAND_CUT;
break;
case APPCOMMAND_PASTE:
contentCommandMessage = NS_CONTENT_COMMAND_PASTE;
break;
case APPCOMMAND_REDO:
contentCommandMessage = NS_CONTENT_COMMAND_REDO;
break;
case APPCOMMAND_UNDO:
contentCommandMessage = NS_CONTENT_COMMAND_UNDO;
break;
}
if (contentCommandMessage) {
WidgetContentCommandEvent contentCommand(true, contentCommandMessage,
this);
DispatchWindowEvent(&contentCommand);
// tell the driver that we handled the event
*aRetValue = 1;
return true;
}
// default = false - tell the driver that the event was not handled
return false;
}

View File

@ -78,7 +78,6 @@ public:
return (mInputContext.mIMEState.mEnabled == IMEState::PLUGIN);
}
public:
/*
* Touch input injection apis
*/
@ -89,7 +88,15 @@ public:
uint32_t aPointerOrientation);
virtual nsresult ClearNativeTouchSequence();
/*
* WM_APPCOMMAND common handler. Sends events via DispatchWindowEvent.
*/
virtual bool HandleAppCommandMsg(WPARAM aWParam,
LPARAM aLParam,
LRESULT *aRetValue);
protected:
bool DispatchCommandEvent(uint32_t aEventCommand);
static bool InitTouchInjection();
bool InjectTouchPoint(uint32_t aId, nsIntPoint& aPointerScreenPoint,
POINTER_FLAGS aFlags, uint32_t aPressure = 1024,

View File

@ -889,6 +889,10 @@ MetroWidget::WindowProcedure(HWND aWnd, UINT aMsg, WPARAM aWParam, LPARAM aLPara
break;
}
case WM_APPCOMMAND:
processDefault = HandleAppCommandMsg(aWParam, aLParam, &processResult);
break;
case WM_GETOBJECT:
{
DWORD dwObjId = (LPARAM)(DWORD) aLParam;