diff --git a/accessible/src/generic/HyperTextAccessible.cpp b/accessible/src/generic/HyperTextAccessible.cpp index ea130f3b2627..f929eddf4ded 100644 --- a/accessible/src/generic/HyperTextAccessible.cpp +++ b/accessible/src/generic/HyperTextAccessible.cpp @@ -772,9 +772,9 @@ HyperTextAccessible::GetRelativeOffset(nsIPresShell* aPresShell, } int32_t -HyperTextAccessible::FindBoundary(int32_t aOffset, nsDirection aDirection, - nsSelectionAmount aAmount, - EWordMovementType aWordMovementType) +HyperTextAccessible::FindOffset(int32_t aOffset, nsDirection aDirection, + nsSelectionAmount aAmount, + EWordMovementType aWordMovementType) { // Convert hypertext offset to frame-relative offset. int32_t offsetInFrame = aOffset, notUsedOffset = aOffset; @@ -803,6 +803,81 @@ HyperTextAccessible::FindBoundary(int32_t aOffset, nsDirection aDirection, aWordMovementType); } +int32_t +HyperTextAccessible::FindLineBoundary(int32_t aOffset, + EWhichLineBoundary aWhichLineBoundary) +{ + // Note: empty last line doesn't have own frame (a previous line contains '\n' + // character instead) thus when it makes a difference we need to process this + // case separately (otherwise operations are performed on previous line). + switch (aWhichLineBoundary) { + case ePrevLineBegin: { + // Fetch a previous line and move to its start (as arrow up and home keys + // were pressed). + if (IsEmptyLastLineOffset(aOffset)) + return FindOffset(aOffset, eDirPrevious, eSelectBeginLine); + + int32_t tmpOffset = FindOffset(aOffset, eDirPrevious, eSelectLine); + return FindOffset(tmpOffset, eDirPrevious, eSelectBeginLine); + } + + case ePrevLineEnd: { + if (IsEmptyLastLineOffset(aOffset)) + return aOffset - 1; + + // If offset is at first line then return 0 (first line start). + int32_t tmpOffset = FindOffset(aOffset, eDirPrevious, eSelectBeginLine); + if (tmpOffset == 0) + return 0; + + // Otherwise move to end of previous line (as arrow up and end keys were + // pressed). + tmpOffset = FindOffset(aOffset, eDirPrevious, eSelectLine); + return FindOffset(tmpOffset, eDirNext, eSelectEndLine); + } + + case eThisLineBegin: + if (IsEmptyLastLineOffset(aOffset)) + return aOffset; + + // Move to begin of the current line (as home key was pressed). + return FindOffset(aOffset, eDirPrevious, eSelectBeginLine); + + case eThisLineEnd: + if (IsEmptyLastLineOffset(aOffset)) + return aOffset; + + // Move to end of the current line (as end key was pressed). + return FindOffset(aOffset, eDirNext, eSelectEndLine); + + case eNextLineBegin: { + if (IsEmptyLastLineOffset(aOffset)) + return aOffset; + + // Move to begin of the next line if any (arrow down and home keys), + // otherwise end of the current line (arrow down only). + int32_t tmpOffset = FindOffset(aOffset, eDirNext, eSelectLine); + if (tmpOffset == CharacterCount()) + return tmpOffset; + + return FindOffset(tmpOffset, eDirPrevious, eSelectBeginLine); + } + + case eNextLineEnd: { + if (IsEmptyLastLineOffset(aOffset)) + return aOffset; + + // Move to next line end (as down arrow and end key were pressed). + int32_t tmpOffset = FindOffset(aOffset, eDirNext, eSelectLine); + if (tmpOffset != CharacterCount()) + return FindOffset(tmpOffset, eDirNext, eSelectEndLine); + return tmpOffset; + } + } + + return -1; +} + /* Gets the specified text relative to aBoundaryType, which means: BOUNDARY_CHAR The character before/at/after the offset is returned. @@ -1029,54 +1104,21 @@ HyperTextAccessible::GetTextBeforeOffset(int32_t aOffset, return GetText(*aStartOffset, *aEndOffset, aText); } - case BOUNDARY_LINE_START: { + case BOUNDARY_LINE_START: if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET) offset = AdjustCaretOffset(offset); - // If we are at last empty then home key and get the text (last empty line - // doesn't have own frame). - if (IsEmptyLastLineOffset(offset)) { - *aStartOffset = FindLineBoundary(offset, eDirPrevious, eSelectBeginLine); - *aEndOffset = offset; - return GetText(*aStartOffset, *aEndOffset, aText); - } - - // Home key, up arrow, home key. - *aEndOffset = FindLineBoundary(offset, eDirPrevious, eSelectBeginLine); - *aStartOffset = FindLineBoundary(offset, eDirPrevious, eSelectLine); - *aStartOffset = FindLineBoundary(*aStartOffset, eDirPrevious, eSelectBeginLine); - + *aStartOffset = FindLineBoundary(offset, ePrevLineBegin); + *aEndOffset = FindLineBoundary(offset, eThisLineBegin); return GetText(*aStartOffset, *aEndOffset, aText); - } - case BOUNDARY_LINE_END: { + case BOUNDARY_LINE_END: if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET) offset = AdjustCaretOffset(offset); - // Nothing if we are at first line. - int32_t tmpOffset = FindLineBoundary(offset, eDirPrevious, eSelectBeginLine); - if (tmpOffset == 0) { - *aStartOffset = *aEndOffset = 0; - return NS_OK; - } - - // Up arrow, end key to find previous line endings. - if (IsEmptyLastLineOffset(offset)) { // no own frame for a last line - tmpOffset = FindLineBoundary(offset, eDirPrevious, eSelectLine); - *aStartOffset = FindLineBoundary(tmpOffset, eDirNext, eSelectEndLine); - *aEndOffset = offset - 1; - return GetText(*aStartOffset, *aEndOffset, aText); - } - - tmpOffset = FindLineBoundary(offset, eDirPrevious, eSelectLine); - *aEndOffset = FindLineBoundary(tmpOffset, eDirNext, eSelectEndLine); - tmpOffset = FindLineBoundary(*aEndOffset, eDirPrevious, eSelectLine); - *aStartOffset = FindLineBoundary(tmpOffset, eDirNext, eSelectEndLine); - if (*aStartOffset == *aEndOffset) // we are at second line - *aStartOffset = 0; - + *aEndOffset = FindLineBoundary(offset, ePrevLineEnd); + *aStartOffset = FindLineBoundary(*aEndOffset, ePrevLineEnd); return GetText(*aStartOffset, *aEndOffset, aText); - } case BOUNDARY_ATTRIBUTE_RANGE: return GetTextHelper(eGetBefore, aBoundaryType, aOffset, @@ -1118,56 +1160,22 @@ HyperTextAccessible::GetTextAtOffset(int32_t aOffset, *aStartOffset = FindWordBoundary(*aEndOffset, eDirPrevious, eEndWord); return GetText(*aStartOffset, *aEndOffset, aText); - case BOUNDARY_LINE_START: { - // Empty last line doesn't have own frame (a previous line contains '\n' - // character instead) thus we can't operate on last line separately - // from previous line. - if (IsEmptyLastLineOffset(offset)) { - *aStartOffset = *aEndOffset = offset; - return NS_OK; - } - + case BOUNDARY_LINE_START: if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET) offset = AdjustCaretOffset(offset); - // Start offset is begin of the current line (as the home key was - // pressed). End offset is begin of the next line if any (arrow down and - // home keys), otherwise end of the current line (arrow down only). - *aStartOffset = FindLineBoundary(offset, eDirPrevious, eSelectBeginLine); - *aEndOffset = FindLineBoundary(offset, eDirNext, eSelectLine); - int32_t tmpOffset = FindLineBoundary(*aEndOffset, eDirPrevious, eSelectBeginLine); - if (tmpOffset != *aStartOffset) - *aEndOffset = tmpOffset; - + *aStartOffset = FindLineBoundary(offset, eThisLineBegin); + *aEndOffset = FindLineBoundary(offset, eNextLineBegin); return GetText(*aStartOffset, *aEndOffset, aText); - } - - case BOUNDARY_LINE_END: { - // Empty last line doesn't have own frame (a previous line contains '\n' - // character instead) thus we can't operate on last line separately - // from the previous line. - if (IsEmptyLastLineOffset(offset)) { - *aStartOffset = offset - 1; - *aEndOffset = offset; - aText.AssignLiteral("\n"); - return NS_OK; - } + case BOUNDARY_LINE_END: if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET) offset = AdjustCaretOffset(offset); // In contrast to word end boundary we follow the spec here. - // End offset is end of the current line (as the end key was pressed). - // Start offset is end of the previous line if any (up arrow and end keys), - // otherwise 0 offset (up arrow only). - *aEndOffset = FindLineBoundary(offset, eDirNext, eSelectEndLine); - int32_t tmpOffset = FindLineBoundary(offset, eDirPrevious, eSelectLine); - *aStartOffset = FindLineBoundary(tmpOffset, eDirNext, eSelectEndLine); - if (*aStartOffset == *aEndOffset) - *aStartOffset = 0; - + *aStartOffset = FindLineBoundary(offset, ePrevLineEnd); + *aEndOffset = FindLineBoundary(offset, eThisLineEnd); return GetText(*aStartOffset, *aEndOffset, aText); - } case BOUNDARY_ATTRIBUTE_RANGE: return GetTextHelper(eGetAt, aBoundaryType, aOffset, @@ -1223,36 +1231,16 @@ HyperTextAccessible::GetTextAfterOffset(int32_t aOffset, if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET) offset = AdjustCaretOffset(offset); - // Down arrow, home key, down arrow, home key. - *aStartOffset = FindLineBoundary(offset, eDirNext, eSelectLine); - if (*aStartOffset != CharacterCount()) { - *aStartOffset = FindLineBoundary(*aStartOffset, eDirPrevious, eSelectBeginLine); - *aEndOffset = FindLineBoundary(*aStartOffset, eDirNext, eSelectLine); - if (*aEndOffset != CharacterCount()) - *aEndOffset = FindLineBoundary(*aEndOffset, eDirPrevious, eSelectBeginLine); - } else { - *aEndOffset = CharacterCount(); - } + *aStartOffset = FindLineBoundary(offset, eNextLineBegin); + *aEndOffset = FindLineBoundary(*aStartOffset, eNextLineBegin); return GetText(*aStartOffset, *aEndOffset, aText); case BOUNDARY_LINE_END: if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET) offset = AdjustCaretOffset(offset); - // Empty last line doesn't have own frame (a previous line contains '\n' - // character instead) thus we can't operate on last line separately - // from the previous line. - if (IsEmptyLastLineOffset(offset)) { - *aStartOffset = *aEndOffset = offset; - return NS_OK; - } - - // End key, down arrow, end key. - *aStartOffset = FindLineBoundary(offset, eDirNext, eSelectEndLine); - *aEndOffset = FindLineBoundary(*aStartOffset, eDirNext, eSelectLine); - if (*aEndOffset != CharacterCount()) - *aEndOffset = FindLineBoundary(*aEndOffset, eDirNext, eSelectEndLine); - + *aStartOffset = FindLineBoundary(offset, eThisLineEnd); + *aEndOffset = FindLineBoundary(offset, eNextLineEnd); return GetText(*aStartOffset, *aEndOffset, aText); case BOUNDARY_ATTRIBUTE_RANGE: diff --git a/accessible/src/generic/HyperTextAccessible.h b/accessible/src/generic/HyperTextAccessible.h index 5f3bcda301ef..ffa7fd1140ce 100644 --- a/accessible/src/generic/HyperTextAccessible.h +++ b/accessible/src/generic/HyperTextAccessible.h @@ -302,24 +302,37 @@ protected: int32_t FindWordBoundary(int32_t aOffset, nsDirection aDirection, EWordMovementType aWordMovementType) { - return FindBoundary(aOffset, aDirection, eSelectWord, aWordMovementType); + return FindOffset(aOffset, aDirection, eSelectWord, aWordMovementType); } /** - * Return an offset of the found line boundary. + * Used to get begin/end of previous/this/next line. Note: end of line + * is an offset right before '\n' character if any, the offset is right after + * '\n' character is begin of line. In case of wrap word breaks these offsets + * are equal. */ - int32_t FindLineBoundary(int32_t aOffset, nsDirection aDirection, - nsSelectionAmount aAmount) - { - return FindBoundary(aOffset, aDirection, aAmount, eDefaultBehavior); - } + enum EWhichLineBoundary { + ePrevLineBegin, + ePrevLineEnd, + eThisLineBegin, + eThisLineEnd, + eNextLineBegin, + eNextLineEnd + }; /** - * Return an offset of the found word or line boundary. Helper. + * Return an offset for requested line boundary. See constants above. */ - int32_t FindBoundary(int32_t aOffset, nsDirection aDirection, - nsSelectionAmount aAmount, - EWordMovementType aWordMovementType = eDefaultBehavior); + int32_t FindLineBoundary(int32_t aOffset, + EWhichLineBoundary aWhichLineBoundary); + + /** + * Return an offset corresponding to the given direction and selection amount + * relative the given offset. A helper used to find word or line boundaries. + */ + int32_t FindOffset(int32_t aOffset, nsDirection aDirection, + nsSelectionAmount aAmount, + EWordMovementType aWordMovementType = eDefaultBehavior); /* * This does the work for nsIAccessibleText::GetText[At|Before|After]Offset diff --git a/accessible/src/jsat/AccessFu.jsm b/accessible/src/jsat/AccessFu.jsm index a17283e1c989..10f740af2de2 100644 --- a/accessible/src/jsat/AccessFu.jsm +++ b/accessible/src/jsat/AccessFu.jsm @@ -274,9 +274,7 @@ this.AccessFu = { case 'Accessibility:Focus': this._focused = JSON.parse(aData); if (this._focused) { - let mm = Utils.getMessageManager(Utils.CurrentBrowser); - mm.sendAsyncMessage('AccessFu:VirtualCursor', - {action: 'whereIsIt', move: true}); + this.showCurrent(true); } break; case 'Accessibility:MoveCaret': @@ -327,20 +325,24 @@ this.AccessFu = { case 'TabSelect': { if (this._focused) { - let mm = Utils.getMessageManager(Utils.CurrentBrowser); // We delay this for half a second so the awesomebar could close, // and we could use the current coordinates for the content item. // XXX TODO figure out how to avoid magic wait here. Utils.win.setTimeout( function () { - mm.sendAsyncMessage('AccessFu:VirtualCursor', {action: 'whereIsIt'}); - }, 500); + this.showCurrent(false); + }.bind(this), 500); } break; } } }, + showCurrent: function showCurrent(aMove) { + let mm = Utils.getMessageManager(Utils.CurrentBrowser); + mm.sendAsyncMessage('AccessFu:ShowCurrent', { move: aMove }); + }, + announce: function announce(aAnnouncement) { this._output(Presentation.announce(aAnnouncement), Utils.CurrentBrowser); @@ -632,8 +634,7 @@ var Input = { switch (gestureName) { case 'dwell1': case 'explore1': - this.moveCursor('moveToPoint', 'SimpleTouch', 'gesture', - aGesture.x, aGesture.y); + this.moveToPoint('SimpleTouch', aGesture.x, aGesture.y); break; case 'doubletap1': this.activateCurrent(); @@ -754,12 +755,18 @@ var Input = { aEvent.stopPropagation(); }, - moveCursor: function moveCursor(aAction, aRule, aInputType, aX, aY) { + moveToPoint: function moveToPoint(aRule, aX, aY) { let mm = Utils.getMessageManager(Utils.CurrentBrowser); - mm.sendAsyncMessage('AccessFu:VirtualCursor', + mm.sendAsyncMessage('AccessFu:MoveToPoint', {rule: aRule, + x: aX, y: aY, + origin: 'top'}); + }, + + moveCursor: function moveCursor(aAction, aRule, aInputType) { + let mm = Utils.getMessageManager(Utils.CurrentBrowser); + mm.sendAsyncMessage('AccessFu:MoveCursor', {action: aAction, rule: aRule, - x: aX, y: aY, origin: 'top', - inputType: aInputType}); + origin: 'top', inputType: aInputType}); }, moveCaret: function moveCaret(aDetails) { diff --git a/accessible/src/jsat/Utils.jsm b/accessible/src/jsat/Utils.jsm index 257a2e394c94..b9a924963dbe 100644 --- a/accessible/src/jsat/Utils.jsm +++ b/accessible/src/jsat/Utils.jsm @@ -234,7 +234,8 @@ this.Utils = { inHiddenSubtree: function inHiddenSubtree(aAccessible) { for (let acc=aAccessible; acc; acc=acc.parent) { - if (JSON.parse(Utils.getAttributes(acc).hidden)) { + let hidden = Utils.getAttributes(acc).hidden; + if (hidden && JSON.parse(hidden)) { return true; } } diff --git a/accessible/src/jsat/content-script.js b/accessible/src/jsat/content-script.js index 34f53f315957..f1bcf76e7d12 100644 --- a/accessible/src/jsat/content-script.js +++ b/accessible/src/jsat/content-script.js @@ -26,100 +26,136 @@ Logger.debug('content-script.js'); let eventManager = null; -function virtualCursorControl(aMessage) { - if (Logger.logLevel >= Logger.DEBUG) - Logger.debug(aMessage.name, JSON.stringify(aMessage.json)); +function moveCursor(aMessage) { + if (Logger.logLevel >= Logger.DEBUG) { + Logger.debug(aMessage.name, JSON.stringify(aMessage.json, null, ' ')); + } - try { - let vc = Utils.getVirtualCursor(content.document); - let origin = aMessage.json.origin; - if (origin != 'child') { - if (forwardMessage(vc, aMessage)) - return; - } + let vc = Utils.getVirtualCursor(content.document); + let origin = aMessage.json.origin; + let action = aMessage.json.action; + let rule = TraversalRules[aMessage.json.rule]; - let details = aMessage.json; - let rule = TraversalRules[details.rule]; - let moved = 0; - switch (details.action) { - case 'moveFirst': - case 'moveLast': - moved = vc[details.action](rule); - break; - case 'moveNext': - case 'movePrevious': - try { - if (origin == 'parent' && vc.position == null) { - if (details.action == 'moveNext') - moved = vc.moveFirst(rule); - else - moved = vc.moveLast(rule); - } else { - moved = vc[details.action](rule); + function moveCursorInner() { + try { + if (origin == 'parent' && + !Utils.isAliveAndVisible(vc.position)) { + // We have a bad position in this frame, move vc to last or first item. + if (action == 'moveNext') { + return vc.moveFirst(rule); + } else if (action == 'movePrevious') { + return vc.moveLast(rule); } - } catch (x) { + } + + return vc[action](rule); + } catch (x) { + if (action == 'moveNext' || action == 'movePrevious') { + // If we are trying to move next/prev put the vc on the focused item. let acc = Utils.AccRetrieval. getAccessibleFor(content.document.activeElement); - moved = vc.moveNext(rule, acc, true); + return vc.moveNext(rule, acc, true); + } else { + throw x; } - break; - case 'moveToPoint': - if (!this._ppcp) { - this._ppcp = Utils.getPixelsPerCSSPixel(content); - } - moved = vc.moveToPoint(rule, - details.x * this._ppcp, details.y * this._ppcp, - true); - break; - case 'whereIsIt': - if (!forwardMessage(vc, aMessage)) { - if (!vc.position && aMessage.json.move) - vc.moveFirst(TraversalRules.Simple); - else { - sendAsyncMessage('AccessFu:Present', Presentation.pivotChanged( - vc.position, null, Ci.nsIAccessiblePivot.REASON_NONE)); - } - } - - break; - default: - break; } - if (moved == true) { - forwardMessage(vc, aMessage); - } else if (moved == false && details.action != 'moveToPoint') { + return false; + } + + try { + if (origin != 'child' && + forwardToChild(aMessage, moveCursor, vc.position)) { + // We successfully forwarded the move to the child document. + return; + } + + if (moveCursorInner()) { + // If we moved, try forwarding the message to the new position, + // it may be a frame with a vc of its own. + forwardToChild(aMessage, moveCursor, vc.position); + } else { + // If we did not move, we probably reached the end or start of the + // document, go back to parent content and move us out of the iframe. if (origin == 'parent') { vc.position = null; } - aMessage.json.origin = 'child'; - sendAsyncMessage('AccessFu:VirtualCursor', aMessage.json); + forwardToParent(aMessage); } } catch (x) { - Logger.logException(x, 'Failed to move virtual cursor'); + Logger.logException(x, 'Cursor move failed'); } } -function forwardMessage(aVirtualCursor, aMessage) { - try { - let acc = aVirtualCursor.position; - if (acc && acc.role == ROLE_INTERNAL_FRAME) { - let mm = Utils.getMessageManager(acc.DOMNode); - mm.addMessageListener(aMessage.name, virtualCursorControl); - aMessage.json.origin = 'parent'; - if (Utils.isContentProcess) { - // XXX: OOP content's screen offset is 0, - // so we remove the real screen offset here. - aMessage.json.x -= content.mozInnerScreenX; - aMessage.json.y -= content.mozInnerScreenY; - } - mm.sendAsyncMessage(aMessage.name, aMessage.json); - return true; - } - } catch (x) { - // Frame may be hidden, we regard this case as false. +function moveToPoint(aMessage) { + if (Logger.logLevel >= Logger.DEBUG) { + Logger.debug(aMessage.name, JSON.stringify(aMessage.json, null, ' ')); } - return false; + + let vc = Utils.getVirtualCursor(content.document); + let details = aMessage.json; + let rule = TraversalRules[details.rule]; + + try { + if (!this._ppcp) { + this._ppcp = Utils.getPixelsPerCSSPixel(content); + } + vc.moveToPoint(rule, details.x * this._ppcp, details.y * this._ppcp, true); + forwardToChild(aMessage, moveToPoint, vc.position); + } catch (x) { + Logger.logException(x, 'Failed move to point'); + } +} + +function showCurrent(aMessage) { + if (Logger.logLevel >= Logger.DEBUG) { + Logger.debug(aMessage.name, JSON.stringify(aMessage.json, null, ' ')); + } + + let vc = Utils.getVirtualCursor(content.document); + + if (!forwardToChild(vc, showCurrent, aMessage)) { + if (!vc.position && aMessage.json.move) { + vc.moveFirst(TraversalRules.Simple); + } else { + sendAsyncMessage('AccessFu:Present', Presentation.pivotChanged( + vc.position, null, Ci.nsIAccessiblePivot.REASON_NONE)); + } + } +} + +function forwardToParent(aMessage) { + // XXX: This is a silly way to make a deep copy + let newJSON = JSON.parse(JSON.stringify(aMessage.json)); + newJSON.origin = 'child'; + sendAsyncMessage(aMessage.name, newJSON); +} + +function forwardToChild(aMessage, aListener, aVCPosition) { + let acc = aVCPosition || Utils.getVirtualCursor(content.document).position; + + if (!Utils.isAliveAndVisible(acc) || acc.role != ROLE_INTERNAL_FRAME) { + return false; + } + + if (Logger.logLevel >= Logger.DEBUG) { + Logger.debug('forwardToChild', Logger.accessibleToString(acc), + aMessage.name, JSON.stringify(aMessage.json, null, ' ')); + } + + let mm = Utils.getMessageManager(acc.DOMNode); + mm.addMessageListener(aMessage.name, aListener); + // XXX: This is a silly way to make a deep copy + let newJSON = JSON.parse(JSON.stringify(aMessage.json)); + newJSON.origin = 'parent'; + if (Utils.isContentProcess) { + // XXX: OOP content's screen offset is 0, + // so we remove the real screen offset here. + newJSON.x -= content.mozInnerScreenX; + newJSON.y -= content.mozInnerScreenY; + } + mm.sendAsyncMessage(aMessage.name, newJSON); + return true; } function activateCurrent(aMessage) { @@ -174,9 +210,10 @@ function activateCurrent(aMessage) { return; } - let vc = Utils.getVirtualCursor(content.document); - if (!forwardMessage(vc, aMessage)) - activateAccessible(vc.position); + let position = Utils.getVirtualCursor(content.document).position; + if (!forwardToChild(aMessage, activateCurrent, position)) { + activateAccessible(position); + } } function activateContextMenu(aMessage) { @@ -188,9 +225,9 @@ function activateContextMenu(aMessage) { sendAsyncMessage('AccessFu:ActivateContextMenu', {x: x, y: y}); } - let vc = Utils.getVirtualCursor(content.document); - if (!forwardMessage(vc, aMessage)) + if (!forwardToChild(aMessage, activateContextMenu, vc.position)) { sendContextMenuCoordinates(vc.position); + } } function moveCaret(aMessage) { @@ -315,15 +352,14 @@ function scroll(aMessage) { return false; } - if (aMessage.json.origin != 'child') { - if (forwardMessage(vc, aMessage)) - return; + if (aMessage.json.origin != 'child' && + forwardToChild(aMessage, scroll, vc.position)) { + return; } if (!tryToScroll()) { // Failed to scroll anything in this document. Try in parent document. - aMessage.json.origin = 'child'; - sendAsyncMessage('AccessFu:Scroll', aMessage.json); + forwardToParent(aMessage); } } @@ -334,7 +370,9 @@ addMessageListener( if (m.json.buildApp) Utils.MozBuildApp = m.json.buildApp; - addMessageListener('AccessFu:VirtualCursor', virtualCursorControl); + addMessageListener('AccessFu:MoveToPoint', moveToPoint); + addMessageListener('AccessFu:MoveCursor', moveCursor); + addMessageListener('AccessFu:ShowCurrent', showCurrent); addMessageListener('AccessFu:Activate', activateCurrent); addMessageListener('AccessFu:ContextMenu', activateContextMenu); addMessageListener('AccessFu:Scroll', scroll); @@ -351,7 +389,9 @@ addMessageListener( function(m) { Logger.debug('AccessFu:Stop'); - removeMessageListener('AccessFu:VirtualCursor', virtualCursorControl); + removeMessageListener('AccessFu:MoveToPoint', moveToPoint); + removeMessageListener('AccessFu:MoveCursor', moveCursor); + removeMessageListener('AccessFu:ShowCurrent', showCurrent); removeMessageListener('AccessFu:Activate', activateCurrent); removeMessageListener('AccessFu:ContextMenu', activateContextMenu); removeMessageListener('AccessFu:Scroll', scroll); diff --git a/browser/devtools/webconsole/test/browser_console_iframe_messages.js b/browser/devtools/webconsole/test/browser_console_iframe_messages.js index 3f6ed2db5c43..8131f928bf2c 100644 --- a/browser/devtools/webconsole/test/browser_console_iframe_messages.js +++ b/browser/devtools/webconsole/test/browser_console_iframe_messages.js @@ -8,16 +8,56 @@ const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-consoleiframes.html"; +let expectedMessages = [ + { + text: "main file", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + }, + { + text: "blah", + category: CATEGORY_JS, + severity: SEVERITY_ERROR + }, + { + text: "iframe 2", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG + }, + { + text: "iframe 3", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG + } +]; + +// "iframe 1" console messages can be coalesced into one if they follow each +// other in the sequence of messages (depending on timing). If they do not, then +// they will be displayed in the console output independently, as separate +// messages. This is why we need to match any of the following two rules. +let expectedMessagesAny = [ + { + name: "iframe 1 (count: 2)", + text: "iframe 1", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + count: 2 + }, + { + name: "iframe 1 (repeats: 2)", + text: "iframe 1", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + repeats: 2 + }, +]; + function test() { expectUncaughtException(); addTab(TEST_URI); browser.addEventListener("load", function onLoad() { browser.removeEventListener("load", onLoad, true); - - // Test for cached nsIConsoleMessages. - Services.console.logStringMessage("test1 for bug859756"); - info("open web console"); openConsole(null, consoleOpened); }, true); @@ -29,31 +69,16 @@ function consoleOpened(hud) waitForMessages({ webconsole: hud, - messages: [ - { - text: "main file", - category: CATEGORY_WEBDEV, - severity: SEVERITY_LOG, - }, - { - text: "blah", - category: CATEGORY_JS, - severity: SEVERITY_ERROR - }, - { - text: "iframe 1", - category: CATEGORY_WEBDEV, - severity: SEVERITY_LOG, - count: 2 - }, - { - text: "iframe 2", - category: CATEGORY_WEBDEV, - severity: SEVERITY_LOG - } - ], + messages: expectedMessages, }).then(() => { - closeConsole(null, onWebConsoleClose); + info("first messages matched"); + waitForMessages({ + webconsole: hud, + messages: expectedMessagesAny, + matchCondition: "any", + }).then(() => { + closeConsole(null, onWebConsoleClose); + }); }); } @@ -66,34 +91,17 @@ function onWebConsoleClose() function onBrowserConsoleOpen(hud) { ok(hud, "browser console opened"); - Services.console.logStringMessage("test2 for bug859756"); - waitForMessages({ webconsole: hud, - messages: [ - { - text: "main file", - category: CATEGORY_WEBDEV, - severity: SEVERITY_LOG, - }, - { - text: "blah", - category: CATEGORY_JS, - severity: SEVERITY_ERROR - }, - { - text: "iframe 1", - category: CATEGORY_WEBDEV, - severity: SEVERITY_LOG, - count: 2 - }, - { - text: "iframe 2", - category: CATEGORY_WEBDEV, - severity: SEVERITY_LOG - } - ], + messages: expectedMessages, }).then(() => { - closeConsole(null, finishTest); + info("first messages matched"); + waitForMessages({ + webconsole: hud, + messages: expectedMessagesAny, + matchCondition: "any", + }).then(() => { + closeConsole(null, finishTest); + }); }); } diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_613013_console_api_iframe.js b/browser/devtools/webconsole/test/browser_webconsole_bug_613013_console_api_iframe.js index 978c15880319..7e0fe28e1d6a 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_bug_613013_console_api_iframe.js +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_613013_console_api_iframe.js @@ -35,15 +35,14 @@ function performTest() { Services.obs.removeObserver(TestObserver, "console-api-log-event"); TestObserver = null; - waitForSuccess({ - name: "console.log() message", - validatorFn: function() - { - return hud.outputNode.textContent.indexOf("foobarBug613013") > -1; - }, - successFn: finishTest, - failureFn: finishTest, - }); + waitForMessages({ + webconsole: hud, + messages: [{ + text: "foobarBug613013", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + }], + }).then(finishTest); } function test() { diff --git a/browser/devtools/webconsole/test/head.js b/browser/devtools/webconsole/test/head.js index b1f5d622ea19..b7f1f4ce2cdc 100644 --- a/browser/devtools/webconsole/test/head.js +++ b/browser/devtools/webconsole/test/head.js @@ -855,6 +855,12 @@ function getMessageElementText(aElement) * @param object aOptions * Options for what you want to wait for: * - webconsole: the webconsole instance you work with. + * - matchCondition: "any" or "all". Default: "all". The promise + * returned by this function resolves when all of the messages are + * matched, if the |matchCondition| is "all". If you set the condition to + * "any" then the promise is resolved by any message rule that matches, + * irrespective of order - waiting for messages stops whenever any rule + * matches. * - messages: an array of objects that tells which messages to wait for. * Properties: * - text: string or RegExp to match the textContent of each new @@ -905,6 +911,7 @@ function waitForMessages(aOptions) let rulesMatched = 0; let listenerAdded = false; let deferred = promise.defer(); + aOptions.matchCondition = aOptions.matchCondition || "all"; function checkText(aRule, aText) { @@ -1154,9 +1161,15 @@ function waitForMessages(aOptions) } } + function allRulesMatched() + { + return aOptions.matchCondition == "all" && rulesMatched == rules.length || + aOptions.matchCondition == "any" && rulesMatched > 0; + } + function maybeDone() { - if (rulesMatched == rules.length) { + if (allRulesMatched()) { if (listenerAdded) { webconsole.ui.off("messages-added", onMessagesAdded); webconsole.ui.off("messages-updated", onMessagesAdded); @@ -1169,7 +1182,7 @@ function waitForMessages(aOptions) } function testCleanup() { - if (rulesMatched == rules.length) { + if (allRulesMatched()) { return; } @@ -1198,7 +1211,7 @@ function waitForMessages(aOptions) executeSoon(() => { onMessagesAdded("messages-added", webconsole.outputNode.childNodes); - if (rulesMatched != rules.length) { + if (!allRulesMatched()) { listenerAdded = true; registerCleanupFunction(testCleanup); webconsole.ui.on("messages-added", onMessagesAdded); diff --git a/browser/modules/SitePermissions.jsm b/browser/modules/SitePermissions.jsm index 583be1897069..d26163a1405b 100644 --- a/browser/modules/SitePermissions.jsm +++ b/browser/modules/SitePermissions.jsm @@ -27,7 +27,11 @@ this.SitePermissions = { /* Returns an array of all permission IDs. */ listPermissions: function () { - return Object.keys(gPermissionObject); + let array = Object.keys(gPermissionObject); + array.sort((a, b) => { + return this.getPermissionLabel(a).localeCompare(this.getPermissionLabel(b)); + }); + return array; }, /* Returns an array of permission states to be exposed to the user for a diff --git a/content/events/src/nsEventStateManager.cpp b/content/events/src/nsEventStateManager.cpp index 29d0b0d4ac9c..0ed1a4d8ab90 100644 --- a/content/events/src/nsEventStateManager.cpp +++ b/content/events/src/nsEventStateManager.cpp @@ -4567,7 +4567,10 @@ nsEventStateManager::CheckForAndDispatchClick(nsPresContext* aPresContext, if (!mouseContent && !mCurrentTarget) { return NS_OK; } - ret = presShell->HandleEventWithTarget(&event, mCurrentTarget, + + // HandleEvent clears out mCurrentTarget which we might need again + nsWeakFrame currentTarget = mCurrentTarget; + ret = presShell->HandleEventWithTarget(&event, currentTarget, mouseContent, aStatus); if (NS_SUCCEEDED(ret) && aEvent->clickCount == 2) { //fire double click @@ -4581,7 +4584,7 @@ nsEventStateManager::CheckForAndDispatchClick(nsPresContext* aPresContext, event2.button = aEvent->button; event2.inputSource = aEvent->inputSource; - ret = presShell->HandleEventWithTarget(&event2, mCurrentTarget, + ret = presShell->HandleEventWithTarget(&event2, currentTarget, mouseContent, aStatus); } } diff --git a/content/events/test/Makefile.in b/content/events/test/Makefile.in index 66725c5bf3ab..f1fbc4f0cb62 100644 --- a/content/events/test/Makefile.in +++ b/content/events/test/Makefile.in @@ -104,6 +104,7 @@ MOCHITEST_FILES = \ test_focus_disabled.html \ test_bug847597.html \ test_bug855741.html \ + test_dblclick_explicit_original_target.html \ $(NULL) ifeq (,$(filter gonk,$(MOZ_WIDGET_TOOLKIT))) diff --git a/content/events/test/test_dblclick_explicit_original_target.html b/content/events/test/test_dblclick_explicit_original_target.html new file mode 100644 index 000000000000..8aa5f5d4c794 --- /dev/null +++ b/content/events/test/test_dblclick_explicit_original_target.html @@ -0,0 +1,33 @@ + + + + Test explicit original target of dblclick event + + + + + +

Test explicit original target of dblclick event

+ +
+
+
+ + diff --git a/dom/ipc/Blob.cpp b/dom/ipc/Blob.cpp index 59093416ce29..399c508ced01 100644 --- a/dom/ipc/Blob.cpp +++ b/dom/ipc/Blob.cpp @@ -840,7 +840,7 @@ private: typename ActorType::ConstructorParamsType params; ActorType::BaseType::SetBlobConstructorParams(params, normalParams); - ActorType* newActor = ActorType::Create(params); + ActorType* newActor = ActorType::Create(mActor->Manager(), params); MOZ_ASSERT(newActor); SlicedBlobConstructorParams slicedParams; @@ -1011,11 +1011,13 @@ RemoteBlob::GetInternalStream(nsIInputStream** aStream) } template -Blob::Blob(nsIDOMBlob* aBlob) -: mBlob(aBlob), mRemoteBlob(nullptr), mOwnsBlob(true), mBlobIsFile(false) +Blob::Blob(ContentManager* aManager, nsIDOMBlob* aBlob) +: mBlob(aBlob), mRemoteBlob(nullptr), mOwnsBlob(true) +, mBlobIsFile(false), mManager(aManager) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(aBlob); + MOZ_ASSERT(aManager); aBlob->AddRef(); nsCOMPtr file = do_QueryInterface(aBlob); @@ -1023,10 +1025,13 @@ Blob::Blob(nsIDOMBlob* aBlob) } template -Blob::Blob(const ConstructorParamsType& aParams) -: mBlob(nullptr), mRemoteBlob(nullptr), mOwnsBlob(false), mBlobIsFile(false) +Blob::Blob(ContentManager* aManager, + const ConstructorParamsType& aParams) +: mBlob(nullptr), mRemoteBlob(nullptr), mOwnsBlob(false) +, mBlobIsFile(false), mManager(aManager) { MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aManager); ChildBlobConstructorParams::Type paramType = BaseType::GetBlobConstructorParams(aParams).type(); @@ -1048,7 +1053,8 @@ Blob::Blob(const ConstructorParamsType& aParams) template Blob* -Blob::Create(const ConstructorParamsType& aParams) +Blob::Create(ContentManager* aManager, + const ConstructorParamsType& aParams) { MOZ_ASSERT(NS_IsMainThread()); @@ -1059,7 +1065,7 @@ Blob::Create(const ConstructorParamsType& aParams) case ChildBlobConstructorParams::TNormalBlobConstructorParams: case ChildBlobConstructorParams::TFileBlobConstructorParams: case ChildBlobConstructorParams::TMysteryBlobConstructorParams: - return new Blob(aParams); + return new Blob(aManager, aParams); case ChildBlobConstructorParams::TSlicedBlobConstructorParams: { const SlicedBlobConstructorParams& params = @@ -1074,7 +1080,7 @@ Blob::Create(const ConstructorParamsType& aParams) getter_AddRefs(slice)); NS_ENSURE_SUCCESS(rv, nullptr); - return new Blob(slice); + return new Blob(aManager, slice); } default: diff --git a/dom/ipc/Blob.h b/dom/ipc/Blob.h index 460ddd345327..78fa76c86cce 100644 --- a/dom/ipc/Blob.h +++ b/dom/ipc/Blob.h @@ -162,6 +162,7 @@ class Blob : public BlobTraits::BaseType friend class RemoteBlob; public: + typedef typename BlobTraits::ConcreteContentManagerType ContentManager; typedef typename BlobTraits::ProtocolType ProtocolType; typedef typename BlobTraits::StreamType StreamType; typedef typename BlobTraits::ConstructorParamsType @@ -183,14 +184,14 @@ protected: public: // This create function is called on the sending side. static Blob* - Create(nsIDOMBlob* aBlob) + Create(ContentManager* aManager, nsIDOMBlob* aBlob) { - return new Blob(aBlob); + return new Blob(aManager, aBlob); } // This create function is called on the receiving side. static Blob* - Create(const ConstructorParamsType& aParams); + Create(ContentManager* aManager, const ConstructorParamsType& aParams); // Get the blob associated with this actor. This may always be called on the // sending side. It may also be called on the receiving side unless this is a @@ -207,12 +208,17 @@ public: bool SetMysteryBlobInfo(const nsString& aContentType, uint64_t aLength); + ContentManager* Manager() + { + return mManager; + } + private: // This constructor is called on the sending side. - Blob(nsIDOMBlob* aBlob); + Blob(ContentManager* aManager, nsIDOMBlob* aBlob); // This constructor is called on the receiving side. - Blob(const ConstructorParamsType& aParams); + Blob(ContentManager* aManager, const ConstructorParamsType& aParams); static already_AddRefed CreateRemoteBlob(const ConstructorParamsType& aParams); @@ -229,6 +235,8 @@ private: virtual bool RecvPBlobStreamConstructor(StreamType* aActor) MOZ_OVERRIDE; + + nsRefPtr mManager; }; } // namespace ipc diff --git a/dom/ipc/ContentChild.cpp b/dom/ipc/ContentChild.cpp index 0464ddffca3a..08062664fd17 100644 --- a/dom/ipc/ContentChild.cpp +++ b/dom/ipc/ContentChild.cpp @@ -636,7 +636,7 @@ ContentChild::DeallocPBrowserChild(PBrowserChild* iframe) PBlobChild* ContentChild::AllocPBlobChild(const BlobConstructorParams& aParams) { - return BlobChild::Create(aParams); + return BlobChild::Create(this, aParams); } bool @@ -723,7 +723,7 @@ ContentChild::GetOrCreateActorForBlob(nsIDOMBlob* aBlob) } } - BlobChild* actor = BlobChild::Create(aBlob); + BlobChild* actor = BlobChild::Create(this, aBlob); NS_ENSURE_TRUE(actor, nullptr); if (!SendPBlobConstructor(actor, params)) { diff --git a/dom/ipc/ContentParent.cpp b/dom/ipc/ContentParent.cpp index caefdcbdbc98..dad5ca324610 100644 --- a/dom/ipc/ContentParent.cpp +++ b/dom/ipc/ContentParent.cpp @@ -1692,7 +1692,7 @@ ContentParent::DeallocPDeviceStorageRequestParent(PDeviceStorageRequestParent* d PBlobParent* ContentParent::AllocPBlobParent(const BlobConstructorParams& aParams) { - return BlobParent::Create(aParams); + return BlobParent::Create(this, aParams); } bool @@ -1772,7 +1772,7 @@ ContentParent::GetOrCreateActorForBlob(nsIDOMBlob* aBlob) } } - BlobParent* actor = BlobParent::Create(aBlob); + BlobParent* actor = BlobParent::Create(this, aBlob); NS_ENSURE_TRUE(actor, nullptr); if (!SendPBlobConstructor(actor, params)) { diff --git a/dom/media/PeerConnection.js b/dom/media/PeerConnection.js index d002e7eb1c38..8815c6576f42 100644 --- a/dom/media/PeerConnection.js +++ b/dom/media/PeerConnection.js @@ -849,8 +849,8 @@ RTCPeerConnection.prototype = { } if (dict.maxRetransmitTime != undefined && - dict.maxRetransmitNum != undefined) { - throw new Components.Exception("Both maxRetransmitTime and maxRetransmitNum cannot be provided"); + dict.maxRetransmits != undefined) { + throw new Components.Exception("Both maxRetransmitTime and maxRetransmits cannot be provided"); } let protocol; if (dict.protocol == undefined) { @@ -863,7 +863,7 @@ RTCPeerConnection.prototype = { let type; if (dict.maxRetransmitTime != undefined) { type = Ci.IPeerConnection.kDataChannelPartialReliableTimed; - } else if (dict.maxRetransmitNum != undefined) { + } else if (dict.maxRetransmits != undefined) { type = Ci.IPeerConnection.kDataChannelPartialReliableRexmit; } else { type = Ci.IPeerConnection.kDataChannelReliable; @@ -871,9 +871,9 @@ RTCPeerConnection.prototype = { // Synchronous since it doesn't block. let channel = this._getPC().createDataChannel( - label, protocol, type, dict.outOfOrderAllowed, dict.maxRetransmitTime, - dict.maxRetransmitNum, dict.preset ? true : false, - dict.stream != undefined ? dict.stream : 0xFFFF + label, protocol, type, !dict.ordered, dict.maxRetransmitTime, + dict.maxRetransmits, dict.negotiated ? true : false, + dict.id != undefined ? dict.id : 0xFFFF ); return channel; }, diff --git a/gfx/layers/opengl/CompositingRenderTargetOGL.h b/gfx/layers/opengl/CompositingRenderTargetOGL.h index ee41ad3d3e04..904fb79328ed 100644 --- a/gfx/layers/opengl/CompositingRenderTargetOGL.h +++ b/gfx/layers/opengl/CompositingRenderTargetOGL.h @@ -185,9 +185,9 @@ private: GLenum result = mGL->fCheckFramebufferStatus(LOCAL_GL_FRAMEBUFFER); if (result != LOCAL_GL_FRAMEBUFFER_COMPLETE) { nsAutoCString msg; - msg.AppendPrintf("Framebuffer not complete -- error 0x%x, aFBOTextureTarget 0x%x, aRect.width %d, aRect.height %d", - result, mInitParams.mFBOTextureTarget, mInitParams.mSize.width, mInitParams.mSize.height); - NS_RUNTIMEABORT(msg.get()); + msg.AppendPrintf("Framebuffer not complete -- error 0x%x, aFBOTextureTarget 0x%x, mFBO %d, mTextureHandle %d, aRect.width %d, aRect.height %d", + result, mInitParams.mFBOTextureTarget, mFBO, mTextureHandle, mInitParams.mSize.width, mInitParams.mSize.height); + NS_ERROR(msg.get()); } mCompositor->PrepareViewport(mInitParams.mSize, mTransform); diff --git a/gfx/layers/opengl/CompositorOGL.cpp b/gfx/layers/opengl/CompositorOGL.cpp index a7de1dbf94bc..3ce748fdf421 100644 --- a/gfx/layers/opengl/CompositorOGL.cpp +++ b/gfx/layers/opengl/CompositorOGL.cpp @@ -896,6 +896,13 @@ CompositorOGL::CreateFBOWithTexture(const IntRect& aRect, SurfaceInitMode aInit, LOCAL_GL_UNSIGNED_BYTE, buf); } + GLenum error = mGLContext->GetAndClearError(); + if (error != LOCAL_GL_NO_ERROR) { + nsAutoCString msg; + msg.AppendPrintf("Texture initialization failed! -- error 0x%x, Source %d, Source format %d, RGBA Compat %d", + error, aSourceFrameBuffer, format, isFormatCompatibleWithRGBA); + NS_ERROR(msg.get()); + } } else { mGLContext->fTexImage2D(mFBOTextureTarget, 0, diff --git a/image/src/VectorImage.cpp b/image/src/VectorImage.cpp index 63260e621e68..575032d3f3ab 100644 --- a/image/src/VectorImage.cpp +++ b/image/src/VectorImage.cpp @@ -230,9 +230,11 @@ class SVGDrawingCallback : public gfxDrawingCallback { public: SVGDrawingCallback(SVGDocumentWrapper* aSVGDocumentWrapper, const nsIntRect& aViewport, + const gfxSize& aScale, uint32_t aImageFlags) : mSVGDocumentWrapper(aSVGDocumentWrapper), mViewport(aViewport), + mScale(aScale), mImageFlags(aImageFlags) {} virtual bool operator()(gfxContext* aContext, @@ -242,6 +244,7 @@ public: private: nsRefPtr mSVGDocumentWrapper; const nsIntRect mViewport; + const gfxSize mScale; uint32_t mImageFlags; }; @@ -271,6 +274,7 @@ SVGDrawingCallback::operator()(gfxContext* aContext, gfxContextMatrixAutoSaveRestore contextMatrixRestorer(aContext); aContext->Multiply(gfxMatrix(aTransform).Invert()); + aContext->Scale(1.0 / mScale.width, 1.0 / mScale.height); nsPresContext* presContext = presShell->GetPresContext(); MOZ_ASSERT(presContext, "pres shell w/out pres context"); @@ -698,18 +702,11 @@ VectorImage::Draw(gfxContext* aContext, // if we hit the tiling path. Unfortunately, the temporary surface isn't // created at the size at which we'll ultimately draw, causing fuzzy output. // To fix this we pre-apply the transform's scaling to the drawing parameters - // and then remove the scaling from the transform, so the fact that temporary + // and remove the scaling from the transform, so the fact that temporary // surfaces won't take the scaling into account doesn't matter. (Bug 600207.) gfxSize scale(aUserSpaceToImageSpace.ScaleFactors(true)); gfxPoint translation(aUserSpaceToImageSpace.GetTranslation()); - // Rescale everything. - nsIntSize scaledViewport(aViewportSize.width / scale.width, - aViewportSize.height / scale.height); - gfxIntSize scaledViewportGfx(scaledViewport.width, scaledViewport.height); - nsIntRect scaledSubimage(aSubimage); - scaledSubimage.ScaleRoundOut(1.0 / scale.width, 1.0 / scale.height); - // Remove the scaling from the transform. gfxMatrix unscale; unscale.Translate(gfxPoint(translation.x / scale.width, @@ -718,28 +715,30 @@ VectorImage::Draw(gfxContext* aContext, unscale.Translate(-translation); gfxMatrix unscaledTransform(aUserSpaceToImageSpace * unscale); - mSVGDocumentWrapper->UpdateViewportBounds(scaledViewport); + mSVGDocumentWrapper->UpdateViewportBounds(aViewportSize); mSVGDocumentWrapper->FlushImageTransformInvalidation(); - // Based on imgFrame::Draw - gfxRect sourceRect = unscaledTransform.Transform(aFill); - gfxRect imageRect(0, 0, scaledViewport.width, scaledViewport.height); - gfxRect subimage(scaledSubimage.x, scaledSubimage.y, - scaledSubimage.width, scaledSubimage.height); - + // Rescale drawing parameters. + gfxIntSize drawableSize(aViewportSize.width / scale.width, + aViewportSize.height / scale.height); + gfxRect drawableSourceRect = unscaledTransform.Transform(aFill); + gfxRect drawableImageRect(0, 0, drawableSize.width, drawableSize.height); + gfxRect drawableSubimage(aSubimage.x, aSubimage.y, + aSubimage.width, aSubimage.height); + drawableSubimage.ScaleRoundOut(1.0 / scale.width, 1.0 / scale.height); nsRefPtr cb = new SVGDrawingCallback(mSVGDocumentWrapper, - nsIntRect(nsIntPoint(0, 0), scaledViewport), + nsIntRect(nsIntPoint(0, 0), aViewportSize), + scale, aFlags); - nsRefPtr drawable = new gfxCallbackDrawable(cb, scaledViewportGfx); + nsRefPtr drawable = new gfxCallbackDrawable(cb, drawableSize); - gfxUtils::DrawPixelSnapped(aContext, drawable, - unscaledTransform, - subimage, sourceRect, imageRect, aFill, - gfxASurface::ImageFormatARGB32, aFilter, - aFlags); + gfxUtils::DrawPixelSnapped(aContext, drawable, unscaledTransform, + drawableSubimage, drawableSourceRect, + drawableImageRect, aFill, + gfxASurface::ImageFormatARGB32, aFilter, aFlags); MOZ_ASSERT(mRenderingObserver, "Should have a rendering observer by now"); mRenderingObserver->ResumeHonoringInvalidations(); diff --git a/intl/icu/source/i18n/decimfmt.cpp b/intl/icu/source/i18n/decimfmt.cpp index 8e1cb0b6dbaa..f1cac6ab7642 100644 --- a/intl/icu/source/i18n/decimfmt.cpp +++ b/intl/icu/source/i18n/decimfmt.cpp @@ -1714,6 +1714,14 @@ DecimalFormat::subformat(UnicodeString& appendTo, appendTo.append(*grouping); handler.addAttribute(kGroupingSeparatorField, currentLength, appendTo.length()); } + } + + // This handles the special case of formatting 0. For zero only, we count the + // zero to the left of the decimal point as one signficant digit. Ordinarily we + // do not count any leading 0's as significant. If the number we are formatting + // is not zero, then either sigCount or digits.getCount() will be non-zero. + if (sigCount == 0 && digits.getCount() == 0) { + sigCount = 1; } // TODO(dlf): this looks like it was a bug, we marked the int field as ending diff --git a/intl/update-icu.sh b/intl/update-icu.sh index 5d0f54d79e8c..152a4172ae3d 100755 --- a/intl/update-icu.sh +++ b/intl/update-icu.sh @@ -14,6 +14,15 @@ # and reapply it after running update-icu.sh (additional updates may be needed). # If the bug has been addressed, please delete this warning. +# Warning +# ======= +# The fix for ICU bug 10045 has been individually backported into this tree. +# If you update ICU to a version that does not have this fix yet, obtain the +# patch "Backported fix for formatting 0 with significant digits from ICU" from +# https://bugzilla.mozilla.org/show_bug.cgi?id=853706 +# and reapply it after running update-icu.sh. +# If you update ICU to a version that has the fix, please delete this warning. + # Usage: update-icu.sh # E.g., for ICU 50.1.1: update-icu.sh http://source.icu-project.org/repos/icu/icu/tags/release-50-1-1/ diff --git a/js/src/builtin/Object.cpp b/js/src/builtin/Object.cpp index 07f20e872670..82b55afd808e 100644 --- a/js/src/builtin/Object.cpp +++ b/js/src/builtin/Object.cpp @@ -706,9 +706,6 @@ obj_create(JSContext *cx, unsigned argc, Value *vp) if (!obj) return false; - /* Don't track types or array-ness for objects created here. */ - MarkTypeObjectUnknownProperties(cx, obj->type()); - /* 15.2.3.5 step 4. */ if (args.hasDefined(1)) { if (args[1].isPrimitive()) { diff --git a/js/src/jsinfer.cpp b/js/src/jsinfer.cpp index 2d7d68bca6b5..c27cd21f7126 100644 --- a/js/src/jsinfer.cpp +++ b/js/src/jsinfer.cpp @@ -6288,12 +6288,6 @@ TypeObject::clearProperties() inline void TypeObject::sweep(FreeOp *fop) { - /* - * We may be regenerating existing type sets containing this object, - * so reset contributions on each GC to avoid tripping the limit. - */ - contribution = 0; - if (singleton) { JS_ASSERT(!newScript); diff --git a/js/src/jsinfer.h b/js/src/jsinfer.h index 6a4bcdc84cb3..bdcbc96f161f 100644 --- a/js/src/jsinfer.h +++ b/js/src/jsinfer.h @@ -287,7 +287,7 @@ enum { TYPE_FLAG_INT32 | TYPE_FLAG_DOUBLE | TYPE_FLAG_STRING, /* Mask/shift for the number of objects in objectSet */ - TYPE_FLAG_OBJECT_COUNT_MASK = 0xff00, + TYPE_FLAG_OBJECT_COUNT_MASK = 0x1f00, TYPE_FLAG_OBJECT_COUNT_SHIFT = 8, TYPE_FLAG_OBJECT_COUNT_LIMIT = TYPE_FLAG_OBJECT_COUNT_MASK >> TYPE_FLAG_OBJECT_COUNT_SHIFT, @@ -971,20 +971,6 @@ struct TypeObject : gc::Cell static inline size_t offsetOfFlags() { return offsetof(TypeObject, flags); } - /* - * Estimate of the contribution of this object to the type sets it appears in. - * This is the sum of the sizes of those sets at the point when the object - * was added. - * - * When the contribution exceeds the CONTRIBUTION_LIMIT, any type sets the - * object is added to are instead marked as unknown. If we get to this point - * we are probably not adding types which will let us do meaningful optimization - * later, and we want to ensure in such cases that our time/space complexity - * is linear, not worst-case cubic as it would otherwise be. - */ - uint32_t contribution; - static const uint32_t CONTRIBUTION_LIMIT = 2000; - /* * If non-NULL, objects of this type have always been constructed using * 'new' on the specified script, which adds some number of properties to @@ -1027,6 +1013,10 @@ struct TypeObject : gc::Cell /* If this is an interpreted function, the function object. */ HeapPtrFunction interpretedFunction; +#if JS_BITS_PER_WORD == 32 + uint32_t padding; +#endif + inline TypeObject(Class *clasp, TaggedProto proto, bool isFunction, bool unknown); bool isFunction() { return !!(flags & OBJECT_FLAG_FUNCTION); } diff --git a/js/src/jsinferinlines.h b/js/src/jsinferinlines.h index 37b34717b34d..350410c1899e 100644 --- a/js/src/jsinferinlines.h +++ b/js/src/jsinferinlines.h @@ -970,9 +970,14 @@ TypeScript::MonitorAssign(JSContext *cx, HandleObject obj, jsid id) // But if we don't have too many properties yet, don't do anything. The // idea here is that normal object initialization should not trigger // deoptimization in most cases, while actual usage as a hashmap should. + // Except for vanilla objects and arrays work around bug 894447 for now + // by deoptimizing more eagerly. Since in those cases we often have a + // pc-keyed TypeObject, this is ok. TypeObject* type = obj->type(); - if (type->getPropertyCount() < 8) + if (!obj->is() && !obj->is() && + type->getPropertyCount() < 8) { return; + } MarkTypeObjectUnknownProperties(cx, type); } } @@ -1360,14 +1365,6 @@ TypeSet::addType(JSContext *cx, Type type) JS_ASSERT(!nobject->singleton); if (nobject->unknownProperties()) goto unknownObject; - if (objectCount > 1) { - nobject->contribution += (objectCount - 1) * (objectCount - 1); - if (nobject->contribution >= TypeObject::CONTRIBUTION_LIMIT) { - InferSpew(ISpewOps, "limitUnknown: %sT%p%s", - InferSpewColor(this), this, InferSpewColorReset()); - goto unknownObject; - } - } } } diff --git a/js/src/tests/Intl/NumberFormat/format.js b/js/src/tests/Intl/NumberFormat/format.js index 108adbb9dcec..32b51adc0edd 100644 --- a/js/src/tests/Intl/NumberFormat/format.js +++ b/js/src/tests/Intl/NumberFormat/format.js @@ -40,7 +40,7 @@ format = new Intl.NumberFormat("th-th-u-nu-thai", {style: "percent", minimumSignificantDigits: 2, maximumSignificantDigits: 2}); -assertEq(format.format(0), "๐.๐๐%"); +assertEq(format.format(0), "๐.๐%"); assertEq(format.format(-0.01), "-๑.๐%"); assertEq(format.format(1.10), "๑๑๐%"); diff --git a/js/src/tests/Intl/NumberFormat/significantDigitsOfZero.js b/js/src/tests/Intl/NumberFormat/significantDigitsOfZero.js new file mode 100644 index 000000000000..f2cd77cda359 --- /dev/null +++ b/js/src/tests/Intl/NumberFormat/significantDigitsOfZero.js @@ -0,0 +1,36 @@ +// |reftest| skip-if(!this.hasOwnProperty("Intl")) +// -- test that NumberFormat correctly formats 0 with various numbers of significant digits + +/* 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/. */ + +var testData = [ + {minimumSignificantDigits: 1, maximumSignificantDigits: 1, expected: "0"}, + {minimumSignificantDigits: 1, maximumSignificantDigits: 2, expected: "0"}, + {minimumSignificantDigits: 1, maximumSignificantDigits: 3, expected: "0"}, + {minimumSignificantDigits: 1, maximumSignificantDigits: 4, expected: "0"}, + {minimumSignificantDigits: 1, maximumSignificantDigits: 5, expected: "0"}, + {minimumSignificantDigits: 2, maximumSignificantDigits: 2, expected: "0.0"}, + {minimumSignificantDigits: 2, maximumSignificantDigits: 3, expected: "0.0"}, + {minimumSignificantDigits: 2, maximumSignificantDigits: 4, expected: "0.0"}, + {minimumSignificantDigits: 2, maximumSignificantDigits: 5, expected: "0.0"}, + {minimumSignificantDigits: 3, maximumSignificantDigits: 3, expected: "0.00"}, + {minimumSignificantDigits: 3, maximumSignificantDigits: 4, expected: "0.00"}, + {minimumSignificantDigits: 3, maximumSignificantDigits: 5, expected: "0.00"}, +]; + +for (var i = 0; i < testData.length; i++) { + var min = testData[i].minimumSignificantDigits; + var max = testData[i].maximumSignificantDigits; + var options = {minimumSignificantDigits: min, maximumSignificantDigits: max}; + var format = new Intl.NumberFormat("en-US", options); + assertEq(format.format(0), testData[i].expected, + "Wrong formatted string for 0 with " + + "minimumSignificantDigits " + min + + ", maximumSignificantDigits " + max + + ": expected \"" + expected + + "\", actual \"" + actual + "\""); +} + +reportCompare(true, true); diff --git a/js/src/tests/jstests.list b/js/src/tests/jstests.list index bd5eb8f030ae..5485bd6033e9 100644 --- a/js/src/tests/jstests.list +++ b/js/src/tests/jstests.list @@ -38,7 +38,6 @@ skip script test262/ch10/10.4/10.4.3/10.4.3-1-106.js # bug 603201 skip script test262/intl402/ch10/10.1/10.1.1_13.js # bug 853704 skip script test262/intl402/ch10/10.1/10.1.1_19_c.js # bug 853704 -skip script test262/intl402/ch11/11.3/11.3.2_TRP.js # bug 853706 ####################################################################### # Tests disabled due to jstest limitations wrt imported test262 tests # diff --git a/js/src/vm/Interpreter.cpp b/js/src/vm/Interpreter.cpp index 8746271f15bb..0b83f5a0dad3 100644 --- a/js/src/vm/Interpreter.cpp +++ b/js/src/vm/Interpreter.cpp @@ -1288,10 +1288,6 @@ Interpret(JSContext *cx, RunState &state) InterruptEnabler interrupts(&switchMask, -1); # define DO_OP() goto do_op -# define DO_NEXT_OP(n) JS_BEGIN_MACRO \ - JS_ASSERT((n) == len); \ - goto advance_pc; \ - JS_END_MACRO # define BEGIN_CASE(OP) case OP: # define END_CASE(OP) END_CASE_LEN(OP##_LENGTH) @@ -1300,21 +1296,21 @@ Interpret(JSContext *cx, RunState &state) /* * To share the code for all len == 1 cases we use the specialized label with - * code that falls through to advance_pc: . + * code that falls through to advanceAndDoOp: . */ # define END_CASE_LEN1 goto advance_pc_by_one; -# define END_CASE_LEN2 len = 2; goto advance_pc; -# define END_CASE_LEN3 len = 3; goto advance_pc; -# define END_CASE_LEN4 len = 4; goto advance_pc; -# define END_CASE_LEN5 len = 5; goto advance_pc; -# define END_CASE_LEN6 len = 6; goto advance_pc; -# define END_CASE_LEN7 len = 7; goto advance_pc; -# define END_CASE_LEN8 len = 8; goto advance_pc; -# define END_CASE_LEN9 len = 9; goto advance_pc; -# define END_CASE_LEN10 len = 10; goto advance_pc; -# define END_CASE_LEN11 len = 11; goto advance_pc; -# define END_CASE_LEN12 len = 12; goto advance_pc; -# define END_VARLEN_CASE goto advance_pc; +# define END_CASE_LEN2 len = 2; goto advanceAndDoOp; +# define END_CASE_LEN3 len = 3; goto advanceAndDoOp; +# define END_CASE_LEN4 len = 4; goto advanceAndDoOp; +# define END_CASE_LEN5 len = 5; goto advanceAndDoOp; +# define END_CASE_LEN6 len = 6; goto advanceAndDoOp; +# define END_CASE_LEN7 len = 7; goto advanceAndDoOp; +# define END_CASE_LEN8 len = 8; goto advanceAndDoOp; +# define END_CASE_LEN9 len = 9; goto advanceAndDoOp; +# define END_CASE_LEN10 len = 10; goto advanceAndDoOp; +# define END_CASE_LEN11 len = 11; goto advanceAndDoOp; +# define END_CASE_LEN12 len = 12; goto advanceAndDoOp; +# define END_VARLEN_CASE goto advanceAndDoOp; # define ADD_EMPTY_CASE(OP) BEGIN_CASE(OP) # define END_EMPTY_CASES goto advance_pc_by_one; @@ -1443,8 +1439,8 @@ Interpret(JSContext *cx, RunState &state) /* * It is important that "op" be initialized before calling DO_OP because * it is possible for "op" to be specially assigned during the normal - * processing of an opcode while looping. We rely on DO_NEXT_OP to manage - * "op" correctly in all other cases. + * processing of an opcode while looping. We rely on |advanceAndDoOp:| to + * manage "op" correctly in all other cases. */ JSOp op; int32_t len; @@ -1453,13 +1449,13 @@ Interpret(JSContext *cx, RunState &state) if (rt->profilingScripts || cx->runtime()->debugHooks.interruptHook) interrupts.enable(); - DO_NEXT_OP(len); + goto advanceAndDoOp; for (;;) { advance_pc_by_one: JS_ASSERT(js_CodeSpec[op].length == 1); len = 1; - advance_pc: + advanceAndDoOp: js::gc::MaybeVerifyBarriers(cx); regs.pc += len; op = (JSOp) *regs.pc; @@ -1723,7 +1719,7 @@ BEGIN_CASE(JSOP_STOP) TypeScript::Monitor(cx, script, regs.pc, regs.sp[-1]); len = JSOP_CALL_LENGTH; - DO_NEXT_OP(len); + goto advanceAndDoOp; } /* Increment pc so that |sp - fp->slots == ReconstructStackDepth(pc)|. */ @@ -1773,7 +1769,7 @@ BEGIN_CASE(JSOP_OR) bool cond = ToBooleanOp(regs); if (cond == true) { len = GET_JUMP_OFFSET(regs.pc); - DO_NEXT_OP(len); + goto advanceAndDoOp; } } END_CASE(JSOP_OR) @@ -1783,7 +1779,7 @@ BEGIN_CASE(JSOP_AND) bool cond = ToBooleanOp(regs); if (cond == false) { len = GET_JUMP_OFFSET(regs.pc); - DO_NEXT_OP(len); + goto advanceAndDoOp; } } END_CASE(JSOP_AND) @@ -1806,7 +1802,7 @@ END_CASE(JSOP_AND) BRANCH(len); \ } \ len = 1 + JSOP_IFEQ_LENGTH; \ - DO_NEXT_OP(len); \ + goto advanceAndDoOp; \ } \ JS_END_MACRO @@ -2506,7 +2502,7 @@ BEGIN_CASE(JSOP_FUNCALL) TypeScript::Monitor(cx, script, regs.pc, newsp[-1]); regs.sp = newsp; len = JSOP_CALL_LENGTH; - DO_NEXT_OP(len); + goto advanceAndDoOp; } InitialFrameFlags initial = construct ? INITIAL_CONSTRUCT : INITIAL_NONE; @@ -2719,7 +2715,7 @@ BEGIN_CASE(JSOP_TABLESWITCH) double d; /* Don't use mozilla::DoubleIsInt32; treat -0 (double) as 0. */ if (!rref.isDouble() || (d = rref.toDouble()) != (i = int32_t(rref.toDouble()))) - DO_NEXT_OP(len); + goto advanceAndDoOp; } pc2 += JUMP_OFFSET_LEN; @@ -3249,7 +3245,7 @@ BEGIN_CASE(JSOP_LEAVEBLOCKEXPR) } else { /* Another op will pop; nothing to do here. */ len = JSOP_LEAVEFORLETIN_LENGTH; - DO_NEXT_OP(len); + goto advanceAndDoOp; } } END_CASE(JSOP_LEAVEBLOCK) @@ -3361,7 +3357,7 @@ END_CASE(JSOP_ARRAYPUSH) * catch block. */ len = 0; - DO_NEXT_OP(len); + goto advanceAndDoOp; case JSTRY_FINALLY: /* @@ -3372,7 +3368,7 @@ END_CASE(JSOP_ARRAYPUSH) PUSH_COPY(cx->getPendingException()); cx->clearPendingException(); len = 0; - DO_NEXT_OP(len); + goto advanceAndDoOp; case JSTRY_ITER: { /* This is similar to JSOP_ENDITER in the interpreter loop. */ diff --git a/js/src/vm/Shape.h b/js/src/vm/Shape.h index b10a50f50d7a..d506c60470d5 100644 --- a/js/src/vm/Shape.h +++ b/js/src/vm/Shape.h @@ -1044,28 +1044,64 @@ struct StackShape SkipRoot skip; MOZ_DECL_USE_GUARD_OBJECT_NOTIFIER }; - }; +}; } /* namespace js */ /* js::Shape pointer tag bit indicating a collision. */ #define SHAPE_COLLISION (uintptr_t(1)) -#define SHAPE_REMOVED ((Shape *) SHAPE_COLLISION) +#define SHAPE_REMOVED ((js::Shape *) SHAPE_COLLISION) -/* Macros to get and set shape pointer values and collision flags. */ -#define SHAPE_IS_FREE(shape) ((shape) == NULL) -#define SHAPE_IS_REMOVED(shape) ((shape) == SHAPE_REMOVED) -#define SHAPE_IS_LIVE(shape) ((shape) > SHAPE_REMOVED) -#define SHAPE_FLAG_COLLISION(spp,shape) (*(spp) = (Shape *) \ - (uintptr_t(shape) | SHAPE_COLLISION)) -#define SHAPE_HAD_COLLISION(shape) (uintptr_t(shape) & SHAPE_COLLISION) -#define SHAPE_FETCH(spp) SHAPE_CLEAR_COLLISION(*(spp)) +/* Functions to get and set shape pointer values and collision flags. */ -#define SHAPE_CLEAR_COLLISION(shape) \ - ((Shape *) (uintptr_t(shape) & ~SHAPE_COLLISION)) +inline bool +SHAPE_IS_FREE(js::Shape *shape) +{ + return shape == NULL; +} -#define SHAPE_STORE_PRESERVING_COLLISION(spp, shape) \ - (*(spp) = (Shape *) (uintptr_t(shape) | SHAPE_HAD_COLLISION(*(spp)))) +inline bool +SHAPE_IS_REMOVED(js::Shape *shape) +{ + return shape == SHAPE_REMOVED; +} + +inline bool +SHAPE_IS_LIVE(js::Shape *shape) +{ + return shape > SHAPE_REMOVED; +} + +inline void +SHAPE_FLAG_COLLISION(js::Shape **spp, js::Shape *shape) +{ + *spp = reinterpret_cast(uintptr_t(shape) | SHAPE_COLLISION); +} + +inline bool +SHAPE_HAD_COLLISION(js::Shape *shape) +{ + return uintptr_t(shape) & SHAPE_COLLISION; +} + +inline js::Shape * +SHAPE_CLEAR_COLLISION(js::Shape *shape) +{ + return reinterpret_cast(uintptr_t(shape) & ~SHAPE_COLLISION); +} + +inline js::Shape * +SHAPE_FETCH(js::Shape **spp) +{ + return SHAPE_CLEAR_COLLISION(*spp); +} + +inline void +SHAPE_STORE_PRESERVING_COLLISION(js::Shape **spp, js::Shape *shape) +{ + *spp = reinterpret_cast(uintptr_t(shape) | + SHAPE_HAD_COLLISION(*spp)); +} namespace js { diff --git a/layout/inspector/tests/test_bug877690.html b/layout/inspector/tests/test_bug877690.html index 773ea4f80a2a..6dc3f4b6ba9b 100644 --- a/layout/inspector/tests/test_bug877690.html +++ b/layout/inspector/tests/test_bug877690.html @@ -92,7 +92,7 @@ function do_test() { "repeat-x", "repeat-y", "fixed", "scroll", "center", "top", "bottom", "left", "right", "border-box", "padding-box", "content-box", "border-box", "padding-box", "content-box", "contain", "cover" ]; - ok(testValues(values, expected), "Shorthand proprety values."); + ok(testValues(values, expected), "Shorthand property values."); // test keywords only var prop = "border-top"; diff --git a/layout/reftests/backgrounds/vector/empty/reftest.list b/layout/reftests/backgrounds/vector/empty/reftest.list index 1a7af876cdca..d83e5c1da69c 100644 --- a/layout/reftests/backgrounds/vector/empty/reftest.list +++ b/layout/reftests/backgrounds/vector/empty/reftest.list @@ -2,10 +2,12 @@ == tall--contain--width.html ref-tall-empty.html == wide--contain--height.html ref-wide-empty.html == wide--contain--width.html ref-wide-empty.html -== tall--cover--height.html ref-tall-lime.html -== tall--cover--width.html ref-tall-lime.html -== wide--cover--height.html ref-wide-lime.html -== wide--cover--width.html ref-wide-lime.html + +# These tests fail because of integer overflow; see bug 894555. +fails == tall--cover--height.html ref-tall-lime.html +fails == tall--cover--width.html ref-tall-lime.html +fails == wide--cover--height.html ref-wide-lime.html +fails == wide--cover--width.html ref-wide-lime.html == zero-height-ratio-contain.html ref-tall-empty.html == zero-height-ratio-cover.html ref-tall-empty.html diff --git a/layout/reftests/svg/as-image/background-scale-no-viewbox-1-ref.html b/layout/reftests/svg/as-image/background-scale-no-viewbox-1-ref.html new file mode 100644 index 000000000000..e9ee74d29b10 --- /dev/null +++ b/layout/reftests/svg/as-image/background-scale-no-viewbox-1-ref.html @@ -0,0 +1,21 @@ + + + + + + +
+ + diff --git a/layout/reftests/svg/as-image/background-scale-no-viewbox-1.html b/layout/reftests/svg/as-image/background-scale-no-viewbox-1.html new file mode 100644 index 000000000000..e8ea278bb61e --- /dev/null +++ b/layout/reftests/svg/as-image/background-scale-no-viewbox-1.html @@ -0,0 +1,22 @@ + + + + + + +
+ + diff --git a/layout/reftests/svg/as-image/background-scale-with-viewbox-1-ref.html b/layout/reftests/svg/as-image/background-scale-with-viewbox-1-ref.html new file mode 100644 index 000000000000..e9ee74d29b10 --- /dev/null +++ b/layout/reftests/svg/as-image/background-scale-with-viewbox-1-ref.html @@ -0,0 +1,21 @@ + + + + + + +
+ + diff --git a/layout/reftests/svg/as-image/background-scale-with-viewbox-1.html b/layout/reftests/svg/as-image/background-scale-with-viewbox-1.html new file mode 100644 index 000000000000..295aedf52020 --- /dev/null +++ b/layout/reftests/svg/as-image/background-scale-with-viewbox-1.html @@ -0,0 +1,22 @@ + + + + + + +
+ + diff --git a/layout/reftests/svg/as-image/background-stretch-1-ref.html b/layout/reftests/svg/as-image/background-stretch-1-ref.html new file mode 100644 index 000000000000..f84768e27c4b --- /dev/null +++ b/layout/reftests/svg/as-image/background-stretch-1-ref.html @@ -0,0 +1,37 @@ + + + + + + +
+
+
+
+
+ + diff --git a/layout/reftests/svg/as-image/background-stretch-1.html b/layout/reftests/svg/as-image/background-stretch-1.html new file mode 100644 index 000000000000..0931cf3ccce9 --- /dev/null +++ b/layout/reftests/svg/as-image/background-stretch-1.html @@ -0,0 +1,22 @@ + + + + + + +
+ + diff --git a/layout/reftests/svg/as-image/reftest.list b/layout/reftests/svg/as-image/reftest.list index 7e0195d0f87d..0ba21dd7503b 100644 --- a/layout/reftests/svg/as-image/reftest.list +++ b/layout/reftests/svg/as-image/reftest.list @@ -16,6 +16,13 @@ skip-if(B2G) == background-simple-1.html lime100x100-ref.html # bug 773482 == background-resize-3.html lime100x100-ref.html == background-resize-4.html lime100x100-ref.html +# Test for stretching background images by different amounts in each dimension +== background-stretch-1.html background-stretch-1-ref.html + +# Tests for scaling background images +== background-scale-no-viewbox-1.html background-scale-no-viewbox-1-ref.html +== background-scale-with-viewbox-1.html background-scale-with-viewbox-1-ref.html + # Tests with -moz-image-rect() skip-if(B2G) == background-image-rect-1svg.html lime100x100-ref.html # bug 773482 == background-image-rect-1png.html lime100x100-ref.html diff --git a/layout/reftests/svg/as-image/white-rect-no-viewbox.svg b/layout/reftests/svg/as-image/white-rect-no-viewbox.svg new file mode 100644 index 000000000000..76a7efd3dc80 --- /dev/null +++ b/layout/reftests/svg/as-image/white-rect-no-viewbox.svg @@ -0,0 +1,8 @@ + + + + + + diff --git a/layout/reftests/svg/as-image/white-rect-with-viewbox.svg b/layout/reftests/svg/as-image/white-rect-with-viewbox.svg new file mode 100644 index 000000000000..6bb59d19a492 --- /dev/null +++ b/layout/reftests/svg/as-image/white-rect-with-viewbox.svg @@ -0,0 +1,11 @@ + + + + + + diff --git a/mobile/android/base/Makefile.in b/mobile/android/base/Makefile.in index 94b2f7db233e..d575b02da9bd 100644 --- a/mobile/android/base/Makefile.in +++ b/mobile/android/base/Makefile.in @@ -234,7 +234,6 @@ FENNEC_JAVA_FILES = \ menu/MenuItemDefault.java \ menu/MenuPanel.java \ menu/MenuPopup.java \ - preferences/SearchPreferenceCategory.java \ widget/AboutHome.java \ widget/AboutHomeView.java \ widget/AboutHomeSection.java \ @@ -311,7 +310,6 @@ FENNEC_PP_JAVA_FILES = \ FENNEC_PP_XML_FILES = \ res/xml/preferences.xml \ - res/xml/preferences_customize.xml \ res/xml/searchable.xml \ $(NULL) @@ -575,15 +573,14 @@ RES_VALUES_V14 = \ $(NULL) RES_XML = \ + res/xml/preferences_customize.xml \ res/xml/preferences_display.xml \ - res/xml/preferences_search.xml \ res/xml/preferences_privacy.xml \ res/xml/preferences_vendor.xml \ $(SYNC_RES_XML) \ $(NULL) RES_XML_V11 = \ - res/xml-v11/preferences_customize.xml \ res/xml-v11/preference_headers.xml \ res/xml-v11/preferences_customize_tablet.xml \ res/xml-v11/preferences.xml \ diff --git a/mobile/android/base/locales/en-US/android_strings.dtd b/mobile/android/base/locales/en-US/android_strings.dtd index aeadc707ebf2..de7b3db80474 100644 --- a/mobile/android/base/locales/en-US/android_strings.dtd +++ b/mobile/android/base/locales/en-US/android_strings.dtd @@ -68,12 +68,10 @@ - - diff --git a/mobile/android/base/preferences/SearchPreferenceCategory.java b/mobile/android/base/preferences/SearchPreferenceCategory.java deleted file mode 100644 index 313787485912..000000000000 --- a/mobile/android/base/preferences/SearchPreferenceCategory.java +++ /dev/null @@ -1,89 +0,0 @@ -package org.mozilla.gecko.preferences; - -import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.drawable.BitmapDrawable; -import android.os.Build; -import android.preference.Preference; -import android.preference.PreferenceCategory; -import android.util.AttributeSet; -import android.util.Log; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; -import org.mozilla.gecko.GeckoAppShell; -import org.mozilla.gecko.GeckoEvent; -import org.mozilla.gecko.R; -import org.mozilla.gecko.gfx.BitmapUtils; -import org.mozilla.gecko.util.GeckoEventListener; - -public class SearchPreferenceCategory extends PreferenceCategory implements GeckoEventListener { - public static final String LOGTAG = "SearchPrefCategory"; - - private static int sIconSize; - - public SearchPreferenceCategory(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - init(); - } - public SearchPreferenceCategory(Context context, AttributeSet attrs) { - super(context, attrs); - init(); - } - - public SearchPreferenceCategory(Context context) { - super(context); - init(); - } - - private void init() { - sIconSize = getContext().getResources().getDimensionPixelSize(R.dimen.searchpreferences_icon_size); - } - - @Override - protected void onAttachedToActivity() { - super.onAttachedToActivity(); - - // Request list of search engines from Gecko - GeckoAppShell.registerEventListener("SearchEngines:Data", this); - GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("SearchEngines:Get", null)); - } - - @Override - public void handleMessage(String event, final JSONObject data) { - if (event.equals("SearchEngines:Data")) { - JSONArray engines; - try { - engines = data.getJSONArray("searchEngines"); - } catch (JSONException e) { - Log.e(LOGTAG, "Unable to decode search engine data from Gecko.", e); - return; - } - - // Create an element in this PreferenceCategory for each engine. - for (int i = 0; i < engines.length(); i++) { - try { - JSONObject engineJSON = engines.getJSONObject(i); - final String engineName = engineJSON.getString("name"); - - Preference engine = new Preference(getContext()); - engine.setTitle(engineName); - engine.setKey(engineName); - - // The setIcon feature is not available prior to API 11. - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { - String iconURI = engineJSON.getString("iconURI"); - Bitmap iconBitmap = BitmapUtils.getBitmapFromDataURI(iconURI); - Bitmap scaledIconBitmap = Bitmap.createScaledBitmap(iconBitmap, sIconSize, sIconSize, false); - BitmapDrawable drawable = new BitmapDrawable(scaledIconBitmap); - engine.setIcon(drawable); - } - addPreference(engine); - // TODO: Bug 892113 - Add event listener here for tapping on each element. Produce a dialog to provide options. - } catch (JSONException e) { - Log.e(LOGTAG, "JSONException parsing engine at index " + i, e); - } - } - } - } -} diff --git a/mobile/android/base/resources/values/dimens.xml b/mobile/android/base/resources/values/dimens.xml index 8d9670aa1b40..592d6278a904 100644 --- a/mobile/android/base/resources/values/dimens.xml +++ b/mobile/android/base/resources/values/dimens.xml @@ -61,7 +61,6 @@ 48dp 64dp 26dp - 32dp 90dp 160dp 22sp diff --git a/mobile/android/base/resources/xml-v11/preferences_customize_tablet.xml b/mobile/android/base/resources/xml-v11/preferences_customize_tablet.xml index 48f10bdf1768..40977f940932 100644 --- a/mobile/android/base/resources/xml-v11/preferences_customize_tablet.xml +++ b/mobile/android/base/resources/xml-v11/preferences_customize_tablet.xml @@ -14,12 +14,6 @@ - - - - - + - - - - + + - - - - - - - - - - - - - - - - - diff --git a/mobile/android/base/resources/xml/preferences_search.xml b/mobile/android/base/resources/xml/preferences_search.xml deleted file mode 100644 index 721aefbfda00..000000000000 --- a/mobile/android/base/resources/xml/preferences_search.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - diff --git a/mobile/android/base/strings.xml.in b/mobile/android/base/strings.xml.in index 428dc4e223ec..efa1ec131cfb 100644 --- a/mobile/android/base/strings.xml.in +++ b/mobile/android/base/strings.xml.in @@ -80,12 +80,10 @@ &settings; &settings_title; &pref_category_customize; - &pref_category_search; &pref_category_display; &pref_category_privacy_short; &pref_category_vendor; &pref_category_datareporting; - &pref_category_installed_search_engines; &pref_header_customize; &pref_header_display; diff --git a/netwerk/protocol/http/nsHttpChannel.cpp b/netwerk/protocol/http/nsHttpChannel.cpp index 60af7a572ba6..f2c9f225e8d7 100644 --- a/netwerk/protocol/http/nsHttpChannel.cpp +++ b/netwerk/protocol/http/nsHttpChannel.cpp @@ -2548,6 +2548,12 @@ nsHttpChannel::OpenCacheEntry(bool usingSSL) } } + if (mLoadFlags & LOAD_INITIAL_DOCUMENT_URI) { + mozilla::Telemetry::Accumulate( + Telemetry::HTTP_OFFLINE_CACHE_DOCUMENT_LOAD, + !!mApplicationCache); + } + nsCOMPtr session; // If we have an application cache, we check it first. diff --git a/python/mozboot/mozboot/osx.py b/python/mozboot/mozboot/osx.py index a4c917d32da6..8aa2d26b70b4 100644 --- a/python/mozboot/mozboot/osx.py +++ b/python/mozboot/mozboot/osx.py @@ -240,7 +240,7 @@ class OSXBootstrapper(BaseBootstrapper): self.run_as_root([self.port, '-v', 'install', MACPORTS_CLANG_PACKAGE]) self.run_as_root([self.port, 'select', '--set', 'python', 'python27']) - self.run_as_root([self.port, 'select', '--set', 'clang', MACPORTS_CLANG_PACKAGE]) + self.run_as_root([self.port, 'select', '--set', 'clang', 'mp-' + MACPORTS_CLANG_PACKAGE]) def ensure_package_manager(self): ''' diff --git a/testing/xpcshell/example/unit/test_do_get_tempdir.js b/testing/xpcshell/example/unit/test_do_get_tempdir.js new file mode 100644 index 000000000000..506a80b2eaec --- /dev/null +++ b/testing/xpcshell/example/unit/test_do_get_tempdir.js @@ -0,0 +1,16 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* This tests that do_get_tempdir returns a directory that we can write to. */ + +const Ci = Components.interfaces; + +function run_test() { + let tmpd = do_get_tempdir(); + do_check_true(tmpd.exists()); + tmpd.append("testfile"); + tmpd.create(Ci.nsIFile.NORMAL_FILE_TYPE, 600); + do_check_true(tmpd.exists()); +} diff --git a/testing/xpcshell/example/unit/xpcshell.ini b/testing/xpcshell/example/unit/xpcshell.ini index 6a51f336491d..edd2c0446795 100644 --- a/testing/xpcshell/example/unit/xpcshell.ini +++ b/testing/xpcshell/example/unit/xpcshell.ini @@ -6,6 +6,7 @@ head = tail = +[test_do_get_tempdir.js] [test_execute_soon.js] [test_get_file.js] [test_get_idle.js] diff --git a/testing/xpcshell/head.js b/testing/xpcshell/head.js index ed41443235bd..d0f4afd06a08 100644 --- a/testing/xpcshell/head.js +++ b/testing/xpcshell/head.js @@ -917,6 +917,23 @@ function do_register_cleanup(aFunction) _cleanupFunctions.push(aFunction); } +/** + * Returns the directory for a temp dir, which is created by the + * test harness. Every test gets its own temp dir. + * + * @return nsILocalFile of the temporary directory + */ +function do_get_tempdir() { + let env = Components.classes["@mozilla.org/process/environment;1"] + .getService(Components.interfaces.nsIEnvironment); + // the python harness sets this in the environment for us + let path = env.get("XPCSHELL_TEST_TEMP_DIR"); + let file = Components.classes["@mozilla.org/file/local;1"] + .createInstance(Components.interfaces.nsILocalFile); + file.initWithPath(path); + return file; +} + /** * Registers a directory with the profile service, * and return the directory as an nsILocalFile. diff --git a/testing/xpcshell/remotexpcshelltests.py b/testing/xpcshell/remotexpcshelltests.py index 2b02e3ed4f29..17267abfab85 100644 --- a/testing/xpcshell/remotexpcshelltests.py +++ b/testing/xpcshell/remotexpcshelltests.py @@ -248,6 +248,17 @@ class XPCShellRemote(xpcshell.XPCShellTests, object): return ['-e', 'const _TEST_FILE = ["%s"];' % replaceBackSlashes(remoteName)] + def setupTempDir(self): + # make sure the temp dir exists + if self.device.dirExists(self.remoteTmpDir): + self.device.removeDir(self.remoteTmpDir) + self.device.mkDir(self.remoteTmpDir) + + self.env["XPCSHELL_TEST_TEMP_DIR"] = self.remoteTmpDir + if self.interactive: + self.log.info("TEST-INFO | temp dir is %s" % self.remoteTmpDir) + return self.remoteTmpDir + def setupProfileDir(self): self.device.removeDir(self.profileDir) self.device.mkDir(self.profileDir) @@ -299,6 +310,7 @@ class XPCShellRemote(xpcshell.XPCShellTests, object): self.env["XPCSHELL_TEST_PROFILE_DIR"]=self.profileDir self.env["TMPDIR"]=self.remoteTmpDir self.env["HOME"]=self.profileDir + self.setupTempDir() if self.options.setup: self.pushWrapper() diff --git a/testing/xpcshell/runxpcshelltests.py b/testing/xpcshell/runxpcshelltests.py index 43371aea6456..7ef2af471758 100644 --- a/testing/xpcshell/runxpcshelltests.py +++ b/testing/xpcshell/runxpcshelltests.py @@ -312,6 +312,13 @@ class XPCShellTests(object): self.log.info("TEST-INFO | profile dir is %s" % profileDir) return profileDir + def setupTempDir(self): + tempDir = mkdtemp() + self.env["XPCSHELL_TEST_TEMP_DIR"] = tempDir + if self.interactive: + self.log.info("TEST-INFO | temp dir is %s" % tempDir) + return tempDir + def setupLeakLogging(self): """ Enable leaks (only) detection to its own log file and set environment variables. @@ -823,6 +830,39 @@ class XPCShellTests(object): return self.failCount == 0 + def print_stdout(self, stdout): + """Print stdout line-by-line to avoid overflowing buffers.""" + self.log.info(">>>>>>>") + if (stdout): + for line in stdout.splitlines(): + self.log.info(line) + self.log.info("<<<<<<<") + + def cleanupDir(self, directory, name, stdout, xunit_result): + try: + self.removeDir(directory) + except Exception: + self.log.info("TEST-INFO | Failed to remove directory: %s. Waiting." % directory) + + # We suspect the filesystem may still be making changes. Wait a + # little bit and try again. + time.sleep(5) + + try: + self.removeDir(directory) + except Exception: + message = "TEST-UNEXPECTED-FAIL | %s | Failed to clean up directory: %s" % (name, sys.exc_info()[1]) + self.log.error(message) + self.print_stdout(stdout) + self.print_stdout(traceback.format_exc()) + + self.failCount += 1 + xunit_result["passed"] = False + xunit_result["failure"] = { + "type": "TEST-UNEXPECTED-FAIL", + "message": message, + "text": "%s\n%s" % (stdout, traceback.format_exc()) + } def run_test(self, test, tests_root_dir=None, app_dir_key=None, interactive=False, verbose=False, pStdout=None, pStderr=None, @@ -871,8 +911,10 @@ class XPCShellTests(object): head_files, tail_files = self.getHeadAndTailFiles(test) cmdH = self.buildCmdHead(head_files, tail_files, self.xpcsCmd) - # Create a temp dir that the JS harness can stick a profile in + # Create a profile and a temp dir that the JS harness can stick + # a profile and temporary data in self.profileDir = self.setupProfileDir() + self.tempDir = self.setupTempDir() self.leakLogFile = self.setupLeakLogging() # The test file will have to be loaded after the head files. @@ -917,14 +959,6 @@ class XPCShellTests(object): if testTimer: testTimer.cancel() - def print_stdout(stdout): - """Print stdout line-by-line to avoid overflowing buffers.""" - self.log.info(">>>>>>>") - if (stdout): - for line in stdout.splitlines(): - self.log.info(line) - self.log.info("<<<<<<<") - result = not ((self.getReturnCode(proc) != 0) or # if do_throw or do_check failed (stdout and re.search("^((parent|child): )?TEST-UNEXPECTED-", @@ -943,7 +977,7 @@ class XPCShellTests(object): message = "%s | %s | test failed (with xpcshell return code: %d), see following log:" % ( failureType, name, self.getReturnCode(proc)) self.log.error(message) - print_stdout(stdout) + self.print_stdout(stdout) self.failCount += 1 xunit_result["passed"] = False @@ -958,7 +992,7 @@ class XPCShellTests(object): xunit_result["time"] = now - startTime self.log.info("TEST-%s | %s | test passed (time: %.3fms)" % ("PASS" if expected else "KNOWN-FAIL", name, timeTaken)) if verbose: - print_stdout(stdout) + self.print_stdout(stdout) xunit_result["passed"] = True @@ -996,7 +1030,7 @@ class XPCShellTests(object): if proc and self.poll(proc) is None: message = "TEST-UNEXPECTED-FAIL | %s | Process still running after test!" % name self.log.error(message) - print_stdout(stdout) + self.print_stdout(stdout) self.failCount += 1 xunit_result["passed"] = False xunit_result["failure"] = { @@ -1010,30 +1044,9 @@ class XPCShellTests(object): # We don't want to delete the profile when running check-interactive # or check-one. if self.profileDir and not self.interactive and not self.singleFile: - try: - self.removeDir(self.profileDir) - except Exception: - self.log.info("TEST-INFO | Failed to remove profile directory. Waiting.") + self.cleanupDir(self.profileDir, name, stdout, xunit_result) - # We suspect the filesystem may still be making changes. Wait a - # little bit and try again. - time.sleep(5) - - try: - self.removeDir(self.profileDir) - except Exception: - message = "TEST-UNEXPECTED-FAIL | %s | Failed to clean up the test profile directory: %s" % (name, sys.exc_info()[1]) - self.log.error(message) - print_stdout(stdout) - print_stdout(traceback.format_exc()) - - self.failCount += 1 - xunit_result["passed"] = False - xunit_result["failure"] = { - "type": "TEST-UNEXPECTED-FAIL", - "message": message, - "text": "%s\n%s" % (stdout, traceback.format_exc()) - } + self.cleanupDir(self.tempDir, name, stdout, xunit_result) if gotSIGINT: xunit_result["passed"] = False diff --git a/toolkit/components/telemetry/Histograms.json b/toolkit/components/telemetry/Histograms.json index d3518f3a663d..f348ac6c6d1a 100644 --- a/toolkit/components/telemetry/Histograms.json +++ b/toolkit/components/telemetry/Histograms.json @@ -1067,6 +1067,10 @@ "n_values": 5, "description": "HTTP Offline Cache Hit, Reval, Failed-Reval, Miss" }, + "HTTP_OFFLINE_CACHE_DOCUMENT_LOAD": { + "kind": "boolean", + "description": "Rate of page load from offline cache" + }, "CACHE_DEVICE_SEARCH_2": { "kind": "exponential", "high": "10000", @@ -3708,5 +3712,45 @@ "kind": "enumerated", "n_values": 4, "description": "Accumulates type of content (mixed, mixed passive, unmixed) per page load" + }, + "FX_THUMBNAILS_BG_QUEUE_SIZE_ON_CAPTURE": { + "kind": "exponential", + "high": 100, + "n_buckets": 15, + "extended_statistics_ok": true, + "description": "BACKGROUND THUMBNAILS: Size of capture queue when a capture request is received" + }, + "FX_THUMBNAILS_BG_CAPTURE_QUEUE_TIME_MS": { + "kind": "exponential", + "high": 300000, + "n_buckets": 20, + "extended_statistics_ok": true, + "description": "BACKGROUND THUMBNAILS: Time the capture request spent in the queue before being serviced (ms)" + }, + "FX_THUMBNAILS_BG_CAPTURE_SERVICE_TIME_MS": { + "kind": "exponential", + "high": 30000, + "n_buckets": 20, + "extended_statistics_ok": true, + "description": "BACKGROUND THUMBNAILS: Time the capture took once it started and successfully completed (ms)" + }, + "FX_THUMBNAILS_BG_CAPTURE_DONE_REASON": { + "kind": "enumerated", + "n_values": 4, + "description": "BACKGROUND THUMBNAILS: Reason the capture completed (see TEL_CAPTURE_DONE_* constants in BackgroundPageThumbs.jsm)" + }, + "FX_THUMBNAILS_BG_CAPTURE_PAGE_LOAD_TIME_MS": { + "kind": "exponential", + "high": 60000, + "n_buckets": 20, + "extended_statistics_ok": true, + "description": "BACKGROUND THUMBNAILS: Time the capture's page load took (ms)" + }, + "FX_THUMBNAILS_BG_CAPTURE_CANVAS_DRAW_TIME_MS": { + "kind": "exponential", + "high": 500, + "n_buckets": 15, + "extended_statistics_ok": true, + "description": "BACKGROUND THUMBNAILS: Time it took to draw the capture's window to canvas (ms)" } } diff --git a/toolkit/components/thumbnails/BackgroundPageThumbs.jsm b/toolkit/components/thumbnails/BackgroundPageThumbs.jsm index fcfc3973925d..bb3266cb6a6a 100644 --- a/toolkit/components/thumbnails/BackgroundPageThumbs.jsm +++ b/toolkit/components/thumbnails/BackgroundPageThumbs.jsm @@ -10,6 +10,14 @@ const DEFAULT_CAPTURE_TIMEOUT = 30000; // ms const DESTROY_BROWSER_TIMEOUT = 60000; // ms const FRAME_SCRIPT_URL = "chrome://global/content/backgroundPageThumbsContent.js"; +const TELEMETRY_HISTOGRAM_ID_PREFIX = "FX_THUMBNAILS_BG_"; + +// possible FX_THUMBNAILS_BG_CAPTURE_DONE_REASON telemetry values +const TEL_CAPTURE_DONE_OK = 0; +const TEL_CAPTURE_DONE_TIMEOUT = 1; +const TEL_CAPTURE_DONE_PB_BEFORE_START = 2; +const TEL_CAPTURE_DONE_PB_AFTER_START = 3; + const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; const HTML_NS = "http://www.w3.org/1999/xhtml"; @@ -38,19 +46,11 @@ const BackgroundPageThumbs = { * the queue and started. Defaults to 30000 (30 seconds). */ capture: function (url, options={}) { - if (isPrivateBrowsingActive()) { - // There's only one, global private-browsing state shared by all private - // windows and the thumbnail browser. Just as if you log into a site in - // one private window you're logged in in all private windows, you're also - // logged in in the thumbnail browser. A crude way to avoid capturing - // sites in this situation is to refuse to capture at all when any private - // windows are open. See bug 870179. - if (options.onDone) - Services.tm.mainThread.dispatch(options.onDone.bind(options, url), 0); - return; - } this._captureQueue = this._captureQueue || []; this._capturesByURL = this._capturesByURL || new Map(); + + tel("QUEUE_SIZE_ON_CAPTURE", this._captureQueue.length); + // We want to avoid duplicate captures for the same URL. If there is an // existing one, we just add the callback to that one and we are done. let existing = this._capturesByURL.get(url); @@ -222,6 +222,7 @@ function Capture(url, captureCallback, options) { this.captureCallback = captureCallback; this.options = options; this.id = Capture.nextID++; + this.creationDate = new Date(); this.doneCallbacks = []; if (options.onDone) this.doneCallbacks.push(options.onDone); @@ -239,11 +240,32 @@ Capture.prototype = { * @param messageManager The nsIMessageSender of the thumbnail browser. */ start: function (messageManager) { - let timeout = typeof(this.options.timeout) == "number" ? this.options.timeout : + this.startDate = new Date(); + tel("CAPTURE_QUEUE_TIME_MS", this.startDate - this.creationDate); + + // The thumbnail browser uses private browsing mode and therefore shares + // browsing state with private windows. To avoid capturing sites that the + // user is logged into in private browsing windows, (1) observe window + // openings, and if a private window is opened during capture, discard the + // capture when it finishes, and (2) don't start the capture at all if a + // private window is open already. + Services.ww.registerNotification(this); + if (isPrivateBrowsingActive()) { + tel("CAPTURE_DONE_REASON", TEL_CAPTURE_DONE_PB_BEFORE_START); + // Captures should always finish asyncly. + schedule(() => this._done(null)); + return; + } + + // timeout timer + let timeout = typeof(this.options.timeout) == "number" ? + this.options.timeout : DEFAULT_CAPTURE_TIMEOUT; this._timeoutTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); - this._timeoutTimer.initWithCallback(this, timeout, Ci.nsITimer.TYPE_ONE_SHOT); + this._timeoutTimer.initWithCallback(this, timeout, + Ci.nsITimer.TYPE_ONE_SHOT); + // didCapture registration this._msgMan = messageManager; this._msgMan.sendAsyncMessage("BackgroundPageThumbs:capture", { id: this.id, url: this.url }); @@ -268,10 +290,14 @@ Capture.prototype = { delete this._msgMan; } delete this.captureCallback; + Services.ww.unregisterNotification(this); }, // Called when the didCapture message is received. receiveMessage: function (msg) { + tel("CAPTURE_DONE_REASON", TEL_CAPTURE_DONE_OK); + tel("CAPTURE_SERVICE_TIME_MS", new Date() - this.startDate); + // A different timed-out capture may have finally successfully completed, so // discard messages that aren't meant for this capture. if (msg.json.id == this.id) @@ -280,9 +306,17 @@ Capture.prototype = { // Called when the timeout timer fires. notify: function () { + tel("CAPTURE_DONE_REASON", TEL_CAPTURE_DONE_TIMEOUT); this._done(null); }, + // Called when the window watcher notifies us. + observe: function (subj, topic, data) { + if (topic == "domwindowopened" && + PrivateBrowsingUtils.isWindowPrivate(subj)) + this._privateWinOpenedDuringCapture = true; + }, + _done: function (data) { // Note that _done will be called only once, by either receiveMessage or // notify, since it calls destroy, which cancels the timeout timer and @@ -291,6 +325,13 @@ Capture.prototype = { this.captureCallback(this); this.destroy(); + if (data && data.telemetry) { + // Telemetry is currently disabled in the content process (bug 680508). + for (let id in data.telemetry) { + tel(id, data.telemetry[id]); + } + } + let callOnDones = function callOnDonesFn() { for (let callback of this.doneCallbacks) { try { @@ -302,7 +343,9 @@ Capture.prototype = { } }.bind(this); - if (!data) { + if (!data || this._privateWinOpenedDuringCapture) { + if (this._privateWinOpenedDuringCapture) + tel("CAPTURE_DONE_REASON", TEL_CAPTURE_DONE_PB_AFTER_START); callOnDones(); return; } @@ -343,3 +386,18 @@ function isPrivateBrowsingActive() { return true; return false; } + +/** + * Adds a value to one of this module's telemetry histograms. + * + * @param histogramID This is prefixed with this module's ID. + * @param value The value to add. + */ +function tel(histogramID, value) { + let id = TELEMETRY_HISTOGRAM_ID_PREFIX + histogramID; + Services.telemetry.getHistogramById(id).add(value); +} + +function schedule(callback) { + Services.tm.mainThread.dispatch(callback, Ci.nsIThread.DISPATCH_NORMAL); +} diff --git a/toolkit/components/thumbnails/content/backgroundPageThumbsContent.js b/toolkit/components/thumbnails/content/backgroundPageThumbsContent.js index 7ef92e500c7a..c40b7316e7ed 100644 --- a/toolkit/components/thumbnails/content/backgroundPageThumbsContent.js +++ b/toolkit/components/thumbnails/content/backgroundPageThumbsContent.js @@ -43,11 +43,14 @@ const backgroundPageThumbsContent = { this._onLoad = function onLoad(event) { if (event.target != content.document) return; + let pageLoadTime = new Date() - loadDate; removeEventListener("load", this._onLoad, true); delete this._onLoad; let canvas = PageThumbs._createCanvas(content); + let captureDate = new Date(); PageThumbs._captureToCanvas(content, canvas); + let captureTime = new Date() - captureDate; let finalURL = this._webNav.currentURI.spec; let fileReader = Cc["@mozilla.org/files/filereader;1"]. @@ -57,6 +60,10 @@ const backgroundPageThumbsContent = { id: msg.json.id, imageData: fileReader.result, finalURL: finalURL, + telemetry: { + CAPTURE_PAGE_LOAD_TIME_MS: pageLoadTime, + CAPTURE_CANVAS_DRAW_TIME_MS: captureTime, + }, }); }; canvas.toBlob(blob => fileReader.readAsArrayBuffer(blob)); @@ -70,6 +77,7 @@ const backgroundPageThumbsContent = { addEventListener("load", this._onLoad, true); this._webNav.loadURI(msg.json.url, Ci.nsIWebNavigation.LOAD_FLAGS_NONE, null, null, null); + let loadDate = new Date(); }, }; diff --git a/toolkit/components/thumbnails/test/browser_thumbnails_background.js b/toolkit/components/thumbnails/test/browser_thumbnails_background.js index 8657033f012e..689b0c404b3c 100644 --- a/toolkit/components/thumbnails/test/browser_thumbnails_background.js +++ b/toolkit/components/thumbnails/test/browser_thumbnails_background.js @@ -164,6 +164,42 @@ let tests = [ win.close(); }, + function openPrivateWindowDuringCapture() { + let url = "http://example.com/"; + let file = fileForURL(url); + ok(!file.exists(), "Thumbnail file should not already exist."); + + let deferred = imports.Promise.defer(); + + let waitCount = 0; + function maybeFinish() { + if (++waitCount == 2) + deferred.resolve(); + } + + imports.BackgroundPageThumbs.capture(url, { + onDone: function (capturedURL) { + is(capturedURL, url, "Captured URL should be URL passed to capture."); + ok(!file.exists(), + "Thumbnail file should not exist because a private window " + + "was opened during the capture."); + maybeFinish(); + }, + }); + + // Opening the private window at this point relies on a couple of + // implementation details: (1) The capture will start immediately and + // synchronously (since at this point in the test, the service is + // initialized and its queue is empty), and (2) when it starts the capture + // registers with the window watcher. + openPrivateWindow().then(function (win) { + win.close(); + maybeFinish(); + }); + + yield deferred.promise; + }, + function noCookies() { // Visit the test page in the browser and tell it to set a cookie. let url = testPageURL({ setGreenCookie: true }); @@ -263,7 +299,7 @@ let tests = [ imports.BackgroundPageThumbs.capture(url, {onDone: doneCallback}); imports.BackgroundPageThumbs.capture(url, {onDone: doneCallback}); yield deferred.promise; - } + }, ]; function capture(url, options) {