Merge f-t to m-c

This commit is contained in:
Phil Ringnalda 2014-02-15 10:10:31 -08:00
commit 89c3345063
110 changed files with 1661 additions and 1227 deletions

View File

@ -1360,6 +1360,9 @@ pref("network.disable.ipc.security", true);
// CustomizableUI debug logging.
pref("browser.uiCustomization.debug", false);
// CustomizableUI state of the browser's user interface
pref("browser.uiCustomization.state", "");
// The URL where remote content that composes the UI for Firefox Accounts should
// be fetched. Must use HTTPS.
pref("identity.fxaccounts.remote.uri", "https://accounts.firefox.com/?service=sync&context=fx_desktop_v1");

View File

@ -50,9 +50,6 @@
<command id="cmd_findPrevious"
oncommand="gFindBar.onFindAgainCommand(true);"
observes="isImage"/>
#ifdef XP_MACOSX
<command id="cmd_findSelection" oncommand="gFindBar.onFindSelectionCommand();"/>
#endif
<!-- work-around bug 392512 -->
<command id="Browser:AddBookmarkAs"
oncommand="PlacesCommandHook.bookmarkCurrentPage(true, PlacesUtils.bookmarksMenuFolderId);"/>
@ -353,9 +350,6 @@
<key id="key_find" key="&findOnCmd.commandkey;" command="cmd_find" modifiers="accel"/>
<key id="key_findAgain" key="&findAgainCmd.commandkey;" command="cmd_findAgain" modifiers="accel"/>
<key id="key_findPrevious" key="&findAgainCmd.commandkey;" command="cmd_findPrevious" modifiers="accel,shift"/>
#ifdef XP_MACOSX
<key id="key_findSelection" key="&findSelectionCmd.commandkey;" command="cmd_findSelection" modifiers="accel"/>
#endif
<key keycode="&findAgainCmd.commandkey2;" command="cmd_findAgain"/>
<key keycode="&findAgainCmd.commandkey2;" command="cmd_findPrevious" modifiers="shift"/>

View File

@ -4238,6 +4238,7 @@ function onViewToolbarsPopupShowing(aEvent, aInsertPoint) {
while (toolbarItem && toolbarItem.parentNode) {
let parent = toolbarItem.parentNode;
if ((parent.classList && parent.classList.contains("customization-target")) ||
parent.getAttribute("overflowfortoolbar") || // Needs to work in the overflow list as well.
parent.localName == "toolbarpaletteitem" ||
parent.localName == "toolbar")
break;

View File

@ -269,7 +269,14 @@
class="customize-context-removeFromToolbar"/>
<menuseparator/>
<menuseparator id="viewToolbarsMenuSeparator"/>
<menuitem command="cmd_CustomizeToolbars"
<!-- XXXgijs: we're using oncommand handler here to avoid the event being
redirected to the command element, thus preventing
listeners on the menupopup or further up the tree from
seeing the command event pass by. The observes attribute is
here so that the menuitem is still disabled and re-enabled
correctly. -->
<menuitem oncommand="BrowserCustomizeToolbar()"
observes="cmd_CustomizeToolbars"
class="viewCustomizeToolbar"
label="&viewCustomizeToolbar.label;"
accesskey="&viewCustomizeToolbar.accesskey;"/>

View File

@ -11,9 +11,6 @@ let texts = [
"To err is human; to forgive is not company policy."
];
let Clipboard = Cc["@mozilla.org/widget/clipboard;1"].getService(Ci.nsIClipboard);
let HasFindClipboard = Clipboard.supportsFindClipboard();
function addTabWithText(aText, aCallback) {
let newTab = gBrowser.addTab("data:text/html,<h1 id='h1'>" + aText + "</h1>");
tabs.push(newTab);
@ -64,9 +61,7 @@ function continueTests1() {
// Confirm the first tab is still correct, ensure re-hiding works as expected
gBrowser.selectedTab = tabs[0];
ok(!gFindBar.hidden, "First tab shows find bar!");
// When the Find Clipboard is supported, this test not relevant.
if (!HasFindClipboard)
is(gFindBar._findField.value, texts[0], "First tab persists find value!");
is(gFindBar._findField.value, texts[0], "First tab persists find value!");
ok(gFindBar.getElement("highlight").checked,
"Highlight button state persists!");
@ -99,8 +94,7 @@ function continueTests2() {
ok(gFindBar.hidden, "Fourth tab doesn't show find bar!");
is(gFindBar, gBrowser.getFindBar(), "Find bar is right one!");
gFindBar.open();
let toTest = HasFindClipboard ? texts[2] : texts[1];
is(gFindBar._findField.value, toTest,
is(gFindBar._findField.value, texts[1],
"Fourth tab has second tab's find value!");
newWindow = gBrowser.replaceTabWithWindow(tabs.pop());
@ -110,8 +104,7 @@ function continueTests2() {
// Test that findbar gets restored when a tab is moved to a new window.
function checkNewWindow() {
ok(!newWindow.gFindBar.hidden, "New window shows find bar!");
let toTest = HasFindClipboard ? texts[2] : texts[1];
is(newWindow.gFindBar._findField.value, toTest,
is(newWindow.gFindBar._findField.value, texts[1],
"New window find bar has correct find value!");
ok(!newWindow.gFindBar.getElement("find-next").disabled,
"New window findbar has enabled buttons!");

View File

@ -2,10 +2,7 @@
* http://creativecommons.org/publicdomain/zero/1.0/
*/
const {Ci: interfaces, Cc: classes} = Components;
let Clipboard = Cc["@mozilla.org/widget/clipboard;1"].getService(Ci.nsIClipboard);
let HasFindClipboard = Clipboard.supportsFindClipboard();
let Ci = Components.interfaces;
function test() {
waitForExplicitFinish();
@ -40,10 +37,7 @@ function onFocus(win) {
let findBar = win.gFindBar;
selectText(win.content);
findBar.onFindCommand();
// When the OS supports the Find Clipboard (OSX), the find field value is
// persisted across Fx sessions, thus not useful to test.
if (!HasFindClipboard)
is(findBar._findField.value, "Select Me", "Findbar is initialized with selection");
is(findBar._findField.value, "Select Me", "Findbar is initialized with selection");
findBar.close();
win.close();
finish();

View File

@ -95,7 +95,7 @@ static RedirEntry kRedirMap[] = {
nsIAboutModule::ALLOW_SCRIPT },
{ "app-manager", "chrome://browser/content/devtools/app-manager/index.xul",
nsIAboutModule::ALLOW_SCRIPT },
{ "customizing", "chrome://browser/content/customizableui/aboutCustomizing.xhtml",
{ "customizing", "chrome://browser/content/customizableui/aboutCustomizing.xul",
nsIAboutModule::ALLOW_SCRIPT },
};
static const int kRedirTotal = ArrayLength(kRedirMap);

View File

@ -1,25 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- 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/. -->
<!DOCTYPE html [
<!ENTITY % htmlDTD
PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"DTD/xhtml1-strict.dtd">
%htmlDTD;
<!DOCTYPE window [
<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
%brandDTD;
<!ENTITY % browserDTD SYSTEM "chrome://browser/locale/browser.dtd">
%browserDTD;
]>
<html xmlns="http://www.w3.org/1999/xhtml"
disablefastfind="true">
<head>
<title>&customizeMode.tabTitle;</title>
<link rel="icon" type="image/x-icon"
href="chrome://browser/skin/customizableui/customizeFavicon.ico"/>
</head>
<body></body>
</html>
<window id="aboutCustomizingWindow"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
xmlns:html="http://www.w3.org/1999/xhtml"
title="&customizeMode.tabTitle;">
<html:head>
<html:link rel="icon" type="image/x-icon"
href="chrome://browser/skin/customizableui/customizeFavicon.ico"/>
</html:head>
</window>

View File

@ -30,7 +30,7 @@
<menupopup id="customization-toolbar-menu" onpopupshowing="onViewToolbarsPopupShowing(event)"/>
</button>
<spacer flex="1"/>
<button id="customization-undo-reset"
<button id="customization-undo-reset-button"
class="customizationmode-button"
hidden="true"
oncommand="gCustomizeMode.undoReset();"

View File

@ -3,7 +3,7 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
browser.jar:
content/browser/customizableui/aboutCustomizing.xhtml
content/browser/customizableui/aboutCustomizing.xul
content/browser/customizableui/panelUI.css
* content/browser/customizableui/panelUI.js
content/browser/customizableui/panelUI.xml

View File

@ -222,8 +222,10 @@
role="group"
type="arrow"
level="top"
context="toolbar-context-menu"
hidden="true">
<vbox id="widget-overflow-scroller">
<vbox id="widget-overflow-list" class="widget-overflow-list"/>
<vbox id="widget-overflow-list" class="widget-overflow-list"
overflowfortoolbar="nav-bar"/>
</vbox>
</panel>

View File

@ -36,6 +36,7 @@ const kSpecialWidgetPfx = "customizableui-special-";
const kPrefCustomizationState = "browser.uiCustomization.state";
const kPrefCustomizationAutoAdd = "browser.uiCustomization.autoAdd";
const kPrefCustomizationDebug = "browser.uiCustomization.debug";
const kPrefDrawInTitlebar = "browser.tabs.drawInTitlebar";
/**
* The keys are the handlers that are fired when the event type (the value)
@ -128,7 +129,10 @@ let gGroupWrapperCache = new Map();
let gSingleWrapperCache = new WeakMap();
let gListeners = new Set();
let gUIStateBeforeReset = null;
let gUIStateBeforeReset = {
uiCustomizationState: null,
drawInTitlebar: null,
};
let gModuleName = "[CustomizableUI]";
#include logging.js
@ -697,7 +701,7 @@ let CustomizableUIInternal = {
this.insertNode(aWidgetId, aArea, aPosition, true);
if (!gResetting) {
gUIStateBeforeReset = null;
this._clearPreviousUIState();
}
},
@ -756,19 +760,19 @@ let CustomizableUIInternal = {
}
}
if (!gResetting) {
gUIStateBeforeReset = null;
this._clearPreviousUIState();
}
},
onWidgetMoved: function(aWidgetId, aArea, aOldPosition, aNewPosition) {
this.insertNode(aWidgetId, aArea, aNewPosition);
if (!gResetting) {
gUIStateBeforeReset = null;
this._clearPreviousUIState();
}
},
onCustomizeEnd: function(aWindow) {
gUIStateBeforeReset = null;
this._clearPreviousUIState();
},
registerBuildArea: function(aArea, aNode) {
@ -2075,10 +2079,12 @@ let CustomizableUIInternal = {
_resetUIState: function() {
try {
gUIStateBeforeReset = Services.prefs.getCharPref(kPrefCustomizationState);
gUIStateBeforeReset.drawInTitlebar = Services.prefs.getBoolPref(kPrefDrawInTitlebar);
gUIStateBeforeReset.uiCustomizationState = Services.prefs.getCharPref(kPrefCustomizationState);
} catch(e) { }
Services.prefs.clearUserPref(kPrefCustomizationState);
Services.prefs.clearUserPref(kPrefDrawInTitlebar);
LOG("State reset");
// Reset placements to make restoring default placements possible.
@ -2113,17 +2119,31 @@ let CustomizableUIInternal = {
* Undoes a previous reset, restoring the state of the UI to the state prior to the reset.
*/
undoReset: function() {
if (!gUIStateBeforeReset) {
if (gUIStateBeforeReset.uiCustomizationState == null ||
gUIStateBeforeReset.drawInTitlebar == null) {
return;
}
Services.prefs.setCharPref(kPrefCustomizationState, gUIStateBeforeReset);
let uiCustomizationState = gUIStateBeforeReset.uiCustomizationState;
let drawInTitlebar = gUIStateBeforeReset.drawInTitlebar;
// Need to clear the previous state before setting the prefs
// because pref observers may check if there is a previous UI state.
this._clearPreviousUIState();
Services.prefs.setCharPref(kPrefCustomizationState, uiCustomizationState);
Services.prefs.setBoolPref(kPrefDrawInTitlebar, drawInTitlebar);
this.loadSavedState();
for (let areaId of Object.keys(gSavedState.placements)) {
let placements = gSavedState.placements[areaId];
gPlacements.set(areaId, placements);
}
this._rebuildRegisteredAreas();
gUIStateBeforeReset = null;
},
_clearPreviousUIState: function() {
Object.getOwnPropertyNames(gUIStateBeforeReset).forEach((prop) => {
gUIStateBeforeReset[prop] = null;
});
},
/**
@ -2270,6 +2290,11 @@ let CustomizableUIInternal = {
}
}
if (Services.prefs.prefHasUserValue(kPrefDrawInTitlebar)) {
LOG(kPrefDrawInTitlebar + " pref is non-default");
return false;
}
return true;
}
};
@ -2893,7 +2918,8 @@ this.CustomizableUI = {
* Restore Defaults can be performed.
*/
get canUndoReset() {
return !!gUIStateBeforeReset;
return gUIStateBeforeReset.uiCustomizationState != null ||
gUIStateBeforeReset.drawInTitlebar != null;
},
/**
@ -3395,7 +3421,11 @@ OverflowableToolbar.prototype = {
this._onResize(aEvent);
break;
case "command":
this._onClickChevron(aEvent);
if (aEvent.target == this._chevron) {
this._onClickChevron(aEvent);
} else {
this._panel.hidePopup();
}
break;
case "popuphiding":
this._onPanelHiding(aEvent);
@ -3430,11 +3460,13 @@ OverflowableToolbar.prototype = {
},
_onClickChevron: function(aEvent) {
if (this._chevron.open)
if (this._chevron.open) {
this._panel.hidePopup();
else {
} else {
let doc = aEvent.target.ownerDocument;
this._panel.hidden = false;
let contextMenu = doc.getElementById(this._panel.getAttribute("context"));
gELS.addSystemEventListener(contextMenu, 'command', this, true);
let anchor = doc.getAnonymousElementByAttribute(this._chevron, "class", "toolbarbutton-icon");
this._panel.openPopup(anchor || this._chevron, "bottomcenter topright");
}
@ -3443,6 +3475,9 @@ OverflowableToolbar.prototype = {
_onPanelHiding: function(aEvent) {
this._chevron.open = false;
let doc = aEvent.target.ownerDocument;
let contextMenu = doc.getElementById(this._panel.getAttribute("context"));
gELS.removeSystemEventListener(contextMenu, 'command', this, true);
},
onOverflow: function(aEvent) {

View File

@ -497,12 +497,34 @@ CustomizeMode.prototype = {
},
_getCustomizableChildForNode: function(aNode) {
let area = this._getCustomizableParent(aNode);
area = area.customizationTarget || area;
while (aNode && aNode.parentNode != area) {
aNode = aNode.parentNode;
// NB: adjusted from _getCustomizableParent to keep that method fast
// (it's used during drags), and avoid multiple DOM loops
let areas = CustomizableUI.areas;
// Caching this length is important because otherwise we'll also iterate
// over items we add to the end from within the loop.
let numberOfAreas = areas.length;
for (let i = 0; i < numberOfAreas; i++) {
let area = areas[i];
let areaNode = aNode.ownerDocument.getElementById(area);
let customizationTarget = areaNode && areaNode.customizationTarget;
if (customizationTarget && customizationTarget != areaNode) {
areas.push(customizationTarget.id);
}
let overflowTarget = areaNode.getAttribute("overflowtarget");
if (overflowTarget) {
areas.push(overflowTarget);
}
}
return aNode;
areas.push(kPaletteId);
while (aNode && aNode.parentNode) {
let parent = aNode.parentNode;
if (areas.indexOf(parent.id) != -1) {
return aNode;
}
aNode = parent;
}
return null;
},
addToToolbar: function(aNode) {
@ -999,8 +1021,8 @@ CustomizeMode.prototype = {
},
_updateUndoResetButton: function() {
let undoReset = this.document.getElementById("customization-undo-reset");
undoReset.hidden = !CustomizableUI.canUndoReset;
let undoResetButton = this.document.getElementById("customization-undo-reset-button");
undoResetButton.hidden = !CustomizableUI.canUndoReset;
},
handleEvent: function(aEvent) {
@ -1052,7 +1074,9 @@ CustomizeMode.prototype = {
observe: function(aSubject, aTopic, aData) {
switch (aTopic) {
case "nsPref:changed":
this._updateResetButton();
this._updateTitlebarButton();
this._updateUndoResetButton();
break;
}
},

View File

@ -11,6 +11,8 @@ support-files =
[browser_878452_drag_to_panel.js]
[browser_880164_customization_context_menus.js]
[browser_880382_drag_wide_widgets_in_panel.js]
[browser_884402_customize_from_overflow.js]
skip-if = os == "linux"
[browser_885052_customize_mode_observers_disabed.js]
# Bug 951403 - Disabled on OSX for frequent failures
skip-if = os == "mac"

View File

@ -305,58 +305,3 @@ add_task(function() {
yield hiddenPromise;
});
function contextMenuShown(aContextMenu) {
let deferred = Promise.defer();
let win = aContextMenu.ownerDocument.defaultView;
let timeoutId = win.setTimeout(() => {
deferred.reject("Context menu (" + aContextMenu.id + ") did not show within 20 seconds.");
}, 20000);
function onPopupShown(e) {
aContextMenu.removeEventListener("popupshown", onPopupShown);
win.clearTimeout(timeoutId);
deferred.resolve();
};
aContextMenu.addEventListener("popupshown", onPopupShown);
return deferred.promise;
}
function contextMenuHidden(aContextMenu) {
let deferred = Promise.defer();
let win = aContextMenu.ownerDocument.defaultView;
let timeoutId = win.setTimeout(() => {
deferred.reject("Context menu (" + aContextMenu.id + ") did not hide within 20 seconds.");
}, 20000);
function onPopupHidden(e) {
win.clearTimeout(timeoutId);
aContextMenu.removeEventListener("popuphidden", onPopupHidden);
deferred.resolve();
};
aContextMenu.addEventListener("popuphidden", onPopupHidden);
return deferred.promise;
}
// This is a simpler version of the context menu check that
// exists in contextmenu_common.js.
function checkContextMenu(aContextMenu, aExpectedEntries, aWindow=window) {
let childNodes = aContextMenu.childNodes;
for (let i = 0; i < childNodes.length; i++) {
let menuitem = childNodes[i];
try {
if (aExpectedEntries[i][0] == "---") {
is(menuitem.localName, "menuseparator", "menuseparator expected");
continue;
}
let selector = aExpectedEntries[i][0];
ok(menuitem.mozMatchesSelector(selector), "menuitem should match " + selector + " selector");
let commandValue = menuitem.getAttribute("command");
let relatedCommand = commandValue ? aWindow.document.getElementById(commandValue) : null;
let menuItemDisabled = relatedCommand ?
relatedCommand.getAttribute("disabled") == "true" :
menuitem.getAttribute("disabled") == "true";
is(menuItemDisabled, !aExpectedEntries[i][1], "disabled state for " + selector);
} catch (e) {
ok(false, "Exception when checking context menu: " + e);
}
}
}

View File

@ -0,0 +1,78 @@
"use strict";
let overflowPanel = document.getElementById("widget-overflow");
const isOSX = (Services.appinfo.OS === "Darwin");
let originalWindowWidth;
registerCleanupFunction(function() {
window.resizeTo(originalWindowWidth, window.outerHeight);
});
// Right-click on an item within the overflow panel should
// show a context menu with options to move it.
add_task(function() {
originalWindowWidth = window.outerWidth;
let navbar = document.getElementById(CustomizableUI.AREA_NAVBAR);
ok(!navbar.hasAttribute("overflowing"), "Should start with a non-overflowing toolbar.");
let oldChildCount = navbar.customizationTarget.childElementCount;
window.resizeTo(400, window.outerHeight);
yield waitForCondition(() => navbar.hasAttribute("overflowing"));
ok(navbar.hasAttribute("overflowing"), "Should have an overflowing toolbar.");
let chevron = document.getElementById("nav-bar-overflow-button");
let shownPanelPromise = promisePanelElementShown(window, overflowPanel);
chevron.click();
yield shownPanelPromise;
let contextMenu = document.getElementById("toolbar-context-menu");
let shownContextPromise = contextMenuShown(contextMenu);
let homeButton = document.getElementById("home-button");
ok(homeButton, "home-button was found");
ok(homeButton.classList.contains("overflowedItem"), "Home button is overflowing");
EventUtils.synthesizeMouse(homeButton, 2, 2, {type: "contextmenu", button: 2});
yield shownContextPromise;
is(overflowPanel.state, "open", "The widget overflow panel should still be open.");
let expectedEntries = [
[".customize-context-moveToPanel", true],
[".customize-context-removeFromToolbar", true],
["---"]
];
if (!isOSX) {
expectedEntries.push(["#toggle_toolbar-menubar", true]);
}
expectedEntries.push(
["#toggle_PersonalToolbar", true],
["---"],
[".viewCustomizeToolbar", true]
);
checkContextMenu(contextMenu, expectedEntries);
let hiddenContextPromise = contextMenuHidden(contextMenu);
let hiddenPromise = promisePanelElementHidden(window, overflowPanel);
let moveToPanel = contextMenu.querySelector(".customize-context-moveToPanel");
if (moveToPanel) {
moveToPanel.click();
}
contextMenu.hidePopup();
yield hiddenContextPromise;
yield hiddenPromise;
let homeButtonPlacement = CustomizableUI.getPlacementOfWidget("home-button");
ok(homeButtonPlacement, "Home button should still have a placement");
is(homeButtonPlacement && homeButtonPlacement.area, "PanelUI-contents", "Home button should be in the panel now");
CustomizableUI.reset();
// In some cases, it can take a tick for the navbar to overflow again. Wait for it:
yield waitForCondition(() => navbar.hasAttribute("overflowing"));
ok(navbar.hasAttribute("overflowing"), "Should have an overflowing toolbar.");
let homeButtonPlacement = CustomizableUI.getPlacementOfWidget("home-button");
ok(homeButtonPlacement, "Home button should still have a placement");
is(homeButtonPlacement && homeButtonPlacement.area, "nav-bar", "Home button should be back in the navbar now");
ok(homeButton.classList.contains("overflowedItem"), "Home button should still be overflowed");
});

View File

@ -11,18 +11,18 @@ add_task(function() {
yield startCustomizing();
ok(!CustomizableUI.inDefaultState, "Not in default state to begin with");
is(CustomizableUI.getPlacementOfWidget(homeButtonId), null, "Home button is in palette");
let undoReset = document.getElementById("customization-undo-reset");
is(undoReset.hidden, true, "The undo button is hidden before reset");
let undoResetButton = document.getElementById("customization-undo-reset-button");
is(undoResetButton.hidden, true, "The undo button is hidden before reset");
yield gCustomizeMode.reset();
ok(CustomizableUI.inDefaultState, "In default state after reset");
is(undoReset.hidden, false, "The undo button is visible after reset");
is(undoResetButton.hidden, false, "The undo button is visible after reset");
undoReset.click();
undoResetButton.click();
yield waitForCondition(function() !gCustomizeMode.resetting);
ok(!CustomizableUI.inDefaultState, "Not in default state after reset-undo");
is(undoReset.hidden, true, "The undo button is hidden after clicking on the undo button");
is(undoResetButton.hidden, true, "The undo button is hidden after clicking on the undo button");
is(CustomizableUI.getPlacementOfWidget(homeButtonId), null, "Home button is in palette");
yield gCustomizeMode.reset();
@ -34,29 +34,71 @@ add_task(function() {
CustomizableUI.removeWidgetFromArea(homeButtonId);
ok(!CustomizableUI.inDefaultState, "Not in default state to begin with");
is(CustomizableUI.getPlacementOfWidget(homeButtonId), null, "Home button is in palette");
let undoReset = document.getElementById("customization-undo-reset");
is(undoReset.hidden, true, "The undo button is hidden before reset");
let undoResetButton = document.getElementById("customization-undo-reset-button");
is(undoResetButton.hidden, true, "The undo button is hidden before reset");
yield gCustomizeMode.reset();
ok(CustomizableUI.inDefaultState, "In default state after reset");
is(undoReset.hidden, false, "The undo button is visible after reset");
is(undoResetButton.hidden, false, "The undo button is visible after reset");
CustomizableUI.addWidgetToArea(homeButtonId, CustomizableUI.AREA_PANEL);
is(undoReset.hidden, true, "The undo button is hidden after another change");
is(undoResetButton.hidden, true, "The undo button is hidden after another change");
});
// "Restore defaults", exiting customize, and re-entering shouldn't show the Undo button
add_task(function() {
let undoReset = document.getElementById("customization-undo-reset");
is(undoReset.hidden, true, "The undo button is hidden before a reset");
let undoResetButton = document.getElementById("customization-undo-reset-button");
is(undoResetButton.hidden, true, "The undo button is hidden before a reset");
ok(!CustomizableUI.inDefaultState, "The browser should not be in default state");
yield gCustomizeMode.reset();
is(undoReset.hidden, false, "The undo button is hidden after a reset");
is(undoResetButton.hidden, false, "The undo button is visible after a reset");
yield endCustomizing();
yield startCustomizing();
is(undoReset.hidden, true, "The undo reset button should be hidden after entering customization mode");
is(undoResetButton.hidden, true, "The undo reset button should be hidden after entering customization mode");
});
// Bug 971626 - Restore Defaults should collapse the Title Bar
add_task(function() {
if (Services.appinfo.OS != "WINNT" &&
Services.appinfo.OS != "Darwin") {
return;
}
let prefName = "browser.tabs.drawInTitlebar";
let defaultValue = Services.prefs.getBoolPref(prefName);
let restoreDefaultsButton = document.getElementById("customization-reset-button");
let titleBarButton = document.getElementById("customization-titlebar-visibility-button");
let undoResetButton = document.getElementById("customization-undo-reset-button");
ok(CustomizableUI.inDefaultState, "Should be in default state at start of test");
ok(restoreDefaultsButton.disabled, "Restore defaults button should be disabled when in default state");
is(titleBarButton.hasAttribute("checked"), !defaultValue, "Title bar button should reflect pref value");
is(undoResetButton.hidden, true, "Undo reset button should be hidden at start of test");
Services.prefs.setBoolPref(prefName, !defaultValue);
ok(!restoreDefaultsButton.disabled, "Restore defaults button should be enabled when pref changed");
is(titleBarButton.hasAttribute("checked"), defaultValue, "Title bar button should reflect changed pref value");
ok(!CustomizableUI.inDefaultState, "With titlebar flipped, no longer default");
is(undoResetButton.hidden, true, "Undo reset button should be hidden after pref change");
yield gCustomizeMode.reset();
ok(restoreDefaultsButton.disabled, "Restore defaults button should be disabled after reset");
is(titleBarButton.hasAttribute("checked"), !defaultValue, "Title bar button should reflect default value after reset");
is(Services.prefs.getBoolPref(prefName), defaultValue, "Reset should reset drawInTitlebar");
ok(CustomizableUI.inDefaultState, "In default state after titlebar reset");
is(undoResetButton.hidden, false, "Undo reset button should be visible after reset");
ok(!undoResetButton.disabled, "Undo reset button should be enabled after reset");
yield gCustomizeMode.undoReset();
ok(!restoreDefaultsButton.disabled, "Restore defaults button should be enabled after undo-reset");
is(titleBarButton.hasAttribute("checked"), defaultValue, "Title bar button should reflect undo-reset value");
ok(!CustomizableUI.inDefaultState, "No longer in default state after undo");
is(Services.prefs.getBoolPref(prefName), !defaultValue, "Undo-reset goes back to previous pref value");
is(undoResetButton.hidden, true, "Undo reset button should be hidden after undo-reset clicked");
Services.prefs.clearUserPref(prefName);
ok(CustomizableUI.inDefaultState, "In default state after pref cleared");
is(undoResetButton.hidden, true, "Undo reset button should be hidden at end of test");
});
add_task(function asyncCleanup() {

View File

@ -376,3 +376,60 @@ function promiseTabHistoryNavigation(aDirection = -1, aConditionFn) {
return deferred.promise;
}
function contextMenuShown(aContextMenu) {
let deferred = Promise.defer();
let win = aContextMenu.ownerDocument.defaultView;
let timeoutId = win.setTimeout(() => {
deferred.reject("Context menu (" + aContextMenu.id + ") did not show within 20 seconds.");
}, 20000);
function onPopupShown(e) {
aContextMenu.removeEventListener("popupshown", onPopupShown);
win.clearTimeout(timeoutId);
deferred.resolve();
};
aContextMenu.addEventListener("popupshown", onPopupShown);
return deferred.promise;
}
function contextMenuHidden(aContextMenu) {
let deferred = Promise.defer();
let win = aContextMenu.ownerDocument.defaultView;
let timeoutId = win.setTimeout(() => {
deferred.reject("Context menu (" + aContextMenu.id + ") did not hide within 20 seconds.");
}, 20000);
function onPopupHidden(e) {
win.clearTimeout(timeoutId);
aContextMenu.removeEventListener("popuphidden", onPopupHidden);
deferred.resolve();
};
aContextMenu.addEventListener("popuphidden", onPopupHidden);
return deferred.promise;
}
// This is a simpler version of the context menu check that
// exists in contextmenu_common.js.
function checkContextMenu(aContextMenu, aExpectedEntries, aWindow=window) {
let childNodes = aContextMenu.childNodes;
for (let i = 0; i < childNodes.length; i++) {
let menuitem = childNodes[i];
try {
if (aExpectedEntries[i][0] == "---") {
is(menuitem.localName, "menuseparator", "menuseparator expected");
continue;
}
let selector = aExpectedEntries[i][0];
ok(menuitem.mozMatchesSelector(selector), "menuitem should match " + selector + " selector");
let commandValue = menuitem.getAttribute("command");
let relatedCommand = commandValue ? aWindow.document.getElementById(commandValue) : null;
let menuItemDisabled = relatedCommand ?
relatedCommand.getAttribute("disabled") == "true" :
menuitem.getAttribute("disabled") == "true";
is(menuItemDisabled, !aExpectedEntries[i][1], "disabled state for " + selector);
} catch (e) {
ok(false, "Exception when checking context menu: " + e);
}
}
}

View File

@ -47,6 +47,7 @@ support-files =
doc_frame-parameters.html
doc_function-display-name.html
doc_function-search.html
doc_global-method-override.html
doc_iframes.html
doc_included-script.html
doc_inline-debugger-statement.html
@ -122,6 +123,7 @@ support-files =
[browser_dbg_event-listeners.js]
[browser_dbg_file-reload.js]
[browser_dbg_function-display-name.js]
[browser_dbg_global-method-override.js]
[browser_dbg_globalactor.js]
[browser_dbg_host-layout.js]
[browser_dbg_iframes.js]

View File

@ -0,0 +1,20 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests that scripts that override properties of the global object, like
* toString don't break the debugger. The test page used to cause the debugger
* to throw when trying to attach to the thread actor.
*/
const TAB_URL = EXAMPLE_URL + "doc_global-method-override.html";
function test() {
initDebugger(TAB_URL).then(([aTab, aDebuggee, aPanel]) => {
let gDebugger = aPanel.panelWin;
ok(gDebugger, "Should have a debugger available.");
is(gDebugger.gThreadClient.state, "attached", "Debugger should be attached.");
closeDebuggerAndFinish(aPanel);
});
}

View File

@ -0,0 +1,16 @@
<!-- Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ -->
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Debugger global method override test page</title>
</head>
<body>
<script type="text/javascript">
console.log( "Error: " + toString( { x: 0, y: 0 } ) );
function toString(v) { return "[ " + v.x + ", " + v.y + " ]"; }
</script>
</body>
</html>

View File

@ -60,7 +60,10 @@ const EVENTS = {
// When the html response preview is displayed in the UI.
RESPONSE_HTML_PREVIEW_DISPLAYED: "NetMonitor:ResponseHtmlPreviewAvailable",
// When `onTabSelect` is fired and subsequently rendered.
// When the image response thumbnail is displayed in the UI.
RESPONSE_IMAGE_THUMBNAIL_DISPLAYED: "NetMonitor:ResponseImageThumbnailAvailable",
// When a tab is selected in the NetworkDetailsView and subsequently rendered.
TAB_UPDATED: "NetMonitor:TabUpdated",
// Fired when Sidebar has finished being populated.
@ -105,6 +108,7 @@ const require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devt
const promise = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise;
const EventEmitter = require("devtools/shared/event-emitter");
const Editor = require("devtools/sourceeditor/editor");
const {Tooltip} = require("devtools/shared/widgets/Tooltip");
XPCOMUtils.defineLazyModuleGetter(this, "Chart",
"resource:///modules/devtools/Chart.jsm");

View File

@ -11,6 +11,8 @@ const SOURCE_SYNTAX_HIGHLIGHT_MAX_FILE_SIZE = 102400; // 100 KB in bytes
const RESIZE_REFRESH_RATE = 50; // ms
const REQUESTS_REFRESH_RATE = 50; // ms
const REQUESTS_HEADERS_SAFE_BOUNDS = 30; // px
const REQUESTS_TOOLTIP_POSITION = "topcenter bottomleft";
const REQUESTS_TOOLTIP_IMAGE_MAX_DIM = 400; // px
const REQUESTS_WATERFALL_SAFE_BOUNDS = 90; // px
const REQUESTS_WATERFALL_HEADER_TICKS_MULTIPLE = 5; // ms
const REQUESTS_WATERFALL_HEADER_TICKS_SPACING_MIN = 60; // px
@ -318,7 +320,9 @@ function RequestsMenuView() {
dumpn("RequestsMenuView was instantiated");
this._flushRequests = this._flushRequests.bind(this);
this._onHover = this._onHover.bind(this);
this._onSelect = this._onSelect.bind(this);
this._onSwap = this._onSwap.bind(this);
this._onResize = this._onResize.bind(this);
this._byFile = this._byFile.bind(this);
this._byDomain = this._byDomain.bind(this);
@ -345,6 +349,7 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
this.widget.autoscrollWithAppendedItems = true;
this.widget.addEventListener("select", this._onSelect, false);
this.widget.addEventListener("swap", this._onSwap, false);
this._splitter.addEventListener("mousemove", this._onResize, false);
window.addEventListener("resize", this._onResize, false);
@ -390,6 +395,7 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
Prefs.filters = this._activeFilters;
this.widget.removeEventListener("select", this._onSelect, false);
this.widget.removeEventListener("swap", this._onSwap, false);
this._splitter.removeEventListener("mousemove", this._onResize, false);
window.removeEventListener("resize", this._onResize, false);
@ -463,11 +469,21 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
}
});
// Create a tooltip for the newly appended network request item.
let requestTooltip = requestItem.attachment.tooltip = new Tooltip(document, {
closeOnEvents: [{
emitter: $("#requests-menu-contents"),
event: "scroll",
useCapture: true
}]
});
$("#details-pane-toggle").disabled = false;
$("#requests-menu-empty-notice").hidden = true;
this.refreshSummary();
this.refreshZebra();
this.refreshTooltip(requestItem);
if (aId == this._preferredItemId) {
this.selectedItem = requestItem;
@ -497,6 +513,7 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
copyImageAsDataUri: function() {
let selected = this.selectedItem.attachment;
let { mimeType, text, encoding } = selected.responseContent.content;
gNetwork.getString(text).then(aString => {
let data = "data:" + mimeType + ";" + encoding + "," + aString;
clipboardHelper.copyString(data, document);
@ -922,6 +939,19 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
}
},
/**
* Refreshes the toggling anchor for the specified item's tooltip.
*
* @param object aItem
* The network request item in this container.
*/
refreshTooltip: function(aItem) {
let tooltip = aItem.attachment.tooltip;
tooltip.hide();
tooltip.startTogglingOnHover(aItem.target, this._onHover);
tooltip.defaultPosition = REQUESTS_TOOLTIP_POSITION;
},
/**
* Schedules adding additional information to a network request.
*
@ -1010,13 +1040,14 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
this.updateMenuView(requestItem, key, value);
break;
case "responseContent":
requestItem.attachment.responseContent = value;
// If there's no mime type available when the response content
// is received, assume text/plain as a fallback.
if (!requestItem.attachment.mimeType) {
requestItem.attachment.mimeType = "text/plain";
this.updateMenuView(requestItem, "mimeType", "text/plain");
}
requestItem.attachment.responseContent = value;
this.updateMenuView(requestItem, key, value);
break;
case "totalTime":
requestItem.attachment.totalTime = value;
@ -1112,9 +1143,9 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
let nameWithQuery = this._getUriNameWithQuery(uri);
let hostPort = this._getUriHostPort(uri);
let node = $(".requests-menu-file", target);
node.setAttribute("value", nameWithQuery);
node.setAttribute("tooltiptext", nameWithQuery);
let file = $(".requests-menu-file", target);
file.setAttribute("value", nameWithQuery);
file.setAttribute("tooltiptext", nameWithQuery);
let domain = $(".requests-menu-domain", target);
domain.setAttribute("value", hostPort);
@ -1148,6 +1179,21 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
node.setAttribute("tooltiptext", aValue);
break;
}
case "responseContent": {
let { mimeType } = aItem.attachment;
let { text, encoding } = aValue.content;
if (mimeType.contains("image/")) {
gNetwork.getString(text).then(aString => {
let node = $(".requests-menu-icon", aItem.target);
node.src = "data:" + mimeType + ";" + encoding + "," + aString;
node.setAttribute("type", "thumbnail");
node.removeAttribute("hidden");
window.emit(EVENTS.RESPONSE_IMAGE_THUMBNAIL_DISPLAYED);
});
}
break;
}
case "totalTime": {
let node = $(".requests-menu-timings-total", target);
let text = L10N.getFormatStr("networkMenu.totalMS", aValue); // integer
@ -1425,6 +1471,49 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
}
},
/**
* The swap listener for this container.
* Called when two items switch places, when the contents are sorted.
*/
_onSwap: function({ detail: [firstItem, secondItem] }) {
// Sorting will create new anchor nodes for all the swapped request items
// in this container, so it's necessary to refresh the Tooltip instances.
this.refreshTooltip(firstItem);
this.refreshTooltip(secondItem);
},
/**
* The predicate used when deciding whether a popup should be shown
* over a request item or not.
*
* @param nsIDOMNode aTarget
* The element node currently being hovered.
* @param object aTooltip
* The current tooltip instance.
*/
_onHover: function(aTarget, aTooltip) {
let requestItem = this.getItemForElement(aTarget);
if (!requestItem || !requestItem.attachment.responseContent) {
return;
}
let hovered = requestItem.attachment;
let { url } = hovered;
let { mimeType, text, encoding } = hovered.responseContent.content;
if (mimeType && mimeType.contains("image/") && (
aTarget.classList.contains("requests-menu-icon") ||
aTarget.classList.contains("requests-menu-file")))
{
return gNetwork.getString(text).then(aString => {
let anchor = $(".requests-menu-icon", requestItem.target);
let src = "data:" + mimeType + ";" + encoding + "," + aString;
aTooltip.setImageContent(src, { maxDim: REQUESTS_TOOLTIP_IMAGE_MAX_DIM });
return anchor;
});
}
},
/**
* The resize listener for this container's window.
*/
@ -1626,7 +1715,7 @@ SidebarView.prototype = {
NetMonitorView.NetworkDetails;
return view.populate(aData).then(() => {
$("#details-pane").selectedIndex = isCustom ? 0 : 1
$("#details-pane").selectedIndex = isCustom ? 0 : 1;
window.emit(EVENTS.SIDEBAR_POPULATED);
});
}
@ -1906,6 +1995,7 @@ NetworkDetailsView.prototype = {
}
populated[tab] = true;
window.emit(EVENTS.TAB_UPDATED);
NetMonitorView.RequestsMenu.ensureSelectedItemIsVisible();
});
},

View File

@ -60,7 +60,7 @@
-moz-box-flex: 1;
}
#network-table[domain-overflows] .requests-menu-file {
#network-table[domain-overflows] .requests-menu-icon-and-file {
-moz-box-flex: 1;
}
}

View File

@ -68,8 +68,8 @@
flex="1">
</button>
</hbox>
<hbox id="requests-menu-file-header-box"
class="requests-menu-header requests-menu-file"
<hbox id="requests-menu-icon-and-file-header-box"
class="requests-menu-header requests-menu-icon-and-file"
align="center">
<button id="requests-menu-file-button"
class="requests-menu-header-button requests-menu-file"
@ -152,8 +152,13 @@
crop="end"
flex="1"/>
</hbox>
<label class="plain requests-menu-subitem requests-menu-file"
crop="end"/>
<hbox class="requests-menu-subitem requests-menu-icon-and-file"
align="center">
<image class="requests-menu-icon" hidden="true"/>
<label class="plain requests-menu-file"
crop="end"
flex="1"/>
</hbox>
<label class="plain requests-menu-subitem requests-menu-domain"
crop="end"/>
<label class="plain requests-menu-subitem requests-menu-type"

View File

@ -47,6 +47,8 @@ support-files =
[browser_net_filter-03.js]
[browser_net_footer-summary.js]
[browser_net_html-preview.js]
[browser_net_icon-preview.js]
[browser_net_image-tooltip.js]
[browser_net_json-long.js]
[browser_net_json-malformed.js]
[browser_net_json_custom_mime.js]

View File

@ -138,8 +138,6 @@ function test() {
"The text shown in the source editor is incorrect.");
is(aEditor.getMode(), Editor.modes[aEditorMode],
"The mode active in the source editor is incorrect.");
teardown(aMonitor).then(finish);
});
}

View File

@ -14,13 +14,11 @@ function test() {
RequestsMenu.lazyUpdate = false;
let imageDataUri = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==";
waitForNetworkEvents(aMonitor, 6).then(() => {
let requestItem = RequestsMenu.getItemAtIndex(5);
RequestsMenu.selectedItem = requestItem;
waitForClipboard(imageDataUri, function setup() {
waitForClipboard(TEST_IMAGE_DATA_URI, function setup() {
RequestsMenu.copyImageAsDataUri();
}, function onSuccess() {
ok(true, "Clipboard contains the currently selected image as data uri.");

View File

@ -9,7 +9,7 @@ function test() {
initNetMonitor(CONTENT_TYPE_URL).then(([aTab, aDebuggee, aMonitor]) => {
info("Starting test... ");
let { $, document, NetMonitorView } = aMonitor.panelWin;
let { $, document, EVENTS, NetMonitorView } = aMonitor.panelWin;
let { RequestsMenu } = NetMonitorView;
RequestsMenu.lazyUpdate = false;
@ -35,8 +35,7 @@ function test() {
is($("#preview-tabpanel").hidden, false,
"The preview tabpanel should be visible now.");
let RESPONSE_HTML_PREVIEW_DISPLAYED = aMonitor.panelWin.EVENTS.RESPONSE_HTML_PREVIEW_DISPLAYED;
waitFor(aMonitor.panelWin, RESPONSE_HTML_PREVIEW_DISPLAYED).then(() => {
waitFor(aMonitor.panelWin, EVENTS.RESPONSE_HTML_PREVIEW_DISPLAYED).then(() => {
let iframe = $("#response-preview");
ok(iframe,
"There should be a response preview iframe available.");

View File

@ -0,0 +1,61 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests if image responses show a thumbnail in the requests menu.
*/
function test() {
initNetMonitor(CONTENT_TYPE_WITHOUT_CACHE_URL).then(([aTab, aDebuggee, aMonitor]) => {
info("Starting test... ");
let { $, $all, EVENTS, ACTIVITY_TYPE, NetMonitorView, NetMonitorController } = aMonitor.panelWin;
let { RequestsMenu } = NetMonitorView;
promise.all([
waitForNetworkEvents(aMonitor, 6),
waitFor(aMonitor.panelWin, EVENTS.RESPONSE_IMAGE_THUMBNAIL_DISPLAYED)
]).then(() => {
info("Checking the image thumbnail when all items are shown.");
checkImageThumbnail();
RequestsMenu.sortBy("size");
info("Checking the image thumbnail when all items are sorted.");
checkImageThumbnail();
RequestsMenu.filterOn("images");
info("Checking the image thumbnail when only images are shown.");
checkImageThumbnail();
info("Reloading the debuggee and performing all requests again...");
reloadAndPerformRequests();
return promise.all([
waitForNetworkEvents(aMonitor, 7), // 6 + 1
waitFor(aMonitor.panelWin, EVENTS.RESPONSE_IMAGE_THUMBNAIL_DISPLAYED)
]);
}).then(() => {
info("Checking the image thumbnail after a reload.");
checkImageThumbnail();
teardown(aMonitor).then(finish);
});
function reloadAndPerformRequests() {
NetMonitorController.triggerActivity(ACTIVITY_TYPE.RELOAD.WITH_CACHE_ENABLED).then(() => {
aDebuggee.performRequests();
});
}
function checkImageThumbnail() {
is($all(".requests-menu-icon[type=thumbnail]").length, 1,
"There should be only one image request with a thumbnail displayed.");
is($(".requests-menu-icon[type=thumbnail]").src, TEST_IMAGE_DATA_URI,
"The image requests-menu-icon thumbnail is displayed correctly.");
is($(".requests-menu-icon[type=thumbnail]").hidden, false,
"The image requests-menu-icon thumbnail should not be hidden.");
}
aDebuggee.performRequests();
});
}

View File

@ -0,0 +1,76 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests if image responses show a popup in the requests menu when hovered.
*/
function test() {
initNetMonitor(CONTENT_TYPE_WITHOUT_CACHE_URL).then(([aTab, aDebuggee, aMonitor]) => {
info("Starting test... ");
let { $, EVENTS, ACTIVITY_TYPE, NetMonitorView, NetMonitorController } = aMonitor.panelWin;
let { RequestsMenu } = NetMonitorView;
promise.all([
waitForNetworkEvents(aMonitor, 6),
waitFor(aMonitor.panelWin, EVENTS.RESPONSE_IMAGE_THUMBNAIL_DISPLAYED)
]).then(() => {
info("Checking the image thumbnail after a few requests were made...");
let requestItem = RequestsMenu.items[5];
let requestTooltip = requestItem.attachment.tooltip;
ok(requestTooltip, "There should be a tooltip instance for the image request.");
let anchor = $(".requests-menu-file", requestItem.target);
return showTooltipOn(requestTooltip, anchor);
}).then(aTooltip => {
ok(true,
"An tooltip was successfully opened for the image request.");
is(aTooltip.content.querySelector("image").src, TEST_IMAGE_DATA_URI,
"The tooltip's image content is displayed correctly.");
info("Reloading the debuggee and performing all requests again...");
reloadAndPerformRequests();
return promise.all([
waitForNetworkEvents(aMonitor, 7), // 6 + 1
waitFor(aMonitor.panelWin, EVENTS.RESPONSE_IMAGE_THUMBNAIL_DISPLAYED)
]);
}).then(() => {
info("Checking the image thumbnail after a reload.");
let requestItem = RequestsMenu.items[6];
let requestTooltip = requestItem.attachment.tooltip;
ok(requestTooltip, "There should be a tooltip instance for the image request.");
let anchor = $(".requests-menu-file", requestItem.target);
return showTooltipOn(requestTooltip, anchor);
}).then(aTooltip => {
ok(true,
"An tooltip was successfully opened for the image request.");
is(aTooltip.content.querySelector("image").src, TEST_IMAGE_DATA_URI,
"The tooltip's image content is displayed correctly.");
teardown(aMonitor).then(finish);
});
function reloadAndPerformRequests() {
NetMonitorController.triggerActivity(ACTIVITY_TYPE.RELOAD.WITH_CACHE_ENABLED).then(() => {
aDebuggee.performRequests();
});
}
function showTooltipOn(aTooltip, aTarget) {
let deferred = promise.defer();
aTooltip.panel.addEventListener("popupshown", function onEvent() {
aTooltip.panel.removeEventListener("popupshown", onEvent, true);
deferred.resolve(aTooltip);
}, true);
aTooltip._showOnHover(aTarget);
return deferred.promise;
}
aDebuggee.performRequests();
});
}

View File

@ -40,6 +40,7 @@ const STATUS_CODES_SJS = EXAMPLE_URL + "sjs_status-codes-test-server.sjs";
const SORTING_SJS = EXAMPLE_URL + "sjs_sorting-test-server.sjs";
const TEST_IMAGE = EXAMPLE_URL + "test-image.png";
const TEST_IMAGE_DATA_URI = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==";
// All tests are asynchronous.
waitForExplicitFinish();

View File

@ -399,12 +399,12 @@ Tooltip.prototype = {
_showOnHover: function(target) {
let res = this._targetNodeCb(target, this);
let show = arg => this.show(arg instanceof Ci.nsIDOMNode ? arg : target);
if (res && res.then) {
res.then(() => {
this.show(target);
});
res.then(show);
} else if (res) {
this.show(target);
show(res);
}
},

View File

@ -769,6 +769,13 @@ this.WidgetMethods = {
this.ensureItemIsVisible(this.getItemAtIndex(aIndex));
},
/**
* Sugar for ensuring the selected item is visible in this container.
*/
ensureSelectedItemIsVisible: function() {
this.ensureItemIsVisible(this.selectedItem);
},
/**
* If supported by the widget, the label string temporarily added to this
* container when there are no child items present.
@ -896,6 +903,9 @@ this.WidgetMethods = {
} else if (selectedIndex == j) {
this._widget.selectedItem = aSecond._target;
}
// 6. Let the outside world know that these two items were swapped.
ViewHelpers.dispatchEvent(aFirst.target, "swap", [aSecond, aFirst]);
},
/**

View File

@ -614,7 +614,6 @@ you can use these alternative items. Otherwise, their values should be empty. -
<!ENTITY findAgainCmd.accesskey "g">
<!ENTITY findAgainCmd.commandkey "g">
<!ENTITY findAgainCmd.commandkey2 "VK_F3">
<!ENTITY findSelectionCmd.commandkey "e">
<!ENTITY spellAddDictionaries.label "Add Dictionaries…">
<!ENTITY spellAddDictionaries.accesskey "A">

View File

@ -1176,6 +1176,7 @@ toolbar .toolbarbutton-1:not([type="menu-button"]),
-moz-image-region: rect(0px, 192px, 32px, 160px);
}
#PanelUI-fxa-status > .toolbarbutton-icon,
#PanelUI-quit > .toolbarbutton-icon,
#PanelUI-customize > .toolbarbutton-icon,
#PanelUI-help > .toolbarbutton-icon {

View File

@ -83,6 +83,10 @@
-moz-appearance: none;
}
.customizationmode-button[disabled="true"] {
opacity: .5;
}
#customization-titlebar-visibility-button {
list-style-image: url("chrome://browser/skin/customizableui/customize-titleBar-toggle.png");
-moz-image-region: rect(0, 24px, 24px, 0);
@ -104,7 +108,7 @@
inset 0 1px rgb(196, 196, 196);
}
#customization-undo-reset {
#customization-undo-reset-button {
-moz-margin-end: 10px;
}

View File

@ -126,11 +126,31 @@
font-weight: 600;
}
.requests-menu-file {
.requests-menu-icon-and-file {
width: 20vw;
min-width: 4em;
}
.requests-menu-icon {
background: #fff;
width: calc(1em + 4px);
height: calc(1em + 4px);
margin: -4px 0px;
-moz-margin-end: 4px;
}
.theme-dark .requests-menu-icon {
outline: 1px solid @table_itemDarkStartBorder@;
}
.theme-light .requests-menu-icon {
outline: 1px solid @table_itemLightStartBorder@;
}
.requests-menu-file {
text-align: start;
}
.requests-menu-domain {
width: 14vw;
min-width: 10em;
@ -288,8 +308,6 @@ box.requests-menu-status {
-moz-padding-end: 4px;
background-repeat: repeat-y; /* Background created on a <canvas> in js. */
background-position: -1px center;
margin-top: -1px; /* Compensate borders. */
margin-bottom: -1px;
}
.requests-menu-subitem.requests-menu-waterfall:-moz-locale-dir(rtl) {
@ -771,7 +789,10 @@ box.requests-menu-status {
width: 16vw;
}
.requests-menu-file,
.requests-menu-icon-and-file {
width: 30vw;
}
.requests-menu-domain {
width: 30vw;
}
@ -797,7 +818,7 @@ box.requests-menu-status {
right border and box-shadow of "Domain" column should be hidden. */
}
#network-table[domain-overflows] .requests-menu-file {
#network-table[domain-overflows] .requests-menu-icon-and-file {
border-width: 0 !important;
box-shadow: none !important;
/* The "Domain" header is not visible anymore, and thus the

View File

@ -176,8 +176,14 @@ Files referenced by manifests are automatically installed into the object
directory into paths defined in
:py:func:`mozbuild.frontend.emitter.TreeMetadataEmitter._process_test_manifest`.
Referenced files in the manifest not in the same directory tree as the manifest
file are **not** installed.
Relative paths resolving to parent directory (e.g.
``support-files = ../foo.txt`` have special behavior.
For ``support-files``, the file will be installed to the default destination
for that manifest. Only the file's base name is used to construct the final
path: directories are irrelevant.
For all other entry types, the file installation is skipped.
.. _reftest_manifests:

View File

@ -4,275 +4,30 @@
package org.mozilla.gecko;
import org.mozilla.gecko.prompts.Prompt;
import org.mozilla.gecko.prompts.PromptService;
import org.mozilla.gecko.util.ActivityResultHandler;
import org.mozilla.gecko.util.ActivityResultHandlerMap;
import org.mozilla.gecko.util.ThreadUtils;
import org.mozilla.gecko.util.GeckoEventListener;
import org.json.JSONException;
import org.json.JSONObject;
import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.os.Environment;
import android.provider.MediaStore;
import android.util.Log;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.TimeUnit;
public class ActivityHandlerHelper implements GeckoEventListener {
public class ActivityHandlerHelper {
private static final String LOGTAG = "GeckoActivityHandlerHelper";
private static final ActivityResultHandlerMap mActivityResultHandlerMap = new ActivityResultHandlerMap();
private final ConcurrentLinkedQueue<String> mFilePickerResult;
private final ActivityResultHandlerMap mActivityResultHandlerMap;
private final FilePickerResultHandlerSync mFilePickerResultHandlerSync;
private final CameraImageResultHandler mCameraImageResultHandler;
private final CameraVideoResultHandler mCameraVideoResultHandler;
public interface FileResultHandler {
public void gotFile(String filename);
}
@SuppressWarnings("serial")
public ActivityHandlerHelper() {
mFilePickerResult = new ConcurrentLinkedQueue<String>() {
@Override public boolean offer(String e) {
if (super.offer(e)) {
// poke the Gecko thread in case it's waiting for new events
GeckoAppShell.sendEventToGecko(GeckoEvent.createNoOpEvent());
return true;
}
return false;
}
};
mActivityResultHandlerMap = new ActivityResultHandlerMap();
mFilePickerResultHandlerSync = new FilePickerResultHandlerSync(mFilePickerResult);
mCameraImageResultHandler = new CameraImageResultHandler(mFilePickerResult);
mCameraVideoResultHandler = new CameraVideoResultHandler(mFilePickerResult);
GeckoAppShell.getEventDispatcher().registerEventListener("FilePicker:Show", this);
}
@Override
public void handleMessage(String event, final JSONObject message) {
if (event.equals("FilePicker:Show")) {
String mimeType = "*/*";
String mode = message.optString("mode");
if ("mimeType".equals(mode))
mimeType = message.optString("mimeType");
else if ("extension".equals(mode))
mimeType = GeckoAppShell.getMimeTypeFromExtensions(message.optString("extensions"));
Log.i(LOGTAG, "Mime: " + mimeType);
showFilePickerAsync(GeckoAppShell.getGeckoInterface().getActivity(), mimeType, new FileResultHandler() {
public void gotFile(String filename) {
try {
message.put("file", filename);
} catch (JSONException ex) {
Log.i(LOGTAG, "Can't add filename to message " + filename);
}
GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent(
"FilePicker:Result", message.toString()));
}
});
}
}
public int makeRequestCode(ActivityResultHandler aHandler) {
private static int makeRequestCode(ActivityResultHandler aHandler) {
return mActivityResultHandlerMap.put(aHandler);
}
public void startIntentForActivity (Activity activity, Intent intent, ActivityResultHandler activityResultHandler) {
public static void startIntent(Intent intent, ActivityResultHandler activityResultHandler) {
startIntentForActivity(GeckoAppShell.getGeckoInterface().getActivity(), intent, activityResultHandler);
}
public static void startIntentForActivity(Activity activity, Intent intent, ActivityResultHandler activityResultHandler) {
activity.startActivityForResult(intent, mActivityResultHandlerMap.put(activityResultHandler));
}
private int addIntentActivitiesToList(Context context, Intent intent, ArrayList<Prompt.PromptListItem> items, ArrayList<Intent> aIntents) {
PackageManager pm = context.getPackageManager();
List<ResolveInfo> lri = pm.queryIntentActivityOptions(GeckoAppShell.getGeckoInterface().getActivity().getComponentName(), null, intent, 0);
if (lri == null) {
return 0;
}
for (ResolveInfo ri : lri) {
Intent rintent = new Intent(intent);
rintent.setComponent(new ComponentName(
ri.activityInfo.applicationInfo.packageName,
ri.activityInfo.name));
Prompt.PromptListItem item = new Prompt.PromptListItem(ri.loadLabel(pm).toString());
item.icon = ri.loadIcon(pm);
items.add(item);
aIntents.add(rintent);
}
return lri.size();
}
private int addFilePickingActivities(Context context, ArrayList<Prompt.PromptListItem> aItems, String aType, ArrayList<Intent> aIntents) {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType(aType);
intent.addCategory(Intent.CATEGORY_OPENABLE);
return addIntentActivitiesToList(context, intent, aItems, aIntents);
}
private Prompt.PromptListItem[] getItemsAndIntentsForFilePicker(Context context, String aMimeType, ArrayList<Intent> aIntents) {
ArrayList<Prompt.PromptListItem> items = new ArrayList<Prompt.PromptListItem>();
if (aMimeType.equals("audio/*")) {
if (addFilePickingActivities(context, items, "audio/*", aIntents) <= 0) {
addFilePickingActivities(context, items, "*/*", aIntents);
}
} else if (aMimeType.equals("image/*")) {
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
intent.putExtra(MediaStore.EXTRA_OUTPUT,
Uri.fromFile(new File(Environment.getExternalStorageDirectory(),
CameraImageResultHandler.generateImageName())));
addIntentActivitiesToList(context, intent, items, aIntents);
if (addFilePickingActivities(context, items, "image/*", aIntents) <= 0) {
addFilePickingActivities(context, items, "*/*", aIntents);
}
} else if (aMimeType.equals("video/*")) {
Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
addIntentActivitiesToList(context, intent, items, aIntents);
if (addFilePickingActivities(context, items, "video/*", aIntents) <= 0) {
addFilePickingActivities(context, items, "*/*", aIntents);
}
} else {
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
intent.putExtra(MediaStore.EXTRA_OUTPUT,
Uri.fromFile(new File(Environment.getExternalStorageDirectory(),
CameraImageResultHandler.generateImageName())));
addIntentActivitiesToList(context, intent, items, aIntents);
intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
addIntentActivitiesToList(context, intent, items, aIntents);
addFilePickingActivities(context, items, "*/*", aIntents);
}
return items.toArray(new Prompt.PromptListItem[] {});
}
private String getFilePickerTitle(Context context, String aMimeType) {
if (aMimeType.equals("audio/*")) {
return context.getString(R.string.filepicker_audio_title);
} else if (aMimeType.equals("image/*")) {
return context.getString(R.string.filepicker_image_title);
} else if (aMimeType.equals("video/*")) {
return context.getString(R.string.filepicker_video_title);
} else {
return context.getString(R.string.filepicker_title);
}
}
private interface IntentHandler {
public void gotIntent(Intent intent);
}
/* Gets an intent that can open a particular mimetype. Will show a prompt with a list
* of Activities that can handle the mietype. Asynchronously calls the handler when
* one of the intents is selected. If the caller passes in null for the handler, will still
* prompt for the activity, but will throw away the result.
*/
private void getFilePickerIntentAsync(final Context context, String aMimeType, final IntentHandler handler) {
final ArrayList<Intent> intents = new ArrayList<Intent>();
final Prompt.PromptListItem[] items =
getItemsAndIntentsForFilePicker(context, aMimeType, intents);
if (intents.size() == 0) {
Log.i(LOGTAG, "no activities for the file picker!");
handler.gotIntent(null);
return;
}
if (intents.size() == 1) {
handler.gotIntent(intents.get(0));
return;
}
final Prompt prompt = new Prompt(context, new Prompt.PromptCallback() {
public void onPromptFinished(String promptServiceResult) {
if (handler == null) {
return;
}
int itemId = -1;
try {
itemId = new JSONObject(promptServiceResult).getInt("button");
} catch (JSONException e) {
Log.e(LOGTAG, "result from promptservice was invalid: ", e);
}
if (itemId == -1) {
handler.gotIntent(null);
} else {
handler.gotIntent(intents.get(itemId));
}
}
});
final String title = getFilePickerTitle(context, aMimeType);
// Runnable has to be called to show an intent-like
// context menu UI using the PromptService.
ThreadUtils.postToUiThread(new Runnable() {
@Override public void run() {
prompt.show(title, "", items, false);
}
});
}
/* Allows the user to pick an activity to load files from using a list prompt. Then opens the activity and
* sends the file returned to the passed in handler. If a null handler is passed in, will still
* pick and launch the file picker, but will throw away the result.
*/
public void showFilePickerAsync(final Activity parentActivity, String aMimeType, final FileResultHandler handler) {
getFilePickerIntentAsync(parentActivity, aMimeType, new IntentHandler() {
public void gotIntent(Intent intent) {
if (handler == null) {
return;
}
if (intent == null) {
handler.gotFile("");
return;
}
if (MediaStore.ACTION_IMAGE_CAPTURE.equals(intent.getAction())) {
CameraImageResultHandler cam = new CameraImageResultHandler(handler);
parentActivity.startActivityForResult(intent, mActivityResultHandlerMap.put(cam));
} else if (MediaStore.ACTION_VIDEO_CAPTURE.equals(intent.getAction())) {
CameraVideoResultHandler vid = new CameraVideoResultHandler(handler);
parentActivity.startActivityForResult(intent, mActivityResultHandlerMap.put(vid));
} else if (Intent.ACTION_GET_CONTENT.equals(intent.getAction())) {
FilePickerResultHandlerSync file = new FilePickerResultHandlerSync(handler);
parentActivity.startActivityForResult(intent, mActivityResultHandlerMap.put(file));
} else {
Log.e(LOGTAG, "We should not get an intent with another action!");
handler.gotFile("");
return;
}
}
});
}
boolean handleActivityResult(int requestCode, int resultCode, Intent data) {
public static boolean handleActivityResult(int requestCode, int resultCode, Intent data) {
ActivityResultHandler handler = mActivityResultHandlerMap.getAndRemove(requestCode);
if (handler != null) {
handler.onActivityResult(resultCode, data);

View File

@ -1,67 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko;
import org.mozilla.gecko.util.ActivityResultHandler;
import android.app.Activity;
import android.content.Intent;
import android.os.Environment;
import android.text.format.Time;
import android.util.Log;
import java.io.File;
import java.util.Queue;
class CameraImageResultHandler implements ActivityResultHandler {
private static final String LOGTAG = "GeckoCameraImageResultHandler";
private final Queue<String> mFilePickerResult;
private final ActivityHandlerHelper.FileResultHandler mHandler;
CameraImageResultHandler(Queue<String> resultQueue) {
mFilePickerResult = resultQueue;
mHandler = null;
}
/* Use this constructor to asynchronously listen for results */
public CameraImageResultHandler(ActivityHandlerHelper.FileResultHandler handler) {
mHandler = handler;
mFilePickerResult = null;
}
@Override
public void onActivityResult(int resultCode, Intent data) {
if (resultCode != Activity.RESULT_OK) {
if (mFilePickerResult != null) {
mFilePickerResult.offer("");
}
return;
}
File file = new File(Environment.getExternalStorageDirectory(), sImageName);
sImageName = "";
if (mFilePickerResult != null) {
mFilePickerResult.offer(file.getAbsolutePath());
}
if (mHandler != null) {
mHandler.gotFile(file.getAbsolutePath());
}
}
// this code is really hacky and doesn't belong anywhere so I'm putting it here for now
// until I can come up with a better solution.
private static String sImageName = "";
static String generateImageName() {
Time now = new Time();
now.setToNow();
sImageName = now.format("%Y-%m-%d %H.%M.%S") + ".jpg";
return sImageName;
}
}

View File

@ -1,82 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko;
import org.mozilla.gecko.util.ActivityResultHandler;
import android.app.Activity;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.LoaderManager;
import android.support.v4.app.LoaderManager.LoaderCallbacks;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.content.Intent;
import android.database.Cursor;
import android.os.Bundle;
import android.provider.MediaStore;
import android.util.Log;
import java.util.Queue;
class CameraVideoResultHandler implements ActivityResultHandler {
private static final String LOGTAG = "GeckoCameraVideoResultHandler";
private final Queue<String> mFilePickerResult;
private final ActivityHandlerHelper.FileResultHandler mHandler;
CameraVideoResultHandler(Queue<String> resultQueue) {
mFilePickerResult = resultQueue;
mHandler = null;
}
/* Use this constructor to asynchronously listen for results */
public CameraVideoResultHandler(ActivityHandlerHelper.FileResultHandler handler) {
mFilePickerResult = null;
mHandler = handler;
}
private void sendResult(String res) {
if (mFilePickerResult != null)
mFilePickerResult.offer(res);
if (mHandler != null)
mHandler.gotFile(res);
}
@Override
public void onActivityResult(int resultCode, final Intent data) {
// Intent.getData() can return null. Avoid a crash. See bug 904551.
if (data == null || data.getData() == null || resultCode != Activity.RESULT_OK) {
sendResult("");
return;
}
final FragmentActivity fa = (FragmentActivity) GeckoAppShell.getGeckoInterface().getActivity();
final LoaderManager lm = fa.getSupportLoaderManager();
lm.initLoader(data.hashCode(), null, new LoaderCallbacks<Cursor>() {
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
return new CursorLoader(fa,
data.getData(),
new String[] { MediaStore.Video.Media.DATA },
null, // selection
null, // selectionArgs
null); // sortOrder
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
if (cursor.moveToFirst()) {
sendResult(cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DATA)));
} else {
sendResult("");
}
}
@Override
public void onLoaderReset(Loader<Cursor> loader) { }
});
}
}

View File

@ -0,0 +1,222 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko;
import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.util.ThreadUtils;
import org.mozilla.gecko.util.GeckoEventListener;
import org.json.JSONException;
import org.json.JSONObject;
import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.os.Environment;
import android.os.Parcelable;
import android.provider.MediaStore;
import android.util.Log;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
public class FilePicker implements GeckoEventListener {
private static final String LOGTAG = "GeckoFilePicker";
private static FilePicker sFilePicker;
private final Context mContext;
public interface ResultHandler {
public void gotFile(String filename);
}
public static void init(Context context) {
if (sFilePicker == null) {
sFilePicker = new FilePicker(context);
}
}
protected FilePicker(Context context) {
mContext = context;
GeckoAppShell.getEventDispatcher().registerEventListener("FilePicker:Show", this);
}
@Override
public void handleMessage(String event, final JSONObject message) {
if (event.equals("FilePicker:Show")) {
String mimeType = "*/*";
String mode = message.optString("mode");
if ("mimeType".equals(mode))
mimeType = message.optString("mimeType");
else if ("extension".equals(mode))
mimeType = GeckoAppShell.getMimeTypeFromExtensions(message.optString("extensions"));
showFilePickerAsync(mimeType, new ResultHandler() {
public void gotFile(String filename) {
try {
message.put("file", filename);
} catch (JSONException ex) {
Log.i(LOGTAG, "Can't add filename to message " + filename);
}
GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent(
"FilePicker:Result", message.toString()));
}
});
}
}
private void addActivities(Intent intent, HashMap<String, Intent> intents, HashMap<String, Intent> filters) {
PackageManager pm = mContext.getPackageManager();
List<ResolveInfo> lri = pm.queryIntentActivities(intent, 0);
for (ResolveInfo ri : lri) {
ComponentName cn = new ComponentName(ri.activityInfo.applicationInfo.packageName, ri.activityInfo.name);
if (filters != null && !filters.containsKey(cn.toString())) {
Intent rintent = new Intent(intent);
rintent.setComponent(cn);
intents.put(cn.toString(), rintent);
}
}
}
private Intent getIntent(String mimeType) {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType(mimeType);
intent.addCategory(Intent.CATEGORY_OPENABLE);
return intent;
}
private List<Intent> getIntentsForFilePicker(final String mimeType,
final FilePickerResultHandler fileHandler) {
// The base intent to use for the file picker. Even if this is an implicit intent, Android will
// still show a list of Activitiees that match this action/type.
Intent baseIntent;
// A HashMap of Activities the base intent will show in the chooser. This is used
// to filter activities from other intents so that we don't show duplicates.
HashMap<String, Intent> baseIntents = new HashMap<String, Intent>();
// A list of other activities to shwo in the picker (and the intents to launch them).
HashMap<String, Intent> intents = new HashMap<String, Intent> ();
if ("audio/*".equals(mimeType)) {
// For audio the only intent is the mimetype
baseIntent = getIntent(mimeType);
addActivities(baseIntent, baseIntents, null);
} else if ("image/*".equals(mimeType)) {
// For images the base is a capture intent
baseIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
baseIntent.putExtra(MediaStore.EXTRA_OUTPUT,
Uri.fromFile(new File(Environment.getExternalStorageDirectory(),
fileHandler.generateImageName())));
addActivities(baseIntent, baseIntents, null);
// We also add the mimetype intent
addActivities(getIntent(mimeType), intents, baseIntents);
} else if ("video/*".equals(mimeType)) {
// For videos the base is a capture intent
baseIntent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
addActivities(baseIntent, baseIntents, null);
// We also add the mimetype intent
addActivities(getIntent(mimeType), intents, baseIntents);
} else {
baseIntent = getIntent("*/*");
addActivities(baseIntent, baseIntents, null);
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
intent.putExtra(MediaStore.EXTRA_OUTPUT,
Uri.fromFile(new File(Environment.getExternalStorageDirectory(),
fileHandler.generateImageName())));
addActivities(intent, intents, baseIntents);
intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
addActivities(intent, intents, baseIntents);
}
// If we didn't find any activities, we fall back to the */* mimetype intent
if (baseIntents.size() == 0 && intents.size() == 0) {
intents.clear();
baseIntent = getIntent("*/*");
addActivities(baseIntent, baseIntents, null);
}
ArrayList<Intent> vals = new ArrayList<Intent>(intents.values());
vals.add(0, baseIntent);
return vals;
}
private String getFilePickerTitle(String mimeType) {
if (mimeType.equals("audio/*")) {
return mContext.getString(R.string.filepicker_audio_title);
} else if (mimeType.equals("image/*")) {
return mContext.getString(R.string.filepicker_image_title);
} else if (mimeType.equals("video/*")) {
return mContext.getString(R.string.filepicker_video_title);
} else {
return mContext.getString(R.string.filepicker_title);
}
}
private interface IntentHandler {
public void gotIntent(Intent intent);
}
/* Gets an intent that can open a particular mimetype. Will show a prompt with a list
* of Activities that can handle the mietype. Asynchronously calls the handler when
* one of the intents is selected. If the caller passes in null for the handler, will still
* prompt for the activity, but will throw away the result.
*/
private void getFilePickerIntentAsync(final String mimeType,
final FilePickerResultHandler fileHandler,
final IntentHandler handler) {
List<Intent> intents = getIntentsForFilePicker(mimeType, fileHandler);
if (intents.size() == 0) {
Log.i(LOGTAG, "no activities for the file picker!");
handler.gotIntent(null);
return;
}
Intent base = intents.remove(0);
if (intents.size() == 0) {
handler.gotIntent(base);
return;
}
Intent chooser = Intent.createChooser(base, getFilePickerTitle(mimeType));
chooser.putExtra(Intent.EXTRA_INITIAL_INTENTS, intents.toArray(new Parcelable[]{}));
handler.gotIntent(chooser);
}
/* Allows the user to pick an activity to load files from using a list prompt. Then opens the activity and
* sends the file returned to the passed in handler. If a null handler is passed in, will still
* pick and launch the file picker, but will throw away the result.
*/
protected void showFilePickerAsync(String mimeType, final ResultHandler handler) {
final FilePickerResultHandler fileHandler = new FilePickerResultHandler(handler);
getFilePickerIntentAsync(mimeType, fileHandler, new IntentHandler() {
@Override
public void gotIntent(Intent intent) {
if (handler == null) {
return;
}
if (intent == null) {
handler.gotFile("");
return;
}
ActivityHandlerHelper.startIntent(intent, fileHandler);
}
});
}
}

View File

@ -12,77 +12,201 @@ import android.content.ContentResolver;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.provider.MediaStore;
import android.provider.OpenableColumns;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.LoaderManager;
import android.support.v4.app.LoaderManager.LoaderCallbacks;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.text.format.Time;
import android.util.Log;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.IOException;
import java.util.Queue;
abstract class FilePickerResultHandler implements ActivityResultHandler {
class FilePickerResultHandler implements ActivityResultHandler {
private static final String LOGTAG = "GeckoFilePickerResultHandler";
protected final Queue<String> mFilePickerResult;
protected final ActivityHandlerHelper.FileResultHandler mHandler;
protected final FilePicker.ResultHandler mHandler;
protected FilePickerResultHandler(Queue<String> resultQueue, ActivityHandlerHelper.FileResultHandler handler) {
// this code is really hacky and doesn't belong anywhere so I'm putting it here for now
// until I can come up with a better solution.
private String mImageName = "";
public FilePickerResultHandler(Queue<String> resultQueue) {
mFilePickerResult = resultQueue;
mHandler = null;
}
/* Use this constructor to asynchronously listen for results */
public FilePickerResultHandler(FilePicker.ResultHandler handler) {
mFilePickerResult = null;
mHandler = handler;
}
protected String handleActivityResult(int resultCode, Intent data) {
if (data == null || resultCode != Activity.RESULT_OK)
return "";
Uri uri = data.getData();
if (uri == null)
return "";
private void sendResult(String res) {
if (mFilePickerResult != null)
mFilePickerResult.offer(res);
if (mHandler != null)
mHandler.gotFile(res);
}
@Override
public void onActivityResult(int resultCode, Intent intent) {
if (resultCode != Activity.RESULT_OK) {
sendResult("");
return;
}
// Camera results won't return an Intent. Use the file name we passed to the original intent.
if (intent == null) {
if (mImageName != null) {
File file = new File(Environment.getExternalStorageDirectory(), mImageName);
sendResult(file.getAbsolutePath());
} else {
sendResult("");
}
return;
}
Uri uri = intent.getData();
if (uri == null) {
sendResult("");
return;
}
// Some file pickers may return a file uri
if ("file".equals(uri.getScheme())) {
String path = uri.getPath();
return path == null ? "" : path;
sendResult(path == null ? "" : path);
return;
}
try {
ContentResolver cr = GeckoAppShell.getContext().getContentResolver();
Cursor cursor = cr.query(uri, new String[] { OpenableColumns.DISPLAY_NAME },
null, null, null);
String name = null;
if (cursor != null) {
try {
if (cursor.moveToNext()) {
name = cursor.getString(0);
}
} finally {
cursor.close();
}
}
// tmp filenames must be at least 3 characters long. Add a prefix to make sure that happens
String fileName = "tmp_";
String fileExt = null;
int period;
if (name == null || (period = name.lastIndexOf('.')) == -1) {
String mimeType = cr.getType(uri);
fileExt = "." + GeckoAppShell.getExtensionFromMimeType(mimeType);
} else {
fileExt = name.substring(period);
fileName += name.substring(0, period);
}
Log.i(LOGTAG, "Filename: " + fileName + " . " + fileExt);
File file = File.createTempFile(fileName, fileExt, GeckoLoader.getGREDir(GeckoAppShell.getContext()));
FileOutputStream fos = new FileOutputStream(file);
InputStream is = cr.openInputStream(uri);
byte[] buf = new byte[4096];
int len = is.read(buf);
while (len != -1) {
fos.write(buf, 0, len);
len = is.read(buf);
}
fos.close();
String path = file.getAbsolutePath();
return path == null ? "" : path;
} catch (Exception e) {
Log.e(LOGTAG, "showing file picker", e);
}
return "";
final FragmentActivity fa = (FragmentActivity) GeckoAppShell.getGeckoInterface().getActivity();
final LoaderManager lm = fa.getSupportLoaderManager();
// Finally, Video pickers and some file pickers may return a content provider.
try {
// Try a query to make sure the expected columns exist
final ContentResolver cr = fa.getContentResolver();
Cursor cursor = cr.query(uri, new String[] { "MediaStore.Video.Media.DATA" }, null, null, null);
cursor.close();
lm.initLoader(intent.hashCode(), null, new VideoLoaderCallbacks(uri));
return;
} catch(Exception ex) { }
lm.initLoader(uri.hashCode(), null, new FileLoaderCallbacks(uri));
return;
}
public String generateImageName() {
Time now = new Time();
now.setToNow();
mImageName = now.format("%Y-%m-%d %H.%M.%S") + ".jpg";
return mImageName;
}
private class VideoLoaderCallbacks implements LoaderCallbacks<Cursor> {
final private Uri mUri;
public VideoLoaderCallbacks(Uri uri) {
mUri = uri;
}
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
final FragmentActivity fa = (FragmentActivity) GeckoAppShell.getGeckoInterface().getActivity();
return new CursorLoader(fa,
mUri,
new String[] { "MediaStore.Video.Media.DATA" },
null, // selection
null, // selectionArgs
null); // sortOrder
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
if (cursor.moveToFirst()) {
String res = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DATA));
sendResult(res);
}
}
@Override
public void onLoaderReset(Loader<Cursor> loader) { }
}
private class FileLoaderCallbacks implements LoaderCallbacks<Cursor> {
final private Uri mUri;
public FileLoaderCallbacks(Uri uri) {
mUri = uri;
}
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
final FragmentActivity fa = (FragmentActivity) GeckoAppShell.getGeckoInterface().getActivity();
return new CursorLoader(fa,
mUri,
new String[] { OpenableColumns.DISPLAY_NAME },
null, // selection
null, // selectionArgs
null); // sortOrder
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
if (cursor.moveToFirst()) {
String name = cursor.getString(0);
// tmp filenames must be at least 3 characters long. Add a prefix to make sure that happens
String fileName = "tmp_";
String fileExt = null;
int period;
final FragmentActivity fa = (FragmentActivity) GeckoAppShell.getGeckoInterface().getActivity();
final ContentResolver cr = fa.getContentResolver();
// Generate an extension if we don't already have one
if (name == null || (period = name.lastIndexOf('.')) == -1) {
String mimeType = cr.getType(mUri);
fileExt = "." + GeckoAppShell.getExtensionFromMimeType(mimeType);
} else {
fileExt = name.substring(period);
fileName += name.substring(0, period);
}
// Now write the data to the temp file
try {
File file = File.createTempFile(fileName, fileExt, GeckoLoader.getGREDir(GeckoAppShell.getContext()));
FileOutputStream fos = new FileOutputStream(file);
InputStream is = cr.openInputStream(mUri);
byte[] buf = new byte[4096];
int len = is.read(buf);
while (len != -1) {
fos.write(buf, 0, len);
len = is.read(buf);
}
fos.close();
String path = file.getAbsolutePath();
sendResult((path == null) ? "" : path);
} catch(IOException ex) {
Log.i(LOGTAG, "Error writing file", ex);
}
} else {
sendResult("");
}
}
@Override
public void onLoaderReset(Loader<Cursor> loader) { }
}
}

View File

@ -1,32 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko;
import android.content.Intent;
import android.util.Log;
import java.util.Queue;
class FilePickerResultHandlerSync extends FilePickerResultHandler {
private static final String LOGTAG = "GeckoFilePickerResultHandlerSync";
FilePickerResultHandlerSync(Queue<String> resultQueue) {
super(resultQueue, null);
}
/* Use this constructor to asynchronously listen for results */
public FilePickerResultHandlerSync(ActivityHandlerHelper.FileResultHandler handler) {
super(null, handler);
}
@Override
public void onActivityResult(int resultCode, Intent data) {
if (mFilePickerResult != null)
mFilePickerResult.offer(handleActivityResult(resultCode, data));
if (mHandler != null)
mHandler.gotFile(handleActivityResult(resultCode, data));
}
}

View File

@ -979,14 +979,14 @@ public abstract class GeckoApp
intent.setData(Uri.parse(path));
// Removes the image from storage once the chooser activity ends.
GeckoAppShell.sActivityHelper.startIntentForActivity(this,
Intent.createChooser(intent, sAppContext.getString(R.string.set_image_chooser_title)),
new ActivityResultHandler() {
@Override
public void onActivityResult (int resultCode, Intent data) {
getContentResolver().delete(intent.getData(), null, null);
}
});
ActivityHandlerHelper.startIntentForActivity(this,
Intent.createChooser(intent, sAppContext.getString(R.string.set_image_chooser_title)),
new ActivityResultHandler() {
@Override
public void onActivityResult (int resultCode, Intent data) {
getContentResolver().delete(intent.getData(), null, null);
}
});
} else {
Toast.makeText(sAppContext, R.string.set_image_fail, Toast.LENGTH_SHORT).show();
}
@ -2321,7 +2321,7 @@ public abstract class GeckoApp
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (!GeckoAppShell.sActivityHelper.handleActivityResult(requestCode, resultCode, data)) {
if (!ActivityHandlerHelper.handleActivityResult(requestCode, resultCode, data)) {
super.onActivityResult(requestCode, resultCode, data);
}
}

View File

@ -194,7 +194,6 @@ public class GeckoAppShell
private static volatile boolean mLocationHighAccuracy;
public static ActivityHandlerHelper sActivityHelper = new ActivityHandlerHelper();
static NotificationClient sNotificationClient;
/* The Android-side API: API methods that Android calls */

View File

@ -114,6 +114,7 @@ public class GeckoApplication extends Application {
public void onCreate() {
HardwareUtils.init(getApplicationContext());
Clipboard.init(getApplicationContext());
FilePicker.init(getApplicationContext());
GeckoLoader.loadMozGlue();
super.onCreate();
}

View File

@ -58,6 +58,8 @@ public class HomeBanner extends LinearLayout
@Override
public void onClick(View view) {
HomeBanner.this.setVisibility(View.GONE);
// Send the current message id back to JS.
GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("HomeBanner:Dismiss", (String) getTag()));
}
});

View File

@ -117,7 +117,7 @@ abstract class HomeFragment extends Fragment {
}
final HomeContextMenuInfo info = (HomeContextMenuInfo) menuInfo;
final Context context = getActivity().getApplicationContext();
final Context context = getActivity();
final int itemId = item.getItemId();
if (itemId == R.id.home_share) {
@ -160,7 +160,7 @@ abstract class HomeFragment extends Fragment {
if (itemId == R.id.home_edit_bookmark) {
// UI Dialog associates to the activity context, not the applications'.
new EditBookmarkDialog(getActivity()).show(info.url);
new EditBookmarkDialog(context).show(info.url);
return true;
}

View File

@ -103,8 +103,6 @@ gbjar.sources += [
'AppNotificationClient.java',
'BaseGeckoInterface.java',
'BrowserApp.java',
'CameraImageResultHandler.java',
'CameraVideoResultHandler.java',
'ContactService.java',
'ContextGetter.java',
'CustomEditText.java',
@ -136,8 +134,8 @@ gbjar.sources += [
'favicons/Favicons.java',
'favicons/LoadFaviconTask.java',
'favicons/OnFaviconLoadedListener.java',
'FilePicker.java',
'FilePickerResultHandler.java',
'FilePickerResultHandlerSync.java',
'FindInPageBar.java',
'FormAssistPopup.java',
'GeckoAccessibility.java',

View File

@ -45,7 +45,7 @@ public class ColorPickerInput extends PromptInput {
}
@Override
public String getValue() {
public Object getValue() {
BasicColorPicker cp = (BasicColorPicker) mView.findViewById(R.id.colorpicker);
int color = cp.getColor();
return "#" + Integer.toHexString(color).substring(2);

View File

@ -103,8 +103,8 @@ public class IconGridInput extends PromptInput implements OnItemClickListener {
}
@Override
public String getValue() {
return Integer.toString(mSelected);
public Object getValue() {
return new Integer(mSelected);
}
@Override

View File

@ -160,18 +160,25 @@ public class Prompt implements OnClickListener, OnCancelListener, OnItemClickLis
/* Adds to a result value from the lists that can be shown in dialogs.
* Will set the selected value(s) to the button attribute of the
* object that's passed in. If this is a multi-select dialog, can set
* the button attribute to an array.
* object that's passed in. If this is a multi-select dialog, sets a
* selected attribute to an array of booleans.
*/
private void addListResult(final JSONObject result, int which) {
try {
if (mSelected != null) {
JSONArray selected = new JSONArray();
for (int i = 0; i < mSelected.length; i++) {
selected.put(mSelected[i]);
if (mSelected[i]) {
selected.put(i);
}
}
result.put("button", selected);
result.put("list", selected);
} else {
// Mirror the selected array from multi choice for consistency.
JSONArray selected = new JSONArray();
selected.put(which);
result.put("list", selected);
// Make the button be the index of the select item.
result.put("button", which);
}
} catch(JSONException ex) { }
@ -212,12 +219,12 @@ public class Prompt implements OnClickListener, OnCancelListener, OnItemClickLis
JSONObject ret = new JSONObject();
try {
ListView list = mDialog.getListView();
addButtonResult(ret, which);
addInputValues(ret);
if (list != null || mSelected != null) {
addListResult(ret, which);
} else {
addButtonResult(ret, which);
}
addInputValues(ret);
} catch(Exception ex) {
Log.i(LOGTAG, "Error building return: " + ex);
}

View File

@ -83,10 +83,10 @@ public class PromptInput {
mView = (View)input;
return mView;
}
public String getValue() {
@Override
public Object getValue() {
EditText edit = (EditText)mView;
return edit.getText().toString();
return edit.getText();
}
}
@ -118,10 +118,10 @@ public class PromptInput {
InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
return input;
}
public String getValue() {
@Override
public Object getValue() {
EditText edit = (EditText)mView;
return edit.getText().toString();
return edit.getText();
}
}
@ -142,10 +142,10 @@ public class PromptInput {
mView = (View)checkbox;
return mView;
}
public String getValue() {
@Override
public Object getValue() {
CheckBox checkbox = (CheckBox)mView;
return checkbox.isChecked() ? "true" : "false";
return checkbox.isChecked() ? Boolean.TRUE : Boolean.FALSE;
}
}
@ -220,8 +220,8 @@ public class PromptInput {
private static String formatDateString(String dateFormat, Calendar calendar) {
return new SimpleDateFormat(dateFormat).format(calendar.getTime());
}
public String getValue() {
@Override
public Object getValue() {
if (Build.VERSION.SDK_INT < 11 && mType.equals("date")) {
// We can't use the custom DateTimePicker with a sdk older than 11.
// Fallback on the native DatePicker.
@ -300,9 +300,9 @@ public class PromptInput {
return spinner;
}
public String getValue() {
return Integer.toString(spinner.getSelectedItemPosition());
@Override
public Object getValue() {
return new Integer(spinner.getSelectedItemPosition());
}
}
@ -319,8 +319,8 @@ public class PromptInput {
mView = view;
return mView;
}
public String getValue() {
@Override
public Object getValue() {
return "";
}
}
@ -369,7 +369,7 @@ public class PromptInput {
return mId;
}
public String getValue() {
public Object getValue() {
return "";
}

View File

@ -17,6 +17,7 @@ import com.jayway.android.robotium.solo.Solo;
import android.support.v4.view.PagerAdapter;
import android.support.v4.view.ViewPager;
import android.view.View;
import android.widget.TextView;
/**
* A class representing any interactions that take place on the Awesomescreen.
@ -62,6 +63,10 @@ public class AboutHomeComponent extends BaseComponent {
return (ViewPager) mSolo.getView(R.id.home_pager);
}
private View getHomeBannerView() {
return mSolo.getView(R.id.home_banner);
}
public AboutHomeComponent assertCurrentPanel(final PanelType expectedPanel) {
assertVisible();
@ -83,6 +88,43 @@ public class AboutHomeComponent extends BaseComponent {
return this;
}
public AboutHomeComponent assertBannerNotVisible() {
assertFalse("The HomeBanner is not visible",
getHomeBannerView().getVisibility() == View.VISIBLE);
return this;
}
public AboutHomeComponent assertBannerVisible() {
assertEquals("The HomeBanner is visible",
View.VISIBLE, getHomeBannerView().getVisibility());
return this;
}
public AboutHomeComponent assertBannerText(String text) {
assertBannerVisible();
final TextView textView = (TextView) getHomeBannerView().findViewById(R.id.text);
assertEquals("The correct HomeBanner text is shown",
text, textView.getText().toString());
return this;
}
public AboutHomeComponent clickOnBanner() {
assertBannerVisible();
mTestContext.dumpLog(LOGTAG, "Clicking on HomeBanner.");
mSolo.clickOnView(getHomeBannerView());
return this;
}
public AboutHomeComponent dismissBanner() {
assertBannerVisible();
mTestContext.dumpLog(LOGTAG, "Clicking on HomeBanner close button.");
mSolo.clickOnView(getHomeBannerView().findViewById(R.id.close));
return this;
}
public AboutHomeComponent swipeToPanelOnRight() {
mTestContext.dumpLog(LOGTAG, "Swiping to the panel on the right.");
swipeToPanel(Solo.RIGHT);

View File

@ -27,13 +27,25 @@ public final class GeckoHelper {
}
public static void blockForReady() {
final EventExpecter geckoReady = sActions.expectGeckoEvent("Gecko:Ready");
blockForEvent("Gecko:Ready");
}
final boolean isReady = GeckoThread.checkLaunchState(LaunchState.GeckoRunning);
if (!isReady) {
geckoReady.blockForEvent();
/**
* Blocks for the "Gecko:DelayedStartup" event, which occurs after "Gecko:Ready" and the
* first page load.
*/
public static void blockForDelayedStartup() {
blockForEvent("Gecko:DelayedStartup");
}
private static void blockForEvent(final String eventName) {
final EventExpecter eventExpecter = sActions.expectGeckoEvent(eventName);
final boolean isRunning = GeckoThread.checkLaunchState(LaunchState.GeckoRunning);
if (!isRunning) {
eventExpecter.blockForEvent();
}
geckoReady.unregisterListener();
eventExpecter.unregisterListener();
}
}

View File

@ -49,11 +49,11 @@ final public class NavigationHelper {
private static String adjustUrl(final String url) {
assertNotNull("url is not null", url);
if (!url.startsWith("about:")) {
return sContext.getAbsoluteHostnameUrl(url);
if (url.startsWith("about:") || url.startsWith("chrome:")) {
return url;
}
return url;
return sContext.getAbsoluteHostnameUrl(url);
}
public static void goBack() {

View File

@ -6,6 +6,7 @@
</head>
<body>
<div style='float: left; width: 100%; height: 500px; margin: 0; padding: 0; border: none; background-color: rgb(250,0,0)'> </div>
<div style='float: left; width: 10%; height: 500px; margin: 0; padding: 0; border: none; background-color: rgb(250,0,0)'> </div>
<p>Text taken from Wikipedia.org</p>
<p> <b>Will be searching for this string:</b> Robocop </p>
<p>Mozilla is a free software community best known for producing the Firefox web browser. The Mozilla community uses, develops, spreads and supports Mozilla products and works to advance the goals of the Open Web described in the Mozilla Manifesto.[1] The community is supported institutionally by the Mozilla Foundation and its tax-paying subsidiary, the Mozilla Corporation.[2] </p>

View File

@ -15,19 +15,29 @@ function start() {
window[test]();
}
var messageId;
function addMessage() {
Home.banner.add({
messageId = Home.banner.add({
text: TEXT,
onclick: function() {
sendMessageToJava({ type: "TestHomeBanner:MessageClicked" });
},
onshown: function() {
sendMessageToJava({ type: "TestHomeBanner:MessageShown" });
},
ondismiss: function() {
sendMessageToJava({ type: "TestHomeBanner:MessageDismissed" });
}
});
sendMessageToJava({ type: "TestHomeBanner:MessageAdded" });
}
function removeMessage() {
Home.banner.remove(messageId);
sendMessageToJava({ type: "TestHomeBanner:MessageRemoved" });
}
</script>
</head>
<body onload="start();">

View File

@ -13,7 +13,7 @@ public class testAboutHomePageNavigation extends UITest {
// enum for both phone and tablet, then swiping through the panels. This will also
// benefit having a HomePager with custom panels.
public void testAboutHomePageNavigation() {
GeckoHelper.blockForReady();
GeckoHelper.blockForDelayedStartup();
mAboutHome.assertVisible()
.assertCurrentPanel(PanelType.TOP_SITES);

View File

@ -18,9 +18,12 @@ public class testFindInPage extends PixelTest {
blockForGeckoReady();
String url = getAbsoluteUrl("/robocop/robocop_text_page.html");
loadAndPaint(url);
height = mDriver.getGeckoHeight()/8;
width = mDriver.getGeckoWidth()/2;
// Select the upper left corner of the screen
height = mDriver.getGeckoHeight()/8;
width = mDriver.getGeckoWidth()/8;
/* Disabled by bug 958111.
// Search that does not find the term and therefor should not pan the page
Actions.RepeatedEventExpecter paintExpecter = mActions.expectPaint();
findText("Robocoop", 3); // This will be close enough to existing text to test that search finds just what it should
@ -31,11 +34,12 @@ public class testFindInPage extends PixelTest {
} finally {
painted.close();
}
*/
// Search that finds matches and therefor pans the page
paintExpecter = mActions.expectPaint();
Actions.RepeatedEventExpecter paintExpecter = mActions.expectPaint();
findText("Robocop", 3);
painted = waitForPaint(paintExpecter);
PaintedSurface painted = waitForPaint(paintExpecter);
paintExpecter.unregisterListener();
try {
mAsserter.isnotpixel(painted.getPixelAt(width,height), 255, 0, 0, "Pixel at " + String.valueOf(width) + "," + String.valueOf(height));

View File

@ -1,37 +1,116 @@
package org.mozilla.gecko.tests;
import org.mozilla.gecko.*;
import static org.mozilla.gecko.tests.helpers.AssertionHelper.*;
public class testHomeBanner extends BaseTest {
import org.mozilla.gecko.Actions;
import org.mozilla.gecko.R;
import org.mozilla.gecko.tests.helpers.*;
import android.view.View;
public class testHomeBanner extends UITest {
private static final String TEST_URL = "chrome://roboextender/content/robocop_home_banner.html";
private static final String TEXT = "The quick brown fox jumps over the lazy dog.";
@Override
protected int getTestType() {
return TEST_MOCHITEST;
public void testHomeBanner() {
GeckoHelper.blockForReady();
// Make sure the banner is not visible to start.
mAboutHome.assertVisible()
.assertBannerNotVisible();
// These test methods depend on being run in this order.
addBannerTest();
removeBannerTest();
// Make sure to test dismissing the banner after everything else, since dismissing
// the banner will prevent it from showing up again.
dismissBannerTest();
}
public void testHomeBanner() {
blockForGeckoReady();
/**
* Adds a banner message, verifies that it appears when it should, and verifies that
* onshown/onclick handlers are called in JS.
*
* Note: This test does not remove the message after it is done.
*/
private void addBannerTest() {
addBannerMessage();
Actions.EventExpecter eventExpecter = mActions.expectGeckoEvent("TestHomeBanner:MessageAdded");
// Add a message to the home banner
inputAndLoadUrl(TEST_URL + "#addMessage");
// Load about:home again, and make sure the onshown handler is called.
Actions.EventExpecter eventExpecter = getActions().expectGeckoEvent("TestHomeBanner:MessageShown");
NavigationHelper.enterAndLoadUrl("about:home");
eventExpecter.blockForEvent();
// Load about:home, and test to make sure the onshown handler is called
eventExpecter = mActions.expectGeckoEvent("TestHomeBanner:MessageShown");
inputAndLoadUrl("about:home");
// Verify that the banner is visible with the correct text.
mAboutHome.assertBannerText(TEXT);
// Test to make sure the onclick handler is called.
eventExpecter = getActions().expectGeckoEvent("TestHomeBanner:MessageClicked");
mAboutHome.clickOnBanner();
eventExpecter.blockForEvent();
// Verify that the correct message text showed up in the banner
mAsserter.ok(waitForText(TEXT), "banner text", "correct text appeared in the home banner");
// Verify that the banner isn't visible after navigating away from about:home.
NavigationHelper.enterAndLoadUrl("about:firefox");
// Test to make sure the onclick handler is called
eventExpecter = mActions.expectGeckoEvent("TestHomeBanner:MessageClicked");
mSolo.clickOnText(TEXT);
// AboutHomeComponent calls mSolo.getView, which will fail an assertion if the
// view is not present, so we need to use findViewById in this case.
final View banner = getActivity().findViewById(R.id.home_banner);
assertTrue("The HomeBanner is not visible", banner == null || banner.getVisibility() != View.VISIBLE);
}
/**
* Removes a banner message, and verifies that it no longer appears on about:home.
*
* Note: This test expects for a message to have been added before it runs.
*/
private void removeBannerTest() {
removeBannerMessage();
// Verify that the banner no longer appears.
NavigationHelper.enterAndLoadUrl("about:home");
mAboutHome.assertVisible()
.assertBannerNotVisible();
}
/**
* Adds a banner message, verifies that its ondismiss handler is called in JS,
* and verifies that the banner is no longer shown after it is dismissed.
*
* Note: This test does not remove the message after it is done.
*/
private void dismissBannerTest() {
// Add back the banner message to test the dismiss functionality.
addBannerMessage();
NavigationHelper.enterAndLoadUrl("about:home");
mAboutHome.assertVisible();
// Test to make sure the ondismiss handler is called when the close button is clicked.
final Actions.EventExpecter eventExpecter = getActions().expectGeckoEvent("TestHomeBanner:MessageDismissed");
mAboutHome.dismissBanner();
eventExpecter.blockForEvent();
mAboutHome.assertBannerNotVisible();
}
/**
* Loads the roboextender page to add a message to the banner.
*/
private void addBannerMessage() {
final Actions.EventExpecter eventExpecter = getActions().expectGeckoEvent("TestHomeBanner:MessageAdded");
NavigationHelper.enterAndLoadUrl(TEST_URL + "#addMessage");
eventExpecter.blockForEvent();
}
/**
* Loads the roboextender page to remove the message from the banner.
*/
private void removeBannerMessage() {
final Actions.EventExpecter eventExpecter = getActions().expectGeckoEvent("TestHomeBanner:MessageRemoved");
NavigationHelper.enterAndLoadUrl(TEST_URL + "#removeMessage");
eventExpecter.blockForEvent();
}
}

View File

@ -5,6 +5,7 @@
package org.mozilla.gecko.webapp;
import org.mozilla.gecko.ActivityHandlerHelper;
import org.mozilla.gecko.AppConstants;
import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.GeckoProfile;
@ -223,7 +224,7 @@ public class EventListener implements GeckoEventListener {
intent.setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive");
// Now call the package installer.
GeckoAppShell.sActivityHelper.startIntentForActivity(context, intent, new ActivityResultHandler() {
ActivityHandlerHelper.startIntentForActivity(context, intent, new ActivityResultHandler() {
@Override
public void onActivityResult(int resultCode, Intent data) {
// The InstallListener will catch the case where the user pressed install.

View File

@ -77,6 +77,7 @@ var FindHelper = {
this._targetTab.sendViewportUpdate();
}
} else {
ZoomHelper.zoomToRect(aData.rect, -1, false, true);
this._viewportChanged = true;
}
}

View File

@ -37,32 +37,25 @@ var SelectHelper = {
}
p.show((function(data) {
let selected = data.button;
if (selected == -1)
return;
let selected = data.list;
if (aElement instanceof Ci.nsIDOMXULMenuListElement) {
if (aElement.selectedIndex != selected) {
aElement.selectedIndex = selected;
if (aElement.selectedIndex != selected[0]) {
aElement.selectedIndex = selected[0];
this.fireOnCommand(aElement);
}
} else if (aElement instanceof HTMLSelectElement) {
let changed = false;
if (!Array.isArray(selected)) {
let temp = [];
for (let i = 0; i <= list.length; i++) {
temp[i] = (i == selected);
}
selected = temp;
}
let i = 0;
this.forOptions(aElement, function(aNode) {
if (aNode.selected != selected[i]) {
if (aNode.selected && selected.indexOf(i) == -1) {
changed = true;
aNode.selected = selected[i];
aNode.selected = false;
} else if (!aNode.selected && selected.indexOf(i) != -1) {
changed = true;
aNode.selected = true;
}
i++
i++;
});
if (changed)

View File

@ -0,0 +1,148 @@
/* 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/. */
"use strict";
var ZoomHelper = {
zoomInAndSnapToRange: function(aRange) {
// aRange is always non-null here, since a check happened previously.
let viewport = BrowserApp.selectedTab.getViewport();
let fudge = 15; // Add a bit of fudge.
let boundingElement = aRange.offsetNode;
while (!boundingElement.getBoundingClientRect && boundingElement.parentNode) {
boundingElement = boundingElement.parentNode;
}
let rect = ElementTouchHelper.getBoundingContentRect(boundingElement);
let drRect = aRange.getClientRect();
let scrollTop =
BrowserApp.selectedBrowser.contentDocument.documentElement.scrollTop ||
BrowserApp.selectedBrowser.contentDocument.body.scrollTop;
// We subtract half the height of the viewport so that we can (ideally)
// center the area of interest on the screen.
let topPos = scrollTop + drRect.top - (viewport.cssHeight / 2.0);
// Factor in the border and padding
let boundingStyle = window.getComputedStyle(boundingElement);
let leftAdjustment = parseInt(boundingStyle.paddingLeft) +
parseInt(boundingStyle.borderLeftWidth);
BrowserApp.selectedTab._mReflozPositioned = true;
rect.type = "Browser:ZoomToRect";
rect.x = Math.max(viewport.cssPageLeft, rect.x - fudge + leftAdjustment);
rect.y = Math.max(topPos, viewport.cssPageTop);
rect.w = viewport.cssWidth;
rect.h = viewport.cssHeight;
rect.animate = false;
sendMessageToJava(rect);
BrowserApp.selectedTab._mReflozPoint = null;
},
zoomOut: function() {
BrowserEventHandler.resetMaxLineBoxWidth();
sendMessageToJava({ type: "Browser:ZoomToPageWidth" });
},
isRectZoomedIn: function(aRect, aViewport) {
// This function checks to see if the area of the rect visible in the
// viewport (i.e. the "overlapArea" variable below) is approximately
// the max area of the rect we can show. It also checks that the rect
// is actually on-screen by testing the left and right edges of the rect.
// In effect, this tells us whether or not zooming in to this rect
// will significantly change what the user is seeing.
const minDifference = -20;
const maxDifference = 20;
const maxZoomAllowed = 4; // keep this in sync with mobile/android/base/ui/PanZoomController.MAX_ZOOM
let vRect = new Rect(aViewport.cssX, aViewport.cssY, aViewport.cssWidth, aViewport.cssHeight);
let overlap = vRect.intersect(aRect);
let overlapArea = overlap.width * overlap.height;
let availHeight = Math.min(aRect.width * vRect.height / vRect.width, aRect.height);
let showing = overlapArea / (aRect.width * availHeight);
let dw = (aRect.width - vRect.width);
let dx = (aRect.x - vRect.x);
if (fuzzyEquals(aViewport.zoom, maxZoomAllowed) && overlap.width / aRect.width > 0.9) {
// we're already at the max zoom and the block is not spilling off the side of the screen so that even
// if the block isn't taking up most of the viewport we can't pan/zoom in any more. return true so that we zoom out
return true;
}
return (showing > 0.9 &&
dx > minDifference && dx < maxDifference &&
dw > minDifference && dw < maxDifference);
},
/* Zoom to an element, optionally keeping a particular part of it
* in view if it is really tall.
*/
zoomToElement: function(aElement, aClickY = -1, aCanZoomOut = true, aCanScrollHorizontally = true) {
let rect = ElementTouchHelper.getBoundingContentRect(aElement);
ZoomHelper.zoomToRect(rect, aClickY, aCanZoomOut, aCanScrollHorizontally);
},
zoomToRect: function(aRect, aClickY = -1, aCanZoomOut = true, aCanScrollHorizontally = true) {
const margin = 15;
if(!aRect.h || !aRect.w) {
aRect.h = aRect.height;
aRect.w = aRect.width;
}
let viewport = BrowserApp.selectedTab.getViewport();
let bRect = new Rect(aCanScrollHorizontally ? Math.max(viewport.cssPageLeft, aRect.x - margin) : viewport.cssX,
aRect.y,
aCanScrollHorizontally ? aRect.w + 2 * margin : viewport.cssWidth,
aRect.h);
// constrict the rect to the screen's right edge
bRect.width = Math.min(bRect.width, viewport.cssPageRight - bRect.x);
// if the rect is already taking up most of the visible area and is stretching the
// width of the page, then we want to zoom out instead.
if (BrowserEventHandler.mReflozPref) {
let zoomFactor = BrowserApp.selectedTab.getZoomToMinFontSize(aElement);
bRect.width = zoomFactor <= 1.0 ? bRect.width : gScreenWidth / zoomFactor;
bRect.height = zoomFactor <= 1.0 ? bRect.height : bRect.height / zoomFactor;
if (zoomFactor == 1.0 || ZoomHelper.isRectZoomedIn(bRect, viewport)) {
if (aCanZoomOut) {
ZoomHelper.zoomOut();
}
return;
}
} else if (ZoomHelper.isRectZoomedIn(bRect, viewport)) {
if (aCanZoomOut) {
ZoomHelper.zoomOut();
}
return;
}
let rect = {};
rect.type = "Browser:ZoomToRect";
rect.x = bRect.x;
rect.y = bRect.y;
rect.w = bRect.width;
rect.h = Math.min(bRect.width * viewport.cssHeight / viewport.cssWidth, bRect.height);
if (aClickY >= 0) {
// if the block we're zooming to is really tall, and we want to keep a particular
// part of it in view, then adjust the y-coordinate of the target rect accordingly.
// the 1.2 multiplier is just a little fuzz to compensate for bRect including horizontal
// margins but not vertical ones.
let cssTapY = viewport.cssY + aClickY;
if ((bRect.height > rect.h) && (cssTapY > rect.y + (rect.h * 1.2))) {
rect.y = cssTapY - (rect.h / 2);
}
}
if (rect.w > viewport.cssWidth || rect.h > viewport.cssHeight) {
BrowserEventHandler.resetMaxLineBoxWidth();
}
sendMessageToJava(rect);
},
};

View File

@ -91,6 +91,7 @@ XPCOMUtils.defineLazyModuleGetter(this, "WebappManager",
["PluginHelper", "chrome://browser/content/PluginHelper.js"],
["OfflineApps", "chrome://browser/content/OfflineApps.js"],
["Linkifier", "chrome://browser/content/Linkify.js"],
["ZoomHelper", "chrome://browser/content/ZoomHelper.js"],
["CastingApps", "chrome://browser/content/CastingApps.js"],
].forEach(function (aScript) {
let [name, script] = aScript;
@ -188,7 +189,7 @@ function doChangeMaxLineBoxWidth(aWidth) {
docViewer.changeMaxLineBoxWidth(aWidth);
if (range) {
BrowserEventHandler._zoomInAndSnapToRange(range);
ZoomHelper.zoomInAndSnapToRange(range);
} else {
// In this case, we actually didn't zoom into a specific range. It
// probably happened from a page load reflow-on-zoom event, so we
@ -1376,8 +1377,8 @@ var BrowserApp = {
let shouldZoom = Services.prefs.getBoolPref("formhelper.autozoom");
if (formHelperMode == kFormHelperModeDynamic && this.isTablet)
shouldZoom = false;
// _zoomToElement will handle not sending any message if this input is already mostly filling the screen
BrowserEventHandler._zoomToElement(focused, -1, false,
// ZoomHelper.zoomToElement will handle not sending any message if this input is already mostly filling the screen
ZoomHelper.zoomToElement(focused, -1, false,
aAllowZoom && shouldZoom && !ViewportHandler.getViewportMetadata(aBrowser.contentWindow).isSpecified);
}
},
@ -2929,16 +2930,16 @@ Tab.prototype = {
* 2a. PerformReflowOnZoom() and resetMaxLineBoxWidth() schedule a call to
* doChangeMaxLineBoxWidth, based on a timeout specified in preferences.
* 3. doChangeMaxLineBoxWidth changes the line box width (which also
* schedules a reflow event), and then calls _zoomInAndSnapToRange.
* 4. _zoomInAndSnapToRange performs the positioning of reflow-on-zoom and
* then re-enables painting.
* schedules a reflow event), and then calls ZoomHelper.zoomInAndSnapToRange.
* 4. ZoomHelper.zoomInAndSnapToRange performs the positioning of reflow-on-zoom
* and then re-enables painting.
*
* Some of the events happen synchronously, while others happen asynchronously.
* The following is a rough sketch of the progression of events:
*
* double tap event seen -> onDoubleTap() -> ... asynchronous ...
* -> setViewport() -> performReflowOnZoom() -> ... asynchronous ...
* -> doChangeMaxLineBoxWidth() -> _zoomInAndSnapToRange()
* -> doChangeMaxLineBoxWidth() -> ZoomHelper.zoomInAndSnapToRange()
* -> ... asynchronous ... -> setViewport() -> Observe('after-viewport-change')
* -> resumePainting()
*/
@ -4578,41 +4579,6 @@ var BrowserEventHandler = {
}
},
_zoomOut: function() {
BrowserEventHandler.resetMaxLineBoxWidth();
sendMessageToJava({ type: "Browser:ZoomToPageWidth" });
},
_isRectZoomedIn: function(aRect, aViewport) {
// This function checks to see if the area of the rect visible in the
// viewport (i.e. the "overlapArea" variable below) is approximately
// the max area of the rect we can show. It also checks that the rect
// is actually on-screen by testing the left and right edges of the rect.
// In effect, this tells us whether or not zooming in to this rect
// will significantly change what the user is seeing.
const minDifference = -20;
const maxDifference = 20;
const maxZoomAllowed = 4; // keep this in sync with mobile/android/base/ui/PanZoomController.MAX_ZOOM
let vRect = new Rect(aViewport.cssX, aViewport.cssY, aViewport.cssWidth, aViewport.cssHeight);
let overlap = vRect.intersect(aRect);
let overlapArea = overlap.width * overlap.height;
let availHeight = Math.min(aRect.width * vRect.height / vRect.width, aRect.height);
let showing = overlapArea / (aRect.width * availHeight);
let dw = (aRect.width - vRect.width);
let dx = (aRect.x - vRect.x);
if (fuzzyEquals(aViewport.zoom, maxZoomAllowed) && overlap.width / aRect.width > 0.9) {
// we're already at the max zoom and the block is not spilling off the side of the screen so that even
// if the block isn't taking up most of the viewport we can't pan/zoom in any more. return true so that we zoom out
return true;
}
return (showing > 0.9 &&
dx > minDifference && dx < maxDifference &&
dw > minDifference && dw < maxDifference);
},
onDoubleTap: function(aData) {
let data = JSON.parse(aData);
let element = ElementTouchHelper.anyElementFromPoint(data.x, data.y);
@ -4643,7 +4609,7 @@ var BrowserEventHandler = {
}
if (!element) {
this._zoomOut();
ZoomHelper.zoomOut();
return;
}
@ -4651,9 +4617,9 @@ var BrowserEventHandler = {
element = element.parentNode;
if (!element) {
this._zoomOut();
ZoomHelper.zoomOut();
} else {
this._zoomToElement(element, data.y);
ZoomHelper.zoomToElement(element, data.y);
}
},
@ -4679,102 +4645,6 @@ var BrowserEventHandler = {
return false;
},
/* Zoom to an element, optionally keeping a particular part of it
* in view if it is really tall.
*/
_zoomToElement: function(aElement, aClickY = -1, aCanZoomOut = true, aCanScrollHorizontally = true) {
const margin = 15;
let rect = ElementTouchHelper.getBoundingContentRect(aElement);
let viewport = BrowserApp.selectedTab.getViewport();
let bRect = new Rect(aCanScrollHorizontally ? Math.max(viewport.cssPageLeft, rect.x - margin) : viewport.cssX,
rect.y,
aCanScrollHorizontally ? rect.w + 2 * margin : viewport.cssWidth,
rect.h);
// constrict the rect to the screen's right edge
bRect.width = Math.min(bRect.width, viewport.cssPageRight - bRect.x);
// if the rect is already taking up most of the visible area and is stretching the
// width of the page, then we want to zoom out instead.
if (BrowserEventHandler.mReflozPref) {
let zoomFactor = BrowserApp.selectedTab.getZoomToMinFontSize(aElement);
bRect.width = zoomFactor <= 1.0 ? bRect.width : gScreenWidth / zoomFactor;
bRect.height = zoomFactor <= 1.0 ? bRect.height : bRect.height / zoomFactor;
if (zoomFactor == 1.0 || this._isRectZoomedIn(bRect, viewport)) {
if (aCanZoomOut) {
this._zoomOut();
}
return;
}
} else if (this._isRectZoomedIn(bRect, viewport)) {
if (aCanZoomOut) {
this._zoomOut();
}
return;
}
rect.type = "Browser:ZoomToRect";
rect.x = bRect.x;
rect.y = bRect.y;
rect.w = bRect.width;
rect.h = Math.min(bRect.width * viewport.cssHeight / viewport.cssWidth, bRect.height);
if (aClickY >= 0) {
// if the block we're zooming to is really tall, and we want to keep a particular
// part of it in view, then adjust the y-coordinate of the target rect accordingly.
// the 1.2 multiplier is just a little fuzz to compensate for bRect including horizontal
// margins but not vertical ones.
let cssTapY = viewport.cssY + aClickY;
if ((bRect.height > rect.h) && (cssTapY > rect.y + (rect.h * 1.2))) {
rect.y = cssTapY - (rect.h / 2);
}
}
if (rect.w > viewport.cssWidth || rect.h > viewport.cssHeight) {
BrowserEventHandler.resetMaxLineBoxWidth();
}
sendMessageToJava(rect);
},
_zoomInAndSnapToRange: function(aRange) {
// aRange is always non-null here, since a check happened previously.
let viewport = BrowserApp.selectedTab.getViewport();
let fudge = 15; // Add a bit of fudge.
let boundingElement = aRange.offsetNode;
while (!boundingElement.getBoundingClientRect && boundingElement.parentNode) {
boundingElement = boundingElement.parentNode;
}
let rect = ElementTouchHelper.getBoundingContentRect(boundingElement);
let drRect = aRange.getClientRect();
let scrollTop =
BrowserApp.selectedBrowser.contentDocument.documentElement.scrollTop ||
BrowserApp.selectedBrowser.contentDocument.body.scrollTop;
// We subtract half the height of the viewport so that we can (ideally)
// center the area of interest on the screen.
let topPos = scrollTop + drRect.top - (viewport.cssHeight / 2.0);
// Factor in the border and padding
let boundingStyle = window.getComputedStyle(boundingElement);
let leftAdjustment = parseInt(boundingStyle.paddingLeft) +
parseInt(boundingStyle.borderLeftWidth);
BrowserApp.selectedTab._mReflozPositioned = true;
rect.type = "Browser:ZoomToRect";
rect.x = Math.max(viewport.cssPageLeft, rect.x - fudge + leftAdjustment);
rect.y = Math.max(topPos, viewport.cssPageTop);
rect.w = viewport.cssWidth;
rect.h = viewport.cssHeight;
rect.animate = false;
sendMessageToJava(rect);
BrowserApp.selectedTab._mReflozPoint = null;
},
onPinchFinish: function(aData) {
let data = {};
try {

View File

@ -52,6 +52,7 @@ chrome.jar:
content/FeedHandler.js (content/FeedHandler.js)
content/Feedback.js (content/Feedback.js)
content/Linkify.js (content/Linkify.js)
content/ZoomHelper.js (content/ZoomHelper.js)
content/CastingApps.js (content/CastingApps.js)
#ifdef MOZ_SERVICES_HEALTHREPORT
content/aboutHealthReport.xhtml (content/aboutHealthReport.xhtml)

View File

@ -43,6 +43,9 @@ function BannerMessage(options) {
if ("onclick" in options && typeof options.onclick === "function")
this.onclick = options.onclick;
if ("ondismiss" in options && typeof options.ondismiss === "function")
this.ondismiss = options.ondismiss;
}
let HomeBanner = (function () {
@ -77,6 +80,12 @@ let HomeBanner = (function () {
message.onclick();
};
let _handleDismiss = function(id) {
let message = _messages[id];
if (message.ondismiss)
message.ondismiss();
};
return Object.freeze({
observe: function(subject, topic, data) {
switch(topic) {
@ -87,6 +96,10 @@ let HomeBanner = (function () {
case "HomeBanner:Click":
_handleClick(data);
break;
case "HomeBanner:Dismiss":
_handleDismiss(data);
break;
}
},
@ -107,6 +120,7 @@ let HomeBanner = (function () {
if (Object.keys(_messages).length == 1) {
Services.obs.addObserver(this, "HomeBanner:Get", false);
Services.obs.addObserver(this, "HomeBanner:Click", false);
Services.obs.addObserver(this, "HomeBanner:Dismiss", false);
// Send a message to Java, in case there's an active HomeBanner
// waiting for a response.
@ -132,6 +146,7 @@ let HomeBanner = (function () {
if (Object.keys(_messages).length == 0) {
Services.obs.removeObserver(this, "HomeBanner:Get");
Services.obs.removeObserver(this, "HomeBanner:Click");
Services.obs.removeObserver(this, "HomeBanner:Dismiss");
}
}
});

View File

@ -1042,12 +1042,14 @@ class RecursiveMakeBackend(CommonBackend):
self.backend_input_files.add(mozpath.join(obj.topsrcdir,
obj.manifest_relpath))
# Duplicate manifests may define the same file. That's OK.
for source, dest in obj.installs.items():
# Don't allow files to be defined multiple times unless it is allowed.
# We currently allow duplicates for non-test files or test files if
# the manifest is listed as a duplicate.
for source, (dest, is_test) in obj.installs.items():
try:
self._install_manifests['tests'].add_symlink(source, dest)
except ValueError:
if not obj.dupe_manifest:
if not obj.dupe_manifest and is_test:
raise
for base, pattern, dest in obj.pattern_installs:

View File

@ -391,7 +391,9 @@ class TestManifest(SandboxDerived):
'flavor',
# Maps source filename to destination filename. The destination
# path is relative from the tests root directory.
# path is relative from the tests root directory. Values are 2-tuples
# of (destpath, is_test_file) where the 2nd item is True if this
# item represents a test file (versus a support file).
'installs',
# A list of pattern matching installs to perform. Entries are

View File

@ -490,18 +490,37 @@ class TreeMetadataEmitter(LoggingMixin):
else:
full = mozpath.normpath(mozpath.join(manifest_dir,
pattern))
# Only install paths in our directory. This
# rule is somewhat arbitrary and could be lifted.
if not full.startswith(manifest_dir):
continue
obj.installs[full] = mozpath.join(out_dir, pattern)
dest_path = mozpath.join(out_dir, pattern)
# If the path resolves to a different directory
# tree, we take special behavior depending on the
# entry type.
if not full.startswith(manifest_dir):
# If it's a support file, we install the file
# into the current destination directory.
# This implementation makes installing things
# with custom prefixes impossible. If this is
# needed, we can add support for that via a
# special syntax later.
if thing == 'support-files':
dest_path = mozpath.join(out_dir,
os.path.basename(pattern))
# If it's not a support file, we ignore it.
# This preserves old behavior so things like
# head files doesn't get installed multiple
# times.
else:
continue
obj.installs[full] = (mozpath.normpath(dest_path),
False)
for test in filtered:
obj.tests.append(test)
obj.installs[mozpath.normpath(test['path'])] = \
mozpath.join(out_dir, test['relpath'])
(mozpath.join(out_dir, test['relpath']), True)
process_support_files(test)
@ -511,7 +530,7 @@ class TreeMetadataEmitter(LoggingMixin):
# We also copy the manifest into the output directory.
out_path = mozpath.join(out_dir, mozpath.basename(manifest_path))
obj.installs[path] = out_path
obj.installs[path] = (out_path, False)
# Some manifests reference files that are auto generated as
# part of the build or shouldn't be installed for some

View File

@ -0,0 +1,4 @@
[DEFAULT]
support-files = support-file.txt
[test_foo.js]

View File

@ -0,0 +1,4 @@
[DEFAULT]
support-files = support-file.txt
[test_bar.js]

View File

@ -0,0 +1,7 @@
# Any copyright is dedicated to the Public Domain.
# http://creativecommons.org/publicdomain/zero/1.0/
MOCHITEST_MANIFESTS += [
'mochitest1.ini',
'mochitest2.ini',
]

View File

@ -616,6 +616,15 @@ class TestRecursiveMakeBackend(BackendTester):
self.assertIn('JAR_MANIFEST := %s/jar.mn' % env.topsrcdir, lines)
def test_test_manifests_duplicate_support_files(self):
"""Ensure duplicate support-files in test manifests work."""
env = self._consume('test-manifests-duplicate-support-files',
RecursiveMakeBackend)
p = os.path.join(env.topobjdir, '_build_manifests', 'install', 'tests')
m = InstallManifest(p)
self.assertIn('testing/mochitest/tests/support-file.txt', m)
def test_android_eclipse(self):
env = self._consume('android_eclipse', RecursiveMakeBackend)

View File

@ -0,0 +1,4 @@
[DEFAULT]
support-files = ../support-file.txt
[test_foo.js]

View File

@ -0,0 +1,4 @@
# Any copyright is dedicated to the Public Domain.
# http://creativecommons.org/publicdomain/zero/1.0/
MOCHITEST_MANIFESTS += ['child/mochitest.ini']

View File

@ -340,32 +340,32 @@ class TestEmitterBasic(unittest.TestCase):
'a11y.ini': {
'flavor': 'a11y',
'installs': {
'a11y.ini',
'test_a11y.js',
'a11y.ini': False,
'test_a11y.js': True,
},
'pattern-installs': 1,
},
'browser.ini': {
'flavor': 'browser-chrome',
'installs': {
'browser.ini',
'test_browser.js',
'support1',
'support2',
'browser.ini': False,
'test_browser.js': True,
'support1': False,
'support2': False,
},
},
'metro.ini': {
'flavor': 'metro-chrome',
'installs': {
'metro.ini',
'test_metro.js',
'metro.ini': False,
'test_metro.js': True,
},
},
'mochitest.ini': {
'flavor': 'mochitest',
'installs': {
'mochitest.ini',
'test_mochitest.js',
'mochitest.ini': False,
'test_mochitest.js': True,
},
'external': {
'external1',
@ -375,20 +375,20 @@ class TestEmitterBasic(unittest.TestCase):
'chrome.ini': {
'flavor': 'chrome',
'installs': {
'chrome.ini',
'test_chrome.js',
'chrome.ini': False,
'test_chrome.js': True,
},
},
'xpcshell.ini': {
'flavor': 'xpcshell',
'dupe': True,
'installs': {
'xpcshell.ini',
'test_xpcshell.js',
'head1',
'head2',
'tail1',
'tail2',
'xpcshell.ini': False,
'test_xpcshell.js': True,
'head1': False,
'head2': False,
'tail1': False,
'tail2': False,
},
},
}
@ -407,9 +407,10 @@ class TestEmitterBasic(unittest.TestCase):
self.assertEqual(len(o.installs), len(m['installs']))
for path in o.installs.keys():
self.assertTrue(path.startswith(o.directory))
path = path[len(o.directory)+1:]
relpath = path[len(o.directory)+1:]
self.assertIn(path, m['installs'])
self.assertIn(relpath, m['installs'])
self.assertEqual(o.installs[path][1], m['installs'][relpath])
if 'pattern-installs' in m:
self.assertEqual(len(o.pattern_installs), m['pattern-installs'])
@ -438,6 +439,22 @@ class TestEmitterBasic(unittest.TestCase):
basenames = set(mozpath.basename(k) for k in o.installs.keys())
self.assertEqual(basenames, {'mochitest.ini', 'test_active.html'})
def test_test_manifest_parent_support_files_dir(self):
"""support-files referencing a file in a parent directory works."""
reader = self.reader('test-manifest-parent-support-files-dir')
objs = [o for o in self.read_topsrcdir(reader)
if isinstance(o, TestManifest)]
self.assertEqual(len(objs), 1)
o = objs[0]
expected = mozpath.join(o.srcdir, 'support-file.txt')
self.assertIn(expected, o.installs)
self.assertEqual(o.installs[expected],
('testing/mochitest/tests/child/support-file.txt', False))
def test_ipdl_sources(self):
reader = self.reader('ipdl_sources')
objs = self.read_topsrcdir(reader)

View File

@ -74,12 +74,10 @@ add_test(function test_bad_argument() {
add_task(function test_stringAsHex() {
do_check_eq(TEST_HEX, CommonUtils.stringAsHex(TEST_STR));
run_next_test();
});
add_task(function test_hexAsString() {
do_check_eq(TEST_STR, CommonUtils.hexAsString(TEST_HEX));
run_next_test();
});
add_task(function test_hexToBytes() {
@ -88,38 +86,32 @@ add_task(function test_hexToBytes() {
// Ensure that the decimal values of each byte are correct
do_check_true(arraysEqual(TEST_BYTES,
CommonUtils.stringToByteArray(bytes)));
run_next_test();
});
add_task(function test_bytesToHex() {
// Create a list of our character bytes from the reference int values
let bytes = CommonUtils.byteArrayToString(TEST_BYTES);
do_check_eq(TEST_HEX, CommonUtils.bytesAsHex(bytes));
run_next_test();
});
add_task(function test_stringToBytes() {
do_check_true(arraysEqual(TEST_BYTES,
CommonUtils.stringToByteArray(CommonUtils.stringToBytes(TEST_STR))));
run_next_test();
});
add_task(function test_stringRoundTrip() {
do_check_eq(TEST_STR,
CommonUtils.hexAsString(CommonUtils.stringAsHex(TEST_STR)));
run_next_test();
});
add_task(function test_hexRoundTrip() {
do_check_eq(TEST_HEX,
CommonUtils.stringAsHex(CommonUtils.hexAsString(TEST_HEX)));
run_next_test();
});
add_task(function test_byteArrayRoundTrip() {
do_check_true(arraysEqual(TEST_BYTES,
CommonUtils.stringToByteArray(CommonUtils.byteArrayToString(TEST_BYTES))));
run_next_test();
});
// turn formatted test vectors into normal hex strings

View File

@ -91,8 +91,6 @@ add_task(function test_pbkdf2_hmac_sha1() {
for (let v of vectors) {
do_check_eq(v.DK, b2h(pbkdf2(v.P, v.S, v.c, v.dkLen)));
}
run_next_test();
});
// I can't find any normative ietf test vectors for pbkdf2 hmac-sha256.
@ -156,8 +154,6 @@ add_task(function test_pbkdf2_hmac_sha256() {
do_check_eq(v.DK,
b2h(pbkdf2(v.P, v.S, v.c, v.dkLen, Ci.nsICryptoHMAC.SHA256, 32)));
}
run_next_test();
});
// turn formatted test vectors into normal hex strings

View File

@ -485,16 +485,12 @@ add_task(function test_resend_email_not_signed_in() {
} catch(err) {
do_check_eq(err.message,
"Cannot resend verification email; no signed-in user");
do_test_finished();
run_next_test();
return;
}
do_throw("Should not be able to resend email when nobody is signed in");
});
add_task(function test_resend_email() {
do_test_pending();
add_test(function test_resend_email() {
let fxa = new MockFxAccounts();
let alice = getTestUser("alice");
@ -528,7 +524,6 @@ add_task(function test_resend_email() {
// Ok abort polling before we go on to the next test
fxa.internal.abortExistingFlow();
do_test_finished();
run_next_test();
});
});

View File

@ -64,8 +64,6 @@ add_task(function test_onepw_setup_credentials() {
let unwrapKey = hkdf(quickStretchedPW, hkdfSalt, unwrapKeyInfo, hkdfLen);
do_check_eq(b2h(unwrapKey), "8ff58975be391338e4ec5d7138b5ed7b65c7d1bfd1f3a4f93e05aa47d5b72be9");
run_next_test();
});
add_task(function test_client_stretch_kdf() {
@ -103,8 +101,6 @@ add_task(function test_client_stretch_kdf() {
do_check_eq(expected.authPW, b2h(results.authPW),
"authPW is wrong");
run_next_test();
});
// End of tests

View File

@ -1375,8 +1375,15 @@ function add_task(func) {
*/
let _gRunningTest = null;
let _gTestIndex = 0; // The index of the currently running test.
let _gTaskRunning = false;
function run_next_test()
{
if (_gTaskRunning) {
throw new Error("run_next_test() called from an add_task() test function. " +
"run_next_test() should not be called from inside add_task() " +
"under any circumstances!");
}
function _run_next_test()
{
if (_gTestIndex < _gTests.length) {
@ -1386,8 +1393,11 @@ function run_next_test()
do_test_pending(_gRunningTest.name);
if (_isTask) {
_Task.spawn(_gRunningTest)
.then(run_next_test, do_report_unexpected_exception);
_gTaskRunning = true;
_Task.spawn(_gRunningTest).then(
() => { _gTaskRunning = false; run_next_test(); },
(ex) => { _gTaskRunning = false; do_report_unexpected_exception(ex); }
);
} else {
// Exceptions do not kill asynchronous tests, so they'll time out.
try {

View File

@ -118,6 +118,16 @@ add_task(function test() {
});
'''
ADD_TASK_RUN_NEXT_TEST = '''
function run_test() { run_next_test(); }
add_task(function () {
Assert.ok(true);
run_next_test();
});
'''
ADD_TEST_THROW_STRING = '''
function run_test() {do_throw("Passing a string to do_throw")};
'''
@ -516,6 +526,19 @@ tail =
self.assertEquals(0, self.x.passCount)
self.assertEquals(1, self.x.failCount)
def testAddTaskRunNextTest(self):
"""
Calling run_next_test() from inside add_task() results in failure.
"""
self.writeFile("test_add_task_run_next_test.js",
ADD_TASK_RUN_NEXT_TEST)
self.writeManifest(["test_add_task_run_next_test.js"])
self.assertTestResult(False)
self.assertEquals(1, self.x.testCount)
self.assertEquals(0, self.x.passCount)
self.assertEquals(1, self.x.failCount)
def testMissingHeadFile(self):
"""
Ensure that missing head file results in fatal error.

View File

@ -5,7 +5,7 @@
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
Cu.import("resource://gre/modules/CrashManager.jsm", this);
let bsp = Cu.import("resource://gre/modules/CrashManager.jsm", this);
Cu.import("resource://gre/modules/Promise.jsm", this);
Cu.import("resource://gre/modules/Task.jsm", this);
Cu.import("resource://gre/modules/osfile.jsm", this);
@ -41,8 +41,6 @@ add_task(function* test_get_manager() {
yield m.createDummyDump(true);
yield m.createDummyDump(false);
run_next_test();
});
// Unsubmitted dump files on disk are detected properly.

View File

@ -51,10 +51,6 @@
oncommand="document.getElementById('FindToolbar').onFindAgainCommand(false);"/>
<command id="cmd_findPrevious"
oncommand="document.getElementById('FindToolbar').onFindAgainCommand(true);"/>
#ifdef XP_MACOSX
<command id="cmd_findSelection"
oncommand="document.getElementById('FindToolbar').onFindSelectionCommand();"/>
#endif
<command id="cmd_reload" oncommand="ViewSourceReload();"/>
<command id="cmd_goToLine" oncommand="ViewSourceGoToLine();" disabled="true"/>
<command id="cmd_highlightSyntax" oncommand="highlightSyntax();"/>
@ -90,9 +86,6 @@
<key id="key_find" key="&findOnCmd.commandkey;" command="cmd_find" modifiers="accel"/>
<key id="key_findAgain" key="&findAgainCmd.commandkey;" command="cmd_findAgain" modifiers="accel"/>
<key id="key_findPrevious" key="&findAgainCmd.commandkey;" command="cmd_findPrevious" modifiers="accel,shift"/>
#ifdef XP_MACOSX
<key id="key_findSelection" key="&findSelectionCmd.commandkey;" command="cmd_findSelection" modifiers="accel"/>
#endif
<key keycode="&findAgainCmd.commandkey2;" command="cmd_findAgain"/>
<key keycode="&findAgainCmd.commandkey2;" command="cmd_findPrevious" modifiers="shift"/>

View File

@ -2,11 +2,6 @@
# 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/.
MOCHITEST_CHROME_FILES := \
../widgets/popup_shared.js \
../widgets/tree_shared.js \
$(SHULL)
# test_panel_focus.xul won't work if the Full Keyboard Access preference is set to
# textboxes and lists only, so skip this test on Mac
ifneq (cocoa,$(MOZ_WIDGET_TOOLKIT))

View File

@ -58,7 +58,6 @@
// Simulate typical input
textbox.focus();
gFindBar.clear();
sendChar("m");
ok(gFindBar.canClear, "canClear property true after input");
let preSelection = gBrowser.contentWindow.getSelection();

View File

@ -30,9 +30,6 @@
var gFindBar = null;
var gBrowser;
var gClipboard = Cc["@mozilla.org/widget/clipboard;1"].getService("nsIClipboard");
var gHasFindClipboard = gClipboard.supportsFindClipboard();
var gStatusText;
var gXULBrowserWindow = {
QueryInterface: function(aIID) {
@ -97,8 +94,7 @@
testFindbarSelection();
testDrop();
testQuickFindLink();
if (gHasFindClipboard)
testStatusText();
testStatusText();
testQuickFindClose();
}
@ -108,12 +104,8 @@
ok(!gFindBar.hidden, "testFindbarSelection: failed to open findbar: " + aTestName);
ok(document.commandDispatcher.focusedElement == gFindBar._findField.inputField,
"testFindbarSelection: find field is not focused: " + aTestName);
if (gHasFindClipboard) {
ok(gFindBar._findField.value == aExpSelection,
"Incorrect selection in testFindbarSelection: " + aTestName +
". Selection: " + gFindBar._findField.value);
}
testClipboardSearchString(aExpSelection);
ok(gFindBar._findField.value == aExpSelection,
"Incorrect selection in testFindbarSelection: " + aTestName + ". Selection: " + gFindBar._findField.value);
// Clear the value, close the findbar
gFindBar._findField.value = "";
@ -193,14 +185,12 @@
enterStringIntoFindField(searchStr);
ok(gBrowser.contentWindow.getSelection().toString().toLowerCase() == searchStr,
"testNormalFind: failed to find '" + searchStr + "'");
testClipboardSearchString(gBrowser.contentWindow.getSelection().toString());
if (!matchCaseCheckbox.hidden) {
matchCaseCheckbox.click();
enterStringIntoFindField("t");
ok(gBrowser.contentWindow.getSelection() != searchStr,
"testNormalFind: Case-sensitivy is broken '" + searchStr + "'");
testClipboardSearchString(gBrowser.contentWindow.getSelection().toString());
matchCaseCheckbox.click();
}
}
@ -238,7 +228,6 @@
ok(gBrowser.contentWindow.getSelection().toString().toLowerCase() != searchStr,
"testNormalFindWithComposition: text shouldn't be found during composition");
testClipboardSearchString(gBrowser.contentWindow.getSelection().toString());
synthesizeText(
{ "composition":
@ -254,7 +243,6 @@
ok(gBrowser.contentWindow.getSelection().toString().toLowerCase() == searchStr,
"testNormalFindWithComposition: text should be found after committing composition");
testClipboardSearchString(gBrowser.contentWindow.getSelection().toString());
if (clicked) {
matchCaseCheckbox.click();
@ -314,7 +302,6 @@
enterStringIntoFindField(searchStr);
ok(gBrowser.contentWindow.getSelection() == searchStr,
"testQuickFindLink: failed to find sample link");
testClipboardSearchString(searchStr);
}
function testQuickFindText() {
@ -332,18 +319,6 @@
enterStringIntoFindField(SEARCH_TEXT);
ok(gBrowser.contentWindow.getSelection() == SEARCH_TEXT,
"testQuickFindText: failed to find '" + SEARCH_TEXT + "'");
testClipboardSearchString(SEARCH_TEXT);
}
function testClipboardSearchString(aExpected) {
if (!gHasFindClipboard)
return;
if (!aExpected)
aExpected = "";
var searchStr = gFindBar.browser.finder.clipboardSearchString;
ok(searchStr == aExpected, "testClipboardSearchString: search string not " +
"set to '" + aExpected + "', instead found '" + searchStr + "'");
}
]]></script>

View File

@ -110,13 +110,6 @@
findbar.browser.finder.enableSelection();
]]></handler>
#ifdef XP_MACOSX
<handler event="focus"><![CDATA[
let findbar = this.findbar;
findbar._onFindFieldFocus();
]]></handler>
#endif
<handler event="compositionstart"><![CDATA[
// Don't close the find toolbar while IME is composing.
let findbar = this.findbar;
@ -1052,17 +1045,8 @@
userWantsPrefill =
prefsvc.getBoolPref("accessibility.typeaheadfind.prefillwithselection");
let initialString = null;
if (this.prefillWithSelection && userWantsPrefill)
initialString = this._getInitialSelection();
#ifdef XP_MACOSX
if (!initialString) {
let clipboardSearchString = this.browser.finder.clipboardSearchString;
if (clipboardSearchString)
initialString = clipboardSearchString;
}
#endif
let initialString = (this.prefillWithSelection && userWantsPrefill) ?
this._getInitialSelection() : null;
if (initialString)
this._findField.value = initialString;
@ -1118,32 +1102,6 @@
]]></body>
</method>
#ifdef XP_MACOSX
<!--
- Fetches the currently selected text and sets that as the text to search
- next. This is a MacOS specific feature.
-->
<method name="onFindSelectionCommand">
<body><![CDATA[
let searchString = this.browser.finder.setSearchStringToSelection();
if (searchString)
this._findField.value = searchString;
]]></body>
</method>
<method name="_onFindFieldFocus">
<body><![CDATA[
let clipboardSearchString = this._browser.finder.clipboardSearchString;
if (clipboardSearchString && this._findField.value != clipboardSearchString) {
this._findField.value = clipboardSearchString;
// Changing the search string makes the previous status invalid, so
// we better clear it here.
this._updateStatusUI();
}
]]></body>
</method>
#endif
<!--
- This handles all the result changes for both
- type-ahead-find and highlighting.
@ -1161,8 +1119,6 @@
<method name="onFindResult">
<parameter name="aData"/>
<body><![CDATA[
if (this._findField.value != this.browser.finder.searchString)
this._findField.value = this.browser.finder.searchString;
this._updateStatusUI(aData.result, aData.findBackwards);
this._updateStatusUIBar(aData.linkURL);

View File

@ -436,7 +436,10 @@ function ThreadActor(aHooks, aGlobal)
this._state = "detached";
this._frameActors = [];
this._hooks = aHooks;
this.global = aGlobal;
this.global = this.globalSafe = aGlobal;
if (aGlobal && aGlobal.wrappedJSObject) {
this.global = aGlobal.wrappedJSObject;
}
// A map of actorID -> actor for breakpoints created and managed by the server.
this._hiddenBreakpoints = new Map();
@ -1763,7 +1766,7 @@ ThreadActor.prototype = {
// Clear DOM event breakpoints.
// XPCShell tests don't use actual DOM windows for globals and cause
// removeListenerForAllEvents to throw.
if (this.global && !this.global.toString().contains("Sandbox")) {
if (this.globalSafe && !this.globalSafe.toString().contains("Sandbox")) {
let els = Cc["@mozilla.org/eventlistenerservice;1"]
.getService(Ci.nsIEventListenerService);
els.removeListenerForAllEvents(this.global, this._allEventsListener, true);

View File

@ -655,7 +655,7 @@ BrowserTabActor.prototype = {
this._contextPool = new ActorPool(this.conn);
this.conn.addActorPool(this._contextPool);
this.threadActor = new ThreadActor(this, this.window.wrappedJSObject);
this.threadActor = new ThreadActor(this, this.window);
this._contextPool.addActor(this.threadActor);
},

View File

@ -57,6 +57,7 @@ function TestTabActor(aConnection, aGlobal)
{
this.conn = aConnection;
this._global = aGlobal;
this._global.wrappedJSObject = aGlobal;
this._threadActor = new ThreadActor(this, this._global);
this.conn.addActor(this._threadActor);
this._attached = false;

View File

@ -113,5 +113,6 @@ function createRootActor()
DebuggerServer.addTestGlobal = function addTestGlobal(aGlobal)
{
aGlobal.wrappedJSObject = aGlobal;
gTestGlobals.push(aGlobal);
}

View File

@ -71,7 +71,6 @@ you can use these alternative items. Otherwise, their values should be empty. -
<!ENTITY findAgainCmd.accesskey "g">
<!ENTITY findAgainCmd.commandkey "g">
<!ENTITY findAgainCmd.commandkey2 "VK_F3">
<!ENTITY findSelectionCmd.commandkey "e">
<!ENTITY backCmd.label "Back">
<!ENTITY backCmd.accesskey "B">

Some files were not shown because too many files have changed in this diff Show More