From 84926e90d305e0d67b3e9877115c0d1b754ccadd Mon Sep 17 00:00:00 2001 From: Margaret Leibovic Date: Mon, 23 Mar 2015 11:29:23 -0700 Subject: [PATCH 01/31] No bug: update readability libs to the up-to-date github versions. rs=me+Gijs --- toolkit/components/reader/Readability.js | 213 ++++++++++++----------- 1 file changed, 116 insertions(+), 97 deletions(-) diff --git a/toolkit/components/reader/Readability.js b/toolkit/components/reader/Readability.js index 4389e4d56d2..2872da4e5db 100644 --- a/toolkit/components/reader/Readability.js +++ b/toolkit/components/reader/Readability.js @@ -103,7 +103,7 @@ Readability.prototype = { byline: /byline|author|dateline|writtenby/i, replaceFonts: /<(\/?)font[^>]*>/gi, normalize: /\s{2,}/g, - videos: /https?:\/\/(www\.)?(youtube|vimeo)\.com/i, + videos: /https?:\/\/(www\.)?(youtube|youtube-nocookie|player\.vimeo)\.com/i, nextLink: /(next|weiter|continue|>([^\|]|$)|»([^\|]|$))/i, prevLink: /(prev|earl|old|new|<|«)/i, whitespace: /^\s*$/, @@ -125,6 +125,36 @@ Readability.prototype = { this._fixRelativeUris(articleContent); }, + /** + * Iterate over a NodeList, which doesn't natively fully implement the Array + * interface. + * + * For convenience, the current object context is applied to the provided + * iterate function. + * + * @param NodeList nodeList The NodeList. + * @param Function fn The iterate function. + * @return void + */ + _forEachNode: function(nodeList, fn) { + return Array.prototype.forEach.call(nodeList, fn, this); + }, + + /** + * Iterate over a NodeList, return true if any of the provided iterate + * function calls returns true, false otherwise. + * + * For convenience, the current object context is applied to the + * provided iterate function. + * + * @param NodeList nodeList The NodeList. + * @param Function fn The iterate function. + * @return Boolean + */ + _someNode: function(nodeList, fn) { + return Array.prototype.some.call(nodeList, fn, this); + }, + /** * Converts each and uri in the given element to an absolute URI. * @@ -149,6 +179,10 @@ Readability.prototype = { if (uri[0] == "/") return prePath + uri; + // Dotslash relative URI. + if (uri.indexOf("./") === 0) + return pathBase + uri.slice(2); + // Standard relative URI; add entire path. pathBase already includes a // trailing "/". return pathBase + uri; @@ -156,19 +190,18 @@ Readability.prototype = { function convertRelativeURIs(tagName, propName) { var elems = articleContent.getElementsByTagName(tagName); - for (var i = elems.length; --i >= 0;) { - var elem = elems[i]; + this._forEachNode(elems, function(elem) { var relativeURI = elem.getAttribute(propName); if (relativeURI != null) - elems[i].setAttribute(propName, toAbsoluteURI(relativeURI)); - } + elem.setAttribute(propName, toAbsoluteURI(relativeURI)); + }); } // Fix links. - convertRelativeURIs("a", "href"); + convertRelativeURIs.call(this, "a", "href"); // Fix images. - convertRelativeURIs("img", "src"); + convertRelativeURIs.call(this, "img", "src"); }, /** @@ -224,19 +257,17 @@ Readability.prototype = { var doc = this._doc; // Remove all style tags in head - var styleTags = doc.getElementsByTagName("style"); - for (var st = styleTags.length - 1; st >= 0; st -= 1) { - styleTags[st].parentNode.removeChild(styleTags[st]); - } + this._forEachNode(doc.getElementsByTagName("style"), function(styleNode) { + styleNode.parentNode.removeChild(styleNode); + }); if (doc.body) { this._replaceBrs(doc.body); } - var fonts = doc.getElementsByTagName("FONT"); - for (var i = fonts.length; --i >=0;) { - this._setNodeTag(fonts[i], "SPAN"); - } + this._forEachNode(doc.getElementsByTagName("font"), function(fontNode) { + this._setNodeTag(fontNode, "SPAN"); + }); }, /** @@ -262,9 +293,7 @@ Readability.prototype = { *
foo
bar

abc

*/ _replaceBrs: function (elem) { - var brs = elem.getElementsByTagName("br"); - for (var i = 0; i < brs.length; i++) { - var br = brs[i]; + this._forEachNode(elem.getElementsByTagName("br"), function(br) { var next = br.nextSibling; // Whether 2 or more
elements have been found and replaced with a @@ -303,7 +332,7 @@ Readability.prototype = { next = sibling; } } - } + }); }, _setNodeTag: function (node, tag) { @@ -326,6 +355,7 @@ Readability.prototype = { // Clean out junk from the article content this._cleanConditionally(articleContent, "form"); this._clean(articleContent, "object"); + this._clean(articleContent, "embed"); this._clean(articleContent, "h1"); // If there is only one h2, they are probably using it as a header @@ -343,26 +373,23 @@ Readability.prototype = { this._cleanConditionally(articleContent, "div"); // Remove extra paragraphs - var articleParagraphs = articleContent.getElementsByTagName('p'); - for (var i = articleParagraphs.length - 1; i >= 0; i -= 1) { - var imgCount = articleParagraphs[i].getElementsByTagName('img').length; - var embedCount = articleParagraphs[i].getElementsByTagName('embed').length; - var objectCount = articleParagraphs[i].getElementsByTagName('object').length; + this._forEachNode(articleContent.getElementsByTagName('p'), function(paragraph) { + var imgCount = paragraph.getElementsByTagName('img').length; + var embedCount = paragraph.getElementsByTagName('embed').length; + var objectCount = paragraph.getElementsByTagName('object').length; + // At this point, nasty iframes have been removed, only remain embedded video ones. + var iframeCount = paragraph.getElementsByTagName('iframe').length; + var totalCount = imgCount + embedCount + objectCount + iframeCount; - if (imgCount === 0 && - embedCount === 0 && - objectCount === 0 && - this._getInnerText(articleParagraphs[i], false) === '') - articleParagraphs[i].parentNode.removeChild(articleParagraphs[i]); - } + if (totalCount === 0 && !this._getInnerText(paragraph, false)) + paragraph.parentNode.removeChild(paragraph); + }); - var brs = articleContent.getElementsByTagName("BR"); - for (var i = brs.length; --i >= 0;) { - var br = brs[i]; + this._forEachNode(articleContent.getElementsByTagName("br"), function(br) { var next = this._nextElement(br.nextSibling); if (next && next.tagName == "P") br.parentNode.removeChild(br); - } + }); }, /** @@ -529,8 +556,7 @@ Readability.prototype = { elementsToScore.push(node); } else { // EXPERIMENTAL - for (var i = 0, il = node.childNodes.length; i < il; i += 1) { - var childNode = node.childNodes[i]; + this._forEachNode(node.childNodes, function(childNode) { if (childNode.nodeType === Node.TEXT_NODE) { var p = doc.createElement('p'); p.textContent = childNode.textContent; @@ -538,7 +564,7 @@ Readability.prototype = { p.className = 'readability-styled'; node.replaceChild(p, childNode); } - } + }); } } node = this._getNextNode(node); @@ -551,17 +577,17 @@ Readability.prototype = { * A score is determined by things like number of commas, class names, etc. Maybe eventually link density. **/ var candidates = []; - for (var pt = 0; pt < elementsToScore.length; pt += 1) { - var parentNode = elementsToScore[pt].parentNode; + this._forEachNode(elementsToScore, function(elementToScore) { + var parentNode = elementToScore.parentNode; var grandParentNode = parentNode ? parentNode.parentNode : null; - var innerText = this._getInnerText(elementsToScore[pt]); + var innerText = this._getInnerText(elementToScore); if (!parentNode || typeof(parentNode.tagName) === 'undefined') - continue; + return; // If this paragraph is less than 25 characters, don't even count it. if (innerText.length < 25) - continue; + return; // Initialize readability data for the parent. if (typeof parentNode.readability === 'undefined') { @@ -593,7 +619,7 @@ Readability.prototype = { if (grandParentNode) grandParentNode.readability.contentScore += contentScore / 2; - } + }); // After we've calculated scores, loop through all of the possible // candidate nodes we found and find the one with the highest score. @@ -650,9 +676,9 @@ Readability.prototype = { // below does some of that - but only if we've looked high enough up the DOM // tree. var parentOfTopCandidate = topCandidate.parentNode; + var lastScore = topCandidate.readability.contentScore; // The scores shouldn't get too low. - var scoreThreshold = topCandidate.readability.contentScore / 3; - var lastScore = parentOfTopCandidate.readability.contentScore; + var scoreThreshold = lastScore / 3; while (parentOfTopCandidate && parentOfTopCandidate.readability) { var parentScore = parentOfTopCandidate.readability.contentScore; if (parentScore < scoreThreshold) @@ -662,6 +688,7 @@ Readability.prototype = { topCandidate = parentOfTopCandidate; break; } + lastScore = parentOfTopCandidate.readability.contentScore; parentOfTopCandidate = parentOfTopCandidate.parentNode; } } @@ -804,7 +831,7 @@ Readability.prototype = { /** * Attempts to get excerpt and byline metadata for the article. - * + * * @return Object with optional "excerpt" and "byline" properties */ _getArticleMetadata: function() { @@ -820,14 +847,13 @@ Readability.prototype = { var propertyPattern = /^\s*og\s*:\s*description\s*$/gi; // Find description tags. - for (var i = 0; i < metaElements.length; i++) { - var element = metaElements[i]; + this._forEachNode(metaElements, function(element) { var elementName = element.getAttribute("name"); var elementProperty = element.getAttribute("property"); if (elementName === "author") { metadata.byline = element.getAttribute("content"); - continue; + return; } var name = null; @@ -846,7 +872,7 @@ Readability.prototype = { values[name] = content.trim(); } } - } + }); if ("description" in values) { metadata.excerpt = values["description"]; @@ -867,14 +893,13 @@ Readability.prototype = { * @param Element **/ _removeScripts: function(doc) { - var scripts = doc.getElementsByTagName('script'); - for (var i = scripts.length - 1; i >= 0; i -= 1) { - scripts[i].nodeValue=""; - scripts[i].removeAttribute('src'); + this._forEachNode(doc.getElementsByTagName('script'), function(scriptNode) { + scriptNode.nodeValue = ""; + scriptNode.removeAttribute('src'); - if (scripts[i].parentNode) - scripts[i].parentNode.removeChild(scripts[i]); - } + if (scriptNode.parentNode) + scriptNode.parentNode.removeChild(scriptNode); + }); }, /** @@ -884,22 +909,17 @@ Readability.prototype = { * * @param Element **/ - _hasSinglePInsideElement: function(e) { + _hasSinglePInsideElement: function(element) { // There should be exactly 1 element child which is a P: - if (e.children.length != 1 || e.firstElementChild.tagName !== "P") { + if (element.children.length != 1 || element.firstElementChild.tagName !== "P") { return false; } - // And there should be no text nodes with real content - var childNodes = e.childNodes; - for (var i = childNodes.length; --i >= 0;) { - var node = childNodes[i]; - if (node.nodeType == Node.TEXT_NODE && - this.REGEXPS.hasContent.test(node.textContent)) { - return false; - } - } - return true; + // And there should be no text nodes with real content + return !this._someNode(element.childNodes, function(node) { + return node.nodeType === Node.TEXT_NODE && + this.REGEXPS.hasContent.test(node.textContent); + }); }, /** @@ -907,14 +927,11 @@ Readability.prototype = { * * @param Element */ - _hasChildBlockElement: function (e) { - var length = e.children.length; - for (var i = 0; i < length; i++) { - var child = e.children[i]; - if (this.DIV_TO_P_ELEMS.indexOf(child.tagName) !== -1 || this._hasChildBlockElement(child)) - return true; - } - return false; + _hasChildBlockElement: function (element) { + return this._someNode(element.childNodes, function(node) { + return this.DIV_TO_P_ELEMS.indexOf(node.tagName) !== -1 || + this._hasChildBlockElement(node); + }); }, /** @@ -922,11 +939,12 @@ Readability.prototype = { * This also strips out any excess whitespace to be found. * * @param Element + * @param Boolean normalizeSpaces (default: true) * @return string **/ _getInnerText: function(e, normalizeSpaces) { - var textContent = e.textContent.trim(); normalizeSpaces = (typeof normalizeSpaces === 'undefined') ? true : normalizeSpaces; + var textContent = e.textContent.trim(); if (normalizeSpaces) { return textContent.replace(this.REGEXPS.normalize, " "); @@ -985,14 +1003,17 @@ Readability.prototype = { * @param Element * @return number (float) **/ - _getLinkDensity: function(e) { - var links = e.getElementsByTagName("a"); - var textLength = this._getInnerText(e).length; + _getLinkDensity: function(element) { + var textLength = this._getInnerText(element).length; + if (textLength === 0) + return; + var linkLength = 0; - for (var i = 0, il = links.length; i < il; i += 1) { - linkLength += this._getInnerText(links[i]).length; - } + // XXX implement _reduceNodeList? + this._forEachNode(element.getElementsByTagName("a"), function(linkNode) { + linkLength += this._getInnerText(linkNode).length; + }); return linkLength / textLength; }, @@ -1405,28 +1426,26 @@ Readability.prototype = { * @return void **/ _clean: function(e, tag) { - var targetList = e.getElementsByTagName(tag); - var isEmbed = (tag === 'object' || tag === 'embed'); + var isEmbed = ["object", "embed", "iframe"].indexOf(tag) !== -1; - for (var y = targetList.length - 1; y >= 0; y -= 1) { + this._forEachNode(e.getElementsByTagName(tag), function(element) { // Allow youtube and vimeo videos through as people usually want to see those. if (isEmbed) { - var attributeValues = ""; - for (var i = 0, il = targetList[y].attributes.length; i < il; i += 1) { - attributeValues += targetList[y].attributes[i].value + '|'; - } + var attributeValues = [].map.call(element.attributes, function(attr) { + return attr.value; + }).join("|"); // First, check the elements attributes to see if any of them contain youtube or vimeo if (this.REGEXPS.videos.test(attributeValues)) - continue; + return; // Then check the elements inside this element for the same. - if (this.REGEXPS.videos.test(targetList[y].innerHTML)) - continue; + if (this.REGEXPS.videos.test(element.innerHTML)) + return; } - targetList[y].parentNode.removeChild(targetList[y]); - } + element.parentNode.removeChild(element); + }); }, /** @@ -1578,7 +1597,7 @@ Readability.prototype = { if (!metadata.excerpt) { var paragraphs = articleContent.getElementsByTagName("p"); if (paragraphs.length > 0) { - metadata.excerpt = paragraphs[0].textContent; + metadata.excerpt = paragraphs[0].textContent.trim(); } } From 9bd33b3e6e6469a59fca5094dca5c0baac38640f Mon Sep 17 00:00:00 2001 From: Mantaroh Yoshinaga Date: Thu, 26 Feb 2015 02:39:00 -0500 Subject: [PATCH 02/31] Bug 910634 - Disabled should not popup a dialog. r=wesj --- mobile/android/chrome/content/InputWidgetHelper.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/mobile/android/chrome/content/InputWidgetHelper.js b/mobile/android/chrome/content/InputWidgetHelper.js index f52032cae7f..d626b23304b 100644 --- a/mobile/android/chrome/content/InputWidgetHelper.js +++ b/mobile/android/chrome/content/InputWidgetHelper.js @@ -13,7 +13,7 @@ var InputWidgetHelper = { handleClick: function(aTarget) { // if we're busy looking at a InputWidget we want to eat any clicks that // come to us, but not to process them - if (this._uiBusy || !this.hasInputWidget(aTarget)) + if (this._uiBusy || !this.hasInputWidget(aTarget) || this._isDisabledElement(aTarget)) return; this._uiBusy = true; @@ -81,5 +81,16 @@ var InputWidgetHelper = { setTimeout(function() { aElement.dispatchEvent(evt); }, 0); + }, + + _isDisabledElement : function(aElement) { + let currentElement = aElement; + while (currentElement) { + if (currentElement.disabled) + return true; + + currentElement = currentElement.parentElement; + } + return false; } }; From e70fd3ed2b68536b5ae0849bf0ab4f859c592bc5 Mon Sep 17 00:00:00 2001 From: Brian Grinstead Date: Wed, 18 Mar 2015 13:29:00 -0400 Subject: [PATCH 03/31] Bug 1066504 - Allow timeline range selection to continue even after mouseout of the graph window. r=vporof --- .../shared/test/browser_graphs-07a.js | 25 ++++++++++--- browser/devtools/shared/widgets/Graphs.jsm | 36 +++++++++++-------- 2 files changed, 42 insertions(+), 19 deletions(-) diff --git a/browser/devtools/shared/test/browser_graphs-07a.js b/browser/devtools/shared/test/browser_graphs-07a.js index c55d75f5387..2b641190aad 100644 --- a/browser/devtools/shared/test/browser_graphs-07a.js +++ b/browser/devtools/shared/test/browser_graphs-07a.js @@ -17,14 +17,18 @@ function* performTest() { let [host, win, doc] = yield createHost(); let graph = new LineGraphWidget(doc.body, "fps"); yield graph.once("ready"); - - testGraph(graph); - + testGraph(graph, normalDragStop); yield graph.destroy(); + + let graph2 = new LineGraphWidget(doc.body, "fps"); + yield graph2.once("ready"); + testGraph(graph2, buggyDragStop); + yield graph2.destroy(); + host.destroy(); } -function testGraph(graph) { +function testGraph(graph, dragStop) { graph.setData(TEST_DATA); info("Making a selection."); @@ -186,13 +190,24 @@ function dragStart(graph, x, y = 1) { graph._onMouseDown({ clientX: x, clientY: y }); } -function dragStop(graph, x, y = 1) { +function normalDragStop(graph, x, y = 1) { x /= window.devicePixelRatio; y /= window.devicePixelRatio; graph._onMouseMove({ clientX: x, clientY: y }); graph._onMouseUp({ clientX: x, clientY: y }); } +function buggyDragStop(graph, x, y = 1) { + x /= window.devicePixelRatio; + y /= window.devicePixelRatio; + + // Only fire a mousemove instead of a mouseup. + // This happens when the mouseup happens outside of the toolbox, + // see Bug 1066504. + graph._onMouseMove({ clientX: x, clientY: y }); + graph._onMouseMove({ clientX: x, clientY: y, buttons: 0 }); +} + function scroll(graph, wheel, x, y = 1) { x /= window.devicePixelRatio; y /= window.devicePixelRatio; diff --git a/browser/devtools/shared/widgets/Graphs.jsm b/browser/devtools/shared/widgets/Graphs.jsm index b7f644bc8fb..657158cbd40 100644 --- a/browser/devtools/shared/widgets/Graphs.jsm +++ b/browser/devtools/shared/widgets/Graphs.jsm @@ -181,6 +181,7 @@ this.AbstractCanvasGraph = function(parent, name, sharpness) { this._selection = new GraphArea(); this._selectionDragger = new GraphAreaDragger(); this._selectionResizer = new GraphAreaResizer(); + this._isMouseActive = false; this._onAnimationFrame = this._onAnimationFrame.bind(this); this._onMouseMove = this._onMouseMove.bind(this); @@ -952,13 +953,23 @@ AbstractCanvasGraph.prototype = { * Listener for the "mousemove" event on the graph's container. */ _onMouseMove: function(e) { + let resizer = this._selectionResizer; + let dragger = this._selectionDragger; + + // If a mouseup happened outside the toolbox and the current operation + // is causing the selection changed, then end it. + if (e.buttons == 0 && (this.hasSelectionInProgress() || + resizer.margin != null || + dragger.origin != null)) { + return this._onMouseUp(e); + } + let offset = this._getContainerOffset(); let mouseX = (e.clientX - offset.left) * this._pixelRatio; let mouseY = (e.clientY - offset.top) * this._pixelRatio; this._cursor.x = mouseX; this._cursor.y = mouseY; - let resizer = this._selectionResizer; if (resizer.margin != null) { this._selection[resizer.margin] = mouseX; this._shouldRedraw = true; @@ -966,7 +977,6 @@ AbstractCanvasGraph.prototype = { return; } - let dragger = this._selectionDragger; if (dragger.origin != null) { this._selection.start = dragger.anchor.start - dragger.origin + mouseX; this._selection.end = dragger.anchor.end - dragger.origin + mouseX; @@ -1013,6 +1023,7 @@ AbstractCanvasGraph.prototype = { * Listener for the "mousedown" event on the graph's container. */ _onMouseDown: function(e) { + this._isMouseActive = true; let offset = this._getContainerOffset(); let mouseX = (e.clientX - offset.left) * this._pixelRatio; @@ -1051,6 +1062,7 @@ AbstractCanvasGraph.prototype = { * Listener for the "mouseup" event on the graph's container. */ _onMouseUp: function(e) { + this._isMouseActive = false; let offset = this._getContainerOffset(); let mouseX = (e.clientX - offset.left) * this._pixelRatio; @@ -1161,21 +1173,17 @@ AbstractCanvasGraph.prototype = { this.emit("scroll"); }, - /** + /** * Listener for the "mouseout" event on the graph's container. + * Clear any active cursors if a drag isn't happening. */ - _onMouseOut: function() { - if (this.hasSelectionInProgress()) { - this.dropSelection(); + _onMouseOut: function(e) { + if (!this._isMouseActive) { + this._cursor.x = null; + this._cursor.y = null; + this._canvas.removeAttribute("input"); + this._shouldRedraw = true; } - - this._cursor.x = null; - this._cursor.y = null; - this._selectionResizer.margin = null; - this._selectionDragger.origin = null; - - this._canvas.removeAttribute("input"); - this._shouldRedraw = true; }, /** From bcec7b58d71f9a621d57cbca395fac975d6217dd Mon Sep 17 00:00:00 2001 From: Abhinav Koppula Date: Wed, 18 Mar 2015 16:22:00 -0400 Subject: [PATCH 04/31] Bug 1127337 - Show article favicon in the browser tab in reader mode. r=jaws --- browser/modules/ReaderParent.jsm | 14 +++++++++++++- toolkit/components/places/PlacesUtils.jsm | 2 +- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/browser/modules/ReaderParent.jsm b/browser/modules/ReaderParent.jsm index 50e698f842c..3b4f76d7278 100644 --- a/browser/modules/ReaderParent.jsm +++ b/browser/modules/ReaderParent.jsm @@ -13,6 +13,7 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/Task.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils","resource://gre/modules/PlacesUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "ReaderMode", "resource://gre/modules/ReaderMode.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "ReadingList", "resource:///modules/readinglist/ReadingList.jsm"); @@ -56,7 +57,18 @@ let ReaderParent = { break; case "Reader:FaviconRequest": { - // XXX: To implement. + if (message.target.messageManager) { + let faviconUrl = PlacesUtils.promiseFaviconLinkUrl(message.data.url); + faviconUrl.then(function onResolution(favicon) { + message.target.messageManager.sendAsyncMessage("Reader:FaviconReturn", { + url: message.data.url, + faviconUrl: favicon.path.replace(/^favicon:/, "") + }) + }, + function onRejection(reason) { + Cu.reportError("Error requesting favicon URL for about:reader content: " + reason); + }).catch(Cu.reportError); + } break; } case "Reader:ListStatusRequest": diff --git a/toolkit/components/places/PlacesUtils.jsm b/toolkit/components/places/PlacesUtils.jsm index 03fc0ec4fb8..4d3f139a583 100644 --- a/toolkit/components/places/PlacesUtils.jsm +++ b/toolkit/components/places/PlacesUtils.jsm @@ -1562,7 +1562,7 @@ this.PlacesUtils = { uri = PlacesUtils.favicons.getFaviconLinkForIcon(uri); deferred.resolve(uri); } else { - deferred.reject(); + deferred.reject("favicon not found for uri"); } }); return deferred.promise; From 3cd52de3e0e0aeb209e788408c9a899a4999b634 Mon Sep 17 00:00:00 2001 From: Michael Ratcliffe Date: Wed, 18 Mar 2015 10:04:59 +0000 Subject: [PATCH 05/31] Bug 1144363 - Fix this._telemetry is undefined in gDevTools. r=bgrins --- browser/devtools/framework/gDevTools.jsm | 5 +++-- toolkit/components/telemetry/Histograms.json | 8 ++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/browser/devtools/framework/gDevTools.jsm b/browser/devtools/framework/gDevTools.jsm index 07d02aa1801..2cd5a269525 100644 --- a/browser/devtools/framework/gDevTools.jsm +++ b/browser/devtools/framework/gDevTools.jsm @@ -31,8 +31,8 @@ const Telemetry = devtools.require("devtools/shared/telemetry"); const TABS_OPEN_PEAK_HISTOGRAM = "DEVTOOLS_TABS_OPEN_PEAK_LINEAR"; const TABS_OPEN_AVG_HISTOGRAM = "DEVTOOLS_TABS_OPEN_AVERAGE_LINEAR"; -const TABS_PINNED_PEAK_HISTOGRAM = "DEVTOOLS_TABS_PINNED_PEAK_EXPONENTIAL"; -const TABS_PINNED_AVG_HISTOGRAM = "DEVTOOLS_TABS_PINNED_AVERAGE_EXPONENTIAL"; +const TABS_PINNED_PEAK_HISTOGRAM = "DEVTOOLS_TABS_PINNED_PEAK_LINEAR"; +const TABS_PINNED_AVG_HISTOGRAM = "DEVTOOLS_TABS_PINNED_AVERAGE_LINEAR"; const FORBIDDEN_IDS = new Set(["toolbox", ""]); const MAX_ORDINAL = 99; @@ -47,6 +47,7 @@ this.DevTools = function DevTools() { this._tools = new Map(); // Map this._themes = new Map(); // Map this._toolboxes = new Map(); // Map + this._telemetry = new Telemetry(); // destroy() is an observer's handler so we need to preserve context. this.destroy = this.destroy.bind(this); diff --git a/toolkit/components/telemetry/Histograms.json b/toolkit/components/telemetry/Histograms.json index 5c74d25cb74..06051e09923 100644 --- a/toolkit/components/telemetry/Histograms.json +++ b/toolkit/components/telemetry/Histograms.json @@ -6571,16 +6571,16 @@ "n_buckets": 100, "description": "The peak number of open tabs in all windows for a session for devtools users." }, - "DEVTOOLS_TABS_OPEN_AVERAGE_EXPONENTIAL": { + "DEVTOOLS_TABS_OPEN_AVERAGE_LINEAR": { "expires_in_version": "never", - "kind": "exponential", + "kind": "linear", "high": "101", "n_buckets": "100", "description": "The mean number of open tabs in all windows for a session for devtools users." }, - "DEVTOOLS_TABS_PINNED_PEAK_EXPONENTIAL": { + "DEVTOOLS_TABS_PINNED_PEAK_LINEAR": { "expires_in_version": "never", - "kind": "exponential", + "kind": "linear", "high": "101", "n_buckets": "100", "description": "The peak number of pinned tabs (app tabs) in all windows for a session for devtools users." From 3896fdf013f8cb783045617e54b8fc385e77a365 Mon Sep 17 00:00:00 2001 From: John Giannakos Date: Mon, 23 Mar 2015 08:01:00 -0400 Subject: [PATCH 06/31] Bug 1134568 - Implement a preset gallery for CubicBezierWidget incl. 8 preset functions for categories: ease-in, ease-out, and ease-in-out. r=pbrosset --- browser/devtools/shared/moz.build | 1 + browser/devtools/shared/test/browser.ini | 3 + .../shared/test/browser_cubic-bezier-01.js | 8 +- .../shared/test/browser_cubic-bezier-02.js | 89 +++- .../shared/test/browser_cubic-bezier-03.js | 3 +- .../shared/test/browser_cubic-bezier-04.js | 51 +++ .../shared/test/browser_cubic-bezier-05.js | 49 +++ .../shared/test/browser_cubic-bezier-06.js | 80 ++++ .../shared/test/unit/test_bezierCanvas.js | 5 +- .../shared/test/unit/test_cubicBezier.js | 22 +- .../shared/widgets/CubicBezierPresets.js | 64 +++ .../shared/widgets/CubicBezierWidget.js | 404 +++++++++++++++--- browser/devtools/shared/widgets/Tooltip.js | 4 +- .../shared/widgets/cubic-bezier-frame.xhtml | 9 +- .../devtools/shared/widgets/cubic-bezier.css | 206 ++++++--- 15 files changed, 862 insertions(+), 136 deletions(-) create mode 100644 browser/devtools/shared/test/browser_cubic-bezier-04.js create mode 100644 browser/devtools/shared/test/browser_cubic-bezier-05.js create mode 100644 browser/devtools/shared/test/browser_cubic-bezier-06.js create mode 100644 browser/devtools/shared/widgets/CubicBezierPresets.js diff --git a/browser/devtools/shared/moz.build b/browser/devtools/shared/moz.build index e7f2995d1d5..70ce730c7da 100644 --- a/browser/devtools/shared/moz.build +++ b/browser/devtools/shared/moz.build @@ -61,6 +61,7 @@ EXTRA_JS_MODULES.devtools.shared += [ ] EXTRA_JS_MODULES.devtools.shared.widgets += [ + 'widgets/CubicBezierPresets.js', 'widgets/CubicBezierWidget.js', 'widgets/FastListWidget.js', 'widgets/Spectrum.js', diff --git a/browser/devtools/shared/test/browser.ini b/browser/devtools/shared/test/browser.ini index 71bfda0caa1..06005ab532e 100644 --- a/browser/devtools/shared/test/browser.ini +++ b/browser/devtools/shared/test/browser.ini @@ -15,6 +15,9 @@ support-files = [browser_cubic-bezier-01.js] [browser_cubic-bezier-02.js] [browser_cubic-bezier-03.js] +[browser_cubic-bezier-04.js] +[browser_cubic-bezier-05.js] +[browser_cubic-bezier-06.js] [browser_flame-graph-01.js] [browser_flame-graph-02.js] [browser_flame-graph-03a.js] diff --git a/browser/devtools/shared/test/browser_cubic-bezier-01.js b/browser/devtools/shared/test/browser_cubic-bezier-01.js index 2e288c0ea74..85cd2ba4f19 100644 --- a/browser/devtools/shared/test/browser_cubic-bezier-01.js +++ b/browser/devtools/shared/test/browser_cubic-bezier-01.js @@ -7,16 +7,20 @@ // Tests that the CubicBezierWidget generates content in a given parent node const TEST_URI = "chrome://browser/content/devtools/cubic-bezier-frame.xhtml"; -const {CubicBezierWidget} = devtools.require("devtools/shared/widgets/CubicBezierWidget"); +const {CubicBezierWidget} = + devtools.require("devtools/shared/widgets/CubicBezierWidget"); add_task(function*() { yield promiseTab("about:blank"); let [host, win, doc] = yield createHost("bottom", TEST_URI); - info("Checking that the markup is created in the parent"); + info("Checking that the graph markup is created in the parent"); let container = doc.querySelector("#container"); let w = new CubicBezierWidget(container); + ok(container.querySelector(".display-wrap"), + "The display has been added"); + ok(container.querySelector(".coordinate-plane"), "The coordinate plane has been added"); let buttons = container.querySelectorAll("button"); diff --git a/browser/devtools/shared/test/browser_cubic-bezier-02.js b/browser/devtools/shared/test/browser_cubic-bezier-02.js index 30887a74d53..254614e52ea 100644 --- a/browser/devtools/shared/test/browser_cubic-bezier-02.js +++ b/browser/devtools/shared/test/browser_cubic-bezier-02.js @@ -7,26 +7,37 @@ // Tests the CubicBezierWidget events const TEST_URI = "chrome://browser/content/devtools/cubic-bezier-frame.xhtml"; -const {CubicBezierWidget, PREDEFINED} = +const {CubicBezierWidget} = devtools.require("devtools/shared/widgets/CubicBezierWidget"); +const {PREDEFINED} = require("devtools/shared/widgets/CubicBezierPresets"); add_task(function*() { yield promiseTab("about:blank"); let [host, win, doc] = yield createHost("bottom", TEST_URI); + // Required or widget will be clipped inside of 'bottom' + // host by -14. Setting `fixed` zeroes this which is needed for + // calculating offsets. Occurs in test env only. + doc.body.setAttribute("style", "position: fixed"); + let container = doc.querySelector("#container"); let w = new CubicBezierWidget(container, PREDEFINED.linear); - yield pointsCanBeDragged(w, win, doc); - yield curveCanBeClicked(w, win, doc); - yield pointsCanBeMovedWithKeyboard(w, win, doc); + let rect = w.curve.getBoundingClientRect(); + rect.graphTop = rect.height * w.bezierCanvas.padding[0]; + rect.graphBottom = rect.height - rect.graphTop; + rect.graphHeight = rect.graphBottom - rect.graphTop; + + yield pointsCanBeDragged(w, win, doc, rect); + yield curveCanBeClicked(w, win, doc, rect); + yield pointsCanBeMovedWithKeyboard(w, win, doc, rect); w.destroy(); host.destroy(); gBrowser.removeCurrentTab(); }); -function* pointsCanBeDragged(widget, win, doc) { +function* pointsCanBeDragged(widget, win, doc, offsets) { info("Checking that the control points can be dragged with the mouse"); info("Listening for the update event"); @@ -34,7 +45,7 @@ function* pointsCanBeDragged(widget, win, doc) { info("Generating a mousedown/move/up on P1"); widget._onPointMouseDown({target: widget.p1}); - doc.onmousemove({pageX: 0, pageY: 100}); + doc.onmousemove({pageX: offsets.left, pageY: offsets.graphTop}); doc.onmouseup(); let bezier = yield onUpdated; @@ -48,7 +59,7 @@ function* pointsCanBeDragged(widget, win, doc) { info("Generating a mousedown/move/up on P2"); widget._onPointMouseDown({target: widget.p2}); - doc.onmousemove({pageX: 200, pageY: 300}); + doc.onmousemove({pageX: offsets.right, pageY: offsets.graphBottom}); doc.onmouseup(); bezier = yield onUpdated; @@ -56,14 +67,16 @@ function* pointsCanBeDragged(widget, win, doc) { is(bezier.P2[1], 0, "The new P2 progress coordinate is correct"); } -function* curveCanBeClicked(widget, win, doc) { +function* curveCanBeClicked(widget, win, doc, offsets) { info("Checking that clicking on the curve moves the closest control point"); info("Listening for the update event"); let onUpdated = widget.once("updated"); info("Click close to P1"); - widget._onCurveClick({pageX: 50, pageY: 150}); + let x = offsets.left + (offsets.width / 4.0); + let y = offsets.graphTop + (offsets.graphHeight / 4.0); + widget._onCurveClick({pageX: x, pageY: y}); let bezier = yield onUpdated; ok(true, "The widget fired the updated event"); @@ -76,7 +89,9 @@ function* curveCanBeClicked(widget, win, doc) { onUpdated = widget.once("updated"); info("Click close to P2"); - widget._onCurveClick({pageX: 150, pageY: 250}); + x = offsets.right - (offsets.width / 4); + y = offsets.graphBottom - (offsets.graphHeight / 4); + widget._onCurveClick({pageX: x, pageY: y}); bezier = yield onUpdated; is(bezier.P2[0], 0.75, "The new P2 time coordinate is correct"); @@ -85,57 +100,89 @@ function* curveCanBeClicked(widget, win, doc) { is(bezier.P1[1], 0.75, "P1 progress coordinate remained unchanged"); } -function* pointsCanBeMovedWithKeyboard(widget, win, doc) { +function* pointsCanBeMovedWithKeyboard(widget, win, doc, offsets) { info("Checking that points respond to keyboard events"); + let singleStep = 3; + let shiftStep = 30; + info("Moving P1 to the left"); + let newOffset = parseInt(widget.p1.style.left) - singleStep; + let x = widget.bezierCanvas. + offsetsToCoordinates({style: {left: newOffset}})[0]; + let onUpdated = widget.once("updated"); widget._onPointKeyDown(getKeyEvent(widget.p1, 37)); let bezier = yield onUpdated; - is(bezier.P1[0], 0.235, "The new P1 time coordinate is correct"); + + is(bezier.P1[0], x, "The new P1 time coordinate is correct"); is(bezier.P1[1], 0.75, "The new P1 progress coordinate is correct"); info("Moving P1 to the left, fast"); + newOffset = parseInt(widget.p1.style.left) - shiftStep; + x = widget.bezierCanvas. + offsetsToCoordinates({style: {left: newOffset}})[0]; + onUpdated = widget.once("updated"); widget._onPointKeyDown(getKeyEvent(widget.p1, 37, true)); bezier = yield onUpdated; - is(bezier.P1[0], 0.085, "The new P1 time coordinate is correct"); + is(bezier.P1[0], x, "The new P1 time coordinate is correct"); is(bezier.P1[1], 0.75, "The new P1 progress coordinate is correct"); info("Moving P1 to the right, fast"); + newOffset = parseInt(widget.p1.style.left) + shiftStep; + x = widget.bezierCanvas. + offsetsToCoordinates({style: {left: newOffset}})[0]; + onUpdated = widget.once("updated"); widget._onPointKeyDown(getKeyEvent(widget.p1, 39, true)); bezier = yield onUpdated; - is(bezier.P1[0], 0.235, "The new P1 time coordinate is correct"); + is(bezier.P1[0], x, "The new P1 time coordinate is correct"); is(bezier.P1[1], 0.75, "The new P1 progress coordinate is correct"); info("Moving P1 to the bottom"); + newOffset = parseInt(widget.p1.style.top) + singleStep; + let y = widget.bezierCanvas. + offsetsToCoordinates({style: {top: newOffset}})[1]; + onUpdated = widget.once("updated"); widget._onPointKeyDown(getKeyEvent(widget.p1, 40)); bezier = yield onUpdated; - is(bezier.P1[0], 0.235, "The new P1 time coordinate is correct"); - is(bezier.P1[1], 0.735, "The new P1 progress coordinate is correct"); + is(bezier.P1[0], x, "The new P1 time coordinate is correct"); + is(bezier.P1[1], y, "The new P1 progress coordinate is correct"); info("Moving P1 to the bottom, fast"); + newOffset = parseInt(widget.p1.style.top) + shiftStep; + y = widget.bezierCanvas. + offsetsToCoordinates({style: {top: newOffset}})[1]; + onUpdated = widget.once("updated"); widget._onPointKeyDown(getKeyEvent(widget.p1, 40, true)); bezier = yield onUpdated; - is(bezier.P1[0], 0.235, "The new P1 time coordinate is correct"); - is(bezier.P1[1], 0.585, "The new P1 progress coordinate is correct"); + is(bezier.P1[0], x, "The new P1 time coordinate is correct"); + is(bezier.P1[1], y, "The new P1 progress coordinate is correct"); info("Moving P1 to the top, fast"); + newOffset = parseInt(widget.p1.style.top) - shiftStep; + y = widget.bezierCanvas. + offsetsToCoordinates({style: {top: newOffset}})[1]; + onUpdated = widget.once("updated"); widget._onPointKeyDown(getKeyEvent(widget.p1, 38, true)); bezier = yield onUpdated; - is(bezier.P1[0], 0.235, "The new P1 time coordinate is correct"); - is(bezier.P1[1], 0.735, "The new P1 progress coordinate is correct"); + is(bezier.P1[0], x, "The new P1 time coordinate is correct"); + is(bezier.P1[1], y, "The new P1 progress coordinate is correct"); info("Checking that keyboard events also work with P2"); info("Moving P2 to the left"); + newOffset = parseInt(widget.p2.style.left) - singleStep; + x = widget.bezierCanvas. + offsetsToCoordinates({style: {left: newOffset}})[0]; + onUpdated = widget.once("updated"); widget._onPointKeyDown(getKeyEvent(widget.p2, 37)); bezier = yield onUpdated; - is(bezier.P2[0], 0.735, "The new P2 time coordinate is correct"); + is(bezier.P2[0], x, "The new P2 time coordinate is correct"); is(bezier.P2[1], 0.25, "The new P2 progress coordinate is correct"); } diff --git a/browser/devtools/shared/test/browser_cubic-bezier-03.js b/browser/devtools/shared/test/browser_cubic-bezier-03.js index 2ce5fe4561d..2c231d5d921 100644 --- a/browser/devtools/shared/test/browser_cubic-bezier-03.js +++ b/browser/devtools/shared/test/browser_cubic-bezier-03.js @@ -7,8 +7,9 @@ // Tests that coordinates can be changed programatically in the CubicBezierWidget const TEST_URI = "chrome://browser/content/devtools/cubic-bezier-frame.xhtml"; -const {CubicBezierWidget, PREDEFINED} = +const {CubicBezierWidget} = devtools.require("devtools/shared/widgets/CubicBezierWidget"); +const {PREDEFINED} = require("devtools/shared/widgets/CubicBezierPresets"); add_task(function*() { yield promiseTab("about:blank"); diff --git a/browser/devtools/shared/test/browser_cubic-bezier-04.js b/browser/devtools/shared/test/browser_cubic-bezier-04.js new file mode 100644 index 00000000000..d6f447f8859 --- /dev/null +++ b/browser/devtools/shared/test/browser_cubic-bezier-04.js @@ -0,0 +1,51 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests that the CubicBezierPresetWidget generates markup. + +const TEST_URI = "chrome://browser/content/devtools/cubic-bezier-frame.xhtml"; +const {CubicBezierPresetWidget} = + devtools.require("devtools/shared/widgets/CubicBezierWidget"); +const {PRESETS} = require("devtools/shared/widgets/CubicBezierPresets"); + +add_task(function*() { + yield promiseTab("about:blank"); + let [host, win, doc] = yield createHost("bottom", TEST_URI); + + let container = doc.querySelector("#container"); + let w = new CubicBezierPresetWidget(container); + + info("Checking that the presets are created in the parent"); + ok(container.querySelector(".preset-pane"), + "The preset pane has been added"); + + ok(container.querySelector("#preset-categories"), + "The preset categories have been added"); + let categories = container.querySelectorAll(".category"); + is(categories.length, Object.keys(PRESETS).length, + "The preset categories have been added"); + Object.keys(PRESETS).forEach(category => { + ok(container.querySelector("#" + category), `${category} has been added`); + ok(container.querySelector("#preset-category-" + category), + `The preset list for ${category} has been added.`); + }); + + info("Checking that each of the presets and its preview have been added"); + Object.keys(PRESETS).forEach(category => { + Object.keys(PRESETS[category]).forEach(presetLabel => { + let preset = container.querySelector("#" + presetLabel); + ok(preset, `${presetLabel} has been added`); + ok(preset.querySelector("canvas"), + `${presetLabel}'s canvas preview has been added`); + ok(preset.querySelector("p"), + `${presetLabel}'s label has been added`); + }); + }); + + w.destroy(); + host.destroy(); + gBrowser.removeCurrentTab(); +}); diff --git a/browser/devtools/shared/test/browser_cubic-bezier-05.js b/browser/devtools/shared/test/browser_cubic-bezier-05.js new file mode 100644 index 00000000000..5c9ad0c57bc --- /dev/null +++ b/browser/devtools/shared/test/browser_cubic-bezier-05.js @@ -0,0 +1,49 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests that the CubicBezierPresetWidget cycles menus + +const TEST_URI = "chrome://browser/content/devtools/cubic-bezier-frame.xhtml"; +const {CubicBezierPresetWidget} = + devtools.require("devtools/shared/widgets/CubicBezierWidget"); +const {PREDEFINED, PRESETS, DEFAULT_PRESET_CATEGORY} = + require("devtools/shared/widgets/CubicBezierPresets"); + +add_task(function*() { + yield promiseTab("about:blank"); + let [host, win, doc] = yield createHost("bottom", TEST_URI); + + let container = doc.querySelector("#container"); + let w = new CubicBezierPresetWidget(container); + + info("Checking that preset is selected if coordinates are known"); + + w.refreshMenu([0, 0, 0, 0]); + is(w.activeCategory, container.querySelector(`#${DEFAULT_PRESET_CATEGORY}`), + "The default category is selected"); + is(w._activePreset, null, "There is no selected category"); + + w.refreshMenu(PREDEFINED["linear"]); + is(w.activeCategory, container.querySelector("#ease-in-out"), + "The ease-in-out category is active"); + is(w._activePreset, container.querySelector("#ease-in-out-linear"), + "The ease-in-out-linear preset is active"); + + w.refreshMenu(PRESETS["ease-out"]["ease-out-sine"]); + is(w.activeCategory, container.querySelector("#ease-out"), + "The ease-out category is active"); + is(w._activePreset, container.querySelector("#ease-out-sine"), + "The ease-out-sine preset is active"); + + w.refreshMenu([0, 0, 0, 0]); + is(w.activeCategory, container.querySelector("#ease-out"), + "The ease-out category is still active"); + is(w._activePreset, null, "No preset is active"); + + w.destroy(); + host.destroy(); + gBrowser.removeCurrentTab(); +}); diff --git a/browser/devtools/shared/test/browser_cubic-bezier-06.js b/browser/devtools/shared/test/browser_cubic-bezier-06.js new file mode 100644 index 00000000000..612589ee8b9 --- /dev/null +++ b/browser/devtools/shared/test/browser_cubic-bezier-06.js @@ -0,0 +1,80 @@ + +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests the integration between CubicBezierWidget and CubicBezierPresets + +const TEST_URI = "chrome://browser/content/devtools/cubic-bezier-frame.xhtml"; +const {CubicBezierWidget} = + devtools.require("devtools/shared/widgets/CubicBezierWidget"); +const {PRESETS} = require("devtools/shared/widgets/CubicBezierPresets"); + +add_task(function*() { + yield promiseTab("about:blank"); + let [host, win, doc] = yield createHost("bottom", TEST_URI); + + let container = doc.querySelector("#container"); + let w = new CubicBezierWidget(container, + PRESETS["ease-in"]["ease-in-sine"]); + w.presets.refreshMenu(PRESETS["ease-in"]["ease-in-sine"]); + + let rect = w.curve.getBoundingClientRect(); + rect.graphTop = rect.height * w.bezierCanvas.padding[0]; + + yield adjustingBezierUpdatesPreset(w, win, doc, rect); + yield selectingPresetUpdatesBezier(w, win, doc, rect); + + w.destroy(); + host.destroy(); + gBrowser.removeCurrentTab(); +}); + +function* adjustingBezierUpdatesPreset(widget, win, doc, rect) { + info("Checking that changing the bezier refreshes the preset menu"); + + is(widget.presets.activeCategory, + doc.querySelector("#ease-in"), + "The selected category is ease-in"); + + is(widget.presets._activePreset, + doc.querySelector("#ease-in-sine"), + "The selected preset is ease-in-sine"); + + info("Generating custom bezier curve by dragging"); + widget._onPointMouseDown({target: widget.p1}); + doc.onmousemove({pageX: rect.left, pageY: rect.graphTop}); + doc.onmouseup(); + + is(widget.presets.activeCategory, + doc.querySelector("#ease-in"), + "The selected category is still ease-in"); + + is(widget.presets._activePreset, null, + "There is no active preset"); + } + +function* selectingPresetUpdatesBezier(widget, win, doc, rect) { + info("Checking that selecting a preset updates bezier curve"); + + info("Listening for the new coordinates event"); + let onNewCoordinates = widget.presets.once("new-coordinates"); + let onUpdated = widget.once("updated"); + + info("Click a preset"); + let preset = doc.querySelector("#ease-in-sine"); + widget.presets._onPresetClick({currentTarget: preset}); + + yield onNewCoordinates; + ok(true, "The preset widget fired the new-coordinates event"); + + let bezier = yield onUpdated; + ok(true, "The bezier canvas fired the updated event"); + + is(bezier.P1[0], preset.coordinates[0], "The new P1 time coordinate is correct"); + is(bezier.P1[1], preset.coordinates[1], "The new P1 progress coordinate is correct"); + is(bezier.P2[0], preset.coordinates[2], "P2 time coordinate is correct "); + is(bezier.P2[1], preset.coordinates[3], "P2 progress coordinate is correct"); +} diff --git a/browser/devtools/shared/test/unit/test_bezierCanvas.js b/browser/devtools/shared/test/unit/test_bezierCanvas.js index 21ffad1f07d..55d2e8dbcdc 100644 --- a/browser/devtools/shared/test/unit/test_bezierCanvas.js +++ b/browser/devtools/shared/test/unit/test_bezierCanvas.js @@ -104,7 +104,10 @@ function getCanvasMock(w=200, h=400) { stroke: () => {}, arc: () => {}, fill: () => {}, - bezierCurveTo: () => {} + bezierCurveTo: () => {}, + save: () => {}, + restore: () => {}, + setTransform: () => {} }; }, width: w, diff --git a/browser/devtools/shared/test/unit/test_cubicBezier.js b/browser/devtools/shared/test/unit/test_cubicBezier.js index b8a6231b2ab..ee6c0e07bfd 100644 --- a/browser/devtools/shared/test/unit/test_cubicBezier.js +++ b/browser/devtools/shared/test/unit/test_cubicBezier.js @@ -19,6 +19,7 @@ function run_test() { coordinatesToStringOutputsAString(); pointGettersReturnPointCoordinatesArrays(); toStringOutputsCubicBezierValue(); + toStringOutputsCssPresetValues(); } function throwsWhenMissingCoordinates() { @@ -84,8 +85,27 @@ function pointGettersReturnPointCoordinatesArrays() { function toStringOutputsCubicBezierValue() { do_print("toString() outputs the cubic-bezier() value"); + let c = new CubicBezier([0, 1, 1, 0]); + do_check_eq(c.toString(), "cubic-bezier(0,1,1,0)"); +} + +function toStringOutputsCssPresetValues() { + do_print("toString() outputs the css predefined values"); + let c = new CubicBezier([0, 0, 1, 1]); - do_check_eq(c.toString(), "cubic-bezier(0,0,1,1)"); + do_check_eq(c.toString(), "linear"); + + c = new CubicBezier([0.25, 0.1, 0.25, 1]); + do_check_eq(c.toString(), "ease"); + + c = new CubicBezier([0.42, 0, 1, 1]); + do_check_eq(c.toString(), "ease-in"); + + c = new CubicBezier([0, 0, 0.58, 1]); + do_check_eq(c.toString(), "ease-out"); + + c = new CubicBezier([0.42, 0, 0.58, 1]); + do_check_eq(c.toString(), "ease-in-out"); } function do_check_throws(cb, info) { diff --git a/browser/devtools/shared/widgets/CubicBezierPresets.js b/browser/devtools/shared/widgets/CubicBezierPresets.js new file mode 100644 index 00000000000..d2a77a85c46 --- /dev/null +++ b/browser/devtools/shared/widgets/CubicBezierPresets.js @@ -0,0 +1,64 @@ +/** + * 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/. + */ + +// Set of preset definitions for use with CubicBezierWidget +// Credit: http://easings.net + +"use strict"; + +const PREDEFINED = { + "ease": [0.25, 0.1, 0.25, 1], + "linear": [0, 0, 1, 1], + "ease-in": [0.42, 0, 1, 1], + "ease-out": [0, 0, 0.58, 1], + "ease-in-out": [0.42, 0, 0.58, 1] +}; + +const PRESETS = { + "ease-in": { + "ease-in-linear": [0, 0, 1, 1], + "ease-in-ease-in": [0.42, 0, 1, 1], + "ease-in-sine": [0.47, 0, 0.74, 0.71], + "ease-in-quadratic": [0.55, 0.09, 0.68, 0.53], + "ease-in-cubic": [0.55, 0.06, 0.68, 0.19], + "ease-in-quartic": [0.9, 0.03, 0.69, 0.22], + "ease-in-quintic": [0.76, 0.05, 0.86, 0.06], + "ease-in-exponential": [0.95, 0.05, 0.8, 0.04], + "ease-in-circular": [0.6, 0.04, 0.98, 0.34], + "ease-in-backward": [0.6, -0.28, 0.74, 0.05] + }, + "ease-out": { + "ease-out-linear": [0, 0, 1, 1], + "ease-out-ease-out": [0, 0, 0.58, 1], + "ease-out-sine": [0.39, 0.58, 0.57, 1], + "ease-out-quadratic": [0.25, 0.46, 0.45, 0.94], + "ease-out-cubic": [0.22, 0.61, 0.36, 1], + "ease-out-quartic": [0.17, 0.84, 0.44, 1], + "ease-out-quintic": [0.23, 1, 0.32, 1], + "ease-out-exponential": [0.19, 1, 0.22, 1], + "ease-out-circular": [0.08, 0.82, 0.17, 1], + "ease-out-backward": [0.18, 0.89, 0.32, 1.28] + }, + "ease-in-out": { + "ease-in-out-linear": [0, 0, 1, 1], + "ease-in-out-ease": [0.25, 0.1, 0.25, 1], + "ease-in-out-ease-in-out": [0.42, 0, 0.58, 1], + "ease-in-out-sine": [0.45, 0.05, 0.55, 0.95], + "ease-in-out-quadratic": [0.46, 0.03, 0.52, 0.96], + "ease-in-out-cubic": [0.65, 0.05, 0.36, 1], + "ease-in-out-quartic": [0.77, 0, 0.18, 1], + "ease-in-out-quintic": [0.86, 0, 0.07, 1], + "ease-in-out-exponential": [1, 0, 0, 1], + "ease-in-out-circular": [0.79, 0.14, 0.15, 0.86], + "ease-in-out-backward": [0.68, -0.55, 0.27, 1.55] + } +}; + +const DEFAULT_PRESET_CATEGORY = Object.keys(PRESETS)[0]; + +exports.PRESETS = PRESETS; +exports.PREDEFINED = PREDEFINED; +exports.DEFAULT_PRESET_CATEGORY = DEFAULT_PRESET_CATEGORY; diff --git a/browser/devtools/shared/widgets/CubicBezierWidget.js b/browser/devtools/shared/widgets/CubicBezierWidget.js index 9177253b80b..a4e3fa3ad27 100644 --- a/browser/devtools/shared/widgets/CubicBezierWidget.js +++ b/browser/devtools/shared/widgets/CubicBezierWidget.js @@ -27,14 +27,7 @@ const EventEmitter = require("devtools/toolkit/event-emitter"); const {setTimeout, clearTimeout} = require("sdk/timers"); - -const PREDEFINED = exports.PREDEFINED = { - "ease": [.25, .1, .25, 1], - "linear": [0, 0, 1, 1], - "ease-in": [.42, 0, 1, 1], - "ease-out": [0, 0, .58, 1], - "ease-in-out": [.42, 0, .58, 1] -}; +const {PREDEFINED, PRESETS, DEFAULT_PRESET_CATEGORY} = require("devtools/shared/widgets/CubicBezierPresets"); /** * CubicBezier data structure helper @@ -59,7 +52,7 @@ function CubicBezier(coordinates) { return this.map(n => { return (Math.round(n * 100)/100 + '').replace(/^0\./, '.'); }) + ""; - } + }; } exports.CubicBezier = CubicBezier; @@ -74,7 +67,11 @@ CubicBezier.prototype = { }, toString: function() { - return 'cubic-bezier(' + this.coordinates + ')'; + // Check first if current coords are one of css predefined functions + let predefName = Object.keys(PREDEFINED) + .find(key => coordsAreEqual(PREDEFINED[key], this.coordinates)); + + return predefName || 'cubic-bezier(' + this.coordinates + ')'; } }; @@ -97,7 +94,7 @@ function BezierCanvas(canvas, bezier, padding) { -canvas.height * (1 - p[0] - p[2])); this.ctx.translate(p[3] / (1 - p[1] - p[3]), -1 - p[0] / (1 - p[0] - p[2])); -}; +} exports.BezierCanvas = BezierCanvas; @@ -115,7 +112,7 @@ BezierCanvas.prototype = { }, { left: w * (this.bezier.coordinates[2] * (1 - p[3] - p[1]) - p[3]) + 'px', top: h * (1 - this.bezier.coordinates[3] * (1 - p[0] - p[2]) - p[0]) + 'px' - }] + }]; }, /** @@ -128,8 +125,8 @@ BezierCanvas.prototype = { p = p.map(function(a, i) { return a * (i % 2? w : h)}); return [ - (parseInt(element.style.left) - p[3]) / (w + p[1] + p[3]), - (h - parseInt(element.style.top) - p[2]) / (h - p[0] - p[2]) + (parseFloat(element.style.left) - p[3]) / (w + p[1] + p[3]), + (h - parseFloat(element.style.top) - p[2]) / (h - p[0] - p[2]) ]; }, @@ -143,40 +140,48 @@ BezierCanvas.prototype = { handleColor: '#666', handleThickness: .008, bezierColor: '#4C9ED9', - bezierThickness: .015 + bezierThickness: .015, + drawHandles: true }; for (let setting in settings) { defaultSettings[setting] = settings[setting]; } - this.ctx.clearRect(-.5,-.5, 2, 2); + // Clear the canvas –making sure to clear the + // whole area by resetting the transform first. + this.ctx.save(); + this.ctx.setTransform(1, 0, 0, 1, 0, 0); + this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); + this.ctx.restore(); - // Draw control handles - this.ctx.beginPath(); - this.ctx.fillStyle = defaultSettings.handleColor; - this.ctx.lineWidth = defaultSettings.handleThickness; - this.ctx.strokeStyle = defaultSettings.handleColor; + if (defaultSettings.drawHandles) { + // Draw control handles + this.ctx.beginPath(); + this.ctx.fillStyle = defaultSettings.handleColor; + this.ctx.lineWidth = defaultSettings.handleThickness; + this.ctx.strokeStyle = defaultSettings.handleColor; - this.ctx.moveTo(0, 0); - this.ctx.lineTo(xy[0], xy[1]); - this.ctx.moveTo(1,1); - this.ctx.lineTo(xy[2], xy[3]); + this.ctx.moveTo(0, 0); + this.ctx.lineTo(xy[0], xy[1]); + this.ctx.moveTo(1,1); + this.ctx.lineTo(xy[2], xy[3]); - this.ctx.stroke(); - this.ctx.closePath(); + this.ctx.stroke(); + this.ctx.closePath(); - function circle(ctx, cx, cy, r) { - return ctx.beginPath(); - ctx.arc(cx, cy, r, 0, 2*Math.PI, !1); - ctx.closePath(); + var circle = function(ctx, cx, cy, r) { + return ctx.beginPath(); + ctx.arc(cx, cy, r, 0, 2*Math.PI, !1); + ctx.closePath(); + }; + + circle(this.ctx, xy[0], xy[1], 1.5 * defaultSettings.handleThickness); + this.ctx.fill(); + circle(this.ctx, xy[2], xy[3], 1.5 * defaultSettings.handleThickness); + this.ctx.fill(); } - circle(this.ctx, xy[0], xy[1], 1.5 * defaultSettings.handleThickness); - this.ctx.fill(); - circle(this.ctx, xy[2], xy[3], 1.5 * defaultSettings.handleThickness); - this.ctx.fill(); - // Draw bezier curve this.ctx.beginPath(); this.ctx.lineWidth = defaultSettings.bezierThickness; @@ -197,18 +202,20 @@ BezierCanvas.prototype = { * Emits "updated" events whenever the curve is changed. Along with the event is * sent a CubicBezier object */ -function CubicBezierWidget(parent, coordinates=PREDEFINED["ease-in-out"]) { +function CubicBezierWidget(parent, coordinates=PRESETS["ease-in"]["ease-in-sine"]) { + EventEmitter.decorate(this); + this.parent = parent; let {curve, p1, p2} = this._initMarkup(); - this.curve = curve; this.curveBoundingBox = curve.getBoundingClientRect(); + this.curve = curve; this.p1 = p1; this.p2 = p2; // Create and plot the bezier curve this.bezierCanvas = new BezierCanvas(this.curve, - new CubicBezier(coordinates), [.25, 0]); + new CubicBezier(coordinates), [0.30, 0]); this.bezierCanvas.plot(); // Place the control points @@ -221,12 +228,15 @@ function CubicBezierWidget(parent, coordinates=PREDEFINED["ease-in-out"]) { this._onPointMouseDown = this._onPointMouseDown.bind(this); this._onPointKeyDown = this._onPointKeyDown.bind(this); this._onCurveClick = this._onCurveClick.bind(this); - this._initEvents(); + this._onNewCoordinates = this._onNewCoordinates.bind(this); + + // Add preset preview menu + this.presets = new CubicBezierPresetWidget(parent); // Add the timing function previewer this.timingPreview = new TimingFunctionPreviewWidget(parent); - EventEmitter.decorate(this); + this._initEvents(); } exports.CubicBezierWidget = CubicBezierWidget; @@ -235,6 +245,9 @@ CubicBezierWidget.prototype = { _initMarkup: function() { let doc = this.parent.ownerDocument; + let wrap = doc.createElement("div"); + wrap.className = "display-wrap"; + let plane = doc.createElement("div"); plane.className = "coordinate-plane"; @@ -249,22 +262,24 @@ CubicBezierWidget.prototype = { plane.appendChild(p2); let curve = doc.createElement("canvas"); - curve.setAttribute("height", "400"); - curve.setAttribute("width", "200"); + curve.setAttribute("width", 150); + curve.setAttribute("height", 370); curve.id = "curve"; - plane.appendChild(curve); - this.parent.appendChild(plane); + plane.appendChild(curve); + wrap.appendChild(plane); + + this.parent.appendChild(wrap); return { p1: p1, p2: p2, curve: curve - } + }; }, _removeMarkup: function() { - this.parent.ownerDocument.querySelector(".coordinate-plane").remove(); + this.parent.ownerDocument.querySelector(".display-wrap").remove(); }, _initEvents: function() { @@ -275,6 +290,8 @@ CubicBezierWidget.prototype = { this.p2.addEventListener("keydown", this._onPointKeyDown); this.curve.addEventListener("click", this._onCurveClick); + + this.presets.on("new-coordinates", this._onNewCoordinates); }, _removeEvents: function() { @@ -285,6 +302,8 @@ CubicBezierWidget.prototype = { this.p2.removeEventListener("keydown", this._onPointKeyDown); this.curve.removeEventListener("click", this._onCurveClick); + + this.presets.off("new-coordinates", this._onNewCoordinates); }, _onPointMouseDown: function(event) { @@ -317,7 +336,7 @@ CubicBezierWidget.prototype = { doc.onmouseup = function () { point.focus(); doc.onmousemove = doc.onmouseup = null; - } + }; }, _onPointKeyDown: function(event) { @@ -344,6 +363,8 @@ CubicBezierWidget.prototype = { }, _onCurveClick: function(event) { + this.curveBoundingBox = this.curve.getBoundingClientRect(); + let left = this.curveBoundingBox.left; let top = this.curveBoundingBox.top; let x = event.pageX - left; @@ -362,14 +383,19 @@ CubicBezierWidget.prototype = { this._updateFromPoints(); }, + _onNewCoordinates: function(event, coordinates) { + this.coordinates = coordinates; + }, + /** * Get the current point coordinates and redraw the curve to match */ _updateFromPoints: function() { // Get the new coordinates from the point's offsets - let coordinates = this.bezierCanvas.offsetsToCoordinates(this.p1) + let coordinates = this.bezierCanvas.offsetsToCoordinates(this.p1); coordinates = coordinates.concat(this.bezierCanvas.offsetsToCoordinates(this.p2)); + this.presets.refreshMenu(coordinates); this._redraw(coordinates); }, @@ -391,7 +417,7 @@ CubicBezierWidget.prototype = { * @param {Array} coordinates */ set coordinates(coordinates) { - this._redraw(coordinates) + this._redraw(coordinates); // Move the points let offsets = this.bezierCanvas.offsets; @@ -420,6 +446,7 @@ CubicBezierWidget.prototype = { coordinates = value.replace(/cubic-bezier|\(|\)/g, "").split(",").map(parseFloat); } + this.presets.refreshMenu(coordinates); this.coordinates = coordinates; }, @@ -428,11 +455,262 @@ CubicBezierWidget.prototype = { this._removeMarkup(); this.timingPreview.destroy(); + this.presets.destroy(); this.curve = this.p1 = this.p2 = null; } }; +/** + * CubicBezierPreset widget. + * Builds a menu of presets from CubicBezierPresets + * @param {DOMNode} parent The container where the preset panel should be created + * + * Emits "new-coordinate" event along with the coordinates + * whenever a preset is selected. + */ +function CubicBezierPresetWidget(parent) { + this.parent = parent; + + let {presetPane, presets, categories} = this._initMarkup(); + this.presetPane = presetPane; + this.presets = presets; + this.categories = categories; + + this._activeCategory = null; + this._activePresetList = null; + this._activePreset = null; + + this._onCategoryClick = this._onCategoryClick.bind(this); + this._onPresetClick = this._onPresetClick.bind(this); + + EventEmitter.decorate(this); + this._initEvents(); +} + +exports.CubicBezierPresetWidget = CubicBezierPresetWidget; + +CubicBezierPresetWidget.prototype = { + /* + * Constructs a list of all preset categories and a list + * of presets for each category. + * + * High level markup: + * div .preset-pane + * div .preset-categories + * div .category + * div .category + * ... + * div .preset-container + * div .presetList + * div .preset + * ... + * div .presetList + * div .preset + * ... + */ + _initMarkup: function() { + let doc = this.parent.ownerDocument; + + let presetPane = doc.createElement("div"); + presetPane.className = "preset-pane"; + + let categoryList = doc.createElement("div"); + categoryList.id = "preset-categories"; + + let presetContainer = doc.createElement("div"); + presetContainer.id = "preset-container"; + + Object.keys(PRESETS).forEach(categoryLabel => { + let category = this._createCategory(categoryLabel); + categoryList.appendChild(category); + + let presetList = this._createPresetList(categoryLabel); + presetContainer.appendChild(presetList); + }); + + presetPane.appendChild(categoryList); + presetPane.appendChild(presetContainer); + + this.parent.appendChild(presetPane); + + let allCategories = presetPane.querySelectorAll(".category"); + let allPresets = presetPane.querySelectorAll(".preset"); + + return { + presetPane: presetPane, + presets: allPresets, + categories: allCategories + }; + }, + + _createCategory: function(categoryLabel) { + let doc = this.parent.ownerDocument; + + let category = doc.createElement("div"); + category.id = categoryLabel; + category.classList.add("category"); + + let categoryDisplayLabel = this._normalizeCategoryLabel(categoryLabel); + category.textContent = categoryDisplayLabel; + + return category; + }, + + _normalizeCategoryLabel: function(categoryLabel) { + return categoryLabel.replace("/-/g", " "); + }, + + _createPresetList: function(categoryLabel) { + let doc = this.parent.ownerDocument; + + let presetList = doc.createElement("div"); + presetList.id = "preset-category-" + categoryLabel; + presetList.classList.add("preset-list"); + + Object.keys(PRESETS[categoryLabel]).forEach(presetLabel => { + let preset = this._createPreset(categoryLabel, presetLabel); + presetList.appendChild(preset); + }); + + return presetList; + }, + + _createPreset: function(categoryLabel, presetLabel) { + let doc = this.parent.ownerDocument; + + let preset = doc.createElement("div"); + preset.classList.add("preset"); + preset.id = presetLabel; + preset.coordinates = PRESETS[categoryLabel][presetLabel]; + + // Create preset preview + let curve = doc.createElement("canvas"); + let bezier = new CubicBezier(preset.coordinates); + + curve.setAttribute("height", 55); + curve.setAttribute("width", 55); + + preset.bezierCanvas = new BezierCanvas(curve, bezier, [0.15, 0]); + preset.bezierCanvas.plot({ + drawHandles: false, + bezierThickness: 0.025 + }); + + preset.appendChild(curve); + + // Create preset label + let presetLabelElem = doc.createElement("p"); + let presetDisplayLabel = this._normalizePresetLabel(categoryLabel, presetLabel); + presetLabelElem.textContent = presetDisplayLabel; + preset.appendChild(presetLabelElem); + + return preset; + }, + + _normalizePresetLabel: function(categoryLabel, presetLabel) { + return presetLabel.replace(categoryLabel + "-", "").replace("/-/g", " "); + }, + + _initEvents: function() { + for (let category of this.categories) { + category.addEventListener("click", this._onCategoryClick); + } + + for (let preset of this.presets) { + preset.addEventListener("click", this._onPresetClick); + } + }, + + _removeEvents: function() { + for (let category of this.categories) { + category.removeEventListener("click", this._onCategoryClick); + } + + for (let preset of this.presets) { + preset.removeEventListener("click", this._onPresetClick); + } + }, + + _onPresetClick: function(event) { + this.emit("new-coordinates", event.currentTarget.coordinates); + this.activePreset = event.currentTarget; + }, + + _onCategoryClick: function(event) { + this.activeCategory = event.target; + }, + + _setActivePresetList: function(presetListId) { + let presetList = this.presetPane.querySelector("#" + presetListId); + swapClassName("active-preset-list", this._activePresetList, presetList); + this._activePresetList = presetList; + }, + + set activeCategory(category) { + swapClassName("active-category", this._activeCategory, category); + this._activeCategory = category; + this._setActivePresetList("preset-category-" + category.id); + }, + + get activeCategory() { + return this._activeCategory; + }, + + set activePreset(preset) { + swapClassName("active-preset", this._activePreset, preset); + this._activePreset = preset; + }, + + get activePreset() { + return this._activePreset; + }, + + /** + * Called by CubicBezierWidget onload and when + * the curve is modified via the canvas. + * Attempts to match the new user setting with an + * existing preset. + * @param {Array} coordinates new coords [i, j, k, l] + */ + refreshMenu: function(coordinates) { + // If we cannot find a matching preset, keep + // menu on last known preset category. + let category = this._activeCategory; + + // If we cannot find a matching preset + // deselect any selected preset. + let preset = null; + + // If a category has never been viewed before + // show the default category. + if (!category) { + category = this.parent.querySelector("#" + DEFAULT_PRESET_CATEGORY); + } + + // If the new coordinates do match a preset, + // set its category and preset button as active. + Object.keys(PRESETS).forEach(categoryLabel => { + + Object.keys(PRESETS[categoryLabel]).forEach(presetLabel => { + if (coordsAreEqual(PRESETS[categoryLabel][presetLabel], coordinates)) { + category = this.parent.querySelector("#" + categoryLabel); + preset = this.parent.querySelector("#" + presetLabel); + } + }); + + }); + + this.activeCategory = category; + this.activePreset = preset; + }, + + destroy: function() { + this._removeEvents(); + this.parent.querySelector(".preset-pane").remove(); + } +}; + /** * The TimingFunctionPreviewWidget animates a dot on a scale with a given * timing-function @@ -554,3 +832,29 @@ function isValidTimingFunction(value) { return false; } + +/** + * Removes a class from a node and adds it to another. + * @param {String} className the class to swap + * @param {DOMNode} from the node to remove the class from + * @param {DOMNode} to the node to add the class to + */ +function swapClassName(className, from, to) { + if (from !== null) { + from.classList.remove(className); + } + + if (to !== null) { + to.classList.add(className); + } +} + +/** + * Compares two arrays of coordinates [i, j, k, l] + * @param {Array} c1 first coordinate array to compare + * @param {Array} c2 second coordinate array to compare + * @return {Boolean} + */ +function coordsAreEqual(c1, c2) { + return c1.reduce((prev, curr, index) => prev && (curr === c2[index]), true); +} diff --git a/browser/devtools/shared/widgets/Tooltip.js b/browser/devtools/shared/widgets/Tooltip.js index 7b29cb998d5..5607196fe04 100644 --- a/browser/devtools/shared/widgets/Tooltip.js +++ b/browser/devtools/shared/widgets/Tooltip.js @@ -795,8 +795,8 @@ Tooltip.prototype = { // Create an iframe to host the cubic-bezier widget let iframe = this.doc.createElementNS(XHTML_NS, "iframe"); iframe.setAttribute("transparent", true); - iframe.setAttribute("width", "200"); - iframe.setAttribute("height", "415"); + iframe.setAttribute("width", "410"); + iframe.setAttribute("height", "360"); iframe.setAttribute("flex", "1"); iframe.setAttribute("class", "devtools-tooltip-iframe"); diff --git a/browser/devtools/shared/widgets/cubic-bezier-frame.xhtml b/browser/devtools/shared/widgets/cubic-bezier-frame.xhtml index f3c7a65b06b..8e2ac45fec8 100644 --- a/browser/devtools/shared/widgets/cubic-bezier-frame.xhtml +++ b/browser/devtools/shared/widgets/cubic-bezier-frame.xhtml @@ -8,14 +8,15 @@ - +